Hello, world!
Everything the previous page does works on a whole string with almost no
new code — the rasterizer already has fillText, and the distance transform
is pixel-agnostic. So @sdf is generalised: whatever sits between the quotes
becomes the atlas.
For a multi-character quoted argument, the engine measures the string, sizes
the offscreen canvas to fit (a fixed 512-pixel height, natural width from
Canvas2D’s measureText), rasterises the whole string, and runs the same
2D Felzenszwalb distance transform on the rectangle. The atlas is no longer
square, so the shader needs to know its aspect ratio; the engine auto-sets a
companion uniform float <NAME>Aspect (declared as iGlyphAspect here,
matching the sampler’s name) to canvas.width / canvas.height.
With aspect in hand, the display math is a standard fit-contain: shrink the atlas rectangle uniformly to the largest size that fits inside the viewport with a 10% margin. For a long string, that’s a fit-to-width, and the result is a slim horizontal band centered vertically — a title bar.
Shown here via #+attr_diff as a changeset from the previous letter A (SDF)
— the cinematic zoom loop is gone (this page is a static display of text),
replaced by the aspect-fit sampling.
--- letter-a-sdf-shader
+++ hello-world-shader
@@ -1,37 +1,33 @@
-// @sdf iGlyph "A"
-// @loop 12.0
+// @sdf iGlyph "Hello, world!"
uniform sampler2D iGlyph;
+uniform float iGlyphAspect; // atlas width / height, auto-set by the engine
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// aspect-corrected coords centered on the middle; y in <-0.5, 0.5>
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
- // one 12-second loop: an eased ping-pong zoom from a wide shot (0.6×) to
- // a close inspection (7×) and back, panned on a Lissajous 1:2 so the
- // camera visits the apex, both legs, and the crossbar over one cycle.
- // Pan is scaled by 1/zoom so a fixed sweep tracks the same visible
- // fraction of the texture regardless of magnification.
- float t = iTime * 6.2831853 / 12.0;
- float zoom = mix(0.6, 7.0, 0.5 - 0.5 * cos(t));
- vec2 pan = vec2(sin(t) * 0.35, sin(t * 2.0) * 0.20) / zoom;
-
- // apply zoom + pan, then map into the glyph's [0,1] atlas domain — dividing
- // by zoom shrinks the visible slice, so higher zoom = tighter close-up
- vec2 gUV = uv / (0.9 * zoom) + 0.5 + pan;
+ // fit-contain the atlas rectangle inside the screen with a 10% margin.
+ // For "Hello, world!" the atlas is much wider than the screen, so this
+ // resolves as fit-to-width — the string spans 90% of the reading column
+ // and sits as a slim horizontal band centered vertically.
+ float scrAspect = iResolution.x / iResolution.y;
+ vec2 fit = iGlyphAspect > scrAspect
+ ? vec2(0.9 * scrAspect, 0.9 * scrAspect / iGlyphAspect)
+ : vec2(0.9 * iGlyphAspect, 0.9);
+ vec2 gUV = uv / fit + 0.5;
// read the SDF alpha; outside the texture there is no letter
float mask = 0.0;
if(gUV.x >= 0.0 && gUV.x <= 1.0 && gUV.y >= 0.0 && gUV.y <= 1.0){
mask = texture2D(iGlyph, gUV).a;
}
- // same fwidth-scaled threshold as before — but the texture now stores a
- // distance field, so `fwidth(mask)` scales with zoom and the smoothstep
- // window collapses back to one output pixel regardless of magnification
+ // same fwidth-scaled threshold as before — no zoom, but any future scaling
+ // or panning of this shader will keep the edge one output pixel wide
float aa = fwidth(mask);
float glyph = smoothstep(0.5 - aa, 0.5 + aa, mask);
vec3 col = mix(vec3(0.0), iAccent, glyph);
fragColor = vec4(col, 1.0);
}
|
|
// @sdf iGlyph "A"
// @loop 12.0
uniform sampler2D iGlyph;
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// aspect-corrected coords centered on the middle; y in <-0.5, 0.5>
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
// one 12-second loop: an eased ping-pong zoom from a wide shot (0.6×) to
// a close inspection (7×) and back, panned on a Lissajous 1:2 so the
// camera visits the apex, both legs, and the crossbar over one cycle.
// Pan is scaled by 1/zoom so a fixed sweep tracks the same visible
// fraction of the texture regardless of magnification.
float t = iTime * 6.2831853 / 12.0;
float zoom = mix(0.6, 7.0, 0.5 - 0.5 * cos(t));
vec2 pan = vec2(sin(t) * 0.35, sin(t * 2.0) * 0.20) / zoom;
// apply zoom + pan, then map into the glyph's [0,1] atlas domain — dividing
// by zoom shrinks the visible slice, so higher zoom = tighter close-up
vec2 gUV = uv / (0.9 * zoom) + 0.5 + pan;
// read the SDF alpha; outside the texture there is no letter
float mask = 0.0;
if(gUV.x >= 0.0 && gUV.x <= 1.0 && gUV.y >= 0.0 && gUV.y <= 1.0){
mask = texture2D(iGlyph, gUV).a;
}
// same fwidth-scaled threshold as before — but the texture now stores a
// distance field, so `fwidth(mask)` scales with zoom and the smoothstep
// window collapses back to one output pixel regardless of magnification
float aa = fwidth(mask);
float glyph = smoothstep(0.5 - aa, 0.5 + aa, mask);
vec3 col = mix(vec3(0.0), iAccent, glyph);
fragColor = vec4(col, 1.0);
}