255 lines
6.8 KiB
TypeScript
255 lines
6.8 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { useTheme } from "next-themes";
|
||
import Matter from "matter-js";
|
||
|
||
/**
|
||
* Antigravity Background Component
|
||
* 使用 Matter.js 实现完整的 Google Antigravity 物理效果
|
||
*/
|
||
export function AntigravityBackground() {
|
||
const sceneRef = useRef<HTMLDivElement>(null);
|
||
const engineRef = useRef<Matter.Engine | null>(null);
|
||
const renderRef = useRef<Matter.Render | null>(null);
|
||
const mouseConstraintRef = useRef<Matter.MouseConstraint | null>(null);
|
||
const { resolvedTheme } = useTheme();
|
||
const [mounted, setMounted] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!mounted || !sceneRef.current) return;
|
||
|
||
// 清理旧实例
|
||
if (engineRef.current) {
|
||
Matter.Engine.clear(engineRef.current);
|
||
}
|
||
if (renderRef.current) {
|
||
Matter.Render.stop(renderRef.current);
|
||
renderRef.current.canvas.remove();
|
||
}
|
||
|
||
// 创建物理引擎
|
||
const engine = Matter.Engine.create({
|
||
gravity: { x: 0, y: 1, scale: 0.001 },
|
||
});
|
||
engineRef.current = engine;
|
||
|
||
// 创建渲染器
|
||
const render = Matter.Render.create({
|
||
element: sceneRef.current,
|
||
engine: engine,
|
||
options: {
|
||
width: window.innerWidth,
|
||
height: window.innerHeight,
|
||
wireframes: false,
|
||
background: "transparent",
|
||
pixelRatio: window.devicePixelRatio,
|
||
},
|
||
});
|
||
renderRef.current = render;
|
||
|
||
// 创建墙壁(边界)
|
||
const wallOptions: Matter.IBodyDefinition = {
|
||
isStatic: true,
|
||
render: {
|
||
fillStyle: "transparent",
|
||
strokeStyle: "transparent",
|
||
},
|
||
friction: 0.3,
|
||
restitution: 0.6,
|
||
};
|
||
|
||
const walls = [
|
||
// 底部
|
||
Matter.Bodies.rectangle(
|
||
window.innerWidth / 2,
|
||
window.innerHeight + 25,
|
||
window.innerWidth,
|
||
50,
|
||
wallOptions
|
||
),
|
||
// 顶部
|
||
Matter.Bodies.rectangle(
|
||
window.innerWidth / 2,
|
||
-25,
|
||
window.innerWidth,
|
||
50,
|
||
wallOptions
|
||
),
|
||
// 左侧
|
||
Matter.Bodies.rectangle(
|
||
-25,
|
||
window.innerHeight / 2,
|
||
50,
|
||
window.innerHeight,
|
||
wallOptions
|
||
),
|
||
// 右侧
|
||
Matter.Bodies.rectangle(
|
||
window.innerWidth + 25,
|
||
window.innerHeight / 2,
|
||
50,
|
||
window.innerHeight,
|
||
wallOptions
|
||
),
|
||
];
|
||
|
||
Matter.Composite.add(engine.world, walls);
|
||
|
||
// 创建可拖动的物体
|
||
const emojis = [
|
||
{ emoji: "🤖", x: 200, y: 100, size: 40, color: "#667eea" },
|
||
{ emoji: "💬", x: 400, y: 150, size: 35, color: "#764ba2" },
|
||
{ emoji: "✨", x: 600, y: 200, size: 38, color: "#f093fb" },
|
||
{ emoji: "🚀", x: 800, y: 120, size: 36, color: "#4facfe" },
|
||
{ emoji: "🎨", x: 300, y: 400, size: 34, color: "#43e97b" },
|
||
{ emoji: "⚡", x: 700, y: 350, size: 37, color: "#fa709a" },
|
||
];
|
||
|
||
const bodies = emojis.map((item) => {
|
||
const body = Matter.Bodies.circle(item.x, item.y, item.size / 2, {
|
||
restitution: 0.8,
|
||
friction: 0.05,
|
||
density: 0.001,
|
||
render: {
|
||
fillStyle: item.color + "40",
|
||
strokeStyle: item.color + "80",
|
||
lineWidth: 2,
|
||
},
|
||
});
|
||
|
||
// 存储 emoji 信息到 body 上
|
||
(body as any).emoji = item.emoji;
|
||
(body as any).size = item.size;
|
||
|
||
return body;
|
||
});
|
||
|
||
Matter.Composite.add(engine.world, bodies);
|
||
|
||
// 添加鼠标控制
|
||
const mouse = Matter.Mouse.create(render.canvas);
|
||
const mouseConstraint = Matter.MouseConstraint.create(engine, {
|
||
mouse: mouse,
|
||
constraint: {
|
||
stiffness: 0.2,
|
||
render: {
|
||
visible: false,
|
||
},
|
||
},
|
||
});
|
||
mouseConstraintRef.current = mouseConstraint;
|
||
|
||
Matter.Composite.add(engine.world, mouseConstraint);
|
||
|
||
// 保持鼠标与渲染器同步
|
||
render.mouse = mouse;
|
||
|
||
// 自定义渲染函数(绘制 emoji)
|
||
Matter.Events.on(render, "afterRender", () => {
|
||
const context = render.context;
|
||
|
||
bodies.forEach((body) => {
|
||
const emoji = (body as any).emoji;
|
||
const size = (body as any).size;
|
||
|
||
if (!emoji) return;
|
||
|
||
context.save();
|
||
context.translate(body.position.x, body.position.y);
|
||
context.rotate(body.angle);
|
||
|
||
// 绘制半透明背景圆
|
||
context.beginPath();
|
||
context.arc(0, 0, size / 2, 0, Math.PI * 2);
|
||
context.fillStyle = body.render.fillStyle || "#667eea40";
|
||
context.fill();
|
||
|
||
// 绘制 emoji
|
||
context.font = `${size * 0.8}px Arial`;
|
||
context.textAlign = "center";
|
||
context.textBaseline = "middle";
|
||
context.fillText(emoji, 0, 0);
|
||
|
||
context.restore();
|
||
});
|
||
});
|
||
|
||
// 启动引擎和渲染器
|
||
const runner = Matter.Runner.create();
|
||
Matter.Runner.run(runner, engine);
|
||
Matter.Render.run(render);
|
||
|
||
// 窗口大小调整
|
||
const handleResize = () => {
|
||
render.canvas.width = window.innerWidth;
|
||
render.canvas.height = window.innerHeight;
|
||
render.options.width = window.innerWidth;
|
||
render.options.height = window.innerHeight;
|
||
|
||
// 更新墙壁位置
|
||
Matter.Body.setPosition(walls[0], {
|
||
x: window.innerWidth / 2,
|
||
y: window.innerHeight + 25,
|
||
});
|
||
Matter.Body.setPosition(walls[1], {
|
||
x: window.innerWidth / 2,
|
||
y: -25,
|
||
});
|
||
Matter.Body.setPosition(walls[2], {
|
||
x: -25,
|
||
y: window.innerHeight / 2,
|
||
});
|
||
Matter.Body.setPosition(walls[3], {
|
||
x: window.innerWidth + 25,
|
||
y: window.innerHeight / 2,
|
||
});
|
||
|
||
// 更新墙壁尺寸
|
||
Matter.Body.scale(walls[0], window.innerWidth / walls[0].bounds.max.x, 1);
|
||
Matter.Body.scale(walls[1], window.innerWidth / walls[1].bounds.max.x, 1);
|
||
Matter.Body.scale(walls[2], 1, window.innerHeight / walls[2].bounds.max.y);
|
||
Matter.Body.scale(walls[3], 1, window.innerHeight / walls[3].bounds.max.y);
|
||
};
|
||
|
||
window.addEventListener("resize", handleResize);
|
||
|
||
// 清理函数
|
||
return () => {
|
||
window.removeEventListener("resize", handleResize);
|
||
|
||
if (mouseConstraintRef.current) {
|
||
Matter.Composite.remove(engine.world, mouseConstraintRef.current);
|
||
}
|
||
|
||
if (renderRef.current) {
|
||
Matter.Render.stop(renderRef.current);
|
||
renderRef.current.canvas.remove();
|
||
}
|
||
|
||
if (engineRef.current) {
|
||
Matter.Engine.clear(engineRef.current);
|
||
}
|
||
|
||
Matter.Runner.stop(runner);
|
||
};
|
||
}, [mounted, resolvedTheme]);
|
||
|
||
if (!mounted) return null;
|
||
|
||
return (
|
||
<div
|
||
ref={sceneRef}
|
||
className="fixed inset-0 z-0"
|
||
style={{
|
||
filter: resolvedTheme === "dark" ? "brightness(0.8)" : "brightness(1)",
|
||
transition: "filter 0.5s ease-in-out",
|
||
}}
|
||
/>
|
||
);
|
||
}
|