mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
Flatten workspace (#120)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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)}%`;
|
||||
}
|
||||
Reference in New Issue
Block a user