@xsynaptic/sonic-ui 0.3.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.
Files changed (237) hide show
  1. package/README.md +2 -21
  2. package/dist/default/dat.js +50 -0
  3. package/dist/default/define/button.js +7 -0
  4. package/dist/default/define/envelope.js +7 -0
  5. package/dist/default/define/split.js +7 -0
  6. package/dist/default/define/switch.js +7 -0
  7. package/dist/default/define/waveform.js +7 -0
  8. package/dist/default/define/wavestrip.js +7 -0
  9. package/dist/default/define/xy.js +7 -0
  10. package/dist/default/define.js +7 -1
  11. package/dist/default/elements/button.js +165 -0
  12. package/dist/default/elements/dial.js +19 -23
  13. package/dist/default/elements/envelope.js +367 -0
  14. package/dist/default/elements/meter.js +168 -26
  15. package/dist/default/elements/number.js +21 -8
  16. package/dist/default/elements/radio-group.js +185 -0
  17. package/dist/default/elements/readout-claim.js +81 -0
  18. package/dist/default/elements/readout.js +35 -0
  19. package/dist/default/elements/segmented.js +9 -159
  20. package/dist/default/elements/slider.js +64 -27
  21. package/dist/default/elements/sonic-element.js +24 -13
  22. package/dist/default/elements/split.js +249 -0
  23. package/dist/default/elements/switch.js +279 -0
  24. package/dist/default/elements/value-element.js +691 -0
  25. package/dist/default/elements/{range-entry.js → value-entry.js} +3 -3
  26. package/dist/default/elements/wave-element.js +77 -0
  27. package/dist/default/elements/waveform.js +305 -0
  28. package/dist/default/elements/wavestrip.js +182 -0
  29. package/dist/default/elements/xy.js +473 -0
  30. package/dist/default/index.js +10 -2
  31. package/dist/default/lib/adsr-shape.js +108 -0
  32. package/dist/default/lib/ballistics.js +55 -0
  33. package/dist/default/lib/canvas-surface.js +192 -0
  34. package/dist/default/lib/copy-node.js +32 -2
  35. package/dist/default/lib/crossfade.js +22 -0
  36. package/dist/default/lib/distribute.js +215 -0
  37. package/dist/default/lib/drag-step.js +99 -0
  38. package/dist/default/lib/envelope-curve.js +9 -0
  39. package/dist/default/lib/field.js +94 -0
  40. package/dist/default/lib/focus-by-pointer.js +28 -0
  41. package/dist/default/lib/frame-timeline.js +62 -0
  42. package/dist/default/lib/hold.js +38 -0
  43. package/dist/default/lib/marker-band.js +36 -0
  44. package/dist/default/lib/marker-rider.js +114 -0
  45. package/dist/default/lib/math.js +25 -0
  46. package/dist/default/lib/modifier-press.js +11 -0
  47. package/dist/default/lib/number-list.js +14 -0
  48. package/dist/default/lib/pointer-drag.js +43 -0
  49. package/dist/default/lib/render.js +20 -5
  50. package/dist/default/lib/resample-peaks.js +36 -0
  51. package/dist/default/lib/strip-scene.js +48 -0
  52. package/dist/default/lib/taper.js +32 -16
  53. package/dist/default/lib/time-regions.js +8 -0
  54. package/dist/default/lib/tracking-clock.js +35 -0
  55. package/dist/default/lib/value-mapping.js +115 -0
  56. package/dist/default/lib/waveform-buckets.js +41 -0
  57. package/dist/dev/dat.d.ts +19 -0
  58. package/dist/dev/dat.js +51 -0
  59. package/dist/dev/dat.js.map +1 -0
  60. package/dist/dev/define/button.d.ts +2 -0
  61. package/dist/dev/define/button.js +8 -0
  62. package/dist/dev/define/button.js.map +1 -0
  63. package/dist/dev/define/dial.js.map +1 -1
  64. package/dist/dev/define/envelope.d.ts +2 -0
  65. package/dist/dev/define/envelope.js +8 -0
  66. package/dist/dev/define/envelope.js.map +1 -0
  67. package/dist/dev/define/meter.js.map +1 -1
  68. package/dist/dev/define/number.js.map +1 -1
  69. package/dist/dev/define/segmented.js.map +1 -1
  70. package/dist/dev/define/slider.js.map +1 -1
  71. package/dist/dev/define/split.d.ts +2 -0
  72. package/dist/dev/define/split.js +8 -0
  73. package/dist/dev/define/split.js.map +1 -0
  74. package/dist/dev/define/switch.d.ts +2 -0
  75. package/dist/dev/define/switch.js +8 -0
  76. package/dist/dev/define/switch.js.map +1 -0
  77. package/dist/dev/define/waveform.d.ts +2 -0
  78. package/dist/dev/define/waveform.js +8 -0
  79. package/dist/dev/define/waveform.js.map +1 -0
  80. package/dist/dev/define/wavestrip.d.ts +2 -0
  81. package/dist/dev/define/wavestrip.js +8 -0
  82. package/dist/dev/define/wavestrip.js.map +1 -0
  83. package/dist/dev/define/xy.d.ts +2 -0
  84. package/dist/dev/define/xy.js +8 -0
  85. package/dist/dev/define/xy.js.map +1 -0
  86. package/dist/dev/define.d.ts +15 -3
  87. package/dist/dev/define.js +7 -1
  88. package/dist/dev/elements/{key.d.ts → button.d.ts} +12 -6
  89. package/dist/dev/elements/button.js +166 -0
  90. package/dist/dev/elements/button.js.map +1 -0
  91. package/dist/dev/elements/dial.d.ts +6 -4
  92. package/dist/dev/elements/dial.js +19 -23
  93. package/dist/dev/elements/dial.js.map +1 -1
  94. package/dist/dev/elements/envelope.d.ts +36 -0
  95. package/dist/dev/elements/envelope.js +372 -0
  96. package/dist/dev/elements/envelope.js.map +1 -0
  97. package/dist/dev/elements/form-element.d.ts +1 -1
  98. package/dist/dev/elements/form-element.js.map +1 -1
  99. package/dist/dev/elements/meter.d.ts +12 -1
  100. package/dist/dev/elements/meter.js +168 -26
  101. package/dist/dev/elements/meter.js.map +1 -1
  102. package/dist/dev/elements/number.d.ts +7 -3
  103. package/dist/dev/elements/number.js +21 -8
  104. package/dist/dev/elements/number.js.map +1 -1
  105. package/dist/dev/elements/radio-group.d.ts +25 -0
  106. package/dist/dev/elements/radio-group.js +186 -0
  107. package/dist/dev/elements/radio-group.js.map +1 -0
  108. package/dist/dev/elements/readout-claim.js +82 -0
  109. package/dist/dev/elements/readout-claim.js.map +1 -0
  110. package/dist/dev/elements/readout.js +36 -0
  111. package/dist/dev/elements/readout.js.map +1 -0
  112. package/dist/dev/elements/segmented.d.ts +4 -10
  113. package/dist/dev/elements/segmented.js +9 -159
  114. package/dist/dev/elements/segmented.js.map +1 -1
  115. package/dist/dev/elements/slider.d.ts +9 -3
  116. package/dist/dev/elements/slider.js +64 -27
  117. package/dist/dev/elements/slider.js.map +1 -1
  118. package/dist/dev/elements/sonic-element.d.ts +4 -3
  119. package/dist/dev/elements/sonic-element.js +51 -22
  120. package/dist/dev/elements/sonic-element.js.map +1 -1
  121. package/dist/dev/elements/split.d.ts +21 -0
  122. package/dist/dev/elements/split.js +258 -0
  123. package/dist/dev/elements/split.js.map +1 -0
  124. package/dist/dev/elements/switch.d.ts +35 -0
  125. package/dist/dev/elements/switch.js +280 -0
  126. package/dist/dev/elements/switch.js.map +1 -0
  127. package/dist/dev/elements/value-element.d.ts +84 -0
  128. package/dist/dev/elements/value-element.js +692 -0
  129. package/dist/dev/elements/value-element.js.map +1 -0
  130. package/dist/dev/elements/{range-entry.js → value-entry.js} +4 -4
  131. package/dist/dev/elements/value-entry.js.map +1 -0
  132. package/dist/dev/elements/wave-element.d.ts +36 -0
  133. package/dist/dev/elements/wave-element.js +78 -0
  134. package/dist/dev/elements/wave-element.js.map +1 -0
  135. package/dist/dev/elements/waveform.d.ts +55 -0
  136. package/dist/dev/elements/waveform.js +306 -0
  137. package/dist/dev/elements/waveform.js.map +1 -0
  138. package/dist/dev/elements/wavestrip.d.ts +51 -0
  139. package/dist/dev/elements/wavestrip.js +183 -0
  140. package/dist/dev/elements/wavestrip.js.map +1 -0
  141. package/dist/dev/elements/xy.d.ts +62 -0
  142. package/dist/dev/elements/xy.js +474 -0
  143. package/dist/dev/elements/xy.js.map +1 -0
  144. package/dist/dev/index.d.ts +11 -2
  145. package/dist/dev/index.js +10 -2
  146. package/dist/dev/lib/adsr-shape.js +109 -0
  147. package/dist/dev/lib/adsr-shape.js.map +1 -0
  148. package/dist/dev/lib/ballistics.js +56 -0
  149. package/dist/dev/lib/ballistics.js.map +1 -0
  150. package/dist/dev/lib/canvas-surface.d.ts +25 -0
  151. package/dist/dev/lib/canvas-surface.js +193 -0
  152. package/dist/dev/lib/canvas-surface.js.map +1 -0
  153. package/dist/dev/lib/copy-node.js +32 -2
  154. package/dist/dev/lib/copy-node.js.map +1 -1
  155. package/dist/dev/lib/crossfade.d.ts +12 -0
  156. package/dist/dev/lib/crossfade.js +23 -0
  157. package/dist/dev/lib/crossfade.js.map +1 -0
  158. package/dist/dev/lib/distribute.d.ts +4 -0
  159. package/dist/dev/lib/distribute.js +216 -0
  160. package/dist/dev/lib/distribute.js.map +1 -0
  161. package/dist/dev/lib/drag-step.js +100 -0
  162. package/dist/dev/lib/drag-step.js.map +1 -0
  163. package/dist/dev/lib/envelope-curve.d.ts +4 -0
  164. package/dist/dev/lib/envelope-curve.js +10 -0
  165. package/dist/dev/lib/envelope-curve.js.map +1 -0
  166. package/dist/dev/lib/field.d.ts +4 -0
  167. package/dist/dev/lib/field.js +95 -0
  168. package/dist/dev/lib/field.js.map +1 -0
  169. package/dist/dev/lib/focus-by-pointer.js +29 -0
  170. package/dist/dev/lib/focus-by-pointer.js.map +1 -0
  171. package/dist/dev/lib/frame-timeline.js +63 -0
  172. package/dist/dev/lib/frame-timeline.js.map +1 -0
  173. package/dist/dev/lib/hold.js +39 -0
  174. package/dist/dev/lib/hold.js.map +1 -0
  175. package/dist/dev/lib/marker-band.js +37 -0
  176. package/dist/dev/lib/marker-band.js.map +1 -0
  177. package/dist/dev/lib/marker-rider.js +115 -0
  178. package/dist/dev/lib/marker-rider.js.map +1 -0
  179. package/dist/dev/lib/math.js +26 -0
  180. package/dist/dev/lib/math.js.map +1 -0
  181. package/dist/dev/lib/modifier-press.js +12 -0
  182. package/dist/dev/lib/modifier-press.js.map +1 -0
  183. package/dist/dev/lib/number-list.js +15 -0
  184. package/dist/dev/lib/number-list.js.map +1 -0
  185. package/dist/dev/lib/pointer-drag.js +44 -0
  186. package/dist/dev/lib/pointer-drag.js.map +1 -0
  187. package/dist/dev/lib/read-px-property.js.map +1 -1
  188. package/dist/dev/lib/render.js +20 -5
  189. package/dist/dev/lib/render.js.map +1 -1
  190. package/dist/dev/lib/resample-peaks.js +37 -0
  191. package/dist/dev/lib/resample-peaks.js.map +1 -0
  192. package/dist/dev/lib/strip-scene.js +49 -0
  193. package/dist/dev/lib/strip-scene.js.map +1 -0
  194. package/dist/dev/lib/taper.js +32 -16
  195. package/dist/dev/lib/taper.js.map +1 -1
  196. package/dist/dev/lib/time-regions.d.ts +4 -0
  197. package/dist/dev/lib/time-regions.js +9 -0
  198. package/dist/dev/lib/time-regions.js.map +1 -0
  199. package/dist/dev/lib/tracking-clock.js +36 -0
  200. package/dist/dev/lib/tracking-clock.js.map +1 -0
  201. package/dist/dev/lib/value-mapping.d.ts +23 -0
  202. package/dist/dev/lib/value-mapping.js +116 -0
  203. package/dist/dev/lib/value-mapping.js.map +1 -0
  204. package/dist/dev/lib/waveform-buckets.d.ts +9 -0
  205. package/dist/dev/lib/waveform-buckets.js +42 -0
  206. package/dist/dev/lib/waveform-buckets.js.map +1 -0
  207. package/dist/skins/ivory.css +1 -2
  208. package/dist/styles/button.css +212 -0
  209. package/dist/styles/controls.css +8 -1
  210. package/dist/styles/dial.css +130 -44
  211. package/dist/styles/envelope.css +159 -0
  212. package/dist/styles/led.css +44 -6
  213. package/dist/styles/material.css +321 -71
  214. package/dist/styles/meter.css +109 -28
  215. package/dist/styles/number.css +5 -1
  216. package/dist/styles/panel.css +59 -0
  217. package/dist/styles/ring.css +74 -0
  218. package/dist/styles/segmented.css +22 -54
  219. package/dist/styles/slider.css +256 -58
  220. package/dist/styles/switch.css +347 -0
  221. package/dist/styles/waveform.css +173 -0
  222. package/dist/styles/wavestrip.css +215 -0
  223. package/dist/styles/xy.css +204 -0
  224. package/package.json +14 -2
  225. package/dist/default/define/key.js +0 -7
  226. package/dist/default/elements/key.js +0 -144
  227. package/dist/default/elements/range-element.js +0 -433
  228. package/dist/dev/define/key.d.ts +0 -2
  229. package/dist/dev/define/key.js +0 -8
  230. package/dist/dev/define/key.js.map +0 -1
  231. package/dist/dev/elements/key.js +0 -145
  232. package/dist/dev/elements/key.js.map +0 -1
  233. package/dist/dev/elements/range-element.d.ts +0 -56
  234. package/dist/dev/elements/range-element.js +0 -434
  235. package/dist/dev/elements/range-element.js.map +0 -1
  236. package/dist/dev/elements/range-entry.js.map +0 -1
  237. package/dist/styles/key.css +0 -197
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distribute.js","names":[],"sources":["../../../src/lib/distribute.ts"],"sourcesContent":["import { clamp, trimFloat } from '#lib/math.ts';\n\nexport interface SplitMember {\n\tisFree: boolean;\n\tmax: number;\n\tmin: number;\n\tsnap: (value: number) => number;\n\tstepFrom: (value: number, direction: -1 | 1) => number;\n\tvalue: number;\n}\n\nexport type SplitMode = 'cascade' | 'equal' | 'proportional';\n\ninterface SplitMove {\n\tdirection?: -1 | 0 | 1;\n\tindex: number;\n\ttarget: number;\n}\n\ninterface SplitRule {\n\tmode: SplitMode;\n\ttotal: number;\n}\n\ninterface Pooled {\n\tindex: number;\n\troom: number;\n\tweight: number;\n}\n\nconst epsilon = 1e-9;\n\nfunction sumOf(values: ReadonlyArray<number>): number {\n\treturn values.reduce((total, value) => total + value, 0);\n}\n\nfunction others(members: ReadonlyArray<SplitMember>, index: number): Array<SplitMember> {\n\treturn members.filter((_member, at) => at !== index);\n}\n\nexport function splitLimits(\n\tmembers: ReadonlyArray<SplitMember>,\n\tindex: number,\n\ttotal: number,\n): [number, number] {\n\tconst member = members[index];\n\tif (!member) return [NaN, NaN];\n\n\tconst rest = others(members, index);\n\tconst free = rest.filter((other) => other.isFree);\n\tif (free.length === 0) return [member.value, member.value];\n\n\tconst fixed = sumOf(rest.filter((other) => !other.isFree).map((other) => other.value));\n\tconst room = total - fixed;\n\n\treturn [\n\t\tMath.max(member.min, room - sumOf(free.map((other) => other.max))),\n\t\tMath.min(member.max, room - sumOf(free.map((other) => other.min))),\n\t];\n}\n\nfunction spreadWeighted(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\tlet active = [...pool];\n\n\twhile (active.length > 0 && Math.abs(remaining) > epsilon) {\n\t\tconst weights = sumOf(active.map(({ weight }) => weight));\n\t\tconst count = active.length;\n\t\tconst shareOf = (entry: Pooled): number =>\n\t\t\tremaining * (weights > 0 ? entry.weight / weights : 1 / count);\n\t\tconst full = active.filter((entry) => Math.abs(shareOf(entry)) >= entry.room);\n\n\t\tif (full.length === 0) {\n\t\t\tfor (const entry of active) shares.set(entry.index, shareOf(entry));\n\t\t\tbreak;\n\t\t}\n\n\t\tconst sign = Math.sign(remaining);\n\n\t\tfor (const entry of full) {\n\t\t\tshares.set(entry.index, sign * entry.room);\n\t\t\tremaining -= sign * entry.room;\n\t\t}\n\t\tactive = active.filter((entry) => !full.includes(entry));\n\t}\n\n\treturn shares;\n}\n\nfunction spreadInOrder(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\n\tfor (const entry of pool) {\n\t\tif (Math.abs(remaining) <= epsilon) break;\n\n\t\tconst share = Math.sign(remaining) * Math.min(entry.room, Math.abs(remaining));\n\n\t\tshares.set(entry.index, share);\n\t\tremaining -= share;\n\t}\n\n\treturn shares;\n}\n\nfunction poolOrder(members: ReadonlyArray<SplitMember>, index: number): Array<number> {\n\tconst count = members.length;\n\tconst start = index + 1;\n\n\treturn Array.from({ length: count }, (_entry, offset) => (start + offset) % count).filter(\n\t\t(at) => at !== index && members[at]?.isFree === true,\n\t);\n}\n\nfunction stepWithin(member: SplitMember, from: number, owed: number): number | undefined {\n\tconst next = member.stepFrom(from, owed > 0 ? 1 : -1);\n\tconst change = trimFloat(next - from);\n\tconst isInside = next >= member.min && next <= member.max;\n\n\treturn change !== 0 && isInside && Math.abs(change) <= Math.abs(owed) + epsilon\n\t\t? next\n\t\t: undefined;\n}\n\nfunction stepToward(member: SplitMember, from: number, owed: number): number {\n\tlet value = from;\n\tlet remainder = owed;\n\n\twhile (Math.abs(remainder) > epsilon) {\n\t\tconst next = stepWithin(member, value, remainder);\n\t\tif (next === undefined) break;\n\n\t\tremainder = trimFloat(remainder - (next - value));\n\t\tvalue = next;\n\t}\n\n\treturn value;\n}\n\ninterface Settling {\n\texact: ReadonlyMap<number, number>;\n\torder: ReadonlyArray<number>;\n\ttotal: number;\n}\n\n// Furthest from its exact share first; a tie goes to the last in pool order\nfunction byShortfall(\n\tvalues: ReadonlyArray<number>,\n\t{ exact, order }: Settling,\n\tdirection: number,\n): Array<number> {\n\tconst shortfall = (index: number): number => {\n\t\tconst value = values[index] ?? NaN;\n\n\t\treturn trimFloat(direction * ((exact.get(index) ?? value) - value));\n\t};\n\n\treturn order.toReversed().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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"}
@@ -0,0 +1,100 @@
1
+ import { clampProportion, wrapProportion } from "./math.js";
2
+
3
+ //#region src/lib/drag-step.ts
4
+ const fineFactor = .1;
5
+ const mouseDragThresholdPx = 3;
6
+ const touchDragThresholdPx = 7;
7
+ function dragThresholdPx(pointerType) {
8
+ return pointerType === "touch" ? touchDragThresholdPx : mouseDragThresholdPx;
9
+ }
10
+ function isAcross(from, to, proportion) {
11
+ return from !== proportion && (from - proportion) * (to - proportion) <= 0;
12
+ }
13
+ function passDetent(hold, from, to) {
14
+ if (hold.slack === void 0 && !isAcross(from, to, hold.proportion)) return {
15
+ proportion: to,
16
+ slack: void 0
17
+ };
18
+ const slack = (hold.slack ?? 0) + to - hold.proportion;
19
+ if (Math.abs(slack) <= hold.zone) return {
20
+ proportion: void 0,
21
+ slack
22
+ };
23
+ return {
24
+ proportion: hold.proportion + slack - Math.sign(slack) * hold.zone,
25
+ slack: void 0
26
+ };
27
+ }
28
+ function nearestTurn(hold, from) {
29
+ return {
30
+ ...hold,
31
+ proportion: hold.proportion + Math.round(from - hold.proportion)
32
+ };
33
+ }
34
+ function land(mapping, state, proportion) {
35
+ const rawProportion = mapping.isWrapping ? wrapProportion(proportion) : clampProportion(proportion);
36
+ return {
37
+ state: {
38
+ ...state,
39
+ rawProportion
40
+ },
41
+ value: mapping.snap(mapping.valueAt(rawProportion))
42
+ };
43
+ }
44
+ function startDrag(mapping, start) {
45
+ const { detent: wanted, from, position, proportion, ...fixed } = start;
46
+ const state = {
47
+ ...fixed,
48
+ isEngaged: false,
49
+ lastPosition: position,
50
+ rawProportion: proportion,
51
+ startPosition: position
52
+ };
53
+ if (!wanted) return state;
54
+ const value = mapping.snap(wanted.value);
55
+ const detent = {
56
+ proportion: mapping.proportionOf(value),
57
+ slack: value === from ? 0 : void 0,
58
+ value,
59
+ zone: wanted.zone
60
+ };
61
+ return {
62
+ ...state,
63
+ detent
64
+ };
65
+ }
66
+ function stepDrag(mapping, state, move) {
67
+ if (!(state.isEngaged || move.isOutside || Math.abs(move.position - state.startPosition) >= state.thresholdPx)) return {
68
+ state,
69
+ value: void 0
70
+ };
71
+ const pace = move.isFine ? fineFactor : 1;
72
+ const to = state.rawProportion + (move.position - state.lastPosition) / state.travelPx * pace;
73
+ const moved = {
74
+ ...state,
75
+ isEngaged: true,
76
+ lastPosition: move.position
77
+ };
78
+ const { detent } = state;
79
+ if (!detent) return land(mapping, moved, to);
80
+ const passed = passDetent(mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent, state.rawProportion, to);
81
+ const held = {
82
+ ...moved,
83
+ detent: {
84
+ ...detent,
85
+ slack: passed.slack
86
+ }
87
+ };
88
+ if (passed.proportion === void 0) return {
89
+ state: {
90
+ ...held,
91
+ rawProportion: detent.proportion
92
+ },
93
+ value: detent.value
94
+ };
95
+ return land(mapping, held, passed.proportion);
96
+ }
97
+
98
+ //#endregion
99
+ export { dragThresholdPx, startDrag, stepDrag };
100
+ //# sourceMappingURL=drag-step.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drag-step.js","names":[],"sources":["../../../src/lib/drag-step.ts"],"sourcesContent":["import type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { clampProportion, wrapProportion } from '#lib/math.ts';\n\ninterface DetentHold {\n\tproportion: number;\n\tslack: number | undefined;\n\tvalue: number;\n\tzone: number;\n}\n\nexport interface DragState {\n\tdetent?: DetentHold;\n\tisEngaged: boolean;\n\tlastPosition: number;\n\trawProportion: number;\n\tstartPosition: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\nexport interface DragMove {\n\tisFine: boolean;\n\tisOutside: boolean;\n\tposition: number;\n}\n\ninterface DragStart {\n\tdetent?: { value: number; zone: number };\n\tfrom: number;\n\tposition: number;\n\tproportion: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\ninterface DragStep {\n\tstate: DragState;\n\tvalue: number | undefined;\n}\n\nconst fineFactor = 0.1;\nconst mouseDragThresholdPx = 3;\nconst touchDragThresholdPx = 7;\n\nexport function dragThresholdPx(pointerType: string): number {\n\treturn pointerType === 'touch' ? touchDragThresholdPx : mouseDragThresholdPx;\n}\n\nfunction isAcross(from: number, to: number, proportion: number): boolean {\n\treturn from !== proportion && (from - proportion) * (to - proportion) <= 0;\n}\n\nfunction passDetent(\n\thold: DetentHold,\n\tfrom: number,\n\tto: 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;\n\n\tif (Math.abs(slack) <= hold.zone) return { proportion: undefined, slack };\n\n\treturn { proportion: hold.proportion + slack - Math.sign(slack) * hold.zone, slack: undefined };\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);\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,MACA,IACgE;CAChE,IAAI,KAAK,UAAU,UAAa,CAAC,SAAS,MAAM,IAAI,KAAK,UAAU,GAClE,OAAO;EAAE,YAAY;EAAI,OAAO;CAAU;CAG3C,MAAM,SAAS,KAAK,SAAS,KAAK,KAAK,KAAK;CAE5C,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO;EAAE,YAAY;EAAW;CAAM;CAExE,OAAO;EAAE,YAAY,KAAK,aAAa,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK;EAAM,OAAO;CAAU;AAC/F;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,MAAM,eAAe,EAAE;CACvD,MAAM,OAAO;EAAE,GAAG;EAAO,QAAQ;GAAE,GAAG;GAAQ,OAAO,OAAO;EAAM;CAAE;CACpE,IAAI,OAAO,eAAe,QACzB,OAAO;EAAE,OAAO;GAAE,GAAG;GAAM,eAAe,OAAO;EAAW;EAAG,OAAO,OAAO;CAAM;CAGpF,OAAO,KAAK,SAAS,MAAM,OAAO,UAAU;AAC7C"}
@@ -0,0 +1,4 @@
1
+ //#region src/lib/envelope-curve.d.ts
2
+ export declare function envelopeCurve(position: number, curve: number): number;
3
+ //#endregion
4
+ //# sourceMappingURL=envelope-curve.d.ts.map
@@ -0,0 +1,10 @@
1
+ //#region src/lib/envelope-curve.ts
2
+ const flatCurve = .001;
3
+ function envelopeCurve(position, curve) {
4
+ if (Math.abs(curve) < flatCurve) return position;
5
+ return Math.expm1(curve * position) / Math.expm1(curve);
6
+ }
7
+
8
+ //#endregion
9
+ export { envelopeCurve };
10
+ //# sourceMappingURL=envelope-curve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope-curve.js","names":[],"sources":["../../../src/lib/envelope-curve.ts"],"sourcesContent":["const flatCurve = 0.001;\n\nexport function envelopeCurve(position: number, curve: number): number {\n\tif (Math.abs(curve) < flatCurve) return position;\n\n\treturn Math.expm1(curve * position) / Math.expm1(curve);\n}\n"],"mappings":";AAAA,MAAM,YAAY;AAElB,SAAgB,cAAc,UAAkB,OAAuB;CACtE,IAAI,KAAK,IAAI,KAAK,IAAI,WAAW,OAAO;CAExC,OAAO,KAAK,MAAM,QAAQ,QAAQ,IAAI,KAAK,MAAM,KAAK;AACvD"}
@@ -0,0 +1,4 @@
1
+ //#region src/lib/field.d.ts
2
+ export type FieldAxis = 'x' | 'y';
3
+ //#endregion
4
+ //# sourceMappingURL=field.d.ts.map
@@ -0,0 +1,95 @@
1
+ import { startDrag, stepDrag } from "./drag-step.js";
2
+
3
+ //#region src/lib/field.ts
4
+ function startAxis(start, position) {
5
+ if (!start) return void 0;
6
+ const { mapping, ...rest } = start;
7
+ return startDrag(mapping, {
8
+ ...rest,
9
+ position,
10
+ thresholdPx: 0
11
+ });
12
+ }
13
+ function startFieldDrag(start) {
14
+ const { position, thresholdPx } = start;
15
+ return {
16
+ isEngaged: false,
17
+ lock: void 0,
18
+ startPosition: position,
19
+ thresholdPx,
20
+ x: startAxis(start.x, position.x),
21
+ y: startAxis(start.y, position.y)
22
+ };
23
+ }
24
+ function dominantAxis(state, move) {
25
+ if (!state.x) return "y";
26
+ if (!state.y) return "x";
27
+ const { startPosition } = state;
28
+ return Math.abs(move.x - startPosition.x) >= Math.abs(move.y - startPosition.y) ? "x" : "y";
29
+ }
30
+ function stepAxis(mapping, drag, move) {
31
+ if (!mapping || !drag) return {
32
+ state: drag,
33
+ value: void 0
34
+ };
35
+ if (move.isHeld) return {
36
+ state: {
37
+ ...drag,
38
+ lastPosition: move.position
39
+ },
40
+ value: void 0
41
+ };
42
+ return stepDrag(mapping, drag, {
43
+ isFine: move.isFine,
44
+ isOutside: false,
45
+ position: move.position
46
+ });
47
+ }
48
+ function stepFieldDrag(mappings, state, move) {
49
+ const { startPosition } = state;
50
+ if (!(state.isEngaged || Math.hypot(move.x - startPosition.x, move.y - startPosition.y) >= state.thresholdPx)) return {
51
+ state,
52
+ x: void 0,
53
+ y: void 0
54
+ };
55
+ const lock = move.isLocked ? state.lock ?? dominantAxis(state, move) : void 0;
56
+ const { isFine } = move;
57
+ const x = stepAxis(mappings.x, state.x, {
58
+ isFine,
59
+ isHeld: lock === "y",
60
+ position: move.x
61
+ });
62
+ const y = stepAxis(mappings.y, state.y, {
63
+ isFine,
64
+ isHeld: lock === "x",
65
+ position: move.y
66
+ });
67
+ return {
68
+ state: {
69
+ ...state,
70
+ isEngaged: true,
71
+ lock,
72
+ x: x.state,
73
+ y: y.state
74
+ },
75
+ x: x.value,
76
+ y: y.value
77
+ };
78
+ }
79
+ function pointerPosition(event) {
80
+ return {
81
+ x: event.clientX,
82
+ y: -event.clientY
83
+ };
84
+ }
85
+ function pointerMove(event) {
86
+ return {
87
+ ...pointerPosition(event),
88
+ isFine: event.shiftKey,
89
+ isLocked: event.altKey
90
+ };
91
+ }
92
+
93
+ //#endregion
94
+ export { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag };
95
+ //# sourceMappingURL=field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.js","names":[],"sources":["../../../src/lib/field.ts"],"sourcesContent":["import type { DragState } from '#lib/drag-step.ts';\nimport type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { startDrag, stepDrag } from '#lib/drag-step.ts';\n\nexport type FieldAxis = 'x' | 'y';\n\nexport interface FieldPoint {\n\tx: number;\n\ty: number;\n}\n\ninterface FieldAxisStart {\n\tfrom: number;\n\tmapping: ValueMapping;\n\tproportion: number;\n\ttravelPx: number;\n}\n\ninterface FieldStart {\n\tposition: FieldPoint;\n\tthresholdPx: number;\n\tx?: FieldAxisStart | undefined;\n\ty?: FieldAxisStart | undefined;\n}\n\nexport interface FieldDragState {\n\tisEngaged: boolean;\n\tlock: FieldAxis | undefined;\n\tstartPosition: FieldPoint;\n\tthresholdPx: number;\n\tx: DragState | undefined;\n\ty: DragState | undefined;\n}\n\nexport interface FieldMove extends FieldPoint {\n\tisFine: boolean;\n\tisLocked: boolean;\n}\n\nexport interface FieldMappings {\n\tx?: undefined | ValueMapping;\n\ty?: undefined | ValueMapping;\n}\n\ninterface FieldStep {\n\tstate: FieldDragState;\n\tx: number | undefined;\n\ty: number | undefined;\n}\n\ninterface AxisMove {\n\tisFine: boolean;\n\tisHeld: boolean;\n\tposition: number;\n}\n\nfunction startAxis(start: FieldAxisStart | undefined, position: number): DragState | undefined {\n\tif (!start) return undefined;\n\n\tconst { mapping, ...rest } = start;\n\n\treturn startDrag(mapping, { ...rest, position, thresholdPx: 0 });\n}\n\nexport function startFieldDrag(start: FieldStart): FieldDragState {\n\tconst { position, thresholdPx } = start;\n\n\treturn {\n\t\tisEngaged: false,\n\t\tlock: undefined,\n\t\tstartPosition: position,\n\t\tthresholdPx,\n\t\tx: startAxis(start.x, position.x),\n\t\ty: startAxis(start.y, position.y),\n\t};\n}\n\nfunction dominantAxis(state: FieldDragState, move: FieldPoint): FieldAxis {\n\tif (!state.x) return 'y';\n\tif (!state.y) return 'x';\n\n\tconst { startPosition } = state;\n\n\treturn Math.abs(move.x - startPosition.x) >= Math.abs(move.y - startPosition.y) ? 'x' : 'y';\n}\n\nfunction stepAxis(\n\tmapping: undefined | ValueMapping,\n\tdrag: DragState | undefined,\n\tmove: AxisMove,\n): { state: DragState | undefined; value: number | undefined } {\n\tif (!mapping || !drag) return { state: drag, value: undefined };\n\tif (move.isHeld) return { state: { ...drag, lastPosition: move.position }, value: undefined };\n\n\treturn stepDrag(mapping, drag, {\n\t\tisFine: move.isFine,\n\t\tisOutside: false,\n\t\tposition: move.position,\n\t});\n}\n\nexport function stepFieldDrag(\n\tmappings: FieldMappings,\n\tstate: FieldDragState,\n\tmove: FieldMove,\n): FieldStep {\n\tconst { startPosition } = state;\n\tconst isEngaged =\n\t\tstate.isEngaged ||\n\t\tMath.hypot(move.x - startPosition.x, move.y - startPosition.y) >= state.thresholdPx;\n\n\tif (!isEngaged) return { state, x: undefined, y: undefined };\n\n\tconst lock = move.isLocked ? (state.lock ?? dominantAxis(state, move)) : undefined;\n\tconst { isFine } = move;\n\tconst x = stepAxis(mappings.x, state.x, { isFine, isHeld: lock === 'y', position: move.x });\n\tconst y = stepAxis(mappings.y, state.y, { isFine, isHeld: lock === 'x', position: move.y });\n\n\treturn {\n\t\tstate: { ...state, isEngaged: true, lock, x: x.state, y: y.state },\n\t\tx: x.value,\n\t\ty: y.value,\n\t};\n}\n\nexport function pointerPosition(event: PointerEvent): FieldPoint {\n\treturn { x: event.clientX, y: -event.clientY };\n}\n\nexport function pointerMove(event: PointerEvent): FieldMove {\n\treturn { ...pointerPosition(event), isFine: event.shiftKey, isLocked: event.altKey };\n}\n"],"mappings":";;;AAyDA,SAAS,UAAU,OAAmC,UAAyC;CAC9F,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,EAAE,SAAS,GAAG,SAAS;CAE7B,OAAO,UAAU,SAAS;EAAE,GAAG;EAAM;EAAU,aAAa;CAAE,CAAC;AAChE;AAEA,SAAgB,eAAe,OAAmC;CACjE,MAAM,EAAE,UAAU,gBAAgB;CAElC,OAAO;EACN,WAAW;EACX,MAAM;EACN,eAAe;EACf;EACA,GAAG,UAAU,MAAM,GAAG,SAAS,CAAC;EAChC,GAAG,UAAU,MAAM,GAAG,SAAS,CAAC;CACjC;AACD;AAEA,SAAS,aAAa,OAAuB,MAA6B;CACzE,IAAI,CAAC,MAAM,GAAG,OAAO;CACrB,IAAI,CAAC,MAAM,GAAG,OAAO;CAErB,MAAM,EAAE,kBAAkB;CAE1B,OAAO,KAAK,IAAI,KAAK,IAAI,cAAc,CAAC,KAAK,KAAK,IAAI,KAAK,IAAI,cAAc,CAAC,IAAI,MAAM;AACzF;AAEA,SAAS,SACR,SACA,MACA,MAC8D;CAC9D,IAAI,CAAC,WAAW,CAAC,MAAM,OAAO;EAAE,OAAO;EAAM,OAAO;CAAU;CAC9D,IAAI,KAAK,QAAQ,OAAO;EAAE,OAAO;GAAE,GAAG;GAAM,cAAc,KAAK;EAAS;EAAG,OAAO;CAAU;CAE5F,OAAO,SAAS,SAAS,MAAM;EAC9B,QAAQ,KAAK;EACb,WAAW;EACX,UAAU,KAAK;CAChB,CAAC;AACF;AAEA,SAAgB,cACf,UACA,OACA,MACY;CACZ,MAAM,EAAE,kBAAkB;CAK1B,IAAI,EAHH,MAAM,aACN,KAAK,MAAM,KAAK,IAAI,cAAc,GAAG,KAAK,IAAI,cAAc,CAAC,KAAK,MAAM,cAEzD,OAAO;EAAE;EAAO,GAAG;EAAW,GAAG;CAAU;CAE3D,MAAM,OAAO,KAAK,WAAY,MAAM,QAAQ,aAAa,OAAO,IAAI,IAAK;CACzE,MAAM,EAAE,WAAW;CACnB,MAAM,IAAI,SAAS,SAAS,GAAG,MAAM,GAAG;EAAE;EAAQ,QAAQ,SAAS;EAAK,UAAU,KAAK;CAAE,CAAC;CAC1F,MAAM,IAAI,SAAS,SAAS,GAAG,MAAM,GAAG;EAAE;EAAQ,QAAQ,SAAS;EAAK,UAAU,KAAK;CAAE,CAAC;CAE1F,OAAO;EACN,OAAO;GAAE,GAAG;GAAO,WAAW;GAAM;GAAM,GAAG,EAAE;GAAO,GAAG,EAAE;EAAM;EACjE,GAAG,EAAE;EACL,GAAG,EAAE;CACN;AACD;AAEA,SAAgB,gBAAgB,OAAiC;CAChE,OAAO;EAAE,GAAG,MAAM;EAAS,GAAG,CAAC,MAAM;CAAQ;AAC9C;AAEA,SAAgB,YAAY,OAAgC;CAC3D,OAAO;EAAE,GAAG,gBAAgB,KAAK;EAAG,QAAQ,MAAM;EAAU,UAAU,MAAM;CAAO;AACpF"}
@@ -0,0 +1,29 @@
1
+ //#region src/lib/focus-by-pointer.ts
2
+ const modifierKeys = /* @__PURE__ */ new Set([
3
+ "Alt",
4
+ "Control",
5
+ "Meta",
6
+ "Shift"
7
+ ]);
8
+ function focusByPointer(target) {
9
+ const isMarked = "sonicPointerFocus" in target.dataset;
10
+ target.dataset.sonicPointerFocus = "";
11
+ target.focus();
12
+ if (isMarked) return;
13
+ const marking = new AbortController();
14
+ const clear = () => {
15
+ delete target.dataset.sonicPointerFocus;
16
+ marking.abort();
17
+ };
18
+ target.addEventListener("blur", () => {
19
+ if (target.ownerDocument.activeElement !== target) clear();
20
+ }, { signal: marking.signal });
21
+ target.addEventListener("keydown", (event) => {
22
+ if (event.metaKey || event.ctrlKey || event.altKey || modifierKeys.has(event.key)) return;
23
+ clear();
24
+ }, { signal: marking.signal });
25
+ }
26
+
27
+ //#endregion
28
+ export { focusByPointer };
29
+ //# sourceMappingURL=focus-by-pointer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-by-pointer.js","names":[],"sources":["../../../src/lib/focus-by-pointer.ts"],"sourcesContent":["const modifierKeys = new Set(['Alt', 'Control', 'Meta', 'Shift']);\n\n// A script focus during a mouse press still matches `:focus-visible`\nexport function focusByPointer(target: HTMLElement): void {\n\tconst isMarked = 'sonicPointerFocus' in target.dataset;\n\n\ttarget.dataset.sonicPointerFocus = '';\n\ttarget.focus();\n\tif (isMarked) return;\n\n\tconst marking = new AbortController();\n\tconst clear = (): void => {\n\t\tdelete target.dataset.sonicPointerFocus;\n\t\tmarking.abort();\n\t};\n\n\ttarget.addEventListener(\n\t\t'blur',\n\t\t() => {\n\t\t\t// A window or tab switch blurs the target and leaves it the active element\n\t\t\tif (target.ownerDocument.activeElement !== target) clear();\n\t\t},\n\t\t{ signal: marking.signal },\n\t);\n\ttarget.addEventListener(\n\t\t'keydown',\n\t\t(event) => {\n\t\t\tif (event.metaKey || event.ctrlKey || event.altKey || modifierKeys.has(event.key)) return;\n\n\t\t\tclear();\n\t\t},\n\t\t{ signal: marking.signal },\n\t);\n}\n"],"mappings":";AAAA,MAAM,+BAAe,IAAI,IAAI;CAAC;CAAO;CAAW;CAAQ;AAAO,CAAC;AAGhE,SAAgB,eAAe,QAA2B;CACzD,MAAM,WAAW,uBAAuB,OAAO;CAE/C,OAAO,QAAQ,oBAAoB;CACnC,OAAO,MAAM;CACb,IAAI,UAAU;CAEd,MAAM,UAAU,IAAI,gBAAgB;CACpC,MAAM,cAAoB;EACzB,OAAO,OAAO,QAAQ;EACtB,QAAQ,MAAM;CACf;CAEA,OAAO,iBACN,cACM;EAEL,IAAI,OAAO,cAAc,kBAAkB,QAAQ,MAAM;CAC1D,GACA,EAAE,QAAQ,QAAQ,OAAO,CAC1B;CACA,OAAO,iBACN,YACC,UAAU;EACV,IAAI,MAAM,WAAW,MAAM,WAAW,MAAM,UAAU,aAAa,IAAI,MAAM,GAAG,GAAG;EAEnF,MAAM;CACP,GACA,EAAE,QAAQ,QAAQ,OAAO,CAC1B;AACD"}
@@ -0,0 +1,63 @@
1
+ import { trimFloat } from "./math.js";
2
+
3
+ //#region src/lib/frame-timeline.ts
4
+ const placeholderPeriodMs = 2500;
5
+ function windowStart(input, seconds, windowSeconds) {
6
+ if (!input.isPaged) return seconds - windowSeconds / 2;
7
+ if (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;
8
+ return Math.floor(trimFloat(seconds / windowSeconds)) * windowSeconds;
9
+ }
10
+ function overlap([from, to], [low, high]) {
11
+ const start = Math.max(from, low);
12
+ const end = Math.min(to, high);
13
+ return end > start ? [start, end] : void 0;
14
+ }
15
+ function pendingIn({ pending, range }, window) {
16
+ return pending.flatMap((region) => {
17
+ const inRange = overlap(region, range);
18
+ const shown = inRange && overlap(inRange, window);
19
+ return shown ? [shown] : [];
20
+ });
21
+ }
22
+ function placeholderPhase({ frameMs, isPaged }, pending) {
23
+ return !isPaged && pending.length > 0 ? frameMs / placeholderPeriodMs : void 0;
24
+ }
25
+ function frameTimeline(input) {
26
+ const { clockSeconds, held, range, size } = input;
27
+ const seconds = held?.seconds ?? clockSeconds;
28
+ const pixelsPerSecond = input.zoom * size.dpr;
29
+ const windowSeconds = size.width / pixelsPerSecond;
30
+ const startSeconds = windowStart(input, seconds, windowSeconds);
31
+ const pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);
32
+ const placeholder = placeholderPhase(input, pending);
33
+ const phase = placeholder ?? 0;
34
+ const wanted = overlap([startSeconds - windowSeconds, startSeconds + 2 * windowSeconds], range);
35
+ const at = (time) => (time - startSeconds) / windowSeconds;
36
+ return {
37
+ ...held ? { ghostAt: at(clockSeconds) } : {},
38
+ isMoving: held !== void 0 || placeholder !== void 0 || input.isPlaying,
39
+ paintKey: [
40
+ startSeconds,
41
+ windowSeconds,
42
+ ...range,
43
+ phase,
44
+ pending.join(",")
45
+ ].join(":"),
46
+ playheadAt: at(seconds),
47
+ seconds,
48
+ view: {
49
+ dpr: size.dpr,
50
+ height: size.height,
51
+ pending,
52
+ phase,
53
+ pixelsPerSecond,
54
+ startSeconds,
55
+ width: size.width
56
+ },
57
+ ...wanted ? { wanted } : {}
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ export { frameTimeline };
63
+ //# sourceMappingURL=frame-timeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-timeline.js","names":[],"sources":["../../../src/lib/frame-timeline.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { trimFloat } from '#lib/math.ts';\n\nexport interface View {\n\tdpr: number;\n\theight: number;\n\tpending: Array<[number, number]>;\n\tphase: number;\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\nexport interface Drawn {\n\tseconds: number;\n\tstartSeconds: number;\n}\n\ninterface TimelineInput {\n\tclockSeconds: number;\n\tframeMs: number;\n\theld?: { grabbed: Drawn; seconds: number };\n\tisPaged: boolean;\n\tisPlaying: boolean;\n\tpending: ReadonlyArray<[number, number]>;\n\trange: [number, number];\n\tsize: SurfaceSize;\n\tzoom: number;\n}\n\ninterface Timeline {\n\tghostAt?: number;\n\tisMoving: boolean;\n\tpaintKey: string;\n\tplayheadAt: number;\n\tseconds: number;\n\tview: View;\n\twanted?: [number, number];\n}\n\nconst placeholderPeriodMs = 2500;\n\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([startSeconds - windowSeconds, startSeconds + 2 * windowSeconds], range);\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,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,QAAQ,CAAC,eAAe,eAAe,eAAe,IAAI,aAAa,GAAG,KAAK;CAC9F,MAAM,MAAM,UAA0B,OAAO,gBAAgB;CAE7D,OAAO;EACN,GAAI,OAAO,EAAE,SAAS,GAAG,YAAY,EAAE,IAAI,CAAC;EAC5C,UAAU,SAAS,UAAa,gBAAgB,UAAa,MAAM;EACnE,UAAU;GAAC;GAAc;GAAe,GAAG;GAAO;GAAO,QAAQ,KAAK,GAAG;EAAC,CAAC,CAAC,KAAK,GAAG;EACpF,YAAY,GAAG,OAAO;EACtB;EACA,MAAM;GACL,KAAK,KAAK;GACV,QAAQ,KAAK;GACb;GACA;GACA;GACA;GACA,OAAO,KAAK;EACb;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC5B;AACD"}
@@ -0,0 +1,39 @@
1
+ //#region src/lib/hold.ts
2
+ function bindHold(target, options, signal) {
3
+ let holder;
4
+ const hold = {
5
+ hold(by) {
6
+ if (holder !== void 0 || !options.canHold()) return false;
7
+ holder = by;
8
+ options.onHold();
9
+ return true;
10
+ },
11
+ release() {
12
+ if (holder === void 0) return;
13
+ holder = void 0;
14
+ options.onRelease();
15
+ }
16
+ };
17
+ const releasePointer = (event) => {
18
+ if (event.pointerId === holder) hold.release();
19
+ };
20
+ target.addEventListener("lostpointercapture", releasePointer, { signal });
21
+ target.addEventListener("pointercancel", releasePointer, { signal });
22
+ target.addEventListener("pointerup", releasePointer, { signal });
23
+ target.addEventListener("keyup", (event) => {
24
+ if (event.key === "Meta" && typeof holder === "string" || event.key === holder) hold.release();
25
+ }, { signal });
26
+ target.addEventListener(options.leave, (event) => {
27
+ const next = event.relatedTarget;
28
+ const isInside = next instanceof Node && target.contains(next);
29
+ if (typeof holder === "string" && !isInside) hold.release();
30
+ }, { signal });
31
+ signal.addEventListener("abort", () => {
32
+ hold.release();
33
+ }, { once: true });
34
+ return hold;
35
+ }
36
+
37
+ //#endregion
38
+ export { bindHold };
39
+ //# sourceMappingURL=hold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hold.js","names":[],"sources":["../../../src/lib/hold.ts"],"sourcesContent":["export interface Hold {\n\thold(by: number | string): boolean;\n\trelease(): void;\n}\n\ninterface HoldOptions {\n\tcanHold: () => boolean;\n\tleave: 'blur' | 'focusout';\n\tonHold: () => void;\n\tonRelease: () => void;\n}\n\nexport function bindHold(target: HTMLElement, options: HoldOptions, signal: AbortSignal): Hold {\n\tlet holder: number | string | undefined;\n\tconst hold: Hold = {\n\t\thold(by) {\n\t\t\tif (holder !== undefined || !options.canHold()) return false;\n\n\t\t\tholder = by;\n\t\t\toptions.onHold();\n\n\t\t\treturn true;\n\t\t},\n\t\trelease() {\n\t\t\tif (holder === undefined) return;\n\n\t\t\tholder = undefined;\n\t\t\toptions.onRelease();\n\t\t},\n\t};\n\tconst releasePointer = (event: PointerEvent): void => {\n\t\tif (event.pointerId === holder) hold.release();\n\t};\n\n\ttarget.addEventListener('lostpointercapture', releasePointer, { signal });\n\ttarget.addEventListener('pointercancel', releasePointer, { signal });\n\ttarget.addEventListener('pointerup', releasePointer, { signal });\n\ttarget.addEventListener(\n\t\t'keyup',\n\t\t(event) => {\n\t\t\t// macOS sends no `keyup` for a key let go while Cmd is down\n\t\t\tconst isMetaLift = event.key === 'Meta' && typeof holder === 'string';\n\n\t\t\tif (isMetaLift || event.key === holder) hold.release();\n\t\t},\n\t\t{ signal },\n\t);\n\ttarget.addEventListener(\n\t\toptions.leave,\n\t\t(event) => {\n\t\t\tconst next = event.relatedTarget;\n\t\t\tconst isInside = next instanceof Node && target.contains(next);\n\n\t\t\tif (typeof holder === 'string' && !isInside) hold.release();\n\t\t},\n\t\t{ signal },\n\t);\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\thold.release();\n\t\t},\n\t\t{ once: true },\n\t);\n\n\treturn hold;\n}\n"],"mappings":";AAYA,SAAgB,SAAS,QAAqB,SAAsB,QAA2B;CAC9F,IAAI;CACJ,MAAM,OAAa;EAClB,KAAK,IAAI;GACR,IAAI,WAAW,UAAa,CAAC,QAAQ,QAAQ,GAAG,OAAO;GAEvD,SAAS;GACT,QAAQ,OAAO;GAEf,OAAO;EACR;EACA,UAAU;GACT,IAAI,WAAW,QAAW;GAE1B,SAAS;GACT,QAAQ,UAAU;EACnB;CACD;CACA,MAAM,kBAAkB,UAA8B;EACrD,IAAI,MAAM,cAAc,QAAQ,KAAK,QAAQ;CAC9C;CAEA,OAAO,iBAAiB,sBAAsB,gBAAgB,EAAE,OAAO,CAAC;CACxE,OAAO,iBAAiB,iBAAiB,gBAAgB,EAAE,OAAO,CAAC;CACnE,OAAO,iBAAiB,aAAa,gBAAgB,EAAE,OAAO,CAAC;CAC/D,OAAO,iBACN,UACC,UAAU;EAIV,IAFmB,MAAM,QAAQ,UAAU,OAAO,WAAW,YAE3C,MAAM,QAAQ,QAAQ,KAAK,QAAQ;CACtD,GACA,EAAE,OAAO,CACV;CACA,OAAO,iBACN,QAAQ,QACP,UAAU;EACV,MAAM,OAAO,MAAM;EACnB,MAAM,WAAW,gBAAgB,QAAQ,OAAO,SAAS,IAAI;EAE7D,IAAI,OAAO,WAAW,YAAY,CAAC,UAAU,KAAK,QAAQ;CAC3D,GACA,EAAE,OAAO,CACV;CACA,OAAO,iBACN,eACM;EACL,KAAK,QAAQ;CACd,GACA,EAAE,MAAM,KAAK,CACd;CAEA,OAAO;AACR"}
@@ -0,0 +1,37 @@
1
+ import { readPxProperty } from "./read-px-property.js";
2
+
3
+ //#region src/lib/marker-band.ts
4
+ const kindPattern = /^[a-z][a-z0-9-]*$/;
5
+ const snapSlopPx = 4;
6
+ function renderMarker(marker, [from, to]) {
7
+ const part = document.createElement("div");
8
+ part.className = marker.end === void 0 ? "sonic-wavestrip-marker" : "sonic-wavestrip-region";
9
+ part.style.setProperty("--_sonic-marker-from", String(from));
10
+ part.style.setProperty("--_sonic-marker-to", String(to));
11
+ if (marker.kind !== void 0 && kindPattern.test(marker.kind)) part.style.setProperty("--_sonic-marker", `var(--sonic-marker-${marker.kind}, var(--_sonic-marker-default))`);
12
+ if (marker.dimmed === true) part.dataset.sonicDimmed = "";
13
+ return part;
14
+ }
15
+ function createMarkerBand(band) {
16
+ return {
17
+ reachPx: () => readPxProperty(getComputedStyle(band), "--_sonic-marker-size", 0) / 2 + snapSlopPx,
18
+ render: (markers, proportions) => {
19
+ band.replaceChildren(...markers.map((marker, index) => renderMarker(marker, proportions[index] ?? [0, 0])));
20
+ }
21
+ };
22
+ }
23
+ function nearestMarker(proportion, markerProportions, reach) {
24
+ let nearest;
25
+ let nearestDistance = reach;
26
+ for (const [index, markerProportion] of markerProportions.entries()) {
27
+ const distance = Math.abs(markerProportion - proportion);
28
+ if (distance > nearestDistance) continue;
29
+ nearest = index;
30
+ nearestDistance = distance;
31
+ }
32
+ return nearest;
33
+ }
34
+
35
+ //#endregion
36
+ export { createMarkerBand, nearestMarker };
37
+ //# sourceMappingURL=marker-band.js.map
@@ -0,0 +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\treachPx: () => number;\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\nfunction renderMarker(marker: MarkerLook, [from, to]: [number, number]): HTMLElement {\n\tconst part = document.createElement('div');\n\n\tpart.className = marker.end === undefined ? 'sonic-wavestrip-marker' : 'sonic-wavestrip-region';\n\tpart.style.setProperty('--_sonic-marker-from', String(from));\n\tpart.style.setProperty('--_sonic-marker-to', String(to));\n\tif (marker.kind !== undefined && kindPattern.test(marker.kind)) {\n\t\tpart.style.setProperty(\n\t\t\t'--_sonic-marker',\n\t\t\t`var(--sonic-marker-${marker.kind}, var(--_sonic-marker-default))`,\n\t\t);\n\t}\n\tif (marker.dimmed === true) part.dataset.sonicDimmed = '';\n\n\treturn part;\n}\n\nexport function createMarkerBand(band: HTMLElement): MarkerBand {\n\treturn {\n\t\treachPx: () =>\n\t\t\treadPxProperty(getComputedStyle(band), '--_sonic-marker-size', 0) / 2 + snapSlopPx,\n\t\trender: (markers, proportions) => {\n\t\t\tband.replaceChildren(\n\t\t\t\t...markers.map((marker, index) => renderMarker(marker, proportions[index] ?? [0, 0])),\n\t\t\t);\n\t\t},\n\t};\n}\n\nexport function nearestMarker(\n\tproportion: number,\n\tmarkerProportions: ReadonlyArray<number>,\n\treach: number,\n): number | undefined {\n\tlet nearest: number | undefined;\n\tlet nearestDistance = reach;\n\n\tfor (const [index, markerProportion] of markerProportions.entries()) {\n\t\tconst distance = Math.abs(markerProportion - proportion);\n\t\tif (distance > nearestDistance) continue;\n\n\t\tnearest = index;\n\t\tnearestDistance = distance;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";;;AAiBA,MAAM,cAAc;AAEpB,MAAM,aAAa;AAEnB,SAAS,aAAa,QAAoB,CAAC,MAAM,KAAoC;CACpF,MAAM,OAAO,SAAS,cAAc,KAAK;CAEzC,KAAK,YAAY,OAAO,QAAQ,SAAY,2BAA2B;CACvE,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;CAC3D,KAAK,MAAM,YAAY,sBAAsB,OAAO,EAAE,CAAC;CACvD,IAAI,OAAO,SAAS,UAAa,YAAY,KAAK,OAAO,IAAI,GAC5D,KAAK,MAAM,YACV,mBACA,sBAAsB,OAAO,KAAK,gCACnC;CAED,IAAI,OAAO,WAAW,MAAM,KAAK,QAAQ,cAAc;CAEvD,OAAO;AACR;AAEA,SAAgB,iBAAiB,MAA+B;CAC/D,OAAO;EACN,eACC,eAAe,iBAAiB,IAAI,GAAG,wBAAwB,CAAC,IAAI,IAAI;EACzE,SAAS,SAAS,gBAAgB;GACjC,KAAK,gBACJ,GAAG,QAAQ,KAAK,QAAQ,UAAU,aAAa,QAAQ,YAAY,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CACrF;EACD;CACD;AACD;AAEA,SAAgB,cACf,YACA,mBACA,OACqB;CACrB,IAAI;CACJ,IAAI,kBAAkB;CAEtB,KAAK,MAAM,CAAC,OAAO,qBAAqB,kBAAkB,QAAQ,GAAG;EACpE,MAAM,WAAW,KAAK,IAAI,mBAAmB,UAAU;EACvD,IAAI,WAAW,iBAAiB;EAEhC,UAAU;EACV,kBAAkB;CACnB;CAEA,OAAO;AACR"}
@@ -0,0 +1,115 @@
1
+ import { clamp } from "./math.js";
2
+ import { requireChild } from "./render.js";
3
+ import { readPxProperty } from "./read-px-property.js";
4
+
5
+ //#region src/lib/marker-rider.ts
6
+ const fadeShare = .25;
7
+ function lineX(seconds, view) {
8
+ return (seconds - view.startSeconds) / view.windowSeconds * view.widthPx;
9
+ }
10
+ function parkedPlacement(labels, index, [view, arrivingX]) {
11
+ const parked = labels[index];
12
+ if (!parked) return void 0;
13
+ const next = labels[index + 1];
14
+ const x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);
15
+ const approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;
16
+ return {
17
+ index,
18
+ opacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),
19
+ shownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),
20
+ x
21
+ };
22
+ }
23
+ function isDrawable(length) {
24
+ return Number.isFinite(length) && length > 0;
25
+ }
26
+ function layoutRider(labels, view) {
27
+ if (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};
28
+ const parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);
29
+ const next = labels[parkedIndex + 1];
30
+ const arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;
31
+ const parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);
32
+ const arriving = next && arrivingX < view.widthPx ? {
33
+ index: parkedIndex + 1,
34
+ opacity: 1,
35
+ shownPx: Math.min(next.widthPx, view.widthPx - arrivingX),
36
+ x: arrivingX
37
+ } : void 0;
38
+ return {
39
+ ...arriving ? { arriving } : {},
40
+ ...parked ? { parked } : {}
41
+ };
42
+ }
43
+ function labelSlot(element) {
44
+ return {
45
+ element,
46
+ opacity: NaN,
47
+ shownPx: NaN,
48
+ x: NaN
49
+ };
50
+ }
51
+ function writePlacement(slot, { opacity, shownPx, x }, widthPx) {
52
+ const { style } = slot.element;
53
+ if (slot.x !== x) {
54
+ slot.x = x;
55
+ style.setProperty("translate", `${x.toFixed(2)}px`);
56
+ }
57
+ if (slot.shownPx !== shownPx) {
58
+ slot.shownPx = shownPx;
59
+ style.setProperty("clip-path", shownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : "");
60
+ }
61
+ if (slot.opacity !== opacity) {
62
+ slot.opacity = opacity;
63
+ style.setProperty("opacity", opacity < 1 ? opacity.toFixed(3) : "");
64
+ }
65
+ }
66
+ function writeLabel(slot, labels, placement) {
67
+ const { element } = slot;
68
+ const label = placement && placement.shownPx > 0 ? labels[placement.index] : void 0;
69
+ if (element.hidden !== !label) element.hidden = !label;
70
+ if (!label || !placement) return;
71
+ if (element.textContent !== label.text) element.textContent = label.text;
72
+ element.toggleAttribute("data-sonic-dimmed", label.isDimmed);
73
+ writePlacement(slot, placement, label.widthPx);
74
+ }
75
+ function measureWidths(control, className, texts) {
76
+ const probes = texts.map((text) => {
77
+ const probe = document.createElement("div");
78
+ probe.className = className;
79
+ probe.textContent = text;
80
+ return probe;
81
+ });
82
+ control.append(...probes);
83
+ const widths = probes.map((probe) => probe.getBoundingClientRect().width);
84
+ for (const probe of probes) probe.remove();
85
+ return widths;
86
+ }
87
+ function createLabelRider(control, options) {
88
+ const { className, insetProperty } = options;
89
+ const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
90
+ const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
91
+ let insetPx = 0;
92
+ let measured = [];
93
+ return {
94
+ measure: (labels) => {
95
+ const widths = measureWidths(control, className, labels.map(({ text }) => text));
96
+ insetPx = readPxProperty(getComputedStyle(control), insetProperty, 0);
97
+ measured = labels.map((label, index) => ({
98
+ ...label,
99
+ widthPx: widths[index] ?? 0
100
+ })).toSorted((first, second) => first.start - second.start);
101
+ },
102
+ place: (window) => {
103
+ const layout = layoutRider(measured, {
104
+ ...window,
105
+ insetPx
106
+ });
107
+ writeLabel(parked, measured, layout.parked);
108
+ writeLabel(arriving, measured, layout.arriving);
109
+ }
110
+ };
111
+ }
112
+
113
+ //#endregion
114
+ export { createLabelRider, layoutRider };
115
+ //# sourceMappingURL=marker-rider.js.map
@@ -0,0 +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\tlabels: ReadonlyArray<RiderLabel>,\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\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: { className: string; insetProperty: `--_sonic-${string}` },\n): {\n\tmeasure: (labels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>) => void;\n\tplace: (window: RiderWindow) => void;\n} {\n\tconst { className, 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, layout.parked);\n\t\t\twriteLabel(arriving, measured, layout.arriving);\n\t\t},\n\t};\n}\n"],"mappings":"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"}