Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bad5d30
Give each cross-module value one owner
ParthJadhav Nov 7, 2026
038a45c
Describe sound loops in one table the engine reads
ParthJadhav Nov 7, 2026
985f18a
Let the overlay own its damage and fx canvases
ParthJadhav Nov 7, 2026
6fd9b5c
Re-read a decal target's device scale when its canvas resizes
ParthJadhav Nov 7, 2026
8a2c577
Remove dead decals, an unused setter, and constant wood knobs
ParthJadhav Nov 7, 2026
d284420
Register icon bounds next to each drawing
ParthJadhav Nov 7, 2026
44c525c
Define the official tool order once
ParthJadhav Nov 7, 2026
f3cb491
Run React, Vue and the custom element from one toolbar session
ParthJadhav Nov 7, 2026
935abb1
Keep frame-rate policy in the performance monitor
ParthJadhav Nov 7, 2026
081078a
Remove comments that only restate the code
ParthJadhav Nov 7, 2026
b652c58
Drop the pointer-input lcov exemption
ParthJadhav Nov 7, 2026
f70a5c0
Make tests fail on regressions and only then
ParthJadhav Nov 7, 2026
cf2c10d
Read the effects layer at its own pixel ratio in snapshots
ParthJadhav Nov 7, 2026
918e038
Use the overlay's own dispose and transform reset
ParthJadhav Nov 7, 2026
823e4b6
Treat only the sentinel as a parked pointer
ParthJadhav Nov 7, 2026
a1cdfdd
Keep the Vue toolbar's style tag attribute
ParthJadhav Nov 7, 2026
7eec8dd
Stop the singularity muting the gravity gun's hum
ParthJadhav Nov 7, 2026
ddb6e3f
Derive the quality profile from the tier
ParthJadhav Nov 7, 2026
4f65291
Document the icon bounds once and the void paint accurately
ParthJadhav Nov 7, 2026
94df1ef
Use isParked in the remaining tool ticks
ParthJadhav Nov 7, 2026
88adf8b
Restore three comments that explain a rule, not the code
ParthJadhav Nov 7, 2026
6bbb815
Drop rateCap60; tests read the frame interval it decides
ParthJadhav Nov 7, 2026
d826436
Let the overlay draw its decals into a snapshot
ParthJadhav Nov 7, 2026
7a8d849
Test toolbar session teardown; make its onClose optional
ParthJadhav Nov 7, 2026
fff2af0
Remove the unused damageReady getter; clean up the session test
ParthJadhav Nov 7, 2026
9a5903b
Clear dev-dependency advisories so the CI audit step passes
ParthJadhav Nov 7, 2026
fc3d84c
Keep the generic loop voice for names outside the voice table
ParthJadhav Nov 7, 2026
eb74b2c
Load Vue after the DOM harness in the adapter tests
ParthJadhav Nov 7, 2026
5c37134
Accept live capture in the browser runtime suite
ParthJadhav Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 44 additions & 124 deletions bun.lock

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@
"access": "public"
},
"overrides": {
"vite": "6.4.3"
"vite": "6.4.5",
"launch-editor": "^2.14.2",
"shell-quote": "^1.11.0",
"source-map-js": "^1.2.2",
"vue": "3.5.44"
},
"scripts": {
"build": "tsdown",
Expand Down Expand Up @@ -208,20 +212,20 @@
"@biomejs/biome": "^2.5.11",
"@changesets/changelog-github": "^1.0.0",
"@changesets/cli": "^3.0.1",
"@happy-dom/global-registrator": "^20.11.15",
"@happy-dom/global-registrator": "^21.0.4",
"@napi-rs/canvas": "^1.0.8",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"happy-dom": "^20.11.15",
"happy-dom": "^21.0.4",
"html-to-image": "^1.11.13",
"playwright-core": "^1.62.1",
"publint": "^0.3.24",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"tsdown": "^0.22.14",
"typescript": "^7.0.2",
"vite": "6.4.3",
"vite": "6.4.5",
"vitepress": "^1.6.4",
"vue": "^3.5.42"
"vue": "3.5.44"
}
}
6 changes: 5 additions & 1 deletion scripts/browser-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ const checks = [
name: "the live DOM is captured into a destructible surface",
async run(run) {
const status = await run("__dd.engine.captureStatus");
assert(status === "snapshot", `capture status was "${status}", expected "snapshot"`);
// Chrome 157+ ships the live capture API, so "auto" mode may pick it.
assert(
status === "snapshot" || status === "live",
`capture status was "${status}", expected "snapshot" or "live"`,
);
assert(await run("Boolean(__dd.engine.content)"), "no content layer after capture");
assert(
await run("__dd.engine.pageOpacityAt(400, 400) === 1"),
Expand Down
4 changes: 1 addition & 3 deletions scripts/check-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ const BROWSER_ONLY = new Map([
* the global line ratchet enforced; skip only the corrupted per-file line
* value until Bun merges instances correctly.
*/
const BUN_LCOV_LINE_EXEMPT = new Map([
["src/pointer-input.ts", "direct unit and engine tests; duplicated transpiled-module line map"],
]);
const BUN_LCOV_LINE_EXEMPT = new Map();

function parseLcov(text) {
const files = [];
Expand Down
1 change: 1 addition & 0 deletions src/advanced-toolart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export const stickyBombArt: ToolArtFn = (ctx, state) => {
ctx.restore();
};

// Measured icon silhouettes; see the note in ./toolart/base.ts.
registerToolIconBounds(gravityGunArt, [40, 45, 140, 150]);
registerToolIconBounds(laserCutterArt, [34, 40, 135, 140]);
registerToolIconBounds(acidSprayerArt, [55, 57, 119, 139]);
Expand Down
4 changes: 2 additions & 2 deletions src/advanced-tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { acidSprayerArt, gravityGunArt, laserCutterArt, stickyBombArt } from "./advanced-toolart";
import { TAU } from "./math";
import { PARKED_POINTER, TAU } from "./math";
import { createEngineState } from "./tool-kit";
import { includeTopologySegment, surfaceRuns, type TopologyBounds } from "./topology";
import type { RageLayerEngineApi, Tool } from "./types";
Expand Down Expand Up @@ -445,7 +445,7 @@ export const stickyBombs: Tool = {
}
},
backgroundTick(engine, dt) {
stickyBombs.tick?.(engine, dt, false, { x: -1000, y: -1000 });
stickyBombs.tick?.(engine, dt, false, { x: PARKED_POINTER, y: PARKED_POINTER });
},
};

Expand Down
76 changes: 49 additions & 27 deletions src/audio.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
import type { SoundApi } from "./types";

type LoopName = Parameters<SoundApi["loop"]>[0];

interface LoopVoice {
filter: BiquadFilterType;
frequency: number;
q?: number;
playbackRate?: number;
/** Re-randomise the filter frequency within `[base, base + span)` on every update. */
jitter?: { base: number; span: number };
/** Sounds only while a tool is held, so a released or cancelled gesture fades it. */
held: boolean;
}

/** How each continuous loop is filtered out of the shared noise buffer. */
const LOOP_VOICES: Record<LoopName, LoopVoice> = {
// Random crackle keeps the fire organic.
fire: {
filter: "lowpass",
frequency: 500,
playbackRate: 0.35,
jitter: { base: 350, span: 500 },
held: false,
},
water: { filter: "highpass", frequency: 2500, held: true },
saw: { filter: "bandpass", frequency: 1400, q: 6, playbackRate: 1.6, held: true },
flamethrower: { filter: "bandpass", frequency: 700, q: 0.4, playbackRate: 0.6, held: true },
// Sub-bass rumble: a singularity should be felt more than heard.
void: { filter: "lowpass", frequency: 110, q: 8, playbackRate: 0.2, held: true },
};

/** The loops a held tool drives, as opposed to the ambient crackle of a burning page. */
export const HELD_TOOL_LOOPS = (Object.keys(LOOP_VOICES) as LoopName[]).filter(
(name) => LOOP_VOICES[name].held,
);

/**
* Fully procedural sound effects — no audio assets, so the package stays a
* zero-download dependency. Everything is synthesized from noise buffers and
Expand Down Expand Up @@ -293,37 +328,20 @@ export class SoundEngine implements SoundApi {
}
const ctx = this.ensure();
if (!ctx || !this.master || !this.noiseBuffer) return;
// `loop` is reachable from untyped custom tools; an unknown name keeps the
// generic saw voice it always fell back to instead of throwing mid-frame.
const voice = LOOP_VOICES[name] ?? LOOP_VOICES.saw;
if (!entry) {
const source = ctx.createBufferSource();
source.buffer = this.noiseBuffer;
source.loop = true;
const filter = ctx.createBiquadFilter();
const gain = ctx.createGain();
gain.gain.value = 0;
if (name === "fire") {
filter.type = "lowpass";
filter.frequency.value = 500;
source.playbackRate.value = 0.35;
} else if (name === "water") {
filter.type = "highpass";
filter.frequency.value = 2500;
} else if (name === "void") {
// Sub-bass rumble: a singularity should be felt more than heard.
filter.type = "lowpass";
filter.frequency.value = 110;
filter.Q.value = 8;
source.playbackRate.value = 0.2;
} else if (name === "flamethrower") {
filter.type = "bandpass";
filter.frequency.value = 700;
filter.Q.value = 0.4;
source.playbackRate.value = 0.6;
} else {
filter.type = "bandpass";
filter.frequency.value = 1400;
filter.Q.value = 6;
source.playbackRate.value = 1.6;
}
filter.type = voice.filter;
filter.frequency.value = voice.frequency;
if (voice.q !== undefined) filter.Q.value = voice.q;
if (voice.playbackRate !== undefined) source.playbackRate.value = voice.playbackRate;
source.connect(filter).connect(gain).connect(this.master);
source.start();
entry = { source, gain, filter, stopTimer: null };
Expand All @@ -334,9 +352,13 @@ export class SoundEngine implements SoundApi {
entry.stopTimer = null;
}
entry.gain.gain.setTargetAtTime(target, ctx.currentTime, 0.08);
if (name === "fire") {
// Random crackle: jitter the filter to keep the loop organic.
entry.filter.frequency.setTargetAtTime(350 + Math.random() * 500, ctx.currentTime, 0.05);
const { jitter } = voice;
if (jitter) {
entry.filter.frequency.setTargetAtTime(
jitter.base + Math.random() * jitter.span,
ctx.currentTime,
0.05,
);
}
}

Expand Down
10 changes: 0 additions & 10 deletions src/bugs-sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import { TAU } from "./math";
import type { ContentApi, Flame, Particle, SoundApi } from "./types";

/** Population cap. */
const MAX_BUGS = 36;

/** One crawling bug: position, heading, and an appetite timer. */
Expand Down Expand Up @@ -91,7 +90,6 @@ export class BugSwarm {
seed: Math.random() * TAU,
});
}
// At the population cap nothing was released — so nothing chirps either.
if (this.bugs.length === before) return false;
host.sound.pop();
return true;
Expand All @@ -107,8 +105,6 @@ export class BugSwarm {
if (dx * dx + dy * dy > r2) continue;
this.bugs.splice(i, 1);
squashed++;
// The smear a squashed bug leaves. Drawn through surfaceCtx so it
// persists like any other decal.
const ctx = host.surfaceCtx;
ctx.save();
// The smear is on the page, so it clips to the page — a bug squashed at
Expand Down Expand Up @@ -152,7 +148,6 @@ export class BugSwarm {
if (dx * dx + dy * dy > r2) continue;
this.bugs.splice(i, 1);
flushed++;
// The bug itself, tumbling downstream and off the heap...
host.spawnParticle({
kind: "debris",
x: b.x,
Expand All @@ -168,7 +163,6 @@ export class BugSwarm {
bounce: 0.2,
restY: b.y + 180 + Math.random() * 220,
});
// ...in a burst of the water that took it.
for (let s = 0; s < 5; s++) {
const a = Math.random() * TAU;
host.spawnParticle({
Expand Down Expand Up @@ -242,7 +236,6 @@ export class BugSwarm {
this.bugs.splice(i, 1);
continue;
}
// Skittering: constant jitter plus an occasional decisive turn.
b.turn -= dt;
if (b.turn <= 0) {
b.turn = 0.5 + Math.random() * 1.6;
Expand All @@ -251,7 +244,6 @@ export class BugSwarm {
b.a += (Math.random() - 0.5) * 3.4 * dt;
b.x += Math.cos(b.a) * b.speed * dt;
b.y += Math.sin(b.a) * b.speed * dt;
// Turn back at the page edge instead of wandering into the void.
if (b.x < 4 || b.x > host.width - 4 || b.y < 4 || b.y > host.height - 4) {
b.a += Math.PI;
b.x = Math.max(4, Math.min(host.width - 4, b.x));
Expand Down Expand Up @@ -284,7 +276,6 @@ export class BugSwarm {
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.a + wiggle * 0.2);
// Legs first, three per side, alternating with the gait.
ctx.strokeStyle = "rgba(20, 14, 8, 0.9)";
ctx.lineWidth = 0.8;
ctx.beginPath();
Expand All @@ -297,7 +288,6 @@ export class BugSwarm {
ctx.lineTo(lx - phase * b.size * 0.4, -b.size * 0.9);
}
ctx.stroke();
// Two body segments and a head.
ctx.fillStyle = "rgba(38, 26, 14, 0.95)";
ctx.beginPath();
ctx.ellipse(-b.size * 0.35, 0, b.size * 0.62, b.size * 0.45, 0, 0, TAU);
Expand Down
12 changes: 3 additions & 9 deletions src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,9 @@ export class ContentLayer {
return;
}

// Inset roots still need composition: backdrop across the whole layer,
// then the raster at the root's document offset. Anything outside the
// root's box (body margins, a reserved scrollbar gutter) keeps the page
// backdrop instead of falling through to the void.
// Inset roots: anything outside the root's box (body margins, a reserved
// scrollbar gutter) keeps the page backdrop instead of falling through to
// the void.
const snap = document.createElement("canvas");
snap.width = pixelWidth;
snap.height = pixelHeight;
Expand Down Expand Up @@ -571,7 +570,6 @@ export class ContentLayer {
ctx.globalCompositeOperation = "source-over";
}
ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
// Every pixel in the band was replaced; upload exactly that band.
this.renderer.markDirty(0, y0, w, y1);
}

Expand Down Expand Up @@ -650,7 +648,6 @@ export class ContentLayer {
rect.h = h;
}

/** Drop the live buffers, releasing their backing stores immediately. */
private releaseLayers() {
for (const canvas of [this.wounds, this.decals]) {
if (!canvas) continue;
Expand Down Expand Up @@ -898,8 +895,6 @@ export class ContentLayer {
}

/**
* Remove `path` from the content.
*
* The randomised geometry is built into a `Path2D` once and replayed against
* both targets, so the visible canvas (immediate feedback) and the wound mask
* (which survives a live refresh) can never drift apart.
Expand All @@ -926,7 +921,6 @@ export class ContentLayer {
if (!this.ready) return;
const path = new Path2D();
path.arc(x, y, r, 0, TAU);
// Ragged rim bites, batched into the same path as the core.
for (let i = 0; i < 7; i++) {
const a = Math.random() * TAU;
const d = r * rand(0.7, 1.1);
Expand Down
Loading
Loading