@xsynaptic/sonic-ui 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -21
- package/dist/default/dat.js +50 -0
- package/dist/default/define/button.js +7 -0
- package/dist/default/define/envelope.js +7 -0
- package/dist/default/define/split.js +7 -0
- package/dist/default/define/switch.js +7 -0
- package/dist/default/define/waveform.js +7 -0
- package/dist/default/define/wavestrip.js +7 -0
- package/dist/default/define/xy.js +7 -0
- package/dist/default/define.js +7 -1
- package/dist/default/elements/button.js +165 -0
- package/dist/default/elements/dial.js +16 -22
- package/dist/default/elements/envelope.js +367 -0
- package/dist/default/elements/meter.js +149 -61
- package/dist/default/elements/number.js +21 -8
- package/dist/default/elements/radio-group.js +185 -0
- package/dist/default/elements/readout-claim.js +81 -0
- package/dist/default/elements/readout.js +35 -0
- package/dist/default/elements/segmented.js +9 -159
- package/dist/default/elements/slider.js +42 -51
- package/dist/default/elements/sonic-element.js +24 -13
- package/dist/default/elements/split.js +249 -0
- package/dist/default/elements/switch.js +279 -0
- package/dist/default/elements/value-element.js +691 -0
- package/dist/default/elements/{range-entry.js → value-entry.js} +3 -3
- package/dist/default/elements/wave-element.js +77 -0
- package/dist/default/elements/waveform.js +305 -0
- package/dist/default/elements/wavestrip.js +182 -0
- package/dist/default/elements/xy.js +473 -0
- package/dist/default/index.js +10 -2
- package/dist/default/lib/adsr-shape.js +108 -0
- package/dist/default/lib/ballistics.js +55 -0
- package/dist/default/lib/canvas-surface.js +192 -0
- package/dist/default/lib/copy-node.js +32 -2
- package/dist/default/lib/crossfade.js +22 -0
- package/dist/default/lib/distribute.js +215 -0
- package/dist/default/lib/drag-step.js +99 -0
- package/dist/default/lib/envelope-curve.js +9 -0
- package/dist/default/lib/field.js +94 -0
- package/dist/default/lib/focus-by-pointer.js +28 -0
- package/dist/default/lib/frame-timeline.js +62 -0
- package/dist/default/lib/hold.js +38 -0
- package/dist/default/lib/marker-band.js +36 -0
- package/dist/default/lib/marker-rider.js +114 -0
- package/dist/default/lib/math.js +12 -3
- package/dist/default/lib/modifier-press.js +11 -0
- package/dist/default/lib/number-list.js +6 -6
- package/dist/default/lib/pointer-drag.js +43 -0
- package/dist/default/lib/render.js +20 -5
- package/dist/default/lib/resample-peaks.js +36 -0
- package/dist/default/lib/strip-scene.js +48 -0
- package/dist/default/lib/taper.js +23 -17
- package/dist/default/lib/time-regions.js +8 -0
- package/dist/default/lib/tracking-clock.js +35 -0
- package/dist/default/lib/value-mapping.js +115 -0
- package/dist/default/lib/waveform-buckets.js +41 -0
- package/dist/dev/dat.d.ts +19 -0
- package/dist/dev/dat.js +51 -0
- package/dist/dev/dat.js.map +1 -0
- package/dist/dev/define/button.d.ts +2 -0
- package/dist/dev/define/button.js +8 -0
- package/dist/dev/define/button.js.map +1 -0
- package/dist/dev/define/dial.js.map +1 -1
- package/dist/dev/define/envelope.d.ts +2 -0
- package/dist/dev/define/envelope.js +8 -0
- package/dist/dev/define/envelope.js.map +1 -0
- package/dist/dev/define/meter.js.map +1 -1
- package/dist/dev/define/number.js.map +1 -1
- package/dist/dev/define/segmented.js.map +1 -1
- package/dist/dev/define/slider.js.map +1 -1
- package/dist/dev/define/split.d.ts +2 -0
- package/dist/dev/define/split.js +8 -0
- package/dist/dev/define/split.js.map +1 -0
- package/dist/dev/define/switch.d.ts +2 -0
- package/dist/dev/define/switch.js +8 -0
- package/dist/dev/define/switch.js.map +1 -0
- package/dist/dev/define/waveform.d.ts +2 -0
- package/dist/dev/define/waveform.js +8 -0
- package/dist/dev/define/waveform.js.map +1 -0
- package/dist/dev/define/wavestrip.d.ts +2 -0
- package/dist/dev/define/wavestrip.js +8 -0
- package/dist/dev/define/wavestrip.js.map +1 -0
- package/dist/dev/define/xy.d.ts +2 -0
- package/dist/dev/define/xy.js +8 -0
- package/dist/dev/define/xy.js.map +1 -0
- package/dist/dev/define.d.ts +15 -3
- package/dist/dev/define.js +7 -1
- package/dist/dev/elements/{key.d.ts → button.d.ts} +8 -6
- package/dist/dev/elements/button.js +166 -0
- package/dist/dev/elements/button.js.map +1 -0
- package/dist/dev/elements/dial.d.ts +6 -4
- package/dist/dev/elements/dial.js +16 -22
- package/dist/dev/elements/dial.js.map +1 -1
- package/dist/dev/elements/envelope.d.ts +36 -0
- package/dist/dev/elements/envelope.js +372 -0
- package/dist/dev/elements/envelope.js.map +1 -0
- package/dist/dev/elements/form-element.d.ts +1 -1
- package/dist/dev/elements/form-element.js.map +1 -1
- package/dist/dev/elements/meter.d.ts +10 -5
- package/dist/dev/elements/meter.js +149 -61
- package/dist/dev/elements/meter.js.map +1 -1
- package/dist/dev/elements/number.d.ts +7 -3
- package/dist/dev/elements/number.js +21 -8
- package/dist/dev/elements/number.js.map +1 -1
- package/dist/dev/elements/radio-group.d.ts +25 -0
- package/dist/dev/elements/radio-group.js +186 -0
- package/dist/dev/elements/radio-group.js.map +1 -0
- package/dist/dev/elements/readout-claim.js +82 -0
- package/dist/dev/elements/readout-claim.js.map +1 -0
- package/dist/dev/elements/readout.js +36 -0
- package/dist/dev/elements/readout.js.map +1 -0
- package/dist/dev/elements/segmented.d.ts +4 -10
- package/dist/dev/elements/segmented.js +9 -159
- package/dist/dev/elements/segmented.js.map +1 -1
- package/dist/dev/elements/slider.d.ts +6 -6
- package/dist/dev/elements/slider.js +42 -51
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/sonic-element.d.ts +4 -3
- package/dist/dev/elements/sonic-element.js +51 -22
- package/dist/dev/elements/sonic-element.js.map +1 -1
- package/dist/dev/elements/split.d.ts +21 -0
- package/dist/dev/elements/split.js +258 -0
- package/dist/dev/elements/split.js.map +1 -0
- package/dist/dev/elements/switch.d.ts +35 -0
- package/dist/dev/elements/switch.js +280 -0
- package/dist/dev/elements/switch.js.map +1 -0
- package/dist/dev/elements/value-element.d.ts +84 -0
- package/dist/dev/elements/value-element.js +692 -0
- package/dist/dev/elements/value-element.js.map +1 -0
- package/dist/dev/elements/{range-entry.js → value-entry.js} +4 -4
- package/dist/dev/elements/value-entry.js.map +1 -0
- package/dist/dev/elements/wave-element.d.ts +36 -0
- package/dist/dev/elements/wave-element.js +78 -0
- package/dist/dev/elements/wave-element.js.map +1 -0
- package/dist/dev/elements/waveform.d.ts +55 -0
- package/dist/dev/elements/waveform.js +306 -0
- package/dist/dev/elements/waveform.js.map +1 -0
- package/dist/dev/elements/wavestrip.d.ts +51 -0
- package/dist/dev/elements/wavestrip.js +183 -0
- package/dist/dev/elements/wavestrip.js.map +1 -0
- package/dist/dev/elements/xy.d.ts +62 -0
- package/dist/dev/elements/xy.js +474 -0
- package/dist/dev/elements/xy.js.map +1 -0
- package/dist/dev/index.d.ts +11 -2
- package/dist/dev/index.js +10 -2
- package/dist/dev/lib/adsr-shape.js +109 -0
- package/dist/dev/lib/adsr-shape.js.map +1 -0
- package/dist/dev/lib/ballistics.js +56 -0
- package/dist/dev/lib/ballistics.js.map +1 -0
- package/dist/dev/lib/canvas-surface.d.ts +25 -0
- package/dist/dev/lib/canvas-surface.js +193 -0
- package/dist/dev/lib/canvas-surface.js.map +1 -0
- package/dist/dev/lib/copy-node.js +32 -2
- package/dist/dev/lib/copy-node.js.map +1 -1
- package/dist/dev/lib/crossfade.d.ts +12 -0
- package/dist/dev/lib/crossfade.js +23 -0
- package/dist/dev/lib/crossfade.js.map +1 -0
- package/dist/dev/lib/distribute.d.ts +4 -0
- package/dist/dev/lib/distribute.js +216 -0
- package/dist/dev/lib/distribute.js.map +1 -0
- package/dist/dev/lib/drag-step.js +100 -0
- package/dist/dev/lib/drag-step.js.map +1 -0
- package/dist/dev/lib/envelope-curve.d.ts +4 -0
- package/dist/dev/lib/envelope-curve.js +10 -0
- package/dist/dev/lib/envelope-curve.js.map +1 -0
- package/dist/dev/lib/field.d.ts +4 -0
- package/dist/dev/lib/field.js +95 -0
- package/dist/dev/lib/field.js.map +1 -0
- package/dist/dev/lib/focus-by-pointer.js +29 -0
- package/dist/dev/lib/focus-by-pointer.js.map +1 -0
- package/dist/dev/lib/frame-timeline.js +63 -0
- package/dist/dev/lib/frame-timeline.js.map +1 -0
- package/dist/dev/lib/hold.js +39 -0
- package/dist/dev/lib/hold.js.map +1 -0
- package/dist/dev/lib/marker-band.js +37 -0
- package/dist/dev/lib/marker-band.js.map +1 -0
- package/dist/dev/lib/marker-rider.js +115 -0
- package/dist/dev/lib/marker-rider.js.map +1 -0
- package/dist/dev/lib/math.js +12 -3
- package/dist/dev/lib/math.js.map +1 -1
- package/dist/dev/lib/modifier-press.js +12 -0
- package/dist/dev/lib/modifier-press.js.map +1 -0
- package/dist/dev/lib/number-list.js +6 -6
- package/dist/dev/lib/number-list.js.map +1 -1
- package/dist/dev/lib/pointer-drag.js +44 -0
- package/dist/dev/lib/pointer-drag.js.map +1 -0
- package/dist/dev/lib/read-px-property.js.map +1 -1
- package/dist/dev/lib/render.js +20 -5
- package/dist/dev/lib/render.js.map +1 -1
- package/dist/dev/lib/resample-peaks.js +37 -0
- package/dist/dev/lib/resample-peaks.js.map +1 -0
- package/dist/dev/lib/strip-scene.js +49 -0
- package/dist/dev/lib/strip-scene.js.map +1 -0
- package/dist/dev/lib/taper.js +23 -17
- package/dist/dev/lib/taper.js.map +1 -1
- package/dist/dev/lib/time-regions.d.ts +4 -0
- package/dist/dev/lib/time-regions.js +9 -0
- package/dist/dev/lib/time-regions.js.map +1 -0
- package/dist/dev/lib/tracking-clock.js +36 -0
- package/dist/dev/lib/tracking-clock.js.map +1 -0
- package/dist/dev/lib/value-mapping.d.ts +23 -0
- package/dist/dev/lib/value-mapping.js +116 -0
- package/dist/dev/lib/value-mapping.js.map +1 -0
- package/dist/dev/lib/waveform-buckets.d.ts +9 -0
- package/dist/dev/lib/waveform-buckets.js +42 -0
- package/dist/dev/lib/waveform-buckets.js.map +1 -0
- package/dist/skins/ivory.css +1 -2
- package/dist/styles/button.css +212 -0
- package/dist/styles/controls.css +8 -1
- package/dist/styles/dial.css +88 -42
- package/dist/styles/envelope.css +159 -0
- package/dist/styles/led.css +36 -5
- package/dist/styles/material.css +277 -76
- package/dist/styles/meter.css +33 -48
- package/dist/styles/number.css +5 -1
- package/dist/styles/panel.css +59 -0
- package/dist/styles/ring.css +74 -0
- package/dist/styles/segmented.css +21 -48
- package/dist/styles/slider.css +175 -39
- package/dist/styles/switch.css +347 -0
- package/dist/styles/waveform.css +173 -0
- package/dist/styles/wavestrip.css +215 -0
- package/dist/styles/xy.css +204 -0
- package/package.json +14 -2
- package/dist/default/define/key.js +0 -7
- package/dist/default/elements/key.js +0 -169
- package/dist/default/elements/range-element.js +0 -546
- package/dist/default/lib/detent.js +0 -17
- package/dist/dev/define/key.d.ts +0 -2
- package/dist/dev/define/key.js +0 -8
- package/dist/dev/define/key.js.map +0 -1
- package/dist/dev/elements/key.js +0 -170
- package/dist/dev/elements/key.js.map +0 -1
- package/dist/dev/elements/range-element.d.ts +0 -64
- package/dist/dev/elements/range-element.js +0 -547
- package/dist/dev/elements/range-element.js.map +0 -1
- package/dist/dev/elements/range-entry.js.map +0 -1
- package/dist/dev/lib/detent.js +0 -18
- package/dist/dev/lib/detent.js.map +0 -1
- package/dist/styles/key.css +0 -242
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adsr-shape.js","names":[],"sources":["../../../src/lib/adsr-shape.ts"],"sourcesContent":["import { envelopeCurve } from '#lib/envelope-curve.ts';\nimport { roundTo } from '#lib/math.ts';\n\nexport const adsrStages = ['delay', 'attack', 'hold', 'decay', 'sustain', 'release'] as const;\n\nexport const timeStages = ['delay', 'attack', 'hold', 'decay', 'release'] as const;\n\ntype AdsrStage = (typeof adsrStages)[number];\n\ntype TimeStage = (typeof timeStages)[number];\n\nexport type AdsrProportions = Partial<Record<AdsrStage, number | undefined>>;\n\nexport type AdsrCurves = Partial<Record<TimeStage, number | undefined>>;\n\ninterface AdsrPoint {\n\tx: number;\n\ty: number;\n}\n\ninterface AdsrHandle extends AdsrPoint {\n\tstage: TimeStage;\n}\n\nexport interface AdsrShape {\n\tcurveHandles: Array<AdsrHandle>;\n\thandles: Array<AdsrHandle>;\n\tpoints: Array<AdsrPoint>;\n\tshare: number;\n}\n\nconst curveSamples = 24;\n\nfunction along(from: AdsrPoint, to: AdsrPoint, at: { curve: number; position: number }): AdsrPoint {\n\treturn {\n\t\tx: from.x + (to.x - from.x) * at.position,\n\t\ty: from.y + (to.y - from.y) * envelopeCurve(at.position, at.curve),\n\t};\n}\n\nfunction curved(from: AdsrPoint, to: AdsrPoint, curve: number): Array<AdsrPoint> {\n\tif (envelopeCurve(0.5, curve) === 0.5) return [];\n\n\treturn Array.from({ length: curveSamples - 1 }, (_, index) =>\n\t\talong(from, to, { curve, position: (index + 1) / curveSamples }),\n\t);\n}\n\nfunction leg(\n\tfrom: AdsrPoint,\n\tto: AdsrPoint,\n\tcurve: number | undefined,\n): { curveHandle: AdsrPoint | undefined; points: Array<AdsrPoint> } {\n\tif (curve === undefined) return { curveHandle: undefined, points: [to] };\n\n\treturn {\n\t\tcurveHandle: along(from, to, { curve, position: 0.5 }),\n\t\tpoints: [...curved(from, to, curve), to],\n\t};\n}\n\nfunction isHandled(proportions: AdsrProportions, stage: TimeStage): boolean {\n\tconst proportion =\n\t\tstage === 'decay' ? (proportions.sustain ?? proportions.decay) : proportions[stage];\n\n\treturn proportion !== undefined;\n}\n\nexport function adsrShape(proportions: AdsrProportions, curves: AdsrCurves = {}): AdsrShape {\n\tconst sustain = proportions.sustain ?? 1;\n\tconst levels = { attack: 1, decay: sustain, delay: 0, hold: 1, release: 0 };\n\tconst share = 1 / (timeStages.filter((stage) => proportions[stage] !== undefined).length + 1);\n\tconst shape: AdsrShape = { curveHandles: [], handles: [], points: [{ x: 0, y: 0 }], share };\n\tlet from = { x: 0, y: 0 };\n\n\tfor (const stage of timeStages) {\n\t\tif (stage === 'release') {\n\t\t\tfrom = { x: from.x + share, y: sustain };\n\t\t\tshape.points.push(from);\n\t\t}\n\n\t\tconst to = { x: from.x + (proportions[stage] ?? 0) * share, y: levels[stage] };\n\t\tconst { curveHandle, points } = leg(from, to, curves[stage]);\n\n\t\tshape.points.push(...points);\n\t\tif (curveHandle) shape.curveHandles.push({ ...curveHandle, stage });\n\t\tif (isHandled(proportions, stage)) shape.handles.push({ ...to, stage });\n\t\tfrom = to;\n\t}\n\n\treturn shape;\n}\n\nexport function adsrPath({ points }: AdsrShape): { fill: string; stroke: string } {\n\tconst stroke = points\n\t\t.map(\n\t\t\t({ x, y }, index) =>\n\t\t\t\t`${index === 0 ? 'M' : 'L'}${String(roundTo(x, 4))} ${String(roundTo(1 - y, 4))}`,\n\t\t)\n\t\t.join('');\n\n\treturn { fill: `${stroke}Z`, stroke };\n}\n"],"mappings":";;;;AAGA,MAAa,aAAa;CAAC;CAAS;CAAU;CAAQ;CAAS;CAAW;AAAS;AAEnF,MAAa,aAAa;CAAC;CAAS;CAAU;CAAQ;CAAS;AAAS;AA0BxE,MAAM,eAAe;AAErB,SAAS,MAAM,MAAiB,IAAe,IAAoD;CAClG,OAAO;EACN,GAAG,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,GAAG;EACjC,GAAG,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,cAAc,GAAG,UAAU,GAAG,KAAK;CAClE;AACD;AAEA,SAAS,OAAO,MAAiB,IAAe,OAAiC;CAChF,IAAI,cAAc,IAAK,KAAK,MAAM,IAAK,OAAO,CAAC;CAE/C,OAAO,MAAM,KAAK,EAAE,QAAQ,GAAiB,IAAI,GAAG,UACnD,MAAM,MAAM,IAAI;EAAE;EAAO,WAAW,QAAQ,KAAK;CAAa,CAAC,CAChE;AACD;AAEA,SAAS,IACR,MACA,IACA,OACmE;CACnE,IAAI,UAAU,QAAW,OAAO;EAAE,aAAa;EAAW,QAAQ,CAAC,EAAE;CAAE;CAEvE,OAAO;EACN,aAAa,MAAM,MAAM,IAAI;GAAE;GAAO,UAAU;EAAI,CAAC;EACrD,QAAQ,CAAC,GAAG,OAAO,MAAM,IAAI,KAAK,GAAG,EAAE;CACxC;AACD;AAEA,SAAS,UAAU,aAA8B,OAA2B;CAI3E,QAFC,UAAU,UAAW,YAAY,WAAW,YAAY,QAAS,YAAY,YAExD;AACvB;AAEA,SAAgB,UAAU,aAA8B,SAAqB,CAAC,GAAc;CAC3F,MAAM,UAAU,YAAY,WAAW;CACvC,MAAM,SAAS;EAAE,QAAQ;EAAG,OAAO;EAAS,OAAO;EAAG,MAAM;EAAG,SAAS;CAAE;CAC1E,MAAM,QAAQ,KAAK,WAAW,QAAQ,UAAU,YAAY,WAAW,MAAS,CAAC,CAAC,SAAS;CAC3F,MAAM,QAAmB;EAAE,cAAc,CAAC;EAAG,SAAS,CAAC;EAAG,QAAQ,CAAC;GAAE,GAAG;GAAG,GAAG;EAAE,CAAC;EAAG;CAAM;CAC1F,IAAI,OAAO;EAAE,GAAG;EAAG,GAAG;CAAE;CAExB,KAAK,MAAM,SAAS,YAAY;EAC/B,IAAI,UAAU,WAAW;GACxB,OAAO;IAAE,GAAG,KAAK,IAAI;IAAO,GAAG;GAAQ;GACvC,MAAM,OAAO,KAAK,IAAI;EACvB;EAEA,MAAM,KAAK;GAAE,GAAG,KAAK,KAAK,YAAY,UAAU,KAAK;GAAO,GAAG,OAAO;EAAO;EAC7E,MAAM,EAAE,aAAa,WAAW,IAAI,MAAM,IAAI,OAAO,MAAM;EAE3D,MAAM,OAAO,KAAK,GAAG,MAAM;EAC3B,IAAI,aAAa,MAAM,aAAa,KAAK;GAAE,GAAG;GAAa;EAAM,CAAC;EAClE,IAAI,UAAU,aAAa,KAAK,GAAG,MAAM,QAAQ,KAAK;GAAE,GAAG;GAAI;EAAM,CAAC;EACtE,OAAO;CACR;CAEA,OAAO;AACR;AAEA,SAAgB,SAAS,EAAE,UAAuD;CACjF,MAAM,SAAS,OACb,KACC,EAAE,GAAG,KAAK,UACV,GAAG,UAAU,IAAI,MAAM,MAAM,OAAO,QAAQ,GAAG,CAAC,CAAC,EAAE,GAAG,OAAO,QAAQ,IAAI,GAAG,CAAC,CAAC,GAChF,CAAC,CACA,KAAK,EAAE;CAET,OAAO;EAAE,MAAM,GAAG,OAAO;EAAI;CAAO;AACrC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/lib/ballistics.ts
|
|
2
|
+
const burstTimeConstants = -Math.log(1 - 10 ** (-2 / 20));
|
|
3
|
+
function ppmRate({ burstMs, fallDecibels, fallSeconds }) {
|
|
4
|
+
return {
|
|
5
|
+
fallDecibelsPerSecond: fallDecibels / fallSeconds,
|
|
6
|
+
riseMs: burstMs / burstTimeConstants
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
const ballisticsRates = {
|
|
10
|
+
peak: {
|
|
11
|
+
fallDecibelsPerSecond: 20,
|
|
12
|
+
riseMs: 0
|
|
13
|
+
},
|
|
14
|
+
"ppm-1": ppmRate({
|
|
15
|
+
burstMs: 5,
|
|
16
|
+
fallDecibels: 20,
|
|
17
|
+
fallSeconds: 1.7
|
|
18
|
+
}),
|
|
19
|
+
"ppm-2": ppmRate({
|
|
20
|
+
burstMs: 10,
|
|
21
|
+
fallDecibels: 24,
|
|
22
|
+
fallSeconds: 2.8
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
const needleDamping = .8;
|
|
26
|
+
const needleRadiansPerSecond = 13.5;
|
|
27
|
+
const needleStepMs = 4;
|
|
28
|
+
const needleSettleMs = 1e3;
|
|
29
|
+
function fall(current, { fallDecibelsPerSecond, floor }, elapsedMs) {
|
|
30
|
+
return Math.max(floor, current - fallDecibelsPerSecond * elapsedMs / 1e3);
|
|
31
|
+
}
|
|
32
|
+
function rise(current, { target, timeConstantMs }, elapsedMs) {
|
|
33
|
+
if (timeConstantMs <= 0) return target;
|
|
34
|
+
return target + (current - target) * Math.exp(-elapsedMs / timeConstantMs);
|
|
35
|
+
}
|
|
36
|
+
function stepNeedle(needle, target, elapsedMs) {
|
|
37
|
+
const dueMs = Math.min(needle.carryMs + elapsedMs, needleSettleMs);
|
|
38
|
+
const steps = Math.floor(dueMs / needleStepMs);
|
|
39
|
+
const seconds = needleStepMs / 1e3;
|
|
40
|
+
let { position, velocity } = needle;
|
|
41
|
+
for (let step = 0; step < steps; step += 1) {
|
|
42
|
+
const pull = needleRadiansPerSecond ** 2 * (target - position);
|
|
43
|
+
const drag = 2 * needleDamping * needleRadiansPerSecond * velocity;
|
|
44
|
+
velocity += (pull - drag) * seconds;
|
|
45
|
+
position += velocity * seconds;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
carryMs: dueMs - steps * needleStepMs,
|
|
49
|
+
position,
|
|
50
|
+
velocity
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { ballisticsRates, fall, rise, stepNeedle };
|
|
56
|
+
//# sourceMappingURL=ballistics.js.map
|
|
@@ -0,0 +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\triseMs: 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\triseMs: 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, riseMs: 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,QAAQ,UAAU;CACnB;AACD;AAGA,MAAa,kBAAkB;CAC9B,MAAM;EAAE,uBAAuB;EAAI,QAAQ;CAAE;CAC7C,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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/lib/canvas-surface.d.ts
|
|
2
|
+
export interface SurfaceSize {
|
|
3
|
+
dpr: number;
|
|
4
|
+
height: number;
|
|
5
|
+
width: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SurfaceLook<Colour extends string, Length extends string = never> {
|
|
8
|
+
colours: Record<Colour, string>;
|
|
9
|
+
isReducedMotion: boolean;
|
|
10
|
+
lengths: Record<Length, number>;
|
|
11
|
+
}
|
|
12
|
+
export interface SurfaceFrame<Colour extends string, Length extends string = never> {
|
|
13
|
+
frameMs: number;
|
|
14
|
+
isDirty: boolean;
|
|
15
|
+
look: SurfaceLook<Colour, Length>;
|
|
16
|
+
size: SurfaceSize;
|
|
17
|
+
}
|
|
18
|
+
export interface Surface {
|
|
19
|
+
invalidate: () => void;
|
|
20
|
+
rebuild: () => void;
|
|
21
|
+
requestFrame: () => void;
|
|
22
|
+
readonly size: SurfaceSize;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//# sourceMappingURL=canvas-surface.d.ts.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readPxProperty } from "./read-px-property.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/canvas-surface.ts
|
|
4
|
+
function deviceRatio() {
|
|
5
|
+
return globalThis.devicePixelRatio || 1;
|
|
6
|
+
}
|
|
7
|
+
function deviceBox(entry, css) {
|
|
8
|
+
const device = "devicePixelContentBoxSize" in entry ? entry.devicePixelContentBoxSize[0] : void 0;
|
|
9
|
+
const dpr = deviceRatio();
|
|
10
|
+
if (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return void 0;
|
|
11
|
+
return Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? void 0 : device;
|
|
12
|
+
}
|
|
13
|
+
function readEach(properties, read) {
|
|
14
|
+
return Object.fromEntries(Object.entries(properties).flatMap(([name, property]) => property === void 0 ? [] : [[name, read(property)]]));
|
|
15
|
+
}
|
|
16
|
+
var CanvasSurface = class {
|
|
17
|
+
get size() {
|
|
18
|
+
return this.#size;
|
|
19
|
+
}
|
|
20
|
+
#context;
|
|
21
|
+
#cssBox;
|
|
22
|
+
#frame;
|
|
23
|
+
#isDirty = true;
|
|
24
|
+
#isRebuildWanted = true;
|
|
25
|
+
#isVisible = true;
|
|
26
|
+
#look;
|
|
27
|
+
#options;
|
|
28
|
+
#reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
29
|
+
#size = {
|
|
30
|
+
dpr: deviceRatio(),
|
|
31
|
+
height: 0,
|
|
32
|
+
width: 0
|
|
33
|
+
};
|
|
34
|
+
constructor(options) {
|
|
35
|
+
const { canvas, signal } = options;
|
|
36
|
+
this.#options = options;
|
|
37
|
+
this.#context = canvas.getContext("2d") ?? void 0;
|
|
38
|
+
this.#armTransitions();
|
|
39
|
+
this.#observeSize();
|
|
40
|
+
this.#observeView();
|
|
41
|
+
this.#watchResolution();
|
|
42
|
+
this.#reducedMotion.addEventListener("change", () => {
|
|
43
|
+
this.rebuild();
|
|
44
|
+
}, { signal });
|
|
45
|
+
canvas.addEventListener("transitionrun", () => {
|
|
46
|
+
this.rebuild();
|
|
47
|
+
}, { signal });
|
|
48
|
+
signal.addEventListener("abort", () => {
|
|
49
|
+
if (this.#frame !== void 0) cancelAnimationFrame(this.#frame);
|
|
50
|
+
this.#frame = void 0;
|
|
51
|
+
}, { once: true });
|
|
52
|
+
this.#schedule();
|
|
53
|
+
}
|
|
54
|
+
invalidate() {
|
|
55
|
+
this.#isDirty = true;
|
|
56
|
+
this.#schedule();
|
|
57
|
+
}
|
|
58
|
+
rebuild() {
|
|
59
|
+
this.#isRebuildWanted = true;
|
|
60
|
+
this.invalidate();
|
|
61
|
+
}
|
|
62
|
+
requestFrame() {
|
|
63
|
+
this.#schedule();
|
|
64
|
+
}
|
|
65
|
+
#armTransitions() {
|
|
66
|
+
const { style } = this.#options.canvas;
|
|
67
|
+
style.setProperty("transition-property", Object.values(this.#options.colours).join(", "));
|
|
68
|
+
style.setProperty("transition-duration", "0.001ms");
|
|
69
|
+
style.setProperty("transition-timing-function", "step-start");
|
|
70
|
+
style.setProperty("transition-behavior", "allow-discrete");
|
|
71
|
+
}
|
|
72
|
+
#draw(frameMs) {
|
|
73
|
+
this.#frame = void 0;
|
|
74
|
+
const size = this.#size;
|
|
75
|
+
if (size.width === 0 || size.height === 0) return;
|
|
76
|
+
const { canvas, paint } = this.#options;
|
|
77
|
+
const look = this.#isRebuildWanted || !this.#look ? this.#read() : this.#look;
|
|
78
|
+
const isDirty = this.#isDirty;
|
|
79
|
+
this.#look = look;
|
|
80
|
+
this.#isDirty = false;
|
|
81
|
+
this.#isRebuildWanted = false;
|
|
82
|
+
if (canvas.width !== size.width) canvas.width = size.width;
|
|
83
|
+
if (canvas.height !== size.height) canvas.height = size.height;
|
|
84
|
+
if (this.#context) paint(this.#context, {
|
|
85
|
+
frameMs,
|
|
86
|
+
isDirty,
|
|
87
|
+
look,
|
|
88
|
+
size
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
#measure(entry) {
|
|
92
|
+
const [box] = entry.contentBoxSize;
|
|
93
|
+
if (!box) return;
|
|
94
|
+
const device = deviceBox(entry, box);
|
|
95
|
+
if (device) {
|
|
96
|
+
this.#cssBox = void 0;
|
|
97
|
+
this.#resize(device.inlineSize, device.blockSize);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.#cssBox = {
|
|
101
|
+
height: box.blockSize,
|
|
102
|
+
width: box.inlineSize
|
|
103
|
+
};
|
|
104
|
+
this.#rescale();
|
|
105
|
+
}
|
|
106
|
+
#observeSize() {
|
|
107
|
+
const { canvas, signal } = this.#options;
|
|
108
|
+
const observer = new ResizeObserver((entries) => {
|
|
109
|
+
const entry = entries.at(-1);
|
|
110
|
+
if (entry) this.#measure(entry);
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
observer.observe(canvas, { box: "device-pixel-content-box" });
|
|
114
|
+
} catch {
|
|
115
|
+
observer.observe(canvas, { box: "content-box" });
|
|
116
|
+
}
|
|
117
|
+
signal.addEventListener("abort", () => {
|
|
118
|
+
observer.disconnect();
|
|
119
|
+
}, { once: true });
|
|
120
|
+
}
|
|
121
|
+
#observeView() {
|
|
122
|
+
const { canvas, signal } = this.#options;
|
|
123
|
+
const observer = new IntersectionObserver((entries) => {
|
|
124
|
+
const entry = entries.at(-1);
|
|
125
|
+
if (!entry) return;
|
|
126
|
+
const wasVisible = this.#isVisible;
|
|
127
|
+
this.#isVisible = entry.isIntersecting;
|
|
128
|
+
if (!entry.isIntersecting && this.#frame !== void 0) {
|
|
129
|
+
cancelAnimationFrame(this.#frame);
|
|
130
|
+
this.#frame = void 0;
|
|
131
|
+
}
|
|
132
|
+
if (!wasVisible && entry.isIntersecting) this.rebuild();
|
|
133
|
+
});
|
|
134
|
+
observer.observe(canvas);
|
|
135
|
+
signal.addEventListener("abort", () => {
|
|
136
|
+
observer.disconnect();
|
|
137
|
+
}, { once: true });
|
|
138
|
+
}
|
|
139
|
+
#read() {
|
|
140
|
+
const { canvas, colours, lengths = {} } = this.#options;
|
|
141
|
+
const styles = getComputedStyle(canvas);
|
|
142
|
+
return {
|
|
143
|
+
colours: readEach(colours, (property) => styles.getPropertyValue(property).trim()),
|
|
144
|
+
isReducedMotion: this.#reducedMotion.matches,
|
|
145
|
+
lengths: readEach(lengths, (property) => readPxProperty(styles, property, 0))
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
#rescale() {
|
|
149
|
+
const box = this.#cssBox;
|
|
150
|
+
if (!box) {
|
|
151
|
+
this.#resize(this.#size.width, this.#size.height);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const dpr = deviceRatio();
|
|
155
|
+
this.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));
|
|
156
|
+
}
|
|
157
|
+
#resize(width, height) {
|
|
158
|
+
const previous = this.#size;
|
|
159
|
+
const dpr = deviceRatio();
|
|
160
|
+
if (previous.width === width && previous.height === height && previous.dpr === dpr) return;
|
|
161
|
+
this.#size = {
|
|
162
|
+
dpr,
|
|
163
|
+
height,
|
|
164
|
+
width
|
|
165
|
+
};
|
|
166
|
+
this.#options.resize?.(this.#size);
|
|
167
|
+
if (previous.width === 0 || previous.height === 0) this.rebuild();
|
|
168
|
+
else this.invalidate();
|
|
169
|
+
}
|
|
170
|
+
#schedule() {
|
|
171
|
+
if (this.#options.signal.aborted || this.#frame !== void 0 || !this.#isVisible) return;
|
|
172
|
+
this.#frame = requestAnimationFrame((frameMs) => {
|
|
173
|
+
this.#draw(frameMs);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
#watchResolution() {
|
|
177
|
+
const { signal } = this.#options;
|
|
178
|
+
matchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener("change", () => {
|
|
179
|
+
this.#watchResolution();
|
|
180
|
+
this.#rescale();
|
|
181
|
+
}, {
|
|
182
|
+
once: true,
|
|
183
|
+
signal
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function bindSurface(options) {
|
|
188
|
+
return new CanvasSurface(options);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
export { bindSurface };
|
|
193
|
+
//# sourceMappingURL=canvas-surface.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -2,14 +2,44 @@
|
|
|
2
2
|
const copyPrefix = "sonic-copy-";
|
|
3
3
|
const idListAttributes = /* @__PURE__ */ new Set(["aria-describedby", "aria-labelledby"]);
|
|
4
4
|
const hrefAttributes = /* @__PURE__ */ new Set(["href", "xlink:href"]);
|
|
5
|
-
const
|
|
5
|
+
const timingAttributes = /* @__PURE__ */ new Set(["begin", "end"]);
|
|
6
|
+
const urlAttributes = /* @__PURE__ */ new Set([
|
|
7
|
+
"clip-path",
|
|
8
|
+
"color-profile",
|
|
9
|
+
"fill",
|
|
10
|
+
"filter",
|
|
11
|
+
"marker-end",
|
|
12
|
+
"marker-mid",
|
|
13
|
+
"marker-start",
|
|
14
|
+
"mask",
|
|
15
|
+
"stroke",
|
|
16
|
+
"style"
|
|
17
|
+
]);
|
|
18
|
+
const urlReference = /url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/gi;
|
|
19
|
+
const idSelector = /#([\w-]+)(?=[^;{}]*\{)/g;
|
|
20
|
+
const timingReference = /(^|;\s*)([^\s;.]+)(?=\.[a-z])/gi;
|
|
21
|
+
function decoded(id) {
|
|
22
|
+
try {
|
|
23
|
+
return decodeURIComponent(id);
|
|
24
|
+
} catch {
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function rewriteUrls(value, ids) {
|
|
29
|
+
return value.replaceAll(urlReference, (reference, quote, id) => ids.has(decoded(id)) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference);
|
|
30
|
+
}
|
|
6
31
|
function rewriteReferences(name, value, ids) {
|
|
7
32
|
if (hrefAttributes.has(name)) return value.startsWith("#") && ids.has(value.slice(1)) ? `#${copyPrefix}${value.slice(1)}` : value;
|
|
8
33
|
if (idListAttributes.has(name)) return value.split(/\s+/).map((id) => ids.has(id) ? copyPrefix + id : id).join(" ");
|
|
9
|
-
return value.replaceAll(
|
|
34
|
+
if (timingAttributes.has(name)) return value.replaceAll(timingReference, (reference, lead, id) => ids.has(id) ? lead + copyPrefix + id : reference);
|
|
35
|
+
return urlAttributes.has(name) ? rewriteUrls(value, ids) : value;
|
|
36
|
+
}
|
|
37
|
+
function rewriteStyle(text, ids) {
|
|
38
|
+
return rewriteUrls(text, ids).replaceAll(idSelector, (selector, id) => ids.has(id) ? `#${copyPrefix}${id}` : selector);
|
|
10
39
|
}
|
|
11
40
|
function prefixIds(element, ids) {
|
|
12
41
|
if (element.id !== "") element.id = copyPrefix + element.id;
|
|
42
|
+
if (element.localName === "style") element.textContent = rewriteStyle(element.textContent, ids);
|
|
13
43
|
for (const attribute of element.attributes) {
|
|
14
44
|
if (attribute.name === "id") continue;
|
|
15
45
|
const value = rewriteReferences(attribute.name, attribute.value, ids);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copy-node.js","names":[],"sources":["../../../src/lib/copy-node.ts"],"sourcesContent":["const copyPrefix = 'sonic-copy-';\n\nconst idListAttributes = new Set(['aria-describedby', 'aria-labelledby']);\n\nconst hrefAttributes = new Set(['href', 'xlink:href']);\n\nconst urlReference = /url\\(\\s*(['\"]?)#([^'\")\\s]+)\\1\\s*\\)/g;\n\nfunction rewriteReferences(name: string, value: string, ids: Set<string>): string {\n\tif (hrefAttributes.has(name)) {\n\t\treturn value.startsWith('#') && ids.has(value.slice(1))\n\t\t\t? `#${copyPrefix}${value.slice(1)}`\n\t\t\t: value;\n\t}\n\tif (idListAttributes.has(name)) {\n\t\treturn value\n\t\t\t.split(/\\s+/)\n\t\t\t.map((id) => (ids.has(id) ? copyPrefix + id : id))\n\t\t\t.join(' ');\n\t}\n\n\treturn value.replaceAll(
|
|
1
|
+
{"version":3,"file":"copy-node.js","names":[],"sources":["../../../src/lib/copy-node.ts"],"sourcesContent":["const copyPrefix = 'sonic-copy-';\n\nconst idListAttributes = new Set(['aria-describedby', 'aria-labelledby']);\n\nconst hrefAttributes = new Set(['href', 'xlink:href']);\n\nconst timingAttributes = new Set(['begin', 'end']);\n\nconst urlAttributes = new Set([\n\t'clip-path',\n\t'color-profile',\n\t'fill',\n\t'filter',\n\t'marker-end',\n\t'marker-mid',\n\t'marker-start',\n\t'mask',\n\t'stroke',\n\t'style',\n]);\n\nconst urlReference = /url\\(\\s*(['\"]?)#([^'\")\\s]+)\\1\\s*\\)/gi;\n\n// A `#name` with a block still to open is a selector; inside a block it is a colour\nconst idSelector = /#([\\w-]+)(?=[^;{}]*\\{)/g;\n\n// SMIL: an id leads a `;` list entry, before `.event`; `2.5s` is a clock value\nconst timingReference = /(^|;\\s*)([^\\s;.]+)(?=\\.[a-z])/gi;\n\nfunction decoded(id: string): string {\n\ttry {\n\t\treturn decodeURIComponent(id);\n\t} catch {\n\t\treturn id;\n\t}\n}\n\nfunction rewriteUrls(value: string, ids: Set<string>): string {\n\treturn value.replaceAll(urlReference, (reference, quote: string, id: string) =>\n\t\tids.has(decoded(id)) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference,\n\t);\n}\n\nfunction rewriteReferences(name: string, value: string, ids: Set<string>): string {\n\tif (hrefAttributes.has(name)) {\n\t\treturn value.startsWith('#') && ids.has(value.slice(1))\n\t\t\t? `#${copyPrefix}${value.slice(1)}`\n\t\t\t: value;\n\t}\n\tif (idListAttributes.has(name)) {\n\t\treturn value\n\t\t\t.split(/\\s+/)\n\t\t\t.map((id) => (ids.has(id) ? copyPrefix + id : id))\n\t\t\t.join(' ');\n\t}\n\tif (timingAttributes.has(name)) {\n\t\treturn value.replaceAll(timingReference, (reference, lead: string, id: string) =>\n\t\t\tids.has(id) ? lead + copyPrefix + id : reference,\n\t\t);\n\t}\n\n\treturn urlAttributes.has(name) ? rewriteUrls(value, ids) : value;\n}\n\nfunction rewriteStyle(text: string, ids: Set<string>): string {\n\treturn rewriteUrls(text, ids).replaceAll(idSelector, (selector, id: string) =>\n\t\tids.has(id) ? `#${copyPrefix}${id}` : selector,\n\t);\n}\n\nfunction prefixIds(element: Element, ids: Set<string>): void {\n\tif (element.id !== '') element.id = copyPrefix + element.id;\n\tif (element.localName === 'style') element.textContent = rewriteStyle(element.textContent, ids);\n\tfor (const attribute of element.attributes) {\n\t\tif (attribute.name === 'id') continue;\n\n\t\tconst value = rewriteReferences(attribute.name, attribute.value, ids);\n\t\tif (value !== attribute.value) attribute.value = value;\n\t}\n}\n\n// The original stays in the document unrendered, and `url(#id)` in a copy would resolve to its `<defs>`\nexport function copyNode(node: Node): Node {\n\tconst copy = node.cloneNode(true);\n\tif (!(copy instanceof Element)) return copy;\n\n\tconst elements = [copy, ...copy.querySelectorAll('*')];\n\tconst ids = new Set(elements.map((element) => element.id).filter((id) => id !== ''));\n\tif (ids.size === 0) return copy;\n\n\tfor (const element of elements) prefixIds(element, ids);\n\n\treturn copy;\n}\n"],"mappings":";AAAA,MAAM,aAAa;AAEnB,MAAM,mCAAmB,IAAI,IAAI,CAAC,oBAAoB,iBAAiB,CAAC;AAExE,MAAM,iCAAiB,IAAI,IAAI,CAAC,QAAQ,YAAY,CAAC;AAErD,MAAM,mCAAmB,IAAI,IAAI,CAAC,SAAS,KAAK,CAAC;AAEjD,MAAM,gCAAgB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,CAAC;AAED,MAAM,eAAe;AAGrB,MAAM,aAAa;AAGnB,MAAM,kBAAkB;AAExB,SAAS,QAAQ,IAAoB;CACpC,IAAI;EACH,OAAO,mBAAmB,EAAE;CAC7B,QAAQ;EACP,OAAO;CACR;AACD;AAEA,SAAS,YAAY,OAAe,KAA0B;CAC7D,OAAO,MAAM,WAAW,eAAe,WAAW,OAAe,OAChE,IAAI,IAAI,QAAQ,EAAE,CAAC,IAAI,OAAO,MAAM,GAAG,aAAa,KAAK,MAAM,KAAK,SACrE;AACD;AAEA,SAAS,kBAAkB,MAAc,OAAe,KAA0B;CACjF,IAAI,eAAe,IAAI,IAAI,GAC1B,OAAO,MAAM,WAAW,GAAG,KAAK,IAAI,IAAI,MAAM,MAAM,CAAC,CAAC,IACnD,IAAI,aAAa,MAAM,MAAM,CAAC,MAC9B;CAEJ,IAAI,iBAAiB,IAAI,IAAI,GAC5B,OAAO,MACL,MAAM,KAAK,CAAC,CACZ,KAAK,OAAQ,IAAI,IAAI,EAAE,IAAI,aAAa,KAAK,EAAG,CAAC,CACjD,KAAK,GAAG;CAEX,IAAI,iBAAiB,IAAI,IAAI,GAC5B,OAAO,MAAM,WAAW,kBAAkB,WAAW,MAAc,OAClE,IAAI,IAAI,EAAE,IAAI,OAAO,aAAa,KAAK,SACxC;CAGD,OAAO,cAAc,IAAI,IAAI,IAAI,YAAY,OAAO,GAAG,IAAI;AAC5D;AAEA,SAAS,aAAa,MAAc,KAA0B;CAC7D,OAAO,YAAY,MAAM,GAAG,CAAC,CAAC,WAAW,aAAa,UAAU,OAC/D,IAAI,IAAI,EAAE,IAAI,IAAI,aAAa,OAAO,QACvC;AACD;AAEA,SAAS,UAAU,SAAkB,KAAwB;CAC5D,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK,aAAa,QAAQ;CACzD,IAAI,QAAQ,cAAc,SAAS,QAAQ,cAAc,aAAa,QAAQ,aAAa,GAAG;CAC9F,KAAK,MAAM,aAAa,QAAQ,YAAY;EAC3C,IAAI,UAAU,SAAS,MAAM;EAE7B,MAAM,QAAQ,kBAAkB,UAAU,MAAM,UAAU,OAAO,GAAG;EACpE,IAAI,UAAU,UAAU,OAAO,UAAU,QAAQ;CAClD;AACD;AAGA,SAAgB,SAAS,MAAkB;CAC1C,MAAM,OAAO,KAAK,UAAU,IAAI;CAChC,IAAI,EAAE,gBAAgB,UAAU,OAAO;CAEvC,MAAM,WAAW,CAAC,MAAM,GAAG,KAAK,iBAAiB,GAAG,CAAC;CACrD,MAAM,MAAM,IAAI,IAAI,SAAS,KAAK,YAAY,QAAQ,EAAE,CAAC,CAAC,QAAQ,OAAO,OAAO,EAAE,CAAC;CACnF,IAAI,IAAI,SAAS,GAAG,OAAO;CAE3B,KAAK,MAAM,WAAW,UAAU,UAAU,SAAS,GAAG;CAEtD,OAAO;AACR"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/lib/crossfade.d.ts
|
|
2
|
+
interface CrossfadeOptions {
|
|
3
|
+
law?: 'linear' | 'power';
|
|
4
|
+
sharpness?: number;
|
|
5
|
+
}
|
|
6
|
+
interface CrossfadeGains {
|
|
7
|
+
a: number;
|
|
8
|
+
b: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function crossfadeGains(position: number, options?: CrossfadeOptions): CrossfadeGains;
|
|
11
|
+
//#endregion
|
|
12
|
+
//# sourceMappingURL=crossfade.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { clampProportion } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/crossfade.ts
|
|
4
|
+
function progress(side, span) {
|
|
5
|
+
if (span === 0) return side > 0 ? 1 : 0;
|
|
6
|
+
return Math.min(1, side / span);
|
|
7
|
+
}
|
|
8
|
+
function crossfadeGains(position, options = {}) {
|
|
9
|
+
const at = clampProportion(position);
|
|
10
|
+
const span = 1 - clampProportion(options.sharpness ?? 0);
|
|
11
|
+
const gain = (side) => {
|
|
12
|
+
const reached = progress(side, span);
|
|
13
|
+
return options.law === "linear" ? reached : Math.sin(reached * Math.PI / 2);
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
a: gain(1 - at),
|
|
17
|
+
b: gain(at)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { crossfadeGains };
|
|
23
|
+
//# sourceMappingURL=crossfade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossfade.js","names":[],"sources":["../../../src/lib/crossfade.ts"],"sourcesContent":["import { clampProportion } from '#lib/math.ts';\n\ninterface CrossfadeOptions {\n\tlaw?: 'linear' | 'power';\n\tsharpness?: number;\n}\n\ninterface CrossfadeGains {\n\ta: number;\n\tb: number;\n}\n\nfunction progress(side: number, span: number): number {\n\tif (span === 0) return side > 0 ? 1 : 0;\n\n\treturn Math.min(1, side / span);\n}\n\nexport function crossfadeGains(position: number, options: CrossfadeOptions = {}): CrossfadeGains {\n\tconst at = clampProportion(position);\n\tconst span = 1 - clampProportion(options.sharpness ?? 0);\n\tconst gain = (side: number): number => {\n\t\tconst reached = progress(side, span);\n\n\t\treturn options.law === 'linear' ? reached : Math.sin((reached * Math.PI) / 2);\n\t};\n\n\treturn { a: gain(1 - at), b: gain(at) };\n}\n"],"mappings":";;;AAYA,SAAS,SAAS,MAAc,MAAsB;CACrD,IAAI,SAAS,GAAG,OAAO,OAAO,IAAI,IAAI;CAEtC,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI;AAC/B;AAEA,SAAgB,eAAe,UAAkB,UAA4B,CAAC,GAAmB;CAChG,MAAM,KAAK,gBAAgB,QAAQ;CACnC,MAAM,OAAO,IAAI,gBAAgB,QAAQ,aAAa,CAAC;CACvD,MAAM,QAAQ,SAAyB;EACtC,MAAM,UAAU,SAAS,MAAM,IAAI;EAEnC,OAAO,QAAQ,QAAQ,WAAW,UAAU,KAAK,IAAK,UAAU,KAAK,KAAM,CAAC;CAC7E;CAEA,OAAO;EAAE,GAAG,KAAK,IAAI,EAAE;EAAG,GAAG,KAAK,EAAE;CAAE;AACvC"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { clamp, trimFloat } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/distribute.ts
|
|
4
|
+
const epsilon = 1e-9;
|
|
5
|
+
function sumOf(values) {
|
|
6
|
+
return values.reduce((total, value) => total + value, 0);
|
|
7
|
+
}
|
|
8
|
+
function others(members, index) {
|
|
9
|
+
return members.filter((_member, at) => at !== index);
|
|
10
|
+
}
|
|
11
|
+
function splitLimits(members, index, total) {
|
|
12
|
+
const member = members[index];
|
|
13
|
+
if (!member) return [NaN, NaN];
|
|
14
|
+
const rest = others(members, index);
|
|
15
|
+
const free = rest.filter((other) => other.isFree);
|
|
16
|
+
if (free.length === 0) return [member.value, member.value];
|
|
17
|
+
const room = total - sumOf(rest.filter((other) => !other.isFree).map((other) => other.value));
|
|
18
|
+
return [Math.max(member.min, room - sumOf(free.map((other) => other.max))), Math.min(member.max, room - sumOf(free.map((other) => other.min)))];
|
|
19
|
+
}
|
|
20
|
+
function spreadWeighted(owed, pool) {
|
|
21
|
+
const shares = /* @__PURE__ */ new Map();
|
|
22
|
+
let remaining = owed;
|
|
23
|
+
let active = [...pool];
|
|
24
|
+
while (active.length > 0 && Math.abs(remaining) > epsilon) {
|
|
25
|
+
const weights = sumOf(active.map(({ weight }) => weight));
|
|
26
|
+
const count = active.length;
|
|
27
|
+
const shareOf = (entry) => remaining * (weights > 0 ? entry.weight / weights : 1 / count);
|
|
28
|
+
const full = active.filter((entry) => Math.abs(shareOf(entry)) >= entry.room);
|
|
29
|
+
if (full.length === 0) {
|
|
30
|
+
for (const entry of active) shares.set(entry.index, shareOf(entry));
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
const sign = Math.sign(remaining);
|
|
34
|
+
for (const entry of full) {
|
|
35
|
+
shares.set(entry.index, sign * entry.room);
|
|
36
|
+
remaining -= sign * entry.room;
|
|
37
|
+
}
|
|
38
|
+
active = active.filter((entry) => !full.includes(entry));
|
|
39
|
+
}
|
|
40
|
+
return shares;
|
|
41
|
+
}
|
|
42
|
+
function spreadInOrder(owed, pool) {
|
|
43
|
+
const shares = /* @__PURE__ */ new Map();
|
|
44
|
+
let remaining = owed;
|
|
45
|
+
for (const entry of pool) {
|
|
46
|
+
if (Math.abs(remaining) <= epsilon) break;
|
|
47
|
+
const share = Math.sign(remaining) * Math.min(entry.room, Math.abs(remaining));
|
|
48
|
+
shares.set(entry.index, share);
|
|
49
|
+
remaining -= share;
|
|
50
|
+
}
|
|
51
|
+
return shares;
|
|
52
|
+
}
|
|
53
|
+
function poolOrder(members, index) {
|
|
54
|
+
const count = members.length;
|
|
55
|
+
const start = index + 1;
|
|
56
|
+
return Array.from({ length: count }, (_entry, offset) => (start + offset) % count).filter((at) => at !== index && members[at]?.isFree === true);
|
|
57
|
+
}
|
|
58
|
+
function stepWithin(member, from, owed) {
|
|
59
|
+
const next = member.stepFrom(from, owed > 0 ? 1 : -1);
|
|
60
|
+
const change = trimFloat(next - from);
|
|
61
|
+
const isInside = next >= member.min && next <= member.max;
|
|
62
|
+
return change !== 0 && isInside && Math.abs(change) <= Math.abs(owed) + epsilon ? next : void 0;
|
|
63
|
+
}
|
|
64
|
+
function stepToward(member, from, owed) {
|
|
65
|
+
let value = from;
|
|
66
|
+
let remainder = owed;
|
|
67
|
+
while (Math.abs(remainder) > epsilon) {
|
|
68
|
+
const next = stepWithin(member, value, remainder);
|
|
69
|
+
if (next === void 0) break;
|
|
70
|
+
remainder = trimFloat(remainder - (next - value));
|
|
71
|
+
value = next;
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function byShortfall(values, { exact, order }, direction) {
|
|
76
|
+
const shortfall = (index) => {
|
|
77
|
+
const value = values[index] ?? NaN;
|
|
78
|
+
return trimFloat(direction * ((exact.get(index) ?? value) - value));
|
|
79
|
+
};
|
|
80
|
+
return order.toReversed().sort((first, second) => shortfall(second) - shortfall(first));
|
|
81
|
+
}
|
|
82
|
+
function stepEach(members, values, pass) {
|
|
83
|
+
let remainder = pass.owed;
|
|
84
|
+
for (const index of pass.neediest) {
|
|
85
|
+
const member = members[index];
|
|
86
|
+
const value = values[index];
|
|
87
|
+
const next = member && value !== void 0 ? stepWithin(member, value, remainder) : void 0;
|
|
88
|
+
if (value === void 0 || next === void 0) continue;
|
|
89
|
+
values[index] = next;
|
|
90
|
+
remainder = trimFloat(remainder - (next - value));
|
|
91
|
+
}
|
|
92
|
+
return remainder;
|
|
93
|
+
}
|
|
94
|
+
function settle(members, values, settling) {
|
|
95
|
+
let remainder = trimFloat(settling.total - sumOf(values));
|
|
96
|
+
let before = NaN;
|
|
97
|
+
while (remainder !== before && Math.abs(remainder) > epsilon) {
|
|
98
|
+
before = remainder;
|
|
99
|
+
remainder = stepEach(members, values, {
|
|
100
|
+
neediest: byShortfall(values, settling, Math.sign(remainder)),
|
|
101
|
+
owed: remainder
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return remainder;
|
|
105
|
+
}
|
|
106
|
+
function pooled(members, order, owed) {
|
|
107
|
+
return order.flatMap((at) => {
|
|
108
|
+
const member = members[at];
|
|
109
|
+
if (!member) return [];
|
|
110
|
+
const room = owed > 0 ? member.max - member.value : member.value - member.min;
|
|
111
|
+
return [{
|
|
112
|
+
index: at,
|
|
113
|
+
room: Math.max(0, room),
|
|
114
|
+
weight: member.value - member.min
|
|
115
|
+
}];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function shares(pool, owed, mode) {
|
|
119
|
+
if (mode === "cascade") return spreadInOrder(owed, pool);
|
|
120
|
+
if (mode === "equal") return spreadWeighted(owed, pool.map((entry) => ({
|
|
121
|
+
...entry,
|
|
122
|
+
weight: 1
|
|
123
|
+
})));
|
|
124
|
+
return spreadWeighted(owed, pool);
|
|
125
|
+
}
|
|
126
|
+
function landMover(members, values, move) {
|
|
127
|
+
const { index, target, total } = move;
|
|
128
|
+
const mover = members[index];
|
|
129
|
+
if (!mover?.isFree || !Number.isFinite(target)) return void 0;
|
|
130
|
+
const next = mover.snap(clamp(target, ...splitLimits(members, index, total)));
|
|
131
|
+
values[index] = next;
|
|
132
|
+
return mover.value - next;
|
|
133
|
+
}
|
|
134
|
+
function landShares(members, values, spread) {
|
|
135
|
+
const exact = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const [at, share] of spread) {
|
|
137
|
+
const member = members[at];
|
|
138
|
+
if (!member) continue;
|
|
139
|
+
exact.set(at, member.value + share);
|
|
140
|
+
values[at] = clamp(member.snap(member.value + share), member.min, member.max);
|
|
141
|
+
}
|
|
142
|
+
return exact;
|
|
143
|
+
}
|
|
144
|
+
function spread(members, rule, move) {
|
|
145
|
+
const values = members.map(({ value }) => value);
|
|
146
|
+
const owed = move ? landMover(members, values, {
|
|
147
|
+
...move,
|
|
148
|
+
total: rule.total
|
|
149
|
+
}) : rule.total - sumOf(values);
|
|
150
|
+
if (owed === void 0) return void 0;
|
|
151
|
+
const order = poolOrder(members, move?.index ?? -1);
|
|
152
|
+
return {
|
|
153
|
+
left: settle(members, values, {
|
|
154
|
+
exact: landShares(members, values, shares(pooled(members, order, owed), owed, rule.mode)),
|
|
155
|
+
order,
|
|
156
|
+
total: rule.total
|
|
157
|
+
}),
|
|
158
|
+
order,
|
|
159
|
+
values
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function smallestStep(members, { left, order, values }) {
|
|
163
|
+
return order.flatMap((at) => {
|
|
164
|
+
const member = members[at];
|
|
165
|
+
const value = values[at];
|
|
166
|
+
if (!member || value === void 0) return [];
|
|
167
|
+
const next = member.stepFrom(value, left > 0 ? 1 : -1);
|
|
168
|
+
return next >= member.min && next <= member.max && next !== value ? [trimFloat(next - value)] : [];
|
|
169
|
+
}).toSorted((one, other) => Math.abs(one) - Math.abs(other))[0];
|
|
170
|
+
}
|
|
171
|
+
function moverTargets(members, landing, index) {
|
|
172
|
+
const mover = members[index];
|
|
173
|
+
const landed = landing.values[index];
|
|
174
|
+
if (!mover || landed === void 0) return [];
|
|
175
|
+
const step = smallestStep(members, landing);
|
|
176
|
+
const held = stepToward(mover, landed, landing.left);
|
|
177
|
+
return step === void 0 ? [held] : [held, stepToward(mover, landed, trimFloat(landing.left - step))];
|
|
178
|
+
}
|
|
179
|
+
function nearest(candidates, move, from) {
|
|
180
|
+
const { direction = 0, index } = move;
|
|
181
|
+
const moverOf = (candidate) => candidate[index] ?? NaN;
|
|
182
|
+
const [first, ...rest] = candidates.toSorted((one, other) => Math.abs(moverOf(one) - from.landed) - Math.abs(moverOf(other) - from.landed));
|
|
183
|
+
if (!first || direction === 0 || moverOf(first) !== from.start) return first;
|
|
184
|
+
return rest.find((candidate) => Math.sign(moverOf(candidate) - from.start) === direction) ?? first;
|
|
185
|
+
}
|
|
186
|
+
function close(members, rule, held) {
|
|
187
|
+
const { landing, move, mover } = held;
|
|
188
|
+
const { index } = move;
|
|
189
|
+
const landed = landing.values[index] ?? mover.value;
|
|
190
|
+
const targets = moverTargets(members, landing, index);
|
|
191
|
+
return nearest(targets.flatMap((target) => {
|
|
192
|
+
const attempt = spread(members, rule, {
|
|
193
|
+
index,
|
|
194
|
+
target
|
|
195
|
+
});
|
|
196
|
+
return attempt && Math.abs(attempt.left) <= epsilon ? [attempt.values] : [];
|
|
197
|
+
}), move, {
|
|
198
|
+
landed,
|
|
199
|
+
start: mover.value
|
|
200
|
+
}) ?? landing.values.with(index, targets[0] ?? landed);
|
|
201
|
+
}
|
|
202
|
+
function distribute(members, rule, move) {
|
|
203
|
+
const landing = spread(members, rule, move);
|
|
204
|
+
if (!landing) return members.map(({ value }) => value);
|
|
205
|
+
const mover = members[move?.index ?? -1];
|
|
206
|
+
if (!move || !mover || Math.abs(landing.left) <= epsilon) return landing.values;
|
|
207
|
+
return close(members, rule, {
|
|
208
|
+
landing,
|
|
209
|
+
move,
|
|
210
|
+
mover
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
export { distribute, splitLimits };
|
|
216
|
+
//# sourceMappingURL=distribute.js.map
|