awesome-clean-tool / index.html
Tingchenliang's picture
Upload index.html with huggingface_hub
921e409 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<title>Three.js 云霄飞车动画</title>
<style>
html, body {
margin: 0;
height: 100%;
background: #ffeef6; /* 页面背景与场景淡粉色一致 */
overflow: hidden;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",sans-serif;
}
#container {
width: 100%;
height: 100%;
}
.hud {
position: fixed;
top: 10px;
left: 10px;
padding: 8px 12px;
background: rgba(0,0,0,0.5);
color: #fff;
border-radius: 6px;
font-size: 14px;
z-index: 10;
user-select: none;
}
.hud a {
color: #7fd4ff;
text-decoration: none;
}
.hud .hint {
opacity: 0.8;
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
}
</style>
</head>
<body>
<div id="container"></div>
<div class="hud">
<div><strong>Three.js 云霄飞车</strong></div>
<div class="hint">
模式:<span id="mode">第三人称</span><br/>
快捷键:V 切换视角<br/>
鼠标左键:旋转 | 右键:平移 | 滚轮:缩放
</div>
<div style="margin-top:6px; opacity:0.85;">
Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener">anycoder</a>
</div>
</div>
<!-- Three.js 和 OrbitControls (CDN) -->
<script src="https://unpkg.com/[email protected]/build/three.min.js"></script>
<script src="https://unpkg.com/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script>
// ===== 基础场景 =====
const container = document.getElementById('container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffeef6); // 淡粉色背景
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(80, 45, 80);
camera.lookAt(0, 10, 0);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.maxDistance = 200;
controls.minDistance = 10;
controls.target.set(0, 10, 0);
// ===== 光照 =====
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
dirLight.position.set(50, 80, 40);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
dirLight.shadow.camera.near = 10;
dirLight.shadow.camera.far = 300;
dirLight.shadow.camera.left = -120;
dirLight.shadow.camera.right = 120;
dirLight.shadow.camera.top = 120;
dirLight.shadow.camera.bottom = -120;
scene.add(dirLight);
// ===== 地面 =====
const groundGeo = new THREE.PlaneGeometry(600, 600);
const groundMat = new THREE.MeshStandardMaterial({
color: 0xf0f0f3,
roughness: 0.95,
metalness: 0.0
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// ===== 辅助函数:Chaikin 平滑点列表 =====
function chaikinSmooth(points, iterations = 2) {
let pts = points.map(p => p.clone());
for (let it = 0; it < iterations; it++) {
const newPts = [];
// 保持起点
newPts.push(pts[0].clone());
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i];
const p1 = pts[i + 1];
const Q = p0.clone().lerp(p1, 0.25);
const R = p0.clone().lerp(p1, 0.75);
newPts.push(Q, R);
}
// 保持终点
newPts.push(pts[pts.length - 1].clone());
pts = newPts;
}
return pts;
}
// ===== 辅助函数:在曲线上均匀采样点 =====
function sampleCurve(curve, targetSegments = 300, smoothIterations = 1) {
const points = [];
for (let i = 0; i <= targetSegments; i++) {
const t = i / targetSegments;
points.push(curve.getPointAt(t));
}
if (smoothIterations > 0) {
return chaikinSmooth(points, smoothIterations);
}
return points;
}
// ===== 轨道段生成函数 =====
// 1) 螺旋段(Helix):从 start 点开始,绕Y轴上升/下降多圈
function generateHelixPoints(start, radius = 15, heightDelta = 20, turns = 3, direction = 1, samples = 300) {
const pts = [];
const yStart = start.y;
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const angle = t * turns * Math.PI * 2 * direction;
const y = yStart + t * heightDelta * direction;
const x = start.x + radius * Math.cos(angle);
const z = start.z + radius * Math.sin(angle);
pts.push(new THREE.Vector3(x, y, z));
}
return { points: pts, end: pts[pts.length - 1].clone() };
}
// 2) 波浪段(Spring/Wave):沿X方向起伏,Y保持给定高度
function generateWaveYPoints(start, length = 60, amplitude = 4, frequency = 2, height = 6, samples = 180) {
const pts = [];
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const x = start.x + t * length;
const z = start.z + amplitude * Math.sin(t * Math.PI * 2 * frequency);
const y = height;
pts.push(new THREE.Vector3(x, y, z));
}
return { points: pts, end: pts[pts.length - 1].clone() };
}
// 3) 漏斗下降(Funnel Drop):螺旋半径逐渐缩小,Y下降
function generateFunnelPoints(start, startRadius = 12, endRadius = 3, heightDelta = 20, turns = 4, direction = 1, samples = 280) {
const pts = [];
const yStart = start.y;
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const angle = t * turns * Math.PI * 2 * direction;
const r = THREE.MathUtils.lerp(startRadius, endRadius, t);
const y = yStart - t * heightDelta; // 向下
const x = start.x + r * Math.cos(angle);
const z = start.z + r * Math.sin(angle);
pts.push(new THREE.Vector3(x, y, z));
}
return { points: pts, end: pts[pts.length - 1].clone() };
}
// 4) 垂直环(Loop-the-Loop):在YZ平面绘制完整360°圆环,回到同一X,Z略微偏移
function generateLoopPoints(center, radius = 12, samples = 120) {
const pts = [];
const cx = center.x;
const cy = center.y;
const cz = center.z;
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const ang = t * Math.PI * 2;
// 在YZ平面画圆,X保持不变
const y = cy + radius * Math.cos(ang);
const z = cz + radius * Math.sin(ang);
const x = cx;
pts.push(new THREE.Vector3(x, y, z));
}
return { points: pts, end: pts[pts.length - 1].clone() };
}
// 5) 过渡段(Transition):使用贝塞尔曲线连接两个点,同时对齐切线方向
function generateTransitionPoints(start, end, align = 'end', samples = 80) {
// 自动生成控制点,使起点或终点方向对齐(使用前/后一段的切线方向)
// 这里简单采用直线插值+轻微抬升,避免锐角
const pts = [];
for (let i = 0; i <= samples; i++) {
const t = i / samples;
// 轻微抛物线抬升,水平方向线性插值
const p = start.clone().lerp(end, t);
const midLift = Math.sin(t * Math.PI) * 3.0; // 最高抬升3米
p.y += midLift;
pts.push(p);
}
return { points: pts, end: pts[pts.length - 1].clone() };
}
// ===== 生成完整轨道点云并平滑 =====
function generateTrackPoints() {
const pts = [];
const start = new THREE.Vector3(0, 6, 0);
// A) 螺旋上升(3圈,半径15,高度+20)
const helix1 = generateHelixPoints(start, 15, 20, 3, 1, 260);
// 1-2) 过渡至波浪段
const trans1 = generateTransitionPoints(helix1.end, helix1.end.clone().add(new THREE.Vector3(0, 0, 0)), 'end', 60);
// B) 波浪段(X方向60,长度对应2个波,Y=6)
const wave = generateWaveYPoints(helix1.end, 60, 5, 2, 6, 220);
// 2-3) 过渡至漏斗入口
const trans2 = generateTransitionPoints(wave.end, wave.end.clone().add(new THREE.Vector3(0, 0, 0)), 'end', 60);
// C) 漏斗下降(半径12->3,下降20,4圈)
const funnel = generateFunnelPoints(wave.end, 12, 3, 20, 4, -1, 300);
// 3-4) 过渡至垂直环
const trans3 = generateTransitionPoints(funnel.end, funnel.end.clone().add(new THREE.Vector3(0, 0, 0)), 'end', 60);
// D) 垂直环(半径12)
const loop = generateLoopPoints(funnel.end, 12, 140);
// 合并所有点,避免重复起点
const all = []
.concat(helix1.points.slice(1))
.concat(trans1.points.slice(1))
.concat(wave.points.slice(1))
.concat(trans2.points.slice(1))
.concat(funnel.points.slice(1))
.concat(trans3.points.slice(1))
.concat(loop.points.slice(1));
// 额外平滑,确保连接更自然
const smoothed = chaikinSmooth(all, 2);
return smoothed;
}
// ===== 构建轨道(TubeGeometry) =====
const trackPoints = generateTrackPoints();
const curve = new THREE.CatmullRomCurve3(trackPoints, true, 'chordal', 0.15);
const tubularSegments = 1500; // 建议 800-1500
const radialSegments = 16; // 建议 12-16
const tubeRadius = 1.2;
const tubeGeo = new THREE.TubeGeometry(curve, tubularSegments, tubeRadius, radialSegments, true);
const tubeMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.25,
metalness: 0.1,
transparent: true,
opacity: 0.85,
side: THREE.DoubleSide
});
const track = new THREE.Mesh(tubeGeo, tubeMat);
track.castShadow = true;
track.receiveShadow = true;
scene.add(track);
// 起点标记
const startPoint = curve.getPointAt(0);
const startMarkerGeo = new THREE.SphereGeometry(0.6, 16, 16);
const startMarkerMat = new THREE.MeshStandardMaterial({ color: 0x44aaff, emissive: 0x0, roughness: 0.5 });
const startMarker = new THREE.Mesh(startMarkerGeo, startMarkerMat);
startMarker.position.copy(startPoint);
startMarker.castShadow = true;
scene.add(startMarker);
// ===== 小球(列车) =====
const ballGeo = new THREE.SphereGeometry(0.9, 24, 24);
const ballMat = new THREE.MeshStandardMaterial({ color: 0xff4d4d, roughness: 0.3, metalness: 0.0 });
const ball = new THREE.Mesh(ballGeo, ballMat);
ball.castShadow = true;
scene.add(ball);
// ===== 轨道支架系统 =====
function buildSupports(curve, tubeRadius, groundY = 0, intervalU = 0.02, minHeight = 12) {
const supports = new THREE.Group();
const points = [];
for (let u = 0; u <= 1.0; u += intervalU) {
points.push(curve.getPointAt(u));
}
const cylGeo = new THREE.CylinderGeometry(0.15, 0.15, 1, 8, 1, true);
const cylMat = new THREE.MeshStandardMaterial({ color: 0x9aa0a6, metalness: 0.4, roughness: 0.7 });
const capGeoTop = new THREE.CylinderGeometry(0.25, 0.25, 0.3, 12);
const capGeoBottom = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 12);
const capMat = new THREE.MeshStandardMaterial({ color: 0x8b9098, metalness: 0.2, roughness: 0.8 });
for (const p of points) {
const height = p.y - tubeRadius - groundY;
if (height < minHeight) continue;
const cyl = new THREE.Mesh(cylGeo, cylMat);
cyl.position.set(p.x, groundY + height / 2, p.z);
cyl.scale.y = height;
cyl.castShadow = true;
cyl.receiveShadow = true;
supports.add(cyl);
// 顶部接触轨道的小圆盘
const capTop = new THREE.Mesh(capGeoTop, capMat);
capTop.position.set(p.x, p.y - tubeRadius, p.z);
capTop.castShadow = true;
supports.add(capTop);
// 底部底座
const capBottom = new THREE.Mesh(capGeoBottom, capMat);
capBottom.position.set(p.x, groundY + 0.1, p.z);
capBottom.castShadow = true;
supports.add(capBottom);
}
return supports;
}
const supports = buildSupports(curve, tubeRadius, 0, 0.025, 10);
scene.add(supports);
// ===== 动画控制 =====
const clock = new THREE.Clock();
let t = 0; // 0..1 路径参数
let speed = 0.06; // 循环速度
let isFirstPerson = false; // 视角模式切换
const modeLabel = document.getElementById('mode');
function animate() {
const dt = clock.getDelta();
t = (t + dt * speed) % 1.0;
// 小球沿轨迹运动
const pos = curve.getPointAt(t);
const tan = curve.getTangentAt(t);
ball.position.copy(pos);
ball.lookAt(pos.clone().add(tan));
// 相机跟随
if (isFirstPerson) {
// 第一人称:相机位于小球稍后方,看向前进方向
const backOffset = tan.clone().multiplyScalar(-6);
const up = new THREE.Vector3(0, 1, 0);
const side = new THREE.Vector3().crossVectors(tan, up).normalize();
const camPos = pos.clone().add(backOffset).add(side.multiplyScalar(1.2)).add(up.multiplyScalar(1.5));
camera.position.lerp(camPos, 0.15);
const lookAtPoint = pos.clone().add(tan.clone().multiplyScalar(10));
camera.lookAt(lookAtPoint);
} else {
// 第三人称:OrbitControls
controls.update();
}
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
// ===== 交互 =====
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
window.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'v') {
isFirstPerson = !isFirstPerson;
modeLabel.textContent = isFirstPerson ? '第一人称' : '第三人称';
}
if (e.key === '+' || e.key === '=') {
speed = Math.min(0.2, speed + 0.01);
}
if (e.key === '-' || e.key === '_') {
speed = Math.max(0.01, speed - 0.01);
}
});
// 鼠标右键禁用默认菜单,便于平移操作
window.addEventListener('contextmenu', (e) => e.preventDefault(), false);
</script>
</body>
</html>