diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 7f8717b16..3409bdecf 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -136,5 +136,17 @@ "src/ui/visualInput.test.mjs", "scripts/qa-visual-input.mjs", "src/ui/displayControls.js", - "src/ui/displayControls.test.mjs" + "src/ui/displayControls.test.mjs", + "src/ui/effects.js", + "src/ui/visualEffects.js", + "src/ui/visualPresets.js", + "src/ui/visualEffects.test.mjs", + "src/bloom.js", + "src/styles/retro.js", + "src/styles/surveillance.js", + "src/styles/thermal.js", + "src/styles/anime.js", + "src/styles/noir.js", + "src/styles/snow.js", + "scripts/qa-visual-effects.mjs" ] diff --git a/scripts/qa-visual-effects.mjs b/scripts/qa-visual-effects.mjs index b2a5fe656..e51e5b01f 100644 --- a/scripts/qa-visual-effects.mjs +++ b/scripts/qa-visual-effects.mjs @@ -3,7 +3,15 @@ import fs from 'node:fs'; import puppeteer from 'puppeteer'; -const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', ...(process.platform === 'darwin' ? ['--use-angle=metal', '--enable-gpu'] : ['--use-gl=angle', '--use-angle=swiftshader'])] }); +const browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + ...(process.platform === 'darwin' + ? ['--use-angle=metal', '--enable-gpu'] + : ['--use-gl=angle', '--use-angle=swiftshader']), + ], +}); const page = await browser.newPage(); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); @@ -17,67 +25,165 @@ try { const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173'); url.searchParams.set('welcome', '0'); await page.goto(url.href, { waitUntil: 'domcontentloaded' }); - await page.waitForFunction(() => window.__godsEyeView?.styleManager?._visualEffects && document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60_000 }); + await page.waitForFunction( + () => + window.__godsEyeView?.styleManager?._visualEffects && + document.getElementById('loading-screen')?.classList.contains('hidden'), + { timeout: 60_000 }, + ); await page.evaluate(() => { const viewer = window.__godsEyeView.viewer; viewer.camera.cancelFlight?.(); viewer.scene.tweens?.removeAll?.(); viewer.camera.setView({ - destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({ longitude: -97.7431 * Math.PI / 180, latitude: 30.2568 * Math.PI / 180, height: 1200 }), + destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({ + longitude: (-97.7431 * Math.PI) / 180, + latitude: (30.2568 * Math.PI) / 180, + height: 1200, + }), orientation: { heading: 0, pitch: -0.85, roll: 0 }, }); viewer.scene.requestRender(); }); - await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); - const settled = await page.waitForFunction(() => { - const primitives = window.__godsEyeView.viewer.scene.primitives; - for (let i = 0; i < primitives.length; i++) { - const primitive = primitives.get(i); - if (typeof primitive.tilesLoaded === 'boolean' && !primitive.tilesLoaded) return false; - } - return true; - }, { timeout: 60_000 }).then(() => true, () => false); + await page.evaluate( + () => + new Promise((resolve) => + requestAnimationFrame(() => requestAnimationFrame(resolve)), + ), + ); + const settled = await page + .waitForFunction( + () => { + const primitives = window.__godsEyeView.viewer.scene.primitives; + for (let i = 0; i < primitives.length; i++) { + const primitive = primitives.get(i); + if ( + typeof primitive.tilesLoaded === 'boolean' && + !primitive.tilesLoaded + ) + return false; + } + return true; + }, + { timeout: 60_000 }, + ) + .then( + () => true, + () => false, + ); check('visible tile content settles before visual captures', settled); fs.mkdirSync('qa-shots/visual-effects', { recursive: true }); - for (const style of ['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', 'snow', 'normal']) { - await page.evaluate((name) => window.__godsEyeView.styleManager.setStyle(name), style); - await page.waitForFunction(() => window.__godsEyeView.styleManager.transitions.size === 0, { timeout: 10_000 }); + for (const style of [ + 'normal', + 'retro', + 'surveillance', + 'thermal', + 'anime', + 'noir', + 'snow', + 'normal', + ]) { + await page.evaluate( + (name) => window.__godsEyeView.styleManager.setStyle(name), + style, + ); + await page.waitForFunction( + () => window.__godsEyeView.styleManager.transitions.size === 0, + { timeout: 10_000 }, + ); const state = await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; const effects = manager._visualEffects; - const visible = Object.entries(effects.stages).filter(([, stage]) => stage.enabled).map(([name]) => name); - const animated = Object.values(effects.stages).some((stage) => stage.enabled && stage.uniforms.time !== undefined && stage.uniforms.intensity > 0.001); - return { active: manager.activeStyle, visible, clock: effects.frameId !== null, animated }; + const visible = Object.entries(effects.stages) + .filter(([, stage]) => stage.enabled) + .map(([name]) => name); + const animated = Object.values(effects.stages).some( + (stage) => + stage.enabled && + stage.uniforms.time !== undefined && + stage.uniforms.intensity > 0.001, + ); + return { + active: manager.activeStyle, + visible, + clock: effects.frameId !== null, + animated, + }; }); - check(`${style}: only the selected style remains visible`, state.active === style && JSON.stringify(state.visible) === JSON.stringify(style === 'normal' ? [] : [style])); - check(`${style}: the animation clock follows visible animated stages`, state.clock === state.animated); + check( + `${style}: only the selected style remains visible`, + state.active === style && + JSON.stringify(state.visible) === + JSON.stringify(style === 'normal' ? [] : [style]), + ); + check( + `${style}: the animation clock follows visible animated stages`, + state.clock === state.animated, + ); if (['normal', 'surveillance', 'thermal'].includes(style)) { await page.screenshot({ path: `qa-shots/visual-effects/${style}.png` }); - if (style === 'surveillance') await page.evaluate(() => window.__godsEyeView.viewer.camera.lookRight(0.2)); + if (style === 'surveillance') + await page.evaluate(() => + window.__godsEyeView.viewer.camera.lookRight(0.2), + ); } } await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; - manager.setStyle('noir'); manager.setStyle('retro'); manager.setStyle('normal'); + manager.setStyle('noir'); + manager.setStyle('retro'); + manager.setStyle('normal'); }); - await page.waitForFunction(() => window.__godsEyeView.styleManager.transitions.size === 0, { timeout: 10_000 }); - check('rapid switches settle to Normal without an old style or clock', await page.evaluate(() => { - const effects = window.__godsEyeView.styleManager._visualEffects; - return effects.frameId === null && Object.values(effects.stages).every((stage) => !stage.enabled); - })); - check('bloom actions, effect state and saved snapshot agree', await page.evaluate(() => { - const manager = window.__godsEyeView.styleManager; - const result = manager.setBloom({ enabled: true, intensityPct: 98 }); - const snapshot = manager.getVisualState(); - return result.ok && result.bloom.intensityPct === 98 && snapshot.bloom.intensity === 98 && manager._visualEffects.bloomIntensity === 98 && manager._bloomStage.enabled; - })); - check('sharpen actions, effect state and saved snapshot agree', await page.evaluate(() => { - const manager = window.__godsEyeView.styleManager; - const result = manager.setSharpen({ enabled: true, intensityPct: 61 }); - return result.ok && result.sharpen.intensityPct === 61 && manager.getVisualState().sharpen.intensity === 61 && manager._visualEffects.sharpenIntensity === 0.61 && Math.abs(manager._sharpenStage.uniforms.amount - 1.32) < 1e-9; - })); + await page.waitForFunction( + () => window.__godsEyeView.styleManager.transitions.size === 0, + { timeout: 10_000 }, + ); + check( + 'rapid switches settle to Normal without an old style or clock', + await page.evaluate(() => { + const effects = window.__godsEyeView.styleManager._visualEffects; + return ( + effects.frameId === null && + Object.values(effects.stages).every((stage) => !stage.enabled) + ); + }), + ); + check( + 'bloom actions, effect state and saved snapshot agree', + await page.evaluate(() => { + const manager = window.__godsEyeView.styleManager; + const result = manager.setBloom({ enabled: true, intensityPct: 98 }); + const snapshot = manager.getVisualState(); + return ( + result.ok && + result.bloom.intensityPct === 98 && + snapshot.bloom.intensity === 98 && + manager._visualEffects.bloomIntensity === 98 && + manager._bloomStage.enabled + ); + }), + ); + check( + 'sharpen actions, effect state and saved snapshot agree', + await page.evaluate(() => { + const manager = window.__godsEyeView.styleManager; + const result = manager.setSharpen({ enabled: true, intensityPct: 61 }); + return ( + result.ok && + result.sharpen.intensityPct === 61 && + manager.getVisualState().sharpen.intensity === 61 && + manager._visualEffects.sharpenIntensity === 0.61 && + Math.abs(manager._sharpenStage.uniforms.amount - 1.32) < 1e-9 + ); + }), + ); await page.setViewport({ width: 620, height: 900 }); - await page.evaluate(() => window.__godsEyeView.styleManager.setPanelCollapsed('pp-toggles', false, { persist: false, syncShare: false })); + await page.evaluate(() => + window.__godsEyeView.styleManager.setPanelCollapsed('pp-toggles', false, { + persist: false, + syncShare: false, + }), + ); await page.screenshot({ path: 'qa-shots/visual-effects/narrow.png' }); const teardown = await page.evaluate(async () => { const manager = window.__godsEyeView.styleManager; @@ -87,17 +193,33 @@ try { const collection = manager.viewer.scene.postProcessStages; const previousBloom = effects.previousBloom; const pending = manager.dispose(); - const stoppedBeforeAwait = effects.stopped && effects.frameId === null && effects.transitions.size === 0; + const stoppedBeforeAwait = + effects.stopped && + effects.frameId === null && + effects.transitions.size === 0; await pending; return { stoppedBeforeAwait, removed: owned.every((stage) => !collection.contains(stage)), - restored: effects.bloomStage.enabled === previousBloom.enabled && Object.entries(previousBloom.uniforms).every(([name, value]) => effects.bloomStage.uniforms[name] === value), + restored: + effects.bloomStage.enabled === previousBloom.enabled && + Object.entries(previousBloom.uniforms).every( + ([name, value]) => effects.bloomStage.uniforms[name] === value, + ), }; }); - check('UI teardown stops effect animation before its first await', teardown.stoppedBeforeAwait); - check('UI teardown removes owned stages and restores borrowed bloom', teardown.removed && teardown.restored); - check('effects do not introduce uncaught browser errors', errors.length === 0); + check( + 'UI teardown stops effect animation before its first await', + teardown.stoppedBeforeAwait, + ); + check( + 'UI teardown removes owned stages and restores borrowed bloom', + teardown.removed && teardown.restored, + ); + check( + 'effects do not introduce uncaught browser errors', + errors.length === 0, + ); if (errors.length) console.log(JSON.stringify(errors)); } finally { await browser.close(); diff --git a/src/bloom.js b/src/bloom.js index 297d93ee6..fba0472d2 100644 --- a/src/bloom.js +++ b/src/bloom.js @@ -43,7 +43,11 @@ export function clampBloomIntensity(value) { * @returns {number} equivalent v2 intensity */ export function legacyBloomToV2(value) { - const legacy = clamp(Math.round(Number(value) || 0), 0, LEGACY_BLOOM_INTENSITY_MAX); + const legacy = clamp( + Math.round(Number(value) || 0), + 0, + LEGACY_BLOOM_INTENSITY_MAX, + ); return clampBloomIntensity((LEGACY_BLOOM_INTENSITY_MAX - legacy) * 2); } @@ -59,7 +63,10 @@ export function decodeBloomIntensity(value, version = 1) { if (!Number.isFinite(num)) return BLOOM_INTENSITY_DEFAULT; const normalizedVersion = Number(version); - if (Number.isFinite(normalizedVersion) && normalizedVersion >= BLOOM_SCALE_VERSION) { + if ( + Number.isFinite(normalizedVersion) && + normalizedVersion >= BLOOM_SCALE_VERSION + ) { return clampBloomIntensity(num); } diff --git a/src/styles/surveillance.js b/src/styles/surveillance.js index 21a3cd190..8ab7eb914 100644 --- a/src/styles/surveillance.js +++ b/src/styles/surveillance.js @@ -13,7 +13,7 @@ export const nightVisionShader = { name: 'surveillance', uniforms: { gain: { default: 0.55, min: 0, max: 1, label: 'Gain' }, - bloom: { default: 0.30, min: 0, max: 1, label: 'Bloom' }, + bloom: { default: 0.3, min: 0, max: 1, label: 'Bloom' }, scanlineStr: { default: 1.0, min: 0, max: 1, label: 'Scanlines' }, pixelation: { default: 2.5, min: 1, max: 6, label: 'Pixelation' }, }, diff --git a/src/ui/effects.js b/src/ui/effects.js index d215f3500..87eda3212 100644 --- a/src/ui/effects.js +++ b/src/ui/effects.js @@ -1,2 +1,7 @@ export { VisualEffects } from './visualEffects.js'; -export { STYLES, GLOBAL_POST_DEFAULTS, STYLE_PRESET_DEFAULTS, MILITARY_DETECTION_PRESET } from './visualPresets.js'; +export { + STYLES, + GLOBAL_POST_DEFAULTS, + STYLE_PRESET_DEFAULTS, + MILITARY_DETECTION_PRESET, +} from './visualPresets.js'; diff --git a/src/ui/visualEffects.js b/src/ui/visualEffects.js index b0b54ba0b..6533bb748 100644 --- a/src/ui/visualEffects.js +++ b/src/ui/visualEffects.js @@ -1,6 +1,14 @@ import { PostProcessStage } from 'cesium'; -import { bloomStrengthFromIntensity, clampBloomIntensity, BLOOM_INTENSITY_DEFAULT } from '../bloom.js'; -import { STYLES, SHARPEN_SHADER, TRANSITION_DURATION_MS } from './visualPresets.js'; +import { + bloomStrengthFromIntensity, + clampBloomIntensity, + BLOOM_INTENSITY_DEFAULT, +} from '../bloom.js'; +import { + STYLES, + SHARPEN_SHADER, + TRANSITION_DURATION_MS, +} from './visualPresets.js'; /** Own the post-process stages and their animation, without DOM dependencies. */ export class VisualEffects { @@ -8,10 +16,15 @@ export class VisualEffects { * @param {object} options - Viewer, render ownership callbacks and optional clocks. * Construction is inert; initStyles/initPostProcess retain startup ordering. */ - constructor({ viewer, requestRender, holdRender, releaseRender, + constructor({ + viewer, + requestRender, + holdRender, + releaseRender, requestFrame = (callback) => requestAnimationFrame(callback), cancelFrame = (id) => cancelAnimationFrame(id), - now = () => performance.now(), wallNow = () => Date.now(), + now = () => performance.now(), + wallNow = () => Date.now(), createStage = (options) => new PostProcessStage(options), }) { this.viewer = viewer; @@ -43,9 +56,15 @@ export class VisualEffects { if (this.stopped || this.stageEntries.length) return; for (const [name, shader] of Object.entries(STYLES)) { const uniforms = { intensity: 0.0 }; - if (shader.fragmentShader.includes('uniform float time')) uniforms.time = 0.0; - for (const [name, meta] of Object.entries(shader.uniforms || {})) uniforms[name] = meta.default; - const stage = this.createStage({ name: `godsEyeView_${name}`, fragmentShader: shader.fragmentShader, uniforms }); + if (shader.fragmentShader.includes('uniform float time')) + uniforms.time = 0.0; + for (const [name, meta] of Object.entries(shader.uniforms || {})) + uniforms[name] = meta.default; + const stage = this.createStage({ + name: `godsEyeView_${name}`, + fragmentShader: shader.fragmentShader, + uniforms, + }); stage.enabled = false; this.viewer.scene.postProcessStages.add(stage); this.stages[name] = stage; @@ -56,11 +75,34 @@ export class VisualEffects { initPostProcess(sharpenIntensity = this.sharpenIntensity) { if (this.stopped || this.sharpenStage) return; this.bloomStage = this.viewer.scene.postProcessStages.bloom; - const names = ['glowOnly', 'contrast', 'brightness', 'delta', 'sigma', 'stepSize']; - this.previousBloom = { enabled: this.bloomStage.enabled, uniforms: Object.fromEntries(names.map((name) => [name, this.bloomStage.uniforms[name]])) }; + const names = [ + 'glowOnly', + 'contrast', + 'brightness', + 'delta', + 'sigma', + 'stepSize', + ]; + this.previousBloom = { + enabled: this.bloomStage.enabled, + uniforms: Object.fromEntries( + names.map((name) => [name, this.bloomStage.uniforms[name]]), + ), + }; this.bloomStage.enabled = false; - Object.assign(this.bloomStage.uniforms, { glowOnly: false, contrast: 256.0, brightness: -0.35, delta: 0.25, sigma: 0.35, stepSize: 1.0 }); - this.sharpenStage = this.createStage({ name: 'godsEyeView_sharpen', fragmentShader: SHARPEN_SHADER, uniforms: { amount: 1.3 } }); + Object.assign(this.bloomStage.uniforms, { + glowOnly: false, + contrast: 256.0, + brightness: -0.35, + delta: 0.25, + sigma: 0.35, + stepSize: 1.0, + }); + this.sharpenStage = this.createStage({ + name: 'godsEyeView_sharpen', + fragmentShader: SHARPEN_SHADER, + uniforms: { amount: 1.3 }, + }); this.sharpenStage.enabled = false; this.viewer.scene.postProcessStages.add(this.sharpenStage); this.applySharpenIntensity(sharpenIntensity); @@ -70,17 +112,21 @@ export class VisualEffects { if (this.stopped || !stage) return; stage.uniforms.intensity = value; stage.enabled = value > 0.001; - if (stage.enabled && stage.uniforms.time !== undefined) this.startAnimationLoop(); + if (stage.enabled && stage.uniforms.time !== undefined) + this.startAnimationLoop(); this.requestRender('style-stage'); } syncStagesEnabledFromIntensity() { - for (const [, stage] of this.stageEntries) this.setStageIntensity(stage, stage.uniforms.intensity); + for (const [, stage] of this.stageEntries) + this.setStageIntensity(stage, stage.uniforms.intensity); } syncBloomEnabled() { if (this.stopped || !this.bloomStage) return; - this.bloomStage.enabled = this.bloomEnabled && bloomStrengthFromIntensity(this.bloomIntensity) > 0.06; + this.bloomStage.enabled = + this.bloomEnabled && + bloomStrengthFromIntensity(this.bloomIntensity) > 0.06; } applyBloomIntensity(intensity) { @@ -89,7 +135,7 @@ export class VisualEffects { this.requestRender('bloom'); if (!this.bloomStage) return; const rawStrength = bloomStrengthFromIntensity(this.bloomIntensity); - const strength = rawStrength <= 0.06 ? 0.0 : ((rawStrength - 0.06) / 0.94); + const strength = rawStrength <= 0.06 ? 0.0 : (rawStrength - 0.06) / 0.94; const eased = strength * strength * (3.0 - 2.0 * strength); this.bloomStage.uniforms.contrast = 255.0 - eased * 168.0; this.bloomStage.uniforms.brightness = -0.5 + eased * 0.36; @@ -109,7 +155,8 @@ export class VisualEffects { applySharpenIntensity(value) { if (this.stopped) return; this.sharpenIntensity = value; - if (this.sharpenStage) this.sharpenStage.uniforms.amount = 0.1 + value * 2.0; + if (this.sharpenStage) + this.sharpenStage.uniforms.amount = 0.1 + value * 2.0; this.requestRender('sharpen'); } @@ -133,9 +180,15 @@ export class VisualEffects { const now = this.now(); const elapsedSec = (this.wallNow() - this.startTime) / 1000.0; for (const [name, transition] of this.transitions) { - const t = Math.min((now - transition.start) / TRANSITION_DURATION_MS, 1.0); + const t = Math.min( + (now - transition.start) / TRANSITION_DURATION_MS, + 1.0, + ); const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; - this.setStageIntensity(this.stages[name], transition.from + (transition.to - transition.from) * eased); + this.setStageIntensity( + this.stages[name], + transition.from + (transition.to - transition.from) * eased, + ); if (t >= 1.0) { this.setStageIntensity(this.stages[name], transition.to); this.transitions.delete(name); diff --git a/src/ui/visualEffects.test.mjs b/src/ui/visualEffects.test.mjs index f28288e52..76615d0e8 100644 --- a/src/ui/visualEffects.test.mjs +++ b/src/ui/visualEffects.test.mjs @@ -1,7 +1,11 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { VisualEffects } from './visualEffects.js'; -import { GLOBAL_POST_DEFAULTS, STYLE_PRESET_DEFAULTS, MILITARY_DETECTION_PRESET } from './visualPresets.js'; +import { + GLOBAL_POST_DEFAULTS, + STYLE_PRESET_DEFAULTS, + MILITARY_DETECTION_PRESET, +} from './visualPresets.js'; function fixture() { let time = 0; @@ -10,15 +14,40 @@ function fixture() { const stages = new Set(); const holds = new Set(); const requests = []; - const bloom = { enabled: true, uniforms: { glowOnly: true, contrast: 3, brightness: 4, delta: 5, sigma: 6, stepSize: 7 } }; + const bloom = { + enabled: true, + uniforms: { + glowOnly: true, + contrast: 3, + brightness: 4, + delta: 5, + sigma: 6, + stepSize: 7, + }, + }; const originalBloom = structuredClone(bloom); - const pipeline = { bloom, add(stage) { stages.add(stage); }, remove(stage) { return stages.delete(stage); } }; + const pipeline = { + bloom, + add(stage) { + stages.add(stage); + }, + remove(stage) { + return stages.delete(stage); + }, + }; const effects = new VisualEffects({ viewer: { scene: { postProcessStages: pipeline } }, requestRender: (reason) => requests.push(reason), - holdRender: (reason) => holds.add(reason), releaseRender: (reason) => holds.delete(reason), - requestFrame: (callback) => { const id = next++; frames.set(id, callback); return id; }, - cancelFrame: (id) => frames.delete(id), now: () => time, wallNow: () => 10_000 + time, + holdRender: (reason) => holds.add(reason), + releaseRender: (reason) => holds.delete(reason), + requestFrame: (callback) => { + const id = next++; + frames.set(id, callback); + return id; + }, + cancelFrame: (id) => frames.delete(id), + now: () => time, + wallNow: () => 10_000 + time, createStage: (options) => ({ ...options, enabled: true }), }); function tick(value) { @@ -27,7 +56,16 @@ function fixture() { frames.clear(); for (const callback of pending) callback(); } - return { effects, frames, stages, holds, requests, bloom, originalBloom, tick }; + return { + effects, + frames, + stages, + holds, + requests, + bloom, + originalBloom, + tick, + }; } test('construction is inert and initialization creates one owned pipeline', () => { @@ -37,7 +75,11 @@ test('construction is inert and initialization creates one owned pipeline', () = f.effects.initStyles(); f.effects.initPostProcess(); assert.equal(f.stages.size, 7); - assert.ok(Object.values(f.effects.stages).every((stage) => !stage.enabled && stage.uniforms.intensity === 0)); + assert.ok( + Object.values(f.effects.stages).every( + (stage) => !stage.enabled && stage.uniforms.intensity === 0, + ), + ); f.effects.initStyles(); f.effects.initPostProcess(); assert.equal(f.stages.size, 7); @@ -50,7 +92,11 @@ test('crossfade uses the established 500ms easing and settles without idle frame f.effects.startTransition('noir', 0, 1); assert.equal(f.effects.frameId, 0, 'zero is a valid pending animation id'); f.effects.startAnimationLoop(); - assert.equal(f.frames.size, 1, 'starting twice cannot schedule duplicate frames'); + assert.equal( + f.frames.size, + 1, + 'starting twice cannot schedule duplicate frames', + ); f.tick(125); assert.equal(f.effects.stages.noir.uniforms.intensity, 0.125); f.tick(250); @@ -69,7 +115,11 @@ test('a superseding transition starts from the current rendered intensity', () = f.effects.initStyles(); f.effects.startTransition('noir', 0, 1); f.tick(250); - f.effects.startTransition('noir', f.effects.stages.noir.uniforms.intensity, 0); + f.effects.startTransition( + 'noir', + f.effects.stages.noir.uniforms.intensity, + 0, + ); f.tick(500); assert.equal(f.effects.stages.noir.uniforms.intensity, 0.25); f.tick(750); @@ -147,7 +197,11 @@ test('stop revokes pending work immediately but retains stages until final destr const staleFrame = [...f.frames.values()][0]; f.effects.stop(); assert.equal(f.frames.size, 0); - assert.equal(f.stages.size, 7, 'Context and Cockpit may still be releasing these stages'); + assert.equal( + f.stages.size, + 7, + 'Context and Cockpit may still be releasing these stages', + ); const intensity = f.effects.stages.retro.uniforms.intensity; staleFrame(); f.effects.startTransition('retro', 0, 1); @@ -156,14 +210,21 @@ test('stop revokes pending work immediately but retains stages until final destr assert.equal(f.effects.stages.retro.uniforms.intensity, intensity); f.effects.destroy(); assert.equal(f.stages.size, 0); - assert.deepEqual(f.bloom, f.originalBloom, 'borrowed bloom configuration is restored'); + assert.deepEqual( + f.bloom, + f.originalBloom, + 'borrowed bloom configuration is restored', + ); f.effects.destroy(); }); test('destroying one instance does not remove another pipeline or clock', () => { const a = fixture(); const b = fixture(); - for (const f of [a, b]) { f.effects.initStyles(); f.effects.startTransition('retro', 0, 1); } + for (const f of [a, b]) { + f.effects.initStyles(); + f.effects.startTransition('retro', 0, 1); + } a.effects.destroy(); b.tick(250); assert.equal(a.stages.size, 0); @@ -179,7 +240,10 @@ test('existing baseline and military presets keep one detection default', () => assert.equal(GLOBAL_POST_DEFAULTS.detectionFadePct, 7); assert.equal(GLOBAL_POST_DEFAULTS.detectionOutsideOpacityPct, 1); for (const name of ['retro', 'surveillance', 'thermal']) { - assert.equal(STYLE_PRESET_DEFAULTS[name].detection, MILITARY_DETECTION_PRESET); + assert.equal( + STYLE_PRESET_DEFAULTS[name].detection, + MILITARY_DETECTION_PRESET, + ); } assert.equal(STYLE_PRESET_DEFAULTS.normal, undefined); }); diff --git a/src/ui/visualPresets.js b/src/ui/visualPresets.js index a1415efcc..197605faa 100644 --- a/src/ui/visualPresets.js +++ b/src/ui/visualPresets.js @@ -9,7 +9,14 @@ import { BLOOM_INTENSITY_DEFAULT } from '../bloom.js'; /** Duration (ms) for shader intensity crossfade between style presets. */ export const TRANSITION_DURATION_MS = 500; /** Map of style name to its GLSL shader module for post-process stages. */ -export const STYLES = { retro: retroShader, surveillance: nightVisionShader, thermal: thermalShader, anime: animeShader, noir: noirShader, snow: snowShader }; +export const STYLES = { + retro: retroShader, + surveillance: nightVisionShader, + thermal: thermalShader, + anime: animeShader, + noir: noirShader, + snow: snowShader, +}; /** * The tactical detection look: Dense at 75%. @@ -28,7 +35,10 @@ export const STYLES = { retro: retroShader, surveillance: nightVisionShader, the * SPARSE. Declared ahead of GLOBAL_POST_DEFAULTS because that baseline now reads * from it. */ -export const MILITARY_DETECTION_PRESET = Object.freeze({ mode: 'dense', densityPct: 75 }); +export const MILITARY_DETECTION_PRESET = Object.freeze({ + mode: 'dense', + densityPct: 75, +}); /** Baseline post-processing settings applied on first load (before share-link restore). */ export const GLOBAL_POST_DEFAULTS = {