@ksteinstudio/game-controller 1.0.0 → 1.0.1
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/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +186 -12
- package/dist/index.mjs +183 -11
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -242,6 +242,15 @@ interface DpadRenderContext {
|
|
|
242
242
|
declare function createDpadElement(context: DpadRenderContext): HTMLElement;
|
|
243
243
|
declare function updateDpadElement(container: HTMLElement, element: DpadElement, canvasWidth: number, canvasHeight: number): void;
|
|
244
244
|
|
|
245
|
+
interface SliderRenderContext {
|
|
246
|
+
element: SliderElement;
|
|
247
|
+
canvasWidth: number;
|
|
248
|
+
canvasHeight: number;
|
|
249
|
+
onSliderChange: (elementId: string, actionKey: string, value: number) => void;
|
|
250
|
+
}
|
|
251
|
+
declare function createSliderElement(context: SliderRenderContext): HTMLElement;
|
|
252
|
+
declare function updateSliderElement(container: HTMLElement, element: SliderElement, canvasWidth: number, canvasHeight: number): void;
|
|
253
|
+
|
|
245
254
|
interface RendererState {
|
|
246
255
|
config: ControllerConfig | null;
|
|
247
256
|
canvas: HTMLElement | null;
|
|
@@ -269,4 +278,4 @@ interface ControllerSDKInstance {
|
|
|
269
278
|
}
|
|
270
279
|
declare function createControllerSDK(options: ControllerSDKOptions): ControllerSDKInstance;
|
|
271
280
|
|
|
272
|
-
export { type AlignmentGuide, type BaseElement, type ButtonElement, type ButtonRenderContext, type CanvasSettings, type ConfigLoadMessage, type ConfigUpdateMessage, type ControllerConfig, type ControllerElement, type ControllerMessage, type ControllerSDKInstance, type ControllerSDKOptions, type DpadElement, type DpadPressMessage, type DpadReleaseMessage, type DpadRenderContext, type ElementStyle, type IframeBridge, type InputEndMessage, type InputEvent, type InputStartMessage, type JoystickElement, type JoystickMoveMessage, type JoystickRenderContext, MessageType, type ParentBridge, type Position, type RendererReadyMessage, type SliderChangeMessage, type SliderElement, type SnapResult, type Vector2D, angleFromVector, applyDeadzone, applyDeadzoneToVector, calculateCanvasDimensions, clamp, clampPercentage, convertPercentagePositionToPixel, convertPixelPositionToPercentage, createButtonElement, createControllerSDK, createDpadElement, createIframeBridge, createJoystickElement, createParentBridge, destroyRenderer, distance, findAlignmentGuides, generateGridLines, initializeRenderer, normalizeVector, parseAspectRatio, percentageToPixel, pixelToPercentage, renderControllerFromConfig, snapPositionToGrid, snapToGrid, updateButtonElement, updateDpadElement, updateJoystickElement };
|
|
281
|
+
export { type AlignmentGuide, type BaseElement, type ButtonElement, type ButtonRenderContext, type CanvasSettings, type ConfigLoadMessage, type ConfigUpdateMessage, type ControllerConfig, type ControllerElement, type ControllerMessage, type ControllerSDKInstance, type ControllerSDKOptions, type DpadElement, type DpadPressMessage, type DpadReleaseMessage, type DpadRenderContext, type ElementStyle, type IframeBridge, type InputEndMessage, type InputEvent, type InputStartMessage, type JoystickElement, type JoystickMoveMessage, type JoystickRenderContext, MessageType, type ParentBridge, type Position, type RendererReadyMessage, type SliderChangeMessage, type SliderElement, type SliderRenderContext, type SnapResult, type Vector2D, angleFromVector, applyDeadzone, applyDeadzoneToVector, calculateCanvasDimensions, clamp, clampPercentage, convertPercentagePositionToPixel, convertPixelPositionToPercentage, createButtonElement, createControllerSDK, createDpadElement, createIframeBridge, createJoystickElement, createParentBridge, createSliderElement, destroyRenderer, distance, findAlignmentGuides, generateGridLines, initializeRenderer, normalizeVector, parseAspectRatio, percentageToPixel, pixelToPercentage, renderControllerFromConfig, snapPositionToGrid, snapToGrid, updateButtonElement, updateDpadElement, updateJoystickElement, updateSliderElement };
|
package/dist/index.d.ts
CHANGED
|
@@ -242,6 +242,15 @@ interface DpadRenderContext {
|
|
|
242
242
|
declare function createDpadElement(context: DpadRenderContext): HTMLElement;
|
|
243
243
|
declare function updateDpadElement(container: HTMLElement, element: DpadElement, canvasWidth: number, canvasHeight: number): void;
|
|
244
244
|
|
|
245
|
+
interface SliderRenderContext {
|
|
246
|
+
element: SliderElement;
|
|
247
|
+
canvasWidth: number;
|
|
248
|
+
canvasHeight: number;
|
|
249
|
+
onSliderChange: (elementId: string, actionKey: string, value: number) => void;
|
|
250
|
+
}
|
|
251
|
+
declare function createSliderElement(context: SliderRenderContext): HTMLElement;
|
|
252
|
+
declare function updateSliderElement(container: HTMLElement, element: SliderElement, canvasWidth: number, canvasHeight: number): void;
|
|
253
|
+
|
|
245
254
|
interface RendererState {
|
|
246
255
|
config: ControllerConfig | null;
|
|
247
256
|
canvas: HTMLElement | null;
|
|
@@ -269,4 +278,4 @@ interface ControllerSDKInstance {
|
|
|
269
278
|
}
|
|
270
279
|
declare function createControllerSDK(options: ControllerSDKOptions): ControllerSDKInstance;
|
|
271
280
|
|
|
272
|
-
export { type AlignmentGuide, type BaseElement, type ButtonElement, type ButtonRenderContext, type CanvasSettings, type ConfigLoadMessage, type ConfigUpdateMessage, type ControllerConfig, type ControllerElement, type ControllerMessage, type ControllerSDKInstance, type ControllerSDKOptions, type DpadElement, type DpadPressMessage, type DpadReleaseMessage, type DpadRenderContext, type ElementStyle, type IframeBridge, type InputEndMessage, type InputEvent, type InputStartMessage, type JoystickElement, type JoystickMoveMessage, type JoystickRenderContext, MessageType, type ParentBridge, type Position, type RendererReadyMessage, type SliderChangeMessage, type SliderElement, type SnapResult, type Vector2D, angleFromVector, applyDeadzone, applyDeadzoneToVector, calculateCanvasDimensions, clamp, clampPercentage, convertPercentagePositionToPixel, convertPixelPositionToPercentage, createButtonElement, createControllerSDK, createDpadElement, createIframeBridge, createJoystickElement, createParentBridge, destroyRenderer, distance, findAlignmentGuides, generateGridLines, initializeRenderer, normalizeVector, parseAspectRatio, percentageToPixel, pixelToPercentage, renderControllerFromConfig, snapPositionToGrid, snapToGrid, updateButtonElement, updateDpadElement, updateJoystickElement };
|
|
281
|
+
export { type AlignmentGuide, type BaseElement, type ButtonElement, type ButtonRenderContext, type CanvasSettings, type ConfigLoadMessage, type ConfigUpdateMessage, type ControllerConfig, type ControllerElement, type ControllerMessage, type ControllerSDKInstance, type ControllerSDKOptions, type DpadElement, type DpadPressMessage, type DpadReleaseMessage, type DpadRenderContext, type ElementStyle, type IframeBridge, type InputEndMessage, type InputEvent, type InputStartMessage, type JoystickElement, type JoystickMoveMessage, type JoystickRenderContext, MessageType, type ParentBridge, type Position, type RendererReadyMessage, type SliderChangeMessage, type SliderElement, type SliderRenderContext, type SnapResult, type Vector2D, angleFromVector, applyDeadzone, applyDeadzoneToVector, calculateCanvasDimensions, clamp, clampPercentage, convertPercentagePositionToPixel, convertPixelPositionToPercentage, createButtonElement, createControllerSDK, createDpadElement, createIframeBridge, createJoystickElement, createParentBridge, createSliderElement, destroyRenderer, distance, findAlignmentGuides, generateGridLines, initializeRenderer, normalizeVector, parseAspectRatio, percentageToPixel, pixelToPercentage, renderControllerFromConfig, snapPositionToGrid, snapToGrid, updateButtonElement, updateDpadElement, updateJoystickElement, updateSliderElement };
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
createIframeBridge: () => createIframeBridge,
|
|
36
36
|
createJoystickElement: () => createJoystickElement,
|
|
37
37
|
createParentBridge: () => createParentBridge,
|
|
38
|
+
createSliderElement: () => createSliderElement,
|
|
38
39
|
destroyRenderer: () => destroyRenderer,
|
|
39
40
|
distance: () => distance,
|
|
40
41
|
findAlignmentGuides: () => findAlignmentGuides,
|
|
@@ -49,7 +50,8 @@ __export(index_exports, {
|
|
|
49
50
|
snapToGrid: () => snapToGrid,
|
|
50
51
|
updateButtonElement: () => updateButtonElement,
|
|
51
52
|
updateDpadElement: () => updateDpadElement,
|
|
52
|
-
updateJoystickElement: () => updateJoystickElement
|
|
53
|
+
updateJoystickElement: () => updateJoystickElement,
|
|
54
|
+
updateSliderElement: () => updateSliderElement
|
|
53
55
|
});
|
|
54
56
|
module.exports = __toCommonJS(index_exports);
|
|
55
57
|
|
|
@@ -196,10 +198,12 @@ function isInputEvent(message) {
|
|
|
196
198
|
|
|
197
199
|
// src/math/coordinate-converter.ts
|
|
198
200
|
function snapToGrid(value, gridDensity) {
|
|
201
|
+
if (gridDensity <= 0) return value;
|
|
199
202
|
const step = 100 / gridDensity;
|
|
200
203
|
return Math.round(value / step) * step;
|
|
201
204
|
}
|
|
202
205
|
function snapPositionToGrid(position, gridDensity) {
|
|
206
|
+
if (gridDensity <= 0) return position;
|
|
203
207
|
return {
|
|
204
208
|
x: snapToGrid(position.x, gridDensity),
|
|
205
209
|
y: snapToGrid(position.y, gridDensity)
|
|
@@ -336,6 +340,7 @@ function findCenterSnap(element, position, threshold) {
|
|
|
336
340
|
return { guides, snappedX, snappedY };
|
|
337
341
|
}
|
|
338
342
|
function generateGridLines(gridDensity) {
|
|
343
|
+
if (gridDensity <= 0) return [];
|
|
339
344
|
const step = 100 / gridDensity;
|
|
340
345
|
const lines = [];
|
|
341
346
|
for (let i = 0; i <= gridDensity; i++) {
|
|
@@ -698,6 +703,113 @@ function updateDpadElement(container, element, canvasWidth, canvasHeight) {
|
|
|
698
703
|
container.style.height = `${pixelSize}px`;
|
|
699
704
|
}
|
|
700
705
|
|
|
706
|
+
// src/renderer/slider-renderer.ts
|
|
707
|
+
function createSliderElement(context) {
|
|
708
|
+
const { element, canvasWidth, canvasHeight, onSliderChange } = context;
|
|
709
|
+
const container = document.createElement("div");
|
|
710
|
+
container.dataset.elementId = element.id;
|
|
711
|
+
container.dataset.elementType = "slider";
|
|
712
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
713
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
714
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
715
|
+
const trackLength = percentageToPixel(element.length, referenceDimension);
|
|
716
|
+
const trackThickness = percentageToPixel(4, referenceDimension);
|
|
717
|
+
const isHorizontal = element.orientation === "horizontal";
|
|
718
|
+
const trackWidth = isHorizontal ? trackLength : trackThickness;
|
|
719
|
+
const trackHeight = isHorizontal ? trackThickness : trackLength;
|
|
720
|
+
applySliderTrackStyles(container, pixelX, pixelY, trackWidth, trackHeight, element);
|
|
721
|
+
const thumb = createThumbElement(isHorizontal, trackWidth, trackHeight, element);
|
|
722
|
+
container.appendChild(thumb);
|
|
723
|
+
attachSliderInteraction(container, thumb, element, isHorizontal, onSliderChange);
|
|
724
|
+
return container;
|
|
725
|
+
}
|
|
726
|
+
function applySliderTrackStyles(container, pixelX, pixelY, trackWidth, trackHeight, element) {
|
|
727
|
+
const halfW = trackWidth / 2;
|
|
728
|
+
const halfH = trackHeight / 2;
|
|
729
|
+
Object.assign(container.style, {
|
|
730
|
+
position: "absolute",
|
|
731
|
+
left: `${pixelX - halfW}px`,
|
|
732
|
+
top: `${pixelY - halfH}px`,
|
|
733
|
+
width: `${trackWidth}px`,
|
|
734
|
+
height: `${trackHeight}px`,
|
|
735
|
+
borderRadius: "8px",
|
|
736
|
+
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
|
737
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
738
|
+
opacity: String(element.style?.opacity ?? 0.85),
|
|
739
|
+
zIndex: String(element.zIndex || 1),
|
|
740
|
+
touchAction: "none",
|
|
741
|
+
userSelect: "none",
|
|
742
|
+
boxSizing: "border-box"
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function createThumbElement(isHorizontal, trackWidth, trackHeight, element) {
|
|
746
|
+
const thumb = document.createElement("div");
|
|
747
|
+
const thumbWidth = isHorizontal ? trackWidth * 0.2 : trackWidth * 0.7;
|
|
748
|
+
const thumbHeight = isHorizontal ? trackHeight * 0.7 : trackHeight * 0.2;
|
|
749
|
+
Object.assign(thumb.style, {
|
|
750
|
+
position: "absolute",
|
|
751
|
+
width: `${thumbWidth}px`,
|
|
752
|
+
height: `${thumbHeight}px`,
|
|
753
|
+
borderRadius: "6px",
|
|
754
|
+
backgroundColor: element.style?.color || "#555",
|
|
755
|
+
left: "50%",
|
|
756
|
+
top: "50%",
|
|
757
|
+
transform: "translate(-50%, -50%)",
|
|
758
|
+
pointerEvents: "none",
|
|
759
|
+
willChange: "left, top"
|
|
760
|
+
});
|
|
761
|
+
return thumb;
|
|
762
|
+
}
|
|
763
|
+
function attachSliderInteraction(container, thumb, element, isHorizontal, onSliderChange) {
|
|
764
|
+
const activePointers = /* @__PURE__ */ new Set();
|
|
765
|
+
container.addEventListener("pointerdown", (event) => {
|
|
766
|
+
event.preventDefault();
|
|
767
|
+
activePointers.add(event.pointerId);
|
|
768
|
+
container.setPointerCapture(event.pointerId);
|
|
769
|
+
updateThumbPosition(event, container, thumb, element, isHorizontal, onSliderChange);
|
|
770
|
+
});
|
|
771
|
+
container.addEventListener("pointermove", (event) => {
|
|
772
|
+
if (!activePointers.has(event.pointerId)) return;
|
|
773
|
+
event.preventDefault();
|
|
774
|
+
updateThumbPosition(event, container, thumb, element, isHorizontal, onSliderChange);
|
|
775
|
+
});
|
|
776
|
+
const releaseHandler = (event) => {
|
|
777
|
+
activePointers.delete(event.pointerId);
|
|
778
|
+
};
|
|
779
|
+
container.addEventListener("pointerup", releaseHandler);
|
|
780
|
+
container.addEventListener("pointercancel", releaseHandler);
|
|
781
|
+
}
|
|
782
|
+
function updateThumbPosition(event, container, thumb, element, isHorizontal, onSliderChange) {
|
|
783
|
+
const rect = container.getBoundingClientRect();
|
|
784
|
+
let value;
|
|
785
|
+
if (isHorizontal) {
|
|
786
|
+
value = (event.clientX - rect.left) / rect.width;
|
|
787
|
+
value = clamp(value, 0, 1);
|
|
788
|
+
thumb.style.left = `${value * 100}%`;
|
|
789
|
+
} else {
|
|
790
|
+
value = (event.clientY - rect.top) / rect.height;
|
|
791
|
+
value = clamp(value, 0, 1);
|
|
792
|
+
thumb.style.top = `${value * 100}%`;
|
|
793
|
+
}
|
|
794
|
+
onSliderChange(element.id, element.actionKey, parseFloat(value.toFixed(4)));
|
|
795
|
+
}
|
|
796
|
+
function updateSliderElement(container, element, canvasWidth, canvasHeight) {
|
|
797
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
798
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
799
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
800
|
+
const trackLength = percentageToPixel(element.length, referenceDimension);
|
|
801
|
+
const trackThickness = percentageToPixel(4, referenceDimension);
|
|
802
|
+
const isHorizontal = element.orientation === "horizontal";
|
|
803
|
+
const trackWidth = isHorizontal ? trackLength : trackThickness;
|
|
804
|
+
const trackHeight = isHorizontal ? trackThickness : trackLength;
|
|
805
|
+
const halfW = trackWidth / 2;
|
|
806
|
+
const halfH = trackHeight / 2;
|
|
807
|
+
container.style.left = `${pixelX - halfW}px`;
|
|
808
|
+
container.style.top = `${pixelY - halfH}px`;
|
|
809
|
+
container.style.width = `${trackWidth}px`;
|
|
810
|
+
container.style.height = `${trackHeight}px`;
|
|
811
|
+
}
|
|
812
|
+
|
|
701
813
|
// src/renderer/rendering-engine.ts
|
|
702
814
|
function initializeRenderer(rootElement) {
|
|
703
815
|
const bridge = createIframeBridge();
|
|
@@ -794,6 +906,13 @@ function createElementByType(state, element, canvasWidth, canvasHeight) {
|
|
|
794
906
|
onDpadPress: (elementId, direction, actionKey) => state.bridge.emitDpadPress(elementId, direction, actionKey),
|
|
795
907
|
onDpadRelease: (elementId, direction, actionKey) => state.bridge.emitDpadRelease(elementId, direction, actionKey)
|
|
796
908
|
});
|
|
909
|
+
case "slider":
|
|
910
|
+
return createSliderElement({
|
|
911
|
+
element,
|
|
912
|
+
canvasWidth,
|
|
913
|
+
canvasHeight,
|
|
914
|
+
onSliderChange: (elementId, actionKey, value) => state.bridge.emitSliderChange(elementId, actionKey, value)
|
|
915
|
+
});
|
|
797
916
|
default:
|
|
798
917
|
return null;
|
|
799
918
|
}
|
|
@@ -828,17 +947,24 @@ function createControllerSDK(options) {
|
|
|
828
947
|
const iframe = createIframeElement(width, height);
|
|
829
948
|
container.appendChild(iframe);
|
|
830
949
|
let bridge = null;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (
|
|
834
|
-
|
|
950
|
+
let configSent = false;
|
|
951
|
+
function sendConfigOnce() {
|
|
952
|
+
if (configSent || !bridge) return;
|
|
953
|
+
configSent = true;
|
|
954
|
+
bridge.sendConfig(config);
|
|
955
|
+
onReady?.();
|
|
956
|
+
}
|
|
957
|
+
bridge = createParentBridge(iframe);
|
|
958
|
+
if (onInput) {
|
|
959
|
+
bridge.onInput(onInput);
|
|
960
|
+
}
|
|
961
|
+
bridge.onMessage((message) => {
|
|
962
|
+
if (message.type === "RENDERER_READY" /* RENDERER_READY */) {
|
|
963
|
+
sendConfigOnce();
|
|
835
964
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
onReady?.();
|
|
840
|
-
}
|
|
841
|
-
});
|
|
965
|
+
});
|
|
966
|
+
iframe.addEventListener("load", () => {
|
|
967
|
+
sendConfigOnce();
|
|
842
968
|
});
|
|
843
969
|
if (iframeSrc) {
|
|
844
970
|
iframe.src = iframeSrc;
|
|
@@ -851,6 +977,7 @@ function createControllerSDK(options) {
|
|
|
851
977
|
},
|
|
852
978
|
destroy() {
|
|
853
979
|
bridge?.destroy();
|
|
980
|
+
bridge = null;
|
|
854
981
|
iframe.remove();
|
|
855
982
|
},
|
|
856
983
|
getIframe() {
|
|
@@ -1050,6 +1177,50 @@ function getEmbeddedRendererScript() {
|
|
|
1050
1177
|
canvas.appendChild(container);
|
|
1051
1178
|
}
|
|
1052
1179
|
|
|
1180
|
+
function renderSlider(el, cw, ch, canvas) {
|
|
1181
|
+
const ref = Math.min(cw, ch);
|
|
1182
|
+
const trackLen = percentageToPixel(el.length, ref);
|
|
1183
|
+
const trackThick = percentageToPixel(4, ref);
|
|
1184
|
+
const px = percentageToPixel(el.position.x, cw);
|
|
1185
|
+
const py = percentageToPixel(el.position.y, ch);
|
|
1186
|
+
const isH = el.orientation === 'horizontal';
|
|
1187
|
+
const tw = isH ? trackLen : trackThick;
|
|
1188
|
+
const th = isH ? trackThick : trackLen;
|
|
1189
|
+
const container = document.createElement('div');
|
|
1190
|
+
container.dataset.elementId = el.id;
|
|
1191
|
+
Object.assign(container.style, {
|
|
1192
|
+
position:'absolute', left:(px-tw/2)+'px', top:(py-th/2)+'px',
|
|
1193
|
+
width:tw+'px', height:th+'px', borderRadius:'8px',
|
|
1194
|
+
backgroundColor:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.08)',
|
|
1195
|
+
opacity:String(el.style?.opacity??0.85), zIndex:String(el.zIndex||1),
|
|
1196
|
+
touchAction:'none', userSelect:'none', boxSizing:'border-box'
|
|
1197
|
+
});
|
|
1198
|
+
const thumb = document.createElement('div');
|
|
1199
|
+
const thumbW = isH ? tw*0.2 : tw*0.7;
|
|
1200
|
+
const thumbH = isH ? th*0.7 : th*0.2;
|
|
1201
|
+
Object.assign(thumb.style, {
|
|
1202
|
+
position:'absolute', width:thumbW+'px', height:thumbH+'px',
|
|
1203
|
+
borderRadius:'6px', backgroundColor:el.style?.color||'#555',
|
|
1204
|
+
left:'50%', top:'50%', transform:'translate(-50%,-50%)',
|
|
1205
|
+
pointerEvents:'none', willChange:'left, top'
|
|
1206
|
+
});
|
|
1207
|
+
container.appendChild(thumb);
|
|
1208
|
+
const active = new Set();
|
|
1209
|
+
container.addEventListener('pointerdown', e => { e.preventDefault(); active.add(e.pointerId); container.setPointerCapture(e.pointerId); updateThumb(e); });
|
|
1210
|
+
container.addEventListener('pointermove', e => { if(!active.has(e.pointerId)) return; e.preventDefault(); updateThumb(e); });
|
|
1211
|
+
container.addEventListener('pointerup', e => { active.delete(e.pointerId); });
|
|
1212
|
+
container.addEventListener('pointercancel', e => { active.delete(e.pointerId); });
|
|
1213
|
+
function updateThumb(e) {
|
|
1214
|
+
const rect = container.getBoundingClientRect();
|
|
1215
|
+
let val;
|
|
1216
|
+
if(isH) { val = (e.clientX - rect.left) / rect.width; } else { val = (e.clientY - rect.top) / rect.height; }
|
|
1217
|
+
val = clamp(val, 0, 1);
|
|
1218
|
+
if(isH) thumb.style.left = (val*100)+'%'; else thumb.style.top = (val*100)+'%';
|
|
1219
|
+
emitToParent({type:MessageType.SLIDER_CHANGE,payload:{elementId:el.id,actionKey:el.actionKey,value:parseFloat(val.toFixed(4)),timestamp:Date.now()}});
|
|
1220
|
+
}
|
|
1221
|
+
canvas.appendChild(container);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1053
1224
|
function renderConfig(config) {
|
|
1054
1225
|
const root = document.getElementById('controller-root');
|
|
1055
1226
|
root.innerHTML = '';
|
|
@@ -1067,6 +1238,7 @@ function getEmbeddedRendererScript() {
|
|
|
1067
1238
|
if(el.type==='button') renderButton(el, dims.width, dims.height, canvas);
|
|
1068
1239
|
else if(el.type==='joystick') renderJoystick(el, dims.width, dims.height, canvas);
|
|
1069
1240
|
else if(el.type==='dpad') renderDpad(el, dims.width, dims.height, canvas);
|
|
1241
|
+
else if(el.type==='slider') renderSlider(el, dims.width, dims.height, canvas);
|
|
1070
1242
|
}
|
|
1071
1243
|
}
|
|
1072
1244
|
|
|
@@ -1098,6 +1270,7 @@ function getEmbeddedRendererScript() {
|
|
|
1098
1270
|
createIframeBridge,
|
|
1099
1271
|
createJoystickElement,
|
|
1100
1272
|
createParentBridge,
|
|
1273
|
+
createSliderElement,
|
|
1101
1274
|
destroyRenderer,
|
|
1102
1275
|
distance,
|
|
1103
1276
|
findAlignmentGuides,
|
|
@@ -1112,5 +1285,6 @@ function getEmbeddedRendererScript() {
|
|
|
1112
1285
|
snapToGrid,
|
|
1113
1286
|
updateButtonElement,
|
|
1114
1287
|
updateDpadElement,
|
|
1115
|
-
updateJoystickElement
|
|
1288
|
+
updateJoystickElement,
|
|
1289
|
+
updateSliderElement
|
|
1116
1290
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -141,10 +141,12 @@ function isInputEvent(message) {
|
|
|
141
141
|
|
|
142
142
|
// src/math/coordinate-converter.ts
|
|
143
143
|
function snapToGrid(value, gridDensity) {
|
|
144
|
+
if (gridDensity <= 0) return value;
|
|
144
145
|
const step = 100 / gridDensity;
|
|
145
146
|
return Math.round(value / step) * step;
|
|
146
147
|
}
|
|
147
148
|
function snapPositionToGrid(position, gridDensity) {
|
|
149
|
+
if (gridDensity <= 0) return position;
|
|
148
150
|
return {
|
|
149
151
|
x: snapToGrid(position.x, gridDensity),
|
|
150
152
|
y: snapToGrid(position.y, gridDensity)
|
|
@@ -281,6 +283,7 @@ function findCenterSnap(element, position, threshold) {
|
|
|
281
283
|
return { guides, snappedX, snappedY };
|
|
282
284
|
}
|
|
283
285
|
function generateGridLines(gridDensity) {
|
|
286
|
+
if (gridDensity <= 0) return [];
|
|
284
287
|
const step = 100 / gridDensity;
|
|
285
288
|
const lines = [];
|
|
286
289
|
for (let i = 0; i <= gridDensity; i++) {
|
|
@@ -643,6 +646,113 @@ function updateDpadElement(container, element, canvasWidth, canvasHeight) {
|
|
|
643
646
|
container.style.height = `${pixelSize}px`;
|
|
644
647
|
}
|
|
645
648
|
|
|
649
|
+
// src/renderer/slider-renderer.ts
|
|
650
|
+
function createSliderElement(context) {
|
|
651
|
+
const { element, canvasWidth, canvasHeight, onSliderChange } = context;
|
|
652
|
+
const container = document.createElement("div");
|
|
653
|
+
container.dataset.elementId = element.id;
|
|
654
|
+
container.dataset.elementType = "slider";
|
|
655
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
656
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
657
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
658
|
+
const trackLength = percentageToPixel(element.length, referenceDimension);
|
|
659
|
+
const trackThickness = percentageToPixel(4, referenceDimension);
|
|
660
|
+
const isHorizontal = element.orientation === "horizontal";
|
|
661
|
+
const trackWidth = isHorizontal ? trackLength : trackThickness;
|
|
662
|
+
const trackHeight = isHorizontal ? trackThickness : trackLength;
|
|
663
|
+
applySliderTrackStyles(container, pixelX, pixelY, trackWidth, trackHeight, element);
|
|
664
|
+
const thumb = createThumbElement(isHorizontal, trackWidth, trackHeight, element);
|
|
665
|
+
container.appendChild(thumb);
|
|
666
|
+
attachSliderInteraction(container, thumb, element, isHorizontal, onSliderChange);
|
|
667
|
+
return container;
|
|
668
|
+
}
|
|
669
|
+
function applySliderTrackStyles(container, pixelX, pixelY, trackWidth, trackHeight, element) {
|
|
670
|
+
const halfW = trackWidth / 2;
|
|
671
|
+
const halfH = trackHeight / 2;
|
|
672
|
+
Object.assign(container.style, {
|
|
673
|
+
position: "absolute",
|
|
674
|
+
left: `${pixelX - halfW}px`,
|
|
675
|
+
top: `${pixelY - halfH}px`,
|
|
676
|
+
width: `${trackWidth}px`,
|
|
677
|
+
height: `${trackHeight}px`,
|
|
678
|
+
borderRadius: "8px",
|
|
679
|
+
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
|
680
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
681
|
+
opacity: String(element.style?.opacity ?? 0.85),
|
|
682
|
+
zIndex: String(element.zIndex || 1),
|
|
683
|
+
touchAction: "none",
|
|
684
|
+
userSelect: "none",
|
|
685
|
+
boxSizing: "border-box"
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function createThumbElement(isHorizontal, trackWidth, trackHeight, element) {
|
|
689
|
+
const thumb = document.createElement("div");
|
|
690
|
+
const thumbWidth = isHorizontal ? trackWidth * 0.2 : trackWidth * 0.7;
|
|
691
|
+
const thumbHeight = isHorizontal ? trackHeight * 0.7 : trackHeight * 0.2;
|
|
692
|
+
Object.assign(thumb.style, {
|
|
693
|
+
position: "absolute",
|
|
694
|
+
width: `${thumbWidth}px`,
|
|
695
|
+
height: `${thumbHeight}px`,
|
|
696
|
+
borderRadius: "6px",
|
|
697
|
+
backgroundColor: element.style?.color || "#555",
|
|
698
|
+
left: "50%",
|
|
699
|
+
top: "50%",
|
|
700
|
+
transform: "translate(-50%, -50%)",
|
|
701
|
+
pointerEvents: "none",
|
|
702
|
+
willChange: "left, top"
|
|
703
|
+
});
|
|
704
|
+
return thumb;
|
|
705
|
+
}
|
|
706
|
+
function attachSliderInteraction(container, thumb, element, isHorizontal, onSliderChange) {
|
|
707
|
+
const activePointers = /* @__PURE__ */ new Set();
|
|
708
|
+
container.addEventListener("pointerdown", (event) => {
|
|
709
|
+
event.preventDefault();
|
|
710
|
+
activePointers.add(event.pointerId);
|
|
711
|
+
container.setPointerCapture(event.pointerId);
|
|
712
|
+
updateThumbPosition(event, container, thumb, element, isHorizontal, onSliderChange);
|
|
713
|
+
});
|
|
714
|
+
container.addEventListener("pointermove", (event) => {
|
|
715
|
+
if (!activePointers.has(event.pointerId)) return;
|
|
716
|
+
event.preventDefault();
|
|
717
|
+
updateThumbPosition(event, container, thumb, element, isHorizontal, onSliderChange);
|
|
718
|
+
});
|
|
719
|
+
const releaseHandler = (event) => {
|
|
720
|
+
activePointers.delete(event.pointerId);
|
|
721
|
+
};
|
|
722
|
+
container.addEventListener("pointerup", releaseHandler);
|
|
723
|
+
container.addEventListener("pointercancel", releaseHandler);
|
|
724
|
+
}
|
|
725
|
+
function updateThumbPosition(event, container, thumb, element, isHorizontal, onSliderChange) {
|
|
726
|
+
const rect = container.getBoundingClientRect();
|
|
727
|
+
let value;
|
|
728
|
+
if (isHorizontal) {
|
|
729
|
+
value = (event.clientX - rect.left) / rect.width;
|
|
730
|
+
value = clamp(value, 0, 1);
|
|
731
|
+
thumb.style.left = `${value * 100}%`;
|
|
732
|
+
} else {
|
|
733
|
+
value = (event.clientY - rect.top) / rect.height;
|
|
734
|
+
value = clamp(value, 0, 1);
|
|
735
|
+
thumb.style.top = `${value * 100}%`;
|
|
736
|
+
}
|
|
737
|
+
onSliderChange(element.id, element.actionKey, parseFloat(value.toFixed(4)));
|
|
738
|
+
}
|
|
739
|
+
function updateSliderElement(container, element, canvasWidth, canvasHeight) {
|
|
740
|
+
const pixelX = percentageToPixel(element.position.x, canvasWidth);
|
|
741
|
+
const pixelY = percentageToPixel(element.position.y, canvasHeight);
|
|
742
|
+
const referenceDimension = Math.min(canvasWidth, canvasHeight);
|
|
743
|
+
const trackLength = percentageToPixel(element.length, referenceDimension);
|
|
744
|
+
const trackThickness = percentageToPixel(4, referenceDimension);
|
|
745
|
+
const isHorizontal = element.orientation === "horizontal";
|
|
746
|
+
const trackWidth = isHorizontal ? trackLength : trackThickness;
|
|
747
|
+
const trackHeight = isHorizontal ? trackThickness : trackLength;
|
|
748
|
+
const halfW = trackWidth / 2;
|
|
749
|
+
const halfH = trackHeight / 2;
|
|
750
|
+
container.style.left = `${pixelX - halfW}px`;
|
|
751
|
+
container.style.top = `${pixelY - halfH}px`;
|
|
752
|
+
container.style.width = `${trackWidth}px`;
|
|
753
|
+
container.style.height = `${trackHeight}px`;
|
|
754
|
+
}
|
|
755
|
+
|
|
646
756
|
// src/renderer/rendering-engine.ts
|
|
647
757
|
function initializeRenderer(rootElement) {
|
|
648
758
|
const bridge = createIframeBridge();
|
|
@@ -739,6 +849,13 @@ function createElementByType(state, element, canvasWidth, canvasHeight) {
|
|
|
739
849
|
onDpadPress: (elementId, direction, actionKey) => state.bridge.emitDpadPress(elementId, direction, actionKey),
|
|
740
850
|
onDpadRelease: (elementId, direction, actionKey) => state.bridge.emitDpadRelease(elementId, direction, actionKey)
|
|
741
851
|
});
|
|
852
|
+
case "slider":
|
|
853
|
+
return createSliderElement({
|
|
854
|
+
element,
|
|
855
|
+
canvasWidth,
|
|
856
|
+
canvasHeight,
|
|
857
|
+
onSliderChange: (elementId, actionKey, value) => state.bridge.emitSliderChange(elementId, actionKey, value)
|
|
858
|
+
});
|
|
742
859
|
default:
|
|
743
860
|
return null;
|
|
744
861
|
}
|
|
@@ -773,17 +890,24 @@ function createControllerSDK(options) {
|
|
|
773
890
|
const iframe = createIframeElement(width, height);
|
|
774
891
|
container.appendChild(iframe);
|
|
775
892
|
let bridge = null;
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (
|
|
779
|
-
|
|
893
|
+
let configSent = false;
|
|
894
|
+
function sendConfigOnce() {
|
|
895
|
+
if (configSent || !bridge) return;
|
|
896
|
+
configSent = true;
|
|
897
|
+
bridge.sendConfig(config);
|
|
898
|
+
onReady?.();
|
|
899
|
+
}
|
|
900
|
+
bridge = createParentBridge(iframe);
|
|
901
|
+
if (onInput) {
|
|
902
|
+
bridge.onInput(onInput);
|
|
903
|
+
}
|
|
904
|
+
bridge.onMessage((message) => {
|
|
905
|
+
if (message.type === "RENDERER_READY" /* RENDERER_READY */) {
|
|
906
|
+
sendConfigOnce();
|
|
780
907
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
onReady?.();
|
|
785
|
-
}
|
|
786
|
-
});
|
|
908
|
+
});
|
|
909
|
+
iframe.addEventListener("load", () => {
|
|
910
|
+
sendConfigOnce();
|
|
787
911
|
});
|
|
788
912
|
if (iframeSrc) {
|
|
789
913
|
iframe.src = iframeSrc;
|
|
@@ -796,6 +920,7 @@ function createControllerSDK(options) {
|
|
|
796
920
|
},
|
|
797
921
|
destroy() {
|
|
798
922
|
bridge?.destroy();
|
|
923
|
+
bridge = null;
|
|
799
924
|
iframe.remove();
|
|
800
925
|
},
|
|
801
926
|
getIframe() {
|
|
@@ -995,6 +1120,50 @@ function getEmbeddedRendererScript() {
|
|
|
995
1120
|
canvas.appendChild(container);
|
|
996
1121
|
}
|
|
997
1122
|
|
|
1123
|
+
function renderSlider(el, cw, ch, canvas) {
|
|
1124
|
+
const ref = Math.min(cw, ch);
|
|
1125
|
+
const trackLen = percentageToPixel(el.length, ref);
|
|
1126
|
+
const trackThick = percentageToPixel(4, ref);
|
|
1127
|
+
const px = percentageToPixel(el.position.x, cw);
|
|
1128
|
+
const py = percentageToPixel(el.position.y, ch);
|
|
1129
|
+
const isH = el.orientation === 'horizontal';
|
|
1130
|
+
const tw = isH ? trackLen : trackThick;
|
|
1131
|
+
const th = isH ? trackThick : trackLen;
|
|
1132
|
+
const container = document.createElement('div');
|
|
1133
|
+
container.dataset.elementId = el.id;
|
|
1134
|
+
Object.assign(container.style, {
|
|
1135
|
+
position:'absolute', left:(px-tw/2)+'px', top:(py-th/2)+'px',
|
|
1136
|
+
width:tw+'px', height:th+'px', borderRadius:'8px',
|
|
1137
|
+
backgroundColor:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.08)',
|
|
1138
|
+
opacity:String(el.style?.opacity??0.85), zIndex:String(el.zIndex||1),
|
|
1139
|
+
touchAction:'none', userSelect:'none', boxSizing:'border-box'
|
|
1140
|
+
});
|
|
1141
|
+
const thumb = document.createElement('div');
|
|
1142
|
+
const thumbW = isH ? tw*0.2 : tw*0.7;
|
|
1143
|
+
const thumbH = isH ? th*0.7 : th*0.2;
|
|
1144
|
+
Object.assign(thumb.style, {
|
|
1145
|
+
position:'absolute', width:thumbW+'px', height:thumbH+'px',
|
|
1146
|
+
borderRadius:'6px', backgroundColor:el.style?.color||'#555',
|
|
1147
|
+
left:'50%', top:'50%', transform:'translate(-50%,-50%)',
|
|
1148
|
+
pointerEvents:'none', willChange:'left, top'
|
|
1149
|
+
});
|
|
1150
|
+
container.appendChild(thumb);
|
|
1151
|
+
const active = new Set();
|
|
1152
|
+
container.addEventListener('pointerdown', e => { e.preventDefault(); active.add(e.pointerId); container.setPointerCapture(e.pointerId); updateThumb(e); });
|
|
1153
|
+
container.addEventListener('pointermove', e => { if(!active.has(e.pointerId)) return; e.preventDefault(); updateThumb(e); });
|
|
1154
|
+
container.addEventListener('pointerup', e => { active.delete(e.pointerId); });
|
|
1155
|
+
container.addEventListener('pointercancel', e => { active.delete(e.pointerId); });
|
|
1156
|
+
function updateThumb(e) {
|
|
1157
|
+
const rect = container.getBoundingClientRect();
|
|
1158
|
+
let val;
|
|
1159
|
+
if(isH) { val = (e.clientX - rect.left) / rect.width; } else { val = (e.clientY - rect.top) / rect.height; }
|
|
1160
|
+
val = clamp(val, 0, 1);
|
|
1161
|
+
if(isH) thumb.style.left = (val*100)+'%'; else thumb.style.top = (val*100)+'%';
|
|
1162
|
+
emitToParent({type:MessageType.SLIDER_CHANGE,payload:{elementId:el.id,actionKey:el.actionKey,value:parseFloat(val.toFixed(4)),timestamp:Date.now()}});
|
|
1163
|
+
}
|
|
1164
|
+
canvas.appendChild(container);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
998
1167
|
function renderConfig(config) {
|
|
999
1168
|
const root = document.getElementById('controller-root');
|
|
1000
1169
|
root.innerHTML = '';
|
|
@@ -1012,6 +1181,7 @@ function getEmbeddedRendererScript() {
|
|
|
1012
1181
|
if(el.type==='button') renderButton(el, dims.width, dims.height, canvas);
|
|
1013
1182
|
else if(el.type==='joystick') renderJoystick(el, dims.width, dims.height, canvas);
|
|
1014
1183
|
else if(el.type==='dpad') renderDpad(el, dims.width, dims.height, canvas);
|
|
1184
|
+
else if(el.type==='slider') renderSlider(el, dims.width, dims.height, canvas);
|
|
1015
1185
|
}
|
|
1016
1186
|
}
|
|
1017
1187
|
|
|
@@ -1042,6 +1212,7 @@ export {
|
|
|
1042
1212
|
createIframeBridge,
|
|
1043
1213
|
createJoystickElement,
|
|
1044
1214
|
createParentBridge,
|
|
1215
|
+
createSliderElement,
|
|
1045
1216
|
destroyRenderer,
|
|
1046
1217
|
distance,
|
|
1047
1218
|
findAlignmentGuides,
|
|
@@ -1056,5 +1227,6 @@ export {
|
|
|
1056
1227
|
snapToGrid,
|
|
1057
1228
|
updateButtonElement,
|
|
1058
1229
|
updateDpadElement,
|
|
1059
|
-
updateJoystickElement
|
|
1230
|
+
updateJoystickElement,
|
|
1231
|
+
updateSliderElement
|
|
1060
1232
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksteinstudio/game-controller",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Universal Game Controller Engine - render interactive UI layouts via JSON configuration in an Iframe-based SDK. Percentage-based positioning, multi-touch, zero dependencies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|