Skip to content

Instantly share code, notes, and snippets.

@7dir
Created January 11, 2024 17:32
Show Gist options
  • Save 7dir/d45c2702a2f235a97e0410e0777940a9 to your computer and use it in GitHub Desktop.
Save 7dir/d45c2702a2f235a97e0410e0777940a9 to your computer and use it in GitHub Desktop.

Revisions

  1. 7dir created this gist Jan 11, 2024.
    4 changes: 4 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    <div class="container">
    <canvas class="illo"></canvas>
    <p>Click &amp; drag to rotate</p>
    </div>
    688 changes: 688 additions & 0 deletions script.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,688 @@
    // Made with Zdog


    var BokehShape = Zdog.Shape.subclass({
    bokehSize: 5,
    bokehLimit: 64,
    });

    BokehShape.prototype.updateBokeh = function() {
    // bokeh 0 -> 1
    this.bokeh = Math.abs( this.sortValue ) / this.bokehLimit;
    this.bokeh = Math.max( 0, Math.min( 1, this.bokeh ) );
    return this.bokeh;
    };

    BokehShape.prototype.getLineWidth = function() {
    return this.stroke + this.bokehSize * this.bokeh * this.bokeh;
    };

    BokehShape.prototype.getBokehAlpha = function() {
    var alpha = 1 - this.bokeh;
    alpha *= alpha;
    return alpha * 0.8 + 0.2;
    };

    BokehShape.prototype.renderCanvasDot = function( ctx ) {
    this.updateBokeh();
    ctx.globalAlpha = this.getBokehAlpha(); // set opacity
    Zdog.Shape.prototype.renderCanvasDot.apply( this, arguments );
    ctx.globalAlpha = 1; // reset
    };

    BokehShape.prototype.renderPath = function( ctx, renderer ) {
    this.updateBokeh();
    // set opacity
    if ( renderer.isCanvas ) {
    ctx.globalAlpha = this.getBokehAlpha();
    }
    Zdog.Shape.prototype.renderPath.apply( this, arguments );
    // reset opacity
    if ( renderer.isCanvas ) {
    ctx.globalAlpha = 1;
    }
    };

    var TAU = Zdog.TAU;

    function makeMadeline( isGood, colors, options ) {

    var rotor = new Zdog.Anchor( options );

    var body = new Zdog.Group({
    addTo: rotor,
    rotate: { x: -TAU/8 },
    translate: { z: -48 },
    updateSort: true,
    });

    var head = new Zdog.Anchor({
    addTo: body,
    translate: { y: -11, z: -2 },
    rotate: { x: TAU/8 },
    });

    // face
    var face = new Zdog.Ellipse({
    diameter: 6,
    addTo: head,
    translate: { z: 4 },
    stroke: 8,
    color: colors.skin,
    });

    var eyeGroup = new Zdog.Group({
    addTo: face,
    translate: { z: face.stroke/2 - 0.5 },
    });


    // eyes
    [ -1, 1 ].forEach( function( xSide ) {
    // cheek blush
    if ( isGood ) {
    new Zdog.Ellipse({
    width: 2,
    height: 1.3,
    addTo: eyeGroup,
    translate: { x: 4.5*xSide, y: 3, z: -1 },
    rotate: { y: -TAU/16*xSide },
    stroke: 1,
    color: '#FA8',
    fill: true,
    });
    }

    var eyeX = 3.5*xSide;

    // eye
    new Zdog.Ellipse({
    width: 0.75,
    height: 1.5,
    addTo: eyeGroup,
    color: colors.eye,
    translate: { x: eyeX },
    stroke: 2,
    fill: true,
    });

    // eye brow
    new Zdog.Ellipse({
    addTo: eyeGroup,
    height: 3,
    width: 1.2,
    quarters: 2,
    translate: { x: eyeX, y: -3 },
    rotate: { z: -TAU/4 + 0.15*xSide * (isGood ? 1 : -1) },
    color: colors.hair,
    stroke: 1,
    fill: false,
    closed: true,
    });

    });


    // hair ball
    new Zdog.Shape({
    path: [
    { x: -1 },
    { x: 1 },
    { z: -4 },
    ],
    addTo: head,
    translate: { y: -4, z: -1 },
    stroke: 18,
    color: colors.hair,
    });

    var bang = new Zdog.Shape({
    path: [
    {},
    { arc: [
    { z: 4, y: 4 },
    { z: 0, y: 8 },
    ]},
    ],
    addTo: head,
    translate: { x: 2, y: -7.5, z: 6 },
    rotate: { x: 0.5, z: -0.5 },
    stroke: 4,
    color: colors.hair,
    closed: false,
    });
    bang.copy({
    translate: { x: 5, y: -6, z: 5 },
    rotate: { x: -0.3, z: -0.5 },
    });
    bang.copy({
    translate: { x: 5, y: -6, z: 3 },
    rotate: { y: -0.7, z: -1 },
    });

    // left side
    bang.copy({
    translate: { x: -2, y: -7.5, z: 6 },
    rotate: { x: 0, z: TAU/16*6 },
    });
    bang.copy({
    translate: { x: -5, y: -6, z: 5 },
    rotate: { x: 0, z: TAU/4 },
    });
    bang.copy({
    translate: { x: -5, y: -6, z: 3 },
    rotate: { y: 0.7, z: 1 },
    });

    // hair cover
    new Zdog.Shape({
    path: [
    { x: -3 },
    { x: 3 },
    ],
    addTo: head,
    stroke: 7,
    translate: { y: -8, z: 5 },
    color: colors.hair,
    });

    // trail locks

    var trailLock = new Zdog.Shape({
    path: [
    { y: -4, z: 0 },
    { bezier: [
    { y: -10, z: -14 },
    { y: 0, z: -16 },
    { y: 0, z: -26 }
    ]},
    ],
    addTo: head,
    translate: { z: -4, y: 0 },
    stroke: 10,
    color: colors.hair,
    closed: false,
    });

    trailLock.copy({
    translate: { x: -3, z: -4 },
    rotate: { z: -TAU/8 },
    stroke: 8,
    });
    trailLock.copy({
    translate: { x: 3, z: -4 },
    rotate: { z: TAU/8 },
    stroke: 8,
    });
    trailLock.copy({
    translate: { y: 2 },
    // rotate: { z: TAU/2 },
    scale: { y: 0.5 },
    stroke: 8,
    });

    // ----- torso ----- //

    // 2nd rib
    var torsoRib = new Zdog.Ellipse({
    width: 12,
    height: 10,
    addTo: body,
    rotate: { x: -TAU/4 },
    translate: { y: -1 },
    stroke: 6,
    color: colors.parkaLight,
    fill: true,
    });
    // neck rib
    torsoRib.copy({
    width: 6,
    height: 6,
    translate: { y: -5 },
    });
    // 3rd rib
    torsoRib.copy({
    translate: { y: 3 },
    });
    // 4th rib
    torsoRib.copy({
    translate: { y: 7 },
    color: colors.parkaDark,
    });
    // waist
    new Zdog.Ellipse({
    width: 10,
    height: 8,
    addTo: body,
    rotate: { x: -TAU/4 },
    translate: { y: 11 },
    stroke: 4,
    color: colors.tight,
    fill: true,
    });

    // arms
    [ -1, 1 ].forEach( function( xSide ) {
    var isLeft = xSide == 1;
    // shoulder ball
    new Zdog.Shape({
    addTo: body,
    stroke: 6,
    translate: { x: 6*xSide, y: -5, z: -1 },
    color: colors.parkaLight,
    });

    var shoulderJoint = new Zdog.Anchor({
    addTo: body,
    translate: { x: 9*xSide, y: -3, z: -2 },
    rotate: isLeft ? { x: TAU/8*3, z: -TAU/32 } : { z: TAU/16*2, x: -TAU/16*2 },
    });

    // top shoulder rib
    var armRib = new Zdog.Ellipse({
    diameter: 2,
    rotate: { x: -TAU/4 },
    addTo: shoulderJoint,
    translate: { x: 0*xSide },
    stroke: 6,
    color: colors.parkaLight,
    fill: true,
    });
    armRib.copy({
    translate: { y: 4 },
    });

    var elbowJoint = new Zdog.Anchor({
    addTo: shoulderJoint,
    translate: { y: 8 },
    rotate: isLeft ? {} : { z: TAU/8 },
    });

    armRib.copy({
    addTo: elbowJoint,
    translate: { x: 0, y: 0 },
    });
    armRib.copy({
    addTo: elbowJoint,
    translate: { y: 4 },
    color: colors.parkaDark,
    });

    // hand
    new Zdog.Shape({
    addTo: elbowJoint,
    translate: { y: 9, z: -1 },
    stroke: 8,
    color: colors.skin,
    });

    // ----- legs ----- //
    var knee = { y: 7 };
    var thigh = new Zdog.Shape({
    path: [ { y: 0 }, knee ],
    addTo: body,
    translate: { x: 4*xSide, y: 13 },
    rotate: isLeft ? {} : { x: TAU/16*3, z: TAU/16 },
    stroke: 8,
    color: colors.tight,
    });

    var shin = new Zdog.Shape({
    path: [ { y: 0 }, { y: 8 } ],
    addTo: thigh,
    stroke: 6,
    translate: knee,
    rotate: isLeft ? {} : { x: -TAU/16*5 },
    color: colors.tight,
    });

    });

    // butt
    new Zdog.Shape({
    path: [
    { x: -3 },
    { x: 3 },
    ],
    visible: false,
    addTo: body,
    translate: { y: 11, z: -2 },
    stroke: 8,
    color: colors.tight,
    });

    }

    window.makeBird = function( options ) {

    var spin = options.spin || 0;

    var arrow = new Zdog.Anchor({
    addTo: options.addTo,
    scale: 2/3,
    rotate: { z: spin },
    });

    var bird = new Zdog.Group({
    addTo: arrow,
    translate: { x: 87 },
    rotate: { x: -spin },
    });

    // bird body
    new Zdog.Shape({
    path: [
    { x: -3, y: 0 },
    { arc: [
    { x: -2, y: 1.5 },
    { x: 0, y: 1.5 },
    ]},
    { arc: [
    { x: 2, y: 1.5 },
    { x: 2, y: 0 },
    ]},
    ],
    addTo: bird,
    translate: { x: 0.5 },
    stroke: 3,
    color: options.color,
    fill: true,
    });

    // bird head
    new Zdog.Shape({
    translate: { x: 4, y: -1 },
    addTo: bird,
    stroke: 4,
    color: options.color,
    });

    // beak
    new Zdog.Shape({
    path: [
    { x: 0, y: -1 },
    { x: 3, y: 0 },
    { x: 0, y: 1 },
    ],
    addTo: bird,
    translate: { x: 5, y: -1 },
    stroke: 1,
    color: options.color,
    fill: true,
    });

    // tail feather
    new Zdog.Shape({
    path: [
    { x: -3, z: -2 },
    { x: 0, z: 0 },
    { x: -3, z: 2 },
    ],
    addTo: bird,
    translate: { x: -4, y: 0 },
    stroke: 2,
    color: options.color,
    fill: true,
    });

    var wing = new Zdog.Shape({
    path: [
    { x: 3, y: 0 },
    { x: -1, y: -9 },
    { arc: [
    { x: -5, y: -4 },
    { x: -3, y: 0 },
    ]},
    ],
    addTo: bird,
    translate: { z: -1.5},
    rotate: { x: TAU/8 },
    stroke: 1,
    color: options.color,
    fill: true,
    });

    wing.copy({
    translate: { z: 1.5},
    scale: { z: -1 },
    rotate: { x: -TAU/8 },
    });

    };

    // -------------------------- demo -------------------------- //

    var illoElem = document.querySelector('.illo');
    var w = 160;
    var h = 160;
    var minWindowSize = Math.min( window.innerWidth, window.innerHeight );
    var zoom = Math.min( 5, Math.floor( minWindowSize / w ) );
    illoElem.setAttribute( 'width', w * zoom );
    illoElem.setAttribute( 'height', h * zoom );

    var isSpinning = true;
    var TAU = Zdog.TAU;

    var illo = new Zdog.Illustration({
    element: illoElem,
    zoom: zoom,
    rotate: { y: -TAU/4 },
    dragRotate: true,
    onDragStart: function() {
    isSpinning = false;
    },
    });

    var madColor = {
    skin: '#FD9',
    hair: '#D53',
    parkaLight: '#67F',
    parkaDark: '#35D',
    tight: '#742',
    eye: '#333',
    };
    var badColor = {
    skin: '#EBC',
    hair: '#D4B',
    parkaLight: '#85A',
    parkaDark: '#527',
    tight: '#412',
    eye: '#D02',
    };

    var glow = 'hsla(60, 100%, 80%, 0.3)';
    var featherGold = '#FE5';

    // -- illustration shapes --- //

    makeMadeline( true, madColor, {
    addTo: illo,
    });
    makeMadeline( false, badColor, {
    addTo: illo,
    rotate: { y: TAU/2 },
    });


    // ----- feather ----- //

    var feather = new Zdog.Group({
    addTo: illo,
    rotate: { y: -TAU/4 },
    });

    ( function() {

    var featherPartCount = 8;
    var radius = 12;
    var angleX = (TAU/featherPartCount) / 2;
    var sector = (TAU * radius)/2 / featherPartCount;

    for ( var i=0; i < featherPartCount; i++ ) {
    var curve = Math.cos( (i/featherPartCount) * TAU*3/4 + TAU*1/4 );
    var x = 4 - curve*2;
    var y0 = sector/2;
    // var y2 = -sector/2;
    var isLast = i == featherPartCount - 1;
    var y3 = isLast ? sector * -1 : -y0;
    var z1 = -radius + 2 + curve*-1.5;
    var z2 = isLast ? -radius : -radius;
    var barb = new Zdog.Shape({
    path: [
    { x: 0, y: y0, z: -radius },
    { x: x, y: -sector/2, z: z1 },
    { x: x, y: -sector*3/4, z: z1 },
    { x: 0, y: y3, z: z2 },
    ],
    addTo: feather,
    rotate: { x: angleX * -i + TAU/8 },
    stroke: 1,
    color: featherGold,
    fill: true,
    });
    barb.copy({
    scale: { x: -1 },
    });
    }

    // rachis
    var rachis = new Zdog.Ellipse({
    addTo: feather,
    diameter: radius*2,
    quarters: 2,
    rotate: { y: -TAU/4 },
    stroke: 2,
    color: featherGold,
    });
    rachis.copy({
    stroke: 8,
    color: glow,
    rotate: { y: -TAU/4, x: -0.5 }
    });
    })();

    // ----- rods ----- //

    ( function() {

    var rodCount = 14;
    for ( var i=0; i < rodCount; i++ ) {
    var zRotor = new Zdog.Anchor({
    addTo: illo,
    rotate: { z: TAU/rodCount * i },
    });

    var y0 = 32;
    var y1 = y0 + 2 + Math.random()*24;
    new BokehShape({
    path: [
    { y: y0 },
    { y: y1 },
    ],
    addTo: zRotor,
    rotate: { x: ( Math.random() * 2 - 1 ) * TAU/8 },
    color: madColor.skin,
    stroke: 1,
    bokehSize: 6,
    bokehLimit: 70,
    });
    }

    })();

    // dots

    ( function() {
    var dotCount = 64;

    for ( var i=0; i < dotCount; i++ ) {
    var yRotor = new Zdog.Anchor({
    addTo: illo,
    rotate: { y: TAU/dotCount * i },
    });

    new BokehShape({
    path: [
    { z: 40*(1 - Math.random()*Math.random()) + 32 },
    ],
    addTo: yRotor,
    rotate: { x: ( Math.random() * 2 - 1 ) * TAU*3/16 },
    color: badColor.skin,
    stroke: 1 + Math.random(),
    bokehSize: 6,
    bokehLimit: 74,
    });
    }

    })();

    // ----- birds ----- //

    var birdRotor = new Zdog.Anchor({
    addTo: illo,
    rotate: { y: TAU*-1/8 },
    });

    makeBird({
    addTo: birdRotor,
    color: madColor.parkaLight,
    spin: TAU/2,
    });

    makeBird({
    addTo: birdRotor,
    color: featherGold,
    spin: -TAU * 3/8,
    });

    makeBird({
    addTo: birdRotor,
    color: 'white',
    spin: -TAU/4,
    });

    makeBird({
    addTo: birdRotor,
    color: madColor.hair,
    spin: -TAU/8,
    });

    makeBird({
    addTo: birdRotor,
    color: madColor.parkaDark,
    spin: TAU/8,
    });

    // -- animate --- //

    var isSpinning = true;
    var rotateSpeed = -TAU/60;
    var xClock = 0;
    var then = new Date() - 1/60;

    function animate() {
    update();
    illo.renderGraph();
    requestAnimationFrame( animate );
    }

    animate();

    // -- update -- //

    function update() {
    var now = new Date();
    var delta = now - then;
    // auto rotate
    if ( isSpinning ) {
    var theta = rotateSpeed/60 * delta * -1;
    illo.rotate.y += theta;
    xClock += theta/4;
    illo.rotate.x = Math.sin( xClock ) * TAU/12;
    }

    illo.updateGraph();

    then = now;
    }

    1 change: 1 addition & 0 deletions scripts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    <script src="https://unpkg.com/zdog@1/dist/zdog.dist.js"></script>
    19 changes: 19 additions & 0 deletions style.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    html { height: 100%; }

    body {
    min-height: 100%;
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #435;
    color: white;
    font-family: sans-serif;
    text-align: center;
    }

    canvas {
    display: block;
    margin: 0 auto;
    cursor: move;
    }
    21 changes: 21 additions & 0 deletions zdog-celeste-snowglobe.markdown
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    Zdog - Celeste snowglobe
    ------------------------
    [Made with Zdog](https://zzz.dog)

    Been playing a lot of Celeste. In the eye of the storm with Madeline.

    Special sauce includes bokeh shapes for the snowflakes and light rods, which grow & fade out as the move further from the scene center.

    [View more round 3D Pens](https://codepen.io/desandro/pens/tags/?selected_tag=round3d)

    ---

    Built with my own vanilla JS (still rocking that ES5) using the &lt;canvas&gt; drawing API. No WebGL. Shapes are rendered with thick lineWidth, giving the illusion of 3D form. A sphere has the same 2D contour as a flat circle. A 3D pill has the same contour of a 2D pill. That's the trick. This is the best engine to render hot dogs and burger patties.

    [3D code comes from this Khan Academy lesson](https://www.khanacademy.org/computing/computer-programming/programming-games-visualizations/programming-3d-shapes/a/rotating-3d-shapes). It's great because it only uses simple trig, and no matrix mindbenders.

    Yeah it's buggy as heck, but that's the _charm_. It has no concept of intersecting or clipping. Shapes are either rendered before or after one another. Two shapes that occupy the same space will pop over one another when rotated.

    A [Pen](https://codepen.io/desandro/pen/RQeYYp) by [Dave DeSandro](https://codepen.io/desandro) on [CodePen](https://codepen.io).

    [License](https://codepen.io/license/pen/RQeYYp).