├── .gitignore ├── Definitions.js ├── Editor.js ├── EditorDragDrop.js ├── ExpressionParser.js ├── LICENSE ├── NodeGraph.js ├── NodePinUI.js ├── NodeUI.js ├── README.md ├── RenderUtils.js ├── Rendering.js ├── Shaders.js ├── UIControls.js ├── cubemaps ├── README ├── hdri2ldrcube.c ├── makefile ├── stb_image.h └── tiny_jpeg.h ├── ext ├── fa │ ├── LICENSE.txt │ ├── css │ │ ├── fa-solid.css │ │ └── fontawesome.css │ └── webfonts │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 ├── noisejs │ ├── LICENSE │ └── perlin.js └── wabi │ ├── LICENSE │ ├── wabi.dev.min.js │ ├── wabi.min.js │ ├── wabi.patch │ ├── wabi.unpooled.dev.js │ ├── wabi.unpooled.edit.js │ └── wabi.unpooled.js ├── imgsrc └── grid.png ├── notes ├── notes.txt └── shedit-pinbtn.dia ├── rsrc ├── wall1BaseColor.jpg ├── wall1Metallic.jpg ├── wall1Normal.jpg └── wall1Roughness.jpg ├── shaderedit.css └── shaderedit.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.hdr 3 | cubemaps/*.jpg 4 | -------------------------------------------------------------------------------- /Definitions.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | noise.seed(1) 5 | 6 | 7 | 8 | function texGenChecker(w, h, div) 9 | { 10 | const data = new Uint32Array(w * h) 11 | const wd = w / div 12 | const hd = h / div 13 | for (var y = 0; y < h; ++y) 14 | { 15 | for (var x = 0; x < w; ++x) 16 | { 17 | data[x + w * y] = (Math.floor(x / wd) + Math.floor(y / hd)) % 2 ? 0xff000000 : 0xffffffff 18 | } 19 | } 20 | return { width: w, height: h, data: data } 21 | } 22 | 23 | function texGenPerlinNoiseGray(w, h, div) 24 | { 25 | const data = new Uint32Array(w * h) 26 | for (var y = 0; y < h; ++y) 27 | { 28 | for (var x = 0; x < w; ++x) 29 | { 30 | var v = Math.floor((noise.perlin2(x / div, y / div) * 0.5 + 0.5) * 255) | 0 31 | data[x + w * y] = 0xff000000 | (v << 16) | (v << 8) | v 32 | } 33 | } 34 | return { width: w, height: h, data: data } 35 | } 36 | 37 | function texGenFractalNoiseGray(w, h) 38 | { 39 | const data = new Uint32Array(w * h) 40 | for (var y = 0; y < h; ++y) 41 | { 42 | for (var x = 0; x < w; ++x) 43 | { 44 | var acc = 0 45 | var num = 0 46 | for (var div = 1; div < w; div *= 2) 47 | { 48 | acc += noise.perlin2(x / div, y / div) * 0.5 + 0.5 49 | num++ 50 | } 51 | acc /= num 52 | var v = Math.floor(acc * 255) | 0 53 | data[x + w * y] = 0xff000000 | (v << 16) | (v << 8) | v 54 | } 55 | } 56 | return { width: w, height: h, data: data } 57 | } 58 | 59 | 60 | 61 | var nodeResources = 62 | { 63 | variable: 64 | { 65 | time: { type: "uniform", dims: 1 }, 66 | uCameraPos: { type: "uniform", dims: 3 }, 67 | vWorldPos: { type: "varying", dims: 3 }, 68 | vWorldNormal: { type: "varying", dims: 3 }, 69 | vWorldTangent: { type: "varying", dims: 4 }, 70 | vTexCoord0: { type: "varying", dims: 2 }, 71 | }, 72 | sampler2D: 73 | { 74 | // generated 75 | checker2x2: 76 | { 77 | desc: "2x2 Checkerboard pattern (256x256 px)", 78 | genFunc: () => texGenChecker(256, 256, 2), 79 | }, 80 | checker8x8: 81 | { 82 | desc: "8x8 Checkerboard pattern (256x256 px)", 83 | genFunc: () => texGenChecker(256, 256, 8), 84 | }, 85 | perlinGrayscale4: 86 | { 87 | desc: "Grayscale Perlin noise [step=4] (256x256 px)", 88 | genFunc: () => texGenPerlinNoiseGray(256, 256, 4), 89 | }, 90 | fractalGrayscale: 91 | { 92 | desc: "Grayscale fractal noise (256x256 px)", 93 | genFunc: () => texGenFractalNoiseGray(256, 256), 94 | }, 95 | // images 96 | wall1BaseColor: 97 | { 98 | desc: "Wall 1 - base color (1024x1024 px)", 99 | genFunc: () => "rsrc/wall1BaseColor.jpg", 100 | }, 101 | wall1Normal: 102 | { 103 | desc: "Wall 1 - normals (1024x1024 px)", 104 | genFunc: () => "rsrc/wall1Normal.jpg", 105 | }, 106 | wall1Metallic: 107 | { 108 | desc: "Wall 1 - metalness (1024x1024 px)", 109 | genFunc: () => "rsrc/wall1Metallic.jpg", 110 | }, 111 | wall1Roughness: 112 | { 113 | desc: "Wall 1 - roughness (1024x1024 px)", 114 | genFunc: () => "rsrc/wall1Roughness.jpg", 115 | }, 116 | }, 117 | op: 118 | { 119 | add: { args: 2, name: "Add" }, 120 | sub: { args: 2, name: "Subtract" }, 121 | mul: { args: 2, name: "Multiply" }, 122 | div: { args: 2, name: "Divide" }, 123 | mod: { args: 2, name: "Modulo" }, 124 | 125 | eq: { args: 2, name: "Equal" }, 126 | neq: { args: 2, name: "Not equal" }, 127 | lt: { args: 2, name: "Less than" }, 128 | gt: { args: 2, name: "Greater than" }, 129 | lte: { args: 2, name: "Less than or equal" }, 130 | gte: { args: 2, name: "Greater than or equal" }, 131 | 132 | min: { args: 2, name: "Min" }, 133 | max: { args: 2, name: "Max" }, 134 | clamp: { args: 3, name: "Clamp" }, 135 | mix: { args: 3, name: "Mix" }, 136 | step: { args: 2, name: "Step" }, 137 | smoothstep: { args: 3, name: "Smoothstep" }, 138 | 139 | abs: { args: 1, name: "Absolute" }, 140 | sign: { args: 1, name: "Sign" }, 141 | round: { args: 1, name: "Round" }, 142 | floor: { args: 1, name: "Floor" }, 143 | ceil: { args: 1, name: "Ceiling" }, 144 | fract: { args: 1, name: "Fractional part" }, 145 | 146 | normalize: { args: 1, name: "Normalize vector" }, 147 | length: { args: 1, name: "Length of vector" }, 148 | distance: { args: 2, name: "Distance" }, 149 | dot: { args: 2, name: "Dot product" }, 150 | cross: { args: 2, name: "Cross product" }, 151 | 152 | unpacknormal: { args: 1, name: "Unpack normal" }, 153 | height2normal: { args: 1, name: "Height -> normal" }, 154 | combine: { args: 4, name: "Combine" }, 155 | }, 156 | expr2op: 157 | { 158 | "b+": "add", 159 | "b-": "sub", 160 | "b*": "mul", 161 | "b/": "div", 162 | "mod": "mod", 163 | 164 | "b==": "eq", 165 | "b!=": "neq", 166 | "b<": "lt", 167 | "b>": "gt", 168 | "b<=": "lte", 169 | "b>=": "gte", 170 | 171 | "min": "min", 172 | "max": "max", 173 | "clamp": "clamp", 174 | "mix": "mix", 175 | "step": "step", 176 | "smoothstep": "smoothstep", 177 | 178 | "abs": "abs", 179 | "sign": "sign", 180 | "round": "round", 181 | "floor": "floor", 182 | "ceil": "ceil", 183 | "fract": "fract", 184 | 185 | "normalize": "normalize", 186 | "length": "length", 187 | "distance": "distance", 188 | "dot": "dot", 189 | "cross": "cross", 190 | 191 | "unpacknormal": "unpacknormal", 192 | "height2normal": "height2normal", 193 | "combine": "combine", 194 | }, 195 | func: 196 | { 197 | PBR_Metallic: 198 | { 199 | name: "PBR (Metallic)", 200 | retDims: 4, 201 | variable: { uCameraPos: true, vWorldPos: true, vWorldNormal: true, vWorldTangent: true }, 202 | samplerCube: { sCubemap: true }, 203 | args: 204 | [ 205 | { dims: 3, name: "BaseColor", defval: [0.25, 0.5, 0.9] }, 206 | { dims: 3, name: "Normal", flags: ARG_NONUMEDIT, defval: [0, 0, 1] }, 207 | { dims: 1, name: "Metallic", defval: [1] }, 208 | { dims: 1, name: "Roughness", defval: [0.3] }, 209 | { dims: 1, name: "Opacity", defval: [1] }, 210 | ], 211 | code: ` 212 | BaseColor = pow(BaseColor, vec3(2.2)); 213 | vec3 T = normalize(vWorldTangent.xyz); 214 | vec3 N = normalize(vWorldNormal); 215 | vec3 V = normalize(vWorldPos - uCameraPos); 216 | vec3 diffuseColor = mix(BaseColor, vec3(0), Metallic); 217 | vec3 F0 = mix(vec3(0.05), BaseColor, Metallic); 218 | 219 | mat3 TBN = mat3(T, cross(T, N) * vWorldTangent.w, N); 220 | N = TBN * Normal; 221 | 222 | vec4 cmDiffSample = textureCubeLodEXT(sCubemap, N, 20.0); 223 | vec4 cmSpecSample = textureCubeLodEXT(sCubemap, reflect(V, N), sqrt(Roughness) * 9.0); 224 | vec3 diffuseLighting = pow(cmDiffSample.rgb, vec3(2.2)); 225 | vec3 specularLighting = pow(cmSpecSample.rgb, vec3(2.2)); 226 | 227 | vec3 fSpec = F0 + (vec3(1.0) - F0) * pow(1.0 - abs(dot(V, N)), 5.0); 228 | fSpec = mix(fSpec, F0, Roughness); // custom lerp trick for removing edge highlights on rough surfaces 229 | vec3 fDiff = (1.0 - fSpec) * (1.0 - Metallic); 230 | vec3 totalSpec = specularLighting * fSpec; 231 | 232 | //vec4 lit = vec4(diffuseColor * diffuseLighting * fDiff * Opacity + totalSpec, Opacity); -- premultiplied alpha opacity 233 | // refraction-based fake opacity 234 | diffuseLighting = mix(textureCubeLodEXT(sCubemap, refract(V, N, 1.0 / 1.05), sqrt(Roughness) * 9.0).rgb, diffuseLighting, Opacity); 235 | vec4 lit = vec4(diffuseColor * diffuseLighting * fDiff + totalSpec, 1); 236 | 237 | //lit.rgb = lit.rgb / (1.0 + lit.rgb); 238 | lit.rgb = pow(lit.rgb, vec3(1.0 / 2.2)); 239 | return lit; 240 | `, 241 | }, 242 | }, 243 | } 244 | 245 | 246 | 247 | // I/O dimensionality of nodes: 248 | // to minimize code size, casts are folded into function (node) arguments and made implicit 249 | // there are several types of dimension configurations for shader functions 250 | // - fixed (data - texture/variable - reads, [cross]) 251 | // arguments and return values have a fixed, never-changing type 252 | // - adaptive (most math functions) 253 | // return value and argument dimension count is defined by biggest argument, smaller arguments are casted before the operation 254 | // - complicated (matrix-related functions) 255 | // not used or considered so far 256 | // how does adaptive dimension configuration solve the embedded constant issue? 257 | // - let the user pick whatever is desired, with an option to match the other nodes (- max(args where explicit)) 258 | // - if no arguments have nodes assigned or explicit dimension count picked, match makes all arguments 1-D 259 | 260 | window.nodeIDGen = 0 261 | var nodeTypes = 262 | { 263 | output: 264 | { 265 | name: "Output", 266 | desc: "Return a color value from this shader", 267 | getArgCount: (n) => 1, 268 | getArgName: (n, i) => "Output", 269 | getArgDims: (n, i) => funcGetData(n.func).retDims, 270 | getArgFlags: (n, i) => 0, 271 | getArgDefVal: (n, i) => null, 272 | genInline: true, 273 | getRVDims: (n) => funcGetData(n.func).retDims, 274 | getCode: (n) => _shGenArg(n, 0), 275 | getExpr: () => { throw "not supposed to be called" } 276 | }, 277 | func: 278 | { 279 | name: "Function", 280 | desc: "Call another function", 281 | rsrcType: "func", 282 | rsrcFullRefreshOnChange: true, 283 | getArgCount: (n) => n.rsrc !== null ? funcGetDataAll(n.rsrc).args.length : 0, 284 | getArgName: (n, i) => n.rsrc !== null ? funcGetDataAll(n.rsrc).args[i].name : "#" + (i + 1), 285 | getArgDims: (n, i) => n.rsrc !== null ? funcGetDataAll(n.rsrc).args[i].dims : 0, 286 | getArgFlags: (n, i) => n.rsrc !== null ? funcGetDataAll(n.rsrc).args[i].flags : 0, 287 | getArgDefVal: (n, i) => n.rsrc !== null ? funcGetDataAll(n.rsrc).args[i].defval : null, 288 | getRVDims: (n) => n.rsrc !== null ? funcGetDataAll(n.rsrc).retDims : 4, 289 | getCode: (n) => 290 | { 291 | if (n.rsrc !== null) 292 | { 293 | const argCount = funcGetDataAll(n.rsrc).args.length 294 | const argsStrs = [] 295 | for (var i = 0; i < argCount; ++i) 296 | argsStrs.push(_shGenArg(n, i)) 297 | const argsStr = argsStrs.join(", ") 298 | return `${n.rsrc}_f(${argsStr})` 299 | } 300 | return shGenVal(nodeTypes[n.type].getRVDims(n)) 301 | }, 302 | getExpr: (n, l) => 303 | { 304 | if (n.rsrc !== null) 305 | { 306 | const argCount = funcGetDataAll(n.rsrc).args.length 307 | const argsStrs = [] 308 | for (var i = 0; i < argCount; ++i) 309 | argsStrs.push(nodeGetArgExpr(n, i, l+3)) 310 | const argsStr = argsStrs.join(", ") 311 | return `${n.rsrc}(${argsStr})` 312 | } 313 | return shGenVal(nodeTypes[n.type].getRVDims(n)) 314 | }, 315 | }, 316 | tex2D: 317 | { 318 | name: "Texture (2D)", 319 | desc: "Sample a 2D texture", 320 | rsrcType: "sampler2D", 321 | getArgCount: (n) => 1, 322 | getArgName: (n, i) => "UV", 323 | getArgDims: (n, i) => 2, 324 | getArgFlags: (n, i) => 0, 325 | getArgDefVal: (n, i) => null, 326 | getRVDims: (n) => 4, 327 | getCode: (n) => n.rsrc !== null ? `texture2D(${n.rsrc}, ${_shGenArg(n, 0)})` : `vec4(0,0,0,1)`, 328 | getExpr: (n, l) => `texture(${n.rsrc}, ${nodeGetArgExpr(n, 0, l+3)})` 329 | }, 330 | math: 331 | { 332 | name: "Math",//(node) => node ? nodeResources.op[node.rsrc].name : "Math", 333 | desc: "Perform a math operation", 334 | rsrcType: "op", 335 | rsrcFullRefreshOnChange: true, 336 | defRsrc: "add", 337 | getArgCount: (n) => nodeResources.op[n.rsrc].args, 338 | getArgName: (n, i) => 339 | { 340 | if (n.rsrc == "combine") 341 | return "XYZW"[i] 342 | return "#" + (i + 1) 343 | }, 344 | getArgDims: (n, i) => 345 | { 346 | if (n.rsrc == "cross" || n.rsrc == "unpacknormal") 347 | return 3 348 | if (n.rsrc == "height2normal" || n.rsrc == "combine") 349 | return 1 350 | return "adapt" 351 | }, 352 | getArgFlags: (n, i) => 0, 353 | getArgDefVal: (n, i) => null, 354 | getRVDims: function(n) 355 | { 356 | if (n.rsrc == "length" || 357 | n.rsrc == "distance" || 358 | n.rsrc == "dot") 359 | return 1 360 | if (n.rsrc == "cross" || 361 | n.rsrc == "unpacknormal" || 362 | n.rsrc == "height2normal") 363 | return 3 364 | if (n.rsrc == "combine") 365 | return 4 366 | return nodeCalcDimsFromArgs(n) 367 | }, 368 | getCode: function(n) 369 | { 370 | const args = [] 371 | const argNum = nodeGetArgCount(n) 372 | for (var i = 0; i < argNum; ++i) 373 | args.push(`(${_shGenArg(n, i)})`) 374 | var a = args[0] 375 | var b = args[1] 376 | var c = args[2] 377 | var d = args[3] 378 | function glslOpOrFunc(op, fn) 379 | { 380 | const dims = nodeCalcDimsFromArgs(n) 381 | if (dims == 1) 382 | return `${a} ${op} ${b}` 383 | return `${type2glsl[dims]}(${fn}(${a}, ${b}))` 384 | } 385 | switch (n.rsrc) 386 | { 387 | case "add": return a + " + " + b 388 | case "sub": return a + " - " + b 389 | case "mul": return a + " * " + b 390 | case "div": return a + " / " + b 391 | case "mod": return `mod(${a}, ${b})` 392 | 393 | case "eq": return glslOpOrFunc("==", "equal") 394 | case "neq": return glslOpOrFunc("!=", "notEqual") 395 | case "lt": return glslOpOrFunc("<", "lessThan") 396 | case "gt": return glslOpOrFunc(">", "greaterThan") 397 | case "lte": return glslOpOrFunc("<=", "lessThanEqual") 398 | case "gte": return glslOpOrFunc(">=", "greaterThanEqual") 399 | 400 | case "min": return `min(${a}, ${b})` 401 | case "max": return `max(${a}, ${b})` 402 | case "clamp": return `clamp(${a}, ${b}, ${c})` 403 | case "mix": return `mix(${a}, ${b}, ${c})` 404 | case "step": return `step(${a}, ${b})` 405 | case "smoothstep": return `smoothstep(${a}, ${b}, ${c})` 406 | 407 | case "abs": return `abs(${a})` 408 | case "sign": return `sign(${a})` 409 | case "round": return `floor((${a})+0.5)` 410 | case "floor": return `floor(${a})` 411 | case "ceil": return `ceil(${a})` 412 | case "fract": return `fract(${a})` 413 | 414 | case "normalize": return `normalize(${a})` 415 | case "length": return `length(${a})` 416 | case "distance": return `distance(${a}, ${b})` 417 | case "dot": return `dot(${a}, ${b})` 418 | case "cross": return `cross(${a}, ${b})` 419 | 420 | case "unpacknormal": return `(${a}*2.0-1.0)` 421 | case "height2normal": return `normalize(vec3(dFdx(${a}), dFdy(${a}), 1))` 422 | case "combine": return `vec4(${a}, ${b}, ${c}, ${d})` 423 | 424 | default: throw `unknown op: ${n.rsrc}` 425 | } 426 | }, 427 | getExpr: function(n, l) 428 | { 429 | const args = [] 430 | const argNum = nodeGetArgCount(n) 431 | for (var i = 0; i < argNum; ++i) 432 | args.push(`(${nodeGetArgExpr(n, i, l+1)})`) 433 | var a = args[0] 434 | var b = args[1] 435 | var c = args[2] 436 | var d = args[3] 437 | switch (n.rsrc) 438 | { 439 | case "add": return a + " + " + b 440 | case "sub": return a + " - " + b 441 | case "mul": return a + " * " + b 442 | case "div": return a + " / " + b 443 | case "mod": return `mod(${a}, ${b})` 444 | 445 | case "eq": return a + " == " + b 446 | case "neq": return a + " != " + b 447 | case "lt": return a + " < " + b 448 | case "gt": return a + " > " + b 449 | case "lte": return a + " <= " + b 450 | case "gte": return a + " >= " + b 451 | 452 | case "min": return `min(${a}, ${b})` 453 | case "max": return `max(${a}, ${b})` 454 | case "clamp": return `clamp(${a}, ${b}, ${c})` 455 | case "mix": return `mix(${a}, ${b}, ${c})` 456 | case "step": return `step(${a}, ${b})` 457 | case "smoothstep": return `smoothstep(${a}, ${b}, ${c})` 458 | 459 | case "abs": return `abs(${a})` 460 | case "sign": return `sign(${a})` 461 | case "round": return `round(${a})` 462 | case "floor": return `floor(${a})` 463 | case "ceil": return `ceil(${a})` 464 | case "fract": return `fract(${a})` 465 | 466 | case "normalize": return `normalize(${a})` 467 | case "length": return `length(${a})` 468 | case "distance": return `distance(${a}, ${b})` 469 | case "dot": return `dot(${a}, ${b})` 470 | case "cross": return `cross(${a}, ${b})` 471 | 472 | case "unpacknormal": return `unpacknormal(${a})` 473 | case "height2normal": return `height2normal(${a})` 474 | case "combine": return `combine(${a}, ${b}, ${c}, ${d})` 475 | 476 | default: throw `unknown op: ${n.rsrc}` 477 | } 478 | }, 479 | }, 480 | } 481 | 482 | 483 | -------------------------------------------------------------------------------- /EditorDragDrop.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function dist2d(x1, y1, x2, y2) 5 | { 6 | const xd = x2 - x1 7 | const yd = y2 - y1 8 | return Math.sqrt(xd * xd + yd * yd) 9 | } 10 | 11 | var cursorClientPos = { x: 0, y: 0 } 12 | 13 | var queuedRedrawDragCanvas = false 14 | function redrawDragCanvas() 15 | { 16 | var nodeCols = funcGetCurNodeCols() 17 | // console.log("dragredraw") 18 | queuedRedrawDragCanvas = false 19 | var editArea = document.getElementById("editArea") 20 | var canvas = document.getElementById("dragOverlay") 21 | var parent = canvas.parentElement 22 | canvas.width = parent.offsetWidth 23 | canvas.height = parent.offsetHeight 24 | var ctx = canvas.getContext("2d") 25 | ctx.clearRect(0, 0, canvas.width, canvas.height) 26 | ctx.strokeStyle = "rgba(250,240,230,0.5)" 27 | ctx.lineWidth = 2 28 | ctx.lineCap = "square" 29 | 30 | var eaCR = editArea.getBoundingClientRect() 31 | 32 | var drag = store.get("editor/drag") 33 | if (!drag) 34 | return 35 | 36 | if (drag.nodeID !== null) 37 | { 38 | if (typeof drag.argNum === "number") 39 | { 40 | ctx.strokeStyle = "rgba(200,100,0,0.8)" 41 | var inEl = document.getElementById(`node_${drag.nodeID}_input_${drag.argNum}`) 42 | var pinEl = inEl.querySelector(".pin") 43 | var pinCR = pinEl.getBoundingClientRect() 44 | ctx.beginPath() 45 | var clientCenterX = pinCR.left + pinCR.width / 2 46 | var clientCenterY = pinCR.top + pinCR.height / 2 47 | ctx.arc(clientCenterX - eaCR.left, clientCenterY - eaCR.top, pinCR.width, 0, Math.PI * 2) 48 | var endPos = cursorClientPos 49 | if (drag.target) 50 | { 51 | var dtID = nodeCols[drag.target.x][drag.target.y] 52 | var nodeContEl = document.getElementById(`node_${dtID}`) 53 | var nodeContCR = nodeContEl.getBoundingClientRect() 54 | if (dtID != drag.nodeID && !nodeReadsFrom(dtID, drag.nodeID)) 55 | { 56 | //ctx.strokeStyle = "rgba(200,200,200,0.2)" 57 | ctx.rect(nodeContCR.left - eaCR.left, nodeContCR.top - eaCR.top, nodeContCR.width, nodeContCR.height) 58 | endPos = { x: nodeContCR.right, y: nodeContCR.top } 59 | } 60 | } 61 | if (endPos !== null && drag.type == "link" && dist2d(endPos.x, endPos.y, clientCenterX, clientCenterY) > pinCR.width) 62 | { 63 | ctx.strokeStyle = "rgba(200,100,0,0.8)" 64 | ctx.moveTo(pinCR.left - eaCR.left - pinCR.width / 2, pinCR.top - eaCR.top + pinCR.height / 2) 65 | ctx.bezierCurveTo( 66 | pinCR.left - eaCR.left - pinCR.width / 2 - 32, pinCR.top - eaCR.top + pinCR.height / 2, 67 | endPos.x - eaCR.left + 32, endPos.y - eaCR.top, 68 | endPos.x - eaCR.left, endPos.y - eaCR.top) 69 | } 70 | ctx.stroke() 71 | } 72 | else 73 | { 74 | var nodeEl = document.getElementById(`node_${drag.nodeID}`) 75 | var nodeCR = nodeEl.getBoundingClientRect() 76 | ctx.beginPath() 77 | ctx.rect(nodeCR.left - eaCR.left, nodeCR.top - eaCR.top, nodeCR.width, nodeCR.height) 78 | ctx.stroke() 79 | } 80 | } 81 | 82 | if (drag.type == "node" && drag.target) 83 | { 84 | ctx.strokeStyle = "rgba(250,240,230,0.4)" 85 | ctx.lineWidth = 8 86 | if (drag.target.x !== null) 87 | { 88 | if (drag.target.y !== null) 89 | { 90 | var dtID = nodeCols[drag.target.x][drag.target.y] 91 | var nodeContEl = document.getElementById(`node_cont_${dtID}`) 92 | var nodeContCR = nodeContEl.getBoundingClientRect() 93 | ctx.beginPath() 94 | switch (drag.target.edge) 95 | { 96 | case 0: 97 | ctx.moveTo(nodeContCR.left - eaCR.left, nodeContCR.top - eaCR.top) 98 | ctx.lineTo(nodeContCR.right - eaCR.left, nodeContCR.top - eaCR.top) 99 | break 100 | case 1: 101 | ctx.moveTo(nodeContCR.right - eaCR.left, nodeContCR.top - eaCR.top) 102 | ctx.lineTo(nodeContCR.right - eaCR.left, nodeContCR.bottom - eaCR.top) 103 | break 104 | case 2: 105 | ctx.moveTo(nodeContCR.left - eaCR.left, nodeContCR.bottom - eaCR.top) 106 | ctx.lineTo(nodeContCR.right - eaCR.left, nodeContCR.bottom - eaCR.top) 107 | break 108 | case 3: 109 | ctx.moveTo(nodeContCR.left - eaCR.left, nodeContCR.top - eaCR.top) 110 | ctx.lineTo(nodeContCR.left - eaCR.left, nodeContCR.bottom - eaCR.top) 111 | break 112 | } 113 | ctx.stroke() 114 | } 115 | else 116 | { 117 | var colEl = document.getElementById(`node_col_${drag.target.x}`) 118 | var colCR = colEl.getBoundingClientRect() 119 | ctx.beginPath() 120 | switch (drag.target.edge) 121 | { 122 | case 0: 123 | ctx.moveTo(colCR.left - eaCR.left, colCR.top - eaCR.top) 124 | ctx.lineTo(colCR.right - eaCR.left, colCR.top - eaCR.top) 125 | break 126 | case 1: 127 | ctx.moveTo(colCR.right - eaCR.left, colCR.top - eaCR.top) 128 | ctx.lineTo(colCR.right - eaCR.left, colCR.bottom - eaCR.top) 129 | break 130 | case 2: 131 | ctx.moveTo(colCR.left - eaCR.left, colCR.bottom - eaCR.top) 132 | ctx.lineTo(colCR.right - eaCR.left, colCR.bottom - eaCR.top) 133 | break 134 | case 3: 135 | ctx.moveTo(colCR.left - eaCR.left, colCR.top - eaCR.top) 136 | ctx.lineTo(colCR.left - eaCR.left, colCR.bottom - eaCR.top) 137 | break 138 | } 139 | ctx.stroke() 140 | } 141 | } 142 | } 143 | } 144 | 145 | function queueRedrawDragCanvas() 146 | { 147 | if (!queuedRedrawDragCanvas) 148 | { 149 | requestAnimationFrame(redrawDragCanvas) 150 | queuedRedrawDragCanvas = true 151 | } 152 | } 153 | 154 | function dist(a, b) 155 | { 156 | return Math.abs(a - b) 157 | } 158 | 159 | function Drag_onPointerMove(e) 160 | { 161 | var drag = store.get("editor/drag") 162 | if (!drag) 163 | return 164 | var nodeCols = funcGetCurNodeCols() 165 | cursorClientPos.x = e.clientX 166 | cursorClientPos.y = e.clientY 167 | if (drag.type == "node") 168 | { 169 | for (var el = e.target; el; el = el.parentElement) 170 | { 171 | if (el.classList) 172 | { 173 | if (el.classList.contains("nodeCont")) 174 | { 175 | // pointer on node container, set as target and calculate closest edge 176 | var cr = el.getBoundingClientRect() 177 | var node = nodeMap[el.dataset.id] 178 | var closestEdge = 0 179 | var dist2Edge = dist(e.clientY, cr.top) 180 | 181 | var dist2Right = dist(e.clientX, cr.right) 182 | if (dist2Edge > dist2Right) 183 | { 184 | dist2Edge = dist2Right 185 | closestEdge = 1 186 | } 187 | var dist2Bottom = dist(e.clientY, cr.bottom) 188 | if (dist2Edge > dist2Bottom) 189 | { 190 | dist2Edge = dist2Bottom 191 | closestEdge = 2 192 | } 193 | var dist2Left = dist(e.clientX, cr.left) 194 | if (dist2Edge > dist2Left) 195 | { 196 | dist2Edge = dist2Left 197 | closestEdge = 3 198 | } 199 | 200 | store.set("editor/drag/target", { x: node.x, y: closestEdge == 1 || closestEdge == 3 ? null : node.y, edge: closestEdge }) 201 | break 202 | } 203 | if (el.classList.contains("col")) 204 | { 205 | // pointer on node column, set as target and calculate closest edge (top excluded) 206 | var cr = el.getBoundingClientRect() 207 | var x = el.dataset.col 208 | var closestEdge = 2 209 | var dist2Edge = 8 210 | 211 | var dist2Right = dist(e.clientX, cr.right) 212 | if (dist2Edge > dist2Right) 213 | { 214 | dist2Edge = dist2Right 215 | closestEdge = 1 216 | } 217 | var dist2Left = dist(e.clientX, cr.left) 218 | if (dist2Edge > dist2Left) 219 | { 220 | dist2Edge = dist2Left 221 | closestEdge = 3 222 | } 223 | 224 | store.set("editor/drag/target", { x: x, y: closestEdge == 2 ? nodeCols[x].length - 1 : null, edge: closestEdge }) 225 | break 226 | } 227 | if (el.classList.contains("area")) 228 | { 229 | store.set("editor/drag/target", { x: nodeCols.length - 1, y: null, edge: 3 }) 230 | break 231 | } 232 | } 233 | if (el == document.body) 234 | { 235 | // nothing was found 236 | store.set("editor/drag/target", null) 237 | break 238 | } 239 | } 240 | 241 | queueRedrawDragCanvas() 242 | } 243 | else if (drag.type == "link" || drag.type == "expr") 244 | { 245 | for (var el = e.target; el; el = el.parentElement) 246 | { 247 | if (el.classList) 248 | { 249 | if (el.classList.contains("node")) 250 | { 251 | var node = nodeMap[el.dataset.id] 252 | if (el.dataset.id <= window.nodeIDGen) 253 | { 254 | store.set("editor/drag/target", { x: node.x, y: node.y, id: node.id }) 255 | break 256 | } 257 | } 258 | } 259 | if (el == document.body) 260 | { 261 | // nothing was found 262 | store.set("editor/drag/target", null) 263 | break 264 | } 265 | } 266 | queueRedrawDragCanvas() 267 | } 268 | } 269 | 270 | function Drag_onPointerDown(e) 271 | { 272 | var drag = store.get("editor/drag") 273 | if (!drag) 274 | return 275 | if (drag.type == "expr") 276 | { 277 | if (drag.target !== null && e.button == 0) 278 | { 279 | ExpressionEdit_InsertText("#" + drag.target.id) 280 | e.preventDefault() 281 | } 282 | } 283 | } 284 | 285 | function Drag_onPointerUp(e) 286 | { 287 | var drag = store.get("editor/drag") 288 | if (drag) 289 | { 290 | var nodeCols = funcGetCurNodeCols() 291 | if (drag.type == "node") 292 | { 293 | var dt = drag && drag.type == "node" ? drag.target : null 294 | mainChk: if (dt !== null) 295 | { 296 | var node = nodeMap[drag.nodeID] 297 | if (dt.edge == 1 || dt.edge == 3) 298 | { 299 | // insert new column 300 | if (dt.edge == 3) 301 | dt.x++ // left edge - next position 302 | if (dt.x > node.x && nodeCols[node.x].length == 1) 303 | dt.x-- // previous node's column will be removed by removing node 304 | nodeRemoveFromCols(node) 305 | nodeColsInsertCol(dt.x, [node]) 306 | } 307 | else 308 | { 309 | // insert into existing column [x] 310 | if (dt.edge == 2) 311 | dt.y++ // bottom edge - next position 312 | if (dt.x == node.x && dt.y > node.y) 313 | dt.y-- // node is in the same column, behind target position, its removal will shift target position 314 | if (dt.x == node.x && nodeCols[node.x].length == 1) 315 | break mainChk 316 | if (dt.x >= node.x && nodeCols[node.x].length == 1) 317 | dt.x-- // previous node's column will be removed by removing node 318 | nodeRemoveFromCols(node) 319 | nodeInsertIntoCols(node, dt.x, dt.y) 320 | } 321 | store.set("editor/nodeBlinkID", drag.nodeID) 322 | } 323 | } 324 | else if (drag.type == "link") 325 | { 326 | if (drag.target) 327 | { 328 | var dtID = nodeCols[drag.target.x][drag.target.y] 329 | if (dtID != drag.nodeID && !nodeReadsFrom(dtID, drag.nodeID)) 330 | { 331 | nodeArgSetLink(drag.nodeID, drag.argNum, dtID) 332 | } 333 | } 334 | } 335 | else if (drag.type == "expr") 336 | { 337 | // do not disable this state here 338 | return 339 | } 340 | 341 | store.set("editor/drag", null) 342 | document.getElementById("shaderEditor").classList.remove("disableNodeControls") 343 | queueRedrawDragCanvas() 344 | } 345 | } 346 | 347 | function Drag_StartNodeDrag(id) 348 | { 349 | store.set("editor/drag", { type: "node", nodeID: id, target: null }) 350 | document.getElementById("shaderEditor").classList.add("disableNodeControls") 351 | queueRedrawDragCanvas() 352 | } 353 | 354 | function Drag_StartLinkDrag(nodeID, argNum, e) 355 | { 356 | cursorClientPos.x = e.clientX 357 | cursorClientPos.y = e.clientY 358 | store.set("editor/drag", { type: "link", nodeID: nodeID, argNum: argNum, target: null }) 359 | if (e.type != "pointerdown") 360 | document.getElementById("shaderEditor").classList.add("disableNodeControls") 361 | queueRedrawDragCanvas() 362 | } 363 | 364 | function Drag_StartExprClick(nodeID, argNum) 365 | { 366 | store.set("editor/drag", { type: "expr", nodeID: nodeID, argNum: argNum, target: null }) 367 | queueRedrawDragCanvas() 368 | } 369 | 370 | function Drag_StopExprClick() 371 | { 372 | store.set("editor/drag", null) 373 | queueRedrawDragCanvas() 374 | } 375 | 376 | const NodeDragOverlay = component 377 | ({ 378 | render() 379 | { 380 | evoid("canvas", { "class": "overlay eaBlurred", id: "dragOverlay" }) 381 | }, 382 | }) 383 | 384 | 385 | -------------------------------------------------------------------------------- /ExpressionParser.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function numToFloatStr(v, keepDotZero) 5 | { 6 | v = (+v).toFixed(6) 7 | v = v.replace(/0+$/, "") 8 | v = v.replace(/\.$/, keepDotZero ? ".0" : "") 9 | return v 10 | } 11 | 12 | const cc0 = "0".charCodeAt(0) 13 | const cc9 = "9".charCodeAt(0) 14 | const cca = "a".charCodeAt(0) 15 | const ccz = "z".charCodeAt(0) 16 | const ccA = "A".charCodeAt(0) 17 | const ccZ = "Z".charCodeAt(0) 18 | function isDigit(x, at) 19 | { 20 | const cc = x.charCodeAt(at || 0) 21 | return cc >= cc0 && cc <= cc9 22 | } 23 | function isAlpha(x, at) 24 | { 25 | const cc = x.charCodeAt(at || 0) 26 | return (cc >= cca && cc <= ccz) || (cc >= ccA && cc <= ccZ) 27 | } 28 | 29 | function Token(type, value, off, len) 30 | { 31 | this.type = type 32 | this.value = value 33 | this.off = off 34 | this.len = len 35 | } 36 | const TokenType = Object.freeze 37 | ({ 38 | IDENT: "IDENT", 39 | NUMBER: "NUMBER", 40 | LPAREN: "LPAREN", 41 | RPAREN: "RPAREN", 42 | COMMA: "COMMA", 43 | NODE_REF: "NODE_REF", // # 44 | OP_MEMBER: "OP_MEMBER", // . 45 | OP_ADD: "OP_ADD", 46 | OP_SUB: "OP_SUB", 47 | OP_MUL: "OP_MUL", 48 | OP_DIV: "OP_DIV", 49 | }) 50 | const TokenTypeNames = {} 51 | for (var k in TokenType) 52 | TokenTypeNames[TokenType[k]] = k 53 | const CHAR_TOKEN_MAP = 54 | { 55 | "(": TokenType.LPAREN, 56 | ")": TokenType.RPAREN, 57 | ",": TokenType.COMMA, 58 | ".": TokenType.OP_MEMBER, 59 | "+": TokenType.OP_ADD, 60 | "-": TokenType.OP_SUB, 61 | "*": TokenType.OP_MUL, 62 | "/": TokenType.OP_DIV, 63 | } 64 | const TOKEN_CHAR_MAP = {} 65 | for (var ch in CHAR_TOKEN_MAP) 66 | TokenTypeNames[CHAR_TOKEN_MAP[ch]] = ch 67 | function tokensToString(tokens, from, to) 68 | { 69 | const strs = [] 70 | for (var i = from; i < to; ++i) 71 | { 72 | if (tokens[i].type == TokenType.IDENT || tokens[i].type == TokenType.NUMBER) 73 | strs.push(tokens[i].value) 74 | else 75 | strs.push(TOKEN_CHAR_MAP[tokens[i].type]) 76 | } 77 | return strs.join(" ") 78 | } 79 | function tokenize(str) 80 | { 81 | const out = [] 82 | var i = 0 83 | while (i < str.length) 84 | { 85 | var sctt = CHAR_TOKEN_MAP[str[i]] 86 | if (typeof sctt !== "undefined") 87 | { 88 | // single character token 89 | out.push(new Token(sctt, str[i], i, 1)) 90 | i++ 91 | continue 92 | } 93 | 94 | if (str[i] == " " || str[i] == "\t" || str[i] == "\r" || str[i] == "\n") 95 | { 96 | // whitespace 97 | i++ 98 | continue 99 | } 100 | 101 | if (isDigit(str, i)) 102 | { 103 | // number 104 | var start = i 105 | for ( ; i < str.length && isDigit(str, i); ++i); 106 | if (i < str.length && str[i] == ".") 107 | { 108 | i++ 109 | for ( ; i < str.length && isDigit(str, i); ++i); 110 | } 111 | if (i < str.length && (str[i] == "e" || str[i] == "E")) 112 | { 113 | i++ 114 | if (i >= str.length || (str[i] != "+" && str[i] != "-")) 115 | throw "expected + or - after e/E" 116 | i++ 117 | var before = i 118 | for ( ; i < str.length && isDigit(str, i); ++i); 119 | if (i == before) 120 | throw "expected number after e/E and +/-" 121 | } 122 | out.push(new Token(TokenType.NUMBER, parseFloat(str.substring(start, i)), start, i - start)) 123 | continue 124 | } 125 | 126 | if (str[i] == "#") 127 | { 128 | // node reference 129 | i++ 130 | var start = i 131 | for ( ; i < str.length && isDigit(str, i); ++i); 132 | if (start == i) 133 | throw "expected number after '#'" 134 | out.push(new Token(TokenType.NODE_REF, parseInt(str.substring(start, i), 10), start, i - start)) 135 | continue 136 | } 137 | 138 | if (str[i] == "_" || isAlpha(str, i)) 139 | { 140 | // identifier 141 | var start = i 142 | for ( ; i < str.length; ++i) 143 | { 144 | if (str[i] == "_") 145 | continue 146 | if (isDigit(str, i)) 147 | continue 148 | if (isAlpha(str, i)) 149 | continue 150 | break 151 | } 152 | out.push(new Token(TokenType.IDENT, str.substring(start, i), start, i - start)) 153 | continue 154 | } 155 | 156 | throw "unrecognized character: '" + str[i] + "'" 157 | } 158 | return out 159 | } 160 | 161 | const SPLITSCORE_RTLASSOC = 0x80 162 | 163 | function tokenIsExprPreceding(tt) 164 | { 165 | switch (tt) 166 | { 167 | case TokenType.IDENT: 168 | case TokenType.RPAREN: 169 | case TokenType.NUMBER: 170 | case TokenType.NODE_REF: 171 | return true; 172 | default: 173 | return false; 174 | } 175 | } 176 | 177 | function getSplitScore(tokens, pos, start) 178 | { 179 | // http://en.cppreference.com/w/c/language/operator_precedence 180 | 181 | var tt = tokens[pos].type; 182 | 183 | // if (tt == STT_OP_Ternary) return 13 | SPLITSCORE_RTLASSOC; 184 | 185 | if (start < pos && tokenIsExprPreceding(tokens[pos - 1].type)) 186 | { 187 | // if (tt == STT_OP_Eq || tt == STT_OP_NEq) return 7; 188 | // if (tt == STT_OP_Less || tt == STT_OP_LEq || tt == STT_OP_Greater || tt == STT_OP_GEq) 189 | // return 6; 190 | 191 | if (tt == TokenType.OP_ADD || tt == TokenType.OP_SUB) return 4; 192 | if (tt == TokenType.OP_MUL || tt == TokenType.OP_DIV) return 3; 193 | } 194 | 195 | // unary operators 196 | if (pos == start) 197 | { 198 | if (tt == TokenType.OP_ADD || tt == TokenType.OP_SUB) 199 | return 2 | SPLITSCORE_RTLASSOC; 200 | } 201 | 202 | if (start < pos) 203 | { 204 | // function call 205 | if (tt == TokenType.LPAREN) return 1; 206 | } 207 | 208 | // member operator 209 | if (tt == TokenType.OP_MEMBER) return 1; 210 | 211 | return -1; 212 | } 213 | 214 | function findBestSplit(tokens, curPos, to, endTT) 215 | { 216 | var bestSplit = tokens.length 217 | var bestScore = 0 218 | var parenCount = 0 219 | var startPos = curPos 220 | while (curPos < to && ((tokens[curPos].type != TokenType.COMMA && tokens[curPos].type != endTT) || parenCount > 0)) 221 | { 222 | if (parenCount == 0) 223 | { 224 | var curScore = getSplitScore(tokens, curPos, startPos) 225 | var rtl = (curScore & SPLITSCORE_RTLASSOC) != 0 226 | curScore &= ~SPLITSCORE_RTLASSOC 227 | if (curScore - (rtl ? 1 : 0) >= bestScore) // ltr: >=, rtl: > (-1) 228 | { 229 | bestScore = curScore 230 | bestSplit = curPos 231 | } 232 | } 233 | 234 | if (tokens[curPos].type == TokenType.LPAREN) 235 | parenCount++ 236 | else if (tokens[curPos].type == TokenType.RPAREN) 237 | { 238 | if (parenCount == 0) 239 | { 240 | throw "brace mismatch (too many endings)" 241 | } 242 | parenCount-- 243 | } 244 | 245 | curPos++ 246 | } 247 | 248 | if (parenCount) 249 | { 250 | throw "brace mismatch (too many beginnings)" 251 | } 252 | 253 | return { at: curPos, bestSplit: bestSplit } 254 | } 255 | 256 | function IdentNode(name) 257 | { 258 | this.name = name 259 | } 260 | 261 | function NodeRefNode(id) 262 | { 263 | this.id = id 264 | } 265 | 266 | function NumberNode(num) 267 | { 268 | this.num = num 269 | } 270 | 271 | function PropertyNode(src, prop) 272 | { 273 | this.src = src 274 | this.prop = prop 275 | } 276 | 277 | function FuncNode(func, args) 278 | { 279 | this.func = func 280 | this.args = args 281 | } 282 | 283 | function parseASTNode(tokens, from, to) 284 | { 285 | if (from >= to) 286 | throw "empty expression" 287 | var split = findBestSplit(tokens, from, to, 0) 288 | 289 | if (split.bestSplit == tokens.length) 290 | { 291 | if (split.at - from == 1) 292 | { 293 | // one item long expression 294 | if (tokens[from].type == TokenType.IDENT) 295 | { 296 | return new IdentNode(tokens[from].value) 297 | } 298 | else if (tokens[from].type == TokenType.NODE_REF) 299 | { 300 | return new NodeRefNode(tokens[from].value) 301 | } 302 | else if (tokens[from].type == TokenType.NUMBER) 303 | { 304 | return new NumberNode(tokens[from].value) 305 | } 306 | } 307 | if (tokens[from].type == TokenType.LPAREN && tokens[split.at - 1].type == TokenType.RPAREN) 308 | { 309 | return parseASTNode(tokens, from + 1, split.at - 1) 310 | } 311 | throw "unexpected subexpression: " + tokensToString(tokens, from, split.at) //+ "|" + split.at + "|" + from 312 | } 313 | 314 | var ttSplit = tokens[split.bestSplit].type; 315 | if (from < split.bestSplit && ttSplit == TokenType.LPAREN) 316 | { 317 | if (from + 1 == split.bestSplit && tokens[from].type == TokenType.IDENT && tokens[split.at - 1].type == TokenType.RPAREN) 318 | { 319 | // function call 320 | const args = [] 321 | const funcName = tokens[from].value 322 | from += 2 // right after first LPAREN 323 | to = split.at - 1 // until ending RPAREN 324 | while (from < to && tokens[from].type != TokenType.RPAREN) 325 | { 326 | // find next comma 327 | var parenCount = 0 328 | var nc = from 329 | while (nc < to) 330 | { 331 | if (tokens[nc].type == TokenType.COMMA) 332 | { 333 | if (parenCount == 0) 334 | break 335 | } 336 | else if (tokens[nc].type == TokenType.LPAREN) 337 | parenCount++ 338 | else if (tokens[nc].type == TokenType.RPAREN) 339 | { 340 | if (parenCount == 0) 341 | { 342 | throw "brace mismatch (too many endings)" 343 | } 344 | parenCount-- 345 | } 346 | nc++ 347 | } 348 | if (parenCount) 349 | { 350 | throw "brace mismatch (too many beginnings)" 351 | } 352 | 353 | args.push(parseASTNode(tokens, from, nc)) 354 | from = nc 355 | 356 | if (tokens[from].type != TokenType.RPAREN) 357 | { 358 | if (tokens[from].type != TokenType.COMMA) 359 | throw "expected ','" 360 | if (++from >= to) 361 | throw "unexpected end of subexpression" 362 | } 363 | } 364 | return new FuncNode(funcName, args) 365 | } 366 | else throw "expected function call" 367 | } 368 | else if (from == split.bestSplit && ttSplit == TokenType.LPAREN && tokens[split.at - 1].type == TokenType.RPAREN) 369 | { 370 | return parseASTNode(tokens, from + 1, split.at - 1) 371 | } 372 | /* TODO 373 | else if (from < split.bestSplit && ttSplit == TokenType.TERNARY) 374 | { 375 | }*/ 376 | else 377 | { 378 | if (split.bestSplit == from) 379 | { 380 | if (ttSplit == TokenType.OP_ADD) 381 | { 382 | return parseASTNode(tokens, split.bestSplit + 1, to) 383 | } 384 | if (ttSplit == TokenType.OP_SUB) 385 | { 386 | return new FuncNode("u-", 387 | [ 388 | parseASTNode(tokens, split.bestSplit + 1, to), 389 | ]) 390 | } 391 | } 392 | 393 | if (ttSplit == TokenType.OP_MEMBER) 394 | { 395 | if (split.at - split.bestSplit == 2 && tokens[split.bestSplit + 1].type == TokenType.IDENT) 396 | { 397 | return new PropertyNode(parseASTNode(tokens, from, split.bestSplit), tokens[split.bestSplit + 1].value) 398 | } 399 | throw "expected identifier after '.'" 400 | } 401 | else 402 | { 403 | var op = null 404 | switch (ttSplit) 405 | { 406 | case TokenType.OP_ADD: op = "b+"; break 407 | case TokenType.OP_SUB: op = "b-"; break 408 | case TokenType.OP_MUL: op = "b*"; break 409 | case TokenType.OP_DIV: op = "b/"; break 410 | } 411 | if (op !== null) 412 | { 413 | return new FuncNode(op, 414 | [ 415 | parseASTNode(tokens, from, split.bestSplit), 416 | parseASTNode(tokens, split.bestSplit + 1, to), 417 | ]) 418 | } 419 | } 420 | } 421 | throw "unhandled case" 422 | } 423 | 424 | function parseAST(str) 425 | { 426 | tokens = tokenize(String(str)) 427 | return parseASTNode(tokens, 0, tokens.length) 428 | } 429 | 430 | 431 | 432 | function evaluateNode(node) 433 | { 434 | if (node instanceof NumberNode) 435 | return node.num 436 | if (node instanceof IdentNode) 437 | { 438 | if (node.name == "pi") 439 | return Math.PI 440 | if (node.name == "e") 441 | return Math.E 442 | throw "unknown identifier" 443 | } 444 | if (node instanceof FuncNode) 445 | { 446 | switch (node.func) 447 | { 448 | case "u-": return -evaluateNode(node.args[0]) 449 | case "b+": return evaluateNode(node.args[0]) + evaluateNode(node.args[1]) 450 | case "b-": return evaluateNode(node.args[0]) - evaluateNode(node.args[1]) 451 | case "b*": return evaluateNode(node.args[0]) * evaluateNode(node.args[1]) 452 | case "b/": return evaluateNode(node.args[0]) / evaluateNode(node.args[1]) 453 | } 454 | } 455 | throw "unhandled case" 456 | } 457 | 458 | function calculate(str) 459 | { 460 | try 461 | { 462 | var node = parseAST(str) 463 | var v = evaluateNode(node) 464 | return isFinite(v) ? numToFloatStr(v) : "0" 465 | } 466 | catch (ex) 467 | { 468 | console.log(ex) 469 | } 470 | return 0 471 | } 472 | 473 | 474 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Arvīds Kokins 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /NodeGraph.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | var dims2type = { "1": "float", "2": "float2", "3": "float3", "4": "float4" } 5 | var type2glsl = { "1": "float", "2": "vec2", "3": "vec3", "4": "vec4" } 6 | var labelsXYZW = ["X", "Y", "Z", "W"] 7 | var labelsRGBA = ["R", "G", "B", "A"] 8 | var labelsHSVA = ["H", "S", "V", "A"] 9 | 10 | var nodeRsrcNames = { uniform: "Uniform", varying: "Varying", sampler2D: "2D Texture" } 11 | 12 | window.ARG_NONUMEDIT = 0x01 13 | 14 | function nodeResourceGenerateDesc(type, name) 15 | { 16 | var rsrcInfo = nodeResources[type][name] 17 | if (rsrcInfo.desc) 18 | return rsrcInfo.desc 19 | if (type == "uniform" || type == "varying") 20 | return dims2type[rsrcInfo.dims] 21 | if (type == "variable") 22 | return dims2type[rsrcInfo.dims] + " " + rsrcInfo.type 23 | return "" 24 | } 25 | 26 | 27 | 28 | function rgbFromHSV(hsv) 29 | { 30 | const cor = [hsv[0] % 1, hsv[1], hsv[2]] 31 | const hi = Math.floor(Math.floor(cor[0] * 6) % 6) 32 | const f = (cor[0] * 6) - Math.floor(cor[0] * 6) 33 | const p = cor[2] * (1 - cor[1]) 34 | const q = cor[2] * (1 - (f * cor[1])) 35 | const t = cor[2] * (1 - ((1 - f) * cor[1])) 36 | switch (hi) 37 | { 38 | case 0: return [cor[2], t, p] 39 | case 1: return [q, cor[2], p] 40 | case 2: return [p, cor[2], t] 41 | case 3: return [p, q, cor[2]] 42 | case 4: return [t, p, cor[2]] 43 | case 5: return [cor[2], p, q] 44 | } 45 | return [0, 0, 0] 46 | } 47 | function minFromRGB(rgb) { return Math.min(rgb[0], Math.min(rgb[1], rgb[2])) } 48 | function maxFromRGB(rgb) { return Math.max(rgb[0], Math.max(rgb[1], rgb[2])) } 49 | function hueFromRGB(rgb) 50 | { 51 | const M = maxFromRGB(rgb) 52 | const m = minFromRGB(rgb) 53 | const C = M - m 54 | if (C == 0) 55 | return 0 56 | var h = 0 57 | if (M == rgb[0]) 58 | h = ((rgb[1] - rgb[2]) / C) % 6 59 | else if (M == rgb[1]) 60 | h = (rgb[2] - rgb[0]) / C + 2 61 | else if (M == rgb[2]) 62 | h = (rgb[0] - rgb[1]) / C + 4 63 | return h / 6 64 | } 65 | function satFromRGB(rgb) 66 | { 67 | const M = maxFromRGB(rgb) 68 | const m = minFromRGB(rgb) 69 | return M == 0 ? 0 : (M - m) / M 70 | } 71 | function valFromRGB(rgb) 72 | { 73 | return maxFromRGB(rgb) 74 | } 75 | function hsvFromRGB(rgb) { return [hueFromRGB(rgb), satFromRGB(rgb), valFromRGB(rgb)] } 76 | 77 | 78 | 79 | var defArg = [0,0,0,1] 80 | function constructArgValue(val) 81 | { 82 | if (val) 83 | { 84 | const out = [] 85 | for (var i = 0; i < 4; ++i) 86 | out.push(i < val.length ? val[i] : defArg[i]) 87 | return out 88 | } 89 | return defArg.slice(0, 4) 90 | } 91 | 92 | 93 | 94 | var nodeMap = {} 95 | store.set("nodeMap", nodeMap) 96 | store.set("functions", 97 | { 98 | main: 99 | { 100 | retDims: 4, 101 | nodeCols: [] 102 | }, 103 | }) 104 | 105 | function funcCreate(name, dims, args) 106 | { 107 | var outNode = nodeConstruct("output") 108 | outNode.x = 0 109 | outNode.y = 0 110 | outNode.func = name 111 | var func = 112 | { 113 | retDims: dims, 114 | args: args.map((x) => ({ dims: x.dims, name: x.name })), 115 | nodeCols: [[outNode.id]], 116 | outputNode: outNode.id, 117 | } 118 | funcGetMap()[name] = func 119 | } 120 | 121 | function funcMove(name, oldName, dims, args) 122 | { 123 | var nfm = funcGetMap() 124 | var func = 125 | { 126 | retDims: dims, 127 | args: args.map((x) => ({ dims: x.dims, name: x.name })), 128 | nodeCols: nfm[oldName].nodeCols, 129 | outputNode: nfm[oldName].outputNode, 130 | } 131 | delete nfm[oldName] 132 | nfm[name] = func 133 | 134 | // change output function editor if fixed 135 | nodeArgAdjustFixedEditors(func.outputNode, 0, dims) 136 | // TODO remap args of function nodes 137 | } 138 | 139 | function funcNameValidate(name) 140 | { 141 | return name.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) 142 | } 143 | 144 | function funcSetCur(name) 145 | { 146 | store.set("editor/curFunc", name) 147 | storeUpdateCurFunc() 148 | } 149 | 150 | function funcGetCurName() 151 | { 152 | return store.get("editor/curFunc") 153 | } 154 | 155 | function funcGetData(name) 156 | { 157 | return store.get("functions/" + name) 158 | } 159 | 160 | function funcGetDataAll(name) 161 | { 162 | return funcGetData(name) || nodeResources.func[name] 163 | } 164 | 165 | function funcGetCurData() 166 | { 167 | return store.get("functions/" + funcGetCurName()) 168 | } 169 | 170 | function funcGetArgDimsCache(name) 171 | { 172 | const fnData = funcGetData(name) 173 | var cache = fnData.cachedArgDimsByName 174 | if (!cache) 175 | { 176 | fnData.cachedArgDimsByName = cache = {} 177 | for (var i = 0; i < fnData.args.length; ++i) 178 | cache[fnData.args[i].name] = fnData.args[i].dims 179 | } 180 | return cache 181 | } 182 | 183 | function storeUpdateCurFuncNodes() 184 | { 185 | store.update("nodeMap") 186 | //store.update(`functions/${funcGetCurName()}/nodeCols`) 187 | store.update("functions") 188 | } 189 | 190 | function storeUpdateCurFunc() 191 | { 192 | store.update("editor/globalUpdateTrigger") 193 | } 194 | 195 | function funcGetMap() 196 | { 197 | return store.get("functions") 198 | } 199 | 200 | function funcGetCurNodeCols() 201 | { 202 | return store.get(`functions/${funcGetCurName()}/nodeCols`) 203 | } 204 | 205 | 206 | 207 | function _nodeResolve(node) 208 | { 209 | return (typeof node === "string" || typeof node === "number") ? nodeMap[node] : node 210 | } 211 | 212 | function nodeGetArgCount(node) 213 | { 214 | node = _nodeResolve(node) 215 | const nt = nodeTypes[node.type] 216 | if (nt.getArgCount) 217 | return nt.getArgCount(node) 218 | return 0 219 | } 220 | 221 | function nodeArgGetName(node, argNum) 222 | { 223 | node = _nodeResolve(node) 224 | const nt = nodeTypes[node.type] 225 | argNum |= 0 226 | if (argNum < 0 || argNum >= nodeGetArgCount(node)) 227 | throw "[nodeArgGetName] index out of bounds" 228 | return nt.getArgName(node, argNum) 229 | } 230 | 231 | function nodeArgGetDefDims(node, argNum) 232 | { 233 | node = _nodeResolve(node) 234 | const nt = nodeTypes[node.type] 235 | argNum |= 0 236 | if (argNum < 0 || argNum >= nodeGetArgCount(node)) 237 | throw "[nodeArgGetDefDims] index out of bounds" 238 | return nt.getArgDims(node, argNum) 239 | } 240 | 241 | function nodeArgGetFlags(node, argNum) 242 | { 243 | node = _nodeResolve(node) 244 | const nt = nodeTypes[node.type] 245 | argNum |= 0 246 | if (argNum < 0 || argNum >= nodeGetArgCount(node)) 247 | throw "[nodeArgGetFlags] index out of bounds" 248 | return nt.getArgFlags(node, argNum) 249 | } 250 | 251 | function funcArgGetDefDimsByName(func, argName) 252 | { 253 | var cache = funcGetArgDimsCache(func) 254 | return cache[argName] || null 255 | } 256 | 257 | function nodeArgGetEditor(node, argNum) 258 | { 259 | return _nodeResolve(node).args[argNum].ed 260 | } 261 | 262 | function nodeArgSetEditor(node, argNum, ed) 263 | { 264 | node = _nodeResolve(node) 265 | if (node.args[argNum].ed === ed) 266 | return 267 | var arg = node.args[argNum] 268 | var olded = arg.ed 269 | if ((ed == "colhsv" || ed == "colhsva") && olded != "colhsv" && olded != "colhsva") 270 | { 271 | const hsv = hsvFromRGB(arg.value) 272 | arg.hsva = [hsv[0], hsv[1], hsv[2], arg.value[3]] 273 | } 274 | /*else if (olded == "colhsv" || olded == "colhsva") 275 | { 276 | const rgb = rgbFromHSV(arg.hsva) 277 | arg.value = [rgb[0], rgb[1], rgb[2], arg.hsva[3]] 278 | }*/ 279 | if ((ed == "collum" || ed == "colluma") && olded != "collum" && olded != "colluma") 280 | { 281 | const dims = nodeArgGetDefDims(node, argNum) 282 | var lum 283 | if (olded == "num1" || (olded == "numauto" && dims == 1)) 284 | lum = arg.value[0] 285 | else if (olded == "num2" || (olded == "numauto" && dims == 2)) 286 | lum = (arg.value[0] + arg.value[1]) / 2 287 | else 288 | lum = 0.2126 * arg.value[0] + 0.7152 * arg.value[1] + 0.0722 * arg.value[2] 289 | arg.value = [lum, lum, lum, arg.value[3]] 290 | } 291 | node.args[argNum].ed = ed 292 | store.update(`nodeMap/${node.id}`) 293 | } 294 | 295 | function nodeArgAdjustFixedEditors(node, argNum, dims) 296 | { 297 | node = _nodeResolve(node) 298 | var ed = node.args[argNum].ed 299 | if (ed == "num1" || ed == "num2" || ed == "num3" || ed == "num4") 300 | nodeArgSetEditor(node, argNum, "num" + dims) 301 | } 302 | 303 | 304 | 305 | function _nodesCheckColEmpty(colID) 306 | { 307 | var nodeCols = funcGetCurNodeCols() 308 | var col = nodeCols[colID] 309 | if (col.length != 0) 310 | return 311 | nodeCols.splice(colID, 1) 312 | for (var i = colID; i < nodeCols.length; ++i) 313 | { 314 | for (var j = 0; j < nodeCols[i].length; ++j) 315 | { 316 | nodeMap[nodeCols[i][j]].x = i 317 | nodeMap[nodeCols[i][j]].y = j 318 | } 319 | } 320 | } 321 | 322 | function _nodeAdjustArgs(node) 323 | { 324 | node = _nodeResolve(node) 325 | var argCount = nodeGetArgCount(node) 326 | if (argCount > 64) 327 | argCount = 64 328 | while (node.args.length < argCount) 329 | node.args.push(nodeArgConstruct(node, node.args.length, null)) 330 | } 331 | 332 | function nodeArgConstruct(node, argNum, srcData) 333 | { 334 | const arg = 335 | { 336 | node: null, 337 | swizzle: srcData ? srcData.swizzle : "", 338 | ed: srcData ? srcData.ed : nodeArgGetFlags(node, argNum) & ARG_NONUMEDIT ? "defval" : "numauto", 339 | value: srcData ? srcData.value.slice(0) : constructArgValue(nodeTypes[node.type].getArgDefVal(node, argNum)), 340 | hsva: srcData ? srcData.hsva.slice(0) : [0,0,0,1], 341 | varName: srcData ? srcData.varName : null, 342 | } 343 | return arg 344 | } 345 | 346 | function nodeConstruct(type, /*opt*/ srcData) 347 | { 348 | if (!(type in nodeTypes)) 349 | throw `Node type '${type}' is not a node` 350 | var nt = nodeTypes[type] 351 | var node = 352 | { 353 | id: ++window.nodeIDGen, 354 | type: type, 355 | x: null, 356 | y: null, 357 | showPreview: srcData ? srcData.showPreview : false, 358 | rsrc: srcData ? srcData.rsrc : (nt.defRsrc || null), 359 | args: [], 360 | outNodes: {}, 361 | toBeRemoved: false, 362 | } 363 | if (srcData) 364 | { 365 | const argCount = nodeGetArgCount(node) 366 | for (var i = 0; i < argCount; ++i) 367 | { 368 | node.args.push(nodeArgConstruct(node, i, srcData ? srcData.args[i] : null)) 369 | } 370 | } 371 | nodeMap[node.id] = node 372 | return node 373 | } 374 | 375 | function nodeRemoveFromCols(node) 376 | { 377 | var nodeCols = funcGetCurNodeCols() 378 | node = _nodeResolve(node) 379 | if (node.x !== null) 380 | { 381 | var col = nodeCols[node.x] 382 | col.splice(node.y, 1) 383 | for (var i = node.y; i < col.length; ++i) 384 | nodeMap[col[i]].y = i 385 | _nodesCheckColEmpty(node.x) 386 | node.x = null 387 | node.y = null 388 | node.func = null 389 | } 390 | } 391 | 392 | function nodeDelete(node) 393 | { 394 | node = _nodeResolve(node) 395 | for (var k in node.outNodes) 396 | { 397 | var on = nodeMap[k] 398 | for (var i = 0; i < on.args.length; ++i) 399 | { 400 | if (on.args[i].node === node.id) 401 | { 402 | on.args[i].node = null 403 | } 404 | } 405 | } 406 | for (var i = 0; i < node.args.length; ++i) 407 | { 408 | if (node.args[i].node !== null) 409 | nodeArgSetLink(node, i, null) 410 | } 411 | nodeRemoveFromCols(node) 412 | delete nodeMap[node.id] 413 | storeUpdateCurFuncNodes() 414 | } 415 | 416 | function nodeArgGetLink(node, argNum) 417 | { 418 | node = _nodeResolve(node) 419 | _nodeAdjustArgs(node) 420 | const arg = node.args[argNum] 421 | return arg.node 422 | } 423 | 424 | function _nodeArgUnlink(node, argNum) 425 | { 426 | node = _nodeResolve(node) 427 | _nodeAdjustArgs(node) 428 | const arg = node.args[argNum] 429 | if (arg.node !== null) 430 | { 431 | const prevNode = nodeMap[arg.node] 432 | arg.node = null 433 | var hasAny = false 434 | for (var i = 0; i < node.args.length; ++i) 435 | { 436 | if (node.args[i].node === prevNode.id) 437 | { 438 | hasAny = true 439 | break 440 | } 441 | } 442 | if (!hasAny) 443 | { 444 | delete prevNode.outNodes[node.id] 445 | } 446 | } 447 | } 448 | 449 | function nodeArgSetLink(node, argNum, tgtNode) 450 | { 451 | node = _nodeResolve(node) 452 | _nodeAdjustArgs(node) 453 | const arg = node.args[argNum] 454 | if (tgtNode) 455 | { 456 | tgtNode = _nodeResolve(tgtNode) 457 | if (arg.node === tgtNode.id) 458 | return 459 | _nodeArgUnlink(node, argNum) 460 | tgtNode.outNodes[node.id] = true 461 | arg.node = tgtNode.id 462 | } 463 | else 464 | { 465 | _nodeArgUnlink(node, argNum) 466 | } 467 | storeUpdateCurFuncNodes() 468 | } 469 | 470 | function nodeArgSetValue(node, argNum, value) 471 | { 472 | node = _nodeResolve(node) 473 | _nodeAdjustArgs(node) 474 | const arg = node.args[argNum] 475 | if (arg.ed != "num1" && arg.ed != "num2" && arg.ed != "num3" && arg.ed != "num4" && arg.ed != "numauto") 476 | nodeArgSetEditor(node, argNum, "numauto") 477 | for (var i = 0; i < 4; ++i) 478 | arg.value[i] = i < value.length ? value[i] : (i == 3 ? 1 : 0) 479 | } 480 | 481 | function nodeSetArgVariable(node, argNum, varName) 482 | { 483 | node = _nodeResolve(node) 484 | _nodeAdjustArgs(node) 485 | const arg = node.args[argNum] 486 | if (arg.ed != "var") 487 | nodeArgSetEditor(node, argNum, "var") 488 | arg.varName = varName 489 | } 490 | 491 | function nodeSetArgSwizzle(node, argNum, swizzle) 492 | { 493 | node = _nodeResolve(node) 494 | _nodeAdjustArgs(node) 495 | const arg = node.args[argNum] 496 | arg.swizzle = swizzle 497 | } 498 | 499 | function nodeColsInsertCol(x, /*opt*/ nodes) 500 | { 501 | var nodeCols = funcGetCurNodeCols() 502 | x = Math.max(0, Math.min(nodeCols.length, x)) 503 | for (var i = x; i < nodeCols.length; ++i) 504 | for (var j = 0; j < nodeCols[i].length; ++j) 505 | nodeMap[nodeCols[i][j]].x++ 506 | nodes = nodes || [] 507 | for (var j = 0; j < nodes.length; ++j) 508 | { 509 | nodes[j] = _nodeResolve(nodes[j]) 510 | if (nodes[j].x !== null) 511 | throw `Node ${nodes[j].id} already inserted, remove it first` 512 | nodes[j].x = x 513 | nodes[j].y = j 514 | nodes[j].func = funcGetCurName() 515 | nodes[j] = nodes[j].id 516 | } 517 | nodeCols.splice(x, 0, nodes) 518 | storeUpdateCurFuncNodes() 519 | } 520 | 521 | function nodeInsertIntoCols(node, x, y) 522 | { 523 | var nodeCols = funcGetCurNodeCols() 524 | node = _nodeResolve(node) 525 | if (node.x !== null) 526 | throw `Node ${node.id} already inserted, remove it first` 527 | x = Math.max(0, Math.min(nodeCols.length - 1, x)) 528 | var col = nodeCols[x] 529 | y = Math.max(0, Math.min(col.length, y)) 530 | for (var i = y; i < col.length; ++i) 531 | nodeMap[col[i]].y++ 532 | col.splice(y, 0, node.id) 533 | node.x = x 534 | node.y = y 535 | node.func = funcGetCurName() 536 | storeUpdateCurFuncNodes() 537 | } 538 | 539 | function nodeInsertAtCol(node, x) 540 | { 541 | var nodeCols = funcGetCurNodeCols() 542 | node = _nodeResolve(node) 543 | if (x > nodeCols.length) 544 | x = nodeCols.length 545 | if (x == nodeCols.length) 546 | nodeColsInsertCol(x, [node]) 547 | else 548 | nodeInsertIntoCols(node, x, nodeCols[x].length) 549 | } 550 | 551 | function nodeInsertBehind(node, behind) 552 | { 553 | var nodeCols = funcGetCurNodeCols() 554 | node = _nodeResolve(node) 555 | behind = _nodeResolve(behind) 556 | if (behind.x === null) 557 | throw `Node ${behind.id} not inserted, cannot know where 'behind' is` 558 | nodeInsertAtCol(node, behind.x + 1) 559 | } 560 | 561 | function nodeReadsFrom(node, fromNode) 562 | { 563 | node = _nodeResolve(node) 564 | fromNode = _nodeResolve(fromNode).id 565 | if (node.id == fromNode) 566 | return true 567 | const argNum = nodeGetArgCount(node) 568 | for (var i = 0; i < argNum; ++i) 569 | { 570 | if (node.args[i].node && nodeReadsFrom(node.args[i].node, fromNode)) 571 | return true 572 | } 573 | return false 574 | } 575 | 576 | function nodeArgGetSrcDims(node, argNum, tgtDims) 577 | { 578 | node = _nodeResolve(node) 579 | _nodeAdjustArgs(node) 580 | const arg = node.args[argNum] 581 | if (arg.node) 582 | { 583 | var argNode = nodeMap[arg.node] 584 | return nodeTypes[argNode.type].getRVDims(argNode) 585 | } 586 | if (arg.ed == "var" && arg.varName) 587 | { 588 | return funcArgGetDefDimsByName(node.func, arg.varName) || nodeResources.variable[arg.varName].dims 589 | } 590 | var argDims = nodeArgGetDefDims(node, argNum) 591 | if (argDims === "adapt") 592 | { 593 | if (arg.ed == "num1") return 1 594 | if (arg.ed == "num2") return 2 595 | if (arg.ed == "num3") return 3 596 | if (arg.ed == "num4") return 4 597 | if (arg.ed == "colrgb" || arg.ed == "colhsv" || arg.ed == "collum") return 3 598 | if (arg.ed == "colrgba" || arg.ed == "colhsva" || arg.ed == "colluma") return 4 599 | return tgtDims || nodeCalcDimsFromArgs(node) 600 | } 601 | return argDims 602 | } 603 | 604 | function nodeArgGetDimsResolved(node, argNum) 605 | { 606 | node = _nodeResolve(node) 607 | var argDims = nodeArgGetDefDims(node, argNum) 608 | return argDims === "adapt" ? nodeCalcDimsFromArgs(node) : argDims 609 | } 610 | 611 | function nodeCalcDimsFromArgs(node) 612 | { 613 | var maxDims = 1 614 | _nodeAdjustArgs(node) 615 | const argNum = nodeGetArgCount(node) 616 | for (var i = 0; i < argNum; ++i) 617 | { 618 | var arg = node.args[i] 619 | var anID = arg.node 620 | if (anID) 621 | { 622 | var anode = nodeMap[anID] 623 | maxDims = Math.max(maxDims, nodeTypes[anode.type].getRVDims(anode)) 624 | } 625 | else if (arg.ed == "num1") 626 | maxDims = Math.max(maxDims, 1) 627 | else if (arg.ed == "num2") 628 | maxDims = Math.max(maxDims, 2) 629 | else if (arg.ed == "num3") 630 | maxDims = Math.max(maxDims, 3) 631 | else if (arg.ed == "num4") 632 | maxDims = Math.max(maxDims, 4) 633 | else if (arg.ed == "var" && arg.varName) 634 | maxDims = Math.max(maxDims, funcArgGetDefDimsByName(node.func, arg.varName) || nodeResources.variable[arg.varName].dims) 635 | } 636 | return maxDims 637 | } 638 | 639 | function nodeArgGetInfo(node, argNum) 640 | { 641 | node = _nodeResolve(node) 642 | const arg = node.args[argNum] 643 | return { 644 | node: arg.node, 645 | swizzle: arg.swizzle, 646 | ed: arg.ed, 647 | varName: arg.varName, 648 | value: arg.value.slice(0), 649 | hsva: arg.hsva.slice(0), 650 | } 651 | } 652 | 653 | function nodeArgSetInfo(node, argNum, info) 654 | { 655 | node = _nodeResolve(node) 656 | _nodeAdjustArgs(node) 657 | const arg = node.args[argNum] 658 | if (typeof info.node !== "undefined") 659 | nodeArgSetLink(node, argNum, info.node) 660 | if (typeof info.swizzle !== "undefined") 661 | arg.swizzle = info.swizzle 662 | if (typeof info.ed !== "undefined") 663 | arg.ed = info.ed 664 | if (typeof info.varName !== "undefined") 665 | arg.varName = info.varName 666 | if (typeof info.value !== "undefined") 667 | arg.value = info.value.slice(0) 668 | if (typeof info.hsva !== "undefined") 669 | arg.hsva = info.hsva.slice(0) 670 | } 671 | 672 | function nodeGetLinkedInputCount(node) 673 | { 674 | node = _nodeResolve(node) 675 | var count = 0 676 | for (var oid in node.outNodes) 677 | { 678 | const on = nodeMap[oid] 679 | const argCount = nodeGetArgCount(on) 680 | for (var i = 0; i < argCount; ++i) 681 | if (nodeArgGetLink(on, i) === node.id) 682 | count++ 683 | } 684 | return count 685 | } 686 | 687 | function _nodeGetExpr(node, depth) 688 | { 689 | node = _nodeResolve(node) 690 | //console.log(depth) 691 | if (depth > 3 || nodeGetLinkedInputCount(node) >= 2) 692 | return "#" + node.id 693 | return nodeTypes[node.type].getExpr(node, depth) 694 | } 695 | 696 | function _valGetExpr(val, dims, ed) 697 | { 698 | //console.log(ed) 699 | if (dims == 1 || ed == "num1") 700 | return numToFloatStr(val[0], false) 701 | return type2glsl[dims] + "(" + val.slice(0, dims).map((n) => numToFloatStr(n, false)).join(",") + ")" 702 | } 703 | 704 | function nodeGetArgExpr(node, argNum, level) 705 | { 706 | node = _nodeResolve(node) 707 | var arg = node.args[argNum] 708 | if (arg.node) 709 | return _nodeGetExpr(arg.node, level) + (arg.swizzle ? "." + arg.swizzle : "") 710 | if (arg.ed == "var") 711 | return arg.varName + (arg.swizzle ? "." + arg.swizzle : "") 712 | return _valGetExpr(arg.value, nodeArgGetDimsResolved(node, argNum), nodeArgGetEditor(node, argNum)) 713 | } 714 | 715 | 716 | -------------------------------------------------------------------------------- /NodePinUI.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | var queuedRedrawCurveCanvas = false; 5 | function redrawCurveCanvas() 6 | { 7 | console.log("redraw") 8 | queuedRedrawCurveCanvas = false; 9 | 10 | var editArea = document.getElementById("editArea") 11 | var canvas = document.getElementById("curveOverlay") 12 | var parent = canvas.parentElement 13 | if (canvas.width != parent.offsetWidth) 14 | canvas.width = parent.offsetWidth 15 | if (canvas.height != parent.offsetHeight) 16 | canvas.height = parent.offsetHeight 17 | 18 | var ctx = canvas.getContext("2d") 19 | ctx.clearRect(0, 0, canvas.width, canvas.height) 20 | ctx.strokeStyle = "rgba(200,150,0,0.5)" 21 | ctx.lineWidth = 2 22 | ctx.lineCap = "round" 23 | 24 | var cr_ea = editArea.getBoundingClientRect() 25 | 26 | var nodeCols = funcGetCurNodeCols() 27 | for (var i = 0; i < nodeCols.length; ++i) 28 | { 29 | for (var j = 0; j < nodeCols[i].length; ++j) 30 | { 31 | var id_to = nodeCols[i][j] 32 | var node_to = nodeMap[id_to] 33 | const argNum = nodeGetArgCount(node_to) 34 | for (var k = 0; k < argNum; ++k) 35 | { 36 | var arg = node_to.args[k] 37 | if (arg.node) 38 | { 39 | var id_from = arg.node 40 | 41 | var el_from = document.getElementById(`node_${id_from}`) 42 | var el_to = document.querySelector(`#node_${id_to}_input_${k} .pin`) 43 | 44 | var cr_from = el_from.getBoundingClientRect() 45 | var cr_to = el_to.getBoundingClientRect() 46 | 47 | var fx = cr_from.right - cr_ea.left 48 | var fy = cr_from.top - cr_ea.top 49 | var tx = cr_to.left - cr_ea.left 50 | var ty = cr_to.top + cr_to.height / 2 - cr_ea.top 51 | 52 | ctx.beginPath() 53 | ctx.moveTo(fx, fy + 16) 54 | ctx.bezierCurveTo( 55 | fx + 32, fy + 16, 56 | tx - 32, ty, 57 | tx, ty 58 | ) 59 | ctx.stroke() 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | function onNodeLayoutChange() 67 | { 68 | if (!queuedRedrawCurveCanvas) 69 | { 70 | requestAnimationFrame(redrawCurveCanvas) 71 | queuedRedrawCurveCanvas = true 72 | } 73 | } 74 | 75 | 76 | 77 | const NodePinEdit = component 78 | ({ 79 | mount() 80 | { 81 | this.handleOuterClick = this.handleOuterClick.bind(this) 82 | 83 | this.handleAddClick = this.handleAddClick.bind(this) 84 | this.handleNumberClick = this.handleNumberClick.bind(this) 85 | this.handlePickClick = this.handlePickClick.bind(this) 86 | this.handleColorClick = this.handleColorClick.bind(this) 87 | this.handleVariableClick = this.handleVariableClick.bind(this) 88 | this.handleExprClick = this.handleExprClick.bind(this) 89 | 90 | this.handleBackClick = this.handleBackClick.bind(this) 91 | this.handleAddMathNodeClick = this.handleAddMathNodeClick.bind(this) 92 | this.handleAddTextureNodeClick = this.handleAddTextureNodeClick.bind(this) 93 | this.handleAddFunctionNodeClick = this.handleAddFunctionNodeClick.bind(this) 94 | 95 | this.view = "select" 96 | }, 97 | 98 | handleOuterClick(e) 99 | { 100 | if (e.button == 1) 101 | return 102 | e.preventDefault() 103 | this.onCloseClick(e) 104 | }, 105 | 106 | handleAddClick(e) 107 | { 108 | this.view = "addNode" 109 | update(this) 110 | }, 111 | handleNumberClick(e) 112 | { 113 | nodeArgSetLink(this.node, this.argNum, null) 114 | arg = this.node.args[this.argNum] 115 | if (arg.ed != "numauto" && arg.ed != "num1" && arg.ed != "num2" && arg.ed != "num3" && arg.ed != "num4" && arg.ed != "defval") 116 | nodeArgSetEditor(this.node, this.argNum, nodeArgGetFlags(this.node, this.argNum) & ARG_NONUMEDIT ? "defval" : "numauto") 117 | this.onCloseClick(e) 118 | }, 119 | handlePickClick(e) 120 | { 121 | Drag_StartLinkDrag(this.node.id, this.argNum, e) 122 | this.onCloseClick(e) 123 | }, 124 | handleColorClick(e) 125 | { 126 | nodeArgSetLink(this.node, this.argNum, null) 127 | arg = this.node.args[this.argNum] 128 | if (arg.ed != "colrgba" && arg.ed != "colrgb" && 129 | arg.ed != "colhsva" && arg.ed != "colhsv" && 130 | arg.ed != "colluma" && arg.ed != "collum" && 131 | arg.ed != "defval") 132 | { 133 | const dims = nodeArgGetDefDims(this.node, this.argNum) 134 | nodeArgSetEditor(this.node, this.argNum, dims === 1 ? "collum" : dims == 3 ? "colhsv" : "colhsva") 135 | } 136 | this.onCloseClick(e) 137 | }, 138 | handleVariableClick(e) 139 | { 140 | nodeArgSetLink(this.node, this.argNum, null) 141 | arg = this.node.args[this.argNum] 142 | nodeArgSetEditor(this.node, this.argNum, "var") 143 | this.onCloseClick(e) 144 | }, 145 | handleExprClick(e) 146 | { 147 | //this.view = "expr" 148 | ExpressionEdit_Start(this.node, this.argNum) 149 | this.onCloseClick(e) 150 | }, 151 | 152 | handleBackClick(e) 153 | { 154 | this.view = "select" 155 | update(this) 156 | }, 157 | handleAddMathNodeClick(e) 158 | { 159 | const nn = nodeConstruct("math") 160 | nodeInsertBehind(nn, this.node) 161 | nodeArgSetLink(this.node, this.argNum, nn) 162 | this.onCloseClick(e) 163 | }, 164 | handleAddTextureNodeClick(e) 165 | { 166 | const nn = nodeConstruct("tex2D") 167 | nodeInsertBehind(nn, this.node) 168 | nodeArgSetLink(this.node, this.argNum, nn) 169 | this.onCloseClick(e) 170 | }, 171 | handleAddFunctionNodeClick(e) 172 | { 173 | const nn = nodeConstruct("func") 174 | nodeInsertBehind(nn, this.node) 175 | nodeArgSetLink(this.node, this.argNum, nn) 176 | this.onCloseClick(e) 177 | }, 178 | 179 | render() 180 | { 181 | const node = nodeArgGetLink(this.node, this.argNum) 182 | const ed = nodeArgGetEditor(this.node, this.argNum) 183 | const isVarEdit = !node && (ed == "var") 184 | const isNumEdit = !node && (ed == "num1" || ed == "num2" || ed == "num3" || ed == "num4" || ed == "numauto" || ed == "defval") 185 | const isColorEdit = !node && (ed == "colrgb" || ed == "colrgba" || ed == "colhsv" || ed == "colhsva" || ed == "collum" || ed == "colluma") 186 | evoid("div", { "class": "bgr", onpointerdown: this.handleOuterClick }) 187 | eopen("NodePinEdit") 188 | switch (this.view) 189 | { 190 | case "select": 191 | eopen("div", { "class": "selectView" }) 192 | eopen("span", { "class": "newNodeBtn btn", onclick: this.handleAddClick }) 193 | evoid("i", { "class": "fa fa-plus" }) 194 | eopen("name") 195 | text("Add node") 196 | eclose("name") 197 | eclose("span") 198 | eopen("span", { "class": "numberBtn btn" + (isNumEdit ? " active disabled" : ""), onclick: this.handleNumberClick }) 199 | if (nodeArgGetFlags(this.node, this.argNum) & ARG_NONUMEDIT) 200 | { 201 | evoid("i", { "class": "fa fa-undo-alt" }) 202 | eopen("name") 203 | text("Default") 204 | eclose("name") 205 | } 206 | else 207 | { 208 | evoid("i", { "class": "ico ico-num" }) 209 | eopen("name") 210 | text("Number") 211 | eclose("name") 212 | } 213 | eclose("span") 214 | 215 | eopen("span", { "class": "pickNodeBtn btn", onclick: this.handlePickClick }) 216 | evoid("i", { "class": "fa fa-hand-point-left" }) 217 | eopen("name") 218 | text("Pick node") 219 | eclose("name") 220 | eclose("span") 221 | 222 | eopen("span", { "class": "closeBtn btn", onclick: this.onCloseClick }) 223 | evoid("i", { "class": "fa fa-times io" }) 224 | eclose("span") 225 | 226 | var canShowColor = EditorOpts[2].test(nodeArgGetDefDims(this.node, this.argNum), nodeArgGetFlags(this.node, this.argNum)) 227 | eopen("span", { "class": "colorBtn btn" + (canShowColor && !isColorEdit ? "" : " disabled") + (isColorEdit ? " active" : ""), onclick: this.handleColorClick }) 228 | evoid("i", { "class": "fa fa-palette" }) 229 | eopen("name") 230 | text("Color") 231 | eclose("name") 232 | eclose("span") 233 | 234 | eopen("span", { "class": "variableBtn btn" + (isVarEdit ? " active disabled" : ""), onclick: this.handleVariableClick }) 235 | evoid("i", { "class": "ico ico-var" }) 236 | eopen("name") 237 | text("Variable") 238 | eclose("name") 239 | eclose("span") 240 | eopen("span", { "class": "expressionBtn btn", onclick: this.handleExprClick }) 241 | evoid("i", { "class": "fa fa-superscript" }) 242 | eopen("name") 243 | text("Expression") 244 | eclose("name") 245 | eclose("span") 246 | eclose("div") 247 | break 248 | case "addNode": 249 | eopen("div", { "class": "addNodeView" }) 250 | eopen("span", { "class": "backBtn btn", onclick: this.handleBackClick }) 251 | evoid("i", { "class": "fa fa-chevron-left" }) 252 | eopen("name") 253 | text("Back") 254 | eclose("name") 255 | eclose("span") 256 | eopen("span", { "class": "addMathNodeBtn btn", onclick: this.handleAddMathNodeClick }) 257 | evoid("i", { "class": "fa fa-superscript" }) 258 | eopen("name") 259 | text("Math") 260 | eclose("name") 261 | eclose("span") 262 | eopen("span", { "class": "addTextureNodeBtn btn", onclick: this.handleAddTextureNodeClick }) 263 | evoid("i", { "class": "fa fa-image" }) 264 | eopen("name") 265 | text("Texture") 266 | eclose("name") 267 | eclose("span") 268 | eopen("span", { "class": "addFunctionNodeBtn btn", onclick: this.handleAddFunctionNodeClick }) 269 | evoid("i", { "class": "ico ico-function" }) 270 | eopen("name") 271 | text("Function") 272 | eclose("name") 273 | eclose("span") 274 | eclose("div") 275 | break 276 | case "expr": 277 | eopen("div", { "class": "exprView" }) 278 | eopen("span", { "class": "backBtn btn", onclick: this.handleBackClick }) 279 | evoid("i", { "class": "fa fa-chevron-left" }) 280 | eopen("name") 281 | text("Back") 282 | eclose("name") 283 | eclose("span") 284 | cvoid(Checkbox, { bind: "editor/nodeExprCleanup", label: "Clean up" }) 285 | var el = evoid("textarea", { "class": "expr" }).element 286 | el.value = this.calcExpr 287 | el.focus() 288 | eclose("div") 289 | break 290 | } 291 | eclose("NodePinEdit") 292 | }, 293 | }) 294 | 295 | const EditorOpts = 296 | [ 297 | { 298 | test: (d, f) => (f & ARG_NONUMEDIT) != 0, 299 | name: "Values", 300 | opts: 301 | [ 302 | ["defval", "Default value", () => true], 303 | ], 304 | }, 305 | { 306 | test: (d, f) => (f & ARG_NONUMEDIT) == 0, 307 | name: "Number editor", 308 | opts: 309 | [ 310 | ["numauto", "Auto", () => true], 311 | ["num1", "1", () => true], 312 | ["num2", "2", (d) => d == "adapt" || d == 2], 313 | ["num3", "3", (d) => d == "adapt" || d == 3], 314 | ["num4", "4", (d) => d == "adapt" || d == 4], 315 | ], 316 | }, 317 | { 318 | test: (d, f) => (d == "adapt" || d == 1 || d == 3 || d == 4) && (f & ARG_NONUMEDIT) == 0, 319 | name: "Color editor", 320 | opts: 321 | [ 322 | ["colrgb", "RGB", (d) => d == "adapt" || d == 3], 323 | ["colrgba", "RGBA", (d) => d == "adapt" || d == 4], 324 | ["colhsv", "HSV", (d) => d == "adapt" || d == 3], 325 | ["colhsva", "HSVA", (d) => d == "adapt" || d == 4], 326 | ["collum", "Lum", (d) => d == "adapt" || d == 1 || d == 3], 327 | ["colluma", "LumA", (d) => d == "adapt" || d == 4], 328 | ], 329 | }, 330 | { 331 | test: () => true, 332 | name: "Variable", 333 | opts: 334 | [ 335 | ["var", "Uniform / Varying", () => true], 336 | ], 337 | }, 338 | ] 339 | const NodeInput = component 340 | ({ 341 | mount() 342 | { 343 | this.handlePinPointerDown = this.handlePinPointerDown.bind(this) 344 | this.handlePinClick = this.handlePinClick.bind(this) 345 | this.handleValueTypeEditClick = this.handleValueTypeEditClick.bind(this) 346 | this.handleVTEOuterClick = this.handleVTEOuterClick.bind(this) 347 | this.handleVTEPickClick = this.handleVTEPickClick.bind(this) 348 | }, 349 | handlePinPointerDown(e) 350 | { 351 | Drag_StartLinkDrag(this.node.id, this.argNum, e) 352 | }, 353 | handlePinClick(e) 354 | { 355 | this.pinEditOpen = !this.pinEditOpen 356 | update(this) 357 | }, 358 | handleValueTypeEditClick(e) 359 | { 360 | this.valueTypeEditOpen = !this.valueTypeEditOpen 361 | update(this) 362 | }, 363 | handleVTEOuterClick(e) 364 | { 365 | this.valueTypeEditOpen = false 366 | update(this) 367 | }, 368 | handleVTEPickClick(e) 369 | { 370 | this.valueTypeEditOpen = false 371 | nodeArgSetEditor(this.node, this.argNum, e.currentTarget.dataset.ed) 372 | //update(this) 373 | }, 374 | render() 375 | { 376 | var argNum = this.argNum 377 | var node = this.node 378 | _nodeAdjustArgs(node) 379 | var arg = node.args[argNum] 380 | var tgtArgDims = nodeArgGetDimsResolved(node, argNum) 381 | var srcArgDims = nodeArgGetSrcDims(node, argNum, tgtArgDims) 382 | 383 | eopen("NodeInput", { "class": "input", id: `node_${node.id}_input_${argNum}` }) 384 | eopen("span", { "class": arg.node ? "pin linked" : "pin", onpointerdown: this.handlePinPointerDown, onclick: this.handlePinClick }) 385 | evoid("i", { "class": "fa fa-" + (arg.node ? "pencil-alt" : "plus") }) 386 | eclose("span") 387 | if (this.pinEditOpen) 388 | cvoid(NodePinEdit, { node: this.node, argNum: this.argNum, onCloseClick: this.handlePinClick }) 389 | eopen("span", { "class": "name" }) 390 | text(nodeArgGetName(node, argNum)) 391 | eclose("span") 392 | eopen("span", { "class": "type" }) 393 | if (tgtArgDims != srcArgDims) 394 | { 395 | cvoid(AxisMarker, { dims: srcArgDims }) 396 | evoid("i", { "class": "fa fa-caret-right" }) 397 | } 398 | cvoid(AxisMarker, { dims: tgtArgDims }) 399 | eclose("span") 400 | if (arg.node === null) 401 | { 402 | eopen("span", { "class": "editorBtn", onclick: this.handleValueTypeEditClick }) 403 | evoid("i", { "class": "fa fa-sliders-h" }) 404 | eclose("span") 405 | if (this.valueTypeEditOpen) 406 | { 407 | evoid("div", { "class": "bgr", onpointerdown: this.handleVTEOuterClick }) 408 | eopen("ValueTypeEdit") 409 | eopen("span", { "class": "editorBtn", onclick: this.handleValueTypeEditClick }) 410 | evoid("i", { "class": "fa fa-sliders-h" }) 411 | eclose("span") 412 | eopen("Name") 413 | text("Select editor type") 414 | eclose("Name") 415 | for (var i = 0; i < EditorOpts.length; ++i) 416 | { 417 | const argDims = nodeArgGetDefDims(node, argNum) 418 | const argFlags = nodeArgGetFlags(node, argNum) 419 | if (!EditorOpts[i].test(argDims, argFlags)) 420 | continue 421 | eopen("GroupName") 422 | text(EditorOpts[i].name) 423 | eclose("GroupName") 424 | eopen("GroupOpts") 425 | for (var j = 0; j < EditorOpts[i].opts.length; ++j) 426 | { 427 | if (!EditorOpts[i].opts[j][2](argDims, argFlags)) 428 | continue 429 | var cls = "btn" + (EditorOpts[i].opts[j][0] == arg.ed ? " used" : "") 430 | evoid("input", { type: "button", "class": cls, value: EditorOpts[i].opts[j][1], "data-ed": EditorOpts[i].opts[j][0], onclick: this.handleVTEPickClick }) 431 | } 432 | eclose("GroupOpts") 433 | } 434 | eclose("ValueTypeEdit") 435 | } 436 | if (arg.ed == "numauto") 437 | cvoid(ValueEdit, { bind: `${this.bind}/value`, dims: tgtArgDims }) 438 | else if (arg.ed == "num1") 439 | cvoid(ValueEdit, { bind: `${this.bind}/value`, dims: 1 }) 440 | else if (arg.ed == "num2") 441 | cvoid(ValueEdit, { bind: `${this.bind}/value`, dims: 2 }) 442 | else if (arg.ed == "num3") 443 | cvoid(ValueEdit, { bind: `${this.bind}/value`, dims: 3 }) 444 | else if (arg.ed == "num4") 445 | cvoid(ValueEdit, { bind: `${this.bind}/value`, dims: 4 }) 446 | else if (arg.ed == "colhsv" || arg.ed == "colhsva") 447 | cvoid(ColHSVAEdit, { bind: `${this.bind}/hsva`, rgbaBind: `${this.bind}/value`, dims: tgtArgDims }) 448 | else if (arg.ed == "colrgb" || arg.ed == "colrgba") 449 | cvoid(ColRGBAEdit, { bind: `${this.bind}/value`, dims: tgtArgDims }) 450 | else if (arg.ed == "collum" || arg.ed == "colluma") 451 | cvoid(ColLuminanceEdit, { bind: `${this.bind}/value`, dims: tgtArgDims, alpha: arg.ed == "colluma" }) 452 | else if (arg.ed == "var") 453 | cvoid(NodeResource, { bind: `${this.bind}/varName`, node: this.node, type: "variable", fullRefreshOnChange: true }) 454 | } 455 | if (arg.node !== null || arg.ed == "var") 456 | cvoid(SwizzleEdit, { bind: `${this.bind}/swizzle`, srcDims: srcArgDims, tgtDims: tgtArgDims }) 457 | eclose("NodeInput") 458 | }, 459 | }) 460 | 461 | 462 | -------------------------------------------------------------------------------- /NodeUI.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | const NodePreview = component 5 | ({ 6 | render() 7 | { 8 | eopen("div", { "class": "previewCont checkerBgr" }) 9 | evoid("canvas", { "width": 118, "height": 118, "class": "nodePreviewCanvas" }) 10 | eclose("div") 11 | } 12 | }) 13 | 14 | const NodeAutoCompleteTextField = component 15 | ({ 16 | mount() 17 | { 18 | this.selectCallback = this.selectCallback.bind(this) 19 | this.itemCallback = this.itemCallback.bind(this) 20 | }, 21 | selectCallback(key) 22 | { 23 | var nodeCols = funcGetCurNodeCols() 24 | var colID = nodeCols.length - 1 25 | 26 | var rsrc = null 27 | var rsrcPos = key.indexOf(":") 28 | if (rsrcPos != -1) 29 | { 30 | rsrc = key.substring(rsrcPos + 1) 31 | key = key.substring(0, rsrcPos) 32 | } 33 | 34 | if (key in nodeTypes) 35 | { 36 | var node = nodeConstruct(key) 37 | if (rsrc !== null) 38 | node.rsrc = rsrc 39 | store.set("editor/nodeBlinkID", node.id) 40 | nodeInsertIntoCols(node, colID, nodeCols[colID].length) 41 | } 42 | }, 43 | itemCallback(text) 44 | { 45 | var out = [] 46 | for (var key in nodeTypes) 47 | { 48 | if (key == "output") 49 | continue 50 | const itemName = resolveString(nodeTypes[key].name, null) 51 | ACItemMatch(out, text, key, itemName, nodeTypes[key].desc) 52 | const rsrcType = nodeTypes[key].rsrcType 53 | if (rsrcType) 54 | { 55 | const resMap = nodeResources[rsrcType] 56 | for (var rkey in resMap) 57 | { 58 | ACItemMatch(out, text, key + ":" + rkey, 59 | (resMap[rkey].name || rkey) + " - " + itemName, 60 | nodeResourceGenerateDesc(rsrcType, rkey) || nodeTypes[key].desc) 61 | } 62 | } 63 | } 64 | return out 65 | }, 66 | render() 67 | { 68 | eopen("AddNodeAC") 69 | cvoid(AutoCompleteTextField, { bind: "editor/nodeAC", selectCallback: this.selectCallback, itemCallback: this.itemCallback }) 70 | eclose("AddNodeAC") 71 | }, 72 | }) 73 | 74 | const NodeResource = component 75 | ({ 76 | mount() 77 | { 78 | this.open = false 79 | this.handleSelectBtnClick = this.handleSelectBtnClick.bind(this) 80 | this.selectCallback = this.selectCallback.bind(this) 81 | this.itemCallback = this.itemCallback.bind(this) 82 | }, 83 | handleSelectBtnClick(e) 84 | { 85 | this.open = !this.open 86 | update(this) 87 | }, 88 | selectCallback(key) 89 | { 90 | if (key !== null) 91 | this.$value = key 92 | this.open = false 93 | if (this.fullRefreshOnChange) 94 | storeUpdateCurFuncNodes() 95 | else 96 | update(this) 97 | }, 98 | itemCallback(text) 99 | { 100 | var out = []; 101 | var rsrcMap = nodeResources[this.type] 102 | for (var key in rsrcMap) 103 | { 104 | var rsrc = rsrcMap[key] 105 | ACItemMatch(out, text, key, rsrc.name || key, nodeResourceGenerateDesc(this.type, key)) 106 | } 107 | if (this.type == "variable") 108 | { 109 | const fnData = funcGetCurData() 110 | for (var i = 0; i < fnData.args.length; ++i) 111 | { 112 | const a = fnData.args[i] 113 | ACItemMatch(out, text, a.name, a.name, `${dims2type[a.dims]} argument`) 114 | } 115 | } 116 | return out 117 | }, 118 | render() 119 | { 120 | var rsrcName = "-" 121 | if (this.$value) 122 | { 123 | if (this.type == "variable" && funcArgGetDefDimsByName(this.node.func, this.$value)) 124 | rsrcName = this.$value 125 | else 126 | rsrcName = nodeResources[this.type][this.$value].name || this.$value 127 | } 128 | else 129 | this.open = true 130 | eopen("NodeResource", { "class": "selectWrap" }) 131 | eopen("NodeRsrcSelect", { "class": "selectCont" + (this.open ? " open" : "") }) 132 | eopen("NodeRsrcSelectBtn", { "class": "selectBtn", onclick: this.handleSelectBtnClick }) 133 | eopen("Name") 134 | text(rsrcName) 135 | eclose("Name") 136 | eopen("ToggleMarker") 137 | text("▼") 138 | eclose("ToggleMarker") 139 | eclose("NodeRsrcSelectBtn") 140 | if (this.open) 141 | { 142 | cvoid(AutoCompleteTextField, 143 | { 144 | bind: `editor/${this.type}AC`, 145 | placeholder: rsrcName, 146 | alwaysOpen: true, 147 | focusOnOpen: true, 148 | selectCallback: this.selectCallback, 149 | itemCallback: this.itemCallback, 150 | }) 151 | } 152 | eclose("NodeRsrcSelect") 153 | eclose("NodeResource") 154 | }, 155 | }) 156 | 157 | const Node = component 158 | ({ 159 | mount() 160 | { 161 | this.handleTitlePointerDown = this.handleTitlePointerDown.bind(this) 162 | }, 163 | handleTitlePointerDown(e) 164 | { 165 | if (e.target != e.currentTarget || e.button != 0) 166 | return 167 | Drag_StartNodeDrag(this.$value.id) 168 | }, 169 | render() 170 | { 171 | var type = nodeTypes[this.$value.type] 172 | var cls = "node node" + this.$value.id 173 | if (store.get("editor/nodeBlinkID") == this.$value.id) 174 | cls += " blinkOnce" 175 | if (type.rsrcType) 176 | cls += " hasRsrc" 177 | if (this.$value.id > window.nodeIDGen) 178 | cls += " new" 179 | if (this.$value.toBeRemoved) 180 | cls += " toBeRemoved" 181 | eopen("div", { "class": "nodeCont", "id": `node_cont_${this.$value.id}`, "data-id": this.$value.id }) 182 | eopen("div", { "class": cls, "id": `node_${this.$value.id}`, "data-id": this.$value.id, oncontextmenu: (e) => 183 | { 184 | if (!document.getElementById("shaderEditor").classList.contains("disableNodeControls")) 185 | openContextMenu(e, "editor/nodeContextID", this.$value.id) 186 | else 187 | e.preventDefault() 188 | } 189 | }) 190 | eopen("div", { "class": "name", onpointerdown: this.handleTitlePointerDown }) 191 | text(resolveString(type.name, this.$value)) 192 | cvoid(OpenToggle, { bind: `${this.bind}/showPreview`, "class": "togglePreview fa " + (this.$value.showPreview ? "fa-eye" : "fa-eye-slash") }) 193 | eclose("div") 194 | if (this.$value.showPreview) 195 | { 196 | cvoid(NodePreview) 197 | } 198 | if (type.rsrcType) 199 | { 200 | if (type.rsrcType === "func") 201 | cvoid(FunctionSelect, { bind: `${this.bind}/rsrc`, type: type.rsrcType, noMain: true, isResource: true }) 202 | else 203 | cvoid(NodeResource, { bind: `${this.bind}/rsrc`, node: this.$value, type: type.rsrcType, fullRefreshOnChange: type.rsrcFullRefreshOnChange }) 204 | } 205 | eopen("div", { "class": "args" }) 206 | const argNum = nodeGetArgCount(this.$value) 207 | for (var i = 0; i < argNum; ++i) 208 | { 209 | cvoid(NodeInput, { argNum: i, node: this.$value, bind: `${this.bind}/args/${i}` }) 210 | } 211 | eclose("div") 212 | eclose("div") 213 | eclose("div") 214 | }, 215 | }) 216 | 217 | function openContextMenu(event, id, data) 218 | { 219 | event.preventDefault() 220 | store.set("contextMenuPos", { x: event.clientX, y: event.clientY }) 221 | store.set(id, data) 222 | } 223 | 224 | const NodeContextMenu = component 225 | ({ 226 | mount() 227 | { 228 | this.handleItemClick = this.handleItemClick.bind(this) 229 | this.handleOuterClick = this.handleOuterClick.bind(this) 230 | }, 231 | handleItemClick(e) 232 | { 233 | this["action_" + e.currentTarget.dataset.action].call(this) 234 | store.set("editor/nodeContextID", null) 235 | }, 236 | handleOuterClick(e) 237 | { 238 | if (e.button == 1) 239 | return 240 | e.preventDefault() 241 | store.set("editor/nodeContextID", null) 242 | }, 243 | menuitem(name, action) 244 | { 245 | if (action) 246 | evoid("menuitem", { "data-action": action, "onclick": this.handleItemClick }).element.textContent = name 247 | else 248 | evoid("menuitem", { "class": "inactive" }).element.textContent = name 249 | }, 250 | render() 251 | { 252 | if (store.get("editor/nodeContextID")) 253 | { 254 | const node = nodeMap[this.$value] 255 | const nt = nodeTypes[node.type] 256 | const name = resolveString(nt.name, node) 257 | var canCD = node.type != "output" 258 | evoid("menubgr", { "class": "bgr", onpointerdown: this.handleOuterClick }) 259 | var el = eopen("menuwindow", { id: "nodeContextMenu" }).element 260 | el.style.left = store.get("contextMenuPos/x") + "px" 261 | el.style.top = store.get("contextMenuPos/y") + "px" 262 | evoid("menulabel").element.textContent = `Node #${this.$value} - ${name}` 263 | if (canCD) 264 | { 265 | this.menuitem("Delete", "delete") 266 | this.menuitem("Duplicate", "duplicate") 267 | } 268 | else 269 | { 270 | this.menuitem("Cannot delete '" + nt.name + "'") 271 | this.menuitem("Cannot duplicate '" + nt.name + "'") 272 | } 273 | this.menuitem("Copy shader to clipboard", "copyshader") 274 | eclose("menuwindow") 275 | } 276 | }, 277 | 278 | action_delete() 279 | { 280 | nodeDelete(store.get("editor/nodeContextID")) 281 | }, 282 | action_duplicate() 283 | { 284 | var node = nodeMap[store.get("editor/nodeContextID")] 285 | var newNode = nodeConstruct(node.type, node) 286 | nodeInsertIntoCols(newNode, node.x, node.y + 1) 287 | }, 288 | action_copyshader() 289 | { 290 | const node = nodeMap[store.get("editor/nodeContextID")] 291 | const genSh = nodesGenerateShader(node) 292 | copyTextToClipboard(genSh.error ? genSh.error : genSh.fshader) 293 | }, 294 | }) 295 | 296 | const NodeCol = component 297 | ({ 298 | render() 299 | { 300 | var col = this.$value 301 | eopen("div", { "class": "col", "data-col": this.colID, id: `node_col_${this.colID}` }) 302 | for (var j = 0; j < col.length; ++j) 303 | { 304 | var id = col[j] 305 | cvoid(Node, { bind: `nodeMap/${id}` }) 306 | } 307 | eclose("div") 308 | }, 309 | }) 310 | 311 | const NodeCols = component 312 | ({ 313 | render() 314 | { 315 | for (var i = 0; i < this.$value.length; ++i) 316 | { 317 | cvoid(NodeCol, { colID: i, bind: `${this.bind}/${i}` }) 318 | } 319 | }, 320 | }) 321 | 322 | function OnEditAreaLayoutChange() 323 | { 324 | onNodeLayoutChange() 325 | queueRedrawDragCanvas() 326 | } 327 | window.addEventListener("resize", OnEditAreaLayoutChange) 328 | 329 | const NodeEditArea = component 330 | ({ 331 | render() 332 | { 333 | eopen("div", { "class": "area eaBlurred customScroll", "id": "editArea", onscroll: OnEditAreaLayoutChange, onpointermove: Drag_onPointerMove }) 334 | cvoid(NodeCols, { bind: `functions/${funcGetCurName()}/nodeCols` }) 335 | eclose("div") 336 | evoid("canvas", { "class": "overlay eaBlurred", id: "curveOverlay" }) 337 | cvoid(NodeDragOverlay) 338 | cvoid(NodeContextMenu, { bind: "editor/nodeContextID" }) 339 | }, 340 | }) 341 | 342 | 343 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Shader Editor prototype 2 | 3 | [Online Demo](http://archo.work/ShaderEditorHTML5/shaderedit.html) 4 | 5 | Special features: 6 | 7 | - expression editing 8 | - automatic layout 9 | 10 | Regular features: 11 | 12 | - realtime preview of any node 13 | - PBR (metallic/roughness) shader 14 | - texture, math and function nodes 15 | - autocomplete resource selection 16 | - calculator for entered values 17 | - some sample textures are included 18 | 19 | Some GIFs: 20 | 21 | ![Expression editing 1](http://archo.work/ShaderEditorHTML5/gifs/se_expredit.gif) 22 | ![Expression editing 2](http://archo.work/ShaderEditorHTML5/gifs/se_expredit2.gif) 23 | 24 | ![Pick resource](http://archo.work/ShaderEditorHTML5/gifs/se_pickres.gif) 25 | ![Calculator](http://archo.work/ShaderEditorHTML5/gifs/se_typecalc.gif) 26 | 27 | Credits: 28 | 29 | - main programming & UI design: [Arvīds Kokins](http://archo.work/) 30 | - Editor: 31 | - [UI framework: Wabi](https://github.com/tenjou/wabi) 32 | - [Noise generator](https://github.com/josephg/noisejs) 33 | - [Font Awesome](https://fontawesome.com/) 34 | - Resources: 35 | - [Cubemap: Venice Dawn 2 from HDRI Haven](https://hdrihaven.com/hdri/?c=urban&h=venice_dawn_2) 36 | - Cubemap converter: 37 | - [stb_image.h](https://github.com/nothings/stb) 38 | - [tiny_jpeg.h](https://github.com/serge-rgb/TinyJPEG) 39 | -------------------------------------------------------------------------------- /RenderUtils.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function v3dot(a, b) 5 | { 6 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] 7 | } 8 | function v3cross(a, b) 9 | { 10 | return [ 11 | a[1] * b[2] - b[1] * a[2], 12 | a[2] * b[0] - b[2] * a[0], 13 | a[0] * b[1] - b[0] * a[1], 14 | ] 15 | } 16 | function v3scale(v, s) 17 | { 18 | return [v[0] * s, v[1] * s, v[2] * s] 19 | } 20 | function v3normalize(v) 21 | { 22 | var len = v3dot(v, v) 23 | if (len == 0) 24 | return v 25 | return v3scale(v, 1.0 / Math.sqrt(len)) 26 | } 27 | function v3sub(a, b) 28 | { 29 | return [a[0] - b[0], a[1] - b[1], a[2] - b[2]] 30 | } 31 | function lookAtMatrix(eye, at, up) 32 | { 33 | var zaxis = v3normalize(v3sub(at, eye)) 34 | var xaxis = v3normalize(v3cross(up, zaxis)) 35 | var yaxis = v3cross(zaxis, xaxis) 36 | return [ 37 | xaxis[0], xaxis[1], xaxis[2], -v3dot(xaxis, eye), 38 | yaxis[0], yaxis[1], yaxis[2], -v3dot(yaxis, eye), 39 | zaxis[0], zaxis[1], zaxis[2], -v3dot(zaxis, eye), 40 | 0, 0, 0, 1, 41 | ] 42 | } 43 | function lookAtMatrixInv(eye, at, up) 44 | { 45 | var zaxis = v3normalize(v3sub(at, eye)) 46 | var xaxis = v3normalize(v3cross(up, zaxis)) 47 | var yaxis = v3cross(zaxis, xaxis) 48 | return [ 49 | xaxis[0], yaxis[0], -zaxis[0], eye[0], 50 | xaxis[1], yaxis[1], -zaxis[1], eye[1], 51 | xaxis[2], yaxis[2], -zaxis[2], eye[2], 52 | 0, 0, 0, 1, 53 | ] 54 | } 55 | function perspectiveMatrix(fovY, aspect, znear, zfar) 56 | { 57 | fovY *= Math.PI / 180 58 | const yScale = 1.0 / Math.tan(fovY / 2) 59 | const xScale = yScale / aspect 60 | return [ 61 | xScale, 0, 0, 0, 62 | 0, yScale, 0, 0, 63 | 0, 0, zfar / (zfar - znear), -znear * zfar / (zfar - znear), 64 | 0, 0, 1, 0, 65 | ] 66 | } 67 | function multiplyMatrices(a, b) 68 | { 69 | const out = [] 70 | for (var y = 0; y < 4; ++y) 71 | { 72 | for (var x = 0; x < 4; ++x) 73 | { 74 | var sum = 0 75 | for (var i = 0; i < 4; ++i) 76 | { 77 | sum += a[y * 4 + i] * b[i * 4 + x] 78 | } 79 | out.push(sum) 80 | } 81 | } 82 | return out 83 | } 84 | function transposeMatrix(m) 85 | { 86 | const out = new Array(16) 87 | for (var y = 0; y < 4; ++y) 88 | { 89 | for (var x = 0; x < 4; ++x) 90 | { 91 | out[y * 4 + x] = m[x * 4 + y] 92 | } 93 | } 94 | return out 95 | } 96 | function invertMatrix(m) 97 | { 98 | const inv = new Array(16) 99 | 100 | inv[0] = m[5] * m[10] * m[15] - 101 | m[5] * m[11] * m[14] - 102 | m[9] * m[6] * m[15] + 103 | m[9] * m[7] * m[14] + 104 | m[13] * m[6] * m[11] - 105 | m[13] * m[7] * m[10] 106 | 107 | inv[4] = -m[4] * m[10] * m[15] + 108 | m[4] * m[11] * m[14] + 109 | m[8] * m[6] * m[15] - 110 | m[8] * m[7] * m[14] - 111 | m[12] * m[6] * m[11] + 112 | m[12] * m[7] * m[10] 113 | 114 | inv[8] = m[4] * m[9] * m[15] - 115 | m[4] * m[11] * m[13] - 116 | m[8] * m[5] * m[15] + 117 | m[8] * m[7] * m[13] + 118 | m[12] * m[5] * m[11] - 119 | m[12] * m[7] * m[9] 120 | 121 | inv[12] = -m[4] * m[9] * m[14] + 122 | m[4] * m[10] * m[13] + 123 | m[8] * m[5] * m[14] - 124 | m[8] * m[6] * m[13] - 125 | m[12] * m[5] * m[10] + 126 | m[12] * m[6] * m[9] 127 | 128 | inv[1] = -m[1] * m[10] * m[15] + 129 | m[1] * m[11] * m[14] + 130 | m[9] * m[2] * m[15] - 131 | m[9] * m[3] * m[14] - 132 | m[13] * m[2] * m[11] + 133 | m[13] * m[3] * m[10] 134 | 135 | inv[5] = m[0] * m[10] * m[15] - 136 | m[0] * m[11] * m[14] - 137 | m[8] * m[2] * m[15] + 138 | m[8] * m[3] * m[14] + 139 | m[12] * m[2] * m[11] - 140 | m[12] * m[3] * m[10] 141 | 142 | inv[9] = -m[0] * m[9] * m[15] + 143 | m[0] * m[11] * m[13] + 144 | m[8] * m[1] * m[15] - 145 | m[8] * m[3] * m[13] - 146 | m[12] * m[1] * m[11] + 147 | m[12] * m[3] * m[9] 148 | 149 | inv[13] = m[0] * m[9] * m[14] - 150 | m[0] * m[10] * m[13] - 151 | m[8] * m[1] * m[14] + 152 | m[8] * m[2] * m[13] + 153 | m[12] * m[1] * m[10] - 154 | m[12] * m[2] * m[9] 155 | 156 | inv[2] = m[1] * m[6] * m[15] - 157 | m[1] * m[7] * m[14] - 158 | m[5] * m[2] * m[15] + 159 | m[5] * m[3] * m[14] + 160 | m[13] * m[2] * m[7] - 161 | m[13] * m[3] * m[6] 162 | 163 | inv[6] = -m[0] * m[6] * m[15] + 164 | m[0] * m[7] * m[14] + 165 | m[4] * m[2] * m[15] - 166 | m[4] * m[3] * m[14] - 167 | m[12] * m[2] * m[7] + 168 | m[12] * m[3] * m[6] 169 | 170 | inv[10] = m[0] * m[5] * m[15] - 171 | m[0] * m[7] * m[13] - 172 | m[4] * m[1] * m[15] + 173 | m[4] * m[3] * m[13] + 174 | m[12] * m[1] * m[7] - 175 | m[12] * m[3] * m[5] 176 | 177 | inv[14] = -m[0] * m[5] * m[14] + 178 | m[0] * m[6] * m[13] + 179 | m[4] * m[1] * m[14] - 180 | m[4] * m[2] * m[13] - 181 | m[12] * m[1] * m[6] + 182 | m[12] * m[2] * m[5] 183 | 184 | inv[3] = -m[1] * m[6] * m[11] + 185 | m[1] * m[7] * m[10] + 186 | m[5] * m[2] * m[11] - 187 | m[5] * m[3] * m[10] - 188 | m[9] * m[2] * m[7] + 189 | m[9] * m[3] * m[6] 190 | 191 | inv[7] = m[0] * m[6] * m[11] - 192 | m[0] * m[7] * m[10] - 193 | m[4] * m[2] * m[11] + 194 | m[4] * m[3] * m[10] + 195 | m[8] * m[2] * m[7] - 196 | m[8] * m[3] * m[6] 197 | 198 | inv[11] = -m[0] * m[5] * m[11] + 199 | m[0] * m[7] * m[9] + 200 | m[4] * m[1] * m[11] - 201 | m[4] * m[3] * m[9] - 202 | m[8] * m[1] * m[7] + 203 | m[8] * m[3] * m[5] 204 | 205 | inv[15] = m[0] * m[5] * m[10] - 206 | m[0] * m[6] * m[9] - 207 | m[4] * m[1] * m[10] + 208 | m[4] * m[2] * m[9] + 209 | m[8] * m[1] * m[6] - 210 | m[8] * m[2] * m[5] 211 | 212 | const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12] 213 | 214 | if (det == 0) 215 | return null 216 | 217 | const invdet = 1.0 / det 218 | 219 | return inv.map((x) => x * invdet) 220 | } 221 | 222 | 223 | 224 | const VertexDecl_P2UV2 = 225 | { 226 | vertexSize: 16, 227 | attribs: 228 | { 229 | aPosition: [2, 0], 230 | aTexCoord0: [2, 8], 231 | }, 232 | } 233 | 234 | const VertexDecl_P3N3T4UV2 = 235 | { 236 | vertexSize: (3+3+4+2)*4, 237 | attribs: 238 | { 239 | aPosition: [3, 0], 240 | aNormal: [3, 12], 241 | aTangent: [4, 24], 242 | aTexCoord0: [2, 40], 243 | }, 244 | } 245 | 246 | function createTriangleMesh(verts, indices, vertexDecl) 247 | { 248 | const gl = window.GLCtx 249 | 250 | const VB = gl.createBuffer() 251 | gl.bindBuffer(gl.ARRAY_BUFFER, VB) 252 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW) 253 | 254 | const IB = gl.createBuffer() 255 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, IB) 256 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW) 257 | 258 | return { VB: VB, IB: IB, indexCount: indices.length, vertexDecl: vertexDecl } 259 | } 260 | 261 | function applyVertexAttrib(mesh, prog, name) 262 | { 263 | const gl = window.GLCtx 264 | const pos = gl.getAttribLocation(prog, name) 265 | if (pos == -1) 266 | return 267 | 268 | var vdInfo = mesh.vertexDecl.attribs[name] 269 | if (vdInfo) 270 | { 271 | gl.vertexAttribPointer(pos, vdInfo[0], gl.FLOAT, false, mesh.vertexDecl.vertexSize, vdInfo[1]) 272 | gl.enableVertexAttribArray(pos) 273 | } 274 | else 275 | gl.disableVertexAttribArray(pos) 276 | } 277 | 278 | function drawTriangleMesh(mesh, prog) 279 | { 280 | const gl = window.GLCtx 281 | 282 | gl.bindBuffer(gl.ARRAY_BUFFER, mesh.VB) 283 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.IB) 284 | 285 | applyVertexAttrib(mesh, prog, "aPosition") 286 | applyVertexAttrib(mesh, prog, "aNormal") 287 | applyVertexAttrib(mesh, prog, "aTangent") 288 | applyVertexAttrib(mesh, prog, "aTexCoord0") 289 | 290 | gl.drawElements(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0) 291 | } 292 | 293 | function createQuadMesh() 294 | { 295 | const posTexData = 296 | [ 297 | -1.0, -1.0, 0.0, 0.0, 298 | 1.0, -1.0, 1.0, 0.0, 299 | -1.0, 1.0, 0.0, 1.0, 300 | 1.0, 1.0, 1.0, 1.0, 301 | ] 302 | return createTriangleMesh(posTexData, [0, 1, 2, 2, 1, 3], VertexDecl_P2UV2) 303 | } 304 | 305 | function createSphereMesh(hparts, vparts) 306 | { 307 | hparts = hparts || 32 308 | vparts = vparts || 32 309 | 310 | const verts = [] // px, py, pz, nx, ny, nz, tx, ty, tz, ts, uvx, uvy 311 | const indices = [] 312 | 313 | for (var h = 0; h <= hparts; ++h) 314 | { 315 | const hq1 = h / hparts 316 | const hdir1x = Math.cos(hq1 * Math.PI * 2) 317 | const hdir1y = Math.sin(hq1 * Math.PI * 2) 318 | for (var v = 0; v <= vparts; ++v) 319 | { 320 | const vq1 = v / vparts 321 | const cv1 = Math.cos((vq1 * 2 - 1) * Math.PI * 0.5) 322 | const sv1 = Math.sin((vq1 * 2 - 1) * Math.PI * 0.5) 323 | const dir1x = hdir1x * cv1 324 | const dir1y = hdir1y * cv1 325 | const dir1z = sv1 326 | const tan_v3 = v3normalize(v3cross([hdir1x, hdir1y, 0], [0, 0, 1])) 327 | verts.push( 328 | dir1x, dir1y, dir1z, // position 329 | dir1x, dir1y, dir1z, // normal 330 | tan_v3[0], tan_v3[1], tan_v3[2], 1, // tangent 331 | hq1, 1 - vq1 // texcoord 332 | ) 333 | } 334 | } 335 | for (var h = 0; h < hparts; ++h) 336 | { 337 | const h1 = h + 1 338 | for (var v = 0; v < vparts; ++v) 339 | { 340 | const v1 = v + 1 341 | const i1 = v + h * (vparts + 1) 342 | const i2 = v + h1 * (vparts + 1) 343 | const i4 = v1 + h * (vparts + 1) 344 | const i3 = v1 + h1 * (vparts + 1) 345 | indices.push(i1, i4, i3, i3, i2, i1) 346 | } 347 | } 348 | 349 | return createTriangleMesh(verts, indices, VertexDecl_P3N3T4UV2) 350 | } 351 | 352 | 353 | 354 | function loadCubemap(url) 355 | { 356 | const gl = window.GLCtx 357 | const glCubeEnums = 358 | [ 359 | gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 360 | gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 361 | gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 362 | ] 363 | const glCX = [0, 1, 0, 1, 0, 1] 364 | const glCY = [0, 0, 1, 1, 2, 2] 365 | 366 | const texture = gl.createTexture() 367 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture) 368 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) 369 | for (var i = 0; i < glCubeEnums.length; ++i) 370 | gl.texImage2D(glCubeEnums[i], 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([105, 120, 139, 255])) 371 | 372 | function cubeUpload(img) 373 | { 374 | // cubemap image is 2*N x 3*N 375 | // layout: 376 | // +X -X 377 | // +Y -Y 378 | // +Z -Z 379 | var tmpCanvas = document.createElement("canvas") 380 | tmpCanvas.width = img.width 381 | tmpCanvas.height = img.height 382 | document.body.appendChild(tmpCanvas) 383 | var tmpCtx = tmpCanvas.getContext("2d") 384 | tmpCtx.drawImage(img, 0, 0) 385 | var xsz = (img.width / 2) | 0 386 | var ysz = (img.height / 3) | 0 387 | 388 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture) 389 | for (var i = 0; i < glCubeEnums.length; ++i) 390 | gl.texImage2D(glCubeEnums[i], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, tmpCtx.getImageData(glCX[i] * xsz, glCY[i] * ysz, xsz, ysz)) 391 | 392 | gl.generateMipmap(gl.TEXTURE_CUBE_MAP) 393 | document.body.removeChild(tmpCanvas) 394 | } 395 | 396 | //* 397 | const image = new Image() 398 | image.onload = function() 399 | { 400 | cubeUpload(image) 401 | image.onload = function() 402 | { 403 | cubeUpload(image) 404 | } 405 | image.src = url + ".jpg" 406 | } 407 | image.src = url + ".preload.jpg" 408 | //*/ 409 | /* 410 | const xhr = new XMLHttpRequest 411 | xhr.responseType = "arraybuffer" 412 | xhr.onreadystatechange = function() 413 | { 414 | if (this.readyState == 4 && this.status == 200) 415 | { 416 | const image = new Image() 417 | image.src = "data:image/jpeg;base64:" + atob(xhr.responseText) 418 | 419 | // cubemap image is 2*N x 3*N 420 | // layout: 421 | // +X -X 422 | // +Y -Y 423 | // +Z -Z 424 | var tmpCanvas = document.createElement("canvas") 425 | tmpCanvas.width = image.width 426 | tmpCanvas.height = image.height 427 | document.body.appendChild(tmpCanvas) 428 | var tmpCtx = tmpCanvas.getContext("2d") 429 | tmpCtx.drawImage(image, 0, 0) 430 | var xsz = (image.width / 2) | 0 431 | var ysz = (image.height / 2) | 0 432 | 433 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture) 434 | for (var i = 0; i < glCubeEnums.length; ++i) 435 | gl.texImage2D(glCubeEnums[i], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, tmpCtx.getImageData(glCX[i] * xsz, glCY[i] * ysz, xsz, ysz)) 436 | 437 | gl.generateMipmap(gl.TEXTURE_CUBE_MAP) 438 | document.body.removeChild(tmpCanvas) 439 | } 440 | } 441 | xhr.open("GET", url, true) 442 | xhr.send() 443 | //*/ 444 | return texture 445 | } 446 | 447 | 448 | -------------------------------------------------------------------------------- /Rendering.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | const textureCache = {} 5 | function TextureCache_get(name) 6 | { 7 | var tex = textureCache[name] 8 | if (tex) 9 | return tex 10 | 11 | const info = nodeResources.sampler2D[name] 12 | const data = info.genFunc() 13 | 14 | const gl = window.GLCtx 15 | tex = gl.createTexture() 16 | gl.bindTexture(gl.TEXTURE_2D, tex) 17 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) 18 | 19 | if (typeof data === "string") 20 | { 21 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([100, 100, 100, 255])) 22 | const image = new Image() 23 | image.onload = function() 24 | { 25 | gl.bindTexture(gl.TEXTURE_2D, tex) 26 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) 27 | gl.generateMipmap(gl.TEXTURE_2D) 28 | } 29 | image.src = data 30 | } 31 | else 32 | { 33 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, data.width, data.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(data.data.buffer)) 34 | gl.generateMipmap(gl.TEXTURE_2D) 35 | } 36 | 37 | textureCache[name] = tex 38 | return tex 39 | } 40 | 41 | function TextureCache_getCubemapInternal(url) 42 | { 43 | var tex = textureCache[url] 44 | if (tex) 45 | return tex 46 | tex = loadCubemap(url) 47 | textureCache[url] = tex 48 | return tex 49 | } 50 | 51 | function loadShader(type, source) 52 | { 53 | const gl = window.GLCtx 54 | const shader = gl.createShader(type) 55 | gl.shaderSource(shader, source) 56 | gl.compileShader(shader) 57 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) 58 | { 59 | console.log("[GL] shader compilation failed: " + gl.getShaderInfoLog(shader)) 60 | gl.deleteShader(shader) 61 | return null 62 | } 63 | return shader 64 | } 65 | 66 | function initShaderProgram(vsSource, fsSource) 67 | { 68 | const gl = window.GLCtx 69 | const vertexShader = loadShader(gl.VERTEX_SHADER, vsSource) 70 | const fragmentShader = loadShader(gl.FRAGMENT_SHADER, fsSource) 71 | const shaderProgram = gl.createProgram() 72 | gl.attachShader(shaderProgram, vertexShader) 73 | gl.attachShader(shaderProgram, fragmentShader) 74 | gl.linkProgram(shaderProgram) 75 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) 76 | { 77 | console.log("[GL] shader program linkage failed: " + gl.getProgramInfoLog(shaderProgram)) 78 | return null 79 | } 80 | return shaderProgram 81 | } 82 | 83 | const shaderCache = {} 84 | function ShaderCache_getProgram(vsSource, fsSource) 85 | { 86 | const key = vsSource + fsSource 87 | const pinfo = shaderCache[key] 88 | if (pinfo) 89 | { 90 | pinfo.time = Date.now() 91 | return pinfo.program 92 | } 93 | 94 | const p = initShaderProgram(vsSource, fsSource) 95 | shaderCache[key] = { time: Date.now(), program: p } 96 | return p 97 | } 98 | 99 | function ShaderCache_GC(expirationTimeSec) 100 | { 101 | const toClean = {} 102 | const now = Date.now() 103 | for (var key in shaderCache) 104 | { 105 | var pinfo = shaderCache[key] 106 | if (now - pinfo.time > expirationTimeSec * 1000) 107 | toClean[key] = true 108 | } 109 | var num = 0 110 | for (var key in toClean) 111 | { 112 | num++ 113 | delete shaderCache[key] 114 | } 115 | if (num) 116 | console.log(`[ShaderCache GC] dropped ${num} items`) 117 | } 118 | 119 | function initGL() 120 | { 121 | window.PreviewStartTime = Date.now() 122 | const gl = window.GLCtx 123 | 124 | gl.getExtension("OES_standard_derivatives") 125 | gl.getExtension("EXT_shader_texture_lod") 126 | 127 | gl.enable(gl.CULL_FACE); 128 | gl.cullFace(gl.BACK); 129 | gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE_MINUS_DST_ALPHA, gl.ONE) 130 | 131 | window.GLQuadMesh = createQuadMesh() 132 | window.GLSphereMesh = createSphereMesh() 133 | } 134 | 135 | window.GLCameraDist = 3 136 | window.GLCameraYaw = 160 137 | window.GLCameraPitch = 75 138 | window.GLCameraPos = [-2.5, 1.5, 0] 139 | window.GLCameraTarget = [0, 0, 0] 140 | 141 | function shaderProgramLinkInputs(p, info, asp) 142 | { 143 | const gl = window.GLCtx 144 | gl.useProgram(p) 145 | 146 | if (info.uniform.time) 147 | { 148 | const time = (Date.now() - window.PreviewStartTime) / 1000 149 | gl.uniform1f(gl.getUniformLocation(p, "time"), time) 150 | } 151 | if (info.uniform.uCameraPos) 152 | { 153 | gl.uniform3fv(gl.getUniformLocation(p, "uCameraPos"), GLCameraPos) 154 | } 155 | if (info.uniform.uViewProjMatrix) 156 | { 157 | const vm = lookAtMatrix(GLCameraPos, GLCameraTarget, [0, 0, 1]) 158 | const pm = perspectiveMatrix(60, asp, 0.001, 1000) 159 | const vpm = multiplyMatrices(pm, vm) 160 | gl.uniformMatrix4fv(gl.getUniformLocation(p, "uViewProjMatrix"), false, vpm) 161 | } 162 | if (info.uniform.uInvViewMatrix) 163 | { 164 | const vm = lookAtMatrix(GLCameraPos, GLCameraTarget, [0, 0, 1]) 165 | const ivm = invertMatrix(vm) 166 | gl.uniformMatrix4fv(gl.getUniformLocation(p, "uInvViewMatrix"), false, ivm) 167 | } 168 | if (info.uniform.uProjMatrix) 169 | { 170 | const pm = perspectiveMatrix(60, asp, 0.001, 1000) 171 | gl.uniformMatrix4fv(gl.getUniformLocation(p, "uProjMatrix"), false, pm) 172 | } 173 | 174 | var sid = 0 175 | for (var key in info.sampler2D) 176 | { 177 | gl.uniform1i(gl.getUniformLocation(p, key), sid) 178 | var tex = TextureCache_get(key) 179 | gl.activeTexture(gl.TEXTURE0 + sid) 180 | gl.bindTexture(gl.TEXTURE_2D, tex) 181 | sid++ 182 | } 183 | 184 | for (var key in info.samplerCube) 185 | { 186 | var tex 187 | if (key == "sCubemap") 188 | tex = TextureCache_getCubemapInternal("cubemaps/cubemap") 189 | gl.uniform1i(gl.getUniformLocation(p, key), sid) 190 | gl.activeTexture(gl.TEXTURE0 + sid) 191 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex) 192 | sid++ 193 | } 194 | } 195 | 196 | function drawOnePreview(nodeID, aspect, drawBgr) 197 | { 198 | const gl = window.GLCtx 199 | 200 | if (drawBgr) 201 | { 202 | var shBgr = 203 | { 204 | vshader: shader_cubeBgrVS, 205 | fshader: shader_cubeBgrFS, 206 | uniform: { uInvViewMatrix: true, uProjMatrix: true }, 207 | samplerCube: { sCubemap: true } 208 | } 209 | const progBgr = ShaderCache_getProgram(shBgr.vshader, shBgr.fshader) 210 | shaderProgramLinkInputs(progBgr, shBgr, aspect) 211 | drawTriangleMesh(GLQuadMesh, progBgr) 212 | } 213 | 214 | gl.enable(gl.BLEND) 215 | const genSh = nodesGenerateShader(nodeMap[nodeID]) 216 | const prog = ShaderCache_getProgram(genSh.vshader, genSh.fshader) 217 | shaderProgramLinkInputs(prog, genSh, aspect) 218 | drawTriangleMesh(GLSphereMesh, prog) 219 | gl.disable(gl.BLEND) 220 | } 221 | 222 | function redrawPreviews() 223 | { 224 | const nodePreviewCanvas = document.getElementsByClassName("nodePreviewCanvas") 225 | var minw = 0, minh = 0 226 | for (var i = 0; i < nodePreviewCanvas.length; ++i) 227 | { 228 | // all preview canvas are assumed to be equal in size 229 | const pnc = nodePreviewCanvas[i] 230 | minw = pnc.offsetWidth 231 | minh = pnc.offsetHeight 232 | break 233 | } 234 | 235 | const canvas = document.getElementById("mainPreview") 236 | const parent = canvas.parentElement 237 | var tw = parent.offsetWidth 238 | var th = parent.offsetHeight 239 | if (tw < minw) 240 | tw = minw 241 | if (th < minh) 242 | th = minh 243 | 244 | if (canvas.width != tw) 245 | canvas.width = tw 246 | if (canvas.height != th) 247 | canvas.height = th 248 | 249 | var gl = window.GLCtx 250 | if (typeof gl === "undefined") 251 | { 252 | gl = canvas.getContext("webgl") 253 | if (!gl) 254 | gl = false 255 | window.GLCtx = gl 256 | if (gl) 257 | initGL() 258 | } 259 | 260 | requestAnimationFrame(redrawPreviews) 261 | 262 | if (!gl) 263 | { 264 | var ctx = canvas.getContext("2d") 265 | 266 | ctx.fillStyle = "#EEE" 267 | ctx.font = "32px sans-serif" 268 | ctx.fillText("WebGL is unavailable", 32, 64) 269 | ctx.fillText("Please restart your browser or try another one", 32, 96+16) 270 | return 271 | } 272 | 273 | var yaw = GLCameraYaw * Math.PI / 180 274 | var pitch = GLCameraPitch * Math.PI / 180 275 | var dist = GLCameraDist 276 | window.GLCameraPos = [Math.cos(yaw) * Math.sin(pitch) * dist, Math.sin(yaw) * Math.sin(pitch) * dist, Math.cos(pitch) * dist] 277 | 278 | gl.clearColor(0, 0, 0, 0) 279 | if (minw && minh) 280 | { 281 | // calculate batch size 282 | const numX = Math.floor(tw / minw) 283 | const numY = Math.floor(th / minh) 284 | const numBatch = numX * numY 285 | 286 | for (var batchOff = 0; batchOff < nodePreviewCanvas.length; batchOff += numBatch) 287 | { 288 | gl.clear(gl.COLOR_BUFFER_BIT) 289 | for (var i = batchOff; i < Math.min(nodePreviewCanvas.length, batchOff + numBatch); ++i) 290 | { 291 | const pnc = nodePreviewCanvas[i] 292 | const w = pnc.offsetWidth 293 | const h = pnc.offsetHeight 294 | const inBatch = i - batchOff 295 | const bx = inBatch % numX 296 | const by = Math.floor(inBatch / numX) 297 | 298 | var id 299 | for (var el = pnc; el; el = el.parentElement) 300 | { 301 | id = el.dataset.id 302 | if (id) 303 | break 304 | } 305 | 306 | gl.viewport(bx * w, canvas.height - h * (1 + by), w, h) 307 | drawOnePreview(id, w / h, false) 308 | } 309 | 310 | for (var i = batchOff; i < Math.min(nodePreviewCanvas.length, batchOff + numBatch); ++i) 311 | { 312 | const pnc = nodePreviewCanvas[i] 313 | const w = pnc.offsetWidth 314 | const h = pnc.offsetHeight 315 | const inBatch = i - batchOff 316 | const bx = inBatch % numX 317 | const by = Math.floor(inBatch / numX) 318 | 319 | const tctx = pnc.getContext("2d") 320 | tctx.clearRect(0, 0, w, h) 321 | tctx.drawImage(canvas, bx * w, by * h, w, h, 0, 0, w, h) 322 | } 323 | } 324 | } 325 | 326 | gl.clear(gl.COLOR_BUFFER_BIT) 327 | gl.viewport(0, 0, canvas.width, canvas.height) 328 | 329 | drawOnePreview(funcGetCurData().outputNode, canvas.width / canvas.height, true) 330 | } 331 | 332 | 333 | -------------------------------------------------------------------------------- /Shaders.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function shGenVal(dims, value) 5 | { 6 | return type2glsl[dims] + "(" + (value ? value : defArg).slice(0, dims).join(",") + ")" 7 | } 8 | 9 | function _shAdjustDims(code, srcDims, tgtDims, swizzle) 10 | { 11 | if (tgtDims > srcDims) 12 | { 13 | if (srcDims == 1) 14 | { 15 | var out = type2glsl[tgtDims] + "(" 16 | for (var i = 0; i < tgtDims; ++i) 17 | { 18 | if (i) 19 | out += "," 20 | out += code 21 | } 22 | code = out + ")" 23 | } 24 | else 25 | { 26 | code = type2glsl[tgtDims] + "(" + code 27 | while (srcDims < tgtDims) 28 | code += "," + defArg[srcDims++] 29 | code += ")" 30 | if (swizzle) 31 | code += "." + swizzle.substring(0, tgtDims) 32 | } 33 | } 34 | else if (tgtDims < srcDims) 35 | { 36 | code = "(" + code + ")." + (swizzle || "xyzw").substring(0, tgtDims) 37 | } 38 | else 39 | { 40 | if (swizzle) 41 | code += "." + swizzle.substring(0, tgtDims) 42 | } 43 | return code 44 | } 45 | 46 | function _shGenArg(node, argNum) 47 | { 48 | const arg = node.args[argNum] 49 | const tgtArgDims = nodeArgGetDimsResolved(node, argNum) 50 | if (arg.node) 51 | { 52 | var argNode = nodeMap[arg.node] 53 | var ant = nodeTypes[argNode.type] 54 | var srcArgDims = ant.getRVDims(argNode) 55 | 56 | var outCode = ant.genInline ? ant.getCode(argNode) : `t_${arg.node}` 57 | outCode = _shAdjustDims(outCode, srcArgDims, tgtArgDims, arg.swizzle) 58 | return outCode 59 | } 60 | else if (arg.ed == "var") 61 | { 62 | var funcArgDims = funcArgGetDefDimsByName(node.func, arg.varName) 63 | var outCode = (funcArgDims && arg.varName + "_a") || arg.varName || "0.0" 64 | var srcArgDims = arg.varName ? funcArgDims || nodeResources.variable[arg.varName].dims : 1 65 | outCode = _shAdjustDims(outCode, srcArgDims, tgtArgDims, arg.swizzle) 66 | return outCode 67 | } 68 | else 69 | { 70 | if (arg.ed == "num1" || tgtArgDims == 1) 71 | { 72 | var out = type2glsl[tgtArgDims] + "(" 73 | for (var i = 0; i < tgtArgDims; ++i) 74 | { 75 | if (i) 76 | out += "," 77 | out += arg.value[0] 78 | } 79 | return out + ")" 80 | } 81 | return shGenVal(tgtArgDims, arg.value) 82 | } 83 | } 84 | 85 | function NodeShaderGen(startingFunc) 86 | { 87 | this.funcs = {} 88 | this.variable = {} 89 | this.uniform = {} 90 | this.sampler2D = {} 91 | this.samplerCube = {} 92 | this.funcsToProcess = [startingFunc] 93 | this.startingFunc = startingFunc 94 | } 95 | 96 | NodeShaderGen.prototype.gatherNodes = function(firstNode) 97 | { 98 | const fnData = funcGetData(firstNode.func) 99 | var funcNodes = [firstNode] 100 | this.funcs[firstNode.func] = { nodes: funcNodes } 101 | for (var i = 0; i < funcNodes.length; ++i) 102 | { 103 | const node = funcNodes[i] 104 | const argCount = nodeGetArgCount(node) 105 | for (var argNum = argCount; argNum > 0; ) 106 | { 107 | --argNum 108 | var arg = node.args[argNum] 109 | var anID = arg.node 110 | if (anID) 111 | funcNodes.push(nodeMap[anID]) 112 | else if (arg.ed == "var" && arg.varName && !funcArgGetDefDimsByName(firstNode.func, arg.varName)) 113 | { 114 | this.variable[arg.varName] = true 115 | } 116 | } 117 | 118 | if (node.type == "tex2D" && node.rsrc !== null) 119 | this.sampler2D[node.rsrc] = true 120 | if (node.type == "func" && node.rsrc !== null) 121 | { 122 | if (node.rsrc == this.startingFunc) 123 | throw "recursion is not allowed" 124 | this.funcsToProcess.push(node.rsrc) 125 | } 126 | } 127 | } 128 | 129 | NodeShaderGen.prototype.gatherFuncs = function() 130 | { 131 | const visited = {} 132 | for (var i = 1; i < this.funcsToProcess.length; ++i) 133 | { 134 | const func = this.funcsToProcess[i] 135 | if (visited[func]) 136 | continue 137 | visited[func] = true 138 | var fnData = funcGetData(func) 139 | if (fnData) 140 | this.gatherNodes(nodeMap[fnData.outputNode]) 141 | else 142 | { 143 | fnData = nodeResources.func[func] 144 | for (var key in fnData.variable) 145 | this.variable[key] = true 146 | for (var key in fnData.sampler2D) 147 | this.sampler2D[key] = true 148 | for (var key in fnData.samplerCube) 149 | this.samplerCube[key] = true 150 | } 151 | } 152 | } 153 | 154 | NodeShaderGen.prototype.generateGlobals = function(lines) 155 | { 156 | for (var key in this.variable) 157 | { 158 | const vi = nodeResources.variable[key] 159 | if (vi.type == "uniform") 160 | this.uniform[key] = true 161 | lines.push(`${vi.type} ${type2glsl[vi.dims]} ${key};`) 162 | } 163 | for (var key in this.sampler2D) 164 | lines.push(`uniform sampler2D ${key};`) 165 | for (var key in this.samplerCube) 166 | lines.push(`uniform samplerCube ${key};`) 167 | } 168 | 169 | NodeShaderGen.prototype.generateFunction = function(lines, name, nodes, retDims, args) 170 | { 171 | const argsStr = args.map((x) => `${type2glsl[x.dims]} ${x.name}_a`).join(", ") 172 | lines.push(`${type2glsl[retDims]} ${name}_f(${argsStr}) {`) 173 | 174 | const visited = {} 175 | for (var i = nodes.length; i > 0; ) 176 | { 177 | --i 178 | const node = nodes[i] 179 | 180 | if (visited[node.id]) 181 | continue 182 | visited[node.id] = true 183 | 184 | const nt = nodeTypes[node.type] 185 | 186 | if (!nt.genInline) 187 | { 188 | const dims = nt.getRVDims(node) 189 | const ty = type2glsl[dims] 190 | const src = nt.getCode(node) 191 | lines.push(`${ty} t_${node.id} = ${src};`) 192 | } 193 | 194 | if (i == 0) 195 | { 196 | const srcDims = nt.getRVDims(node) 197 | var retCode = nt.genInline ? nt.getCode(node) : `t_${node.id}` 198 | retCode = _shAdjustDims(retCode, srcDims, retDims, null) 199 | lines.push(`return ${retCode};`) 200 | } 201 | } 202 | 203 | lines.push("}") 204 | } 205 | 206 | NodeShaderGen.prototype.generateBuiltinFunction = function(lines, name) 207 | { 208 | const fnData = nodeResources.func[name] 209 | const argsStr = fnData.args.map((x) => `${type2glsl[x.dims]} ${x.name}`).join(", ") 210 | lines.push(`${type2glsl[fnData.retDims]} ${name}_f(${argsStr}) {`) 211 | lines.push(fnData.code) 212 | lines.push("}") 213 | } 214 | 215 | NodeShaderGen.prototype.generateAllFunctions = function(lines) 216 | { 217 | const visited = {} 218 | for (var i = this.funcsToProcess.length; i > 0; ) 219 | { 220 | i-- 221 | const func = this.funcsToProcess[i] 222 | if (visited[func]) 223 | continue 224 | visited[func] = true 225 | const fnData = funcGetData(func) 226 | if (fnData) 227 | { 228 | this.generateFunction( 229 | lines, 230 | func, 231 | this.funcs[func].nodes, 232 | fnData.retDims, 233 | fnData.args) 234 | } 235 | else 236 | { 237 | this.generateBuiltinFunction(lines, func) 238 | } 239 | if (i == 0) 240 | { 241 | var argsStr = fnData.args.map((x) => shGenVal(x.dims)).join(", ") 242 | var funcCall = `${this.startingFunc}_f(${argsStr})` 243 | funcCall = _shAdjustDims(funcCall, fnData.retDims, 4, null) 244 | lines.push(`void main() { gl_FragColor = ${funcCall}; }`) 245 | } 246 | } 247 | } 248 | 249 | function getVertexShader() 250 | { 251 | return ` 252 | uniform mat4 uViewProjMatrix; 253 | attribute vec3 aPosition; 254 | attribute vec3 aNormal; 255 | attribute vec4 aTangent; 256 | attribute vec2 aTexCoord0; 257 | varying vec3 vWorldPos; 258 | varying vec3 vWorldNormal; 259 | varying vec4 vWorldTangent; 260 | varying vec2 vTexCoord0; 261 | void main() 262 | { 263 | gl_Position = vec4(aPosition, 1.0) * uViewProjMatrix; 264 | vWorldPos = aPosition; 265 | vWorldNormal = aNormal; 266 | vWorldTangent = aTangent; 267 | vTexCoord0 = aTexCoord0; 268 | } 269 | ` 270 | } 271 | 272 | function nodesGenerateShader(outNode) 273 | { 274 | try 275 | { 276 | if (!outNode) 277 | throw "No output node" 278 | 279 | lines = 280 | [ 281 | "#extension GL_OES_standard_derivatives : enable", 282 | "#extension GL_EXT_shader_texture_lod : enable", 283 | "precision highp float;", 284 | ] 285 | 286 | var shaderGen = new NodeShaderGen(outNode.func) 287 | shaderGen.gatherNodes(outNode) 288 | shaderGen.gatherFuncs() 289 | shaderGen.generateGlobals(lines) 290 | shaderGen.generateAllFunctions(lines) 291 | 292 | //console.log(lines.join("\n")) 293 | return { 294 | vshader: getVertexShader(), 295 | fshader: lines.join("\n"), 296 | uniform: Object.assign(shaderGen.uniform, { uViewProjMatrix: true }), 297 | sampler2D: shaderGen.sampler2D, 298 | samplerCube: shaderGen.samplerCube, 299 | } 300 | } 301 | catch (err) 302 | { 303 | return { 304 | vshader: getVertexShader(), 305 | fshader: "precision highp float; void main() { gl_FragColor = vec4(0,0,0,1); }", 306 | uniform: {}, 307 | sampler2D: {}, 308 | samplerCube: {}, 309 | error: err, 310 | } 311 | } 312 | } 313 | 314 | 315 | 316 | const shader_cubeBgrVS = ` 317 | attribute vec2 aPosition; 318 | varying vec2 vProjPos; 319 | void main() 320 | { 321 | gl_Position = vec4(aPosition, 0.0, 1.0); 322 | vProjPos = aPosition; 323 | }` 324 | 325 | const shader_cubeBgrFS = ` 326 | precision highp float; 327 | varying vec2 vProjPos; 328 | uniform mat4 uInvViewMatrix; 329 | uniform mat4 uProjMatrix; 330 | uniform samplerCube sCubemap; 331 | void main() 332 | { 333 | vec4 wp = (vProjPos.xyxy * vec4(1.0 / uProjMatrix[0][0], 1.0 / uProjMatrix[1][1], 0.0, 0.0) + vec4(0.0, 0.0, 1.0, 0.0)) * uInvViewMatrix; 334 | vec4 cmSample = textureCube(sCubemap, wp.xyz); 335 | vec3 outColor = cmSample.rgb; 336 | gl_FragColor = vec4(outColor, 1.0); 337 | } 338 | ` 339 | 340 | const shader_objTestVS = ` 341 | uniform mat4 uViewProjMatrix; 342 | attribute vec3 aPosition; 343 | attribute vec3 aNormal; 344 | varying vec3 vWorldPos; 345 | varying vec3 vWorldNormal; 346 | void main() 347 | { 348 | gl_Position = vec4(aPosition, 1.0) * uViewProjMatrix; 349 | vWorldPos = aPosition; 350 | vWorldNormal = aNormal; 351 | }` 352 | 353 | const shader_objTestFS = ` 354 | precision highp float; 355 | varying vec3 vWorldPos; 356 | varying vec3 vWorldNormal; 357 | uniform vec3 uCameraPos; 358 | uniform samplerCube sCubemap; 359 | void main() 360 | { 361 | gl_FragColor = textureCube(sCubemap, reflect(vWorldPos - uCameraPos, vWorldNormal)); 362 | } 363 | ` 364 | 365 | 366 | -------------------------------------------------------------------------------- /UIControls.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function copyTextToClipboard(text) 5 | { 6 | const textarea = document.createElement("textarea") 7 | textarea.value = text 8 | document.body.appendChild(textarea) 9 | textarea.select() 10 | // TODO check return value 11 | if (!document.execCommand("copy")) 12 | throw "failed to copy" 13 | document.body.removeChild(textarea) 14 | } 15 | 16 | 17 | 18 | function resolveString(strOrFn, arg) 19 | { 20 | if (typeof strOrFn === "string") 21 | return strOrFn; 22 | return strOrFn(arg); 23 | } 24 | 25 | 26 | 27 | const TextInput = component 28 | ({ 29 | mount() 30 | { 31 | this.handleChange = this.handleChange.bind(this) 32 | }, 33 | handleChange(e) 34 | { 35 | this.$value = e.currentTarget.value 36 | }, 37 | render() 38 | { 39 | evoid("input", { type: "text", onchange: this.handleChange }).element.value = this.$value 40 | }, 41 | }) 42 | 43 | //window.RIID = new Map() 44 | const RangeInput = component 45 | ({ 46 | mount() 47 | { 48 | // if (RIID.has(this)) 49 | // throw "Object was already created" 50 | // RIID.set(this, true) 51 | this.min = this.min || 0 52 | this.max = this.max || 100 53 | this.step = this.step || 1 54 | this.clicked = false 55 | this.handlePointerEvent = this.handlePointerEvent.bind(this) 56 | }, 57 | handlePointerEvent(e) 58 | { 59 | if (e.type == "pointerdown" && e.button == 0) 60 | { 61 | e.currentTarget.setPointerCapture(e.pointerId) 62 | this.clicked = true 63 | } 64 | if ((e.type == "pointerdown" && e.button == 0) || (e.type == "pointermove" && e.buttons & 0x1 && this.clicked)) 65 | { 66 | const r = e.currentTarget.getBoundingClientRect() 67 | var q = r.width > 2 ? (e.clientX - r.left - 2) / (r.width - 4) : 0 68 | q = Math.max(0, Math.min(1, q)) 69 | q = this.min * (1 - q) + this.max * q 70 | q = Math.round(q / this.step) * this.step 71 | this.$value = numToFloatStr(q, false) 72 | if (this.onedit) 73 | this.onedit() 74 | } 75 | if (e.type == "click" && e.button == 0) 76 | { 77 | this.clicked = false 78 | e.preventDefault() 79 | } 80 | }, 81 | render() 82 | { 83 | const el = eopen("RangeInput", { onpointerdown: this.handlePointerEvent, onpointermove: this.handlePointerEvent, onclick: this.handlePointerEvent }).element 84 | const r = el.getBoundingClientRect() 85 | const q = Math.max(0, Math.min(1, (this.$value - this.min) / (this.max - this.min))) 86 | evoid("RangeInputTrack", { "class": "track", style: this.trackStyle }) 87 | eopen("RangeInputThumbLimiter") 88 | evoid("RangeInputThumb", { "class": "thumb", style: { left: (q * 100) + "%" } }) 89 | eclose("RangeInputThumbLimiter") 90 | eclose("RangeInput") 91 | }, 92 | }) 93 | 94 | const NumberEdit = component 95 | ({ 96 | mount() 97 | { 98 | this.handleChange = this.handleChange.bind(this) 99 | this.handleRangeEdit = this.handleRangeEdit.bind(this) 100 | }, 101 | handleChange(e) 102 | { 103 | if (e.currentTarget.type == "text") 104 | this.$value = calculate(e.currentTarget.value) 105 | update(this) 106 | if (this.onedit) 107 | this.onedit() 108 | }, 109 | handleRangeEdit() 110 | { 111 | update(this) 112 | if (this.onedit) 113 | this.onedit() 114 | }, 115 | render() 116 | { 117 | evoid("input", { type: "text", onchange: this.handleChange }).element.value = this.$value 118 | cvoid(RangeInput, { bind: this.bind, onedit: this.handleRangeEdit, min: 0, max: 1, step: 0.001, trackStyle: this.trackStyle }) 119 | }, 120 | }) 121 | 122 | const ValueEdit = component 123 | ({ 124 | render() 125 | { 126 | eopen("div", { "class": "valueEdit" }) 127 | for (var i = 0; i < this.dims; ++i) 128 | { 129 | eopen("div", { "class": "row" }) 130 | eopen("label") 131 | eopen("span", { "class": "lbl" }) 132 | text(labelsXYZW[i]) 133 | eclose("span") 134 | cvoid(NumberEdit, { bind: `${this.bind}/${i}` }) 135 | eclose("label") 136 | eclose("div") 137 | } 138 | eclose("div") 139 | }, 140 | }) 141 | 142 | const ColLuminanceEdit = component 143 | ({ 144 | mount() 145 | { 146 | this.onLumEdit = this.onLumEdit.bind(this) 147 | }, 148 | onLumEdit() 149 | { 150 | const v = this.$value 151 | this.$value = [v[0], v[0], v[0], v[3]] 152 | }, 153 | render() 154 | { 155 | eopen("div", { "class": "valueEdit colEdit colLumEdit" }) 156 | eopen("div", { "class": "row" }) 157 | eopen("label") 158 | eopen("span", { "class": "lbl" }) 159 | text("Lum") 160 | eclose("span") 161 | cvoid(NumberEdit, { bind: `${this.bind}/0`, onedit: this.onLumEdit }) 162 | eclose("label") 163 | eclose("div") 164 | if (this.alpha) 165 | { 166 | eopen("div", { "class": "row" }) 167 | eopen("label") 168 | eopen("span", { "class": "lbl" }) 169 | text("Alpha") 170 | eclose("span") 171 | cvoid(NumberEdit, { bind: `${this.bind}/3` }) 172 | eclose("label") 173 | eclose("div") 174 | } 175 | eclose("div") 176 | }, 177 | }) 178 | 179 | const ColRGBAEdit = component 180 | ({ 181 | render() 182 | { 183 | const rgba = this.$value.map((x) => Math.round(x * 255)) 184 | eopen("div", { "class": "valueEdit colEdit colRGBAEdit" }) 185 | for (var i = 0; i < this.dims; ++i) 186 | { 187 | eopen("div", { "class": "row row" + i }) 188 | eopen("label") 189 | eopen("span", { "class": "lbl" }) 190 | text(labelsRGBA[i]) 191 | eclose("span") 192 | var trackStyle 193 | 194 | if (i == 0) 195 | trackStyle = { background: `linear-gradient(to right, rgb(0, ${rgba[1]}, ${rgba[2]}), rgb(255, ${rgba[1]}, ${rgba[2]}))` } 196 | else if (i == 1) 197 | trackStyle = { background: `linear-gradient(to right, rgb(${rgba[0]}, 0, ${rgba[2]}), rgb(${rgba[0]}, 255, ${rgba[2]}))` } 198 | else if (i == 2) 199 | trackStyle = { background: `linear-gradient(to right, rgb(${rgba[0]}, ${rgba[1]}, 0), rgb(${rgba[0]}, ${rgba[1]}, 255))` } 200 | else 201 | trackStyle = void 0 202 | cvoid(NumberEdit, { bind: `${this.bind}/${i}`, trackStyle: trackStyle }) 203 | eclose("label") 204 | eclose("div") 205 | } 206 | eclose("div") 207 | }, 208 | }) 209 | 210 | const ColHSVAEdit = component 211 | ({ 212 | mount() 213 | { 214 | this.handleEdit = this.handleEdit.bind(this) 215 | }, 216 | handleEdit() 217 | { 218 | const rgb = rgbFromHSV(this.$value) 219 | store.set(this.rgbaBind, [rgb[0], rgb[1], rgb[2], this.$value[3]]) 220 | }, 221 | render() 222 | { 223 | const hsva = this.$value 224 | eopen("div", { "class": "valueEdit colEdit colHSVAEdit" }) 225 | for (var i = 0; i < this.dims; ++i) 226 | { 227 | eopen("div", { "class": "row row" + i }) 228 | eopen("label") 229 | eopen("span", { "class": "lbl" }) 230 | text(labelsHSVA[i]) 231 | eclose("span") 232 | var trackStyle 233 | 234 | if (i == 0) 235 | { 236 | trackStyle = { background: `linear-gradient(to right, rgb(255,0,0), rgb(255,255,0), rgb(0,255,0), 237 | rgb(0,255,255), rgb(0,0,255), rgb(255,0,255), rgb(255,0,0))` } 238 | } 239 | else if (i == 1) 240 | { 241 | const rgb0 = rgbFromHSV([hsva[0], 0, hsva[2]]).map((x) => x * 255).join(",") 242 | const rgb1 = rgbFromHSV([hsva[0], 1, hsva[2]]).map((x) => x * 255).join(",") 243 | trackStyle = { background: `linear-gradient(to right, rgb(${rgb0}), rgb(${rgb1}))` } 244 | } 245 | else if (i == 2) 246 | { 247 | const rgb0 = rgbFromHSV([hsva[0], hsva[1], 0]).map((x) => x * 255).join(",") 248 | const rgb1 = rgbFromHSV([hsva[0], hsva[1], 1]).map((x) => x * 255).join(",") 249 | trackStyle = { background: `linear-gradient(to right, rgb(${rgb0}), rgb(${rgb1}))` } 250 | } 251 | else 252 | trackStyle = void 0 253 | cvoid(NumberEdit, { bind: `${this.bind}/${i}`, onedit: this.handleEdit, trackStyle: trackStyle }) 254 | eclose("label") 255 | eclose("div") 256 | } 257 | eclose("div") 258 | }, 259 | }) 260 | 261 | const OpenToggle = component 262 | ({ 263 | mount() 264 | { 265 | this.handleClick = this.handleClick.bind(this) 266 | }, 267 | handleClick() 268 | { 269 | this.$value = !this.$value 270 | }, 271 | render() 272 | { 273 | eopen("span", { "class": this["class"] || "toggle", onclick: this.handleClick }) 274 | eclose("span") 275 | }, 276 | }) 277 | 278 | const Checkbox = component 279 | ({ 280 | mount() 281 | { 282 | this.handleClick = this.handleClick.bind(this) 283 | }, 284 | handleClick() 285 | { 286 | this.$value = !this.$value 287 | if (this.onedit) 288 | this.onedit() 289 | }, 290 | render() 291 | { 292 | eopen("span", { "class": "checkbox " + (this["class"] || ""), onclick: this.handleClick }) 293 | evoid("i", { "class": "fa fa-" + (this.$value ? "check-circle" : "circle") }) 294 | text(this.label) 295 | eclose("span") 296 | }, 297 | }) 298 | 299 | const TypeSwitch = component 300 | ({ 301 | mount() 302 | { 303 | this.handleClick = this.handleClick.bind(this) 304 | }, 305 | handleClick(e) 306 | { 307 | this.$value = e.currentTarget.dataset.num 308 | }, 309 | render() 310 | { 311 | eopen("TypeSwitch", { "class": "typeSwitch" }) 312 | for (var i = 1; i <= 4; ++i) 313 | { 314 | eopen("TypeSwitchBtn", { "class": "btn" + (this.$value == i ? " used" : ""), "data-num": i, onclick: this.handleClick }) 315 | text(i) 316 | eclose("TypeSwitchBtn") 317 | } 318 | eclose("TypeSwitch") 319 | }, 320 | }) 321 | 322 | const ArrayEdit = component 323 | ({ 324 | mount() 325 | { 326 | this.handleUpClick = this.handleUpClick.bind(this) 327 | this.handleDownClick = this.handleDownClick.bind(this) 328 | this.handleDeleteClick = this.handleDeleteClick.bind(this) 329 | }, 330 | handleUpClick(e) 331 | { 332 | var v = this.$value 333 | var tmp = v[this.i] 334 | v[this.i] = v[this.i - 1] 335 | v[this.i - 1] = tmp 336 | store.update(this.bind) 337 | }, 338 | handleDownClick(e) 339 | { 340 | var v = this.$value 341 | var tmp = v[this.i] 342 | v[this.i] = v[this.i + 1] 343 | v[this.i + 1] = tmp 344 | store.update(this.bind) 345 | }, 346 | handleDeleteClick(e) 347 | { 348 | this.$value.splice(this.i, 1) 349 | store.update(this.bind) 350 | }, 351 | render() 352 | { 353 | eopen("ArrayEdit", { "class": "arrayEdit" }) 354 | eopen("span", { "class": "btn upBtn" + (this.i <= 0 ? " disabled" : ""), onclick: this.handleUpClick }) 355 | evoid("i", { "class": "fa fa-chevron-up io" }) 356 | eclose("span") 357 | eopen("span", { "class": "btn downBtn" + (this.i >= this.$value.length - 1 ? " disabled" : ""), onclick: this.handleDownClick }) 358 | evoid("i", { "class": "fa fa-chevron-down io" }) 359 | eclose("span") 360 | eopen("span", { "class": "btn deleteBtn", onclick: this.handleDeleteClick }) 361 | evoid("i", { "class": "fa fa-trash" }) 362 | text("Remove") 363 | eclose("span") 364 | eclose("ArrayEdit") 365 | }, 366 | }) 367 | 368 | 369 | 370 | const AutoCompleteTextField = component 371 | ({ 372 | // arguments: 373 | // - placeholder (string) [optional] = ... 374 | // - limit (int) [optional] = 10 375 | // - 376 | // - itemCallback(string text, int limit) -> [{ 377 | // key: string -- the ID returned by selection event 378 | // name: string 379 | // desc: string 380 | // match: int -- how far from typed query this match is (smaller number = better) 381 | // }...] 382 | // ^^ return a list of matching options 383 | // - 384 | mount() 385 | { 386 | this.handleInput = this.handleInput.bind(this) 387 | this.handleKeyDown = this.handleKeyDown.bind(this) 388 | this.handleOptionClick = this.handleOptionClick.bind(this) 389 | this.handleOuterClick = this.handleOuterClick.bind(this) 390 | }, 391 | handleInput(e) 392 | { 393 | this.$value = { open: this.alwaysOpen || e.currentTarget.value.length != 0, sel: 0, text: e.currentTarget.value } 394 | }, 395 | getSel() 396 | { 397 | return Math.min(this.$value.sel, this.options.length - 1) 398 | }, 399 | handleKeyDown(e) 400 | { 401 | if (this.options && this.options.length) 402 | { 403 | if (e.keyCode == 13) // enter 404 | { 405 | var sel = this.getSel() 406 | this.selectCallback(this.options[sel].key) 407 | this.$value = { open: false, sel: 0, text: "" } 408 | e.preventDefault() 409 | } 410 | else if (e.keyCode == 38) // up arrow 411 | { 412 | this.$value.sel = Math.max(this.getSel() - 1, 0) 413 | update(this) 414 | e.preventDefault() 415 | } 416 | else if (e.keyCode == 40) // down arrow 417 | { 418 | this.$value.sel = Math.min(this.getSel() + 1, this.options.length - 1) 419 | update(this) 420 | e.preventDefault() 421 | } 422 | } 423 | }, 424 | handleOptionClick(e) 425 | { 426 | this.selectCallback(e.currentTarget.dataset.key) 427 | this.$value = { open: false, sel: 0, text: "" } 428 | }, 429 | handleOuterClick(e) 430 | { 431 | if (e.button == 1) 432 | return 433 | e.preventDefault() 434 | if (this.alwaysOpen) 435 | this.selectCallback(null) 436 | this.$value = { open: false, sel: 0, text: "" } 437 | }, 438 | render() 439 | { 440 | var open = this.alwaysOpen || this.$value.open 441 | eopen("div", { "class": "autoCompleteTextField" + (open ? " open" : "") }) 442 | const el = evoid("input", 443 | { 444 | type: "text", 445 | placeholder: this.placeholder || "Start typing to see available options", 446 | oninput: this.handleInput, 447 | onkeydown: this.handleKeyDown, 448 | }).element 449 | el.value = this.$value.text 450 | if (open) 451 | { 452 | if (this.focusOnOpen) 453 | el.focus() 454 | evoid("acbgr", { "class": "bgr", onpointerdown: this.handleOuterClick }) 455 | var options = this.itemCallback(this.$value.text) 456 | this.options = options 457 | //if (this.$value.text) 458 | { 459 | options.sort((a, b) => 460 | { 461 | if (a.match != b.match) 462 | return a.match - b.match 463 | return a.sortText.localeCompare(b.sortText) 464 | }) 465 | } 466 | var length = options.length// < 10 ? options.length : 10 467 | if (length) 468 | { 469 | eopen("div", { "class": "options customScroll" }) 470 | var sel = this.getSel() 471 | for (var i = 0; i < length; ++i) 472 | { 473 | const oel = eopen("div", { "class": "option" + (sel == i ? " sel" : ""), onclick: this.handleOptionClick, "data-key": options[i].key }).element 474 | if (sel == i) 475 | oel.scrollIntoView({ block: "nearest" }) 476 | eopen("name") 477 | text(options[i].name) 478 | eclose("name") 479 | eopen("desc") 480 | text(options[i].desc) 481 | eclose("desc") 482 | //eopen("info");text(`match=${options[i].match} sortText=${options[i].sortText}`);eclose("info") 483 | eclose("div") 484 | } 485 | eclose("div") 486 | } 487 | } 488 | eclose("div") 489 | }, 490 | }) 491 | 492 | const NO_MATCH = 0xffffffff 493 | function ACTextMatch(query, text) 494 | { 495 | text = text.toLowerCase() 496 | var at = text.indexOf(query) 497 | if (at !== -1) 498 | return (at === 0 ? 1000 : 2000)// + text.length - query.length 499 | return NO_MATCH 500 | } 501 | function ACItemMatch(outArr, query, key, name, desc) 502 | { 503 | query = query.toLowerCase() 504 | var match = Math.min(ACTextMatch(query, name), ACTextMatch(query, desc)) 505 | if (match < NO_MATCH) 506 | outArr.push({ key: key, name: name, desc: desc, match: match, sortText: name.toLowerCase() + desc.toLowerCase() }) 507 | } 508 | 509 | 510 | 511 | const AxisClasses = ["dot-x", "dot-y", "dot-z", "dot-w"] 512 | const AxisMarker = component 513 | ({ 514 | render() 515 | { 516 | eopen("AxisMarker", { "class": "dots-" + this.dims, title: this.dims + "-dimensional value" }) 517 | for (var i = 0; i < this.dims; ++i) 518 | { 519 | evoid("Dot", { "class": AxisClasses[i] }) 520 | } 521 | eclose("AxisMarker") 522 | }, 523 | }) 524 | 525 | 526 | 527 | var swizzles = 528 | [ 529 | [], [], [], [], 530 | ]; 531 | var swizzleBase = "xyzw" 532 | for (var x = 0; x < 4; ++x) 533 | { 534 | swizzles[0].push(swizzleBase[x]) 535 | for (var y = 0; y < 4; ++y) 536 | { 537 | swizzles[1].push(swizzleBase[x] + swizzleBase[y]) 538 | for (var z = 0; z < 4; ++z) 539 | { 540 | swizzles[2].push(swizzleBase[x] + swizzleBase[y] + swizzleBase[z]) 541 | for (var w = 0; w < 4; ++w) 542 | { 543 | swizzles[3].push(swizzleBase[x] + swizzleBase[y] + swizzleBase[z] + swizzleBase[w]) 544 | } 545 | } 546 | } 547 | } 548 | 549 | const SwizzleEdit = component 550 | ({ 551 | mount() 552 | { 553 | this.handleOpenClick = this.handleOpenClick.bind(this) 554 | this.handleOuterClick = this.handleOuterClick.bind(this) 555 | this.handleButtonClick = this.handleButtonClick.bind(this) 556 | this.handleResetClick = this.handleResetClick.bind(this) 557 | }, 558 | handleOpenClick(e) 559 | { 560 | this.open = true 561 | update(this) 562 | var pel = e.currentTarget.parentElement 563 | // TODO fix placement? 564 | // setTimeout(() => { pel.querySelector("SwizzleEditPopup").scrollIntoView() }, 100) 565 | }, 566 | handleOuterClick(e) 567 | { 568 | if (e.button == 1) 569 | return 570 | e.preventDefault() 571 | this.open = false 572 | update(this) 573 | }, 574 | handleButtonClick(e) 575 | { 576 | var curSw = this.$value 577 | var nSw = "" 578 | for (var i = 0; i < 4; ++i) 579 | { 580 | if (i == e.currentTarget.dataset.dst) 581 | nSw += e.currentTarget.dataset.swz 582 | else if (i < curSw.length) 583 | nSw += curSw[i] 584 | else 585 | nSw += swizzleBase[i] 586 | } 587 | //console.log(nSw, e.currentTarget.dataset.dst, e.currentTarget.dataset.swz) 588 | this.$value = nSw == "xyzw" ? "" : nSw 589 | }, 590 | handleResetClick(e) 591 | { 592 | this.$value = "" 593 | }, 594 | render() 595 | { 596 | eopen("SwizzleEdit", { "class": this.open ? " open" : "" }) 597 | eopen("Name") 598 | text("Swizzle:") 599 | eclose("Name") 600 | evoid("input", { type: "button", "class": "btn openBtn", value: this.$value ? this.$value.substring(0, this.tgtDims) : "~", onclick: this.handleOpenClick }) 601 | if (this.open) 602 | { 603 | evoid("SwizzleEditBgr", { "class": "bgr", onpointerdown: this.handleOuterClick }) 604 | eopen("SwizzleEditPopup") 605 | eopen("SwizzleEditPopupAxes") 606 | for (var d = 0; d < this.tgtDims; ++d) 607 | { 608 | eopen("SwizzleEditCol") 609 | eopen("Name") 610 | text(swizzleBase[d]) 611 | eclose("Name") 612 | eopen("ColEls") 613 | for (var s = 0; s < Math.max(this.srcDims, this.tgtDims); ++s) 614 | { 615 | eopen("span", 616 | { 617 | "class": "btn" + ((this.$value[d] || swizzleBase[d]) == swizzleBase[s] ? " used" : ""), 618 | "data-dst": d, 619 | "data-src": s, 620 | "data-swz": swizzleBase[s], 621 | onclick: this.handleButtonClick 622 | }) 623 | text(s < this.srcDims ? swizzleBase[s] : defArg[s]) 624 | eclose("span") 625 | } 626 | eclose("ColEls") 627 | eclose("SwizzleEditCol") 628 | } 629 | eclose("SwizzleEditPopupAxes") 630 | evoid("input", { type: "button", "class": "btn resetBtn", value: "Reset", onclick: this.handleResetClick }) 631 | eclose("SwizzleEditPopup") 632 | } 633 | eclose("SwizzleEdit") 634 | }, 635 | }) 636 | 637 | 638 | -------------------------------------------------------------------------------- /cubemaps/README: -------------------------------------------------------------------------------- 1 | cubemaps are hosted at http://archo.work/ShaderEditorHTML5/cubemaps 2 | 3 | files: 4 | - input.hdr 5 | - cubemap.jpg 6 | - cubemap.preload.jpg 7 | -------------------------------------------------------------------------------- /cubemaps/hdri2ldrcube.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #define STB_IMAGE_IMPLEMENTATION 12 | #include "stb_image.h" 13 | 14 | #define TJE_IMPLEMENTATION 15 | #include "tiny_jpeg.h" 16 | 17 | 18 | 19 | int img_width, img_height; 20 | float* img_data; 21 | 22 | void load_image(const char* filename) 23 | { 24 | int ch; 25 | img_data = stbi_loadf(filename, &img_width, &img_height, &ch, 3); 26 | if (!img_data) 27 | { 28 | fprintf(stderr, "failed to load image: %s\n", filename); 29 | exit(1); 30 | } 31 | } 32 | 33 | void check_ranges() 34 | { 35 | float minval = FLT_MAX, gminval = FLT_MAX; 36 | float maxval = 0.0f, gmaxval = 0.0f; 37 | for (int i = 0; i < img_width * img_height * 3; ++i) 38 | { 39 | float v = img_data[i]; 40 | if (minval > v) 41 | minval = v; 42 | if (maxval < v) 43 | maxval = v; 44 | float g = powf(v, 1.0f / 2.2f); 45 | if (gminval > g) 46 | gminval = g; 47 | if (gmaxval < g) 48 | gmaxval = g; 49 | } 50 | printf("data range (regular): [%f; %f]\n", minval, maxval); 51 | printf("data range (gamma): [%f; %f]\n", gminval, gmaxval); 52 | } 53 | 54 | 55 | uint8_t* out_data; 56 | int out_width, out_height, out_comps; 57 | 58 | void alloc_image(int w, int h, int c) 59 | { 60 | if (out_data) 61 | free(out_data); 62 | out_width = w; 63 | out_height = h; 64 | out_comps = c; 65 | out_data = (uint8_t*) malloc(c * w * h); 66 | } 67 | 68 | void generate_dump() 69 | { 70 | alloc_image(img_width, img_height, 3); 71 | for (int i = 0; i < img_width * img_height * 3; ++i) 72 | { 73 | int v = powf(img_data[i], 1.0f / 2.2f) * 255; 74 | out_data[i] = v > 255 ? 255 : v; 75 | } 76 | } 77 | 78 | void ds2x() 79 | { 80 | uint8_t* src_data = out_data; 81 | out_data = NULL; 82 | alloc_image(out_width / 2, out_height / 2, out_comps); 83 | #pragma omp parallel for 84 | for (int y = 0; y < out_height; ++y) 85 | { 86 | for (int x = 0; x < out_width; ++x) 87 | { 88 | for (int c = 0; c < out_comps; ++c) 89 | { 90 | out_data[c + out_comps * (x + y * out_width)] = ( 91 | src_data[c + out_comps * (x * 2 + y * 2 * out_width * 2)] + 92 | src_data[c + out_comps * (x * 2 + 1 + y * 2 * out_width * 2)] + 93 | src_data[c + out_comps * (x * 2 + (y * 2 + 1) * out_width * 2)] + 94 | src_data[c + out_comps * (x * 2 + 1 + (y * 2 + 1) * out_width * 2)]) / 4; 95 | } 96 | } 97 | } 98 | free(src_data); 99 | } 100 | 101 | void save_image_jpeg(const char* filename, int qual) 102 | { 103 | if (!tje_encode_to_file_at_quality(filename, qual, out_width, out_height, out_comps, out_data)) 104 | { 105 | fprintf(stderr, "failed to save output image: %s\n", filename); 106 | exit(1); 107 | } 108 | } 109 | 110 | 111 | #define PI 3.14159f 112 | static const float INVPI = 1.0f / PI; 113 | static const float INVPI2 = 0.5f / PI; 114 | 115 | void sample_hdr_uv(float out[3], float u, float v) 116 | { 117 | int x0 = floor(u); 118 | int x1 = x0 + 1 < img_width - 1 ? x0 + 1 : img_width - 1; 119 | int y0 = floor(v); 120 | int y1 = y0 + 1 < img_height - 1 ? y0 + 1 : img_height - 1; 121 | int off00 = (x0 + y0 * img_width) * 3; 122 | int off10 = (x1 + y0 * img_width) * 3; 123 | int off01 = (x0 + y1 * img_width) * 3; 124 | int off11 = (x1 + y1 * img_width) * 3; 125 | float fx = fmodf(u, 1.0f); 126 | float fy = fmodf(v, 1.0f); 127 | float ifx = 1.0f - fx; 128 | float ify = 1.0f - fy; 129 | out[0] = img_data[off00 + 0] * ifx * ify + img_data[off10 + 0] * fx * ify + img_data[off01 + 0] * ifx * fy + img_data[off11 + 0] * fx * fy; 130 | out[1] = img_data[off00 + 1] * ifx * ify + img_data[off10 + 1] * fx * ify + img_data[off01 + 1] * ifx * fy + img_data[off11 + 1] * fx * fy; 131 | out[2] = img_data[off00 + 2] * ifx * ify + img_data[off10 + 2] * fx * ify + img_data[off01 + 2] * ifx * fy + img_data[off11 + 2] * fx * fy; 132 | } 133 | 134 | void sample_hdr_vec(float out[3], float dir[3]) 135 | { 136 | float u = (atan2(dir[1], dir[0]) + PI) * INVPI2; 137 | float v = acos(dir[2]) * INVPI; 138 | sample_hdr_uv(out, u * img_width, v * img_height); 139 | } 140 | 141 | void vec3_normalize(float vec[3]) 142 | { 143 | float lensq = vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]; 144 | if (lensq) 145 | { 146 | float inv = 1.0f / sqrtf(lensq); 147 | vec[0] *= inv; 148 | vec[1] *= inv; 149 | vec[2] *= inv; 150 | } 151 | } 152 | 153 | void generate_cube_face(int dx, int dy, int w, int h, float dir[3], float up[3], float rt[3]) 154 | { 155 | #pragma omp parallel for 156 | for (int y = 0; y < h; ++y) 157 | { 158 | float fy = ((float)y) / ((float)(h-1)) * -2.0f + 1.0f; 159 | for (int x = 0; x < w; ++x) 160 | { 161 | float val[3]; 162 | float fx = ((float)x) / ((float)(w-1)) * -2.0f + 1.0f; 163 | float vec[3] = 164 | { 165 | dir[0] + rt[0] * fx + up[0] * fy, 166 | dir[1] + rt[1] * fx + up[1] * fy, 167 | dir[2] + rt[2] * fx + up[2] * fy, 168 | }; 169 | vec3_normalize(vec); 170 | sample_hdr_vec(val, vec); 171 | // tonemap 172 | #define ACES_TONEMAP 173 | #ifdef ACES_TONEMAP 174 | // https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ 175 | val[0] *= 0.6f; 176 | val[1] *= 0.6f; 177 | val[2] *= 0.6f; 178 | static const float a = 2.51f; 179 | static const float b = 0.03f; 180 | static const float c = 2.43f; 181 | static const float d = 0.59f; 182 | static const float e = 0.14f; 183 | val[0] = (val[0]*(a*val[0]+b))/(val[0]*(c*val[0]+d)+e); 184 | val[1] = (val[1]*(a*val[1]+b))/(val[1]*(c*val[1]+d)+e); 185 | val[2] = (val[2]*(a*val[2]+b))/(val[2]*(c*val[2]+d)+e); 186 | val[0] = powf(val[0], 1.0f / 2.2f); 187 | val[1] = powf(val[1], 1.0f / 2.2f); 188 | val[2] = powf(val[2], 1.0f / 2.2f); 189 | #else 190 | val[0] = val[0] / (1 + val[0]); 191 | val[1] = val[1] / (1 + val[1]); 192 | val[2] = val[2] / (1 + val[2]); 193 | // gamma 194 | val[0] = powf(val[0], 1.0f / 2.2f); 195 | val[1] = powf(val[1], 1.0f / 2.2f); 196 | val[2] = powf(val[2], 1.0f / 2.2f); 197 | #endif 198 | // clamp 199 | if (val[0] > 1) val[0] = 1; 200 | if (val[1] > 1) val[1] = 1; 201 | if (val[2] > 1) val[2] = 1; 202 | // write 203 | uint8_t* pixel = &out_data[3 * (x + dx + (y + dy) * out_width)]; 204 | pixel[0] = val[0] * 255.0f; 205 | pixel[1] = val[1] * 255.0f; 206 | pixel[2] = val[2] * 255.0f; 207 | } 208 | } 209 | } 210 | 211 | typedef struct CubeFace 212 | { 213 | float dir[3]; 214 | float up[3]; 215 | float rt[3]; 216 | int x, y; 217 | } 218 | CubeFace; 219 | 220 | CubeFace cubeFaces[6] = 221 | { 222 | { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }, 0, 0, }, 223 | { { -1, 0, 0 }, { 0, 1, 0 }, { 0, 0, -1 }, 1, 0, }, 224 | { { 0, 1, 0 }, { 0, 0, -1 }, { -1, 0, 0 }, 0, 1, }, 225 | { { 0, -1, 0 }, { 0, 0, 1 }, { -1, 0, 0 }, 1, 1, }, 226 | { { 0, 0, 1 }, { 0, 1, 0 }, { -1, 0, 0 }, 0, 2, }, 227 | { { 0, 0, -1 }, { 0, 1, 0 }, { 1, 0, 0 }, 1, 2, }, 228 | }; 229 | 230 | 231 | #define SIDE_WIDTH 512 232 | #define SIDE_WIDTH_PRE 128 233 | int main() 234 | { 235 | load_image("input.hdr"); 236 | // check_ranges(); 237 | 238 | // generate_dump(); 239 | // save_image_jpeg("output.jpg"); 240 | 241 | alloc_image(SIDE_WIDTH*2, SIDE_WIDTH*3, 3); 242 | for (int i = 0; i < 6; ++i) 243 | { 244 | generate_cube_face(SIDE_WIDTH * cubeFaces[i].x, SIDE_WIDTH * cubeFaces[i].y, SIDE_WIDTH, SIDE_WIDTH, cubeFaces[i].dir, cubeFaces[i].up, cubeFaces[i].rt); 245 | } 246 | save_image_jpeg("cubemap.jpg", 2); 247 | 248 | while (out_width > SIDE_WIDTH_PRE) 249 | ds2x(); 250 | save_image_jpeg("cubemap.preload.jpg", 2); 251 | 252 | stbi_image_free(img_data); 253 | free(out_data); 254 | return 0; 255 | } 256 | -------------------------------------------------------------------------------- /cubemaps/makefile: -------------------------------------------------------------------------------- 1 | 2 | run: hdri2ldrcube.exe 3 | hdri2ldrcube 4 | 5 | hdri2ldrcube.exe: hdri2ldrcube.c 6 | gcc -o $@ -O2 -fopenmp -std=c99 -Wall -Wextra -Wno-unused-parameter $< 7 | -------------------------------------------------------------------------------- /ext/fa/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OLF license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OLF, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** 35 | -------------------------------------------------------------------------------- /ext/fa/css/fa-solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | src: url("../webfonts/fa-solid-900.eot"); 10 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 11 | 12 | .fa, 13 | .fas { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 900; } 16 | -------------------------------------------------------------------------------- /ext/fa/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/ext/fa/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /ext/fa/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/ext/fa/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /ext/fa/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/ext/fa/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /ext/fa/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/ext/fa/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /ext/noisejs/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2013, Joseph Gentle 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /ext/noisejs/perlin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A speed-improved perlin and simplex noise algorithms for 2D. 3 | * 4 | * Based on example code by Stefan Gustavson (stegu@itn.liu.se). 5 | * Optimisations by Peter Eastman (peastman@drizzle.stanford.edu). 6 | * Better rank ordering method by Stefan Gustavson in 2012. 7 | * Converted to Javascript by Joseph Gentle. 8 | * 9 | * Version 2012-03-09 10 | * 11 | * This code was placed in the public domain by its original author, 12 | * Stefan Gustavson. You may use it as you see fit, but 13 | * attribution is appreciated. 14 | * 15 | */ 16 | 17 | (function(global){ 18 | var module = global.noise = {}; 19 | 20 | function Grad(x, y, z) { 21 | this.x = x; this.y = y; this.z = z; 22 | } 23 | 24 | Grad.prototype.dot2 = function(x, y) { 25 | return this.x*x + this.y*y; 26 | }; 27 | 28 | Grad.prototype.dot3 = function(x, y, z) { 29 | return this.x*x + this.y*y + this.z*z; 30 | }; 31 | 32 | var grad3 = [new Grad(1,1,0),new Grad(-1,1,0),new Grad(1,-1,0),new Grad(-1,-1,0), 33 | new Grad(1,0,1),new Grad(-1,0,1),new Grad(1,0,-1),new Grad(-1,0,-1), 34 | new Grad(0,1,1),new Grad(0,-1,1),new Grad(0,1,-1),new Grad(0,-1,-1)]; 35 | 36 | var p = [151,160,137,91,90,15, 37 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 38 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 39 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 40 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 41 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 42 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 43 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 44 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 45 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 46 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 47 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 48 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]; 49 | // To remove the need for index wrapping, double the permutation table length 50 | var perm = new Array(512); 51 | var gradP = new Array(512); 52 | 53 | // This isn't a very good seeding function, but it works ok. It supports 2^16 54 | // different seed values. Write something better if you need more seeds. 55 | module.seed = function(seed) { 56 | if(seed > 0 && seed < 1) { 57 | // Scale the seed out 58 | seed *= 65536; 59 | } 60 | 61 | seed = Math.floor(seed); 62 | if(seed < 256) { 63 | seed |= seed << 8; 64 | } 65 | 66 | for(var i = 0; i < 256; i++) { 67 | var v; 68 | if (i & 1) { 69 | v = p[i] ^ (seed & 255); 70 | } else { 71 | v = p[i] ^ ((seed>>8) & 255); 72 | } 73 | 74 | perm[i] = perm[i + 256] = v; 75 | gradP[i] = gradP[i + 256] = grad3[v % 12]; 76 | } 77 | }; 78 | 79 | module.seed(0); 80 | 81 | /* 82 | for(var i=0; i<256; i++) { 83 | perm[i] = perm[i + 256] = p[i]; 84 | gradP[i] = gradP[i + 256] = grad3[perm[i] % 12]; 85 | }*/ 86 | 87 | // Skewing and unskewing factors for 2, 3, and 4 dimensions 88 | var F2 = 0.5*(Math.sqrt(3)-1); 89 | var G2 = (3-Math.sqrt(3))/6; 90 | 91 | var F3 = 1/3; 92 | var G3 = 1/6; 93 | 94 | // 2D simplex noise 95 | module.simplex2 = function(xin, yin) { 96 | var n0, n1, n2; // Noise contributions from the three corners 97 | // Skew the input space to determine which simplex cell we're in 98 | var s = (xin+yin)*F2; // Hairy factor for 2D 99 | var i = Math.floor(xin+s); 100 | var j = Math.floor(yin+s); 101 | var t = (i+j)*G2; 102 | var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed. 103 | var y0 = yin-j+t; 104 | // For the 2D case, the simplex shape is an equilateral triangle. 105 | // Determine which simplex we are in. 106 | var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords 107 | if(x0>y0) { // lower triangle, XY order: (0,0)->(1,0)->(1,1) 108 | i1=1; j1=0; 109 | } else { // upper triangle, YX order: (0,0)->(0,1)->(1,1) 110 | i1=0; j1=1; 111 | } 112 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and 113 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where 114 | // c = (3-sqrt(3))/6 115 | var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords 116 | var y1 = y0 - j1 + G2; 117 | var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords 118 | var y2 = y0 - 1 + 2 * G2; 119 | // Work out the hashed gradient indices of the three simplex corners 120 | i &= 255; 121 | j &= 255; 122 | var gi0 = gradP[i+perm[j]]; 123 | var gi1 = gradP[i+i1+perm[j+j1]]; 124 | var gi2 = gradP[i+1+perm[j+1]]; 125 | // Calculate the contribution from the three corners 126 | var t0 = 0.5 - x0*x0-y0*y0; 127 | if(t0<0) { 128 | n0 = 0; 129 | } else { 130 | t0 *= t0; 131 | n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient 132 | } 133 | var t1 = 0.5 - x1*x1-y1*y1; 134 | if(t1<0) { 135 | n1 = 0; 136 | } else { 137 | t1 *= t1; 138 | n1 = t1 * t1 * gi1.dot2(x1, y1); 139 | } 140 | var t2 = 0.5 - x2*x2-y2*y2; 141 | if(t2<0) { 142 | n2 = 0; 143 | } else { 144 | t2 *= t2; 145 | n2 = t2 * t2 * gi2.dot2(x2, y2); 146 | } 147 | // Add contributions from each corner to get the final noise value. 148 | // The result is scaled to return values in the interval [-1,1]. 149 | return 70 * (n0 + n1 + n2); 150 | }; 151 | 152 | // 3D simplex noise 153 | module.simplex3 = function(xin, yin, zin) { 154 | var n0, n1, n2, n3; // Noise contributions from the four corners 155 | 156 | // Skew the input space to determine which simplex cell we're in 157 | var s = (xin+yin+zin)*F3; // Hairy factor for 2D 158 | var i = Math.floor(xin+s); 159 | var j = Math.floor(yin+s); 160 | var k = Math.floor(zin+s); 161 | 162 | var t = (i+j+k)*G3; 163 | var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed. 164 | var y0 = yin-j+t; 165 | var z0 = zin-k+t; 166 | 167 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron. 168 | // Determine which simplex we are in. 169 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords 170 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords 171 | if(x0 >= y0) { 172 | if(y0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=1; k2=0; } 173 | else if(x0 >= z0) { i1=1; j1=0; k1=0; i2=1; j2=0; k2=1; } 174 | else { i1=0; j1=0; k1=1; i2=1; j2=0; k2=1; } 175 | } else { 176 | if(y0 < z0) { i1=0; j1=0; k1=1; i2=0; j2=1; k2=1; } 177 | else if(x0 < z0) { i1=0; j1=1; k1=0; i2=0; j2=1; k2=1; } 178 | else { i1=0; j1=1; k1=0; i2=1; j2=1; k2=0; } 179 | } 180 | // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z), 181 | // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and 182 | // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where 183 | // c = 1/6. 184 | var x1 = x0 - i1 + G3; // Offsets for second corner 185 | var y1 = y0 - j1 + G3; 186 | var z1 = z0 - k1 + G3; 187 | 188 | var x2 = x0 - i2 + 2 * G3; // Offsets for third corner 189 | var y2 = y0 - j2 + 2 * G3; 190 | var z2 = z0 - k2 + 2 * G3; 191 | 192 | var x3 = x0 - 1 + 3 * G3; // Offsets for fourth corner 193 | var y3 = y0 - 1 + 3 * G3; 194 | var z3 = z0 - 1 + 3 * G3; 195 | 196 | // Work out the hashed gradient indices of the four simplex corners 197 | i &= 255; 198 | j &= 255; 199 | k &= 255; 200 | var gi0 = gradP[i+ perm[j+ perm[k ]]]; 201 | var gi1 = gradP[i+i1+perm[j+j1+perm[k+k1]]]; 202 | var gi2 = gradP[i+i2+perm[j+j2+perm[k+k2]]]; 203 | var gi3 = gradP[i+ 1+perm[j+ 1+perm[k+ 1]]]; 204 | 205 | // Calculate the contribution from the four corners 206 | var t0 = 0.6 - x0*x0 - y0*y0 - z0*z0; 207 | if(t0<0) { 208 | n0 = 0; 209 | } else { 210 | t0 *= t0; 211 | n0 = t0 * t0 * gi0.dot3(x0, y0, z0); // (x,y) of grad3 used for 2D gradient 212 | } 213 | var t1 = 0.6 - x1*x1 - y1*y1 - z1*z1; 214 | if(t1<0) { 215 | n1 = 0; 216 | } else { 217 | t1 *= t1; 218 | n1 = t1 * t1 * gi1.dot3(x1, y1, z1); 219 | } 220 | var t2 = 0.6 - x2*x2 - y2*y2 - z2*z2; 221 | if(t2<0) { 222 | n2 = 0; 223 | } else { 224 | t2 *= t2; 225 | n2 = t2 * t2 * gi2.dot3(x2, y2, z2); 226 | } 227 | var t3 = 0.6 - x3*x3 - y3*y3 - z3*z3; 228 | if(t3<0) { 229 | n3 = 0; 230 | } else { 231 | t3 *= t3; 232 | n3 = t3 * t3 * gi3.dot3(x3, y3, z3); 233 | } 234 | // Add contributions from each corner to get the final noise value. 235 | // The result is scaled to return values in the interval [-1,1]. 236 | return 32 * (n0 + n1 + n2 + n3); 237 | 238 | }; 239 | 240 | // ##### Perlin noise stuff 241 | 242 | function fade(t) { 243 | return t*t*t*(t*(t*6-15)+10); 244 | } 245 | 246 | function lerp(a, b, t) { 247 | return (1-t)*a + t*b; 248 | } 249 | 250 | // 2D Perlin Noise 251 | module.perlin2 = function(x, y) { 252 | // Find unit grid cell containing point 253 | var X = Math.floor(x), Y = Math.floor(y); 254 | // Get relative xy coordinates of point within that cell 255 | x = x - X; y = y - Y; 256 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 257 | X = X & 255; Y = Y & 255; 258 | 259 | // Calculate noise contributions from each of the four corners 260 | var n00 = gradP[X+perm[Y]].dot2(x, y); 261 | var n01 = gradP[X+perm[Y+1]].dot2(x, y-1); 262 | var n10 = gradP[X+1+perm[Y]].dot2(x-1, y); 263 | var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1); 264 | 265 | // Compute the fade curve value for x 266 | var u = fade(x); 267 | 268 | // Interpolate the four results 269 | return lerp( 270 | lerp(n00, n10, u), 271 | lerp(n01, n11, u), 272 | fade(y)); 273 | }; 274 | 275 | // 3D Perlin Noise 276 | module.perlin3 = function(x, y, z) { 277 | // Find unit grid cell containing point 278 | var X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z); 279 | // Get relative xyz coordinates of point within that cell 280 | x = x - X; y = y - Y; z = z - Z; 281 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 282 | X = X & 255; Y = Y & 255; Z = Z & 255; 283 | 284 | // Calculate noise contributions from each of the eight corners 285 | var n000 = gradP[X+ perm[Y+ perm[Z ]]].dot3(x, y, z); 286 | var n001 = gradP[X+ perm[Y+ perm[Z+1]]].dot3(x, y, z-1); 287 | var n010 = gradP[X+ perm[Y+1+perm[Z ]]].dot3(x, y-1, z); 288 | var n011 = gradP[X+ perm[Y+1+perm[Z+1]]].dot3(x, y-1, z-1); 289 | var n100 = gradP[X+1+perm[Y+ perm[Z ]]].dot3(x-1, y, z); 290 | var n101 = gradP[X+1+perm[Y+ perm[Z+1]]].dot3(x-1, y, z-1); 291 | var n110 = gradP[X+1+perm[Y+1+perm[Z ]]].dot3(x-1, y-1, z); 292 | var n111 = gradP[X+1+perm[Y+1+perm[Z+1]]].dot3(x-1, y-1, z-1); 293 | 294 | // Compute the fade curve value for x, y, z 295 | var u = fade(x); 296 | var v = fade(y); 297 | var w = fade(z); 298 | 299 | // Interpolate 300 | return lerp( 301 | lerp( 302 | lerp(n000, n100, u), 303 | lerp(n001, n101, u), w), 304 | lerp( 305 | lerp(n010, n110, u), 306 | lerp(n011, n111, u), w), 307 | v); 308 | }; 309 | 310 | })(this); 311 | -------------------------------------------------------------------------------- /ext/wabi/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Arturs Sefers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ext/wabi/wabi.min.js: -------------------------------------------------------------------------------- 1 | "use strict";!function(scope){scope.module={exports:{}},scope.modules={}}(window||global),function(){modules[2]={VNode:function(id,type,props,element){this.id=id,this.type=type,this.props=props,this.element=element,this.children=[],this.index=0,this.component=null}}}(),function(){var VNode=modules[2].VNode;const stack=new Array(64),components={};let stackIndex=0,bodyNode=null;const elementOpen=function(type,props,srcElement){const parent=stack[stackIndex];let prevNode=parent.children[parent.index],vnode=prevNode;if(prevNode)if(vnode.type!==type){const element=srcElement||document.createElement(type);if(vnode.component)vnode.element.replaceChild(element,vnode.component.base),removeComponent(vnode.component),vnode.component=null,appendChildren(element,vnode.children);else{const prevElement=prevNode.element;appendChildren(element,vnode.children),prevElement.parentElement.replaceChild(element,prevElement)}if(vnode.element=element,vnode.type=type,props){for(let key in props)setProp(element,key,props[key]);vnode.props=props}}else{const element=prevNode.element,prevProps=prevNode.props;if(props!==prevProps)if(props){if(prevProps){for(let key in prevProps)void 0===props[key]&&unsetProp(element,key);for(let key in props){const value=props[key];value!==prevProps[key]&&setProp(element,key,value)}}else for(let key in props)setProp(element,key,props[key]);prevNode.props=props}else if(prevProps){for(let key in prevProps)unsetProp(element,key);prevNode.props=null}}else{const element=srcElement||document.createElement(type);if(vnode=new VNode(parent.index,type,null,element),props){for(let key in props)setProp(element,key,props[key]);vnode.props=props}if(parent.component)if(parent.index>0){const parentNext=stack[stackIndex-1].children[parent.id+1];parentNext&&parentNext.component?parent.element.insertBefore(element,parentNext.component.base):parent.element.appendChild(element)}else parent.element.insertBefore(element,parent.component.base.nextSibling);else parent.element.appendChild(element);parent.children.push(vnode)}return parent.index++,stack[++stackIndex]=vnode,vnode},appendChildren=(element,children)=>{for(let n=0;n{const parent=stack[stackIndex];let component,vnode=parent.children[parent.index];if(vnode)if(component=vnode.component)if(component.constructor===ctor)diffComponentProps(component,vnode,props);else{const newComponent=createComponent(ctor);newComponent.vnode=vnode,vnode.component=newComponent,vnode.element.replaceChild(newComponent.base,component.base),removeComponent(component),diffComponentProps(component=newComponent,vnode,props)}else{const vnodeNew=new VNode(vnode.id,null,null,parent.element);(component=createComponent(ctor)).vnode=vnodeNew,vnodeNew.component=component,vnodeNew.children.push(vnode),parent.element.insertBefore(component.base,vnode.element),parent.children[vnode.id]=vnodeNew,vnode.id=0,vnode.parent=vnodeNew,diffComponentProps(component,vnode=vnodeNew,props)}else vnode=new VNode(parent.children.length,null,null,parent.element),(component=createComponent(ctor)).vnode=vnode,vnode.component=component,parent.children.push(vnode),parent.element.appendChild(component.base),diffComponentProps(component,vnode,props);return parent.index++,stack[++stackIndex]=vnode,component.depth=stackIndex,component.render(),component.dirty=!1,vnode.index!==vnode.children.length&&removeUnusedNodes(vnode),vnode.index=0,stackIndex--,component},diffComponentProps=(component,node,props)=>{const prevProps=node.props;if(props!==prevProps)if(props){if(prevProps){for(let key in prevProps)void 0===props[key]&&("$"===key[0]?component[key]=component.state[key.slice(1)]:component[key]=null);for(let key in props){const value=props[key];component[key]!==value&&(component[key]=value)}}else for(let key in props)component[key]=props[key];node.props=props}else if(prevProps){for(let key in prevProps)"$"===key[0]?component[key]=component.state[ket.slice(1)]:component[key]=null;node.props=null}},createComponent=ctor=>{const buffer=components[ctor.prototype.__componentIndex];let component=buffer?buffer.pop():null;return component||(component=new ctor),component.mount&&component.mount(),component.dirty=!0,component},removeComponent=component=>{const buffer=components[component.__componentIndex];buffer?buffer.push(component):components[component.__componentIndex]=[component],component.remove(),component.base.remove()},setProp=(element,name,value)=>{if("class"===name)element.className=value;else if("style"===name)if("object"==typeof value){const elementStyle=element.style;for(let key in value)elementStyle[key]=value[key]}else element.style.cssText=value;else"o"===name[0]&&"n"===name[1]?element[name]=value:element.setAttribute(name,value)},unsetProp=function(element,name){"class"===name?element.className="":"style"===name?element.style.cssText="":"o"===name[0]&&"n"===name[1]?element[name]=null:element.removeAttribute(name)},removeUnusedNodes=node=>{const children=node.children;for(let n=node.index;n{node.component?removeComponent(node.component):node.element.parentElement&&node.element.parentElement.removeChild(node.element);const children=node.children;for(let n=0;n{const node=elementOpen(type,props);return elementClose(type),node},element:(element,props)=>{const node=elementOpen(element.localName,props,element);return elementClose(element.localName),node},componentVoid:componentVoid,text:text=>{const parent=stack[stackIndex];let vnode=parent.children[parent.index];if(vnode)if("#text"===vnode.type)vnode.element.nodeValue!==text&&(vnode.element.nodeValue=text);else{const element=document.createTextNode(text);vnode.component?(vnode.element.replaceChild(element,vnode.component.base),removeComponent(vnode.component),vnode.component=null):vnode.element.parentElement.replaceChild(element,vnode.element),removeUnusedNodes(vnode),vnode.type="#text",vnode.element=element}else{const element=document.createTextNode(text);vnode=new VNode(parent.children.length,"#text",null,element),parent.children.push(vnode),parent.element.appendChild(element)}return parent.index++,vnode},render:function(component,parentElement){bodyNode||(bodyNode=new VNode(0,"body",null,parentElement)),stackIndex=0,stack[0]=bodyNode,componentVoid(component),bodyNode.index!==bodyNode.children.length&&removeUnusedNodes(bodyNode),bodyNode.index=0},renderInstance:function(instance){const vnode=instance.vnode;stackIndex=instance.depth,stack[instance.depth]=vnode,instance.render(),instance.dirty=!1,vnode.index!==vnode.children.length&&removeUnusedNodes(vnode),vnode.index=0},removeAll:()=>{removeUnusedNodes(bodyNode)},getBodyNode:()=>bodyNode}}(),function(){modules[5].getBodyNode;let tabs="";const dumpNode=node=>{const tag=node.component?"component":node.type,children=node.children;if(children.length>0){dumpOpen(tag);for(let n=0;n{console.log(tabs+"<"+name+">"),incTabs()},dumpClose=name=>{decTabs(),console.log(tabs+"")},dumpVoid=name=>{console.log(tabs+"<"+name+">")},incTabs=()=>{tabs+="\t"},decTabs=()=>{tabs=tabs.substring(0,tabs.length-1)};modules[6]=(node=>{console.log("---"),dumpNode(node),console.log("\n")})}(),function(){modules[2].VNode;var __module5=modules[5],render=__module5.render,renderInstance=__module5.renderInstance,removeAll=__module5.removeAll;__module5.getBodyNode,modules[6];const updateBuffer=[],routes=[];let needUpdate=!1,needUpdateRoute=!1,currRouteResult=[],currRoute=null,url=null;const renderLoop=function(){needUpdate&&updateRender(),needUpdateRoute&&updateRoute(),window.requestAnimationFrame(renderLoop)},updateRender=function(){updateBuffer.sort(sortByDepth);for(let n=0;n{updateRoute()}),renderLoop(),modules[4]={update:function(instance){instance.dirty||(instance.dirty=!0,updateBuffer.push(instance),needUpdate=!0)},route:function(regexp,component,enterFunc,exitFunc,readyFunc){routes.push(new function(regexp,component,enterFunc,exitFunc,readyFunc){this.regexp=regexp,this.component=component,this.enterFunc=enterFunc||null,this.exitFunc=exitFunc||null,this.readyFunc=readyFunc||null}(regexp,component,enterFunc,exitFunc,readyFunc)),needUpdateRoute=!0},clearRoutes:function(remove){routes.length=0,currRoute=null,remove&&removeAll()}}}(),function(){modules[4].update;function WatcherBuffer(){this.funcs=[],this.buffer=null}function Store(){this.data={},this.proxies=[],this.emitting=0,this.removeWatchers=[],this.watchers=new WatcherBuffer,this.watchers.buffer={}}Store.prototype={set:function(key,value){this.dispatch({action:"SET",key:key,value:value})},add:function(key,value){this.dispatch({action:"ADD",key:key,value:value})},remove:function(key,value){this.dispatch({action:"REMOVE",key:key,value:value})},dispatch:function(data){this.globalProxy?this.globalProxy(data):this.handle(data,null)},performSet:function(payload,promise){const tuple=this.getData(payload.key);tuple&&(payload.key?(tuple.data[tuple.key]=payload.value,promise?promise.then((resolve,reject)=>{this.emit({action:"SET",key:tuple.parentKey,value:tuple.data},tuple.watchers,"SET",tuple.key,payload.value)}):this.emit({action:"SET",key:tuple.parentKey,value:tuple.data},tuple.watchers,"SET",tuple.key,payload.value)):(this.data=payload.value,promise?promise.then((resolve,reject)=>{this.emitWatchers({action:"SET",key:"",value:payload.value},this.watchers)}):this.emitWatchers({action:"SET",key:"",value:payload.value},this.watchers)))},performAdd:function(payload,promise){const tuple=this.getData(payload.key);if(!tuple)return;let array=tuple.data[tuple.key];if(array){if(!Array.isArray(array))return void console.warn("(store) Data at key '"+payload.key+"' is not an Array");array.push(payload.value)}else array=[payload.value],tuple.data[tuple.key]=array;const funcs=tuple.watchers.funcs;if(funcs){const payloadSet={action:"SET",key:tuple.key,value:tuple.data};for(let n=0;n=index&&data.length>keyIndex&&(payloadOut.key=key,payloadOut.value=data[keyIndex],this.emitWatchers(payloadOut,buffer[key]))}}}else{if(void 0!==payload.value)return delete data[payload.value],void this.emitWatchers({action:"REMOVE",key:payload.value},tuple.watchers.buffer[tuple.key]);delete data[tuple.key],this.emit({action:"SET",key:tuple.parentKey,value:tuple.data},tuple.watchers,"REMOVE",tuple.key,null)}},handle:function(data,promise){for(let n=0;n0){for(let n=0;n{this.handleAction(key,payload.value)}),this.bindFuncs[key]=func),store.unwatch(prevBind[key],func),store.watch(bindPath,func),this.$[key]=store.get(bindPath)}}}else if("string"==typeof prevBind)store.unwatch(prevBind,this.bindFuncs.value),this.$.value=this.state.value;else for(let key in prevBind)store.unwatch(prevBind[key],this.bindFuncs[key]),this.bindFuncs[key]=void 0,this.$[key]=this.state[key];else if("string"==typeof value){const func=payload=>{this.handleAction("value",payload.value)};this.bindFuncs.value=func,store.watch(value,func),this.$.value=store.get(value)}else for(let key in value){const bindValue=value[key];if(!bindValue)continue;const func=payload=>{this.handleAction(key,payload.value)};this.bindFuncs[key]=func,store.watch(bindValue,func),this.$[key]=store.get(bindValue)}this._bind=value,this.dirty=!0},get bind(){return this._bind},updateAll:function(){update(this);const children=this.vnode.children;for(let n=0;n{function WabiComponent(){WabiComponentInternal.call(this)}const proto=Object.create(WabiComponentInternal.prototype);for(let key in componentProto){const param=Object.getOwnPropertyDescriptor(componentProto,key);param.get||param.set?Object.defineProperty(proto,key,param):proto[key]=componentProto[key]}proto.__componentIndex=componentIndex++;const states=proto.state;for(let key in states)Object.defineProperty(proto,"$"+key,{set:function(value){this.setState(key,value)},get:function(){return this.$[key]}});return WabiComponent.prototype=proto,WabiComponent.prototype.constructor=WabiComponent,WabiComponent}}}(),modules[8]={lastSegment:function(str){const index=str.lastIndexOf(".");return-1===index?null:str.slice(index+1)},selectElementContents:function(node){const range=document.createRange();range.selectNodeContents(node);const selection=window.getSelection();selection.removeAllRanges(),selection.addRange(range)}},function(){var VNode=modules[2].VNode,component=modules[3].component,__module5=modules[5],elementOpen=__module5.elementOpen,elementClose=__module5.elementClose,elementVoid=__module5.elementVoid,element=__module5.element,componentVoid=__module5.componentVoid,text=__module5.text,render=__module5.render,__module4=modules[4],update=__module4.update,route=__module4.route,clearRoutes=__module4.clearRoutes,__module7=modules[7],store=__module7.store,lastSegment=__module7.lastSegment,__module8=modules[8],selectElementContents=(lastSegment=__module8.lastSegment,__module8.selectElementContents);window.wabi={VNode:VNode,component:component,elementOpen:elementOpen,elementClose:elementClose,elementVoid:elementVoid,element:element,componentVoid:componentVoid,text:text,render:render,update:update,route:route,clearRoutes:clearRoutes,store:store,lastSegment:lastSegment,lastSegment:lastSegment,selectElementContents:selectElementContents}}(); -------------------------------------------------------------------------------- /ext/wabi/wabi.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/ext/wabi/wabi.patch -------------------------------------------------------------------------------- /imgsrc/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/imgsrc/grid.png -------------------------------------------------------------------------------- /notes/notes.txt: -------------------------------------------------------------------------------- 1 | : : : : :features: : : : : 2 | [LOW] all/any/not() support 3 | [LOW] texture sampling func support (*bias, *proj??, cube*) 4 | [LOW] infinite args for add/mul 5 | [MED] auto dependent node location adjustment 6 | 7 | : : : : :bugs: : : : : 8 | 9 | : : : : :improvements: : : : : 10 | - preserve undo stack when adding nodes to expr (how?) 11 | - [expr] add editors as type constructors to preserve editing capabilities 12 | 13 | : : : : :UI design: : : : : 14 | > when pin has no links 15 | - node pin contains + icon 16 | > when pin has a link 17 | - node pin contains some kind of editing icon 18 | - clicking on pin asks you what you want it to be. it can be: 19 | - numbers/constant value (clicking unlinks link if any, opens editor menu) --------- RIGHT ? 20 | - existing node (clicking goes into node selection mode) --------- TO THE LEFT 21 | - new node (clicking opens the add menu) \__ --------- LEFT LOWER 22 | - variable (clicking opens the add menu) / ^ possibly joined into one? -------- TOP LEFT 23 | - function (clicking opens a window where expression to be evaluated can be entered) -------- TOP RIGHT 24 | !> the buttons are laid out in a grid 25 | 26 | > expression editor replaces top bar 27 | - the small menu is too short to contain even a single texture2D call 28 | - top bar will allow unlocking of the node editor for casual clicks 29 | ? where to put the "clean up" checkbox? 30 | 31 | : : : : :blind spots: : : : : 32 | ~~~ these are the things that still need some serious thought ~~~ 33 | > how dimension fitting works 34 | - downdimming via swizzle (that way full selection is possible) 35 | - updimming applies swizzle after [0,0,0,1] padding (padding is necessary in the general case like 3->4 so it comes for free with the swizzle as well) 36 | 37 | > expression generation 38 | - should ref-stop at nodes referenced by at least one other node (2+ refs, otherwise could cause repeated computation) 39 | 40 | 41 | // cool shader 1 42 | vec4 t_2 = texture2D(fractalGrayscale, vTexCoord0); 43 | vec4 t_3 = smoothstep((t_2.xyzz), (vec4(vTexCoord0,0,1).xyyw), (vec4(0.5,0.477,0.5,1))); 44 | gl_FragColor = t_3; 45 | -------------------------------------------------------------------------------- /notes/shedit-pinbtn.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/notes/shedit-pinbtn.dia -------------------------------------------------------------------------------- /rsrc/wall1BaseColor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/rsrc/wall1BaseColor.jpg -------------------------------------------------------------------------------- /rsrc/wall1Metallic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/rsrc/wall1Metallic.jpg -------------------------------------------------------------------------------- /rsrc/wall1Normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/rsrc/wall1Normal.jpg -------------------------------------------------------------------------------- /rsrc/wall1Roughness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archo5/ShaderEditorHTML5/cddb72852dc74488aaa2b29a9637edcc6f51d774/rsrc/wall1Roughness.jpg -------------------------------------------------------------------------------- /shaderedit.css: -------------------------------------------------------------------------------- 1 | 2 | html { height: 100%; } 3 | body { background: #333; color: #EEE; font-family: sans-serif; font-size: 12px; user-select: none; -moz-user-select: none; margin: 0; height: 100%; } 4 | input[type=text], textarea { background: #222; border: #555 solid 1px; color: #EEE; font-size: 11px; padding: 2px; font-family: sans-serif; } 5 | .checkerBgr { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAgMAAADw5/WeAAAACVBMVEUAAAAyMjIzMzMmhMtFAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAANElEQVQI12NgYOBatYoBjVy5EsGehSyLLD6LYdWqBUDWqlUTwCSIvRIqAiZnIcSRyVkgcQADpCCh5kRXxgAAAABJRU5ErkJggg==); } 6 | .nodeCont { padding: 4px; } 7 | .node { background: #333; border: #555 solid 1px; padding: 2px; margin: 4px; min-width: 120px; box-shadow: 0 0 2px #000; border-radius: 2px; } 8 | /*.node:hover { background: #383838; }*/ 9 | .node.blinkOnce { animation: animBlinkOnce 1s; } 10 | @keyframes animBlinkOnce 11 | { 12 | from { border-color: #d73500; box-shadow: 0 0 2px #d73500; } 13 | to { border-color: #555; box-shadow: 0 0 2px #000; } 14 | } 15 | .node > .name { background: #222; padding: 3px 4px; margin: -2px -2px 2px -2px; text-shadow: 1px 1px 2px #000; } 16 | .node > .name:hover { background: #252525; } 17 | .node > .name .togglePreview { float: right; width: 14px; height: 14px; line-height: 14px; cursor: pointer; text-shadow: 0 0 2px #000; color: #666; } 18 | .node > .name .togglePreview:hover { color: #d73500; } 19 | .node .previewCont { border: #444 solid 1px; width: 118px; margin-top: 2px; position: relative; } 20 | .node .previewCont:before { content: ""; display: block; padding-top: 100%; } 21 | .node .previewCont canvas { position: absolute; left: 0; top: 0; right: 0; bottom: 0; } 22 | .node.hasRsrc .args { border-top: 1px solid #222; } 23 | .node.new { border-color: #e4d52b; box-shadow: 0 0 8px #e4d52b; } 24 | .node.toBeRemoved { border-color: #d71500; box-shadow: 0 0 8px #d71500; position: relative; } 25 | .node.toBeRemoved > .name::before { content: "to be removed"; position: absolute; left: 12px; top: 12px; color: #d31500; background: #000; padding: 2px 4px; border-radius: 4px; border: 1px solid #d31500; } 26 | .node.toBeRemoved .previewCont { display: none; } 27 | .node.toBeRemoved NodeInput { max-height: 24px; overflow: hidden; } 28 | 29 | NodeInput { position: relative; display: block; border-top: 1px solid #555; border-bottom: 1px solid #222; } 30 | .node:not(.hasRsrc) NodeInput:first-of-type { border-top: 0; } 31 | NodeInput:last-of-type { border-bottom: 0; } 32 | NodeInput .editorBtn { padding: 2px; color: #aaa; text-shadow: 0 1px 4px #000; cursor: pointer; float: right; } 33 | NodeInput .pin { width: 17px; height: 17px; line-height: 17px; font-size: 10px; margin: 2px; background: #222; border: #666 solid 1px; border-radius: 20px; display: inline-block; cursor: pointer; text-align: center; } 34 | NodeInput .pin i { padding: 3px; } 35 | NodeInput .pin:hover { background: #444; } 36 | NodeInput .pin.linked { background: rgb(200,150,0); border: 0; margin: 3px; box-shadow: 0 0 4px #000; color: rgb(148,111,0); text-shadow: 0 1px 0 #e6b018; } 37 | NodeInput .pin.linked:hover { background: rgb(240,190,40); } 38 | NodeInput .name { } 39 | NodeInput .type { border-radius: 4px; font-size: 10px; /*background: #111;*/ vertical-align: top; margin-left: 2px; padding: 1px 2px; display: inline-block; } 40 | NodeInput .type i { padding: 0 4px; } 41 | 42 | NodeInput ValueTypeEdit { background: #333; display: block; position: absolute; top: -2px; right: -3px; z-index: 1000; padding: 2px; border: 1px solid #444; box-shadow: 0 0 20px #000; width: 130px; } 43 | NodeInput ValueTypeEdit Name { padding: 2px; display: block; } 44 | NodeInput ValueTypeEdit GroupName { display: block; padding: 2px; margin: 2px; border-bottom: 1px solid #222; border-bottom: 1px solid #AAA; color: #AAA; } 45 | NodeInput ValueTypeEdit GroupOpts { display: block; } 46 | 47 | .ico { display: inline-block; line-height: 1; } 48 | .ico-num { letter-spacing: -1px; } 49 | .ico-num::before { content: "123"; } 50 | .ico-var { letter-spacing: -1px; } 51 | .ico-var::before { content: "(x)"; } 52 | .ico-function { letter-spacing: -1px; } 53 | .ico-function::before { content: "f(x)"; } 54 | 55 | RangeInput { display: flex; width: 100%; align-items: center; cursor: pointer; position: relative; } 56 | RangeInput RangeInputTrack { display: flex; width: 100%; height: 4px; box-shadow: 0px 0px 2px #000000; background: #1a1a1a; border: 1px solid #646464; } 57 | RangeInput RangeInputThumbLimiter { position: absolute; left: 1px; right: 3px; top: 0; bottom: 0; display: flex; align-items: center; } 58 | RangeInput RangeInputThumb { display: flex; box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.25); height: 10px; width: 2px; background: #d73500; position: absolute; } 59 | 60 | .valueEdit { width: 120px; } 61 | .valueEdit label { display: flex; font-size: 10px; flex-wrap: nowrap; } 62 | .valueEdit label .lbl { margin: 4px 0; } 63 | .valueEdit input[type=text] { width: 50px; margin: 2px; font-size: 10px; } 64 | .valueEdit input[type=range] { background: transparent; } 65 | .valueEdit.colEdit input[type=text] { width: 30px; } 66 | .valueEdit.colEdit RangeInputTrack { height: 8px; } 67 | .valueEdit.colEdit .row3 RangeInputTrack { background: linear-gradient(to right, black, white); } 68 | .valueEdit.colLumEdit RangeInputTrack { background: linear-gradient(to right, black, white); } 69 | ._node2 .input { min-height: 100px; } 70 | 71 | .editor { height: 100%; } 72 | .editor .area { display: flex; flex-direction: row-reverse; overflow: scroll; position: absolute; bottom: 0; top: 0; left: 0; right: 0; } 73 | .editor .area > .col { display: flex; flex-direction: column; padding: 0 4px; } 74 | .editor .area > .col:first-child { padding-right: 0; } 75 | .editor .area > .col:last-child { padding-left: 0; } 76 | .editor .areaWrapper { width: 50%; /*border: 1px solid #666; -- messes with overflow resizing*/ overflow: hidden; 77 | box-sizing: border-box; box-shadow: inset 0px 0px 10px #000; position: absolute; bottom: 0; top: 24px; left: 0; right: 50%; } 78 | .editor .topBar { position: absolute; top: 0; left: 0; right: 50%; height: 24px; background: #444; display: flex; align-items: baseline; } 79 | .editor .overlay { position: absolute; left: 0; top: 0; right: 0; bottom: 0; pointer-events: none; } 80 | 81 | .editor .functionUI { display: flex; align-items: baseline; } 82 | 83 | .editor .previewWrapper { position: absolute; left: 50%; top: 0; right: 0; bottom: 0; background: #444; padding: 8px; } 84 | .editor .previewWrapper .previewBlock { height: 100%; } 85 | .editor .previewWrapper .previewBlock canvas { display: block; cursor: move; } 86 | 87 | .editor .node * { transition: opacity 0.3s ease-out; } 88 | .editor.disableNodeControls .node * { pointer-events: none; opacity: 0.7; transition: opacity 0.15s ease-in; } 89 | .editor.disableNodeControls #curveOverlay { opacity: 0.7; } 90 | .editor.editAreaBlur .areaWrapper .eaBlurred { filter: blur(16px); } 91 | .editor.disableTopBar .topBar { display: none; } 92 | .editor.disableFunctionUI .topBar .functionUI { display: none; } 93 | 94 | .editor .form { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow-x: auto; overflow-y: scroll; background: rgba(0,0,0,0.4); padding: 16px; } 95 | .form .title { font-size: 24px; } 96 | .form .closeBtn { float: right; font-size: 24px; padding: 4px 12px; } 97 | .form .btn.applyBtn { font-size: 20px; padding: 3px 7px; margin: 0 0 0 10px; } 98 | .form .section { margin: 16px 0; font-size: 14px; } 99 | .form .section .row { margin: 8px 0; } 100 | .form .invalid { outline: #b31717 solid 2px; } 101 | .form .lbl { min-width: 200px; margin-right: 10px; display: inline-block; vertical-align: top; margin-top: 4px; } 102 | .form .btn { margin-left: 0; } 103 | 104 | .form .argsEdit { display: inline-block; } 105 | 106 | TypeSwitchBtn.btn { font-size: 14px; padding: 6px 8px; vertical-align: baseline; margin: 0 1px 0 0; } 107 | TypeSwitchBtn.btn + .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } 108 | TypeSwitchBtn.btn:not(:last-of-type) { border-top-right-radius: 0; border-bottom-right-radius: 0; } 109 | 110 | .editor .editName { font-size: 24px; } 111 | 112 | .bgr { position: fixed; left: 0; right: 0; top: 0; bottom: 0; /*background: rgba(255,0,0,0.1);*/ z-index: 999; } 113 | 114 | menuwindow { margin: 0; padding: 2px; background: #222; border: #444 solid 1px; display: inline-block; box-shadow: 0 0 8px #000; border-radius: 2px; font-size: 11px; 115 | position: fixed; left: 2px; top: 24px; z-index: 1000; } 116 | menuwindow menuitem { display: block; padding: 4px; } 117 | menuwindow menuitem i { margin-right: 8px; min-width: 12px; } 118 | menuwindow menuitem:hover { background: #d73500; } 119 | menuwindow menuitem.inactive { color: #666; text-shadow: 0 -1px 0 #111; } 120 | menuwindow menuitem.inactive:hover { background: transparent; } 121 | menuwindow menulabel { display: block; padding: 4px; background: #333; color: #AAA; } 122 | 123 | .autoCompleteTextField.open input { z-index: 1000; position: relative; } 124 | .autoCompleteTextField .options { background: #333; position: absolute; z-index: 100; padding: 2px; border: 1px solid #444; box-shadow: 0 4px 8px rgba(0,0,0,0.2); max-height: 300px; overflow-y: auto; overflow-x: hidden; } 125 | .autoCompleteTextField.open .options { z-index: 1000; } 126 | .autoCompleteTextField .options .option { padding: 4px; } 127 | .autoCompleteTextField .options .option:nth-child(even) { background: #3A3A3A; } 128 | .autoCompleteTextField .options .option:hover { background: #d73500; } 129 | .autoCompleteTextField .options .option.sel { background: #a73500; } 130 | .autoCompleteTextField .options .option name { font-size: 14px; font-weight: bold; font-style: italic; display: inline-block; min-width: 110px; padding-right: 10px; color: #DDD; user-select: none; } 131 | .autoCompleteTextField .options .option desc { color: #CCC; font-size: 11px; vertical-align: bottom; } 132 | 133 | .customScroll::-webkit-scrollbar { background: #333; border: 1px solid #555; border-radius: 2px; width: 16px; } 134 | .customScroll::-webkit-scrollbar-track { background: #222; border: 1px solid #555; border-radius: 2px; } 135 | .customScroll::-webkit-scrollbar-thumb { background: #555; border-radius: 2px; border: 1px solid #666; } 136 | /*.customScroll::-webkit-scrollbar-button { width: 16px; height: 16px; } 137 | .customScroll::-webkit-scrollbar-button:vertical:decrement::before { content: "\u25B2"; color: #EEE; }*/ 138 | .customScroll::-webkit-scrollbar-corner { background: #222; } 139 | 140 | .btn { margin: 2px; background: #555; border: 0; box-shadow: 0 0 4px rgba(0,0,0,0.5); outline: 0; padding: 1px 6px; 141 | border-radius: 2px; display: inline-block; cursor: pointer; color: #FFF; line-height: 10px; font-size: 10px; vertical-align: top; } 142 | .btn:hover { background: #666; } 143 | .btn:active { background: #333; border-color: #333; box-shadow: 1px 2px 4px rgba(0,0,0,1) inset; } 144 | .btn.used { background: #d73500; } 145 | .btn i { padding: 4px 4px 4px 0; } 146 | .btn i.io { padding: 4px 0; } 147 | .btn.disabled { pointer-events: none; background: #555; color: #888; } 148 | .btn.active { background: #d73500; } 149 | .btn.active.disabled { background: #572d1f; color: #8a6b5b; } 150 | 151 | .checkbox { margin: 4px; display: inline-block; line-height: 16px; cursor: pointer; } 152 | .checkbox i { margin-right: 4px; } 153 | .checkbox:hover i { color: #c4c4c4; } 154 | .checkbox:active i { color: #b4b4b4; } 155 | 156 | .error { color: #d73500; display: inline-block; background: #111; padding: 2px 4px; border-radius: 4px; border: 1px solid #d73500; } 157 | .error i { margin-right: 4px; } 158 | .abserr { z-index: 500; position: absolute; } 159 | 160 | NodePinEdit { display: block; position: absolute; z-index: 1000; background: #333; border: 1px solid #444; box-shadow: 0 0 20px #000; border-radius: 6px; padding: 4px; top: -28px; right: 16px; width: 175px; } 161 | NodePinEdit .selectView .btn { width: 60px; } 162 | NodePinEdit .selectView .closeBtn { width: 7px; } 163 | NodePinEdit .numberBtn { margin-left: 25px; } 164 | NodePinEdit .selectView .expressionBtn { width: 83px; } 165 | /* 166 | .unlinkBtn { background: #d73500; } 167 | .unlinkBtn:hover { background: #e74510; } 168 | .unlinkBtn:active { background: #a73500; } 169 | .newNodeBtn { background: #516d25; } 170 | .newNodeBtn:hover { background: #547324; } 171 | .newNodeBtn:active { background: #3f561c; } 172 | .numberBtn { background: #d73500; } 173 | .numberBtn:hover { background: #e74510; } 174 | .numberBtn:active { background: #a73500; } 175 | .pickNodeBtn { background: #d73500; } 176 | .pickNodeBtn:hover { background: #e74510; } 177 | .pickNodeBtn:active { background: #a73500; } 178 | .colorBtn { background: #d73500; } 179 | .colorBtn:hover { background: #e74510; } 180 | .colorBtn:active { background: #a73500; } 181 | .variableBtn { background: #d73500; } 182 | .variableBtn:hover { background: #e74510; } 183 | .variableBtn:active { background: #a73500; } 184 | .expressionBtn { background: #d73500; } 185 | .expressionBtn:hover { background: #e74510; } 186 | .expressionBtn:active { background: #a73500; }*/ 187 | NodePinEdit .addNodeView { margin-right: -1px; } 188 | NodePinEdit .addNodeView .btn { width: 72px; font-size: 12px; line-height: 32px; } 189 | NodePinEdit .exprView .expr { width: 140px; margin: 2px; } 190 | 191 | SwizzleEdit { position: relative; display: block; } 192 | SwizzleEdit > Name { font-size: 10px; vertical-align: text-bottom; } 193 | SwizzleEditPopup { background: #333; position: absolute; top: 16px; left: 0px; z-index: 1000; padding: 2px; border: 1px solid #444; box-shadow: 0 0 8px #000; } 194 | SwizzleEditPopupAxes { display: flex; flex-direction: row; } 195 | SwizzleEditCol { display: flex; flex-direction: column; align-items: center; } 196 | SwizzleEditCol Name { width: 100%; text-align: center; border-bottom: 1px solid #444; box-shadow: 0px -1px inset #111; padding-bottom: 2px; font-weight: bold; } 197 | SwizzleEditCol ColEls { box-shadow: 0 0 4px rgba(0,0,0,0.5); display: grid; margin: 2px; border-radius: 6px; } 198 | SwizzleEditCol ColEls .btn { min-width: 6px; text-align: center; line-height: 12px; box-shadow: none; padding: 2px 5px; margin: 0; } 199 | SwizzleEditCol ColEls .btn + .btn { border-top-left-radius: 0; border-top-right-radius: 0; margin-top: 0; border-top: 0; } 200 | SwizzleEditCol ColEls .btn:not(:last-of-type) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 1px; border-bottom: 0; } 201 | 202 | AxisMarker { display: inline-block; position: relative; width: 9px; height: 9px; } 203 | AxisMarker.dots-1, AxisMarker.dots-2 { width: 4px; } 204 | AxisMarker Dot { display: block; position: absolute; width: 4px; height: 4px; border-radius: 2px; box-shadow: 0 0 2px #000; } 205 | AxisMarker .dot-x { background: #d04611; } 206 | AxisMarker .dot-y { background: #47bd17; } 207 | AxisMarker .dot-z { background: #4a75ef; } 208 | AxisMarker .dot-w { background: #b1b1b1; } 209 | AxisMarker.dots-1 .dot-x { left: 0px; top: 3px; } 210 | AxisMarker.dots-2 .dot-x { left: 0px; top: 0px; } 211 | AxisMarker.dots-2 .dot-y { left: 0px; top: 5px; } 212 | AxisMarker.dots-3 .dot-x { left: 3px; top: 0px; } 213 | AxisMarker.dots-3 .dot-y { left: 0px; top: 5px; } 214 | AxisMarker.dots-3 .dot-z { left: 5px; top: 5px; } 215 | AxisMarker.dots-4 .dot-x { left: 0px; top: 0px; } 216 | AxisMarker.dots-4 .dot-y { left: 5px; top: 0px; } 217 | AxisMarker.dots-4 .dot-z { left: 5px; top: 5px; } 218 | AxisMarker.dots-4 .dot-w { left: 0px; top: 5px; } 219 | 220 | .selectWrap { width: 120px; } 221 | .selectCont { position: relative; display: block; } 222 | .selectBtn { display: flex; border: 1px solid #444; padding: 4px; box-shadow: 0 0 2px inset #444; background: #222; cursor: pointer; font-size: 11px; } 223 | .selectBtn:hover { background: #282828; } 224 | .selectBtn Name { flex-grow: 1; } 225 | .selectBtn ToggleMarker { text-shadow: 0 0 2px #000; color: #666; } 226 | .selectCont.open ToggleMarker { color: #444; } 227 | .selectCont .autoCompleteTextField.open { position: absolute; top: 2px; } 228 | .selectCont .autoCompleteTextField.open input { width: 96px; background: #222; border: 0; color: #EEE; margin: 0 2px; font-size: 12px; padding: 2px; } 229 | 230 | .node .selectBtn { margin: 2px 0; } 231 | 232 | .topBar .lbl { padding: 2px 2px 2px 10px; } 233 | .topBar AddNodeAC input[type=text] { min-width: 200px; margin: 2px; } 234 | .topBar ExprEdit { width: 90%; } 235 | .topBar ExprEdit input[type=text] { width: 50%; } 236 | .topBar ExprEdit .btn { vertical-align: baseline; } 237 | 238 | .menuBar { } 239 | .menuBar menuitem { padding: 2px 8px; height: 20px; line-height: 20px; } 240 | .menuBar menuitem:hover { background: #a22; } 241 | .menuBar menuitem:hover > menuwindow { display: block; } 242 | .menuBar menuitem menuwindow { display: none; min-width: 120px; position: absolute; top: 24px; left: 4px; } 243 | .menuBar menuwindow menuitem { display: block; } 244 | .menuBar menuwindow menuitem menuwindow { left: 120px; top: 4px; } 245 | .menuBar .submenuMarker { float: right; margin: 5px 0 0 0; } 246 | 247 | input[type=range] { 248 | -webkit-appearance: none; 249 | width: 100%; 250 | margin: 3px 0; 251 | } 252 | input[type=range]:focus { 253 | outline: none; 254 | } 255 | input[type=range]::-webkit-slider-runnable-track { 256 | width: 100%; 257 | height: 4px; 258 | cursor: pointer; 259 | box-shadow: 0px 0px 2px #000000, 0px 0px 0px #0d0d0d; 260 | background: #1a1a1a; 261 | border-radius: 0px; 262 | border: 1px solid #646464; 263 | } 264 | input[type=range]::-webkit-slider-thumb { 265 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.25), 0px 0px 0px rgba(13, 13, 13, 0.25); 266 | border: 0px solid #000000; 267 | height: 10px; 268 | width: 2px; 269 | border-radius: 0px; 270 | background: #d73500; 271 | cursor: pointer; 272 | -webkit-appearance: none; 273 | margin-top: -4px; 274 | } 275 | input[type=range]:focus::-webkit-slider-runnable-track { 276 | background: #272727; 277 | } 278 | input[type=range]::-moz-range-track { 279 | width: 100%; 280 | height: 4px; 281 | cursor: pointer; 282 | box-shadow: 0px 0px 2px #000000, 0px 0px 0px #0d0d0d; 283 | background: #1a1a1a; 284 | border-radius: 0px; 285 | border: 1px solid #646464; 286 | } 287 | input[type=range]::-moz-range-thumb { 288 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.25), 0px 0px 0px rgba(13, 13, 13, 0.25); 289 | border: 0px solid #000000; 290 | height: 10px; 291 | width: 2px; 292 | border-radius: 0px; 293 | background: #d73500; 294 | cursor: pointer; 295 | } 296 | input[type=range]::-ms-track { 297 | width: 100%; 298 | height: 4px; 299 | cursor: pointer; 300 | background: transparent; 301 | border-color: transparent; 302 | color: transparent; 303 | } 304 | input[type=range]::-ms-fill-lower { 305 | background: #0d0d0d; 306 | border: 1px solid #646464; 307 | border-radius: 0px; 308 | box-shadow: 0px 0px 2px #000000, 0px 0px 0px #0d0d0d; 309 | } 310 | input[type=range]::-ms-fill-upper { 311 | background: #1a1a1a; 312 | border: 1px solid #646464; 313 | border-radius: 0px; 314 | box-shadow: 0px 0px 2px #000000, 0px 0px 0px #0d0d0d; 315 | } 316 | input[type=range]::-ms-thumb { 317 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.25), 0px 0px 0px rgba(13, 13, 13, 0.25); 318 | border: 0px solid #000000; 319 | height: 10px; 320 | width: 2px; 321 | border-radius: 0px; 322 | background: #d73500; 323 | cursor: pointer; 324 | height: 4px; 325 | } 326 | input[type=range]:focus::-ms-fill-lower { 327 | background: #1a1a1a; 328 | } 329 | input[type=range]:focus::-ms-fill-upper { 330 | background: #272727; 331 | } 332 | -------------------------------------------------------------------------------- /shaderedit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ShaderEdit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 117 | 118 | 119 | --------------------------------------------------------------------------------