This commit is contained in:
xiao.xiong
2025-10-11 15:35:42 +08:00
commit 10cdbb2967
9 changed files with 981 additions and 0 deletions

77
src/fly.ts Normal file
View File

@@ -0,0 +1,77 @@
import * as THREE from 'three';
export interface FlyConfig {
duration?: number; // 飞出去和飞回来的总时间,单位 ms
radius?: number; // 飞出去的最大半径
}
/**
* 让卡片在正面时无序飞出去并环绕一圈后飞回原位
* @param cards 卡片 Mesh 数组
* @param scene three.js 场景
* @param onStart 回调,开始飞行时触发
* @param onEnd 回调,飞行结束时触发
* @param config 配置项
*/
export function flyChaos(
cards: THREE.Mesh[],
scene: THREE.Scene,
onStart?: () => void,
onEnd?: () => void,
config: FlyConfig = {}
) {
const duration = config.duration ?? 2000;
const radius = config.radius ?? 20;
const originalPositions = cards.map(card => card.position.clone());
const randomTargets = cards.map(card => {
// 随机一个圆环上的点
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
const r = radius * (0.7 + Math.random() * 0.3);
return new THREE.Vector3(
r * Math.sin(phi) * Math.cos(theta),
r * Math.sin(phi) * Math.sin(theta),
r * Math.cos(phi)
);
});
let startTime = performance.now();
let phase = 0; // 0: 飞出去, 1: 飞回来
let flying = true;
if (onStart) onStart();
function animateFly() {
const now = performance.now();
const t = Math.min((now - startTime) / duration, 1);
for (let i = 0; i < cards.length; i++) {
if (phase === 0) {
// 飞出去
cards[i].position.lerpVectors(originalPositions[i], randomTargets[i], t);
} else {
// 飞回来
cards[i].position.lerpVectors(randomTargets[i], originalPositions[i], t);
}
}
if (t < 1) {
requestAnimationFrame(animateFly);
} else {
if (phase === 0) {
// 飞出去结束等待2s再飞回来
phase = 1;
startTime = performance.now();
setTimeout(() => {
requestAnimationFrame(animateFly);
}, 2000);
} else {
// 飞回来结束
flying = false;
if (onEnd) onEnd();
}
}
}
requestAnimationFrame(animateFly);
return () => flying; // 返回一个状态查询函数
}

123
src/main.ts Normal file
View File

@@ -0,0 +1,123 @@
import './style.css';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { flyChaos } from './fly';
const gridSize = 10;
const cardSize = 3;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f2027);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 50);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.minDistance = 20;
controls.maxDistance = 100;
const cards: THREE.Mesh[] = [];
const geometry = new THREE.BoxGeometry(cardSize, cardSize, 0.1);
const textureLoader = new THREE.TextureLoader();
const pandaTexture = textureLoader.load('./panda.png');
pandaTexture.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshStandardMaterial({
map: pandaTexture,
metalness: 0.2,
roughness: 0.8,
// 去掉 emissive 让图片更自然
});
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
const card = new THREE.Mesh(geometry, material.clone());
card.position.x = (x - gridSize / 2) * (cardSize + 0.5);
card.position.y = (y - gridSize / 2) * (cardSize + 0.5);
card.position.z = 0;
cards.push(card);
scene.add(card);
}
}
const ambientLight = new THREE.AmbientLight(0xffffff, 3.5);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0x00ffe7, 1, 200);
pointLight.position.set(0, 0, 40);
scene.add(pointLight);
let flyState = 0; // 0: 原排列, 1: 无序
let flyTimer: number | null = null;
let randomTargets: THREE.Vector3[] = [];
const originalPositions = cards.map(card => card.position.clone());
function genRandomTargets() {
const radius = 20;
return cards.map(card => {
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
const r = radius * (0.7 + Math.random() * 0.3);
return new THREE.Vector3(
r * Math.sin(phi) * Math.cos(theta),
r * Math.sin(phi) * Math.sin(theta),
r * Math.cos(phi)
);
});
}
function flyTo(targets: THREE.Vector3[], duration: number, cb?: () => void) {
const start = performance.now();
const from = cards.map(card => card.position.clone());
function anim() {
const t = Math.min((performance.now() - start) / duration, 1);
for (let i = 0; i < cards.length; i++) {
cards[i].position.lerpVectors(from[i], targets[i], t);
}
if (t < 1) {
requestAnimationFrame(anim);
} else {
if (cb) cb();
}
}
requestAnimationFrame(anim);
}
function loopFly() {
if (flyState === 0) {
// 飞到无序
randomTargets = genRandomTargets();
flyTo(randomTargets, 2000, () => {
flyState = 1;
flyTimer = window.setTimeout(loopFly, 2000);
});
} else {
// 飞回原排列
flyTo(originalPositions, 2000, () => {
flyState = 0;
flyTimer = window.setTimeout(loopFly, 2000);
});
}
}
loopFly();
function animate() {
requestAnimationFrame(animate);
scene.rotation.y += 0.005;
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});

51
src/style.css Normal file
View File

@@ -0,0 +1,51 @@
.sci-fi-grid {
display: grid;
grid-template-columns: repeat(30, 1fr);
grid-template-rows: repeat(30, 1fr);
gap: 2px;
width: 900px;
height: 900px;
margin: 40px auto;
perspective: 1200px;
}
.sci-fi-card {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #0f2027 0%, #2c5364 100%);
border-radius: 6px;
box-shadow: 0 0 8px #00ffe7, 0 0 2px #1a1a1a inset;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orbitron', 'Roboto', sans-serif;
position: relative;
transition: box-shadow 0.3s;
overflow: hidden;
}
.sci-fi-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: 6px;
border: 1px solid #00ffe7;
box-shadow: 0 0 6px #00ffe7, 0 0 2px #00ffe7 inset;
pointer-events: none;
}
.card-content {
text-align: center;
z-index: 1;
}
.card-content h2 {
font-size: 0.6rem;
letter-spacing: 1px;
margin-bottom: 2px;
text-shadow: 0 0 2px #00ffe7;
}
.card-content p {
font-size: 0.4rem;
color: #b3eaff;
text-shadow: 0 0 1px #00ffe7;
}