Marquee
The previous page treats a tileset as a lookup: partition the screen into
cells, and let a hash of (cell, time) pick a character for each. The
choice function is the whole show.
Flip the axes. Instead of many tiny cells with per-cell choices, sample from a single virtual cell that fills the frame and drive the character choice from a scrolling world x — a linear coordinate that advances one integer per glyph. Now the whole screen shows one giant glyph, and the next slides in from the right as the coordinate crosses the next integer. A classic marquee, over the same atlas.
The scan is a two-line change: worldX = uv.x / FIT + pan, idx = floor(worldX) mod count, cell-local UV = (fract(worldX), y). Feed
@tileset the marquee’s own text (“Hello, World!”) so the atlas is the
string, one cell per character — including the space at position 6, whose
empty cell reads on screen exactly as it should: a gap.
For a seamless loop use the @loop directive. Over LOOP = 13.0
seconds the pan advances iTilesCount character-widths — one full pass of
the string — so the visible content at t = LOOP matches t = 0 exactly,
and the mod wrap closes the seam. The transport scrubber comes with it,
so you can drop the camera anywhere in the sentence.
Shown here via #+attr_diff as a changeset from the previous Tileset.
The grid partition, per-cell periods, and hash-based choice function are
gone; a single pan and a character-unit world x are in.
--- letters-tileset-shader
+++ letters-marquee-shader
@@ -1,42 +1,48 @@
-// @tileset iTiles "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!,@#$%^&*()-+="
+// @tileset iTiles "Hello, World!"
+// @loop 13.0
uniform sampler2D iTiles;
uniform vec2 iTilesGrid; // (cols, rows) laid out in the atlas
uniform float iTilesCount; // number of cells actually filled
-float hash(vec2 p){
- return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
-}
-
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
- // Partition the screen into fixed 40-pixel square cells. Row and column
- // indices come from the integer part of fragCoord/CELL_PX; the local UV
- // inside the cell is the fractional part.
- const float CELL_PX = 40.0;
- vec2 cell = floor(fragCoord / CELL_PX);
- vec2 uvInCell = fract(fragCoord / CELL_PX);
-
- // Choice function: pick a character index per cell, on a *per-cell* random
- // period so each cell ticks on its own clock — some flip several times a
- // second, others hold for seconds at a time. Swap the hash for any
- // (cell, time) function to get radiating rings, falling scrolls, waves…
- float period = 0.15 + hash(cell + vec2(1.7)) * 2.5;
- float phase = hash(cell + vec2(4.3)) * period;
- float stepT = floor((iTime + phase) / period);
- float idx = floor(hash(cell + vec2(stepT * 0.317)) * iTilesCount);
-
- // Decode idx → atlas (col, row), sample the atlas at cell-local UV.
- // LINEAR filtering + SDF threshold keeps each glyph one-pixel sharp.
- vec2 atlasCell = vec2(mod(idx, iTilesGrid.x), floor(idx / iTilesGrid.x));
- vec2 atlasUV = (atlasCell + uvInCell) / iTilesGrid;
- float mask = texture2D(iTiles, atlasUV).a;
+ // aspect-corrected coords centered on the middle; y in <-0.5, 0.5>
+ vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
+
+ // Character-unit world x: one integer per glyph. FIT sizes a single cell
+ // to 90% of the screen height, matching the single-glyph pages, so one
+ // giant character fills the frame and a slice of the next slides in from
+ // the right. Over one LOOP the pan advances iTilesCount character-widths
+ // — a full pass of the string — and `mod(idx, count)` closes the seam
+ // exactly, so t=0 and t=LOOP show identical frames.
+ const float FIT = 0.9;
+ float pan = iTime / 13.0 * iTilesCount;
+ float worldX = uv.x / FIT + pan;
+
+ // Choice function: which character sits under this fragment. Wrap so the
+ // string tiles infinitely as the camera pans right — the '!' end butts
+ // straight up against the 'H' start, one continuous marquee ribbon.
+ float idx = mod(floor(worldX), iTilesCount);
+
+ // Cell-local UV inside the glyph's atlas cell — fract wraps the horizontal
+ // scan, aspect-corrected y maps directly with the same 90% fit
+ vec2 cellUV = vec2(fract(worldX), uv.y / FIT + 0.5);
+
+ // Decode idx → atlas (col, row), sample the atlas at cell-local UV. The
+ // atlas is uploaded with UNPACK_FLIP_Y_WEBGL=true so canvas row 0 (where
+ // 'H' was drawn) lives at the *top* of texture v, not the bottom — invert
+ // the row so ordered indices actually hit their glyphs. LINEAR filtering
+ // + SDF threshold keeps each glyph one-pixel sharp.
+ vec2 atlasCell = vec2(mod(idx, iTilesGrid.x),
+ (iTilesGrid.y - 1.0) - floor(idx / iTilesGrid.x));
+ vec2 atlasUV = (atlasCell + cellUV) / iTilesGrid;
+ float mask = 0.0;
+ if(cellUV.y >= 0.0 && cellUV.y <= 1.0){
+ mask = texture2D(iTiles, atlasUV).a;
+ }
float aa = fwidth(mask);
float glyph = smoothstep(0.5 - aa, 0.5 + aa, mask);
- // Highlight layer: a slow diagonal sine over the grid modulates the fill
- // brightness. Independent of the choice function, composable on top.
- float wave = 0.5 + 0.5 * sin(cell.x * 0.15 + cell.y * 0.08 - iTime * 1.5);
-
- vec3 col = mix(vec3(0.0), iAccent, glyph) * (0.3 + 0.7 * wave);
+ vec3 col = mix(vec3(0.0), iAccent, glyph);
fragColor = vec4(col, 1.0);
}
|
|
// @tileset iTiles "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!,@#$%^&*()-+="
uniform sampler2D iTiles;
uniform vec2 iTilesGrid; // (cols, rows) laid out in the atlas
uniform float iTilesCount; // number of cells actually filled
float hash(vec2 p){
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Partition the screen into fixed 40-pixel square cells. Row and column
// indices come from the integer part of fragCoord/CELL_PX; the local UV
// inside the cell is the fractional part.
const float CELL_PX = 40.0;
vec2 cell = floor(fragCoord / CELL_PX);
vec2 uvInCell = fract(fragCoord / CELL_PX);
// Choice function: pick a character index per cell, on a *per-cell* random
// period so each cell ticks on its own clock — some flip several times a
// second, others hold for seconds at a time. Swap the hash for any
// (cell, time) function to get radiating rings, falling scrolls, waves…
float period = 0.15 + hash(cell + vec2(1.7)) * 2.5;
float phase = hash(cell + vec2(4.3)) * period;
float stepT = floor((iTime + phase) / period);
float idx = floor(hash(cell + vec2(stepT * 0.317)) * iTilesCount);
// Decode idx → atlas (col, row), sample the atlas at cell-local UV.
// LINEAR filtering + SDF threshold keeps each glyph one-pixel sharp.
vec2 atlasCell = vec2(mod(idx, iTilesGrid.x), floor(idx / iTilesGrid.x));
vec2 atlasUV = (atlasCell + uvInCell) / iTilesGrid;
float mask = texture2D(iTiles, atlasUV).a;
float aa = fwidth(mask);
float glyph = smoothstep(0.5 - aa, 0.5 + aa, mask);
// Highlight layer: a slow diagonal sine over the grid modulates the fill
// brightness. Independent of the choice function, composable on top.
float wave = 0.5 + 0.5 * sin(cell.x * 0.15 + cell.y * 0.08 - iTime * 1.5);
vec3 col = mix(vec3(0.0), iAccent, glyph) * (0.3 + 0.7 * wave);
fragColor = vec4(col, 1.0);
}