0.8.0: group colour cascade, still-frame past end, project rename, UX polish

Headline:

- Group colour cascades to every non-overridden child (layer or clip
  track). New `colorOverridden` flag on Layer / ClipTrack / ScriptTrack
  / TrackGroup; setTrackColor on a non-group pins the track, clearing
  re-inherits from the parent group. moveTrackIntoGroup also inherits.
  normalizeScene migrates legacy scenes (existing colour ⇒ overridden).
- Keyframes inside a button-asset timeline snap to the 4 state slots
  (normal/hover/pressed/disabled). upsertKeyframe / setKeyframeTime /
  duplicateKeyframe all enforce it; colliding drags refuse the move,
  duplicates walk to the nearest free slot.
- Backspace / Delete on a selected layer removes it (respecting the
  layer's own lock and any enclosing group lock).
- "Still frame" allowed on the last keyframe: the layer stays on stage
  past its final kf. Timeline draws a solid dim tail line from the
  last kf to the end, and single-keyframe tracks now show a full-span
  line so on-stage/off-stage reads at a glance. Height stays 3px.
- Project name moved next to the logo, turns into a rename button on
  hover (pencil fades in). Rename syncs the stored record when the
  project is already persisted.
- Shadow + blur keyframes animate in the preview and exported runtime:
  the runtime's lerpProps used to drop them, so every preview rendered
  shadow=0 between keyframes. RUNTIME_VERSION 11 → 13 (covers the
  button-mask + shadow fixes). Held-last-kf also taught to the runtime.
- Button masks render across every path: edit-mode mask-preview,
  nested buttons on the main stage, sidebar thumbnails, fullscreen
  preview + exports. drawButtonFrame new in render.ts.

Smaller fixes:

- Dragging layer A on the stage with a stale selection on layer B used
  to move B and snap A back. Now the shift-selected branch only runs
  when the dragged layer is itself in the selection.
- Clip-track row was missing its icon — PhFilmStrip was referenced but
  never imported. Added, plus dropped the bespoke `gap` on clip / script
  name rows so their icon+label spacing matches layer rows.
- Clip-art drawer pre-lays-out its 400px grid behind `overflow: hidden`
  so slide-in reveals a finished grid instead of reflowing auto-fill
  columns over 220ms.
- Timeline clip-track dropzone hides until the library has a clip; its
  "drop a clip here" hint stays centred in the visible ruler area.
- Keyframe context menu: check-mark and icons share a single 12px column
  so the ✓ on an easing option lines up with PhPauseCircle / PhCopy /
  PhTrash on the rows below. "Still frame"'s active indicator moved to
  the trailing-check slot.
- Button editor's mask-preview now shows the button's own stage instead
  of leaking through to the main scene. drawButtonFrame drives it.

Regression tests cover all of the above: group-color cascade, pin,
clear-re-inherit, migration, button-kf snap and collision rules,
backspace-deletes-selected-layer (incl. lock guards + editable-target
guard), still-frame-past-end visibility, clipart drawer rigid width,
mask rendering in drawButtonFrame / drawSceneFrame, shadow + blur
interpolation in both canvas and runtime paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-21 21:26:06 +02:00
1 parent ac89f6762d
commit 1e89ea86e0
15 files changed
+849 -74

No files matched your search

+77 -12
View File
@@ -11,6 +11,7 @@ import {
PhFloppyDisk,
PhGearSix,
PhPause,
PhPencilSimpleLine,
PhPlay,
PhSkipBack,
PhSparkle,
@@ -169,6 +170,34 @@ async function saveProject() {
showSavedToast(t('app.savedToast', { name: rec.name }))
}
async function renameProject() {
const suggested =
currentProjectName.value ??
t('app.saveDialog.defaultName', { date: new Date().toLocaleDateString() })
const entered = await promptDialog({
title: t('app.renameProjectDialog.title'),
message: t('app.renameProjectDialog.message'),
defaultValue: suggested,
confirmLabel: t('app.renameProjectDialog.confirm'),
})
if (entered === null) return
const trimmed = entered.trim()
if (!trimmed || trimmed === currentProjectName.value) return
currentProjectName.value = trimmed
// If the project is already persisted in browser storage, sync the
// new name to its record so future loads (and the recents list) use
// it. Unsaved projects just get the in-memory name — the next save
// picks it up.
if (currentProjectId.value) {
const scene = JSON.parse(JSON.stringify(store.scene))
const rec = await saveProjectToStore(scene, {
id: currentProjectId.value,
name: trimmed,
})
currentProjectName.value = rec.name
}
}
function exportProjectFile() {
const scene = JSON.parse(JSON.stringify(store.scene))
const base = currentProjectName.value ?? 'animatr'
@@ -366,6 +395,17 @@ onBeforeUnmount(() => {
<button class="brand" :title="t('about.openTooltip')" @click="aboutOpen = true">
<img :src="ruwioLogo" alt="RUWIO" class="brand-logo" />
</button>
<button
class="project-name-btn"
:class="{ 'is-untitled': !currentProjectName }"
:title="t('app.renameProject')"
@click="renameProject"
>
<span class="project-name-label">
{{ currentProjectName ?? t('app.untitledProject') }}
</span>
<PhPencilSimpleLine :size="12" class="project-name-pencil" />
</button>
</div>
<div class="transport">
<IconButton
@@ -410,9 +450,6 @@ onBeforeUnmount(() => {
<span class="time">{{ store.playhead.toFixed(2) }}s / {{ store.scene.duration }}s</span>
</div>
<div class="topbar-right">
<span v-if="currentProjectName" class="project-name" :title="currentProjectName">
{{ currentProjectName }}
</span>
<button class="icon-btn" :title="t('app.recentProjects')" @click="welcomeOpen = true">
<PhFolderOpen :size="16" />
</button>
@@ -734,19 +771,47 @@ onBeforeUnmount(() => {
background: rgba(110, 168, 255, 0.08);
}
.project-name {
.project-name-btn {
display: inline-flex;
align-items: center;
max-width: 220px;
padding: 4px 10px;
font-size: 11px;
color: var(--text-dim-strong);
background: var(--panel-alt);
border: 1px solid var(--border);
gap: 6px;
max-width: 260px;
padding: 4px 8px;
font: inherit;
font-size: 12px;
color: var(--text);
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
white-space: nowrap;
cursor: pointer;
}
.project-name-btn.is-untitled {
color: var(--text-dim);
font-style: italic;
}
.project-name-btn:hover {
background: var(--panel-alt);
border-color: var(--border);
}
.project-name-label {
overflow: hidden;
text-overflow: ellipsis;
margin-right: 4px;
white-space: nowrap;
min-width: 0;
}
.project-name-pencil {
color: var(--text-dim);
opacity: 0;
transition: opacity 140ms var(--ease);
flex-shrink: 0;
}
.project-name-btn:hover .project-name-pencil,
.project-name-btn:focus-visible .project-name-pencil {
opacity: 1;
}
</style>