diff --git a/README.md b/README.md
index 3988666..ce9f8f0 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ Physilinks is a browser-based physics linking game built with Matter.js. Match a
- `src/scenes/`: Scene presets split per file (`scene-*.js`) plus `index.js` that registers them to `window.PhysilinksScenes` (e.g., zero-G grid, balanced, low-G, fast drop, lava drift).
- `src/scenes/scene-template.js`: Reference-only template documenting every scene config option; not loaded by default.
- `src/config.js`: Base game config defaults (gravity, spawn timing, link settings, palettes, message defaults).
+- `src/engine.js`: Matter engine/render/runner setup helpers (create, start/stop runner, resize render).
- `src/decomp-setup.js`: Registers `poly-decomp` with Matter to allow concave shapes (stars, blobs) built via `Bodies.fromVertices`.
- `src/ui.js`: DOM access, HUD updates, overlays, popups, and control/selector wiring.
- `src/spawn.js`: Spawner utilities (intervals, batch/column/grid spawns), ball creation (shapes/blobs), radius scaling, and blob cleanup.
diff --git a/index.html b/index.html
index 074299f..1813246 100644
--- a/index.html
+++ b/index.html
@@ -106,6 +106,7 @@
+
diff --git a/src/engine.js b/src/engine.js
new file mode 100644
index 0000000..2baab92
--- /dev/null
+++ b/src/engine.js
@@ -0,0 +1,73 @@
+(() => {
+ const { Engine, Render, Runner } = Matter;
+
+ const create = ({
+ sceneEl,
+ width,
+ height,
+ background = "transparent",
+ wireframes = false,
+ showAngleIndicator = false,
+ }) => {
+ const engine = Engine.create();
+ const render = Render.create({
+ element: sceneEl,
+ engine,
+ options: {
+ width,
+ height,
+ wireframes,
+ background,
+ showAngleIndicator,
+ pixelRatio: window.devicePixelRatio || 1,
+ },
+ });
+ Render.run(render);
+
+ const runner = Runner.create();
+ Runner.run(runner, engine);
+ let runnerActive = true;
+
+ const startRunner = () => {
+ if (!runnerActive) {
+ Runner.run(runner, engine);
+ runnerActive = true;
+ }
+ };
+
+ const stopRunner = () => {
+ if (runnerActive) {
+ Runner.stop(runner);
+ runnerActive = false;
+ }
+ };
+
+ const setRenderSize = (nextWidth, nextHeight) => {
+ const pixelRatio = window.devicePixelRatio || 1;
+ render.options.width = nextWidth;
+ render.options.height = nextHeight;
+ render.options.pixelRatio = pixelRatio;
+ render.canvas.style.width = `${nextWidth}px`;
+ render.canvas.style.height = `${nextHeight}px`;
+ render.canvas.width = nextWidth * pixelRatio;
+ render.canvas.height = nextHeight * pixelRatio;
+ Render.setPixelRatio(render, pixelRatio);
+ render.bounds.min.x = 0;
+ render.bounds.min.y = 0;
+ render.bounds.max.x = nextWidth;
+ render.bounds.max.y = nextHeight;
+ Render.lookAt(render, render.bounds);
+ };
+
+ return {
+ engine,
+ render,
+ runner,
+ startRunner,
+ stopRunner,
+ setRenderSize,
+ };
+ };
+
+ window.PhysilinksEngine = { create };
+})();
diff --git a/src/main.js b/src/main.js
index f7c7304..5e395cb 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,6 +1,5 @@
(() => {
- const { Engine, Render, Runner, World, Body, Constraint, Events, Vector } =
- Matter;
+ const { World, Body, Constraint, Events, Vector } = Matter;
const { config: baseConfig, defaultMessageConfig } = window.PhysilinksConfig
?.create
@@ -14,6 +13,7 @@
colors: null,
},
};
+
const config = {
...baseConfig,
link: { ...(baseConfig?.link || {}) },
@@ -25,6 +25,7 @@
defaultSceneId,
order: sceneOrder = [],
} = window.PhysilinksScenes || {};
+
const { getSceneById, getSceneIdFromUrl, setSceneIdInUrl, getNextSceneId } =
window.PhysilinksSceneRegistry || {};
@@ -35,44 +36,20 @@
let height = sceneEl.clientHeight;
const BALL_BASELINE = 680; // reference height used for relative ball sizing
- const engine = Engine.create();
+ const { engine, render, runner, startRunner, stopRunner, setRenderSize } =
+ window.PhysilinksEngine.create({
+ sceneEl,
+ width,
+ height,
+ background: "transparent",
+ wireframes: false,
+ showAngleIndicator: false,
+ });
const defaultGravityScale = engine.gravity.scale;
const defaultTimeScale = engine.timing.timeScale || 1;
engine.gravity.y = config.gravity;
const world = engine.world;
- const render = Render.create({
- element: sceneEl,
- engine,
- options: {
- width,
- height,
- wireframes: false,
- background: "transparent",
- showAngleIndicator: false,
- pixelRatio: window.devicePixelRatio || 1,
- },
- });
- Render.run(render);
-
- const runner = Runner.create();
- Runner.run(runner, engine);
- let runnerActive = true;
-
- const startRunner = () => {
- if (!runnerActive) {
- Runner.run(runner, engine);
- runnerActive = true;
- }
- };
-
- const stopRunner = () => {
- if (runnerActive) {
- Runner.stop(runner);
- runnerActive = false;
- }
- };
-
// Static boundaries and scene-specific obstacles.
let boundaries = [];
let rotators = [];
@@ -580,20 +557,7 @@
const prevRadius = config.ballRadius;
width = sceneEl.clientWidth;
height = sceneEl.clientHeight;
- const pixelRatio = window.devicePixelRatio || 1;
- render.options.width = width;
- render.options.height = height;
- render.options.pixelRatio = pixelRatio;
- render.canvas.style.width = `${width}px`;
- render.canvas.style.height = `${height}px`;
- render.canvas.width = width * pixelRatio;
- render.canvas.height = height * pixelRatio;
- Render.setPixelRatio(render, pixelRatio);
- render.bounds.min.x = 0;
- render.bounds.min.y = 0;
- render.bounds.max.x = width;
- render.bounds.max.y = height;
- Render.lookAt(render, render.bounds);
+ setRenderSize(width, height);
spawnSystem.updateBallRadius(prevRadius);
clampBodiesIntoView(prevWidth, prevHeight);
rebuildSceneBodies();