Add output camera framing
This commit is contained in:
1 parent
616ced17ce
commit
23a66c6fdf
19 files changed
+1955
-43
No files matched your search
@@ -17,8 +17,9 @@ import {
|
||||
PhX,
|
||||
} from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { sampleCamera } from '@/lib/camera'
|
||||
import { sampleLayer } from '@/lib/interpolate'
|
||||
import type { Action, AnimatableProps, Layer } from '@/types/scene'
|
||||
import type { Action, AnimatableProps, CameraProps, Layer } from '@/types/scene'
|
||||
import RangeSlider from './RangeSlider.vue'
|
||||
import ActionEditor from './ActionEditor.vue'
|
||||
import FontPicker from './FontPicker.vue'
|
||||
@@ -97,6 +98,65 @@ const sampled = computed<AnimatableProps | null>(() =>
|
||||
selected.value ? sampleLayer(selected.value, store.playhead) : null,
|
||||
)
|
||||
|
||||
const selectedCameraTrack = computed(() =>
|
||||
store.selectedCamera ? (store.scene.camera ?? null) : null,
|
||||
)
|
||||
|
||||
const sampledCamera = computed<CameraProps | null>(() =>
|
||||
selectedCameraTrack.value ? sampleCamera(store.scene, store.playhead) : null,
|
||||
)
|
||||
|
||||
const cameraHasKeyframeHere = computed(() => {
|
||||
const camera = selectedCameraTrack.value
|
||||
if (!camera) return false
|
||||
return camera.keyframes.some((kf) => Math.abs(kf.time - store.playhead) < 1e-3)
|
||||
})
|
||||
|
||||
const cameraEnabled = computed<boolean>({
|
||||
get: () => selectedCameraTrack.value?.enabled ?? false,
|
||||
set: (v) => {
|
||||
if (v) store.enableCamera()
|
||||
else store.disableCamera()
|
||||
},
|
||||
})
|
||||
|
||||
const cameraOverlayVisible = computed<boolean>({
|
||||
get: () => selectedCameraTrack.value?.overlayVisible !== false,
|
||||
set: (v) => store.setCameraOverlayVisible(v),
|
||||
})
|
||||
|
||||
const cameraLocked = computed<boolean>({
|
||||
get: () => selectedCameraTrack.value?.locked === true,
|
||||
set: (v) => store.setCameraLocked(v),
|
||||
})
|
||||
|
||||
function setCameraProp(patch: Partial<CameraProps>) {
|
||||
const camera = selectedCameraTrack.value
|
||||
if (!camera || camera.locked) return
|
||||
store.upsertCameraKeyframe(store.playhead, patch)
|
||||
}
|
||||
|
||||
const cameraX = computed<number>({
|
||||
get: () => Math.round(sampledCamera.value?.x ?? 0),
|
||||
set: (v) => setCameraProp({ x: v }),
|
||||
})
|
||||
const cameraY = computed<number>({
|
||||
get: () => Math.round(sampledCamera.value?.y ?? 0),
|
||||
set: (v) => setCameraProp({ y: v }),
|
||||
})
|
||||
const cameraWidth = computed<number>({
|
||||
get: () => Math.round(sampledCamera.value?.width ?? store.scene.canvas.width),
|
||||
set: (v) => setCameraProp({ width: Math.max(1, v) }),
|
||||
})
|
||||
const cameraHeight = computed<number>({
|
||||
get: () => Math.round(sampledCamera.value?.height ?? store.scene.canvas.height),
|
||||
set: (v) => setCameraProp({ height: Math.max(1, v) }),
|
||||
})
|
||||
|
||||
function removeCameraKeyframeHere() {
|
||||
store.removeCameraKeyframeAt(store.playhead)
|
||||
}
|
||||
|
||||
/** True when the user has more than one layer in the selection.
|
||||
* Drives the multi-edit flow: writers fan out across every
|
||||
* editable selected layer, kind-specific sections collapse, and
|
||||
@@ -599,7 +659,71 @@ function deleteSelection() {
|
||||
<template>
|
||||
<div class="panel">
|
||||
<h3>Properties</h3>
|
||||
<template v-if="selected && sampled && editableLayers.length > 0">
|
||||
<template v-if="selectedCameraTrack && sampledCamera">
|
||||
<div class="multi-banner">{{ t('properties.camera.banner') }}</div>
|
||||
<section class="prop-group">
|
||||
<button class="group-header" @click="toggleSection('transform')">
|
||||
<component :is="sectionOpen.transform ? PhCaretDown : PhCaretRight" :size="11" />
|
||||
<span>{{ t('properties.camera.frame') }}</span>
|
||||
</button>
|
||||
<div v-if="sectionOpen.transform" class="group-body">
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">{{ t('properties.camera.enabled') }}</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: cameraEnabled }"
|
||||
@click="cameraEnabled = !cameraEnabled"
|
||||
>
|
||||
{{ cameraEnabled ? 'On' : 'Off' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">{{ t('properties.camera.overlay') }}</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: cameraOverlayVisible }"
|
||||
@click="cameraOverlayVisible = !cameraOverlayVisible"
|
||||
>
|
||||
{{ cameraOverlayVisible ? 'On' : 'Off' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">{{ t('properties.camera.locked') }}</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: cameraLocked }"
|
||||
@click="cameraLocked = !cameraLocked"
|
||||
>
|
||||
{{ cameraLocked ? 'On' : 'Off' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>X<input type="number" v-model.number="cameraX" :disabled="cameraLocked" /></label>
|
||||
<label>Y<input type="number" v-model.number="cameraY" :disabled="cameraLocked" /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>{{ t('properties.camera.width') }}<input type="number" min="1" v-model.number="cameraWidth" :disabled="cameraLocked" /></label>
|
||||
<label>{{ t('properties.camera.height') }}<input type="number" min="1" v-model.number="cameraHeight" :disabled="cameraLocked" /></label>
|
||||
</div>
|
||||
<div class="kf-state">
|
||||
<span>
|
||||
{{ cameraHasKeyframeHere ? t('properties.keyframeLabel') : t('properties.interpolated') }}
|
||||
<span class="kf-time">@ {{ formatTime(store.playhead) }}</span>
|
||||
</span>
|
||||
<button
|
||||
v-if="cameraHasKeyframeHere"
|
||||
class="kf-btn"
|
||||
v-tooltip="t('properties.removeKeyframe')"
|
||||
:disabled="cameraLocked"
|
||||
@click="removeCameraKeyframeHere"
|
||||
>
|
||||
<PhMinus :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<template v-else-if="selected && sampled && editableLayers.length > 0">
|
||||
<div v-if="multiSelected" class="multi-banner">
|
||||
{{ t('properties.multiSelected', { n: editableLayers.length }) }}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user