Clarify export runtime copy

This commit is contained in:
th committed 2026-06-07 12:10:35 +02:00
1 parent 8d296c963e
commit f469ab7444
4 files changed
+85 -32

No files matched your search

+5 -1
View File
@@ -68,6 +68,7 @@ const welcomeOpen = ref(false)
const fullscreenOpen = ref(false) const fullscreenOpen = ref(false)
const aboutOpen = ref(false) const aboutOpen = ref(false)
const helpOpen = ref(false) const helpOpen = ref(false)
const isDesktop = computed(() => !!window.ruwioDesktop)
const TestOverlay = import.meta.env.DEV const TestOverlay = import.meta.env.DEV
? defineAsyncComponent(() => import('./devtools/TestOverlay.vue')) ? defineAsyncComponent(() => import('./devtools/TestOverlay.vue'))
: null : null
@@ -745,7 +746,10 @@ onBeforeUnmount(() => {
</button> </button>
<button <button
class="export-btn" class="export-btn"
v-tooltip="{ label: t('app.export'), help: t('app.exportHelp') }" v-tooltip="{
label: t('app.export'),
help: t(isDesktop ? 'app.exportHelpDesktop' : 'app.exportHelpServer'),
}"
@click="exportOpen = true" @click="exportOpen = true"
> >
<PhExport :size="14" weight="bold" /> <PhExport :size="14" weight="bold" />
+22 -9
View File
@@ -16,6 +16,7 @@ import { useProjectName } from '@/composables/useProjectName'
import type { ButtonAsset, Layer } from '@/types/scene' import type { ButtonAsset, Layer } from '@/types/scene'
import { Modal } from '@/components/ui' import { Modal } from '@/components/ui'
import { t } from '@/i18n' import { t } from '@/i18n'
import { hasDesktopRender } from '@/lib/desktopRenderExport'
const props = defineProps<{ modelValue: boolean }>() const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>() const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
@@ -45,6 +46,8 @@ function canRecordVideo(): boolean {
} }
const videoSupported = canRecordVideo() const videoSupported = canRecordVideo()
const isDesktop = computed(() => !!window.ruwioDesktop)
const hasDesktopMp4 = computed(() => hasDesktopRender())
// Opt-in: strip layers that are hidden in the editor (`editorHidden`) // Opt-in: strip layers that are hidden in the editor (`editorHidden`)
// from the exported scene. Default off — editor-hide is a workflow // from the exported scene. Default off — editor-hide is a workflow
@@ -260,7 +263,7 @@ async function exportWebm() {
progress.value = { progress.value = {
progress: 0, progress: 0,
controller, controller,
label: t('export.labelWebm'), label: t(isDesktop.value ? 'export.labelWebmDesktop' : 'export.labelWebmBrowser'),
error: null, error: null,
} }
store.pause() store.pause()
@@ -370,7 +373,7 @@ async function exportApngPreview() {
} }
} }
async function exportMp4Server() { async function exportMp4() {
if (progress.value) return if (progress.value) return
const controller = new AbortController() const controller = new AbortController()
const base = (projectName.value ?? 'animation').toString() const base = (projectName.value ?? 'animation').toString()
@@ -378,14 +381,14 @@ async function exportMp4Server() {
progress.value = { progress.value = {
progress: 0, progress: 0,
controller, controller,
label: t('export.labelMp4'), label: t(hasDesktopMp4.value ? 'export.labelMp4Desktop' : 'export.labelMp4Server'),
error: null, error: null,
} }
store.pause() store.pause()
try { try {
const scene = activeSceneClone() const scene = activeSceneClone()
const { hasDesktopRender, renderMp4OnDesktop } = await import('@/lib/desktopRenderExport')
if (hasDesktopRender()) { if (hasDesktopRender()) {
const { renderMp4OnDesktop } = await import('@/lib/desktopRenderExport')
const result = await renderMp4OnDesktop(scene, { const result = await renderMp4OnDesktop(scene, {
signal: controller.signal, signal: controller.signal,
fps: targetFps.value, fps: targetFps.value,
@@ -593,14 +596,18 @@ function onUpdate(v: boolean) {
</button> </button>
</div> </div>
<button class="opt" @click="exportMp4Server"> <button class="opt" @click="exportMp4">
<div class="opt-icon"><PhFilmSlate :size="20" /></div> <div class="opt-icon"><PhFilmSlate :size="20" /></div>
<div class="opt-text"> <div class="opt-text">
<div class="opt-title"> <div class="opt-title">
MP4 Video MP4 Video
<span class="badge accent">{{ t('export.mp4Badge') }}</span> <span class="badge accent">
{{ t(hasDesktopMp4 ? 'export.mp4DesktopBadge' : 'export.mp4ServerBadge') }}
</span>
</div>
<div class="opt-desc">
{{ t(hasDesktopMp4 ? 'export.mp4DesktopDesc' : 'export.mp4ServerDesc') }}
</div> </div>
<div class="opt-desc">{{ t('export.mp4Desc') }}</div>
</div> </div>
<PhDownloadSimple :size="14" class="opt-arrow" /> <PhDownloadSimple :size="14" class="opt-arrow" />
</button> </button>
@@ -610,10 +617,16 @@ function onUpdate(v: boolean) {
<div class="opt-text"> <div class="opt-text">
<div class="opt-title"> <div class="opt-title">
WebM Video WebM Video
<span class="badge">{{ t('export.webmBadge') }}</span> <span class="badge">
{{ t(isDesktop ? 'export.webmDesktopBadge' : 'export.webmBrowserBadge') }}
</span>
</div> </div>
<div class="opt-desc"> <div class="opt-desc">
{{ videoSupported ? t('export.webmDesc') : t('export.webmNotSupported') }} {{
videoSupported
? t(isDesktop ? 'export.webmDesktopDesc' : 'export.webmBrowserDesc')
: t('export.webmNotSupported')
}}
</div> </div>
</div> </div>
<PhDownloadSimple :size="14" class="opt-arrow" /> <PhDownloadSimple :size="14" class="opt-arrow" />
+14 -2
View File
@@ -17,6 +17,7 @@ import {
} from '@phosphor-icons/vue' } from '@phosphor-icons/vue'
import { Modal } from '@/components/ui' import { Modal } from '@/components/ui'
import { t } from '@/i18n' import { t } from '@/i18n'
import { hasDesktopRender } from '@/lib/desktopRenderExport'
const props = defineProps<{ modelValue: boolean }>() const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>() const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
@@ -63,6 +64,9 @@ interface Chapter {
titleKey: string titleKey: string
} }
const isDesktop = !!window.ruwioDesktop
const hasDesktopMp4 = hasDesktopRender()
const CHAPTERS: Chapter[] = [ const CHAPTERS: Chapter[] = [
{ {
id: 'getting-started', id: 'getting-started',
@@ -509,8 +513,16 @@ const CHAPTERS: Chapter[] = [
{ bodyKey: 'help.content.export.formats.runtime' }, { bodyKey: 'help.content.export.formats.runtime' },
{ bodyKey: 'help.content.export.formats.scene' }, { bodyKey: 'help.content.export.formats.scene' },
{ bodyKey: 'help.content.export.formats.html' }, { bodyKey: 'help.content.export.formats.html' },
{ bodyKey: 'help.content.export.formats.mp4' }, {
{ bodyKey: 'help.content.export.formats.webm' }, bodyKey: hasDesktopMp4
? 'help.content.export.formats.mp4Desktop'
: 'help.content.export.formats.mp4',
},
{
bodyKey: isDesktop
? 'help.content.export.formats.webmDesktop'
: 'help.content.export.formats.webm',
},
], ],
}, },
{ {
+44 -20
View File
@@ -39,8 +39,10 @@ export const en = {
untitledProject: 'Untitled project', untitledProject: 'Untitled project',
saveProjectDirtyHelp: saveProjectDirtyHelp:
'Your project has unsaved changes. Ctrl/Cmd+S writes them to browser storage.', 'Your project has unsaved changes. Ctrl/Cmd+S writes them to browser storage.',
exportHelp: exportHelpServer:
'Save as runtime script, HTML, or video. MP4 needs the render server; WebM records in real time in the browser.', 'Save as runtime script, HTML, or video. MP4 uses the render server; WebM records in real time with MediaRecorder.',
exportHelpDesktop:
"Save as runtime script, HTML, or video. MP4 renders in the desktop app with bundled ffmpeg; WebM records in Electron's Chromium runtime.",
replaceDialog: { replaceDialog: {
title: 'Replace project?', title: 'Replace project?',
message: message:
@@ -509,10 +511,14 @@ export const en = {
sceneDesc: 'Scene data only — requires the runtime to be loaded', sceneDesc: 'Scene data only — requires the runtime to be loaded',
exampleTitle: 'Example HTML', exampleTitle: 'Example HTML',
exampleDesc: 'Standalone HTML — just open it in a browser', exampleDesc: 'Standalone HTML — just open it in a browser',
mp4Badge: 'Server · Frame-accurate', mp4ServerBadge: 'Server · Frame-by-frame',
mp4Desc: 'H.264, any resolution — requires render server', mp4ServerDesc: 'H.264 export via render server',
webmBadge: 'Browser · Realtime', mp4DesktopBadge: 'Desktop · Frame-by-frame',
webmDesc: 'No server — recorded in real time', mp4DesktopDesc: 'H.264 export via bundled ffmpeg',
webmBrowserBadge: 'Browser · Realtime',
webmBrowserDesc: 'Recorded in real time with MediaRecorder',
webmDesktopBadge: 'Chromium · Realtime',
webmDesktopDesc: "Recorded in real time by Electron's Chromium runtime",
webmNotSupported: 'Browser does not support MediaRecorder', webmNotSupported: 'Browser does not support MediaRecorder',
gifBadge: 'Browser · Frame-by-frame', gifBadge: 'Browser · Frame-by-frame',
gifDesc: '256-colour palette, broadest compatibility', gifDesc: '256-colour palette, broadest compatibility',
@@ -524,8 +530,10 @@ export const en = {
cancel: 'Cancel', cancel: 'Cancel',
failed: 'Export failed', failed: 'Export failed',
back: 'Back', back: 'Back',
labelWebm: 'Rendering WebM (browser, realtime)…', labelWebmBrowser: 'Rendering WebM in browser realtime...',
labelMp4: 'Rendering MP4 (server)…', labelWebmDesktop: 'Rendering WebM in Chromium realtime...',
labelMp4Server: 'Rendering MP4 with render server...',
labelMp4Desktop: 'Rendering MP4 in desktop app...',
labelGif: 'Rendering GIF (browser, frame-by-frame)…', labelGif: 'Rendering GIF (browser, frame-by-frame)…',
labelApng: 'Rendering APNG (browser, frame-by-frame)…', labelApng: 'Rendering APNG (browser, frame-by-frame)…',
labelApngPreview: 'Rendering APNG + preview page…', labelApngPreview: 'Rendering APNG + preview page…',
@@ -1078,9 +1086,13 @@ play
html: html:
'<strong>HTML example:</strong> A quick standalone page with everything wired up.', '<strong>HTML example:</strong> A quick standalone page with everything wired up.',
mp4: mp4:
'<strong>MP4 video:</strong> Frame-accurate server render. This needs the render server configured in Settings.', '<strong>MP4 video:</strong> Frame-by-frame H.264 export via the render server.',
mp4Desktop:
'<strong>MP4 video:</strong> Frame-by-frame H.264 export in the desktop app with bundled ffmpeg.',
webm: webm:
'<strong>WebM video:</strong> Browser-side realtime recording. This depends on <code>MediaRecorder</code> support.', '<strong>WebM video:</strong> Browser realtime recording with <code>MediaRecorder</code>.',
webmDesktop:
"<strong>WebM video:</strong> Realtime recording with <code>MediaRecorder</code> in Electron's Chromium runtime.",
}, },
skipHiddenTitle: 'Skip hidden layers', skipHiddenTitle: 'Skip hidden layers',
skipHiddenBody: skipHiddenBody:
@@ -1149,8 +1161,10 @@ export const de: DeepPartial<Messages> = {
untitledProject: 'Unbenanntes Projekt', untitledProject: 'Unbenanntes Projekt',
saveProjectDirtyHelp: saveProjectDirtyHelp:
'Dein Projekt hat ungespeicherte Änderungen. Mit Ctrl/Cmd+S wird es im Browser gespeichert.', 'Dein Projekt hat ungespeicherte Änderungen. Mit Ctrl/Cmd+S wird es im Browser gespeichert.',
exportHelp: exportHelpServer:
'Als Runtime, HTML oder Video exportieren. MP4 braucht den Render-Server. WebM wird direkt im Browser in Echtzeit aufgenommen.', 'Als Runtime, HTML oder Video exportieren. MP4 nutzt den Render-Server. WebM wird in Echtzeit mit MediaRecorder aufgenommen.',
exportHelpDesktop:
'Als Runtime, HTML oder Video exportieren. MP4 rendert in der Desktop-App mit gebündeltem ffmpeg. WebM wird in Electrons Chromium-Runtime aufgenommen.',
replaceDialog: { replaceDialog: {
title: 'Projekt ersetzen?', title: 'Projekt ersetzen?',
message: message:
@@ -1363,10 +1377,14 @@ export const de: DeepPartial<Messages> = {
sceneDesc: 'Nur Projektdaten: Die Runtime muss schon geladen sein.', sceneDesc: 'Nur Projektdaten: Die Runtime muss schon geladen sein.',
exampleTitle: 'HTML-Beispiel', exampleTitle: 'HTML-Beispiel',
exampleDesc: 'Eigenständige HTML-Datei: Einfach im Browser öffnen.', exampleDesc: 'Eigenständige HTML-Datei: Einfach im Browser öffnen.',
mp4Badge: 'Server · Frame-genau', mp4ServerBadge: 'Server · Frame-für-Frame',
mp4Desc: 'H.264 in jeder Auflösung: braucht den Render-Server.', mp4ServerDesc: 'H.264-Export über den Render-Server',
webmBadge: 'Browser · Echtzeit', mp4DesktopBadge: 'Desktop · Frame-für-Frame',
webmDesc: 'Ohne Server: direkt im Browser aufgenommen.', mp4DesktopDesc: 'H.264-Export über gebündeltes ffmpeg',
webmBrowserBadge: 'Browser · Echtzeit',
webmBrowserDesc: 'Echtzeit-Aufnahme mit MediaRecorder',
webmDesktopBadge: 'Chromium · Echtzeit',
webmDesktopDesc: 'Echtzeit-Aufnahme durch Electrons Chromium-Runtime',
webmNotSupported: 'Dieser Browser unterstützt MediaRecorder nicht.', webmNotSupported: 'Dieser Browser unterstützt MediaRecorder nicht.',
gifBadge: 'Browser · Frame-für-Frame', gifBadge: 'Browser · Frame-für-Frame',
gifDesc: '256-Farben-Palette, maximale Kompatibilität.', gifDesc: '256-Farben-Palette, maximale Kompatibilität.',
@@ -1378,8 +1396,10 @@ export const de: DeepPartial<Messages> = {
cancel: 'Abbrechen', cancel: 'Abbrechen',
failed: 'Export fehlgeschlagen', failed: 'Export fehlgeschlagen',
back: 'Zurück', back: 'Zurück',
labelWebm: 'WebM wird gerendert (Browser, Echtzeit)…', labelWebmBrowser: 'WebM wird in Browser-Echtzeit gerendert...',
labelMp4: 'MP4 wird gerendert (Server)…', labelWebmDesktop: 'WebM wird in Chromium-Echtzeit gerendert...',
labelMp4Server: 'MP4 wird mit dem Render-Server gerendert...',
labelMp4Desktop: 'MP4 wird in der Desktop-App gerendert...',
labelGif: 'GIF wird gerendert (Browser, frame-für-frame)…', labelGif: 'GIF wird gerendert (Browser, frame-für-frame)…',
labelApng: 'APNG wird gerendert (Browser, frame-für-frame)…', labelApng: 'APNG wird gerendert (Browser, frame-für-frame)…',
labelApngPreview: 'APNG + Vorschau werden gerendert…', labelApngPreview: 'APNG + Vorschau werden gerendert…',
@@ -1920,9 +1940,13 @@ play
html: html:
'<strong>HTML-Beispiel:</strong> Eine schnelle eigenständige Seite mit allem fertig verdrahtet.', '<strong>HTML-Beispiel:</strong> Eine schnelle eigenständige Seite mit allem fertig verdrahtet.',
mp4: mp4:
'<strong>MP4-Video:</strong> Frame-genauer Server-Render. Dafür muss der Render-Server in den Einstellungen konfiguriert sein.', '<strong>MP4-Video:</strong> Frame-für-Frame-H.264-Export über den Render-Server.',
mp4Desktop:
'<strong>MP4-Video:</strong> Frame-für-Frame-H.264-Export in der Desktop-App mit gebündeltem ffmpeg.',
webm: webm:
'<strong>WebM-Video:</strong> Browser-seitige Echtzeit-Aufnahme. Das hängt von <code>MediaRecorder</code> ab.', '<strong>WebM-Video:</strong> Echtzeit-Aufnahme im Browser mit <code>MediaRecorder</code>.',
webmDesktop:
'<strong>WebM-Video:</strong> Echtzeit-Aufnahme mit <code>MediaRecorder</code> in Electrons Chromium-Runtime.',
}, },
skipHiddenTitle: 'Versteckte Ebenen auslassen', skipHiddenTitle: 'Versteckte Ebenen auslassen',
skipHiddenBody: skipHiddenBody: