@phin-tech/dottie 0.1.0 → 0.3.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/dist/dottie.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.1.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.3.0 | 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);
@@ -6,13 +6,13 @@ var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "s
6
6
  // src/model.ts
7
7
  var SUPPORTED_VERSION = 2;
8
8
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image"];
9
- var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold"];
9
+ var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
10
10
  var KNOWN_FIELDS = {
11
11
  file: ["version", "pages", "images"],
12
12
  page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline"],
13
13
  layer: ["id", "name", "visible", "locked"],
14
14
  rgba: ["r", "g", "b", "a"],
15
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "gridSize", "opacity"],
15
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "gridSize", "opacity", "dashOffset"],
16
16
  element: [
17
17
  "id",
18
18
  "kind",
@@ -42,7 +42,7 @@ var KNOWN_FIELDS = {
42
42
  endpoint: ["elementID", "port", "x", "y"],
43
43
  connector: ["start", "end", "routing", "startHead", "endHead"],
44
44
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
45
- track: ["element", "keys", "boil", "follow", "repeats"],
45
+ track: ["element", "keys", "boil", "follow", "repeats", "march"],
46
46
  keyframe: ["t", "x", "y", "width", "height", "rotation", "strokeWidth", "stroke", "fill", "visible", "reveal", "opacity", "ease"],
47
47
  follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
48
48
  repeat: ["start", "end", "count"],
@@ -50,6 +50,8 @@ var KNOWN_FIELDS = {
50
50
  marker: ["t", "name"]
51
51
  };
52
52
  var MAX_DURATION = 3600;
53
+ var MAX_MARCH = 1e4;
54
+ var clampMarch = (v) => v === void 0 || !Number.isFinite(v) || v === 0 ? null : Math.min(Math.max(v, -MAX_MARCH), MAX_MARCH);
53
55
  var BOIL_DRAWINGS = 3;
54
56
  var black = { r: 0.12, g: 0.12, b: 0.13, a: 1 };
55
57
  var clear = { r: 0, g: 0, b: 0, a: 0 };
@@ -107,7 +109,8 @@ var Reader = class {
107
109
  bowing: num(s.bowing) ?? 1,
108
110
  fontSize: num(s.fontSize) ?? 18,
109
111
  gridSize: num(s.gridSize) ?? null,
110
- opacity: num(s.opacity) ?? null
112
+ opacity: num(s.opacity) ?? null,
113
+ dashOffset: num(s.dashOffset) ?? null
111
114
  };
112
115
  }
113
116
  endpoint(v) {
@@ -230,7 +233,7 @@ var Reader = class {
230
233
  const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
231
234
  return repeatIsValid(rep) ? [rep] : [];
232
235
  }) : null;
233
- return { element, keys, boil: bool(v.boil) ?? null, follow, repeats };
236
+ return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
234
237
  }
235
238
  timeline(v) {
236
239
  if (!isObj(v)) return null;
@@ -2061,7 +2064,40 @@ function applyEase(ease, u) {
2061
2064
  return u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
2062
2065
  case "hold":
2063
2066
  return 0;
2064
- }
2067
+ case "spring":
2068
+ return spring(u);
2069
+ case "bounce":
2070
+ return bounce(u);
2071
+ case "elastic":
2072
+ return elastic(u);
2073
+ }
2074
+ }
2075
+ function spring(u) {
2076
+ if (u <= 0) return 0;
2077
+ if (u >= 1) return 1;
2078
+ const s = (u2) => 1 - Math.exp(-6 * u2) * (Math.cos(3 * Math.PI * u2) + 6 / (3 * Math.PI) * Math.sin(3 * Math.PI * u2));
2079
+ return s(u) + u * (1 - s(1));
2080
+ }
2081
+ function bounce(u) {
2082
+ if (u <= 0) return 0;
2083
+ if (u >= 1) return 1;
2084
+ const n = 7.5625, d = 2.75;
2085
+ if (u < 1 / d) return n * u * u;
2086
+ if (u < 2 / d) {
2087
+ const v2 = u - 1.5 / d;
2088
+ return n * v2 * v2 + 0.75;
2089
+ }
2090
+ if (u < 2.5 / d) {
2091
+ const v2 = u - 2.25 / d;
2092
+ return n * v2 * v2 + 0.9375;
2093
+ }
2094
+ const v = u - 2.625 / d;
2095
+ return n * v * v + 0.984375;
2096
+ }
2097
+ function elastic(u) {
2098
+ if (u <= 0) return 0;
2099
+ if (u >= 1) return 1;
2100
+ return Math.pow(2, -10 * u) * Math.sin((u * 10 - 0.75) * (2 * Math.PI / 3)) + 1;
2065
2101
  }
2066
2102
  var rem = (a, b) => a % b;
2067
2103
  function localTime(track, t) {
@@ -2096,11 +2132,12 @@ function value(track, t, get, base, lerp) {
2096
2132
  return lerp(get(b), get(a), applyEase(b.ease ?? "linear", u));
2097
2133
  }
2098
2134
  var lerpNum = (a, b, u) => a + (b - a) * u;
2135
+ var mix01 = (a, b, u) => Math.min(Math.max(a + (b - a) * u, 0), 1);
2099
2136
  var lerpColor = (a, b, u) => ({
2100
- r: a.r + (b.r - a.r) * u,
2101
- g: a.g + (b.g - a.g) * u,
2102
- b: a.b + (b.b - a.b) * u,
2103
- a: a.a + (b.a - a.a) * u
2137
+ r: mix01(a.r, b.r, u),
2138
+ g: mix01(a.g, b.g, u),
2139
+ b: mix01(a.b, b.b, u),
2140
+ a: mix01(a.a, b.a, u)
2104
2141
  });
2105
2142
  function num2(track, t, p, base) {
2106
2143
  return value(track, t, (k) => k[p], base, lerpNum);
@@ -2138,10 +2175,10 @@ function applyTrack(track, e, t) {
2138
2175
  if (e.kind === "connector") return e;
2139
2176
  e.x = num2(track, t, "x", e.x);
2140
2177
  e.y = num2(track, t, "y", e.y);
2141
- e.width = num2(track, t, "width", e.width);
2142
- e.height = num2(track, t, "height", e.height);
2178
+ e.width = Math.max(0, num2(track, t, "width", e.width));
2179
+ e.height = Math.max(0, num2(track, t, "height", e.height));
2143
2180
  if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num2(track, t, "rotation", angle(e));
2144
- e.style.strokeWidth = num2(track, t, "strokeWidth", e.style.strokeWidth);
2181
+ e.style.strokeWidth = Math.max(0, num2(track, t, "strokeWidth", e.style.strokeWidth));
2145
2182
  e.style.stroke = value(track, t, (k) => k.stroke, e.style.stroke, lerpColor);
2146
2183
  e.style.fill = value(track, t, (k) => k.fill, e.style.fill, lerpColor);
2147
2184
  return e;
@@ -2170,7 +2207,7 @@ function following(track, e, t, page) {
2170
2207
  if (!pathElement || pathElement.id === e.id) return e;
2171
2208
  const line2 = tracing(pathElement, page);
2172
2209
  const span = f.end - f.start;
2173
- let u = applyEase(f.ease ?? "easeInOut", span > 0 ? Math.min(1, (t - f.start) / span) : 1) * (f.laps ?? 1);
2210
+ let u = Math.min(Math.max(applyEase(f.ease ?? "easeInOut", span > 0 ? Math.min(1, (t - f.start) / span) : 1), 0), 1) * (f.laps ?? 1);
2174
2211
  if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
2175
2212
  if (f.reverse === true) u = 1 - u;
2176
2213
  const p = line2.point(u);
@@ -2184,6 +2221,16 @@ function following(track, e, t, page) {
2184
2221
  }
2185
2222
  return moved;
2186
2223
  }
2224
+ function marchOffset(tl, speed, period, t) {
2225
+ if (!(period > 0)) return 0;
2226
+ const d = clampTime(tl.duration);
2227
+ if (d > 0) {
2228
+ const laps = roundHalfAway(speed * d / period);
2229
+ if (laps !== 0) speed = laps * period / d;
2230
+ }
2231
+ const r = rem(speed * t, period);
2232
+ return r < 0 ? -r : r === 0 ? 0 : period - r;
2233
+ }
2187
2234
  function trackMap(tl) {
2188
2235
  const m = /* @__PURE__ */ new Map();
2189
2236
  for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
@@ -2216,6 +2263,11 @@ function resolved(page, t) {
2216
2263
  if (out === e) out = cloneElement(e);
2217
2264
  out.seed = boiled(e.seed, reseed);
2218
2265
  }
2266
+ const pattern = track?.march != null ? dashPattern(out.style.dash, out.style.strokeWidth) : null;
2267
+ if (track?.march != null && pattern) {
2268
+ if (out === e) out = cloneElement(e);
2269
+ out.style.dashOffset = marchOffset(timeline, track.march, pattern.reduce((a, b) => a + b, 0), t);
2270
+ }
2219
2271
  return out;
2220
2272
  });
2221
2273
  elements = elements.map((e, i) => hidden.has(e.id) ? page.elements[i] : e);
@@ -2258,7 +2310,7 @@ function cameraRect(tl, t) {
2258
2310
  if (!after) return rectOf(before);
2259
2311
  const u = applyEase(before.ease ?? "easeInOut", (t - before.t) / Math.max(after.t - before.t, 1e-9));
2260
2312
  const mix2 = (p, q) => p + (q - p) * u;
2261
- return { x: mix2(before.x, after.x), y: mix2(before.y, after.y), width: mix2(before.width, after.width), height: mix2(before.height, after.height) };
2313
+ return { x: mix2(before.x, after.x), y: mix2(before.y, after.y), width: Math.max(1, mix2(before.width, after.width)), height: Math.max(1, mix2(before.height, after.height)) };
2262
2314
  }
2263
2315
  var rectOf = (k) => ({ x: k.x, y: k.y, width: k.width, height: k.height });
2264
2316
  function cameraView(tl, t) {
@@ -2308,8 +2360,9 @@ function bounds(page, padding = 24) {
2308
2360
  return inset(rotatedBounds(e), -reach, -reach);
2309
2361
  };
2310
2362
  if (page.elements.length === 0) return { x: 0, y: 0, width: 200, height: 200 };
2363
+ const shown = page.elements.filter((e) => (e.style.opacity ?? 1) > 0);
2311
2364
  let r = null;
2312
- for (const e of page.elements) r = union(r, box(e));
2365
+ for (const e of shown.length ? shown : page.elements) r = union(r, box(e));
2313
2366
  return inset(r, -padding, -padding);
2314
2367
  }
2315
2368
  function plan(page) {
@@ -2393,6 +2446,7 @@ function svgElement(e, index, labelBackground) {
2393
2446
  let strokeAttrs = `stroke="${stroke}" stroke-width="${fmt(e.style.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round"`;
2394
2447
  const dash = dashPattern(e.style.dash, e.style.strokeWidth);
2395
2448
  if (dash) strokeAttrs += ` stroke-dasharray="${dash.map(fmt).join(" ")}"`;
2449
+ if (dash && e.style.dashOffset !== null && fmt(e.style.dashOffset) !== "0") strokeAttrs += ` stroke-dashoffset="${fmt(e.style.dashOffset)}"`;
2396
2450
  let text = e.text;
2397
2451
  const r = revealProgress(e);
2398
2452
  if (r !== null) {
@@ -2923,6 +2977,7 @@ function inspectFrame(page, t) {
2923
2977
  if (e.rotation !== null) d.rotation = r4(e.rotation);
2924
2978
  if (e.style.opacity !== null) d.opacity = r4(e.style.opacity);
2925
2979
  if (e.reveal !== null) d.reveal = r4(e.reveal);
2980
+ if (e.style.dashOffset !== null) d.dashOffset = r4(e.style.dashOffset);
2926
2981
  d.strokeWidth = r4(e.style.strokeWidth);
2927
2982
  d.stroke = rgba2(e.style.stroke);
2928
2983
  d.fill = rgba2(e.style.fill);
@@ -2969,7 +3024,7 @@ function render(source, options = {}) {
2969
3024
  function isFile(v) {
2970
3025
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
2971
3026
  }
2972
- var VERSION = "0.1.0";
3027
+ var VERSION = "0.3.0";
2973
3028
  export {
2974
3029
  BOIL_DRAWINGS,
2975
3030
  DottieFormatError,
@@ -2977,6 +3032,7 @@ export {
2977
3032
  KINDS,
2978
3033
  KNOWN_FIELDS,
2979
3034
  MAX_DURATION,
3035
+ MAX_MARCH,
2980
3036
  Playback,
2981
3037
  SUPPORTED_VERSION,
2982
3038
  VERSION,
@@ -2984,6 +3040,7 @@ export {
2984
3040
  bounds,
2985
3041
  cameraView,
2986
3042
  clampFPS,
3043
+ clampMarch,
2987
3044
  clampTime,
2988
3045
  clear,
2989
3046
  cloneElement,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ /** What follows the runtime; DOTTIE_DATA and DOTTIE_OPTIONS are replaced with JSON. Plain ES5, unminified. */
2
+ export declare const LOADER = ";(function () {\n var data = DOTTIE_DATA, options = DOTTIE_OPTIONS;\n var script = document.currentScript;\n function attr(el, name, value) { el.setAttribute(name.replace(/[A-Z]/g, function (c) { return \"-\" + c.toLowerCase(); }), value); }\n function mount() {\n var player = document.createElement(\"dottie-player\"), k;\n for (k in options) attr(player, k, options[k]);\n if (script) for (k in script.dataset) if (k !== \"target\") attr(player, k, script.dataset[k]);\n var target = script && script.dataset.target ? document.querySelector(script.dataset.target) : null;\n if (target) target.appendChild(player);\n else if (script && script.parentNode && script.parentNode !== document.head) script.parentNode.insertBefore(player, script);\n else document.body.appendChild(player);\n player.setFile(window.Dottie.parseDottie(data));\n }\n if (document.readyState === \"loading\" && (!script || script.parentNode === document.head || script.dataset.target)) {\n document.addEventListener(\"DOMContentLoaded\", mount);\n } else {\n mount();\n }\n})();\n";
3
+ export interface CompileOptions {
4
+ /** Page name or 1-based number; only that page goes in. The first page by default. */
5
+ page?: string | number;
6
+ /** Show this moment (seconds) as a still. */
7
+ at?: number;
8
+ clean?: boolean;
9
+ noControls?: boolean;
10
+ noAutoplay?: boolean;
11
+ noLoop?: boolean;
12
+ label?: string;
13
+ }
14
+ /** JSON safe inside a script: it can't close the tag or start a comment. */
15
+ export declare function scriptJSON(value: unknown): string;
16
+ /** The player's attributes for `options`, as both compilers write them. */
17
+ export declare function playerAttributes(options: CompileOptions): Record<string, string>;
18
+ /** The compiled script for `source` (a .dottie file's text or parsed JSON). `runtime` is dottie-player.min.js. */
19
+ export declare function compile(source: string | unknown, runtime: string, options?: CompileOptions): string;
@@ -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.1.0";
21
+ export declare const VERSION = "0.3.0";
@@ -10,7 +10,7 @@ export type ElementKind = "rectangle" | "ellipse" | "diamond" | "text" | "connec
10
10
  export type Port = "top" | "right" | "bottom" | "left";
11
11
  export type Routing = "straight" | "curved" | "elbow";
12
12
  export type Arrowhead = "none" | "arrow" | "triangle" | "dot" | "bar";
13
- export type Ease = "linear" | "easeIn" | "easeOut" | "easeInOut" | "hold";
13
+ export type Ease = "linear" | "easeIn" | "easeOut" | "easeInOut" | "hold" | "spring" | "bounce" | "elastic";
14
14
  export interface Style {
15
15
  stroke: RGBA;
16
16
  fill: RGBA;
@@ -22,6 +22,8 @@ export interface Style {
22
22
  fontSize: number;
23
23
  gridSize: number | null;
24
24
  opacity: number | null;
25
+ /** How far into its dash pattern a dashed or dotted line starts; marching ants set it on every frame. */
26
+ dashOffset: number | null;
25
27
  }
26
28
  export interface Endpoint {
27
29
  elementID: string | null;
@@ -114,6 +116,8 @@ export interface Track {
114
116
  boil: boolean | null;
115
117
  follow: Follow[] | null;
116
118
  repeats: Repeat[] | null;
119
+ /** Marching ants: points per second the dashes travel (Track.march). */
120
+ march: number | null;
117
121
  }
118
122
  export interface CameraKey {
119
123
  t: number;
@@ -158,6 +162,9 @@ export declare const KINDS: readonly string[];
158
162
  * in a file is newer than this version and is reported in `warnings`. */
159
163
  export declare const KNOWN_FIELDS: Record<string, readonly string[]>;
160
164
  export declare const MAX_DURATION = 3600;
165
+ /** The fastest march, points per second (Track.maxMarch). */
166
+ export declare const MAX_MARCH = 10000;
167
+ export declare const clampMarch: (v: number | undefined) => number | null;
161
168
  export declare const BOIL_DRAWINGS = 3;
162
169
  export declare const black: RGBA;
163
170
  export declare const clear: RGBA;
@@ -15,6 +15,9 @@ export declare function followSpan(track: Track, t: number): Follow | null;
15
15
  export declare function tracing(e: Element, page: Page): Polyline;
16
16
  /** `e` moved onto the path it is following at `t`, if any. `page` has every key applied. */
17
17
  export declare function following(track: Track, e: Element, t: number, page: Page): Element;
18
+ /** Where the dashes of a line marching at `speed` stand at `t`, within one pattern `period` (Timeline.marchOffset):
19
+ * the speed is rounded to a whole number of patterns over the timeline, so a loop is seamless. */
20
+ export declare function marchOffset(tl: Timeline, speed: number, period: number, t: number): number;
18
21
  /** The page as a still drawing at `t` seconds (DottieDocument.resolved(at:)). A page without a timeline is returned as is. */
19
22
  export declare function resolved(page: Page, t: number): Page;
20
23
  /** The moment a still picture of the page shows: its poster, else the end. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phin-tech/dottie",
3
- "version": "0.1.0",
4
- "description": "Render and play Dottie (.dottie) drawings and animations in the browser",
3
+ "version": "0.3.0",
4
+ "description": "Render, play and compile Dottie (.dottie) drawings and animations in the browser",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -21,6 +21,10 @@
21
21
  "./player": {
22
22
  "types": "./dist/types/player-entry.d.ts",
23
23
  "default": "./dist/dottie-player.js"
24
+ },
25
+ "./compile": {
26
+ "types": "./dist/types/compile.d.ts",
27
+ "default": "./dist/compile.js"
24
28
  }
25
29
  },
26
30
  "files": [
@@ -64,5 +68,8 @@
64
68
  ],
65
69
  "engines": {
66
70
  "node": ">=18"
71
+ },
72
+ "bin": {
73
+ "dottie-compile": "dist/dottie-compile.mjs"
67
74
  }
68
75
  }