update
This commit is contained in:
77
src/fly.ts
Normal file
77
src/fly.ts
Normal 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
123
src/main.ts
Normal 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
51
src/style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user