Skip to content

Instantly share code, notes, and snippets.

@fabiogiolito
Created February 23, 2023 14:25
Show Gist options
  • Save fabiogiolito/b5aac76c4d1ae0eb23012be7befcd73f to your computer and use it in GitHub Desktop.
Save fabiogiolito/b5aac76c4d1ae0eb23012be7befcd73f to your computer and use it in GitHub Desktop.
A Svelte component that types text like ChatGPT.
<script>
import { onMount } from "svelte";
// Text to display
export let text = "";
// <Typerwiter paused … /> to start paused
export let paused = false;
// <Typerwiter cursor … /> to show blinking cursor
export let cursor = false;
// <Typerwiter debug … /> to show debug buttons and data
export let debug = false;
// More options
export let delayStart = 1000; // Delay in ms before words start appearing
export let delayRange = [250, 1000]; // Delay in ms between each update [min, max]
export let wordRange = [3, 8]; // How many words at each update [min, max]
// Local state
let timeout = null; // Store timeout so we can pause/resume/end
let remainingWords = text.trim().split(" "); // Split text in words
let visibleText = ""; // We'll add words here to be displayed
// ==============================
// Functions
// ------------------------------
// Start showing words
function handlePlay() {
paused = false; // unpause
// Check for initial delay
if (delayStart) {
timeout = setTimeout(handlePlay, delayStart);
delayStart = 0;
return
}
// No initial delay, good to go
// Get some random duration based on delayRange
const delay = Math.floor(Math.random() * (delayRange[1] - delayRange[0] + 1)) + delayRange[0];
// Set timeout to add a new block of words
timeout = setTimeout(() => {
// Reached the end
if (!remainingWords.length) {
handlePause();
return;
}
// Get some random number of characters in given range
const length = Math.floor(Math.random() * (wordRange[1] - wordRange[0] + 1)) + wordRange[0];
// Append that number of words to visible text
visibleText += remainingWords.slice(0, length).join(" ") + " ";
// Remove that number of words from remaining words
remainingWords = remainingWords.slice(length);
handlePlay();
}, delay);
}
// ------------------------------
// Stop showing words
function handlePause() {
paused = true;
clearTimeout(timeout);
}
// ------------------------------
// Show entire text
function handleFinish() {
handlePause();
remainingWords = "";
visibleText = text;
}
// ------------------------------
// Start over
function handleRestart() {
handlePause();
remainingWords = text.trim().split(" "); // Split text in words
visibleText = ""; // We'll add words here to be displayed
handlePlay();
}
// ------------------------------
// Component mounted
onMount(() => {
// Start on mount unless paused
if (!paused) handlePlay();
});
</script>
{#if debug}
<div>
<button disabled={paused == true || !remainingWords.length} on:click={handlePause}>Pause</button>
<button disabled={paused == false || !remainingWords.length} on:click={handlePlay}>Resume</button>
<button disabled={!remainingWords.length} on:click={handleFinish}>Finish</button>
<button on:click={handleRestart}>Restart</button>
<p>Remaining words: {remainingWords.length}</p>
</div>
{/if}
<p class:with-cursor={cursor && !paused && remainingWords.length}>{visibleText}</p>
<style>
div {
border-bottom: 1px solid;
margin-bottom: 1em;
padding-bottom: 1em;
}
button {
border: 1px solid;
padding: 0.25em 0.5em;
border-radius: 0.25em;
}
button:disabled {
opacity: 0.5;
}
p { white-space: pre-wrap;}
.with-cursor:after {
content: '';
display: inline-block;
vertical-align: middle;
position: relative;
top: -2px;
width: 0.5em;
height: 1.2em;
margin: -1.2em 0;
background-color: currentColor;
opacity: 0.5;
animation: blink-animation 1s steps(5, start) infinite;
}
@keyframes blink-animation {
to { visibility: hidden; }
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment