Skip to content

Commit 1421497

Browse files
authored
Merge pull request #432 from akashic-games/add-cross-fade
feat: クロスフェード機構を追加
2 parents b807b2b + 5c2c01b commit 1421497

File tree

10 files changed

+727
-3
lines changed

10 files changed

+727
-3
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# ChangeLog
22

3+
## 3.9.2
4+
機能追加
5+
* `g.AudioUtil` を追加
6+
* 音声のフェードイン・フェードアウト・クロスフェード等の機能を提供します。
7+
* `g.Game#onUpdate` を追加
8+
* ティックの進行後 (`g.Scene#onUpdate` が発火した後) に発火します。
9+
* `g.Util#clamp()` を追加
10+
* `EasingFunction` `AudioTransitionContext` を追加
11+
312
## 3.9.1
413
* 早送り中に `g.AudioPlayContext` の再生を抑制するように
514

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@akashic/akashic-engine",
3-
"version": "3.9.1",
3+
"version": "3.9.2",
44
"description": "The core library of Akashic Engine",
55
"main": "index.js",
66
"dependencies": {

src/AudioUtil.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import type { AudioPlayContext } from "./AudioPlayContext";
2+
import type { Game } from "./Game";
3+
import { Util } from "./Util";
4+
5+
/**
6+
* イージング関数。
7+
*
8+
* @param t 経過時間
9+
* @param b 開始位置
10+
* @param c 差分
11+
* @param d 所要時間
12+
*/
13+
export type EasingFunction = (t: number, b: number, c: number, d: number) => number;
14+
15+
export type AudioTransitionContext = {
16+
/**
17+
* 遷移を即座に完了する。
18+
* 音量は遷移完了後の値となる。
19+
*/
20+
complete: () => void;
21+
/**
22+
* 遷移を取り消す。音量はこの関数を実行した時点での値となる。
23+
* @param revert 音量を遷移実行前まで戻すかどうか。省略時は `false` 。
24+
*/
25+
cancel: (revert?: boolean) => void;
26+
};
27+
28+
/**
29+
* linear のイージング関数。
30+
*/
31+
const linear: EasingFunction = (t: number, b: number, c: number, d: number) => (c * t) / d + b;
32+
33+
/**
34+
* Audio に関連するユーティリティ。
35+
*/
36+
export module AudioUtil {
37+
/**
38+
* 音声をフェードインさせる。
39+
*
40+
* @param game 対象の `Game`。
41+
* @param context 対象の `AudioPlayContext` 。
42+
* @param duration フェードインの長さ (ms)。
43+
* @param to フェードイン後の音量。0 未満または 1 より大きい値を指定した場合の挙動は不定である。省略時は `1` 。
44+
* @param easing イージング関数。省略時は linear 。
45+
*/
46+
export function fadeIn(
47+
game: Game,
48+
context: AudioPlayContext,
49+
duration: number,
50+
to: number = 1,
51+
easing: EasingFunction = linear
52+
): AudioTransitionContext {
53+
context.changeVolume(0);
54+
context.play();
55+
const { complete, cancel } = transitionVolume(game, context, duration, to, easing);
56+
57+
return {
58+
complete: () => {
59+
complete();
60+
},
61+
cancel: (revert: boolean = false) => {
62+
cancel(revert);
63+
if (revert) {
64+
context.stop();
65+
}
66+
}
67+
};
68+
}
69+
70+
/**
71+
* 音声をフェードアウトさせる。
72+
*
73+
* @param game 対象の `Game`。
74+
* @param context 対象の `AudioPlayContext` 。
75+
* @param duration フェードアウトの長さ (ms)。
76+
* @param easing イージング関数。省略時は linear が指定される。
77+
*/
78+
export function fadeOut(
79+
game: Game,
80+
context: AudioPlayContext,
81+
duration: number,
82+
easing: EasingFunction = linear
83+
): AudioTransitionContext {
84+
const { complete, cancel } = transitionVolume(game, context, duration, 0, easing);
85+
86+
return {
87+
complete: () => {
88+
complete();
89+
context.stop();
90+
},
91+
cancel: (revert: boolean = false) => {
92+
cancel(revert);
93+
}
94+
};
95+
}
96+
97+
/**
98+
* 二つの音声をクロスフェードさせる。
99+
*
100+
* @param game 対象の `Game`。
101+
* @param fadeInContext フェードイン対象の `AudioPlayContext` 。
102+
* @param fadeOutContext フェードアウト対象の `AudioPlayContext` 。
103+
* @param duration クロスフェードの長さ (ms)。
104+
* @param to クロスフェード後の音量。0 未満または 1 より大きい値を指定した場合の挙動は不定。省略時は `1` 。
105+
* @param easing イージング関数。フェードインとフェードアウトで共通であることに注意。省略時は linear が指定される。
106+
*/
107+
export function crossFade(
108+
game: Game,
109+
fadeInContext: AudioPlayContext,
110+
fadeOutContext: AudioPlayContext,
111+
duration: number,
112+
to: number = 1,
113+
easing: EasingFunction = linear
114+
): AudioTransitionContext {
115+
const fadeInFuncs = fadeIn(game, fadeInContext, duration, to, easing);
116+
const fadeOutFuncs = fadeOut(game, fadeOutContext, duration, easing);
117+
118+
return {
119+
complete: () => {
120+
fadeInFuncs.complete();
121+
fadeOutFuncs.complete();
122+
},
123+
cancel: (revert: boolean = false) => {
124+
fadeInFuncs.cancel(revert);
125+
fadeOutFuncs.cancel(revert);
126+
}
127+
};
128+
}
129+
130+
/**
131+
* 音量を指定のイージングで遷移させる。
132+
*
133+
* @param game 対象の `Game`。
134+
* @param context 対象の `AudioPlayContext` 。
135+
* @param duration 遷移の長さ (ms)。
136+
* @param to 遷移後の音量。0 未満または 1 より大きい値を指定した場合の挙動は不定。
137+
* @param easing イージング関数。省略時は linear が指定される。
138+
*/
139+
export function transitionVolume(
140+
game: Game,
141+
context: AudioPlayContext,
142+
duration: number,
143+
to: number,
144+
easing: EasingFunction = linear
145+
): AudioTransitionContext {
146+
const frame = 1000 / game.fps;
147+
const from = context.volume;
148+
let elapsed = 0;
149+
context.changeVolume(Util.clamp(from, 0, 1));
150+
151+
const handler = (): boolean => {
152+
elapsed += frame;
153+
if (elapsed <= duration) {
154+
const progress = easing(elapsed, from, to - from, duration);
155+
context.changeVolume(Util.clamp(progress, 0, 1));
156+
return false;
157+
} else {
158+
context.changeVolume(to);
159+
return true;
160+
}
161+
};
162+
const remove = (): void => {
163+
if (game.onUpdate.contains(handler)) {
164+
game.onUpdate.remove(handler);
165+
}
166+
};
167+
game.onUpdate.add(handler);
168+
169+
return {
170+
complete: () => {
171+
remove();
172+
context.changeVolume(to);
173+
},
174+
cancel: revert => {
175+
remove();
176+
if (revert) {
177+
context.changeVolume(from);
178+
}
179+
}
180+
};
181+
}
182+
}

src/Game.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,11 @@ export class Game {
538538
*/
539539
skippingChanged: Trigger<boolean>;
540540

541+
/**
542+
* ティック消化後にfireされるTrigger。
543+
*/
544+
onUpdate: Trigger<void>;
545+
541546
/**
542547
* ゲームが早送りに状態にあるかどうか。
543548
*
@@ -924,6 +929,8 @@ export class Game {
924929
this._onSceneChange.add(this._handleSceneChanged, this);
925930
this._sceneChanged = this._onSceneChange;
926931

932+
this.onUpdate = new Trigger<void>();
933+
927934
this._initialScene = new Scene({
928935
game: this,
929936
assetIds: this._assetManager.globalAssetIds(),
@@ -1033,6 +1040,8 @@ export class Game {
10331040
if (advanceAge) ++this.age;
10341041
}
10351042

1043+
this.onUpdate.fire();
1044+
10361045
if (this._postTickTasks.length) {
10371046
this._flushPostTickTasks();
10381047
return scene !== this.scenes[this.scenes.length - 1];
@@ -1449,6 +1458,7 @@ export class Game {
14491458
this.onResized.removeAll();
14501459
this.onSkipChange.removeAll();
14511460
this.onSceneChange.removeAll();
1461+
this.onUpdate.removeAll();
14521462
this.handlerSet.removeAllEventFilters();
14531463

14541464
this.isSkipping = false;
@@ -1573,6 +1583,8 @@ export class Game {
15731583
this.onSkipChange = undefined!;
15741584
this.onSceneChange.destroy();
15751585
this.onSceneChange = undefined!;
1586+
this.onUpdate.destroy();
1587+
this.onUpdate = undefined!;
15761588
this.onSnapshotRequest.destroy();
15771589
this.onSnapshotRequest = undefined!;
15781590
this.join = undefined!;

src/Util.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ export module Util {
7171
return (s[0].toLowerCase() + s.slice(1).replace(/[A-Z]/g, (c: string) => "-" + c.toLowerCase())) as U;
7272
}
7373

74+
/**
75+
* 数値を範囲内[min, max]に丸める
76+
* @param num 丸める値
77+
* @param min 値の下限
78+
* @param max 値の上限
79+
*/
80+
export function clamp(num: number, min: number, max: number): number {
81+
return Math.min(Math.max(num, min), max);
82+
}
83+
7484
/**
7585
* CompositeOperation を CompositeOperationString に読み替えるテーブル。
7686
* @deprecated 非推奨である。非推奨の機能との互換性のために存在する。ゲーム開発者が使用すべきではない。

0 commit comments

Comments
 (0)