planck-js
Version:
2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development
444 lines (380 loc) • 11.8 kB
text/typescript
import * as Stage from "stage-js";
import type { World } from "../src/dynamics/World";
import type { Joint } from "../src/dynamics/Joint";
import type { Fixture } from "../src/dynamics/Fixture";
import type { Body } from "../src/dynamics/Body";
import type { AABBValue } from "../src/collision/AABB";
import { Testbed } from "../src/util/Testbed";
import { MouseJoint } from "../src/dynamics/joint/MouseJoint";
import { WorldComponent, WorldDragEnd, WorldDragMove, WorldDragStart } from "./world-view";
const math_PI = Math.PI;
let mounted: StageTestbed | null = null;
Testbed.mount = () => {
if (mounted) {
return mounted;
}
mounted = new StageTestbed();
const playButton = document.getElementById("testbed-play");
const statusElement = document.getElementById("testbed-status");
const infoElement = document.getElementById("testbed-info");
if (playButton) {
playButton.addEventListener("click", () => {
if (mounted.isPaused()) {
mounted.resume();
} else {
mounted.pause();
}
});
mounted._pause = () => {
playButton.classList.add("pause");
playButton.classList.remove("play");
};
mounted._resume = () => {
playButton.classList.add("play");
playButton.classList.remove("pause");
};
} else {
console.log("Please create a button with id='testbed-play'");
}
let lastStatus = "";
if (statusElement) {
statusElement.innerText = lastStatus;
}
mounted._status = (text: string) => {
if (lastStatus === text) {
return;
}
lastStatus = text;
if (statusElement) {
statusElement.innerText = text;
}
};
let lastInfo = "";
if (infoElement) {
infoElement.innerText = lastInfo;
}
mounted._info = (text: string) => {
if (lastInfo === text) {
return;
}
lastInfo = text;
if (infoElement) {
infoElement.innerText = text;
}
};
return mounted;
};
/** @internal */
export class StageTestbed extends Testbed {
private canvas: HTMLCanvasElement;
private stage: Stage.Root;
paused: boolean = false;
private lastDrawHash = "";
private newDrawHash = "";
private buffer: ((context: CanvasRenderingContext2D, ratio: number) => void)[] = [];
start(world: World) {
const stage = (this.stage = Stage.mount());
const canvas = (this.canvas = stage.dom as HTMLCanvasElement);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const testbed = this;
this.canvas = canvas;
stage.on(Stage.POINTER_DOWN, () => {
window.focus();
// @ts-ignore
document.activeElement?.blur();
canvas.focus();
});
stage.MAX_ELAPSE = 1000 / 30;
stage.flipY(true);
stage.on("resume", () => {
this.paused = false;
this._resume();
});
stage.on("pause", () => {
this.paused = true;
this._pause();
});
const drawingTexture = new Stage.CanvasTexture();
drawingTexture.draw = (ctx: CanvasRenderingContext2D) => {
const pixelRatio = drawingTexture.getDevicePixelRatio();
ctx.save();
ctx.transform(1, 0, 0, 1, -this.x, -this.y);
ctx.lineWidth = 3 / pixelRatio;
ctx.lineCap = "round";
for (let drawing = this.buffer.shift(); drawing; drawing = this.buffer.shift()) {
drawing(ctx, pixelRatio);
}
ctx.restore();
};
const drawingElement = Stage.sprite(drawingTexture);
stage.append(drawingElement);
stage.tick(() => {
this.buffer.length = 0;
}, true);
stage.background(this.background);
stage.viewbox(this.width, this.height);
stage.pin("alignX", -0.5);
stage.pin("alignY", -0.5);
const mouseGround = world.createBody();
let mouseJoint: MouseJoint | null = null;
let targetBody: Body | null = null;
const mouseMove = { x: 0, y: 0 };
const pointerStart = (event: WorldDragStart) => {
const point = event.point;
if (targetBody) {
return;
}
const fixture = worldNode.findFixture(point);
if (!fixture) {
return;
}
const body = fixture.getBody();
if (this.mouseForce) {
targetBody = body;
} else if (this.mouseForce === 0) {
} else {
mouseJoint = new MouseJoint({ maxForce: 1000 }, mouseGround, body, {
x: point.x,
y: point.y,
});
world.createJoint(mouseJoint);
}
};
const pointerMove = (event: WorldDragMove) => {
const point = event.point;
if (mouseJoint) {
mouseJoint.setTarget(point);
}
mouseMove.x = point.x;
mouseMove.y = point.y;
};
const pointerEnd = (event: WorldDragEnd) => {
const point = event.point;
if (mouseJoint) {
world.destroyJoint(mouseJoint);
mouseJoint = null;
}
if (targetBody && this.mouseForce) {
const target = targetBody.getPosition();
const force = {
x: (point.x - target.x) * this.mouseForce,
y: (point.y - target.y) * this.mouseForce,
};
targetBody.applyForceToCenter(force, true);
targetBody = null;
}
};
const pointerCancel = () => {
if (mouseJoint) {
world.destroyJoint(mouseJoint);
mouseJoint = null;
}
if (targetBody) {
targetBody = null;
}
};
const worldNode = new WorldComponent(this, (name, event) => {
if (name === "world-drag-start") {
pointerStart(event as WorldDragStart);
} else if (name === "world-drag-move") {
pointerMove(event as WorldDragMove);
} else if (name === "world-drag-end") {
pointerEnd(event as WorldDragEnd);
} else if (name === "world-pointer-cancel") {
pointerCancel();
}
});
worldNode.setWorld(world);
// stage.empty();
stage.prepend(worldNode);
let lastX = 0;
let lastY = 0;
stage.tick((dt: number, t: number) => {
// update camera position
if (lastX !== this.x || lastY !== this.y) {
worldNode.offset(this.x, this.y);
lastX = this.x;
lastY = this.y;
}
});
worldNode.tick((dt: number, t: number) => {
this.step(dt, t);
if (targetBody) {
this.drawSegment(targetBody.getPosition(), mouseMove, "rgba(255,255,255,0.2)");
}
if (this.lastDrawHash !== this.newDrawHash) {
this.lastDrawHash = this.newDrawHash;
stage.touch();
}
this.newDrawHash = "";
return true;
});
const activeKeys = testbed.activeKeys;
const downKeys: Record<number, boolean> = {};
function updateActiveKeys(keyCode: number, down: boolean) {
const char = String.fromCharCode(keyCode);
if (/\w/.test(char)) {
activeKeys[char] = down;
}
activeKeys.right = downKeys[39] || activeKeys["D"];
activeKeys.left = downKeys[37] || activeKeys["A"];
activeKeys.up = downKeys[38] || activeKeys["W"];
activeKeys.down = downKeys[40] || activeKeys["S"];
activeKeys.fire = downKeys[32] || downKeys[13];
}
window.addEventListener("keydown", function (e) {
const keyCode = e.keyCode;
downKeys[keyCode] = true;
updateActiveKeys(keyCode, true);
testbed.keydown?.(keyCode, String.fromCharCode(keyCode));
});
window.addEventListener("keyup", function (e) {
const keyCode = e.keyCode;
downKeys[keyCode] = false;
updateActiveKeys(keyCode, false);
testbed.keyup?.(keyCode, String.fromCharCode(keyCode));
});
this.resume();
}
/** @private @internal */
focus() {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
document.activeElement && document.activeElement.blur();
this.canvas.focus();
}
/** @internal */
_pause() {}
/** @internal */
_resume() {}
private statusText = "";
private statusMap: Record<string, any> = {};
status(name: string, value: any): void;
status(value: object | string): void;
status(a: any, b?: any) {
if (typeof b !== "undefined") {
const key = a;
const value = b;
if (typeof value !== "function" && typeof value !== "object") {
this.statusMap[key] = value;
}
} else if (a && typeof a === "object") {
// tslint:disable-next-line:no-for-in
for (const key in a) {
const value = a[key];
if (typeof value !== "function" && typeof value !== "object") {
this.statusMap[key] = value;
}
}
} else if (typeof a === "string") {
this.statusText = a;
}
var newline = "\n";
var text = this.statusText || "";
for (var key in this.statusMap) {
var value = this.statusMap[key];
if (typeof value === "function") continue;
text += (text && newline) + key + ": " + value;
}
this._status(text);
}
info(text: string): void {
this._info(text);
}
/** @internal */
_status(string: string) {}
/** @internal */
_info(text: string) {}
/** @internal */
isPaused() {
return this.paused;
}
/** @internal */
togglePause() {
if (this.paused) {
this.resume();
} else {
this.pause();
}
}
/** @internal */
pause() {
this.stage.pause();
}
/** @internal */
resume() {
this.stage.resume();
this.focus();
}
drawPoint(p: { x: number; y: number }, r: number, color: string): void {
this.buffer.push(function (ctx, ratio) {
ctx.beginPath();
ctx.arc(p.x, p.y, 5 / ratio, 0, 2 * math_PI);
ctx.strokeStyle = color;
ctx.stroke();
});
this.newDrawHash += "point" + p.x + "," + p.y + "," + r + "," + color;
}
drawCircle(p: { x: number; y: number }, r: number, color: string): void {
this.buffer.push(function (ctx) {
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, 2 * math_PI);
ctx.strokeStyle = color;
ctx.stroke();
});
this.newDrawHash += "circle" + p.x + "," + p.y + "," + r + "," + color;
}
drawEdge(a: { x: number; y: number }, b: { x: number; y: number }, color: string): void {
this.buffer.push(function (ctx) {
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = color;
ctx.stroke();
});
this.newDrawHash += "segment" + a.x + "," + a.y + "," + b.x + "," + b.y + "," + color;
}
drawSegment = this.drawEdge;
drawPolygon(points: Array<{ x: number; y: number }>, color: string): void {
if (!points || !points.length) {
return;
}
this.buffer.push(function (ctx) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.strokeStyle = color;
ctx.closePath();
ctx.stroke();
});
this.newDrawHash += "segment";
for (let i = 1; i < points.length; i++) {
this.newDrawHash += points[i].x + "," + points[i].y + ",";
}
this.newDrawHash += color;
}
drawAABB(aabb: AABBValue, color: string): void {
this.buffer.push(function (ctx) {
ctx.beginPath();
ctx.moveTo(aabb.lowerBound.x, aabb.lowerBound.y);
ctx.lineTo(aabb.upperBound.x, aabb.lowerBound.y);
ctx.lineTo(aabb.upperBound.x, aabb.upperBound.y);
ctx.lineTo(aabb.lowerBound.x, aabb.upperBound.y);
ctx.strokeStyle = color;
ctx.closePath();
ctx.stroke();
});
this.newDrawHash += "aabb";
this.newDrawHash += aabb.lowerBound.x + "," + aabb.lowerBound.y + ",";
this.newDrawHash += aabb.upperBound.x + "," + aabb.upperBound.y + ",";
this.newDrawHash += color;
}
findOne(query: string): Body | Joint | Fixture | null {
throw new Error("Not implemented");
}
findAll(query: string): (Body | Joint | Fixture)[] {
throw new Error("Not implemented");
}
}