Skip to content

Instantly share code, notes, and snippets.

@Slakinov
Last active August 11, 2023 00:49
Show Gist options
  • Save Slakinov/c218809022f096d53240c08245cf60f3 to your computer and use it in GitHub Desktop.
Save Slakinov/c218809022f096d53240c08245cf60f3 to your computer and use it in GitHub Desktop.
Garbage text animation transition for Svelte as seen on MusicForProgramming.net
export default function(
node,
{
duration = 1000,
delay = 0,
reverse = false,
absolute = false,
pointerEvents = true,
}
) {
// recursively find text nodes
let textNodes = findTextNodes(node);
let nodeLengths = textNodes.map(n => n.nodeValue.length);
let fullText = textNodes.map(n => n.nodeValue).join('');
let blankText = fullText.split(' ').map(e => {
let w = '';
for(let c = 0; c < e.length; c++) { w+=' ' } // <-- unicode non-breaking space character
return w;
}).join(' ');
let bufferText = ''+blankText;
let garbageSpread = ~~(fullText.length * (reverse ? 0.25 : 1.5));
let garbageDensity = reverse ? 20 : 20;
let garbageOpacity = reverse ? 0.1 : 0.8;
let mult = reverse ? -1 : 1;
let glitchiness = 0.5;
// prevent content being shoved down the page when 2 transitions overlap
if(absolute) {
node.style.position = 'absolute';
node.style.top = '0';
}
// disable clicks during transtition
if(!pointerEvents) {
node.style.pointerEvents = 'none';
}
// duration = ~~(fullText.length * 2); // fixed speed
return {
duration,
delay,
tick: t => {
t = easeInOutSine(t);
t = Math.pow(t, 2);
if(reverse) t = 1-t;
let progress = ~~(fullText.length * Math.abs(t*mult));
let garbageWidth = ~~((0.5 - Math.abs(t-0.5)) * 2 * garbageSpread);
let output;
if(reverse) {
// fill with blank text up to progress minus garbage region
output = blankText.slice(0, Math.max(progress-1-garbageWidth, 0));
} else {
// fill with original text up to progress
output = fullText.slice(0, progress);
}
if(Math.random() < glitchiness && t < 1 && t != 0) {
// garbageify non-space characters beyond the extent of progress
for(let g = 0; g < garbageDensity; g++) {
let taper = g / garbageDensity;
// let pos = reverse ? progress + ~~(Math.random()*garbageSpread*taper*mult) : progress + ~~((1-Math.random())*garbageSpread*taper);
let pos = progress + ~~((1-Math.random())*garbageSpread*taper);
if(bufferText[pos] != ' ') {
if(Math.random() > garbageOpacity) {
// occasionally add an original character
bufferText = setCharAt(bufferText, pos, fullText[pos]);
} else {
bufferText = setCharAt(bufferText, pos, garbage(reverse));
}
}
}
}
if(reverse) {
// add garbage region, fill with original text
output += bufferText.slice(Math.max(progress-1-garbageWidth, 0), Math.max(progress-1, 0));
output += fullText.slice(Math.max(progress-1, 0));
} else {
// add garbage region, fill with black text
output += bufferText.slice(progress, progress+garbageWidth);
output += blankText.slice(progress+garbageWidth);
}
// fill up text nodes with output
let pointer = 0;
for(let n = 0; n < textNodes.length; n++) {
textNodes[n].nodeValue = output.slice(pointer, pointer+nodeLengths[n]);
pointer += nodeLengths[n];
}
}
};
}
function findTextNodes(root) {
let candidates = [];
if(root.childNodes.length > 0) {
root.childNodes.forEach(n => {
if(n.nodeType == Node.TEXT_NODE) {
if(n.nodeValue != ' ') {
n.nodeValue = n.nodeValue.replace(/(\n|\r|\t)/gm, "");
candidates.push(n);
}
} else {
// recursion
candidates.push(...findTextNodes(n));
}
});
}
return candidates;
}
const junk = '—~±§|[].+$^@*()•x%!?#';
const reverseJunk = 'x';
function garbage(reverse) {
if(reverse) {
return reverseJunk[~~(Math.random() * (reverseJunk.length))];
} else {
return junk[~~(Math.random() * (junk.length))];
}
}
function setCharAt(str, index, chr) {
if(index > str.length-1 || index < 0) return str;
return str.substring(0,index) + chr + str.substring(index+1);
}
function easeInOutSine(x) {
return -(Math.cos(Math.PI * x) - 1) / 2;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment