├── .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 |
2 |
3 |
4 |
5 |
WebGL is not enabled :(
6 |
This website needs WebGL to perform numerical integration.
7 |
8 | You can try another browser. If problem persists - very likely your video card isn't supported then.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
about...
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
93 |
94 |
168 |
--------------------------------------------------------------------------------
/src/components/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | This website allows you to explore vector fields in real time.
9 |
10 |
11 | "Vector field" is just a fancy way of saying that each point on a screen has some vector
12 | associated with it. This vector could mean anything, but for our purposes we consider it to be
13 | a velocity vector.
14 |
15 |
16 | Now that we have velocity vectors at every single point, let's drop thousands of small particles
17 | and see how they move. Resulting visualization could be used by scientist to study vector
18 | fields, or by artist to get inspiration!
19 |
20 |
21 |
22 | Learn more about this project on GitHub
23 |
24 | Stay tuned for updates on Twitter
25 |
26 | /r/fieldplay - join our small subreddit
27 |
28 |
29 |
30 |
With passion, Anvaka
31 |
32 | close
33 |
34 |
35 |
36 |
37 |
38 |
59 |
116 |
--------------------------------------------------------------------------------
/src/components/CodeEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
{{model.error}}
10 |
{{model.errorDetail}}
11 | Did you forget to add a dot symbol? E.g. 10 should be 10. and 42 should be 42.
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/ColorPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
44 |
57 |
--------------------------------------------------------------------------------
/src/components/Controls.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
64 |
111 |
--------------------------------------------------------------------------------
/src/components/Inputs.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
39 |
40 |
61 |
--------------------------------------------------------------------------------
/src/components/Ruler.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
90 |
174 |
175 |
--------------------------------------------------------------------------------
/src/components/Share.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Share link
7 |
x
8 |
13 |
14 |
15 |
You can also copy the link from your browser's address bar.
16 |
17 |
18 |
19 |
20 |
21 |
22 |
119 |
222 |
223 |
--------------------------------------------------------------------------------
/src/components/VectorView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
--------------------------------------------------------------------------------
/src/components/help/Syntax.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Vector field is defined with GLSL language. This is a high level programming language for the graphic card.
4 |
Two most important things you need to know:
5 |
6 | GLSL is strictly typed. Each number must have a dot. E.g. "42" should be written as "42."
7 | List of available built-in functions is available here
8 |
9 |
10 |
Close help
11 |
12 |
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 |
--------------------------------------------------------------------------------