Flatten workspace (#120)

This commit is contained in:
Rahim
2025-10-23 15:19:36 -07:00
committed by GitHub
parent e41f883aea
commit 1ad0bc436f
215 changed files with 804 additions and 6445 deletions
+202
View File
@@ -0,0 +1,202 @@
import { shallowEqual } from '@vjs-10/utils';
import { map } from 'nanostores';
export interface SliderState {
_trackElement: HTMLElement | null;
_pointerRatio: number;
_hovering: boolean;
_dragging: boolean;
_keying: boolean;
_fillWidth: number;
_pointerWidth: number;
_stepSize: number;
}
export class Slider {
#element: HTMLElement | null = null;
#abortController: AbortController | null = null;
#state = map<SliderState>({
_trackElement: null,
_pointerRatio: 0,
_hovering: false,
_dragging: false,
_keying: false,
_fillWidth: 0,
_pointerWidth: 0,
_stepSize: 0.01,
});
attach(target: HTMLElement): void {
this.#element = target;
this.#abortController = new AbortController();
const { signal } = this.#abortController;
this.#element.addEventListener('pointerdown', this, { signal });
this.#element.addEventListener('pointermove', this, { signal });
this.#element.addEventListener('pointerup', this, { signal });
this.#element.addEventListener('pointerenter', this, { signal });
this.#element.addEventListener('pointerleave', this, { signal });
this.#element.addEventListener('keydown', this, { signal });
this.#element.addEventListener('keyup', this, { signal });
}
detach(): void {
this.#element = null;
this.#abortController?.abort();
this.#abortController = null;
}
subscribe(callback: (state: SliderState) => void): () => void {
return this.#state.subscribe(callback);
}
setState(state: Partial<SliderState>): void {
if (shallowEqual(state, this.#state.get())) return;
this.#state.set({ ...this.#state.get(), ...state });
}
getState(): SliderState {
const state = this.#state.get();
let _pointerWidth = 0;
if (state._hovering) {
_pointerWidth = state._pointerRatio;
}
return { ...state, _pointerWidth };
}
handleEvent(event: Event): void {
const { type } = event;
switch (type) {
case 'pointerdown':
this.#handlePointerDown(event as PointerEvent);
break;
case 'pointermove':
this.#handlePointerMove(event as PointerEvent);
break;
case 'pointerup':
this.#handlePointerUp(event as PointerEvent);
break;
case 'pointerenter':
this.#handlePointerEnter(event as PointerEvent);
break;
case 'pointerleave':
this.#handlePointerLeave(event as PointerEvent);
break;
case 'keydown':
this.#handleKeyDown(event as KeyboardEvent);
break;
case 'keyup':
this.#handleKeyUp(event as KeyboardEvent);
break;
}
}
getPointerRatio(evt: PointerEvent): number {
const { _trackElement } = this.#state.get();
if (!_trackElement) return 0;
const rect = _trackElement.getBoundingClientRect();
return getPointProgressOnLine(
evt.clientX,
evt.clientY,
{ x: rect.left, y: rect.bottom },
{ x: rect.right, y: rect.top },
);
}
#handlePointerDown(event: PointerEvent) {
this.#element?.setPointerCapture(event.pointerId);
this.setState({ _pointerRatio: this.getPointerRatio(event), _dragging: true });
}
#handlePointerMove(event: PointerEvent) {
this.setState({ _pointerRatio: this.getPointerRatio(event) });
}
#handlePointerUp(event: PointerEvent) {
this.setState({ _pointerRatio: this.getPointerRatio(event), _dragging: false });
this.#element?.releasePointerCapture(event.pointerId);
}
#handlePointerEnter(_event: PointerEvent) {
this.setState({ _hovering: true });
}
#handlePointerLeave(_event: PointerEvent) {
this.setState({ _hovering: false });
}
#handleKeyDown(event: KeyboardEvent) {
const { key } = event;
const { _pointerRatio, _stepSize } = this.#state.get();
let newRatio = _pointerRatio;
switch (key) {
case 'ArrowLeft':
case 'ArrowDown':
event.preventDefault();
newRatio = Math.max(0, _pointerRatio - _stepSize);
break;
case 'ArrowRight':
case 'ArrowUp':
event.preventDefault();
newRatio = Math.min(1, _pointerRatio + _stepSize);
break;
case 'Home':
event.preventDefault();
newRatio = 0;
break;
case 'End':
event.preventDefault();
newRatio = 1;
break;
default:
return; // Don't update state for other keys
}
this.setState({ _pointerRatio: newRatio, _keying: true });
}
#handleKeyUp(_event: KeyboardEvent) {
this.setState({ _keying: false });
}
setStepSize(stepSize: number): void {
this.setState({ _stepSize: Math.max(0.001, Math.min(1, stepSize)) });
}
}
export interface Point {
x: number;
y: number;
}
/**
* Get progress ratio of a point on a line segment.
* @param x - The x coordinate of the point.
* @param y - The y coordinate of the point.
* @param p1 - The first point of the line segment.
* @param p2 - The second point of the line segment.
*/
export function getPointProgressOnLine(x: number, y: number, p1: Point, p2: Point): number {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const lengthSquared = dx * dx + dy * dy;
if (lengthSquared === 0) return 0; // Avoid division by zero if p1 === p2
const projection = ((x - p1.x) * dx + (y - p1.y) * dy) / lengthSquared;
return Math.max(0, Math.min(1, projection)); // Clamp between 0 and 1
}
export function distance(p1: Point, p2: Point): number {
return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
}
+123
View File
@@ -0,0 +1,123 @@
import type { SliderState } from './slider';
import { Slider } from './slider';
export interface TimeSliderState extends SliderState {
currentTime: number;
duration: number;
requestSeek: (time: number) => void;
requestPreview: (time: number) => void;
_currentTimeText: string;
_durationText: string;
}
export class TimeSlider extends Slider {
#seekingTime: number | null = null;
#oldCurrentTime: number | null = null;
getState(): TimeSliderState {
const state = super.getState() as TimeSliderState;
// When dragging or keying, use pointer position for immediate feedback;
// While seeking, use seeking time so it doesn't jump back to the current time;
// Otherwise, use current time;
let _fillWidth = 0;
if (state._dragging || state._keying) {
_fillWidth = state._pointerRatio * 100;
} else if (state.duration > 0) {
if (this.#seekingTime !== null && this.#oldCurrentTime === state.currentTime) {
_fillWidth = (this.#seekingTime / state.duration) * 100;
} else {
_fillWidth = (state.currentTime / state.duration) * 100;
this.#seekingTime = null;
}
}
this.#oldCurrentTime = state.currentTime;
const _currentTimeText = formatTime(state.currentTime);
const _durationText = formatTime(state.duration);
return { ...state, _fillWidth, _currentTimeText, _durationText };
}
setState(newState: Partial<TimeSliderState>): void {
const state = this.getState();
// When not dragging or keying, set pointer ratio to current time / duration.
if (!state._dragging && !state._keying && newState.currentTime && newState.duration) {
super.setState({ ...newState, _pointerRatio: newState.currentTime / newState.duration });
return;
}
super.setState(newState);
}
handleEvent(event: Event): void {
const { type } = event;
switch (type) {
case 'pointerdown':
this.#handlePointerDown(event as PointerEvent);
break;
case 'pointermove':
this.#handlePointerMove(event as PointerEvent);
break;
case 'pointerup':
this.#handlePointerUp(event as PointerEvent);
break;
case 'keydown':
this.#handleKeyDown(event as KeyboardEvent);
break;
default:
super.handleEvent(event);
break;
}
}
#handlePointerDown(event: PointerEvent) {
super.handleEvent(event);
const { _pointerRatio, duration, requestSeek } = super.getState() as TimeSliderState;
this.#seekingTime = _pointerRatio * duration;
requestSeek(this.#seekingTime);
}
#handlePointerMove(event: PointerEvent) {
super.handleEvent(event);
const { _dragging, _pointerRatio, duration, requestSeek, requestPreview } = super.getState() as TimeSliderState;
const previewTime = _pointerRatio * duration;
requestPreview(previewTime);
if (_dragging) {
this.#seekingTime = previewTime;
requestSeek(this.#seekingTime);
}
}
#handlePointerUp(event: PointerEvent) {
const { _dragging, _pointerRatio, duration, requestSeek } = super.getState() as TimeSliderState;
if (_dragging) {
this.#seekingTime = _pointerRatio * duration;
requestSeek(this.#seekingTime);
}
super.handleEvent(event);
}
#handleKeyDown(event: KeyboardEvent) {
super.handleEvent(event);
const { _pointerRatio, duration, requestSeek } = super.getState() as TimeSliderState;
this.#seekingTime = _pointerRatio * duration;
requestSeek(this.#seekingTime);
}
}
function formatTime(time: number): string {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
@@ -0,0 +1,104 @@
import type { SliderState } from './slider';
import { Slider } from './slider';
export interface VolumeSliderState extends SliderState {
volume: number;
muted: boolean;
volumeLevel: string;
requestVolumeChange: (volume: number) => void;
_volumeText: string;
}
export class VolumeSlider extends Slider {
constructor() {
super();
this.setStepSize(0.1);
}
getState(): VolumeSliderState {
const state = super.getState() as VolumeSliderState;
// When dragging or keying, use pointer position for immediate feedback;
// Otherwise, use current volume;
let _fillWidth = 0;
if (state._dragging || state._keying) {
_fillWidth = state._pointerRatio * 100;
} else {
_fillWidth = state.muted ? 0 : (state.volume || 0) * 100;
}
const _volumeText = formatVolume(state.muted ? 0 : state.volume || 0);
return { ...state, _fillWidth, _volumeText };
}
setState(newState: Partial<VolumeSliderState>): void {
const state = this.getState();
// When not dragging or keying, set pointer ratio to current volume.
if (!state._dragging && !state._keying && newState.volume) {
super.setState({ ...newState, _pointerRatio: newState.volume });
return;
}
super.setState(newState);
}
handleEvent(event: Event): void {
const { type } = event;
switch (type) {
case 'pointerdown':
this.#handlePointerDown(event as PointerEvent);
break;
case 'pointermove':
this.#handlePointerMove(event as PointerEvent);
break;
case 'pointerup':
this.#handlePointerUp(event as PointerEvent);
break;
case 'keydown':
this.#handleKeyDown(event as KeyboardEvent);
break;
default:
super.handleEvent(event);
break;
}
}
#handlePointerDown(event: PointerEvent) {
super.handleEvent(event);
const { _pointerRatio, requestVolumeChange } = super.getState() as VolumeSliderState;
requestVolumeChange(_pointerRatio);
}
#handlePointerMove(event: PointerEvent) {
super.handleEvent(event);
const { _dragging, _pointerRatio, requestVolumeChange } = super.getState() as VolumeSliderState;
if (_dragging) {
requestVolumeChange(_pointerRatio);
}
}
#handlePointerUp(event: PointerEvent) {
const { _dragging, _pointerRatio, requestVolumeChange } = super.getState() as VolumeSliderState;
if (_dragging) {
requestVolumeChange(_pointerRatio);
}
super.handleEvent(event);
}
#handleKeyDown(event: KeyboardEvent) {
super.handleEvent(event);
const { _pointerRatio, requestVolumeChange } = super.getState() as VolumeSliderState;
requestVolumeChange(_pointerRatio);
}
}
function formatVolume(volume: number): string {
return `${Math.round(volume * 100)}%`;
}