This commit is contained in:
th committed 2026-04-20 01:05:27 +02:00
1 parent 43933f93d1
commit 03b1147a93
10 files changed
+804 -80

No files matched your search

+92 -1
View File
@@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type Konva from 'konva'
import { PhCornersIn, PhCrop } from '@phosphor-icons/vue'
import { PhCornersIn, PhCrop, PhMaskHappy, PhPencilSimple } from '@phosphor-icons/vue'
import { useSceneStore } from '@/stores/scene'
import { sampleLayer } from '@/lib/interpolate'
import { loadImage } from '@/lib/fileLoader'
import { drawSceneFrame, loadSceneImages } from '@/lib/render'
import type { AnimatableProps, Asset, Layer } from '@/types/scene'
import { t } from '@/i18n'
import { useKfSelection } from '@/composables/useKfSelection'
@@ -22,6 +23,9 @@ const zoom = ref(1)
const viewX = ref(0)
const viewY = ref(0)
const cropObjects = ref(false)
const maskPreview = ref(false)
const previewCanvasEl = ref<HTMLCanvasElement | null>(null)
const previewImages = ref<Record<string, HTMLImageElement>>({})
let didInitialFit = false
const ZOOM_MIN = 0.05
@@ -251,6 +255,15 @@ function shadowCfg(props: AnimatableProps): Record<string, unknown> {
}
function baseNodeConfig(layer: Layer, props: AnimatableProps) {
const maskGhost = layer.isMask
? {
opacity: props.opacity * 0.45,
stroke: layer.maskMode === 'negative' ? '#ff6b6b' : '#6ea8ff',
strokeWidth: 1.5,
dash: [6, 4],
strokeScaleEnabled: false,
}
: {}
return {
id: layer.id,
x: props.x,
@@ -262,6 +275,7 @@ function baseNodeConfig(layer: Layer, props: AnimatableProps) {
visible: layer.visible,
draggable: true,
...shadowCfg(props),
...maskGhost,
}
}
@@ -825,6 +839,62 @@ function onContainerDrop(e: DragEvent) {
}
}
function resizePreviewCanvas() {
const canvas = previewCanvasEl.value
if (!canvas) return
const dpr = window.devicePixelRatio || 1
const cw = containerSize.value.width
const ch = containerSize.value.height
canvas.width = Math.max(1, Math.round(cw * dpr))
canvas.height = Math.max(1, Math.round(ch * dpr))
canvas.style.width = `${cw}px`
canvas.style.height = `${ch}px`
}
function drawPreview() {
const canvas = previewCanvasEl.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const dpr = window.devicePixelRatio || 1
const cw = containerSize.value.width
const ch = containerSize.value.height
if (!cw || !ch) return
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, cw, ch)
ctx.save()
ctx.translate(viewX.value, viewY.value)
ctx.scale(zoom.value, zoom.value)
drawSceneFrame(ctx, store.scene, store.playhead, previewImages.value)
ctx.restore()
}
watch(
[
() => store.playhead,
() => store.scene,
() => containerSize.value,
zoom,
viewX,
viewY,
],
async () => {
if (!maskPreview.value) return
await nextTick()
resizePreviewCanvas()
drawPreview()
},
{ deep: true },
)
watch(maskPreview, async (on) => {
if (!on) return
previewImages.value = await loadSceneImages(store.scene)
await nextTick()
resizePreviewCanvas()
drawPreview()
})
function zoomBy(factor: number) {
const cw = containerSize.value.width
const ch = containerSize.value.height
@@ -966,6 +1036,11 @@ onBeforeUnmount(() => {
<v-transformer ref="transformerRef" :config="transformerConfig" />
</v-layer>
</v-stage>
<canvas
v-show="maskPreview"
ref="previewCanvasEl"
class="mask-preview-canvas"
></canvas>
<div class="zoom-controls" @pointerdown.stop>
<button class="zc-btn" :title="t('canvas.zoomOut')" @click="zoomBy(1 / 1.25)">−</button>
@@ -987,6 +1062,15 @@ onBeforeUnmount(() => {
>
<PhCrop :size="13" />
</button>
<button
class="zc-btn"
:class="{ active: maskPreview }"
:title="maskPreview ? t('canvas.maskPreviewOff') : t('canvas.maskPreviewOn')"
@click="maskPreview = !maskPreview"
>
<PhMaskHappy v-if="!maskPreview" :size="13" />
<PhPencilSimple v-else :size="13" />
</button>
</div>
<div class="dims">{{ store.scene.canvas.width }} × {{ store.scene.canvas.height }}</div>
@@ -1001,6 +1085,13 @@ onBeforeUnmount(() => {
overflow: hidden;
}
.mask-preview-canvas {
position: absolute;
inset: 0;
pointer-events: none;
background: var(--bg);
}
.dims {
position: absolute;
bottom: 10px;