Files
tools/vue/primitives/src/canvas/keyframe-track/KeyframeTrackEasingEditor.vue
T
robonen eefd7abf83 feat(primitives): media-editor components, category reorg, perf + type cleanup
Reorganize components into category folders (forms/canvas/overlays/etc.); add the
media-editor headless family (timeline, curve-editor, waveform, crop, color
picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag
state, gesture-leak teardown, shallowRef color state, rect caching) and replace
source `any` with proper types.
2026-06-15 16:54:29 +07:00

98 lines
3.6 KiB
Vue

<script lang="ts">
import type { PrimitiveProps } from '../../internal/primitive';
/**
* Embeds a `CurveEditorRoot` (in `'bezier'` interpolation) bound to the SELECTED
* keyframe's segment easing. The curve's two anchors are pinned at `(0, 0)` and
* `(1, 1)` (CSS `cubic-bezier` semantics); the start anchor's `outHandle` and the
* end anchor's `inHandle` map to the easing tuple `[x1, y1, x2, y2]`.
*
* The binding is one-way IN (the editor is seeded from the keyframe's easing via
* the CurveEditor `defaultValue`) and one-way OUT (every anchor commit reads the
* handles back and calls `ctx.setEasing`). The CurveEditor is remounted (keyed on
* the selected id) whenever the selection changes so its seed always reflects the
* newly selected keyframe.
*
* Renders nothing unless a keyframe with a FOLLOWING segment is selected (the
* last keyframe has no outgoing segment to ease).
*/
export interface KeyframeTrackEasingEditorProps extends PrimitiveProps {
/** Sample count for the rendered easing polyline. @default 256 */
samples?: number;
}
</script>
<script setup lang="ts">
import { computed } from 'vue';
import { useForwardExpose } from '@robonen/vue';
import { Primitive } from '../../internal/primitive';
import { CurveEditorRoot } from '../curve-editor';
import type { CurveEditorAnchor } from '../curve-editor';
import { DEFAULT_KEYFRAME_EASING, useKeyframeTrackContext } from './context';
const { samples = 256, as = 'div' } = defineProps<KeyframeTrackEasingEditorProps>();
const ctx = useKeyframeTrackContext();
const { forwardRef } = useForwardExpose();
// The selected keyframe must exist AND have a following segment to ease.
const selected = computed(() => {
const id = ctx.selectedId.value;
if (id === null) return null;
const list = ctx.keyframes.value;
const i = list.findIndex(k => k.id === id);
if (i === -1 || i >= list.length - 1) return null;
return list[i]!;
});
// Seed anchors for the embedded CurveEditor from the segment's easing tuple.
// Anchors are pinned at (0,0)/(1,1); the handles are the bezier control points
// relative to their anchor (CSS cubic-bezier control points).
const seedAnchors = computed<CurveEditorAnchor[]>(() => {
const kf = selected.value;
const e = kf?.easing ?? DEFAULT_KEYFRAME_EASING;
const [x1, y1, x2, y2] = e;
return [
{ id: 'kf-easing-start', x: 0, y: 0, outHandle: { x: x1, y: y1 } },
{ id: 'kf-easing-end', x: 1, y: 1, inHandle: { x: x2 - 1, y: y2 - 1 } },
];
});
// Read the handles back off the committed anchors and write the easing tuple.
function onAnchorsCommit(anchors: CurveEditorAnchor[]): void {
const kf = selected.value;
if (!kf || anchors.length < 2) return;
const startA = anchors[0]!;
const endA = anchors[anchors.length - 1]!;
const x1 = startA.outHandle ? startA.x + startA.outHandle.x : 0;
const y1 = startA.outHandle ? startA.y + startA.outHandle.y : 0;
const x2 = endA.inHandle ? endA.x + endA.inHandle.x : 1;
const y2 = endA.inHandle ? endA.y + endA.inHandle.y : 1;
ctx.setEasing(kf.id, [x1, y1, x2, y2]);
}
</script>
<template>
<Primitive
v-if="selected"
:ref="forwardRef"
:as="as"
data-easing-editor
:data-disabled="ctx.disabled.value ? '' : undefined"
>
<CurveEditorRoot
:key="selected.id"
interpolation="bezier"
:default-value="seedAnchors"
:samples="samples"
:disabled="ctx.disabled.value"
:dir="ctx.direction.value"
@anchors-commit="onAnchorsCommit"
>
<template #default="curveProps">
<slot v-bind="curveProps" :keyframe="selected" />
</template>
</CurveEditorRoot>
</Primitive>
</template>