Camera view frame, viewport clipping, overlay layer for editor symbols

- Camera assets get width/height properties; play mode renders only the
  camera frame (letterboxed, masked, culled) and nothing without a camera
- Remove demo arrow-key/camera-follow systems from play mode
- Clip world and gizmos to the viewport (selection frame no longer leaks
  over the left panel)
- Blur focused widget on pointerdown outside it so inspector edits commit
  before selection changes
- Separate objects/overlay layers so camera symbols always render on top
- Add tools/seed-demo.mjs for reproducible E2E runs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 11:13:08 +02:00
1 parent 5a46124454
commit aaa8154adf
11 files changed
+307 -142

No files matched your search

-25
View File
@@ -1,25 +0,0 @@
import type { Entity } from '../Entity';
import type { World } from '../World';
import type { System } from './System';
/**
* Makes the first camera entity follow a target entity (smoothly). If the scene has no
* camera the viewport falls back to a fixed view at the origin.
*/
export class CameraFollowSystem implements System {
private camera: Entity | undefined;
constructor(private readonly target: () => Entity | undefined, private readonly smoothing = 8) {}
start(world: World): void {
this.camera = world.firstOfType('camera');
}
update(_world: World, dt: number): void {
const target = this.target();
if (!this.camera || !target) return;
const t = 1 - Math.exp(-this.smoothing * dt);
this.camera.x += (target.x - this.camera.x) * t;
this.camera.y += (target.y - this.camera.y) * t;
}
}
@@ -1,41 +0,0 @@
import type { Entity } from '../Entity';
import type { World } from '../World';
import { Input } from './Input';
import type { System } from './System';
/**
* Phase-1 demo behaviour: the arrow keys (or WASD) move one sprite entity. Which one is
* decided at start: the entity of the scene object that was selected when Play was
* pressed, otherwise the first sprite in the scene.
*/
export class PlayerControlSystem implements System {
private readonly input = new Input();
private player: Entity | undefined;
constructor(private readonly preferredObjectId: string | null, private readonly speed = 240) {}
start(world: World): void {
this.input.attach();
const preferred = this.preferredObjectId ? world.findByObjectId(this.preferredObjectId) : undefined;
this.player = preferred && preferred.type === 'sprite' ? preferred : world.firstOfType('sprite');
}
update(_world: World, dt: number): void {
if (!this.player) return;
const dx = this.input.axis('ArrowLeft', 'ArrowRight') || this.input.axis('a', 'd');
const dy = this.input.axis('ArrowUp', 'ArrowDown') || this.input.axis('w', 's');
if (dx === 0 && dy === 0) return;
const length = Math.hypot(dx, dy);
this.player.x += (dx / length) * this.speed * dt;
this.player.y += (dy / length) * this.speed * dt;
}
stop(): void {
this.input.detach();
this.player = undefined;
}
get controlled(): Entity | undefined {
return this.player;
}
}