Update fullscreen preview and camera output
This commit is contained in:
1 parent
9b314ad817
commit
7093e1f6d2
9 files changed
+188
-17
No files matched your search
+15
-4
@@ -12,6 +12,7 @@ import {
|
|||||||
PhFloppyDisk,
|
PhFloppyDisk,
|
||||||
PhGearSix,
|
PhGearSix,
|
||||||
PhImageSquare,
|
PhImageSquare,
|
||||||
|
PhMonitorPlay,
|
||||||
PhPause,
|
PhPause,
|
||||||
PhPencilSimpleLine,
|
PhPencilSimpleLine,
|
||||||
PhPlay,
|
PhPlay,
|
||||||
@@ -195,6 +196,11 @@ function stop() {
|
|||||||
store.pause()
|
store.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFullscreenPreview() {
|
||||||
|
store.pause()
|
||||||
|
fullscreenOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function saveProject() {
|
async function saveProject() {
|
||||||
// projectForSave stitches the current editor-session UI state
|
// projectForSave stitches the current editor-session UI state
|
||||||
// (zoom, pan, overlay toggles, timeline display) onto the
|
// (zoom, pan, overlay toggles, timeline display) onto the
|
||||||
@@ -508,8 +514,7 @@ function onKeyDown(e: KeyboardEvent) {
|
|||||||
if (isEditable(e.target)) return
|
if (isEditable(e.target)) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
store.pause()
|
openFullscreenPreview()
|
||||||
fullscreenOpen.value = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store.togglePlay()
|
store.togglePlay()
|
||||||
@@ -595,8 +600,7 @@ async function runDesktopMenuCommand(command: string) {
|
|||||||
rewind()
|
rewind()
|
||||||
break
|
break
|
||||||
case 'fullscreen-preview':
|
case 'fullscreen-preview':
|
||||||
store.pause()
|
openFullscreenPreview()
|
||||||
fullscreenOpen.value = true
|
|
||||||
break
|
break
|
||||||
case 'toggle-panels':
|
case 'toggle-panels':
|
||||||
panelsHidden.value = !panelsHidden.value
|
panelsHidden.value = !panelsHidden.value
|
||||||
@@ -706,6 +710,13 @@ onBeforeUnmount(() => {
|
|||||||
@click="store.togglePlay()"
|
@click="store.togglePlay()"
|
||||||
/>
|
/>
|
||||||
<div class="transport-side transport-right">
|
<div class="transport-side transport-right">
|
||||||
|
<IconButton
|
||||||
|
:icon="PhMonitorPlay"
|
||||||
|
icon-weight="regular"
|
||||||
|
size="md"
|
||||||
|
:title="t('app.fullscreenPlayback')"
|
||||||
|
@click="openFullscreenPreview"
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
:icon="PhStop"
|
:icon="PhStop"
|
||||||
icon-weight="fill"
|
icon-weight="fill"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type { Theme } from '@/composables/useTheme'
|
|||||||
import { useTooltipPref } from '@/composables/useTooltipPref'
|
import { useTooltipPref } from '@/composables/useTooltipPref'
|
||||||
import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref'
|
import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref'
|
||||||
import { useShowWelcomeOnStart } from '@/composables/useShowWelcomeOnStart'
|
import { useShowWelcomeOnStart } from '@/composables/useShowWelcomeOnStart'
|
||||||
|
import { useFullscreenPreviewAutoplay } from '@/composables/useFullscreenPreviewAutoplay'
|
||||||
import { showLocked } from '@/composables/useShowLocked'
|
import { showLocked } from '@/composables/useShowLocked'
|
||||||
import { Modal, ToggleSwitch } from '@/components/ui'
|
import { Modal, ToggleSwitch } from '@/components/ui'
|
||||||
import { setLocale, t, useI18n } from '@/i18n'
|
import { setLocale, t, useI18n } from '@/i18n'
|
||||||
@@ -27,6 +28,7 @@ const { theme, setTheme } = useTheme()
|
|||||||
const { enabled: tooltipsEnabled } = useTooltipPref()
|
const { enabled: tooltipsEnabled } = useTooltipPref()
|
||||||
const { enabled: thumbsEnabled } = useAssetThumbsPref()
|
const { enabled: thumbsEnabled } = useAssetThumbsPref()
|
||||||
const { enabled: welcomeOnStart } = useShowWelcomeOnStart()
|
const { enabled: welcomeOnStart } = useShowWelcomeOnStart()
|
||||||
|
const { enabled: fullscreenAutoplay } = useFullscreenPreviewAutoplay()
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
|
||||||
// Re-check the stored clipart folder whenever settings opens — covers the
|
// Re-check the stored clipart folder whenever settings opens — covers the
|
||||||
@@ -288,6 +290,17 @@ function applyTheme(t: Theme) {
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="row-toggle">
|
||||||
|
<ToggleSwitch
|
||||||
|
v-model="fullscreenAutoplay"
|
||||||
|
:aria-label="t('settings.fullscreenAutoplayLabel')"
|
||||||
|
/>
|
||||||
|
<div class="row-toggle-text">
|
||||||
|
<div class="row-toggle-label">{{ t('settings.fullscreenAutoplayLabel') }}</div>
|
||||||
|
<div class="row-toggle-desc">{{ t('settings.fullscreenAutoplayDesc') }}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="setting-block">
|
<div class="setting-block">
|
||||||
<div class="setting-heading">{{ t('settings.appearanceTitle') }}</div>
|
<div class="setting-heading">{{ t('settings.appearanceTitle') }}</div>
|
||||||
<div class="setting-desc">{{ t('settings.themeHint') }}</div>
|
<div class="setting-desc">{{ t('settings.themeHint') }}</div>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { cameraAspect, isCameraEnabled } from '@/lib/camera'
|
|||||||
import { mountPlayer, type PlayerHandle } from '@/lib/exportPlayer'
|
import { mountPlayer, type PlayerHandle } from '@/lib/exportPlayer'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { useTimeFormat } from '@/composables/useTimeFormat'
|
import { useTimeFormat } from '@/composables/useTimeFormat'
|
||||||
|
import { useFullscreenPreviewAutoplay } from '@/composables/useFullscreenPreviewAutoplay'
|
||||||
|
|
||||||
const { formatTime } = useTimeFormat()
|
const { formatTime } = useTimeFormat()
|
||||||
|
const { enabled: autoplay } = useFullscreenPreviewAutoplay()
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
@@ -51,14 +53,14 @@ async function startPreview() {
|
|||||||
// stage. Otherwise the two runtimes fight — editor audio pool +
|
// stage. Otherwise the two runtimes fight — editor audio pool +
|
||||||
// preview audio pool would both emit the same mp3s at once.
|
// preview audio pool would both emit the same mp3s at once.
|
||||||
store.pause()
|
store.pause()
|
||||||
playing.value = true
|
playing.value = autoplay.value
|
||||||
showControls.value = false
|
showControls.value = false
|
||||||
const start = rangeIn.value
|
const start = rangeIn.value
|
||||||
const end = rangeOut.value
|
const end = rangeOut.value
|
||||||
const head = store.playhead
|
const head = store.playhead
|
||||||
const initialTime = head < start || head >= end ? start : head
|
const initialTime = head < start || head >= end ? start : head
|
||||||
localTime.value = initialTime
|
localTime.value = initialTime
|
||||||
mountAt(el, initialTime, true)
|
mountAt(el, initialTime, autoplay.value)
|
||||||
requestBrowserFullscreen()
|
requestBrowserFullscreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ watch(
|
|||||||
if (!props.modelValue) return
|
if (!props.modelValue) return
|
||||||
const still = frameEl.value
|
const still = frameEl.value
|
||||||
if (!still) return
|
if (!still) return
|
||||||
mountAt(still, keepTime, true)
|
mountAt(still, keepTime, playing.value)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'animatr.fullscreenPreviewAutoplay'
|
||||||
|
|
||||||
|
function read(): boolean {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(STORAGE_KEY) === 'true'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module-level shared ref so settings and fullscreen preview use the
|
||||||
|
// same preference without prop drilling through the app shell.
|
||||||
|
export const fullscreenPreviewAutoplay = ref<boolean>(read())
|
||||||
|
|
||||||
|
watch(fullscreenPreviewAutoplay, (v) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, String(v))
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useFullscreenPreviewAutoplay() {
|
||||||
|
return { enabled: fullscreenPreviewAutoplay }
|
||||||
|
}
|
||||||
+88
-3
@@ -34,6 +34,7 @@ import { drawButtonFrame, drawSceneFrame, renderButtonThumbnail } from '@/lib/re
|
|||||||
import { t as i18nT } from '@/i18n'
|
import { t as i18nT } from '@/i18n'
|
||||||
import { expandedTrackOrder } from '@/lib/sceneOrder'
|
import { expandedTrackOrder } from '@/lib/sceneOrder'
|
||||||
import { CLIP_FALLBACK_BOUNDS, computeClipBounds } from '@/lib/clipBounds'
|
import { CLIP_FALLBACK_BOUNDS, computeClipBounds } from '@/lib/clipBounds'
|
||||||
|
import { renderSceneFrameForOutput } from '@/lib/camera'
|
||||||
import {
|
import {
|
||||||
deleteStoredProject,
|
deleteStoredProject,
|
||||||
getStoredProject,
|
getStoredProject,
|
||||||
@@ -43,6 +44,7 @@ import {
|
|||||||
import Modal from '@/components/ui/Modal.vue'
|
import Modal from '@/components/ui/Modal.vue'
|
||||||
import IconButton from '@/components/ui/IconButton.vue'
|
import IconButton from '@/components/ui/IconButton.vue'
|
||||||
import Toast from '@/components/ui/Toast.vue'
|
import Toast from '@/components/ui/Toast.vue'
|
||||||
|
import App from '@/App.vue'
|
||||||
import PropertiesPanel from '@/components/PropertiesPanel.vue'
|
import PropertiesPanel from '@/components/PropertiesPanel.vue'
|
||||||
import ClipartDrawer from '@/components/ClipartDrawer.vue'
|
import ClipartDrawer from '@/components/ClipartDrawer.vue'
|
||||||
import SceneDrawer from '@/components/SceneDrawer.vue'
|
import SceneDrawer from '@/components/SceneDrawer.vue'
|
||||||
@@ -55,6 +57,7 @@ import ExportDialog from '@/components/ExportDialog.vue'
|
|||||||
import WelcomeDialog from '@/components/WelcomeDialog.vue'
|
import WelcomeDialog from '@/components/WelcomeDialog.vue'
|
||||||
import FullscreenPreview from '@/components/FullscreenPreview.vue'
|
import FullscreenPreview from '@/components/FullscreenPreview.vue'
|
||||||
import HelpDialog from '@/components/HelpDialog.vue'
|
import HelpDialog from '@/components/HelpDialog.vue'
|
||||||
|
import { fullscreenPreviewAutoplay } from '@/composables/useFullscreenPreviewAutoplay'
|
||||||
import { createAudioRuntime } from '@/composables/useAudioRuntime'
|
import { createAudioRuntime } from '@/composables/useAudioRuntime'
|
||||||
import {
|
import {
|
||||||
clearWaveformCache,
|
clearWaveformCache,
|
||||||
@@ -1428,6 +1431,88 @@ export function registerAllTests(): void {
|
|||||||
expect(frame?.querySelector('canvas')).toBeTruthy()
|
expect(frame?.querySelector('canvas')).toBeTruthy()
|
||||||
handle.unmount()
|
handle.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('fullscreen preview autoplay preference controls the initial transport state', async () => {
|
||||||
|
makeFreshStore()
|
||||||
|
const before = fullscreenPreviewAutoplay.value
|
||||||
|
const primaryButton = () =>
|
||||||
|
document.body.querySelector('.fs-toolbar .fs-btn') as HTMLButtonElement | null
|
||||||
|
try {
|
||||||
|
fullscreenPreviewAutoplay.value = false
|
||||||
|
const pausedHandle = mountComponent(FullscreenPreview, { modelValue: true })
|
||||||
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
|
expect(primaryButton()?.getAttribute('aria-label')).toBe('Play')
|
||||||
|
pausedHandle.unmount()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
fullscreenPreviewAutoplay.value = true
|
||||||
|
const playingHandle = mountComponent(FullscreenPreview, { modelValue: true })
|
||||||
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
|
expect(primaryButton()?.getAttribute('aria-label')).toBe('Pause')
|
||||||
|
playingHandle.unmount()
|
||||||
|
} finally {
|
||||||
|
fullscreenPreviewAutoplay.value = before
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('top transport fullscreen button opens the preview and pauses editor playback', async () => {
|
||||||
|
const s = makeFreshStore()
|
||||||
|
s.play()
|
||||||
|
expect(s.playing).toBe(true)
|
||||||
|
const handle = mountComponent(App, {})
|
||||||
|
try {
|
||||||
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
|
const btn = handle.host.querySelector(
|
||||||
|
`[aria-label="${i18nT('app.fullscreenPlayback')}"]`,
|
||||||
|
) as HTMLButtonElement | null
|
||||||
|
expect(btn).toBeTruthy()
|
||||||
|
btn!.click()
|
||||||
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
|
expect(s.playing).toBe(false)
|
||||||
|
expect(document.body.querySelector('.fs-root')).toBeTruthy()
|
||||||
|
expect(document.body.querySelector('.fs-frame')).toBeTruthy()
|
||||||
|
} finally {
|
||||||
|
handle.unmount()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('camera output renders off-stage content over the scene background', () => {
|
||||||
|
const s = makeFreshStore()
|
||||||
|
s.scene.canvas.width = 100
|
||||||
|
s.scene.canvas.height = 100
|
||||||
|
s.scene.canvas.background = '#123456'
|
||||||
|
const rect = s.addAsset({
|
||||||
|
kind: 'rect',
|
||||||
|
name: 'Outside',
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
fill: '#ff0000',
|
||||||
|
})
|
||||||
|
const layer = s.addLayer(rect.id)!
|
||||||
|
s.upsertKeyframe(layer.id, 0, { x: -15, y: 50 })
|
||||||
|
s.enableCamera()
|
||||||
|
s.setCameraBaseProps({ x: -50, y: 0, width: 100, height: 100 })
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 100
|
||||||
|
canvas.height = 100
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
renderSceneFrameForOutput(ctx, s.scene, 0, {}, { width: 100, height: 100 })
|
||||||
|
|
||||||
|
const outsideStageBg = ctx.getImageData(10, 50, 1, 1).data
|
||||||
|
expect(Array.from(outsideStageBg.slice(0, 3))).toEqual([0x12, 0x34, 0x56])
|
||||||
|
expect(outsideStageBg[3]).toBe(255)
|
||||||
|
|
||||||
|
const outsideStageLayer = ctx.getImageData(35, 50, 1, 1).data
|
||||||
|
expect(outsideStageLayer[0]).toBeGreaterThan(200)
|
||||||
|
expect(outsideStageLayer[1]).toBeLessThan(40)
|
||||||
|
expect(outsideStageLayer[2]).toBeLessThan(40)
|
||||||
|
expect(outsideStageLayer[3]).toBe(255)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
suite('export — target size scaling', () => {
|
suite('export — target size scaling', () => {
|
||||||
@@ -15897,7 +15982,7 @@ export function registerAllTests(): void {
|
|||||||
expect(src).toContain('clipKaleidoPivot')
|
expect(src).toContain('clipKaleidoPivot')
|
||||||
expect(src).toContain('clipArmLocalT')
|
expect(src).toContain('clipArmLocalT')
|
||||||
// Runtime version got bumped during the kaleido + wiggle work.
|
// Runtime version got bumped during the kaleido + wiggle work.
|
||||||
expect(src).toContain("'35'")
|
expect(src).toContain("'36'")
|
||||||
})
|
})
|
||||||
|
|
||||||
test('exported runtime layerActiveAt is unaffected by kaleido fields', async () => {
|
test('exported runtime layerActiveAt is unaffected by kaleido fields', async () => {
|
||||||
@@ -16275,7 +16360,7 @@ export function registerAllTests(): void {
|
|||||||
expect(src).toContain('function wiggleSeeds(')
|
expect(src).toContain('function wiggleSeeds(')
|
||||||
// Runtime version got bumped through the wiggle work and the
|
// Runtime version got bumped through the wiggle work and the
|
||||||
// later layer-kaleido-pivot landing.
|
// later layer-kaleido-pivot landing.
|
||||||
expect(src).toContain("'35'")
|
expect(src).toContain("'36'")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -16807,7 +16892,7 @@ export function registerAllTests(): void {
|
|||||||
const src = generateRuntimeScript()
|
const src = generateRuntimeScript()
|
||||||
expect(src).toContain('function layerArmTf(')
|
expect(src).toContain('function layerArmTf(')
|
||||||
// Bumped on this work.
|
// Bumped on this work.
|
||||||
expect(src).toContain("'35'")
|
expect(src).toContain("'36'")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ export const en = {
|
|||||||
pause: 'Pause (Space)',
|
pause: 'Pause (Space)',
|
||||||
play: 'Play (Space)',
|
play: 'Play (Space)',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
|
fullscreenPlayback: 'Fullscreen playback (Shift+Space)',
|
||||||
recentProjects: 'Recent projects',
|
recentProjects: 'Recent projects',
|
||||||
copyStill: {
|
copyStill: {
|
||||||
label: 'Copy current frame',
|
label: 'Copy current frame',
|
||||||
@@ -467,6 +468,9 @@ export const en = {
|
|||||||
welcomeOnStartLabel: 'Show welcome screen on start',
|
welcomeOnStartLabel: 'Show welcome screen on start',
|
||||||
welcomeOnStartDesc:
|
welcomeOnStartDesc:
|
||||||
'Open the welcome / recents dialog when RUWIO launches. Off → boot straight into the last edited project. The last file is always restored either way; the welcome dialog just gives you a chance to switch.',
|
'Open the welcome / recents dialog when RUWIO launches. Off → boot straight into the last edited project. The last file is always restored either way; the welcome dialog just gives you a chance to switch.',
|
||||||
|
fullscreenAutoplayLabel: 'Autoplay in fullscreen preview',
|
||||||
|
fullscreenAutoplayDesc:
|
||||||
|
'Start playback immediately when fullscreen preview opens. When off, fullscreen opens paused and Space starts it.',
|
||||||
bgVideoLabel: 'Video',
|
bgVideoLabel: 'Video',
|
||||||
bgVideoNone: 'No background video',
|
bgVideoNone: 'No background video',
|
||||||
bgVideoPickFile: 'Pick video file…',
|
bgVideoPickFile: 'Pick video file…',
|
||||||
@@ -1140,6 +1144,7 @@ export const de: DeepPartial<Messages> = {
|
|||||||
pause: 'Pause (Leertaste)',
|
pause: 'Pause (Leertaste)',
|
||||||
play: 'Abspielen (Leertaste)',
|
play: 'Abspielen (Leertaste)',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
|
fullscreenPlayback: 'Fullscreen-Wiedergabe (Shift+Leertaste)',
|
||||||
recentProjects: 'Letzte Projekte',
|
recentProjects: 'Letzte Projekte',
|
||||||
copyStill: {
|
copyStill: {
|
||||||
label: 'Aktuellen Frame kopieren',
|
label: 'Aktuellen Frame kopieren',
|
||||||
@@ -1334,6 +1339,12 @@ export const de: DeepPartial<Messages> = {
|
|||||||
assetThumbsLabel: 'Asset-Vorschaubilder',
|
assetThumbsLabel: 'Asset-Vorschaubilder',
|
||||||
assetThumbsDesc:
|
assetThumbsDesc:
|
||||||
'Zeigt kleine Vorschauen in Assets, Clips und Buttons. Aus ist kompakter, an ist übersichtlicher.',
|
'Zeigt kleine Vorschauen in Assets, Clips und Buttons. Aus ist kompakter, an ist übersichtlicher.',
|
||||||
|
welcomeOnStartLabel: 'Willkommensfenster beim Start anzeigen',
|
||||||
|
welcomeOnStartDesc:
|
||||||
|
'Öffnet beim Start von RUWIO den Willkommen-/Letzte-Projekte-Dialog. Aus startet direkt mit dem zuletzt bearbeiteten Projekt.',
|
||||||
|
fullscreenAutoplayLabel: 'Autoplay in Fullscreen-Vorschau',
|
||||||
|
fullscreenAutoplayDesc:
|
||||||
|
'Startet die Wiedergabe sofort, wenn die Fullscreen-Vorschau geöffnet wird. Wenn aus, öffnet sie pausiert und Leertaste startet sie.',
|
||||||
bgVideoLabel: 'Video',
|
bgVideoLabel: 'Video',
|
||||||
bgVideoNone: 'Kein Hintergrundvideo',
|
bgVideoNone: 'Kein Hintergrundvideo',
|
||||||
bgVideoPickFile: 'Videodatei wählen…',
|
bgVideoPickFile: 'Videodatei wählen…',
|
||||||
|
|||||||
+10
-1
@@ -182,11 +182,20 @@ export function renderSceneFrameForOutput(
|
|||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||||
ctx.clearRect(0, 0, outputWidth, outputHeight)
|
ctx.clearRect(0, 0, outputWidth, outputHeight)
|
||||||
|
const bg = scene.canvas.background
|
||||||
|
if (!scene.backgroundVideo && bg && bg !== 'transparent') {
|
||||||
|
ctx.fillStyle = bg
|
||||||
|
ctx.fillRect(0, 0, outputWidth, outputHeight)
|
||||||
|
}
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.scale(outputWidth / camera.width, outputHeight / camera.height)
|
ctx.scale(outputWidth / camera.width, outputHeight / camera.height)
|
||||||
ctx.translate(-camera.x, -camera.y)
|
ctx.translate(-camera.x, -camera.y)
|
||||||
drawSceneFrame(ctx, scene, time, images, { crop: options?.cropStage })
|
drawSceneFrame(ctx, scene, time, images, {
|
||||||
|
crop: options?.cropStage ?? false,
|
||||||
|
clear: false,
|
||||||
|
background: false,
|
||||||
|
})
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ function isProject(p: ExportPayload): p is Project {
|
|||||||
return Array.isArray((p as Partial<Project>).scenes)
|
return Array.isArray((p as Partial<Project>).scenes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const RUNTIME_VERSION = '35'
|
const RUNTIME_VERSION = '36'
|
||||||
|
|
||||||
/** Materialize the engine source. The font map is rebuilt on every
|
/** Materialize the engine source. The font map is rebuilt on every
|
||||||
* call so user-uploaded custom fonts (which live in localStorage
|
* call so user-uploaded custom fonts (which live in localStorage
|
||||||
@@ -1178,10 +1178,16 @@ const ENGINE_TEMPLATE = `(function(){
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(0,0,vw,vh);
|
ctx.rect(0,0,vw,vh);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
|
if(useCamera && !scene.backgroundVideo && scene.canvas.background && scene.canvas.background !== 'transparent'){
|
||||||
|
ctx.fillStyle=scene.canvas.background;
|
||||||
|
ctx.fillRect(0,0,vw,vh);
|
||||||
|
}
|
||||||
if(useCamera) ctx.translate(-cam.x,-cam.y);
|
if(useCamera) ctx.translate(-cam.x,-cam.y);
|
||||||
if(!scene.backgroundVideo){
|
if(!scene.backgroundVideo){
|
||||||
ctx.fillStyle=scene.canvas.background;
|
if(!useCamera){
|
||||||
ctx.fillRect(0,0,lw,lh);
|
ctx.fillStyle=scene.canvas.background;
|
||||||
|
ctx.fillRect(0,0,lw,lh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var layerById={};
|
var layerById={};
|
||||||
for(var li=0;li<scene.layers.length;li++)layerById[scene.layers[li].id]=scene.layers[li];
|
for(var li=0;li<scene.layers.length;li++)layerById[scene.layers[li].id]=scene.layers[li];
|
||||||
|
|||||||
+10
-3
@@ -566,11 +566,18 @@ export function drawSceneFrame(
|
|||||||
scene: Scene,
|
scene: Scene,
|
||||||
time: number,
|
time: number,
|
||||||
images: Record<string, HTMLImageElement>,
|
images: Record<string, HTMLImageElement>,
|
||||||
options?: { crop?: boolean; buttonStates?: Map<string, ButtonState> },
|
options?: {
|
||||||
|
crop?: boolean
|
||||||
|
clear?: boolean
|
||||||
|
background?: boolean
|
||||||
|
buttonStates?: Map<string, ButtonState>
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const lw = scene.canvas.width
|
const lw = scene.canvas.width
|
||||||
const lh = scene.canvas.height
|
const lh = scene.canvas.height
|
||||||
const crop = options?.crop !== false
|
const crop = options?.crop !== false
|
||||||
|
const clear = options?.clear !== false
|
||||||
|
const paintBackground = options?.background !== false
|
||||||
const buttonStates = options?.buttonStates
|
const buttonStates = options?.buttonStates
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
@@ -583,13 +590,13 @@ export function drawSceneFrame(
|
|||||||
// Mandatory for transparent-background scenes so subsequent frames
|
// Mandatory for transparent-background scenes so subsequent frames
|
||||||
// don't accumulate alpha; harmless for opaque ones since `fillRect`
|
// don't accumulate alpha; harmless for opaque ones since `fillRect`
|
||||||
// below paints over everything.
|
// below paints over everything.
|
||||||
ctx.clearRect(0, 0, lw, lh)
|
if (clear) ctx.clearRect(0, 0, lw, lh)
|
||||||
// A background video takes over the backdrop — leave the stage area
|
// A background video takes over the backdrop — leave the stage area
|
||||||
// transparent so the DOM video underneath is visible. Same treatment
|
// transparent so the DOM video underneath is visible. Same treatment
|
||||||
// when the canvas background is explicitly 'transparent' so
|
// when the canvas background is explicitly 'transparent' so
|
||||||
// APNG/WebM exports carry the alpha channel out to the file.
|
// APNG/WebM exports carry the alpha channel out to the file.
|
||||||
const bg = scene.canvas.background
|
const bg = scene.canvas.background
|
||||||
if (!scene.backgroundVideo && bg && bg !== 'transparent') {
|
if (paintBackground && !scene.backgroundVideo && bg && bg !== 'transparent') {
|
||||||
ctx.fillStyle = bg
|
ctx.fillStyle = bg
|
||||||
ctx.fillRect(0, 0, lw, lh)
|
ctx.fillRect(0, 0, lw, lh)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user