├── .babelrc ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── .vscode └── settings.json ├── API.md ├── Awesome Fields.md ├── LICENSE ├── README.md ├── ScreenRecording.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.test.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── deploy.sh ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── components │ ├── About.vue │ ├── CodeEditor.vue │ ├── ColorPicker.vue │ ├── Controls.vue │ ├── Inputs.vue │ ├── Ruler.vue │ ├── Settings.vue │ ├── Share.vue │ ├── VectorView.vue │ ├── glsl-theme.styl │ ├── glslmode.js │ ├── help │ │ ├── Icon.vue │ │ └── Syntax.vue │ └── shared.styl ├── lib │ ├── appState.js │ ├── autoMode.js │ ├── autoPresets.js │ ├── bus.js │ ├── config.js │ ├── createInputsModel.js │ ├── editor │ │ ├── fetchGLSL.js │ │ ├── getParsedVectorFieldFunction.js │ │ ├── pragmaParser.js │ │ └── vectorFieldState.js │ ├── generate-equation.js │ ├── gl-utils.js │ ├── hsl2rgb.js │ ├── isSmallScreen.js │ ├── nativeMain.js │ ├── nativeMediaRecorder.js │ ├── programs │ │ ├── audioProgram.js │ │ ├── colorModes.js │ │ ├── colorProgram.js │ │ ├── drawParticlesProgram.js │ │ ├── inputs │ │ │ ├── imageInput.js │ │ │ ├── initTexture.js │ │ │ ├── inputCollection.js │ │ │ ├── loadTexture.js │ │ │ └── videoInput.js │ │ ├── screenProgram.js │ │ └── updatePositionProgram.js │ ├── scene.js │ ├── shaderGraph │ │ ├── BaseShaderNode.js │ │ ├── DrawParticleGraph.js │ │ ├── PanzoomTransform.js │ │ ├── RungeKuttaIntegrator.js │ │ ├── TexturePositionNode.js │ │ ├── UserDefinedVelocityFunction.js │ │ ├── customInput.js │ │ ├── getVertexShaderCode.js │ │ ├── parts │ │ │ ├── decodeFloatRGBA.js │ │ │ ├── encodeFloatRGBA.js │ │ │ └── simplex-noise.js │ │ ├── renderNodes.js │ │ ├── shaderBasedColor.js │ │ └── updatePositionGraph.js │ ├── sound │ │ ├── audioSource.js │ │ └── soundLoader.js │ ├── utils │ │ ├── cursorUpdater.js │ │ ├── drag.js │ │ ├── floatPacking.js │ │ ├── makeStatCounter.js │ │ ├── request.js │ │ └── textureCollection.js │ ├── wglPanZoom.js │ └── wrapVectorField.js └── vueApp.js ├── static └── .gitkeep ├── test └── unit │ ├── .eslintrc │ ├── index.js │ ├── karma.conf.js │ └── specs │ └── FloatPacking.spec.js └── util └── makeAutoPresets.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | 9 | stats.json 10 | 11 | # Editor directories and files 12 | .idea 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "kutta", 4 | "runge" 5 | ] 6 | } -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # List of variables available to shader 2 | 3 | On top of standard [glsl functions](https://www.khronos.org/files/opengles_shading_language.pdf), the following 4 | list of variables can be used in velocity shader: 5 | 6 | * `float PI` - 3.1415926535897932384626433832795 7 | * `float frame` - Current frame number. Increases over time. Resets to 0 if you change shader's code 8 | * `vec4 cursor` - Defines position of a cursor in bounding box coordinates. `cursor.xy` - position 9 | where mouse was clicked (or tapped) last time. `cursor.zw` - current mouse hover position. On mobile 10 | phones, where mouse is not available, `zw` component will be the `xy` of [`touchmove`](https://developer.mozilla.org/en-US/docs/Web/Events/touchmove) event. 11 | -------------------------------------------------------------------------------- /Awesome Fields.md: -------------------------------------------------------------------------------- 1 | # Awesome Fields 2 | 3 | This is a collection of fields, created by community. Please feel free to contribute. 4 | Also check out small subreddit: [/r/fieldplay](https://www.reddit.com/r/fieldplay/) 5 | 6 | * [Behold](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.988&dp=0.008&cm=2&cx=0.12704999999999966&cy=0.1923499999999998&w=22.5709&h=22.5709&code=float%20x%20%3D%20abs%28p.x%29%20-%205.%3B%0Afloat%20side%20%3D%20sign%28p.x%29%3B%0Afloat%20range%20%3D%20length%28vec2%28x%2C%20p.y%29%29%3B%0Afloat%20irisrange%20%3D%20length%28vec2%28x%2C%20p.y%20%2B%202.*sign%28p.y%29%29%29%3B%0A%0Avec2%20border%20%3D%201.*vec2%28p.y%20%2B%202.2*sign%28p.y%29%20*%20%28p.y*p.y%20%2F%20%28p.y*p.y%20%2B%200.01%29%29%2C%20-x%29%3B%0A%0Avec2%20outside%20%3D%20vec2%28x%20%2F%20%281.%2B10.%2Fabs%28p.x*p.x%29%29%2C%20p.y%29%3B%0A%0Avec2%20spiral%20%3D%20vec2%28p.y%2C%20-x%29%3B%0A%0Avec2%20iris%20%3D%20sin%28-range%20*%2010.%29%20*%20spiral%20%2B%200.05*vec2%28x%2C%20p.y%29%3B%0A%0Av%20%20%2B%3D%20outside%20*%20%28smoothstep%284.0%2C%204.5%2C%20irisrange%29%2Frange*5.%20-%205.*smoothstep%280.9%2C%200.7%2C%20range%29%2Frange%29%3B%0Av%20%2B%3D%20border%20*%20smoothstep%283.5%2C%204.%2C%20irisrange%29%20*%20smoothstep%284.5%2C%204.%2C%20irisrange%29%3B%0Av%20%2B%3D%20iris%20*%20smoothstep%282.0%2C%201.5%2C%20range%29%20*%20smoothstep%280.8%2C%201.%2C%20range%29%3B%0Av%20-%3D%2010.0*spiral%20*%20smoothstep%281.0%2C%200.8%2C%20range%29%20*%20smoothstep%280.7%2C%200.9%2C%20range%29%3B%0A%0Av.x%20*%3D%20side%3B%0Av%20*%3D%20-1.%3B&pc=30000) by [/u/censored_username](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dpbdtvp/) 7 | * [Sauron's Eye](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.988&dp=0.008&cm=1&cx=-2.905099999999999&cy=-2.3917999999999995&w=30.6514&h=30.6514&code=%0A%2F%2F%20center%20parts%0Afloat%20pupilrange%20%3D%20length%28vec2%28p.y%2C%20p.x%20%2B%206.*sign%28p.x%29%29%29%3B%0Avec2%20pupilborder%20%3D%202.6*vec2%28-p.y%2C%20%28p.x%20%2B%206.*sign%28p.x%29%29%20%29%3B%0Av%20%2B%3D%20pupilborder%20*%20smoothstep%286.6%2C%206.8%2C%20pupilrange%29%20*%20smoothstep%287.1%2C%206.9%2C%20pupilrange%29%3B%0A%0Afloat%20range%20%3D%20length%28p%29%3B%0Avec2%20iris%20%3D%207.*p%2Fsqrt%28range%29%3B%0Av%20%2B%3D%20iris%20*%20smoothstep%287.0%2C%207.5%2C%20pupilrange%29%20*%20smoothstep%284.0%2C%203.8%2C%20range%29%3B%0A%0Avec2%20pupil%20%3D%201.*vec2%28p.x%2B1.*sign%28p.x%29%2C%20p.y%29%3B%0Av%20%2B%3D%20pupil%20*%20smoothstep%286.8%2C%206.6%2C%20pupilrange%29%3B%0A%0A%0A%2F%2F%20absolute%20parts%0Avec2%20psign%20%3D%20sign%28p%29%3B%0Avec2%20a%20%3D%20abs%28p%29%3B%0Avec2%20vabs%20%3D%20vec2%280.0%2C%200.0%29%3B%0A%0Afloat%20borderrange%20%3D%20length%28vec2%28p.x%2C%20p.y%20%2B%207.*sign%28p.y%29%29%29%3B%0Avec2%20border%20%3D%20-1.5*vec2%28a.y%20%2B%207.*sign%28a.y%29%20*%20%28a.y*a.y%20%2F%20%28a.y*a.y%20%2B%200.01%29%29%20%2F%20sqrt%283.%2F%28a.x%20%2B%201.%29%29%2C%20-a.x%20%2B%203.%2Fsqrt%28a.x%20%2B%201.%29%29%3B%0Avabs%20%2B%3D%20border%20*%20smoothstep%2810.8%2C%2011.25%2C%20borderrange%29%20*%20smoothstep%2811.7%2C%2011.25%2C%20borderrange%29%20*%20smoothstep%283.8%2C%204.1%2C%20range%29%3B%0A%0Avec2%20irisborder%20%3D%205.*vec2%28a.y%2C%20-a.x%29%20*%20%28a.y%20%2F%20%28a.y%20%2B%203.%29%29%2B%20.2%20*%20a%3B%0Avabs%20%2B%3D%20irisborder%20*%20smoothstep%283.8%2C%204.25%2C%20range%29%20*%20smoothstep%284.7%2C%204.25%2C%20range%29%3B%0A%0Avec2%20white%20%3D%2012.*vec2%281.0%2C%20-0.2%20*%20%28a.y%29%29%3B%0Avabs%20%2B%3D%20white%20*%20smoothstep%284.3%2C%204.5%2C%20range%29%20*%20smoothstep%2811.2%2C%2011.%2C%20borderrange%29%3B%0A%0Av%20%2B%3D%20vabs%20*%20psign%3B%0A%0A%2F%2F%20outside%20part%0Avec2%20outside%20%3D%20p%20%2F%20pow%28borderrange%20-%2010.%2C%202.%29%3B%0Av%20-%3D%20outside%20*%20smoothstep%2811.3%2C%2011.5%2C%20borderrange%29%3B%0A&pc=20000) by [/u/censored_username](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dpbdtvp/) 8 | * [Heart](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.998&dp=0.009&cm=2&cx=-0.6516500000000001&cy=0.5642000000000001&w=8.5397&h=8.5397&code=float%20size%20%3D%202.0%3B%0Avec2%20o%20%3D%20%28p%29%2F%281.6*%20size%29%3B%0A%20%20float%20a%20%3D%20o.x*o.x%2Bo.y*o.y-0.3%3B%0A%20v%20%3D%20vec2%28step%28a*a*a%2C%20o.x*o.x*o.y*o.y*o.y%29%29%3B%0A%20%20) by [/u/sakrist](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dpaewq0/) 9 | * [True Dipole](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.998&dp=0.009&cm=1&cx=0&cy=0&w=8.5398&h=8.5398&code=float%20x%20%3D%20p.x%3B%0Afloat%20y%20%3D%20p.y%3B%0A%0A%2F%2F%20true%20dipole%0Av.x%20%3D%202.0*x*y%3B%0Av.y%20%3D%20y*y%20-%20x*x%3B) by [/u/julesjacobs](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dp82qyg/) 10 | * [Flow profile of a sphere](https://anvaka.github.io/fieldplay/?dt=0.011&fo=0.99999&dp=0.009&cm=1&cx=-0.7177000000000002&cy=-0.11769999999999992&w=11.434999999999999&h=11.434999999999999&code=float%20x%20%3D%20p.x%3B%0Afloat%20y%20%3D%20p.y%3B%0Afloat%20r%20%3D%20sqrt%28x*x%2By*y%29%3B%0Afloat%20sinth%20%3D%20y%2Fr%3B%0Afloat%20costh%20%3D%20x%2Fr%3B%0Afloat%20R%20%3D%201.%3B%0Afloat%20Uinf%20%3D%201.%3B%0A%2F%2F%20radial%20flow%0Afloat%20ur%20%3D%20Uinf*%281.-1.5*R%2Fr%2B0.5*R*R*R%2F%28r*r*r%29%29*costh%3B%0A%2F%2F%20theta%20flow%0Afloat%20uth%20%3D%20Uinf*%28-1.%2B0.75*R%2Fr%2B0.25*R*R*R%2F%28r*r*r%29%29*sinth%3B%0A%2F%2F%20to%20ux%20uy%0Av.x%20%3D%20costh*ur-sinth*uth%3B%0Av.y%20%3D%20sinth*ur%2Bcosth*uth%3B&pc=7000) by [/u/NitroXSC](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dp8wuli/) 11 | * [Best vortex](https://anvaka.github.io/fieldplay/?cm=2&cx=-6.158449999999998&cy=-0.9834499999999995&w=96.8415&h=96.8415&code=float%20r%20%3D%20length%28p%29%3B%0Afloat%20theta%20%3D%20atan%28p.y%2C%20p.x%29%3B%0Av%20%3D%20vec2%28p.y%2C%20-p.x%29%20%2F%20r%3B%0Afloat%20t%20%3D%20sqrt%28r%20*%2010.%29%20%2B%20theta%20%2B%20frame%20*%20.02%3B%0Av%20*%3D%20sin%28t%29%3B%0Av%20*%3D%20length%28v%29%20*%2010.%3B%0Av%20%2B%3D%20p%20*%20.2%3B&dt=0.01&fo=0.9&dp=0.009&pc=100000) by [/u/Jumpy89](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dp7o0cm/) 12 | * [Black hole](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.998&dp=0.009&cm=1&cx=-0.47934999999999994&cy=0.3591500000000001&w=8.5397&h=8.5397&code=float%20a%20%3D%20.1%3B%0Afloat%20r2%20%3D%20p.x%20*%20p.x%20%2B%20p.y%20*%20p.y%3B%0Av%20%3D%20vec2%28p.y%2C%20-p.x%29%20%2F%20r2%20-%20a%20*%20p%3B) by [/u/Jumpy89](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dp7ehf5/) 13 | * [Julia set](https://anvaka.github.io/fieldplay/?dt=0.004&fo=0.998&dp=0.009&cm=1&cx=-0.40235&cy=-0.01795000000000002&w=5.0845&h=5.0845&code=vec2%20c%20%3D%20p%3B%0Avec2%20z%20%3D%20vec2%28.4%2C%20.5%29%3B%0Afor%20%28int%20i%20%3D%200%3B%20i%20%3C%208%3B%20i%2B%2B%29%20%7B%0A%20%20%20c%20%3D%20vec2%28c.x%20*%20c.x%20-%20c.y%20*%20c.y%2C%20c.y%20*%20c.x%20%2B%20c.x%20*%20c.y%29%3B%0A%20%20%20c%20%2B%3D%20z%3B%0A%7D%0Av%20%3D%20c%3B%0A&pc=10000) by [/u/brokenAmmonite](https://www.reddit.com/r/programming/comments/7a4wfu/vector_fields_gpu_and_your_browser/dp8zo5q/) 14 | * [Mandelbrot set](https://anvaka.github.io/fieldplay/?dt=0.004&fo=0.998&dp=0.009&cm=3&cx=-0.5678&cy=-0.07015000000000005&w=4.9902&h=4.9902&code=vec2%20z%20%3D%20p%3B%0Afor%28int%20k%3D0%3B%20k%3C50%3B%20k%2B%2B%29%20%7B%0Az%20%3D%20vec2%28z.x%20*%20z.x%20-%20z.y%20*%20z.y%2C%202.%20*%20z.x%20*%20z.y%29%20%2B%20p%3B%0A%7D%0A%0Afloat%20mask%20%3D%20step%28length%28z%29%2C%202.%29%3B%0Av.x%20%3D%20-p.y%2Flength%28p%29%20*%20%280.5%20-%20mask%29%3B%0Av.y%20%3D%20p.x%2Flength%28p%29%20*%20%280.5%20-%20mask%29%3B%0A%0A%0A&pc=30000) by [/u/ThatSpysASpy](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dp8au9e/) 15 | * [Reflecting pool](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.998&dp=0.009&cm=1&cx=0&cy=0&w=8.5398&h=8.5398&code=v.x%20%3D%20sin%285.0*p.y%20%2B%20p.x%29%3B%0Av.y%20%3D%20cos%285.0*p.x%20-%20p.y%29%3B) by [/u/p2p_editor](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dp7f6vv/) 16 | * [Shear zone](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.998&dp=0.009&cm=1&cx=0&cy=0&w=8.539734222673566&h=8.539734222673566&code=float%20r%20%3D%20length%28p%29%20-%201.5%3B%0Afloat%20c%20%3D%201.0%2F%281.0%2Bexp%28-5.0*r%29%29%3B%0Afloat%20vx1%20%3D%20-p.y%2C%20%20%2F%2F%20circle%0A%20%20%20%20%20%20vy1%20%3D%20p.x%3B%0Afloat%20vx2%20%3D%200.2*p.x%2Bp.y%2C%20%2F%2F%20spiral%0A%20%20%20%20%20%20vy2%20%3D%200.2*p.y-p.x%3B%0Av.x%20%3D%20c*vx1%20%2B%20%281.0-c%29*vx2%3B%0Av.y%20%3D%20c*vy1%20%2B%20%281.0-c%29*vy2%3B%0A%20%20) by [/u/TooLateForMeTF](https://www.reddit.com/r/math/comments/7a4z4u/beautiful_world_of_vector_fields_this_is_the_tool/dp7k4cz/) 17 | * [Beautiful field](https://anvaka.github.io/fieldplay/?dt=0.01&fo=0.998&dp=0.009&cm=3&cx=-1.6564499999999995&cy=-0.36424999999999974&w=24.7317&h=24.7317&code=float%20dt%20%3D%200.01%3B%0Afloat%20t%20%3D%20frame*dt%3B%0Afloat%20w%20%3D%202.*PI%2F5.%3B%0Afloat%20A%20%3D%202.%3B%0A%0Afloat%20d%20%3D%20sqrt%28p.x*p.x%20%2B%20p.y*p.y%29%3B%0Av.x%20%3D%20A*cos%28w*t%2Fd%29%3B%0Av.y%20%3D%20A*sin%28w*t%2Fd%29%3B&pc=3000) by [@gsomix](https://twitter.com/gsomix/status/927277954324934657) 18 | * [Heart on fire](https://anvaka.github.io/fieldplay/?dt=0.003&fo=0.998&dp=0.009&cm=2&cx=-1.774750000000001&cy=0.03524999999999956&w=34.9125&h=34.9125&pc=30000&vf=%2F%2F%20p.x%20and%20p.y%20are%20current%20coordinates%0A%2F%2F%20v.x%20and%20v.y%20is%20a%20velocity%20at%20point%20p%0Avec2%20get_velocity%28vec2%20p%29%20%7B%0A%20%20vec2%20v%20%3D%20vec2%280.%29%3B%0A%0A%20%20vec2%20o%20%3D%20p%20%2F%208.%3B%0A%20%20float%20a%20%3D%20dot%28o%2C%20o%29%20-%200.6%3B%0A%0A%20%20vec2%20h%20%3D%20vec2%28step%28%0A%20%20%20%20%20%20a*a*a%2C%20%0A%20%20%20%20%20%20o.x*o.x%20*%20o.y%20*%20o.y%20*%20o.y%20%0A%20%20%29%29%3B%0A%20%20%0A%20%20float%20r%20%3D%20length%28p%29%3B%0A%20%20float%20theta%20%3D%20atan%28p.y%2C%20p.x%29%3B%0A%20%20v%20%3D%20vec2%28p.y%2C%20-p.x%29%20%2F%20r%3B%0A%20%20float%20t%20%3D%20sqrt%28r%20*%208.%29%20%2B%20theta%20%2B%20frame%20*%20.02%3B%0A%20%20v%20*%3D%20sin%28t%29%20*%20length%28v%29%20*%208.%3B%0A%20%20v%20%2B%3D%20p%20*%20.3%3B%0A%0A%0A%20%20return%20h*v%3B%0A%7D&code=%2F%2F%20p.x%20and%20p.y%20are%20current%20coordinates%0A%2F%2F%20v.x%20and%20v.y%20is%20a%20velocity%20at%20point%20p%0Avec2%20get_velocity%28vec2%20p%29%20%7B%0A%20%20vec2%20v%20%3D%20vec2%280.%2C%200.%29%3B%0A%0A%20%20vec2%20o%20%3D%20p%20%2F%208.%3B%0A%20%20float%20a%20%3D%20dot%28o%2C%20o%29%20-%200.6%3B%0A%0A%20%20vec2%20h%20%3D%20vec2%28step%28%0A%20%20%20%20%20%20a*a*a%2C%20%0A%20%20%20%20%20%20o.x*o.x%20*%20o.y%20*%20o.y%20*%20o.y%20%0A%20%20%29%29%3B%0A%20%20%0A%20%20float%20r%20%3D%20length%28p%29%3B%0A%20%20float%20theta%20%3D%20atan%28p.y%2C%20p.x%29%3B%0A%20%20v%20%3D%20vec2%28p.y%2C%20-p.x%29%20%2F%20r%3B%0A%20%20float%20t%20%3D%20sqrt%28r%20*%208.%29%20%2B%20theta%20%2B%20frame%20*%20.02%3B%0A%20%20v%20*%3D%20sin%28t%29%20*%20length%28v%29%20*%208.%3B%0A%20%20v%20%2B%3D%20p%20*%20.3%3B%0A%0A%0A%20%20return%20h*v%3B%0A%7D) 19 | * [Learning about Hamiltonian Monte Carlo](https://twitter.com/rlmcelreath/status/926736976031596545) by @betanalpha and @rlmcelreath 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2025 Andrei Kashcha 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 | -------------------------------------------------------------------------------- /ScreenRecording.md: -------------------------------------------------------------------------------- 1 | # How to record vector field videos? 2 | 3 | In physics, the **observer effect** is the fact that simply observing a situation or phenomenon 4 | necessarily changes that phenomenon. 5 | 6 | Similarly, when I tried to record what's happening on the screen, things were loosing 7 | their appeal. Video recording software quite often dropped frames or impacted overall performance. 8 | 9 | So, how can we observe the system without affecting the system? - Let's become part of the system! 10 | 11 | I built in video recorder into the website. It can only be started from developer's console 12 | in your browser. When frame is rendered, a screenshot is sent to the server to build a movie. 13 | 14 | Unfortunately I don't have spare capacity to host the server, so if you want to record a video, 15 | you'll need to follow these instructions. NOTE: This approach will not likely work from hosted 16 | application (i.e. anvaka.github.io/fieldplay). Set up repository [locally](https://github.com/anvaka/fieldplay#local-development). 17 | 18 | **Step 1:** Make sure you have [node.js](https://nodejs.org/) installed. 19 | 20 | **Step 2:** Install the video recording service 21 | ``` 22 | git clone https://github.com/greggman/ffmpegserver.js.git 23 | cd ffmpegserver.js 24 | npm install 25 | ``` 26 | 27 | **Step 3:** Start the server `node start.js --allow-arbitrary-ffmpeg-arguments`. Note: The ffmpeg-arguments 28 | is required because I [request higher screen capturing bitrate](https://github.com/anvaka/fieldplay/blob/e128f580bc9495189e5e56015f00650d75f44a38/src/lib/nativeMain.js#L69) than 29 | what is available by default. 30 | 31 | **Step 4:** In the developer tools console of your browser type `startRecord()`. If you see a `Mixed Content error`, 32 | make sure to make an exception for the screen recording (it is safe, as all communication happens between your 33 | browser and your local host - requests should not go to external network). 34 | 35 | This will make application seem unresponsive, but in the console you'll notice video recording log. After 36 | a while type in the console `stopRecord()`. This will "commit" video recording. When video is processed, it can be 37 | found in the `output` of your `ffmpegserver.js` server. E.g. `ffmpegserver.js/output/fieldplay-6.mp4` -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | var fs = require('fs'); 13 | 14 | var spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, function (err, stats) { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | fs.writeFile('stats.json', JSON.stringify(stats.toJson('verbose')), 'utf8', (err) => { 31 | if (!err) { 32 | console.log('Stats files is saved to stats.json'); 33 | } else { 34 | console.log('Failed to save stats file', err); 35 | } 36 | }); 37 | 38 | 39 | if (stats.hasErrors()) { 40 | console.log(chalk.red(' Build failed with errors.\n')) 41 | process.exit(1) 42 | } 43 | 44 | console.log(chalk.cyan(' Build complete.\n')) 45 | console.log(chalk.yellow( 46 | ' Tip: built files are meant to be served over an HTTP server.\n' + 47 | ' Opening index.html over file:// won\'t work.\n' 48 | )) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | } 15 | ] 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = require('./webpack.dev.conf') 14 | 15 | // default port where dev server listens for incoming traffic 16 | var port = process.env.PORT || config.dev.port 17 | // automatically open browser, if not set will be false 18 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 19 | // Define HTTP proxies to your custom API backend 20 | // https://github.com/chimurai/http-proxy-middleware 21 | var proxyTable = config.dev.proxyTable 22 | 23 | var app = express() 24 | var compiler = webpack(webpackConfig) 25 | 26 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 27 | publicPath: webpackConfig.output.publicPath, 28 | quiet: true 29 | }) 30 | 31 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 32 | log: false, 33 | heartbeat: 2000 34 | }) 35 | // force page reload when html-webpack-plugin template changes 36 | compiler.plugin('compilation', function (compilation) { 37 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 38 | hotMiddleware.publish({ action: 'reload' }) 39 | cb() 40 | }) 41 | }) 42 | 43 | // proxy api requests 44 | Object.keys(proxyTable).forEach(function (context) { 45 | var options = proxyTable[context] 46 | if (typeof options === 'string') { 47 | options = { target: options } 48 | } 49 | app.use(proxyMiddleware(options.filter || context, options)) 50 | }) 51 | 52 | // handle fallback for HTML5 history API 53 | app.use(require('connect-history-api-fallback')()) 54 | 55 | // serve webpack bundle output 56 | app.use(devMiddleware) 57 | 58 | // enable hot-reload and state-preserving 59 | // compilation error display 60 | app.use(hotMiddleware) 61 | 62 | // serve pure static assets 63 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 64 | app.use(staticPath, express.static('./static')) 65 | 66 | var uri = 'http://localhost:' + port 67 | 68 | var _resolve 69 | var readyPromise = new Promise(resolve => { 70 | _resolve = resolve 71 | }) 72 | 73 | console.log('> Starting dev server...') 74 | devMiddleware.waitUntilValid(() => { 75 | console.log('> Listening at ' + uri + '\n') 76 | // when env is testing, don't need open it 77 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 78 | opn(uri) 79 | } 80 | _resolve() 81 | }) 82 | 83 | var server = app.listen(port) 84 | 85 | module.exports = { 86 | ready: readyPromise, 87 | close: () => { 88 | server.close() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }), 12 | transformToRequire: { 13 | video: 'src', 14 | source: 'src', 15 | img: 'src', 16 | image: 'xlink:href' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: './src/lib/nativeMain.js' 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | alias: { 24 | 'vue$': 'vue/dist/vue.esm.js', 25 | '@': resolve('src'), 26 | } 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.vue$/, 32 | loader: 'vue-loader', 33 | options: vueLoaderConfig 34 | }, 35 | { 36 | test: /\.js$/, 37 | loader: 'babel-loader', 38 | include: [resolve('src'), resolve('test')] 39 | }, 40 | { 41 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 42 | loader: 'url-loader', 43 | options: { 44 | limit: 10000, 45 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 46 | } 47 | }, 48 | { 49 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 50 | loader: 'url-loader', 51 | options: { 52 | limit: 10000, 53 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 54 | } 55 | }, 56 | { 57 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 58 | loader: 'url-loader', 59 | options: { 60 | limit: 10000, 61 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 62 | } 63 | } 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = config.build.env 13 | 14 | var webpackConfig = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ 17 | sourceMap: config.build.productionSourceMap, 18 | extract: true 19 | }) 20 | }, 21 | devtool: config.build.productionSourceMap ? '#source-map' : false, 22 | output: { 23 | path: config.build.assetsRoot, 24 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 25 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 26 | }, 27 | plugins: [ 28 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 29 | new webpack.DefinePlugin({ 30 | 'process.env': env 31 | }), 32 | new webpack.optimize.UglifyJsPlugin({ 33 | compress: { 34 | warnings: false 35 | }, 36 | sourceMap: true 37 | }), 38 | // extract css into its own file 39 | new ExtractTextPlugin({ 40 | filename: utils.assetsPath('css/[name].[contenthash].css') 41 | }), 42 | // Compress extracted CSS. We are using this plugin so that possible 43 | // duplicated CSS from different components can be deduped. 44 | new OptimizeCSSPlugin({ 45 | cssProcessorOptions: { 46 | safe: true 47 | } 48 | }), 49 | // generate dist index.html with correct asset hash for caching. 50 | // you can customize output by editing /index.html 51 | // see https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: config.build.index, 54 | template: 'index.html', 55 | inject: true, 56 | minify: { 57 | removeComments: true, 58 | collapseWhitespace: true, 59 | removeAttributeQuotes: true 60 | // more options: 61 | // https://github.com/kangax/html-minifier#options-quick-reference 62 | }, 63 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 64 | chunksSortMode: 'dependency' 65 | }), 66 | // keep module.id stable when vender modules does not change 67 | new webpack.HashedModuleIdsPlugin(), 68 | // split vendor js into its own file 69 | new webpack.optimize.CommonsChunkPlugin({ 70 | name: 'vendor', 71 | minChunks: function (module, count) { 72 | // any required modules inside node_modules are extracted to vendor 73 | return ( 74 | module.resource && 75 | /\.js$/.test(module.resource) && 76 | module.resource.indexOf( 77 | path.join(__dirname, '../node_modules') 78 | ) === 0 79 | ) 80 | } 81 | }), 82 | // extract webpack runtime and module manifest to its own file in order to 83 | // prevent vendor hash from being updated whenever app bundle is updated 84 | new webpack.optimize.CommonsChunkPlugin({ 85 | name: 'manifest', 86 | chunks: ['vendor'] 87 | }), 88 | // copy custom static assets 89 | new CopyWebpackPlugin([ 90 | { 91 | from: path.resolve(__dirname, '../static'), 92 | to: config.build.assetsSubDirectory, 93 | ignore: ['.*'] 94 | } 95 | ]) 96 | ] 97 | }) 98 | 99 | if (config.build.productionGzip) { 100 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 101 | 102 | webpackConfig.plugins.push( 103 | new CompressionWebpackPlugin({ 104 | asset: '[path].gz[query]', 105 | algorithm: 'gzip', 106 | test: new RegExp( 107 | '\\.(' + 108 | config.build.productionGzipExtensions.join('|') + 109 | ')$' 110 | ), 111 | threshold: 10240, 112 | minRatio: 0.8 113 | }) 114 | ) 115 | } 116 | 117 | if (config.build.bundleAnalyzerReport) { 118 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 119 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 120 | } 121 | 122 | module.exports = webpackConfig 123 | -------------------------------------------------------------------------------- /build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // This is the webpack config used for unit tests. 3 | 4 | const utils = require('./utils') 5 | const webpack = require('webpack') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | 9 | const webpackConfig = merge(baseWebpackConfig, { 10 | // use inline sourcemap for karma-sourcemap-loader 11 | module: { 12 | rules: utils.styleLoaders() 13 | }, 14 | devtool: '#inline-source-map', 15 | resolveLoader: { 16 | alias: { 17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 19 | 'scss-loader': 'sass-loader' 20 | } 21 | }, 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': require('../config/test.env') 25 | }) 26 | ] 27 | }) 28 | 29 | // no need for app entry during tests 30 | delete webpackConfig.entry 31 | 32 | module.exports = webpackConfig 33 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8880, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf ./dist 3 | npm run build 4 | cd ./dist 5 | git init 6 | git add . 7 | git commit -m 'push to gh-pages' 8 | git push --force git@github.com:anvaka/fieldplay.git main:gh-pages 9 | cd ../ 10 | git tag `date "+release-%Y%m%d%H%M%S"` 11 | git push --tags 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Field Play 23 | 43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "field-play", 3 | "version": "1.0.0", 4 | "description": "playing with vector fields", 5 | "author": "Andrei Kashcha", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", 12 | "test": "npm run unit" 13 | }, 14 | "dependencies": { 15 | "autosize": "^4.0.0", 16 | "glsl-parser": "git+https://github.com/anvaka/glslx.git#glsl-parser", 17 | "ngraph.events": "0.0.4", 18 | "panzoom": "^9.2.5", 19 | "query-state": "^4.0.0", 20 | "stylus": "^0.54.5", 21 | "stylus-loader": "^3.0.1", 22 | "vue": "^2.4.2", 23 | "vue-codemirror-lite": "^1.0.3", 24 | "vue-color": "^2.4.0" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^7.1.2", 28 | "babel-core": "^6.22.1", 29 | "babel-loader": "^7.1.1", 30 | "babel-plugin-istanbul": "^4.1.1", 31 | "babel-plugin-transform-runtime": "^6.22.0", 32 | "babel-preset-env": "^1.3.2", 33 | "babel-preset-stage-2": "^6.22.0", 34 | "babel-register": "^6.22.0", 35 | "ccapture.js": "1.0.7", 36 | "chai": "^4.1.2", 37 | "chalk": "^2.0.1", 38 | "connect-history-api-fallback": "^1.3.0", 39 | "copy-webpack-plugin": "^4.0.1", 40 | "cross-env": "^5.0.1", 41 | "css-loader": "^0.28.0", 42 | "cssnano": "^3.10.0", 43 | "eventsource-polyfill": "^0.9.6", 44 | "express": "^4.14.1", 45 | "extract-text-webpack-plugin": "^2.0.0", 46 | "file-loader": "^0.11.1", 47 | "friendly-errors-webpack-plugin": "^1.1.3", 48 | "html-webpack-plugin": "^2.28.0", 49 | "http-proxy-middleware": "^0.17.3", 50 | "inject-loader": "^3.0.0", 51 | "karma": "^1.4.1", 52 | "karma-chrome-launcher": "^2.2.0", 53 | "karma-coverage": "^1.1.1", 54 | "karma-mocha": "^1.3.0", 55 | "karma-phantomjs-launcher": "^1.0.2", 56 | "karma-phantomjs-shim": "^1.4.0", 57 | "karma-sinon-chai": "^1.3.1", 58 | "karma-sourcemap-loader": "^0.3.7", 59 | "karma-spec-reporter": "0.0.31", 60 | "karma-webpack": "^2.0.2", 61 | "mocha": "^5.2.0", 62 | "opn": "^5.1.0", 63 | "optimize-css-assets-webpack-plugin": "^2.0.0", 64 | "ora": "^1.2.0", 65 | "phantomjs-prebuilt": "^2.1.14", 66 | "rimraf": "^2.6.0", 67 | "semver": "^5.3.0", 68 | "shelljs": "^0.7.6", 69 | "sinon": "^4.0.0", 70 | "sinon-chai": "^2.8.0", 71 | "url-loader": "^0.5.8", 72 | "vue-loader": "^13.0.4", 73 | "vue-style-loader": "^3.0.1", 74 | "vue-template-compiler": "^2.4.2", 75 | "webpack": "^2.6.1", 76 | "webpack-bundle-analyzer": "^2.2.1", 77 | "webpack-dev-middleware": "^1.10.0", 78 | "webpack-hot-middleware": "^2.18.0", 79 | "webpack-merge": "^4.1.0" 80 | }, 81 | "engines": { 82 | "node": ">= 4.0.0", 83 | "npm": ">= 3.0.0" 84 | }, 85 | "browserslist": [ 86 | "> 1%", 87 | "last 2 versions", 88 | "not ie <= 8" 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 93 | 94 | 168 | -------------------------------------------------------------------------------- /src/components/About.vue: -------------------------------------------------------------------------------- 1 | 2 | 37 | 38 | 59 | 116 | -------------------------------------------------------------------------------- /src/components/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/ColorPicker.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | 57 | -------------------------------------------------------------------------------- /src/components/Controls.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 64 | 111 | -------------------------------------------------------------------------------- /src/components/Inputs.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | 40 | 61 | -------------------------------------------------------------------------------- /src/components/Ruler.vue: -------------------------------------------------------------------------------- 1 | 11 | 90 | 174 | 175 | -------------------------------------------------------------------------------- /src/components/Share.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 119 | 222 | 223 | -------------------------------------------------------------------------------- /src/components/VectorView.vue: -------------------------------------------------------------------------------- 1 | 15 | 109 | 110 | 111 | 126 | -------------------------------------------------------------------------------- /src/components/glsl-theme.styl: -------------------------------------------------------------------------------- 1 | // (C) Copyright (c) 2014 fergaldoyle 2 | // https://github.com/fergaldoyle/brackets-visual-studio-dark/blob/master/LICENSE 3 | // 4 | // Adjusted to vector fields need by https://github.com/anvaka 5 | 6 | @import "./shared.styl"; 7 | 8 | standardText = #DDD; 9 | maxHeight = 320px; 10 | 11 | .CodeMirror { 12 | height: auto; 13 | max-height: maxHeight; 14 | font-size: 14px; 15 | z-index: 0; 16 | background-color: window-background; 17 | border: 1px solid window-background; 18 | 19 | .CodeMirror-gutter-elt{ 20 | background: transparent; 21 | } 22 | 23 | .CodeMirror-scroll { 24 | height: auto; 25 | max-height: maxHeight; 26 | } 27 | .CodeMirror-linenumber, 28 | .CodeMirror-scroll, 29 | .CodeMirror-gutters { 30 | color: standardText; 31 | font-family: Consolas,'SourceCodePro-Medium',monaco,monospace; 32 | } 33 | 34 | .CodeMirror-linenumber { 35 | color: #2B91AF; 36 | } 37 | 38 | .CodeMirror-selected { 39 | background-color: #264F78; 40 | } 41 | 42 | .cm-matchhighlight { 43 | //background-color: #123E70; 44 | } 45 | 46 | .cm-matchingtag, 47 | .CodeMirror-matchingtag { 48 | background-color: #113D6F; 49 | //box-shadow: 0px 0px 2px #ADC0D3; 50 | } 51 | 52 | .cm-matchingbracket, 53 | .CodeMirror-matchingbracket { 54 | background-color: #113D6F; 55 | box-shadow: 0px 0px 3px #99B0C7; 56 | color: standardText !important; 57 | } 58 | 59 | 60 | .CodeMirror-cursor { 61 | border-left: 1px solid standardText; 62 | z-index: 3; 63 | } 64 | 65 | .CodeMirror-activeline-background { 66 | outline: 2px solid #dddddd; 67 | } 68 | // Languages 69 | // common 70 | .cm-m-glsl, 71 | .cm-variable, 72 | .cm-variable-2, 73 | .cm-variable-3 { 74 | color: standardText; 75 | } 76 | 77 | .cm-pragma { 78 | font-weight: bold; 79 | color: #0afff8; 80 | } 81 | 82 | .cm-operator { 83 | color: #B4B4B4; 84 | } 85 | 86 | .cm-comment { 87 | color: #57A64A; 88 | font-style: italic; 89 | } 90 | 91 | .cm-string { 92 | color: #D69D85; 93 | } 94 | 95 | .cm-attribute { 96 | color: #9CDCFE; 97 | } 98 | 99 | .cm-number { 100 | color: #B5CEA8; 101 | } 102 | 103 | .cm-def { 104 | color: #4EC9B0; 105 | } 106 | 107 | .cm-meta { 108 | color: #9B9B9B; 109 | } 110 | 111 | .cm-keyword, 112 | .cm-tag, .cm-atom { 113 | color: #569CD6; 114 | } 115 | 116 | .cm-bracket { 117 | color: #808080; 118 | } 119 | 120 | .cm-m-clike.cm-builtin { 121 | color: #569CD6; 122 | } 123 | 124 | .cm-m-clike.cm-variable-2 { 125 | //color: #d26bca; 126 | } 127 | // search highlight 128 | .CodeMirror-searching { 129 | color: #ddd !important; 130 | } 131 | 132 | .CodeMirror-searching.searching-last, 133 | .CodeMirror-searching.searching-first { 134 | border-radius: 1px; 135 | background-color: #653306; 136 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.8); 137 | padding: 1px 0; 138 | } 139 | 140 | .CodeMirror-searching.searching-current-match { 141 | background-color: #515C6A; 142 | } 143 | } 144 | 145 | .CodeMirror-focused { 146 | border: 1px dashed white; 147 | background: #13294f; 148 | } -------------------------------------------------------------------------------- /src/components/glslmode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 hughsk, MIT license 3 | * https://github.com/hughsk/glsl-editor/blob/master/LICENSE.md 4 | * 5 | * This file provides glsl syntax highlight for code-mirror. 6 | */ 7 | module.exports = function(CodeMirror) { 8 | CodeMirror.defineMode("glsl", function(config, parserConfig) { 9 | var indentUnit = config.indentUnit, 10 | keywords = parserConfig.keywords || words(glslKeywords), 11 | builtins = parserConfig.builtins || words(glslBuiltins), 12 | blockKeywords = parserConfig.blockKeywords || words("case do else for if switch while struct"), 13 | atoms = parserConfig.atoms || words("null"), 14 | hooks = parserConfig.hooks || {}, 15 | multiLineStrings = parserConfig.multiLineStrings; 16 | var isOperatorChar = /[+\-*&%=<>!?|\/]/; 17 | 18 | var curPunc; 19 | 20 | function tokenBase(stream, state) { 21 | var ch = stream.next(); 22 | if (hooks[ch]) { 23 | var result = hooks[ch](stream, state); 24 | if (result !== false) return result; 25 | } 26 | if (ch == '"' || ch == "'") { 27 | state.tokenize = tokenString(ch); 28 | return state.tokenize(stream, state); 29 | } 30 | if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 31 | curPunc = ch; 32 | return "bracket"; 33 | } 34 | if (/\d/.test(ch)) { 35 | stream.eatWhile(/[\w\.]/); 36 | return "number"; 37 | } 38 | if (ch == "/") { 39 | if (stream.eat("*")) { 40 | state.tokenize = tokenComment; 41 | return tokenComment(stream, state); 42 | } 43 | if (stream.eat("/")) { 44 | stream.skipToEnd(); 45 | return "comment"; 46 | } 47 | } 48 | if (ch == "#") { 49 | stream.eatWhile(/[\S]+/); 50 | stream.eatWhile(/[\s]+/); 51 | stream.eatWhile(/[\S]+/); 52 | stream.eatWhile(/[\s]+/); 53 | return "pragma"; 54 | } 55 | if (isOperatorChar.test(ch)) { 56 | stream.eatWhile(isOperatorChar); 57 | return "operator"; 58 | } 59 | stream.eatWhile(/[\w\$_]/); 60 | var cur = stream.current(); 61 | if (keywords.propertyIsEnumerable(cur)) { 62 | if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; 63 | return "keyword"; 64 | } 65 | if (builtins.propertyIsEnumerable(cur)) { 66 | return "builtin"; 67 | } 68 | if (atoms.propertyIsEnumerable(cur)) return "atom"; 69 | return "word"; 70 | } 71 | 72 | function tokenString(quote) { 73 | return function(stream, state) { 74 | var escaped = false, next, end = false; 75 | while ((next = stream.next()) != null) { 76 | if (next == quote && !escaped) {end = true; break;} 77 | escaped = !escaped && next == "\\"; 78 | } 79 | if (end || !(escaped || multiLineStrings)) 80 | state.tokenize = tokenBase; 81 | return "string"; 82 | }; 83 | } 84 | 85 | function tokenComment(stream, state) { 86 | var maybeEnd = false, ch; 87 | while (ch = stream.next()) { 88 | if (ch == "/" && maybeEnd) { 89 | state.tokenize = tokenBase; 90 | break; 91 | } 92 | maybeEnd = (ch == "*"); 93 | } 94 | return "comment"; 95 | } 96 | 97 | function Context(indented, column, type, align, prev) { 98 | this.indented = indented; 99 | this.column = column; 100 | this.type = type; 101 | this.align = align; 102 | this.prev = prev; 103 | } 104 | function pushContext(state, col, type) { 105 | return state.context = new Context(state.indented, col, type, null, state.context); 106 | } 107 | function popContext(state) { 108 | var t = state.context.type; 109 | if (t == ")" || t == "]" || t == "}") 110 | state.indented = state.context.indented; 111 | return state.context = state.context.prev; 112 | } 113 | 114 | // Interface 115 | 116 | return { 117 | startState: function(basecolumn) { 118 | return { 119 | tokenize: null, 120 | context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), 121 | indented: 0, 122 | startOfLine: true 123 | }; 124 | }, 125 | 126 | token: function(stream, state) { 127 | var ctx = state.context; 128 | if (stream.sol()) { 129 | if (ctx.align == null) ctx.align = false; 130 | state.indented = stream.indentation(); 131 | state.startOfLine = true; 132 | } 133 | if (stream.eatSpace()) return null; 134 | curPunc = null; 135 | var style = (state.tokenize || tokenBase)(stream, state); 136 | if (style == "comment" || style == "meta") return style; 137 | if (ctx.align == null) ctx.align = true; 138 | 139 | if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state); 140 | else if (curPunc == "{") pushContext(state, stream.column(), "}"); 141 | else if (curPunc == "[") pushContext(state, stream.column(), "]"); 142 | else if (curPunc == "(") pushContext(state, stream.column(), ")"); 143 | else if (curPunc == "}") { 144 | while (ctx.type == "statement") ctx = popContext(state); 145 | if (ctx.type == "}") ctx = popContext(state); 146 | while (ctx.type == "statement") ctx = popContext(state); 147 | } 148 | else if (curPunc == ctx.type) popContext(state); 149 | else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement")) 150 | pushContext(state, stream.column(), "statement"); 151 | state.startOfLine = false; 152 | return style; 153 | }, 154 | 155 | indent: function(state, textAfter) { 156 | if (state.tokenize != tokenBase && state.tokenize != null) return 0; 157 | var firstChar = textAfter && textAfter.charAt(0), ctx = state.context, closing = firstChar == ctx.type; 158 | if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit); 159 | else if (ctx.align) return ctx.column + (closing ? 0 : 1); 160 | else return ctx.indented + (closing ? 0 : indentUnit); 161 | }, 162 | 163 | electricChars: "{}" 164 | }; 165 | }); 166 | 167 | function words(str) { 168 | var obj = {}, words = str.split(" "); 169 | for (var i = 0; i < words.length; ++i) obj[words[i]] = true; 170 | return obj; 171 | } 172 | var glslKeywords = "attribute const uniform varying break continue " + 173 | "do for while if else in out inout float int void bool true false " + 174 | "lowp mediump highp precision invariant discard return mat2 mat3 " + 175 | "mat4 vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 sampler2D " + 176 | "samplerCube struct gl_FragCoord gl_FragColor"; 177 | var glslBuiltins = "radians degrees sin cos tan asin acos atan pow " + 178 | "exp log exp2 log2 sqrt inversesqrt abs sign floor ceil fract mod " + 179 | "min max clamp mix step smoothstep length distance dot cross " + 180 | "normalize faceforward reflect refract matrixCompMult lessThan " + 181 | "lessThanEqual greaterThan greaterThanEqual equal notEqual any all " + 182 | "not dFdx dFdy fwidth texture2D texture2DProj texture2DLod " + 183 | "texture2DProjLod textureCube textureCubeLod require export"; 184 | 185 | function cppHook(stream, state) { 186 | if (!state.startOfLine) return false; 187 | stream.skipToEnd(); 188 | return "meta"; 189 | } 190 | 191 | ;(function() { 192 | // C#-style strings where "" escapes a quote. 193 | function tokenAtString(stream, state) { 194 | var next; 195 | while ((next = stream.next()) != null) { 196 | if (next == '"' && !stream.eat('"')) { 197 | state.tokenize = null; 198 | break; 199 | } 200 | } 201 | return "string"; 202 | } 203 | 204 | CodeMirror.defineMIME("text/x-glsl", { 205 | name: "glsl", 206 | keywords: words(glslKeywords), 207 | builtins: words(glslBuiltins), 208 | blockKeywords: words("case do else for if switch while struct"), 209 | atoms: words("null"), 210 | hooks: {"#": cppHook} 211 | }); 212 | }()); 213 | } -------------------------------------------------------------------------------- /src/components/help/Icon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /src/components/help/Syntax.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/shared.styl: -------------------------------------------------------------------------------- 1 | window-background = rgba(6, 24, 56, 1.0); 2 | primary-border=#99c5f1; 3 | secondary-border=#455B7D; 4 | primary-text = white; 5 | secondary-text = #99c5f1; 6 | ternary-text = #435970; 7 | help-text-color = #267fcd; 8 | small-screen = 600px; 9 | settings-width = 392px; 10 | control-bar-height = 42px; -------------------------------------------------------------------------------- /src/lib/appState.js: -------------------------------------------------------------------------------- 1 | import queryState from 'query-state'; 2 | import bus from './bus'; 3 | import ColorModes from './programs/colorModes'; 4 | import wrapVectorField from './wrapVectorField'; 5 | import isSmallScreen from './isSmallScreen'; 6 | 7 | /** 8 | * The state of the fieldplay is stored in the query string. This is the 9 | * only place where query string can be manipulated or fetched. 10 | */ 11 | 12 | var qs = queryState({}, { 13 | useSearch: true, 14 | // Older version of the app used hash to store application arguments. 15 | // Turns out hash is not good for websites like reddit. They can block 16 | // url, saying "url was already submitted" if the only part that is different 17 | // is hash. So, we switch to search string, and maintain backward compatibility 18 | // for fields created before. 19 | rewriteHashToSearch: true 20 | }); 21 | 22 | var currentState = qs.get(); 23 | 24 | var defaultVectorField = wrapVectorField(`v.x = 0.1 * p.y; 25 | v.y = -0.2 * p.y;`); 26 | 27 | var pendingSave; 28 | var defaults = { 29 | timeStep: 0.01, 30 | dropProbability: 0.009, 31 | particleCount: 10000, 32 | fadeout: .998, 33 | colorMode: ColorModes.UNIFORM 34 | } 35 | 36 | let settingsPanel = { 37 | collapsed: isSmallScreen(), 38 | }; 39 | 40 | export default { 41 | settingsPanel, 42 | saveBBox, 43 | getBBox, 44 | makeBBox, 45 | getQS() { return qs; }, 46 | saveCode, 47 | getCode, 48 | getDefaultCode, 49 | 50 | getDropProbability, 51 | setDropProbability, 52 | 53 | getIntegrationTimeStep, 54 | setIntegrationTimeStep, 55 | 56 | getParticleCount, 57 | setParticleCount, 58 | 59 | getFadeout, 60 | setFadeout, 61 | 62 | getColorMode, 63 | setColorMode, 64 | 65 | getColorFunction, 66 | setColorFunction 67 | } 68 | 69 | qs.onChange(function() { 70 | bus.fire('scene-ready', window.scene); 71 | }); 72 | 73 | function getColorMode() { 74 | let colorMode = qs.get('cm'); 75 | return defined(colorMode) ? colorMode : defaults.colorMode; 76 | } 77 | 78 | function setColorMode(colorMode) { 79 | if (!defined(colorMode)) return; 80 | qs.set({cm: colorMode}); 81 | currentState.cm = colorMode; 82 | } 83 | 84 | function getColorFunction() { 85 | let colorFunction = qs.get('cf'); 86 | return colorFunction || ''; 87 | } 88 | 89 | function setColorFunction(colorFunction) { 90 | qs.set({cf: colorFunction}); 91 | currentState.cf = colorFunction; 92 | } 93 | 94 | function getFadeout() { 95 | let fadeout = qs.get('fo'); 96 | return defined(fadeout) ? fadeout : defaults.fadeout; 97 | } 98 | 99 | function setFadeout(fadeout) { 100 | if (!defined(fadeout)) return; 101 | qs.set({fo: fadeout}); 102 | currentState.fo = fadeout; 103 | } 104 | 105 | function getParticleCount() { 106 | let particleCount = qs.get('pc'); 107 | return defined(particleCount) ? particleCount : defaults.particleCount; 108 | } 109 | 110 | function setParticleCount(particleCount) { 111 | if (!defined(particleCount)) return; 112 | qs.set({pc: particleCount}); 113 | currentState.pc = particleCount; 114 | } 115 | 116 | function getIntegrationTimeStep() { 117 | let timeStep = qs.get('dt'); 118 | return defined(timeStep) ? timeStep : defaults.timeStep; 119 | } 120 | 121 | function setIntegrationTimeStep(dt) { 122 | if (!defined(dt)) return; 123 | qs.set({dt: dt}) 124 | currentState.dt = dt; 125 | } 126 | 127 | function getDropProbability() { 128 | let dropProbability = qs.get('dp'); 129 | return defined(dropProbability) ? dropProbability : defaults.dropProbability; 130 | } 131 | 132 | function setDropProbability(dropProbability) { 133 | if (!defined(dropProbability)) return; 134 | clamp(dropProbability, 0, 1); 135 | qs.set({dp: dropProbability}) 136 | } 137 | 138 | function getBBox() { 139 | let cx = qs.get('cx'); 140 | let cy = qs.get('cy'); 141 | let w = qs.get('w'); 142 | let h = qs.get('h'); 143 | return makeBBox(cx, cy, w, h); 144 | } 145 | 146 | function makeBBox(cx, cy, w, h) { 147 | let bboxDefined = defined(cx) && defined(cy) && defined(w) && defined(h); 148 | if (!bboxDefined) return; 149 | 150 | let w2 = w/2; 151 | let h2 = h/2; 152 | var p = 10000; 153 | return { 154 | minX: Math.round(p * (cx - w2))/p, 155 | maxX: Math.round(p * (cx + w2))/p, 156 | minY: Math.round(p * (cy - h2))/p, 157 | maxY: Math.round(p * (cy + h2))/p 158 | }; 159 | } 160 | 161 | function saveBBox(bbox, immediate = false) { 162 | bbox = { 163 | cx: (bbox.minX + bbox.maxX) * 0.5, 164 | cy: (bbox.minY + bbox.maxY) * 0.5, 165 | w: (bbox.maxX - bbox.minX), 166 | h: (bbox.maxX - bbox.minX) 167 | } 168 | 169 | if (bbox.w <= 0 || bbox.h <= 0) return; 170 | 171 | currentState.cx = bbox.cx; 172 | currentState.cy = bbox.cy; 173 | currentState.w = bbox.w; 174 | currentState.h = bbox.h; 175 | 176 | if(pendingSave) { 177 | clearTimeout(pendingSave); 178 | pendingSave = 0; 179 | } 180 | 181 | if (immediate) qs.set(bbox); 182 | else { 183 | pendingSave = setTimeout(() => { 184 | pendingSave = 0; 185 | qs.set(bbox); 186 | }, 300); 187 | } 188 | } 189 | 190 | function getCode() { 191 | var vfCode = qs.get('vf'); 192 | if (vfCode) return vfCode; 193 | 194 | // If we didn't get code yet, let's try read to read it from previous version 195 | // of the API. 196 | // TODO: Need to figure out how to develop this in backward/future compatible way. 197 | var oldCode = qs.get('code'); 198 | if (oldCode) { 199 | vfCode = wrapVectorField(oldCode); 200 | // side effect - let's clean the old URL 201 | delete(currentState.code); 202 | qs.set('vf', vfCode); 203 | return vfCode; 204 | } 205 | 206 | return defaultVectorField; 207 | } 208 | 209 | function getDefaultCode() { 210 | return defaultVectorField; 211 | } 212 | 213 | function saveCode(code) { 214 | qs.set({ 215 | vf: code 216 | }); 217 | currentState.code = code; 218 | } 219 | 220 | function defined(number) { 221 | return Number.isFinite(number); 222 | } 223 | 224 | function clamp(x, min, max) { 225 | return x < min ? min : 226 | (x > max) ? max : x; 227 | } 228 | -------------------------------------------------------------------------------- /src/lib/autoMode.js: -------------------------------------------------------------------------------- 1 | import appState from './appState'; 2 | import presets from './autoPresets'; 3 | import generateFunction from './generate-equation'; 4 | import wrapVectorField from './wrapVectorField'; 5 | 6 | let delayTime, incomingPresetsQueue, scene, scheduledUpdate, autoSource; 7 | 8 | export function initAutoMode(_scene) { 9 | scene = _scene; 10 | 11 | const qs = appState.getQS(); 12 | 13 | autoSource = qs.get('autosource'); 14 | if (!['presets', 'generator', 'both'].includes(autoSource)) { 15 | if (autoSource) { 16 | console.error('unknown autosource param; options are presets, generator, or both'); 17 | } 18 | 19 | autoSource = 'both'; 20 | } 21 | 22 | let autoTime = qs.get('autotime'); 23 | if (!autoTime) { 24 | autoTime = qs.get('auto'); // Backwards compatibility 25 | if (!autoTime) { 26 | return; 27 | } 28 | 29 | console.warn('the auto param is deprecated; please use autotime'); 30 | } 31 | 32 | let parsedMilliseconds = parseFloat(autoTime); 33 | if (Number.isNaN(parsedMilliseconds)) { 34 | console.error('malformed autotime param; not a number'); 35 | return; 36 | } 37 | 38 | if (/ms$/i.test(autoTime)) { 39 | // Already good 40 | } else if (/s$/i.test(autoTime)) { 41 | parsedMilliseconds *= 1000; // Convert from seconds 42 | } else if (/m$/i.test(autoTime)) { 43 | parsedMilliseconds *= 1000 * 60; // Convert from minutes 44 | } else if (/h$/i.test(autoTime)) { 45 | parsedMilliseconds *= 1000 * 60 * 60; // Convert from hours 46 | } 47 | 48 | if (parsedMilliseconds <= 500) { 49 | console.warn('autotime param is too small; defaulting to 30 seconds'); 50 | parsedMilliseconds = 30000; 51 | } 52 | 53 | delayTime = parsedMilliseconds; 54 | next({ immediately: true }); 55 | 56 | // TODO: When user changes any argument of a field, we need to stop the mode. 57 | // we could use `bus` here to listen for change events, and dispose. 58 | return dispose; 59 | } 60 | 61 | function dispose() { 62 | clearTimeout(scheduledUpdate); 63 | scheduledUpdate = 0; 64 | // TODO: When disposed we need to drop the `auto` argument from the query string. 65 | // otherwise if people share it, they can unintentionally switch on auto mode 66 | } 67 | 68 | function next(options) { 69 | options = options || {}; 70 | 71 | let source = autoSource; 72 | if (source === 'both') { 73 | source = Math.random() < 0.5 ? 'presets' : 'generator'; 74 | } 75 | 76 | if (source === 'generator') { 77 | scene.setParticlesCount(10000); 78 | scene.vectorFieldEditorState.setCode(wrapVectorField(generateFunction())); 79 | } else if (source === 'presets') { 80 | if (!incomingPresetsQueue || !incomingPresetsQueue.length) { 81 | incomingPresetsQueue = shuffle(presets); 82 | } 83 | 84 | const preset = incomingPresetsQueue.shift(); 85 | 86 | scene.vectorFieldEditorState.setCode(preset.code); 87 | 88 | if (defined(preset.colorMode)) { 89 | scene.setColorMode(preset.colorMode); 90 | } 91 | 92 | if (defined(preset.timeStep)) { 93 | scene.setIntegrationTimeStep(preset.timeStep); 94 | } 95 | 96 | if (defined(preset.fadeOut)) { 97 | scene.setFadeOutSpeed(preset.fadeOut); 98 | } 99 | 100 | if (defined(preset.dropProbability)) { 101 | scene.setDropProbability(preset.dropProbability); 102 | } 103 | 104 | if (defined(preset.particleCount)) { 105 | scene.setParticlesCount(preset.particleCount); 106 | } 107 | 108 | const bbox = appState.makeBBox(preset.cx, preset.cy, preset.w, preset.h); 109 | if (bbox) { 110 | if (options.immediately) { 111 | scene.applyBoundingBox(bbox); 112 | } else { 113 | animateBBox(bbox); 114 | } 115 | } 116 | 117 | // TODO: support these additional params: i0, showBindings 118 | } 119 | 120 | scheduledUpdate = setTimeout(next, delayTime); 121 | } 122 | 123 | function animateBBox(endBBox) { 124 | const startBBox = Object.assign({}, scene.getBoundingBox()); 125 | const duration = 3000; 126 | const startTime = Date.now(); 127 | const diffMinX = endBBox.minX - startBBox.minX; 128 | const diffMaxX = endBBox.maxX - startBBox.maxX; 129 | const diffMinY = endBBox.minY - startBBox.minY; 130 | const diffMaxY = endBBox.maxY - startBBox.maxY; 131 | 132 | const frame = function() { 133 | const factor = (Date.now() - startTime) / duration; 134 | if (factor >= 1) { 135 | scene.applyBoundingBox(endBBox); 136 | return; 137 | } 138 | 139 | requestAnimationFrame(frame); 140 | 141 | const bbox = { 142 | minX: startBBox.minX + (diffMinX * factor), 143 | maxX: startBBox.maxX + (diffMaxX * factor), 144 | minY: startBBox.minY + (diffMinY * factor), 145 | maxY: startBBox.maxY + (diffMaxY * factor) 146 | }; 147 | 148 | scene.applyBoundingBox(bbox); 149 | }; 150 | 151 | frame(); 152 | } 153 | 154 | function shuffle(inputArray) { 155 | const outputArray = inputArray.slice(); 156 | for (let i = 0; i < outputArray.length; i++) { 157 | const j = Math.floor(Math.random() * outputArray.length); 158 | const temp = outputArray[i]; 159 | outputArray[i] = outputArray[j]; 160 | outputArray[j] = temp; 161 | } 162 | 163 | return outputArray; 164 | } 165 | 166 | function defined(number) { 167 | return Number.isFinite(number); 168 | } 169 | -------------------------------------------------------------------------------- /src/lib/bus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple message bus. Facilitates uncoupled communication between 3 | * components of fieldplay. 4 | */ 5 | var eventify = require('ngraph.events'); 6 | 7 | module.exports = eventify({}); -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | import appState from './appState'; 2 | 3 | const defaultConfig = { 4 | // I need to flash our more details before making any promises. 5 | isAudioEnabled: false, 6 | // this allows to render an overlay grid with vectors. Enable it 7 | // and drag the scene a little bit. 8 | vectorLinesEnabled: false, 9 | 10 | // Starting from which texture unit we can bind custom inputs? 11 | FREE_TEXTURE_UNIT: 4, 12 | 13 | // whether input bindings should be visible 14 | showBindings: appState.getQS().get('showBindings') || false 15 | } 16 | 17 | export default defaultConfig; -------------------------------------------------------------------------------- /src/lib/createInputsModel.js: -------------------------------------------------------------------------------- 1 | import createInputCollection from './programs/inputs/inputCollection'; 2 | import createVideoInput from './programs/inputs/videoInput'; 3 | import createImageInputBinding from './programs/inputs/imageInput'; 4 | import appState from './appState.js'; 5 | 6 | // Allows to bind media elements to vector field 7 | export default function createInputsModel(ctx) { 8 | ctx.inputs = createInputCollection(); 9 | var inputs = []; 10 | readInputsFromAppState(); 11 | 12 | var api = { 13 | getInputs, 14 | addInput, 15 | }; 16 | 17 | return api; 18 | 19 | function getInputs() { 20 | return inputs; 21 | } 22 | 23 | function addInput(inputNumber) { 24 | var vm = createInputElementViewModel(ctx, inputNumber); 25 | inputs.push(vm); 26 | return vm; 27 | } 28 | 29 | function readInputsFromAppState() { 30 | var i0 = appState.getQS().get('i0'); 31 | if (i0) { 32 | var vm = addInput(0); 33 | vm.link = i0; 34 | vm.updateBinding(/* immediate = */ true); 35 | } 36 | } 37 | } 38 | 39 | function createInputElementViewModel(ctx, inputNumber) { 40 | var pendingUpdate = null; 41 | 42 | var input = { 43 | link: '', 44 | error: null, 45 | name: `input${inputNumber}`, 46 | updateBinding 47 | } 48 | 49 | return input; 50 | 51 | function updateBinding(immediate) { 52 | if (pendingUpdate) { 53 | clearTimeout(pendingUpdate); 54 | pendingUpdate = null; 55 | } 56 | 57 | if (immediate) { 58 | setBinding(); 59 | } else { 60 | pendingUpdate = setTimeout(setBinding, 300); 61 | } 62 | } 63 | 64 | function setBinding() { 65 | input.error = null; 66 | pendingUpdate = null; 67 | var binding = createImageInputBinding(ctx, input.link, { 68 | done() { 69 | // TODO: Preview 70 | appState.getQS().set(`i${inputNumber}`, input.link); 71 | }, 72 | error(err) { 73 | // TODO: Better Error checking 74 | input.error = err; 75 | } 76 | }); 77 | ctx.inputs.bindInput(0, binding); 78 | } 79 | } -------------------------------------------------------------------------------- /src/lib/editor/fetchGLSL.js: -------------------------------------------------------------------------------- 1 | var loadedLinks = new Map(); // from link to response 2 | import request from '../utils/request'; 3 | 4 | export default function fetchGLSL(link) { 5 | if (!link) return Promise.reject('Missing link') 6 | var trimmed = link.trim(); 7 | if (!trimmed) return Promise.reject('Missing link'); 8 | 9 | let cachedResponse = loadedLinks.get(trimmed) 10 | if (cachedResponse) return Promise.resolve(cachedResponse); 11 | 12 | return request(link).then(code => { 13 | loadedLinks.set(link, code); 14 | return code; 15 | }); 16 | } -------------------------------------------------------------------------------- /src/lib/editor/getParsedVectorFieldFunction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module parses user defined vector field code. 3 | */ 4 | 5 | import bus from '../bus'; 6 | import pragmaParse from './pragmaParser'; 7 | 8 | // This is naive parser that is being used until the real `glsl-parser` 9 | // is loaded asynchronously. This parser assumes there are no errors 10 | // TODO: maybe I should be more careful here? 11 | var glslParser = { 12 | check(code) { 13 | return { 14 | code, 15 | log: { 16 | errorCount: 0 17 | } 18 | }; 19 | } 20 | }; 21 | 22 | // glsl-parser is ~179KB uncompressed, we don't want to wait until it is downloaded. 23 | // So we load it asynchronously... 24 | require.ensure('glsl-parser', () => { 25 | // ... and replace the naive parser with the real one, when ready. 26 | glslParser = require('glsl-parser'); 27 | 28 | // notify interested parties, so that they can recheck code if they wish. 29 | bus.fire('glsl-parser-ready'); 30 | }); 31 | 32 | var vectorFieldGlobals = ` 33 | import { 34 | float PI; 35 | float snoise(vec2 v); 36 | float frame; 37 | vec4 cursor; 38 | vec2 rotate(vec2 p,float a); 39 | float audio(float index); 40 | float rand(const vec2 co); 41 | sampler2D input0; 42 | sampler2D input1; 43 | }`; 44 | 45 | /** 46 | * Given a string, verifies that it is a valid glsl code for a vector field, 47 | * and then returns code + log. 48 | * 49 | * @param {String} vectorFieldCode 50 | */ 51 | export default function getParsedVectorFieldFunction(vectorFieldCode) { 52 | // TODO: what if we want to support 3d? 53 | return pragmaParse(vectorFieldCode).then(pragmaParseResult => { 54 | if (pragmaParseResult.error) { 55 | return pragmaParseResult; 56 | } 57 | 58 | vectorFieldCode = pragmaParseResult.getCode(); 59 | 60 | var parserResult = glslParser.check(vectorFieldCode, { globals: vectorFieldGlobals }); 61 | parserResult.code = vectorFieldCode; 62 | 63 | if (parserResult.log.errorCount) parserResult.error = parserError(parserResult.log); 64 | 65 | return parserResult; 66 | }); 67 | } 68 | 69 | function parserError(log) { 70 | let diag = log.diagnostics[0]; 71 | // TODO probably need to check kind (errors are 0, warnings are 1) 72 | let firstError = diag.range; 73 | let lineColumn = firstError.lineColumn(); 74 | let source = firstError.source; 75 | let offset = source._lineOffsets[lineColumn.line] 76 | let line = source.contents.substr(offset, lineColumn.column); 77 | line += source.contents.substring(firstError.start, firstError.end); 78 | let prefix = 'Line ' + lineColumn.line + ': '; 79 | let diagText = diag.text; 80 | return { 81 | error: 82 | prefix + line + '\n' + 83 | whitespace(prefix.length) + whitespace(lineColumn.column) + '^', 84 | errorDetail: diagText, 85 | isFloatError: isFloatError(diagText) 86 | }; 87 | } 88 | 89 | function isFloatError(diagText) { 90 | return diagText.indexOf('"int"') > -1 && 91 | diagText.indexOf('"float"') > -1; 92 | } 93 | 94 | function whitespace(length) { 95 | return new Array(length + 1).join(' '); 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/editor/pragmaParser.js: -------------------------------------------------------------------------------- 1 | import fetchGLSL from './fetchGLSL.js'; 2 | 3 | var pragmaInclude = '#include '; 4 | var nullCode = { code: '' } 5 | 6 | /** 7 | * Naively parses glsl code and tries to replace all `#pragma` statements 8 | * with empty string. Gives structured collection of pragma statements back 9 | * 10 | * @param {String} code 11 | */ 12 | export default function makePragmaParser(code) { 13 | if (!code) return new Promise(resolve => resolve(nullCode)); 14 | 15 | var parsedLines = processLineByLine(code); 16 | if (parsedLines.pending.length > 0) { 17 | return Promise.all(parsedLines.pending).then(() => parsedLines) 18 | .catch(error => { return {error: {error}}; }); 19 | } 20 | 21 | return new Promise(resolve => resolve(parsedLines)); 22 | } 23 | 24 | function processLineByLine(code) { 25 | var pending = [] 26 | var lines = code.split('\n'); 27 | var outputLines = []; 28 | var currentIndex = 0; 29 | lines.forEach((line, index) => { 30 | currentIndex = index; 31 | if (line && line[0] === '#') { 32 | outputLines.push(''); 33 | processPragma(line); 34 | } else { 35 | outputLines.push(line); 36 | } 37 | }); 38 | 39 | return { 40 | getCode, 41 | pending 42 | }; 43 | 44 | function getCode() { 45 | return outputLines.join('\n'); 46 | } 47 | 48 | function processPragma(line) { 49 | if (line.indexOf(pragmaInclude) === 0) { 50 | var include = line.substr(pragmaInclude.length); 51 | var insertIndex = currentIndex; 52 | pending.push(fetchGLSL(include).then(code => { 53 | outputLines[insertIndex] = code 54 | })) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/lib/editor/vectorFieldState.js: -------------------------------------------------------------------------------- 1 | import bus from '../bus'; 2 | import appState from '../appState'; 3 | import getParsedVectorFieldFunction from './getParsedVectorFieldFunction'; 4 | 5 | /** 6 | * A text editor state for the vector field equation. Manages vector field 7 | * program compilation and error reporting state. 8 | * 9 | * @param {Object} drawProgram 10 | */ 11 | export default function createVectorFieldEditorState(drawProgram) { 12 | bus.on('glsl-parser-ready', parseCode); 13 | var currentVectorFieldVersion = 0; 14 | 15 | // What is the current code? 16 | var currentVectorFieldCode = appState.getCode(); 17 | 18 | // For delayed parsing result verification (e.g. when vue is loaded it 19 | // can request us to see if there were any errors) 20 | var parserResult; 21 | 22 | loadCodeFromAppState(); 23 | 24 | var api = { 25 | getCode, 26 | setCode, 27 | dispose, 28 | 29 | // These properties are for UI only 30 | code: currentVectorFieldCode, 31 | error: '', 32 | errorDetail: '', 33 | isFloatError: false 34 | }; 35 | 36 | return api; 37 | 38 | function dispose() { 39 | bus.off('glsl-parser-ready', parseCode); 40 | } 41 | 42 | function getCode() { 43 | return appState.getCode(); 44 | } 45 | 46 | function setCode(vectorFieldCode) { 47 | if (vectorFieldCode === currentVectorFieldCode) { 48 | // If field hasn't changed, let's make sure that there was no previous 49 | // error 50 | if (parserResult && parserResult.error) { 51 | // And if there was error, let's revalidate code: 52 | parseCode(); 53 | } 54 | return; 55 | } 56 | 57 | trySetNewCode(vectorFieldCode).then((result) => { 58 | if (result.cancelled) return; 59 | 60 | if (result && result.error) { 61 | updateErrorInfo(result.error); 62 | return result; 63 | } 64 | 65 | currentVectorFieldCode = vectorFieldCode; 66 | api.code = vectorFieldCode; 67 | appState.saveCode(vectorFieldCode); 68 | }); 69 | } 70 | 71 | function updateErrorInfo(parserResult) { 72 | if (parserResult && parserResult.error) { 73 | api.error = parserResult.error; 74 | api.errorDetail = parserResult.errorDetail; 75 | api.isFloatError = parserResult.isFloatError; 76 | } else { 77 | api.error = ''; 78 | api.errorDetail = ''; 79 | api.isFloatError = false; 80 | } 81 | } 82 | 83 | function loadCodeFromAppState() { 84 | let persistedCode = appState.getCode(); 85 | if (persistedCode) { 86 | trySetNewCode(persistedCode).then(result => { 87 | if (!result.error) return; // This means we set correctly; 88 | // If we get here - something went wrong. see the console 89 | console.error('Failed to restore previous vector field: ', result.error); 90 | // Let's use default vector field 91 | trySetNewCode(appState.getDefaultCode()); 92 | }); 93 | } else { 94 | // we want a default vector field 95 | trySetNewCode(appState.getDefaultCode()); 96 | } 97 | } 98 | 99 | function parseCode(customCode) { 100 | return getParsedVectorFieldFunction(customCode || currentVectorFieldCode) 101 | .then(currentResult => { 102 | parserResult = currentResult 103 | updateErrorInfo(parserResult.error); 104 | return parserResult; 105 | }); 106 | } 107 | 108 | function trySetNewCode(vectorFieldCode) { 109 | currentVectorFieldVersion += 1; 110 | var capturedVersion = currentVectorFieldVersion; 111 | // step 1 - run through parser 112 | return parseCode(vectorFieldCode).then(parserResult => { 113 | if (capturedVersion !== currentVectorFieldVersion) { 114 | parserResult.cancelled = true; 115 | // a newer request was issued. Ignore these results. 116 | return parserResult; 117 | } 118 | 119 | if (parserResult.error) { 120 | return parserResult; 121 | } 122 | // step 2 - run through real webgl 123 | try { 124 | drawProgram.updateCode(parserResult.code); 125 | return parserResult; 126 | } catch (e) { 127 | return { 128 | error: { 129 | error: e.message 130 | } 131 | } 132 | } 133 | }); 134 | } 135 | } -------------------------------------------------------------------------------- /src/lib/generate-equation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A tiny toy equation generator. It is very naive, and does silly things 3 | * sometimes. Feel free to improve. 4 | */ 5 | 6 | var cfProb = 10; // base probability to generate a point. 7 | 8 | var probabilityClass = { 9 | POINT: cfProb, 10 | LENGTH: cfProb * 0.5, 11 | TRIGONOMETRY: cfProb * 0.9, 12 | ARITHMETICS: cfProb * 0.6, 13 | MINMAX: cfProb * 0.4, 14 | EXP: cfProb * 0.1, 15 | SIGN: cfProb * 0.01, 16 | } 17 | 18 | class BaseFunctionNode { 19 | constructor(className) { 20 | this.probability = 0; 21 | this.className = className; 22 | } 23 | 24 | getProbability() { 25 | return probabilityClass[this.className]; 26 | } 27 | 28 | render() { 29 | return ''; 30 | } 31 | } 32 | 33 | class SingleArgumentFunction extends BaseFunctionNode { 34 | constructor(operator, p) { 35 | super(p); 36 | this.operator = operator; 37 | } 38 | 39 | render() { 40 | var prevP = this.p; 41 | 42 | var prevP = this.getProbability(); 43 | probabilityClass[this.className] *= 0.25; 44 | normalizeProbabilities(); 45 | let args = generateArguments(); 46 | probabilityClass[this.className] = prevP; 47 | normalizeProbabilities(); 48 | return this.operator(args); 49 | } 50 | } 51 | 52 | class DualArgumentFunction extends BaseFunctionNode { 53 | constructor(operator, p) { 54 | super(p); 55 | this.operator = operator; 56 | } 57 | 58 | render() { 59 | // Decrease our probability to appear 60 | var prevP = this.getProbability(); 61 | probabilityClass[this.className] *= 0.25; 62 | 63 | normalizeProbabilities(); 64 | var left = generateArguments(); 65 | var right = generateArguments(); 66 | // revert it back; 67 | probabilityClass[this.className] = prevP; 68 | normalizeProbabilities(); 69 | return this.operator(left, right); 70 | } 71 | } 72 | 73 | class ConstantFunction extends BaseFunctionNode { 74 | constructor(constant, p) { 75 | super(p); 76 | this.constant = constant; 77 | } 78 | 79 | render() { 80 | return this.constant; 81 | } 82 | } 83 | 84 | 85 | var fList = [ 86 | new ConstantFunction('p.x', 'POINT'), 87 | new ConstantFunction('p.y', 'POINT'), 88 | 89 | // new DualArgumentFunction((a, b) => `length(vec2(${a}, ${b}))`, 'TRIGONOMETRY'), 90 | new ConstantFunction('length(p)', 'LENGTH'), 91 | 92 | new SingleArgumentFunction(a => `sin(${a})`, 'TRIGONOMETRY'), 93 | new SingleArgumentFunction(a => `cos(${a})`, 'TRIGONOMETRY'), 94 | // new SingleArgumentFunction(a => `sqrt(${a})`, cfProb * 0.8), 95 | // new SingleArgumentFunction(a => `inversesqrt(${a})`, cfProb * 0.8), 96 | 97 | 98 | new DualArgumentFunction((a, b) => `${a}*${b}`, 'ARITHMETICS'), 99 | new DualArgumentFunction((a, b) => `${a}/${b}`, 'ARITHMETICS'), 100 | new DualArgumentFunction((a, b) => `(${a}+${b})`, 'ARITHMETICS'), 101 | new DualArgumentFunction((a, b) => `(${a}-${b})`, 'ARITHMETICS'), 102 | 103 | new DualArgumentFunction((a, b) => { 104 | if (a === b) return a; 105 | return `min(${a},${b})` 106 | }, 'MINMAX'), 107 | new DualArgumentFunction((a, b) => { 108 | if (a === b) return a; 109 | return `max(${a},${b})` 110 | } , 'MINMAX'), 111 | 112 | new SingleArgumentFunction(a => `log(${a})`, 'EXP'), 113 | new SingleArgumentFunction(a => `exp(${a})`, 'EXP'), 114 | new DualArgumentFunction((a, b) => `pow(${a}, ${b})`, 'EXP'), 115 | 116 | new SingleArgumentFunction(a => `abs(${a})`, 'SIGN'), 117 | new SingleArgumentFunction(a => `sign(${a})`, 'SIGN'), 118 | 119 | //new ConstantFunction('1.', cfProb * 0.001), 120 | ]; 121 | 122 | 123 | function normalizeProbabilities() { 124 | var sum = 0; 125 | fList.forEach(element => (sum += element.getProbability())); 126 | fList.forEach(element => element.probability = element.getProbability()/sum); 127 | } 128 | 129 | function generateArguments() { 130 | var p = Math.random(); 131 | var cumulativeProbability = 0; 132 | var item; 133 | for (var i = 0; i < fList.length; ++i) { 134 | item = fList[i]; 135 | cumulativeProbability += item.probability; 136 | if (p < cumulativeProbability) { 137 | break; 138 | } 139 | } 140 | 141 | if (!item) throw new Error('no more items'); 142 | 143 | return item.render(); 144 | } 145 | 146 | export default function generate() { 147 | normalizeProbabilities(); 148 | var vX = generateArguments(); 149 | var vY = generateArguments(); 150 | return `v.x = ${vX}; 151 | v.y = ${vY};`; 152 | } -------------------------------------------------------------------------------- /src/lib/gl-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is based on https://github.com/mapbox/webgl-wind 3 | * by Vladimir Agafonkin 4 | * 5 | * Released under ISC License, Copyright (c) 2016, Mapbox 6 | * https://github.com/mapbox/webgl-wind/blob/master/LICENSE 7 | * 8 | * Adapted to field maps by Andrei Kashcha 9 | * Copyright (C) 2017 10 | */ 11 | export default { 12 | createTexture: createTexture, 13 | bindFramebuffer: bindFramebuffer, 14 | createProgram: createProgram, 15 | createBuffer: createBuffer, 16 | bindAttribute: bindAttribute, 17 | bindTexture: bindTexture 18 | } 19 | 20 | function bindTexture(gl, texture, unit) { 21 | gl.activeTexture(gl.TEXTURE0 + unit); 22 | gl.bindTexture(gl.TEXTURE_2D, texture); 23 | } 24 | 25 | function createBuffer(gl, data) { 26 | var buffer = gl.createBuffer(); 27 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 28 | gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); 29 | return buffer; 30 | } 31 | 32 | function createTexture(gl, filter, data, width, height) { 33 | var texture = gl.createTexture(); 34 | gl.bindTexture(gl.TEXTURE_2D, texture); 35 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 36 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 37 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); 38 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); 39 | if (data instanceof Uint8Array) { 40 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); 41 | } else { 42 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); 43 | } 44 | gl.bindTexture(gl.TEXTURE_2D, null); 45 | return texture; 46 | } 47 | 48 | function bindFramebuffer(gl, framebuffer, texture) { 49 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 50 | if (texture) { 51 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 52 | } 53 | } 54 | 55 | function bindAttribute(gl, buffer, attribute, numComponents) { 56 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 57 | gl.enableVertexAttribArray(attribute); 58 | gl.vertexAttribPointer(attribute, numComponents, gl.FLOAT, false, 0, 0); 59 | } 60 | 61 | function createProgram(gl, vertexSource, fragmentSource) { 62 | var program = gl.createProgram(); 63 | 64 | var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource); 65 | var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource); 66 | 67 | gl.attachShader(program, vertexShader); 68 | gl.attachShader(program, fragmentShader); 69 | 70 | gl.linkProgram(program); 71 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 72 | throw new Error(gl.getProgramInfoLog(program)); 73 | } 74 | 75 | var wrapper = { 76 | program: program, 77 | unload: unload 78 | }; 79 | 80 | var numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); 81 | var i; 82 | for (i = 0; i < numAttributes; i++) { 83 | var attribute = gl.getActiveAttrib(program, i); 84 | wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name); 85 | } 86 | var numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 87 | for (i = 0; i < numUniforms; i++) { 88 | var uniform = gl.getActiveUniform(program, i); 89 | wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name); 90 | } 91 | 92 | return wrapper; 93 | 94 | function unload() { 95 | gl.deleteProgram(program); 96 | } 97 | } 98 | 99 | function createShader(gl, type, source) { 100 | var shader = gl.createShader(type); 101 | gl.shaderSource(shader, source); 102 | 103 | gl.compileShader(shader); 104 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 105 | throw new Error(gl.getShaderInfoLog(shader)); 106 | } 107 | 108 | return shader; 109 | } -------------------------------------------------------------------------------- /src/lib/hsl2rgb.js: -------------------------------------------------------------------------------- 1 | // Based on https://gist.github.com/mjackson/5311256 2 | /** 3 | * Converts an HSL color value to RGB. Conversion formula 4 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space. 5 | * Assumes h, s, and l are contained in the set [0, 1] and 6 | * returns r, g, and b in the set [0, 255]. 7 | * 8 | * @param Number h The hue 9 | * @param Number s The saturation 10 | * @param Number l The lightness 11 | * @return Array The RGB representation 12 | */ 13 | export default function hslToRgb(h, s, l) { 14 | var r, g, b; 15 | 16 | if (s == 0) { 17 | r = g = b = l; // achromatic 18 | } else { 19 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 20 | var p = 2 * l - q; 21 | 22 | r = hue2rgb(p, q, h + 1/3); 23 | g = hue2rgb(p, q, h); 24 | b = hue2rgb(p, q, h - 1/3); 25 | } 26 | 27 | return [ r * 255, g * 255, b * 255 ]; 28 | 29 | function hue2rgb(p, q, t) { 30 | if (t < 0) t += 1; 31 | if (t > 1) t -= 1; 32 | if (t < 1/6) return p + (q - p) * 6 * t; 33 | if (t < 1/2) return q; 34 | if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; 35 | return p; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/isSmallScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks whether current window belongs to a "Small" screen 3 | * size. Small screens have slightly different initial behavior. 4 | * E.g. the settings window is always collapsed. 5 | * 6 | * This needs to be in sync with `small-screen` variable in 7 | * ../commponents/shared.styl 8 | */ 9 | export default function isSmallScreen() { 10 | return window.innerWidth < 600 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/nativeMain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The main entry point to the application. 3 | * 4 | * It is initialized immediately with webgl, and puts 5 | * vue.js app loading into the future. 6 | */ 7 | import initScene from './scene'; 8 | import bus from './bus'; 9 | import { initAutoMode } from './autoMode'; 10 | 11 | var canvas = document.getElementById('scene'); 12 | // Canvas may not be available in test run 13 | if (canvas) initVectorFieldApp(canvas); 14 | 15 | // Tell webpack to split bundle, and download settings UI later. 16 | require.ensure('@/vueApp.js', () => { 17 | // Settings UI is ready, initialize vue.js application 18 | require('@/vueApp.js'); 19 | }); 20 | 21 | function initVectorFieldApp(canvas) { 22 | canvas.width = window.innerWidth; 23 | canvas.height = window.innerHeight; 24 | var ctxOptions = {antialiasing: false }; 25 | 26 | var gl = canvas.getContext('webgl', ctxOptions) || 27 | canvas.getContext('experimental-webgl', ctxOptions); 28 | 29 | if (gl) { 30 | window.webGLEnabled = true; 31 | var scene = initScene(gl); 32 | scene.start(); 33 | initAutoMode(scene); 34 | window.scene = scene; 35 | } else { 36 | window.webGLEnabled = false; 37 | } 38 | } 39 | 40 | var CCapture; 41 | var currentCapturer; 42 | 43 | window.startRecord = startRecord; 44 | window.isRecording = false; 45 | 46 | function startRecord(url) { 47 | if (!CCapture) { 48 | require.ensure('ccapture.js', () => { 49 | CCapture = require('ccapture.js'); 50 | window.stopRecord = stopRecord; 51 | startRecord(url); 52 | }); 53 | 54 | return; 55 | } 56 | 57 | if (currentCapturer) { 58 | currentCapturer.stop(); 59 | } 60 | 61 | if (!ffmpegScriptLoaded()) { 62 | var ffmpegServer = document.createElement('script'); 63 | ffmpegServer.setAttribute('src', url || 'http://localhost:8080/ffmpegserver/ffmpegserver.js'); 64 | ffmpegServer.onload = () => startRecord(url); 65 | document.head.appendChild(ffmpegServer); 66 | return; 67 | } 68 | 69 | currentCapturer = new CCapture( { 70 | format: 'ffmpegserver', 71 | framerate: 60, 72 | verbose: true, 73 | name: "fieldplay", 74 | extension: ".mp4", 75 | codec: "mpeg4", 76 | ffmpegArguments: [ 77 | "-b:v", "12M", 78 | ], 79 | }); 80 | 81 | window.isRecording = true; 82 | currentCapturer.start(); 83 | bus.fire('start-record', currentCapturer) 84 | } 85 | 86 | function ffmpegScriptLoaded() { 87 | return typeof FFMpegServer !== 'undefined' 88 | } 89 | 90 | function stopRecord() { 91 | window.isRecording = false; 92 | bus.fire('stop-record', currentCapturer) 93 | currentCapturer.stop(); 94 | currentCapturer.save(); 95 | } 96 | -------------------------------------------------------------------------------- /src/lib/nativeMediaRecorder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This API allows to record vector field locally. 3 | * 4 | * See https://github.com/anvaka/fieldplay/blob/main/ScreenRecording.md for more details 5 | * @param {HTMLCanvas} canvas 6 | */ 7 | export default function saveVideo(canvas) { 8 | var recordedChunks = []; 9 | 10 | var options = {mimeType: 'video/webm'}; 11 | var mediaRecorder = new MediaRecorder(canvas.captureStream(60), options); 12 | mediaRecorder.ondataavailable = handleDataAvailable; 13 | mediaRecorder.onstop = handleStop; 14 | 15 | mediaRecorder.start(); 16 | 17 | return stop; 18 | 19 | function stop() { 20 | mediaRecorder.stop(); 21 | } 22 | 23 | function handleStop() { 24 | console.log('done'); 25 | download(); 26 | } 27 | 28 | 29 | function handleDataAvailable(event) { 30 | console.log('data'); 31 | if (event.data.size > 0) { 32 | recordedChunks.push(event.data); 33 | } else { 34 | // ... 35 | } 36 | } 37 | function download() { 38 | var blob = new Blob(recordedChunks, { 39 | type: 'video/webm' 40 | }); 41 | var url = URL.createObjectURL(blob); 42 | var a = document.createElement('a'); 43 | document.body.appendChild(a); 44 | a.style = 'display: none'; 45 | a.href = url; 46 | a.download = 'test.webm'; 47 | a.click(); 48 | window.URL.revokeObjectURL(url); 49 | } 50 | } -------------------------------------------------------------------------------- /src/lib/programs/audioProgram.js: -------------------------------------------------------------------------------- 1 | import bus from '../bus'; 2 | import glUtil from '../gl-utils'; 3 | 4 | export default audioProgram; 5 | 6 | function audioProgram(ctx) { 7 | var gl = ctx.gl; 8 | 9 | var audioWidth = 8, audioHeight = 8; 10 | var audioBuffer = new Uint8Array(audioWidth * audioHeight * 4); 11 | var audioTexture = glUtil.createTexture(gl, gl.NEAREST, audioBuffer, audioWidth, audioHeight); 12 | var audioDirty = false; 13 | ctx.audioTexture = audioTexture; 14 | 15 | bus.on('audio', updateAudioBuffer); 16 | 17 | return { 18 | updateTextures, 19 | dispose 20 | }; 21 | 22 | function dispose() { 23 | bus.off('audio', updateAudioBuffer); 24 | gl.deleteTexture(audioTexture); 25 | } 26 | 27 | function updateTextures() { 28 | if (!audioDirty) return; 29 | audioDirty = false; 30 | 31 | // TODO: This should come from fftSize? 32 | var width = 5, height = 5; 33 | gl.bindTexture(gl.TEXTURE_2D, audioTexture); 34 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, audioBuffer); 35 | gl.bindTexture(gl.TEXTURE_2D, null); 36 | } 37 | 38 | function updateAudioBuffer(newBuffer) { 39 | audioBuffer = newBuffer; 40 | audioDirty = true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/programs/colorModes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Various color modes. 3 | */ 4 | export default { 5 | /** 6 | * Each particle gets its own color 7 | */ 8 | UNIFORM: 1, 9 | 10 | /** 11 | * Color of a particle depends on its velocity 12 | */ 13 | VELOCITY: 2, 14 | 15 | /** 16 | * Color of a particle depends on its velocity vector angle. 17 | */ 18 | ANGLE: 3, 19 | 20 | /** 21 | * The color comes from a shader. WIP 22 | */ 23 | CUSTOM: 4 24 | } -------------------------------------------------------------------------------- /src/lib/programs/colorProgram.js: -------------------------------------------------------------------------------- 1 | import util from '../gl-utils'; 2 | import bus from '../bus'; 3 | import {decodeFloatRGBA} from '../utils/floatPacking'; 4 | import makeStatCounter from '../utils/makeStatCounter'; 5 | 6 | const OUT_V_X = 6; 7 | const OUT_V_Y = 7; 8 | /** 9 | * This program allows to change color of each particle. It works by 10 | * reading current velocities into a texture from the framebuffer. Once 11 | * velocities are read, it checks velocity scale and passes it to a draw program. 12 | */ 13 | export default function colorProgram(ctx) { 14 | var speedNeedsUpdate = true; 15 | var {gl} = ctx; 16 | var velocity_y_texture, velocity_x_texture; 17 | var particleStateResolution; 18 | var pendingSpeedUpdate; 19 | var numParticles; 20 | var velocityCounter = makeStatCounter(); 21 | var velocity_x; 22 | var velocity_y; 23 | 24 | listenToEvents(); 25 | 26 | return { 27 | updateCode, 28 | updateParticlesPositions, 29 | updateParticlesCount, 30 | setColorMinMax, 31 | requestSpeedUpdate, 32 | dispose 33 | }; 34 | 35 | function listenToEvents() { 36 | bus.on('integration-timestep-changed', requestSpeedUpdate); 37 | bus.on('bbox-change', requestSpeedUpdate); 38 | bus.on('refresh-speed', requestSpeedUpdate); 39 | } 40 | 41 | function dispose() { 42 | bus.off('integration-timestep-changed', requestSpeedUpdate); 43 | bus.off('bbox-change', requestSpeedUpdate); 44 | bus.off('refresh-speed', requestSpeedUpdate); 45 | disposeTextures(); 46 | } 47 | 48 | function disposeTextures() { 49 | if (velocity_x_texture) gl.deleteTexture(velocity_x_texture); 50 | if (velocity_y_texture) gl.deleteTexture(velocity_y_texture); 51 | } 52 | 53 | function requestSpeedUpdate() { 54 | if (pendingSpeedUpdate) clearTimeout(pendingSpeedUpdate); 55 | pendingSpeedUpdate = setTimeout(() => { 56 | speedNeedsUpdate = true; 57 | pendingSpeedUpdate = 0; 58 | }, 50); 59 | } 60 | 61 | function setColorMinMax(program) { 62 | gl.uniform2f(program.u_velocity_range, velocityCounter.getMin(), velocityCounter.getMax()); 63 | } 64 | 65 | function updateParticlesCount() { 66 | disposeTextures(); 67 | 68 | particleStateResolution = ctx.particleStateResolution; 69 | numParticles = particleStateResolution * particleStateResolution; 70 | 71 | velocity_x = new Uint8Array(numParticles * 4); 72 | velocity_y = new Uint8Array(numParticles * 4); 73 | velocity_x_texture = util.createTexture(gl, gl.NEAREST, velocity_x, particleStateResolution, particleStateResolution); 74 | velocity_y_texture = util.createTexture(gl, gl.NEAREST, velocity_y, particleStateResolution, particleStateResolution); 75 | 76 | requestSpeedUpdate(); 77 | } 78 | 79 | function updateCode() { 80 | requestSpeedUpdate(); 81 | } 82 | 83 | function updateParticlesPositions(program) { 84 | if (!speedNeedsUpdate || !velocity_x || !velocity_y) return; 85 | speedNeedsUpdate = false; 86 | 87 | // We assume this is called from update position program 88 | util.bindFramebuffer(gl, ctx.framebuffer, velocity_x_texture); 89 | gl.uniform1i(program.u_out_coordinate, OUT_V_X); 90 | gl.drawArrays(gl.TRIANGLES, 0, 6); 91 | gl.readPixels(0, 0, particleStateResolution, particleStateResolution, gl.RGBA, gl.UNSIGNED_BYTE, velocity_x); 92 | 93 | util.bindFramebuffer(gl, ctx.framebuffer, velocity_y_texture); 94 | gl.uniform1i(program.u_out_coordinate, OUT_V_Y); 95 | gl.drawArrays(gl.TRIANGLES, 0, 6); 96 | gl.readPixels(0, 0, particleStateResolution, particleStateResolution, gl.RGBA, gl.UNSIGNED_BYTE, velocity_y); 97 | 98 | updateMinMax(); 99 | } 100 | 101 | function updateMinMax() { 102 | velocityCounter.reset(); 103 | // TODO: Do I want this to be async? 104 | for(var i = 0; i < velocity_y.length; i+=4) { 105 | var vx = readFloat(velocity_x, i); 106 | var vy = readFloat(velocity_y, i); 107 | var v = Math.sqrt(vx * vx + vy * vy); 108 | velocityCounter.add(v); 109 | } 110 | } 111 | } 112 | 113 | function readFloat(buffer, offset) { 114 | return decodeFloatRGBA( 115 | buffer[offset + 0], 116 | buffer[offset + 1], 117 | buffer[offset + 2], 118 | buffer[offset + 3] 119 | ); 120 | } -------------------------------------------------------------------------------- /src/lib/programs/drawParticlesProgram.js: -------------------------------------------------------------------------------- 1 | import util from '../gl-utils'; 2 | import DrawParticleGraph from '../shaderGraph/DrawParticleGraph'; 3 | import makeUpdatePositionProgram from './updatePositionProgram'; 4 | import { encodeFloatRGBA } from '../utils/floatPacking.js'; 5 | import config from '../config'; 6 | import createAudioProgram from './audioProgram'; 7 | 8 | /** 9 | * This program manages particles life-cycle. It updates particles positions 10 | * and initiates drawing them on screen. 11 | * 12 | * @param {Object} ctx rendering context. Holds WebGL state 13 | */ 14 | export default function drawParticlesProgram(ctx) { 15 | var gl = ctx.gl; 16 | 17 | var particleStateResolution, particleIndexBuffer; 18 | var numParticles; 19 | 20 | var currentVectorField = ''; 21 | var updatePositionProgram = makeUpdatePositionProgram(ctx); 22 | var audioProgram; 23 | 24 | var drawProgram; 25 | initPrograms(); 26 | 27 | return { 28 | updateParticlesCount, 29 | updateParticlesPositions, 30 | drawParticles, 31 | updateCode, 32 | updateColorMode 33 | } 34 | 35 | function initPrograms() { 36 | // need to update the draw graph because color mode shader has changed. 37 | initDrawProgram(); 38 | 39 | if (config.isAudioEnabled) { 40 | if (audioProgram) audioProgram.dispose(); 41 | audioProgram = createAudioProgram(ctx); 42 | } 43 | } 44 | 45 | function initDrawProgram() { 46 | if (drawProgram) drawProgram.unload(); 47 | 48 | const drawGraph = new DrawParticleGraph(ctx); 49 | const vertexShaderCode = drawGraph.getVertexShader(currentVectorField); 50 | drawProgram = util.createProgram(gl, vertexShaderCode, drawGraph.getFragmentShader()); 51 | } 52 | 53 | function updateParticlesPositions() { 54 | if (!currentVectorField) return; 55 | 56 | ctx.frame += 1 57 | ctx.frameSeed = Math.random(); 58 | 59 | // TODO: Remove this. 60 | if (audioProgram) audioProgram.updateTextures(); 61 | 62 | updatePositionProgram.updateParticlesPositions(); 63 | } 64 | 65 | function updateColorMode() { 66 | initDrawProgram(); 67 | } 68 | 69 | function updateCode(vfCode) { 70 | ctx.frame = 0; 71 | currentVectorField = vfCode; 72 | updatePositionProgram.updateCode(vfCode); 73 | 74 | initDrawProgram(); 75 | } 76 | 77 | function updateParticlesCount() { 78 | particleStateResolution = ctx.particleStateResolution; 79 | numParticles = particleStateResolution * particleStateResolution; 80 | var particleIndices = new Float32Array(numParticles); 81 | var particleStateX = new Uint8Array(numParticles * 4); 82 | var particleStateY = new Uint8Array(numParticles * 4); 83 | 84 | var minX = ctx.bbox.minX; var minY = ctx.bbox.minY; 85 | var width = ctx.bbox.maxX - minX; 86 | var height = ctx.bbox.maxY - minY; 87 | for (var i = 0; i < numParticles; i++) { 88 | encodeFloatRGBA((Math.random()) * width + minX, particleStateX, i * 4); // randomize the initial particle positions 89 | encodeFloatRGBA((Math.random()) * height + minY, particleStateY, i * 4); // randomize the initial particle positions 90 | 91 | particleIndices[i] = i; 92 | } 93 | 94 | if (particleIndexBuffer) gl.deleteBuffer(particleIndexBuffer); 95 | particleIndexBuffer = util.createBuffer(gl, particleIndices); 96 | 97 | updatePositionProgram.updateParticlesCount(particleStateX, particleStateY); 98 | } 99 | 100 | function drawParticles() { 101 | if (!currentVectorField) return; 102 | 103 | var program = drawProgram; 104 | gl.useProgram(program.program); 105 | 106 | util.bindAttribute(gl, particleIndexBuffer, program.a_index, 1); 107 | 108 | updatePositionProgram.prepareToDraw(program); 109 | ctx.inputs.updateBindings(program); 110 | 111 | gl.uniform1f(program.u_h, ctx.integrationTimeStep); 112 | gl.uniform1f(program.frame, ctx.frame); 113 | gl.uniform1f(program.u_particles_res, particleStateResolution); 114 | var bbox = ctx.bbox; 115 | gl.uniform2f(program.u_min, bbox.minX, bbox.minY); 116 | gl.uniform2f(program.u_max, bbox.maxX, bbox.maxY); 117 | 118 | var cursor = ctx.cursor; 119 | gl.uniform4f(program.cursor, cursor.clickX, cursor.clickY, cursor.hoverX, cursor.hoverY); 120 | gl.drawArrays(gl.POINTS, 0, numParticles); 121 | } 122 | } -------------------------------------------------------------------------------- /src/lib/programs/inputs/imageInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a single image binding element in the input collection. 3 | */ 4 | import loadTexture from "./loadTexture"; 5 | import glUtils from '../../gl-utils'; 6 | import config from '../../config'; 7 | import bus from '../../bus'; 8 | 9 | const FREE_TEXTURE_UNIT = config.FREE_TEXTURE_UNIT; 10 | 11 | export default function createImageInputBinding(ctx, url, callbacks) { 12 | var texture = null; 13 | loadTexture(ctx.gl, url).then(setTexture).catch(handleError); 14 | 15 | return { 16 | updateBinding, 17 | dispose() { 18 | // TODO: Potential race condition, as loadTexture is async. 19 | ctx.gl.deleteTexture(texture); 20 | } 21 | } 22 | 23 | function handleError(err) { 24 | if (callbacks && callbacks.error) { 25 | callbacks.error(err); 26 | } 27 | } 28 | 29 | function setTexture(loadedTexture) { 30 | texture = loadedTexture; 31 | bus.fire('refresh-speed') 32 | if (callbacks && callbacks.done) { 33 | callbacks.done(url); 34 | } 35 | } 36 | 37 | function updateBinding(program, inputIndex) { 38 | if (!texture) return; 39 | 40 | var realIndex = inputIndex + FREE_TEXTURE_UNIT; 41 | glUtils.bindTexture(ctx.gl, texture, realIndex); 42 | ctx.gl.uniform1i(program[`input${inputIndex}`], realIndex); 43 | } 44 | } -------------------------------------------------------------------------------- /src/lib/programs/inputs/initTexture.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/fieldplay/0d37a303b3278685528827b7d7199855c9ab3db3/src/lib/programs/inputs/initTexture.js -------------------------------------------------------------------------------- /src/lib/programs/inputs/inputCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Collection of input bindings. Individual program works with this collection 3 | * to command it update texture bindings. 4 | */ 5 | export default function createInputCollection() { 6 | var boundInputs = new Map() 7 | var currentProgram; 8 | return { 9 | updateBindings, 10 | bindInput, 11 | } 12 | 13 | function bindInput(inputIndex, inputBinding) { 14 | var prevBinding = boundInputs.get(inputIndex); 15 | if (prevBinding) { 16 | prevBinding.dispose(); 17 | } 18 | boundInputs.set(inputIndex, inputBinding); 19 | } 20 | 21 | function updateBindings(program) { 22 | currentProgram = program; 23 | boundInputs.forEach(updateInputBinding); 24 | } 25 | 26 | function updateInputBinding(input, inputIndex) { 27 | input.updateBinding(currentProgram, inputIndex); 28 | } 29 | } -------------------------------------------------------------------------------- /src/lib/programs/inputs/loadTexture.js: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL 2 | export default function loadTexture(gl, url) { 3 | var resolveTexture, rejectTexture; 4 | 5 | var image = new Image(); 6 | image.crossOrigin = ''; 7 | 8 | image.onload = bindTexture; 9 | image.onerror = reportError; 10 | image.src = url; 11 | 12 | return new Promise((resolve, reject) => { 13 | resolveTexture = resolve; 14 | rejectTexture = reject; 15 | }); 16 | 17 | function reportError(err) { 18 | rejectTexture(err); 19 | } 20 | 21 | function bindTexture() { 22 | var texture = gl.createTexture(); 23 | var level = 0; 24 | var internalFormat = gl.RGBA; 25 | var srcFormat = gl.RGBA; 26 | var srcType = gl.UNSIGNED_BYTE; 27 | gl.bindTexture(gl.TEXTURE_2D, texture); 28 | gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, 29 | srcFormat, srcType, image); 30 | 31 | if (isPowerOf2(image.width) && isPowerOf2(image.height)) { 32 | gl.generateMipmap(gl.TEXTURE_2D); 33 | } else { 34 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 35 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 36 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 37 | } 38 | 39 | resolveTexture(texture); 40 | } 41 | } 42 | 43 | function isPowerOf2(value) { 44 | return (value & (value - 1)) == 0; 45 | } -------------------------------------------------------------------------------- /src/lib/programs/inputs/videoInput.js: -------------------------------------------------------------------------------- 1 | import config from '../../config'; 2 | import glUtils from '../../gl-utils'; 3 | 4 | export default function createVideoInput(ctx, url) { 5 | var playing = false; 6 | var timeupdate = false; 7 | var copyVideo = false; 8 | 9 | var video = setupVideo(); 10 | var currentFrame = createTexture(); 11 | 12 | return { 13 | updateBinding, 14 | dispose 15 | }; 16 | 17 | function updateBinding(program, inputIndex) { 18 | var realIndex = inputIndex + config.FREE_TEXTURE_UNIT; 19 | glUtils.bindTexture(ctx.gl, currentFrame, realIndex); 20 | if (copyVideo) updateTexture(ctx.gl) 21 | ctx.gl.uniform1i(program[`input${inputIndex}`], realIndex); 22 | } 23 | 24 | function updateTexture(gl) { 25 | const level = 0; 26 | const internalFormat = gl.RGBA; 27 | const srcFormat = gl.RGBA; 28 | const srcType = gl.UNSIGNED_BYTE; 29 | gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, video); 30 | } 31 | 32 | function createTexture() { 33 | var gl = ctx.gl; 34 | var texture = gl.createTexture(); 35 | gl.bindTexture(gl.TEXTURE_2D, texture); 36 | 37 | var level = 0; 38 | var internalFormat = gl.RGBA; 39 | var width = 1; 40 | var height = 1; 41 | var border = 0; 42 | var srcFormat = gl.RGBA; 43 | var srcType = gl.UNSIGNED_BYTE; 44 | var pixel = new Uint8Array([0, 0, 255, 255]); 45 | gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, 46 | width, height, border, srcFormat, srcType, 47 | pixel); 48 | 49 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 50 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 51 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 52 | 53 | return texture; 54 | } 55 | function dispose() { 56 | video.removeEventListener('playing', onPlaying, true); 57 | video.removeEventListener('timeupdate', onTimeUpdate, true); 58 | } 59 | 60 | function setupVideo() { 61 | var video = document.createElement('video'); 62 | video.crossOrigin = ''; 63 | video.autoplay = true; 64 | video.muted = true; 65 | video.loop = true; 66 | 67 | // Waiting for these 2 events ensures there is data in the video 68 | video.addEventListener('playing', onPlaying, true); 69 | video.addEventListener('timeupdate', onTimeUpdate, true); 70 | 71 | video.src = url; 72 | video.play(); 73 | 74 | return video; 75 | } 76 | 77 | function checkReady() { 78 | if (playing && timeupdate) { 79 | copyVideo = true; 80 | } 81 | } 82 | function onPlaying() { 83 | playing = true; 84 | checkReady(); 85 | } 86 | 87 | function onTimeUpdate() { 88 | timeupdate = true; 89 | checkReady(); 90 | } 91 | } -------------------------------------------------------------------------------- /src/lib/programs/screenProgram.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Renders computed state onto the screen. 3 | */ 4 | import glUtils from '../gl-utils'; 5 | 6 | const NO_TRANSFORM = {dx: 0, dy: 0, scale: 1}; 7 | 8 | export default function makeScreenProgram(ctx) { 9 | var {gl, canvasRect} = ctx; 10 | 11 | var screenTexture, backgroundTexture; 12 | var boundBoxTextureTransform = {dx: 0, dy: 0, scale: 1}; 13 | var lastRenderedBoundingBox = null; 14 | 15 | // TODO: Allow customization? Last time I tried, I didn't like it too much. 16 | // It was very easy to screw up the design, and the tool looked ugly :-/ 17 | let backgroundColor = { r: 19/255, g: 41/255, b: 79/255, a: 1 }; 18 | 19 | updateScreenTextures(); 20 | var screenProgram = glUtils.createProgram(gl, getScreenVertexShader(), getScreenFragmentShader()); 21 | 22 | var api = { 23 | fadeOutLastFrame, 24 | renderCurrentScreen, 25 | updateScreenTextures, 26 | 27 | boundingBoxUpdated: false 28 | }; 29 | 30 | return api; 31 | 32 | function fadeOutLastFrame() { 33 | // render to the frame buffer 34 | glUtils.bindFramebuffer(gl, ctx.framebuffer, screenTexture); 35 | gl.viewport(0, 0, canvasRect.width, canvasRect.height); 36 | 37 | if (api.boundingBoxUpdated && lastRenderedBoundingBox) { 38 | // We move the back texture, relative to the bounding box change. This eliminates 39 | // particle train artifacts, though, not all of them: https://computergraphics.stackexchange.com/questions/5754/fading-particles-and-transition 40 | // If you know how to improve this - please let me know. 41 | boundBoxTextureTransform.dx = -(ctx.bbox.minX - lastRenderedBoundingBox.minX)/(ctx.bbox.maxX - ctx.bbox.minX); 42 | boundBoxTextureTransform.dy = -(ctx.bbox.minY - lastRenderedBoundingBox.minY)/(ctx.bbox.maxY - ctx.bbox.minY); 43 | boundBoxTextureTransform.scale = (ctx.bbox.maxX - ctx.bbox.minX) / (lastRenderedBoundingBox.maxX - lastRenderedBoundingBox.minX); 44 | drawTexture(backgroundTexture, ctx.fadeOpacity, boundBoxTextureTransform); 45 | } else { 46 | drawTexture(backgroundTexture, ctx.fadeOpacity, NO_TRANSFORM) 47 | } 48 | } 49 | 50 | function renderCurrentScreen() { 51 | glUtils.bindFramebuffer(gl, null); 52 | 53 | saveLastBbox(); 54 | 55 | gl.enable(gl.BLEND); 56 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 57 | gl.clearColor(backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a); 58 | gl.clear(gl.COLOR_BUFFER_BIT); 59 | drawTexture(screenTexture, 1.0, NO_TRANSFORM); 60 | gl.disable(gl.BLEND); 61 | 62 | var temp = backgroundTexture; 63 | backgroundTexture = screenTexture; 64 | screenTexture = temp; 65 | 66 | api.boundingBoxUpdated = false; 67 | if (window.audioTexture) { 68 | drawTexture(window.audioTexture, 1.0, NO_TRANSFORM); 69 | } 70 | } 71 | 72 | function updateScreenTextures() { 73 | var {width, height} = canvasRect; 74 | var emptyPixels = new Uint8Array(width * height * 4); 75 | if (screenTexture) { 76 | gl.deleteTexture(screenTexture); 77 | } 78 | if (backgroundTexture) { 79 | gl.deleteTexture(backgroundTexture); 80 | } 81 | 82 | screenTexture = glUtils.createTexture(gl, gl.NEAREST, emptyPixels, width, height); 83 | backgroundTexture = glUtils.createTexture(gl, gl.NEAREST, emptyPixels, width, height); 84 | } 85 | 86 | function saveLastBbox() { 87 | if (!lastRenderedBoundingBox) { 88 | lastRenderedBoundingBox = { 89 | minX: ctx.bbox.minX, 90 | minY: ctx.bbox.minY, 91 | maxX: ctx.bbox.maxX, 92 | maxY: ctx.bbox.maxY 93 | } 94 | 95 | return; 96 | } 97 | 98 | lastRenderedBoundingBox.minX = ctx.bbox.minX; 99 | lastRenderedBoundingBox.minY = ctx.bbox.minY; 100 | lastRenderedBoundingBox.maxX = ctx.bbox.maxX; 101 | lastRenderedBoundingBox.maxY = ctx.bbox.maxY; 102 | } 103 | 104 | function drawTexture(texture, opacity, textureTransform) { 105 | var program = screenProgram; 106 | gl.useProgram(program.program); 107 | glUtils.bindAttribute(gl, ctx.quadBuffer, program.a_pos, 2); 108 | 109 | // TODO: This index is very fragile. I need to find a way 110 | glUtils.bindTexture(gl, texture, ctx.screenTextureUnit); 111 | gl.uniform1i(program.u_screen, ctx.screenTextureUnit); 112 | 113 | gl.uniform1f(program.u_opacity_border, 0.02); 114 | gl.uniform1f(program.u_opacity, opacity); 115 | gl.uniform3f(program.u_transform, textureTransform.dx, textureTransform.dy, textureTransform.scale); 116 | 117 | gl.drawArrays(gl.TRIANGLES, 0, 6); 118 | } 119 | } 120 | 121 | function getScreenVertexShader() { 122 | return `// screen program 123 | precision highp float; 124 | 125 | attribute vec2 a_pos; 126 | varying vec2 v_tex_pos; 127 | uniform vec3 u_transform; 128 | 129 | void main() { 130 | v_tex_pos = a_pos; 131 | vec2 pos = a_pos; 132 | 133 | // This transformation tries to move texture (raster) to the approximate position 134 | // of particles on the current frame. This is needed to avoid rendering artifacts 135 | // during pan/zoom: https://computergraphics.stackexchange.com/questions/5754/fading-particles-and-transition 136 | 137 | // PS: I must admit, I wrote this formula through sweat and tears, and 138 | // I still have no idea why I don't need to apply (pos.y - 0.5) to Y coordinate. 139 | // Is it because I use aspect ratio for bounding box? 140 | pos.x = (pos.x - 0.5) / u_transform.z - u_transform.x + 0.5 * u_transform.z; 141 | pos.y = pos.y / u_transform.z + u_transform.y; 142 | 143 | pos = 1.0 - 2.0 * pos; 144 | gl_Position = vec4(pos, 0, 1); 145 | }` 146 | } 147 | 148 | function getScreenFragmentShader() { 149 | return `precision highp float; 150 | uniform sampler2D u_screen; 151 | uniform float u_opacity; 152 | uniform float u_opacity_border; 153 | 154 | varying vec2 v_tex_pos; 155 | 156 | void main() { 157 | vec2 p = 1.0 - v_tex_pos; 158 | vec4 color = texture2D(u_screen, p); 159 | 160 | // For some reason particles near border leave trace when we translate the texture 161 | // This is my dirty hack to fix it: https://computergraphics.stackexchange.com/questions/5754/fading-particles-and-transition 162 | if (p.x < u_opacity_border || p.x > 1. - u_opacity_border || p.y < u_opacity_border || p.y > 1. - u_opacity_border) { 163 | gl_FragColor = vec4(0.); 164 | } else { 165 | // opacity fade out even with a value close to 0.0 166 | gl_FragColor = vec4(floor(255.0 * color * u_opacity) / 255.0); 167 | } 168 | }` 169 | } -------------------------------------------------------------------------------- /src/lib/programs/updatePositionProgram.js: -------------------------------------------------------------------------------- 1 | import util from '../gl-utils'; 2 | import UpdatePositionGraph from '../shaderGraph/updatePositionGraph'; 3 | import ColorMode from './colorModes'; 4 | import makeReadProgram from './colorProgram'; 5 | import textureCollection from '../utils/textureCollection'; 6 | import makeStatCounter from '../utils/makeStatCounter'; 7 | import {decodeFloatRGBA} from '../utils/floatPacking'; 8 | import bus from '../bus'; 9 | 10 | const particlePositionShaderCodeBuilder = new UpdatePositionGraph(); 11 | 12 | export default function updatePositionProgram(ctx) { 13 | var gl = ctx.gl; 14 | var readTextures, writeTextures; 15 | var particleStateResolution; 16 | var updateProgram; 17 | var readVelocity = makeReadProgram(ctx); 18 | 19 | // If someone needs to get vectors out from the GPU, they send a `vector-lines-request` 20 | // over the bus. This request is delayed until next compute frame. Once it is handled, 21 | // we send them back response with calculated vectors. 22 | var pendingVectorLines; 23 | 24 | // TODO: need to make sure we are not leaking. 25 | bus.on('vector-lines-request', putVectorLinesRequestIntoQueue); 26 | 27 | return { 28 | updateCode, 29 | updateParticlesPositions, 30 | updateParticlesCount, 31 | prepareToDraw, 32 | }; 33 | 34 | function updateCode(vectorField) { 35 | particlePositionShaderCodeBuilder.setCustomVectorField(vectorField); 36 | let fragment = particlePositionShaderCodeBuilder.getFragmentShader(); 37 | let vertex = particlePositionShaderCodeBuilder.getVertexShader(); 38 | 39 | let newProgram = util.createProgram(gl, vertex, fragment); 40 | 41 | if (updateProgram) updateProgram.unload(); 42 | updateProgram = newProgram; 43 | 44 | if (ctx.colorMode === ColorMode.VELOCITY) readVelocity.requestSpeedUpdate(); 45 | } 46 | 47 | function updateParticlesCount(x, y) { 48 | particleStateResolution = ctx.particleStateResolution; 49 | 50 | var dimensions = [{ 51 | name: 'x', 52 | particleState: x 53 | }, { 54 | name: 'y', 55 | particleState: y 56 | }]; 57 | 58 | if (readTextures) readTextures.dispose(); 59 | readTextures = textureCollection(gl, dimensions, particleStateResolution); 60 | 61 | if (writeTextures) writeTextures.dispose(); 62 | writeTextures = textureCollection(gl, dimensions, particleStateResolution); 63 | 64 | readVelocity.updateParticlesCount(); 65 | } 66 | 67 | function prepareToDraw(program) { 68 | var colorMode = ctx.colorMode; 69 | if (colorMode === ColorMode.VELOCITY) readVelocity.setColorMinMax(program); 70 | 71 | readTextures.bindTextures(gl, program); 72 | } 73 | 74 | function updateParticlesPositions() { 75 | var program = updateProgram; 76 | gl.useProgram(program.program); 77 | 78 | util.bindAttribute(gl, ctx.quadBuffer, program.a_pos, 2); 79 | 80 | ctx.inputs.updateBindings(program); 81 | 82 | // TODO: Remove this. 83 | if (ctx.audioTexture) { 84 | util.bindTexture(gl, ctx.audioTexture, 5); 85 | gl.uniform1i(program['u_audio'], 5); 86 | } 87 | 88 | readTextures.bindTextures(gl, program); 89 | 90 | gl.uniform1f(program.u_rand_seed, ctx.frameSeed); 91 | gl.uniform1f(program.u_h, ctx.integrationTimeStep); 92 | gl.uniform1f(program.frame, ctx.frame); 93 | var cursor = ctx.cursor; 94 | gl.uniform4f(program.cursor, cursor.clickX, cursor.clickY, cursor.hoverX, cursor.hoverY); 95 | 96 | var bbox = ctx.bbox; 97 | gl.uniform2f(program.u_min, bbox.minX, bbox.minY); 98 | gl.uniform2f(program.u_max, bbox.maxX, bbox.maxY); 99 | 100 | gl.uniform1f(program.u_drop_rate, ctx.dropProbability); 101 | 102 | // Draw each coordinate individually 103 | for(var i = 0; i < writeTextures.length; ++i) { 104 | var writeInfo = writeTextures.get(i); 105 | gl.uniform1i(program.u_out_coordinate, i); 106 | util.bindFramebuffer(gl, ctx.framebuffer, writeInfo.texture); 107 | gl.viewport(0, 0, particleStateResolution, particleStateResolution); 108 | gl.drawArrays(gl.TRIANGLES, 0, 6); 109 | } 110 | 111 | // TODO: I think I need to keep this time-bound, i.e. allocate X ms to 112 | // process particle positions, and move on. So that the rendering thread is not paused for too long 113 | if (ctx.colorMode === ColorMode.VELOCITY) { 114 | readVelocity.updateParticlesPositions(program); 115 | } 116 | 117 | if (pendingVectorLines) { 118 | processVectorLinesRequest(program); 119 | pendingVectorLines = null; 120 | } 121 | 122 | // swap the particle state textures so the new one becomes the current one 123 | var temp = readTextures; 124 | readTextures = writeTextures; 125 | writeTextures = temp; 126 | } 127 | 128 | function putVectorLinesRequestIntoQueue(request) { 129 | pendingVectorLines = request; 130 | } 131 | 132 | function processVectorLinesRequest(program) { 133 | // TODO: Move this out 134 | var dimensions = [{ 135 | name: 'x', 136 | particleState: pendingVectorLines.x 137 | }, { 138 | name: 'y', 139 | particleState: pendingVectorLines.y 140 | }]; 141 | 142 | // We create temporary textures and load requested positions in there 143 | var resolutionOfParticlesInRequest = pendingVectorLines.resolution; 144 | var numParticles = resolutionOfParticlesInRequest * resolutionOfParticlesInRequest; 145 | 146 | var texturesForRead = textureCollection(gl, dimensions, resolutionOfParticlesInRequest); 147 | var texturesForWrite = textureCollection(gl, dimensions, resolutionOfParticlesInRequest); 148 | 149 | texturesForRead.bindTextures(gl, program); 150 | 151 | // Then we request coordinates out from GPU for each dimension 152 | var writeInfo = texturesForWrite.get(0); 153 | gl.uniform1i(program.u_out_coordinate, 6); // v_x 154 | 155 | util.bindFramebuffer(gl, ctx.framebuffer, writeInfo.texture); 156 | gl.viewport(0, 0, resolutionOfParticlesInRequest, resolutionOfParticlesInRequest); 157 | gl.drawArrays(gl.TRIANGLES, 0, 6); 158 | 159 | var velocity_x = new Uint8Array(numParticles * 4); 160 | gl.readPixels(0, 0, resolutionOfParticlesInRequest, resolutionOfParticlesInRequest, gl.RGBA, gl.UNSIGNED_BYTE, velocity_x); 161 | 162 | gl.uniform1i(program.u_out_coordinate, 7); // v_y 163 | writeInfo = texturesForWrite.get(1); 164 | util.bindFramebuffer(gl, ctx.framebuffer, writeInfo.texture); 165 | gl.viewport(0, 0, resolutionOfParticlesInRequest, resolutionOfParticlesInRequest); 166 | gl.drawArrays(gl.TRIANGLES, 0, 6); 167 | 168 | var velocity_y = new Uint8Array(numParticles * 4); 169 | gl.readPixels(0, 0, resolutionOfParticlesInRequest, resolutionOfParticlesInRequest, gl.RGBA, gl.UNSIGNED_BYTE, velocity_y); 170 | 171 | texturesForWrite.dispose(); 172 | texturesForRead.dispose(); 173 | 174 | var xStats = makeStatCounter(); 175 | var yStats = makeStatCounter(); 176 | 177 | var decoded_velocity_x = new Float32Array(numParticles); 178 | var decoded_velocity_y = new Float32Array(numParticles); 179 | for(var i = 0; i < velocity_y.length; i+=4) { 180 | var idx = i/4; 181 | var vx = readFloat(velocity_x, i); 182 | var vy = readFloat(velocity_y, i); 183 | decoded_velocity_x[idx] = vx; 184 | decoded_velocity_y[idx] = vy; 185 | xStats.add(vx); 186 | yStats.add(vy); 187 | } 188 | 189 | var vectorLineInfo = { 190 | xStats, 191 | yStats, 192 | decoded_velocity_x, 193 | decoded_velocity_y, 194 | resolution: resolutionOfParticlesInRequest 195 | }; 196 | 197 | bus.fire('vector-line-ready', vectorLineInfo); 198 | } 199 | } 200 | 201 | function readFloat(buffer, offset) { 202 | return decodeFloatRGBA( 203 | buffer[offset + 0], 204 | buffer[offset + 1], 205 | buffer[offset + 2], 206 | buffer[offset + 3] 207 | ); 208 | } 209 | -------------------------------------------------------------------------------- /src/lib/scene.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is based on https://github.com/mapbox/webgl-wind 3 | * by Vladimir Agafonkin 4 | * 5 | * Released under ISC License, Copyright (c) 2016, Mapbox 6 | * https://github.com/mapbox/webgl-wind/blob/master/LICENSE 7 | * 8 | * Adapted to field maps by Andrei Kashcha 9 | * Copyright (C) 2017 10 | */ 11 | import util from './gl-utils'; 12 | import makePanzoom from 'panzoom'; 13 | import bus from './bus'; 14 | import appState from './appState'; 15 | import wglPanZoom from './wglPanZoom'; 16 | 17 | import createScreenProgram from './programs/screenProgram'; 18 | import createDrawParticlesProgram from './programs/drawParticlesProgram'; 19 | import createCursorUpdater from './utils/cursorUpdater'; 20 | import createVectorFieldEditorState from './editor/vectorFieldState'; 21 | import createInputsModel from './createInputsModel'; 22 | 23 | /** 24 | * Kicks offs the app rendering. Initialized before even vue is loaded. 25 | * 26 | * @param {WebGLRenderingContext} gl 27 | */ 28 | export default function initScene(gl) { 29 | // Canvas size management 30 | var canvasRect = { width: 0, height: 0, top: 0, left: 0 }; 31 | setWidthHeight(gl.canvas.width, gl.canvas.height); 32 | window.addEventListener('resize', onResize, true); 33 | 34 | // Video capturing is available in super advanced mode. You'll need to install 35 | // and start https://github.com/greggman/ffmpegserver.js 36 | // Then type in the console: window.startRecord(); 37 | // This will trigger frame-by-frame recording (it is slow). To stop it, call window.stopRecord(); 38 | bus.on('start-record', startRecord); 39 | bus.on('stop-record', stopRecord); 40 | var currentCapturer = null; 41 | 42 | // TODO: It feels like bounding box management needs to be moved out from here. 43 | // TODO: bbox needs to be a class with width/height properties. 44 | var bbox = appState.getBBox() || {}; 45 | var currentPanZoomTransform = { 46 | scale: 1, 47 | x: 0, 48 | y: 0 49 | }; 50 | 51 | // How many particles do we want? 52 | var particleCount = appState.getParticleCount(); 53 | 54 | gl.disable(gl.DEPTH_TEST); 55 | gl.disable(gl.STENCIL_TEST); 56 | 57 | // Context variable is a way to share rendering state between multiple programs. It has a lot of stuff on it. 58 | // I found that it's the easiest way to work in state-full world of WebGL. 59 | // Until I discover a better way to write WebGL code. 60 | var ctx = { 61 | gl, 62 | bbox, 63 | canvasRect, 64 | 65 | inputs: null, 66 | 67 | framebuffer: gl.createFramebuffer(), 68 | 69 | // This is used only to render full-screen rectangle. Main magic happens inside textures. 70 | quadBuffer: util.createBuffer(gl, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1])), 71 | 72 | colorMode: appState.getColorMode(), 73 | colorFunction: appState.getColorFunction(), 74 | 75 | // This defines texture unit for screen rendering. First few indices are taken by textures 76 | // that compute particles position/color 77 | // TODO: I need to find a better way to manage this. 78 | screenTextureUnit: 3, 79 | 80 | integrationTimeStep: appState.getIntegrationTimeStep(), 81 | 82 | // On each frame the likelihood for a particle to reset its position is this: 83 | dropProbability: appState.getDropProbability(), 84 | 85 | // current frame number. Reset every time when new shader is compiled 86 | frame: 0, 87 | 88 | // Information about mouse cursor. Could be useful to simplify 89 | // exploration 90 | cursor: { 91 | // Where mouse was last time clicked (or tapped) 92 | clickX: 0, clickY: 0, 93 | // where mouse was last time moved. If this is a touch device 94 | // this is the same as clickX, clickY 95 | hoverX: 0, hoverY: 0 96 | }, 97 | 98 | // Texture size to store particles' positions 99 | particleStateResolution: 0, 100 | 101 | // How quickly we should fade previous frame (from 0..1) 102 | fadeOpacity: appState.getFadeout(), 103 | 104 | // Ignore this one for a moment. Yes, the app support web audio API, 105 | // but it's rudimentary, so... shhh! it's a secret. 106 | // Don't shhh on me! 107 | audioTexture: null 108 | }; 109 | 110 | // Frame management 111 | var lastAnimationFrame; 112 | var isPaused = false; 113 | 114 | var inputsModel = createInputsModel(ctx); 115 | 116 | // screen rendering; 117 | var screenProgram = createScreenProgram(ctx); 118 | var drawProgram = createDrawParticlesProgram(ctx); 119 | var cursorUpdater = createCursorUpdater(ctx); 120 | var vectorFieldEditorState = createVectorFieldEditorState(drawProgram); 121 | 122 | // particles 123 | updateParticlesCount(particleCount); 124 | 125 | var api = { 126 | start: nextFrame, 127 | stop, 128 | dispose, 129 | 130 | resetBoundingBox, 131 | moveBoundingBox, 132 | applyBoundingBox, 133 | 134 | setPaused, 135 | 136 | getParticlesCount, 137 | setParticlesCount, 138 | 139 | setFadeOutSpeed, 140 | getFadeOutSpeed, 141 | 142 | setDropProbability, 143 | getDropProbability, 144 | 145 | getIntegrationTimeStep, 146 | setIntegrationTimeStep, 147 | 148 | setColorMode, 149 | getColorMode, 150 | 151 | vectorFieldEditorState, 152 | 153 | inputsModel, 154 | 155 | getCanvasRect() { 156 | // We trust they don't do anything bad with this ... 157 | return canvasRect; 158 | }, 159 | 160 | getBoundingBox() { 161 | // again, we trust. Maybe to much? 162 | return ctx.bbox; 163 | } 164 | } 165 | 166 | var panzoom = initPanzoom(); 167 | restoreBBox(); 168 | 169 | setTimeout(() => { 170 | bus.fire('scene-ready', api); 171 | }) 172 | 173 | return api; 174 | 175 | function moveBoundingBox(changes) { 176 | if (!changes) return; 177 | var parsedBoundingBox = Object.assign({}, ctx.bbox); 178 | 179 | assignIfPossible(changes, 'minX', parsedBoundingBox); 180 | assignIfPossible(changes, 'minY', parsedBoundingBox); 181 | assignIfPossible(changes, 'maxX', parsedBoundingBox); 182 | assignIfPossible(changes, 'maxY', parsedBoundingBox); 183 | 184 | // for Y axis changes we need to preserve aspect ration, which means 185 | // we also need to change X... 186 | if (changes.minY !== undefined || changes.maxY !== undefined) { 187 | // adjust values for X 188 | var heightChange = Math.abs(parsedBoundingBox.minY - parsedBoundingBox.maxY)/Math.abs(ctx.bbox.minY - ctx.bbox.maxY); 189 | var cx = (ctx.bbox.maxX + ctx.bbox.minX)/2; 190 | var prevWidth = (ctx.bbox.maxX - ctx.bbox.minX)/2; 191 | parsedBoundingBox.minX = cx - prevWidth * heightChange; 192 | parsedBoundingBox.maxX = cx + prevWidth * heightChange; 193 | 194 | } 195 | 196 | applyBoundingBox(parsedBoundingBox); 197 | } 198 | 199 | function assignIfPossible(change, key, newBoundingBox) { 200 | var value = Number.parseFloat(change[key]); 201 | if (Number.isFinite(value)) { 202 | newBoundingBox[key] = value; 203 | } 204 | } 205 | 206 | function startRecord(capturer) { 207 | currentCapturer = capturer; 208 | } 209 | 210 | function stopRecord() { 211 | currentCapturer = null; 212 | } 213 | 214 | function setColorMode(x) { 215 | var mode = parseInt(x, 10); 216 | appState.setColorMode(mode); 217 | ctx.colorMode = appState.getColorMode(); 218 | drawProgram.updateColorMode(mode); 219 | } 220 | 221 | function getColorMode() { 222 | return appState.getColorMode(); 223 | } 224 | 225 | function getIntegrationTimeStep() { 226 | return appState.getIntegrationTimeStep(); 227 | } 228 | 229 | function setIntegrationTimeStep(x) { 230 | var f = parseFloat(x); 231 | if (Number.isFinite(f)) { 232 | ctx.integrationTimeStep = f; 233 | appState.setIntegrationTimeStep(f); 234 | bus.fire('integration-timestep-changed', f); 235 | } 236 | } 237 | 238 | function setPaused(shouldPause) { 239 | isPaused = shouldPause; 240 | nextFrame(); 241 | } 242 | 243 | // Main screen fade out configuration 244 | function setFadeOutSpeed(x) { 245 | var f = parseFloat(x); 246 | if (Number.isFinite(f)) { 247 | ctx.fadeOpacity = f; 248 | appState.setFadeout(f); 249 | } 250 | } 251 | 252 | function getFadeOutSpeed() { 253 | return appState.getFadeout(); 254 | } 255 | 256 | // Number of particles configuration 257 | function getParticlesCount() { 258 | return appState.getParticleCount(); 259 | } 260 | 261 | function setParticlesCount(newParticleCount) { 262 | if (!Number.isFinite(newParticleCount)) return; 263 | if (newParticleCount === particleCount) return; 264 | if (newParticleCount < 1) return; 265 | 266 | updateParticlesCount(newParticleCount); 267 | 268 | particleCount = newParticleCount; 269 | appState.setParticleCount(newParticleCount); 270 | } 271 | 272 | // drop probability 273 | function setDropProbability(x) { 274 | var f = parseFloat(x); 275 | if (Number.isFinite(f)) { 276 | // TODO: Do I need to worry about duplication/clamping? 277 | appState.setDropProbability(f); 278 | ctx.dropProbability = f; 279 | } 280 | } 281 | 282 | function getDropProbability() { 283 | return appState.getDropProbability(); 284 | } 285 | 286 | function onResize() { 287 | setWidthHeight(window.innerWidth, window.innerHeight); 288 | 289 | screenProgram.updateScreenTextures(); 290 | 291 | updateBoundingBox(currentPanZoomTransform); 292 | } 293 | 294 | function setWidthHeight(w, h) { 295 | var dx = Math.max(w * 0.02, 30); 296 | var dy = Math.max(h * 0.02, 30); 297 | canvasRect.width = w + 2 * dx; 298 | canvasRect.height = h + 2 * dy; 299 | canvasRect.top = - dy; 300 | canvasRect.left = - dx; 301 | 302 | 303 | let canvas = gl.canvas; 304 | canvas.width = canvasRect.width; 305 | canvas.height = canvasRect.height; 306 | canvas.style.left = (-dx) + 'px'; 307 | canvas.style.top = (-dy) + 'px'; 308 | } 309 | 310 | function dispose() { 311 | stop(); 312 | panzoom.dispose(); 313 | window.removeEventListener('resize', onResize, true); 314 | cursorUpdater.dispose(); 315 | vectorFieldEditorState.dispose(); 316 | } 317 | 318 | function nextFrame() { 319 | if (lastAnimationFrame) return; 320 | 321 | if (isPaused) return; 322 | 323 | lastAnimationFrame = requestAnimationFrame(draw); 324 | } 325 | 326 | function stop() { 327 | cancelAnimationFrame(lastAnimationFrame); 328 | lastAnimationFrame = 0; 329 | } 330 | 331 | function draw() { 332 | lastAnimationFrame = 0; 333 | 334 | drawScreen(); 335 | 336 | if (currentCapturer) currentCapturer.capture(gl.canvas); 337 | 338 | nextFrame(); 339 | } 340 | 341 | function drawScreen() { 342 | screenProgram.fadeOutLastFrame() 343 | drawProgram.drawParticles(); 344 | screenProgram.renderCurrentScreen(); 345 | drawProgram.updateParticlesPositions(); 346 | } 347 | 348 | function updateParticlesCount(numParticles) { 349 | // we create a square texture where each pixel will hold a particle position encoded as RGBA 350 | ctx.particleStateResolution = Math.ceil(Math.sqrt(numParticles)); 351 | drawProgram.updateParticlesCount(); 352 | } 353 | 354 | function initPanzoom() { 355 | let initializedPanzoom = makePanzoom(gl.canvas, { 356 | controller: wglPanZoom(gl.canvas, updateBoundingBox) 357 | }); 358 | 359 | return initializedPanzoom; 360 | } 361 | 362 | function restoreBBox() { 363 | var savedBBox = appState.getBBox(); 364 | var {width, height} = canvasRect; 365 | 366 | let sX = Math.PI * Math.E; 367 | let sY = Math.PI * Math.E; 368 | let tX = 0; 369 | let tY = 0; 370 | if (savedBBox) { 371 | sX = savedBBox.maxX - savedBBox.minX; 372 | sY = savedBBox.maxY - savedBBox.minY; 373 | // TODO: Not sure if this is really the best way to do it. 374 | // var ar = width/height; 375 | tX = width * (savedBBox.minX + savedBBox.maxX)/2; 376 | tY = width * (savedBBox.minY + savedBBox.maxY)/2; 377 | } 378 | 379 | var w2 = sX * width/2; 380 | var h2 = sY * height/2; 381 | panzoom.showRectangle({ 382 | left: -w2 + tX, 383 | top: -h2 - tY, 384 | right: w2 + tX, 385 | bottom: h2 - tY , 386 | }); 387 | } 388 | 389 | function updateBoundingBox(transform) { 390 | screenProgram.boundingBoxUpdated = true; 391 | 392 | currentPanZoomTransform.x = transform.x; 393 | currentPanZoomTransform.y = transform.y; 394 | currentPanZoomTransform.scale = transform.scale; 395 | 396 | var {width, height} = canvasRect; 397 | 398 | var minX = clientX(0); 399 | var minY = clientY(0); 400 | var maxX = clientX(width); 401 | var maxY = clientY(height); 402 | 403 | // we divide by width to keep aspect ratio 404 | // var ar = width/height; 405 | var p = 10000; 406 | bbox.minX = Math.round(p * minX/width)/p; 407 | bbox.minY = Math.round(p * -minY/width)/p; 408 | bbox.maxX = Math.round(p * maxX/width)/p; 409 | bbox.maxY = Math.round(p * -maxY/ width)/p; 410 | 411 | 412 | appState.saveBBox(bbox); 413 | 414 | bus.fire('bbox-change', bbox); 415 | 416 | function clientX(x) { 417 | return (x - transform.x)/transform.scale; 418 | } 419 | 420 | function clientY(y) { 421 | return (y - transform.y)/transform.scale; 422 | } 423 | } 424 | 425 | function resetBoundingBox() { 426 | var w = Math.PI * Math.E * 0.5; 427 | var h = Math.PI * Math.E * 0.5; 428 | 429 | applyBoundingBox({ 430 | minX: -w, 431 | minY: -h, 432 | maxX: w, 433 | maxY: h 434 | }) 435 | } 436 | 437 | function applyBoundingBox(boundingBox) { 438 | appState.saveBBox(boundingBox); 439 | restoreBBox(); 440 | // a hack to trigger panzoom event 441 | panzoom.moveBy(0, 0, false); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /src/lib/shaderGraph/BaseShaderNode.js: -------------------------------------------------------------------------------- 1 | export default class BaseShaderNode { 2 | constructor() { } 3 | getDefines() { return ''; } 4 | getFunctions() { return ''; } 5 | getMainBody() { return ''; } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/shaderGraph/DrawParticleGraph.js: -------------------------------------------------------------------------------- 1 | import decodeFloatRGBA from './parts/decodeFloatRGBA'; 2 | import shaderBasedColor from './shaderBasedColor'; 3 | 4 | // TODO: this duplicates code from texture position. 5 | export default class DrawParticleGraph { 6 | constructor(ctx) { 7 | this.colorMode = ctx.colorMode; 8 | this.colorFunction = ctx.colorFunction || ''; 9 | } 10 | 11 | getFragmentShader() { 12 | return `precision highp float; 13 | varying vec4 v_particle_color; 14 | void main() { 15 | gl_FragColor = v_particle_color; 16 | }` 17 | } 18 | 19 | getVertexShader(vfCode) { 20 | let decodePositions = textureBasedPosition(); 21 | let colorParts = shaderBasedColor(this.colorMode, vfCode, this.colorFunction); 22 | let methods = [] 23 | addMethods(decodePositions, methods); 24 | addMethods(colorParts, methods); 25 | let main = []; 26 | addMain(decodePositions, main); 27 | addMain(colorParts, main); 28 | 29 | return `precision highp float; 30 | attribute float a_index; 31 | uniform float u_particles_res; 32 | uniform vec2 u_min; 33 | uniform vec2 u_max; 34 | 35 | ${decodePositions.getVariables() || ''} 36 | ${colorParts.getVariables()} 37 | 38 | ${decodeFloatRGBA} 39 | 40 | ${methods.join('\n')} 41 | 42 | void main() { 43 | vec2 txPos = vec2( 44 | fract(a_index / u_particles_res), 45 | floor(a_index / u_particles_res) / u_particles_res); 46 | gl_PointSize = 1.0; 47 | 48 | ${main.join('\n')} 49 | 50 | vec2 du = (u_max - u_min); 51 | v_particle_pos = (v_particle_pos - u_min)/du; 52 | gl_Position = vec4(2.0 * v_particle_pos.x - 1.0, (1. - 2. * (v_particle_pos.y)), 0., 1.); 53 | }` 54 | } 55 | } 56 | 57 | function addMethods(producer, array) { 58 | if (producer.getMethods) { 59 | array.push(producer.getMethods()); 60 | } 61 | } 62 | 63 | function addMain(producer, array) { 64 | if (producer.getMain) { 65 | array.push(producer.getMain()); 66 | } 67 | } 68 | 69 | function textureBasedPosition() { 70 | return { 71 | getVariables, 72 | getMain 73 | } 74 | 75 | function getVariables() { 76 | return ` 77 | uniform sampler2D u_particles_x; 78 | uniform sampler2D u_particles_y; 79 | ` 80 | } 81 | 82 | function getMain() { 83 | return ` 84 | vec2 v_particle_pos = vec2( 85 | decodeFloatRGBA(texture2D(u_particles_x, txPos)), 86 | decodeFloatRGBA(texture2D(u_particles_y, txPos)) 87 | ); 88 | ` 89 | } 90 | } -------------------------------------------------------------------------------- /src/lib/shaderGraph/PanzoomTransform.js: -------------------------------------------------------------------------------- 1 | import BaseShaderNode from './BaseShaderNode'; 2 | 3 | export default class PanzoomTransform extends BaseShaderNode { 4 | constructor(config) { 5 | super(); 6 | // decode is used when we move particle read from the texture 7 | // otherwise we write particle to texture and need to reverse transform 8 | this.decode = config && config.decode; 9 | this.srcPosName = (config && config.posName) || 'pos'; 10 | } 11 | 12 | getDefines() { 13 | if (this.decode) { 14 | // TODO: Need to figure out how to not duplicate this. 15 | return ` 16 | uniform vec2 u_min; 17 | uniform vec2 u_max; 18 | `; 19 | } 20 | } 21 | 22 | getMainBody() { 23 | if (this.decode) { 24 | return ` 25 | // move particle position according to current transform 26 | vec2 du = (u_max - u_min); 27 | pos.x = ${this.srcPosName}.x * du.x + u_min.x; 28 | pos.y = ${this.srcPosName}.y * du.y + u_min.y; 29 | ` 30 | } 31 | return ` 32 | pos.x = (${this.srcPosName}.x - u_min.x)/du.x; 33 | pos.y = (${this.srcPosName}.y - u_min.y)/du.y; 34 | ` 35 | } 36 | } -------------------------------------------------------------------------------- /src/lib/shaderGraph/RungeKuttaIntegrator.js: -------------------------------------------------------------------------------- 1 | import BaseShaderNode from './BaseShaderNode'; 2 | 3 | export default class RungeKuttaIntegrator extends BaseShaderNode { 4 | constructor () { 5 | super(); 6 | } 7 | 8 | getDefines() { 9 | return ` 10 | uniform float u_h; 11 | ` 12 | } 13 | 14 | getFunctions() { 15 | return ` 16 | vec2 rk4(const vec2 point) { 17 | vec2 k1 = get_velocity( point ); 18 | vec2 k2 = get_velocity( point + k1 * u_h * 0.5); 19 | vec2 k3 = get_velocity( point + k2 * u_h * 0.5); 20 | vec2 k4 = get_velocity( point + k3 * u_h); 21 | 22 | return k1 * u_h / 6. + k2 * u_h/3. + k3 * u_h/3. + k4 * u_h/6.; 23 | }` 24 | } 25 | 26 | getMainBody() { 27 | return ` 28 | vec2 velocity = rk4(pos); 29 | ` 30 | } 31 | } -------------------------------------------------------------------------------- /src/lib/shaderGraph/TexturePositionNode.js: -------------------------------------------------------------------------------- 1 | import BaseShaderNode from './BaseShaderNode'; 2 | import encodeFloatRGBA from './parts/encodeFloatRGBA'; 3 | import decodeFloatRGBA from './parts/decodeFloatRGBA'; 4 | 5 | /** 6 | * Reads/writes particle coordinates from/to a texture; 7 | */ 8 | export default class TexturePosition extends BaseShaderNode { 9 | constructor(isDecode) { 10 | super(); 11 | 12 | // When it's decoding, it must read from the texture. 13 | // Otherwise it must write to the texture; 14 | this.isDecode = isDecode; 15 | } 16 | 17 | getFunctions() { 18 | if (this.isDecode) { 19 | return ` 20 | ${encodeFloatRGBA} 21 | ${decodeFloatRGBA} 22 | ` 23 | } 24 | } 25 | 26 | getDefines() { 27 | if (this.isDecode) { 28 | // TODO: How to avoid duplication and silly checks? 29 | return ` 30 | precision highp float; 31 | 32 | uniform sampler2D u_particles_x; 33 | uniform sampler2D u_particles_y; 34 | 35 | // Which coordinate needs to be printed onto the texture 36 | uniform int u_out_coordinate; 37 | 38 | varying vec2 v_tex_pos; 39 | `; 40 | } 41 | } 42 | 43 | getMainBody() { 44 | if (this.isDecode) { 45 | return ` 46 | vec2 pos = vec2( 47 | decodeFloatRGBA(texture2D(u_particles_x, v_tex_pos)), 48 | decodeFloatRGBA(texture2D(u_particles_y, v_tex_pos)) 49 | ); 50 | ` 51 | } 52 | return ` 53 | if (u_out_coordinate == 0) gl_FragColor = encodeFloatRGBA(newPos.x); 54 | else if (u_out_coordinate == 1) gl_FragColor = encodeFloatRGBA(newPos.y); 55 | else if (u_out_coordinate == 6) gl_FragColor = encodeFloatRGBA(get_velocity(pos).x); 56 | else if (u_out_coordinate == 7) gl_FragColor = encodeFloatRGBA(get_velocity(pos).y); 57 | ` 58 | } 59 | } -------------------------------------------------------------------------------- /src/lib/shaderGraph/UserDefinedVelocityFunction.js: -------------------------------------------------------------------------------- 1 | import BaseShaderNode from './BaseShaderNode'; 2 | import snoise from './parts/simplex-noise'; 3 | import {getInputUniforms} from './customInput'; 4 | 5 | export default class UserDefinedVelocityFunction extends BaseShaderNode { 6 | constructor(updateCode) { 7 | super(); 8 | this.updateCode = updateCode || ''; 9 | } 10 | 11 | setNewUpdateCode(newUpdateCode) { 12 | this.updateCode = newUpdateCode; 13 | } 14 | 15 | getDefines() { 16 | return ` 17 | uniform float frame; 18 | uniform vec4 cursor; 19 | // TODO: use inputN instead. 20 | uniform sampler2D u_audio; 21 | 22 | #define PI 3.1415926535897932384626433832795 23 | 24 | ${getInputUniforms()} 25 | ` 26 | } 27 | 28 | getFunctions() { 29 | // TODO: Do I need to worry about "glsl injection" (i.e. is there potential for security attack?) 30 | // TODO: Do I need to inject snoise only when it's used? 31 | return ` 32 | // pseudo-random generator 33 | const vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453); 34 | float rand(const vec2 co) { 35 | float t = dot(rand_constants.xy, co); 36 | return fract(sin(t) * (rand_constants.z + t)); 37 | } 38 | 39 | ${snoise} 40 | 41 | vec2 rotate(vec2 p,float a) { 42 | return cos(a)*p+sin(a)*vec2(p.y,-p.x); 43 | } 44 | 45 | // TODO: This will change. Don't use it. 46 | float audio(float index) { 47 | float rgbI = floor(index/4.); 48 | vec2 txPos = vec2(fract(rgbI / 5.), floor(rgbI / 5.) / 5.); 49 | vec4 rgba = texture2D(u_audio, txPos); 50 | 51 | float offset = mod(index, 4.); 52 | if (offset == 0.) return rgba[0]; 53 | if (offset == 1.) return rgba[1]; 54 | if (offset == 2.) return rgba[2]; 55 | return rgba[3]; 56 | } 57 | 58 | ${this.updateCode ? this.updateCode : 'vec2 get_velocity(vec2 p) { return vec2(0.); }'} 59 | ` 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/shaderGraph/customInput.js: -------------------------------------------------------------------------------- 1 | export function getInputUniforms() { 2 | return `uniform sampler2D input0; 3 | uniform sampler2D input1;`; 4 | } -------------------------------------------------------------------------------- /src/lib/shaderGraph/getVertexShaderCode.js: -------------------------------------------------------------------------------- 1 | export default function getVSCode(nodes) { 2 | // We just generate shader code in multiple passes. 3 | let code = [] 4 | nodes.forEach(node => { 5 | if (node.globals) code.push(node.globals()); 6 | }); 7 | code.push('void main() {') 8 | nodes.forEach(node => { 9 | if (node.mainBody) code.push(node.mainBody()); 10 | }); 11 | code.push('}') 12 | 13 | return code.join('\n'); 14 | } -------------------------------------------------------------------------------- /src/lib/shaderGraph/parts/decodeFloatRGBA.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A shader function to decode rgba encoded color into float position. 3 | */ 4 | const code = ` 5 | highp float decodeFloatRGBA( vec4 v ) { 6 | float a = floor(v.r * 255.0 + 0.5); 7 | float b = floor(v.g * 255.0 + 0.5); 8 | float c = floor(v.b * 255.0 + 0.5); 9 | float d = floor(v.a * 255.0 + 0.5); 10 | 11 | float exponent = a - 127.0; 12 | float sign = 1.0 - mod(d, 2.0)*2.0; 13 | float mantissa = float(a > 0.0) 14 | + b / 256.0 15 | + c / 65536.0 16 | + floor(d / 2.0) / 8388608.0; 17 | return sign * mantissa * exp2(exponent); 18 | } 19 | ` 20 | 21 | export default code; -------------------------------------------------------------------------------- /src/lib/shaderGraph/parts/encodeFloatRGBA.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | vec4 encodeFloatRGBA(highp float val) { 3 | if (val == 0.0) { 4 | return vec4(0.0, 0.0, 0.0, 0.0); 5 | } 6 | 7 | float mag = abs(val); 8 | float exponent = floor(log2(mag)); 9 | // Correct log2 approximation errors. 10 | exponent += float(exp2(exponent) <= mag / 2.0); 11 | exponent -= float(exp2(exponent) > mag); 12 | 13 | float mantissa; 14 | if (exponent > 100.0) { 15 | // Not sure why this needs to be done in two steps for the largest float to work. 16 | // Best guess is the optimizer rewriting '/ exp2(e)' into '* exp2(-e)', 17 | // but exp2(-128.0) is too small to represent. 18 | mantissa = mag / 1024.0 / exp2(exponent - 10.0) - 1.0; 19 | } else { 20 | mantissa = mag / float(exp2(exponent)) - 1.0; 21 | } 22 | 23 | float a = exponent + 127.0; 24 | mantissa *= 256.0; 25 | float b = floor(mantissa); 26 | mantissa -= b; 27 | mantissa *= 256.0; 28 | float c = floor(mantissa); 29 | mantissa -= c; 30 | mantissa *= 128.0; 31 | float d = floor(mantissa) * 2.0 + float(val < 0.0); 32 | return vec4(a, b, c, d) / 255.0; 33 | } 34 | ` 35 | 36 | export default code; -------------------------------------------------------------------------------- /src/lib/shaderGraph/parts/simplex-noise.js: -------------------------------------------------------------------------------- 1 | // 2 | // Description : Array and textureless GLSL 2D simplex noise function. 3 | // Author : Ian McEwan, Ashima Arts. 4 | // Maintainer : stegu 5 | // Lastmod : 20110822 (ijm) 6 | // License : Copyright (C) 2011 Ashima Arts. All rights reserved. 7 | // Distributed under the MIT License. See LICENSE file. 8 | // https://github.com/ashima/webgl-noise 9 | // https://github.com/stegu/webgl-noise 10 | // 11 | 12 | var code = ` 13 | vec3 mod289(vec3 x) { 14 | return x - floor(x * (1.0 / 289.0)) * 289.0; 15 | } 16 | 17 | vec2 mod289(vec2 x) { 18 | return x - floor(x * (1.0 / 289.0)) * 289.0; 19 | } 20 | 21 | vec3 permute(vec3 x) { 22 | return mod289(((x*34.0)+1.0)*x); 23 | } 24 | 25 | float snoise(vec2 v) 26 | { 27 | const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0 28 | 0.366025403784439, // 0.5*(sqrt(3.0)-1.0) 29 | -0.577350269189626, // -1.0 + 2.0 * C.x 30 | 0.024390243902439); // 1.0 / 41.0 31 | // First corner 32 | vec2 i = floor(v + dot(v, C.yy) ); 33 | vec2 x0 = v - i + dot(i, C.xx); 34 | 35 | // Other corners 36 | vec2 i1; 37 | //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0 38 | //i1.y = 1.0 - i1.x; 39 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 40 | // x0 = x0 - 0.0 + 0.0 * C.xx ; 41 | // x1 = x0 - i1 + 1.0 * C.xx ; 42 | // x2 = x0 - 1.0 + 2.0 * C.xx ; 43 | vec4 x12 = x0.xyxy + C.xxzz; 44 | x12.xy -= i1; 45 | 46 | // Permutations 47 | i = mod289(i); // Avoid truncation effects in permutation 48 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) 49 | + i.x + vec3(0.0, i1.x, 1.0 )); 50 | 51 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); 52 | m = m*m ; 53 | m = m*m ; 54 | 55 | // Gradients: 41 points uniformly over a line, mapped onto a diamond. 56 | // The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287) 57 | 58 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 59 | vec3 h = abs(x) - 0.5; 60 | vec3 ox = floor(x + 0.5); 61 | vec3 a0 = x - ox; 62 | 63 | // Normalise gradients implicitly by scaling m 64 | // Approximation of: m *= inversesqrt( a0*a0 + h*h ); 65 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); 66 | 67 | // Compute final noise value at P 68 | vec3 g; 69 | g.x = a0.x * x0.x + h.x * x0.y; 70 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 71 | return 130.0 * dot(m, g); 72 | } 73 | ` 74 | 75 | export default code; -------------------------------------------------------------------------------- /src/lib/shaderGraph/renderNodes.js: -------------------------------------------------------------------------------- 1 | 2 | export default function renderNodes(nodes) { 3 | let code = [] 4 | 5 | nodes.forEach(node => { if (node.getDefines) addToCode(node.getDefines()); }); 6 | nodes.forEach(node => { if (node.getFunctions) addToCode(node.getFunctions()); }); 7 | 8 | addToCode('void main() {') 9 | nodes.forEach(node => { if (node.getMainBody) addToCode(node.getMainBody()); }); 10 | addToCode('}') 11 | return code.join('\n'); 12 | 13 | function addToCode(line) { 14 | if (line) code.push(line) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/shaderGraph/shaderBasedColor.js: -------------------------------------------------------------------------------- 1 | import UserDefinedVelocityFunction from './UserDefinedVelocityFunction'; 2 | import RungeKuttaIntegrator from './RungeKuttaIntegrator'; 3 | import ColorModes from '../programs/colorModes'; 4 | 5 | export default function shaderBasedColor(colorMode, vfCode, colorCode) { 6 | var udf = new UserDefinedVelocityFunction(vfCode); 7 | var integrate = new RungeKuttaIntegrator(); 8 | 9 | return { 10 | getVariables, 11 | getMain, 12 | getMethods 13 | } 14 | 15 | function getVariables() { 16 | return ` 17 | uniform vec2 u_velocity_range; 18 | varying vec4 v_particle_color; 19 | 20 | ${udf.getDefines()} 21 | ${integrate.getDefines()} 22 | ` 23 | } 24 | 25 | function getMethods() { 26 | return ` 27 | // https://github.com/hughsk/glsl-hsv2rgb 28 | vec3 hsv2rgb(vec3 c) { 29 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 30 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 31 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 32 | } 33 | 34 | ${udf.getFunctions()} 35 | ${integrate.getFunctions()} 36 | ${getColorFunctionBody()} 37 | ` 38 | } 39 | 40 | function getColorFunctionBody() { 41 | if (colorMode === ColorModes.UNIFORM) { 42 | return ` 43 | vec4 get_color(vec2 p) { 44 | return vec4(0.302, 0.737, 0.788, 1.); 45 | } 46 | ` 47 | } 48 | if (colorMode === ColorModes.VELOCITY) { 49 | return ` 50 | vec4 get_color(vec2 p) { 51 | vec2 velocity = get_velocity(p); 52 | float speed = (length(velocity) - u_velocity_range[0])/(u_velocity_range[1] - u_velocity_range[0]); 53 | return vec4(hsv2rgb(vec3(0.05 + (1. - speed) * 0.5, 0.9, 1.)), 1.0); 54 | } 55 | ` 56 | } 57 | 58 | if (colorMode === ColorModes.CUSTOM) { 59 | if (!colorCode) throw new Error('color mode is set to custom, but no color function is specified'); 60 | 61 | return colorCode; 62 | } 63 | 64 | return ` 65 | vec4 get_color(vec2 p) { 66 | vec2 velocity = get_velocity(p); 67 | float speed = (atan(velocity.y, velocity.x) + PI)/(2.0 * PI); 68 | return vec4(hsv2rgb(vec3(speed, 0.9, 1.)), 1.0); 69 | } 70 | `; 71 | } 72 | 73 | function getMain() { 74 | return ` v_particle_color = get_color(v_particle_pos);` 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/shaderGraph/updatePositionGraph.js: -------------------------------------------------------------------------------- 1 | import BaseShaderNode from './BaseShaderNode'; 2 | import TexturePositionNode from './TexturePositionNode'; 3 | import renderNodes from './renderNodes'; 4 | import UserDefinedVelocityFunction from './UserDefinedVelocityFunction'; 5 | import PanzoomTransform from './PanzoomTransform'; 6 | import RungeKuttaIntegrator from './RungeKuttaIntegrator'; 7 | 8 | export default class UpdatePositionGraph { 9 | constructor(options) { 10 | this.readStoredPosition = new TexturePositionNode(/* isDecode = */ true); 11 | this.udfVelocity = new UserDefinedVelocityFunction(); 12 | this.integratePositions = new RungeKuttaIntegrator(); 13 | this.dropParticles = new RandomParticleDropper(); 14 | this.writeComputedPosition = new TexturePositionNode(/* isDecode = */ false); 15 | this.panZoomDecode = new PanzoomTransform({decode: true}); 16 | this.panZoomEncode = new PanzoomTransform({decode: false}); 17 | 18 | this.colorMode = options && options.colorMode; 19 | } 20 | 21 | setCustomVectorField(velocityCode) { 22 | this.udfVelocity.setNewUpdateCode(velocityCode); 23 | } 24 | 25 | getVertexShader () { 26 | return `precision highp float; 27 | 28 | attribute vec2 a_pos; 29 | varying vec2 v_tex_pos; 30 | uniform vec2 u_min; 31 | uniform vec2 u_max; 32 | 33 | void main() { 34 | v_tex_pos = a_pos; 35 | gl_Position = vec4(1.0 - 2.0 * a_pos, 0, 1); 36 | }` 37 | } 38 | 39 | getFragmentShader() { 40 | var nodes = [ 41 | this.readStoredPosition, 42 | this.dropParticles, 43 | this.udfVelocity, 44 | this.integratePositions, { 45 | getMainBody() { 46 | return ` 47 | vec2 newPos = pos + velocity; 48 | ` 49 | } 50 | }, 51 | this.writeComputedPosition 52 | ]; 53 | return renderNodes(nodes); 54 | } 55 | } 56 | 57 | class RandomParticleDropper extends BaseShaderNode { 58 | getDefines() { 59 | return ` 60 | uniform float u_drop_rate; 61 | uniform float u_rand_seed; 62 | uniform vec2 u_min; 63 | uniform vec2 u_max; 64 | ` 65 | } 66 | 67 | getFunctions() { 68 | // TODO: Ideally this node should probably depend on 69 | // random number generator node, so that we don't duplicate code 70 | return ` 71 | ` 72 | } 73 | 74 | getMainBody() { 75 | return ` 76 | // a random seed to use for the particle drop 77 | vec2 seed = (pos + v_tex_pos) * u_rand_seed; 78 | // drop rate is a chance a particle will restart at random position, to avoid degeneration 79 | float drop = step(1.0 - u_drop_rate, rand(seed)); 80 | 81 | // TODO: This can be customized to produce various emitters 82 | // random_pos is in range from 0..1, we move it to the bounding box: 83 | vec2 random_pos = vec2(rand(seed + 1.9), rand(seed + 8.4)) * (u_max - u_min) + u_min; 84 | pos = mix(pos, random_pos, drop); 85 | `; 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/lib/sound/audioSource.js: -------------------------------------------------------------------------------- 1 | import bus from '../bus'; 2 | window.frequenciesCount = 0; 3 | 4 | var bufferLength = 2200320; 5 | window.audioBuffer = new Uint8Array(bufferLength); 6 | 7 | function SoundCloudAudioSource(player) { 8 | var self = this; 9 | var analyser; 10 | var audioCtx = new (window.AudioContext || window.webkitAudioContext); 11 | analyser = audioCtx.createAnalyser(); 12 | analyser.fftSize = 256; 13 | 14 | var source = audioCtx.createMediaElementSource(player); 15 | source.connect(analyser); 16 | analyser.connect(audioCtx.destination); 17 | var lastAnimationFrame = 0; 18 | 19 | function sampleAudioStream() { 20 | analyser.getByteFrequencyData(self.streamData); 21 | window.frequenciesCount += analyser.frequencyBinCount; 22 | 23 | // // Calculate an overall volume value 24 | var total = 0; 25 | for (var i = 0; i < 64; i++) { // Get the volume from the first 64 bins 26 | total += self.streamData[i]; 27 | } 28 | self.volume = total; 29 | 30 | var totalLow = 0; 31 | for (var i = 0; i < 31; i++) { // Get the volume from the first 32 bins 32 | totalLow += self.streamData[i]; 33 | } 34 | self.volumeLow = totalLow; 35 | 36 | var totalHi = 0; 37 | for (var i = 31; i < 64; i++) { // Get the volume from the second 32 bins 38 | totalHi += self.streamData[i]; 39 | } 40 | self.volumeHi = totalHi; 41 | 42 | // self.streamData[256 - 1] = self.volume/64; 43 | // self.streamData[256 - 2] = self.volumeLow/32; 44 | // self.streamData[256 - 3] = self.volumeHi/32; 45 | bus.fire('audio', self.streamData); 46 | lastAnimationFrame = requestAnimationFrame(sampleAudioStream); 47 | } 48 | 49 | 50 | // Public properties and methods 51 | this.volume = 0; 52 | this.volumeLow = 0; 53 | this.volumeHi = 0; 54 | this.streamData = new Uint8Array(analyser.frequencyBinCount); 55 | 56 | this.playStream = function(streamUrl) { 57 | onPlayerEnded(); 58 | player.crossOrigin = 'anonymous'; 59 | player.setAttribute('src', streamUrl); 60 | player.play(); 61 | 62 | player.removeEventListener('ended', onPlayerEnded) 63 | player.addEventListener('ended', onPlayerEnded) 64 | 65 | player.removeEventListener('play', onPlayerStarted) 66 | player.addEventListener('play', onPlayerStarted) 67 | 68 | player.removeEventListener('pause', onPlayerEnded) 69 | player.addEventListener('pause', onPlayerEnded) 70 | } 71 | 72 | function onPlayerStarted() { 73 | console.log('started') 74 | lastAnimationFrame = requestAnimationFrame(sampleAudioStream); 75 | } 76 | 77 | function onPlayerEnded() { 78 | console.log('stop') 79 | // 2200320 80 | cancelAnimationFrame(lastAnimationFrame); 81 | lastAnimationFrame = false; 82 | } 83 | }; 84 | 85 | export default SoundCloudAudioSource; -------------------------------------------------------------------------------- /src/lib/sound/soundLoader.js: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/michaelbromley/soundcloud-visualizer 2 | // Copyright (c) 2013 Michael Bromley 3 | 4 | /* global SC */ 5 | function SoundcloudLoader(player) { 6 | var self = this; 7 | var client_id = "oyOHfaO0Xhi6nqwntte71KmwsEQbCmCG"; // to get an ID go to https://developers.soundcloud.com/ 8 | this.sound = {}; 9 | this.streamUrl = ""; 10 | this.errorMessage = ""; 11 | this.player = player; 12 | 13 | /** 14 | * Loads the JSON stream data object from the URL of the track (as given in the location bar of the browser when browsing Soundcloud), 15 | * and on success it calls the callback passed to it (for example, used to then send the stream_url to the audiosource object). 16 | * @param track_url 17 | * @param callback 18 | */ 19 | this.loadStream = loadStream; 20 | 21 | function loadStream(track_url) { 22 | if (typeof SC === 'undefined') { 23 | return new Promise((resolve, reject) => { 24 | var scAPI = document.createElement('script'); 25 | scAPI.setAttribute('src', '//connect.soundcloud.com/sdk.js'); 26 | scAPI.onload = resolve; 27 | scAPI.onerror = reject; 28 | document.head.appendChild(scAPI); 29 | }).then(() => self.loadStream(track_url)); 30 | } 31 | 32 | return new Promise((successCallback, errorCallback) => { 33 | SC.initialize({ 34 | client_id: client_id 35 | }); 36 | SC.get('/resolve', { url: track_url }, function(sound) { 37 | if (sound.errors) { 38 | self.errorMessage = ""; 39 | for (var i = 0; i < sound.errors.length; i++) { 40 | self.errorMessage += sound.errors[i].error_message + '
'; 41 | } 42 | self.errorMessage += 'Make sure the URL has the correct format: https://soundcloud.com/user/title-of-the-track'; 43 | errorCallback(self.errorMessage); 44 | } else { 45 | 46 | if(sound.kind=="playlist"){ 47 | self.sound = sound; 48 | self.streamPlaylistIndex = 0; 49 | self.streamUrl = function(){ 50 | return sound.tracks[self.streamPlaylistIndex].stream_url + '?client_id=' + client_id; 51 | } 52 | successCallback(self); 53 | }else{ 54 | self.sound = sound; 55 | self.streamUrl = function(){ return sound.stream_url + '?client_id=' + client_id; }; 56 | successCallback(self); 57 | } 58 | } 59 | }); 60 | }); 61 | } 62 | 63 | 64 | this.directStream = function(direction){ 65 | if(direction=='toggle'){ 66 | if (this.player.paused) { 67 | this.player.play(); 68 | } else { 69 | this.player.pause(); 70 | } 71 | } 72 | else if(this.sound.kind=="playlist"){ 73 | if(direction=='coasting') { 74 | this.streamPlaylistIndex++; 75 | }else if(direction=='forward') { 76 | if(this.streamPlaylistIndex>=this.sound.track_count-1) this.streamPlaylistIndex = 0; 77 | else this.streamPlaylistIndex++; 78 | }else{ 79 | if(this.streamPlaylistIndex<=0) this.streamPlaylistIndex = this.sound.track_count-1; 80 | else this.streamPlaylistIndex--; 81 | } 82 | if(this.streamPlaylistIndex>=0 && this.streamPlaylistIndex<=this.sound.track_count-1) { 83 | this.player.setAttribute('src', this.streamUrl()); 84 | this.player.play(); 85 | } 86 | } 87 | } 88 | 89 | 90 | }; 91 | 92 | export default SoundcloudLoader; -------------------------------------------------------------------------------- /src/lib/utils/cursorUpdater.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module allows to pass mouse coordinates down to the shader. Coordinates 3 | * will be available as `vec4 cursor` variable, where `xy` are the last 4 | * click position, and `zw` are the last hover position. 5 | * 6 | * Note: On Touch devices hover is the same click. 7 | * 8 | * Hopefully this will enables easier exploration 9 | */ 10 | export default function createCursorUpdater(ctx) { 11 | var {canvasRect, bbox} = ctx; 12 | 13 | window.addEventListener('mousemove', onMouseMove, true); 14 | window.addEventListener('mousedown', onMouseClick, true); 15 | window.addEventListener('touchstart', onTouchStart, true); 16 | window.addEventListener('touchmove', onTouchMove, true); 17 | 18 | return { 19 | dispose 20 | } 21 | 22 | function dispose() { 23 | window.removeEventListener('mousemove', onMouseMove, true); 24 | window.removeEventListener('mousedown', onMouseClick, true); 25 | window.removeEventListener('touchstart', onTouchStart, true); 26 | window.removeEventListener('touchmove', onTouchMove, true); 27 | } 28 | 29 | function onTouchStart(e) { 30 | var firstTouch = e.touches[0]; 31 | if (!firstTouch) return; 32 | 33 | setClick(firstTouch.clientX, firstTouch.clientY); 34 | setHover(firstTouch.clientX, firstTouch.clientY); 35 | } 36 | 37 | function onTouchMove(e) { 38 | var firstTouch = e.touches[0]; 39 | if (!firstTouch) return; 40 | setHover(firstTouch.clientX, firstTouch.clientY); 41 | } 42 | 43 | function onMouseMove(e) { setHover(e.clientX, e.clientY); } 44 | 45 | function onMouseClick(e) { setClick(e.clientX, e.clientY); } 46 | 47 | function setHover(clientX, clientY) { 48 | ctx.cursor.hoverX = getSceneXFromClientX(clientX); 49 | ctx.cursor.hoverY = getSceneYFromClientY(clientY); 50 | } 51 | 52 | function setClick(clientX, clientY) { 53 | ctx.cursor.clickX = getSceneXFromClientX(clientX); 54 | ctx.cursor.clickY = getSceneYFromClientY(clientY); 55 | } 56 | 57 | function getSceneXFromClientX(clientX) { 58 | var dx = (clientX - canvasRect.left)/canvasRect.width; 59 | return (bbox.maxX - bbox.minX) * dx + bbox.minX; 60 | } 61 | 62 | function getSceneYFromClientY(clientY) { 63 | var dy = 1. - ((clientY - canvasRect.top)/canvasRect.height); 64 | return (bbox.minY - bbox.maxY) * dy + bbox.maxY; 65 | } 66 | } -------------------------------------------------------------------------------- /src/lib/utils/drag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a simple dragging listener. It supports touch devices, and works 3 | * even when iframes are present on the page 4 | */ 5 | const preventTextSelection = createTextSelectionInterceptor(); 6 | 7 | /** 8 | * @param {DOMElement} owner that triggers dragging behavior 9 | * @param {Function(dx, dy)} onDrag called when user drags an element. It receives 10 | * related offsets dx, dy - by how far the element was moved compared to last time. 11 | */ 12 | export default function createDrag(owner, onDrag) { 13 | let overlay; 14 | let mouseX; 15 | let mouseY; 16 | let touchInProgress = false; 17 | 18 | if (owner) listenForEvents(); 19 | 20 | return { 21 | dispose 22 | }; 23 | 24 | function dispose() { 25 | if (!owner) return; 26 | 27 | releaseDocumentMouse(); 28 | releaseTouches(); 29 | 30 | owner.removeEventListener('mousedown', onMouseDown); 31 | owner.removeEventListener('touchstart', onTouch); 32 | } 33 | 34 | function listenForEvents() { 35 | owner.addEventListener('mousedown', onMouseDown); 36 | owner.addEventListener('touchstart', onTouch); 37 | } 38 | 39 | function injectWindowOverlay() { 40 | // so that mouse events are exclusively sent to us (even if we hover over iframes) 41 | if (!overlay) { 42 | overlay = document.createElement('div'); 43 | overlay.classList.add('drag-overlay'); 44 | overlay.style.left = '0'; 45 | overlay.style.top = '0'; 46 | overlay.style.right = '0'; 47 | overlay.style.bottom = '0'; 48 | overlay.style.position = 'fixed'; 49 | overlay.style.position = 'fixed'; 50 | overlay.style.zIndex = '42'; // not sure if this will help us win z-index war. 51 | } 52 | document.body.appendChild(overlay); 53 | } 54 | 55 | function removeOverlay() { 56 | if (overlay) { 57 | document.body.removeChild(overlay); 58 | } 59 | } 60 | 61 | function onMouseDown(e) { 62 | if (e.target.classList.contains('no-drag')) return; 63 | if (touchInProgress) { 64 | // modern browsers will fire mousedown for touch events too 65 | // we do not want this: touch is handled separately. 66 | e.stopPropagation(); 67 | return false; 68 | } 69 | 70 | // for IE, left click == 1 71 | // for Firefox, left click == 0 72 | const isLeftButton = ((e.button === 1 && window.event !== null) || e.button === 0); 73 | if (!isLeftButton) return; 74 | 75 | injectWindowOverlay(); 76 | 77 | mouseX = e.clientX; 78 | mouseY = e.clientY; 79 | 80 | // We need to listen on document itself, since mouse can go outside of the 81 | // window, and we will loose it 82 | document.addEventListener('mousemove', onMouseMove); 83 | document.addEventListener('mouseup', onMouseUp); 84 | 85 | // preventTextSelection.capture(e.target || e.srcElement); 86 | 87 | return false; 88 | } 89 | 90 | function onMouseMove(e) { 91 | // no need to worry about mouse events when touch is happening 92 | if (touchInProgress) return; 93 | 94 | triggerPanStart(); 95 | 96 | const dx = e.clientX - mouseX; 97 | const dy = e.clientY - mouseY; 98 | 99 | mouseX = e.clientX; 100 | mouseY = e.clientY; 101 | 102 | onDrag(dx, dy); 103 | } 104 | 105 | function onMouseUp() { 106 | preventTextSelection.release(); 107 | triggerPanEnd(); 108 | releaseDocumentMouse(); 109 | } 110 | 111 | function releaseDocumentMouse() { 112 | document.removeEventListener('mousemove', onMouseMove); 113 | document.removeEventListener('mouseup', onMouseUp); 114 | removeOverlay(); 115 | } 116 | 117 | function onTouch(e) { 118 | if (e.touches.length === 1) { 119 | return handleSignleFingerTouch(e, e.touches[0]); 120 | } 121 | } 122 | 123 | function handleSignleFingerTouch(e) { 124 | if (e.target.classList.contains('no-drag')) return; 125 | 126 | e.stopPropagation(); 127 | e.preventDefault(); 128 | 129 | const touch = e.touches[0]; 130 | mouseX = touch.clientX; 131 | mouseY = touch.clientY; 132 | 133 | startTouchListenerIfNeeded(); 134 | } 135 | 136 | function startTouchListenerIfNeeded() { 137 | if (!touchInProgress) { 138 | touchInProgress = true; 139 | document.addEventListener('touchmove', handleTouchMove); 140 | document.addEventListener('touchend', handleTouchEnd); 141 | document.addEventListener('touchcancel', handleTouchEnd); 142 | } 143 | } 144 | 145 | function handleTouchEnd(e) { 146 | if (e.touches.length > 0) { 147 | mouseX = e.touches[0].clientX; 148 | mouseY = e.touches[0].clientY; 149 | } else { 150 | touchInProgress = false; 151 | triggerPanEnd(); 152 | releaseTouches(); 153 | } 154 | } 155 | 156 | function releaseTouches() { 157 | document.removeEventListener('touchmove', handleTouchMove); 158 | document.removeEventListener('touchend', handleTouchEnd); 159 | document.removeEventListener('touchcancel', handleTouchEnd); 160 | } 161 | 162 | function handleTouchMove(e) { 163 | if (e.touches.length !== 1) return; 164 | e.stopPropagation(); 165 | const touch = e.touches[0]; 166 | 167 | const dx = touch.clientX - mouseX; 168 | const dy = touch.clientY - mouseY; 169 | 170 | if (dx !== 0 && dy !== 0) { 171 | triggerPanStart(); 172 | } 173 | mouseX = touch.clientX; 174 | mouseY = touch.clientY; 175 | onDrag(dx, dy); 176 | } 177 | 178 | function triggerPanStart() { 179 | } 180 | 181 | function triggerPanEnd() { 182 | } 183 | } 184 | 185 | function createTextSelectionInterceptor() { 186 | let dragObject; 187 | let prevSelectStart; 188 | let prevDragStart; 189 | 190 | return { 191 | capture, 192 | release 193 | }; 194 | 195 | function capture(domObject) { 196 | prevSelectStart = window.document.onselectstart; 197 | prevDragStart = window.document.ondragstart; 198 | 199 | window.document.onselectstart = disabled; 200 | 201 | dragObject = domObject; 202 | dragObject.ondragstart = disabled; 203 | } 204 | 205 | function release() { 206 | window.document.onselectstart = prevSelectStart; 207 | if (dragObject) dragObject.ondragstart = prevDragStart; 208 | } 209 | } 210 | 211 | function disabled(e) { 212 | e.stopPropagation(); 213 | return false; 214 | } -------------------------------------------------------------------------------- /src/lib/utils/floatPacking.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file encodes/decodes float values into 32bit rgba array 3 | * 4 | * It is based on https://computergraphics.stackexchange.com/questions/4151/webgl-packing-unpacking-functions-that-can-roundtrip-all-typical-32-bit-floats 5 | * and it is not perfect. If you know how to improve it - please let me know. 6 | */ 7 | 8 | /** 9 | * Encodes float value into output array 10 | * @param {float} val - value to be encode 11 | * @param {Uint8Array} out - array where encoded value needs to be written. 12 | * @param {Number} writeOffset - offset in the original array where values should be written. 13 | */ 14 | export function encodeFloatRGBA(val, out, writeOffset) { 15 | if (val == 0.0) { 16 | out[writeOffset + 0] = 0; out[writeOffset + 1] = 0; out[writeOffset + 2] = 0; out[writeOffset + 3] = 0; 17 | return; 18 | } 19 | 20 | var mag = Math.abs(val); 21 | var exponent = Math.floor(Math.log2(mag)); 22 | // Correct log2 approximation errors. 23 | exponent += (exp2(exponent) <= mag / 2.0) ? 1 : 0; 24 | exponent -= (exp2(exponent) > mag) ? 1 : 0; 25 | 26 | var mantissa; 27 | if (exponent > 100.0) { 28 | mantissa = mag / 1024.0 / exp2(exponent - 10.0) - 1.0; 29 | } else { 30 | mantissa = mag / (exp2(exponent)) - 1.0; 31 | } 32 | 33 | var a = exponent + 127.0; 34 | mantissa *= 256.0; 35 | var b = Math.floor(mantissa); 36 | mantissa -= b; 37 | mantissa *= 256.0; 38 | var c = Math.floor(mantissa); 39 | mantissa -= c; 40 | mantissa *= 128.0; 41 | var d = Math.floor(mantissa) * 2.0 + ((val < 0.0) ? 1: 0); 42 | 43 | out[writeOffset + 0] = a; out[writeOffset + 1] = b; out[writeOffset + 2] = c; out[writeOffset + 3] = d; 44 | } 45 | 46 | /** 47 | * Given byte values in range [0..255] returns decoded float value. 48 | * 49 | * @param {Byte} r 50 | * @param {Byte} g 51 | * @param {Byte} b 52 | * @param {Byte} a 53 | */ 54 | export function decodeFloatRGBA(r, g, b, a) { 55 | var A = Math.floor(r + 0.5); 56 | var B = Math.floor(g + 0.5); 57 | var C = Math.floor(b + 0.5); 58 | var D = Math.floor(a + 0.5); 59 | 60 | var exponent = A - 127.0; 61 | var sign = 1.0 - (D % 2.0) * 2.0; 62 | var mantissa = ((A > 0.0) ? 1 : 0) 63 | + B / 256.0 64 | + C / 65536.0 65 | + Math.floor(D / 2.0) / 8388608.0; 66 | return sign * mantissa * exp2(exponent); 67 | } 68 | 69 | function exp2(exponent) { return Math.exp(exponent * Math.LN2); } -------------------------------------------------------------------------------- /src/lib/utils/makeStatCounter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple interface to compute eventual min/max 3 | */ 4 | export default function makeStatCounter() { 5 | var min, max; 6 | 7 | var api = { 8 | getMin() { return min; }, 9 | getMax() { return max; }, 10 | add(x) { 11 | if (x < min) min = x; 12 | if (x > max) max = x; 13 | }, 14 | reset: reset 15 | }; 16 | 17 | return api; 18 | 19 | function reset() { 20 | min = Number.POSITIVE_INFINITY; 21 | max = Number.NEGATIVE_INFINITY; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/utils/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * XMLHttpRequest wrapped into a promise. 3 | * 4 | * @param {String} url 5 | */ 6 | export default function request(url, options) { 7 | if (!options) options = {}; 8 | 9 | return new Promise(download); 10 | 11 | function download(resolve, reject) { 12 | var req = new XMLHttpRequest(); 13 | 14 | if (typeof options.progress === 'function') { 15 | req.addEventListener('progress', updateProgress, false); 16 | } 17 | 18 | req.addEventListener('load', transferComplete, false); 19 | req.addEventListener('error', transferFailed, false); 20 | req.addEventListener('abort', transferCanceled, false); 21 | 22 | req.open('GET', url); 23 | if (options.responseType) { 24 | req.responseType = options.responseType; 25 | } 26 | req.send(null); 27 | 28 | function updateProgress(e) { 29 | if (e.lengthComputable) { 30 | options.progress({ 31 | loaded: e.loaded, 32 | total: e.total, 33 | percent: e.loaded / e.total 34 | }); 35 | } 36 | } 37 | 38 | function transferComplete() { 39 | if (req.status !== 200) { 40 | reject(`Unexpected status code ${req.status} when calling ${url}`); 41 | return; 42 | } 43 | var response = req.response; 44 | 45 | if (options.responseType === 'json' && typeof response === 'string') { 46 | // IE 47 | response = JSON.parse(response); 48 | } 49 | 50 | resolve(response); 51 | } 52 | 53 | function transferFailed() { 54 | reject(`Failed to download ${url}`); 55 | } 56 | 57 | function transferCanceled() { 58 | reject(`Cancelled download of ${url}`); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/lib/utils/textureCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A wrapper over collection of textures. Can be used to represent 3 | * individual textures for every dimension. 4 | */ 5 | import glUtil from '../gl-utils'; 6 | 7 | export default function textureCollection(gl, dimensions, particleStateResolution) { 8 | var textures = dimensions.map((d, index) => { 9 | var textureInfo = { 10 | texture: glUtil.createTexture(gl, gl.NEAREST, d.particleState, particleStateResolution, particleStateResolution), 11 | index: index, 12 | name: d.name 13 | } 14 | 15 | return textureInfo; 16 | }) 17 | 18 | return { 19 | dispose, 20 | bindTextures, 21 | assignProgramUniforms, 22 | length: dimensions.length, 23 | textures, 24 | get(i) { return textures[i]; } 25 | } 26 | 27 | function assignProgramUniforms(program) { 28 | textures.forEach(tInfo => { 29 | gl.uniform1i(program['u_particles_' + tInfo.name], tInfo.index); 30 | }); 31 | } 32 | 33 | function dispose() { 34 | textures.forEach(tInfo => gl.deleteTexture(tInfo.texture)); 35 | } 36 | 37 | function bindTextures(gl, program) { 38 | textures.forEach((tInfo) => { 39 | glUtil.bindTexture(gl, tInfo.texture, tInfo.index); 40 | gl.uniform1i(program['u_particles_' + tInfo.name], tInfo.index); 41 | }) 42 | } 43 | } -------------------------------------------------------------------------------- /src/lib/wglPanZoom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enables panning and zooming of canvas with vector field 3 | * 4 | * @param {HTMLCanvasElement} canvas that needs to be panned and zoomed 5 | * @param {*} updateBoundingBoxCallback callback that is called when vector field bounding box 6 | * needs to be updated 7 | */ 8 | export default function wglPanZoom(canvas, updateBoundingBoxCallback) { 9 | var lastDx = 0; 10 | var lastDy = 0; 11 | var lastScale = 1; 12 | 13 | // We need to be moved at least this many pixels in order to let 14 | // transform update bounding box. 15 | var transformThreshold = 2.1; 16 | 17 | return { 18 | applyTransform(newTransform) { 19 | var dx = newTransform.x; 20 | var dy = newTransform.y; 21 | 22 | let dScale = (lastScale - newTransform.scale); 23 | if (dScale === 0 && 24 | Math.abs(dx - lastDx) < transformThreshold && 25 | Math.abs(dy - lastDy) < transformThreshold) { 26 | // Wait for larger transform 27 | return; 28 | } 29 | 30 | lastDx = dx; 31 | lastDy = dy; 32 | lastScale = newTransform.scale; 33 | 34 | updateBoundingBoxCallback(newTransform) 35 | }, 36 | 37 | getOwner() { 38 | return canvas 39 | } 40 | }; 41 | } -------------------------------------------------------------------------------- /src/lib/wrapVectorField.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wraps a simple vector field string into our default shader code. 3 | * @param {String} field 4 | */ 5 | export default function wrapVectorField(field) { 6 | return `// p.x and p.y are current coordinates 7 | // v.x and v.y is a velocity at point p 8 | vec2 get_velocity(vec2 p) { 9 | vec2 v = vec2(0., 0.); 10 | 11 | // change this to get a new vector field 12 | ${field} 13 | 14 | return v; 15 | }` 16 | } -------------------------------------------------------------------------------- /src/vueApp.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | 6 | Vue.config.productionTip = false 7 | 8 | /* eslint-disable no-new */ 9 | new Vue({ 10 | el: '#app', 11 | template: '', 12 | components: { App }, 13 | }) 14 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/fieldplay/0d37a303b3278685528827b7d7199855c9ab3db3/static/.gitkeep -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['Chrome'], 15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/specs/FloatPacking.spec.js: -------------------------------------------------------------------------------- 1 | import {encodeFloatRGBA, decodeFloatRGBA } from '@/lib/utils/floatPacking'; 2 | 3 | describe('Float packing', () => { 4 | it('should pack variables to 1e-6 precision', () => { 5 | var out = new Uint8Array(4); 6 | var input = 0.1; 7 | var from = -Math.PI * Math.E 8 | var to = Math.PI * Math.E 9 | var steps = 1000; 10 | var dt = (to - from)/steps; 11 | var maxError = 0; 12 | for(var f = from; f <= to; f += dt) { 13 | out[0] = out[1] = out[2] = out[3] = 0; 14 | 15 | encodeFloatRGBA(f, out, 0); 16 | var decoded = decodeFloatRGBA(out[0], out[1], out[2], out[3]); 17 | var currentError = Math.abs(decoded - f); 18 | if (currentError > maxError) maxError = currentError; 19 | } 20 | console.log(maxError); 21 | expect(maxError < 1e-6).to.be.true; 22 | }) 23 | }) 24 | --------------------------------------------------------------------------------