Skip to content

Instantly share code, notes, and snippets.

@adimancv
Created March 5, 2018 08:41
Show Gist options
  • Save adimancv/af749ab5726f0bacebea84cb74201709 to your computer and use it in GitHub Desktop.
Save adimancv/af749ab5726f0bacebea84cb74201709 to your computer and use it in GitHub Desktop.
gambar-3d-dengan-js
// -------------------------- utils -------------------------- //
var TAU = Math.PI * 2;
function extend( a, b ) {
for ( var prop in b ) {
a[ prop ] = b[ prop ];
}
return a;
}
function lerp( a, b, t ) {
return ( b - a ) * t + a;
}
function modulo( num, div ) {
return ( ( num % div ) + div ) % div;
}
// -------------------------- Vector3 -------------------------- //
function Vector3( position ) {
this.set( position );
}
Vector3.prototype.set = function( pos ) {
pos = Vector3.sanitize( pos );
this.x = pos.x;
this.y = pos.y;
this.z = pos.z;
return this;
};
Vector3.prototype.rotate = function( rotation ) {
if ( !rotation ) {
return;
}
this.rotateZ( rotation.z );
this.rotateY( rotation.y );
this.rotateX( rotation.x );
return this;
};
Vector3.prototype.rotateZ = function( angle ) {
rotateProperty( this, angle, 'x', 'y' );
};
Vector3.prototype.rotateX = function( angle ) {
rotateProperty( this, angle, 'y', 'z' );
};
Vector3.prototype.rotateY = function( angle ) {
rotateProperty( this, angle, 'x', 'z' );
};
function rotateProperty( vec, angle, propA, propB ) {
if ( angle % TAU === 0 ) {
return;
}
var cos = Math.cos( angle );
var sin = Math.sin( angle );
var a = vec[ propA ];
var b = vec[ propB ];
vec[ propA ] = a*cos - b*sin;
vec[ propB ] = b*cos + a*sin;
}
Vector3.prototype.add = function( vec ) {
if ( !vec ) {
return;
}
vec = Vector3.sanitize( vec );
this.x += vec.x;
this.y += vec.y;
this.z += vec.z;
return this;
};
Vector3.prototype.multiply = function( vec ) {
if ( !vec ) {
return;
}
vec = Vector3.sanitize( vec );
this.x *= vec.x;
this.y *= vec.y;
this.z *= vec.z;
return this;
};
Vector3.prototype.lerp = function( vec, t ) {
this.x = lerp( this.x, vec.x, t );
this.y = lerp( this.y, vec.y, t );
this.z = lerp( this.z, vec.z, t );
return this;
};
// ----- utils ----- //
// add missing properties
Vector3.sanitize = function( vec ) {
vec = vec || {};
vec.x = vec.x || 0;
vec.y = vec.y || 0;
vec.z = vec.z || 0;
return vec;
};
// -------------------------- PathAction -------------------------- //
function PathAction( method, points, previousPoint ) {
this.method = method;
this.points = points.map( mapVectorPoint );
this.renderPoints = points.map( mapVectorPoint );
this.previousPoint = previousPoint;
this.endRenderPoint = this.renderPoints[ this.renderPoints.length - 1 ];
// arc actions come with previous point & corner point
// but require bezier control points
if ( method == 'arc' ) {
this.controlPoints = [ new Vector3(), new Vector3() ];
}
}
function mapVectorPoint( point ) {
return new Vector3( point );
}
PathAction.prototype.reset = function() {
// reset renderPoints back to orignal points position
var points = this.points;
this.renderPoints.forEach( function( renderPoint, i ) {
var point = points[i];
renderPoint.set( point );
});
};
PathAction.prototype.transform = function( translation, rotation, scale ) {
this.renderPoints.forEach( function( renderPoint ) {
renderPoint.multiply( scale );
renderPoint.rotate( rotation );
renderPoint.add( translation );
});
};
PathAction.prototype.render = function( ctx ) {
this[ this.method ]( ctx );
};
PathAction.prototype.move = function( ctx ) {
var point = this.renderPoints[0];
ctx.moveTo( point.x, point.y );
};
PathAction.prototype.line = function( ctx ) {
var point = this.renderPoints[0];
ctx.lineTo( point.x, point.y );
};
PathAction.prototype.bezier = function( ctx ) {
var cp0 = this.renderPoints[0];
var cp1 = this.renderPoints[1];
var end = this.renderPoints[2];
ctx.bezierCurveTo( cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y );
};
PathAction.prototype.arc = function( ctx ) {
var prev = this.previousPoint;
var corner = this.renderPoints[0];
var end = this.renderPoints[1];
var cp0 = this.controlPoints[0];
var cp1 = this.controlPoints[1];
cp0.set( prev ).lerp( corner, 9/16 );
cp1.set( end ).lerp( corner, 9/16 );
ctx.bezierCurveTo( cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y );
};
// -------------------------- Shape -------------------------- //
function Shape( options ) {
this.create( options );
}
Shape.prototype.create = function( options ) {
// default
extend( this, Shape.defaults );
// set options
setOptions( this, options );
this.updatePathActions();
// transform
this.translate = new Vector3( options.translate );
this.rotate = new Vector3( options.rotate );
var scale = extend( { x: 1, y: 1, z: 1 }, options.scale );
this.scale = new Vector3( scale );
// children
this.children = [];
if ( this.addTo ) {
this.addTo.addChild( this );
}
};
Shape.defaults = {
stroke: true,
fill: false,
color: 'black',
lineWidth: 1,
closed: true,
rendering: true,
path: [ {} ],
};
var optionKeys = Object.keys( Shape.defaults ).concat([
'rotate',
'translate',
'scale',
'addTo',
'width',
'height',
]);
function setOptions( shape, options ) {
for ( var key in options ) {
if ( optionKeys.includes( key ) ) {
shape[ key ] = options[ key ];
}
}
}
var actionNames = [
'move',
'line',
'bezier',
'arc',
];
// parse path into PathActions
Shape.prototype.updatePathActions = function() {
var previousPoint;
this.pathActions = this.path.map( function( pathPart, i ) {
// pathPart can be just vector coordinates -> { x, y, z }
// or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] }
var keys = Object.keys( pathPart );
var method = keys[0];
var points = pathPart[ method ];
var isInstruction = keys.length === 1 && actionNames.includes( method ) &&
Array.isArray( points );
if ( !isInstruction ) {
method = 'line';
points = [ pathPart ];
}
// first action is always move
method = i === 0 ? 'move' : method;
// arcs require previous last point
var pathAction = new PathAction( method, points, previousPoint );
// update previousLastPoint
previousPoint = pathAction.endRenderPoint;
return pathAction;
});
};
Shape.prototype.addChild = function( shape ) {
this.children.push( shape );
};
// ----- update ----- //
Shape.prototype.update = function() {
// update self
this.reset();
// update children
this.children.forEach( function( child ) {
child.update();
});
this.transform( this.translate, this.rotate, this.scale );
};
Shape.prototype.reset = function() {
// reset pathAction render points
this.pathActions.forEach( function( pathAction ) {
pathAction.reset();
});
};
Shape.prototype.transform = function( translation, rotation, scale ) {
// transform points
this.pathActions.forEach( function( pathAction ) {
pathAction.transform( translation, rotation, scale );
});
// transform children
this.children.forEach( function( child ) {
child.transform( translation, rotation, scale );
});
};
Shape.prototype.updateSortValue = function() {
var sortValueTotal = 0;
this.pathActions.forEach( function( pathAction ) {
sortValueTotal += pathAction.endRenderPoint.z;
});
// average sort value of all points
// def not geometrically correct, but works for me
this.sortValue = sortValueTotal / this.pathActions.length;
};
// ----- render ----- //
Shape.prototype.render = function( ctx ) {
var length = this.pathActions.length;
if ( !this.rendering || !length ) {
return;
}
var isDot = length == 1;
if ( isDot ) {
this.renderDot( ctx );
} else {
this.renderPath( ctx );
}
};
// Safari does not render lines with no size, have to render circle instead
Shape.prototype.renderDot = function( ctx ) {
ctx.fillStyle = this.color;
var point = this.pathActions[0].endRenderPoint;
ctx.beginPath();
var radius = this.lineWidth/2;
ctx.arc( point.x, point.y, radius, 0, TAU );
ctx.fill();
};
Shape.prototype.renderPath = function( ctx ) {
// set render properties
ctx.fillStyle = this.color;
ctx.strokeStyle = this.color;
ctx.lineWidth = this.lineWidth;
// render points
ctx.beginPath();
this.pathActions.forEach( function( pathAction ) {
pathAction.render( ctx );
});
var isTwoPoints = this.pathActions.length == 2 &&
this.pathActions[1].method == 'line';
if ( !isTwoPoints && this.closed ) {
ctx.closePath();
}
if ( this.stroke ) {
ctx.stroke();
}
if ( this.fill ) {
ctx.fill();
}
};
// return Array of self & all child shapes
Shape.prototype.getShapes = function() {
var shapes = [ this ];
this.children.forEach( function( child ) {
var childShapes = child.getShapes();
shapes = shapes.concat( childShapes );
});
return shapes;
};
Shape.prototype.copy = function( options ) {
// copy options
var shapeOptions = {};
optionKeys.forEach( function( key ) {
shapeOptions[ key ] = this[ key ];
}, this );
// add set options
setOptions( shapeOptions, options );
var ShapeClass = this.constructor;
return new ShapeClass( shapeOptions );
};
// -------------------------- Ellipse -------------------------- //
function Ellipse( options ) {
options = this.setPath( options );
// always keep open
// fixes overlap bug when lineWidth is greater than radius
options.closed = false;
this.create( options );
}
Ellipse.prototype = Object.create( Shape.prototype );
Ellipse.prototype.constructor = Ellipse;
Ellipse.prototype.setPath = function( options ) {
var w = options.width/2;
var h = options.height/2;
options.path = [
{ x: 0, y: -h },
{ arc: [ // top right
{ x: w, y: -h },
{ x: w, y: 0 },
]},
{ arc: [ // bottom right
{ x: w, y: h },
{ x: 0, y: h },
]},
{ arc: [ // bottom left
{ x: -w, y: h },
{ x: -w, y: 0 },
]},
{ arc: [ // bottom left
{ x: -w, y: -h },
{ x: 0, y: -h },
]},
];
return options;
};
// -------------------------- Group -------------------------- //
function Group( options ) {
this.create( options );
}
Group.prototype.create = function( options ) {
// set options
setGroupOptions( this, options );
// transform
this.translate = Vector3.sanitize( this.translate );
this.rotate = Vector3.sanitize( this.rotate );
// children
this.children = [];
if ( this.addTo ) {
this.addTo.addChild( this );
}
};
var groupOptionKeys = [
'rotate',
'translate',
'addTo',
];
function setGroupOptions( shape, options ) {
for ( var key in options ) {
if ( groupOptionKeys.includes( key ) ) {
shape[ key ] = options[ key ];
}
}
}
Group.prototype.addChild = function( shape ) {
this.children.push( shape );
};
// ----- update ----- //
Group.prototype.update = function() {
// update self
this.reset();
// update children
this.children.forEach( function( child ) {
child.update();
});
this.transform( this.translate, this.rotate, this.scale );
};
Group.prototype.reset = function() {};
Group.prototype.transform = function( translation, rotation, scale ) {
// transform children
this.children.forEach( function( child ) {
child.transform( translation, rotation, scale );
});
};
Group.prototype.updateSortValue = function() {
var sortValueTotal = 0;
this.children.forEach( function( child ) {
child.updateSortValue();
sortValueTotal += child.sortValue;
});
// TODO sort children?
// average sort value of all points
// def not geometrically correct, but works for me
this.sortValue = sortValueTotal / this.children.length;
};
// ----- render ----- //
Group.prototype.render = function( ctx ) {
this.children.forEach( function( child ) {
child.render( ctx );
});
};
// do not include children, group handles rendering & sorting internally
Group.prototype.getShapes = function() {
return [ this ];
};
// -------------------------- Dragger -------------------------- //
// quick & dirty drag event stuff
// messes up if multiple pointers/touches
// event support, default to mouse events
var downEvent = 'mousedown';
var moveEvent = 'mousemove';
var upEvent = 'mouseup';
if ( window.PointerEvent ) {
// PointerEvent, Chrome
downEvent = 'pointerdown';
moveEvent = 'pointermove';
upEvent = 'pointerup';
} else if ( 'ontouchstart' in window ) {
// Touch Events, iOS Safari
downEvent = 'touchstart';
moveEvent = 'touchmove';
upEvent = 'touchend';
}
function noop() {}
function Dragger( options ) {
this.startElement = options.startElement;
this.onPointerDown = options.onPointerDown || noop;
this.onPointerMove = options.onPointerMove || noop;
this.onPointerUp = options.onPointerUp || noop;
this.startElement.addEventListener( downEvent, this );
}
Dragger.prototype.handleEvent = function( event ) {
var method = this[ 'on' + event.type ];
if ( method ) {
method.call( this, event );
}
};
Dragger.prototype.onmousedown =
Dragger.prototype.onpointerdown = function( event ) {
this.pointerDown( event, event );
};
Dragger.prototype.ontouchstart = function( event ) {
this.pointerDown( event, event.changedTouches[0] );
};
Dragger.prototype.pointerDown = function( event, pointer ) {
event.preventDefault();
this.dragStartX = pointer.pageX;
this.dragStartY = pointer.pageY;
window.addEventListener( moveEvent, this );
window.addEventListener( upEvent, this );
this.onPointerDown( pointer );
};
Dragger.prototype.ontouchmove = function( event ) {
// HACK, moved touch may not be first
this.pointerMove( event, event.changedTouches[0] );
};
Dragger.prototype.onmousemove =
Dragger.prototype.onpointermove = function( event ) {
this.pointerMove( event, event );
};
Dragger.prototype.pointerMove = function( event, pointer ) {
event.preventDefault();
var moveX = pointer.pageX - this.dragStartX;
var moveY = pointer.pageY - this.dragStartY;
this.onPointerMove( pointer, moveX, moveY );
};
Dragger.prototype.onmouseup =
Dragger.prototype.onpointerup =
Dragger.prototype.ontouchend =
Dragger.prototype.pointerUp = function( event ) {
window.removeEventListener( moveEvent, this );
window.removeEventListener( upEvent, this );
this.onPointerUp( event );
};
function BokehShape( options ) {
this.create( options );
this.bokehSize = options.bokehSize || 5;
this.bokehLimit = options.bokehLimit || 64;
}
BokehShape.prototype = Object.create( Shape.prototype );
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.getBokehLineWidth = function() {
return this.lineWidth + this.bokehSize * this.bokeh * this.bokeh;
};
BokehShape.prototype.getBokehAlpha = function() {
var revBokeh = 1 - this.bokeh;
revBokeh *= revBokeh;
return revBokeh * 0.8 + 0.2;
};
// Safari does not render lines with no size, have to render circle instead
BokehShape.prototype.renderDot = function( ctx ) {
this.updateBokeh();
ctx.globalAlpha = this.getBokehAlpha();
ctx.fillStyle = this.color;
var point = this.pathActions[0].endRenderPoint;
ctx.beginPath();
var radius = this.getBokehLineWidth()/2;
ctx.arc( point.x, point.y, radius, 0, TAU );
ctx.fill();
ctx.globalAlpha = 1;
};
BokehShape.prototype.renderPath = function( ctx ) {
this.updateBokeh();
ctx.globalAlpha = this.getBokehAlpha();
// set render properties
ctx.fillStyle = this.color;
ctx.strokeStyle = this.color;
ctx.lineWidth = this.getBokehLineWidth();
// render points
ctx.beginPath();
this.pathActions.forEach( function( pathAction ) {
pathAction.render( ctx );
});
var isTwoPoints = this.pathActions.length == 2 &&
this.pathActions[1].method == 'line';
if ( !isTwoPoints && this.closed ) {
ctx.closePath();
}
if ( this.stroke ) {
ctx.stroke();
}
if ( this.fill ) {
ctx.fill();
}
ctx.globalAlpha = 1;
};
/* jshint unused: false */
function makeMadeline( camera, isGood, colors, rotation ) {
var rotor = new Shape({
rendering: false,
addTo: camera,
rotate: rotation,
});
var body = new Shape({
rendering: false,
addTo: rotor,
rotate: { x: TAU/8 },
translate: { z: 48 },
});
var head = new Shape({
rendering: false,
addTo: body,
translate: { y: -11, z: 2 },
rotate: { x: -TAU/8 },
});
// face
var face = new Ellipse({
width: 6,
height: 6,
addTo: head,
translate: { z: -4 },
lineWidth: 8,
color: colors.skin,
});
var eyeGroup = new Group({
addTo: face,
translate: { z: -face.lineWidth/2 + 0.5 },
});
// eyes
[ -1, 1 ].forEach( function( xSide ) {
// cheek blush
if ( isGood ) {
new Ellipse({
width: 2,
height: 1.3,
addTo: eyeGroup,
translate: { x: 4.5*xSide, y: 3, z: 1 },
rotate: { y: TAU/16*xSide },
lineWidth: 1,
color: '#FA8',
fill: true,
});
}
var eyeX = 3.5*xSide;
// eye
var eyeWhite = new Ellipse({
width: 0.75,
height: 1.5,
addTo: eyeGroup,
color: colors.eye,
translate: { x: eyeX },
lineWidth: 2,
fill: true,
});
// eye brow
new Shape({
path: [
{ x: -1, y: 0 },
{ arc: [
{ x: -1, y: -1 },
{ x: 0, y: -1 }
]},
{ arc: [
{ x: 1, y: -1 },
{ x: 1, y: 0 }
]},
],
addTo: eyeGroup,
translate: { x: eyeX, y: -3 },
scale: { x: 1.5, y: 0.6 },
rotate: { z: 0.15*xSide * (isGood ? 1 : -1) },
color: colors.hair,
lineWidth: 1,
fill: true,
});
});
// hair ball
new Shape({
path: [
{ x: -1 },
{ x: 1 },
{ z: 4 },
],
addTo: head,
translate: { y: -4, z: 1 },
lineWidth: 18,
color: colors.hair,
});
var bang = new 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 },
lineWidth: 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 Shape({
path: [
{ x: -3 },
{ x: 3 },
],
addTo: head,
lineWidth: 7,
translate: { y: -8, z: -5 },
color: colors.hair,
});
// trail locks
var trailLock = new 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 },
lineWidth: 10,
color: colors.hair,
closed: false,
});
trailLock.copy({
translate: { x: -3, z: 4 },
rotate: { z: -TAU/8 },
lineWidth: 8,
});
trailLock.copy({
translate: { x: 3, z: 4 },
rotate: { z: TAU/8 },
lineWidth: 8,
});
trailLock.copy({
translate: { y: 2 },
// rotate: { z: TAU/2 },
scale: { y: 0.5 },
lineWidth: 8,
});
// ----- torso ----- //
// 2nd rib
var torsoRib = new Ellipse({
width: 12,
height: 10,
addTo: body,
rotate: { x: TAU/4 },
translate: { y: -1 },
lineWidth: 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 Ellipse({
width: 10,
height: 8,
addTo: body,
rotate: { x: TAU/4 },
translate: { y: 11 },
lineWidth: 4,
color: colors.tight,
fill: true,
});
// arms
[ -1, 1 ].forEach( function( xSide ) {
// shoulder ball
new Shape({
addTo: body,
lineWidth: 6,
translate: { x: 6*xSide, y: -5, z: 1 },
color: colors.parkaLight,
});
var shoulderJoint = new Shape({
rendering: false,
addTo: body,
translate: { x: 9*xSide, y: -3, z: 2 },
});
// top shoulder rib
var armRib = new Ellipse({
width: 2,
height: 2,
rotate: { x: TAU/4 },
addTo: shoulderJoint,
translate: { x: 0*xSide },
lineWidth: 6,
color: colors.parkaLight,
fill: true,
});
armRib.copy({
translate: { y: 4 },
});
var elbowJoint = new Shape({
rendering: false,
addTo: shoulderJoint,
translate: { y: 8 },
});
armRib.copy({
addTo: elbowJoint,
translate: { x: 0, y: 0 },
});
armRib.copy({
addTo: elbowJoint,
translate: { y: 4 },
color: colors.parkaDark,
});
// hand
new Shape({
addTo: elbowJoint,
translate: { y: 9, z: -1 },
lineWidth: 8,
color: colors.skin,
});
if ( xSide == 1 ) {
// extend left hand
shoulderJoint.rotate = Vector3.sanitize({ x: -TAU/8*3, z: -TAU/32 });
} else {
// back right hand
shoulderJoint.rotate = Vector3.sanitize({ z: TAU/16*2, x: TAU/16*2 });
elbowJoint.rotate = Vector3.sanitize({ z: TAU/8 });
}
// ----- legs ----- //
var knee = { y: 7 };
var thigh = new Shape({
path: [ { y: 0 }, knee ],
addTo: body,
translate: { x: 4*xSide, y: 13 },
lineWidth: 8,
color: colors.tight,
});
var shin = new Shape({
path: [ { y: 0 }, { y: 8 } ],
addTo: thigh,
lineWidth: 6,
translate: knee,
color: colors.tight,
});
if ( xSide == -1 ) {
// bend right leg
thigh.rotate = Vector3.sanitize({ x: -TAU/16*3, z: TAU/16 });
shin.rotate = Vector3.sanitize({ x: TAU/16*5 });
}
});
// butt
new Shape({
path: [
{ x: -3 },
{ x: 3 },
],
rendering: false,
addTo: body,
translate: { y: 11, z: 2 },
lineWidth: 8,
color: colors.tight,
});
}
function makeBird( options ) {
var spin = options.spin || 0;
var arrow = new Shape({
rendering: false,
addTo: options.addTo,
scale: { x: 2/3, y: 2/3, z: 2/3 },
rotate: { z: spin },
});
var bird = new Group({
addTo: arrow,
translate: { x: 87 },
rotate: { x: spin },
});
// bird body
new 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 },
lineWidth: 3,
color: options.color,
fill: true,
});
// bird head
new Shape({
translate: { x: 4, y: -1 },
addTo: bird,
lineWidth: 4,
color: options.color,
});
// beak
new Shape({
path: [
{ x: 0, y: -1 },
{ x: 3, y: 0 },
{ x: 0, y: 1 },
],
addTo: bird,
translate: { x: 5, y: -1 },
lineWidth: 1,
color: options.color,
fill: true,
});
// tail feather
new Shape({
path: [
{ x: -3, z: -2 },
{ x: 0, z: 0 },
{ x: -3, z: 2 },
],
addTo: bird,
translate: { x: -4, y: 0 },
lineWidth: 2,
color: options.color,
fill: true,
});
var wing = new 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 },
lineWidth: 1,
color: options.color,
fill: true,
});
wing.copy({
translate: { z: 1.5},
scale: { z: -1 },
rotate: { x: -TAU/8 },
});
}
/* globals makeMadeline, BokehShape, makeBird */
// -------------------------- demo -------------------------- //
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var w = 160;
var h = 160;
var minWindowSize = Math.min( window.innerWidth, window.innerHeight );
var zoom = Math.min( 5, Math.floor( minWindowSize / w ) );
var pixelRatio = window.devicePixelRatio || 1;
zoom *= pixelRatio;
var canvasWidth = canvas.width = w * zoom;
var canvasHeight = canvas.height = h * zoom;
// set canvas screen size
if ( pixelRatio > 1 ) {
canvas.style.width = canvasWidth / pixelRatio + 'px';
canvas.style.height = canvasHeight / pixelRatio + 'px';
}
var isRotating = true;
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';
var camera = new Shape({
rendering: false,
rotate: { y: TAU/4 },
});
// -- illustration shapes --- //
makeMadeline( camera, true, madColor );
makeMadeline( camera, false, badColor, { y: TAU/2 } );
// ----- feather ----- //
var feather = new Shape({
rendering: false,
addTo: camera,
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 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 },
lineWidth: 1,
color: featherGold,
fill: true,
});
barb.copy({
scale: { x: -1 },
});
}
// rachis
var rachis = new Shape({
path: [
{ y: -radius },
{ arc: [
{ y: -radius, z: -radius },
{ y: 0, z: -radius },
]},
{ arc: [
{ y: radius, z: -radius },
{ y: radius, z: 0 },
]},
],
addTo: feather,
lineWidth: 2,
color: featherGold,
closed: false,
});
rachis.copy({
lineWidth: 8,
color: glow,
rotate: { x: -0.5 }
});
})();
// ----- rods ----- //
( function() {
var rodCount = 14;
for ( var i=0; i < rodCount; i++ ) {
var zRotor = new Shape({
rendering: false,
addTo: camera,
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,
lineWidth: 1,
bokehSize: 6,
bokehLimit: 70,
});
}
})();
// dots
( function() {
var dotCount = 64;
for ( var i=0; i < dotCount; i++ ) {
var yRotor = new Shape({
rendering: false,
addTo: camera,
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,
lineWidth: 1 + Math.random(),
bokehSize: 6,
bokehLimit: 74,
});
}
})();
// ----- birds ----- //
var birdRotor = new Shape({
rendering: false,
addTo: camera,
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,
});
// ----- ----- //
var shapes = camera.getShapes();
// -- animate --- //
var rotateSpeed = -TAU/60;
var xClock = 0;
var then = new Date() - 1/60;
function animate() {
update();
render();
requestAnimationFrame( animate );
}
animate();
// -- update -- //
function update() {
var now = new Date();
var delta = now - then;
// auto rotate
if ( isRotating ) {
var theta = rotateSpeed/60 * delta;
camera.rotate.y += theta;
xClock += theta/4;
camera.rotate.x = Math.sin( xClock ) * TAU/12;
}
// rotate
camera.update();
shapes.forEach( function( shape ) {
shape.updateSortValue();
});
// perspective sort
shapes.sort( function( a, b ) {
return b.sortValue - a.sortValue;
});
then = now;
}
// -- render -- //
function render() {
ctx.clearRect( 0, 0, canvasWidth, canvasHeight );
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.save();
ctx.scale( zoom, zoom );
ctx.translate( w/2, h/2 );
shapes.forEach( function( shape ) {
shape.render( ctx );
});
ctx.restore();
}
// ----- inputs ----- //
// click drag to rotate
var dragStartAngleX, dragStartAngleY;
new Dragger({
startElement: canvas,
onPointerDown: function() {
isRotating = false;
dragStartAngleX = camera.rotate.x;
dragStartAngleY = camera.rotate.y;
},
onPointerMove: function( pointer, moveX, moveY ) {
var angleXMove = moveY / canvasWidth * TAU;
var angleYMove = moveX / canvasWidth * TAU;
camera.rotate.x = dragStartAngleX + angleXMove;
camera.rotate.y = dragStartAngleY + angleYMove;
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment