From fa8c52b239c0533318977d2eef73d3c0c086f285 Mon Sep 17 00:00:00 2001 From: th Date: Sun, 7 Jun 2026 11:08:41 +0200 Subject: [PATCH] Split renderer bundles by feature --- src/App.vue | 4 +-- src/components/ExportDialog.vue | 42 +++++++++++++++++-------- vite.config.ts | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/App.vue b/src/App.vue index f7f5597..4b8387a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -28,7 +28,6 @@ import ClipartDrawer from './components/ClipartDrawer.vue' import SceneDrawer from './components/SceneDrawer.vue' import DocumentDrawer from './components/DocumentDrawer.vue' import DocumentSettings from './components/DocumentSettings.vue' -import ExportDialog from './components/ExportDialog.vue' import FullscreenPreview from './components/FullscreenPreview.vue' import PropertiesPanel from './components/PropertiesPanel.vue' import SvgLayerPanel from './components/SvgLayerPanel.vue' @@ -46,7 +45,6 @@ import { usePlaybackLoop } from './composables/usePlaybackLoop' import { useTimeFormat } from './composables/useTimeFormat' import { useProjectName } from './composables/useProjectName' import { shouldChromeClickDeselect } from './lib/chromeClick' -import { renderSceneToStillImage } from './lib/stillImageExport' import { ProjectFileError, pickProjectFile } from './lib/projectFile' import { readAnyProjectFile } from './lib/ruwFile' import { @@ -73,6 +71,7 @@ const helpOpen = ref(false) const TestOverlay = import.meta.env.DEV ? defineAsyncComponent(() => import('./devtools/TestOverlay.vue')) : null +const ExportDialog = defineAsyncComponent(() => import('./components/ExportDialog.vue')) const TIMELINE_HEIGHT_KEY = 'animatr:timelineHeight' const DEFAULT_TIMELINE_HEIGHT = 220 const MIN_TIMELINE_HEIGHT = 120 @@ -313,6 +312,7 @@ async function copyStillToClipboard() { return } try { + const { renderSceneToStillImage } = await import('./lib/stillImageExport') const scene = JSON.parse(JSON.stringify(store.scene)) const blob = await renderSceneToStillImage(scene, store.playhead, 'png') // ClipboardItem is widely available in modern Chromium / Firefox diff --git a/src/components/ExportDialog.vue b/src/components/ExportDialog.vue index df53534..7b9c65b 100644 --- a/src/components/ExportDialog.vue +++ b/src/components/ExportDialog.vue @@ -12,14 +12,6 @@ import { PhGif, } from '@phosphor-icons/vue' import { useSceneStore } from '@/stores/scene' -import { downloadExampleHtml, downloadRuntime, downloadSceneExport } from '@/lib/exportPlayer' -import { downloadSceneAsVideo, pickMimeType } from '@/lib/videoExport' -import { downloadSceneAsGif } from '@/lib/gifExport' -import { downloadSceneAsApng, downloadSceneAsApngPreview } from '@/lib/apngExport' -import { hasDesktopRender, renderMp4OnDesktop } from '@/lib/desktopRenderExport' -import { downloadFromServer } from '@/lib/serverExport' -import { downloadRuwFile } from '@/lib/ruwFile' -import { downloadSceneAsStillImage } from '@/lib/stillImageExport' import { useProjectName } from '@/composables/useProjectName' import type { ButtonAsset, Layer } from '@/types/scene' import { Modal } from '@/components/ui' @@ -39,7 +31,20 @@ interface ProgressState { } const progress = ref(null) -const videoSupported = pickMimeType() !== null +const VIDEO_MIME_CANDIDATES = [ + 'video/webm;codecs=vp9', + 'video/webm;codecs=vp8', + 'video/webm', + 'video/mp4;codecs=h264', + 'video/mp4', +] + +function canRecordVideo(): boolean { + if (typeof MediaRecorder === 'undefined') return false + return VIDEO_MIME_CANDIDATES.some((mime) => MediaRecorder.isTypeSupported?.(mime)) +} + +const videoSupported = canRecordVideo() // Opt-in: strip layers that are hidden in the editor (`editorHidden`) // from the exported scene. Default off — editor-hide is a workflow @@ -198,17 +203,20 @@ function activeSceneClone() { return scene } -function exportScene() { +async function exportScene() { + const { downloadSceneExport } = await import('@/lib/exportPlayer') downloadSceneExport(projectClone()) emit('update:modelValue', false) } -function exportRuntime() { +async function exportRuntime() { + const { downloadRuntime } = await import('@/lib/exportPlayer') downloadRuntime() emit('update:modelValue', false) } -function exportRuwBundle() { +async function exportRuwBundle() { + const { downloadRuwFile } = await import('@/lib/ruwFile') // Re-import target: .ruw is the round-trippable format the welcome // dialog + file-picker accept. Pull the project via projectForSave // so the editor-session UI state (zoom, toggles, …) rides along. @@ -219,7 +227,8 @@ function exportRuwBundle() { emit('update:modelValue', false) } -function exportHtml() { +async function exportHtml() { + const { downloadExampleHtml } = await import('@/lib/exportPlayer') downloadExampleHtml(projectClone()) emit('update:modelValue', false) } @@ -234,6 +243,7 @@ async function exportStillImage(format: 'png' | 'jpg') { const opts = targetSizeChanged.value ? { targetWidth: targetWidth.value, targetHeight: targetHeight.value } : undefined + const { downloadSceneAsStillImage } = await import('@/lib/stillImageExport') await downloadSceneAsStillImage( activeSceneClone(), store.playhead, @@ -255,6 +265,7 @@ async function exportWebm() { } store.pause() try { + const { downloadSceneAsVideo } = await import('@/lib/videoExport') await downloadSceneAsVideo(activeSceneClone(), { signal: controller.signal, onProgress: (p) => { @@ -283,6 +294,7 @@ async function exportGif() { } store.pause() try { + const { downloadSceneAsGif } = await import('@/lib/gifExport') await downloadSceneAsGif( activeSceneClone(), frameExportOpts(controller, (p) => { @@ -311,6 +323,7 @@ async function exportApng() { } store.pause() try { + const { downloadSceneAsApng } = await import('@/lib/apngExport') await downloadSceneAsApng( activeSceneClone(), frameExportOpts(controller, (p) => { @@ -339,6 +352,7 @@ async function exportApngPreview() { } store.pause() try { + const { downloadSceneAsApngPreview } = await import('@/lib/apngExport') await downloadSceneAsApngPreview( activeSceneClone(), frameExportOpts(controller, (p) => { @@ -370,6 +384,7 @@ async function exportMp4Server() { store.pause() try { const scene = activeSceneClone() + const { hasDesktopRender, renderMp4OnDesktop } = await import('@/lib/desktopRenderExport') if (hasDesktopRender()) { const result = await renderMp4OnDesktop(scene, { signal: controller.signal, @@ -384,6 +399,7 @@ async function exportMp4Server() { return } } else { + const { downloadFromServer } = await import('@/lib/serverExport') await downloadFromServer(scene, { signal: controller.signal, fps: targetFps.value, diff --git a/vite.config.ts b/vite.config.ts index 356922b..4a4f4e8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,60 @@ import { fileURLToPath, URL } from 'node:url' export default defineConfig({ base: './', plugins: [vue()], + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('/src/i18n/')) return 'i18n' + if ( + id.includes('/src/components/CanvasStage.vue') || + id.includes('/src/components/AssetSidebar.vue') || + id.includes('/src/components/PropertiesPanel.vue') || + id.includes('/src/components/ActionEditor.vue') || + id.includes('/src/components/FontPicker.vue') || + id.includes('/src/components/RangeSlider.vue') || + id.includes('/src/components/SvgLayerPanel.vue') || + id.includes('/src/stores/scene.ts') || + id.includes('/src/types/scene.ts') || + id.includes('/src/lib/render.ts') || + id.includes('/src/lib/interpolate.ts') || + id.includes('/src/lib/wiggle.ts') || + id.includes('/src/lib/kaleido.ts') || + id.includes('/src/lib/clipBounds.ts') || + id.includes('/src/lib/actions.ts') || + id.includes('/src/lib/sceneOrder.ts') || + id.includes('/src/composables/useButtonRuntime.ts') + ) { + return 'editor' + } + if ( + id.includes('/src/components/Timeline.vue') || + id.includes('/src/lib/timelineSnap.ts') || + id.includes('/src/composables/useAudioWaveform.ts') + ) { + return 'timeline' + } + if (!id.includes('/node_modules/')) return undefined + if (id.includes('/@phosphor-icons/vue/')) return 'icons' + if (id.includes('/konva/') || id.includes('/vue-konva/')) return 'canvas' + if ( + id.includes('/gifenc/') || + id.includes('/upng-js/') + ) { + return 'encoders' + } + if ( + id.includes('/vue/') || + id.includes('/@vue/') || + id.includes('/pinia/') + ) { + return 'framework' + } + return 'vendor' + }, + }, + }, + }, server: { port: 5200,