asciify-engine 1.0.1 → 1.0.3

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
@@ -11,7 +11,9 @@ var CHARSETS = {
11
11
  binary: "01",
12
12
  dots: " \u2801\u2803\u2807\u2847\u28C7\u28E7\u28F7\u28FF",
13
13
  letters: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
14
- 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"
14
+ 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",
15
+ box: " \u25AA\u25FE\u25FC\u25A0\u2588",
16
+ lines: " \u02D7\u2010\u2013\u2014\u2015\u2501"
15
17
  };
16
18
  var ART_STYLE_PRESETS = {
17
19
  classic: {
@@ -44,6 +46,16 @@ var ART_STYLE_PRESETS = {
44
46
  renderMode: "ascii",
45
47
  charset: CHARSETS.standard,
46
48
  colorMode: "matrix"
49
+ },
50
+ box: {
51
+ renderMode: "ascii",
52
+ charset: CHARSETS.box,
53
+ colorMode: "grayscale"
54
+ },
55
+ lines: {
56
+ renderMode: "ascii",
57
+ charset: CHARSETS.lines,
58
+ colorMode: "fullcolor"
47
59
  }
48
60
  };
49
61
  var DEFAULT_OPTIONS = {
@@ -61,43 +73,44 @@ var DEFAULT_OPTIONS = {
61
73
  dotSizeRatio: 0.8,
62
74
  ditherStrength: 0,
63
75
  hoverStrength: 0,
64
- hoverRadius: 0.5,
76
+ hoverRadius: 0.2,
65
77
  hoverEffect: "spotlight",
66
78
  hoverColor: "#ffffff",
67
- artStyle: "classic"
79
+ artStyle: "classic",
80
+ customText: ""
68
81
  };
69
82
  var HOVER_PRESETS = {
70
83
  none: {
71
84
  label: "Off",
72
- options: { hoverStrength: 0, hoverEffect: "spotlight", hoverRadius: 0.5, hoverColor: "#ffffff" }
85
+ options: { hoverStrength: 0, hoverEffect: "spotlight", hoverRadius: 0.2, hoverColor: "#ffffff" }
73
86
  },
74
87
  subtle: {
75
88
  label: "Subtle",
76
- options: { hoverStrength: 0.8, hoverEffect: "glow", hoverRadius: 0.4, hoverColor: "#ffffff" }
89
+ options: { hoverStrength: 0.25, hoverEffect: "glow", hoverRadius: 0.12, hoverColor: "#ffffff" }
77
90
  },
78
91
  flashlight: {
79
92
  label: "Flashlight",
80
- options: { hoverStrength: 2.5, hoverEffect: "spotlight", hoverRadius: 0.6, hoverColor: "#fffbe6" }
93
+ options: { hoverStrength: 0.6, hoverEffect: "spotlight", hoverRadius: 0.15, hoverColor: "#fffbe6" }
81
94
  },
82
95
  magnifier: {
83
96
  label: "Magnifier",
84
- options: { hoverStrength: 3, hoverEffect: "magnify", hoverRadius: 0.35, hoverColor: "#ffffff" }
97
+ options: { hoverStrength: 0.7, hoverEffect: "magnify", hoverRadius: 0.12, hoverColor: "#ffffff" }
85
98
  },
86
99
  forceField: {
87
100
  label: "Force Field",
88
- options: { hoverStrength: 3.5, hoverEffect: "repel", hoverRadius: 0.5, hoverColor: "#a0e8ff" }
101
+ options: { hoverStrength: 0.7, hoverEffect: "repel", hoverRadius: 0.15, hoverColor: "#a0e8ff" }
89
102
  },
90
103
  neon: {
91
104
  label: "Neon",
92
- options: { hoverStrength: 2.5, hoverEffect: "colorShift", hoverRadius: 0.55, hoverColor: "#d946ef" }
105
+ options: { hoverStrength: 0.6, hoverEffect: "colorShift", hoverRadius: 0.15, hoverColor: "#d946ef" }
93
106
  },
94
107
  fire: {
95
108
  label: "Fire",
96
- options: { hoverStrength: 3, hoverEffect: "spotlight", hoverRadius: 0.5, hoverColor: "#ff6b2b" }
109
+ options: { hoverStrength: 0.7, hoverEffect: "spotlight", hoverRadius: 0.15, hoverColor: "#ff6b2b" }
97
110
  },
98
111
  ice: {
99
112
  label: "Ice",
100
- options: { hoverStrength: 2, hoverEffect: "glow", hoverRadius: 0.65, hoverColor: "#60d5f7" }
113
+ options: { hoverStrength: 0.5, hoverEffect: "glow", hoverRadius: 0.15, hoverColor: "#60d5f7" }
101
114
  }
102
115
  };
103
116
  function createOffscreenCanvas(width, height) {
@@ -118,6 +131,12 @@ function luminanceToChar(lum, charset, invert) {
118
131
  const index = Math.floor(normalized * (charset.length - 1));
119
132
  return charset[Math.max(0, Math.min(charset.length - 1, index))];
120
133
  }
134
+ function customTextToChar(lum, text, x, y, cols, invert) {
135
+ const normalized = invert ? 1 - lum / 255 : lum / 255;
136
+ if (normalized < 0.12) return " ";
137
+ const pos = y * cols + x;
138
+ return text[pos % text.length];
139
+ }
121
140
  var BAYER_4X4 = [
122
141
  [0, 8, 2, 10],
123
142
  [12, 4, 14, 6],
@@ -293,7 +312,7 @@ function imageToAsciiFrame(source, options, targetWidth, targetHeight) {
293
312
  const lum = 0.299 * r + 0.587 * g + 0.114 * b;
294
313
  const adjustedLum = adjustLuminance(lum, options.brightness, options.contrast);
295
314
  const ditheredLum = applyDither(adjustedLum, x, y, options.ditherStrength);
296
- const char = luminanceToChar(ditheredLum, options.charset, options.invert);
315
+ const char = options.customText ? customTextToChar(ditheredLum, options.customText, x, y, cols, options.invert) : luminanceToChar(ditheredLum, options.charset, options.invert);
297
316
  row.push({ char, r, g, b, a });
298
317
  }
299
318
  frame.push(row);
@@ -472,8 +491,11 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
472
491
  }
473
492
  const cellW = canvasWidth / cols;
474
493
  const cellH = canvasHeight / rows;
494
+ const totalCells = rows * cols;
475
495
  const hoverIntensity = hoverPos?.intensity ?? 1;
476
- const hoverActive = !!(hoverPos && options.hoverStrength > 0 && hoverIntensity > 5e-3);
496
+ const animationActive = options.animationStyle !== "none";
497
+ const suppressHover = animationActive && totalCells > 5e3;
498
+ const hoverActive = !suppressHover && !!(hoverPos && options.hoverStrength > 0 && hoverIntensity > 5e-3);
477
499
  const hc = options.hoverColor || "#ffffff";
478
500
  const hcR = parseInt(hc.slice(1, 3), 16) || 255;
479
501
  const hcG = parseInt(hc.slice(3, 5), 16) || 255;
@@ -482,12 +504,14 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
482
504
  const acR = parseInt(acHex.substring(0, 2), 16) || 255;
483
505
  const acG = parseInt(acHex.substring(2, 4), 16) || 255;
484
506
  const acB = parseInt(acHex.substring(4, 6), 16) || 255;
507
+ const radiusScale = totalCells > 3e4 ? 0.25 : totalCells > 15e3 ? 0.4 : totalCells > 5e3 ? 0.6 : 1;
508
+ const effectiveHoverRadius = options.hoverRadius * radiusScale;
485
509
  let hoverMinCol = 0, hoverMaxCol = cols, hoverMinRow = 0, hoverMaxRow = rows;
486
510
  let hoverPosX = 0, hoverPosY = 0;
487
511
  if (hoverActive && hoverPos) {
488
512
  hoverPosX = hoverPos.x;
489
513
  hoverPosY = hoverPos.y;
490
- const hoverNormRadius = 0.08 + options.hoverRadius * 0.35 + options.hoverStrength * 0.04;
514
+ const hoverNormRadius = 0.08 + effectiveHoverRadius * 0.35 + options.hoverStrength * 0.04;
491
515
  hoverMinCol = Math.max(0, Math.floor((hoverPosX - hoverNormRadius) * cols) - 1);
492
516
  hoverMaxCol = Math.min(cols, Math.ceil((hoverPosX + hoverNormRadius) * cols) + 1);
493
517
  hoverMinRow = Math.max(0, Math.floor((hoverPosY - hoverNormRadius) * rows) - 1);
@@ -498,7 +522,7 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
498
522
  const noAnimation = animStyle === "none";
499
523
  const hoverStrength = options.hoverStrength;
500
524
  const hoverEffect = options.hoverEffect;
501
- const hoverRadiusFactor = options.hoverRadius;
525
+ const hoverRadiusFactor = effectiveHoverRadius;
502
526
  const isInverted = options.invert;
503
527
  const colorMode = options.colorMode;
504
528
  const TWO_PI = Math.PI * 2;
@@ -564,7 +588,7 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
564
588
  ctx.fillStyle = color;
565
589
  lastFillStyle = color;
566
590
  }
567
- if (radius <= 1.5) {
591
+ if (radius <= 3) {
568
592
  const d = radius * 2;
569
593
  ctx.fillRect(px - radius, py - radius, d, d);
570
594
  } else {
@@ -577,10 +601,22 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
577
601
  } else {
578
602
  const charAspect = 0.55;
579
603
  const fontSize = Math.min(cellW / charAspect, cellH) * 0.9;
580
- ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
581
- ctx.textAlign = "center";
582
- ctx.textBaseline = "middle";
583
- const baseTransform = ctx.getTransform();
604
+ const useFastRect = fontSize < 6;
605
+ if (!useFastRect) {
606
+ ctx.font = `${fontSize}px "JetBrains Mono", monospace`;
607
+ ctx.textAlign = "center";
608
+ ctx.textBaseline = "middle";
609
+ }
610
+ let charWeights = null;
611
+ if (useFastRect) {
612
+ charWeights = {};
613
+ const cs = options.charset;
614
+ const csLen = cs.length;
615
+ for (let i = 0; i < csLen; i++) {
616
+ charWeights[cs[i]] = Math.max(0.1, (i + 0.3) / csLen);
617
+ }
618
+ }
619
+ const baseTransform = !useFastRect ? ctx.getTransform() : null;
584
620
  for (let y = 0; y < rows; y++) {
585
621
  const rowData = frame[y];
586
622
  for (let x = 0; x < cols; x++) {
@@ -614,11 +650,6 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
614
650
  }
615
651
  const px = x * cellW + cellW * 0.5 + hoverOffX;
616
652
  const py = y * cellH + cellH * 0.5 + hoverOffY;
617
- const alpha = Math.min(1, cell.a * 0.00392156863 * animMul * (1 + hoverGlow));
618
- if (alpha !== lastAlpha) {
619
- ctx.globalAlpha = alpha;
620
- lastAlpha = alpha;
621
- }
622
653
  let color;
623
654
  if (hoverBlend > 0) {
624
655
  const rgb = getCellColorRGB(cell, colorMode, acR, acG, acB);
@@ -629,17 +660,39 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
629
660
  } else {
630
661
  color = getCellColorStr(cell, colorMode, acR, acG, acB);
631
662
  }
632
- if (color !== lastFillStyle) {
633
- ctx.fillStyle = color;
634
- lastFillStyle = color;
635
- }
636
- if (hoverScale !== 1) {
637
- ctx.translate(px, py);
638
- ctx.scale(hoverScale, hoverScale);
639
- ctx.fillText(cell.char, 0, 0);
640
- ctx.setTransform(baseTransform);
663
+ if (useFastRect) {
664
+ const weight = charWeights[cell.char] ?? 0.5;
665
+ const effAlpha = Math.min(1, cell.a * 0.00392156863 * animMul * (1 + hoverGlow)) * weight;
666
+ if (effAlpha < 0.02) continue;
667
+ if (effAlpha !== lastAlpha) {
668
+ ctx.globalAlpha = effAlpha;
669
+ lastAlpha = effAlpha;
670
+ }
671
+ if (color !== lastFillStyle) {
672
+ ctx.fillStyle = color;
673
+ lastFillStyle = color;
674
+ }
675
+ const rw = cellW * hoverScale;
676
+ const rh = cellH * hoverScale;
677
+ ctx.fillRect(px - rw * 0.5, py - rh * 0.5, rw, rh);
641
678
  } else {
642
- ctx.fillText(cell.char, px, py);
679
+ const alpha = Math.min(1, cell.a * 0.00392156863 * animMul * (1 + hoverGlow));
680
+ if (alpha !== lastAlpha) {
681
+ ctx.globalAlpha = alpha;
682
+ lastAlpha = alpha;
683
+ }
684
+ if (color !== lastFillStyle) {
685
+ ctx.fillStyle = color;
686
+ lastFillStyle = color;
687
+ }
688
+ if (hoverScale !== 1) {
689
+ ctx.translate(px, py);
690
+ ctx.scale(hoverScale, hoverScale);
691
+ ctx.fillText(cell.char, 0, 0);
692
+ ctx.setTransform(baseTransform);
693
+ } else {
694
+ ctx.fillText(cell.char, px, py);
695
+ }
643
696
  }
644
697
  }
645
698
  }
@@ -678,6 +731,33 @@ function buildColorFnJS(options) {
678
731
  return `function(c){var g=Math.floor(.299*c.r+.587*c.g+.114*c.b);return 'rgb('+g+','+g+','+g+')'}`;
679
732
  }
680
733
  }
734
+ async function asciify(source, canvas, { fontSize = 10, style = "classic", options = {} } = {}) {
735
+ let el;
736
+ if (typeof source === "string") {
737
+ const img = new Image();
738
+ img.crossOrigin = "anonymous";
739
+ await new Promise((resolve, reject) => {
740
+ img.onload = () => resolve();
741
+ img.onerror = () => reject(new Error(`Failed to load image: ${source}`));
742
+ img.src = source;
743
+ });
744
+ el = img;
745
+ } else if (source instanceof HTMLImageElement && !source.complete) {
746
+ await new Promise((resolve, reject) => {
747
+ source.onload = () => resolve();
748
+ source.onerror = () => reject(new Error("Image failed to load"));
749
+ });
750
+ el = source;
751
+ } else {
752
+ el = source;
753
+ }
754
+ const preset = ART_STYLE_PRESETS[style];
755
+ const merged = { ...DEFAULT_OPTIONS, ...preset, ...options, fontSize };
756
+ const ctx = canvas.getContext("2d");
757
+ if (!ctx) throw new Error("Could not get 2d context from canvas");
758
+ const { frame } = imageToAsciiFrame(el, merged, canvas.width, canvas.height);
759
+ renderFrameToCanvas(ctx, frame, merged, canvas.width, canvas.height);
760
+ }
681
761
  function generateEmbedCode(frame, options, width, height) {
682
762
  const rows = frame.length;
683
763
  if (rows === 0) return "";
@@ -862,15 +942,465 @@ function generateAnimatedEmbedCode(frames, options, fps, width, height) {
862
942
  <!-- /Asciify Animated Embed -->`;
863
943
  }
864
944
 
945
+ // src/webgl-engine.ts
946
+ var VERT_SRC = (
947
+ /* glsl */
948
+ `#version 300 es
949
+ precision highp float;
950
+ precision highp int;
951
+
952
+ // Unit quad corners: (-0.5,-0.5) to (0.5,0.5) - TRIANGLE_STRIP, 4 verts
953
+ in vec2 a_corner;
954
+
955
+ // Layout
956
+ uniform vec2 u_canvasSize;
957
+ uniform vec2 u_gridSize;
958
+ uniform vec2 u_cellSize;
959
+
960
+ // Animation
961
+ uniform float u_time;
962
+ uniform float u_animSpeed;
963
+ uniform int u_animStyle;
964
+
965
+ // Color
966
+ uniform int u_colorMode;
967
+ uniform vec3 u_accentColor;
968
+ uniform int u_invert;
969
+
970
+ // Hover
971
+ uniform float u_hoverStrength;
972
+ uniform float u_hoverIntensity;
973
+ uniform vec2 u_hoverPos;
974
+ uniform float u_hoverRadius;
975
+ uniform int u_hoverEffect;
976
+ uniform vec3 u_hoverColor;
977
+
978
+ // Render mode: 0=rect 1=dots
979
+ uniform int u_renderMode;
980
+ uniform float u_dotSizeRatio;
981
+
982
+ // Cell data: RGBA8 texture (cols x rows)
983
+ // rgb = cell colour, a = source alpha
984
+ uniform sampler2D u_cellTex;
985
+
986
+ out vec4 v_color;
987
+ out float v_alpha;
988
+ out vec2 v_localUv;
989
+
990
+ #define PI 3.141592653589793
991
+
992
+ float ss(float t) { return t * t * (3.0 - 2.0 * t); }
993
+
994
+ float getAnim(float nx, float ny, float cx, float cy, float t) {
995
+ if (u_animStyle == 0) return 1.0;
996
+ if (u_animStyle == 1) { // wave
997
+ return 0.3 + 0.7 * (sin(nx * PI * 4.0 + t * 3.0) * 0.3 + 0.5
998
+ + sin(ny * PI * 3.0 + t * 2.0) * 0.2);
999
+ }
1000
+ if (u_animStyle == 2) { // pulse
1001
+ float d = length(vec2(nx - 0.5, ny - 0.5)) * 1.41421;
1002
+ return 0.2 + 0.8 * (sin(d * PI * 6.0 - t * 4.0) * 0.5 + 0.5);
1003
+ }
1004
+ if (u_animStyle == 3) { // rain
1005
+ float drop = sin(ny * PI * 8.0 - t * 5.0 + cx * 0.3) * 0.5 + 0.5;
1006
+ return 0.1 + 0.9 * drop * (sin(nx * PI * 2.0 + t) * 0.3 + 0.7);
1007
+ }
1008
+ if (u_animStyle == 4) { // breathe
1009
+ return clamp(sin(t * 2.0) * 0.3 + 0.7 + sin((cx + cy) * 0.1 + t) * 0.1, 0.1, 1.0);
1010
+ }
1011
+ if (u_animStyle == 5) { // sparkle
1012
+ float sp = fract(sin(cx * 127.1 + cy * 311.7 + floor(t * 8.0) * 43758.5453) * 43758.5453);
1013
+ return sp > 0.7 ? 1.0 : 0.15 + sp * 0.4;
1014
+ }
1015
+ if (u_animStyle == 6) { // glitch
1016
+ float band = floor(cy / max(1.0, u_gridSize.y * 0.05));
1017
+ float gv = fract(sin(band * 43.23 + floor(t * 6.0) * 17.89) * 43758.5453);
1018
+ if (gv > 0.85) { float fl = sin(t * 30.0 + band) * 0.5 + 0.5; return fl < 0.3 ? 0.0 : fl; }
1019
+ return 1.0;
1020
+ }
1021
+ if (u_animStyle == 7) { // spiral
1022
+ float angle = atan(ny - 0.5, nx - 0.5);
1023
+ float dist = length(vec2(nx - 0.5, ny - 0.5)) * 1.41421;
1024
+ return 0.15 + 0.85 * (sin(angle * 3.0 + dist * PI * 8.0 - t * 3.0) * 0.5 + 0.5);
1025
+ }
1026
+ if (u_animStyle == 8) { // typewriter
1027
+ float reveal = fract(t * 0.5) * u_gridSize.x * u_gridSize.y * 1.3;
1028
+ float dist = cy * u_gridSize.x + cx - reveal;
1029
+ if (dist > 0.0) return 0.0;
1030
+ return clamp(1.0 + dist / (u_gridSize.x * u_gridSize.y * 0.15), 0.0, 1.0);
1031
+ }
1032
+ if (u_animStyle == 9) { // scatter
1033
+ float d = length(vec2(nx - 0.5, ny - 0.5)) * 1.41421;
1034
+ float phase = sin(t * 1.5) * 0.5 + 0.5;
1035
+ if (d > phase) return max(0.0, 1.0 - (d - phase) * 3.0);
1036
+ return 0.7 + 0.3 * sin(d * 10.0 - t * 2.0);
1037
+ }
1038
+ return 1.0;
1039
+ }
1040
+
1041
+ vec3 applyColorMode(vec3 rgb) {
1042
+ float lum = dot(rgb, vec3(0.299, 0.587, 0.114));
1043
+ if (u_colorMode == 0) return vec3(lum);
1044
+ if (u_colorMode == 2) return vec3(0.0, lum, 0.0);
1045
+ if (u_colorMode == 3) return lum * u_accentColor;
1046
+ return rgb;
1047
+ }
1048
+
1049
+ void main() {
1050
+ int cols = int(u_gridSize.x);
1051
+ int rows = int(u_gridSize.y);
1052
+ int cx = gl_InstanceID % cols;
1053
+ int cy = gl_InstanceID / cols;
1054
+
1055
+ float fnx = (float(cx) + 0.5) / float(cols);
1056
+ float fny = (float(cy) + 0.5) / float(rows);
1057
+
1058
+ // Sample cell data
1059
+ vec4 cell = texture(u_cellTex, vec2(fnx, fny));
1060
+ // cell.rgb = colour, cell.a = source alpha (0=transparent, 1=opaque)
1061
+
1062
+ // Invisible cells (fully transparent source pixel)
1063
+ if (cell.a < 0.004) {
1064
+ gl_Position = vec4(2.0, 2.0, 0.0, 1.0); v_alpha = 0.0; return;
1065
+ }
1066
+
1067
+ // Char weight: 0=dark/space .. 1=dense/bright (luminance, matches luminanceToChar ordering)
1068
+ float charW = dot(cell.rgb, vec3(0.299, 0.587, 0.114));
1069
+
1070
+ float cellW = u_cellSize.x;
1071
+ float cellH = u_cellSize.y;
1072
+
1073
+ // Animation
1074
+ float t = u_time * u_animSpeed;
1075
+ float am = getAnim(fnx, fny, float(cx), float(cy), t);
1076
+ if (am < 0.04) {
1077
+ gl_Position = vec4(2.0, 2.0, 0.0, 1.0); v_alpha = 0.0; return;
1078
+ }
1079
+
1080
+ // Hover
1081
+ float hoverScale = 1.0;
1082
+ vec2 hoverOff = vec2(0.0);
1083
+ float hoverGlow = 0.0;
1084
+ float hoverBlend = 0.0;
1085
+
1086
+ if (u_hoverStrength > 0.0 && u_hoverIntensity > 0.005) {
1087
+ vec2 d = vec2(fnx - u_hoverPos.x, fny - u_hoverPos.y);
1088
+ float r = 0.08 + u_hoverRadius * 0.35 + u_hoverStrength * 0.04;
1089
+ float dist = length(d);
1090
+ if (dist < r) {
1091
+ float tv = 1.0 - dist / r;
1092
+ float e = ss(tv) * u_hoverIntensity;
1093
+ if (u_hoverEffect == 0) { // spotlight
1094
+ hoverScale = 1.0 + e * u_hoverStrength * 1.8;
1095
+ float ang = atan(d.y, d.x);
1096
+ hoverOff = vec2(cos(ang), sin(ang)) * e * e * u_hoverStrength * 0.6 * vec2(cellW, cellH);
1097
+ hoverGlow = e * u_hoverStrength * 0.4;
1098
+ hoverBlend = e * e * u_hoverStrength * 0.25;
1099
+ } else if (u_hoverEffect == 1) { // magnify
1100
+ hoverScale = 1.0 + e * u_hoverStrength * 2.5;
1101
+ hoverGlow = e * u_hoverStrength * 0.15;
1102
+ } else if (u_hoverEffect == 2) { // repel
1103
+ hoverScale = 1.0 + e * u_hoverStrength * 0.3;
1104
+ float ang = atan(d.y, d.x);
1105
+ hoverOff = vec2(cos(ang), sin(ang)) * e * e * u_hoverStrength * 1.2 * vec2(cellW, cellH);
1106
+ } else if (u_hoverEffect == 3) { // glow
1107
+ hoverGlow = e * u_hoverStrength * 0.8;
1108
+ hoverBlend = e * u_hoverStrength * 0.4;
1109
+ } else { // colorShift
1110
+ hoverScale = 1.0 + e * u_hoverStrength * 0.4;
1111
+ hoverGlow = e * u_hoverStrength * 0.2;
1112
+ hoverBlend = e * u_hoverStrength * 0.7;
1113
+ }
1114
+ }
1115
+ }
1116
+
1117
+ // Final colour
1118
+ vec3 rgb = (u_invert == 1) ? (1.0 - cell.rgb) : cell.rgb;
1119
+ vec3 baseColor = applyColorMode(rgb);
1120
+ v_color = vec4(mix(baseColor, u_hoverColor, hoverBlend), 1.0);
1121
+
1122
+ // Quad size & alpha
1123
+ vec2 quadSize;
1124
+ float finalAlpha;
1125
+
1126
+ if (u_renderMode == 1) { // dots: radius driven by char weight (bright=big dot)
1127
+ float maxR = min(cellW, cellH) * 0.5 * u_dotSizeRatio;
1128
+ float radius = maxR * charW * am * hoverScale;
1129
+ if (radius < 0.3) { gl_Position = vec4(2.0, 2.0, 0.0, 1.0); v_alpha = 0.0; return; }
1130
+ quadSize = vec2(radius * 2.0);
1131
+ finalAlpha = min(1.0, cell.a * am * (1.0 + hoverGlow));
1132
+ } else { // rect: size proportional to luminance \u2014 dark=tiny, bright=full (ASCII density look)
1133
+ if (charW < 0.01) { gl_Position = vec4(2.0, 2.0, 0.0, 1.0); v_alpha = 0.0; return; }
1134
+ quadSize = vec2(cellW * charW * hoverScale, cellH * charW * hoverScale);
1135
+ finalAlpha = min(1.0, am * (1.0 + hoverGlow));
1136
+ }
1137
+
1138
+ v_alpha = finalAlpha;
1139
+ v_localUv = a_corner;
1140
+
1141
+ // NDC position (Y flipped: WebGL bottom-up, canvas top-down)
1142
+ float px = float(cx) * cellW + cellW * 0.5 + hoverOff.x;
1143
+ float py = float(cy) * cellH + cellH * 0.5 + hoverOff.y;
1144
+ vec2 pos = vec2(px, py) + a_corner * quadSize;
1145
+ vec2 ndc = pos / u_canvasSize * 2.0 - 1.0;
1146
+ ndc.y = -ndc.y;
1147
+ gl_Position = vec4(ndc, 0.0, 1.0);
1148
+ }
1149
+ `
1150
+ );
1151
+ var FRAG_SRC = (
1152
+ /* glsl */
1153
+ `#version 300 es
1154
+ precision mediump float;
1155
+ precision highp int;
1156
+
1157
+ in vec4 v_color;
1158
+ in float v_alpha;
1159
+ in vec2 v_localUv;
1160
+
1161
+ uniform int u_renderMode;
1162
+
1163
+ out vec4 outColor;
1164
+
1165
+ void main() {
1166
+ if (v_alpha < 0.01) discard;
1167
+ if (u_renderMode == 1) {
1168
+ // Dots: circle SDF clip
1169
+ if (length(v_localUv) > 0.5) discard;
1170
+ }
1171
+ outColor = vec4(v_color.rgb, v_alpha);
1172
+ }
1173
+ `
1174
+ );
1175
+ function compileShader(gl, type, src) {
1176
+ const sh = gl.createShader(type);
1177
+ gl.shaderSource(sh, src);
1178
+ gl.compileShader(sh);
1179
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
1180
+ const info = gl.getShaderInfoLog(sh) ?? "unknown";
1181
+ gl.deleteShader(sh);
1182
+ throw new Error(`Shader compile error: ${info}`);
1183
+ }
1184
+ return sh;
1185
+ }
1186
+ function linkProgram(gl, vert, frag) {
1187
+ const prog = gl.createProgram();
1188
+ gl.attachShader(prog, vert);
1189
+ gl.attachShader(prog, frag);
1190
+ gl.linkProgram(prog);
1191
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
1192
+ const info = gl.getProgramInfoLog(prog) ?? "unknown";
1193
+ gl.deleteProgram(prog);
1194
+ throw new Error(`Program link error: ${info}`);
1195
+ }
1196
+ return prog;
1197
+ }
1198
+ function makeTexture(gl, filter = gl.NEAREST) {
1199
+ const t = gl.createTexture();
1200
+ gl.bindTexture(gl.TEXTURE_2D, t);
1201
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
1202
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
1203
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1204
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1205
+ return t;
1206
+ }
1207
+ var ANIM_STYLES = ["none", "wave", "pulse", "rain", "breathe", "sparkle", "glitch", "spiral", "typewriter", "scatter"];
1208
+ var HOVER_EFFECTS = ["spotlight", "magnify", "repel", "glow", "colorShift"];
1209
+ var COLOR_MODES = ["grayscale", "fullcolor", "matrix", "accent"];
1210
+ function tryCreateWebGLRenderer(canvas) {
1211
+ const glOrNull = canvas.getContext("webgl2", {
1212
+ alpha: true,
1213
+ antialias: false,
1214
+ premultipliedAlpha: false,
1215
+ preserveDrawingBuffer: false
1216
+ });
1217
+ if (!glOrNull) return null;
1218
+ const gl = glOrNull;
1219
+ try {
1220
+ let render2 = function(frame, options, displayW, displayH, time, hoverPos) {
1221
+ const rows = frame.length;
1222
+ if (rows === 0) return;
1223
+ const cols = frame[0].length;
1224
+ if (cols === 0) return;
1225
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
1226
+ let hasTransparency = false;
1227
+ const sy = Math.max(1, rows >> 2), sx = Math.max(1, cols >> 2);
1228
+ outer: for (let iy = 0; iy < rows; iy += sy)
1229
+ for (let ix = 0; ix < cols; ix += sx)
1230
+ if (frame[iy][ix].a < 200) {
1231
+ hasTransparency = true;
1232
+ break outer;
1233
+ }
1234
+ gl.clearColor(
1235
+ hasTransparency ? 0 : 0.0392,
1236
+ hasTransparency ? 0 : 0.0392,
1237
+ hasTransparency ? 0 : 0.0392,
1238
+ hasTransparency ? 0 : 1
1239
+ );
1240
+ gl.clear(gl.COLOR_BUFFER_BIT);
1241
+ if (cols !== lastCols || rows !== lastRows) {
1242
+ cellBuf = new Uint8Array(cols * rows * 4);
1243
+ lastCols = cols;
1244
+ lastRows = rows;
1245
+ cellDataDirty = true;
1246
+ }
1247
+ if (cellDataDirty) {
1248
+ for (let y = 0; y < rows; y++) {
1249
+ const row = frame[y];
1250
+ for (let x = 0; x < cols; x++) {
1251
+ const cell = row[x];
1252
+ const i = (y * cols + x) * 4;
1253
+ cellBuf[i] = cell.r;
1254
+ cellBuf[i + 1] = cell.g;
1255
+ cellBuf[i + 2] = cell.b;
1256
+ cellBuf[i + 3] = cell.a;
1257
+ }
1258
+ }
1259
+ gl.activeTexture(gl.TEXTURE0);
1260
+ gl.bindTexture(gl.TEXTURE_2D, cellTex);
1261
+ gl.texImage2D(
1262
+ gl.TEXTURE_2D,
1263
+ 0,
1264
+ gl.RGBA,
1265
+ cols,
1266
+ rows,
1267
+ 0,
1268
+ gl.RGBA,
1269
+ gl.UNSIGNED_BYTE,
1270
+ cellBuf
1271
+ );
1272
+ cellDataDirty = false;
1273
+ }
1274
+ gl.activeTexture(gl.TEXTURE0);
1275
+ gl.bindTexture(gl.TEXTURE_2D, cellTex);
1276
+ gl.useProgram(prog);
1277
+ gl.bindVertexArray(vao);
1278
+ const cellW = displayW / cols;
1279
+ const cellH = displayH / rows;
1280
+ gl.uniform2f(u.canvasSize, displayW, displayH);
1281
+ gl.uniform2f(u.gridSize, cols, rows);
1282
+ gl.uniform2f(u.cellSize, cellW, cellH);
1283
+ gl.uniform1f(u.time, time);
1284
+ gl.uniform1f(u.animSpeed, options.animationSpeed);
1285
+ gl.uniform1i(u.animStyle, ANIM_STYLES.indexOf(options.animationStyle));
1286
+ gl.uniform1i(u.colorMode, COLOR_MODES.indexOf(options.colorMode));
1287
+ const acHex = (options.accentColor || "#ffffff").replace("#", "");
1288
+ gl.uniform3f(
1289
+ u.accentColor,
1290
+ parseInt(acHex.slice(0, 2), 16) / 255,
1291
+ parseInt(acHex.slice(2, 4), 16) / 255,
1292
+ parseInt(acHex.slice(4, 6), 16) / 255
1293
+ );
1294
+ gl.uniform1i(u.invert, options.invert ? 1 : 0);
1295
+ const totalCells = rows * cols;
1296
+ const animActive = options.animationStyle !== "none";
1297
+ const suppressHover = animActive && totalCells > 5e3;
1298
+ const hoverActive = !suppressHover && !!hoverPos && options.hoverStrength > 0;
1299
+ const radiusScale = totalCells > 3e4 ? 0.25 : totalCells > 15e3 ? 0.4 : totalCells > 5e3 ? 0.6 : 1;
1300
+ gl.uniform1f(u.hoverStrength, hoverActive ? options.hoverStrength : 0);
1301
+ gl.uniform1f(u.hoverIntensity, hoverActive ? hoverPos?.intensity ?? 0 : 0);
1302
+ gl.uniform2f(u.hoverPos, hoverPos?.x ?? 0.5, hoverPos?.y ?? 0.5);
1303
+ gl.uniform1f(u.hoverRadius, options.hoverRadius * radiusScale);
1304
+ gl.uniform1i(u.hoverEffect, HOVER_EFFECTS.indexOf(options.hoverEffect));
1305
+ const hc = options.hoverColor || "#ffffff";
1306
+ gl.uniform3f(
1307
+ u.hoverColor,
1308
+ parseInt(hc.slice(1, 3), 16) / 255,
1309
+ parseInt(hc.slice(3, 5), 16) / 255,
1310
+ parseInt(hc.slice(5, 7), 16) / 255
1311
+ );
1312
+ gl.uniform1i(u.renderMode, options.renderMode === "dots" ? 1 : 0);
1313
+ gl.uniform1f(u.dotSizeRatio, options.dotSizeRatio);
1314
+ gl.uniform1i(u.cellTexU, 0);
1315
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, cols * rows);
1316
+ gl.bindVertexArray(null);
1317
+ };
1318
+ var render = render2;
1319
+ const vert = compileShader(gl, gl.VERTEX_SHADER, VERT_SRC);
1320
+ const frag = compileShader(gl, gl.FRAGMENT_SHADER, FRAG_SRC);
1321
+ const prog = linkProgram(gl, vert, frag);
1322
+ gl.deleteShader(vert);
1323
+ gl.deleteShader(frag);
1324
+ const vao = gl.createVertexArray();
1325
+ gl.bindVertexArray(vao);
1326
+ const corners = new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]);
1327
+ const vbo = gl.createBuffer();
1328
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
1329
+ gl.bufferData(gl.ARRAY_BUFFER, corners, gl.STATIC_DRAW);
1330
+ const aCorner = gl.getAttribLocation(prog, "a_corner");
1331
+ gl.enableVertexAttribArray(aCorner);
1332
+ gl.vertexAttribPointer(aCorner, 2, gl.FLOAT, false, 0, 0);
1333
+ gl.bindVertexArray(null);
1334
+ const cellTex = makeTexture(gl);
1335
+ const g = (n) => gl.getUniformLocation(prog, n);
1336
+ const u = {
1337
+ canvasSize: g("u_canvasSize"),
1338
+ gridSize: g("u_gridSize"),
1339
+ cellSize: g("u_cellSize"),
1340
+ time: g("u_time"),
1341
+ animSpeed: g("u_animSpeed"),
1342
+ animStyle: g("u_animStyle"),
1343
+ colorMode: g("u_colorMode"),
1344
+ accentColor: g("u_accentColor"),
1345
+ invert: g("u_invert"),
1346
+ hoverStrength: g("u_hoverStrength"),
1347
+ hoverIntensity: g("u_hoverIntensity"),
1348
+ hoverPos: g("u_hoverPos"),
1349
+ hoverRadius: g("u_hoverRadius"),
1350
+ hoverEffect: g("u_hoverEffect"),
1351
+ hoverColor: g("u_hoverColor"),
1352
+ renderMode: g("u_renderMode"),
1353
+ dotSizeRatio: g("u_dotSizeRatio"),
1354
+ cellTexU: g("u_cellTex")
1355
+ };
1356
+ let lastCols = 0;
1357
+ let lastRows = 0;
1358
+ let cellBuf = new Uint8Array(0);
1359
+ let cellDataDirty = true;
1360
+ gl.enable(gl.BLEND);
1361
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1362
+ return {
1363
+ render: render2,
1364
+ notifyDirty() {
1365
+ cellDataDirty = true;
1366
+ },
1367
+ destroy() {
1368
+ gl.deleteProgram(prog);
1369
+ gl.deleteVertexArray(vao);
1370
+ gl.deleteBuffer(vbo);
1371
+ gl.deleteTexture(cellTex);
1372
+ }
1373
+ };
1374
+ } catch (err) {
1375
+ console.error("[asciify] WebGL renderer setup FAILED (returning stub):", err);
1376
+ const stub = {
1377
+ render() {
1378
+ try {
1379
+ gl.clearColor(0.039, 0.039, 0.039, 1);
1380
+ gl.clear(gl.COLOR_BUFFER_BIT);
1381
+ } catch {
1382
+ }
1383
+ },
1384
+ notifyDirty() {
1385
+ },
1386
+ destroy() {
1387
+ }
1388
+ };
1389
+ return stub;
1390
+ }
1391
+ }
1392
+
865
1393
  exports.ART_STYLE_PRESETS = ART_STYLE_PRESETS;
866
1394
  exports.CHARSETS = CHARSETS;
867
1395
  exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
868
1396
  exports.HOVER_PRESETS = HOVER_PRESETS;
1397
+ exports.asciify = asciify;
869
1398
  exports.generateAnimatedEmbedCode = generateAnimatedEmbedCode;
870
1399
  exports.generateEmbedCode = generateEmbedCode;
871
1400
  exports.gifToAsciiFrames = gifToAsciiFrames;
872
1401
  exports.imageToAsciiFrame = imageToAsciiFrame;
873
1402
  exports.renderFrameToCanvas = renderFrameToCanvas;
1403
+ exports.tryCreateWebGLRenderer = tryCreateWebGLRenderer;
874
1404
  exports.videoToAsciiFrames = videoToAsciiFrames;
875
1405
  //# sourceMappingURL=index.cjs.map
876
1406
  //# sourceMappingURL=index.cjs.map