<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Imporant meta information to make the page as rigid as possible on mobiles, to avoid unintentional zooming on the page itself -->
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Chair Tutorial</title>
<link href="https://fonts.googleapis.com/css?family=Raleway:500,700&display=swap" rel="stylesheet">
<!-- We add the loader CSS here so that assets don't pop in before the bundled CSS is loaded -->
<style>
.loading {
position: fixed;
z-index: 50;
width: 100%;
height: 100%;
top: 0; left: 0;
background: #f1f1f1;
display: flex;
justify-content: center;
align-items: center;
}
.loader{
-webkit-perspective: 120px;
-moz-perspective: 120px;
-ms-perspective: 120px;
perspective: 120px;
width: 100px;
height: 100px;
}
.loader:before{
content: "";
position: absolute;
left: 25px;
top: 25px;
width: 50px;
height: 50px;
background-color: #ff0000;
animation: flip 1s infinite;
}
@keyframes flip {
0% {
transform: rotate(0);
}
50% {
transform: rotateY(180deg);
}
100% {
transform: rotateY(180deg) rotateX(180deg);
}
}
</style>
</head>
<body>
<!-- The loading element overlays all else until the model is loaded, at which point we remove this element from the DOM -->
<div class="loading" id="js-loader"><div class="loader"></div></div>
<!-- These toggle the the different parts of the chair that can be edited, note data-option is the key that links to the name of the part in the 3D file -->
<div class="options">
<div class="option --is-active" data-option="legs">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/legs.svg" alt=""/>
</div>
<div class="option" data-option="cushions">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/cushions.svg" alt=""/>
</div>
<div class="option" data-option="base">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/base.svg" alt=""/>
</div>
<div class="option" data-option="supports">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/supports.svg" alt=""/>
</div>
<div class="option" data-option="back">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/back.svg" alt=""/>
</div>
</div>
<!-- Just a quick notice to the user that it can be interacted with -->
<span class="drag-notice" id="js-drag-notice">Drag to rotate 360°</span>
<!-- The canvas element is used to draw the 3D scene -->
<canvas id="c"></canvas>
<!-- This section currently only shows a message. Named controls as I encourage you once completing this tutorial to add the ability to filter which textures and colours are allowed on which parts ( you can't really have wood cushions can you? ) -->
<div class="controls">
<div class="info">
<div class="info__message">
<p><strong> Grab </strong> to rotate chair. <strong> Scroll </strong> to zoom. <strong> Drag </strong> swatches to view more.</p>
</div>
</div>
<!-- This tray will be filled with colors via JS, and the ability to slide this panel will be added in with a lightweight slider script (no dependency used for this) -->
<div id="js-tray" class="tray">
<div id="js-tray-slide" class="tray__slide"></div>
</div>
</div>
<!-- The main three.js file -->
<script src='https://unpkg.com/three@0.127.0/build/three.js'></script>
<!-- This brings in the ability to load custom 3D objects in the .gltf file format. Blender allows the ability to export to this format out the box -->
<script src='https://cdn.jsdelivr.net/gh/mrdoob/three.js@r92/examples/js/loaders/GLTFLoader.js'></script>
<!-- This is a simple to use extension for three.js that activates all the rotating, dragging and zooming controls we need for both mouse and touch, there isn't a clear CDN for this that I can find -->
<script src='https://threejs.org/examples/js/controls/OrbitControls.js'></script>
<script src='https://threejs.org/examples/js/controls/TransformControls.js'></script>
</body>
</html>
body,
html {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Raleway', sans-serif;
font-size: 14px;
color: #444444;
}
* {
touch-action: manipulation;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
overflow: hidden;
}
#c {
width: 100%;
height: 100%;
display: block;
top: 0;
left: 0;
}
.controls {
position: absolute;
bottom: 0;
width: 100%;
}
.options {
position: absolute;
left: 0;
}
.option {
background-size: cover;
background-position: 50%;
background-color: white;
margin-bottom: 3px;
padding: 10px;
height: 55px;
width: 55px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.option:hover {
border-left: 5px solid white;
width: 58px;
}
.option.--is-active {
border-right: 3px solid red;
width: 58px;
cursor: default;
}
.option.--is-active:hover {
border-left: none;
}
.option img {
height: 100%;
width: auto;
pointer-events: none;
}
.info {
padding: 0 1em;
display: flex;
justify-content: flex-end;
}
.info p {
margin-top: 0;
}
.tray {
width: 100%;
height: 50px;
position: relative;
overflow-x: hidden;
}
.tray__slide {
position: absolute;
display: flex;
left: 0;
transform: translateX(-50%);
animation: wheelin 1s 2s ease-in-out forwards;
}
.tray__swatch {
transition: 0.1s ease-in;
height: 50px;
min-width: 50px;
flex: 1;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.3);
background-size: cover;
background-position: center;
}
.tray__swatch:nth-child(5n+5) {
margin-right: 20px;
}
.drag-notice {
display: flex;
justify-content: center;
align-items: center;
padding: 2em;
width: 10em;
height: 10em;
box-sizing: border-box;
font-size: 0.9em;
font-weight: 800;
text-transform: uppercase;
text-align: center;
border-radius: 5em;
background: white;
position: absolute;
}
.drag-notice.start {
animation: popout 0.25s 3s forwards;
}
@keyframes popout {
to {
transform: scale(0);
}
}
@keyframes wheelin {
to {
transform: translateX(0);
}
}
@media (max-width: 960px) {
.options {
top: 0;
}
.info {
padding: 0 1em 1em 0;
}
.info__message {
display: flex;
align-items: flex-end;
}
.info__message p {
margin: 0;
font-size: 0.7em;
}
}
@media (max-width: 720px) {
.info {
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 1em 1em;
}
.info__message {
margin-bottom: 1em;
}
}
@media (max-width: 680px) {
.info {
padding: 1em 2em;
}
.info__message {
display: none;
}
.options {
bottom: 50px;
}
.option {
margin-bottom: 1px;
padding: 5px;
height: 45px;
width: 45px;
display: flex;
}
.option.--is-active {
border-right: 2px solid red;
width: 47px;
}
.option img {
height: 100%;
width: auto;
pointer-events: none;
}
}
const LOADER = document.getElementById('js-loader');
const DRAG_NOTICE = document.getElementById('js-drag-notice');
const TRAY = document.getElementById('js-tray-slide');
var theModel;
const MODEL_PATH = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/chair.glb";
var loaded = false;
var cameraFar = 5;
var activeOption = 'legs';
const colors = [
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/wood_.jpg',
size: [2,2,2],
shininess: 60
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/fabric_.jpg',
size: [4, 4, 4],
shininess: 0
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/pattern_.jpg',
size: [8, 8, 8],
shininess: 10
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/denim_.jpg',
size: [3, 3, 3],
shininess: 0
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/quilt_.jpg',
size: [6, 6, 6],
shininess: 0
},
{
color: '131417'
},
{
color: '374047'
},
{
color: '5f6e78'
},
{
color: '7f8a93'
},
{
color: '97a1a7'
},
{
color: 'acb4b9'
},
{
color: 'DF9998',
},
{
color: '7C6862'
},
{
color: 'A3AB84'
},
{
color: 'D6CCB1'
},
{
color: 'F8D5C4'
},
{
color: 'A3AE99'
},
{
color: 'EFF2F2'
},
{
color: 'B0C5C1'
},
{
color: '8B8C8C'
},
{
color: '565F59'
},
{
color: 'CB304A'
},
{
color: 'FED7C8'
},
{
color: 'C7BDBD'
},
{
color: '3DCBBE'
},
{
color: '264B4F'
},
{
color: '389389'
},
{
color: '85BEAE'
},
{
color: 'F2DABA'
},
{
color: 'F2A97F'
},
{
color: 'D85F52'
},
{
color: 'D92E37'
},
{
color: 'FC9736'
},
{
color: 'F7BD69'
},
{
color: 'A4D09C'
},
{
color: '4C8A67'
},
{
color: '25608A'
},
{
color: '75C8C6'
},
{
color: 'F5E4B7'
},
{
color: 'E69041'
},
{
color: 'E56013'
},
{
color: '11101D'
},
{
color: '630609'
},
{
color: 'C9240E'
},
{
color: 'EC4B17'
},
{
color: '281A1C'
},
{
color: '4F556F'
},
{
color: '64739B'
},
{
color: 'CDBAC7'
},
{
color: '946F43'
},
{
color: '66533C'
},
{
color: '173A2F'
},
{
color: '153944'
},
{
color: '27548D'
},
{
color: '438AAC'
}
]
const BACKGROUND_COLOR = 0xf1f1f1;
// Init the scene
const scene = new THREE.Scene();
// Set background
scene.background = new THREE.Color(BACKGROUND_COLOR );
scene.fog = new THREE.Fog(BACKGROUND_COLOR, 20, 100);
const canvas = document.querySelector('#c');
// Init the renderer
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Add a camera
var camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = cameraFar;
camera.position.x = 0;
// Add controls
var controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.maxPolarAngle = Math.PI / 2;
controls.minPolarAngle = Math.PI / 3;
controls.enableDamping = true;
controls.enablePan = false;
controls.dampingFactor = 0.1;
controls.autoRotate = false; // Toggle this if you'd like the chair to automatically rotate
controls.autoRotateSpeed = 0.2; // 30
var transformControls = new THREE.TransformControls( camera, renderer.domElement );
transformControls.addEventListener( 'change', render );
transformControls.addEventListener( 'dragging-changed', function ( event ) {
controls.enabled = ! event.value;
} );
// Initial material
const INITIAL_MTL = new THREE.MeshPhongMaterial( { color: 0xf1f1f1, shininess: 10 } );
const INITIAL_MAP = [
{childID: "back", mtl: INITIAL_MTL},
{childID: "base", mtl: INITIAL_MTL},
{childID: "cushions", mtl: INITIAL_MTL},
{childID: "legs", mtl: INITIAL_MTL},
{childID: "supports", mtl: INITIAL_MTL},
];
// Init the object loader
var loader = new THREE.GLTFLoader();
loader.load(MODEL_PATH, function(gltf) {
theModel = gltf.scene;
theModel.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});
// Set the models initial scale
theModel.scale.set(2,2,2);
theModel.rotation.y = Math.PI;
// Add the model to the scene
theModel.position.y = -1;
// Set initial textures
for (let object of INITIAL_MAP) {
initColor(theModel, object.childID, object.mtl);
}
scene.add(theModel);
transformControls.attach( theModel );
scene.add( transformControls );
// Remove the loader
LOADER.remove();
}, undefined, function(error) {
console.error(error)
});
// Function - Add the textures to the models
function initColor(parent, type, mtl) {
parent.traverse((o) => {
if (o.isMesh) {
if (o.name.includes(type)) {
o.material = mtl;
o.nameID = type; // Set a new property to identify this object
}
}
});
}
// Add lights
var hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 0.61 );
hemiLight.position.set( 0, 50, 0 );
// Add hemisphere light to scene
scene.add( hemiLight );
var dirLight = new THREE.DirectionalLight( 0xffffff, 0.54 );
dirLight.position.set( -8, 12, 8 );
dirLight.castShadow = true;
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
// Add directional Light to scene
scene.add( dirLight );
// Floor
var floorGeometry = new THREE.PlaneGeometry(5000, 5000, 1, 1);
var floorMaterial = new THREE.MeshPhongMaterial({
color: 0xeeeeee, // This color is manually dialed in to match the background color
shininess: 0
});
var floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -0.5 * Math.PI;
floor.receiveShadow = true;
floor.position.y = -1;
scene.add(floor);
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
if (theModel != null && loaded == false) {
initialRotation();
DRAG_NOTICE.classList.add('start');
}
}
animate();
// Function - New resizing method
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
var width = window.innerWidth;
var height = window.innerHeight;
var canvasPixelWidth = canvas.width / window.devicePixelRatio;
var canvasPixelHeight = canvas.height / window.devicePixelRatio;
const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
// Disable scrolling .. (??)
window.onscroll = function () { window.scrollTo(0, 0); };
// Function - Build Colors
function buildColors(colors) {
for (let [i, color] of colors.entries()) {
let swatch = document.createElement('div');
swatch.classList.add('tray__swatch');
if (color.texture)
{
swatch.style.backgroundImage = "url(" + color.texture + ")";
} else
{
swatch.style.background = "#" + color.color;
}
swatch.setAttribute('data-key', i);
TRAY.append(swatch);
}
}
buildColors(colors);
// Select Option
const options = document.querySelectorAll(".option");
for (const option of options) {
option.addEventListener('click',selectOption);
}
function selectOption(e) {
let option = e.target;
activeOption = e.target.dataset.option;
for (const otherOption of options) {
otherOption.classList.remove('--is-active');
}
option.classList.add('--is-active');
}
// Swatches
const swatches = document.querySelectorAll(".tray__swatch");
for (const swatch of swatches) {
swatch.addEventListener('click', selectSwatch);
}
function selectSwatch(e) {
let color = colors[parseInt(e.target.dataset.key)];
let new_mtl;
if (color.texture) {
let txt = new THREE.TextureLoader().load(color.texture);
txt.repeat.set( color.size[0], color.size[1], color.size[2]);
txt.wrapS = THREE.RepeatWrapping;
txt.wrapT = THREE.RepeatWrapping;
new_mtl = new THREE.MeshPhongMaterial( {
map: txt,
shininess: color.shininess ? color.shininess : 10
});
}
else
{
new_mtl = new THREE.MeshPhongMaterial({
color: parseInt('0x' + color.color),
shininess: color.shininess ? color.shininess : 10
});
}
setMaterial(theModel, activeOption, new_mtl);
}
function setMaterial(parent, type, mtl) {
parent.traverse((o) => {
if (o.isMesh && o.nameID != null) {
if (o.nameID == type) {
o.material = mtl;
}
}
});
}
// Function - Opening rotate
let initRotate = 0;
function initialRotation() {
initRotate++;
if (initRotate <= 120) {
theModel.rotation.y += Math.PI / 60;
} else {
loaded = true;
}
}
var slider = document.getElementById('js-tray'), sliderItems = document.getElementById('js-tray-slide'), difference;
function slide(wrapper, items) {
var posX1 = 0,
posX2 = 0,
posInitial,
threshold = 20,
posFinal,
slides = items.getElementsByClassName('tray__swatch');
// Mouse events
items.onmousedown = dragStart;
// Touch events
items.addEventListener('touchstart', dragStart);
items.addEventListener('touchend', dragEnd);
items.addEventListener('touchmove', dragAction);
function dragStart (e) {
e = e || window.event;
posInitial = items.offsetLeft;
difference = sliderItems.offsetWidth - slider.offsetWidth;
difference = difference * -1;
if (e.type == 'touchstart') {
posX1 = e.touches[0].clientX;
} else {
posX1 = e.clientX;
document.onmouseup = dragEnd;
document.onmousemove = dragAction;
}
}
function dragAction (e) {
e = e || window.event;
if (e.type == 'touchmove') {
posX2 = posX1 - e.touches[0].clientX;
posX1 = e.touches[0].clientX;
} else {
posX2 = posX1 - e.clientX;
posX1 = e.clientX;
}
if (items.offsetLeft - posX2 <= 0 && items.offsetLeft - posX2 >= difference) {
items.style.left = (items.offsetLeft - posX2) + "px";
}
}
function dragEnd (e) {
posFinal = items.offsetLeft;
if (posFinal - posInitial < -threshold) {
} else if (posFinal - posInitial > threshold) {
} else {
items.style.left = (posInitial) + "px";
}
document.onmouseup = null;
document.onmousemove = null;
}
}
slide(slider, sliderItems);
function render() {
renderer.render( scene, camera );
}
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.