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