Created
July 11, 2026 08:21
-
-
Save jasalt/cbd0847fa55d006173e010a3651044fb to your computer and use it in GitHub Desktop.
JSCAD cow test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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