“使用 Three.js 的 WebGL 小实验。可交互的3D时钟。”
实现代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
touch-action: none;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background-color: #222;
overflow: hidden;
}
</style>
</head>
<body>
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three@0.140.0";
import { OrbitControls } from "https://cdn.skypack.dev/three@0.120.0/examples/jsm/controls/OrbitControls.js";
import * as dat from "https://cdn.skypack.dev/dat.gui@0.7.9";
class Ring {
constructor(options) {
var _a, _b, _c, _d;
const size = (_a = options.size) !== null && _a !== void 0 ? _a : 2;
const thickness = (_b = options.thickness) !== null && _b !== void 0 ? _b : 0.75;
const depth = (_c = options.depth) !== null && _c !== void 0 ? _c : 0.5;
const extrudeOptions = { depth, bevelEnabled: false, curveSegments: 500 };
const arc = new THREE.Shape();
arc.absarc(0, 0, size, 0, Math.PI * 2, false);
const hole = new THREE.Path();
hole.absarc(0, 0, size - thickness, 0, Math.PI * 2, true);
arc.holes.push(hole);
const geometry = new THREE.ExtrudeGeometry(arc, extrudeOptions);
const material = new THREE.MeshStandardMaterial({
side: THREE.DoubleSide,
metalness: 1,
roughness: 0.15,
wireframe: (_d = options.wireframe) !== null && _d !== void 0 ? _d : false,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
this._mesh = mesh;
}
get mesh() {
return this._mesh;
}
}
class ClockLine {
constructor(options) {
var _a;
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(0, 1);
shape.lineTo(1, 1);
shape.lineTo(1, 0);
shape.lineTo(0, 0);
const geometry = new THREE.ExtrudeBufferGeometry(shape);
const material = new THREE.MeshStandardMaterial({
side: THREE.DoubleSide,
metalness: 1,
roughness: 1,
wireframe: (_a = options.wireframe) !== null && _a !== void 0 ? _a : false,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.scale.set(options.width, options.height, options.depth);
mesh.position.z = options.depth;
mesh.castShadow = true;
mesh.receiveShadow = true;
this._mesh = mesh;
}
get mesh() {
return this._mesh;
}
}
//#endregion
//#region GUI
const gui = new dat.GUI();
//#endregion
//#region Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = false;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setSize(innerWidth, innerHeight);
// renderer.setClearColor("#fff");
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
//#endregion
//#region Camera
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 2000);
camera.position.set(0, 0, 15);
//#endregion
//#region Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
gui.add(controls, "enabled").name("Orbit controls").setValue(false).onChange(controls.reset);
//#endregion
//#region Scene
const scene = new THREE.Scene();
//#endregion
//#region Lights
const lightOne = new THREE.SpotLight(new THREE.Color("#0ff"), 1);
lightOne.position.z = 30;
const lightTwo = new THREE.SpotLight(new THREE.Color("#009795"), 1);
lightTwo.position.set(0, 2, 20);
[lightOne, lightTwo].map((light, i) => {
light.castShadow = true;
light.shadow.mapSize.width = 512;
light.shadow.mapSize.height = 512;
gui.addColor(light, "color").name(`Light ${i + 1} color`);
scene.add(light);
});
//#endregion
//#region Resize handler
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
//#endregion
//#region Mouse positioning
const mousePosition = new THREE.Vector2(0, 0);
function updateMousePosition(e) {
const x = e.clientX - innerWidth * 0.5;
const y = e.clientY - innerHeight * 0.5;
mousePosition.x = x * 0.001;
mousePosition.y = y * 0.001;
}
window.addEventListener("pointermove", updateMousePosition);
//#endregion
//#region Rings
const ringsConfiguration = { amount: 4, innerSpace: 3 };
const ringsThickness = 0.75;
function setRings({ amount, innerSpace }) {
const newRings = [];
for (let size = innerSpace; size <= amount + innerSpace; size++) {
newRings.push(new Ring({ size: size + 1, thickness: ringsThickness, color: "#eee" }).mesh);
}
newRings.forEach((ring) => scene.add(ring));
return newRings;
}
const rings = setRings(ringsConfiguration);
//#endregion
//#region Clock hands
const hoursHand = new ClockLine({ width: 0.075, height: 1, depth: 0.1 }).mesh;
const minutesHand = new ClockLine({ width: 0.075, height: 1.75, depth: 0.1 }).mesh;
const secondsHand = new ClockLine({ width: 0.025, height: 1.5, depth: 0.1 }).mesh;
const clockCenter = new THREE.Mesh(new THREE.SphereGeometry(0.2), new THREE.MeshStandardMaterial({ metalness: 1, roughness: 1 }));
clockCenter.position.z = 0.15;
scene.add(hoursHand, minutesHand, secondsHand, clockCenter);
//#endregion
//#region Initialization
function init() {
// Rings animation
// ------------------------------
rings.forEach((ring, i) => {
ring.rotation.x = ring.rotation.x * 0.95 + mousePosition.y * 0.025 * (i + 0.05);
ring.rotation.y = ring.rotation.y * 0.95 + mousePosition.x * 0.025 * (i + 0.05);
});
// Clock hands rotation
// ------------------------------
const date = new Date();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const milliseconds = date.getMilliseconds();
const smoothSeconds = seconds + milliseconds / 1000;
hoursHand.rotation.z = -THREE.MathUtils.degToRad(0.5 * (60 * hours + minutes));
minutesHand.rotation.z = -THREE.MathUtils.degToRad(6 * minutes);
secondsHand.rotation.z = -THREE.MathUtils.degToRad(6 * smoothSeconds);
// Renderer & controls
// ------------------------------
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(init);
}
init();
//#endregion
</script>
</body>
</html>