(
21 | children,
22 | (child) =>
23 | React.isValidElement(child) &&
24 | React.cloneElement(child, {
25 | selectedClassName,
26 | selected,
27 | onSelect,
28 | })
29 | )}
30 | >
31 | );
32 | };
33 |
34 | // Group of the tabs themselves
35 | const TabGroup = ({
36 | children,
37 | selected,
38 | selectedClassName = 'tab_selected',
39 | onSelect,
40 | ...props
41 | }: {
42 | children?: React.ReactNode;
43 | selected?: number;
44 | className?: string;
45 | selectedClassName?: string;
46 | onSelect?: Function;
47 | }) => {
48 | return (
49 |
50 | {React.Children.map(
51 | children,
52 | (child, index) =>
53 | React.isValidElement(child) &&
54 | React.cloneElement(child, {
55 | selectedClassName,
56 | selected,
57 | onSelect,
58 | index,
59 | })
60 | )}
61 |
62 | );
63 | };
64 |
65 | // An individual tab
66 | const Tab = ({
67 | children,
68 | selected,
69 | className,
70 | selectedClassName,
71 | onSelect,
72 | index,
73 | ...props
74 | }: {
75 | children?: React.ReactNode;
76 | selected?: number;
77 | className?: any;
78 | selectedClassName?: any;
79 | onSelect?: Function;
80 | index?: number;
81 | }) => {
82 | return (
83 | {
89 | event.preventDefault();
90 | onSelect && onSelect(index);
91 | }}
92 | >
93 | {children}
94 |
95 | );
96 | };
97 |
98 | // Wraps all panels, shows the selected panel
99 | const TabPanels = ({
100 | selected,
101 | children,
102 | }: {
103 | selected?: number;
104 | children: React.ReactNode;
105 | }) => (
106 | <>
107 | {React.Children.map(children, (child, index) =>
108 | selected === index ? child : null
109 | )}
110 | >
111 | );
112 |
113 | // The contents for each tab
114 | interface TabPanelProps extends React.HTMLAttributes {}
115 | const TabPanel = React.forwardRef(
116 | ({ children, ...props }, ref) => {
117 | return (
118 |
119 | {children}
120 |
121 | );
122 | }
123 | );
124 | TabPanel.displayName = 'TabPanel';
125 |
126 | export { Tabs, Tab, TabGroup, TabPanels, TabPanel };
127 |
--------------------------------------------------------------------------------
/src/site/components/tabs/tabs.module.css:
--------------------------------------------------------------------------------
1 | .tab_tabs {
2 | display: grid;
3 | background: linear-gradient(#222, #111);
4 | grid-auto-flow: column;
5 | color: #fff;
6 | grid-auto-columns: max-content;
7 | border-bottom: 1px solid #000;
8 | }
9 | .tab_tab {
10 | user-select: none;
11 | max-height: 28px;
12 | border-radius: 6px 6px 0 0;
13 | border-right: 1px solid #000;
14 | padding: 5px 10px;
15 | font-size: 14px;
16 | cursor: pointer;
17 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.15);
18 | text-shadow: -1px -1px 0 #000;
19 | background: linear-gradient(rgba(63, 63, 63, 1), rgba(46, 46, 46, 1));
20 | }
21 |
22 | .tab_tab:hover {
23 | background: linear-gradient(#4f4f4f, #3f3f3f);
24 | }
25 |
26 | .tab_selected {
27 | box-shadow: inset 0 1px #19d600, inset 0 2px #358a00be;
28 | background: linear-gradient(rgb(66, 141, 1), rgb(23, 67, 3));
29 | }
30 | .tab_selected:hover {
31 | background: linear-gradient(rgb(68, 136, 8), rgb(32, 94, 4));
32 | }
33 |
--------------------------------------------------------------------------------
/src/site/components/useGraph.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | collectConnectedNodes,
3 | compileGraph,
4 | computeGraphContext,
5 | filterGraphNodes,
6 | Graph,
7 | GraphNode,
8 | isDataInput,
9 | } from '@shaderfrog/core/src/core/graph';
10 | import {
11 | Edge as GraphEdge,
12 | EdgeType,
13 | makeEdge,
14 | } from '@shaderfrog/core/src/core/nodes/edge';
15 | import {
16 | Engine,
17 | EngineContext,
18 | convertToEngine,
19 | convertNode,
20 | } from '@shaderfrog/core/src/core/engine';
21 | import { UICompileGraphResult } from '../uICompileGraphResult';
22 | import { generate } from '@shaderfrog/glsl-parser';
23 | import { shaderSectionsToProgram } from '@shaderfrog/core/src/ast/shader-sections';
24 |
25 | import {
26 | arrayNode,
27 | colorNode,
28 | numberNode,
29 | samplerCubeNode,
30 | textureNode,
31 | Vector2,
32 | Vector3,
33 | Vector4,
34 | vectorNode,
35 | } from '@shaderfrog/core/src/core/nodes/data-nodes';
36 |
37 | import { fireFrag, fireVert } from '../../shaders/fireNode';
38 | import fluidCirclesNode from '../../shaders/fluidCirclesNode';
39 | import {
40 | heatShaderFragmentNode,
41 | heatShaderVertexNode,
42 | } from '../../shaders/heatmapShaderNode';
43 | import perlinCloudsFNode from '../../shaders/perlinClouds';
44 | import { hellOnEarthFrag, hellOnEarthVert } from '../../shaders/hellOnEarth';
45 | import { outlineShaderF, outlineShaderV } from '../../shaders/outlineShader';
46 | import purpleNoiseNode from '../../shaders/purpleNoiseNode';
47 | import solidColorNode from '../../shaders/solidColorNode';
48 | import staticShaderNode from '../../shaders/staticShaderNode';
49 | import { checkerboardF, checkerboardV } from '../../shaders/checkboardNode';
50 | import {
51 | cubemapReflectionF,
52 | cubemapReflectionV,
53 | } from '../../shaders/cubemapReflectionNode';
54 | import normalMapify from '../../shaders/normalmapifyNode';
55 | import { makeId } from '../../util/id';
56 | import {
57 | addNode,
58 | multiplyNode,
59 | phongNode,
60 | sourceNode,
61 | } from '@shaderfrog/core/src/core/nodes/engine-node';
62 | import {
63 | declarationOfStrategy,
64 | texture2DStrategy,
65 | uniformStrategy,
66 | } from '@shaderfrog/core/src/core/strategy';
67 | import { serpentF, serpentV } from '../../shaders/serpentNode';
68 | import { badTvFrag } from '../../shaders/badTvNode';
69 | import whiteNoiseNode from '../../shaders/whiteNoiseNode';
70 | import { babylengine } from '@shaderfrog/core/src/plugins/babylon/bablyengine';
71 | import sinCosVertWarp from '../../shaders/sinCosVertWarp';
72 | import { juliaF, juliaV } from '../../shaders/juliaNode';
73 |
74 | const compileGraphAsync = async (
75 | graph: Graph,
76 | engine: Engine,
77 | ctx: EngineContext
78 | ): Promise =>
79 | new Promise((resolve, reject) => {
80 | setTimeout(async () => {
81 | console.warn('Compiling!', graph, 'for nodes', ctx.nodes);
82 |
83 | const allStart = performance.now();
84 |
85 | let result;
86 |
87 | try {
88 | await computeGraphContext(ctx, engine, graph);
89 | result = compileGraph(ctx, engine, graph);
90 | } catch (err) {
91 | return reject(err);
92 | }
93 | const fragmentResult = generate(
94 | shaderSectionsToProgram(result.fragment, engine.mergeOptions).program
95 | );
96 | const vertexResult = generate(
97 | shaderSectionsToProgram(result.vertex, engine.mergeOptions).program
98 | );
99 |
100 | const dataInputs = filterGraphNodes(
101 | graph,
102 | [result.outputFrag, result.outputVert],
103 | { input: isDataInput }
104 | ).inputs;
105 |
106 | // Find which nodes flow up into uniform inputs, for colorizing and for
107 | // not recompiling when their data changes
108 | const dataNodes = Object.entries(dataInputs).reduce<
109 | Record
110 | >((acc, [nodeId, inputs]) => {
111 | return inputs.reduce((iAcc, input) => {
112 | const fromEdge = graph.edges.find(
113 | (edge) => edge.to === nodeId && edge.input === input.id
114 | );
115 | const fromNode =
116 | fromEdge && graph.nodes.find((node) => node.id === fromEdge.from);
117 | return fromNode
118 | ? {
119 | ...iAcc,
120 | ...collectConnectedNodes(graph, fromNode),
121 | }
122 | : iAcc;
123 | }, acc);
124 | }, {});
125 |
126 | const now = performance.now();
127 | resolve({
128 | compileMs: (now - allStart).toFixed(3),
129 | result,
130 | fragmentResult,
131 | vertexResult,
132 | dataNodes,
133 | dataInputs,
134 | graph,
135 | });
136 | }, 10);
137 | });
138 |
139 | const expandUniformDataNodes = (graph: Graph): Graph =>
140 | graph.nodes.reduce((updated, node) => {
141 | if ('config' in node && node.config.uniforms) {
142 | const newNodes = node.config.uniforms.reduce<[GraphNode[], GraphEdge[]]>(
143 | (acc, uniform, index) => {
144 | const position = {
145 | x: node.position.x - 250,
146 | y: node.position.y - 200 + index * 100,
147 | };
148 | let n;
149 | switch (uniform.type) {
150 | case 'texture': {
151 | n = textureNode(makeId(), uniform.name, position, uniform.value);
152 | break;
153 | }
154 | case 'number': {
155 | n = numberNode(makeId(), uniform.name, position, uniform.value, {
156 | range: uniform.range,
157 | stepper: uniform.stepper,
158 | });
159 | break;
160 | }
161 | case 'vector2': {
162 | n = vectorNode(
163 | makeId(),
164 | uniform.name,
165 | position,
166 | uniform.value as Vector2
167 | );
168 | break;
169 | }
170 | case 'vector3': {
171 | n = vectorNode(
172 | makeId(),
173 | uniform.name,
174 | position,
175 | uniform.value as Vector3
176 | );
177 | break;
178 | }
179 | case 'vector4': {
180 | n = vectorNode(
181 | makeId(),
182 | uniform.name,
183 | position,
184 | uniform.value as Vector4
185 | );
186 | break;
187 | }
188 | case 'rgb': {
189 | n = colorNode(
190 | makeId(),
191 | uniform.name,
192 | position,
193 | uniform.value as Vector3
194 | );
195 | break;
196 | }
197 | case 'samplerCube': {
198 | n = samplerCubeNode(
199 | makeId(),
200 | uniform.name,
201 | position,
202 | uniform.value as string
203 | );
204 | break;
205 | }
206 | case 'rgba': {
207 | n = colorNode(
208 | makeId(),
209 | uniform.name,
210 | position,
211 | uniform.value as Vector4
212 | );
213 | break;
214 | }
215 | }
216 | return [
217 | [...acc[0], n],
218 | [
219 | ...acc[1],
220 | makeEdge(
221 | makeId(),
222 | n.id,
223 | node.id,
224 | 'out',
225 | `uniform_${uniform.name}`,
226 | uniform.type
227 | ),
228 | ],
229 | ];
230 | },
231 | [[], []]
232 | );
233 |
234 | return {
235 | nodes: [...updated.nodes, ...newNodes[0]],
236 | edges: [...updated.edges, ...newNodes[1]],
237 | };
238 | }
239 | return updated;
240 | }, graph);
241 |
242 | const createGraphNode = (
243 | nodeDataType: string,
244 | name: string,
245 | position: { x: number; y: number },
246 | engine: Engine,
247 | newEdgeData?: Omit,
248 | defaultValue?: any
249 | ): [Set, Graph] => {
250 | const makeName = (type: string) => name || type;
251 | const id = makeId();
252 | const groupId = makeId();
253 | let newGns: GraphNode[];
254 |
255 | if (nodeDataType === 'number') {
256 | newGns = [
257 | numberNode(
258 | id,
259 | makeName('number'),
260 | position,
261 | defaultValue === undefined || defaultValue === null ? '1' : defaultValue
262 | ),
263 | ];
264 | } else if (nodeDataType === 'texture') {
265 | newGns = [
266 | textureNode(
267 | id,
268 | makeName('texture'),
269 | position,
270 | defaultValue || 'grayscale-noise'
271 | ),
272 | ];
273 | } else if (nodeDataType === 'vector2') {
274 | newGns = [
275 | vectorNode(id, makeName('vec2'), position, defaultValue || ['1', '1']),
276 | ];
277 | } else if (nodeDataType === 'array') {
278 | newGns = [
279 | arrayNode(id, makeName('array'), position, defaultValue || ['1', '1']),
280 | ];
281 | } else if (nodeDataType === 'vector3') {
282 | newGns = [
283 | vectorNode(
284 | id,
285 | makeName('vec3'),
286 | position,
287 | defaultValue || ['1', '1', '1']
288 | ),
289 | ];
290 | } else if (nodeDataType === 'vector4') {
291 | newGns = [
292 | vectorNode(
293 | id,
294 | makeName('vec4'),
295 | position,
296 | defaultValue || ['1', '1', '1', '1']
297 | ),
298 | ];
299 | } else if (nodeDataType === 'rgb') {
300 | newGns = [
301 | colorNode(id, makeName('rgb'), position, defaultValue || ['1', '1', '1']),
302 | ];
303 | } else if (nodeDataType === 'rgba') {
304 | newGns = [
305 | colorNode(
306 | id,
307 | makeName('rgba'),
308 | position,
309 | defaultValue || ['1', '1', '1', '1']
310 | ),
311 | ];
312 | } else if (nodeDataType === 'multiply') {
313 | newGns = [multiplyNode(id, position)];
314 | } else if (nodeDataType === 'add') {
315 | newGns = [addNode(id, position)];
316 | } else if (nodeDataType === 'phong') {
317 | newGns = [
318 | phongNode(id, 'Phong', groupId, position, 'fragment'),
319 | phongNode(makeId(), 'Phong', groupId, position, 'vertex', id),
320 | ];
321 | } else if (nodeDataType === 'physical') {
322 | newGns = [
323 | engine.constructors.physical(
324 | id,
325 | 'Physical',
326 | groupId,
327 | position,
328 | [],
329 | 'fragment'
330 | ),
331 | engine.constructors.physical(
332 | makeId(),
333 | 'Physical',
334 | groupId,
335 | position,
336 | [],
337 | 'vertex',
338 | id
339 | ),
340 | ];
341 | } else if (nodeDataType === 'toon') {
342 | newGns = [
343 | engine.constructors.toon(id, 'Toon', groupId, position, [], 'fragment'),
344 | engine.constructors.toon(
345 | makeId(),
346 | 'Toon',
347 | groupId,
348 | position,
349 | [],
350 | 'vertex',
351 | id
352 | ),
353 | ];
354 | } else if (nodeDataType === 'simpleVertex') {
355 | newGns = [sinCosVertWarp(makeId(), position)];
356 | } else if (nodeDataType === 'julia') {
357 | newGns = [juliaF(id, position), juliaV(makeId(), id, position)];
358 | } else if (nodeDataType === 'fireNode') {
359 | newGns = [fireFrag(id, position), fireVert(makeId(), id, position)];
360 | } else if (nodeDataType === 'badTv') {
361 | newGns = [badTvFrag(id, position)];
362 | } else if (nodeDataType === 'whiteNoiseNode') {
363 | newGns = [whiteNoiseNode(id, position)];
364 | } else if (nodeDataType === 'checkerboardF') {
365 | newGns = [
366 | checkerboardF(id, position),
367 | checkerboardV(makeId(), id, position),
368 | ];
369 | } else if (nodeDataType === 'serpent') {
370 | newGns = [serpentF(id, position), serpentV(makeId(), id, position)];
371 | } else if (nodeDataType === 'cubemapReflection') {
372 | newGns = [
373 | cubemapReflectionF(id, position),
374 | cubemapReflectionV(makeId(), id, position),
375 | ];
376 | } else if (nodeDataType === 'fluidCirclesNode') {
377 | newGns = [fluidCirclesNode(id, position)];
378 | } else if (nodeDataType === 'heatmapShaderNode') {
379 | newGns = [
380 | heatShaderFragmentNode(id, position),
381 | heatShaderVertexNode(makeId(), id, position),
382 | ];
383 | } else if (nodeDataType === 'hellOnEarth') {
384 | newGns = [
385 | hellOnEarthFrag(id, position),
386 | hellOnEarthVert(makeId(), id, position),
387 | ];
388 | } else if (nodeDataType === 'outlineShader') {
389 | newGns = [
390 | outlineShaderF(id, position),
391 | outlineShaderV(makeId(), id, position),
392 | ];
393 | } else if (nodeDataType === 'perlinClouds') {
394 | newGns = [perlinCloudsFNode(id, position)];
395 | } else if (nodeDataType === 'purpleNoiseNode') {
396 | newGns = [purpleNoiseNode(id, position)];
397 | } else if (nodeDataType === 'solidColorNode') {
398 | newGns = [solidColorNode(id, position)];
399 | } else if (nodeDataType === 'staticShaderNode') {
400 | newGns = [staticShaderNode(id, position)];
401 | } else if (nodeDataType === 'normalMapify') {
402 | newGns = [normalMapify(id, position)];
403 | } else if (nodeDataType === 'samplerCube') {
404 | newGns = [
405 | samplerCubeNode(
406 | id,
407 | makeName('samplerCube'),
408 | position,
409 | 'warehouseEnvTexture'
410 | ),
411 | ];
412 | } else if (nodeDataType === 'fragment' || nodeDataType === 'vertex') {
413 | newGns = [
414 | sourceNode(
415 | makeId(),
416 | 'Source Code ' + id,
417 | position,
418 | {
419 | version: 2,
420 | preprocess: true,
421 | strategies: [uniformStrategy(), texture2DStrategy()],
422 | uniforms: [],
423 | },
424 | nodeDataType === 'fragment'
425 | ? `void main() {
426 | gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
427 | }`
428 | : `void main() {
429 | gl_Position = vec4(1.0);
430 | }`,
431 | nodeDataType,
432 | engine.name
433 | ),
434 | ];
435 | } else {
436 | throw new Error(
437 | `Could not create node: Unknown node type "${nodeDataType}'"`
438 | );
439 | }
440 |
441 | // Hack: Auto-converting nodes to threejs for testing
442 | newGns = newGns.map((gn) => {
443 | if (gn.type === 'source' && engine.name === 'babylon') {
444 | return convertNode(gn, babylengine.importers.three);
445 | }
446 | return gn;
447 | });
448 |
449 | let newGEs: GraphEdge[] = newEdgeData
450 | ? [
451 | makeEdge(
452 | makeId(),
453 | id,
454 | newEdgeData.to,
455 | newEdgeData.output,
456 | newEdgeData.input,
457 | newEdgeData.type
458 | ),
459 | ]
460 | : [];
461 |
462 | // Expand uniforms on new nodes automatically
463 | const originalNodes = new Set(newGns.map((n) => n.id));
464 | return [
465 | originalNodes,
466 | expandUniformDataNodes({ nodes: newGns, edges: newGEs }),
467 | ];
468 | };
469 |
470 | export { createGraphNode, expandUniformDataNodes, compileGraphAsync };
471 |
--------------------------------------------------------------------------------
/src/site/flowEventHack.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | // Pass an onchange handler down to nodes in context
4 |
5 | export const Context = createContext({});
6 | export type ChangeHandler = (id: string, value: any) => void;
7 |
8 | export const useFlowEventHack = () => {
9 | return useContext(Context) as ChangeHandler;
10 | };
11 |
12 | export const FlowEventHack = ({
13 | onChange,
14 | children,
15 | }: {
16 | onChange: ChangeHandler;
17 | children: React.ReactNode;
18 | }) => {
19 | return {children};
20 | };
21 |
--------------------------------------------------------------------------------
/src/site/hoistedRefContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useRef } from 'react';
2 |
3 | // TODO: Try replacing with zustand?
4 | export const HoistedRef = createContext({});
5 | export type HoistedRefGetter = (
6 | key: string,
7 | setter?: () => T
8 | ) => T;
9 |
10 | export const useHoisty = () => {
11 | return useContext(HoistedRef) as {
12 | getRefData: HoistedRefGetter;
13 | };
14 | };
15 |
16 | export const Hoisty: React.FC = ({ children }) => {
17 | const refData = useRef<{ [key: string]: any }>({});
18 |
19 | // TODO: I've hard to hard code "three" / "babylon" in the respective places
20 | // that use this hook, to keep the old context around to destroy it, and to
21 | // prevent babylon calling this hook and getting a brand new context. Can I
22 | // instead clear this, or forceUpdate?
23 | const getRefData: HoistedRefGetter = (key, setter) => {
24 | if (!refData.current[key] && setter) {
25 | refData.current[key] = setter();
26 | }
27 | return refData.current[key];
28 | };
29 |
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/site/hooks/useAsyncExtendedState.ts:
--------------------------------------------------------------------------------
1 | import { useState, useMemo, SetStateAction } from 'react'
2 |
3 | export type AsyncSetState = (nextState: SetStateAction | Promise>) => void
4 |
5 | export type AsyncExtendState = (extendState: SetStateAction> | Promise>>) => void
6 |
7 | /**
8 | * A hook similar to React's `useState` which allows you to also pass promises which resolve to the next state.
9 | *
10 | * Also returns an extra method which extends the current state, synchronously and/or asynchronously.
11 | * For the current state to be extended, it must first be a non-null value.
12 | *
13 | * Example:
14 | * ```ts
15 | * interface State {
16 | * foo: string
17 | * bar: string
18 | * }
19 | *
20 | * const [ state, setState, extendState ] = useAsyncExtendedState({
21 | * foo: 'foo',
22 | * bar: 'bar'
23 | * })
24 | *
25 | * // This works as usual.
26 | * setState({ foo: 'Hello', bar: 'World!' })
27 | * setState(state => ({ foo: 'Hello', bar: 'World!' }))
28 | *
29 | * // This also works.
30 | * const fetchState = () => API.client.get('data').then(response => {
31 | * return response.data
32 | *
33 | * // or return (state: State) => {
34 | * // return response.data
35 | * // }
36 | * })
37 | *
38 | * // The state will eventually be set to the asynchronously resolved value.
39 | * setState(fetchState())
40 | *
41 | * // Or extend the state immediately.
42 | * extendState({ foo: 'Hello' })
43 | * extendState(state => ({ bar: 'World!' }))
44 | *
45 | * // Or extend the state asynchronously.
46 | * const fetchPartialState = () => API.client.get>('data').then(response => {
47 | * return response.data
48 | *
49 | * // or return (state: State) => {
50 | * // return response.data
51 | * // }
52 | * })
53 | *
54 | * // The state will eventually be extended by the asynchronously resolved value.
55 | * extendState(fetchPartialState())
56 | * ```
57 | */
58 | export const useAsyncExtendedState = (initialState: State): [ State, AsyncSetState, AsyncExtendState ] => {
59 | const [ state, setState ] = useState(initialState)
60 |
61 | const asyncSetState: AsyncSetState = useMemo(() => async nextState => {
62 | const initialNextState = nextState
63 |
64 | try {
65 | if (nextState instanceof Promise) {
66 | nextState = await nextState
67 |
68 | if (nextState === initialNextState) { // there was an error but it was caught by something else - i.e., nextState.catch()
69 | throw new Error(`Uncatchable error.`)
70 | }
71 | }
72 |
73 | setState(state => {
74 | if (typeof nextState === `function`) {
75 | nextState = (nextState as ((state: State) => State))(state)
76 | }
77 |
78 | return nextState as State
79 | })
80 | } catch (error) {
81 | }
82 | }, [])
83 |
84 | const asyncExtendState: AsyncExtendState = useMemo(() => async extendState => {
85 | const initialExtendState = extendState
86 |
87 | try {
88 | if (extendState instanceof Promise) {
89 | extendState = await extendState
90 |
91 | if (extendState === initialExtendState) { // there was an error but it was caught by something else - i.e., extendState.catch()
92 | throw new Error(`Uncatchable error.`)
93 | }
94 | }
95 |
96 | setState(state => {
97 | if (typeof extendState === `function`) {
98 | extendState = (extendState as ((state: State) => Partial))(state)
99 | }
100 |
101 | return { ...state, ...extendState } as State
102 | })
103 | } catch (err) {
104 | }
105 | }, [])
106 |
107 | return [ state, asyncSetState, asyncExtendState ]
108 | }
109 |
--------------------------------------------------------------------------------
/src/site/hooks/useLocalStorage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | type Setter = (value: T | ((value: T) => T)) => void;
4 |
5 | export function useLocalStorage(
6 | key: string,
7 | initialValue: T | (() => T)
8 | ): [val: T, setter: Setter, reset: () => T] {
9 | // State to store our value
10 | // Pass initial state function to useState so logic is only executed once
11 | const [storedValue, setStoredValue] = useState(() => {
12 | // Get from local storage by key
13 | const item = window.localStorage.getItem(key + 'dorfle');
14 | // Parse stored json or if none return initialValue
15 | return item
16 | ? JSON.parse(item)
17 | : initialValue instanceof Function
18 | ? initialValue()
19 | : initialValue;
20 | });
21 |
22 | useEffect(() => {
23 | if (typeof window !== 'undefined') {
24 | window.localStorage.setItem(key, JSON.stringify(storedValue));
25 | }
26 | }, [key, storedValue]);
27 |
28 | const reset = (): T => {
29 | if (typeof window !== 'undefined') {
30 | window.localStorage.removeItem(key);
31 | }
32 | const initial =
33 | initialValue instanceof Function ? initialValue() : initialValue;
34 | setStoredValue(initial);
35 | return initial;
36 | };
37 |
38 | return [storedValue, setStoredValue, reset];
39 | }
40 |
--------------------------------------------------------------------------------
/src/site/hooks/useOnce.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 |
3 | // Utility function to preserve specific things against fast-refresh, as
4 | // *all* useMemo and useEffect and useCallbacks rerun during a fast-refresh
5 | // https://nextjs.org/docs/basic-features/fast-refresh
6 | const useOnce = (creator: (...args: any) => T): T => {
7 | const ref = useRef();
8 | if (ref.current) {
9 | return ref.current;
10 | }
11 | ref.current = creator();
12 | return ref.current;
13 | };
14 |
15 | export default useOnce;
16 |
--------------------------------------------------------------------------------
/src/site/hooks/usePrevious.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 |
3 | export const usePrevious = (value: T): T | undefined => {
4 | const ref = useRef();
5 | useEffect(() => {
6 | ref.current = value;
7 | });
8 | return ref.current;
9 | };
10 |
--------------------------------------------------------------------------------
/src/site/hooks/usePromise.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { useState, useMemo, useEffect, useRef } from 'react'
3 |
4 | export type PromiseStatus = `pending` | `resolved` | `rejected`
5 | export type CancelPromise = (message?: string) => void
6 | export type ResetPromiseState = (keys?: Array>) => void
7 |
8 | export interface PromiseState {
9 | status?: PromiseStatus
10 | promise?: Promise
11 | value?: Awaited
12 | error?: Error
13 | cancel?: CancelPromise
14 | }
15 |
16 | export type PromiseStateWithReset = PromiseState & {
17 | reset: ResetPromiseState
18 | }
19 |
20 | /**
21 | * A hook which accepts an asynchronous function (i.e., a function which returns a promise).
22 | *
23 | * Returns a new asynchronous function wrapping the original to be called as necessary,
24 | * along with the state of the promise as `status`, `promise`, `value`, `error`, `cancel()`,
25 | * and a `reset()` method to reset the state.
26 | *
27 | * Pairs well with the {@link hooks.useAsyncExtendedState | `useAsyncExtendedState`} hook.
28 | *
29 | * Example:
30 | * ```ts
31 | * interface State {
32 | * foo: string
33 | * bar: string
34 | * }
35 | *
36 | * const [ state, setState, extendState ] = useAsyncExtendedState({
37 | * foo: 'foo',
38 | * bar: 'bar'
39 | * })
40 | *
41 | * const read = (id: string) => API.client.get(`data/${id}`).then(response => {
42 | * return response.data as Partial
43 | * })
44 | *
45 | * const [ readRequest, requestRead ] = usePromise(read)
46 | *
47 | * const isPending = readRequest.status === 'pending'
48 | *
49 | * return (
50 | * <>
51 | *
52 | * foo: {state.foo}
53 | *
54 | *
55 | *
56 | * bar: {state.bar}
57 | *
58 | *
59 | * extendState(requestRead('someId'))} disabled={isPending}>
60 | *
61 | * {isPending
62 | * ? 'Requesting data...'
63 | * : 'Request data'
64 | * }
65 | *
66 | *
67 | *
68 | *
69 | * {readRequest.error}
70 | *
71 | * >
72 | * )
73 | * ```
74 | */
75 | export const usePromise = any>(asyncFunction: T, initialState?: PromiseState>): [ PromiseStateWithReset>, (...asyncFuncArgs: Parameters) => ReturnType ] => {
76 | const [ state, setState ] = useState>>(initialState || {})
77 | const isCancelled = useRef(false)
78 | const isUnmounted = useRef(false)
79 |
80 | const callAsyncFunction = useMemo(() => (...args: Parameters): ReturnType => {
81 | const promise = asyncFunction(...args)
82 | const cancel: CancelPromise = message => {
83 | isCancelled.current = true
84 |
85 | if (!isUnmounted.current) {
86 | setState(({ value }) => ({ status: `rejected`, value, error: message ? new Error(message) : undefined }))
87 | }
88 | }
89 |
90 | isCancelled.current = false
91 |
92 | if (promise instanceof Promise) {
93 | return new Promise(resolve => {
94 | const fulfillPromise = async () => {
95 | try {
96 | const value: Awaited> = await promise
97 |
98 | if (!isCancelled.current && !isUnmounted.current) {
99 | setState({ status: `resolved`, value })
100 | resolve(value)
101 | }
102 | } catch (error) {
103 | if (!isCancelled.current && !isUnmounted.current) {
104 | setState(({ value }) => ({ status: `rejected`, value, error: error instanceof Error ? error : new Error(error as string) }))
105 | }
106 | }
107 | }
108 |
109 | setState(({ value }) => ({ status: `pending`, value, promise, cancel }))
110 | fulfillPromise()
111 | }) as ReturnType
112 | } else {
113 | setState({ status: `resolved`, value: promise })
114 | return promise
115 | }
116 | }, [ asyncFunction ])
117 |
118 | const reset: ResetPromiseState = useMemo(() => keys => setState(state => {
119 | if (!keys) {
120 | return {}
121 | }
122 |
123 | const nextState = { ...state }
124 |
125 | if (keys) {
126 | for (const key of keys) {
127 | delete nextState[key]
128 | }
129 | }
130 |
131 | return nextState
132 | }), [])
133 |
134 | const stateWithReset: PromiseStateWithReset> = useMemo(() => ({ ...state, reset }), [ state, reset ])
135 |
136 | useEffect(() => {
137 | return () => {
138 | isCancelled.current = true
139 | isUnmounted.current = true
140 | }
141 | }, [])
142 |
143 | return [ stateWithReset, callAsyncFunction ]
144 | }
145 |
--------------------------------------------------------------------------------
/src/site/hooks/useSize.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useLayoutEffect } from 'react';
2 | import useResizeObserver from '@react-hook/resize-observer';
3 |
4 | export const useSize = (target: React.RefObject) => {
5 | const [size, setSize] = useState(null);
6 |
7 | useLayoutEffect(() => {
8 | if (target.current) {
9 | setSize(target.current.getBoundingClientRect());
10 | }
11 | }, [target]);
12 |
13 | // Where the magic happens
14 | useResizeObserver(target, (entry) => setSize(entry.contentRect));
15 | return size;
16 | };
17 |
--------------------------------------------------------------------------------
/src/site/hooks/useThrottle.tsx:
--------------------------------------------------------------------------------
1 | import throttle from 'lodash.throttle';
2 | import { useCallback, useEffect, useRef } from 'react';
3 |
4 | type AnyFn = (...args: any) => any;
5 | function useThrottle(callback: AnyFn, delay: number) {
6 | const cbRef = useRef(callback);
7 |
8 | // use mutable ref to make useCallback/throttle not depend on `cb` dep
9 | useEffect(() => {
10 | cbRef.current = callback;
11 | }, [callback]);
12 |
13 | // eslint-disable-next-line react-hooks/exhaustive-deps
14 | return useCallback(
15 | throttle((...args) => cbRef.current(...args), delay),
16 | [delay]
17 | );
18 | }
19 |
20 | export default useThrottle;
21 |
--------------------------------------------------------------------------------
/src/site/hooks/useWindowSize.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | interface Size {
4 | width: number;
5 | height: number;
6 | }
7 |
8 | export const useWindowSize = (): Size => {
9 | const [windowSize, setWindowSize] = useState({
10 | width: window.innerWidth,
11 | height: window.innerHeight,
12 | });
13 |
14 | useEffect(() => {
15 | const handleResize = () => {
16 | setWindowSize({
17 | width: window.innerWidth,
18 | height: window.innerHeight,
19 | });
20 | };
21 |
22 | window.addEventListener('resize', handleResize);
23 | return () => window.removeEventListener('resize', handleResize);
24 | }, []);
25 |
26 | return windowSize;
27 | };
28 |
--------------------------------------------------------------------------------
/src/site/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | height: 100vh;
9 | }
10 |
11 | .main {
12 | padding: 5rem 0;
13 | flex: 1;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | .footer {
21 | width: 100%;
22 | height: 100px;
23 | border-top: 1px solid #eaeaea;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 |
29 | .footer a {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | flex-grow: 1;
34 | }
35 |
36 | .title a {
37 | color: #0070f3;
38 | text-decoration: none;
39 | }
40 |
41 | .title a:hover,
42 | .title a:focus,
43 | .title a:active {
44 | text-decoration: underline;
45 | }
46 |
47 | .title {
48 | margin: 0;
49 | line-height: 1.15;
50 | font-size: 4rem;
51 | }
52 |
53 | .title,
54 | .description {
55 | text-align: center;
56 | }
57 |
58 | .description {
59 | line-height: 1.5;
60 | font-size: 1.5rem;
61 | }
62 |
63 | .code {
64 | background: #fafafa;
65 | border-radius: 5px;
66 | padding: 0.75rem;
67 | font-size: 1.1rem;
68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
69 | Bitstream Vera Sans Mono, Courier New, monospace;
70 | }
71 |
72 | .grid {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | flex-wrap: wrap;
77 | max-width: 800px;
78 | margin-top: 3rem;
79 | }
80 |
81 | .card {
82 | margin: 1rem;
83 | padding: 1.5rem;
84 | text-align: left;
85 | color: inherit;
86 | text-decoration: none;
87 | border: 1px solid #eaeaea;
88 | border-radius: 10px;
89 | transition: color 0.15s ease, border-color 0.15s ease;
90 | width: 45%;
91 | }
92 |
93 | .card:hover,
94 | .card:focus,
95 | .card:active {
96 | color: #0070f3;
97 | border-color: #0070f3;
98 | }
99 |
100 | .card h2 {
101 | margin: 0 0 1rem 0;
102 | font-size: 1.5rem;
103 | }
104 |
105 | .card p {
106 | margin: 0;
107 | font-size: 1.25rem;
108 | line-height: 1.5;
109 | }
110 |
111 | .logo {
112 | height: 1em;
113 | margin-left: 0.5rem;
114 | }
115 |
116 | @media (max-width: 600px) {
117 | .grid {
118 | width: 100%;
119 | flex-direction: column;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/site/styles/flow.theme.css:
--------------------------------------------------------------------------------
1 | .react-flow__node.react-flow__node-output {
2 | background: transparent;
3 | border: 0 none;
4 | border-radius: 0;
5 | color: inherit;
6 | font-size: inherit;
7 | padding: 0;
8 | text-align: unset;
9 | width: auto;
10 | }
11 |
12 | .flownode {
13 | border-radius: 8px;
14 | background: #111;
15 | color: #fff;
16 | border: 1px solid #666;
17 | box-shadow: 0 0 10px rgba(0, 0, 0, 1);
18 | position: relative;
19 | min-width: 240px;
20 | min-height: 80px;
21 | }
22 |
23 | .react-flow__node-output.selectable:hover,
24 | .react-flow__node-output.selected,
25 | .react-flow__node-output.selectable.selected {
26 | box-shadow: inherit !important;
27 | }
28 |
29 | .flowlabel {
30 | border-radius: 8px 8px 0 0;
31 | padding: 3px 4px 4px 8px;
32 | border-bottom: 1px solid #666;
33 | text-shadow: 0 0 2px #000;
34 | }
35 |
36 | .selected .flownode {
37 | border: 1px solid #ccc;
38 | }
39 |
40 | .selected .flowlabel {
41 | background: linear-gradient(to right, rgb(59, 59, 59), rgb(48, 48, 48));
42 | border-bottom: 1px solid #ccc;
43 | }
44 |
45 | .flowlabel .stage,
46 | .flowlabel .dataType {
47 | text-transform: uppercase;
48 | font-size: 12px;
49 | float: right;
50 | padding: 2px 8px;
51 | margin: 1px 0 0;
52 | border-radius: 6px;
53 | }
54 | .inactive .flowlabel .stage {
55 | opacity: 0.5;
56 | }
57 |
58 | /* The hidden handle in the middle of the node to link fragment to vertex stages */
59 | .flownode .react-flow__handle.next-stage-handle {
60 | position: absolute;
61 | top: 50%;
62 | left: 50%;
63 | right: auto;
64 | display: none;
65 | }
66 |
67 | .react-flow__edge,
68 | .react-flow__edge-path,
69 | .react-flow__connection {
70 | pointer-events: none;
71 | }
72 |
73 | .flowInputs {
74 | position: absolute;
75 | top: 25px; /* height of node header label */
76 | width: 100%;
77 | }
78 |
79 | .react-flow__edgeupdater {
80 | opacity: 0.5;
81 | r: 4;
82 | }
83 |
84 | .react-flow__edge-path-selector:hover {
85 | cursor: pointer;
86 | }
87 | .react-flow__edge-path-selector:hover + .react-flow__edge-path,
88 | .react-flow__edge-path:hover {
89 | stroke: #555;
90 | cursor: pointer;
91 | }
92 |
93 | .react-flow__edge-path-selector {
94 | fill: transparent;
95 | stroke-linecap: round;
96 | stroke-width: 12;
97 | }
98 |
99 | .react-flow__edge.selected .react-flow__edge-path {
100 | pointer-events: none;
101 | stroke-width: 3;
102 | }
103 |
104 | .react-flow_handle_label {
105 | min-width: 200px;
106 | white-space: nowrap;
107 | left: 20px;
108 | top: -3px;
109 | pointer-events: none;
110 | color: rgb(176, 228, 156);
111 | position: absolute;
112 | }
113 |
114 | .flownode .react-flow__handle {
115 | width: 14px;
116 | height: 14px;
117 | transition: 0.1s all ease-out;
118 | }
119 |
120 | .flownode .react-flow__handle .react-flow__handle-left {
121 | left: -7px;
122 | }
123 |
124 | .flownode .react-flow__handle .react-flow__handle-right {
125 | right: -7px;
126 | }
127 |
128 | .react-flow__handle.react-flow__handle-connecting + .react-flow_handle_label {
129 | box-shadow: 0 0 10px 4px rgb(70, 70, 70);
130 | background: rgb(70, 70, 70);
131 | border-radius: 4px;
132 | }
133 |
134 | .react-flow__handle.validTarget {
135 | box-shadow: 0 0 10px #fff;
136 | border-color: #d4bcbc;
137 | background: #bfbfbf;
138 | }
139 |
140 | .react-flow__handle.react-flow__handle-connecting {
141 | box-shadow: 0 0 10px #fff;
142 | background: #fff;
143 | }
144 |
145 | .flownode .body {
146 | position: absolute;
147 | top: 35px;
148 | left: 10px;
149 | right: 10px;
150 | }
151 |
152 | .flownode .switch {
153 | pointer-events: all;
154 | display: inline-block;
155 | cursor: pointer;
156 | margin: 0 6px 0 0;
157 | }
158 | .flownode .switch:hover {
159 | opacity: 0.5;
160 | }
161 | .flownode .switch:active {
162 | opacity: 0.25;
163 | }
164 |
165 | /* -------------- */
166 | /* Data theme */
167 | /* -------------- */
168 | .flow-node_data .flowlabel .dataType {
169 | color: #fff;
170 | background: #3c627c;
171 | }
172 |
173 | .flownode.flow-node_data {
174 | background: rgb(6, 16, 68);
175 | min-height: 80px;
176 | }
177 |
178 | .flow-node_data .flowlabel {
179 | background: linear-gradient(to right, rgb(19, 40, 144), rgb(9, 23, 91));
180 | }
181 |
182 | .flownode.texture {
183 | min-height: 40px;
184 | }
185 |
186 | .flownode.vector2,
187 | .flownode.vector3,
188 | .flownode.vector4,
189 | .flownode.rgb,
190 | .flownode.rgba {
191 | height: 90px !important;
192 | }
193 |
194 | .flownode.number input {
195 | width: 50%;
196 | }
197 |
198 | .flow-node_data.flownode .flowlabel {
199 | border-bottom: 1px solid #00a5c6;
200 | }
201 |
202 | .flow-node_data.flownode {
203 | border: 1px solid #00a5c6;
204 | }
205 |
206 | .selected .flow-node_data.flownode {
207 | border: 1px solid #00d5ff;
208 | }
209 |
210 | .selected .flow-node_data .flowlabel {
211 | background: linear-gradient(to right, rgb(9, 22, 84), rgb(13, 29, 108));
212 | border-bottom: 1px solid #00d5ff;
213 | }
214 |
215 | .react-flow__edge.texture .react-flow__edge-path,
216 | .react-flow__edge.samplerCube .react-flow__edge-path,
217 | .react-flow__edge.number .react-flow__edge-path,
218 | .react-flow__edge.array .react-flow__edge-path,
219 | .react-flow__edge.vector2 .react-flow__edge-path,
220 | .react-flow__edge.vector3 .react-flow__edge-path,
221 | .react-flow__edge.vector4 .react-flow__edge-path,
222 | .react-flow__edge.rgb .react-flow__edge-path,
223 | .react-flow__edge.rgba .react-flow__edge-path,
224 | .react-flow__edge.vector4 .react-flow__edge-path {
225 | stroke: #00d5ff;
226 | }
227 |
228 | .react-flow__edge.array.updating .react-flow__edge-path,
229 | .react-flow__edge.vector2.updating .react-flow__edge-path,
230 | .react-flow__edge.vector3.updating .react-flow__edge-path,
231 | .react-flow__edge.vector4.updating .react-flow__edge-path,
232 | .react-flow__edge.rgb.updating .react-flow__edge-path,
233 | .react-flow__edge.rgba.updating .react-flow__edge-path,
234 | .react-flow__edge.texture.updating .react-flow__edge-path,
235 | .react-flow__edge.samplerCube.updating .react-flow__edge-path,
236 | .react-flow__edge.number.updating .react-flow__edge-path {
237 | stroke: #00aeff;
238 | }
239 |
240 | /* selected edge */
241 | .react-flow__edge.array.selected .react-flow__edge-path,
242 | .react-flow__edge.vector2.selected .react-flow__edge-path,
243 | .react-flow__edge.vector3.selected .react-flow__edge-path,
244 | .react-flow__edge.vector4.selected .react-flow__edge-path,
245 | .react-flow__edge.rgb.selected .react-flow__edge-path,
246 | .react-flow__edge.rgba.selected .react-flow__edge-path,
247 | .react-flow__edge.texture.selected .react-flow__edge-path,
248 | .react-flow__edge.samplerCube.selected .react-flow__edge-path,
249 | .react-flow__edge.number.selected .react-flow__edge-path {
250 | stroke: #00d5ff;
251 | }
252 |
253 | /* selectable edge path */
254 | .react-flow__edge.array .react-flow__edge-path-selector,
255 | .react-flow__edge.vector2 .react-flow__edge-path-selector,
256 | .react-flow__edge.vector3 .react-flow__edge-path-selector,
257 | .react-flow__edge.vector4 .react-flow__edge-path-selector,
258 | .react-flow__edge.rgb .react-flow__edge-path-selector,
259 | .react-flow__edge.rgba .react-flow__edge-path-selector,
260 | .react-flow__edge.texture .react-flow__edge-path-selector,
261 | .react-flow__edge.samplerCube .react-flow__edge-path-selector,
262 | .react-flow__edge.number .react-flow__edge-path-selector {
263 | stroke: rgba(0, 68, 255, 0.3);
264 | }
265 |
266 | /* updateable drag handle */
267 | .flow-node_data .react-flow__edgeupdater {
268 | fill: #333d00;
269 | stroke: #00d5ff;
270 | }
271 |
272 | /* -------------- */
273 | /* Fragment theme */
274 | /* -------------- */
275 |
276 | .fragment .react-flow_handle_label {
277 | color: rgb(176, 228, 156);
278 | }
279 |
280 | .fragment.flownode .flowlabel {
281 | border-bottom: 1px solid #8ca800;
282 | }
283 |
284 | .fragment.flownode {
285 | border: 1px solid #8ca800;
286 | }
287 |
288 | .selected .fragment.flownode {
289 | border: 1px solid #d4ff00;
290 | }
291 |
292 | .fragment .flowlabel {
293 | background: linear-gradient(to right, #326d29, #2d792d);
294 | }
295 | .fragment .flowlabel .stage {
296 | background: #37b037;
297 | color: rgb(255, 255, 255);
298 | }
299 |
300 | .selected .fragment .flowlabel {
301 | background: linear-gradient(to right, #3c7534, #3d833d);
302 | border-bottom: 1px solid #d4ff0080;
303 | }
304 |
305 | .inactive.fragment .flowlabel {
306 | background: linear-gradient(to right, #303d2e, #283a28);
307 | }
308 |
309 | .react-flow__edge.fragment .react-flow__edge-path {
310 | stroke: #d4ff00;
311 | }
312 |
313 | .react-flow__edge.fragment.updating .react-flow__edge-path {
314 | stroke: #7bff00;
315 | }
316 |
317 | /* selected edge */
318 | .react-flow__edge.fragment.selected .react-flow__edge-path {
319 | stroke: #d4ff00;
320 | }
321 |
322 | /* selectable edge path */
323 | .fragment .react-flow__edge-path-selector {
324 | stroke: rgba(0, 255, 0, 0.3);
325 | }
326 |
327 | /* updateable drag handle */
328 | .fragment .react-flow__edgeupdater {
329 | fill: #333d00;
330 | stroke: #d4ff00;
331 | }
332 |
333 | .fragment.flownode .react-flow__handle {
334 | border-color: #d4ff00;
335 | background: #333d00;
336 | }
337 | .fragment.flownode .react-flow__handle.react-flow__handle-connecting {
338 | border-color: #fff;
339 | background: #22ff00;
340 | }
341 | .fragment.flownode
342 | .react-flow__handle.react-flow__handle-connecting
343 | .react-flow_handle_label {
344 | color: #fff;
345 | }
346 |
347 | .fragment .react-flow__handle.validTarget {
348 | box-shadow: 0 0 20px 5px #0f0;
349 | border-color: #fefff7;
350 | background: #d4ff00;
351 | }
352 |
353 | .fragment .react-flow__handle.validTarget.react-flow__handle-connecting {
354 | box-shadow: 0 0 20px 5px #efe;
355 | border-color: #efe;
356 | background: #efe;
357 | }
358 |
359 | /* ------------ */
360 | /* Vertex theme */
361 | /* ------------ */
362 |
363 | .vertex .react-flow_handle_label {
364 | color: #f86464;
365 | }
366 |
367 | .vertex.flownode .flowlabel {
368 | border-bottom: 1px solid #b84242;
369 | }
370 |
371 | .vertex.flownode {
372 | border: 1px solid #b84242;
373 | }
374 |
375 | .selected .vertex.flownode {
376 | border: 1px solid #ff0c0c;
377 | }
378 |
379 | .vertex .flowlabel {
380 | background-color: #ffe53b;
381 | background-image: linear-gradient(147deg, #770921 0%, #8a0808 74%);
382 | }
383 |
384 | .vertex .flowlabel .stage {
385 | background: #be0404;
386 | color: rgb(255, 255, 255);
387 | }
388 |
389 | .selected .vertex .flowlabel {
390 | background-image: linear-gradient(147deg, #810b25 0%, #940d0d 74%);
391 | border-bottom: 1px solid #da1842;
392 | }
393 |
394 | .inactive.vertex .flowlabel {
395 | background-image: linear-gradient(147deg, #3b101a 0%, #3b1111 74%);
396 | }
397 |
398 | .react-flow__edge.vertex .react-flow__edge-path {
399 | stroke: #fa5b5b;
400 | }
401 |
402 | .react-flow__edge.vertex.updating .react-flow__edge-path {
403 | stroke: #ff0000;
404 | }
405 |
406 | /* selected edge */
407 | .react-flow__edge.vertex.selected .react-flow__edge-path {
408 | stroke: #ff0000;
409 | }
410 |
411 | /* selectable edge path */
412 | .vertex .react-flow__edge-path-selector {
413 | stroke: rgba(255, 0, 0, 0.3);
414 | }
415 |
416 | /* updateable drag handle */
417 | .vertex .react-flow__edgeupdater {
418 | fill: #3a1919;
419 | stroke: #992424;
420 | }
421 |
422 | .vertex.flownode .react-flow__handle {
423 | border-color: #fa5b5b;
424 | background: #3a1919;
425 | transition: all 0.2s ease-in;
426 | }
427 | .vertex.flownode .react-flow__handle.react-flow__handle-connecting {
428 | border-color: #fff;
429 | background: #ff2525;
430 | }
431 | .vertex.flownode
432 | .react-flow__handle.react-flow__handle-connecting
433 | .react-flow_handle_label {
434 | color: #fff;
435 | }
436 |
437 | .vertex .react-flow__handle.validTarget {
438 | box-shadow: 0 0 10px #f00;
439 | border-color: #ffd3d3;
440 | background: #fa5b5b;
441 | }
442 |
443 | .vertex .react-flow__handle.validTarget.react-flow__handle-connecting {
444 | box-shadow: 0 0 20px 5px #fee;
445 | border-color: #fee;
446 | background: #fee;
447 | }
448 |
--------------------------------------------------------------------------------
/src/site/styles/forms.css:
--------------------------------------------------------------------------------
1 | .inlinecontrol {
2 | display: grid;
3 | grid-template-columns: auto 1fr;
4 | gap: 4px;
5 | }
6 |
7 | .label {
8 | line-height: 25px;
9 | color: #ccc;
10 | font-size: 14px;
11 | letter-spacing: 0.5px;
12 | }
13 | .label:not([for='']) {
14 | cursor: pointer;
15 | }
16 |
17 | .select,
18 | .input,
19 | .checkbox,
20 | .textinput,
21 | .formbutton {
22 | line-height: 18px;
23 | box-sizing: border-box;
24 | color: #eee;
25 | font-size: 14px;
26 | padding: 4px;
27 | border-radius: 4px;
28 | border: 0 none;
29 | }
30 |
31 | /* "button" is a default css class of monaco/vscode editor and causes conflicts,
32 | which is why this is instead 'formbutton' */
33 | .formbutton {
34 | cursor: pointer;
35 | width: 100%;
36 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.15);
37 | text-shadow: -1px -1px 0 #000;
38 | background: linear-gradient(rgba(60, 70, 60, 1), rgba(42, 50, 42, 1));
39 | }
40 | .formbutton:hover {
41 | background: linear-gradient(rgba(40, 80, 40, 1), rgba(45, 60, 45, 1));
42 | }
43 | .buttonauto {
44 | width: auto;
45 | padding-left: 24px;
46 | padding-right: 24px;
47 | }
48 |
49 | .select {
50 | cursor: pointer;
51 | width: 100%;
52 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.15);
53 | text-shadow: -1px -1px 0 #000;
54 | background: linear-gradient(rgba(63, 63, 63, 1), rgba(46, 46, 46, 1));
55 | }
56 | .select:hover {
57 | background: linear-gradient(rgba(70, 70, 70, 1), rgba(51, 51, 51, 1));
58 | }
59 |
60 | .select,
61 | .input,
62 | .textinput {
63 | width: 100%;
64 | }
65 |
66 | .textinput {
67 | background: linear-gradient(rgba(46, 46, 46, 1), rgba(68, 68, 68, 1));
68 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.15);
69 | appearance: none;
70 | }
71 | .textinput[readonly] {
72 | color: #ccc;
73 | background: linear-gradient(rgba(60, 60, 60, 1), rgba(62, 62, 62, 1));
74 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.15);
75 | }
76 |
77 | .checkbox {
78 | margin: 0;
79 | cursor: pointer;
80 | position: relative;
81 | top: 4px;
82 | background: linear-gradient(rgba(46, 46, 46, 1), rgba(68, 68, 68, 1));
83 |
84 | /* Add if not using autoprefixer */
85 | -webkit-appearance: none;
86 | /* Remove most all native input styles */
87 | appearance: none;
88 | /* For iOS < 15 */
89 | background-color: var(--form-background);
90 | /* Not removed via appearance */
91 | margin: 0;
92 |
93 | font: inherit;
94 | color: currentColor;
95 | width: 1.15em;
96 | height: 1.15em;
97 | border: 0 none;
98 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.15);
99 |
100 | display: grid;
101 | place-content: center;
102 | }
103 |
104 | .checkbox:hover {
105 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.15),
106 | inset 0 0 10px rgba(255, 255, 255, 0.1);
107 | }
108 |
109 | /* Checkmark (hidden by default) */
110 | .checkbox::before {
111 | content: '';
112 | width: 0.65em;
113 | height: 0.65em;
114 | clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
115 | transform: scale(0);
116 | transform-origin: center center;
117 | transition: 40ms transform ease-in-out;
118 | box-shadow: inset 1px 1px 5px rgba(0, 0, 0, 0.8);
119 | /* Windows High Contrast Mode */
120 | background-color: rgb(47, 255, 0);
121 | }
122 |
123 | .checkbox:checked::before {
124 | transform: scale(1);
125 | }
126 |
127 | .checkbox:focus {
128 | outline: none;
129 | outline-offset: max(2px, 0.15em);
130 | }
131 |
132 | .checkbox:disabled {
133 | --form-control-color: var(--form-control-disabled);
134 |
135 | color: var(--form-control-disabled);
136 | cursor: not-allowed;
137 | }
138 |
--------------------------------------------------------------------------------
/src/site/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | height: 100%;
8 | background: #000;
9 | /* stop mac left swipe gesture going back */
10 | overscroll-behavior-x: none;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | * {
19 | box-sizing: border-box;
20 | }
21 |
22 | .mTop1 {
23 | margin-top: 20px !important;
24 | }
25 |
26 | .noselect {
27 | user-select: none;
28 | }
29 |
30 | .m-right-15 {
31 | margin-right: 15px;
32 | }
33 |
34 | .grid {
35 | display: grid;
36 | grid-auto-flow: column;
37 | grid-template-columns: auto;
38 | grid-column-gap: 2px;
39 | }
40 |
41 | .span2 {
42 | grid-column: 1 / span 2;
43 | }
44 |
--------------------------------------------------------------------------------
/src/site/styles/resizer.custom.css:
--------------------------------------------------------------------------------
1 | .Resizer {
2 | background: #000;
3 | opacity: 0.2;
4 | z-index: 1;
5 | box-sizing: border-box;
6 | background-clip: padding-box;
7 | }
8 |
9 | .Resizer:hover {
10 | transition: all 2s ease;
11 | }
12 |
13 | .Resizer.horizontal {
14 | height: 11px;
15 | margin: -5px 0;
16 | border-top: 5px solid rgba(255, 255, 255, 0);
17 | border-bottom: 5px solid rgba(255, 255, 255, 0);
18 | cursor: row-resize;
19 | }
20 |
21 | .Resizer.horizontal:hover,
22 | .Resizer.horizontal.resizing {
23 | border-top: 5px solid rgba(0, 0, 0, 0.5);
24 | border-bottom: 5px solid rgba(0, 0, 0, 0.5);
25 | }
26 |
27 | .Resizer.vertical {
28 | width: 11px;
29 | margin: 0 -5px;
30 | border-left: 5px solid rgba(255, 255, 255, 0);
31 | border-right: 5px solid rgba(255, 255, 255, 0);
32 | cursor: col-resize;
33 | }
34 |
35 | .Resizer.vertical:hover,
36 | .Resizer.vertical.resizing {
37 | border-left: 5px solid rgba(0, 0, 0, 0.5);
38 | border-right: 5px solid rgba(0, 0, 0, 0.5);
39 | }
40 |
41 | .DragLayer {
42 | z-index: 1;
43 | pointer-events: none;
44 | }
45 |
46 | .DragLayer.resizing {
47 | pointer-events: auto;
48 | }
49 |
50 | .DragLayer.horizontal {
51 | cursor: row-resize;
52 | }
53 |
54 | .DragLayer.vertical {
55 | cursor: col-resize;
56 | }
57 |
--------------------------------------------------------------------------------
/src/site/uICompileGraphResult.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CompileGraphResult,
3 | Graph,
4 | GraphNode,
5 | } from '@shaderfrog/core/src/core/graph';
6 | import { NodeInput } from '@shaderfrog/core/src/core/nodes/core-node';
7 |
8 | export type IndexedDataInputs = Record;
9 |
10 | export type UICompileGraphResult = {
11 | compileMs: string;
12 | fragmentResult: string;
13 | vertexResult: string;
14 | result: CompileGraphResult;
15 | dataNodes: Record;
16 | dataInputs: IndexedDataInputs;
17 | graph: Graph;
18 | };
19 |
--------------------------------------------------------------------------------
/src/util/ensure.ts:
--------------------------------------------------------------------------------
1 | export const ensure = (
2 | argument: T | undefined | null,
3 | message: string = 'This value was promised to be there.'
4 | ): T => {
5 | if (argument === undefined || argument === null) {
6 | throw new TypeError(message);
7 | }
8 |
9 | return argument;
10 | };
11 |
--------------------------------------------------------------------------------
/src/util/hasParent.ts:
--------------------------------------------------------------------------------
1 | export const hasParent = (
2 | element: HTMLElement | null,
3 | matcher: string
4 | ): boolean =>
5 | !element
6 | ? false
7 | : element.matches(matcher) || hasParent(element.parentElement, matcher);
8 |
--------------------------------------------------------------------------------
/src/util/id.ts:
--------------------------------------------------------------------------------
1 | let counter = 0;
2 | export const makeId = () => '' + counter++;
3 |
--------------------------------------------------------------------------------
/src/util/replaceAt.ts:
--------------------------------------------------------------------------------
1 | export const replaceAt = (array: any[], index: number, value: any) => [
2 | ...array.slice(0, index),
3 | value,
4 | ...array.slice(index + 1),
5 | ];
6 |
--------------------------------------------------------------------------------
/src/util/union.ts:
--------------------------------------------------------------------------------
1 | export const union = (...iterables: Set[]) => {
2 | const set = new Set();
3 |
4 | for (const iterable of iterables) {
5 | for (const item of iterable) {
6 | set.add(item);
7 | }
8 | }
9 |
10 | return set;
11 | };
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "downlevelIteration": true,
17 | "plugins": [{ "name": "typescript-plugin-css-modules" }]
18 | },
19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------