Clean up
This commit is contained in:
parent
ea0304356f
commit
7c70f15e49
65 changed files with 1122 additions and 1906 deletions
|
|
@ -2,23 +2,17 @@ import { vec2 } from 'gl-matrix';
|
|||
|
||||
import { GardenAudio } from '../audio/garden-audio';
|
||||
import { appConfig } from '../config';
|
||||
import {
|
||||
BrushPipeline,
|
||||
getSafePixelRatio,
|
||||
} 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';
|
||||
import { getSafePixelRatio } from '../pipelines/brush/brush-pipeline';
|
||||
import { activeVibe } from '../settings';
|
||||
import { BrushStrokeSmoother } from './brush-stroke-smoother';
|
||||
import { type StrokeSegment } from './game-loop-types';
|
||||
import { getMirroredStrokeSegments } from './stroke-mirroring';
|
||||
import { type StrokeOutput } from './stroke-output';
|
||||
|
||||
interface GardenPointerInputOptions {
|
||||
canvas: HTMLCanvasElement;
|
||||
audio: GardenAudio;
|
||||
brushPipeline: BrushPipeline;
|
||||
eraserAgentPipeline: EraserAgentPipeline;
|
||||
eraserTexturePipeline: EraserTexturePipeline;
|
||||
eraserPreview: EraserPreview;
|
||||
strokeOutput: StrokeOutput;
|
||||
getCanvasPixelRatio: () => number;
|
||||
getMirrorSegmentCount: () => number;
|
||||
onStartDrawing: () => void;
|
||||
|
|
@ -26,19 +20,28 @@ interface GardenPointerInputOptions {
|
|||
spawnStrokeAgents: (from: vec2, to: vec2) => void;
|
||||
}
|
||||
|
||||
interface PointerSample {
|
||||
position: vec2;
|
||||
previousPosition: vec2;
|
||||
elapsedSeconds: number;
|
||||
timeStamp: number;
|
||||
}
|
||||
|
||||
export class GardenPointerInput {
|
||||
private readonly brushSmoother: BrushStrokeSmoother;
|
||||
private activePointerId: number | null = null;
|
||||
private lastPointerPosition: vec2 | null = null;
|
||||
private lastPointerEventTimeMs: number | null = null;
|
||||
private smoothedStrokePoints: Array<vec2> = [];
|
||||
private lastSmoothedBrushPosition: vec2 | null = null;
|
||||
private isErasing = false;
|
||||
|
||||
public constructor(private readonly options: GardenPointerInputOptions) {}
|
||||
public constructor(private readonly options: GardenPointerInputOptions) {
|
||||
this.brushSmoother = new BrushStrokeSmoother({
|
||||
getCanvasPixelRatio: options.getCanvasPixelRatio,
|
||||
getMirrorSegmentCount: options.getMirrorSegmentCount,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -46,8 +49,6 @@ export class GardenPointerInput {
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
@ -56,11 +57,6 @@ export class GardenPointerInput {
|
|||
|
||||
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 {
|
||||
|
|
@ -68,9 +64,7 @@ export class GardenPointerInput {
|
|||
return;
|
||||
}
|
||||
|
||||
this.options.brushPipeline.clearSwipes();
|
||||
this.options.eraserAgentPipeline.clearSwipes();
|
||||
this.options.eraserTexturePipeline.clearSwipes();
|
||||
this.options.strokeOutput.clearSwipes();
|
||||
}
|
||||
|
||||
public scaleLastPointerPosition(scale: vec2): void {
|
||||
|
|
@ -78,13 +72,7 @@ export class GardenPointerInput {
|
|||
vec2.mul(this.lastPointerPosition, this.lastPointerPosition, scale);
|
||||
}
|
||||
|
||||
this.smoothedStrokePoints.forEach((point) => {
|
||||
vec2.mul(point, point, scale);
|
||||
});
|
||||
|
||||
if (this.lastSmoothedBrushPosition !== null) {
|
||||
vec2.mul(this.lastSmoothedBrushPosition, this.lastSmoothedBrushPosition, scale);
|
||||
}
|
||||
this.brushSmoother.scale(scale);
|
||||
}
|
||||
|
||||
public get isSwipeActive(): boolean {
|
||||
|
|
@ -100,8 +88,6 @@ export class GardenPointerInput {
|
|||
}
|
||||
|
||||
private readonly onPointerDown = (event: PointerEvent) => {
|
||||
this.options.eraserPreview.setPointerHoveringCanvas(true);
|
||||
this.updateEraserPreview(event);
|
||||
if (this.activePointerId !== null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -111,17 +97,14 @@ export class GardenPointerInput {
|
|||
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.options.strokeOutput.clearSwipes();
|
||||
this.lastPointerPosition = null;
|
||||
this.lastPointerEventTimeMs = null;
|
||||
this.clearSmoothedStroke();
|
||||
this.brushSmoother.clear();
|
||||
this.addSwipeAt(event, { emitAudio: false });
|
||||
};
|
||||
|
||||
private readonly onPointerMove = (event: PointerEvent) => {
|
||||
this.updateEraserPreview(event);
|
||||
if (event.pointerId !== this.activePointerId) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -136,7 +119,7 @@ export class GardenPointerInput {
|
|||
}
|
||||
this.options.audio.start(activeVibe, { userGesture: true });
|
||||
this.addSwipeAt(event, { emitAudio: false });
|
||||
this.finishSmoothedStroke();
|
||||
this.finishBrushStroke();
|
||||
this.options.audio.endGesture();
|
||||
if (this.isErasing) {
|
||||
this.options.onEraseGestureEnded();
|
||||
|
|
@ -145,24 +128,27 @@ export class GardenPointerInput {
|
|||
this.activePointerId = null;
|
||||
this.lastPointerPosition = null;
|
||||
this.lastPointerEventTimeMs = null;
|
||||
this.clearSmoothedStroke();
|
||||
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();
|
||||
this.brushSmoother.clear();
|
||||
};
|
||||
|
||||
private addSwipeAt(event: PointerEvent, options: { emitAudio?: boolean } = {}): void {
|
||||
const sample = this.getPointerSample(event);
|
||||
|
||||
if (this.isErasing) {
|
||||
this.addEraseSample(sample);
|
||||
} else {
|
||||
this.addBrushSample(sample);
|
||||
}
|
||||
|
||||
if (options.emitAudio !== false) {
|
||||
this.emitStrokeAudio(sample);
|
||||
}
|
||||
|
||||
this.lastPointerPosition = sample.position;
|
||||
this.lastPointerEventTimeMs = sample.timeStamp;
|
||||
}
|
||||
|
||||
private getPointerSample(event: PointerEvent): PointerSample {
|
||||
const position = this.getCanvasPointerPosition(event);
|
||||
const previousPosition = this.lastPointerPosition ?? position;
|
||||
const previousTimeMs = this.lastPointerEventTimeMs ?? event.timeStamp;
|
||||
|
|
@ -171,33 +157,36 @@ export class GardenPointerInput {
|
|||
(event.timeStamp - previousTimeMs) / 1000
|
||||
);
|
||||
|
||||
const segments = this.isErasing
|
||||
? [{ from: previousPosition, to: position }]
|
||||
: this.getMirroredStrokeSegments(previousPosition, position);
|
||||
return {
|
||||
position,
|
||||
previousPosition,
|
||||
elapsedSeconds,
|
||||
timeStamp: event.timeStamp,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isErasing) {
|
||||
segments.forEach((segment) => {
|
||||
this.options.eraserAgentPipeline.addSwipeSegment();
|
||||
this.options.eraserTexturePipeline.addSwipeSegment(segment.from, segment.to);
|
||||
});
|
||||
} else {
|
||||
this.addSmoothedBrushSample(position);
|
||||
segments.forEach((segment) => {
|
||||
private addBrushSample(sample: PointerSample): void {
|
||||
this.addBrushSegments(this.brushSmoother.addSample(sample.position));
|
||||
this.getMirroredSegments(sample.previousPosition, sample.position).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.canvas.width, this.canvas.height],
|
||||
isErasing: this.isErasing,
|
||||
elapsedSeconds,
|
||||
});
|
||||
}
|
||||
this.lastPointerPosition = position;
|
||||
this.lastPointerEventTimeMs = event.timeStamp;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private addEraseSample(sample: PointerSample): void {
|
||||
this.options.strokeOutput.addEraseSegment(sample.previousPosition, sample.position);
|
||||
}
|
||||
|
||||
private emitStrokeAudio(sample: PointerSample): void {
|
||||
this.options.audio.stroke({
|
||||
vibe: activeVibe,
|
||||
from: sample.previousPosition,
|
||||
to: sample.position,
|
||||
canvasSize: [this.canvas.width, this.canvas.height],
|
||||
isErasing: this.isErasing,
|
||||
elapsedSeconds: sample.elapsedSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
private getCanvasPointerPosition(event: PointerEvent): vec2 {
|
||||
|
|
@ -210,103 +199,23 @@ export class GardenPointerInput {
|
|||
);
|
||||
}
|
||||
|
||||
private addSmoothedBrushSample(position: vec2): void {
|
||||
const previousSample =
|
||||
this.smoothedStrokePoints[this.smoothedStrokePoints.length - 1];
|
||||
if (
|
||||
previousSample !== undefined &&
|
||||
vec2.squaredDistance(previousSample, position) <=
|
||||
getBrushSmoothingDistanceSquared(this.options.getCanvasPixelRatio())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.smoothedStrokePoints.push(vec2.clone(position));
|
||||
|
||||
if (this.smoothedStrokePoints.length > 3) {
|
||||
this.smoothedStrokePoints.shift();
|
||||
}
|
||||
|
||||
if (this.smoothedStrokePoints.length === 1) {
|
||||
this.addMirroredBrushSegment(position, position);
|
||||
this.lastSmoothedBrushPosition = vec2.clone(position);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.smoothedStrokePoints.length === 2) {
|
||||
const [start, end] = this.smoothedStrokePoints;
|
||||
const midpoint = getMidpoint(start, end);
|
||||
this.addMirroredBrushSegment(start, midpoint);
|
||||
this.lastSmoothedBrushPosition = midpoint;
|
||||
return;
|
||||
}
|
||||
|
||||
const [start, control, end] = this.smoothedStrokePoints;
|
||||
const curveStart = getMidpoint(start, control);
|
||||
const curveEnd = getMidpoint(control, end);
|
||||
this.addQuadraticBrushSegments(curveStart, control, curveEnd);
|
||||
this.lastSmoothedBrushPosition = curveEnd;
|
||||
}
|
||||
|
||||
private addQuadraticBrushSegments(start: vec2, control: vec2, end: vec2): void {
|
||||
const curveLength = vec2.distance(start, control) + vec2.distance(control, end);
|
||||
const canvasPixelRatio = getSafePixelRatio(
|
||||
this.options.getCanvasPixelRatio()
|
||||
);
|
||||
const brushRadius = Math.max(
|
||||
settings.brushCurveMinBrushRadius * canvasPixelRatio,
|
||||
(settings.brushSize * canvasPixelRatio) / 2
|
||||
);
|
||||
const segmentSpacing = Math.max(
|
||||
settings.brushCurveMinSegmentSpacing * canvasPixelRatio,
|
||||
brushRadius * settings.brushCurveSegmentBrushRadiusRatio
|
||||
);
|
||||
const mirrorSegmentCount = Math.max(1, this.options.getMirrorSegmentCount());
|
||||
const curveResolution = getBrushCurveResolution();
|
||||
const maxCurveSegments = Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
curveResolution /
|
||||
Math.max(1, mirrorSegmentCount ** settings.brushCurveMirrorResolutionExponent)
|
||||
)
|
||||
);
|
||||
const segmentCount = Math.min(
|
||||
maxCurveSegments,
|
||||
Math.max(1, Math.ceil(curveLength / segmentSpacing))
|
||||
);
|
||||
|
||||
let previousPoint = start;
|
||||
for (let i = 1; i <= segmentCount; i++) {
|
||||
const point = getQuadraticPoint(start, control, end, i / segmentCount);
|
||||
this.addMirroredBrushSegment(previousPoint, point);
|
||||
previousPoint = point;
|
||||
}
|
||||
}
|
||||
|
||||
private addMirroredBrushSegment(from: vec2, to: vec2): void {
|
||||
this.getMirroredStrokeSegments(from, to).forEach((segment) => {
|
||||
this.options.brushPipeline.addSwipeSegment(segment.from, segment.to);
|
||||
private addBrushSegments(segments: Array<StrokeSegment>): void {
|
||||
segments.forEach((segment) => {
|
||||
this.getMirroredSegments(segment.from, segment.to).forEach((mirroredSegment) => {
|
||||
this.options.strokeOutput.addBrushSegment(
|
||||
mirroredSegment.from,
|
||||
mirroredSegment.to
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private finishSmoothedStroke(): void {
|
||||
if (this.isErasing || this.smoothedStrokePoints.length === 0) {
|
||||
private finishBrushStroke(): void {
|
||||
if (this.isErasing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const finalSample = this.smoothedStrokePoints[this.smoothedStrokePoints.length - 1];
|
||||
if (
|
||||
this.lastSmoothedBrushPosition !== null &&
|
||||
vec2.squaredDistance(this.lastSmoothedBrushPosition, finalSample) >
|
||||
getBrushSmoothingDistanceSquared(this.options.getCanvasPixelRatio())
|
||||
) {
|
||||
this.addMirroredBrushSegment(this.lastSmoothedBrushPosition, finalSample);
|
||||
}
|
||||
}
|
||||
|
||||
private clearSmoothedStroke(): void {
|
||||
this.smoothedStrokePoints.length = 0;
|
||||
this.lastSmoothedBrushPosition = null;
|
||||
this.addBrushSegments(this.brushSmoother.finish());
|
||||
}
|
||||
|
||||
private getCoalescedPointerEvents(event: PointerEvent): Array<PointerEvent> {
|
||||
|
|
@ -326,67 +235,16 @@ export class GardenPointerInput {
|
|||
: [...coalescedEvents, event];
|
||||
}
|
||||
|
||||
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 getMirroredSegments(from: vec2, to: vec2): Array<StrokeSegment> {
|
||||
return getMirroredStrokeSegments(
|
||||
from,
|
||||
to,
|
||||
vec2.fromValues(this.canvas.width, this.canvas.height),
|
||||
this.options.getMirrorSegmentCount()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
const getMidpoint = (from: vec2, to: vec2): vec2 =>
|
||||
vec2.fromValues((from[0] + to[0]) / 2, (from[1] + to[1]) / 2);
|
||||
|
||||
const getQuadraticPoint = (start: vec2, control: vec2, end: vec2, t: number): vec2 => {
|
||||
const inverseT = 1 - t;
|
||||
return vec2.fromValues(
|
||||
inverseT * inverseT * start[0] + 2 * inverseT * t * control[0] + t * t * end[0],
|
||||
inverseT * inverseT * start[1] + 2 * inverseT * t * control[1] + t * t * end[1]
|
||||
);
|
||||
};
|
||||
|
||||
const getBrushCurveResolution = (): number => {
|
||||
const resolution = Number.isFinite(settings.brushCurveResolution)
|
||||
? settings.brushCurveResolution
|
||||
: appConfig.defaultSettings.brushCurveResolution;
|
||||
return Math.max(1, Math.floor(resolution));
|
||||
};
|
||||
|
||||
const getBrushSmoothingDistanceSquared = (pixelRatio?: number): number => {
|
||||
const distance = Number.isFinite(settings.brushSmoothingMinSampleDistance)
|
||||
? settings.brushSmoothingMinSampleDistance
|
||||
: appConfig.defaultSettings.brushSmoothingMinSampleDistance;
|
||||
return Math.max(0, distance * getSafePixelRatio(pixelRatio)) ** 2;
|
||||
};
|
||||
|
||||
const isSamePointerSample = (left: PointerEvent, right: PointerEvent): boolean =>
|
||||
left.clientX === right.clientX &&
|
||||
left.clientY === right.clientY &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue