0.16.0: animated GIF / APNG export + transparent canvas
- New frame-by-frame export pipeline in src/lib/frameExport.ts: loads scene images, walks rangeIn..rangeOut at scene.fps, drawSceneFrame to an offscreen canvas, awaits a per-frame callback, yields every 4 frames to keep progress responsive, honours AbortSignal. - Animated GIF via gifenc (per-frame quantize/palette, ~5 KB lib). - Animated PNG via UPNG.js + pako (lossless RGBA, alpha-aware, ~30 KB). - drawSceneFrame now clearRects first and skips the background fill when scene.canvas.background === 'transparent' — prerequisite for the alpha to reach the APNG output. - DocumentDrawer: new checkerboard toggle next to the colour picker flips the stage between its last-picked colour and transparent; disabled swatch + 'Transparent' hex label make the state obvious. - ExportDialog: three new option rows — Animated GIF, Animated PNG, and an APNG preview page that bundles the rendered APNG into a standalone HTML with a checkerboard backdrop + dark/light toggles for verifying transparency at a glance. - Tests: renderFrames frame count + abort, GIF header magic, APNG acTL presence, transparent round-trip (decoded alpha == 0 on un-painted pixels), preview HTML structure, and the two UI tests covering ExportDialog's new rows and the progress-view transition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
827d80bfd0
commit
fcfd21428d
12 files changed
+956
-14
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { PhFilmReel, PhPlus, PhX } from '@phosphor-icons/vue'
|
||||
import { PhCheckerboard, PhFilmReel, PhPlus, PhX } from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { confirmDialog, promptDialog, dialogState } from '@/components/ui'
|
||||
import { useDocumentDrawer } from '@/composables/useDocumentDrawer'
|
||||
@@ -240,6 +240,29 @@ const background = computed({
|
||||
set: (v: string) => store.setCanvasBackground(v),
|
||||
})
|
||||
|
||||
// Transparent toggle: stores 'transparent' sentinel when on, falls back
|
||||
// to the most recent colour the user picked when turned off. The fallback
|
||||
// sticks in a local ref so flipping the toggle doesn't lose their custom
|
||||
// colour each time.
|
||||
const lastNonTransparent = ref<string>(
|
||||
store.scene.canvas.background && store.scene.canvas.background !== 'transparent'
|
||||
? store.scene.canvas.background
|
||||
: '#ffffff',
|
||||
)
|
||||
const bgTransparent = computed({
|
||||
get: () => store.scene.canvas.background === 'transparent',
|
||||
set: (on: boolean) => {
|
||||
if (on) {
|
||||
if (store.scene.canvas.background !== 'transparent') {
|
||||
lastNonTransparent.value = store.scene.canvas.background || '#ffffff'
|
||||
}
|
||||
store.setCanvasBackground('transparent')
|
||||
} else {
|
||||
store.setCanvasBackground(lastNonTransparent.value || '#ffffff')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const duration = computed({
|
||||
get: () => store.scene.duration,
|
||||
set: (v: number) => store.setDuration(v),
|
||||
@@ -339,14 +362,26 @@ async function onRemovePreset(p: CanvasPreset) {
|
||||
<div class="fields-group">
|
||||
<label class="field color-field">
|
||||
<span>{{ t('settings.background') }}</span>
|
||||
<div class="color-row">
|
||||
<div class="color-row" :class="{ transparent: bgTransparent }">
|
||||
<input
|
||||
type="color"
|
||||
class="color-swatch"
|
||||
:value="background"
|
||||
:value="bgTransparent ? lastNonTransparent : background"
|
||||
:disabled="bgTransparent"
|
||||
@input="background = ($event.target as HTMLInputElement).value"
|
||||
/>
|
||||
<span class="hex">{{ background.toUpperCase() }}</span>
|
||||
<span class="hex">
|
||||
{{ bgTransparent ? t('settings.backgroundTransparent') : background.toUpperCase() }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-transparent-btn"
|
||||
:class="{ active: bgTransparent }"
|
||||
v-tooltip="t('settings.backgroundTransparentTooltip')"
|
||||
@click="bgTransparent = !bgTransparent"
|
||||
>
|
||||
<PhCheckerboard :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field bg-video-field">
|
||||
@@ -590,6 +625,38 @@ async function onRemovePreset(p: CanvasPreset) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.color-swatch:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bg-transparent-btn {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
border-radius: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bg-transparent-btn:hover {
|
||||
color: var(--text);
|
||||
background: var(--panel-alt);
|
||||
}
|
||||
|
||||
.bg-transparent-btn.active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.color-swatch::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user