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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aseprite Importer Plugin -- registers an importer for .ase/.aseprite files.
|
|
3
|
+
*
|
|
4
|
+
* Converts parsed Aseprite data into PixelWeaver's internal state:
|
|
5
|
+
* canvas dimensions, layers, frames with pixel data, and palette.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PluginModule } from '../../../src/lib/core/plugin-loader.js';
|
|
9
|
+
import type { PluginAPI } from '../../../src/lib/core/plugin-types.js';
|
|
10
|
+
import FileImage from '~icons/lucide/file-image';
|
|
11
|
+
import { parseAseprite } from './aseprite-parser.js';
|
|
12
|
+
import type { Palette } from '../../../src/lib/color/palette.js';
|
|
13
|
+
import type { AsepriteLayer } from './aseprite-parser.js';
|
|
14
|
+
import { rgbToHex } from '../../../src/lib/color/color-utils.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a PixelWeaver layer tree from the flat Aseprite layer list.
|
|
18
|
+
* Aseprite layers use childLevel to indicate nesting depth; we reconstruct
|
|
19
|
+
* the parent-child relationships and create PW layers accordingly.
|
|
20
|
+
*
|
|
21
|
+
* Returns a map from Aseprite layer index to PixelWeaver layer ID.
|
|
22
|
+
*/
|
|
23
|
+
function createLayerTree(api: PluginAPI, aseLayers: AsepriteLayer[]): Map<number, string> {
|
|
24
|
+
const indexToId = new Map<number, string>();
|
|
25
|
+
|
|
26
|
+
// Track the group stack: each entry is the PW group layer ID at that depth.
|
|
27
|
+
// childLevel 0 = root, childLevel 1 = inside a depth-0 group, etc.
|
|
28
|
+
const groupStack: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < aseLayers.length; i++) {
|
|
31
|
+
const aseLayer = aseLayers[i];
|
|
32
|
+
if (!aseLayer) continue;
|
|
33
|
+
|
|
34
|
+
// Trim the group stack to match the current child level
|
|
35
|
+
while (groupStack.length > aseLayer.childLevel) {
|
|
36
|
+
groupStack.pop();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const parentId = groupStack.length > 0 ? groupStack[groupStack.length - 1] : undefined;
|
|
40
|
+
const isVisible = (aseLayer.flags & 1) !== 0;
|
|
41
|
+
// Aseprite opacity is 0-255, PixelWeaver uses 0-100
|
|
42
|
+
const pwOpacity = Math.round((aseLayer.opacity / 255) * 100);
|
|
43
|
+
|
|
44
|
+
// Build options object, omitting parentId entirely when undefined
|
|
45
|
+
// (exactOptionalPropertyTypes disallows passing { parentId: undefined })
|
|
46
|
+
const layerOptions = parentId !== undefined ? { parentId } : {};
|
|
47
|
+
|
|
48
|
+
if (aseLayer.type === 1) {
|
|
49
|
+
// Group layer
|
|
50
|
+
const groupId = api.addGroup(aseLayer.name, layerOptions);
|
|
51
|
+
api.setLayerVisibility(groupId, isVisible);
|
|
52
|
+
api.setLayerOpacity(groupId, pwOpacity);
|
|
53
|
+
indexToId.set(i, groupId);
|
|
54
|
+
groupStack.push(groupId);
|
|
55
|
+
} else {
|
|
56
|
+
// Pixel layer (type 0) or tilemap (type 2, treated as pixel)
|
|
57
|
+
const layerId = api.addLayer(aseLayer.name, layerOptions);
|
|
58
|
+
api.setLayerVisibility(layerId, isVisible);
|
|
59
|
+
api.setLayerOpacity(layerId, pwOpacity);
|
|
60
|
+
indexToId.set(i, layerId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return indexToId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const asepriteImporterPlugin: PluginModule = {
|
|
68
|
+
name: 'builtin/import-aseprite',
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
description: 'Import Aseprite .ase/.aseprite files',
|
|
71
|
+
|
|
72
|
+
register(api) {
|
|
73
|
+
api.addCommand('import_aseprite', {
|
|
74
|
+
tier: 'project',
|
|
75
|
+
execute() { /* Will be wired to file picker */ },
|
|
76
|
+
undo() {},
|
|
77
|
+
describe() { return 'Import Aseprite file'; },
|
|
78
|
+
label: 'Aseprite (.ase/.aseprite)',
|
|
79
|
+
category: 'File',
|
|
80
|
+
icon: FileImage,
|
|
81
|
+
});
|
|
82
|
+
api.addMenuItem('menu:file:import:aseprite', {
|
|
83
|
+
commandId: 'import_aseprite',
|
|
84
|
+
menuPath: 'file/import',
|
|
85
|
+
group: 'import',
|
|
86
|
+
order: 10,
|
|
87
|
+
label: 'Aseprite (.ase/.aseprite)',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
api.addImporter('aseprite', {
|
|
91
|
+
label: 'Aseprite File',
|
|
92
|
+
extensions: ['ase', 'aseprite'],
|
|
93
|
+
|
|
94
|
+
async import(data) {
|
|
95
|
+
const buffer = data instanceof File ? await data.arrayBuffer() : data;
|
|
96
|
+
const aseFile = await parseAseprite(buffer);
|
|
97
|
+
|
|
98
|
+
// 1. Set canvas dimensions
|
|
99
|
+
api.setCanvasSize(aseFile.width, aseFile.height);
|
|
100
|
+
|
|
101
|
+
// 2. Clear existing layers and frames to start fresh
|
|
102
|
+
api.deserializeLayers({ layers: [], activeLayerId: '' });
|
|
103
|
+
api.deserializeFrames({
|
|
104
|
+
frames: [],
|
|
105
|
+
currentFrameIndex: 0,
|
|
106
|
+
globalFps: 12,
|
|
107
|
+
originX: 0,
|
|
108
|
+
originY: 0,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 3. Create PixelWeaver layers from the Aseprite layer list
|
|
112
|
+
const layerIndexToId = createLayerTree(api, aseFile.layers);
|
|
113
|
+
|
|
114
|
+
// 4. Create frames and populate pixel data
|
|
115
|
+
for (let f = 0; f < aseFile.frames.length; f++) {
|
|
116
|
+
const aseFrame = aseFile.frames[f];
|
|
117
|
+
if (!aseFrame) continue;
|
|
118
|
+
const frameIndex = api.addFrame({ afterIndex: f - 1 });
|
|
119
|
+
|
|
120
|
+
// Set per-frame duration if specified
|
|
121
|
+
if (aseFrame.duration > 0) {
|
|
122
|
+
api.setFrameDuration(frameIndex, aseFrame.duration);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Copy cel pixel data into each layer's PixelBuffer for this frame
|
|
126
|
+
for (const cel of aseFrame.cels) {
|
|
127
|
+
const layerId = layerIndexToId.get(cel.layerIndex);
|
|
128
|
+
if (!layerId) continue; // layer not mapped (e.g., unsupported type)
|
|
129
|
+
if (cel.pixels.length === 0) continue; // empty or unsupported cel
|
|
130
|
+
|
|
131
|
+
// Create a full-canvas-sized PixelBuffer and place cel pixels at (cel.x, cel.y)
|
|
132
|
+
const pixelBuffer = api.createPixelBuffer(aseFile.width, aseFile.height);
|
|
133
|
+
|
|
134
|
+
for (let py = 0; py < cel.height; py++) {
|
|
135
|
+
for (let px = 0; px < cel.width; px++) {
|
|
136
|
+
const srcIdx = (py * cel.width + px) * 4;
|
|
137
|
+
const destX = cel.x + px;
|
|
138
|
+
const destY = cel.y + py;
|
|
139
|
+
pixelBuffer.setPixel(
|
|
140
|
+
destX,
|
|
141
|
+
destY,
|
|
142
|
+
cel.pixels[srcIdx] ?? 0,
|
|
143
|
+
cel.pixels[srcIdx + 1] ?? 0,
|
|
144
|
+
cel.pixels[srcIdx + 2] ?? 0,
|
|
145
|
+
cel.pixels[srcIdx + 3] ?? 0,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
api.setFramePixelData(frameIndex, layerId, pixelBuffer);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Select the first frame
|
|
155
|
+
if (api.getFrames().length > 0) {
|
|
156
|
+
api.setCurrentFrame(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 5. Set the project palette from Aseprite colors
|
|
160
|
+
if (aseFile.palette.length > 0) {
|
|
161
|
+
const palette: Palette = {
|
|
162
|
+
name: 'Imported (Aseprite)',
|
|
163
|
+
colors: aseFile.palette
|
|
164
|
+
.filter((c) => c.a > 0) // skip fully transparent entries
|
|
165
|
+
.map((c) => rgbToHex(c.r, c.g, c.b)),
|
|
166
|
+
};
|
|
167
|
+
// Deduplicate colors while preserving order
|
|
168
|
+
palette.colors = [...new Set(palette.colors)];
|
|
169
|
+
api.setProjectPalette(palette);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
};
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aseprite (.ase/.aseprite) binary file parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses the binary format into a structured AsepriteFile object with layers,
|
|
5
|
+
* frames, cels, and palette data. All pixel data is normalized to RGBA
|
|
6
|
+
* regardless of the source color depth (indexed, grayscale, or RGBA).
|
|
7
|
+
*
|
|
8
|
+
* Format spec: https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md
|
|
9
|
+
* All multi-byte integers are little-endian.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// --- Public types ---
|
|
13
|
+
|
|
14
|
+
export interface AsepriteFile {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
colorDepth: number; // 8 = indexed, 16 = grayscale, 32 = RGBA
|
|
18
|
+
transparentIndex: number;
|
|
19
|
+
frames: AsepriteFrame[];
|
|
20
|
+
layers: AsepriteLayer[];
|
|
21
|
+
palette: AsepriteColor[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AsepriteFrame {
|
|
25
|
+
duration: number; // milliseconds
|
|
26
|
+
cels: AsepriteCel[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AsepriteLayer {
|
|
30
|
+
name: string;
|
|
31
|
+
flags: number; // bit 0: visible, bit 1: editable, bit 4: group
|
|
32
|
+
type: number; // 0 = normal (image), 1 = group, 2 = tilemap
|
|
33
|
+
opacity: number; // 0-255
|
|
34
|
+
blendMode: number;
|
|
35
|
+
childLevel: number; // nesting depth (0 = root)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AsepriteCel {
|
|
39
|
+
layerIndex: number;
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
opacity: number; // 0-255
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
pixels: Uint8Array; // always RGBA, 4 bytes per pixel
|
|
46
|
+
linkedFrame?: number; // if this cel references another frame's cel
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AsepriteColor {
|
|
50
|
+
r: number;
|
|
51
|
+
g: number;
|
|
52
|
+
b: number;
|
|
53
|
+
a: number;
|
|
54
|
+
name?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Constants ---
|
|
58
|
+
|
|
59
|
+
const FILE_MAGIC = 0xa5e0;
|
|
60
|
+
const FRAME_MAGIC = 0xf1fa;
|
|
61
|
+
|
|
62
|
+
// Chunk type identifiers
|
|
63
|
+
const CHUNK_LAYER = 0x2004;
|
|
64
|
+
const CHUNK_CEL = 0x2005;
|
|
65
|
+
const CHUNK_PALETTE = 0x2019;
|
|
66
|
+
const CHUNK_OLD_PALETTE_1 = 0x0004; // legacy palette (fallback for indexed files)
|
|
67
|
+
const CHUNK_OLD_PALETTE_2 = 0x0011; // legacy palette variant
|
|
68
|
+
|
|
69
|
+
// Cel type identifiers
|
|
70
|
+
const CEL_RAW = 0;
|
|
71
|
+
const CEL_LINKED = 1;
|
|
72
|
+
const CEL_COMPRESSED = 2;
|
|
73
|
+
const CEL_COMPRESSED_TILEMAP = 3;
|
|
74
|
+
|
|
75
|
+
// --- Binary reader helper ---
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wraps a DataView with a position cursor for sequential reads.
|
|
79
|
+
* All reads are little-endian as per the Aseprite format.
|
|
80
|
+
*/
|
|
81
|
+
class BinaryReader {
|
|
82
|
+
private view: DataView;
|
|
83
|
+
pos: number;
|
|
84
|
+
|
|
85
|
+
constructor(buffer: ArrayBuffer, offset = 0) {
|
|
86
|
+
this.view = new DataView(buffer);
|
|
87
|
+
this.pos = offset;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
byte(): number {
|
|
91
|
+
const v = this.view.getUint8(this.pos);
|
|
92
|
+
this.pos += 1;
|
|
93
|
+
return v;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
word(): number {
|
|
97
|
+
const v = this.view.getUint16(this.pos, true);
|
|
98
|
+
this.pos += 2;
|
|
99
|
+
return v;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
short(): number {
|
|
103
|
+
const v = this.view.getInt16(this.pos, true);
|
|
104
|
+
this.pos += 2;
|
|
105
|
+
return v;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
dword(): number {
|
|
109
|
+
const v = this.view.getUint32(this.pos, true);
|
|
110
|
+
this.pos += 4;
|
|
111
|
+
return v;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Read a STRING: WORD length prefix followed by UTF-8 bytes. */
|
|
115
|
+
string(): string {
|
|
116
|
+
const len = this.word();
|
|
117
|
+
const bytes = new Uint8Array(this.view.buffer, this.pos, len);
|
|
118
|
+
this.pos += len;
|
|
119
|
+
return new TextDecoder().decode(bytes);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read raw bytes without advancing the position cursor. */
|
|
123
|
+
bytes(length: number): Uint8Array {
|
|
124
|
+
const result = new Uint8Array(this.view.buffer, this.pos, length);
|
|
125
|
+
this.pos += length;
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Skip forward by n bytes. */
|
|
130
|
+
skip(n: number): void {
|
|
131
|
+
this.pos += n;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Zlib decompression using browser DecompressionStream ---
|
|
136
|
+
|
|
137
|
+
async function decompress(data: Uint8Array): Promise<Uint8Array> {
|
|
138
|
+
// DecompressionStream('deflate') handles raw deflate; Aseprite uses zlib
|
|
139
|
+
// which is deflate with a zlib header -- 'deflate' mode in the Web API
|
|
140
|
+
// actually handles the zlib wrapper (RFC 1950), not raw deflate (RFC 1951).
|
|
141
|
+
const ds = new DecompressionStream('deflate');
|
|
142
|
+
const writer = ds.writable.getWriter();
|
|
143
|
+
void writer.write(data as unknown as BufferSource);
|
|
144
|
+
void writer.close();
|
|
145
|
+
const reader = ds.readable.getReader();
|
|
146
|
+
const chunks: Uint8Array[] = [];
|
|
147
|
+
for (;;) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (done) break;
|
|
150
|
+
chunks.push(value);
|
|
151
|
+
}
|
|
152
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
153
|
+
const result = new Uint8Array(totalLength);
|
|
154
|
+
let offset = 0;
|
|
155
|
+
for (const chunk of chunks) {
|
|
156
|
+
result.set(chunk, offset);
|
|
157
|
+
offset += chunk.length;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Pixel format conversion ---
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Convert indexed pixel data to RGBA using the palette.
|
|
166
|
+
* Pixels matching transparentIndex become fully transparent.
|
|
167
|
+
*/
|
|
168
|
+
function indexedToRgba(
|
|
169
|
+
indexed: Uint8Array,
|
|
170
|
+
palette: AsepriteColor[],
|
|
171
|
+
transparentIndex: number,
|
|
172
|
+
): Uint8Array {
|
|
173
|
+
const rgba = new Uint8Array(indexed.length * 4);
|
|
174
|
+
for (let i = 0; i < indexed.length; i++) {
|
|
175
|
+
const idx = indexed[i];
|
|
176
|
+
const out = i * 4;
|
|
177
|
+
if (idx === undefined || idx === transparentIndex) {
|
|
178
|
+
// Fully transparent
|
|
179
|
+
rgba[out] = 0;
|
|
180
|
+
rgba[out + 1] = 0;
|
|
181
|
+
rgba[out + 2] = 0;
|
|
182
|
+
rgba[out + 3] = 0;
|
|
183
|
+
} else if (idx < palette.length) {
|
|
184
|
+
const entry = palette[idx];
|
|
185
|
+
if (entry) {
|
|
186
|
+
rgba[out] = entry.r;
|
|
187
|
+
rgba[out + 1] = entry.g;
|
|
188
|
+
rgba[out + 2] = entry.b;
|
|
189
|
+
rgba[out + 3] = entry.a;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Out-of-range index: treat as transparent
|
|
193
|
+
rgba[out] = 0;
|
|
194
|
+
rgba[out + 1] = 0;
|
|
195
|
+
rgba[out + 2] = 0;
|
|
196
|
+
rgba[out + 3] = 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return rgba;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Convert grayscale pixel data (2 bytes per pixel: gray, alpha) to RGBA.
|
|
204
|
+
*/
|
|
205
|
+
function grayscaleToRgba(gray: Uint8Array): Uint8Array {
|
|
206
|
+
const pixelCount = gray.length / 2;
|
|
207
|
+
const rgba = new Uint8Array(pixelCount * 4);
|
|
208
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
209
|
+
const g = gray[i * 2] ?? 0;
|
|
210
|
+
const a = gray[i * 2 + 1] ?? 0;
|
|
211
|
+
const out = i * 4;
|
|
212
|
+
rgba[out] = g;
|
|
213
|
+
rgba[out + 1] = g;
|
|
214
|
+
rgba[out + 2] = g;
|
|
215
|
+
rgba[out + 3] = a;
|
|
216
|
+
}
|
|
217
|
+
return rgba;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- Chunk parsers ---
|
|
221
|
+
|
|
222
|
+
function parseLayerChunk(reader: BinaryReader): AsepriteLayer {
|
|
223
|
+
const flags = reader.word();
|
|
224
|
+
const type = reader.word();
|
|
225
|
+
const childLevel = reader.word();
|
|
226
|
+
reader.word(); // default width (ignored)
|
|
227
|
+
reader.word(); // default height (ignored)
|
|
228
|
+
const blendMode = reader.word();
|
|
229
|
+
const opacity = reader.byte();
|
|
230
|
+
reader.skip(3); // reserved
|
|
231
|
+
const name = reader.string();
|
|
232
|
+
|
|
233
|
+
return { name, flags, type, opacity, blendMode, childLevel };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse the cel chunk header and raw/compressed pixel data.
|
|
238
|
+
* Returns the cel metadata; pixel data for compressed cels is decompressed asynchronously.
|
|
239
|
+
*/
|
|
240
|
+
async function parseCelChunk(
|
|
241
|
+
reader: BinaryReader,
|
|
242
|
+
chunkEnd: number,
|
|
243
|
+
colorDepth: number,
|
|
244
|
+
palette: AsepriteColor[],
|
|
245
|
+
transparentIndex: number,
|
|
246
|
+
): Promise<AsepriteCel> {
|
|
247
|
+
const layerIndex = reader.word();
|
|
248
|
+
const x = reader.short();
|
|
249
|
+
const y = reader.short();
|
|
250
|
+
const opacity = reader.byte();
|
|
251
|
+
const celType = reader.word();
|
|
252
|
+
reader.skip(7); // reserved / z-index (not needed)
|
|
253
|
+
|
|
254
|
+
if (celType === CEL_LINKED) {
|
|
255
|
+
// Linked cel: references pixels from another frame
|
|
256
|
+
const frameIndex = reader.word();
|
|
257
|
+
return {
|
|
258
|
+
layerIndex,
|
|
259
|
+
x,
|
|
260
|
+
y,
|
|
261
|
+
opacity,
|
|
262
|
+
width: 0,
|
|
263
|
+
height: 0,
|
|
264
|
+
pixels: new Uint8Array(0),
|
|
265
|
+
linkedFrame: frameIndex,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (celType === CEL_COMPRESSED_TILEMAP) {
|
|
270
|
+
// Tilemap cels are not supported; skip the rest
|
|
271
|
+
return {
|
|
272
|
+
layerIndex,
|
|
273
|
+
x,
|
|
274
|
+
y,
|
|
275
|
+
opacity,
|
|
276
|
+
width: 0,
|
|
277
|
+
height: 0,
|
|
278
|
+
pixels: new Uint8Array(0),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const width = reader.word();
|
|
283
|
+
const height = reader.word();
|
|
284
|
+
|
|
285
|
+
let rawPixels: Uint8Array;
|
|
286
|
+
|
|
287
|
+
if (celType === CEL_RAW) {
|
|
288
|
+
// Bytes per pixel depends on color depth: 4 (RGBA), 2 (grayscale), 1 (indexed)
|
|
289
|
+
const bpp = colorDepth / 8;
|
|
290
|
+
rawPixels = reader.bytes(width * height * bpp);
|
|
291
|
+
} else if (celType === CEL_COMPRESSED) {
|
|
292
|
+
// Remaining bytes in this chunk are zlib-compressed pixel data
|
|
293
|
+
const compressedSize = chunkEnd - reader.pos;
|
|
294
|
+
const compressed = reader.bytes(compressedSize);
|
|
295
|
+
rawPixels = await decompress(compressed);
|
|
296
|
+
} else {
|
|
297
|
+
// Unknown cel type; return empty
|
|
298
|
+
return { layerIndex, x, y, opacity, width, height, pixels: new Uint8Array(0) };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Convert to RGBA based on color depth
|
|
302
|
+
let rgbaPixels: Uint8Array;
|
|
303
|
+
if (colorDepth === 32) {
|
|
304
|
+
rgbaPixels = rawPixels;
|
|
305
|
+
} else if (colorDepth === 16) {
|
|
306
|
+
rgbaPixels = grayscaleToRgba(rawPixels);
|
|
307
|
+
} else {
|
|
308
|
+
// colorDepth === 8 (indexed)
|
|
309
|
+
rgbaPixels = indexedToRgba(rawPixels, palette, transparentIndex);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { layerIndex, x, y, opacity, width, height, pixels: rgbaPixels };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parsePaletteChunk(reader: BinaryReader): AsepriteColor[] {
|
|
316
|
+
const size = reader.dword();
|
|
317
|
+
const firstIndex = reader.dword();
|
|
318
|
+
const lastIndex = reader.dword();
|
|
319
|
+
reader.skip(8); // reserved
|
|
320
|
+
|
|
321
|
+
// Pre-fill with transparent black for indices before firstIndex
|
|
322
|
+
const colors: AsepriteColor[] = Array.from(
|
|
323
|
+
{ length: size },
|
|
324
|
+
(): AsepriteColor => ({ r: 0, g: 0, b: 0, a: 255 }),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
for (let i = firstIndex; i <= lastIndex; i++) {
|
|
328
|
+
const flags = reader.word();
|
|
329
|
+
const r = reader.byte();
|
|
330
|
+
const g = reader.byte();
|
|
331
|
+
const b = reader.byte();
|
|
332
|
+
const a = reader.byte();
|
|
333
|
+
// Omit name when absent (exactOptionalPropertyTypes disallows explicit undefined)
|
|
334
|
+
const color: AsepriteColor = { r, g, b, a };
|
|
335
|
+
if (flags & 1) {
|
|
336
|
+
color.name = reader.string();
|
|
337
|
+
}
|
|
338
|
+
colors[i] = color;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return colors;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Parse the legacy palette chunk (0x0004).
|
|
346
|
+
* Used as a fallback when no new-format palette chunk (0x2019) is present.
|
|
347
|
+
*/
|
|
348
|
+
function parseOldPaletteChunk(reader: BinaryReader): AsepriteColor[] {
|
|
349
|
+
const numPackets = reader.word();
|
|
350
|
+
const colors: AsepriteColor[] = [];
|
|
351
|
+
|
|
352
|
+
for (let p = 0; p < numPackets; p++) {
|
|
353
|
+
const skipCount = reader.byte(); // entries to skip from current position
|
|
354
|
+
let count = reader.byte(); // number of colors in this packet
|
|
355
|
+
if (count === 0) count = 256;
|
|
356
|
+
// Pad skipped slots with black
|
|
357
|
+
for (let s = 0; s < skipCount; s++) {
|
|
358
|
+
colors.push({ r: 0, g: 0, b: 0, a: 255 });
|
|
359
|
+
}
|
|
360
|
+
for (let c = 0; c < count; c++) {
|
|
361
|
+
const r = reader.byte();
|
|
362
|
+
const g = reader.byte();
|
|
363
|
+
const b = reader.byte();
|
|
364
|
+
colors.push({ r, g, b, a: 255 });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return colors;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --- Main parser ---
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Parse an Aseprite file from an ArrayBuffer.
|
|
375
|
+
* Returns the complete file structure with all layers, frames, cels, and palette.
|
|
376
|
+
*/
|
|
377
|
+
export async function parseAseprite(buffer: ArrayBuffer): Promise<AsepriteFile> {
|
|
378
|
+
const reader = new BinaryReader(buffer);
|
|
379
|
+
|
|
380
|
+
// --- File header (128 bytes) ---
|
|
381
|
+
reader.dword(); // bytes 0-3: file size (not needed for parsing)
|
|
382
|
+
const magic = reader.word(); // bytes 4-5
|
|
383
|
+
if (magic !== FILE_MAGIC) {
|
|
384
|
+
throw new Error(`Not a valid Aseprite file (magic: 0x${magic.toString(16)}, expected 0x${FILE_MAGIC.toString(16)})`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const frameCount = reader.word(); // bytes 6-7
|
|
388
|
+
const width = reader.word(); // bytes 8-9
|
|
389
|
+
const height = reader.word(); // bytes 10-11
|
|
390
|
+
const colorDepth = reader.word(); // bytes 12-13
|
|
391
|
+
reader.dword(); // bytes 14-17: flags (unused)
|
|
392
|
+
reader.skip(10); // bytes 18-27: deprecated speed, reserved
|
|
393
|
+
const transparentIndex = reader.byte(); // byte 28
|
|
394
|
+
reader.skip(99); // bytes 29-127: remaining header padding
|
|
395
|
+
|
|
396
|
+
const layers: AsepriteLayer[] = [];
|
|
397
|
+
let palette: AsepriteColor[] = [];
|
|
398
|
+
let hasNewPalette = false;
|
|
399
|
+
const frames: AsepriteFrame[] = [];
|
|
400
|
+
|
|
401
|
+
// --- Parse frames ---
|
|
402
|
+
for (let f = 0; f < frameCount; f++) {
|
|
403
|
+
const frameStart = reader.pos;
|
|
404
|
+
|
|
405
|
+
// Frame header (16 bytes)
|
|
406
|
+
const frameSizeBytes = reader.dword();
|
|
407
|
+
const frameMagic = reader.word();
|
|
408
|
+
if (frameMagic !== FRAME_MAGIC) {
|
|
409
|
+
throw new Error(`Invalid frame magic at frame ${String(f)} (0x${frameMagic.toString(16)})`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const oldChunkCount = reader.word();
|
|
413
|
+
const duration = reader.word();
|
|
414
|
+
reader.skip(2); // reserved
|
|
415
|
+
const newChunkCount = reader.dword();
|
|
416
|
+
|
|
417
|
+
// Use new chunk count if available, otherwise fall back to old
|
|
418
|
+
const chunkCount = newChunkCount !== 0 ? newChunkCount : oldChunkCount;
|
|
419
|
+
|
|
420
|
+
const cels: AsepriteCel[] = [];
|
|
421
|
+
|
|
422
|
+
// --- Parse chunks within this frame ---
|
|
423
|
+
for (let c = 0; c < chunkCount; c++) {
|
|
424
|
+
const chunkStart = reader.pos;
|
|
425
|
+
const chunkSize = reader.dword();
|
|
426
|
+
const chunkType = reader.word();
|
|
427
|
+
const chunkEnd = chunkStart + chunkSize;
|
|
428
|
+
|
|
429
|
+
switch (chunkType) {
|
|
430
|
+
case CHUNK_LAYER:
|
|
431
|
+
layers.push(parseLayerChunk(reader));
|
|
432
|
+
break;
|
|
433
|
+
|
|
434
|
+
case CHUNK_CEL:
|
|
435
|
+
cels.push(
|
|
436
|
+
await parseCelChunk(reader, chunkEnd, colorDepth, palette, transparentIndex),
|
|
437
|
+
);
|
|
438
|
+
break;
|
|
439
|
+
|
|
440
|
+
case CHUNK_PALETTE:
|
|
441
|
+
palette = parsePaletteChunk(reader);
|
|
442
|
+
hasNewPalette = true;
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case CHUNK_OLD_PALETTE_1:
|
|
446
|
+
case CHUNK_OLD_PALETTE_2:
|
|
447
|
+
// Only use legacy palette if no new-format palette has been seen
|
|
448
|
+
if (!hasNewPalette) {
|
|
449
|
+
palette = parseOldPaletteChunk(reader);
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
|
|
453
|
+
// Other chunk types (tags, slices, user data, etc.) are skipped
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Advance to end of chunk regardless of how much was read
|
|
457
|
+
reader.pos = chunkEnd;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
frames.push({ duration, cels });
|
|
461
|
+
|
|
462
|
+
// Advance to end of frame in case of padding
|
|
463
|
+
reader.pos = frameStart + frameSizeBytes;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// --- Resolve linked cels ---
|
|
467
|
+
// Linked cels reference the pixels of the same layer in another frame.
|
|
468
|
+
for (const frame of frames) {
|
|
469
|
+
for (let i = 0; i < frame.cels.length; i++) {
|
|
470
|
+
const cel = frame.cels[i];
|
|
471
|
+
if (!cel) continue;
|
|
472
|
+
if (cel.linkedFrame !== undefined && cel.pixels.length === 0) {
|
|
473
|
+
const sourceFrame = frames[cel.linkedFrame];
|
|
474
|
+
if (sourceFrame) {
|
|
475
|
+
const sourceCel = sourceFrame.cels.find(
|
|
476
|
+
(sc) => sc.layerIndex === cel.layerIndex && sc.linkedFrame === undefined,
|
|
477
|
+
);
|
|
478
|
+
if (sourceCel) {
|
|
479
|
+
// Rebuild without linkedFrame so exactOptionalPropertyTypes is happy
|
|
480
|
+
const rebuilt: AsepriteCel = {
|
|
481
|
+
width: sourceCel.width,
|
|
482
|
+
height: sourceCel.height,
|
|
483
|
+
pixels: sourceCel.pixels,
|
|
484
|
+
x: sourceCel.x,
|
|
485
|
+
y: sourceCel.y,
|
|
486
|
+
layerIndex: cel.layerIndex,
|
|
487
|
+
opacity: cel.opacity,
|
|
488
|
+
};
|
|
489
|
+
frame.cels[i] = rebuilt;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return { width, height, colorDepth, transparentIndex, frames, layers, palette };
|
|
497
|
+
}
|