+
@@ -279,7 +355,18 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
-
+
@@ -303,7 +390,7 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
.app {
display: grid;
grid-template-columns: 260px 1fr 300px;
- grid-template-rows: 48px 1fr 220px;
+ grid-template-rows: 48px 1fr var(--timeline-height, 220px);
grid-template-areas:
'top top top'
'side stage props'
@@ -480,6 +567,40 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
background: var(--panel);
border-top: 1px solid var(--border);
overflow: hidden;
+ position: relative;
+}
+
+.tl-resize {
+ position: absolute;
+ top: -7px;
+ left: 0;
+ right: 0;
+ height: 14px;
+ cursor: ns-resize;
+ z-index: 20;
+ touch-action: none;
+ background: transparent;
+ transition: background 120ms var(--ease);
+}
+
+.tl-resize::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 7px;
+ height: 1px;
+ background: transparent;
+ transition: background 120ms var(--ease);
+}
+
+.tl-resize:hover::after,
+.tl-resize.resizing::after {
+ background: var(--accent);
+}
+
+.tl-resize.resizing {
+ background: rgba(110, 168, 255, 0.08);
}
.project-name {
diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue
index e9ed6ca..bc719d5 100644
--- a/src/components/Timeline.vue
+++ b/src/components/Timeline.vue
@@ -5,6 +5,8 @@ import {
PhCheck,
PhCopy,
PhDotsSixVertical,
+ PhLock,
+ PhLockOpen,
PhMagnet,
PhPauseCircle,
PhTrash,
@@ -206,7 +208,7 @@ function onLaneDblClick(e: MouseEvent, track: TimelineLayerTrack) {
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
const time = snapTime(ratio * duration.value)
const layer = store.activeLayers.find((l) => l.id === track.layerId)
- if (!layer) return
+ if (!layer || layer.locked) return
const sampled = sampleLayer(layer, time)
const props: Partial = {}
if (track.kind === 'position') {
@@ -247,6 +249,8 @@ function onKeyframeClick(e: MouseEvent, layerId: string, time: number) {
function onKeyframeDblClick(e: MouseEvent, layerId: string, keyframeId: string) {
e.stopPropagation()
if (kfDrag.value?.moved) return
+ const layer = store.activeLayers.find((l) => l.id === layerId)
+ if (layer?.locked) return
store.removeKeyframe(layerId, keyframeId)
}
@@ -325,6 +329,8 @@ const kfDrag = ref(null)
function onKeyframePointerDown(e: PointerEvent, layerId: string, keyframeId: string) {
if (e.button !== 0) return
+ const layer = store.activeLayers.find((l) => l.id === layerId)
+ if (layer?.locked) return
const kfEl = e.currentTarget as HTMLElement
const lane = kfEl.parentElement
if (!lane) return
@@ -482,12 +488,28 @@ function duplicateFromMenu() {
}
interface TrackMenuState {
- layerId: string
+ kind: 'layer' | 'clip'
+ trackId: string
x: number
y: number
separated: boolean
+ currentColor: string | null
+ locked: boolean
}
+const TRACK_COLORS: Array<{ value: string | null; label: string }> = [
+ { value: null, label: 'Keine' },
+ { value: '#ff6b6b', label: 'Rot' },
+ { value: '#ffa94d', label: 'Orange' },
+ { value: '#ffd43b', label: 'Gelb' },
+ { value: '#51cf66', label: 'Grün' },
+ { value: '#4dc4ff', label: 'Türkis' },
+ { value: '#6ea8ff', label: 'Blau' },
+ { value: '#b197fc', label: 'Lila' },
+ { value: '#ff8cc6', label: 'Pink' },
+ { value: '#adb5bd', label: 'Grau' },
+]
+
const dragTrackId = ref(null)
const dropIndex = ref(null)
@@ -537,16 +559,7 @@ function onTrackDragEnd() {
dropIndex.value = null
}
-interface ClipDrag {
- kind: 'new' | 'move'
- clipId: string
- instanceId?: string
- grabOffsetSec: number
-}
-
-const clipDrag = ref(null)
-
-function pixelTimeFromEvent(e: DragEvent, el: HTMLElement): number {
+function pixelTimeFromEvent(e: DragEvent | PointerEvent, el: HTMLElement): number {
const rect = el.getBoundingClientRect()
if (rect.width <= 0) return 0
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
@@ -554,60 +567,76 @@ function pixelTimeFromEvent(e: DragEvent, el: HTMLElement): number {
}
function onClipLaneDragOver(e: DragEvent, _clipTrackId: string) {
- if (!clipDrag.value) {
- if (e.dataTransfer?.types.includes('application/x-animatr-clip')) {
- e.preventDefault()
- if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
- }
- return
+ if (e.dataTransfer?.types.includes('application/x-animatr-clip')) {
+ e.preventDefault()
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}
- e.preventDefault()
- if (e.dataTransfer) e.dataTransfer.dropEffect = clipDrag.value.kind === 'new' ? 'copy' : 'move'
}
function onClipLaneDrop(e: DragEvent, clipTrackId: string) {
e.preventDefault()
+ const track = store.scene.clipTracks?.find((t) => t.id === clipTrackId)
+ if (track?.locked) return
const el = e.currentTarget as HTMLElement
const time = pixelTimeFromEvent(e, el)
- const cd = clipDrag.value
- if (cd) {
- const start = Math.max(0, time - cd.grabOffsetSec)
- if (cd.kind === 'move' && cd.instanceId) {
- store.moveClipInstance(cd.instanceId, start, clipTrackId)
- } else {
- store.addClipInstance(cd.clipId, start, clipTrackId)
- }
- clipDrag.value = null
- return
- }
const clipId = e.dataTransfer?.getData('application/x-animatr-clip')
if (clipId) {
store.addClipInstance(clipId, time, clipTrackId)
}
}
-function onClipBlockDragStart(e: DragEvent, ci: ClipInstance) {
- if (clipTrim.value) {
- e.preventDefault()
- return
- }
- if (!e.dataTransfer) return
+interface ClipMoveState {
+ instanceId: string
+ pointerId: number
+ startX: number
+ initialStartTime: number
+ moved: boolean
+}
+
+const clipMove = ref(null)
+
+function onClipBlockPointerDown(e: PointerEvent, ci: ClipInstance) {
+ if (e.button !== 0) return
+ const track = store.scene.clipTracks?.find((t) => t.id === ci.clipTrackId)
+ if (track?.locked) return
+ // Let handles handle their own pointerdowns
+ const target = e.target as HTMLElement | null
+ if (target?.classList.contains('clip-handle')) return
+ e.preventDefault()
+ e.stopPropagation()
const el = e.currentTarget as HTMLElement
- const rect = el.getBoundingClientRect()
- const visible = visibleDurationOf(ci)
- const grabOffsetSec =
- rect.width > 0 ? ((e.clientX - rect.left) / rect.width) * visible : 0
- clipDrag.value = { kind: 'move', clipId: ci.clipId, instanceId: ci.id, grabOffsetSec }
- e.dataTransfer.effectAllowed = 'move'
- try {
- e.dataTransfer.setData('application/x-animatr-clip-move', ci.id)
- } catch {
- /* ignore */
+ el.setPointerCapture(e.pointerId)
+ clipMove.value = {
+ instanceId: ci.id,
+ pointerId: e.pointerId,
+ startX: e.clientX,
+ initialStartTime: ci.startTime,
+ moved: false,
}
}
-function onClipBlockDragEnd() {
- clipDrag.value = null
+function onClipBlockPointerMove(e: PointerEvent) {
+ const m = clipMove.value
+ if (!m || m.pointerId !== e.pointerId) return
+ const deltaPx = e.clientX - m.startX
+ if (!m.moved && Math.abs(deltaPx) < 2) return
+ m.moved = true
+ const pps = effectivePxPerSec.value
+ if (pps <= 0) return
+ const deltaSec = deltaPx / pps
+ const newStart = Math.max(0, m.initialStartTime + deltaSec)
+ store.moveClipInstance(m.instanceId, newStart)
+}
+
+function onClipBlockPointerUp(e: PointerEvent) {
+ const m = clipMove.value
+ if (!m || m.pointerId !== e.pointerId) return
+ const el = e.currentTarget as HTMLElement
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId)
+ const wasMoved = m.moved
+ clipMove.value = null
+ // suppress click/dblclick if we actually dragged
+ if (wasMoved) e.preventDefault()
}
function addClipTrack() {
@@ -656,6 +685,8 @@ const clipTrim = ref(null)
function onClipHandlePointerDown(e: PointerEvent, ci: ClipInstance, side: 'start' | 'end') {
if (e.button !== 0) return
+ const track = store.scene.clipTracks?.find((t) => t.id === ci.clipTrackId)
+ if (track?.locked) return
e.preventDefault()
e.stopPropagation()
const el = e.currentTarget as HTMLElement
@@ -715,26 +746,66 @@ const trackMenuRef = ref(null)
function onTrackContextMenu(e: MouseEvent, layerId: string) {
e.preventDefault()
e.stopPropagation()
- const layer = store.scene.layers.find((l) => l.id === layerId)
+ const layer = store.activeLayers.find((l) => l.id === layerId)
if (!layer) return
store.selectLayer(layerId)
trackMenu.value = {
- layerId,
+ kind: 'layer',
+ trackId: layerId,
x: e.clientX,
y: e.clientY,
separated: !!layer.separated,
+ currentColor: layer.color ?? null,
+ locked: !!layer.locked,
}
}
+function onClipTrackContextMenu(e: MouseEvent, clipTrackId: string) {
+ e.preventDefault()
+ e.stopPropagation()
+ const track = store.scene.clipTracks?.find((t) => t.id === clipTrackId)
+ if (!track) return
+ trackMenu.value = {
+ kind: 'clip',
+ trackId: clipTrackId,
+ x: e.clientX,
+ y: e.clientY,
+ separated: false,
+ currentColor: track.color ?? null,
+ locked: !!track.locked,
+ }
+}
+
+function toggleLocked() {
+ if (!trackMenu.value) return
+ store.setTrackLocked(trackMenu.value.trackId, !trackMenu.value.locked)
+ trackMenu.value = null
+}
+
+function applyTrackColor(color: string | null) {
+ if (!trackMenu.value) return
+ store.setTrackColor(trackMenu.value.trackId, color)
+ trackMenu.value = null
+}
+
+function trackColorStyle(color: string | null | undefined): Record | undefined {
+ if (!color) return undefined
+ return { '--track-color': color }
+}
+
function toggleSeparated() {
if (!trackMenu.value) return
- store.setLayerSeparated(trackMenu.value.layerId, !trackMenu.value.separated)
+ store.setLayerSeparated(trackMenu.value.trackId, !trackMenu.value.separated)
trackMenu.value = null
}
-function deleteLayer() {
+function deleteTrack() {
if (!trackMenu.value) return
- store.removeLayer(trackMenu.value.layerId)
+ if (trackMenu.value.kind === 'layer') {
+ store.removeLayer(trackMenu.value.trackId)
+ } else {
+ store.removeClipTrack(trackMenu.value.trackId)
+ }
trackMenu.value = null
}
@@ -876,7 +947,10 @@ onBeforeUnmount(() => {
'drop-before': isDropBefore(track),
'drop-after': isDropAfter(track, tracks[idx + 1]),
dragging: track.isParent && dragTrackId === track.layerId,
+ colored: !!track.layer.color,
+ locked: !!track.layer.locked,
}"
+ :style="trackColorStyle(track.layer.color)"
@dragover="track.isParent ? onTrackDragOver($event, track.orderIndex) : null"
@drop="track.isParent ? onTrackDrop($event) : null"
@dragleave="onTrackDragLeave"
@@ -895,6 +969,11 @@ onBeforeUnmount(() => {
@dragend="onTrackDragEnd"
>
+
{
'drop-before': isDropBefore(track),
'drop-after': isDropAfter(track, tracks[idx + 1]),
dragging: dragTrackId === track.clipTrackId,
+ colored: !!clipTrackById.get(track.clipTrackId)?.color,
+ locked: !!clipTrackById.get(track.clipTrackId)?.locked,
}"
+ :style="trackColorStyle(clipTrackById.get(track.clipTrackId)?.color)"
@dragover="onTrackDragOver($event, track.orderIndex)"
@drop="onTrackDrop($event)"
@dragleave="onTrackDragLeave"
@@ -955,10 +1037,16 @@ onBeforeUnmount(() => {
+
{{ track.name }}
@@ -976,12 +1064,15 @@ onBeforeUnmount(() => {
'trimmed-start': (ci.trimStart ?? 0) > 0,
'trimmed-end': (ci.trimEnd ?? 0) > 0,
trimming: clipTrim?.instanceId === ci.id,
+ moving: clipMove?.instanceId === ci.id,
}"
:style="clipBlockStyle(ci)"
:title="trimTooltip(ci)"
- draggable="true"
- @dragstart.stop="onClipBlockDragStart($event, ci)"
- @dragend="onClipBlockDragEnd"
+ @pointerdown="onClipBlockPointerDown($event, ci)"
+ @pointermove="onClipBlockPointerMove"
+ @pointerup="onClipBlockPointerUp"
+ @pointercancel="onClipBlockPointerUp"
+ @dragstart.prevent.stop
@dblclick="onClipBlockDblClick(ci)"
@contextmenu="onClipBlockContextMenu($event, ci)"
>
@@ -1084,16 +1175,51 @@ onBeforeUnmount(() => {
class="kf-menu"
:style="{ left: trackMenu.x + 'px', top: trackMenu.y + 'px' }"
>
-
+
-
@@ -1324,6 +1450,51 @@ onBeforeUnmount(() => {
opacity: 0.75;
}
+.track.locked {
+ height: 15px;
+}
+
+.track.clip-track.locked {
+ height: 17px;
+}
+
+.track.sub-track.locked {
+ height: 13px;
+}
+
+.track.locked .track-name {
+ font-size: 10px;
+}
+
+.track.locked .track-lane {
+ background-image: repeating-linear-gradient(
+ 45deg,
+ rgba(255, 255, 255, 0.04) 0,
+ rgba(255, 255, 255, 0.04) 4px,
+ transparent 4px,
+ transparent 8px
+ );
+ cursor: not-allowed;
+}
+
+.track.locked .kf,
+.track.locked .kf-seg,
+.track.locked .clip-block {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
+.track.locked .drag-handle {
+ display: none;
+}
+
+.lock-badge {
+ color: var(--text-dim);
+ margin-right: 4px;
+ flex-shrink: 0;
+ opacity: 0.85;
+}
+
.track-name {
position: sticky;
left: 0;
@@ -1357,6 +1528,10 @@ onBeforeUnmount(() => {
var(--panel);
}
+.track.colored > .track-name {
+ box-shadow: inset 3px 0 0 0 var(--track-color);
+}
+
.track.sub-track .track-name {
padding-left: 24px;
font-size: 11px;
@@ -1441,20 +1616,20 @@ onBeforeUnmount(() => {
margin-top: -1.5px;
border-radius: 999px;
pointer-events: none;
- background: var(--accent);
+ background: var(--track-color, var(--accent));
opacity: 0.55;
}
.kf-seg.easing-linear {
- background: var(--accent);
+ background: var(--track-color, var(--accent));
}
.kf-seg.easing-easeIn {
background: linear-gradient(
90deg,
- rgba(110, 168, 255, 0.15) 0%,
- rgba(110, 168, 255, 0.25) 30%,
- rgba(110, 168, 255, 1) 100%
+ color-mix(in srgb, var(--track-color, var(--accent)) 15%, transparent) 0%,
+ color-mix(in srgb, var(--track-color, var(--accent)) 25%, transparent) 30%,
+ var(--track-color, var(--accent)) 100%
);
opacity: 0.9;
}
@@ -1462,9 +1637,9 @@ onBeforeUnmount(() => {
.kf-seg.easing-easeOut {
background: linear-gradient(
90deg,
- rgba(110, 168, 255, 1) 0%,
- rgba(110, 168, 255, 0.25) 70%,
- rgba(110, 168, 255, 0.15) 100%
+ var(--track-color, var(--accent)) 0%,
+ color-mix(in srgb, var(--track-color, var(--accent)) 25%, transparent) 70%,
+ color-mix(in srgb, var(--track-color, var(--accent)) 15%, transparent) 100%
);
opacity: 0.9;
}
@@ -1472,9 +1647,9 @@ onBeforeUnmount(() => {
.kf-seg.easing-easeInOut {
background: linear-gradient(
90deg,
- rgba(110, 168, 255, 0.15) 0%,
- rgba(110, 168, 255, 1) 50%,
- rgba(110, 168, 255, 0.15) 100%
+ color-mix(in srgb, var(--track-color, var(--accent)) 15%, transparent) 0%,
+ var(--track-color, var(--accent)) 50%,
+ color-mix(in srgb, var(--track-color, var(--accent)) 15%, transparent) 100%
);
opacity: 0.95;
}
@@ -1497,7 +1672,7 @@ onBeforeUnmount(() => {
height: 11px;
margin-left: -5.5px;
margin-top: -5.5px;
- background: var(--accent);
+ background: var(--track-color, var(--accent));
transform: rotate(45deg);
border-radius: 2px;
cursor: grab;
@@ -1524,7 +1699,7 @@ onBeforeUnmount(() => {
transform: none;
border-radius: 50%;
background: var(--panel);
- border: 2px solid var(--accent);
+ border: 2px solid var(--track-color, var(--accent));
box-shadow: none;
}
@@ -1574,10 +1749,10 @@ onBeforeUnmount(() => {
align-items: center;
gap: 4px;
padding: 0 8px;
- background: var(--accent-soft);
- border: 1px solid var(--accent);
+ background: color-mix(in srgb, var(--track-color, var(--accent)) 14%, transparent);
+ border: 1px solid var(--track-color, var(--accent));
border-radius: var(--radius-sm);
- color: var(--accent);
+ color: var(--track-color, var(--accent));
font-size: 11px;
font-weight: 500;
white-space: nowrap;
@@ -1589,7 +1764,7 @@ onBeforeUnmount(() => {
}
.clip-block:hover {
- background: var(--accent-soft-strong);
+ background: color-mix(in srgb, var(--track-color, var(--accent)) 22%, transparent);
}
.clip-block:active {
@@ -1647,7 +1822,7 @@ onBeforeUnmount(() => {
bottom: 20%;
width: 2px;
border-radius: 2px;
- background: var(--accent);
+ background: var(--track-color, var(--accent));
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
}
@@ -1682,6 +1857,12 @@ onBeforeUnmount(() => {
box-shadow: 0 0 0 1px #ffc857;
}
+.clip-block.moving {
+ box-shadow: 0 0 0 1px var(--track-color, var(--accent)), 0 4px 12px rgba(0, 0, 0, 0.3);
+ cursor: grabbing;
+ z-index: 4;
+}
+
.clip-handle:hover,
.clip-handle:active {
background: rgba(255, 255, 255, 0.25) !important;
@@ -1792,4 +1973,45 @@ onBeforeUnmount(() => {
background: var(--border);
margin: 4px 0;
}
+
+.color-tiles {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 4px;
+ padding: 4px 10px 6px;
+}
+
+.color-tile {
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #000;
+ cursor: pointer;
+ transition: transform 100ms var(--ease), box-shadow 100ms var(--ease);
+}
+
+.color-tile:hover {
+ transform: scale(1.1);
+ border-color: var(--text-dim);
+}
+
+.color-tile.selected {
+ border-color: var(--text);
+ box-shadow: 0 0 0 2px var(--panel), 0 0 0 3px var(--accent);
+}
+
+.color-tile.none {
+ background:
+ linear-gradient(45deg, transparent calc(50% - 1px), var(--danger) calc(50% - 1px), var(--danger) calc(50% + 1px), transparent calc(50% + 1px)),
+ var(--panel-alt);
+}
+
+.color-tile.none.selected {
+ box-shadow: 0 0 0 2px var(--panel), 0 0 0 3px var(--accent);
+}
diff --git a/src/devtools/tests.ts b/src/devtools/tests.ts
index f31100b..93d140a 100644
--- a/src/devtools/tests.ts
+++ b/src/devtools/tests.ts
@@ -1114,6 +1114,67 @@ export function registerAllTests(): void {
expect(ci.trimEnd).toBe(0)
})
+ test('setTrackColor applies to layer and can be cleared with null', () => {
+ const s = makeFreshStore()
+ const a = s.addAsset({ kind: 'rect', name: 'Col', width: 20, height: 20, fill: '#123' })
+ const l = s.addLayer(a.id)
+ s.setTrackColor(l.id, '#ff6b6b')
+ expect(l.color).toBe('#ff6b6b')
+ s.setTrackColor(l.id, null)
+ expect(l.color).toBe(undefined)
+ })
+
+ test('setTrackColor applies to clip track', () => {
+ const s = makeFreshStore()
+ s.addClip('X')
+ const t = s.scene.clipTracks![0]
+ s.setTrackColor(t.id, '#51cf66')
+ expect(t.color).toBe('#51cf66')
+ s.setTrackColor(t.id, null)
+ expect(t.color).toBe(undefined)
+ })
+
+ test('setTrackLocked toggles layer lock flag', () => {
+ const s = makeFreshStore()
+ const a = s.addAsset({ kind: 'rect', name: 'L', width: 10, height: 10, fill: '#111' })
+ const l = s.addLayer(a.id)
+ s.setTrackLocked(l.id, true)
+ expect(l.locked).toBe(true)
+ s.setTrackLocked(l.id, false)
+ expect(l.locked).toBe(undefined)
+ })
+
+ test('setTrackLocked toggles clip-track lock flag', () => {
+ const s = makeFreshStore()
+ s.addClip('L')
+ const t = s.scene.clipTracks![0]
+ s.setTrackLocked(t.id, true)
+ expect(t.locked).toBe(true)
+ s.setTrackLocked(t.id, false)
+ expect(t.locked).toBe(undefined)
+ })
+
+ test('setTrackColor on unknown id is a no-op', () => {
+ const s = makeFreshStore()
+ s.setTrackColor('nope', '#ff0000')
+ // just verify no throw, no layer/clipTrack changed
+ expect(s.scene.layers).toHaveLength(0)
+ expect(s.scene.clipTracks ?? []).toHaveLength(0)
+ })
+
+ test('moveClipInstance without clipTrackId keeps the same track (x-only move)', () => {
+ const s = makeFreshStore()
+ const c = s.addClip('MoveX')
+ const t1 = s.scene.clipTracks![0]
+ const t2 = s.addClipTrack()
+ const ci = s.addClipInstance(c.id, 0, t1.id)!
+ // sanity: distinct tracks
+ expect(t1.id === t2.id).toBe(false)
+ s.moveClipInstance(ci.id, 5)
+ expect(ci.startTime).toBe(5)
+ expect(ci.clipTrackId).toBe(t1.id)
+ })
+
test('moveTrack reorders the unified stack (layer above clip track)', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'M', width: 10, height: 10, fill: '#000' })
diff --git a/src/stores/scene.ts b/src/stores/scene.ts
index 9bfed3f..2abaed8 100644
--- a/src/stores/scene.ts
+++ b/src/stores/scene.ts
@@ -396,6 +396,32 @@ export const useSceneStore = defineStore('scene', {
sortKfs(arr)
return newKf.id
},
+ setTrackColor(trackId: string, color: string | null) {
+ const layer = this.activeLayers.find((l) => l.id === trackId)
+ if (layer) {
+ if (color === null) delete layer.color
+ else layer.color = color
+ return
+ }
+ const clipTrack = this.scene.clipTracks?.find((t) => t.id === trackId)
+ if (clipTrack) {
+ if (color === null) delete clipTrack.color
+ else clipTrack.color = color
+ }
+ },
+ setTrackLocked(trackId: string, locked: boolean) {
+ const layer = this.activeLayers.find((l) => l.id === trackId)
+ if (layer) {
+ if (locked) layer.locked = true
+ else delete layer.locked
+ return
+ }
+ const clipTrack = this.scene.clipTracks?.find((t) => t.id === trackId)
+ if (clipTrack) {
+ if (locked) clipTrack.locked = true
+ else delete clipTrack.locked
+ }
+ },
setLayerSeparated(layerId: string, separated: boolean) {
const layer = this.activeLayers.find((l) => l.id === layerId)
if (!layer) return
diff --git a/src/types/scene.ts b/src/types/scene.ts
index 9e14540..38e1bf4 100644
--- a/src/types/scene.ts
+++ b/src/types/scene.ts
@@ -69,6 +69,8 @@ export interface Layer {
rotationKeyframes?: Keyframe[]
offsetX?: number
offsetY?: number
+ color?: string | null
+ locked?: boolean
}
export interface Clip {
@@ -81,6 +83,8 @@ export interface Clip {
export interface ClipTrack {
id: string
name?: string
+ color?: string | null
+ locked?: boolean
}
export interface ClipInstance {
diff --git a/todo b/todo
index f0c4657..43a085a 100644
--- a/todo
+++ b/todo
@@ -40,8 +40,16 @@ für user-interaktionen (wie neues text-objekt) brauchen wir eigene popups. kein
den zurück pfeil bitte als phosphor icon machen
-
-
spuren muss man einfärben können. wir bieten die grundfarben als color tiles bei rechtsklick auf die spur an gilt auch für clipspuren
-clips draggen darf nur auf der x-achse gehen. y verschiebung darf nicht möglich sein - das ist irreführend
\ No newline at end of file
+clips draggen darf nur auf der x-achse gehen. y verschiebung darf nicht möglich sein - das ist irreführend
+
+das timeline panel muss man per anfasser höher ziehen können
+
+spuren muss man locken können. sie dürfen dann nur noch die halbe höhe haben und an der zeitlinie muss erkennbar sein das nicht bearbeitet werden kann
+
+
+ALLE texte der gui bitte in englisch
+
+
+per box select muss man über mehrere spuren keyframe multiselection durchführen können um sie zusammen in der zeit zu verschieben