fix(effects): снизить лаги сцены на Mac
Pixi режется по contentRect, пульт идёт в preview (12 FPS, 0.5 DPR), штампы тумана/огня/дождя прореживаются, draft темноты и маркера не чаще 20 Гц. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
FIELD_STAMP_MAX_SPRITES,
|
||||
decimateFieldStampPoints,
|
||||
fieldStampMinSpacingN,
|
||||
} from './fieldEffectStampPoints';
|
||||
import type { NPoint } from './types/effects';
|
||||
|
||||
function pt(x: number, y: number, tMs = 0): NPoint {
|
||||
return { x, y, tMs };
|
||||
}
|
||||
|
||||
void test('decimateFieldStampPoints: пустой и одна точка', () => {
|
||||
assert.deepEqual(decimateFieldStampPoints([], 0.05), []);
|
||||
const one = [pt(0.2, 0.3)];
|
||||
assert.deepEqual(decimateFieldStampPoints(one, 0.05), one);
|
||||
});
|
||||
|
||||
void test('decimateFieldStampPoints: оставляет концы и выкидывает близкие середины', () => {
|
||||
const points = [pt(0, 0.5), pt(0.01, 0.5), pt(0.02, 0.5), pt(0.5, 0.5), pt(0.51, 0.5), pt(1, 0.5)];
|
||||
const out = decimateFieldStampPoints(points, 0.1);
|
||||
assert.equal(out[0]?.x, 0);
|
||||
assert.equal(out[out.length - 1]?.x, 1);
|
||||
assert.ok(out.length < points.length);
|
||||
assert.ok(out.length >= 3);
|
||||
});
|
||||
|
||||
void test('decimateFieldStampPoints: maxPoints режет длинный штрих', () => {
|
||||
const points: NPoint[] = [];
|
||||
for (let i = 0; i <= 200; i += 1) {
|
||||
points.push(pt(i / 200, 0.4, i));
|
||||
}
|
||||
const out = decimateFieldStampPoints(points, 0.001, 16);
|
||||
assert.ok(out.length <= 16);
|
||||
assert.equal(out[0]?.x, 0);
|
||||
assert.equal(out[out.length - 1]?.x, 1);
|
||||
});
|
||||
|
||||
void test('fieldStampMinSpacingN: огонь реже тумана', () => {
|
||||
const r = 0.04;
|
||||
assert.ok(fieldStampMinSpacingN('fire', r) > fieldStampMinSpacingN('fog', r));
|
||||
assert.ok(fieldStampMinSpacingN('fog', r) >= 0.006);
|
||||
assert.equal(FIELD_STAMP_MAX_SPRITES, 40);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { NPoint } from './types/effects';
|
||||
|
||||
/** Верхняя граница спрайтов на один штрих тумана/огня/дождя. */
|
||||
export const FIELD_STAMP_MAX_SPRITES = 40;
|
||||
|
||||
/**
|
||||
* Минимальное расстояние между центрами спрайтов (норм. 0..1).
|
||||
* Огонь крупнее пятна радиуса — реже ставим штампы.
|
||||
*/
|
||||
export function fieldStampMinSpacingN(type: 'fog' | 'fire' | 'rain', radiusN: number): number {
|
||||
const factor = type === 'fire' ? 0.75 : type === 'rain' ? 0.55 : 0.5;
|
||||
return Math.max(0.006, radiusN * factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Оставляет концы штриха и точки не ближе `minSpacingN`.
|
||||
* Если точек всё ещё больше `maxPoints` — равномерный stride.
|
||||
*/
|
||||
export function decimateFieldStampPoints(
|
||||
points: readonly NPoint[],
|
||||
minSpacingN: number,
|
||||
maxPoints: number = FIELD_STAMP_MAX_SPRITES,
|
||||
): NPoint[] {
|
||||
if (points.length <= 1) return points.slice();
|
||||
const first = points[0];
|
||||
const last = points[points.length - 1];
|
||||
if (!first || !last) {
|
||||
return points.filter((p): p is NPoint => Boolean(p));
|
||||
}
|
||||
|
||||
const cap = Math.max(2, Math.floor(maxPoints));
|
||||
const minDistSq = Math.max(0, minSpacingN) * Math.max(0, minSpacingN);
|
||||
const kept: NPoint[] = [first];
|
||||
for (let i = 1; i < points.length - 1; i += 1) {
|
||||
const p = points[i];
|
||||
const prev = kept[kept.length - 1];
|
||||
if (!p || !prev) continue;
|
||||
const dx = p.x - prev.x;
|
||||
const dy = p.y - prev.y;
|
||||
if (dx * dx + dy * dy >= minDistSq) kept.push(p);
|
||||
}
|
||||
const tail = kept[kept.length - 1];
|
||||
if (!tail) {
|
||||
kept.push(last);
|
||||
} else {
|
||||
const dx = last.x - tail.x;
|
||||
const dy = last.y - tail.y;
|
||||
if (dx * dx + dy * dy > 1e-12) kept.push(last);
|
||||
}
|
||||
|
||||
if (kept.length <= cap) return kept;
|
||||
|
||||
const reduced: NPoint[] = [];
|
||||
const lastIdx = kept.length - 1;
|
||||
const stride = Math.max(1, Math.ceil(lastIdx / (cap - 1)));
|
||||
for (let i = 0; i < kept.length; i += stride) {
|
||||
const p = kept[i];
|
||||
if (p) reduced.push(p);
|
||||
}
|
||||
const reducedTail = reduced[reduced.length - 1];
|
||||
if (reducedTail !== last) reduced.push(last);
|
||||
if (reduced.length > cap) {
|
||||
const head = reduced[0];
|
||||
if (!head) return kept.slice(0, cap);
|
||||
return [head, ...reduced.slice(reduced.length - (cap - 1))];
|
||||
}
|
||||
return reduced;
|
||||
}
|
||||
Reference in New Issue
Block a user