@xsynaptic/sonic-ui 0.4.0 → 0.6.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/README.md +3 -20
- package/dist/default/dat.js +51 -0
- package/dist/default/define/button.js +7 -0
- package/dist/default/define/envelope.js +7 -0
- package/dist/default/define/split.js +7 -0
- package/dist/default/define/switch.js +7 -0
- package/dist/default/define/waveform.js +7 -0
- package/dist/default/define/wavestrip.js +7 -0
- package/dist/default/define/xy.js +7 -0
- package/dist/default/define.js +7 -1
- package/dist/default/elements/button.js +165 -0
- package/dist/default/elements/dial.js +16 -22
- package/dist/default/elements/envelope.js +383 -0
- package/dist/default/elements/meter.js +155 -63
- package/dist/default/elements/number.js +22 -9
- package/dist/default/elements/radio-group.js +183 -0
- package/dist/default/elements/readout-claim.js +81 -0
- package/dist/default/elements/readout.js +35 -0
- package/dist/default/elements/segmented.js +9 -159
- package/dist/default/elements/slider.js +42 -51
- package/dist/default/elements/sonic-element.js +69 -31
- package/dist/default/elements/split.js +249 -0
- package/dist/default/elements/switch.js +273 -0
- package/dist/default/elements/value-element.js +694 -0
- package/dist/default/elements/{range-entry.js → value-entry.js} +3 -3
- package/dist/default/elements/wave-element.js +80 -0
- package/dist/default/elements/waveform.js +328 -0
- package/dist/default/elements/wavestrip.js +185 -0
- package/dist/default/elements/xy.js +487 -0
- package/dist/default/index.js +10 -2
- package/dist/default/lib/adsr-shape.js +113 -0
- package/dist/default/lib/ballistics.js +55 -0
- package/dist/default/lib/canvas-surface.js +203 -0
- package/dist/default/lib/copy-node.js +32 -2
- package/dist/default/lib/crossfade.js +22 -0
- package/dist/default/lib/distribute.js +215 -0
- package/dist/default/lib/drag-step.js +99 -0
- package/dist/default/lib/envelope-curve.js +9 -0
- package/dist/default/lib/field.js +94 -0
- package/dist/default/lib/focus-by-pointer.js +28 -0
- package/dist/default/lib/frame-timeline.js +63 -0
- package/dist/default/lib/hold.js +38 -0
- package/dist/default/lib/marker-band.js +80 -0
- package/dist/default/lib/marker-rider.js +120 -0
- package/dist/default/lib/math.js +17 -3
- package/dist/default/lib/modifier-press.js +11 -0
- package/dist/default/lib/number-list.js +6 -6
- package/dist/default/lib/pointer-drag.js +46 -0
- package/dist/default/lib/render.js +20 -5
- package/dist/default/lib/resample-peaks.js +46 -0
- package/dist/default/lib/strip-scene.js +48 -0
- package/dist/default/lib/taper.js +23 -17
- package/dist/default/lib/time-regions.js +8 -0
- package/dist/default/lib/tracking-clock.js +35 -0
- package/dist/default/lib/value-mapping.js +115 -0
- package/dist/default/lib/waveform-buckets.js +41 -0
- package/dist/default/lib/write-attribute.js +1 -0
- package/dist/dev/dat.d.ts +19 -0
- package/dist/dev/dat.js +52 -0
- package/dist/dev/dat.js.map +1 -0
- package/dist/dev/define/button.d.ts +2 -0
- package/dist/dev/define/button.js +8 -0
- package/dist/dev/define/button.js.map +1 -0
- package/dist/dev/define/dial.js.map +1 -1
- package/dist/dev/define/envelope.d.ts +2 -0
- package/dist/dev/define/envelope.js +8 -0
- package/dist/dev/define/envelope.js.map +1 -0
- package/dist/dev/define/meter.js.map +1 -1
- package/dist/dev/define/number.js.map +1 -1
- package/dist/dev/define/segmented.js.map +1 -1
- package/dist/dev/define/slider.js.map +1 -1
- package/dist/dev/define/split.d.ts +2 -0
- package/dist/dev/define/split.js +8 -0
- package/dist/dev/define/split.js.map +1 -0
- package/dist/dev/define/switch.d.ts +2 -0
- package/dist/dev/define/switch.js +8 -0
- package/dist/dev/define/switch.js.map +1 -0
- package/dist/dev/define/waveform.d.ts +2 -0
- package/dist/dev/define/waveform.js +8 -0
- package/dist/dev/define/waveform.js.map +1 -0
- package/dist/dev/define/wavestrip.d.ts +2 -0
- package/dist/dev/define/wavestrip.js +8 -0
- package/dist/dev/define/wavestrip.js.map +1 -0
- package/dist/dev/define/xy.d.ts +2 -0
- package/dist/dev/define/xy.js +8 -0
- package/dist/dev/define/xy.js.map +1 -0
- package/dist/dev/define.d.ts +15 -3
- package/dist/dev/define.js +7 -1
- package/dist/dev/elements/{key.d.ts → button.d.ts} +8 -6
- package/dist/dev/elements/button.js +166 -0
- package/dist/dev/elements/button.js.map +1 -0
- package/dist/dev/elements/dial.d.ts +6 -4
- package/dist/dev/elements/dial.js +16 -22
- package/dist/dev/elements/dial.js.map +1 -1
- package/dist/dev/elements/envelope.d.ts +36 -0
- package/dist/dev/elements/envelope.js +388 -0
- package/dist/dev/elements/envelope.js.map +1 -0
- package/dist/dev/elements/form-element.d.ts +1 -1
- package/dist/dev/elements/form-element.js.map +1 -1
- package/dist/dev/elements/meter.d.ts +11 -6
- package/dist/dev/elements/meter.js +155 -63
- package/dist/dev/elements/meter.js.map +1 -1
- package/dist/dev/elements/number.d.ts +7 -3
- package/dist/dev/elements/number.js +22 -9
- package/dist/dev/elements/number.js.map +1 -1
- package/dist/dev/elements/radio-group.d.ts +25 -0
- package/dist/dev/elements/radio-group.js +184 -0
- package/dist/dev/elements/radio-group.js.map +1 -0
- package/dist/dev/elements/readout-claim.js +82 -0
- package/dist/dev/elements/readout-claim.js.map +1 -0
- package/dist/dev/elements/readout.js +36 -0
- package/dist/dev/elements/readout.js.map +1 -0
- package/dist/dev/elements/segmented.d.ts +4 -10
- package/dist/dev/elements/segmented.js +9 -159
- package/dist/dev/elements/segmented.js.map +1 -1
- package/dist/dev/elements/slider.d.ts +6 -6
- package/dist/dev/elements/slider.js +42 -51
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/sonic-element.d.ts +6 -3
- package/dist/dev/elements/sonic-element.js +119 -40
- package/dist/dev/elements/sonic-element.js.map +1 -1
- package/dist/dev/elements/split.d.ts +21 -0
- package/dist/dev/elements/split.js +258 -0
- package/dist/dev/elements/split.js.map +1 -0
- package/dist/dev/elements/switch.d.ts +35 -0
- package/dist/dev/elements/switch.js +274 -0
- package/dist/dev/elements/switch.js.map +1 -0
- package/dist/dev/elements/value-element.d.ts +84 -0
- package/dist/dev/elements/value-element.js +695 -0
- package/dist/dev/elements/value-element.js.map +1 -0
- package/dist/dev/elements/{range-entry.js → value-entry.js} +4 -4
- package/dist/dev/elements/value-entry.js.map +1 -0
- package/dist/dev/elements/wave-element.d.ts +37 -0
- package/dist/dev/elements/wave-element.js +81 -0
- package/dist/dev/elements/wave-element.js.map +1 -0
- package/dist/dev/elements/waveform.d.ts +55 -0
- package/dist/dev/elements/waveform.js +329 -0
- package/dist/dev/elements/waveform.js.map +1 -0
- package/dist/dev/elements/wavestrip.d.ts +51 -0
- package/dist/dev/elements/wavestrip.js +186 -0
- package/dist/dev/elements/wavestrip.js.map +1 -0
- package/dist/dev/elements/xy.d.ts +62 -0
- package/dist/dev/elements/xy.js +488 -0
- package/dist/dev/elements/xy.js.map +1 -0
- package/dist/dev/index.d.ts +11 -2
- package/dist/dev/index.js +10 -2
- package/dist/dev/lib/adsr-shape.js +114 -0
- package/dist/dev/lib/adsr-shape.js.map +1 -0
- package/dist/dev/lib/ballistics.js +56 -0
- package/dist/dev/lib/ballistics.js.map +1 -0
- package/dist/dev/lib/canvas-surface.d.ts +26 -0
- package/dist/dev/lib/canvas-surface.js +204 -0
- package/dist/dev/lib/canvas-surface.js.map +1 -0
- package/dist/dev/lib/copy-node.js +32 -2
- package/dist/dev/lib/copy-node.js.map +1 -1
- package/dist/dev/lib/crossfade.d.ts +12 -0
- package/dist/dev/lib/crossfade.js +23 -0
- package/dist/dev/lib/crossfade.js.map +1 -0
- package/dist/dev/lib/distribute.d.ts +4 -0
- package/dist/dev/lib/distribute.js +216 -0
- package/dist/dev/lib/distribute.js.map +1 -0
- package/dist/dev/lib/drag-step.js +100 -0
- package/dist/dev/lib/drag-step.js.map +1 -0
- package/dist/dev/lib/envelope-curve.d.ts +4 -0
- package/dist/dev/lib/envelope-curve.js +10 -0
- package/dist/dev/lib/envelope-curve.js.map +1 -0
- package/dist/dev/lib/field.d.ts +4 -0
- package/dist/dev/lib/field.js +95 -0
- package/dist/dev/lib/field.js.map +1 -0
- package/dist/dev/lib/focus-by-pointer.js +29 -0
- package/dist/dev/lib/focus-by-pointer.js.map +1 -0
- package/dist/dev/lib/frame-timeline.js +64 -0
- package/dist/dev/lib/frame-timeline.js.map +1 -0
- package/dist/dev/lib/hold.js +39 -0
- package/dist/dev/lib/hold.js.map +1 -0
- package/dist/dev/lib/marker-band.js +81 -0
- package/dist/dev/lib/marker-band.js.map +1 -0
- package/dist/dev/lib/marker-rider.js +121 -0
- package/dist/dev/lib/marker-rider.js.map +1 -0
- package/dist/dev/lib/math.js +17 -3
- package/dist/dev/lib/math.js.map +1 -1
- package/dist/dev/lib/modifier-press.js +12 -0
- package/dist/dev/lib/modifier-press.js.map +1 -0
- package/dist/dev/lib/number-list.js +6 -6
- package/dist/dev/lib/number-list.js.map +1 -1
- package/dist/dev/lib/pointer-drag.js +47 -0
- package/dist/dev/lib/pointer-drag.js.map +1 -0
- package/dist/dev/lib/read-px-property.js.map +1 -1
- package/dist/dev/lib/render.js +20 -5
- package/dist/dev/lib/render.js.map +1 -1
- package/dist/dev/lib/resample-peaks.js +47 -0
- package/dist/dev/lib/resample-peaks.js.map +1 -0
- package/dist/dev/lib/strip-scene.js +49 -0
- package/dist/dev/lib/strip-scene.js.map +1 -0
- package/dist/dev/lib/taper.js +23 -17
- package/dist/dev/lib/taper.js.map +1 -1
- package/dist/dev/lib/time-regions.d.ts +4 -0
- package/dist/dev/lib/time-regions.js +9 -0
- package/dist/dev/lib/time-regions.js.map +1 -0
- package/dist/dev/lib/tracking-clock.js +36 -0
- package/dist/dev/lib/tracking-clock.js.map +1 -0
- package/dist/dev/lib/value-mapping.d.ts +23 -0
- package/dist/dev/lib/value-mapping.js +116 -0
- package/dist/dev/lib/value-mapping.js.map +1 -0
- package/dist/dev/lib/waveform-buckets.d.ts +9 -0
- package/dist/dev/lib/waveform-buckets.js +42 -0
- package/dist/dev/lib/waveform-buckets.js.map +1 -0
- package/dist/dev/lib/write-attribute.js +1 -0
- package/dist/dev/lib/write-attribute.js.map +1 -1
- package/dist/skins/amber.css +8 -0
- package/dist/skins/flat.css +25 -0
- package/dist/skins/ivory.css +2 -2
- package/dist/skins/lime.css +2 -0
- package/dist/skins/slate.css +2 -0
- package/dist/styles/button.css +240 -0
- package/dist/styles/controls.css +8 -1
- package/dist/styles/dial.css +110 -56
- package/dist/styles/envelope.css +204 -0
- package/dist/styles/led.css +39 -8
- package/dist/styles/material.css +343 -100
- package/dist/styles/meter.css +41 -52
- package/dist/styles/number.css +5 -1
- package/dist/styles/panel.css +61 -0
- package/dist/styles/ring.css +74 -0
- package/dist/styles/segmented.css +46 -51
- package/dist/styles/slider.css +177 -40
- package/dist/styles/switch.css +386 -0
- package/dist/styles/waveform.css +196 -0
- package/dist/styles/wavestrip.css +232 -0
- package/dist/styles/xy.css +241 -0
- package/package.json +15 -6
- package/dist/default/define/key.js +0 -7
- package/dist/default/elements/key.js +0 -169
- package/dist/default/elements/range-element.js +0 -546
- package/dist/default/lib/detent.js +0 -17
- package/dist/dev/define/key.d.ts +0 -2
- package/dist/dev/define/key.js +0 -8
- package/dist/dev/define/key.js.map +0 -1
- package/dist/dev/elements/key.js +0 -170
- package/dist/dev/elements/key.js.map +0 -1
- package/dist/dev/elements/range-element.d.ts +0 -64
- package/dist/dev/elements/range-element.js +0 -547
- package/dist/dev/elements/range-element.js.map +0 -1
- package/dist/dev/elements/range-entry.js.map +0 -1
- package/dist/dev/lib/detent.js +0 -18
- package/dist/dev/lib/detent.js.map +0 -1
- package/dist/styles/key.css +0 -242
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { clamp, trimFloat } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/distribute.ts
|
|
4
|
+
const epsilon = 1e-9;
|
|
5
|
+
function sumOf(values) {
|
|
6
|
+
return values.reduce((total, value) => total + value, 0);
|
|
7
|
+
}
|
|
8
|
+
function others(members, index) {
|
|
9
|
+
return members.filter((_member, at) => at !== index);
|
|
10
|
+
}
|
|
11
|
+
function splitLimits(members, index, total) {
|
|
12
|
+
const member = members[index];
|
|
13
|
+
if (!member) return [NaN, NaN];
|
|
14
|
+
const rest = others(members, index);
|
|
15
|
+
const free = rest.filter((other) => other.isFree);
|
|
16
|
+
if (free.length === 0) return [member.value, member.value];
|
|
17
|
+
const room = total - sumOf(rest.filter((other) => !other.isFree).map((other) => other.value));
|
|
18
|
+
return [Math.max(member.min, room - sumOf(free.map((other) => other.max))), Math.min(member.max, room - sumOf(free.map((other) => other.min)))];
|
|
19
|
+
}
|
|
20
|
+
function spreadWeighted(owed, pool) {
|
|
21
|
+
const shares = /* @__PURE__ */ new Map();
|
|
22
|
+
let remaining = owed;
|
|
23
|
+
let active = [...pool];
|
|
24
|
+
while (active.length > 0 && Math.abs(remaining) > epsilon) {
|
|
25
|
+
const weights = sumOf(active.map(({ weight }) => weight));
|
|
26
|
+
const count = active.length;
|
|
27
|
+
const shareOf = (entry) => remaining * (weights > 0 ? entry.weight / weights : 1 / count);
|
|
28
|
+
const full = active.filter((entry) => Math.abs(shareOf(entry)) >= entry.room);
|
|
29
|
+
if (full.length === 0) {
|
|
30
|
+
for (const entry of active) shares.set(entry.index, shareOf(entry));
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
const sign = Math.sign(remaining);
|
|
34
|
+
for (const entry of full) {
|
|
35
|
+
shares.set(entry.index, sign * entry.room);
|
|
36
|
+
remaining -= sign * entry.room;
|
|
37
|
+
}
|
|
38
|
+
active = active.filter((entry) => !full.includes(entry));
|
|
39
|
+
}
|
|
40
|
+
return shares;
|
|
41
|
+
}
|
|
42
|
+
function spreadInOrder(owed, pool) {
|
|
43
|
+
const shares = /* @__PURE__ */ new Map();
|
|
44
|
+
let remaining = owed;
|
|
45
|
+
for (const entry of pool) {
|
|
46
|
+
if (Math.abs(remaining) <= epsilon) break;
|
|
47
|
+
const share = Math.sign(remaining) * Math.min(entry.room, Math.abs(remaining));
|
|
48
|
+
shares.set(entry.index, share);
|
|
49
|
+
remaining -= share;
|
|
50
|
+
}
|
|
51
|
+
return shares;
|
|
52
|
+
}
|
|
53
|
+
function poolOrder(members, index) {
|
|
54
|
+
const count = members.length;
|
|
55
|
+
const start = index + 1;
|
|
56
|
+
return Array.from({ length: count }, (_entry, offset) => (start + offset) % count).filter((at) => at !== index && members[at]?.isFree === true);
|
|
57
|
+
}
|
|
58
|
+
function stepWithin(member, from, owed) {
|
|
59
|
+
const next = member.stepFrom(from, owed > 0 ? 1 : -1);
|
|
60
|
+
const change = trimFloat(next - from);
|
|
61
|
+
const isInside = next >= member.min && next <= member.max;
|
|
62
|
+
return change !== 0 && isInside && Math.abs(change) <= Math.abs(owed) + epsilon ? next : void 0;
|
|
63
|
+
}
|
|
64
|
+
function stepToward(member, from, owed) {
|
|
65
|
+
let value = from;
|
|
66
|
+
let remainder = owed;
|
|
67
|
+
while (Math.abs(remainder) > epsilon) {
|
|
68
|
+
const next = stepWithin(member, value, remainder);
|
|
69
|
+
if (next === void 0) break;
|
|
70
|
+
remainder = trimFloat(remainder - (next - value));
|
|
71
|
+
value = next;
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function byShortfall(values, { exact, order }, direction) {
|
|
76
|
+
const shortfall = (index) => {
|
|
77
|
+
const value = values[index] ?? NaN;
|
|
78
|
+
return trimFloat(direction * ((exact.get(index) ?? value) - value));
|
|
79
|
+
};
|
|
80
|
+
return order.toReversed().toSorted((first, second) => shortfall(second) - shortfall(first));
|
|
81
|
+
}
|
|
82
|
+
function stepEach(members, values, pass) {
|
|
83
|
+
let remainder = pass.owed;
|
|
84
|
+
for (const index of pass.neediest) {
|
|
85
|
+
const member = members[index];
|
|
86
|
+
const value = values[index];
|
|
87
|
+
const next = member && value !== void 0 ? stepWithin(member, value, remainder) : void 0;
|
|
88
|
+
if (value === void 0 || next === void 0) continue;
|
|
89
|
+
values[index] = next;
|
|
90
|
+
remainder = trimFloat(remainder - (next - value));
|
|
91
|
+
}
|
|
92
|
+
return remainder;
|
|
93
|
+
}
|
|
94
|
+
function settle(members, values, settling) {
|
|
95
|
+
let remainder = trimFloat(settling.total - sumOf(values));
|
|
96
|
+
let before = NaN;
|
|
97
|
+
while (remainder !== before && Math.abs(remainder) > epsilon) {
|
|
98
|
+
before = remainder;
|
|
99
|
+
remainder = stepEach(members, values, {
|
|
100
|
+
neediest: byShortfall(values, settling, Math.sign(remainder)),
|
|
101
|
+
owed: remainder
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return remainder;
|
|
105
|
+
}
|
|
106
|
+
function pooled(members, order, owed) {
|
|
107
|
+
return order.flatMap((at) => {
|
|
108
|
+
const member = members[at];
|
|
109
|
+
if (!member) return [];
|
|
110
|
+
const room = owed > 0 ? member.max - member.value : member.value - member.min;
|
|
111
|
+
return [{
|
|
112
|
+
index: at,
|
|
113
|
+
room: Math.max(0, room),
|
|
114
|
+
weight: member.value - member.min
|
|
115
|
+
}];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function shares(pool, owed, mode) {
|
|
119
|
+
if (mode === "cascade") return spreadInOrder(owed, pool);
|
|
120
|
+
if (mode === "equal") return spreadWeighted(owed, pool.map((entry) => ({
|
|
121
|
+
...entry,
|
|
122
|
+
weight: 1
|
|
123
|
+
})));
|
|
124
|
+
return spreadWeighted(owed, pool);
|
|
125
|
+
}
|
|
126
|
+
function landMover(members, values, move) {
|
|
127
|
+
const { index, target, total } = move;
|
|
128
|
+
const mover = members[index];
|
|
129
|
+
if (!mover?.isFree || !Number.isFinite(target)) return void 0;
|
|
130
|
+
const next = mover.snap(clamp(target, ...splitLimits(members, index, total)));
|
|
131
|
+
values[index] = next;
|
|
132
|
+
return mover.value - next;
|
|
133
|
+
}
|
|
134
|
+
function landShares(members, values, spread) {
|
|
135
|
+
const exact = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const [at, share] of spread) {
|
|
137
|
+
const member = members[at];
|
|
138
|
+
if (!member) continue;
|
|
139
|
+
exact.set(at, member.value + share);
|
|
140
|
+
values[at] = clamp(member.snap(member.value + share), member.min, member.max);
|
|
141
|
+
}
|
|
142
|
+
return exact;
|
|
143
|
+
}
|
|
144
|
+
function spread(members, rule, move) {
|
|
145
|
+
const values = members.map(({ value }) => value);
|
|
146
|
+
const owed = move ? landMover(members, values, {
|
|
147
|
+
...move,
|
|
148
|
+
total: rule.total
|
|
149
|
+
}) : rule.total - sumOf(values);
|
|
150
|
+
if (owed === void 0) return void 0;
|
|
151
|
+
const order = poolOrder(members, move?.index ?? -1);
|
|
152
|
+
return {
|
|
153
|
+
left: settle(members, values, {
|
|
154
|
+
exact: landShares(members, values, shares(pooled(members, order, owed), owed, rule.mode)),
|
|
155
|
+
order,
|
|
156
|
+
total: rule.total
|
|
157
|
+
}),
|
|
158
|
+
order,
|
|
159
|
+
values
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function smallestStep(members, { left, order, values }) {
|
|
163
|
+
return order.flatMap((at) => {
|
|
164
|
+
const member = members[at];
|
|
165
|
+
const value = values[at];
|
|
166
|
+
if (!member || value === void 0) return [];
|
|
167
|
+
const next = member.stepFrom(value, left > 0 ? 1 : -1);
|
|
168
|
+
return next >= member.min && next <= member.max && next !== value ? [trimFloat(next - value)] : [];
|
|
169
|
+
}).toSorted((one, other) => Math.abs(one) - Math.abs(other))[0];
|
|
170
|
+
}
|
|
171
|
+
function moverTargets(members, landing, index) {
|
|
172
|
+
const mover = members[index];
|
|
173
|
+
const landed = landing.values[index];
|
|
174
|
+
if (!mover || landed === void 0) return [];
|
|
175
|
+
const step = smallestStep(members, landing);
|
|
176
|
+
const held = stepToward(mover, landed, landing.left);
|
|
177
|
+
return step === void 0 ? [held] : [held, stepToward(mover, landed, trimFloat(landing.left - step))];
|
|
178
|
+
}
|
|
179
|
+
function nearest(candidates, move, from) {
|
|
180
|
+
const { direction = 0, index } = move;
|
|
181
|
+
const moverOf = (candidate) => candidate[index] ?? NaN;
|
|
182
|
+
const [first, ...rest] = candidates.toSorted((one, other) => Math.abs(moverOf(one) - from.landed) - Math.abs(moverOf(other) - from.landed));
|
|
183
|
+
if (!first || direction === 0 || moverOf(first) !== from.start) return first;
|
|
184
|
+
return rest.find((candidate) => Math.sign(moverOf(candidate) - from.start) === direction) ?? first;
|
|
185
|
+
}
|
|
186
|
+
function close(members, rule, held) {
|
|
187
|
+
const { landing, move, mover } = held;
|
|
188
|
+
const { index } = move;
|
|
189
|
+
const landed = landing.values[index] ?? mover.value;
|
|
190
|
+
const targets = moverTargets(members, landing, index);
|
|
191
|
+
return nearest(targets.flatMap((target) => {
|
|
192
|
+
const attempt = spread(members, rule, {
|
|
193
|
+
index,
|
|
194
|
+
target
|
|
195
|
+
});
|
|
196
|
+
return attempt && Math.abs(attempt.left) <= epsilon ? [attempt.values] : [];
|
|
197
|
+
}), move, {
|
|
198
|
+
landed,
|
|
199
|
+
start: mover.value
|
|
200
|
+
}) ?? landing.values.with(index, targets[0] ?? landed);
|
|
201
|
+
}
|
|
202
|
+
function distribute(members, rule, move) {
|
|
203
|
+
const landing = spread(members, rule, move);
|
|
204
|
+
if (!landing) return members.map(({ value }) => value);
|
|
205
|
+
const mover = members[move?.index ?? -1];
|
|
206
|
+
if (!move || !mover || Math.abs(landing.left) <= epsilon) return landing.values;
|
|
207
|
+
return close(members, rule, {
|
|
208
|
+
landing,
|
|
209
|
+
move,
|
|
210
|
+
mover
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
export { distribute, splitLimits };
|
|
216
|
+
//# sourceMappingURL=distribute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"distribute.js","names":[],"sources":["../../../src/lib/distribute.ts"],"sourcesContent":["import { clamp, trimFloat } from '#lib/math.ts';\n\nexport interface SplitMember {\n\tisFree: boolean;\n\tmax: number;\n\tmin: number;\n\tsnap: (value: number) => number;\n\tstepFrom: (value: number, direction: -1 | 1) => number;\n\tvalue: number;\n}\n\nexport type SplitMode = 'cascade' | 'equal' | 'proportional';\n\ninterface SplitMove {\n\tdirection?: -1 | 0 | 1;\n\tindex: number;\n\ttarget: number;\n}\n\ninterface SplitRule {\n\tmode: SplitMode;\n\ttotal: number;\n}\n\ninterface Pooled {\n\tindex: number;\n\troom: number;\n\tweight: number;\n}\n\nconst epsilon = 1e-9;\n\nfunction sumOf(values: ReadonlyArray<number>): number {\n\treturn values.reduce((total, value) => total + value, 0);\n}\n\nfunction others(members: ReadonlyArray<SplitMember>, index: number): Array<SplitMember> {\n\treturn members.filter((_member, at) => at !== index);\n}\n\nexport function splitLimits(\n\tmembers: ReadonlyArray<SplitMember>,\n\tindex: number,\n\ttotal: number,\n): [number, number] {\n\tconst member = members[index];\n\tif (!member) return [NaN, NaN];\n\n\tconst rest = others(members, index);\n\tconst free = rest.filter((other) => other.isFree);\n\tif (free.length === 0) return [member.value, member.value];\n\n\tconst fixed = sumOf(rest.filter((other) => !other.isFree).map((other) => other.value));\n\tconst room = total - fixed;\n\n\treturn [\n\t\tMath.max(member.min, room - sumOf(free.map((other) => other.max))),\n\t\tMath.min(member.max, room - sumOf(free.map((other) => other.min))),\n\t];\n}\n\nfunction spreadWeighted(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\tlet active = [...pool];\n\n\twhile (active.length > 0 && Math.abs(remaining) > epsilon) {\n\t\tconst weights = sumOf(active.map(({ weight }) => weight));\n\t\tconst count = active.length;\n\t\tconst shareOf = (entry: Pooled): number =>\n\t\t\tremaining * (weights > 0 ? entry.weight / weights : 1 / count);\n\t\tconst full = active.filter((entry) => Math.abs(shareOf(entry)) >= entry.room);\n\n\t\tif (full.length === 0) {\n\t\t\tfor (const entry of active) shares.set(entry.index, shareOf(entry));\n\t\t\tbreak;\n\t\t}\n\n\t\tconst sign = Math.sign(remaining);\n\n\t\tfor (const entry of full) {\n\t\t\tshares.set(entry.index, sign * entry.room);\n\t\t\tremaining -= sign * entry.room;\n\t\t}\n\t\tactive = active.filter((entry) => !full.includes(entry));\n\t}\n\n\treturn shares;\n}\n\nfunction spreadInOrder(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\n\tfor (const entry of pool) {\n\t\tif (Math.abs(remaining) <= epsilon) break;\n\n\t\tconst share = Math.sign(remaining) * Math.min(entry.room, Math.abs(remaining));\n\n\t\tshares.set(entry.index, share);\n\t\tremaining -= share;\n\t}\n\n\treturn shares;\n}\n\nfunction poolOrder(members: ReadonlyArray<SplitMember>, index: number): Array<number> {\n\tconst count = members.length;\n\tconst start = index + 1;\n\n\treturn Array.from({ length: count }, (_entry, offset) => (start + offset) % count).filter(\n\t\t(at) => at !== index && members[at]?.isFree === true,\n\t);\n}\n\nfunction stepWithin(member: SplitMember, from: number, owed: number): number | undefined {\n\tconst next = member.stepFrom(from, owed > 0 ? 1 : -1);\n\tconst change = trimFloat(next - from);\n\tconst isInside = next >= member.min && next <= member.max;\n\n\treturn change !== 0 && isInside && Math.abs(change) <= Math.abs(owed) + epsilon\n\t\t? next\n\t\t: undefined;\n}\n\nfunction stepToward(member: SplitMember, from: number, owed: number): number {\n\tlet value = from;\n\tlet remainder = owed;\n\n\twhile (Math.abs(remainder) > epsilon) {\n\t\tconst next = stepWithin(member, value, remainder);\n\t\tif (next === undefined) break;\n\n\t\tremainder = trimFloat(remainder - (next - value));\n\t\tvalue = next;\n\t}\n\n\treturn value;\n}\n\ninterface Settling {\n\texact: ReadonlyMap<number, number>;\n\torder: ReadonlyArray<number>;\n\ttotal: number;\n}\n\n// Furthest from its exact share first; a tie goes to the last in pool order\nfunction byShortfall(\n\tvalues: ReadonlyArray<number>,\n\t{ exact, order }: Settling,\n\tdirection: number,\n): Array<number> {\n\tconst shortfall = (index: number): number => {\n\t\tconst value = values[index] ?? NaN;\n\n\t\treturn trimFloat(direction * ((exact.get(index) ?? value) - value));\n\t};\n\n\treturn order.toReversed().toSorted((first, second) => shortfall(second) - shortfall(first));\n}\n\nfunction stepEach(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tpass: { neediest: ReadonlyArray<number>; owed: number },\n): number {\n\tlet remainder = pass.owed;\n\n\tfor (const index of pass.neediest) {\n\t\tconst member = members[index];\n\t\tconst value = values[index];\n\t\tconst next = member && value !== undefined ? stepWithin(member, value, remainder) : undefined;\n\t\tif (value === undefined || next === undefined) continue;\n\n\t\tvalues[index] = next;\n\t\tremainder = trimFloat(remainder - (next - value));\n\t}\n\n\treturn remainder;\n}\n\nfunction settle(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tsettling: Settling,\n): number {\n\tlet remainder = trimFloat(settling.total - sumOf(values));\n\tlet before = NaN;\n\n\twhile (remainder !== before && Math.abs(remainder) > epsilon) {\n\t\tbefore = remainder;\n\t\tremainder = stepEach(members, values, {\n\t\t\tneediest: byShortfall(values, settling, Math.sign(remainder)),\n\t\t\towed: remainder,\n\t\t});\n\t}\n\n\treturn remainder;\n}\n\nfunction pooled(\n\tmembers: ReadonlyArray<SplitMember>,\n\torder: ReadonlyArray<number>,\n\towed: number,\n): Array<Pooled> {\n\treturn order.flatMap((at): Array<Pooled> => {\n\t\tconst member = members[at];\n\t\tif (!member) return [];\n\n\t\tconst room = owed > 0 ? member.max - member.value : member.value - member.min;\n\n\t\treturn [{ index: at, room: Math.max(0, room), weight: member.value - member.min }];\n\t});\n}\n\nfunction shares(pool: Array<Pooled>, owed: number, mode: SplitMode): Map<number, number> {\n\tif (mode === 'cascade') return spreadInOrder(owed, pool);\n\tif (mode === 'equal')\n\t\treturn spreadWeighted(\n\t\t\towed,\n\t\t\tpool.map((entry) => ({ ...entry, weight: 1 })),\n\t\t);\n\n\treturn spreadWeighted(owed, pool);\n}\n\nfunction landMover(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tmove: SplitMove & { total: number },\n): number | undefined {\n\tconst { index, target, total } = move;\n\tconst mover = members[index];\n\tif (!mover?.isFree || !Number.isFinite(target)) return undefined;\n\n\tconst next = mover.snap(clamp(target, ...splitLimits(members, index, total)));\n\n\tvalues[index] = next;\n\n\treturn mover.value - next;\n}\n\nfunction landShares(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tspread: ReadonlyMap<number, number>,\n): Map<number, number> {\n\tconst exact = new Map<number, number>();\n\n\tfor (const [at, share] of spread) {\n\t\tconst member = members[at];\n\t\tif (!member) continue;\n\n\t\texact.set(at, member.value + share);\n\t\tvalues[at] = clamp(member.snap(member.value + share), member.min, member.max);\n\t}\n\n\treturn exact;\n}\n\ninterface Spread {\n\tleft: number;\n\torder: ReadonlyArray<number>;\n\tvalues: Array<number>;\n}\n\nfunction spread(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\tmove?: SplitMove,\n): Spread | undefined {\n\tconst values = members.map(({ value }) => value);\n\tconst owed = move\n\t\t? landMover(members, values, { ...move, total: rule.total })\n\t\t: rule.total - sumOf(values);\n\tif (owed === undefined) return undefined;\n\n\tconst order = poolOrder(members, move?.index ?? -1);\n\tconst exact = landShares(members, values, shares(pooled(members, order, owed), owed, rule.mode));\n\n\treturn { left: settle(members, values, { exact, order, total: rule.total }), order, values };\n}\n\nfunction smallestStep(\n\tmembers: ReadonlyArray<SplitMember>,\n\t{ left, order, values }: Spread,\n): number | undefined {\n\tconst steps = order.flatMap((at) => {\n\t\tconst member = members[at];\n\t\tconst value = values[at];\n\t\tif (!member || value === undefined) return [];\n\n\t\tconst next = member.stepFrom(value, left > 0 ? 1 : -1);\n\t\tconst isInside = next >= member.min && next <= member.max;\n\n\t\treturn isInside && next !== value ? [trimFloat(next - value)] : [];\n\t});\n\n\treturn steps.toSorted((one, other) => Math.abs(one) - Math.abs(other))[0];\n}\n\n// The mover takes what is left on its own grid, or a sibling steps past the total and the mover gives the difference back\nfunction moverTargets(\n\tmembers: ReadonlyArray<SplitMember>,\n\tlanding: Spread,\n\tindex: number,\n): Array<number> {\n\tconst mover = members[index];\n\tconst landed = landing.values[index];\n\tif (!mover || landed === undefined) return [];\n\n\tconst step = smallestStep(members, landing);\n\tconst held = stepToward(mover, landed, landing.left);\n\n\treturn step === undefined\n\t\t? [held]\n\t\t: [held, stepToward(mover, landed, trimFloat(landing.left - step))];\n}\n\nfunction nearest(\n\tcandidates: Array<Array<number>>,\n\tmove: SplitMove,\n\tfrom: { landed: number; start: number },\n): Array<number> | undefined {\n\tconst { direction = 0, index } = move;\n\tconst moverOf = (candidate: Array<number>): number => candidate[index] ?? NaN;\n\tconst [first, ...rest] = candidates.toSorted(\n\t\t(one, other) => Math.abs(moverOf(one) - from.landed) - Math.abs(moverOf(other) - from.landed),\n\t);\n\tif (!first || direction === 0 || moverOf(first) !== from.start) return first;\n\n\treturn (\n\t\trest.find((candidate) => Math.sign(moverOf(candidate) - from.start) === direction) ?? first\n\t);\n}\n\nfunction close(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\theld: { landing: Spread; move: SplitMove; mover: SplitMember },\n): Array<number> {\n\tconst { landing, move, mover } = held;\n\tconst { index } = move;\n\tconst landed = landing.values[index] ?? mover.value;\n\tconst targets = moverTargets(members, landing, index);\n\tconst closed = targets.flatMap((target) => {\n\t\tconst attempt = spread(members, rule, { index, target });\n\n\t\treturn attempt && Math.abs(attempt.left) <= epsilon ? [attempt.values] : [];\n\t});\n\n\treturn (\n\t\tnearest(closed, move, { landed, start: mover.value }) ??\n\t\tlanding.values.with(index, targets[0] ?? landed)\n\t);\n}\n\nexport function distribute(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\tmove?: SplitMove,\n): Array<number> {\n\tconst landing = spread(members, rule, move);\n\tif (!landing) return members.map(({ value }) => value);\n\n\tconst mover = members[move?.index ?? -1];\n\tif (!move || !mover || Math.abs(landing.left) <= epsilon) return landing.values;\n\n\treturn close(members, rule, { landing, move, mover });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { clampProportion, wrapProportion } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/drag-step.ts
|
|
4
|
+
const fineFactor = .1;
|
|
5
|
+
const mouseDragThresholdPx = 3;
|
|
6
|
+
const touchDragThresholdPx = 7;
|
|
7
|
+
function dragThresholdPx(pointerType) {
|
|
8
|
+
return pointerType === "touch" ? touchDragThresholdPx : mouseDragThresholdPx;
|
|
9
|
+
}
|
|
10
|
+
function isAcross(from, to, proportion) {
|
|
11
|
+
return from !== proportion && (from - proportion) * (to - proportion) <= 0;
|
|
12
|
+
}
|
|
13
|
+
function passDetent(hold, [from, to], pace) {
|
|
14
|
+
if (hold.slack === void 0 && !isAcross(from, to, hold.proportion)) return {
|
|
15
|
+
proportion: to,
|
|
16
|
+
slack: void 0
|
|
17
|
+
};
|
|
18
|
+
const slack = (hold.slack ?? 0) + (to - hold.proportion) / pace;
|
|
19
|
+
if (Math.abs(slack) <= hold.zone) return {
|
|
20
|
+
proportion: void 0,
|
|
21
|
+
slack
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
proportion: hold.proportion + (slack - Math.sign(slack) * hold.zone) * pace,
|
|
25
|
+
slack: void 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function nearestTurn(hold, from) {
|
|
29
|
+
return {
|
|
30
|
+
...hold,
|
|
31
|
+
proportion: hold.proportion + Math.round(from - hold.proportion)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function land(mapping, state, proportion) {
|
|
35
|
+
const rawProportion = mapping.isWrapping ? wrapProportion(proportion) : clampProportion(proportion);
|
|
36
|
+
return {
|
|
37
|
+
state: {
|
|
38
|
+
...state,
|
|
39
|
+
rawProportion
|
|
40
|
+
},
|
|
41
|
+
value: mapping.snap(mapping.valueAt(rawProportion))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function startDrag(mapping, start) {
|
|
45
|
+
const { detent: wanted, from, position, proportion, ...fixed } = start;
|
|
46
|
+
const state = {
|
|
47
|
+
...fixed,
|
|
48
|
+
isEngaged: false,
|
|
49
|
+
lastPosition: position,
|
|
50
|
+
rawProportion: proportion,
|
|
51
|
+
startPosition: position
|
|
52
|
+
};
|
|
53
|
+
if (!wanted) return state;
|
|
54
|
+
const value = mapping.snap(wanted.value);
|
|
55
|
+
const detent = {
|
|
56
|
+
proportion: mapping.proportionOf(value),
|
|
57
|
+
slack: value === from ? 0 : void 0,
|
|
58
|
+
value,
|
|
59
|
+
zone: wanted.zone
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
...state,
|
|
63
|
+
detent
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function stepDrag(mapping, state, move) {
|
|
67
|
+
if (!(state.isEngaged || move.isOutside || Math.abs(move.position - state.startPosition) >= state.thresholdPx)) return {
|
|
68
|
+
state,
|
|
69
|
+
value: void 0
|
|
70
|
+
};
|
|
71
|
+
const pace = move.isFine ? fineFactor : 1;
|
|
72
|
+
const to = state.rawProportion + (move.position - state.lastPosition) / state.travelPx * pace;
|
|
73
|
+
const moved = {
|
|
74
|
+
...state,
|
|
75
|
+
isEngaged: true,
|
|
76
|
+
lastPosition: move.position
|
|
77
|
+
};
|
|
78
|
+
const { detent } = state;
|
|
79
|
+
if (!detent) return land(mapping, moved, to);
|
|
80
|
+
const passed = passDetent(mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent, [state.rawProportion, to], pace);
|
|
81
|
+
const held = {
|
|
82
|
+
...moved,
|
|
83
|
+
detent: {
|
|
84
|
+
...detent,
|
|
85
|
+
slack: passed.slack
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
if (passed.proportion === void 0) return {
|
|
89
|
+
state: {
|
|
90
|
+
...held,
|
|
91
|
+
rawProportion: detent.proportion
|
|
92
|
+
},
|
|
93
|
+
value: detent.value
|
|
94
|
+
};
|
|
95
|
+
return land(mapping, held, passed.proportion);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//#endregion
|
|
99
|
+
export { dragThresholdPx, startDrag, stepDrag };
|
|
100
|
+
//# sourceMappingURL=drag-step.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drag-step.js","names":[],"sources":["../../../src/lib/drag-step.ts"],"sourcesContent":["import type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { clampProportion, wrapProportion } from '#lib/math.ts';\n\ninterface DetentHold {\n\tproportion: number;\n\tslack: number | undefined;\n\tvalue: number;\n\tzone: number;\n}\n\nexport interface DragState {\n\tdetent?: DetentHold;\n\tisEngaged: boolean;\n\tlastPosition: number;\n\trawProportion: number;\n\tstartPosition: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\nexport interface DragMove {\n\tisFine: boolean;\n\tisOutside: boolean;\n\tposition: number;\n}\n\ninterface DragStart {\n\tdetent?: { value: number; zone: number };\n\tfrom: number;\n\tposition: number;\n\tproportion: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\ninterface DragStep {\n\tstate: DragState;\n\tvalue: number | undefined;\n}\n\nconst fineFactor = 0.1;\nconst mouseDragThresholdPx = 3;\nconst touchDragThresholdPx = 7;\n\nexport function dragThresholdPx(pointerType: string): number {\n\treturn pointerType === 'touch' ? touchDragThresholdPx : mouseDragThresholdPx;\n}\n\nfunction isAcross(from: number, to: number, proportion: number): boolean {\n\treturn from !== proportion && (from - proportion) * (to - proportion) <= 0;\n}\n\nfunction passDetent(\n\thold: DetentHold,\n\t[from, to]: [number, number],\n\tpace: number,\n): { proportion: number | undefined; slack: number | undefined } {\n\tif (hold.slack === undefined && !isAcross(from, to, hold.proportion)) {\n\t\treturn { proportion: to, slack: undefined };\n\t}\n\n\tconst slack = (hold.slack ?? 0) + (to - hold.proportion) / pace;\n\n\tif (Math.abs(slack) <= hold.zone) return { proportion: undefined, slack };\n\n\treturn {\n\t\tproportion: hold.proportion + (slack - Math.sign(slack) * hold.zone) * pace,\n\t\tslack: undefined,\n\t};\n}\n\nfunction nearestTurn(hold: DetentHold, from: number): DetentHold {\n\treturn { ...hold, proportion: hold.proportion + Math.round(from - hold.proportion) };\n}\n\nfunction land(mapping: ValueMapping, state: DragState, proportion: number): DragStep {\n\tconst rawProportion = mapping.isWrapping\n\t\t? wrapProportion(proportion)\n\t\t: clampProportion(proportion);\n\n\treturn {\n\t\tstate: { ...state, rawProportion },\n\t\tvalue: mapping.snap(mapping.valueAt(rawProportion)),\n\t};\n}\n\nexport function startDrag(mapping: ValueMapping, start: DragStart): DragState {\n\tconst { detent: wanted, from, position, proportion, ...fixed } = start;\n\tconst state = {\n\t\t...fixed,\n\t\tisEngaged: false,\n\t\tlastPosition: position,\n\t\trawProportion: proportion,\n\t\tstartPosition: position,\n\t};\n\tif (!wanted) return state;\n\n\tconst value = mapping.snap(wanted.value);\n\tconst detent = {\n\t\tproportion: mapping.proportionOf(value),\n\t\tslack: value === from ? 0 : undefined,\n\t\tvalue,\n\t\tzone: wanted.zone,\n\t};\n\n\treturn { ...state, detent };\n}\n\nexport function stepDrag(mapping: ValueMapping, state: DragState, move: DragMove): DragStep {\n\tconst isEngaged =\n\t\tstate.isEngaged ||\n\t\tmove.isOutside ||\n\t\tMath.abs(move.position - state.startPosition) >= state.thresholdPx;\n\n\tif (!isEngaged) return { state, value: undefined };\n\n\tconst pace = move.isFine ? fineFactor : 1;\n\tconst to = state.rawProportion + ((move.position - state.lastPosition) / state.travelPx) * pace;\n\tconst moved = { ...state, isEngaged: true, lastPosition: move.position };\n\tconst { detent } = state;\n\tif (!detent) return land(mapping, moved, to);\n\n\tconst near = mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent;\n\tconst passed = passDetent(near, [state.rawProportion, to], pace);\n\tconst held = { ...moved, detent: { ...detent, slack: passed.slack } };\n\tif (passed.proportion === undefined) {\n\t\treturn { state: { ...held, rawProportion: detent.proportion }, value: detent.value };\n\t}\n\n\treturn land(mapping, held, passed.proportion);\n}\n"],"mappings":";;;AAyCA,MAAM,aAAa;AACnB,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAE7B,SAAgB,gBAAgB,aAA6B;CAC5D,OAAO,gBAAgB,UAAU,uBAAuB;AACzD;AAEA,SAAS,SAAS,MAAc,IAAY,YAA6B;CACxE,OAAO,SAAS,eAAe,OAAO,eAAe,KAAK,eAAe;AAC1E;AAEA,SAAS,WACR,MACA,CAAC,MAAM,KACP,MACgE;CAChE,IAAI,KAAK,UAAU,UAAa,CAAC,SAAS,MAAM,IAAI,KAAK,UAAU,GAClE,OAAO;EAAE,YAAY;EAAI,OAAO;CAAU;CAG3C,MAAM,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK,cAAc;CAE3D,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO;EAAE,YAAY;EAAW;CAAM;CAExE,OAAO;EACN,YAAY,KAAK,cAAc,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,QAAQ;EACvE,OAAO;CACR;AACD;AAEA,SAAS,YAAY,MAAkB,MAA0B;CAChE,OAAO;EAAE,GAAG;EAAM,YAAY,KAAK,aAAa,KAAK,MAAM,OAAO,KAAK,UAAU;CAAE;AACpF;AAEA,SAAS,KAAK,SAAuB,OAAkB,YAA8B;CACpF,MAAM,gBAAgB,QAAQ,aAC3B,eAAe,UAAU,IACzB,gBAAgB,UAAU;CAE7B,OAAO;EACN,OAAO;GAAE,GAAG;GAAO;EAAc;EACjC,OAAO,QAAQ,KAAK,QAAQ,QAAQ,aAAa,CAAC;CACnD;AACD;AAEA,SAAgB,UAAU,SAAuB,OAA6B;CAC7E,MAAM,EAAE,QAAQ,QAAQ,MAAM,UAAU,YAAY,GAAG,UAAU;CACjE,MAAM,QAAQ;EACb,GAAG;EACH,WAAW;EACX,cAAc;EACd,eAAe;EACf,eAAe;CAChB;CACA,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK;CACvC,MAAM,SAAS;EACd,YAAY,QAAQ,aAAa,KAAK;EACtC,OAAO,UAAU,OAAO,IAAI;EAC5B;EACA,MAAM,OAAO;CACd;CAEA,OAAO;EAAE,GAAG;EAAO;CAAO;AAC3B;AAEA,SAAgB,SAAS,SAAuB,OAAkB,MAA0B;CAM3F,IAAI,EAJH,MAAM,aACN,KAAK,aACL,KAAK,IAAI,KAAK,WAAW,MAAM,aAAa,KAAK,MAAM,cAExC,OAAO;EAAE;EAAO,OAAO;CAAU;CAEjD,MAAM,OAAO,KAAK,SAAS,aAAa;CACxC,MAAM,KAAK,MAAM,iBAAkB,KAAK,WAAW,MAAM,gBAAgB,MAAM,WAAY;CAC3F,MAAM,QAAQ;EAAE,GAAG;EAAO,WAAW;EAAM,cAAc,KAAK;CAAS;CACvE,MAAM,EAAE,WAAW;CACnB,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,OAAO,EAAE;CAG3C,MAAM,SAAS,WADF,QAAQ,aAAa,YAAY,QAAQ,MAAM,aAAa,IAAI,QAC7C,CAAC,MAAM,eAAe,EAAE,GAAG,IAAI;CAC/D,MAAM,OAAO;EAAE,GAAG;EAAO,QAAQ;GAAE,GAAG;GAAQ,OAAO,OAAO;EAAM;CAAE;CACpE,IAAI,OAAO,eAAe,QACzB,OAAO;EAAE,OAAO;GAAE,GAAG;GAAM,eAAe,OAAO;EAAW;EAAG,OAAO,OAAO;CAAM;CAGpF,OAAO,KAAK,SAAS,MAAM,OAAO,UAAU;AAC7C"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/lib/envelope-curve.ts
|
|
2
|
+
const flatCurve = .001;
|
|
3
|
+
function envelopeCurve(position, curve) {
|
|
4
|
+
if (Math.abs(curve) < flatCurve) return position;
|
|
5
|
+
return Math.expm1(curve * position) / Math.expm1(curve);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
//#endregion
|
|
9
|
+
export { envelopeCurve };
|
|
10
|
+
//# sourceMappingURL=envelope-curve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope-curve.js","names":[],"sources":["../../../src/lib/envelope-curve.ts"],"sourcesContent":["const flatCurve = 0.001;\n\nexport function envelopeCurve(position: number, curve: number): number {\n\tif (Math.abs(curve) < flatCurve) return position;\n\n\treturn Math.expm1(curve * position) / Math.expm1(curve);\n}\n"],"mappings":";AAAA,MAAM,YAAY;AAElB,SAAgB,cAAc,UAAkB,OAAuB;CACtE,IAAI,KAAK,IAAI,KAAK,IAAI,WAAW,OAAO;CAExC,OAAO,KAAK,MAAM,QAAQ,QAAQ,IAAI,KAAK,MAAM,KAAK;AACvD"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { startDrag, stepDrag } from "./drag-step.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/field.ts
|
|
4
|
+
function startAxis(start, position) {
|
|
5
|
+
if (!start) return void 0;
|
|
6
|
+
const { mapping, ...rest } = start;
|
|
7
|
+
return startDrag(mapping, {
|
|
8
|
+
...rest,
|
|
9
|
+
position,
|
|
10
|
+
thresholdPx: 0
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function startFieldDrag(start) {
|
|
14
|
+
const { position, thresholdPx } = start;
|
|
15
|
+
return {
|
|
16
|
+
isEngaged: false,
|
|
17
|
+
lock: void 0,
|
|
18
|
+
startPosition: position,
|
|
19
|
+
thresholdPx,
|
|
20
|
+
x: startAxis(start.x, position.x),
|
|
21
|
+
y: startAxis(start.y, position.y)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function dominantAxis(state, move) {
|
|
25
|
+
if (!state.x) return "y";
|
|
26
|
+
if (!state.y) return "x";
|
|
27
|
+
const { startPosition } = state;
|
|
28
|
+
return Math.abs(move.x - startPosition.x) >= Math.abs(move.y - startPosition.y) ? "x" : "y";
|
|
29
|
+
}
|
|
30
|
+
function stepAxis(mapping, drag, move) {
|
|
31
|
+
if (!mapping || !drag) return {
|
|
32
|
+
state: drag,
|
|
33
|
+
value: void 0
|
|
34
|
+
};
|
|
35
|
+
if (move.isHeld) return {
|
|
36
|
+
state: {
|
|
37
|
+
...drag,
|
|
38
|
+
lastPosition: move.position
|
|
39
|
+
},
|
|
40
|
+
value: void 0
|
|
41
|
+
};
|
|
42
|
+
return stepDrag(mapping, drag, {
|
|
43
|
+
isFine: move.isFine,
|
|
44
|
+
isOutside: false,
|
|
45
|
+
position: move.position
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function stepFieldDrag(mappings, state, move) {
|
|
49
|
+
const { startPosition } = state;
|
|
50
|
+
if (!(state.isEngaged || Math.hypot(move.x - startPosition.x, move.y - startPosition.y) >= state.thresholdPx)) return {
|
|
51
|
+
state,
|
|
52
|
+
x: void 0,
|
|
53
|
+
y: void 0
|
|
54
|
+
};
|
|
55
|
+
const lock = move.isLocked ? state.lock ?? dominantAxis(state, move) : void 0;
|
|
56
|
+
const { isFine } = move;
|
|
57
|
+
const x = stepAxis(mappings.x, state.x, {
|
|
58
|
+
isFine,
|
|
59
|
+
isHeld: lock === "y",
|
|
60
|
+
position: move.x
|
|
61
|
+
});
|
|
62
|
+
const y = stepAxis(mappings.y, state.y, {
|
|
63
|
+
isFine,
|
|
64
|
+
isHeld: lock === "x",
|
|
65
|
+
position: move.y
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
state: {
|
|
69
|
+
...state,
|
|
70
|
+
isEngaged: true,
|
|
71
|
+
lock,
|
|
72
|
+
x: x.state,
|
|
73
|
+
y: y.state
|
|
74
|
+
},
|
|
75
|
+
x: x.value,
|
|
76
|
+
y: y.value
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function pointerPosition(event) {
|
|
80
|
+
return {
|
|
81
|
+
x: event.clientX,
|
|
82
|
+
y: -event.clientY
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function pointerMove(event) {
|
|
86
|
+
return {
|
|
87
|
+
...pointerPosition(event),
|
|
88
|
+
isFine: event.shiftKey,
|
|
89
|
+
isLocked: event.altKey
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
export { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag };
|
|
95
|
+
//# sourceMappingURL=field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field.js","names":[],"sources":["../../../src/lib/field.ts"],"sourcesContent":["import type { DragState } from '#lib/drag-step.ts';\nimport type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { startDrag, stepDrag } from '#lib/drag-step.ts';\n\nexport type FieldAxis = 'x' | 'y';\n\nexport interface FieldPoint {\n\tx: number;\n\ty: number;\n}\n\ninterface FieldAxisStart {\n\tfrom: number;\n\tmapping: ValueMapping;\n\tproportion: number;\n\ttravelPx: number;\n}\n\ninterface FieldStart {\n\tposition: FieldPoint;\n\tthresholdPx: number;\n\tx?: FieldAxisStart | undefined;\n\ty?: FieldAxisStart | undefined;\n}\n\nexport interface FieldDragState {\n\tisEngaged: boolean;\n\tlock: FieldAxis | undefined;\n\tstartPosition: FieldPoint;\n\tthresholdPx: number;\n\tx: DragState | undefined;\n\ty: DragState | undefined;\n}\n\nexport interface FieldMove extends FieldPoint {\n\tisFine: boolean;\n\tisLocked: boolean;\n}\n\nexport interface FieldMappings {\n\tx?: undefined | ValueMapping;\n\ty?: undefined | ValueMapping;\n}\n\ninterface FieldStep {\n\tstate: FieldDragState;\n\tx: number | undefined;\n\ty: number | undefined;\n}\n\ninterface AxisMove {\n\tisFine: boolean;\n\tisHeld: boolean;\n\tposition: number;\n}\n\nfunction startAxis(start: FieldAxisStart | undefined, position: number): DragState | undefined {\n\tif (!start) return undefined;\n\n\tconst { mapping, ...rest } = start;\n\n\treturn startDrag(mapping, { ...rest, position, thresholdPx: 0 });\n}\n\nexport function startFieldDrag(start: FieldStart): FieldDragState {\n\tconst { position, thresholdPx } = start;\n\n\treturn {\n\t\tisEngaged: false,\n\t\tlock: undefined,\n\t\tstartPosition: position,\n\t\tthresholdPx,\n\t\tx: startAxis(start.x, position.x),\n\t\ty: startAxis(start.y, position.y),\n\t};\n}\n\nfunction dominantAxis(state: FieldDragState, move: FieldPoint): FieldAxis {\n\tif (!state.x) return 'y';\n\tif (!state.y) return 'x';\n\n\tconst { startPosition } = state;\n\n\treturn Math.abs(move.x - startPosition.x) >= Math.abs(move.y - startPosition.y) ? 'x' : 'y';\n}\n\nfunction stepAxis(\n\tmapping: undefined | ValueMapping,\n\tdrag: DragState | undefined,\n\tmove: AxisMove,\n): { state: DragState | undefined; value: number | undefined } {\n\tif (!mapping || !drag) return { state: drag, value: undefined };\n\tif (move.isHeld) return { state: { ...drag, lastPosition: move.position }, value: undefined };\n\n\treturn stepDrag(mapping, drag, {\n\t\tisFine: move.isFine,\n\t\tisOutside: false,\n\t\tposition: move.position,\n\t});\n}\n\nexport function stepFieldDrag(\n\tmappings: FieldMappings,\n\tstate: FieldDragState,\n\tmove: FieldMove,\n): FieldStep {\n\tconst { startPosition } = state;\n\tconst isEngaged =\n\t\tstate.isEngaged ||\n\t\tMath.hypot(move.x - startPosition.x, move.y - startPosition.y) >= state.thresholdPx;\n\n\tif (!isEngaged) return { state, x: undefined, y: undefined };\n\n\tconst lock = move.isLocked ? (state.lock ?? dominantAxis(state, move)) : undefined;\n\tconst { isFine } = move;\n\tconst x = stepAxis(mappings.x, state.x, { isFine, isHeld: lock === 'y', position: move.x });\n\tconst y = stepAxis(mappings.y, state.y, { isFine, isHeld: lock === 'x', position: move.y });\n\n\treturn {\n\t\tstate: { ...state, isEngaged: true, lock, x: x.state, y: y.state },\n\t\tx: x.value,\n\t\ty: y.value,\n\t};\n}\n\nexport function pointerPosition(event: PointerEvent): FieldPoint {\n\treturn { x: event.clientX, y: -event.clientY };\n}\n\nexport function pointerMove(event: PointerEvent): FieldMove {\n\treturn { ...pointerPosition(event), isFine: event.shiftKey, isLocked: event.altKey };\n}\n"],"mappings":";;;AAyDA,SAAS,UAAU,OAAmC,UAAyC;CAC9F,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,EAAE,SAAS,GAAG,SAAS;CAE7B,OAAO,UAAU,SAAS;EAAE,GAAG;EAAM;EAAU,aAAa;CAAE,CAAC;AAChE;AAEA,SAAgB,eAAe,OAAmC;CACjE,MAAM,EAAE,UAAU,gBAAgB;CAElC,OAAO;EACN,WAAW;EACX,MAAM;EACN,eAAe;EACf;EACA,GAAG,UAAU,MAAM,GAAG,SAAS,CAAC;EAChC,GAAG,UAAU,MAAM,GAAG,SAAS,CAAC;CACjC;AACD;AAEA,SAAS,aAAa,OAAuB,MAA6B;CACzE,IAAI,CAAC,MAAM,GAAG,OAAO;CACrB,IAAI,CAAC,MAAM,GAAG,OAAO;CAErB,MAAM,EAAE,kBAAkB;CAE1B,OAAO,KAAK,IAAI,KAAK,IAAI,cAAc,CAAC,KAAK,KAAK,IAAI,KAAK,IAAI,cAAc,CAAC,IAAI,MAAM;AACzF;AAEA,SAAS,SACR,SACA,MACA,MAC8D;CAC9D,IAAI,CAAC,WAAW,CAAC,MAAM,OAAO;EAAE,OAAO;EAAM,OAAO;CAAU;CAC9D,IAAI,KAAK,QAAQ,OAAO;EAAE,OAAO;GAAE,GAAG;GAAM,cAAc,KAAK;EAAS;EAAG,OAAO;CAAU;CAE5F,OAAO,SAAS,SAAS,MAAM;EAC9B,QAAQ,KAAK;EACb,WAAW;EACX,UAAU,KAAK;CAChB,CAAC;AACF;AAEA,SAAgB,cACf,UACA,OACA,MACY;CACZ,MAAM,EAAE,kBAAkB;CAK1B,IAAI,EAHH,MAAM,aACN,KAAK,MAAM,KAAK,IAAI,cAAc,GAAG,KAAK,IAAI,cAAc,CAAC,KAAK,MAAM,cAEzD,OAAO;EAAE;EAAO,GAAG;EAAW,GAAG;CAAU;CAE3D,MAAM,OAAO,KAAK,WAAY,MAAM,QAAQ,aAAa,OAAO,IAAI,IAAK;CACzE,MAAM,EAAE,WAAW;CACnB,MAAM,IAAI,SAAS,SAAS,GAAG,MAAM,GAAG;EAAE;EAAQ,QAAQ,SAAS;EAAK,UAAU,KAAK;CAAE,CAAC;CAC1F,MAAM,IAAI,SAAS,SAAS,GAAG,MAAM,GAAG;EAAE;EAAQ,QAAQ,SAAS;EAAK,UAAU,KAAK;CAAE,CAAC;CAE1F,OAAO;EACN,OAAO;GAAE,GAAG;GAAO,WAAW;GAAM;GAAM,GAAG,EAAE;GAAO,GAAG,EAAE;EAAM;EACjE,GAAG,EAAE;EACL,GAAG,EAAE;CACN;AACD;AAEA,SAAgB,gBAAgB,OAAiC;CAChE,OAAO;EAAE,GAAG,MAAM;EAAS,GAAG,CAAC,MAAM;CAAQ;AAC9C;AAEA,SAAgB,YAAY,OAAgC;CAC3D,OAAO;EAAE,GAAG,gBAAgB,KAAK;EAAG,QAAQ,MAAM;EAAU,UAAU,MAAM;CAAO;AACpF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/lib/focus-by-pointer.ts
|
|
2
|
+
const modifierKeys = /* @__PURE__ */ new Set([
|
|
3
|
+
"Alt",
|
|
4
|
+
"Control",
|
|
5
|
+
"Meta",
|
|
6
|
+
"Shift"
|
|
7
|
+
]);
|
|
8
|
+
function focusByPointer(target) {
|
|
9
|
+
const isMarked = "sonicPointerFocus" in target.dataset;
|
|
10
|
+
target.dataset.sonicPointerFocus = "";
|
|
11
|
+
target.focus();
|
|
12
|
+
if (isMarked) return;
|
|
13
|
+
const marking = new AbortController();
|
|
14
|
+
const clear = () => {
|
|
15
|
+
delete target.dataset.sonicPointerFocus;
|
|
16
|
+
marking.abort();
|
|
17
|
+
};
|
|
18
|
+
target.addEventListener("blur", () => {
|
|
19
|
+
if (target.ownerDocument.activeElement !== target) clear();
|
|
20
|
+
}, { signal: marking.signal });
|
|
21
|
+
target.addEventListener("keydown", (event) => {
|
|
22
|
+
if (event.metaKey || event.ctrlKey || event.altKey || modifierKeys.has(event.key)) return;
|
|
23
|
+
clear();
|
|
24
|
+
}, { signal: marking.signal });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { focusByPointer };
|
|
29
|
+
//# sourceMappingURL=focus-by-pointer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-by-pointer.js","names":[],"sources":["../../../src/lib/focus-by-pointer.ts"],"sourcesContent":["const modifierKeys = new Set(['Alt', 'Control', 'Meta', 'Shift']);\n\n// A script focus during a mouse press still matches `:focus-visible`\nexport function focusByPointer(target: HTMLElement): void {\n\tconst isMarked = 'sonicPointerFocus' in target.dataset;\n\n\ttarget.dataset.sonicPointerFocus = '';\n\ttarget.focus();\n\tif (isMarked) return;\n\n\tconst marking = new AbortController();\n\tconst clear = (): void => {\n\t\tdelete target.dataset.sonicPointerFocus;\n\t\tmarking.abort();\n\t};\n\n\ttarget.addEventListener(\n\t\t'blur',\n\t\t() => {\n\t\t\t// A window or tab switch blurs the target and leaves it the active element\n\t\t\tif (target.ownerDocument.activeElement !== target) clear();\n\t\t},\n\t\t{ signal: marking.signal },\n\t);\n\ttarget.addEventListener(\n\t\t'keydown',\n\t\t(event) => {\n\t\t\tif (event.metaKey || event.ctrlKey || event.altKey || modifierKeys.has(event.key)) return;\n\n\t\t\tclear();\n\t\t},\n\t\t{ signal: marking.signal },\n\t);\n}\n"],"mappings":";AAAA,MAAM,+BAAe,IAAI,IAAI;CAAC;CAAO;CAAW;CAAQ;AAAO,CAAC;AAGhE,SAAgB,eAAe,QAA2B;CACzD,MAAM,WAAW,uBAAuB,OAAO;CAE/C,OAAO,QAAQ,oBAAoB;CACnC,OAAO,MAAM;CACb,IAAI,UAAU;CAEd,MAAM,UAAU,IAAI,gBAAgB;CACpC,MAAM,cAAoB;EACzB,OAAO,OAAO,QAAQ;EACtB,QAAQ,MAAM;CACf;CAEA,OAAO,iBACN,cACM;EAEL,IAAI,OAAO,cAAc,kBAAkB,QAAQ,MAAM;CAC1D,GACA,EAAE,QAAQ,QAAQ,OAAO,CAC1B;CACA,OAAO,iBACN,YACC,UAAU;EACV,IAAI,MAAM,WAAW,MAAM,WAAW,MAAM,UAAU,aAAa,IAAI,MAAM,GAAG,GAAG;EAEnF,MAAM;CACP,GACA,EAAE,QAAQ,QAAQ,OAAO,CAC1B;AACD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { trimFloat } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/frame-timeline.ts
|
|
4
|
+
const placeholderPeriodMs = 2500;
|
|
5
|
+
const lookAheadSeconds = 30;
|
|
6
|
+
function windowStart(input, seconds, windowSeconds) {
|
|
7
|
+
if (!input.isPaged) return seconds - windowSeconds / 2;
|
|
8
|
+
if (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;
|
|
9
|
+
return Math.floor(trimFloat(seconds / windowSeconds)) * windowSeconds;
|
|
10
|
+
}
|
|
11
|
+
function overlap([from, to], [low, high]) {
|
|
12
|
+
const start = Math.max(from, low);
|
|
13
|
+
const end = Math.min(to, high);
|
|
14
|
+
return end > start ? [start, end] : void 0;
|
|
15
|
+
}
|
|
16
|
+
function pendingIn({ pending, range }, window) {
|
|
17
|
+
return pending.flatMap((region) => {
|
|
18
|
+
const inRange = overlap(region, range);
|
|
19
|
+
const shown = inRange && overlap(inRange, window);
|
|
20
|
+
return shown ? [shown] : [];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function placeholderPhase({ frameMs, isPaged }, pending) {
|
|
24
|
+
return !isPaged && pending.length > 0 ? frameMs / placeholderPeriodMs : void 0;
|
|
25
|
+
}
|
|
26
|
+
function frameTimeline(input) {
|
|
27
|
+
const { clockSeconds, held, range, size } = input;
|
|
28
|
+
const seconds = held?.seconds ?? clockSeconds;
|
|
29
|
+
const pixelsPerSecond = input.zoom * size.dpr;
|
|
30
|
+
const windowSeconds = size.width / pixelsPerSecond;
|
|
31
|
+
const startSeconds = windowStart(input, seconds, windowSeconds);
|
|
32
|
+
const pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);
|
|
33
|
+
const placeholder = placeholderPhase(input, pending);
|
|
34
|
+
const phase = placeholder ?? 0;
|
|
35
|
+
const wanted = overlap([startSeconds - windowSeconds, startSeconds + windowSeconds + Math.max(windowSeconds, lookAheadSeconds)], range);
|
|
36
|
+
const at = (time) => (time - startSeconds) / windowSeconds;
|
|
37
|
+
return {
|
|
38
|
+
...held ? { ghostAt: at(clockSeconds) } : {},
|
|
39
|
+
isMoving: held !== void 0 || placeholder !== void 0 || input.isPlaying,
|
|
40
|
+
paintKey: [
|
|
41
|
+
startSeconds,
|
|
42
|
+
windowSeconds,
|
|
43
|
+
...range,
|
|
44
|
+
phase,
|
|
45
|
+
pending.join(",")
|
|
46
|
+
].join(":"),
|
|
47
|
+
playheadAt: at(seconds),
|
|
48
|
+
seconds,
|
|
49
|
+
view: {
|
|
50
|
+
dpr: size.dpr,
|
|
51
|
+
height: size.height,
|
|
52
|
+
pending,
|
|
53
|
+
phase,
|
|
54
|
+
pixelsPerSecond,
|
|
55
|
+
startSeconds,
|
|
56
|
+
width: size.width
|
|
57
|
+
},
|
|
58
|
+
...wanted ? { wanted } : {}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { frameTimeline };
|
|
64
|
+
//# sourceMappingURL=frame-timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-timeline.js","names":[],"sources":["../../../src/lib/frame-timeline.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { trimFloat } from '#lib/math.ts';\n\nexport interface View {\n\tdpr: number;\n\theight: number;\n\tpending: Array<[number, number]>;\n\tphase: number;\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\nexport interface Drawn {\n\tseconds: number;\n\tstartSeconds: number;\n}\n\ninterface TimelineInput {\n\tclockSeconds: number;\n\tframeMs: number;\n\theld?: { grabbed: Drawn; seconds: number };\n\tisPaged: boolean;\n\tisPlaying: boolean;\n\tpending: ReadonlyArray<[number, number]>;\n\trange: [number, number];\n\tsize: SurfaceSize;\n\tzoom: number;\n}\n\ninterface Timeline {\n\tghostAt?: number;\n\tisMoving: boolean;\n\tpaintKey: string;\n\tplayheadAt: number;\n\tseconds: number;\n\tview: View;\n\twanted?: [number, number];\n}\n\nconst placeholderPeriodMs = 2500;\n\nconst lookAheadSeconds = 30;\n\nfunction windowStart(input: TimelineInput, seconds: number, windowSeconds: number): number {\n\tif (!input.isPaged) return seconds - windowSeconds / 2;\n\tif (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;\n\n\t// 0.3 / 0.1 is 2.9999999999999996, a page short\n\treturn Math.floor(trimFloat(seconds / windowSeconds)) * windowSeconds;\n}\n\nfunction overlap(\n\t[from, to]: readonly [number, number],\n\t[low, high]: readonly [number, number],\n): [number, number] | undefined {\n\tconst start = Math.max(from, low);\n\tconst end = Math.min(to, high);\n\n\treturn end > start ? [start, end] : undefined;\n}\n\nfunction pendingIn(\n\t{ pending, range }: TimelineInput,\n\twindow: [number, number],\n): Array<[number, number]> {\n\treturn pending.flatMap((region) => {\n\t\tconst inRange = overlap(region, range);\n\t\tconst shown = inRange && overlap(inRange, window);\n\n\t\treturn shown ? [shown] : [];\n\t});\n}\n\nfunction placeholderPhase(\n\t{ frameMs, isPaged }: TimelineInput,\n\tpending: ReadonlyArray<unknown>,\n): number | undefined {\n\treturn !isPaged && pending.length > 0 ? frameMs / placeholderPeriodMs : undefined;\n}\n\nexport function frameTimeline(input: TimelineInput): Timeline {\n\tconst { clockSeconds, held, range, size } = input;\n\tconst seconds = held?.seconds ?? clockSeconds;\n\tconst pixelsPerSecond = input.zoom * size.dpr;\n\tconst windowSeconds = size.width / pixelsPerSecond;\n\tconst startSeconds = windowStart(input, seconds, windowSeconds);\n\tconst pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);\n\tconst placeholder = placeholderPhase(input, pending);\n\tconst phase = placeholder ?? 0;\n\tconst wanted = overlap(\n\t\t[\n\t\t\tstartSeconds - windowSeconds,\n\t\t\tstartSeconds + windowSeconds + Math.max(windowSeconds, lookAheadSeconds),\n\t\t],\n\t\trange,\n\t);\n\tconst at = (time: number): number => (time - startSeconds) / windowSeconds;\n\n\treturn {\n\t\t...(held ? { ghostAt: at(clockSeconds) } : {}),\n\t\tisMoving: held !== undefined || placeholder !== undefined || input.isPlaying,\n\t\tpaintKey: [startSeconds, windowSeconds, ...range, phase, pending.join(',')].join(':'),\n\t\tplayheadAt: at(seconds),\n\t\tseconds,\n\t\tview: {\n\t\t\tdpr: size.dpr,\n\t\t\theight: size.height,\n\t\t\tpending,\n\t\t\tphase,\n\t\t\tpixelsPerSecond,\n\t\t\tstartSeconds,\n\t\t\twidth: size.width,\n\t\t},\n\t\t...(wanted ? { wanted } : {}),\n\t};\n}\n"],"mappings":";;;AAyCA,MAAM,sBAAsB;AAE5B,MAAM,mBAAmB;AAEzB,SAAS,YAAY,OAAsB,SAAiB,eAA+B;CAC1F,IAAI,CAAC,MAAM,SAAS,OAAO,UAAU,gBAAgB;CACrD,IAAI,MAAM,MAAM,OAAO,MAAM,KAAK,QAAQ,eAAe,UAAU,MAAM,KAAK,QAAQ;CAGtF,OAAO,KAAK,MAAM,UAAU,UAAU,aAAa,CAAC,IAAI;AACzD;AAEA,SAAS,QACR,CAAC,MAAM,KACP,CAAC,KAAK,OACyB;CAC/B,MAAM,QAAQ,KAAK,IAAI,MAAM,GAAG;CAChC,MAAM,MAAM,KAAK,IAAI,IAAI,IAAI;CAE7B,OAAO,MAAM,QAAQ,CAAC,OAAO,GAAG,IAAI;AACrC;AAEA,SAAS,UACR,EAAE,SAAS,SACX,QAC0B;CAC1B,OAAO,QAAQ,SAAS,WAAW;EAClC,MAAM,UAAU,QAAQ,QAAQ,KAAK;EACrC,MAAM,QAAQ,WAAW,QAAQ,SAAS,MAAM;EAEhD,OAAO,QAAQ,CAAC,KAAK,IAAI,CAAC;CAC3B,CAAC;AACF;AAEA,SAAS,iBACR,EAAE,SAAS,WACX,SACqB;CACrB,OAAO,CAAC,WAAW,QAAQ,SAAS,IAAI,UAAU,sBAAsB;AACzE;AAEA,SAAgB,cAAc,OAAgC;CAC7D,MAAM,EAAE,cAAc,MAAM,OAAO,SAAS;CAC5C,MAAM,UAAU,MAAM,WAAW;CACjC,MAAM,kBAAkB,MAAM,OAAO,KAAK;CAC1C,MAAM,gBAAgB,KAAK,QAAQ;CACnC,MAAM,eAAe,YAAY,OAAO,SAAS,aAAa;CAC9D,MAAM,UAAU,UAAU,OAAO,CAAC,cAAc,eAAe,aAAa,CAAC;CAC7E,MAAM,cAAc,iBAAiB,OAAO,OAAO;CACnD,MAAM,QAAQ,eAAe;CAC7B,MAAM,SAAS,QACd,CACC,eAAe,eACf,eAAe,gBAAgB,KAAK,IAAI,eAAe,gBAAgB,CACxE,GACA,KACD;CACA,MAAM,MAAM,UAA0B,OAAO,gBAAgB;CAE7D,OAAO;EACN,GAAI,OAAO,EAAE,SAAS,GAAG,YAAY,EAAE,IAAI,CAAC;EAC5C,UAAU,SAAS,UAAa,gBAAgB,UAAa,MAAM;EACnE,UAAU;GAAC;GAAc;GAAe,GAAG;GAAO;GAAO,QAAQ,KAAK,GAAG;EAAC,CAAC,CAAC,KAAK,GAAG;EACpF,YAAY,GAAG,OAAO;EACtB;EACA,MAAM;GACL,KAAK,KAAK;GACV,QAAQ,KAAK;GACb;GACA;GACA;GACA;GACA,OAAO,KAAK;EACb;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC5B;AACD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/lib/hold.ts
|
|
2
|
+
function bindHold(target, options, signal) {
|
|
3
|
+
let holder;
|
|
4
|
+
const hold = {
|
|
5
|
+
hold(by) {
|
|
6
|
+
if (holder !== void 0 || !options.canHold()) return false;
|
|
7
|
+
holder = by;
|
|
8
|
+
options.onHold();
|
|
9
|
+
return true;
|
|
10
|
+
},
|
|
11
|
+
release() {
|
|
12
|
+
if (holder === void 0) return;
|
|
13
|
+
holder = void 0;
|
|
14
|
+
options.onRelease();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const releasePointer = (event) => {
|
|
18
|
+
if (event.pointerId === holder) hold.release();
|
|
19
|
+
};
|
|
20
|
+
target.addEventListener("lostpointercapture", releasePointer, { signal });
|
|
21
|
+
target.addEventListener("pointercancel", releasePointer, { signal });
|
|
22
|
+
target.addEventListener("pointerup", releasePointer, { signal });
|
|
23
|
+
target.addEventListener("keyup", (event) => {
|
|
24
|
+
if (event.key === "Meta" && typeof holder === "string" || event.key === holder) hold.release();
|
|
25
|
+
}, { signal });
|
|
26
|
+
target.addEventListener(options.leave, (event) => {
|
|
27
|
+
const next = event.relatedTarget;
|
|
28
|
+
const isInside = next instanceof Node && target.contains(next);
|
|
29
|
+
if (typeof holder === "string" && !isInside) hold.release();
|
|
30
|
+
}, { signal });
|
|
31
|
+
signal.addEventListener("abort", () => {
|
|
32
|
+
hold.release();
|
|
33
|
+
}, { once: true });
|
|
34
|
+
return hold;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { bindHold };
|
|
39
|
+
//# sourceMappingURL=hold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hold.js","names":[],"sources":["../../../src/lib/hold.ts"],"sourcesContent":["export interface Hold {\n\thold(by: number | string): boolean;\n\trelease(): void;\n}\n\ninterface HoldOptions {\n\tcanHold: () => boolean;\n\tleave: 'blur' | 'focusout';\n\tonHold: () => void;\n\tonRelease: () => void;\n}\n\nexport function bindHold(target: HTMLElement, options: HoldOptions, signal: AbortSignal): Hold {\n\tlet holder: number | string | undefined;\n\tconst hold: Hold = {\n\t\thold(by) {\n\t\t\tif (holder !== undefined || !options.canHold()) return false;\n\n\t\t\tholder = by;\n\t\t\toptions.onHold();\n\n\t\t\treturn true;\n\t\t},\n\t\trelease() {\n\t\t\tif (holder === undefined) return;\n\n\t\t\tholder = undefined;\n\t\t\toptions.onRelease();\n\t\t},\n\t};\n\tconst releasePointer = (event: PointerEvent): void => {\n\t\tif (event.pointerId === holder) hold.release();\n\t};\n\n\ttarget.addEventListener('lostpointercapture', releasePointer, { signal });\n\ttarget.addEventListener('pointercancel', releasePointer, { signal });\n\ttarget.addEventListener('pointerup', releasePointer, { signal });\n\ttarget.addEventListener(\n\t\t'keyup',\n\t\t(event) => {\n\t\t\t// macOS sends no `keyup` for a key let go while Cmd is down\n\t\t\tconst isMetaLift = event.key === 'Meta' && typeof holder === 'string';\n\n\t\t\tif (isMetaLift || event.key === holder) hold.release();\n\t\t},\n\t\t{ signal },\n\t);\n\ttarget.addEventListener(\n\t\toptions.leave,\n\t\t(event) => {\n\t\t\tconst next = event.relatedTarget;\n\t\t\tconst isInside = next instanceof Node && target.contains(next);\n\n\t\t\tif (typeof holder === 'string' && !isInside) hold.release();\n\t\t},\n\t\t{ signal },\n\t);\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\thold.release();\n\t\t},\n\t\t{ once: true },\n\t);\n\n\treturn hold;\n}\n"],"mappings":";AAYA,SAAgB,SAAS,QAAqB,SAAsB,QAA2B;CAC9F,IAAI;CACJ,MAAM,OAAa;EAClB,KAAK,IAAI;GACR,IAAI,WAAW,UAAa,CAAC,QAAQ,QAAQ,GAAG,OAAO;GAEvD,SAAS;GACT,QAAQ,OAAO;GAEf,OAAO;EACR;EACA,UAAU;GACT,IAAI,WAAW,QAAW;GAE1B,SAAS;GACT,QAAQ,UAAU;EACnB;CACD;CACA,MAAM,kBAAkB,UAA8B;EACrD,IAAI,MAAM,cAAc,QAAQ,KAAK,QAAQ;CAC9C;CAEA,OAAO,iBAAiB,sBAAsB,gBAAgB,EAAE,OAAO,CAAC;CACxE,OAAO,iBAAiB,iBAAiB,gBAAgB,EAAE,OAAO,CAAC;CACnE,OAAO,iBAAiB,aAAa,gBAAgB,EAAE,OAAO,CAAC;CAC/D,OAAO,iBACN,UACC,UAAU;EAIV,IAFmB,MAAM,QAAQ,UAAU,OAAO,WAAW,YAE3C,MAAM,QAAQ,QAAQ,KAAK,QAAQ;CACtD,GACA,EAAE,OAAO,CACV;CACA,OAAO,iBACN,QAAQ,QACP,UAAU;EACV,MAAM,OAAO,MAAM;EACnB,MAAM,WAAW,gBAAgB,QAAQ,OAAO,SAAS,IAAI;EAE7D,IAAI,OAAO,WAAW,YAAY,CAAC,UAAU,KAAK,QAAQ;CAC3D,GACA,EAAE,OAAO,CACV;CACA,OAAO,iBACN,eACM;EACL,KAAK,QAAQ;CACd,GACA,EAAE,MAAM,KAAK,CACd;CAEA,OAAO;AACR"}
|