@whykusanagi/corrupted-theme 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Typing Animation with Buffer Corruption
3
3
  *
4
- * Simulates typing text with "buffer corruption" - phrases flickering through
5
- * as the neural network attempts to decode corrupted data before revealing
6
- * the final stable text.
4
+ * Simulates typing text with "buffer corruption" - phrases continuously flickering
5
+ * to the right of the revealed text as the neural network decodes corrupted data,
6
+ * before finally settling on stable readable output.
7
7
  *
8
8
  * Implements Pattern 2 (Phrase Flickering / Buffer Corruption) from CORRUPTED_THEME_SPEC.md
9
9
  *
10
+ * Key design: char-advance and buffer-flicker run on **independent** timers so the
11
+ * buffer is always visible while typing is in progress, not probabilistic.
12
+ *
10
13
  * @class TypingAnimation
11
- * @version 1.0.0
14
+ * @version 1.1.0
12
15
  * @author whykusanagi
13
16
  * @license MIT
14
17
  *
@@ -16,8 +19,9 @@
16
19
  * ```javascript
17
20
  * const element = document.querySelector('.typing-text');
18
21
  * const typing = new TypingAnimation(element, {
19
- * typingSpeed: 40,
20
- * glitchChance: 0.08
22
+ * duration: 2000, // 2s total regardless of text length
23
+ * loop: true,
24
+ * loopDelay: 1500,
21
25
  * // nsfw: false (default - SFW phrases only)
22
26
  * });
23
27
  *
@@ -36,35 +40,66 @@
36
40
  * @see corruption-phrases.js - Phrase library with SFW/NSFW split
37
41
  */
38
42
 
39
- import { getRandomPhrase, getRandomPhraseByCategory } from './corruption-phrases.js';
40
-
41
43
  class TypingAnimation {
44
+ /**
45
+ * Module-scope flag: fire the glitchChance deprecation warning at most once
46
+ * per page load, regardless of how many TypingAnimation instances are created.
47
+ * @type {boolean}
48
+ */
49
+ static _warnedGlitchChance = false;
50
+
42
51
  /**
43
52
  * Creates a new TypingAnimation instance
44
53
  *
45
54
  * @param {HTMLElement} element - The DOM element to animate
46
55
  * @param {Object} [options={}] - Configuration options
47
- * @param {number} [options.typingSpeed=40] - Characters per second
48
- * @param {number} [options.glitchChance=0.08] - Probability of corruption appearing (0-1)
49
- * @param {number} [options.tickRate=33] - Update interval in milliseconds (~30fps)
56
+ * @param {number|null} [options.duration=null] - Total ms for one typing pass.
57
+ * When set, char interval = max(33, duration/length). Takes priority over typingSpeed.
58
+ * @param {number} [options.typingSpeed=12] - Chars/sec; used only when duration is null.
59
+ * @param {boolean} [options.bufferEnabled=true] - Show always-on buffer corruption phrase.
60
+ * Set false for a clean typewriter effect with no buffer.
61
+ * @param {number} [options.bufferFlickerSpeed=100] - ms between buffer phrase swaps
62
+ * (independent of char-advance rate).
63
+ * @param {boolean} [options.loop=false] - Automatically restart after loopDelay ms.
64
+ * @param {number} [options.loopDelay=1500] - ms to hold settled text before restarting.
50
65
  * @param {boolean} [options.nsfw=false] - Enable NSFW phrases (explicit opt-in required)
51
66
  * @param {Function} [options.onComplete=null] - Callback when typing completes
67
+ * @param {number} [options.glitchChance] - DEPRECATED. Ignored; buffer is always-on.
68
+ * Fires a one-time console.warn per page load. Use bufferEnabled: false to disable buffer.
52
69
  */
53
70
  constructor(element, options = {}) {
54
71
  this.element = element;
72
+
73
+ // Deprecation warning for glitchChance — fires at most once per page load
74
+ if (options.glitchChance !== undefined && !TypingAnimation._warnedGlitchChance) {
75
+ console.warn(
76
+ "TypingAnimation: 'glitchChance' is deprecated and ignored — buffer is always-on now. " +
77
+ "Use bufferEnabled: false to disable."
78
+ );
79
+ TypingAnimation._warnedGlitchChance = true;
80
+ }
81
+
55
82
  this.options = {
56
- typingSpeed: options.typingSpeed || 40, // characters per second
57
- glitchChance: options.glitchChance || 0.08, // 8% chance of buffer corruption
58
- tickRate: options.tickRate || 33, // ~30fps update rate
59
- nsfw: options.nsfw || false, // SFW by default
60
- onComplete: options.onComplete || null,
61
- ...options
83
+ duration: options.duration ?? null,
84
+ typingSpeed: options.typingSpeed ?? 12,
85
+ bufferEnabled: options.bufferEnabled ?? true,
86
+ bufferFlickerSpeed: options.bufferFlickerSpeed ?? 100,
87
+ loop: options.loop ?? false,
88
+ loopDelay: options.loopDelay ?? 1500,
89
+ nsfw: options.nsfw ?? false,
90
+ onComplete: options.onComplete ?? null,
62
91
  };
63
92
 
64
- this.content = '';
65
- this.displayedLen = 0;
66
- this.done = false;
67
- this.intervalId = null;
93
+ // Instance state
94
+ this.content = '';
95
+ this.displayedLen = 0;
96
+ this.done = false;
97
+ this.currentBufferPhrase = null;
98
+
99
+ // Three independent timer IDs (replacing the old single intervalId)
100
+ this.charIntervalId = null;
101
+ this.flickerIntervalId = null;
102
+ this.loopTimeoutId = null;
68
103
  }
69
104
 
70
105
  /**
@@ -181,60 +216,120 @@ class TypingAnimation {
181
216
  '▲', '▼', '◄', '►', '◊', '○', '●', '◘'
182
217
  ];
183
218
 
219
+ // ---------------------------------------------------------------------------
220
+ // Public API
221
+ // ---------------------------------------------------------------------------
222
+
184
223
  /**
185
224
  * Start typing animation with buffer corruption
186
225
  *
226
+ * Clears any running timers, resets state, then kicks off two independent
227
+ * intervals: one for char-advance, one for buffer-phrase flicker.
228
+ *
187
229
  * @param {string} content - The final text to reveal
188
230
  * @public
189
231
  */
190
232
  start(content) {
191
- this.content = content;
233
+ this._clearTimers();
234
+
235
+ this.content = content;
192
236
  this.displayedLen = 0;
193
- this.done = false;
237
+ this.done = false;
194
238
 
195
- if (this.intervalId) {
196
- clearInterval(this.intervalId);
239
+ // Seed initial buffer phrase so first render() is not empty
240
+ if (this.options.bufferEnabled) {
241
+ this.currentBufferPhrase = this.getRandomCorruption();
197
242
  }
198
243
 
199
- this.intervalId = setInterval(() => this.tick(), this.options.tickRate);
244
+ // Render first frame immediately (no 1-tick blank flash)
245
+ this.render();
246
+
247
+ // Char-advance timer
248
+ const charInterval = this._computeCharInterval();
249
+ this.charIntervalId = setInterval(() => this.tick(), charInterval);
250
+
251
+ // Buffer-flicker timer (independent of char advance)
252
+ if (this.options.bufferEnabled) {
253
+ this.flickerIntervalId = setInterval(() => {
254
+ this.currentBufferPhrase = this.getRandomCorruption();
255
+ this.render();
256
+ }, this.options.bufferFlickerSpeed);
257
+ }
200
258
  }
201
259
 
202
260
  /**
203
- * Stop the typing animation
261
+ * Stop the typing animation, clearing all timers.
262
+ * Leaves current visual state intact.
204
263
  * @public
205
264
  */
206
265
  stop() {
207
- if (this.intervalId) {
208
- clearInterval(this.intervalId);
209
- this.intervalId = null;
210
- }
266
+ this._clearTimers();
211
267
  this.done = true;
212
268
  }
213
269
 
214
270
  /**
215
- * Advance the typing animation by one tick
216
- * @private
271
+ * Fully tear down this instance: stop all timers, clear the element,
272
+ * and release the DOM reference. After calling destroy(), this instance
273
+ * is not reusable — create a new TypingAnimation if you need one.
274
+ * @public
217
275
  */
218
- tick() {
219
- if (this.isDone()) {
220
- this.stop();
221
- if (this.options.onComplete) {
222
- this.options.onComplete();
223
- }
224
- return;
276
+ destroy() {
277
+ this._clearTimers();
278
+ this.done = true;
279
+ this.currentBufferPhrase = null;
280
+ if (this.element) {
281
+ this.element.textContent = '';
282
+ this.element = null;
225
283
  }
284
+ }
285
+
286
+ /**
287
+ * Restart the animation from the beginning using the same content.
288
+ * @public
289
+ */
290
+ restart() {
291
+ this.start(this.content);
292
+ }
293
+
294
+ /**
295
+ * Stop animation and immediately show final text (no animation).
296
+ *
297
+ * @param {string} [finalText] - Text to display (defaults to this.content)
298
+ * @public
299
+ */
300
+ settle(finalText) {
301
+ if (!this.element) return;
302
+ this.stop();
303
+ this.currentBufferPhrase = null;
304
+ this.element.textContent = '';
305
+ const span = document.createElement('span');
306
+ span.style.color = '#ffffff';
307
+ span.textContent = finalText ?? this.content;
308
+ this.element.appendChild(span);
309
+ }
226
310
 
227
- // Calculate characters to advance based on speed
228
- // typingSpeed is chars/sec, we tick ~30 times/sec
229
- const charsPerTick = Math.max(1, Math.floor(this.options.typingSpeed / 30));
230
- this.displayedLen = Math.min(this.displayedLen + charsPerTick, this.content.length);
311
+ // ---------------------------------------------------------------------------
312
+ // Internal helpers
313
+ // ---------------------------------------------------------------------------
231
314
 
315
+ /**
316
+ * Advance revealed text by one character and call render().
317
+ * Delegates to _onComplete() when the full text is revealed.
318
+ * @private
319
+ */
320
+ tick() {
321
+ // Advance exactly one character per tick
322
+ this.displayedLen = Math.min(this.displayedLen + 1, this.content.length);
232
323
  this.render();
324
+
325
+ if (this.displayedLen >= this.content.length) {
326
+ this._onComplete();
327
+ }
233
328
  }
234
329
 
235
330
  /**
236
- * Check if typing is complete
237
- * @returns {boolean} True if animation is done
331
+ * Check if typing is complete.
332
+ * @returns {boolean}
238
333
  * @private
239
334
  */
240
335
  isDone() {
@@ -242,8 +337,8 @@ class TypingAnimation {
242
337
  }
243
338
 
244
339
  /**
245
- * Get the currently revealed portion of text
246
- * @returns {string} Revealed text
340
+ * Get the currently revealed portion of text.
341
+ * @returns {string}
247
342
  * @private
248
343
  */
249
344
  getDisplayed() {
@@ -251,18 +346,140 @@ class TypingAnimation {
251
346
  }
252
347
 
253
348
  /**
254
- * Get random buffer corruption phrase with appropriate color
349
+ * Compute char-advance interval in ms.
350
+ *
351
+ * Priority: duration (fixed total time) → typingSpeed (chars/sec).
352
+ * Floored at 33ms (~30fps) to avoid burning unnecessary frames.
353
+ *
354
+ * @returns {number} Interval in ms
355
+ * @private
356
+ */
357
+ _computeCharInterval() {
358
+ const { duration, typingSpeed } = this.options;
359
+ if (duration !== null && this.content.length > 0) {
360
+ return Math.max(33, duration / this.content.length);
361
+ }
362
+ return Math.max(33, 1000 / typingSpeed);
363
+ }
364
+
365
+ /**
366
+ * Clear all active timers. Safe to call repeatedly.
367
+ *
368
+ * NOTE: _onComplete() intentionally does NOT call this method — it needs
369
+ * to clear char+flicker intervals while preserving loopTimeoutId so the
370
+ * loop restart can be scheduled. If you add a fourth timer here, update
371
+ * _onComplete() to clear or preserve it as appropriate.
372
+ *
373
+ * @private
374
+ */
375
+ _clearTimers() {
376
+ if (this.charIntervalId !== null) {
377
+ clearInterval(this.charIntervalId);
378
+ this.charIntervalId = null;
379
+ }
380
+ if (this.flickerIntervalId !== null) {
381
+ clearInterval(this.flickerIntervalId);
382
+ this.flickerIntervalId = null;
383
+ }
384
+ if (this.loopTimeoutId !== null) {
385
+ clearTimeout(this.loopTimeoutId);
386
+ this.loopTimeoutId = null;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Called when the full text has been revealed.
392
+ *
393
+ * Stops char and flicker intervals, hides buffer, fires onComplete callback,
394
+ * then either schedules a loop restart or marks animation as done.
395
+ * @private
396
+ */
397
+ _onComplete() {
398
+ // Stop char and flicker intervals (NOT loopTimeoutId — set below)
399
+ if (this.charIntervalId !== null) {
400
+ clearInterval(this.charIntervalId);
401
+ this.charIntervalId = null;
402
+ }
403
+ if (this.flickerIntervalId !== null) {
404
+ clearInterval(this.flickerIntervalId);
405
+ this.flickerIntervalId = null;
406
+ }
407
+
408
+ // Hide buffer, render settled state
409
+ this.currentBufferPhrase = null;
410
+ this.render();
411
+
412
+ // Fire user callback
413
+ if (this.options.onComplete) {
414
+ this.options.onComplete();
415
+ }
416
+
417
+ if (this.options.loop) {
418
+ // Schedule restart; store ID so stop()/destroy() can cancel it
419
+ this.loopTimeoutId = setTimeout(() => {
420
+ this.loopTimeoutId = null;
421
+ this.start(this.content);
422
+ }, this.options.loopDelay);
423
+ } else {
424
+ this.done = true;
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Render the current state of the typing animation.
430
+ *
431
+ * DOM layout while typing:
432
+ * [revealed-text (white)] [space] [bufferPhrase (magenta/purple)]
433
+ *
434
+ * DOM layout when done:
435
+ * [revealed-text (white)]
436
+ *
437
+ * All DOM construction uses createElement/textContent/appendChild —
438
+ * no innerHTML, preserving XSS hardening from v0.1.7.
439
+ *
440
+ * @private
441
+ */
442
+ render() {
443
+ if (!this.element) return;
444
+ const isDone = this.isDone();
445
+ const displayed = this.getDisplayed();
446
+
447
+ // Clear and rebuild safely (no innerHTML)
448
+ this.element.textContent = '';
449
+
450
+ // Revealed text span (white)
451
+ const textSpan = document.createElement('span');
452
+ textSpan.style.color = '#ffffff';
453
+ textSpan.textContent = displayed;
454
+ this.element.appendChild(textSpan);
455
+
456
+ // Buffer corruption phrase — always present while typing, if enabled and available
457
+ if (!isDone && this.options.bufferEnabled && this.currentBufferPhrase) {
458
+ this.element.appendChild(document.createTextNode(' '));
459
+ this.element.appendChild(this.currentBufferPhrase.cloneNode(true));
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Get random buffer corruption phrase as a colored <span> element.
255
465
  *
256
466
  * Samples from phrase buffer based on SFW/NSFW mode and renders
257
467
  * with color-coded corruption aesthetic.
258
468
  *
469
+ * Category distribution:
470
+ * 0–29% Japanese phrase (SFW or NSFW per options.nsfw)
471
+ * 30–49% English phrase
472
+ * 50–64% Romaji phrase
473
+ * 65–79% Symbol (always SFW)
474
+ * 80–99% Block character (always SFW)
475
+ *
259
476
  * Color scheme:
260
- * - SFW phrases: Magenta (#d94f90) - playful corruption
261
- * - NSFW phrases: Purple (#8b5cf6) - deep/intimate corruption
262
- * - Symbols: Magenta (#d94f90)
263
- * - Blocks: Red (#ff0000) - terminal/critical state
477
+ * SFW phrases: Magenta (#d94f90) playful corruption
478
+ * NSFW phrases: Purple (#8b5cf6) deep/intimate corruption
479
+ * Symbols: Magenta (#d94f90)
480
+ * Blocks: Red (#ff0000) terminal/critical state
264
481
  *
265
- * @returns {string} HTML string with colored corruption phrase
482
+ * @returns {HTMLSpanElement} Colored span with textContent set
266
483
  * @private
267
484
  */
268
485
  getRandomCorruption() {
@@ -300,74 +517,25 @@ class TypingAnimation {
300
517
  text = romajiSet[Math.floor(Math.random() * romajiSet.length)];
301
518
  color = phraseColor;
302
519
  } else if (r < 0.80) {
303
- // Symbols - decorative corruption (always SFW)
520
+ // Symbols decorative corruption (always SFW)
304
521
  text = TypingAnimation.SYMBOLS[
305
522
  Math.floor(Math.random() * TypingAnimation.SYMBOLS.length)
306
523
  ];
307
524
  color = '#d94f90';
308
525
  } else {
309
- // Block chars - terminal/critical state (always SFW)
526
+ // Block chars terminal/critical state (always SFW)
310
527
  text = TypingAnimation.BLOCKS[
311
528
  Math.floor(Math.random() * TypingAnimation.BLOCKS.length)
312
529
  ];
313
530
  color = '#ff0000';
314
531
  }
315
532
 
316
- // Return DOM element instead of HTML string (XSS-safe)
533
+ // Build DOM element (XSS-safe — no innerHTML)
317
534
  const span = document.createElement('span');
318
535
  span.style.color = color;
319
536
  span.textContent = text;
320
537
  return span;
321
538
  }
322
-
323
- /**
324
- * Render the current state of the typing animation
325
- *
326
- * Displays revealed text (white) and occasional buffer corruption
327
- * phrases (magenta or purple) simulating neural decoding process.
328
- *
329
- * @private
330
- */
331
- render() {
332
- const displayed = this.getDisplayed();
333
-
334
- // Clear and rebuild using safe DOM methods (no innerHTML)
335
- this.element.textContent = '';
336
-
337
- // Stable revealed text (white)
338
- const textSpan = document.createElement('span');
339
- textSpan.style.color = '#ffffff';
340
- textSpan.textContent = displayed;
341
- this.element.appendChild(textSpan);
342
-
343
- // Add buffer corruption element at the "cursor" position
344
- if (!this.isDone() && Math.random() < this.options.glitchChance) {
345
- this.element.appendChild(this.getRandomCorruption());
346
- }
347
- }
348
-
349
- /**
350
- * Stop animation and immediately show final text
351
- *
352
- * @param {string} [finalText] - Text to display (defaults to content)
353
- * @public
354
- */
355
- settle(finalText) {
356
- this.stop();
357
- const span = document.createElement('span');
358
- span.style.color = '#ffffff';
359
- span.textContent = finalText || this.content;
360
- this.element.textContent = '';
361
- this.element.appendChild(span);
362
- }
363
-
364
- /**
365
- * Restart the animation from the beginning
366
- * @public
367
- */
368
- restart() {
369
- this.start(this.content);
370
- }
371
539
  }
372
540
 
373
541
  /**
package/src/css/theme.css CHANGED
@@ -28,21 +28,6 @@
28
28
  /* ========== EXTENSION COMPONENTS ========== */
29
29
  @import './extensions.css';
30
30
 
31
- /* ========== RESPONSIVE GRID SYSTEM ========== */
32
-
33
- .container {
34
- position: relative;
35
- z-index: 1;
36
- display: grid;
37
- grid-template-columns: 1fr 1fr;
38
- gap: 4rem;
39
- align-items: center;
40
- min-height: 100vh;
41
- padding: 3rem;
42
- max-width: 1400px;
43
- margin: 0 auto;
44
- }
45
-
46
31
  .content {
47
32
  padding: 2.5rem;
48
33
  background: var(--glass);