{"id":13785127,"url":"https://anvaka.github.io/fieldplay/","last_synced_at":"2025-05-11T20:32:06.691Z","repository":{"id":37270492,"uuid":"107498895","full_name":"anvaka/fieldplay","owner":"anvaka","description":"A vector field explorer","archived":false,"fork":false,"pushed_at":"2025-01-23T04:47:12.000Z","size":920,"stargazers_count":1244,"open_issues_count":21,"forks_count":77,"subscribers_count":26,"default_branch":"main","last_synced_at":"2025-05-07T22:02:15.922Z","etag":null,"topics":["glsl","gpu","particles","vector-field","webgl"],"latest_commit_sha":null,"homepage":"https://anvaka.github.io/fieldplay/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anvaka.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-19T04:38:41.000Z","updated_at":"2025-05-05T10:41:04.000Z","dependencies_parsed_at":"2024-01-08T01:44:40.869Z","dependency_job_id":"94e2c19c-5744-49fd-9326-61337aaf1765","html_url":"https://github.com/anvaka/fieldplay","commit_stats":{"total_commits":156,"total_committers":2,"mean_commits":78.0,"dds":"0.10256410256410253","last_synced_commit":"e9515c0d8c3c20bd7c7809c92cce872ea84ded28"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anvaka%2Ffieldplay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anvaka%2Ffieldplay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anvaka%2Ffieldplay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anvaka%2Ffieldplay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anvaka","download_url":"https://codeload.github.com/anvaka/fieldplay/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253632084,"owners_count":21939371,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["glsl","gpu","particles","vector-field","webgl"],"created_at":"2024-08-03T19:00:57.250Z","updated_at":"2025-05-11T20:32:06.081Z","avatar_url":"https://github.com/anvaka.png","language":"JavaScript","funding_links":[],"categories":["Mathematics"],"sub_categories":[],"readme":"# Field Play\n\nVector fields explorer. Made with WebGL, love and passion.\n\n[![field](https://github.com/anvaka/fieldplay/wiki/images/field_1.png)](https://anvaka.github.io/fieldplay/?dt=0.007\u0026fo=0.998\u0026dp=0.009\u0026cm=1\u0026cx=-1.275949999999999\u0026cy=-1.6277\u0026w=30.2937\u0026h=30.2937\u0026code=v.x%20%3D%20length%28p%29*min%28sin%28p.y%29%2Ccos%28p.x%29%29%3B%0Av.y%20%3D%20cos%28%28p.y%2Bp.y%29%29%3B%0A%20%20)\n[![field 2](https://github.com/anvaka/fieldplay/wiki/images/field_2.png)](https://anvaka.github.io/fieldplay/?dt=0.007\u0026fo=0.998\u0026dp=0.009\u0026cm=1\u0026cx=-1.275949999999999\u0026cy=-1.62765\u0026w=30.2937\u0026h=30.2937\u0026code=v.x%20%3D%20cos%28p.y%29%3B%0Av.y%20%3D%20cos%28p.x%29%3B%0A%20%20)\n[![field 3](https://github.com/anvaka/fieldplay/wiki/images/field_3.png)](https://anvaka.github.io/fieldplay/?dt=0.02\u0026fo=0.998\u0026dp=0.009\u0026cm=1\u0026cx=0.21419999999999995\u0026cy=-0.7710999999999997\u0026w=55.970200000000006\u0026h=55.970200000000006\u0026code=v.x%20%3D%20min%28sin%28exp%28p.x%29%29%2Csin%28length%28p%29%29%29%3B%0Av.y%20%3D%20sin%28p.x%29%3B%0A%20%20)\n[![field 4](https://github.com/anvaka/fieldplay/wiki/images/field_4.png)](https://anvaka.github.io/fieldplay/?dt=0.02\u0026fo=0.998\u0026dp=0.009\u0026cm=1\u0026cx=2.43185\u0026cy=-1.1695\u0026w=11.4385\u0026h=11.4385\u0026code=v.x%20%3D%20%28p.y%2Bcos%28p.y%29%29%3B%0Av.y%20%3D%20sin%28min%28length%28p%29%2Clog%28%28p.y%2Bp.x%29%29*p.x%29%29%3B%0A%20%20)\n\n[more examples...](https://github.com/anvaka/fieldplay/blob/main/Awesome%20Fields.md)\n\n## What?\n\nLet's assign to every point on a grid a vector `(1, 0)`. This means\nwe have an arrow, pointing to the right:\n\n![Vector field V(1, 0)](https://github.com/anvaka/fieldplay/wiki/images/field_1_0.png)\n\nLet's pretend these vectors represent velocity. What if we drop a thousand particles onto\nthis grid? How would they move?\n\n![Moving particles in V(1, 0)](https://github.com/anvaka/fieldplay/wiki/images/field_1_0_move.gif)\n\nWhen we assigned a vector to each point on the plain, we created a mathematical structure\ncalled `Vector Field`.\n\nLet's create a bit more interesting vector field:\n\n* Points with even `y` coordinate get vector `(1, 0)`;\n* Points with odd `y` coordinate get an opposite vector `(-1, 0)`;\n\n![Even odd directions](https://github.com/anvaka/fieldplay/wiki/images/field_even_odd.png)\n\nAgain we drop a few thousands particles and see what happens:\n\n![Moving even odd directions](https://github.com/anvaka/fieldplay/wiki/images/field_even_odd_move.gif)\n\nThe field above can be written in a single formula:\n\n```\nv.x = -2.0 * mod(floor(y), 2.0) + 1.0;\nv.y = 0.0;\n```\n\nThe remainder after integer division `y/2` can be either `1` or `0`.\nThen we transform the remainder, so that the final vector is either `(-1, 0)` or `(1, 0)`.\n\nSo far, we've used only one component of the velocity vector `v.x`. And particles\nmoved only horizontally. Let's try to set both components and see what happens\n\n```\nv.x = -2.0 * mod(floor(y), 2.0) + 1.0;\nv.y = -2.0 * mod(floor(x), 2.0) + 1.0;\n```\n\n![Field x, y](https://github.com/anvaka/fieldplay/wiki/images/field_xy.png)\n![Animated field x, y](https://github.com/anvaka/fieldplay/wiki/images/field_xy_small.gif)\n\nWow! Two simple operations, and the final animation looks like an art piece!\n\n![Field x, y](https://github.com/anvaka/fieldplay/wiki/images/field_xy_final.png)\n\nVector fields turns out to be very flexible generative framework.\n\n## How this project works?\n\nThis project is inspired by Vladimir Agafonkin's article: [How I built a wind map with WebGL](https://blog.mapbox.com/how-i-built-a-wind-map-with-webgl-b63022b5537f).\nVladimir shows how to render up to a million particles at 60 frames per second, entirely on GPU.\n\nI used almost the same technique with a few modifications:\n\n1. The vector field is defined in shader language with GLSL code, so that\nmathematical formulas can be expressed in free form\n2. Position of a particle is computed with 4th order [Runge-Kutta method](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods) on GPU\n3. Each dimension X and Y is computed independently, so that we can store positions more accurately\n4. Added pan/zoom with [panzoom](https://github.com/anvaka/panzoom) library\n5. The vector field definition is saved in the URL with [query-state](https://github.com/anvaka/query-state)\nlibrary. So that you can bookmark/share your vector fields easily\n\n## GLSL code for vector field\n\nEvery time when vector field code is changed I compile a new shader:\n\n![shader compilation](https://github.com/anvaka/fieldplay/wiki/images/edit_small.gif)\n\nThe biggest challenge here was... to provide informative error messages when code is wrong.\nWeb browser gives basic info, which can be confusing sometimes.\n\nFor example, the following code has a tiny error:\n\n``` glsl\nvec2 velocity(vec2 p) {\n  return p\n}\n```\n\nIf we compile this shader, the browser will say: `ERROR: 0:3: '}' : syntax error`.\nWhat's wrong? - There is a missing semicolon on line `2`, the correct version is:\n\n``` glsl\nvec2 velocity(vec2 p) {\n  return p;\n}\n```\n\nIt would be better if I could just tell that the semicolon is missing.\n\nLuckily, there is a great glsl type checker and minifier [evanw/glslx](https://github.com/evanw/glslx).\nIt is written in [Skew](http://skew-lang.org/), and provides great many more feature than I needed.\n\nAfter several hours of playing with code, I [extracted parsing bits](https://github.com/evanw/glslx/issues/10)\nspecific to type checking, and got validation (fingers crossed) more intuitive:\n\n![glsl validation](https://github.com/anvaka/fieldplay/wiki/images/glsl_validation.gif)\n\n\n## Page load time\n\nUnfortunately, GLSL parsing came at cost - the library is ~64KB of compressed javascript. Together with\nvue.js (~26KB) users would have to download almost 90KB of code, that is not immediately needed\nto render the vector field.\n\nAt the same time, the code that loads vector field from query string, compiles it, and shows it on the\nscreen is very small. Less than `28KB`.\n\n*For a regular Wi-Fi connection this may seem like not a big deal. But when you try to open the website\non slow 3G mobile network, the difference between extra 90KB of code becomes painfully obvious.*\n\nSo, how can we load the website faster?\n\nI am using [webpack vuejs template](https://github.com/vuejs-templates/webpack), and solution came\nalmost trivial. I just needed to split library into chunks.\n\nThe main website's entry point would be my small WebGL renderer, which initializes the scene and\nkicks of download of vue.js immediately:\n\n``` js\ninitVectorFieldApp(canvas);\n\n// Tell webpack to split bundle, and download settings UI later.\nrequire.ensure('@/vueApp.js', () =\u003e {\n  // Settings UI is ready, initialize vue.js application\n  require('@/vueApp.js');\n});\n```\n\nSimilarly, the GLSL parser is lazy-loaded. By default I create a naive parser that assumes GLSL\ncode is fine. Once the real parser is loaded, the naive parser is replaced:\n\n``` js\nvar glslParser = {\n  check(code) { return no errors }\n};\n\n// Load heavy-weight parser:\nrequire.ensure('glsl-parser', () =\u003e {\n  // ... and replace the naive parser with the real one.\n  glslParser = require('glsl-parser');\n});\n```\n\nYou might be wondering what happens when the parser is being loaded? Well, if there are no errors\nwe immediately show the vector field on the screen. If there are errors, the browser would not\ncompile the shader, and the website visitors will not see informative error message until the\nreal GLSL parser is loaded.\n\nWas the split worth the effort?\n\nI think so. On slow 3G network the vector field is visible in ~3,000ms. On my regular\nWi-Fi network, the first frame with vector field is rendered in less than 500ms:\n\n![split results](https://github.com/anvaka/fieldplay/wiki/images/first_frame.png)\n\n*Screenshot is taken with Chrome's developer tools, disabled cache. Start time is counted from the navigation start*\n\n# Float packing\n\nThe core idea of WebGL based computation is quite simple.\n\nGPU can render images very fast. Each image is a collection of pixels. Each pixels is just a number,\nthat represents color, usually written in 32 bits (RGBA format).\n\nBut who said that these 32 bits per pixel have to represent a color? Why can't we compute some number,\nand store it into 32 bits? This number could be, for example, position of a particle that follows\nalong some velocity vector...\n\nIf we do so, the GPU would still treat these numbers as colors:\n\n![colorful mess](https://github.com/anvaka/fieldplay/wiki/images/number_pixels.png)\n\nLuckily, we don't have to make this seemingly random images visible to the users. WebGL allows\nto render things onto \"virtual\" screens, called `frame buffers`.\n\nThese virtual screens are just images (textures) in the video memory. With two textures we can\ntrick GPU into solving math problems. On each frame the algorithm works like this:\n\n```\n1. Tell GPU, to read data from a \"background\" texture;\n2. Tell GPU, to write data to a \"screen\" texture using frame buffer;\n3. Swap \"background\" with \"screen\";\n```\n\nIn theory this should work nice. In practice there is a problem. WebGL doesn't\nlet you write floating point numbers into textures. So we need to convert a float number into\n`RGBA` format, with 8 bits per channel.\n\nIn his article, Vladimir used the following encoding/decoding schema:\n\n``` glsl\n// decode particle position (x, y) from pixel RGBA color\nvec2 pos = vec2(\n    color.r / 255.0 + color.b,\n    color.g / 255.0 + color.a);\n... // move the position\n// encode the position back into RGBA\ngl_FragColor = vec4(\n    fract(pos * 255.0),\n    floor(pos * 255.0) / 255.0);\n```\n\nHere both `X` and `Y` coordinate of the particle are stored into a single 32bit number.\nI used this approach in the beginning, and it worked well on desktop and on my Android phone.\n\nHowever, when I opened a website on iPhone, unpleasant surprise was waiting for me. Severe\nartifacts appeared without any apparent reason.\n\nCompare. The same code runs on desktop (left) and on the iPhone (right)\n\n![regular circle](https://github.com/anvaka/fieldplay/wiki/images/no_banding_desktop_small.gif)\n![iPhone banding effect](https://github.com/anvaka/fieldplay/wiki/images/banding_iphone_small.gif)\n\nWhat's even worse, when field is static (velocity is 0 everywhere), the particles on iPhone were kept moving:\n\n![Desktop - no movement, fine](https://github.com/anvaka/fieldplay/wiki/images/zero_v_desktop_small.gif)\n![iPhone - moving. Why?](https://github.com/anvaka/fieldplay/wiki/images/zero_v_moving_iphone_small.gif)\n\nI checked that requested floating point resolution was set to the highest available (`highp`). Yet the artifacts\nwere to obvious to let them be.\n\n### How can we fix this?\n\nI didn't want to go the easiest path of enabling floating point textures. They are\n[not as much widely supported](https://webglstats.com/search?query=OES_texture_float) as I'd like.\nInstead, I did what years of non-GPU programming told me not to do.\n\nI decided to solve thousands of ordinary differential equations not just once per frame. But one time per each dimension.\nI'd pass an attribute to the shader, telling which dimension needs to be written as an output for this \"draw\" call:\n\n``` glsl\nif (u_out_coordinate == 0) gl_FragColor = encodeFloatRGBA(pos.x);\nelse if (u_out_coordinate == 1) gl_FragColor = encodeFloatRGBA(pos.y);\n```\n\nIn pseudo-code it looks like this:\n\n```\nFrame 1:\n  Step 1: Hey WebGL, set u_out_coordinate to 0 and render everything into `texture_x`;\n  Step 2: Hey WebGL, set u_out_coordinate to 1 and render everything AGAIN into `texture_y`;\n```\n\nWe solve the same problem and throw away everything but `x` component of the solution. Then repeat it for `y`.\n\nIt seemed crazy to me, as I thought this would kill performance. But even my low-end Android phone had no problems\nwith this approach.\n\nThe `encodeFloatRGBA()` uses all 32 bits to encode float as RGBA vector. I found [its implementation](https://github.com/anvaka/fieldplay/blob/main/src/lib/utils/floatPacking.js)\nsomewhere on stackoverflow, and I'm not sure if it's the best possible way of packing\n(if you know better, please let me know).\n\nThe good news is that artifacts were gone:\n\n![No artifacts](https://github.com/anvaka/fieldplay/wiki/images/no_artifacts_small.gif)\n\n# Sharing\n\nMany times I was amazed by how beautiful some vector fields are. To encourage exploration I created a [very naive random vector field\ngenerator](https://github.com/anvaka/fieldplay/blob/main/src/lib/generate-equation.js). You can trigger it by pressing \"Randomize\" button.\n\n![Generator](https://github.com/anvaka/fieldplay/wiki/images/generator_small.gif)\n\nBut please don't think that what generator can do is all there is. It's just a tip of an iceberg, and I hope\nyou play with vector field yourself.\n\nWhen you find something interesting - don't forget to share! Just copy the link and share it away.\nThe link holds all necessary information to restore vector field state (this is done with help of\n[query-state](https://github.com/anvaka/query-state) library).\n\n## Video recording\n\nIf you'd like to record a video with a vector tool, please refer to [instructions in this\nfile](https://github.com/anvaka/fieldplay/blob/main/ScreenRecording.md).\n\n## Auto Mode\n\nYou can have it automatically shuffle through a variety of interesting fields by using the\n`autotime` URL param, which specifies how long to hold each field.\nFor instance `autotime=30s` will show a field for `30` seconds and then move to the next one.\nYou can use `ms`, `s`, `m`, or `h` to specify milliseconds, seconds, minutes, or hours, respectively.\nYou can also specify whether the fields are presets or generated or both, using `autosource=presets`,\n`autosource=generator`, or `autosource=both`.\n[Give it a try](https://anvaka.github.io/fieldplay/?autotime=30s\u0026autosource=both)!\n\n# Local development\n\nClone this repository, then:\n\n``` bash\n# install dependencies\nnpm install\n\n# serve with hot reload at http://localhost:8880\nnpm run dev\n```\n\n# License\n\nThe project is released under the [MIT license](https://github.com/anvaka/fieldplay/blob/main/LICENSE).\n\n# Thanks!\nI learned a lot building this project. I hope you too liked this short voyage into world of vector fields, math and WebGL.\n\nPlease [let me know](https://twitter.com/anvaka) what you think.\n\nHave fun!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/anvaka.github.io%2Ffieldplay%2F","html_url":"https://awesome.ecosyste.ms/projects/anvaka.github.io%2Ffieldplay%2F","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/anvaka.github.io%2Ffieldplay%2F/lists"}