1.2.9: global seconds/frames toggle + sane number-input widths/rounding
The Timeline ruler's existing s/fr toggle (store.editor.timelineDisplayMode)
now drives every time display and time input across the editor. Mixing
"frames here, seconds there" was inconsistent — pick the unit once, see
it everywhere.
New composable (src/composables/useTimeFormat.ts):
- formatTime(seconds, opts) → "1.42s" or "42 fr" (short variant drops
trailing zeros for ruler-style ticks).
- timeToInputValue / inputValueToSeconds — paired conversions used at
every <input type="number"> bound to a time. Storage stays in seconds
(renderer + .ruw files unchanged); conversion happens at the UI edge.
- inputStep ('0.01' / '1') and unitLabel ('seconds' / 'frames') for
consistent input attributes / labels.
Surfaces switched over:
- App.vue topbar playhead + duration.
- PropertiesPanel keyframe-here label (`@ 1.50s` → `@ 45 fr`).
- FullscreenPreview time label.
- AssetSidebar clip + audio asset durations.
- Timeline audio clip + clip trim tooltips (route through formatKfTime,
which already honoured the toggle).
- DocumentDrawer scene duration input — step + min flip with the mode,
label gets a (seconds)/(frames) suffix.
- ScriptFrameDialog time input — same flip.
- ActionEditor jumpTo time input — same flip. (jumpToFrame stays as
frames since "frame" is its semantic, not a unit choice.)
- Kaleido offset on ClipInstance — now follows the global toggle too,
was hardcoded frames before. The frames-by-default UX comes from
the user defaulting to 'frames' on the ruler; switching to seconds
shows the same offset in seconds without losing precision.
Number-input audit (cleaning up "ewig viele Nachkommastellen"):
- ScriptFrameDialog time: rounded display to ≤3 decimals at the edge.
- ActionEditor jumpTo time: same.
- PropertiesPanel kaleido pivot X/Y (ClipInstance): rounded to int.
- RangeSlider strokeWidth format: snapped to 0.5 increments.
Number-input widths (capping the panel sprawl):
- PropertiesPanel inputs: max-width 96px + tabular-nums; .row inputs
(side-by-side pivot X/Y, transform X/Y) keep flex-share.
- ActionEditor .params input[type='number']: explicit 96px.
- ScriptFrameDialog .row input[type='number']: explicit 96px.
- DocumentDrawer + ExportDialog: already had explicit widths.
Tests (6 new under "time format — global seconds/frames toggle"):
- useTimeFormat composable roundtrips through both modes at fps=30.
- PropertiesPanel keyframe label flips on mode change.
- DocumentDrawer duration input flips step + value (2s ↔ 60fr).
- ActionEditor jumpTo flips step + value (1s ↔ 24fr at fps=24).
- Kaleido offset display switches between modes for the same stored
value (0.5s ↔ 15fr).
- Kaleido offset write-back: typing "30" in frames mode at fps=30
stores exactly 1.0s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
80e1510b53
commit
7d81a50e74
13 files changed
+373
-46
No files matched your search
@@ -21,6 +21,9 @@ import RangeSlider from './RangeSlider.vue'
|
||||
import ActionEditor from './ActionEditor.vue'
|
||||
import FontPicker from './FontPicker.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useTimeFormat } from '@/composables/useTimeFormat'
|
||||
|
||||
const { formatTime, timeToInputValue, inputValueToSeconds, inputStep, unitLabel } = useTimeFormat()
|
||||
|
||||
const SECTION_STORAGE_KEY = 'animatr:propSections'
|
||||
|
||||
@@ -177,16 +180,18 @@ const selectedClipInstance = computed(() => {
|
||||
|
||||
const ciKaleidoOn = computed(() => !!selectedClipInstance.value?.kaleido)
|
||||
const ciKaleidoCount = computed(() => selectedClipInstance.value?.kaleidoCount ?? 6)
|
||||
// Offset is stored as seconds (the renderer's unit) but presented to
|
||||
// the user as frames — that's how animators reason about staggered
|
||||
// playback. Conversion uses the scene's fps.
|
||||
const ciKaleidoOffsetFrames = computed(() => {
|
||||
const sec = selectedClipInstance.value?.kaleidoOffset ?? 0
|
||||
const fps = Math.max(1, store.scene.fps || 30)
|
||||
return Math.round(sec * fps)
|
||||
})
|
||||
const ciPivotX = computed(() => selectedClipInstance.value?.kaleidoPivotX ?? selectedClipInstance.value?.x ?? 0)
|
||||
const ciPivotY = computed(() => selectedClipInstance.value?.kaleidoPivotY ?? selectedClipInstance.value?.y ?? 0)
|
||||
// Offset is stored as seconds (the renderer's unit) but presented in
|
||||
// whichever unit the user picked globally (Timeline ruler toggle) —
|
||||
// frames or seconds. Conversion at the UI boundary via useTimeFormat.
|
||||
const ciKaleidoOffsetInput = computed(() =>
|
||||
timeToInputValue(selectedClipInstance.value?.kaleidoOffset ?? 0),
|
||||
)
|
||||
const ciPivotX = computed(() =>
|
||||
Math.round(selectedClipInstance.value?.kaleidoPivotX ?? selectedClipInstance.value?.x ?? 0),
|
||||
)
|
||||
const ciPivotY = computed(() =>
|
||||
Math.round(selectedClipInstance.value?.kaleidoPivotY ?? selectedClipInstance.value?.y ?? 0),
|
||||
)
|
||||
const ciRotateCopies = computed(() => selectedClipInstance.value?.kaleidoRotateCopies !== false)
|
||||
|
||||
function setCiKaleido(patch: Parameters<typeof store.setClipInstanceKaleido>[1]) {
|
||||
@@ -195,10 +200,8 @@ function setCiKaleido(patch: Parameters<typeof store.setClipInstanceKaleido>[1])
|
||||
store.setClipInstanceKaleido(inst.id, patch)
|
||||
}
|
||||
|
||||
function setCiKaleidoOffsetFrames(frames: number) {
|
||||
if (!Number.isFinite(frames)) return
|
||||
const fps = Math.max(1, store.scene.fps || 30)
|
||||
setCiKaleido({ kaleidoOffset: Math.round(frames) / fps })
|
||||
function setCiKaleidoOffset(value: number) {
|
||||
setCiKaleido({ kaleidoOffset: inputValueToSeconds(value) })
|
||||
}
|
||||
|
||||
const kaleidoOn = computed(() => !!selected.value?.kaleido)
|
||||
@@ -523,7 +526,7 @@ function deleteSelection() {
|
||||
<PhDiamond :size="11" :weight="hasKeyframeHere ? 'fill' : 'regular'" class="kf-dot" />
|
||||
<span class="kf-label">
|
||||
{{ hasKeyframeHere ? t('properties.keyframeLabel') : t('properties.interpolated') }}
|
||||
<span class="kf-time">@ {{ store.playhead.toFixed(2) }}s</span>
|
||||
<span class="kf-time">@ {{ formatTime(store.playhead) }}</span>
|
||||
</span>
|
||||
<button v-if="hasKeyframeHere" class="kf-btn" v-tooltip="t('properties.removeKeyframe')" @click="removeKeyframeHere">
|
||||
<PhX :size="12" weight="bold" />
|
||||
@@ -646,7 +649,7 @@ function deleteSelection() {
|
||||
:min="0"
|
||||
:max="32"
|
||||
:step="0.5"
|
||||
:format="(v: number) => `${v}px`"
|
||||
:format="(v: number) => `${Math.round(v * 2) / 2}px`"
|
||||
/>
|
||||
</label>
|
||||
<label class="slider-label">
|
||||
@@ -859,12 +862,12 @@ function deleteSelection() {
|
||||
/>
|
||||
</label>
|
||||
<label v-if="ciKaleidoOn">
|
||||
{{ t('properties.kaleido.offset') }}
|
||||
{{ t('properties.kaleido.offset') }} ({{ unitLabel }})
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
:value="ciKaleidoOffsetFrames"
|
||||
@change="setCiKaleidoOffsetFrames(Number(($event.target as HTMLInputElement).value))"
|
||||
:step="inputStep"
|
||||
:value="ciKaleidoOffsetInput"
|
||||
@change="setCiKaleidoOffset(Number(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</label>
|
||||
<div v-if="ciKaleidoOn" class="row">
|
||||
@@ -946,6 +949,20 @@ input {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Number inputs hold short numeric values. Capping the width keeps
|
||||
them from stretching to the panel's full ~260px and aligns the
|
||||
panel visually with the more compact slider controls. Inputs
|
||||
inside a `.row` (two side-by-side labels) still split that row
|
||||
evenly because their label parent is `flex: 1 1 0`. */
|
||||
input[type='number'] {
|
||||
max-width: 96px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.row input[type='number'] {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
Reference in new issue
Block a user