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,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate Effect -- rotates the active layer by 90/180/270 degrees or free angle.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - Command: `rotate` (tier: 'frame')
|
|
6
|
+
*
|
|
7
|
+
* For cardinal rotations (90/180/270), pixels are swapped directly.
|
|
8
|
+
* For free angle rotation, uses nearest-neighbor sampling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PluginModule } from '../../../src/lib/core/plugin-loader.js';
|
|
12
|
+
import { PixelBuffer } from '../../../src/lib/canvas/pixel-buffer.js';
|
|
13
|
+
import RotateCw from '~icons/lucide/rotate-cw';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot captured by rotate execute() for undo: the original buffer
|
|
17
|
+
* dimensions, a copy of its pixel data, and the (frameIndex, layerId)
|
|
18
|
+
* pair the rotation was applied to. On undo we reconstruct a PixelBuffer
|
|
19
|
+
* from this snapshot and write it back to the ORIGINAL pair via
|
|
20
|
+
* setBufferForLayer so that switching the active layer/frame between
|
|
21
|
+
* execute and undo still restores the correct layer.
|
|
22
|
+
*
|
|
23
|
+
* frameIndex / layerId may be null/empty if the context doesn't implement
|
|
24
|
+
* getActiveFrameIndex/getActiveLayerId -- in that case undo falls back to
|
|
25
|
+
* setActiveBuffer (legacy contexts).
|
|
26
|
+
*/
|
|
27
|
+
interface BufferSnapshot {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
data: Uint8ClampedArray;
|
|
31
|
+
frameIndex: number | null;
|
|
32
|
+
layerId: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Snapshot for rotate_canvas: captures the pre-rotation dimensions and a
|
|
37
|
+
* list of every (frameIndex, layerId) buffer the command touched. Undo
|
|
38
|
+
* iterates the list and writes each captured buffer back through
|
|
39
|
+
* setBufferForLayer, then restores canvas size to oldWidth/oldHeight.
|
|
40
|
+
*/
|
|
41
|
+
interface WholeCanvasRotateSnapshot {
|
|
42
|
+
oldWidth: number;
|
|
43
|
+
oldHeight: number;
|
|
44
|
+
entries: Array<{
|
|
45
|
+
frameIndex: number;
|
|
46
|
+
layerId: string;
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
data: Uint8ClampedArray;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compute the rotated pixel data for a buffer.
|
|
55
|
+
*
|
|
56
|
+
* Returns a NEW PixelBuffer sized to match the rotation result:
|
|
57
|
+
* - 0 / 180 degrees: same dimensions (w x h)
|
|
58
|
+
* - 90 / 270 degrees: swapped dimensions (h x w)
|
|
59
|
+
* - non-cardinal angles: same dimensions (w x h), nearest-neighbor sampled
|
|
60
|
+
*
|
|
61
|
+
* The returned buffer is independent of the input. Callers are responsible for
|
|
62
|
+
* writing it back into the canvas (and, for dim-swapping cases, updating
|
|
63
|
+
* canvas/layer state accordingly).
|
|
64
|
+
*/
|
|
65
|
+
export function computeRotation(
|
|
66
|
+
buffer: PixelBuffer,
|
|
67
|
+
angle: number,
|
|
68
|
+
): PixelBuffer {
|
|
69
|
+
const w = buffer.width;
|
|
70
|
+
const h = buffer.height;
|
|
71
|
+
|
|
72
|
+
// Normalize angle to [0, 360)
|
|
73
|
+
const normalizedAngle = ((angle % 360) + 360) % 360;
|
|
74
|
+
|
|
75
|
+
if (normalizedAngle === 90) {
|
|
76
|
+
// 90 CW: output buffer is h wide x w tall.
|
|
77
|
+
// Source (sx, sy) -> destination (h-1-sy, sx).
|
|
78
|
+
// Inverse (used here): dest (dx, dy) <- source (dy, h-1-dx).
|
|
79
|
+
const out = new PixelBuffer(h, w);
|
|
80
|
+
for (let dy = 0; dy < w; dy++) {
|
|
81
|
+
for (let dx = 0; dx < h; dx++) {
|
|
82
|
+
const sx = dy;
|
|
83
|
+
const sy = h - 1 - dx;
|
|
84
|
+
const [r, g, b, a] = buffer.getPixel(sx, sy);
|
|
85
|
+
out.setPixel(dx, dy, r, g, b, a);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
} else if (normalizedAngle === 180) {
|
|
90
|
+
// 180: dimensions unchanged. dest(dx, dy) <- source(w-1-dx, h-1-dy).
|
|
91
|
+
const out = new PixelBuffer(w, h);
|
|
92
|
+
for (let dy = 0; dy < h; dy++) {
|
|
93
|
+
for (let dx = 0; dx < w; dx++) {
|
|
94
|
+
const sx = w - 1 - dx;
|
|
95
|
+
const sy = h - 1 - dy;
|
|
96
|
+
const [r, g, b, a] = buffer.getPixel(sx, sy);
|
|
97
|
+
out.setPixel(dx, dy, r, g, b, a);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
} else if (normalizedAngle === 270) {
|
|
102
|
+
// 270 CW (= 90 CCW): output buffer is h wide x w tall.
|
|
103
|
+
// Source (sx, sy) -> destination (sy, w-1-sx).
|
|
104
|
+
// Inverse (used here): dest (dx, dy) <- source (w-1-dy, dx).
|
|
105
|
+
const out = new PixelBuffer(h, w);
|
|
106
|
+
for (let dy = 0; dy < w; dy++) {
|
|
107
|
+
for (let dx = 0; dx < h; dx++) {
|
|
108
|
+
const sx = w - 1 - dy;
|
|
109
|
+
const sy = dx;
|
|
110
|
+
const [r, g, b, a] = buffer.getPixel(sx, sy);
|
|
111
|
+
out.setPixel(dx, dy, r, g, b, a);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
} else if (normalizedAngle === 0) {
|
|
116
|
+
// No rotation: return a clone.
|
|
117
|
+
return buffer.clone();
|
|
118
|
+
} else {
|
|
119
|
+
// Free angle: nearest-neighbor sampling, same dimensions.
|
|
120
|
+
const out = new PixelBuffer(w, h);
|
|
121
|
+
const rad = (normalizedAngle * Math.PI) / 180;
|
|
122
|
+
const cos = Math.cos(rad);
|
|
123
|
+
const sin = Math.sin(rad);
|
|
124
|
+
const cx = (w - 1) / 2;
|
|
125
|
+
const cy = (h - 1) / 2;
|
|
126
|
+
|
|
127
|
+
for (let dy = 0; dy < h; dy++) {
|
|
128
|
+
for (let dx = 0; dx < w; dx++) {
|
|
129
|
+
// Rotate destination pixel back to find source
|
|
130
|
+
const rx = dx - cx;
|
|
131
|
+
const ry = dy - cy;
|
|
132
|
+
const sx = Math.round(rx * cos + ry * sin + cx);
|
|
133
|
+
const sy = Math.round(-rx * sin + ry * cos + cy);
|
|
134
|
+
|
|
135
|
+
const [r, g, b, a] = buffer.inBounds(sx, sy) ? buffer.getPixel(sx, sy) : [0, 0, 0, 0];
|
|
136
|
+
out.setPixel(dx, dy, r, g, b, a);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const rotateEffectPlugin: PluginModule = {
|
|
144
|
+
name: 'builtin/effects/rotate',
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
dependencies: [],
|
|
147
|
+
register(api) {
|
|
148
|
+
api.addCommand('rotate', {
|
|
149
|
+
tier: 'frame',
|
|
150
|
+
label: 'Rotate',
|
|
151
|
+
category: 'Effects',
|
|
152
|
+
icon: RotateCw,
|
|
153
|
+
|
|
154
|
+
execute(params, ctx) {
|
|
155
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
156
|
+
if (!buffer) return;
|
|
157
|
+
|
|
158
|
+
// Snapshot old dims + raw pixel data + the active (frameIndex, layerId)
|
|
159
|
+
// so undo can rebuild the original buffer and write it back to the
|
|
160
|
+
// ORIGINAL layer/frame -- not whatever happens to be active at undo
|
|
161
|
+
// time. Captured defensively: contexts that don't implement
|
|
162
|
+
// getActiveFrameIndex/getActiveLayerId fall through to the legacy
|
|
163
|
+
// setActiveBuffer path at undo time.
|
|
164
|
+
const snapshot: BufferSnapshot = {
|
|
165
|
+
width: buffer.width,
|
|
166
|
+
height: buffer.height,
|
|
167
|
+
data: new Uint8ClampedArray(buffer.data),
|
|
168
|
+
frameIndex: ctx.getActiveFrameIndex?.() ?? null,
|
|
169
|
+
layerId: ctx.getActiveLayerId?.() ?? null,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// computeRotation returns a NEW PixelBuffer sized to the result:
|
|
173
|
+
// - 0/180/free: same dimensions
|
|
174
|
+
// - 90/270: swapped dimensions (h x w)
|
|
175
|
+
// Push it through setActiveBuffer so the canvas and active layer
|
|
176
|
+
// adopt the new dimensions. If the context does not implement
|
|
177
|
+
// setActiveBuffer (legacy contexts), fall back to blitting the
|
|
178
|
+
// rotated pixels into the existing buffer (clipped to its bounds).
|
|
179
|
+
const rotated = computeRotation(buffer, params["angle"]);
|
|
180
|
+
if (ctx.setActiveBuffer) {
|
|
181
|
+
ctx.setActiveBuffer(rotated);
|
|
182
|
+
} else {
|
|
183
|
+
// Fallback: clear then overlay within the fixed buffer. This will
|
|
184
|
+
// visually clip for 90/270 on non-square canvases but keeps the
|
|
185
|
+
// command functional in contexts without buffer replacement.
|
|
186
|
+
for (let y = 0; y < buffer.height; y++) {
|
|
187
|
+
for (let x = 0; x < buffer.width; x++) {
|
|
188
|
+
buffer.setPixel(x, y, 0, 0, 0, 0);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (let y = 0; y < rotated.height; y++) {
|
|
192
|
+
for (let x = 0; x < rotated.width; x++) {
|
|
193
|
+
const [r, g, b, a] = rotated.getPixel(x, y);
|
|
194
|
+
buffer.setPixel(x, y, r, g, b, a);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return snapshot;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
undo(_params, ctx, snapshot) {
|
|
202
|
+
const snap = snapshot as BufferSnapshot | undefined;
|
|
203
|
+
if (!snap) return;
|
|
204
|
+
// Rebuild the original buffer from the snapshot and push it back.
|
|
205
|
+
// new Uint8ClampedArray(snap.data) decouples the restored buffer
|
|
206
|
+
// from the snapshot so redo can re-snapshot cleanly.
|
|
207
|
+
const restored = new PixelBuffer(
|
|
208
|
+
snap.width,
|
|
209
|
+
snap.height,
|
|
210
|
+
new Uint8ClampedArray(snap.data),
|
|
211
|
+
);
|
|
212
|
+
// Prefer writing back to the ORIGINAL (frame, layer) pair captured
|
|
213
|
+
// at execute time. Falls back to setActiveBuffer (current pair) if
|
|
214
|
+
// the context doesn't support per-layer writes or the snapshot
|
|
215
|
+
// predates this change.
|
|
216
|
+
if (
|
|
217
|
+
ctx.setBufferForLayer &&
|
|
218
|
+
snap.frameIndex !== null &&
|
|
219
|
+
snap.layerId !== null
|
|
220
|
+
) {
|
|
221
|
+
ctx.setBufferForLayer(snap.frameIndex, snap.layerId, restored);
|
|
222
|
+
} else if (ctx.setActiveBuffer) {
|
|
223
|
+
ctx.setActiveBuffer(restored);
|
|
224
|
+
} else {
|
|
225
|
+
// Fallback: write snapshot pixels back into the current buffer.
|
|
226
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
227
|
+
if (!buffer) return;
|
|
228
|
+
for (let y = 0; y < snap.height; y++) {
|
|
229
|
+
for (let x = 0; x < snap.width; x++) {
|
|
230
|
+
const i = (y * snap.width + x) * 4;
|
|
231
|
+
buffer.setPixel(
|
|
232
|
+
x,
|
|
233
|
+
y,
|
|
234
|
+
snap.data[i] ?? 0,
|
|
235
|
+
snap.data[i + 1] ?? 0,
|
|
236
|
+
snap.data[i + 2] ?? 0,
|
|
237
|
+
snap.data[i + 3] ?? 0,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
describe(params) {
|
|
245
|
+
return `Rotated layer by ${String(params["angle"])} degrees`;
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
api.addMenuItem('menu:effects:rotate', {
|
|
250
|
+
commandId: 'rotate',
|
|
251
|
+
menuPath: 'effects',
|
|
252
|
+
group: 'transform',
|
|
253
|
+
order: 20,
|
|
254
|
+
label: 'Rotate',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// --- Whole-canvas rotation ---
|
|
258
|
+
//
|
|
259
|
+
// rotate_canvas rotates EVERY pixel layer across EVERY frame by the
|
|
260
|
+
// same angle so the entire project stays consistent. Canvas dimensions
|
|
261
|
+
// are updated exactly once at the end, not per-layer write (each
|
|
262
|
+
// setBufferForLayer call does update canvas size, but the final
|
|
263
|
+
// setCanvasSize wins after all writes).
|
|
264
|
+
api.addCommand('rotate_canvas', {
|
|
265
|
+
tier: 'project',
|
|
266
|
+
undoable: true,
|
|
267
|
+
label: 'Rotate Canvas',
|
|
268
|
+
category: 'Image',
|
|
269
|
+
icon: RotateCw,
|
|
270
|
+
|
|
271
|
+
execute(params, ctx): WholeCanvasRotateSnapshot | undefined {
|
|
272
|
+
const angle = params["angle"];
|
|
273
|
+
// Require the richer context APIs -- whole-canvas ops have no
|
|
274
|
+
// sensible fallback path in legacy contexts.
|
|
275
|
+
if (!ctx.getAllFrameLayerBuffers || !ctx.setBufferForLayer || !ctx.setCanvasSize) {
|
|
276
|
+
console.warn('rotate_canvas: context missing whole-canvas APIs');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const pairs = ctx.getAllFrameLayerBuffers();
|
|
280
|
+
if (pairs.length === 0) return;
|
|
281
|
+
|
|
282
|
+
// Capture old canvas dims from the first buffer. All buffers
|
|
283
|
+
// should share the same dimensions (they always do in the current
|
|
284
|
+
// model -- canvas-wide resize is enforced via canvasState).
|
|
285
|
+
const firstPair = pairs[0];
|
|
286
|
+
if (!firstPair) return;
|
|
287
|
+
const first = firstPair.buffer;
|
|
288
|
+
const snapshot: WholeCanvasRotateSnapshot = {
|
|
289
|
+
oldWidth: first.width,
|
|
290
|
+
oldHeight: first.height,
|
|
291
|
+
entries: pairs.map((p) => ({
|
|
292
|
+
frameIndex: p.frameIndex,
|
|
293
|
+
layerId: p.layerId,
|
|
294
|
+
width: p.buffer.width,
|
|
295
|
+
height: p.buffer.height,
|
|
296
|
+
// Copy the data -- the original buffer object gets replaced
|
|
297
|
+
// below so we can't just hold a reference.
|
|
298
|
+
data: new Uint8ClampedArray(p.buffer.data),
|
|
299
|
+
})),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Rotate each buffer and write it back through setBufferForLayer.
|
|
303
|
+
// computeRotation returns a new buffer; for 90/270 on non-square
|
|
304
|
+
// source the dims swap -- that's expected and applies uniformly
|
|
305
|
+
// to every layer, so the final canvas dims stay consistent.
|
|
306
|
+
let finalW = first.width;
|
|
307
|
+
let finalH = first.height;
|
|
308
|
+
for (const { frameIndex, layerId, buffer } of pairs) {
|
|
309
|
+
const rotated = computeRotation(buffer, angle);
|
|
310
|
+
ctx.setBufferForLayer(frameIndex, layerId, rotated);
|
|
311
|
+
finalW = rotated.width;
|
|
312
|
+
finalH = rotated.height;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Explicit canvas resize once at the end -- setBufferForLayer
|
|
316
|
+
// already updates canvasState per write, but an explicit call
|
|
317
|
+
// here makes the "one authoritative final size" semantics clear.
|
|
318
|
+
ctx.setCanvasSize(finalW, finalH);
|
|
319
|
+
|
|
320
|
+
return snapshot;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
undo(_params, ctx, snapshot) {
|
|
324
|
+
const snap = snapshot as WholeCanvasRotateSnapshot | undefined;
|
|
325
|
+
if (!snap) return;
|
|
326
|
+
if (!ctx.setBufferForLayer || !ctx.setCanvasSize) return;
|
|
327
|
+
|
|
328
|
+
// Restore every captured buffer, then restore canvas dims.
|
|
329
|
+
// Order matters: the last setBufferForLayer call resizes canvas
|
|
330
|
+
// to the restored buffer's dims (which equals snap.oldWidth/Height
|
|
331
|
+
// because all entries share the same pre-rotation dims), and the
|
|
332
|
+
// final setCanvasSize is a no-op-or-correction guard.
|
|
333
|
+
for (const entry of snap.entries) {
|
|
334
|
+
const restored = new PixelBuffer(
|
|
335
|
+
entry.width,
|
|
336
|
+
entry.height,
|
|
337
|
+
new Uint8ClampedArray(entry.data),
|
|
338
|
+
);
|
|
339
|
+
ctx.setBufferForLayer(entry.frameIndex, entry.layerId, restored);
|
|
340
|
+
}
|
|
341
|
+
ctx.setCanvasSize(snap.oldWidth, snap.oldHeight);
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
describe(params) {
|
|
345
|
+
return `Rotated canvas by ${String(params["angle"])} degrees`;
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
api.addMenuItem('menu:image:rotate-canvas', {
|
|
350
|
+
commandId: 'rotate_canvas',
|
|
351
|
+
menuPath: 'image',
|
|
352
|
+
group: 'transform',
|
|
353
|
+
order: 40,
|
|
354
|
+
label: 'Rotate Canvas',
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale Effect -- resizes the active layer with nearest-neighbor interpolation.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - Command: `scale` (tier: 'frame')
|
|
6
|
+
*
|
|
7
|
+
* computeScale() returns a NEW PixelBuffer sized to (newWidth, newHeight).
|
|
8
|
+
* execute() pushes it through ctx.setActiveBuffer so the canvas adopts the
|
|
9
|
+
* new dimensions. Undo rebuilds the original buffer from a dim+data snapshot.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PluginModule } from '../../../src/lib/core/plugin-loader.js';
|
|
13
|
+
import { PixelBuffer } from '../../../src/lib/canvas/pixel-buffer.js';
|
|
14
|
+
import Scaling from '~icons/lucide/scaling';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Snapshot captured for undo: original dimensions, a copy of the pixel data,
|
|
18
|
+
* and the (frameIndex, layerId) pair the scale was applied to. Mirrors the
|
|
19
|
+
* BufferSnapshot type in rotate.ts -- kept private per module to avoid a
|
|
20
|
+
* cross-effect dependency. See rotate.ts for the full rationale behind
|
|
21
|
+
* capturing frameIndex/layerId for undo.
|
|
22
|
+
*/
|
|
23
|
+
interface BufferSnapshot {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
data: Uint8ClampedArray;
|
|
27
|
+
frameIndex: number | null;
|
|
28
|
+
layerId: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Snapshot for scale_canvas: captures the pre-scale dimensions and a list
|
|
33
|
+
* of every (frameIndex, layerId) buffer the command touched. Undo iterates
|
|
34
|
+
* the list and writes each captured buffer back through setBufferForLayer,
|
|
35
|
+
* then restores canvas size. Mirrors WholeCanvasRotateSnapshot in rotate.ts.
|
|
36
|
+
*/
|
|
37
|
+
interface WholeCanvasScaleSnapshot {
|
|
38
|
+
oldWidth: number;
|
|
39
|
+
oldHeight: number;
|
|
40
|
+
entries: Array<{
|
|
41
|
+
frameIndex: number;
|
|
42
|
+
layerId: string;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
data: Uint8ClampedArray;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Compute scaled pixels using nearest-neighbor sampling.
|
|
51
|
+
*
|
|
52
|
+
* Returns a NEW PixelBuffer sized (newWidth x newHeight). Each destination
|
|
53
|
+
* pixel is mapped back to the nearest source pixel. The returned buffer is
|
|
54
|
+
* independent of the input; callers are responsible for swapping it into
|
|
55
|
+
* the active state (setActiveBuffer) and updating canvas dimensions.
|
|
56
|
+
*/
|
|
57
|
+
export function computeScale(
|
|
58
|
+
buffer: PixelBuffer,
|
|
59
|
+
newWidth: number,
|
|
60
|
+
newHeight: number,
|
|
61
|
+
): PixelBuffer {
|
|
62
|
+
const srcW = buffer.width;
|
|
63
|
+
const srcH = buffer.height;
|
|
64
|
+
const out = new PixelBuffer(newWidth, newHeight);
|
|
65
|
+
|
|
66
|
+
for (let dy = 0; dy < newHeight; dy++) {
|
|
67
|
+
for (let dx = 0; dx < newWidth; dx++) {
|
|
68
|
+
// Nearest-neighbor: map destination to source.
|
|
69
|
+
const sx = Math.floor((dx * srcW) / newWidth);
|
|
70
|
+
const sy = Math.floor((dy * srcH) / newHeight);
|
|
71
|
+
const [r, g, b, a] = buffer.getPixel(sx, sy);
|
|
72
|
+
out.setPixel(dx, dy, r, g, b, a);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const scaleEffectPlugin: PluginModule = {
|
|
80
|
+
name: 'builtin/effects/scale',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
dependencies: [],
|
|
83
|
+
register(api) {
|
|
84
|
+
api.addCommand('scale', {
|
|
85
|
+
tier: 'frame',
|
|
86
|
+
label: 'Scale',
|
|
87
|
+
category: 'Effects',
|
|
88
|
+
icon: Scaling,
|
|
89
|
+
|
|
90
|
+
execute(params, ctx) {
|
|
91
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
92
|
+
if (!buffer) return;
|
|
93
|
+
|
|
94
|
+
// Snapshot old dims + raw pixel data + active (frameIndex, layerId).
|
|
95
|
+
// See rotate.ts for why the pair is captured: undo must write back
|
|
96
|
+
// to the ORIGINAL layer/frame regardless of what's active at undo
|
|
97
|
+
// time.
|
|
98
|
+
const snapshot: BufferSnapshot = {
|
|
99
|
+
width: buffer.width,
|
|
100
|
+
height: buffer.height,
|
|
101
|
+
data: new Uint8ClampedArray(buffer.data),
|
|
102
|
+
frameIndex: ctx.getActiveFrameIndex?.() ?? null,
|
|
103
|
+
layerId: ctx.getActiveLayerId?.() ?? null,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const scaled = computeScale(
|
|
107
|
+
buffer,
|
|
108
|
+
params["newWidth"],
|
|
109
|
+
params["newHeight"],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (ctx.setActiveBuffer) {
|
|
113
|
+
ctx.setActiveBuffer(scaled);
|
|
114
|
+
} else {
|
|
115
|
+
// Fallback for contexts without buffer replacement: blit the
|
|
116
|
+
// scaled pixels into the existing buffer, clipped to its bounds.
|
|
117
|
+
// Out-of-range pixels (beyond the old buffer dims) are lost and
|
|
118
|
+
// unused cells in the old buffer are cleared to transparent.
|
|
119
|
+
for (let y = 0; y < buffer.height; y++) {
|
|
120
|
+
for (let x = 0; x < buffer.width; x++) {
|
|
121
|
+
if (x < scaled.width && y < scaled.height) {
|
|
122
|
+
const [r, g, b, a] = scaled.getPixel(x, y);
|
|
123
|
+
buffer.setPixel(x, y, r, g, b, a);
|
|
124
|
+
} else {
|
|
125
|
+
buffer.setPixel(x, y, 0, 0, 0, 0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return snapshot;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
undo(_params, ctx, snapshot) {
|
|
134
|
+
const snap = snapshot as BufferSnapshot | undefined;
|
|
135
|
+
if (!snap) return;
|
|
136
|
+
const restored = new PixelBuffer(
|
|
137
|
+
snap.width,
|
|
138
|
+
snap.height,
|
|
139
|
+
new Uint8ClampedArray(snap.data),
|
|
140
|
+
);
|
|
141
|
+
// Prefer writing back to the ORIGINAL (frame, layer) pair captured
|
|
142
|
+
// at execute time; fall back to setActiveBuffer for legacy contexts.
|
|
143
|
+
if (
|
|
144
|
+
ctx.setBufferForLayer &&
|
|
145
|
+
snap.frameIndex !== null &&
|
|
146
|
+
snap.layerId !== null
|
|
147
|
+
) {
|
|
148
|
+
ctx.setBufferForLayer(snap.frameIndex, snap.layerId, restored);
|
|
149
|
+
} else if (ctx.setActiveBuffer) {
|
|
150
|
+
ctx.setActiveBuffer(restored);
|
|
151
|
+
} else {
|
|
152
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
153
|
+
if (!buffer) return;
|
|
154
|
+
for (let y = 0; y < snap.height; y++) {
|
|
155
|
+
for (let x = 0; x < snap.width; x++) {
|
|
156
|
+
const i = (y * snap.width + x) * 4;
|
|
157
|
+
buffer.setPixel(
|
|
158
|
+
x,
|
|
159
|
+
y,
|
|
160
|
+
snap.data[i] ?? 0,
|
|
161
|
+
snap.data[i + 1] ?? 0,
|
|
162
|
+
snap.data[i + 2] ?? 0,
|
|
163
|
+
snap.data[i + 3] ?? 0,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
describe(params) {
|
|
171
|
+
return `Scaled layer to ${String(params["newWidth"])}x${String(params["newHeight"])}`;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
api.addMenuItem('menu:effects:scale', {
|
|
176
|
+
commandId: 'scale',
|
|
177
|
+
menuPath: 'effects',
|
|
178
|
+
group: 'transform',
|
|
179
|
+
order: 30,
|
|
180
|
+
label: 'Scale',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// --- Whole-canvas scale ---
|
|
184
|
+
//
|
|
185
|
+
// scale_canvas resizes EVERY pixel layer across EVERY frame to the
|
|
186
|
+
// same new dimensions. Canvas size is updated exactly once at the
|
|
187
|
+
// end. See rotate.ts's rotate_canvas for the shared design rationale.
|
|
188
|
+
api.addCommand('scale_canvas', {
|
|
189
|
+
tier: 'project',
|
|
190
|
+
undoable: true,
|
|
191
|
+
label: 'Scale Canvas',
|
|
192
|
+
category: 'Image',
|
|
193
|
+
icon: Scaling,
|
|
194
|
+
|
|
195
|
+
execute(params, ctx): WholeCanvasScaleSnapshot | undefined {
|
|
196
|
+
const newWidth = params["newWidth"];
|
|
197
|
+
const newHeight = params["newHeight"];
|
|
198
|
+
if (!ctx.getAllFrameLayerBuffers || !ctx.setBufferForLayer || !ctx.setCanvasSize) {
|
|
199
|
+
console.warn('scale_canvas: context missing whole-canvas APIs');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const pairs = ctx.getAllFrameLayerBuffers();
|
|
203
|
+
if (pairs.length === 0) return;
|
|
204
|
+
|
|
205
|
+
const firstPair = pairs[0];
|
|
206
|
+
if (!firstPair) return;
|
|
207
|
+
const first = firstPair.buffer;
|
|
208
|
+
const snapshot: WholeCanvasScaleSnapshot = {
|
|
209
|
+
oldWidth: first.width,
|
|
210
|
+
oldHeight: first.height,
|
|
211
|
+
entries: pairs.map((p) => ({
|
|
212
|
+
frameIndex: p.frameIndex,
|
|
213
|
+
layerId: p.layerId,
|
|
214
|
+
width: p.buffer.width,
|
|
215
|
+
height: p.buffer.height,
|
|
216
|
+
data: new Uint8ClampedArray(p.buffer.data),
|
|
217
|
+
})),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
for (const { frameIndex, layerId, buffer } of pairs) {
|
|
221
|
+
const scaled = computeScale(buffer, newWidth, newHeight);
|
|
222
|
+
ctx.setBufferForLayer(frameIndex, layerId, scaled);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
ctx.setCanvasSize(newWidth, newHeight);
|
|
226
|
+
return snapshot;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
undo(_params, ctx, snapshot) {
|
|
230
|
+
const snap = snapshot as WholeCanvasScaleSnapshot | undefined;
|
|
231
|
+
if (!snap) return;
|
|
232
|
+
if (!ctx.setBufferForLayer || !ctx.setCanvasSize) return;
|
|
233
|
+
|
|
234
|
+
for (const entry of snap.entries) {
|
|
235
|
+
const restored = new PixelBuffer(
|
|
236
|
+
entry.width,
|
|
237
|
+
entry.height,
|
|
238
|
+
new Uint8ClampedArray(entry.data),
|
|
239
|
+
);
|
|
240
|
+
ctx.setBufferForLayer(entry.frameIndex, entry.layerId, restored);
|
|
241
|
+
}
|
|
242
|
+
ctx.setCanvasSize(snap.oldWidth, snap.oldHeight);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
describe(params) {
|
|
246
|
+
return `Scaled canvas to ${String(params["newWidth"])}x${String(params["newHeight"])}`;
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
api.addMenuItem('menu:image:scale-canvas', {
|
|
251
|
+
commandId: 'scale_canvas',
|
|
252
|
+
menuPath: 'image',
|
|
253
|
+
group: 'transform',
|
|
254
|
+
order: 50,
|
|
255
|
+
label: 'Scale Canvas',
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
};
|