animot-presenter 0.6.3 → 0.6.5
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/dist/AnimotPresenter.svelte +380 -57
- package/dist/cdn/animot-presenter.css +1 -1
- package/dist/cdn/animot-presenter.esm.js +9840 -7600
- package/dist/cdn/animot-presenter.min.js +14 -10
- package/dist/styles/presenter.css +38 -0
- package/dist/types.d.ts +33 -2
- package/dist/utils/arrow-clip-draw.d.ts +6 -3
- package/dist/utils/arrow-clip-draw.js +78 -63
- package/dist/utils/freehand.d.ts +26 -0
- package/dist/utils/freehand.js +70 -0
- package/dist/utils/path-morph.d.ts +89 -0
- package/dist/utils/path-morph.js +420 -0
- package/dist/utils/svg-path-edit.d.ts +37 -0
- package/dist/utils/svg-path-edit.js +279 -0
- package/package.json +3 -1
|
@@ -217,6 +217,44 @@
|
|
|
217
217
|
0% { opacity: 1; } 100% { opacity: 0; }
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/* Freehand draw element */
|
|
221
|
+
.animot-draw-element { display: block; width: 100%; height: 100%; overflow: visible; }
|
|
222
|
+
|
|
223
|
+
/* Sticky note element */
|
|
224
|
+
.animot-sticky-element {
|
|
225
|
+
position: relative;
|
|
226
|
+
width: 100%;
|
|
227
|
+
height: 100%;
|
|
228
|
+
display: flex;
|
|
229
|
+
box-sizing: border-box;
|
|
230
|
+
overflow: hidden;
|
|
231
|
+
}
|
|
232
|
+
.animot-sticky-sheen {
|
|
233
|
+
position: absolute;
|
|
234
|
+
inset: 0;
|
|
235
|
+
pointer-events: none;
|
|
236
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0.06));
|
|
237
|
+
}
|
|
238
|
+
.animot-sticky-fold {
|
|
239
|
+
position: absolute;
|
|
240
|
+
right: 0;
|
|
241
|
+
bottom: 0;
|
|
242
|
+
width: 22px;
|
|
243
|
+
height: 22px;
|
|
244
|
+
pointer-events: none;
|
|
245
|
+
background: linear-gradient(135deg, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.18) 50%);
|
|
246
|
+
border-top-left-radius: 4px;
|
|
247
|
+
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.12);
|
|
248
|
+
}
|
|
249
|
+
.animot-sticky-text {
|
|
250
|
+
position: relative;
|
|
251
|
+
z-index: 1;
|
|
252
|
+
width: 100%;
|
|
253
|
+
line-height: 1.3;
|
|
254
|
+
white-space: pre-wrap;
|
|
255
|
+
word-break: break-word;
|
|
256
|
+
}
|
|
257
|
+
|
|
220
258
|
/* SVG path-trace animations (Icon + Svg elements) */
|
|
221
259
|
.animot-svg-element.icon-anim-draw path,
|
|
222
260
|
.animot-svg-element.icon-anim-draw circle,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ElementType = 'code' | 'text' | 'arrow' | 'image' | 'shape' | 'counter' | 'chart' | 'icon' | 'svg' | 'motionPath' | 'video' | 'progress' | 'container';
|
|
1
|
+
export type ElementType = 'code' | 'text' | 'arrow' | 'image' | 'shape' | 'counter' | 'chart' | 'icon' | 'svg' | 'motionPath' | 'video' | 'progress' | 'container' | 'draw' | 'sticky';
|
|
2
2
|
/**
|
|
3
3
|
* Per-element keyframe — animates state at a specific time WITHIN a slide's
|
|
4
4
|
* display window. Layered on top of slide-to-slide morphing: morphing handles
|
|
@@ -28,6 +28,7 @@ export interface Keyframe {
|
|
|
28
28
|
contrast?: number;
|
|
29
29
|
saturate?: number;
|
|
30
30
|
grayscale?: number;
|
|
31
|
+
path?: string;
|
|
31
32
|
}
|
|
32
33
|
export type ShapeType = 'rectangle' | 'circle' | 'triangle' | 'ellipse' | 'star' | 'hexagon';
|
|
33
34
|
export interface Position {
|
|
@@ -118,6 +119,8 @@ export interface BaseElement {
|
|
|
118
119
|
depth?: number;
|
|
119
120
|
motionPathId?: string;
|
|
120
121
|
motionPathConfig?: MotionPathConfig;
|
|
122
|
+
/** Solid color for the shape-morph transition only (render-time override). */
|
|
123
|
+
morphColor?: string;
|
|
121
124
|
blur?: number;
|
|
122
125
|
brightness?: number;
|
|
123
126
|
contrast?: number;
|
|
@@ -416,6 +419,34 @@ export interface SvgElement extends BaseElement {
|
|
|
416
419
|
viewBox?: string;
|
|
417
420
|
animation?: SvgPathAnimationConfig;
|
|
418
421
|
}
|
|
422
|
+
export type DrawBrush = 'pen' | 'marker' | 'highlighter';
|
|
423
|
+
export interface DrawElement extends BaseElement {
|
|
424
|
+
type: 'draw';
|
|
425
|
+
points: number[][];
|
|
426
|
+
color: string;
|
|
427
|
+
opacity: number;
|
|
428
|
+
brush: DrawBrush;
|
|
429
|
+
strokeWidth: number;
|
|
430
|
+
thinning?: number;
|
|
431
|
+
smoothing?: number;
|
|
432
|
+
streamline?: number;
|
|
433
|
+
taperStart?: boolean;
|
|
434
|
+
taperEnd?: boolean;
|
|
435
|
+
}
|
|
436
|
+
export interface StickyElement extends BaseElement {
|
|
437
|
+
type: 'sticky';
|
|
438
|
+
text: string;
|
|
439
|
+
bgColor: string;
|
|
440
|
+
textColor: string;
|
|
441
|
+
fontSize: number;
|
|
442
|
+
fontFamily: string;
|
|
443
|
+
fontWeight: number;
|
|
444
|
+
textAlign: 'left' | 'center' | 'right';
|
|
445
|
+
padding: number;
|
|
446
|
+
borderRadius: number;
|
|
447
|
+
opacity: number;
|
|
448
|
+
shadow?: boolean;
|
|
449
|
+
}
|
|
419
450
|
export interface MotionPathElement extends BaseElement {
|
|
420
451
|
type: 'motionPath';
|
|
421
452
|
points: PathPoint[];
|
|
@@ -480,7 +511,7 @@ export interface ContainerElement extends BaseElement {
|
|
|
480
511
|
borderWidth?: number;
|
|
481
512
|
borderRadius?: number;
|
|
482
513
|
}
|
|
483
|
-
export type CanvasElement = CodeElement | TextElement | ArrowElement | ImageElement | VideoElement | ShapeElement | CounterElement | ChartElement | IconElement | SvgElement | MotionPathElement | ProgressElement | ContainerElement;
|
|
514
|
+
export type CanvasElement = CodeElement | TextElement | ArrowElement | ImageElement | VideoElement | ShapeElement | CounterElement | ChartElement | IconElement | SvgElement | MotionPathElement | ProgressElement | ContainerElement | DrawElement | StickyElement;
|
|
484
515
|
export type ParticleShape = 'circle' | 'square' | 'star' | 'triangle';
|
|
485
516
|
export interface ParticlesConfig {
|
|
486
517
|
enabled: boolean;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Svelte action for arrow animations. Two strategies depending on mode:
|
|
3
3
|
*
|
|
4
|
-
* • 'draw' / 'undraw' / 'draw-undraw' —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* • 'draw' / 'undraw' / 'draw-undraw' — reveals the stroke ALONG THE PATH using
|
|
5
|
+
* stroke-dasharray/offset keyed to the path's total length. This follows the
|
|
6
|
+
* real geometry, so curved, looping and spiral arrows draw the way they're
|
|
7
|
+
* shaped (a linear clip-path wipe could only sweep left↔right / top↔bottom).
|
|
8
|
+
* The arrowhead (a separate sub-path) is revealed in step. Any dashed/dotted
|
|
9
|
+
* pattern is overridden during the reveal and restored when fully drawn.
|
|
7
10
|
*
|
|
8
11
|
* • 'flow' — marching ants. Continuously shifts stroke-dashoffset on the
|
|
9
12
|
* inner .arrow-path, so the dash pattern appears to flow along the path
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Svelte action for arrow animations. Two strategies depending on mode:
|
|
3
3
|
*
|
|
4
|
-
* • 'draw' / 'undraw' / 'draw-undraw' —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* • 'draw' / 'undraw' / 'draw-undraw' — reveals the stroke ALONG THE PATH using
|
|
5
|
+
* stroke-dasharray/offset keyed to the path's total length. This follows the
|
|
6
|
+
* real geometry, so curved, looping and spiral arrows draw the way they're
|
|
7
|
+
* shaped (a linear clip-path wipe could only sweep left↔right / top↔bottom).
|
|
8
|
+
* The arrowhead (a separate sub-path) is revealed in step. Any dashed/dotted
|
|
9
|
+
* pattern is overridden during the reveal and restored when fully drawn.
|
|
7
10
|
*
|
|
8
11
|
* • 'flow' — marching ants. Continuously shifts stroke-dashoffset on the
|
|
9
12
|
* inner .arrow-path, so the dash pattern appears to flow along the path
|
|
@@ -13,55 +16,37 @@
|
|
|
13
16
|
export function arrowClipDraw(node, params) {
|
|
14
17
|
let raf = 0;
|
|
15
18
|
let pathEl = null;
|
|
19
|
+
let headEl = null;
|
|
16
20
|
let baseDash = '';
|
|
17
21
|
let baseOffset = '';
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
function restorePath() {
|
|
23
|
+
// Back to the element's natural (fully-drawn) appearance.
|
|
24
|
+
if (pathEl) {
|
|
25
|
+
pathEl.style.strokeDasharray = baseDash;
|
|
26
|
+
pathEl.style.strokeDashoffset = baseOffset;
|
|
27
|
+
}
|
|
28
|
+
if (headEl)
|
|
29
|
+
headEl.style.opacity = '';
|
|
23
30
|
}
|
|
24
31
|
function reset() {
|
|
25
32
|
if (raf)
|
|
26
33
|
cancelAnimationFrame(raf);
|
|
27
34
|
raf = 0;
|
|
28
35
|
node.style.clipPath = '';
|
|
29
|
-
|
|
36
|
+
restorePath();
|
|
30
37
|
}
|
|
31
38
|
function run() {
|
|
32
39
|
reset();
|
|
33
40
|
if (!params.enabled || params.mode === 'none')
|
|
34
41
|
return;
|
|
35
|
-
// Capture the inner arrow path (used by 'flow' mode).
|
|
36
42
|
pathEl = node.querySelector('.arrow-path');
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// arrows reveal left↔right. clip-path inset(top right bottom left).
|
|
43
|
-
const dx = params.endX - params.startX;
|
|
44
|
-
const dy = params.endY - params.startY;
|
|
45
|
-
const horizontal = Math.abs(dx) >= Math.abs(dy);
|
|
46
|
-
const positive = horizontal ? dx >= 0 : dy >= 0;
|
|
47
|
-
const goesPositive = params.reverse ? !positive : positive;
|
|
48
|
-
// Build the clip-path string for a given progress (0 = fully hidden,
|
|
49
|
-
// 100 = fully visible) on the dominant axis.
|
|
50
|
-
function clip(insetPct) {
|
|
51
|
-
if (horizontal) {
|
|
52
|
-
return goesPositive
|
|
53
|
-
? `inset(0 ${insetPct}% 0 0)` // hide from right edge
|
|
54
|
-
: `inset(0 0 0 ${insetPct}%)`; // hide from left edge
|
|
55
|
-
}
|
|
56
|
-
return goesPositive
|
|
57
|
-
? `inset(0 0 ${insetPct}% 0)` // hide from bottom
|
|
58
|
-
: `inset(${insetPct}% 0 0 0)`; // hide from top
|
|
59
|
-
}
|
|
60
|
-
const clipFull = clip(100); // fully hidden (100% inset on hide side)
|
|
61
|
-
const clipNone = clip(0);
|
|
43
|
+
headEl = node.querySelector('.arrow-head');
|
|
44
|
+
if (!pathEl)
|
|
45
|
+
return;
|
|
46
|
+
baseDash = pathEl.style.strokeDasharray || '';
|
|
47
|
+
baseOffset = pathEl.style.strokeDashoffset || '';
|
|
62
48
|
// Round duration so an integer number of cycles fits in slide_duration.
|
|
63
|
-
// Without this, GIF loop boundary
|
|
64
|
-
// invisible → re-draw, which the user perceives as a reset.
|
|
49
|
+
// Without this, a GIF loop boundary can show the arrow mid-draw → snap.
|
|
65
50
|
const requested = Math.max(50, params.duration);
|
|
66
51
|
const dur = params.slideDuration && params.slideDuration > 0 && (params.loop || params.mode === 'flow')
|
|
67
52
|
? params.slideDuration / Math.max(1, Math.round(params.slideDuration / requested))
|
|
@@ -70,21 +55,15 @@ export function arrowClipDraw(node, params) {
|
|
|
70
55
|
const m = params.mode;
|
|
71
56
|
// FLOW: marching ants — animate dashoffset continuously, keep base pattern.
|
|
72
57
|
if (m === 'flow') {
|
|
73
|
-
if (!pathEl)
|
|
74
|
-
return;
|
|
75
|
-
// If the arrow has no inline dasharray (i.e. it's solid), give it one
|
|
76
|
-
// so dashes are visible to flow. Otherwise keep the user's pattern.
|
|
77
58
|
const dashAttr = pathEl.getAttribute('stroke-dasharray');
|
|
78
59
|
if (!dashAttr || dashAttr === 'none') {
|
|
79
60
|
pathEl.style.strokeDasharray = '8 5';
|
|
80
61
|
}
|
|
81
|
-
// One pixel of dashoffset shift per ms feels like a steady current; use
|
|
82
|
-
// `duration` as the ms per cycle of 24px (one base dash repeat-ish).
|
|
83
62
|
const cycle = 24;
|
|
84
63
|
const dir = params.reverse ? 1 : -1;
|
|
85
64
|
function flowStep(now) {
|
|
86
65
|
const elapsed = now - start;
|
|
87
|
-
const offset = (dir * (elapsed / dur) * cycle) % (cycle * 1000);
|
|
66
|
+
const offset = (dir * (elapsed / dur) * cycle) % (cycle * 1000);
|
|
88
67
|
if (pathEl)
|
|
89
68
|
pathEl.style.strokeDashoffset = String(offset);
|
|
90
69
|
raf = requestAnimationFrame(flowStep);
|
|
@@ -92,59 +71,95 @@ export function arrowClipDraw(node, params) {
|
|
|
92
71
|
raf = requestAnimationFrame(flowStep);
|
|
93
72
|
return;
|
|
94
73
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
74
|
+
// PATH-LENGTH REVEAL for draw / undraw / draw-undraw.
|
|
75
|
+
let len = 0;
|
|
76
|
+
try {
|
|
77
|
+
len = pathEl.getTotalLength();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
len = 0;
|
|
81
|
+
}
|
|
82
|
+
if (!len) {
|
|
83
|
+
// Degenerate path — nothing sensible to animate.
|
|
84
|
+
restorePath();
|
|
85
|
+
return;
|
|
98
86
|
}
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
// dashoffset reveals from the path start (positive) or the path end
|
|
88
|
+
// (negative) toward the other end, so `reverse` flips the draw direction.
|
|
89
|
+
const hideOffset = params.reverse ? -len : len;
|
|
90
|
+
// p: 0 = nothing drawn, 1 = fully drawn (along the path).
|
|
91
|
+
function setProgress(p) {
|
|
92
|
+
if (!pathEl)
|
|
93
|
+
return;
|
|
94
|
+
pathEl.style.strokeDasharray = `${len} ${len}`;
|
|
95
|
+
pathEl.style.strokeDashoffset = String(hideOffset * (1 - p));
|
|
96
|
+
}
|
|
97
|
+
// Head only shows once the end of the line is reached.
|
|
98
|
+
function setHead(p) {
|
|
99
|
+
if (headEl)
|
|
100
|
+
headEl.style.opacity = p >= 0.999 ? '1' : '0';
|
|
101
|
+
}
|
|
102
|
+
const cubicOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
103
|
+
// Initial frame.
|
|
104
|
+
if (m === 'undraw') {
|
|
105
|
+
setProgress(1);
|
|
106
|
+
setHead(1);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
setProgress(0);
|
|
110
|
+
setHead(0);
|
|
101
111
|
}
|
|
102
112
|
function step(now) {
|
|
103
113
|
const elapsed = now - start;
|
|
104
114
|
if (m === 'draw') {
|
|
105
115
|
const t = Math.min(elapsed / dur, 1);
|
|
106
|
-
const
|
|
107
|
-
|
|
116
|
+
const p = cubicOut(t);
|
|
117
|
+
setProgress(p);
|
|
118
|
+
setHead(p);
|
|
108
119
|
if (t < 1)
|
|
109
120
|
raf = requestAnimationFrame(step);
|
|
110
121
|
else if (params.loop)
|
|
111
122
|
run();
|
|
112
123
|
else {
|
|
113
|
-
|
|
124
|
+
restorePath();
|
|
114
125
|
raf = 0;
|
|
115
|
-
}
|
|
126
|
+
} // settle fully drawn (restores dashes)
|
|
116
127
|
}
|
|
117
128
|
else if (m === 'undraw') {
|
|
118
129
|
const t = Math.min(elapsed / dur, 1);
|
|
119
|
-
const
|
|
120
|
-
|
|
130
|
+
const p = 1 - cubicOut(t);
|
|
131
|
+
setProgress(p);
|
|
132
|
+
setHead(p);
|
|
121
133
|
if (t < 1)
|
|
122
134
|
raf = requestAnimationFrame(step);
|
|
123
135
|
else if (params.loop)
|
|
124
136
|
run();
|
|
125
137
|
else {
|
|
126
|
-
|
|
138
|
+
setProgress(0);
|
|
139
|
+
setHead(0);
|
|
127
140
|
raf = 0;
|
|
128
|
-
}
|
|
141
|
+
} // settle hidden
|
|
129
142
|
}
|
|
130
143
|
else if (m === 'draw-undraw') {
|
|
131
144
|
const half = dur / 2;
|
|
132
145
|
if (elapsed < half) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
const p = cubicOut(Math.min(elapsed / half, 1));
|
|
147
|
+
setProgress(p);
|
|
148
|
+
setHead(p);
|
|
136
149
|
raf = requestAnimationFrame(step);
|
|
137
150
|
}
|
|
138
151
|
else {
|
|
139
152
|
const t = Math.min((elapsed - half) / half, 1);
|
|
140
|
-
const
|
|
141
|
-
|
|
153
|
+
const p = 1 - cubicOut(t);
|
|
154
|
+
setProgress(p);
|
|
155
|
+
setHead(p);
|
|
142
156
|
if (t < 1)
|
|
143
157
|
raf = requestAnimationFrame(step);
|
|
144
158
|
else if (params.loop)
|
|
145
159
|
run();
|
|
146
160
|
else {
|
|
147
|
-
|
|
161
|
+
setProgress(0);
|
|
162
|
+
setHead(0);
|
|
148
163
|
raf = 0;
|
|
149
164
|
}
|
|
150
165
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DrawElement } from '../types';
|
|
2
|
+
export interface StrokeOptions {
|
|
3
|
+
size: number;
|
|
4
|
+
thinning: number;
|
|
5
|
+
smoothing: number;
|
|
6
|
+
streamline: number;
|
|
7
|
+
taperStart: boolean;
|
|
8
|
+
taperEnd: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveStrokeOptions(el: Pick<DrawElement, 'brush' | 'strokeWidth' | 'thinning' | 'smoothing' | 'streamline' | 'taperStart' | 'taperEnd'>): StrokeOptions;
|
|
11
|
+
/** Compute the axis-aligned bounding box of raw input points. */
|
|
12
|
+
export declare function pointsBounds(points: number[][]): {
|
|
13
|
+
minX: number;
|
|
14
|
+
minY: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Build the renderable path + viewBox for a draw element. Points are assumed
|
|
20
|
+
* to already be in a 0,0-origin local space; the viewBox spans their bounds
|
|
21
|
+
* (plus stroke padding) so the element box scales the stroke on resize.
|
|
22
|
+
*/
|
|
23
|
+
export declare function drawElementToPath(el: Pick<DrawElement, 'points' | 'brush' | 'strokeWidth' | 'thinning' | 'smoothing' | 'streamline' | 'taperStart' | 'taperEnd'>): {
|
|
24
|
+
d: string;
|
|
25
|
+
viewBox: string;
|
|
26
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import getStroke from 'perfect-freehand';
|
|
2
|
+
const BRUSH_DEFAULTS = {
|
|
3
|
+
// Tapered, pressure-responsive — feels like an ink pen.
|
|
4
|
+
pen: { thinning: 0.6, smoothing: 0.5, streamline: 0.5, taperStart: true, taperEnd: true },
|
|
5
|
+
// Flat, even width — a chisel marker.
|
|
6
|
+
marker: { thinning: 0.1, smoothing: 0.55, streamline: 0.5, taperStart: false, taperEnd: false },
|
|
7
|
+
// Even width like marker; the translucency comes from element opacity.
|
|
8
|
+
highlighter: { thinning: 0, smoothing: 0.6, streamline: 0.5, taperStart: false, taperEnd: false }
|
|
9
|
+
};
|
|
10
|
+
export function resolveStrokeOptions(el) {
|
|
11
|
+
const d = BRUSH_DEFAULTS[el.brush] ?? BRUSH_DEFAULTS.pen;
|
|
12
|
+
return {
|
|
13
|
+
size: el.strokeWidth,
|
|
14
|
+
thinning: el.thinning ?? d.thinning ?? 0.5,
|
|
15
|
+
smoothing: el.smoothing ?? d.smoothing ?? 0.5,
|
|
16
|
+
streamline: el.streamline ?? d.streamline ?? 0.5,
|
|
17
|
+
taperStart: el.taperStart ?? d.taperStart ?? false,
|
|
18
|
+
taperEnd: el.taperEnd ?? d.taperEnd ?? false
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Turn a perfect-freehand outline polygon into an SVG path `d` string. */
|
|
22
|
+
function outlineToPath(points) {
|
|
23
|
+
if (points.length === 0)
|
|
24
|
+
return '';
|
|
25
|
+
const d = points.reduce((acc, [x0, y0], i, arr) => {
|
|
26
|
+
const [x1, y1] = arr[(i + 1) % arr.length];
|
|
27
|
+
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
|
28
|
+
return acc;
|
|
29
|
+
}, ['M', points[0][0], points[0][1], 'Q']);
|
|
30
|
+
d.push('Z');
|
|
31
|
+
return d.join(' ');
|
|
32
|
+
}
|
|
33
|
+
/** Compute the axis-aligned bounding box of raw input points. */
|
|
34
|
+
export function pointsBounds(points) {
|
|
35
|
+
if (points.length === 0)
|
|
36
|
+
return { minX: 0, minY: 0, width: 0, height: 0 };
|
|
37
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
38
|
+
for (const [x, y] of points) {
|
|
39
|
+
if (x < minX)
|
|
40
|
+
minX = x;
|
|
41
|
+
if (y < minY)
|
|
42
|
+
minY = y;
|
|
43
|
+
if (x > maxX)
|
|
44
|
+
maxX = x;
|
|
45
|
+
if (y > maxY)
|
|
46
|
+
maxY = y;
|
|
47
|
+
}
|
|
48
|
+
return { minX, minY, width: maxX - minX, height: maxY - minY };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the renderable path + viewBox for a draw element. Points are assumed
|
|
52
|
+
* to already be in a 0,0-origin local space; the viewBox spans their bounds
|
|
53
|
+
* (plus stroke padding) so the element box scales the stroke on resize.
|
|
54
|
+
*/
|
|
55
|
+
export function drawElementToPath(el) {
|
|
56
|
+
const opts = resolveStrokeOptions(el);
|
|
57
|
+
const stroke = getStroke(el.points, {
|
|
58
|
+
size: opts.size,
|
|
59
|
+
thinning: opts.thinning,
|
|
60
|
+
smoothing: opts.smoothing,
|
|
61
|
+
streamline: opts.streamline,
|
|
62
|
+
start: { taper: opts.taperStart ? opts.size * 4 : 0, cap: !opts.taperStart },
|
|
63
|
+
end: { taper: opts.taperEnd ? opts.size * 4 : 0, cap: !opts.taperEnd }
|
|
64
|
+
});
|
|
65
|
+
const d = outlineToPath(stroke);
|
|
66
|
+
const b = pointsBounds(el.points);
|
|
67
|
+
const pad = opts.size;
|
|
68
|
+
const vb = `${b.minX - pad} ${b.minY - pad} ${b.width + pad * 2} ${b.height + pad * 2}`;
|
|
69
|
+
return { d, viewBox: vb };
|
|
70
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ShapeType, CanvasElement } from '../types';
|
|
2
|
+
/** Build a `d` string for a shape type, fitted to a w×h box (origin 0,0). */
|
|
3
|
+
export declare function shapeToPath(type: ShapeType, w: number, h: number, borderRadius?: number): string;
|
|
4
|
+
/**
|
|
5
|
+
* Extract drawable path `d` strings from raw SVG markup. Returns an array of
|
|
6
|
+
* subpath `d` strings (one per primitive). Empty when nothing is drawable.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractPaths(svgContent: string): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Like extractPaths, but pairs each subpath with its own fill color so a
|
|
11
|
+
* multi-color SVG morphs per-piece (one `<path>` per subpath) instead of
|
|
12
|
+
* flattening to a single representative color. Primitives without their own
|
|
13
|
+
* fill inherit the root `<svg fill="…">` (default black, matching the
|
|
14
|
+
* browser), which is exactly the black outline strokes on icon SVGs.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractPathsWithFill(svgContent: string): Array<{
|
|
17
|
+
d: string;
|
|
18
|
+
fill: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** Single combined `d` (all subpaths concatenated) — for simple morphs. */
|
|
21
|
+
export declare function extractCombinedPath(svgContent: string): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Pick a representative fill color from SVG markup — the first non-"none"
|
|
24
|
+
* `fill` attribute. Used to color the flattened morph path (which loses
|
|
25
|
+
* per-subpath fills). Returns null if none found.
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractFill(svgContent: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Re-fit a path `d` from its own viewBox into a target W×H box (origin 0,0).
|
|
30
|
+
* Morph stops must share a coordinate space or flubber produces scale jumps;
|
|
31
|
+
* authoring normalizes every stop into the element's box via this. Parses to
|
|
32
|
+
* absolute cubics, scales anchors + handles, re-serializes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function fitPathToBox(d: string, sourceViewBox: string, targetW: number, targetH: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve any vector element (shape / svg / draw) to its current geometry as
|
|
37
|
+
* a path `d` plus the viewBox that `d` lives in. Single source of truth for
|
|
38
|
+
* both the morph engine and the "snapshot current" authoring action.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveElementPath(el: CanvasElement): {
|
|
41
|
+
d: string;
|
|
42
|
+
viewBox: string;
|
|
43
|
+
} | null;
|
|
44
|
+
/**
|
|
45
|
+
* Like resolveElementPath but keeps subpaths SEPARATE, each with its own fill,
|
|
46
|
+
* so multi-color SVGs morph per-piece. Shapes/draws return a single part.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveElementPathParts(el: CanvasElement): {
|
|
49
|
+
parts: Array<{
|
|
50
|
+
d: string;
|
|
51
|
+
fill: string;
|
|
52
|
+
}>;
|
|
53
|
+
viewBox: string;
|
|
54
|
+
} | null;
|
|
55
|
+
export type PathInterpolator = (t: number) => string;
|
|
56
|
+
/** One morphing piece: a `d` interpolator plus the color it lerps across. */
|
|
57
|
+
export interface MorphPart {
|
|
58
|
+
interp: PathInterpolator;
|
|
59
|
+
fromColor: string;
|
|
60
|
+
toColor: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build per-piece morph parts between two fill-tagged subpath lists. Each `d`
|
|
64
|
+
* MUST already be in the same coordinate space (caller fits the source into
|
|
65
|
+
* the target viewBox). Picks the right flubber routine by count so 1↔many and
|
|
66
|
+
* many↔1 map correctly, and pairs colors per piece so multi-color SVGs keep
|
|
67
|
+
* their colors instead of flattening to one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function makeMorphParts(from: Array<{
|
|
70
|
+
d: string;
|
|
71
|
+
fill: string;
|
|
72
|
+
}>, to: Array<{
|
|
73
|
+
d: string;
|
|
74
|
+
fill: string;
|
|
75
|
+
}>): MorphPart[];
|
|
76
|
+
/**
|
|
77
|
+
* Build an interpolator between two `d` strings. Handles differing subpath
|
|
78
|
+
* counts via flubber's many-to-many interpolation, falling back to a simple
|
|
79
|
+
* single-path interpolate. Always returns a function (t:0..1) => d.
|
|
80
|
+
*/
|
|
81
|
+
export declare function makePathInterpolator(fromD: string, toD: string): PathInterpolator;
|
|
82
|
+
/**
|
|
83
|
+
* Build a single interpolator that morphs through an ordered list of `d`
|
|
84
|
+
* stops. `t` runs 0..1 across the whole sequence; the segment is picked by
|
|
85
|
+
* scaling `t` across (stops.length - 1) and interpolating within it. Used by
|
|
86
|
+
* the within-slide multi-target morph (circle → star → hexagon → …).
|
|
87
|
+
* When `loop` is true the sequence wraps back to the first stop.
|
|
88
|
+
*/
|
|
89
|
+
export declare function makeSequenceInterpolator(stops: string[], loop?: boolean): PathInterpolator;
|