A Pen by HARUN PEHLİVAN on CodePen.
Created
July 10, 2021 11:26
-
-
Save harunpehlivan/964cf652cca4c8e05006f234af8a4512 to your computer and use it in GitHub Desktop.
Beat Burger
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
<div id="root"></div> |
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
console.clear(); | |
import React, { useEffect, useRef, useState } from 'https://cdn.skypack.dev/react@17'; | |
import ReactDOM from 'https://cdn.skypack.dev/react-dom@17'; | |
import gsap from "https://cdn.skypack.dev/[email protected]"; | |
import { MotionPathPlugin } from "https://cdn.skypack.dev/[email protected]/MotionPathPlugin"; | |
import * as THREE from "https://cdn.skypack.dev/[email protected]"; | |
import { GLTFLoader } from 'https://cdn.skypack.dev/[email protected]/examples/jsm/loaders/GLTFLoader.js' | |
const FILES = { | |
drum: "https://assets.codepen.io/557388/drums.glb", | |
burger: "https://assets.codepen.io/557388/burger.glb", | |
snare: "https://res.cloudinary.com/tercuman-b-l-m-merkez/video/upload/v1625915365/snare_gif5zd.mp3", | |
bass: "https://res.cloudinary.com/tercuman-b-l-m-merkez/video/upload/v1625915462/bass_duijtn.mp3", | |
tom1: "https://res.cloudinary.com/tercuman-b-l-m-merkez/video/upload/v1625915501/tom-1_k5hgzb.mp3", | |
tom2: "https://res.cloudinary.com/tercuman-b-l-m-merkez/video/upload/v1625915561/tom-2_iapm3o.mp3", | |
tom3: "https://res.cloudinary.com/tercuman-b-l-m-merkez/video/upload/v1625915617/tom-3_ue9wxl.mp3", | |
cymbal1: "https://res.cloudinary.com/tercuman-b-l-m-merkez/video/upload/v1625915674/cymbal-1_ap5c05.mp3", | |
cymbal2: "https://res.cloudinary.com/tercuman-b-l-m-merkez/video/upload/v1625915718/cymbal-2_ir5aw3.mp3" | |
} | |
gsap.registerPlugin(MotionPathPlugin); | |
function App() { | |
return ( | |
<div className="App"> | |
<BurgerDrum /> | |
<a href="http://harunpehlivan.fm.tc/" target="_blank" className="component-carousel"> | |
<span>HARUN PEHLİVAN FOUNDER CEO</span> | |
<img src="https://res.cloudinary.com/tercuman-b-l-m-merkez/image/upload/v1624991448/210313392_2960210997593940_3313825535339149962_n_ix59fg.jpg" alt="Component Carousel" /> | |
</a> | |
</div> | |
); | |
} | |
function BurgerDrum() { | |
const mount = useRef(); | |
const [view, setView] = useState('loading'); | |
const [manager, setManager] = useState(null); | |
const init = () => { | |
if(mount.current) | |
{ | |
const stage = new Stage(mount.current) | |
const _manager = new Manager(stage, view, setView); | |
setManager(_manager); | |
return () => { | |
stage.destroy(); | |
_manager.fire(); | |
} | |
} | |
} | |
const toggleView = () => { setView(view === 'burger' ? 'drums' : 'burger') } | |
useEffect(init, [mount]) | |
useEffect(() => { if(manager) manager.updateView(view) }, [view, manager]) | |
return ( | |
<div className={`burger-drum ${view}`}> | |
<div className="container" ref={mount}></div> | |
<div className="info"> | |
<p className="presents">Buns N' Roses presents:</p> | |
<h1>Beat Burger</h1> | |
<p>Our signature burger, inspired by legendary drummer <i>[your favorite drummer here]</i>. Order online now <i>(or don't because this is all pretend)</i> or transform this burger into a drum kit and play some sweet beats!</p> | |
<div className="buttons"> | |
<button onClick={(toggleView)}>Play</button> | |
</div> | |
</div> | |
<div className="controls"> | |
<button onClick={(toggleView)}>Back</button> | |
<div>Tap the drums or use these keyboard keys: <span>T</span><span>T</span><span>G</span><span>R</span><span>T</span></div></div> | |
<div className="loader">🤘 Loading 🤘</div> | |
</div> | |
) | |
} | |
class Manager { | |
constructor(stage, view, setView) { | |
this.stage = stage; | |
this.setView = setView; | |
this.debug = false; | |
this.sounds = {}; | |
this.raycaster = new THREE.Raycaster(); | |
this.view = view; | |
this.models = { | |
burger: { | |
file: FILES.burger, | |
items: {} | |
}, | |
drumkit: { | |
file: FILES.drum, | |
items: {} | |
} | |
} | |
this.setupSpotLights(); | |
this.loadModels(); | |
} | |
setupSpotLights() { | |
this.spotlights = { | |
left: { light: new THREE.SpotLight('white', 0), target: new THREE.Object3D() }, | |
right: { light: new THREE.SpotLight('white', 0), target: new THREE.Object3D() } | |
} | |
const sides = ['left', 'right']; | |
sides.forEach(side => { | |
const spotLight = this.spotlights[side].light; | |
const target = this.spotlights[side].target; | |
spotLight.penumbra = 0.1; | |
spotLight.angle = 0.6; | |
spotLight.castShadow = true; | |
spotLight.shadow.mapSize.width = 1024; | |
spotLight.shadow.mapSize.height = 1024; | |
spotLight.shadow.camera.near = 1; | |
spotLight.shadow.camera.far = 10; | |
spotLight.shadow.camera.fov = 50; | |
spotLight.target = target; | |
this.stage.add(spotLight) | |
this.stage.add(target) | |
}) | |
this.spotlights.left.light.position.set(-3, 5, 1) | |
this.spotlights.right.light.position.set(3, 5, 1) | |
} | |
playSound(id) { | |
const sound = this.sounds[id]; | |
if(this.view === 'drums' && sound) | |
{ | |
sound.audio.currentTime = 0; | |
sound.audio.play() | |
gsap.fromTo(sound.object.position, {...sound.from}, {...sound.to, ease: 'elastic' }) | |
} | |
} | |
setupSounds() { | |
const testObjects = []; | |
for (const [name, drum] of Object.entries(drumSettings)) { | |
if(drum.sound){ | |
const sound = { | |
audio: new Audio(drum.sound), | |
object: this.models.burger.items[name], | |
from: {[drum.direction] : drum.position[drum.direction] - 0.3}, | |
to: {[drum.direction] : drum.position[drum.direction]}, | |
} | |
// test doesn't work on groups, so need to add children and rename | |
if(sound.object instanceof THREE.Mesh) | |
{ | |
testObjects.push(sound.object) | |
} | |
else { | |
sound.object.children.forEach(obj => { | |
obj.name = sound.object.name; | |
testObjects.push(obj) | |
}) | |
} | |
this.sounds[drum.key] = sound; | |
this.sounds[name] = sound; | |
} | |
} | |
document.addEventListener("keydown", (event) => { this.playSound(event.key)}) | |
this.stage.container.addEventListener('click', (event) => | |
{ | |
const mouse = { | |
x: event.offsetX / this.stage.size.width * 2 - 1, | |
y: - (event.offsetY / this.stage.size.height) * 2 + 1 | |
} | |
this.raycaster.setFromCamera(mouse, this.stage.camera); | |
const intersects = this.raycaster.intersectObjects(testObjects); | |
if(intersects.length) { | |
this.playSound(intersects[0].object.name) | |
} | |
}) | |
} | |
loadModels() { | |
const loadingManager = new THREE.LoadingManager(() => { | |
this.setupSounds(); | |
this.setView('burger'); | |
}) | |
const gltfLoader = new GLTFLoader(loadingManager) | |
Object.keys(this.models).forEach(id => { | |
const model = this.models[id]; | |
gltfLoader.load( | |
model.file, | |
(gltf) => | |
{ | |
gltf.scene.traverse(child => { | |
if(child instanceof THREE.Mesh) | |
{ | |
child.receiveShadow = true; | |
child.castShadow = true; | |
} | |
}) | |
const children = [ ...gltf.scene.children ] | |
children.forEach(child => { | |
model.items[child.name] = child; | |
child.home = { | |
position: {...child.position}, | |
rotation: {x: child.rotation.x, y: child.rotation.y, z: child.rotation.z}, | |
scale: {...child.scale}, | |
} | |
child.position.y *= 2; | |
child.visible = false; | |
this.stage.add(child) | |
}) | |
} | |
) | |
}) | |
} | |
moveToDrums() { | |
gsap.to(this.stage.camera.position, { x: 0, y: 6, z: 6 }) | |
gsap.to(this.stage.lookAt, { x: 0, y: 1, z: -1 }) | |
gsap.to(this.stage, { light: 0 }) | |
gsap.to(this.spotlights.left.target.position, {x: -1, z: -1}) | |
gsap.to(this.spotlights.right.target.position, {x: 1, z: -1}) | |
gsap.to(this.spotlights.left.light, {intensity: 10, delay: 0.3}) | |
gsap.to(this.spotlights.right.light, {intensity: 10, delay: 0.3}) | |
Object.keys(this.models.burger.items).forEach(key => { | |
const item = this.models.burger.items[key]; | |
const pos = drumSettings[item.name]; | |
const delay = 0.6 - item.home.position.y * 0.6; | |
gsap.to(item.position, {motionPath: [{x: pos.position.x , y: pos.position.y + 0.5 , z: pos.position.z}, {...pos.position}], delay, duration: 1, ease: 'power2.inOut'}) | |
gsap.to(item.rotation, {...pos.rotation, duration: 3, delay: delay + 0.5, ease: 'elastic'}) | |
gsap.to(item.scale, {...pos.scale, duration: 1, delay: delay, ease: 'power2.inOut'}) | |
}) | |
Object.keys(this.models.drumkit.items).forEach(key => { | |
const item = this.models.drumkit.items[key]; | |
item.visible = true; | |
gsap.to(item.position, {...item.home.position, duration: 1, ease: 'power4.out'}) | |
gsap.to(item.rotation, {...item.home.rotation, duration: 1, ease: 'power4.out'}) | |
gsap.to(item.scale, {...item.home.scale, duration: 1, ease: 'power4.out'}) | |
}) | |
} | |
moveToBurger() { | |
gsap.to(this.stage.camera.position, {...this.stage.camera.home.position, duration: 0.6}) | |
gsap.to(this.stage.lookAt, { x: -1.5, y: 1, z: 0, duration: 0.6 }) | |
gsap.to(this.stage, { light: 2 }) | |
gsap.to(this.spotlights.left.target.position, {x: -10, y: 1}) | |
gsap.to(this.spotlights.right.target.position, {x: 10, y: 1}) | |
gsap.to(this.spotlights.left.light, {intensity: 0}) | |
gsap.to(this.spotlights.right.light, {intensity: 0}) | |
Object.keys(this.models.burger.items).forEach(key => { | |
const item = this.models.burger.items[key]; | |
const delay = 0.2 + item.home.position.y * 0.4; | |
item.visible = true; | |
gsap.to(item.position, {motionPath: [{x: item.home.position.x , y: item.home.position.y * 3, z: item.home.position.z}, {...item.home.position}], delay, duration: 1.5, ease: 'bounce'}) | |
gsap.to(item.rotation, {...item.home.rotation, duration: 1, delay, ease: 'power4.inOut'}) | |
gsap.to(item.scale, {...item.home.scale, duration: 1, delay, ease: 'power4.inOut'}) | |
}) | |
Object.keys(this.models.drumkit.items).forEach(key => { | |
const item = this.models.drumkit.items[key]; | |
gsap.to(item.position, {y: -3, duration: 0.5}) | |
gsap.to(item.rotation, {z: (Math.random() * 0.5) - 0.25, duration: 0.5}) | |
}) | |
} | |
updateView(newState) { | |
this.view = newState; | |
gsap.globalTimeline.clear(); | |
if(newState === 'burger') this.moveToBurger(); | |
if(newState === 'drums') this.moveToDrums(); | |
} | |
fire() { | |
this.gui.destroy() | |
} | |
} | |
const COLORS = { | |
background: '#142522', | |
floor: '#522142' | |
} | |
class Stage { | |
constructor(mount) { | |
this.container = mount; | |
this.scene = new THREE.Scene(); | |
this.scene.background = new THREE.Color( COLORS.background ); | |
this.size = { | |
width: 1, | |
height: 1 | |
} | |
this.setupLights(); | |
this.setupCamera(); | |
this.setupFloor(); | |
this.setupFog(); | |
this.setupRenderer(); | |
this.onResize(); | |
window.addEventListener('resize', () => this.onResize()); | |
this.tick(); | |
} | |
setupLights() { | |
this.directionalLight = new THREE.DirectionalLight('#ffffff', 2); | |
this.directionalLight.castShadow = true; | |
this.directionalLight.shadow.camera.far = 10; | |
this.directionalLight.shadow.mapSize.set(1024, 1024); | |
this.directionalLight.shadow.normalBias = 0.05; | |
this.directionalLight.position.set(2, 4, 1); | |
this.add(this.directionalLight); | |
const hemisphereLight = new THREE.HemisphereLight( 0xffffff, COLORS.floor, 0.5 ); | |
this.add(hemisphereLight) | |
} | |
setupCamera() { | |
this.lookAt = new THREE.Vector3(2, 1, 0); | |
this.camera = new THREE.PerspectiveCamera(40, this.size.width / this.size.height, 0.1, 100); | |
this.camera.position.set(0, 3, 6); | |
this.camera.home = { | |
position: { ...this.camera.position } | |
} | |
this.add(this.camera); | |
} | |
setupFloor() { | |
const plane = new THREE.PlaneGeometry(100, 100); | |
const floorMaterial = new THREE.MeshStandardMaterial({ color: COLORS.floor }) | |
const floor = new THREE.Mesh(plane, floorMaterial); | |
floor.receiveShadow = true; | |
floor.rotateX(-Math.PI * 0.5) | |
this.add(floor); | |
} | |
setupFog() { | |
const fog = new THREE.Fog(COLORS.background, 6, 20) | |
this.scene.fog = fog; | |
} | |
setupRenderer() { | |
this.renderer = new THREE.WebGLRenderer({ | |
canvas: this.canvas, | |
antialias: true, | |
}) | |
this.renderer.physicallyCorrectLights = true; | |
this.renderer.outputEncoding = THREE.sRGBEncoding; | |
this.renderer.toneMapping = THREE.ReinhardToneMapping; | |
this.renderer.toneMappingExposure = 3; | |
this.renderer.shadowMap.enabled = true; | |
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
this.container.appendChild( this.renderer.domElement ); | |
} | |
onResize() { | |
this.size.width = this.container.clientWidth; | |
this.size.height = this.container.clientHeight; | |
this.camera.aspect = this.size.width / this.size.height | |
this.camera.updateProjectionMatrix() | |
this.renderer.setSize(this.size.width, this.size.height) | |
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) | |
} | |
tick() { | |
this.camera.lookAt(this.lookAt); | |
this.renderer.render(this.scene, this.camera); | |
window.requestAnimationFrame(() => this.tick()) | |
} | |
add(element) { this.scene.add(element);} | |
set light(value) { | |
this.directionalLight.intensity = value; | |
} | |
get light() { | |
return this.directionalLight.intensity; | |
} | |
destroy() { | |
this.container.removeChild( this.renderer.domElement); | |
window.removeEventListener('resize', this.onResize); | |
} | |
} | |
const drumSettings = { | |
"bun-bottom": { | |
"sound": FILES.snare, | |
"key": "q", | |
"direction": "y", | |
"position": { | |
"x": -1.6, | |
"y": 1.55, | |
"z": 0 | |
}, | |
"rotation": { | |
"x": 0, | |
"y": 0.67, | |
"z": 0 | |
}, | |
"scale": { | |
"x": 0.6000000000000001, | |
"y": 0.6000000000000001, | |
"z": 0.6000000000000001 | |
} | |
}, | |
"bun-top": { | |
"sound": FILES.bass, | |
"key": "w", | |
"direction": "z", | |
"position": { | |
"x": 0, | |
"y": 0.9, | |
"z": -0.5 | |
}, | |
"rotation": { | |
"x": -1.9000000000000001, | |
"y": 1.7000000000000002, | |
"z": 0 | |
}, | |
"scale": { | |
"x": 0.8, | |
"y": 0.8, | |
"z": 0.8 | |
} | |
}, | |
"tomato-1": { | |
"position": { | |
"x": 1.6, | |
"y": 1, | |
"z": 0.2 | |
}, | |
"rotation": { | |
"x": -3.141592653589793, | |
"y": -5, | |
"z": 0 | |
}, | |
"scale": { | |
"x": 0.7000000000000001, | |
"y": 0.7000000000000001, | |
"z": 0.7000000000000001 | |
} | |
}, | |
"patty-1": { | |
"sound": FILES.tom1, | |
"key": "e", | |
"direction": "y", | |
"position": { | |
"x": -0.5, | |
"y": 2.1, | |
"z": -0.1 | |
}, | |
"rotation": { | |
"x": 0, | |
"y": 3.1, | |
"z": 0 | |
}, | |
"scale": { | |
"x": 0.5, | |
"y": 0.5, | |
"z": 0.5 | |
} | |
}, | |
"lettuce": { | |
"sound": FILES.tom3, | |
"key": "r", | |
"direction": "y", | |
"position": { | |
"x": 1.6, | |
"y": 1.4000000000000001, | |
"z": 0.2 | |
}, | |
"rotation": { | |
"x": 0, | |
"y": 0, | |
"z": 0 | |
}, | |
"scale": { | |
"x": 0.6000000000000001, | |
"y": 0.6000000000000001, | |
"z": 0.6000000000000001 | |
} | |
}, | |
"tomato-2": { | |
"position": { | |
"x": 1.5, | |
"y": 1.2000000000000002, | |
"z": 0.30000000000000004 | |
}, | |
"rotation": { | |
"x": -3.141592653589793, | |
"y": 0.39626465267840905, | |
"z": 0 | |
}, | |
"scale": { | |
"x": 0.7000000000000001, | |
"y": 0.7000000000000001, | |
"z": 0.7000000000000001 | |
} | |
}, | |
"cheese-2": { | |
"sound": FILES.cymbal1, | |
"key": "t", | |
"direction": "y", | |
"position": { | |
"x": -0.98, | |
"y": 2.43, | |
"z": -0.5 | |
}, | |
"rotation": { | |
"x": 0.01, | |
"y": -1.6500000000000001, | |
"z": 0.01 | |
}, | |
"scale": { | |
"x": 0.7000000000000001, | |
"y": 0.7000000000000001, | |
"z": 0.7000000000000001 | |
} | |
}, | |
"patty-2": { | |
"sound": FILES.tom2, | |
"key": "y", | |
"direction": "y", | |
"position": { | |
"x": 0.6000000000000001, | |
"y": 2.1, | |
"z": -0.1 | |
}, | |
"rotation": { | |
"x": 0, | |
"y": -4.18, | |
"z": 0 | |
}, | |
"scale": { | |
"x": 0.5, | |
"y": 0.5, | |
"z": 0.5 | |
} | |
}, | |
"cheese-1": { | |
"sound": FILES.cymbal2, | |
"key": "u", | |
"direction": "y", | |
"position": { | |
"x": 1, | |
"y": 2.85, | |
"z": -0.4 | |
}, | |
"rotation": { | |
"x": -3.141592653589793, | |
"y": -1.32, | |
"z": -3.141592653589793 | |
}, | |
"scale": { | |
"x": 0.6, | |
"y": 0.6, | |
"z": 0.6 | |
} | |
} | |
} | |
ReactDOM.render( | |
<React.StrictMode> | |
<App /> | |
</React.StrictMode>, | |
document.getElementById('root') | |
); |
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
.App { | |
text-align: center; | |
display: flex; | |
width: 100%; | |
height: 100%; | |
} | |
.component-carousel { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
flex-direction: column; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: white; | |
text-decoration: none; | |
transition: transform 0.2s ease-in-out; | |
} | |
.component-carousel img { | |
width: 187px; | |
margin-top: 10px; | |
} | |
.component-carousel:hover { | |
text-decoration: underline; | |
} | |
.component-carousel:hover img { | |
transform: translate(2px, 2px) ; | |
} | |
@import url('https://fonts.googleapis.com/css2?family=Metal+Mania&display=swap'); | |
html, body { | |
width: 100%; | |
height: 100%; | |
overflow: hidden; | |
margin: 0; | |
padding: 0; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: white; | |
background-color: #142522; | |
font-family: sans-serif; | |
} | |
.burger-drum { | |
overflow: hidden; | |
position: relative; | |
} | |
.burger-drum .container { | |
width: 100vw; | |
max-width: 1800px; | |
height: 60vw; | |
min-height: 400px; | |
max-height: 100vh; | |
} | |
.info { | |
position: absolute; | |
bottom: 40px; | |
left: 40px; | |
width: 40vw; | |
max-width: 500px; | |
display: flex; | |
justify-content: center; | |
align-items: flex-start; | |
flex-direction: column; | |
transform: translateX(-50vw); | |
transition: transform 0.5s ease-in-out; | |
} | |
.presents { | |
margin-bottom: 20px; | |
} | |
.burger .info { | |
transform: translateX(0); | |
} | |
h1 { | |
font-family: 'Metal Mania', cursive; | |
font-size: clamp(40px, 6vw, 100px); | |
letter-spacing: 5px; | |
margin: 0; | |
white-space: nowrap; | |
} | |
p { | |
text-align: left; | |
font-size: 20px; | |
margin-bottom: 40px; | |
/* letter-spacing: 0.03em; */ | |
line-height: 130%; | |
} | |
.buttons { | |
} | |
button { | |
margin-right: 40px; | |
background-color: rgba(0, 0, 0, 0.3); | |
padding: 10px 30px; | |
border: solid 1px white; | |
color: white; | |
font-size: 30px; | |
/* text-transform: uppercase; */ | |
letter-spacing: 3px; | |
font-family: 'Metal Mania', cursive; | |
cursor: pointer; | |
outline: solid 1px transparent; | |
outline-offset: 0px; | |
transition-property: background-color, outline-offset, outline; | |
transition-duration: 0.1s; | |
transition-timing-function: ease-in-out; | |
} | |
button:hover:not(:disabled) { | |
background-color: darkred; | |
outline: solid 1px white; | |
outline-offset: 2px; | |
} | |
button:disabled { | |
opacity: 0.3; | |
} | |
.controls { | |
position: absolute; | |
bottom: 40px; | |
right: 40px; | |
width: calc(100% - 80px); | |
display: flex; | |
justify-content: space-between; | |
align-items: flex-end; | |
transform: translateY(200%); | |
transition: transform 0.5s ease-in-out; | |
} | |
.drums .controls { | |
transform: translateY(0%); | |
} | |
.loader { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
font-size: 50px; | |
font-family: 'Metal Mania', cursive; | |
transform: translate(-50%, -50%); | |
pointer-events: none; | |
opacity: 0; | |
transition: opacity 0.2s ease-in-out; | |
} | |
.loading .loader { | |
opacity:10; | |
} | |
.controls span { | |
text-transform: uppercase; | |
padding: 2px; | |
display: inline-block; | |
border: 2px solid white; | |
border-bottom: 4px solid white; | |
border-radius: 6px; | |
width: 20px; | |
height: 20px; | |
margin-left: 10px | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment