@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 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
- iframe.addEventListener("load", () => {
832
- bridge = createParentBridge(iframe);
833
- if (onInput) {
834
- bridge.onInput(onInput);
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
- bridge.onMessage((message) => {
837
- if (message.type === "RENDERER_READY" /* RENDERER_READY */) {
838
- bridge?.sendConfig(config);
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
- iframe.addEventListener("load", () => {
777
- bridge = createParentBridge(iframe);
778
- if (onInput) {
779
- bridge.onInput(onInput);
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
- bridge.onMessage((message) => {
782
- if (message.type === "RENDERER_READY" /* RENDERER_READY */) {
783
- bridge?.sendConfig(config);
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.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",