@wasm-gaming/engine-specs 0.2.0 → 0.2.2

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/demo/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Themable Engine Demo Template
2
+
3
+ This directory contains a standalone, themable web interface template for hosting interactive WASM gaming engine demos.
4
+
5
+ ## 🚀 How to use in another engine repository
6
+
7
+ 1. **Copy the `demo/` folder** into your engine repository.
8
+
9
+ 2. **Connect your engine SDK in `demo/sdk.js`**:
10
+ Open `demo/sdk.js` and point it to your engine SDK build:
11
+ ```javascript
12
+ import sdk from "../src/index.js"; // or "../dist/index.js"
13
+
14
+ export async function getSdk() {
15
+ return sdk;
16
+ }
17
+ ```
18
+ *Alternatively*, if embedding in HTML without modifying `sdk.js`, set `window.SDK = myEngineSdk;` before `demo.js` runs.
19
+
20
+ 3. **Serve the demo**:
21
+ Serve `demo/index.html` using any local development server (e.g. `npx serve demo` or Vite).
22
+
23
+ ---
24
+
25
+ ## 🎨 Customizing & Theming
26
+
27
+ The demo visual design system is fully controlled via **CSS Custom Properties** defined in `demo.css`.
28
+
29
+ ### Overriding CSS Variables
30
+
31
+ To apply your custom branding, override any of the `:root` variables in `demo.css` or in your own custom stylesheet:
32
+
33
+ ```css
34
+ :root {
35
+ /* Brand colors */
36
+ --demo-bg-a: #1e1b4b;
37
+ --demo-bg-b: #0f172a;
38
+ --demo-text: #f8fafc;
39
+ --demo-muted: #94a3b8;
40
+
41
+ /* Accent highlights */
42
+ --demo-accent: #00ffcc;
43
+ --demo-accent-rgb: 0, 255, 204;
44
+ --demo-accent2: #38bdf8;
45
+ --demo-accent2-rgb: 56, 189, 248;
46
+
47
+ /* Glassmorphism panels */
48
+ --demo-panel-bg: rgba(15, 23, 42, 0.75);
49
+ --demo-panel-border: rgba(255, 255, 255, 0.1);
50
+ --demo-subpanel-bg: rgba(30, 41, 59, 0.6);
51
+ --demo-subpanel-border: rgba(255, 255, 255, 0.1);
52
+
53
+ /* Buttons & Dropzone */
54
+ --demo-drop-bg: rgba(30, 41, 59, 0.4);
55
+ --demo-picker-bg: rgba(255, 255, 255, 0.1);
56
+ --demo-picker-hover-bg: rgba(255, 255, 255, 0.2);
57
+ --demo-picker-text: #ffffff;
58
+ }
59
+ ```
60
+
61
+ ### Full Token Reference
62
+
63
+ | Variable | Description | Default |
64
+ |---|---|---|
65
+ | `--demo-bg-a` | Gradient background stop 1 | `#d8b4fe` |
66
+ | `--demo-bg-b` | Gradient background stop 2 | `#7dd3fc` |
67
+ | `--demo-text` | Primary text color | `#0f172a` |
68
+ | `--demo-muted` | Secondary/muted text color | `#475569` |
69
+ | `--demo-accent` | Primary accent color | `#ff007f` |
70
+ | `--demo-accent-rgb` | Primary accent RGB channels | `255, 0, 127` |
71
+ | `--demo-accent2` | Secondary accent color | `#0284c7` |
72
+ | `--demo-accent2-rgb` | Secondary accent RGB channels | `2, 132, 199` |
73
+ | `--demo-panel-bg` | Main card/launcher background | `rgba(255, 255, 255, 0.25)` |
74
+ | `--demo-panel-border` | Main card border color | `rgba(255, 255, 255, 0.4)` |
75
+ | `--demo-panel-radius` | Main card corner rounding | `12px` |
76
+ | `--demo-panel-blur` | Backdrop glass blur effect | `24px` |
77
+ | `--demo-subpanel-bg` | Inner info section background | `rgba(255, 255, 255, 0.35)` |
78
+ | `--demo-drop-bg` | Dropzone background | `rgba(255, 255, 255, 0.15)` |
79
+ | `--demo-drop-active-bg` | Drag-over active background | `rgba(255, 255, 255, 0.4)` |
80
+ | `--demo-picker-bg` | Pick File button background | `rgba(255, 255, 255, 0.5)` |
81
+ | `--demo-pill-bg` | Metadata badge background | `rgba(var(--demo-accent-rgb), 0.1)` |
82
+ | `--demo-file-info-bg` | Canvas overlay panel background | `rgba(255, 255, 255, 0.7)` |
83
+ | `--demo-font-sans` | Primary sans-serif font family | `"Outfit", system-ui, sans-serif` |
84
+ | `--demo-font-mono` | Monospace code font family | `"Space Mono", monospace` |
85
+
86
+ ---
87
+
88
+ ## 🛠 Features
89
+
90
+ - **Automated Manifest Integration**: Page title, SDK info, asset key mapping, and file type (`accept`) filters are automatically loaded from `sdk.manifest`.
91
+ - **ROM Drag & Drop**: Interactive drop zone with file picker fallback.
92
+ - **Checksum Calculation**: Computes MD5, SHA-1, and SHA-256 hashes of loaded files asynchronously in the background.
93
+ - **Shadow DOM Isolation**: UI components are isolated using Shadow DOM to prevent style leakage.
94
+ - **Subpath Hosting Ready**: Uses `import.meta.url` for component templates to work seamlessly on subpaths (e.g. GitHub Pages).
@@ -0,0 +1,101 @@
1
+ const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, n) => {
2
+ let c = n;
3
+ for (let k = 0; k < 8; k += 1) {
4
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
5
+ }
6
+ return c >>> 0;
7
+ });
8
+
9
+ const crc32 = (bytes) => {
10
+ let crc = 0xffffffff;
11
+ for (const byte of bytes) {
12
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
13
+ }
14
+ return ((crc ^ 0xffffffff) >>> 0).toString(16).padStart(8, "0");
15
+ };
16
+
17
+ const toHex = (buffer) =>
18
+ Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
19
+
20
+ // RFC 1321. Hand-rolled because crypto.subtle does not offer MD5.
21
+ const MD5_K = Uint32Array.from({ length: 64 }, (_, i) =>
22
+ Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32));
23
+
24
+ const MD5_S = [
25
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
26
+ 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
27
+ 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
28
+ 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
29
+ ];
30
+
31
+ const md5 = (bytes) => {
32
+ const paddedLength = (((bytes.length + 8) >> 6) + 1) << 6;
33
+ const padded = new Uint8Array(paddedLength);
34
+ padded.set(bytes);
35
+ padded[bytes.length] = 0x80;
36
+ const view = new DataView(padded.buffer);
37
+ view.setUint32(paddedLength - 8, (bytes.length * 8) >>> 0, true);
38
+ view.setUint32(paddedLength - 4, Math.floor(bytes.length / 0x20000000), true);
39
+
40
+ let [h0, h1, h2, h3] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476];
41
+ const words = new Uint32Array(16);
42
+
43
+ for (let offset = 0; offset < paddedLength; offset += 64) {
44
+ for (let j = 0; j < 16; j += 1) {
45
+ words[j] = view.getUint32(offset + j * 4, true);
46
+ }
47
+ let [a, b, c, d] = [h0, h1, h2, h3];
48
+ for (let i = 0; i < 64; i += 1) {
49
+ let f;
50
+ let g;
51
+ if (i < 16) {
52
+ f = (b & c) | (~b & d);
53
+ g = i;
54
+ } else if (i < 32) {
55
+ f = (d & b) | (~d & c);
56
+ g = (5 * i + 1) % 16;
57
+ } else if (i < 48) {
58
+ f = b ^ c ^ d;
59
+ g = (3 * i + 5) % 16;
60
+ } else {
61
+ f = c ^ (b | ~d);
62
+ g = (7 * i) % 16;
63
+ }
64
+ const sum = (a + f + MD5_K[i] + words[g]) >>> 0;
65
+ const rotated = (sum << MD5_S[i]) | (sum >>> (32 - MD5_S[i]));
66
+ [a, b, c, d] = [d, (b + rotated) >>> 0, b, c];
67
+ }
68
+ h0 = (h0 + a) >>> 0;
69
+ h1 = (h1 + b) >>> 0;
70
+ h2 = (h2 + c) >>> 0;
71
+ h3 = (h3 + d) >>> 0;
72
+ }
73
+
74
+ const digest = new DataView(new ArrayBuffer(16));
75
+ digest.setUint32(0, h0, true);
76
+ digest.setUint32(4, h1, true);
77
+ digest.setUint32(8, h2, true);
78
+ digest.setUint32(12, h3, true);
79
+ return toHex(digest.buffer);
80
+ };
81
+
82
+ const sha = async (algorithm, buffer) => {
83
+ // crypto.subtle only exists in secure contexts (https or localhost).
84
+ if (!globalThis.crypto?.subtle) {
85
+ return "unavailable (requires a secure context)";
86
+ }
87
+ return toHex(await crypto.subtle.digest(algorithm, buffer));
88
+ };
89
+
90
+ export const checksumAlgorithms = ["CRC32", "MD5", "SHA-1", "SHA-256"];
91
+
92
+ export async function computeChecksums(buffer) {
93
+ const bytes = new Uint8Array(buffer);
94
+ const [sha1, sha256] = await Promise.all([sha("SHA-1", buffer), sha("SHA-256", buffer)]);
95
+ return [
96
+ { label: "CRC32", value: crc32(bytes) },
97
+ { label: "MD5", value: md5(bytes) },
98
+ { label: "SHA-1", value: sha1 },
99
+ { label: "SHA-256", value: sha256 },
100
+ ];
101
+ }
@@ -0,0 +1,66 @@
1
+ <h2>{{ fileName }}</h2>
2
+ <dl>
3
+ <div class="row">
4
+ <dt>Size</dt>
5
+ <dd>{{ size }}</dd>
6
+ </div>
7
+ <div :each="checksum in checksums" class="row">
8
+ <dt>{{ checksum.label }}</dt>
9
+ <dd>{{ checksum.value }}</dd>
10
+ </div>
11
+ </dl>
12
+
13
+ <style>
14
+ :host {
15
+ position: absolute;
16
+ top: 4rem;
17
+ left: 2rem;
18
+ max-width: min(600px, calc(100vw - 2rem));
19
+ border: 1px solid var(--demo-file-info-border, rgba(255, 255, 255, 0.8));
20
+ border-radius: var(--demo-panel-radius, 12px);
21
+ background: var(--demo-file-info-bg, rgba(255, 255, 255, 0.7));
22
+ backdrop-filter: blur(var(--demo-file-info-blur, 10px));
23
+ padding: 1.2rem;
24
+ font-size: 0.9rem;
25
+ }
26
+
27
+ h2 {
28
+ margin: 0 0 0.8rem;
29
+ font-size: 1.1rem;
30
+ font-weight: 600;
31
+ word-break: break-all;
32
+ color: var(--demo-text, var(--text, #0f172a));
33
+ letter-spacing: 0.05em;
34
+ border-bottom: 1px solid var(--demo-panel-border, rgba(255, 255, 255, 0.6));
35
+ padding-bottom: 0.4rem;
36
+ }
37
+
38
+ dl {
39
+ margin: 0;
40
+ display: grid;
41
+ gap: 0.5rem;
42
+ }
43
+
44
+ .row {
45
+ display: grid;
46
+ grid-template-columns: 4.4rem 1fr;
47
+ gap: 0.8rem;
48
+ align-items: baseline;
49
+ }
50
+
51
+ dt {
52
+ color: var(--demo-text, var(--text, #0f172a));
53
+ opacity: 0.7;
54
+ font-size: 0.8rem;
55
+ text-transform: uppercase;
56
+ letter-spacing: 0.05em;
57
+ }
58
+
59
+ dd {
60
+ margin: 0;
61
+ font-family: var(--demo-font-mono, var(--font-mono, monospace));
62
+ word-break: break-all;
63
+ user-select: all;
64
+ color: var(--demo-text, var(--text, #0f172a));
65
+ }
66
+ </style>
@@ -0,0 +1,163 @@
1
+ <form class="launcher">
2
+ <section class="sdk">
3
+ <CSdkInfo ...sdkInfo />
4
+ </section>
5
+ <section class="drop" :class.active="dragging" @dragover.prevent="dragging = true" @dragleave="dragging = false"
6
+ @drop.prevent="onDrop">
7
+ <strong>Drop your ROM/asset file here</strong>
8
+ <p>{{ assetHint }}</p>
9
+ <label class="picker">
10
+ Pick file
11
+ <input type="file" :accept="accept" @change="onPick" />
12
+ </label>
13
+ </section>
14
+ <div class="status">{{ status }}</div>
15
+ </form>
16
+
17
+ <script :setup="{
18
+ CSdkInfo,
19
+ sdkInfo,
20
+ onFile,
21
+ accept,
22
+ assetHint = 'or choose one from your computer',
23
+ }">
24
+ let status = "Ready";
25
+ let dragging = false;
26
+
27
+ const onFileList = async (files) => {
28
+ const file = files.item(0);
29
+ if (!file) {
30
+ return;
31
+ }
32
+ try {
33
+ status = `Loading ${file.name}...`;
34
+ await onFile(file);
35
+ } catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ status = `Failed to load file: ${message}`;
38
+ }
39
+ };
40
+
41
+ const onDrop = async (event) => {
42
+ dragging = false;
43
+ if (event.dataTransfer?.files) {
44
+ await onFileList(event.dataTransfer.files);
45
+ }
46
+ };
47
+
48
+ const onPick = async (event) => {
49
+ if (event.target.files) {
50
+ await onFileList(event.target.files);
51
+ }
52
+ };
53
+ </script>
54
+
55
+ <style>
56
+ form.launcher {
57
+ width: min(740px, 100%);
58
+ border: 1px solid var(--demo-panel-border, rgba(255, 255, 255, 0.4));
59
+ border-radius: var(--demo-panel-radius, 12px);
60
+ background: var(--demo-panel-bg, rgba(255, 255, 255, 0.25));
61
+ box-shadow: var(--demo-panel-shadow, inset 0 0 0 1px rgba(255, 255, 255, 0.5));
62
+ backdrop-filter: blur(var(--demo-panel-blur, 24px));
63
+ -webkit-backdrop-filter: blur(var(--demo-panel-blur, 24px));
64
+ padding: 1.5rem;
65
+ display: grid;
66
+ gap: 1.25rem;
67
+ position: relative;
68
+ overflow: hidden;
69
+ }
70
+
71
+ form.launcher::before {
72
+ content: "";
73
+ position: absolute;
74
+ top: 0;
75
+ right: 0;
76
+ width: 60px;
77
+ height: 60px;
78
+ background: linear-gradient(135deg, transparent 50%, var(--demo-accent, #ff007f) 50%);
79
+ opacity: 0.8;
80
+ }
81
+
82
+ .sdk {
83
+ border: 1px solid var(--demo-subpanel-border, rgba(255, 255, 255, 0.3));
84
+ border-radius: var(--demo-subpanel-radius, 8px);
85
+ padding: 1.25rem;
86
+ background: var(--demo-subpanel-bg, rgba(255, 255, 255, 0.35));
87
+ backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
88
+ -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
89
+ box-shadow: var(--demo-subpanel-shadow, inset 0 0 0 1px rgba(255, 255, 255, 0.6));
90
+ }
91
+
92
+ .drop {
93
+ border: 2px dashed var(--demo-drop-border, rgba(255, 255, 255, 0.6));
94
+ border-radius: var(--demo-subpanel-radius, 8px);
95
+ padding: 2.5rem 1.5rem;
96
+ text-align: center;
97
+ transition: all 250ms ease;
98
+ background: var(--demo-drop-bg, rgba(255, 255, 255, 0.15));
99
+ backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
100
+ -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
101
+ }
102
+
103
+ .drop.active {
104
+ border-color: var(--demo-drop-active-border, var(--demo-accent2, #0284c7));
105
+ background: var(--demo-drop-active-bg, rgba(255, 255, 255, 0.4));
106
+ box-shadow: var(--demo-drop-active-shadow, inset 0 0 0 1px rgba(255, 255, 255, 0.8));
107
+ }
108
+
109
+ .drop strong {
110
+ display: block;
111
+ margin-bottom: 0.5rem;
112
+ font-size: 1.2rem;
113
+ font-weight: 600;
114
+ text-transform: uppercase;
115
+ letter-spacing: 0.05em;
116
+ color: var(--demo-text, #0f172a);
117
+ }
118
+
119
+ .drop p {
120
+ margin: 0.2rem 0;
121
+ color: var(--demo-muted, #475569);
122
+ font-family: var(--demo-font-mono, monospace);
123
+ font-size: 0.9rem;
124
+ }
125
+
126
+ .picker {
127
+ display: inline-flex;
128
+ margin-top: 1.2rem;
129
+ background: var(--demo-picker-bg, rgba(255, 255, 255, 0.5));
130
+ color: var(--demo-picker-text, var(--demo-text, #0f172a));
131
+ border: 1px solid var(--demo-picker-border, rgba(255, 255, 255, 0.6));
132
+ border-radius: var(--demo-subpanel-radius, 8px);
133
+ padding: 0.6rem 1.2rem;
134
+ cursor: pointer;
135
+ text-transform: uppercase;
136
+ font-weight: 600;
137
+ font-size: 0.9rem;
138
+ letter-spacing: 0.05em;
139
+ transition: all 0.2s;
140
+ backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
141
+ -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
142
+ }
143
+
144
+ .picker:hover {
145
+ background: var(--demo-picker-hover-bg, rgba(255, 255, 255, 0.8));
146
+ border-color: var(--demo-picker-hover-border, rgba(255, 255, 255, 0.9));
147
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
148
+ }
149
+
150
+ .picker input {
151
+ display: none;
152
+ }
153
+
154
+ .status {
155
+ min-height: 1.2rem;
156
+ color: var(--demo-text, #0f172a);
157
+ font-family: var(--demo-font-mono, monospace);
158
+ font-size: 0.85rem;
159
+ text-transform: uppercase;
160
+ letter-spacing: 0.05em;
161
+ opacity: 0.8;
162
+ }
163
+ </style>
@@ -0,0 +1,42 @@
1
+ <h1>{{ name }} ({{ id }})</h1>
2
+ <p>{{ description }}</p>
3
+ <div class="row">
4
+ <span class="pill">Version {{ version }}</span>
5
+ <span class="pill">{{ baseWidth }}x{{ baseHeight }}</span>
6
+ <span class="pill">Asset key: {{ assetKey }}</span>
7
+ </div>
8
+
9
+ <style>
10
+ h1 {
11
+ margin: 0 0 0.4rem;
12
+ font-size: 1.4rem;
13
+ font-weight: 800;
14
+ color: var(--demo-text, var(--text, #0f172a));
15
+ letter-spacing: 0.05em;
16
+ }
17
+
18
+ p {
19
+ margin: 0;
20
+ color: var(--demo-muted, var(--muted, #475569));
21
+ font-size: 0.95rem;
22
+ }
23
+
24
+ .row {
25
+ margin-top: 0.9rem;
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ gap: 0.6rem;
29
+ }
30
+
31
+ .pill {
32
+ border: 1px solid var(--demo-pill-border, var(--demo-accent, var(--accent, #ff007f)));
33
+ border-radius: 2px;
34
+ padding: 0.3rem 0.75rem;
35
+ font-family: var(--demo-font-mono, var(--font-mono, monospace));
36
+ font-size: 0.8rem;
37
+ color: var(--demo-pill-text, var(--demo-accent, var(--accent, #ff007f)));
38
+ background: var(--demo-pill-bg, rgba(255, 0, 127, 0.1));
39
+ box-shadow: var(--demo-pill-shadow, inset 0 0 5px rgba(255, 0, 127, 0.3));
40
+ text-transform: uppercase;
41
+ }
42
+ </style>
package/demo/demo.css ADDED
@@ -0,0 +1,137 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap');
2
+
3
+ :root {
4
+ /* Colors & Palette */
5
+ --demo-bg-a: #d8b4fe;
6
+ --demo-bg-b: #7dd3fc;
7
+ --demo-bg-gradient: linear-gradient(145deg, var(--demo-bg-a) 0%, var(--demo-bg-b) 100%);
8
+ --demo-bg-radial-1: rgba(255, 0, 127, 0.08);
9
+ --demo-bg-radial-2: rgba(2, 132, 199, 0.12);
10
+
11
+ --demo-text: #0f172a;
12
+ --demo-muted: #475569;
13
+
14
+ --demo-accent: #ff007f;
15
+ --demo-accent-rgb: 255, 0, 127;
16
+ --demo-accent-glow: 0 0 15px rgba(var(--demo-accent-rgb), 0.3), 0 0 30px rgba(var(--demo-accent-rgb), 0.15);
17
+
18
+ --demo-accent2: #0284c7;
19
+ --demo-accent2-rgb: 2, 132, 199;
20
+ --demo-accent2-glow: 0 0 15px rgba(var(--demo-accent2-rgb), 0.3), 0 0 30px rgba(var(--demo-accent2-rgb), 0.15);
21
+
22
+ /* Glassmorphism & Panel System */
23
+ --demo-panel-bg: rgba(255, 255, 255, 0.25);
24
+ --demo-panel-border: rgba(255, 255, 255, 0.4);
25
+ --demo-panel-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5);
26
+ --demo-panel-blur: 24px;
27
+ --demo-panel-radius: 12px;
28
+
29
+ --demo-subpanel-bg: rgba(255, 255, 255, 0.35);
30
+ --demo-subpanel-border: rgba(255, 255, 255, 0.3);
31
+ --demo-subpanel-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
32
+ --demo-subpanel-blur: 12px;
33
+ --demo-subpanel-radius: 8px;
34
+
35
+ /* Drop Zone */
36
+ --demo-drop-bg: rgba(255, 255, 255, 0.15);
37
+ --demo-drop-border: rgba(255, 255, 255, 0.6);
38
+ --demo-drop-active-bg: rgba(255, 255, 255, 0.4);
39
+ --demo-drop-active-border: var(--demo-accent2);
40
+ --demo-drop-active-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8);
41
+
42
+ /* Button / File Picker */
43
+ --demo-picker-bg: rgba(255, 255, 255, 0.5);
44
+ --demo-picker-hover-bg: rgba(255, 255, 255, 0.8);
45
+ --demo-picker-border: rgba(255, 255, 255, 0.6);
46
+ --demo-picker-hover-border: rgba(255, 255, 255, 0.9);
47
+ --demo-picker-text: var(--demo-text);
48
+
49
+ /* Metadata Pills */
50
+ --demo-pill-bg: rgba(var(--demo-accent-rgb), 0.1);
51
+ --demo-pill-border: var(--demo-accent);
52
+ --demo-pill-text: var(--demo-accent);
53
+ --demo-pill-shadow: inset 0 0 5px rgba(var(--demo-accent-rgb), 0.3);
54
+
55
+ /* File Info Overlay */
56
+ --demo-file-info-bg: rgba(255, 255, 255, 0.7);
57
+ --demo-file-info-border: rgba(255, 255, 255, 0.8);
58
+ --demo-file-info-blur: 10px;
59
+
60
+ /* Typography & Fonts */
61
+ --demo-font-sans: "Outfit", system-ui, -apple-system, sans-serif;
62
+ --demo-font-mono: "Space Mono", monospace;
63
+
64
+ /* Legacy Aliases for Backward Compatibility */
65
+ --bg-a: var(--demo-bg-a);
66
+ --bg-b: var(--demo-bg-b);
67
+ --text: var(--demo-text);
68
+ --muted: var(--demo-muted);
69
+ --accent: var(--demo-accent);
70
+ --accent-glow: var(--demo-accent-glow);
71
+ --accent2: var(--demo-accent2);
72
+ --accent2-glow: var(--demo-accent2-glow);
73
+ --font-sans: var(--demo-font-sans);
74
+ --font-mono: var(--demo-font-mono);
75
+ }
76
+
77
+ * {
78
+ box-sizing: border-box;
79
+ }
80
+
81
+ html,
82
+ body {
83
+ width: 100%;
84
+ height: 100%;
85
+ margin: 0;
86
+ color: var(--demo-text);
87
+ background:
88
+ radial-gradient(circle at 15% 50%, var(--demo-bg-radial-1), transparent 45%),
89
+ radial-gradient(circle at 85% 30%, var(--demo-bg-radial-2), transparent 50%),
90
+ var(--demo-bg-gradient);
91
+ background-attachment: fixed;
92
+ font-family: var(--demo-font-sans);
93
+ letter-spacing: 0.02em;
94
+ }
95
+
96
+ body::after {
97
+ content: "";
98
+ position: fixed;
99
+ inset: 0;
100
+ background: linear-gradient(to bottom,
101
+ rgba(0, 0, 0, 0),
102
+ rgba(0, 0, 0, 0) 50%,
103
+ rgba(0, 0, 0, 0.02) 50%,
104
+ rgba(0, 0, 0, 0.02));
105
+ background-size: 100% 4px;
106
+ pointer-events: none;
107
+ z-index: 9999;
108
+ }
109
+
110
+ main {
111
+ min-height: 100vh;
112
+ min-height: 100dvh;
113
+ display: grid;
114
+ place-items: center;
115
+ padding: 2rem;
116
+ position: relative;
117
+ z-index: 1;
118
+ }
119
+
120
+ main.running {
121
+ padding: 0;
122
+ background: black;
123
+ }
124
+
125
+ .runtime {
126
+ position: relative;
127
+ aspect-ratio: 5 / 4;
128
+ overflow: hidden;
129
+ }
130
+
131
+ .runtime canvas {
132
+ aspect-ratio: 5 / 4;
133
+ width: min(100vw, calc(100vh * 5 / 4));
134
+ height: auto;
135
+ max-height: 100vh;
136
+ display: block;
137
+ }
package/demo/demo.js ADDED
@@ -0,0 +1,100 @@
1
+ import { getSdk } from "./sdk.js";
2
+ import { checksumAlgorithms, computeChecksums } from "./checksums.js";
3
+ import { $, $create, Component79 } from "https://jgermade.github.io/jq79/jq79.js";
4
+
5
+ // Fetch component HTML templates using import.meta.url for resilient subpath resolution
6
+ const fetchComponent = (path) => Component79.fetch(new URL(path, import.meta.url).href);
7
+
8
+ // Fetch all components at once to leverage http2 request multiplexing.
9
+ const [CLauncher, CSdkInfo, CFileInfo] = await Promise.all([
10
+ fetchComponent("./components/launcher.html"),
11
+ fetchComponent("./components/sdk-info.html"),
12
+ fetchComponent("./components/file-info.html"),
13
+ ]);
14
+
15
+ const mainEl = $("main");
16
+ if (!(mainEl instanceof HTMLElement)) {
17
+ throw new Error("Demo page boot failed: missing required elements.");
18
+ }
19
+
20
+ const sdk = await getSdk();
21
+
22
+ if (sdk.manifest?.name) {
23
+ document.title = `${sdk.manifest.name} - Demo`;
24
+ }
25
+
26
+ // Compute asset accept filter and hint text from manifest
27
+ const acceptedExts = sdk.manifest?.assets?.flatMap((a) => a.accept ?? []) ?? [];
28
+ const acceptAttr = acceptedExts.length > 0 ? acceptedExts.join(",") : undefined;
29
+ const assetHint = acceptedExts.length > 0
30
+ ? `Supported files: ${acceptedExts.join(", ")}`
31
+ : "or choose one from your computer";
32
+
33
+ const bootWithFile = async (file) => {
34
+ const bytes = await file.arrayBuffer();
35
+
36
+ mainEl.classList.add("running");
37
+
38
+ const fileInfoEl = $create("section", { className: "file-info" });
39
+ const runtimeEl = $create("div", {
40
+ className: "runtime",
41
+ children: [fileInfoEl],
42
+ });
43
+
44
+ mainEl.replaceChildren(runtimeEl);
45
+
46
+ const fileInfoProps = {
47
+ fileName: file.name,
48
+ size: `${bytes.byteLength.toLocaleString()} bytes`,
49
+ };
50
+ const fileInfoPlaceholder = CFileInfo
51
+ .renderShadow({
52
+ ...fileInfoProps,
53
+ checksums: checksumAlgorithms.map((label) => ({ label, value: "computing…" })),
54
+ })
55
+ .mount(fileInfoEl);
56
+
57
+ const assetKey = sdk.manifest?.assets?.[0]?.key ?? "rom";
58
+ const instance = await sdk.load({
59
+ attachTo: runtimeEl,
60
+ assets: {
61
+ [assetKey]: bytes,
62
+ },
63
+ options: {
64
+ fileName: file.name,
65
+ byteLength: bytes.byteLength,
66
+ },
67
+ onEvent(event) {
68
+ if (event.type === "error") {
69
+ console.error(event.error);
70
+ }
71
+ },
72
+ });
73
+
74
+ instance.start();
75
+
76
+ const checksums = await computeChecksums(bytes);
77
+ fileInfoPlaceholder.destroy();
78
+
79
+ CFileInfo
80
+ .renderShadow({ ...fileInfoProps, checksums })
81
+ .mount(fileInfoEl);
82
+ };
83
+
84
+ CLauncher
85
+ .render({
86
+ CSdkInfo,
87
+ sdkInfo: {
88
+ name: sdk.manifest.name,
89
+ id: sdk.manifest.id,
90
+ description: sdk.manifest.description,
91
+ version: sdk.manifest.version,
92
+ baseWidth: sdk.manifest.video?.baseWidth ?? 320,
93
+ baseHeight: sdk.manifest.video?.baseHeight ?? 240,
94
+ assetKey: sdk.manifest.assets?.[0]?.key ?? "rom",
95
+ },
96
+ onFile: bootWithFile,
97
+ accept: acceptAttr,
98
+ assetHint,
99
+ })
100
+ .mount(mainEl);
@@ -0,0 +1,195 @@
1
+ export function createDummySdk() {
2
+ const manifest = {
3
+ id: "dummy-canvas-sdk",
4
+ version: "0.1.0-demo",
5
+ name: "Dummy Canvas Engine",
6
+ description: "A placeholder SDK that renders animated content on canvas.",
7
+ artifacts: {
8
+ wasm: "./dummy.wasm",
9
+ js: "./dummy.js",
10
+ },
11
+ assets: [
12
+ {
13
+ key: "rom",
14
+ mountPath: "/rom.bin",
15
+ required: true,
16
+ accept: [".rom", ".bin", ".zip", ".dat"],
17
+ description: "Any file works in this demo; data is not executed.",
18
+ },
19
+ ],
20
+ input: "keyboard-default",
21
+ video: {
22
+ baseWidth: 320,
23
+ baseHeight: 256,
24
+ aspect: "5:4",
25
+ },
26
+ capabilities: {
27
+ saveStates: false,
28
+ sram: false,
29
+ },
30
+ };
31
+
32
+ return {
33
+ manifest,
34
+ async load(config) {
35
+ const ownsCanvas = !config.canvasEl;
36
+ const canvasEl = config.canvasEl ?? document.createElement("canvas");
37
+ if (ownsCanvas) {
38
+ config.attachTo.appendChild(canvasEl);
39
+ }
40
+
41
+ const ctx = canvasEl.getContext("2d");
42
+ if (!ctx) {
43
+ const error = new Error("Could not create 2D canvas context.");
44
+ config.onEvent?.({ type: "error", error });
45
+ throw error;
46
+ }
47
+
48
+ let raf = 0;
49
+ let running = false;
50
+ let t0 = performance.now();
51
+ let lastFrame = t0;
52
+
53
+ const fontSize = 30;
54
+ let drops = [];
55
+
56
+ const fitCanvasToWindow = () => {
57
+ const dpr = window.devicePixelRatio || 1;
58
+ const rect = canvasEl.getBoundingClientRect();
59
+ const width = Math.max(1, Math.floor(rect.width * dpr));
60
+ const height = Math.max(1, Math.floor(rect.height * dpr));
61
+ canvasEl.width = width;
62
+ canvasEl.height = height;
63
+ };
64
+
65
+ const render = (time) => {
66
+ if (!running) {
67
+ return;
68
+ }
69
+
70
+ const width = canvasEl.width;
71
+ const height = canvasEl.height;
72
+ const dt = Math.min((time - lastFrame) / 1000, 0.1);
73
+ const fps = Math.max(1, 1000 / Math.max(1, time - lastFrame));
74
+ lastFrame = time;
75
+
76
+ const expectedColumns = Math.floor(width / fontSize);
77
+ const maxRows = Math.floor(height / fontSize) + 30;
78
+
79
+ if (drops.length !== expectedColumns) {
80
+ drops = [];
81
+ for (let x = 0; x < expectedColumns; x++) {
82
+ drops[x] = {
83
+ y: Math.random() * maxRows,
84
+ speed: Math.random() * 1.5 + 0.5,
85
+ opacity: Math.random() * 0.7 + 0.3,
86
+ length: Math.floor(Math.random() * 20 + 5),
87
+ chars: Array.from({ length: maxRows }, () => String.fromCharCode(0x30A0 + Math.random() * 96))
88
+ };
89
+ }
90
+ }
91
+
92
+ // Dark background inside the canvas to contrast with the light UI components
93
+ // ctx.fillStyle = "#0f172a";
94
+ ctx.fillStyle = "#060910ff";
95
+ ctx.fillRect(0, 0, width, height);
96
+
97
+ ctx.font = "bold " + fontSize + "px 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans CJK JP', sans-serif";
98
+ ctx.textAlign = "center";
99
+
100
+ for (let x = 0; x < drops.length; x++) {
101
+ let drop = drops[x];
102
+
103
+ // Change characters dynamically
104
+ if (Math.random() > 0.4) {
105
+ let randIdx = Math.floor(Math.random() * drop.chars.length);
106
+ drop.chars[randIdx] = String.fromCharCode(0x30A0 + Math.random() * 96);
107
+ }
108
+
109
+ // Move the drop (slower)
110
+ drop.y += drop.speed * (dt * 12);
111
+
112
+ let startY = Math.floor(drop.y);
113
+
114
+ // Draw the column
115
+ for (let i = 0; i < drop.length; i++) {
116
+ let charY = startY - i;
117
+ if (charY < 0 || charY >= maxRows) continue;
118
+
119
+ let text = drop.chars[charY];
120
+ let alpha = drop.opacity * (1 - (i / drop.length));
121
+
122
+ if (i === 0) {
123
+ // Head of the drop (bright cyan-white)
124
+ ctx.fillStyle = `rgba(220, 255, 255, ${drop.opacity})`;
125
+ } else {
126
+ // Tail of the drop (bright colors for dark background)
127
+ const hue = ((time / 15) + (x * 10)) % 360;
128
+ ctx.fillStyle = `hsla(${hue}, 80%, 65%, ${alpha})`;
129
+ }
130
+
131
+ ctx.fillText(text, x * fontSize + fontSize / 2, charY * fontSize);
132
+ }
133
+
134
+ // Reset drop when fully off-screen
135
+ if ((drop.y - drop.length) * fontSize > height) {
136
+ drop.y = 0;
137
+ drop.speed = Math.random() * 1.5 + 0.5;
138
+ drop.opacity = Math.random() * 0.7 + 0.3;
139
+ drop.length = Math.floor(Math.random() * 20 + 5);
140
+ }
141
+ }
142
+
143
+ ctx.fillStyle = "rgba(255,255,255,0.85)";
144
+ ctx.fillRect(width - 210, 20, 180, 64);
145
+ ctx.fillStyle = "#0f172a";
146
+ ctx.font = "600 36px 'Outfit', system-ui, sans-serif";
147
+ ctx.textAlign = "left";
148
+ ctx.fillText(`FPS ${fps.toFixed(0)}`, width - 182, 65);
149
+
150
+ config.onEvent?.({ type: "frame", fps });
151
+ raf = requestAnimationFrame(render);
152
+ };
153
+
154
+ fitCanvasToWindow();
155
+ window.addEventListener("resize", fitCanvasToWindow);
156
+ config.onEvent?.({ type: "ready" });
157
+
158
+ return {
159
+ start() {
160
+ if (running) {
161
+ return;
162
+ }
163
+ running = true;
164
+ raf = requestAnimationFrame(render);
165
+ },
166
+ pause() {
167
+ running = false;
168
+ cancelAnimationFrame(raf);
169
+ },
170
+ resume() {
171
+ if (running) {
172
+ return;
173
+ }
174
+ running = true;
175
+ raf = requestAnimationFrame(render);
176
+ },
177
+ reset() {
178
+ t0 = performance.now();
179
+ lastFrame = t0;
180
+ },
181
+ setInput() {
182
+ return;
183
+ },
184
+ destroy() {
185
+ running = false;
186
+ cancelAnimationFrame(raf);
187
+ window.removeEventListener("resize", fitCanvasToWindow);
188
+ if (ownsCanvas) {
189
+ canvasEl.remove();
190
+ }
191
+ },
192
+ };
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>WASM Gaming Engine Specs Demo</title>
7
+ <link rel="stylesheet" href="./demo.css" />
8
+ </head>
9
+ <body>
10
+ <main></main>
11
+
12
+ <script type="module" src="./demo.js"></script>
13
+ </body>
14
+ </html>
package/demo/sdk.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createDummySdk } from "./demo.sdk.js";
2
+
3
+ /**
4
+ * Resolves the Engine SDK to use for the demo.
5
+ *
6
+ * To use this template in another repository:
7
+ * 1. Modify this file to import your engine's SDK:
8
+ * import sdk from "../src/index.js"; // or "../dist/index.js"
9
+ * export async function getSdk() { return sdk; }
10
+ *
11
+ * 2. Or attach window.SDK before demo.js boots:
12
+ * window.SDK = myEngineSdk;
13
+ */
14
+ export async function getSdk() {
15
+ if (typeof window !== "undefined" && window.SDK) {
16
+ return window.SDK;
17
+ }
18
+ return createDummySdk();
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wasm-gaming/engine-specs",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "The wasm-gaming engine contract: the single shared interface every WASM game/emulator engine and the host app conform to. TypeScript types + JSON Schema + a Zod runtime validator.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "dist/",
16
+ "demo/",
16
17
  "src/",
17
18
  "schema/"
18
19
  ],