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,
|