Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions apps/lab/app/components/lab/LabLayerProps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -457,6 +462,18 @@ const TEXT_TOGGLES = [
:default="0"
@update:model-value="emit('update', { start: $event })"
/>
<LabNumber
v-if="canChangeLayerSpeed(layer)"
:model-value="layerSpeed(layer)"
label="Speed"
hint="Playback rate. Faster playback shortens the clip without changing its source out-point."
:min="MIN_LAYER_SPEED"
:max="MAX_LAYER_SPEED"
:step="0.05"
unit="×"
:default="1"
@update:model-value="setSpeed"
/>
<LabNumber
:model-value="layer.duration"
label="Length"
Expand Down
182 changes: 111 additions & 71 deletions apps/lab/app/components/lab/LabPanel.vue

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions apps/lab/app/components/lab/LabStage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
* mounting.
*/

import { LAB_STAGE_LAYER } from '~/utils/lab/sequence'
import { LAB_STAGE_LAYER, LAB_STAGE_SPEED } from '~/utils/lab/sequence'

const props = defineProps<{ layerId: string }>()
const props = defineProps<{ layerId: string, speed: number }>()

provide(LAB_STAGE_LAYER, props.layerId)
provide(LAB_STAGE_SPEED, () => props.speed)
</script>

<template>
Expand Down
9 changes: 7 additions & 2 deletions apps/lab/app/components/lab/LabTimeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { effectRampMs } from '~/utils/lab/effects'
import { canJoin, layerEnd } from '~/utils/lab/layers'
import { canChangeLayerSpeed, canJoin, layerEnd, layerSpeed } from '~/utils/lab/layers'
import type { Layer } from '~/utils/lab/layers'

const props = defineProps<{
Expand Down Expand Up @@ -414,7 +414,9 @@ function applyDrag(event: PointerEvent) {
const start = snap(raw - current.offset, layer.id)
updated.start = Math.max(0, Math.min(start, content.value - layer.duration))
} else if (current.grab === 'start') {
const start = Math.max(0, Math.min(snap(raw, layer.id), layerEnd(layer) - MIN_SEGMENT))
const earliest = layer.start - (layer.trim ?? 0) / layerSpeed(layer)
const start = Math.max(0, earliest, Math.min(snap(raw, layer.id), layerEnd(layer) - MIN_SEGMENT))
updated.trim = (layer.trim ?? 0) + (start - layer.start) * layerSpeed(layer)
updated.duration = layerEnd(layer) - start
updated.start = start
} else {
Expand Down Expand Up @@ -815,6 +817,9 @@ const seconds = (ms: number) => `${(ms / 1000).toFixed(2)}s`
name="i-lucide-sparkles"
class="ml-auto size-2.5 shrink-0 opacity-60"
/>
<span v-if="canChangeLayerSpeed(layer) && layerSpeed(layer) !== 1" class="shrink-0 tabular-nums opacity-60">
{{ Number(layerSpeed(layer).toFixed(2)) }}×
</span>
<!-- Only once the clip is wide enough that a number reads as a
number rather than as noise crowding the name. -->
<span
Expand Down
6 changes: 4 additions & 2 deletions apps/lab/app/composables/useTimedSequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@

import { useTimedSequence as sequence } from '../../../docs/app/composables/useTimedSequence'
import type { UseTimedSequenceOptions } from '../../../docs/app/composables/useTimedSequence'
import { LAB_STAGE_LAYER, reportSequenceDuration } from '~/utils/lab/sequence'
import { LAB_STAGE_LAYER, LAB_STAGE_SPEED, reportSequenceDuration, sequenceAtSpeed } from '~/utils/lab/sequence'

export type { TimedEvent, UseTimedSequenceOptions } from '../../../docs/app/composables/useTimedSequence'

export function useTimedSequence(options: UseTimedSequenceOptions) {
// Absent outside a stage — the same component can be mounted with no clip
// behind it, and then there is nothing to report to.
const layerId = inject(LAB_STAGE_LAYER, null)
const speed = inject(LAB_STAGE_SPEED, () => 1)()
const timing = sequenceAtSpeed(options.events, options.totalDuration, speed)
if (layerId) reportSequenceDuration(layerId, options.totalDuration)

return sequence(options)
return sequence({ ...options, ...timing })
}
Loading
Loading