bc-asset-loader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -0
- package/dist/bc-asset-loader.cjs.js +223 -0
- package/dist/bc-asset-loader.d.ts +53 -0
- package/dist/bc-asset-loader.es.js +223 -0
- package/dist/bc-asset-loader.js +225 -0
- package/dist/bc-asset-loader.umd.js +227 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# bc-asset-loader
|
|
2
|
+
|
|
3
|
+
画像・フォント・動画を非同期で一括プリロードする JavaScript ライブラリです。
|
|
4
|
+
ロードの進捗を `progress` プロパティで参照でき、プログレスバーやパーセント表示UIの実装に使用できます。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## インストール
|
|
9
|
+
|
|
10
|
+
**npm**
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install bc-asset-loader
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**scriptタグ or CDN(jsDelivr)**
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<script src="bc-asset-loader.js"></script>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/gh/beicun/bc-asset-loader/dist/bc-asset-loader.js"></script>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 使用例
|
|
29
|
+
|
|
30
|
+
### npm経由での使用
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import BcAssetLoader from 'bc-asset-loader';
|
|
34
|
+
|
|
35
|
+
const loader = new BcAssetLoader({
|
|
36
|
+
image: [
|
|
37
|
+
{ url: '/img/hero.jpg' },
|
|
38
|
+
{ url: '/img/bg.png' },
|
|
39
|
+
],
|
|
40
|
+
font: [
|
|
41
|
+
{ url: '/fonts/NotoSansJP-Regular.woff2', family: 'Noto Sans JP', weight: '400' },
|
|
42
|
+
{ url: '/fonts/NotoSansJP-Bold.woff2', family: 'Noto Sans JP', weight: '700' },
|
|
43
|
+
],
|
|
44
|
+
extFont: [
|
|
45
|
+
{
|
|
46
|
+
url: 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;700',
|
|
47
|
+
families: [
|
|
48
|
+
{ family: 'Roboto', weights: ['400', '700'] },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
video: [
|
|
53
|
+
{ url: '/video/intro.mp4' },
|
|
54
|
+
],
|
|
55
|
+
onComplete: () => {
|
|
56
|
+
console.log('全アセットのロード完了');
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
loader.load();
|
|
61
|
+
|
|
62
|
+
// プログレスバーの更新
|
|
63
|
+
const progressBar = document.querySelector('.progress-bar');
|
|
64
|
+
|
|
65
|
+
requestAnimationFrame(function update() {
|
|
66
|
+
progressBar.style.width = `${loader.progress * 100}%`;
|
|
67
|
+
if (!loader.loaded) requestAnimationFrame(update);
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### scriptタグ or CDN(jsDelivr)での使用
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- ローカルファイル -->
|
|
75
|
+
<script src="bc-asset-loader.js"></script>
|
|
76
|
+
|
|
77
|
+
<!-- CDN(jsDelivr) -->
|
|
78
|
+
<script src="https://cdn.jsdelivr.net/gh/beicun/bc-asset-loader/dist/bc-asset-loader.js"></script>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
const loader = new BcAssetLoader({
|
|
82
|
+
image: [{ url: '/img/hero.jpg' }],
|
|
83
|
+
onComplete: () => {
|
|
84
|
+
console.log('全アセットのロード完了');
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
loader.load();
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### コンストラクタ
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
new BcAssetLoader(options: BcAssetLoaderOptions)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### BcAssetLoaderOptions
|
|
102
|
+
|
|
103
|
+
| プロパティ | 型 | 必須 | 説明 |
|
|
104
|
+
|-----------|-----|------|------|
|
|
105
|
+
| `image` | 配列 | 任意 | ロードする画像のリスト |
|
|
106
|
+
| `font` | 配列 | 任意 | ロードするセルフホストフォントのリスト |
|
|
107
|
+
| `extFont` | 配列 | 任意 | ロードする外部ホストフォント(Google Fontsなど)のリスト |
|
|
108
|
+
| `video` | 配列 | 任意 | ロードする動画のリスト |
|
|
109
|
+
| `onComplete` | 関数 | 任意 | 全アセットのロード完了時に呼ばれるコールバック |
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface BcAssetLoaderOptions {
|
|
113
|
+
image?: {
|
|
114
|
+
url: string;
|
|
115
|
+
}[];
|
|
116
|
+
font?: {
|
|
117
|
+
url: string;
|
|
118
|
+
family: string;
|
|
119
|
+
weight?: string;
|
|
120
|
+
style?: string;
|
|
121
|
+
stretch?: string;
|
|
122
|
+
}[];
|
|
123
|
+
extFont?: {
|
|
124
|
+
url: string;
|
|
125
|
+
families: {
|
|
126
|
+
family: string;
|
|
127
|
+
weights?: string[];
|
|
128
|
+
style?: 'normal' | 'italic';
|
|
129
|
+
}[];
|
|
130
|
+
}[];
|
|
131
|
+
video?: {
|
|
132
|
+
url: string;
|
|
133
|
+
}[];
|
|
134
|
+
onComplete?: () => void;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### メソッド
|
|
141
|
+
|
|
142
|
+
#### `load(): Promise<void>`
|
|
143
|
+
|
|
144
|
+
全アセットのロードを開始する。
|
|
145
|
+
`onComplete` コールバックは、ロード完了後に呼ばれる。
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
await loader.load();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### プロパティ
|
|
154
|
+
|
|
155
|
+
#### `loaded: boolean`
|
|
156
|
+
|
|
157
|
+
全アセットのロードが完了したかどうか。
|
|
158
|
+
|
|
159
|
+
#### `loadedAmount: number`
|
|
160
|
+
|
|
161
|
+
ロード済みの量。以下の合計値。
|
|
162
|
+
|
|
163
|
+
| アセット種別 | 加算タイミング・値 |
|
|
164
|
+
|-------------|-----------------|
|
|
165
|
+
| `image` | 1ファイル完了ごとに `+1` |
|
|
166
|
+
| `font` | 1ファイル完了ごとに `+1` |
|
|
167
|
+
| `extFont` | 1URLの全フォントが完了したら `+1` |
|
|
168
|
+
| `video` | バッファ済み割合(`0〜1`)をリアルタイムで加算 |
|
|
169
|
+
|
|
170
|
+
> `video` のみ小数になるため、`loadedAmount` は整数とは限らない。
|
|
171
|
+
|
|
172
|
+
#### `totalAmount: number`
|
|
173
|
+
|
|
174
|
+
アセットの総量。以下の合計値。
|
|
175
|
+
|
|
176
|
+
| アセット種別 | カウント方法 |
|
|
177
|
+
|-------------|------------|
|
|
178
|
+
| `image` | 指定した画像ファイルの数 |
|
|
179
|
+
| `font` | 指定したフォントファイルの数 |
|
|
180
|
+
| `extFont` | 指定した URL の数(1URL = 1) |
|
|
181
|
+
| `video` | 指定した動画ファイルの数 |
|
|
182
|
+
|
|
183
|
+
#### `progress: number`
|
|
184
|
+
|
|
185
|
+
ロードの進捗(`0〜1`)。`loadedAmount / totalAmount` で計算される。
|
|
186
|
+
|
|
187
|
+
#### `onComplete: () => void`
|
|
188
|
+
|
|
189
|
+
コンストラクタで渡した `onComplete` コールバックへの参照。
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## エラーハンドリング
|
|
194
|
+
|
|
195
|
+
- 個別アセットのロードが失敗した場合、コンソールにエラーを出力し、そのアセットをスキップして残りのロードを続行する。
|
|
196
|
+
- 全アセットのロード試行が完了した時点で `loaded = true` となり `onComplete` が呼ばれる。
|
|
197
|
+
- ロード失敗したアセットは `progress` の計算から除外される。
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## TypeScript
|
|
202
|
+
|
|
203
|
+
型定義ファイル(`dist/bc-asset-loader.d.ts`)が同梱されています。追加インストールは不要です。
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## ライセンス
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* bc-asset-loader v1.0.0
|
|
3
|
+
* (c) 2026 beicun
|
|
4
|
+
* Released under the MIT License
|
|
5
|
+
*/
|
|
6
|
+
//#region src/core/ImageLoader.ts
|
|
7
|
+
var ImageLoader = class {
|
|
8
|
+
url;
|
|
9
|
+
loaded = false;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.url = options.url;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 画像をロードする。
|
|
15
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
16
|
+
*/
|
|
17
|
+
load() {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const img = new Image();
|
|
20
|
+
img.onload = () => {
|
|
21
|
+
this.loaded = true;
|
|
22
|
+
resolve();
|
|
23
|
+
};
|
|
24
|
+
img.onerror = () => {
|
|
25
|
+
console.error(`ImageLoader: Failed to load image "${this.url}".`);
|
|
26
|
+
resolve();
|
|
27
|
+
};
|
|
28
|
+
img.src = this.url;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/core/FontLoader.ts
|
|
34
|
+
var FontLoader = class {
|
|
35
|
+
url;
|
|
36
|
+
family;
|
|
37
|
+
weight;
|
|
38
|
+
style;
|
|
39
|
+
stretch;
|
|
40
|
+
loaded = false;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.url = options.url;
|
|
43
|
+
this.family = options.family;
|
|
44
|
+
this.weight = options.weight ?? "400";
|
|
45
|
+
this.style = options.style ?? "normal";
|
|
46
|
+
this.stretch = options.stretch ?? "normal";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* FontFace API を使ってフォントをロードし、document.fonts に登録する。
|
|
50
|
+
* 登録後は CSS で font-family 名を指定するだけで使用できるようになる。
|
|
51
|
+
* 失敗した場合はエラーをコンソールに出力し、処理を継続する。
|
|
52
|
+
*/
|
|
53
|
+
async load() {
|
|
54
|
+
const fontFace = new FontFace(this.family, `url(${this.url})`, {
|
|
55
|
+
weight: this.weight,
|
|
56
|
+
style: this.style,
|
|
57
|
+
stretch: this.stretch
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await fontFace.load();
|
|
61
|
+
document.fonts.add(fontFace);
|
|
62
|
+
this.loaded = true;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(`FontLoader: Failed to load font "${this.family}".`, e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/core/CdnFontLoader.ts
|
|
70
|
+
var CdnFontLoader = class {
|
|
71
|
+
url;
|
|
72
|
+
families;
|
|
73
|
+
loaded = false;
|
|
74
|
+
constructor(options) {
|
|
75
|
+
this.url = options.url;
|
|
76
|
+
this.families = options.families;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 外部Webフォントをロードする。
|
|
80
|
+
* <link> タグを注入してスタイルシートを読み込み、
|
|
81
|
+
* 指定したフォントファミリーが使用可能になるまで待つ。
|
|
82
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
83
|
+
*/
|
|
84
|
+
load() {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const link = document.createElement("link");
|
|
87
|
+
link.rel = "stylesheet";
|
|
88
|
+
link.href = this.url;
|
|
89
|
+
link.onload = async () => {
|
|
90
|
+
const checks = this.families.flatMap(({ family, weights, style }) => {
|
|
91
|
+
const styleStr = style === "italic" ? "italic" : "normal";
|
|
92
|
+
return (weights && weights.length > 0 ? weights : ["400"]).map((weight) => document.fonts.load(`${styleStr} ${weight} 16px "${family}"`));
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
await Promise.all(checks);
|
|
96
|
+
this.loaded = true;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`CdnFontLoader: Failed to verify fonts from "${this.url}".`, e);
|
|
99
|
+
}
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
link.onerror = () => {
|
|
103
|
+
console.error(`CdnFontLoader: Failed to load stylesheet "${this.url}".`);
|
|
104
|
+
resolve();
|
|
105
|
+
};
|
|
106
|
+
document.head.appendChild(link);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/core/VideoLoader.ts
|
|
112
|
+
var VideoLoader = class {
|
|
113
|
+
url;
|
|
114
|
+
loaded = false;
|
|
115
|
+
/** バッファ済みの割合(0〜1)。progress イベントのたびに更新される。 */
|
|
116
|
+
bufferProgress = 0;
|
|
117
|
+
constructor(options) {
|
|
118
|
+
this.url = options.url;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 動画をロードする。
|
|
122
|
+
* progress イベントでバッファ済み割合をリアルタイムに onProgress へ通知し、
|
|
123
|
+
* canplaythrough イベントを完了とみなして resolve する。
|
|
124
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
125
|
+
* @param onProgress - バッファ済み割合(0〜1)を受け取るコールバック
|
|
126
|
+
*/
|
|
127
|
+
load(onProgress) {
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
const video = document.createElement("video");
|
|
130
|
+
video.preload = "auto";
|
|
131
|
+
video.src = this.url;
|
|
132
|
+
video.addEventListener("progress", () => {
|
|
133
|
+
if (video.duration && video.buffered.length > 0) {
|
|
134
|
+
this.bufferProgress = video.buffered.end(video.buffered.length - 1) / video.duration;
|
|
135
|
+
onProgress(this.bufferProgress);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
video.addEventListener("canplaythrough", () => {
|
|
139
|
+
this.bufferProgress = 1;
|
|
140
|
+
this.loaded = true;
|
|
141
|
+
onProgress(1);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
video.addEventListener("error", () => {
|
|
145
|
+
console.error(`VideoLoader: Failed to load video "${this.url}".`);
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
video.load();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/index.ts
|
|
154
|
+
var BcAssetLoader = class {
|
|
155
|
+
loaded = false;
|
|
156
|
+
/**
|
|
157
|
+
* ロード済みの量。image・font・extFont は完了ごとに +1、
|
|
158
|
+
* video はバッファ済み割合(0〜1)をリアルタイムで加算するため小数になる場合がある。
|
|
159
|
+
*/
|
|
160
|
+
loadedAmount = 0;
|
|
161
|
+
/**
|
|
162
|
+
* アセットの総量。
|
|
163
|
+
* extFont は URL 単位で 1 としてカウントする。
|
|
164
|
+
*/
|
|
165
|
+
totalAmount = 0;
|
|
166
|
+
/** ロードの進捗(0〜1)。loadedAmount / totalAmount で計算される。 */
|
|
167
|
+
progress = 0;
|
|
168
|
+
onComplete;
|
|
169
|
+
imageLoaders;
|
|
170
|
+
fontLoaders;
|
|
171
|
+
extFontLoaders;
|
|
172
|
+
videoLoaders;
|
|
173
|
+
constructor(options) {
|
|
174
|
+
this.onComplete = options.onComplete ?? (() => {});
|
|
175
|
+
this.imageLoaders = (options.image ?? []).map((o) => new ImageLoader(o));
|
|
176
|
+
this.fontLoaders = (options.font ?? []).map((o) => new FontLoader(o));
|
|
177
|
+
this.extFontLoaders = (options.extFont ?? []).map((o) => new CdnFontLoader(o));
|
|
178
|
+
this.videoLoaders = (options.video ?? []).map((o) => new VideoLoader(o));
|
|
179
|
+
this.totalAmount = this.imageLoaders.length + this.fontLoaders.length + this.extFontLoaders.length + this.videoLoaders.length;
|
|
180
|
+
}
|
|
181
|
+
async load() {
|
|
182
|
+
if (this.totalAmount === 0) {
|
|
183
|
+
this.loaded = true;
|
|
184
|
+
this.progress = 1;
|
|
185
|
+
this.onComplete();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const imagePromises = this.imageLoaders.map((loader) => loader.load().then(() => {
|
|
189
|
+
this.loadedAmount += 1;
|
|
190
|
+
this._updateProgress();
|
|
191
|
+
}));
|
|
192
|
+
const fontPromises = this.fontLoaders.map((loader) => loader.load().then(() => {
|
|
193
|
+
this.loadedAmount += 1;
|
|
194
|
+
this._updateProgress();
|
|
195
|
+
}));
|
|
196
|
+
const extFontPromises = this.extFontLoaders.map((loader) => loader.load().then(() => {
|
|
197
|
+
this.loadedAmount += 1;
|
|
198
|
+
this._updateProgress();
|
|
199
|
+
}));
|
|
200
|
+
const videoProgressValues = this.videoLoaders.map(() => 0);
|
|
201
|
+
const videoPromises = this.videoLoaders.map((loader, index) => loader.load((progress) => {
|
|
202
|
+
const prev = videoProgressValues[index];
|
|
203
|
+
videoProgressValues[index] = progress;
|
|
204
|
+
this.loadedAmount += progress - prev;
|
|
205
|
+
this._updateProgress();
|
|
206
|
+
}));
|
|
207
|
+
await Promise.all([
|
|
208
|
+
...imagePromises,
|
|
209
|
+
...fontPromises,
|
|
210
|
+
...extFontPromises,
|
|
211
|
+
...videoPromises
|
|
212
|
+
]);
|
|
213
|
+
this.loaded = true;
|
|
214
|
+
this.loadedAmount = this.totalAmount;
|
|
215
|
+
this.progress = 1;
|
|
216
|
+
this.onComplete();
|
|
217
|
+
}
|
|
218
|
+
_updateProgress() {
|
|
219
|
+
this.progress = this.loadedAmount / this.totalAmount;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
module.exports = BcAssetLoader;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
declare class BcAssetLoader {
|
|
2
|
+
loaded: boolean;
|
|
3
|
+
/**
|
|
4
|
+
* ロード済みの量。image・font・extFont は完了ごとに +1、
|
|
5
|
+
* video はバッファ済み割合(0〜1)をリアルタイムで加算するため小数になる場合がある。
|
|
6
|
+
*/
|
|
7
|
+
loadedAmount: number;
|
|
8
|
+
/**
|
|
9
|
+
* アセットの総量。
|
|
10
|
+
* extFont は URL 単位で 1 としてカウントする。
|
|
11
|
+
*/
|
|
12
|
+
totalAmount: number;
|
|
13
|
+
/** ロードの進捗(0〜1)。loadedAmount / totalAmount で計算される。 */
|
|
14
|
+
progress: number;
|
|
15
|
+
onComplete: () => void;
|
|
16
|
+
private imageLoaders;
|
|
17
|
+
private fontLoaders;
|
|
18
|
+
private extFontLoaders;
|
|
19
|
+
private videoLoaders;
|
|
20
|
+
constructor(options: BcAssetLoaderOptions);
|
|
21
|
+
load(): Promise<void>;
|
|
22
|
+
private _updateProgress;
|
|
23
|
+
}
|
|
24
|
+
export default BcAssetLoader;
|
|
25
|
+
|
|
26
|
+
declare interface BcAssetLoaderOptions {
|
|
27
|
+
image?: {
|
|
28
|
+
url: string;
|
|
29
|
+
}[];
|
|
30
|
+
font?: {
|
|
31
|
+
url: string;
|
|
32
|
+
family: string;
|
|
33
|
+
weight?: string;
|
|
34
|
+
style?: string;
|
|
35
|
+
stretch?: string;
|
|
36
|
+
}[];
|
|
37
|
+
extFont?: {
|
|
38
|
+
url: string;
|
|
39
|
+
/** ロード完了の確認に使うフォントファミリーのリスト */
|
|
40
|
+
families: {
|
|
41
|
+
family: string;
|
|
42
|
+
weights?: string[];
|
|
43
|
+
style?: 'normal' | 'italic';
|
|
44
|
+
}[];
|
|
45
|
+
}[];
|
|
46
|
+
video?: {
|
|
47
|
+
url: string;
|
|
48
|
+
}[];
|
|
49
|
+
/** 全アセットのロード完了時に呼ばれるコールバック */
|
|
50
|
+
onComplete?: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { }
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* bc-asset-loader v1.0.0
|
|
3
|
+
* (c) 2026 beicun
|
|
4
|
+
* Released under the MIT License
|
|
5
|
+
*/
|
|
6
|
+
//#region src/core/ImageLoader.ts
|
|
7
|
+
var ImageLoader = class {
|
|
8
|
+
url;
|
|
9
|
+
loaded = false;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.url = options.url;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 画像をロードする。
|
|
15
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
16
|
+
*/
|
|
17
|
+
load() {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const img = new Image();
|
|
20
|
+
img.onload = () => {
|
|
21
|
+
this.loaded = true;
|
|
22
|
+
resolve();
|
|
23
|
+
};
|
|
24
|
+
img.onerror = () => {
|
|
25
|
+
console.error(`ImageLoader: Failed to load image "${this.url}".`);
|
|
26
|
+
resolve();
|
|
27
|
+
};
|
|
28
|
+
img.src = this.url;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/core/FontLoader.ts
|
|
34
|
+
var FontLoader = class {
|
|
35
|
+
url;
|
|
36
|
+
family;
|
|
37
|
+
weight;
|
|
38
|
+
style;
|
|
39
|
+
stretch;
|
|
40
|
+
loaded = false;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.url = options.url;
|
|
43
|
+
this.family = options.family;
|
|
44
|
+
this.weight = options.weight ?? "400";
|
|
45
|
+
this.style = options.style ?? "normal";
|
|
46
|
+
this.stretch = options.stretch ?? "normal";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* FontFace API を使ってフォントをロードし、document.fonts に登録する。
|
|
50
|
+
* 登録後は CSS で font-family 名を指定するだけで使用できるようになる。
|
|
51
|
+
* 失敗した場合はエラーをコンソールに出力し、処理を継続する。
|
|
52
|
+
*/
|
|
53
|
+
async load() {
|
|
54
|
+
const fontFace = new FontFace(this.family, `url(${this.url})`, {
|
|
55
|
+
weight: this.weight,
|
|
56
|
+
style: this.style,
|
|
57
|
+
stretch: this.stretch
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await fontFace.load();
|
|
61
|
+
document.fonts.add(fontFace);
|
|
62
|
+
this.loaded = true;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(`FontLoader: Failed to load font "${this.family}".`, e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/core/CdnFontLoader.ts
|
|
70
|
+
var CdnFontLoader = class {
|
|
71
|
+
url;
|
|
72
|
+
families;
|
|
73
|
+
loaded = false;
|
|
74
|
+
constructor(options) {
|
|
75
|
+
this.url = options.url;
|
|
76
|
+
this.families = options.families;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 外部Webフォントをロードする。
|
|
80
|
+
* <link> タグを注入してスタイルシートを読み込み、
|
|
81
|
+
* 指定したフォントファミリーが使用可能になるまで待つ。
|
|
82
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
83
|
+
*/
|
|
84
|
+
load() {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const link = document.createElement("link");
|
|
87
|
+
link.rel = "stylesheet";
|
|
88
|
+
link.href = this.url;
|
|
89
|
+
link.onload = async () => {
|
|
90
|
+
const checks = this.families.flatMap(({ family, weights, style }) => {
|
|
91
|
+
const styleStr = style === "italic" ? "italic" : "normal";
|
|
92
|
+
return (weights && weights.length > 0 ? weights : ["400"]).map((weight) => document.fonts.load(`${styleStr} ${weight} 16px "${family}"`));
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
await Promise.all(checks);
|
|
96
|
+
this.loaded = true;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`CdnFontLoader: Failed to verify fonts from "${this.url}".`, e);
|
|
99
|
+
}
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
link.onerror = () => {
|
|
103
|
+
console.error(`CdnFontLoader: Failed to load stylesheet "${this.url}".`);
|
|
104
|
+
resolve();
|
|
105
|
+
};
|
|
106
|
+
document.head.appendChild(link);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/core/VideoLoader.ts
|
|
112
|
+
var VideoLoader = class {
|
|
113
|
+
url;
|
|
114
|
+
loaded = false;
|
|
115
|
+
/** バッファ済みの割合(0〜1)。progress イベントのたびに更新される。 */
|
|
116
|
+
bufferProgress = 0;
|
|
117
|
+
constructor(options) {
|
|
118
|
+
this.url = options.url;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 動画をロードする。
|
|
122
|
+
* progress イベントでバッファ済み割合をリアルタイムに onProgress へ通知し、
|
|
123
|
+
* canplaythrough イベントを完了とみなして resolve する。
|
|
124
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
125
|
+
* @param onProgress - バッファ済み割合(0〜1)を受け取るコールバック
|
|
126
|
+
*/
|
|
127
|
+
load(onProgress) {
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
const video = document.createElement("video");
|
|
130
|
+
video.preload = "auto";
|
|
131
|
+
video.src = this.url;
|
|
132
|
+
video.addEventListener("progress", () => {
|
|
133
|
+
if (video.duration && video.buffered.length > 0) {
|
|
134
|
+
this.bufferProgress = video.buffered.end(video.buffered.length - 1) / video.duration;
|
|
135
|
+
onProgress(this.bufferProgress);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
video.addEventListener("canplaythrough", () => {
|
|
139
|
+
this.bufferProgress = 1;
|
|
140
|
+
this.loaded = true;
|
|
141
|
+
onProgress(1);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
video.addEventListener("error", () => {
|
|
145
|
+
console.error(`VideoLoader: Failed to load video "${this.url}".`);
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
video.load();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/index.ts
|
|
154
|
+
var BcAssetLoader = class {
|
|
155
|
+
loaded = false;
|
|
156
|
+
/**
|
|
157
|
+
* ロード済みの量。image・font・extFont は完了ごとに +1、
|
|
158
|
+
* video はバッファ済み割合(0〜1)をリアルタイムで加算するため小数になる場合がある。
|
|
159
|
+
*/
|
|
160
|
+
loadedAmount = 0;
|
|
161
|
+
/**
|
|
162
|
+
* アセットの総量。
|
|
163
|
+
* extFont は URL 単位で 1 としてカウントする。
|
|
164
|
+
*/
|
|
165
|
+
totalAmount = 0;
|
|
166
|
+
/** ロードの進捗(0〜1)。loadedAmount / totalAmount で計算される。 */
|
|
167
|
+
progress = 0;
|
|
168
|
+
onComplete;
|
|
169
|
+
imageLoaders;
|
|
170
|
+
fontLoaders;
|
|
171
|
+
extFontLoaders;
|
|
172
|
+
videoLoaders;
|
|
173
|
+
constructor(options) {
|
|
174
|
+
this.onComplete = options.onComplete ?? (() => {});
|
|
175
|
+
this.imageLoaders = (options.image ?? []).map((o) => new ImageLoader(o));
|
|
176
|
+
this.fontLoaders = (options.font ?? []).map((o) => new FontLoader(o));
|
|
177
|
+
this.extFontLoaders = (options.extFont ?? []).map((o) => new CdnFontLoader(o));
|
|
178
|
+
this.videoLoaders = (options.video ?? []).map((o) => new VideoLoader(o));
|
|
179
|
+
this.totalAmount = this.imageLoaders.length + this.fontLoaders.length + this.extFontLoaders.length + this.videoLoaders.length;
|
|
180
|
+
}
|
|
181
|
+
async load() {
|
|
182
|
+
if (this.totalAmount === 0) {
|
|
183
|
+
this.loaded = true;
|
|
184
|
+
this.progress = 1;
|
|
185
|
+
this.onComplete();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const imagePromises = this.imageLoaders.map((loader) => loader.load().then(() => {
|
|
189
|
+
this.loadedAmount += 1;
|
|
190
|
+
this._updateProgress();
|
|
191
|
+
}));
|
|
192
|
+
const fontPromises = this.fontLoaders.map((loader) => loader.load().then(() => {
|
|
193
|
+
this.loadedAmount += 1;
|
|
194
|
+
this._updateProgress();
|
|
195
|
+
}));
|
|
196
|
+
const extFontPromises = this.extFontLoaders.map((loader) => loader.load().then(() => {
|
|
197
|
+
this.loadedAmount += 1;
|
|
198
|
+
this._updateProgress();
|
|
199
|
+
}));
|
|
200
|
+
const videoProgressValues = this.videoLoaders.map(() => 0);
|
|
201
|
+
const videoPromises = this.videoLoaders.map((loader, index) => loader.load((progress) => {
|
|
202
|
+
const prev = videoProgressValues[index];
|
|
203
|
+
videoProgressValues[index] = progress;
|
|
204
|
+
this.loadedAmount += progress - prev;
|
|
205
|
+
this._updateProgress();
|
|
206
|
+
}));
|
|
207
|
+
await Promise.all([
|
|
208
|
+
...imagePromises,
|
|
209
|
+
...fontPromises,
|
|
210
|
+
...extFontPromises,
|
|
211
|
+
...videoPromises
|
|
212
|
+
]);
|
|
213
|
+
this.loaded = true;
|
|
214
|
+
this.loadedAmount = this.totalAmount;
|
|
215
|
+
this.progress = 1;
|
|
216
|
+
this.onComplete();
|
|
217
|
+
}
|
|
218
|
+
_updateProgress() {
|
|
219
|
+
this.progress = this.loadedAmount / this.totalAmount;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
export { BcAssetLoader as default };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* bc-asset-loader v1.0.0
|
|
3
|
+
* (c) 2026 beicun
|
|
4
|
+
* Released under the MIT License
|
|
5
|
+
*/
|
|
6
|
+
var BcAssetLoader = (function() {
|
|
7
|
+
//#region src/core/ImageLoader.ts
|
|
8
|
+
var ImageLoader = class {
|
|
9
|
+
url;
|
|
10
|
+
loaded = false;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.url = options.url;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 画像をロードする。
|
|
16
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
17
|
+
*/
|
|
18
|
+
load() {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const img = new Image();
|
|
21
|
+
img.onload = () => {
|
|
22
|
+
this.loaded = true;
|
|
23
|
+
resolve();
|
|
24
|
+
};
|
|
25
|
+
img.onerror = () => {
|
|
26
|
+
console.error(`ImageLoader: Failed to load image "${this.url}".`);
|
|
27
|
+
resolve();
|
|
28
|
+
};
|
|
29
|
+
img.src = this.url;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/core/FontLoader.ts
|
|
35
|
+
var FontLoader = class {
|
|
36
|
+
url;
|
|
37
|
+
family;
|
|
38
|
+
weight;
|
|
39
|
+
style;
|
|
40
|
+
stretch;
|
|
41
|
+
loaded = false;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.url = options.url;
|
|
44
|
+
this.family = options.family;
|
|
45
|
+
this.weight = options.weight ?? "400";
|
|
46
|
+
this.style = options.style ?? "normal";
|
|
47
|
+
this.stretch = options.stretch ?? "normal";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* FontFace API を使ってフォントをロードし、document.fonts に登録する。
|
|
51
|
+
* 登録後は CSS で font-family 名を指定するだけで使用できるようになる。
|
|
52
|
+
* 失敗した場合はエラーをコンソールに出力し、処理を継続する。
|
|
53
|
+
*/
|
|
54
|
+
async load() {
|
|
55
|
+
const fontFace = new FontFace(this.family, `url(${this.url})`, {
|
|
56
|
+
weight: this.weight,
|
|
57
|
+
style: this.style,
|
|
58
|
+
stretch: this.stretch
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
await fontFace.load();
|
|
62
|
+
document.fonts.add(fontFace);
|
|
63
|
+
this.loaded = true;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(`FontLoader: Failed to load font "${this.family}".`, e);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/core/CdnFontLoader.ts
|
|
71
|
+
var CdnFontLoader = class {
|
|
72
|
+
url;
|
|
73
|
+
families;
|
|
74
|
+
loaded = false;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.url = options.url;
|
|
77
|
+
this.families = options.families;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 外部Webフォントをロードする。
|
|
81
|
+
* <link> タグを注入してスタイルシートを読み込み、
|
|
82
|
+
* 指定したフォントファミリーが使用可能になるまで待つ。
|
|
83
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
84
|
+
*/
|
|
85
|
+
load() {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const link = document.createElement("link");
|
|
88
|
+
link.rel = "stylesheet";
|
|
89
|
+
link.href = this.url;
|
|
90
|
+
link.onload = async () => {
|
|
91
|
+
const checks = this.families.flatMap(({ family, weights, style }) => {
|
|
92
|
+
const styleStr = style === "italic" ? "italic" : "normal";
|
|
93
|
+
return (weights && weights.length > 0 ? weights : ["400"]).map((weight) => document.fonts.load(`${styleStr} ${weight} 16px "${family}"`));
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await Promise.all(checks);
|
|
97
|
+
this.loaded = true;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error(`CdnFontLoader: Failed to verify fonts from "${this.url}".`, e);
|
|
100
|
+
}
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
link.onerror = () => {
|
|
104
|
+
console.error(`CdnFontLoader: Failed to load stylesheet "${this.url}".`);
|
|
105
|
+
resolve();
|
|
106
|
+
};
|
|
107
|
+
document.head.appendChild(link);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/core/VideoLoader.ts
|
|
113
|
+
var VideoLoader = class {
|
|
114
|
+
url;
|
|
115
|
+
loaded = false;
|
|
116
|
+
/** バッファ済みの割合(0〜1)。progress イベントのたびに更新される。 */
|
|
117
|
+
bufferProgress = 0;
|
|
118
|
+
constructor(options) {
|
|
119
|
+
this.url = options.url;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 動画をロードする。
|
|
123
|
+
* progress イベントでバッファ済み割合をリアルタイムに onProgress へ通知し、
|
|
124
|
+
* canplaythrough イベントを完了とみなして resolve する。
|
|
125
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
126
|
+
* @param onProgress - バッファ済み割合(0〜1)を受け取るコールバック
|
|
127
|
+
*/
|
|
128
|
+
load(onProgress) {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const video = document.createElement("video");
|
|
131
|
+
video.preload = "auto";
|
|
132
|
+
video.src = this.url;
|
|
133
|
+
video.addEventListener("progress", () => {
|
|
134
|
+
if (video.duration && video.buffered.length > 0) {
|
|
135
|
+
this.bufferProgress = video.buffered.end(video.buffered.length - 1) / video.duration;
|
|
136
|
+
onProgress(this.bufferProgress);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
video.addEventListener("canplaythrough", () => {
|
|
140
|
+
this.bufferProgress = 1;
|
|
141
|
+
this.loaded = true;
|
|
142
|
+
onProgress(1);
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
video.addEventListener("error", () => {
|
|
146
|
+
console.error(`VideoLoader: Failed to load video "${this.url}".`);
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
video.load();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/index.ts
|
|
155
|
+
var BcAssetLoader = class {
|
|
156
|
+
loaded = false;
|
|
157
|
+
/**
|
|
158
|
+
* ロード済みの量。image・font・extFont は完了ごとに +1、
|
|
159
|
+
* video はバッファ済み割合(0〜1)をリアルタイムで加算するため小数になる場合がある。
|
|
160
|
+
*/
|
|
161
|
+
loadedAmount = 0;
|
|
162
|
+
/**
|
|
163
|
+
* アセットの総量。
|
|
164
|
+
* extFont は URL 単位で 1 としてカウントする。
|
|
165
|
+
*/
|
|
166
|
+
totalAmount = 0;
|
|
167
|
+
/** ロードの進捗(0〜1)。loadedAmount / totalAmount で計算される。 */
|
|
168
|
+
progress = 0;
|
|
169
|
+
onComplete;
|
|
170
|
+
imageLoaders;
|
|
171
|
+
fontLoaders;
|
|
172
|
+
extFontLoaders;
|
|
173
|
+
videoLoaders;
|
|
174
|
+
constructor(options) {
|
|
175
|
+
this.onComplete = options.onComplete ?? (() => {});
|
|
176
|
+
this.imageLoaders = (options.image ?? []).map((o) => new ImageLoader(o));
|
|
177
|
+
this.fontLoaders = (options.font ?? []).map((o) => new FontLoader(o));
|
|
178
|
+
this.extFontLoaders = (options.extFont ?? []).map((o) => new CdnFontLoader(o));
|
|
179
|
+
this.videoLoaders = (options.video ?? []).map((o) => new VideoLoader(o));
|
|
180
|
+
this.totalAmount = this.imageLoaders.length + this.fontLoaders.length + this.extFontLoaders.length + this.videoLoaders.length;
|
|
181
|
+
}
|
|
182
|
+
async load() {
|
|
183
|
+
if (this.totalAmount === 0) {
|
|
184
|
+
this.loaded = true;
|
|
185
|
+
this.progress = 1;
|
|
186
|
+
this.onComplete();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const imagePromises = this.imageLoaders.map((loader) => loader.load().then(() => {
|
|
190
|
+
this.loadedAmount += 1;
|
|
191
|
+
this._updateProgress();
|
|
192
|
+
}));
|
|
193
|
+
const fontPromises = this.fontLoaders.map((loader) => loader.load().then(() => {
|
|
194
|
+
this.loadedAmount += 1;
|
|
195
|
+
this._updateProgress();
|
|
196
|
+
}));
|
|
197
|
+
const extFontPromises = this.extFontLoaders.map((loader) => loader.load().then(() => {
|
|
198
|
+
this.loadedAmount += 1;
|
|
199
|
+
this._updateProgress();
|
|
200
|
+
}));
|
|
201
|
+
const videoProgressValues = this.videoLoaders.map(() => 0);
|
|
202
|
+
const videoPromises = this.videoLoaders.map((loader, index) => loader.load((progress) => {
|
|
203
|
+
const prev = videoProgressValues[index];
|
|
204
|
+
videoProgressValues[index] = progress;
|
|
205
|
+
this.loadedAmount += progress - prev;
|
|
206
|
+
this._updateProgress();
|
|
207
|
+
}));
|
|
208
|
+
await Promise.all([
|
|
209
|
+
...imagePromises,
|
|
210
|
+
...fontPromises,
|
|
211
|
+
...extFontPromises,
|
|
212
|
+
...videoPromises
|
|
213
|
+
]);
|
|
214
|
+
this.loaded = true;
|
|
215
|
+
this.loadedAmount = this.totalAmount;
|
|
216
|
+
this.progress = 1;
|
|
217
|
+
this.onComplete();
|
|
218
|
+
}
|
|
219
|
+
_updateProgress() {
|
|
220
|
+
this.progress = this.loadedAmount / this.totalAmount;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
//#endregion
|
|
224
|
+
return BcAssetLoader;
|
|
225
|
+
})();
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* bc-asset-loader v1.0.0
|
|
3
|
+
* (c) 2026 beicun
|
|
4
|
+
* Released under the MIT License
|
|
5
|
+
*/
|
|
6
|
+
(function(global, factory) {
|
|
7
|
+
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define([], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.BcAssetLoader = factory());
|
|
8
|
+
})(this, function() {
|
|
9
|
+
//#region src/core/ImageLoader.ts
|
|
10
|
+
var ImageLoader = class {
|
|
11
|
+
url;
|
|
12
|
+
loaded = false;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.url = options.url;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 画像をロードする。
|
|
18
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
19
|
+
*/
|
|
20
|
+
load() {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const img = new Image();
|
|
23
|
+
img.onload = () => {
|
|
24
|
+
this.loaded = true;
|
|
25
|
+
resolve();
|
|
26
|
+
};
|
|
27
|
+
img.onerror = () => {
|
|
28
|
+
console.error(`ImageLoader: Failed to load image "${this.url}".`);
|
|
29
|
+
resolve();
|
|
30
|
+
};
|
|
31
|
+
img.src = this.url;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/core/FontLoader.ts
|
|
37
|
+
var FontLoader = class {
|
|
38
|
+
url;
|
|
39
|
+
family;
|
|
40
|
+
weight;
|
|
41
|
+
style;
|
|
42
|
+
stretch;
|
|
43
|
+
loaded = false;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.url = options.url;
|
|
46
|
+
this.family = options.family;
|
|
47
|
+
this.weight = options.weight ?? "400";
|
|
48
|
+
this.style = options.style ?? "normal";
|
|
49
|
+
this.stretch = options.stretch ?? "normal";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* FontFace API を使ってフォントをロードし、document.fonts に登録する。
|
|
53
|
+
* 登録後は CSS で font-family 名を指定するだけで使用できるようになる。
|
|
54
|
+
* 失敗した場合はエラーをコンソールに出力し、処理を継続する。
|
|
55
|
+
*/
|
|
56
|
+
async load() {
|
|
57
|
+
const fontFace = new FontFace(this.family, `url(${this.url})`, {
|
|
58
|
+
weight: this.weight,
|
|
59
|
+
style: this.style,
|
|
60
|
+
stretch: this.stretch
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
await fontFace.load();
|
|
64
|
+
document.fonts.add(fontFace);
|
|
65
|
+
this.loaded = true;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(`FontLoader: Failed to load font "${this.family}".`, e);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/core/CdnFontLoader.ts
|
|
73
|
+
var CdnFontLoader = class {
|
|
74
|
+
url;
|
|
75
|
+
families;
|
|
76
|
+
loaded = false;
|
|
77
|
+
constructor(options) {
|
|
78
|
+
this.url = options.url;
|
|
79
|
+
this.families = options.families;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 外部Webフォントをロードする。
|
|
83
|
+
* <link> タグを注入してスタイルシートを読み込み、
|
|
84
|
+
* 指定したフォントファミリーが使用可能になるまで待つ。
|
|
85
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
86
|
+
*/
|
|
87
|
+
load() {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const link = document.createElement("link");
|
|
90
|
+
link.rel = "stylesheet";
|
|
91
|
+
link.href = this.url;
|
|
92
|
+
link.onload = async () => {
|
|
93
|
+
const checks = this.families.flatMap(({ family, weights, style }) => {
|
|
94
|
+
const styleStr = style === "italic" ? "italic" : "normal";
|
|
95
|
+
return (weights && weights.length > 0 ? weights : ["400"]).map((weight) => document.fonts.load(`${styleStr} ${weight} 16px "${family}"`));
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
await Promise.all(checks);
|
|
99
|
+
this.loaded = true;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error(`CdnFontLoader: Failed to verify fonts from "${this.url}".`, e);
|
|
102
|
+
}
|
|
103
|
+
resolve();
|
|
104
|
+
};
|
|
105
|
+
link.onerror = () => {
|
|
106
|
+
console.error(`CdnFontLoader: Failed to load stylesheet "${this.url}".`);
|
|
107
|
+
resolve();
|
|
108
|
+
};
|
|
109
|
+
document.head.appendChild(link);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/core/VideoLoader.ts
|
|
115
|
+
var VideoLoader = class {
|
|
116
|
+
url;
|
|
117
|
+
loaded = false;
|
|
118
|
+
/** バッファ済みの割合(0〜1)。progress イベントのたびに更新される。 */
|
|
119
|
+
bufferProgress = 0;
|
|
120
|
+
constructor(options) {
|
|
121
|
+
this.url = options.url;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 動画をロードする。
|
|
125
|
+
* progress イベントでバッファ済み割合をリアルタイムに onProgress へ通知し、
|
|
126
|
+
* canplaythrough イベントを完了とみなして resolve する。
|
|
127
|
+
* 失敗した場合はエラーをコンソールに出力し、resolve する(後続のロードを止めない)。
|
|
128
|
+
* @param onProgress - バッファ済み割合(0〜1)を受け取るコールバック
|
|
129
|
+
*/
|
|
130
|
+
load(onProgress) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const video = document.createElement("video");
|
|
133
|
+
video.preload = "auto";
|
|
134
|
+
video.src = this.url;
|
|
135
|
+
video.addEventListener("progress", () => {
|
|
136
|
+
if (video.duration && video.buffered.length > 0) {
|
|
137
|
+
this.bufferProgress = video.buffered.end(video.buffered.length - 1) / video.duration;
|
|
138
|
+
onProgress(this.bufferProgress);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
video.addEventListener("canplaythrough", () => {
|
|
142
|
+
this.bufferProgress = 1;
|
|
143
|
+
this.loaded = true;
|
|
144
|
+
onProgress(1);
|
|
145
|
+
resolve();
|
|
146
|
+
});
|
|
147
|
+
video.addEventListener("error", () => {
|
|
148
|
+
console.error(`VideoLoader: Failed to load video "${this.url}".`);
|
|
149
|
+
resolve();
|
|
150
|
+
});
|
|
151
|
+
video.load();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/index.ts
|
|
157
|
+
var BcAssetLoader = class {
|
|
158
|
+
loaded = false;
|
|
159
|
+
/**
|
|
160
|
+
* ロード済みの量。image・font・extFont は完了ごとに +1、
|
|
161
|
+
* video はバッファ済み割合(0〜1)をリアルタイムで加算するため小数になる場合がある。
|
|
162
|
+
*/
|
|
163
|
+
loadedAmount = 0;
|
|
164
|
+
/**
|
|
165
|
+
* アセットの総量。
|
|
166
|
+
* extFont は URL 単位で 1 としてカウントする。
|
|
167
|
+
*/
|
|
168
|
+
totalAmount = 0;
|
|
169
|
+
/** ロードの進捗(0〜1)。loadedAmount / totalAmount で計算される。 */
|
|
170
|
+
progress = 0;
|
|
171
|
+
onComplete;
|
|
172
|
+
imageLoaders;
|
|
173
|
+
fontLoaders;
|
|
174
|
+
extFontLoaders;
|
|
175
|
+
videoLoaders;
|
|
176
|
+
constructor(options) {
|
|
177
|
+
this.onComplete = options.onComplete ?? (() => {});
|
|
178
|
+
this.imageLoaders = (options.image ?? []).map((o) => new ImageLoader(o));
|
|
179
|
+
this.fontLoaders = (options.font ?? []).map((o) => new FontLoader(o));
|
|
180
|
+
this.extFontLoaders = (options.extFont ?? []).map((o) => new CdnFontLoader(o));
|
|
181
|
+
this.videoLoaders = (options.video ?? []).map((o) => new VideoLoader(o));
|
|
182
|
+
this.totalAmount = this.imageLoaders.length + this.fontLoaders.length + this.extFontLoaders.length + this.videoLoaders.length;
|
|
183
|
+
}
|
|
184
|
+
async load() {
|
|
185
|
+
if (this.totalAmount === 0) {
|
|
186
|
+
this.loaded = true;
|
|
187
|
+
this.progress = 1;
|
|
188
|
+
this.onComplete();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const imagePromises = this.imageLoaders.map((loader) => loader.load().then(() => {
|
|
192
|
+
this.loadedAmount += 1;
|
|
193
|
+
this._updateProgress();
|
|
194
|
+
}));
|
|
195
|
+
const fontPromises = this.fontLoaders.map((loader) => loader.load().then(() => {
|
|
196
|
+
this.loadedAmount += 1;
|
|
197
|
+
this._updateProgress();
|
|
198
|
+
}));
|
|
199
|
+
const extFontPromises = this.extFontLoaders.map((loader) => loader.load().then(() => {
|
|
200
|
+
this.loadedAmount += 1;
|
|
201
|
+
this._updateProgress();
|
|
202
|
+
}));
|
|
203
|
+
const videoProgressValues = this.videoLoaders.map(() => 0);
|
|
204
|
+
const videoPromises = this.videoLoaders.map((loader, index) => loader.load((progress) => {
|
|
205
|
+
const prev = videoProgressValues[index];
|
|
206
|
+
videoProgressValues[index] = progress;
|
|
207
|
+
this.loadedAmount += progress - prev;
|
|
208
|
+
this._updateProgress();
|
|
209
|
+
}));
|
|
210
|
+
await Promise.all([
|
|
211
|
+
...imagePromises,
|
|
212
|
+
...fontPromises,
|
|
213
|
+
...extFontPromises,
|
|
214
|
+
...videoPromises
|
|
215
|
+
]);
|
|
216
|
+
this.loaded = true;
|
|
217
|
+
this.loadedAmount = this.totalAmount;
|
|
218
|
+
this.progress = 1;
|
|
219
|
+
this.onComplete();
|
|
220
|
+
}
|
|
221
|
+
_updateProgress() {
|
|
222
|
+
this.progress = this.loadedAmount / this.totalAmount;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
//#endregion
|
|
226
|
+
return BcAssetLoader;
|
|
227
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bc-asset-loader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "beicun",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite --config vite.config.dev.ts",
|
|
9
|
+
"build": "tsc && vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\" \"demo/**/*.{ts,html,css}\""
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/bc-asset-loader.cjs.js",
|
|
14
|
+
"module": "./dist/bc-asset-loader.es.js",
|
|
15
|
+
"types": "./dist/bc-asset-loader.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/bc-asset-loader.es.js",
|
|
19
|
+
"require": "./dist/bc-asset-loader.cjs.js",
|
|
20
|
+
"types": "./dist/bc-asset-loader.d.ts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^25.5.0",
|
|
28
|
+
"prettier": "^3.8.1",
|
|
29
|
+
"typescript": "~5.9.3",
|
|
30
|
+
"vite": "^8.0.1",
|
|
31
|
+
"vite-plugin-dts": "^4.5.4"
|
|
32
|
+
}
|
|
33
|
+
}
|