Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/Web/fx-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const POINTER_ID = 1;
const DEFAULT_COLOR = '#2dafff';
const DEFAULT_INPUT_SAMPLING_RATE = 40;
const EFFECT_HOST_ID = 'baspark-fx-host';
const HOST_GENERATION =
typeof window.__basparkRendererGeneration === 'string'
? window.__basparkRendererGeneration
Expand All @@ -23,6 +24,7 @@
const state =
{
fx: null,
outputHost: null,
initialized: false,
paused: false,
inputMode: 'mouse',
Expand Down Expand Up @@ -194,8 +196,22 @@
state.fx.setThemeColor(state.color);
}

function applyOutputOpacity()
{
if (!state.outputHost)
{
return;
}

// 全局透明度必须在 HDR 与 Bloom 完成后衰减,否则高能拖尾在低值下
// 仍会越过 Bloom 阈值,导致用户几乎看不出透明度变化。
state.outputHost.style.opacity = String(state.settings.opacity);
}

function applyEffectSettings()
{
applyOutputOpacity();

if (!state.fx)
{
return;
Expand All @@ -205,7 +221,8 @@
{
// 旧引擎以 1.5 为默认尺寸,新引擎以 1 为默认尺寸。
scale: Math.max(0.01, state.settings.scale / 1.5),
opacity: state.settings.opacity,
// 用户设置由最外层宿主统一执行,库内保持完整 HDR 发射。
opacity: 1,
trailTimeScale: state.settings.trailSpeed,
clickTimeScale: state.settings.clickSpeed,
});
Expand Down Expand Up @@ -619,9 +636,20 @@
throw new Error('ba-click-fx IIFE 未在适配器之前注入');
}

const outputHost = document.getElementById(EFFECT_HOST_ID);

if (!outputHost)
{
throw new Error('BASpark 特效宿主元素不存在');
}

state.outputHost = outputHost;

state.fx = new window.BAClickFX.BAClickFX(
{
target: outputHost,
inputSource: 'manual',
opacity: 1,
// WebView2 无法读取窗口后的桌面背景;使用 source-over,
// 再以浅色背景补偿和 Alpha 上限提高未知背景上的可见性。
effectBackend: 'webgl2',
Expand Down
12 changes: 12 additions & 0 deletions src/Web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,22 @@
background: transparent !important;
pointer-events: none;
}

#baspark-fx-host
{
position: fixed;
inset: 0;
width: 100%;
height: 100%;
overflow: hidden;
opacity: 1;
pointer-events: none;
}
</style>
</head>

<body>
<div id="baspark-fx-host" aria-hidden="true"></div>
<!-- C# 必须按 ba-click-fx.iife.js、fx-adapter.js 的顺序注入内联脚本。 -->
<!-- BASPARK_RENDERER_SCRIPTS -->
</body>
Expand Down
48 changes: 24 additions & 24 deletions src/Web/vendor/ba-click-fx.iife.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/licenses/ba-click-fx/VERSION.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Package: ba-click-fx
Version: v1.2.25
Commit: cbefdb849a96dae8d418547cf8cc90ab230029e3
Version: v1.2.29
Commit: e02336877fa74b46dff1f95d6876c58267251b1a
Artifact: dist/ba-click-fx.iife.js
SHA256: F198EEF9B29AE84D7B11B275DC0877D6BDD6C4D79035DDD200943CA8D5C3B764
SHA256: 7AC5DCB27EB652C7709751B2337A18EAD5ECFBEA02B49CC651AC6B2A841CA946
55 changes: 54 additions & 1 deletion tests/Web/fx-adapter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ function createHarness(options = {})
};
const windowListeners = new Map();
const canvasListeners = new Map();
const effectHost =
{
style:
{
opacity: '1',
},
};

class FakeFx
{
Expand Down Expand Up @@ -173,6 +180,15 @@ function createHarness(options = {})
document:
{
readyState: 'complete',
getElementById(id)
{
if (options.missingEffectHost || id !== 'baspark-fx-host')
{
return null;
}

return effectHost;
},
addEventListener()
{
throw new Error('DOMContentLoaded listener is not expected for a complete document.');
Expand All @@ -186,6 +202,7 @@ function createHarness(options = {})
return {
calls,
canvasListeners,
effectHost,
fx: FakeFx.instance,
window: windowMock,
windowListeners,
Expand All @@ -196,7 +213,9 @@ test('initializes the vendored renderer in manual WebView2 mode', () =>
{
const harness = createHarness();

assert.equal(harness.fx.config.target, harness.effectHost);
assert.equal(harness.fx.config.inputSource, 'manual');
assert.equal(harness.fx.config.opacity, 1);
assert.equal(harness.fx.config.effectBackend, 'webgl2');
assert.equal(harness.fx.config.bloomBackend, 'webgl2');
assert.equal(harness.fx.config.inputSamplingRate, 40);
Expand All @@ -210,6 +229,7 @@ test('initializes the vendored renderer in manual WebView2 mode', () =>
assert.equal(harness.fx.config.isolatedCompositing, false);
assert.equal(harness.fx.config.lightBackgroundContrastAlpha, 0);
assert.equal(harness.fx.config.maxDpr, 2);
assert.equal(harness.effectHost.style.opacity, '1');
assert.equal(harness.calls.setCompositingReference.length, 0);
assert.equal(harness.calls.messages.at(-1).type, 'ready');
assert.equal(harness.calls.messages.at(-1).generation, 'test-generation');
Expand All @@ -234,9 +254,10 @@ test('maps normalized host input and BASpark settings to BAClickFX', () =>
harness.window.updateEffectSettings(1.5, 0.75, 1.2, 0.8);
const settings = harness.calls.updateConfig.at(-1);
assert.equal(settings.scale, 1);
assert.equal(settings.opacity, 0.75);
assert.equal(settings.opacity, 1);
assert.equal(settings.trailTimeScale, 1.2);
assert.equal(settings.clickTimeScale, 0.8);
assert.equal(harness.effectHost.style.opacity, '0.75');

harness.window.updateColor('45,175,255');
assert.equal(harness.calls.setThemeColor.at(-1), '#2dafff');
Expand Down Expand Up @@ -270,6 +291,20 @@ test('maps normalized host input and BASpark settings to BAClickFX', () =>
assert.equal(harness.calls.clearTrail, 2);
});

test('applies global opacity after the renderer has completed Bloom', () =>
{
const harness = createHarness();

for (const opacity of [0.1, 0.5, 1])
{
harness.window.updateEffectSettings(1.5, opacity, 1, 1);

assert.equal(harness.effectHost.style.opacity, String(opacity));
assert.equal(harness.calls.updateConfig.at(-1).opacity, 1);
assert.equal(harness.fx.config.opacity, 1);
}
});

test('keeps the current color when host configuration is invalid', () =>
{
const harness = createHarness();
Expand Down Expand Up @@ -303,6 +338,7 @@ test('disables always-trail for touch and blocks input while paused', () =>
{
const harness = createHarness();

harness.window.updateEffectSettings(1.5, 0.5, 1, 1);
harness.window.setInputContext('touch', true);
assert.equal(harness.calls.updateConfig.at(-1).trailAlways, false);

Expand All @@ -320,13 +356,15 @@ test('disables always-trail for touch and blocks input while paused', () =>
harness.window.setRenderingPaused(false);
assert.equal(harness.calls.setPaused.at(-1).paused, false);
assert.equal(harness.calls.setPaused.at(-1).pauseOptions, undefined);
assert.equal(harness.effectHost.style.opacity, '0.5');
});

test('changes a software Bloom fallback to the bounded native backend', () =>
{
const harness = createHarness();
const listener = harness.canvasListeners.get('baclickfxbackendchange');

harness.window.updateEffectSettings(1.5, 0.5, 1, 1);
harness.fx.resolvedEffectBackend = 'canvas2d';
harness.fx.resolvedBloomBackend = 'software';

Expand All @@ -344,6 +382,7 @@ test('changes a software Bloom fallback to the bounded native backend', () =>
assert.equal(harness.calls.messages.at(-1).resolvedEffectBackend, 'canvas2d');
assert.equal(harness.calls.messages.at(-1).resolvedBloomBackend, 'software');
assert.equal(harness.calls.updateConfig.at(-1).bloomBackend, 'native');
assert.equal(harness.effectHost.style.opacity, '0.5');
});

test('reports Full WebGL backend resolution to the host', () =>
Expand Down Expand Up @@ -414,6 +453,20 @@ test('reports initialization failure without announcing readiness', () =>
assert.equal(harness.calls.messages.at(-1).phase, 'initialize');
});

test('reports a missing renderer host without announcing readiness', () =>
{
const harness = createHarness(
{
missingEffectHost: true,
},
);

assert.equal(harness.fx, undefined);
assert.equal(harness.calls.messages.some((message) => message.type === 'ready'), false);
assert.equal(harness.calls.messages.at(-1).type, 'error');
assert.equal(harness.calls.messages.at(-1).phase, 'initialize');
});

test('destroys renderer-owned resources before the document unloads', () =>
{
const harness = createHarness();
Expand Down
13 changes: 8 additions & 5 deletions tests/Web/vendor-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import test from 'node:test';
import vm from 'node:vm';

const expectedSha256 =
'F198EEF9B29AE84D7B11B275DC0877D6BDD6C4D79035DDD200943CA8D5C3B764';
'7AC5DCB27EB652C7709751B2337A18EAD5ECFBEA02B49CC651AC6B2A841CA946';
const vendorPath = new URL(
'../../src/Web/vendor/ba-click-fx.iife.js',
import.meta.url,
Expand All @@ -18,7 +18,7 @@ const versionPath = new URL(
import.meta.url,
);

test('vendored artifact matches the reviewed v1.2.25 build', () =>
test('vendored artifact matches the reviewed v1.2.29 build', () =>
{
const bytes = readFileSync(vendorPath);
const actual = createHash('sha256').update(bytes).digest('hex').toUpperCase();
Expand All @@ -30,10 +30,10 @@ test('vendored metadata identifies the exact reviewed artifact', () =>
{
const metadata = readFileSync(versionPath, 'utf8');

assert.match(metadata, /^Version: v1\.2\.25$/m);
assert.match(metadata, /^Version: v1\.2\.29$/m);
assert.match(
metadata,
/^Commit: cbefdb849a96dae8d418547cf8cc90ab230029e3$/m,
/^Commit: e02336877fa74b46dff1f95d6876c58267251b1a$/m,
);
assert.match(metadata, new RegExp(`^SHA256: ${expectedSha256}$`, 'm'));
});
Expand Down Expand Up @@ -109,10 +109,13 @@ test('inline resources cannot terminate their script element early', () =>
assert.equal(vendor.includes('sourceMappingURL'), false);
});

test('renderer template contains one deterministic injection marker', () =>
test('renderer template contains one host before its injection marker', () =>
{
const template = readFileSync(templatePath, 'utf8');
const host = 'id="baspark-fx-host"';
const marker = '<!-- BASPARK_RENDERER_SCRIPTS -->';

assert.equal(template.split(host).length - 1, 1);
assert.equal(template.split(marker).length - 1, 1);
assert.ok(template.indexOf(host) < template.indexOf(marker));
});
Loading