概要
wsl(wsl2じゃない)で、elixirやってみた。
Livebookで、box2dwebkino、書いてみた。
box2dwebとは
Box2DWebとは、C++で開発された有名な2D物理演算エンジン「Box2D」を、JavaScriptに移植したオープンソースのライブラリです。
Webブラウザ上で、重力、物体の質量、摩擦、跳ね返り(弾性)といった古典力学的な物理シミュレーションをリアルタイムに計算・描画することができます。
かつて大ヒットしたゲーム『Angry Birds』などにもBox2Dの技術が使われており、Webゲームやインタラクティブなコンテンツの開発で広く利用されてきました。
写真
setup
Mix.install([
{:kino, "~> 0.19"}
])
サンプルコード
defmodule Box2DWebKino do
use Kino.JS
def new() do
Kino.JS.new(__MODULE__, nil)
end
asset "main.js" do
"""
export async function init(ctx, data) {
const STAGE_WIDTH = 450;
const STAGE_HEIGHT = 450;
const SCALE = 30;
const FPS = 1000 / 60;
ctx.root.innerHTML = `
<div style="padding: 10px; background: #222; border-radius: 8px; width: ${STAGE_WIDTH}px; margin: 0 auto; user-select: none;">
<canvas id="canvas" width="${STAGE_WIDTH}" height="${STAGE_HEIGHT}" style="background: #111; display: block; cursor: crosshair;"></canvas>
<div style="color: #aaa; font-family: monospace; font-size: 12px; margin-top: 5px; text-align: center;">
Box2DWeb + Kino.JS (Drag & Drop Physics)
</div>
</div>
`;
const canvas = ctx.root.querySelector("#canvas");
const canvasCtx = canvas.getContext("2d");
await new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/box2dweb@2.1.0-b/box2d.min.js";
script.onload = resolve;
script.onerror = reject;
ctx.root.appendChild(script);
});
const b2Vec2 = Box2D.Common.Math.b2Vec2;
const b2BodyDef = Box2D.Dynamics.b2BodyDef;
const b2Body = Box2D.Dynamics.b2Body;
const b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
const b2World = Box2D.Dynamics.b2World;
const b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
const b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
const b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
const b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef;
const b2AABB = Box2D.Collision.b2AABB;
const world = new b2World(new b2Vec2(0, 10), true);
const fixDef = new b2FixtureDef();
fixDef.density = 1.0;
fixDef.friction = 0.5;
fixDef.restitution = 0.2;
const bodyDef = new b2BodyDef();
bodyDef.type = b2Body.b2_staticBody;
bodyDef.position.Set(STAGE_WIDTH / 2 / SCALE, (STAGE_HEIGHT - 10) / SCALE);
fixDef.shape = new b2PolygonShape();
fixDef.shape.SetAsBox((STAGE_WIDTH / 2) / SCALE, 10 / SCALE);
world.CreateBody(bodyDef).CreateFixture(fixDef);
bodyDef.position.Set(STAGE_WIDTH / 2 / SCALE, 10 / SCALE);
world.CreateBody(bodyDef).CreateFixture(fixDef);
bodyDef.position.Set(10 / SCALE, STAGE_HEIGHT / 2 / SCALE);
fixDef.shape.SetAsBox(10 / SCALE, (STAGE_HEIGHT / 2) / SCALE);
world.CreateBody(bodyDef).CreateFixture(fixDef);
bodyDef.position.Set((STAGE_WIDTH - 10) / SCALE, STAGE_HEIGHT / 2 / SCALE);
world.CreateBody(bodyDef).CreateFixture(fixDef);
bodyDef.type = b2Body.b2_dynamicBody;
for (let i = 0; i < 20; ++i)
{
bodyDef.position.Set((Math.random() * (STAGE_WIDTH - 60) + 30) / SCALE, (Math.random() * (STAGE_HEIGHT - 60) + 30) / SCALE);
if (Math.random() > 0.5)
{
fixDef.shape = new b2CircleShape(Math.random() * 15 / SCALE + 10 / SCALE);
}
else
{
fixDef.shape = new b2PolygonShape();
fixDef.shape.SetAsBox((Math.random() * 15 + 10) / SCALE, (Math.random() * 15 + 10) / SCALE);
}
world.CreateBody(bodyDef).CreateFixture(fixDef);
}
const debugDraw = new b2DebugDraw();
debugDraw.SetSprite(canvasCtx);
debugDraw.SetDrawScale(SCALE);
debugDraw.SetFillAlpha(0.3);
debugDraw.SetLineThickness(1.0);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
world.SetDebugDraw(debugDraw);
let mouseJoint = null;
let mousePVec = new b2Vec2();
let isMouseDown = false;
function getCanvasMousePos(e) {
const rect = canvas.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return new b2Vec2((clientX - rect.left) / SCALE, (clientY - rect.top) / SCALE);
}
function getBodyAtMouse(pX, pY) {
mousePVec.Set(pX, pY);
const aabb = new b2AABB();
aabb.lowerBound.Set(pX - 0.001, pY - 0.001);
aabb.upperBound.Set(pX + 0.001, pY + 0.001);
let selectedBody = null;
world.QueryAABB(function(fixture) {
if (fixture.GetBody().GetType() !== b2Body.b2_staticBody)
{
if (fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), mousePVec))
{
selectedBody = fixture.GetBody();
return false;
}
}
return true;
}, aabb);
return selectedBody;
}
function handleMouseDown(e) {
isMouseDown = true;
const p = getCanvasMousePos(e);
const body = getBodyAtMouse(p.x, p.y);
if (body)
{
const md = new b2MouseJointDef();
md.bodyA = world.GetGroundBody();
md.bodyB = body;
md.target.Set(p.x, p.y);
md.collideConnected = true;
md.maxForce = 300.0 * body.GetMass();
mouseJoint = world.CreateJoint(md);
body.SetAwake(true);
}
}
function handleMouseMove(e) {
if (!isMouseDown) return;
const p = getCanvasMousePos(e);
if (mouseJoint)
{
mouseJoint.SetTarget(new b2Vec2(p.x, p.y));
}
}
function handleMouseUp() {
isMouseDown = false;
if (mouseJoint)
{
world.DestroyJoint(mouseJoint);
mouseJoint = null;
}
}
canvas.addEventListener("mousedown", handleMouseDown);
canvas.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
canvas.addEventListener("touchstart", handleMouseDown, { passive: true });
canvas.addEventListener("touchmove", handleMouseMove, { passive: true });
canvas.addEventListener("touchend", handleMouseUp);
function update() {
world.Step(1 / 60, 10, 10);
world.DrawDebugData();
world.ClearForces();
}
const loopInterval = setInterval(update, FPS);
ctx.handleDestroy(() => {
clearInterval(loopInterval);
canvas.removeEventListener("mousedown", handleMouseDown);
canvas.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
canvas.removeEventListener("touchstart", handleMouseDown);
canvas.removeEventListener("touchmove", handleMouseMove);
canvas.removeEventListener("touchend", handleMouseUp);
});
}
"""
end
end
Box2DWebKino.new()
以上。
