From 31f3a69a1debeb7e39ced092a3ea8ca8af0de74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Dani=C3=ABl=20Maat?= Date: Thu, 23 Jun 2022 23:46:41 +0100 Subject: [PATCH] Add CSS-animation based typing animation --- src/index.html | 13 +--- src/index.ts | 119 ----------------------------------- src/lib/js/main.ts | 3 - src/lib/scss/_typed.scss | 130 +++++++++++++++++++++++++++++++++++++++ src/lib/scss/main.scss | 11 ++++ 5 files changed, 142 insertions(+), 134 deletions(-) delete mode 100644 src/index.ts delete mode 100644 src/lib/js/main.ts create mode 100644 src/lib/scss/_typed.scss diff --git a/src/index.html b/src/index.html index 7709d42..7716bea 100644 --- a/src/index.html +++ b/src/index.html @@ -1,18 +1,7 @@ - - - -

- $ Welcome to tlater.net! + $ 


diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 0a32b9a..0000000 --- a/src/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -import jQuery from "jquery"; - -// Helpers - -/** - * "Types" out a DOM element, emulating the way a human might. - */ -class Typer { - private element: JQuery; - private text: string; - private cursor: boolean; - private typed: number; - private min: number; - private max: number; - private blink_tick: number; - private blink_timeout: number; - private end?: number; - - /** - * Create the typer. - * @param {HTMLElement} element - The element to type. - * @param {number} blink - The time between cursor blinks. - * @param {number} blink_timeout - How long the cursor should keep - * blinking for after the text - * finishes typing. - */ - constructor(element: JQuery, blink: number, blink_timeout: number) { - // Retrieve the current content and wipe it. We also make the - // element visible if it was hidden. - this.element = element; - this.text = this.element.html(); - this.element.html(""); - this.element.css("visibility", "visible"); - - this.cursor = false; - this.typed = 0; - - this.min = 20; - this.max = 70; - this.blink_tick = blink; - this.blink_timeout = blink_timeout; - - this.end = null; - } - - /** - * Start typing. - */ - type() { - this._type(); - this._blink(); - } - - /** - * Draw the current text line, i.e., anything that has been typed - * so far, and a cursor if it is currently supposed to be on. - * @private - */ - _draw() { - let text = this.text.slice(0, this.typed); - - if (this.cursor) { - text += "\u2588"; - } - - window.requestAnimationFrame(() => this.element.html(text)); - } - - /** - * Type the next character, and prepare to draw the next one. If - * no new characters are to be drawn, set the end timestamp. - * @private - */ - _type() { - this.typed += 1; - this._draw(); - - if (this.typed != this.text.length) - setTimeout(this._type.bind(this), this._type_tick()); - else { - this.end = Date.now(); - } - } - - /** - * Make the cursor change blink status, and prepare for the next - * blink. - * @private - */ - _blink() { - this.cursor = !this.cursor; - this._draw(); - - // As long as we are typing, keep blinking - if (this.typed != this.text.length) - setTimeout(this._blink.bind(this), this.blink_tick); - // Once typing ends, keep going for a little bit - else if (Date.now() - this.end < this.blink_timeout) - setTimeout(this._blink.bind(this), this.blink_tick); - // Make sure we get rid of the cursor in the end - else { - this.cursor = true; - setTimeout(this._blink.bind(this), this.blink_tick); - } - } - - /** - * Calculate a "human" time for the next character to type. - * @private - */ - _type_tick() { - return Math.round(Math.random() * this.max) + this.min; - } -} - -jQuery(($) => { - const typer = new Typer($(".head-line .typed").first(), 500, 3000); - typer.type(); -}); diff --git a/src/lib/js/main.ts b/src/lib/js/main.ts deleted file mode 100644 index a9894f5..0000000 --- a/src/lib/js/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -import jQuery from "jquery"; - -jQuery(($) => $("html").removeClass("no-js")); diff --git a/src/lib/scss/_typed.scss b/src/lib/scss/_typed.scss new file mode 100644 index 0000000..1082e72 --- /dev/null +++ b/src/lib/scss/_typed.scss @@ -0,0 +1,130 @@ +@use "sass:math"; +@use "sass:list"; + +/// Animate a blinking cursor. +@mixin cursor($duration) { + $name: cursor-09d03260130069771b6ddc1cb415f39fdd27ddfab7b01ba91273398c2d245ae4; + // The number of times we need to blink is = the number of full + // seconds (500ms * 2) that fit in the total duration, rounded up, + // and doubled. + $iterations: math.ceil(math.div($duration, 1s)) * 2; + + animation: $name ease-in-out 500ms $iterations alternate; + content: " "; + + @keyframes #{$name} { + from { + content: " "; + } + + to { + content: "█"; + } + } +} + +/// Animate a piece of text as if it was being typed by a human. +@mixin typed($text, $duration) { + // We don't want a linearly typed set of text, which makes this + // singificantly more complex. + // + // CSS animations normally do not permit per-frame changes in + // duration (since the total animation time is fixed). This means we + // need to create multiple animations, and delay them so that they + // happen in the time sequence we want. + // + // We generate the raw values with _generate-animations, and then + // split up the result into the animation API. + $frames: str-length($text); + $animations: _generate-animations($frames, 1.2s); + + animation-name: _unzip($animations, 1); + animation-delay: _unzip($animations, 3); + animation-fill-mode: forwards; + content: ""; + + // We need to type each character in separate animations, see above + // comment. + @each $name, $character in $animations { + @keyframes #{$name} { + from { + content: str-slice($text, 0, $character); + } + + to { + content: str-slice($text, 0, $character + 1); + } + } + } +} + +/// Unzip a nested set of lists, taking the nth value of each sublist. +@function _unzip($lists, $i) { + $out: (); + $sep: comma; + @each $sublist in $lists { + $out: list.append($out, list.nth($sublist, $i), $sep); + } + @return $out; +} + +/// Compute the sum of all numbers in a list. +@function _sum($list) { + $out: 0; + @each $val in $list { + $out: $out + $val; + } + @return $out; +} + +/// Produce a list from a shorter list by repeating it up until size +/// $length. +@function _round-robin($base, $length) { + $out: (); + $sep: list.separator($out); + @for $i from 0 through $length { + $out: list.append($out, list.nth($base, $i % list.length($base) + 1)); + } + @return $out; +} + +/// Generate the actual animation values. +/// +/// This generates a nested list as: +/// +/// (keyframe-name, index, start time) +/// +/// The duration of each frame is taken from the internal $delays in a +/// round robin fashion, to give some amount of human-like variance to +/// the duration of each frame. +/// +/// Start time is set to the time at which the frame should start to +/// achieve the desired frame-by-frame duration. +@function _generate-animations($number, $total_duration) { + $id: d66fa0449c0b4d4ca287f8c96428af928b2987b4d88b72b7d60152d9a55d9f29; + $out: (); + $sep: list.separator($out); + + // A set of "human-like" delays for each typed character. In + // practice, my typing seems to be about 20-70ms, but it looks a bit + // nicer to increase all typing by 20ms to make the effect more + // noticeable. + // + // Numbers generated once with a random number generator, rather + // than using `math.random()`, since they end up in CSS verbatim, + // and the build would be non-reproducible if we didn't do it this + // way. Using `math.random() wouldn't change this dynamically each + // time the page loads anyway, so we don't really lose anything by + // pre-generating these numbers. + $delays: 69ms, 83ms, 49ms, 48ms, 52ms, 59ms, 40ms, 71ms, 80ms, 67ms; + + @for $animation from 0 through $number { + $out: list.append($out, ( + type-#{$id}-#{$animation}, + $animation, + _sum(_round_robin($delays, $animation)) + ), $sep); + } + + @return $out; +} diff --git a/src/lib/scss/main.scss b/src/lib/scss/main.scss index 889905e..3b0b16f 100644 --- a/src/lib/scss/main.scss +++ b/src/lib/scss/main.scss @@ -1,6 +1,17 @@ @import "./_custom-bulma"; +@import "./_typed"; html, body { height: 100%; width: 100%; } + +#typed-welcome { + &::before { + @include typed("Welcome to tlater.net!", 1.2s); + } + + &::after { + @include cursor(6s); + } +}