@xsynaptic/sonic-ui 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/default/dat.js +5 -4
- package/dist/default/define/button.js +1 -1
- package/dist/default/define.js +2 -2
- package/dist/default/elements/button.js +6 -5
- package/dist/default/elements/dial.js +5 -1
- package/dist/default/elements/envelope.js +46 -18
- package/dist/default/elements/meter.js +12 -8
- package/dist/default/elements/number.js +1 -1
- package/dist/default/elements/radio-group.js +46 -47
- package/dist/default/elements/readout-claim.js +0 -29
- package/dist/default/elements/readout.js +1 -1
- package/dist/default/elements/scale.js +43 -0
- package/dist/default/elements/slider.js +16 -3
- package/dist/default/elements/sonic-element.js +17 -55
- package/dist/default/elements/switch.js +33 -39
- package/dist/default/elements/value-element.js +142 -187
- package/dist/default/elements/wave-element.js +5 -2
- package/dist/default/elements/waveform.js +35 -12
- package/dist/default/elements/wavestrip.js +59 -18
- package/dist/default/elements/xy.js +34 -19
- package/dist/default/index.js +2 -2
- package/dist/default/lib/adsr-shape.js +5 -0
- package/dist/default/lib/ballistics.js +2 -2
- package/dist/default/lib/canvas-surface.js +13 -2
- package/dist/default/lib/distribute.js +1 -1
- package/dist/default/lib/drag-step.js +4 -4
- package/dist/default/lib/frame-timeline.js +2 -1
- package/dist/default/lib/marker-band.js +54 -10
- package/dist/default/lib/marker-rider.js +10 -4
- package/dist/default/lib/math.js +6 -1
- package/dist/default/lib/mirror-children.js +64 -0
- package/dist/default/lib/owned-control.js +10 -0
- package/dist/default/lib/pointer-drag.js +8 -5
- package/dist/default/lib/resample-peaks.js +11 -1
- package/dist/default/lib/time-regions.js +4 -1
- package/dist/default/lib/write-attribute.js +1 -0
- package/dist/dev/dat.d.ts +2 -2
- package/dist/dev/dat.js +5 -4
- package/dist/dev/dat.js.map +1 -1
- package/dist/dev/define/button.js +1 -1
- package/dist/dev/define.d.ts +4 -4
- package/dist/dev/define.js +2 -2
- package/dist/dev/elements/button.js +6 -5
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/dial.d.ts +1 -1
- package/dist/dev/elements/dial.js +5 -1
- package/dist/dev/elements/dial.js.map +1 -1
- package/dist/dev/elements/envelope.js +46 -18
- package/dist/dev/elements/envelope.js.map +1 -1
- package/dist/dev/elements/meter.d.ts +1 -1
- package/dist/dev/elements/meter.js +12 -8
- package/dist/dev/elements/meter.js.map +1 -1
- package/dist/dev/elements/number.js +1 -1
- package/dist/dev/elements/number.js.map +1 -1
- package/dist/dev/elements/radio-group.js +46 -47
- package/dist/dev/elements/radio-group.js.map +1 -1
- package/dist/dev/elements/readout-claim.js +0 -29
- package/dist/dev/elements/readout-claim.js.map +1 -1
- package/dist/dev/elements/readout.js +1 -1
- package/dist/dev/elements/readout.js.map +1 -1
- package/dist/dev/elements/scale.js +44 -0
- package/dist/dev/elements/scale.js.map +1 -0
- package/dist/dev/elements/slider.d.ts +4 -1
- package/dist/dev/elements/slider.js +16 -3
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/sonic-element.d.ts +2 -8
- package/dist/dev/elements/sonic-element.js +18 -55
- package/dist/dev/elements/sonic-element.js.map +1 -1
- package/dist/dev/elements/switch.js +33 -39
- package/dist/dev/elements/switch.js.map +1 -1
- package/dist/dev/elements/value-element.d.ts +6 -8
- package/dist/dev/elements/value-element.js +142 -187
- package/dist/dev/elements/value-element.js.map +1 -1
- package/dist/dev/elements/wave-element.d.ts +1 -0
- package/dist/dev/elements/wave-element.js +5 -2
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +1 -1
- package/dist/dev/elements/waveform.js +35 -12
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +4 -0
- package/dist/dev/elements/wavestrip.js +59 -18
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/elements/xy.d.ts +3 -3
- package/dist/dev/elements/xy.js +34 -19
- package/dist/dev/elements/xy.js.map +1 -1
- package/dist/dev/index.d.ts +2 -2
- package/dist/dev/index.js +2 -2
- package/dist/dev/lib/adsr-shape.js +5 -0
- package/dist/dev/lib/adsr-shape.js.map +1 -1
- package/dist/dev/lib/ballistics.js +2 -2
- package/dist/dev/lib/ballistics.js.map +1 -1
- package/dist/dev/lib/canvas-surface.d.ts +1 -0
- package/dist/dev/lib/canvas-surface.js +13 -2
- package/dist/dev/lib/canvas-surface.js.map +1 -1
- package/dist/dev/lib/distribute.js +1 -1
- package/dist/dev/lib/distribute.js.map +1 -1
- package/dist/dev/lib/drag-step.js +4 -4
- package/dist/dev/lib/drag-step.js.map +1 -1
- package/dist/dev/lib/frame-timeline.js +2 -1
- package/dist/dev/lib/frame-timeline.js.map +1 -1
- package/dist/dev/lib/marker-band.js +54 -10
- package/dist/dev/lib/marker-band.js.map +1 -1
- package/dist/dev/lib/marker-rider.js +10 -4
- package/dist/dev/lib/marker-rider.js.map +1 -1
- package/dist/dev/lib/math.js +6 -1
- package/dist/dev/lib/math.js.map +1 -1
- package/dist/dev/lib/mirror-children.js +80 -0
- package/dist/dev/lib/mirror-children.js.map +1 -0
- package/dist/dev/lib/owned-control.js +23 -0
- package/dist/dev/lib/owned-control.js.map +1 -0
- package/dist/dev/lib/pointer-drag.js +8 -5
- package/dist/dev/lib/pointer-drag.js.map +1 -1
- package/dist/dev/lib/resample-peaks.js +11 -1
- package/dist/dev/lib/resample-peaks.js.map +1 -1
- package/dist/dev/lib/time-regions.js +4 -1
- package/dist/dev/lib/time-regions.js.map +1 -1
- 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 +26 -0
- package/dist/skins/ivory.css +1 -0
- package/dist/skins/lime.css +2 -0
- package/dist/skins/slate.css +2 -0
- package/dist/styles/button.css +59 -10
- package/dist/styles/dial.css +24 -16
- package/dist/styles/envelope.css +58 -13
- package/dist/styles/led.css +20 -4
- package/dist/styles/material.css +143 -55
- package/dist/styles/meter.css +8 -4
- package/dist/styles/panel.css +7 -4
- package/dist/styles/segmented.css +27 -5
- package/dist/styles/slider.css +2 -1
- package/dist/styles/switch.css +65 -23
- package/dist/styles/waveform.css +28 -5
- package/dist/styles/wavestrip.css +23 -6
- package/dist/styles/xy.css +48 -27
- package/package.json +2 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ballistics.js","names":[],"sources":["../../../src/lib/ballistics.ts"],"sourcesContent":["interface Needle {\n\tcarryMs: number;\n\tposition: number;\n\tvelocity: number;\n}\n\ninterface BallisticsRate {\n\tfallDecibelsPerSecond: number;\n\
|
|
1
|
+
{"version":3,"file":"ballistics.js","names":[],"sources":["../../../src/lib/ballistics.ts"],"sourcesContent":["interface Needle {\n\tcarryMs: number;\n\tposition: number;\n\tvelocity: number;\n}\n\ninterface BallisticsRate {\n\tfallDecibelsPerSecond: number;\n\triseTimeConstantMs: number;\n}\n\ninterface PpmSpec {\n\tburstMs: number;\n\tfallDecibels: number;\n\tfallSeconds: number;\n}\n\n// IEC 60268-10: a tone burst as long as the integration time reads this far under a steady tone\nconst burstShortfallDecibels = 2;\nconst burstTimeConstants = -Math.log(1 - 10 ** (-burstShortfallDecibels / 20));\n\nfunction ppmRate({ burstMs, fallDecibels, fallSeconds }: PpmSpec): BallisticsRate {\n\treturn {\n\t\tfallDecibelsPerSecond: fallDecibels / fallSeconds,\n\t\triseTimeConstantMs: burstMs / burstTimeConstants,\n\t};\n}\n\n// The PPM figures are IEC 60268-10's Type I and Type II\nexport const ballisticsRates = {\n\tpeak: { fallDecibelsPerSecond: 20, riseTimeConstantMs: 0 },\n\t'ppm-1': ppmRate({ burstMs: 5, fallDecibels: 20, fallSeconds: 1.7 }),\n\t'ppm-2': ppmRate({ burstMs: 10, fallDecibels: 24, fallSeconds: 2.8 }),\n} as const satisfies Record<string, BallisticsRate>;\n\n// Tuned to a VU's step response: 99% in 300ms with 1 to 1.5% overshoot\nconst needleDamping = 0.8;\nconst needleRadiansPerSecond = 13.5;\n\nconst needleStepMs = 4;\nconst needleSettleMs = 1000;\n\n// A meter's return is specified linear in dB\nexport function fall(\n\tcurrent: number,\n\t{ fallDecibelsPerSecond, floor }: { fallDecibelsPerSecond: number; floor: number },\n\telapsedMs: number,\n): number {\n\treturn Math.max(floor, current - (fallDecibelsPerSecond * elapsedMs) / 1000);\n}\n\nexport function rise(\n\tcurrent: number,\n\t{ target, timeConstantMs }: { target: number; timeConstantMs: number },\n\telapsedMs: number,\n): number {\n\tif (timeConstantMs <= 0) return target;\n\n\treturn target + (current - target) * Math.exp(-elapsedMs / timeConstantMs);\n}\n\nexport function stepNeedle(needle: Needle, target: number, elapsedMs: number): Needle {\n\tconst dueMs = Math.min(needle.carryMs + elapsedMs, needleSettleMs);\n\tconst steps = Math.floor(dueMs / needleStepMs);\n\tconst seconds = needleStepMs / 1000;\n\tlet { position, velocity } = needle;\n\n\tfor (let step = 0; step < steps; step += 1) {\n\t\tconst pull = needleRadiansPerSecond ** 2 * (target - position);\n\t\tconst drag = 2 * needleDamping * needleRadiansPerSecond * velocity;\n\n\t\tvelocity += (pull - drag) * seconds;\n\t\tposition += velocity * seconds;\n\t}\n\n\treturn { carryMs: dueMs - steps * needleStepMs, position, velocity };\n}\n"],"mappings":";AAmBA,MAAM,qBAAqB,CAAC,KAAK,IAAI,IAAI,OAAO,KAA0B,GAAG;AAE7E,SAAS,QAAQ,EAAE,SAAS,cAAc,eAAwC;CACjF,OAAO;EACN,uBAAuB,eAAe;EACtC,oBAAoB,UAAU;CAC/B;AACD;AAGA,MAAa,kBAAkB;CAC9B,MAAM;EAAE,uBAAuB;EAAI,oBAAoB;CAAE;CACzD,SAAS,QAAQ;EAAE,SAAS;EAAG,cAAc;EAAI,aAAa;CAAI,CAAC;CACnE,SAAS,QAAQ;EAAE,SAAS;EAAI,cAAc;EAAI,aAAa;CAAI,CAAC;AACrE;AAGA,MAAM,gBAAgB;AACtB,MAAM,yBAAyB;AAE/B,MAAM,eAAe;AACrB,MAAM,iBAAiB;AAGvB,SAAgB,KACf,SACA,EAAE,uBAAuB,SACzB,WACS;CACT,OAAO,KAAK,IAAI,OAAO,UAAW,wBAAwB,YAAa,GAAI;AAC5E;AAEA,SAAgB,KACf,SACA,EAAE,QAAQ,kBACV,WACS;CACT,IAAI,kBAAkB,GAAG,OAAO;CAEhC,OAAO,UAAU,UAAU,UAAU,KAAK,IAAI,CAAC,YAAY,cAAc;AAC1E;AAEA,SAAgB,WAAW,QAAgB,QAAgB,WAA2B;CACrF,MAAM,QAAQ,KAAK,IAAI,OAAO,UAAU,WAAW,cAAc;CACjE,MAAM,QAAQ,KAAK,MAAM,QAAQ,YAAY;CAC7C,MAAM,UAAU,eAAe;CAC/B,IAAI,EAAE,UAAU,aAAa;CAE7B,KAAK,IAAI,OAAO,GAAG,OAAO,OAAO,QAAQ,GAAG;EAC3C,MAAM,OAAO,0BAA0B,KAAK,SAAS;EACrD,MAAM,OAAO,IAAI,gBAAgB,yBAAyB;EAE1D,aAAa,OAAO,QAAQ;EAC5B,YAAY,WAAW;CACxB;CAEA,OAAO;EAAE,SAAS,QAAQ,QAAQ;EAAc;EAAU;CAAS;AACpE"}
|
|
@@ -12,6 +12,7 @@ export interface SurfaceLook<Colour extends string, Length extends string = neve
|
|
|
12
12
|
export interface SurfaceFrame<Colour extends string, Length extends string = never> {
|
|
13
13
|
frameMs: number;
|
|
14
14
|
isDirty: boolean;
|
|
15
|
+
isRebuilt: boolean;
|
|
15
16
|
look: SurfaceLook<Colour, Length>;
|
|
16
17
|
size: SurfaceSize;
|
|
17
18
|
}
|
|
@@ -69,12 +69,22 @@ var CanvasSurface = class {
|
|
|
69
69
|
style.setProperty("transition-timing-function", "step-start");
|
|
70
70
|
style.setProperty("transition-behavior", "allow-discrete");
|
|
71
71
|
}
|
|
72
|
+
#current() {
|
|
73
|
+
if (this.#look && !this.#isRebuildWanted) return {
|
|
74
|
+
isRebuilt: false,
|
|
75
|
+
look: this.#look
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
isRebuilt: true,
|
|
79
|
+
look: this.#read()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
72
82
|
#draw(frameMs) {
|
|
73
83
|
this.#frame = void 0;
|
|
74
84
|
const size = this.#size;
|
|
75
85
|
if (size.width === 0 || size.height === 0) return;
|
|
76
86
|
const { canvas, paint } = this.#options;
|
|
77
|
-
const
|
|
87
|
+
const { isRebuilt, look } = this.#current();
|
|
78
88
|
const isDirty = this.#isDirty;
|
|
79
89
|
this.#look = look;
|
|
80
90
|
this.#isDirty = false;
|
|
@@ -84,6 +94,7 @@ var CanvasSurface = class {
|
|
|
84
94
|
if (this.#context) paint(this.#context, {
|
|
85
95
|
frameMs,
|
|
86
96
|
isDirty,
|
|
97
|
+
isRebuilt,
|
|
87
98
|
look,
|
|
88
99
|
size
|
|
89
100
|
});
|
|
@@ -140,7 +151,7 @@ var CanvasSurface = class {
|
|
|
140
151
|
const { canvas, colours, lengths = {} } = this.#options;
|
|
141
152
|
const styles = getComputedStyle(canvas);
|
|
142
153
|
return {
|
|
143
|
-
colours: readEach(colours, (property) => styles.getPropertyValue(property).trim()),
|
|
154
|
+
colours: readEach(colours, (property) => styles.getPropertyValue(property).trim().replaceAll("currentcolor", () => styles.color)),
|
|
144
155
|
isReducedMotion: this.#reducedMotion.matches,
|
|
145
156
|
lengths: readEach(lengths, (property) => readPxProperty(styles, property, 0))
|
|
146
157
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-surface.js","names":["named"],"sources":["../../../src/lib/canvas-surface.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ntype PrivateProperty = `--_sonic-${string}`;\n\nexport interface SurfaceSize {\n\tdpr: number;\n\theight: number;\n\twidth: number;\n}\n\nexport interface SurfaceLook<Colour extends string, Length extends string = never> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n}\n\nexport interface SurfaceFrame<Colour extends string, Length extends string = never> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tlook: SurfaceLook<Colour, Length>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tlengths?: Record<Length, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>) => void;\n\tresize?: (size: SurfaceSize) => void;\n\tsignal: AbortSignal;\n}\n\nfunction deviceRatio(): number {\n\treturn globalThis.devicePixelRatio || 1;\n}\n\n// WebKit reports none, and an emulated device scale reports it in CSS px, so it counts only where it agrees\nfunction deviceBox(\n\tentry: ResizeObserverEntry,\n\tcss: ResizeObserverSize,\n): ResizeObserverSize | undefined {\n\tconst device =\n\t\t'devicePixelContentBoxSize' in entry ? entry.devicePixelContentBoxSize[0] : undefined;\n\tconst dpr = deviceRatio();\n\tif (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return undefined;\n\n\treturn Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? undefined : device;\n}\n\n// `Object.fromEntries` widens the keys to `string`\nfunction readEach<Name extends string, Value>(\n\tproperties: Partial<Record<Name, PrivateProperty>>,\n\tread: (property: PrivateProperty) => Value,\n): Record<Name, Value> {\n\tconst named: Readonly<Record<string, PrivateProperty | undefined>> = properties;\n\n\treturn Object.fromEntries(\n\t\tObject.entries(named).flatMap(([name, property]) =>\n\t\t\tproperty === undefined ? [] : [[name, read(property)]],\n\t\t),\n\t) as Record<Name, Value>;\n}\n\nclass CanvasSurface<Colour extends string, Length extends string> implements Surface {\n\tget size(): SurfaceSize {\n\t\treturn this.#size;\n\t}\n\n\t// happy-dom has no canvas context\n\treadonly #context: CanvasRenderingContext2D | undefined;\n\n\t#cssBox: undefined | { height: number; width: number };\n\n\t#frame: number | undefined;\n\n\t#isDirty = true;\n\n\t#isRebuildWanted = true;\n\n\t#isVisible = true;\n\n\t#look: SurfaceLook<Colour, Length> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length>;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#size: SurfaceSize = { dpr: deviceRatio(), height: 0, width: 0 };\n\n\tconstructor(options: SurfaceOptions<Colour, Length>) {\n\t\tconst { canvas, signal } = options;\n\n\t\tthis.#options = options;\n\t\tthis.#context = canvas.getContext('2d') ?? undefined;\n\t\tthis.#armTransitions();\n\t\tthis.#observeSize();\n\t\tthis.#observeView();\n\t\tthis.#watchResolution();\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// `transitionstart` drops events when a change replaces a pending transition in WebKit\n\t\tcanvas.addEventListener(\n\t\t\t'transitionrun',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t\tthis.#schedule();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#isDirty = true;\n\t\tthis.#schedule();\n\t}\n\n\trebuild(): void {\n\t\tthis.#isRebuildWanted = true;\n\t\tthis.invalidate();\n\t}\n\n\trequestFrame(): void {\n\t\tthis.#schedule();\n\t}\n\n\t#armTransitions(): void {\n\t\tconst { style } = this.#options.canvas;\n\n\t\tstyle.setProperty('transition-property', Object.values(this.#options.colours).join(', '));\n\t\tstyle.setProperty('transition-duration', '0.001ms');\n\t\tstyle.setProperty('transition-timing-function', 'step-start');\n\t\tstyle.setProperty('transition-behavior', 'allow-discrete');\n\t}\n\n\t#draw(frameMs: number): void {\n\t\tthis.#frame = undefined;\n\n\t\tconst size = this.#size;\n\t\tif (size.width === 0 || size.height === 0) return;\n\n\t\tconst { canvas, paint } = this.#options;\n\t\tconst look = this.#isRebuildWanted || !this.#look ? this.#read() : this.#look;\n\t\tconst isDirty = this.#isDirty;\n\n\t\tthis.#look = look;\n\t\tthis.#isDirty = false;\n\t\tthis.#isRebuildWanted = false;\n\t\t// Assigning either clears the canvas and its state\n\t\tif (canvas.width !== size.width) canvas.width = size.width;\n\t\tif (canvas.height !== size.height) canvas.height = size.height;\n\t\tif (this.#context) paint(this.#context, { frameMs, isDirty, look, size });\n\t}\n\n\t#measure(entry: ResizeObserverEntry): void {\n\t\tconst [box] = entry.contentBoxSize;\n\t\tif (!box) return;\n\n\t\tconst device = deviceBox(entry, box);\n\n\t\tif (device) {\n\t\t\tthis.#cssBox = undefined;\n\t\t\tthis.#resize(device.inlineSize, device.blockSize);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#cssBox = { height: box.blockSize, width: box.inlineSize };\n\t\tthis.#rescale();\n\t}\n\n\t#observeSize(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\n\t\t\tif (entry) this.#measure(entry);\n\t\t});\n\n\t\ttry {\n\t\t\tobserver.observe(canvas, { box: 'device-pixel-content-box' });\n\t\t} catch {\n\t\t\t// WebKit throws a TypeError on the option\n\t\t\tobserver.observe(canvas, { box: 'content-box' });\n\t\t}\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#observeView(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new IntersectionObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\t\t\tif (!entry) return;\n\n\t\t\tconst wasVisible = this.#isVisible;\n\n\t\t\tthis.#isVisible = entry.isIntersecting;\n\t\t\tif (!entry.isIntersecting && this.#frame !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t}\n\n\t\t\t// Chromium fires no transition inside a skipped `content-visibility` subtree\n\t\t\tif (!wasVisible && entry.isIntersecting) this.rebuild();\n\t\t});\n\n\t\tobserver.observe(canvas);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#read(): SurfaceLook<Colour, Length> {\n\t\tconst { canvas, colours, lengths = {} } = this.#options;\n\t\tconst styles = getComputedStyle(canvas);\n\n\t\treturn {\n\t\t\tcolours: readEach(colours, (property) => styles.getPropertyValue(property).trim()),\n\t\t\tisReducedMotion: this.#reducedMotion.matches,\n\t\t\tlengths: readEach<Length, number>(lengths, (property) => readPxProperty(styles, property, 0)),\n\t\t};\n\t}\n\n\t#rescale(): void {\n\t\tconst box = this.#cssBox;\n\t\tif (!box) {\n\t\t\tthis.#resize(this.#size.width, this.#size.height);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dpr = deviceRatio();\n\n\t\tthis.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));\n\t}\n\n\t#resize(width: number, height: number): void {\n\t\tconst previous = this.#size;\n\t\tconst dpr = deviceRatio();\n\t\tif (previous.width === width && previous.height === height && previous.dpr === dpr) return;\n\n\t\tthis.#size = { dpr, height, width };\n\t\tthis.#options.resize?.(this.#size);\n\t\tif (previous.width === 0 || previous.height === 0) this.rebuild();\n\t\telse this.invalidate();\n\t}\n\n\t#schedule(): void {\n\t\tif (this.#options.signal.aborted || this.#frame !== undefined || !this.#isVisible) return;\n\n\t\tthis.#frame = requestAnimationFrame((frameMs) => {\n\t\t\tthis.#draw(frameMs);\n\t\t});\n\t}\n\n\t#watchResolution(): void {\n\t\tconst { signal } = this.#options;\n\n\t\tmatchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#watchResolution();\n\t\t\t\tthis.#rescale();\n\t\t\t},\n\t\t\t{ once: true, signal },\n\t\t);\n\t}\n}\n\nexport function bindSurface<Colour extends string, Length extends string = never>(\n\toptions: SurfaceOptions<Colour, Length>,\n): Surface {\n\treturn new CanvasSurface(options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"canvas-surface.js","names":["named"],"sources":["../../../src/lib/canvas-surface.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ntype PrivateProperty = `--_sonic-${string}`;\n\nexport interface SurfaceSize {\n\tdpr: number;\n\theight: number;\n\twidth: number;\n}\n\nexport interface SurfaceLook<Colour extends string, Length extends string = never> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n}\n\nexport interface SurfaceFrame<Colour extends string, Length extends string = never> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tisRebuilt: boolean;\n\tlook: SurfaceLook<Colour, Length>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tlengths?: Record<Length, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>) => void;\n\tresize?: (size: SurfaceSize) => void;\n\tsignal: AbortSignal;\n}\n\nfunction deviceRatio(): number {\n\treturn globalThis.devicePixelRatio || 1;\n}\n\n// WebKit reports none, and an emulated device scale reports it in CSS px, so it counts only where it agrees\nfunction deviceBox(\n\tentry: ResizeObserverEntry,\n\tcss: ResizeObserverSize,\n): ResizeObserverSize | undefined {\n\tconst device =\n\t\t'devicePixelContentBoxSize' in entry ? entry.devicePixelContentBoxSize[0] : undefined;\n\tconst dpr = deviceRatio();\n\tif (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return undefined;\n\n\treturn Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? undefined : device;\n}\n\n// `Object.fromEntries` widens the keys to `string`\nfunction readEach<Name extends string, Value>(\n\tproperties: Partial<Record<Name, PrivateProperty>>,\n\tread: (property: PrivateProperty) => Value,\n): Record<Name, Value> {\n\tconst named: Readonly<Record<string, PrivateProperty | undefined>> = properties;\n\n\treturn Object.fromEntries(\n\t\tObject.entries(named).flatMap(([name, property]) =>\n\t\t\tproperty === undefined ? [] : [[name, read(property)]],\n\t\t),\n\t) as Record<Name, Value>;\n}\n\nclass CanvasSurface<Colour extends string, Length extends string> implements Surface {\n\tget size(): SurfaceSize {\n\t\treturn this.#size;\n\t}\n\n\t// happy-dom has no canvas context\n\treadonly #context: CanvasRenderingContext2D | undefined;\n\n\t#cssBox: undefined | { height: number; width: number };\n\n\t#frame: number | undefined;\n\n\t#isDirty = true;\n\n\t#isRebuildWanted = true;\n\n\t#isVisible = true;\n\n\t#look: SurfaceLook<Colour, Length> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length>;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#size: SurfaceSize = { dpr: deviceRatio(), height: 0, width: 0 };\n\n\tconstructor(options: SurfaceOptions<Colour, Length>) {\n\t\tconst { canvas, signal } = options;\n\n\t\tthis.#options = options;\n\t\tthis.#context = canvas.getContext('2d') ?? undefined;\n\t\tthis.#armTransitions();\n\t\tthis.#observeSize();\n\t\tthis.#observeView();\n\t\tthis.#watchResolution();\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// `transitionstart` drops events when a change replaces a pending transition in WebKit\n\t\tcanvas.addEventListener(\n\t\t\t'transitionrun',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t\tthis.#schedule();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#isDirty = true;\n\t\tthis.#schedule();\n\t}\n\n\trebuild(): void {\n\t\tthis.#isRebuildWanted = true;\n\t\tthis.invalidate();\n\t}\n\n\trequestFrame(): void {\n\t\tthis.#schedule();\n\t}\n\n\t#armTransitions(): void {\n\t\tconst { style } = this.#options.canvas;\n\n\t\tstyle.setProperty('transition-property', Object.values(this.#options.colours).join(', '));\n\t\tstyle.setProperty('transition-duration', '0.001ms');\n\t\tstyle.setProperty('transition-timing-function', 'step-start');\n\t\tstyle.setProperty('transition-behavior', 'allow-discrete');\n\t}\n\n\t#current(): { isRebuilt: boolean; look: SurfaceLook<Colour, Length> } {\n\t\tif (this.#look && !this.#isRebuildWanted) return { isRebuilt: false, look: this.#look };\n\n\t\treturn { isRebuilt: true, look: this.#read() };\n\t}\n\n\t#draw(frameMs: number): void {\n\t\tthis.#frame = undefined;\n\n\t\tconst size = this.#size;\n\t\tif (size.width === 0 || size.height === 0) return;\n\n\t\tconst { canvas, paint } = this.#options;\n\t\tconst { isRebuilt, look } = this.#current();\n\t\tconst isDirty = this.#isDirty;\n\n\t\tthis.#look = look;\n\t\tthis.#isDirty = false;\n\t\tthis.#isRebuildWanted = false;\n\t\t// Assigning either clears the canvas and its state\n\t\tif (canvas.width !== size.width) canvas.width = size.width;\n\t\tif (canvas.height !== size.height) canvas.height = size.height;\n\t\tif (this.#context) paint(this.#context, { frameMs, isDirty, isRebuilt, look, size });\n\t}\n\n\t#measure(entry: ResizeObserverEntry): void {\n\t\tconst [box] = entry.contentBoxSize;\n\t\tif (!box) return;\n\n\t\tconst device = deviceBox(entry, box);\n\n\t\tif (device) {\n\t\t\tthis.#cssBox = undefined;\n\t\t\tthis.#resize(device.inlineSize, device.blockSize);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#cssBox = { height: box.blockSize, width: box.inlineSize };\n\t\tthis.#rescale();\n\t}\n\n\t#observeSize(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\n\t\t\tif (entry) this.#measure(entry);\n\t\t});\n\n\t\ttry {\n\t\t\tobserver.observe(canvas, { box: 'device-pixel-content-box' });\n\t\t} catch {\n\t\t\t// WebKit throws a TypeError on the option\n\t\t\tobserver.observe(canvas, { box: 'content-box' });\n\t\t}\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#observeView(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new IntersectionObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\t\t\tif (!entry) return;\n\n\t\t\tconst wasVisible = this.#isVisible;\n\n\t\t\tthis.#isVisible = entry.isIntersecting;\n\t\t\tif (!entry.isIntersecting && this.#frame !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t}\n\n\t\t\t// Chromium fires no transition inside a skipped `content-visibility` subtree\n\t\t\tif (!wasVisible && entry.isIntersecting) this.rebuild();\n\t\t});\n\n\t\tobserver.observe(canvas);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#read(): SurfaceLook<Colour, Length> {\n\t\tconst { canvas, colours, lengths = {} } = this.#options;\n\t\tconst styles = getComputedStyle(canvas);\n\n\t\treturn {\n\t\t\tcolours: readEach(colours, (property) =>\n\t\t\t\tstyles\n\t\t\t\t\t.getPropertyValue(property)\n\t\t\t\t\t.trim()\n\t\t\t\t\t.replaceAll('currentcolor', () => styles.color),\n\t\t\t),\n\t\t\tisReducedMotion: this.#reducedMotion.matches,\n\t\t\tlengths: readEach<Length, number>(lengths, (property) => readPxProperty(styles, property, 0)),\n\t\t};\n\t}\n\n\t#rescale(): void {\n\t\tconst box = this.#cssBox;\n\t\tif (!box) {\n\t\t\tthis.#resize(this.#size.width, this.#size.height);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dpr = deviceRatio();\n\n\t\tthis.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));\n\t}\n\n\t#resize(width: number, height: number): void {\n\t\tconst previous = this.#size;\n\t\tconst dpr = deviceRatio();\n\t\tif (previous.width === width && previous.height === height && previous.dpr === dpr) return;\n\n\t\tthis.#size = { dpr, height, width };\n\t\tthis.#options.resize?.(this.#size);\n\t\tif (previous.width === 0 || previous.height === 0) this.rebuild();\n\t\telse this.invalidate();\n\t}\n\n\t#schedule(): void {\n\t\tif (this.#options.signal.aborted || this.#frame !== undefined || !this.#isVisible) return;\n\n\t\tthis.#frame = requestAnimationFrame((frameMs) => {\n\t\t\tthis.#draw(frameMs);\n\t\t});\n\t}\n\n\t#watchResolution(): void {\n\t\tconst { signal } = this.#options;\n\n\t\tmatchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#watchResolution();\n\t\t\t\tthis.#rescale();\n\t\t\t},\n\t\t\t{ once: true, signal },\n\t\t);\n\t}\n}\n\nexport function bindSurface<Colour extends string, Length extends string = never>(\n\toptions: SurfaceOptions<Colour, Length>,\n): Surface {\n\treturn new CanvasSurface(options);\n}\n"],"mappings":"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"}
|
|
@@ -77,7 +77,7 @@ function byShortfall(values, { exact, order }, direction) {
|
|
|
77
77
|
const value = values[index] ?? NaN;
|
|
78
78
|
return trimFloat(direction * ((exact.get(index) ?? value) - value));
|
|
79
79
|
};
|
|
80
|
-
return order.toReversed().
|
|
80
|
+
return order.toReversed().toSorted((first, second) => shortfall(second) - shortfall(first));
|
|
81
81
|
}
|
|
82
82
|
function stepEach(members, values, pass) {
|
|
83
83
|
let remainder = pass.owed;
|
|
@@ -1 +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().sort((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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"}
|
|
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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"}
|
|
@@ -10,18 +10,18 @@ function dragThresholdPx(pointerType) {
|
|
|
10
10
|
function isAcross(from, to, proportion) {
|
|
11
11
|
return from !== proportion && (from - proportion) * (to - proportion) <= 0;
|
|
12
12
|
}
|
|
13
|
-
function passDetent(hold, from, to) {
|
|
13
|
+
function passDetent(hold, [from, to], pace) {
|
|
14
14
|
if (hold.slack === void 0 && !isAcross(from, to, hold.proportion)) return {
|
|
15
15
|
proportion: to,
|
|
16
16
|
slack: void 0
|
|
17
17
|
};
|
|
18
|
-
const slack = (hold.slack ?? 0) + to - hold.proportion;
|
|
18
|
+
const slack = (hold.slack ?? 0) + (to - hold.proportion) / pace;
|
|
19
19
|
if (Math.abs(slack) <= hold.zone) return {
|
|
20
20
|
proportion: void 0,
|
|
21
21
|
slack
|
|
22
22
|
};
|
|
23
23
|
return {
|
|
24
|
-
proportion: hold.proportion + slack - Math.sign(slack) * hold.zone,
|
|
24
|
+
proportion: hold.proportion + (slack - Math.sign(slack) * hold.zone) * pace,
|
|
25
25
|
slack: void 0
|
|
26
26
|
};
|
|
27
27
|
}
|
|
@@ -77,7 +77,7 @@ function stepDrag(mapping, state, move) {
|
|
|
77
77
|
};
|
|
78
78
|
const { detent } = state;
|
|
79
79
|
if (!detent) return land(mapping, moved, to);
|
|
80
|
-
const passed = passDetent(mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent, state.rawProportion, to);
|
|
80
|
+
const passed = passDetent(mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent, [state.rawProportion, to], pace);
|
|
81
81
|
const held = {
|
|
82
82
|
...moved,
|
|
83
83
|
detent: {
|
|
@@ -1 +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\
|
|
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"}
|
|
@@ -2,6 +2,7 @@ import { trimFloat } from "./math.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/lib/frame-timeline.ts
|
|
4
4
|
const placeholderPeriodMs = 2500;
|
|
5
|
+
const lookAheadSeconds = 30;
|
|
5
6
|
function windowStart(input, seconds, windowSeconds) {
|
|
6
7
|
if (!input.isPaged) return seconds - windowSeconds / 2;
|
|
7
8
|
if (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;
|
|
@@ -31,7 +32,7 @@ function frameTimeline(input) {
|
|
|
31
32
|
const pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);
|
|
32
33
|
const placeholder = placeholderPhase(input, pending);
|
|
33
34
|
const phase = placeholder ?? 0;
|
|
34
|
-
const wanted = overlap([startSeconds - windowSeconds, startSeconds +
|
|
35
|
+
const wanted = overlap([startSeconds - windowSeconds, startSeconds + windowSeconds + Math.max(windowSeconds, lookAheadSeconds)], range);
|
|
35
36
|
const at = (time) => (time - startSeconds) / windowSeconds;
|
|
36
37
|
return {
|
|
37
38
|
...held ? { ghostAt: at(clockSeconds) } : {},
|
|
@@ -1 +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\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([
|
|
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"}
|
|
@@ -3,28 +3,72 @@ import { readPxProperty } from "./read-px-property.js";
|
|
|
3
3
|
//#region src/lib/marker-band.ts
|
|
4
4
|
const kindPattern = /^[a-z][a-z0-9-]*$/;
|
|
5
5
|
const snapSlopPx = 4;
|
|
6
|
-
function
|
|
6
|
+
function isKind(kind) {
|
|
7
|
+
return kind !== void 0 && kindPattern.test(kind);
|
|
8
|
+
}
|
|
9
|
+
function writeKind(part, kind, fallback) {
|
|
10
|
+
if (!isKind(kind)) {
|
|
11
|
+
part.style.removeProperty("--_sonic-marker");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
part.style.setProperty("--_sonic-marker", `var(--sonic-marker-${kind}, var(${fallback}))`);
|
|
15
|
+
}
|
|
16
|
+
function renderMarker(marker, [from, to], lane) {
|
|
7
17
|
const part = document.createElement("div");
|
|
8
|
-
part.className =
|
|
18
|
+
part.className = lane === void 0 ? "sonic-wavestrip-region" : "sonic-wavestrip-marker";
|
|
19
|
+
if (lane !== void 0) part.style.setProperty("--_sonic-marker-lane", String(lane));
|
|
9
20
|
part.style.setProperty("--_sonic-marker-from", String(from));
|
|
10
21
|
part.style.setProperty("--_sonic-marker-to", String(to));
|
|
11
|
-
|
|
22
|
+
writeKind(part, marker.kind, "--_sonic-marker-default");
|
|
12
23
|
if (marker.dimmed === true) part.dataset.sonicDimmed = "";
|
|
13
24
|
return part;
|
|
14
25
|
}
|
|
15
26
|
function createMarkerBand(band) {
|
|
27
|
+
let dots = [];
|
|
28
|
+
const px = (property) => readPxProperty(getComputedStyle(band), property, 0);
|
|
16
29
|
return {
|
|
17
|
-
|
|
30
|
+
at: ({ x, y }, widthPx) => {
|
|
31
|
+
const size = px("--_sonic-marker-size");
|
|
32
|
+
const step = px("--_sonic-marker-step");
|
|
33
|
+
const nearest = nearestDot({
|
|
34
|
+
x,
|
|
35
|
+
y
|
|
36
|
+
}, dots.map(({ at, lane }) => ({
|
|
37
|
+
x: at * widthPx,
|
|
38
|
+
y: size / 2 + lane * step
|
|
39
|
+
})), size / 2 + snapSlopPx);
|
|
40
|
+
return nearest === void 0 ? void 0 : dots[nearest]?.index;
|
|
41
|
+
},
|
|
18
42
|
render: (markers, proportions) => {
|
|
19
|
-
|
|
43
|
+
const widthPx = band.getBoundingClientRect().width;
|
|
44
|
+
const points = markers.flatMap((marker, index) => marker.end === void 0 ? [{
|
|
45
|
+
at: proportions[index]?.[0] ?? 0,
|
|
46
|
+
index
|
|
47
|
+
}] : []);
|
|
48
|
+
const lanes = markerLanes(points.map(({ at }) => at * widthPx), px("--_sonic-marker-step"));
|
|
49
|
+
dots = points.map((point, order) => ({
|
|
50
|
+
...point,
|
|
51
|
+
lane: lanes[order] ?? 0
|
|
52
|
+
}));
|
|
53
|
+
const laneOf = new Map(dots.map(({ index, lane }) => [index, lane]));
|
|
54
|
+
band.replaceChildren(...markers.map((marker, index) => renderMarker(marker, proportions[index] ?? [0, 0], laneOf.get(index))));
|
|
20
55
|
}
|
|
21
56
|
};
|
|
22
57
|
}
|
|
23
|
-
function
|
|
58
|
+
function markerLanes(positions, minDistance) {
|
|
59
|
+
const lastInLane = [];
|
|
60
|
+
return positions.map((position) => {
|
|
61
|
+
const free = lastInLane.findIndex((last) => position - last >= minDistance);
|
|
62
|
+
const lane = free === -1 ? lastInLane.length : free;
|
|
63
|
+
lastInLane[lane] = position;
|
|
64
|
+
return lane;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function nearestDot(point, dots, reach) {
|
|
24
68
|
let nearest;
|
|
25
|
-
let nearestDistance = reach;
|
|
26
|
-
for (const [index,
|
|
27
|
-
const distance =
|
|
69
|
+
let nearestDistance = reach * reach;
|
|
70
|
+
for (const [index, dot] of dots.entries()) {
|
|
71
|
+
const distance = (dot.x - point.x) ** 2 + (dot.y - point.y) ** 2;
|
|
28
72
|
if (distance > nearestDistance) continue;
|
|
29
73
|
nearest = index;
|
|
30
74
|
nearestDistance = distance;
|
|
@@ -33,5 +77,5 @@ function nearestMarker(proportion, markerProportions, reach) {
|
|
|
33
77
|
}
|
|
34
78
|
|
|
35
79
|
//#endregion
|
|
36
|
-
export { createMarkerBand,
|
|
80
|
+
export { createMarkerBand, isKind, markerLanes, nearestDot, writeKind };
|
|
37
81
|
//# sourceMappingURL=marker-band.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marker-band.js","names":[],"sources":["../../../src/lib/marker-band.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ninterface MarkerLook {\n\tdimmed?: boolean;\n\tend?: number;\n\tkind?: string;\n}\n\ninterface MarkerBand {\n\
|
|
1
|
+
{"version":3,"file":"marker-band.js","names":[],"sources":["../../../src/lib/marker-band.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ninterface MarkerLook {\n\tdimmed?: boolean;\n\tend?: number;\n\tkind?: string;\n}\n\ninterface Point {\n\tx: number;\n\ty: number;\n}\n\ninterface MarkerBand {\n\tat: (point: Point, widthPx: number) => number | undefined;\n\trender: (\n\t\tmarkers: ReadonlyArray<MarkerLook>,\n\t\tproportions: ReadonlyArray<[number, number]>,\n\t) => void;\n}\n\n// Interpolated into a style, so anything else falls back to the default colour\nconst kindPattern = /^[a-z][a-z0-9-]*$/;\n\nconst snapSlopPx = 4;\n\nexport function isKind(kind: string | undefined): kind is string {\n\treturn kind !== undefined && kindPattern.test(kind);\n}\n\nexport function writeKind(part: HTMLElement, kind: string | undefined, fallback: string): void {\n\tif (!isKind(kind)) {\n\t\tpart.style.removeProperty('--_sonic-marker');\n\t\treturn;\n\t}\n\n\tpart.style.setProperty('--_sonic-marker', `var(--sonic-marker-${kind}, var(${fallback}))`);\n}\n\nfunction renderMarker(\n\tmarker: MarkerLook,\n\t[from, to]: [number, number],\n\tlane: number | undefined,\n): HTMLElement {\n\tconst part = document.createElement('div');\n\n\tpart.className = lane === undefined ? 'sonic-wavestrip-region' : 'sonic-wavestrip-marker';\n\tif (lane !== undefined) part.style.setProperty('--_sonic-marker-lane', String(lane));\n\tpart.style.setProperty('--_sonic-marker-from', String(from));\n\tpart.style.setProperty('--_sonic-marker-to', String(to));\n\twriteKind(part, marker.kind, '--_sonic-marker-default');\n\tif (marker.dimmed === true) part.dataset.sonicDimmed = '';\n\n\treturn part;\n}\n\nexport function createMarkerBand(band: HTMLElement): MarkerBand {\n\tlet dots: Array<{ at: number; index: number; lane: number }> = [];\n\n\tconst px = (property: string): number => readPxProperty(getComputedStyle(band), property, 0);\n\n\treturn {\n\t\tat: ({ x, y }, widthPx) => {\n\t\t\tconst size = px('--_sonic-marker-size');\n\t\t\tconst step = px('--_sonic-marker-step');\n\t\t\tconst nearest = nearestDot(\n\t\t\t\t{ x, y },\n\t\t\t\tdots.map(({ at, lane }) => ({ x: at * widthPx, y: size / 2 + lane * step })),\n\t\t\t\tsize / 2 + snapSlopPx,\n\t\t\t);\n\n\t\t\treturn nearest === undefined ? undefined : dots[nearest]?.index;\n\t\t},\n\t\trender: (markers, proportions) => {\n\t\t\tconst widthPx = band.getBoundingClientRect().width;\n\t\t\tconst points = markers.flatMap((marker, index) =>\n\t\t\t\tmarker.end === undefined ? [{ at: proportions[index]?.[0] ?? 0, index }] : [],\n\t\t\t);\n\t\t\tconst lanes = markerLanes(\n\t\t\t\tpoints.map(({ at }) => at * widthPx),\n\t\t\t\tpx('--_sonic-marker-step'),\n\t\t\t);\n\n\t\t\tdots = points.map((point, order) => ({ ...point, lane: lanes[order] ?? 0 }));\n\n\t\t\tconst laneOf = new Map(dots.map(({ index, lane }) => [index, lane]));\n\n\t\t\tband.replaceChildren(\n\t\t\t\t...markers.map((marker, index) =>\n\t\t\t\t\trenderMarker(marker, proportions[index] ?? [0, 0], laneOf.get(index)),\n\t\t\t\t),\n\t\t\t);\n\t\t},\n\t};\n}\n\nexport function markerLanes(positions: ReadonlyArray<number>, minDistance: number): Array<number> {\n\tconst lastInLane: Array<number> = [];\n\n\treturn positions.map((position) => {\n\t\tconst free = lastInLane.findIndex((last) => position - last >= minDistance);\n\t\tconst lane = free === -1 ? lastInLane.length : free;\n\n\t\tlastInLane[lane] = position;\n\n\t\treturn lane;\n\t});\n}\n\nexport function nearestDot(\n\tpoint: Point,\n\tdots: ReadonlyArray<Point>,\n\treach: number,\n): number | undefined {\n\tlet nearest: number | undefined;\n\tlet nearestDistance = reach * reach;\n\n\tfor (const [index, dot] of dots.entries()) {\n\t\tconst distance = (dot.x - point.x) ** 2 + (dot.y - point.y) ** 2;\n\t\tif (distance > nearestDistance) continue;\n\n\t\tnearest = index;\n\t\tnearestDistance = distance;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";;;AAsBA,MAAM,cAAc;AAEpB,MAAM,aAAa;AAEnB,SAAgB,OAAO,MAA0C;CAChE,OAAO,SAAS,UAAa,YAAY,KAAK,IAAI;AACnD;AAEA,SAAgB,UAAU,MAAmB,MAA0B,UAAwB;CAC9F,IAAI,CAAC,OAAO,IAAI,GAAG;EAClB,KAAK,MAAM,eAAe,iBAAiB;EAC3C;CACD;CAEA,KAAK,MAAM,YAAY,mBAAmB,sBAAsB,KAAK,QAAQ,SAAS,GAAG;AAC1F;AAEA,SAAS,aACR,QACA,CAAC,MAAM,KACP,MACc;CACd,MAAM,OAAO,SAAS,cAAc,KAAK;CAEzC,KAAK,YAAY,SAAS,SAAY,2BAA2B;CACjE,IAAI,SAAS,QAAW,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;CACnF,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;CAC3D,KAAK,MAAM,YAAY,sBAAsB,OAAO,EAAE,CAAC;CACvD,UAAU,MAAM,OAAO,MAAM,yBAAyB;CACtD,IAAI,OAAO,WAAW,MAAM,KAAK,QAAQ,cAAc;CAEvD,OAAO;AACR;AAEA,SAAgB,iBAAiB,MAA+B;CAC/D,IAAI,OAA2D,CAAC;CAEhE,MAAM,MAAM,aAA6B,eAAe,iBAAiB,IAAI,GAAG,UAAU,CAAC;CAE3F,OAAO;EACN,KAAK,EAAE,GAAG,KAAK,YAAY;GAC1B,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,UAAU,WACf;IAAE;IAAG;GAAE,GACP,KAAK,KAAK,EAAE,IAAI,YAAY;IAAE,GAAG,KAAK;IAAS,GAAG,OAAO,IAAI,OAAO;GAAK,EAAE,GAC3E,OAAO,IAAI,UACZ;GAEA,OAAO,YAAY,SAAY,SAAY,KAAK,QAAQ,EAAE;EAC3D;EACA,SAAS,SAAS,gBAAgB;GACjC,MAAM,UAAU,KAAK,sBAAsB,CAAC,CAAC;GAC7C,MAAM,SAAS,QAAQ,SAAS,QAAQ,UACvC,OAAO,QAAQ,SAAY,CAAC;IAAE,IAAI,YAAY,MAAM,GAAG,MAAM;IAAG;GAAM,CAAC,IAAI,CAAC,CAC7E;GACA,MAAM,QAAQ,YACb,OAAO,KAAK,EAAE,SAAS,KAAK,OAAO,GACnC,GAAG,sBAAsB,CAC1B;GAEA,OAAO,OAAO,KAAK,OAAO,WAAW;IAAE,GAAG;IAAO,MAAM,MAAM,UAAU;GAAE,EAAE;GAE3E,MAAM,SAAS,IAAI,IAAI,KAAK,KAAK,EAAE,OAAO,WAAW,CAAC,OAAO,IAAI,CAAC,CAAC;GAEnE,KAAK,gBACJ,GAAG,QAAQ,KAAK,QAAQ,UACvB,aAAa,QAAQ,YAAY,UAAU,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,KAAK,CAAC,CACrE,CACD;EACD;CACD;AACD;AAEA,SAAgB,YAAY,WAAkC,aAAoC;CACjG,MAAM,aAA4B,CAAC;CAEnC,OAAO,UAAU,KAAK,aAAa;EAClC,MAAM,OAAO,WAAW,WAAW,SAAS,WAAW,QAAQ,WAAW;EAC1E,MAAM,OAAO,SAAS,KAAK,WAAW,SAAS;EAE/C,WAAW,QAAQ;EAEnB,OAAO;CACR,CAAC;AACF;AAEA,SAAgB,WACf,OACA,MACA,OACqB;CACrB,IAAI;CACJ,IAAI,kBAAkB,QAAQ;CAE9B,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,QAAQ,GAAG;EAC1C,MAAM,YAAY,IAAI,IAAI,MAAM,MAAM,KAAK,IAAI,IAAI,MAAM,MAAM;EAC/D,IAAI,WAAW,iBAAiB;EAEhC,UAAU;EACV,kBAAkB;CACnB;CAEA,OAAO;AACR"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clamp } from "./math.js";
|
|
2
2
|
import { requireChild } from "./render.js";
|
|
3
3
|
import { readPxProperty } from "./read-px-property.js";
|
|
4
|
+
import { writeKind } from "./marker-band.js";
|
|
4
5
|
|
|
5
6
|
//#region src/lib/marker-rider.ts
|
|
6
7
|
const fadeShare = .25;
|
|
@@ -43,6 +44,7 @@ function layoutRider(labels, view) {
|
|
|
43
44
|
function labelSlot(element) {
|
|
44
45
|
return {
|
|
45
46
|
element,
|
|
47
|
+
kind: void 0,
|
|
46
48
|
opacity: NaN,
|
|
47
49
|
shownPx: NaN,
|
|
48
50
|
x: NaN
|
|
@@ -63,13 +65,17 @@ function writePlacement(slot, { opacity, shownPx, x }, widthPx) {
|
|
|
63
65
|
style.setProperty("opacity", opacity < 1 ? opacity.toFixed(3) : "");
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
|
-
function writeLabel(slot, labels, placement) {
|
|
68
|
+
function writeLabel(slot, [labels, colourProperty], placement) {
|
|
67
69
|
const { element } = slot;
|
|
68
70
|
const label = placement && placement.shownPx > 0 ? labels[placement.index] : void 0;
|
|
69
71
|
if (element.hidden !== !label) element.hidden = !label;
|
|
70
72
|
if (!label || !placement) return;
|
|
71
73
|
if (element.textContent !== label.text) element.textContent = label.text;
|
|
72
74
|
element.toggleAttribute("data-sonic-dimmed", label.isDimmed);
|
|
75
|
+
if (slot.kind !== label.kind) {
|
|
76
|
+
slot.kind = label.kind;
|
|
77
|
+
writeKind(element, label.kind, colourProperty);
|
|
78
|
+
}
|
|
73
79
|
writePlacement(slot, placement, label.widthPx);
|
|
74
80
|
}
|
|
75
81
|
function measureWidths(control, className, texts) {
|
|
@@ -85,7 +91,7 @@ function measureWidths(control, className, texts) {
|
|
|
85
91
|
return widths;
|
|
86
92
|
}
|
|
87
93
|
function createLabelRider(control, options) {
|
|
88
|
-
const { className, insetProperty } = options;
|
|
94
|
+
const { className, colourProperty, insetProperty } = options;
|
|
89
95
|
const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
|
|
90
96
|
const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
|
|
91
97
|
let insetPx = 0;
|
|
@@ -104,8 +110,8 @@ function createLabelRider(control, options) {
|
|
|
104
110
|
...window,
|
|
105
111
|
insetPx
|
|
106
112
|
});
|
|
107
|
-
writeLabel(parked, measured, layout.parked);
|
|
108
|
-
writeLabel(arriving, measured, layout.arriving);
|
|
113
|
+
writeLabel(parked, [measured, colourProperty], layout.parked);
|
|
114
|
+
writeLabel(arriving, [measured, colourProperty], layout.arriving);
|
|
109
115
|
}
|
|
110
116
|
};
|
|
111
117
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marker-rider.js","names":[],"sources":["../../../src/lib/marker-rider.ts"],"sourcesContent":["import { clamp } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild } from '#lib/render.ts';\n\ninterface RiderWindow {\n\tplayheadSeconds: number;\n\tstartSeconds: number;\n\twidthPx: number;\n\twindowSeconds: number;\n}\n\nexport interface RiderView extends RiderWindow {\n\tinsetPx: number;\n}\n\nexport interface RiderLabel {\n\tisDimmed: boolean;\n\tstart: number;\n\ttext: string;\n\twidthPx: number;\n}\n\ninterface LabelPlacement {\n\tindex: number;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\ninterface RiderLayout {\n\tarriving?: LabelPlacement;\n\tparked?: LabelPlacement;\n}\n\ninterface LabelSlot {\n\telement: HTMLElement;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\nconst fadeShare = 0.25;\n\nfunction lineX(seconds: number, view: RiderView): number {\n\treturn ((seconds - view.startSeconds) / view.windowSeconds) * view.widthPx;\n}\n\nfunction parkedPlacement(\n\tlabels: ReadonlyArray<RiderLabel>,\n\tindex: number,\n\t[view, arrivingX]: [RiderView, number],\n): LabelPlacement | undefined {\n\tconst parked = labels[index];\n\tif (!parked) return undefined;\n\n\tconst next = labels[index + 1];\n\tconst x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);\n\tconst approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;\n\n\treturn {\n\t\tindex,\n\t\topacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),\n\t\tshownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),\n\t\tx,\n\t};\n}\n\nfunction isDrawable(length: number): boolean {\n\treturn Number.isFinite(length) && length > 0;\n}\n\nexport function layoutRider(labels: ReadonlyArray<RiderLabel>, view: RiderView): RiderLayout {\n\tif (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};\n\n\tconst parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);\n\tconst next = labels[parkedIndex + 1];\n\tconst arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;\n\tconst parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);\n\tconst arriving: LabelPlacement | undefined =\n\t\tnext && arrivingX < view.widthPx\n\t\t\t? {\n\t\t\t\t\tindex: parkedIndex + 1,\n\t\t\t\t\topacity: 1,\n\t\t\t\t\tshownPx: Math.min(next.widthPx, view.widthPx - arrivingX),\n\t\t\t\t\tx: arrivingX,\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn { ...(arriving ? { arriving } : {}), ...(parked ? { parked } : {}) };\n}\n\nfunction labelSlot(element: HTMLElement): LabelSlot {\n\treturn { element, opacity: NaN, shownPx: NaN, x: NaN };\n}\n\nfunction writePlacement(\n\tslot: LabelSlot,\n\t{ opacity, shownPx, x }: LabelPlacement,\n\twidthPx: number,\n): void {\n\tconst { style } = slot.element;\n\n\tif (slot.x !== x) {\n\t\tslot.x = x;\n\t\tstyle.setProperty('translate', `${x.toFixed(2)}px`);\n\t}\n\tif (slot.shownPx !== shownPx) {\n\t\tslot.shownPx = shownPx;\n\t\tstyle.setProperty(\n\t\t\t'clip-path',\n\t\t\tshownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : '',\n\t\t);\n\t}\n\tif (slot.opacity !== opacity) {\n\t\tslot.opacity = opacity;\n\t\tstyle.setProperty('opacity', opacity < 1 ? opacity.toFixed(3) : '');\n\t}\n}\n\nfunction writeLabel(\n\tslot: LabelSlot,\n\
|
|
1
|
+
{"version":3,"file":"marker-rider.js","names":[],"sources":["../../../src/lib/marker-rider.ts"],"sourcesContent":["import { writeKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild } from '#lib/render.ts';\n\ninterface RiderWindow {\n\tplayheadSeconds: number;\n\tstartSeconds: number;\n\twidthPx: number;\n\twindowSeconds: number;\n}\n\nexport interface RiderView extends RiderWindow {\n\tinsetPx: number;\n}\n\nexport interface RiderLabel {\n\tisDimmed: boolean;\n\tkind?: string;\n\tstart: number;\n\ttext: string;\n\twidthPx: number;\n}\n\ninterface LabelPlacement {\n\tindex: number;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\ninterface RiderLayout {\n\tarriving?: LabelPlacement;\n\tparked?: LabelPlacement;\n}\n\ninterface LabelSlot {\n\telement: HTMLElement;\n\tkind: string | undefined;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\nconst fadeShare = 0.25;\n\nfunction lineX(seconds: number, view: RiderView): number {\n\treturn ((seconds - view.startSeconds) / view.windowSeconds) * view.widthPx;\n}\n\nfunction parkedPlacement(\n\tlabels: ReadonlyArray<RiderLabel>,\n\tindex: number,\n\t[view, arrivingX]: [RiderView, number],\n): LabelPlacement | undefined {\n\tconst parked = labels[index];\n\tif (!parked) return undefined;\n\n\tconst next = labels[index + 1];\n\tconst x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);\n\tconst approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;\n\n\treturn {\n\t\tindex,\n\t\topacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),\n\t\tshownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),\n\t\tx,\n\t};\n}\n\nfunction isDrawable(length: number): boolean {\n\treturn Number.isFinite(length) && length > 0;\n}\n\nexport function layoutRider(labels: ReadonlyArray<RiderLabel>, view: RiderView): RiderLayout {\n\tif (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};\n\n\tconst parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);\n\tconst next = labels[parkedIndex + 1];\n\tconst arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;\n\tconst parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);\n\tconst arriving: LabelPlacement | undefined =\n\t\tnext && arrivingX < view.widthPx\n\t\t\t? {\n\t\t\t\t\tindex: parkedIndex + 1,\n\t\t\t\t\topacity: 1,\n\t\t\t\t\tshownPx: Math.min(next.widthPx, view.widthPx - arrivingX),\n\t\t\t\t\tx: arrivingX,\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn { ...(arriving ? { arriving } : {}), ...(parked ? { parked } : {}) };\n}\n\nfunction labelSlot(element: HTMLElement): LabelSlot {\n\treturn { element, kind: undefined, opacity: NaN, shownPx: NaN, x: NaN };\n}\n\nfunction writePlacement(\n\tslot: LabelSlot,\n\t{ opacity, shownPx, x }: LabelPlacement,\n\twidthPx: number,\n): void {\n\tconst { style } = slot.element;\n\n\tif (slot.x !== x) {\n\t\tslot.x = x;\n\t\tstyle.setProperty('translate', `${x.toFixed(2)}px`);\n\t}\n\tif (slot.shownPx !== shownPx) {\n\t\tslot.shownPx = shownPx;\n\t\tstyle.setProperty(\n\t\t\t'clip-path',\n\t\t\tshownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : '',\n\t\t);\n\t}\n\tif (slot.opacity !== opacity) {\n\t\tslot.opacity = opacity;\n\t\tstyle.setProperty('opacity', opacity < 1 ? opacity.toFixed(3) : '');\n\t}\n}\n\nfunction writeLabel(\n\tslot: LabelSlot,\n\t[labels, colourProperty]: [ReadonlyArray<RiderLabel>, string],\n\tplacement: LabelPlacement | undefined,\n): void {\n\tconst { element } = slot;\n\tconst label = placement && placement.shownPx > 0 ? labels[placement.index] : undefined;\n\n\tif (element.hidden !== !label) element.hidden = !label;\n\tif (!label || !placement) return;\n\n\tif (element.textContent !== label.text) element.textContent = label.text;\n\telement.toggleAttribute('data-sonic-dimmed', label.isDimmed);\n\tif (slot.kind !== label.kind) {\n\t\tslot.kind = label.kind;\n\t\twriteKind(element, label.kind, colourProperty);\n\t}\n\twritePlacement(slot, placement, label.widthPx);\n}\n\nfunction measureWidths(\n\tcontrol: HTMLElement,\n\tclassName: string,\n\ttexts: Array<string>,\n): Array<number> {\n\tconst probes = texts.map((text) => {\n\t\tconst probe = document.createElement('div');\n\n\t\tprobe.className = className;\n\t\tprobe.textContent = text;\n\n\t\treturn probe;\n\t});\n\n\tcontrol.append(...probes);\n\n\tconst widths = probes.map((probe) => probe.getBoundingClientRect().width);\n\n\tfor (const probe of probes) probe.remove();\n\n\treturn widths;\n}\n\nexport function createLabelRider(\n\tcontrol: HTMLElement,\n\toptions: {\n\t\tclassName: string;\n\t\tcolourProperty: `--_sonic-${string}`;\n\t\tinsetProperty: `--_sonic-${string}`;\n\t},\n): {\n\tmeasure: (labels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>) => void;\n\tplace: (window: RiderWindow) => void;\n} {\n\tconst { className, colourProperty, insetProperty } = options;\n\tconst parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));\n\tconst arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));\n\tlet insetPx = 0;\n\tlet measured: Array<RiderLabel> = [];\n\n\treturn {\n\t\tmeasure: (labels) => {\n\t\t\tconst widths = measureWidths(\n\t\t\t\tcontrol,\n\t\t\t\tclassName,\n\t\t\t\tlabels.map(({ text }) => text),\n\t\t\t);\n\n\t\t\tinsetPx = readPxProperty(getComputedStyle(control), insetProperty, 0);\n\t\t\tmeasured = labels\n\t\t\t\t.map((label, index) => ({ ...label, widthPx: widths[index] ?? 0 }))\n\t\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\t},\n\t\tplace: (window) => {\n\t\t\tconst layout = layoutRider(measured, { ...window, insetPx });\n\n\t\t\twriteLabel(parked, [measured, colourProperty], layout.parked);\n\t\t\twriteLabel(arriving, [measured, colourProperty], layout.arriving);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
package/dist/dev/lib/math.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
function clamp(value, min, max) {
|
|
3
3
|
return Math.min(max, Math.max(min, value));
|
|
4
4
|
}
|
|
5
|
+
function toNumber(value) {
|
|
6
|
+
if (typeof value === "number") return value;
|
|
7
|
+
if (typeof value !== "string" || value.trim() === "") return NaN;
|
|
8
|
+
return Number(value);
|
|
9
|
+
}
|
|
5
10
|
function clampProportion(fraction) {
|
|
6
11
|
return clamp(fraction, 0, 1);
|
|
7
12
|
}
|
|
@@ -22,5 +27,5 @@ function roundTo(value, places) {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
//#endregion
|
|
25
|
-
export { clamp, clampProportion, decimalPlaces, roundTo, trimFloat, wrapProportion };
|
|
30
|
+
export { clamp, clampProportion, decimalPlaces, roundTo, toNumber, trimFloat, wrapProportion };
|
|
26
31
|
//# sourceMappingURL=math.js.map
|
package/dist/dev/lib/math.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"math.js","names":[],"sources":["../../../src/lib/math.ts"],"sourcesContent":["export function clamp(value: number, min: number, max: number): number {\n\treturn Math.min(max, Math.max(min, value));\n}\n\nexport function clampProportion(fraction: number): number {\n\treturn clamp(fraction, 0, 1);\n}\n\nexport function wrapProportion(fraction: number): number {\n\treturn fraction - Math.floor(fraction);\n}\n\nexport function trimFloat(value: number): number {\n\treturn Number(value.toPrecision(12));\n}\n\n// `String(1e-7)` is \"1e-7\", so the exponent counts as places too\nexport function decimalPlaces(value: number): number {\n\tconst [digits = '', exponent = '0'] = String(value).split('e', 2);\n\tconst fraction = digits.split('.', 2)[1] ?? '';\n\n\treturn Math.max(0, fraction.length - Number(exponent));\n}\n\n// `toFixed` takes at most 100 places, and turns a residue below zero into -0\nexport function roundTo(value: number, places: number): number {\n\tconst rounded = Number(value.toFixed(Math.min(100, places)));\n\n\treturn rounded === 0 ? 0 : rounded;\n}\n"],"mappings":";AAAA,SAAgB,MAAM,OAAe,KAAa,KAAqB;CACtE,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC1C;AAEA,SAAgB,gBAAgB,UAA0B;CACzD,OAAO,MAAM,UAAU,GAAG,CAAC;AAC5B;AAEA,SAAgB,eAAe,UAA0B;CACxD,OAAO,WAAW,KAAK,MAAM,QAAQ;AACtC;AAEA,SAAgB,UAAU,OAAuB;CAChD,OAAO,OAAO,MAAM,YAAY,EAAE,CAAC;AACpC;AAGA,SAAgB,cAAc,OAAuB;CACpD,MAAM,CAAC,SAAS,IAAI,WAAW,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;CAChE,MAAM,WAAW,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM;CAE5C,OAAO,KAAK,IAAI,GAAG,SAAS,SAAS,OAAO,QAAQ,CAAC;AACtD;AAGA,SAAgB,QAAQ,OAAe,QAAwB;CAC9D,MAAM,UAAU,OAAO,MAAM,QAAQ,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC;CAE3D,OAAO,YAAY,IAAI,IAAI;AAC5B"}
|
|
1
|
+
{"version":3,"file":"math.js","names":[],"sources":["../../../src/lib/math.ts"],"sourcesContent":["export function clamp(value: number, min: number, max: number): number {\n\treturn Math.min(max, Math.max(min, value));\n}\n\n// A framework may set a number property as a string; a blank one is no number, as in an attribute\nexport function toNumber(value: unknown): number {\n\tif (typeof value === 'number') return value;\n\tif (typeof value !== 'string' || value.trim() === '') return NaN;\n\n\treturn Number(value);\n}\n\nexport function clampProportion(fraction: number): number {\n\treturn clamp(fraction, 0, 1);\n}\n\nexport function wrapProportion(fraction: number): number {\n\treturn fraction - Math.floor(fraction);\n}\n\nexport function trimFloat(value: number): number {\n\treturn Number(value.toPrecision(12));\n}\n\n// `String(1e-7)` is \"1e-7\", so the exponent counts as places too\nexport function decimalPlaces(value: number): number {\n\tconst [digits = '', exponent = '0'] = String(value).split('e', 2);\n\tconst fraction = digits.split('.', 2)[1] ?? '';\n\n\treturn Math.max(0, fraction.length - Number(exponent));\n}\n\n// `toFixed` takes at most 100 places, and turns a residue below zero into -0\nexport function roundTo(value: number, places: number): number {\n\tconst rounded = Number(value.toFixed(Math.min(100, places)));\n\n\treturn rounded === 0 ? 0 : rounded;\n}\n"],"mappings":";AAAA,SAAgB,MAAM,OAAe,KAAa,KAAqB;CACtE,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC1C;AAGA,SAAgB,SAAS,OAAwB;CAChD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAAI,OAAO;CAE7D,OAAO,OAAO,KAAK;AACpB;AAEA,SAAgB,gBAAgB,UAA0B;CACzD,OAAO,MAAM,UAAU,GAAG,CAAC;AAC5B;AAEA,SAAgB,eAAe,UAA0B;CACxD,OAAO,WAAW,KAAK,MAAM,QAAQ;AACtC;AAEA,SAAgB,UAAU,OAAuB;CAChD,OAAO,OAAO,MAAM,YAAY,EAAE,CAAC;AACpC;AAGA,SAAgB,cAAc,OAAuB;CACpD,MAAM,CAAC,SAAS,IAAI,WAAW,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;CAChE,MAAM,WAAW,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM;CAE5C,OAAO,KAAK,IAAI,GAAG,SAAS,SAAS,OAAO,QAAQ,CAAC;AACtD;AAGA,SAAgB,QAAQ,OAAe,QAAwB;CAC9D,MAAM,UAAU,OAAO,MAAM,QAAQ,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC;CAE3D,OAAO,YAAY,IAAI,IAAI;AAC5B"}
|