Stretch and stack
Stretch-and-stack flow on lattices: the unit cell warps under the flow and restacks, driven by the shared lattice computation.
Source
common.glsl
// Stretch and Stack — driven by the shared JS lattice driver (script.js).
// uLattice (the lattice mat2) and uFlowInv (inverse of the per-frame flow) are
// pushed from script.js; uScale comes from the UI. UnitCell warps the previous
// frame by uFlowInv to animate the stretch, like the original Shadertoy.
#include "../shared/2d_utils.glsl"
#include "../shared/coord_utils.glsl"
bool fadeMoves = false; // fade the warped trail instead of hard-clearing it
vec4 backCol = vec4(1., 1., 1., 0.);
vec4 dotCol = vec4(.8, .05, .04, .1);
vec4 edgeCol = vec4(.651, .669, .95, .1);
// Screen origin for this view: horizontally centred, one third up from the bottom.
vec2 viewOffset(in vec3 res) { return vec2(res.x * 0.5, res.y / 3.); } fullLattice.glsl
// Whole-plane view. With basis b1 = (a, c) and b2 = (-b, d) (the driver
// keeps b1 rightward and b2 leftward), UnitCell draws its picture on a
// two-box staircase cell: (-a, 0)x(0, d) and (0, b)x(0, c), whose combined
// area is the parallelogram's. Here each pixel is first reduced into the
// parallelogram cell, then walked down the staircase into the box pair, and
// UnitCell is sampled there, tiling the plane with copies.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec4 col;
vec2 p = coords(fragCoord, iResolution, uScale, viewOffset(iResolution));
mat2 m = mat2(uLattice);
float a = vec4(m).x;
float b = -vec4(m).z;
float c = vec4(m).y;
float d = vec4(m).w;
mat2 im = inverse(m);
vec2 pC = p-m*floor(im*p);
// Walk pC from the parallelogram into the box pair. The shift counts
// divide by a box width (a or b); when that width is ~0 (axis-aligned
// basis, e.g. the identity start lattice) the count must be 0, not x/0.
if(a > b)
{
if(pC.x < 0. && pC.y > d) { pC = pC - vec2(-b,d); }
else if(pC.x > 0.)
{
pC = pC + (b > 1e-6 ? floor(pC.x/b) : 0.)*vec2(-b, d);
if(pC.y > c )
{
pC = pC - floor((pC.y-c)/d)*vec2(-b, d);
pC = pC - vec2(a,c);
}
}
}
else
{
if(pC.x > 0. && pC.y > c) { pC = pC - vec2(a,c); }
else if(pC.x < 0.)
{
pC = pC + (a > 1e-6 ? floor(-pC.x/a) : 0.)*vec2(a, c);
if(pC.y > d )
{
pC = pC - floor((pC.y-d)/c)*vec2(a, c);
pC = pC - vec2(-b,d);
}
}
}
vec2 uv = iCoords(pC, iResolution, uScale, viewOffset(iResolution)) / iResolution.xy;
// Keep the sample off the buffer's top edge.
if(uv.y > .998) { uv.y = .998;}
col = vec4(texture(UnitCell,uv).xyz,1);
fragColor = col;
} image.glsl
// Composite: show either the whole-plane view (uView 1) or the single
// warped unit cell.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec4 col;
vec2 uv = fragCoord.xy / iResolution.xy;
// Keep the sample off the buffer's top edge.
if(uv.y > .998) { uv.y = .998;}
if(uView == 1) { col = vec4(texture(fullLattice,uv).xyz,1); }
else { col = vec4(texture(UnitCell,uv).xyz,1); }
fragColor = col;
} UnitCell.glsl
// Unit cell — warps the previous frame back by one flow step (uFlowInv) to
// animate the stretch, then draws the current fundamental domain on top.
// The domain is the two-box staircase cell (see fullLattice.glsl): the blue
// box spans (-b1.x, 0) to (0, b2.y), the red box (0, 0) to (-b2.x, b1.y).
// Restored from the original Shadertoy, with uFlowInv replacing gFlow(-1.)
// and uLattice replacing the bufA matrix.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 p = coords(fragCoord, iResolution, uScale, viewOffset(iResolution));
// Where this pixel came from one flow step ago.
vec2 oFrag = iCoords(mat2(uFlowInv) * p, iResolution, uScale, viewOffset(iResolution));
vec2 uv = oFrag / iResolution.xy;
vec4 col = backCol;
vec4 pt = uLattice; // (b1.x, b1.y, b2.x, b2.y)
float mixing = 0.;
if (iFrame == 0 || uNewLattice || uv.x > 1. || uv.x < 0. || uv.y > .99 || uv.y < 0.)
{
// Fresh start (or the warp reaches outside the buffer): draw the
// cell solid.
col = drBox(col, p, vec4(-pt.x, 0., 0., pt.w), vec4(edgeCol.xyz, 1.), .0);
col = drBox(col, p, vec4(0., 0., -pt.z, pt.y), vec4(dotCol.xyz, 1.), .0);
}
else
{
col = vec4(texture(UnitCell, uv).xyz, 1.);
vec4 fullCol = vec4(texture(fullLattice, uv).xyz, 1.);
if (fadeMoves) {
col = mix(col, vec4(backCol.xyz, 0), .1);
mixing = .1;
} else {
col = vec4(backCol.xyz,0);
mixing = 1.;
}
if (uFade == 1) {
// Repaint the cell with the warped whole-plane colours at full
// strength, then tint it very lightly with the box colours.
mixing = 1.0;
col = drBox(col, p, vec4(-pt.x, 0., 0., pt.w), vec4(fullCol.xyz, mixing), 0.01);
col = drBox(col, p, vec4(0., 0., -pt.z, pt.y), vec4(fullCol.xyz, mixing), 0.01);
mixing = .01;
col = drBox(col, p, vec4(-pt.x, 0., 0., pt.w), vec4(edgeCol.xyz, mixing), .001);
col = drBox(col, p, vec4(0., 0., -pt.z, pt.y), vec4(dotCol.xyz, mixing), .001);
} else {
col = drBox(col, p, vec4(-pt.x, 0., 0., pt.w), vec4(edgeCol.xyz, mixing), .1);
col = drBox(col, p, vec4(0., 0., -pt.z, pt.y), vec4(dotCol.xyz, mixing), .1);
}
}
fragColor = col;
} lattice/script.js
// 2dFlowJS: the plain lattice-flow shader UI. All the driving logic lives in
// the shared lattice driver; this script is just its instantiation, so state
// is per-instance and other shaders can layer their own UI on the same core.
import { createLatticeDriver } from '../shared/lattice_driver.js';
const driver = createLatticeDriver();
export const setup = driver.setup;
export const onFrame = driver.onFrame;
export const onSetLattice = driver.onSetLattice;