diff --git a/README.md b/README.md
index e9e416c..9639549 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Physilinks is a browser-based physics linking game built with Matter.js. Match a
## Tech notes
- **Engine**: Matter.js (via CDN). Canvas rendering with custom overlays for HUD, pause, game over, and score popups.
-- **Scenes**: Defined in `main.js` as an array of presets (config + `createBodies` factory). Applying a scene updates gravity, spawn rate, ball radius, palette, and static bodies, then restarts the game.
+- **Scenes**: Defined in `main.js` as an array of presets (config + `createBodies` factory). Applying a scene updates gravity, spawn rate, ball radius, palette, link constraints (stiffness/stretch/damping/width), and static bodies, then restarts the game.
- **Physics entities**: Falling balls (`Bodies.circle`) with gentle restitution/friction; static boundaries/obstacles per scene. The top is open; sides and floor are static bodies.
- **Input**: Pointer/touch events mapped to scene coords; chain state tracks bodies and a dashed preview line to the pointer. Undo by dragging back to the previous node.
- **Scoring**: `10 × length²` per cleared chain. Score popup rendered as DOM element near release point.
@@ -22,5 +22,5 @@ Physilinks is a browser-based physics linking game built with Matter.js. Match a
## Development quick start
- No build step. Open `index.html` directly in the browser.
-- Key files: `index.html` (layout/styles), `main.js` (game logic).
+- Key files: `index.html` (layout), `styles.css` (styling), `main.js` (game logic).
- Adjust or add scenes in `main.js` by extending the `scenes` array with config and a `createBodies(width, height)` function.
diff --git a/index.html b/index.html
index 3968c12..3becc5b 100644
--- a/index.html
+++ b/index.html
@@ -10,289 +10,8 @@
href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;700&display=swap"
rel="stylesheet"
/>
-
+
+
diff --git a/main.js b/main.js
index 807aab9..4c61321 100644
--- a/main.js
+++ b/main.js
@@ -17,6 +17,7 @@
minChain: 3,
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
ballRadius: 18,
+ link: { stiffness: 0.9, lengthScale: 1, damping: 0.05, lineWidth: 3 },
};
const scenes = [
@@ -24,11 +25,12 @@
id: "scene1",
name: "Balanced (default)",
config: {
- gravity: 0.88,
- spawnIntervalMs: 720,
+ gravity: 1,
+ spawnIntervalMs: 520,
minChain: 3,
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
- ballRadius: 38,
+ ballRadius: 18,
+ link: { stiffness: 0.9, lengthScale: 1, damping: 0.05, lineWidth: 3 },
},
createBodies: (w, h) => [
Bodies.rectangle(w / 2, h + 40, w, 80, {
@@ -56,6 +58,7 @@
minChain: 3,
palette: ["#fb7185", "#fbbf24", "#34d399", "#38bdf8"],
ballRadius: 22,
+ link: { stiffness: 0.6, lengthScale: 1.2, damping: 0.01, lineWidth: 4 },
},
createBodies: (w, h) => [
Bodies.rectangle(w / 2, h + 50, w, 100, {
@@ -87,6 +90,7 @@
minChain: 3,
palette: ["#e879f9", "#38bdf8", "#f97316", "#22c55e"],
ballRadius: 16,
+ link: { stiffness: 1, lengthScale: 0.85, damping: 0.15, lineWidth: 3 },
},
createBodies: (w, h) => {
const bodies = [
@@ -211,6 +215,7 @@
currentScene = next;
if (sceneSelectEl) sceneSelectEl.value = next.id;
Object.assign(config, next.config);
+ config.link = { ...next.config.link };
engine.gravity.y = config.gravity;
highScore = loadHighScore(next.id);
rebuildSceneBodies();
@@ -353,14 +358,16 @@
const addToChain = (body) => {
const last = chain.bodies[chain.bodies.length - 1];
const dist = Vector.magnitude(Vector.sub(last.position, body.position));
+ const linkCfg = config.link || {};
const constraint = Constraint.create({
bodyA: last,
bodyB: body,
- length: dist,
- stiffness: 0.9,
+ length: dist * (linkCfg.lengthScale ?? 1),
+ stiffness: linkCfg.stiffness ?? 0.9,
+ damping: linkCfg.damping ?? 0,
render: {
strokeStyle: chain.color,
- lineWidth: 3,
+ lineWidth: linkCfg.lineWidth ?? 3,
},
});
chain.constraints.push(constraint);
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..5fb5c5b
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,238 @@
+:root {
+ --bg: #0f172a;
+ --panel: #111827;
+ --accent: #22d3ee;
+ --text: #e2e8f0;
+ --muted: #94a3b8;
+}
+* { box-sizing: border-box; }
+body {
+ margin: 0;
+ font-family: 'Manrope', system-ui, -apple-system, sans-serif;
+ background: radial-gradient(circle at 25% 20%, rgba(56,189,248,0.12), transparent 25%),
+ radial-gradient(circle at 80% 10%, rgba(167,139,250,0.16), transparent 30%),
+ radial-gradient(circle at 40% 80%, rgba(52,211,153,0.12), transparent 25%),
+ var(--bg);
+ color: var(--text);
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+.shell {
+ width: min(1100px, 100%);
+ background: rgba(17, 24, 39, 0.72);
+ border: 1px solid rgba(148, 163, 184, 0.1);
+ box-shadow: 0 20px 60px rgba(0,0,0,0.35);
+ border-radius: 18px;
+ overflow: hidden;
+ position: relative;
+ backdrop-filter: blur(10px);
+}
+header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 18px;
+ background: rgba(255, 255, 255, 0.02);
+ border-bottom: 1px solid rgba(148, 163, 184, 0.08);
+}
+header h1 {
+ font-size: 20px;
+ margin: 0;
+ letter-spacing: 0.5px;
+}
+header .meta {
+ display: flex;
+ gap: 14px;
+ align-items: center;
+ font-size: 13px;
+ color: var(--muted);
+}
+header .pill {
+ padding: 6px 10px;
+ border-radius: 8px;
+ background: rgba(34, 211, 238, 0.12);
+ color: #67e8f9;
+ border: 1px solid rgba(34, 211, 238, 0.35);
+ font-weight: 700;
+ font-size: 12px;
+ letter-spacing: 0.6px;
+ text-transform: uppercase;
+}
+.pause-btn {
+ background: rgba(34, 211, 238, 0.14);
+ color: #67e8f9;
+ border: 1px solid rgba(34, 211, 238, 0.4);
+ border-radius: 10px;
+ padding: 8px 12px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: transform 120ms ease, filter 120ms ease;
+}
+.pause-btn:hover {
+ filter: brightness(1.05);
+ transform: translateY(-1px);
+}
+.pause-btn:active {
+ transform: translateY(0);
+}
+#scene-wrapper {
+ position: relative;
+ width: 100%;
+ height: 680px;
+ background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.02), transparent 40%),
+ radial-gradient(circle at 75% 60%, rgba(255,255,255,0.02), transparent 45%),
+ #0b1222;
+}
+canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+.hud-bar {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ padding: 12px 16px;
+ background: rgba(255, 255, 255, 0.03);
+ border-top: 1px solid rgba(148, 163, 184, 0.08);
+}
+.hud-bar .card {
+ background: rgba(255, 255, 255, 0.04);
+ padding: 10px 12px;
+ border: 1px solid rgba(148, 163, 184, 0.14);
+ border-radius: 10px;
+ font-size: 13px;
+ color: var(--muted);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.hud-bar .card strong {
+ color: var(--text);
+ font-size: 13px;
+}
+.selector {
+ background: rgba(0, 0, 0, 0.25);
+ color: var(--text);
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ border-radius: 8px;
+ padding: 6px 8px;
+ font-size: 13px;
+}
+.legend {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ background: rgba(255, 255, 255, 0.04);
+ padding: 10px 12px;
+ border: 1px solid rgba(148, 163, 184, 0.14);
+ border-radius: 10px;
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ align-items: center;
+ pointer-events: none;
+}
+.legend span {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ display: inline-block;
+}
+.pause-overlay {
+ position: absolute;
+ top: 12px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ color: #e2e8f0;
+ padding: 8px 14px;
+ border-radius: 12px;
+ font-weight: 800;
+ letter-spacing: 0.5px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 160ms ease;
+}
+.pause-overlay.visible {
+ opacity: 1;
+}
+.game-over {
+ position: absolute;
+ inset: 0;
+ background: rgba(10, 13, 25, 0.72);
+ backdrop-filter: blur(8px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 200ms ease;
+}
+.game-over.visible {
+ pointer-events: auto;
+ opacity: 1;
+}
+.game-over__card {
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ border-radius: 14px;
+ padding: 20px 24px;
+ text-align: center;
+ min-width: 240px;
+ box-shadow: 0 12px 35px rgba(0, 0, 0, 0.35);
+}
+.game-over__card .title {
+ font-size: 22px;
+ font-weight: 700;
+ margin-bottom: 10px;
+}
+.game-over__card .score-line {
+ font-size: 14px;
+ color: var(--muted);
+ margin-bottom: 14px;
+}
+.game-over__card button {
+ background: linear-gradient(135deg, #22d3ee, #0ea5e9);
+ border: none;
+ color: #0b1222;
+ font-weight: 700;
+ padding: 10px 16px;
+ border-radius: 10px;
+ cursor: pointer;
+ font-size: 14px;
+}
+.game-over__card button:hover {
+ filter: brightness(1.08);
+}
+.floating-score {
+ position: absolute;
+ color: #e0f2fe;
+ font-weight: 800;
+ font-size: 18px;
+ pointer-events: none;
+ text-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
+ animation: floatUp 900ms ease-out forwards;
+}
+@keyframes floatUp {
+ 0% { opacity: 1; transform: translate(-50%, 0); }
+ 60% { opacity: 0.9; transform: translate(-50%, -22px); }
+ 100% { opacity: 0; transform: translate(-50%, -38px); }
+}
+.instructions {
+ padding: 14px 18px;
+ font-size: 14px;
+ color: var(--muted);
+ background: rgba(255, 255, 255, 0.03);
+ border-top: 1px solid rgba(148, 163, 184, 0.08);
+ line-height: 1.6;
+}
+@media (max-width: 800px) {
+ #scene-wrapper { height: 520px; }
+ header h1 { font-size: 18px; }
+}