Files
SpineWallpaper/dist/scripts/index.js
T
Shuery eb56efbd9e feat: 比例适配、bgm 音源选择与 fps 限流,打包 v3
取景(issue 04)
- 新增 viewport-fitter.js:让立绘层缩放跟随背景图的 cover 缩放——背景放大多少倍、
  特效就放大多少倍。原先背景走 cover、立绘层走 contain,比例一变两者就分叉。
  16:9 下与作者设定逐像素一致(kv37 为 0 差异)。实测证伪了"contain 会裁掉作者视口"
  这个最初的判断:contain 在任何比例下都完整含住作者视口。
- 应用方式为原地改写 config.viewport 字段后调 setViewport(),不重建播放器;
  维护挂在 config.frame 上按画布比例增量进行(config.success 早于 setViewport,拿不到视口)。

WE 属性(issue 05)
- project.json 新增 bgm combo(所有壁纸音源的并集 + 「随预设」);所选项不属于当前壁纸时
  音频回落该壁纸默认,选择本身保留。
- fps 限流包裹实例 drawFrame 并接管排程。修掉两个实测缺陷:朴素丢帧会把 rAF 抖动放大成
  整拍丢失(目标 30fps 实测只有 22.5fps);fps:0 被真值判断吞掉导致无法取消限流。

打包(issue 06)
- project.json version 升至 3 并更新描述;preset 的 combo 取值保持 xilian/kv37 不变。
- preview.gif 821 KB → 347 KB(每 2 帧取 1 帧 + 128 色不抖动,逐帧与源帧像素完全相同)。
- index.html 加内联 favicon,消除 CEF 自动请求 /favicon.ico 造成的 404 报错。

其它
- dist/scripts/wallpaper-engine.js 移到 tools/:它没有任何入口引用,且环境判据依赖真实 WE
  并不存在的 wallpaperRegisterListener,一旦按它自己的说明挂进 index.html 会在生产环境自我激活。
- 修正死文件检查:原检查会拿文件名匹配到文件自身的内容,使这类文件漏网。
- 新增离线算式验证与 resize / 属性 / 全量验收三套浏览器测试脚本。
2026-09-19 20:29:25 +08:00

123 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Presets from "./presets.js";
import PresetController from "./preset-controller.js";
import BackgroundController from "./background-controller.js";
import SpineController from "./spine-controller.js";
import AudioController from "./audio-controller.js";
// 与 project.json 里属性的默认值保持一致
const DEFAULT_PRESET_ID = "xilian";
const DEFAULT_VOLUME = 0.5;
const presets = new PresetController(Presets);
const backgroundController = new BackgroundController();
const spineController = new SpineController("spine-container");
const audioController = new AudioController("background-music");
// 唯一的真相来源。WE 可能在 window.load 之前就下发用户属性,所以这里只记录状态;
// 渲染时再决定"能不能立刻应用"——只有播放器需要等到 load 之后才存在。
const state = {
ready: false,
presetId: "",
audioSource: "", // 空字符串 = 用当前预设的默认音源
audioVolume: DEFAULT_VOLUME,
bgm: "", // 空字符串 = 随预设;否则是某个 preset.js 里 audioChoices 的 id
};
let appliedPresetId = "";
// 量出背景图的真实宽高比:立绘层要靠它才能和背景同步缩放。
// 直接读图片,避免在预设里再手写一个容易写错的数字。
function measureImageAspect(url) {
return new Promise((resolve) => {
if (!url) {
resolve(undefined);
return;
}
const probe = new Image();
probe.onload = () => resolve(probe.naturalWidth / probe.naturalHeight);
probe.onerror = () => resolve(undefined);
probe.src = url;
});
}
// bgm combo 选中的音源只在本壁纸声明过时才生效。否则回落本壁纸默认音源——
// 选择本身记着不动,切回那份壁纸时用户的选择还在。
function bgmSource(preset) {
if (!state.bgm) return "";
const choices = preset.audioChoices || [];
const hit = choices.find((choice) => choice.id === state.bgm);
return hit ? hit.source : "";
}
function render() {
const presetId = state.presetId || DEFAULT_PRESET_ID;
const preset = presets.get(presetId);
if (!preset) return;
// 只有壁纸真的换了才碰背景与骨架。否则拖动音量滑块都会重建播放器
// (重建 = 重新下载 3.4 MB 骨架与全部贴图)。
if (appliedPresetId !== presetId) {
appliedPresetId = presetId;
backgroundController.setImage(preset.backgroundImage);
spineController.setConfig(preset.spineConfig); // 播放器还不存在时只是记下来
// 图片解码是异步的,回来时可能已经切到别的预设了,所以要核对一次
measureImageAspect(preset.backgroundImage).then((aspect) => {
if (appliedPresetId === presetId) spineController.setImageAspect(aspect);
});
}
// 音源优先级:用户自填 URL > bgm 里选的(若本壁纸有)> 本壁纸默认音源
audioController.setSource(state.audioSource || bgmSource(preset) || preset.audioOptions.source);
audioController.setVolume(state.audioVolume);
if (state.ready) audioController.play();
}
window.addEventListener("load", function () {
state.ready = true;
render(); // 先把状态落到各控制器(此时播放器尚不存在,只写配置)
spineController.create(); // 再用写好的配置创建播放器
});
// 必须在模块顶层注册:属性事件可能在 window.load 之前到达
window.wallpaperPropertyListener = {
applyUserProperties: function (properties) {
// 先写状态:无条件记录,与播放器是否存在、是否加载完成无关
if (properties.preset) {
const presetId = properties.preset.value;
if (presets.has(presetId)) {
state.presetId = presetId;
} else if (presetId) {
console.warn(`忽略未知的预设 id:${presetId}`);
}
}
if (properties.audio_file) {
state.audioSource = String(properties.audio_file.value || "").trim();
}
if (properties.bgm) {
const bgm = String(properties.bgm.value || "").trim();
state.bgm = bgm === "auto" ? "" : bgm;
}
if (properties.audio_volume) {
const volume = Number(properties.audio_volume.value);
if (Number.isFinite(volume)) {
state.audioVolume = Math.max(0, Math.min(1, volume));
}
}
render();
},
applyGeneralProperties: function (properties) {
// WE 不会替壁纸限流,fps 得自己落实。这个属性与预设无关,不进 state。
// 判 undefined 而不是真值:0 表示不限流,用真值判断会把"取消限流"整个吞掉。
if (properties.fps !== undefined) {
spineController.setFps(properties.fps);
}
},
};