├── .gitignore ├── LICENSE ├── README.md ├── example ├── interrupt.js └── lots-of-points.js ├── index.js ├── package.json ├── src ├── easings │ ├── compiled-easings.js │ ├── easings.glsl │ └── index.js └── index.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | gh-pages 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Matthew Conlen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # regl-tween 3 | 4 | A helper module that wraps [regl](https://github.com/mikolalysenko/regl/) buffers and automatically provides interpolation. 5 | 6 | ### Why? 7 | 8 | This automatically computes tweened values in the shader so it's much faster than 9 | doing it in the main javascript thread. See it running with a quarter of a million points: https://mathisonian.github.io/regl-tween/ 10 | 11 | ## installation 12 | 13 | ``` 14 | $ npm install --save regl-tween 15 | ``` 16 | 17 | ## Example 18 | 19 | ```js 20 | var regl = require('regl')(); 21 | var tween = require('regl-tween')(regl); 22 | 23 | var COUNT = 10000; 24 | 25 | // Pass in the initial data and optionally provide some customizations to 26 | // the interpolation function. 27 | var positionBuffer = tween.buffer(getRandomPositions(COUNT), { duration: 1000 }); 28 | 29 | // Wrap your command in tween() instead of in regl() 30 | // A modified regl command is returned. 31 | const drawParticles = tween({ 32 | vert: ` 33 | precision mediump float; 34 | attribute vec2 position; 35 | uniform float pointSize; 36 | 37 | void main() { 38 | gl_PointSize = pointSize; 39 | gl_Position = vec4(position, 0, 1); 40 | }`, 41 | frag: ` 42 | precision lowp float; 43 | void main() { 44 | if (length(gl_PointCoord.xy - 0.5) > 0.5) { 45 | discard; 46 | } 47 | gl_FragColor = vec4(1.0, 0.0, 0.0, 0.8); 48 | }`, 49 | 50 | attributes: { 51 | position: positionBuffer, 52 | }, 53 | 54 | uniforms: { 55 | pointSize: 6, 56 | }, 57 | 58 | count: COUNT, 59 | primitive: 'points' 60 | }) 61 | 62 | 63 | setInterval(() => { 64 | positionBuffer.update(getRandomPositions(COUNT)); 65 | }, 1000); 66 | 67 | regl.frame(() => { 68 | drawParticles(); 69 | }) 70 | ``` 71 | 72 | ## API 73 | 74 | ### tween(commandObject) 75 | 76 | Returns a `regl` command, modified it to support the in-shader tweening. See the 77 | [regl documentation](https://github.com/mikolalysenko/regl) for more info. 78 | 79 | ### tween.buffer(data, [options]) 80 | 81 | Creates a new buffer with the provided data. Options are not required but 82 | the following keys are supported: 83 | 84 | * `duration` - animation duration in milliseconds. Defaults to 750 milliseconds. 85 | * `easing` - easing type. Defaults to `quad-in-out`. Any function in https://github.com/mattdesl/eases is a valid input option. e.g. `linear`, `expo-in-out`, etc. 86 | 87 | #### Example 88 | 89 | ```js 90 | var tweenedBuffer = tween.buffer(myPositionArray, { duration: 500, ease: 'expo-in-out'}); 91 | ``` 92 | 93 | ### tween.buffer.update(data) 94 | 95 | Updates the data on an existing buffer. 96 | 97 | #### Example 98 | 99 | ```js 100 | var tweenedBuffer = tween.buffer(myPositionArray, { duration: 500}); 101 | tweenedBuffer.update(newPositionArray); 102 | ``` 103 | 104 | ## TODO's / caveats 105 | 106 | Things to know: 107 | 108 | * This hangs if you interrupt an animation on a very large data set. The issue is that with the current implementation interrupted animations fall back to doing computation on the CPU and hence it ends up being slow. 109 | -------------------------------------------------------------------------------- /example/interrupt.js: -------------------------------------------------------------------------------- 1 | var regl = require('regl')(); 2 | var tween = require('..')(regl); 3 | 4 | var COUNT = 30000; 5 | 6 | var count = 0; 7 | var getRandomPositions = function (n) { 8 | var positions = []; 9 | for (var i = 0; i < n; i++) { 10 | if (count % 5 === 0) { 11 | positions.push([Math.random() * 2 - 1, Math.random() * 2 - 1]); 12 | } else if (count % 5 === 1) { 13 | positions.push([Math.random(), Math.random() * 2 - 1]); 14 | } else if (count % 5 === 2) { 15 | positions.push([Math.random() * 2 - 1, Math.random()]); 16 | } else if (count % 5 === 3) { 17 | positions.push([Math.random() * -1, Math.random() * -1]); 18 | } else if (count % 5 === 4) { 19 | positions.push([Math.random(), Math.random()]); 20 | } 21 | } 22 | count++; 23 | return positions; 24 | }; 25 | 26 | var positionBuffer = tween.buffer(getRandomPositions(COUNT), { duration: 1500 }); 27 | 28 | const drawParticles = tween({ 29 | vert: ` 30 | precision mediump float; 31 | attribute vec2 position; 32 | uniform float pointSize; 33 | 34 | void main() { 35 | gl_PointSize = pointSize; 36 | gl_Position = vec4(position, 0, 1); 37 | } 38 | `, 39 | frag: ` 40 | precision lowp float; 41 | void main() { 42 | if (length(gl_PointCoord.xy - 0.5) > 0.5) { 43 | discard; 44 | } 45 | gl_FragColor = vec4(1.0, 0.0, 0.0, 0.6); 46 | } 47 | `, 48 | 49 | attributes: { 50 | position: positionBuffer 51 | }, 52 | 53 | uniforms: { 54 | pointSize: 4 55 | }, 56 | 57 | count: COUNT, 58 | primitive: 'points' 59 | }); 60 | 61 | setInterval(() => { 62 | positionBuffer.update(getRandomPositions(COUNT)); 63 | }, 1000); 64 | 65 | regl.frame(() => { 66 | drawParticles(); 67 | }); 68 | -------------------------------------------------------------------------------- /example/lots-of-points.js: -------------------------------------------------------------------------------- 1 | var regl = require('regl')(); 2 | var tween = require('..')(regl); 3 | 4 | var COUNT = 1250000; 5 | 6 | var count = 0; 7 | var getRandomPositions = function (n) { 8 | var positions = []; 9 | for (var i = 0; i < n; i++) { 10 | if (count % 5 === 0) { 11 | positions.push([Math.random() * 2 - 1, Math.random() * 2 - 1]); 12 | } else if (count % 5 === 1) { 13 | positions.push([Math.random(), Math.random() * 2 - 1]); 14 | } else if (count % 5 === 2) { 15 | positions.push([Math.random() * 2 - 1, Math.random()]); 16 | } else if (count % 5 === 3) { 17 | positions.push([Math.random() * -1, Math.random() * -1]); 18 | } else if (count % 5 === 4) { 19 | positions.push([Math.random(), Math.random()]); 20 | } 21 | } 22 | count++; 23 | return positions; 24 | }; 25 | 26 | var positionBuffer = tween.buffer(getRandomPositions(COUNT), { duration: 1000, ease: 'expo-in-out' }); 27 | 28 | const drawParticles = tween({ 29 | vert: ` 30 | precision lowp float; 31 | attribute vec2 position; 32 | uniform float pointSize; 33 | 34 | void main() { 35 | gl_PointSize = pointSize; 36 | gl_Position = vec4(position, 0, 1); 37 | } 38 | `, 39 | frag: ` 40 | precision lowp float; 41 | void main() { 42 | if (length(gl_PointCoord.xy - 0.5) > 0.5) { 43 | discard; 44 | } 45 | gl_FragColor = vec4(1.0, 0.0, 0.0, 0.3); 46 | } 47 | `, 48 | 49 | attributes: { 50 | position: positionBuffer 51 | }, 52 | 53 | uniforms: { 54 | pointSize: 2 55 | }, 56 | 57 | count: COUNT, 58 | primitive: 'points' 59 | }); 60 | 61 | setInterval(() => { 62 | positionBuffer.update(getRandomPositions(COUNT)); 63 | }, 2000); 64 | 65 | regl.frame(() => { 66 | drawParticles(); 67 | }); 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regl-tween", 3 | "version": "1.1.2", 4 | "description": "Automatically tween buffer data in regl", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "semistandard && mocha", 8 | "example": "budo example/lots-of-points" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/mathisonian/regl-tween.git" 13 | }, 14 | "keywords": [ 15 | "regl", 16 | "animation", 17 | "tween", 18 | "webgl" 19 | ], 20 | "browserify": { 21 | "transform": [ 22 | "glslify" 23 | ] 24 | }, 25 | "author": "Matthew Conlen", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/mathisonian/regl-tween/issues" 29 | }, 30 | "homepage": "https://github.com/mathisonian/regl-tween#readme", 31 | "devDependencies": { 32 | "budo": "^9.0.0", 33 | "expect": "^1.20.2", 34 | "gl": "^4.0.2", 35 | "glslify": "^5.1.0", 36 | "mocha": "^3.0.2", 37 | "regl": "^1.0.0", 38 | "semistandard": "^8.0.0" 39 | }, 40 | "dependencies": { 41 | "camelcase": "^3.0.0", 42 | "eases": "^1.0.8", 43 | "glsl-easings": "^1.0.0", 44 | "lerp": "^1.0.3", 45 | "strip-comments": "^0.3.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/easings/compiled-easings.js: -------------------------------------------------------------------------------- 1 | module.exports = "#define GLSLIFY 1\n#ifndef PI\n#define PI 3.141592653589793\n#endif\n\nfloat backOut_1(float t) {\n float f = t < 0.5\n ? 2.0 * t\n : 1.0 - (2.0 * t - 1.0);\n\n float g = pow(f, 3.0) - f * sin(f * PI);\n\n return t < 0.5\n ? 0.5 * g\n : 0.5 * (1.0 - g) + 0.5;\n}\n\n#ifndef PI\n#define PI 3.141592653589793\n#endif\n\nfloat backIn(float t) {\n return pow(t, 3.0) - t * sin(t * PI);\n}\n\n#ifndef PI\n#define PI 3.141592653589793\n#endif\n\nfloat backOut_0(float t) {\n float f = 1.0 - t;\n return 1.0 - (pow(f, 3.0) - f * sin(f * PI));\n}\n\n#ifndef PI\n#define PI 3.141592653589793\n#endif\n\nfloat bounceOut(float t) {\n const float a = 4.0 / 11.0;\n const float b = 8.0 / 11.0;\n const float c = 9.0 / 10.0;\n\n const float ca = 4356.0 / 361.0;\n const float cb = 35442.0 / 1805.0;\n const float cc = 16061.0 / 1805.0;\n\n float t2 = t * t;\n\n return t < a\n ? 7.5625 * t2\n : t < b\n ? 9.075 * t2 - 9.9 * t + 3.4\n : t < c\n ? ca * t2 - cb * t + cc\n : 10.8 * t * t - 20.52 * t + 10.72;\n}\n\nfloat bounceInOut(float t) {\n return t < 0.5\n ? 0.5 * (1.0 - bounceOut(1.0 - t * 2.0))\n : 0.5 * bounceOut(t * 2.0 - 1.0) + 0.5;\n}\n\nfloat bounceIn(float t) {\n return 1.0 - bounceOut(1.0 - t);\n}\n\nfloat circularInOut(float t) {\n return t < 0.5\n ? 0.5 * (1.0 - sqrt(1.0 - 4.0 * t * t))\n : 0.5 * (sqrt((3.0 - 2.0 * t) * (2.0 * t - 1.0)) + 1.0);\n}\n\nfloat circularIn(float t) {\n return 1.0 - sqrt(1.0 - t * t);\n}\n\nfloat circularOut(float t) {\n return sqrt((2.0 - t) * t);\n}\n\nfloat cubicInOut(float t) {\n return t < 0.5\n ? 4.0 * t * t * t\n : 0.5 * pow(2.0 * t - 2.0, 3.0) + 1.0;\n}\n\nfloat cubicIn(float t) {\n return t * t * t;\n}\n\nfloat cubicOut(float t) {\n float f = t - 1.0;\n return f * f * f + 1.0;\n}\n\n#ifndef HALF_PI\n#define HALF_PI 1.5707963267948966\n#endif\n\nfloat elasticInOut(float t) {\n return t < 0.5\n ? 0.5 * sin(+13.0 * HALF_PI * 2.0 * t) * pow(2.0, 10.0 * (2.0 * t - 1.0))\n : 0.5 * sin(-13.0 * HALF_PI * ((2.0 * t - 1.0) + 1.0)) * pow(2.0, -10.0 * (2.0 * t - 1.0)) + 1.0;\n}\n\n#ifndef HALF_PI\n#define HALF_PI 1.5707963267948966\n#endif\n\nfloat elasticIn(float t) {\n return sin(13.0 * t * HALF_PI) * pow(2.0, 10.0 * (t - 1.0));\n}\n\n#ifndef HALF_PI\n#define HALF_PI 1.5707963267948966\n#endif\n\nfloat elasticOut(float t) {\n return sin(-13.0 * (t + 1.0) * HALF_PI) * pow(2.0, -10.0 * t) + 1.0;\n}\n\nfloat exponentialInOut(float t) {\n return t == 0.0 || t == 1.0\n ? t\n : t < 0.5\n ? +0.5 * pow(2.0, (20.0 * t) - 10.0)\n : -0.5 * pow(2.0, 10.0 - (t * 20.0)) + 1.0;\n}\n\nfloat exponentialIn(float t) {\n return t == 0.0 ? t : pow(2.0, 10.0 * (t - 1.0));\n}\n\nfloat exponentialOut(float t) {\n return t == 1.0 ? t : 1.0 - pow(2.0, -10.0 * t);\n}\n\nfloat linear(float t) {\n return t;\n}\n\nfloat quadraticInOut(float t) {\n float p = 2.0 * t * t;\n return t < 0.5 ? p : -p + (4.0 * t) - 1.0;\n}\n\nfloat quadraticIn(float t) {\n return t * t;\n}\n\nfloat quadraticOut(float t) {\n return -t * (t - 2.0);\n}\n\nfloat quarticInOut(float t) {\n return t < 0.5\n ? +8.0 * pow(t, 4.0)\n : -8.0 * pow(t - 1.0, 4.0) + 1.0;\n}\n\nfloat quarticIn(float t) {\n return pow(t, 4.0);\n}\n\nfloat quarticOut(float t) {\n return pow(t - 1.0, 3.0) * (1.0 - t) + 1.0;\n}\n\nfloat qinticInOut(float t) {\n return t < 0.5\n ? +16.0 * pow(t, 5.0)\n : -0.5 * pow(2.0 * t - 2.0, 5.0) + 1.0;\n}\n\nfloat qinticIn(float t) {\n return pow(t, 5.0);\n}\n\nfloat qinticOut(float t) {\n return 1.0 - (pow(t - 1.0, 5.0));\n}\n\n#ifndef PI\n#define PI 3.141592653589793\n#endif\n\nfloat sineInOut(float t) {\n return -0.5 * (cos(PI * t) - 1.0);\n}\n\n#ifndef HALF_PI\n#define HALF_PI 1.5707963267948966\n#endif\n\nfloat sineIn(float t) {\n return sin((t - 1.0) * HALF_PI) + 1.0;\n}\n\n#ifndef HALF_PI\n#define HALF_PI 1.5707963267948966\n#endif\n\nfloat sineOut(float t) {\n return sin(t * HALF_PI);\n}" 2 | -------------------------------------------------------------------------------- /src/easings/easings.glsl: -------------------------------------------------------------------------------- 1 | #pragma glslify: backInOut = require(glsl-easings/back-in-out); 2 | #pragma glslify: backIn = require(glsl-easings/back-in); 3 | #pragma glslify: backInOut = require(glsl-easings/back-out); 4 | #pragma glslify: bounceInOut = require(glsl-easings/bounce-in-out); 5 | #pragma glslify: bounceIn = require(glsl-easings/bounce-in); 6 | #pragma glslify: bounceOut = require(glsl-easings/bounce-out); 7 | #pragma glslify: circularInOut = require(glsl-easings/circular-in-out); 8 | #pragma glslify: circularIn = require(glsl-easings/circular-in); 9 | #pragma glslify: circularOut = require(glsl-easings/circular-out); 10 | #pragma glslify: cubicInOut = require(glsl-easings/cubic-in-out); 11 | #pragma glslify: cubicIn = require(glsl-easings/cubic-in); 12 | #pragma glslify: cubicOut = require(glsl-easings/cubic-out); 13 | #pragma glslify: elasticInOut = require(glsl-easings/elastic-in-out); 14 | #pragma glslify: elasticIn = require(glsl-easings/elastic-in); 15 | #pragma glslify: elasticOut = require(glsl-easings/elastic-out); 16 | #pragma glslify: exponentialInOut = require(glsl-easings/exponential-in-out); 17 | #pragma glslify: exponentialIn = require(glsl-easings/exponential-in); 18 | #pragma glslify: exponentialOut = require(glsl-easings/exponential-out); 19 | #pragma glslify: ease = require(glsl-easings/linear); 20 | #pragma glslify: quadraticInOut = require(glsl-easings/quadratic-in-out); 21 | #pragma glslify: quadraticIn = require(glsl-easings/quadratic-in); 22 | #pragma glslify: quadraticOut = require(glsl-easings/quadratic-out); 23 | #pragma glslify: quarticInOut = require(glsl-easings/quartic-in-out); 24 | #pragma glslify: quarticIn = require(glsl-easings/quartic-in); 25 | #pragma glslify: quarticOut = require(glsl-easings/quartic-out); 26 | #pragma glslify: quinticInOut = require(glsl-easings/quintic-in-out); 27 | #pragma glslify: quinticIn = require(glsl-easings/quintic-in); 28 | #pragma glslify: quinticOut = require(glsl-easings/quintic-out); 29 | #pragma glslify: sineInOut = require(glsl-easings/sine-in-out); 30 | #pragma glslify: sineIn = require(glsl-easings/sine-in); 31 | #pragma glslify: sineOut = require(glsl-easings/sine-out); 32 | -------------------------------------------------------------------------------- /src/easings/index.js: -------------------------------------------------------------------------------- 1 | 2 | var camelCase = require('camelcase'); 3 | var eases = require('eases'); 4 | var glslEasings = require('./compiled-easings'); 5 | 6 | var nameMap = [ 7 | ['quad-', 'quadratic-'], 8 | ['circ-', 'circular-'], 9 | ['expo-', 'exponential-'], 10 | ['quart-', 'quartic-'], 11 | ['quint-', 'quintic-'] 12 | ]; 13 | 14 | var getEasingName = function (name) { 15 | return camelCase(name); 16 | }; 17 | 18 | var getGLSLEasingName = function (name) { 19 | nameMap.forEach(function (r) { 20 | name = name.replace(r[0], r[1]); 21 | }); 22 | return camelCase(name); 23 | }; 24 | 25 | var getEasingFunction = function (name) { 26 | return eases[camelCase(name)]; 27 | }; 28 | 29 | var getShaderEasings = function (name) { 30 | return glslEasings; 31 | }; 32 | 33 | module.exports = { 34 | getEasingFunction: getEasingFunction, 35 | getEasingName: getEasingName, 36 | getShaderEasings: getShaderEasings, 37 | getGLSLEasingName: getGLSLEasingName 38 | }; 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var strip = require('strip-comments'); 2 | var lerp = require('lerp'); 3 | var easings = require('./easings'); 4 | 5 | var namespace = '_rtb'; 6 | 7 | var getPreviousName = function (name, type) { 8 | return namespace + '_' + type + '_previous_' + name; 9 | }; 10 | 11 | var getCurrentName = function (name, type) { 12 | return namespace + '_' + type + '_current_' + name; 13 | }; 14 | 15 | var getNextName = function (name, type) { 16 | return namespace + '_' + type + '_next_' + name; 17 | }; 18 | 19 | var getTimeName = function (name, type) { 20 | return namespace + '_' + type + '_t_' + name; 21 | }; 22 | 23 | var transformShader = function (name, shader, type, ease) { 24 | // 25 | // [X] Strip comments 26 | // [X] Replace declaration with prev / next / current declarations 27 | // [X] Initialize timestamp 28 | // [X] Initialize current in the main() function 29 | // [X] Replace all old usages with current 30 | // 31 | 32 | var easingName = easings.getGLSLEasingName(ease); 33 | var glslEasings = easings.getShaderEasings(); 34 | 35 | shader = strip(shader); 36 | var prev = getPreviousName(name, type); 37 | var next = getNextName(name, type); 38 | var current = getCurrentName(name, type); 39 | var t = getTimeName(name, type); 40 | 41 | // first occurence of name: 42 | var re = new RegExp(';([^;]*' + name + '[^;]*;)'); 43 | var found = shader.match(re); 44 | 45 | if (!found || found.length < 2) { 46 | return shader; 47 | } 48 | 49 | var invocation = found[1]; 50 | var prevDeclare = invocation.replace(name, prev); 51 | var nextDeclare = invocation.replace(name, next); 52 | var currentDeclare = '\n' + invocation.replace(name, current).replace('attribute', '').replace('uniform', '').trim(); 53 | var timeDeclare = '\nuniform float ' + t + ';'; 54 | 55 | var replaceString = prevDeclare + nextDeclare + currentDeclare + timeDeclare; 56 | 57 | shader = shader.replace(invocation, replaceString); 58 | 59 | // add the mix() call 60 | re = new RegExp('main\\s*\\(\\s*\\)\\s*{'); 61 | found = shader.match(re); 62 | 63 | if (found && found.length) { 64 | var mixString = current + ' = ' + 'mix(' + prev + ', ' + next + ', ' + easingName + '(' + t + '));'; 65 | shader = shader.replace(found[0], found[0] + '\n ' + mixString); 66 | } 67 | 68 | // replace all the old names 69 | re = new RegExp('\\b' + name + '\\b', 'g'); 70 | shader = shader.replace(re, current); 71 | shader = glslEasings + '\n' + shader; 72 | 73 | return shader; 74 | }; 75 | 76 | var TweenBuffer = function (data, options) { 77 | this.data = data; 78 | this.previousData = data; 79 | this.options = Object.assign({}, { 80 | duration: 750, 81 | ease: 'quad-in-out' 82 | }, options || {}); 83 | }; 84 | 85 | module.exports = function (regl) { 86 | var tween = function (commandObject) { 87 | commandObject.uniforms = commandObject.uniforms || {}; 88 | commandObject.attributes = commandObject.attributes || {}; 89 | commandObject.vert = commandObject.vert || ''; 90 | commandObject.frag = commandObject.frag || ''; 91 | if (!commandObject.vert || !commandObject.frag) { 92 | throw new Error('Must provide vert and frag shaders!'); 93 | } 94 | 95 | var tweenedProps = { 96 | uniforms: [], 97 | attributes: [] 98 | }; 99 | var buffers = { 100 | uniforms: {}, 101 | attributes: {} 102 | }; 103 | var tweenBuffers = { 104 | uniforms: {}, 105 | attributes: {} 106 | }; 107 | var timers = { 108 | uniforms: {}, 109 | attributes: {} 110 | }; 111 | var tweenFlags = { 112 | uniforms: {}, 113 | attributes: {} 114 | }; 115 | var startTimes = { 116 | uniforms: {}, 117 | attributes: {} 118 | }; 119 | 120 | var tweenBufferUpdate = function (data) { 121 | var timer = timers[this.type][this.key]; 122 | 123 | if (timer < 1.0) { 124 | var eased = easings.getEasingFunction(this.options.ease)(timer); 125 | var previousData = this.previousData; 126 | this.previousData = this.data.map(function (d, i) { 127 | return d.map(function (elt, j) { 128 | return lerp(previousData[i][j], elt, eased); 129 | }); 130 | }); 131 | } else { 132 | this.previousData = this.data; 133 | } 134 | 135 | this.data = data; 136 | buffers[this.type][this.key].previous({ 137 | usage: 'dynamic', 138 | data: this.previousData 139 | }); 140 | 141 | buffers[this.type][this.key].next({ 142 | usage: 'dynamic', 143 | data: data 144 | }); 145 | tweenFlags[this.type][this.key] = true; 146 | }; 147 | 148 | var initialize = function (type) { 149 | Object.keys(commandObject[type] || {}).filter(function (key) { 150 | return commandObject[type][key] instanceof TweenBuffer; 151 | }).map(function (key) { 152 | tweenedProps[type].push(key); 153 | var tweenBuffer = commandObject[type][key]; 154 | tweenBuffer.type = type; 155 | tweenBuffer.key = key; 156 | tweenBuffer.update = tweenBufferUpdate; 157 | tweenBuffers[type][key] = tweenBuffer; 158 | timers[type][key] = 1.0; 159 | tweenFlags[type][key] = false; 160 | buffers[type][key] = { 161 | previous: regl.buffer({ 162 | usage: 'dynamic', 163 | data: tweenBuffer.data 164 | }), 165 | next: regl.buffer({ 166 | usage: 'dynamic', 167 | data: tweenBuffer.data 168 | }) 169 | }; 170 | }); 171 | }; 172 | 173 | initialize('attributes'); 174 | initialize('uniforms'); 175 | 176 | var transform = function (type) { 177 | tweenedProps[type].forEach(function (attr) { 178 | var tweenBuffer = tweenBuffers[type][attr]; 179 | var duration = tweenBuffer.options.duration; 180 | commandObject.vert = transformShader(attr, commandObject.vert || '', type, tweenBuffer.options.ease); 181 | delete commandObject.attributes[attr]; 182 | commandObject[type][getPreviousName(attr, type)] = buffers[type][attr].previous; 183 | commandObject[type][getNextName(attr, type)] = buffers[type][attr].next; 184 | commandObject.uniforms[getTimeName(attr, type)] = function (context, props, batchId) { 185 | if (tweenFlags[type][attr]) { 186 | timers[type][attr] = 0.0; 187 | startTimes[type][attr] = context.time; 188 | } 189 | 190 | // we go up to maxT to allow for some random delays 191 | var startTime = startTimes[type][attr]; 192 | var t = timers[type][attr]; 193 | if (t < 1.0) { 194 | t = Math.min(1.0, 1000 * (context.time - startTime) / duration); 195 | } 196 | 197 | timers[type][attr] = t; 198 | tweenFlags[type][attr] = false; 199 | return t; 200 | }; 201 | }); 202 | }; 203 | 204 | transform('attributes'); 205 | transform('uniforms'); 206 | 207 | return regl(commandObject); 208 | }; 209 | 210 | tween.buffer = function (data, options) { 211 | return new TweenBuffer(data, options); 212 | }; 213 | 214 | return tween; 215 | }; 216 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it */ 2 | 3 | var expect = require('expect'); 4 | var regl = require('regl')(require('gl')(256, 256)); 5 | var tween = require('..')(regl); 6 | 7 | describe('regl-tween-buffer tests', () => { 8 | it('should transform shader correctly', () => { 9 | var tweened = tween({ 10 | vert: ` 11 | precision mediump float; 12 | attribute vec3 color; 13 | varying vec3 vcolor; 14 | void main() { 15 | gl_Position = vec4(0.0, 0.0, 0.0, 1.0); 16 | vcolor = color; 17 | } 18 | `, 19 | 20 | frag: ` 21 | precision mediump float; 22 | varying vec3 vcolor; 23 | void main() { 24 | gl_FragColor = vec4(vcolor, 1.0); 25 | } 26 | `, 27 | 28 | attributes: { 29 | color: tween.buffer([]) 30 | } 31 | }); 32 | 33 | var expectedVert = ` 34 | precision mediump float; 35 | attribute vec3 _rtb_previous_color; 36 | attribute vec3 _rtb_next_color; 37 | vec3 _rtb_current_color; 38 | uniform float _rtb_t_color; 39 | varying vec3 vcolor; 40 | void main() { 41 | _rtb_current_color = mix(_rtb_previous_color, _rtb_next_color, _rtb_t_color); 42 | gl_Position = vec4(0.0, 0.0, 0.0, 1.0); 43 | vcolor = _rtb_current_color; 44 | } 45 | `; 46 | 47 | expect(tweened.vert.trim()).toEqual(expectedVert.trim()); 48 | }); 49 | }); 50 | --------------------------------------------------------------------------------