├── .eslintrc.json ├── .gitignore ├── .prettierrc.js ├── README.md ├── babel.config.js ├── deploy.sh ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── 3tone.jpg ├── Big_pebbles_pxr128.jpeg ├── Big_pebbles_pxr128_bmp.jpeg ├── Big_pebbles_pxr128_normal.jpeg ├── blank-normal-map.png ├── brick-texture.jpeg ├── bricknormal.jpeg ├── bricknormal.png ├── bricks.jpeg ├── checkerboard-glsl.png ├── checkerboard-graph.png ├── contrast-noise.png ├── envmaps │ ├── citycourtyard.dds │ ├── empty_warehouse_01_2k.hdr │ ├── environmentSpecular.env │ ├── pond │ │ ├── negx.jpg │ │ ├── negy.jpg │ │ ├── negz.jpg │ │ ├── posx.jpg │ │ ├── posy.jpg │ │ └── posz.jpg │ └── room.hdr ├── explosion.png ├── favicon.ico ├── grayscale-noise.png ├── hybrid-graph-screenshot.png └── vercel.svg ├── src ├── babylon-fragment.ts ├── babylon-vertex.ts ├── monaco-glsl.ts ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ ├── editor │ │ ├── editor.module.css │ │ └── index.tsx │ └── index.tsx ├── plugins │ ├── babylon │ │ ├── BabylonComponent.tsx │ │ ├── examples.ts │ │ ├── index.ts │ │ └── useBabylon.tsx │ └── three │ │ ├── RoomEnvironment.ts │ │ ├── ThreeComponent.tsx │ │ ├── examples.ts │ │ ├── index.ts │ │ └── useThree.tsx ├── shaders │ ├── badTvNode.tsx │ ├── checkboardNode.tsx │ ├── cubemapReflectionNode.tsx │ ├── fireNode.tsx │ ├── fluidCirclesNode.tsx │ ├── heatmapShaderNode.tsx │ ├── hellOnEarth.tsx │ ├── juliaNode.tsx │ ├── normalmapifyNode.tsx │ ├── outlineShader.tsx │ ├── perlinClouds.tsx │ ├── purpleNoiseNode.tsx │ ├── serpentNode.tsx │ ├── sinCosVertWarp.tsx │ ├── solidColorNode.tsx │ ├── starterNode.tsx │ ├── staticShaderNode.tsx │ └── whiteNoiseNode.tsx ├── site │ ├── components │ │ ├── CodeEditor.tsx │ │ ├── Editor.tsx │ │ ├── flow │ │ │ ├── ConnectionLine.tsx │ │ │ ├── FlowEdge.tsx │ │ │ ├── FlowEditor.tsx │ │ │ ├── FlowNode.tsx │ │ │ ├── context.menu.module.css │ │ │ ├── flownode.module.css │ │ │ └── helpers.ts │ │ ├── tabs │ │ │ ├── Tabs.tsx │ │ │ └── tabs.module.css │ │ └── useGraph.tsx │ ├── flowEventHack.tsx │ ├── hoistedRefContext.tsx │ ├── hooks │ │ ├── useAsyncExtendedState.ts │ │ ├── useLocalStorage.tsx │ │ ├── useOnce.tsx │ │ ├── usePrevious.tsx │ │ ├── usePromise.ts │ │ ├── useSize.tsx │ │ ├── useThrottle.tsx │ │ └── useWindowSize.tsx │ ├── styles │ │ ├── Home.module.css │ │ ├── flow.theme.css │ │ ├── forms.css │ │ ├── globals.css │ │ └── resizer.custom.css │ └── uICompileGraphResult.ts └── util │ ├── ensure.ts │ ├── hasParent.ts │ ├── id.ts │ ├── replaceAt.ts │ └── union.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | }; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shaderfrog 2.0 "Hybrid Graph" Tech Demo 2 | 3 | ![Hybrid Graph editor screenshot](/public/hybrid-graph-screenshot.png) 4 | 5 | # [Live Demo Link](http://frogger.andrewray.me/editor.html) 6 | 7 | This is only a tech demo, and does not currently offer the ability to save shaders. 8 | 9 | # What? 10 | 11 | The Shaderfrog 2.0 "Hybrid Graph" editor is a tool that lets you arbitrarily 12 | compose shader source code (GLSL) using a combination of source code and graph 13 | nodes. 14 | 15 | In the below screenshots, you can see a shader that produes a checkerboard 16 | pattern. In a traditional graph editor, you need many nodes to reproduce the 17 | math to create such a pattern. With the Hybird Graph, the entire checkerboard 18 | shader is a single node, and you can edit its source code and modify its 19 | uniforms. This hybrid editing creates a powerful and novel way to compose 20 | effects together. 21 | 22 | ![Hybrid Graph editor screenshot](/public/checkerboard-graph.png) 23 | ![Hybrid Graph editor screenshot](/public/checkerboard-glsl.png) 24 | 25 | # Engines (Three.js, Babylon, ...) 26 | 27 | The hybrid graph is a GLSL editor, and is not specific to an engine. Engines are 28 | implemented as [plugins](src/plugins/) to the hybrid graph. The same algorithm 29 | of shader composing can in theory work with any GLSL based engine. 30 | 31 | I've started with support for Three.js and Babylon. 32 | 33 | # Implementation 34 | 35 | The hybrid graph utilizes the compiler I wrote 36 | [@Shaderfrog/glsl-parser](https://github.com/ShaderFrog/glsl-parser) which among 37 | other things, exposed [a 38 | bug](https://bugs.chromium.org/p/angleproject/issues/detail?id=6338#c1) in 39 | Chrome's ANGLE compiler. 40 | 41 | # State of this demo 42 | 43 | This demo is a constant WIP. The main focus right now is on the UI/UX of the 44 | graph editor and core APIs. There is currently no DX (developer experience), 45 | meaning there is no way to export shaders to use in your own system, nor is there 46 | a way to use the Hybrid Graph as a standalone library. (Yet!) 47 | 48 | # Contact 49 | 50 | You can follow along on Twitter, and my DMs are open: 51 | 52 | - Twitter [@andrewray](https://twitter.com/andrewray) 53 | - Twitter [@shaderfrog](https://twitter.com/shaderfrog) 54 | - Mastodon [@andyray@mastodon.social](https://mastodon.social/@andyray) 55 | 56 | # Hacky Documentaiton 57 | 58 | ## How a RawShaderMaterial acts like a MeshPhysicalMaterial 59 | 60 | (Three.js flow, similar to Babylon flow). 61 | 62 | 1. The graph compiles all the nodes and sees there's a physical ndoe 63 | 2. It tells threngine to compile the megashader, which makes a new 64 | MeshPhysicalMaterial() 65 | 3. The properties of this material are based on the nodes in the graph, because 66 | to replace a "map" uniform, the material needs a "map" property so that the 67 | guts of three will add that uniform to the GLSL and then we can do the source 68 | code replcaement. 69 | 4. The material also gets specific properties set on the material, like 70 | isMeshStandardMaterial, which is a required switch 71 | (https://github.com/mrdoob/three.js/blob/e7042de7c1a2c70e38654a04b6fd97d9c978e781/src/renderers/webgl/WebGLMaterials.js#L42-L49) 72 | to get some uniforms on the material for example the transmissionRenderTarget 73 | which is a private variable of the WebGLRenderer 74 | (https://github.com/mrdoob/three.js/blob/e7042de7c1a2c70e38654a04b6fd97d9c978e781/src/renderers/WebGLRenderer.js#L1773) 75 | 5. Shaderfrog copies all the properties from the material onto the raw shader 76 | material. Properties like "transmission" are set with getters and need to be 77 | updated manually 78 | 6. The same needs to be done at runtime for uniforms, so "ior" needs to be set 79 | as a property of the runtime material, which explains why my material looked 80 | different when I set isMeshPhysicalMaterial = true, it started overwriting 81 | that uniform every render. 82 | 83 | # Data Flow 84 | 85 | ## Initial Page Load 86 | 87 | - On page load, the Graph is intialized from the URL param in Editor.tsx in 88 | `makeExampleGraph` 89 | - Then the three scene mounts, which passes newly created context up to 90 | Editor.tsx 91 | - This first generates the Flow Elements from the Graph using `graphToFlowGraph()` 92 | - Then Editor.tsx calls `initializeGraph()`, which: 93 | - First computes context for the graph 94 | - Calls compileGraphAsync() which calls `compileGraph()` which processes the 95 | Graph 96 | - Graph elements are re-copied into Flow Elements using `setFlowElements()` 97 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | 'next/babel', 4 | ['@babel/preset-env', { targets: { node: 'current' } }], 5 | '@babel/preset-typescript', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | npm run build 5 | npm run export 6 | scp -r out/* aray:/var/www/frogger 7 | 8 | # This hoses local development, so nuke the folder after 9 | rm -rf .next 10 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | module.exports = { 5 | reactStrictMode: true, 6 | webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { 7 | config.resolve = { 8 | ...config.resolve, 9 | alias: { 10 | ...config.resolve.alias, 11 | babylonjs: path.resolve( 12 | __dirname, 13 | 'node_modules/babylonjs/babylon.max.js' 14 | ), 15 | 'babylonjs-loaders': path.resolve( 16 | __dirname, 17 | 'node_modules/babylonjs-loaders/babylonjs.loaders.js' 18 | ), 19 | }, 20 | }; 21 | 22 | // Hard code the "@shaderfrog/core" npm link'd path so that we can load its 23 | // typescript files directly (aka @shaderfrog/core/src/...) 24 | config.module.rules = config.module.rules.map((rule) => 25 | /\bts\b/.test(rule.test?.toString()) 26 | ? { 27 | ...rule, 28 | include: [ 29 | ...rule.include, 30 | path.resolve(__dirname, '../core-shaderfrog'), 31 | ], 32 | } 33 | : rule 34 | ); 35 | 36 | // Important: return the modified config 37 | return config; 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "deploy": "./deploy.sh", 8 | "build": "next build", 9 | "export": "next export", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "watch-test": "jest --watch" 13 | }, 14 | "dependencies": { 15 | "@monaco-editor/react": "^4.3.1", 16 | "@playcanvas/observer": "^1.1.0", 17 | "@playcanvas/pcui": "^2.2.4", 18 | "@playcanvas/pcui-graph": "^1.0.2", 19 | "@react-hook/resize-observer": "^1.2.6", 20 | "@shaderfrog/core": "^0.0.1", 21 | "@shaderfrog/glsl-parser": "^1.2.0", 22 | "@types/lodash.groupby": "^4.6.7", 23 | "@types/three": "^0.131.0", 24 | "babylonjs": "^5.43.0", 25 | "classnames": "^2.3.1", 26 | "is-what": "^4.0.0", 27 | "litegraph.js": "^0.7.10", 28 | "lodash.debounce": "^4.0.8", 29 | "lodash.groupby": "^4.6.0", 30 | "lodash.throttle": "^4.1.1", 31 | "next": "11.1.0", 32 | "react": "17.0.2", 33 | "react-dom": "17.0.2", 34 | "react-hotkeys-hook": "^3.4.6", 35 | "react-multi-split-pane": "^0.3.2", 36 | "reactflow": "^11.2.0", 37 | "three": "^0.149.0", 38 | "zustand": "^4.0.0-rc.1" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.15.5", 42 | "@babel/preset-env": "^7.15.6", 43 | "@babel/preset-typescript": "^7.15.0", 44 | "@types/jest": "^27.0.2", 45 | "@types/lodash.debounce": "^4.0.7", 46 | "@types/lodash.throttle": "^4.1.6", 47 | "@types/react": "17.0.19", 48 | "babel-jest": "^27.2.2", 49 | "eslint": "7.32.0", 50 | "eslint-config-next": "11.1.0", 51 | "jest": "^27.2.2", 52 | "prettier": "^2.3.2", 53 | "typescript": "^4.5.4", 54 | "typescript-plugin-css-modules": "^3.4.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/3tone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/3tone.jpg -------------------------------------------------------------------------------- /public/Big_pebbles_pxr128.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/Big_pebbles_pxr128.jpeg -------------------------------------------------------------------------------- /public/Big_pebbles_pxr128_bmp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/Big_pebbles_pxr128_bmp.jpeg -------------------------------------------------------------------------------- /public/Big_pebbles_pxr128_normal.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/Big_pebbles_pxr128_normal.jpeg -------------------------------------------------------------------------------- /public/blank-normal-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/blank-normal-map.png -------------------------------------------------------------------------------- /public/brick-texture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/brick-texture.jpeg -------------------------------------------------------------------------------- /public/bricknormal.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/bricknormal.jpeg -------------------------------------------------------------------------------- /public/bricknormal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/bricknormal.png -------------------------------------------------------------------------------- /public/bricks.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/bricks.jpeg -------------------------------------------------------------------------------- /public/checkerboard-glsl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/checkerboard-glsl.png -------------------------------------------------------------------------------- /public/checkerboard-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/checkerboard-graph.png -------------------------------------------------------------------------------- /public/contrast-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/contrast-noise.png -------------------------------------------------------------------------------- /public/envmaps/citycourtyard.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/citycourtyard.dds -------------------------------------------------------------------------------- /public/envmaps/empty_warehouse_01_2k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/empty_warehouse_01_2k.hdr -------------------------------------------------------------------------------- /public/envmaps/environmentSpecular.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/environmentSpecular.env -------------------------------------------------------------------------------- /public/envmaps/pond/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/pond/negx.jpg -------------------------------------------------------------------------------- /public/envmaps/pond/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/pond/negy.jpg -------------------------------------------------------------------------------- /public/envmaps/pond/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/pond/negz.jpg -------------------------------------------------------------------------------- /public/envmaps/pond/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/pond/posx.jpg -------------------------------------------------------------------------------- /public/envmaps/pond/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/pond/posy.jpg -------------------------------------------------------------------------------- /public/envmaps/pond/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/pond/posz.jpg -------------------------------------------------------------------------------- /public/envmaps/room.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/envmaps/room.hdr -------------------------------------------------------------------------------- /public/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/explosion.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/favicon.ico -------------------------------------------------------------------------------- /public/grayscale-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/grayscale-noise.png -------------------------------------------------------------------------------- /public/hybrid-graph-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewRayCode/shaderfrog-2.0-hybrid-graph-demo/a22ef0e387c3a0b718e311a5c4370021bf7800d0/public/hybrid-graph-screenshot.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/babylon-vertex.ts: -------------------------------------------------------------------------------- 1 | const vertex = ` 2 | precision highp float; 3 | layout(std140,column_major) uniform; 4 | uniform Material 5 | { 6 | uniform vec2 vAlbedoInfos; 7 | uniform vec4 vAmbientInfos; 8 | uniform vec2 vOpacityInfos; 9 | uniform vec2 vEmissiveInfos; 10 | uniform vec2 vLightmapInfos; 11 | uniform vec3 vReflectivityInfos; 12 | uniform vec2 vMicroSurfaceSamplerInfos; 13 | uniform vec2 vReflectionInfos; 14 | uniform vec2 vReflectionFilteringInfo; 15 | uniform vec3 vReflectionPosition; 16 | uniform vec3 vReflectionSize; 17 | uniform vec3 vBumpInfos; 18 | uniform mat4 albedoMatrix; 19 | uniform mat4 ambientMatrix; 20 | uniform mat4 opacityMatrix; 21 | uniform mat4 emissiveMatrix; 22 | uniform mat4 lightmapMatrix; 23 | uniform mat4 reflectivityMatrix; 24 | uniform mat4 microSurfaceSamplerMatrix; 25 | uniform mat4 bumpMatrix; 26 | uniform vec2 vTangentSpaceParams; 27 | uniform mat4 reflectionMatrix; 28 | uniform vec3 vReflectionColor; 29 | uniform vec4 vAlbedoColor; 30 | uniform vec4 vLightingIntensity; 31 | uniform vec3 vReflectionMicrosurfaceInfos; 32 | uniform float pointSize; 33 | uniform vec4 vReflectivityColor; 34 | uniform vec3 vEmissiveColor; 35 | uniform float visibility; 36 | uniform vec4 vMetallicReflectanceFactors; 37 | uniform vec2 vMetallicReflectanceInfos; 38 | uniform mat4 metallicReflectanceMatrix; 39 | uniform vec2 vClearCoatParams; 40 | uniform vec4 vClearCoatRefractionParams; 41 | uniform vec4 vClearCoatInfos; 42 | uniform mat4 clearCoatMatrix; 43 | uniform mat4 clearCoatRoughnessMatrix; 44 | uniform vec2 vClearCoatBumpInfos; 45 | uniform vec2 vClearCoatTangentSpaceParams; 46 | uniform mat4 clearCoatBumpMatrix; 47 | uniform vec4 vClearCoatTintParams; 48 | uniform float clearCoatColorAtDistance; 49 | uniform vec2 vClearCoatTintInfos; 50 | uniform mat4 clearCoatTintMatrix; 51 | uniform vec3 vAnisotropy; 52 | uniform vec2 vAnisotropyInfos; 53 | uniform mat4 anisotropyMatrix; 54 | uniform vec4 vSheenColor; 55 | uniform float vSheenRoughness; 56 | uniform vec4 vSheenInfos; 57 | uniform mat4 sheenMatrix; 58 | uniform mat4 sheenRoughnessMatrix; 59 | uniform vec3 vRefractionMicrosurfaceInfos; 60 | uniform vec2 vRefractionFilteringInfo; 61 | uniform vec4 vRefractionInfos; 62 | uniform mat4 refractionMatrix; 63 | uniform vec2 vThicknessInfos; 64 | uniform mat4 thicknessMatrix; 65 | uniform vec2 vThicknessParam; 66 | uniform vec3 vDiffusionDistance; 67 | uniform vec4 vTintColor; 68 | uniform vec3 vSubSurfaceIntensity; 69 | uniform float scatteringDiffusionProfile; 70 | uniform vec4 vDetailInfos; 71 | uniform mat4 detailMatrix; 72 | }; 73 | uniform Scene { 74 | mat4 viewProjection; 75 | mat4 view; 76 | }; 77 | #define CUSTOM_VERTEX_BEGIN 78 | in vec3 position; 79 | in vec3 normal; 80 | in vec2 uv; 81 | out vec2 vMainUV1; 82 | const float PI=3.1415926535897932384626433832795; 83 | const float HALF_MIN=5.96046448e-08; 84 | const float LinearEncodePowerApprox=2.2; 85 | const float GammaEncodePowerApprox=1.0/LinearEncodePowerApprox; 86 | const vec3 LuminanceEncodeApprox=vec3(0.2126,0.7152,0.0722); 87 | const float Epsilon=0.0000001; 88 | #define saturate(x) clamp(x,0.0,1.0) 89 | #define absEps(x) abs(x)+Epsilon 90 | #define maxEps(x) max(x,Epsilon) 91 | #define saturateEps(x) clamp(x,Epsilon,1.0) 92 | mat3 transposeMat3(mat3 inMatrix) { 93 | vec3 i0=inMatrix[0]; 94 | vec3 i1=inMatrix[1]; 95 | vec3 i2=inMatrix[2]; 96 | mat3 outMatrix=mat3( 97 | vec3(i0.x,i1.x,i2.x), 98 | vec3(i0.y,i1.y,i2.y), 99 | vec3(i0.z,i1.z,i2.z) 100 | ); 101 | return outMatrix; 102 | } 103 | mat3 inverseMat3(mat3 inMatrix) { 104 | float a00=inMatrix[0][0],a01=inMatrix[0][1],a02=inMatrix[0][2]; 105 | float a10=inMatrix[1][0],a11=inMatrix[1][1],a12=inMatrix[1][2]; 106 | float a20=inMatrix[2][0],a21=inMatrix[2][1],a22=inMatrix[2][2]; 107 | float b01=a22*a11-a12*a21; 108 | float b11=-a22*a10+a12*a20; 109 | float b21=a21*a10-a11*a20; 110 | float det=a00*b01+a01*b11+a02*b21; 111 | return mat3(b01,(-a22*a01+a02*a21),(a12*a01-a02*a11), 112 | b11,(a22*a00-a02*a20),(-a12*a00+a02*a10), 113 | b21,(-a21*a00+a01*a20),(a11*a00-a01*a10))/det; 114 | } 115 | float toLinearSpace(float color) 116 | { 117 | return pow(color,LinearEncodePowerApprox); 118 | } 119 | vec3 toLinearSpace(vec3 color) 120 | { 121 | return pow(color,vec3(LinearEncodePowerApprox)); 122 | } 123 | vec4 toLinearSpace(vec4 color) 124 | { 125 | return vec4(pow(color.rgb,vec3(LinearEncodePowerApprox)),color.a); 126 | } 127 | vec3 toGammaSpace(vec3 color) 128 | { 129 | return pow(color,vec3(GammaEncodePowerApprox)); 130 | } 131 | vec4 toGammaSpace(vec4 color) 132 | { 133 | return vec4(pow(color.rgb,vec3(GammaEncodePowerApprox)),color.a); 134 | } 135 | float toGammaSpace(float color) 136 | { 137 | return pow(color,GammaEncodePowerApprox); 138 | } 139 | float square(float value) 140 | { 141 | return value*value; 142 | } 143 | float pow5(float value) { 144 | float sq=value*value; 145 | return sq*sq*value; 146 | } 147 | float getLuminance(vec3 color) 148 | { 149 | return clamp(dot(color,LuminanceEncodeApprox),0.,1.); 150 | } 151 | float getRand(vec2 seed) { 152 | return fract(sin(dot(seed.xy ,vec2(12.9898,78.233)))*43758.5453); 153 | } 154 | float dither(vec2 seed,float varianceAmount) { 155 | float rand=getRand(seed); 156 | float dither=mix(-varianceAmount/255.0,varianceAmount/255.0,rand); 157 | return dither; 158 | } 159 | const float rgbdMaxRange=255.0; 160 | vec4 toRGBD(vec3 color) { 161 | float maxRGB=maxEps(max(color.r,max(color.g,color.b))); 162 | float D=max(rgbdMaxRange/maxRGB,1.); 163 | D=clamp(floor(D)/255.0,0.,1.); 164 | vec3 rgb=color.rgb*D; 165 | rgb=toGammaSpace(rgb); 166 | return vec4(rgb,D); 167 | } 168 | vec3 fromRGBD(vec4 rgbd) { 169 | rgbd.rgb=toLinearSpace(rgbd.rgb); 170 | return rgbd.rgb/rgbd.a; 171 | } 172 | uniform mat4 world; 173 | out vec3 vPositionW; 174 | out vec3 vNormalW; 175 | uniform Light0 176 | { 177 | vec4 vLightData; 178 | vec4 vLightDiffuse; 179 | vec4 vLightSpecular; 180 | vec3 vLightGround; 181 | vec4 shadowsInfo; 182 | vec2 depthValues; 183 | } light0; 184 | #define CUSTOM_VERTEX_DEFINITIONS 185 | void main(void) { 186 | #define CUSTOM_VERTEX_MAIN_BEGIN 187 | vec3 positionUpdated=position; 188 | vec3 normalUpdated=normal; 189 | vec2 uvUpdated=uv; 190 | #define CUSTOM_VERTEX_UPDATE_POSITION 191 | #define CUSTOM_VERTEX_UPDATE_NORMAL 192 | mat4 finalWorld=world; 193 | vec4 worldPos=finalWorld*vec4(positionUpdated,1.0); 194 | vPositionW=vec3(worldPos); 195 | mat3 normalWorld=mat3(finalWorld); 196 | vNormalW=normalize(normalWorld*normalUpdated); 197 | #define CUSTOM_VERTEX_UPDATE_WORLDPOS 198 | gl_Position=viewProjection*worldPos; 199 | vec2 uv2=vec2(0.,0.); 200 | vMainUV1=uvUpdated; 201 | #define CUSTOM_VERTEX_MAIN_END 202 | } 203 | `; 204 | export default vertex; 205 | -------------------------------------------------------------------------------- /src/monaco-glsl.ts: -------------------------------------------------------------------------------- 1 | export const monacoGlsl = (monaco: any) => { 2 | // Register a new language 3 | monaco.languages.register({ id: 'glsl' }); 4 | 5 | // Monarch language example https://microsoft.github.io/monaco-editor/monarch.html 6 | monaco.languages.setMonarchTokensProvider('glsl', { 7 | // Set defaultToken to invalid to see what you do not tokenize yet 8 | defaultToken: 'invalid', 9 | tokenPostfix: '.js', 10 | 11 | keywords: [ 12 | 'attribute', 13 | 'varying', 14 | 'const', 15 | 'bool', 16 | 'float', 17 | 'double', 18 | 'int', 19 | 'uint', 20 | 'break', 21 | 'continue', 22 | 'do', 23 | 'else', 24 | 'for', 25 | 'if', 26 | 'discard', 27 | 'return', 28 | 'switch', 29 | 'case', 30 | 'default', 31 | 'subroutine', 32 | 'bvec2', 33 | 'bvec3', 34 | 'bvec4', 35 | 'ivec2', 36 | 'ivec3', 37 | 'ivec4', 38 | 'uvec2', 39 | 'uvec3', 40 | 'uvec4', 41 | 'vec2', 42 | 'vec3', 43 | 'vec4', 44 | 'mat2', 45 | 'mat3', 46 | 'mat4', 47 | 'centroid', 48 | 'in', 49 | 'out', 50 | 'inout', 51 | 'uniform', 52 | 'patch', 53 | 'sample', 54 | 'buffer', 55 | 'shared', 56 | 'coherent', 57 | 'volatile', 58 | 'restrict', 59 | 'readonly', 60 | 'writeonly', 61 | 'dvec2', 62 | 'dvec3', 63 | 'dvec4', 64 | 'dmat2', 65 | 'dmat3', 66 | 'dmat4', 67 | 'noperspective', 68 | 'flat', 69 | 'smooth', 70 | 'layout', 71 | 'mat2x2', 72 | 'mat2x3', 73 | 'mat2x4', 74 | 'mat3x2', 75 | 'mat3x3', 76 | 'mat3x4', 77 | 'mat4x2', 78 | 'mat4x3', 79 | 'mat4x4', 80 | 'dmat2x2', 81 | 'dmat2x3', 82 | 'dmat2x4', 83 | 'dmat3x2', 84 | 'dmat3x3', 85 | 'dmat3x4', 86 | 'dmat4x2', 87 | 'dmat4x3', 88 | 'dmat4x4', 89 | 'atomic_uint', 90 | 'sampler1D', 91 | 'sampler2D', 92 | 'sampler3D', 93 | 'samplerCube', 94 | 'sampler1DShadow', 95 | 'sampler2DShadow', 96 | 'samplerCubeShadow', 97 | 'sampler1DArray', 98 | 'sampler2DArray', 99 | 'sampler1DArrayShadow', 100 | 'sampler2DArrayshadow', 101 | 'isampler1D', 102 | 'isampler2D', 103 | 'isampler3D', 104 | 'isamplerCube', 105 | 'isampler1Darray', 106 | 'isampler2DArray', 107 | 'usampler1D', 108 | 'usampler2D', 109 | 'usampler3D', 110 | 'usamplerCube', 111 | 'usampler1DArray', 112 | 'usampler2DArray', 113 | 'sampler2DRect', 114 | 'sampler2DRectshadow', 115 | 'isampler2DRect', 116 | 'usampler2DRect', 117 | 'samplerBuffer', 118 | 'isamplerBuffer', 119 | 'usamplerBuffer', 120 | 'samplerCubeArray', 121 | 'samplerCubeArrayShadow', 122 | 'isamplerCubeArray', 123 | 'usamplerCubeArray', 124 | 'sampler2DMS', 125 | 'isampler2DMS', 126 | 'usampler2DMS', 127 | 'sampler2DMSArray', 128 | 'isampler2DMSArray', 129 | 'usampler2DMSArray', 130 | 'image1D', 131 | 'iimage1D', 132 | 'uimage1D', 133 | 'image2D', 134 | 'iimage2D', 135 | 'uimage2D', 136 | 'image3D', 137 | 'iimage3D', 138 | 'uimage3D', 139 | 'image2DRect', 140 | 'iimage2DRect', 141 | 'uimage2DRect', 142 | 'imageCube', 143 | 'iimageCube', 144 | 'uimageCube', 145 | 'imageBuffer', 146 | 'iimageBuffer', 147 | 'uimageBuffer', 148 | 'image1DArray', 149 | 'iimage1DArray', 150 | 'uimage1DArray', 151 | 'image2DArray', 152 | 'iimage2DArray', 153 | 'uimage2DArray', 154 | 'imageCubeArray', 155 | 'iimageCubeArray', 156 | 'uimageCubeArray', 157 | 'image2DMS', 158 | 'iimage2DMS', 159 | 'uimage2DMS', 160 | 'image2DMArray', 161 | 'iimage2DMSArray', 162 | 'uimage2DMSArray', 163 | 'struct', 164 | 'void', 165 | 'while', 166 | 'invariant', 167 | 'precise', 168 | 'highp', 169 | 'mediump', 170 | 'lowp', 171 | 'precision', 172 | ], 173 | 174 | typeKeywords: ['any', 'boolean', 'number', 'object', 'string', 'undefined'], 175 | 176 | operators: [ 177 | '<=', 178 | '>=', 179 | '==', 180 | '!=', 181 | '===', 182 | '!==', 183 | '=>', 184 | '+', 185 | '-', 186 | '**', 187 | '*', 188 | '/', 189 | '%', 190 | '++', 191 | '--', 192 | '<<', 193 | '>', 195 | '>>>', 196 | '&', 197 | '|', 198 | '^', 199 | '!', 200 | '~', 201 | '&&', 202 | '||', 203 | '?', 204 | ':', 205 | '=', 206 | '+=', 207 | '-=', 208 | '*=', 209 | '**=', 210 | '/=', 211 | '%=', 212 | '<<=', 213 | '>>=', 214 | '>>>=', 215 | '&=', 216 | '|=', 217 | '^=', 218 | '@', 219 | ], 220 | 221 | // we include these common regular expressions 222 | symbols: /[=>](?!@symbols)/, '@brackets'], 265 | [ 266 | /@symbols/, 267 | { 268 | cases: { 269 | '@operators': 'delimiter', 270 | '@default': '', 271 | }, 272 | }, 273 | ], 274 | 275 | // numbers 276 | [/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'], 277 | [/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'], 278 | [/0[xX](@hexdigits)/, 'number.hex'], 279 | [/0[oO]?(@octaldigits)/, 'number.octal'], 280 | [/0[bB](@binarydigits)/, 'number.binary'], 281 | [/(@digits)/, 'number'], 282 | 283 | // delimiter: after number because of .\d floats 284 | [/[;,.]/, 'delimiter'], 285 | 286 | // strings 287 | [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string 288 | [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string 289 | [/"/, 'string', '@string_double'], 290 | [/'/, 'string', '@string_single'], 291 | [/`/, 'string', '@string_backtick'], 292 | ], 293 | 294 | whitespace: [ 295 | [/[ \t\r\n]+/, ''], 296 | [/\/\*/, 'comment', '@comment'], 297 | [/\/\/.*$/, 'comment'], 298 | ], 299 | 300 | comment: [ 301 | [/[^\/*]+/, 'comment'], 302 | [/\*\//, 'comment', '@pop'], 303 | [/[\/*]/, 'comment'], 304 | ], 305 | 306 | preprocessor: [[/^#.*$/, 'preprocessor']], 307 | 308 | // We match regular expression quite precisely 309 | regexp: [ 310 | [ 311 | /(\{)(\d+(?:,\d*)?)(\})/, 312 | [ 313 | 'regexp.escape.control', 314 | 'regexp.escape.control', 315 | 'regexp.escape.control', 316 | ], 317 | ], 318 | [ 319 | /(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, 320 | [ 321 | 'regexp.escape.control', 322 | { token: 'regexp.escape.control', next: '@regexrange' }, 323 | ], 324 | ], 325 | [ 326 | /(\()(\?:|\?=|\?!)/, 327 | ['regexp.escape.control', 'regexp.escape.control'], 328 | ], 329 | [/[()]/, 'regexp.escape.control'], 330 | [/@regexpctl/, 'regexp.escape.control'], 331 | [/[^\\\/]/, 'regexp'], 332 | [/@regexpesc/, 'regexp.escape'], 333 | [/\\\./, 'regexp.invalid'], 334 | [ 335 | /(\/)([gimsuy]*)/, 336 | [ 337 | { token: 'regexp', bracket: '@close', next: '@pop' }, 338 | 'keyword.other', 339 | ], 340 | ], 341 | ], 342 | 343 | regexrange: [ 344 | [/-/, 'regexp.escape.control'], 345 | [/\^/, 'regexp.invalid'], 346 | [/@regexpesc/, 'regexp.escape'], 347 | [/[^\]]/, 'regexp'], 348 | [ 349 | /\]/, 350 | { token: 'regexp.escape.control', next: '@pop', bracket: '@close' }, 351 | ], 352 | ], 353 | 354 | string_double: [ 355 | [/[^\\"]+/, 'string'], 356 | [/@escapes/, 'string.escape'], 357 | [/\\./, 'string.escape.invalid'], 358 | [/"/, 'string', '@pop'], 359 | ], 360 | 361 | string_single: [ 362 | [/[^\\']+/, 'string'], 363 | [/@escapes/, 'string.escape'], 364 | [/\\./, 'string.escape.invalid'], 365 | [/'/, 'string', '@pop'], 366 | ], 367 | 368 | string_backtick: [ 369 | [/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }], 370 | [/[^\\`$]+/, 'string'], 371 | [/@escapes/, 'string.escape'], 372 | [/\\./, 'string.escape.invalid'], 373 | [/`/, 'string', '@pop'], 374 | ], 375 | 376 | bracketCounting: [ 377 | [/\{/, 'delimiter.bracket', '@bracketCounting'], 378 | [/\}/, 'delimiter.bracket', '@pop'], 379 | { include: 'common' }, 380 | ], 381 | }, 382 | }); 383 | }; 384 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../site/styles/globals.css'; 2 | import '../site/styles/forms.css'; 3 | import '../site/styles/flow.theme.css'; 4 | import '../site/styles/resizer.custom.css'; 5 | import type { AppProps } from 'next/app'; 6 | 7 | function MyApp({ Component, pageProps }: AppProps) { 8 | return ; 9 | } 10 | 11 | export default MyApp; 12 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/editor/editor.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .gutter { 7 | background: green; 8 | width: 10px; 9 | height: 100%; 10 | } 11 | 12 | .splitInner { 13 | position: reltive; 14 | width: 100%; 15 | } 16 | 17 | .leftCol { 18 | overflow: auto; 19 | resize: horizontal; 20 | border: 1px solid #000; 21 | } 22 | 23 | .edges { 24 | width: 100%; 25 | background: #010101; 26 | color: rgb(227, 252, 0); 27 | } 28 | 29 | .shader { 30 | width: 100%; 31 | min-height: 360px; 32 | background: #040404; 33 | color: #fff; 34 | } 35 | 36 | textarea.error { 37 | border: 1px inset red; 38 | } 39 | 40 | .button { 41 | align-items: center; 42 | margin: 1px; 43 | cursor: pointer; 44 | padding: 6px 14px; 45 | font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif; 46 | border-radius: 6px; 47 | border: none; 48 | 49 | color: #fff; 50 | background: linear-gradient(180deg, #4b91f7 0%, #367af6 100%); 51 | background-origin: border-box; 52 | box-shadow: 0px 0.5px 1.5px rgba(54, 122, 246, 0.25), 53 | inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2); 54 | user-select: none; 55 | -webkit-user-select: none; 56 | touch-action: manipulation; 57 | } 58 | 59 | .button:disabled { 60 | background: linear-gradient(0, #4b91f7 0%, #367af6 100%); 61 | color: #111; 62 | text-shadow: 2px ​1px 2px #aaa; 63 | } 64 | 65 | .button:focus { 66 | box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2), 67 | 0px 0.5px 1.5px rgba(54, 122, 246, 0.25), 68 | 0px 0px 0px 3.5px rgba(58, 108, 217, 0.5); 69 | outline: 0; 70 | } 71 | 72 | .editor { 73 | display: grid; 74 | grid-template-columns: 43px 1fr; 75 | max-height: 400px; 76 | overflow: scroll; 77 | } 78 | 79 | .sidebar { 80 | white-space: pre; 81 | font-family: monospace; 82 | color: #666; 83 | background: #000; 84 | font-size: 14px; 85 | user-select: none; 86 | line-height: 1.2em; 87 | padding: 0 7px; 88 | } 89 | 90 | .code { 91 | border: 0; 92 | font-size: 14px; 93 | min-height: 360px; 94 | background: #111; 95 | color: #fff; 96 | line-height: 1.2em; 97 | overflow: hidden; 98 | resize: none; 99 | } 100 | 101 | .codeError { 102 | background: rgb(46, 0, 0); 103 | color: rgb(255, 161, 161); 104 | font-family: monospace; 105 | white-space: break-spaces; 106 | padding: 10px; 107 | } 108 | 109 | /* editor specific tab styles */ 110 | .errored { 111 | background: rgb(176, 0, 0); 112 | } 113 | 114 | .selected.errored, 115 | .secondary .tab_tab.selected.errored { 116 | background: rgb(84, 0, 0); 117 | } 118 | 119 | .secondary { 120 | background: #444; 121 | } 122 | .secondary .tab_tab { 123 | background: #444; 124 | } 125 | .secondary .tab_tab.selected { 126 | background: #111; 127 | } 128 | .secondary .tab_tab.errored { 129 | background: rgb(176, 0, 0); 130 | } 131 | 132 | .scene { 133 | position: relative; 134 | background: #222; 135 | } 136 | .tabBar { 137 | height: 28px; 138 | padding: 2px 0 0; 139 | } 140 | .tabControls { 141 | position: absolute; 142 | top: 2px; 143 | right: 2px; 144 | z-index: 10; 145 | display: grid; 146 | grid-template-columns: auto 1fr; 147 | } 148 | .tabControls.col3 { 149 | grid-template-columns: auto 1fr 1fr; 150 | } 151 | 152 | .activeEngine { 153 | color: #fff; 154 | font-size: 14px; 155 | line-height: 14px; 156 | text-shadow: 0 0 10px #000; 157 | } 158 | 159 | .uiGroup { 160 | margin: 5px; 161 | border: 1px inset #444; 162 | border-radius: 4px; 163 | box-shadow: 0 0 10px inset #222; 164 | color: #fff; 165 | padding: 20px 25px; 166 | background: linear-gradient(45deg, #1a1a1a, rgb(41, 41, 41)); 167 | } 168 | 169 | .uiHeader { 170 | padding: 0; 171 | margin: 0 0 10px; 172 | font-size: 16px; 173 | font-weight: bold; 174 | } 175 | 176 | .uiInput { 177 | margin: 1px; 178 | padding: 3px 14px; 179 | font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif; 180 | border-radius: 6px; 181 | border: none; 182 | padding: 8px 16px; 183 | 184 | color: #fff; 185 | background: linear-gradient(180deg, #222 0%, #202020 100%); 186 | background-origin: border-box; 187 | box-shadow: 0px 1px 1px rgb(114, 114, 114), 188 | inset 0px 0.8px 0px -0.25px rgb(0, 0, 0); 189 | } 190 | .uiInput[readonly] { 191 | background: linear-gradient(180deg, #303030 0%, #2d2d2d 100%); 192 | color: #ddd; 193 | } 194 | 195 | .formButton { 196 | align-items: center; 197 | margin: 1px; 198 | cursor: pointer; 199 | padding: 3px 14px; 200 | font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif; 201 | border-radius: 6px; 202 | border: 1px solid #999; 203 | 204 | color: #fff; 205 | background: linear-gradient(180deg, #333 0%, #1f1f1f 100%); 206 | background-origin: border-box; 207 | box-shadow: 0px 1px -1px rgb(255, 255, 255), 208 | inset 0px 0.8px 0px 0.25px rgb(51, 51, 51); 209 | user-select: none; 210 | -webkit-user-select: none; 211 | touch-action: manipulation; 212 | } 213 | 214 | .formButton:disabled { 215 | background: linear-gradient(180deg, rgb(77, 77, 77) 0%, #414141 100%); 216 | color: #ddd; 217 | text-shadow: 2px ​1px 2px #aaa; 218 | } 219 | 220 | .editorControls { 221 | position: relative; 222 | } 223 | 224 | .sceneControls { 225 | padding: 6px 12px; 226 | background: linear-gradient(45deg, #1a1a1a, rgb(41, 41, 41)); 227 | } 228 | 229 | .controlGrid { 230 | display: grid; 231 | grid-template-columns: auto 1fr; 232 | gap: 4px; 233 | } 234 | 235 | .nodeEditorPanel { 236 | background: #2a2a2a; 237 | overflow: scroll; 238 | } 239 | .belowTabs { 240 | top: 29px !important; 241 | position: absolute; 242 | left: 0; 243 | right: 0; 244 | bottom: 0; 245 | } 246 | 247 | .colcolauto { 248 | display: grid; 249 | grid-template-columns: repeat(2, minmax(auto, 200px)) 1fr; 250 | gap: 4px; 251 | } 252 | .autocolmax { 253 | display: grid; 254 | grid-template-columns: auto minmax(auto, 200px) 1fr; 255 | gap: 4px; 256 | } 257 | 258 | .reactFlowWrapper { 259 | flex-grow: 1; 260 | /* approximate height of tabs :( */ 261 | height: calc(100% - 28px); 262 | } 263 | 264 | .sceneContainer { 265 | overflow: hidden; 266 | height: 100%; 267 | width: 100%; 268 | } 269 | 270 | .babylonContainer { 271 | height: 100%; 272 | width: 100%; 273 | } 274 | 275 | .babylonContainer canvas { 276 | height: 100%; 277 | width: 100%; 278 | } 279 | 280 | .sceneAndControls { 281 | height: 100vh; 282 | display: grid; 283 | } 284 | .sceneSmallScreen { 285 | height: calc(100vh - 47px); 286 | } 287 | 288 | .guiMsg { 289 | position: absolute; 290 | bottom: 10px; 291 | left: 10px; 292 | color: #fff; 293 | } 294 | .guiError { 295 | position: absolute; 296 | bottom: 0; 297 | left: 0; 298 | right: 0; 299 | background: #fee; 300 | color: #f00; 301 | padding: 10px; 302 | } 303 | 304 | .compiling { 305 | position: absolute; 306 | bottom: 0; 307 | left: 0; 308 | right: 0; 309 | padding: 10px; 310 | text-align: center; 311 | } 312 | .compiling:before { 313 | content: ''; 314 | position: absolute; 315 | top: 0; 316 | right: 0; 317 | bottom: 0; 318 | left: 0; 319 | background: linear-gradient( 320 | rgba(200, 200, 200, 0.7), 321 | rgba(150, 150, 150, 0.6) 322 | ); 323 | z-index: 1; 324 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); 325 | } 326 | .compiling span { 327 | color: #fff; 328 | text-shadow: 0 0 5px #000; 329 | position: relative; 330 | z-index: 2; 331 | } 332 | 333 | .graphFooter { 334 | position: absolute; 335 | bottom: 0; 336 | left: 0; 337 | background: linear-gradient(#31382a, #2b3028); 338 | color: #fff; 339 | border-top: 1px solid #47553e; 340 | padding: 3px 12px 4px; 341 | text-align: center; 342 | font-size: 12px; 343 | width: 100%; 344 | box-shadow: 0 0 20px #000; 345 | } 346 | 347 | .graphFooter span { 348 | color: #66715e; 349 | padding: 0 4px; 350 | } 351 | .graphFooter a { 352 | color: #a6cb00; 353 | } 354 | .graphFooter a:hover { 355 | text-decoration: underline; 356 | color: #d1f72a; 357 | } 358 | -------------------------------------------------------------------------------- /src/pages/editor/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | 3 | const DynamicComponentWithNoSSR = dynamic( 4 | () => import('../../site/components/Editor'), 5 | { 6 | ssr: false, 7 | } 8 | ); 9 | 10 | function Editor() { 11 | return ; 12 | } 13 | 14 | export default Editor; 15 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Head from 'next/head'; 3 | import Link from 'next/link'; 4 | import styles from '../site/styles/Home.module.css'; 5 | import Router from 'next/router'; 6 | import { useEffect } from 'react'; 7 | 8 | const Home: NextPage = () => { 9 | useEffect(() => { 10 | const isLocal = window.location.toString().includes('localhost'); 11 | Router.push(isLocal ? '/editor' : '/editor.html'); 12 | }, []); 13 | return ( 14 |
15 | 16 | Shaderfrog 2.0 Hybrid Graph Demo 17 | 18 | 19 | 20 | 21 | /editor 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Home; 28 | -------------------------------------------------------------------------------- /src/plugins/babylon/examples.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from '@shaderfrog/core/src/core/graph'; 2 | import { 3 | colorNode, 4 | DataNode, 5 | numberNode, 6 | numberUniformData, 7 | textureNode, 8 | vectorUniformData, 9 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 10 | import { EdgeType, makeEdge } from '@shaderfrog/core/src/core/nodes/edge'; 11 | import { outputNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 12 | import { fireFrag, fireVert } from '../../shaders/fireNode'; 13 | import { 14 | heatShaderFragmentNode, 15 | heatShaderVertexNode, 16 | variation1 as heatmapV1, 17 | } from '../../shaders/heatmapShaderNode'; 18 | import purpleNoiseNode from '../../shaders/purpleNoiseNode'; 19 | import staticShaderNode, { variation1 } from '../../shaders/staticShaderNode'; 20 | import { makeId } from '../../util/id'; 21 | import { checkerboardF, checkerboardV } from '../../shaders/checkboardNode'; 22 | import normalMapify from '../../shaders/normalmapifyNode'; 23 | import { 24 | convertNode, 25 | convertToEngine, 26 | Engine, 27 | } from '@shaderfrog/core/src/core/engine'; 28 | import { babylengine } from '@shaderfrog/core/src/plugins/babylon/bablyengine'; 29 | import { CoreNode } from '@shaderfrog/core/src/core/nodes/core-node'; 30 | import { SourceNode } from '@shaderfrog/core/src/core/nodes/code-nodes'; 31 | 32 | export enum Example { 33 | GLASS_FIREBALL = 'Glass Fireball', 34 | // GEMSTONE = 'Gemstone', 35 | LIVING_DIAMOND = 'Living Diamond', 36 | // TOON = 'Toon', 37 | DEFAULT = 'Mesh Physical Material', 38 | } 39 | 40 | const edgeFrom = ( 41 | fromNode: CoreNode, 42 | toId: string, 43 | input: string, 44 | type?: EdgeType 45 | ) => makeEdge(makeId(), fromNode.id, toId, outFrom(fromNode), input, type); 46 | 47 | const outFrom = (node: CoreNode) => node.outputs[0].name; 48 | 49 | const konvert = (node: SourceNode) => 50 | convertNode(node, babylengine.importers.three); 51 | 52 | export const makeExampleGraph = (example: Example): [Graph, string, string] => { 53 | console.log('🌈 Making new graph!!'); 54 | let newGraph: Graph; 55 | let previewObject: string; 56 | let bg: string = ''; 57 | 58 | // @ts-ignore 59 | if (example === Example.GEMSTONE) { 60 | const outputF = outputNode( 61 | makeId(), 62 | 'Output', 63 | { x: 434, y: -97 }, 64 | 'fragment' 65 | ); 66 | const outputV = outputNode(makeId(), 'Output', { x: 434, y: 20 }, 'vertex'); 67 | 68 | const physicalGroupId = makeId(); 69 | const physicalF = babylengine.constructors.physical( 70 | makeId(), 71 | 'Physical', 72 | physicalGroupId, 73 | { x: 178, y: -103 }, 74 | [], 75 | 'fragment' 76 | ); 77 | const physicalV = babylengine.constructors.physical( 78 | makeId(), 79 | 'Physical', 80 | physicalGroupId, 81 | { x: 434, y: 130 }, 82 | [], 83 | 'vertex', 84 | physicalF.id 85 | ); 86 | const staticF = konvert( 87 | staticShaderNode(makeId(), { x: -196, y: -303 }, variation1) 88 | ); 89 | const heatmap = konvert( 90 | heatShaderFragmentNode(makeId(), { x: -478, y: 12 }, heatmapV1) 91 | ); 92 | const heatmapV = konvert( 93 | heatShaderVertexNode(makeId(), heatmap.id, { 94 | x: -478, 95 | y: -194, 96 | }) 97 | ); 98 | 99 | const normaled = normalMapify(makeId(), { x: -178, y: -149 }); 100 | const normalStrength = numberNode( 101 | makeId(), 102 | 'Normal Strength', 103 | { x: -482, y: -105 }, 104 | '1..0' 105 | ); 106 | 107 | const color = colorNode(makeId(), 'Color', { x: -187, y: -413 }, [ 108 | '1.0', 109 | '0.75', 110 | '1.0', 111 | ]); 112 | const roughness = numberNode( 113 | makeId(), 114 | 'Roughness', 115 | { x: -187, y: 54 }, 116 | '0.37' 117 | ); 118 | // const transmission = numberNode( 119 | // makeId(), 120 | // 'Transmission', 121 | // { x: -187, y: 153 }, 122 | // '0.5' 123 | // ); 124 | // const thickness = numberNode( 125 | // makeId(), 126 | // 'Thickness', 127 | // { x: -187, y: 240 }, 128 | // '1.0' 129 | // ); 130 | // const ior = numberNode(makeId(), 'Ior', { x: -187, y: 328 }, '2.0'); 131 | 132 | newGraph = { 133 | nodes: [ 134 | color, 135 | normaled, 136 | normalStrength, 137 | roughness, 138 | // transmission, 139 | // thickness, 140 | // ior, 141 | staticF, 142 | heatmap, 143 | heatmapV, 144 | outputF, 145 | outputV, 146 | physicalF, 147 | physicalV, 148 | ], 149 | edges: [ 150 | edgeFrom(physicalF, outputF.id, 'filler_frogFragOut', 'fragment'), 151 | edgeFrom(physicalV, outputV.id, 'filler_gl_Position', 'vertex'), 152 | edgeFrom(staticF, physicalF.id, 'property_albedoTexture', 'fragment'), 153 | edgeFrom(color, physicalF.id, 'property_albedoColor', 'fragment'), 154 | edgeFrom(roughness, physicalF.id, 'property_roughness', 'fragment'), 155 | 156 | // edgeFrom( 157 | // transmission, 158 | // physicalF.id, 159 | // 'property_transmission', 160 | // 'fragment' 161 | // ), 162 | // edgeFrom(thickness, physicalF.id, 'property_thickness', 'fragment'), 163 | // edgeFrom(ior, physicalF.id, 'property_ior', 'fragment'), 164 | edgeFrom( 165 | normalStrength, 166 | normaled.id, 167 | 'uniform_normal_strength', 168 | 'fragment' 169 | ), 170 | edgeFrom(heatmap, normaled.id, 'filler_normal_map', 'fragment'), 171 | edgeFrom(normaled, physicalF.id, 'property_bumpTexture', 'fragment'), 172 | ], 173 | }; 174 | previewObject = 'icosahedron'; 175 | bg = 'cityCourtYard'; 176 | // @ts-ignore 177 | } else if (example === Example.TOON) { 178 | const outputF = outputNode( 179 | makeId(), 180 | 'Output', 181 | { x: 434, y: -97 }, 182 | 'fragment' 183 | ); 184 | const outputV = outputNode(makeId(), 'Output', { x: 434, y: 16 }, 'vertex'); 185 | 186 | const toonGroupId = makeId(); 187 | const toonF = babylengine.constructors.toon( 188 | makeId(), 189 | 'Toon', 190 | toonGroupId, 191 | { x: 178, y: -103 }, 192 | [], 193 | 'fragment' 194 | ); 195 | const toonV = babylengine.constructors.toon( 196 | makeId(), 197 | 'Toon', 198 | toonGroupId, 199 | { x: 434, y: 130 }, 200 | [], 201 | 'vertex', 202 | toonF.id 203 | ); 204 | const properties: [string, DataNode][] = [ 205 | [ 206 | 'albedoColor', 207 | colorNode(makeId(), 'Color', { x: -153, y: -268 }, ['0', '0.7', '0']), 208 | ], 209 | // [ 210 | // 'gradientMap', 211 | // textureNode( 212 | // makeId(), 213 | // 'Gradient Map', 214 | // { x: -153, y: -160 }, 215 | // 'threeTone' 216 | // ), 217 | // ], 218 | [ 219 | 'bumpTexture', 220 | textureNode( 221 | makeId(), 222 | 'Bump Texture', 223 | { x: -153, y: -50 }, 224 | 'brickNormal' 225 | ), 226 | ], 227 | ]; 228 | 229 | newGraph = { 230 | nodes: [outputF, outputV, toonF, toonV, ...properties.map(([, p]) => p)], 231 | edges: [ 232 | edgeFrom(toonF, outputF.id, 'filler_frogFragOut', 'fragment'), 233 | edgeFrom(toonV, outputV.id, 'filler_gl_Position', 'vertex'), 234 | ...properties.map(([name, prop]) => 235 | edgeFrom(prop, toonF.id, `property_${name}`, prop.type) 236 | ), 237 | ], 238 | }; 239 | previewObject = 'torusknot'; 240 | bg = ''; 241 | } else if (example === Example.DEFAULT) { 242 | const outputF = outputNode( 243 | makeId(), 244 | 'Output', 245 | { x: 434, y: -97 }, 246 | 'fragment' 247 | ); 248 | const outputV = outputNode(makeId(), 'Output', { x: 434, y: 16 }, 'vertex'); 249 | 250 | const physicalGroupId = makeId(); 251 | const physicalF = babylengine.constructors.physical( 252 | makeId(), 253 | 'Physical', 254 | physicalGroupId, 255 | { x: 178, y: -103 }, 256 | [], 257 | 'fragment' 258 | ); 259 | const physicalV = babylengine.constructors.physical( 260 | makeId(), 261 | 'Physical', 262 | physicalGroupId, 263 | { x: 434, y: 130 }, 264 | [], 265 | 'vertex', 266 | physicalF.id 267 | ); 268 | 269 | const checkerboardf = checkerboardF(makeId(), { x: -162, y: -105 }); 270 | const checkerboardv = checkerboardV(makeId(), checkerboardf.id, { 271 | x: -162, 272 | y: 43, 273 | }); 274 | newGraph = { 275 | nodes: [ 276 | outputF, 277 | outputV, 278 | physicalF, 279 | physicalV, 280 | checkerboardf, 281 | checkerboardv, 282 | ], 283 | edges: [ 284 | edgeFrom(physicalF, outputF.id, 'filler_frogFragOut', 'fragment'), 285 | edgeFrom(physicalV, outputV.id, 'filler_gl_Position', 'vertex'), 286 | edgeFrom( 287 | checkerboardf, 288 | physicalF.id, 289 | 'property_albedoTexture', 290 | 'fragment' 291 | ), 292 | ], 293 | }; 294 | previewObject = 'sphere'; 295 | bg = ''; 296 | } else if (example === Example.LIVING_DIAMOND) { 297 | const outputF = outputNode( 298 | makeId(), 299 | 'Output', 300 | { x: 434, y: -97 }, 301 | 'fragment' 302 | ); 303 | const outputV = outputNode(makeId(), 'Output', { x: 434, y: 16 }, 'vertex'); 304 | 305 | const nMap = konvert(normalMapify(makeId(), { x: -185, y: 507 })); 306 | 307 | const purpleNoise = konvert( 308 | purpleNoiseNode(makeId(), { x: -512, y: 434 }, [ 309 | numberUniformData( 310 | 'speed', 311 | babylengine.name === 'babylon' ? '1.0' : '0.2' 312 | ), 313 | numberUniformData('brightnessX', '1.0'), 314 | numberUniformData('permutations', '10'), 315 | numberUniformData('iterations', '2'), 316 | vectorUniformData( 317 | 'uvScale', 318 | babylengine.name === 'babylon' ? ['0.1', '0.1'] : ['0.9', '0.9'] 319 | ), 320 | vectorUniformData('color1', ['0', '1', '1']), 321 | vectorUniformData('color2', ['1', '0', '1']), 322 | vectorUniformData('color3', ['1', '1', '0']), 323 | ]) 324 | ); 325 | 326 | const properties = [ 327 | numberNode(makeId(), 'Metallic', { x: -185, y: -110 }, '0.1'), 328 | numberNode(makeId(), 'Roughness', { x: -185, y: 0 }, '0.055'), 329 | numberNode(makeId(), 'Alpha', { x: -185, y: 110 }, '0.2'), 330 | ]; 331 | const ior = numberNode( 332 | makeId(), 333 | 'Index Of Refraction', 334 | { x: -110, y: 62 }, 335 | '1.05' 336 | ); 337 | 338 | const physicalGroupId = makeId(); 339 | const physicalF = babylengine.constructors.physical( 340 | makeId(), 341 | 'Physical', 342 | physicalGroupId, 343 | { x: 178, y: -103 }, 344 | [], 345 | 'fragment' 346 | ); 347 | const physicalV = babylengine.constructors.physical( 348 | makeId(), 349 | 'Physical', 350 | physicalGroupId, 351 | { x: 434, y: 130 }, 352 | [], 353 | 'vertex', 354 | physicalF.id 355 | ); 356 | 357 | newGraph = { 358 | nodes: [ 359 | outputF, 360 | outputV, 361 | physicalF, 362 | physicalV, 363 | purpleNoise, 364 | nMap, 365 | ior, 366 | ...properties, 367 | ], 368 | edges: [ 369 | edgeFrom(physicalF, outputF.id, 'filler_frogFragOut', 'fragment'), 370 | edgeFrom(physicalV, outputV.id, 'filler_gl_Position', 'vertex'), 371 | edgeFrom(purpleNoise, nMap.id, 'filler_normal_map', 'fragment'), 372 | edgeFrom(nMap, physicalF.id, 'property_bumpTexture', 'fragment'), 373 | edgeFrom(ior, physicalF.id, `property_indexOfRefraction`, 'number'), 374 | ...properties.map((prop) => 375 | edgeFrom( 376 | prop, 377 | physicalF.id, 378 | `property_${prop.name.toLowerCase()}`, 379 | prop.type 380 | ) 381 | ), 382 | ], 383 | }; 384 | previewObject = 'icosahedron'; 385 | bg = 'cityCourtYard'; 386 | } else if (example === Example.GLASS_FIREBALL) { 387 | const outputF = outputNode( 388 | makeId(), 389 | 'Output', 390 | { x: 434, y: -97 }, 391 | 'fragment' 392 | ); 393 | const outputV = outputNode(makeId(), 'Output', { x: 434, y: 20 }, 'vertex'); 394 | 395 | const physicalGroupId = makeId(); 396 | const physicalF = babylengine.constructors.physical( 397 | makeId(), 398 | 'Physical', 399 | physicalGroupId, 400 | { x: 178, y: -103 }, 401 | [], 402 | 'fragment' 403 | ); 404 | const physicalV = babylengine.constructors.physical( 405 | makeId(), 406 | 'Physical', 407 | physicalGroupId, 408 | { x: 434, y: 130 }, 409 | [], 410 | 'vertex', 411 | physicalF.id 412 | ); 413 | const fireF = konvert(fireFrag(makeId(), { x: -88, y: -120 })); 414 | const fireV = konvert( 415 | fireVert(makeId(), fireF.id, { x: -88, y: 610 }, [ 416 | numberUniformData('fireSpeed', '0.768'), 417 | numberUniformData('pulseHeight', '0.0'), 418 | numberUniformData('displacementHeight', '0.481'), 419 | numberUniformData('turbulenceDetail', '0.907'), 420 | ]) 421 | ); 422 | 423 | const roughness = numberNode( 424 | makeId(), 425 | 'Roughness', 426 | { x: -103, y: -16 }, 427 | '0.1' 428 | ); 429 | const metalness = numberNode( 430 | makeId(), 431 | 'Metalness', 432 | { x: -103, y: 62 }, 433 | '0.09' 434 | ); 435 | const alpha = numberNode(makeId(), 'Alpha', { x: -103, y: 62 }, '0.0'); 436 | const ior = numberNode( 437 | makeId(), 438 | 'Index Of Refraction', 439 | { x: -110, y: 62 }, 440 | '1.05' 441 | ); 442 | 443 | newGraph = { 444 | nodes: [ 445 | alpha, 446 | ior, 447 | roughness, 448 | metalness, 449 | fireF, 450 | fireV, 451 | outputF, 452 | outputV, 453 | physicalF, 454 | physicalV, 455 | ], 456 | edges: [ 457 | edgeFrom(physicalF, outputF.id, 'filler_frogFragOut', 'fragment'), 458 | edgeFrom(physicalV, outputV.id, 'filler_gl_Position', 'vertex'), 459 | edgeFrom(fireF, physicalF.id, 'property_albedoTexture', 'fragment'), 460 | edgeFrom(roughness, physicalF.id, 'property_roughness', 'fragment'), 461 | edgeFrom(metalness, physicalF.id, 'property_metallic', 'fragment'), 462 | edgeFrom(alpha, physicalF.id, 'property_alpha', 'fragment'), 463 | edgeFrom(ior, physicalF.id, 'property_indexOfRefraction', 'fragment'), 464 | edgeFrom(fireV, physicalV.id, 'filler_position', 'vertex'), 465 | ], 466 | }; 467 | previewObject = 'sphere'; 468 | bg = 'cityCourtYard'; 469 | } else { 470 | const outputF = outputNode( 471 | makeId(), 472 | 'Output', 473 | { x: 434, y: -97 }, 474 | 'fragment' 475 | ); 476 | const outputV = outputNode(makeId(), 'Output', { x: 434, y: 20 }, 'vertex'); 477 | 478 | const physicalGroupId = makeId(); 479 | const physicalF = babylengine.constructors.physical( 480 | makeId(), 481 | 'Physical', 482 | physicalGroupId, 483 | { x: 178, y: -103 }, 484 | [], 485 | 'fragment' 486 | ); 487 | const physicalV = babylengine.constructors.physical( 488 | makeId(), 489 | 'Physical', 490 | physicalGroupId, 491 | { x: 434, y: 130 }, 492 | [], 493 | 'vertex', 494 | physicalF.id 495 | ); 496 | 497 | const purpleNoise = konvert(purpleNoiseNode(makeId(), { x: -100, y: 0 })); 498 | 499 | newGraph = { 500 | nodes: [purpleNoise, outputF, outputV, physicalF, physicalV], 501 | edges: [ 502 | edgeFrom(physicalF, outputF.id, 'filler_frogFragOut', 'fragment'), 503 | edgeFrom(physicalV, outputV.id, 'filler_gl_Position', 'vertex'), 504 | edgeFrom( 505 | purpleNoise, 506 | physicalF.id, 507 | 'property_albedoTexture', 508 | 'fragment' 509 | ), 510 | ], 511 | }; 512 | previewObject = 'torusknot'; 513 | } 514 | 515 | return [newGraph, previewObject, bg]; 516 | }; 517 | -------------------------------------------------------------------------------- /src/plugins/babylon/index.ts: -------------------------------------------------------------------------------- 1 | import BabylonComponent from './BabylonComponent'; 2 | 3 | export { BabylonComponent as Editor }; 4 | 5 | export * from './examples'; 6 | -------------------------------------------------------------------------------- /src/plugins/babylon/useBabylon.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState, useContext } from 'react'; 2 | import * as BABYLON from 'babylonjs'; 3 | import { useHoisty } from '../../site/hoistedRefContext'; 4 | 5 | type SceneData = { 6 | lights: BABYLON.Node[]; 7 | mesh?: BABYLON.Mesh; 8 | }; 9 | type ScenePersistence = { 10 | sceneData: SceneData; 11 | canvas: HTMLCanvasElement; 12 | engine: BABYLON.Engine; 13 | scene: BABYLON.Scene; 14 | camera: BABYLON.ArcRotateCamera; 15 | loadingMaterial: BABYLON.Material; 16 | }; 17 | 18 | type Callback = (time: number) => void; 19 | 20 | export const useBabylon = (callback: Callback) => { 21 | const { getRefData } = useHoisty(); 22 | 23 | const { loadingMaterial, engine, camera, sceneData, canvas, scene } = 24 | getRefData('babylon', () => { 25 | const canvas = document.createElement('canvas'); 26 | const engine = new BABYLON.Engine(canvas, true, { 27 | preserveDrawingBuffer: true, 28 | stencil: true, 29 | }); 30 | const scene = new BABYLON.Scene(engine); 31 | const loadingMaterial = new BABYLON.StandardMaterial('mat2', scene); 32 | loadingMaterial.emissiveColor = new BABYLON.Color3(0.8, 0.2, 0.5); 33 | // scene.createDefaultEnvironment(); 34 | // This line makes the object disappear on page load - race condition? 35 | // Bad shader compile? 36 | // scene.environmentTexture = hdrTexture; 37 | return { 38 | sceneData: { 39 | lights: [], 40 | }, 41 | canvas, 42 | engine, 43 | scene, 44 | loadingMaterial, 45 | camera: new BABYLON.ArcRotateCamera( 46 | 'camera1', 47 | Math.PI / 2, 48 | Math.PI / 2, 49 | 4, 50 | new BABYLON.Vector3(0, 0, 0), 51 | scene 52 | ), 53 | destroy: (data: ScenePersistence) => { 54 | console.log('👋🏻 Bye Bye Babylon!'); 55 | data.scene.dispose(); 56 | data.engine.dispose(); 57 | }, 58 | }; 59 | }); 60 | 61 | const [babylonDom, setBabylonDom] = useState(null); 62 | const babylonDomRef = useCallback((node) => setBabylonDom(node), []); 63 | 64 | const frameRef = useRef(0); 65 | 66 | useEffect(() => { 67 | // Target the camera to scene origin 68 | camera.setTarget(BABYLON.Vector3.Zero()); 69 | // Attach the camera to the canvas 70 | camera.attachControl(canvas, false); 71 | }, [scene, camera, canvas]); 72 | 73 | const savedCallback = useRef(callback); 74 | // Remember the latest callback. 75 | useEffect(() => { 76 | savedCallback.current = callback; 77 | }, [callback]); 78 | 79 | useEffect(() => { 80 | if (babylonDom && !babylonDom.childNodes.length) { 81 | console.log('Re-attaching Babylon DOM', canvas, 'to', babylonDom); 82 | babylonDom.appendChild(canvas); 83 | } 84 | }, [canvas, babylonDom]); 85 | 86 | const animate = useCallback( 87 | (time: number) => { 88 | scene.render(); 89 | savedCallback.current(time); 90 | 91 | frameRef.current = requestAnimationFrame(animate); 92 | }, 93 | [scene] 94 | ); 95 | 96 | useEffect(() => { 97 | if (babylonDom) { 98 | console.log('🎬 Starting Babylon requestAnimationFrame'); 99 | frameRef.current = requestAnimationFrame(animate); 100 | } 101 | 102 | return () => { 103 | console.log('🛑 Cleaning up Babylon animationframe'); 104 | if (frameRef.current) { 105 | cancelAnimationFrame(frameRef.current); 106 | } 107 | // TODO: How to cleanup? 108 | // engine.dispose(); 109 | }; 110 | }, [engine, animate, babylonDom]); 111 | 112 | return { 113 | canvas, 114 | babylonDomRef, 115 | engine, 116 | scene, 117 | camera, 118 | sceneData, 119 | loadingMaterial, 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /src/plugins/three/RoomEnvironment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts 3 | */ 4 | 5 | import { 6 | BackSide, 7 | BoxGeometry, 8 | Mesh, 9 | MeshBasicMaterial, 10 | MeshStandardMaterial, 11 | PointLight, 12 | Scene, 13 | } from 'three'; 14 | 15 | class RoomEnvironment extends Scene { 16 | constructor() { 17 | super(); 18 | 19 | const geometry = new BoxGeometry(); 20 | geometry.deleteAttribute('uv'); 21 | 22 | const roomMaterial = new MeshStandardMaterial({ side: BackSide }); 23 | const boxMaterial = new MeshStandardMaterial(); 24 | 25 | const mainLight = new PointLight(0xffffff, 5.0, 28, 2); 26 | mainLight.position.set(0.418, 16.199, 0.3); 27 | this.add(mainLight); 28 | 29 | const room = new Mesh(geometry, roomMaterial); 30 | room.position.set(-0.757, 13.219, 0.717); 31 | room.scale.set(31.713, 28.305, 28.591); 32 | this.add(room); 33 | 34 | const box1 = new Mesh(geometry, boxMaterial); 35 | box1.position.set(-10.906, 2.009, 1.846); 36 | box1.rotation.set(0, -0.195, 0); 37 | box1.scale.set(2.328, 7.905, 4.651); 38 | this.add(box1); 39 | 40 | const box2 = new Mesh(geometry, boxMaterial); 41 | box2.position.set(-5.607, -0.754, -0.758); 42 | box2.rotation.set(0, 0.994, 0); 43 | box2.scale.set(1.97, 1.534, 3.955); 44 | this.add(box2); 45 | 46 | const box3 = new Mesh(geometry, boxMaterial); 47 | box3.position.set(6.167, 0.857, 7.803); 48 | box3.rotation.set(0, 0.561, 0); 49 | box3.scale.set(3.927, 6.285, 3.687); 50 | this.add(box3); 51 | 52 | const box4 = new Mesh(geometry, boxMaterial); 53 | box4.position.set(-2.017, 0.018, 6.124); 54 | box4.rotation.set(0, 0.333, 0); 55 | box4.scale.set(2.002, 4.566, 2.064); 56 | this.add(box4); 57 | 58 | const box5 = new Mesh(geometry, boxMaterial); 59 | box5.position.set(2.291, -0.756, -2.621); 60 | box5.rotation.set(0, -0.286, 0); 61 | box5.scale.set(1.546, 1.552, 1.496); 62 | this.add(box5); 63 | 64 | const box6 = new Mesh(geometry, boxMaterial); 65 | box6.position.set(-2.193, -0.369, -5.547); 66 | box6.rotation.set(0, 0.516, 0); 67 | box6.scale.set(3.875, 3.487, 2.986); 68 | this.add(box6); 69 | 70 | // -x right 71 | const light1 = new Mesh(geometry, createAreaLightMaterial(50)); 72 | light1.position.set(-16.116, 14.37, 8.208); 73 | light1.scale.set(0.1, 2.428, 2.739); 74 | this.add(light1); 75 | 76 | // -x left 77 | const light2 = new Mesh(geometry, createAreaLightMaterial(50)); 78 | light2.position.set(-16.109, 18.021, -8.207); 79 | light2.scale.set(0.1, 2.425, 2.751); 80 | this.add(light2); 81 | 82 | // +x 83 | const light3 = new Mesh(geometry, createAreaLightMaterial(17)); 84 | light3.position.set(14.904, 12.198, -1.832); 85 | light3.scale.set(0.15, 4.265, 6.331); 86 | this.add(light3); 87 | 88 | // +z 89 | const light4 = new Mesh(geometry, createAreaLightMaterial(43)); 90 | light4.position.set(-0.462, 8.89, 14.52); 91 | light4.scale.set(4.38, 5.441, 0.088); 92 | this.add(light4); 93 | 94 | // -z 95 | const light5 = new Mesh(geometry, createAreaLightMaterial(20)); 96 | light5.position.set(3.235, 11.486, -12.541); 97 | light5.scale.set(2.5, 2.0, 0.1); 98 | this.add(light5); 99 | 100 | // +y 101 | const light6 = new Mesh(geometry, createAreaLightMaterial(100)); 102 | light6.position.set(0.0, 20.0, 0.0); 103 | light6.scale.set(1.0, 0.1, 1.0); 104 | this.add(light6); 105 | } 106 | 107 | dispose() { 108 | const resources = new Set(); 109 | 110 | this.traverse((object) => { 111 | if ((object as Mesh).isMesh) { 112 | resources.add((object as Mesh).geometry); 113 | resources.add((object as Mesh).material); 114 | } 115 | }); 116 | 117 | for (const resource of resources) { 118 | resource.dispose(); 119 | } 120 | } 121 | } 122 | 123 | function createAreaLightMaterial(intensity: number) { 124 | const material = new MeshBasicMaterial(); 125 | material.color.setScalar(intensity); 126 | return material; 127 | } 128 | 129 | export { RoomEnvironment }; 130 | -------------------------------------------------------------------------------- /src/plugins/three/index.ts: -------------------------------------------------------------------------------- 1 | import ThreeComponent from './ThreeComponent'; 2 | 3 | export { ThreeComponent as Editor }; 4 | 5 | export * from './examples'; 6 | -------------------------------------------------------------------------------- /src/plugins/three/useThree.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 2 | import { useCallback, useEffect, useRef, useState, useContext } from 'react'; 3 | import * as three from 'three'; 4 | 5 | import { useHoisty } from '../../site/hoistedRefContext'; 6 | 7 | type Callback = (time: number) => void; 8 | 9 | type SceneData = { 10 | helpers: three.Object3D[]; 11 | lights: three.Object3D[]; 12 | mesh?: three.Mesh; 13 | bg?: three.Mesh; 14 | }; 15 | type ScenePersistence = { 16 | sceneData: SceneData; 17 | scene: three.Scene; 18 | camera: three.Camera; 19 | cubeCamera: three.CubeCamera; 20 | cubeRenderTarget: three.WebGLCubeRenderTarget; 21 | renderer: three.WebGLRenderer; 22 | }; 23 | 24 | export const useThree = (callback: Callback) => { 25 | const { getRefData } = useHoisty(); 26 | const { sceneData, scene, cubeCamera, camera, renderer } = 27 | getRefData('three', () => { 28 | const cubeRenderTarget = new three.WebGLCubeRenderTarget(128, { 29 | generateMipmaps: true, 30 | minFilter: three.LinearMipmapLinearFilter, 31 | }); 32 | return { 33 | sceneData: { 34 | lights: [], 35 | helpers: [], 36 | }, 37 | scene: new three.Scene(), 38 | camera: new three.PerspectiveCamera(75, 1 / 1, 0.1, 1000), 39 | cubeCamera: new three.CubeCamera(0.1, 1000, cubeRenderTarget), 40 | cubeRenderTarget, 41 | renderer: new three.WebGLRenderer(), 42 | destroy: (data: ScenePersistence) => { 43 | console.log('👋🏻 Bye Bye Three.js!'); 44 | data.renderer.forceContextLoss(); 45 | // @ts-ignore 46 | data.renderer.domElement = null; 47 | }, 48 | }; 49 | }); 50 | 51 | const [threeDomElement, setThreeDom] = useState(null); 52 | // We use a callback ref to handle re-attaching scene controls when the 53 | // scene unmounts or re-mounts 54 | const threeDomCbRef = useCallback((node) => setThreeDom(node), []); 55 | 56 | const frameRef = useRef(0); 57 | const controlsRef = useRef(); 58 | 59 | useEffect(() => { 60 | if (!scene.children.find((child: any) => child === camera)) { 61 | camera.position.set(0, 0, 2); 62 | camera.lookAt(0, 0, 0); 63 | scene.add(camera); 64 | scene.add(cubeCamera); 65 | } 66 | }, [scene, camera, cubeCamera]); 67 | 68 | const savedCallback = useRef(callback); 69 | // Remember the latest callback. 70 | useEffect(() => { 71 | savedCallback.current = callback; 72 | }, [callback]); 73 | 74 | useEffect(() => { 75 | if (threeDomElement && !threeDomElement.childNodes.length) { 76 | console.log( 77 | 'Re-attaching three.js DOM and instantiate OrbitControls, appendingx', 78 | renderer.domElement, 79 | 'to', 80 | threeDomElement 81 | ); 82 | threeDomElement.appendChild(renderer.domElement); 83 | const controls = new OrbitControls(camera, renderer.domElement); 84 | controls.update(); 85 | controlsRef.current = controls; 86 | } 87 | }, [camera, renderer, threeDomElement]); 88 | 89 | const animate = useCallback( 90 | (time: number) => { 91 | if (controlsRef.current) { 92 | controlsRef.current.update(); 93 | } 94 | renderer.render(scene, camera); 95 | savedCallback.current(time); 96 | 97 | frameRef.current = requestAnimationFrame(animate); 98 | }, 99 | [camera, renderer, scene] 100 | ); 101 | 102 | useEffect(() => { 103 | if (threeDomElement) { 104 | console.log('🎬 Starting requestAnimationFrame'); 105 | frameRef.current = requestAnimationFrame(animate); 106 | } 107 | 108 | return () => { 109 | console.log('🛑 Cleaning up Three animationframe'); 110 | if (controlsRef.current) { 111 | controlsRef.current.dispose(); 112 | } 113 | if (frameRef.current) { 114 | cancelAnimationFrame(frameRef.current); 115 | } 116 | }; 117 | }, [animate, threeDomElement]); 118 | 119 | return { sceneData, threeDomElement, threeDomCbRef, scene, camera, renderer }; 120 | }; 121 | -------------------------------------------------------------------------------- /src/shaders/badTvNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | numberUniformData, 4 | textureUniformData, 5 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 6 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 7 | import { 8 | texture2DStrategy, 9 | uniformStrategy, 10 | } from '@shaderfrog/core/src/core/strategy'; 11 | 12 | const badTvFrag = (id: string, position: NodePosition) => 13 | sourceNode( 14 | id, 15 | 'Bad TV', 16 | position, 17 | { 18 | version: 2, 19 | preprocess: true, 20 | strategies: [uniformStrategy(), texture2DStrategy()], 21 | uniforms: [ 22 | textureUniformData('image', 'bricks'), 23 | numberUniformData('distortion', '3.0', [0, 10]), 24 | numberUniformData('distortion2', '5.0', [0, 10]), 25 | numberUniformData('speed', '0.2', [-1, 1]), 26 | numberUniformData('rollSpeed', '0.1', [-1, 1]), 27 | ], 28 | }, 29 | `/** 30 | * Adapted from: 31 | * @author Felix Turner / www.airtight.cc / @felixturner 32 | * MIT License: https://github.com/felixturner/bad-tv-shader/blob/6d279ebb6d0962c7d409d801424a3b7303efeec9/BadTVShader.js#L15 33 | */ 34 | precision highp float; 35 | precision highp int; 36 | 37 | uniform sampler2D image; 38 | uniform float time; 39 | uniform float distortion; 40 | uniform float distortion2; 41 | uniform float speed; 42 | uniform float rollSpeed; 43 | varying vec2 vUv; 44 | 45 | vec3 mod289(vec3 x) { 46 | return x - floor(x * (1.0 / 289.0)) * 289.0; 47 | } 48 | vec2 mod289(vec2 x) { 49 | return x - floor(x * (1.0 / 289.0)) * 289.0; 50 | } 51 | vec3 permute(vec3 x) { 52 | return mod289(((x*34.0)+1.0)*x); 53 | } 54 | float snoise(vec2 v) { 55 | const vec4 C = vec4( 56 | 0.211324865405187, 57 | 0.366025403784439, 58 | -0.577350269189626, 59 | 0.024390243902439 60 | ); 61 | vec2 i = floor(v + dot(v, C.yy) ); 62 | vec2 x0 = v - i + dot(i, C.xx); 63 | vec2 i1; 64 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 65 | vec4 x12 = x0.xyxy + C.xxzz; 66 | x12.xy -= i1; 67 | i = mod289(i); // Avoid truncation effects in permutation 68 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) 69 | + i.x + vec3(0.0, i1.x, 1.0 )); 70 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); 71 | m = m*m ; 72 | m = m*m ; 73 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 74 | vec3 h = abs(x) - 0.5; 75 | vec3 ox = floor(x + 0.5); 76 | vec3 a0 = x - ox; 77 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); 78 | vec3 g; 79 | g.x = a0.x * x0.x + h.x * x0.y; 80 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 81 | return 130.0 * dot(m, g); 82 | } 83 | void main() { 84 | vec2 p = vUv; 85 | float ty = time*speed; 86 | float yt = p.y - ty; 87 | float offset = snoise(vec2(yt*3.0,0.0))*0.2; 88 | offset = offset*distortion * offset*distortion * offset; 89 | offset += snoise(vec2(yt*50.0,0.0))*distortion2*0.001; 90 | vec3 color = texture2D( 91 | image, 92 | vec2(fract(p.x + offset), fract(p.y-time*rollSpeed)) 93 | ).rgb; 94 | gl_FragColor = vec4( 95 | color + sin(vUv.x * 100.0), 96 | 1.0 97 | ); 98 | } 99 | `, 100 | 'fragment', 101 | 'three' 102 | ); 103 | 104 | export { badTvFrag }; 105 | -------------------------------------------------------------------------------- /src/shaders/checkboardNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | numberUniformData, 4 | UniformDataType, 5 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 6 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 7 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 8 | 9 | const checkerboardF = (id: string, position: NodePosition) => 10 | sourceNode( 11 | id, 12 | 'Checkerboard', 13 | position, 14 | { 15 | version: 2, 16 | preprocess: true, 17 | strategies: [uniformStrategy()], 18 | uniforms: [ 19 | numberUniformData('multiplicationFactor', '12.0', [2, 100], 1), 20 | ], 21 | }, 22 | ` 23 | precision highp float; 24 | precision highp int; 25 | 26 | uniform float multiplicationFactor; 27 | 28 | varying vec2 vUv; 29 | varying vec3 vPosition; 30 | 31 | void main() { 32 | vec2 t = vUv * multiplicationFactor; 33 | vec3 p = vPosition * multiplicationFactor; 34 | vec3 color; 35 | 36 | if (mod(floor(t.x) + floor(t.y), 2.0) == 1.0) { 37 | color = vec3( 1.0, 1.0, 1.0 ); 38 | } else { 39 | color = vec3( 0.0, 0.0, 0.0 ); 40 | } 41 | gl_FragColor = vec4(color, 1.0); 42 | 43 | } 44 | `, 45 | 'fragment', 46 | 'three' 47 | ); 48 | 49 | const checkerboardV = ( 50 | id: string, 51 | nextStageNodeId: string, 52 | position: NodePosition 53 | ) => 54 | sourceNode( 55 | id, 56 | 'Checkerboard', 57 | position, 58 | { 59 | version: 2, 60 | preprocess: true, 61 | strategies: [uniformStrategy()], 62 | uniforms: [], 63 | }, 64 | ` 65 | precision highp float; 66 | precision highp int; 67 | 68 | uniform mat4 modelMatrix; 69 | uniform mat4 modelViewMatrix; 70 | uniform mat4 projectionMatrix; 71 | uniform mat4 viewMatrix; 72 | uniform mat3 normalMatrix; 73 | 74 | attribute vec3 position; 75 | attribute vec3 normal; 76 | attribute vec2 uv; 77 | attribute vec2 uv2; 78 | 79 | varying vec2 vUv; 80 | varying vec3 vPosition; 81 | 82 | void main() { 83 | vUv = uv; 84 | vPosition = position; 85 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 86 | } 87 | `, 88 | 'vertex', 89 | 'three', 90 | nextStageNodeId 91 | ); 92 | 93 | export { checkerboardF, checkerboardV }; 94 | -------------------------------------------------------------------------------- /src/shaders/cubemapReflectionNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { samplerCubeUniformData } from '@shaderfrog/core/src/core/nodes/data-nodes'; 3 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 4 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 5 | 6 | const cubemapReflectionF = (id: string, position: NodePosition) => 7 | sourceNode( 8 | id, 9 | 'cubemapReflection', 10 | position, 11 | { 12 | version: 2, 13 | preprocess: true, 14 | strategies: [uniformStrategy()], 15 | uniforms: [samplerCubeUniformData('reflectionSampler', 'pondCubeMap')], 16 | }, 17 | ` 18 | precision highp float; 19 | precision highp int; 20 | 21 | varying vec3 vReflect; 22 | 23 | uniform float mirrorReflection; 24 | uniform samplerCube reflectionSampler; 25 | 26 | void main() { 27 | vec4 cubeColor = texture( reflectionSampler, vReflect ); 28 | gl_FragColor = vec4(cubeColor.rgb, 1.0); 29 | } 30 | `, 31 | 'fragment', 32 | 'three' 33 | ); 34 | 35 | const cubemapReflectionV = ( 36 | id: string, 37 | nextStageNodeId: string, 38 | position: NodePosition 39 | ) => 40 | sourceNode( 41 | id, 42 | 'cubemapReflection', 43 | position, 44 | { 45 | version: 2, 46 | preprocess: true, 47 | strategies: [uniformStrategy()], 48 | uniforms: [], 49 | }, 50 | ` 51 | precision highp float; 52 | precision highp int; 53 | 54 | uniform mat4 modelMatrix; 55 | uniform mat4 modelViewMatrix; 56 | uniform mat4 projectionMatrix; 57 | uniform mat4 viewMatrix; 58 | uniform mat3 normalMatrix; 59 | uniform vec3 cameraPosition; 60 | 61 | attribute vec3 position; 62 | attribute vec3 normal; 63 | attribute vec2 uv; 64 | attribute vec2 uv2; 65 | 66 | varying vec3 vReflect; 67 | 68 | void main() { 69 | vec3 worldPosition = ( modelMatrix * vec4( position, 1.0 )).xyz; 70 | vec3 cameraToVertex = normalize( worldPosition - cameraPosition ); 71 | vec3 worldNormal = normalize( 72 | mat3( modelMatrix[ 0 ].xyz, modelMatrix[ 1 ].xyz, modelMatrix[ 2 ].xyz ) * normal 73 | ); 74 | vReflect = reflect( cameraToVertex, worldNormal ); 75 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 76 | } 77 | `, 78 | 'vertex', 79 | 'three', 80 | nextStageNodeId 81 | ); 82 | 83 | export { cubemapReflectionF, cubemapReflectionV }; 84 | -------------------------------------------------------------------------------- /src/shaders/fireNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | numberUniformData, 4 | textureUniformData, 5 | UniformDataType, 6 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 7 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 8 | import { 9 | namedAttributeStrategy, 10 | uniformStrategy, 11 | } from '@shaderfrog/core/src/core/strategy'; 12 | 13 | const fireFrag = (id: string, position: NodePosition) => 14 | sourceNode( 15 | id, 16 | 'Fireball', 17 | position, 18 | { 19 | version: 2, 20 | preprocess: true, 21 | strategies: [uniformStrategy()], 22 | uniforms: [textureUniformData('tExplosion', 'explosion')], 23 | }, 24 | ` 25 | // Indstiller presisionen, hvor meget plads denne type variabel må bruge (high betyder meget plads) 26 | precision highp float; 27 | 28 | // Varying er en variabel som interpolader for fragments, mellem hver vertex. 29 | varying float noise; 30 | 31 | uniform sampler2D tExplosion; 32 | 33 | void main() { 34 | float offset = .02; 35 | float brightness = 0.8; 36 | 37 | // Det her er ikke helt depth, men mere hvor lys kuglen er. Da farven sættes ud fra dens dybde (se nærmere på brugen af noise i Vertex, for at finde ud om det). higher is dimmer 38 | float depth = 0.22; 39 | 40 | // lookup vertically i texturen, ved brug af noise og offset (lidt ligesom normal) 41 | // For at få de rigtige RGB værdier 42 | vec2 tPos = vec2( 0, ( brightness + depth ) * noise + offset ); 43 | vec4 color = texture2D( tExplosion, ( brightness - depth ) - tPos ); 44 | gl_FragColor = vec4( color.rgb, 1.0 ); 45 | } 46 | 47 | `, 48 | 'fragment', 49 | 'three' 50 | ); 51 | 52 | const fireVert = ( 53 | id: string, 54 | nextStageNodeId: string, 55 | position: NodePosition, 56 | uniforms?: UniformDataType[] 57 | ) => 58 | sourceNode( 59 | id, 60 | 'Fireball', 61 | position, 62 | { 63 | version: 2, 64 | preprocess: true, 65 | strategies: [uniformStrategy(), namedAttributeStrategy('position')], 66 | 67 | uniforms: uniforms || [ 68 | numberUniformData('fireSpeed', '0.6321'), 69 | numberUniformData('pulseHeight', '0.1'), 70 | numberUniformData('displacementHeight', '0.612'), 71 | numberUniformData('turbulenceDetail', '0.8'), 72 | ], 73 | }, 74 | ` 75 | uniform mat4 modelMatrix; 76 | uniform mat4 modelViewMatrix; 77 | uniform mat4 projectionMatrix; 78 | uniform mat4 viewMatrix; 79 | uniform mat3 normalMatrix; 80 | 81 | attribute vec3 position; 82 | attribute vec3 normal; 83 | attribute vec2 uv; 84 | attribute vec2 uv2; 85 | 86 | varying float noise; 87 | uniform float time; 88 | uniform float fireSpeed; 89 | uniform float pulseHeight; 90 | uniform float displacementHeight; 91 | uniform float turbulenceDetail; 92 | 93 | vec3 mod289(vec3 x) { 94 | return x - floor(x * (1.0 / 289.0)) * 289.0; 95 | } 96 | 97 | vec4 mod289(vec4 x) { 98 | return x - floor(x * (1.0 / 289.0)) * 289.0; 99 | } 100 | 101 | vec4 permute(vec4 x) { 102 | return mod289(((x*34.0)+1.0)*x); 103 | } 104 | 105 | vec4 taylorInvSqrt(vec4 r) { 106 | return 1.79284291400159 - 0.85373472095314 * r; 107 | } 108 | 109 | vec3 fade(vec3 t) { 110 | return t*t*t*(t*(t*6.0-15.0)+10.0); 111 | } 112 | 113 | // Klassisk Perlin noise 114 | float cnoise(vec3 P) { 115 | vec3 Pi0 = floor(P); // indexing 116 | vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 117 | Pi0 = mod289(Pi0); 118 | Pi1 = mod289(Pi1); 119 | vec3 Pf0 = fract(P); // Fractional part for interpolation 120 | vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 121 | vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 122 | vec4 iy = vec4(Pi0.yy, Pi1.yy); 123 | vec4 iz0 = Pi0.zzzz; 124 | vec4 iz1 = Pi1.zzzz; 125 | 126 | vec4 ixy = permute(permute(ix) + iy); 127 | vec4 ixy0 = permute(ixy + iz0); 128 | vec4 ixy1 = permute(ixy + iz1); 129 | 130 | vec4 gx0 = ixy0 * (1.0 / 7.0); 131 | vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; 132 | gx0 = fract(gx0); 133 | vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); 134 | vec4 sz0 = step(gz0, vec4(0.0)); 135 | gx0 -= sz0 * (step(0.0, gx0) - 0.5); 136 | gy0 -= sz0 * (step(0.0, gy0) - 0.5); 137 | 138 | vec4 gx1 = ixy1 * (1.0 / 7.0); 139 | vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; 140 | gx1 = fract(gx1); 141 | vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); 142 | vec4 sz1 = step(gz1, vec4(0.0)); 143 | gx1 -= sz1 * (step(0.0, gx1) - 0.5); 144 | gy1 -= sz1 * (step(0.0, gy1) - 0.5); 145 | 146 | vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); 147 | vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); 148 | vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); 149 | vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); 150 | vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); 151 | vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); 152 | vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); 153 | vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); 154 | 155 | vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); 156 | g000 *= norm0.x; 157 | g010 *= norm0.y; 158 | g100 *= norm0.z; 159 | g110 *= norm0.w; 160 | vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); 161 | g001 *= norm1.x; 162 | g011 *= norm1.y; 163 | g101 *= norm1.z; 164 | g111 *= norm1.w; 165 | 166 | float n000 = dot(g000, Pf0); 167 | float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); 168 | float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); 169 | float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); 170 | float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); 171 | float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); 172 | float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); 173 | float n111 = dot(g111, Pf1); 174 | 175 | vec3 fade_xyz = fade(Pf0); 176 | vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); 177 | vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); 178 | float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 179 | return 2.2 * n_xyz; 180 | } 181 | 182 | // Classic Perlin noise, periodic variant 183 | float pnoise(vec3 P, vec3 rep) 184 | { 185 | vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period 186 | vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period 187 | Pi0 = mod289(Pi0); 188 | Pi1 = mod289(Pi1); 189 | vec3 Pf0 = fract(P); // Fractional part for interpolation 190 | vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 191 | vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 192 | vec4 iy = vec4(Pi0.yy, Pi1.yy); 193 | vec4 iz0 = Pi0.zzzz; 194 | vec4 iz1 = Pi1.zzzz; 195 | 196 | vec4 ixy = permute(permute(ix) + iy); 197 | vec4 ixy0 = permute(ixy + iz0); 198 | vec4 ixy1 = permute(ixy + iz1); 199 | 200 | vec4 gx0 = ixy0 * (1.0 / 7.0); 201 | vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; 202 | gx0 = fract(gx0); 203 | vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); 204 | vec4 sz0 = step(gz0, vec4(0.0)); 205 | gx0 -= sz0 * (step(0.0, gx0) - 0.5); 206 | gy0 -= sz0 * (step(0.0, gy0) - 0.5); 207 | 208 | vec4 gx1 = ixy1 * (1.0 / 7.0); 209 | vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; 210 | gx1 = fract(gx1); 211 | vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); 212 | vec4 sz1 = step(gz1, vec4(0.0)); 213 | gx1 -= sz1 * (step(0.0, gx1) - 0.5); 214 | gy1 -= sz1 * (step(0.0, gy1) - 0.5); 215 | 216 | vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); 217 | vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); 218 | vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); 219 | vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); 220 | vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); 221 | vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); 222 | vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); 223 | vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); 224 | 225 | vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); 226 | g000 *= norm0.x; 227 | g010 *= norm0.y; 228 | g100 *= norm0.z; 229 | g110 *= norm0.w; 230 | vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); 231 | g001 *= norm1.x; 232 | g011 *= norm1.y; 233 | g101 *= norm1.z; 234 | g111 *= norm1.w; 235 | 236 | float n000 = dot(g000, Pf0); 237 | float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); 238 | float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); 239 | float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); 240 | float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); 241 | float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); 242 | float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); 243 | float n111 = dot(g111, Pf1); 244 | 245 | vec3 fade_xyz = fade(Pf0); 246 | vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); 247 | vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); 248 | float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 249 | return 2.2 * n_xyz; 250 | } 251 | 252 | 253 | // Ashima code 254 | float turbulence( vec3 p ) { 255 | float t = -.5; 256 | for (float f = 1.0 ; f <= 10.0 ; f++ ){ 257 | float power = pow( 2.0, f ); 258 | t += abs( pnoise( vec3( power * p ), vec3( 10.0, 10.0, 10.0 ) ) / power ); 259 | } 260 | return t; 261 | 262 | } 263 | 264 | void main() { 265 | 266 | /* 267 | noise = -0.9 * turbulence( .65 * normal + 0.1 * time); 268 | float b = .25 * pnoise( 0.05 * position + vec3(0.5 * time), vec3( 100.0 ) ); 269 | */ 270 | 271 | noise = -displacementHeight * turbulence( turbulenceDetail * normal + fireSpeed*time); 272 | float b = pulseHeight * pnoise( 0.05 * position + vec3(0.5 * time), vec3( 100.0 ) ); 273 | float displacement = -noise + b; 274 | 275 | vec3 newPosition = position + normal * displacement; 276 | gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 ); 277 | } 278 | 279 | `, 280 | 'vertex', 281 | 'three', 282 | nextStageNodeId 283 | ); 284 | 285 | export { fireFrag, fireVert }; 286 | -------------------------------------------------------------------------------- /src/shaders/fluidCirclesNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 3 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 4 | import { 5 | numberUniformData, 6 | colorUniformData, 7 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 8 | 9 | const fluidCirclesNode = (id: string, position: NodePosition) => 10 | sourceNode( 11 | id, 12 | 'Fluid Circles', 13 | position, 14 | { 15 | version: 2, 16 | preprocess: true, 17 | strategies: [uniformStrategy()], 18 | uniforms: [ 19 | numberUniformData('speed', '0.2'), 20 | numberUniformData('baseRadius', '0.286'), 21 | numberUniformData('colorVariation', '0.6'), 22 | numberUniformData('brightnessVariation', '0'), 23 | numberUniformData('variation', '8'), 24 | colorUniformData('backgroundColor', ['0.0', '0.0', '0.5']), 25 | ], 26 | }, 27 | ` 28 | // Original https://www.shadertoy.com/view/MljSzW - Imported with permission 29 | // Created by Stefan Draganov - vortex/2015 30 | // License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. 31 | 32 | precision highp float; 33 | precision highp int; 34 | 35 | uniform float time; 36 | uniform float speed; 37 | uniform float baseRadius; 38 | uniform float colorVariation; 39 | uniform float brightnessVariation; 40 | uniform vec3 backgroundColor; 41 | uniform float variation; 42 | 43 | varying vec2 vUv; 44 | 45 | vec3 n(vec2 x, float t) { 46 | vec3 v = floor(vec3(x, t)); 47 | vec3 u = vec3(mod(v.xy, variation), v.z); 48 | vec3 c = fract( u.xyz * ( 49 | vec3(0.16462, 0.84787, 0.98273) + 50 | u.xyz * vec3(0.24808, 0.75905, 0.13898) + 51 | u.yzx * vec3(0.31517, 0.62703, 0.26063) + 52 | u.zxy * vec3(0.47127, 0.58568, 0.37244) 53 | ) + u.yzx * ( 54 | vec3(0.35425, 0.65187, 0.12423) + 55 | u.yzx * vec3(0.95238, 0.93187, 0.95213) + 56 | u.zxy * vec3(0.31526, 0.62512, 0.71837) 57 | ) + u.zxy * ( 58 | vec3(0.95213, 0.13841, 0.16479) + 59 | u.zxy * vec3(0.47626, 0.69257, 0.19738) 60 | ) ); 61 | return v + c; 62 | } 63 | 64 | // Generate a predictably random color based on an input coord 65 | vec3 col(vec2 x, float t) { 66 | return vec3( 67 | 0.5 + max( brightnessVariation * cos( x.y * x.x ), 0.0 ) 68 | ) + clamp( 69 | colorVariation * cos(fract(vec3(x, t)) * 371.0241), 70 | vec3( -0.4 ), 71 | vec3( 1.0 ) 72 | ); 73 | } 74 | 75 | vec2 idx(vec2 x) { 76 | return floor(fract(x * 29.0) * 3.0) - vec2(1.0); 77 | } 78 | 79 | float circle(vec2 x, vec2 c, float r) { 80 | return max(0.0, 1.0 - dot(x - c, x - c) / (r * r)); 81 | } 82 | 83 | void main() { 84 | 85 | vec2 x = vUv * 10.0; 86 | float t = time * speed; 87 | vec4 c = vec4(vec3(0.0), 0.1); 88 | 89 | for (int N = 0; N < 3; N++) { 90 | for (int k = -1; k <= 0; k++) { 91 | for (int i = -1; i <= 1; i++) { 92 | for (int j = -1; j <= 1; j++) { 93 | vec2 X = x + vec2(j, i); 94 | float t = t + float(N) * 38.0; 95 | float T = t + float(k); 96 | vec3 a = n(X, T); 97 | vec2 o = idx(a.xy); 98 | vec3 b = n(X + o, T + 1.0); 99 | vec2 m = mix(a.xy, b.xy, (t - a.z) / (b.z - a.z)); 100 | float r = baseRadius * sin(3.1415927 * clamp((t - a.z) / (b.z - a.z), 0.0, 1.0)); 101 | if (length(a.xy - b.xy) / (b.z - a.z) > 2.0) { 102 | r = 0.0; 103 | } 104 | c += vec4(col(a.xy, a.z), 1.0) * circle(x, m, r); 105 | } 106 | } 107 | } 108 | } 109 | 110 | gl_FragColor = vec4(c.rgb / max(1e-5, c.w) + backgroundColor, 1.0); 111 | 112 | } 113 | 114 | `, 115 | 'fragment', 116 | 'three' 117 | ); 118 | 119 | export default fluidCirclesNode; 120 | -------------------------------------------------------------------------------- /src/shaders/heatmapShaderNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { numberUniformData } from '@shaderfrog/core/src/core/nodes/data-nodes'; 3 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 4 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 5 | 6 | export const variation0 = ` 7 | // Adapted from http://blogs.msdn.com/b/eternalcoding/archive/2014/04/17/learning-shaders-create-your-own-shaders-with-babylon-js.aspx 8 | 9 | precision highp float; 10 | 11 | varying vec3 vNormal; 12 | varying vec3 vPosition; 13 | 14 | uniform mat4 modelMatrix; 15 | uniform vec3 vLightPosition; 16 | uniform mat4 modelViewMatrix; 17 | uniform mat4 projectionMatrix; 18 | uniform mat4 viewMatrix; 19 | uniform mat3 normalMatrix; 20 | 21 | uniform float scale; // bigger scale = more outer surface is blue 22 | uniform float power; // power = heat the object emits 23 | 24 | vec3 toneToHeatColorMap(in float tone) { 25 | if(tone > 0.95) 26 | return vec3(0.0, 0.0, 1.0); 27 | else if(tone > 0.80) 28 | return vec3(0.0, 0.2+tone, 0.0); 29 | else if(tone > 0.25) 30 | return vec3(1.0, tone, 0.0); 31 | else if(tone > 0.1) 32 | return vec3(1.0-tone, 0.0, 0.0); 33 | 34 | return vec3(0.4, 0.05, 0.2); 35 | } 36 | 37 | void main(void) { 38 | // color 39 | vec3 fresnel = vec3(1.0, 1.0, 1.0); 40 | 41 | vec3 pos2World = (modelViewMatrix * vec4(vPosition, 1.0)).xyz; 42 | vec3 norm2World = normalize(modelViewMatrix * vec4(vNormal, 1.0)).xyz; 43 | vec3 cameraPos2World = (modelViewMatrix * vec4(viewMatrix[0][3], viewMatrix[1][3], viewMatrix[2][3], 1.0)).xyz; 44 | 45 | // Light 46 | vec3 lightVectorW = normalize(vec3(vec4( vLightPosition, 1.0) * modelMatrix) - vPosition); 47 | 48 | // diffuse 49 | float ndl = max(0.0, dot(vNormal, lightVectorW)); 50 | 51 | vec3 I = normalize(pos2World - cameraPos2World); 52 | float R = scale * pow(1.0 + dot(I, norm2World), power); 53 | 54 | vec3 color = vec3(0); 55 | 56 | color = clamp(mix(color, fresnel, R), 0.0, 1.0); 57 | 58 | 59 | gl_FragColor = vec4( toneToHeatColorMap(color.r), 1.0 ); 60 | } 61 | `; 62 | 63 | export const variation1 = ` 64 | // Adapted from http://blogs.msdn.com/b/eternalcoding/archive/2014/04/17/learning-shaders-create-your-own-shaders-with-babylon-js.aspx 65 | 66 | precision highp float; 67 | 68 | varying vec3 vNormal; 69 | varying vec3 vPosition; 70 | 71 | uniform mat4 modelMatrix; 72 | uniform vec3 vLightPosition; 73 | uniform mat4 modelViewMatrix; 74 | uniform mat4 projectionMatrix; 75 | uniform mat4 viewMatrix; 76 | uniform mat3 normalMatrix; 77 | 78 | uniform float scale; // bigger scale = more outer surface is blue 79 | uniform float power; // power = heat the object emits 80 | 81 | vec3 toneToHeatColorMap(in float tone) { 82 | if(tone > 0.95) 83 | return vec3(0.0, 0.0, 1.0); 84 | else if(tone > 0.80) 85 | return vec3(0.0, 0.2+tone, 0.0); 86 | else if(tone > 0.25) 87 | return vec3(1.0, tone, 0.0); 88 | else if(tone > 0.1) 89 | return vec3(1.0-tone, 0.0, 0.0); 90 | 91 | return vec3(0.4, 0.05, 0.2); 92 | } 93 | 94 | void main(void) { 95 | // color 96 | vec3 fresnel = vec3(1.0, 1.0, 1.0); 97 | 98 | vec3 pos2World = (modelViewMatrix * vec4(vPosition, 1.0)).xyz; 99 | vec3 norm2World = normalize(modelViewMatrix * vec4(vNormal, 1.0)).xyz; 100 | vec3 cameraPos2World = (modelViewMatrix * vec4(viewMatrix[0][3], viewMatrix[1][3], viewMatrix[2][3], 1.0)).xyz; 101 | 102 | // Light 103 | vec3 lightVectorW = normalize(vec3(vec4( vLightPosition, 1.0) * modelMatrix) - vPosition); 104 | 105 | // diffuse 106 | float ndl = max(0.0, dot(vNormal, lightVectorW)); 107 | 108 | vec3 I = normalize(pos2World - cameraPos2World); 109 | float R = scale * pow(1.0 + dot(I, norm2World), power); 110 | 111 | vec3 color = vec3(0); 112 | 113 | color = clamp(mix(color, fresnel, R), 0.0, 1.0); 114 | 115 | 116 | gl_FragColor = vec4( vec3(0.5, 0.5, 1.0) + 0.3 * toneToHeatColorMap(color.r), 1.0 ); 117 | } 118 | `; 119 | 120 | const heatShaderFragmentNode = ( 121 | id: string, 122 | position: NodePosition, 123 | source = variation0 124 | ) => 125 | sourceNode( 126 | id, 127 | 'Fake Heatmap', 128 | position, 129 | { 130 | version: 2, 131 | preprocess: true, 132 | strategies: [uniformStrategy()], 133 | uniforms: [ 134 | numberUniformData('scale', '1.2'), 135 | numberUniformData('power', '1'), 136 | ], 137 | }, 138 | source, 139 | 'fragment', 140 | 'three' 141 | ); 142 | 143 | const heatShaderVertexNode = ( 144 | id: string, 145 | nextStageNodeId: string, 146 | position: NodePosition 147 | ) => 148 | sourceNode( 149 | id, 150 | 'Fake Heatmap', 151 | position, 152 | { version: 2, preprocess: true, strategies: [], uniforms: [] }, 153 | ` 154 | precision highp float; 155 | precision highp int; 156 | 157 | uniform mat4 modelMatrix; 158 | uniform mat4 modelViewMatrix; 159 | uniform mat4 projectionMatrix; 160 | uniform mat4 viewMatrix; 161 | uniform mat3 normalMatrix; 162 | attribute vec3 position; 163 | attribute vec3 normal; 164 | attribute vec2 uv; 165 | attribute vec2 uv2; 166 | varying vec3 vNormal; 167 | varying vec3 vPosition; 168 | 169 | void main() 170 | { 171 | vNormal = normal; 172 | vPosition = position; 173 | 174 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 175 | } 176 | `, 177 | 'vertex', 178 | 'three', 179 | nextStageNodeId 180 | ); 181 | 182 | export { heatShaderFragmentNode, heatShaderVertexNode }; 183 | -------------------------------------------------------------------------------- /src/shaders/juliaNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | numberUniformData, 4 | UniformDataType, 5 | vectorUniformData, 6 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 7 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 8 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 9 | 10 | const juliaF = (id: string, position: NodePosition) => 11 | sourceNode( 12 | id, 13 | 'Julia', 14 | position, 15 | { 16 | version: 2, 17 | preprocess: true, 18 | strategies: [uniformStrategy()], 19 | uniforms: [ 20 | vectorUniformData('start', ['-0.2307', '0.69230']), 21 | numberUniformData('iter', '60'), 22 | vectorUniformData('color', ['0.9', '0.6', '0.43']), 23 | ], 24 | }, 25 | `precision highp float; 26 | precision highp int; 27 | 28 | uniform vec2 start; 29 | uniform int iter; 30 | uniform vec3 color; 31 | uniform float time; 32 | varying vec2 vUv; 33 | 34 | void main() { 35 | vec2 z; 36 | z.x = 3.0 * (vUv.x - .5); 37 | z.y = 3.0 * (vUv.y - .5); 38 | 39 | int y = 0; 40 | for (int i = 0; i < 100; i++) { 41 | y++; 42 | float x = (z.x * z.x - z.y * z.y) + start.x; 43 | float y = (z.x * z.y + z.x * z.y) + start.y; 44 | 45 | if ((x * x + y * y) > 10.0) { 46 | break; 47 | } 48 | z.x = x; 49 | z.y = y; 50 | } 51 | 52 | float val = float(y) / float(iter); 53 | gl_FragColor = vec4(color * val, 1.0) + 0.1; 54 | } 55 | `, 56 | 'fragment', 57 | 'three' 58 | ); 59 | 60 | const juliaV = (id: string, nextStageNodeId: string, position: NodePosition) => 61 | sourceNode( 62 | id, 63 | 'Julia', 64 | position, 65 | { 66 | version: 2, 67 | preprocess: true, 68 | strategies: [uniformStrategy()], 69 | uniforms: [], 70 | }, 71 | `precision highp float; 72 | precision highp int; 73 | 74 | uniform mat4 modelViewMatrix; 75 | uniform mat4 projectionMatrix; 76 | varying vec2 vUv; 77 | 78 | attribute vec3 position; 79 | attribute vec2 uv; 80 | 81 | void main() { 82 | vUv = uv; 83 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 84 | }`, 85 | 'vertex', 86 | 'three', 87 | nextStageNodeId 88 | ); 89 | 90 | export { juliaF, juliaV }; 91 | -------------------------------------------------------------------------------- /src/shaders/normalmapifyNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | numberUniformData, 4 | UniformDataType, 5 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 6 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 7 | import { 8 | texture2DStrategy, 9 | uniformStrategy, 10 | } from '@shaderfrog/core/src/core/strategy'; 11 | 12 | const normalMapify = (id: string, position: NodePosition) => 13 | sourceNode( 14 | id, 15 | 'Normal Map-ify', 16 | position, 17 | { 18 | version: 2, 19 | preprocess: true, 20 | strategies: [uniformStrategy(), texture2DStrategy()], 21 | uniforms: [numberUniformData('normal_strength', '1.0')], 22 | }, 23 | ` 24 | uniform sampler2D normal_map; 25 | uniform float normal_strength; 26 | varying vec2 vUv; 27 | 28 | void main() { 29 | gl_FragColor = vec4(vec3(0.5, 0.5, 1.0) + normal_strength * texture2D(normal_map, vUv).rgb, 1.0); 30 | } 31 | `, 32 | 'fragment', 33 | 'three' 34 | ); 35 | 36 | export default normalMapify; 37 | -------------------------------------------------------------------------------- /src/shaders/outlineShader.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | colorUniformData, 4 | numberUniformData, 5 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 6 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 7 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 8 | 9 | const outlineShaderF = (id: string, position: NodePosition) => 10 | sourceNode( 11 | id, 12 | 'Outline', 13 | position, 14 | { 15 | version: 2, 16 | preprocess: true, 17 | strategies: [uniformStrategy()], 18 | uniforms: [ 19 | colorUniformData('color', ['1', '1', '1']), 20 | numberUniformData('start', '0'), 21 | numberUniformData('end', '1'), 22 | numberUniformData('alpha', '1'), 23 | ], 24 | }, 25 | ` 26 | precision highp float; 27 | 28 | uniform vec3 color; 29 | uniform float start; 30 | uniform float end; 31 | uniform float alpha; 32 | 33 | varying vec3 fPosition; 34 | varying vec3 fNormal; 35 | 36 | void main() 37 | { 38 | vec3 normal = normalize(fNormal); 39 | vec3 eye = normalize(-fPosition.xyz); 40 | float rim = smoothstep(start, end, 1.0 - dot(normal, eye)); 41 | gl_FragColor = vec4( clamp(rim, 0.0, 1.0) * alpha * color, 1.0 ); 42 | } 43 | `, 44 | 'fragment' 45 | ); 46 | 47 | const outlineShaderV = ( 48 | id: string, 49 | nextStageNodeId: string, 50 | position: NodePosition 51 | ) => 52 | sourceNode( 53 | id, 54 | 'Outline', 55 | position, 56 | { version: 2, preprocess: true, strategies: [], uniforms: [] }, 57 | ` 58 | precision highp float; 59 | 60 | attribute vec3 position; 61 | attribute vec3 normal; 62 | attribute vec2 uv; 63 | attribute vec2 uv2; 64 | 65 | uniform mat3 normalMatrix; 66 | uniform mat4 modelViewMatrix; 67 | uniform mat4 projectionMatrix; 68 | 69 | varying vec3 fNormal; 70 | varying vec3 fPosition; 71 | varying vec2 fUv; 72 | 73 | void main() 74 | { 75 | fNormal = normalize(normalMatrix * normal); 76 | vec4 pos = modelViewMatrix * vec4(position, 1.0); 77 | fPosition = pos.xyz; 78 | fUv = uv; 79 | gl_Position = projectionMatrix * pos; 80 | } 81 | `, 82 | 'vertex', 83 | 'three', 84 | nextStageNodeId 85 | ); 86 | 87 | export { outlineShaderF, outlineShaderV }; 88 | -------------------------------------------------------------------------------- /src/shaders/perlinClouds.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | colorUniformData, 4 | numberUniformData, 5 | textureUniformData, 6 | UniformDataType, 7 | vectorUniformData, 8 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 9 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 10 | import { 11 | texture2DStrategy, 12 | uniformStrategy, 13 | } from '@shaderfrog/core/src/core/strategy'; 14 | 15 | const perlinCloudsF = ( 16 | id: string, 17 | position: NodePosition, 18 | uniforms?: UniformDataType[] 19 | ) => 20 | sourceNode( 21 | id, 22 | 'Perlin Clouds', 23 | position, 24 | { 25 | version: 2, 26 | preprocess: true, 27 | strategies: [texture2DStrategy(), uniformStrategy()], 28 | uniforms: uniforms || [ 29 | colorUniformData('color', ['1', '1', '1']), 30 | numberUniformData('scale', '0.05'), 31 | textureUniformData('noiseImage', 'grayscale-noise'), 32 | vectorUniformData('speed', ['-0.002', '-0.002']), 33 | numberUniformData('cloudBrightness', '0.2'), 34 | numberUniformData('cloudMorphSpeed', '0.2'), 35 | numberUniformData('cloudMorphDirection', '1'), 36 | numberUniformData('cloudCover', '0.6'), 37 | ], 38 | }, 39 | `precision highp float; 40 | precision highp int; 41 | 42 | uniform float scale; 43 | uniform sampler2D noiseImage; 44 | uniform vec2 speed; 45 | uniform float cloudBrightness; 46 | uniform float cloudMorphSpeed; 47 | uniform float cloudMorphDirection; 48 | uniform float cloudCover; 49 | 50 | uniform float time; 51 | uniform vec3 color; 52 | varying vec2 vUv; 53 | 54 | void main() { 55 | 56 | vec4 colorOutput = vec4( 0.0 ); 57 | vec2 elapsed = time * speed; 58 | vec2 uv = ( vUv + elapsed ) * scale; 59 | 60 | for( int i = 1; i <= 4; i++ ) { 61 | float f = float( i ); 62 | 63 | float divis = pow( 2.0, f ); 64 | float uvPow = pow( 2.0, f - 1.0 ); 65 | 66 | vec4 computed = texture2D( 67 | noiseImage, uvPow * ( uv + vec2( 0.1, 0.0 ) + ( time * 0.001 * cloudMorphSpeed ) ) 68 | ) / divis; 69 | computed += texture2D( 70 | noiseImage, uvPow * ( uv + vec2( 0.1 ) ) 71 | ) / divis; 72 | computed += texture2D( 73 | noiseImage, uvPow * ( uv + vec2( 0.0, 0.1 ) + ( cloudMorphDirection * time * 0.001 * cloudMorphSpeed ) ) 74 | ) / divis; 75 | 76 | computed *= 0.25; 77 | 78 | colorOutput += computed; 79 | } 80 | 81 | colorOutput = max( colorOutput - ( 1.0 - cloudCover ), 0.0 ); 82 | colorOutput = vec4( 1.0 - pow( ( 1.0 - cloudBrightness ), colorOutput.r * 255.0 ) ); 83 | 84 | gl_FragColor = vec4( vec3(0.4, 0.6, 1.0) + color * colorOutput.rgb, 1.0 ); 85 | 86 | } 87 | `, 88 | 'fragment' 89 | ); 90 | 91 | export default perlinCloudsF; 92 | -------------------------------------------------------------------------------- /src/shaders/purpleNoiseNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | numberUniformData, 4 | UniformDataType, 5 | vectorUniformData, 6 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 7 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 8 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 9 | 10 | const purpleNoiseNode = ( 11 | id: string, 12 | position: NodePosition, 13 | uniforms?: UniformDataType[] 14 | ) => 15 | sourceNode( 16 | id, 17 | 'Purple Metal', 18 | position, 19 | { 20 | version: 2, 21 | preprocess: true, 22 | strategies: [uniformStrategy()], 23 | uniforms: uniforms || [ 24 | numberUniformData('speed', '3.0'), 25 | numberUniformData('brightnessX', '1.0'), 26 | numberUniformData('permutations', '10'), 27 | numberUniformData('iterations', '1'), 28 | vectorUniformData('uvScale', ['1', '1']), 29 | vectorUniformData('color1', ['0.7', '0.3', '0.8']), 30 | vectorUniformData('color2', ['0.1', '0.2', '0.9']), 31 | vectorUniformData('color3', ['0.8', '0.3', '0.8']), 32 | ], 33 | }, 34 | ` 35 | precision highp float; 36 | precision highp int; 37 | 38 | // (sqrt(5) - 1)/4 = F4, used once below 39 | #define F4 0.309016994374947451 40 | #define PI 3.14159 41 | 42 | uniform float time; 43 | uniform float permutations; 44 | uniform float iterations; 45 | uniform vec2 uvScale; 46 | uniform vec3 color1; 47 | uniform vec3 color2; 48 | uniform vec3 color3; 49 | uniform float brightnessX; 50 | uniform float speed; 51 | 52 | varying vec2 vUv; 53 | 54 | // Description : Array and textureless GLSL 2D/3D/4D simplex 55 | // noise functions. 56 | // Author : Ian McEwan, Ashima Arts. 57 | // Maintainer : ijm 58 | // Lastmod : 20110822 (ijm) 59 | // License : Copyright (C) 2011 Ashima Arts. All rights reserved. 60 | // Distributed under the MIT License. See LICENSE file. 61 | // https://github.com/ashima/webgl-noise 62 | // 63 | 64 | vec4 mod289(vec4 x) { 65 | return x - floor(x * (1.0 / 289.0)) * 289.0; 66 | } 67 | 68 | float mod289(float x) { 69 | return x - floor(x * (1.0 / 289.0)) * 289.0; 70 | } 71 | 72 | vec4 permute(vec4 x) { 73 | return mod289(((x*34.0)+1.0)*x); 74 | } 75 | 76 | float permute(float x) { 77 | return mod289(((x*34.0)+1.0)*x); 78 | } 79 | 80 | vec4 taylorInvSqrt(vec4 r) { 81 | return 1.79284291400159 - 0.85373472095314 * r; 82 | } 83 | 84 | float taylorInvSqrt(float r) { 85 | return 1.79284291400159 - 0.85373472095314 * r; 86 | } 87 | 88 | vec4 grad4(float j, vec4 ip) { 89 | const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0); 90 | vec4 p,s; 91 | 92 | p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0; 93 | p.w = 1.5 - dot(abs(p.xyz), ones.xyz); 94 | s = vec4(lessThan(p, vec4(0.0))); 95 | p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www; 96 | 97 | return p; 98 | } 99 | 100 | float snoise(vec4 v) { 101 | const vec4 C = vec4( 0.138196601125011, // (5 - sqrt(5))/20 G4 102 | 0.276393202250021, // 2 * G4 103 | 0.414589803375032, // 3 * G4 104 | -0.447213595499958); // -1 + 4 * G4 105 | 106 | // First corner 107 | vec4 i = floor(v + dot(v, vec4(F4)) ); 108 | vec4 x0 = v - i + dot(i, C.xxxx); 109 | 110 | // Other corners 111 | 112 | // Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI) 113 | vec4 i0; 114 | vec3 isX = step( x0.yzw, x0.xxx ); 115 | vec3 isYZ = step( x0.zww, x0.yyz ); 116 | // i0.x = dot( isX, vec3( 1.0 ) ); 117 | i0.x = isX.x + isX.y + isX.z; 118 | i0.yzw = 1.0 - isX; 119 | // i0.y += dot( isYZ.xy, vec2( 1.0 ) ); 120 | i0.y += isYZ.x + isYZ.y; 121 | i0.zw += 1.0 - isYZ.xy; 122 | i0.z += isYZ.z; 123 | i0.w += 1.0 - isYZ.z; 124 | 125 | // i0 now contains the unique values 0,1,2,3 in each channel 126 | vec4 i3 = clamp( i0, 0.0, 1.0 ); 127 | vec4 i2 = clamp( i0-1.0, 0.0, 1.0 ); 128 | vec4 i1 = clamp( i0-2.0, 0.0, 1.0 ); 129 | 130 | // x0 = x0 - 0.0 + 0.0 * C.xxxx 131 | // x1 = x0 - i1 + 1.0 * C.xxxx 132 | // x2 = x0 - i2 + 2.0 * C.xxxx 133 | // x3 = x0 - i3 + 3.0 * C.xxxx 134 | // x4 = x0 - 1.0 + 4.0 * C.xxxx 135 | vec4 x1 = x0 - i1 + C.xxxx; 136 | vec4 x2 = x0 - i2 + C.yyyy; 137 | vec4 x3 = x0 - i3 + C.zzzz; 138 | vec4 x4 = x0 + C.wwww; 139 | 140 | // Permutations 141 | i = mod289(i); 142 | float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x); 143 | vec4 j1 = permute( permute( permute( permute ( 144 | i.w + vec4(i1.w, i2.w, i3.w, 1.0 )) 145 | + i.z + vec4(i1.z, i2.z, i3.z, 1.0 )) 146 | + i.y + vec4(i1.y, i2.y, i3.y, 1.0 )) 147 | + i.x + vec4(i1.x, i2.x, i3.x, 1.0 )); 148 | 149 | // Gradients: 7x7x6 points over a cube, mapped onto a 4-cross polytope 150 | // 7*7*6 = 294, which is close to the ring size 17*17 = 289. 151 | vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ; 152 | 153 | vec4 p0 = grad4(j0, ip); 154 | vec4 p1 = grad4(j1.x, ip); 155 | vec4 p2 = grad4(j1.y, ip); 156 | vec4 p3 = grad4(j1.z, ip); 157 | vec4 p4 = grad4(j1.w, ip); 158 | 159 | // Normalise gradients 160 | vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); 161 | p0 *= norm.x; 162 | p1 *= norm.y; 163 | p2 *= norm.z; 164 | p3 *= norm.w; 165 | p4 *= taylorInvSqrt(dot(p4,p4)); 166 | 167 | // Mix contributions from the five corners 168 | vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0); 169 | vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4) ), 0.0); 170 | m0 = m0 * m0; 171 | m1 = m1 * m1; 172 | return ( 173 | 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 ))) 174 | + dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) 175 | ); 176 | 177 | } 178 | 179 | // makes a pseudorandom number between 0 and 1 180 | float hash(float n) { 181 | return fract(sin(n)*93942.234); 182 | } 183 | 184 | // rotation matrix 185 | mat2 m = mat2(0.6,0.8,-0.8,0.6); 186 | 187 | // fractional brownian motion (i.e. photoshop clouds) 188 | float fbm(vec4 p) { 189 | float f = 0.0; 190 | f += 0.5 * snoise(vec4( p.xy * m, p.zw * m )); 191 | p *= 2.02; 192 | f += 0.25 * snoise(vec4( p.xy * m, p.zw * m )); 193 | p *= 2.01; 194 | f += 0.125 * snoise(vec4( p.xy * m, p.zw * m )); 195 | p *= 2.03; 196 | f += 0.0625 * snoise(vec4( p.xy * m, p.zw * m )); 197 | f /= 0.9375; 198 | return f; 199 | } 200 | 201 | void main() { 202 | // relative coordinates 203 | vec2 p = vUv * uvScale; 204 | float elapsed = time * speed * 0.01; 205 | 206 | float s = vUv.x * uvScale.x; 207 | float t = vUv.y * uvScale.y; 208 | 209 | // Tiling 4d noise based on 210 | // https://gamedev.stackexchange.com/questions/23625/how-do-you-generate-tileable-perlin-noise/23639#23639 211 | float multiplier = iterations / ( 2.0 * PI ); 212 | float nx = cos( s * 2.0 * PI ) * multiplier; 213 | float ny = cos( t * 2.0 * PI ) * multiplier; 214 | float nz = sin( s * 2.0 * PI ) * multiplier; 215 | float nw = sin( t * 2.0 * PI ) * multiplier; 216 | 217 | vec4 tile4d = vec4( nx, ny, nz, nw ); 218 | 219 | vec2 a = vec2( 220 | fbm( tile4d + elapsed * 1.1 ), 221 | fbm( tile4d - elapsed * 1.3 ) 222 | ); 223 | 224 | vec2 b = vec2( 225 | fbm( tile4d + elapsed * 1.2 + a.x * 2.0 ), 226 | fbm( tile4d - elapsed * 1.2 + a.x * 3.0 ) 227 | ); 228 | 229 | float surf = fbm( tile4d + elapsed + length( b ) * permutations ); 230 | 231 | // mix in some color 232 | vec3 colorOutput = 1.0 * brightnessX * ( 233 | ( ( b.x + surf ) * color1 ) + 234 | ( ( b.y + surf ) * color2 ) + 235 | ( ( surf + b.x ) * color3 ) 236 | ); 237 | 238 | gl_FragColor = vec4( clamp(colorOutput, vec3(0.0), vec3(0.9)), 1.0 ); 239 | } 240 | `, 241 | 'fragment', 242 | 'three' 243 | ); 244 | 245 | export default purpleNoiseNode; 246 | -------------------------------------------------------------------------------- /src/shaders/serpentNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { 3 | numberUniformData, 4 | textureUniformData, 5 | UniformDataType, 6 | } from '@shaderfrog/core/src/core/nodes/data-nodes'; 7 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 8 | import { 9 | texture2DStrategy, 10 | uniformStrategy, 11 | } from '@shaderfrog/core/src/core/strategy'; 12 | 13 | const serpentF = (id: string, position: NodePosition) => 14 | sourceNode( 15 | id, 16 | 'Serpent', 17 | position, 18 | { 19 | version: 2, 20 | preprocess: true, 21 | strategies: [uniformStrategy(), texture2DStrategy()], 22 | uniforms: [ 23 | numberUniformData('lineWidth', '0.5'), 24 | numberUniformData('tiling', '4.9'), 25 | numberUniformData('rotationSpeed', '0.23'), 26 | textureUniformData('image1', 'brick'), 27 | textureUniformData('image2', 'pebbles'), 28 | ], 29 | }, 30 | `precision highp float; 31 | precision highp int; 32 | 33 | // Default THREE.js uniforms available to both fragment and vertex shader 34 | uniform mat4 modelMatrix; 35 | uniform mat4 modelViewMatrix; 36 | uniform mat4 projectionMatrix; 37 | uniform mat4 viewMatrix; 38 | uniform mat3 normalMatrix; 39 | 40 | // Default uniforms provided by ShaderFrog. 41 | uniform vec3 cameraPosition; 42 | uniform float time; 43 | 44 | uniform sampler2D image1; 45 | uniform sampler2D image2; 46 | uniform float lineWidth; 47 | 48 | // Example varyings passed from the vertex shader 49 | varying vec3 vPosition; 50 | varying vec3 vNormal; 51 | varying vec2 vUv; 52 | varying vec2 vUv2; 53 | 54 | uniform float rotationSpeed; 55 | uniform float tiling; 56 | 57 | vec2 rotateUV(vec2 uv, float rotation) { 58 | float mid = 0.5; 59 | return vec2( 60 | cos(rotation) * (uv.x - mid) + sin(rotation) * (uv.y - mid) + mid, 61 | cos(rotation) * (uv.y - mid) - sin(rotation) * (uv.x - mid) + mid 62 | ); 63 | } 64 | 65 | float reverse(float val) { 66 | return 1.0 + -1.0 * val; 67 | } 68 | 69 | void main() { 70 | float tile = tiling + 2.0 * sin(time * 0.5); 71 | vec2 posTurn = tile*(rotateUV(vUv, time * rotationSpeed * 2.0)); 72 | vec2 negTurn = tile*(rotateUV(vUv, time * -rotationSpeed * 2.0)); 73 | 74 | float x = fract(fract(posTurn.x * 2.0) + fract(posTurn.y * 2.0)); 75 | float shadow = clamp(x * reverse(x) * 3.0, 0.0, 1.0); 76 | float pos = fract(fract(posTurn.x) + fract(posTurn.y)); 77 | float val = step(pos, lineWidth); 78 | 79 | vec3 col; 80 | if(val > 0.0) { 81 | col = texture2D(image1, posTurn - 0.4 * time).rgb * (shadow + 0.4); 82 | } else { 83 | col = texture2D(image2, negTurn - 0.4 * time).rgb + shadow * 0.2 * shadow; 84 | } 85 | 86 | gl_FragColor = vec4( col, 1.0 ); 87 | } 88 | `, 89 | 'fragment', 90 | 'three' 91 | ); 92 | 93 | const serpentV = ( 94 | id: string, 95 | nextStageNodeId: string, 96 | position: NodePosition 97 | ) => 98 | sourceNode( 99 | id, 100 | 'Serpent', 101 | position, 102 | { 103 | version: 2, 104 | preprocess: true, 105 | strategies: [uniformStrategy()], 106 | uniforms: [], 107 | }, 108 | ` 109 | precision highp float; 110 | precision highp int; 111 | 112 | // Default THREE.js uniforms available to both fragment and vertex shader 113 | uniform mat4 modelMatrix; 114 | uniform mat4 modelViewMatrix; 115 | uniform mat4 projectionMatrix; 116 | uniform mat4 viewMatrix; 117 | uniform mat3 normalMatrix; 118 | 119 | // Default uniforms provided by ShaderFrog. 120 | uniform vec3 cameraPosition; 121 | uniform float time; 122 | 123 | // Default attributes provided by THREE.js. Attributes are only available in the 124 | // vertex shader. You can pass them to the fragment shader using varyings 125 | attribute vec3 position; 126 | attribute vec3 normal; 127 | attribute vec2 uv; 128 | attribute vec2 uv2; 129 | 130 | // Examples of variables passed from vertex to fragment shader 131 | varying vec3 vPosition; 132 | varying vec3 vNormal; 133 | varying vec2 vUv; 134 | varying vec2 vUv2; 135 | 136 | void main() { 137 | 138 | // To pass variables to the fragment shader, you assign them here in the 139 | // main function. Traditionally you name the varying with vAttributeName 140 | vNormal = normal; 141 | vUv = uv; 142 | vUv2 = uv2; 143 | vPosition = position; 144 | 145 | // This sets the position of the vertex in 3d space. The correct math is 146 | // provided below to take into account camera and object data. 147 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 148 | 149 | } 150 | `, 151 | 'vertex', 152 | 'three', 153 | nextStageNodeId 154 | ); 155 | 156 | export { serpentF, serpentV }; 157 | -------------------------------------------------------------------------------- /src/shaders/sinCosVertWarp.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { numberUniformData } from '@shaderfrog/core/src/core/nodes/data-nodes'; 3 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 4 | import { 5 | namedAttributeStrategy, 6 | texture2DStrategy, 7 | uniformStrategy, 8 | } from '@shaderfrog/core/src/core/strategy'; 9 | 10 | const sinCosVertWarp = (id: string, position: NodePosition) => 11 | sourceNode( 12 | id, 13 | 'Simple Vertex Warp', 14 | position, 15 | { 16 | version: 2, 17 | preprocess: true, 18 | strategies: [uniformStrategy(), namedAttributeStrategy('position')], 19 | uniforms: [ 20 | numberUniformData('height', '1'), 21 | numberUniformData('frequency', '10.0', [0, 100]), 22 | ], 23 | }, 24 | ` 25 | precision highp float; 26 | precision highp int; 27 | 28 | uniform mat4 modelMatrix; 29 | uniform mat4 modelViewMatrix; 30 | uniform mat4 projectionMatrix; 31 | uniform mat4 viewMatrix; 32 | uniform mat3 normalMatrix; 33 | 34 | uniform float height; 35 | uniform float frequency; 36 | 37 | attribute vec3 position; 38 | attribute vec3 normal; 39 | attribute vec2 uv; 40 | attribute vec2 uv2; 41 | 42 | varying vec2 vUv; 43 | varying vec3 vPosition; 44 | 45 | uniform float time; 46 | 47 | vec3 warp(vec3 position) { 48 | return vec3( 49 | 0.0, 50 | height * 0.1 * cos(position.y * frequency * 2.0 + time * 4.5), 51 | height * 0.1 * sin(position.z * frequency + time * 3.5) 52 | ) * normal; 53 | } 54 | 55 | // http://lolengine.net/blog/2013/09/21/picking-orthogonal-vector-combing-coconuts 56 | vec3 orthogonal(vec3 v) { 57 | return normalize(abs(v.x) > abs(v.z) ? vec3(-v.y, v.x, 0.0) 58 | : vec3(0.0, -v.z, v.y)); 59 | } 60 | 61 | void main() { 62 | vUv = uv; 63 | vPosition = position; 64 | 65 | vec3 displacedPosition = position + warp(position); 66 | 67 | float offset = 0.5; 68 | vec3 tangent = orthogonal(normal); 69 | vec3 bitangent = normalize(cross(normal, tangent)); 70 | vec3 neighbour1 = position + tangent * offset; 71 | vec3 neighbour2 = position + bitangent * offset; 72 | vec3 displacedNeighbour1 = neighbour1 + normal * warp(neighbour1); 73 | vec3 displacedNeighbour2 = neighbour2 + normal * warp(neighbour2); 74 | 75 | // https://i.ya-webdesign.com/images/vector-normals-tangent-16.png 76 | vec3 displacedTangent = displacedNeighbour1 - displacedPosition; 77 | vec3 displacedBitangent = displacedNeighbour2 - displacedPosition; 78 | 79 | // https://upload.wikimedia.org/wikipedia/commons/d/d2/Right_hand_rule_cross_product.svg 80 | vec3 displacedNormal = normalMatrix * normalize(cross(displacedTangent, displacedBitangent)); 81 | vNormal = displacedNormal; 82 | 83 | gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0); 84 | } 85 | 86 | `, 87 | 'vertex', 88 | 'three' 89 | ); 90 | 91 | export default sinCosVertWarp; 92 | -------------------------------------------------------------------------------- /src/shaders/solidColorNode.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | texture2DStrategy, 3 | uniformStrategy, 4 | } from '@shaderfrog/core/src/core/strategy'; 5 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 6 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 7 | 8 | const solidColorNode = (id: string, position: NodePosition) => 9 | sourceNode( 10 | id, 11 | 'Solid Color', 12 | position, 13 | { 14 | version: 2, 15 | preprocess: true, 16 | strategies: [uniformStrategy(), texture2DStrategy()], 17 | uniforms: [], 18 | }, 19 | `precision highp float; 20 | precision highp int; 21 | 22 | uniform float blorf; 23 | 24 | void main() { 25 | gl_FragColor = vec4( 26 | vec3(blorf), 27 | 1.0 28 | ); 29 | } 30 | 31 | `, 32 | 'fragment', 33 | 'three' 34 | ); 35 | 36 | export default solidColorNode; 37 | -------------------------------------------------------------------------------- /src/shaders/starterNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 3 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 4 | 5 | const starterVertex = (id: string, position: NodePosition) => 6 | sourceNode( 7 | id, 8 | 'Vertex', 9 | position, 10 | { 11 | version: 2, 12 | preprocess: true, 13 | strategies: [uniformStrategy()], 14 | uniforms: [], 15 | }, 16 | `precision highp float; 17 | precision highp int; 18 | 19 | uniform mat4 modelMatrix; 20 | uniform mat4 modelViewMatrix; 21 | uniform mat4 projectionMatrix; 22 | uniform mat4 viewMatrix; 23 | uniform mat3 normalMatrix; 24 | 25 | attribute vec3 position; 26 | attribute vec3 normal; 27 | attribute vec2 uv; 28 | attribute vec2 uv2; 29 | 30 | varying vec3 vPosition; 31 | varying vec3 vNormal; 32 | varying vec2 vUv; 33 | varying vec2 vUv2; 34 | 35 | void main() { 36 | vNormal = normal; 37 | vUv = uv; 38 | vUv2 = uv2; 39 | vPosition = position; 40 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 41 | } 42 | `, 43 | 'vertex', 44 | 'three' 45 | ); 46 | 47 | export default starterVertex; 48 | -------------------------------------------------------------------------------- /src/shaders/staticShaderNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 3 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 4 | 5 | export const variation0 = ` 6 | precision highp float; 7 | precision highp int; 8 | uniform float time; 9 | 10 | varying vec3 vPosition; 11 | varying vec3 vNormal; 12 | varying vec2 vUv; 13 | varying vec2 vUv2; 14 | 15 | void main() { 16 | gl_FragColor = vec4( clamp(tan(vNormal*time*time), vec3(0.0), vec3(1.0)), 1.0 ); 17 | } 18 | `; 19 | 20 | export const variation1 = ` 21 | precision highp float; 22 | precision highp int; 23 | uniform float time; 24 | 25 | varying vec3 vPosition; 26 | varying vec3 vNormal; 27 | varying vec2 vUv; 28 | varying vec2 vUv2; 29 | 30 | const float PI = 3.14159; 31 | 32 | void main() { 33 | vec2 scaledUv = sin(vUv * PI); 34 | gl_FragColor = vec4( 35 | clamp( 36 | 1.0 + vNormal * vec3(scaledUv, -scaledUv.y) * (1.0 + vec3(viewMatrix[0])), 37 | vec3(0.0), 38 | vec3(1.0) 39 | ), 40 | 1.0 41 | ); 42 | } 43 | `; 44 | 45 | const staticShaderNode = ( 46 | id: string, 47 | position: NodePosition, 48 | source = variation0 49 | ) => 50 | sourceNode( 51 | id, 52 | 'Static Shader', 53 | position, 54 | { 55 | version: 2, 56 | preprocess: true, 57 | strategies: [uniformStrategy()], 58 | uniforms: [], 59 | }, 60 | source, 61 | 'fragment', 62 | 'three' 63 | ); 64 | 65 | export default staticShaderNode; 66 | -------------------------------------------------------------------------------- /src/shaders/whiteNoiseNode.tsx: -------------------------------------------------------------------------------- 1 | import { NodePosition } from '@shaderfrog/core/src/core/nodes/core-node'; 2 | import { numberUniformData } from '@shaderfrog/core/src/core/nodes/data-nodes'; 3 | import { sourceNode } from '@shaderfrog/core/src/core/nodes/engine-node'; 4 | import { uniformStrategy } from '@shaderfrog/core/src/core/strategy'; 5 | 6 | const whiteNoiseNode = ( 7 | id: string, 8 | position: NodePosition, 9 | source = `precision highp float; 10 | precision highp int; 11 | 12 | uniform float speed; 13 | uniform float scale; 14 | uniform float time; 15 | varying vec2 vUv; 16 | 17 | float random2d(vec2 uv) { 18 | return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453); 19 | } 20 | 21 | void main() { 22 | float grain = random2d(vec2(sin(vUv * (scale * 100.0)) / 999999.9) * time * speed); 23 | vec3 color = vec3(grain); 24 | gl_FragColor = vec4(color, 1.0); 25 | } 26 | ` 27 | ) => 28 | sourceNode( 29 | id, 30 | 'White Noise', 31 | position, 32 | { 33 | version: 2, 34 | preprocess: true, 35 | strategies: [uniformStrategy()], 36 | uniforms: [ 37 | numberUniformData('scale', '10000.0', [0, 10000]), 38 | numberUniformData('speed', '1.0', [0, 10]), 39 | ], 40 | }, 41 | source, 42 | 'fragment', 43 | 'three' 44 | ); 45 | 46 | export default whiteNoiseNode; 47 | -------------------------------------------------------------------------------- /src/site/components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import MonacoEditor, { Monaco } from '@monaco-editor/react'; 2 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 3 | import { monacoGlsl } from '../../monaco-glsl'; 4 | 5 | import { Engine } from '@shaderfrog/core/src/core/engine'; 6 | 7 | type AnyFn = (...args: any) => any; 8 | 9 | type MonacoProps = { 10 | engine: Engine; 11 | defaultValue?: string; 12 | value?: string; 13 | readOnly?: boolean; 14 | onChange?: AnyFn; 15 | onSave?: AnyFn; 16 | }; 17 | const CodeEditor = ({ 18 | engine, 19 | value, 20 | defaultValue, 21 | readOnly, 22 | onChange, 23 | onSave, 24 | }: MonacoProps) => { 25 | const beforeMount = (monaco: Monaco) => { 26 | monaco.editor.defineTheme('frogTheme', { 27 | base: 'vs-dark', // can also be vs-dark or hc-black 28 | inherit: true, // can also be false to completely replace the builtin rules 29 | rules: [ 30 | { 31 | token: 'comment', 32 | foreground: 'ffa500', 33 | fontStyle: 'italic underline', 34 | }, 35 | { token: 'comment.js', foreground: '008800', fontStyle: 'bold' }, 36 | { token: 'comment.css', foreground: '0000ff' }, // will inherit fontStyle from `comment` above 37 | ], 38 | colors: { 39 | 'editor.background': '#000000', 40 | }, 41 | }); 42 | 43 | monacoGlsl(monaco); 44 | 45 | monaco.languages.registerCompletionItemProvider('glsl', { 46 | provideCompletionItems: (model, position) => { 47 | return { 48 | suggestions: [...engine.preserve.values()].map((keyword) => ({ 49 | label: keyword, 50 | kind: monaco.languages.CompletionItemKind.Text, 51 | insertText: keyword, 52 | range: { 53 | startLineNumber: 0, 54 | endLineNumber: 0, 55 | startColumn: 0, 56 | endColumn: 0, 57 | }, 58 | })), 59 | }; 60 | }, 61 | }); 62 | }; 63 | 64 | const onMount = ( 65 | editor: monaco.editor.IStandaloneCodeEditor, 66 | monaco: Monaco 67 | ) => { 68 | if (onSave) { 69 | editor.addAction({ 70 | // An unique identifier of the contributed action. 71 | id: 'my-unique-id', 72 | 73 | // A label of the action that will be presented to the user. 74 | label: 'My Label!!!', 75 | 76 | // An optional array of keybindings for the action. 77 | // @ts-ignore https://github.com/suren-atoyan/monaco-react/issues/338 78 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S], 79 | 80 | // A precondition for this action. 81 | // precondition: null, 82 | 83 | // A rule to evaluate on top of the precondition in order to dispatch the keybindings. 84 | // keybindingContext: null, 85 | 86 | contextMenuGroupId: 'navigation', 87 | 88 | contextMenuOrder: 1.5, 89 | 90 | // Method that will be executed when the action is triggered. 91 | // @param editor The editor instance is passed in as a convenience 92 | run: function (ed: any) { 93 | console.log( 94 | 'Monaco command-s run() called at editor position ' + 95 | ed.getPosition() 96 | ); 97 | onSave(); 98 | }, 99 | }); 100 | } 101 | }; 102 | 103 | return ( 104 | 118 | ); 119 | }; 120 | 121 | export default CodeEditor; 122 | -------------------------------------------------------------------------------- /src/site/components/flow/ConnectionLine.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import { 5 | ConnectionLineComponentProps, 6 | getBezierPath, 7 | Position, 8 | } from 'reactflow'; 9 | 10 | const ConnectionLine = ({ 11 | fromX, 12 | fromY, 13 | fromPosition, 14 | toX, 15 | toY, 16 | toPosition, 17 | connectionLineType, 18 | connectionLineStyle, 19 | fromNode, 20 | fromHandle, 21 | }: ConnectionLineComponentProps) => { 22 | const [edgePath] = getBezierPath({ 23 | sourceX: fromX, 24 | sourceY: fromY, 25 | sourcePosition: fromPosition, 26 | targetX: toX, 27 | targetY: toY, 28 | targetPosition: toPosition, 29 | }); 30 | 31 | return ( 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default ConnectionLine; 39 | -------------------------------------------------------------------------------- /src/site/components/flow/FlowEdge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | EdgeProps, 4 | getBezierPath, 5 | // getEdgeCenter, 6 | // getMarkerEnd, 7 | } from 'reactflow'; 8 | import { EdgeType } from '@shaderfrog/core/src/core/nodes/edge'; 9 | 10 | export type LinkEdgeData = { 11 | type: 'link'; 12 | }; 13 | 14 | export type FlowEdgeData = { 15 | type?: EdgeType; 16 | }; 17 | 18 | export default function FlowEdge({ 19 | id, 20 | sourceX, 21 | sourceY, 22 | targetX, 23 | targetY, 24 | sourcePosition, 25 | targetPosition, 26 | style = {}, 27 | markerEnd, 28 | }: EdgeProps) { 29 | const [edgePath] = getBezierPath({ 30 | sourceX, 31 | sourceY, 32 | sourcePosition, 33 | targetX, 34 | targetY, 35 | targetPosition, 36 | }); 37 | // const [edgeCenterX, edgeCenterY] = getEdgeCenter({ 38 | // sourceX, 39 | // sourceY, 40 | // targetX, 41 | // targetY, 42 | // }); 43 | 44 | // Note that className is an edge prop, not explicitly set here 45 | return ( 46 | <> 47 | 54 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/site/components/flow/FlowEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useRef, 4 | MouseEvent, 5 | useState, 6 | useEffect, 7 | useMemo, 8 | } from 'react'; 9 | import { useHotkeys } from 'react-hotkeys-hook'; 10 | import ConnectionLine from './ConnectionLine'; 11 | import create from 'zustand'; 12 | 13 | import ReactFlow, { 14 | Background, 15 | BackgroundVariant, 16 | XYPosition, 17 | ReactFlowProps, 18 | ReactFlowInstance, 19 | } from 'reactflow'; 20 | import 'reactflow/dist/style.css'; 21 | 22 | import { NodeType } from '@shaderfrog/core/src/core/graph'; 23 | import { EngineNodeType } from '@shaderfrog/core/src/core/engine'; 24 | import FlowEdgeComponent from './FlowEdge'; 25 | import { DataNodeComponent, SourceNodeComponent } from './FlowNode'; 26 | import { GraphDataType } from '@shaderfrog/core/src/core/nodes/data-nodes'; 27 | import { FlowEventHack } from '../../flowEventHack'; 28 | 29 | import styles from './context.menu.module.css'; 30 | 31 | /** 32 | * This file is an attempt to break up Editor.tsx by abstracting out the view 33 | * implementaiton of FlowEditor. Any visual / non-graph functionality inside 34 | * the graph editor is meant to go in here. 35 | * 36 | * The menu and the mouse position need input from the parent component. Right 37 | * now I pass the mouse as a mutable object and the menu position with zustand. 38 | * Maybe instead put both in zustand or pull it all up into the parent? I don't 39 | * want to cause a re-render on every mouse move which is why it's an object 40 | */ 41 | 42 | interface EditorStore { 43 | menuPosition: XYPosition | undefined; 44 | setMenuPosition: (p?: XYPosition) => void; 45 | } 46 | 47 | export const useEditorStore = create((set) => ({ 48 | menuPosition: undefined, 49 | setMenuPosition: (menuPosition) => set(() => ({ menuPosition })), 50 | })); 51 | 52 | // Terrible hack to make the flow graph full height minus the tab height - I 53 | // need better layoutting of the tabs + graph 54 | const flowStyles = { height: 'calc(100vh - 33px)', background: '#111' }; 55 | 56 | const flowKey = 'example-flow'; 57 | 58 | const nodeTypes: Record = { 59 | toon: SourceNodeComponent, 60 | phong: SourceNodeComponent, 61 | physical: SourceNodeComponent, 62 | shader: SourceNodeComponent, 63 | output: SourceNodeComponent, 64 | binary: SourceNodeComponent, 65 | source: SourceNodeComponent, 66 | vector2: DataNodeComponent, 67 | vector3: DataNodeComponent, 68 | vector4: DataNodeComponent, 69 | rgb: DataNodeComponent, 70 | rgba: DataNodeComponent, 71 | mat2: DataNodeComponent, 72 | mat3: DataNodeComponent, 73 | mat4: DataNodeComponent, 74 | mat2x2: DataNodeComponent, 75 | mat2x3: DataNodeComponent, 76 | mat2x4: DataNodeComponent, 77 | mat3x2: DataNodeComponent, 78 | mat3x3: DataNodeComponent, 79 | mat3x4: DataNodeComponent, 80 | mat4x2: DataNodeComponent, 81 | mat4x3: DataNodeComponent, 82 | mat4x4: DataNodeComponent, 83 | texture: DataNodeComponent, 84 | samplerCube: DataNodeComponent, 85 | number: DataNodeComponent, 86 | array: DataNodeComponent, 87 | }; 88 | 89 | const edgeTypes = { 90 | special: FlowEdgeComponent, 91 | }; 92 | 93 | export type MouseData = { real: XYPosition; projected: XYPosition }; 94 | 95 | type FlowEditorProps = 96 | | { 97 | mouse: React.MutableRefObject; 98 | onNodeValueChange: (id: string, value: any) => void; 99 | onMenuAdd: (type: string) => void; 100 | } & Pick< 101 | ReactFlowProps, 102 | | 'nodes' 103 | | 'edges' 104 | | 'onConnect' 105 | | 'onEdgeUpdate' 106 | | 'onEdgesChange' 107 | | 'onNodesChange' 108 | | 'onNodesDelete' 109 | | 'onNodeDoubleClick' 110 | | 'onEdgesDelete' 111 | | 'onConnectStart' 112 | | 'onEdgeUpdateStart' 113 | | 'onEdgeUpdateEnd' 114 | | 'onConnectEnd' 115 | >; 116 | 117 | const FlowEditor = ({ 118 | mouse, 119 | onMenuAdd, 120 | nodes, 121 | edges, 122 | onConnect, 123 | onEdgeUpdate, 124 | onEdgesChange, 125 | onNodesChange, 126 | onNodesDelete, 127 | onNodeDoubleClick, 128 | onEdgesDelete, 129 | onConnectStart, 130 | onEdgeUpdateStart, 131 | onEdgeUpdateEnd, 132 | onConnectEnd, 133 | onNodeValueChange, 134 | }: FlowEditorProps) => { 135 | const menuPos = useEditorStore((state) => state.menuPosition); 136 | const setMenuPos = useEditorStore((state) => state.setMenuPosition); 137 | 138 | useHotkeys('esc', () => setMenuPos()); 139 | useHotkeys('shift+a', () => setMenuPos(mouse.current.real)); 140 | 141 | const onContextMenu = useCallback( 142 | (event: MouseEvent) => { 143 | event.preventDefault(); 144 | setMenuPos(mouse.current.real); 145 | }, 146 | // eslint-disable-next-line react-hooks/exhaustive-deps 147 | [setMenuPos] 148 | ); 149 | 150 | const [rfInstance, setRfInstance] = useState(); 151 | const onMoveEnd = useCallback(() => { 152 | if (rfInstance) { 153 | const flow = rfInstance.toObject().viewport; 154 | localStorage.setItem(flowKey, JSON.stringify(flow)); 155 | } 156 | }, [rfInstance]); 157 | const defaultViewport = useMemo( 158 | () => 159 | JSON.parse(localStorage.getItem(flowKey) || 'null') || { 160 | x: 200, 161 | y: 150, 162 | zoom: 0.5, 163 | }, 164 | [] 165 | ); 166 | 167 | return ( 168 |
169 | {menuPos ? : null} 170 | 171 | 194 | 200 | 201 | 202 |
203 | ); 204 | }; 205 | 206 | FlowEditor.displayName = 'FlowEditor'; 207 | 208 | type Menu = [string, string | Menu][]; 209 | const ctxNodes: Menu = [ 210 | [ 211 | 'Source Code', 212 | [ 213 | ['Fragment', 'fragment'], 214 | ['Vertex', 'vertex'], 215 | ], 216 | ], 217 | [ 218 | 'Data', 219 | [ 220 | ['Number', 'number'], 221 | ['Texture', 'texture'], 222 | ['Sampler Cube', 'samplerCube'], 223 | ['Vector2', 'vector2'], 224 | ['Vector3', 'vector3'], 225 | ['Vector4', 'vector4'], 226 | ['Color (RGB)', 'rgb'], 227 | ['Color (RGBA)', 'rgba'], 228 | ], 229 | ], 230 | [ 231 | 'Math', 232 | [ 233 | ['Add', 'add'], 234 | ['Multiply', 'multiply'], 235 | ], 236 | ], 237 | [ 238 | 'Example Shader', 239 | [ 240 | ['Physical', 'physical'], 241 | ['Phong', 'phong'], 242 | ['Toon', 'toon'], 243 | ['Serpent', 'serpent'], 244 | ['Fireball', 'fireNode'], 245 | ['Julia', 'julia'], 246 | ['Bad TV', 'badTv'], 247 | ['Checkerboard', 'checkerboardF'], 248 | ['Fluid Circles', 'fluidCirclesNode'], 249 | ['Heatmap', 'heatmapShaderNode'], 250 | ['Hell', 'hellOnEarth'], 251 | ['Outline', 'outlineShader'], 252 | ['Perlin Clouds', 'perlinClouds'], 253 | ['Purple Noise', 'purpleNoiseNode'], 254 | ['Solid Color', 'solidColorNode'], 255 | ['White Noise', 'whiteNoiseNode'], 256 | ['Tangent Noise', 'staticShaderNode'], 257 | ['Normal Map-ify', 'normalMapify'], 258 | ['Vertex Noise', 'simpleVertex'], 259 | ['Cube Map Reflection', 'cubemapReflection'], 260 | ], 261 | ], 262 | ]; 263 | const ContextMenu = ({ 264 | position, 265 | onAdd, 266 | menu = ctxNodes, 267 | title = 'Add a node', 268 | onMouseEnter, 269 | }: { 270 | onAdd: (name: string) => void; 271 | position: XYPosition; 272 | menu?: Menu; 273 | title?: string; 274 | onMouseEnter?: (e: MouseEvent) => void; 275 | }) => { 276 | const [childMenu, setChildMenu] = useState<[string, Menu]>(); 277 | 278 | const timeout = useRef(); 279 | const onParentMenuEnter = useCallback(() => { 280 | if (childMenu) { 281 | if (timeout.current) { 282 | clearTimeout(timeout.current); 283 | } 284 | timeout.current = setTimeout(() => { 285 | setChildMenu(undefined); 286 | }, 500); 287 | } 288 | }, [childMenu, setChildMenu]); 289 | 290 | useEffect(() => { 291 | return () => { 292 | if (timeout.current) { 293 | clearTimeout(timeout.current); 294 | } 295 | }; 296 | }, []); 297 | 298 | return ( 299 | <> 300 |
306 |
{title}
307 | {menu.map(([display, typeOrChildren]) => 308 | typeof typeOrChildren === 'string' ? ( 309 |
onAdd(typeOrChildren)} 313 | onMouseEnter={onParentMenuEnter} 314 | > 315 | {display} 316 |
317 | ) : ( 318 |
{ 322 | if (timeout.current) { 323 | clearTimeout(timeout.current); 324 | } 325 | setChildMenu([display, typeOrChildren]); 326 | }} 327 | > 328 | {display} ➤ 329 |
330 | ) 331 | )} 332 |
333 | {childMenu ? ( 334 | { 340 | if (timeout.current) { 341 | clearTimeout(timeout.current); 342 | } 343 | }} 344 | > 345 | ) : null} 346 | 347 | ); 348 | }; 349 | 350 | export default FlowEditor; 351 | -------------------------------------------------------------------------------- /src/site/components/flow/context.menu.module.css: -------------------------------------------------------------------------------- 1 | .contextMenu { 2 | position: absolute; 3 | z-index: 10; 4 | background: #222; 5 | color: #fff; 6 | border-radius: 8px; 7 | border: 1px solid #444; 8 | padding: 0 0 8px 0; 9 | user-select: none; 10 | min-width: 125px; 11 | font-size: 12px; 12 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); 13 | } 14 | 15 | .contextBg { 16 | position: absolute; 17 | z-index: 9; 18 | top: 0; 19 | border: 1px solid red; 20 | min-width: 120px; 21 | height: 100%; 22 | } 23 | 24 | .contextHeader { 25 | padding: 4px 12px; 26 | color: rgb(165, 165, 165); 27 | border-bottom: 1px solid #444; 28 | cursor: default; 29 | } 30 | 31 | .contextRow { 32 | padding: 4px 4px 4px 12px; 33 | } 34 | 35 | .contextRow:hover { 36 | background: rgb(58, 58, 124); 37 | cursor: pointer; 38 | } 39 | 40 | .flowContainer { 41 | height: 100%; 42 | width: 100%; 43 | } 44 | -------------------------------------------------------------------------------- /src/site/components/flow/flownode.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | grid-auto-flow: column; 4 | grid-template-columns: repeat(auto-fit, 1fr); 5 | grid-column-gap: 2px; 6 | } 7 | 8 | .grid input[type='text'] { 9 | width: 100%; 10 | } 11 | 12 | .vectorLabel { 13 | color: #666; 14 | display: inline-block; 15 | margin-right: 4px; 16 | } 17 | 18 | :global(.fragment) .inputSection { 19 | background: #334433; 20 | } 21 | :global(.vertex) .inputSection { 22 | background: #443333; 23 | } 24 | 25 | .inputSection { 26 | color: #eee; 27 | background: linear-gradient(#333, #333344); 28 | position: absolute; 29 | right: 0; 30 | left: 0; 31 | padding: 2px 0 2px 12px; 32 | } 33 | 34 | .outputLabel { 35 | text-align: right; 36 | left: -206px; 37 | } 38 | 39 | .outputs { 40 | position: absolute; 41 | right: 0; 42 | width: 200px; 43 | top: 0; 44 | } 45 | 46 | .dataType { 47 | font-size: 10px; 48 | margin-left: 4px; 49 | color: #888; 50 | } 51 | -------------------------------------------------------------------------------- /src/site/components/flow/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Graph, 3 | GraphNode, 4 | ShaderStage, 5 | MAGIC_OUTPUT_STMTS, 6 | isSourceNode, 7 | NodeType, 8 | alphabet, 9 | findNode, 10 | } from '@shaderfrog/core/src/core/graph'; 11 | import { Edge as GraphEdge } from '@shaderfrog/core/src/core/nodes/edge'; 12 | import { Node as FlowNode, Edge as FlowEdge, XYPosition } from 'reactflow'; 13 | import { FlowEdgeData } from './FlowEdge'; 14 | import { 15 | FlowNodeData, 16 | FlowNodeSourceData, 17 | flowOutput, 18 | InputNodeHandle, 19 | } from './FlowNode'; 20 | import { NodeInput } from '@shaderfrog/core/src/core/nodes/core-node'; 21 | 22 | export type FlowElement = FlowNode | FlowEdge; 23 | export type FlowEdgeOrLink = FlowEdge; 24 | export type FlowElements = { 25 | nodes: FlowNode[]; 26 | edges: FlowEdgeOrLink[]; 27 | }; 28 | 29 | // Determine the stage of a node (vertex/fragment) by recursively looking at 30 | // the nodes that feed into this one, until we find one that has a stage set 31 | export const findInputStage = ( 32 | ids: Record>, 33 | edgesByTarget: Record[]>, 34 | node: FlowNode 35 | ): ShaderStage | undefined => { 36 | let nodeData = node.data as FlowNodeSourceData; 37 | return ( 38 | (!nodeData?.biStage && nodeData?.stage) || 39 | (edgesByTarget[node.id] || []).reduce( 40 | (found, edge) => { 41 | const type = edge.data?.type; 42 | return ( 43 | found || 44 | (type === 'fragment' || type === 'vertex' ? type : false) || 45 | findInputStage(ids, edgesByTarget, ids[edge.source]) 46 | ); 47 | }, 48 | undefined 49 | ) 50 | ); 51 | }; 52 | 53 | export const setFlowNodeCategories = ( 54 | flowElements: FlowElements, 55 | dataNodes: Record 56 | ): FlowElements => ({ 57 | ...flowElements, 58 | nodes: flowElements.nodes.map((node) => { 59 | if (node.id in dataNodes) { 60 | return { 61 | ...node, 62 | data: { 63 | ...node.data, 64 | category: 'code', 65 | }, 66 | }; 67 | } 68 | return node; 69 | }), 70 | }); 71 | 72 | // Some nodes, like add, can be used for either fragment or vertex stage. When 73 | // we connect edges in the graph, update it to figure out which stage we should 74 | // set the add node to based on inputs to the node. 75 | export const setFlowNodeStages = (flowElements: FlowElements): FlowElements => { 76 | const targets = flowElements.edges.reduce>( 77 | (acc, edge) => ({ 78 | ...acc, 79 | [edge.target]: [...(acc[edge.target] || []), edge], 80 | }), 81 | {} 82 | ); 83 | const ids = flowElements.nodes.reduce>( 84 | (acc, node) => ({ 85 | ...acc, 86 | [node.id]: node, 87 | }), 88 | {} 89 | ); 90 | 91 | const updatedSides: Record = {}; 92 | // Update the node stages by looking at their inputs 93 | return { 94 | nodes: flowElements.nodes.map((node) => { 95 | if (!node.data || !('biStage' in node.data)) { 96 | return node; 97 | } 98 | if (!node.data.biStage && node.data.stage) { 99 | return node; 100 | } 101 | return (updatedSides[node.id] = { 102 | ...node, 103 | data: { 104 | ...node.data, 105 | stage: findInputStage(ids, targets, node), 106 | }, 107 | }); 108 | }), 109 | // Set the stage for edges connected to nodes whose stage changed 110 | edges: flowElements.edges.map((element) => { 111 | if (!('source' in element) || !(element.source in updatedSides)) { 112 | return element; 113 | } 114 | const { stage } = updatedSides[element.source].data as FlowNodeSourceData; 115 | return { 116 | ...element, 117 | // className: element.data?.type === 'data' ? element.data.type : stage, 118 | data: { 119 | ...element.data, 120 | stage, 121 | }, 122 | }; 123 | }), 124 | }; 125 | }; 126 | 127 | export const toFlowInputs = (node: GraphNode): InputNodeHandle[] => 128 | (node.inputs || []) 129 | .filter(({ displayName }) => displayName !== MAGIC_OUTPUT_STMTS) 130 | .map((input) => ({ 131 | id: input.id, 132 | name: input.displayName, 133 | type: input.type, 134 | dataType: input.dataType, 135 | baked: input.baked, 136 | bakeable: input.bakeable, 137 | validTarget: false, 138 | accepts: input.accepts, 139 | })); 140 | 141 | export const graphNodeToFlowNode = ( 142 | node: GraphNode, 143 | onInputBakedToggle: any, 144 | position: XYPosition 145 | ): FlowNode => { 146 | const data: FlowNodeData = isSourceNode(node) 147 | ? { 148 | label: node.name, 149 | stage: node.stage, 150 | active: false, 151 | biStage: node.biStage || false, 152 | inputs: toFlowInputs(node), 153 | outputs: node.outputs.map((o) => flowOutput(o.name)), 154 | onInputBakedToggle, 155 | } 156 | : { 157 | label: node.name, 158 | type: node.type, 159 | value: node.value, 160 | inputs: toFlowInputs(node), 161 | outputs: node.outputs.map((o) => flowOutput(o.name)), 162 | config: { ...node }, 163 | }; 164 | return { 165 | id: node.id, 166 | data, 167 | // type: isSourceNode(node) ? 'source' : 'data', 168 | type: node.type, 169 | position, 170 | }; 171 | }; 172 | 173 | export const flowEdgeToGraphEdge = ( 174 | edge: FlowEdge 175 | ): GraphEdge => ({ 176 | id: edge.id, 177 | from: edge.source, 178 | to: edge.target, 179 | output: 'out', 180 | input: edge.targetHandle as string, 181 | type: edge.data?.type, 182 | }); 183 | 184 | export const graphEdgeToFlowEdge = ( 185 | edge: GraphEdge 186 | ): FlowEdge => ({ 187 | id: edge.id, 188 | source: edge.from, 189 | sourceHandle: edge.output, 190 | targetHandle: edge.input, 191 | target: edge.to, 192 | data: { type: edge.type }, 193 | className: edge.type, 194 | // Not the edge type, the flow edge component type that renders this edge 195 | type: 'special', 196 | }); 197 | 198 | export const updateGraphInput = ( 199 | graph: Graph, 200 | nodeId: string, 201 | inputId: string, 202 | data: Partial 203 | ): Graph => ({ 204 | ...graph, 205 | nodes: graph.nodes.map((node) => 206 | node.id === nodeId 207 | ? { 208 | ...node, 209 | inputs: node.inputs.map((input) => 210 | input.id === inputId 211 | ? { 212 | ...input, 213 | ...data, 214 | } 215 | : input 216 | ), 217 | } 218 | : node 219 | ), 220 | }); 221 | 222 | export const updateGraphNode = ( 223 | graph: Graph, 224 | nodeId: string, 225 | data: Partial 226 | ): Graph => ({ 227 | ...graph, 228 | // @ts-ignore 229 | nodes: graph.nodes.map((node) => 230 | node.id === nodeId 231 | ? { 232 | ...node, 233 | ...data, 234 | } 235 | : node 236 | ), 237 | }); 238 | 239 | export const updateFlowNodeData = ( 240 | flowElements: FlowElements, 241 | nodeId: string, 242 | data: Partial 243 | ): FlowElements => ({ 244 | ...flowElements, 245 | nodes: flowElements.nodes.map((node) => 246 | node.id === nodeId 247 | ? { 248 | ...node, 249 | data: { 250 | ...node.data, 251 | ...data, 252 | }, 253 | } 254 | : node 255 | ), 256 | }); 257 | 258 | export const updateFlowInput = ( 259 | flowElements: FlowElements, 260 | nodeId: string, 261 | inputId: string, 262 | data: Partial 263 | ): FlowElements => ({ 264 | ...flowElements, 265 | nodes: flowElements.nodes.map((node) => 266 | node.id === nodeId 267 | ? { 268 | ...node, 269 | data: { 270 | ...node.data, 271 | inputs: node.data.inputs.map((input) => 272 | input.id === inputId 273 | ? { 274 | ...input, 275 | ...data, 276 | } 277 | : input 278 | ), 279 | }, 280 | } 281 | : node 282 | ), 283 | }); 284 | 285 | export const addGraphEdge = (graph: Graph, newEdge: GraphEdge): Graph => { 286 | const updatedEdges = graph.edges.filter( 287 | (edge) => 288 | // Prevent one input handle from having multiple inputs 289 | !( 290 | (edge.to === newEdge.to && edge.input === newEdge.input) 291 | // Prevent one output handle from having multiple lines out 292 | ) && !(edge.from === newEdge.from && edge.output === newEdge.output) 293 | ); 294 | 295 | const updatedGraph: Graph = { 296 | ...graph, 297 | edges: [...updatedEdges, newEdge], 298 | }; 299 | return collapseBinaryGraphEdges(updatedGraph); 300 | }; 301 | 302 | export const addFlowEdge = ( 303 | flowElements: FlowElements, 304 | newEdge: FlowEdge 305 | ): FlowElements => { 306 | const updatedEdges = flowElements.edges.filter( 307 | (element) => 308 | // Prevent one input handle from having multiple inputs 309 | !( 310 | ( 311 | 'targetHandle' in element && 312 | element.targetHandle === newEdge.targetHandle && 313 | element.target === newEdge.target 314 | ) 315 | // Prevent one output handle from having multiple lines out 316 | ) && 317 | !( 318 | 'sourceHandle' in element && 319 | element.sourceHandle === newEdge.sourceHandle && 320 | element.source === newEdge.source 321 | ) 322 | ); 323 | 324 | const updatedFlowElements = setFlowNodeStages({ 325 | ...flowElements, 326 | edges: [...updatedEdges, newEdge], 327 | }); 328 | return collapseBinaryFlowEdges(updatedFlowElements); 329 | }; 330 | 331 | /** 332 | * A binary node automatically adds/removes inputs based on how many edges 333 | * connect to it. If a binary node has edges to "a" and "b", removing the edge 334 | * to "a" means the edge to "b" needs to be moved down to the "a" one. This 335 | * function essentially groups edges by target node id, and resets the edge 336 | * target to its index. This doesn't feel good to do here but I don't have a 337 | * better idea at the moment. One reason the inputs to binary nodes are 338 | * automatically updated after compile, but the edges are updated here 339 | * at the editor layer, before compile. This also hard codes assumptions about 340 | * (binary) node inputs into the graph, namely they can't have blank inputs. 341 | */ 342 | export const collapseBinaryGraphEdges = (graph: Graph): Graph => { 343 | // Find all edges that flow into a binary node, grouped by the target node's 344 | // id, since we need to know the total number of edges per node first 345 | const binaryEdges = graph.edges.reduce>( 346 | (acc, edge) => { 347 | const toNode = findNode(graph, edge.to); 348 | return toNode.type === NodeType.BINARY 349 | ? { 350 | ...acc, 351 | [toNode.id]: [...(acc[toNode.id] || []), edge], 352 | } 353 | : acc; 354 | }, 355 | {} 356 | ); 357 | 358 | // Then collapse them 359 | const updatedEdges = graph.edges.map((edge) => { 360 | return edge.input in binaryEdges 361 | ? { 362 | ...edge, 363 | input: alphabet.charAt(binaryEdges[edge.input].indexOf(edge)), 364 | } 365 | : edge; 366 | }); 367 | return { 368 | ...graph, 369 | edges: updatedEdges, 370 | }; 371 | }; 372 | 373 | export const collapseBinaryFlowEdges = ( 374 | flowGraph: FlowElements 375 | ): FlowElements => { 376 | // Find all edges that flow into a binary node, grouped by the target node's 377 | // id, since we need to know the total number of edges per node first 378 | const binaryEdges = flowGraph.edges.reduce>( 379 | (acc, edge) => { 380 | const toNode = flowGraph.nodes.find(({ id }) => id === edge.target); 381 | return toNode?.type === NodeType.BINARY 382 | ? { 383 | ...acc, 384 | [toNode.id]: [...(acc[toNode.id] || []), edge], 385 | } 386 | : acc; 387 | }, 388 | {} 389 | ); 390 | 391 | // Then collapse them 392 | const updatedEdges = flowGraph.edges.map((edge) => { 393 | return edge.target in binaryEdges 394 | ? { 395 | ...edge, 396 | targetHandle: alphabet.charAt(binaryEdges[edge.target].indexOf(edge)), 397 | } 398 | : edge; 399 | }); 400 | return { 401 | ...flowGraph, 402 | edges: updatedEdges, 403 | }; 404 | }; 405 | 406 | export const graphToFlowGraph = ( 407 | graph: Graph, 408 | onInputBakedToggle: any 409 | ): FlowElements => { 410 | console.log('Initializing flow elements from', { graph }); 411 | 412 | const nodes = graph.nodes.map((node) => 413 | graphNodeToFlowNode(node, onInputBakedToggle, node.position) 414 | ); 415 | 416 | const edges: FlowEdgeOrLink[] = graph.edges.map(graphEdgeToFlowEdge); 417 | 418 | return setFlowNodeStages({ nodes, edges }); 419 | }; 420 | -------------------------------------------------------------------------------- /src/site/components/tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import classnames from 'classnames/bind'; 3 | import style from './tabs.module.css'; 4 | const cx = classnames.bind(style); 5 | 6 | // Overall wrapping component 7 | const Tabs = ({ 8 | children, 9 | selected, 10 | onSelect, 11 | selectedClassName, 12 | }: { 13 | children: React.ReactNode; 14 | selectedClassName?: any; 15 | onSelect: (index: number) => void; 16 | selected: number; 17 | }) => { 18 | return ( 19 | <> 20 | {React.Children.map( 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 | --------------------------------------------------------------------------------