flowsvg 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/PATTERNS.md +61 -5
- package/README.md +149 -461
- package/README.zh-CN.md +149 -461
- package/dist/adapters/react.d.ts +7 -0
- package/dist/adapters/react.js +14 -2
- package/dist/adapters/vue.d.ts +29 -3
- package/dist/adapters/vue.js +20 -4
- package/dist/adapters/web-component.d.ts +14 -2
- package/dist/adapters/web-component.js +32 -3
- package/dist/browser/controls.d.ts +7 -3
- package/dist/browser/controls.js +23 -31
- package/dist/browser/export-controls.d.ts +32 -0
- package/dist/browser/export-controls.js +151 -0
- package/dist/browser/player.d.ts +9 -1
- package/dist/browser/player.js +83 -24
- package/dist/browser/select-control.d.ts +33 -0
- package/dist/browser/select-control.js +171 -0
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +2 -1
- package/dist/core/diagram.d.ts +4 -2
- package/dist/core/diagram.js +111 -21
- package/dist/core/edge-geometry.d.ts +16 -3
- package/dist/core/edge-geometry.js +264 -37
- package/dist/core/model.d.ts +35 -9
- package/dist/core/node-content-measurement.d.ts +34 -0
- package/dist/core/node-content-measurement.js +66 -0
- package/dist/core/node-content.d.ts +17 -0
- package/dist/core/node-content.js +30 -6
- package/dist/core/timeline.d.ts +11 -3
- package/dist/core/timeline.js +29 -5
- package/dist/core/validation.d.ts +1 -1
- package/dist/core/validation.js +13 -3
- package/dist/export/browser.d.ts +9 -0
- package/dist/export/browser.js +172 -0
- package/dist/export/captions.d.ts +18 -0
- package/dist/export/captions.js +44 -0
- package/dist/export/plan.d.ts +25 -0
- package/dist/export/plan.js +94 -0
- package/dist/export/types.d.ts +64 -0
- package/dist/export/types.js +9 -0
- package/dist/export/worker-source.d.ts +1 -0
- package/dist/export/worker-source.js +1 -0
- package/dist/export.d.ts +3 -0
- package/dist/export.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +72 -0
- package/dist/node/gif.d.ts +16 -0
- package/dist/node/gif.js +184 -0
- package/dist/node/html.d.ts +19 -0
- package/dist/node/html.js +61 -0
- package/dist/node/output.d.ts +2 -0
- package/dist/node/output.js +22 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +3 -0
- package/dist/portable/LICENSE.esbuild.txt +21 -0
- package/dist/portable/LICENSE.flowsvg.txt +21 -0
- package/dist/portable/LICENSE.gifenc.txt +21 -0
- package/dist/portable/export.js +5 -0
- package/dist/portable/manifest.json +36 -0
- package/dist/portable/player.js +2 -0
- package/dist/portable/styles.css +227 -0
- package/dist/styles.css +194 -14
- package/docs/images/flowsvg-wordmark.gif +0 -0
- package/package.json +37 -6
package/dist/adapters/react.d.ts
CHANGED
|
@@ -10,8 +10,12 @@ export interface FlowPlayerProps {
|
|
|
10
10
|
document: DiagramDocument;
|
|
11
11
|
/** Positive playback multiplier; omit to use the player default. */
|
|
12
12
|
speed?: number;
|
|
13
|
+
/** Defaults to false. Changes update live loop mode without rebuilding the player. */
|
|
14
|
+
loop?: boolean;
|
|
13
15
|
theme?: ThemeName;
|
|
14
16
|
controls?: boolean;
|
|
17
|
+
/** Defaults to false. Enable GIF downloads when controls are visible; export is independent of playback. */
|
|
18
|
+
gifExport?: boolean;
|
|
15
19
|
/** Defaults to false: hover marks borders only. True also highlights backgrounds, including groups. */
|
|
16
20
|
hoverFill?: boolean;
|
|
17
21
|
reducedMotion?: boolean;
|
|
@@ -22,6 +26,7 @@ export interface FlowPlayerProps {
|
|
|
22
26
|
onStepChange?: (state: PlayerState) => void;
|
|
23
27
|
onTimeUpdate?: (state: PlayerState) => void;
|
|
24
28
|
onPlayStateChange?: (state: PlayerState) => void;
|
|
29
|
+
onLoopChange?: (state: PlayerState) => void;
|
|
25
30
|
onError?: (detail: PlayerErrorDetail) => void;
|
|
26
31
|
}
|
|
27
32
|
/** Imperative controls for a mounted component; calls before mounting or after unmounting throw a not-mounted error. */
|
|
@@ -36,6 +41,8 @@ export interface FlowPlayerHandle {
|
|
|
36
41
|
step(direction: 1 | -1): void;
|
|
37
42
|
/** Set a finite positive multiplier; ignore invalid values. */
|
|
38
43
|
setSpeed(speed: number): void;
|
|
44
|
+
/** Toggle whole-timeline playback without changing the current position or play state. */
|
|
45
|
+
setLoop(loop: boolean): void;
|
|
39
46
|
/** Reset/pause at zero on success; emit an error and retain playback state if validation fails. */
|
|
40
47
|
load(document: DiagramDocument): void;
|
|
41
48
|
/** Read the current browser-player snapshot. */
|
package/dist/adapters/react.js
CHANGED
|
@@ -22,6 +22,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
|
|
|
22
22
|
onStepChange: props.onStepChange,
|
|
23
23
|
onTimeUpdate: props.onTimeUpdate,
|
|
24
24
|
onPlayStateChange: props.onPlayStateChange,
|
|
25
|
+
onLoopChange: props.onLoopChange,
|
|
25
26
|
onError: props.onError
|
|
26
27
|
};
|
|
27
28
|
useImperativeHandle(ref, () => ({
|
|
@@ -30,6 +31,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
|
|
|
30
31
|
seek: (timeMs) => requirePlayer(playerRef).seek(timeMs),
|
|
31
32
|
step: (direction) => requirePlayer(playerRef).step(direction),
|
|
32
33
|
setSpeed: (speed) => requirePlayer(playerRef).setSpeed(speed),
|
|
34
|
+
setLoop: (loop) => requirePlayer(playerRef).setLoop(loop),
|
|
33
35
|
load: (document) => requirePlayer(playerRef).load(document),
|
|
34
36
|
getState: () => requirePlayer(playerRef).getState()
|
|
35
37
|
}), []);
|
|
@@ -40,6 +42,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
|
|
|
40
42
|
const nextOptions = {
|
|
41
43
|
theme: props.theme,
|
|
42
44
|
controls: props.controls,
|
|
45
|
+
gifExport: props.gifExport,
|
|
43
46
|
hoverFill: props.hoverFill,
|
|
44
47
|
reducedMotion: props.reducedMotion
|
|
45
48
|
};
|
|
@@ -53,7 +56,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
|
|
|
53
56
|
// Detach old callbacks before replacement so old lifecycle events cannot reach the new component state.
|
|
54
57
|
eventCleanupRef.current?.();
|
|
55
58
|
existing?.destroy();
|
|
56
|
-
const player = createPlayer(host, props.document, nextOptions);
|
|
59
|
+
const player = createPlayer(host, props.document, { ...nextOptions, loop: props.loop ?? existing?.getState().loop });
|
|
57
60
|
// A new instance needs current props even when the separate speed effect has no changed dependency.
|
|
58
61
|
if (props.speed !== undefined)
|
|
59
62
|
player.setSpeed(props.speed);
|
|
@@ -68,12 +71,15 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
|
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
optionsRef.current = nextOptions;
|
|
71
|
-
}, [props.document, props.theme, props.controls, props.reducedMotion, props.hoverFill]);
|
|
74
|
+
}, [props.document, props.theme, props.controls, props.gifExport, props.reducedMotion, props.hoverFill]);
|
|
72
75
|
useEffect(() => {
|
|
73
76
|
const player = playerRef.current;
|
|
74
77
|
if (player && props.speed !== undefined)
|
|
75
78
|
player.setSpeed(props.speed);
|
|
76
79
|
}, [props.speed]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
playerRef.current?.setLoop(props.loop ?? false);
|
|
82
|
+
}, [props.loop]);
|
|
77
83
|
useEffect(() => () => {
|
|
78
84
|
eventCleanupRef.current?.();
|
|
79
85
|
eventCleanupRef.current = null;
|
|
@@ -96,6 +102,7 @@ function sameOptions(previous, next) {
|
|
|
96
102
|
return false;
|
|
97
103
|
return previous.theme === next.theme
|
|
98
104
|
&& previous.controls === next.controls
|
|
105
|
+
&& previous.gifExport === next.gifExport
|
|
99
106
|
&& previous.hoverFill === next.hoverFill
|
|
100
107
|
&& previous.reducedMotion === next.reducedMotion;
|
|
101
108
|
}
|
|
@@ -110,17 +117,22 @@ function attachPlayerEvents(player, callbacksRef) {
|
|
|
110
117
|
const onPlayStateChange = (event) => {
|
|
111
118
|
callbacksRef.current.onPlayStateChange?.(event.detail);
|
|
112
119
|
};
|
|
120
|
+
const onLoopChange = (event) => {
|
|
121
|
+
callbacksRef.current.onLoopChange?.(event.detail);
|
|
122
|
+
};
|
|
113
123
|
const onError = (event) => {
|
|
114
124
|
callbacksRef.current.onError?.(event.detail);
|
|
115
125
|
};
|
|
116
126
|
player.element.addEventListener("stepchange", onStepChange);
|
|
117
127
|
player.element.addEventListener("timeupdate", onTimeUpdate);
|
|
118
128
|
player.element.addEventListener("playstatechange", onPlayStateChange);
|
|
129
|
+
player.element.addEventListener("loopchange", onLoopChange);
|
|
119
130
|
player.element.addEventListener("error", onError);
|
|
120
131
|
return () => {
|
|
121
132
|
player.element.removeEventListener("stepchange", onStepChange);
|
|
122
133
|
player.element.removeEventListener("timeupdate", onTimeUpdate);
|
|
123
134
|
player.element.removeEventListener("playstatechange", onPlayStateChange);
|
|
135
|
+
player.element.removeEventListener("loopchange", onLoopChange);
|
|
124
136
|
player.element.removeEventListener("error", onError);
|
|
125
137
|
};
|
|
126
138
|
}
|
package/dist/adapters/vue.d.ts
CHANGED
|
@@ -10,8 +10,12 @@ export interface FlowPlayerProps {
|
|
|
10
10
|
document: DiagramDocument;
|
|
11
11
|
/** Positive playback multiplier; omit to use the player default. */
|
|
12
12
|
speed?: number;
|
|
13
|
+
/** Defaults to false. Changes update live loop mode without rebuilding the player. */
|
|
14
|
+
loop?: boolean;
|
|
13
15
|
theme?: ThemeName;
|
|
14
16
|
controls?: boolean;
|
|
17
|
+
/** Defaults to false. Enable GIF downloads when controls are visible; export is independent of playback. */
|
|
18
|
+
gifExport?: boolean;
|
|
15
19
|
/** Defaults to false: hover marks borders only. True also highlights backgrounds, including groups. */
|
|
16
20
|
hoverFill?: boolean;
|
|
17
21
|
reducedMotion?: boolean;
|
|
@@ -22,6 +26,7 @@ export interface FlowPlayerProps {
|
|
|
22
26
|
onStepChange?: (state: PlayerState) => void;
|
|
23
27
|
onTimeUpdate?: (state: PlayerState) => void;
|
|
24
28
|
onPlayStateChange?: (state: PlayerState) => void;
|
|
29
|
+
onLoopChange?: (state: PlayerState) => void;
|
|
25
30
|
onError?: (detail: PlayerErrorDetail) => void;
|
|
26
31
|
}
|
|
27
32
|
/** Imperative controls for a mounted component; calls before mounting or after unmounting throw a not-mounted error. */
|
|
@@ -36,6 +41,8 @@ export interface FlowPlayerHandle {
|
|
|
36
41
|
step(direction: 1 | -1): void;
|
|
37
42
|
/** Set a finite positive multiplier; ignore invalid values. */
|
|
38
43
|
setSpeed(speed: number): void;
|
|
44
|
+
/** Toggle whole-timeline playback without changing the current position or play state. */
|
|
45
|
+
setLoop(loop: boolean): void;
|
|
39
46
|
/** Reset/pause at zero on success; emit an error and retain playback state if validation fails. */
|
|
40
47
|
load(document: DiagramDocument): void;
|
|
41
48
|
/** Read the current browser-player snapshot. */
|
|
@@ -54,10 +61,18 @@ export declare const FlowPlayer: import("vue").DefineComponent<import("vue").Ext
|
|
|
54
61
|
readonly type: NumberConstructor;
|
|
55
62
|
readonly required: false;
|
|
56
63
|
};
|
|
64
|
+
readonly loop: {
|
|
65
|
+
readonly type: PropType<boolean | undefined>;
|
|
66
|
+
readonly default: undefined;
|
|
67
|
+
};
|
|
57
68
|
readonly theme: {
|
|
58
69
|
readonly type: PropType<ThemeName>;
|
|
59
70
|
readonly required: false;
|
|
60
71
|
};
|
|
72
|
+
readonly gifExport: {
|
|
73
|
+
readonly type: PropType<boolean | undefined>;
|
|
74
|
+
readonly default: undefined;
|
|
75
|
+
};
|
|
61
76
|
readonly controls: {
|
|
62
77
|
readonly type: PropType<boolean | undefined>;
|
|
63
78
|
readonly default: undefined;
|
|
@@ -94,6 +109,10 @@ export declare const FlowPlayer: import("vue").DefineComponent<import("vue").Ext
|
|
|
94
109
|
readonly type: PropType<(state: PlayerState) => void>;
|
|
95
110
|
readonly required: false;
|
|
96
111
|
};
|
|
112
|
+
readonly onLoopChange: {
|
|
113
|
+
readonly type: PropType<(state: PlayerState) => void>;
|
|
114
|
+
readonly required: false;
|
|
115
|
+
};
|
|
97
116
|
readonly onError: {
|
|
98
117
|
readonly type: PropType<(detail: PlayerErrorDetail) => void>;
|
|
99
118
|
readonly required: false;
|
|
@@ -104,29 +123,36 @@ export declare const FlowPlayer: import("vue").DefineComponent<import("vue").Ext
|
|
|
104
123
|
stepchange: (_state: PlayerState) => true;
|
|
105
124
|
timeupdate: (_state: PlayerState) => true;
|
|
106
125
|
playstatechange: (_state: PlayerState) => true;
|
|
126
|
+
loopchange: (_state: PlayerState) => true;
|
|
107
127
|
error: (_detail: PlayerErrorDetail) => true;
|
|
108
128
|
}, string, import("vue").PublicProps, Readonly<{
|
|
109
129
|
readonly document: DiagramDocument;
|
|
110
130
|
} & {
|
|
111
131
|
readonly style?: CSSProperties | undefined;
|
|
112
132
|
readonly theme?: ThemeName | undefined;
|
|
133
|
+
readonly reducedMotion?: boolean | undefined;
|
|
134
|
+
readonly speed?: number | undefined;
|
|
113
135
|
readonly controls?: boolean | undefined;
|
|
136
|
+
readonly loop?: boolean | undefined;
|
|
137
|
+
readonly gifExport?: boolean | undefined;
|
|
114
138
|
readonly hoverFill?: boolean | undefined;
|
|
115
|
-
readonly speed?: number | undefined;
|
|
116
|
-
readonly reducedMotion?: boolean | undefined;
|
|
117
139
|
readonly initialTimeMs?: number | undefined;
|
|
118
140
|
readonly className?: string | undefined;
|
|
119
141
|
readonly onStepChange?: ((state: PlayerState) => void) | undefined;
|
|
120
142
|
readonly onTimeUpdate?: ((state: PlayerState) => void) | undefined;
|
|
121
143
|
readonly onPlayStateChange?: ((state: PlayerState) => void) | undefined;
|
|
144
|
+
readonly onLoopChange?: ((state: PlayerState) => void) | undefined;
|
|
122
145
|
readonly onError?: ((detail: PlayerErrorDetail) => void) | undefined;
|
|
123
146
|
} & {
|
|
124
147
|
onError?: ((_detail: PlayerErrorDetail) => any) | undefined;
|
|
125
148
|
onTimeupdate?: ((_state: PlayerState) => any) | undefined;
|
|
126
149
|
onPlaystatechange?: ((_state: PlayerState) => any) | undefined;
|
|
127
150
|
onStepchange?: ((_state: PlayerState) => any) | undefined;
|
|
151
|
+
onLoopchange?: ((_state: PlayerState) => any) | undefined;
|
|
128
152
|
}>, {
|
|
153
|
+
readonly reducedMotion: boolean | undefined;
|
|
129
154
|
readonly controls: boolean | undefined;
|
|
155
|
+
readonly loop: boolean | undefined;
|
|
156
|
+
readonly gifExport: boolean | undefined;
|
|
130
157
|
readonly hoverFill: boolean | undefined;
|
|
131
|
-
readonly reducedMotion: boolean | undefined;
|
|
132
158
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}>;
|
package/dist/adapters/vue.js
CHANGED
|
@@ -7,7 +7,9 @@ import { createPlayer } from "../browser/player.js";
|
|
|
7
7
|
const propsOptions = {
|
|
8
8
|
document: { type: Object, required: true },
|
|
9
9
|
speed: { type: Number, required: false },
|
|
10
|
+
loop: { type: Boolean, default: undefined },
|
|
10
11
|
theme: { type: String, required: false },
|
|
12
|
+
gifExport: { type: Boolean, default: undefined },
|
|
11
13
|
controls: { type: Boolean, default: undefined },
|
|
12
14
|
hoverFill: { type: Boolean, default: undefined },
|
|
13
15
|
reducedMotion: { type: Boolean, default: undefined },
|
|
@@ -17,6 +19,7 @@ const propsOptions = {
|
|
|
17
19
|
onStepChange: { type: Function, required: false },
|
|
18
20
|
onTimeUpdate: { type: Function, required: false },
|
|
19
21
|
onPlayStateChange: { type: Function, required: false },
|
|
22
|
+
onLoopChange: { type: Function, required: false },
|
|
20
23
|
onError: { type: Function, required: false }
|
|
21
24
|
};
|
|
22
25
|
/**
|
|
@@ -31,6 +34,7 @@ export const FlowPlayer = defineComponent({
|
|
|
31
34
|
stepchange: (_state) => true,
|
|
32
35
|
timeupdate: (_state) => true,
|
|
33
36
|
playstatechange: (_state) => true,
|
|
37
|
+
loopchange: (_state) => true,
|
|
34
38
|
error: (_detail) => true
|
|
35
39
|
},
|
|
36
40
|
setup(props, { attrs, emit, expose }) {
|
|
@@ -51,12 +55,14 @@ export const FlowPlayer = defineComponent({
|
|
|
51
55
|
seek: (timeMs) => requirePlayer().seek(timeMs),
|
|
52
56
|
step: (direction) => requirePlayer().step(direction),
|
|
53
57
|
setSpeed: (speed) => requirePlayer().setSpeed(speed),
|
|
58
|
+
setLoop: (loop) => requirePlayer().setLoop(loop),
|
|
54
59
|
load: (document) => requirePlayer().load(document),
|
|
55
60
|
getState: () => requirePlayer().getState()
|
|
56
61
|
});
|
|
57
62
|
const currentOptions = () => ({
|
|
58
63
|
theme: props.theme,
|
|
59
64
|
controls: props.controls,
|
|
65
|
+
gifExport: props.gifExport,
|
|
60
66
|
hoverFill: props.hoverFill,
|
|
61
67
|
reducedMotion: props.reducedMotion
|
|
62
68
|
});
|
|
@@ -76,6 +82,11 @@ export const FlowPlayer = defineComponent({
|
|
|
76
82
|
emit("playstatechange", state);
|
|
77
83
|
props.onPlayStateChange?.(state);
|
|
78
84
|
};
|
|
85
|
+
const onLoopChange = (event) => {
|
|
86
|
+
const state = event.detail;
|
|
87
|
+
emit("loopchange", state);
|
|
88
|
+
props.onLoopChange?.(state);
|
|
89
|
+
};
|
|
79
90
|
const onError = (event) => {
|
|
80
91
|
const detail = event.detail;
|
|
81
92
|
// Vue's error event already invokes the onError prop; an explicit callback would duplicate delivery.
|
|
@@ -84,20 +95,22 @@ export const FlowPlayer = defineComponent({
|
|
|
84
95
|
next.element.addEventListener("stepchange", onStepChange);
|
|
85
96
|
next.element.addEventListener("timeupdate", onTimeUpdate);
|
|
86
97
|
next.element.addEventListener("playstatechange", onPlayStateChange);
|
|
98
|
+
next.element.addEventListener("loopchange", onLoopChange);
|
|
87
99
|
next.element.addEventListener("error", onError);
|
|
88
100
|
return () => {
|
|
89
101
|
next.element.removeEventListener("stepchange", onStepChange);
|
|
90
102
|
next.element.removeEventListener("timeupdate", onTimeUpdate);
|
|
91
103
|
next.element.removeEventListener("playstatechange", onPlayStateChange);
|
|
104
|
+
next.element.removeEventListener("loopchange", onLoopChange);
|
|
92
105
|
next.element.removeEventListener("error", onError);
|
|
93
106
|
};
|
|
94
107
|
};
|
|
95
108
|
// Keep initialApplied across option rebuilds so initialTimeMs is not reapplied.
|
|
96
|
-
const mount = () => {
|
|
109
|
+
const mount = (previousLoop) => {
|
|
97
110
|
const host = hostRef.value;
|
|
98
111
|
if (!host || player)
|
|
99
112
|
return;
|
|
100
|
-
const next = createPlayer(host, props.document, currentOptions());
|
|
113
|
+
const next = createPlayer(host, props.document, { ...currentOptions(), loop: props.loop ?? previousLoop });
|
|
101
114
|
player = next;
|
|
102
115
|
optionsSnapshot = currentOptions();
|
|
103
116
|
cleanupEvents = attachEvents(next);
|
|
@@ -118,8 +131,9 @@ export const FlowPlayer = defineComponent({
|
|
|
118
131
|
const rebuild = () => {
|
|
119
132
|
if (!mounted || !player)
|
|
120
133
|
return;
|
|
134
|
+
const previousLoop = player.getState().loop;
|
|
121
135
|
unmount();
|
|
122
|
-
mount();
|
|
136
|
+
mount(previousLoop);
|
|
123
137
|
};
|
|
124
138
|
onMounted(() => {
|
|
125
139
|
mounted = true;
|
|
@@ -130,7 +144,7 @@ export const FlowPlayer = defineComponent({
|
|
|
130
144
|
return;
|
|
131
145
|
player.load(next);
|
|
132
146
|
});
|
|
133
|
-
watch(() => [props.theme, props.controls, props.reducedMotion, props.hoverFill], () => {
|
|
147
|
+
watch(() => [props.theme, props.controls, props.gifExport, props.reducedMotion, props.hoverFill], () => {
|
|
134
148
|
const next = currentOptions();
|
|
135
149
|
if (!mounted || sameOptions(optionsSnapshot, next))
|
|
136
150
|
return;
|
|
@@ -140,6 +154,7 @@ export const FlowPlayer = defineComponent({
|
|
|
140
154
|
if (player && speed !== undefined)
|
|
141
155
|
player.setSpeed(speed);
|
|
142
156
|
});
|
|
157
|
+
watch(() => props.loop, (loop) => player?.setLoop(loop ?? false));
|
|
143
158
|
onBeforeUnmount(() => {
|
|
144
159
|
mounted = false;
|
|
145
160
|
unmount();
|
|
@@ -156,6 +171,7 @@ function sameOptions(previous, next) {
|
|
|
156
171
|
return false;
|
|
157
172
|
return previous.theme === next.theme
|
|
158
173
|
&& previous.controls === next.controls
|
|
174
|
+
&& previous.gifExport === next.gifExport
|
|
159
175
|
&& previous.hoverFill === next.hoverFill
|
|
160
176
|
&& previous.reducedMotion === next.reducedMotion;
|
|
161
177
|
}
|
|
@@ -7,7 +7,11 @@ declare const HTMLElementBase: ElementBase;
|
|
|
7
7
|
/** Declarative options; autoplay defaults to false, speed to 1 and controls to true. */
|
|
8
8
|
export interface FlowPlayerElementOptions extends EvaluateOptions {
|
|
9
9
|
autoplay?: boolean;
|
|
10
|
+
/** Defaults to false; loop is a live playback preference, independent of GIF loopCount. */
|
|
11
|
+
loop?: boolean;
|
|
10
12
|
controls?: boolean;
|
|
13
|
+
/** Defaults to false. Enable GIF download controls; ignored when controls is false. */
|
|
14
|
+
gifExport?: boolean;
|
|
11
15
|
/** Defaults to false; true enables background feedback as well as borders on hover. */
|
|
12
16
|
hoverFill?: boolean;
|
|
13
17
|
speed?: number;
|
|
@@ -17,12 +21,12 @@ export type FlowPlayerElementErrorDetail = Partial<PlayerErrorDetail> & {
|
|
|
17
21
|
error: Error;
|
|
18
22
|
};
|
|
19
23
|
/**
|
|
20
|
-
* Custom-element adapter for attribute conversion, URL requests and
|
|
24
|
+
* Custom-element adapter for attribute conversion, URL requests and playback/error events.
|
|
21
25
|
* The document property may be assigned before connection; disconnect releases the player/requests but retains the document.
|
|
22
26
|
* Rendering/timeline behavior delegates to createPlayer; registration requires an explicit defineFlowPlayer call.
|
|
23
27
|
*/
|
|
24
28
|
export declare class FlowPlayerElement extends HTMLElementBase {
|
|
25
|
-
static readonly observedAttributes: readonly ["document", "src", "autoplay", "controls", "speed", "theme", "reduced-motion", "hover-fill"];
|
|
29
|
+
static readonly observedAttributes: readonly ["document", "src", "autoplay", "controls", "speed", "theme", "reduced-motion", "hover-fill", "gif-export", "loop"];
|
|
26
30
|
private player;
|
|
27
31
|
private playerHost;
|
|
28
32
|
private sourceAbort;
|
|
@@ -41,6 +45,9 @@ export declare class FlowPlayerElement extends HTMLElementBase {
|
|
|
41
45
|
/** Whether to autoplay; false pauses a mounted player. */
|
|
42
46
|
get autoplay(): boolean;
|
|
43
47
|
set autoplay(value: boolean);
|
|
48
|
+
/** Live loop mode, reflected by the loop attribute; changes retain time and play state. */
|
|
49
|
+
get loop(): boolean;
|
|
50
|
+
set loop(value: boolean);
|
|
44
51
|
/** Controls default on; an explicit false is retained in HTML as controls="false". */
|
|
45
52
|
get controls(): boolean;
|
|
46
53
|
set controls(value: boolean);
|
|
@@ -56,6 +63,9 @@ export declare class FlowPlayerElement extends HTMLElementBase {
|
|
|
56
63
|
/** Hover paints only borders by default. The hover-fill attribute enables background and content feedback. */
|
|
57
64
|
get hoverFill(): boolean;
|
|
58
65
|
set hoverFill(value: boolean);
|
|
66
|
+
/** Opt-in GIF controls, reflected by the gif-export attribute; defaults to false. */
|
|
67
|
+
get gifExport(): boolean;
|
|
68
|
+
set gifExport(value: boolean);
|
|
59
69
|
connectedCallback(): void;
|
|
60
70
|
disconnectedCallback(): void;
|
|
61
71
|
attributeChangedCallback(name: string, _oldValue: string | null, value: string | null): void;
|
|
@@ -69,6 +79,8 @@ export declare class FlowPlayerElement extends HTMLElementBase {
|
|
|
69
79
|
step(direction: 1 | -1): void;
|
|
70
80
|
/** Set the speed property, retaining it for future mounts; ignore non-finite or non-positive values. */
|
|
71
81
|
setSpeed(value: number): void;
|
|
82
|
+
/** Set loop mode and retain it for future mounts. */
|
|
83
|
+
setLoop(value: boolean): void;
|
|
72
84
|
/** Return undefined before mounting or after destruction. */
|
|
73
85
|
getState(): PlayerState | undefined;
|
|
74
86
|
/** Explicitly release the player and requests; the manual-destruction flag resets on the next connection. */
|
|
@@ -12,7 +12,7 @@ const HTMLElementBase = (typeof HTMLElement === "undefined"
|
|
|
12
12
|
}
|
|
13
13
|
: HTMLElement);
|
|
14
14
|
/**
|
|
15
|
-
* Custom-element adapter for attribute conversion, URL requests and
|
|
15
|
+
* Custom-element adapter for attribute conversion, URL requests and playback/error events.
|
|
16
16
|
* The document property may be assigned before connection; disconnect releases the player/requests but retains the document.
|
|
17
17
|
* Rendering/timeline behavior delegates to createPlayer; registration requires an explicit defineFlowPlayer call.
|
|
18
18
|
*/
|
|
@@ -25,7 +25,9 @@ export class FlowPlayerElement extends HTMLElementBase {
|
|
|
25
25
|
"speed",
|
|
26
26
|
"theme",
|
|
27
27
|
"reduced-motion",
|
|
28
|
-
"hover-fill"
|
|
28
|
+
"hover-fill",
|
|
29
|
+
"gif-export",
|
|
30
|
+
"loop"
|
|
29
31
|
];
|
|
30
32
|
player;
|
|
31
33
|
playerHost;
|
|
@@ -67,6 +69,13 @@ export class FlowPlayerElement extends HTMLElementBase {
|
|
|
67
69
|
setBooleanAttribute(this, "autoplay", value);
|
|
68
70
|
this.applyAutoplay();
|
|
69
71
|
}
|
|
72
|
+
/** Live loop mode, reflected by the loop attribute; changes retain time and play state. */
|
|
73
|
+
get loop() {
|
|
74
|
+
return readBooleanAttribute(this, "loop", false);
|
|
75
|
+
}
|
|
76
|
+
set loop(value) {
|
|
77
|
+
setBooleanAttribute(this, "loop", value);
|
|
78
|
+
}
|
|
70
79
|
/** Controls default on; an explicit false is retained in HTML as controls="false". */
|
|
71
80
|
get controls() {
|
|
72
81
|
return this.controlsOverride ?? readBooleanAttribute(this, "controls", true);
|
|
@@ -121,6 +130,13 @@ export class FlowPlayerElement extends HTMLElementBase {
|
|
|
121
130
|
set hoverFill(value) {
|
|
122
131
|
setBooleanAttribute(this, "hover-fill", value);
|
|
123
132
|
}
|
|
133
|
+
/** Opt-in GIF controls, reflected by the gif-export attribute; defaults to false. */
|
|
134
|
+
get gifExport() {
|
|
135
|
+
return readBooleanAttribute(this, "gif-export", false);
|
|
136
|
+
}
|
|
137
|
+
set gifExport(value) {
|
|
138
|
+
setBooleanAttribute(this, "gif-export", value);
|
|
139
|
+
}
|
|
124
140
|
connectedCallback() {
|
|
125
141
|
this.connected = true;
|
|
126
142
|
this.manuallyDestroyed = false;
|
|
@@ -166,8 +182,12 @@ export class FlowPlayerElement extends HTMLElementBase {
|
|
|
166
182
|
case "theme":
|
|
167
183
|
case "reduced-motion":
|
|
168
184
|
case "hover-fill":
|
|
185
|
+
case "gif-export":
|
|
169
186
|
this.rebuildForOptions();
|
|
170
187
|
break;
|
|
188
|
+
case "loop":
|
|
189
|
+
this.player?.setLoop(this.loop);
|
|
190
|
+
break;
|
|
171
191
|
case "speed":
|
|
172
192
|
this.player?.setSpeed(this.speed);
|
|
173
193
|
break;
|
|
@@ -198,6 +218,10 @@ export class FlowPlayerElement extends HTMLElementBase {
|
|
|
198
218
|
setSpeed(value) {
|
|
199
219
|
this.speed = value;
|
|
200
220
|
}
|
|
221
|
+
/** Set loop mode and retain it for future mounts. */
|
|
222
|
+
setLoop(value) {
|
|
223
|
+
this.loop = value;
|
|
224
|
+
}
|
|
201
225
|
/** Return undefined before mounting or after destruction. */
|
|
202
226
|
getState() {
|
|
203
227
|
return this.player?.getState();
|
|
@@ -307,9 +331,12 @@ export class FlowPlayerElement extends HTMLElementBase {
|
|
|
307
331
|
}
|
|
308
332
|
forwardPlayerEvents(host) {
|
|
309
333
|
this.removeForwardedEvents();
|
|
310
|
-
for (const name of ["stepchange", "timeupdate", "playstatechange", "error"]) {
|
|
334
|
+
for (const name of ["stepchange", "timeupdate", "playstatechange", "loopchange", "error"]) {
|
|
311
335
|
const listener = (event) => {
|
|
312
336
|
const source = event;
|
|
337
|
+
if (name === "loopchange") {
|
|
338
|
+
setBooleanAttribute(this, "loop", source.detail.loop);
|
|
339
|
+
}
|
|
313
340
|
this.dispatchEvent(new CustomEvent(name, {
|
|
314
341
|
detail: source.detail,
|
|
315
342
|
bubbles: true,
|
|
@@ -332,6 +359,8 @@ export class FlowPlayerElement extends HTMLElementBase {
|
|
|
332
359
|
playerOptions() {
|
|
333
360
|
return {
|
|
334
361
|
controls: this.controls,
|
|
362
|
+
loop: this.loop,
|
|
363
|
+
gifExport: this.gifExport,
|
|
335
364
|
hoverFill: this.hoverFill,
|
|
336
365
|
...(this.theme ? { theme: this.theme } : {}),
|
|
337
366
|
...(this.reducedMotion === undefined ? {} : { reducedMotion: this.reducedMotion })
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Built-in controls consume state and forward actions; the browser player owns evaluation and scheduling. */
|
|
2
1
|
/** Scene selection metadata with its global start time in milliseconds. */
|
|
3
2
|
interface SceneChoice {
|
|
4
3
|
id: string;
|
|
@@ -11,23 +10,26 @@ interface ControlActions {
|
|
|
11
10
|
step(direction: 1 | -1): void;
|
|
12
11
|
seek(timeMs: number): void;
|
|
13
12
|
setSpeed(speed: number): void;
|
|
13
|
+
toggleLoop(): void;
|
|
14
14
|
}
|
|
15
15
|
/** Player-state projection for controls; the scene list is replaced on document load. */
|
|
16
16
|
interface ControlState {
|
|
17
17
|
timeMs: number;
|
|
18
18
|
totalDuration: number;
|
|
19
19
|
playing: boolean;
|
|
20
|
+
loop: boolean;
|
|
20
21
|
speed: number;
|
|
21
22
|
sceneId: string | null;
|
|
22
23
|
scenes: readonly SceneChoice[];
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Persistent control DOM translates input to commands; render synchronizes labels, progress and disabled states.
|
|
26
|
-
* Own no clock or independent position;
|
|
27
|
+
* Own no clock or independent position; Time-related controls are disabled for static documents.
|
|
27
28
|
*/
|
|
28
29
|
export declare class PlayerControls {
|
|
29
30
|
readonly element: HTMLDivElement;
|
|
30
31
|
private readonly play;
|
|
32
|
+
private readonly loop;
|
|
31
33
|
private readonly previous;
|
|
32
34
|
private readonly next;
|
|
33
35
|
private readonly progress;
|
|
@@ -35,7 +37,9 @@ export declare class PlayerControls {
|
|
|
35
37
|
private readonly speed;
|
|
36
38
|
private sceneChoices;
|
|
37
39
|
constructor(owner: Document, actions: ControlActions);
|
|
38
|
-
/** Update existing controls;
|
|
40
|
+
/** Update existing controls; custom speeds join the default choices without changing committed state. */
|
|
39
41
|
render(state: ControlState): void;
|
|
42
|
+
/** Release menu listeners before the player removes its DOM. */
|
|
43
|
+
destroy(): void;
|
|
40
44
|
}
|
|
41
45
|
export type { SceneChoice };
|
package/dist/browser/controls.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
/** Built-in controls consume state and forward actions; the browser player owns evaluation and scheduling. */
|
|
2
|
+
import { SelectControl } from "./select-control.js";
|
|
1
3
|
const DEFAULT_SPEEDS = [0.5, 1, 1.5, 2];
|
|
2
4
|
/**
|
|
3
5
|
* Persistent control DOM translates input to commands; render synchronizes labels, progress and disabled states.
|
|
4
|
-
* Own no clock or independent position;
|
|
6
|
+
* Own no clock or independent position; Time-related controls are disabled for static documents.
|
|
5
7
|
*/
|
|
6
8
|
export class PlayerControls {
|
|
7
9
|
element;
|
|
8
10
|
play;
|
|
11
|
+
loop;
|
|
9
12
|
previous;
|
|
10
13
|
next;
|
|
11
14
|
progress;
|
|
@@ -16,6 +19,9 @@ export class PlayerControls {
|
|
|
16
19
|
this.element = owner.createElement("div");
|
|
17
20
|
this.element.className = "flowsvg-controls";
|
|
18
21
|
this.play = createButton(owner, "Play", actions.toggle);
|
|
22
|
+
this.play.className = "flowsvg-play-toggle";
|
|
23
|
+
this.loop = createButton(owner, "Loop", actions.toggleLoop);
|
|
24
|
+
this.loop.className = "flowsvg-loop-toggle";
|
|
19
25
|
this.previous = createButton(owner, "Previous step", () => actions.step(-1));
|
|
20
26
|
this.next = createButton(owner, "Next step", () => actions.step(1));
|
|
21
27
|
this.progress = owner.createElement("input");
|
|
@@ -24,39 +30,36 @@ export class PlayerControls {
|
|
|
24
30
|
this.progress.step = "any";
|
|
25
31
|
this.progress.setAttribute("aria-label", "Progress");
|
|
26
32
|
this.progress.addEventListener("input", () => actions.seek(Number(this.progress.value)));
|
|
27
|
-
this.scene =
|
|
28
|
-
|
|
29
|
-
const choice = this.sceneChoices?.find((scene) => scene.id === this.scene.value);
|
|
33
|
+
this.scene = new SelectControl(owner, "Scene", (value) => {
|
|
34
|
+
const choice = this.sceneChoices?.find((scene) => scene.id === value);
|
|
30
35
|
if (choice)
|
|
31
36
|
actions.seek(choice.startMs);
|
|
32
37
|
});
|
|
33
|
-
this.speed =
|
|
34
|
-
this.
|
|
35
|
-
this.element.append(this.previous, this.play, this.next, this.progress, this.scene, this.speed);
|
|
38
|
+
this.speed = new SelectControl(owner, "Speed", (value) => actions.setSpeed(Number(value)));
|
|
39
|
+
this.element.append(this.previous, this.play, this.next, this.progress, this.scene.element, this.speed.element, this.loop);
|
|
36
40
|
}
|
|
37
|
-
/** Update existing controls;
|
|
41
|
+
/** Update existing controls; custom speeds join the default choices without changing committed state. */
|
|
38
42
|
render(state) {
|
|
39
43
|
const isStatic = state.totalDuration === 0;
|
|
40
44
|
this.play.textContent = state.playing ? "Pause" : "Play";
|
|
41
45
|
this.play.setAttribute("aria-label", this.play.textContent);
|
|
42
|
-
this.play.disabled = isStatic || (!state.playing && state.timeMs >= state.totalDuration);
|
|
46
|
+
this.play.disabled = isStatic || (!state.playing && !state.loop && state.timeMs >= state.totalDuration);
|
|
47
|
+
this.loop.setAttribute("aria-pressed", String(state.loop));
|
|
48
|
+
this.loop.disabled = isStatic;
|
|
43
49
|
this.previous.disabled = isStatic || state.timeMs <= 0;
|
|
44
50
|
this.next.disabled = isStatic || state.timeMs >= state.totalDuration;
|
|
45
51
|
this.progress.disabled = isStatic;
|
|
46
52
|
this.progress.max = String(state.totalDuration);
|
|
47
53
|
this.progress.value = String(state.timeMs);
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
if (this.sceneChoices !== state.scenes) {
|
|
51
|
-
this.sceneChoices = state.scenes;
|
|
52
|
-
this.scene.replaceChildren(...state.scenes.map((scene) => createOption(this.scene.ownerDocument, scene.id, scene.label)));
|
|
53
|
-
}
|
|
54
|
-
this.scene.value = state.sceneId ?? "";
|
|
54
|
+
this.sceneChoices = state.scenes;
|
|
55
|
+
this.scene.render(state.scenes.map((scene) => ({ value: scene.id, label: scene.label })), state.sceneId ?? "", isStatic);
|
|
55
56
|
const speeds = [...new Set([...DEFAULT_SPEEDS, state.speed])].sort((left, right) => left - right);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
this.speed.render(speeds.map((speed) => ({ value: String(speed), label: `${speed}×` })), String(state.speed), isStatic);
|
|
58
|
+
}
|
|
59
|
+
/** Release menu listeners before the player removes its DOM. */
|
|
60
|
+
destroy() {
|
|
61
|
+
this.scene.destroy();
|
|
62
|
+
this.speed.destroy();
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
function createButton(owner, label, click) {
|
|
@@ -67,14 +70,3 @@ function createButton(owner, label, click) {
|
|
|
67
70
|
button.addEventListener("click", click);
|
|
68
71
|
return button;
|
|
69
72
|
}
|
|
70
|
-
function createSelect(owner, label) {
|
|
71
|
-
const select = owner.createElement("select");
|
|
72
|
-
select.setAttribute("aria-label", label);
|
|
73
|
-
return select;
|
|
74
|
-
}
|
|
75
|
-
function createOption(owner, value, label) {
|
|
76
|
-
const option = owner.createElement("option");
|
|
77
|
-
option.value = value;
|
|
78
|
-
option.textContent = label;
|
|
79
|
-
return option;
|
|
80
|
-
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional player GIF UI. One task owns its input, abort signal and download resources independently of playback.
|
|
3
|
+
* The browser exporter stays behind a dynamic import; portable builds replace only that import with an inline bridge.
|
|
4
|
+
*/
|
|
5
|
+
import type { DiagramDocument, ThemeName } from "../core/model.js";
|
|
6
|
+
interface ExportInput {
|
|
7
|
+
document: DiagramDocument;
|
|
8
|
+
theme: ThemeName;
|
|
9
|
+
}
|
|
10
|
+
/** Own the visible export state and one cancellable task; destroy silences asynchronous results and releases downloads. */
|
|
11
|
+
export declare class GifExportControls {
|
|
12
|
+
private readonly owner;
|
|
13
|
+
private readonly captureInput;
|
|
14
|
+
readonly element: HTMLSpanElement;
|
|
15
|
+
private readonly startButton;
|
|
16
|
+
private readonly cancelButton;
|
|
17
|
+
private readonly status;
|
|
18
|
+
private readonly progress;
|
|
19
|
+
private task;
|
|
20
|
+
private destroyed;
|
|
21
|
+
private readonly downloadUrls;
|
|
22
|
+
constructor(owner: Document, captureInput: () => ExportInput);
|
|
23
|
+
destroy(): void;
|
|
24
|
+
/** Reserve the slot and capture input before loading code so repeated clicks and document loads cannot race the import. */
|
|
25
|
+
private start;
|
|
26
|
+
private cancel;
|
|
27
|
+
private run;
|
|
28
|
+
private isActive;
|
|
29
|
+
private renderProgress;
|
|
30
|
+
private download;
|
|
31
|
+
}
|
|
32
|
+
export {};
|