react-godot-shader-preview 0.5.0 → 0.5.2
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 +5 -1
- package/dist/GodotMaterialPreview.css +43 -8
- package/dist/GodotMaterialPreview.d.ts +1 -0
- package/dist/GodotMaterialPreview.js +3 -0
- package/dist/GodotMaterialPreview.types.d.ts +5 -0
- package/dist/GodotMaterialPreview.types.js +2 -0
- package/dist/GodotMaterialPreviewView.d.ts +2 -1
- package/dist/GodotMaterialPreviewView.js +4 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/style.css +43 -8
- package/godot/embed.html +4 -2
- package/godot/index.html +1 -1
- package/package.json +22 -5
- package/scripts/copy-css.mjs +11 -0
- package/scripts/copy-embed.mjs +37 -0
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ yarn add react-godot-shader-preview
|
|
|
10
10
|
npm install react-godot-shader-preview
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
On install, the package **postinstall** copies the Godot export from `node_modules/react-godot-shader-preview/godot` into your project root as **`react_godot_shader_preview_embed/`**. Serve that folder (e.g. as static assets) and pass its embed URL to the component (see `embedUrl` below). If you use a different path or host the embed elsewhere, pass that URL instead.
|
|
14
|
+
|
|
13
15
|
## Usage
|
|
14
16
|
|
|
15
17
|
Import the component and styles:
|
|
@@ -45,6 +47,7 @@ function App() {
|
|
|
45
47
|
|
|
46
48
|
| Prop | Type | Default | Description |
|
|
47
49
|
|------|------|---------|-------------|
|
|
50
|
+
| `embedUrl` | `string` | `"/react_godot_shader_preview_embed/embed.html"` | URL of the Godot embed page. After postinstall this path points to the copied folder; override if you serve the embed elsewhere. |
|
|
48
51
|
| `previewWidth` | `number` | `512` | Preview width (px). |
|
|
49
52
|
| `showMeshSwitch` | `boolean` | `true` | Show mesh switch (Circle/Plane). |
|
|
50
53
|
| `allowMouseInteraction` | `boolean` | `true` | Allow mouse interaction with 3D view. |
|
|
@@ -74,10 +77,11 @@ function App() {
|
|
|
74
77
|
| `validateShader` | function | Checks GDShader source before sending to Godot; <br/> Returns `{ valid, errors }`. Use for custom editor validation or before calling `loadShader`. |
|
|
75
78
|
| `ValidationError` | type | One validation error: `{ line: number; message: string }`. |
|
|
76
79
|
| `ValidationResult` | type | Return type of `validateShader`: `{ valid: boolean; errors: ValidationError[] }`. |
|
|
80
|
+
| `RGS_EMBED_FOLDER` | const | `'react_godot_shader_preview_embed'` - folder name created by postinstall. |
|
|
77
81
|
|
|
78
82
|
## How the preview works
|
|
79
83
|
|
|
80
|
-
1. The component renders an **iframe** whose `src` is
|
|
84
|
+
1. The component renders an **iframe** whose `src` is the **`embedUrl`** you pass (default: `/react_godot_shader_preview_embed/embed.html` after postinstall).
|
|
81
85
|
2. **embed.html** loads the Godot Web build (engine + exported game). When the game is ready, the Godot project assigns a bridge object to **`window.top.GodotShaderViewer`** - i.e. the parent page’s `window`, since the game runs inside the iframe. The React app therefore sees it on its own **`window.GodotShaderViewer`**.
|
|
82
86
|
3. The component **polls** for `window.GodotShaderViewer` until it appears, then sets **`onSuccess`**, **`onError`**, and **`onShaderLoaded`** on that object (so Godot can call back) and uses **`loadShader`**, **`setParameter`**, **`setDisplayMode`** to drive the preview.
|
|
83
87
|
|
|
@@ -14,19 +14,33 @@
|
|
|
14
14
|
display: flex;
|
|
15
15
|
flex-direction: column;
|
|
16
16
|
min-width: 0;
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
max-height: 100%;
|
|
20
|
+
min-height: 0;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
.rgs-previewWrap {
|
|
23
|
+
.rgs-previewWrap {
|
|
24
|
+
max-width: 100%;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
flex: 1 1 0;
|
|
27
|
+
min-height: 0;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
}
|
|
20
32
|
|
|
21
33
|
.rgs-preview {
|
|
22
|
-
width: 100%;
|
|
23
|
-
padding-bottom: 100%;
|
|
24
|
-
height: 0;
|
|
25
34
|
position: relative;
|
|
26
35
|
overflow: hidden;
|
|
27
36
|
border: 1px solid var(--rgs-b);
|
|
28
37
|
border-radius: 6px;
|
|
29
38
|
background: #000;
|
|
39
|
+
aspect-ratio: 1;
|
|
40
|
+
height: 100%;
|
|
41
|
+
width: auto;
|
|
42
|
+
max-width: 100%;
|
|
43
|
+
max-height: 100%;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
.rgs-canvas {
|
|
@@ -41,15 +55,28 @@
|
|
|
41
55
|
|
|
42
56
|
.rgs-mouseBlockOverlay { position: absolute; inset: 0; z-index: 5; pointer-events: auto; cursor: default; }
|
|
43
57
|
|
|
44
|
-
/*
|
|
58
|
+
/* Overlay layer: same size as preview so mode strip and loading overlay are positioned correctly */
|
|
59
|
+
.rgs-previewOverlays {
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset: 0;
|
|
62
|
+
z-index: 8;
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
padding: 12px;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.rgs-previewOverlays > * { pointer-events: auto; }
|
|
69
|
+
|
|
70
|
+
/* Mode strip (sphere/plane): positioned inside overlay padding so it never gets clipped */
|
|
45
71
|
.rgs-modeStrip {
|
|
46
72
|
position: absolute;
|
|
47
73
|
bottom: 8px;
|
|
48
74
|
right: 8px;
|
|
75
|
+
left: auto;
|
|
76
|
+
top: auto;
|
|
49
77
|
display: flex;
|
|
50
78
|
gap: 4px;
|
|
51
79
|
z-index: 10;
|
|
52
|
-
pointer-events: auto;
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
.rgs-modeStripButton {
|
|
@@ -81,8 +108,16 @@
|
|
|
81
108
|
z-index: 10;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
/* Status */
|
|
85
|
-
.rgs-
|
|
111
|
+
/* Status: fixed-height slot so canvas size does not change when status appears */
|
|
112
|
+
.rgs-statusBarSlot {
|
|
113
|
+
flex: 0 0 auto;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
margin-top: 8px;
|
|
117
|
+
min-height: 32px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.rgs-statusBar { display: flex; align-items: center; min-height: 32px; }
|
|
86
121
|
.rgs-statusSuccess, .rgs-statusError { padding: 8px 12px; border-radius: 4px; font-size: 13px; }
|
|
87
122
|
.rgs-statusSuccess { background: var(--rgs-ok); color: var(--rgs-ok-fg); }
|
|
88
123
|
.rgs-statusError { background: var(--rgs-err); color: var(--rgs-err-fg); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { GodotMaterialPreviewHandle, GodotMaterialPreviewProps } from './GodotMaterialPreview.types';
|
|
2
|
+
export { RGS_EMBED_FOLDER } from './GodotMaterialPreview.types';
|
|
2
3
|
export type { ShaderUniform, GodotMaterialPreviewHandle, ShaderLoadedPayload, DisplayMode, GodotMaterialPreviewProps } from './GodotMaterialPreview.types';
|
|
3
4
|
export declare const GodotMaterialPreview: import("react").ForwardRefExoticComponent<GodotMaterialPreviewProps & import("react").RefAttributes<GodotMaterialPreviewHandle>>;
|
|
@@ -2,7 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { GodotMaterialPreviewView } from './GodotMaterialPreviewView';
|
|
4
4
|
import { useGodotMaterialPreview } from './useGodotMaterialPreview';
|
|
5
|
+
import { RGS_EMBED_FOLDER } from './GodotMaterialPreview.types';
|
|
6
|
+
export { RGS_EMBED_FOLDER } from './GodotMaterialPreview.types';
|
|
5
7
|
const DEFAULTS = {
|
|
8
|
+
embedUrl: `/${RGS_EMBED_FOLDER}/embed.html`,
|
|
6
9
|
previewWidth: 512,
|
|
7
10
|
showMeshSwitch: true,
|
|
8
11
|
allowMouseInteraction: true,
|
|
@@ -48,7 +48,12 @@ declare global {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
/** Default folder name created by postinstall in the app root. */
|
|
52
|
+
export declare const RGS_EMBED_FOLDER = "react_godot_shader_preview_embed";
|
|
51
53
|
export interface GodotMaterialPreviewProps {
|
|
54
|
+
/** URL to the Godot embed page (e.g. `/react_godot_shader_preview_embed/embed.html` after postinstall).
|
|
55
|
+
* postinstall copies it to your project root as `react_godot_shader_preview_embed`. */
|
|
56
|
+
embedUrl?: string;
|
|
52
57
|
previewWidth?: number;
|
|
53
58
|
showMeshSwitch?: boolean;
|
|
54
59
|
allowMouseInteraction?: boolean;
|
|
@@ -4,6 +4,8 @@ export const DEFAULT_SAMPLER_STATE = {
|
|
|
4
4
|
gradient1d: { width: 256, stops: [[0, 0, 0, 0, 1], [1, 1, 1, 1, 1]] },
|
|
5
5
|
gradient2d: { width: 256, height: 256, fill: 0, stops: [[0, 0, 0, 0, 1], [1, 1, 1, 1, 1]] },
|
|
6
6
|
};
|
|
7
|
+
/** Default folder name created by postinstall in the app root. */
|
|
8
|
+
export const RGS_EMBED_FOLDER = 'react_godot_shader_preview_embed';
|
|
7
9
|
export const STATUS_HIDE_MS = 3000;
|
|
8
10
|
export const toPayload = (v) => v === null ? '' : typeof v === 'boolean' ? (v ? 'true' : 'false') : Array.isArray(v) ? JSON.stringify(v) : typeof v === 'string' ? v : String(v);
|
|
9
11
|
export function rgbToHex(rgb) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DisplayMode, SamplerState, ShaderUniform } from './GodotMaterialPreview.types';
|
|
2
2
|
export interface GodotMaterialPreviewViewProps {
|
|
3
3
|
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
4
|
+
embedUrl: string;
|
|
4
5
|
previewWidth: number;
|
|
5
6
|
showMeshSwitch: boolean;
|
|
6
7
|
allowMouseInteraction: boolean;
|
|
@@ -19,4 +20,4 @@ export interface GodotMaterialPreviewViewProps {
|
|
|
19
20
|
setParameter: (name: string, value: number | boolean | number[] | string | null) => void;
|
|
20
21
|
className?: string;
|
|
21
22
|
}
|
|
22
|
-
export declare function GodotMaterialPreviewView({ iframeRef, previewWidth, showMeshSwitch, allowMouseInteraction, showParameters, status, godotLoading, uniforms, samplerState, displayMode, setDisplayMode, updateUniform, updateSampler, setParameter, className, }: GodotMaterialPreviewViewProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export declare function GodotMaterialPreviewView({ iframeRef, embedUrl, previewWidth, showMeshSwitch, allowMouseInteraction, showParameters, status, godotLoading, uniforms, samplerState, displayMode, setDisplayMode, updateUniform, updateSampler, setParameter, className, }: GodotMaterialPreviewViewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -12,11 +12,13 @@ const cn = {
|
|
|
12
12
|
preview: 'rgs-preview',
|
|
13
13
|
canvas: 'rgs-canvas',
|
|
14
14
|
mouseBlockOverlay: 'rgs-mouseBlockOverlay',
|
|
15
|
+
previewOverlays: 'rgs-previewOverlays',
|
|
15
16
|
modeStrip: 'rgs-modeStrip',
|
|
16
17
|
modeStripButton: 'rgs-modeStripButton',
|
|
17
18
|
modeStripButtonActive: 'rgs-modeStripButtonActive',
|
|
18
19
|
loadingOverlay: 'rgs-loadingOverlay',
|
|
19
20
|
statusBar: 'rgs-statusBar',
|
|
21
|
+
statusBarSlot: 'rgs-statusBarSlot',
|
|
20
22
|
statusSuccess: 'rgs-statusSuccess',
|
|
21
23
|
statusError: 'rgs-statusError',
|
|
22
24
|
paramsPanel: 'rgs-paramsPanel',
|
|
@@ -40,9 +42,8 @@ const cn = {
|
|
|
40
42
|
paramSamplerGradient: 'rgs-paramSamplerGradient',
|
|
41
43
|
paramSelect: 'rgs-paramSelect',
|
|
42
44
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return (_jsxs("div", { className: `${cn.root} ${className ?? ''}`, style: { flex: `0 0 ${previewWidth}px` }, children: [_jsx("div", { className: cn.previewWrap, children: _jsxs("div", { className: cn.preview, children: [_jsx("iframe", { ref: iframeRef, src: GODOT_EMBED_URL, title: "Shader preview", className: cn.canvas }), !allowMouseInteraction && _jsx("div", { className: cn.mouseBlockOverlay, "aria-hidden": true }), showMeshSwitch && (_jsxs("div", { className: cn.modeStrip, role: "group", "aria-label": "Preview display mode", children: [_jsx("button", { type: "button", className: `${cn.modeStripButton} ${displayMode === 'Circle' ? cn.modeStripButtonActive : ''}`, onClick: () => setDisplayMode('Circle'), title: "Sphere", "aria-pressed": displayMode === 'Circle', "aria-label": "Sphere", children: _jsx(CircleIcon, {}) }), _jsx("button", { type: "button", className: `${cn.modeStripButton} ${displayMode === 'Plane' ? cn.modeStripButtonActive : ''}`, onClick: () => setDisplayMode('Plane'), title: "Plane", "aria-pressed": displayMode === 'Plane', "aria-label": "Plane", children: _jsx(DiamondIcon, {}) })] })), godotLoading && (_jsx("div", { className: cn.loadingOverlay, children: _jsx("div", { children: status?.message ?? 'Loading...' }) }))] }) }), status && !godotLoading && (_jsx("div", { className: cn.statusBar, children: _jsx("span", { className: status.error ? cn.statusError : cn.statusSuccess, children: status.message }) })), showParameters && (_jsxs("div", { className: cn.paramsPanel, children: [_jsx("div", { className: cn.paramsTitle, children: uniforms.length > 0 ? 'Shader parameters' : 'Parameters' }), uniforms.length > 0 ? (uniforms.map((u, i) => (_jsx(ParamRow, { u: u, i: i, samplerState: samplerState, updateUniform: updateUniform, updateSampler: updateSampler, setParameter: setParameter }, u.name)))) : (_jsx("p", { className: cn.paramsPlaceholder, children: "Load a shader with uniform variables to see and edit parameters here." }))] }))] }));
|
|
45
|
+
export function GodotMaterialPreviewView({ iframeRef, embedUrl, previewWidth, showMeshSwitch, allowMouseInteraction, showParameters, status, godotLoading, uniforms, samplerState, displayMode, setDisplayMode, updateUniform, updateSampler, setParameter, className, }) {
|
|
46
|
+
return (_jsxs("div", { className: `${cn.root} ${className ?? ''}`, style: { flex: `0 0 ${previewWidth}px` }, children: [_jsx("div", { className: cn.previewWrap, children: _jsxs("div", { className: cn.preview, children: [_jsx("iframe", { ref: iframeRef, src: embedUrl, title: "Shader preview", className: cn.canvas }), !allowMouseInteraction && _jsx("div", { className: cn.mouseBlockOverlay, "aria-hidden": true }), _jsxs("div", { className: cn.previewOverlays, children: [showMeshSwitch && (_jsxs("div", { className: cn.modeStrip, role: "group", "aria-label": "Preview display mode", children: [_jsx("button", { type: "button", className: `${cn.modeStripButton} ${displayMode === 'Circle' ? cn.modeStripButtonActive : ''}`, onClick: () => setDisplayMode('Circle'), title: "Sphere", "aria-pressed": displayMode === 'Circle', "aria-label": "Sphere", children: _jsx(CircleIcon, {}) }), _jsx("button", { type: "button", className: `${cn.modeStripButton} ${displayMode === 'Plane' ? cn.modeStripButtonActive : ''}`, onClick: () => setDisplayMode('Plane'), title: "Plane", "aria-pressed": displayMode === 'Plane', "aria-label": "Plane", children: _jsx(DiamondIcon, {}) })] })), godotLoading && (_jsx("div", { className: cn.loadingOverlay, children: _jsx("div", { children: status?.message ?? 'Loading...' }) }))] })] }) }), _jsx("div", { className: cn.statusBarSlot, children: status && !godotLoading ? (_jsx("span", { className: status.error ? cn.statusError : cn.statusSuccess, children: status.message })) : null }), showParameters && (_jsxs("div", { className: cn.paramsPanel, children: [_jsx("div", { className: cn.paramsTitle, children: uniforms.length > 0 ? 'Shader parameters' : 'Parameters' }), uniforms.length > 0 ? (uniforms.map((u, i) => (_jsx(ParamRow, { u: u, i: i, samplerState: samplerState, updateUniform: updateUniform, updateSampler: updateSampler, setParameter: setParameter }, u.name)))) : (_jsx("p", { className: cn.paramsPlaceholder, children: "Load a shader with uniform variables to see and edit parameters here." }))] }))] }));
|
|
46
47
|
}
|
|
47
48
|
function ParamRow({ u, i, samplerState, updateUniform, updateSampler, setParameter }) {
|
|
48
49
|
const state = samplerState[u.name] ?? DEFAULT_SAMPLER_STATE;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import './GodotMaterialPreview.css';
|
|
2
|
-
export { GodotMaterialPreview } from './GodotMaterialPreview';
|
|
2
|
+
export { GodotMaterialPreview, RGS_EMBED_FOLDER } from './GodotMaterialPreview';
|
|
3
3
|
export type { GodotMaterialPreviewProps, GodotMaterialPreviewHandle, ShaderUniform, ShaderLoadedPayload, DisplayMode, } from './GodotMaterialPreview.types';
|
|
4
4
|
export { validateShader, type ValidationError, type ValidationResult } from './lib';
|
package/dist/index.js
CHANGED
package/dist/style.css
CHANGED
|
@@ -14,19 +14,33 @@
|
|
|
14
14
|
display: flex;
|
|
15
15
|
flex-direction: column;
|
|
16
16
|
min-width: 0;
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
max-height: 100%;
|
|
20
|
+
min-height: 0;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
.rgs-previewWrap {
|
|
23
|
+
.rgs-previewWrap {
|
|
24
|
+
max-width: 100%;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
flex: 1 1 0;
|
|
27
|
+
min-height: 0;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
}
|
|
20
32
|
|
|
21
33
|
.rgs-preview {
|
|
22
|
-
width: 100%;
|
|
23
|
-
padding-bottom: 100%;
|
|
24
|
-
height: 0;
|
|
25
34
|
position: relative;
|
|
26
35
|
overflow: hidden;
|
|
27
36
|
border: 1px solid var(--rgs-b);
|
|
28
37
|
border-radius: 6px;
|
|
29
38
|
background: #000;
|
|
39
|
+
aspect-ratio: 1;
|
|
40
|
+
height: 100%;
|
|
41
|
+
width: auto;
|
|
42
|
+
max-width: 100%;
|
|
43
|
+
max-height: 100%;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
.rgs-canvas {
|
|
@@ -41,15 +55,28 @@
|
|
|
41
55
|
|
|
42
56
|
.rgs-mouseBlockOverlay { position: absolute; inset: 0; z-index: 5; pointer-events: auto; cursor: default; }
|
|
43
57
|
|
|
44
|
-
/*
|
|
58
|
+
/* Overlay layer: same size as preview so mode strip and loading overlay are positioned correctly */
|
|
59
|
+
.rgs-previewOverlays {
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset: 0;
|
|
62
|
+
z-index: 8;
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
padding: 12px;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.rgs-previewOverlays > * { pointer-events: auto; }
|
|
69
|
+
|
|
70
|
+
/* Mode strip (sphere/plane): positioned inside overlay padding so it never gets clipped */
|
|
45
71
|
.rgs-modeStrip {
|
|
46
72
|
position: absolute;
|
|
47
73
|
bottom: 8px;
|
|
48
74
|
right: 8px;
|
|
75
|
+
left: auto;
|
|
76
|
+
top: auto;
|
|
49
77
|
display: flex;
|
|
50
78
|
gap: 4px;
|
|
51
79
|
z-index: 10;
|
|
52
|
-
pointer-events: auto;
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
.rgs-modeStripButton {
|
|
@@ -81,8 +108,16 @@
|
|
|
81
108
|
z-index: 10;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
/* Status */
|
|
85
|
-
.rgs-
|
|
111
|
+
/* Status: fixed-height slot so canvas size does not change when status appears */
|
|
112
|
+
.rgs-statusBarSlot {
|
|
113
|
+
flex: 0 0 auto;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
margin-top: 8px;
|
|
117
|
+
min-height: 32px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.rgs-statusBar { display: flex; align-items: center; min-height: 32px; }
|
|
86
121
|
.rgs-statusSuccess, .rgs-statusError { padding: 8px 12px; border-radius: 4px; font-size: 13px; }
|
|
87
122
|
.rgs-statusSuccess { background: var(--rgs-ok); color: var(--rgs-ok-fg); }
|
|
88
123
|
.rgs-statusError { background: var(--rgs-err); color: var(--rgs-err-fg); }
|
package/godot/embed.html
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
<title>React Godot Shader Preview</title>
|
|
7
7
|
<style>
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
|
|
10
|
-
#godot-canvas { display: block; width: 100%; height: 100%; object-fit:
|
|
9
|
+
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; display: flex; align-items: center; justify-content: center; }
|
|
10
|
+
#godot-canvas { display: block; max-width: 100%; max-height: 100%; width: 100% !important; height: 100% !important; object-fit: contain; cursor: grab;
|
|
11
|
+
/* Override Godot engine inline positioning/sizing so flex centering works */
|
|
12
|
+
position: relative !important; left: auto !important; top: auto !important; min-width: 0 !important; }
|
|
11
13
|
</style>
|
|
12
14
|
</head>
|
|
13
15
|
<body>
|
package/godot/index.html
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
.viewer-preview-wrap { flex: 0 0 320px; max-width: 100%; min-width: 0; }
|
|
47
47
|
.viewer-preview { width: 100%; padding-bottom: 100%; height: 0; position: relative; overflow: hidden; border: 1px solid #444; border-radius: 6px; background: #000; }
|
|
48
48
|
@media (max-width: 700px) { .viewer-row { flex-direction: column; } .viewer-preview-wrap { flex: 0 0 auto; width: 100%; max-width: 400px; } }
|
|
49
|
-
#godot-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; object-fit:
|
|
49
|
+
#godot-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; object-fit: contain; }
|
|
50
50
|
.status { padding: 8px 12px; border-radius: 4px; font-size: 13px; margin-top: 8px; }
|
|
51
51
|
.status.success { background: #1e3a1e; color: #8fef8f; }
|
|
52
52
|
.status.error { background: #3a1e1e; color: #ef8f8f; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-godot-shader-preview",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "React component for live Godot shader preview via WebAssembly. Load shader code, switch mesh (sphere/plane), edit uniforms and samplers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
"default": "./dist/style.css"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"files": [
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"godot",
|
|
22
|
+
"scripts"
|
|
23
|
+
],
|
|
20
24
|
"scripts": {
|
|
21
25
|
"build": "tsc && node scripts/copy-css.mjs",
|
|
22
|
-
"prepublishOnly": "yarn build"
|
|
26
|
+
"prepublishOnly": "yarn build",
|
|
27
|
+
"postinstall": "node scripts/copy-embed.mjs"
|
|
23
28
|
},
|
|
24
29
|
"peerDependencies": {
|
|
25
30
|
"react": ">=18.0.0",
|
|
@@ -32,8 +37,20 @@
|
|
|
32
37
|
"react-dom": "^19.0.0",
|
|
33
38
|
"typescript": "~5.9.0"
|
|
34
39
|
},
|
|
35
|
-
"keywords": [
|
|
40
|
+
"keywords": [
|
|
41
|
+
"godot",
|
|
42
|
+
"shader",
|
|
43
|
+
"gdshader",
|
|
44
|
+
"webgl",
|
|
45
|
+
"webassembly",
|
|
46
|
+
"react",
|
|
47
|
+
"preview"
|
|
48
|
+
],
|
|
36
49
|
"author": "Daniil Pshegorlisnkii (https://github.com/nnoxnnox)",
|
|
37
50
|
"license": "MIT",
|
|
38
|
-
"repository": {
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/nnoxnnox/react-godot-shader-preview"
|
|
54
|
+
},
|
|
55
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
39
56
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const root = join(__dirname, '..');
|
|
7
|
+
const src = join(root, 'src', 'GodotMaterialPreview.css');
|
|
8
|
+
const dist = join(root, 'dist');
|
|
9
|
+
if (!existsSync(dist)) mkdirSync(dist, { recursive: true });
|
|
10
|
+
copyFileSync(src, join(dist, 'GodotMaterialPreview.css'));
|
|
11
|
+
copyFileSync(src, join(dist, 'style.css'));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
7
|
+
const sourceDir = path.join(packageRoot, 'godot');
|
|
8
|
+
const embedFolder = 'react_godot_shader_preview_embed';
|
|
9
|
+
// When installed as a dependency, cwd is the package dir; project root is two levels up.
|
|
10
|
+
const projectRoot = path.resolve(packageRoot, '..', '..');
|
|
11
|
+
const destDir = path.join(projectRoot, embedFolder);
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(sourceDir)) {
|
|
14
|
+
console.warn('[react-godot-shader-preview] postinstall: godot folder not found, skip copy');
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(destDir)) {
|
|
19
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function copyRecursive(src, dest) {
|
|
23
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
const srcPath = path.join(src, e.name);
|
|
26
|
+
const destPath = path.join(dest, e.name);
|
|
27
|
+
if (e.isDirectory()) {
|
|
28
|
+
if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
|
|
29
|
+
copyRecursive(srcPath, destPath);
|
|
30
|
+
} else {
|
|
31
|
+
fs.copyFileSync(srcPath, destPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
copyRecursive(sourceDir, destDir);
|
|
37
|
+
console.log(`[react-godot-shader-preview] Copied godot export to ${embedFolder}/`);
|