├── .gitignore ├── LICENSE.txt ├── README.md ├── assets ├── example.gif ├── example2.gif └── mosaicmosqueedeparis.jpg ├── dist └── bundle.js ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json ├── scripts ├── advectTextureByField │ └── index.js ├── app.js ├── disturbFieldWithMouse │ └── index.js ├── drawFieldArrows │ └── index.js ├── drawPattern │ └── index.js ├── drawTexture │ └── index.js ├── drawVelocityField │ └── index.js ├── options.js └── utils │ └── index.js └── styles ├── dist └── site.css └── site.scss /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | ./node_modules/ 3 | ./node_modules/* 4 | .DS_Store 5 | fluid-sim.js 6 | deploy.sh 7 | ./assets/tigerstripes.jpg 8 | assets/tigerstripes.jpg 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Main application 3 | - 4 | 5 | Copyright 2018 Jakob Stasilowicz 6 | 7 | The MIT License 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 9 | documentation files (the "Software"), to deal in the Software without restriction, including without 10 | limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | Image of Mosaic in Mosquee de Paris 23 | - 24 | 25 | Copyright MarcCooperUK, licensed under https://creativecommons.org/licenses/by/2.0/deed.en 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluid simulation base 2 | 3 | This is a base for fluid simulations built using the amazing (really, try it!) declarative webgl helper api [regl](https://regl.party). 4 | 5 | **Only velocity field helpers and advection functionality is implemented**, but since I really like the look and feel of this almost-but-not-quite-a-fluid effect I will probably build upon this base when creating various **color soups** and more realistic fluid stuff :) 6 | 7 | Based heavily on Jamie Wong's article (see further resources below). 8 | 9 | ![Example](/assets/example.gif?raw=true "Example") 10 | ![Example2](/assets/example2.gif?raw=true "Example2") 11 | 12 | By Jakob Stasilowicz - kontakt [at] stasilo.se 13 | 14 | ## Building & running 15 | 16 | ```sh 17 | $ npm install 18 | $ gulp webserver 19 | ``` 20 | 21 | 22 | ## Further resources 23 | 24 | - Jamie Wong's great [article and code on fluid simulations in webgl](http://jamie-wong.com/2016/08/05/webgl-fluid-simulation/) 25 | - ["Fast Fluid Dynamics Simulation on the GPU"](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch38.html) in Nvidia's GPU Gems 26 | - [Amanda Ghassaei's webgl fluid simulation implementation](https://github.com/amandaghassaei/FluidSimulation) 27 | -------------------------------------------------------------------------------- /assets/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stasilo/webgl-regl-fluid-base/2df67444a77c809c0c20c91e19c3c4a15e8a51f1/assets/example.gif -------------------------------------------------------------------------------- /assets/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stasilo/webgl-regl-fluid-base/2df67444a77c809c0c20c91e19c3c4a15e8a51f1/assets/example2.gif -------------------------------------------------------------------------------- /assets/mosaicmosqueedeparis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stasilo/webgl-regl-fluid-base/2df67444a77c809c0c20c91e19c3c4a15e8a51f1/assets/mosaicmosqueedeparis.jpg -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | !function e(t,n,r){function i(o,u){if(!n[o]){if(!t[o]){var f="function"==typeof require&&require;if(!u&&f)return f(o,!0);if(a)return a(o,!0);var s=new Error("Cannot find module '"+o+"'");throw s.code="MODULE_NOT_FOUND",s}var c=n[o]={exports:{}};t[o][0].call(c.exports,function(e){var n=t[o][1][e];return i(n||e)},c,c.exports,e,t,n,r)}return n[o].exports}for(var a="function"==typeof require&&require,o=0;oc;)if((u=f[c++])!=u)return!0}else for(;s>c;c++)if((e||c in f)&&f[c]===n)return e||c||0;return!e&&-1}}},{"./_to-absolute-index":45,"./_to-iobject":47,"./_to-length":48}],8:[function(e,t,n){var r=e("./_cof"),i=e("./_wks")("toStringTag"),a="Arguments"==r(function(){return arguments}());t.exports=function(e){var t,n,o;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?n:a?r(t):"Object"==(o=r(t))&&"function"==typeof t.callee?"Arguments":o}},{"./_cof":9,"./_wks":52}],9:[function(e,t,n){var r={}.toString;t.exports=function(e){return r.call(e).slice(8,-1)}},{}],10:[function(e,t,n){var r=t.exports={version:"2.5.3"};"number"==typeof __e&&(__e=r)},{}],11:[function(e,t,n){"use strict";var r=e("./_object-dp"),i=e("./_property-desc");t.exports=function(e,t,n){t in e?r.f(e,t,i(0,n)):e[t]=n}},{"./_object-dp":34,"./_property-desc":39}],12:[function(e,t,n){var r=e("./_a-function");t.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},{"./_a-function":5}],13:[function(e,t,n){t.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},{}],14:[function(e,t,n){t.exports=!e("./_fails")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},{"./_fails":18}],15:[function(e,t,n){var r=e("./_is-object"),i=e("./_global").document,a=r(i)&&r(i.createElement);t.exports=function(e){return a?i.createElement(e):{}}},{"./_global":19,"./_is-object":26}],16:[function(e,t,n){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},{}],17:[function(e,t,n){var r=e("./_global"),i=e("./_core"),a=e("./_ctx"),o=e("./_hide"),u="prototype",f=function(e,t,n){var s,c,l,d=e&f.F,p=e&f.G,m=e&f.S,h=e&f.P,v=e&f.B,b=e&f.W,g=p?i:i[t]||(i[t]={}),y=g[u],x=p?r:m?r[t]:(r[t]||{})[u];p&&(n=t);for(s in n)(c=!d&&x&&void 0!==x[s])&&s in g||(l=c?x[s]:n[s],g[s]=p&&"function"!=typeof x[s]?n[s]:v&&c?a(l,r):b&&x[s]==l?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t[u]=e[u],t}(l):h&&"function"==typeof l?a(Function.call,l):l,h&&((g.virtual||(g.virtual={}))[s]=l,e&f.R&&y&&!y[s]&&o(y,s,l)))};f.F=1,f.G=2,f.S=4,f.P=8,f.B=16,f.W=32,f.U=64,f.R=128,t.exports=f},{"./_core":10,"./_ctx":12,"./_global":19,"./_hide":21}],18:[function(e,t,n){t.exports=function(e){try{return!!e()}catch(e){return!0}}},{}],19:[function(e,t,n){var r=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=r)},{}],20:[function(e,t,n){var r={}.hasOwnProperty;t.exports=function(e,t){return r.call(e,t)}},{}],21:[function(e,t,n){var r=e("./_object-dp"),i=e("./_property-desc");t.exports=e("./_descriptors")?function(e,t,n){return r.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},{"./_descriptors":14,"./_object-dp":34,"./_property-desc":39}],22:[function(e,t,n){var r=e("./_global").document;t.exports=r&&r.documentElement},{"./_global":19}],23:[function(e,t,n){t.exports=!e("./_descriptors")&&!e("./_fails")(function(){return 7!=Object.defineProperty(e("./_dom-create")("div"),"a",{get:function(){return 7}}).a})},{"./_descriptors":14,"./_dom-create":15,"./_fails":18}],24:[function(e,t,n){var r=e("./_cof");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==r(e)?e.split(""):Object(e)}},{"./_cof":9}],25:[function(e,t,n){var r=e("./_iterators"),i=e("./_wks")("iterator"),a=Array.prototype;t.exports=function(e){return void 0!==e&&(r.Array===e||a[i]===e)}},{"./_iterators":31,"./_wks":52}],26:[function(e,t,n){t.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},{}],27:[function(e,t,n){var r=e("./_an-object");t.exports=function(e,t,n,i){try{return i?t(r(n)[0],n[1]):t(n)}catch(t){var a=e.return;throw void 0!==a&&r(a.call(e)),t}}},{"./_an-object":6}],28:[function(e,t,n){"use strict";var r=e("./_object-create"),i=e("./_property-desc"),a=e("./_set-to-string-tag"),o={};e("./_hide")(o,e("./_wks")("iterator"),function(){return this}),t.exports=function(e,t,n){e.prototype=r(o,{next:i(1,n)}),a(e,t+" Iterator")}},{"./_hide":21,"./_object-create":33,"./_property-desc":39,"./_set-to-string-tag":41,"./_wks":52}],29:[function(e,t,n){"use strict";var r=e("./_library"),i=e("./_export"),a=e("./_redefine"),o=e("./_hide"),u=e("./_has"),f=e("./_iterators"),s=e("./_iter-create"),c=e("./_set-to-string-tag"),l=e("./_object-gpo"),d=e("./_wks")("iterator"),p=!([].keys&&"next"in[].keys()),m="values",h=function(){return this};t.exports=function(e,t,n,v,b,g,y){s(n,t,v);var x,_,w,A=function(e){if(!p&&e in T)return T[e];switch(e){case"keys":case m:return function(){return new n(this,e)}}return function(){return new n(this,e)}},S=t+" Iterator",E=b==m,k=!1,T=e.prototype,M=T[d]||T["@@iterator"]||b&&T[b],D=!p&&M||A(b),O=b?E?A("entries"):D:void 0,j="Array"==t?T.entries||M:M;if(j&&(w=l(j.call(new e)))!==Object.prototype&&w.next&&(c(w,S,!0),r||u(w,d)||o(w,d,h)),E&&M&&M.name!==m&&(k=!0,D=function(){return M.call(this)}),r&&!y||!p&&!k&&T[d]||o(T,d,D),f[t]=D,f[S]=h,b)if(x={values:E?D:A(m),keys:g?D:A("keys"),entries:O},y)for(_ in x)_ in T||a(T,_,x[_]);else i(i.P+i.F*(p||k),t,x);return x}},{"./_export":17,"./_has":20,"./_hide":21,"./_iter-create":28,"./_iterators":31,"./_library":32,"./_object-gpo":36,"./_redefine":40,"./_set-to-string-tag":41,"./_wks":52}],30:[function(e,t,n){var r=e("./_wks")("iterator"),i=!1;try{var a=[7][r]();a.return=function(){i=!0},Array.from(a,function(){throw 2})}catch(e){}t.exports=function(e,t){if(!t&&!i)return!1;var n=!1;try{var a=[7],o=a[r]();o.next=function(){return{done:n=!0}},a[r]=function(){return o},e(a)}catch(e){}return n}},{"./_wks":52}],31:[function(e,t,n){t.exports={}},{}],32:[function(e,t,n){t.exports=!0},{}],33:[function(e,t,n){var r=e("./_an-object"),i=e("./_object-dps"),a=e("./_enum-bug-keys"),o=e("./_shared-key")("IE_PROTO"),u=function(){},f="prototype",s=function(){var t,n=e("./_dom-create")("iframe"),r=a.length;for(n.style.display="none",e("./_html").appendChild(n),n.src="javascript:",(t=n.contentWindow.document).open(),t.write(" 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angest-wow", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "scripts/dist/bundle.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Jakob Stasilowicz (https://stasilo.se)", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "babel-plugin-add-module-exports": "^0.2.1", 13 | "babel-plugin-transform-es2015-parameters": "^6.24.1", 14 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 15 | "babel-plugin-transform-runtime": "^6.23.0", 16 | "babel-polyfill": "^6.26.0", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-es2017": "^6.24.1", 19 | "babel-preset-stage-0": "^6.24.1", 20 | "babelify": "^7.3.0", 21 | "baboon-image": "^2.1.0", 22 | "browserify": "^14.4.0", 23 | "fps-sampler": "^1.1.0", 24 | "glsl-blend-overlay": "^1.0.5", 25 | "glsl-fxaa": "^3.0.0", 26 | "glsl-hsv2rgb": "^1.0.0", 27 | "glsl-luma": "^1.0.1", 28 | "glsl-map": "^1.0.1", 29 | "glsl-noise": "0.0.0", 30 | "glsl-y-rotate": "^2.0.0", 31 | "glslify": "^6.1.1", 32 | "gulp": "^3.9.1", 33 | "gulp-autoprefixer": "^4.0.0", 34 | "gulp-clean": "^0.3.2", 35 | "gulp-glslify": "git+https://github.com/yuichiroharai/gulp-glslify.git", 36 | "gulp-noop": "^1.0.0", 37 | "gulp-sass": "^3.1.0", 38 | "gulp-server-livereload": "^1.9.2", 39 | "gulp-sourcemaps": "^2.6.1", 40 | "gulp-uglify": "^3.0.0", 41 | "in-viewport": "^3.6.0", 42 | "lodash.chunk": "^4.2.0", 43 | "mouse-change": "^1.4.0", 44 | "mouse-position": "^2.0.1", 45 | "query-string": "^5.0.0", 46 | "regl": "^1.3.1", 47 | "stringify": "^5.1.0", 48 | "vinyl-buffer": "^1.0.0", 49 | "vinyl-source-stream": "^1.1.0", 50 | "watchify": "^3.9.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/advectTextureByField/index.js: -------------------------------------------------------------------------------- 1 | const glsl = require('glslify'); 2 | const defined = require('../utils').defined; 3 | 4 | // advection time delta 5 | const deltaT = 1/100; 6 | 7 | // given an velocity vector field texture and a time delta, advect the 8 | // quantities in the input texture into the output texture 9 | 10 | module.exports = regl => { 11 | const advectTextureByField = args => regl({ 12 | framebuffer: regl.prop('output'), 13 | frag: glsl` 14 | precision mediump float; 15 | 16 | uniform sampler2D velocityTexture; 17 | uniform sampler2D inputTexture; 18 | 19 | uniform vec2 resolution; 20 | uniform float deltaT; 21 | // uniform float time; 22 | 23 | varying vec2 uv; 24 | 25 | #pragma glslify: map = require('glsl-map'); 26 | 27 | void main () { 28 | vec2 q = map( 29 | texture2D(velocityTexture, uv).xy, 30 | vec2(0.), vec2(1.), 31 | vec2(-1.), vec2(1.) 32 | ); 33 | 34 | vec2 pastCoord = fract(uv + (0.5 * deltaT * q)); 35 | gl_FragColor = texture2D(inputTexture, pastCoord); 36 | } 37 | `, 38 | 39 | vert: glsl` 40 | precision mediump float; 41 | 42 | attribute vec2 position; 43 | varying vec2 uv; 44 | 45 | void main () { 46 | uv = 1. - position; 47 | gl_Position = vec4(1.0 - 2.0 * position, 0, 1); 48 | } 49 | `, 50 | attributes: { 51 | position: [ 52 | -2, 0, 53 | 0, -2, 54 | 2, 2 55 | ] 56 | }, 57 | uniforms: { 58 | resolution: context => [context.viewportWidth, context.viewportHeight], 59 | velocityTexture: regl.prop('velocityField'), 60 | inputTexture: regl.prop('input'), 61 | deltaT: defined(args.deltaT) ? args.deltaT : deltaT, 62 | // time: ({tick}) => 0.01 * tick 63 | }, 64 | count: 3, 65 | })(args); 66 | 67 | return advectTextureByField; 68 | } 69 | -------------------------------------------------------------------------------- /scripts/app.js: -------------------------------------------------------------------------------- 1 | const options = require('./options'); 2 | const defined = require('./utils').defined; 3 | 4 | const regl = defined(options.pixelRatio) && options.pixelRatio != null 5 | ? require('regl')({pixelRatio: options.pixelRatio, container: options.container}) 6 | : require('regl')({container: options.container}); 7 | 8 | const glsl = require('glslify'); 9 | const fpsSampler = new (require('fps-sampler'))(); 10 | const inViewport = require('in-viewport'); 11 | 12 | const drawFieldArrows = require('./drawFieldArrows')(regl); 13 | const drawVelocityField = require('./drawVelocityField')(regl); 14 | const {drawTexture, drawTextureToScreen} = require('./drawTexture')(regl); 15 | const drawPattern = require('./drawPattern')(regl); 16 | 17 | const advectTextureByField = require('./advectTextureByField')(regl); 18 | const disturbFieldWithMouse = require('./disturbFieldWithMouse')(regl); 19 | 20 | const copyFrameBufferToTexture = (args, texture) =>  21 | regl({framebuffer: args.fbo})(() => texture({copy: true})); 22 | 23 | let paused = false; 24 | let stop = false; 25 | 26 | let showArrows = defined(options.showArrows) ? options.showArrows : false; 27 | let waveMode = defined(options.waveMode) ? options.waveMode : false; 28 | 29 | let canvas = document.querySelector(options.container ? `${options.container} canvas` : 'canvas'); 30 | let fpsContainer = document.getElementById('fps'); 31 | 32 | let colorDeltaT = defined(options.colorDeltaT) ? options.colorDeltaT : 1/60; 33 | let fieldDeltaT = defined(options.fieldDeltaT) ? options.fieldDeltaT : 1/120; 34 | 35 | if(defined(options.pixelRatio) && options.pixelRatio != null) { 36 | colorDeltaT *= options.pixelRatio; 37 | fieldDeltaT *= options.pixelRatio; 38 | } else { 39 | colorDeltaT *= defined(devicePixelRatio) ? devicePixelRatio : 1; 40 | fieldDeltaT *= defined(devicePixelRatio) ? devicePixelRatio : 1; 41 | } 42 | 43 | const fieldSettings = { 44 | colorDeltaT, 45 | fieldDeltaT 46 | } 47 | 48 | const fboSettings = { 49 | width: canvas.width, 50 | height: canvas.height, 51 | depth: false, 52 | stencil: false, 53 | } 54 | 55 | let velocityFbo0 = regl.framebuffer(fboSettings); 56 | let velocityFbo1 = regl.framebuffer(fboSettings); 57 | let velocityTexture = regl.texture(); 58 | 59 | let colorFieldFbo0 = regl.framebuffer(fboSettings); 60 | let colorFieldFbo1 = regl.framebuffer(fboSettings); 61 | 62 | let frameRenderer = null; 63 | let image = null; 64 | 65 | const setupKeyboardEvents = () => { 66 | document.addEventListener('keydown', e => { 67 | const key = String.fromCharCode(e.keyCode); 68 | if(key === 'A') { 69 | showArrows = !showArrows; 70 | } 71 | }); 72 | } 73 | 74 | const advectColorsAndFieldLoop = () => { 75 | regl.clear({ 76 | color: [0, 0, 0, 0] 77 | }) 78 | 79 | // debug 80 | // drawPattern({output: colorFieldFbo0}); 81 | let imageTexture = regl.texture({data: image, flipY: true}); 82 | drawTexture({ 83 | texture: imageTexture, 84 | output: colorFieldFbo0 85 | }, true, false, true, [image.width, image.height]); 86 | 87 | // drawTextureToScreen({texture: colorFieldFbo0}, false, false, false); 88 | // return; 89 | 90 | drawVelocityField({ 91 | output: velocityFbo0, 92 | field: { // empty field, see drawVelocityField() for examples 93 | vX: `0.0`, 94 | vY: `0.0` 95 | } 96 | }); 97 | 98 | frameRenderer = regl.frame(() => { 99 | if(paused || !inViewport(canvas)) { 100 | fpsSampler.update(); 101 | return; 102 | } 103 | 104 | copyFrameBufferToTexture({fbo: velocityFbo0}, velocityTexture); 105 | 106 | // wow 107 | if(waveMode) { 108 | velocityTexture({flipY: true}); 109 | } 110 | 111 | advectTextureByField({ 112 | velocityField: velocityTexture, 113 | input: velocityFbo0, 114 | output: velocityFbo1, 115 | deltaT: fieldSettings.fieldDeltaT 116 | }); 117 | 118 | disturbFieldWithMouse({ 119 | velocityField: velocityFbo1, 120 | output: velocityFbo0 121 | }); 122 | 123 | advectTextureByField({ 124 | velocityField: velocityFbo0, 125 | input: colorFieldFbo0, 126 | output: colorFieldFbo1, 127 | deltaT: fieldSettings.colorDeltaT, 128 | flip: true, 129 | }); 130 | 131 | [colorFieldFbo0, colorFieldFbo1] = [colorFieldFbo1, colorFieldFbo0]; 132 | 133 | drawTextureToScreen({texture: colorFieldFbo1}); 134 | 135 | if(showArrows) { 136 | drawFieldArrows({ 137 | fieldTexture: velocityFbo0, 138 | arrowColor: options.arrowColor 139 | }); 140 | } 141 | 142 | fpsContainer.innerHTML = `${fpsSampler.update().fps} fps`; 143 | }); 144 | } 145 | 146 | function app() { 147 | image = new Image(); 148 | 149 | image.onload = () => { 150 | setupKeyboardEvents(); 151 | advectColorsAndFieldLoop(); 152 | } 153 | 154 | image.src = options.imageUrl; 155 | } 156 | 157 | document.addEventListener('DOMContentLoaded', app, false); 158 | -------------------------------------------------------------------------------- /scripts/disturbFieldWithMouse/index.js: -------------------------------------------------------------------------------- 1 | const glsl = require('glslify'); 2 | const defined = require('../utils').defined; 3 | const mouse = require('mouse-position')(); 4 | 5 | module.exports = regl => { 6 | const disturbFieldWithMouse = args => regl({ 7 | framebuffer: args.output, 8 | frag: glsl` 9 | precision mediump float; 10 | 11 | uniform sampler2D velocityTexture; 12 | uniform vec4 mouse; 13 | uniform vec2 resolution; 14 | uniform float time; 15 | uniform float pixelRatio; 16 | 17 | varying vec2 uv; 18 | 19 | #pragma glslify: map = require('glsl-map'); 20 | // #pragma glslify: snoise = require(glsl-noise/simplex/2d); 21 | 22 | // bigger splat for safari looks better (probably because safari mouse movement calculations are different) 23 | #define SPLAT_INTENSITY ${defined(window.safari) ? '50.' : '10.'} 24 | 25 | void main () { 26 | float dist = length(gl_FragCoord.xy - mouse.xy); 27 | float radius = 0.30 * pixelRatio; 28 | vec2 mouseDir = mouse.zw; 29 | 30 | // float blobIntensity = exp(-(0.01 * (dist + snoise(mouse.xy*100.) * 100.)) / radius); 31 | float blobIntensity = exp(-(0.01 * dist) / radius); 32 | 33 | vec2 blob = clamp(blobIntensity * mouseDir, -1.0, 1.0); 34 | 35 | gl_FragColor = texture2D(velocityTexture, uv) + vec4(blob * SPLAT_INTENSITY, 0., 1.); 36 | } 37 | `, 38 | vert: glsl` 39 | precision mediump float; 40 | 41 | attribute vec2 position; 42 | varying vec2 uv; 43 | 44 | void main () { 45 | uv = 1.0 - position; 46 | //uv = position; 47 | gl_Position = vec4(1.0 - 2.0 * position, 0, 1); 48 | } 49 | `, 50 | attributes: { 51 | position: [ 52 | -2, 0, 53 | 0, -2, 54 | 2, 2 55 | ] 56 | }, 57 | uniforms: { 58 | velocityTexture: args.velocityField, 59 | resolution: context => [context.viewportWidth, context.viewportHeight], 60 | time: ({tick}) => 0.01 * tick, 61 | pixelRatio: ({pixelRatio}) => pixelRatio, 62 | mouse: ({pixelRatio, viewportHeight, viewportWidth}) => { 63 | const mouseX = viewportWidth - mouse[0]*pixelRatio; 64 | // correct offset since we're listening for pos on body el 65 | const topY = window.pageYOffset || document.documentElement.scrollTop; 66 | const mouseY = (mouse[1] + topY)*pixelRatio; 67 | 68 | const dX = (mouse.prev[0] - mouse[0]) / viewportWidth; 69 | const dY = (mouse.prev[1] - mouse[1]) / viewportHeight; 70 | 71 | mouse.flush(); 72 | 73 | return [ 74 | mouseX, 75 | mouseY, 76 | dX * -1, 77 | dY 78 | ]; 79 | } 80 | }, 81 | count: 3 82 | })(); 83 | 84 | return disturbFieldWithMouse; 85 | } 86 | -------------------------------------------------------------------------------- /scripts/drawFieldArrows/index.js: -------------------------------------------------------------------------------- 1 | const glsl = require('glslify'); 2 | const {defined, flatten} = require('../utils'); 3 | 4 | // triangle coord offsets for a 19x19 grid of triangles covering the canvas 5 | let triangleOffsets = flatten( 6 | [...Array(19)].map((_, i) => 7 | [...Array(19)].map((_, j) => ({ offset: [ 8 | -0.93 + 0.1 * j, 9 | 0.9 - 0.1 * i 10 | ]})) 11 | ) 12 | ); 13 | 14 | module.exports = regl => { 15 | // draw a field of arrows to track velocity field activity 16 | const drawArrows = (args) => regl({ 17 | frag: glsl` 18 | precision mediump float; 19 | uniform vec4 color; 20 | 21 | void main() { 22 | gl_FragColor = color; 23 | } 24 | `, 25 | vert: glsl` 26 | precision mediump float; 27 | 28 | attribute vec2 position; 29 | uniform sampler2D fieldTexture; 30 | uniform vec2 offset; 31 | 32 | #pragma glslify: map = require('glsl-map'); 33 | 34 | void main() { 35 | vec2 uv = 1. - map(position + offset, 36 | vec2(-1.), vec2(1.), 37 | vec2(0.), vec2(1.) 38 | ); 39 | 40 | vec2 v = map( 41 | texture2D(fieldTexture, uv).xy, 42 | vec2(0.), vec2(1.), 43 | vec2(-1.), vec2(1.) 44 | ); 45 | 46 | float scale = 1.0 * length(v); 47 | float angle = -atan(v.y, v.x); // angle between vector and x-axis 48 | 49 | vec2 pos = position * scale; 50 | 51 | // rotate and translate by offset 52 | gl_Position = vec4( 53 | cos(angle) * pos.x + sin(angle) * pos.y + offset.x, 54 | -sin(angle) * pos.x + cos(angle) * pos.y + offset.y, 55 | 0, 56 | 1 57 | ); 58 | } 59 | `, 60 | attributes: { 61 | position: [ // centered triangle vertices 62 | 0, 0.2, 63 | 1, 0, 64 | 0, -0.2 65 | ].map(p => p * 0.05) // scale down 66 | }, 67 | uniforms: { 68 | fieldTexture: () => args.fieldTexture, 69 | offset: regl.prop('offset'), 70 | color: defined(args.arrowColor) 71 | ? [...args.arrowColor.map(rgb => rgb === 0 ? 0 : rgb/255), 1.0] 72 | : [0, 1, 0, 1] 73 | }, 74 | depth: { 75 | enable: false 76 | }, 77 | count: 3 78 | })(triangleOffsets); 79 | 80 | return drawArrows; 81 | } 82 | -------------------------------------------------------------------------------- /scripts/drawPattern/index.js: -------------------------------------------------------------------------------- 1 | const glsl = require('glslify'); 2 | 3 | module.exports = regl => { 4 | const drawPattern = regl({ 5 | framebuffer: regl.prop('output'), 6 | frag: glsl` 7 | precision mediump float; 8 | 9 | uniform vec2 resolution; 10 | uniform vec3 color; 11 | varying vec2 uv; 12 | 13 | // from book of shaders 14 | float circle(in vec2 uv, in float radius) { 15 | uv.y *= resolution.y / resolution.x; // fix aspect ratio 16 | uv *= 7.0; // scale the space 17 | uv = fract(uv); // wrap around 1.0 18 | 19 | vec2 l = uv - vec2(0.5); 20 | return 1. - smoothstep(radius - (radius * 0.01), radius + (radius * 0.01), dot(l, l) * 4.); 21 | } 22 | 23 | void main () { 24 | // black bg 25 | // gl_FragColor = vec4(color * circle(uv, 0.25), 1.0); 26 | 27 | // trans bg 28 | gl_FragColor = circle(uv, 0.25) * vec4(color, 1.0); 29 | } 30 | `, 31 | 32 | vert: glsl` 33 | precision mediump float; 34 | attribute vec2 position; 35 | varying vec2 uv; 36 | 37 | void main () { 38 | uv = position; 39 | gl_Position = vec4(1.0 - 2.0 * position, 0, 1); 40 | } 41 | `, 42 | 43 | attributes: { 44 | position: [ 45 | -2, 0, 46 | 0, -2, 47 | 2, 2 48 | ] 49 | }, 50 | uniforms: { 51 | resolution: context => [context.viewportWidth, context.viewportHeight], 52 | color: [211, 69, 69].map(rgb => rgb/255) 53 | }, 54 | count: 3, 55 | }); 56 | 57 | return drawPattern; 58 | } 59 | -------------------------------------------------------------------------------- /scripts/drawTexture/index.js: -------------------------------------------------------------------------------- 1 | const glsl = require('glslify'); 2 | const {defined} = require('../utils'); 3 | 4 | module.exports = regl => { 5 | const drawTexture = (args, antialias = false, grain = false, cover = false, textureResolution = [0, 0]) => regl({ 6 | framebuffer: regl.prop('output'), 7 | frag: glsl` 8 | precision mediump float; 9 | 10 | uniform sampler2D texture; 11 | uniform vec2 resolution; 12 | uniform vec2 textureResolution; 13 | uniform bool antialias; 14 | 15 | varying vec2 uv; 16 | 17 | #pragma glslify: fxaa = require(glsl-fxaa) 18 | 19 | float rand(vec2 co) { 20 | return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453); 21 | } 22 | 23 | vec3 saturate(vec3 a) { 24 | return clamp(a, 0., 1.); 25 | } 26 | 27 | void main () { 28 | 29 | // draw texture using "background-size: cover"-ish fill? 30 | // see: https://gist.github.com/statico/df64c5d167362ecf7b34fca0b1459a44 31 | ${defined(cover) && cover ? ` 32 | vec2 s = resolution; // Screen 33 | vec2 i = textureResolution; //vec2(1920.0, 1080.0); // Image 34 | 35 | float rs = s.x / s.y; 36 | float ri = i.x / i.y; 37 | 38 | vec2 new = rs < ri 39 | ? vec2(i.x * s.y / i.y, s.y) 40 | : vec2(s.x, i.y * s.x / i.x); 41 | 42 | vec2 offset = rs < ri 43 | ? vec2((new.x - s.x) / 2.0, 0.0) / new 44 | : vec2(0.0, (new.y - s.y) / 2.0) / new; 45 | 46 | vec2 uw = uv * s / new + offset; 47 | ` : ` 48 | vec2 uw = uv; 49 | `}; 50 | 51 | // screen space aa? 52 | // (conditional in-shader 'cause glslify transforms required func names, i think) 53 | 54 | vec4 col = antialias == true 55 | ? fxaa(texture, resolution*uw, resolution) // fxaa(texture, gl_FragCoord.xy, resolution) 56 | : texture2D(texture, uw); 57 | 58 | // add noise grain? 59 | 60 | ${defined(grain) && grain ? ` 61 | col.rgb += (rand(uv) - 0.5) * 0.07; 62 | col.rgb = saturate(col.rgb); 63 | ` :''}; 64 | 65 | gl_FragColor = col; 66 | } 67 | `, 68 | vert: glsl` 69 | precision mediump float; 70 | 71 | attribute vec2 position; 72 | uniform vec2 resolution; 73 | varying vec2 uv; 74 | 75 | void main () { 76 | uv = position; 77 | gl_Position = vec4(1.0 - 2.0 * position, 0, 1); 78 | } 79 | `, 80 | attributes: { 81 | position: [ 82 | -2, 0, 83 | 0, -2, 84 | 2, 2 85 | ] 86 | }, 87 | uniforms: { 88 | resolution: context => [context.viewportWidth, context.viewportHeight], 89 | texture: regl.prop('texture'), 90 | antialias, 91 | textureResolution 92 | }, 93 | count: 3 94 | })(args); 95 | 96 | const drawTextureToScreen = (args, antialias = false, grain = false, cover = false, textureResolution = [0,0]) => 97 | drawTexture({output: null, texture: args.texture}, antialias, grain, cover, textureResolution); 98 | 99 | return {drawTexture, drawTextureToScreen}; 100 | } 101 | -------------------------------------------------------------------------------- /scripts/drawVelocityField/index.js: -------------------------------------------------------------------------------- 1 | // const regl = require('../reglInstance')(); 2 | const glsl = require('glslify'); 3 | const defined = require('../utils').defined; 4 | 5 | // divergence free field copied from Jamie Wong - http://jamie-wong.com/2016/08/05/webgl-fluid-simulation/ 6 | const defaultField = { 7 | vX: `sin(2.0 * ${Math.PI} * uv.y * SCALE)`, 8 | vY: `sin(2.0 * ${Math.PI} * uv.x * SCALE)` 9 | }; 10 | 11 | // output field to a framebuffer, input range of scalar functions is assumed to be [-1, 1] 12 | // output range is mapped to [0, 1] 13 | 14 | module.exports = regl => { 15 | const drawVelocityField = args => regl({ 16 | framebuffer: args.output, 17 | frag: glsl` 18 | precision mediump float; 19 | varying vec2 uv; 20 | 21 | #define SCALE 2. // bigger scale => smaller swirls in bigger numbers 22 | 23 | #pragma glslify: map = require('glsl-map'); 24 | 25 | void main () { 26 | gl_FragColor = vec4( 27 | map(${defined(args.field) ? args.field.vX : defaultField.vX}, -1., 1., 0., 1.), 28 | map(${defined(args.field) ? args.field.vY : defaultField.vY}, -1., 1., 0., 1.), 29 | 0.0, 30 | 1.0 31 | ); 32 | } 33 | `, 34 | vert: glsl` 35 | precision mediump float; 36 | attribute vec2 position; 37 | varying vec2 uv; 38 | 39 | void main () { 40 | uv = 1. - position; 41 | gl_Position = vec4(1.0 - 2.0 * position, 0, 1); 42 | } 43 | `, 44 | attributes: { 45 | position: [ 46 | -2, 0, 47 | 0, -2, 48 | 2, 2 49 | ] 50 | }, 51 | uniforms: { 52 | // time: ({tick}) => 0.01 * tick 53 | }, 54 | count: 3 55 | })(); 56 | 57 | return drawVelocityField; 58 | } 59 | -------------------------------------------------------------------------------- /scripts/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | container: null, 3 | imageUrl: 'assets/mosaicmosqueedeparis.jpg', 4 | //pixelRatio: 1, 5 | waveMode: false, 6 | fieldDeltaT: 1/100, 7 | colorDeltaT: 1/60, 8 | showArrows: false, 9 | arrowColor: [0, 0, 0] 10 | }; 11 | -------------------------------------------------------------------------------- /scripts/utils/index.js: -------------------------------------------------------------------------------- 1 | const defined = obj => typeof obj !== 'undefined'; 2 | 3 | const reverse = ([x, ...xs]) => typeof x !== 'undefined' 4 | ? [...reverse(xs), x] 5 | : []; 6 | 7 | const flatten = ([x, ...xs]) => typeof x !== 'undefined' 8 | ? Array.isArray(x) ? [...flatten(x), ...flatten(xs)] : [x, ...flatten(xs)] 9 | : []; 10 | 11 | module.exports = {flatten, reverse, defined}; 12 | -------------------------------------------------------------------------------- /styles/dist/site.css: -------------------------------------------------------------------------------- 1 | body{background-color:white} 2 | -------------------------------------------------------------------------------- /styles/site.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | } 4 | --------------------------------------------------------------------------------