feat: Add background color setting and apply it to HTML and WebGL output.

This commit is contained in:
syntaxbullet
2026-02-10 21:08:38 +01:00
parent 2cdc9bd0b6
commit cabf963e94
6 changed files with 64 additions and 9 deletions

View File

@@ -194,6 +194,19 @@ import { ChevronDown } from "@lucide/astro";
<span class="label">TINT</span> <span class="label">TINT</span>
</div> </div>
<!-- Background Color Picker -->
<div class="tui-color-btn" title="Background Color">
<input
type="color"
id="input-bg-color"
value="#000000"
/>
<span
id="bg-color-swatch-display"
class="color-swatch"></span>
<span class="label">BG</span>
</div>
<TuiToggle <TuiToggle
id="toggle-denoise" id="toggle-denoise"
label="Denoise" label="Denoise"

View File

@@ -132,7 +132,8 @@ export class AsciiController {
highlights: 0, highlights: 0,
scanlines: 0, scanlines: 0,
vignette: 0, vignette: 0,
monoColor: '#ffffff' monoColor: '#ffffff',
backgroundColor: '#000000'
}; };
} }
@@ -260,7 +261,8 @@ export class AsciiController {
highlights: suggestions.highlights ?? this.settings.highlights, highlights: suggestions.highlights ?? this.settings.highlights,
scanlines: suggestions.scanlines ?? this.settings.scanlines, scanlines: suggestions.scanlines ?? this.settings.scanlines,
vignette: suggestions.vignette ?? this.settings.vignette, vignette: suggestions.vignette ?? this.settings.vignette,
monoColor: suggestions.monoColor ?? this.settings.monoColor monoColor: suggestions.monoColor ?? this.settings.monoColor,
backgroundColor: suggestions.backgroundColor ?? this.settings.backgroundColor
}; };
this.onSettingsChange?.(); this.onSettingsChange?.();

View File

@@ -80,7 +80,7 @@ export class AsciiExporter {
heightRows: number heightRows: number
): string { ): string {
const { pixels } = this.getPixels(img, widthCols, heightRows); const { pixels } = this.getPixels(img, widthCols, heightRows);
let output = `<pre style="font-family: monospace; line-height: 1; letter-spacing: 0; background-color: #000; color: #fff; font-size: 8px;">`; let output = `<pre style="font-family: monospace; line-height: 1; letter-spacing: 0; background-color: ${settings.backgroundColor}; color: #fff; font-size: 8px;">`;
const charSet = CHAR_SETS[settings.charSet] || CHAR_SETS.standard; const charSet = CHAR_SETS[settings.charSet] || CHAR_SETS.standard;
const charCount = charSet.length; const charCount = charSet.length;
@@ -101,13 +101,11 @@ export class AsciiExporter {
// Adjust color for display // Adjust color for display
const adjColor = this.adjustColor(r, g, b, settings); const adjColor = this.adjustColor(r, g, b, settings);
const finalColor = settings.color ? adjColor : this.hexToRgb(settings.monoColor);
// Recalculate luma from adjusted color for char selection // Recalculate luma from adjusted color for char selection
let finalLuma = (0.2126 * adjColor.r + 0.7152 * adjColor.g + 0.0722 * adjColor.b) / 255; let finalLuma = (0.2126 * adjColor.r + 0.7152 * adjColor.g + 0.0722 * adjColor.b) / 255;
// Adjust luma curve for char selection (gamma/contrast/exposure) explicitly?
// Actually `adjustColor` does that.
if (settings.invert) finalLuma = 1.0 - finalLuma; if (settings.invert) finalLuma = 1.0 - finalLuma;
const charIndex = Math.floor(finalLuma * (charCount - 1) + 0.5); const charIndex = Math.floor(finalLuma * (charCount - 1) + 0.5);
@@ -116,7 +114,7 @@ export class AsciiExporter {
// Escape HTML // Escape HTML
const safeChar = char === '<' ? '&lt;' : char === '>' ? '&gt;' : char === '&' ? '&amp;' : char; const safeChar = char === '<' ? '&lt;' : char === '>' ? '&gt;' : char === '&' ? '&amp;' : char;
const colorStyle = `color: rgb(${Math.round(adjColor.r)}, ${Math.round(adjColor.g)}, ${Math.round(adjColor.b)})`; const colorStyle = `color: rgb(${Math.round(finalColor.r)}, ${Math.round(finalColor.g)}, ${Math.round(finalColor.b)})`;
output += `<span style="${colorStyle}">${safeChar}</span>`; output += `<span style="${colorStyle}">${safeChar}</span>`;
} }
output += "<br>"; output += "<br>";
@@ -177,4 +175,11 @@ export class AsciiExporter {
b: Math.max(0, Math.min(255, bNorm * 255)) b: Math.max(0, Math.min(255, bNorm * 255))
}; };
} }
private static hexToRgb(hex: string): { r: number, g: number, b: number } {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return { r, g, b };
}
} }

View File

@@ -30,6 +30,7 @@ export interface AsciiOptions {
scanlines?: number; scanlines?: number;
vignette?: number; vignette?: number;
monoColor?: string; monoColor?: string;
backgroundColor?: string;
} }
export interface AsciiResult { export interface AsciiResult {
@@ -69,6 +70,7 @@ export interface AsciiSettings {
scanlines: number; scanlines: number;
vignette: number; vignette: number;
monoColor: string; monoColor: string;
backgroundColor: string;
} }
// ============= Constants ============= // ============= Constants =============

View File

@@ -243,6 +243,19 @@ export class UIBindings {
} }
}); });
} }
const bgColorInput = document.getElementById('input-bg-color') as HTMLInputElement;
const bgColorSwatch = document.getElementById('bg-color-swatch-display');
if (bgColorInput) {
bgColorInput.addEventListener('input', () => {
if (this.isUpdatingUI) return;
this.controller.setSetting('backgroundColor', bgColorInput.value);
if (bgColorSwatch) {
bgColorSwatch.style.backgroundColor = bgColorInput.value;
}
});
}
} }
// ============= Segments ============= // ============= Segments =============
@@ -552,6 +565,18 @@ export class UIBindings {
} }
} }
const bgColorInput = document.getElementById('input-bg-color') as HTMLInputElement;
const bgColorSwatch = document.getElementById('bg-color-swatch-display');
if (bgColorInput && settings.backgroundColor) {
if (bgColorInput.value !== settings.backgroundColor) {
bgColorInput.value = settings.backgroundColor;
}
if (bgColorSwatch) {
bgColorSwatch.style.backgroundColor = settings.backgroundColor;
}
}
this.updateQueueDisplay(); this.updateQueueDisplay();
this.isUpdatingUI = false; this.isUpdatingUI = false;

View File

@@ -21,6 +21,7 @@ export interface RenderOptions {
scanlines?: number; scanlines?: number;
vignette?: number; vignette?: number;
monoColor?: string; monoColor?: string;
backgroundColor?: string;
zoom?: number; zoom?: number;
zoomCenter?: { x: number; y: number }; zoomCenter?: { x: number; y: number };
mousePos?: { x: number; y: number }; mousePos?: { x: number; y: number };
@@ -120,6 +121,7 @@ export class WebGLAsciiRenderer {
uniform float u_scanlines; uniform float u_scanlines;
uniform float u_vignette; uniform float u_vignette;
uniform vec3 u_monoColor; uniform vec3 u_monoColor;
uniform vec3 u_backgroundColor;
uniform float u_zoom; uniform float u_zoom;
uniform vec2 u_zoomCenter; uniform vec2 u_zoomCenter;
@@ -323,7 +325,7 @@ export class WebGLAsciiRenderer {
finalColor = u_monoColor; finalColor = u_monoColor;
} }
gl_FragColor = vec4(finalColor * charAlpha, charAlpha); gl_FragColor = vec4(mix(u_backgroundColor, finalColor, charAlpha), 1.0);
} }
`; `;
@@ -358,7 +360,7 @@ export class WebGLAsciiRenderer {
'u_invert', 'u_color', 'u_overlayStrength', 'u_edgeMode', 'u_invert', 'u_color', 'u_overlayStrength', 'u_edgeMode',
'u_dither', 'u_denoise', 'u_dither', 'u_denoise',
'u_sharpen', 'u_edgeThreshold', 'u_shadows', 'u_highlights', 'u_sharpen', 'u_edgeThreshold', 'u_shadows', 'u_highlights',
'u_scanlines', 'u_vignette', 'u_monoColor', 'u_scanlines', 'u_vignette', 'u_monoColor', 'u_backgroundColor',
'u_zoom', 'u_zoomCenter', 'u_mousePos', 'u_zoom', 'u_zoomCenter', 'u_mousePos',
'u_magnifierRadius', 'u_magnifierZoom', 'u_showMagnifier', 'u_aspect' 'u_magnifierRadius', 'u_magnifierZoom', 'u_showMagnifier', 'u_aspect'
]; ];
@@ -515,6 +517,12 @@ export class WebGLAsciiRenderer {
const b = parseInt(hex.slice(5, 7), 16) / 255.0; const b = parseInt(hex.slice(5, 7), 16) / 255.0;
gl.uniform3f(u['u_monoColor'], r, g, b); gl.uniform3f(u['u_monoColor'], r, g, b);
const bgHex = options.backgroundColor || '#000000';
const br = parseInt(bgHex.slice(1, 3), 16) / 255.0;
const bg = parseInt(bgHex.slice(3, 5), 16) / 255.0;
const bb = parseInt(bgHex.slice(5, 7), 16) / 255.0;
gl.uniform3f(u['u_backgroundColor'], br, bg, bb);
gl.uniform1f(u['u_zoom'], options.zoom || 1.0); 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_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); gl.uniform2f(u['u_mousePos'], options.mousePos?.x ?? -1.0, options.mousePos?.y ?? -1.0);