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:
1 parent
ac89f6762d
commit
1e89ea86e0
15 files changed
+849
-74
No files matched your search
+77
-12
@@ -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>
|
||||
Reference in new issue
Block a user