@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.
- package/CHANGELOG.md +28 -0
- package/README.md +140 -4
- package/docs/COMPONENTS_REFERENCE.md +205 -2
- package/docs/governance/VERSION_MANAGEMENT.md +2 -2
- package/docs/governance/VERSION_REFERENCES.md +65 -202
- package/docs/platforms/NPM_PACKAGE.md +8 -6
- package/examples/advanced/glsl-vortex.html +297 -0
- package/examples/advanced/particles-bg.html +263 -0
- package/examples/basic/corrupted-text.html +2 -3
- package/examples/basic/typing-animation.html +136 -55
- package/examples/button.html +1 -2
- package/examples/card.html +1 -2
- package/examples/extensions-showcase.html +36 -1
- package/examples/form.html +1 -2
- package/examples/index.html +28 -4
- package/examples/layout.html +1 -2
- package/examples/nikke-team-builder.html +1 -2
- package/examples/showcase-complete.html +2 -3
- package/examples/showcase.html +1 -2
- package/package.json +4 -3
- package/src/core/typing-animation.js +274 -106
- package/src/css/theme.css +0 -15
- package/src/lib/corrupted-particles.js +309 -0
- package/src/lib/corrupted-text.js +127 -36
- package/src/lib/corrupted-vortex.js +329 -0
- package/docs/ROADMAP.md +0 -266
- package/examples/advanced/nsfw-corruption.html +0 -348
- package/src/core/corrupted-text.js +0 -300
|
@@ -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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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,
|
|
20
|
-
cycleDelay: options.cycleDelay || 100,
|
|
21
|
-
startDelay: options.startDelay || 0,
|
|
22
|
-
loop: options.loop !== false,
|
|
23
|
-
finalText: options.finalText || null,
|
|
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
|
|
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
|
|
166
|
+
// Check if animation should stop
|
|
108
167
|
if (!this.options.loop && this.currentVariantIndex === 0) {
|
|
109
|
-
//
|
|
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
|
|
134
|
-
const
|
|
135
|
-
const
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
+
}
|