@umicat/three-sdk 0.14.0 → 0.16.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 +4 -3
- package/dist/GameAudio.js +13 -0
- package/dist/capture/RecordingManager.d.ts +19 -0
- package/dist/capture/RecordingManager.js +67 -0
- package/dist/capture/ScreenshotManager.d.ts +22 -0
- package/dist/capture/ScreenshotManager.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/scene3d.d.ts +8 -0
- package/dist/scene3d.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,8 +9,9 @@ is what new 3D projects start from.
|
|
|
9
9
|
|
|
10
10
|
What it has: the scene3d format and loader, physics wiring, a character
|
|
11
11
|
controller and animator, an input layer that mounts on-screen controls on touch
|
|
12
|
-
devices and binds keys everywhere, bone sockets, hit tints,
|
|
13
|
-
still does not have: an
|
|
12
|
+
devices and binds keys everywhere, bone sockets, hit tints, audio, and the
|
|
13
|
+
editor's screenshot/video-capture protocol. What it still does not have: an
|
|
14
|
+
editor.
|
|
14
15
|
|
|
15
16
|
> This paragraph said "seed, not a product — no input system, no character
|
|
16
17
|
> controller, no audio, and no published package" for ten minor versions after
|
|
@@ -23,7 +24,7 @@ still does not have: an editor.
|
|
|
23
24
|
│
|
|
24
25
|
@umicat/three-sdk ThreeUmicat · scene3d · loadScene3D · physics · Input3D
|
|
25
26
|
CharacterController3D · CharacterAnimator · GameAudio
|
|
26
|
-
sockets · tints
|
|
27
|
+
sockets · tints · setupScreenshotListener/setupRecordingListener
|
|
27
28
|
▲
|
|
28
29
|
your game gameplay
|
|
29
30
|
```
|
package/dist/GameAudio.js
CHANGED
|
@@ -230,6 +230,19 @@ export class GameAudio {
|
|
|
230
230
|
const ctx = this.ctx;
|
|
231
231
|
if (this.muted)
|
|
232
232
|
return;
|
|
233
|
+
// The music may still be waiting for a gesture even though the context is
|
|
234
|
+
// not. `start()` is the only other place that starts it, and it early-exits
|
|
235
|
+
// when the context is ALREADY running — which is what happens in a
|
|
236
|
+
// cross-origin iframe, where the context comes up running and the first
|
|
237
|
+
// `play()` therefore never takes the unlock path. Measured: a game
|
|
238
|
+
// embedded that way had a live context, decoded buffers, working effects
|
|
239
|
+
// and no music at all, because `startMusic()` had never been reached.
|
|
240
|
+
//
|
|
241
|
+
// `play()` is the right place for the second attempt: it is called from
|
|
242
|
+
// inside a real gesture (a button, a move), which is exactly the
|
|
243
|
+
// permission an element needs, and `startMusic` is idempotent.
|
|
244
|
+
if (this.ready && this.musicName && !this.musicEl)
|
|
245
|
+
this.startMusic();
|
|
233
246
|
if (!this.ready || !ctx || !this.master) {
|
|
234
247
|
// Not "drop it": this is the unlocking gesture, and the sound it asked
|
|
235
248
|
// for is the one the player is waiting to hear. `start()` plays it.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Sets up postMessage listeners for video recording of the renderer's
|
|
4
|
+
* canvas. Same wire protocol as `@umicat/phaser-sdk`'s
|
|
5
|
+
* `recording/RecordingManager.ts` — the host doesn't need to know which
|
|
6
|
+
* engine is running.
|
|
7
|
+
*
|
|
8
|
+
* Parent sends: 'startRecording'
|
|
9
|
+
* Game responds: { type: 'recordingStarted' }
|
|
10
|
+
*
|
|
11
|
+
* Parent sends: 'stopRecording'
|
|
12
|
+
* Game responds: { type: 'recordingComplete', data: string (base64 data URL), mimeType: string }
|
|
13
|
+
*
|
|
14
|
+
* `canvas.captureStream()` samples the compositor's output directly, so
|
|
15
|
+
* unlike {@link "./ScreenshotManager.js".takeScreenshot}, it works whether
|
|
16
|
+
* or not the renderer was constructed with `preserveDrawingBuffer` — but the
|
|
17
|
+
* template sets it anyway, for the screenshot path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function setupRecordingListener(renderer: THREE.WebGLRenderer): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
let mediaRecorder = null;
|
|
2
|
+
let recordedChunks = [];
|
|
3
|
+
/**
|
|
4
|
+
* Sets up postMessage listeners for video recording of the renderer's
|
|
5
|
+
* canvas. Same wire protocol as `@umicat/phaser-sdk`'s
|
|
6
|
+
* `recording/RecordingManager.ts` — the host doesn't need to know which
|
|
7
|
+
* engine is running.
|
|
8
|
+
*
|
|
9
|
+
* Parent sends: 'startRecording'
|
|
10
|
+
* Game responds: { type: 'recordingStarted' }
|
|
11
|
+
*
|
|
12
|
+
* Parent sends: 'stopRecording'
|
|
13
|
+
* Game responds: { type: 'recordingComplete', data: string (base64 data URL), mimeType: string }
|
|
14
|
+
*
|
|
15
|
+
* `canvas.captureStream()` samples the compositor's output directly, so
|
|
16
|
+
* unlike {@link "./ScreenshotManager.js".takeScreenshot}, it works whether
|
|
17
|
+
* or not the renderer was constructed with `preserveDrawingBuffer` — but the
|
|
18
|
+
* template sets it anyway, for the screenshot path.
|
|
19
|
+
*/
|
|
20
|
+
export function setupRecordingListener(renderer) {
|
|
21
|
+
window.addEventListener('message', (event) => {
|
|
22
|
+
if (event.data === 'startRecording') {
|
|
23
|
+
try {
|
|
24
|
+
const canvas = renderer.domElement;
|
|
25
|
+
if (!canvas)
|
|
26
|
+
return;
|
|
27
|
+
recordedChunks = [];
|
|
28
|
+
const stream = canvas.captureStream(30);
|
|
29
|
+
// Pick best available codec.
|
|
30
|
+
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
|
31
|
+
? 'video/webm;codecs=vp9'
|
|
32
|
+
: 'video/webm';
|
|
33
|
+
mediaRecorder = new MediaRecorder(stream, {
|
|
34
|
+
mimeType,
|
|
35
|
+
videoBitsPerSecond: 1500000, // 1.5 Mbps
|
|
36
|
+
});
|
|
37
|
+
mediaRecorder.ondataavailable = (e) => {
|
|
38
|
+
if (e.data.size > 0)
|
|
39
|
+
recordedChunks.push(e.data);
|
|
40
|
+
};
|
|
41
|
+
mediaRecorder.onstop = () => {
|
|
42
|
+
const blob = new Blob(recordedChunks, { type: 'video/webm' });
|
|
43
|
+
const reader = new FileReader();
|
|
44
|
+
reader.onloadend = () => {
|
|
45
|
+
window.parent.postMessage({
|
|
46
|
+
type: 'recordingComplete',
|
|
47
|
+
data: reader.result,
|
|
48
|
+
mimeType: 'video/webm',
|
|
49
|
+
}, '*');
|
|
50
|
+
};
|
|
51
|
+
reader.readAsDataURL(blob);
|
|
52
|
+
};
|
|
53
|
+
mediaRecorder.start(1000); // collect data every 1 second
|
|
54
|
+
window.parent.postMessage({ type: 'recordingStarted' }, '*');
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error('[UmicatSDK] Recording failed to start:', e);
|
|
58
|
+
window.parent.postMessage({ type: 'recordingError', error: String(e) }, '*');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (event.data === 'stopRecording') {
|
|
62
|
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
|
63
|
+
mediaRecorder.stop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Sets up a postMessage listener that captures the renderer's canvas when the
|
|
4
|
+
* parent window requests a screenshot. Same wire protocol as
|
|
5
|
+
* `@umicat/phaser-sdk`'s `screenshot/ScreenshotManager.ts` — the host
|
|
6
|
+
* (umicat-home-ui) speaks one protocol to both engines and never needs to
|
|
7
|
+
* know which one is running.
|
|
8
|
+
*
|
|
9
|
+
* Parent sends: { type: 'screenshot' }
|
|
10
|
+
* Game responds: { type: 'screenshot_result', dataUrl: string }
|
|
11
|
+
*
|
|
12
|
+
* Requires the renderer to have been constructed with
|
|
13
|
+
* `preserveDrawingBuffer: true` — without it, `canvas.toDataURL()` on a
|
|
14
|
+
* WebGL context can return a blank image depending on exactly when the
|
|
15
|
+
* browser clears the drawing buffer after compositing. This is NOT
|
|
16
|
+
* something the SDK can fix after the fact (it's a renderer-construction-time
|
|
17
|
+
* option), so a game that wants captures to work must pass it when creating
|
|
18
|
+
* its `THREE.WebGLRenderer`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function setupScreenshotListener(renderer: THREE.WebGLRenderer): void;
|
|
21
|
+
/** Take a screenshot programmatically. Returns a base64 PNG data URL. */
|
|
22
|
+
export declare function takeScreenshot(renderer: THREE.WebGLRenderer): string | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets up a postMessage listener that captures the renderer's canvas when the
|
|
3
|
+
* parent window requests a screenshot. Same wire protocol as
|
|
4
|
+
* `@umicat/phaser-sdk`'s `screenshot/ScreenshotManager.ts` — the host
|
|
5
|
+
* (umicat-home-ui) speaks one protocol to both engines and never needs to
|
|
6
|
+
* know which one is running.
|
|
7
|
+
*
|
|
8
|
+
* Parent sends: { type: 'screenshot' }
|
|
9
|
+
* Game responds: { type: 'screenshot_result', dataUrl: string }
|
|
10
|
+
*
|
|
11
|
+
* Requires the renderer to have been constructed with
|
|
12
|
+
* `preserveDrawingBuffer: true` — without it, `canvas.toDataURL()` on a
|
|
13
|
+
* WebGL context can return a blank image depending on exactly when the
|
|
14
|
+
* browser clears the drawing buffer after compositing. This is NOT
|
|
15
|
+
* something the SDK can fix after the fact (it's a renderer-construction-time
|
|
16
|
+
* option), so a game that wants captures to work must pass it when creating
|
|
17
|
+
* its `THREE.WebGLRenderer`.
|
|
18
|
+
*/
|
|
19
|
+
export function setupScreenshotListener(renderer) {
|
|
20
|
+
window.addEventListener('message', (event) => {
|
|
21
|
+
if (event.data?.type === 'screenshot') {
|
|
22
|
+
try {
|
|
23
|
+
const dataUrl = renderer.domElement.toDataURL('image/png');
|
|
24
|
+
window.parent.postMessage({ type: 'screenshot_result', dataUrl }, '*');
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.error('[UmicatSDK] Screenshot failed:', e);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (event.data?.type === 'screenshotRegion') {
|
|
31
|
+
// Marquee-to-chat: the host drew a selection rect over the iframe (in
|
|
32
|
+
// iframe-relative CSS px); crop the canvas to it here, where the
|
|
33
|
+
// canvas's on-screen position + intrinsic resolution are known
|
|
34
|
+
// (handles devicePixelRatio scaling — a 3D game always runs 'resize'
|
|
35
|
+
// scale mode, never letterboxed, but pixel ratio still differs from
|
|
36
|
+
// CSS size).
|
|
37
|
+
try {
|
|
38
|
+
const r = event.data.rect;
|
|
39
|
+
const dataUrl = cropCanvasRegion(renderer.domElement, r);
|
|
40
|
+
window.parent.postMessage({ type: 'screenshot_region_result', dataUrl, requestId: event.data.requestId }, '*');
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.error('[UmicatSDK] Region screenshot failed:', e);
|
|
44
|
+
window.parent.postMessage({ type: 'screenshot_region_result', dataUrl: null, requestId: event.data?.requestId }, '*');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Crop the canvas to a rect given in iframe-relative CSS pixels (the host's
|
|
51
|
+
* marquee). Maps display px → intrinsic canvas px via the canvas's bounding
|
|
52
|
+
* rect + resolution, clamps to bounds. Same math as phaser-sdk's
|
|
53
|
+
* `cropCanvasRegion` — deliberately engine-agnostic, it only looks at the
|
|
54
|
+
* canvas element.
|
|
55
|
+
*/
|
|
56
|
+
function cropCanvasRegion(canvas, rect) {
|
|
57
|
+
const cr = canvas.getBoundingClientRect();
|
|
58
|
+
if (cr.width <= 0 || cr.height <= 0)
|
|
59
|
+
return null;
|
|
60
|
+
const scale = canvas.width / cr.width; // intrinsic px per displayed px
|
|
61
|
+
let sx = Math.round((rect.x - cr.x) * scale);
|
|
62
|
+
let sy = Math.round((rect.y - cr.y) * scale);
|
|
63
|
+
let sw = Math.round(rect.width * scale);
|
|
64
|
+
let sh = Math.round(rect.height * scale);
|
|
65
|
+
// Clamp to the canvas (the marquee can run past the edges).
|
|
66
|
+
sx = Math.max(0, Math.min(sx, canvas.width - 1));
|
|
67
|
+
sy = Math.max(0, Math.min(sy, canvas.height - 1));
|
|
68
|
+
sw = Math.max(1, Math.min(sw, canvas.width - sx));
|
|
69
|
+
sh = Math.max(1, Math.min(sh, canvas.height - sy));
|
|
70
|
+
const out = document.createElement('canvas');
|
|
71
|
+
out.width = sw;
|
|
72
|
+
out.height = sh;
|
|
73
|
+
const ctx = out.getContext('2d');
|
|
74
|
+
if (!ctx)
|
|
75
|
+
return null;
|
|
76
|
+
ctx.drawImage(canvas, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
77
|
+
return out.toDataURL('image/png');
|
|
78
|
+
}
|
|
79
|
+
/** Take a screenshot programmatically. Returns a base64 PNG data URL. */
|
|
80
|
+
export function takeScreenshot(renderer) {
|
|
81
|
+
try {
|
|
82
|
+
return renderer.domElement.toDataURL('image/png');
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
console.error('[UmicatSDK] Screenshot failed:', e);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { attachToSocket, findBone, boneNames } from './Sockets.js';
|
|
|
12
12
|
export { flashTint, updateTints, isTinted } from './Tint.js';
|
|
13
13
|
export { GameAudio } from './GameAudio.js';
|
|
14
14
|
export type { GameAudioOptions, AudioClipSpec } from './GameAudio.js';
|
|
15
|
+
export { setupScreenshotListener, takeScreenshot } from './capture/ScreenshotManager.js';
|
|
16
|
+
export { setupRecordingListener } from './capture/RecordingManager.js';
|
|
15
17
|
export type { Attachment } from './Sockets.js';
|
|
16
18
|
export type { LoadedScene3D, LoadSceneOptions } from './SceneLoader3D.js';
|
|
17
19
|
export { ORIENTATION_DIMENSIONS } from '@umicat/platform-sdk/orientation.js';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export { Input3D } from './Input3D.js';
|
|
|
12
12
|
export { attachToSocket, findBone, boneNames } from './Sockets.js';
|
|
13
13
|
export { flashTint, updateTints, isTinted } from './Tint.js';
|
|
14
14
|
export { GameAudio } from './GameAudio.js';
|
|
15
|
+
export { setupScreenshotListener, takeScreenshot } from './capture/ScreenshotManager.js';
|
|
16
|
+
export { setupRecordingListener } from './capture/RecordingManager.js';
|
|
15
17
|
// Re-exported so a game imports one package for the common case. A game should
|
|
16
18
|
// not have to know that identity and saves come from a different package than
|
|
17
19
|
// the renderer.
|
package/dist/scene3d.d.ts
CHANGED
|
@@ -176,6 +176,14 @@ export interface Manifest3D {
|
|
|
176
176
|
/** Identity quaternion, spelled out so callers don't have to remember the order. */
|
|
177
177
|
export declare const IDENTITY_QUAT: Quat;
|
|
178
178
|
export declare function vec3(x?: number, y?: number, z?: number): Vec3;
|
|
179
|
+
/**
|
|
180
|
+
* Wrap a single model as a `Manifest3D` so `loadModelAsset` can load it
|
|
181
|
+
* without a caller hand-rolling the boilerplate every time. `scenes`/
|
|
182
|
+
* `initialScene` are unused by `loadModelAsset` (only `loadScene3D` reads
|
|
183
|
+
* them) — left empty rather than optional so the type stays exactly
|
|
184
|
+
* `Manifest3D`, not a new shape callers have to learn.
|
|
185
|
+
*/
|
|
186
|
+
export declare function singleModelManifest(model: ModelAsset3D): Manifest3D;
|
|
179
187
|
/**
|
|
180
188
|
* Validate a scene enough to fail loudly at author time rather than as a blank
|
|
181
189
|
* screen at play time. Returns the problems; empty means usable.
|
package/dist/scene3d.js
CHANGED
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
/** Identity quaternion, spelled out so callers don't have to remember the order. */
|
|
27
27
|
export const IDENTITY_QUAT = [0, 0, 0, 1];
|
|
28
28
|
export function vec3(x = 0, y = 0, z = 0) { return { x, y, z }; }
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a single model as a `Manifest3D` so `loadModelAsset` can load it
|
|
31
|
+
* without a caller hand-rolling the boilerplate every time. `scenes`/
|
|
32
|
+
* `initialScene` are unused by `loadModelAsset` (only `loadScene3D` reads
|
|
33
|
+
* them) — left empty rather than optional so the type stays exactly
|
|
34
|
+
* `Manifest3D`, not a new shape callers have to learn.
|
|
35
|
+
*/
|
|
36
|
+
export function singleModelManifest(model) {
|
|
37
|
+
return { schemaVersion: 1, initialScene: '', scenes: [], models: [model] };
|
|
38
|
+
}
|
|
29
39
|
/**
|
|
30
40
|
* Validate a scene enough to fail loudly at author time rather than as a blank
|
|
31
41
|
* screen at play time. Returns the problems; empty means usable.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|