@stanko/kaplay-inspector 0.0.4 → 0.1.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/README.md CHANGED
@@ -15,6 +15,8 @@ A dev tool for [Kaplay](https://kaplayjs.com/) which allows you to explore and i
15
15
  - Pause an object
16
16
  - Dark theme (is this a feature?)
17
17
 
18
+ The layout is made with desktop in mind. That said, it is somewhat usable on phones.
19
+
18
20
  ## Usage
19
21
 
20
22
  Install it:
@@ -50,6 +52,12 @@ if (
50
52
  }
51
53
  ```
52
54
 
55
+ If typescript is complaining about importing CSS files, you probably need to add this to one of your `.d.ts` files.
56
+
57
+ ```ts
58
+ declare module '*.scss';
59
+ ```
60
+
53
61
  ### Options
54
62
 
55
63
  You can pass options object to the init method as a second parameter:
@@ -64,22 +72,64 @@ available options are:
64
72
 
65
73
  ```ts
66
74
  interface InspectorOptions {
67
- updateTimeout?: number; // in milliseconds, default: 100
68
- isVisible?: boolean; // is inspector visible on load, default: true
69
- className?: string; // optional CSS class to add to the root element
75
+ // CSS class to add to the root element
76
+ className?: string;
77
+ // is inspector visible on load, default: true
78
+ isVisible?: boolean;
79
+ // default update time in milliseconds, default: 250
80
+ initUpdateTimeout?: number;
81
+ // should area, anchor and bounding box be drawn on object hover, default: true
82
+ shouldDrawInspect?: boolean:
83
+ }
84
+ ```
85
+
86
+ ## Customizing colors
87
+
88
+ Kaplay Inspector defines colors in [OKLCH color space](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/oklch). This makes changing of the primary and the secondary color pretty straight forward. You only need to update two hue variables like this:
89
+
90
+ ```css
91
+ .k-inspector.your-custom-class {
92
+ --ki-h: 300; /* Purple */
93
+ --ki-h-secondary: 200; /* Teal */
70
94
  }
71
95
  ```
72
96
 
73
- ### Positioning
97
+ Please note that if you load inspector's CSS dynamically, you'll have to add a custom class to create a higher specificity selector.
98
+
99
+ If you want to change other colors as well, check the [styles.css](./src/styles/styles.css).
100
+
101
+
102
+ ## Positioning
74
103
 
75
104
  By default, the inspector has `position: fixed` and it sits at the bottom of the screen. If you want to move it around, the easiest way it to pass a custom class name through the options and position it yourself.
76
105
 
106
+ Assuming we have only the canvas and the inspector element on the page, here is an example of what I like to do:
107
+
108
+ ```css
109
+ body:has(.k-inspector__hide) {
110
+ display: grid;
111
+ grid-template-rows: 60vh 40vh;
112
+
113
+ canvas {
114
+ width: 100% !important;
115
+ height: 60vh !important;
116
+ object-fit: contain;
117
+ display: block;
118
+ }
119
+
120
+ .k-inspector {
121
+ position: relative;
122
+ }
123
+ }
124
+ ```
125
+
126
+ This fixed the game canvas in the upper part of the viewport (60% of it) and the bottom part is taken by the inspector. It only applies this layout when inspector is visible (by checking if the hide button is shown).
127
+
128
+ Same as with colors, be sure to have a higher specificity selector if inspector's CSS is loaded dynamically.
77
129
 
78
130
  ## TODO
79
131
 
80
- * [ ] Bounding box - handle a case when anchor is a `Vec2`
81
132
  * [ ] Controllable theme - system/light/dark. At the moment it is always matching the system.
82
133
  * [ ] Filter/search
83
134
  * [ ] Persist search in URL or local storage
84
- * [ ] Figure out why child bounding box are drawn in the wrong position
85
135
  * [ ] Collapse/Expand all button
@@ -0,0 +1,11 @@
1
+ import type { GameObj } from "kaplay";
2
+ type CheckboxCompProps = {
3
+ obj: GameObj;
4
+ propName: string;
5
+ };
6
+ export declare const useObjectBoolean: (obj: GameObj, propName: string) => {
7
+ checked: any;
8
+ onChange: (checked: boolean) => void;
9
+ };
10
+ export declare const BooleanComp: ({ obj, propName }: CheckboxCompProps) => import("preact").JSX.Element;
11
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useEffect, useState } from "preact/hooks";
3
+ export const useObjectBoolean = (obj, propName) => {
4
+ const [checked, setChecked] = useState(obj[propName]);
5
+ useEffect(() => {
6
+ setChecked(obj[propName]);
7
+ }, [obj[propName]]);
8
+ const onChange = (checked) => {
9
+ setChecked(checked);
10
+ obj[propName] = checked;
11
+ };
12
+ return {
13
+ checked,
14
+ onChange,
15
+ };
16
+ };
17
+ export const BooleanComp = ({ obj, propName }) => {
18
+ const id = `${propName}-${obj.id}`;
19
+ const { checked, onChange } = useObjectBoolean(obj, propName);
20
+ return (_jsxs("div", { class: "game-object__comps-row", children: [_jsx("label", { for: id, children: _jsx("b", { children: propName }) }), _jsx("div", { children: _jsx("input", { id: id, type: "checkbox", checked: checked, onChange: (e) => onChange(e.target.checked) }) })] }));
21
+ };
@@ -0,0 +1,6 @@
1
+ import type { GameObj } from "kaplay";
2
+ export interface ColorProps {
3
+ className?: string;
4
+ obj: GameObj;
5
+ }
6
+ export declare const Color: ({ obj }: ColorProps) => import("preact").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useEffect, useState } from "preact/hooks";
3
+ import { cx } from "../lib/cx";
4
+ const ColorSlider = ({ onChange, value, channel }) => {
5
+ const handleChange = (e) => {
6
+ const target = e.target;
7
+ onChange(channel, parseInt(target.value));
8
+ };
9
+ return (_jsx("input", { className: cx("color-controls__slider-input", `color-controls__slider-input--${channel}`), type: "range", min: "0", max: "255", value: value, onInput: handleChange }));
10
+ };
11
+ export const Color = ({ obj }) => {
12
+ const object = obj;
13
+ const { r, g, b } = object.color;
14
+ const [color, setColor] = useState({ r, g, b });
15
+ useEffect(() => {
16
+ setColor({ r, g, b });
17
+ }, [r, g, b]);
18
+ const updateColor = (channel, value) => {
19
+ const newColor = { ...color, [channel]: value };
20
+ setColor(newColor);
21
+ object.color[channel] = value;
22
+ };
23
+ return (_jsxs("div", { class: "color-controls", children: [_jsx("div", { class: "color-controls__swatch", style: { background: `rgb(${color.r} ${color.g} ${color.b})` } }), _jsxs("div", { children: [_jsx(ColorSlider, { value: color.r, onChange: updateColor, channel: "r" }), _jsx(ColorSlider, { value: color.g, onChange: updateColor, channel: "g" }), _jsx(ColorSlider, { value: color.b, onChange: updateColor, channel: "b" })] }), _jsxs("div", { children: ["rgb(", color.r, ", ", color.g, ", ", color.b, ")"] })] }));
24
+ };
@@ -1,10 +1,12 @@
1
- import type { GameObj, KAPLAYCtx } from "kaplay";
1
+ import type { GameObj } from "kaplay";
2
+ import type { KAPLAYCtxType } from "../init";
2
3
  export interface GameObjectProps {
3
4
  className?: string;
4
5
  obj: GameObj;
5
6
  setRenderRoot: (obj: GameObj) => void;
6
7
  isExpanded?: boolean;
7
8
  isRenderRoot?: boolean;
8
- k: KAPLAYCtx;
9
+ shouldDrawInspect: boolean;
10
+ k: KAPLAYCtxType;
9
11
  }
10
- export declare const GameObject: ({ obj, className, isExpanded: isExpandedExternal, isRenderRoot, setRenderRoot, k, }: GameObjectProps) => import("preact").JSX.Element;
12
+ export declare const GameObject: ({ obj, className, isExpanded: isExpandedExternal, isRenderRoot, setRenderRoot, shouldDrawInspect, k, }: GameObjectProps) => import("preact").JSX.Element;
@@ -1,42 +1,59 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { useEffect, useRef, useState } from "preact/hooks";
2
+ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
3
3
  import { MinusIcon, PlusIcon } from "../components/icons";
4
4
  import { cx } from "../lib/cx";
5
5
  import { drawBoundingBox } from "../lib/draw-bbox";
6
6
  import { getObjectInfo } from "../lib/get-object-info";
7
7
  import { Breadcrumbs } from "./breadcrumbs";
8
- export const GameObject = ({ obj, className = "", isExpanded: isExpandedExternal = false, isRenderRoot, setRenderRoot, k, }) => {
8
+ import { BooleanComp } from "./boolean-comp";
9
+ export const GameObject = ({ obj, className = "", isExpanded: isExpandedExternal = false, isRenderRoot, setRenderRoot, shouldDrawInspect, k, }) => {
9
10
  const [isExpanded, setIsExpanded] = useState(isExpandedExternal);
10
- const mouseHoverController = useRef(null);
11
+ const updateControllers = useRef([]);
11
12
  const { compsData, tags, compsLabel } = getObjectInfo(obj);
12
13
  const isRootObject = obj.id === 0;
13
14
  const isObjectDestroyed = !obj.exists() && !isRootObject;
14
15
  const showExpandTree = obj.children.length > 0;
15
16
  const hasChildren = obj.children.length > 0;
16
17
  const isInspecting = isRenderRoot && obj.id !== 0;
18
+ const cancelUpdateControllers = useCallback(() => {
19
+ updateControllers.current.forEach((controller) => controller.cancel());
20
+ updateControllers.current = [];
21
+ }, []);
22
+ const drawInspect = useCallback((obj, isChild = false) => {
23
+ if (!obj.hidden) {
24
+ const updateController = obj.onDraw(() => {
25
+ // Kaplay calls drawInspect on all of the children, no need to call it again
26
+ if (!isChild) {
27
+ obj.drawInspect();
28
+ }
29
+ drawBoundingBox(obj, k);
30
+ });
31
+ updateControllers.current.push(updateController);
32
+ obj.children.forEach((child) => {
33
+ drawInspect(child, true);
34
+ });
35
+ }
36
+ }, []);
17
37
  useEffect(() => {
18
38
  return () => {
19
- mouseHoverController.current?.cancel();
39
+ cancelUpdateControllers();
20
40
  };
21
41
  }, []);
22
42
  const handleToggleClick = () => {
23
43
  setIsExpanded(!isExpanded);
24
44
  };
25
45
  const handleMouseEnter = () => {
26
- mouseHoverController.current?.cancel();
27
- if (!obj.hidden) {
28
- mouseHoverController.current = obj.onDraw(() => {
29
- obj.drawInspect();
30
- drawBoundingBox(obj, k);
31
- });
46
+ cancelUpdateControllers();
47
+ if (!isRootObject && shouldDrawInspect) {
48
+ drawInspect(obj);
32
49
  }
33
50
  };
34
51
  const handleMouseLeave = () => {
35
- mouseHoverController.current?.cancel();
52
+ cancelUpdateControllers();
36
53
  };
37
54
  return (_jsxs("div", { class: cx("game-object", className, {
38
55
  "game-object--no-children": !hasChildren,
39
56
  }), children: [isInspecting && _jsx(Breadcrumbs, { setRenderRoot: setRenderRoot, obj: obj }), _jsxs("div", { class: "game-object__content", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [_jsxs("button", { class: cx("game-object__header", {
40
57
  "game-object__header--expandable": showExpandTree,
41
- }), onClick: handleToggleClick, children: [_jsx("span", { class: "game-object__expand-icon", children: isExpanded ? _jsx(MinusIcon, {}) : _jsx(PlusIcon, {}) }), _jsxs("div", { class: "game-object__id", children: ["ID ", obj.id, ":"] }), tags ? (_jsx("div", { class: "game-object__tags", children: isRootObject ? "Root" : tags })) : (_jsx("div", { class: "game-object__comp-names", children: compsLabel })), obj.children.length > 0 && _jsxs("div", { children: ["(", obj.children.length, ")"] }), isObjectDestroyed && (_jsx("div", { class: "game-object__destroyed", children: "DESTROYED" }))] }), _jsxs("div", { class: "game-object__buttons", children: [!isRenderRoot && (_jsx("button", { class: "ki-btn", onClick: () => setRenderRoot(obj), children: "inspect" })), _jsx("button", { class: "ki-btn ", onClick: () => console.log(obj), children: "log" })] }), isExpanded && (_jsx("div", { class: "game-object__comps-wrapper", children: _jsxs("div", { class: "game-object__comps", children: [_jsxs("div", { class: "game-object__comps-row", children: [_jsx("label", { for: `paused-${obj.id}`, children: _jsx("b", { children: "paused" }) }), _jsx("div", { children: _jsx("input", { id: `paused-${obj.id}`, type: "checkbox", defaultChecked: obj.paused, onChange: () => (obj.paused = !obj.paused) }) })] }), compsData.map((comp) => (_jsxs("div", { class: "game-object__comps-row", children: [_jsx("div", { children: _jsx("b", { children: comp.tag }) }), _jsx("div", { children: comp.value })] }, comp.tag)))] }) }))] }), isExpanded && hasChildren && (_jsx("div", { class: "game-object__children", style: { display: isExpanded ? "block" : "none" }, children: obj.children.map((child) => (_jsx(GameObject, { k: k, obj: child, setRenderRoot: setRenderRoot }, child.id))) }))] }, obj.id));
58
+ }), onClick: handleToggleClick, children: [isExpanded ? (_jsx(MinusIcon, { className: "game-object__expand-icon" })) : (_jsx(PlusIcon, { className: "game-object__expand-icon" })), _jsxs("div", { class: "game-object__id", children: ["ID ", obj.id, ":"] }), tags ? (_jsx("div", { class: "game-object__tags", children: isRootObject ? "Root" : tags })) : (_jsx("div", { class: "game-object__comp-names", children: compsLabel })), obj.children.length > 0 && _jsxs("div", { children: ["(", obj.children.length, ")"] }), isObjectDestroyed && (_jsx("div", { class: "game-object__destroyed", children: "DESTROYED" }))] }), _jsxs("div", { class: "game-object__buttons", children: [!isRenderRoot && (_jsx("button", { class: "ki-btn", onClick: () => setRenderRoot(obj), children: "inspect" })), _jsx("button", { class: "ki-btn ", onClick: () => console.log(obj), children: "log" })] }), isExpanded && (_jsx("div", { class: "game-object__comps-wrapper", children: _jsxs("div", { class: "game-object__comps", children: [_jsx(BooleanComp, { obj: obj, propName: "paused" }), _jsx(BooleanComp, { obj: obj, propName: "hidden" }), compsData.map((comp) => (_jsxs("div", { class: "game-object__comps-row", children: [_jsx("div", { children: _jsx("b", { children: comp.tag }) }), _jsx("div", { children: comp.value })] }, comp.tag)))] }) }))] }), isExpanded && hasChildren && (_jsx("div", { class: "game-object__children", style: { display: isExpanded ? "block" : "none" }, children: obj.children.map((child) => (_jsx(GameObject, { k: k, obj: child, setRenderRoot: setRenderRoot, shouldDrawInspect: shouldDrawInspect }, child.id))) }))] }, obj.id));
42
59
  };
@@ -0,0 +1,9 @@
1
+ import type { JSX } from "preact/jsx-runtime";
2
+ export type HoldButtonProps = {
3
+ children?: JSX.Element | string | number;
4
+ className?: string;
5
+ onClickAndHold: () => void;
6
+ interval?: number;
7
+ startRepeatingDelay?: number;
8
+ };
9
+ export declare const HoldButton: ({ children, className, onClickAndHold, interval, startRepeatingDelay, }: HoldButtonProps) => JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "preact/hooks";
3
+ export const HoldButton = ({ children, className, onClickAndHold, interval = 50, startRepeatingDelay = 200, }) => {
4
+ const intervalRef = useRef();
5
+ const timeoutRef = useRef();
6
+ const [isMouseDown, setIsMouseDown] = useState(false);
7
+ useEffect(() => {
8
+ document.addEventListener("mouseup", handleMouseUp);
9
+ return () => {
10
+ clearTimeout(timeoutRef.current);
11
+ clearInterval(intervalRef.current);
12
+ document.removeEventListener("mouseup", handleMouseUp);
13
+ };
14
+ }, []);
15
+ useEffect(() => {
16
+ clearTimeout(timeoutRef.current);
17
+ clearInterval(intervalRef.current);
18
+ if (isMouseDown) {
19
+ timeoutRef.current = setTimeout(() => {
20
+ intervalRef.current = setInterval(() => {
21
+ onClickAndHold();
22
+ }, interval);
23
+ }, startRepeatingDelay);
24
+ }
25
+ }, [isMouseDown]);
26
+ const handleMouseDown = () => {
27
+ setIsMouseDown(true);
28
+ };
29
+ const handleMouseUp = () => {
30
+ setIsMouseDown(false);
31
+ };
32
+ return (_jsx("button", { className: className, onMouseDown: handleMouseDown, onClick: onClickAndHold, children: children }));
33
+ };
@@ -1,6 +1,10 @@
1
- export declare const PlusIcon: () => import("preact").JSX.Element;
2
- export declare const MinusIcon: () => import("preact").JSX.Element;
3
- export declare const ArrowUpIcon: () => import("preact").JSX.Element;
4
- export declare const ArrowDownIcon: () => import("preact").JSX.Element;
5
- export declare const ArrowLeftIcon: () => import("preact").JSX.Element;
6
- export declare const ArrowRightIcon: () => import("preact").JSX.Element;
1
+ type IconProps = {
2
+ className?: string;
3
+ };
4
+ export declare const PlusIcon: ({ className }: IconProps) => import("preact").JSX.Element;
5
+ export declare const MinusIcon: ({ className }: IconProps) => import("preact").JSX.Element;
6
+ export declare const ArrowUpIcon: ({ className }: IconProps) => import("preact").JSX.Element;
7
+ export declare const ArrowDownIcon: ({ className }: IconProps) => import("preact").JSX.Element;
8
+ export declare const ArrowLeftIcon: ({ className }: IconProps) => import("preact").JSX.Element;
9
+ export declare const ArrowRightIcon: ({ className }: IconProps) => import("preact").JSX.Element;
10
+ export {};
@@ -1,19 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- export const PlusIcon = () => {
3
- return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", xmlns: "http://www.w3.org/2000/svg", children: _jsxs("g", { fill: "none", "stroke-width": "1.25", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: [_jsx("rect", { width: "14", height: "14", x: "1", y: "1", rx: "4" }), _jsx("path", { d: "M 8 4 V 12 M 4 8 H12" })] }) }));
2
+ export const PlusIcon = ({ className = "" }) => {
3
+ return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", xmlns: "http://www.w3.org/2000/svg", class: className, children: _jsxs("g", { fill: "none", "stroke-width": "1.25", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: [_jsx("rect", { width: "14", height: "14", x: "1", y: "1", rx: "4" }), _jsx("path", { d: "M 8 4 V 12 M 4 8 H12" })] }) }));
4
4
  };
5
- export const MinusIcon = () => {
6
- return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsxs("g", { fill: "none", "stroke-width": "1.25", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: [_jsx("rect", { width: "14", height: "14", x: "1", y: "1", rx: "4" }), _jsx("path", { d: "M 4 8 H 12" })] }) }));
5
+ export const MinusIcon = ({ className = "" }) => {
6
+ return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: className, children: _jsxs("g", { fill: "none", "stroke-width": "1.25", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: [_jsx("rect", { width: "14", height: "14", x: "1", y: "1", rx: "4" }), _jsx("path", { d: "M 4 8 H 12" })] }) }));
7
7
  };
8
- export const ArrowUpIcon = () => {
9
- return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 3 10 L 8 6 L 13 10" }) }) }));
8
+ export const ArrowUpIcon = ({ className = "" }) => {
9
+ return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: className, children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 3 10 L 8 6 L 13 10" }) }) }));
10
10
  };
11
- export const ArrowDownIcon = () => {
12
- return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 3 6 L 8 10 L 13 6" }) }) }));
11
+ export const ArrowDownIcon = ({ className = "" }) => {
12
+ return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: className, children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 3 6 L 8 10 L 13 6" }) }) }));
13
13
  };
14
- export const ArrowLeftIcon = () => {
15
- return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 10 3 L 6 8 L 10 13" }) }) }));
14
+ export const ArrowLeftIcon = ({ className = "" }) => {
15
+ return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: className, children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 10 3 L 6 8 L 10 13" }) }) }));
16
16
  };
17
- export const ArrowRightIcon = () => {
18
- return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 6 3 L 10 8 L 6 13" }) }) }));
17
+ export const ArrowRightIcon = ({ className = "" }) => {
18
+ return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: className, children: _jsx("g", { fill: "none", stroke: "currentColor", "stroke-linejoin": "round", "stroke-linecap": "round", children: _jsx("path", { d: "M 6 3 L 10 8 L 6 13" }) }) }));
19
19
  };
@@ -1,6 +1,5 @@
1
- import type { KAPLAYCtx } from "kaplay";
2
- import type { InspectorOptions } from "../init";
1
+ import type { InspectorOptions, KAPLAYCtxType } from "../init";
3
2
  export interface InspectorProps extends InspectorOptions {
4
- k: KAPLAYCtx;
3
+ k: KAPLAYCtxType;
5
4
  }
6
- export declare const Inspector: ({ updateTimeout, isVisibleOnLoad, k, }: InspectorProps) => import("preact").JSX.Element;
5
+ export declare const Inspector: ({ initUpdateTimeout, isVisibleOnLoad, initDrawInspectOnHover, k, }: InspectorProps) => import("preact").JSX.Element;
@@ -1,10 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
2
  import { useEffect, useState } from "preact/hooks";
3
3
  import { GameObject } from "./game-object";
4
- export const Inspector = ({ updateTimeout = 250, isVisibleOnLoad = true, k, }) => {
4
+ import { useObjectBoolean } from "./boolean-comp";
5
+ const INTERVAL_OPTIONS = [
6
+ { value: 100, label: "100ms" },
7
+ { value: 250, label: "250ms" },
8
+ { value: 500, label: "500ms" },
9
+ { value: 1000, label: "1s" },
10
+ ];
11
+ export const Inspector = ({ initUpdateTimeout = 250, isVisibleOnLoad = true, initDrawInspectOnHover = true, k, }) => {
12
+ const [updateTimeout, setUpdateTimeout] = useState(initUpdateTimeout);
13
+ const [shouldDrawInspect, setShouldDrawInspect] = useState(initDrawInspectOnHover);
5
14
  const [root, setRoot] = useState(k.getTreeRoot());
6
15
  const [renderIndex, setRenderIndex] = useState(0);
7
16
  const [isVisible, setIsVisible] = useState(isVisibleOnLoad);
17
+ const paused = useObjectBoolean(k.getTreeRoot(), "paused");
8
18
  // Force re-render every updateTimeout milliseconds
9
19
  useEffect(() => {
10
20
  const interval = setInterval(() => {
@@ -12,15 +22,15 @@ export const Inspector = ({ updateTimeout = 250, isVisibleOnLoad = true, k, }) =
12
22
  }, updateTimeout);
13
23
  return () => clearInterval(interval);
14
24
  }, [renderIndex, updateTimeout]);
15
- const handlePauseClick = () => {
16
- const root = k.getTreeRoot();
17
- root.paused = !root.paused;
18
- };
25
+ // const handlePauseClick = () => {
26
+ // const root = k.getTreeRoot();
27
+ // root.paused = !root.paused;
28
+ // };
19
29
  const toggleVisibility = () => {
20
30
  setIsVisible(!isVisible);
21
31
  };
22
32
  if (!isVisible) {
23
33
  return (_jsx("button", { class: "ki-btn k-inspector__show", onClick: toggleVisibility, children: "Show Inspector" }));
24
34
  }
25
- return (_jsxs(_Fragment, { children: [_jsxs("div", { class: "k-inspector__header", children: [_jsx("button", { class: "ki-btn", onClick: handlePauseClick, children: "Pause/Resume" }), "\u2022", _jsxs("div", { children: [k.get("*", { recursive: true }).length, " objects"] }), "\u2022", _jsxs("div", { children: [Math.round(k.debug.fps()), " fps"] }), _jsx("button", { class: "ki-btn k-inspector__hide", onClick: toggleVisibility, children: "Hide" })] }), _jsx("div", { class: "k-inspector__objects", children: _jsx(GameObject, { k: k, className: "game-object--root", obj: root, setRenderRoot: setRoot, isExpanded: true, isRenderRoot: true }) })] }));
35
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { class: "k-inspector__header", children: [_jsx("button", { class: "ki-btn", onClick: () => paused.onChange(!paused.checked), children: paused.checked ? "Resume Game" : "Pause Game" }), "\u2022", _jsxs("div", { children: [k.get("*", { recursive: true }).length, " objects"] }), "\u2022", _jsxs("div", { children: [Math.round(k.debug.fps()), " fps"] }), "\u2022", _jsxs("div", { class: "k-inspector__interval", children: ["Update:", INTERVAL_OPTIONS.map((option) => (_jsxs("label", { children: [_jsx("input", { type: "radio", name: "interval", value: option.value, checked: updateTimeout === option.value, onChange: () => setUpdateTimeout(option.value) }), option.label] }, option.value)))] }), "\u2022", _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: shouldDrawInspect, onChange: () => setShouldDrawInspect(!shouldDrawInspect) }), "Draw bbox on hover"] }), _jsx("button", { class: "ki-btn k-inspector__hide", onClick: toggleVisibility, children: "Hide" })] }), _jsx("div", { class: "k-inspector__objects", children: _jsx(GameObject, { k: k, className: "game-object--root", obj: root, setRenderRoot: setRoot, shouldDrawInspect: shouldDrawInspect, isExpanded: true, isRenderRoot: true }) })] }));
26
36
  };
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  import { roundToDecimal } from "../lib/round-to-decimal";
3
+ import { HoldButton } from "./hold-button";
3
4
  import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, } from "./icons";
4
5
  export const PositionControls = ({ obj }) => {
5
6
  if (!obj.pos) {
6
7
  return null;
7
8
  }
8
- return (_jsxs("div", { class: "pos-controls", children: [_jsx("button", { class: "ki-btn", onClick: () => (obj.pos.x -= 1), children: _jsx(ArrowLeftIcon, {}) }), _jsx("button", { class: "ki-btn", onClick: () => (obj.pos.x += 1), children: _jsx(ArrowRightIcon, {}) }), _jsxs("div", { class: "pos-controls__value", children: ["x: ", roundToDecimal(obj.pos.x, 2)] }), _jsxs("div", { class: "pos-controls__value", children: ["y: ", roundToDecimal(obj.pos.y, 2)] }), _jsx("button", { class: "ki-btn", onClick: () => (obj.pos.y -= 1), children: _jsx(ArrowUpIcon, {}) }), _jsx("button", { class: "ki-btn", onClick: () => (obj.pos.y += 1), children: _jsx(ArrowDownIcon, {}) })] }));
9
+ return (_jsxs("div", { class: "pos-controls", children: [_jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => (obj.pos.x -= 1), children: _jsx(ArrowLeftIcon, {}) }), _jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => (obj.pos.x += 1), children: _jsx(ArrowRightIcon, {}) }), _jsxs("div", { class: "pos-controls__value", children: ["x: ", roundToDecimal(obj.pos.x, 2)] }), _jsxs("div", { class: "pos-controls__value", children: ["y: ", roundToDecimal(obj.pos.y, 2)] }), _jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => (obj.pos.y -= 1), children: _jsx(ArrowUpIcon, {}) }), _jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => (obj.pos.y += 1), children: _jsx(ArrowDownIcon, {}) })] }));
9
10
  };
@@ -0,0 +1,6 @@
1
+ import type { GameObj } from "kaplay";
2
+ export interface SpriteControlsProps {
3
+ className?: string;
4
+ obj: GameObj;
5
+ }
6
+ export declare const SpriteControls: ({ obj }: SpriteControlsProps) => import("preact").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ export const SpriteControls = ({ obj }) => {
3
+ const object = obj;
4
+ const animation = object.getCurAnim();
5
+ return (_jsxs("div", { class: "sprite-controls", children: [_jsx("b", { children: object.sprite }), _jsxs("div", { children: ["frame: ", object.frame] }), animation && (_jsxs(_Fragment, { children: [_jsxs("div", { children: ["animation: ", animation.name] }), _jsxs("div", { children: ["animation frame: ", animation?.frameIndex] })] }))] }));
6
+ };
@@ -1,9 +1,16 @@
1
1
  import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { useEffect, useState } from "preact/hooks";
2
3
  export const TextControls = ({ obj }) => {
4
+ const [text, setText] = useState(obj.text);
5
+ useEffect(() => {
6
+ setText(obj.text);
7
+ }, [obj.text]);
3
8
  if (typeof obj.text !== "string") {
4
9
  return null;
5
10
  }
6
- return (_jsx("div", { class: "text-controls", children: _jsx("input", { class: "text-controls__input", type: "text", placeholder: "Enter text", defaultValue: obj.text, onInput: (e) => {
7
- obj.text = e.target.value;
8
- } }) }));
11
+ const handleInput = (e) => {
12
+ obj.text = e.target.value;
13
+ setText(text);
14
+ };
15
+ return (_jsx("div", { class: "text-controls", children: _jsx("input", { class: "text-controls__input", type: "text", placeholder: "Enter text", value: text, onInput: handleInput }) }));
9
16
  };
package/dist/init.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import type { KAPLAYCtx } from "kaplay";
1
+ import kaplay from "kaplay";
2
+ export type KAPLAYCtxType = ReturnType<typeof kaplay>;
2
3
  export interface InspectorOptions {
3
- updateTimeout?: number;
4
+ initUpdateTimeout?: number;
5
+ initDrawInspectOnHover?: boolean;
4
6
  isVisibleOnLoad?: boolean;
5
7
  className?: string;
6
8
  }
7
- export default function init(k: KAPLAYCtx, props?: InspectorOptions): void;
9
+ export default function init(k: KAPLAYCtxType, props?: InspectorOptions): void;
package/dist/init.js CHANGED
@@ -2,11 +2,9 @@ import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { render } from "preact";
3
3
  import { Inspector } from "./components/inspector";
4
4
  export default function init(k, props = {}) {
5
- const {
6
- //
7
- className = "", updateTimeout = 100, isVisibleOnLoad = true, } = props;
5
+ const { className = "", initUpdateTimeout = 250, isVisibleOnLoad = true, initDrawInspectOnHover = true, } = props;
8
6
  const appElement = document.createElement("div");
9
7
  appElement.className = `k-inspector ${className}`;
10
8
  document.body.appendChild(appElement);
11
- render(_jsx(Inspector, { k: k, updateTimeout: updateTimeout, isVisibleOnLoad: isVisibleOnLoad }), appElement);
9
+ render(_jsx(Inspector, { k: k, initUpdateTimeout: initUpdateTimeout, isVisibleOnLoad: isVisibleOnLoad, initDrawInspectOnHover: initDrawInspectOnHover }), appElement);
12
10
  }
@@ -1,2 +1,3 @@
1
- import type { GameObj, KAPLAYCtx } from "kaplay";
2
- export declare const drawBoundingBox: (obj: GameObj, k: KAPLAYCtx) => void;
1
+ import type { GameObj } from "kaplay";
2
+ import type { KAPLAYCtxType } from "../init";
3
+ export declare const drawBoundingBox: (obj: GameObj, k: KAPLAYCtxType) => void;
@@ -1,26 +1,31 @@
1
1
  export const drawBoundingBox = (obj, k) => {
2
- // TODO handle a case when anchor is a vec2
3
- const anchor = obj.anchor || "topleft";
4
- if (obj.renderArea && obj.has("anchor")) {
5
- const offset = k.vec2(0);
2
+ if (obj.renderArea) {
6
3
  const rect = obj.renderArea().bbox();
7
- if (anchor.includes("left")) {
8
- offset.x = 0;
9
- }
10
- else if (anchor.includes("right")) {
11
- offset.x = rect.width;
12
- }
13
- else {
14
- offset.x = rect.width / 2;
15
- }
16
- if (anchor.includes("top")) {
17
- offset.y = 0;
18
- }
19
- else if (anchor.includes("bot")) {
20
- offset.y = rect.height;
4
+ const anchor = obj.anchor || "topleft";
5
+ const offset = k.vec2(0);
6
+ if (typeof anchor === "string") {
7
+ if (anchor.includes("left")) {
8
+ offset.x = 0;
9
+ }
10
+ else if (anchor.includes("right")) {
11
+ offset.x = rect.width;
12
+ }
13
+ else {
14
+ offset.x = rect.width / 2;
15
+ }
16
+ if (anchor.includes("top")) {
17
+ offset.y = 0;
18
+ }
19
+ else if (anchor.includes("bot")) {
20
+ offset.y = rect.height;
21
+ }
22
+ else {
23
+ offset.y = rect.height / 2;
24
+ }
21
25
  }
22
26
  else {
23
- offset.y = rect.height / 2;
27
+ offset.x = (anchor.x * rect.width + 1) / 2 + rect.width / 2;
28
+ offset.y = (anchor.y * rect.height + 1) / 2 + rect.height / 2;
24
29
  }
25
30
  rect.pos = rect.pos.sub(offset);
26
31
  k.drawRect({
@@ -32,7 +37,5 @@ export const drawBoundingBox = (obj, k) => {
32
37
  opacity: 0.75,
33
38
  },
34
39
  });
35
- // TODO figure out why these positions are wrong
36
- // obj.children.forEach((child) => drawBoundingBox(child, k));
37
40
  }
38
41
  };
@@ -2,7 +2,7 @@ import type { GameObj } from "kaplay";
2
2
  export declare const getObjectInfo: (obj: GameObj) => {
3
3
  compsData: {
4
4
  tag: string;
5
- value?: string | import("preact").JSX.Element;
5
+ value?: string | import("preact").JSX.Element | null;
6
6
  }[];
7
7
  tags: string;
8
8
  compsLabel: string;
@@ -2,5 +2,5 @@ import type { GameObj } from "kaplay";
2
2
  import type { JSX } from "preact";
3
3
  export declare const inspectComps: (obj: GameObj) => {
4
4
  tag: string;
5
- value?: string | JSX.Element;
5
+ value?: string | JSX.Element | null;
6
6
  }[];
@@ -2,60 +2,74 @@ import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { stringify } from "./stringify";
3
3
  import { PositionControls } from "../components/position-controls";
4
4
  import { TextControls } from "../components/text-controls";
5
+ import { SpriteControls } from "../components/sprite-controls";
6
+ import { Color } from "../components/color-controls";
7
+ const componentMap = {
8
+ pos: PositionControls,
9
+ text: TextControls,
10
+ sprite: SpriteControls,
11
+ color: Color,
12
+ };
5
13
  export const inspectComps = (obj) => {
6
- const info = {};
7
- for (const [tag, comp] of obj._compStates) {
8
- info[tag] = comp.inspect?.() ?? null;
14
+ const object = obj;
15
+ const data = [];
16
+ for (const [tag, comp] of object._compStates) {
17
+ if (componentMap[tag]) {
18
+ const CompComponent = componentMap[tag];
19
+ data.push({
20
+ tag,
21
+ value: _jsx(CompComponent, { obj: obj }),
22
+ });
23
+ }
24
+ else if (comp.inspect) {
25
+ const value = comp.inspect();
26
+ data.push({
27
+ tag,
28
+ // Remove component name if it is present in the inspect result.
29
+ // Native Kaplay components are doing this,
30
+ // and because we are displaying the name in the left column already,
31
+ // we don't need to display it again.
32
+ value: value ? value.replace(`${tag}: `, "") : "",
33
+ });
34
+ }
35
+ else {
36
+ data.push({
37
+ tag,
38
+ // Commented out on purpose
39
+ // For now, only the name of the component is shown,
40
+ // until I try it out and figure if it would be useful to display the full component state
41
+ // value: stringify(comp),
42
+ });
43
+ }
9
44
  }
10
- for (const [i, comp] of obj._anonymousCompStates.entries()) {
45
+ for (const [i, comp] of object._anonymousCompStates.entries()) {
11
46
  if (comp.inspect) {
12
- info[i] = comp.inspect();
47
+ data.push({
48
+ tag: `anonymous ${i}`,
49
+ value: comp.inspect(),
50
+ });
13
51
  continue;
14
52
  }
15
53
  for (const [key, value] of Object.entries(comp)) {
16
54
  if (typeof value === "function") {
17
- info[key] = `${key}: function`;
18
- }
19
- else if (typeof value === "object") {
20
- info[key] = `${key}: ${stringify(value)}`;
21
- }
22
- else {
23
- info[key] = `${key}: ${value}`;
24
- }
25
- }
26
- }
27
- const lines = [];
28
- for (const tag in info) {
29
- if (info[tag]) {
30
- if (tag === "pos") {
31
- // Custom component for position
32
- lines.push({
33
- tag,
34
- value: _jsx(PositionControls, { obj: obj }),
55
+ data.push({
56
+ tag: key,
57
+ value: "function",
35
58
  });
36
59
  }
37
- else {
38
- lines.push({
39
- tag,
40
- value: info[tag].replace(`${tag}: `, ""),
41
- });
42
- }
43
- }
44
- else {
45
- if (tag === "text") {
46
- // Custom component for text
47
- lines.push({
48
- tag,
49
- value: _jsx(TextControls, { obj: obj }),
60
+ else if (typeof value === "object") {
61
+ data.push({
62
+ tag: key,
63
+ value: stringify(value),
50
64
  });
51
65
  }
52
66
  else {
53
- // pushes only the tag (name of the component)
54
- lines.push({
55
- tag,
67
+ data.push({
68
+ tag: key,
69
+ value,
56
70
  });
57
71
  }
58
72
  }
59
73
  }
60
- return lines.sort((a, b) => a.tag.localeCompare(b.tag));
74
+ return data.sort((a, b) => a.tag.localeCompare(b.tag));
61
75
  };
@@ -1,6 +1,6 @@
1
1
  export const stringify = (obj, maxDepth = 1, currentDepth = 0) => {
2
2
  if (typeof obj !== "object" || obj === null) {
3
- return obj.toString();
3
+ return JSON.stringify(obj);
4
4
  }
5
5
  const lines = Object.entries(obj).map(([key, value]) => {
6
6
  if (typeof value === "function") {
package/dist/styles.css CHANGED
@@ -113,6 +113,8 @@
113
113
  background-color: var(--ki-bg);
114
114
  border-bottom: 1px solid var(--ki-gray-light);
115
115
  border-top: 1px solid var(--ki-gray-light);
116
+ flex-wrap: wrap;
117
+ font-size: 0.75rem;
116
118
  }
117
119
 
118
120
  .k-inspector__objects {
@@ -129,6 +131,18 @@
129
131
  right: 0.5rem;
130
132
  }
131
133
 
134
+ .k-inspector__interval {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 0.5rem;
138
+ }
139
+
140
+ .k-inspector__header label {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 0.25rem;
144
+ }
145
+
132
146
  /* Game object */
133
147
 
134
148
  .game-object {
@@ -312,14 +326,49 @@
312
326
  outline: none;
313
327
  border-color: var(--ki-primary);
314
328
  }
315
- }
316
329
 
317
- /* Breadcrumbs */
330
+ /* Color */
331
+
332
+ .color-controls {
333
+ display: flex;
334
+ align-items: center;
335
+ gap: 0.5rem;
336
+ width: fit-content;
337
+ min-width: 12rem;
338
+ }
339
+
340
+ .color-controls__swatch {
341
+ width: 2rem;
342
+ height: 2rem;
343
+ border-radius: 8px;
344
+ border: 1px solid var(--ki-border-light);
345
+ }
346
+
347
+ .color-controls__slider {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 0.25rem;
351
+ }
352
+
353
+ .color-controls__slider-input--r {
354
+ accent-color: red;
355
+ }
318
356
 
319
- .breadcrumbs {
320
- font-size: 0.75rem;
321
- display: flex;
322
- margin-bottom: 0.5rem;
323
- gap: 0.25rem;
324
- align-items: center;
357
+ .color-controls__slider-input--g {
358
+ accent-color: green;
359
+ }
360
+
361
+ .color-controls__slider-input--b {
362
+ accent-color: blue;
363
+ }
364
+
365
+ /* Breadcrumbs */
366
+
367
+ .breadcrumbs {
368
+ font-size: 0.75rem;
369
+ display: flex;
370
+ margin-bottom: 0.5rem;
371
+ gap: 0.5rem;
372
+ align-items: center;
373
+ }
325
374
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stanko/kaplay-inspector",
3
3
  "description": "A dev tool for Kaplay which allows you to explore and inspect the game object tree real time.",
4
4
  "private": false,
5
- "version": "0.0.4",
5
+ "version": "0.1.1",
6
6
  "type": "module",
7
7
  "main": "./dist/init.js",
8
8
  "types": "./dist/init.d.ts",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "devDependencies": {
30
30
  "@preact/preset-vite": "^2.10.2",
31
- "@types/node": "^25.0.2",
31
+ "@types/node": "^25.0.3",
32
32
  "typescript": "~5.9.3",
33
33
  "vite": "^7.3.0"
34
34
  },
Binary file