matterjs 实现弹珠台


matterjs 实现弹珠台

webpack 构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
html,
body {
padding: 0;
margin: 0;
overflow: hidden;
}

.container {
margin: 20px;
}
</style>
</head>

<body>
<div class="container"></div>
<script src="./bundle.js"></script>
</body>
</html>

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require("path");

module.exports = {
entry: path.join(__dirname, "src", "index.js"),
mode: "development",
output: {
filename: "bundle.js",
path: path.join(__dirname),
},
devServer: {
hot: true,
static: {
directory: path.join(__dirname),
},
},
};

安装一些会用到的依赖:

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "marble",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "npx webpack server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"matter-attractors": "^0.1.6",
"matter-js": "^0.19.0",
"pathseg": "^1.2.1",
"poly-decomp": "^0.3.0",
"svgpath": "^2.6.0",
"webpack-dev-server": "^4.15.1"
}
}

初始化并添加墙壁等物块

位置啥的都是自己根据别人的效果图,初略调的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import Matter from "matter-js";

let engine;

const WIDTH = 500;
const HEIGHT = 640;

function init() {
engine = Matter.Engine.create();

const render = Matter.Render.create({
element: document.querySelector(".container"),
engine: engine,
options: {
width: WIDTH,
height: HEIGHT,
wireframes: false,
},
});
Matter.Render.run(render);

const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);

const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false,
},
},
});

Matter.Composite.add(engine.world, mouseConstraint);
}

function addBoundries() {
Matter.Composite.add(engine.world, [
boundary(0, HEIGHT / 2, 40, HEIGHT),
boundary(WIDTH, HEIGHT / 2, 40, HEIGHT),
boundary(WIDTH / 2, HEIGHT, HEIGHT, 40),

wall(150, 100, 18, 40),
wall(230, 100, 18, 40),
wall(320, 100, 18, 40),

circle(100, 180, 20),
circle(225, 180, 20),
circle(350, 180, 20),

circle(160, 260, 20),
circle(290, 260, 20),

wall(440, 420, 20, 450),

wall(120, 380, 20, 110),
wall(320, 380, 20, 110),

wall(60, 400, 20, 150),
wall(88, 485, 20, 88, {
angle: -0.95,
}),

wall(380, 400, 20, 150),
wall(352, 485, 20, 88, {
angle: 0.95,
}),

// reset(0, 50),
reset(225, 64),
reset(465, 32),
]);
}

function boundary(x, y, width, height) {
return Matter.Bodies.rectangle(x, y, width, height, {
isStatic: true,
render: {
fillStyle: "#495057",
},
});
}

function circle(x, y, radius) {
const circle = Matter.Bodies.circle(x, y, radius, {
isStatic: true,
render: {
fillStyle: "#495057",
},
});

return circle;
}

function wall(x, y, width, height, options) {
return Matter.Bodies.rectangle(x, y, width, height, {
isStatic: true,
angle: options && options.angle ? options.angle : 0,
chamfer: {
radius: 10,
},
render: {
fillStyle: "#495057",
},
});
}

function reset(x, width) {
return Matter.Bodies.rectangle(x, 620, width, 2, {
label: "reset",
isStatic: true,
render: {
fillStyle: "#fff",
},
});
}

function load() {
init();
addBoundries();
}

load();

添加三角形物块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const PATHS = {
// leftArrow: 'M 0 0 L 40 60 L 0 100 L 0 0',
leftArrow: "M 0 0 L 40 60 L 0 100 L 0 0",
// 最高的那个点要作为出发点(不知道具体原因)
rightArrow: "M 40 -60 L 40 40 L 0 0 L 40 -60",
leftBottom: "M 0 0 L 0 -140 L 180 0 L 0 0",
rightBottom: "M 0 -140 L 0 0 L -180 0 L 0 -140",
};

function path(x, y, path) {
const vertices = Matter.Vertices.fromPath(path);

return Matter.Bodies.fromVertices(x, y, vertices, {
isStatic: true,
render: {
fillStyle: "pink",
strokeStyle: "pink",
lineWidth: 1,
},
});
}

function addBoundries() {
Matter.Composite.add(engine.world, [
// ...
path(35, 260, PATHS.leftArrow),
path(416, 280, PATHS.rightArrow),

path(80, 580, PATHS.leftBottom),
path(370, 580, PATHS.rightBottom),
]);
}

PATHS 里的路径可以在SvgPathEditor中使用 SVG 语法来绘制。

需要注意的是:最高的那个点要作为出发点(不知道具体原因)

去掉isStatic: true后:

M 0 -140 L 0 0 L -180 0 L 0 -140:最高点为出发点。

M 0 0 L -180 0 L 0 -140 L 0 0:最高点不是出发点(和上面的形状是一样的

可以看到,这时候墙壁基本就是虚设。(具体原因不清楚,但是有可能是因为 svg 路径的凸凹啥的,之前听过一丢丢

添加半圆物块

半圆物块没有采用上面的办法,因为按上面的办法的话,半圆还得得到很多点才行。(SVG 语法的 A 命令之类的不生效,基本只能用 L 语法去得到点,即需要很多点来弄出一个半圆)

采用的方案是通过SvgPathEditor绘制半圆,并得到对应的 SVG,然后再通过Matter.Svg.pathToVertices来将 SVG 路径转成物块。

  1. 绘制

  2. 加载 SVG,并将 path 元素通过Matter.Svg.pathToVeritices(path, 36)将 path 转成顶点 Vertices。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    const loadSvg = function (url) {
    return fetch(url)
    .then(function (response) {
    return response.text();
    })
    .then(function (raw) {
    // 把加载的svg转成document对象
    return new window.DOMParser().parseFromString(raw, "image/svg+xml");
    });
    };

    function svg(x, y, svgName) {
    loadSvg(`/svg/${svgName}.svg`).then((root) => {
    const vertices = [...root.querySelectorAll("path")].map((path) => {
    return Matter.Svg.pathToVertices(path, 36);
    });

    Matter.Composite.add(
    engine.world,
    Matter.Bodies.fromVertices(x, y, vertices, {
    isStatic: true,
    render: {
    fillStyle: "#495057",
    lineWidth: 1,
    },
    })
    );
    });
    }
  3. 通过Matter.Bodies.fromVertices根据顶点生成物块,并添加到物理世界中。

  4. 调用方法svg

    1
    2
    3
    4
    function addBoundries() {
    svg(236, 80, "dome");
    // ...
    }
  5. 安装pathsegpoly-decomp

    pathseg:因为Matter.Svg.pathToVertices不支持复杂路径,所以需要pathseg来 polyfill。

    poly-decomp:将 2D 多边形分解为凸块(matter-js 不支持凹顶点)\color{red}{凹凸顶点有机会再了解}

    1
    2
    3
    4
    5
    6
    import "pathseg";

    function init() {
    Matter.Common.setDecomp(require("poly-decomp"));
    // ...
    }

添加操作浆

原本看了别人的博客实现方式,觉得一个浆,加两个停止器就行了。但是实现起来才发现效果不行,浆会有很高频的抖动效果。最后有参考(抄)别人的思路。

上面的蓝色物块就是觉得没必要,后面才发现很有必要的。添加好后,设置visiblefalse即可。

  1. 添加键盘事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    let isLeftPaddleUp = false;
    let isRightPaddleUp = false;
    function addEvents() {
    window.addEventListener("keypress", (e) => {
    if (e.key.toLowerCase() === "a") {
    isLeftPaddleUp = true;
    } else {
    isLeftPaddleUp = false;
    }

    if (e.key.toLowerCase() === "d") {
    isRightPaddleUp = true;
    } else {
    isRightPaddleUp = false;
    }
    });

    window.addEventListener("keyup", (e) => {
    if (e.key.toLowerCase() === "a") {
    isLeftPaddleUp = false;
    }

    if (e.key.toLowerCase() === "d") {
    isRightPaddleUp = false;
    }
    });
    }

    function load() {
    init();
    addBoundries();
    addEvents();
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2.  使用`matter-attractors`,创建stopper物块。

```js
// 1.
import MatterAttractors from 'matter-attractors';


function init() {
Matter.use(MatterAttractors);
// ...
}

// 2.
function getPaddleStatus(side) {
const isPaddleUp = side === 'left' ? isLeftPaddleUp : isRightPaddleUp;
return isPaddleUp;
}

function stopper(x, y, side, position) {
const judgeLabel = side === 'left' ? 'paddleLeftComp' : 'paddleRightComp';


const options = {
isStatic: true,
render: {
fillStyle: 'red'
},
plugin: {
attractors: [
function (bodyA, bodyB) {
if (bodyB.label === judgeLabel) {
return {
x: (bodyA.position.x - bodyB.position.x) * 0.002 * ((getPaddleStatus(side)) ? -1 : 0.5), // 0.5是防止松手后,吸引力太大导致变形
y: (bodyA.position.y - bodyB.position.y) * 0.002 * ((getPaddleStatus(side)) ? -1 : 0.5)
}
}
}
]
}
};

const hadForce = position === 'bottom';
if (!hadForce) {
// 只有下面的stopper有引(斥)力
Reflect.deleteProperty(options, 'plugin');
}

return Matter.Bodies.circle(x, y, 20, options);
}


// 3.
function addPaddles() {
const stoperLeftTop = stopper(170, 460, 'left', 'top');
const stoperLeftBottom = stopper(136, 580, 'left', 'bottom');
const stoperRightTop = stopper(280, 460, 'right', 'top');
const stoperRightBottom = stopper(300, 580, 'right', 'bottom');

Matter.Composite.add(engine.world, [
stoperLeftTop,
stoperLeftBottom,
stoperRightTop,
stoperRightBottom
]);
}

matter-attractors可以对物块添加持续的力。通过键盘事件来控制力是引力还是斥力。直接添加会导致对所有物块都有影响,所以通过label来判断是否添加。

  1. 创建浆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    function addPaddles() {
    const paddleLeft = {};
    paddleLeft.paddle = Matter.Bodies.trapezoid(134, 512, 20, 88, 0.33, {
    label: "paddleLeft",
    angle: 1.57,
    chamfer: {},
    render: {
    fillStyle: "skyblue",
    },
    });

    paddleLeft.brick = Matter.Bodies.rectangle(134, 524, 40, 40, {
    render: {
    fillStyle: "blue",
    // visible: false
    },
    });

    // 将两个物块组装在一起
    paddleLeft.comp = Matter.Body.create({
    label: "paddleLeftComp",
    parts: [paddleLeft.paddle, paddleLeft.brick],
    });

    Matter.Composite.add(engine.world, [paddleLeft.comp]);
    }

  2. 添加约束,固定浆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function addPaddles() {
    // ...
    // bodyB表示给paddleLeft.comp添加约束,pointB则是相对paddleLeft.comp的坐标
    // pointA是这个物理世界的坐标
    const constraintLeft = Matter.Constraint.create({
    bodyB: paddleLeft.comp,
    pointB: {
    x: -32,
    y: -8,
    },
    pointA: {
    x: paddleLeft.comp.position.x,
    y: paddleLeft.comp.position.y,
    },
    stiffness: 0.9,
    length: 0,
    render: {
    strokeStyle: "pink",
    },
    });
    Matter.Composite.add(engine.world, constraintLeft);
    }

  3. 右边的浆同理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    const paddleRight = {};
    paddleRight.paddle = Matter.Bodies.trapezoid(304, 512, 20, 88, 0.33, {
    // isStatic: true,
    label: "paddleRight",
    angle: -1.57,
    chamfer: {},
    render: {
    fillStyle: "skyblue",
    },
    });

    paddleRight.brick = Matter.Bodies.rectangle(304, 524, 40, 40, {
    render: {
    fillStyle: "blue",
    // visible: false
    },
    });

    paddleRight.comp = Matter.Body.create({
    label: "paddleRightComp",
    parts: [paddleRight.paddle, paddleRight.brick],
    });

    const constraintRight = Matter.Constraint.create({
    bodyB: paddleRight.comp,
    pointB: {
    x: 32,
    y: -8,
    },
    pointA: {
    x: paddleRight.comp.position.x,
    y: paddleRight.comp.position.y,
    },
    stiffness: 0.9,
    length: 0,
    render: {
    strokeStyle: "pink",
    },
    });
    Matter.Composite.add(engine.world, constraintRight);

    Matter.Composite.add(engine.world, [paddleLeft.comp, paddleRight.comp]);

添加弹珠,并添加碰撞事件,实现reset效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function addMarble() {
const marble = Matter.Bodies.circle(0, 0, 14, {
render: {
fillStyle: "green",
},
});
Matter.Composite.add(engine.world, marble);

// 设置坐标
Matter.Body.setPosition(marble, {
x: 464,
y: 500,
});

// 设置力
Matter.Body.setVelocity(marble, {
x: 0,
y: -25,
});
}

function addEvents() {
// ...
Matter.Events.on(engine, "collisionStart", (e) => {
e.pairs.forEach(function (pair) {
const bodyA = pair.bodyA;
const bodyB = pair.bodyB;

if (bodyA.label === "reset") {
Matter.Composite.remove(engine.world, [bodyB]);
addMarble();
}

if (bodyB.label === "reset") {
Matter.Composite.remove(engine.world, [bodyA]);
addMarble();
}
});
});
}

给中间的 5 个圆加弹力

现在的弹珠基本会掉到中间,浆根本没有用武之地。

1
2
3
4
5
6
7
8
9
10
11
12
13
function circle(x, y, radius) {
const circle = Matter.Bodies.circle(x, y, radius, {
isStatic: true,
render: {
fillStyle: "#495057",
},
});

// 直接在初始化那里加不会生效
circle.restitution = 1.5;

return circle;
}

设置group。让 stopper、marble 它们变成“一伙”

现在弹珠碰到stopper物块也会有碰撞效果。所以设置group,成群之后就不会再有碰撞效果了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const group = Matter.Body.nextGroup(true);


function stopper(x, y, side, position) {
// ...
const options = {
isStatic true,
render: {
fillStyle: 'red'
},
collisionFilter: {
group: group,
}
// ...
};
// ...
}

function addMarble() {
const marble = Matter.Bodies.circle(0, 0, 10, {
render: {
fillStyle: 'green'
},
collisionFilter: {
group: group
}
});
Matter.Composite.add(engine.world, marble);

// 设置坐标
Matter.Body.setPosition(marble, {
x: 464,
y: 500,
});

// 设置力
Matter.Body.setVelocity(marble, {
x: 0,
y: -22, // 球变小了。所以里也应该相应减小。
});
}

\color{red}{弹珠的大小从 14 变成了 10}。后面发现会被brick卡住,并且没法单独给brick设置group

隐藏辅助物块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function stopper(x, y, side, position) {
// ...
const options = {
isStatic true,
render: {
fillStyle: 'red',
visible: false,
},
// ...
};
// ...
}


paddleLeft.brick = Matter.Bodies.rectangle(134, 524, 40, 40, {
render: {
fillStyle: 'blue',
visible: false
},
});
// paddleRight.brick同理

效果以及完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
import Matter from "matter-js";
import "pathseg";
import MatterAttractors from "matter-attractors";

let engine;

const PATHS = {
leftArrow: "M 0 0 L 40 60 L 0 100 L 0 0",
// 最高的那个点要作为出发点(不知道具体原因)
rightArrow: "M 40 -60 L 40 40 L 0 0 L 40 -60",
leftBottom: "M 0 0 L 0 -140 L 180 0 L 0 0",
rightBottom: "M 0 -140 L 0 0 L -180 0 L 0 -140",
};

const WIDTH = 500;
const HEIGHT = 640;

const group = Matter.Body.nextGroup(true);

function init() {
Matter.Common.setDecomp(require("poly-decomp"));
Matter.use(MatterAttractors);

engine = Matter.Engine.create();

const render = Matter.Render.create({
element: document.querySelector(".container"),
engine: engine,
options: {
width: WIDTH,
height: HEIGHT,
wireframes: false,
},
});
Matter.Render.run(render);

const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);

const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false,
},
},
});

Matter.Composite.add(engine.world, mouseConstraint);
}

function addBoundries() {
svg(236, 80, "dome");

Matter.Composite.add(engine.world, [
boundary(0, HEIGHT / 2, 40, HEIGHT),
boundary(WIDTH, HEIGHT / 2, 40, HEIGHT),
boundary(WIDTH / 2, HEIGHT, HEIGHT, 40),

wall(150, 100, 18, 40),
wall(230, 100, 18, 40),
wall(320, 100, 18, 40),

circle(100, 180, 20),
circle(225, 180, 20),
circle(350, 180, 20),

circle(160, 260, 20),
circle(290, 260, 20),

wall(440, 420, 20, 450),

wall(120, 380, 20, 110),
wall(320, 380, 20, 110),

wall(60, 400, 20, 150),
wall(88, 485, 20, 88, {
angle: -0.95,
}),

wall(380, 400, 20, 150),
wall(352, 485, 20, 88, {
angle: 0.95,
}),

reset(225, 64),
reset(465, 32),

path(35, 260, PATHS.leftArrow),
path(416, 280, PATHS.rightArrow),

path(80, 580, PATHS.leftBottom),
path(370, 580, PATHS.rightBottom),
]);
}

let isLeftPaddleUp = false;
let isRightPaddleUp = false;
function addEvents() {
window.addEventListener("keypress", (e) => {
if (e.key.toLowerCase() === "a") {
isLeftPaddleUp = true;
} else {
isLeftPaddleUp = false;
}

if (e.key.toLowerCase() === "d") {
isRightPaddleUp = true;
} else {
isRightPaddleUp = false;
}
});

window.addEventListener("keyup", (e) => {
if (e.key.toLowerCase() === "a") {
isLeftPaddleUp = false;
}

if (e.key.toLowerCase() === "d") {
isRightPaddleUp = false;
}
});

Matter.Events.on(engine, "collisionStart", (e) => {
e.pairs.forEach(function (pair) {
const bodyA = pair.bodyA;
const bodyB = pair.bodyB;

if (bodyA.label === "reset") {
Matter.Composite.remove(engine.world, [bodyB]);
addMarble();
}

if (bodyB.label === "reset") {
Matter.Composite.remove(engine.world, [bodyA]);
addMarble();
}
});
});
}

function getPaddleStatus(side) {
const isPaddleUp = side === "left" ? isLeftPaddleUp : isRightPaddleUp;
return isPaddleUp;
}

function stopper(x, y, side, position) {
const judgeLabel = side === "left" ? "paddleLeftComp" : "paddleRightComp";

const options = {
isStatic: true,
render: {
fillStyle: "red",
visible: false,
},
collisionFilter: {
group: group,
},
plugin: {
attractors: [
function (bodyA, bodyB) {
if (bodyB.label === judgeLabel) {
return {
x:
(bodyA.position.x - bodyB.position.x) *
0.002 *
(getPaddleStatus(side) ? -1 : 0.5), // 0.5是防止松手后,吸引力太大导致变形
y:
(bodyA.position.y - bodyB.position.y) *
0.002 *
(getPaddleStatus(side) ? -1 : 0.5),
};
}
},
],
},
};

const hadForce = position === "bottom";
if (!hadForce) {
// 只有下面的stopper有引(斥)力
Reflect.deleteProperty(options, "plugin");
}

return Matter.Bodies.circle(x, y, 20, options);
}

function addPaddles() {
const stoperLeftTop = stopper(170, 460, "left", "top");
const stoperLeftBottom = stopper(136, 580, "left", "bottom");
const stoperRightTop = stopper(280, 460, "right", "top");
const stoperRightBottom = stopper(300, 580, "right", "bottom");

Matter.Composite.add(engine.world, [
stoperLeftTop,
stoperLeftBottom,
stoperRightTop,
stoperRightBottom,
]);

const paddleLeft = {};
paddleLeft.paddle = Matter.Bodies.trapezoid(134, 512, 20, 88, 0.33, {
label: "paddleLeft",
angle: 1.57,
chamfer: {},
render: {
fillStyle: "skyblue",
},
});

paddleLeft.brick = Matter.Bodies.rectangle(134, 524, 40, 40, {
render: {
fillStyle: "blue",
visible: false,
},
});

// 将两个物块组装在一起
paddleLeft.comp = Matter.Body.create({
label: "paddleLeftComp",
parts: [paddleLeft.paddle, paddleLeft.brick],
});

// bodyB表示给paddleLeft.comp添加约束,pointB则是相对paddleLeft.comp的坐标
// pointA是这个物理世界的坐标
const constraintLeft = Matter.Constraint.create({
bodyB: paddleLeft.comp,
pointB: {
x: -32,
y: -8,
},
pointA: {
x: paddleLeft.comp.position.x,
y: paddleLeft.comp.position.y,
},
stiffness: 0.9,
length: 0,
render: {
strokeStyle: "pink",
},
});
Matter.Composite.add(engine.world, constraintLeft);

const paddleRight = {};
paddleRight.paddle = Matter.Bodies.trapezoid(304, 512, 20, 88, 0.33, {
// isStatic: true,
label: "paddleRight",
angle: -1.57,
chamfer: {},
render: {
fillStyle: "skyblue",
},
});

paddleRight.brick = Matter.Bodies.rectangle(304, 524, 40, 40, {
render: {
fillStyle: "blue",
visible: false,
},
});

paddleRight.comp = Matter.Body.create({
label: "paddleRightComp",
parts: [paddleRight.paddle, paddleRight.brick],
});

const constraintRight = Matter.Constraint.create({
bodyB: paddleRight.comp,
pointB: {
x: 32,
y: -8,
},
pointA: {
x: paddleRight.comp.position.x,
y: paddleRight.comp.position.y,
},
stiffness: 0.9,
length: 0,
render: {
strokeStyle: "pink",
},
});
Matter.Composite.add(engine.world, constraintRight);

Matter.Composite.add(engine.world, [paddleLeft.comp, paddleRight.comp]);
}

function addMarble() {
const marble = Matter.Bodies.circle(0, 0, 10, {
render: {
fillStyle: "green",
},
collisionFilter: {
group: group,
},
});
Matter.Composite.add(engine.world, marble);

// 设置坐标
Matter.Body.setPosition(marble, {
x: 464,
y: 500,
});

// 设置力
Matter.Body.setVelocity(marble, {
x: 0,
y: -22,
});
}

function boundary(x, y, width, height) {
return Matter.Bodies.rectangle(x, y, width, height, {
isStatic: true,
render: {
fillStyle: "#495057",
},
});
}

function circle(x, y, radius) {
const circle = Matter.Bodies.circle(x, y, radius, {
isStatic: true,
render: {
fillStyle: "#495057",
},
});

// 直接在初始化那里加不会生效
circle.restitution = 1.5;

return circle;
}

function wall(x, y, width, height, options) {
return Matter.Bodies.rectangle(x, y, width, height, {
isStatic: true,
angle: options && options.angle ? options.angle : 0,
chamfer: {
radius: 10,
},
render: {
fillStyle: "#495057",
},
});
}

function reset(x, width) {
return Matter.Bodies.rectangle(x, 620, width, 2, {
label: "reset",
isStatic: true,
render: {
fillStyle: "#fff",
},
});
}

function path(x, y, path) {
const vertices = Matter.Vertices.fromPath(path);

return Matter.Bodies.fromVertices(x, y, vertices, {
isStatic: true,
render: {
fillStyle: "pink",
strokeStyle: "pink",
lineWidth: 1,
},
});
}

const loadSvg = function (url) {
return fetch(url)
.then(function (response) {
return response.text();
})
.then(function (raw) {
// 把加载的svg转成document对象
return new window.DOMParser().parseFromString(raw, "image/svg+xml");
});
};

function svg(x, y, svgName) {
loadSvg(`/svg/${svgName}.svg`).then((root) => {
console.log(root);

const vertices = [...root.querySelectorAll("path")].map((path) => {
return Matter.Svg.pathToVertices(path, 36);
});

Matter.Composite.add(
engine.world,
Matter.Bodies.fromVertices(x, y, vertices, {
isStatic: true,
render: {
fillStyle: "#495057",
lineWidth: 1,
},
})
);
});
}

function load() {
init();
addBoundries();
addEvents();
addPaddles();
addMarble();
}

load();

代码放仓库了,有兴趣的话,可以查看。

GitHub - clzczhc/marble

参考链接

JavaScript Physics with Matter.js / Coder’s Block

SvgPathEditor

Matter.js - a 2D rigid body JavaScript physics engine · code by @liabru

GitHub - liabru/matter-attractors: an attractors plugin for matter.js


文章作者: 赤蓝紫
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 赤蓝紫 !
评论
  目录