Last active
August 11, 2023 00:49
-
-
Save Slakinov/c218809022f096d53240c08245cf60f3 to your computer and use it in GitHub Desktop.
Garbage text animation transition for Svelte as seen on MusicForProgramming.net
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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