asciify-engine 1.0.17 → 1.0.18

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/dist/index.cjs CHANGED
@@ -113,6 +113,8 @@ var HOVER_PRESETS = {
113
113
  options: { hoverStrength: 0.5, hoverEffect: "glow", hoverRadius: 0.15, hoverColor: "#60d5f7" }
114
114
  }
115
115
  };
116
+
117
+ // src/core/utils.ts
116
118
  function createOffscreenCanvas(width, height) {
117
119
  const canvas = document.createElement("canvas");
118
120
  canvas.width = width;
@@ -143,29 +145,29 @@ var BAYER_4X4 = [
143
145
  [3, 11, 1, 9],
144
146
  [15, 7, 13, 5]
145
147
  ];
146
- var _GRAY_LUT = new Array(256);
147
- var _GREEN_LUT = new Array(256);
148
- for (let _i = 0; _i < 256; _i++) {
149
- _GRAY_LUT[_i] = `rgb(${_i},${_i},${_i})`;
150
- _GREEN_LUT[_i] = `rgb(0,${_i},0)`;
151
- }
152
148
  function applyDither(lum, x, y, strength) {
153
149
  if (strength <= 0) return lum;
154
150
  const threshold = (BAYER_4X4[y % 4][x % 4] / 16 - 0.5) * strength * 128;
155
151
  return Math.max(0, Math.min(255, lum + threshold));
156
152
  }
153
+ var GRAY_LUT = new Array(256);
154
+ var GREEN_LUT = new Array(256);
155
+ for (let _i = 0; _i < 256; _i++) {
156
+ GRAY_LUT[_i] = `rgb(${_i},${_i},${_i})`;
157
+ GREEN_LUT[_i] = `rgb(0,${_i},0)`;
158
+ }
157
159
  function getCellColorStr(cell, colorMode, acR, acG, acB) {
158
160
  switch (colorMode) {
159
161
  case "fullcolor":
160
162
  return `rgb(${cell.r},${cell.g},${cell.b})`;
161
163
  case "matrix":
162
- return _GREEN_LUT[0.299 * cell.r + 0.587 * cell.g + 0.114 * cell.b | 0];
164
+ return GREEN_LUT[0.299 * cell.r + 0.587 * cell.g + 0.114 * cell.b | 0];
163
165
  case "accent": {
164
166
  const ab = (0.299 * cell.r + 0.587 * cell.g + 0.114 * cell.b) / 255;
165
167
  return `rgb(${acR * ab | 0},${acG * ab | 0},${acB * ab | 0})`;
166
168
  }
167
169
  default:
168
- return _GRAY_LUT[0.299 * cell.r + 0.587 * cell.g + 0.114 * cell.b | 0];
170
+ return GRAY_LUT[0.299 * cell.r + 0.587 * cell.g + 0.114 * cell.b | 0];
169
171
  }
170
172
  }
171
173
  var _colorRGB = [0, 0, 0];
@@ -200,6 +202,11 @@ function getCellColorRGB(cell, colorMode, acR, acG, acB) {
200
202
  }
201
203
  return _colorRGB;
202
204
  }
205
+
206
+ // src/core/animation.ts
207
+ function smoothstep(t) {
208
+ return t * t * (3 - 2 * t);
209
+ }
203
210
  function getAnimationMultiplier(x, y, cols, rows, time, style, speed) {
204
211
  if (style === "none") return 1;
205
212
  const t = time * speed;
@@ -219,8 +226,8 @@ function getAnimationMultiplier(x, y, cols, rows, time, style, speed) {
219
226
  }
220
227
  case "rain": {
221
228
  const drop = Math.sin(y / rows * Math.PI * 8 - t * 5 + x * 0.3) * 0.5 + 0.5;
222
- const fade = Math.sin(x / cols * Math.PI * 2 + t) * 0.3 + 0.7;
223
- return 0.1 + 0.9 * drop * fade;
229
+ const fade2 = Math.sin(x / cols * Math.PI * 2 + t) * 0.3 + 0.7;
230
+ return 0.1 + 0.9 * drop * fade2;
224
231
  }
225
232
  case "breathe": {
226
233
  const breathe = Math.sin(t * 2) * 0.3 + 0.7;
@@ -260,8 +267,8 @@ function getAnimationMultiplier(x, y, cols, rows, time, style, speed) {
260
267
  const revealPoint = progress * totalCells * 1.3;
261
268
  const dist = cellIndex - revealPoint;
262
269
  if (dist > 0) return 0;
263
- const fade = Math.max(0, 1 + dist / (totalCells * 0.15));
264
- return Math.min(1, fade);
270
+ const fade2 = Math.max(0, 1 + dist / (totalCells * 0.15));
271
+ return Math.min(1, fade2);
265
272
  }
266
273
  case "scatter": {
267
274
  const scx = cols / 2;
@@ -282,6 +289,200 @@ function getAnimationMultiplier(x, y, cols, rows, time, style, speed) {
282
289
  return 1;
283
290
  }
284
291
  }
292
+ var _hoverResult = { scale: 1, offsetX: 0, offsetY: 0, glow: 0, colorBlend: 0, proximity: 0 };
293
+ function computeHoverEffect(nx, ny, hoverX, hoverY, hoverIntensity, strength, cellW, cellH, effect = "spotlight", radiusFactor = 0.5) {
294
+ const dx = nx - hoverX;
295
+ const dy = ny - hoverY;
296
+ const distSq = dx * dx + dy * dy;
297
+ const radius = 0.08 + radiusFactor * 0.35 + strength * 0.04;
298
+ if (distSq >= radius * radius) {
299
+ _hoverResult.scale = 1;
300
+ _hoverResult.offsetX = 0;
301
+ _hoverResult.offsetY = 0;
302
+ _hoverResult.glow = 0;
303
+ _hoverResult.colorBlend = 0;
304
+ _hoverResult.proximity = 0;
305
+ return _hoverResult;
306
+ }
307
+ const dist = Math.sqrt(distSq);
308
+ const t = 1 - dist / radius;
309
+ const eased = smoothstep(t) * hoverIntensity;
310
+ let scale = 1;
311
+ let offsetX = 0;
312
+ let offsetY = 0;
313
+ let glow = 0;
314
+ let colorBlend = 0;
315
+ switch (effect) {
316
+ case "spotlight": {
317
+ scale = 1 + eased * strength * 1.8;
318
+ const angle = Math.atan2(dy, dx);
319
+ const pushForce = eased * eased * strength * 0.6;
320
+ offsetX = Math.cos(angle) * pushForce * cellW;
321
+ offsetY = Math.sin(angle) * pushForce * cellH;
322
+ glow = eased * strength * 0.4;
323
+ colorBlend = eased * eased * strength * 0.25;
324
+ break;
325
+ }
326
+ case "magnify":
327
+ scale = 1 + eased * strength * 2.5;
328
+ glow = eased * strength * 0.15;
329
+ break;
330
+ case "repel": {
331
+ scale = 1 + eased * strength * 0.3;
332
+ const angle2 = Math.atan2(dy, dx);
333
+ const push = eased * eased * strength * 1.2;
334
+ offsetX = Math.cos(angle2) * push * cellW;
335
+ offsetY = Math.sin(angle2) * push * cellH;
336
+ break;
337
+ }
338
+ case "glow":
339
+ glow = eased * strength * 0.8;
340
+ colorBlend = eased * strength * 0.4;
341
+ break;
342
+ case "colorShift":
343
+ scale = 1 + eased * strength * 0.4;
344
+ glow = eased * strength * 0.2;
345
+ colorBlend = eased * strength * 0.7;
346
+ break;
347
+ }
348
+ _hoverResult.scale = scale;
349
+ _hoverResult.offsetX = offsetX;
350
+ _hoverResult.offsetY = offsetY;
351
+ _hoverResult.glow = glow;
352
+ _hoverResult.colorBlend = colorBlend;
353
+ _hoverResult.proximity = eased;
354
+ return _hoverResult;
355
+ }
356
+
357
+ // src/backgrounds/_shared.ts
358
+ function parseColor(c) {
359
+ const hex = c.match(/^#([0-9a-f]{3,8})$/i)?.[1];
360
+ if (hex) {
361
+ const h = hex.length <= 4 ? hex.split("").map((x) => parseInt(x + x, 16)) : [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
362
+ return { r: h[0], g: h[1], b: h[2] };
363
+ }
364
+ const rgb = c.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);
365
+ if (rgb) return { r: +rgb[1], g: +rgb[2], b: +rgb[3] };
366
+ return null;
367
+ }
368
+ function fade(t) {
369
+ return t * t * t * (t * (t * 6 - 15) + 10);
370
+ }
371
+ function lerp(a, b, t) {
372
+ return a + (b - a) * t;
373
+ }
374
+ function hash2(ix, iy) {
375
+ let n = ix * 127 + iy * 311;
376
+ n = n >> 13 ^ n;
377
+ return (n * (n * n * 15731 + 789221) + 1376312589 & 2147483647) / 1073741823 - 1;
378
+ }
379
+ function vnoise(x, y) {
380
+ const ix = Math.floor(x), iy = Math.floor(y);
381
+ const fx = x - ix, fy = y - iy;
382
+ const ux = fade(fx), uy = fade(fy);
383
+ const v00 = hash2(ix, iy);
384
+ const v10 = hash2(ix + 1, iy);
385
+ const v01 = hash2(ix, iy + 1);
386
+ const v11 = hash2(ix + 1, iy + 1);
387
+ return lerp(lerp(v00, v10, ux), lerp(v01, v11, ux), uy);
388
+ }
389
+ function fbm(x, y) {
390
+ return (vnoise(x, y) * 0.5 + vnoise(x * 2.1, y * 2.1) * 0.25 + vnoise(x * 4.3, y * 4.3) * 0.125) / 0.875;
391
+ }
392
+
393
+ // src/backgrounds/wave.ts
394
+ function renderWaveBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
395
+ const {
396
+ fontSize = 13,
397
+ charAspect = 0.62,
398
+ lineHeightRatio = 1.4,
399
+ chars = " .:-=+*#%@",
400
+ baseColor = null,
401
+ accentColor = "#d4ff00",
402
+ accentThreshold = 0.52,
403
+ mouseInfluence = 0.55,
404
+ mouseFalloff = 2.8,
405
+ speed = 1,
406
+ vortex = true,
407
+ sparkles = true,
408
+ breathe = true,
409
+ lightMode = false
410
+ } = options;
411
+ const charW = fontSize * charAspect;
412
+ const lineH = fontSize * lineHeightRatio;
413
+ const cols = Math.ceil(width / charW);
414
+ const rows = Math.ceil(height / lineH);
415
+ const mx = mousePos.x;
416
+ const my = mousePos.y;
417
+ let acR = 212, acG = 255, acB = 0;
418
+ if (accentColor) {
419
+ const hex = accentColor.replace("#", "");
420
+ if (hex.length === 6) {
421
+ acR = parseInt(hex.slice(0, 2), 16);
422
+ acG = parseInt(hex.slice(2, 4), 16);
423
+ acB = parseInt(hex.slice(4, 6), 16);
424
+ }
425
+ }
426
+ ctx.clearRect(0, 0, width, height);
427
+ ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
428
+ ctx.textBaseline = "top";
429
+ const t = time * speed;
430
+ const breatheAmp = breathe ? (Math.sin(t * 0.22) * 0.5 + 0.5) * 0.12 : 0;
431
+ for (let row = 0; row < rows; row++) {
432
+ for (let col = 0; col < cols; col++) {
433
+ const nx = col / cols;
434
+ const ny = row / rows;
435
+ const w1 = Math.sin(col * 0.08 + row * 0.05 + t * 0.6) * 0.5 + 0.5;
436
+ const w2 = Math.sin(col * 0.03 - row * 0.07 + t * 0.4) * 0.5 + 0.5;
437
+ const w3 = Math.sin(col * 0.05 + row * 0.03 + t * 0.8) * 0.5 + 0.5;
438
+ const sinePart = (w1 + w2 + w3) / 3;
439
+ const noiseScale = 0.045;
440
+ const noiseShift = t * 0.08;
441
+ const noisePart = fbm(col * noiseScale + noiseShift, row * noiseScale * 1.4 - noiseShift * 0.7) * 0.5 + 0.5;
442
+ const driftFreq = 0.06;
443
+ const driftPart = Math.sin((col + row * 0.65) * driftFreq + t * 1.1) * 0.5 + 0.5;
444
+ const wavePart = sinePart * 0.45 + noisePart * 0.35 + driftPart * 0.2 + breatheAmp;
445
+ const dxRaw = nx - mx;
446
+ const dyRaw = ny - my;
447
+ const distRaw = Math.sqrt(dxRaw * dxRaw + dyRaw * dyRaw);
448
+ let vortexBump = 0;
449
+ if (vortex && distRaw < 0.35) {
450
+ const angle = Math.atan2(dyRaw, dxRaw);
451
+ const swirl = Math.sin(angle * 4 + t * 2.2 - distRaw * 14);
452
+ const falloff = Math.max(0, 1 - distRaw / 0.35);
453
+ vortexBump = swirl * falloff * falloff * 0.22;
454
+ }
455
+ const proximity = Math.max(0, 1 - distRaw * mouseFalloff);
456
+ const intensity = wavePart * (1 - mouseInfluence) + (proximity + vortexBump * 0.5) * mouseInfluence + vortexBump * 0.15;
457
+ const clamped = Math.min(1, Math.max(0, intensity));
458
+ let finalIntensity = clamped;
459
+ if (sparkles && clamped > 0.72) {
460
+ const bucket = Math.floor(t * 8);
461
+ const sparkleSeed = hash2(col * 7 + bucket * 3, row * 11 + bucket);
462
+ if (sparkleSeed > 0.88) {
463
+ finalIntensity = Math.min(1, clamped + (sparkleSeed - 0.88) * 4);
464
+ }
465
+ }
466
+ const charIdx = Math.floor(finalIntensity * (chars.length - 1));
467
+ if (chars[charIdx] === " ") continue;
468
+ const alpha = 0.015 + finalIntensity * 0.07;
469
+ const isAccent = finalIntensity > accentThreshold;
470
+ if (isAccent) {
471
+ const accentAlpha = Math.min(lightMode ? 0.4 : 0.28, alpha * (lightMode ? 4 : 2.8));
472
+ ctx.fillStyle = `rgba(${acR},${acG},${acB},${accentAlpha})`;
473
+ } else if (baseColor) {
474
+ ctx.fillStyle = baseColor.replace("{a}", String(alpha));
475
+ } else if (lightMode) {
476
+ ctx.fillStyle = `rgba(0,0,0,${alpha * 1.3})`;
477
+ } else {
478
+ ctx.fillStyle = `rgba(255,255,255,${alpha})`;
479
+ }
480
+ ctx.fillText(chars[charIdx], col * charW, row * lineH);
481
+ }
482
+ }
483
+ }
484
+
485
+ // src/core/renderer.ts
285
486
  function imageToAsciiFrame(source, options, targetWidth, targetHeight) {
286
487
  const srcWidth = source instanceof HTMLVideoElement ? source.videoWidth : source.width;
287
488
  const srcHeight = source instanceof HTMLVideoElement ? source.videoHeight : source.height;
@@ -402,80 +603,10 @@ async function gifToAsciiFrames(buffer, options, targetWidth, targetHeight, onPr
402
603
  }
403
604
  return { frames, cols, rows, fps };
404
605
  }
405
- function smoothstep(t) {
406
- return t * t * (3 - 2 * t);
407
- }
408
- var _hoverResult = { scale: 1, offsetX: 0, offsetY: 0, glow: 0, colorBlend: 0, proximity: 0 };
409
- function computeHoverEffect(nx, ny, hoverX, hoverY, hoverIntensity, strength, cellW, cellH, effect = "spotlight", radiusFactor = 0.5) {
410
- const dx = nx - hoverX;
411
- const dy = ny - hoverY;
412
- const distSq = dx * dx + dy * dy;
413
- const radius = 0.08 + radiusFactor * 0.35 + strength * 0.04;
414
- if (distSq >= radius * radius) {
415
- _hoverResult.scale = 1;
416
- _hoverResult.offsetX = 0;
417
- _hoverResult.offsetY = 0;
418
- _hoverResult.glow = 0;
419
- _hoverResult.colorBlend = 0;
420
- _hoverResult.proximity = 0;
421
- return _hoverResult;
422
- }
423
- const dist = Math.sqrt(distSq);
424
- const t = 1 - dist / radius;
425
- const eased = smoothstep(t) * hoverIntensity;
426
- let scale = 1;
427
- let offsetX = 0;
428
- let offsetY = 0;
429
- let glow = 0;
430
- let colorBlend = 0;
431
- switch (effect) {
432
- case "spotlight": {
433
- scale = 1 + eased * strength * 1.8;
434
- const angle = Math.atan2(dy, dx);
435
- const pushForce = eased * eased * strength * 0.6;
436
- offsetX = Math.cos(angle) * pushForce * cellW;
437
- offsetY = Math.sin(angle) * pushForce * cellH;
438
- glow = eased * strength * 0.4;
439
- colorBlend = eased * eased * strength * 0.25;
440
- break;
441
- }
442
- case "magnify":
443
- scale = 1 + eased * strength * 2.5;
444
- glow = eased * strength * 0.15;
445
- break;
446
- case "repel": {
447
- scale = 1 + eased * strength * 0.3;
448
- const angle2 = Math.atan2(dy, dx);
449
- const push = eased * eased * strength * 1.2;
450
- offsetX = Math.cos(angle2) * push * cellW;
451
- offsetY = Math.sin(angle2) * push * cellH;
452
- break;
453
- }
454
- case "glow":
455
- glow = eased * strength * 0.8;
456
- colorBlend = eased * strength * 0.4;
457
- break;
458
- case "colorShift":
459
- scale = 1 + eased * strength * 0.4;
460
- glow = eased * strength * 0.2;
461
- colorBlend = eased * strength * 0.7;
462
- break;
463
- }
464
- _hoverResult.scale = scale;
465
- _hoverResult.offsetX = offsetX;
466
- _hoverResult.offsetY = offsetY;
467
- _hoverResult.glow = glow;
468
- _hoverResult.colorBlend = colorBlend;
469
- _hoverResult.proximity = eased;
470
- return _hoverResult;
471
- }
472
606
  function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, time = 0, hoverPos) {
473
607
  if (options.animationStyle === "waveField") {
474
608
  const mouseNorm = hoverPos ? { x: hoverPos.x, y: hoverPos.y } : { x: 0.5, y: 0.5 };
475
609
  const acHexWF = (options.accentColor || "#d4ff00").replace("#", "");
476
- parseInt(acHexWF.substring(0, 2), 16) || 212;
477
- parseInt(acHexWF.substring(2, 4), 16) || 255;
478
- parseInt(acHexWF.substring(4, 6), 16) || 0;
479
610
  renderWaveBackground(ctx, canvasWidth, canvasHeight, time, mouseNorm, {
480
611
  accentColor: `#${acHexWF}`,
481
612
  accentThreshold: 0.52,
@@ -719,6 +850,9 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
719
850
  }
720
851
  ctx.globalAlpha = 1;
721
852
  }
853
+
854
+ // src/core/embed-gen.ts
855
+ var EMBED_CDN_VERSION = "1.0.17";
722
856
  function serializeFrame(frame, fullColor) {
723
857
  const rows = frame.length;
724
858
  const cols = rows > 0 ? frame[0].length : 0;
@@ -742,6 +876,60 @@ function serializeFrame(frame, fullColor) {
742
876
  for (let j = 0; j < buf.length; j++) binary += String.fromCharCode(buf[j]);
743
877
  return btoa(binary);
744
878
  }
879
+ function buildEmbedOpts(options, rows, cols, width, height, fps, animated) {
880
+ const o = {
881
+ r: rows,
882
+ c: cols,
883
+ w: width,
884
+ h: height,
885
+ cs: options.charset,
886
+ cm: options.colorMode,
887
+ as: options.animationStyle,
888
+ sp: options.animationSpeed,
889
+ inv: options.invert,
890
+ hs: options.hoverStrength,
891
+ hr: options.hoverRadius,
892
+ he: options.hoverEffect,
893
+ hc: options.hoverColor,
894
+ dr: options.dotSizeRatio,
895
+ dots: options.renderMode === "dots"
896
+ };
897
+ if (options.colorMode === "accent") o.ac = options.accentColor;
898
+ if (fps !== void 0) o.fps = fps;
899
+ if (animated) o.anim = true;
900
+ return JSON.stringify(o);
901
+ }
902
+ var CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/asciify-engine@${EMBED_CDN_VERSION}/dist/embed.js" async></script>`;
903
+ function generateEmbedCode(frame, options, width, height) {
904
+ const rows = frame.length;
905
+ if (rows === 0) return "";
906
+ const cols = frame[0].length;
907
+ const isFullColor = options.colorMode === "fullcolor";
908
+ const data = serializeFrame(frame, isFullColor);
909
+ const id = `ar-${Math.random().toString(36).slice(2, 9)}`;
910
+ const opts = buildEmbedOpts(options, rows, cols, width, height);
911
+ return `<!-- Asciify Embed -->
912
+ <canvas id="${id}" data-asciify-opts='${opts}' width="${width}" height="${height}"></canvas>
913
+ <script type="application/json" id="${id}-d">"${data}"</script>
914
+ ${CDN_SCRIPT}
915
+ <!-- /Asciify Embed -->`;
916
+ }
917
+ function generateAnimatedEmbedCode(frames, options, fps, width, height) {
918
+ if (frames.length === 0) return "";
919
+ const rows = frames[0].length;
920
+ const cols = frames[0][0].length;
921
+ const isFullColor = options.colorMode === "fullcolor";
922
+ const allData = frames.map((f) => serializeFrame(f, isFullColor));
923
+ const id = `ar-${Math.random().toString(36).slice(2, 9)}`;
924
+ const opts = buildEmbedOpts(options, rows, cols, width, height, fps, true);
925
+ return `<!-- Asciify Animated Embed -->
926
+ <canvas id="${id}" data-asciify-opts='${opts}' width="${width}" height="${height}"></canvas>
927
+ <script type="application/json" id="${id}-d">${JSON.stringify(allData)}</script>
928
+ ${CDN_SCRIPT}
929
+ <!-- /Asciify Animated Embed -->`;
930
+ }
931
+
932
+ // src/core/simple-api.ts
745
933
  async function asciify(source, canvas, { fontSize = 10, style = "classic", options = {} } = {}) {
746
934
  let el;
747
935
  if (typeof source === "string") {
@@ -832,173 +1020,8 @@ async function asciifyVideo(source, canvas, { fontSize = 10, style = "classic",
832
1020
  cancelAnimationFrame(animId);
833
1021
  };
834
1022
  }
835
- var EMBED_CDN_VERSION = "1.0.17";
836
- function buildEmbedOpts(options, rows, cols, width, height, fps, animated) {
837
- const o = {
838
- r: rows,
839
- c: cols,
840
- w: width,
841
- h: height,
842
- cs: options.charset,
843
- cm: options.colorMode,
844
- as: options.animationStyle,
845
- sp: options.animationSpeed,
846
- inv: options.invert,
847
- hs: options.hoverStrength,
848
- hr: options.hoverRadius,
849
- he: options.hoverEffect,
850
- hc: options.hoverColor,
851
- dr: options.dotSizeRatio,
852
- dots: options.renderMode === "dots"
853
- };
854
- if (options.colorMode === "accent") o.ac = options.accentColor;
855
- if (fps !== void 0) o.fps = fps;
856
- if (animated) o.anim = true;
857
- return JSON.stringify(o);
858
- }
859
- var CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/asciify-engine@${EMBED_CDN_VERSION}/dist/embed.js" async></script>`;
860
- function generateEmbedCode(frame, options, width, height) {
861
- const rows = frame.length;
862
- if (rows === 0) return "";
863
- const cols = frame[0].length;
864
- const isFullColor = options.colorMode === "fullcolor";
865
- const data = serializeFrame(frame, isFullColor);
866
- const id = `ar-${Math.random().toString(36).slice(2, 9)}`;
867
- const opts = buildEmbedOpts(options, rows, cols, width, height);
868
- return `<!-- Asciify Embed -->
869
- <canvas id="${id}" data-asciify-opts='${opts}' width="${width}" height="${height}"></canvas>
870
- <script type="application/json" id="${id}-d">"${data}"</script>
871
- ${CDN_SCRIPT}
872
- <!-- /Asciify Embed -->`;
873
- }
874
- function generateAnimatedEmbedCode(frames, options, fps, width, height) {
875
- if (frames.length === 0) return "";
876
- const rows = frames[0].length;
877
- const cols = frames[0][0].length;
878
- const isFullColor = options.colorMode === "fullcolor";
879
- const allData = frames.map((f) => serializeFrame(f, isFullColor));
880
- const id = `ar-${Math.random().toString(36).slice(2, 9)}`;
881
- const opts = buildEmbedOpts(options, rows, cols, width, height, fps, true);
882
- return `<!-- Asciify Animated Embed -->
883
- <canvas id="${id}" data-asciify-opts='${opts}' width="${width}" height="${height}"></canvas>
884
- <script type="application/json" id="${id}-d">${JSON.stringify(allData)}</script>
885
- ${CDN_SCRIPT}
886
- <!-- /Asciify Animated Embed -->`;
887
- }
888
- function _fade(t) {
889
- return t * t * t * (t * (t * 6 - 15) + 10);
890
- }
891
- function _lerp(a, b, t) {
892
- return a + (b - a) * t;
893
- }
894
- function _hash2(ix, iy) {
895
- let n = ix * 127 + iy * 311;
896
- n = n >> 13 ^ n;
897
- return (n * (n * n * 15731 + 789221) + 1376312589 & 2147483647) / 1073741823 - 1;
898
- }
899
- function _vnoise(x, y) {
900
- const ix = Math.floor(x), iy = Math.floor(y);
901
- const fx = x - ix, fy = y - iy;
902
- const ux = _fade(fx), uy = _fade(fy);
903
- const v00 = _hash2(ix, iy);
904
- const v10 = _hash2(ix + 1, iy);
905
- const v01 = _hash2(ix, iy + 1);
906
- const v11 = _hash2(ix + 1, iy + 1);
907
- return _lerp(_lerp(v00, v10, ux), _lerp(v01, v11, ux), uy);
908
- }
909
- function _fbm(x, y) {
910
- return (_vnoise(x, y) * 0.5 + _vnoise(x * 2.1, y * 2.1) * 0.25 + _vnoise(x * 4.3, y * 4.3) * 0.125) / 0.875;
911
- }
912
- function renderWaveBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
913
- const {
914
- fontSize = 13,
915
- charAspect = 0.62,
916
- lineHeightRatio = 1.4,
917
- chars = " .:-=+*#%@",
918
- baseColor = null,
919
- accentColor = "#d4ff00",
920
- accentThreshold = 0.52,
921
- mouseInfluence = 0.55,
922
- mouseFalloff = 2.8,
923
- speed = 1,
924
- vortex = true,
925
- sparkles = true,
926
- breathe = true,
927
- lightMode = false
928
- } = options;
929
- const charW = fontSize * charAspect;
930
- const lineH = fontSize * lineHeightRatio;
931
- const cols = Math.ceil(width / charW);
932
- const rows = Math.ceil(height / lineH);
933
- const mx = mousePos.x;
934
- const my = mousePos.y;
935
- let acR = 212, acG = 255, acB = 0;
936
- if (accentColor) {
937
- const hex = accentColor.replace("#", "");
938
- if (hex.length === 6) {
939
- acR = parseInt(hex.slice(0, 2), 16);
940
- acG = parseInt(hex.slice(2, 4), 16);
941
- acB = parseInt(hex.slice(4, 6), 16);
942
- }
943
- }
944
- ctx.clearRect(0, 0, width, height);
945
- ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
946
- ctx.textBaseline = "top";
947
- const t = time * speed;
948
- const breatheAmp = breathe ? (Math.sin(t * 0.22) * 0.5 + 0.5) * 0.12 : 0;
949
- for (let row = 0; row < rows; row++) {
950
- for (let col = 0; col < cols; col++) {
951
- const nx = col / cols;
952
- const ny = row / rows;
953
- const w1 = Math.sin(col * 0.08 + row * 0.05 + t * 0.6) * 0.5 + 0.5;
954
- const w2 = Math.sin(col * 0.03 - row * 0.07 + t * 0.4) * 0.5 + 0.5;
955
- const w3 = Math.sin(col * 0.05 + row * 0.03 + t * 0.8) * 0.5 + 0.5;
956
- const sinePart = (w1 + w2 + w3) / 3;
957
- const noiseScale = 0.045;
958
- const noiseShift = t * 0.08;
959
- const noisePart = _fbm(col * noiseScale + noiseShift, row * noiseScale * 1.4 - noiseShift * 0.7) * 0.5 + 0.5;
960
- const driftFreq = 0.06;
961
- const driftPart = Math.sin((col + row * 0.65) * driftFreq + t * 1.1) * 0.5 + 0.5;
962
- const wavePart = sinePart * 0.45 + noisePart * 0.35 + driftPart * 0.2 + breatheAmp;
963
- const dxRaw = nx - mx;
964
- const dyRaw = ny - my;
965
- const distRaw = Math.sqrt(dxRaw * dxRaw + dyRaw * dyRaw);
966
- let vortexBump = 0;
967
- if (vortex && distRaw < 0.35) {
968
- const angle = Math.atan2(dyRaw, dxRaw);
969
- const swirl = Math.sin(angle * 4 + t * 2.2 - distRaw * 14);
970
- const falloff = Math.max(0, 1 - distRaw / 0.35);
971
- vortexBump = swirl * falloff * falloff * 0.22;
972
- }
973
- const proximity = Math.max(0, 1 - distRaw * mouseFalloff);
974
- const intensity = wavePart * (1 - mouseInfluence) + (proximity + vortexBump * 0.5) * mouseInfluence + vortexBump * 0.15;
975
- const clamped = Math.min(1, Math.max(0, intensity));
976
- let finalIntensity = clamped;
977
- if (sparkles && clamped > 0.72) {
978
- const bucket = Math.floor(t * 8);
979
- const sparkleSeed = _hash2(col * 7 + bucket * 3, row * 11 + bucket);
980
- if (sparkleSeed > 0.88) {
981
- finalIntensity = Math.min(1, clamped + (sparkleSeed - 0.88) * 4);
982
- }
983
- }
984
- const charIdx = Math.floor(finalIntensity * (chars.length - 1));
985
- if (chars[charIdx] === " ") continue;
986
- const alpha = 0.015 + finalIntensity * 0.07;
987
- const isAccent = finalIntensity > accentThreshold;
988
- if (isAccent) {
989
- const accentAlpha = Math.min(lightMode ? 0.4 : 0.28, alpha * (lightMode ? 4 : 2.8));
990
- ctx.fillStyle = `rgba(${acR},${acG},${acB},${accentAlpha})`;
991
- } else if (baseColor) {
992
- ctx.fillStyle = baseColor.replace("{a}", String(alpha));
993
- } else if (lightMode) {
994
- ctx.fillStyle = `rgba(0,0,0,${alpha * 1.3})`;
995
- } else {
996
- ctx.fillStyle = `rgba(255,255,255,${alpha})`;
997
- }
998
- ctx.fillText(chars[charIdx], col * charW, row * lineH);
999
- }
1000
- }
1001
- }
1023
+
1024
+ // src/backgrounds/rain.ts
1002
1025
  function renderRainBackground(ctx, width, height, time, options = {}) {
1003
1026
  const {
1004
1027
  fontSize = 13,
@@ -1024,7 +1047,7 @@ function renderRainBackground(ctx, width, height, time, options = {}) {
1024
1047
  bb = 0;
1025
1048
  }
1026
1049
  if (color) {
1027
- const p = _parseColor(color);
1050
+ const p = parseColor(color);
1028
1051
  if (p) {
1029
1052
  br = p.r;
1030
1053
  bg = p.g;
@@ -1032,7 +1055,7 @@ function renderRainBackground(ctx, width, height, time, options = {}) {
1032
1055
  }
1033
1056
  }
1034
1057
  let acR = 212, acG = 255, acB = 0;
1035
- const ap = _parseColor(accentColor);
1058
+ const ap = parseColor(accentColor);
1036
1059
  if (ap) {
1037
1060
  acR = ap.r;
1038
1061
  acG = ap.g;
@@ -1040,28 +1063,30 @@ function renderRainBackground(ctx, width, height, time, options = {}) {
1040
1063
  }
1041
1064
  const period = rows + tailLength;
1042
1065
  for (let c = 0; c < cols; c++) {
1043
- if (_hash2(c * 17, 3) > density) continue;
1044
- const colSpeed = (0.5 + _hash2(c * 31, 7) * 1.5) * speed;
1045
- const phase = _hash2(c * 13, 11) * period;
1066
+ if (hash2(c * 17, 3) > density) continue;
1067
+ const colSpeed = (0.5 + hash2(c * 31, 7) * 1.5) * speed;
1068
+ const phase = hash2(c * 13, 11) * period;
1046
1069
  const headRow = Math.floor((time * colSpeed * 7 + phase) % period);
1047
1070
  const x = c * charW;
1048
1071
  for (let k = 0; k <= tailLength; k++) {
1049
1072
  const row = headRow - (tailLength - k);
1050
1073
  if (row < 0 || row >= rows) continue;
1051
1074
  const y = row * lineH;
1052
- const charSeed = _hash2(c * 53 + Math.floor(time * 5 + k), row * 7);
1075
+ const charSeed = hash2(c * 53 + Math.floor(time * 5 + k), row * 7);
1053
1076
  const ch = chars[Math.floor(charSeed * chars.length)];
1054
- const t = k / tailLength;
1077
+ const tRatio = k / tailLength;
1055
1078
  if (k === tailLength) {
1056
1079
  ctx.fillStyle = `rgba(${acR},${acG},${acB},${lightMode ? 0.7 : 0.85})`;
1057
1080
  } else {
1058
- const alpha = lightMode ? t * 0.22 : t * 0.15;
1081
+ const alpha = lightMode ? tRatio * 0.22 : tRatio * 0.15;
1059
1082
  ctx.fillStyle = `rgba(${br},${bg},${bb},${alpha})`;
1060
1083
  }
1061
1084
  ctx.fillText(ch, x, y);
1062
1085
  }
1063
1086
  }
1064
1087
  }
1088
+
1089
+ // src/backgrounds/stars.ts
1065
1090
  function renderStarsBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
1066
1091
  const {
1067
1092
  fontSize = 14,
@@ -1085,7 +1110,7 @@ function renderStarsBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1085
1110
  bb = 0;
1086
1111
  }
1087
1112
  if (color) {
1088
- const p = _parseColor(color);
1113
+ const p = parseColor(color);
1089
1114
  if (p) {
1090
1115
  br = p.r;
1091
1116
  bg = p.g;
@@ -1093,7 +1118,7 @@ function renderStarsBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1093
1118
  }
1094
1119
  }
1095
1120
  let acR = 212, acG = 255, acB = 0;
1096
- const ap = _parseColor(accentColor);
1121
+ const ap = parseColor(accentColor);
1097
1122
  if (ap) {
1098
1123
  acR = ap.r;
1099
1124
  acG = ap.g;
@@ -1102,9 +1127,9 @@ function renderStarsBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1102
1127
  const charArr = chars.replace(/ /g, "").split("");
1103
1128
  if (charArr.length === 0) return;
1104
1129
  for (let i = 0; i < count; i++) {
1105
- const angle = _hash2(i * 17, 3) * Math.PI * 2;
1106
- const baseSpd = 0.15 + _hash2(i * 31, 7) * 0.85;
1107
- const phase = _hash2(i * 13, 11);
1130
+ const angle = hash2(i * 17, 3) * Math.PI * 2;
1131
+ const baseSpd = 0.15 + hash2(i * 31, 7) * 0.85;
1132
+ const phase = hash2(i * 13, 11);
1108
1133
  const r = (time * baseSpd * speed * 0.22 + phase) % 1;
1109
1134
  const x = cx + Math.cos(angle) * r * maxR;
1110
1135
  const y = cy + Math.sin(angle) * r * maxR;
@@ -1120,68 +1145,81 @@ function renderStarsBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1120
1145
  }
1121
1146
  ctx.textAlign = "left";
1122
1147
  }
1148
+
1149
+ // src/backgrounds/pulse.ts
1123
1150
  function renderPulseBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
1124
1151
  const {
1125
- fontSize = 13,
1126
- chars = " .:-=+*#%@",
1127
- accentColor = "#d4ff00",
1152
+ fontSize = 14,
1153
+ chars = ". \xB7 \u25CB \u25CE \u25CF",
1154
+ accentColor = "#00ffcc",
1128
1155
  color,
1129
- rings = 6,
1156
+ rings = 5,
1130
1157
  speed = 1,
1131
1158
  sharpness = 4,
1132
1159
  lightMode = false
1133
1160
  } = options;
1134
- const charW = fontSize * 0.62;
1135
- const lineH = fontSize * 1.4;
1136
- const cols = Math.ceil(width / charW);
1137
- const rows = Math.ceil(height / lineH);
1138
1161
  ctx.clearRect(0, 0, width, height);
1139
- ctx.font = `${fontSize}px monospace`;
1140
- ctx.textBaseline = "top";
1141
- const maxR = Math.sqrt(width * width + height * height) * 0.5;
1142
- const ox = mousePos.x * width;
1143
- const oy = mousePos.y * height;
1144
- let br = 255, bg2 = 255, bb = 255;
1162
+ ctx.textBaseline = "middle";
1163
+ ctx.textAlign = "center";
1164
+ const cx = width * mousePos.x;
1165
+ const cy = height * mousePos.y;
1166
+ const maxDist = Math.sqrt(cx * cx + cy * cy) * 1.6 + Math.sqrt(width * width + height * height) * 0.2;
1167
+ let br = 255, bg = 255, bb = 255;
1145
1168
  if (lightMode) {
1146
1169
  br = 0;
1147
- bg2 = 0;
1170
+ bg = 0;
1148
1171
  bb = 0;
1149
1172
  }
1150
1173
  if (color) {
1151
- const p = _parseColor(color);
1174
+ const p = parseColor(color);
1152
1175
  if (p) {
1153
1176
  br = p.r;
1154
- bg2 = p.g;
1177
+ bg = p.g;
1155
1178
  bb = p.b;
1156
1179
  }
1157
1180
  }
1158
- let acR = 212, acG = 255, acB = 0;
1159
- const ap = _parseColor(accentColor);
1181
+ let acR = 0, acG = 255, acB = 204;
1182
+ const ap = parseColor(accentColor);
1160
1183
  if (ap) {
1161
1184
  acR = ap.r;
1162
1185
  acG = ap.g;
1163
1186
  acB = ap.b;
1164
1187
  }
1165
- const t = time * speed;
1188
+ const charArr = chars.replace(/ /g, "").split("");
1189
+ if (charArr.length === 0) return;
1190
+ const cols = Math.ceil(width / fontSize);
1191
+ const rows = Math.ceil(height / fontSize);
1166
1192
  for (let row = 0; row < rows; row++) {
1167
1193
  for (let col = 0; col < cols; col++) {
1168
- const px = col * charW + charW * 0.5;
1169
- const py = row * lineH + lineH * 0.5;
1170
- const dist = Math.sqrt((px - ox) ** 2 + (py - oy) ** 2);
1171
- const norm = dist / maxR;
1172
- const phase = (norm * rings - t * 1.4) % 1;
1173
- const wave = Math.pow(Math.max(0, Math.cos(phase * Math.PI * 2)), sharpness);
1174
- if (wave < 0.02) continue;
1175
- const charIdx = Math.floor(wave * (chars.length - 1));
1176
- const ch = chars[charIdx];
1177
- if (ch === " ") continue;
1178
- const isAccent = wave > 0.75;
1179
- const alpha = lightMode ? wave * 0.22 : wave * 0.14;
1180
- ctx.fillStyle = isAccent ? `rgba(${acR},${acG},${acB},${lightMode ? 0.5 : 0.35})` : `rgba(${br},${bg2},${bb},${alpha})`;
1181
- ctx.fillText(ch, col * charW, row * lineH);
1194
+ const px = col * fontSize + fontSize * 0.5;
1195
+ const py = row * fontSize + fontSize * 0.5;
1196
+ const dx = px - cx;
1197
+ const dy = py - cy;
1198
+ const dist = Math.sqrt(dx * dx + dy * dy);
1199
+ const norm = dist / maxDist;
1200
+ let totalIntensity = 0;
1201
+ for (let r = 0; r < rings; r++) {
1202
+ const phase = r / rings;
1203
+ const t = (time * speed * 0.38 + phase) % 1;
1204
+ const ringDist = Math.abs(norm - t);
1205
+ const ringNorm = Math.max(0, 1 - ringDist * maxDist / (fontSize * (12 - sharpness)));
1206
+ totalIntensity += Math.cos(ringNorm * Math.PI * 0.5) * ringNorm;
1207
+ }
1208
+ totalIntensity = Math.min(1, totalIntensity);
1209
+ if (totalIntensity < 0.02) continue;
1210
+ const isAccent = totalIntensity > 0.6;
1211
+ ctx.font = `${fontSize}px monospace`;
1212
+ const charIdx = Math.floor(totalIntensity * (charArr.length - 1));
1213
+ const ch = charArr[Math.min(charIdx, charArr.length - 1)];
1214
+ const alpha = lightMode ? totalIntensity * 0.32 : totalIntensity * 0.22;
1215
+ ctx.fillStyle = isAccent ? `rgba(${acR},${acG},${acB},${Math.min(lightMode ? 0.5 : 0.4, totalIntensity * 0.55)})` : `rgba(${br},${bg},${bb},${alpha})`;
1216
+ ctx.fillText(ch, px, py);
1182
1217
  }
1183
1218
  }
1219
+ ctx.textAlign = "left";
1184
1220
  }
1221
+
1222
+ // src/backgrounds/noise.ts
1185
1223
  function renderNoiseBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
1186
1224
  const {
1187
1225
  fontSize = 14,
@@ -1209,7 +1247,7 @@ function renderNoiseBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1209
1247
  bb = 0;
1210
1248
  }
1211
1249
  if (color) {
1212
- const p = _parseColor(color);
1250
+ const p = parseColor(color);
1213
1251
  if (p) {
1214
1252
  br = p.r;
1215
1253
  bgc = p.g;
@@ -1217,7 +1255,7 @@ function renderNoiseBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1217
1255
  }
1218
1256
  }
1219
1257
  let acR = 212, acG = 255, acB = 0;
1220
- const ap = _parseColor(accentColor);
1258
+ const ap = parseColor(accentColor);
1221
1259
  if (ap) {
1222
1260
  acR = ap.r;
1223
1261
  acG = ap.g;
@@ -1229,7 +1267,7 @@ function renderNoiseBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1229
1267
  const fbmN = (x, y) => {
1230
1268
  let v = 0, amp = 0.5, freq = 1, norm = 0;
1231
1269
  for (let o = 0; o < oct; o++) {
1232
- v += _vnoise(x * freq, y * freq) * amp;
1270
+ v += vnoise(x * freq, y * freq) * amp;
1233
1271
  norm += amp;
1234
1272
  amp *= 0.5;
1235
1273
  freq *= 2.1;
@@ -1259,6 +1297,8 @@ function renderNoiseBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1259
1297
  }
1260
1298
  }
1261
1299
  }
1300
+
1301
+ // src/backgrounds/grid.ts
1262
1302
  function renderGridBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
1263
1303
  const {
1264
1304
  fontSize = 12,
@@ -1285,7 +1325,7 @@ function renderGridBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1285
1325
  bb = 0;
1286
1326
  }
1287
1327
  if (color) {
1288
- const p = _parseColor(color);
1328
+ const p = parseColor(color);
1289
1329
  if (p) {
1290
1330
  br = p.r;
1291
1331
  bgv = p.g;
@@ -1293,7 +1333,7 @@ function renderGridBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1293
1333
  }
1294
1334
  }
1295
1335
  let acR = 212, acG = 255, acB = 0;
1296
- const ap = _parseColor(accentColor);
1336
+ const ap = parseColor(accentColor);
1297
1337
  if (ap) {
1298
1338
  acR = ap.r;
1299
1339
  acG = ap.g;
@@ -1305,7 +1345,7 @@ function renderGridBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1305
1345
  const ny = row / rows;
1306
1346
  const scanPhase = ((ny * bands - t * 0.5) % 1 + 1) % 1;
1307
1347
  const bandIntensity = Math.max(0, 1 - scanPhase / bandWidth);
1308
- const gridSeed = _hash2(col * 3, row * 7);
1348
+ const gridSeed = hash2(col * 3, row * 7);
1309
1349
  const gridBase = (gridSeed * 0.5 + 0.5) * 0.35;
1310
1350
  let glitchBump = 0;
1311
1351
  if (glitch) {
@@ -1313,7 +1353,7 @@ function renderGridBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1313
1353
  const dy = ny - mousePos.y;
1314
1354
  const d = Math.sqrt(dx * dx + dy * dy);
1315
1355
  if (d < 0.18) {
1316
- const g = _hash2(col * 11 + Math.floor(t * 12), row * 5);
1356
+ const g = hash2(col * 11 + Math.floor(t * 12), row * 5);
1317
1357
  glitchBump = Math.max(0, 1 - d / 0.18) * (g > 0.5 ? g - 0.3 : 0);
1318
1358
  }
1319
1359
  }
@@ -1328,6 +1368,8 @@ function renderGridBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1328
1368
  }
1329
1369
  }
1330
1370
  }
1371
+
1372
+ // src/backgrounds/aurora.ts
1331
1373
  function renderAuroraBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
1332
1374
  const {
1333
1375
  fontSize = 14,
@@ -1354,7 +1396,7 @@ function renderAuroraBackground(ctx, width, height, time, mousePos = { x: 0.5, y
1354
1396
  cb = 0;
1355
1397
  }
1356
1398
  if (color) {
1357
- const p = _parseColor(color);
1399
+ const p = parseColor(color);
1358
1400
  if (p) {
1359
1401
  cr = p.r;
1360
1402
  cg = p.g;
@@ -1362,7 +1404,7 @@ function renderAuroraBackground(ctx, width, height, time, mousePos = { x: 0.5, y
1362
1404
  }
1363
1405
  }
1364
1406
  let acR = 212, acG = 255, acB = 0;
1365
- const ap = _parseColor(accentColor);
1407
+ const ap = parseColor(accentColor);
1366
1408
  if (ap) {
1367
1409
  acR = ap.r;
1368
1410
  acG = ap.g;
@@ -1371,19 +1413,14 @@ function renderAuroraBackground(ctx, width, height, time, mousePos = { x: 0.5, y
1371
1413
  const t = time * speed;
1372
1414
  const layerParams = [];
1373
1415
  for (let l = 0; l < layers; l++) {
1374
- const seed = _hash2(l * 17, l * 31 + 7);
1375
- const seed2 = _hash2(l * 23 + 5, l * 11);
1416
+ const seed = hash2(l * 17, l * 31 + 7);
1417
+ const seed2 = hash2(l * 23 + 5, l * 11);
1376
1418
  layerParams.push({
1377
1419
  fx: 0.8 + seed * 2.2,
1378
- // x spatial frequency
1379
1420
  fy: 1.2 + seed2 * 1.8,
1380
- // y spatial frequency
1381
1421
  phase: seed * Math.PI * 4,
1382
- // phase offset
1383
- dt: (0.3 + _hash2(l * 7, l * 13 + 3) * 0.5) * (l % 2 === 0 ? 1 : -1),
1384
- // drift speed & direction
1385
- amp: 0.55 + _hash2(l * 29, l * 3) * 0.45
1386
- // amplitude weight
1422
+ dt: (0.3 + hash2(l * 7, l * 13 + 3) * 0.5) * (l % 2 === 0 ? 1 : -1),
1423
+ amp: 0.55 + hash2(l * 29, l * 3) * 0.45
1387
1424
  });
1388
1425
  }
1389
1426
  for (let row = 0; row < rows; row++) {
@@ -1417,6 +1454,8 @@ function renderAuroraBackground(ctx, width, height, time, mousePos = { x: 0.5, y
1417
1454
  }
1418
1455
  }
1419
1456
  }
1457
+
1458
+ // src/backgrounds/silk.ts
1420
1459
  function renderSilkBackground(ctx, width, height, time, options = {}) {
1421
1460
  const {
1422
1461
  fontSize = 13,
@@ -1441,7 +1480,7 @@ function renderSilkBackground(ctx, width, height, time, options = {}) {
1441
1480
  cb = 0;
1442
1481
  }
1443
1482
  if (color) {
1444
- const p = _parseColor(color);
1483
+ const p = parseColor(color);
1445
1484
  if (p) {
1446
1485
  cr = p.r;
1447
1486
  cg = p.g;
@@ -1449,7 +1488,7 @@ function renderSilkBackground(ctx, width, height, time, options = {}) {
1449
1488
  }
1450
1489
  }
1451
1490
  let acR = 212, acG = 255, acB = 0;
1452
- const ap = _parseColor(accentColor);
1491
+ const ap = parseColor(accentColor);
1453
1492
  if (ap) {
1454
1493
  acR = ap.r;
1455
1494
  acG = ap.g;
@@ -1464,12 +1503,12 @@ function renderSilkBackground(ctx, width, height, time, options = {}) {
1464
1503
  let angleSum = 0;
1465
1504
  let intensitySum = 0;
1466
1505
  for (let l = 0; l < layers; l++) {
1467
- const ls = _hash2(l * 13, l * 7 + 3);
1468
- const ls2 = _hash2(l * 29, l * 11 + 1);
1506
+ const ls = hash2(l * 13, l * 7 + 3);
1507
+ const ls2 = hash2(l * 29, l * 11 + 1);
1469
1508
  const fx = 1.1 + ls * 2.4;
1470
1509
  const fy = 0.9 + ls2 * 2;
1471
1510
  const ph = ls * Math.PI * 6;
1472
- const dr = (0.2 + _hash2(l * 41, l * 17) * 0.5) * (l % 2 === 0 ? 1 : -1.3);
1511
+ const dr = (0.2 + hash2(l * 41, l * 17) * 0.5) * (l % 2 === 0 ? 1 : -1.3);
1473
1512
  const u = Math.sin(nx * fx * Math.PI * 2 + t * dr + ph);
1474
1513
  const v = Math.cos(ny * fy * Math.PI * 2 + t * dr * 0.6 + ph * 1.7);
1475
1514
  const cross = Math.sin(nx * fy * Math.PI * turbulence + ny * fx * Math.PI * turbulence + t * dr * 0.4);
@@ -1489,6 +1528,8 @@ function renderSilkBackground(ctx, width, height, time, options = {}) {
1489
1528
  }
1490
1529
  }
1491
1530
  }
1531
+
1532
+ // src/backgrounds/void.ts
1492
1533
  function renderVoidBackground(ctx, width, height, time, mousePos = { x: 0.5, y: 0.5 }, options = {}) {
1493
1534
  const {
1494
1535
  fontSize = 13,
@@ -1515,7 +1556,7 @@ function renderVoidBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1515
1556
  cb = 0;
1516
1557
  }
1517
1558
  if (color) {
1518
- const p = _parseColor(color);
1559
+ const p = parseColor(color);
1519
1560
  if (p) {
1520
1561
  cr = p.r;
1521
1562
  cg = p.g;
@@ -1523,7 +1564,7 @@ function renderVoidBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1523
1564
  }
1524
1565
  }
1525
1566
  let acR = 212, acG = 255, acB = 0;
1526
- const ap = _parseColor(accentColor);
1567
+ const ap = parseColor(accentColor);
1527
1568
  if (ap) {
1528
1569
  acR = ap.r;
1529
1570
  acG = ap.g;
@@ -1539,25 +1580,20 @@ function renderVoidBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1539
1580
  const dist = Math.sqrt(dx * dx + dy * dy);
1540
1581
  const r = dist / radius;
1541
1582
  if (r > 1) {
1542
- const outerNoise = _hash2(col * 3, row * 7) * Math.max(0, 1 - (r - 1) * 3);
1583
+ const outerNoise = hash2(col * 3, row * 7) * Math.max(0, 1 - (r - 1) * 3);
1543
1584
  if (outerNoise < 0.62) continue;
1544
1585
  const alpha2 = outerNoise * (lightMode ? 0.05 : 0.04);
1545
- const ch2 = chars[1];
1546
1586
  ctx.fillStyle = `rgba(${cr},${cg},${cb},${alpha2})`;
1547
- ctx.fillText(ch2, col * charW, row * lineH);
1587
+ ctx.fillText(chars[1], col * charW, row * lineH);
1548
1588
  continue;
1549
1589
  }
1550
- const baseAngle = Math.atan2(dy, dx * aspect);
1551
- const swirlAmt = (1 - r) * swirl;
1552
- const angle = baseAngle + swirlAmt + t * 0.4;
1553
1590
  const pulseRing = Math.max(0, 1 - Math.abs(r - (0.15 + 0.12 * Math.sin(t * 1.1))) / 0.07);
1554
1591
  const gravity = Math.pow(1 - r, 2.2);
1555
1592
  const intensity = Math.min(1, gravity + pulseRing * 0.6);
1556
1593
  if (intensity < 0.06) continue;
1557
- const angleIdx = Math.floor((angle % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2) / (Math.PI * 2) * 4);
1558
1594
  const densityI = Math.floor(intensity * (chars.length - 1));
1559
1595
  const charIdx = Math.min(chars.length - 1, densityI);
1560
- const ch = chars[charIdx + (angleIdx > 2 && densityI > 2 ? 0 : 0)];
1596
+ const ch = chars[charIdx];
1561
1597
  const isAccent = pulseRing > 0.35 || r < 0.08;
1562
1598
  const alpha = lightMode ? intensity * 0.22 : intensity * 0.18;
1563
1599
  ctx.fillStyle = isAccent ? `rgba(${acR},${acG},${acB},${lightMode ? 0.55 : 0.38})` : `rgba(${cr},${cg},${cb},${alpha})`;
@@ -1565,6 +1601,8 @@ function renderVoidBackground(ctx, width, height, time, mousePos = { x: 0.5, y:
1565
1601
  }
1566
1602
  }
1567
1603
  }
1604
+
1605
+ // src/backgrounds/morph.ts
1568
1606
  function renderMorphBackground(ctx, width, height, time, options = {}) {
1569
1607
  const {
1570
1608
  fontSize = 14,
@@ -1589,7 +1627,7 @@ function renderMorphBackground(ctx, width, height, time, options = {}) {
1589
1627
  cb = 0;
1590
1628
  }
1591
1629
  if (color) {
1592
- const p = _parseColor(color);
1630
+ const p = parseColor(color);
1593
1631
  if (p) {
1594
1632
  cr = p.r;
1595
1633
  cg = p.g;
@@ -1597,24 +1635,24 @@ function renderMorphBackground(ctx, width, height, time, options = {}) {
1597
1635
  }
1598
1636
  }
1599
1637
  let acR = 212, acG = 255, acB = 0;
1600
- const ap = _parseColor(accentColor);
1638
+ const ap = parseColor(accentColor);
1601
1639
  if (ap) {
1602
1640
  acR = ap.r;
1603
1641
  acG = ap.g;
1604
1642
  acB = ap.b;
1605
1643
  }
1606
1644
  const t = time * speed;
1645
+ const maxV = Array.from({ length: harmonics }, (_, h) => 1 / (h + 1)).reduce((a, b) => a + b, 0);
1607
1646
  for (let row = 0; row < rows; row++) {
1608
1647
  for (let col = 0; col < cols; col++) {
1609
1648
  let v = 0;
1610
1649
  for (let h = 0; h < harmonics; h++) {
1611
- const fBase = _hash2(col * (h + 3) + 7, row * (h + 5) + 11);
1650
+ const fBase = hash2(col * (h + 3) + 7, row * (h + 5) + 11);
1612
1651
  const fineF = 0.18 + fBase * 1.4;
1613
- const phase = _hash2(col * (h + 7), row * (h + 9) + 3) * Math.PI * 2;
1652
+ const phase = hash2(col * (h + 7), row * (h + 9) + 3) * Math.PI * 2;
1614
1653
  const weight = 1 / (h + 1);
1615
1654
  v += Math.sin(t * fineF + phase) * weight;
1616
1655
  }
1617
- const maxV = Array.from({ length: harmonics }, (_, h) => 1 / (h + 1)).reduce((a, b) => a + b, 0);
1618
1656
  const norm = (v / maxV + 1) * 0.5;
1619
1657
  if (norm < 0.28) continue;
1620
1658
  const remapped = (norm - 0.28) / 0.72;
@@ -1627,6 +1665,8 @@ function renderMorphBackground(ctx, width, height, time, options = {}) {
1627
1665
  }
1628
1666
  }
1629
1667
  }
1668
+
1669
+ // src/backgrounds/index.ts
1630
1670
  function _parseColor(c) {
1631
1671
  const hex = c.match(/^#([0-9a-f]{3,8})$/i)?.[1];
1632
1672
  if (hex) {