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.
Zdog - Celeste snowglobe
<div class="container">
<canvas class="illo"></canvas>
<p>Click &amp; drag to rotate</p>
</div>
// 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;
}
<script src="https://unpkg.com/zdog@1/dist/zdog.dist.js"></script>
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;
}

Zdog - Celeste snowglobe

Made with Zdog

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


Built with my own vanilla JS (still rocking that ES5) using the <canvas> 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. 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 by Dave DeSandro on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment