WIP
This commit is contained in:
parent
34ac200437
commit
39b0160064
136 changed files with 7129 additions and 1950 deletions
248
src/game-loop/pointer-input.ts
Normal file
248
src/game-loop/pointer-input.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { GardenAudio } from '../audio/garden-audio';
|
||||
import { gardenAudioConfig } from '../audio/garden-audio-config';
|
||||
import { appConfig } from '../config';
|
||||
import { BrushPipeline } from '../pipelines/brush/brush-pipeline';
|
||||
import { EraserAgentPipeline } from '../pipelines/eraser/eraser-agent-pipeline';
|
||||
import { EraserTexturePipeline } from '../pipelines/eraser/eraser-texture-pipeline';
|
||||
import { activeVibe, settings } from '../settings';
|
||||
import { EraserPreview } from './eraser-preview';
|
||||
import { StrokeSegment } from './game-loop-types';
|
||||
|
||||
interface GardenPointerInputOptions {
|
||||
canvas: HTMLCanvasElement;
|
||||
audio: GardenAudio;
|
||||
brushPipeline: BrushPipeline;
|
||||
eraserAgentPipeline: EraserAgentPipeline;
|
||||
eraserTexturePipeline: EraserTexturePipeline;
|
||||
eraserPreview: EraserPreview;
|
||||
getCanvasSize: () => vec2;
|
||||
getDevicePixelRatio: () => number;
|
||||
getMirrorSegmentCount: () => number;
|
||||
onStartDrawing: () => void;
|
||||
onEraseGestureEnded: () => void;
|
||||
spawnStrokeAgents: (from: vec2, to: vec2) => void;
|
||||
}
|
||||
|
||||
export class GardenPointerInput {
|
||||
private activePointerId: number | null = null;
|
||||
private lastPointerPosition: vec2 | null = null;
|
||||
private lastPointerEventTimeMs: number | null = null;
|
||||
private lastPointerPressure = 0.5;
|
||||
private isErasing = false;
|
||||
|
||||
public constructor(private readonly options: GardenPointerInputOptions) {}
|
||||
|
||||
public attach(): void {
|
||||
this.canvas.addEventListener('pointerenter', this.onPointerEnter);
|
||||
this.canvas.addEventListener('pointerleave', this.onPointerLeave);
|
||||
this.canvas.addEventListener('pointerdown', this.onPointerDown);
|
||||
this.canvas.addEventListener('pointermove', this.onPointerMove);
|
||||
this.canvas.addEventListener('pointerup', this.onPointerUp);
|
||||
this.canvas.addEventListener('pointercancel', this.onPointerUp);
|
||||
}
|
||||
|
||||
public detach(): void {
|
||||
this.canvas.removeEventListener('pointerenter', this.onPointerEnter);
|
||||
this.canvas.removeEventListener('pointerleave', this.onPointerLeave);
|
||||
this.canvas.removeEventListener('pointerdown', this.onPointerDown);
|
||||
this.canvas.removeEventListener('pointermove', this.onPointerMove);
|
||||
this.canvas.removeEventListener('pointerup', this.onPointerUp);
|
||||
this.canvas.removeEventListener('pointercancel', this.onPointerUp);
|
||||
}
|
||||
|
||||
public setEraseMode(isErasing: boolean): void {
|
||||
this.isErasing = isErasing;
|
||||
this.options.eraserPreview.setEraseMode(isErasing, this.isSwipeActive);
|
||||
}
|
||||
|
||||
public updateEraserPreview(event?: PointerEvent): void {
|
||||
this.options.eraserPreview.update(event, this.isSwipeActive);
|
||||
}
|
||||
|
||||
public clearSwipesIfIdle(): void {
|
||||
if (this.isSwipeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.options.brushPipeline.clearSwipes();
|
||||
this.options.eraserAgentPipeline.clearSwipes();
|
||||
this.options.eraserTexturePipeline.clearSwipes();
|
||||
}
|
||||
|
||||
public scaleLastPointerPosition(scale: vec2): void {
|
||||
if (this.lastPointerPosition !== null) {
|
||||
vec2.mul(this.lastPointerPosition, this.lastPointerPosition, scale);
|
||||
}
|
||||
}
|
||||
|
||||
public get isSwipeActive(): boolean {
|
||||
return this.activePointerId !== null;
|
||||
}
|
||||
|
||||
public get isEraseMode(): boolean {
|
||||
return this.isErasing;
|
||||
}
|
||||
|
||||
private get canvas(): HTMLCanvasElement {
|
||||
return this.options.canvas;
|
||||
}
|
||||
|
||||
private readonly onPointerDown = (event: PointerEvent) => {
|
||||
this.options.eraserPreview.setPointerHoveringCanvas(true);
|
||||
this.updateEraserPreview(event);
|
||||
if (this.activePointerId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.options.audio.start(activeVibe, { userGesture: event.isTrusted });
|
||||
this.options.audio.beginGesture();
|
||||
this.options.onStartDrawing();
|
||||
this.activePointerId = event.pointerId;
|
||||
this.canvas.setPointerCapture(event.pointerId);
|
||||
this.options.brushPipeline.clearSwipes();
|
||||
this.options.eraserAgentPipeline.clearSwipes();
|
||||
this.options.eraserTexturePipeline.clearSwipes();
|
||||
this.lastPointerPosition = null;
|
||||
this.lastPointerEventTimeMs = null;
|
||||
this.lastPointerPressure = this.getPointerPressure(event);
|
||||
this.addSwipeAt(event);
|
||||
};
|
||||
|
||||
private readonly onPointerMove = (event: PointerEvent) => {
|
||||
this.updateEraserPreview(event);
|
||||
if (event.pointerId !== this.activePointerId) {
|
||||
return;
|
||||
}
|
||||
this.addSwipeAt(event);
|
||||
};
|
||||
|
||||
private readonly onPointerUp = (event: PointerEvent) => {
|
||||
if (event.pointerId !== this.activePointerId) {
|
||||
return;
|
||||
}
|
||||
this.addSwipeAt(event, { emitAudio: false });
|
||||
this.options.audio.endGesture();
|
||||
if (this.isErasing) {
|
||||
this.options.onEraseGestureEnded();
|
||||
}
|
||||
this.canvas.releasePointerCapture(event.pointerId);
|
||||
this.activePointerId = null;
|
||||
this.lastPointerPosition = null;
|
||||
this.lastPointerEventTimeMs = null;
|
||||
this.options.eraserPreview.setPointerHoveringCanvas(
|
||||
this.options.eraserPreview.isPointerInsideCanvas(event)
|
||||
);
|
||||
this.updateEraserPreview(event);
|
||||
};
|
||||
|
||||
private readonly onPointerEnter = (event: PointerEvent) => {
|
||||
this.options.eraserPreview.setPointerHoveringCanvas(true);
|
||||
this.updateEraserPreview(event);
|
||||
};
|
||||
|
||||
private readonly onPointerLeave = () => {
|
||||
this.options.eraserPreview.setPointerHoveringCanvas(false);
|
||||
this.updateEraserPreview();
|
||||
};
|
||||
|
||||
private addSwipeAt(event: PointerEvent, options: { emitAudio?: boolean } = {}): void {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const devicePixelRatio = this.options.getDevicePixelRatio();
|
||||
const position = vec2.fromValues(
|
||||
(event.clientX - rect.left) * devicePixelRatio,
|
||||
(event.clientY - rect.top) * devicePixelRatio
|
||||
);
|
||||
const previousPosition = this.lastPointerPosition ?? position;
|
||||
const previousTimeMs = this.lastPointerEventTimeMs ?? event.timeStamp;
|
||||
const elapsedSeconds = Math.max(
|
||||
appConfig.deltaTime.minDeltaTimeSeconds,
|
||||
(event.timeStamp - previousTimeMs) / 1000
|
||||
);
|
||||
const distancePixels = vec2.distance(previousPosition, position);
|
||||
const velocityPixelsPerSecond = distancePixels / elapsedSeconds;
|
||||
const pressure = this.getPointerPressure(event);
|
||||
this.lastPointerPressure = pressure > 0 ? pressure : this.lastPointerPressure;
|
||||
|
||||
const segments = this.isErasing
|
||||
? [{ from: previousPosition, to: position }]
|
||||
: this.getMirroredStrokeSegments(previousPosition, position);
|
||||
|
||||
segments.forEach((segment) => {
|
||||
if (this.isErasing) {
|
||||
this.options.eraserAgentPipeline.addSwipeSegment(segment.from, segment.to);
|
||||
this.options.eraserTexturePipeline.addSwipeSegment(segment.from, segment.to);
|
||||
} else {
|
||||
this.options.brushPipeline.addSwipeSegment(segment.from, segment.to);
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.isErasing) {
|
||||
segments.forEach((segment) => {
|
||||
this.options.spawnStrokeAgents(segment.from, segment.to);
|
||||
});
|
||||
}
|
||||
if (options.emitAudio !== false) {
|
||||
this.options.audio.stroke({
|
||||
vibe: activeVibe,
|
||||
from: previousPosition,
|
||||
to: position,
|
||||
canvasSize: this.options.getCanvasSize(),
|
||||
colorIndex: settings.selectedColorIndex,
|
||||
isErasing: this.isErasing,
|
||||
pressure: pressure > 0 ? pressure : this.lastPointerPressure,
|
||||
velocityPixelsPerSecond,
|
||||
eraserSizePixels: settings.eraserSize * devicePixelRatio,
|
||||
pointerType: event.pointerType,
|
||||
});
|
||||
}
|
||||
this.lastPointerPosition = position;
|
||||
this.lastPointerEventTimeMs = event.timeStamp;
|
||||
}
|
||||
|
||||
private getMirroredStrokeSegments(from: vec2, to: vec2): Array<StrokeSegment> {
|
||||
const segmentCount = this.options.getMirrorSegmentCount();
|
||||
if (segmentCount <= 1) {
|
||||
return [{ from, to }];
|
||||
}
|
||||
|
||||
const center = vec2.fromValues(this.canvas.width / 2, this.canvas.height / 2);
|
||||
const angleStep = (Math.PI * 2) / segmentCount;
|
||||
const segments: Array<StrokeSegment> = [];
|
||||
for (let i = 0; i < segmentCount; i++) {
|
||||
const angle = angleStep * i;
|
||||
segments.push({
|
||||
from: rotatePointAround(from, center, angle),
|
||||
to: rotatePointAround(to, center, angle),
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private getPointerPressure(event: PointerEvent): number {
|
||||
if (Number.isFinite(event.pressure) && event.pressure > 0) {
|
||||
return Math.min(1, Math.max(0, event.pressure));
|
||||
}
|
||||
|
||||
return event.buttons > 0 || event.type === 'pointerdown'
|
||||
? gardenAudioConfig.input.pressureFallback
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
const rotatePointAround = (point: vec2, center: vec2, angle: number): vec2 => {
|
||||
if (angle === 0) {
|
||||
return point;
|
||||
}
|
||||
|
||||
const offsetX = point[0] - center[0];
|
||||
const offsetY = point[1] - center[1];
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
return vec2.fromValues(
|
||||
center[0] + offsetX * cos - offsetY * sin,
|
||||
center[1] + offsetX * sin + offsetY * cos
|
||||
);
|
||||
};
|
||||
Loading…
Reference in a new issue