src/scene.js
Ready
localhost:5173
Three.js 0.185ReadyJSLn 1, Col 1
Three.js playground with local project files, npm-style imports, interactive WebGL scenes, live browser preview, and a runnable Vite download.
Three.js playground with local project files, npm-style imports, interactive WebGL scenes, live browser preview, and a runnable Vite download.
Runtime: v0.185.1
Press Enter to evaluate, ↑ and ↓ to browse command history, and Ctrl/⌘ + L to clear the console.
import './styles.css'
import { createCrystalScene } from './scene.js'
import { connectSceneControls } from './interaction.js'
const canvas = document.querySelector('#scene')
const status = document.querySelector('#scene-status')
const paletteButton = document.querySelector('#palette-button')
const motionButton = document.querySelector('#motion-button')
if (
!(canvas instanceof HTMLCanvasElement) ||
!(status instanceof HTMLElement) ||
!(paletteButton instanceof HTMLButtonElement) ||
!(motionButton instanceof HTMLButtonElement)
) {
throw new Error('Scene controls were not found')
}
const scene = createCrystalScene(canvas)
const disconnectControls = connectSceneControls({
canvas,
status,
paletteButton,
motionButton,
scene,
})
canvas.dataset.sceneReady = 'true'
window.addEventListener('pagehide', () => {
disconnectControls()
scene.dispose()
}, { once: true })
import * as THREE from 'three'
const PALETTES = [
{ color: 0x8b5cf6, emissive: 0x24114d },
{ color: 0x22d3ee, emissive: 0x073344 },
{ color: 0xfb7185, emissive: 0x4a1020 },
]
export function createCrystalScene(canvas) {
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x080b16)
scene.fog = new THREE.Fog(0x080b16, 7, 13)
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100)
camera.position.set(0, 0.25, 6)
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
powerPreference: 'high-performance',
})
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 1.15
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
const crystalGeometry = new THREE.IcosahedronGeometry(1.25, 2)
const crystalMaterial = new THREE.MeshPhysicalMaterial({
...PALETTES[0],
metalness: 0.35,
roughness: 0.18,
clearcoat: 1,
clearcoatRoughness: 0.12,
})
const crystal = new THREE.Mesh(crystalGeometry, crystalMaterial)
crystal.rotation.set(-0.18, 0.35, 0.08)
scene.add(crystal)
const haloGeometry = new THREE.TorusGeometry(1.82, 0.012, 8, 120)
const haloMaterial = new THREE.MeshBasicMaterial({
color: 0x9ca3ff,
transparent: true,
opacity: 0.6,
})
const halo = new THREE.Mesh(haloGeometry, haloMaterial)
halo.rotation.x = Math.PI * 0.64
scene.add(halo)
scene.add(new THREE.HemisphereLight(0xbec8ff, 0x140b2e, 2.1))
const keyLight = new THREE.PointLight(0x8b5cf6, 42, 12)
keyLight.position.set(3.5, 2.5, 4)
scene.add(keyLight)
const rimLight = new THREE.PointLight(0x22d3ee, 34, 10)
rimLight.position.set(-3.5, -1.5, 2)
scene.add(rimLight)
const starsGeometry = new THREE.BufferGeometry()
const starPositions = new Float32Array(180 * 3)
for (let index = 0; index < starPositions.length; index += 3) {
const radius = 4 + Math.random() * 5
const angle = Math.random() * Math.PI * 2
starPositions[index] = Math.cos(angle) * radius
starPositions[index + 1] = (Math.random() - 0.5) * 7
starPositions[index + 2] = Math.sin(angle) * radius - 2
}
starsGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3))
const starsMaterial = new THREE.PointsMaterial({
color: 0x9ba8d4,
size: 0.025,
transparent: true,
opacity: 0.75,
})
const stars = new THREE.Points(starsGeometry, starsMaterial)
scene.add(stars)
const pointer = new THREE.Vector2()
const targetRotation = new THREE.Vector2()
const raycaster = new THREE.Raycaster()
const clock = new THREE.Clock()
let paletteIndex = 0
let motionPaused = false
let selected = false
const resize = () => {
const width = Math.max(canvas.clientWidth, 1)
const height = Math.max(canvas.clientHeight, 1)
renderer.setSize(width, height, false)
camera.aspect = width / height
camera.updateProjectionMatrix()
}
const resizeObserver = new ResizeObserver(resize)
resizeObserver.observe(canvas)
resize()
renderer.setAnimationLoop(() => {
const elapsed = clock.getElapsedTime()
if (!motionPaused) {
targetRotation.y += 0.002
crystal.position.y = Math.sin(elapsed * 1.1) * 0.1
halo.rotation.z = elapsed * 0.16
}
crystal.rotation.x = THREE.MathUtils.damp(
crystal.rotation.x,
targetRotation.x,
4,
1 / 60,
)
crystal.rotation.y = THREE.MathUtils.damp(
crystal.rotation.y,
targetRotation.y,
4,
1 / 60,
)
crystal.scale.setScalar(
THREE.MathUtils.damp(crystal.scale.x, selected ? 1.08 : 1, 7, 1 / 60),
)
stars.rotation.y = elapsed * 0.018
renderer.render(scene, camera)
})
return {
setPointer(x, y) {
pointer.set(x, y)
targetRotation.x = y * -0.3
targetRotation.y = x * 0.55
},
selectAt(x, y) {
pointer.set(x, y)
raycaster.setFromCamera(pointer, camera)
const hit = raycaster.intersectObject(crystal).length > 0
selected = hit ? !selected : false
return selected
},
shiftPalette() {
paletteIndex = (paletteIndex + 1) % PALETTES.length
crystalMaterial.color.setHex(PALETTES[paletteIndex].color)
crystalMaterial.emissive.setHex(PALETTES[paletteIndex].emissive)
return paletteIndex + 1
},
toggleMotion() {
motionPaused = !motionPaused
return motionPaused
},
dispose() {
renderer.setAnimationLoop(null)
resizeObserver.disconnect()
crystalGeometry.dispose()
crystalMaterial.dispose()
haloGeometry.dispose()
haloMaterial.dispose()
starsGeometry.dispose()
starsMaterial.dispose()
renderer.dispose()
},
}
}