Skip to content

Commit 5110cbf

Browse files
Merge branch 'dev' into steamworks
2 parents ccf9018 + 806840e commit 5110cbf

File tree

9 files changed

+187
-45
lines changed

9 files changed

+187
-45
lines changed

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# Auto detect text files and perform LF normalization
2-
* text=auto
2+
* text=auto eol=lf

packages/webgal/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"angular-expressions": "^1.4.3",
1616
"axios": "^0.30.2",
1717
"cloudlogjs": "^1.0.9",
18+
"gifuct-js": "^2.1.2",
1819
"i18next": "^22.4.15",
1920
"localforage": "^1.10.0",
2021
"lodash": "^4.17.21",

packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const scriptExecutor = () => {
4343
WebGAL.sceneManager.sceneData.currentSentenceId >
4444
WebGAL.sceneManager.sceneData.currentScene.sentenceList.length - 1
4545
) {
46-
if (WebGAL.sceneManager.sceneData.sceneStack.length !== 0) {
46+
if (WebGAL.sceneManager.sceneData.sceneStack.length !== 0 && !WebGAL.sceneManager.lockSceneWrite) {
4747
const sceneToRestore: ISceneEntry | undefined = WebGAL.sceneManager.sceneData.sceneStack.pop();
4848
if (sceneToRestore !== undefined) {
4949
restoreScene(sceneToRestore);
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { BaseImageResource, Ticker, UPDATE_PRIORITY, settings } from 'pixi.js';
2+
import { parseGIF, decompressFrames } from 'gifuct-js';
3+
4+
export interface GifResourceOptions {
5+
autoLoad?: boolean;
6+
autoPlay?: boolean;
7+
loop?: boolean;
8+
animationSpeed?: number;
9+
fps?: number;
10+
}
11+
12+
interface PrecomputedFrame {
13+
start: number;
14+
end: number;
15+
imageData: ImageData;
16+
}
17+
18+
const findFrame = (frames: PrecomputedFrame[], time: number) => {
19+
let low = 0;
20+
let high = frames.length - 1;
21+
while (low <= high) {
22+
const mid = (low + high) >> 1;
23+
const f = frames[mid];
24+
if (time >= f.start && time < f.end) return f;
25+
if (time < f.start) high = mid - 1;
26+
else low = mid + 1;
27+
}
28+
return undefined;
29+
};
30+
31+
export class GifResource extends BaseImageResource {
32+
public static override test(_src: unknown, ext?: string): boolean {
33+
return (
34+
ext === 'gif' ||
35+
(typeof _src === 'string' && _src.trim().toLowerCase().endsWith('.gif')) ||
36+
(_src instanceof HTMLImageElement && _src.src.toLowerCase().endsWith('.gif'))
37+
);
38+
}
39+
40+
public declare source: HTMLCanvasElement;
41+
42+
public autoPlay: boolean;
43+
public loop: boolean;
44+
public animationSpeed: number;
45+
public readonly fps: number;
46+
public readonly url: string = '';
47+
48+
private _frames: PrecomputedFrame[] = [];
49+
private _currentTime = 0;
50+
private _playing = false;
51+
private _loadPromise: Promise<this> | null = null;
52+
53+
public constructor(src: unknown, options: GifResourceOptions = {}) {
54+
super(document.createElement('canvas'));
55+
if (typeof src === 'string') this.url = src;
56+
else if (src instanceof HTMLImageElement) this.url = src.src;
57+
this.autoPlay = options.autoPlay ?? true;
58+
this.loop = options.loop ?? true;
59+
this.animationSpeed = options.animationSpeed ?? 1;
60+
this.fps = options.fps ?? 30;
61+
62+
if (options.autoLoad !== false) this.load();
63+
}
64+
65+
public override async load(): Promise<this> {
66+
if (this._loadPromise) return this._loadPromise;
67+
68+
this._loadPromise = (async () => {
69+
const res = await settings.ADAPTER.fetch(this.url);
70+
const buffer = await res.arrayBuffer();
71+
72+
if (!buffer?.byteLength) throw new Error('Invalid GIF buffer');
73+
74+
const gif = parseGIF(buffer);
75+
const gifFrames = decompressFrames(gif, true);
76+
if (!gifFrames.length) throw new Error('Invalid GIF file');
77+
78+
const canvas = document.createElement('canvas');
79+
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
80+
const patchCanvas = document.createElement('canvas');
81+
const patchCtx = patchCanvas.getContext('2d')!;
82+
83+
canvas.width = gif.lsd.width;
84+
canvas.height = gif.lsd.height;
85+
86+
let time = 0;
87+
let prevFrame: ImageData | null = null;
88+
const defaultDelay = 1000 / this.fps;
89+
90+
for (const frame of gifFrames) {
91+
const { dims, delay = defaultDelay, disposalType = 2, patch } = frame;
92+
const { width, height, left, top } = dims;
93+
94+
patchCanvas.width = width;
95+
patchCanvas.height = height;
96+
const patchData = new ImageData(new Uint8ClampedArray(patch), width, height);
97+
patchCtx.putImageData(patchData, 0, 0);
98+
99+
ctx.drawImage(patchCanvas, left, top);
100+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
101+
102+
this._frames.push({ start: time, end: (time += delay), imageData });
103+
104+
if (disposalType === 2) ctx.clearRect(0, 0, canvas.width, canvas.height);
105+
else if (disposalType === 3 && prevFrame) ctx.putImageData(prevFrame, 0, 0);
106+
107+
prevFrame = imageData;
108+
}
109+
110+
this.source.width = canvas.width;
111+
this.source.height = canvas.height;
112+
super.update();
113+
114+
if (this.autoPlay) this.play();
115+
116+
return this;
117+
})();
118+
119+
return this._loadPromise;
120+
}
121+
122+
public play(): void {
123+
if (this._playing) return;
124+
this._playing = true;
125+
Ticker.shared.add(this._update, this, UPDATE_PRIORITY.HIGH);
126+
}
127+
128+
public stop(): void {
129+
if (!this._playing) return;
130+
this._playing = false;
131+
Ticker.shared.remove(this._update, this);
132+
}
133+
134+
public override dispose(): void {
135+
this.stop();
136+
super.dispose();
137+
this._frames = [];
138+
this._loadPromise = null;
139+
}
140+
141+
private _update(): void {
142+
if (!this._playing || !this._frames.length) return;
143+
144+
this._currentTime += Ticker.shared.deltaMS * this.animationSpeed;
145+
const frame = findFrame(this._frames, this._currentTime);
146+
147+
if (frame) {
148+
this.source.getContext('2d')!.putImageData(frame.imageData, 0, 0);
149+
super.update();
150+
}
151+
152+
const end = this._frames[this._frames.length - 1].end;
153+
if (this._currentTime > end) {
154+
if (this.loop) this._currentTime %= end;
155+
else this.stop();
156+
}
157+
}
158+
}

packages/webgal/src/Core/controller/stage/pixi/PixiController.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { logger } from '@/Core/util/logger';
1111
import { v4 as uuid } from 'uuid';
1212
import { cloneDeep, isEqual } from 'lodash';
1313
import * as PIXI from 'pixi.js';
14+
import { INSTALLED } from 'pixi.js';
15+
import { GifResource } from './GifResource';
1416

1517
export interface IAnimationObject {
1618
setStartState: Function;
@@ -62,6 +64,8 @@ export interface ILive2DRecord {
6264
// @ts-ignore
6365
window.PIXI = PIXI;
6466

67+
INSTALLED.push(GifResource);
68+
6569
export default class PixiStage {
6670
public static assignTransform<T extends ITransform>(target: T, source?: ITransform) {
6771
if (!source) return;
@@ -990,7 +994,6 @@ export default class PixiStage {
990994
}
991995

992996
public getFigureMetadataByKey(key: string): IFigureMetadata | undefined {
993-
console.log(key, webgalStore.getState().stage.figureMetaData);
994997
return webgalStore.getState().stage.figureMetaData[key];
995998
}
996999

packages/webgal/src/Core/gameScripts/playEffect.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IPerform } from '@/Core/Modules/perform/performInterface';
66
import { useSelector } from 'react-redux';
77
import { WebGAL } from '@/Core/WebGAL';
88
import { WEBGAL_NONE } from '@/Core/constants';
9+
import { end } from './end';
910

1011
/**
1112
* 播放一段效果音
@@ -91,7 +92,7 @@ export const playEffect = (sentence: ISentence): IPerform => {
9192
};
9293
resolve(perform);
9394
seElement?.play();
94-
seElement.onended = () => {
95+
const endFunc = () => {
9596
for (const e of WebGAL.gameplay.performController.performList) {
9697
if (e.performName === performInitName) {
9798
isOver = true;
@@ -100,6 +101,12 @@ export const playEffect = (sentence: ISentence): IPerform => {
100101
}
101102
}
102103
};
104+
seElement.onended = endFunc;
105+
seElement.addEventListener('error', (e) => {
106+
logger.error(`播放效果音失败: ${url}`);
107+
// 播放失败提前结束
108+
endFunc();
109+
});
103110
}, 1);
104111
}),
105112
};
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
import '../../gameScripts/pixi/performs/cherryBlossoms';
2-
import '../../gameScripts/pixi/performs/rain';
3-
import '../../gameScripts/pixi/performs/snow';
1+
import.meta.glob('../../gameScripts/pixi/performs/*.{ts,js,tsx,jsx}', { eager: true });

packages/webgal/vite.config.ts

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { defineConfig } from 'vite';
22
import react from '@vitejs/plugin-react';
33
import loadVersion from 'vite-plugin-package-version';
4-
import { resolve, relative } from 'path';
5-
import { visualizer } from 'rollup-plugin-visualizer';
6-
import { readdirSync, watch, writeFileSync } from 'fs';
7-
import { isEqual } from 'lodash';
4+
import { resolve } from 'path';
85
import Info from 'unplugin-info/vite';
96
import viteCompression from 'vite-plugin-compression';
107

@@ -13,40 +10,6 @@ import viteCompression from 'vite-plugin-compression';
1310
// @ts-ignore
1411
const env = process.env.NODE_ENV;
1512
console.log(env);
16-
(() => {
17-
const pixiPerformScriptDirPath = './src/Core/gameScripts/pixi/performs/';
18-
const pixiPerformManagerDirPath = './src/Core/util/pixiPerformManager/';
19-
const relativePath = relative(pixiPerformManagerDirPath, pixiPerformScriptDirPath).replaceAll('\\', '/');
20-
let lastFiles: string[] = [];
21-
22-
function setInitFile() {
23-
console.log('正在自动编写pixi特效依赖注入');
24-
writeFileSync(
25-
resolve(pixiPerformManagerDirPath, 'initRegister.ts'),
26-
lastFiles
27-
.map((v) => {
28-
const filePath = relativePath + '/' + v.slice(0, v.lastIndexOf('.'));
29-
return `import '${filePath}';`;
30-
})
31-
.join('\n') + '\n',
32-
{ encoding: 'utf-8' },
33-
);
34-
}
35-
36-
function getPixiPerformScriptFiles() {
37-
const pixiPerformScriptFiles = readdirSync(pixiPerformScriptDirPath, { encoding: 'utf-8' }).filter((v) =>
38-
['ts', 'js', 'tsx', 'jsx'].includes(v.slice(v.indexOf('.') + 1, v.length)),
39-
);
40-
if (!isEqual(pixiPerformScriptFiles, lastFiles)) {
41-
lastFiles = pixiPerformScriptFiles;
42-
setInitFile();
43-
}
44-
}
45-
46-
getPixiPerformScriptFiles();
47-
48-
if (env !== 'production') watch(pixiPerformScriptDirPath, { encoding: 'utf-8' }, getPixiPerformScriptFiles);
49-
})();
5013

5114
export default defineConfig({
5215
plugins: [

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3037,6 +3037,13 @@ gh-pages@^4.0.0:
30373037
fs-extra "^8.1.0"
30383038
globby "^6.1.0"
30393039

3040+
gifuct-js@^2.1.2:
3041+
version "2.1.2"
3042+
resolved "https://registry.yarnpkg.com/gifuct-js/-/gifuct-js-2.1.2.tgz#06152437ba30ec914db8398bd838bd0fbc8a6ecd"
3043+
integrity sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==
3044+
dependencies:
3045+
js-binary-schema-parser "^2.0.3"
3046+
30403047
git-up@^8.1.0:
30413048
version "8.1.1"
30423049
resolved "https://registry.yarnpkg.com/git-up/-/git-up-8.1.1.tgz#06262adadb89a4a614d2922d803a0eda054be8c5"
@@ -3658,6 +3665,11 @@ jest-worker@^26.2.1:
36583665
merge-stream "^2.0.0"
36593666
supports-color "^7.0.0"
36603667

3668+
js-binary-schema-parser@^2.0.3:
3669+
version "2.0.3"
3670+
resolved "https://registry.yarnpkg.com/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz#3d7848748e8586e63b34e8911b643f59cfb6396e"
3671+
integrity sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==
3672+
36613673
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
36623674
version "4.0.0"
36633675
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"

0 commit comments

Comments
 (0)