diff --git a/src/components/TuiButton.astro b/src/components/TuiButton.astro
index 4cc5c01..ab994f0 100644
--- a/src/components/TuiButton.astro
+++ b/src/components/TuiButton.astro
@@ -2,6 +2,7 @@
interface Props {
id: string;
label: string;
+ abbr?: string;
shortcut?: string;
variant?: "default" | "primary" | "subtle";
title?: string;
@@ -11,6 +12,7 @@ interface Props {
const {
id,
label,
+ abbr,
shortcut,
variant = "default",
title = "",
@@ -26,7 +28,10 @@ const {
data-tooltip-desc={description}
>
{shortcut &&
diff --git a/src/components/TuiSegment.astro b/src/components/TuiSegment.astro
index f87c5bb..e380e3f 100644
--- a/src/components/TuiSegment.astro
+++ b/src/components/TuiSegment.astro
@@ -2,6 +2,7 @@
interface Props {
id: string;
label: string;
+ abbr?: string;
options: string[];
value?: string;
title?: string;
@@ -11,6 +12,7 @@ interface Props {
const {
id,
label,
+ abbr,
options,
value = options[0],
title = "",
@@ -24,7 +26,10 @@ const {
data-tooltip-title={title}
data-tooltip-desc={description}
>
-
{
options.map((opt) => (
@@ -56,8 +61,23 @@ const {
min-width: 3ch;
font-weight: 700;
font-family: var(--font-mono);
- color: rgba(255, 255, 255, 0.4);
+ color: #fff;
opacity: 0.7;
+ display: flex;
+ transition: all 0.2s;
+ }
+
+ .tui-segment-label .abbr {
+ display: none;
+ }
+
+ @media (max-width: 1400px) {
+ .tui-segment-label.has-abbr .full {
+ display: none;
+ }
+ .tui-segment-label.has-abbr .abbr {
+ display: inline;
+ }
}
.tui-segment-options {
@@ -72,7 +92,8 @@ const {
background: transparent;
border: none;
border-right: 1px solid rgba(255, 255, 255, 0.05);
- color: rgba(255, 255, 255, 0.5);
+ color: #fff;
+ opacity: 0.6;
font-family: inherit;
font-size: inherit;
padding: 4px 10px;
@@ -87,24 +108,26 @@ const {
}
.tui-segment-option:hover {
- color: #fff;
- background: rgba(255, 255, 255, 0.05);
+ color: var(--accent-color);
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent-color), transparent 95%);
}
.tui-segment-option.active {
- background: rgba(255, 255, 255, 0.15);
+ background: var(--accent-color);
color: #fff;
- font-weight: 600;
+ font-weight: 700;
+ opacity: 1;
}
/* Hover the whole group */
.tui-segment:hover .tui-segment-label {
opacity: 1;
- color: rgba(255, 255, 255, 0.8);
+ color: var(--accent-color);
}
.tui-segment:hover .tui-segment-options {
- border-color: rgba(255, 255, 255, 0.2);
+ border-color: var(--accent-color);
}
diff --git a/src/components/TuiSlider.astro b/src/components/TuiSlider.astro
index e593816..f2f2a5e 100644
--- a/src/components/TuiSlider.astro
+++ b/src/components/TuiSlider.astro
@@ -2,6 +2,7 @@
interface Props {
id: string;
label: string;
+ abbr?: string;
min?: number;
max?: number;
step?: number;
@@ -13,6 +14,7 @@ interface Props {
const {
id,
label,
+ abbr,
min = 0,
max = 5,
step = 0.1,
@@ -28,10 +30,18 @@ const segments = 12;
-
{label}
+
@@ -56,26 +66,50 @@ const segments = 12;
value={value}
/>
-
{value.toFixed(2)}
@@ -166,6 +227,9 @@ const segments = 12;
const valueDisplay = sliderContainer.querySelector(
".tui-slider-value",
) as HTMLElement;
+ const defaultValue = parseFloat(
+ sliderContainer.getAttribute("data-default-value") || "0",
+ );
if (!input || !track || !valueDisplay) return;
@@ -196,6 +260,10 @@ const segments = 12;
});
valueDisplay.textContent = val.toFixed(2);
+
+ // Add modified class if value differs from default
+ const isModified = Math.abs(val - defaultValue) > 0.001;
+ sliderContainer.classList.toggle("modified", isModified);
}
input.addEventListener("input", updateVisual);
diff --git a/src/components/TuiToggle.astro b/src/components/TuiToggle.astro
index df23841..a18624c 100644
--- a/src/components/TuiToggle.astro
+++ b/src/components/TuiToggle.astro
@@ -2,6 +2,7 @@
interface Props {
id: string;
label: string;
+ abbr?: string;
checked?: boolean;
title?: string;
description?: string;
@@ -10,6 +11,7 @@ interface Props {
const {
id,
label,
+ abbr,
checked = false,
title = "",
description = "",
@@ -24,7 +26,10 @@ const {
data-tooltip-title={title}
data-tooltip-desc={description}
>
-
{label}
+
+ {label}
+ {abbr && {abbr}}
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 19038e2..d778608 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -351,14 +351,22 @@ import ControlPanel from "../components/ControlPanel.astro";
@media (max-width: 1024px) {
.split-layout {
flex-direction: column;
- overflow: hidden; /* Prevent body scroll, use inner scrolling */
- height: 100dvh; /* Dynamic viewport height */
+ overflow: hidden;
+ height: 100dvh;
}
.ascii-workspace {
- height: auto;
- flex-grow: 1; /* Fill remaining space */
- overflow-y: auto; /* Allow scrolling inside workspace if needed */
+ height: 0; /* Important for flex-grow to work reliably on all browsers */
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0; /* Allow shrinking */
+ }
+
+ .canvas-layer {
+ flex-grow: 1;
+ min-height: 0;
+ height: 0; /* Force flex-grow to determine height */
}
}
diff --git a/src/scripts/ascii-controller.ts b/src/scripts/ascii-controller.ts
index 9ac3416..470c66f 100644
--- a/src/scripts/ascii-controller.ts
+++ b/src/scripts/ascii-controller.ts
@@ -322,7 +322,8 @@ export class AsciiController {
const fontAspectRatio = 0.55;
const marginRatio = 0.05; // Reduced margin for container fit
- const screenW = parent.clientWidth;
+ let screenW = parent.clientWidth;
+ if (screenW <= 0) screenW = window.innerWidth || 1000;
const availW = screenW * (1 - marginRatio);
let widthCols = Math.floor(availW / 6);
@@ -394,10 +395,18 @@ export class AsciiController {
if (!parent) return;
const fontAspectRatio = 0.55;
- const gridAspect = (this.cachedGrid.widthCols * fontAspectRatio) / this.cachedGrid.heightRows;
+ // Safeguard against 0 height or NaNs
+ const heightRows = Math.max(1, Math.floor(this.cachedGrid.heightRows));
+ const widthCols = Math.max(1, this.cachedGrid.widthCols);
+ const gridAspect = (widthCols * fontAspectRatio) / heightRows;
+
+ let screenW = parent.clientWidth;
+ let screenH = parent.clientHeight;
+
+ // Fallback for mobile initialization quirks where parent might be 0 initially
+ if (screenW <= 0) screenW = window.innerWidth;
+ if (screenH <= 0) screenH = window.innerHeight * 0.5; // Guessing half screen for workspace
- const screenW = parent.clientWidth;
- const screenH = parent.clientHeight;
const maxW = screenW * 0.98;
const maxH = screenH * 0.98;
@@ -410,6 +419,12 @@ export class AsciiController {
finalW = maxH * gridAspect;
}
+ // Final safeguard against zero or NaN
+ if (!finalW || !finalH || isNaN(finalW) || isNaN(finalH)) {
+ finalW = 300;
+ finalH = 300 / gridAspect;
+ }
+
this.canvas.style.width = `${finalW}px`;
this.canvas.style.height = `${finalH}px`;
@@ -425,6 +440,12 @@ export class AsciiController {
this.canvas.style.display = 'block';
this.canvas.style.opacity = '1';
this.requestRender('all');
+
+ // Insurance for mobile: trigger a second sizing/render after a short delay
+ // to catch cases where the layout might still be shifting (keyboard, address bar)
+ setTimeout(() => {
+ this.handleResize();
+ }, 100);
}
// ============= Zoom & Touch =============
diff --git a/src/scripts/ui-bindings.ts b/src/scripts/ui-bindings.ts
index 6a7bd50..04ebd39 100644
--- a/src/scripts/ui-bindings.ts
+++ b/src/scripts/ui-bindings.ts
@@ -29,6 +29,8 @@ export class UIBindings {
private queue: ImageQueue;
private loadNewImageFn: () => Promise
;
private isUpdatingUI = false;
+ private lastNextTime = 0;
+ private readonly NEXT_COOLDOWN = 1000; // 1 second cooldown
// Event Handlers implementation references
@@ -315,6 +317,11 @@ export class UIBindings {
if (btnNext) {
const handler = (e: Event) => {
e.stopPropagation();
+
+ const now = Date.now();
+ if (now - this.lastNextTime < this.NEXT_COOLDOWN) return;
+ this.lastNextTime = now;
+
this.loadNewImageFn();
};
this.buttonHandlers.set('btn-next', handler);
@@ -365,6 +372,9 @@ export class UIBindings {
switch (e.key.toLowerCase()) {
case 'n':
+ const now = Date.now();
+ if (now - this.lastNextTime < this.NEXT_COOLDOWN) break;
+ this.lastNextTime = now;
this.loadNewImageFn();
break;
case 'r':
diff --git a/src/scripts/webgl-ascii.ts b/src/scripts/webgl-ascii.ts
index 82f727c..8702820 100644
--- a/src/scripts/webgl-ascii.ts
+++ b/src/scripts/webgl-ascii.ts
@@ -1,5 +1,4 @@
-
export interface RenderOptions {
charSetContent: string;
fontFamily?: string;
@@ -60,8 +59,8 @@ export class WebGLAsciiRenderer {
this.gl = gl;
// Enable required extensions for advanced rendering
- gl.getExtension('OES_standard_derivatives');
- gl.getExtension('EXT_shader_texture_lod');
+ const hasDerivatives = !!gl.getExtension('OES_standard_derivatives');
+ const hasLod = !!gl.getExtension('EXT_shader_texture_lod');
this.program = null;
this.textures = {};
@@ -71,11 +70,11 @@ export class WebGLAsciiRenderer {
this.lastImage = null;
this.fontFamily = "'JetBrains Mono', monospace";
- this.init();
+ this.init(hasDerivatives, hasLod);
this.loadBlueNoiseTexture();
}
- init() {
+ init(hasDerivatives: boolean, hasLod: boolean) {
const gl = this.gl;
// Vertex Shader
@@ -91,8 +90,8 @@ export class WebGLAsciiRenderer {
// Fragment Shader
const fsSource = `
- #extension GL_OES_standard_derivatives : enable
- #extension GL_EXT_shader_texture_lod : enable
+ ${hasDerivatives ? '#extension GL_OES_standard_derivatives : enable' : ''}
+ ${hasLod ? '#extension GL_EXT_shader_texture_lod : enable' : ''}
precision mediump float;
varying vec2 v_texCoord;
@@ -100,11 +99,10 @@ export class WebGLAsciiRenderer {
uniform sampler2D u_atlas;
uniform sampler2D u_blueNoise;
uniform float u_charCount;
- uniform vec2 u_charSizeUV; // Size of one char in UV space (width/texWidth, height/texHeight)
- uniform vec2 u_gridSize; // cols, rows
- uniform vec2 u_texSize; // atlas size
+ uniform vec2 u_charSizeUV;
+ uniform vec2 u_gridSize;
+ uniform vec2 u_texSize;
- // Adjustments
uniform float u_exposure;
uniform float u_contrast;
uniform float u_saturation;
@@ -112,8 +110,8 @@ export class WebGLAsciiRenderer {
uniform bool u_invert;
uniform bool u_color;
uniform float u_overlayStrength;
- uniform int u_edgeMode; // 0=none, 1=simple, 2=sobel, 3=canny
- uniform float u_dither; // Dither strength 0.0 - 1.0
+ uniform int u_edgeMode;
+ uniform float u_dither;
uniform bool u_denoise;
uniform float u_sharpen;
uniform float u_edgeThreshold;
@@ -123,7 +121,6 @@ export class WebGLAsciiRenderer {
uniform float u_vignette;
uniform vec3 u_monoColor;
- // Zoom & Magnifier
uniform float u_zoom;
uniform vec2 u_zoomCenter;
uniform vec2 u_mousePos;
@@ -132,65 +129,41 @@ export class WebGLAsciiRenderer {
uniform bool u_showMagnifier;
uniform float u_aspect;
- // Blue Noise Dithering
float blueNoise(vec2 pos) {
- // Map screen coordinates to texture coordinates (64x64 texture)
vec2 noiseUV = pos / 64.0;
float noiseVal = texture2D(u_blueNoise, noiseUV).r;
- // Shift range to -0.5 to 0.5 for dither offset
return noiseVal - 0.5;
}
vec3 adjust(vec3 color) {
- // Exposure
color *= u_exposure;
-
- // Shadows / Highlights
float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
-
- // Shadows: lift dark areas
if (u_shadows > 0.0) {
float shadowFactor = (1.0 - luma) * u_shadows;
color = color + (vec3(1.0) - color) * shadowFactor * 0.5;
}
-
- // Highlights: dim bright areas
if (u_highlights > 0.0) {
float highlightFactor = luma * u_highlights;
color = color * (1.0 - highlightFactor * 0.5);
}
-
- // Contrast
color = (color - 0.5) * u_contrast + 0.5;
-
- // Saturation
luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
color = mix(vec3(luma), color, u_saturation);
-
- // Gamma
color = pow(max(color, 0.0), vec3(u_gamma));
-
return clamp(color, 0.0, 1.0);
}
- // Function to get average color from a cell using 5 samples (center + corners)
vec3 getAverageColor(vec2 cellCenterUV, vec2 cellSize) {
vec3 sum = vec3(0.0);
- vec2 halfSize = cellSize * 0.25; // Sample halfway to the edge
-
- // Center
+ vec2 halfSize = cellSize * 0.25;
sum += texture2D(u_image, cellCenterUV).rgb;
-
- // Corners
sum += texture2D(u_image, cellCenterUV + vec2(-halfSize.x, -halfSize.y)).rgb;
sum += texture2D(u_image, cellCenterUV + vec2(halfSize.x, -halfSize.y)).rgb;
sum += texture2D(u_image, cellCenterUV + vec2(-halfSize.x, halfSize.y)).rgb;
sum += texture2D(u_image, cellCenterUV + vec2(halfSize.x, halfSize.y)).rgb;
-
return sum / 5.0;
}
- // Sobel Filter - returns gradient magnitude and direction (approx)
vec2 sobelFilter(vec2 uv, vec2 cellSize) {
vec3 t = texture2D(u_image, uv + vec2(0.0, -cellSize.y)).rgb;
vec3 b = texture2D(u_image, uv + vec2(0.0, cellSize.y)).rgb;
@@ -200,8 +173,6 @@ export class WebGLAsciiRenderer {
vec3 tr = texture2D(u_image, uv + vec2(cellSize.x, -cellSize.y)).rgb;
vec3 bl = texture2D(u_image, uv + vec2(-cellSize.x, cellSize.y)).rgb;
vec3 br = texture2D(u_image, uv + vec2(cellSize.x, cellSize.y)).rgb;
-
- // Convert to luma
float lt = dot(t, vec3(0.299, 0.587, 0.114));
float lb = dot(b, vec3(0.299, 0.587, 0.114));
float ll = dot(l, vec3(0.299, 0.587, 0.114));
@@ -210,122 +181,82 @@ export class WebGLAsciiRenderer {
float ltr = dot(tr, vec3(0.299, 0.587, 0.114));
float lbl = dot(bl, vec3(0.299, 0.587, 0.114));
float lbr = dot(br, vec3(0.299, 0.587, 0.114));
-
- // Sobel kernels
- // Gx: -1 0 1
- // -2 0 2
- // -1 0 1
float gx = (ltr + 2.0*lr + lbr) - (ltl + 2.0*ll + lbl);
-
- // Gy: -1 -2 -1
- // 0 0 0
- // 1 2 1
float gy = (lbl + 2.0*lb + lbr) - (ltl + 2.0*lt + ltr);
-
float mag = sqrt(gx*gx + gy*gy);
return vec2(mag, atan(gy, gx));
}
void main() {
vec2 uv = v_texCoord;
-
- // Apply global zoom
uv = (uv - u_zoomCenter) / u_zoom + u_zoomCenter;
- // Magnifier logic
vec2 diff = (v_texCoord - u_mousePos);
diff.x *= u_aspect;
float dist = length(diff);
bool inMagnifier = u_showMagnifier && dist < u_magnifierRadius;
if (inMagnifier) {
- // Zoom towards mouse position inside the magnifier
uv = (v_texCoord - u_mousePos) / u_magnifierZoom + u_mousePos;
- // Also account for the global zoom background
uv = (uv - u_zoomCenter) / u_zoom + u_zoomCenter;
}
- // Calculate which cell we are in
vec2 cellCoords = floor(uv * u_gridSize);
vec2 uvInCell = fract(uv * u_gridSize);
-
- // Sample image at the center of the cell
vec2 cellSize = 1.0 / u_gridSize;
vec2 sampleUV = (cellCoords + 0.5) * cellSize;
- // Out of bounds check for zoomed UV
if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) {
discard;
}
vec3 color;
-
- // Denoise: 3x3 box blur (applied to the base sampling if enabled)
if (u_denoise) {
color = getAverageColor(sampleUV, cellSize * 2.0);
} else {
color = getAverageColor(sampleUV, cellSize);
}
- // Sharpening
if (u_sharpen > 0.0) {
vec3 blurred = getAverageColor(sampleUV, cellSize * 2.0);
color = color + (color - blurred) * u_sharpen;
}
- // Edge Detection Logic
if (u_edgeMode == 1) {
- // Simple Laplacian-like
vec2 texel = cellSize;
vec3 center = color;
vec3 top = getAverageColor(sampleUV + vec2(0.0, -texel.y), cellSize);
vec3 bottom = getAverageColor(sampleUV + vec2(0.0, texel.y), cellSize);
vec3 left = getAverageColor(sampleUV + vec2(-texel.x, 0.0), cellSize);
vec3 right = getAverageColor(sampleUV + vec2(texel.x, 0.0), cellSize);
-
vec3 edges = abs(center - top) + abs(center - bottom) + abs(center - left) + abs(center - right);
float edgeLum = dot(edges, vec3(0.2126, 0.7152, 0.0722));
-
- // Use Threshold
if (edgeLum > u_edgeThreshold * 0.1) {
color = mix(color, color * (1.0 - edgeLum * 2.0), 0.5);
}
-
} else if (u_edgeMode == 2) {
- // Sobel Gradient
vec2 sobel = sobelFilter(sampleUV, cellSize);
float edgeStr = clamp(sobel.x * 2.0, 0.0, 1.0);
-
if (edgeStr > u_edgeThreshold * 0.2) {
color = mix(color, vec3(0.0), edgeStr * 0.8);
}
-
} else if (u_edgeMode == 3) {
- // "Canny-like" (Sobel + gradient suppression)
vec2 sobel = sobelFilter(sampleUV, cellSize);
float mag = sobel.x;
float angle = sobel.y;
-
- // Non-maximum suppression (simplified)
vec2 dir = vec2(cos(angle), sin(angle)) * cellSize;
vec2 s1 = sobelFilter(sampleUV + dir, cellSize);
vec2 s2 = sobelFilter(sampleUV - dir, cellSize);
-
- // Use edge threshold
- if (mag < s1.x || mag < s2.x || mag < u_edgeThreshold * 0.3) { // scaled threshold
+ if (mag < s1.x || mag < s2.x || mag < u_edgeThreshold * 0.3) {
mag = 0.0;
} else {
- mag = 1.0; // Strong edge
+ mag = 1.0;
}
-
- // Apply strong crisp edges
color = mix(color, vec3(0.0), mag);
}
- // Apply adjustments (Contrast, etc.)
color = adjust(color);
- // Overlay blend-like effect (boost mid-contrast)
if (u_overlayStrength > 0.0) {
vec3 overlay = color;
vec3 result;
@@ -337,15 +268,10 @@ export class WebGLAsciiRenderer {
color = mix(color, result, u_overlayStrength);
}
- // Calculate luminance
float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
- // Apply Blue Noise dithering before character mapping
if (u_dither > 0.0) {
- // Use cell coordinates for stable dithering patterns
float noise = blueNoise(cellCoords);
-
- // Scale noise by dither strength and 1/charCount
luma = luma + noise * (1.0 / u_charCount) * u_dither;
luma = clamp(luma, 0.0, 1.0);
}
@@ -354,53 +280,42 @@ export class WebGLAsciiRenderer {
luma = 1.0 - luma;
}
- // Map luma to character index
float charIndex = floor(luma * (u_charCount - 1.0) + 0.5);
- // Sample character atlas
- // Use u_charSizeUV to scale, instead of just 1.0/u_charCount
- // x = charIndex * charWidthUV + uvInCell.x * charWidthUV
vec2 atlasUV = vec2(
(charIndex + uvInCell.x) * u_charSizeUV.x,
uvInCell.y * u_charSizeUV.y
);
- // --- MIPMAP FIX ---
- // Use zoomed 'uv' for derivatives to handle zoom correctly.
- // Multiply by 0.5 to bias towards sharper mipmaps (LOD bias).
- vec2 gradX = dFdx(uv) * u_gridSize * u_charSizeUV * 0.5;
- vec2 gradY = dFdy(uv) * u_gridSize * u_charSizeUV * 0.5;
+ float charAlpha;
+ ${hasDerivatives && hasLod ? `
+ vec2 gradX = dFdx(uv) * u_gridSize * u_charSizeUV * 0.5;
+ vec2 gradY = dFdy(uv) * u_gridSize * u_charSizeUV * 0.5;
+ charAlpha = texture2DGradEXT(u_atlas, atlasUV, gradX, gradY).r;
+ ` : `
+ charAlpha = texture2D(u_atlas, atlasUV).r;
+ `}
- // Use texture2DGradEXT to sample with explicit gradients, resolving cell boundary artifacts
- float charAlpha = texture2DGradEXT(u_atlas, atlasUV, gradX, gradY).r;
-
- // Loup border effect
if (u_showMagnifier) {
float edgeWidth = 0.005;
if (dist > u_magnifierRadius - edgeWidth && dist < u_magnifierRadius) {
charAlpha = 1.0;
- color = vec3(1.0, 1.0, 1.0); // White border for the loupe
+ color = vec3(1.0, 1.0, 1.0);
}
}
- // Vignette
if (u_vignette > 0.0) {
- float dist = distance(uv, vec2(0.5));
- // Smoothsoft vignette
- float vig = smoothstep(0.8 + (1.0 - u_vignette) * 0.5, 0.2, dist);
+ float d = distance(uv, vec2(0.5));
+ float vig = smoothstep(0.8 + (1.0 - u_vignette) * 0.5, 0.2, d);
charAlpha *= vig;
}
- // Scanlines
if (u_scanlines > 0.0) {
- // Sine wave based on grid rows
float scan = 0.5 + 0.5 * sin(uv.y * u_gridSize.y * 3.14159 * 2.0);
- // Blend scanline effect based on strength
float scanEffect = mix(1.0, scan, u_scanlines * 0.5);
charAlpha *= scanEffect;
}
- // Mono Color
vec3 finalColor;
if (u_color) {
finalColor = color;
@@ -494,28 +409,24 @@ export class WebGLAsciiRenderer {
this.fontFamily = fontName;
const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
+ const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) return;
- const fontSize = 32; // Higher resolution for atlas
- // Add padding to prevent bleeding
+ const fontSize = 32;
const padding = 4;
ctx.font = `${fontSize}px ${fontName}`;
- // Measure first char to get dimensions
const metrics = ctx.measureText('W');
const charContentWidth = Math.ceil(metrics.width);
const charContentHeight = Math.ceil(fontSize * 1.2);
- // Full cell size including padding
const charWidth = charContentWidth + padding * 2;
const charHeight = charContentHeight + padding * 2;
const neededWidth = charWidth * charSet.length;
const neededHeight = charHeight;
- // Calculate Next Power of Two
const nextPowerOfTwo = (v: number) => Math.pow(2, Math.ceil(Math.log(v) / Math.log(2)));
const texWidth = nextPowerOfTwo(neededWidth);
const texHeight = nextPowerOfTwo(neededHeight);
@@ -529,9 +440,6 @@ export class WebGLAsciiRenderer {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < charSet.length; i++) {
- // Draw character centered in its padded cell
- // x position: start of cell (i * charWidth) + padding
- // y position: padding
ctx.fillText(charSet[i], i * charWidth + padding, padding);
}
@@ -545,7 +453,6 @@ export class WebGLAsciiRenderer {
gl.bindTexture(gl.TEXTURE_2D, this.textures.atlas);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
- // Use Mipmaps for smoother downscaling (fixes shimmering/aliasing)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
@@ -572,13 +479,12 @@ export class WebGLAsciiRenderer {
const gl = this.gl;
const u = this.uniformLocations;
+ if (!this.program) return;
gl.useProgram(this.program);
- // Update Atlas if needed (expensive check inside)
this.updateAtlas(options.charSetContent, options.fontFamily || 'monospace');
if (this.charAtlas) {
gl.uniform1f(u['u_charCount'], this.charAtlas.count);
- // Pass the normalized size of one character cell for UV mapping
gl.uniform2f(u['u_charSizeUV'],
this.charAtlas.charWidth / this.charAtlas.width,
this.charAtlas.charHeight / this.charAtlas.height
@@ -597,20 +503,18 @@ export class WebGLAsciiRenderer {
gl.uniform1f(u['u_dither'], options.dither || 0.0);
gl.uniform1i(u['u_denoise'], options.denoise ? 1 : 0);
gl.uniform1f(u['u_sharpen'], options.sharpen || 0.0);
- gl.uniform1f(u['u_edgeThreshold'], options.edgeThreshold || 0.5); // Default to mid
+ gl.uniform1f(u['u_edgeThreshold'], options.edgeThreshold || 0.5);
gl.uniform1f(u['u_shadows'], options.shadows || 0.0);
gl.uniform1f(u['u_highlights'], options.highlights || 0.0);
gl.uniform1f(u['u_scanlines'], options.scanlines || 0.0);
gl.uniform1f(u['u_vignette'], options.vignette || 0.0);
- // Parse hex color
const hex = options.monoColor || '#ffffff';
const r = parseInt(hex.slice(1, 3), 16) / 255.0;
const g = parseInt(hex.slice(3, 5), 16) / 255.0;
const b = parseInt(hex.slice(5, 7), 16) / 255.0;
gl.uniform3f(u['u_monoColor'], r, g, b);
- // Zoom & Magnifier
gl.uniform1f(u['u_zoom'], options.zoom || 1.0);
gl.uniform2f(u['u_zoomCenter'], options.zoomCenter?.x ?? 0.5, options.zoomCenter?.y ?? 0.5);
gl.uniform2f(u['u_mousePos'], options.mousePos?.x ?? -1.0, options.mousePos?.y ?? -1.0);
@@ -624,7 +528,6 @@ export class WebGLAsciiRenderer {
const gl = this.gl;
const texture = gl.createTexture();
if (!texture) return;
-
this.textures.blueNoise = texture;
const image = new Image();
@@ -636,27 +539,16 @@ export class WebGLAsciiRenderer {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
- this.requestRender();
};
}
- // Helper to trigger a redraw if we have a controller reference, otherwise just rely on next loop
- private requestRender() {
- // Since we don't have a direct reference to the controller here,
- // and we are in a render loop managed by the controller,
- // the texture will just appear on the next frame.
- }
-
updateTexture(image: HTMLImageElement) {
if (this.lastImage === image && this.textures.image) return;
-
const gl = this.gl;
-
if (this.textures.image) gl.deleteTexture(this.textures.image);
const texture = gl.createTexture();
if (!texture) throw new Error('Failed to create texture');
this.textures.image = texture;
-
gl.bindTexture(gl.TEXTURE_2D, this.textures.image);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
@@ -669,16 +561,13 @@ export class WebGLAsciiRenderer {
draw() {
const gl = this.gl;
const program = this.program;
-
if (!program || !this.textures.image || !this.textures.atlas || !this.buffers.position || !this.buffers.texCoord) return;
gl.useProgram(program);
-
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
- // Attributes
const posLoc = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(posLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.position);
@@ -689,7 +578,6 @@ export class WebGLAsciiRenderer {
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.texCoord);
gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0);
- // Bind Textures
const u = this.uniformLocations;
gl.uniform1i(u['u_image'], 0);
gl.activeTexture(gl.TEXTURE0);
@@ -715,32 +603,14 @@ export class WebGLAsciiRenderer {
this.draw();
}
- /**
- * Dispose of all WebGL resources.
- * Call this when the renderer is no longer needed.
- */
dispose(): void {
const gl = this.gl;
-
- if (this.textures.image) {
- gl.deleteTexture(this.textures.image);
- }
- if (this.textures.atlas) {
- gl.deleteTexture(this.textures.atlas);
- }
- if (this.textures.blueNoise) {
- gl.deleteTexture(this.textures.blueNoise);
- }
- if (this.buffers.position) {
- gl.deleteBuffer(this.buffers.position);
- }
- if (this.buffers.texCoord) {
- gl.deleteBuffer(this.buffers.texCoord);
- }
- if (this.program) {
- gl.deleteProgram(this.program);
- }
-
+ if (this.textures.image) gl.deleteTexture(this.textures.image);
+ if (this.textures.atlas) gl.deleteTexture(this.textures.atlas);
+ if (this.textures.blueNoise) gl.deleteTexture(this.textures.blueNoise);
+ if (this.buffers.position) gl.deleteBuffer(this.buffers.position);
+ if (this.buffers.texCoord) gl.deleteBuffer(this.buffers.texCoord);
+ if (this.program) gl.deleteProgram(this.program);
this.textures = {};
this.buffers = {};
this.program = null;
@@ -748,29 +618,21 @@ export class WebGLAsciiRenderer {
this.lastImage = null;
}
- // Kept for backward compatibility or specialized updates
updateMagnifier(options: MagnifierOptions) {
const gl = this.gl;
const program = this.program;
-
if (!program) return;
-
gl.useProgram(program);
-
- // Only update magnifier-related uniforms (using cached locations)
const u = this.uniformLocations;
const mousePos = options.mousePos ?? { x: -1, y: -1 };
gl.uniform2f(u['u_mousePos'], mousePos.x, mousePos.y);
gl.uniform1f(u['u_magnifierRadius'], options.magnifierRadius || 0.03);
gl.uniform1f(u['u_magnifierZoom'], options.magnifierZoom || 2.0);
gl.uniform1i(u['u_showMagnifier'], options.showMagnifier ? 1 : 0);
-
if (options.zoom !== undefined) {
gl.uniform1f(u['u_zoom'], options.zoom || 1.0);
gl.uniform2f(u['u_zoomCenter'], options.zoomCenter?.x ?? 0.5, options.zoomCenter?.y ?? 0.5);
}
-
- // We can just call draw here as it's lightweight
this.draw();
}
}
diff --git a/src/styles/global.css b/src/styles/global.css
index d951886..ec5fcb4 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1,6 +1,8 @@
:root {
--bg-color: #000000;
--text-color: #FFFFFF;
+ --accent-color: #FF6700;
+ /* Safety Orange */
--font-mono: 'JetBrains Mono', monospace;
}
@@ -28,7 +30,7 @@ button {
button:hover {
opacity: 1;
- background: rgba(255, 103, 0, 0.1);
+ background: color-mix(in srgb, var(--accent-color), transparent 90%);
}
a {