revert macro-clips button feature (keep markers + scripts)
The button authoring ended up too far from the intended model after three correction passes. Pulling it out entirely so we can rebuild from a clean spec. Markers, script tracks, script frames, the Action editor, the Script DSL and the runtime for all of the above stay — they work. Removed: - types: ButtonState, StateTransition, ButtonHitArea, ButtonClip, ButtonInstance, setButtonState/setButtonDisabled Action variants, Scene.buttonClips/buttonInstances - store: activeButtonClip* getters, all add/remove/setButton* actions, seedDefaultButtonClip, enterButtonEdit/setActiveButtonState/ exitButtonEdit, button-label resolution in actionRuntime - useButtonRuntime composable, buttonBlend helper, ButtonInstanceDialog, sidebar Buttons section, Timeline button rendering + pointer handlers, CanvasStage button rendering + pointer state machine, exportPlayer button runtime + hit-testing - DSL commands state/enable/disable and their parse errors - 35 button-related tests + helpers; stale testing doc; stale design doc Re-applied three uncommitted fixes that were unrelated to the button work: - clicking a marker no longer jumps the playhead (only drag / dblclick act on it) - script tracks get a right-click context menu (rename + delete, color in the existing palette) - onTracksPointerDown excludes .script-lane / .script-frame so clicks on empty lanes or on frame dots reach the frame-open path instead of being swallowed by box-select pointer capture Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
616973b0fa
commit
31604052ca
16 files changed
+84
-2809
No files matched your search
@@ -8,7 +8,7 @@ import {
|
||||
} from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { t } from '@/i18n'
|
||||
import type { Action, ButtonState } from '@/types/scene'
|
||||
import type { Action } from '@/types/scene'
|
||||
|
||||
const props = defineProps<{ actions: Action[] }>()
|
||||
const emit = defineEmits<{ 'update:actions': [value: Action[]] }>()
|
||||
@@ -19,19 +19,6 @@ const markerNames = computed(() =>
|
||||
(store.scene.markers ?? []).map((m) => m.name),
|
||||
)
|
||||
|
||||
interface ButtonInstanceOption {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const buttonInstanceOptions = computed<ButtonInstanceOption[]>(() =>
|
||||
(store.scene.buttonInstances ?? [])
|
||||
.filter((bi) => !!bi.label)
|
||||
.map((bi) => ({ id: bi.id, label: bi.label! })),
|
||||
)
|
||||
|
||||
const BUTTON_STATES: ButtonState[] = ['normal', 'hover', 'pressed', 'disabled']
|
||||
|
||||
const ACTION_TYPES: Action['type'][] = [
|
||||
'play',
|
||||
'pause',
|
||||
@@ -39,8 +26,6 @@ const ACTION_TYPES: Action['type'][] = [
|
||||
'jumpTo',
|
||||
'jumpToFrame',
|
||||
'jumpToMarker',
|
||||
'setButtonState',
|
||||
'setButtonDisabled',
|
||||
'script',
|
||||
]
|
||||
|
||||
@@ -60,18 +45,6 @@ function makeDefault(type: Action['type']): Action {
|
||||
return { type: 'jumpToFrame', frame: 0 }
|
||||
case 'jumpToMarker':
|
||||
return { type: 'jumpToMarker', name: markerNames.value[0] ?? '' }
|
||||
case 'setButtonState':
|
||||
return {
|
||||
type: 'setButtonState',
|
||||
instanceId: buttonInstanceOptions.value[0]?.id ?? '',
|
||||
state: 'normal',
|
||||
}
|
||||
case 'setButtonDisabled':
|
||||
return {
|
||||
type: 'setButtonDisabled',
|
||||
instanceId: buttonInstanceOptions.value[0]?.id ?? '',
|
||||
disabled: true,
|
||||
}
|
||||
case 'script':
|
||||
return { type: 'script', source: '' }
|
||||
}
|
||||
@@ -179,47 +152,6 @@ function onTypeChange(index: number, newType: Action['type']) {
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else-if="a.type === 'setButtonState'" class="params">
|
||||
<label>
|
||||
{{ t('actions.param.button') }}
|
||||
<select
|
||||
:value="a.instanceId"
|
||||
@change="setAt(i, { type: 'setButtonState', instanceId: ($event.target as HTMLSelectElement).value, state: a.state })"
|
||||
>
|
||||
<option v-if="!buttonInstanceOptions.length" value="" disabled>{{ t('actions.noButtons') }}</option>
|
||||
<option v-for="b in buttonInstanceOptions" :key="b.id" :value="b.id">{{ b.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{{ t('actions.param.state') }}
|
||||
<select
|
||||
:value="a.state"
|
||||
@change="setAt(i, { type: 'setButtonState', instanceId: a.instanceId, state: ($event.target as HTMLSelectElement).value as ButtonState })"
|
||||
>
|
||||
<option v-for="s in BUTTON_STATES" :key="s" :value="s">{{ s }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else-if="a.type === 'setButtonDisabled'" class="params">
|
||||
<label>
|
||||
{{ t('actions.param.button') }}
|
||||
<select
|
||||
:value="a.instanceId"
|
||||
@change="setAt(i, { type: 'setButtonDisabled', instanceId: ($event.target as HTMLSelectElement).value, disabled: a.disabled })"
|
||||
>
|
||||
<option v-if="!buttonInstanceOptions.length" value="" disabled>{{ t('actions.noButtons') }}</option>
|
||||
<option v-for="b in buttonInstanceOptions" :key="b.id" :value="b.id">{{ b.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="bool">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="a.disabled"
|
||||
@change="setAt(i, { type: 'setButtonDisabled', instanceId: a.instanceId, disabled: ($event.target as HTMLInputElement).checked })"
|
||||
/>
|
||||
{{ t('actions.param.disabled') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-else-if="a.type === 'script'" class="params">
|
||||
<label class="block">
|
||||
{{ t('actions.param.source') }}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
PhCircle,
|
||||
PhCursorClick,
|
||||
PhFilmStrip,
|
||||
PhImage,
|
||||
PhImageSquare,
|
||||
@@ -108,7 +107,6 @@ async function addSampleText() {
|
||||
}
|
||||
|
||||
const clips = computed(() => store.scene.clips ?? [])
|
||||
const buttonClips = computed(() => store.scene.buttonClips ?? [])
|
||||
|
||||
async function addClip() {
|
||||
const input = await promptDialog({
|
||||
@@ -149,45 +147,6 @@ function onClipDragStart(e: DragEvent, clipId: string) {
|
||||
e.dataTransfer.setData('text/plain', clipId)
|
||||
}
|
||||
|
||||
async function addButtonClip() {
|
||||
const input = await promptDialog({
|
||||
title: t('sidebar.newButtonDialog.title'),
|
||||
message: t('sidebar.newButtonDialog.message'),
|
||||
defaultValue: t('sidebar.newButtonDialog.default'),
|
||||
confirmLabel: t('sidebar.newButtonDialog.confirm'),
|
||||
})
|
||||
if (input === null) return
|
||||
store.seedDefaultButtonClip(input || t('sidebar.newButtonDialog.default'))
|
||||
}
|
||||
|
||||
async function renameButtonClipPrompt(id: string, current: string) {
|
||||
const input = await promptDialog({
|
||||
title: t('sidebar.renameButtonDialog.title'),
|
||||
defaultValue: current,
|
||||
confirmLabel: t('sidebar.renameButtonDialog.confirm'),
|
||||
})
|
||||
if (input === null) return
|
||||
store.setButtonClipName(id, input)
|
||||
}
|
||||
|
||||
async function deleteButtonClip(id: string) {
|
||||
const ok = await confirmDialog({
|
||||
title: t('sidebar.deleteButtonDialog.title'),
|
||||
message: t('sidebar.deleteButtonDialog.message'),
|
||||
confirmLabel: t('sidebar.deleteButtonDialog.confirm'),
|
||||
variant: 'danger',
|
||||
})
|
||||
if (!ok) return
|
||||
store.removeButtonClip(id)
|
||||
}
|
||||
|
||||
function onButtonClipDragStart(e: DragEvent, id: string) {
|
||||
if (!e.dataTransfer) return
|
||||
e.dataTransfer.effectAllowed = 'copy'
|
||||
e.dataTransfer.setData('application/x-animatr-button-clip', id)
|
||||
e.dataTransfer.setData('text/plain', id)
|
||||
}
|
||||
|
||||
async function handleFileSelected(event: Event) {
|
||||
uploadError.value = null
|
||||
const input = event.target as HTMLInputElement
|
||||
@@ -377,51 +336,6 @@ function onAssetDragStart(e: DragEvent, asset: Extract<Asset, { kind: 'image' }>
|
||||
</li>
|
||||
<li v-if="!clips.length" class="empty">{{ t('sidebar.noClips') }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="lib-header clips-header">
|
||||
<h4>{{ t('sidebar.buttons') }}</h4>
|
||||
<button
|
||||
class="thumb-toggle"
|
||||
:title="t('sidebar.createButtonTooltip')"
|
||||
@click="addButtonClip"
|
||||
>
|
||||
<PhPlus :size="12" weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
<ul class="clips">
|
||||
<li
|
||||
v-for="bc in buttonClips"
|
||||
:key="bc.id"
|
||||
class="clip button-clip"
|
||||
draggable="true"
|
||||
@dragstart="onButtonClipDragStart($event, bc.id)"
|
||||
>
|
||||
<div class="clip-icon">
|
||||
<PhCursorClick :size="16" />
|
||||
</div>
|
||||
<div class="clip-meta">
|
||||
<span class="clip-name" :title="bc.name">{{ bc.name }}</span>
|
||||
<span class="clip-sub">{{ t('sidebar.buttonSubtitle') }}</span>
|
||||
</div>
|
||||
<div class="clip-actions">
|
||||
<button
|
||||
class="clip-btn"
|
||||
:title="t('sidebar.renameTooltip')"
|
||||
@click.stop="renameButtonClipPrompt(bc.id, bc.name)"
|
||||
>
|
||||
<PhPencilSimple :size="11" />
|
||||
</button>
|
||||
<button
|
||||
class="clip-btn danger"
|
||||
:title="t('sidebar.deleteTooltip')"
|
||||
@click.stop="deleteButtonClip(bc.id)"
|
||||
>
|
||||
<PhTrash :size="11" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!buttonClips.length" class="empty">{{ t('sidebar.noButtons') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { PhPlus, PhTrash } from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { t } from '@/i18n'
|
||||
import { Modal } from '@/components/ui'
|
||||
import ActionEditor from './ActionEditor.vue'
|
||||
import type { Action, ButtonState, Easing, StateTransition } from '@/types/scene'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
instanceId: string | null
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
const store = useSceneStore()
|
||||
|
||||
const instance = computed(() => {
|
||||
if (!props.instanceId) return null
|
||||
return (
|
||||
store.scene.buttonInstances?.find((bi) => bi.id === props.instanceId) ??
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const buttonClip = computed(() => {
|
||||
const bi = instance.value
|
||||
if (!bi) return null
|
||||
return store.scene.buttonClips?.find((c) => c.id === bi.buttonClipId) ?? null
|
||||
})
|
||||
|
||||
const localLabel = ref('')
|
||||
const localDisabled = ref(false)
|
||||
const localActions = ref<Action[]>([])
|
||||
const localTransitions = ref<StateTransition[]>([])
|
||||
const labelError = ref<string | null>(null)
|
||||
|
||||
const BUTTON_STATES: ButtonState[] = ['normal', 'hover', 'pressed', 'disabled']
|
||||
const EASINGS: Easing[] = ['linear', 'easeIn', 'easeOut', 'easeInOut']
|
||||
|
||||
watch(
|
||||
[() => props.modelValue, () => props.instanceId],
|
||||
([open, iid]) => {
|
||||
if (!open || !iid) return
|
||||
const bi = store.scene.buttonInstances?.find((x) => x.id === iid)
|
||||
if (!bi) return
|
||||
localLabel.value = bi.label ?? ''
|
||||
localDisabled.value = !!bi.disabled
|
||||
localActions.value = (bi.onClick ?? []).map((a) => ({ ...a }))
|
||||
const clip = store.scene.buttonClips?.find((c) => c.id === bi.buttonClipId)
|
||||
localTransitions.value = (clip?.transitions ?? []).map((t) => ({ ...t }))
|
||||
labelError.value = null
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function commit(): boolean {
|
||||
if (!props.instanceId) return true
|
||||
// Label uniqueness.
|
||||
const label = localLabel.value.trim()
|
||||
const ok = store.setButtonInstanceLabel(props.instanceId, label)
|
||||
if (!ok) {
|
||||
labelError.value = t('buttonInstanceDialog.labelCollision', { name: label })
|
||||
return false
|
||||
}
|
||||
store.setButtonInstanceOnClick(props.instanceId, localActions.value)
|
||||
store.setButtonInstanceDisabled(props.instanceId, localDisabled.value)
|
||||
const bi = store.scene.buttonInstances?.find((x) => x.id === props.instanceId)
|
||||
if (bi) store.setButtonClipTransitions(bi.buttonClipId, localTransitions.value)
|
||||
return true
|
||||
}
|
||||
|
||||
function addTransition() {
|
||||
localTransitions.value = [
|
||||
...localTransitions.value,
|
||||
{ from: 'normal', to: 'hover', duration: 0.15, easing: 'easeOut' },
|
||||
]
|
||||
}
|
||||
|
||||
function removeTransition(index: number) {
|
||||
const next = [...localTransitions.value]
|
||||
next.splice(index, 1)
|
||||
localTransitions.value = next
|
||||
}
|
||||
|
||||
function updateTransition(index: number, patch: Partial<StateTransition>) {
|
||||
const next = [...localTransitions.value]
|
||||
next[index] = { ...next[index], ...patch }
|
||||
localTransitions.value = next
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (commit()) emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function removeInstance() {
|
||||
if (!props.instanceId) return
|
||||
store.removeButtonInstance(props.instanceId)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:model-value="modelValue"
|
||||
size="md"
|
||||
:title="t('buttonInstanceDialog.title')"
|
||||
@update:model-value="(v) => (v ? null : close())"
|
||||
>
|
||||
<div v-if="instance" class="body">
|
||||
<div class="meta">
|
||||
<span class="meta-label">{{ t('buttonInstanceDialog.clipName') }}</span>
|
||||
<span class="meta-value">{{ buttonClip?.name ?? '?' }}</span>
|
||||
</div>
|
||||
<label class="field">
|
||||
{{ t('buttonInstanceDialog.label') }}
|
||||
<input
|
||||
type="text"
|
||||
:value="localLabel"
|
||||
:placeholder="t('buttonInstanceDialog.labelPlaceholder')"
|
||||
@input="localLabel = ($event.target as HTMLInputElement).value; labelError = null"
|
||||
/>
|
||||
<span v-if="labelError" class="err">{{ labelError }}</span>
|
||||
</label>
|
||||
<label class="field row">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="localDisabled"
|
||||
@change="localDisabled = ($event.target as HTMLInputElement).checked"
|
||||
/>
|
||||
{{ t('buttonInstanceDialog.disabled') }}
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">{{ t('buttonInstanceDialog.onClick') }}</span>
|
||||
<ActionEditor
|
||||
:actions="localActions"
|
||||
@update:actions="localActions = $event"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">
|
||||
{{ t('buttonInstanceDialog.transitions') }}
|
||||
<small class="field-hint">{{ t('buttonInstanceDialog.transitionsHint') }}</small>
|
||||
</span>
|
||||
<ol v-if="localTransitions.length" class="trans-list">
|
||||
<li v-for="(tr, i) in localTransitions" :key="i" class="trans-row">
|
||||
<select
|
||||
:value="tr.from"
|
||||
@change="updateTransition(i, { from: ($event.target as HTMLSelectElement).value as ButtonState })"
|
||||
>
|
||||
<option v-for="s in BUTTON_STATES" :key="s" :value="s">{{ s }}</option>
|
||||
</select>
|
||||
<span class="arrow">→</span>
|
||||
<select
|
||||
:value="tr.to"
|
||||
@change="updateTransition(i, { to: ($event.target as HTMLSelectElement).value as ButtonState })"
|
||||
>
|
||||
<option v-for="s in BUTTON_STATES" :key="s" :value="s">{{ s }}</option>
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
:value="tr.duration"
|
||||
@input="updateTransition(i, { duration: Number(($event.target as HTMLInputElement).value) })"
|
||||
class="dur"
|
||||
/>
|
||||
<select
|
||||
:value="tr.easing"
|
||||
@change="updateTransition(i, { easing: ($event.target as HTMLSelectElement).value as Easing })"
|
||||
>
|
||||
<option v-for="e in EASINGS" :key="e" :value="e">{{ e }}</option>
|
||||
</select>
|
||||
<button class="icon-btn danger" :title="t('actions.remove')" @click="removeTransition(i)">
|
||||
<PhTrash :size="13" />
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
<button class="add-btn" @click="addTransition">
|
||||
<PhPlus :size="12" weight="bold" />
|
||||
{{ t('buttonInstanceDialog.addTransition') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button class="btn danger" @click="removeInstance">
|
||||
<PhTrash :size="12" />
|
||||
{{ t('buttonInstanceDialog.remove') }}
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn primary" @click="close">
|
||||
{{ t('buttonInstanceDialog.done') }}
|
||||
</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.field.row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.field input[type='text'] {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 4px 7px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.err {
|
||||
color: var(--danger);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 10px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--panel-hover);
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
margin-left: 6px;
|
||||
color: var(--text-dim);
|
||||
font-weight: normal;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.trans-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 6px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.trans-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.trans-row select,
|
||||
.trans-row input.dur {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 3px 5px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.trans-row input.dur {
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.trans-row .arrow {
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-btn.danger:hover {
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 9px;
|
||||
background: var(--panel);
|
||||
border: 1px dashed var(--border-strong);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
align-self: flex-start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background: var(--panel-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -8,22 +8,12 @@ import { importImageFile, loadImage } from '@/lib/fileLoader'
|
||||
import { useClipartStore } from '@/stores/clipart'
|
||||
import { drawSceneFrame, loadSceneImages } from '@/lib/render'
|
||||
import type {
|
||||
Action,
|
||||
AnimatableProps,
|
||||
Asset,
|
||||
ButtonClip,
|
||||
ButtonInstance,
|
||||
ButtonState,
|
||||
Keyframe,
|
||||
Layer,
|
||||
Point,
|
||||
} from '@/types/scene'
|
||||
import {
|
||||
applyButtonStateChange,
|
||||
useButtonRuntime,
|
||||
} from '@/composables/useButtonRuntime'
|
||||
import { evalAction } from '@/lib/actions'
|
||||
import { blendButtonStates } from '@/lib/buttonBlend'
|
||||
import { t } from '@/i18n'
|
||||
import { useKfSelection } from '@/composables/useKfSelection'
|
||||
import { showLocked } from '@/composables/useShowLocked'
|
||||
@@ -197,167 +187,6 @@ const canvasBorderConfig = computed(() => ({
|
||||
|
||||
const zoomPercent = computed(() => Math.round(zoom.value * 100))
|
||||
|
||||
const buttonRt = useButtonRuntime()
|
||||
|
||||
interface ButtonRenderItem {
|
||||
bi: ButtonInstance
|
||||
clip: ButtonClip
|
||||
state: ButtonState
|
||||
disabled: boolean
|
||||
layers: Array<{
|
||||
layer: Layer
|
||||
asset: Asset
|
||||
props: AnimatableProps
|
||||
}>
|
||||
hit: { width: number; height: number; offsetX: number; offsetY: number }
|
||||
}
|
||||
|
||||
function sampleStateLayers(
|
||||
clip: ButtonClip,
|
||||
state: ButtonState,
|
||||
): ButtonRenderItem['layers'] {
|
||||
const layers = clip.states[state] ?? []
|
||||
const out: ButtonRenderItem['layers'] = []
|
||||
for (const layer of layers) {
|
||||
const asset = store.scene.assets.find((a) => a.id === layer.assetId)
|
||||
if (!asset) continue
|
||||
out.push({ layer, asset, props: sampleLayer(layer, 0) })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function blendStates(
|
||||
clip: ButtonClip,
|
||||
from: ButtonState,
|
||||
to: ButtonState,
|
||||
u: number,
|
||||
): ButtonRenderItem['layers'] {
|
||||
return blendButtonStates(clip, from, to, u, (id) =>
|
||||
store.scene.assets.find((a) => a.id === id),
|
||||
)
|
||||
}
|
||||
|
||||
const activeButtonInstances = computed<ButtonRenderItem[]>(() => {
|
||||
// Force re-evaluation on every transition RAF tick.
|
||||
void buttonRt.transitionTick.value
|
||||
const out: ButtonRenderItem[] = []
|
||||
const instances = store.scene.buttonInstances ?? []
|
||||
const clips = store.scene.buttonClips ?? []
|
||||
const clipById = new Map(clips.map((c) => [c.id, c]))
|
||||
const now = performance.now()
|
||||
for (const bi of instances) {
|
||||
if (store.playhead < bi.startTime) continue
|
||||
const clip = clipById.get(bi.buttonClipId)
|
||||
if (!clip) continue
|
||||
const disabled = buttonRt.isDisabled(bi.id, bi.disabled)
|
||||
const state = buttonRt.getState(bi.id, bi.disabled)
|
||||
const tr = buttonRt.sampleTransition(bi.id, now)
|
||||
let rendered: ButtonRenderItem['layers']
|
||||
if (tr && !tr.done && !disabled) {
|
||||
rendered = blendStates(clip, tr.from, tr.to, tr.u)
|
||||
} else {
|
||||
rendered = sampleStateLayers(clip, state)
|
||||
}
|
||||
// Hit area: explicit hitArea on the clip, or fall back to the bbox of
|
||||
// the first rect-like asset in the normal state.
|
||||
let hit = clip.hitArea
|
||||
? {
|
||||
width: clip.hitArea.width,
|
||||
height: clip.hitArea.height,
|
||||
offsetX: clip.hitArea.offsetX ?? clip.hitArea.width / 2,
|
||||
offsetY: clip.hitArea.offsetY ?? clip.hitArea.height / 2,
|
||||
}
|
||||
: null
|
||||
if (!hit) {
|
||||
for (const l of clip.states.normal) {
|
||||
const a = store.scene.assets.find((x) => x.id === l.assetId)
|
||||
if (!a) continue
|
||||
if (a.kind === 'rect' || a.kind === 'image') {
|
||||
hit = {
|
||||
width: a.width,
|
||||
height: a.height,
|
||||
offsetX: l.offsetX ?? a.width / 2,
|
||||
offsetY: l.offsetY ?? a.height / 2,
|
||||
}
|
||||
break
|
||||
}
|
||||
if (a.kind === 'circle') {
|
||||
hit = {
|
||||
width: a.radius * 2,
|
||||
height: a.radius * 2,
|
||||
offsetX: (l.offsetX ?? 0) + a.radius,
|
||||
offsetY: (l.offsetY ?? 0) + a.radius,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hit) hit = { width: 120, height: 40, offsetX: 60, offsetY: 20 }
|
||||
out.push({ bi, clip, state, disabled, layers: rendered, hit })
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
function buttonGroupConfig(bi: ButtonInstance) {
|
||||
return {
|
||||
id: `btn:${bi.id}`,
|
||||
x: bi.x ?? 0,
|
||||
y: bi.y ?? 0,
|
||||
listening: true,
|
||||
}
|
||||
}
|
||||
|
||||
function buttonHitRectConfig(item: ButtonRenderItem) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: item.hit.width,
|
||||
height: item.hit.height,
|
||||
offsetX: item.hit.offsetX,
|
||||
offsetY: item.hit.offsetY,
|
||||
fill: 'rgba(0,0,0,0.001)', // nearly transparent but hittable
|
||||
listening: !item.disabled,
|
||||
name: 'button-hit',
|
||||
}
|
||||
}
|
||||
|
||||
function buttonChildConfig<T extends Record<string, unknown>>(cfg: T): T {
|
||||
return { ...cfg, draggable: false, listening: false }
|
||||
}
|
||||
|
||||
function transitionsFor(bi: ButtonInstance) {
|
||||
return store.scene.buttonClips?.find((c) => c.id === bi.buttonClipId)?.transitions
|
||||
}
|
||||
|
||||
function onButtonMouseEnter(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'normal') applyButtonStateChange(bi.id, 'hover', transitionsFor(bi), bi.disabled)
|
||||
}
|
||||
|
||||
function onButtonMouseLeave(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'hover' || cur === 'pressed')
|
||||
applyButtonStateChange(bi.id, 'normal', transitionsFor(bi), bi.disabled)
|
||||
}
|
||||
|
||||
function onButtonMouseDown(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'hover') applyButtonStateChange(bi.id, 'pressed', transitionsFor(bi), bi.disabled)
|
||||
}
|
||||
|
||||
function onButtonMouseUp(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur !== 'pressed') return
|
||||
applyButtonStateChange(bi.id, 'hover', transitionsFor(bi), bi.disabled)
|
||||
const actions = bi.onClick
|
||||
if (actions && actions.length) runButtonActions(actions)
|
||||
}
|
||||
|
||||
function runButtonActions(actions: Action[]) {
|
||||
const rt = store.actionRuntime()
|
||||
for (const a of actions) evalAction(a, rt)
|
||||
}
|
||||
|
||||
const assetMap = computed(() => {
|
||||
const m = new Map<string, Asset>()
|
||||
for (const a of store.scene.assets) m.set(a.id, a)
|
||||
@@ -2523,42 +2352,6 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-for="item in activeButtonInstances" :key="`bi-${item.bi.id}`">
|
||||
<v-group
|
||||
:config="buttonGroupConfig(item.bi)"
|
||||
@mouseenter="onButtonMouseEnter(item.bi)"
|
||||
@mouseleave="onButtonMouseLeave(item.bi)"
|
||||
@mousedown="onButtonMouseDown(item.bi)"
|
||||
@mouseup="onButtonMouseUp(item.bi)"
|
||||
>
|
||||
<v-rect :config="buttonHitRectConfig(item)" />
|
||||
<template
|
||||
v-for="(entry, bli) in item.layers"
|
||||
:key="`${item.bi.id}-${entry.layer.id}-${bli}`"
|
||||
>
|
||||
<v-rect
|
||||
v-if="entry.asset.kind === 'rect'"
|
||||
:config="buttonChildConfig(rectConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-circle
|
||||
v-else-if="entry.asset.kind === 'circle'"
|
||||
:config="buttonChildConfig(circleConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-text
|
||||
v-else-if="entry.asset.kind === 'text'"
|
||||
:config="buttonChildConfig(textConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-line
|
||||
v-else-if="entry.asset.kind === 'polygon'"
|
||||
:config="buttonChildConfig(polygonConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-image
|
||||
v-else-if="entry.asset.kind === 'image' && imageConfig(entry.layer, entry.asset, entry.props)"
|
||||
:config="buttonChildConfig(imageConfig(entry.layer, entry.asset, entry.props)!)"
|
||||
/>
|
||||
</template>
|
||||
</v-group>
|
||||
</template>
|
||||
</v-group>
|
||||
<v-rect :config="canvasBorderConfig" />
|
||||
<v-line v-if="rotationArmConfig" :config="rotationArmConfig" />
|
||||
|
||||
+69
-158
@@ -31,14 +31,12 @@ import { sampleLayer } from '@/lib/interpolate'
|
||||
import { snapToCandidates } from '@/lib/timelineSnap'
|
||||
import { confirmDialog, promptDialog } from '@/components/ui'
|
||||
import ScriptFrameDialog from './ScriptFrameDialog.vue'
|
||||
import ButtonInstanceDialog from './ButtonInstanceDialog.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { kfSelectionKey, useKfSelection, type KfSelectionKey } from '@/composables/useKfSelection'
|
||||
import { showLocked } from '@/composables/useShowLocked'
|
||||
import type {
|
||||
AnimatableProps,
|
||||
Asset,
|
||||
ButtonInstance,
|
||||
Clip,
|
||||
ClipInstance,
|
||||
Easing,
|
||||
@@ -135,21 +133,6 @@ const scriptFramesByTrack = computed<Record<string, typeof store.scene.scriptFra
|
||||
}
|
||||
return out
|
||||
})
|
||||
const buttonClipById = computed(() => {
|
||||
const m = new Map<string, NonNullable<typeof store.scene.buttonClips>[number]>()
|
||||
for (const c of store.scene.buttonClips ?? []) m.set(c.id, c)
|
||||
return m
|
||||
})
|
||||
const buttonInstancesByTrack = computed<
|
||||
Record<string, NonNullable<typeof store.scene.buttonInstances>>
|
||||
>(() => {
|
||||
const out: Record<string, NonNullable<typeof store.scene.buttonInstances>> = {}
|
||||
for (const bi of store.scene.buttonInstances ?? []) {
|
||||
;(out[bi.clipTrackId] ||= []).push(bi)
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
const TRACK_NAME_WIDTH_KEY = 'animatr:trackNameWidth'
|
||||
const TRACK_NAME_WIDTH_DEFAULT = 140
|
||||
const TRACK_NAME_WIDTH_MIN = 80
|
||||
@@ -460,14 +443,8 @@ function onMarkerPointerMove(e: PointerEvent, markerId: string) {
|
||||
function onMarkerPointerUp(e: PointerEvent, markerId: string) {
|
||||
const el = e.currentTarget as HTMLElement
|
||||
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId)
|
||||
const drag = markerDrag.value
|
||||
markerDrag.value = null
|
||||
if (!drag || drag.id !== markerId) return
|
||||
// Click (no movement) → jump playhead to the marker.
|
||||
if (!drag.moved) {
|
||||
const m = store.scene.markers?.find((x) => x.id === markerId)
|
||||
if (m) store.setPlayhead(m.time)
|
||||
}
|
||||
void markerId
|
||||
}
|
||||
|
||||
async function renameMarkerPrompt(markerId: string) {
|
||||
@@ -1077,7 +1054,7 @@ function duplicateFromMenu() {
|
||||
}
|
||||
|
||||
interface TrackMenuState {
|
||||
kind: 'layer' | 'clip' | 'group'
|
||||
kind: 'layer' | 'clip' | 'group' | 'script'
|
||||
trackId: string
|
||||
x: number
|
||||
y: number
|
||||
@@ -1277,7 +1254,7 @@ function onTracksPointerDown(e: PointerEvent) {
|
||||
if (!target) return
|
||||
if (
|
||||
target.closest(
|
||||
'.kf, .clip-block, .clip-handle, .track-name, .drag-handle, .add-clip-btn, .playhead',
|
||||
'.kf, .clip-block, .clip-handle, .track-name, .drag-handle, .add-clip-btn, .playhead, .script-lane, .script-frame',
|
||||
)
|
||||
) {
|
||||
return
|
||||
@@ -1364,11 +1341,7 @@ function pixelTimeFromEvent(e: DragEvent | PointerEvent, el: HTMLElement): numbe
|
||||
}
|
||||
|
||||
function onClipLaneDragOver(e: DragEvent, _clipTrackId: string) {
|
||||
const types = e.dataTransfer?.types
|
||||
if (
|
||||
types?.includes('application/x-animatr-clip') ||
|
||||
types?.includes('application/x-animatr-button-clip')
|
||||
) {
|
||||
if (e.dataTransfer?.types.includes('application/x-animatr-clip')) {
|
||||
e.preventDefault()
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
@@ -1381,14 +1354,7 @@ function onClipLaneDrop(e: DragEvent, clipTrackId: string) {
|
||||
const el = e.currentTarget as HTMLElement
|
||||
const time = pixelTimeFromEvent(e, el)
|
||||
const clipId = e.dataTransfer?.getData('application/x-animatr-clip')
|
||||
if (clipId) {
|
||||
store.addClipInstance(clipId, time, clipTrackId)
|
||||
return
|
||||
}
|
||||
const buttonClipId = e.dataTransfer?.getData('application/x-animatr-button-clip')
|
||||
if (buttonClipId) {
|
||||
store.addButtonInstance(buttonClipId, clipTrackId, time)
|
||||
}
|
||||
if (clipId) store.addClipInstance(clipId, time, clipTrackId)
|
||||
}
|
||||
|
||||
interface ClipMoveState {
|
||||
@@ -1447,24 +1413,16 @@ function onClipBlockPointerUp(e: PointerEvent) {
|
||||
|
||||
const clipDragOverDropzone = ref(false)
|
||||
|
||||
function isClipDragLike(e: DragEvent): boolean {
|
||||
const types = e.dataTransfer?.types
|
||||
return !!(
|
||||
types?.includes('application/x-animatr-clip') ||
|
||||
types?.includes('application/x-animatr-button-clip')
|
||||
)
|
||||
}
|
||||
|
||||
function onNewTrackDragEnter(e: DragEvent) {
|
||||
if (!isClipDragLike(e)) return
|
||||
if (!e.dataTransfer?.types.includes('application/x-animatr-clip')) return
|
||||
e.preventDefault()
|
||||
clipDragOverDropzone.value = true
|
||||
}
|
||||
|
||||
function onNewTrackDragOver(e: DragEvent) {
|
||||
if (!isClipDragLike(e)) return
|
||||
if (!e.dataTransfer?.types.includes('application/x-animatr-clip')) return
|
||||
e.preventDefault()
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
clipDragOverDropzone.value = true
|
||||
}
|
||||
|
||||
@@ -1475,14 +1433,12 @@ function onNewTrackDragLeave() {
|
||||
function onNewTrackDrop(e: DragEvent) {
|
||||
clipDragOverDropzone.value = false
|
||||
const clipId = e.dataTransfer?.getData('application/x-animatr-clip')
|
||||
const buttonClipId = e.dataTransfer?.getData('application/x-animatr-button-clip')
|
||||
if (!clipId && !buttonClipId) return
|
||||
if (!clipId) return
|
||||
e.preventDefault()
|
||||
const el = e.currentTarget as HTMLElement
|
||||
const time = pixelTimeFromLane(e, el)
|
||||
const track = store.addClipTrack()
|
||||
if (clipId) store.addClipInstance(clipId, time, track.id)
|
||||
else if (buttonClipId) store.addButtonInstance(buttonClipId, track.id, time)
|
||||
store.addClipInstance(clipId, time, track.id)
|
||||
}
|
||||
|
||||
function pixelTimeFromLane(e: DragEvent, el: HTMLElement): number {
|
||||
@@ -1515,20 +1471,10 @@ function clipBlockStyle(ci: ClipInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
const BUTTON_STATES = ['normal', 'hover', 'pressed', 'disabled'] as const
|
||||
|
||||
// Script frame dialog state
|
||||
const scriptDialogOpen = ref(false)
|
||||
const scriptDialogFrameId = ref<string | null>(null)
|
||||
|
||||
// Button instance dialog state
|
||||
const buttonDialogOpen = ref(false)
|
||||
const buttonDialogInstanceId = ref<string | null>(null)
|
||||
function openButtonInstance(id: string) {
|
||||
buttonDialogInstanceId.value = id
|
||||
buttonDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openScriptFrame(frameId: string) {
|
||||
scriptDialogFrameId.value = frameId
|
||||
scriptDialogOpen.value = true
|
||||
@@ -1545,16 +1491,6 @@ function onScriptLaneClick(e: MouseEvent, trackId: string) {
|
||||
const frame = store.addScriptFrame(trackId, time)
|
||||
if (frame) openScriptFrame(frame.id)
|
||||
}
|
||||
const BUTTON_CELL_SEC = 0.5 // visual width of each of the 4 state cells
|
||||
function buttonInstanceStyle(bi: ButtonInstance) {
|
||||
// Four cells, each with a fixed per-cell visual width. Kept as a fixed
|
||||
// (time-like) span so the block scales with timeline zoom.
|
||||
const totalSec = BUTTON_CELL_SEC * 4
|
||||
return {
|
||||
left: pxFromTime(bi.startTime) + 'px',
|
||||
width: Math.max(80, pxFromTime(totalSec)) + 'px',
|
||||
}
|
||||
}
|
||||
|
||||
function trimTooltip(ci: ClipInstance): string {
|
||||
const name = clipById.value.get(ci.clipId)?.name ?? '?'
|
||||
@@ -1699,6 +1635,22 @@ function onClipTrackContextMenu(e: MouseEvent, clipTrackId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function onScriptTrackContextMenu(e: MouseEvent, scriptTrackId: string) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const track = store.scene.scriptTracks?.find((t) => t.id === scriptTrackId)
|
||||
if (!track) return
|
||||
trackMenu.value = {
|
||||
kind: 'script',
|
||||
trackId: scriptTrackId,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
separated: false,
|
||||
currentColor: track.color ?? null,
|
||||
locked: !!track.locked,
|
||||
}
|
||||
}
|
||||
|
||||
function applyTrackColor(color: string | null) {
|
||||
if (!trackMenu.value) return
|
||||
store.setTrackColor(trackMenu.value.trackId, color)
|
||||
@@ -1722,6 +1674,8 @@ function deleteTrack() {
|
||||
store.removeLayer(trackMenu.value.trackId)
|
||||
} else if (trackMenu.value.kind === 'clip') {
|
||||
store.removeClipTrack(trackMenu.value.trackId)
|
||||
} else if (trackMenu.value.kind === 'script') {
|
||||
store.removeScriptTrack(trackMenu.value.trackId)
|
||||
} else {
|
||||
store.removeTrackGroup(trackMenu.value.trackId)
|
||||
}
|
||||
@@ -1730,19 +1684,35 @@ function deleteTrack() {
|
||||
|
||||
async function renameFromMenu() {
|
||||
if (!trackMenu.value) return
|
||||
if (trackMenu.value.kind !== 'group') return
|
||||
const group = groupById.value.get(trackMenu.value.trackId)
|
||||
if (!group) return
|
||||
const id = trackMenu.value.trackId
|
||||
const current = group.name
|
||||
trackMenu.value = null
|
||||
const next = await promptDialog({
|
||||
title: t('timeline.renameGroupDialog.title'),
|
||||
defaultValue: current,
|
||||
confirmLabel: t('timeline.renameGroupDialog.confirm'),
|
||||
})
|
||||
if (next === null) return
|
||||
store.renameGroup(id, next)
|
||||
if (trackMenu.value.kind === 'group') {
|
||||
const group = groupById.value.get(trackMenu.value.trackId)
|
||||
if (!group) return
|
||||
const id = trackMenu.value.trackId
|
||||
const current = group.name
|
||||
trackMenu.value = null
|
||||
const next = await promptDialog({
|
||||
title: t('timeline.renameGroupDialog.title'),
|
||||
defaultValue: current,
|
||||
confirmLabel: t('timeline.renameGroupDialog.confirm'),
|
||||
})
|
||||
if (next === null) return
|
||||
store.renameGroup(id, next)
|
||||
return
|
||||
}
|
||||
if (trackMenu.value.kind === 'script') {
|
||||
const track = scriptTrackById.value.get(trackMenu.value.trackId)
|
||||
if (!track) return
|
||||
const id = trackMenu.value.trackId
|
||||
const current = track.name ?? t('timeline.scriptTrackName')
|
||||
trackMenu.value = null
|
||||
const next = await promptDialog({
|
||||
title: t('timeline.renameScriptTrackDialog.title'),
|
||||
defaultValue: current,
|
||||
confirmLabel: t('timeline.renameScriptTrackDialog.confirm'),
|
||||
})
|
||||
if (next === null) return
|
||||
store.setScriptTrackName(id, next)
|
||||
}
|
||||
}
|
||||
|
||||
function onDocumentPointerDown(e: PointerEvent) {
|
||||
@@ -2313,22 +2283,6 @@ onBeforeUnmount(() => {
|
||||
@click.stop
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-for="bi in buttonInstancesByTrack[track.clipTrackId] ?? []"
|
||||
:key="`bi-${bi.id}`"
|
||||
class="button-instance"
|
||||
:class="{ disabled: bi.disabled }"
|
||||
:style="buttonInstanceStyle(bi)"
|
||||
:title="bi.label ?? buttonClipById.get(bi.buttonClipId)?.name ?? ''"
|
||||
@click.stop="openButtonInstance(bi.id)"
|
||||
>
|
||||
<span
|
||||
v-for="state in BUTTON_STATES"
|
||||
:key="state"
|
||||
class="button-instance-cell"
|
||||
:data-state="state"
|
||||
>{{ state[0].toUpperCase() }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2351,6 +2305,7 @@ onBeforeUnmount(() => {
|
||||
<span
|
||||
class="track-name script-track-name"
|
||||
:draggable="!track.locked"
|
||||
@contextmenu="!track.locked ? onScriptTrackContextMenu($event, track.scriptTrackId) : null"
|
||||
@dragstart="!track.locked ? onTrackDragStart($event, track.scriptTrackId) : null"
|
||||
@dragend="onTrackDragEnd"
|
||||
>
|
||||
@@ -2592,6 +2547,14 @@ onBeforeUnmount(() => {
|
||||
{{ t('timeline.renameGroup') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="trackMenu.kind === 'script'">
|
||||
<div class="kf-menu-divider"></div>
|
||||
<button class="kf-menu-item" @click="renameFromMenu">
|
||||
<span class="check"></span>
|
||||
<PhArrowsSplit :size="12" class="kf-menu-icon" style="visibility: hidden" />
|
||||
{{ t('timeline.renameScriptTrack') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="trackMenu.kind === 'layer'">
|
||||
<div class="kf-menu-divider"></div>
|
||||
@@ -2617,7 +2580,9 @@ onBeforeUnmount(() => {
|
||||
? t('timeline.deleteLayer')
|
||||
: trackMenu.kind === 'clip'
|
||||
? t('timeline.deleteClipTrack')
|
||||
: t('timeline.ungroup')
|
||||
: trackMenu.kind === 'script'
|
||||
? t('timeline.deleteScriptTrack')
|
||||
: t('timeline.ungroup')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -2625,10 +2590,6 @@ onBeforeUnmount(() => {
|
||||
v-model="scriptDialogOpen"
|
||||
:frame-id="scriptDialogFrameId"
|
||||
/>
|
||||
<ButtonInstanceDialog
|
||||
v-model="buttonDialogOpen"
|
||||
:instance-id="buttonDialogInstanceId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3496,56 +3457,6 @@ onBeforeUnmount(() => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- Button instance (4 cells) ---- */
|
||||
|
||||
.button-instance {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--track-color, var(--accent));
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--track-color, var(--accent)) 10%, transparent);
|
||||
color: var(--track-color, var(--accent));
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button-instance.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.button-instance-cell {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid color-mix(in srgb, var(--track-color, var(--accent)) 40%, transparent);
|
||||
background: color-mix(in srgb, var(--track-color, var(--accent)) 8%, transparent);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.button-instance-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.button-instance-cell[data-state='hover'] {
|
||||
background: color-mix(in srgb, var(--track-color, var(--accent)) 14%, transparent);
|
||||
}
|
||||
|
||||
.button-instance-cell[data-state='pressed'] {
|
||||
background: color-mix(in srgb, var(--track-color, var(--accent)) 22%, transparent);
|
||||
}
|
||||
|
||||
.button-instance-cell[data-state='disabled'] {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
color-mix(in srgb, var(--track-color, var(--accent)) 8%, transparent) 0 4px,
|
||||
transparent 4px 8px
|
||||
);
|
||||
}
|
||||
|
||||
.clip-block {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
|
||||
Reference in new issue
Block a user