@weft-js/scroll 0.0.0-stage → 0.1.5-nightly.20261111.3
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/LICENSE +16 -0
- package/README.md +579 -2
- package/package.json +39 -3
- package/src/autoscroll.ts +91 -0
- package/src/axis.ts +59 -0
- package/src/bar-input.ts +104 -0
- package/src/bar.ts +29 -0
- package/src/canvas.ts +111 -0
- package/src/engine.ts +374 -0
- package/src/index.ts +21 -0
- package/src/keys.ts +87 -0
- package/src/options.ts +88 -0
- package/src/physics.ts +65 -0
- package/src/render.ts +205 -0
- package/src/scroller-api.ts +75 -0
- package/src/scroller.ts +240 -0
- package/src/snap.ts +35 -0
- package/src/touch.ts +169 -0
- package/src/types.ts +178 -0
- package/src/wheel-input.ts +112 -0
- package/src/wheel.ts +32 -0
- package/src/wrap.ts +27 -0
- package/styles/scroll.css +199 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Autoscroll: holding a text selection past the edge, or dragging something over the edge, scrolls toward it, faster
|
|
2
|
+
// the further out the pointer is. The selection follows the content as it moves, as it does natively.
|
|
3
|
+
import type { Engine } from './engine'
|
|
4
|
+
import type { Axis } from './types'
|
|
5
|
+
|
|
6
|
+
/** px per ms for a pointer `out` px past an edge. */
|
|
7
|
+
export const autoSpeed = (out: number) =>
|
|
8
|
+
out ? Math.sign(out) * Math.min(4, Math.max(0.15, Math.abs(out) / 25)) : 0
|
|
9
|
+
|
|
10
|
+
/** How far `p` is outside `[lo, hi]`, signed. */
|
|
11
|
+
const outside = (p: number, lo: number, hi: number) => (p < lo ? p - lo : p > hi ? p - hi : 0)
|
|
12
|
+
|
|
13
|
+
type CaretDoc = Document & {
|
|
14
|
+
caretPositionFromPoint?: (x: number, y: number) => { offsetNode: Node; offset: number } | null
|
|
15
|
+
caretRangeFromPoint?: (x: number, y: number) => Range | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The caret under a point: Chrome and Firefox have the standard call, Safari the older one. */
|
|
19
|
+
function caretAt(x: number, y: number) {
|
|
20
|
+
const doc = document as CaretDoc
|
|
21
|
+
const pos = doc.caretPositionFromPoint?.(x, y)
|
|
22
|
+
if (pos) return pos
|
|
23
|
+
const r = doc.caretRangeFromPoint?.(x, y)
|
|
24
|
+
return r ? { offsetNode: r.startContainer, offset: r.startOffset } : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Listen for held selections and drags over the viewport. Returns the cleanup. */
|
|
28
|
+
export function autoscroll(e: Engine) {
|
|
29
|
+
let point: { x: number; y: number } | null = null
|
|
30
|
+
const set = (axis: Axis, v: number) => e.auto(axis, e.o.autoscroll ? v : 0)
|
|
31
|
+
const halt = () => {
|
|
32
|
+
point = null
|
|
33
|
+
set('x', 0)
|
|
34
|
+
set('y', 0)
|
|
35
|
+
}
|
|
36
|
+
e.afterAuto = () => {
|
|
37
|
+
if (!point) return
|
|
38
|
+
const r = e.vp.getBoundingClientRect()
|
|
39
|
+
const x = Math.min(r.right - 2, Math.max(r.left + 2, point.x))
|
|
40
|
+
const y = Math.min(r.bottom - 2, Math.max(r.top + 2, point.y))
|
|
41
|
+
const sel = getSelection()
|
|
42
|
+
const at = caretAt(x, y)
|
|
43
|
+
if (sel && at && sel.rangeCount && e.vp.contains(at.offsetNode))
|
|
44
|
+
sel.extend(at.offsetNode, at.offset)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const move = (ev: PointerEvent) => {
|
|
48
|
+
if (!(ev.buttons & 1)) return up()
|
|
49
|
+
const sel = getSelection()
|
|
50
|
+
if (!sel || sel.isCollapsed) return (set('x', 0), set('y', 0))
|
|
51
|
+
const r = e.vp.getBoundingClientRect()
|
|
52
|
+
point = { x: ev.clientX, y: ev.clientY }
|
|
53
|
+
set('x', autoSpeed(outside(ev.clientX, r.left, r.right)))
|
|
54
|
+
set('y', autoSpeed(outside(ev.clientY, r.top, r.bottom)))
|
|
55
|
+
}
|
|
56
|
+
const up = () => {
|
|
57
|
+
halt()
|
|
58
|
+
removeEventListener('pointermove', move)
|
|
59
|
+
removeEventListener('pointerup', up)
|
|
60
|
+
}
|
|
61
|
+
const down = (ev: PointerEvent) => {
|
|
62
|
+
if (ev.pointerType !== 'mouse' || ev.button !== 0 || e.o.grab) return
|
|
63
|
+
addEventListener('pointermove', move)
|
|
64
|
+
addEventListener('pointerup', up)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let dragTimer: ReturnType<typeof setTimeout> | undefined
|
|
68
|
+
const over = (ev: DragEvent) => {
|
|
69
|
+
const r = e.vp.getBoundingClientRect()
|
|
70
|
+
const band = Math.min(40, r.height / 4, r.width / 4)
|
|
71
|
+
const edge = (p: number, lo: number, hi: number) =>
|
|
72
|
+
p < lo + band ? p - lo - band : p > hi - band ? p - hi + band : 0
|
|
73
|
+
point = null
|
|
74
|
+
set('x', autoSpeed(edge(ev.clientX, r.left, r.right)))
|
|
75
|
+
set('y', autoSpeed(edge(ev.clientY, r.top, r.bottom)))
|
|
76
|
+
clearTimeout(dragTimer)
|
|
77
|
+
dragTimer = setTimeout(halt, 150)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
e.vp.addEventListener('pointerdown', down)
|
|
81
|
+
e.vp.addEventListener('dragover', over)
|
|
82
|
+
e.vp.addEventListener('drop', halt)
|
|
83
|
+
return () => {
|
|
84
|
+
clearTimeout(dragTimer)
|
|
85
|
+
up()
|
|
86
|
+
e.afterAuto = null
|
|
87
|
+
e.vp.removeEventListener('pointerdown', down)
|
|
88
|
+
e.vp.removeEventListener('dragover', over)
|
|
89
|
+
e.vp.removeEventListener('drop', halt)
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/axis.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// One axis of a scroller: where it is, how far it can go, what is animating it, and its bar's geometry.
|
|
2
|
+
import type { Axis, Easing } from './types'
|
|
3
|
+
|
|
4
|
+
export type Anim =
|
|
5
|
+
| { kind: 'tween'; from: number; to: number; start: number; dur: number; ease: Easing }
|
|
6
|
+
| { kind: 'coast'; v: number }
|
|
7
|
+
| { kind: 'spring'; x0: number; v0: number; start: number }
|
|
8
|
+
| { kind: 'auto'; v: number }
|
|
9
|
+
|
|
10
|
+
/** One axis: positions are physical (`scrollLeft` runs negative in right-to-left content). */
|
|
11
|
+
export interface AxisState {
|
|
12
|
+
axis: Axis
|
|
13
|
+
on: boolean
|
|
14
|
+
pos: number
|
|
15
|
+
written: number
|
|
16
|
+
min: number
|
|
17
|
+
max: number
|
|
18
|
+
view: number
|
|
19
|
+
total: number
|
|
20
|
+
over: number
|
|
21
|
+
v: number
|
|
22
|
+
anim: Anim | null
|
|
23
|
+
track: HTMLElement | null
|
|
24
|
+
trackLen: number
|
|
25
|
+
trackCross: number
|
|
26
|
+
thumbOff: number
|
|
27
|
+
thumbLen: number
|
|
28
|
+
grow: number
|
|
29
|
+
hover: boolean
|
|
30
|
+
active: boolean
|
|
31
|
+
drawn: string
|
|
32
|
+
aria: number
|
|
33
|
+
points: number[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const axisState = (axis: Axis): AxisState => ({
|
|
37
|
+
axis,
|
|
38
|
+
on: true,
|
|
39
|
+
pos: 0,
|
|
40
|
+
written: 0,
|
|
41
|
+
min: 0,
|
|
42
|
+
max: 0,
|
|
43
|
+
view: 0,
|
|
44
|
+
total: 0,
|
|
45
|
+
over: 0,
|
|
46
|
+
v: 0,
|
|
47
|
+
anim: null,
|
|
48
|
+
track: null,
|
|
49
|
+
trackLen: 0,
|
|
50
|
+
trackCross: 0,
|
|
51
|
+
thumbOff: 0,
|
|
52
|
+
thumbLen: 0,
|
|
53
|
+
grow: 0,
|
|
54
|
+
hover: false,
|
|
55
|
+
active: false,
|
|
56
|
+
drawn: '',
|
|
57
|
+
aria: -1,
|
|
58
|
+
points: [],
|
|
59
|
+
})
|
package/src/bar-input.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Pointer input on the bars: drag the thumb, press the track to page (held, it repeats until the thumb reaches the
|
|
2
|
+
// pointer) or to jump, and hover. Hit-testing uses the engine's geometry, so a canvas bar works the same as a DOM one.
|
|
3
|
+
import { dragRatio, progressAt } from './bar'
|
|
4
|
+
import type { AxisState, Engine } from './engine'
|
|
5
|
+
import type { Visibility } from './render'
|
|
6
|
+
import { pageStep } from './wheel'
|
|
7
|
+
|
|
8
|
+
/** Listen on each bar's track. Returns the cleanup. */
|
|
9
|
+
export function barInput(e: Engine, vis: Visibility) {
|
|
10
|
+
const offs: (() => void)[] = []
|
|
11
|
+
for (const a of e.axes) {
|
|
12
|
+
const track = a.track
|
|
13
|
+
if (!track) continue
|
|
14
|
+
const along = (ev: PointerEvent) => {
|
|
15
|
+
const r = track.getBoundingClientRect()
|
|
16
|
+
return a.axis === 'y' ? ev.clientY - r.top : ev.clientX - r.left
|
|
17
|
+
}
|
|
18
|
+
const hover = (on: boolean) => () => {
|
|
19
|
+
a.hover = on
|
|
20
|
+
vis.apply()
|
|
21
|
+
e.wake()
|
|
22
|
+
}
|
|
23
|
+
const enter = hover(true)
|
|
24
|
+
const leave = hover(false)
|
|
25
|
+
const down = (ev: PointerEvent) => {
|
|
26
|
+
if (ev.button !== 0) return
|
|
27
|
+
ev.preventDefault()
|
|
28
|
+
const at = along(ev)
|
|
29
|
+
const c = { e, vis, a, track, along }
|
|
30
|
+
if (at >= a.thumbOff && at <= a.thumbOff + a.thumbLen) drag(c, ev, at)
|
|
31
|
+
else press(c, ev, at)
|
|
32
|
+
}
|
|
33
|
+
track.addEventListener('pointerenter', enter)
|
|
34
|
+
track.addEventListener('pointerleave', leave)
|
|
35
|
+
track.addEventListener('pointerdown', down)
|
|
36
|
+
offs.push(() => {
|
|
37
|
+
track.removeEventListener('pointerenter', enter)
|
|
38
|
+
track.removeEventListener('pointerleave', leave)
|
|
39
|
+
track.removeEventListener('pointerdown', down)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
return () => offs.forEach((f) => f())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One bar's pointer handling: the engine, its axis, the track and where a pointer is along it. */
|
|
46
|
+
interface Bar {
|
|
47
|
+
e: Engine
|
|
48
|
+
vis: Visibility
|
|
49
|
+
a: AxisState
|
|
50
|
+
track: HTMLElement
|
|
51
|
+
along: (ev: PointerEvent) => number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function drag({ e, vis, a, track, along }: Bar, ev: PointerEvent, at: number) {
|
|
55
|
+
const start = a.pos
|
|
56
|
+
const ratio = dragRatio(a.trackLen, a.thumbLen, a.max - a.min)
|
|
57
|
+
track.setPointerCapture?.(ev.pointerId)
|
|
58
|
+
a.active = true
|
|
59
|
+
vis.apply()
|
|
60
|
+
const move = (m: PointerEvent) => e.moveTo(a.axis, start + (along(m) - at) * ratio, 'bar')
|
|
61
|
+
const up = () => {
|
|
62
|
+
a.active = false
|
|
63
|
+
vis.apply()
|
|
64
|
+
e.settle(a.axis)
|
|
65
|
+
e.wake()
|
|
66
|
+
track.removeEventListener('pointermove', move)
|
|
67
|
+
track.removeEventListener('pointerup', up)
|
|
68
|
+
track.removeEventListener('pointercancel', up)
|
|
69
|
+
}
|
|
70
|
+
track.addEventListener('pointermove', move)
|
|
71
|
+
track.addEventListener('pointerup', up)
|
|
72
|
+
track.addEventListener('pointercancel', up)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function press({ e, a, track, along }: Bar, ev: PointerEvent, at: number) {
|
|
76
|
+
const jump = (e.o.bar.trackClick === 'jump') !== ev.altKey
|
|
77
|
+
if (jump) {
|
|
78
|
+
const p = progressAt(a.trackLen, a.thumbLen, at - e.o.bar.inset)
|
|
79
|
+
return e.tweenTo(a.axis, a.min + p * (a.max - a.min), 'bar')
|
|
80
|
+
}
|
|
81
|
+
let pointer = at
|
|
82
|
+
const dir = at < a.thumbOff ? -1 : 1
|
|
83
|
+
const page = () => {
|
|
84
|
+
const before = pointer < a.thumbOff ? -1 : pointer > a.thumbOff + a.thumbLen ? 1 : 0
|
|
85
|
+
if (before === dir) e.tweenBy(a.axis, dir * pageStep(a.view), 'bar')
|
|
86
|
+
else stop()
|
|
87
|
+
}
|
|
88
|
+
let timer: ReturnType<typeof setTimeout> = setTimeout(function repeat() {
|
|
89
|
+
page()
|
|
90
|
+
timer = setTimeout(repeat, 60)
|
|
91
|
+
}, 350)
|
|
92
|
+
track.setPointerCapture?.(ev.pointerId)
|
|
93
|
+
const move = (m: PointerEvent) => (pointer = along(m))
|
|
94
|
+
const stop = () => {
|
|
95
|
+
clearTimeout(timer)
|
|
96
|
+
track.removeEventListener('pointermove', move)
|
|
97
|
+
track.removeEventListener('pointerup', stop)
|
|
98
|
+
track.removeEventListener('pointercancel', stop)
|
|
99
|
+
}
|
|
100
|
+
track.addEventListener('pointermove', move)
|
|
101
|
+
track.addEventListener('pointerup', stop)
|
|
102
|
+
track.addEventListener('pointercancel', stop)
|
|
103
|
+
e.tweenBy(a.axis, dir * pageStep(a.view), 'bar')
|
|
104
|
+
}
|
package/src/bar.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Scrollbar geometry: how long the thumb is and where it sits, and the inverse for a drag. Pure.
|
|
2
|
+
import type { ThumbOptions } from './types'
|
|
3
|
+
|
|
4
|
+
/** The thumb's length for a `track` px track over a `view` px window into `total` px of content. */
|
|
5
|
+
export function thumbLength(track: number, view: number, total: number, o: ThumbOptions = {}) {
|
|
6
|
+
if (track <= 0) return 0
|
|
7
|
+
if (o.size !== undefined) return Math.min(track, Math.max(0, o.size))
|
|
8
|
+
const min = Math.min(track, o.min ?? 24)
|
|
9
|
+
const max = Math.min(track, o.max ?? track)
|
|
10
|
+
const proportional = total > 0 ? (track * view) / total : track
|
|
11
|
+
return Math.max(min, Math.min(max, proportional))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The thumb's offset along the track for scroll `progress` 0..1. */
|
|
15
|
+
export function thumbOffset(track: number, length: number, progress: number) {
|
|
16
|
+
return Math.max(0, track - length) * Math.min(1, Math.max(0, progress))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** How many px of scroll one px of thumb drag moves. */
|
|
20
|
+
export function dragRatio(track: number, length: number, max: number) {
|
|
21
|
+
const room = track - length
|
|
22
|
+
return room > 0 ? max / room : 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The progress that centres the thumb on `at` px along the track: where a jump-click lands. */
|
|
26
|
+
export function progressAt(track: number, length: number, at: number) {
|
|
27
|
+
const room = track - length
|
|
28
|
+
return room > 0 ? Math.min(1, Math.max(0, (at - length / 2) / room)) : 0
|
|
29
|
+
}
|
package/src/canvas.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The canvas renderer: each bar paints into its own canvas, sized to the track at the device's pixel ratio. Colours
|
|
2
|
+
// may be any CSS colour, `color-mix()` and `currentColor` included: they are resolved through the track's style.
|
|
3
|
+
import { BAR_VARS } from './options'
|
|
4
|
+
import type { BarFrame, Renderer } from './types'
|
|
5
|
+
|
|
6
|
+
/** Paints one bar. `paint` is the default look, for a draw that adds to it rather than replacing it. */
|
|
7
|
+
export type DrawBar = (
|
|
8
|
+
ctx: CanvasRenderingContext2D,
|
|
9
|
+
frame: BarFrame,
|
|
10
|
+
paint: (ctx: CanvasRenderingContext2D, frame: BarFrame) => void,
|
|
11
|
+
) => void
|
|
12
|
+
|
|
13
|
+
/** Options for the canvas renderer. */
|
|
14
|
+
export interface CanvasOptions {
|
|
15
|
+
/** Your own painting. Default: rounded track and thumb in the bar's colours. */
|
|
16
|
+
draw?: DrawBar
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Painted {
|
|
20
|
+
canvas: HTMLCanvasElement
|
|
21
|
+
ctx: CanvasRenderingContext2D
|
|
22
|
+
bar: BarFrame['bar'] | null
|
|
23
|
+
dark: boolean
|
|
24
|
+
colors: BarFrame['bar'] | null
|
|
25
|
+
}
|
|
26
|
+
const canvases = new WeakMap<HTMLElement, Painted>()
|
|
27
|
+
const COLORS = ['color', 'hoverColor', 'activeColor', 'track', 'hoverTrack'] as const
|
|
28
|
+
let scheme: MediaQueryList | null | undefined
|
|
29
|
+
const dark = () =>
|
|
30
|
+
!!(scheme ??=
|
|
31
|
+
typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null)?.matches
|
|
32
|
+
|
|
33
|
+
/** A CSS colour as the canvas can take it: `color-mix()`, `currentColor` and `var()` resolve against `el`. */
|
|
34
|
+
export function cssColor(el: HTMLElement, css: string) {
|
|
35
|
+
const probe = el.ownerDocument.createElement('i')
|
|
36
|
+
probe.style.cssText = `position:absolute;display:none`
|
|
37
|
+
probe.style.color = css
|
|
38
|
+
el.append(probe)
|
|
39
|
+
const out = getComputedStyle(probe).color || css
|
|
40
|
+
probe.remove()
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The bar's colours resolved once per options and colour scheme; the stylesheet's custom properties win. */
|
|
45
|
+
function colors(track: HTMLElement, p: Painted, bar: BarFrame['bar']) {
|
|
46
|
+
if (p.colors && p.bar === bar && p.dark === dark()) return p.colors
|
|
47
|
+
const out = { ...bar }
|
|
48
|
+
for (const k of COLORS) out[k] = cssColor(track, `var(${BAR_VARS[k]}, ${bar[k]})`)
|
|
49
|
+
p.bar = bar
|
|
50
|
+
p.dark = dark()
|
|
51
|
+
p.colors = out
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The default look: the track (when it has a colour) and a rounded thumb that thickens on hover. */
|
|
56
|
+
export function paintBar(ctx: CanvasRenderingContext2D, f: BarFrame) {
|
|
57
|
+
const { bar, thumb } = f
|
|
58
|
+
ctx.fillStyle = f.hover || f.active ? bar.hoverTrack : bar.track
|
|
59
|
+
ctx.fillRect(0, 0, f.track.width, f.track.height)
|
|
60
|
+
ctx.fillStyle = f.active ? bar.activeColor : f.hover ? bar.hoverColor : bar.color
|
|
61
|
+
const r = bar.radius * (Math.min(thumb.width, thumb.height) / bar.width)
|
|
62
|
+
ctx.beginPath()
|
|
63
|
+
ctx.roundRect(
|
|
64
|
+
thumb.x,
|
|
65
|
+
thumb.y,
|
|
66
|
+
thumb.width,
|
|
67
|
+
thumb.height,
|
|
68
|
+
Math.max(0, Math.min(r, thumb.width / 2, thumb.height / 2)),
|
|
69
|
+
)
|
|
70
|
+
ctx.fill()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A renderer that paints the bars on canvas. Pass it as `renderer`. */
|
|
74
|
+
export function canvas(o: CanvasOptions = {}): Renderer {
|
|
75
|
+
return {
|
|
76
|
+
animates: true,
|
|
77
|
+
mount(track) {
|
|
78
|
+
const c = document.createElement('canvas')
|
|
79
|
+
c.dataset.part = 'canvas'
|
|
80
|
+
c.setAttribute('aria-hidden', 'true')
|
|
81
|
+
c.style.cssText =
|
|
82
|
+
'position:absolute;inset:0;inline-size:100%;block-size:100%;pointer-events:none'
|
|
83
|
+
track.querySelector('[data-part="thumb"]')?.setAttribute('hidden', '')
|
|
84
|
+
track.append(c)
|
|
85
|
+
const ctx = c.getContext('2d')
|
|
86
|
+
if (ctx) canvases.set(track, { canvas: c, ctx, bar: null, dark: false, colors: null })
|
|
87
|
+
},
|
|
88
|
+
draw(track, f) {
|
|
89
|
+
const got = canvases.get(track)
|
|
90
|
+
if (!got) return
|
|
91
|
+
const { canvas: c, ctx } = got
|
|
92
|
+
const frame = { ...f, bar: colors(track, got, f.bar) }
|
|
93
|
+
const dpr = globalThis.devicePixelRatio || 1
|
|
94
|
+
const w = Math.round(f.track.width * dpr)
|
|
95
|
+
const h = Math.round(f.track.height * dpr)
|
|
96
|
+
if (c.width !== w || c.height !== h) {
|
|
97
|
+
c.width = w
|
|
98
|
+
c.height = h
|
|
99
|
+
}
|
|
100
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
101
|
+
ctx.clearRect(0, 0, f.track.width, f.track.height)
|
|
102
|
+
if (o.draw) o.draw(ctx, frame, paintBar)
|
|
103
|
+
else paintBar(ctx, frame)
|
|
104
|
+
},
|
|
105
|
+
unmount(track) {
|
|
106
|
+
canvases.get(track)?.canvas.remove()
|
|
107
|
+
canvases.delete(track)
|
|
108
|
+
track.querySelector('[data-part="thumb"]')?.removeAttribute('hidden')
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|