THREE.js FTW. My friend and I designed and built a crib. This is its core functionality minus some additional supports.
A Pen by Jake Albaugh on CodePen.
| <canvas id=canvas></canvas> | |
| <p> | |
| Crib v1.0<br> | |
| <small> | |
| rotate: click + drag | zoom: scroll | |
| </small> | |
| </p> |
THREE.js FTW. My friend and I designed and built a crib. This is its core functionality minus some additional supports.
A Pen by Jake Albaugh on CodePen.
| var crib = new Crib(); | |
| crib.init(); | |
| // refresh crib size | |
| var refreshCrib = debounce(function() { | |
| crib.refresh(); | |
| crib.renderer.domElement.className = ""; | |
| }, 250); | |
| var fadeCrib = function() { | |
| if(crib.renderer.domElement.className == "") { | |
| crib.renderer.domElement.className = "fade-out"; | |
| } | |
| }; | |
| window.addEventListener('resize', refreshCrib); | |
| window.addEventListener('resize', fadeCrib); | |
| // | |
| // crib design | |
| // | |
| function Crib() { | |
| // return relative value for inches | |
| function inch(i) { | |
| return i / 12; | |
| } | |
| return { | |
| // THREE scene object | |
| scene: undefined, | |
| // THREE camera object | |
| camera: undefined, | |
| // THREE renderer | |
| renderer: undefined, | |
| // THREE light objects | |
| directionalLight: undefined, | |
| ambientLight: undefined, | |
| hemisphereLight: undefined, | |
| // container is our master wrapper for animation | |
| container: undefined, | |
| // reset size and positioning | |
| refresh: function() { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize( window.innerWidth, window.innerHeight ); | |
| }, | |
| // instantiation methods | |
| set: { | |
| scene: function() { | |
| return new THREE.Scene(); | |
| }, | |
| container: function() { | |
| return new THREE.Object3D(); | |
| }, | |
| camera: function() { | |
| return new THREE.PerspectiveCamera( | |
| 80, // perspective | |
| window.innerWidth / window.innerHeight, // aspect ratio | |
| 0.001, // near clip | |
| 1000 // far clip | |
| ) | |
| }, | |
| renderer: function() { | |
| return new THREE.WebGLRenderer({ | |
| canvas: document.getElementById("canvas") | |
| }); | |
| }, | |
| directionalLight: function() { | |
| return new THREE.DirectionalLight( 0x999999 ); | |
| }, | |
| ambientLight: function() { | |
| return new THREE.AmbientLight( 0x666666 ); | |
| }, | |
| hemisphereLight: function() { | |
| return new THREE.HemisphereLight({ | |
| skyColorHex: "#fff", | |
| groundColorHex: "#000", | |
| intensity: 1 | |
| }); | |
| } | |
| }, | |
| init: function() { | |
| this.scene = this.set.scene(); | |
| this.container = this.set.container(); | |
| this.camera = this.set.camera(); | |
| this.renderer = this.set.renderer(); | |
| this.directionalLight = this.set.directionalLight(), | |
| this.ambientLight = this.set.ambientLight(); | |
| this.hemisphereLight = this.set.hemisphereLight(); | |
| var scene = this.scene, | |
| container = this.container, | |
| camera = this.camera, | |
| renderer = this.renderer, | |
| directionalLight = this.directionalLight, | |
| ambientLight = this.ambientLight, | |
| hemisphereLight = this.hemisphereLight; | |
| // add container to scene | |
| scene.add(container); | |
| // adjust camera | |
| camera.position.z = 5; | |
| // scale the renderer | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setClearColor( 0x222222, 1); | |
| // add renderer to the DOM | |
| document.body.appendChild(renderer.domElement); | |
| // initialize the lights | |
| directionalLight.position = camera.position; | |
| scene.add(directionalLight); | |
| //ambientLight.position = camera.position; | |
| //scene.add(ambientLight); | |
| hemisphereLight.position = camera.position; | |
| scene.add(hemisphereLight); | |
| // container rotations | |
| container.rotation.y = 0; | |
| container.rotation.x = 0; | |
| // container position | |
| container.position.y = inch(-20); | |
| // calling each segment | |
| var front = this.length( inch(-15.25), "#FFBB00"), | |
| back = this.length( inch( 13.75), "#C69100"), | |
| left = this.depth( inch( 26.75), "#00A383"), | |
| right = this.depth( inch(-26.25), "#007E65"), | |
| floor = this.floor( "#440468"); | |
| // add the floor | |
| floor.init(this.container); | |
| // add the front | |
| front.init(this.container); | |
| // add the back | |
| back.init(this.container); | |
| // add the left | |
| left.init(this.container); | |
| // add the right | |
| right.init(this.container); | |
| var floor_percentage = 0; | |
| // render loop | |
| var render = function () { | |
| requestAnimationFrame(render); | |
| renderer.render(scene, camera); | |
| floor.animate(floor_percentage++ % 1000 / 1000); | |
| //container.rotation.y += inch(0.05); | |
| //container.rotation.x += 0.005; | |
| }; | |
| // call first render | |
| render(); | |
| // | |
| // positioning events | |
| // | |
| var hold = false; | |
| var prev = false; | |
| var canvas = document.getElementById("canvas"); | |
| canvas.addEventListener("mousedown", function(e) { | |
| prev = {x: e.screenX, y: e.screenY} | |
| hold = true; | |
| }, false); | |
| canvas.addEventListener("mouseup", function() { | |
| hold = false; | |
| }, false); | |
| canvas.addEventListener("mousemove", function(e) { | |
| if(hold){ | |
| var y = e.screenY, | |
| x = e.screenX; | |
| container.rotation.x += (y - prev.y) / 500; | |
| container.rotation.y += (x - prev.x) / 500; | |
| prev = {x: x, y: y}; | |
| } | |
| }, false); | |
| var max_zoom_out = 5; | |
| canvas.addEventListener("mousewheel", function(e) { | |
| var delta = e.deltaY, | |
| d; | |
| if (delta < 0) { | |
| d = Math.max(-200, delta) / 200; | |
| } else { | |
| d = Math.min(200, delta) / 200; | |
| } | |
| if ((d < 0 && container.position.z > -5) || (d >= 0 && container.position.z < 3)) container.position.z += d; | |
| }); | |
| }, | |
| // | |
| // length side | |
| // | |
| length: function(z, color) { | |
| return { | |
| obj: undefined, | |
| pos: { x:inch(2.5), y:inch(20), z:z }, | |
| dim: { x:inch(56.5), y:inch(40), z:inch(0.75) }, | |
| material: new THREE.MeshPhongMaterial({ | |
| color: new THREE.Color(color), | |
| wrapAround: true | |
| }), | |
| chunk: function(width,length,x,y) { | |
| var dim = { x:width, y:length, z:inch(0.75)}; | |
| var pos = { | |
| x: this.dim.x / -2 + dim.x / 2 + x, | |
| y: this.dim.y / -2 + dim.y / 2 + y, | |
| z: 0 | |
| }; | |
| var geo = new THREE.BoxGeometry( dim.x, dim.y, dim.z ); | |
| var chunk = new THREE.Mesh(geo); | |
| chunk.position.set(pos.x,pos.y,pos.z); | |
| chunk.updateMatrix(); | |
| return chunk; | |
| }, | |
| init: function(container) { | |
| this.obj = new THREE.Object3D(); | |
| container.add(this.obj); | |
| // adjust vertical position of group | |
| this.obj.position.set(this.pos.x,this.pos.y,this.pos.z); | |
| // | |
| // total geometry for merging shapes | |
| // | |
| var total_geometry = new THREE.Geometry(); | |
| // | |
| // chunks | |
| // | |
| var bottom = this.chunk(inch(52),inch(7),0,0); | |
| total_geometry.merge(bottom.geometry, bottom.matrix); | |
| var top = this.chunk(inch(52),inch(3),0,inch(37)); | |
| total_geometry.merge(top.geometry, top.matrix); | |
| var left = this.chunk(inch(2.5),inch(30),0,inch(7)); | |
| total_geometry.merge(left.geometry, left.matrix); | |
| var right = this.chunk(inch(2.5),inch(30),inch(52 - 2.5),inch(7)); | |
| total_geometry.merge(right.geometry, right.matrix); | |
| // shaft around negative space | |
| for (var i = 0; i < 12; i++) { | |
| var shaft = this.chunk( | |
| inch(1.7), inch(30), | |
| inch(4.5 + ((3.75) * i)), | |
| inch(7) | |
| ); | |
| total_geometry.merge(shaft.geometry, shaft.matrix); | |
| } | |
| // left top hook | |
| var hook_lt = this.chunk(inch(2.3),inch(3),inch(-2.3),inch(32)); | |
| total_geometry.merge(hook_lt.geometry, hook_lt.matrix); | |
| // left middle hook | |
| var hook_lm = this.chunk(inch(2.3),inch(3),inch(-2.3),inch(20)); | |
| total_geometry.merge(hook_lm.geometry, hook_lm.matrix); | |
| // left bottom hook | |
| var hook_lb = this.chunk(inch(2.3),inch(3),inch(-2.3),inch(8)); | |
| total_geometry.merge(hook_lb.geometry, hook_lb.matrix); | |
| // right top hook | |
| var hook_rt = this.chunk(inch(2.3),inch(3),inch(52),inch(32)); | |
| total_geometry.merge(hook_rt.geometry, hook_rt.matrix); | |
| // right middle hook | |
| var hook_rm = this.chunk(inch(2.3),inch(3),inch(52),inch(20)); | |
| total_geometry.merge(hook_rm.geometry, hook_rm.matrix); | |
| // right bottom hook | |
| var hook_rb = this.chunk(inch(2.3),inch(3),inch(52),inch(8)); | |
| total_geometry.merge(hook_rb.geometry, hook_rb.matrix); | |
| var combined = new THREE.Mesh(total_geometry, this.material); | |
| this.obj.add(combined); | |
| } | |
| } | |
| }, | |
| // | |
| // depth side | |
| // | |
| depth: function(x, color) { | |
| return { | |
| obj: undefined, | |
| pos: { x:x, y:inch(20), z:inch(-0.75) }, | |
| dim: { x:inch(0.75), y:inch(40), z:inch(32) }, | |
| material: new THREE.MeshPhongMaterial({ | |
| color: new THREE.Color(color), | |
| wrapAround: true | |
| }), | |
| chunk: function(width,height,y,z) { | |
| var dim = { x:inch(0.75), y:height, z:width}; | |
| var pos = { | |
| x: 0, | |
| y: this.dim.y / -2 + dim.y / 2 + y, | |
| z: this.dim.z / -2 + dim.z / 2 + z | |
| }; | |
| var geo = new THREE.BoxGeometry( dim.x, dim.y, dim.z ); | |
| var chunk = new THREE.Mesh(geo); | |
| chunk.position.set(pos.x,pos.y,pos.z); | |
| chunk.updateMatrix(); | |
| return chunk; | |
| }, | |
| init: function(container) { | |
| this.obj = new THREE.Object3D(); | |
| container.add(this.obj); | |
| // adjust vertical position of group | |
| this.obj.position.set(this.pos.x,this.pos.y,this.pos.z); | |
| // | |
| // total geometry for merging shapes | |
| // | |
| var total_geometry = new THREE.Geometry(); | |
| // | |
| // chunks | |
| // | |
| function holePattern(x) { | |
| return [ | |
| [inch(1),inch(40),0, x], // left runner | |
| [inch(0.9),inch(6.75),0,x + inch(1)], // bottom | |
| [inch(0.9),inch(8.75),inch(10),x + inch(1)], // middle bottom | |
| [inch(0.9),inch(8.75),inch(22),x + inch(1)], // middle top | |
| [inch(0.9),inch(6),inch(34),x + inch(1)], // top | |
| [inch(1),inch(40),0, x + inch(1.9)]//, // right runner | |
| ]; | |
| } | |
| function slotPattern(x) { | |
| return [ | |
| [inch(1.25),inch(40),0, x], // left runner | |
| [inch(3),inch(7),0,x + inch(1)], // bottom chunk | |
| [inch(2),inch(0.875),inch(7),x + inch(1)], // bottom notch | |
| [inch(2),inch(9),inch(9), x + inch(2)], // middle bottom chunk | |
| [inch(1),inch(0.975),inch(18), x + inch(2)], // middle bottom notch | |
| [inch(2),inch(8),inch(20), x + inch(2)], // middle top chunk | |
| [inch(1),inch(0.975),inch(28), x + inch(2)], // middle top notch | |
| [inch(3),inch(10),inch(30), x + inch(1)], // top | |
| [inch(1.25),inch(40),0, x + inch(4)] // right runner | |
| ]; | |
| } | |
| var left_holes = holePattern(inch(0)); | |
| var right_holes = holePattern(inch(32 - 2.9)); | |
| for (var h = 0; h < left_holes.length; h++) { | |
| var left = this.chunk.apply(this, left_holes[h]); | |
| var right = this.chunk.apply(this, right_holes[h]); | |
| total_geometry.merge(left.geometry, left.matrix); | |
| total_geometry.merge(right.geometry, right.matrix); | |
| } | |
| var left_slot = slotPattern(inch(2.875)); | |
| var right_slot = slotPattern(inch(23.875)); | |
| for (var s = 0; s < left_slot.length; s++) { | |
| var left = this.chunk.apply(this, left_slot[s]); | |
| var right = this.chunk.apply(this, right_slot[s]); | |
| total_geometry.merge(left.geometry, left.matrix); | |
| total_geometry.merge(right.geometry, right.matrix); | |
| } | |
| var main = this.chunk(inch(16.5),inch(33),inch(7),inch(8)); | |
| total_geometry.merge(main.geometry, main.matrix); | |
| var combined = new THREE.Mesh(total_geometry, this.material); | |
| this.obj.add(combined); | |
| } | |
| } | |
| }, | |
| // | |
| // the floor structure | |
| // | |
| floor: function(color) { | |
| return { | |
| obj: undefined, | |
| pos: { x:0, y:inch(7), z:0 }, | |
| dim: { x:inch(52), y:inch(0.75), z:inch(26) }, | |
| material: new THREE.MeshPhongMaterial({ | |
| color: new THREE.Color(color), | |
| wrapAround: true | |
| }), | |
| path: undefined, /// animation path | |
| base: function() { | |
| var geo = new THREE.BoxGeometry(this.dim.x, this.dim.y, this.dim.z); | |
| var base = new THREE.Mesh(geo, this.material) | |
| base.updateMatrix(); | |
| return base; | |
| }, | |
| notch: function(width,length,x,z) { | |
| var dim = { x:width, y:inch(0.75), z:length}; | |
| var pos = { | |
| x: this.dim.x / -2 + dim.x / 2 + x, | |
| y: 0, | |
| z: this.dim.z / -2 + dim.z / 2 + z | |
| }; | |
| var geo = new THREE.BoxGeometry( dim.x, dim.y, dim.z ); | |
| var notch = new THREE.Mesh(geo, this.material); | |
| notch.position.set(pos.x,pos.y,pos.z); | |
| notch.updateMatrix(); | |
| return notch; | |
| }, | |
| animate: function(percent) { | |
| var plot = this.path[Math.floor(this.path.length * percent)]; | |
| this.obj.position.z = plot.z; | |
| this.obj.position.y = plot.y; | |
| }, | |
| animationPath: function() { | |
| var inc = inch(0.1), | |
| path = [], | |
| points = [ | |
| [inch(0.3), inch(7.375)], | |
| [inch(0.3), inch(8.375)], | |
| [inch(-1.5), inch(8.375)], | |
| [inch(-1.5), inch(19.375)], | |
| [inch(0.3), inch(19.375)], | |
| [inch(0.3), inch(18.375)], | |
| [inch(0.3), inch(19.375)], | |
| [inch(-1.5), inch(19.375)], | |
| [inch(-1.5), inch(29.375)], | |
| [inch(0.3), inch(29.375)], | |
| [inch(0.3), inch(28.375)], | |
| [inch(0.3), inch(29.375)], | |
| [inch(-1.5), inch(29.375)], | |
| [inch(-1.5), inch(8.375)], | |
| [inch(0.3), inch(8.375)], | |
| [inch(0.3), inch(7.375)] | |
| ], | |
| pauses = [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0]; | |
| for(var i = 0; i < points.length; i++) { | |
| // if not the last | |
| if (i + 1 < points.length) { | |
| var axis = (points[i][0] != points[i+1][0]) ? 'x' : 'y'; | |
| // set initial points | |
| var z = points[i][0]; | |
| var y = points[i][1]; | |
| // add pause frames | |
| if (pauses[i] > 0) { | |
| for (var r = 0; r < 50; r++) path.push({z: z, y: y }); | |
| } | |
| // if traveling along the y axis | |
| if(axis == 'y') { | |
| // get the direction we are moving | |
| var dir = (points[i][1] < points[i+1][1]) ? 'up' : 'down'; | |
| // if up, we are waiting for inc value to go over next plot | |
| if (dir == 'up') { | |
| while(y < points[i+1][1]) { | |
| y += inc; | |
| path.push({z: z, y: y }); | |
| } | |
| } | |
| // if down, we are waiting for inc value to go beneath next plot | |
| else { | |
| while(y > points[i+1][1]) { | |
| y -= inc; | |
| path.push({z: z, y: y }); | |
| } | |
| } | |
| // if traveling on z axis | |
| } else { | |
| // get the direction we are moving | |
| var dir = (points[i][0] < points[i+1][0]) ? 'right' : 'left'; | |
| // if right, we are waiting for inc value to go over next plot | |
| if (dir == 'right') { | |
| while(z < points[i+1][0]) { | |
| z += inc; | |
| path.push({z: z, y: y }); | |
| } | |
| } | |
| // if left, we are waiting for inc value to go beneath next plot | |
| else { | |
| while(z > points[i+1][0]) { | |
| z -= inc; | |
| path.push({z: z, y: y }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return path; | |
| }, | |
| init: function(container) { | |
| this.obj = new THREE.Object3D(); | |
| container.add(this.obj); | |
| // adjust vertical position of group | |
| this.obj.position.set(this.pos.x,this.pos.y,this.pos.z); | |
| this.path = this.animationPath(); | |
| // | |
| // total geometry for merging shapes | |
| // | |
| var total_geometry = new THREE.Geometry(); | |
| // | |
| // base | |
| // | |
| var base = this.base(); | |
| total_geometry.merge(base.geometry, base.matrix); | |
| // | |
| // horizontal notches | |
| // | |
| for (var i = 0; i < 13; i++) { | |
| // front long notches | |
| var notch_front = this.notch( | |
| inch(1.875), | |
| inch(0.75), | |
| inch(2.75 + (3.75 * i)), | |
| inch(26) | |
| ); | |
| // back short notches | |
| var notch_back = this.notch( | |
| inch(1.875), | |
| inch(3.125), | |
| inch(2.75 + (3.75 * i)), | |
| inch(-3.125) | |
| ); | |
| total_geometry.merge(notch_front.geometry, notch_front.matrix); | |
| total_geometry.merge(notch_back.geometry, notch_back.matrix); | |
| } | |
| // | |
| // side notches | |
| // | |
| // left bottom | |
| var left_bottom = this.notch(inch(2.25),inch(0.75),inch(-2.25),inch(1.9)); | |
| total_geometry.merge(left_bottom.geometry, left_bottom.matrix); | |
| // left top | |
| var left_top = this.notch(inch(2.25),inch(0.75),inch(-2.25),inch(22.9)); | |
| total_geometry.merge(left_top.geometry, left_top.matrix); | |
| // right bottom | |
| var right_bottom = this.notch(inch(2.25),inch(0.75),inch(52),inch(1.9)); | |
| total_geometry.merge(right_bottom.geometry, right_bottom.matrix); | |
| // right top | |
| var right_top = this.notch(inch(2.25),inch(0.75),inch(52),inch(22.9)); | |
| total_geometry.merge(right_top.geometry, right_top.matrix); | |
| var combined = new THREE.Mesh(total_geometry, this.material); | |
| this.obj.add(combined); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // david walsh debounce | |
| // http://davidwalsh.name/javascript-debounce-function | |
| // Returns a function, that, as long as it continues to be invoked, will not | |
| // be triggered. The function will be called after it stops being called for | |
| // N milliseconds. If `immediate` is passed, trigger the function on the | |
| // leading edge, instead of the trailing. | |
| function debounce(func, wait, immediate) { | |
| var timeout; | |
| return function() { | |
| var context = this, args = arguments; | |
| var later = function() { | |
| timeout = null; | |
| if (!immediate) func.apply(context, args); | |
| }; | |
| var callNow = immediate && !timeout; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| if (callNow) func.apply(context, args); | |
| }; | |
| }; | |
| jakealbaughSignature("light"); |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.min.js"></script> | |
| canvas { | |
| position: absolute; | |
| top: 0; bottom: 0; left: 0; | |
| width:100%; | |
| transition: opacity 500ms ease-out; | |
| &.fade-out { | |
| transition-duration: 100ms; | |
| opacity: 0; | |
| } | |
| } | |
| p { | |
| z-index: 1; | |
| user-select: none; | |
| color: #999; | |
| position: absolute; | |
| top: 10px; left: 0; | |
| margin: 0; | |
| width: 100%; | |
| text-align: center; | |
| } | |
| // disable bounce | |
| html { | |
| overflow: hidden; | |
| height: 100%; | |
| } | |
| body { | |
| background: #222; | |
| height: 100%; | |
| overflow: auto; | |
| cursor: move; | |
| } |