Created
March 5, 2018 08:41
-
-
Save adimancv/af749ab5726f0bacebea84cb74201709 to your computer and use it in GitHub Desktop.
gambar-3d-dengan-js
This file contains 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
// -------------------------- 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