<canvas id="c"></canvas>
<div class="controls">
<div id="js-tray" class="tray">
<div id="js-tray-slide" class="tray__slide"></div>
</div>
</div>
<span class="drag-notice" id="js-drag-notice">Drag to rotate 360°</span>
@import url('https://fonts.googleapis.com/css?family=Raleway:500,700&display=swap');
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% !important;
height: 100% !important;
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;
}
}
/*
Adapt from this pen.
https://codepen.io/kylewetton/pen/jONpxpa
*/
const bgColor = 0xf1f1f1;
const elements = ['fence', 'chimney', 'support'];
let loaded = false;
let initRotate = 0;
const originMaterials = {
fence: null,
house: null,
chimney: null
};
const colors = [
{
texture: 'https://assets.elecwebmaker.com/texture/paving.jpg',
size: [2, 2, 2]
},
{
texture: 'https://assets.elecwebmaker.com/texture/marble.jpg',
size: [2, 2, 2]
},
{
texture: 'https://assets.elecwebmaker.com/texture/metal.jpg',
size: [2, 2, 2]
},
{
texture: 'https://assets.elecwebmaker.com/texture/tiles.jpg',
size: [2, 2, 2]
},
{
color: '66533C'
},
{
color: '173A2F'
},
{
color: '153944'
},
{
color: '27548D'
},
{
color: '438AAC'
}
];
const tray = document.getElementById('js-tray-slide');
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);
const setUpRenderer = () => {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
});
renderer.shadowMap.enabled = true;
const pixelRatio = window.devicePixelRatio;
renderer.setSize(window.innerWidth * pixelRatio, window.innerHeight * pixelRatio);
return {renderer, canvas};
};
const setUpCamera = () => {
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
const cameraFar = 5;
camera.position.z = cameraFar;
camera.position.x = 4;
camera.position.y = 2.5;
return camera;
};
const setUpScene = () => {
const scene = new THREE.Scene();
scene.background = new THREE.Color(bgColor);
scene.fog = new THREE.Fog(bgColor, 20, 100);
return scene;
};
const setUpLight = () => {
const light = new THREE.Object3D();
const hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.61);
hemiLight.position.set(0, 50, 0);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.54);
dirLight.position.set(-8, 12, 8);
dirLight.castShadow = true;
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
light.add(hemiLight);
light.add(dirLight);
return light;
};
const loadModel = () => {
const modelPath = 'https://assets.elecwebmaker.com/house/scene.gltf?q=' + new Date();
const loader = new THREE.GLTFLoader();
return new Promise((resolve, reject) => {
loader.load(modelPath, (gltf) => {
gltf.scene.scale.set(.3, .3, .3);
gltf.scene.translateY(-1);
const ground = gltf.scene.getObjectByName('ground').children[0];
const fence = gltf.scene.getObjectByName('fence').children[0];
const house = gltf.scene.getObjectByName('house').children[0];
const chimney = gltf.scene.getObjectByName('chimney').children[0];
elements.forEach((elementName) => {
const element = gltf.scene.getObjectByName(elementName).children[0];
originMaterials[elementName] = element.material;
element.material = originMaterials[elementName].clone();
});
const threes = gltf.scene.getObjectByName('Trees_GRP');
fence.castShadow = true;
threes.children.forEach(treeGroup => {
const treeMesh = treeGroup.children[0];
treeMesh.castShadow = true;
});
house.castShadow = true;
ground.receiveShadow = true;
resolve({
model: gltf.scene,
parts: {
fence,
house
}
});
}, undefined, reject);
});
};
const updateTexture = (activeElement, textureUrl, sizes) => {
const texture = new THREE.TextureLoader().load(textureUrl + '?q=' + new Date());
texture.repeat.set(sizes[0], sizes[1], sizes[2]);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
activeElement.material.map = texture;
activeElement.material.color.set('#ffffff');
};
const updateColor = (activeElement, color) => {
if (activeElement.parent.name === elements[0]) {
if (color.texture) {
updateTexture(activeElement, color.texture, color.size);
} else {
activeElement.material.color.set(parseInt('0x' + color.color));
}
} else if (activeElement.parent.name === elements[1]) {
if (color.texture) {
updateTexture(activeElement, color.texture, color.size);
} else {
activeElement.material.map = null;
activeElement.material.color.set(parseInt('0x' + color.color));
}
}else {
if (color.texture) {
updateTexture(activeElement, color.texture, color.size);
} else {
activeElement.material.map = null;
activeElement.material.color.set(parseInt('0x' + color.color));
}
}
};
const main = async () => {
const scene = setUpScene();
const {renderer, canvas} = setUpRenderer();
const camera = setUpCamera();
const cameraControls = new THREE.OrbitControls(camera, renderer.domElement);
cameraControls.maxPolarAngle = Math.PI / 2;
cameraControls.minPolarAngle = Math.PI / 3;
cameraControls.enableDamping = true;
cameraControls.dampingFactor = .1;
cameraControls.enablePan = false;
cameraControls.autoRotate = true;
cameraControls.autoRotateSpeed = 0.2;
const {model, parts } = await loadModel();
scene.add(model);
const light = setUpLight();
scene.add(light);
const swatches = document.querySelectorAll(".tray__swatch");
const traySlide = document.querySelector('.tray__slide');
let activeElement;
let hoverElement;
const dragNotice = document.getElementById('js-drag-notice');
const selectSwatch = (e) => {
const color = colors[parseInt(e.target.dataset.key)];
if (activeElement) {
updateColor(activeElement, color);
originMaterials[activeElement.parent.name] = activeElement.material;
}
};
for (const swatch of swatches) {
swatch.addEventListener('click', (event) => selectSwatch(event));
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
const pixelRatio = window.devicePixelRatio;
renderer.setSize(window.innerWidth * pixelRatio, window.innerHeight * pixelRatio);
});
const rayCaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
});
canvas.addEventListener('mousedown', (event) => {
activeElement = hoverElement;
});
const initialRotation = () => {
initRotate++;
if (initRotate <= 120) {
model.rotation.y += Math.PI / 60;
} else {
loaded = true;
}
};
const animate = () => {
renderer.render(scene, camera);
cameraControls.update();
rayCaster.setFromCamera(mouse, camera);
const intersects = rayCaster.intersectObjects(scene.children, true);
elements.forEach((elementName) => {
const elementMesh = scene.getObjectByName(elementName).children[0];
elementMesh.material = originMaterials[elementName].clone();
});
if(intersects.length > 0) {
// console.log('intersects[0].object;', intersects[0].object.parent);
}
if(intersects.length > 0 && elements.includes(intersects[0].object.parent.name)) {
intersects[0].object.material.color.set('#00ff00');
hoverElement = intersects[0].object;
} else {
hoverElement = null;
};
if (model != null && loaded == false) {
initialRotation();
dragNotice.classList.add('start');
}
if (activeElement) {
traySlide.style.border = '2px solid #f47a7a';
} else {
traySlide.style.border = '2px solid transparent';
}
requestAnimationFrame(animate);
};
animate();
};
main();
View Compiled
This Pen doesn't use any external CSS resources.