star-canvas 0.1.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 +206 -0
- package/dist/index.cjs +19 -0
- package/dist/index.d.cts +166 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.mjs +19 -0
- package/dist/legacy.cjs +19 -0
- package/dist/legacy.d.cts +19 -0
- package/dist/legacy.d.ts +19 -0
- package/dist/legacy.mjs +19 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Star Canvas
|
|
2
|
+
|
|
3
|
+
Essential DOM utilities for reliable game initialization.
|
|
4
|
+
|
|
5
|
+
`star-canvas` solves common bugs in web games: event listeners on null elements, canvas sizing issues, and frame-rate dependent speed. It provides a tiny (~1KB), zero-dependency toolkit with a mobile-first design.
|
|
6
|
+
|
|
7
|
+
This package is part of the **Star SDK**.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Safe Initialization:** Never crash from timing issues with `onReady()`
|
|
12
|
+
- **Delegated Events:** Survive `innerHTML` re-renders with `on()`
|
|
13
|
+
- **Responsive Canvas:** Auto-sizing with DPR handling via `canvas()`
|
|
14
|
+
- **Frame-Rate Independence:** Delta time for consistent speed via `loop()`
|
|
15
|
+
- **Zero Dependencies:** Pure vanilla JavaScript, SSR-safe
|
|
16
|
+
- **Tiny:** <1KB gzipped
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
yarn add star-canvas
|
|
22
|
+
# or
|
|
23
|
+
npm install star-canvas
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### UI Click Game
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
import { onReady, mount, on } from 'star-canvas';
|
|
32
|
+
|
|
33
|
+
onReady(() => {
|
|
34
|
+
const root = mount('#game-root');
|
|
35
|
+
let score = 0;
|
|
36
|
+
|
|
37
|
+
function render() {
|
|
38
|
+
root.innerHTML = `
|
|
39
|
+
<div class="h-screen grid place-items-center bg-purple-900 text-white">
|
|
40
|
+
<div class="text-center space-y-4">
|
|
41
|
+
<h1 class="text-4xl font-bold">Score: ${score}</h1>
|
|
42
|
+
<button id="clickBtn" class="px-8 py-4 rounded-xl bg-cyan-400 text-slate-900 font-bold">
|
|
43
|
+
Click Me!
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Delegated event - survives re-renders
|
|
50
|
+
on(root, 'click', '#clickBtn', () => {
|
|
51
|
+
score++;
|
|
52
|
+
render();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
render();
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Canvas Game
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
import { onReady, mount, canvas, loop } from 'star-canvas';
|
|
63
|
+
|
|
64
|
+
onReady(() => {
|
|
65
|
+
const root = mount('#game-root');
|
|
66
|
+
|
|
67
|
+
root.innerHTML = `
|
|
68
|
+
<div class="h-screen w-screen bg-slate-900">
|
|
69
|
+
<!-- canvas auto-created here -->
|
|
70
|
+
</div>`;
|
|
71
|
+
|
|
72
|
+
const container = root.firstElementChild;
|
|
73
|
+
const { ctx, canvas: c } = canvas(container, { pixelRatio: 'device' });
|
|
74
|
+
|
|
75
|
+
const player = { x: 50, y: 50, vx: 200 }; // 200 px/sec
|
|
76
|
+
|
|
77
|
+
loop((dt) => {
|
|
78
|
+
// Delta time → same speed on all devices
|
|
79
|
+
player.x += player.vx * dt;
|
|
80
|
+
|
|
81
|
+
// Wrap around
|
|
82
|
+
if (player.x > c.width) player.x = 0;
|
|
83
|
+
|
|
84
|
+
// Render
|
|
85
|
+
ctx.fillStyle = '#1e293b';
|
|
86
|
+
ctx.fillRect(0, 0, c.width, c.height);
|
|
87
|
+
|
|
88
|
+
ctx.fillStyle = '#22d3ee';
|
|
89
|
+
ctx.fillRect(player.x, player.y, 32, 32);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API
|
|
95
|
+
|
|
96
|
+
### `onReady(fn: () => void)`
|
|
97
|
+
|
|
98
|
+
Run callback when DOM is ready. Works with all script loading patterns.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
onReady(() => {
|
|
102
|
+
// Safe to access DOM here
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `mount(selector?: string | Element): HTMLElement`
|
|
107
|
+
|
|
108
|
+
Ensure a root element exists. Creates `#game-root` if missing.
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const root = mount('#game-root'); // guaranteed non-null
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `on(root, type, selector, handler, options?)`
|
|
115
|
+
|
|
116
|
+
Delegated event listener that survives re-renders.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
on(root, 'click', '#button', () => {
|
|
120
|
+
console.log('Clicked!');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Later, safe to do:
|
|
124
|
+
root.innerHTML = '<button id="button">Click</button>';
|
|
125
|
+
// Event still works!
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `canvas(root, opts?): { canvas, ctx, resize }`
|
|
129
|
+
|
|
130
|
+
Auto-sizing canvas with DPR handling.
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
const { canvas: c, ctx } = canvas(container, { pixelRatio: 'device' });
|
|
134
|
+
// Auto-resizes when container changes
|
|
135
|
+
// Handles retina displays automatically
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `loop(tick): { start, stop }`
|
|
139
|
+
|
|
140
|
+
RAF loop with delta time for frame-rate independence.
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
const { stop } = loop((dt) => {
|
|
144
|
+
player.x += speed * dt; // Same speed on 60Hz and 120Hz
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Later:
|
|
148
|
+
stop();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Why Star DOM?
|
|
152
|
+
|
|
153
|
+
### Problem: Event Listeners on Null
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// ❌ Breaks if script runs before DOM ready
|
|
157
|
+
const btn = document.getElementById('start');
|
|
158
|
+
btn.addEventListener('click', () => { /* ... */ }); // TypeError!
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
// ✅ Always works
|
|
163
|
+
import { onReady, mount, on } from 'star-canvas';
|
|
164
|
+
|
|
165
|
+
onReady(() => {
|
|
166
|
+
const root = mount();
|
|
167
|
+
on(root, 'click', '#start', () => { /* ... */ });
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Problem: Canvas Doesn't Resize
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
// ❌ Fixed size, doesn't adapt
|
|
175
|
+
const canvas = document.createElement('canvas');
|
|
176
|
+
canvas.width = 800;
|
|
177
|
+
canvas.height = 600;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
// ✅ Auto-sizes to container
|
|
182
|
+
const { canvas: c } = canvas(container);
|
|
183
|
+
// Resizes when sidebar appears/disappears
|
|
184
|
+
// Handles DPR for crisp rendering
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Problem: Game Speed Varies by Device
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
// ❌ 2x faster on 120Hz displays
|
|
191
|
+
function gameLoop() {
|
|
192
|
+
player.x += 5; // 300px/sec on 60Hz, 600px/sec on 120Hz
|
|
193
|
+
requestAnimationFrame(gameLoop);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
// ✅ Same speed everywhere
|
|
199
|
+
loop((dt) => {
|
|
200
|
+
player.x += 300 * dt; // 300px/sec on ALL devices
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";var _=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var W=Object.prototype.hasOwnProperty;var $=(r,n)=>{for(var o in n)_(r,o,{get:n[o],enumerable:!0})},B=(r,n,o,e)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of F(n))!W.call(r,i)&&i!==o&&_(r,i,{get:()=>n[i],enumerable:!(e=Y(n,i))||e.enumerable});return r};var P=r=>B(_({},"__esModule",{value:!0}),r);var V={};$(V,{createDragState:()=>X,game:()=>U,version:()=>I});module.exports=P(V);var I="0.7.0",z={landscape:{width:640,height:360},portrait:{width:360,height:640},responsive:{}};function X(r){let n=null,o=0,e=0;return{point(i){return r(i)},grab(i,f){let{x,y:M}=r(i);n=f,o=x-f.x,e=M-f.y},move(i){if(n){let{x:f,y:x}=r(i);n.x=f-o,n.y=x-e}},release(){let i=n;return n=null,i},get dragging(){return n}}}function k(){return typeof window<"u"&&typeof document<"u"}function N(){if(!k()||document.getElementById("star-dom-base"))return;let r=document.createElement("style");r.id="star-dom-base",r.textContent=`
|
|
2
|
+
html, body { height: 100%; }
|
|
3
|
+
body {
|
|
4
|
+
margin: 0; min-height: 100dvh; overflow: hidden; background: #000;
|
|
5
|
+
-webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
|
|
6
|
+
}
|
|
7
|
+
/* Canvas is at z-index 0 */
|
|
8
|
+
.star-canvas {
|
|
9
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
10
|
+
overflow: hidden; touch-action: none;
|
|
11
|
+
z-index: 0;
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
/* UI overlay - interactive and scrollable by default (standard CSS behavior) */
|
|
15
|
+
.star-ui {
|
|
16
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
17
|
+
overflow-y: auto; z-index: 10;
|
|
18
|
+
}
|
|
19
|
+
`,document.head.appendChild(r)}function U(r,n={}){k()&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>H(r,n),{once:!0}):queueMicrotask(()=>H(r,n)))}function H(r,n){if(N(),window.__STAR_DOM__?.destroy)try{window.__STAR_DOM__.destroy()}catch{}document.querySelectorAll(".star-ui, .star-canvas").forEach(t=>t.remove());let o=document.createElement("div");o.className="star-ui",document.body.appendChild(o);let e=document.createElement("canvas");e.className="star-canvas",document.body.appendChild(e);let i=e.getContext("2d",n.contextAttributes??{alpha:!0});if(!i)throw new Error("[star-dom] Failed to get 2D context");if(n.preventContextMenu!==!1){let t=a=>a.preventDefault();e.addEventListener("contextmenu",t)}let f=e.addEventListener.bind(e);e.addEventListener=function(t,a,s){return/^(pointer|mouse|touch)/.test(t)&&(o.style.pointerEvents="none"),f(t,a,s)};let x=n.preset??"landscape",M=z[x]??z.landscape,w=n.width??M.width,E=n.height??M.height,A=n.maxPixelRatio??2,D=n.pixelRatio??"device",C=n.fit??"contain",g=1,c=w??1,u=E??1,R=new Set,T=null,b=[];function q(){return Math.min(typeof D=="number"?Math.max(1,D):Math.max(1,window.devicePixelRatio||1),A)}function p(){g=q();let t=document.body.getBoundingClientRect(),a=Math.max(1,Math.floor(t.width||window.innerWidth||800)),s=Math.max(1,Math.floor(t.height||window.innerHeight||600));if(w&&E){c=w,u=E;let d=c/u,h=a/s,v=1;if(C==="contain"?v=h>d?s/u:a/c:C==="cover"&&(v=h>d?a/c:s/u),C==="stretch")e.style.width="100%",e.style.height="100%";else{let y=Math.floor(c*v),O=Math.floor(u*v);e.style.width=`${y}px`,e.style.height=`${O}px`,e.style.position="absolute",e.style.left=`${Math.floor((a-y)/2)}px`,e.style.top=`${Math.floor((s-O)/2)}px`}}else if(E){u=E;let d=a/s;c=Math.floor(u*d),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else if(w){c=w;let d=a/s;u=Math.floor(c/d),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else c=a,u=s,e.style.width=`${c}px`,e.style.height=`${u}px`,e.style.position="absolute",e.style.left="0",e.style.top="0";let l=Math.max(1,Math.floor(c*g)),m=Math.max(1,Math.floor(u*g));e.width!==l&&(e.width=l),e.height!==m&&(e.height=m),i.setTransform(g,0,0,g,0,0),R.forEach(d=>d())}let G=new ResizeObserver(p);G.observe(document.body),b.push(()=>G.disconnect()),window.addEventListener("resize",p),b.push(()=>window.removeEventListener("resize",p));let L=window.visualViewport;L&&(L.addEventListener("resize",p),b.push(()=>L.removeEventListener("resize",p)));let S={stage:document.body,canvas:e,ctx:i,get width(){return c},get height(){return u},get dpr(){return g},resize:p,toStagePoint:t=>{let a=e.getBoundingClientRect(),s=(t.clientX-a.left)*(c/a.width),l=(t.clientY-a.top)*(u/a.height);return{x:s,y:l}},createDrag:()=>X(S.toStagePoint),on:(t,a,s,l)=>{let m=h=>{let y=h.target?.closest?.(a);y&&s.call(y,h)};document.addEventListener(t,m,l);let d=()=>document.removeEventListener(t,m,l);return b.push(d),d},loop:t=>{let s=0,l=!1,m=0,d=h=>{if(!l)return;let v=m?Math.min((h-m)/1e3,.1):0;m=h;try{t(v,h)}catch(y){console.error("[star-dom] Game loop error:",y)}s=requestAnimationFrame(d)};return T={get running(){return l},start(){l||(l=!0,m=performance.now(),s=requestAnimationFrame(d))},stop(){l&&(l=!1,cancelAnimationFrame(s))}},T.start(),T},ui:{root:o,render:t=>{o.innerHTML!==t&&(o.innerHTML=t)},el:t=>o.querySelector(t),all:t=>o.querySelectorAll(t)},destroy:()=>{T?.stop(),b.forEach(t=>t()),b=[],R.clear(),e.parentElement&&e.parentElement.removeChild(e),o.parentElement&&o.parentElement.removeChild(o)},scoped:t=>{i.save();try{t()}finally{i.restore()}}};window.__STAR_DOM__={destroy:S.destroy},requestAnimationFrame(()=>requestAnimationFrame(()=>{p();try{r(S)}catch(t){console.error("[star-dom] Game initialization failed:",t)}}))}0&&(module.exports={createDragState,game,version});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Star DOM SDK v0.6.7
|
|
3
|
+
* MIT License
|
|
4
|
+
* https://buildwithstar.com
|
|
5
|
+
*
|
|
6
|
+
* A tiny, reliable DOM & Canvas SDK for browser games.
|
|
7
|
+
* Solves common LLM bugs: DOM timing, null listeners,
|
|
8
|
+
* canvas sizing/DPR, and canvas-vs-UI conflicts.
|
|
9
|
+
*
|
|
10
|
+
* Designed for LLMs and humans: owns the iframe <body>,
|
|
11
|
+
* provides a scrollable UI overlay, stable loop with dt,
|
|
12
|
+
* DPR-correct canvas sizing, and safe delegated events.
|
|
13
|
+
*
|
|
14
|
+
* ~1.5KB min+gz, zero deps, SSR-safe, iframe-first.
|
|
15
|
+
*/
|
|
16
|
+
declare const version = "0.7.0";
|
|
17
|
+
declare global {
|
|
18
|
+
interface Window {
|
|
19
|
+
__STAR_DOM__?: {
|
|
20
|
+
destroy: () => void;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
type GameTick = (dt: number, now: number) => void;
|
|
25
|
+
interface GameLoop {
|
|
26
|
+
start: () => void;
|
|
27
|
+
stop: () => void;
|
|
28
|
+
readonly running: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface GameUI {
|
|
31
|
+
/** The root <div> element for UI, stacked on top of the canvas. */
|
|
32
|
+
readonly root: HTMLElement;
|
|
33
|
+
/** Safely replaces the content of the UI overlay. */
|
|
34
|
+
render: (html: string) => void;
|
|
35
|
+
/** Scoped querySelector for the UI root. */
|
|
36
|
+
el: <T extends Element = HTMLElement>(selector: string) => T | null;
|
|
37
|
+
/** Scoped querySelectorAll for the UI root. */
|
|
38
|
+
all: <T extends Element = HTMLElement>(selector: string) => NodeListOf<T>;
|
|
39
|
+
}
|
|
40
|
+
interface GameContext {
|
|
41
|
+
/** The root <div> element for the canvas. */
|
|
42
|
+
readonly stage: HTMLElement;
|
|
43
|
+
/** The canvas element. */
|
|
44
|
+
readonly canvas: HTMLCanvasElement;
|
|
45
|
+
/** The 2D rendering context. */
|
|
46
|
+
readonly ctx: CanvasRenderingContext2D;
|
|
47
|
+
/** Logical width (CSS px). Use this for game logic. Default: 640 (landscape) or 360 (portrait). */
|
|
48
|
+
readonly width: number;
|
|
49
|
+
/** Logical height (CSS px). Use this for game logic. Default: 360 (landscape) or 640 (portrait). */
|
|
50
|
+
readonly height: number;
|
|
51
|
+
/** Effective device pixel ratio. */
|
|
52
|
+
readonly dpr: number;
|
|
53
|
+
/** Safely attaches delegated event listeners to the document. */
|
|
54
|
+
on: (type: string, selector: string, handler: (this: Element, ev: Event) => void, options?: AddEventListenerOptions) => () => void;
|
|
55
|
+
/** Starts a stable, auto-starting game loop. */
|
|
56
|
+
loop: (tick: GameTick) => GameLoop;
|
|
57
|
+
/** The dedicated UI overlay manager. */
|
|
58
|
+
readonly ui: GameUI;
|
|
59
|
+
/** Converts DOM event coords to logical stage coords (CSS px). */
|
|
60
|
+
toStagePoint: (e: {
|
|
61
|
+
clientX: number;
|
|
62
|
+
clientY: number;
|
|
63
|
+
}) => {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
};
|
|
67
|
+
/** Creates a drag state helper for pointer-based dragging. Handles coordinate conversion and offset. */
|
|
68
|
+
createDrag: <T extends {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
}>() => DragState<T>;
|
|
72
|
+
/** Re-calculates stage size (rarely needed). */
|
|
73
|
+
resize: () => void;
|
|
74
|
+
/** Cleans up all listeners and observers. */
|
|
75
|
+
destroy: () => void;
|
|
76
|
+
/** Runs a function with ctx.save/restore automatically managed. */
|
|
77
|
+
scoped: (fn: () => void) => void;
|
|
78
|
+
}
|
|
79
|
+
interface GameOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Preset for common game orientations (all use 16:9 aspect ratio with letterboxing).
|
|
82
|
+
* - 'landscape' (default): 640x360, works identically on all devices
|
|
83
|
+
* - 'portrait': 360x640, for mobile-style games
|
|
84
|
+
* - 'responsive': No fixed dimensions, fills container (legacy behavior, gameplay varies by device)
|
|
85
|
+
*/
|
|
86
|
+
preset?: 'landscape' | 'portrait' | 'responsive';
|
|
87
|
+
/**
|
|
88
|
+
* Logical width for fixed-size stages (e.g., 800).
|
|
89
|
+
* Default: 640 (landscape preset).
|
|
90
|
+
*/
|
|
91
|
+
width?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Logical height for fixed-size stages (e.g., 600).
|
|
94
|
+
* Default: 360 (landscape preset).
|
|
95
|
+
*/
|
|
96
|
+
height?: number;
|
|
97
|
+
/**
|
|
98
|
+
* How a fixed-size stage fits its container.
|
|
99
|
+
* - 'contain' (default): Letterbox/pillarbox to fit.
|
|
100
|
+
* - 'cover': Fill container and crop.
|
|
101
|
+
* - 'stretch': Distort to fill container.
|
|
102
|
+
*/
|
|
103
|
+
fit?: 'contain' | 'cover' | 'stretch';
|
|
104
|
+
/**
|
|
105
|
+
* Backing store DPR. 'device' (default) or a number.
|
|
106
|
+
*/
|
|
107
|
+
pixelRatio?: 'device' | number;
|
|
108
|
+
/**
|
|
109
|
+
* Max cap for 'device' DPR. Default: 2.
|
|
110
|
+
*/
|
|
111
|
+
maxPixelRatio?: number;
|
|
112
|
+
/**
|
|
113
|
+
* 2D context attributes (e.g., { alpha: false }).
|
|
114
|
+
*/
|
|
115
|
+
contextAttributes?: CanvasRenderingContext2DSettings;
|
|
116
|
+
/**
|
|
117
|
+
* Prevent context menu on canvas. Default: true.
|
|
118
|
+
*/
|
|
119
|
+
preventContextMenu?: boolean;
|
|
120
|
+
}
|
|
121
|
+
/** Drag state helper - handles coordinate conversion and offset tracking */
|
|
122
|
+
interface DragState<T extends {
|
|
123
|
+
x: number;
|
|
124
|
+
y: number;
|
|
125
|
+
}> {
|
|
126
|
+
/** Convert event to stage coordinates (pure function, no side effects) */
|
|
127
|
+
point: (e: {
|
|
128
|
+
clientX: number;
|
|
129
|
+
clientY: number;
|
|
130
|
+
}) => {
|
|
131
|
+
x: number;
|
|
132
|
+
y: number;
|
|
133
|
+
};
|
|
134
|
+
/** Start dragging an object - computes offset from cursor to object origin */
|
|
135
|
+
grab: (e: {
|
|
136
|
+
clientX: number;
|
|
137
|
+
clientY: number;
|
|
138
|
+
}, obj: T) => void;
|
|
139
|
+
/** Update the grabbed object's position based on pointer movement */
|
|
140
|
+
move: (e: {
|
|
141
|
+
clientX: number;
|
|
142
|
+
clientY: number;
|
|
143
|
+
}) => void;
|
|
144
|
+
/** Release the grabbed object and return it (or null if nothing was grabbed) */
|
|
145
|
+
release: () => T | null;
|
|
146
|
+
/** The currently grabbed object (or null) */
|
|
147
|
+
readonly dragging: T | null;
|
|
148
|
+
}
|
|
149
|
+
/** Creates a drag state helper that handles coordinate conversion and offset tracking */
|
|
150
|
+
declare function createDragState<T extends {
|
|
151
|
+
x: number;
|
|
152
|
+
y: number;
|
|
153
|
+
}>(toStagePoint: (e: {
|
|
154
|
+
clientX: number;
|
|
155
|
+
clientY: number;
|
|
156
|
+
}) => {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
}): DragState<T>;
|
|
160
|
+
/**
|
|
161
|
+
* The main entry point. Runs setup when the DOM is ready,
|
|
162
|
+
* providing a safe context for building a game.
|
|
163
|
+
*/
|
|
164
|
+
declare function game(setup: (g: GameContext) => void, options?: GameOptions): void;
|
|
165
|
+
|
|
166
|
+
export { type DragState, type GameContext, type GameLoop, type GameOptions, type GameTick, type GameUI, createDragState, game, version };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Star DOM SDK v0.6.7
|
|
3
|
+
* MIT License
|
|
4
|
+
* https://buildwithstar.com
|
|
5
|
+
*
|
|
6
|
+
* A tiny, reliable DOM & Canvas SDK for browser games.
|
|
7
|
+
* Solves common LLM bugs: DOM timing, null listeners,
|
|
8
|
+
* canvas sizing/DPR, and canvas-vs-UI conflicts.
|
|
9
|
+
*
|
|
10
|
+
* Designed for LLMs and humans: owns the iframe <body>,
|
|
11
|
+
* provides a scrollable UI overlay, stable loop with dt,
|
|
12
|
+
* DPR-correct canvas sizing, and safe delegated events.
|
|
13
|
+
*
|
|
14
|
+
* ~1.5KB min+gz, zero deps, SSR-safe, iframe-first.
|
|
15
|
+
*/
|
|
16
|
+
declare const version = "0.7.0";
|
|
17
|
+
declare global {
|
|
18
|
+
interface Window {
|
|
19
|
+
__STAR_DOM__?: {
|
|
20
|
+
destroy: () => void;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
type GameTick = (dt: number, now: number) => void;
|
|
25
|
+
interface GameLoop {
|
|
26
|
+
start: () => void;
|
|
27
|
+
stop: () => void;
|
|
28
|
+
readonly running: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface GameUI {
|
|
31
|
+
/** The root <div> element for UI, stacked on top of the canvas. */
|
|
32
|
+
readonly root: HTMLElement;
|
|
33
|
+
/** Safely replaces the content of the UI overlay. */
|
|
34
|
+
render: (html: string) => void;
|
|
35
|
+
/** Scoped querySelector for the UI root. */
|
|
36
|
+
el: <T extends Element = HTMLElement>(selector: string) => T | null;
|
|
37
|
+
/** Scoped querySelectorAll for the UI root. */
|
|
38
|
+
all: <T extends Element = HTMLElement>(selector: string) => NodeListOf<T>;
|
|
39
|
+
}
|
|
40
|
+
interface GameContext {
|
|
41
|
+
/** The root <div> element for the canvas. */
|
|
42
|
+
readonly stage: HTMLElement;
|
|
43
|
+
/** The canvas element. */
|
|
44
|
+
readonly canvas: HTMLCanvasElement;
|
|
45
|
+
/** The 2D rendering context. */
|
|
46
|
+
readonly ctx: CanvasRenderingContext2D;
|
|
47
|
+
/** Logical width (CSS px). Use this for game logic. Default: 640 (landscape) or 360 (portrait). */
|
|
48
|
+
readonly width: number;
|
|
49
|
+
/** Logical height (CSS px). Use this for game logic. Default: 360 (landscape) or 640 (portrait). */
|
|
50
|
+
readonly height: number;
|
|
51
|
+
/** Effective device pixel ratio. */
|
|
52
|
+
readonly dpr: number;
|
|
53
|
+
/** Safely attaches delegated event listeners to the document. */
|
|
54
|
+
on: (type: string, selector: string, handler: (this: Element, ev: Event) => void, options?: AddEventListenerOptions) => () => void;
|
|
55
|
+
/** Starts a stable, auto-starting game loop. */
|
|
56
|
+
loop: (tick: GameTick) => GameLoop;
|
|
57
|
+
/** The dedicated UI overlay manager. */
|
|
58
|
+
readonly ui: GameUI;
|
|
59
|
+
/** Converts DOM event coords to logical stage coords (CSS px). */
|
|
60
|
+
toStagePoint: (e: {
|
|
61
|
+
clientX: number;
|
|
62
|
+
clientY: number;
|
|
63
|
+
}) => {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
};
|
|
67
|
+
/** Creates a drag state helper for pointer-based dragging. Handles coordinate conversion and offset. */
|
|
68
|
+
createDrag: <T extends {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
}>() => DragState<T>;
|
|
72
|
+
/** Re-calculates stage size (rarely needed). */
|
|
73
|
+
resize: () => void;
|
|
74
|
+
/** Cleans up all listeners and observers. */
|
|
75
|
+
destroy: () => void;
|
|
76
|
+
/** Runs a function with ctx.save/restore automatically managed. */
|
|
77
|
+
scoped: (fn: () => void) => void;
|
|
78
|
+
}
|
|
79
|
+
interface GameOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Preset for common game orientations (all use 16:9 aspect ratio with letterboxing).
|
|
82
|
+
* - 'landscape' (default): 640x360, works identically on all devices
|
|
83
|
+
* - 'portrait': 360x640, for mobile-style games
|
|
84
|
+
* - 'responsive': No fixed dimensions, fills container (legacy behavior, gameplay varies by device)
|
|
85
|
+
*/
|
|
86
|
+
preset?: 'landscape' | 'portrait' | 'responsive';
|
|
87
|
+
/**
|
|
88
|
+
* Logical width for fixed-size stages (e.g., 800).
|
|
89
|
+
* Default: 640 (landscape preset).
|
|
90
|
+
*/
|
|
91
|
+
width?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Logical height for fixed-size stages (e.g., 600).
|
|
94
|
+
* Default: 360 (landscape preset).
|
|
95
|
+
*/
|
|
96
|
+
height?: number;
|
|
97
|
+
/**
|
|
98
|
+
* How a fixed-size stage fits its container.
|
|
99
|
+
* - 'contain' (default): Letterbox/pillarbox to fit.
|
|
100
|
+
* - 'cover': Fill container and crop.
|
|
101
|
+
* - 'stretch': Distort to fill container.
|
|
102
|
+
*/
|
|
103
|
+
fit?: 'contain' | 'cover' | 'stretch';
|
|
104
|
+
/**
|
|
105
|
+
* Backing store DPR. 'device' (default) or a number.
|
|
106
|
+
*/
|
|
107
|
+
pixelRatio?: 'device' | number;
|
|
108
|
+
/**
|
|
109
|
+
* Max cap for 'device' DPR. Default: 2.
|
|
110
|
+
*/
|
|
111
|
+
maxPixelRatio?: number;
|
|
112
|
+
/**
|
|
113
|
+
* 2D context attributes (e.g., { alpha: false }).
|
|
114
|
+
*/
|
|
115
|
+
contextAttributes?: CanvasRenderingContext2DSettings;
|
|
116
|
+
/**
|
|
117
|
+
* Prevent context menu on canvas. Default: true.
|
|
118
|
+
*/
|
|
119
|
+
preventContextMenu?: boolean;
|
|
120
|
+
}
|
|
121
|
+
/** Drag state helper - handles coordinate conversion and offset tracking */
|
|
122
|
+
interface DragState<T extends {
|
|
123
|
+
x: number;
|
|
124
|
+
y: number;
|
|
125
|
+
}> {
|
|
126
|
+
/** Convert event to stage coordinates (pure function, no side effects) */
|
|
127
|
+
point: (e: {
|
|
128
|
+
clientX: number;
|
|
129
|
+
clientY: number;
|
|
130
|
+
}) => {
|
|
131
|
+
x: number;
|
|
132
|
+
y: number;
|
|
133
|
+
};
|
|
134
|
+
/** Start dragging an object - computes offset from cursor to object origin */
|
|
135
|
+
grab: (e: {
|
|
136
|
+
clientX: number;
|
|
137
|
+
clientY: number;
|
|
138
|
+
}, obj: T) => void;
|
|
139
|
+
/** Update the grabbed object's position based on pointer movement */
|
|
140
|
+
move: (e: {
|
|
141
|
+
clientX: number;
|
|
142
|
+
clientY: number;
|
|
143
|
+
}) => void;
|
|
144
|
+
/** Release the grabbed object and return it (or null if nothing was grabbed) */
|
|
145
|
+
release: () => T | null;
|
|
146
|
+
/** The currently grabbed object (or null) */
|
|
147
|
+
readonly dragging: T | null;
|
|
148
|
+
}
|
|
149
|
+
/** Creates a drag state helper that handles coordinate conversion and offset tracking */
|
|
150
|
+
declare function createDragState<T extends {
|
|
151
|
+
x: number;
|
|
152
|
+
y: number;
|
|
153
|
+
}>(toStagePoint: (e: {
|
|
154
|
+
clientX: number;
|
|
155
|
+
clientY: number;
|
|
156
|
+
}) => {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
}): DragState<T>;
|
|
160
|
+
/**
|
|
161
|
+
* The main entry point. Runs setup when the DOM is ready,
|
|
162
|
+
* providing a safe context for building a game.
|
|
163
|
+
*/
|
|
164
|
+
declare function game(setup: (g: GameContext) => void, options?: GameOptions): void;
|
|
165
|
+
|
|
166
|
+
export { type DragState, type GameContext, type GameLoop, type GameOptions, type GameTick, type GameUI, createDragState, game, version };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
var Y="0.7.0",O={landscape:{width:640,height:360},portrait:{width:360,height:640},responsive:{}};function k(u){let n=null,i=0,e=0;return{point(l){return u(l)},grab(l,f){let{x,y:M}=u(l);n=f,i=x-f.x,e=M-f.y},move(l){if(n){let{x:f,y:x}=u(l);n.x=f-i,n.y=x-e}},release(){let l=n;return n=null,l},get dragging(){return n}}}function H(){return typeof window<"u"&&typeof document<"u"}function q(){if(!H()||document.getElementById("star-dom-base"))return;let u=document.createElement("style");u.id="star-dom-base",u.textContent=`
|
|
2
|
+
html, body { height: 100%; }
|
|
3
|
+
body {
|
|
4
|
+
margin: 0; min-height: 100dvh; overflow: hidden; background: #000;
|
|
5
|
+
-webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
|
|
6
|
+
}
|
|
7
|
+
/* Canvas is at z-index 0 */
|
|
8
|
+
.star-canvas {
|
|
9
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
10
|
+
overflow: hidden; touch-action: none;
|
|
11
|
+
z-index: 0;
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
/* UI overlay - interactive and scrollable by default (standard CSS behavior) */
|
|
15
|
+
.star-ui {
|
|
16
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
17
|
+
overflow-y: auto; z-index: 10;
|
|
18
|
+
}
|
|
19
|
+
`,document.head.appendChild(u)}function F(u,n={}){H()&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>z(u,n),{once:!0}):queueMicrotask(()=>z(u,n)))}function z(u,n){if(q(),window.__STAR_DOM__?.destroy)try{window.__STAR_DOM__.destroy()}catch{}document.querySelectorAll(".star-ui, .star-canvas").forEach(t=>t.remove());let i=document.createElement("div");i.className="star-ui",document.body.appendChild(i);let e=document.createElement("canvas");e.className="star-canvas",document.body.appendChild(e);let l=e.getContext("2d",n.contextAttributes??{alpha:!0});if(!l)throw new Error("[star-dom] Failed to get 2D context");if(n.preventContextMenu!==!1){let t=r=>r.preventDefault();e.addEventListener("contextmenu",t)}let f=e.addEventListener.bind(e);e.addEventListener=function(t,r,o){return/^(pointer|mouse|touch)/.test(t)&&(i.style.pointerEvents="none"),f(t,r,o)};let x=n.preset??"landscape",M=O[x]??O.landscape,w=n.width??M.width,E=n.height??M.height,_=n.maxPixelRatio??2,A=n.pixelRatio??"device",C=n.fit??"contain",g=1,d=w??1,c=E??1,D=new Set,T=null,b=[];function X(){return Math.min(typeof A=="number"?Math.max(1,A):Math.max(1,window.devicePixelRatio||1),_)}function p(){g=X();let t=document.body.getBoundingClientRect(),r=Math.max(1,Math.floor(t.width||window.innerWidth||800)),o=Math.max(1,Math.floor(t.height||window.innerHeight||600));if(w&&E){d=w,c=E;let s=d/c,h=r/o,v=1;if(C==="contain"?v=h>s?o/c:r/d:C==="cover"&&(v=h>s?r/d:o/c),C==="stretch")e.style.width="100%",e.style.height="100%";else{let y=Math.floor(d*v),G=Math.floor(c*v);e.style.width=`${y}px`,e.style.height=`${G}px`,e.style.position="absolute",e.style.left=`${Math.floor((r-y)/2)}px`,e.style.top=`${Math.floor((o-G)/2)}px`}}else if(E){c=E;let s=r/o;d=Math.floor(c*s),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else if(w){d=w;let s=r/o;c=Math.floor(d/s),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else d=r,c=o,e.style.width=`${d}px`,e.style.height=`${c}px`,e.style.position="absolute",e.style.left="0",e.style.top="0";let a=Math.max(1,Math.floor(d*g)),m=Math.max(1,Math.floor(c*g));e.width!==a&&(e.width=a),e.height!==m&&(e.height=m),l.setTransform(g,0,0,g,0,0),D.forEach(s=>s())}let R=new ResizeObserver(p);R.observe(document.body),b.push(()=>R.disconnect()),window.addEventListener("resize",p),b.push(()=>window.removeEventListener("resize",p));let L=window.visualViewport;L&&(L.addEventListener("resize",p),b.push(()=>L.removeEventListener("resize",p)));let S={stage:document.body,canvas:e,ctx:l,get width(){return d},get height(){return c},get dpr(){return g},resize:p,toStagePoint:t=>{let r=e.getBoundingClientRect(),o=(t.clientX-r.left)*(d/r.width),a=(t.clientY-r.top)*(c/r.height);return{x:o,y:a}},createDrag:()=>k(S.toStagePoint),on:(t,r,o,a)=>{let m=h=>{let y=h.target?.closest?.(r);y&&o.call(y,h)};document.addEventListener(t,m,a);let s=()=>document.removeEventListener(t,m,a);return b.push(s),s},loop:t=>{let o=0,a=!1,m=0,s=h=>{if(!a)return;let v=m?Math.min((h-m)/1e3,.1):0;m=h;try{t(v,h)}catch(y){console.error("[star-dom] Game loop error:",y)}o=requestAnimationFrame(s)};return T={get running(){return a},start(){a||(a=!0,m=performance.now(),o=requestAnimationFrame(s))},stop(){a&&(a=!1,cancelAnimationFrame(o))}},T.start(),T},ui:{root:i,render:t=>{i.innerHTML!==t&&(i.innerHTML=t)},el:t=>i.querySelector(t),all:t=>i.querySelectorAll(t)},destroy:()=>{T?.stop(),b.forEach(t=>t()),b=[],D.clear(),e.parentElement&&e.parentElement.removeChild(e),i.parentElement&&i.parentElement.removeChild(i)},scoped:t=>{l.save();try{t()}finally{l.restore()}}};window.__STAR_DOM__={destroy:S.destroy},requestAnimationFrame(()=>requestAnimationFrame(()=>{p();try{u(S)}catch(t){console.error("[star-dom] Game initialization failed:",t)}}))}export{k as createDragState,F as game,Y as version};
|
package/dist/legacy.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";var S=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var I=(o,t)=>{for(var r in t)S(o,r,{get:t[r],enumerable:!0})},P=(o,t,r,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of $(t))!B.call(o,i)&&i!==r&&S(o,i,{get:()=>t[i],enumerable:!(e=W(t,i))||e.enumerable});return o};var U=o=>P(S({},"__esModule",{value:!0}),o);var j={};I(j,{createDragState:()=>_,game:()=>V,version:()=>X});module.exports=U(j);var X="0.7.0",H={landscape:{width:640,height:360},portrait:{width:360,height:640},responsive:{}};function _(o){let t=null,r=0,e=0;return{point(i){return o(i)},grab(i,p){let{x,y:M}=o(i);t=p,r=x-p.x,e=M-p.y},move(i){if(t){let{x:p,y:x}=o(i);t.x=p-r,t.y=x-e}},release(){let i=t;return t=null,i},get dragging(){return t}}}function q(){return typeof window<"u"&&typeof document<"u"}function N(){if(!q()||document.getElementById("star-dom-base"))return;let o=document.createElement("style");o.id="star-dom-base",o.textContent=`
|
|
2
|
+
html, body { height: 100%; }
|
|
3
|
+
body {
|
|
4
|
+
margin: 0; min-height: 100dvh; overflow: hidden; background: #000;
|
|
5
|
+
-webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
|
|
6
|
+
}
|
|
7
|
+
/* Canvas is at z-index 0 */
|
|
8
|
+
.star-canvas {
|
|
9
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
10
|
+
overflow: hidden; touch-action: none;
|
|
11
|
+
z-index: 0;
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
/* UI overlay - interactive and scrollable by default (standard CSS behavior) */
|
|
15
|
+
.star-ui {
|
|
16
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
17
|
+
overflow-y: auto; z-index: 10;
|
|
18
|
+
}
|
|
19
|
+
`,document.head.appendChild(o)}function Y(o,t={}){q()&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>k(o,t),{once:!0}):queueMicrotask(()=>k(o,t)))}function k(o,t){if(N(),window.__STAR_DOM__?.destroy)try{window.__STAR_DOM__.destroy()}catch{}document.querySelectorAll(".star-ui, .star-canvas").forEach(n=>n.remove());let r=document.createElement("div");r.className="star-ui",document.body.appendChild(r);let e=document.createElement("canvas");e.className="star-canvas",document.body.appendChild(e);let i=e.getContext("2d",t.contextAttributes??{alpha:!0});if(!i)throw new Error("[star-dom] Failed to get 2D context");if(t.preventContextMenu!==!1){let n=a=>a.preventDefault();e.addEventListener("contextmenu",n)}let p=e.addEventListener.bind(e);e.addEventListener=function(n,a,s){return/^(pointer|mouse|touch)/.test(n)&&(r.style.pointerEvents="none"),p(n,a,s)};let x=t.preset??"landscape",M=H[x]??H.landscape,w=t.width??M.width,E=t.height??M.height,A=t.maxPixelRatio??2,D=t.pixelRatio??"device",C=t.fit??"contain",g=1,c=w??1,u=E??1,O=new Set,T=null,b=[];function F(){return Math.min(typeof D=="number"?Math.max(1,D):Math.max(1,window.devicePixelRatio||1),A)}function f(){g=F();let n=document.body.getBoundingClientRect(),a=Math.max(1,Math.floor(n.width||window.innerWidth||800)),s=Math.max(1,Math.floor(n.height||window.innerHeight||600));if(w&&E){c=w,u=E;let d=c/u,h=a/s,v=1;if(C==="contain"?v=h>d?s/u:a/c:C==="cover"&&(v=h>d?a/c:s/u),C==="stretch")e.style.width="100%",e.style.height="100%";else{let y=Math.floor(c*v),z=Math.floor(u*v);e.style.width=`${y}px`,e.style.height=`${z}px`,e.style.position="absolute",e.style.left=`${Math.floor((a-y)/2)}px`,e.style.top=`${Math.floor((s-z)/2)}px`}}else if(E){u=E;let d=a/s;c=Math.floor(u*d),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else if(w){c=w;let d=a/s;u=Math.floor(c/d),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else c=a,u=s,e.style.width=`${c}px`,e.style.height=`${u}px`,e.style.position="absolute",e.style.left="0",e.style.top="0";let l=Math.max(1,Math.floor(c*g)),m=Math.max(1,Math.floor(u*g));e.width!==l&&(e.width=l),e.height!==m&&(e.height=m),i.setTransform(g,0,0,g,0,0),O.forEach(d=>d())}let R=new ResizeObserver(f);R.observe(document.body),b.push(()=>R.disconnect()),window.addEventListener("resize",f),b.push(()=>window.removeEventListener("resize",f));let L=window.visualViewport;L&&(L.addEventListener("resize",f),b.push(()=>L.removeEventListener("resize",f)));let G={stage:document.body,canvas:e,ctx:i,get width(){return c},get height(){return u},get dpr(){return g},resize:f,toStagePoint:n=>{let a=e.getBoundingClientRect(),s=(n.clientX-a.left)*(c/a.width),l=(n.clientY-a.top)*(u/a.height);return{x:s,y:l}},createDrag:()=>_(G.toStagePoint),on:(n,a,s,l)=>{let m=h=>{let y=h.target?.closest?.(a);y&&s.call(y,h)};document.addEventListener(n,m,l);let d=()=>document.removeEventListener(n,m,l);return b.push(d),d},loop:n=>{let s=0,l=!1,m=0,d=h=>{if(!l)return;let v=m?Math.min((h-m)/1e3,.1):0;m=h;try{n(v,h)}catch(y){console.error("[star-dom] Game loop error:",y)}s=requestAnimationFrame(d)};return T={get running(){return l},start(){l||(l=!0,m=performance.now(),s=requestAnimationFrame(d))},stop(){l&&(l=!1,cancelAnimationFrame(s))}},T.start(),T},ui:{root:r,render:n=>{r.innerHTML!==n&&(r.innerHTML=n)},el:n=>r.querySelector(n),all:n=>r.querySelectorAll(n)},destroy:()=>{T?.stop(),b.forEach(n=>n()),b=[],O.clear(),e.parentElement&&e.parentElement.removeChild(e),r.parentElement&&r.parentElement.removeChild(r)},scoped:n=>{i.save();try{n()}finally{i.restore()}}};window.__STAR_DOM__={destroy:G.destroy},requestAnimationFrame(()=>requestAnimationFrame(()=>{f();try{o(G)}catch(n){console.error("[star-dom] Game initialization failed:",n)}}))}function V(o,t={}){let r={preset:"responsive",...t};return Y(o,r)}0&&(module.exports={createDragState,game,version});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GameContext, GameOptions } from './index.cjs';
|
|
2
|
+
export { DragState, GameLoop, GameTick, GameUI, createDragState, version } from './index.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Star DOM SDK - Legacy Entry Point
|
|
6
|
+
*
|
|
7
|
+
* This entry point preserves backward compatibility with games created before v0.7.
|
|
8
|
+
* It defaults to 'responsive' mode (no fixed dimensions).
|
|
9
|
+
*
|
|
10
|
+
* New games should use the main entry point which defaults to 'landscape' preset.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Legacy game entry point - defaults to responsive mode for backward compatibility.
|
|
15
|
+
* New games should use '/star-sdk/dom-v1.js' which defaults to fixed-height mode.
|
|
16
|
+
*/
|
|
17
|
+
declare function game(setup: (g: GameContext) => void, options?: GameOptions): void;
|
|
18
|
+
|
|
19
|
+
export { GameContext, GameOptions, game };
|
package/dist/legacy.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GameContext, GameOptions } from './index.js';
|
|
2
|
+
export { DragState, GameLoop, GameTick, GameUI, createDragState, version } from './index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Star DOM SDK - Legacy Entry Point
|
|
6
|
+
*
|
|
7
|
+
* This entry point preserves backward compatibility with games created before v0.7.
|
|
8
|
+
* It defaults to 'responsive' mode (no fixed dimensions).
|
|
9
|
+
*
|
|
10
|
+
* New games should use the main entry point which defaults to 'landscape' preset.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Legacy game entry point - defaults to responsive mode for backward compatibility.
|
|
15
|
+
* New games should use '/star-sdk/dom-v1.js' which defaults to fixed-height mode.
|
|
16
|
+
*/
|
|
17
|
+
declare function game(setup: (g: GameContext) => void, options?: GameOptions): void;
|
|
18
|
+
|
|
19
|
+
export { GameContext, GameOptions, game };
|
package/dist/legacy.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
var Y="0.7.0",R={landscape:{width:640,height:360},portrait:{width:360,height:640},responsive:{}};function H(a){let n=null,i=0,e=0;return{point(d){return a(d)},grab(d,p){let{x,y:M}=a(d);n=p,i=x-p.x,e=M-p.y},move(d){if(n){let{x:p,y:x}=a(d);n.x=p-i,n.y=x-e}},release(){let d=n;return n=null,d},get dragging(){return n}}}function k(){return typeof window<"u"&&typeof document<"u"}function F(){if(!k()||document.getElementById("star-dom-base"))return;let a=document.createElement("style");a.id="star-dom-base",a.textContent=`
|
|
2
|
+
html, body { height: 100%; }
|
|
3
|
+
body {
|
|
4
|
+
margin: 0; min-height: 100dvh; overflow: hidden; background: #000;
|
|
5
|
+
-webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
|
|
6
|
+
}
|
|
7
|
+
/* Canvas is at z-index 0 */
|
|
8
|
+
.star-canvas {
|
|
9
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
10
|
+
overflow: hidden; touch-action: none;
|
|
11
|
+
z-index: 0;
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
/* UI overlay - interactive and scrollable by default (standard CSS behavior) */
|
|
15
|
+
.star-ui {
|
|
16
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
17
|
+
overflow-y: auto; z-index: 10;
|
|
18
|
+
}
|
|
19
|
+
`,document.head.appendChild(a)}function X(a,n={}){k()&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>z(a,n),{once:!0}):queueMicrotask(()=>z(a,n)))}function z(a,n){if(F(),window.__STAR_DOM__?.destroy)try{window.__STAR_DOM__.destroy()}catch{}document.querySelectorAll(".star-ui, .star-canvas").forEach(t=>t.remove());let i=document.createElement("div");i.className="star-ui",document.body.appendChild(i);let e=document.createElement("canvas");e.className="star-canvas",document.body.appendChild(e);let d=e.getContext("2d",n.contextAttributes??{alpha:!0});if(!d)throw new Error("[star-dom] Failed to get 2D context");if(n.preventContextMenu!==!1){let t=o=>o.preventDefault();e.addEventListener("contextmenu",t)}let p=e.addEventListener.bind(e);e.addEventListener=function(t,o,r){return/^(pointer|mouse|touch)/.test(t)&&(i.style.pointerEvents="none"),p(t,o,r)};let x=n.preset??"landscape",M=R[x]??R.landscape,w=n.width??M.width,E=n.height??M.height,S=n.maxPixelRatio??2,_=n.pixelRatio??"device",C=n.fit??"contain",g=1,c=w??1,u=E??1,A=new Set,T=null,b=[];function q(){return Math.min(typeof _=="number"?Math.max(1,_):Math.max(1,window.devicePixelRatio||1),S)}function f(){g=q();let t=document.body.getBoundingClientRect(),o=Math.max(1,Math.floor(t.width||window.innerWidth||800)),r=Math.max(1,Math.floor(t.height||window.innerHeight||600));if(w&&E){c=w,u=E;let l=c/u,h=o/r,v=1;if(C==="contain"?v=h>l?r/u:o/c:C==="cover"&&(v=h>l?o/c:r/u),C==="stretch")e.style.width="100%",e.style.height="100%";else{let y=Math.floor(c*v),O=Math.floor(u*v);e.style.width=`${y}px`,e.style.height=`${O}px`,e.style.position="absolute",e.style.left=`${Math.floor((o-y)/2)}px`,e.style.top=`${Math.floor((r-O)/2)}px`}}else if(E){u=E;let l=o/r;c=Math.floor(u*l),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else if(w){c=w;let l=o/r;u=Math.floor(c/l),e.style.width="100%",e.style.height="100%",e.style.position="absolute",e.style.left="0",e.style.top="0"}else c=o,u=r,e.style.width=`${c}px`,e.style.height=`${u}px`,e.style.position="absolute",e.style.left="0",e.style.top="0";let s=Math.max(1,Math.floor(c*g)),m=Math.max(1,Math.floor(u*g));e.width!==s&&(e.width=s),e.height!==m&&(e.height=m),d.setTransform(g,0,0,g,0,0),A.forEach(l=>l())}let D=new ResizeObserver(f);D.observe(document.body),b.push(()=>D.disconnect()),window.addEventListener("resize",f),b.push(()=>window.removeEventListener("resize",f));let L=window.visualViewport;L&&(L.addEventListener("resize",f),b.push(()=>L.removeEventListener("resize",f)));let G={stage:document.body,canvas:e,ctx:d,get width(){return c},get height(){return u},get dpr(){return g},resize:f,toStagePoint:t=>{let o=e.getBoundingClientRect(),r=(t.clientX-o.left)*(c/o.width),s=(t.clientY-o.top)*(u/o.height);return{x:r,y:s}},createDrag:()=>H(G.toStagePoint),on:(t,o,r,s)=>{let m=h=>{let y=h.target?.closest?.(o);y&&r.call(y,h)};document.addEventListener(t,m,s);let l=()=>document.removeEventListener(t,m,s);return b.push(l),l},loop:t=>{let r=0,s=!1,m=0,l=h=>{if(!s)return;let v=m?Math.min((h-m)/1e3,.1):0;m=h;try{t(v,h)}catch(y){console.error("[star-dom] Game loop error:",y)}r=requestAnimationFrame(l)};return T={get running(){return s},start(){s||(s=!0,m=performance.now(),r=requestAnimationFrame(l))},stop(){s&&(s=!1,cancelAnimationFrame(r))}},T.start(),T},ui:{root:i,render:t=>{i.innerHTML!==t&&(i.innerHTML=t)},el:t=>i.querySelector(t),all:t=>i.querySelectorAll(t)},destroy:()=>{T?.stop(),b.forEach(t=>t()),b=[],A.clear(),e.parentElement&&e.parentElement.removeChild(e),i.parentElement&&i.parentElement.removeChild(i)},scoped:t=>{d.save();try{t()}finally{d.restore()}}};window.__STAR_DOM__={destroy:G.destroy},requestAnimationFrame(()=>requestAnimationFrame(()=>{f();try{a(G)}catch(t){console.error("[star-dom] Game initialization failed:",t)}}))}function B(a,n={}){let i={preset:"responsive",...n};return X(a,i)}export{H as createDragState,B as game,Y as version};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "star-canvas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Canvas game utilities for reliable game initialization - part of Star SDK.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"starSdk": {
|
|
9
|
+
"internalImport": "import { game } from '/star-sdk/v1/dom.js';",
|
|
10
|
+
"publicImport": "import { game } from 'star-canvas';",
|
|
11
|
+
"publicPath": "dist/index.mjs",
|
|
12
|
+
"outputs": [
|
|
13
|
+
{ "src": "dist/legacy.mjs", "dest": "dom.js" },
|
|
14
|
+
{ "src": "dist/index.mjs", "dest": "v1/dom.js" }
|
|
15
|
+
],
|
|
16
|
+
"skill": {
|
|
17
|
+
"name": "star-dom-sdk",
|
|
18
|
+
"description": "Star DOM SDK for canvas games and UI. Use when working with canvas rendering, game loops, sprites, input handling, UI overlays, or the game() initialization function."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.mjs",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./*": {
|
|
28
|
+
"types": "./dist/*.d.ts",
|
|
29
|
+
"import": "./dist/*.mjs",
|
|
30
|
+
"require": "./dist/*.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"module": "./dist/index.mjs",
|
|
34
|
+
"main": "./dist/index.cjs",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"clean": "rm -rf dist",
|
|
42
|
+
"prepublishOnly": "node ../../apps/web/scripts/process-sdks.js --package star-canvas --public-only && yarn build"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"dom",
|
|
46
|
+
"canvas",
|
|
47
|
+
"game-loop",
|
|
48
|
+
"mobile",
|
|
49
|
+
"responsive",
|
|
50
|
+
"dpr",
|
|
51
|
+
"retina",
|
|
52
|
+
"games",
|
|
53
|
+
"web",
|
|
54
|
+
"nextjs",
|
|
55
|
+
"vite",
|
|
56
|
+
"typescript"
|
|
57
|
+
],
|
|
58
|
+
"dependencies": {},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"tsup": "^8.0.2",
|
|
61
|
+
"typescript": "^5.4.5"
|
|
62
|
+
}
|
|
63
|
+
}
|