Add CSS-animation based typing animation
This commit is contained in:
parent
0801e85839
commit
31f3a69a1d
|
@ -1,18 +1,7 @@
|
|||
<extends src="./lib/html/base.html">
|
||||
<block name="stylesheets">
|
||||
<style>
|
||||
.no-js .head-line .typed {
|
||||
visibility: visible;
|
||||
}
|
||||
.head-line .typed {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
</block>
|
||||
|
||||
<block name="content">
|
||||
<h1 class="title head-line has-text-weight-normal is-family-monospace">
|
||||
$ <span class="typed">Welcome to tlater.net!</span>
|
||||
$ <span id="typed-welcome"></span>
|
||||
</h1>
|
||||
|
||||
<hr />
|
||||
|
|
119
src/index.ts
119
src/index.ts
|
@ -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<HTMLElement>, 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();
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
import jQuery from "jquery";
|
||||
|
||||
jQuery(($) => $("html").removeClass("no-js"));
|
130
src/lib/scss/_typed.scss
Normal file
130
src/lib/scss/_typed.scss
Normal file
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue