From ff952bf9589fede861fe716a84bb5549d95f0f73 Mon Sep 17 00:00:00 2001 From: ippo615 Date: Sat, 4 Jan 2014 23:21:26 -0500 Subject: [PATCH] Added ripple filter. --- Gruntfile.js | 1 + src/filters/Ripple.js | 296 +++++++++++++++++++++++++++++++ test/runner.html | 1 + test/unit/filters/Ripple-test.js | 137 ++++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 src/filters/Ripple.js create mode 100644 test/unit/filters/Ripple-test.js diff --git a/Gruntfile.js b/Gruntfile.js index f49eb903..ba373ac5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,6 +26,7 @@ module.exports = function(grunt) { 'src/filters/Threshold.js', 'src/filters/Sepia.js', 'src/filters/Solarize.js', + 'src/filters/Ripple.js', // core 'src/Animation.js', diff --git a/src/filters/Ripple.js b/src/filters/Ripple.js new file mode 100644 index 00000000..4d1a4b38 --- /dev/null +++ b/src/filters/Ripple.js @@ -0,0 +1,296 @@ +(function () { + + /* + * ToPolar Filter. Converts image data to polar coordinates. Performs + * w*h*4 pixel reads and w*h pixel writes. The r axis is placed along + * what would be the y axis and the theta axis along the x axis. + * @function + * @author ippo615 + * @memberof Kinetic.Filters + * @param {ImageData} src, the source image data (what will be transformed) + * @param {ImageData} dst, the destination image data (where it will be saved) + * @param {Object} opt + * @param {Number} [opt.polarCenterX] horizontal location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarCenterY] vertical location for the center of the circle, + * default is in the middle + */ + + var ToPolar = function(src,dst,opt){ + + var srcPixels = src.data, + dstPixels = dst.data, + xSize = src.width, + ySize = src.height, + xMid = opt.polarCenterX || xSize/2, + yMid = opt.polarCenterY || ySize/2, + i, m, x, y, k, tmp, r=0,g=0,b=0,a=0; + + // Find the largest radius + var rad, rMax = Math.sqrt( xMid*xMid + yMid*yMid ); + x = xSize - xMid; + y = ySize - yMid; + rad = Math.sqrt( x*x + y*y ); + rMax = (rad > rMax)?rad:rMax; + + // We'll be uisng y as the radius, and x as the angle (theta=t) + var rSize = ySize, + tSize = xSize, + radius, theta; + + // We want to cover all angles (0-360) and we need to convert to + // radians (*PI/180) + var conversion = 360/tSize*Math.PI/180, sin, cos; + + var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale; + + for( theta=0; theta= xSize-0.5 ){ x = xSize-1; } + if( y <= 1 ){ y = 1; } + if( y >= ySize-0.5 ){ y = ySize-1; } + + // Interpolate x and y by going +-0.5 around the pixel's central point + // this gives us the 4 nearest pixels to our 1x1 non-aligned pixel. + // We average the vaules of those pixels based on how much of our + // non-aligned pixel overlaps each of them. + x1 = x - 0.5; + x2 = x + 0.5; + x1i = Math.floor(x1); + x2i = Math.floor(x2); + y1 = y - 0.5; + y2 = y + 0.5; + y1i = Math.floor(y1); + y2i = Math.floor(y2); + + scale = (1-(x1-x1i))*(1-(y1-y1i)); + i = (y1i*xSize + x1i)*4; + r = srcPixels[i+0]*scale; + g = srcPixels[i+1]*scale; + b = srcPixels[i+2]*scale; + a = srcPixels[i+3]*scale; + + scale = (1-(x1-x1i))*(y2-y2i); + i = (y2i*xSize + x1i)*4; + r += srcPixels[i+0]*scale; + g += srcPixels[i+1]*scale; + b += srcPixels[i+2]*scale; + a += srcPixels[i+3]*scale; + + scale = (x2-x2i)*(y2-y2i); + i = (y2i*xSize + x2i)*4; + r += srcPixels[i+0]*scale; + g += srcPixels[i+1]*scale; + b += srcPixels[i+2]*scale; + a += srcPixels[i+3]*scale; + + scale = (x2-x2i)*(1-(y1-y1i)); + i = (y1i*xSize + x2i)*4; + r += srcPixels[i+0]*scale; + g += srcPixels[i+1]*scale; + b += srcPixels[i+2]*scale; + a += srcPixels[i+3]*scale; + + // Store it + //i = (theta * xSize + radius) * 4; + i = (theta + radius*xSize) * 4; + dstPixels[i+0] = r; + dstPixels[i+1] = g; + dstPixels[i+2] = b; + dstPixels[i+3] = a; + + } + } + }; + + /* + * FromPolar Filter. Converts image data from polar coordinates back to rectangular. + * Performs w*h*4 pixel reads and w*h pixel writes. + * @function + * @author ippo615 + * @memberof Kinetic.Filters + * @param {ImageData} src, the source image data (what will be transformed) + * @param {ImageData} dst, the destination image data (where it will be saved) + * @param {Object} opt + * @param {Number} [opt.polarCenterX] horizontal location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarCenterY] vertical location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarRotation] amount to rotate the image counterclockwis, + * 0 is no rotation, 360 degrees is a full rotation + */ + + var FromPolar = function(src,dst,opt){ + + var srcPixels = src.data, + dstPixels = dst.data, + xSize = src.width, + ySize = src.height, + xMid = opt.polarCenterX || xSize/2, + yMid = opt.polarCenterY || ySize/2, + i, m, x, y, dx, dy, k, tmp, r=0,g=0,b=0,a=0; + + + // Find the largest radius + var rad, rMax = Math.sqrt( xMid*xMid + yMid*yMid ); + x = xSize - xMid; + y = ySize - yMid; + rad = Math.sqrt( x*x + y*y ); + rMax = (rad > rMax)?rad:rMax; + + // We'll be uisng x as the radius, and y as the angle (theta=t) + var rSize = ySize, + tSize = xSize, + radius, theta, + phaseShift = opt.polarRotation || 0; + + // We need to convert to degrees and we need to make sure + // it's between (0-360) + // var conversion = tSize/360*180/Math.PI; + var conversion = tSize/360*180/Math.PI; + + var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale; + + for( x=0; x + diff --git a/test/unit/filters/Ripple-test.js b/test/unit/filters/Ripple-test.js new file mode 100644 index 00000000..940c01a2 --- /dev/null +++ b/test/unit/filters/Ripple-test.js @@ -0,0 +1,137 @@ +suite('Ripple', function() { + // ====================================================== + test('basic ripple', function(done) { + var stage = addStage(); + + var imageObj = new Image(); + imageObj.onload = function() { + + var layer = new Kinetic.Layer(); + darth = new Kinetic.Image({ + x: 10, + y: 10, + image: imageObj, + draggable: true + }); + + layer.add(darth); + stage.add(layer); + + darth.cache(); + darth.filters([Kinetic.Filters.Ripple]); + darth.rippleSize(10); + + assert.equal(darth.rippleSize(), 10); + assert.equal(darth._filterUpToDate, false); + + layer.draw(); + + assert.equal(darth._filterUpToDate, true); + + darth.rippleSize(20); + + assert.equal(darth.rippleSize(), 20); + assert.equal(darth._filterUpToDate, false); + + layer.draw(); + + assert.equal(darth._filterUpToDate, true); + + done(); + }; + imageObj.src = 'assets/lion.png'; + + }); + + // ====================================================== + test('tween ripple offset', function(done) { + var stage = addStage(); + + var imageObj = new Image(); + imageObj.onload = function() { + + var layer = new Kinetic.Layer(); + darth = new Kinetic.Image({ + x: 10, + y: 10, + image: imageObj, + draggable: true + }); + + layer.add(darth); + stage.add(layer); + + darth.cache(); + darth.filters([Kinetic.Filters.Ripple]); + darth.rippleSize(16); + darth.rippleOffset(0); + layer.draw(); + + var tween = new Kinetic.Tween({ + node: darth, + duration: 2.0, + rippleOffset: 32, + //rippleSize: 64, + easing: Kinetic.Easings.EaseInOut + }); + + darth.on('mouseover', function() { + tween.play(); + }); + + darth.on('mouseout', function() { + tween.reverse(); + }); + + done(); + + }; + imageObj.src = 'assets/lion.png'; + }); + + // ====================================================== + test('tween ripple size', function(done) { + var stage = addStage(); + + var imageObj = new Image(); + imageObj.onload = function() { + + var layer = new Kinetic.Layer(); + darth = new Kinetic.Image({ + x: 10, + y: 10, + image: imageObj, + draggable: true + }); + + layer.add(darth); + stage.add(layer); + + darth.cache(); + darth.filters([Kinetic.Filters.Ripple]); + darth.rippleSize(16); + darth.rippleOffset(0); + layer.draw(); + + var tween = new Kinetic.Tween({ + node: darth, + duration: 2.0, + rippleSize: 64, + easing: Kinetic.Easings.EaseInOut + }); + + darth.on('mouseover', function() { + tween.play(); + }); + + darth.on('mouseout', function() { + tween.reverse(); + }); + + done(); + + }; + imageObj.src = 'assets/lion.png'; + }); + +}); \ No newline at end of file