Skip to content

Commit 5fdac9a

Browse files
authored
Merge pull request #533 from Yonava/yva/animation-sync
feat(shapes/Animation): animation synchronization option
2 parents 8c99525 + 9a81311 commit 5fdac9a

4 files changed

Lines changed: 61 additions & 5 deletions

File tree

packages/products/src/markov-chains/MainView.vue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,67 @@
77
import MarkovChainInfo from "./ui/MarkovChainInfo.vue";
88
import MarkovChainInfoLabels from "./ui/MarkovChainInfoLabels.vue";
99
import { useMarkovColorizer } from "./ui/useMarkovColorizer";
10+
import { watch } from "vue";
11+
import GButton from "@magic/ui/graph/button/GButton.vue";
12+
import gsap from "gsap";
13+
import colors from "@magic/utils/colors";
1014
1115
const graphWithCanvas = useGraphWithCanvas(MARKOV_CHAIN_GRAPH_SETTINGS);
1216
const { graph } = graphWithCanvas;
1317
1418
const markov = useMarkovChain(graph);
1519
useMarkovColorizer(graph, markov).colorize();
20+
21+
const int = gsap.utils.interpolate(colors.RED_500, colors.RED_800);
22+
23+
const { play, stop } = graph.defineTimeline({
24+
forShapes: ["circle"],
25+
durationMs: 2000,
26+
customInterpolations: {
27+
stroke: {
28+
value: (progress, schema) => ({
29+
color: progress < 0.5 ? int(progress * 2) : int(2 - progress * 2),
30+
lineWidth: schema.stroke?.lineWidth ?? 10,
31+
}),
32+
easing: "in-out",
33+
},
34+
},
35+
synchronize: true,
36+
});
37+
38+
graph.subscribe("onFocusChange", (newIds, oldIds) => {
39+
const newNodeIds = Array.from(newIds).filter(graph.getNode);
40+
const oldNodeIds = Array.from(oldIds).filter(graph.getNode);
41+
newNodeIds.forEach((nodeId) => {
42+
stop({ shapeId: nodeId });
43+
});
44+
const noLongerFocused = Array.from(oldNodeIds).filter(
45+
(nodeId) => !newNodeIds.includes(nodeId)
46+
);
47+
for (const nodeId of noLongerFocused) {
48+
if (markov.invalidStates.value.has(nodeId)) play({ shapeId: nodeId });
49+
}
50+
});
51+
52+
watch(markov.invalidStates, () => {
53+
for (const node of graph.nodes.value) {
54+
stop({ shapeId: node.id });
55+
if (markov.invalidStates.value.has(node.id)) play({ shapeId: node.id });
56+
}
57+
});
58+
59+
const test = () => {
60+
for (const nodeId of markov.invalidStates.value) {
61+
play({ shapeId: nodeId });
62+
}
63+
};
1664
</script>
1765

1866
<template>
1967
<GraphProduct v-bind="graphWithCanvas">
2068
<template #top-center>
2169
<MarkovChainInfo :markov="markov" />
70+
<GButton @click="test">Test</GButton>
2271
</template>
2372

2473
<template #bottom-center>

packages/shapes/src/animation/autoAnimate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { DefineTimeline } from './timeline/define';
66
import type { LooseSchema, LooseSchemaValue } from './types';
77

88
export const AUTO_ANIMATE_DURATION_MS = 500;
9+
// properties supported by the auto animate feature
910
const AUTO_ANIMATED_PROPERTIES = new Set([
1011
'at',
1112
'start',

packages/shapes/src/animation/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ export const useAnimatedShapes = () => {
2929
const schemaIdToShapeName: Map<SchemaId, ShapeName> = new Map();
3030

3131
const { defineTimeline, timelineIdToTimeline } = useDefineTimeline({
32-
play: ({ shapeId, timelineId, runCount = Infinity }) => {
32+
play: ({ shapeId, timelineId, synchronize, runCount = Infinity }) => {
3333
const newAnimation: ActiveAnimation = {
34-
runCount,
35-
startedAt: Date.now(),
34+
runCount: synchronize ? Infinity : runCount,
35+
startedAt: synchronize ? 0 : Date.now(),
3636
timelineId,
3737
};
3838

packages/shapes/src/animation/timeline/define.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ type IdField = {
2424
type TimelinePlayOptions = ShapeTarget & {
2525
/**
2626
* number of times this animation should run
27+
*
28+
* 👉 NOTE 👈 if {@link Timeline.synchronize} is set to true, `runCount` will always be `Infinity`
2729
* @default Infinity
2830
*/
2931
runCount?: number;
3032
};
3133

3234
export type UseDefineTimelineOptions = {
33-
play: (options: TimelinePlayOptions & IdField) => void;
35+
play: (options: TimelinePlayOptions & IdField & Pick<Timeline<any>, 'synchronize'>) => void;
3436
pause: (options: ShapeTarget & IdField) => void;
3537
resume: (options: ShapeTarget & IdField) => void;
3638
stop: (options: ShapeTarget & IdField) => void;
@@ -126,6 +128,10 @@ export type Timeline<T extends keyof ShapeNameToSchema> = DeepReadonly<
126128
forShapes: T[];
127129
keyframes?: TimelineKeyframe<SchemaWithDefaults[NoInfer<T>]>[];
128130
easing?: Partial<Record<keyof SchemaWithDefaults[NoInfer<T>], EasingOption>>;
131+
/**
132+
* allow shapes to animate in sync even when invoking {@link TimelineControls.play} at different times
133+
*/
134+
synchronize?: boolean;
129135
} & TimelinePlaybackDuration &
130136
TimelinePlaybackDelay &
131137
TimelineCustomInterpolations<SchemaWithDefaults[NoInfer<T>]>
@@ -143,7 +149,7 @@ export const useDefineTimeline = (controls: UseDefineTimelineOptions) => {
143149
timelineIdToTimeline.set(timelineId, compiledTimeline);
144150

145151
return {
146-
play: (opts) => controls.play({ ...opts, timelineId }),
152+
play: (opts) => controls.play({ ...opts, timelineId, synchronize: timeline.synchronize }),
147153
pause: (opts) => controls.pause({ ...opts, timelineId }),
148154
resume: (opts) => controls.resume({ ...opts, timelineId }),
149155
stop: (opts) => controls.stop({ ...opts, timelineId }),

0 commit comments

Comments
 (0)