mirror of
https://github.com/konvajs/konva.git
synced 2026-03-03 16:58:33 +08:00
Added ripple filter.
This commit is contained in:
@@ -26,6 +26,7 @@ module.exports = function(grunt) {
|
|||||||
'src/filters/Threshold.js',
|
'src/filters/Threshold.js',
|
||||||
'src/filters/Sepia.js',
|
'src/filters/Sepia.js',
|
||||||
'src/filters/Solarize.js',
|
'src/filters/Solarize.js',
|
||||||
|
'src/filters/Ripple.js',
|
||||||
|
|
||||||
// core
|
// core
|
||||||
'src/Animation.js',
|
'src/Animation.js',
|
||||||
|
|||||||
296
src/filters/Ripple.js
Normal file
296
src/filters/Ripple.js
Normal file
@@ -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<tSize; theta+=1 ){
|
||||||
|
sin = Math.sin(theta*conversion);
|
||||||
|
cos = Math.cos(theta*conversion);
|
||||||
|
for( radius=0; radius<rSize; radius+=1 ){
|
||||||
|
x = xMid+rMax*radius/rSize*cos;
|
||||||
|
y = yMid+rMax*radius/rSize*sin;
|
||||||
|
if( x <= 1 ){ x = 1; }
|
||||||
|
if( x >= 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<xSize; x+=1 ){
|
||||||
|
for( y=0; y<ySize; y+=1 ){
|
||||||
|
dx = x - xMid;
|
||||||
|
dy = y - yMid;
|
||||||
|
radius = Math.sqrt(dx*dx + dy*dy)*rSize/rMax;
|
||||||
|
theta = (Math.atan2(dy,dx)*180/Math.PI + 360 + phaseShift)%360;
|
||||||
|
theta = theta*tSize/360;
|
||||||
|
|
||||||
|
// 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 = theta - 0.5;
|
||||||
|
x2 = theta + 0.5;
|
||||||
|
x1i = Math.floor(x1);
|
||||||
|
x2i = Math.floor(x2);
|
||||||
|
y1 = radius - 0.5;
|
||||||
|
y2 = radius + 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 = (y*xSize + x)*4;
|
||||||
|
dstPixels[i+0] = r;
|
||||||
|
dstPixels[i+1] = g;
|
||||||
|
dstPixels[i+2] = b;
|
||||||
|
dstPixels[i+3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
//Kinetic.Filters.ToPolar = Kinetic.Util._FilterWrapDoubleBuffer(ToPolar);
|
||||||
|
//Kinetic.Filters.FromPolar = Kinetic.Util._FilterWrapDoubleBuffer(FromPolar);
|
||||||
|
|
||||||
|
// create a temporary canvas for working - shared between multiple calls
|
||||||
|
var tempCanvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ripple Filter.
|
||||||
|
* @function
|
||||||
|
* @author ippo615
|
||||||
|
* @memberof Kinetic.Filters
|
||||||
|
*/
|
||||||
|
Kinetic.Filters.Ripple = function(imageData){
|
||||||
|
var xSize = imageData.width,
|
||||||
|
ySize = imageData.height;
|
||||||
|
var rippleSize = Math.ceil( this.rippleSize() || 5 );
|
||||||
|
var rippleOffset = this.rippleOffset() || 0;
|
||||||
|
|
||||||
|
// Work with our shared buffer canvas
|
||||||
|
tempCanvas.width = xSize;
|
||||||
|
tempCanvas.height = ySize;
|
||||||
|
var scratchData = tempCanvas.getContext('2d').getImageData(0,0,xSize,ySize);
|
||||||
|
|
||||||
|
// Convert thhe original to polar coordinates
|
||||||
|
ToPolar( imageData, scratchData, {
|
||||||
|
polarCenterX:xSize/2,
|
||||||
|
polarCenterY:ySize/2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy/repeat a section along the r axis for effect
|
||||||
|
//var nCopies = 8;
|
||||||
|
//var sectionHeight = Math.floor(ySize/nCopies);
|
||||||
|
var nCopies = Math.floor(ySize/rippleSize);
|
||||||
|
var sectionHeight = rippleSize;
|
||||||
|
var x,y,yoff,i, r,g,b,a, srcPos, dstPos;
|
||||||
|
for( x=0; x<xSize; x+=1 ){
|
||||||
|
for( y=0; y<sectionHeight; y+=1 ){
|
||||||
|
yoff = Math.round(y+rippleOffset) % ySize;
|
||||||
|
srcPos = (xSize*yoff+x)*4;
|
||||||
|
r = scratchData.data[srcPos+0];
|
||||||
|
g = scratchData.data[srcPos+1];
|
||||||
|
b = scratchData.data[srcPos+2];
|
||||||
|
a = scratchData.data[srcPos+3];
|
||||||
|
for( i=1; i<nCopies; i+=1 ){
|
||||||
|
dstPos = (xSize*(sectionHeight*i+yoff)+x)*4;
|
||||||
|
scratchData.data[dstPos+0] = r;
|
||||||
|
scratchData.data[dstPos+1] = g;
|
||||||
|
scratchData.data[dstPos+2] = b;
|
||||||
|
scratchData.data[dstPos+3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back from polar coordinates
|
||||||
|
FromPolar(scratchData,imageData,{});
|
||||||
|
};
|
||||||
|
|
||||||
|
Kinetic.Factory.addFilterGetterSetter(Kinetic.Node, 'rippleSize', 16);
|
||||||
|
Kinetic.Factory.addFilterGetterSetter(Kinetic.Node, 'rippleOffset', 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get/set ripple size
|
||||||
|
* @name rippleSize
|
||||||
|
* @method
|
||||||
|
* @memberof Kinetic.Node.prototype
|
||||||
|
* @param {Integer} radius
|
||||||
|
* @returns {Integer}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get/set ripple offset
|
||||||
|
* @name rippleOffset
|
||||||
|
* @method
|
||||||
|
* @memberof Kinetic.Node.prototype
|
||||||
|
* @param {Integer} offset
|
||||||
|
* @returns {Integer}
|
||||||
|
*/
|
||||||
|
})();
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
<script src="unit/filters/Sepia-test.js"></script>
|
<script src="unit/filters/Sepia-test.js"></script>
|
||||||
<script src="unit/filters/Emboss-test.js"></script>
|
<script src="unit/filters/Emboss-test.js"></script>
|
||||||
<script src="unit/filters/Solarize-test.js"></script>
|
<script src="unit/filters/Solarize-test.js"></script>
|
||||||
|
<script src="unit/filters/Ripple-test.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<!--=============== functional tests ================-->
|
<!--=============== functional tests ================-->
|
||||||
|
|||||||
137
test/unit/filters/Ripple-test.js
Normal file
137
test/unit/filters/Ripple-test.js
Normal file
@@ -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';
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user