5 JS Canvas Effects Recreated in Houdini VEX

• 5 min read

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.1 for a trefoil or r1=0.75, r2=0.5 for 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 birds to 150 and drop separation to 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.