<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>小丑转盘</title>
<style>
@import "https://fonts.googleapis.com/css2?family=Aleo&display=swap";
* {
font-family: Aleo, sans-serif;
font-weight: 400;
margin: 0;
padding: 0;
}
html,
body {
color: #9d988a;
font-size: 0.9em;
overflow: hidden;
}
.webgl {
position: fixed;
top: 0;
left: 0;
outline: none;
}
#container {
background-image: url(https://fecoder-pic-1302080640.cos.ap-nanjing.myqcloud.com/intro-bg.jpg);
background-size: cover;
background-position: center;
width: 100vw;
height: 100vh;
max-width: 100vw;
max-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 101;
position: absolute;
}
#lottie {
height: 40vh;
max-height: 400px;
z-index: 102;
display: flex;
}
#explanation {
display: flex;
width: 66vw;
max-width: 300px;
background-color: #00000040;
padding: 16px;
}
#enter {
width: auto;
}
button {
cursor: pointer;
margin-top: 4px;
padding: 4px;
background-color: #9d988a;
color: #6b2414;
border-radius: 4px;
border: none;
}
</style>
</head>
<body>
<div id="container">
<div id="lottie"></div>
<div id="enter">
<button id="enter" onclick="start()">enter</button>
</div>
</div>
<canvas class="webgl"></canvas>
<script>
let start = () => {
document.getElementById("container").style.display = "none";
window.start3 = true;
anim.stop();
};
</script>
<script type="module">
import * as THREE from "https://esm.sh/three@0.174.0";
import { OrbitControls } from "https://esm.sh/three@0.174.0/addons/controls/OrbitControls.js";
import { GLTFLoader } from "https://esm.sh/three@0.174.0/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from "https://esm.sh/three@0.174.0/examples/jsm/loaders/DRACOLoader";
import Lottie from "https://esm.sh/lottie-web";
window.start3 = false;
let cameraSetup = {
cameraIsSettled: false,
cameraTgt: {
x: 0.11611507465368477,
y: 3.5939784540499456e-16,
z: 5.8682635668005005,
},
totalIterations: 900,
iteration: 0,
leverTgt: 1,
};
let init = () => {
window.addEventListener("touchstart", isDown);
window.addEventListener("mousedown", isDown);
window.addEventListener("mouseup", isUp);
window.addEventListener("touchend", isUp);
window.addEventListener("mousemove", isMove);
window.addEventListener("touchmove", isMove);
initLottie();
};
let anim = null;
let initLottie = () => {
anim = Lottie.loadAnimation({
container: document.getElementById("lottie"),
renderer: "svg",
loop: true,
autoplay: true,
path: "https://fecoder-pic-1302080640.cos.ap-nanjing.myqcloud.com/crnvlintro.json",
});
let loop = () => {
anim.goToAndPlay(120, true);
};
anim.addEventListener("loopComplete", loop);
};
let wheel;
let lever = null;
let hitArea = null;
let pullSign = null;
let ready = false;
let speed = 0;
let inc = 0.02;
let mouse = {
direction: null,
pressing: false,
curY: null,
isClicking: false,
dragStarted: false,
dragDistance: 0,
isSpinning: false,
};
let revealAnim = {
isAnimating: false,
isShowing: false,
};
let signSpinSpeed = 1;
let incSpeed = 12;
let resultSign = null;
let resultAnim = null;
let animAction = null;
let animMixer = null;
let numArr = [];
let deviceType = null;
const canvas = document.querySelector("canvas.webgl");
const scene = new THREE.Scene();
const textureLoader = new THREE.TextureLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("draco/");
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
deviceType = "mobile";
} else {
deviceType = "desktop";
}
const isDown = (e) => {
mouse.pressing = true;
let tgt = "";
if (deviceType == "mobile") {
tgt = e.targetTouches[0];
} else {
tgt = e;
}
mouse.x = tgt.clientX;
mouse.y = tgt.clientY;
pointer.x = (tgt.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(tgt.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObject(hitArea);
if (intersects.length > 0) {
e.preventDefault();
controls.enableRotate = false;
mouse.isClicking = true;
}
};
const isUp = (e) => {
mouse.pressing = false;
mouse.isClicking = false;
mouse.direction = null;
controls.enableRotate = true;
if (mouse.dragStarted) {
mouse.dragStarted = false;
lever.rotation.x = 1;
}
};
const isMove = (e) => {
let tgt = "";
if (deviceType == "mobile") {
tgt = e.targetTouches[0];
} else {
tgt = e;
}
mouse.x = tgt.clientX;
mouse.y = tgt.clientY;
if (mouse.pressing && mouse.isClicking) {
e.preventDefault();
controls.enableRotate = false;
if (mouse.curY == null) {
} else if (mouse.curY > tgt.clientY) {
mouse.direction = "up";
} else if (mouse.curY < tgt.clientY) {
if (mouse.direction != "down") {
mouse.direction = "down";
mouse.dragDistance = 0;
} else {
let dist = tgt.clientY - mouse.curY;
mouse.dragStarted = true;
mouse.dragDistance += dist;
let r = Math.min(2, mouse.dragDistance / 100 + 1);
lever.rotation.x = r;
if (inc < 0.4) {
inc += r / 100;
}
}
}
mouse.curY = tgt.clientY;
} else {
mouse.mouseDirection = null;
mouse.curY = null;
return;
}
};
const sizes = { width: window.innerWidth, height: window.innerHeight };
window.addEventListener("resize", () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
const camera = new THREE.PerspectiveCamera(
35,
sizes.width / sizes.height,
0.1,
100
);
camera.position.set(10, -2.3, -15.4);
scene.add(camera);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.minDistance = 4;
controls.maxDistance = 7;
controls.maxPolarAngle = Math.PI / 2;
controls.maxAzimuthAngle = 0.785;
controls.minAzimuthAngle = -1.5;
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
const bakedTexture = textureLoader.load(
"https://fecoder-pic-1302080640.cos.ap-nanjing.myqcloud.com/baked.jpg"
);
bakedTexture.flipY = false;
bakedTexture.colorSpace = THREE.SRGBColorSpace;
const wheelTexture = textureLoader.load(
"https://fecoder-pic-1302080640.cos.ap-nanjing.myqcloud.com/wheel.jpg"
);
wheelTexture.flipY = false;
wheelTexture.colorSpace = THREE.SRGBColorSpace;
const shadowTexture = textureLoader.load(
"https://fecoder-pic-1302080640.cos.ap-nanjing.myqcloud.com/shadow.webp"
);
shadowTexture.flipY = false;
shadowTexture.colorSpace = THREE.SRGBColorSpace;
const bakedMaterial = new THREE.MeshBasicMaterial({ map: bakedTexture });
const wheelMaterial = new THREE.MeshBasicMaterial({ map: wheelTexture });
const shadowMaterial = new THREE.MeshBasicMaterial({
map: shadowTexture,
transparent: true,
opacity: 0.5,
});
const transpMaterial = new THREE.MeshBasicMaterial({
color: new THREE.Color(0x000000),
transparent: true,
opacity: 0,
});
gltfLoader.load(
"https://fecoder-pic-1302080640.cos.ap-nanjing.myqcloud.com/carnival.glb",
(gltf) => {
gltf.scene.traverse((child) => {
if (child.name == "lever") {
lever = child;
lever.rotation.x = 2;
child.material = bakedMaterial;
} else if (child.name == "hitarea") {
hitArea = child;
child.material = transpMaterial;
} else if (child.name == "wheel") {
child.material = wheelMaterial;
wheel = child;
ready = true;
} else if (child.name == "shadow") {
child.material = shadowMaterial;
} else if (child.name == "sign_swing") {
child.material = bakedMaterial;
pullSign = child;
} else if (child.name == "result_panel") {
child.material = bakedMaterial;
initResultAnimation(child, gltf);
} else if (child.name.includes("num")) {
numArr.push(child);
child.material = bakedMaterial;
child.visible = false;
} else {
child.material = bakedMaterial;
}
});
scene.add(gltf.scene);
}
);
let initResultAnimation = (child, gltf) => {
resultSign = child;
resultAnim = THREE.AnimationClip.findByName(
gltf.animations,
"result_panelAction"
);
animMixer = new THREE.AnimationMixer(resultSign);
animMixer.addEventListener("finished", (e) => {
manageRevealAnim(e);
});
animAction = animMixer.clipAction(resultAnim);
animAction.reset();
animAction.clampWhenFinished = true;
animAction.timeScale = 1;
animAction.setLoop(THREE.LoopOnce, 1);
};
let manageRevealAnim = (e) => {
if (revealAnim.isShowing == true) {
if (!mouse.isSpinning) {
revealAnim.isShowing = false;
}
} else {
if (!mouse.isSpinning) {
revealAnim.isShowing = true;
}
}
revealAnim.isAnimating = false;
animAction.paused = true;
};
const clock = new THREE.Clock();
let showNumber = (answer) => {
let itm = numArr[answer - 1];
numArr.forEach((n) => {
n.visible = false;
});
itm.visible = true;
};
let easeOutCubic = (t, b, c, d) => {
t /= d;
t--;
return c * (t * t * t + 1) + b;
};
let easeInOutCubic = (t, b, c, d) => {
t /= d / 2;
if (t < 1) return (c / 2) * t * t * t + b;
t -= 2;
return (c / 2) * (t * t * t + 2) + b;
};
const tick = () => {
controls.update();
if (ready) {
if (window.start3) {
speed += inc;
if (inc > 0) {
if (!mouse.isSpinning) {
inc += 0.1;
}
wheel.rotation.z = speed;
inc -= 0.001;
mouse.isSpinning = true;
if (revealAnim.isShowing) {
animAction.paused = false;
revealAnim.isAnimating = true;
animAction.setLoop(THREE.LoopOnce);
animAction.timeScale = -1;
animAction.time = 0.5;
animAction.play();
revealAnim.isShowing = false;
} else {
}
} else {
let rad = (wheel.rotation.z * (180 / Math.PI)) % 360;
let answer = Math.floor(1 + rad / 36);
if (mouse.isSpinning == true) {
showNumber(answer);
mouse.isSpinning = false;
animAction.paused = false;
revealAnim.isAnimating = true;
animAction.setLoop(THREE.LoopOnce);
animAction.timeScale = 1;
animAction.play();
}
}
if (incSpeed > 0) {
if (pullSign != null) {
pullSign.rotation.z = Math.sin(signSpinSpeed) / 3;
signSpinSpeed += incSpeed / 100;
incSpeed -= 0.068;
}
}
if (!cameraSetup.cameraIsSettled) {
cameraSetup.iteration++;
if (cameraSetup.iteration < 100) {
let xx = easeOutCubic(
cameraSetup.iteration,
camera.position.x,
cameraSetup.cameraTgt.x - camera.position.x,
cameraSetup.totalIterations
);
let yy = easeOutCubic(
cameraSetup.iteration,
camera.position.y,
cameraSetup.cameraTgt.y - camera.position.y,
cameraSetup.totalIterations
);
let zz = easeOutCubic(
cameraSetup.iteration,
camera.position.z,
cameraSetup.cameraTgt.z - camera.position.z,
cameraSetup.totalIterations
);
camera.position.set(xx, yy, zz);
let leverR = easeInOutCubic(
cameraSetup.iteration,
lever.rotation.x,
cameraSetup.leverTgt - lever.rotation.x,
80
);
lever.rotation.x = leverR;
} else {
cameraSetup.cameraIsSettled = true;
}
}
}
}
const delta = clock.getDelta();
if (animMixer) animMixer.update(delta);
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
init();
tick();
window.camera = camera;
window.controls = controls;
window.anim = anim;
</script>
</body>
</html>