sure
This commit is contained in:
parent
ced0ac56f3
commit
d6a8f898d1
27 changed files with 760 additions and 363 deletions
|
|
@ -19,6 +19,8 @@ vi.hoisted(() => {
|
|||
const originalBrushSize = settings.brushSize;
|
||||
const originalSelectedColorIndex = settings.selectedColorIndex;
|
||||
const originalSpawnPerPixel = settings.spawnPerPixel;
|
||||
const originalStrokeSpawnSpreadBrushSizeMultiplier =
|
||||
settings.strokeSpawnSpreadBrushSizeMultiplier;
|
||||
|
||||
const createPopulation = () => {
|
||||
const pipeline = {
|
||||
|
|
@ -51,12 +53,16 @@ describe('AgentPopulation adaptive budget', () => {
|
|||
settings.brushSize = 1;
|
||||
settings.selectedColorIndex = 0;
|
||||
settings.spawnPerPixel = 1;
|
||||
settings.strokeSpawnSpreadBrushSizeMultiplier = 1;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
settings.brushSize = originalBrushSize;
|
||||
settings.selectedColorIndex = originalSelectedColorIndex;
|
||||
settings.spawnPerPixel = originalSpawnPerPixel;
|
||||
settings.strokeSpawnSpreadBrushSizeMultiplier =
|
||||
originalStrokeSpawnSpreadBrushSizeMultiplier;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('expands beyond the 1M start cap only when new agents arrive under healthy FPS', () => {
|
||||
|
|
@ -102,6 +108,25 @@ describe('AgentPopulation adaptive budget', () => {
|
|||
expect(population.activeAgentCount).toBe(maxAgentCount);
|
||||
});
|
||||
|
||||
it('scales stroke spawn spread by device pixel ratio', () => {
|
||||
settings.brushSize = 10;
|
||||
const writeAgents = vi.fn();
|
||||
const pipeline = {
|
||||
maxAgentCount: 10_000_000,
|
||||
writeAgents,
|
||||
resizeAgents: vi.fn(),
|
||||
compactAgents: vi.fn(),
|
||||
} as unknown as AgentGenerationPipeline;
|
||||
const population = new AgentPopulation(pipeline, 0, () => 2);
|
||||
vi.spyOn(Math, 'random').mockReturnValue(1);
|
||||
|
||||
population.spawnStrokeAgents(vec2.fromValues(0, 0), vec2.fromValues(0, 0));
|
||||
|
||||
const firstBatch = writeAgents.mock.calls[0][1] as Float32Array;
|
||||
expect(firstBatch[0]).toBe(10);
|
||||
expect(firstBatch[1]).toBe(10);
|
||||
});
|
||||
|
||||
it('decreases the cap and active count slowly when FPS falls below the threshold', () => {
|
||||
const population = createPopulation();
|
||||
setPopulationActiveCount(population, 1_000_000);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { vec2 } from 'gl-matrix';
|
|||
import { appConfig } from '../config';
|
||||
import { AGENT_FLOAT_COUNT } from '../pipelines/agents/agent-generation/agent';
|
||||
import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline';
|
||||
import { getSafeDevicePixelRatio } from '../pipelines/brush/brush-pipeline';
|
||||
import { settings } from '../settings';
|
||||
import { createIntroTitleAgents } from './intro-title-agents';
|
||||
|
||||
|
|
@ -19,7 +20,8 @@ export class AgentPopulation {
|
|||
|
||||
public constructor(
|
||||
private readonly pipeline: AgentGenerationPipeline,
|
||||
private readonly introSeed = Math.floor(Math.random() * 0xffffffff)
|
||||
private readonly introSeed = Math.floor(Math.random() * 0xffffffff),
|
||||
private readonly getDevicePixelRatio = () => 1
|
||||
) {
|
||||
this.adaptiveCap = this.clampAdaptiveCap(
|
||||
appConfig.simulation.budget.adaptiveCapInitial
|
||||
|
|
@ -121,7 +123,10 @@ export class AgentPopulation {
|
|||
baseAngle +
|
||||
(Math.random() - 0.5) * appConfig.simulation.stroke.angleJitterRadians;
|
||||
const base = i * AGENT_FLOAT_COUNT;
|
||||
const spread = settings.brushSize * settings.strokeSpawnSpreadBrushSizeMultiplier;
|
||||
const spread =
|
||||
settings.brushSize *
|
||||
getSafeDevicePixelRatio(this.getDevicePixelRatio()) *
|
||||
settings.strokeSpawnSpreadBrushSizeMultiplier;
|
||||
this.strokeAgentData[base] = x + (Math.random() - 0.5) * spread;
|
||||
this.strokeAgentData[base + 1] = y + (Math.random() - 0.5) * spread;
|
||||
this.strokeAgentData[base + 2] = angle;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { appConfig } from '../config';
|
||||
import { RenderPipeline } from '../pipelines/render/render-pipeline';
|
||||
import type { VibeId } from '../vibes';
|
||||
import {
|
||||
estimateExport4KMemory,
|
||||
getAspectFitExport4KDimensions,
|
||||
|
|
@ -15,7 +16,7 @@ interface Export4KRendererOptions {
|
|||
getSourceSize: () => { width: number; height: number };
|
||||
getColorTextureView: () => GPUTextureView;
|
||||
getSourceTextureView: () => GPUTextureView;
|
||||
getVibeId: () => string;
|
||||
getVibeId: () => VibeId;
|
||||
}
|
||||
|
||||
export class Export4KRenderer {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ interface FrameParameters extends RenderInputs {
|
|||
deltaTime: number;
|
||||
canvasSize: vec2;
|
||||
activeAgentCount: number;
|
||||
devicePixelRatio: number;
|
||||
introProgress: number;
|
||||
selectedColorIndex: number;
|
||||
isErasing: boolean;
|
||||
|
|
@ -99,6 +100,7 @@ export class GameLoopResources {
|
|||
deltaTime,
|
||||
canvasSize,
|
||||
activeAgentCount,
|
||||
devicePixelRatio,
|
||||
introProgress,
|
||||
selectedColorIndex,
|
||||
channelColors,
|
||||
|
|
@ -123,6 +125,7 @@ export class GameLoopResources {
|
|||
});
|
||||
this.brushPipeline.setParameters({
|
||||
...settings,
|
||||
devicePixelRatio,
|
||||
selectedColorIndex,
|
||||
});
|
||||
this.diffusionPipeline.setParameters(settings);
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ export default class GameLoop {
|
|||
this.toolbarContrastMonitor = new ToolbarContrastMonitor(canvas, ui.toolbar, device);
|
||||
this.agentPopulation = new AgentPopulation(
|
||||
this.resources.agentGenerationPipeline,
|
||||
this.seedValue
|
||||
this.seedValue,
|
||||
() => this.devicePixelRatio
|
||||
);
|
||||
this.agentPopulation.initializeIntroAgents(this.canvasSize);
|
||||
this.pointerInput = new GardenPointerInput({
|
||||
|
|
@ -155,7 +156,8 @@ export default class GameLoop {
|
|||
|
||||
const { channelColors, backgroundColor } = this.renderInputs.get();
|
||||
const introProgress = this.introPrompt.progress;
|
||||
const eraserPixelSize = settings.eraserSize * this.devicePixelRatio;
|
||||
const devicePixelRatio = this.devicePixelRatio;
|
||||
const eraserPixelSize = settings.eraserSize * devicePixelRatio;
|
||||
const isErasing = this.pointerInput.isEraseMode;
|
||||
const accentColor = channelColors[settings.selectedColorIndex] ?? channelColors[0];
|
||||
this.renderInputs.updateAccentColor(accentColor);
|
||||
|
|
@ -169,6 +171,7 @@ export default class GameLoop {
|
|||
deltaTime,
|
||||
canvasSize: this.canvasSize,
|
||||
activeAgentCount: this.agentPopulation.activeAgentCount,
|
||||
devicePixelRatio,
|
||||
introProgress,
|
||||
selectedColorIndex: settings.selectedColorIndex,
|
||||
isErasing,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,9 @@ const makeSwipePipeline = () => ({
|
|||
clearSwipes: vi.fn(),
|
||||
});
|
||||
|
||||
const createPointerInput = async () => {
|
||||
const createPointerInput = async ({
|
||||
devicePixelRatio = 1,
|
||||
}: { devicePixelRatio?: number } = {}) => {
|
||||
const { GardenPointerInput } = await import('./pointer-input');
|
||||
const { settings: runtimeSettings } = await import('../settings');
|
||||
const canvas = new FakeCanvas();
|
||||
|
|
@ -117,7 +119,7 @@ const createPointerInput = async () => {
|
|||
eraserAgentPipeline,
|
||||
eraserPreview,
|
||||
eraserTexturePipeline,
|
||||
getDevicePixelRatio: () => 1,
|
||||
getDevicePixelRatio: () => devicePixelRatio,
|
||||
getMirrorSegmentCount: () => 1,
|
||||
onEraseGestureEnded,
|
||||
onStartDrawing,
|
||||
|
|
@ -277,6 +279,30 @@ describe('GardenPointerInput drawing startup', () => {
|
|||
expect(toPoint(stroke.to)).toEqual([40, 50]);
|
||||
});
|
||||
|
||||
it('keeps pointer geometry in backing pixels on high-DPR canvases', async () => {
|
||||
const { audio, brushPipeline, canvas } = await createPointerInput({
|
||||
devicePixelRatio: 2,
|
||||
});
|
||||
|
||||
canvas.dispatchPointerEvent('pointerdown', {
|
||||
clientX: 10,
|
||||
clientY: 20,
|
||||
pointerId: 9,
|
||||
timeStamp: 100,
|
||||
});
|
||||
canvas.dispatchPointerEvent('pointermove', {
|
||||
clientX: 40,
|
||||
clientY: 50,
|
||||
pointerId: 9,
|
||||
timeStamp: 150,
|
||||
});
|
||||
|
||||
const firstStroke = audio.stroke.mock.calls[0][0];
|
||||
expect(toPoint(firstStroke.from)).toEqual([20, 40]);
|
||||
expect(toPoint(firstStroke.to)).toEqual([80, 100]);
|
||||
expect(toPoint(brushPipeline.addSwipeSegment.mock.calls[0][0])).toEqual([20, 40]);
|
||||
});
|
||||
|
||||
it('caps curve tessellation with the brush curve resolution setting', async () => {
|
||||
const { brushPipeline, canvas, runtimeSettings } = await createPointerInput();
|
||||
runtimeSettings.brushCurveResolution = 2;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { vec2 } from 'gl-matrix';
|
|||
|
||||
import { GardenAudio } from '../audio/garden-audio';
|
||||
import { appConfig } from '../config';
|
||||
import { BrushPipeline } from '../pipelines/brush/brush-pipeline';
|
||||
import {
|
||||
BrushPipeline,
|
||||
getSafeDevicePixelRatio,
|
||||
} 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';
|
||||
|
|
@ -201,7 +204,9 @@ export class GardenPointerInput {
|
|||
|
||||
private getCanvasPointerPosition(event: PointerEvent): vec2 {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const devicePixelRatio = this.options.getDevicePixelRatio();
|
||||
const devicePixelRatio = getSafeDevicePixelRatio(
|
||||
this.options.getDevicePixelRatio()
|
||||
);
|
||||
return vec2.fromValues(
|
||||
(event.clientX - rect.left) * devicePixelRatio,
|
||||
(event.clientY - rect.top) * devicePixelRatio
|
||||
|
|
@ -213,7 +218,8 @@ export class GardenPointerInput {
|
|||
this.smoothedStrokePoints[this.smoothedStrokePoints.length - 1];
|
||||
if (
|
||||
previousSample !== undefined &&
|
||||
vec2.squaredDistance(previousSample, position) <= getBrushSmoothingDistanceSquared()
|
||||
vec2.squaredDistance(previousSample, position) <=
|
||||
getBrushSmoothingDistanceSquared(this.options.getDevicePixelRatio())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -247,12 +253,13 @@ export class GardenPointerInput {
|
|||
|
||||
private addQuadraticBrushSegments(start: vec2, control: vec2, end: vec2): void {
|
||||
const curveLength = vec2.distance(start, control) + vec2.distance(control, end);
|
||||
const devicePixelRatio = getSafeDevicePixelRatio(this.options.getDevicePixelRatio());
|
||||
const brushRadius = Math.max(
|
||||
settings.brushCurveMinBrushRadius,
|
||||
settings.brushSize / 2
|
||||
settings.brushCurveMinBrushRadius * devicePixelRatio,
|
||||
(settings.brushSize * devicePixelRatio) / 2
|
||||
);
|
||||
const segmentSpacing = Math.max(
|
||||
settings.brushCurveMinSegmentSpacing,
|
||||
settings.brushCurveMinSegmentSpacing * devicePixelRatio,
|
||||
brushRadius * settings.brushCurveSegmentBrushRadiusRatio
|
||||
);
|
||||
const mirrorSegmentCount = Math.max(1, this.options.getMirrorSegmentCount());
|
||||
|
|
@ -292,7 +299,7 @@ export class GardenPointerInput {
|
|||
if (
|
||||
this.lastSmoothedBrushPosition !== null &&
|
||||
vec2.squaredDistance(this.lastSmoothedBrushPosition, finalSample) >
|
||||
getBrushSmoothingDistanceSquared()
|
||||
getBrushSmoothingDistanceSquared(this.options.getDevicePixelRatio())
|
||||
) {
|
||||
this.addMirroredBrushSegment(this.lastSmoothedBrushPosition, finalSample);
|
||||
}
|
||||
|
|
@ -374,11 +381,11 @@ const getBrushCurveResolution = (): number => {
|
|||
return Math.max(1, Math.floor(resolution));
|
||||
};
|
||||
|
||||
const getBrushSmoothingDistanceSquared = (): number => {
|
||||
const getBrushSmoothingDistanceSquared = (devicePixelRatio?: number): number => {
|
||||
const distance = Number.isFinite(settings.brushSmoothingMinSampleDistance)
|
||||
? settings.brushSmoothingMinSampleDistance
|
||||
: appConfig.defaultSettings.brushSmoothingMinSampleDistance;
|
||||
return Math.max(0, distance) ** 2;
|
||||
return Math.max(0, distance * getSafeDevicePixelRatio(devicePixelRatio)) ** 2;
|
||||
};
|
||||
|
||||
const isSamePointerSample = (left: PointerEvent, right: PointerEvent): boolean =>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { activeVibe } from '../settings';
|
||||
import { hexToRgb } from '../vibes';
|
||||
import { hexToRgb, type VibeId } from '../vibes';
|
||||
import { RenderInputs } from './game-loop-types';
|
||||
|
||||
export class RenderInputCache {
|
||||
private cachedVibeId: string | null = null;
|
||||
private cachedVibeId: VibeId | null = null;
|
||||
private cachedRenderInputs?: RenderInputs;
|
||||
private previousAccentColor = '';
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue