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,667 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* AnimationPreviewPanel -- dockable panel that plays back the current
|
|
4
|
+
* animation. Shows a scaled canvas of the composited preview frame,
|
|
5
|
+
* transport controls (play/pause/stop/step), frame counter, FPS display,
|
|
6
|
+
* loop-mode toggle, and a zoom selector.
|
|
7
|
+
*
|
|
8
|
+
* Playback is driven by requestAnimationFrame that calls the
|
|
9
|
+
* animation-preview state machine's tick() with the wall-clock delta.
|
|
10
|
+
* The state machine advances previewFrameIndex based on per-frame
|
|
11
|
+
* duration and the current mode (loop / pingpong / side-by-side).
|
|
12
|
+
*
|
|
13
|
+
* Play/pause goes through the command dispatcher (play_animation) so
|
|
14
|
+
* it participates in the same path as the menu / keyboard shortcut.
|
|
15
|
+
* Stop and step are local state-machine operations with no undo tier,
|
|
16
|
+
* so they call the preview module directly (mirroring how the existing
|
|
17
|
+
* play_animation command body does).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { dispatch } from '../core/dispatcher.js';
|
|
21
|
+
import * as animPreview from './animation-preview.svelte.js';
|
|
22
|
+
import {
|
|
23
|
+
getFrames,
|
|
24
|
+
getGlobalFps,
|
|
25
|
+
getFrameDuration,
|
|
26
|
+
} from './frame-model.svelte.js';
|
|
27
|
+
import { getLayers } from '../layers/layer-tree.svelte.js';
|
|
28
|
+
import { composite } from '../layers/compositor.js';
|
|
29
|
+
import { canvasState } from '../canvas/canvas-state.svelte.js';
|
|
30
|
+
import PlayIcon from '~icons/lucide/play';
|
|
31
|
+
import PauseIcon from '~icons/lucide/pause';
|
|
32
|
+
import SquareIcon from '~icons/lucide/square';
|
|
33
|
+
import StepBackIcon from '~icons/lucide/step-back';
|
|
34
|
+
import StepForwardIcon from '~icons/lucide/step-forward';
|
|
35
|
+
import RepeatIcon from '~icons/lucide/repeat';
|
|
36
|
+
import Repeat1Icon from '~icons/lucide/repeat-1';
|
|
37
|
+
import RotateCwIcon from '~icons/lucide/rotate-cw';
|
|
38
|
+
|
|
39
|
+
// --- Speed presets ---
|
|
40
|
+
const SPEED_PRESETS = [0.5, 1, 2] as const;
|
|
41
|
+
let speed = $derived(animPreview.getSpeedMultiplier());
|
|
42
|
+
|
|
43
|
+
// --- Zoom ---
|
|
44
|
+
// Local to the panel; not persisted. User picks a scale factor for
|
|
45
|
+
// the preview canvas that is independent from the main viewport zoom.
|
|
46
|
+
const ZOOM_OPTIONS = [1, 2, 4, 8] as const;
|
|
47
|
+
let zoom = $state<number>(2);
|
|
48
|
+
|
|
49
|
+
// --- Canvas / stage refs ---
|
|
50
|
+
let previewCanvas = $state<HTMLCanvasElement | null>(null);
|
|
51
|
+
let stageEl = $state<HTMLDivElement | null>(null);
|
|
52
|
+
// Track available stage dimensions via ResizeObserver for side-by-side scaling
|
|
53
|
+
let stageWidth = $state<number>(0);
|
|
54
|
+
let stageHeight = $state<number>(0);
|
|
55
|
+
|
|
56
|
+
// --- Reactive state reads ---
|
|
57
|
+
let frames = $derived(getFrames());
|
|
58
|
+
let playing = $derived(animPreview.isPlaying());
|
|
59
|
+
let mode = $derived(animPreview.getMode());
|
|
60
|
+
let previewIndex = $derived(animPreview.getPreviewFrameIndex());
|
|
61
|
+
let globalFps = $derived(getGlobalFps());
|
|
62
|
+
let totalFrames = $derived(frames.length);
|
|
63
|
+
|
|
64
|
+
// Effective FPS for the CURRENT preview frame. Per-frame duration
|
|
65
|
+
// overrides take precedence over the global fps; we display the
|
|
66
|
+
// instantaneous rate so the number matches what the viewer sees.
|
|
67
|
+
let effectiveFps = $derived.by(() => {
|
|
68
|
+
if (totalFrames === 0) return globalFps;
|
|
69
|
+
const durMs = getFrameDuration(previewIndex);
|
|
70
|
+
if (durMs <= 0) return globalFps;
|
|
71
|
+
return Math.round(1000 / durMs);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Proportional canvas dimensions at the chosen zoom.
|
|
75
|
+
let canvasW = $derived(canvasState.canvasWidth * zoom);
|
|
76
|
+
let canvasH = $derived(canvasState.canvasHeight * zoom);
|
|
77
|
+
|
|
78
|
+
// Observe stage container size for side-by-side layout calculations
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (!stageEl) return;
|
|
81
|
+
const observer = new ResizeObserver((entries) => {
|
|
82
|
+
const entry = entries[0];
|
|
83
|
+
if (!entry) return;
|
|
84
|
+
const { width, height } = entry.contentRect;
|
|
85
|
+
stageWidth = width;
|
|
86
|
+
stageHeight = height;
|
|
87
|
+
});
|
|
88
|
+
observer.observe(stageEl);
|
|
89
|
+
return () => { observer.disconnect(); };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// --- Frame rendering ---
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Composite all visible layers of the preview frame and draw them onto
|
|
96
|
+
* the panel canvas at the current zoom. Uses an offscreen buffer at
|
|
97
|
+
* native pixel size first, then scales with imageSmoothing disabled so
|
|
98
|
+
* pixel art stays crisp.
|
|
99
|
+
*/
|
|
100
|
+
function renderPreviewFrame() {
|
|
101
|
+
const el = previewCanvas;
|
|
102
|
+
if (!el) return;
|
|
103
|
+
if (totalFrames === 0) return;
|
|
104
|
+
|
|
105
|
+
const frame = frames[previewIndex];
|
|
106
|
+
if (!frame) return;
|
|
107
|
+
|
|
108
|
+
const w = canvasState.canvasWidth;
|
|
109
|
+
const h = canvasState.canvasHeight;
|
|
110
|
+
const composited = composite(getLayers(), frame.pixelData, w, h);
|
|
111
|
+
|
|
112
|
+
const imgData = new ImageData(
|
|
113
|
+
new Uint8ClampedArray(composited.data),
|
|
114
|
+
w,
|
|
115
|
+
h,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Native-size offscreen, then scaled blit to the visible canvas.
|
|
119
|
+
const offscreen = new OffscreenCanvas(w, h);
|
|
120
|
+
const offCtx = offscreen.getContext('2d');
|
|
121
|
+
if (!offCtx) return;
|
|
122
|
+
offCtx.putImageData(imgData, 0, 0);
|
|
123
|
+
|
|
124
|
+
el.width = canvasW;
|
|
125
|
+
el.height = canvasH;
|
|
126
|
+
const ctx = el.getContext('2d');
|
|
127
|
+
if (!ctx) return;
|
|
128
|
+
ctx.imageSmoothingEnabled = false;
|
|
129
|
+
ctx.clearRect(0, 0, canvasW, canvasH);
|
|
130
|
+
ctx.drawImage(offscreen, 0, 0, canvasW, canvasH);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Side-by-side constants ---
|
|
134
|
+
const SBS_GAP = 4; // px gap between frames
|
|
135
|
+
const SBS_HIGHLIGHT_WIDTH = 2; // px border around the active frame
|
|
136
|
+
const SBS_LABEL_HEIGHT = 14; // px reserved for frame number labels
|
|
137
|
+
const SBS_PADDING = 6; // px padding inside the canvas edges
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Render ALL frames side by side on the preview canvas, scaling them
|
|
141
|
+
* uniformly so they fit within the available stage area. The frame
|
|
142
|
+
* at previewIndex gets an accent-colored border highlight. Frame
|
|
143
|
+
* numbers are drawn below each thumbnail when there is enough room.
|
|
144
|
+
*/
|
|
145
|
+
function renderSideBySide() {
|
|
146
|
+
const el = previewCanvas;
|
|
147
|
+
if (!el) return;
|
|
148
|
+
if (totalFrames === 0) return;
|
|
149
|
+
// Need stage dimensions to calculate fit; skip if not measured yet
|
|
150
|
+
if (stageWidth === 0 || stageHeight === 0) return;
|
|
151
|
+
|
|
152
|
+
const nativeW = canvasState.canvasWidth;
|
|
153
|
+
const nativeH = canvasState.canvasHeight;
|
|
154
|
+
const layers = getLayers();
|
|
155
|
+
|
|
156
|
+
// Pre-composite every frame into native-size offscreen canvases
|
|
157
|
+
const offscreens: OffscreenCanvas[] = [];
|
|
158
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
159
|
+
const frame = frames[i];
|
|
160
|
+
if (!frame) continue;
|
|
161
|
+
const composited = composite(layers, frame.pixelData, nativeW, nativeH);
|
|
162
|
+
const imgData = new ImageData(
|
|
163
|
+
new Uint8ClampedArray(composited.data),
|
|
164
|
+
nativeW,
|
|
165
|
+
nativeH,
|
|
166
|
+
);
|
|
167
|
+
const osc = new OffscreenCanvas(nativeW, nativeH);
|
|
168
|
+
const octx = osc.getContext('2d');
|
|
169
|
+
if (!octx) continue;
|
|
170
|
+
octx.putImageData(imgData, 0, 0);
|
|
171
|
+
offscreens.push(osc);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Calculate scaling so all frames + gaps + padding fit in the stage.
|
|
175
|
+
// Layout: [padding] [frame] [gap] [frame] [gap] ... [frame] [padding]
|
|
176
|
+
// Vertically: [padding] [frame] [label_area] [padding]
|
|
177
|
+
const totalGaps = (totalFrames - 1) * SBS_GAP;
|
|
178
|
+
const availW = stageWidth - 2 * SBS_PADDING;
|
|
179
|
+
const availH = stageHeight - 2 * SBS_PADDING - SBS_LABEL_HEIGHT;
|
|
180
|
+
|
|
181
|
+
// Scale factor limited by both horizontal and vertical fit
|
|
182
|
+
const scaleByWidth = (availW - totalGaps) / (nativeW * totalFrames);
|
|
183
|
+
const scaleByHeight = availH / nativeH;
|
|
184
|
+
// Use at least 1px per native pixel, cap at the zoom setting
|
|
185
|
+
const scale = Math.max(1, Math.min(zoom, scaleByWidth, scaleByHeight));
|
|
186
|
+
|
|
187
|
+
const thumbW = Math.floor(nativeW * scale);
|
|
188
|
+
const thumbH = Math.floor(nativeH * scale);
|
|
189
|
+
|
|
190
|
+
// Determine if labels fit (need reasonable thumb width)
|
|
191
|
+
const showLabels = thumbW >= 12;
|
|
192
|
+
|
|
193
|
+
// Total canvas size required
|
|
194
|
+
const totalW = 2 * SBS_PADDING + totalFrames * thumbW + totalGaps;
|
|
195
|
+
const totalH = 2 * SBS_PADDING + thumbH + (showLabels ? SBS_LABEL_HEIGHT : 0);
|
|
196
|
+
|
|
197
|
+
el.width = totalW;
|
|
198
|
+
el.height = totalH;
|
|
199
|
+
|
|
200
|
+
const ctx = el.getContext('2d');
|
|
201
|
+
if (!ctx) return;
|
|
202
|
+
ctx.imageSmoothingEnabled = false;
|
|
203
|
+
ctx.clearRect(0, 0, totalW, totalH);
|
|
204
|
+
|
|
205
|
+
// Read CSS accent color from the computed style for the highlight
|
|
206
|
+
const styles = getComputedStyle(el);
|
|
207
|
+
const accentColor = styles.getPropertyValue('--accent').trim() || '#4a9eff';
|
|
208
|
+
const borderColor = styles.getPropertyValue('--border').trim() || '#555';
|
|
209
|
+
const textColor = styles.getPropertyValue('--text-secondary').trim() || '#999';
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
212
|
+
const x = SBS_PADDING + i * (thumbW + SBS_GAP);
|
|
213
|
+
const y = SBS_PADDING;
|
|
214
|
+
|
|
215
|
+
const isActive = i === previewIndex;
|
|
216
|
+
|
|
217
|
+
if (isActive) {
|
|
218
|
+
// Draw highlight border behind the active frame
|
|
219
|
+
ctx.fillStyle = accentColor;
|
|
220
|
+
ctx.fillRect(
|
|
221
|
+
x - SBS_HIGHLIGHT_WIDTH,
|
|
222
|
+
y - SBS_HIGHLIGHT_WIDTH,
|
|
223
|
+
thumbW + 2 * SBS_HIGHLIGHT_WIDTH,
|
|
224
|
+
thumbH + 2 * SBS_HIGHLIGHT_WIDTH,
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
// Subtle border for non-active frames
|
|
228
|
+
ctx.fillStyle = borderColor;
|
|
229
|
+
ctx.fillRect(x - 1, y - 1, thumbW + 2, thumbH + 2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Draw the frame thumbnail (crisp pixel scaling)
|
|
233
|
+
const osc = offscreens[i];
|
|
234
|
+
if (osc) ctx.drawImage(osc, x, y, thumbW, thumbH);
|
|
235
|
+
|
|
236
|
+
// Dim non-active frames slightly for contrast
|
|
237
|
+
if (!isActive) {
|
|
238
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
|
|
239
|
+
ctx.fillRect(x, y, thumbW, thumbH);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Frame number label
|
|
243
|
+
if (showLabels) {
|
|
244
|
+
ctx.fillStyle = isActive ? accentColor : textColor;
|
|
245
|
+
ctx.font = `${String(Math.min(11, SBS_LABEL_HEIGHT - 2))}px sans-serif`;
|
|
246
|
+
ctx.textAlign = 'center';
|
|
247
|
+
ctx.textBaseline = 'top';
|
|
248
|
+
ctx.fillText(
|
|
249
|
+
String(i + 1),
|
|
250
|
+
x + thumbW / 2,
|
|
251
|
+
y + thumbH + 2,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Re-render whenever the preview frame, layers, canvas size, zoom,
|
|
258
|
+
// mode, or stage dimensions change.
|
|
259
|
+
$effect(() => {
|
|
260
|
+
// Touch reactive dependencies so the effect re-runs:
|
|
261
|
+
void previewIndex;
|
|
262
|
+
void frames;
|
|
263
|
+
void getLayers();
|
|
264
|
+
void canvasState.canvasWidth;
|
|
265
|
+
void canvasState.canvasHeight;
|
|
266
|
+
void canvasW;
|
|
267
|
+
void canvasH;
|
|
268
|
+
void mode;
|
|
269
|
+
void stageWidth;
|
|
270
|
+
void stageHeight;
|
|
271
|
+
|
|
272
|
+
if (mode === 'side-by-side') {
|
|
273
|
+
renderSideBySide();
|
|
274
|
+
} else {
|
|
275
|
+
renderPreviewFrame();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// --- Playback loop ---
|
|
280
|
+
// requestAnimationFrame drives tick() with the wall-clock delta. The
|
|
281
|
+
// state machine uses per-frame durations to decide when to advance.
|
|
282
|
+
// We start/stop the loop based on `playing` so we don't burn CPU when
|
|
283
|
+
// paused.
|
|
284
|
+
|
|
285
|
+
let rafId: number | null = null;
|
|
286
|
+
let lastTs: number | null = null;
|
|
287
|
+
|
|
288
|
+
function loop(ts: number) {
|
|
289
|
+
if (lastTs !== null) {
|
|
290
|
+
animPreview.tick(ts - lastTs);
|
|
291
|
+
}
|
|
292
|
+
lastTs = ts;
|
|
293
|
+
rafId = requestAnimationFrame(loop);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
$effect(() => {
|
|
297
|
+
if (playing) {
|
|
298
|
+
lastTs = null;
|
|
299
|
+
rafId = requestAnimationFrame(loop);
|
|
300
|
+
return () => {
|
|
301
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
302
|
+
rafId = null;
|
|
303
|
+
lastTs = null;
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Not playing -- no loop running, nothing to clean up.
|
|
307
|
+
return;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// --- Transport actions ---
|
|
311
|
+
|
|
312
|
+
function handlePlayPause() {
|
|
313
|
+
dispatch({
|
|
314
|
+
type: 'play_animation',
|
|
315
|
+
plugin: 'ui/animation-preview',
|
|
316
|
+
version: '1.0.0',
|
|
317
|
+
params: {},
|
|
318
|
+
id: crypto.randomUUID(),
|
|
319
|
+
timestamp: Date.now(),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function handleStop() {
|
|
324
|
+
animPreview.stop();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function handleStepBack() {
|
|
328
|
+
animPreview.step(-1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function handleStepForward() {
|
|
332
|
+
animPreview.step(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Loop button cycles loop -> pingpong -> side-by-side -> loop.
|
|
336
|
+
// The icon/label reflects the CURRENT mode so the user knows what
|
|
337
|
+
// clicking will switch from.
|
|
338
|
+
function handleLoopToggle() {
|
|
339
|
+
if (mode === 'loop') {
|
|
340
|
+
animPreview.setMode('pingpong');
|
|
341
|
+
} else if (mode === 'pingpong') {
|
|
342
|
+
animPreview.setMode('side-by-side');
|
|
343
|
+
} else {
|
|
344
|
+
animPreview.setMode('loop');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function modeLabel(m: animPreview.PreviewMode): string {
|
|
349
|
+
if (m === 'loop') return 'Loop';
|
|
350
|
+
if (m === 'pingpong') return 'Ping-pong';
|
|
351
|
+
return 'Side-by-side';
|
|
352
|
+
}
|
|
353
|
+
</script>
|
|
354
|
+
|
|
355
|
+
<div class="preview-panel">
|
|
356
|
+
<!-- Preview canvas area -->
|
|
357
|
+
<div class="preview-stage" bind:this={stageEl}>
|
|
358
|
+
{#if totalFrames === 0}
|
|
359
|
+
<div class="empty-state">No frames</div>
|
|
360
|
+
{:else}
|
|
361
|
+
<canvas
|
|
362
|
+
bind:this={previewCanvas}
|
|
363
|
+
class="preview-canvas"
|
|
364
|
+
width={canvasW}
|
|
365
|
+
height={canvasH}
|
|
366
|
+
aria-label="Animation preview"
|
|
367
|
+
></canvas>
|
|
368
|
+
{/if}
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<!-- Transport controls -->
|
|
372
|
+
<div class="controls">
|
|
373
|
+
<div class="transport">
|
|
374
|
+
<button
|
|
375
|
+
class="tbtn"
|
|
376
|
+
title="Step back"
|
|
377
|
+
aria-label="Step back one frame"
|
|
378
|
+
onclick={handleStepBack}
|
|
379
|
+
disabled={totalFrames === 0}
|
|
380
|
+
><StepBackIcon /></button>
|
|
381
|
+
|
|
382
|
+
<button
|
|
383
|
+
class="tbtn tbtn--primary"
|
|
384
|
+
title={playing ? 'Pause' : 'Play'}
|
|
385
|
+
aria-label={playing ? 'Pause animation' : 'Play animation'}
|
|
386
|
+
onclick={handlePlayPause}
|
|
387
|
+
disabled={totalFrames === 0}
|
|
388
|
+
>
|
|
389
|
+
{#if playing}
|
|
390
|
+
<PauseIcon />
|
|
391
|
+
{:else}
|
|
392
|
+
<PlayIcon />
|
|
393
|
+
{/if}
|
|
394
|
+
</button>
|
|
395
|
+
|
|
396
|
+
<button
|
|
397
|
+
class="tbtn"
|
|
398
|
+
title="Stop"
|
|
399
|
+
aria-label="Stop animation"
|
|
400
|
+
onclick={handleStop}
|
|
401
|
+
disabled={totalFrames === 0}
|
|
402
|
+
><SquareIcon /></button>
|
|
403
|
+
|
|
404
|
+
<button
|
|
405
|
+
class="tbtn"
|
|
406
|
+
title="Step forward"
|
|
407
|
+
aria-label="Step forward one frame"
|
|
408
|
+
onclick={handleStepForward}
|
|
409
|
+
disabled={totalFrames === 0}
|
|
410
|
+
><StepForwardIcon /></button>
|
|
411
|
+
|
|
412
|
+
<button
|
|
413
|
+
class="tbtn"
|
|
414
|
+
title={`Mode: ${modeLabel(mode)} (click to change)`}
|
|
415
|
+
aria-label={`Playback mode: ${modeLabel(mode)}. Click to change.`}
|
|
416
|
+
onclick={handleLoopToggle}
|
|
417
|
+
>
|
|
418
|
+
{#if mode === 'loop'}
|
|
419
|
+
<RepeatIcon />
|
|
420
|
+
{:else if mode === 'pingpong'}
|
|
421
|
+
<Repeat1Icon />
|
|
422
|
+
{:else}
|
|
423
|
+
<RotateCwIcon />
|
|
424
|
+
{/if}
|
|
425
|
+
</button>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div class="speed-presets" aria-label="Playback speed">
|
|
429
|
+
{#each SPEED_PRESETS as preset (preset)}
|
|
430
|
+
<button
|
|
431
|
+
class="speed-btn"
|
|
432
|
+
class:speed-btn--active={speed === preset}
|
|
433
|
+
title="{preset}x speed"
|
|
434
|
+
aria-label="Set playback speed to {preset}x"
|
|
435
|
+
aria-pressed={speed === preset}
|
|
436
|
+
onclick={() => { animPreview.setSpeed(preset); }}
|
|
437
|
+
>{preset}x</button>
|
|
438
|
+
{/each}
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div class="info">
|
|
442
|
+
<span class="info-item" aria-label="Frame counter">
|
|
443
|
+
<span class="info-label">Frame</span>
|
|
444
|
+
<span class="info-value">
|
|
445
|
+
{totalFrames === 0 ? 0 : previewIndex + 1} / {totalFrames}
|
|
446
|
+
</span>
|
|
447
|
+
</span>
|
|
448
|
+
<span class="info-item" aria-label="Frames per second">
|
|
449
|
+
<span class="info-label">FPS</span>
|
|
450
|
+
<span class="info-value">{effectiveFps}</span>
|
|
451
|
+
</span>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
<div class="zoom-control">
|
|
455
|
+
<label class="zoom-label" for="anim-preview-zoom">Zoom</label>
|
|
456
|
+
<select
|
|
457
|
+
id="anim-preview-zoom"
|
|
458
|
+
class="zoom-select"
|
|
459
|
+
bind:value={zoom}
|
|
460
|
+
aria-label="Preview zoom level"
|
|
461
|
+
>
|
|
462
|
+
{#each ZOOM_OPTIONS as opt (opt)}
|
|
463
|
+
<option value={opt}>{opt}x</option>
|
|
464
|
+
{/each}
|
|
465
|
+
</select>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<style>
|
|
471
|
+
.preview-panel {
|
|
472
|
+
display: flex;
|
|
473
|
+
flex-direction: column;
|
|
474
|
+
width: 100%;
|
|
475
|
+
height: 100%;
|
|
476
|
+
background: var(--bg-toolbar);
|
|
477
|
+
color: var(--text-primary);
|
|
478
|
+
font-size: var(--text-sm);
|
|
479
|
+
user-select: none;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* --- Preview stage: checkerboard-free, mirrors canvas background --- */
|
|
483
|
+
|
|
484
|
+
.preview-stage {
|
|
485
|
+
flex: 1;
|
|
486
|
+
min-height: 0;
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
justify-content: center;
|
|
490
|
+
overflow: auto;
|
|
491
|
+
padding: var(--space-3);
|
|
492
|
+
background: var(--bg-primary);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.preview-canvas {
|
|
496
|
+
image-rendering: pixelated;
|
|
497
|
+
background: var(--bg-canvas);
|
|
498
|
+
border: 1px solid var(--border);
|
|
499
|
+
border-radius: var(--radius-sm);
|
|
500
|
+
flex-shrink: 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.empty-state {
|
|
504
|
+
color: var(--text-secondary);
|
|
505
|
+
font-size: var(--text-sm);
|
|
506
|
+
font-style: italic;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* --- Controls strip --- */
|
|
510
|
+
|
|
511
|
+
.controls {
|
|
512
|
+
display: flex;
|
|
513
|
+
flex-wrap: wrap;
|
|
514
|
+
align-items: center;
|
|
515
|
+
gap: var(--space-3);
|
|
516
|
+
padding: var(--space-2) var(--space-3);
|
|
517
|
+
border-top: 1px solid var(--border);
|
|
518
|
+
flex-shrink: 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.transport {
|
|
522
|
+
display: flex;
|
|
523
|
+
gap: var(--space-1);
|
|
524
|
+
align-items: center;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.tbtn {
|
|
528
|
+
background: none;
|
|
529
|
+
border: 1px solid transparent;
|
|
530
|
+
border-radius: var(--radius-sm);
|
|
531
|
+
color: var(--text-secondary);
|
|
532
|
+
cursor: pointer;
|
|
533
|
+
width: 26px;
|
|
534
|
+
height: 26px;
|
|
535
|
+
display: flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
justify-content: center;
|
|
538
|
+
padding: 0;
|
|
539
|
+
line-height: 1;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.tbtn:hover:not(:disabled) {
|
|
543
|
+
background: var(--bg-primary);
|
|
544
|
+
color: var(--text-primary);
|
|
545
|
+
border-color: var(--border);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.tbtn:disabled {
|
|
549
|
+
opacity: 0.3;
|
|
550
|
+
cursor: default;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.tbtn--primary {
|
|
554
|
+
color: var(--accent);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.tbtn--primary:hover:not(:disabled) {
|
|
558
|
+
color: var(--accent);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.tbtn :global(svg) {
|
|
562
|
+
width: 15px;
|
|
563
|
+
height: 15px;
|
|
564
|
+
display: block;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* --- Speed presets --- */
|
|
568
|
+
|
|
569
|
+
.speed-presets {
|
|
570
|
+
display: flex;
|
|
571
|
+
gap: 2px;
|
|
572
|
+
align-items: center;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.speed-btn {
|
|
576
|
+
background: none;
|
|
577
|
+
border: 1px solid transparent;
|
|
578
|
+
border-radius: var(--radius-sm);
|
|
579
|
+
color: var(--text-secondary);
|
|
580
|
+
cursor: pointer;
|
|
581
|
+
height: 22px;
|
|
582
|
+
padding: 0 var(--space-1);
|
|
583
|
+
font-size: var(--text-xs);
|
|
584
|
+
font-weight: 600;
|
|
585
|
+
line-height: 1;
|
|
586
|
+
white-space: nowrap;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.speed-btn:hover {
|
|
590
|
+
background: var(--bg-primary);
|
|
591
|
+
color: var(--text-primary);
|
|
592
|
+
border-color: var(--border);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.speed-btn--active {
|
|
596
|
+
background: var(--accent);
|
|
597
|
+
color: var(--bg-primary);
|
|
598
|
+
border-color: var(--accent);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.speed-btn--active:hover {
|
|
602
|
+
background: var(--accent);
|
|
603
|
+
color: var(--bg-primary);
|
|
604
|
+
border-color: var(--accent);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* --- Info block: frame counter + fps --- */
|
|
608
|
+
|
|
609
|
+
.info {
|
|
610
|
+
display: flex;
|
|
611
|
+
align-items: center;
|
|
612
|
+
gap: var(--space-3);
|
|
613
|
+
margin-left: auto;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.info-item {
|
|
617
|
+
display: inline-flex;
|
|
618
|
+
align-items: baseline;
|
|
619
|
+
gap: var(--space-1);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.info-label {
|
|
623
|
+
font-size: var(--text-xs);
|
|
624
|
+
font-weight: 600;
|
|
625
|
+
color: var(--text-secondary);
|
|
626
|
+
text-transform: uppercase;
|
|
627
|
+
letter-spacing: 0.3px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.info-value {
|
|
631
|
+
font-size: var(--text-sm);
|
|
632
|
+
color: var(--text-primary);
|
|
633
|
+
font-variant-numeric: tabular-nums;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* --- Zoom selector --- */
|
|
637
|
+
|
|
638
|
+
.zoom-control {
|
|
639
|
+
display: flex;
|
|
640
|
+
align-items: center;
|
|
641
|
+
gap: var(--space-2);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.zoom-label {
|
|
645
|
+
font-size: var(--text-xs);
|
|
646
|
+
font-weight: 600;
|
|
647
|
+
color: var(--text-secondary);
|
|
648
|
+
text-transform: uppercase;
|
|
649
|
+
letter-spacing: 0.3px;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.zoom-select {
|
|
653
|
+
height: 24px;
|
|
654
|
+
background: var(--bg-primary);
|
|
655
|
+
border: 1px solid var(--border);
|
|
656
|
+
border-radius: var(--radius-sm);
|
|
657
|
+
color: var(--text-primary);
|
|
658
|
+
font-size: var(--text-sm);
|
|
659
|
+
padding: 0 var(--space-1);
|
|
660
|
+
cursor: pointer;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.zoom-select:focus-visible {
|
|
664
|
+
border-color: var(--accent);
|
|
665
|
+
outline: none;
|
|
666
|
+
}
|
|
667
|
+
</style>
|