@ksteinstudio/game-controller 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/index.d.mts +272 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +1116 -0
- package/dist/index.mjs +1060 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MessageType: () => MessageType,
|
|
24
|
+
angleFromVector: () => angleFromVector,
|
|
25
|
+
applyDeadzone: () => applyDeadzone,
|
|
26
|
+
applyDeadzoneToVector: () => applyDeadzoneToVector,
|
|
27
|
+
calculateCanvasDimensions: () => calculateCanvasDimensions,
|
|
28
|
+
clamp: () => clamp,
|
|
29
|
+
clampPercentage: () => clampPercentage,
|
|
30
|
+
convertPercentagePositionToPixel: () => convertPercentagePositionToPixel,
|
|
31
|
+
convertPixelPositionToPercentage: () => convertPixelPositionToPercentage,
|
|
32
|
+
createButtonElement: () => createButtonElement,
|
|
33
|
+
createControllerSDK: () => createControllerSDK,
|
|
34
|
+
createDpadElement: () => createDpadElement,
|
|
35
|
+
createIframeBridge: () => createIframeBridge,
|
|
36
|
+
createJoystickElement: () => createJoystickElement,
|
|
37
|
+
createParentBridge: () => createParentBridge,
|
|
38
|
+
destroyRenderer: () => destroyRenderer,
|
|
39
|
+
distance: () => distance,
|
|
40
|
+
findAlignmentGuides: () => findAlignmentGuides,
|
|
41
|
+
generateGridLines: () => generateGridLines,
|
|
42
|
+
initializeRenderer: () => initializeRenderer,
|
|
43
|
+
normalizeVector: () => normalizeVector,
|
|
44
|
+
parseAspectRatio: () => parseAspectRatio,
|
|
45
|
+
percentageToPixel: () => percentageToPixel,
|
|
46
|
+
pixelToPercentage: () => pixelToPercentage,
|
|
47
|
+
renderControllerFromConfig: () => renderControllerFromConfig,
|
|
48
|
+
snapPositionToGrid: () => snapPositionToGrid,
|
|
49
|
+
snapToGrid: () => snapToGrid,
|
|
50
|
+
updateButtonElement: () => updateButtonElement,
|
|
51
|
+
updateDpadElement: () => updateDpadElement,
|
|
52
|
+
updateJoystickElement: () => updateJoystickElement
|
|
53
|
+
});
|
|
54
|
+
module.exports = __toCommonJS(index_exports);
|
|
55
|
+
|
|
56
|
+
// src/types/events.ts
|
|
57
|
+
var MessageType = /* @__PURE__ */ ((MessageType2) => {
|
|
58
|
+
MessageType2["CONFIG_LOAD"] = "CONFIG_LOAD";
|
|
59
|
+
MessageType2["INPUT_START"] = "INPUT_START";
|
|
60
|
+
MessageType2["INPUT_END"] = "INPUT_END";
|
|
61
|
+
MessageType2["JOYSTICK_MOVE"] = "JOYSTICK_MOVE";
|
|
62
|
+
MessageType2["DPAD_PRESS"] = "DPAD_PRESS";
|
|
63
|
+
MessageType2["DPAD_RELEASE"] = "DPAD_RELEASE";
|
|
64
|
+
MessageType2["SLIDER_CHANGE"] = "SLIDER_CHANGE";
|
|
65
|
+
MessageType2["RENDERER_READY"] = "RENDERER_READY";
|
|
66
|
+
MessageType2["CONFIG_UPDATE"] = "CONFIG_UPDATE";
|
|
67
|
+
return MessageType2;
|
|
68
|
+
})(MessageType || {});
|
|
69
|
+
|
|
70
|
+
// src/bridge/communication-bridge.ts
|
|
71
|
+
function createParentBridge(iframe) {
|
|
72
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
73
|
+
const inputListeners = /* @__PURE__ */ new Set();
|
|
74
|
+
function handleMessage(event) {
|
|
75
|
+
const message = event.data;
|
|
76
|
+
if (!message || !message.type) return;
|
|
77
|
+
listeners.forEach((handler) => handler(message));
|
|
78
|
+
if (isInputEvent(message)) {
|
|
79
|
+
inputListeners.forEach((handler) => handler(message));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
window.addEventListener("message", handleMessage);
|
|
83
|
+
return {
|
|
84
|
+
sendConfig(config) {
|
|
85
|
+
iframe.contentWindow?.postMessage(
|
|
86
|
+
{ type: "CONFIG_LOAD" /* CONFIG_LOAD */, payload: config },
|
|
87
|
+
"*"
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
updateConfig(config) {
|
|
91
|
+
iframe.contentWindow?.postMessage(
|
|
92
|
+
{ type: "CONFIG_UPDATE" /* CONFIG_UPDATE */, payload: config },
|
|
93
|
+
"*"
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
onMessage(handler) {
|
|
97
|
+
listeners.add(handler);
|
|
98
|
+
return () => listeners.delete(handler);
|
|
99
|
+
},
|
|
100
|
+
onInput(handler) {
|
|
101
|
+
inputListeners.add(handler);
|
|
102
|
+
return () => inputListeners.delete(handler);
|
|
103
|
+
},
|
|
104
|
+
destroy() {
|
|
105
|
+
window.removeEventListener("message", handleMessage);
|
|
106
|
+
listeners.clear();
|
|
107
|
+
inputListeners.clear();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function createIframeBridge() {
|
|
112
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
113
|
+
function handleMessage(event) {
|
|
114
|
+
const message = event.data;
|
|
115
|
+
if (!message || !message.type) return;
|
|
116
|
+
listeners.forEach((handler) => handler(message));
|
|
117
|
+
}
|
|
118
|
+
window.addEventListener("message", handleMessage);
|
|
119
|
+
return {
|
|
120
|
+
emitControllerEvent(message) {
|
|
121
|
+
window.parent.postMessage(message, "*");
|
|
122
|
+
},
|
|
123
|
+
emitInputStart(elementId, actionKey) {
|
|
124
|
+
window.parent.postMessage(
|
|
125
|
+
{
|
|
126
|
+
type: "INPUT_START" /* INPUT_START */,
|
|
127
|
+
payload: { elementId, actionKey, timestamp: Date.now() }
|
|
128
|
+
},
|
|
129
|
+
"*"
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
emitInputEnd(elementId, actionKey) {
|
|
133
|
+
window.parent.postMessage(
|
|
134
|
+
{
|
|
135
|
+
type: "INPUT_END" /* INPUT_END */,
|
|
136
|
+
payload: { elementId, actionKey, timestamp: Date.now() }
|
|
137
|
+
},
|
|
138
|
+
"*"
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
emitJoystickMove(elementId, x, y, angle, magnitude) {
|
|
142
|
+
window.parent.postMessage(
|
|
143
|
+
{
|
|
144
|
+
type: "JOYSTICK_MOVE" /* JOYSTICK_MOVE */,
|
|
145
|
+
payload: { elementId, vector: { x, y }, angle, magnitude, timestamp: Date.now() }
|
|
146
|
+
},
|
|
147
|
+
"*"
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
emitDpadPress(elementId, direction, actionKey) {
|
|
151
|
+
window.parent.postMessage(
|
|
152
|
+
{
|
|
153
|
+
type: "DPAD_PRESS" /* DPAD_PRESS */,
|
|
154
|
+
payload: { elementId, direction, actionKey, timestamp: Date.now() }
|
|
155
|
+
},
|
|
156
|
+
"*"
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
emitDpadRelease(elementId, direction, actionKey) {
|
|
160
|
+
window.parent.postMessage(
|
|
161
|
+
{
|
|
162
|
+
type: "DPAD_RELEASE" /* DPAD_RELEASE */,
|
|
163
|
+
payload: { elementId, direction, actionKey, timestamp: Date.now() }
|
|
164
|
+
},
|
|
165
|
+
"*"
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
emitSliderChange(elementId, actionKey, value) {
|
|
169
|
+
window.parent.postMessage(
|
|
170
|
+
{
|
|
171
|
+
type: "SLIDER_CHANGE" /* SLIDER_CHANGE */,
|
|
172
|
+
payload: { elementId, actionKey, value, timestamp: Date.now() }
|
|
173
|
+
},
|
|
174
|
+
"*"
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
signalReady() {
|
|
178
|
+
window.parent.postMessage(
|
|
179
|
+
{ type: "RENDERER_READY" /* RENDERER_READY */ },
|
|
180
|
+
"*"
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
onMessage(handler) {
|
|
184
|
+
listeners.add(handler);
|
|
185
|
+
return () => listeners.delete(handler);
|
|
186
|
+
},
|
|
187
|
+
destroy() {
|
|
188
|
+
window.removeEventListener("message", handleMessage);
|
|
189
|
+
listeners.clear();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function isInputEvent(message) {
|
|
194
|
+
return message.type === "INPUT_START" /* INPUT_START */ || message.type === "INPUT_END" /* INPUT_END */ || message.type === "JOYSTICK_MOVE" /* JOYSTICK_MOVE */ || message.type === "DPAD_PRESS" /* DPAD_PRESS */ || message.type === "DPAD_RELEASE" /* DPAD_RELEASE */ || message.type === "SLIDER_CHANGE" /* SLIDER_CHANGE */;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/math/coordinate-converter.ts
|
|
198
|
+
function snapToGrid(value, gridDensity) {
|
|
199
|
+
const step = 100 / gridDensity;
|
|
200
|
+
return Math.round(value / step) * step;
|
|
201
|
+
}
|
|
202
|
+
function snapPositionToGrid(position, gridDensity) {
|
|
203
|
+
return {
|
|
204
|
+
x: snapToGrid(position.x, gridDensity),
|
|
205
|
+
y: snapToGrid(position.y, gridDensity)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function pixelToPercentage(pixelValue, containerSize) {
|
|
209
|
+
return clampPercentage(pixelValue / containerSize * 100);
|
|
210
|
+
}
|
|
211
|
+
function percentageToPixel(percentageValue, containerSize) {
|
|
212
|
+
return percentageValue / 100 * containerSize;
|
|
213
|
+
}
|
|
214
|
+
function convertPixelPositionToPercentage(pixelX, pixelY, containerWidth, containerHeight) {
|
|
215
|
+
return {
|
|
216
|
+
x: pixelToPercentage(pixelX, containerWidth),
|
|
217
|
+
y: pixelToPercentage(pixelY, containerHeight)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function convertPercentagePositionToPixel(position, containerWidth, containerHeight) {
|
|
221
|
+
return {
|
|
222
|
+
x: percentageToPixel(position.x, containerWidth),
|
|
223
|
+
y: percentageToPixel(position.y, containerHeight)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function clampPercentage(value) {
|
|
227
|
+
return Math.max(0, Math.min(100, value));
|
|
228
|
+
}
|
|
229
|
+
function clamp(value, min, max) {
|
|
230
|
+
return Math.max(min, Math.min(max, value));
|
|
231
|
+
}
|
|
232
|
+
function distance(a, b) {
|
|
233
|
+
const dx = a.x - b.x;
|
|
234
|
+
const dy = a.y - b.y;
|
|
235
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
236
|
+
}
|
|
237
|
+
function normalizeVector(x, y) {
|
|
238
|
+
const magnitude = Math.sqrt(x * x + y * y);
|
|
239
|
+
if (magnitude === 0) return { x: 0, y: 0, magnitude: 0 };
|
|
240
|
+
return { x: x / magnitude, y: y / magnitude, magnitude };
|
|
241
|
+
}
|
|
242
|
+
function angleFromVector(x, y) {
|
|
243
|
+
return Math.atan2(y, x);
|
|
244
|
+
}
|
|
245
|
+
function applyDeadzone(value, deadzone) {
|
|
246
|
+
if (Math.abs(value) < deadzone) return 0;
|
|
247
|
+
const sign = value > 0 ? 1 : -1;
|
|
248
|
+
return sign * ((Math.abs(value) - deadzone) / (1 - deadzone));
|
|
249
|
+
}
|
|
250
|
+
function applyDeadzoneToVector(x, y, deadzone) {
|
|
251
|
+
const magnitude = Math.sqrt(x * x + y * y);
|
|
252
|
+
if (magnitude < deadzone) return { x: 0, y: 0 };
|
|
253
|
+
const scale = (magnitude - deadzone) / (1 - deadzone) / magnitude;
|
|
254
|
+
return { x: x * scale, y: y * scale };
|
|
255
|
+
}
|
|
256
|
+
function parseAspectRatio(aspectRatio) {
|
|
257
|
+
const [width, height] = aspectRatio.split(":").map(Number);
|
|
258
|
+
return { width: width || 16, height: height || 9 };
|
|
259
|
+
}
|
|
260
|
+
function calculateCanvasDimensions(containerWidth, containerHeight, aspectRatio) {
|
|
261
|
+
const ratio = parseAspectRatio(aspectRatio);
|
|
262
|
+
const targetRatio = ratio.width / ratio.height;
|
|
263
|
+
const containerRatio = containerWidth / containerHeight;
|
|
264
|
+
let width;
|
|
265
|
+
let height;
|
|
266
|
+
if (containerRatio > targetRatio) {
|
|
267
|
+
height = containerHeight;
|
|
268
|
+
width = height * targetRatio;
|
|
269
|
+
} else {
|
|
270
|
+
width = containerWidth;
|
|
271
|
+
height = width / targetRatio;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
width: Math.floor(width),
|
|
275
|
+
height: Math.floor(height),
|
|
276
|
+
offsetX: Math.floor((containerWidth - width) / 2),
|
|
277
|
+
offsetY: Math.floor((containerHeight - height) / 2)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/math/alignment-engine.ts
|
|
282
|
+
var SNAP_THRESHOLD = 2;
|
|
283
|
+
var CENTER_POSITION = 50;
|
|
284
|
+
function findAlignmentGuides(draggedElement, dragPosition, otherElements, threshold = SNAP_THRESHOLD) {
|
|
285
|
+
const guides = [];
|
|
286
|
+
const snappedPosition = { ...dragPosition };
|
|
287
|
+
for (const element of otherElements) {
|
|
288
|
+
if (element.id === draggedElement.id) continue;
|
|
289
|
+
if (Math.abs(dragPosition.x - element.position.x) <= threshold) {
|
|
290
|
+
guides.push({
|
|
291
|
+
axis: "vertical",
|
|
292
|
+
position: element.position.x,
|
|
293
|
+
sourceElementId: draggedElement.id,
|
|
294
|
+
targetElementId: element.id
|
|
295
|
+
});
|
|
296
|
+
snappedPosition.x = element.position.x;
|
|
297
|
+
}
|
|
298
|
+
if (Math.abs(dragPosition.y - element.position.y) <= threshold) {
|
|
299
|
+
guides.push({
|
|
300
|
+
axis: "horizontal",
|
|
301
|
+
position: element.position.y,
|
|
302
|
+
sourceElementId: draggedElement.id,
|
|
303
|
+
targetElementId: element.id
|
|
304
|
+
});
|
|
305
|
+
snappedPosition.y = element.position.y;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const centerGuides = findCenterSnap(draggedElement, dragPosition, threshold);
|
|
309
|
+
guides.push(...centerGuides.guides);
|
|
310
|
+
if (centerGuides.snappedX !== null) snappedPosition.x = centerGuides.snappedX;
|
|
311
|
+
if (centerGuides.snappedY !== null) snappedPosition.y = centerGuides.snappedY;
|
|
312
|
+
return { position: snappedPosition, guides };
|
|
313
|
+
}
|
|
314
|
+
function findCenterSnap(element, position, threshold) {
|
|
315
|
+
const guides = [];
|
|
316
|
+
let snappedX = null;
|
|
317
|
+
let snappedY = null;
|
|
318
|
+
if (Math.abs(position.x - CENTER_POSITION) <= threshold) {
|
|
319
|
+
guides.push({
|
|
320
|
+
axis: "vertical",
|
|
321
|
+
position: CENTER_POSITION,
|
|
322
|
+
sourceElementId: element.id,
|
|
323
|
+
targetElementId: "canvas-center"
|
|
324
|
+
});
|
|
325
|
+
snappedX = CENTER_POSITION;
|
|
326
|
+
}
|
|
327
|
+
if (Math.abs(position.y - CENTER_POSITION) <= threshold) {
|
|
328
|
+
guides.push({
|
|
329
|
+
axis: "horizontal",
|
|
330
|
+
position: CENTER_POSITION,
|
|
331
|
+
sourceElementId: element.id,
|
|
332
|
+
targetElementId: "canvas-center"
|
|
333
|
+
});
|
|
334
|
+
snappedY = CENTER_POSITION;
|
|
335
|
+
}
|
|
336
|
+
return { guides, snappedX, snappedY };
|
|
337
|
+
}
|
|
338
|
+
function generateGridLines(gridDensity) {
|
|
339
|
+
const step = 100 / gridDensity;
|
|
340
|
+
const lines = [];
|
|
341
|
+
for (let i = 0; i <= gridDensity; i++) {
|
|
342
|
+
lines.push(i * step);
|
|
343
|
+
}
|
|
344
|
+
return lines;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/renderer/button-renderer.ts
|
|
348
|
+
function createButtonElement(context) {
|
|
349
|
+
const { element, canvasWidth, canvasHeight, onInputStart, onInputEnd } = context;
|
|
350
|
+
const container = document.createElement("div");
|
|
351
|
+
container.dataset.elementId = element.id;
|
|
352
|
+
container.dataset.elementType = "button";
|
|
353
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
354
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
355
|
+
const pixelSize = percentageToPixel(element.size, Math.min(canvasWidth, canvasHeight));
|
|
356
|
+
applyButtonStyles(container, element, pixelX, pixelY, pixelSize);
|
|
357
|
+
attachButtonLabel(container, element);
|
|
358
|
+
attachButtonInteraction(container, element, onInputStart, onInputEnd);
|
|
359
|
+
return container;
|
|
360
|
+
}
|
|
361
|
+
function applyButtonStyles(container, element, pixelX, pixelY, pixelSize) {
|
|
362
|
+
const halfSize = pixelSize / 2;
|
|
363
|
+
Object.assign(container.style, {
|
|
364
|
+
position: "absolute",
|
|
365
|
+
left: `${pixelX - halfSize}px`,
|
|
366
|
+
top: `${pixelY - halfSize}px`,
|
|
367
|
+
width: `${pixelSize}px`,
|
|
368
|
+
height: `${pixelSize}px`,
|
|
369
|
+
borderRadius: element.shape === "circle" ? "50%" : "15%",
|
|
370
|
+
backgroundColor: element.style?.color || "#444",
|
|
371
|
+
opacity: String(element.style?.opacity ?? 0.8),
|
|
372
|
+
zIndex: String(element.zIndex || 1),
|
|
373
|
+
display: "flex",
|
|
374
|
+
alignItems: "center",
|
|
375
|
+
justifyContent: "center",
|
|
376
|
+
userSelect: "none",
|
|
377
|
+
touchAction: "none",
|
|
378
|
+
cursor: "pointer",
|
|
379
|
+
border: `2px solid ${element.style?.borderColor || "rgba(255,255,255,0.3)"}`,
|
|
380
|
+
boxSizing: "border-box",
|
|
381
|
+
transition: "transform 0.05s ease, opacity 0.05s ease"
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function attachButtonLabel(container, element) {
|
|
385
|
+
if (!element.label) return;
|
|
386
|
+
const label = document.createElement("span");
|
|
387
|
+
label.textContent = element.label;
|
|
388
|
+
Object.assign(label.style, {
|
|
389
|
+
color: "#fff",
|
|
390
|
+
fontSize: `${element.style?.fontSize || 14}px`,
|
|
391
|
+
fontFamily: element.style?.fontFamily || "sans-serif",
|
|
392
|
+
fontWeight: "bold",
|
|
393
|
+
pointerEvents: "none",
|
|
394
|
+
userSelect: "none"
|
|
395
|
+
});
|
|
396
|
+
container.appendChild(label);
|
|
397
|
+
}
|
|
398
|
+
function attachButtonInteraction(container, element, onInputStart, onInputEnd) {
|
|
399
|
+
const activePointers = /* @__PURE__ */ new Set();
|
|
400
|
+
container.addEventListener("pointerdown", (event) => {
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
activePointers.add(event.pointerId);
|
|
403
|
+
container.setPointerCapture(event.pointerId);
|
|
404
|
+
applyPressedState(container);
|
|
405
|
+
onInputStart(element.id, element.actionKey);
|
|
406
|
+
});
|
|
407
|
+
container.addEventListener("pointerup", (event) => {
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
activePointers.delete(event.pointerId);
|
|
410
|
+
container.releasePointerCapture(event.pointerId);
|
|
411
|
+
if (activePointers.size === 0) {
|
|
412
|
+
applyReleasedState(container);
|
|
413
|
+
onInputEnd(element.id, element.actionKey);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
container.addEventListener("pointercancel", (event) => {
|
|
417
|
+
activePointers.delete(event.pointerId);
|
|
418
|
+
if (activePointers.size === 0) {
|
|
419
|
+
applyReleasedState(container);
|
|
420
|
+
onInputEnd(element.id, element.actionKey);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
container.addEventListener("pointerleave", (event) => {
|
|
424
|
+
if (activePointers.has(event.pointerId)) {
|
|
425
|
+
activePointers.delete(event.pointerId);
|
|
426
|
+
if (activePointers.size === 0) {
|
|
427
|
+
applyReleasedState(container);
|
|
428
|
+
onInputEnd(element.id, element.actionKey);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function applyPressedState(element) {
|
|
434
|
+
element.style.transform = "scale(0.9)";
|
|
435
|
+
element.style.opacity = "1";
|
|
436
|
+
}
|
|
437
|
+
function applyReleasedState(element) {
|
|
438
|
+
element.style.transform = "scale(1)";
|
|
439
|
+
element.style.opacity = String(0.8);
|
|
440
|
+
}
|
|
441
|
+
function updateButtonElement(container, element, canvasWidth, canvasHeight) {
|
|
442
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
443
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
444
|
+
const pixelSize = percentageToPixel(element.size, Math.min(canvasWidth, canvasHeight));
|
|
445
|
+
const halfSize = pixelSize / 2;
|
|
446
|
+
container.style.left = `${pixelX - halfSize}px`;
|
|
447
|
+
container.style.top = `${pixelY - halfSize}px`;
|
|
448
|
+
container.style.width = `${pixelSize}px`;
|
|
449
|
+
container.style.height = `${pixelSize}px`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/renderer/joystick-renderer.ts
|
|
453
|
+
function createJoystickElement(context) {
|
|
454
|
+
const { element, canvasWidth, canvasHeight, onJoystickMove } = context;
|
|
455
|
+
const container = document.createElement("div");
|
|
456
|
+
container.dataset.elementId = element.id;
|
|
457
|
+
container.dataset.elementType = "joystick";
|
|
458
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
459
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
460
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
461
|
+
const outerRadius = percentageToPixel(element.radius, referenceDimension);
|
|
462
|
+
const innerRadius = percentageToPixel(element.innerStickSize, referenceDimension);
|
|
463
|
+
applyJoystickBaseStyles(container, pixelX, pixelY, outerRadius, element);
|
|
464
|
+
const stick = createStickElement(innerRadius, element);
|
|
465
|
+
container.appendChild(stick);
|
|
466
|
+
const state = {
|
|
467
|
+
isActive: false,
|
|
468
|
+
activePointerId: null,
|
|
469
|
+
originX: pixelX,
|
|
470
|
+
originY: pixelY,
|
|
471
|
+
stickOffsetX: 0,
|
|
472
|
+
stickOffsetY: 0
|
|
473
|
+
};
|
|
474
|
+
attachJoystickInteraction(container, stick, element, outerRadius, innerRadius, state, onJoystickMove);
|
|
475
|
+
return container;
|
|
476
|
+
}
|
|
477
|
+
function applyJoystickBaseStyles(container, pixelX, pixelY, outerRadius, element) {
|
|
478
|
+
const diameter = outerRadius * 2;
|
|
479
|
+
Object.assign(container.style, {
|
|
480
|
+
position: "absolute",
|
|
481
|
+
left: `${pixelX - outerRadius}px`,
|
|
482
|
+
top: `${pixelY - outerRadius}px`,
|
|
483
|
+
width: `${diameter}px`,
|
|
484
|
+
height: `${diameter}px`,
|
|
485
|
+
borderRadius: "50%",
|
|
486
|
+
backgroundColor: element.style?.color || "rgba(80, 80, 80, 0.5)",
|
|
487
|
+
border: `2px solid ${element.style?.borderColor || "rgba(255, 255, 255, 0.2)"}`,
|
|
488
|
+
opacity: String(element.style?.opacity ?? 0.7),
|
|
489
|
+
zIndex: String(element.zIndex || 1),
|
|
490
|
+
touchAction: "none",
|
|
491
|
+
userSelect: "none",
|
|
492
|
+
boxSizing: "border-box"
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function createStickElement(innerRadius, element) {
|
|
496
|
+
const stick = document.createElement("div");
|
|
497
|
+
const stickDiameter = innerRadius * 2;
|
|
498
|
+
Object.assign(stick.style, {
|
|
499
|
+
position: "absolute",
|
|
500
|
+
width: `${stickDiameter}px`,
|
|
501
|
+
height: `${stickDiameter}px`,
|
|
502
|
+
borderRadius: "50%",
|
|
503
|
+
backgroundColor: element.style?.color ? lightenColor(element.style.color) : "rgba(200, 200, 200, 0.8)",
|
|
504
|
+
left: "50%",
|
|
505
|
+
top: "50%",
|
|
506
|
+
transform: "translate(-50%, -50%)",
|
|
507
|
+
pointerEvents: "none",
|
|
508
|
+
willChange: "transform",
|
|
509
|
+
transition: "none"
|
|
510
|
+
});
|
|
511
|
+
return stick;
|
|
512
|
+
}
|
|
513
|
+
function attachJoystickInteraction(container, stick, element, outerRadius, _innerRadius, state, onJoystickMove) {
|
|
514
|
+
const maxDistance = outerRadius;
|
|
515
|
+
container.addEventListener("pointerdown", (event) => {
|
|
516
|
+
event.preventDefault();
|
|
517
|
+
if (state.isActive) return;
|
|
518
|
+
state.isActive = true;
|
|
519
|
+
state.activePointerId = event.pointerId;
|
|
520
|
+
container.setPointerCapture(event.pointerId);
|
|
521
|
+
if (element.mode === "floating") {
|
|
522
|
+
const rect = container.parentElement?.getBoundingClientRect();
|
|
523
|
+
if (rect) {
|
|
524
|
+
state.originX = event.clientX - rect.left;
|
|
525
|
+
state.originY = event.clientY - rect.top;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
handleJoystickPointerMove(event, container, stick, element, maxDistance, state, onJoystickMove);
|
|
529
|
+
});
|
|
530
|
+
container.addEventListener("pointermove", (event) => {
|
|
531
|
+
if (!state.isActive || event.pointerId !== state.activePointerId) return;
|
|
532
|
+
event.preventDefault();
|
|
533
|
+
handleJoystickPointerMove(event, container, stick, element, maxDistance, state, onJoystickMove);
|
|
534
|
+
});
|
|
535
|
+
const releaseHandler = (event) => {
|
|
536
|
+
if (event.pointerId !== state.activePointerId) return;
|
|
537
|
+
state.isActive = false;
|
|
538
|
+
state.activePointerId = null;
|
|
539
|
+
state.stickOffsetX = 0;
|
|
540
|
+
state.stickOffsetY = 0;
|
|
541
|
+
stick.style.transform = "translate(-50%, -50%)";
|
|
542
|
+
onJoystickMove(element.id, { x: 0, y: 0 }, 0, 0);
|
|
543
|
+
};
|
|
544
|
+
container.addEventListener("pointerup", releaseHandler);
|
|
545
|
+
container.addEventListener("pointercancel", releaseHandler);
|
|
546
|
+
}
|
|
547
|
+
function handleJoystickPointerMove(event, container, stick, element, maxDistance, state, onJoystickMove) {
|
|
548
|
+
const rect = container.getBoundingClientRect();
|
|
549
|
+
const centerX = rect.width / 2;
|
|
550
|
+
const centerY = rect.height / 2;
|
|
551
|
+
const pointerX = event.clientX - rect.left;
|
|
552
|
+
const pointerY = event.clientY - rect.top;
|
|
553
|
+
let deltaX = pointerX - centerX;
|
|
554
|
+
let deltaY = pointerY - centerY;
|
|
555
|
+
const distance2 = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
556
|
+
if (distance2 > maxDistance) {
|
|
557
|
+
deltaX = deltaX / distance2 * maxDistance;
|
|
558
|
+
deltaY = deltaY / distance2 * maxDistance;
|
|
559
|
+
}
|
|
560
|
+
state.stickOffsetX = deltaX;
|
|
561
|
+
state.stickOffsetY = deltaY;
|
|
562
|
+
stick.style.transform = `translate(calc(-50% + ${deltaX}px), calc(-50% + ${deltaY}px))`;
|
|
563
|
+
let normalizedX = deltaX / maxDistance;
|
|
564
|
+
let normalizedY = deltaY / maxDistance;
|
|
565
|
+
normalizedX = clamp(normalizedX, -1, 1);
|
|
566
|
+
normalizedY = clamp(normalizedY, -1, 1);
|
|
567
|
+
const deadzoned = applyDeadzoneToVector(normalizedX, normalizedY, element.deadzone || 0);
|
|
568
|
+
const angle = angleFromVector(deadzoned.x, deadzoned.y);
|
|
569
|
+
const magnitude = Math.sqrt(deadzoned.x * deadzoned.x + deadzoned.y * deadzoned.y);
|
|
570
|
+
onJoystickMove(element.id, deadzoned, angle, Math.min(magnitude, 1));
|
|
571
|
+
}
|
|
572
|
+
function lightenColor(color) {
|
|
573
|
+
if (color.startsWith("rgba")) {
|
|
574
|
+
return color.replace(/[\d.]+\)$/, "0.8)");
|
|
575
|
+
}
|
|
576
|
+
return color + "88";
|
|
577
|
+
}
|
|
578
|
+
function updateJoystickElement(container, element, canvasWidth, canvasHeight) {
|
|
579
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
580
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
581
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
582
|
+
const outerRadius = percentageToPixel(element.radius, referenceDimension);
|
|
583
|
+
container.style.left = `${pixelX - outerRadius}px`;
|
|
584
|
+
container.style.top = `${pixelY - outerRadius}px`;
|
|
585
|
+
container.style.width = `${outerRadius * 2}px`;
|
|
586
|
+
container.style.height = `${outerRadius * 2}px`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/renderer/dpad-renderer.ts
|
|
590
|
+
var DEFAULT_DPAD_KEYS = { up: "DPAD_UP", down: "DPAD_DOWN", left: "DPAD_LEFT", right: "DPAD_RIGHT" };
|
|
591
|
+
function createDpadElement(context) {
|
|
592
|
+
const { element, canvasWidth, canvasHeight, onDpadPress, onDpadRelease } = context;
|
|
593
|
+
const container = document.createElement("div");
|
|
594
|
+
container.dataset.elementId = element.id;
|
|
595
|
+
container.dataset.elementType = "dpad";
|
|
596
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
597
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
598
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
599
|
+
const pixelSize = percentageToPixel(element.size, referenceDimension);
|
|
600
|
+
applyDpadContainerStyles(container, pixelX, pixelY, pixelSize, element);
|
|
601
|
+
const actionKeys = element.actionKeys || DEFAULT_DPAD_KEYS;
|
|
602
|
+
const directions = [
|
|
603
|
+
{ direction: "up", gridArea: "1 / 2 / 2 / 3" },
|
|
604
|
+
{ direction: "left", gridArea: "2 / 1 / 3 / 2" },
|
|
605
|
+
{ direction: "right", gridArea: "2 / 3 / 3 / 4" },
|
|
606
|
+
{ direction: "down", gridArea: "3 / 2 / 4 / 3" }
|
|
607
|
+
];
|
|
608
|
+
const centerBlock = document.createElement("div");
|
|
609
|
+
Object.assign(centerBlock.style, {
|
|
610
|
+
gridArea: "2 / 2 / 3 / 3",
|
|
611
|
+
backgroundColor: element.style?.color || "#333",
|
|
612
|
+
borderRadius: "2px"
|
|
613
|
+
});
|
|
614
|
+
container.appendChild(centerBlock);
|
|
615
|
+
for (const { direction, gridArea } of directions) {
|
|
616
|
+
const dirButton = createDirectionButton(direction, gridArea, element, actionKeys);
|
|
617
|
+
attachDpadInteraction(dirButton, element.id, direction, actionKeys[direction], onDpadPress, onDpadRelease);
|
|
618
|
+
container.appendChild(dirButton);
|
|
619
|
+
}
|
|
620
|
+
return container;
|
|
621
|
+
}
|
|
622
|
+
function applyDpadContainerStyles(container, pixelX, pixelY, pixelSize, element) {
|
|
623
|
+
const halfSize = pixelSize / 2;
|
|
624
|
+
Object.assign(container.style, {
|
|
625
|
+
position: "absolute",
|
|
626
|
+
left: `${pixelX - halfSize}px`,
|
|
627
|
+
top: `${pixelY - halfSize}px`,
|
|
628
|
+
width: `${pixelSize}px`,
|
|
629
|
+
height: `${pixelSize}px`,
|
|
630
|
+
display: "grid",
|
|
631
|
+
gridTemplateColumns: "1fr 1fr 1fr",
|
|
632
|
+
gridTemplateRows: "1fr 1fr 1fr",
|
|
633
|
+
gap: "2px",
|
|
634
|
+
zIndex: String(element.zIndex || 1),
|
|
635
|
+
touchAction: "none",
|
|
636
|
+
userSelect: "none"
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function createDirectionButton(direction, gridArea, element, _actionKeys) {
|
|
640
|
+
const button = document.createElement("div");
|
|
641
|
+
button.dataset.direction = direction;
|
|
642
|
+
const arrowMap = {
|
|
643
|
+
up: "\u25B2",
|
|
644
|
+
down: "\u25BC",
|
|
645
|
+
left: "\u25C0",
|
|
646
|
+
right: "\u25B6"
|
|
647
|
+
};
|
|
648
|
+
Object.assign(button.style, {
|
|
649
|
+
gridArea,
|
|
650
|
+
backgroundColor: element.style?.color || "#444",
|
|
651
|
+
borderRadius: "4px",
|
|
652
|
+
display: "flex",
|
|
653
|
+
alignItems: "center",
|
|
654
|
+
justifyContent: "center",
|
|
655
|
+
color: "rgba(255, 255, 255, 0.7)",
|
|
656
|
+
fontSize: "12px",
|
|
657
|
+
opacity: String(element.style?.opacity ?? 0.8),
|
|
658
|
+
cursor: "pointer",
|
|
659
|
+
touchAction: "none",
|
|
660
|
+
userSelect: "none",
|
|
661
|
+
transition: "transform 0.05s ease, opacity 0.05s ease"
|
|
662
|
+
});
|
|
663
|
+
button.textContent = arrowMap[direction];
|
|
664
|
+
return button;
|
|
665
|
+
}
|
|
666
|
+
function attachDpadInteraction(button, elementId, direction, actionKey, onDpadPress, onDpadRelease) {
|
|
667
|
+
const activePointers = /* @__PURE__ */ new Set();
|
|
668
|
+
button.addEventListener("pointerdown", (event) => {
|
|
669
|
+
event.preventDefault();
|
|
670
|
+
event.stopPropagation();
|
|
671
|
+
activePointers.add(event.pointerId);
|
|
672
|
+
button.setPointerCapture(event.pointerId);
|
|
673
|
+
button.style.transform = "scale(0.85)";
|
|
674
|
+
button.style.opacity = "1";
|
|
675
|
+
onDpadPress(elementId, direction, actionKey);
|
|
676
|
+
});
|
|
677
|
+
const releaseHandler = (event) => {
|
|
678
|
+
event.preventDefault();
|
|
679
|
+
activePointers.delete(event.pointerId);
|
|
680
|
+
if (activePointers.size === 0) {
|
|
681
|
+
button.style.transform = "scale(1)";
|
|
682
|
+
button.style.opacity = String(0.8);
|
|
683
|
+
onDpadRelease(elementId, direction, actionKey);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
button.addEventListener("pointerup", releaseHandler);
|
|
687
|
+
button.addEventListener("pointercancel", releaseHandler);
|
|
688
|
+
}
|
|
689
|
+
function updateDpadElement(container, element, canvasWidth, canvasHeight) {
|
|
690
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
691
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
692
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
693
|
+
const pixelSize = percentageToPixel(element.size, referenceDimension);
|
|
694
|
+
const halfSize = pixelSize / 2;
|
|
695
|
+
container.style.left = `${pixelX - halfSize}px`;
|
|
696
|
+
container.style.top = `${pixelY - halfSize}px`;
|
|
697
|
+
container.style.width = `${pixelSize}px`;
|
|
698
|
+
container.style.height = `${pixelSize}px`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/renderer/rendering-engine.ts
|
|
702
|
+
function initializeRenderer(rootElement) {
|
|
703
|
+
const bridge = createIframeBridge();
|
|
704
|
+
const state = {
|
|
705
|
+
config: null,
|
|
706
|
+
canvas: null,
|
|
707
|
+
elementMap: /* @__PURE__ */ new Map(),
|
|
708
|
+
bridge,
|
|
709
|
+
resizeObserver: null
|
|
710
|
+
};
|
|
711
|
+
bridge.onMessage((message) => {
|
|
712
|
+
if (message.type === "CONFIG_LOAD" /* CONFIG_LOAD */ || message.type === "CONFIG_UPDATE" /* CONFIG_UPDATE */) {
|
|
713
|
+
state.config = message.payload;
|
|
714
|
+
renderController(state, rootElement);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
bridge.signalReady();
|
|
718
|
+
return state;
|
|
719
|
+
}
|
|
720
|
+
function renderControllerFromConfig(rootElement, config) {
|
|
721
|
+
const bridge = createIframeBridge();
|
|
722
|
+
const state = {
|
|
723
|
+
config,
|
|
724
|
+
canvas: null,
|
|
725
|
+
elementMap: /* @__PURE__ */ new Map(),
|
|
726
|
+
bridge,
|
|
727
|
+
resizeObserver: null
|
|
728
|
+
};
|
|
729
|
+
renderController(state, rootElement);
|
|
730
|
+
return state;
|
|
731
|
+
}
|
|
732
|
+
function renderController(state, rootElement) {
|
|
733
|
+
if (!state.config) return;
|
|
734
|
+
clearCanvas(state);
|
|
735
|
+
const canvas = createCanvasElement(rootElement, state.config);
|
|
736
|
+
state.canvas = canvas;
|
|
737
|
+
rootElement.appendChild(canvas);
|
|
738
|
+
const { width, height } = canvas.getBoundingClientRect();
|
|
739
|
+
renderElements(state, state.config.elements, width, height);
|
|
740
|
+
attachResizeHandler(state, rootElement);
|
|
741
|
+
}
|
|
742
|
+
function createCanvasElement(rootElement, config) {
|
|
743
|
+
const canvas = document.createElement("div");
|
|
744
|
+
canvas.dataset.role = "controller-canvas";
|
|
745
|
+
const containerRect = rootElement.getBoundingClientRect();
|
|
746
|
+
const dimensions = calculateCanvasDimensions(
|
|
747
|
+
containerRect.width,
|
|
748
|
+
containerRect.height,
|
|
749
|
+
config.canvas.aspectRatio
|
|
750
|
+
);
|
|
751
|
+
Object.assign(canvas.style, {
|
|
752
|
+
position: "relative",
|
|
753
|
+
width: `${dimensions.width}px`,
|
|
754
|
+
height: `${dimensions.height}px`,
|
|
755
|
+
marginLeft: `${dimensions.offsetX}px`,
|
|
756
|
+
marginTop: `${dimensions.offsetY}px`,
|
|
757
|
+
backgroundColor: config.canvas.backgroundColor || "transparent",
|
|
758
|
+
overflow: "hidden",
|
|
759
|
+
touchAction: "none"
|
|
760
|
+
});
|
|
761
|
+
return canvas;
|
|
762
|
+
}
|
|
763
|
+
function renderElements(state, elements, canvasWidth, canvasHeight) {
|
|
764
|
+
for (const element of elements) {
|
|
765
|
+
const domElement = createElementByType(state, element, canvasWidth, canvasHeight);
|
|
766
|
+
if (domElement) {
|
|
767
|
+
state.canvas?.appendChild(domElement);
|
|
768
|
+
state.elementMap.set(element.id, domElement);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function createElementByType(state, element, canvasWidth, canvasHeight) {
|
|
773
|
+
switch (element.type) {
|
|
774
|
+
case "button":
|
|
775
|
+
return createButtonElement({
|
|
776
|
+
element,
|
|
777
|
+
canvasWidth,
|
|
778
|
+
canvasHeight,
|
|
779
|
+
onInputStart: (elementId, actionKey) => state.bridge.emitInputStart(elementId, actionKey),
|
|
780
|
+
onInputEnd: (elementId, actionKey) => state.bridge.emitInputEnd(elementId, actionKey)
|
|
781
|
+
});
|
|
782
|
+
case "joystick":
|
|
783
|
+
return createJoystickElement({
|
|
784
|
+
element,
|
|
785
|
+
canvasWidth,
|
|
786
|
+
canvasHeight,
|
|
787
|
+
onJoystickMove: (elementId, vector, angle, magnitude) => state.bridge.emitJoystickMove(elementId, vector.x, vector.y, angle, magnitude)
|
|
788
|
+
});
|
|
789
|
+
case "dpad":
|
|
790
|
+
return createDpadElement({
|
|
791
|
+
element,
|
|
792
|
+
canvasWidth,
|
|
793
|
+
canvasHeight,
|
|
794
|
+
onDpadPress: (elementId, direction, actionKey) => state.bridge.emitDpadPress(elementId, direction, actionKey),
|
|
795
|
+
onDpadRelease: (elementId, direction, actionKey) => state.bridge.emitDpadRelease(elementId, direction, actionKey)
|
|
796
|
+
});
|
|
797
|
+
default:
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function clearCanvas(state) {
|
|
802
|
+
if (state.canvas) {
|
|
803
|
+
state.canvas.remove();
|
|
804
|
+
state.canvas = null;
|
|
805
|
+
}
|
|
806
|
+
state.elementMap.clear();
|
|
807
|
+
if (state.resizeObserver) {
|
|
808
|
+
state.resizeObserver.disconnect();
|
|
809
|
+
state.resizeObserver = null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function attachResizeHandler(state, rootElement) {
|
|
813
|
+
state.resizeObserver = new ResizeObserver(() => {
|
|
814
|
+
if (state.config) {
|
|
815
|
+
renderController(state, rootElement);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
state.resizeObserver.observe(rootElement);
|
|
819
|
+
}
|
|
820
|
+
function destroyRenderer(state) {
|
|
821
|
+
clearCanvas(state);
|
|
822
|
+
state.bridge.destroy();
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/sdk/controller-sdk.ts
|
|
826
|
+
function createControllerSDK(options) {
|
|
827
|
+
const { config, container, iframeSrc, onInput, onReady, width, height } = options;
|
|
828
|
+
const iframe = createIframeElement(width, height);
|
|
829
|
+
container.appendChild(iframe);
|
|
830
|
+
let bridge = null;
|
|
831
|
+
iframe.addEventListener("load", () => {
|
|
832
|
+
bridge = createParentBridge(iframe);
|
|
833
|
+
if (onInput) {
|
|
834
|
+
bridge.onInput(onInput);
|
|
835
|
+
}
|
|
836
|
+
bridge.onMessage((message) => {
|
|
837
|
+
if (message.type === "RENDERER_READY" /* RENDERER_READY */) {
|
|
838
|
+
bridge?.sendConfig(config);
|
|
839
|
+
onReady?.();
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
if (iframeSrc) {
|
|
844
|
+
iframe.src = iframeSrc;
|
|
845
|
+
} else {
|
|
846
|
+
injectRendererIntoIframe(iframe);
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
updateConfig(newConfig) {
|
|
850
|
+
bridge?.updateConfig(newConfig);
|
|
851
|
+
},
|
|
852
|
+
destroy() {
|
|
853
|
+
bridge?.destroy();
|
|
854
|
+
iframe.remove();
|
|
855
|
+
},
|
|
856
|
+
getIframe() {
|
|
857
|
+
return iframe;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function createIframeElement(width, height) {
|
|
862
|
+
const iframe = document.createElement("iframe");
|
|
863
|
+
Object.assign(iframe.style, {
|
|
864
|
+
width: width || "100%",
|
|
865
|
+
height: height || "100%",
|
|
866
|
+
border: "none",
|
|
867
|
+
display: "block"
|
|
868
|
+
});
|
|
869
|
+
iframe.setAttribute("allowtransparency", "true");
|
|
870
|
+
iframe.setAttribute("allow", "autoplay");
|
|
871
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
872
|
+
return iframe;
|
|
873
|
+
}
|
|
874
|
+
function injectRendererIntoIframe(iframe) {
|
|
875
|
+
const rendererHTML = generateRendererHTML();
|
|
876
|
+
iframe.srcdoc = rendererHTML;
|
|
877
|
+
}
|
|
878
|
+
function generateRendererHTML() {
|
|
879
|
+
return `<!DOCTYPE html>
|
|
880
|
+
<html lang="en">
|
|
881
|
+
<head>
|
|
882
|
+
<meta charset="UTF-8" />
|
|
883
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
|
884
|
+
<title>Game Controller Renderer</title>
|
|
885
|
+
<style>
|
|
886
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
887
|
+
html, body { width: 100%; height: 100%; overflow: hidden; touch-action: none; user-select: none; background: transparent; }
|
|
888
|
+
#controller-root { width: 100%; height: 100%; position: relative; }
|
|
889
|
+
</style>
|
|
890
|
+
</head>
|
|
891
|
+
<body>
|
|
892
|
+
<div id="controller-root"></div>
|
|
893
|
+
<script type="module">
|
|
894
|
+
${getEmbeddedRendererScript()}
|
|
895
|
+
</script>
|
|
896
|
+
</body>
|
|
897
|
+
</html>`;
|
|
898
|
+
}
|
|
899
|
+
function getEmbeddedRendererScript() {
|
|
900
|
+
return `
|
|
901
|
+
const MessageType = {
|
|
902
|
+
CONFIG_LOAD: 'CONFIG_LOAD',
|
|
903
|
+
INPUT_START: 'INPUT_START',
|
|
904
|
+
INPUT_END: 'INPUT_END',
|
|
905
|
+
JOYSTICK_MOVE: 'JOYSTICK_MOVE',
|
|
906
|
+
DPAD_PRESS: 'DPAD_PRESS',
|
|
907
|
+
DPAD_RELEASE: 'DPAD_RELEASE',
|
|
908
|
+
SLIDER_CHANGE: 'SLIDER_CHANGE',
|
|
909
|
+
RENDERER_READY: 'RENDERER_READY',
|
|
910
|
+
CONFIG_UPDATE: 'CONFIG_UPDATE',
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
function emitToParent(message) {
|
|
914
|
+
window.parent.postMessage(message, '*');
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function percentageToPixel(pct, containerSize) {
|
|
918
|
+
return (pct / 100) * containerSize;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function clamp(val, min, max) {
|
|
922
|
+
return Math.max(min, Math.min(max, val));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function applyDeadzoneToVector(x, y, deadzone) {
|
|
926
|
+
const mag = Math.sqrt(x * x + y * y);
|
|
927
|
+
if (mag < deadzone) return { x: 0, y: 0 };
|
|
928
|
+
const scale = (mag - deadzone) / (1 - deadzone) / mag;
|
|
929
|
+
return { x: x * scale, y: y * scale };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function parseAspectRatio(ar) {
|
|
933
|
+
const [w, h] = ar.split(':').map(Number);
|
|
934
|
+
return { width: w || 16, height: h || 9 };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function calculateCanvasDimensions(cw, ch, ar) {
|
|
938
|
+
const r = parseAspectRatio(ar);
|
|
939
|
+
const tr = r.width / r.height;
|
|
940
|
+
const cr = cw / ch;
|
|
941
|
+
let w, h;
|
|
942
|
+
if (cr > tr) { h = ch; w = h * tr; }
|
|
943
|
+
else { w = cw; h = w / tr; }
|
|
944
|
+
return { width: Math.floor(w), height: Math.floor(h), offsetX: Math.floor((cw - w) / 2), offsetY: Math.floor((ch - h) / 2) };
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function renderButton(el, cw, ch, canvas) {
|
|
948
|
+
const btn = document.createElement('div');
|
|
949
|
+
btn.dataset.elementId = el.id;
|
|
950
|
+
const px = percentageToPixel(el.position.x, cw);
|
|
951
|
+
const py = percentageToPixel(el.position.y, ch);
|
|
952
|
+
const sz = percentageToPixel(el.size, Math.min(cw, ch));
|
|
953
|
+
const half = sz / 2;
|
|
954
|
+
Object.assign(btn.style, {
|
|
955
|
+
position: 'absolute', left: (px - half) + 'px', top: (py - half) + 'px',
|
|
956
|
+
width: sz + 'px', height: sz + 'px',
|
|
957
|
+
borderRadius: el.shape === 'circle' ? '50%' : '15%',
|
|
958
|
+
backgroundColor: el.style?.color || '#444', opacity: String(el.style?.opacity ?? 0.8),
|
|
959
|
+
zIndex: String(el.zIndex || 1), display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
960
|
+
userSelect: 'none', touchAction: 'none', cursor: 'pointer',
|
|
961
|
+
border: '2px solid rgba(255,255,255,0.3)', boxSizing: 'border-box',
|
|
962
|
+
transition: 'transform 0.05s ease'
|
|
963
|
+
});
|
|
964
|
+
if (el.label) {
|
|
965
|
+
const lbl = document.createElement('span');
|
|
966
|
+
lbl.textContent = el.label;
|
|
967
|
+
Object.assign(lbl.style, { color: '#fff', fontSize: '14px', fontWeight: 'bold', pointerEvents: 'none', userSelect: 'none' });
|
|
968
|
+
btn.appendChild(lbl);
|
|
969
|
+
}
|
|
970
|
+
const active = new Set();
|
|
971
|
+
btn.addEventListener('pointerdown', e => { e.preventDefault(); active.add(e.pointerId); btn.setPointerCapture(e.pointerId); btn.style.transform='scale(0.9)'; btn.style.opacity='1'; emitToParent({ type: MessageType.INPUT_START, payload: { elementId: el.id, actionKey: el.actionKey, timestamp: Date.now() } }); });
|
|
972
|
+
btn.addEventListener('pointerup', e => { active.delete(e.pointerId); if(!active.size){ btn.style.transform='scale(1)'; btn.style.opacity=String(el.style?.opacity??0.8); emitToParent({ type: MessageType.INPUT_END, payload: { elementId: el.id, actionKey: el.actionKey, timestamp: Date.now() } }); }});
|
|
973
|
+
btn.addEventListener('pointercancel', e => { active.delete(e.pointerId); if(!active.size){ btn.style.transform='scale(1)'; btn.style.opacity=String(el.style?.opacity??0.8); emitToParent({ type: MessageType.INPUT_END, payload: { elementId: el.id, actionKey: el.actionKey, timestamp: Date.now() } }); }});
|
|
974
|
+
canvas.appendChild(btn);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function renderJoystick(el, cw, ch, canvas) {
|
|
978
|
+
const ref = Math.min(cw, ch);
|
|
979
|
+
const outerR = percentageToPixel(el.radius, ref);
|
|
980
|
+
const innerR = percentageToPixel(el.innerStickSize, ref);
|
|
981
|
+
const px = percentageToPixel(el.position.x, cw);
|
|
982
|
+
const py = percentageToPixel(el.position.y, ch);
|
|
983
|
+
const container = document.createElement('div');
|
|
984
|
+
container.dataset.elementId = el.id;
|
|
985
|
+
Object.assign(container.style, {
|
|
986
|
+
position:'absolute', left:(px-outerR)+'px', top:(py-outerR)+'px',
|
|
987
|
+
width:(outerR*2)+'px', height:(outerR*2)+'px', borderRadius:'50%',
|
|
988
|
+
backgroundColor: el.style?.color || 'rgba(80,80,80,0.5)',
|
|
989
|
+
border: '2px solid rgba(255,255,255,0.2)', opacity: String(el.style?.opacity ?? 0.7),
|
|
990
|
+
zIndex: String(el.zIndex || 1), touchAction:'none', userSelect:'none', boxSizing:'border-box'
|
|
991
|
+
});
|
|
992
|
+
const stick = document.createElement('div');
|
|
993
|
+
Object.assign(stick.style, {
|
|
994
|
+
position:'absolute', width:(innerR*2)+'px', height:(innerR*2)+'px', borderRadius:'50%',
|
|
995
|
+
backgroundColor:'rgba(200,200,200,0.8)', left:'50%', top:'50%',
|
|
996
|
+
transform:'translate(-50%,-50%)', pointerEvents:'none', willChange:'transform'
|
|
997
|
+
});
|
|
998
|
+
container.appendChild(stick);
|
|
999
|
+
let isActive = false, activeId = null;
|
|
1000
|
+
container.addEventListener('pointerdown', e => { if(isActive) return; e.preventDefault(); isActive=true; activeId=e.pointerId; container.setPointerCapture(e.pointerId); handleMove(e); });
|
|
1001
|
+
container.addEventListener('pointermove', e => { if(!isActive||e.pointerId!==activeId) return; e.preventDefault(); handleMove(e); });
|
|
1002
|
+
const release = () => { isActive=false; activeId=null; stick.style.transform='translate(-50%,-50%)'; emitToParent({ type:MessageType.JOYSTICK_MOVE, payload:{elementId:el.id,vector:{x:0,y:0},angle:0,magnitude:0,timestamp:Date.now()}}); };
|
|
1003
|
+
container.addEventListener('pointerup', e => { if(e.pointerId===activeId) release(); });
|
|
1004
|
+
container.addEventListener('pointercancel', e => { if(e.pointerId===activeId) release(); });
|
|
1005
|
+
function handleMove(e) {
|
|
1006
|
+
const r = container.getBoundingClientRect();
|
|
1007
|
+
const cx=r.width/2, cy=r.height/2;
|
|
1008
|
+
let dx=e.clientX-r.left-cx, dy=e.clientY-r.top-cy;
|
|
1009
|
+
const d = Math.sqrt(dx*dx+dy*dy);
|
|
1010
|
+
if(d>outerR){dx=(dx/d)*outerR; dy=(dy/d)*outerR;}
|
|
1011
|
+
stick.style.transform='translate(calc(-50% + '+dx+'px), calc(-50% + '+dy+'px))';
|
|
1012
|
+
let nx=clamp(dx/outerR,-1,1), ny=clamp(dy/outerR,-1,1);
|
|
1013
|
+
const dz = applyDeadzoneToVector(nx,ny,el.deadzone||0);
|
|
1014
|
+
const angle=Math.atan2(dz.y,dz.x), mag=Math.min(Math.sqrt(dz.x*dz.x+dz.y*dz.y),1);
|
|
1015
|
+
emitToParent({type:MessageType.JOYSTICK_MOVE,payload:{elementId:el.id,vector:dz,angle,magnitude:mag,timestamp:Date.now()}});
|
|
1016
|
+
}
|
|
1017
|
+
canvas.appendChild(container);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function renderDpad(el, cw, ch, canvas) {
|
|
1021
|
+
const ref = Math.min(cw, ch);
|
|
1022
|
+
const sz = percentageToPixel(el.size, ref);
|
|
1023
|
+
const px = percentageToPixel(el.position.x, cw);
|
|
1024
|
+
const py = percentageToPixel(el.position.y, ch);
|
|
1025
|
+
const half = sz/2;
|
|
1026
|
+
const container = document.createElement('div');
|
|
1027
|
+
container.dataset.elementId = el.id;
|
|
1028
|
+
Object.assign(container.style, {
|
|
1029
|
+
position:'absolute', left:(px-half)+'px', top:(py-half)+'px',
|
|
1030
|
+
width:sz+'px', height:sz+'px', display:'grid',
|
|
1031
|
+
gridTemplateColumns:'1fr 1fr 1fr', gridTemplateRows:'1fr 1fr 1fr', gap:'2px',
|
|
1032
|
+
zIndex:String(el.zIndex||1), touchAction:'none', userSelect:'none'
|
|
1033
|
+
});
|
|
1034
|
+
const keys = el.actionKeys || {up:'DPAD_UP',down:'DPAD_DOWN',left:'DPAD_LEFT',right:'DPAD_RIGHT'};
|
|
1035
|
+
const arrows = {up:'\u25B2',down:'\u25BC',left:'\u25C0',right:'\u25B6'};
|
|
1036
|
+
const areas = {up:'1/2/2/3',left:'2/1/3/2',right:'2/3/3/4',down:'3/2/4/3'};
|
|
1037
|
+
const center = document.createElement('div');
|
|
1038
|
+
Object.assign(center.style,{gridArea:'2/2/3/3',backgroundColor:el.style?.color||'#333',borderRadius:'2px'});
|
|
1039
|
+
container.appendChild(center);
|
|
1040
|
+
for(const dir of ['up','down','left','right']){
|
|
1041
|
+
const b = document.createElement('div');
|
|
1042
|
+
b.dataset.direction = dir;
|
|
1043
|
+
Object.assign(b.style,{gridArea:areas[dir],backgroundColor:el.style?.color||'#444',borderRadius:'4px',display:'flex',alignItems:'center',justifyContent:'center',color:'rgba(255,255,255,0.7)',fontSize:'12px',opacity:String(el.style?.opacity??0.8),cursor:'pointer',touchAction:'none',userSelect:'none',transition:'transform 0.05s ease'});
|
|
1044
|
+
b.textContent = arrows[dir];
|
|
1045
|
+
b.addEventListener('pointerdown', e => { e.preventDefault();e.stopPropagation(); b.setPointerCapture(e.pointerId); b.style.transform='scale(0.85)'; b.style.opacity='1'; emitToParent({type:MessageType.DPAD_PRESS,payload:{elementId:el.id,direction:dir,actionKey:keys[dir],timestamp:Date.now()}}); });
|
|
1046
|
+
b.addEventListener('pointerup', e => { b.style.transform='scale(1)'; b.style.opacity=String(el.style?.opacity??0.8); emitToParent({type:MessageType.DPAD_RELEASE,payload:{elementId:el.id,direction:dir,actionKey:keys[dir],timestamp:Date.now()}}); });
|
|
1047
|
+
b.addEventListener('pointercancel', () => { b.style.transform='scale(1)'; b.style.opacity=String(el.style?.opacity??0.8); emitToParent({type:MessageType.DPAD_RELEASE,payload:{elementId:el.id,direction:dir,actionKey:keys[dir],timestamp:Date.now()}}); });
|
|
1048
|
+
container.appendChild(b);
|
|
1049
|
+
}
|
|
1050
|
+
canvas.appendChild(container);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function renderConfig(config) {
|
|
1054
|
+
const root = document.getElementById('controller-root');
|
|
1055
|
+
root.innerHTML = '';
|
|
1056
|
+
const rect = root.getBoundingClientRect();
|
|
1057
|
+
const dims = calculateCanvasDimensions(rect.width, rect.height, config.canvas.aspectRatio);
|
|
1058
|
+
const canvas = document.createElement('div');
|
|
1059
|
+
Object.assign(canvas.style, {
|
|
1060
|
+
position:'relative', width:dims.width+'px', height:dims.height+'px',
|
|
1061
|
+
marginLeft:dims.offsetX+'px', marginTop:dims.offsetY+'px',
|
|
1062
|
+
backgroundColor:config.canvas.backgroundColor||'transparent',
|
|
1063
|
+
overflow:'hidden', touchAction:'none'
|
|
1064
|
+
});
|
|
1065
|
+
root.appendChild(canvas);
|
|
1066
|
+
for(const el of config.elements){
|
|
1067
|
+
if(el.type==='button') renderButton(el, dims.width, dims.height, canvas);
|
|
1068
|
+
else if(el.type==='joystick') renderJoystick(el, dims.width, dims.height, canvas);
|
|
1069
|
+
else if(el.type==='dpad') renderDpad(el, dims.width, dims.height, canvas);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
window.addEventListener('message', e => {
|
|
1074
|
+
const msg = e.data;
|
|
1075
|
+
if(!msg||!msg.type) return;
|
|
1076
|
+
if(msg.type===MessageType.CONFIG_LOAD||msg.type===MessageType.CONFIG_UPDATE){
|
|
1077
|
+
renderConfig(msg.payload);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
emitToParent({ type: MessageType.RENDERER_READY });
|
|
1082
|
+
`;
|
|
1083
|
+
}
|
|
1084
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1085
|
+
0 && (module.exports = {
|
|
1086
|
+
MessageType,
|
|
1087
|
+
angleFromVector,
|
|
1088
|
+
applyDeadzone,
|
|
1089
|
+
applyDeadzoneToVector,
|
|
1090
|
+
calculateCanvasDimensions,
|
|
1091
|
+
clamp,
|
|
1092
|
+
clampPercentage,
|
|
1093
|
+
convertPercentagePositionToPixel,
|
|
1094
|
+
convertPixelPositionToPercentage,
|
|
1095
|
+
createButtonElement,
|
|
1096
|
+
createControllerSDK,
|
|
1097
|
+
createDpadElement,
|
|
1098
|
+
createIframeBridge,
|
|
1099
|
+
createJoystickElement,
|
|
1100
|
+
createParentBridge,
|
|
1101
|
+
destroyRenderer,
|
|
1102
|
+
distance,
|
|
1103
|
+
findAlignmentGuides,
|
|
1104
|
+
generateGridLines,
|
|
1105
|
+
initializeRenderer,
|
|
1106
|
+
normalizeVector,
|
|
1107
|
+
parseAspectRatio,
|
|
1108
|
+
percentageToPixel,
|
|
1109
|
+
pixelToPercentage,
|
|
1110
|
+
renderControllerFromConfig,
|
|
1111
|
+
snapPositionToGrid,
|
|
1112
|
+
snapToGrid,
|
|
1113
|
+
updateButtonElement,
|
|
1114
|
+
updateDpadElement,
|
|
1115
|
+
updateJoystickElement
|
|
1116
|
+
});
|