@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; }