diff --git a/src/components/vue/EasterEggs.vue b/src/components/vue/EasterEggs.vue index b676e4d..d73c48e 100644 --- a/src/components/vue/EasterEggs.vue +++ b/src/components/vue/EasterEggs.vue @@ -246,6 +246,10 @@ const onKey = (e: KeyboardEvent) => { word = ''; window.dispatchEvent(new CustomEvent('rnp:replay-hero')); toast('Replaying decryption…'); + } else if (word.endsWith('encrypt')) { + word = ''; + window.dispatchEvent(new CustomEvent('rnp:encrypt-hero')); + toast('Encrypting…'); } else if (word.endsWith('rnp') || word.endsWith('pgp')) { word = ''; hexRain(); @@ -370,27 +374,29 @@ const onDaveReq = () => playDaveScene(); /** `help()` in the console — list every egg. */ const HELP_TEXT = `Type anywhere on the page (not in inputs): - rnp · pgp → oracle rain (philosophy) - decrypt → replay hero decryption + rnp · pgp → oracle rain (philosophy) + decrypt → replay hero decryption (garbage → text) + encrypt → scramble the hero text (text → garbage → text) In the fingerprint box (home page): - decrypt → also replays hero decryption - 42 → DON'T PANIC - dave → open the pod bay doors (screenwide) - dh → Diffie–Hellman paint exchange - enigma → ³-rotor cipher, live stepping - hal · 9000 → HAL 9000 (screenwide) - librepgp → the open OpenPGP spec - matrix → wake up, Neo - openpgp · 4880 → ASCII-armored message - otp → one-time pad (XOR of your input) - phil → PGP was a munition (1993–1996) - pqc · quantum → harvest now, decrypt later - privacy → cypherpunk's manifesto - ribose → who's behind RNP - snowden · nsa → classified - thunderbird → powered by Thunderbird's wings - rnp · pgp → also triggers oracle rain + decrypt → same — replay hero decryption + encrypt → same — scramble the hero text + 42 → DON'T PANIC + dave → open the pod bay doors (screenwide) + dh → Diffie–Hellman paint exchange + enigma → ³-rotor cipher, live stepping + hal · 9000 → HAL 9000 (screenwide) + librepgp → the open OpenPGP spec + matrix → wake up, Neo + openpgp · 4880 → ASCII-armored message + otp → one-time pad (XOR of your input) + phil → PGP was a munition (1993–1996) + pqc · quantum → harvest now, decrypt later + privacy → cypherpunk's manifesto + ribose → who's behind RNP + snowden · nsa → classified + thunderbird → powered by Thunderbird's wings + rnp · pgp → also triggers oracle rain Type help() or rnp.help() to reprint.`; diff --git a/src/components/vue/FingerprintPlayground.vue b/src/components/vue/FingerprintPlayground.vue index 85fa2fb..847f07d 100644 --- a/src/components/vue/FingerprintPlayground.vue +++ b/src/components/vue/FingerprintPlayground.vue @@ -209,15 +209,21 @@ watch(input, (val) => { } }); -/* Typing 'decrypt' in the input also replays the hero decryption animation. */ -let lastDecryptInput = ''; +/* Typing 'decrypt' or 'encrypt' in the input drives the hero animation: + decrypt → garbage settles to readable; encrypt → readable scrambles + (then auto-settles back so the page isn't stuck). Substring match so + either word fires whether appended to the default seed, typed alone, + or part of a longer word. Decrypt wins if both are present. */ +let lastHeroInput = ''; watch(input, (val) => { - const matches = /\bdecrypt\b/i.test(val); - if (matches && val !== lastDecryptInput) { - lastDecryptInput = val; - window.dispatchEvent(new CustomEvent('rnp:replay-hero')); - } else if (!matches) { - lastDecryptInput = ''; + let evt: 'rnp:replay-hero' | 'rnp:encrypt-hero' | null = null; + if (/decrypt/i.test(val)) evt = 'rnp:replay-hero'; + else if (/encrypt/i.test(val)) evt = 'rnp:encrypt-hero'; + if (evt && val !== lastHeroInput) { + lastHeroInput = val; + window.dispatchEvent(new CustomEvent(evt)); + } else if (!evt) { + lastHeroInput = ''; } }); diff --git a/src/components/vue/HeroDecrypt.vue b/src/components/vue/HeroDecrypt.vue index 4a09a1d..1aab055 100644 --- a/src/components/vue/HeroDecrypt.vue +++ b/src/components/vue/HeroDecrypt.vue @@ -7,6 +7,8 @@ const GLYPHS = '0123456789ABCDEF$#@%&*+=~'; const display = ref(props.text); let raf = 0; +const isPunct = (ch: string) => ch === ' ' || ch === '.' || ch === ','; + const run = () => { cancelAnimationFrame(raf); const text = props.text; @@ -21,7 +23,7 @@ const run = () => { let out = text.slice(0, settled); for (let i = settled; i < total; i++) { const ch = text[i]; - out += ch === ' ' || ch === '.' || ch === ',' ? ch : GLYPHS[Math.floor(Math.random() * GLYPHS.length)]; + out += isPunct(ch) ? ch : GLYPHS[Math.floor(Math.random() * GLYPHS.length)]; } display.value = out; if (t < 1) raf = requestAnimationFrame(tick); @@ -30,18 +32,46 @@ const run = () => { raf = requestAnimationFrame(tick); }; +/** Encrypt: scramble wave sweeps L→R, leaving the text fully mangled. + * Stays mangled — type 'decrypt' (or refresh) to restore. */ +const runEncrypt = () => { + cancelAnimationFrame(raf); + const text = props.text; + const total = text.length; + const duration = 1400; + const start = performance.now(); + + const tick = (now: number) => { + const t = Math.min(1, (now - start) / duration); + const edge = Math.floor(total * t * t * (3 - 2 * t)); + let out = ''; + for (let i = 0; i < total; i++) { + const ch = text[i]; + out += i < edge && !isPunct(ch) + ? GLYPHS[Math.floor(Math.random() * GLYPHS.length)] + : ch; + } + display.value = out; + if (t < 1) raf = requestAnimationFrame(tick); + // else: final state is fully scrambled — leave it + }; + raf = requestAnimationFrame(tick); +}; + const reducedMotion = () => matchMedia('(prefers-reduced-motion: reduce)').matches; onMounted(() => { // Passive auto-play respects reduced-motion… if (!reducedMotion()) run(); - // …but a user explicitly typing 'decrypt' always gets the replay. + // …but a user explicitly typing 'decrypt' or 'encrypt' always gets the effect. window.addEventListener('rnp:replay-hero', run); + window.addEventListener('rnp:encrypt-hero', runEncrypt); }); onUnmounted(() => { cancelAnimationFrame(raf); window.removeEventListener('rnp:replay-hero', run); + window.removeEventListener('rnp:encrypt-hero', runEncrypt); });