Buckets:
| type Point = { x: number; y: number } | |
| export function createAim(props: { | |
| enabled: () => boolean | |
| active: () => string | undefined | |
| el: () => HTMLElement | undefined | |
| onActivate: (id: string) => void | |
| delay?: number | |
| max?: number | |
| tolerance?: number | |
| edge?: number | |
| }) { | |
| const state = { | |
| locs: [] as Point[], | |
| timer: undefined as number | undefined, | |
| pending: undefined as string | undefined, | |
| over: undefined as string | undefined, | |
| last: undefined as Point | undefined, | |
| } | |
| const delay = props.delay ?? 250 | |
| const max = props.max ?? 4 | |
| const tolerance = props.tolerance ?? 80 | |
| const edge = props.edge ?? 18 | |
| const cancel = () => { | |
| if (state.timer !== undefined) clearTimeout(state.timer) | |
| state.timer = undefined | |
| state.pending = undefined | |
| } | |
| const reset = () => { | |
| cancel() | |
| state.over = undefined | |
| state.last = undefined | |
| state.locs.length = 0 | |
| } | |
| const move = (event: MouseEvent) => { | |
| if (!props.enabled()) return | |
| const el = props.el() | |
| if (!el) return | |
| const rect = el.getBoundingClientRect() | |
| const x = event.clientX | |
| const y = event.clientY | |
| if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return | |
| state.locs.push({ x, y }) | |
| if (state.locs.length > max) state.locs.shift() | |
| } | |
| const wait = () => { | |
| if (!props.enabled()) return 0 | |
| if (!props.active()) return 0 | |
| const el = props.el() | |
| if (!el) return 0 | |
| if (state.locs.length < 2) return 0 | |
| const rect = el.getBoundingClientRect() | |
| const loc = state.locs[state.locs.length - 1] | |
| if (!loc) return 0 | |
| const prev = state.locs[0] ?? loc | |
| if (prev.x < rect.left || prev.x > rect.right || prev.y < rect.top || prev.y > rect.bottom) return 0 | |
| if (state.last && loc.x === state.last.x && loc.y === state.last.y) return 0 | |
| if (rect.right - loc.x <= edge) { | |
| state.last = loc | |
| return delay | |
| } | |
| const upper = { x: rect.right, y: rect.top - tolerance } | |
| const lower = { x: rect.right, y: rect.bottom + tolerance } | |
| const slope = (a: Point, b: Point) => (b.y - a.y) / (b.x - a.x) | |
| const decreasing = slope(loc, upper) | |
| const increasing = slope(loc, lower) | |
| const prevDecreasing = slope(prev, upper) | |
| const prevIncreasing = slope(prev, lower) | |
| if (decreasing < prevDecreasing && increasing > prevIncreasing) { | |
| state.last = loc | |
| return delay | |
| } | |
| state.last = undefined | |
| return 0 | |
| } | |
| const activate = (id: string) => { | |
| cancel() | |
| props.onActivate(id) | |
| } | |
| const request = (id: string) => { | |
| if (!id) return | |
| if (props.active() === id) return | |
| if (!props.active()) { | |
| activate(id) | |
| return | |
| } | |
| const ms = wait() | |
| if (ms === 0) { | |
| activate(id) | |
| return | |
| } | |
| cancel() | |
| state.pending = id | |
| state.timer = window.setTimeout(() => { | |
| state.timer = undefined | |
| if (state.pending !== id) return | |
| state.pending = undefined | |
| if (!props.enabled()) return | |
| if (!props.active()) return | |
| if (state.over !== id) return | |
| props.onActivate(id) | |
| }, ms) | |
| } | |
| const enter = (id: string, event: MouseEvent) => { | |
| if (!props.enabled()) return | |
| state.over = id | |
| move(event) | |
| request(id) | |
| } | |
| const leave = (id: string) => { | |
| if (state.over === id) state.over = undefined | |
| if (state.pending === id) cancel() | |
| } | |
| return { move, enter, leave, activate, request, cancel, reset } | |
| } | |
Xet Storage Details
- Size:
- 3.44 kB
- Xet hash:
- 9921fff0721509ac5ae46f1910efad14100f6e6a04696d236a566b76f7033bfd
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.