effect-browser 0.2.0-beta.8 → 0.3.0-beta.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.
Files changed (202) hide show
  1. package/README.md +705 -1166
  2. package/dist/Browser-CM65Uo7f.mjs +437 -0
  3. package/dist/Browser-CM65Uo7f.mjs.map +1 -0
  4. package/dist/Browser.d.mts +115 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +202 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +295 -0
  13. package/dist/BrowserEvent.mjs.map +1 -0
  14. package/dist/Cdp.d.mts +32 -0
  15. package/dist/Cdp.mjs +61 -0
  16. package/dist/Cdp.mjs.map +1 -0
  17. package/dist/Change-DNvztNLu.mjs +138 -0
  18. package/dist/Change-DNvztNLu.mjs.map +1 -0
  19. package/dist/Change-DVP2IJWm.d.mts +71 -0
  20. package/dist/Change.d.mts +2 -0
  21. package/dist/Change.mjs +2 -0
  22. package/dist/Chromium.d.mts +29 -81
  23. package/dist/Chromium.mjs +57 -392
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Expect.d.mts +62 -0
  26. package/dist/Expect.mjs +114 -0
  27. package/dist/Expect.mjs.map +1 -0
  28. package/dist/Frame-BERmf_gM.d.mts +117 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +123 -0
  31. package/dist/Frame.mjs.map +1 -0
  32. package/dist/Moment.d.mts +2 -0
  33. package/dist/Moment.mjs +288 -0
  34. package/dist/Moment.mjs.map +1 -0
  35. package/dist/Motion.d.mts +40 -0
  36. package/dist/Motion.mjs +122 -0
  37. package/dist/Motion.mjs.map +1 -0
  38. package/dist/Page-D4281dIi.mjs +618 -0
  39. package/dist/Page-D4281dIi.mjs.map +1 -0
  40. package/dist/Page-DYTdVhYa.d.mts +593 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-CNNOU1Tx.d.mts +112 -0
  44. package/dist/Plan-Dtrb-8im.mjs +311 -0
  45. package/dist/Plan-Dtrb-8im.mjs.map +1 -0
  46. package/dist/Plan.d.mts +2 -3
  47. package/dist/Plan.mjs +2 -2
  48. package/dist/Policy.d.mts +76 -0
  49. package/dist/Policy.mjs +237 -0
  50. package/dist/Policy.mjs.map +1 -0
  51. package/dist/Presentation.d.mts +48 -0
  52. package/dist/Presentation.mjs +117 -0
  53. package/dist/Presentation.mjs.map +1 -0
  54. package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
  55. package/dist/Snapshot.d.mts +2 -0
  56. package/dist/Snapshot.mjs +79 -0
  57. package/dist/Snapshot.mjs.map +1 -0
  58. package/dist/Stage.d.mts +54 -0
  59. package/dist/Stage.mjs +133 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-BtEG93gR.mjs +420 -0
  62. package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
  63. package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-DAYjATgC.mjs +137 -0
  67. package/dist/context-DAYjATgC.mjs.map +1 -0
  68. package/dist/index.d.mts +17 -14
  69. package/dist/index.mjs +18 -13
  70. package/dist/match.inpage-xOe9LNO0.mjs +48 -0
  71. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
  72. package/dist/page-Banfm_zc.mjs +5097 -0
  73. package/dist/page-Banfm_zc.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-CcL0K7S0.mjs +262 -0
  76. package/dist/url-CcL0K7S0.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +625 -0
  79. package/src/BrowserError.ts +225 -0
  80. package/src/BrowserEvent.ts +294 -0
  81. package/src/Cdp.ts +101 -0
  82. package/src/Change.ts +88 -0
  83. package/src/Chromium.ts +102 -0
  84. package/src/Expect.ts +180 -0
  85. package/src/Frame.ts +130 -0
  86. package/src/Moment.ts +479 -0
  87. package/src/Motion.ts +166 -0
  88. package/src/Page.ts +520 -0
  89. package/src/Plan.ts +350 -0
  90. package/src/Policy.ts +325 -0
  91. package/src/Presentation.ts +195 -0
  92. package/src/Snapshot.ts +89 -0
  93. package/src/Stage.ts +223 -0
  94. package/src/Supervisor.ts +353 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +607 -0
  97. package/src/internal/input/dispatch.ts +161 -0
  98. package/src/internal/input/edit.inpage.ts +162 -0
  99. package/src/internal/input/evidence.inpage.ts +164 -0
  100. package/src/internal/input/guard.inpage.ts +605 -0
  101. package/src/internal/input/guard.ts +183 -0
  102. package/src/internal/input/keyboard.ts +112 -0
  103. package/src/internal/input/keys.ts +127 -0
  104. package/src/internal/input/perform.ts +322 -0
  105. package/src/internal/input/pointer.ts +153 -0
  106. package/src/internal/input/replies.ts +159 -0
  107. package/src/internal/input/settle.ts +50 -0
  108. package/src/internal/input/style.ts +186 -0
  109. package/src/internal/input/targets.inpage.ts +293 -0
  110. package/src/internal/input/targets.ts +148 -0
  111. package/src/internal/page/bridge.ts +386 -0
  112. package/src/internal/page/context.ts +302 -0
  113. package/src/internal/page/lane.ts +301 -0
  114. package/src/internal/page/navigation.ts +183 -0
  115. package/src/internal/page/openings.ts +87 -0
  116. package/src/internal/page/page.ts +137 -0
  117. package/src/internal/page/url.ts +100 -0
  118. package/src/internal/page/viewport.ts +43 -0
  119. package/src/internal/pictures/capture.ts +778 -0
  120. package/src/internal/pictures/clock.ts +141 -0
  121. package/src/internal/pictures/pictures.ts +520 -0
  122. package/src/internal/pictures/transport.ts +242 -0
  123. package/src/internal/reading/choose.ts +100 -0
  124. package/src/internal/reading/context.inpage.ts +201 -0
  125. package/src/internal/reading/controls.inpage.ts +158 -0
  126. package/src/internal/reading/expect.inpage.ts +70 -0
  127. package/src/internal/reading/match.inpage.ts +92 -0
  128. package/src/internal/reading/names.inpage.ts +306 -0
  129. package/src/internal/reading/outline.inpage.ts +260 -0
  130. package/src/internal/reading/reading.ts +223 -0
  131. package/src/internal/reading/ready.inpage.ts +154 -0
  132. package/src/internal/reading/ready.ts +105 -0
  133. package/src/internal/reading/subjects.inpage.ts +188 -0
  134. package/src/internal/reading/text.inpage.ts +135 -0
  135. package/src/internal/reading/walk.inpage.ts +220 -0
  136. package/src/internal/supervisor/lifecycle.ts +265 -0
  137. package/src/internal/timeline/changes.inpage.ts +216 -0
  138. package/src/internal/timeline/changes.ts +100 -0
  139. package/src/internal/timeline/events.ts +93 -0
  140. package/src/internal/timeline/fold.inpage.ts +123 -0
  141. package/src/internal/timeline/history.inpage.ts +229 -0
  142. package/src/internal/timeline/marks.inpage.ts +207 -0
  143. package/src/internal/timeline/record.inpage.ts +133 -0
  144. package/src/internal/timeline/sight.inpage.ts +122 -0
  145. package/src/internal/timeline/window.ts +96 -0
  146. package/src/internal/timeline/words.ts +65 -0
  147. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  148. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  149. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  150. package/dist/Bootstrap.d.mts +0 -3
  151. package/dist/Bootstrap.mjs +0 -2
  152. package/dist/Browser-n-bKgBNN.d.mts +0 -372
  153. package/dist/Browser.mjs.map +0 -1
  154. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  155. package/dist/BrowserData.d.mts +0 -3
  156. package/dist/BrowserData.mjs +0 -695
  157. package/dist/BrowserData.mjs.map +0 -1
  158. package/dist/BrowserRuntime-CFe9uUTv.d.mts +0 -148
  159. package/dist/BrowserRuntime-HJ1yYlFS.mjs +0 -6741
  160. package/dist/BrowserRuntime-HJ1yYlFS.mjs.map +0 -1
  161. package/dist/BrowserRuntime.d.mts +0 -3
  162. package/dist/BrowserRuntime.mjs +0 -3
  163. package/dist/Capture.d.mts +0 -56
  164. package/dist/Capture.mjs +0 -678
  165. package/dist/Capture.mjs.map +0 -1
  166. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  167. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  168. package/dist/CaptureData.d.mts +0 -219
  169. package/dist/CaptureData.mjs +0 -2
  170. package/dist/Errors.d.mts +0 -158
  171. package/dist/Errors.mjs +0 -207
  172. package/dist/Errors.mjs.map +0 -1
  173. package/dist/Images-CaaPNeed.mjs +0 -76
  174. package/dist/Images-CaaPNeed.mjs.map +0 -1
  175. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  176. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  177. package/dist/PageControl.d.mts +0 -15
  178. package/dist/PageControl.mjs +0 -34
  179. package/dist/PageControl.mjs.map +0 -1
  180. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  181. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  182. package/dist/Plan-D8qQ48Ic.mjs +0 -512
  183. package/dist/Plan-D8qQ48Ic.mjs.map +0 -1
  184. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  185. package/dist/PlanData.d.mts +0 -3
  186. package/dist/PlanData.mjs +0 -268
  187. package/dist/PlanData.mjs.map +0 -1
  188. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  189. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  190. package/dist/PublicSession-Bs61Eal8.mjs +0 -5601
  191. package/dist/PublicSession-Bs61Eal8.mjs.map +0 -1
  192. package/dist/Testing.d.mts +0 -479
  193. package/dist/Testing.mjs +0 -2147
  194. package/dist/Testing.mjs.map +0 -1
  195. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  196. package/dist/Timeline.d.mts +0 -4
  197. package/dist/Timeline.mjs +0 -20
  198. package/dist/Timeline.mjs.map +0 -1
  199. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  200. package/dist/TimelineData.d.mts +0 -3
  201. package/dist/TimelineData.mjs +0 -482
  202. package/dist/TimelineData.mjs.map +0 -1
@@ -0,0 +1,288 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { BrowserEvent } from "./BrowserEvent.mjs";
3
+ import { BrowserError, InvalidRequest } from "./BrowserError.mjs";
4
+ import { s as undispatched } from "./context-DAYjATgC.mjs";
5
+ import { Frame } from "./Frame.mjs";
6
+ import { Snapshot } from "./Snapshot.mjs";
7
+ import { c as quoted, l as where, o as describe, r as Changes, s as named } from "./Change-DNvztNLu.mjs";
8
+ import { Duration, Effect, Option, Result, Schema } from "effect";
9
+ import { Prompt } from "effect/ai";
10
+ //#region src/Moment.ts
11
+ /**
12
+ * What a page showed, and what changed on it, over a window of time.
13
+ *
14
+ * A page's timeline has three tracks on its browser's host clock: its events, what visibly changed
15
+ * on it (`Change`), and its screencast frames. A `Window` holds the three from `since` to `until`,
16
+ * and says why a part it could not read is missing rather than fail. `Page.window` takes one, which
17
+ * can end in the past, at a frame a delayed consumer airs late, so that a line written now tells
18
+ * what its viewers will see. Its `account` says what was so at its end: how long the page had been
19
+ * still, which of its changes were still under way, and which had settled.
20
+ *
21
+ * A `Moment` is a window that ends at a picture of the page now: `capture` takes the picture, then
22
+ * the window up to it, and on request the page's outline. `toPrompt` turns a moment into one user
23
+ * message for any `effect/ai` call: a structured answer from `LanguageModel.generateObject`, a
24
+ * caption from `generateText`, or one turn of a `Chat` that follows moment after moment. It leads
25
+ * with what changed, news and what settled before what was still changing, as its account tells
26
+ * them apart, and names an action as what a change followed; as a step where changes followed it
27
+ * but none names it, or where its effect is drawn rather than written, as on a canvas, which only
28
+ * the screenshots show; and claims nothing of what changed where the record did not see, or of a
29
+ * part it could not read, which it names with why.
30
+ *
31
+ * Frames come from a running screencast, as far back as the browser's `frameHistory` keeps them.
32
+ * A moment's picture is the newest frame if it was painted after the page's latest input and at
33
+ * most 250 ms ago, else a new screenshot timed by its capture.
34
+ *
35
+ * @since 0.3.0
36
+ */
37
+ var Moment_exports = /* @__PURE__ */ __exportAll({
38
+ Moment: () => Moment,
39
+ Window: () => Window,
40
+ account: () => account,
41
+ capture: () => capture,
42
+ toPrompt: () => toPrompt
43
+ });
44
+ /** A page's events, changes and frames over one window of its browser's host clock. */
45
+ var Window = class extends Schema.Class("effect-browser/Window")({
46
+ page: Schema.String,
47
+ /** Where the window starts, in host monotonic milliseconds on the owning browser’s clock. */
48
+ since: Schema.Finite,
49
+ /** Where it ends, never after it was taken. */
50
+ until: Schema.Finite,
51
+ /** The page's events after `since`, up to `until`, oldest first, without the input presentation track. */
52
+ events: Schema.Array(BrowserEvent),
53
+ /**
54
+ * What visibly changed after `since`, up to `until`; absent where `missing` says why. A page's
55
+ * first window starts its record, so the record sees none of it.
56
+ */
57
+ changes: Schema.optional(Changes),
58
+ /** Screencast frames painted from `since` to `until`, oldest first, as far back as kept. */
59
+ frames: Schema.Array(Frame),
60
+ /**
61
+ * Why each part that could not be read is missing: the read of its changes, by whatever
62
+ * operation failed there, or a moment's picture (`frame`) or outline (`snapshot`).
63
+ */
64
+ missing: Schema.Array(BrowserError)
65
+ }) {};
66
+ /**
67
+ * A window that ends at a picture of the page, its last frame, unless `missing` says why it has
68
+ * none. Its frames are a few, spread over the window, so that it still begins where it starts.
69
+ */
70
+ var Moment = class extends Window.extend("effect-browser/Moment")({
71
+ /** The page's outline at the end of the window, when the capture asked for it. */
72
+ snapshot: Schema.optional(Snapshot) }) {};
73
+ /** Pick `count` frames spread evenly over `frames`, always including the last. */
74
+ const spread = (frames, count) => {
75
+ if (frames.length <= count) return frames;
76
+ if (count <= 1) return frames.slice(-1);
77
+ const step = (frames.length - 1) / (count - 1);
78
+ return Array.from({ length: count }, (_, index) => frames[Math.round(index * step)]).filter((frame) => frame !== void 0);
79
+ };
80
+ const capture = Effect.fn("Moment.capture")(function* (page, options = {}) {
81
+ const count = options.frames ?? 2;
82
+ if (!Number.isSafeInteger(count) || count < 1) return yield* undispatched("moment", new InvalidRequest({ detail: "frames must be a positive safe integer" }));
83
+ const outline = options.snapshot ?? false;
84
+ const { pictured, outlined } = yield* Effect.all({
85
+ pictured: Effect.result(page.frame({ after: "input" })).pipe(Effect.flatMap((picture) => Effect.map(page.window({
86
+ since: options.since ?? Duration.seconds(5),
87
+ until: Result.getOrUndefined(picture),
88
+ unmask: options.unmask
89
+ }), (window) => ({
90
+ window,
91
+ picture
92
+ })))),
93
+ outlined: Effect.result(outline === false ? Effect.void : page.snapshot({
94
+ maxChars: 4e3,
95
+ ...outline === true ? {} : outline
96
+ }))
97
+ }, { concurrency: 2 });
98
+ const { window, picture } = pictured;
99
+ const shown = Result.getOrUndefined(picture);
100
+ return new Moment({
101
+ ...window,
102
+ frames: spread(shown === void 0 ? window.frames : [...window.frames.filter((frame) => frame !== shown), shown], count),
103
+ snapshot: Result.getOrUndefined(outlined) ?? void 0,
104
+ missing: [
105
+ ...window.missing,
106
+ ...Option.toArray(Result.getFailure(picture)),
107
+ ...Option.toArray(Result.getFailure(outlined))
108
+ ]
109
+ });
110
+ });
111
+ /** A window's record of changes, if it saw any of the window: a page's first has none. */
112
+ const recordOf = ({ changes }) => changes !== void 0 && changes.from < changes.until ? changes : void 0;
113
+ /** Whether a change was still under way at `until`, by its own pace. */
114
+ const underWay = (change, until) => {
115
+ const pace = change.count > 1 ? (change.at - change.startedAt) / (change.count - 1) : change.earlier === void 0 ? void 0 : change.startedAt - change.earlier;
116
+ return pace !== void 0 && until - change.at < 2 * pace;
117
+ };
118
+ /**
119
+ * An account of the window's page at its end: how long it had been still, and which of its
120
+ * changes were still under way and which had settled. A window whose record saw none of it has
121
+ * neither.
122
+ */
123
+ const account = (window) => {
124
+ const record = recordOf(window);
125
+ const changes = record?.changes ?? [];
126
+ const stirred = [...changes.flatMap((change) => change.kind === "title" ? [] : [change.at]), ...window.frames.flatMap((frame) => frame.timing._tag === "BrowserPaint" ? [frame.hostTime] : [])];
127
+ return {
128
+ stillFor: record === void 0 && stirred.length === 0 ? void 0 : window.until - Math.max(window.since, record?.from ?? window.since, ...stirred),
129
+ changing: changes.filter((change) => underWay(change, window.until)),
130
+ settled: changes.filter((change) => !underWay(change, window.until))
131
+ };
132
+ };
133
+ const seconds = (moment, at) => `${((at - moment.until) / 1e3).toFixed(1)}s`;
134
+ const pointOf = (x, y) => x === void 0 || y === void 0 ? void 0 : `(${Math.round(x)}, ${Math.round(y)})`;
135
+ /**
136
+ * What an action acted on. Without a subject the action failed before finding it, and a ref it was
137
+ * given is left out: refs are reused, so the page's outline may give it to something else by now.
138
+ */
139
+ const acted = (event) => {
140
+ const point = pointOf(event.x, event.y);
141
+ if (event.subject === void 0) return event.target === void 0 || /^e\d+$|"e\d+"/.test(event.target) ? point ?? "" : event.target;
142
+ if (event.to !== void 0) return `${named(event.subject, void 0)} to ${named(event.to, point)}`;
143
+ return named(event.subject, point);
144
+ };
145
+ /**
146
+ * An action as a step: `click button "Pay"`. A failed one says only that: its error is advice for
147
+ * the caller that acted, which may name a ref, not a fact about the page.
148
+ */
149
+ const step = (event) => {
150
+ const target = acted(event);
151
+ const text = event.text === void 0 ? "" : JSON.stringify(event.text);
152
+ const what = event.name === "type" && target !== "" ? `${text} into ${target}` : event.name === "select" && target !== "" ? `${text} in ${target}` : `${target} ${text}`;
153
+ return `${event.name} ${what}${event.ok ? "" : " (failed)"}`.replace(/\s+/g, " ").trim();
154
+ };
155
+ /** What is drawn rather than written, whose changes only the screenshots show. */
156
+ const drawn = /* @__PURE__ */ new Set([
157
+ "canvas",
158
+ "iframe",
159
+ "video",
160
+ "embed",
161
+ "object",
162
+ "svg"
163
+ ]);
164
+ const onDrawn = (event) => event.ok && event.subject !== void 0 && drawn.has(event.subject.role ?? event.subject.tag);
165
+ /** At most this many lines of what happened; the least notable are counted, not listed. */
166
+ const maxLines = 24;
167
+ /**
168
+ * The changes as lines, news first, and the actions they name; cells of one column that changed
169
+ * together as one line. What kept changing says whether it still was at the moment, by its account.
170
+ */
171
+ const changed = (moment, record) => {
172
+ const actions = moment.events.filter((event) => event._tag === "Action");
173
+ const named = /* @__PURE__ */ new Set();
174
+ const changing = new Set(account(moment).changing);
175
+ const state = (change) => changing.has(change) ? ", still changing at the moment" : change.count > 1 || change.earlier !== void 0 ? `, settled since ${seconds(moment, change.at)}` : "";
176
+ const cause = (change) => {
177
+ const input = change.cause;
178
+ const action = input === void 0 ? void 0 : actions.findLast((event) => event.startedAt <= input + 100 && event.at >= input - 100);
179
+ if (action === void 0) return "";
180
+ named.add(action);
181
+ return `, after ${step(action)}`;
182
+ };
183
+ const columns = Map.groupBy(record.changes.filter((change) => change.kind === "text" && change.subject.context.column !== void 0), ({ subject: { context } }) => JSON.stringify([context.heading, context.column]));
184
+ const lines = [];
185
+ const collapsed = /* @__PURE__ */ new Set();
186
+ for (const [first, ...cells] of columns.values()) if (first !== void 0 && cells.length >= 2) {
187
+ const still = [first, ...cells].filter((cell) => changing.has(cell)).length;
188
+ for (const cell of [first, ...cells]) collapsed.add(cell);
189
+ lines.push({
190
+ at: first.startedAt,
191
+ rank: still > 0 ? 3 : 2,
192
+ text: `${cells.length + 1} cells in column ${quoted(first.subject.context.column)}${where({ heading: first.subject.context.heading })} changed, such as ${describe(first, true)}${still > 0 ? `, ${still} still changing at the moment` : ""}`
193
+ });
194
+ }
195
+ for (const change of record.changes) if (!collapsed.has(change)) lines.push({
196
+ at: change.startedAt,
197
+ rank: changing.has(change) ? 3 : 2,
198
+ text: describe(change) + cause(change) + state(change)
199
+ });
200
+ return {
201
+ lines,
202
+ named
203
+ };
204
+ };
205
+ /** What a part a moment could not read was, by the operation that failed: a window's one read is its changes. */
206
+ const parts = /* @__PURE__ */ new Map([["frame", "A picture of the moment"], ["snapshot", "The page's outline"]]);
207
+ /** The moment's changes and events as lines, most notable first when its changes are recorded. */
208
+ const happened = (moment) => {
209
+ const record = recordOf(moment);
210
+ const { lines, named } = record === void 0 ? {
211
+ lines: new Array(),
212
+ named: /* @__PURE__ */ new Set()
213
+ } : changed(moment, record);
214
+ const followed = (event) => record?.changes.some((change) => change.kind !== "value" && change.at > event.startedAt) === true;
215
+ const told = (event) => !(event.ok && [
216
+ "navigate",
217
+ "back",
218
+ "reload"
219
+ ].includes(event.name)) && (record === void 0 || event.startedAt <= record.from || event.ok && event.name !== "scroll" && (onDrawn(event) || event.name !== "hover" && !named.has(event) && followed(event)));
220
+ for (const event of moment.events) if (event._tag === "Action" && event.page === moment.page && told(event)) lines.push({
221
+ at: event.at,
222
+ rank: 4,
223
+ text: step(event)
224
+ });
225
+ else if (event._tag === "Navigated") lines.push({
226
+ at: event.at,
227
+ rank: 0,
228
+ text: `${event.sameDocument ? "moved within the page" : "navigated"} to ${event.url}`
229
+ });
230
+ else if (event._tag === "PageOpened") lines.push({
231
+ at: event.at,
232
+ rank: 0,
233
+ text: `a tab opened at ${event.url}`
234
+ });
235
+ else if (event._tag === "PageClosed") lines.push({
236
+ at: event.at,
237
+ rank: 0,
238
+ text: `the tab ${event.cause === "crashed" ? "crashed" : "closed"}`
239
+ });
240
+ else if (event._tag === "DialogShown") lines.push({
241
+ at: event.at,
242
+ rank: 1,
243
+ text: `a dialog (${event.kind}) said ${JSON.stringify(event.message)}`
244
+ });
245
+ const ordered = lines.toSorted((left, right) => record === void 0 ? left.at - right.at : left.rank - right.rank || left.at - right.at);
246
+ const kept = ordered.slice(0, maxLines);
247
+ const drawing = moment.events.some((event) => event._tag === "Action" && onDrawn(event));
248
+ const begins = record === void 0 ? moment.since : Math.max(moment.since, record.from);
249
+ const unread = moment.missing.some((error) => !parts.has(error.operation));
250
+ return [
251
+ ...kept.map((line) => `${seconds(moment, line.at)} ${line.text}`),
252
+ ...ordered.length > maxLines ? [`…and ${ordered.length - maxLines} less notable`] : [],
253
+ ...record === void 0 ? unread ? [] : ["What changed on the page was not recorded."] : record.changes.length > 0 ? [] : [`No text in view changed${begins > moment.since ? ` after ${seconds(moment, begins)}` : ""}${drawing ? "; what is drawn, such as a canvas, shows only in the screenshots" : ""}.`],
254
+ ...record !== void 0 && record.dropped > 0 ? [`The page changed too much to tell it all: at least ${record.dropped} changes are not told.`] : [],
255
+ ...begins > moment.since ? [`Changes before ${seconds(moment, begins)} are not all known.`] : [],
256
+ ...moment.missing.map((error) => `${parts.get(error.operation) ?? "What changed on the page"} could not be read: ${error.reason.message}.`)
257
+ ];
258
+ };
259
+ /**
260
+ * The moment as one user message: the page's outline when the moment has one, what changed, most
261
+ * notable first, what kept changing told as still changing at the moment or settled since when, by
262
+ * its account, with the actions that the changes followed, that changes followed though none
263
+ * names them, or that drew what only the screenshots show, what could not be read and why, and its
264
+ * frames captioned with their times, the last one "the moment" where it is the picture at its end.
265
+ * Give a model its task with `Prompt.setSystem`, and add what the caller knows about the page as
266
+ * more text.
267
+ */
268
+ const toPrompt = (moment) => {
269
+ const count = moment.frames.length;
270
+ const last = count - 1;
271
+ const pictured = moment.frames.at(-1)?.hostTime === moment.until;
272
+ const following = count === 1 ? "One screenshot follows" : `${count} screenshots follow, oldest first`;
273
+ return Prompt.fromMessages([Prompt.makeMessage("user", { content: [Prompt.makePart("text", { text: [
274
+ "One moment of a browser session. Rely only on what this material shows, and prefer concrete details: numbers, names, colours, positions and motion.",
275
+ "",
276
+ ...moment.snapshot === void 0 ? [] : [moment.snapshot.rendered, ""],
277
+ `${recordOf(moment) === void 0 ? "What happened" : "What changed, most notable first"} (seconds before the moment, over ${((moment.until - moment.since) / 1e3).toFixed(1)}s):`,
278
+ ...happened(moment),
279
+ ...count === 0 ? [] : ["", `${following}${pictured ? count === 1 ? ": the moment itself." : "; the last one is the moment itself." : "."}`]
280
+ ].join("\n") }), ...moment.frames.flatMap((frame, index) => [Prompt.makePart("text", { text: index === last && pictured ? "The moment:" : `${seconds(moment, frame.hostTime)}:` }), Prompt.makePart("file", {
281
+ mediaType: "image/jpeg",
282
+ data: frame.data
283
+ })])] })]);
284
+ };
285
+ //#endregion
286
+ export { Moment, Window, account, capture, Moment_exports as t, toPrompt };
287
+
288
+ //# sourceMappingURL=Moment.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Moment.mjs","names":[],"sources":["../src/Moment.ts"],"sourcesContent":["/**\n * What a page showed, and what changed on it, over a window of time.\n *\n * A page's timeline has three tracks on its browser's host clock: its events, what visibly changed\n * on it (`Change`), and its screencast frames. A `Window` holds the three from `since` to `until`,\n * and says why a part it could not read is missing rather than fail. `Page.window` takes one, which\n * can end in the past, at a frame a delayed consumer airs late, so that a line written now tells\n * what its viewers will see. Its `account` says what was so at its end: how long the page had been\n * still, which of its changes were still under way, and which had settled.\n *\n * A `Moment` is a window that ends at a picture of the page now: `capture` takes the picture, then\n * the window up to it, and on request the page's outline. `toPrompt` turns a moment into one user\n * message for any `effect/ai` call: a structured answer from `LanguageModel.generateObject`, a\n * caption from `generateText`, or one turn of a `Chat` that follows moment after moment. It leads\n * with what changed, news and what settled before what was still changing, as its account tells\n * them apart, and names an action as what a change followed; as a step where changes followed it\n * but none names it, or where its effect is drawn rather than written, as on a canvas, which only\n * the screenshots show; and claims nothing of what changed where the record did not see, or of a\n * part it could not read, which it names with why.\n *\n * Frames come from a running screencast, as far back as the browser's `frameHistory` keeps them.\n * A moment's picture is the newest frame if it was painted after the page's latest input and at\n * most 250 ms ago, else a new screenshot timed by its capture.\n *\n * @since 0.3.0\n */\nimport { Duration, Effect, Option, Result, Schema } from \"effect\";\nimport { Prompt } from \"effect/ai\";\n\nimport { BrowserError, InvalidRequest } from \"./BrowserError.ts\";\nimport { type Action, BrowserEvent } from \"./BrowserEvent.ts\";\nimport { type Change, Changes } from \"./Change.ts\";\nimport { Frame } from \"./Frame.ts\";\nimport { undispatched } from \"./internal/page/context.ts\";\nimport { describe, named, quoted, where } from \"./internal/timeline/words.ts\";\nimport type * as Page from \"./Page.ts\";\nimport { Snapshot, type SnapshotOptions } from \"./Snapshot.ts\";\n\n/** A page's events, changes and frames over one window of its browser's host clock. */\nexport class Window extends Schema.Class<Window>(\"effect-browser/Window\")({\n page: Schema.String,\n /** Where the window starts, in host monotonic milliseconds on the owning browser’s clock. */\n since: Schema.Finite,\n /** Where it ends, never after it was taken. */\n until: Schema.Finite,\n /** The page's events after `since`, up to `until`, oldest first, without the input presentation track. */\n events: Schema.Array(BrowserEvent),\n /**\n * What visibly changed after `since`, up to `until`; absent where `missing` says why. A page's\n * first window starts its record, so the record sees none of it.\n */\n changes: Schema.optional(Changes),\n /** Screencast frames painted from `since` to `until`, oldest first, as far back as kept. */\n frames: Schema.Array(Frame),\n /**\n * Why each part that could not be read is missing: the read of its changes, by whatever\n * operation failed there, or a moment's picture (`frame`) or outline (`snapshot`).\n */\n missing: Schema.Array(BrowserError),\n}) {}\n\n/**\n * A window that ends at a picture of the page, its last frame, unless `missing` says why it has\n * none. Its frames are a few, spread over the window, so that it still begins where it starts.\n */\nexport class Moment extends Window.extend<Moment>(\"effect-browser/Moment\")({\n /** The page's outline at the end of the window, when the capture asked for it. */\n snapshot: Schema.optional(Snapshot),\n}) {}\n\nexport interface CaptureOptions {\n /**\n * Where the window starts, as for `Page.window`: a previous window or moment, so that consecutive\n * ones neither repeat nor miss an event or a change, or a `Duration` back from the picture.\n * Defaults to 5 seconds.\n */\n readonly since?: Page.WindowOptions[\"since\"] | undefined;\n /** Frames to keep, at least 1, spread over the window and ending with the picture. Defaults to 2. */\n readonly frames?: number | undefined;\n /**\n * Whether to take the page's outline at the end, or its options; `true` means 4,000 characters.\n * Defaults to `false`: the frames and the timeline carry most moments, while an outline costs a\n * round trip to the page and can double a moment's tokens.\n */\n readonly snapshot?: SnapshotOptions | boolean | undefined;\n /** Show what fields hold in the moment's changes; a secret field still reads `••••`. */\n readonly unmask?: boolean | undefined;\n}\n\n/** Pick `count` frames spread evenly over `frames`, always including the last. */\nconst spread = (frames: ReadonlyArray<Frame>, count: number): ReadonlyArray<Frame> => {\n if (frames.length <= count) return frames;\n if (count <= 1) return frames.slice(-1);\n const step = (frames.length - 1) / (count - 1);\n\n return Array.from({ length: count }, (_, index) => frames[Math.round(index * step)]).filter(\n (frame): frame is Frame => frame !== undefined,\n );\n};\n\nexport const capture = Effect.fn(\"Moment.capture\")(function* (\n page: Page.Page,\n options: CaptureOptions = {},\n) {\n const count = options.frames ?? 2;\n\n if (!Number.isSafeInteger(count) || count < 1)\n return yield* undispatched(\n \"moment\",\n new InvalidRequest({ detail: \"frames must be a positive safe integer\" }),\n );\n const outline = options.snapshot ?? false;\n\n // The picture comes first, at no call with a running screencast, and the window ends at its\n // paint, so its changes hold nothing it does not show; without one, the window ends now. The\n // outline is taken alongside. What cannot be read is missing from the moment, not its end.\n const { pictured, outlined } = yield* Effect.all(\n {\n pictured: Effect.result(page.frame({ after: \"input\" })).pipe(\n Effect.flatMap((picture) =>\n Effect.map(\n page.window({\n since: options.since ?? Duration.seconds(5),\n until: Result.getOrUndefined(picture),\n unmask: options.unmask,\n }),\n (window) => ({ window, picture }),\n ),\n ),\n ),\n outlined: Effect.result(\n outline === false\n ? Effect.void\n : page.snapshot({ maxChars: 4000, ...(outline === true ? {} : outline) }),\n ),\n },\n { concurrency: 2 },\n );\n\n const { window, picture } = pictured;\n const shown = Result.getOrUndefined(picture);\n\n return new Moment({\n ...window,\n frames: spread(\n shown === undefined\n ? window.frames\n : [...window.frames.filter((frame) => frame !== shown), shown],\n count,\n ),\n snapshot: Result.getOrUndefined(outlined) ?? undefined,\n missing: [\n ...window.missing,\n ...Option.toArray(Result.getFailure(picture)),\n ...Option.toArray(Result.getFailure(outlined)),\n ],\n });\n});\n\n/** A window's record of changes, if it saw any of the window: a page's first has none. */\nconst recordOf = ({ changes }: Window) =>\n changes !== undefined && changes.from < changes.until ? changes : undefined;\n\n/** What a window says of its page at its end: what was still in flux, and what had settled. */\nexport interface Account {\n /**\n * How long, in milliseconds, the page had been still at the end: since the last change in view\n * its record shows, or the last paint its screencast frames show, or else since the window, or\n * its record, began. Undefined where neither could tell, with no record of the window and no\n * painted frame: a screenshot shows the page, not when it last changed. Frames show paint only\n * while a capture runs, and the record does not see a canvas, so a canvas that keeps drawing\n * with no capture running reads as still.\n */\n readonly stillFor: number | undefined;\n /**\n * The changes still under way at the end: what kept changing, more than once in the window or\n * already before it, and changed again within twice its own pace before the end.\n */\n readonly changing: ReadonlyArray<Change>;\n /** Every other change, which shows what it settled at. */\n readonly settled: ReadonlyArray<Change>;\n}\n\n/** Whether a change was still under way at `until`, by its own pace. */\nconst underWay = (change: Change, until: number) => {\n const pace =\n change.count > 1\n ? (change.at - change.startedAt) / (change.count - 1)\n : change.earlier === undefined\n ? undefined\n : change.startedAt - change.earlier;\n\n return pace !== undefined && until - change.at < 2 * pace;\n};\n\n/**\n * An account of the window's page at its end: how long it had been still, and which of its\n * changes were still under way and which had settled. A window whose record saw none of it has\n * neither.\n */\nexport const account = (window: Window): Account => {\n const record = recordOf(window);\n const changes = record?.changes ?? [];\n\n const stirred = [\n ...changes.flatMap((change) => (change.kind === \"title\" ? [] : [change.at])),\n ...window.frames.flatMap((frame) =>\n frame.timing._tag === \"BrowserPaint\" ? [frame.hostTime] : [],\n ),\n ];\n\n return {\n stillFor:\n record === undefined && stirred.length === 0\n ? undefined\n : window.until - Math.max(window.since, record?.from ?? window.since, ...stirred),\n changing: changes.filter((change) => underWay(change, window.until)),\n settled: changes.filter((change) => !underWay(change, window.until)),\n };\n};\n\nconst seconds = (moment: Moment, at: number) => `${((at - moment.until) / 1000).toFixed(1)}s`;\n\nconst pointOf = (x: number | undefined, y: number | undefined) =>\n x === undefined || y === undefined ? undefined : `(${Math.round(x)}, ${Math.round(y)})`;\n\n/**\n * What an action acted on. Without a subject the action failed before finding it, and a ref it was\n * given is left out: refs are reused, so the page's outline may give it to something else by now.\n */\nconst acted = (event: Action): string => {\n const point = pointOf(event.x, event.y);\n\n if (event.subject === undefined)\n return event.target === undefined || /^e\\d+$|\"e\\d+\"/.test(event.target)\n ? (point ?? \"\")\n : event.target;\n // A drag's point is where it ended.\n if (event.to !== undefined)\n return `${named(event.subject, undefined)} to ${named(event.to, point)}`;\n\n return named(event.subject, point);\n};\n\n/**\n * An action as a step: `click button \"Pay\"`. A failed one says only that: its error is advice for\n * the caller that acted, which may name a ref, not a fact about the page.\n */\nconst step = (event: Action): string => {\n const target = acted(event);\n const text = event.text === undefined ? \"\" : JSON.stringify(event.text);\n\n const what =\n event.name === \"type\" && target !== \"\"\n ? `${text} into ${target}`\n : event.name === \"select\" && target !== \"\"\n ? `${text} in ${target}`\n : `${target} ${text}`;\n\n return `${event.name} ${what}${event.ok ? \"\" : \" (failed)\"}`.replace(/\\s+/g, \" \").trim();\n};\n\n/** What is drawn rather than written, whose changes only the screenshots show. */\nconst drawn = new Set([\"canvas\", \"iframe\", \"video\", \"embed\", \"object\", \"svg\"]);\n\nconst onDrawn = (event: Action) =>\n event.ok && event.subject !== undefined && drawn.has(event.subject.role ?? event.subject.tag);\n\ninterface Line {\n readonly at: number;\n readonly rank: number;\n readonly text: string;\n}\n\n/** At most this many lines of what happened; the least notable are counted, not listed. */\nconst maxLines = 24;\n\n/**\n * The changes as lines, news first, and the actions they name; cells of one column that changed\n * together as one line. What kept changing says whether it still was at the moment, by its account.\n */\nconst changed = (moment: Moment, record: Changes) => {\n const actions = moment.events.filter((event): event is Action => event._tag === \"Action\");\n const named = new Set<Action>();\n const changing = new Set(account(moment).changing);\n\n const state = (change: Change) =>\n changing.has(change)\n ? \", still changing at the moment\"\n : change.count > 1 || change.earlier !== undefined\n ? `, settled since ${seconds(moment, change.at)}`\n : \"\";\n\n // The action whose input a change followed: one that ran while the input arrived, give or take\n // the clock's mapping, so input the library did not send names no action.\n const cause = (change: Change) => {\n const input = change.cause;\n\n const action =\n input === undefined\n ? undefined\n : actions.findLast((event) => event.startedAt <= input + 100 && event.at >= input - 100);\n\n if (action === undefined) return \"\";\n named.add(action);\n\n return `, after ${step(action)}`;\n };\n\n const columns = Map.groupBy(\n record.changes.filter(\n (change) => change.kind === \"text\" && change.subject.context.column !== undefined,\n ),\n ({ subject: { context } }) => JSON.stringify([context.heading, context.column]),\n );\n\n const lines: Array<Line> = [];\n const collapsed = new Set<Change>();\n\n for (const [first, ...cells] of columns.values())\n if (first !== undefined && cells.length >= 2) {\n const still = [first, ...cells].filter((cell) => changing.has(cell)).length;\n\n for (const cell of [first, ...cells]) collapsed.add(cell);\n lines.push({\n at: first.startedAt,\n rank: still > 0 ? 3 : 2,\n text: `${cells.length + 1} cells in column ${quoted(first.subject.context.column)}${where({ heading: first.subject.context.heading })} changed, such as ${describe(first, true)}${still > 0 ? `, ${still} still changing at the moment` : \"\"}`,\n });\n }\n for (const change of record.changes)\n if (!collapsed.has(change))\n lines.push({\n at: change.startedAt,\n rank: changing.has(change) ? 3 : 2,\n text: describe(change) + cause(change) + state(change),\n });\n\n return { lines, named };\n};\n\n/** What a part a moment could not read was, by the operation that failed: a window's one read is its changes. */\nconst parts = new Map([\n [\"frame\", \"A picture of the moment\"],\n [\"snapshot\", \"The page's outline\"],\n]);\n\n/** The moment's changes and events as lines, most notable first when its changes are recorded. */\nconst happened = (moment: Moment): ReadonlyArray<string> => {\n const record = recordOf(moment);\n\n const { lines, named } =\n record === undefined\n ? { lines: new Array<Line>(), named: new Set<Action>() }\n : changed(moment, record);\n\n // Whether anything but a field's own edit changed after an action began.\n const followed = (event: Action) =>\n record?.changes.some((change) => change.kind !== \"value\" && change.at > event.startedAt) ===\n true;\n\n // With a record, an action is told as a step where changes followed it and none names it, so a\n // click whose effect a busy page leaves uncredited is not lost; where its effect is drawn; or\n // where it started before the record began, as on the document a click opened. An attempt that\n // nothing followed, a failed one, a hover and a scroll are left out.\n const told = (event: Action) =>\n !(event.ok && [\"navigate\", \"back\", \"reload\"].includes(event.name)) &&\n (record === undefined ||\n event.startedAt <= record.from ||\n (event.ok &&\n event.name !== \"scroll\" &&\n (onDrawn(event) || (event.name !== \"hover\" && !named.has(event) && followed(event)))));\n\n for (const event of moment.events)\n if (event._tag === \"Action\" && event.page === moment.page && told(event))\n lines.push({ at: event.at, rank: 4, text: step(event) });\n else if (event._tag === \"Navigated\")\n lines.push({\n at: event.at,\n rank: 0,\n text: `${event.sameDocument ? \"moved within the page\" : \"navigated\"} to ${event.url}`,\n });\n else if (event._tag === \"PageOpened\")\n lines.push({ at: event.at, rank: 0, text: `a tab opened at ${event.url}` });\n else if (event._tag === \"PageClosed\")\n lines.push({\n at: event.at,\n rank: 0,\n text: `the tab ${event.cause === \"crashed\" ? \"crashed\" : \"closed\"}`,\n });\n else if (event._tag === \"DialogShown\")\n lines.push({\n at: event.at,\n rank: 1,\n text: `a dialog (${event.kind}) said ${JSON.stringify(event.message)}`,\n });\n\n const ordered = lines.toSorted((left, right) =>\n record === undefined ? left.at - right.at : left.rank - right.rank || left.at - right.at,\n );\n\n const kept = ordered.slice(0, maxLines);\n const drawing = moment.events.some((event) => event._tag === \"Action\" && onDrawn(event));\n const begins = record === undefined ? moment.since : Math.max(moment.since, record.from);\n const unread = moment.missing.some((error) => !parts.has(error.operation));\n\n return [\n ...kept.map((line) => `${seconds(moment, line.at)} ${line.text}`),\n ...(ordered.length > maxLines ? [`…and ${ordered.length - maxLines} less notable`] : []),\n ...(record === undefined\n ? unread\n ? []\n : [\"What changed on the page was not recorded.\"]\n : record.changes.length > 0\n ? []\n : [\n `No text in view changed${begins > moment.since ? ` after ${seconds(moment, begins)}` : \"\"}${drawing ? \"; what is drawn, such as a canvas, shows only in the screenshots\" : \"\"}.`,\n ]),\n ...(record !== undefined && record.dropped > 0\n ? [\n `The page changed too much to tell it all: at least ${record.dropped} changes are not told.`,\n ]\n : []),\n ...(begins > moment.since\n ? [`Changes before ${seconds(moment, begins)} are not all known.`]\n : []),\n ...moment.missing.map(\n (error) =>\n `${parts.get(error.operation) ?? \"What changed on the page\"} could not be read: ${error.reason.message}.`,\n ),\n ];\n};\n\n/**\n * The moment as one user message: the page's outline when the moment has one, what changed, most\n * notable first, what kept changing told as still changing at the moment or settled since when, by\n * its account, with the actions that the changes followed, that changes followed though none\n * names them, or that drew what only the screenshots show, what could not be read and why, and its\n * frames captioned with their times, the last one \"the moment\" where it is the picture at its end.\n * Give a model its task with `Prompt.setSystem`, and add what the caller knows about the page as\n * more text.\n */\nexport const toPrompt = (moment: Moment): Prompt.Prompt => {\n const count = moment.frames.length;\n const last = count - 1;\n const pictured = moment.frames.at(-1)?.hostTime === moment.until;\n\n const following =\n count === 1 ? \"One screenshot follows\" : `${count} screenshots follow, oldest first`;\n\n return Prompt.fromMessages([\n Prompt.makeMessage(\"user\", {\n content: [\n Prompt.makePart(\"text\", {\n text: [\n \"One moment of a browser session. Rely only on what this material shows, and prefer concrete details: numbers, names, colours, positions and motion.\",\n \"\",\n ...(moment.snapshot === undefined ? [] : [moment.snapshot.rendered, \"\"]),\n `${recordOf(moment) === undefined ? \"What happened\" : \"What changed, most notable first\"} (seconds before the moment, over ${((moment.until - moment.since) / 1000).toFixed(1)}s):`,\n ...happened(moment),\n ...(count === 0\n ? []\n : [\n \"\",\n `${following}${pictured ? (count === 1 ? \": the moment itself.\" : \"; the last one is the moment itself.\") : \".\"}`,\n ]),\n ].join(\"\\n\"),\n }),\n ...moment.frames.flatMap((frame, index) => [\n Prompt.makePart(\"text\", {\n text:\n index === last && pictured ? \"The moment:\" : `${seconds(moment, frame.hostTime)}:`,\n }),\n Prompt.makePart(\"file\", { mediaType: \"image/jpeg\", data: frame.data }),\n ]),\n ],\n }),\n ]);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,SAAb,cAA4B,OAAO,MAAc,uBAAuB,CAAC,CAAC;CACxE,MAAM,OAAO;;CAEb,OAAO,OAAO;;CAEd,OAAO,OAAO;;CAEd,QAAQ,OAAO,MAAM,YAAY;;;;;CAKjC,SAAS,OAAO,SAAS,OAAO;;CAEhC,QAAQ,OAAO,MAAM,KAAK;;;;;CAK1B,SAAS,OAAO,MAAM,YAAY;AACpC,CAAC,CAAC,CAAC,CAAC;;;;;AAMJ,IAAa,SAAb,cAA4B,OAAO,OAAe,uBAAuB,CAAC,CAAC;;AAEzE,UAAU,OAAO,SAAS,QAAQ,EACpC,CAAC,CAAC,CAAC,CAAC;;AAsBJ,MAAM,UAAU,QAA8B,UAAwC;CACpF,IAAI,OAAO,UAAU,OAAO,OAAO;CACnC,IAAI,SAAS,GAAG,OAAO,OAAO,MAAM,EAAE;CACtC,MAAM,QAAQ,OAAO,SAAS,MAAM,QAAQ;CAE5C,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,UAAU,OAAO,KAAK,MAAM,QAAQ,IAAI,EAAE,CAAC,CAAC,QAClF,UAA0B,UAAU,KAAA,CACvC;AACF;AAEA,MAAa,UAAU,OAAO,GAAG,gBAAgB,CAAC,CAAC,WACjD,MACA,UAA0B,CAAC,GAC3B;CACA,MAAM,QAAQ,QAAQ,UAAU;CAEhC,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,GAC1C,OAAO,OAAO,aACZ,UACA,IAAI,eAAe,EAAE,QAAQ,yCAAyC,CAAC,CACzE;CACF,MAAM,UAAU,QAAQ,YAAY;CAKpC,MAAM,EAAE,UAAU,aAAa,OAAO,OAAO,IAC3C;EACE,UAAU,OAAO,OAAO,KAAK,MAAM,EAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,KACtD,OAAO,SAAS,YACd,OAAO,IACL,KAAK,OAAO;GACV,OAAO,QAAQ,SAAS,SAAS,QAAQ,CAAC;GAC1C,OAAO,OAAO,eAAe,OAAO;GACpC,QAAQ,QAAQ;EAClB,CAAC,IACA,YAAY;GAAE;GAAQ;EAAQ,EACjC,CACF,CACF;EACA,UAAU,OAAO,OACf,YAAY,QACR,OAAO,OACP,KAAK,SAAS;GAAE,UAAU;GAAM,GAAI,YAAY,OAAO,CAAC,IAAI;EAAS,CAAC,CAC5E;CACF,GACA,EAAE,aAAa,EAAE,CACnB;CAEA,MAAM,EAAE,QAAQ,YAAY;CAC5B,MAAM,QAAQ,OAAO,eAAe,OAAO;CAE3C,OAAO,IAAI,OAAO;EAChB,GAAG;EACH,QAAQ,OACN,UAAU,KAAA,IACN,OAAO,SACP,CAAC,GAAG,OAAO,OAAO,QAAQ,UAAU,UAAU,KAAK,GAAG,KAAK,GAC/D,KACF;EACA,UAAU,OAAO,eAAe,QAAQ,KAAK,KAAA;EAC7C,SAAS;GACP,GAAG,OAAO;GACV,GAAG,OAAO,QAAQ,OAAO,WAAW,OAAO,CAAC;GAC5C,GAAG,OAAO,QAAQ,OAAO,WAAW,QAAQ,CAAC;EAC/C;CACF,CAAC;AACH,CAAC;;AAGD,MAAM,YAAY,EAAE,cAClB,YAAY,KAAA,KAAa,QAAQ,OAAO,QAAQ,QAAQ,UAAU,KAAA;;AAuBpE,MAAM,YAAY,QAAgB,UAAkB;CAClD,MAAM,OACJ,OAAO,QAAQ,KACV,OAAO,KAAK,OAAO,cAAc,OAAO,QAAQ,KACjD,OAAO,YAAY,KAAA,IACjB,KAAA,IACA,OAAO,YAAY,OAAO;CAElC,OAAO,SAAS,KAAA,KAAa,QAAQ,OAAO,KAAK,IAAI;AACvD;;;;;;AAOA,MAAa,WAAW,WAA4B;CAClD,MAAM,SAAS,SAAS,MAAM;CAC9B,MAAM,UAAU,QAAQ,WAAW,CAAC;CAEpC,MAAM,UAAU,CACd,GAAG,QAAQ,SAAS,WAAY,OAAO,SAAS,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,CAAE,GAC3E,GAAG,OAAO,OAAO,SAAS,UACxB,MAAM,OAAO,SAAS,iBAAiB,CAAC,MAAM,QAAQ,IAAI,CAAC,CAC7D,CACF;CAEA,OAAO;EACL,UACE,WAAW,KAAA,KAAa,QAAQ,WAAW,IACvC,KAAA,IACA,OAAO,QAAQ,KAAK,IAAI,OAAO,OAAO,QAAQ,QAAQ,OAAO,OAAO,GAAG,OAAO;EACpF,UAAU,QAAQ,QAAQ,WAAW,SAAS,QAAQ,OAAO,KAAK,CAAC;EACnE,SAAS,QAAQ,QAAQ,WAAW,CAAC,SAAS,QAAQ,OAAO,KAAK,CAAC;CACrE;AACF;AAEA,MAAM,WAAW,QAAgB,OAAe,KAAK,KAAK,OAAO,SAAS,IAAA,CAAM,QAAQ,CAAC,EAAE;AAE3F,MAAM,WAAW,GAAuB,MACtC,MAAM,KAAA,KAAa,MAAM,KAAA,IAAY,KAAA,IAAY,IAAI,KAAK,MAAM,CAAC,EAAE,IAAI,KAAK,MAAM,CAAC,EAAE;;;;;AAMvF,MAAM,SAAS,UAA0B;CACvC,MAAM,QAAQ,QAAQ,MAAM,GAAG,MAAM,CAAC;CAEtC,IAAI,MAAM,YAAY,KAAA,GACpB,OAAO,MAAM,WAAW,KAAA,KAAa,gBAAgB,KAAK,MAAM,MAAM,IACjE,SAAS,KACV,MAAM;CAEZ,IAAI,MAAM,OAAO,KAAA,GACf,OAAO,GAAG,MAAM,MAAM,SAAS,KAAA,CAAS,EAAE,MAAM,MAAM,MAAM,IAAI,KAAK;CAEvE,OAAO,MAAM,MAAM,SAAS,KAAK;AACnC;;;;;AAMA,MAAM,QAAQ,UAA0B;CACtC,MAAM,SAAS,MAAM,KAAK;CAC1B,MAAM,OAAO,MAAM,SAAS,KAAA,IAAY,KAAK,KAAK,UAAU,MAAM,IAAI;CAEtE,MAAM,OACJ,MAAM,SAAS,UAAU,WAAW,KAChC,GAAG,KAAK,QAAQ,WAChB,MAAM,SAAS,YAAY,WAAW,KACpC,GAAG,KAAK,MAAM,WACd,GAAG,OAAO,GAAG;CAErB,OAAO,GAAG,MAAM,KAAK,GAAG,OAAO,MAAM,KAAK,KAAK,cAAc,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AACzF;;AAGA,MAAM,wBAAQ,IAAI,IAAI;CAAC;CAAU;CAAU;CAAS;CAAS;CAAU;AAAK,CAAC;AAE7E,MAAM,WAAW,UACf,MAAM,MAAM,MAAM,YAAY,KAAA,KAAa,MAAM,IAAI,MAAM,QAAQ,QAAQ,MAAM,QAAQ,GAAG;;AAS9F,MAAM,WAAW;;;;;AAMjB,MAAM,WAAW,QAAgB,WAAoB;CACnD,MAAM,UAAU,OAAO,OAAO,QAAQ,UAA2B,MAAM,SAAS,QAAQ;CACxF,MAAM,wBAAQ,IAAI,IAAY;CAC9B,MAAM,WAAW,IAAI,IAAI,QAAQ,MAAM,CAAC,CAAC,QAAQ;CAEjD,MAAM,SAAS,WACb,SAAS,IAAI,MAAM,IACf,mCACA,OAAO,QAAQ,KAAK,OAAO,YAAY,KAAA,IACrC,mBAAmB,QAAQ,QAAQ,OAAO,EAAE,MAC5C;CAIR,MAAM,SAAS,WAAmB;EAChC,MAAM,QAAQ,OAAO;EAErB,MAAM,SACJ,UAAU,KAAA,IACN,KAAA,IACA,QAAQ,UAAU,UAAU,MAAM,aAAa,QAAQ,OAAO,MAAM,MAAM,QAAQ,GAAG;EAE3F,IAAI,WAAW,KAAA,GAAW,OAAO;EACjC,MAAM,IAAI,MAAM;EAEhB,OAAO,WAAW,KAAK,MAAM;CAC/B;CAEA,MAAM,UAAU,IAAI,QAClB,OAAO,QAAQ,QACZ,WAAW,OAAO,SAAS,UAAU,OAAO,QAAQ,QAAQ,WAAW,KAAA,CAC1E,IACC,EAAE,SAAS,EAAE,gBAAgB,KAAK,UAAU,CAAC,QAAQ,SAAS,QAAQ,MAAM,CAAC,CAChF;CAEA,MAAM,QAAqB,CAAC;CAC5B,MAAM,4BAAY,IAAI,IAAY;CAElC,KAAK,MAAM,CAAC,OAAO,GAAG,UAAU,QAAQ,OAAO,GAC7C,IAAI,UAAU,KAAA,KAAa,MAAM,UAAU,GAAG;EAC5C,MAAM,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,QAAQ,SAAS,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC;EAErE,KAAK,MAAM,QAAQ,CAAC,OAAO,GAAG,KAAK,GAAG,UAAU,IAAI,IAAI;EACxD,MAAM,KAAK;GACT,IAAI,MAAM;GACV,MAAM,QAAQ,IAAI,IAAI;GACtB,MAAM,GAAG,MAAM,SAAS,EAAE,mBAAmB,OAAO,MAAM,QAAQ,QAAQ,MAAM,IAAI,MAAM,EAAE,SAAS,MAAM,QAAQ,QAAQ,QAAQ,CAAC,EAAE,oBAAoB,SAAS,OAAO,IAAI,IAAI,QAAQ,IAAI,KAAK,MAAM,iCAAiC;EAC5O,CAAC;CACH;CACF,KAAK,MAAM,UAAU,OAAO,SAC1B,IAAI,CAAC,UAAU,IAAI,MAAM,GACvB,MAAM,KAAK;EACT,IAAI,OAAO;EACX,MAAM,SAAS,IAAI,MAAM,IAAI,IAAI;EACjC,MAAM,SAAS,MAAM,IAAI,MAAM,MAAM,IAAI,MAAM,MAAM;CACvD,CAAC;CAEL,OAAO;EAAE;EAAO;CAAM;AACxB;;AAGA,MAAM,wBAAQ,IAAI,IAAI,CACpB,CAAC,SAAS,yBAAyB,GACnC,CAAC,YAAY,oBAAoB,CACnC,CAAC;;AAGD,MAAM,YAAY,WAA0C;CAC1D,MAAM,SAAS,SAAS,MAAM;CAE9B,MAAM,EAAE,OAAO,UACb,WAAW,KAAA,IACP;EAAE,OAAO,IAAI,MAAY;EAAG,uBAAO,IAAI,IAAY;CAAE,IACrD,QAAQ,QAAQ,MAAM;CAG5B,MAAM,YAAY,UAChB,QAAQ,QAAQ,MAAM,WAAW,OAAO,SAAS,WAAW,OAAO,KAAK,MAAM,SAAS,MACvF;CAMF,MAAM,QAAQ,UACZ,EAAE,MAAM,MAAM;EAAC;EAAY;EAAQ;CAAQ,CAAC,CAAC,SAAS,MAAM,IAAI,OAC/D,WAAW,KAAA,KACV,MAAM,aAAa,OAAO,QACzB,MAAM,MACL,MAAM,SAAS,aACd,QAAQ,KAAK,KAAM,MAAM,SAAS,WAAW,CAAC,MAAM,IAAI,KAAK,KAAK,SAAS,KAAK;CAEvF,KAAK,MAAM,SAAS,OAAO,QACzB,IAAI,MAAM,SAAS,YAAY,MAAM,SAAS,OAAO,QAAQ,KAAK,KAAK,GACrE,MAAM,KAAK;EAAE,IAAI,MAAM;EAAI,MAAM;EAAG,MAAM,KAAK,KAAK;CAAE,CAAC;MACpD,IAAI,MAAM,SAAS,aACtB,MAAM,KAAK;EACT,IAAI,MAAM;EACV,MAAM;EACN,MAAM,GAAG,MAAM,eAAe,0BAA0B,YAAY,MAAM,MAAM;CAClF,CAAC;MACE,IAAI,MAAM,SAAS,cACtB,MAAM,KAAK;EAAE,IAAI,MAAM;EAAI,MAAM;EAAG,MAAM,mBAAmB,MAAM;CAAM,CAAC;MACvE,IAAI,MAAM,SAAS,cACtB,MAAM,KAAK;EACT,IAAI,MAAM;EACV,MAAM;EACN,MAAM,WAAW,MAAM,UAAU,YAAY,YAAY;CAC3D,CAAC;MACE,IAAI,MAAM,SAAS,eACtB,MAAM,KAAK;EACT,IAAI,MAAM;EACV,MAAM;EACN,MAAM,aAAa,MAAM,KAAK,SAAS,KAAK,UAAU,MAAM,OAAO;CACrE,CAAC;CAEL,MAAM,UAAU,MAAM,UAAU,MAAM,UACpC,WAAW,KAAA,IAAY,KAAK,KAAK,MAAM,KAAK,KAAK,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,EACxF;CAEA,MAAM,OAAO,QAAQ,MAAM,GAAG,QAAQ;CACtC,MAAM,UAAU,OAAO,OAAO,MAAM,UAAU,MAAM,SAAS,YAAY,QAAQ,KAAK,CAAC;CACvF,MAAM,SAAS,WAAW,KAAA,IAAY,OAAO,QAAQ,KAAK,IAAI,OAAO,OAAO,OAAO,IAAI;CACvF,MAAM,SAAS,OAAO,QAAQ,MAAM,UAAU,CAAC,MAAM,IAAI,MAAM,SAAS,CAAC;CAEzE,OAAO;EACL,GAAG,KAAK,KAAK,SAAS,GAAG,QAAQ,QAAQ,KAAK,EAAE,EAAE,GAAG,KAAK,MAAM;EAChE,GAAI,QAAQ,SAAS,WAAW,CAAC,QAAQ,QAAQ,SAAS,SAAS,cAAc,IAAI,CAAC;EACtF,GAAI,WAAW,KAAA,IACX,SACE,CAAC,IACD,CAAC,4CAA4C,IAC/C,OAAO,QAAQ,SAAS,IACtB,CAAC,IACD,CACE,0BAA0B,SAAS,OAAO,QAAQ,UAAU,QAAQ,QAAQ,MAAM,MAAM,KAAK,UAAU,qEAAqE,GAAG,EACjL;EACN,GAAI,WAAW,KAAA,KAAa,OAAO,UAAU,IACzC,CACE,sDAAsD,OAAO,QAAQ,uBACvE,IACA,CAAC;EACL,GAAI,SAAS,OAAO,QAChB,CAAC,kBAAkB,QAAQ,QAAQ,MAAM,EAAE,oBAAoB,IAC/D,CAAC;EACL,GAAG,OAAO,QAAQ,KACf,UACC,GAAG,MAAM,IAAI,MAAM,SAAS,KAAK,2BAA2B,sBAAsB,MAAM,OAAO,QAAQ,EAC3G;CACF;AACF;;;;;;;;;;AAWA,MAAa,YAAY,WAAkC;CACzD,MAAM,QAAQ,OAAO,OAAO;CAC5B,MAAM,OAAO,QAAQ;CACrB,MAAM,WAAW,OAAO,OAAO,GAAG,EAAE,CAAC,EAAE,aAAa,OAAO;CAE3D,MAAM,YACJ,UAAU,IAAI,2BAA2B,GAAG,MAAM;CAEpD,OAAO,OAAO,aAAa,CACzB,OAAO,YAAY,QAAQ,EACzB,SAAS,CACP,OAAO,SAAS,QAAQ,EACtB,MAAM;EACJ;EACA;EACA,GAAI,OAAO,aAAa,KAAA,IAAY,CAAC,IAAI,CAAC,OAAO,SAAS,UAAU,EAAE;EACtE,GAAG,SAAS,MAAM,MAAM,KAAA,IAAY,kBAAkB,mCAAmC,sCAAsC,OAAO,QAAQ,OAAO,SAAS,IAAA,CAAM,QAAQ,CAAC,EAAE;EAC/K,GAAG,SAAS,MAAM;EAClB,GAAI,UAAU,IACV,CAAC,IACD,CACE,IACA,GAAG,YAAY,WAAY,UAAU,IAAI,yBAAyB,yCAA0C,KAC9G;CACN,CAAC,CAAC,KAAK,IAAI,EACb,CAAC,GACD,GAAG,OAAO,OAAO,SAAS,OAAO,UAAU,CACzC,OAAO,SAAS,QAAQ,EACtB,MACE,UAAU,QAAQ,WAAW,gBAAgB,GAAG,QAAQ,QAAQ,MAAM,QAAQ,EAAE,GACpF,CAAC,GACD,OAAO,SAAS,QAAQ;EAAE,WAAW;EAAc,MAAM,MAAM;CAAK,CAAC,CACvE,CAAC,CACH,EACF,CAAC,CACH,CAAC;AACH"}
@@ -0,0 +1,40 @@
1
+ import { Effect, Schema } from "effect";
2
+ declare namespace Motion_d_exports {
3
+ export { Plan, Point, Sample, Service, lognormal, maximumDurationMillis, maximumSamples };
4
+ }
5
+ export declare const maximumSamples = 2048;
6
+ export declare const maximumDurationMillis = 5000;
7
+ export declare const Point: Schema.Struct<{
8
+ readonly x: Schema.Finite;
9
+ readonly y: Schema.Finite;
10
+ }>;
11
+ export type Point = typeof Point.Type;
12
+ export declare const Sample: Schema.Struct<{
13
+ readonly x: Schema.Finite;
14
+ readonly y: Schema.Finite;
15
+ readonly afterMillis: Schema.Finite;
16
+ }>;
17
+ export type Sample = typeof Sample.Type;
18
+ /** A complete glide: 1 to 2,048 samples whose offsets never decrease. */
19
+ export declare const Plan: Schema.$Array<Schema.Struct<{
20
+ readonly x: Schema.Finite;
21
+ readonly y: Schema.Finite;
22
+ readonly afterMillis: Schema.Finite;
23
+ }>>;
24
+ export type Plan = typeof Plan.Type;
25
+ export interface Service {
26
+ /**
27
+ * Offsets start when the performer publishes the plan, before its first input. The browser
28
+ * decodes the result as a `Plan` whose last sample must be exactly `to`. Glide time has a budget
29
+ * of its own, outside the browser's `actionTimeout`, so a drag may glide twice for 5 seconds.
30
+ */
31
+ readonly plan: (from: Point, to: Point) => Effect.Effect<ReadonlyArray<Sample>>;
32
+ }
33
+ /**
34
+ * The research's tuned two-stroke sigma-lognormal model: a fast stroke toward the target and a
35
+ * slower one that homes in on it, with a little bend, drawn from `Random`.
36
+ */
37
+ export declare const lognormal: Service;
38
+ //#endregion
39
+ export { Motion_d_exports as t };
40
+ //# sourceMappingURL=Motion.d.mts.map
@@ -0,0 +1,122 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { Effect, Random, Schema } from "effect";
3
+ //#region src/Motion.ts
4
+ /**
5
+ * Pointer motion as a complete, bounded schedule, independent of browser input and transport. A
6
+ * presenter takes a planner as a value: `lognormal` here, or another, such as recorded strokes.
7
+ *
8
+ * @since 0.3.0
9
+ */
10
+ var Motion_exports = /* @__PURE__ */ __exportAll({
11
+ Plan: () => Plan,
12
+ Point: () => Point,
13
+ Sample: () => Sample,
14
+ lognormal: () => lognormal,
15
+ maximumDurationMillis: () => maximumDurationMillis,
16
+ maximumSamples: () => maximumSamples
17
+ });
18
+ const maximumSamples = 2048;
19
+ const maximumDurationMillis = 5e3;
20
+ const Point = Schema.Struct({
21
+ x: Schema.Finite,
22
+ y: Schema.Finite
23
+ });
24
+ const Sample = Schema.Struct({
25
+ x: Schema.Finite,
26
+ y: Schema.Finite,
27
+ afterMillis: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(maximumDurationMillis))
28
+ });
29
+ /** A complete glide: 1 to 2,048 samples whose offsets never decrease. */
30
+ const Plan = Schema.Array(Sample).check(Schema.isBetweenLength(1, maximumSamples), Schema.makeFilter((samples) => samples.every((sample, index) => sample.afterMillis >= (samples[index - 1]?.afterMillis ?? 0)), { expected: "nondecreasing sample offsets" }));
31
+ const tuning = {
32
+ intercept: 115.8,
33
+ slope: 291.8,
34
+ durationSigma: .407,
35
+ amplitude: .989,
36
+ amplitudeSigma: .138,
37
+ lateral: .0934,
38
+ primaryDuration: .512,
39
+ correctionDuration: .586,
40
+ overlap: .452,
41
+ sigmaLow: .155,
42
+ sigmaHigh: .53
43
+ };
44
+ const sampleMillis = 16.7;
45
+ const normal = Effect.gen(function* () {
46
+ return Math.sqrt(-2 * Math.log(1 - (yield* Random.next))) * Math.cos(2 * Math.PI * (yield* Random.next));
47
+ });
48
+ const erf = (value) => {
49
+ const x = Math.abs(value);
50
+ const t = 1 / (1 + .3275911 * x);
51
+ const polynomial = ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - .284496736) * t + .254829592) * t;
52
+ return Math.sign(value) * (1 - polynomial * Math.exp(-x * x));
53
+ };
54
+ const cumulative = (time, onset, mu, sigma) => time <= onset ? 0 : (1 + erf((Math.log(time - onset) - mu) / (sigma * Math.SQRT2))) / 2;
55
+ /**
56
+ * The research's tuned two-stroke sigma-lognormal model: a fast stroke toward the target and a
57
+ * slower one that homes in on it, with a little bend, drawn from `Random`.
58
+ */
59
+ const lognormal = { plan: Effect.fnUntraced(function* (from, to) {
60
+ const scale = Math.max(1, Math.abs(from.x), Math.abs(from.y), Math.abs(to.x), Math.abs(to.y));
61
+ const startX = from.x / scale;
62
+ const startY = from.y / scale;
63
+ const deltaX = to.x / scale - startX;
64
+ const deltaY = to.y / scale - startY;
65
+ const distance = Math.hypot(deltaX, deltaY);
66
+ if (distance * scale < 2) return [{
67
+ ...to,
68
+ afterMillis: 0
69
+ }];
70
+ const unitX = deltaX / distance;
71
+ const unitY = deltaY / distance;
72
+ const drawnDuration = (tuning.intercept + tuning.slope * Math.log2(1 + distance * (scale / 40))) * Math.exp((yield* normal) * tuning.durationSigma);
73
+ const endFactor = Math.max(tuning.primaryDuration, tuning.overlap * tuning.primaryDuration + tuning.correctionDuration);
74
+ const lastRegularSample = Math.floor(maximumDurationMillis / sampleMillis) * sampleMillis;
75
+ const duration = Math.min(drawnDuration, lastRegularSample / endFactor);
76
+ const amplitude = distance * (tuning.amplitude + (yield* normal) * tuning.amplitudeSigma);
77
+ const angle = (yield* normal) * Math.PI / 60;
78
+ const primaryX = amplitude * (unitX * Math.cos(angle) - unitY * Math.sin(angle));
79
+ const primaryY = amplitude * (unitY * Math.cos(angle) + unitX * Math.sin(angle));
80
+ const sigma1 = yield* Random.nextBetween(tuning.sigmaLow, tuning.sigmaHigh);
81
+ const primaryDuration = tuning.primaryDuration * duration;
82
+ const mu1 = Math.log(primaryDuration / (Math.exp(3 * sigma1) - Math.exp(-3 * sigma1)));
83
+ const onset1 = -Math.exp(mu1 - 3 * sigma1);
84
+ const sigma2 = yield* Random.nextBetween(tuning.sigmaLow, tuning.sigmaHigh);
85
+ const correctionDuration = tuning.correctionDuration * duration;
86
+ const mu2 = Math.log(correctionDuration / (Math.exp(3 * sigma2) - Math.exp(-3 * sigma2)));
87
+ const onset2 = tuning.overlap * primaryDuration - Math.exp(mu2 - 3 * sigma2);
88
+ const end = Math.max(primaryDuration, onset2 + Math.exp(mu2 + 3 * sigma2));
89
+ const centered = Math.max(Number.EPSILON, yield* Random.next) - .5;
90
+ const lateral = -tuning.lateral * Math.sign(centered) * Math.log(1 - 2 * Math.abs(centered)) * distance;
91
+ const count = Math.min(Math.ceil(end / sampleMillis), Math.floor(lastRegularSample / sampleMillis));
92
+ const samples = [];
93
+ let last = from;
94
+ const coordinate = (value) => Math.round(Math.max(-Number.MAX_VALUE, Math.min(Number.MAX_VALUE, value * scale)));
95
+ for (let index = 0; index < count; index++) {
96
+ const afterMillis = index * sampleMillis;
97
+ const first = cumulative(afterMillis, onset1, mu1, sigma1);
98
+ const second = cumulative(afterMillis, onset2, mu2, sigma2);
99
+ const bend = lateral * Math.sin(Math.PI * first);
100
+ const x = coordinate(startX + primaryX * first + (deltaX - primaryX) * second - unitY * bend);
101
+ const y = coordinate(startY + primaryY * first + (deltaY - primaryY) * second + unitX * bend);
102
+ if (x === last.x && y === last.y) continue;
103
+ last = {
104
+ x,
105
+ y
106
+ };
107
+ samples.push({
108
+ x,
109
+ y,
110
+ afterMillis
111
+ });
112
+ }
113
+ samples.push({
114
+ ...to,
115
+ afterMillis: count * sampleMillis
116
+ });
117
+ return samples;
118
+ }) };
119
+ //#endregion
120
+ export { Plan, Point, Sample, lognormal, maximumDurationMillis, maximumSamples, Motion_exports as t };
121
+
122
+ //# sourceMappingURL=Motion.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Motion.mjs","names":[],"sources":["../src/Motion.ts"],"sourcesContent":["/**\n * Pointer motion as a complete, bounded schedule, independent of browser input and transport. A\n * presenter takes a planner as a value: `lognormal` here, or another, such as recorded strokes.\n *\n * @since 0.3.0\n */\nimport { Effect, Random, Schema } from \"effect\";\n\nexport const maximumSamples = 2048;\nexport const maximumDurationMillis = 5000;\n\nexport const Point = Schema.Struct({ x: Schema.Finite, y: Schema.Finite });\nexport type Point = typeof Point.Type;\n\nexport const Sample = Schema.Struct({\n x: Schema.Finite,\n y: Schema.Finite,\n afterMillis: Schema.Finite.check(\n Schema.isGreaterThanOrEqualTo(0),\n Schema.isLessThanOrEqualTo(maximumDurationMillis),\n ),\n});\n\nexport type Sample = typeof Sample.Type;\n\n/** A complete glide: 1 to 2,048 samples whose offsets never decrease. */\nexport const Plan = Schema.Array(Sample).check(\n Schema.isBetweenLength(1, maximumSamples),\n Schema.makeFilter(\n (samples) =>\n samples.every(\n (sample, index) => sample.afterMillis >= (samples[index - 1]?.afterMillis ?? 0),\n ),\n { expected: \"nondecreasing sample offsets\" },\n ),\n);\n\nexport type Plan = typeof Plan.Type;\n\nexport interface Service {\n /**\n * Offsets start when the performer publishes the plan, before its first input. The browser\n * decodes the result as a `Plan` whose last sample must be exactly `to`. Glide time has a budget\n * of its own, outside the browser's `actionTimeout`, so a drag may glide twice for 5 seconds.\n */\n readonly plan: (from: Point, to: Point) => Effect.Effect<ReadonlyArray<Sample>>;\n}\n\n// These are the research's tuned two-stroke parameters. Width scaling was not evaluated, so the\n// model keeps its 40px reference width rather than implying measured target-size behavior.\nconst tuning = {\n intercept: 115.8,\n slope: 291.8,\n durationSigma: 0.407,\n amplitude: 0.989,\n amplitudeSigma: 0.138,\n lateral: 0.0934,\n primaryDuration: 0.512,\n correctionDuration: 0.586,\n overlap: 0.452,\n sigmaLow: 0.155,\n sigmaHigh: 0.53,\n} as const;\n\nconst sampleMillis = 16.7;\n\nconst normal = Effect.gen(function* () {\n const radius = Math.sqrt(-2 * Math.log(1 - (yield* Random.next)));\n\n return radius * Math.cos(2 * Math.PI * (yield* Random.next));\n});\n\n// Abramowitz-Stegun 7.1.26 supplies erf without another runtime dependency (error < 1.5e-7).\nconst erf = (value: number) => {\n const x = Math.abs(value);\n const t = 1 / (1 + 0.3275911 * x);\n\n const polynomial =\n ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t;\n\n return Math.sign(value) * (1 - polynomial * Math.exp(-x * x));\n};\n\nconst cumulative = (time: number, onset: number, mu: number, sigma: number) =>\n time <= onset ? 0 : (1 + erf((Math.log(time - onset) - mu) / (sigma * Math.SQRT2))) / 2;\n\nconst plan: Service[\"plan\"] = Effect.fnUntraced(function* (from, to) {\n // Work in normalized coordinates so finite extreme inputs cannot overflow the displacement.\n const scale = Math.max(1, Math.abs(from.x), Math.abs(from.y), Math.abs(to.x), Math.abs(to.y));\n const startX = from.x / scale;\n const startY = from.y / scale;\n const deltaX = to.x / scale - startX;\n const deltaY = to.y / scale - startY;\n const distance = Math.hypot(deltaX, deltaY);\n\n if (distance * scale < 2) return [{ ...to, afterMillis: 0 }];\n const unitX = deltaX / distance;\n const unitY = deltaY / distance;\n\n const drawnDuration =\n (tuning.intercept + tuning.slope * Math.log2(1 + distance * (scale / 40))) *\n Math.exp((yield* normal) * tuning.durationSigma);\n\n const endFactor = Math.max(\n tuning.primaryDuration,\n tuning.overlap * tuning.primaryDuration + tuning.correctionDuration,\n );\n\n // Bound the duration before evaluating either stroke: dropping a sampled tail would remove\n // its homing correction. The last regular 16.7ms sample remains inside the public time bound.\n const lastRegularSample = Math.floor(maximumDurationMillis / sampleMillis) * sampleMillis;\n const duration = Math.min(drawnDuration, lastRegularSample / endFactor);\n const amplitude = distance * (tuning.amplitude + (yield* normal) * tuning.amplitudeSigma);\n const angle = ((yield* normal) * Math.PI) / 60;\n const primaryX = amplitude * (unitX * Math.cos(angle) - unitY * Math.sin(angle));\n const primaryY = amplitude * (unitY * Math.cos(angle) + unitX * Math.sin(angle));\n const sigma1 = yield* Random.nextBetween(tuning.sigmaLow, tuning.sigmaHigh);\n const primaryDuration = tuning.primaryDuration * duration;\n const mu1 = Math.log(primaryDuration / (Math.exp(3 * sigma1) - Math.exp(-3 * sigma1)));\n const onset1 = -Math.exp(mu1 - 3 * sigma1);\n const sigma2 = yield* Random.nextBetween(tuning.sigmaLow, tuning.sigmaHigh);\n const correctionDuration = tuning.correctionDuration * duration;\n const mu2 = Math.log(correctionDuration / (Math.exp(3 * sigma2) - Math.exp(-3 * sigma2)));\n const onset2 = tuning.overlap * primaryDuration - Math.exp(mu2 - 3 * sigma2);\n const end = Math.max(primaryDuration, onset2 + Math.exp(mu2 + 3 * sigma2));\n const centered = Math.max(Number.EPSILON, yield* Random.next) - 0.5;\n\n const lateral =\n -tuning.lateral * Math.sign(centered) * Math.log(1 - 2 * Math.abs(centered)) * distance;\n\n const count = Math.min(\n Math.ceil(end / sampleMillis),\n Math.floor(lastRegularSample / sampleMillis),\n );\n\n const samples: Array<Sample> = [];\n let last: Point = from;\n\n const coordinate = (value: number) =>\n Math.round(Math.max(-Number.MAX_VALUE, Math.min(Number.MAX_VALUE, value * scale)));\n\n for (let index = 0; index < count; index++) {\n const afterMillis = index * sampleMillis;\n const first = cumulative(afterMillis, onset1, mu1, sigma1);\n const second = cumulative(afterMillis, onset2, mu2, sigma2);\n const bend = lateral * Math.sin(Math.PI * first);\n const x = coordinate(startX + primaryX * first + (deltaX - primaryX) * second - unitY * bend);\n const y = coordinate(startY + primaryY * first + (deltaY - primaryY) * second + unitX * bend);\n\n // A pointer that stays on its pixel reports nothing; the model's timing is unchanged.\n if (x === last.x && y === last.y) continue;\n last = { x, y };\n samples.push({ x, y, afterMillis });\n }\n\n // The exact destination always lands at the model's end, even if a dwell precedes it.\n samples.push({ ...to, afterMillis: count * sampleMillis });\n\n return samples;\n});\n\n/**\n * The research's tuned two-stroke sigma-lognormal model: a fast stroke toward the target and a\n * slower one that homes in on it, with a little bend, drawn from `Random`.\n */\nexport const lognormal: Service = { plan };\n"],"mappings":"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"}