@phin-tech/dottie 0.5.4 → 0.6.1

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/dottie.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.5.4 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.6.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
4
4
  var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
@@ -33,6 +33,7 @@ var KNOWN_FIELDS = {
33
33
  "evenOdd",
34
34
  "rotation",
35
35
  "link",
36
+ "url",
36
37
  "anchor",
37
38
  "click",
38
39
  "hover",
@@ -41,12 +42,18 @@ var KNOWN_FIELDS = {
41
42
  "offset",
42
43
  "args",
43
44
  "image",
44
- "reveal"
45
+ "reveal",
46
+ "ports",
47
+ "labelRect",
48
+ "extension"
45
49
  ],
46
50
  strokePoint: ["x", "y"],
47
51
  subpath: ["closed", "coords"],
48
52
  inlineImage: ["data", "format"],
49
- endpoint: ["elementID", "port", "x", "y"],
53
+ endpoint: ["elementID", "port", "portIndex", "x", "y"],
54
+ extensionRef: ["id", "shape", "version", "params"],
55
+ normalizedPoint: ["x", "y"],
56
+ normalizedRect: ["x", "y", "width", "height"],
50
57
  connector: ["start", "end", "routing", "startHead", "endHead"],
51
58
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
52
59
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -85,6 +92,9 @@ function allowedURL(url) {
85
92
  if (!/^(https?:\/\/|mailto:)/i.test(u) || [...u].length <= 7 || /[\u0000-\u001f\u007f-\u009f]/.test(u)) return null;
86
93
  return u;
87
94
  }
95
+ function webLink(url) {
96
+ return url ? allowedURL(url) : null;
97
+ }
88
98
  function rgba(v) {
89
99
  if (!isObj(v)) return void 0;
90
100
  const r = num(v.r), g = num(v.g), b = num(v.b);
@@ -131,9 +141,11 @@ var Reader = class {
131
141
  endpoint(v) {
132
142
  const e = isObj(v) ? v : {};
133
143
  const port = str(e.port);
144
+ const portIndex = num(e.portIndex);
134
145
  return {
135
146
  elementID: uuid(e.elementID) ?? null,
136
147
  port: port && ["top", "right", "bottom", "left"].includes(port) ? port : null,
148
+ portIndex: portIndex !== void 0 && Number.isInteger(portIndex) && portIndex >= 0 ? portIndex : null,
137
149
  x: num(e.x) ?? 0,
138
150
  y: num(e.y) ?? 0
139
151
  };
@@ -198,6 +210,7 @@ var Reader = class {
198
210
  evenOdd: bool(v.evenOdd) ?? null,
199
211
  rotation: num(v.rotation) ?? null,
200
212
  link: uuid(v.link) ?? null,
213
+ url: str(v.url) ?? null,
201
214
  anchor: str(v.anchor) ?? null,
202
215
  click: this.click(v.click),
203
216
  hover: this.hover(v.hover),
@@ -206,9 +219,36 @@ var Reader = class {
206
219
  offset: num(v.offset) ?? null,
207
220
  args: strings(v.args),
208
221
  image: this.image(v.image, images),
209
- reveal: num(v.reveal) ?? null
222
+ reveal: num(v.reveal) ?? null,
223
+ ports: Array.isArray(v.ports) ? v.ports.map((p) => {
224
+ const o = isObj(p) ? p : {};
225
+ return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
226
+ }) : null,
227
+ labelRect: this.labelRect(v.labelRect),
228
+ extension: this.extension(v.extension)
210
229
  };
211
230
  }
231
+ labelRect(v) {
232
+ if (!isObj(v)) return null;
233
+ this.fields(v, "normalizedRect");
234
+ return { x: num(v.x) ?? 0, y: num(v.y) ?? 0, width: num(v.width) ?? 0, height: num(v.height) ?? 0 };
235
+ }
236
+ extension(v) {
237
+ if (!isObj(v)) return null;
238
+ this.fields(v, "extensionRef");
239
+ const id = str(v.id), shape = str(v.shape), version = str(v.version);
240
+ if (id === void 0 || shape === void 0 || version === void 0) {
241
+ this.warn("Skipped an extension without an id, shape or version");
242
+ return null;
243
+ }
244
+ const params = {};
245
+ if (isObj(v.params)) {
246
+ for (const [k, p] of Object.entries(v.params)) {
247
+ if (typeof p === "number" || typeof p === "boolean" || typeof p === "string") params[k] = p;
248
+ }
249
+ }
250
+ return { id, shape, version, params };
251
+ }
212
252
  click(v) {
213
253
  if (!isObj(v)) return null;
214
254
  this.fields(v, "click");
@@ -714,12 +754,45 @@ function portPoint(shape, port) {
714
754
  const p = port === "top" ? { x: midX(r), y: r.y } : port === "right" ? { x: maxX(r), y: midY(r) } : port === "bottom" ? { x: midX(r), y: maxY(r) } : { x: r.x, y: midY(r) };
715
755
  return rotated(shape, p);
716
756
  }
757
+ function flattenContour(cp) {
758
+ const out = [];
759
+ for (let s = 0; s + 3 < cp.length; s += 3) {
760
+ const seg = cp.slice(s, s + 4);
761
+ for (let k = 0; k < 16; k++) out.push(bezierPoint(seg, k / 16));
762
+ }
763
+ out.push(cp[cp.length - 1]);
764
+ return out;
765
+ }
766
+ function outlineCrossing(shape, c, d) {
767
+ let nearest2 = null, farthest = null;
768
+ for (const contour of shapeContours(shape)) {
769
+ const pts = flattenContour(contour.points);
770
+ const n = pts.length;
771
+ const edges = contour.closed ? n : n - 1;
772
+ for (let i = 0; i < edges; i++) {
773
+ const a = pts[i], b = pts[(i + 1) % n];
774
+ const e = { x: b.x - a.x, y: b.y - a.y }, w = { x: a.x - c.x, y: a.y - c.y };
775
+ const det = e.x * d.y - d.x * e.y;
776
+ if (det === 0) continue;
777
+ const t = (e.x * w.y - e.y * w.x) / det, s = (d.x * w.y - d.y * w.x) / det;
778
+ if (!(t > 0 && s >= 0 && s <= 1)) continue;
779
+ if (nearest2 === null || t < nearest2) nearest2 = t;
780
+ if (t <= 1 && (farthest === null || t > farthest)) farthest = t;
781
+ }
782
+ }
783
+ const best = farthest ?? nearest2;
784
+ return best === null ? null : { x: c.x + d.x * best, y: c.y + d.y * best };
785
+ }
717
786
  function edgePoint(shape, toward) {
718
787
  const r = frame(shape);
719
788
  const c = center(shape);
720
789
  const target = unrotated(shape, toward);
721
790
  const dx = target.x - c.x, dy = target.y - c.y;
722
791
  if (!((dx !== 0 || dy !== 0) && r.width > 0 && r.height > 0)) return c;
792
+ if (shape.kind === "path") {
793
+ const hit = outlineCrossing(shape, c, { x: dx, y: dy });
794
+ if (hit) return rotated(shape, hit);
795
+ }
723
796
  const hw = r.width / 2, hh = r.height / 2;
724
797
  let t;
725
798
  if (shape.kind === "ellipse") t = 1 / Math.sqrt(dx * dx / (hw * hw) + dy * dy / (hh * hh));
@@ -731,9 +804,15 @@ function reference(ep, index) {
731
804
  const s = ep.elementID ? index.get(ep.elementID) : void 0;
732
805
  return s ? center(s) : { x: ep.x, y: ep.y };
733
806
  }
807
+ function normalizedPoint(shape, p) {
808
+ const r = frame(shape);
809
+ return rotated(shape, { x: r.x + p.x * r.width, y: r.y + p.y * r.height });
810
+ }
734
811
  function resolve(ep, other, index) {
735
812
  const s = ep.elementID ? index.get(ep.elementID) : void 0;
736
813
  if (!s) return { x: ep.x, y: ep.y };
814
+ const i = ep.portIndex, ports = s.ports ?? [];
815
+ if (typeof i === "number" && Number.isInteger(i) && i >= 0 && i < ports.length) return normalizedPoint(s, ports[i]);
737
816
  return ep.port ? portPoint(s, ep.port) : edgePoint(s, other);
738
817
  }
739
818
  function endpoints(e, index) {
@@ -1917,7 +1996,7 @@ function clickTargets(page, file) {
1917
1996
  continue;
1918
1997
  }
1919
1998
  }
1920
- const url = c?.url ? allowedURL(c.url) : null;
1999
+ const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
1921
2000
  if (url) {
1922
2001
  out.set(e.id, { url });
1923
2002
  continue;
@@ -2717,8 +2796,8 @@ function svgOpen(b, background, paint) {
2717
2796
  }
2718
2797
  return out;
2719
2798
  }
2720
- function svgParts(page, background, fixed) {
2721
- if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed);
2799
+ function svgParts(page, background, fixed, links = false) {
2800
+ if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links);
2722
2801
  const theme = themeNamed(page.theme);
2723
2802
  const bg = background ? theme?.background ?? background : null;
2724
2803
  const ink = theme?.ink ?? black;
@@ -2742,14 +2821,19 @@ function svgParts(page, background, fixed) {
2742
2821
  ${body}</g>
2743
2822
  `;
2744
2823
  }
2745
- parts.push({ key: e.id, markup: alpha < 1 ? `<g opacity="${fmt(alpha)}">
2824
+ if (alpha < 1) body = `<g opacity="${fmt(alpha)}">
2746
2825
  ${body}</g>
2747
- ` : body });
2826
+ `;
2827
+ const href = links ? (e.click?.url ? allowedURL(e.click.url) : null) ?? webLink(e.url) : null;
2828
+ if (href) body = `<a href="${escapeXML(href).replace(/"/g, "&quot;")}" target="_blank">
2829
+ ${body}</a>
2830
+ `;
2831
+ parts.push({ key: e.id, markup: body });
2748
2832
  }
2749
2833
  return { bounds: b, parts };
2750
2834
  }
2751
2835
  function svg(page, background = { r: 1, g: 1, b: 1, a: 1 }, fixed) {
2752
- const { bounds: b, parts } = svgParts(page, background, fixed);
2836
+ const { bounds: b, parts } = svgParts(page, background, fixed, true);
2753
2837
  return svgOpen(b, themeNamed(page.theme)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
2754
2838
  }
2755
2839
  function labelRect(e, index) {
@@ -2844,7 +2928,7 @@ function svgElement(e, index, labelBackground) {
2844
2928
  }
2845
2929
  s += `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
2846
2930
  `;
2847
- if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, frame(e), e, true);
2931
+ if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" ? pathLabelBox(e) : frame(e), e, true);
2848
2932
  return s;
2849
2933
  }
2850
2934
  default:
@@ -2857,6 +2941,11 @@ function placeholder(e, strokeAttrs) {
2857
2941
  return `<path d="${p.data()}" fill="none" ${strokeAttrs} stroke-opacity="0.5" stroke-dasharray="6 4"/>
2858
2942
  ` + textElement(`${e.kind}${e.text ? `: ${e.text}` : ""}`, f, { ...e, style: { ...e.style, fontSize: Math.min(e.style.fontSize, 12) } }, true);
2859
2943
  }
2944
+ function pathLabelBox(e) {
2945
+ const f = frame(e), r = e.labelRect;
2946
+ if (!r) return f;
2947
+ return { x: f.x + r.x * f.width, y: f.y + r.y * f.height, width: r.width * f.width, height: r.height * f.height };
2948
+ }
2860
2949
  function textElement(text, f, e, centered) {
2861
2950
  const size = e.style.fontSize;
2862
2951
  const lines = text.split("\n");
@@ -3555,7 +3644,7 @@ function render(source, options = {}) {
3555
3644
  function isFile(v) {
3556
3645
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
3557
3646
  }
3558
- var VERSION = "0.5.4";
3647
+ var VERSION = "0.6.1";
3559
3648
  export {
3560
3649
  BOIL_DRAWINGS,
3561
3650
  DottieFormatError,
@@ -3603,5 +3692,6 @@ export {
3603
3692
  themeNamed,
3604
3693
  themes,
3605
3694
  transientElements,
3695
+ webLink,
3606
3696
  withRoughness
3607
3697
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -11,7 +11,7 @@ export type Click = {
11
11
  /** The marker called `name`: an exact match, else one that differs only in case. */
12
12
  export declare function sceneNamed(name: string, markers: Marker[]): number;
13
13
  /** The page's clickable elements and what each click does, by element id. A scene named by no marker, or a link
14
- * that isn't allowed, makes nothing clickable. Given the whole file, an element whose `link` points at an element on
14
+ * that isn't allowed, makes nothing clickable; an element with no `click` opens its `url` when that is a web link. Given the whole file, an element whose `link` points at an element on
15
15
  * another page shows that page (the HTML export holds one page, so it can't). */
16
16
  export declare function clickTargets(page: Page, file?: DottieFile): Map<string, Click>;
17
17
  /** `e` as it looks hovered (HoverLook.apply). */
@@ -18,4 +18,4 @@ export interface RenderOptions {
18
18
  /** One page of a drawing as an SVG document, as Dottie's own SVG export draws it. */
19
19
  export declare function render(source: string | DottieFile | unknown, options?: RenderOptions): string;
20
20
  /** The library's version. */
21
- export declare const VERSION = "0.5.4";
21
+ export declare const VERSION = "0.6.1";
@@ -26,12 +26,21 @@ export interface Style {
26
26
  /** How far into its dash pattern a dashed or dotted line starts; marching ants set it on every frame. */
27
27
  dashOffset: number | null;
28
28
  }
29
+ /** `portIndex` pins the end to one of the bound element's `ports`, and wins over `port`. */
29
30
  export interface Endpoint {
30
31
  elementID: string | null;
31
32
  port: Port | null;
33
+ portIndex?: number | null;
32
34
  x: number;
33
35
  y: number;
34
36
  }
37
+ /** The shape an extension generated a path with. Read-only here: nothing runs in dottie.js. */
38
+ export interface ExtensionRef {
39
+ id: string;
40
+ shape: string;
41
+ version: string;
42
+ params: Record<string, number | boolean | string>;
43
+ }
35
44
  export interface ConnectorData {
36
45
  start: Endpoint;
37
46
  end: Endpoint;
@@ -82,6 +91,9 @@ export interface Element {
82
91
  evenOdd: boolean | null;
83
92
  rotation: number | null;
84
93
  link: string | null;
94
+ /** Where the element links outside the drawing (Element.url): a web address, or another drawing. Only web
95
+ * addresses are followed here (`webLink`). */
96
+ url: string | null;
85
97
  anchor: string | null;
86
98
  click: ClickAction | null;
87
99
  hover: HoverLook | null;
@@ -94,6 +106,18 @@ export interface Element {
94
106
  /** Base64 bytes; an empty `data` is a picture that couldn't be found (drawn crossed out). */
95
107
  image: EmbeddedImage | null;
96
108
  reveal: number | null;
109
+ /** For an extension shape: connection points and label box, normalized to the frame. */
110
+ ports?: {
111
+ x: number;
112
+ y: number;
113
+ }[] | null;
114
+ labelRect?: {
115
+ x: number;
116
+ y: number;
117
+ width: number;
118
+ height: number;
119
+ } | null;
120
+ extension?: ExtensionRef | null;
97
121
  }
98
122
  export interface Layer {
99
123
  id: string;
@@ -211,6 +235,8 @@ export declare function clampFPS(f: number): number;
211
235
  /** `url` trimmed, if it is a link a page may open: http, https or mailto, as ClickAction.allowed. A drawing can
212
236
  * play on someone else's site, so nothing else (javascript:, data:) is ever followed. */
213
237
  export declare function allowedURL(url: string): string | null;
238
+ /** An element's `url`, if it is a web address (OutsideLink.webAddress); a link to another drawing gives null. */
239
+ export declare function webLink(url: string | null | undefined): string | null;
214
240
  export declare function repeatIsValid(r: Repeat): boolean;
215
241
  /** Sorts in place by `key`, keeping equal keys in their original order (as Swift's sort happens to for these). */
216
242
  export declare function stableSortBy<T>(a: T[], key: (v: T) => number): void;
@@ -23,8 +23,10 @@ export interface Part {
23
23
  key: string;
24
24
  markup: string;
25
25
  }
26
- /** The SVG for each thing drawn, back to front, keyed by element id (rock hatching as "rock"). */
27
- export declare function svgParts(page: Page, background: RGBA | null, fixed?: Rect): {
26
+ /** The SVG for each thing drawn, back to front, keyed by element id (rock hatching as "rock"). With `links`, an
27
+ * element whose click or `url` is a web address is wrapped in an `<a>` that opens it, the click's first (the players
28
+ * make their own clickable groups). */
29
+ export declare function svgParts(page: Page, background: RGBA | null, fixed?: Rect, links?: boolean): {
28
30
  bounds: Rect;
29
31
  parts: Part[];
30
32
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phin-tech/dottie",
3
- "version": "0.5.4",
3
+ "version": "0.6.1",
4
4
  "description": "Render, play and compile Dottie (.dottie) drawings and animations in the browser",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",