pixelweaver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.development +1 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +16 -0
- package/.python-version +1 -0
- package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
- package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
- package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/bases/CHANGELOG.md +5 -0
- package/.rlsbl/bases/LICENSE +21 -0
- package/.rlsbl/changes/.validated +1 -0
- package/.rlsbl/changes/0.1.0.jsonl +85 -0
- package/.rlsbl/changes/0.1.0.md +13 -0
- package/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/config.json +6 -0
- package/.rlsbl/hashes.json +14 -0
- package/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/releases/unreleased.toml +0 -0
- package/.rlsbl/releases/v0.1.0.toml +3 -0
- package/.rlsbl/version +1 -0
- package/.selfdoc/hashes/hashes.json +146 -0
- package/.strictcli/schema.json +227 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +100 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/icon.png +0 -0
- package/docs/_README.md +117 -0
- package/docs/cli-config.md +35 -0
- package/docs/cli-dev.md +21 -0
- package/docs/cli-diagnose.md +12 -0
- package/docs/cli-index.md +30 -0
- package/docs/cli-list.md +18 -0
- package/docs/cli-mcp.md +18 -0
- package/docs/cli-new.md +26 -0
- package/docs/cli-open.md +21 -0
- package/docs/cli-serve.md +21 -0
- package/docs/cli-stop.md +12 -0
- package/docs/gen-index.md +36 -0
- package/docs/index.md +13 -0
- package/docs/server-src-pixelweaver-__main__.md +12 -0
- package/docs/server-src-pixelweaver-autosave.md +12 -0
- package/docs/server-src-pixelweaver-bridge.md +12 -0
- package/docs/server-src-pixelweaver-cli.md +12 -0
- package/docs/server-src-pixelweaver-config.md +12 -0
- package/docs/server-src-pixelweaver-connections.md +12 -0
- package/docs/server-src-pixelweaver-main.md +12 -0
- package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
- package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
- package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
- package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
- package/docs/server-src-pixelweaver-mcp_server.md +12 -0
- package/docs/server-src-pixelweaver-protocol.md +12 -0
- package/docs/server-src-pixelweaver-state.md +12 -0
- package/docs/server-src-pixelweaver-storage.md +12 -0
- package/docs/server-src-pixelweaver-websocket.md +12 -0
- package/docs/server-src-pixelweaver.md +12 -0
- package/e2e/app-launch.test.ts +35 -0
- package/e2e/menus.test.ts +26 -0
- package/e2e/tools.test.ts +27 -0
- package/e2e/undo-redo.test.ts +11 -0
- package/eslint.config.js +62 -0
- package/index.html +13 -0
- package/package.json +48 -0
- package/playwright.config.ts +19 -0
- package/plugins/builtin/.gitkeep +0 -0
- package/plugins/builtin/advanced-fill-tool.ts +146 -0
- package/plugins/builtin/circle-tool.ts +186 -0
- package/plugins/builtin/diamond-tool.ts +182 -0
- package/plugins/builtin/dither-tool.ts +186 -0
- package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
- package/plugins/builtin/drawing-utils.test.ts +495 -0
- package/plugins/builtin/drawing-utils.ts +431 -0
- package/plugins/builtin/effects/blur.ts +97 -0
- package/plugins/builtin/effects/color-effects.ts +278 -0
- package/plugins/builtin/effects/flip.ts +83 -0
- package/plugins/builtin/effects/glow.ts +118 -0
- package/plugins/builtin/effects/outline.ts +110 -0
- package/plugins/builtin/effects/rotate.ts +357 -0
- package/plugins/builtin/effects/scale.ts +258 -0
- package/plugins/builtin/effects/shadow.ts +111 -0
- package/plugins/builtin/effects/sharpen.ts +102 -0
- package/plugins/builtin/effects.test.ts +715 -0
- package/plugins/builtin/eraser-tool.ts +23 -0
- package/plugins/builtin/eyedropper-tool.ts +83 -0
- package/plugins/builtin/fill-tool.ts +93 -0
- package/plugins/builtin/gradient-tool.ts +204 -0
- package/plugins/builtin/gradient.test.ts +142 -0
- package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
- package/plugins/builtin/importers/aseprite-parser.ts +497 -0
- package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
- package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
- package/plugins/builtin/line-tool.ts +267 -0
- package/plugins/builtin/make-stroke-tool.ts +271 -0
- package/plugins/builtin/noise-dither.test.ts +151 -0
- package/plugins/builtin/noise-tool.ts +131 -0
- package/plugins/builtin/pattern-stamp-tool.ts +162 -0
- package/plugins/builtin/pencil-tool.ts +25 -0
- package/plugins/builtin/rect-tool.ts +179 -0
- package/plugins/builtin/selection-tool.ts +388 -0
- package/plugins/builtin/selection.test.ts +195 -0
- package/plugins/builtin/tool-plugins.test.ts +529 -0
- package/public/favicon.svg +7 -0
- package/public/sw.js +91 -0
- package/pyproject.toml +49 -0
- package/scripts/eslint-wave-a-list.sh +24 -0
- package/scripts/eslint-wave-a-status.sh +25 -0
- package/scripts/fix-index-signature-access.py +127 -0
- package/scripts/fix-unchecked-index.py +128 -0
- package/scripts/fix-unchecked-vars.py +127 -0
- package/scripts/fix-wave-a-bangs.py +36 -0
- package/scripts/fix-wave-a-templates.py +108 -0
- package/scripts/fix-wave-b-void-expr.py +167 -0
- package/scripts/generate-command-params.py +540 -0
- package/scripts/migrate-single-frame-to-multi.py +171 -0
- package/scripts/smoke-test.sh +77 -0
- package/selfdoc.json +10 -0
- package/server/README.md +0 -0
- package/server/src/pixelweaver/__init__.py +1 -0
- package/server/src/pixelweaver/__main__.py +4 -0
- package/server/src/pixelweaver/autosave.py +114 -0
- package/server/src/pixelweaver/bridge.py +127 -0
- package/server/src/pixelweaver/cli.py +199 -0
- package/server/src/pixelweaver/config.py +24 -0
- package/server/src/pixelweaver/connections.py +54 -0
- package/server/src/pixelweaver/main.py +271 -0
- package/server/src/pixelweaver/mcp_bridge.py +189 -0
- package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
- package/server/src/pixelweaver/mcp_export_tools.py +291 -0
- package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
- package/server/src/pixelweaver/mcp_history_tools.py +106 -0
- package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
- package/server/src/pixelweaver/mcp_lock.py +37 -0
- package/server/src/pixelweaver/mcp_project_tools.py +302 -0
- package/server/src/pixelweaver/mcp_read_tools.py +163 -0
- package/server/src/pixelweaver/mcp_registry.py +247 -0
- package/server/src/pixelweaver/mcp_resources.py +312 -0
- package/server/src/pixelweaver/mcp_server.py +234 -0
- package/server/src/pixelweaver/protocol.py +219 -0
- package/server/src/pixelweaver/state.py +267 -0
- package/server/src/pixelweaver/storage.py +293 -0
- package/server/src/pixelweaver/websocket.py +282 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/conftest.py +17 -0
- package/server/tests/test_api.py +96 -0
- package/server/tests/test_bridge.py +161 -0
- package/server/tests/test_health.py +9 -0
- package/server/tests/test_integration.py +86 -0
- package/server/tests/test_mcp_bridge.py +293 -0
- package/server/tests/test_mcp_lock.py +34 -0
- package/server/tests/test_mcp_registry.py +679 -0
- package/server/tests/test_mcp_resources.py +648 -0
- package/server/tests/test_protocol.py +125 -0
- package/server/tests/test_state.py +87 -0
- package/server/tests/test_storage.py +306 -0
- package/server/tests/test_websocket.py +275 -0
- package/src/App.svelte +107 -0
- package/src/app.css +215 -0
- package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
- package/src/lib/animation/animation-commands.test.ts +228 -0
- package/src/lib/animation/animation-commands.ts +540 -0
- package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
- package/src/lib/animation/animation-preview.svelte.ts +151 -0
- package/src/lib/animation/clipboard.ts +134 -0
- package/src/lib/animation/frame-model.svelte.ts +437 -0
- package/src/lib/animation/frame-model.test.ts +314 -0
- package/src/lib/animation/frame-selection.svelte.ts +77 -0
- package/src/lib/animation/frame-tags.svelte.ts +238 -0
- package/src/lib/animation/frame-tags.test.ts +136 -0
- package/src/lib/animation/import.test.ts +141 -0
- package/src/lib/animation/import.ts +112 -0
- package/src/lib/animation/spritesheet-export.test.ts +239 -0
- package/src/lib/animation/spritesheet-export.ts +314 -0
- package/src/lib/canvas/CanvasViewport.svelte +216 -0
- package/src/lib/canvas/canvas-init-plugin.ts +178 -0
- package/src/lib/canvas/canvas-renderer.ts +408 -0
- package/src/lib/canvas/canvas-state.svelte.ts +232 -0
- package/src/lib/canvas/canvas-state.test.ts +139 -0
- package/src/lib/canvas/input-handler.ts +221 -0
- package/src/lib/canvas/input-plugin.ts +150 -0
- package/src/lib/canvas/onion-skin.ts +94 -0
- package/src/lib/canvas/pixel-buffer.test.ts +249 -0
- package/src/lib/canvas/pixel-buffer.ts +151 -0
- package/src/lib/canvas/render-state.svelte.ts +18 -0
- package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
- package/src/lib/canvas/tile-mode.test.ts +53 -0
- package/src/lib/canvas/tile-mode.ts +92 -0
- package/src/lib/canvas/viewport-utils.ts +24 -0
- package/src/lib/canvas/zoom-utils.ts +31 -0
- package/src/lib/color/.gitkeep +0 -0
- package/src/lib/color/color-commands.ts +87 -0
- package/src/lib/color/color-state.svelte.ts +98 -0
- package/src/lib/color/color-state.test.ts +91 -0
- package/src/lib/color/color-utils.test.ts +220 -0
- package/src/lib/color/color-utils.ts +243 -0
- package/src/lib/color/palette-state.svelte.ts +127 -0
- package/src/lib/color/palette-state.test.ts +154 -0
- package/src/lib/color/palette.ts +79 -0
- package/src/lib/core/bootstrap.ts +66 -0
- package/src/lib/core/command-params.generated.ts +1549 -0
- package/src/lib/core/command-params.ts +20 -0
- package/src/lib/core/command-runner.ts +79 -0
- package/src/lib/core/commands.ts +134 -0
- package/src/lib/core/dispatcher.test.ts +548 -0
- package/src/lib/core/dispatcher.ts +361 -0
- package/src/lib/core/index.test.ts +7 -0
- package/src/lib/core/notification-state.svelte.ts +119 -0
- package/src/lib/core/plugin-api.ts +210 -0
- package/src/lib/core/plugin-discovery.test.ts +53 -0
- package/src/lib/core/plugin-discovery.ts +65 -0
- package/src/lib/core/plugin-loader.test.ts +159 -0
- package/src/lib/core/plugin-loader.ts +240 -0
- package/src/lib/core/plugin-types.ts +286 -0
- package/src/lib/core/registries.svelte.ts +74 -0
- package/src/lib/core/tool-options-state.svelte.ts +61 -0
- package/src/lib/dock/DockLayout.svelte +375 -0
- package/src/lib/dock/TabAddMenu.svelte +90 -0
- package/src/lib/dock/dock-persistence.ts +46 -0
- package/src/lib/dock/dock-plugin.ts +49 -0
- package/src/lib/dock/dock-presets.ts +156 -0
- package/src/lib/dock/dock-state.svelte.ts +77 -0
- package/src/lib/dock/dock-types.ts +2 -0
- package/src/lib/dock/dockview-theme.css +226 -0
- package/src/lib/dock/dockview-theme.ts +17 -0
- package/src/lib/dock/header-action-renderer.ts +77 -0
- package/src/lib/edit/clipboard-state.ts +34 -0
- package/src/lib/export/download.ts +201 -0
- package/src/lib/export/png-metadata.ts +181 -0
- package/src/lib/history/ActionLogPanel.svelte +418 -0
- package/src/lib/history/action-log-panel-plugin.ts +24 -0
- package/src/lib/history/action-log.svelte.ts +172 -0
- package/src/lib/history/action-log.test.ts +168 -0
- package/src/lib/history/history-commands.ts +403 -0
- package/src/lib/history/macros.svelte.ts +320 -0
- package/src/lib/history/macros.test.ts +224 -0
- package/src/lib/history/replay-engine.test.ts +241 -0
- package/src/lib/history/replay-engine.ts +149 -0
- package/src/lib/history/spec-format.test.ts +250 -0
- package/src/lib/history/spec-format.ts +210 -0
- package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
- package/src/lib/iso/iso-math.test.ts +77 -0
- package/src/lib/iso/iso-math.ts +77 -0
- package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
- package/src/lib/iso/seam-checker.test.ts +126 -0
- package/src/lib/iso/seam-checker.ts +93 -0
- package/src/lib/iso/tessellation.ts +67 -0
- package/src/lib/layers/compositor.test.ts +193 -0
- package/src/lib/layers/compositor.ts +175 -0
- package/src/lib/layers/layer-commands.test.ts +263 -0
- package/src/lib/layers/layer-commands.ts +429 -0
- package/src/lib/layers/layer-tree.svelte.ts +516 -0
- package/src/lib/layers/layer-tree.test.ts +383 -0
- package/src/lib/layers/layer-types.ts +56 -0
- package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
- package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
- package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
- package/src/lib/leveleditor/depth-sort.test.ts +70 -0
- package/src/lib/leveleditor/depth-sort.ts +39 -0
- package/src/lib/leveleditor/level-editor-commands.ts +353 -0
- package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
- package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/map-state.svelte.ts +372 -0
- package/src/lib/leveleditor/map-state.test.ts +243 -0
- package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
- package/src/lib/leveleditor/tiled-export.test.ts +144 -0
- package/src/lib/leveleditor/tiled-export.ts +374 -0
- package/src/lib/pywebview.d.ts +15 -0
- package/src/lib/recovery/.gitkeep +0 -0
- package/src/lib/recovery/idb-store.ts +118 -0
- package/src/lib/recovery/recovery-manager.test.ts +321 -0
- package/src/lib/recovery/recovery-manager.ts +115 -0
- package/src/lib/recovery/recovery-plugin.ts +55 -0
- package/src/lib/recovery/recovery-state.svelte.ts +21 -0
- package/src/lib/recovery/sw-plugin.ts +18 -0
- package/src/lib/recovery/sw-register.ts +17 -0
- package/src/lib/save/directory-format.ts +42 -0
- package/src/lib/save/project-snapshot.ts +139 -0
- package/src/lib/save/recent-projects.ts +56 -0
- package/src/lib/save/save-state.svelte.ts +35 -0
- package/src/lib/save/storage.ts +167 -0
- package/src/lib/save/zip-format.ts +45 -0
- package/src/lib/shortcuts/.gitkeep +0 -0
- package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
- package/src/lib/shortcuts/default-bindings.ts +61 -0
- package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
- package/src/lib/shortcuts/shortcut-init.ts +380 -0
- package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
- package/src/lib/shortcuts/shortcut-manager.ts +281 -0
- package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
- package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
- package/src/lib/sync/patch-applicator.ts +300 -0
- package/src/lib/sync/patch-types.ts +65 -0
- package/src/lib/sync/project-manager.ts +108 -0
- package/src/lib/sync/sync-init.ts +152 -0
- package/src/lib/sync/sync-plugin.ts +19 -0
- package/src/lib/sync/sync-state.svelte.ts +56 -0
- package/src/lib/sync/ws-client.test.ts +604 -0
- package/src/lib/sync/ws-client.ts +574 -0
- package/src/lib/tools/.gitkeep +0 -0
- package/src/lib/ui/.gitkeep +0 -0
- package/src/lib/ui/AboutDialog.svelte +113 -0
- package/src/lib/ui/ColorPicker.svelte +761 -0
- package/src/lib/ui/ContextMenu.svelte +216 -0
- package/src/lib/ui/ExportDialog.svelte +747 -0
- package/src/lib/ui/FrameStrip.svelte +854 -0
- package/src/lib/ui/LayerPanel.svelte +810 -0
- package/src/lib/ui/MenuBar.svelte +590 -0
- package/src/lib/ui/NewProjectDialog.svelte +803 -0
- package/src/lib/ui/PluginManagerPanel.svelte +475 -0
- package/src/lib/ui/PromptDialog.svelte +252 -0
- package/src/lib/ui/RecoveryDialog.svelte +295 -0
- package/src/lib/ui/ResizeDialog.svelte +416 -0
- package/src/lib/ui/StatusBar.svelte +145 -0
- package/src/lib/ui/ToolbarPanel.svelte +488 -0
- package/src/lib/ui/animation-menu-commands.ts +194 -0
- package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
- package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
- package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
- package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
- package/src/lib/ui/dialog-state.svelte.ts +70 -0
- package/src/lib/ui/edit-commands.ts +271 -0
- package/src/lib/ui/file-commands.ts +275 -0
- package/src/lib/ui/file-open.ts +99 -0
- package/src/lib/ui/help-commands.ts +93 -0
- package/src/lib/ui/image-commands.ts +181 -0
- package/src/lib/ui/layer-menu-commands.ts +420 -0
- package/src/lib/ui/menu-builder.ts +224 -0
- package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
- package/src/lib/ui/notifications/notification-plugin.ts +29 -0
- package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
- package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
- package/src/lib/ui/plugin-state.svelte.ts +62 -0
- package/src/lib/ui/select-commands.ts +75 -0
- package/src/lib/ui/theme-plugin.ts +18 -0
- package/src/lib/ui/theme.svelte.ts +45 -0
- package/src/lib/ui/theme.test.ts +51 -0
- package/src/lib/ui/toolbar-config.ts +90 -0
- package/src/lib/ui/toolbar-plugin.ts +39 -0
- package/src/lib/ui/view-commands.ts +629 -0
- package/src/lib/variants/BisectionExportDialog.svelte +500 -0
- package/src/lib/variants/VariantPanel.svelte +822 -0
- package/src/lib/variants/bisection-export.test.ts +113 -0
- package/src/lib/variants/bisection-export.ts +148 -0
- package/src/lib/variants/palette-extraction.test.ts +111 -0
- package/src/lib/variants/palette-extraction.ts +84 -0
- package/src/lib/variants/palette-interpolation.test.ts +113 -0
- package/src/lib/variants/palette-interpolation.ts +87 -0
- package/src/lib/variants/palette-swap.test.ts +101 -0
- package/src/lib/variants/palette-swap.ts +114 -0
- package/src/lib/variants/variant-commands.ts +594 -0
- package/src/lib/variants/variant-panel-plugin.ts +27 -0
- package/src/lib/variants/variant-randomizer.ts +101 -0
- package/src/lib/variants/variant-state.svelte.ts +166 -0
- package/src/lib/variants/variant-state.test.ts +138 -0
- package/src/main.ts +14 -0
- package/src/vite-env.d.ts +3 -0
- package/svelte.config.js +2 -0
- package/todo/.done/audit-design-decisions.md +812 -0
- package/todo/.done/audit-implementation-plan.md +1235 -0
- package/todo/.done/happy-path-polish.md +177 -0
- package/todo/.done/pixelweaver-full-build.md +937 -0
- package/todo/.done/server-multi-frame-design.md +405 -0
- package/todo/.done/typed-dispatcher-design.md +435 -0
- package/todo/.done/unified-toolbar-and-action-system.md +323 -0
- package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
- package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
- package/todo/comprehensive-audit.md +1085 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +20 -0
- package/uv.lock +1167 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Renderer -- draws the pixel canvas, grid, and cursor onto
|
|
3
|
+
* an HTML Canvas 2D context.
|
|
4
|
+
*
|
|
5
|
+
* Performance strategy: pixel data is drawn to an offscreen canvas at
|
|
6
|
+
* 1:1 scale, then scaled up with drawImage() (hardware-accelerated,
|
|
7
|
+
* nearest-neighbor when imageSmoothingEnabled = false).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PixelBuffer } from './pixel-buffer.js';
|
|
11
|
+
import type { ShapePreview } from './shape-preview-state.svelte.js';
|
|
12
|
+
|
|
13
|
+
// --- Offscreen canvas cache (reused between frames) ---
|
|
14
|
+
|
|
15
|
+
let offscreen: OffscreenCanvas | null = null;
|
|
16
|
+
let offCtx: OffscreenCanvasRenderingContext2D | null = null;
|
|
17
|
+
let offWidth = 0;
|
|
18
|
+
let offHeight = 0;
|
|
19
|
+
|
|
20
|
+
/** Ensure the offscreen canvas matches the pixel buffer dimensions. */
|
|
21
|
+
function ensureOffscreen(w: number, h: number): void {
|
|
22
|
+
if (offscreen && offWidth === w && offHeight === h) return;
|
|
23
|
+
offscreen = new OffscreenCanvas(w, h);
|
|
24
|
+
const ctx = offscreen.getContext('2d');
|
|
25
|
+
if (!ctx) throw new Error('Failed to get 2d context from OffscreenCanvas');
|
|
26
|
+
offCtx = ctx;
|
|
27
|
+
offWidth = w;
|
|
28
|
+
offHeight = h;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Theme-aware color helpers ---
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read a CSS custom property from the document root at render time.
|
|
35
|
+
* Falls back to the provided default when running outside a browser
|
|
36
|
+
* or when the variable is not set.
|
|
37
|
+
*/
|
|
38
|
+
function cssVar(name: string, fallback: string): string {
|
|
39
|
+
if (typeof document === 'undefined') return fallback;
|
|
40
|
+
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
41
|
+
return value || fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Workspace background color (the area outside the pixel canvas). */
|
|
45
|
+
function getWorkspaceBg(): string {
|
|
46
|
+
return cssVar('--bg-canvas', '#121212');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Border/outline color used around the pixel canvas. */
|
|
50
|
+
function getBorderColor(): string {
|
|
51
|
+
return cssVar('--border', '#3a3a3a');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Detect whether the active theme is light. */
|
|
55
|
+
function isLightTheme(): boolean {
|
|
56
|
+
if (typeof document === 'undefined') return false;
|
|
57
|
+
return document.documentElement.dataset["theme"] === 'light';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Grid line color -- dark on light backgrounds, light on dark backgrounds. */
|
|
61
|
+
function getGridColor(): string {
|
|
62
|
+
return isLightTheme() ? 'rgba(0, 0, 0, 0.15)' : 'rgba(255, 255, 255, 0.1)';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Cursor highlight fill -- subtle overlay that contrasts with both themes. */
|
|
66
|
+
function getCursorFillColor(): string {
|
|
67
|
+
return isLightTheme() ? 'rgba(0, 0, 0, 0.1)' : 'rgba(255, 255, 255, 0.15)';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Cursor outline -- must be clearly visible on the canvas. */
|
|
71
|
+
function getCursorStrokeColor(): string {
|
|
72
|
+
return isLightTheme() ? 'rgba(0, 0, 0, 0.5)' : 'rgba(255, 255, 255, 0.6)';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Border around the small color indicator next to the cursor. */
|
|
76
|
+
function getIndicatorBorderColor(): string {
|
|
77
|
+
return isLightTheme() ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.5)';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Main render ---
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Render the full canvas view: background, pixel data, grid, and cursor.
|
|
84
|
+
*
|
|
85
|
+
* @param ctx - The destination 2D context (the on-screen canvas).
|
|
86
|
+
* @param buffer - The pixel data to render.
|
|
87
|
+
* @param zoom - Zoom factor (each pixel = zoom x zoom screen pixels).
|
|
88
|
+
* @param panX - Horizontal pan offset in screen pixels.
|
|
89
|
+
* @param panY - Vertical pan offset in screen pixels.
|
|
90
|
+
*/
|
|
91
|
+
export function renderCanvas(
|
|
92
|
+
ctx: CanvasRenderingContext2D,
|
|
93
|
+
buffer: PixelBuffer,
|
|
94
|
+
zoom: number,
|
|
95
|
+
panX: number,
|
|
96
|
+
panY: number,
|
|
97
|
+
): void {
|
|
98
|
+
const { width: cw, height: ch } = ctx.canvas;
|
|
99
|
+
|
|
100
|
+
// Clear to workspace background color (reads CSS variable so it follows theme)
|
|
101
|
+
ctx.fillStyle = getWorkspaceBg();
|
|
102
|
+
ctx.fillRect(0, 0, cw, ch);
|
|
103
|
+
|
|
104
|
+
// Draw pixel data via offscreen canvas for crisp nearest-neighbor scaling
|
|
105
|
+
ensureOffscreen(buffer.width, buffer.height);
|
|
106
|
+
// After ensureOffscreen, both offCtx and offscreen are guaranteed non-null.
|
|
107
|
+
if (!offCtx || !offscreen) return;
|
|
108
|
+
offCtx.putImageData(buffer.toImageData(), 0, 0);
|
|
109
|
+
|
|
110
|
+
ctx.imageSmoothingEnabled = false;
|
|
111
|
+
ctx.drawImage(
|
|
112
|
+
offscreen,
|
|
113
|
+
0, 0, buffer.width, buffer.height,
|
|
114
|
+
panX, panY, buffer.width * zoom, buffer.height * zoom,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Canvas border (1px outline around the canvas area)
|
|
118
|
+
renderBorder(ctx, buffer.width, buffer.height, zoom, panX, panY);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Border ---
|
|
122
|
+
|
|
123
|
+
/** Draw a 1px outline around the canvas area so the user can see its bounds. */
|
|
124
|
+
function renderBorder(
|
|
125
|
+
ctx: CanvasRenderingContext2D,
|
|
126
|
+
width: number,
|
|
127
|
+
height: number,
|
|
128
|
+
zoom: number,
|
|
129
|
+
panX: number,
|
|
130
|
+
panY: number,
|
|
131
|
+
): void {
|
|
132
|
+
// Use the theme border color so the outline is visible on both themes
|
|
133
|
+
ctx.strokeStyle = getBorderColor();
|
|
134
|
+
ctx.lineWidth = 1;
|
|
135
|
+
// Offset by 0.5 to land on exact pixel boundaries and avoid anti-aliasing
|
|
136
|
+
ctx.strokeRect(
|
|
137
|
+
panX - 0.5,
|
|
138
|
+
panY - 0.5,
|
|
139
|
+
width * zoom + 1,
|
|
140
|
+
height * zoom + 1,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Grid ---
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Draw pixel grid lines. Only meaningful when zoom >= 4.
|
|
148
|
+
* Uses theme-aware semi-transparent lines that don't obscure the art.
|
|
149
|
+
*/
|
|
150
|
+
export function renderGrid(
|
|
151
|
+
ctx: CanvasRenderingContext2D,
|
|
152
|
+
width: number,
|
|
153
|
+
height: number,
|
|
154
|
+
zoom: number,
|
|
155
|
+
panX: number,
|
|
156
|
+
panY: number,
|
|
157
|
+
): void {
|
|
158
|
+
if (zoom < 4) return;
|
|
159
|
+
|
|
160
|
+
const totalW = width * zoom;
|
|
161
|
+
const totalH = height * zoom;
|
|
162
|
+
|
|
163
|
+
ctx.strokeStyle = getGridColor();
|
|
164
|
+
ctx.lineWidth = 0.5;
|
|
165
|
+
|
|
166
|
+
ctx.beginPath();
|
|
167
|
+
|
|
168
|
+
// Vertical lines
|
|
169
|
+
for (let x = 0; x <= width; x++) {
|
|
170
|
+
const sx = panX + x * zoom;
|
|
171
|
+
ctx.moveTo(sx, panY);
|
|
172
|
+
ctx.lineTo(sx, panY + totalH);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Horizontal lines
|
|
176
|
+
for (let y = 0; y <= height; y++) {
|
|
177
|
+
const sy = panY + y * zoom;
|
|
178
|
+
ctx.moveTo(panX, sy);
|
|
179
|
+
ctx.lineTo(panX + totalW, sy);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ctx.stroke();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Cursor overlay ---
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Draw a cursor highlight over the pixel(s) the pointer is on.
|
|
189
|
+
*
|
|
190
|
+
* @param ctx - The destination 2D context.
|
|
191
|
+
* @param x - Canvas pixel X coordinate.
|
|
192
|
+
* @param y - Canvas pixel Y coordinate.
|
|
193
|
+
* @param zoom - Current zoom level.
|
|
194
|
+
* @param panX - Horizontal pan offset.
|
|
195
|
+
* @param panY - Vertical pan offset.
|
|
196
|
+
* @param toolSize - Size of the tool brush in pixels (1 = single pixel).
|
|
197
|
+
* @param color - Current drawing color as [r, g, b, a].
|
|
198
|
+
*/
|
|
199
|
+
export function renderCursor(
|
|
200
|
+
ctx: CanvasRenderingContext2D,
|
|
201
|
+
x: number,
|
|
202
|
+
y: number,
|
|
203
|
+
zoom: number,
|
|
204
|
+
panX: number,
|
|
205
|
+
panY: number,
|
|
206
|
+
toolSize: number = 1,
|
|
207
|
+
color: [number, number, number, number] = [255, 255, 255, 255],
|
|
208
|
+
): void {
|
|
209
|
+
// Pixel highlight: semi-transparent outline around the cursor pixel(s)
|
|
210
|
+
const half = Math.floor(toolSize / 2);
|
|
211
|
+
const sx = panX + (x - half) * zoom;
|
|
212
|
+
const sy = panY + (y - half) * zoom;
|
|
213
|
+
const size = toolSize * zoom;
|
|
214
|
+
|
|
215
|
+
// Fill with a subtle overlay
|
|
216
|
+
ctx.fillStyle = getCursorFillColor();
|
|
217
|
+
ctx.fillRect(sx, sy, size, size);
|
|
218
|
+
|
|
219
|
+
// Outline
|
|
220
|
+
ctx.strokeStyle = getCursorStrokeColor();
|
|
221
|
+
ctx.lineWidth = 1;
|
|
222
|
+
ctx.strokeRect(sx + 0.5, sy + 0.5, size - 1, size - 1);
|
|
223
|
+
|
|
224
|
+
// Small color indicator (4x4 square in the top-right corner of cursor area)
|
|
225
|
+
const indicatorSize = Math.max(4, Math.min(8, zoom / 2));
|
|
226
|
+
const ix = sx + size + 2;
|
|
227
|
+
const iy = sy - indicatorSize - 2;
|
|
228
|
+
ctx.fillStyle = `rgba(${String(color[0])}, ${String(color[1])}, ${String(color[2])}, ${String(color[3] / 255)})`;
|
|
229
|
+
ctx.fillRect(ix, iy, indicatorSize, indicatorSize);
|
|
230
|
+
ctx.strokeStyle = getIndicatorBorderColor();
|
|
231
|
+
ctx.lineWidth = 0.5;
|
|
232
|
+
ctx.strokeRect(ix, iy, indicatorSize, indicatorSize);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- Isometric guide overlay ---
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Draw an isometric diamond guide inscribed in the canvas rectangle,
|
|
239
|
+
* plus center crosshair lines. Helps users align isometric tile sprites.
|
|
240
|
+
*
|
|
241
|
+
* The diamond vertices sit at the midpoints of the canvas edges:
|
|
242
|
+
* Top: (width/2, 0)
|
|
243
|
+
* Right: (width, height/2)
|
|
244
|
+
* Bottom: (width/2, height)
|
|
245
|
+
* Left: (0, height/2)
|
|
246
|
+
*
|
|
247
|
+
* All coordinates are in canvas pixel space, converted to screen space
|
|
248
|
+
* via zoom and pan.
|
|
249
|
+
*/
|
|
250
|
+
export function renderIsoGuide(
|
|
251
|
+
ctx: CanvasRenderingContext2D,
|
|
252
|
+
width: number,
|
|
253
|
+
height: number,
|
|
254
|
+
zoom: number,
|
|
255
|
+
panX: number,
|
|
256
|
+
panY: number,
|
|
257
|
+
): void {
|
|
258
|
+
// Convert canvas-pixel vertices to screen-space
|
|
259
|
+
const topX = panX + (width / 2) * zoom;
|
|
260
|
+
const topY = panY;
|
|
261
|
+
const rightX = panX + width * zoom;
|
|
262
|
+
const rightY = panY + (height / 2) * zoom;
|
|
263
|
+
const bottomX = panX + (width / 2) * zoom;
|
|
264
|
+
const bottomY = panY + height * zoom;
|
|
265
|
+
const leftX = panX;
|
|
266
|
+
const leftY = panY + (height / 2) * zoom;
|
|
267
|
+
|
|
268
|
+
const guideColor = isLightTheme() ? 'rgba(200, 0, 200,' : 'rgba(0, 220, 220,';
|
|
269
|
+
|
|
270
|
+
ctx.save();
|
|
271
|
+
|
|
272
|
+
// Diamond outline -- dashed, 60% opacity
|
|
273
|
+
ctx.strokeStyle = `${guideColor} 0.6)`;
|
|
274
|
+
ctx.lineWidth = 1;
|
|
275
|
+
ctx.setLineDash([4, 4]);
|
|
276
|
+
|
|
277
|
+
ctx.beginPath();
|
|
278
|
+
ctx.moveTo(topX, topY);
|
|
279
|
+
ctx.lineTo(rightX, rightY);
|
|
280
|
+
ctx.lineTo(bottomX, bottomY);
|
|
281
|
+
ctx.lineTo(leftX, leftY);
|
|
282
|
+
ctx.closePath();
|
|
283
|
+
ctx.stroke();
|
|
284
|
+
|
|
285
|
+
// Center crosshair lines -- 30% opacity
|
|
286
|
+
ctx.strokeStyle = `${guideColor} 0.3)`;
|
|
287
|
+
ctx.setLineDash([4, 4]);
|
|
288
|
+
|
|
289
|
+
const centerX = panX + (width / 2) * zoom;
|
|
290
|
+
const centerY = panY + (height / 2) * zoom;
|
|
291
|
+
|
|
292
|
+
ctx.beginPath();
|
|
293
|
+
// Horizontal line through center
|
|
294
|
+
ctx.moveTo(panX, centerY);
|
|
295
|
+
ctx.lineTo(panX + width * zoom, centerY);
|
|
296
|
+
// Vertical line through center
|
|
297
|
+
ctx.moveTo(centerX, panY);
|
|
298
|
+
ctx.lineTo(centerX, panY + height * zoom);
|
|
299
|
+
ctx.stroke();
|
|
300
|
+
|
|
301
|
+
ctx.restore();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// --- Shape preview overlay ---
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Draw a live rubber-band preview for shape tools (rect, ellipse, diamond, line).
|
|
308
|
+
*
|
|
309
|
+
* The preview is drawn as a canvas 2D overlay at the zoomed/panned position,
|
|
310
|
+
* NOT as pixel data. It uses dashed strokes and semi-transparent fills so the
|
|
311
|
+
* user can see what will be drawn without modifying the pixel buffer.
|
|
312
|
+
*
|
|
313
|
+
* Coordinates in the preview are in canvas pixel space; this function converts
|
|
314
|
+
* them to screen space using zoom and pan.
|
|
315
|
+
*/
|
|
316
|
+
export function renderShapePreview(
|
|
317
|
+
ctx: CanvasRenderingContext2D,
|
|
318
|
+
preview: ShapePreview,
|
|
319
|
+
zoom: number,
|
|
320
|
+
panX: number,
|
|
321
|
+
panY: number,
|
|
322
|
+
): void {
|
|
323
|
+
// Normalize so x0,y0 is top-left for rect/ellipse/diamond
|
|
324
|
+
// (line uses raw start/end)
|
|
325
|
+
const x0 = Math.min(preview.startX, preview.endX);
|
|
326
|
+
const y0 = Math.min(preview.startY, preview.endY);
|
|
327
|
+
const x1 = Math.max(preview.startX, preview.endX);
|
|
328
|
+
const y1 = Math.max(preview.startY, preview.endY);
|
|
329
|
+
|
|
330
|
+
// Screen coordinates: offset by +0.5 pixel to cover the full pixel cell
|
|
331
|
+
// (a pixel at canvas coord N spans screen range [N*zoom, (N+1)*zoom])
|
|
332
|
+
const sx0 = panX + x0 * zoom;
|
|
333
|
+
const sy0 = panY + y0 * zoom;
|
|
334
|
+
const sw = (x1 - x0 + 1) * zoom;
|
|
335
|
+
const sh = (y1 - y0 + 1) * zoom;
|
|
336
|
+
|
|
337
|
+
ctx.save();
|
|
338
|
+
|
|
339
|
+
// Semi-transparent version of the drawing color for fills
|
|
340
|
+
const fillColor = preview.color + '33'; // ~20% opacity hex
|
|
341
|
+
// Slightly more opaque for strokes
|
|
342
|
+
const strokeColor = preview.color + '99'; // ~60% opacity hex
|
|
343
|
+
|
|
344
|
+
ctx.lineWidth = 1;
|
|
345
|
+
ctx.setLineDash([4, 4]);
|
|
346
|
+
ctx.strokeStyle = strokeColor;
|
|
347
|
+
ctx.fillStyle = fillColor;
|
|
348
|
+
|
|
349
|
+
switch (preview.type) {
|
|
350
|
+
case 'rect':
|
|
351
|
+
if (preview.filled) {
|
|
352
|
+
ctx.fillRect(sx0, sy0, sw, sh);
|
|
353
|
+
}
|
|
354
|
+
ctx.strokeRect(sx0, sy0, sw, sh);
|
|
355
|
+
break;
|
|
356
|
+
|
|
357
|
+
case 'ellipse': {
|
|
358
|
+
// Derive center and radii in screen space
|
|
359
|
+
const cx = sx0 + sw / 2;
|
|
360
|
+
const cy = sy0 + sh / 2;
|
|
361
|
+
const rx = sw / 2;
|
|
362
|
+
const ry = sh / 2;
|
|
363
|
+
|
|
364
|
+
ctx.beginPath();
|
|
365
|
+
ctx.ellipse(cx, cy, Math.max(0, rx), Math.max(0, ry), 0, 0, Math.PI * 2);
|
|
366
|
+
if (preview.filled) {
|
|
367
|
+
ctx.fill();
|
|
368
|
+
}
|
|
369
|
+
ctx.stroke();
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
case 'diamond': {
|
|
374
|
+
// Four vertices at midpoints of the bounding box edges
|
|
375
|
+
const midX = sx0 + sw / 2;
|
|
376
|
+
const midY = sy0 + sh / 2;
|
|
377
|
+
|
|
378
|
+
ctx.beginPath();
|
|
379
|
+
ctx.moveTo(midX, sy0); // top
|
|
380
|
+
ctx.lineTo(sx0 + sw, midY); // right
|
|
381
|
+
ctx.lineTo(midX, sy0 + sh); // bottom
|
|
382
|
+
ctx.lineTo(sx0, midY); // left
|
|
383
|
+
ctx.closePath();
|
|
384
|
+
if (preview.filled) {
|
|
385
|
+
ctx.fill();
|
|
386
|
+
}
|
|
387
|
+
ctx.stroke();
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case 'line': {
|
|
392
|
+
// Line uses raw start/end (not normalized)
|
|
393
|
+
// +0.5 pixel offset so the line goes through pixel centers
|
|
394
|
+
const lx0 = panX + (preview.startX + 0.5) * zoom;
|
|
395
|
+
const ly0 = panY + (preview.startY + 0.5) * zoom;
|
|
396
|
+
const lx1 = panX + (preview.endX + 0.5) * zoom;
|
|
397
|
+
const ly1 = panY + (preview.endY + 0.5) * zoom;
|
|
398
|
+
|
|
399
|
+
ctx.beginPath();
|
|
400
|
+
ctx.moveTo(lx0, ly0);
|
|
401
|
+
ctx.lineTo(lx1, ly1);
|
|
402
|
+
ctx.stroke();
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
ctx.restore();
|
|
408
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas State -- reactive singleton holding viewport and cursor state.
|
|
3
|
+
*
|
|
4
|
+
* Uses Svelte 5 runes ($state) so that any component reading these
|
|
5
|
+
* properties will automatically re-render when they change.
|
|
6
|
+
*
|
|
7
|
+
* Module-level singleton -- one canvas state per app instance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// --- Canvas dimensions ---
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum canvas dimension in pixels. Previously 512; raised to 4096 so that
|
|
14
|
+
* effects producing larger buffers (rotate 90/270 on tall canvases, upscale)
|
|
15
|
+
* don't get silently clamped. 4096 is a conservative sanity ceiling -- the
|
|
16
|
+
* compositor and PixelBuffer are size-agnostic, and the browser's ImageData
|
|
17
|
+
* supports much larger sizes, but 4096x4096 RGBA already costs 64 MB per
|
|
18
|
+
* layer/frame, so raising it further would invite real memory trouble.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_CANVAS_SIZE = 4096;
|
|
21
|
+
|
|
22
|
+
/** Minimum canvas dimension in pixels. */
|
|
23
|
+
export const MIN_CANVAS_SIZE = 1;
|
|
24
|
+
|
|
25
|
+
/** Width of the pixel canvas. */
|
|
26
|
+
let canvasWidth = $state(32);
|
|
27
|
+
|
|
28
|
+
/** Height of the pixel canvas. */
|
|
29
|
+
let canvasHeight = $state(32);
|
|
30
|
+
|
|
31
|
+
// --- Viewport (how the canvas is displayed in the browser) ---
|
|
32
|
+
|
|
33
|
+
/** Zoom level: each canvas pixel is rendered as zoom x zoom screen pixels. */
|
|
34
|
+
let zoom = $state(8);
|
|
35
|
+
|
|
36
|
+
/** Horizontal pan offset in screen pixels. */
|
|
37
|
+
let panX = $state(0);
|
|
38
|
+
|
|
39
|
+
/** Vertical pan offset in screen pixels. */
|
|
40
|
+
let panY = $state(0);
|
|
41
|
+
|
|
42
|
+
// --- Cursor ---
|
|
43
|
+
|
|
44
|
+
/** Cursor X in canvas pixel coordinates (0-based). */
|
|
45
|
+
let cursorX = $state(0);
|
|
46
|
+
|
|
47
|
+
/** Cursor Y in canvas pixel coordinates (0-based). */
|
|
48
|
+
let cursorY = $state(0);
|
|
49
|
+
|
|
50
|
+
/** Whether the cursor is currently over the canvas area. */
|
|
51
|
+
let cursorInBounds = $state(false);
|
|
52
|
+
|
|
53
|
+
// --- Display toggles ---
|
|
54
|
+
|
|
55
|
+
/** Whether the pixel grid overlay is shown (at sufficient zoom). */
|
|
56
|
+
let showGrid = $state(true);
|
|
57
|
+
|
|
58
|
+
/** Whether the sub-pixel grid overlay is shown (finer grid within each pixel). */
|
|
59
|
+
let showPixelGrid = $state(false);
|
|
60
|
+
|
|
61
|
+
/** Whether tile mode is active (3x3 ghost grid + coordinate wrapping). */
|
|
62
|
+
let tileMode = $state(false);
|
|
63
|
+
|
|
64
|
+
/** Whether onion skinning is active (ghost frames before/after current). */
|
|
65
|
+
let onionSkin = $state(false);
|
|
66
|
+
|
|
67
|
+
/** Whether the isometric diamond guide overlay is shown on the canvas. */
|
|
68
|
+
let isoGuide = $state(false);
|
|
69
|
+
|
|
70
|
+
// --- Animated zoom/pan ---
|
|
71
|
+
|
|
72
|
+
/** Target values for smooth interpolation. */
|
|
73
|
+
let targetZoom = $state(8);
|
|
74
|
+
let targetPanX = $state(0);
|
|
75
|
+
let targetPanY = $state(0);
|
|
76
|
+
let isAnimating = false;
|
|
77
|
+
|
|
78
|
+
/** Lerp factor per frame -- 0.25 gives snappy-but-smooth motion. */
|
|
79
|
+
const LERP = 0.25;
|
|
80
|
+
|
|
81
|
+
/** Thresholds below which we snap to the target to avoid endless ticking. */
|
|
82
|
+
const ZOOM_EPSILON = 0.01;
|
|
83
|
+
const PAN_EPSILON = 0.5;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Internal animation tick that lerps zoom/pan toward their targets.
|
|
87
|
+
* Self-schedules via requestAnimationFrame until close enough.
|
|
88
|
+
*/
|
|
89
|
+
function animationTick(): void {
|
|
90
|
+
const zoomDiff = targetZoom - zoom;
|
|
91
|
+
const panXDiff = targetPanX - panX;
|
|
92
|
+
const panYDiff = targetPanY - panY;
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
Math.abs(zoomDiff) < ZOOM_EPSILON &&
|
|
96
|
+
Math.abs(panXDiff) < PAN_EPSILON &&
|
|
97
|
+
Math.abs(panYDiff) < PAN_EPSILON
|
|
98
|
+
) {
|
|
99
|
+
// Close enough -- snap to targets and stop animating
|
|
100
|
+
zoom = targetZoom;
|
|
101
|
+
panX = targetPanX;
|
|
102
|
+
panY = targetPanY;
|
|
103
|
+
isAnimating = false;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
zoom += zoomDiff * LERP;
|
|
108
|
+
panX += panXDiff * LERP;
|
|
109
|
+
panY += panYDiff * LERP;
|
|
110
|
+
|
|
111
|
+
requestAnimationFrame(animationTick);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function startAnimation(): void {
|
|
115
|
+
if (isAnimating) return;
|
|
116
|
+
isAnimating = true;
|
|
117
|
+
requestAnimationFrame(animationTick);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Coordinate conversion ---
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Convert screen (viewport) coordinates to canvas pixel coordinates.
|
|
124
|
+
* Returns fractional values; floor them to get the pixel index.
|
|
125
|
+
*/
|
|
126
|
+
function screenToCanvas(screenX: number, screenY: number): { x: number; y: number } {
|
|
127
|
+
return {
|
|
128
|
+
x: (screenX - panX) / zoom,
|
|
129
|
+
y: (screenY - panY) / zoom,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convert canvas pixel coordinates to screen (viewport) coordinates.
|
|
135
|
+
* Returns the top-left corner of the pixel on screen.
|
|
136
|
+
*/
|
|
137
|
+
function canvasToScreen(canvasX: number, canvasY: number): { x: number; y: number } {
|
|
138
|
+
return {
|
|
139
|
+
x: canvasX * zoom + panX,
|
|
140
|
+
y: canvasY * zoom + panY,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Public API (exported as a singleton object) ---
|
|
145
|
+
|
|
146
|
+
export const canvasState = {
|
|
147
|
+
get canvasWidth() { return canvasWidth; },
|
|
148
|
+
set canvasWidth(v: number) {
|
|
149
|
+
canvasWidth = Math.max(MIN_CANVAS_SIZE, Math.min(MAX_CANVAS_SIZE, Math.round(v)));
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
get canvasHeight() { return canvasHeight; },
|
|
153
|
+
set canvasHeight(v: number) {
|
|
154
|
+
canvasHeight = Math.max(MIN_CANVAS_SIZE, Math.min(MAX_CANVAS_SIZE, Math.round(v)));
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
get zoom() { return zoom; },
|
|
158
|
+
set zoom(v: number) { zoom = v; targetZoom = v; },
|
|
159
|
+
|
|
160
|
+
get panX() { return panX; },
|
|
161
|
+
set panX(v: number) { panX = v; targetPanX = v; },
|
|
162
|
+
|
|
163
|
+
get panY() { return panY; },
|
|
164
|
+
set panY(v: number) { panY = v; targetPanY = v; },
|
|
165
|
+
|
|
166
|
+
get cursorX() { return cursorX; },
|
|
167
|
+
set cursorX(v: number) { cursorX = v; },
|
|
168
|
+
|
|
169
|
+
get cursorY() { return cursorY; },
|
|
170
|
+
set cursorY(v: number) { cursorY = v; },
|
|
171
|
+
|
|
172
|
+
get cursorInBounds() { return cursorInBounds; },
|
|
173
|
+
set cursorInBounds(v: boolean) { cursorInBounds = v; },
|
|
174
|
+
|
|
175
|
+
get showGrid() { return showGrid; },
|
|
176
|
+
set showGrid(v: boolean) { showGrid = v; },
|
|
177
|
+
|
|
178
|
+
get showPixelGrid() { return showPixelGrid; },
|
|
179
|
+
set showPixelGrid(v: boolean) { showPixelGrid = v; },
|
|
180
|
+
|
|
181
|
+
get tileMode() { return tileMode; },
|
|
182
|
+
set tileMode(v: boolean) { tileMode = v; },
|
|
183
|
+
|
|
184
|
+
get onionSkin() { return onionSkin; },
|
|
185
|
+
set onionSkin(v: boolean) { onionSkin = v; },
|
|
186
|
+
|
|
187
|
+
get isoGuide() { return isoGuide; },
|
|
188
|
+
set isoGuide(v: boolean) { isoGuide = v; },
|
|
189
|
+
|
|
190
|
+
screenToCanvas,
|
|
191
|
+
canvasToScreen,
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Animate zoom toward a target value while keeping a pivot point stable.
|
|
195
|
+
* The pivot is in screen-space (relative to the canvas element).
|
|
196
|
+
*/
|
|
197
|
+
setZoomAnimated(newZoom: number, pivotScreenX: number, pivotScreenY: number): void {
|
|
198
|
+
// Canvas-space position under the pivot at the current (or target) zoom
|
|
199
|
+
const before = {
|
|
200
|
+
x: (pivotScreenX - targetPanX) / targetZoom,
|
|
201
|
+
y: (pivotScreenY - targetPanY) / targetZoom,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
targetZoom = newZoom;
|
|
205
|
+
|
|
206
|
+
// Adjust target pan so the same canvas-space point stays under the pivot
|
|
207
|
+
targetPanX = pivotScreenX - before.x * newZoom;
|
|
208
|
+
targetPanY = pivotScreenY - before.y * newZoom;
|
|
209
|
+
|
|
210
|
+
startAnimation();
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/** Animate pan to a specific screen offset. */
|
|
214
|
+
setPanAnimated(x: number, y: number): void {
|
|
215
|
+
targetPanX = x;
|
|
216
|
+
targetPanY = y;
|
|
217
|
+
startAnimation();
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// --- Serialization ---
|
|
222
|
+
|
|
223
|
+
/** Serialize canvas dimensions for project save. */
|
|
224
|
+
export function serializeCanvasState(): { width: number; height: number } {
|
|
225
|
+
return { width: canvasState.canvasWidth, height: canvasState.canvasHeight };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Restore canvas dimensions from a saved project. */
|
|
229
|
+
export function deserializeCanvasState(data: { width: number; height: number }): void {
|
|
230
|
+
canvasState.canvasWidth = data.width;
|
|
231
|
+
canvasState.canvasHeight = data.height;
|
|
232
|
+
}
|