asciify-engine 1.0.23 → 1.0.24

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.js CHANGED
@@ -1,17 +1,29 @@
1
1
  import { parseGIF, decompressFrames } from 'gifuct-js';
2
2
 
3
3
  // src/types.ts
4
+ var PALETTE_THEMES = {
5
+ dracula: { name: "Dracula", accent: "#bd93f9", bg: "#282a36", fg: "#f8f8f2" },
6
+ monokai: { name: "Monokai", accent: "#a6e22e", bg: "#272822", fg: "#f8f8f2" },
7
+ nord: { name: "Nord", accent: "#88c0d0", bg: "#2e3440", fg: "#eceff4" },
8
+ catppuccin: { name: "Catppuccin", accent: "#cba6f7", bg: "#1e1e2e", fg: "#cdd6f4" },
9
+ solarized: { name: "Solarized", accent: "#268bd2", bg: "#002b36", fg: "#839496" },
10
+ gruvbox: { name: "Gruvbox", accent: "#b8bb26", bg: "#282828", fg: "#ebdbb2" }
11
+ };
4
12
  var CHARSETS = {
5
13
  standard: " .:-=+*#%@",
6
14
  blocks: " \u2591\u2592\u2593\u2588",
7
15
  minimal: " .:+",
8
- dense: " .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$",
16
+ dense: " .'`^\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$",
9
17
  binary: "01",
10
18
  dots: " \u2801\u2803\u2807\u2847\u28C7\u28E7\u28F7\u28FF",
11
19
  letters: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
12
20
  claudeCode: " \u2554\u2557\u255A\u255D\u2551\u2550\u2560\u2563\u2566\u2569\u256C\u2591\u2592\u2593\u2588\u2502\u2500\u250C\u2510\u2514\u2518\u251C\u2524\u252C\u2534\u253C",
13
21
  box: " \u25AA\u25FE\u25FC\u25A0\u2588",
14
- lines: " \u02D7\u2010\u2013\u2014\u2015\u2501"
22
+ lines: " \u02D7\u2010\u2013\u2014\u2015\u2501",
23
+ braille: " \u2801\u2802\u2803\u2804\u2805\u2806\u2807\u2808\u2809\u280A\u280B\u280C\u280D\u280E\u280F\u2810\u2811\u2812\u2813\u2814\u2815\u2816\u2817\u2818\u2819\u281A\u281B\u281C\u281D\u281E\u281F\u2820\u2821\u2822\u2823\u2824\u2825\u2826\u2827\u2828\u2829\u282A\u282B\u282C\u282D\u282E\u282F\u2830\u2831\u2832\u2833\u2834\u2835\u2836\u2837\u2838\u2839\u283A\u283B\u283C\u283D\u283E\u283F\u2840\u2841\u2842\u2843\u2844\u2845\u2846\u2847\u28C0\u28C1\u28C2\u28C3\u28C4\u28C5\u28C6\u28C7\u28C8\u28C9\u28CA\u28CB\u28CC\u28CD\u28CE\u28CF\u28D0\u28D1\u28D2\u28D3\u28D4\u28D5\u28D6\u28D7\u28D8\u28D9\u28DA\u28DB\u28DC\u28DD\u28DE\u28DF\u28E0\u28E1\u28E2\u28E3\u28E4\u28E5\u28E6\u28E7\u28E8\u28E9\u28EA\u28EB\u28EC\u28ED\u28EE\u28EF\u28F0\u28F1\u28F2\u28F3\u28F4\u28F5\u28F6\u28F7\u28F8\u28F9\u28FA\u28FB\u28FC\u28FD\u28FE\u28FF",
24
+ katakana: " \uFF66\uFF67\uFF68\uFF69\uFF6A\uFF6B\uFF6C\uFF6D\uFF6E\uFF6F\uFF71\uFF72\uFF73\uFF74\uFF75\uFF76\uFF77\uFF78\uFF79\uFF7A\uFF7B\uFF7C\uFF7D\uFF7E\uFF7F\uFF80\uFF81\uFF82\uFF83\uFF84\uFF85\uFF86\uFF87\uFF88\uFF89\uFF8A\uFF8B\uFF8C\uFF8D\uFF8E\uFF8F\uFF90\uFF91\uFF92\uFF93\uFF94\uFF95\uFF96\uFF97\uFF98\uFF99\uFF9A\uFF9B\uFF9C\uFF9D",
25
+ musical: " \u2669\u266A\u266B\u266C\u266D\u266E\u266F",
26
+ emoji: " \u2B1B\u{1F7EB}\u{1F7E5}\u{1F7E7}\u{1F7E8}\u{1F7E9}\u{1F7E6}\u{1F7EA}\u2B1C"
15
27
  };
16
28
  var ART_STYLE_PRESETS = {
17
29
  classic: {
@@ -54,6 +66,27 @@ var ART_STYLE_PRESETS = {
54
66
  renderMode: "ascii",
55
67
  charset: CHARSETS.lines,
56
68
  colorMode: "fullcolor"
69
+ },
70
+ braille: {
71
+ renderMode: "ascii",
72
+ charset: CHARSETS.braille,
73
+ colorMode: "fullcolor"
74
+ },
75
+ katakana: {
76
+ renderMode: "ascii",
77
+ charset: CHARSETS.katakana,
78
+ colorMode: "matrix"
79
+ },
80
+ musical: {
81
+ renderMode: "ascii",
82
+ charset: CHARSETS.musical,
83
+ colorMode: "accent",
84
+ accentColor: "#e040fb"
85
+ },
86
+ emoji: {
87
+ renderMode: "ascii",
88
+ charset: CHARSETS.emoji,
89
+ colorMode: "fullcolor"
57
90
  }
58
91
  };
59
92
  var DEFAULT_OPTIONS = {
@@ -109,6 +142,18 @@ var HOVER_PRESETS = {
109
142
  ice: {
110
143
  label: "Ice",
111
144
  options: { hoverStrength: 0.5, hoverEffect: "glow", hoverRadius: 0.15, hoverColor: "#60d5f7" }
145
+ },
146
+ gravity: {
147
+ label: "Gravity",
148
+ options: { hoverStrength: 0.7, hoverEffect: "attract", hoverRadius: 0.18, hoverColor: "#a5d6ff" }
149
+ },
150
+ shatter: {
151
+ label: "Shatter",
152
+ options: { hoverStrength: 0.8, hoverEffect: "shatter", hoverRadius: 0.14, hoverColor: "#ff6090" }
153
+ },
154
+ ghost: {
155
+ label: "Ghost",
156
+ options: { hoverStrength: 0.55, hoverEffect: "trail", hoverRadius: 0.2, hoverColor: "#b39ddb" }
112
157
  }
113
158
  };
114
159
 
@@ -128,14 +173,16 @@ function adjustLuminance(lum, brightness, contrast) {
128
173
  }
129
174
  function luminanceToChar(lum, charset, invert) {
130
175
  const normalized = invert ? 1 - lum / 255 : lum / 255;
131
- const index = Math.floor(normalized * (charset.length - 1));
132
- return charset[Math.max(0, Math.min(charset.length - 1, index))];
176
+ const chars = [...charset];
177
+ const index = Math.floor(normalized * (chars.length - 1));
178
+ return chars[Math.max(0, Math.min(chars.length - 1, index))];
133
179
  }
134
180
  function customTextToChar(lum, text, x, y, cols, invert) {
135
181
  const normalized = invert ? 1 - lum / 255 : lum / 255;
136
182
  if (normalized < 0.12) return " ";
183
+ const chars = [...text];
137
184
  const pos = y * cols + x;
138
- return text[pos % text.length];
185
+ return chars[pos % chars.length];
139
186
  }
140
187
  var BAYER_4X4 = [
141
188
  [0, 8, 2, 10],
@@ -283,6 +330,44 @@ function getAnimationMultiplier(x, y, cols, rows, time, style, speed) {
283
330
  }
284
331
  case "waveField":
285
332
  return 1;
333
+ case "ripple": {
334
+ const cx2 = cols / 2;
335
+ const cy2 = rows / 2;
336
+ const dx2 = x - cx2;
337
+ const dy2 = y - cy2;
338
+ const dist2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
339
+ const maxDist2 = Math.sqrt(cx2 * cx2 + cy2 * cy2);
340
+ const ripple = Math.sin(dist2 / maxDist2 * Math.PI * 10 - t * 5) * 0.5 + 0.5;
341
+ const fadeEdge = 1 - Math.min(1, dist2 / (maxDist2 * 1.1));
342
+ return 0.1 + 0.9 * ripple * (0.4 + fadeEdge * 0.6);
343
+ }
344
+ case "melt": {
345
+ const lagPhase = y / rows * Math.PI;
346
+ const drip = Math.sin(lagPhase - t * 1.8 + x * 0.15) * 0.5 + 0.5;
347
+ const gravity = Math.max(0, y / rows - 0.1) / 0.9;
348
+ return 0.05 + 0.95 * (drip * (1 - gravity * 0.6));
349
+ }
350
+ case "orbit": {
351
+ const ocx = cols / 2;
352
+ const ocy = rows / 2;
353
+ const odx = x - ocx;
354
+ const ody = y - ocy;
355
+ const oAngle = Math.atan2(ody, odx);
356
+ const oDist = Math.sqrt(odx * odx + ody * ody) / Math.sqrt(ocx * ocx + ocy * ocy);
357
+ const orbit = Math.sin(oAngle * 2 + oDist * 6 - t * 2.5) * 0.5 + 0.5;
358
+ return 0.1 + 0.9 * orbit;
359
+ }
360
+ case "cellular": {
361
+ const tick = Math.floor(t * 4);
362
+ const alive = (cx, cy) => {
363
+ const h = Math.sin(cx * 127.1 + cy * 311.7 + tick * 43758.5453) * 43758.5453;
364
+ return h - Math.floor(h) > 0.38 ? 1 : 0;
365
+ };
366
+ const self = alive(x, y);
367
+ const neighbours = alive(x - 1, y) + alive(x + 1, y) + alive(x, y - 1) + alive(x, y + 1) + alive(x - 1, y - 1) + alive(x + 1, y - 1) + alive(x - 1, y + 1) + alive(x + 1, y + 1);
368
+ const nextAlive = self === 1 ? neighbours === 2 || neighbours === 3 ? 1 : 0 : neighbours === 3 ? 1 : 0;
369
+ return nextAlive === 1 ? 1 : 0.05;
370
+ }
286
371
  default:
287
372
  return 1;
288
373
  }
@@ -342,6 +427,30 @@ function computeHoverEffect(nx, ny, hoverX, hoverY, hoverIntensity, strength, ce
342
427
  glow = eased * strength * 0.2;
343
428
  colorBlend = eased * strength * 0.7;
344
429
  break;
430
+ case "attract": {
431
+ const angle3 = Math.atan2(dy, dx);
432
+ const pull = eased * eased * strength * 1;
433
+ offsetX = -Math.cos(angle3) * pull * cellW;
434
+ offsetY = -Math.sin(angle3) * pull * cellH;
435
+ glow = eased * strength * 0.3;
436
+ break;
437
+ }
438
+ case "shatter": {
439
+ const angle4 = Math.atan2(dy, dx);
440
+ const jitter = Math.sin(dx * 43.7 + dy * 29.3) * 0.5;
441
+ const scatter = eased * strength * 1.4 * (0.7 + jitter * 0.3);
442
+ offsetX = Math.cos(angle4 + jitter) * scatter * cellW;
443
+ offsetY = Math.sin(angle4 + jitter) * scatter * cellH;
444
+ scale = Math.max(0.1, 1 - eased * strength * 0.6);
445
+ glow = eased * strength * 0.25;
446
+ break;
447
+ }
448
+ case "trail": {
449
+ colorBlend = eased * strength * 0.9;
450
+ glow = eased * strength * 0.6;
451
+ scale = 1 + eased * strength * 0.15;
452
+ break;
453
+ }
345
454
  }
346
455
  _hoverResult.scale = scale;
347
456
  _hoverResult.offsetX = offsetX;
@@ -753,17 +862,18 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
753
862
  const fontSize = Math.min(cellW / charAspect, cellH) * 0.9;
754
863
  const useFastRect = fontSize < 6;
755
864
  if (!useFastRect) {
756
- ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
865
+ const isEmoji = options.artStyle === "emoji";
866
+ ctx.font = isEmoji ? `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla", sans-serif` : `${fontSize}px "JetBrains Mono", monospace`;
757
867
  ctx.textAlign = "center";
758
868
  ctx.textBaseline = "middle";
759
869
  }
760
870
  let charWeights = null;
761
871
  if (useFastRect) {
762
872
  charWeights = {};
763
- const cs = options.charset;
764
- const csLen = cs.length;
873
+ const csChars = [...options.charset];
874
+ const csLen = csChars.length;
765
875
  for (let i = 0; i < csLen; i++) {
766
- charWeights[cs[i]] = Math.max(0.1, (i + 0.3) / csLen);
876
+ charWeights[csChars[i]] = Math.max(0.1, (i + 0.3) / csLen);
767
877
  }
768
878
  }
769
879
  const baseTransform = !useFastRect ? ctx.getTransform() : null;
@@ -1674,6 +1784,292 @@ function renderMorphBackground(ctx, width, height, time, options = {}) {
1674
1784
  }
1675
1785
  }
1676
1786
 
1787
+ // src/backgrounds/fire.ts
1788
+ function renderFireBackground(ctx, width, height, time, options = {}) {
1789
+ const {
1790
+ fontSize = 13,
1791
+ chars = " .,:;i+xX#&@",
1792
+ color = "#ff4500",
1793
+ hotColor = "#ffe066",
1794
+ intensity = 0.85,
1795
+ wind = 0,
1796
+ speed = 1,
1797
+ lightMode = false
1798
+ } = options;
1799
+ const charW = fontSize * 0.62;
1800
+ const lineH = fontSize * 1.4;
1801
+ const cols = Math.ceil(width / charW);
1802
+ const rows = Math.ceil(height / lineH);
1803
+ const len = cols * rows;
1804
+ const key = "__fire_heat__";
1805
+ const canvasAny = ctx.canvas;
1806
+ let heat = canvasAny[key];
1807
+ if (!heat || heat.length !== len) {
1808
+ heat = new Float32Array(len);
1809
+ canvasAny[key] = heat;
1810
+ }
1811
+ const dt = 0.016 * speed;
1812
+ const coolingRate = 0.18 * dt;
1813
+ const windShift = wind * speed * 0.8;
1814
+ const baseRow = rows - 1;
1815
+ const t = time * speed;
1816
+ for (let c = 0; c < cols; c++) {
1817
+ const flicker = Math.sin(c * 0.31 + t * 4.1) * 0.5 + 0.5;
1818
+ const flicker2 = Math.sin(c * 0.73 - t * 2.7) * 0.5 + 0.5;
1819
+ const seed = (flicker * 0.6 + flicker2 * 0.4) * intensity;
1820
+ heat[baseRow * cols + c] = Math.min(1, seed + Math.random() * 0.15 * intensity);
1821
+ if (baseRow > 0) heat[(baseRow - 1) * cols + c] = Math.min(1, seed * 0.85 + Math.random() * 0.1 * intensity);
1822
+ }
1823
+ const newHeat = new Float32Array(len);
1824
+ for (let r = 0; r < rows - 2; r++) {
1825
+ for (let c = 0; c < cols; c++) {
1826
+ const below = heat[(r + 1) * cols + c];
1827
+ const below2 = heat[(r + 2) * cols + Math.max(0, Math.min(cols - 1, c + Math.round(windShift)))];
1828
+ const left = heat[(r + 1) * cols + Math.max(0, c - 1)];
1829
+ const right = heat[(r + 1) * cols + Math.min(cols - 1, c + 1)];
1830
+ const avg = below * 0.4 + below2 * 0.25 + left * 0.175 + right * 0.175;
1831
+ newHeat[r * cols + c] = Math.max(0, avg - coolingRate - Math.random() * 0.02 * speed);
1832
+ }
1833
+ }
1834
+ for (let c = 0; c < cols; c++) {
1835
+ newHeat[(rows - 1) * cols + c] = heat[(rows - 1) * cols + c];
1836
+ if (rows > 1) newHeat[(rows - 2) * cols + c] = heat[(rows - 2) * cols + c];
1837
+ }
1838
+ canvasAny[key] = newHeat;
1839
+ const cp = parseColor(color) ?? { r: 255, g: 69, b: 0 };
1840
+ const hp = parseColor(hotColor) ?? { r: 255, g: 224, b: 102 };
1841
+ ctx.clearRect(0, 0, width, height);
1842
+ ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
1843
+ ctx.textBaseline = "top";
1844
+ for (let r = 0; r < rows; r++) {
1845
+ for (let c = 0; c < cols; c++) {
1846
+ const v = newHeat[r * cols + c];
1847
+ if (v < 0.03) continue;
1848
+ const charIdx = Math.min(chars.length - 1, Math.floor(v * chars.length));
1849
+ const ch = chars[charIdx];
1850
+ if (ch === " ") continue;
1851
+ const blend = Math.min(1, v * 1.2);
1852
+ const r2 = cp.r + (hp.r - cp.r) * blend | 0;
1853
+ const g2 = cp.g + (hp.g - cp.g) * blend | 0;
1854
+ const b2 = cp.b + (hp.b - cp.b) * blend | 0;
1855
+ const alpha = lightMode ? 1 - v * 0.3 : Math.min(1, v + 0.15);
1856
+ ctx.globalAlpha = alpha;
1857
+ ctx.fillStyle = `rgb(${r2},${g2},${b2})`;
1858
+ ctx.fillText(ch, c * charW, r * lineH);
1859
+ }
1860
+ }
1861
+ ctx.globalAlpha = 1;
1862
+ }
1863
+
1864
+ // src/backgrounds/dna.ts
1865
+ function renderDnaBackground(ctx, width, height, time, options = {}) {
1866
+ const {
1867
+ fontSize = 13,
1868
+ baseChars = "ATCG",
1869
+ bridgeChars = "-=\u2261",
1870
+ color = "#00e5ff",
1871
+ color2 = "#ff4081",
1872
+ bridgeColor = "#88ffcc",
1873
+ speed = 1,
1874
+ helixCount,
1875
+ lightMode = false
1876
+ } = options;
1877
+ const charW = fontSize * 0.62;
1878
+ const lineH = fontSize * 1.4;
1879
+ const cols = Math.ceil(width / charW);
1880
+ const rows = Math.ceil(height / lineH);
1881
+ const numHelix = helixCount ?? Math.max(1, Math.floor(width / 80));
1882
+ const sectionW = cols / numHelix;
1883
+ const cp = parseColor(color) ?? { r: 0, g: 229, b: 255 };
1884
+ const cp2 = parseColor(color2) ?? { r: 255, g: 64, b: 129 };
1885
+ const bp = parseColor(bridgeColor) ?? { r: 136, g: 255, b: 204 };
1886
+ ctx.clearRect(0, 0, width, height);
1887
+ ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
1888
+ ctx.textBaseline = "top";
1889
+ const t = time * speed;
1890
+ const amplitude = sectionW * 0.35;
1891
+ for (let h = 0; h < numHelix; h++) {
1892
+ const centerCol = sectionW * (h + 0.5);
1893
+ for (let r = 0; r < rows; r++) {
1894
+ const phase = r / rows * Math.PI * 6 - t * 1.8;
1895
+ const strand1ColF = centerCol + Math.sin(phase) * amplitude;
1896
+ const strand2ColF = centerCol + Math.sin(phase + Math.PI) * amplitude;
1897
+ const strand1Col = Math.round(strand1ColF);
1898
+ const strand2Col = Math.round(strand2ColF);
1899
+ if (strand1Col < 0 || strand1Col >= cols) continue;
1900
+ const baseSeed1 = hash2(h * 31 + r * 7, 3);
1901
+ const ch1 = baseChars[Math.floor(baseSeed1 * baseChars.length)];
1902
+ const depth1 = (Math.sin(phase) + 1) * 0.5;
1903
+ const depth2 = (Math.sin(phase + Math.PI) + 1) * 0.5;
1904
+ ctx.globalAlpha = 0.35 + depth1 * 0.65;
1905
+ ctx.fillStyle = `rgb(${cp.r},${cp.g},${cp.b})`;
1906
+ ctx.fillText(ch1, strand1Col * charW, r * lineH);
1907
+ if (strand2Col >= 0 && strand2Col < cols) {
1908
+ const baseSeed2 = hash2(h * 53 + r * 11, 7);
1909
+ const ch2 = baseChars[Math.floor(baseSeed2 * baseChars.length)];
1910
+ ctx.globalAlpha = 0.35 + depth2 * 0.65;
1911
+ ctx.fillStyle = `rgb(${cp2.r},${cp2.g},${cp2.b})`;
1912
+ ctx.fillText(ch2, strand2Col * charW, r * lineH);
1913
+ }
1914
+ const bridgeInterval = 3;
1915
+ if (r % bridgeInterval === 0) {
1916
+ const minC = Math.min(strand1Col, strand2Col);
1917
+ const maxC = Math.max(strand1Col, strand2Col);
1918
+ const bridgeLen = maxC - minC;
1919
+ if (bridgeLen > 1) {
1920
+ const bSeed = hash2(r * 17 + h * 43, 5);
1921
+ const bCh = bridgeChars[Math.floor(bSeed * bridgeChars.length)];
1922
+ const midBridgeAlpha = (depth1 + depth2) * 0.25 + 0.2;
1923
+ ctx.globalAlpha = midBridgeAlpha;
1924
+ ctx.fillStyle = `rgb(${bp.r},${bp.g},${bp.b})`;
1925
+ for (let bc = minC + 1; bc < maxC; bc++) {
1926
+ ctx.fillText(bCh, bc * charW, r * lineH);
1927
+ }
1928
+ }
1929
+ }
1930
+ }
1931
+ }
1932
+ ctx.globalAlpha = 1;
1933
+ }
1934
+
1935
+ // src/backgrounds/terrain.ts
1936
+ function renderTerrainBackground(ctx, width, height, time, options = {}) {
1937
+ const {
1938
+ fontSize = 13,
1939
+ chars = " .,:;+*#@",
1940
+ color = "#4caf50",
1941
+ skyColor = "#1a237e",
1942
+ peakColor = "#e0e0e0",
1943
+ speed = 1,
1944
+ roughness = 0.55,
1945
+ heightScale = 0.55,
1946
+ stars = true,
1947
+ lightMode = false
1948
+ } = options;
1949
+ const charW = fontSize * 0.62;
1950
+ const lineH = fontSize * 1.4;
1951
+ const cols = Math.ceil(width / charW);
1952
+ const rows = Math.ceil(height / lineH);
1953
+ const cp = parseColor(color) ?? { r: 76, g: 175, b: 80 };
1954
+ const sky = parseColor(skyColor) ?? { r: 26, g: 35, b: 126 };
1955
+ const peak = parseColor(peakColor) ?? { r: 224, g: 224, b: 224 };
1956
+ ctx.clearRect(0, 0, width, height);
1957
+ ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
1958
+ ctx.textBaseline = "top";
1959
+ const scroll = time * speed * 0.4;
1960
+ const terrainRow = new Array(cols);
1961
+ for (let c = 0; c < cols; c++) {
1962
+ const nx = (c / cols + scroll) * roughness * 3;
1963
+ const h = (fbm(nx, 0.5) * 0.5 + 0.5) * heightScale;
1964
+ terrainRow[c] = Math.floor(h * rows);
1965
+ }
1966
+ for (let r = 0; r < rows; r++) {
1967
+ for (let c = 0; c < cols; c++) {
1968
+ const terrainStart = rows - 1 - terrainRow[c];
1969
+ const isGround = r >= terrainStart;
1970
+ const isNearPeak = r === terrainStart;
1971
+ if (!isGround) {
1972
+ if (stars) {
1973
+ const starSeed = hash2(c * 7 + Math.floor(scroll * 0.3), r * 13);
1974
+ if (starSeed > 0.97) {
1975
+ const twinkle = Math.sin(time * 2 + starSeed * 100) * 0.3 + 0.7;
1976
+ ctx.globalAlpha = twinkle * 0.5;
1977
+ ctx.fillStyle = `rgb(${sky.r + 60},${sky.g + 60},${sky.b + 80})`;
1978
+ ctx.fillText("\xB7", c * charW, r * lineH);
1979
+ }
1980
+ }
1981
+ continue;
1982
+ }
1983
+ const depth = (r - terrainStart) / Math.max(1, terrainRow[c]);
1984
+ const charIdx = Math.min(chars.length - 1, Math.floor(depth * chars.length));
1985
+ const ch = chars[charIdx];
1986
+ if (ch === " " && !isNearPeak) continue;
1987
+ const blendPeak = isNearPeak ? 1 : Math.max(0, 1 - depth * 4);
1988
+ const r2 = cp.r + (peak.r - cp.r) * blendPeak | 0;
1989
+ const g2 = cp.g + (peak.g - cp.g) * blendPeak | 0;
1990
+ const b2 = cp.b + (peak.b - cp.b) * blendPeak | 0;
1991
+ ctx.globalAlpha = 0.5 + depth * 0.5;
1992
+ ctx.fillStyle = `rgb(${r2},${g2},${b2})`;
1993
+ ctx.fillText(isNearPeak ? chars[chars.length - 1] : ch, c * charW, r * lineH);
1994
+ }
1995
+ }
1996
+ ctx.globalAlpha = 1;
1997
+ }
1998
+
1999
+ // src/backgrounds/circuit.ts
2000
+ var EAST = 1;
2001
+ var WEST = 2;
2002
+ var NORTH = 4;
2003
+ var SOUTH = 8;
2004
+ var DIR_CHARS = {
2005
+ [EAST | WEST]: "\u2500",
2006
+ [NORTH | SOUTH]: "\u2502",
2007
+ [EAST | SOUTH]: "\u250C",
2008
+ [WEST | SOUTH]: "\u2510",
2009
+ [EAST | NORTH]: "\u2514",
2010
+ [WEST | NORTH]: "\u2518",
2011
+ [EAST | WEST | SOUTH]: "\u252C",
2012
+ [EAST | WEST | NORTH]: "\u2534",
2013
+ [NORTH | SOUTH | EAST]: "\u251C",
2014
+ [NORTH | SOUTH | WEST]: "\u2524",
2015
+ [EAST | WEST | NORTH | SOUTH]: "\u253C",
2016
+ [EAST]: "\u2576",
2017
+ [WEST]: "\u2574",
2018
+ [NORTH]: "\u2575",
2019
+ [SOUTH]: "\u2577"
2020
+ };
2021
+ function renderCircuitBackground(ctx, width, height, time, options = {}) {
2022
+ const {
2023
+ fontSize = 13,
2024
+ pulseColor = "#ffffff",
2025
+ color = "#00ff88",
2026
+ density = 0.38,
2027
+ speed = 1,
2028
+ lightMode = false
2029
+ } = options;
2030
+ const charW = fontSize * 0.62;
2031
+ const lineH = fontSize * 1.4;
2032
+ const cols = Math.ceil(width / charW);
2033
+ const rows = Math.ceil(height / lineH);
2034
+ const cp = parseColor(color) ?? { r: 0, g: 255, b: 136 };
2035
+ const pp = parseColor(pulseColor) ?? { r: 255, g: 255, b: 255 };
2036
+ const getConnections = (c, r) => {
2037
+ if (hash2(c * 17 + 1, r * 7 + 2) > density) return 0;
2038
+ let mask = 0;
2039
+ if (c + 1 < cols && hash2(c * 17 + 1, r * 7 + 2) > 0.15) mask |= EAST;
2040
+ if (c - 1 >= 0 && hash2((c - 1) * 17 + 1, r * 7 + 2) > 0.15) mask |= WEST;
2041
+ if (r + 1 < rows && hash2(c * 17 + 1, (r + 1) * 7 + 2) > 0.15) mask |= SOUTH;
2042
+ if (r - 1 >= 0 && hash2(c * 17 + 1, (r - 1) * 7 + 2) > 0.15) mask |= NORTH;
2043
+ return mask;
2044
+ };
2045
+ ctx.clearRect(0, 0, width, height);
2046
+ ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
2047
+ ctx.textBaseline = "top";
2048
+ const t = time * speed;
2049
+ for (let r = 0; r < rows; r++) {
2050
+ for (let c = 0; c < cols; c++) {
2051
+ const mask = getConnections(c, r);
2052
+ if (mask === 0) continue;
2053
+ const ch = DIR_CHARS[mask] ?? "\xB7";
2054
+ const pulsePosH = (t * 8 + hash2(c, r * 23) * 40) % cols;
2055
+ const pulsePosV = (t * 6 + hash2(r * 17, c * 11) * 40) % rows;
2056
+ const nearH = (mask & EAST || mask & WEST) && Math.abs(c - pulsePosH) < 1.5;
2057
+ const nearV = (mask & NORTH || mask & SOUTH) && Math.abs(r - pulsePosV) < 1.5;
2058
+ const isPulse = nearH || nearV;
2059
+ const baseAlpha = 0.25 + hash2(c * 3, r * 5) * 0.35;
2060
+ if (isPulse) {
2061
+ ctx.globalAlpha = 0.95;
2062
+ ctx.fillStyle = `rgb(${pp.r},${pp.g},${pp.b})`;
2063
+ } else {
2064
+ ctx.globalAlpha = baseAlpha;
2065
+ ctx.fillStyle = `rgb(${cp.r},${cp.g},${cp.b})`;
2066
+ }
2067
+ ctx.fillText(ch, c * charW, r * lineH);
2068
+ }
2069
+ }
2070
+ ctx.globalAlpha = 1;
2071
+ }
2072
+
1677
2073
  // src/backgrounds/index.ts
1678
2074
  function _parseColor(c) {
1679
2075
  const hex = c.match(/^#([0-9a-f]{3,8})$/i)?.[1];
@@ -1778,6 +2174,24 @@ function asciiBackground(target, options = {}) {
1778
2174
  lightMode: renderOpts.lightMode !== void 0 ? renderOpts.lightMode : isLight(),
1779
2175
  color: color ?? renderOpts.color
1780
2176
  });
2177
+ const buildFireOpts = () => ({
2178
+ ...renderOpts,
2179
+ color: color ?? renderOpts.color
2180
+ });
2181
+ const buildDnaOpts = () => ({
2182
+ ...renderOpts,
2183
+ color: color ?? renderOpts.color
2184
+ });
2185
+ const buildTerrainOpts = () => ({
2186
+ ...renderOpts,
2187
+ color: color ?? renderOpts.color,
2188
+ lightMode: renderOpts.lightMode !== void 0 ? renderOpts.lightMode : isLight()
2189
+ });
2190
+ const buildCircuitOpts = () => ({
2191
+ ...renderOpts,
2192
+ color: color ?? renderOpts.color,
2193
+ lightMode: renderOpts.lightMode !== void 0 ? renderOpts.lightMode : isLight()
2194
+ });
1781
2195
  const optsRef = { current: buildWaveOpts() };
1782
2196
  const rebuildOpts = () => {
1783
2197
  if (type === "rain") optsRef.current = buildRainOpts();
@@ -1789,6 +2203,10 @@ function asciiBackground(target, options = {}) {
1789
2203
  else if (type === "silk") optsRef.current = buildSilkOpts();
1790
2204
  else if (type === "void") optsRef.current = buildVoidOpts();
1791
2205
  else if (type === "morph") optsRef.current = buildMorphOpts();
2206
+ else if (type === "fire") optsRef.current = buildFireOpts();
2207
+ else if (type === "dna") optsRef.current = buildDnaOpts();
2208
+ else if (type === "terrain") optsRef.current = buildTerrainOpts();
2209
+ else if (type === "circuit") optsRef.current = buildCircuitOpts();
1792
2210
  else optsRef.current = buildWaveOpts();
1793
2211
  };
1794
2212
  rebuildOpts();
@@ -1835,6 +2253,14 @@ function asciiBackground(target, options = {}) {
1835
2253
  renderVoidBackground(ctx, r.width, r.height, time, smoothMouse, optsRef.current);
1836
2254
  } else if (type === "morph") {
1837
2255
  renderMorphBackground(ctx, r.width, r.height, time, optsRef.current);
2256
+ } else if (type === "fire") {
2257
+ renderFireBackground(ctx, r.width, r.height, time, optsRef.current);
2258
+ } else if (type === "dna") {
2259
+ renderDnaBackground(ctx, r.width, r.height, time, optsRef.current);
2260
+ } else if (type === "terrain") {
2261
+ renderTerrainBackground(ctx, r.width, r.height, time, optsRef.current);
2262
+ } else if (type === "circuit") {
2263
+ renderCircuitBackground(ctx, r.width, r.height, time, optsRef.current);
1838
2264
  } else {
1839
2265
  renderWaveBackground(ctx, r.width, r.height, time, smoothMouse, optsRef.current);
1840
2266
  }
@@ -1855,6 +2281,178 @@ function asciiBackground(target, options = {}) {
1855
2281
  }
1856
2282
  var mountWaveBackground = asciiBackground;
1857
2283
 
2284
+ // src/core/ascii-text.ts
2285
+ function asciiText(source, options = {}, targetWidth, targetHeight) {
2286
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2287
+ const { frame, cols } = imageToAsciiFrame(source, opts, targetWidth, targetHeight);
2288
+ if (!frame.length || cols === 0) return "";
2289
+ const lines = [];
2290
+ for (const row of frame) {
2291
+ lines.push(row.map((cell) => cell.char).join(""));
2292
+ }
2293
+ return lines.join("\n");
2294
+ }
2295
+ function asciiTextAnsi(source, options = {}, targetWidth, targetHeight) {
2296
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2297
+ const { frame, cols } = imageToAsciiFrame(source, opts, targetWidth, targetHeight);
2298
+ if (!frame.length || cols === 0) return "";
2299
+ const RESET = "\x1B[0m";
2300
+ const lines = [];
2301
+ for (const row of frame) {
2302
+ let line = "";
2303
+ for (const cell of row) {
2304
+ if (cell.char === " " || cell.a < 10) {
2305
+ line += " ";
2306
+ } else {
2307
+ line += `\x1B[38;2;${cell.r};${cell.g};${cell.b}m${cell.char}${RESET}`;
2308
+ }
2309
+ }
2310
+ lines.push(line);
2311
+ }
2312
+ return lines.join("\n");
2313
+ }
2314
+
2315
+ // src/core/record.ts
2316
+ function createRecorder(canvas, options = {}) {
2317
+ const {
2318
+ fps = 15,
2319
+ maxFrames = 120,
2320
+ format = "gif",
2321
+ quality = 10,
2322
+ scale = 1
2323
+ } = options;
2324
+ const interval = 1e3 / fps;
2325
+ let recording = false;
2326
+ let timerId = -1;
2327
+ const blobs = [];
2328
+ const captureFrame = () => {
2329
+ if (!recording || blobs.length >= maxFrames) return;
2330
+ let src = canvas;
2331
+ if (scale !== 1) {
2332
+ const off = document.createElement("canvas");
2333
+ off.width = Math.round(canvas.width * scale);
2334
+ off.height = Math.round(canvas.height * scale);
2335
+ const offCtx = off.getContext("2d");
2336
+ offCtx.drawImage(canvas, 0, 0, off.width, off.height);
2337
+ src = off;
2338
+ }
2339
+ blobs.push(src.toDataURL("image/png"));
2340
+ };
2341
+ const encodeGif = async (frames) => {
2342
+ return new Promise((resolve, reject) => {
2343
+ if (typeof GIF === "undefined") {
2344
+ reject(new Error('[asciify recorder] gif.js not found. Add <script src="/gif.worker.js"> to your page.'));
2345
+ return;
2346
+ }
2347
+ const gif = new GIF({
2348
+ workers: 2,
2349
+ quality,
2350
+ workerScript: "/gif.worker.js"
2351
+ });
2352
+ let loaded = 0;
2353
+ const total = frames.length;
2354
+ frames.forEach((dataUrl) => {
2355
+ const img = new Image();
2356
+ img.onload = () => {
2357
+ gif.addFrame(img, { delay: interval, copy: true });
2358
+ loaded++;
2359
+ if (loaded === total) gif.render();
2360
+ };
2361
+ img.src = dataUrl;
2362
+ });
2363
+ gif.on("finished", (blob) => {
2364
+ const reader = new FileReader();
2365
+ reader.onload = () => resolve(reader.result);
2366
+ reader.readAsDataURL(blob);
2367
+ });
2368
+ gif.on("error", reject);
2369
+ });
2370
+ };
2371
+ const encodeWebP = async (frames, _fps) => {
2372
+ if (typeof MediaRecorder === "undefined") {
2373
+ throw new Error("[asciify recorder] MediaRecorder not available in this browser.");
2374
+ }
2375
+ const off = document.createElement("canvas");
2376
+ if (frames.length === 0) return "";
2377
+ const probe = new Image();
2378
+ await new Promise((res) => {
2379
+ probe.onload = () => res();
2380
+ probe.src = frames[0];
2381
+ });
2382
+ off.width = probe.naturalWidth;
2383
+ off.height = probe.naturalHeight;
2384
+ const offCtx = off.getContext("2d");
2385
+ const stream = off.captureStream(_fps);
2386
+ const recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp9" });
2387
+ const chunks = [];
2388
+ recorder.ondataavailable = (e) => chunks.push(e.data);
2389
+ return new Promise((resolve, reject) => {
2390
+ recorder.onstop = () => {
2391
+ const blob = new Blob(chunks, { type: "video/webm" });
2392
+ const reader = new FileReader();
2393
+ reader.onload = () => resolve(reader.result);
2394
+ reader.readAsDataURL(blob);
2395
+ };
2396
+ recorder.onerror = reject;
2397
+ recorder.start();
2398
+ let idx = 0;
2399
+ const drawNext = () => {
2400
+ if (idx >= frames.length) {
2401
+ recorder.stop();
2402
+ return;
2403
+ }
2404
+ const img = new Image();
2405
+ img.onload = () => {
2406
+ offCtx.drawImage(img, 0, 0);
2407
+ idx++;
2408
+ setTimeout(drawNext, interval);
2409
+ };
2410
+ img.src = frames[idx];
2411
+ };
2412
+ drawNext();
2413
+ });
2414
+ };
2415
+ return {
2416
+ get isRecording() {
2417
+ return recording;
2418
+ },
2419
+ get frameCount() {
2420
+ return blobs.length;
2421
+ },
2422
+ start() {
2423
+ if (recording) return;
2424
+ blobs.length = 0;
2425
+ recording = true;
2426
+ timerId = window.setInterval(captureFrame, interval);
2427
+ },
2428
+ async stop() {
2429
+ if (!recording) return "";
2430
+ recording = false;
2431
+ clearInterval(timerId);
2432
+ const frames = blobs.slice();
2433
+ if (format === "png-sequence") {
2434
+ return JSON.stringify(frames);
2435
+ }
2436
+ if (format === "webp") {
2437
+ return encodeWebP(frames, fps);
2438
+ }
2439
+ return encodeGif(frames);
2440
+ }
2441
+ };
2442
+ }
2443
+ async function recordAndDownload(canvas, durationMs, options = {}) {
2444
+ const { filename = "asciify-recording", ...recOpts } = options;
2445
+ const recorder = createRecorder(canvas, recOpts);
2446
+ recorder.start();
2447
+ await new Promise((res) => setTimeout(res, durationMs));
2448
+ const dataUrl = await recorder.stop();
2449
+ const ext = options.format === "webp" ? "webm" : options.format === "png-sequence" ? "json" : "gif";
2450
+ const a = document.createElement("a");
2451
+ a.href = dataUrl;
2452
+ a.download = `${filename}.${ext}`;
2453
+ a.click();
2454
+ }
2455
+
1858
2456
  // src/webgl-engine.ts
1859
2457
  var VERT_SRC = (
1860
2458
  /* glsl */
@@ -2117,8 +2715,8 @@ function makeTexture(gl, filter = gl.NEAREST) {
2117
2715
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2118
2716
  return t;
2119
2717
  }
2120
- var ANIM_STYLES = ["none", "wave", "pulse", "rain", "breathe", "sparkle", "glitch", "spiral", "typewriter", "scatter", "waveField"];
2121
- var HOVER_EFFECTS = ["spotlight", "magnify", "repel", "glow", "colorShift"];
2718
+ var ANIM_STYLES = ["none", "wave", "pulse", "rain", "breathe", "sparkle", "glitch", "spiral", "typewriter", "scatter", "waveField", "ripple", "melt", "orbit", "cellular"];
2719
+ var HOVER_EFFECTS = ["spotlight", "magnify", "repel", "glow", "colorShift", "attract", "shatter", "trail"];
2122
2720
  var COLOR_MODES = ["grayscale", "fullcolor", "matrix", "accent"];
2123
2721
  function tryCreateWebGLRenderer(canvas) {
2124
2722
  const glOrNull = canvas.getContext("webgl2", {
@@ -2303,6 +2901,6 @@ function tryCreateWebGLRenderer(canvas) {
2303
2901
  }
2304
2902
  }
2305
2903
 
2306
- export { ART_STYLE_PRESETS, CHARSETS, DEFAULT_OPTIONS, HOVER_PRESETS, asciiBackground, asciify, asciifyGif, asciifyVideo, generateAnimatedEmbedCode, generateEmbedCode, gifToAsciiFrames, imageToAsciiFrame, mountWaveBackground, renderAuroraBackground, renderFrameToCanvas, renderGridBackground, renderMorphBackground, renderNoiseBackground, renderPulseBackground, renderRainBackground, renderSilkBackground, renderStarsBackground, renderVoidBackground, renderWaveBackground, tryCreateWebGLRenderer, videoToAsciiFrames };
2904
+ export { ART_STYLE_PRESETS, CHARSETS, DEFAULT_OPTIONS, HOVER_PRESETS, PALETTE_THEMES, asciiBackground, asciiText, asciiTextAnsi, asciify, asciifyGif, asciifyVideo, createRecorder, generateAnimatedEmbedCode, generateEmbedCode, gifToAsciiFrames, imageToAsciiFrame, mountWaveBackground, recordAndDownload, renderAuroraBackground, renderCircuitBackground, renderDnaBackground, renderFireBackground, renderFrameToCanvas, renderGridBackground, renderMorphBackground, renderNoiseBackground, renderPulseBackground, renderRainBackground, renderSilkBackground, renderStarsBackground, renderTerrainBackground, renderVoidBackground, renderWaveBackground, tryCreateWebGLRenderer, videoToAsciiFrames };
2307
2905
  //# sourceMappingURL=index.js.map
2308
2906
  //# sourceMappingURL=index.js.map