html
复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Matter.js Mixed Effects Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
box-sizing: border-box;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
color: white;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
font-size: 2.2rem;
}
.controls {
text-align: center;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
button {
background: #4caf50;
color: white;
border: none;
padding: 10px 18px;
margin: 5px 0;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
transition: background 0.3s;
min-width: 90px;
}
button:hover {
background: #45a049;
}
button:active {
transform: scale(0.95);
}
.canvas-container {
text-align: center;
margin-top: 20px;
}
#canvas {
border: 3px solid #333;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
background: #f0f0f0;
width: 100%;
max-width: 800px;
height: auto;
aspect-ratio: 4/3;
display: block;
margin: 0 auto;
}
.info {
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 10px;
margin-top: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
font-size: 1rem;
}
.info h3 {
margin-top: 0;
color: #333;
}
.info p {
margin: 5px 0;
color: #666;
}
@media (max-width: 900px) {
.container {
padding: 0 10px;
}
h1 {
font-size: 1.5rem;
}
.info {
font-size: 0.95rem;
}
}
@media (max-width: 600px) {
body {
padding: 8px;
}
.container {
padding: 0 2px;
}
.controls {
gap: 6px;
}
button {
font-size: 0.95rem;
padding: 8px 10px;
min-width: 70px;
}
#canvas {
max-width: 100vw;
min-width: 0;
border-width: 2px;
}
.info {
font-size: 0.9rem;
padding: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Matter.js Mixed Effects Demo</h1>
<div class="controls">
<button onclick="addBox()">添加方块</button>
<button onclick="addCircle()">添加圆形</button>
<button onclick="addPolygon()">添加多边形</button>
<button onclick="addText()">添加文字</button>
<button onclick="addConstraint()">添加约束</button>
<button onclick="addExplosion()">爆炸效果</button>
<button onclick="addWind()">风力效果</button>
<button onclick="clearAll()">清除所有</button>
<button onclick="toggleGravity()">切换重力</button>
</div>
<div class="canvas-container">
<canvas id="canvas" width="800" height="600"></canvas>
</div>
<div class="info">
<h3>功能说明:</h3>
<p>• <strong>添加方块/圆形/多边形</strong>:创建不同形状的物体</p>
<p>• <strong>添加约束</strong>:在物体之间创建连接</p>
<p>• <strong>爆炸效果</strong>:在鼠标位置创建爆炸力</p>
<p>• <strong>风力效果</strong>:模拟风力对物体的影响</p>
<p>• <strong>切换重力</strong>:开启/关闭重力效果</p>
<p>• <strong>鼠标交互</strong>:点击并拖拽物体</p>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<script>
// 初始化Matter.js模块
const {
Engine,
Render,
World,
Bodies,
Body,
Composite,
Constraint,
Mouse,
MouseConstraint,
Events,
} = Matter;
// 获取canvas实际宽高
function getCanvasSize() {
const container = document.querySelector(".canvas-container");
let width = container.offsetWidth;
let height = width * 0.75; // 4:3比例
if (width > 800) {
width = 800;
height = 600;
}
return { width, height };
}
// 初始化canvas尺寸
const canvas = document.getElementById("canvas");
const { width: initWidth, height: initHeight } = getCanvasSize();
canvas.width = initWidth;
canvas.height = initHeight;
// 创建引擎和渲染器,宽高为自适应canvas宽高
const engine = Engine.create();
const render = Render.create({
canvas: canvas,
engine: engine,
options: {
width: initWidth,
height: initHeight,
wireframes: false,
background: "#f0f0f0",
},
});
// 创建地面和墙体的函数
function createBounds(width, height) {
const ground = Bodies.rectangle(width / 2, height - 10, width, 20, {
isStatic: true,
render: { fillStyle: "#2c3e50" },
});
const leftWall = Bodies.rectangle(10, height / 2, 20, height, {
isStatic: true,
render: { fillStyle: "#2c3e50" },
});
const rightWall = Bodies.rectangle(width - 10, height / 2, 20, height, {
isStatic: true,
render: { fillStyle: "#2c3e50" },
});
const ceiling = Bodies.rectangle(width / 2, 10, width, 20, {
isStatic: true,
render: { fillStyle: "#2c3e50" },
});
return [ground, leftWall, rightWall, ceiling];
}
// 初始边界
let bounds = createBounds(initWidth, initHeight);
World.add(engine.world, bounds);
// 创建鼠标约束
const mouse = Mouse.create(render.canvas);
const mouseConstraint = MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false,
},
},
});
World.add(engine.world, mouseConstraint);
// 启动引擎和渲染器
Engine.run(engine);
Render.run(render);
// 全局变量
let gravityEnabled = true;
let windForce = 0;
let constraints = [];
// 工具函数:生成随机数字
function getRandomNumber() {
return Math.floor(Math.random() * 100) + 1;
}
// 添加方块函数
function addBox() {
const margin = 50;
const width = render.options.width;
const height = render.options.height;
const number = getRandomNumber();
const box = Bodies.rectangle(
Math.random() * (width - 2 * margin) + margin,
Math.random() * (height / 3 - margin) + margin,
40,
40,
{
render: {
fillStyle: `hsl(${Math.random() * 360}, 70%, 60%)`,
number: number,
},
restitution: 0.8,
friction: 0.1,
}
);
box.customNumber = number;
World.add(engine.world, box);
}
// 添加圆形函数
function addCircle() {
const margin = 50;
const width = render.options.width;
const height = render.options.height;
const number = getRandomNumber();
const circle = Bodies.circle(
Math.random() * (width - 2 * margin) + margin,
Math.random() * (height / 3 - margin) + margin,
20,
{
render: {
fillStyle: `hsl(${Math.random() * 360}, 70%, 60%)`,
number: number,
},
restitution: 0.9,
friction: 0.05,
}
);
circle.customNumber = number;
World.add(engine.world, circle);
}
// 添加多边形函数
function addPolygon() {
const margin = 50;
const width = render.options.width;
const height = render.options.height;
const number = getRandomNumber();
const sides = Math.floor(Math.random() * 4) + 3; // 3-6边形
const vertices = [];
for (let i = 0; i < sides; i++) {
const angle = (i / sides) * Math.PI * 2;
const radius = 15 + Math.random() * 10;
vertices.push({
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius,
});
}
const polygon = Bodies.fromVertices(
Math.random() * (width - 2 * margin) + margin,
Math.random() * (height / 3 - margin) + margin,
[vertices],
{
render: {
fillStyle: `hsl(${Math.random() * 360}, 70%, 60%)`,
number: number,
},
restitution: 0.7,
friction: 0.2,
}
);
polygon.customNumber = number;
World.add(engine.world, polygon);
}
// 添加文字函数
function addText() {
const margin = 50;
const width = render.options.width;
const height = render.options.height;
const number = getRandomNumber();
const text = Bodies.rectangle(
Math.random() * (width - 2 * margin) + margin,
Math.random() * (height / 3 - margin) + margin,
80,
30,
{
render: {
fillStyle: `#ffffff00`,
number: number,
isText: true,
},
restitution: 0.8,
friction: 0.1,
}
);
text.customNumber = number;
text.isText = true;
World.add(engine.world, text);
}
// 添加约束函数
function addConstraint() {
const bodies = Composite.allBodies(engine.world).filter(
(body) => !body.isStatic
);
if (bodies.length >= 2) {
const bodyA = bodies[Math.floor(Math.random() * bodies.length)];
const bodyB = bodies[Math.floor(Math.random() * bodies.length)];
if (bodyA !== bodyB) {
const constraint = Constraint.create({
bodyA: bodyA,
bodyB: bodyB,
pointA: { x: 0, y: 0 },
pointB: { x: 0, y: 0 },
stiffness: 0.1,
render: {
strokeStyle: "#e74c3c",
lineWidth: 2,
},
});
constraints.push(constraint);
World.add(engine.world, constraint);
}
}
}
// 爆炸效果函数
function addExplosion() {
const bodies = Composite.allBodies(engine.world).filter(
(body) => !body.isStatic
);
const explosionPoint = { x: 400, y: 300 };
const explosionForce = 0.05;
bodies.forEach((body) => {
const distance = Math.sqrt(
Math.pow(body.position.x - explosionPoint.x, 2) +
Math.pow(body.position.y - explosionPoint.y, 2)
);
if (distance < 200) {
const force = explosionForce * (1 - distance / 200);
const angle = Math.atan2(
body.position.y - explosionPoint.y,
body.position.x - explosionPoint.x
);
Body.applyForce(body, body.position, {
x: Math.cos(angle) * force,
y: Math.sin(angle) * force,
});
}
});
}
// 风力效果函数
function addWind() {
windForce = windForce === 0 ? 0.001 : 0;
}
// 清除所有物体函数
function clearAll() {
const bodies = Composite.allBodies(engine.world).filter(
(body) => !body.isStatic
);
bodies.forEach((body) => {
World.remove(engine.world, body);
});
constraints.forEach((constraint) => {
World.remove(engine.world, constraint);
});
constraints = [];
}
// 切换重力函数
function toggleGravity() {
gravityEnabled = !gravityEnabled;
engine.world.gravity.y = gravityEnabled ? 1 : 0;
}
// 应用风力效果
Events.on(engine, "beforeUpdate", function () {
if (windForce !== 0) {
const bodies = Composite.allBodies(engine.world).filter(
(body) => !body.isStatic
);
bodies.forEach((body) => {
Body.applyForce(body, body.position, {
x: windForce,
y: 0,
});
});
}
});
// 自定义渲染数字
(function patchRender() {
const originalBodies = Render.bodies;
Render.bodies = function (render, bodies, context) {
// 先调用原始的 Render.bodies
originalBodies.call(this, render, bodies, context);
const ctx = context || render.context;
for (let i = 0; i < bodies.length; i++) {
const body = bodies[i];
if (body.customNumber) {
ctx.save();
if (body.isText) {
// 文字图形:只显示文字,不显示背景
ctx.font = "20px Arial";
ctx.fillStyle = "#222";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.globalAlpha = 0.9;
ctx.fillText(
body.customNumber,
body.position.x,
body.position.y
);
} else {
// 普通图形:显示数字
ctx.font = `${Math.max(
16,
Math.floor(
body.circleRadius
? body.circleRadius
: body.bounds.max.x - body.bounds.min.x
) * 0.8
)}px Arial`;
ctx.fillStyle = "#222";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.globalAlpha = 0.9;
ctx.fillText(
body.customNumber,
body.position.x,
body.position.y
);
}
ctx.restore();
}
}
};
})();
// 点击事件,打印数字
let touchStartPos = null;
let touchStartTime = null;
function handleClick(e) {
const rect = render.canvas.getBoundingClientRect();
let mouseX, mouseY;
if (e.type === "touchstart" || e.type === "touchmove") {
const touch = e.touches[0] || e.changedTouches[0];
mouseX =
(touch.clientX - rect.left) *
(render.options.width / render.canvas.width);
mouseY =
(touch.clientY - rect.top) *
(render.options.height / render.canvas.height);
} else {
mouseX =
(e.clientX - rect.left) *
(render.options.width / render.canvas.width);
mouseY =
(e.clientY - rect.top) *
(render.options.height / render.canvas.height);
}
const bodies = Composite.allBodies(engine.world).filter(
(body) => !body.isStatic
);
for (let body of bodies) {
if (Matter.Bounds.contains(body.bounds, { x: mouseX, y: mouseY })) {
// 更精确判断(圆形/多边形)
if (
Matter.Vertices.contains(body.vertices, { x: mouseX, y: mouseY })
) {
if (body.customNumber) {
console.log("点击数字:", body.customNumber);
}
break;
}
}
}
}
function handleTouchStart(e) {
const touch = e.touches[0];
const rect = render.canvas.getBoundingClientRect();
touchStartPos = {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
};
touchStartTime = Date.now();
}
function handleTouchEnd(e) {
if (!touchStartPos || !touchStartTime) return;
const touch = e.changedTouches[0];
const rect = render.canvas.getBoundingClientRect();
const touchEndPos = {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
};
// 计算移动距离和时间
const distance = Math.sqrt(
Math.pow(touchEndPos.x - touchStartPos.x, 2) +
Math.pow(touchEndPos.y - touchStartPos.y, 2)
);
const duration = Date.now() - touchStartTime;
// 只有移动距离小于10px且时间小于300ms才认为是点击
if (distance < 10 && duration < 300) {
const mouseX =
touchEndPos.x * (render.options.width / render.canvas.width);
const mouseY =
touchEndPos.y * (render.options.height / render.canvas.height);
const bodies = Composite.allBodies(engine.world).filter(
(body) => !body.isStatic
);
for (let body of bodies) {
if (Matter.Bounds.contains(body.bounds, { x: mouseX, y: mouseY })) {
if (
Matter.Vertices.contains(body.vertices, {
x: mouseX,
y: mouseY,
})
) {
if (body.customNumber) {
console.log("点击数字:", body.customNumber);
}
break;
}
}
}
}
// 重置触摸状态
touchStartPos = null;
touchStartTime = null;
}
render.canvas.addEventListener("click", handleClick);
render.canvas.addEventListener("touchstart", handleTouchStart);
render.canvas.addEventListener("touchend", handleTouchEnd);
// 鼠标点击事件
Events.on(mouseConstraint, "mousedown", function (event) {
const bodies = event.source.body;
if (bodies) {
Body.setAngularVelocity(bodies, 0);
}
});
// 键盘控制
document.addEventListener("keydown", function (event) {
switch (event.key) {
case "b":
case "B":
addBox();
break;
case "c":
case "C":
addCircle();
break;
case "p":
case "P":
addPolygon();
break;
case "e":
case "E":
addExplosion();
break;
case "w":
case "W":
addWind();
break;
case "g":
case "G":
toggleGravity();
break;
case " ":
clearAll();
break;
}
});
// 让canvas自适应屏幕宽度和高宽比
function resizeCanvas() {
const prevWidth = render.options.width;
const prevHeight = render.options.height;
const { width, height } = getCanvasSize();
canvas.width = width;
canvas.height = height;
render.options.width = width;
render.options.height = height;
render.canvas.width = width;
render.canvas.height = height;
// 缩放所有非静态物体的位置和大小
const scaleX = width / prevWidth;
const scaleY = height / prevHeight;
const bodies = Composite.allBodies(engine.world).filter(
(body) => !body.isStatic
);
bodies.forEach((body) => {
// 缩放位置
Body.setPosition(body, {
x: body.position.x * scaleX,
y: body.position.y * scaleY,
});
// 缩放大小(仅对矩形和圆形,复杂多边形可选)
if (body.circleRadius) {
Body.scale(body, scaleX, scaleY);
} else if (body.vertices.length === 4) {
Body.scale(body, scaleX, scaleY);
}
});
// 移除旧边界
if (bounds) {
bounds.forEach((b) => World.remove(engine.world, b));
}
// 添加新边界
bounds = createBounds(width, height);
World.add(engine.world, bounds);
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
// 初始添加一些物体
setTimeout(() => {
for (let i = 0; i < 5; i++) {
addBox();
addCircle();
}
}, 1000);
</script>
</body>
</html>