Skip to content

Commit 33332eb

Browse files
committed
feat: 'encrypt' keyword scrambles hero text (inverse of 'decrypt')
The hero animation now has two modes driven by separate events: - rnp:replay-hero ('decrypt'): garbage settles L→R into readable text - rnp:encrypt-hero ('encrypt'): readable text scrambles L→R and stays mangled — type 'decrypt' (or refresh) to restore One-way semantics match the metaphor: encryption without the key leaves ciphertext opaque. Wiring: - HeroDecrypt.vue: new runEncrypt() does the scramble wave and stops at the final mangled state (no auto-decrypt cycle) - FingerprintPlayground.vue: input watcher dispatches rnp:replay-hero for /decrypt/ or rnp:encrypt-hero for /encrypt/. Decrypt wins if both appear. Substring match so either fires appended to the default seed, standalone, or inside a longer word. Also loosened the decrypt regex from /\bdecrypt\b/ to /decrypt/ so it triggers in more contexts. - EasterEggs.vue: global keypress (anywhere outside inputs) now branches on endsWith('decrypt') vs endsWith('encrypt'), with distinct toasts ('Replaying decryption…' / 'Encrypting…') rnp.help() / help() updated to list decrypt and encrypt as separate entries with their distinct effects.
1 parent 85868b0 commit 33332eb

3 files changed

Lines changed: 71 additions & 29 deletions

File tree

src/components/vue/EasterEggs.vue

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ const onKey = (e: KeyboardEvent) => {
246246
word = '';
247247
window.dispatchEvent(new CustomEvent('rnp:replay-hero'));
248248
toast('Replaying decryption…');
249+
} else if (word.endsWith('encrypt')) {
250+
word = '';
251+
window.dispatchEvent(new CustomEvent('rnp:encrypt-hero'));
252+
toast('Encrypting…');
249253
} else if (word.endsWith('rnp') || word.endsWith('pgp')) {
250254
word = '';
251255
hexRain();
@@ -370,27 +374,29 @@ const onDaveReq = () => playDaveScene();
370374
371375
/** `help()` in the console — list every egg. */
372376
const HELP_TEXT = `Type anywhere on the page (not in inputs):
373-
rnp · pgp → oracle rain (philosophy)
374-
decrypt → replay hero decryption
377+
rnp · pgp → oracle rain (philosophy)
378+
decrypt → replay hero decryption (garbage → text)
379+
encrypt → scramble the hero text (text → garbage → text)
375380
376381
In the fingerprint box (home page):
377-
decrypt → also replays hero decryption
378-
42 → DON'T PANIC
379-
dave → open the pod bay doors (screenwide)
380-
dh → Diffie–Hellman paint exchange
381-
enigma → ³-rotor cipher, live stepping
382-
hal · 9000 → HAL 9000 (screenwide)
383-
librepgp → the open OpenPGP spec
384-
matrix → wake up, Neo
385-
openpgp · 4880 → ASCII-armored message
386-
otp → one-time pad (XOR of your input)
387-
phil → PGP was a munition (1993–1996)
388-
pqc · quantum → harvest now, decrypt later
389-
privacy → cypherpunk's manifesto
390-
ribose → who's behind RNP
391-
snowden · nsa → classified
392-
thunderbird → powered by Thunderbird's wings
393-
rnp · pgp → also triggers oracle rain
382+
decrypt → same — replay hero decryption
383+
encrypt → same — scramble the hero text
384+
42 → DON'T PANIC
385+
dave → open the pod bay doors (screenwide)
386+
dh → Diffie–Hellman paint exchange
387+
enigma → ³-rotor cipher, live stepping
388+
hal · 9000 → HAL 9000 (screenwide)
389+
librepgp → the open OpenPGP spec
390+
matrix → wake up, Neo
391+
openpgp · 4880 → ASCII-armored message
392+
otp → one-time pad (XOR of your input)
393+
phil → PGP was a munition (1993–1996)
394+
pqc · quantum → harvest now, decrypt later
395+
privacy → cypherpunk's manifesto
396+
ribose → who's behind RNP
397+
snowden · nsa → classified
398+
thunderbird → powered by Thunderbird's wings
399+
rnp · pgp → also triggers oracle rain
394400
395401
Type help() or rnp.help() to reprint.`;
396402

src/components/vue/FingerprintPlayground.vue

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,21 @@ watch(input, (val) => {
209209
}
210210
});
211211
212-
/* Typing 'decrypt' in the input also replays the hero decryption animation. */
213-
let lastDecryptInput = '';
212+
/* Typing 'decrypt' or 'encrypt' in the input drives the hero animation:
213+
decrypt → garbage settles to readable; encrypt → readable scrambles
214+
(then auto-settles back so the page isn't stuck). Substring match so
215+
either word fires whether appended to the default seed, typed alone,
216+
or part of a longer word. Decrypt wins if both are present. */
217+
let lastHeroInput = '';
214218
watch(input, (val) => {
215-
const matches = /\bdecrypt\b/i.test(val);
216-
if (matches && val !== lastDecryptInput) {
217-
lastDecryptInput = val;
218-
window.dispatchEvent(new CustomEvent('rnp:replay-hero'));
219-
} else if (!matches) {
220-
lastDecryptInput = '';
219+
let evt: 'rnp:replay-hero' | 'rnp:encrypt-hero' | null = null;
220+
if (/decrypt/i.test(val)) evt = 'rnp:replay-hero';
221+
else if (/encrypt/i.test(val)) evt = 'rnp:encrypt-hero';
222+
if (evt && val !== lastHeroInput) {
223+
lastHeroInput = val;
224+
window.dispatchEvent(new CustomEvent(evt));
225+
} else if (!evt) {
226+
lastHeroInput = '';
221227
}
222228
});
223229

src/components/vue/HeroDecrypt.vue

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const GLYPHS = '0123456789ABCDEF$#@%&*+=~';
77
const display = ref(props.text);
88
let raf = 0;
99
10+
const isPunct = (ch: string) => ch === ' ' || ch === '.' || ch === ',';
11+
1012
const run = () => {
1113
cancelAnimationFrame(raf);
1214
const text = props.text;
@@ -21,7 +23,7 @@ const run = () => {
2123
let out = text.slice(0, settled);
2224
for (let i = settled; i < total; i++) {
2325
const ch = text[i];
24-
out += ch === ' ' || ch === '.' || ch === ',' ? ch : GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
26+
out += isPunct(ch) ? ch : GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
2527
}
2628
display.value = out;
2729
if (t < 1) raf = requestAnimationFrame(tick);
@@ -30,18 +32,46 @@ const run = () => {
3032
raf = requestAnimationFrame(tick);
3133
};
3234
35+
/** Encrypt: scramble wave sweeps L→R, leaving the text fully mangled.
36+
* Stays mangled — type 'decrypt' (or refresh) to restore. */
37+
const runEncrypt = () => {
38+
cancelAnimationFrame(raf);
39+
const text = props.text;
40+
const total = text.length;
41+
const duration = 1400;
42+
const start = performance.now();
43+
44+
const tick = (now: number) => {
45+
const t = Math.min(1, (now - start) / duration);
46+
const edge = Math.floor(total * t * t * (3 - 2 * t));
47+
let out = '';
48+
for (let i = 0; i < total; i++) {
49+
const ch = text[i];
50+
out += i < edge && !isPunct(ch)
51+
? GLYPHS[Math.floor(Math.random() * GLYPHS.length)]
52+
: ch;
53+
}
54+
display.value = out;
55+
if (t < 1) raf = requestAnimationFrame(tick);
56+
// else: final state is fully scrambled — leave it
57+
};
58+
raf = requestAnimationFrame(tick);
59+
};
60+
3361
const reducedMotion = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
3462
3563
onMounted(() => {
3664
// Passive auto-play respects reduced-motion…
3765
if (!reducedMotion()) run();
38-
// …but a user explicitly typing 'decrypt' always gets the replay.
66+
// …but a user explicitly typing 'decrypt' or 'encrypt' always gets the effect.
3967
window.addEventListener('rnp:replay-hero', run);
68+
window.addEventListener('rnp:encrypt-hero', runEncrypt);
4069
});
4170
4271
onUnmounted(() => {
4372
cancelAnimationFrame(raf);
4473
window.removeEventListener('rnp:replay-hero', run);
74+
window.removeEventListener('rnp:encrypt-hero', runEncrypt);
4575
});
4676
</script>
4777

0 commit comments

Comments
 (0)