Skip to content

Instantly share code, notes, and snippets.

@jasalt
Created July 11, 2026 08:21
Show Gist options
  • Select an option

  • Save jasalt/cbd0847fa55d006173e010a3651044fb to your computer and use it in GitHub Desktop.

Select an option

Save jasalt/cbd0847fa55d006173e010a3651044fb to your computer and use it in GitHub Desktop.
JSCAD cow test
// https://jscad.app/#data:application/gzip;base64,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
const jscad = require('@jscad/modeling')
const { sphere, cylinder } = jscad.primitives
const { translate, rotateX, rotateY, rotateZ, scale, mirror } = jscad.transforms
const { colorize } = jscad.colors
const { hull } = jscad.hulls
// ---------- Colors ----------
const WHITE = [0.96, 0.96, 0.93]
const BLACK = [0.06, 0.06, 0.07]
const PINK = [0.9, 0.62, 0.64]
const HOOF = [0.22, 0.17, 0.13]
const HORN = [0.9, 0.82, 0.62]
// ---------- Helpers ----------
const deg = (d) => d * Math.PI / 180
const flatten = (arr) =>
arr.reduce((acc, v) => Array.isArray(v) ? acc.concat(flatten(v)) : acc.concat(v), [])
function ellipsoid(rx, ry, rz, segments = 32) {
return scale([rx, ry, rz], sphere({ radius: 1, segments }))
}
function sideMirror(side, geom) {
return side < 0 ? mirror({ normal: [1, 0, 0] }, geom) : geom
}
// ---------- Animated leg ----------
// Local origin is the hip. The leg extends downward in -Z.
// phase is 0..2PI.
function animatedLeg({ front = true, phase = 0 }) {
const air = Math.max(0, Math.sin(phase)) // 0 on ground, 1 at top of jump
const ground = 1 - air
const bounce = Math.sin(phase * 2)
const upperLen = 0.48
const lowerLen = 0.42
// Front legs tuck backward in the air, rear legs tuck forward.
const tuckDir = front ? -1 : 1
// Hip swing and knee folding.
const hipAngle =
deg((front ? 6 : -6) + tuckDir * 34 * air + 8 * bounce * ground)
const kneeAngle =
deg(tuckDir * -1 * (12 + 55 * air + 10 * Math.max(0, -Math.cos(phase))))
const upper = colorize(
WHITE,
translate(
[0, 0, -upperLen / 2],
cylinder({ radius: 0.075, height: upperLen, segments: 24 })
)
)
const lower = colorize(
WHITE,
translate(
[0, 0, -lowerLen / 2],
cylinder({ radius: 0.06, height: lowerLen, segments: 24 })
)
)
const hoof = colorize(
HOOF,
translate(
[0, 0, -lowerLen - 0.04],
scale([0.13, 0.18, 0.07], sphere({ radius: 1, segments: 24 }))
)
)
const lowerAssembly = translate(
[0, 0, -upperLen],
rotateX(kneeAngle, [lower, hoof])
)
return rotateX(hipAngle, [upper, lowerAssembly])
}
// ---------- Cow parts ----------
function body() {
return [
colorize(
WHITE,
hull(
ellipsoid(0.46, 0.92, 0.36),
translate([0, -0.55, 0.05], ellipsoid(0.36, 0.48, 0.32)),
translate([0, 0.58, 0.02], ellipsoid(0.38, 0.52, 0.32))
)
),
colorize(
WHITE,
translate([0, 0.55, 0.22], ellipsoid(0.34, 0.27, 0.18))
),
colorize(
WHITE,
translate([0, -0.72, 0.1], ellipsoid(0.36, 0.32, 0.28))
)
]
}
function patches() {
return [
colorize(BLACK, translate([0.23, 0.25, 0.32], ellipsoid(0.18, 0.32, 0.14))),
colorize(BLACK, translate([-0.26, -0.18, 0.32], ellipsoid(0.15, 0.38, 0.14))),
colorize(BLACK, translate([0.15, -0.62, 0.28], ellipsoid(0.2, 0.26, 0.13))),
colorize(BLACK, translate([-0.3, 0.6, 0.22], ellipsoid(0.12, 0.18, 0.12))),
colorize(BLACK, translate([0.2, 0.05, -0.33], ellipsoid(0.18, 0.26, 0.08)))
]
}
function udder() {
const parts = []
parts.push(
colorize(
PINK,
translate(
[0, -0.22, -0.34],
ellipsoid(0.22, 0.3, 0.16)
)
)
)
for (const x of [-0.1, 0.1]) {
for (const y of [-0.08, 0.1]) {
parts.push(
colorize(
PINK,
translate(
[x, y, -0.48],
cylinder({ radius: 0.028, height: 0.13, segments: 16 })
)
)
)
}
}
return parts
}
function ear(side, phase) {
const flap = deg(6 * Math.sin(phase + side))
const outer = colorize(
WHITE,
hull(
sphere({ radius: 0.045, segments: 18 }),
translate([0.06, -0.15, 0.02], ellipsoid(0.08, 0.15, 0.035, 18))
)
)
const inner = colorize(
PINK,
translate([0.055, -0.14, 0.02], ellipsoid(0.055, 0.11, 0.02, 18))
)
return rotateZ(flap, sideMirror(side, [outer, inner]))
}
function horn(side) {
const h = colorize(
HORN,
rotateY(deg(-25),
hull(
translate([0, 0, 0], sphere({ radius: 0.04, segments: 16 })),
translate([0.025, 0, 0.13], sphere({ radius: 0.025, segments: 16 })),
translate([0.055, 0, 0.22], sphere({ radius: 0.012, segments: 16 }))
)
)
)
return sideMirror(side, h)
}
function head(phase) {
const parts = []
parts.push(
colorize(
WHITE,
hull(
ellipsoid(0.23, 0.26, 0.22),
translate([0, 0.3, 0.03], ellipsoid(0.18, 0.22, 0.17))
)
)
)
parts.push(
colorize(
WHITE,
translate(
[0, 0.5, -0.02],
hull(
ellipsoid(0.18, 0.15, 0.13),
translate([0, 0.12, -0.02], ellipsoid(0.15, 0.09, 0.11))
)
)
)
)
parts.push(
colorize(
PINK,
translate([0, 0.63, -0.02], ellipsoid(0.14, 0.065, 0.09))
)
)
parts.push(colorize(BLACK, translate([-0.05, 0.68, 0.0], sphere({ radius: 0.015, segments: 12 }))))
parts.push(colorize(BLACK, translate([0.05, 0.68, 0.0], sphere({ radius: 0.015, segments: 12 }))))
parts.push(colorize([1, 1, 0.95], translate([-0.15, 0.18, 0.16], sphere({ radius: 0.04, segments: 16 }))))
parts.push(colorize([1, 1, 0.95], translate([0.15, 0.18, 0.16], sphere({ radius: 0.04, segments: 16 }))))
parts.push(colorize(BLACK, translate([-0.15, 0.205, 0.17], sphere({ radius: 0.02, segments: 16 }))))
parts.push(colorize(BLACK, translate([0.15, 0.205, 0.17], sphere({ radius: 0.02, segments: 16 }))))
parts.push(translate([-0.22, 0.05, 0.12], rotateZ(deg(-15), ear(-1, phase))))
parts.push(translate([0.22, 0.05, 0.12], rotateZ(deg(15), ear(1, phase))))
parts.push(translate([-0.12, -0.02, 0.2], horn(-1)))
parts.push(translate([0.12, -0.02, 0.2], horn(1)))
return parts
}
function neck() {
return colorize(
WHITE,
hull(
translate([0, 0.86, 0.12], ellipsoid(0.28, 0.22, 0.24)),
translate([0, 1.12, 0.25], ellipsoid(0.22, 0.18, 0.2))
)
)
}
function tail(phase) {
const wag = deg(15 * Math.sin(phase * 1.5))
const tailBones = []
for (let i = 0; i <= 7; i++) {
const t = i / 7
tailBones.push(
colorize(
WHITE,
translate(
[0, -0.12 * i, -0.025 * i],
sphere({ radius: 0.055 * (1 - t * 0.35), segments: 16 })
)
)
)
}
const tuft = colorize(
BLACK,
translate([0, -0.88, -0.2], ellipsoid(0.07, 0.09, 0.07, 16))
)
return rotateX(deg(-12), rotateZ(wag, [tailBones, tuft]))
}
// ---------- Whole animated cow ----------
function cow(phase) {
const air = Math.max(0, Math.sin(phase))
// Body height during jump.
const jumpHeight = 0.58 * Math.pow(air, 0.9)
// Slight compression/crouch while preparing and landing.
const crouch = 0.08 * Math.max(0, -Math.sin(phase))
// Body tilts slightly through the jump.
const bodyTilt = deg(5 * Math.sin(phase - 0.4))
const cowParts = [
body(),
patches(),
udder(),
neck(),
translate(
[0, 1.24, 0.34],
rotateX(deg(-25 + 4 * air), head(phase))
),
// Front legs
translate([-0.24, 0.55, -0.34], animatedLeg({ front: true, phase })),
translate([0.24, 0.55, -0.34], animatedLeg({ front: true, phase: phase + 0.08 })),
// Rear legs
translate([-0.25, -0.62, -0.34], animatedLeg({ front: false, phase: phase + 0.1 })),
translate([0.25, -0.62, -0.34], animatedLeg({ front: false, phase })),
translate([0, -0.95, 0.12], tail(phase))
]
return translate(
[0, 0, jumpHeight - crouch],
rotateX(bodyTilt, flatten(cowParts))
)
}
function groundShadow(phase) {
const air = Math.max(0, Math.sin(phase))
return colorize(
[0.1, 0.1, 0.1, 0.35],
translate(
[0, 0, -1.26],
scale(
[0.9 + air * 0.35, 1.45 + air * 0.45, 0.02],
cylinder({ radius: 0.6, height: 1, segments: 48 })
)
)
)
}
// ---------- Animation parameter ----------
function getParameterDefinitions() {
return [
{
name: 'frame',
type: 'slider',
caption: 'Jump frame',
initial: 0,
min: 0,
max: 100,
step: 1
}
]
}
function main(params) {
const frame = Number(params.frame || 0)
// One full jump loop from frame 0 to 100.
const phase = 2 * Math.PI * (frame / 100)
return [
groundShadow(phase),
cow(phase)
]
}
module.exports = {
main,
getParameterDefinitions
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment