Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions packages/pixel-draw-renderer/.changeset/wild-otters-zoom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jolly-pixel/pixel-draw.renderer": major
---

Extracted zoom state (value, bounds, wheel sensitivity, and delta-stepping math) out of `Viewport` into a new `Zoom` value object, exported from the package. `DefaultViewport.zoom` (and `Viewport.zoom`) is now a `Zoom` instance instead of a plain `number` — use `.zoom.value` for the numeric level and `.zoom.sensitivity` (get/set) instead of the removed `Viewport.zoomSensitivity` accessor pair. `PixelArtCanvas.zoom`/`zoomSensitivity` are unaffected and still return/accept plain numbers. `PixelArtCanvasOptions.zoom` now reuses the exported `ZoomOptions` type instead of an inline duplicate shape; as a result `zoom.default` is no longer required when passing a `zoom` option.
2 changes: 1 addition & 1 deletion packages/pixel-draw-renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Open `http://localhost:5173` to see the interactive demo.

- [`PixelArtCanvas`](./docs/PixelArtCanvas.md): top-level coordinator, the primary public API
- [`Brush`](./docs/tools/Brush.md): brush size, color, opacity, and affected-pixel computation — read/write via `PixelArtCanvas.brush`
- [`PixelBuffer`](./docs/buffer/PixelBuffer.md): headless RGBA pixel storage, usable server-side with no DOM (also documents the `onBufferUpdated`/`applyRemoteCommand` hook events)
- [`PixelBuffer`](./docs/buffer/PixelBuffer.md): headless RGBA pixel storage, usable server-side with no DOM
- [`HistoryStack`](./docs/history/HistoryStack.md): bounded undo/redo stack backing `PixelArtCanvas.undo()`/`redo()`
- [`Keybindings`](./docs/utils/keybindings.md): `Keybindings`/`Keybinding` types, `DEFAULT_KEYBINDINGS`, and the errors thrown by `patchKeybindings()`
- [`Network`](./docs/network/index.md): transport-agnostic, server-authoritative multiplayer for `PixelArtCanvas`
Expand Down
4 changes: 2 additions & 2 deletions packages/pixel-draw-renderer/docs/PixelArtCanvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ The brush instance. Use it to read or change the current brush color, opacity, a
### `viewport`

```ts
readonly viewport: DefaultViewport // { readonly zoom: number; readonly camera: Readonly<Vec2>; }
readonly viewport: DefaultViewport // { readonly zoom: Zoom; readonly camera: Readonly<Vec2>; }
```

Read-only camera/zoom state. Use `camera`/`zoom` for copies, or the methods below for coordinate conversions and mutation.
Read-only camera/zoom state. `viewport.zoom` is a `Zoom` value object (`.value`, `.min`, `.max`, `.sensitivity`), not a plain number — use the top-level `zoom`/`zoomSensitivity` accessors below for the numeric level, or the methods below for coordinate conversions and mutation.

## Methods

Expand Down
148 changes: 64 additions & 84 deletions packages/pixel-draw-renderer/src/PixelArtCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,35 @@ import {
type BrushOptions
} from "./tools/Brush.ts";
import {
BrushController
} from "./tools/BrushController.ts";
ToolControllers
} from "./tools/ToolControllers.ts";
import {
CanvasBuffer
} from "./buffer/CanvasBuffer.ts";
import {
CanvasRenderer
} from "./rendering/CanvasRenderer.ts";
import {
FillController
} from "./tools/FillController.ts";
import {
HistoryController,
type HistoryState
} from "./history/HistoryController.ts";
import {
buildRedoReplayEvents,
buildUndoReplayEvents
} from "./history/replayEvents.ts";
import {
createInputActions
} from "./input/createInputActions.ts";
import {
InputController,
type WindowLike
} from "./input/InputController.ts";
import {
LineController
} from "./tools/LineController.ts";
import {
SelectController
} from "./tools/SelectController.ts";
import {
SvgManager
} from "./rendering/SvgManager.ts";
import {
Viewport,
type DefaultViewport
} from "./rendering/Viewport.ts";
import type {
ZoomOptions
} from "./rendering/Zoom.ts";
import {
SyncController
} from "./sync/SyncController.ts";
Expand Down Expand Up @@ -86,12 +76,7 @@ export interface PixelArtCanvasOptions {
maxSize?: number;
init?: HTMLCanvasElement;
};
zoom?: {
default: number;
sensitivity?: number;
min?: number;
max?: number;
};
zoom?: ZoomOptions;
backgroundTransparency?: {
colors: { odd: string; even: string; };
squareSize: number;
Expand Down Expand Up @@ -140,10 +125,7 @@ export class PixelArtCanvas {
#onDrawEnd?: () => void;
#history: HistoryController;
#mode: Mode;
#brushController: BrushController;
#fillController: FillController;
#lineController: LineController;
#selectController: SelectController;
#tools: ToolControllers;

readonly brush: Brush;
readonly viewport: DefaultViewport;
Expand Down Expand Up @@ -239,41 +221,26 @@ export class PixelArtCanvas {
brush: brushAdapter
});

this.#brushController = new BrushController({
this.#tools = new ToolControllers({
brush: this.brush,
canvasBuffer: this.#canvasBuffer,
renderer: this.#renderer,
onCommit: (pixels, color, beforeColors) => {
linePreview: this.#svgManager.linePreview,
selectionOverlay: this.#svgManager.selection,
eraseColor,
onStrokeCommit: (pixels, color, beforeColors) => {
this.#sync.recordHistory({ action: "stroke", positions: pixels, beforeColors, afterColor: color });
this.#sync.emitHook({ action: "stroke", metadata: { color, positions: pixels } });
this.#onDrawEnd?.();
}
});

this.#fillController = new FillController({
brush: this.brush,
canvasBuffer: this.#canvasBuffer,
onCommit: (pixels) => this.commitPixels(pixels),
onGlobalCommit: ({ positions, beforeColors, fromColor, toColor }) => {
},
onCommitPixels: (pixels) => this.commitPixels(pixels),
onGlobalFillCommit: ({ positions, beforeColors, fromColor, toColor }) => {
this.#sync.applyStroke(toColor, positions);
this.#sync.recordHistory({ action: "stroke", positions, beforeColors, afterColor: toColor });
this.#sync.emitHook({ action: "global-fill", metadata: { fromColor, toColor } });
this.#onDrawEnd?.();
}
});

this.#lineController = new LineController({
brush: this.brush,
linePreview: this.#svgManager.linePreview,
onCommit: (pixels) => this.commitPixels(pixels)
});

this.#selectController = new SelectController({
canvasBuffer: this.#canvasBuffer,
renderer: this.#renderer,
selectionOverlay: this.#svgManager.selection,
eraseColor,
onCommit: (entry) => {
},
onSelectCommit: (entry) => {
this.#sync.recordHistory({
action: "select-edit",
...entry
Expand All @@ -286,21 +253,20 @@ export class PixelArtCanvas {
canvas: this.#renderer.canvas(),
viewport: this.#viewport,
window: options.window,
actions: createInputActions({
getMode: () => this.#mode,
brush: this.brush,
canvasBuffer: this.#canvasBuffer,
renderer: this.#renderer,
svgManager: this.#svgManager,
viewport: this.#viewport,
brushController: this.#brushController,
fillController: this.#fillController,
lineController: this.#lineController,
selectController: this.#selectController,
undo: () => this.undo(),
redo: () => this.redo(),
stopDrawing: () => this.#input.stopDrawing()
}),
actions: {
...createInputActions({
getMode: () => this.#mode,
brush: this.brush,
canvasBuffer: this.#canvasBuffer,
renderer: this.#renderer,
svgManager: this.#svgManager,
viewport: this.#viewport,
tools: this.#tools,
stopDrawing: () => this.#input.stopDrawing()
}),
onUndo: () => this.undo(),
onRedo: () => this.redo()
},
keybindings: options.keybindings
});

Expand All @@ -317,24 +283,24 @@ export class PixelArtCanvas {
this.#mode = mode;
if (mode === "move") {
this.#svgManager.brushHighlight.hide();
this.#lineController.cancelIfArmed();
this.#tools.line.cancelIfArmed();
}
if (mode !== "select") {
this.#selectController.clear();
this.#tools.select.clear();
}
}

/**
* Whether fill recolors all matching pixels instead of only the connected region.
*/
get fillGlobal(): boolean {
return this.#fillController.global;
return this.#tools.fill.global;
}

set fillGlobal(
global: boolean
) {
this.#fillController.global = global;
this.#tools.fill.global = global;
}

get parentHtmlElement(): HTMLDivElement {
Expand Down Expand Up @@ -400,17 +366,17 @@ export class PixelArtCanvas {
}

get zoom(): number {
return this.#viewport.zoom;
return this.#viewport.zoom.value;
}

get zoomSensitivity(): number {
return this.#viewport.zoomSensitivity;
return this.#viewport.zoom.sensitivity;
}

set zoomSensitivity(
sensitivity: number
) {
this.#viewport.zoomSensitivity = sensitivity;
this.#viewport.zoom.sensitivity = sensitivity;
}

/**
Expand All @@ -430,21 +396,21 @@ export class PixelArtCanvas {
* Rotates the active selection clockwise. Returns `false` without a selection.
*/
rotateSelection(): boolean {
return this.#selectController.handleRotate();
return this.#tools.select.handleRotate();
}

/**
* Mirrors the active selection horizontally. Returns `false` without a selection.
*/
flipSelectionHorizontal(): boolean {
return this.#selectController.handleFlipHorizontal();
return this.#tools.select.handleFlipHorizontal();
}

/**
* Mirrors the active selection vertically. Returns `false` without a selection.
*/
flipSelectionVertical(): boolean {
return this.#selectController.handleFlipVertical();
return this.#tools.select.handleFlipVertical();
}

centerTexture(): void {
Expand Down Expand Up @@ -535,16 +501,28 @@ export class PixelArtCanvas {
}

const color = toRGBA(this.brush.colorAsString());
const beforeColors = this.#history.enabled ? this.#canvasBuffer.samplePixels(pixels) : [];
const beforeColors = this.#history.enabled ?
this.#canvasBuffer.samplePixels(pixels) :
[];

this.#sync.applyStroke(color, pixels);

this.#sync.recordHistory({ action: "stroke", positions: pixels, beforeColors, afterColor: color });
this.#sync.emitHook({ action: "stroke", metadata: { color, positions: pixels } });
this.#sync.recordHistory({
action: "stroke",
positions: pixels,
beforeColors,
afterColor: color
});
this.#sync.emitHook({
action: "stroke",
metadata: { color, positions: pixels }
});
this.#onDrawEnd?.();
}

/** Reverts the latest local edit. Returns `false` when history is unavailable. */
/**
* Reverts the latest local edit. Returns `false` when history is unavailable.
**/
undo(): boolean {
const entry = this.#history.undo();
if (!entry) {
Expand All @@ -553,17 +531,19 @@ export class PixelArtCanvas {

this.#refreshAfterHistoryApply();
if (entry.action === "select-edit") {
this.#selectController.syncSelectionAfterHistory(entry.oldRect);
this.#tools.select.syncSelectionAfterHistory(entry.oldRect);
}
for (const event of buildUndoReplayEvents(entry)) {
for (const event of HistoryController.buildUndoReplayEvents(entry)) {
this.#sync.emitHook(event);
}
this.#onDrawEnd?.();

return true;
}

/** Re-applies the latest undone edit. Returns `false` when history is unavailable. */
/**
* Re-applies the latest undone edit. Returns `false` when history is unavailable.
**/
redo(): boolean {
const entry = this.#history.redo();
if (!entry) {
Expand All @@ -572,9 +552,9 @@ export class PixelArtCanvas {

this.#refreshAfterHistoryApply();
if (entry.action === "select-edit") {
this.#selectController.syncSelectionAfterHistory(entry.newRect);
this.#tools.select.syncSelectionAfterHistory(entry.newRect);
}
for (const event of buildRedoReplayEvents(entry)) {
for (const event of HistoryController.buildRedoReplayEvents(entry)) {
this.#sync.emitHook(event);
}
this.#onDrawEnd?.();
Expand Down
Loading
Loading