模板工程升级修改
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
# Spine 运行时 vendor 补丁记录
|
||||
|
||||
> ⚠️ **升级 `spine-canvas.js` / `spine-webgl.js` 前必读。**
|
||||
> 这两个文件**不是官方发行版原样**,含本地补丁。直接用官方文件覆盖会导致
|
||||
> `file://` 协议下资源加载全部失败(表现:所有 Spine 特效不显示)。
|
||||
|
||||
## 基线版本
|
||||
|
||||
| 文件 | npm 包 | 基线版本 | 字节数 | sha256 |
|
||||
|---|---|---|---|---|
|
||||
| `spine-canvas.js` | `@esotericsoftware/spine-canvas` | **4.2.113**(与 4.2.114 官方产物完全相同) | 455670 | `a19ad49c42490d4f41dbc805b0b91d72ccca4236957b2cdab8e57483c3aeb45c` |
|
||||
| `spine-webgl.js` | `@esotericsoftware/spine-webgl` | **4.2.113** | 553987 | `0c2a0d8cb833297168bac7bbb7821d6ebe87aa11c3708831ca6741468b3d4d25` |
|
||||
|
||||
骨架资源导出版本为 **4.2.43**(见 `client/assets/spine/*.json` 的 `skeleton.spine` 字段),
|
||||
运行时须保持在 **4.2.x** 系列。
|
||||
|
||||
官方产物获取地址:
|
||||
```
|
||||
https://cdn.jsdelivr.net/npm/@esotericsoftware/spine-canvas@4.2.113/dist/iife/spine-canvas.js
|
||||
https://cdn.jsdelivr.net/npm/@esotericsoftware/spine-webgl@4.2.113/dist/iife/spine-webgl.js
|
||||
```
|
||||
|
||||
官方原始产物 sha256(用于确认下载无误):
|
||||
- spine-canvas 4.2.113/114:`b706c28a…`(4.2.43 版,仅作对照,非本项目基线)
|
||||
- spine-webgl 4.2.113:`e737ac4e7caa9fae…`(打补丁前)
|
||||
|
||||
---
|
||||
|
||||
## 补丁清单
|
||||
|
||||
### 补丁 1 —— `file://` 下不设 `crossOrigin`
|
||||
|
||||
**应用于**:`spine-canvas.js`、`spine-webgl.js`
|
||||
|
||||
**位置**:`AssetManager.loadTexture` 中构造 `Image` 处(canvas 版约 5798 行 / webgl 版约 5838 行)。
|
||||
|
||||
**原因**:`file://` 协议下设置 `image.crossOrigin = "anonymous"` 会使图片被判为跨域而加载失败。
|
||||
这是 PNG 图集能在 `file://` 下加载的关键。
|
||||
|
||||
```js
|
||||
// 官方
|
||||
image.crossOrigin = "anonymous";
|
||||
|
||||
// 本地
|
||||
if (typeof location === "undefined" || location.protocol !== "file:")
|
||||
image.crossOrigin = "anonymous";
|
||||
```
|
||||
|
||||
⚠️ 只改 `loadTexture` 中的 `image`。webgl 版另有 `logoImage` / `spinnerImage`
|
||||
(约 14004 / 14008 行,属 loading spinner),**不要动**。
|
||||
|
||||
---
|
||||
|
||||
### 补丁 2 —— 删除 `toLoad === 0` 提前 resolve 分支
|
||||
|
||||
**应用于**:仅 `spine-canvas.js`(**`spine-webgl.js` 未移植,保留官方逻辑**)
|
||||
|
||||
**位置**:`AssetManager.loadTextureAtlas`(canvas 版官方约 5826-5830 行)。
|
||||
|
||||
```js
|
||||
// 官方(webgl 版保留此段)
|
||||
if (toLoad === 0) {
|
||||
this.success(success, path, atlas);
|
||||
resolve(atlas);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**行为差异**:官方在 `atlas.pages.length === 0` 时立即 resolve;删除后该情形下 Promise
|
||||
永不 resolve,`isLoadingComplete()` 恒为 false,全部 Spine 都不会显示。
|
||||
|
||||
**未移植到 webgl 版的理由**:本项目 20 个 atlas 文件实测**全部至少 1 页**(共 23 页),
|
||||
该分支在本项目不可达,删与不删行为完全相同;而官方逻辑在边界情形下更安全。
|
||||
|
||||
**注**:此补丁在 `spine-canvas.js` 中随 commit `1f48f4ea`(2026-04-20)打包引入,
|
||||
提交信息未提及,**原始意图在仓库中无记录可考**。上述为行为分析,非意图推定。
|
||||
|
||||
---
|
||||
|
||||
### 补丁 3 —— `rawDataUri` 判定改为 `startsWith("data:")`
|
||||
|
||||
**应用于**:`spine-canvas.js`(仅 `downloadText`)、`spine-webgl.js`(`downloadText` + `downloadBinary`)
|
||||
|
||||
**位置**:`Downloader.downloadText` / `Downloader.downloadBinary`
|
||||
(canvas 版约 6070 / 6096 行,webgl 版约 6110 / 6136 行)。
|
||||
|
||||
```js
|
||||
// 官方
|
||||
if (rawDataUri && !rawDataUri.includes(".")) {
|
||||
|
||||
// 本地
|
||||
if (rawDataUri && rawDataUri.startsWith("data:")) {
|
||||
```
|
||||
|
||||
**原因**:`SpineMgr.js` 用 `setRawDataURI(key, "data:," + textData[key])` 注入嵌入的
|
||||
json/atlas 文本(见 `client/generated/spine_data.js`),以规避 `file://` 下 XHR 的 CORS 拦截。
|
||||
而 json 文本中必然含小数点(坐标、版本号等),官方那个「不含点才算 data URI」的判定会误判为
|
||||
非 data URI,导致回退到 XHR 请求而失败。
|
||||
|
||||
**两版差异说明**:`spine-canvas.js` 只改了 `downloadText`,因为本项目只用 `loadText`
|
||||
(json/atlas 均为文本),不走二进制 `.skel` 路径。`spine-webgl.js` 两处都改了 ——
|
||||
该判定本身是官方的缺陷逻辑,改为 `startsWith("data:")` 在两处均为纯正确性提升、无副作用。
|
||||
|
||||
---
|
||||
|
||||
## 升级流程
|
||||
|
||||
1. 从上述 CDN 取目标版本的官方 iife 产物,记录其原始 sha256。
|
||||
2. 逐条比对本文件的补丁清单,确认每处补丁点在新版中仍存在且语境未变。
|
||||
3. 移植补丁时**必须校验匹配次数**(补丁 1 应命中 1 处、补丁 3 应命中 2 处),
|
||||
匹配数不符即中止并重新核对,不得盲替。
|
||||
4. 验证产物:`node --check` 语法检查;用 vm 加载并确认关键 API 齐备
|
||||
(`AssetManager` / `SkeletonJson` / `AtlasAttachmentLoader` / `Skeleton` /
|
||||
`AnimationState` / `AnimationStateData` / `Physics` / `TextureAtlas`,
|
||||
webgl 版另需 `SceneRenderer` / `PolygonBatcher` / `GLTexture` /
|
||||
`ManagedWebGLRenderingContext`)。
|
||||
5. 更新本文件的版本号与 sha256。
|
||||
6. 真机验证 `file://` 下资源加载正常(补丁 1、3 的实际验收点)。
|
||||
|
||||
## 已知约束
|
||||
|
||||
两个文件都以 `var spine = (() => {...})()` 形式暴露**同一个全局名 `spine`**。
|
||||
**同时加载两者会互相覆盖**,后加载的胜出。因此运行时只能加载其中一个,
|
||||
或在两次加载之间抢存引用。相关设计见
|
||||
`docs/superpowers/specs/2026-08-08-spine-webgl-migration-design.md`。
|
||||
Vendored
+59
-20
@@ -1,3 +1,44 @@
|
||||
/* ============================================================================
|
||||
* GAMEABC_TEXT —— 文字渲染统一层(本地补丁,见 js/vendor/README-gameabc-patches.md)
|
||||
*
|
||||
* 取代原先写在 js/00_Surface/12_Logic.js 里对 CanvasRenderingContext2D.prototype
|
||||
* 的猴子补丁,把「字体族 / 字重 / 测宽 / 竖直锚点」四件事收敛到引擎内一处。
|
||||
*
|
||||
* 1) 字体族与字重:原写法 'lighter Npx Arial',Arial 无中文字形,中文走回退字体又被
|
||||
* weight=100 拉细,导致中英粗细不一;另有三处写成 'Npx bold san-serif'(拼写错误,
|
||||
* 应为 sans-serif),实际落到默认衬线字体。统一为 500 字重 + 中文友好字体族。
|
||||
*
|
||||
* 2) 测宽同源:ifast_measureText 必须与实际绘制用同一 font 串,否则依赖测宽做居中 /
|
||||
* 换行的地方会有约 3% 的横向偏差。现在两者都走 GAMEABC_TEXT.font()。
|
||||
*
|
||||
* 3) 竖直锚点:Canvas 的 textBaseline="top" 锚点 = 基线上移「首个可用字体的 ascent」,
|
||||
* 但 WebKit(iOS 全部浏览器 + WKWebView)取 CoreText 原始 hhea ascent,
|
||||
* Blink(PC Chrome/Edge、安卓 WebView)取归一化 typo ascent —— 同字同号相差 0.125em;
|
||||
* iOS 上首个可用字体是 PingFang SC(ascent 1.06em)会把差距放大到 0.20em,
|
||||
* 表现为「iOS 文字整体偏下」。alphabetic 是唯一不依赖度量解释的锚点,
|
||||
* 因此这里改为固定用 alphabetic,由本层自算基线位置。
|
||||
*
|
||||
* TOP_TO_BASELINE = 0.845 是实测值:让 WebKit 的渲染结果与改造前 Blink 的外观
|
||||
* 在 24 / 40 / 64px 三档下逐像素对齐(最大偏差 1px),即「iOS 向网页/安卓看齐」,
|
||||
* 而不是三端一起位移。调整它会整体上下移动所有文字。
|
||||
* ========================================================================== */
|
||||
var GAMEABC_TEXT = {
|
||||
FAMILY: '"Microsoft YaHei", "PingFang SC", Arial, sans-serif',
|
||||
WEIGHT: '500',
|
||||
TOP_TO_BASELINE: 0.845,
|
||||
font: function(size) {
|
||||
return this.WEIGHT + ' ' + size + 'px ' + this.FAMILY;
|
||||
},
|
||||
/* 绘制前调用:设好字体并把锚点固定为 alphabetic */
|
||||
prepare: function(ctx, size) {
|
||||
ctx.font = this.font(size);
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
},
|
||||
/* 把原先「以文字块顶部为锚点的 y」换算成 alphabetic 基线的 y */
|
||||
baseline: function(y, size) {
|
||||
return y + this.TOP_TO_BASELINE * size;
|
||||
}
|
||||
};
|
||||
var gameabc_senderror = 0;
|
||||
var gameabc = function() {
|
||||
function s(SIt2, L$qig3, Qf4) {
|
||||
@@ -455,7 +496,7 @@ function dfwtime200() {
|
||||
return 0;
|
||||
}
|
||||
;var dc = ifast_getdc();
|
||||
dc.font = 'lighter ' + obj.h + 'px Arial';
|
||||
dc.font = GAMEABC_TEXT.font(obj.h);
|
||||
if (!caption) {
|
||||
caption = obj.caption;
|
||||
}
|
||||
@@ -2120,10 +2161,9 @@ function GameObjectManager() {
|
||||
var yyyy;
|
||||
var uu = gameabc_face.loglist.length - 1;
|
||||
for (yyyy = uu; yyyy >= 0; yyyy--) {
|
||||
this.backBufferContext2D.textBaseline = "top";
|
||||
GAMEABC_TEXT.prepare(this.backBufferContext2D, 32);
|
||||
this.backBufferContext2D.fillStyle = "red";
|
||||
this.backBufferContext2D.font = 32 + 'px' + ' bold san-serif';
|
||||
this.backBufferContext2D.fillText(gameabc_face.loglist[yyyy], 10, 80 + (uu - yyyy) * 32);
|
||||
this.backBufferContext2D.fillText(gameabc_face.loglist[yyyy], 10, GAMEABC_TEXT.baseline(80 + (uu - yyyy) * 32, 32));
|
||||
}
|
||||
;
|
||||
}
|
||||
@@ -2135,7 +2175,10 @@ function GameObjectManager() {
|
||||
}
|
||||
;if (gameabc_face.obj.caption != "") {
|
||||
this.backBufferContext2D.fillStyle = "white";
|
||||
this.backBufferContext2D.font = parseInt(24 * this.backBuffer.width / 480) + "px" + ' bold san-serif';
|
||||
/* 这一处原本就没有设过 textBaseline,实际用的是默认 alphabetic;
|
||||
这里显式写死以免依赖上一次绘制在 context 上的残留状态,y 保持不变。 */
|
||||
this.backBufferContext2D.font = GAMEABC_TEXT.font(parseInt(24 * this.backBuffer.width / 480));
|
||||
this.backBufferContext2D.textBaseline = 'alphabetic';
|
||||
this.backBufferContext2D.fillText(gameabc_face.obj.caption, parseInt(this.backBuffer.width / 2) - 50, parseInt(this.backBuffer.height / 2) + 100);
|
||||
}
|
||||
;if (gameabc_face.checkmustdraw() == 0) {
|
||||
@@ -2641,17 +2684,16 @@ function ifast_float(f, r) {
|
||||
;
|
||||
}
|
||||
;function ifast_mydrawtext(spid, recid, sp_x, sp_y, sp_w, sp_h) {
|
||||
gameabc_face.dc.textBaseline = "top";
|
||||
GAMEABC_TEXT.prepare(gameabc_face.dc, sp_h);
|
||||
gameabc_face.dc.fillStyle = gameabc_GameTxt.GameTxtList[recid].Color;
|
||||
gameabc_face.dc.font = sp_h + 'px' + ' bold san-serif';
|
||||
var caption = gameabc_GameTxt.GameTxtList[recid].Text;
|
||||
if (spid <= 0) {
|
||||
gameabc_face.dc.fillText(caption, sp_x, sp_y, sp_w);
|
||||
gameabc_face.dc.fillText(caption, sp_x, GAMEABC_TEXT.baseline(sp_y, sp_h), sp_w);
|
||||
return;
|
||||
}
|
||||
;var obj = ifast_getobj(spid);
|
||||
if (typeof (obj) == "object") {
|
||||
gameabc_face.dc.fillText(caption, obj.x + sp_x, obj.y + sp_y, sp_w);
|
||||
gameabc_face.dc.fillText(caption, obj.x + sp_x, GAMEABC_TEXT.baseline(obj.y + sp_y, sp_h), sp_w);
|
||||
}
|
||||
;
|
||||
}
|
||||
@@ -5290,9 +5332,8 @@ function gameabc_imgtxt() {
|
||||
}
|
||||
;gameabc_save(this, context);
|
||||
gameabc_charge(this, context);
|
||||
context.textBaseline = "top";
|
||||
GAMEABC_TEXT.prepare(context, this.h);
|
||||
context.fillStyle = gameabc_GameTxt.GameTxtList[this.frame].Color;
|
||||
context.font = 'lighter ' + this.h + 'px Arial';
|
||||
this.caption = gameabc_GameTxt.GameTxtList[this.frame].Text;
|
||||
if (this.uidata.GameTxtStyle == 0) {
|
||||
gameabc_drawtext = this.caption;
|
||||
@@ -5300,18 +5341,17 @@ function gameabc_imgtxt() {
|
||||
gameabc_drawtext = gameabc_password.substring(0, this.caption.length + 1);
|
||||
}
|
||||
;if (this.w > 0) {
|
||||
context.fillText(gameabc_drawtext, this.x - xScroll, this.y - yScroll, this.w);
|
||||
context.fillText(gameabc_drawtext, this.x - xScroll, GAMEABC_TEXT.baseline(this.y - yScroll, this.h), this.w);
|
||||
} else {
|
||||
context.fillText(gameabc_drawtext, this.x - xScroll, this.y - yScroll);
|
||||
context.fillText(gameabc_drawtext, this.x - xScroll, GAMEABC_TEXT.baseline(this.y - yScroll, this.h));
|
||||
}
|
||||
;gameabc_restore(this, context);
|
||||
}
|
||||
}
|
||||
;gameabc_imgtxt.prototype = new VisualGameObject;
|
||||
function draw2(obj, context, ctx) {
|
||||
context.textBaseline = "top";
|
||||
var h = obj.h * iui.ratio;
|
||||
context.font = 'lighter ' + h + 'px Arial';
|
||||
GAMEABC_TEXT.prepare(context, h);
|
||||
var w = context.measureText(obj.caption).width;
|
||||
context.clearRect(0, 0, w, h);
|
||||
if (obj.uidata.BackColorA != 0) {
|
||||
@@ -5319,7 +5359,7 @@ function draw2(obj, context, ctx) {
|
||||
context.fillRect(0, 0, w, h);
|
||||
}
|
||||
;context.fillStyle = obj.uidata.FontColor;
|
||||
context.fillText(obj.caption, 0, 0);
|
||||
context.fillText(obj.caption, 0, GAMEABC_TEXT.baseline(0, h));
|
||||
ctx.drawImage(iui.test, 0, 0, w, h, obj.x, obj.y, w / iui.ratio, obj.h);
|
||||
}
|
||||
;function gameabc_txt() {
|
||||
@@ -5340,13 +5380,12 @@ function draw2(obj, context, ctx) {
|
||||
if (this.uidata.GameTxtStyle > 0) {
|
||||
return;
|
||||
}
|
||||
;context.textBaseline = "top";
|
||||
;GAMEABC_TEXT.prepare(context, this.h);
|
||||
if (this.uidata.BackColorA != 0) {
|
||||
context.fillStyle = this.uidata.BackColor;
|
||||
context.fillRect(this.x, this.y, this.w, this.h);
|
||||
}
|
||||
;context.fillStyle = this.uidata.FontColor;
|
||||
context.font = 'lighter ' + this.h + 'px Arial';
|
||||
if (this.caption.indexOf("\n") > 0) {
|
||||
var strs = new Array();
|
||||
strs = this.caption.split("\n");
|
||||
@@ -5355,11 +5394,11 @@ function draw2(obj, context, ctx) {
|
||||
context.fillRect(this.x, this.y, this.w, this.h * strs.length);
|
||||
}
|
||||
;for (var i = 0; i < strs.length; i++) {
|
||||
context.fillText(strs[i], this.x - xScroll, this.y - yScroll + this.h * i);
|
||||
context.fillText(strs[i], this.x - xScroll, GAMEABC_TEXT.baseline(this.y - yScroll + this.h * i, this.h));
|
||||
}
|
||||
;return;
|
||||
}
|
||||
;context.fillText(this.caption, this.x - xScroll, this.y - yScroll);
|
||||
;context.fillText(this.caption, this.x - xScroll, GAMEABC_TEXT.baseline(this.y - yScroll, this.h));
|
||||
}
|
||||
}
|
||||
;gameabc_txt.prototype = new VisualGameObject;
|
||||
|
||||
Vendored
+14172
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user