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,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eraser Tool Plugin -- sets pixels to transparent (0,0,0,0).
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - Command: `erase_pixels` (tier: 'frame')
|
|
6
|
+
* - Tool: `eraser`
|
|
7
|
+
*
|
|
8
|
+
* Thin wrapper around makeStrokeTool; always writes fully transparent pixels.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { makeStrokeTool } from './make-stroke-tool.js';
|
|
12
|
+
import EraserIcon from '~icons/lucide/eraser';
|
|
13
|
+
|
|
14
|
+
export const eraserToolPlugin = makeStrokeTool({
|
|
15
|
+
pluginName: 'builtin/eraser',
|
|
16
|
+
commandName: 'erase_pixels',
|
|
17
|
+
toolId: 'eraser',
|
|
18
|
+
icon: EraserIcon,
|
|
19
|
+
toolbarOrder: 20,
|
|
20
|
+
toolbarGroup: 'draw',
|
|
21
|
+
describeVerb: 'Erased',
|
|
22
|
+
resolveColor: () => ({ r: 0, g: 0, b: 0, a: 0 }),
|
|
23
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eyedropper Tool Plugin -- picks color from canvas pixels.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - Tool: `eyedropper`
|
|
6
|
+
*
|
|
7
|
+
* Does NOT register its own command. Instead, it dispatches `set_foreground_color`
|
|
8
|
+
* (assumed to be registered by the color commands plugin). If that command is not
|
|
9
|
+
* registered, it falls back to a no-op with a console warning.
|
|
10
|
+
*
|
|
11
|
+
* Reads the pixel color under the pointer from the composited render buffer
|
|
12
|
+
* and sets it as the foreground color. Supports drag-to-sample via onPointerMove.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PluginModule } from '../../src/lib/core/plugin-loader.js';
|
|
16
|
+
import type { ToolContext } from '../../src/lib/core/plugin-types.js';
|
|
17
|
+
import { getRenderBuffer } from '../../src/lib/canvas/render-state.svelte.js';
|
|
18
|
+
import { rgbToHex } from '../../src/lib/color/color-utils.js';
|
|
19
|
+
import EyedropperIcon from '~icons/lucide/pipette';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sample the composited pixel at (canvasX, canvasY) and dispatch
|
|
23
|
+
* set_foreground_color. Skips fully transparent pixels (nothing to pick).
|
|
24
|
+
*/
|
|
25
|
+
function sampleAndDispatch(ctx: ToolContext): void {
|
|
26
|
+
const buffer = getRenderBuffer();
|
|
27
|
+
if (!buffer) return;
|
|
28
|
+
|
|
29
|
+
const x = Math.floor(ctx.canvasX);
|
|
30
|
+
const y = Math.floor(ctx.canvasY);
|
|
31
|
+
|
|
32
|
+
if (!buffer.inBounds(x, y)) return;
|
|
33
|
+
|
|
34
|
+
const [r, g, b, a] = buffer.getPixel(x, y);
|
|
35
|
+
|
|
36
|
+
// Fully transparent pixel -- nothing meaningful to sample
|
|
37
|
+
if (a === 0) return;
|
|
38
|
+
|
|
39
|
+
const hex = rgbToHex(r, g, b);
|
|
40
|
+
ctx.api.dispatch({
|
|
41
|
+
type: 'set_foreground_color',
|
|
42
|
+
plugin: 'builtin/eyedropper',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
params: { color: hex },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const eyedropperToolPlugin: PluginModule = {
|
|
49
|
+
name: 'builtin/eyedropper',
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
dependencies: [],
|
|
52
|
+
register(api) {
|
|
53
|
+
api.addToolbarItem('toolbar:drawing-tools:eyedropper', {
|
|
54
|
+
toolbarId: 'drawing-tools',
|
|
55
|
+
kind: 'tool',
|
|
56
|
+
targetId: 'eyedropper',
|
|
57
|
+
group: 'color',
|
|
58
|
+
order: 10,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let dragging = false;
|
|
62
|
+
|
|
63
|
+
api.addTool('eyedropper', {
|
|
64
|
+
icon: EyedropperIcon,
|
|
65
|
+
cursor: 'crosshair',
|
|
66
|
+
|
|
67
|
+
onPointerDown(_e, ctx) {
|
|
68
|
+
dragging = true;
|
|
69
|
+
sampleAndDispatch(ctx);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
onPointerMove(_e, ctx) {
|
|
73
|
+
// Drag-to-sample: continuously pick color while pointer is held down
|
|
74
|
+
if (!dragging) return;
|
|
75
|
+
sampleAndDispatch(ctx);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
onPointerUp() {
|
|
79
|
+
dragging = false;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fill Bucket Tool Plugin -- flood fills contiguous same-color regions.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - Command: `flood_fill` (tier: 'frame')
|
|
6
|
+
* - Tool: `fill`
|
|
7
|
+
*
|
|
8
|
+
* On click, performs a BFS flood fill from the clicked pixel, collecting all
|
|
9
|
+
* 4-connected pixels with the same color, then sets them to the foreground color.
|
|
10
|
+
* The entire operation is dispatched as a single undoable command.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PluginModule } from '../../src/lib/core/plugin-loader.js';
|
|
14
|
+
import type { ToolOption } from '../../src/lib/core/plugin-types.js';
|
|
15
|
+
import { floodFill, snapshotPixels, applyPixels, hexToRgba, makeSnapshotUndo } from './drawing-utils.js';
|
|
16
|
+
import { getToolOptionValue } from '../../src/lib/core/tool-options-state.svelte.js';
|
|
17
|
+
import FillIcon from '~icons/lucide/paint-bucket';
|
|
18
|
+
|
|
19
|
+
export const fillToolPlugin: PluginModule = {
|
|
20
|
+
name: 'builtin/fill',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
dependencies: [],
|
|
23
|
+
register(api) {
|
|
24
|
+
api.addCommand('flood_fill', {
|
|
25
|
+
tier: 'frame',
|
|
26
|
+
|
|
27
|
+
execute(params, ctx) {
|
|
28
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
29
|
+
if (!buffer) return;
|
|
30
|
+
|
|
31
|
+
const { r, g, b, a } = hexToRgba(params["color"]);
|
|
32
|
+
const tolerance = params["tolerance"] ?? 0;
|
|
33
|
+
const region = floodFill(buffer, params["x"], params["y"], tolerance);
|
|
34
|
+
|
|
35
|
+
const snapshot = snapshotPixels(buffer, region);
|
|
36
|
+
const fillPixels = region.map((p) => ({ ...p, r, g, b, a }));
|
|
37
|
+
applyPixels(buffer, fillPixels);
|
|
38
|
+
return snapshot;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
undo: makeSnapshotUndo(),
|
|
42
|
+
|
|
43
|
+
describe(params) {
|
|
44
|
+
const tol = params["tolerance"] ?? 0;
|
|
45
|
+
const tolSuffix = tol > 0 ? ` (tolerance ${String(tol)})` : '';
|
|
46
|
+
return `Flood fill at (${String(params["x"])}, ${String(params["y"])}) with ${String(params["color"])}${tolSuffix}`;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
api.addToolbarItem('toolbar:drawing-tools:fill', {
|
|
51
|
+
toolbarId: 'drawing-tools',
|
|
52
|
+
kind: 'tool',
|
|
53
|
+
targetId: 'fill',
|
|
54
|
+
group: 'draw',
|
|
55
|
+
order: 30,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const toleranceOption: ToolOption = {
|
|
59
|
+
id: 'tolerance',
|
|
60
|
+
label: 'Tolerance',
|
|
61
|
+
type: 'select',
|
|
62
|
+
choices: [
|
|
63
|
+
{ value: 0, label: '0' },
|
|
64
|
+
{ value: 10, label: '10' },
|
|
65
|
+
{ value: 25, label: '25' },
|
|
66
|
+
{ value: 50, label: '50' },
|
|
67
|
+
],
|
|
68
|
+
defaultValue: 0,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
api.addTool('fill', {
|
|
72
|
+
icon: FillIcon,
|
|
73
|
+
cursor: 'crosshair',
|
|
74
|
+
options: [toleranceOption],
|
|
75
|
+
|
|
76
|
+
onPointerDown(_e, ctx) {
|
|
77
|
+
const tolerance = (getToolOptionValue('fill', 'tolerance') as number) || 0;
|
|
78
|
+
ctx.api.dispatch({
|
|
79
|
+
type: 'flood_fill',
|
|
80
|
+
plugin: 'builtin/fill',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
params: {
|
|
83
|
+
x: ctx.canvasX,
|
|
84
|
+
y: ctx.canvasY,
|
|
85
|
+
color: ctx.color,
|
|
86
|
+
tolerance,
|
|
87
|
+
layerId: '',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradient Tool Plugin -- draws linear gradients between two points.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - Command: `draw_gradient` (tier: 'frame')
|
|
6
|
+
* - Tool: `gradient`
|
|
7
|
+
*
|
|
8
|
+
* Two modes:
|
|
9
|
+
* - 'smooth': linear RGB interpolation between color0 and color1
|
|
10
|
+
* - 'dithered': Bayer 2x2 ordered dithering between the two colors
|
|
11
|
+
*
|
|
12
|
+
* The gradient is drawn across the full canvas, projecting each pixel
|
|
13
|
+
* onto the line from (x0,y0) to (x1,y1).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PluginModule } from '../../src/lib/core/plugin-loader.js';
|
|
17
|
+
import type { PixelData, Point } from './drawing-utils.js';
|
|
18
|
+
import { snapshotPixels, applyPixels, hexToRgba, makeSnapshotUndo } from './drawing-utils.js';
|
|
19
|
+
import { setShapePreview, clearShapePreview } from '../../src/lib/canvas/shape-preview-state.svelte.js';
|
|
20
|
+
import GradientIcon from '~icons/lucide/droplets';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bayer 2x2 dither matrix.
|
|
24
|
+
* Threshold values are 0-3, normalized by dividing by 4.
|
|
25
|
+
*/
|
|
26
|
+
const BAYER_2X2 = [
|
|
27
|
+
[0, 2],
|
|
28
|
+
[3, 1],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** Linearly interpolate between two values. */
|
|
32
|
+
function lerp(a: number, b: number, t: number): number {
|
|
33
|
+
return Math.round(a + (b - a) * t);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute gradient pixels for the region covered by the line from (x0,y0) to (x1,y1).
|
|
38
|
+
* Each pixel on the canvas is projected onto the gradient line to determine its t value.
|
|
39
|
+
*/
|
|
40
|
+
export function computeGradient(
|
|
41
|
+
width: number,
|
|
42
|
+
height: number,
|
|
43
|
+
x0: number,
|
|
44
|
+
y0: number,
|
|
45
|
+
x1: number,
|
|
46
|
+
y1: number,
|
|
47
|
+
color0: string,
|
|
48
|
+
color1: string,
|
|
49
|
+
mode: 'smooth' | 'dithered',
|
|
50
|
+
): PixelData[] {
|
|
51
|
+
const c0 = hexToRgba(color0);
|
|
52
|
+
const c1 = hexToRgba(color1);
|
|
53
|
+
const pixels: PixelData[] = [];
|
|
54
|
+
|
|
55
|
+
// Direction vector and its squared length
|
|
56
|
+
const dx = x1 - x0;
|
|
57
|
+
const dy = y1 - y0;
|
|
58
|
+
const lenSq = dx * dx + dy * dy;
|
|
59
|
+
|
|
60
|
+
// Degenerate case: start and end are the same point; fill with color0
|
|
61
|
+
if (lenSq === 0) {
|
|
62
|
+
for (let py = 0; py < height; py++) {
|
|
63
|
+
for (let px = 0; px < width; px++) {
|
|
64
|
+
pixels.push({ x: px, y: py, r: c0.r, g: c0.g, b: c0.b, a: c0.a });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return pixels;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let py = 0; py < height; py++) {
|
|
71
|
+
for (let px = 0; px < width; px++) {
|
|
72
|
+
// Project (px, py) onto the gradient line to find parameter t
|
|
73
|
+
const t = Math.max(0, Math.min(1,
|
|
74
|
+
((px - x0) * dx + (py - y0) * dy) / lenSq,
|
|
75
|
+
));
|
|
76
|
+
|
|
77
|
+
if (mode === 'smooth') {
|
|
78
|
+
pixels.push({
|
|
79
|
+
x: px,
|
|
80
|
+
y: py,
|
|
81
|
+
r: lerp(c0.r, c1.r, t),
|
|
82
|
+
g: lerp(c0.g, c1.g, t),
|
|
83
|
+
b: lerp(c0.b, c1.b, t),
|
|
84
|
+
a: lerp(c0.a, c1.a, t),
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
// Dithered mode: use Bayer 2x2 threshold to choose between c0 and c1
|
|
88
|
+
const row = BAYER_2X2[py % 2];
|
|
89
|
+
const threshold = (row?.[px % 2] ?? 0) / 4;
|
|
90
|
+
const useColor1 = t > threshold;
|
|
91
|
+
const c = useColor1 ? c1 : c0;
|
|
92
|
+
pixels.push({ x: px, y: py, r: c.r, g: c.g, b: c.b, a: c.a });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return pixels;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const gradientToolPlugin: PluginModule = {
|
|
101
|
+
name: 'builtin/gradient',
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
dependencies: [],
|
|
104
|
+
register(api) {
|
|
105
|
+
api.addCommand('draw_gradient', {
|
|
106
|
+
tier: 'frame',
|
|
107
|
+
|
|
108
|
+
execute(params, ctx) {
|
|
109
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
110
|
+
if (!buffer) return;
|
|
111
|
+
|
|
112
|
+
const allPoints: Point[] = [];
|
|
113
|
+
for (let y = 0; y < buffer.height; y++) {
|
|
114
|
+
for (let x = 0; x < buffer.width; x++) {
|
|
115
|
+
allPoints.push({ x, y });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const snapshot = snapshotPixels(buffer, allPoints);
|
|
119
|
+
|
|
120
|
+
const pixels = computeGradient(
|
|
121
|
+
buffer.width,
|
|
122
|
+
buffer.height,
|
|
123
|
+
params["x0"],
|
|
124
|
+
params["y0"],
|
|
125
|
+
params["x1"],
|
|
126
|
+
params["y1"],
|
|
127
|
+
params["color0"],
|
|
128
|
+
params["color1"],
|
|
129
|
+
params["mode"],
|
|
130
|
+
);
|
|
131
|
+
applyPixels(buffer, pixels);
|
|
132
|
+
return snapshot;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
undo: makeSnapshotUndo(),
|
|
136
|
+
|
|
137
|
+
describe(params) {
|
|
138
|
+
return `Drew ${String(params["mode"])} gradient from (${String(params["x0"])},${String(params["y0"])}) to (${String(params["x1"])},${String(params["y1"])})`;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
api.addToolbarItem('toolbar:drawing-tools:gradient', {
|
|
143
|
+
toolbarId: 'drawing-tools',
|
|
144
|
+
kind: 'tool',
|
|
145
|
+
targetId: 'gradient',
|
|
146
|
+
group: 'color',
|
|
147
|
+
order: 20,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// -- Stroke state for drag interaction --
|
|
151
|
+
let drawing = false;
|
|
152
|
+
let startX = 0;
|
|
153
|
+
let startY = 0;
|
|
154
|
+
let drawColor = '';
|
|
155
|
+
|
|
156
|
+
api.addTool('gradient', {
|
|
157
|
+
icon: GradientIcon,
|
|
158
|
+
cursor: 'crosshair',
|
|
159
|
+
|
|
160
|
+
onPointerDown(_e, ctx) {
|
|
161
|
+
drawing = true;
|
|
162
|
+
startX = ctx.canvasX;
|
|
163
|
+
startY = ctx.canvasY;
|
|
164
|
+
drawColor = ctx.color;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
onPointerMove(_e, ctx) {
|
|
168
|
+
if (!drawing) return;
|
|
169
|
+
// Show the gradient axis as a line preview
|
|
170
|
+
setShapePreview({
|
|
171
|
+
type: 'line',
|
|
172
|
+
startX,
|
|
173
|
+
startY,
|
|
174
|
+
endX: ctx.canvasX,
|
|
175
|
+
endY: ctx.canvasY,
|
|
176
|
+
color: drawColor,
|
|
177
|
+
filled: false,
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
onPointerUp(_e, ctx) {
|
|
182
|
+
if (!drawing) return;
|
|
183
|
+
drawing = false;
|
|
184
|
+
clearShapePreview();
|
|
185
|
+
|
|
186
|
+
ctx.api.dispatch({
|
|
187
|
+
type: 'draw_gradient',
|
|
188
|
+
plugin: 'builtin/gradient',
|
|
189
|
+
version: '1.0.0',
|
|
190
|
+
params: {
|
|
191
|
+
x0: startX,
|
|
192
|
+
y0: startY,
|
|
193
|
+
x1: ctx.canvasX,
|
|
194
|
+
y1: ctx.canvasY,
|
|
195
|
+
color0: drawColor,
|
|
196
|
+
color1: '#000000',
|
|
197
|
+
mode: 'smooth',
|
|
198
|
+
layerId: '',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the gradient tool plugin.
|
|
3
|
+
*
|
|
4
|
+
* Covers smooth and dithered gradient modes, horizontal and diagonal gradients.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
import { PixelBuffer } from '../../src/lib/canvas/pixel-buffer.js';
|
|
9
|
+
import { commandRegistry } from '../../src/lib/core/registries.svelte.js';
|
|
10
|
+
import { dispatch, undoLast, _resetForTesting as resetDispatcher, setContext } from '../../src/lib/core/dispatcher.js';
|
|
11
|
+
import type { Command } from '../../src/lib/core/commands.js';
|
|
12
|
+
import type { CommandType, ParamsOf } from '../../src/lib/core/command-params.js';
|
|
13
|
+
import { createPluginAPI } from '../../src/lib/core/plugin-api.js';
|
|
14
|
+
|
|
15
|
+
import { gradientToolPlugin, computeGradient } from './gradient-tool.js';
|
|
16
|
+
|
|
17
|
+
// --- Helpers ---
|
|
18
|
+
|
|
19
|
+
// Typed overload: validates params when type is a known command literal
|
|
20
|
+
function makeCommand<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
|
|
21
|
+
// String fallback: for dynamic/unknown command types in tests
|
|
22
|
+
function makeCommand(type: string, params?: Record<string, unknown>): Command;
|
|
23
|
+
function makeCommand(type: string, params: Record<string, unknown> = {}): Command {
|
|
24
|
+
return { type, plugin: 'test', version: '1.0.0', params, timestamp: Date.now(), id: crypto.randomUUID() };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupBuffer(width = 8, height = 8): PixelBuffer {
|
|
28
|
+
const buffer = new PixelBuffer(width, height);
|
|
29
|
+
setContext({ getActiveBuffer: () => buffer });
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Tests ---
|
|
34
|
+
|
|
35
|
+
describe('Gradient Tool Plugin', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
resetDispatcher();
|
|
38
|
+
const api = createPluginAPI('builtin/gradient');
|
|
39
|
+
gradientToolPlugin.register(api);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should register the draw_gradient command', () => {
|
|
43
|
+
expect(commandRegistry.get('draw_gradient')).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('smooth gradient: endpoints have correct colors', () => {
|
|
47
|
+
// Horizontal gradient across an 8-wide canvas
|
|
48
|
+
const pixels = computeGradient(8, 1, 0, 0, 7, 0, '#FF0000', '#0000FF', 'smooth');
|
|
49
|
+
|
|
50
|
+
// Leftmost pixel should be red
|
|
51
|
+
const first = pixels[0];
|
|
52
|
+
if (!first) throw new Error("missing pixel");
|
|
53
|
+
expect(first.r).toBe(255);
|
|
54
|
+
expect(first.g).toBe(0);
|
|
55
|
+
expect(first.b).toBe(0);
|
|
56
|
+
|
|
57
|
+
// Rightmost pixel should be blue
|
|
58
|
+
const last = pixels[7];
|
|
59
|
+
if (!last) throw new Error("missing pixel");
|
|
60
|
+
expect(last.r).toBe(0);
|
|
61
|
+
expect(last.g).toBe(0);
|
|
62
|
+
expect(last.b).toBe(255);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('smooth gradient: midpoint is blended', () => {
|
|
66
|
+
const pixels = computeGradient(8, 1, 0, 0, 7, 0, '#FF0000', '#0000FF', 'smooth');
|
|
67
|
+
// Midpoint (x=3 or x=4) should have some red and some blue
|
|
68
|
+
const mid = pixels[4];
|
|
69
|
+
if (!mid) throw new Error("missing pixel");
|
|
70
|
+
expect(mid.r).toBeGreaterThan(0);
|
|
71
|
+
expect(mid.b).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('dithered gradient: produces a pattern (not solid colors)', () => {
|
|
75
|
+
// 4x4 canvas with horizontal dithered gradient
|
|
76
|
+
const pixels = computeGradient(4, 4, 0, 0, 3, 0, '#FF0000', '#0000FF', 'dithered');
|
|
77
|
+
|
|
78
|
+
// Collect unique colors
|
|
79
|
+
const colorSet = new Set<string>();
|
|
80
|
+
for (const p of pixels) {
|
|
81
|
+
colorSet.add(`${String(p.r)},${String(p.g)},${String(p.b)}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Should have at least 2 distinct colors (the dither pattern)
|
|
85
|
+
expect(colorSet.size).toBeGreaterThanOrEqual(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('horizontal gradient fills all pixels', () => {
|
|
89
|
+
const buffer = setupBuffer(8, 4);
|
|
90
|
+
|
|
91
|
+
dispatch(makeCommand('draw_gradient', {
|
|
92
|
+
x0: 0, y0: 0, x1: 7, y1: 0,
|
|
93
|
+
color0: '#FF0000', color1: '#0000FF',
|
|
94
|
+
mode: 'smooth',
|
|
95
|
+
layerId: '',
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
// Every pixel should be non-transparent
|
|
99
|
+
for (let y = 0; y < 4; y++) {
|
|
100
|
+
for (let x = 0; x < 8; x++) {
|
|
101
|
+
const [, , , a] = buffer.getPixel(x, y);
|
|
102
|
+
expect(a).toBe(255);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('diagonal gradient works correctly', () => {
|
|
108
|
+
const pixels = computeGradient(4, 4, 0, 0, 3, 3, '#000000', '#FFFFFF', 'smooth');
|
|
109
|
+
|
|
110
|
+
// Top-left (0,0) should be black
|
|
111
|
+
const topLeft = pixels[0];
|
|
112
|
+
if (!topLeft) throw new Error("missing pixel");
|
|
113
|
+
expect(topLeft.r).toBe(0);
|
|
114
|
+
expect(topLeft.g).toBe(0);
|
|
115
|
+
expect(topLeft.b).toBe(0);
|
|
116
|
+
|
|
117
|
+
// Bottom-right (3,3) should be white
|
|
118
|
+
const bottomRight = pixels[3 * 4 + 3];
|
|
119
|
+
if (!bottomRight) throw new Error("missing pixel");
|
|
120
|
+
expect(bottomRight.r).toBe(255);
|
|
121
|
+
expect(bottomRight.g).toBe(255);
|
|
122
|
+
expect(bottomRight.b).toBe(255);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('gradient undoes correctly', () => {
|
|
126
|
+
const buffer = setupBuffer(4, 4);
|
|
127
|
+
|
|
128
|
+
dispatch(makeCommand('draw_gradient', {
|
|
129
|
+
x0: 0, y0: 0, x1: 3, y1: 0,
|
|
130
|
+
color0: '#FF0000', color1: '#0000FF',
|
|
131
|
+
mode: 'smooth',
|
|
132
|
+
layerId: '',
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
expect(buffer.getPixel(0, 0)[0]).toBe(255); // Red
|
|
136
|
+
|
|
137
|
+
undoLast();
|
|
138
|
+
|
|
139
|
+
// Should be back to transparent
|
|
140
|
+
expect(buffer.getPixel(0, 0)).toEqual([0, 0, 0, 0]);
|
|
141
|
+
});
|
|
142
|
+
});
|