@kusuromi/ascii-shader-renderer
v0.3.0
Published
Two-pass WebGL renderer for the ASCII shader.
Readme
@kusuromi/ascii-shader-renderer
The single WebGL renderer for the ASCII shader. Computes everything on the GPU in two passes, one pixel per grid cell.
How it renders
- Field pass — a fragment shader computes the per-cell luminance field: a float32 value-noise through
hash21→noise2→ 4-octavefbm, blended with two travelling waves and a sweep term, plus an optional pointer glow; finalpow(clamp(lum * lightness − 0.09, 0, 1), contrast)is written to an RGBA8 texture (NEAREST). This shader is the single source of truth for the field formula. - Composite pass — a fullscreen quad samples the field, picks a glyph from a 16-column atlas (
floor(min(lum * glyphCount, …))), shades it (188–246 of 255), and modulates alpha by glyph coverage (quantized to 0.01 steps).
The glyph atlas is rasterized once per settings change with a Canvas 2D context (the only 2D usage — text cannot be rasterized on the GPU), with vertical ink-centering baked in.
Usage
import { createAsciiShaderRenderer } from "@kusuromi/ascii-shader-renderer";
const gl = canvas.getContext("webgl");
if (gl) {
const renderer = createAsciiShaderRenderer(gl);
function frame(time: number) {
renderer.render({
width: canvas.clientWidth,
height: canvas.clientHeight,
dpr: Math.min(window.devicePixelRatio || 1, 2),
time,
settings,
pointer: { currentX: 0.5, currentY: 0.5 },
});
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}The renderer pairs well with the animation loop, pointer tracking, and resize handling from @kusuromi/ascii-shader-core's createAsciiEngineHost; for React, use @kusuromi/ascii-shader-react (which depends on @kusuromi/ascii-shader-renderer and @kusuromi/ascii-shader-core).
Settings and pointer
render accepts the same AsciiSettings shape as the rest of the monorepo (see @kusuromi/ascii-shader-core) plus a normalized pointer position in 0..1 space.
Texture scale
The field shader converts cell positions to canvas pixels (worldPx = gl_FragCoord.xy * uCellSize) and calculates uv as worldPx / WORLD_SCALE. WORLD_SCALE is defined in the shader as:
#define WORLD_SCALE 1600.0It means one texture-space unit represents 1600 canvas pixels. The base periods of the two travelling waves are approximately 1600 / (frequency × 2.8) for waveA and 1600 / (frequency × 5.2) for waveB. At frequency = 4.5, these are about 127 px and 68 px respectively; the FBM octaves add finer detail. Because these distances are in pixels, the details keep the same size on any screen. Resizing the window changes only how many cells and how much of the field are visible; the texture scale and feature proportions do not change.
WORLD_SCALE is a shader constant, not a uniform: it is an intrinsic property of this shader and is not a consumer-configurable setting. The value 1600 was chosen as a fixed design reference because it gives the documented ~127 px and ~68 px base periods at the maximum frequency of 4.5. Those details stay at a useful, consistent pixel size instead of being rescaled to fit each viewport.
Version 0.1.0 settings warning: If you tuned settings against 0.1.0, expect the rendered image to change after upgrading. Previously,
uvwas normalized to0…1across the entire viewport at every screen size, so the texture stretched to fill the canvas. Existingfrequencyvalues therefore produce a different texture scale now, and bothfrequencyandlightnessmay need to be retuned for the desired appearance.
Lifecycle
debug()— reads back the luminance field texture ({ columns, rows, sample: number[] }).dispose()— releases all GL objects.- On
webglcontextlost, dispose the renderer; onwebglcontextrestored, create a new one — all GL objects are invalidated by the loss.
