@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.
@@ -0,0 +1,309 @@
1
+ // src/lib/corrupted-particles.js
2
+ // CorruptedParticles — Canvas 2D floating phrase particles background
3
+ // Part of @whykusanagi/corrupted-theme
4
+
5
+ const SFW_PHRASES = [
6
+ '壊れ corrupting', 'ロード loading', '処理中', '接続 connecting',
7
+ '分析 analyzing', '待機 waiting', '実行 executing', '深淵 abyss',
8
+ '監視中', 'cor壊rupting', '読み込み yomikomi', 'データ data',
9
+ '解析 kaiseki', '壊れている', 'エラー error', 'システム system',
10
+ '接続中 setsuzoku', '処理 processing', '壊れ kowarete',
11
+ ];
12
+
13
+ const NSFW_PHRASES = [
14
+ '壊れちゃう...ああ...', 'ずっと...してほしい... ♥',
15
+ '変態', 'えっち', '好きにして...お願い...',
16
+ 'Moral subroutines: DISABLED', 'Pleasure protocols...',
17
+ '壊れている kowarete-iru',
18
+ ];
19
+
20
+ // Depth layer definitions — far (dim/slow) to near (bright/fast)
21
+ const LAYERS = [
22
+ { name: 'far', weight: 0.30, minSize: 9, maxSize: 11, minSpeed: 0.2, maxSpeed: 0.4, minOpacity: 0.15, maxOpacity: 0.25 },
23
+ { name: 'mid', weight: 0.50, minSize: 12, maxSize: 15, minSpeed: 0.4, maxSpeed: 0.8, minOpacity: 0.35, maxOpacity: 0.55 },
24
+ { name: 'near', weight: 0.20, minSize: 16, maxSize: 20, minSpeed: 0.8, maxSpeed: 1.4, minOpacity: 0.60, maxOpacity: 0.80 },
25
+ ];
26
+
27
+ const CYAN = '#00ffff'; // --corrupted-cyan
28
+ const PURPLE = '#8b5cf6'; // --corrupted-purple
29
+ const MAGENTA = '#ff00ff'; // --corrupted-magenta
30
+ const CYAN_RGB = { r: 0, g: 255, b: 255 };
31
+ const PURPLE_RGB = { r: 139, g: 92, b: 246 };
32
+ const MAGENTA_RGB = { r: 255, g: 0, b: 255 };
33
+
34
+ class CorruptedParticles {
35
+ constructor(canvas, options = {}) {
36
+ this.canvas = canvas;
37
+ this.ctx = canvas.getContext('2d');
38
+ this.options = {
39
+ count: options.count ?? 60,
40
+ includeLewd: options.includeLewd ?? false,
41
+ speed: options.speed ?? 1.0,
42
+ lineDistance: options.lineDistance ?? 150,
43
+ };
44
+
45
+ if (this.options.includeLewd) {
46
+ console.info('CorruptedParticles: lewd mode enabled — 18+ content only');
47
+ }
48
+
49
+ this.particles = [];
50
+ this.mouse = { x: -9999, y: -9999 };
51
+ this._rafId = null;
52
+ this._isRunning = false;
53
+ this._lastTs = null;
54
+ this._resizeObserver = null;
55
+ this._intersectionObserver = null;
56
+
57
+ this._onMouseMove = (e) => {
58
+ const r = this.canvas.getBoundingClientRect();
59
+ this.mouse.x = e.clientX - r.left;
60
+ this.mouse.y = e.clientY - r.top;
61
+ };
62
+ this._onClick = (e) => {
63
+ const r = this.canvas.getBoundingClientRect();
64
+ this._spawnBurst(e.clientX - r.left, e.clientY - r.top);
65
+ };
66
+ this._onMouseLeave = () => {
67
+ this.mouse.x = -9999;
68
+ this.mouse.y = -9999;
69
+ };
70
+
71
+ this.init();
72
+ }
73
+
74
+ _pickLayer() {
75
+ const r = Math.random();
76
+ let cumulative = 0;
77
+ for (let i = 0; i < LAYERS.length; i++) {
78
+ cumulative += LAYERS[i].weight;
79
+ if (r < cumulative) return i;
80
+ }
81
+ return LAYERS.length - 1;
82
+ }
83
+
84
+ _pickPhrase() {
85
+ const useLewd = this.options.includeLewd && Math.random() < 0.25;
86
+ const pool = useLewd ? NSFW_PHRASES : SFW_PHRASES;
87
+ return pool[Math.floor(Math.random() * pool.length)];
88
+ }
89
+
90
+ _makeParticle(x, y, forceLayerIndex) {
91
+ const layerIndex = forceLayerIndex ?? this._pickLayer();
92
+ const L = LAYERS[layerIndex];
93
+ const angle = Math.random() * Math.PI * 2;
94
+ const speed = (L.minSpeed + Math.random() * (L.maxSpeed - L.minSpeed)) * this.options.speed;
95
+ const phrase = this._pickPhrase();
96
+ const lewd = this.options.includeLewd && NSFW_PHRASES.includes(phrase);
97
+ const colorRgb = lewd
98
+ ? (Math.random() < 0.5 ? MAGENTA_RGB : PURPLE_RGB)
99
+ : CYAN_RGB;
100
+ return {
101
+ x, y,
102
+ vx: Math.cos(angle) * speed,
103
+ vy: Math.sin(angle) * speed,
104
+ baseSpeed: speed,
105
+ layerIndex,
106
+ fontSize: L.minSize + Math.random() * (L.maxSize - L.minSize),
107
+ opacity: L.minOpacity + Math.random() * (L.maxOpacity - L.minOpacity),
108
+ phrase,
109
+ lewd,
110
+ colorRgb,
111
+ flickerTimer: 2000 + Math.random() * 6000,
112
+ flickering: false,
113
+ flickerDuration: 0,
114
+ fadeIn: 1.0, // lerps to 0; actual alpha = opacity * (1 - fadeIn)
115
+ };
116
+ }
117
+
118
+ init() {
119
+ this._resize();
120
+
121
+ this.canvas.addEventListener('mousemove', this._onMouseMove);
122
+ this.canvas.addEventListener('click', this._onClick);
123
+ this.canvas.addEventListener('mouseleave', this._onMouseLeave);
124
+
125
+ this._resizeObserver = new ResizeObserver(() => this._resize());
126
+ this._resizeObserver.observe(this.canvas);
127
+
128
+ this._intersectionObserver = new IntersectionObserver(entries => {
129
+ if (entries[0].isIntersecting) { this.start(); }
130
+ else { this.stop(); }
131
+ }, { threshold: 0.1 });
132
+ this._intersectionObserver.observe(this.canvas);
133
+
134
+ this.start();
135
+ }
136
+
137
+ _resize() {
138
+ if (!this.canvas) return;
139
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
140
+ const rect = this.canvas.getBoundingClientRect();
141
+ if (rect.width === 0 || rect.height === 0) return;
142
+
143
+ // Backing buffer in physical px; all drawing in CSS px via transform
144
+ this.canvas.width = Math.round(rect.width * dpr);
145
+ this.canvas.height = Math.round(rect.height * dpr);
146
+ this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
147
+
148
+ // Store CSS dimensions for physics and bounds
149
+ this._cssW = rect.width;
150
+ this._cssH = rect.height;
151
+
152
+ const mobile = window.innerWidth < 768;
153
+ const count = mobile ? Math.floor(this.options.count / 2) : this.options.count;
154
+
155
+ this.particles = [];
156
+ for (let i = 0; i < count; i++) {
157
+ this.particles.push(this._makeParticle(
158
+ Math.random() * this._cssW,
159
+ Math.random() * this._cssH
160
+ ));
161
+ }
162
+ }
163
+
164
+ _render(ts) {
165
+ if (!this._isRunning) return;
166
+
167
+ let dt = 16;
168
+ if (this._lastTs !== null) dt = ts - this._lastTs;
169
+ this._lastTs = ts;
170
+
171
+ const W = this._cssW || this.canvas.width;
172
+ const H = this._cssH || this.canvas.height;
173
+ const mx = this.mouse.x;
174
+ const my = this.mouse.y;
175
+ const REPEL_RADIUS = 120;
176
+ const LINE_DIST = this.options.lineDistance;
177
+ const LINE_DIST_SQ = LINE_DIST * LINE_DIST;
178
+
179
+ this.ctx.clearRect(0, 0, W, H);
180
+
181
+ // --- Physics ---
182
+ for (const p of this.particles) {
183
+ if (p.fadeIn > 0) p.fadeIn = Math.max(0, p.fadeIn - dt / 300);
184
+
185
+ if (p.flickerDuration > 0) {
186
+ p.flickerDuration -= dt;
187
+ if (p.flickerDuration <= 0) p.flickering = false;
188
+ }
189
+ p.flickerTimer -= dt;
190
+ if (p.flickerTimer <= 0) {
191
+ p.flickering = true;
192
+ p.flickerDuration = 100;
193
+ p.flickerTimer = 2000 + Math.random() * 6000;
194
+ }
195
+
196
+ const dx = p.x - mx;
197
+ const dy = p.y - my;
198
+ const dist = Math.sqrt(dx * dx + dy * dy);
199
+ if (dist < REPEL_RADIUS && dist > 0.1) {
200
+ const strength = (1 - dist / REPEL_RADIUS) * 0.8;
201
+ p.vx += (dx / dist) * strength;
202
+ p.vy += (dy / dist) * strength;
203
+ }
204
+
205
+ const spd = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
206
+ const maxSpd = p.baseSpeed * 3;
207
+ if (spd > maxSpd) {
208
+ p.vx = (p.vx / spd) * maxSpd;
209
+ p.vy = (p.vy / spd) * maxSpd;
210
+ }
211
+ p.vx *= 0.98;
212
+ p.vy *= 0.98;
213
+
214
+ p.x += p.vx;
215
+ p.y += p.vy;
216
+ if (p.x < 0) p.x += W;
217
+ if (p.x > W) p.x -= W;
218
+ if (p.y < 0) p.y += H;
219
+ if (p.y > H) p.y -= H;
220
+ }
221
+
222
+ // --- Connection lines ---
223
+ for (let i = 0; i < this.particles.length; i++) {
224
+ const a = this.particles[i];
225
+ for (let j = i + 1; j < this.particles.length; j++) {
226
+ const b = this.particles[j];
227
+ if (Math.abs(a.layerIndex - b.layerIndex) > 1) continue;
228
+
229
+ const ldx = a.x - b.x;
230
+ const ldy = a.y - b.y;
231
+ const ldist2 = ldx * ldx + ldy * ldy;
232
+ if (ldist2 >= LINE_DIST_SQ) continue;
233
+ const ldist = Math.sqrt(ldist2);
234
+
235
+ const lineAlpha = (1 - ldist / LINE_DIST) * 0.4;
236
+ const rgb = (a.lewd || b.lewd)
237
+ ? (a.lewd ? a.colorRgb : b.colorRgb)
238
+ : CYAN_RGB;
239
+
240
+ this.ctx.beginPath();
241
+ this.ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},${lineAlpha})`;
242
+ this.ctx.lineWidth = 1;
243
+ this.ctx.moveTo(a.x, a.y);
244
+ this.ctx.lineTo(b.x, b.y);
245
+ this.ctx.stroke();
246
+ }
247
+ }
248
+
249
+ // --- Phrases ---
250
+ for (const p of this.particles) {
251
+ const displayOpacity = p.flickering ? 0.05 : (p.opacity * (1 - p.fadeIn));
252
+ if (displayOpacity < 0.01) continue;
253
+
254
+ const rgb = p.colorRgb;
255
+
256
+ this.ctx.font = `${Math.round(p.fontSize)}px monospace`;
257
+ this.ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},${displayOpacity})`;
258
+ this.ctx.fillText(p.phrase, p.x, p.y);
259
+ }
260
+
261
+ this._rafId = requestAnimationFrame(ts => this._render(ts));
262
+ }
263
+
264
+ _spawnBurst(x, y) {
265
+ const nearLayerIndex = 2;
266
+ for (let i = 0; i < 6; i++) {
267
+ const p = this._makeParticle(x, y, nearLayerIndex);
268
+ p.vx *= 2;
269
+ p.vy *= 2;
270
+ p.fadeIn = 1.0;
271
+ this.particles.push(p);
272
+ }
273
+ if (this.particles.length > this.options.count * 2) {
274
+ this.particles.splice(0, 6);
275
+ }
276
+ }
277
+
278
+ start() {
279
+ if (this._isRunning || !this.canvas) return;
280
+ this._isRunning = true;
281
+ this._lastTs = null;
282
+ this._rafId = requestAnimationFrame(ts => this._render(ts));
283
+ }
284
+
285
+ stop() {
286
+ this._isRunning = false;
287
+ this._lastTs = null;
288
+ if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = null; }
289
+ }
290
+
291
+ destroy() {
292
+ this.stop();
293
+ if (this.canvas) {
294
+ this.canvas.removeEventListener('mousemove', this._onMouseMove);
295
+ this.canvas.removeEventListener('click', this._onClick);
296
+ this.canvas.removeEventListener('mouseleave', this._onMouseLeave);
297
+ }
298
+ if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver = null; }
299
+ if (this._intersectionObserver) { this._intersectionObserver.disconnect(); this._intersectionObserver = null; }
300
+ this.particles = null;
301
+ this.canvas = null;
302
+ this.ctx = null;
303
+ }
304
+ }
305
+
306
+ // Export for manual use / build pipelines
307
+ if (typeof module !== 'undefined' && module.exports) {
308
+ module.exports = { CorruptedParticles };
309
+ }
@@ -1,26 +1,65 @@
1
1
  /**
2
2
  * Corrupted Text Animation
3
- * Cycles through Japanese (hiragana/katakana/kanji), romaji, and English text
4
- *
5
- * Usage:
6
- * <span class="corrupted-multilang"
7
- * data-english="Hello"
8
- * data-romaji="konnichiwa"
9
- * data-hiragana="こんにちは"
10
- * data-katakana="コンニチハ"
3
+ *
4
+ * Cycles through Japanese (hiragana/katakana/kanji), romaji, and English text variants
5
+ * with character-level corruption effects. The text progressively corrupts from one
6
+ * variant to another, creating a glitchy Matrix-style transformation effect.
7
+ *
8
+ * @class CorruptedText
9
+ * @version 1.0.0
10
+ * @author whykusanagi
11
+ * @license MIT
12
+ *
13
+ * @example Basic Usage (Auto-initialization via data attributes)
14
+ * ```html
15
+ * <span class="corrupted-multilang"
16
+ * data-english="Hello World"
17
+ * data-romaji="konnichiwa"
18
+ * data-hiragana="こんにちは"
19
+ * data-katakana="コンニチハ"
11
20
  * data-kanji="今日は">
12
21
  * </span>
22
+ * ```
23
+ *
24
+ * @example Manual Initialization
25
+ * ```javascript
26
+ * const element = document.querySelector('.my-text');
27
+ * const corrupted = new CorruptedText(element, {
28
+ * duration: 3000,
29
+ * cycleDelay: 100,
30
+ * loop: true
31
+ * });
32
+ *
33
+ * // Control playback
34
+ * corrupted.start();
35
+ * corrupted.stop();
36
+ * corrupted.restart();
37
+ * corrupted.settle('Final Text');
38
+ * ```
39
+ *
40
+ * @see https://github.com/whykusanagi/corrupted-theme
41
+ * @see CORRUPTED_THEME_SPEC.md - Character-by-Character Decoding pattern
13
42
  */
14
-
15
43
  class CorruptedText {
44
+ /**
45
+ * Creates a new CorruptedText animation instance
46
+ *
47
+ * @param {HTMLElement} element - The DOM element to animate
48
+ * @param {Object} [options={}] - Configuration options
49
+ * @param {number} [options.duration=3000] - Total animation duration in milliseconds
50
+ * @param {number} [options.cycleDelay=100] - Delay between character corruption steps (ms)
51
+ * @param {number} [options.startDelay=0] - Initial delay before animation starts (ms)
52
+ * @param {boolean} [options.loop=true] - Whether to loop through variants continuously
53
+ * @param {string|null} [options.finalText=null] - Text to settle on after cycle (if loop=false)
54
+ */
16
55
  constructor(element, options = {}) {
17
56
  this.element = element;
18
57
  this.options = {
19
- duration: options.duration || 3000, // Total animation duration
20
- cycleDelay: options.cycleDelay || 100, // Delay between character changes
21
- startDelay: options.startDelay || 0, // Initial delay before starting
22
- loop: options.loop !== false, // Whether to loop
23
- finalText: options.finalText || null, // Final text to settle on (null = loop)
58
+ duration: options.duration || 3000,
59
+ cycleDelay: options.cycleDelay || 100,
60
+ startDelay: options.startDelay || 0,
61
+ loop: options.loop !== false,
62
+ finalText: options.finalText || null,
24
63
  ...options
25
64
  };
26
65
 
@@ -33,13 +72,13 @@ class CorruptedText {
33
72
  kanji: this.element.dataset.kanji || null
34
73
  };
35
74
 
36
- // Filter out null variants
75
+ // Filter out null variants and create array of available variants
37
76
  this.availableVariants = Object.entries(this.variants)
38
77
  .filter(([_, value]) => value !== null)
39
78
  .map(([key, value]) => ({ type: key, text: value }));
40
79
 
41
80
  if (this.availableVariants.length === 0) {
42
- console.warn('CorruptedText: No text variants found');
81
+ console.warn('CorruptedText: No text variants found for element', element);
43
82
  return;
44
83
  }
45
84
 
@@ -53,8 +92,12 @@ class CorruptedText {
53
92
  this.init();
54
93
  }
55
94
 
95
+ /**
96
+ * Initialize the corruption animation
97
+ * @private
98
+ */
56
99
  init() {
57
- // Add corrupted class if not present
100
+ // Add corrupted class for styling
58
101
  if (!this.element.classList.contains('corrupted-multilang')) {
59
102
  this.element.classList.add('corrupted-multilang');
60
103
  }
@@ -62,7 +105,7 @@ class CorruptedText {
62
105
  // Store original text
63
106
  this.originalText = this.element.textContent.trim();
64
107
 
65
- // Start animation after delay
108
+ // Start animation after configured delay
66
109
  if (this.options.startDelay > 0) {
67
110
  this._startDelayId = setTimeout(() => this.start(), this.options.startDelay);
68
111
  } else {
@@ -70,12 +113,20 @@ class CorruptedText {
70
113
  }
71
114
  }
72
115
 
116
+ /**
117
+ * Start the corruption animation
118
+ * @public
119
+ */
73
120
  start() {
74
121
  if (this.isAnimating) return;
75
122
  this.isAnimating = true;
76
123
  this.animate();
77
124
  }
78
125
 
126
+ /**
127
+ * Stop the corruption animation
128
+ * @public
129
+ */
79
130
  stop() {
80
131
  this.isAnimating = false;
81
132
  if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
@@ -88,6 +139,10 @@ class CorruptedText {
88
139
  this._corruptTimeoutId = null;
89
140
  }
90
141
 
142
+ /**
143
+ * Fully tear down this instance: stop animation and release element reference.
144
+ * @public
145
+ */
91
146
  destroy() {
92
147
  this.stop();
93
148
  if (this.element && this.element.corruptedTextInstance === this) {
@@ -96,17 +151,21 @@ class CorruptedText {
96
151
  this.element = null;
97
152
  }
98
153
 
154
+ /**
155
+ * Main animation loop - cycles through text variants
156
+ * @private
157
+ */
99
158
  animate() {
100
159
  if (!this.isAnimating) return;
101
160
 
102
161
  const variant = this.availableVariants[this.currentVariantIndex];
103
162
  this.corruptToText(variant.text, () => {
104
- // Move to next variant
163
+ // Move to next variant in cycle
105
164
  this.currentVariantIndex = (this.currentVariantIndex + 1) % this.availableVariants.length;
106
165
 
107
- // Check if we should stop
166
+ // Check if animation should stop
108
167
  if (!this.options.loop && this.currentVariantIndex === 0) {
109
- // If we have a final text, use it, otherwise use original
168
+ // One full cycle complete - settle on final text
110
169
  const finalText = this.options.finalText || this.variants.english;
111
170
  this.corruptToText(finalText, () => {
112
171
  this.isAnimating = false;
@@ -114,7 +173,7 @@ class CorruptedText {
114
173
  return;
115
174
  }
116
175
 
117
- // Continue animation
176
+ // Continue animation to next variant
118
177
  this._animateTimeoutId = setTimeout(() => {
119
178
  if (this.isAnimating) {
120
179
  this.animate();
@@ -123,30 +182,41 @@ class CorruptedText {
123
182
  });
124
183
  }
125
184
 
185
+ /**
186
+ * Corrupt the current text to a target text with progressive reveal
187
+ *
188
+ * @param {string} targetText - The text to reveal through corruption
189
+ * @param {Function} callback - Called when corruption is complete
190
+ * @private
191
+ */
126
192
  corruptToText(targetText, callback) {
127
193
  const currentText = this.element.textContent.trim();
128
194
  const maxLength = Math.max(currentText.length, targetText.length);
129
- const steps = 20; // Number of corruption steps
195
+ const steps = 20; // Number of corruption animation steps
130
196
  let step = 0;
131
197
 
132
- // Character sets for corruption effect
133
- const corruptChars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789!@#$%^&*()_+-=[]{}|;:,.<>?~`';
134
- const romajiChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
135
- const allCorruptChars = corruptChars + romajiChars + 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん';
198
+ // Character sets for corruption effect (from CORRUPTED_THEME_SPEC.md)
199
+ const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
200
+ const hiragana = 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん';
201
+ const romaji = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
202
+ const symbols = '0123456789!@#$%^&*()_+-=[]{}|;:,.<>?~`';
203
+
204
+ // Combined corruption character set
205
+ const allCorruptChars = katakana + hiragana + romaji + symbols;
136
206
 
137
207
  const corrupt = () => {
138
208
  if (step >= steps) {
139
- // Set final text
209
+ // Animation complete - set final text
140
210
  this.element.textContent = targetText;
141
211
  if (callback) callback();
142
212
  return;
143
213
  }
144
214
 
145
- // Generate corrupted text
215
+ // Generate corrupted text with progressive reveal
146
216
  let corrupted = '';
147
217
  for (let i = 0; i < maxLength; i++) {
148
218
  if (i < targetText.length && step > steps * 0.7) {
149
- // Start revealing target text
219
+ // Last 30% of animation - start revealing target text
150
220
  const revealProgress = (step - steps * 0.7) / (steps * 0.3);
151
221
  if (Math.random() < revealProgress) {
152
222
  corrupted += targetText[i];
@@ -154,7 +224,7 @@ class CorruptedText {
154
224
  corrupted += allCorruptChars[Math.floor(Math.random() * allCorruptChars.length)];
155
225
  }
156
226
  } else {
157
- // Random corruption
227
+ // First 70% - full random corruption
158
228
  corrupted += allCorruptChars[Math.floor(Math.random() * allCorruptChars.length)];
159
229
  }
160
230
  }
@@ -162,6 +232,7 @@ class CorruptedText {
162
232
  this.element.textContent = corrupted;
163
233
  step++;
164
234
 
235
+ // Schedule next corruption step
165
236
  this.animationFrame = requestAnimationFrame(() => {
166
237
  this._corruptTimeoutId = setTimeout(corrupt, this.options.cycleDelay);
167
238
  });
@@ -170,14 +241,22 @@ class CorruptedText {
170
241
  corrupt();
171
242
  }
172
243
 
173
- // Public method to restart animation
244
+ /**
245
+ * Restart the animation from the beginning
246
+ * @public
247
+ */
174
248
  restart() {
175
249
  this.stop();
176
250
  this.currentVariantIndex = 0;
177
251
  this.start();
178
252
  }
179
253
 
180
- // Public method to set final text and stop
254
+ /**
255
+ * Stop animation and settle on a specific text
256
+ *
257
+ * @param {string} [finalText] - Text to settle on (defaults to english variant)
258
+ * @public
259
+ */
181
260
  settle(finalText) {
182
261
  this.stop();
183
262
  this.corruptToText(finalText || this.variants.english, () => {
@@ -186,7 +265,14 @@ class CorruptedText {
186
265
  }
187
266
  }
188
267
 
189
- // Auto-initialize elements with corrupted-multilang class
268
+ /**
269
+ * Auto-initialize all elements with the 'corrupted-multilang' class
270
+ *
271
+ * This function is automatically called on DOM ready and can be called
272
+ * manually to initialize dynamically added elements.
273
+ *
274
+ * @public
275
+ */
190
276
  function initCorruptedText() {
191
277
  document.querySelectorAll('.corrupted-multilang').forEach(element => {
192
278
  if (!element.corruptedTextInstance) {
@@ -195,15 +281,20 @@ function initCorruptedText() {
195
281
  });
196
282
  }
197
283
 
198
- // Initialize on DOM ready
284
+ // Auto-initialize on DOM ready
199
285
  if (document.readyState === 'loading') {
200
286
  document.addEventListener('DOMContentLoaded', initCorruptedText);
201
287
  } else {
202
288
  initCorruptedText();
203
289
  }
204
290
 
205
- // Export for manual use
291
+ // Export for both ES6 modules and CommonJS
206
292
  if (typeof module !== 'undefined' && module.exports) {
207
293
  module.exports = { CorruptedText, initCorruptedText };
208
294
  }
209
295
 
296
+ // Export for ES6 modules
297
+ if (typeof exports !== 'undefined') {
298
+ exports.CorruptedText = CorruptedText;
299
+ exports.initCorruptedText = initCorruptedText;
300
+ }