diff --git a/index.html b/index.html
index dc7d676..b789be7 100644
--- a/index.html
+++ b/index.html
@@ -122,6 +122,7 @@
+
diff --git a/src/spawn-balls.js b/src/spawn-balls.js
new file mode 100644
index 0000000..6079741
--- /dev/null
+++ b/src/spawn-balls.js
@@ -0,0 +1,181 @@
+(() => {
+ const { Bodies, Composites, Body } = Matter;
+
+ const create = ({ config, getCurrentScene }) => {
+ const applyBallPlugin = (body, { color, shape, blobId } = {}) => {
+ body.plugin = {
+ color,
+ hasEntered: false,
+ entryCheckId: null,
+ };
+ if (shape) body.plugin.shape = shape;
+ if (blobId) body.plugin.blobId = blobId;
+ };
+
+ const getCommonBallOpts = (scene, color) => {
+ const ballPhysics = scene?.config?.ballPhysics || {};
+ return {
+ restitution: ballPhysics.restitution ?? 0.72,
+ friction: ballPhysics.friction ?? 0.01,
+ frictionAir: ballPhysics.frictionAir ?? 0.012,
+ frictionStatic: ballPhysics.frictionStatic ?? 0,
+ density: ballPhysics.density ?? 0.001,
+ render: {
+ fillStyle: color,
+ strokeStyle: "#0b1222",
+ lineWidth: 2,
+ },
+ };
+ };
+
+ const createSoftBlob = (x, y, color, commonOpts) => {
+ const cols = 3;
+ const rows = 2;
+ const radius = Math.max(10, config.ballRadius * 0.55);
+ const soft = Composites.softBody(
+ x - cols * radius * 1.2,
+ y - rows * radius * 1.2,
+ cols,
+ rows,
+ 0,
+ 0,
+ true,
+ radius,
+ commonOpts,
+ );
+ const blobId = `blob-${Date.now()}-${Math.random()
+ .toString(16)
+ .slice(2)}`;
+ soft.bodies.forEach((b) => applyBallPlugin(b, { color, blobId }));
+ soft.constraints.forEach((c) => {
+ c.plugin = { blobId, blobConstraint: true };
+ c.render = c.render || {};
+ c.render.type = "line";
+ });
+ return { bodies: soft.bodies, constraints: soft.constraints, blobId };
+ };
+
+ const createJaggedBall = (x, y, color, commonOpts) => {
+ const points = [];
+ const segments = 6;
+ for (let i = 0; i < segments; i += 1) {
+ const angle = Math.min((i / segments) * Math.PI * 2, Math.PI * 2);
+ const variance = 0.6 + Math.random() * 0.5;
+ const r = config.ballRadius * variance;
+ points.push({
+ x: x + Math.cos(angle) * r,
+ y: y + Math.sin(angle) * r,
+ });
+ }
+ const body = Bodies.fromVertices(x, y, [points], commonOpts, true);
+ applyBallPlugin(body, { color, shape: "jagged" });
+ return { bodies: [body], constraints: [], blobId: null };
+ };
+
+ const createGiftBall = (x, y, color, commonOpts, scene, debugSpawn) => {
+ const size = config.ballRadius * 2;
+ const ribbonSize = Math.max(4, config.ballRadius * 0.35);
+ const ribbonColor = scene?.config?.giftRibbonColor || "#f8fafc";
+ if (debugSpawn) {
+ console.log("Spawn gift", {
+ sceneId: scene?.id,
+ x,
+ y,
+ color,
+ size,
+ });
+ }
+ const base = Bodies.rectangle(x, y, size, size, {
+ ...commonOpts,
+ chamfer: { radius: Math.max(2, config.ballRadius * 0.18) },
+ });
+ const ribbonRender = {
+ fillStyle: ribbonColor,
+ strokeStyle: "#0b1222",
+ lineWidth: 2,
+ };
+ const verticalRibbon = Bodies.rectangle(x, y, ribbonSize, size * 1.02, {
+ render: ribbonRender,
+ });
+ const horizontalRibbon = Bodies.rectangle(
+ x,
+ y,
+ size * 1.02,
+ ribbonSize,
+ {
+ render: ribbonRender,
+ },
+ );
+ const bow = Bodies.rectangle(
+ x,
+ y - size * 0.34,
+ ribbonSize * 1.4,
+ ribbonSize * 0.8,
+ {
+ render: ribbonRender,
+ },
+ );
+ const body = Body.create({
+ parts: [base, verticalRibbon, horizontalRibbon, bow],
+ });
+ Body.setPosition(body, { x, y });
+ body.restitution = commonOpts.restitution;
+ body.friction = commonOpts.friction;
+ body.frictionAir = commonOpts.frictionAir;
+ body.frictionStatic = commonOpts.frictionStatic;
+ body.density = commonOpts.density;
+ body.render = {
+ ...body.render,
+ ...commonOpts.render,
+ visible: true,
+ };
+ applyBallPlugin(body, { color, shape: "gift" });
+ return { bodies: [body], constraints: [], blobId: null };
+ };
+
+ const createRectBall = (x, y, color, commonOpts) => {
+ const side = config.ballRadius * 2;
+ const body = Bodies.rectangle(x, y, side, side, {
+ ...commonOpts,
+ chamfer: 0,
+ });
+ applyBallPlugin(body, { color, shape: "rect" });
+ return { bodies: [body], constraints: [], blobId: null };
+ };
+
+ const createCircleBall = (x, y, color, commonOpts) => {
+ const body = Bodies.circle(x, y, config.ballRadius, commonOpts);
+ applyBallPlugin(body, { color, shape: "circle" });
+ return { bodies: [body], constraints: [], blobId: null };
+ };
+
+ const ballShapeFactories = {
+ gift: createGiftBall,
+ rect: createRectBall,
+ circle: createCircleBall,
+ };
+
+ const createBallBodies = (x, y, color) => {
+ const scene = getCurrentScene();
+ const debugSpawn = !!scene?.config?.debugSpawn;
+ const commonOpts = getCommonBallOpts(scene, color);
+ if (scene?.config?.blobBalls === "soft") {
+ return createSoftBlob(x, y, color, commonOpts);
+ }
+ if (scene?.config?.blobBalls === "jagged") {
+ return createJaggedBall(x, y, color, commonOpts);
+ }
+ const shape = scene?.config?.ballShape || "circle";
+ const factory = ballShapeFactories[shape] || createCircleBall;
+ return factory(x, y, color, commonOpts, scene, debugSpawn);
+ };
+
+ return {
+ createBallBodies,
+ applyBallPlugin,
+ getCommonBallOpts,
+ };
+ };
+
+ window.PhysilinksSpawnBalls = { create };
+})();
diff --git a/src/spawn.js b/src/spawn.js
index aeed323..b4d9022 100644
--- a/src/spawn.js
+++ b/src/spawn.js
@@ -1,5 +1,5 @@
(() => {
- const { World, Bodies, Composites, Query, Body } = Matter;
+ const { World, Bodies, Query, Body } = Matter;
const create = ({
config,
@@ -117,173 +117,10 @@
config.ballRadius = nextRadius;
};
- const applyBallPlugin = (body, { color, shape, blobId } = {}) => {
- body.plugin = {
- color,
- hasEntered: false,
- entryCheckId: null,
- };
- if (shape) body.plugin.shape = shape;
- if (blobId) body.plugin.blobId = blobId;
- };
-
- const getCommonBallOpts = (scene, color) => {
- const ballPhysics = scene?.config?.ballPhysics || {};
- return {
- restitution: ballPhysics.restitution ?? 0.72,
- friction: ballPhysics.friction ?? 0.01,
- frictionAir: ballPhysics.frictionAir ?? 0.012,
- frictionStatic: ballPhysics.frictionStatic ?? 0,
- density: ballPhysics.density ?? 0.001,
- render: {
- fillStyle: color,
- strokeStyle: "#0b1222",
- lineWidth: 2,
- },
- };
- };
-
- const createSoftBlob = (x, y, color, commonOpts) => {
- const cols = 3;
- const rows = 2;
- const radius = Math.max(10, config.ballRadius * 0.55);
- const soft = Composites.softBody(
- x - cols * radius * 1.2,
- y - rows * radius * 1.2,
- cols,
- rows,
- 0,
- 0,
- true,
- radius,
- commonOpts,
- );
- const blobId = `blob-${Date.now()}-${Math.random()
- .toString(16)
- .slice(2)}`;
- soft.bodies.forEach((b) => applyBallPlugin(b, { color, blobId }));
- soft.constraints.forEach((c) => {
- c.plugin = { blobId, blobConstraint: true };
- c.render = c.render || {};
- c.render.type = "line";
- });
- return { bodies: soft.bodies, constraints: soft.constraints, blobId };
- };
-
- const createJaggedBall = (x, y, color, commonOpts) => {
- const points = [];
- const segments = 6;
- for (let i = 0; i < segments; i += 1) {
- const angle = Math.min((i / segments) * Math.PI * 2, Math.PI * 2);
- const variance = 0.6 + Math.random() * 0.5;
- const r = config.ballRadius * variance;
- points.push({
- x: x + Math.cos(angle) * r,
- y: y + Math.sin(angle) * r,
- });
- }
- const body = Bodies.fromVertices(x, y, [points], commonOpts, true);
- applyBallPlugin(body, { color, shape: "jagged" });
- return { bodies: [body], constraints: [], blobId: null };
- };
-
- const createGiftBall = (x, y, color, commonOpts, scene, debugSpawn) => {
- const size = config.ballRadius * 2;
- const ribbonSize = Math.max(4, config.ballRadius * 0.35);
- const ribbonColor = scene?.config?.giftRibbonColor || "#f8fafc";
- if (debugSpawn) {
- console.log("Spawn gift", {
- sceneId: scene?.id,
- x,
- y,
- color,
- size,
- });
- }
- const base = Bodies.rectangle(x, y, size, size, {
- ...commonOpts,
- chamfer: { radius: Math.max(2, config.ballRadius * 0.18) },
- });
- const ribbonRender = {
- fillStyle: ribbonColor,
- strokeStyle: "#0b1222",
- lineWidth: 2,
- };
- const verticalRibbon = Bodies.rectangle(x, y, ribbonSize, size * 1.02, {
- render: ribbonRender,
- });
- const horizontalRibbon = Bodies.rectangle(
- x,
- y,
- size * 1.02,
- ribbonSize,
- {
- render: ribbonRender,
- },
- );
- const bow = Bodies.rectangle(
- x,
- y - size * 0.34,
- ribbonSize * 1.4,
- ribbonSize * 0.8,
- {
- render: ribbonRender,
- },
- );
- const body = Body.create({
- parts: [base, verticalRibbon, horizontalRibbon, bow],
- });
- Body.setPosition(body, { x, y });
- body.restitution = commonOpts.restitution;
- body.friction = commonOpts.friction;
- body.frictionAir = commonOpts.frictionAir;
- body.frictionStatic = commonOpts.frictionStatic;
- body.density = commonOpts.density;
- body.render = {
- ...body.render,
- ...commonOpts.render,
- visible: true,
- };
- applyBallPlugin(body, { color, shape: "gift" });
- return { bodies: [body], constraints: [], blobId: null };
- };
-
- const createRectBall = (x, y, color, commonOpts) => {
- const side = config.ballRadius * 2;
- const body = Bodies.rectangle(x, y, side, side, {
- ...commonOpts,
- chamfer: 0,
- });
- applyBallPlugin(body, { color, shape: "rect" });
- return { bodies: [body], constraints: [], blobId: null };
- };
-
- const createCircleBall = (x, y, color, commonOpts) => {
- const body = Bodies.circle(x, y, config.ballRadius, commonOpts);
- applyBallPlugin(body, { color, shape: "circle" });
- return { bodies: [body], constraints: [], blobId: null };
- };
-
- const ballShapeFactories = {
- gift: createGiftBall,
- rect: createRectBall,
- circle: createCircleBall,
- };
-
- const createBallBodies = (x, y, color) => {
- const scene = getCurrentScene();
- const debugSpawn = !!scene?.config?.debugSpawn;
- const commonOpts = getCommonBallOpts(scene, color);
- if (scene?.config?.blobBalls === "soft") {
- return createSoftBlob(x, y, color, commonOpts);
- }
- if (scene?.config?.blobBalls === "jagged") {
- return createJaggedBall(x, y, color, commonOpts);
- }
- const shape = scene?.config?.ballShape || "circle";
- const factory = ballShapeFactories[shape] || createCircleBall;
- return factory(x, y, color, commonOpts, scene, debugSpawn);
- };
+ const createBallBodies = window.PhysilinksSpawnBalls?.create({
+ config,
+ getCurrentScene,
+ })?.createBallBodies;
const spawnBall = () => {
if (isGameOver()) return;