<div class="stage">
<canvas aria-label="Orbit a reflective Motion logo in a studio environment"></canvas>
</div>
<script type="module">
import { animate, frame, motionValue, press } from "motion"
import * as THREE from "three"
import { OrbitControls } from "three/addons/controls/OrbitControls.js"
const canvas = document.querySelector("canvas")
const ORBIT_SPEED = 0.09
const MAX_RELEASE_SPEED = 2.4
function runOrbitMotion(controls) {
/**
* OrbitControls owns all pointer-to-camera work. Motion only supplies the
* automatic yaw velocity before and after a drag.
*/
const orbitYaw = motionValue(controls.getAzimuthalAngle())
const orbitVelocity = motionValue(ORBIT_SPEED)
let previousAzimuth = controls.getAzimuthalAngle()
let unwrappedYaw = previousAzimuth
let velocityAnimation
let dragging = false
function sampleOrbitYaw() {
const azimuth = controls.getAzimuthalAngle()
let change = azimuth - previousAzimuth
if (change > Math.PI) change -= Math.PI * 2
if (change < -Math.PI) change += Math.PI * 2
unwrappedYaw += change
previousAzimuth = azimuth
orbitYaw.set(unwrappedYaw)
}
function transferDragVelocity() {
const releaseVelocity = Math.max(
-MAX_RELEASE_SPEED,
Math.min(MAX_RELEASE_SPEED, orbitYaw.getVelocity())
)
orbitVelocity.set(releaseVelocity)
velocityAnimation = animate(orbitVelocity, ORBIT_SPEED, {
duration: 2.4,
ease: [0.16, 1, 0.3, 1],
})
}
press(canvas, () => {
dragging = true
velocityAnimation?.stop()
velocityAnimation = undefined
sampleOrbitYaw()
return () => {
if (!dragging) return
dragging = false
transferDragVelocity()
}
})
frame.update(({ delta }) => {
const seconds = Math.min(Math.max(0, delta) / 1000, 0.05)
if (!dragging) {
controls.rotateLeft(-orbitVelocity.get() * seconds)
}
controls.update()
sampleOrbitYaw()
}, true)
}
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
})
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 0.8
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(32, 1, 0.1, 100)
camera.position.set(2.4, 1.6, 7)
/**
* This is the same procedural environment used by the vgpu orbit example.
* One shader draws the camera-centred background and a CubeCamera captures
* the same sky once for the logo's reflections.
*/
const skyShader = `
varying vec3 vDirection;
uniform vec3 sunDirection;
uniform float sunAngularSize;
uniform vec3 sunColor;
uniform float sunIntensity;
uniform vec3 zenithColor;
uniform float cloudCoverage;
uniform vec3 horizonColor;
uniform float cloudScale;
uniform vec3 groundColor;
uniform float groundScale;
float hash(vec2 point) {
vec3 q = fract(vec3(point.x, point.y, point.x) * 0.1031);
q += dot(q, q.yzx + 33.33);
return fract((q.x + q.y) * q.z);
}
float valueNoise(vec2 point) {
vec2 cell = floor(point);
vec2 local = fract(point);
vec2 weight = local * local * (3.0 - 2.0 * local);
float a = hash(cell);
float b = hash(cell + vec2(1.0, 0.0));
float c = hash(cell + vec2(0.0, 1.0));
float d = hash(cell + vec2(1.0, 1.0));
return mix(mix(a, b, weight.x), mix(c, d, weight.x), weight.y);
}
float fbm(vec2 point) {
float sum = 0.0;
float amplitude = 0.5;
for (int octave = 0; octave < 5; octave++) {
sum += amplitude * valueNoise(point);
point = point * 2.03 + vec2(17.0, 9.0);
amplitude *= 0.5;
}
return sum;
}
vec2 cloudLayer(vec3 direction, vec3 sun) {
float height = max(direction.y, 0.035);
vec2 plane = direction.xz / height * cloudScale;
float base = fbm(plane);
float detail = fbm(plane * 3.1 + vec2(base * 1.6));
float density = smoothstep(
cloudCoverage,
cloudCoverage + 0.28,
base * 0.75 + detail * 0.35
);
float horizonFade = smoothstep(0.0, 0.12, direction.y);
float lit = pow(
clamp(dot(direction, sun) * 0.5 + 0.5, 0.0, 1.0),
3.0
);
return vec2(density * horizonFade, lit);
}
float checkerBox(vec2 point, vec2 width) {
vec2 pattern =
2.0 *
(
abs(fract((point - 0.5 * width) * 0.5) - 0.5) -
abs(fract((point + 0.5 * width) * 0.5) - 0.5)
) /
width;
return 0.5 - 0.5 * pattern.x * pattern.y;
}
vec3 ground(vec3 direction, vec3 sun) {
float depth = max(-direction.y, 0.001);
vec2 plane = direction.xz / depth * groundScale;
float fade = 1.0 / (1.0 + dot(plane, plane) * 0.006);
vec3 color =
groundColor *
(
1.0 +
checkerBox(plane, fwidth(plane) + vec2(1e-3)) * 3.4
);
color *= 0.8 + fbm(plane * 0.3) * 0.6;
color +=
sunColor *
0.05 *
clamp(
dot(normalize(vec3(plane.x, 0.0, plane.y)), sun),
0.0,
1.0
);
return mix(horizonColor * 0.22, color, fade);
}
void main() {
vec3 direction = normalize(vDirection);
vec3 sun = normalize(sunDirection);
float up = clamp(direction.y, 0.0, 1.0);
vec3 color = mix(
horizonColor,
zenithColor,
pow(up, 0.75)
);
float sunDot = clamp(dot(direction, sun), 0.0, 1.0);
color += sunColor * pow(sunDot, 60.0) * 0.2;
color += sunColor * pow(sunDot, 900.0) * 0.8;
float disk = smoothstep(
cos(sunAngularSize * 2.2),
cos(sunAngularSize),
sunDot
);
color += sunColor * sunIntensity * disk;
vec2 cloud = cloudLayer(direction, sun);
vec3 cloudColor = mix(
vec3(0.30, 0.34, 0.44),
sunColor * 1.05,
cloud.y
);
color = mix(color, cloudColor, cloud.x * (1.0 - disk));
float horizon = smoothstep(-0.14, 0.02, direction.y);
color = mix(ground(direction, sun), color, horizon);
gl_FragColor = vec4(color, 1.0);
#ifdef TONEMAP_SKY
#include <tonemapping_fragment>
#include <colorspace_fragment>
#endif
}
`
const skyVertexShader = `
varying vec3 vDirection;
void main() {
vDirection = position;
gl_Position =
projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
const skyUniforms = {
sunDirection: {
value: new THREE.Vector3(-0.724, 0.09, -0.684).normalize(),
},
sunAngularSize: { value: 0.018 },
sunColor: { value: new THREE.Color().setRGB(1, 0.88, 0.72) },
sunIntensity: { value: 26 },
zenithColor: { value: new THREE.Color().setRGB(0.05, 0.15, 0.44) },
cloudCoverage: { value: 0.56 },
horizonColor: { value: new THREE.Color().setRGB(0.36, 0.48, 0.74) },
cloudScale: { value: 0.75 },
groundColor: { value: new THREE.Color().setRGB(0.05, 0.05, 0.056) },
groundScale: { value: 4.6 },
}
function createSkyMaterial(toneMapped) {
return new THREE.ShaderMaterial({
uniforms: skyUniforms,
vertexShader: skyVertexShader,
fragmentShader: skyShader,
defines: toneMapped ? { TONEMAP_SKY: "" } : {},
side: THREE.BackSide,
depthWrite: false,
toneMapped,
})
}
const skyGeometry = new THREE.SphereGeometry(50, 64, 32)
const sky = new THREE.Mesh(skyGeometry, createSkyMaterial(true))
sky.frustumCulled = false
scene.add(sky)
const environmentScene = new THREE.Scene()
const environmentSky = new THREE.Mesh(
skyGeometry,
createSkyMaterial(false)
)
environmentScene.add(environmentSky)
const environmentTarget = new THREE.WebGLCubeRenderTarget(1024, {
type: THREE.HalfFloatType,
generateMipmaps: true,
minFilter: THREE.LinearMipmapLinearFilter,
})
const environmentCamera = new THREE.CubeCamera(
0.1,
100,
environmentTarget
)
environmentCamera.update(renderer, environmentScene)
const environment = environmentTarget.texture
scene.environment = environment
/**
* The Motion logo as extruded shapes. Coordinates are the SVG path with
* y flipped, then centred and scaled to world units.
*/
function logoShapes() {
const left = new THREE.Shape()
left.moveTo(9.587, 9)
left.lineTo(4.57, 0)
left.lineTo(0, 0)
left.lineTo(3.917, 7.028)
left.bezierCurveTo(4.524, 8.117, 6.039, 9, 7.301, 9)
const middle = new THREE.Shape()
middle.moveTo(10.443, 9)
middle.lineTo(15.013, 9)
middle.lineTo(9.997, 0)
middle.lineTo(5.427, 0)
const right = new THREE.Shape()
right.moveTo(15.841, 9)
right.lineTo(20.411, 9)
right.lineTo(16.494, 1.972)
right.bezierCurveTo(15.887, 0.883, 14.372, 0, 13.11, 0)
right.lineTo(10.825, 0)
const dot = new THREE.Shape()
dot.absarc(23.079, 6.75, 2.285, 0, Math.PI * 2)
return [left, middle, right, dot]
}
const geometry = new THREE.ExtrudeGeometry(logoShapes(), {
depth: 1.6,
bevelEnabled: true,
bevelThickness: 0.3,
bevelSize: 0.3,
bevelSegments: 6,
curveSegments: 24,
})
geometry.center()
geometry.scale(4 / 25.364, 4 / 25.364, 4 / 25.364)
const material = new THREE.MeshPhysicalMaterial({
color: getComputedStyle(document.body)
.getPropertyValue("--white")
.trim(),
metalness: 1,
roughness: 0.055,
envMapIntensity: 1.2,
})
material.color.multiplyScalar(0.56)
const logo = new THREE.Mesh(geometry, material)
scene.add(logo)
const controls = new OrbitControls(camera, canvas)
controls.enablePan = false
controls.minDistance = 4
controls.maxDistance = 12
runOrbitMotion(controls)
function resize() {
const width = canvas.clientWidth
const height = canvas.clientHeight
renderer.setSize(width, height, false)
camera.aspect = width / height
camera.updateProjectionMatrix()
}
new ResizeObserver(resize).observe(canvas)
resize()
frame.render(() => {
sky.position.copy(camera.position)
renderer.render(scene, camera)
}, true)
</script>
<style>
.stage {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
background: var(--black);
}
.stage canvas {
display: block;
width: 100%;
height: 100%;
cursor: grab;
touch-action: none;
}
.stage canvas:active {
cursor: grabbing;
}
</style>