5 JS Canvas Effects Recreated in Houdini VEX
Saw a TikTok account (@meowish.dev) building beautiful canvas animations in JavaScript. Decided to port all five into Houdini VEX — no SOPs, no simulations, just a single Attribute Wrangle set to Detail class per effect.
Every snippet uses chf() / chi() with inline defaults so you can paste directly and it runs. Add spare parameters later for slider control.
1 — Chromatic Spirograph
Epitrochoid curve with HSV color cycling. The ratio of the two radii controls the petal count.
Node: Attribute Wrangle → Class: Detail → connect Polywire (radius ~0.005)
int npts = chi("points", 2000);
float r1 = chf("r1", 0.54); // outer wheel ratio
float r2 = chf("r2", 0.32); // pen offset ratio
float revs = chf("revolutions", 10.0);
int prim = addprim(0, "polyline");
for (int i = 0; i < npts; i++) {
float t = float(i) / float(npts) * 6.28318530 * revs;
float cx = (1.0 - r1) * cos(t) + r2 * cos((1.0 - r1) / r1 * t);
float cy = (1.0 - r1) * sin(t) - r2 * sin((1.0 - r1) / r1 * t);
int pt = addpoint(0, set(cx, 0, cy));
float hue = (float(i) / float(npts) + @Time * 0.1) % 1.0;
setpointattrib(0, "Cd", pt, hsvtorgb(set(hue, 1.0, 1.0)));
addvertex(0, prim, pt);
}
Try
r1=0.33, r2=0.1for a trefoil orr1=0.75, r2=0.5for a loose star.
2 — Hex Pulse Grid
24×24 hexagonal grid with an animated ripple glow radiating from the centre.
Node: Attribute Wrangle → Class: Detail
int cols = chi("cols", 24);
int rows = chi("rows", 24);
float hexSize = chf("hex_size", 0.22);
float speed = chf("speed", 2.0);
float falloff = chf("falloff", 1.8);
float w = hexSize * 2.0;
float h = hexSize * sqrt(3.0);
float ox = -(cols - 1) * 0.75 * w * 0.5;
float oz = -(rows - 1) * h * 0.5;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
float px = ox + col * w * 0.75;
float pz = oz + row * h + (col % 2) * h * 0.5;
float dist = sqrt(px*px + pz*pz);
float wave = sin(dist * falloff - @Time * speed);
float brightness = clamp(wave * 0.5 + 0.5, 0.0, 1.0);
float hue = frac(dist * 0.08 - @Time * 0.05);
int prim = addprim(0, "poly");
for (int v = 0; v < 6; v++) {
float a = float(v) / 6.0 * 6.28318530;
int pt = addpoint(0, set(px + cos(a)*hexSize, 0, pz + sin(a)*hexSize));
setpointattrib(0, "Cd", pt, hsvtorgb(set(hue, 0.9, brightness)));
addvertex(0, prim, pt);
}
}
}
3 — Quantum Cage
Noise-drifted particles flowing along the 12 edges of a cube with slow Y rotation.
Node: Attribute Wrangle → Class: Detail → display flag on wrangle
float sz = chf("size", 1.0);
int perEdge = chi("per_edge", 80);
float drift = chf("drift", 0.08);
float pSize = chf("point_size", 0.015);
float rotSpd = chf("rotation_speed", 0.2);
int edges[][2] = {{0,1},{1,2},{2,3},{3,0},
{4,5},{5,6},{6,7},{7,4},
{0,4},{1,5},{2,6},{3,7}};
vector corners[];
resize(corners, 8);
corners[0] = set(-1,-1,-1)*sz; corners[1] = set( 1,-1,-1)*sz;
corners[2] = set( 1,-1, 1)*sz; corners[3] = set(-1,-1, 1)*sz;
corners[4] = set(-1, 1,-1)*sz; corners[5] = set( 1, 1,-1)*sz;
corners[6] = set( 1, 1, 1)*sz; corners[7] = set(-1, 1, 1)*sz;
matrix3 rot = ident();
rotate(rot, @Time * rotSpd, {0,1,0});
for (int e = 0; e < 12; e++) {
vector A = corners[edges[e][0]];
vector B = corners[edges[e][1]];
for (int i = 0; i < perEdge; i++) {
float t = float(i) / float(perEdge - 1);
float seed = float(e * perEdge + i);
vector p = lerp(A, B, t);
p += set((noise(set(seed*.1, @Time*.4, 0.)) - .5) * drift,
(noise(set(seed*.1, @Time*.4, 1.)) - .5) * drift,
(noise(set(seed*.1, @Time*.4, 2.)) - .5) * drift);
p *= rot;
int pt = addpoint(0, p);
float bright = 0.6 + t * 0.4;
setpointattrib(0, "Cd", pt, set(bright*0.6, bright*0.85, bright));
setpointattrib(0, "pscale", pt, pSize);
}
}
4 — Silk Web
Procedural spider web: 16 spokes, 10 rings, gravitational sag and 30 animated dew drops.
Node: Attribute Wrangle → Class: Detail → connect Polywire (radius ~0.004)
int spokes = chi("spokes", 16);
int rings = chi("rings", 10);
float maxR = chf("radius", 2.5);
float sag = chf("sag", 0.06);
float wobble = chf("wobble", 0.015);
int drops = chi("drops", 30);
for (int s = 0; s < spokes; s++) {
float angle = float(s) / float(spokes) * 6.28318530;
int prim = addprim(0, "polyline");
for (int r = 0; r <= rings; r++) {
float t = float(r) / float(rings);
float rad = t * maxR;
vector p = set(cos(angle)*rad, 0, sin(angle)*rad);
p.y += sin(@Time*1.5 + rad*2.0) * wobble * rad;
int pt = addpoint(0, p);
setpointattrib(0, "Cd", pt, set(1.0-t*.4, 1.0-t*.4, 1.0-t*.4));
addvertex(0, prim, pt);
}
}
for (int r = 1; r <= rings; r++) {
float t = float(r) / float(rings);
float rad = t * maxR;
int prim = addprim(0, "polyline");
for (int s = 0; s <= spokes; s++) {
float angle = float(s % spokes) / float(spokes) * 6.28318530;
vector p = set(cos(angle)*rad, 0, sin(angle)*rad);
p.y -= sag * t * rad;
p.y += sin(@Time*1.5 + rad*2.0 + angle) * wobble * rad;
int pt = addpoint(0, p);
setpointattrib(0, "Cd", pt, set(0.9-t*.3, 0.9-t*.3, 1.1-t*.2));
addvertex(0, prim, pt);
}
}
for (int d = 0; d < drops; d++) {
float t = fit01(rand(float(d)*3.7), 1.0/float(rings), 1.0);
float rad = t * maxR;
float a = rand(float(d)*7.3) * 6.28318530;
vector p = set(cos(a)*rad, -sag*t*rad, sin(a)*rad);
int pt = addpoint(0, p);
setpointattrib(0, "Cd", pt, set(0.7, 0.9, 1.0));
setpointattrib(0, "pscale", pt, 0.03 + rand(float(d))*0.03);
}
5 — Murmuration
60 point-birds orbiting the origin. Boid separation keeps them spaced, per-bird yaw and pitch give each one a unique heading that drifts slowly over time.
Node: Attribute Wrangle → Class: Detail → display flag on wrangle
int birds = chi("birds", 60);
float orbitR = chf("orbit_radius", 2.5);
float spread = chf("spread", 2.0);
float speed = chf("speed", 0.25);
float flapHz = chf("flap_freq", 5.5);
float sepDist = chf("separation", 0.4);
float pSize = chf("point_size", 0.05);
float oa = @Time * speed;
float cx = cos(oa) * orbitR;
float cz = sin(oa) * orbitR;
float cy = sin(@Time * speed * 0.4) * 0.5;
float tx = -sin(oa);
float tz = cos(oa);
vector pos[]; vector vel[];
resize(pos, birds); resize(vel, birds);
for (int i = 0; i < birds; i++) {
float fi = float(i) / float(birds);
float s = float(i) * 17.3;
float bx = (noise(set(s*.1, @Time*.25, 0.)) - .5) * spread * 2.;
float by = (noise(set(s*.1+5.7, @Time*.25, 1.)) - .5) * spread;
float bz = (noise(set(s*.1+9.1, @Time*.25, 2.)) - .5) * spread * 2.;
float flap = sin(@Time*flapHz + fi*6.28318530*4.37) * 0.10 * (0.4 + fi*0.6);
pos[i] = set(cx+bx, cy+by+flap, cz+bz);
float yaw = (rand(s) - 0.5) * 0.87 + (noise(set(s*.3, @Time*.6, 1.)) - .5) * 0.42;
float pitch = (rand(s+1.) - 0.5) * 0.28 + (noise(set(s*.3, @Time*.5, 2.)) - .5) * 0.17;
float fwdX = tx*cos(yaw) + tz*sin(yaw);
float fwdZ = -tx*sin(yaw) + tz*cos(yaw);
vel[i] = normalize(set(fwdX*cos(pitch), sin(pitch), fwdZ*cos(pitch)));
}
for (int i = 0; i < birds; i++) {
vector sep = {0, 0, 0};
for (int j = 0; j < birds; j++) {
if (i == j) continue;
vector dv = pos[i] - pos[j];
float l = length(dv);
if (l < sepDist && l > 0.001)
sep += normalize(dv) * (sepDist - l) / sepDist;
}
pos[i] += sep * 0.12;
}
for (int i = 0; i < birds; i++) {
float fi = float(i) / float(birds);
int pt = addpoint(0, pos[i]);
float bright = 1.0 - fi * 0.55;
setpointattrib(0, "Cd", pt, set(bright, bright*0.82 + fi*.1, bright*0.45));
setpointattrib(0, "N", pt, vel[i]);
setpointattrib(0, "pscale", pt, pSize * (0.6 + bright*0.7));
}
Raise
birdsto 150 and dropseparationto 0.15 for a dense cloud. Copy to Points with a small arrow geo to see each bird’s unique heading.
All five run in Houdini 19.5+ with no external dependencies. Add a Polywire after effects 1 and 4 for tube rendering, or use Karma / Mantra with an emissive material on Cd for the full glow look.