littlejsengine 1.24.3 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -49,6 +49,18 @@ class PostProcessPlugin
49
49
  /** @property {string} - The shadertoy style mainImage code it shades with, see setShaderCode */
50
50
  this.shaderCode = shaderCode || postProcessEffects(); // no code passes the frame through
51
51
 
52
+ /** @property {Object<string, number|Array<number>>} - The game's own values for the shader, a uniform each
53
+ * by its name, a number a float and a list of 2 to 4 numbers a vector, set every frame as they are; an
54
+ * effect setting can be one of these names, so it changes every frame without making the shader again, all
55
+ * but glow's size, which sets how many samples it takes; adding or removing a name makes the shader again,
56
+ * and the shader is first made at the first render, so values set right after the plugin are in it; a name
57
+ * is a GLSL name not starting with an underscore, which the effects keep for their own, nor i and a capital
58
+ * or gl_, and not c, uv or p, the names mainImage works on
59
+ * @type {Object<string, number|Array<number>>} */
60
+ this.values = {};
61
+ // the names of the values the shader was made with
62
+ this.valueNames = '';
63
+
52
64
  /** @property {WebGLProgram|undefined} - Shader for post processing
53
65
  * @type {WebGLProgram|undefined} */
54
66
  this.shader = undefined;
@@ -62,18 +74,14 @@ class PostProcessPlugin
62
74
  * @type {WebGLVertexArrayObject|undefined} */
63
75
  this.vao = undefined;
64
76
 
65
- // setup the post processing plugin
66
- initPostProcess();
77
+ // the shader is made at the first render, so values the game sets after this, which its code may name, are
78
+ // declared in it
79
+ !headlessMode && !glEnable && console.warn('PostProcessPlugin: WebGL not enabled!');
67
80
  engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
68
81
 
69
82
  function initPostProcess()
70
83
  {
71
- if (headlessMode) return;
72
- if (!glEnable)
73
- {
74
- console.warn('PostProcessPlugin: WebGL not enabled!');
75
- return;
76
- }
84
+ if (headlessMode || !glEnable) return;
77
85
 
78
86
  // create resources, the feedback starting black, as if the frame before the first were empty
79
87
  if (feedbackTexture)
@@ -93,8 +101,9 @@ class PostProcessPlugin
93
101
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
94
102
  '}' // end of shader
95
103
  ,
96
- postProcessFragmentSource(postProcess.shaderCode)
104
+ postProcessFragmentSource(postProcess.shaderCode, postProcess.values)
97
105
  );
106
+ postProcess.valueNames = postProcessValueKey(postProcess.values);
98
107
 
99
108
  // setup VAO for post processing
100
109
  postProcess.vao = glContext.createVertexArray();
@@ -130,13 +139,17 @@ class PostProcessPlugin
130
139
  // clear out the buffer, before anything here binds its own
131
140
  glFlush();
132
141
 
133
- // made now if WebGL was off when the plugin was made, when a lost context came back, or when
134
- // setShaderCode gave it new code
142
+ // made now if WebGL was off when the plugin was made, when a lost context came back, when setShaderCode
143
+ // gave it new code, or when the game added or took away a value, which the shader declares
144
+ if (postProcess.shader && postProcessValueKey(postProcess.values) !== postProcess.valueNames)
145
+ postProcess.setShaderCode(postProcess.shaderCode);
135
146
  if (!postProcess.shader)
136
147
  {
137
148
  if (glContext.isContextLost()) return;
138
149
  initPostProcess();
139
150
  }
151
+ if (glFailedPrograms.has(postProcess.shader))
152
+ return; // a shader that did not build in a release build, the frame shows as it is
140
153
 
141
154
  // ensure we render to the default framebuffer (in case any earlier
142
155
  // caller this frame left a render target bound)
@@ -194,9 +207,10 @@ class PostProcessPlugin
194
207
  glContext.activeTexture(glContext.TEXTURE2);
195
208
  glContext.bindTexture(glContext.TEXTURE_2D, depth?.cameraDepthTexture || null);
196
209
  glContext.activeTexture(glContext.TEXTURE0);
210
+ // all 0 without one, which an effect reads as no depth
197
211
  const camera = depth?.camera;
198
- camera && glContext.uniform3f(glUniformLocation(postProcess.shader, 'iDepthRange'), camera.near,
199
- camera.far == Infinity ? 0 : camera.far, camera.orthographic ? 1 : 0);
212
+ glContext.uniform3f(glUniformLocation(postProcess.shader, 'iDepthRange'), camera?.near || 0,
213
+ !camera || camera.far == Infinity ? 0 : camera.far, camera?.orthographic ? 1 : 0);
200
214
 
201
215
  // set uniforms and draw
202
216
  const uniformLocation = (name)=>glUniformLocation(postProcess.shader, name);
@@ -205,6 +219,14 @@ class PostProcessPlugin
205
219
  glContext.uniform1i(uniformLocation('iChannel2'), 2);
206
220
  glContext.uniform1f(uniformLocation('iTime'), time);
207
221
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
222
+ for (const name in postProcess.values)
223
+ {
224
+ const value = postProcess.values[name], location = uniformLocation(name);
225
+ if (isArray(value))
226
+ [, , glContext.uniform2fv, glContext.uniform3fv, glContext.uniform4fv][value.length]?.call(glContext, location, value);
227
+ else
228
+ glContext.uniform1f(location, value);
229
+ }
208
230
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
209
231
 
210
232
  if (feedbackTexture)
@@ -254,37 +276,56 @@ class PostProcessPlugin
254
276
 
255
277
  ///////////////////////////////////////////////////////////////////////////////
256
278
 
279
+ // what the shader declares of the game's values, each name and its length, so a value that changes either makes the
280
+ // shader again
281
+ const postProcessValueKey = (values)=> Object.entries(values).map(([name, v])=> name + ':' + (isArray(v) ? v.length : 1)).join();
282
+
257
283
  // the fragment shader of the post process pass around its mainImage snippet: the frame on iChannel0, the last output
258
284
  // on iChannel1 with a feedback texture, and the 3D depth on iChannel2 when render3D.depthTexture is on, read with
259
- // sceneDepth(uv), the distance from the camera along its view in world units, uv 0 to 1 across the screen
260
- function postProcessFragmentSource(shaderCode)
285
+ // sceneDepth(uv), the distance from the camera along its view in world units, uv 0 to 1 across the screen; and a
286
+ // uniform for each of the game's values, a float or a vector of the list's length
287
+ function postProcessFragmentSource(shaderCode, values={})
261
288
  {
289
+ let declared = '';
290
+ for (const name in values)
291
+ {
292
+ const value = values[name], length = isArray(value) ? value.length : 1;
293
+ ASSERT(postProcessNameCheck(name), 'a postProcess value needs a name GLSL takes that the shader does not use', name);
294
+ ASSERT(!isArray(value) || length >= 2 && length <= 4, 'a postProcess value is a number or 2 to 4 of them', name);
295
+ declared += `uniform ${length > 1 ? 'vec' + length : 'float'} ${name};`;
296
+ }
262
297
  return '#version 300 es\n' + // specify GLSL ES version
263
298
  'precision highp float;'+ // use highp for accuracy
264
299
  'uniform sampler2D iChannel0;'+ // input texture
265
300
  'uniform sampler2D iChannel1;'+ // the previous frame's output, when feedbackTexture is set
266
- 'uniform sampler2D iChannel2;'+ // the 3D depth, when render3D.depthTexture is on
301
+ 'uniform highp sampler2D iChannel2;'+ // the 3D depth, when render3D.depthTexture is on, highp or it is 11 bits
267
302
  'uniform vec3 iResolution;'+ // size of output texture
268
303
  'uniform float iTime;'+ // time
269
304
  'uniform vec3 iDepthRange;'+ // the camera's near, its far or 0 for none, and 1 when orthographic
305
+ declared + // the game's own values
270
306
  'out vec4 c;'+ // out color
271
307
  // the depth texture's value back to a distance, as the camera's projection put it there
272
308
  'float sceneDepth(vec2 uv){'+
273
309
  'float d=texture(iChannel2,uv).r*2.-1.,n=iDepthRange.x,f=iDepthRange.y;'+
274
- 'return iDepthRange.z>0.?(d*(f-n)+f+n)/2.:f>0.?2.*n*f/(f+n-d*(f-n)):2.*n/(1.-d);}'+
275
- '\n' + shaderCode + '\n'+ // insert custom shader code
310
+ 'return iDepthRange.z>0.?(d*(f-n)+f+n)/2.:f>0.?2.*n*f/(f+n-d*(f-n)):2.*n/max(1.-d,1e-7);}'+
311
+ // whether there is depth to read: a range of all 0 is none, an orthographic near plane may be behind
312
+ '\n#define LJS_HAS_DEPTH (iDepthRange != vec3(0))\n'+ // a define needs a line of its own
313
+ // main first, the code's mainImage declared for it, so a define in the code can not reach main
314
+ 'void mainImage(out vec4,vec2);'+
276
315
  'void main(){'+ // shader entry point
277
316
  'mainImage(c,gl_FragCoord.xy);'+ // call post process function
278
317
  'c.a=1.;'+ // always use full alpha
279
- '}'; // end of shader
318
+ '}'+ // end of shader
319
+ '\n' + shaderCode + '\n'; // insert custom shader code
280
320
  }
281
321
 
282
322
  /**
283
323
  * Shader code for a bloom effect, the bright parts of the image blurred back over it
284
324
  * - Pass it to PostProcessPlugin, or edit the string to build an effect on top of it
285
- * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
286
- * @param {number} [strength] - How much glow to add
287
- * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes
325
+ * @param {number|string} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
326
+ * @param {number|string} [strength] - How much glow to add
327
+ * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes, so a
328
+ * number and not a value's name
288
329
  * @return {string}
289
330
  * @memberof PostProcess
290
331
  */
@@ -295,8 +336,8 @@ function postProcessBloomShader(threshold=.6, strength=1, size=6)
295
336
 
296
337
  /**
297
338
  * Set up post processing with a bloom effect, so bright colors and lights glow
298
- * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
299
- * @param {number} [strength] - How much glow to add
339
+ * @param {number|string} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
340
+ * @param {number|string} [strength] - How much glow to add
300
341
  * @param {number} [size] - How far the glow spreads in pixels
301
342
  * @param {boolean} [includeMainCanvas] - Glow the 2D canvas too, off by default so HUD text stays crisp
302
343
  * (a HUD drawn with WebGL in gameRenderPost glows either way, draw it with useWebGL=false)
@@ -314,7 +355,32 @@ function postProcessBloom(threshold=.6, strength=1, size=6, includeMainCanvas=fa
314
355
  // its pixel; one that bends uv or samples the frame, the curve, chromatic and glow, goes before the ones that shade
315
356
 
316
357
  // a number as GLSL writes it, a float with a point
317
- const postProcessNumber = (n)=> (ASSERT(isNumber(n), 'effect settings must be numbers', n), n.toFixed(4));
358
+ // an effect setting: a number written into the code, or the name of a postProcess value, read from it each frame
359
+ const postProcessNumber = (n)=> typeof n === 'string' ?
360
+ (ASSERT(postProcessNameCheck(n), 'an effect setting is a number or the name of a postProcess value', n), n) :
361
+ (ASSERT(isNumber(n) && isFinite(n), 'an effect setting is a finite number or the name of a postProcess value', n),
362
+ n && abs(n) < .01 ? String(n) : n.toFixed(4)); // a small one keeps its digits, as 1.5e-7 or 0.00015
363
+
364
+ // a GLSL name for a value or a setting, which no part of the shader uses already: not the engine's own, i and a
365
+ // capital, nor those of mainImage or an effect's own locals, which start with an underscore, nor a GLSL word
366
+ const postProcessGLSLWords = new Set(('c uv p sceneDepth mainImage main ' +
367
+ // keywords and reserved words
368
+ 'attribute const uniform varying layout centroid flat smooth noperspective patch sample break continue do for ' +
369
+ 'while switch case default if else subroutine in out inout float double int void bool true false invariant ' +
370
+ 'precise discard return lowp mediump highp precision struct common partition active asm class union enum ' +
371
+ 'typedef template this resource goto inline noinline public static extern external interface long short half ' +
372
+ 'fixed unsigned superp input output filter sizeof cast namespace using coherent volatile restrict readonly ' +
373
+ 'writeonly atomic_uint uint mat2 mat3 mat4 vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 uvec2 uvec3 uvec4 ' +
374
+ // built in functions
375
+ 'radians degrees sin cos tan asin acos atan sinh cosh tanh asinh acosh atanh pow exp log exp2 log2 sqrt ' +
376
+ 'inversesqrt abs sign floor trunc round roundEven ceil fract mod modf min max clamp mix step smoothstep isnan ' +
377
+ 'isinf length distance dot cross normalize faceforward reflect refract matrixCompMult outerProduct transpose ' +
378
+ 'determinant inverse lessThan lessThanEqual greaterThan greaterThanEqual equal notEqual any all not textureSize ' +
379
+ 'texture textureProj textureLod textureOffset texelFetch texelFetchOffset textureProjOffset textureLodOffset ' +
380
+ 'textureProjLod textureProjLodOffset textureGrad textureGradOffset textureProjGrad textureProjGradOffset dFdx ' +
381
+ 'dFdy fwidth').split(' '));
382
+ const postProcessNameCheck = (name)=> /^[A-Za-z][A-Za-z0-9_]*$/.test(name) && !name.includes('__') &&
383
+ !/^(i[A-Z]|gl_|webgl_|GL_|mat[2-4]|[dfh]vec[2-4]|[iu]?(sampler|image)([123]D|Cube|Buffer|External))/.test(name) && !postProcessGLSLWords.has(name);
318
384
 
319
385
  /**
320
386
  * Join effects into one post process shader, in the order given, for PostProcessPlugin or setShaderCode
@@ -339,15 +405,16 @@ function postProcessEffects(...effects)
339
405
 
340
406
  /**
341
407
  * Bright parts glow, the bloom as an effect to join with others; postProcessBloom sets up bloom on its own
342
- * @param {number} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
343
- * @param {number} [strength] - How much glow to add
344
- * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes
408
+ * @param {number|string} [threshold] - Brightness where the glow starts, 0 is everything and 1 is only pure white
409
+ * @param {number|string} [strength] - How much glow to add
410
+ * @param {number} [size] - How far the glow spreads in pixels, which also sets how many samples it takes, so a
411
+ * number and not a value's name
345
412
  * @return {string}
346
413
  * @memberof PostProcess
347
414
  */
348
415
  function postProcessGlow(threshold=.6, strength=1, size=6)
349
416
  {
350
- ASSERT(isNumber(threshold) && isNumber(strength) && isNumber(size), 'glow settings must be numbers');
417
+ ASSERT(isNumber(size), 'glow size is a number, it sets how many samples the glow takes');
351
418
  ASSERT(size > 0, 'glow size must be above zero');
352
419
  ASSERT(size <= 32, 'a glow this wide takes a sample every few pixels of every ring, which is hundreds of samples a pixel', size);
353
420
 
@@ -365,21 +432,22 @@ function postProcessGlow(threshold=.6, strength=1, size=6)
365
432
  const count = max(5 + 2 * j, round(2 * radius)) | 1;
366
433
  taps += count;
367
434
  code += `
368
- for (int k = 0; k < ${count}; ++k)
435
+ for (int _k = 0; _k < ${count}; ++_k)
369
436
  {
370
- float a = float(k) * ${(2 * PI / count).toFixed(7)}${j ? ' + ' + (j * 2.3999632).toFixed(7) : ''};
371
- glow += max(vec3(0), texture(iChannel0, uv + vec2(cos(a), sin(a)) * ${radius.toFixed(4)} / iResolution.xy).rgb - ${threshold.toFixed(4)});
437
+ float _a = float(_k) * ${(2 * PI / count).toFixed(7)}${j ? ' + ' + (j * 2.3999632).toFixed(7) : ''};
438
+ _glow += max(vec3(0), texture(iChannel0, uv + vec2(cos(_a), sin(_a)) * ${radius.toFixed(4)} / iResolution.xy).rgb - ${postProcessNumber(threshold)});
372
439
  }`;
373
440
  }
374
441
  return ` // glow
375
- vec3 glow = vec3(0);${code}
376
- c.rgb += glow * ${(strength / taps).toFixed(6)};`;
442
+ vec3 _glow = vec3(0);${code}
443
+ c.rgb += _glow * ${typeof strength === 'string' ? postProcessNumber(strength) + ' / ' + taps + '.' :
444
+ (strength / taps).toFixed(6)};`; // a number as before, a value's name divided in the shader
377
445
  }
378
446
 
379
447
  /**
380
448
  * Scan lines across the screen, like an old TV
381
- * @param {number} [strength] - How dark the lines are, and how bright between them
382
- * @param {number} [spacing] - Pixels from one line to the next
449
+ * @param {number|string} [strength] - How dark the lines are, and how bright between them
450
+ * @param {number|string} [spacing] - Pixels from one line to the next
383
451
  * @return {string}
384
452
  * @memberof PostProcess
385
453
  */
@@ -391,66 +459,130 @@ function postProcessScanlines(strength=.5, spacing=6)
391
459
 
392
460
  /**
393
461
  * Static noise over the picture, changing every frame
394
- * @param {number} [strength] - How bright the static is
395
- * @param {number} [size] - Size of a speck in pixels
462
+ * @param {number|string} [strength] - How bright the static is
463
+ * @param {number|string} [size] - Size of a speck in pixels
396
464
  * @return {string}
397
465
  * @memberof PostProcess
398
466
  */
399
467
  function postProcessNoise(strength=.1, size=2)
400
468
  {
401
469
  return ` // noise
402
- vec2 q = fract((floor(p / ${postProcessNumber(size)}) + mod(iTime * 500., 1e3)) * .3197);
403
- c.rgb += ${postProcessNumber(strength)} * fract(1. + sin(51. * q.x + 73. * q.y) * 13753.3);`;
470
+ vec2 _q = fract((floor(p / ${postProcessNumber(size)}) + mod(iTime * 500., 1e3)) * .3197);
471
+ c.rgb += ${postProcessNumber(strength)} * fract(1. + sin(51. * _q.x + 73. * _q.y) * 13753.3);`;
404
472
  }
405
473
 
406
474
  /**
407
475
  * Darken toward the edges and corners
408
- * @param {number} [strength] - How dark the corners get, 1 is black
409
- * @param {number} [falloff] - How far in it reaches, low darkens most of the screen, high only the corners
476
+ * @param {number|string} [strength] - How dark the corners get, 1 is black
477
+ * @param {number|string} [falloff] - How far in it reaches, low darkens most of the screen, high only the corners
410
478
  * @return {string}
411
479
  * @memberof PostProcess
412
480
  */
413
481
  function postProcessVignette(strength=1, falloff=3)
414
482
  {
415
483
  return ` // vignette
416
- vec2 d = uv * 2. - 1.;
417
- c.rgb *= 1. - ${postProcessNumber(strength)} * min(1., pow(dot(d, d) / 2., ${postProcessNumber(falloff)}));`;
484
+ vec2 _d = uv * 2. - 1.;
485
+ c.rgb *= 1. - ${postProcessNumber(strength)} * min(1., pow(dot(_d, _d) / 2., ${postProcessNumber(falloff)}));`;
418
486
  }
419
487
 
420
488
  /**
421
489
  * Bend the picture like the bulged glass of an old TV, black past the corners; put it first
422
- * @param {number} [strength] - How much it bends
490
+ * @param {number|string} [strength] - How much it bends
423
491
  * @return {string}
424
492
  * @memberof PostProcess
425
493
  */
426
494
  function postProcessCurve(strength=.1)
427
495
  {
428
496
  return ` // curve
429
- vec2 d = uv * 2. - 1.;
430
- d *= 1. + ${postProcessNumber(strength)} * dot(d, d);
431
- uv = d * .5 + .5;
432
- c = all(lessThan(abs(d), vec2(1))) ? texture(iChannel0, uv) : vec4(0, 0, 0, 1);`;
497
+ vec2 _d = uv * 2. - 1.;
498
+ _d *= 1. + ${postProcessNumber(strength)} * dot(_d, _d);
499
+ uv = _d * .5 + .5;
500
+ c = all(lessThan(abs(_d), vec2(1))) ? texture(iChannel0, uv) : vec4(0, 0, 0, 1);`;
433
501
  }
434
502
 
435
503
  /**
436
504
  * Split red and blue apart toward the edges, like a cheap lens; put it before what shades the picture
437
- * @param {number} [strength] - How far apart at the edge, as a part of the screen
505
+ * @param {number|string} [strength] - How far apart at the edge, as a part of the screen
438
506
  * @return {string}
439
507
  * @memberof PostProcess
440
508
  */
441
509
  function postProcessChromatic(strength=.005)
442
510
  {
443
511
  return ` // chromatic
444
- vec2 d = (uv - .5) * ${postProcessNumber(strength)} * 2.;
445
- c.r = texture(iChannel0, uv + d).r;
446
- c.b = texture(iChannel0, uv - d).b;`;
512
+ vec2 _d = (uv - .5) * ${postProcessNumber(strength)} * 2.;
513
+ c.r = texture(iChannel0, uv + _d).r;
514
+ c.b = texture(iChannel0, uv - _d).b;`;
515
+ }
516
+
517
+ // a blur's widest is 32 pixels, past which its 24 taps sit far enough apart to show; a value's is the game's to keep
518
+ const postProcessBlurCheck = (blur)=>
519
+ ASSERT(typeof blur === 'string' || blur <= 32, 'a blur of more than 32 pixels shows its taps as copies', blur);
520
+
521
+ // a blur over a disc of radius _r pixels around uv, of 24 taps spread evenly by the golden angle; weight, when given,
522
+ // is GLSL for how much a tap at _puv, _s of the radius out, counts, from 0 to 1
523
+ function postProcessDiscBlur(weight)
524
+ {
525
+ return `
526
+ vec3 _sum = vec3(0);
527
+ float _total = 0.;
528
+ for (int _k = 0; _k < 24; ++_k)
529
+ {
530
+ float _s = sqrt((float(_k) + .5) / 24.), _a = float(_k) * 2.39996;
531
+ vec2 _puv = uv + vec2(cos(_a), sin(_a)) * _s * _r / iResolution.xy;
532
+ float _w = ${weight || '1.'};
533
+ _sum += texture(iChannel0, _puv).rgb * _w;
534
+ _total += _w;
535
+ }
536
+ c.rgb = _total > 0. ? _sum / _total : c.rgb;`;
537
+ }
538
+
539
+ /**
540
+ * Keep a band across the screen sharp and blur the picture above and below it, as a tilt shift lens does, which
541
+ * makes a scene look like a small model; it reads only the screen, so it works in 2D and 3D; put it first
542
+ * @param {number|string} [focus] - Height of the middle of the sharp band, 0 the bottom of the screen and 1 the top
543
+ * @param {number|string} [size] - Height of the sharp band, as a part of the screen
544
+ * @param {number|string} [blur] - Widest blur in pixels, reached half the screen past the band, at most 32
545
+ * @return {string}
546
+ * @memberof PostProcess
547
+ * @example
548
+ * new PostProcessPlugin(postProcessEffects(postProcessTiltShift(.4, .2, 10), postProcessVignette(.5)));
549
+ */
550
+ function postProcessTiltShift(focus=.5, size=.25, blur=8)
551
+ {
552
+ postProcessBlurCheck(blur);
553
+ const n = postProcessNumber;
554
+ return ` // tilt shift
555
+ float _r = ${n(blur)} * smoothstep(0., .5, abs(uv.y - ${n(focus)}) - ${n(size)} * .5);${postProcessDiscBlur()}`;
556
+ }
557
+
558
+ /**
559
+ * Keep what is a distance from the camera sharp and blur what is nearer or farther, as a camera lens does; needs
560
+ * render3D.depthTexture on, and blurs 3D only, 2D draws having no depth; put it first
561
+ * - What is in focus stays sharp at its edges: a blur in front of or behind it leaves out what is in focus
562
+ * @param {number|string} [focus] - Distance from the camera, along its view, that is sharpest, in world units
563
+ * @param {number|string} [range] - How deep the sharp part is; the blur grows over as far again past it
564
+ * @param {number|string} [blur] - Widest blur in pixels, at most 32
565
+ * @return {string}
566
+ * @memberof PostProcess
567
+ * @example
568
+ * render3D.depthTexture = true;
569
+ * new PostProcessPlugin(postProcessEffects(postProcessDepthOfField(10, 4, 8)));
570
+ */
571
+ function postProcessDepthOfField(focus=10, range=4, blur=8)
572
+ {
573
+ postProcessBlurCheck(blur);
574
+ const n = postProcessNumber;
575
+ const amount = (depth)=> `${n(blur)} * smoothstep(0., ${n(range)}, abs(${depth} - ${n(focus)}) - ${n(range)} * .5)`;
576
+ // a tap counts as far as its own blur reaches back to here, so a sharp thing in front is not smeared over
577
+ return ` // depth of field, none where there is no depth, as with render3D.depthTexture off
578
+ float _r = LJS_HAS_DEPTH ? ${amount('sceneDepth(uv)')} : 0.;${postProcessDiscBlur(`clamp(${amount('sceneDepth(_puv)')} - _s * _r + 1., 0., 1.)`)}`;
447
579
  }
448
580
 
449
581
  /**
450
582
  * Draw lines where the 3D depth jumps, around objects and along their creases; needs render3D.depthTexture on
451
583
  * @param {Color} [color] - The lines' color, its alpha how strong they are
452
- * @param {number} [thickness] - How wide the lines are in pixels
453
- * @param {number} [threshold] - How big a jump makes a line, as a part of the distance, lower draws more
584
+ * @param {number|string} [thickness] - How wide the lines are in pixels
585
+ * @param {number|string} [threshold] - How big a jump makes a line, as a part of the distance, lower draws more
454
586
  * @return {string}
455
587
  * @memberof PostProcess
456
588
  */
@@ -459,24 +591,25 @@ function postProcessOutline(color=BLACK, thickness=1, threshold=.02)
459
591
  ASSERT(isColor(color), 'outline color must be a Color');
460
592
  const n = postProcessNumber;
461
593
  return ` // outline
462
- vec2 o = ${n(thickness)} / iResolution.xy;
463
- float d = sceneDepth(uv);
464
- float dx = abs(sceneDepth(uv + vec2(o.x, 0)) + sceneDepth(uv - vec2(o.x, 0)) - 2. * d);
465
- float dy = abs(sceneDepth(uv + vec2(0, o.y)) + sceneDepth(uv - vec2(0, o.y)) - 2. * d);
466
- vec4 line = vec4(${n(color.r)}, ${n(color.g)}, ${n(color.b)}, ${n(color.a)});
467
- c.rgb = mix(c.rgb, line.rgb, line.a * step(${n(threshold)}, max(dx, dy) / d));`;
594
+ vec2 _o = ${n(thickness)} / iResolution.xy;
595
+ float _d = sceneDepth(uv);
596
+ float _dx = abs(sceneDepth(uv + vec2(_o.x, 0)) + sceneDepth(uv - vec2(_o.x, 0)) - 2. * _d);
597
+ float _dy = abs(sceneDepth(uv + vec2(0, _o.y)) + sceneDepth(uv - vec2(0, _o.y)) - 2. * _d);
598
+ vec4 _line = vec4(${n(color.r)}, ${n(color.g)}, ${n(color.b)}, ${n(color.a)});
599
+ if (LJS_HAS_DEPTH)
600
+ c.rgb = mix(c.rgb, _line.rgb, _line.a * step(${n(threshold)}, max(_dx, _dy) / max(abs(_d), 1e-6)));`;
468
601
  }
469
602
 
470
603
  /**
471
604
  * The look of an old TV, as one effect to use alone or join with others: static noise, scan lines, a soft glow and
472
605
  * a vignette, and a bulged screen when curve is set; any setting at 0 leaves that part out
473
606
  * @param {Object} [settings]
474
- * @param {number} [settings.noise] - Static noise strength
475
- * @param {number} [settings.scanlines] - Scan line strength
476
- * @param {number} [settings.scanlineSpacing] - Pixels from one scan line to the next
477
- * @param {number} [settings.glow] - Soft glow strength
478
- * @param {number} [settings.vignette] - Vignette strength
479
- * @param {number} [settings.curve] - How much the screen bulges, 0 by default for flat
607
+ * @param {number|string} [settings.noise] - Static noise strength
608
+ * @param {number|string} [settings.scanlines] - Scan line strength
609
+ * @param {number|string} [settings.scanlineSpacing] - Pixels from one scan line to the next
610
+ * @param {number|string} [settings.glow] - Soft glow strength
611
+ * @param {number|string} [settings.vignette] - Vignette strength
612
+ * @param {number|string} [settings.curve] - How much the screen bulges, 0 by default for flat
480
613
  * @return {string}
481
614
  * @example
482
615
  * new PostProcessPlugin(postProcessEffects(postProcessTV({scanlines: .4, curve: .1})));