force graph, palette

This commit is contained in:
Nikita
2024-08-30 16:19:29 +03:00
parent 9e89959dd4
commit 32352ca5f4
38 changed files with 1602 additions and 243 deletions

52
web/lib/utils/canvas.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* Resizes the canvas to match the size it is being displayed.
*
* @param canvas the canvas to resize
* @returns `true` if the canvas was resized
*/
export function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {
// Get the size the browser is displaying the canvas in device pixels.
let dpr = window.devicePixelRatio
let {width, height} = canvas.getBoundingClientRect()
let display_width = Math.round(width * dpr)
let display_height = Math.round(height * dpr)
let need_resize = canvas.width != display_width ||
canvas.height != display_height
if (need_resize) {
canvas.width = display_width
canvas.height = display_height
}
return need_resize
}
export interface CanvasResizeObserver {
/** Canvas was resized since last check. Set it to `false` to reset. */
resized: boolean
canvas: HTMLCanvasElement
observer: ResizeObserver
}
export function resize(observer: CanvasResizeObserver): boolean {
let resized = resizeCanvasToDisplaySize(observer.canvas)
observer.resized ||= resized
return resized
}
export function resizeObserver(canvas: HTMLCanvasElement): CanvasResizeObserver {
let cro: CanvasResizeObserver = {
resized: false,
canvas: canvas,
observer: null!,
}
cro.observer = new ResizeObserver(resize.bind(null, cro))
resize(cro)
cro.observer.observe(canvas)
return cro
}
export function clear(observer: CanvasResizeObserver): void {
observer.observer.disconnect()
}

148
web/lib/utils/schedule.ts Normal file
View File

@@ -0,0 +1,148 @@
export interface Scheduler<Args extends unknown[]> {
trigger: (...args: Args) => void,
clear: () => void,
}
/**
* Creates a callback that is debounced and cancellable. The debounced callback is called on **trailing** edge.
*
* @param callback The callback to debounce
* @param wait The duration to debounce in milliseconds
*
* @example
* ```ts
* const debounce = schedule.debounce((message: string) => console.log(message), 250)
* debounce.trigger('Hello!')
* debounce.clear() // clears a timeout in progress
* ```
*/
export function debounce<Args extends unknown[]>(
callback: (...args: Args) => void,
wait?: number,
): Debounce<Args> {
return new Debounce(callback, wait)
}
export class Debounce<Args extends unknown[]> implements Scheduler<Args> {
timeout_id: ReturnType<typeof setTimeout> | undefined
constructor(
public callback: (...args: Args) => void,
public wait?: number
) {}
trigger(...args: Args): void {
if (this.timeout_id !== undefined) {
this.clear()
}
this.timeout_id = setTimeout(() => {
this.callback(...args)
}, this.wait)
}
clear(): void {
clearTimeout(this.timeout_id)
}
}
/**
* Creates a callback that is throttled and cancellable. The throttled callback is called on **trailing** edge.
*
* @param callback The callback to throttle
* @param wait The duration to throttle
*
* @example
* ```ts
* const throttle = schedule.throttle((val: string) => console.log(val), 250)
* throttle.trigger('my-new-value')
* throttle.clear() // clears a timeout in progress
* ```
*/
export function throttle<Args extends unknown[]>(
callback: (...args: Args) => void,
wait?: number,
): Throttle<Args> {
return new Throttle(callback, wait)
}
export class Throttle<Args extends unknown[]> implements Scheduler<Args> {
is_throttled = false
timeout_id: ReturnType<typeof setTimeout> | undefined
last_args: Args | undefined
constructor(
public callback: (...args: Args) => void,
public wait?: number
) {}
trigger(...args: Args): void {
this.last_args = args
if (this.is_throttled) {
return
}
this.is_throttled = true
this.timeout_id = setTimeout(() => {
this.callback(...this.last_args as Args)
this.is_throttled = false
}, this.wait)
}
clear(): void {
clearTimeout(this.timeout_id)
this.is_throttled = false
}
}
/**
* Creates a callback throttled using `window.requestIdleCallback()`. ([MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback))
*
* The throttled callback is called on **trailing** edge.
*
* @param callback The callback to throttle
* @param max_wait maximum wait time in milliseconds until the callback is called
*
* @example
* ```ts
* const idle = schedule.scheduleIdle((val: string) => console.log(val), 250)
* idle.trigger('my-new-value')
* idle.clear() // clears a timeout in progress
* ```
*/
export function scheduleIdle<Args extends unknown[]>(
callback: (...args: Args) => void,
max_wait?: number,
): ScheduleIdle<Args> | Throttle<Args> {
return typeof requestIdleCallback == "function"
? new ScheduleIdle(callback, max_wait)
: new Throttle(callback)
}
export class ScheduleIdle<Args extends unknown[]> implements Scheduler<Args> {
is_deferred = false
request_id: ReturnType<typeof requestIdleCallback> | undefined
last_args: Args | undefined
constructor(
public callback: (...args: Args) => void,
public max_wait?: number,
) {}
trigger(...args: Args): void {
this.last_args = args
if (this.is_deferred) {
return
}
this.is_deferred = true
this.request_id = requestIdleCallback(() => {
this.callback(...this.last_args as Args)
this.is_deferred = false
}, {timeout: this.max_wait})
}
clear(): void {
if (this.request_id != undefined) {
cancelIdleCallback(this.request_id)
}
this.is_deferred = false
}
}

View File

@@ -0,0 +1,28 @@
import * as react from "react"
export type WindowSize = {
width: number,
height: number,
}
export function getWindowSize(): WindowSize {
return {
width: window.innerWidth,
height: window.innerHeight,
}
}
export function useWindowSize(): WindowSize {
let [window_size, setWindowSize] = react.useState(getWindowSize())
react.useEffect(() => {
function handleResize() {
setWindowSize(getWindowSize())
}
window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize)
}, [])
return window_size
}