diff --git a/apps/lab/app/components/lab/LabLayerProps.vue b/apps/lab/app/components/lab/LabLayerProps.vue index cc3e4f25..b6ed68fc 100644 --- a/apps/lab/app/components/lab/LabLayerProps.vue +++ b/apps/lab/app/components/lab/LabLayerProps.vue @@ -7,7 +7,7 @@ * camera is expressed as a zoom rather than a distance. */ -import { FONT_CATALOGUE } from '~/utils/lab/layers' +import { FONT_CATALOGUE, MAX_LAYER_SPEED, MIN_LAYER_SPEED, canChangeLayerSpeed, layerDurationForSourceEnd, layerSpeed, withLayerSpeed } from '~/utils/lab/layers' import type { Layer } from '~/utils/lab/layers' import { ENTRIES } from '~/utils/lab/registry' @@ -37,10 +37,15 @@ const emit = defineEmits<{ */ const fitLength = computed(() => { if (!props.sequenceMs) return null - const length = Math.max(100, props.sequenceMs - (props.layer.trim ?? 0)) + const length = layerDurationForSourceEnd(props.layer, props.sequenceMs) return Math.abs(length - props.layer.duration) < 50 ? null : length }) +function setSpeed(speed: number) { + const updated = withLayerSpeed(props.layer, speed) + emit('update', { duration: updated.duration, speed: updated.speed }) +} + const KINDS = { component: { label: 'animation', icon: 'i-lucide-square-play' }, video: { label: 'video', icon: 'i-lucide-film' }, @@ -457,6 +462,18 @@ const TEXT_TOGGLES = [ :default="0" @update:model-value="emit('update', { start: $event })" /> + import type { LabMenuAction } from './LabMenu.vue' import { DEFAULT_SETTINGS, FRAME_RATES, HINTS, OUTPUT_PRESETS, RANGES, SPEEDS, VIEWPORTS, frameCountFor, outputDuration } from '~/utils/lab/settings' -import type { AsciiSet, LabSettings, RangedKey, StylizeMode } from '~/utils/lab/settings' +import type { AsciiSet, LabSettings, RangedKey, ShotSettingKey, ShotSettings, StylizeMode } from '~/utils/lab/settings' import { ASCII_MIN_CELL } from '~/utils/lab/ascii' import type { LabMode } from '~/utils/lab/storage' import type { Layer } from '~/utils/lab/layers' @@ -26,6 +26,9 @@ const props = defineProps<{ layers: Layer[] selectedLayerId: string | null selectedLayer: Layer | null + /** Effective visual settings for the selected clip, or the timeline defaults. */ + shotSettings: LabSettings + shotCustomized: boolean /** Length the selected clip's animation declares, when it declares one. */ sequenceMs?: number canUndo: boolean @@ -56,6 +59,8 @@ const emit = defineEmits<{ resetEverything: [] cancel: [] updateLayer: [id: string, patch: Partial] + updateShotSetting: [key: ShotSettingKey, value: ShotSettings[ShotSettingKey]] + resetShot: [] removeLayer: [] duplicateLayer: [] }>() @@ -64,6 +69,17 @@ const settings = defineModel('settings', { required: true }) const picking = defineModel('picking', { required: true }) const camera = defineModel('camera', { required: true }) +const shot = new Proxy({} as LabSettings, { + get(_target, key) { + return typeof key === 'symbol' ? undefined : props.shotSettings[key as keyof LabSettings] + }, + set(_target, key, value) { + if (typeof key === 'symbol') return false + emit('updateShotSetting', key as ShotSettingKey, value as ShotSettings[ShotSettingKey]) + return true + }, +}) + /** * Bind a control to the shared range table, plus the value it resets to. * @@ -239,7 +255,7 @@ const segmentSeconds = computed(() => (settings.value.timelineLength / 1000).toF * a broken slider. */ const hasDepth = computed(() => - Math.abs(settings.value.pitch) > 0.5 || Math.abs(settings.value.yaw) > 0.5, + Math.abs(shot.pitch) > 0.5 || Math.abs(shot.yaw) > 0.5, ) /** @@ -247,11 +263,11 @@ const hasDepth = computed(() => * it is zero. Said outright rather than by disabling them — a control that has * gone grey never explains what would bring it back. */ -const hasSpread = computed(() => settings.value.aberration > 0) +const hasSpread = computed(() => shot.aberration > 0) /** Letters need more room than dots do — see `ASCII_MIN_CELL`. */ const minCell = computed(() => - settings.value.stylize === 'ascii' ? ASCII_MIN_CELL : RANGES.stylizeScale.min, + shot.stylize === 'ascii' ? ASCII_MIN_CELL : RANGES.stylizeScale.min, ) /** @@ -261,9 +277,9 @@ const minCell = computed(() => * disagreeing; moving the value is the honest half of enforcing a minimum. */ function setScreen(mode: StylizeMode) { - settings.value.stylize = mode + shot.stylize = mode if (mode === 'ascii') { - settings.value.stylizeScale = Math.max(ASCII_MIN_CELL, settings.value.stylizeScale) + shot.stylizeScale = Math.max(ASCII_MIN_CELL, shot.stylizeScale) } } @@ -522,6 +538,30 @@ const CONTAINERS = [ +
+
+ + {{ selectedLayer ? `Clip shot · ${selectedLayer.name}` : 'Timeline shot' }} + + +
+

+ + +

+
+ -