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
38 changes: 38 additions & 0 deletions dev-docs/立绘差分切换.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 立绘差分切换

```text
changeFigure:normal.png -id=hero;
changeFigureDiff:smile.png -id=hero;
```

`changeFigure` 是换立绘,`changeFigureDiff` 是同一立绘的差分替换(如换表情)。省略 `-id` 时默认操作中间立绘,也可使用 `-left` 等位置参数。

差分只改状态表中的图片地址,立绘的其余状态全部保留:变换、层级(zIndex)与混合模式、入退场设置,以及舞台上的遮挡顺序。旧图不算退场,不播放自定义退场;这些设置留给之后真正换走立绘的 `changeFigure`。

口型、眨眼图是与表情配套的整张图,差分后旧图不再适用。因此 `changeFigureDiff` 接受 `-mouthOpen`、`-mouthClose`、`-mouthHalfOpen`、`-eyesOpen`、`-eyesClose`、`-animationFlag`,并按本句参数整体替换,未写的即为不启用。仍在进行的语音口型、眨眼若使用旧表情的图,舞台会丢弃这些换图。

```text
changeFigureDiff:smile.png -id=hero -mouthOpen=smile_open.png -mouthClose=smile.png;
```

差分优先于口型眨眼:混合从 Sprite 实际显示的图开始(可能是闭嘴图,闭嘴图常与原图相同),混合期间暂停口型眨眼换图,结束后显示新差分的原图。

```text
changeFigure:normal.png -id=hero -exit=exitSlide;
changeFigureDiff:smile.png -id=hero; // 差分,不播放 exitSlide
changeFigure:other.png -id=hero; // smile.png 以 exitSlide 退场
```

除上述口型眨眼参数外,只接受定位与流程参数(`-id`、位置、`-next`、`-continue`、`-when`),其余参数会被忽略并给出警告;需要同时改变换、动画或模型参数时使用 `changeFigure`。没有可替换的立绘、或内容为 `none` 时不存在差分,按 `changeFigure` 处理。

Live2D 与 Spine 使用各自内置的表情、动作系统,不适用差分:源立绘或目标是模型时,本句不执行。

差分演出命名为 `animation-<key>#diff`:登记时不会去重掉同目标已有的动画演出(先 `setAnimation` 再差分时,混合与动作同时进行);之后同目标的非并行 `setTransform`、`setAnimation`、`changeFigure` 会接管并移除它,同时差分标记被清除,上屏走回退,淡入也随之取消(旧图渐出,新图直接执行动作)。混合期间对象被锁定;对象正被其他动画锁定时同样走回退。

舞台提交时,新指令留下的临时标记(不进入存档;同一次提交内连续差分以最初的源图为起点)与实际上屏的源图和目标 URL 匹配、基准位置未变,两张静态图片的宽高及像素密度完全一致,且两者 GPU 资源已就绪时,复用容器,以 200 ms 的互补 RGBA 混合过渡。

资源未就绪、尺寸不同、跳过动画、标记过期等情况,退回淡出淡入:旧图以默认淡出退场,新对象接替旧对象的遮挡顺序,保持已有变换,仅透明度淡入。不会等待资源来争取混合,也不会为尺寸不同的图片重新计算混合几何。

标记由 `WebGAL.figureDiffManager` 实例管理,只记录源 URL 和目标 URL:脚本执行时 `mark`,舞台替换时 `consume`,每次提交结束时 `clear`。后续修改目标的变换或动画设置、丢弃未提交演出、重建舞台状态时也会清除相应标记,不保存状态快照或演出引用。混合或淡入动画注册到现有 Pixi 动画管理,由差分演出负责结束。

状态表只保存差分后的图片地址,没有额外差分字段;读档、回溯和快速预览不依赖标记来恢复图片。
2 changes: 1 addition & 1 deletion packages/parser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webgal-parser",
"version": "4.6.3",
"version": "4.6.5-beta.1",
"description": "WebGAL script parser",
"scripts": {
"test": "vitest",
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/config/scriptConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const SCRIPT_CONFIG = [
{ scriptString: 'say', scriptType: commandType.say },
{ scriptString: 'changeBg', scriptType: commandType.changeBg },
{ scriptString: 'changeFigure', scriptType: commandType.changeFigure },
{ scriptString: 'changeFigureDiff', scriptType: commandType.changeFigureDiff },
{ scriptString: 'bgm', scriptType: commandType.bgm },
{ scriptString: 'playVideo', scriptType: commandType.video },
{ scriptString: 'pixiPerform', scriptType: commandType.pixi },
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/interface/sceneInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export enum commandType {
wait,
callSteam, // 调用Steam功能
return, // 从被调用的场景返回
changeFigureDiff, // 切换等尺寸静态立绘差分;追加以保持旧指令编号
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/src/scriptParser/assetsScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const assetsScanner = (
type: fileType.background,
});
}
if (command === commandType.changeFigure) {
if (command === commandType.changeFigure || command === commandType.changeFigureDiff) {
returnAssetsList.push({
name: content,
url: content,
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/scriptParser/contentParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const contentParser = (
case commandType.changeBg:
return assetSetter(contentRaw, fileType.background);
case commandType.changeFigure:
case commandType.changeFigureDiff:
return assetSetter(contentRaw, fileType.figure);
case commandType.bgm:
return assetSetter(contentRaw, fileType.bgm);
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"dependencies": {
"@emotion/css": "^11.11.2",
"@icon-park/react": "^1.4.2",
"@pixi/assets": "6.5.10",
"@pixi/basis": "6.5.10",
"@reduxjs/toolkit": "^1.8.1",
"angular-expressions": "^1.4.3",
"axios": "^1.13.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export class PerformController {
perform.isStarted = true;
perform.startFunction?.();

// 时间到后自动清理演出
// 演出持续 duration 毫秒后自动回收;同步清理会使演出刚启动就结束。
const stopTimeout = setTimeout(() => {
// perform.stopFunction();
// perform.isOver = true;
Expand Down Expand Up @@ -332,6 +332,7 @@ export class PerformController {
});
if (isBlockingNext) {
// 有阻塞,提前结束
// 其他演出仍阻塞推进,间隔重试以等待其结束,避免同步递归占住主线程。
setTimeout(this.goNextWhenOver, 100);
} else {
continueSentence();
Expand Down
10 changes: 9 additions & 1 deletion packages/webgal/src/Core/Modules/stage/stageStateManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cloneDeep from 'lodash/cloneDeep';
import { WebGAL } from '@/Core/WebGAL';
import { isUndefined, omitBy } from 'lodash';
import { commandType } from '@/Core/controller/scene/sceneInterface';
import { STAGE_KEYS } from '@/Core/constants';
Expand Down Expand Up @@ -126,11 +127,13 @@ export class StageStateManager {
}

public replaceCalculationStageState(stageState: IStageState) {
// 读档、回溯与场景重建只恢复普通状态,不能携带上一次演算的换图意图。
WebGAL.figureDiffManager.clear();
this.calculationStageState = cloneDeep(stageState);
}

public replaceAllStageState(stageState: IStageState, options?: IStageCommitOptions) {
this.calculationStageState = cloneDeep(stageState);
this.replaceCalculationStageState(stageState);
this.commit(options);
}

Expand All @@ -144,6 +147,7 @@ export class StageStateManager {

public updateEffect(payload: IEffect) {
const { target, transform } = payload;
WebGAL.figureDiffManager.clear(target);
const state = this.calculationStageState;
const activeTargets = [
STAGE_KEYS.STAGE_MAIN,
Expand Down Expand Up @@ -188,6 +192,8 @@ export class StageStateManager {

public updateAnimationSettings(payload: IUpdateAnimationSettingPayload) {
const { target, key, value } = payload;
// 后续普通换图或自定义过渡覆盖了目标;差分指令会在自身状态更新完毕后重新打标。
WebGAL.figureDiffManager.clear(target);
const state = this.calculationStageState;
const animationIndex = state.animationSettings.findIndex((a) => a.target === target);
if (animationIndex >= 0) {
Expand Down Expand Up @@ -352,6 +358,8 @@ export class StageStateManager {
}

public clearUncommittedNonHoldPerforms() {
// 快速预览丢弃演出时,一并丢弃其尚未上屏的差分标记。
WebGAL.figureDiffManager.clear();
this.calculationStageState.PerformList = this.calculationStageState.PerformList.filter(
(perform) => perform.isHoldOn,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/webgal/src/Core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const STAGE_KEYS = {
};

export const WEBGAL_NONE = 'none';
export const DEFAULT_FIG_IN_DURATION = 300;
export const DEFAULT_FIG_OUT_DURATION = 450;
export const DEFAULT_FIG_IN_DURATION = 200;
export const DEFAULT_FIG_OUT_DURATION = 160;
export const DEFAULT_BG_IN_DURATION = 1000;
export const DEFAULT_BG_OUT_DURATION = 1500;
1 change: 1 addition & 0 deletions packages/webgal/src/Core/controller/gamePlay/autoPlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const autoPlay = () => {
}
// nextSentence();
if (WebGAL.gameplay.autoTimeout === null) {
// 保留自动阅读的停留时间,到时再检查能否推进,不能同步连续跳句。
WebGAL.gameplay.autoTimeout = setTimeout(autoNextSentence, autoPlayDelay);
}
};
1 change: 1 addition & 0 deletions packages/webgal/src/Core/controller/gamePlay/fastSkip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const startTemporaryFast = (duration = 150) => {
clearTemporaryFast();
if (WebGAL.gameplay.fastInterval !== null) stopFast();
WebGAL.gameplay.isFast = true;
// 临时快进状态持续 duration 毫秒,供此期间的演出判断是否跳过动画。
temporaryFastTimeout = setTimeout(() => {
if (WebGAL.gameplay.fastInterval === null) {
WebGAL.gameplay.isFast = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum commandType {
wait,
callSteam, // 调用Steam功能
return, // 从被调用的场景返回
changeFigureDiff, // 切换等尺寸静态立绘差分;追加以保持旧指令编号
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/controller/stage/pixi/GifResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export class GifResource extends BaseImageResource {

this.source.width = canvas.width;
this.source.height = canvas.height;
// 预加载期间不推进动画,但要让 prepare 上传真实的首帧。
this.source.getContext('2d')!.putImageData(this._frames[0].imageData, 0, 0);
super.update();

if (this.autoPlay) this.play();
Expand Down
Loading
Loading