Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pinch/pucker an image in canvas

How can I pinch/pucker some area of an image in canvas?

I've made a solar system animation some time ago, and I started rewriting it. Now, I want to add gravity effect to masses. To make the effect visible, I turned the background into a grid and I'll be modifying it.

Desired effect is something like this (made in PS)

enter image description here

enter image description here


context.background("rgb(120,130,145)");
context.grid(25, "rgba(255,255,255,.1)");

var sun = {
    fill        : "rgb(220,210,120)",
    radius      : 30,
    boundingBox : 30*2 + 3*2,
    position    : {
        x       : 200,
        y       : 200,
    },
};
sun.img = saveToImage(sun);

context.drawImage(sun.img, sun.position.x - sun.boundingBox/2, sun.position.y - sun.boundingBox/2);

jsFiddle


Update: I've done some googling and found some resources, but since I've never done pixel manipulation before, I can't put these together.

Pixel Distortions with Bilinear Filtration in HTML5 Canvas | Splashnology.com (functions only)

glfx.js (WebGL library with demos)

JSFiddle (spherize, zoom, twirl examples)

The spherize effect in inverted form would be good for the job, I guess.

like image 701
akinuri Avatar asked Oct 30 '22 16:10

akinuri


1 Answers

UPDATED answer I have improved the performance significantly but reduced the flexibility.

To get a pinch effect you need to use a mask and then redraw the image with the mask. In this case you use a circular mask that you shrink as you draw zoomed in or out copies of the original. The effect is a buldge or pinch.

There is a quality setting that will give you from sub pixel rendering up to very rough. As with these things you sacrifice speed for quality.

I would not recommend this as a final solution to your requirements because of the inconsistent rendering speed between hardware and browsers.

For consistent results you need to use webGL. If I get time I will write a shader to do that if there is not already on on ShaderToy

So this is a pure canvas 2d solution. Canvas 2d can do anything, it just cant do it as quickly as webGL but it can come close.

UPDATE: Have re written example to improve the speed. Now runs a lot faster using clip rather than a pixel mask. Though new version is limited to pinch bulge on both axis at the same time.

See code comments for more info. I have tried to explain it best I can, if you have question do ask. I wish I could have given you a perfect answer but canvas 2d API needs to grow up some more before things like this can be more reliable.

var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");



var createImage= function(w,h){ // create a image of requier size
    var image = document.createElement("canvas"); 
    image.width = w;
    image.height =h;
    image.ctx = image.getContext("2d");  // tack the context onto the image
    return image;
}

// amountX amountY the amount of the effect
// centerX,centerY the center of the effect
// quality the quality of the effect. The smaller the vall the higher the quallity but the slower the processing
// image, the input image
// mask an image to hold the mask. Can be a different size but that will effect quality
// result, the image onto which the effect is rendered
var pinchBuldge = function(amountX,quality,image,result){
    var w = image.width;
    var h = image.height;
    var easeW = (amountX/w)*4; // down unit 0 to 4 top to bottom
    var wh = w/2;   // half size for lazy coder
    var hh = h/2;            
    var stepUnit = (0.5/(wh))*quality;
    result.ctx.drawImage(image,0,0);
    for(i = 0; i < 0.5; i += stepUnit){  // all done in normalised size                                             
        var r = i*2;  // normalise i
        var x = r*wh;  // get the clip x destination pos relative to center
        var y = r*hh;  // get the clip x  destination pos relative to center
        var xw = w-(x*2);  // get the clip  destination width
        var rx = (x)*easeW;   // get the image source pos
        var ry = (y)*easeW;
        var rw = w-(rx*2);     // get the image source size
        var rh = h-(ry*2);
        result.ctx.save();
        result.ctx.beginPath();
        result.ctx.arc(wh,hh,xw/2,0,Math.PI*2);
        result.ctx.clip();
        result.ctx.drawImage(image,rx,ry,rw,rh,0,0,w,h);
        result.ctx.restore();
    }        
    // all done;

}
// create the requiered images
var imageSize = 256; // size of image
var image = createImage(imageSize,imageSize);  // the original image
var result = createImage(imageSize,imageSize); // the result image
image.ctx.fillStyle = "#888";  // add some stuff to the image
image.ctx.fillRect(0,0,imageSize,imageSize);  // fil the background
// draw a grid  Dont need to comment this I hope it is self evident
var gridCount = 16;
var grid = imageSize/gridCount;
var styles = [["black",8],["white",2]];
styles.forEach(function(st){
    image.ctx.strokeStyle = st[0];
    image.ctx.lineWidth = st[1];
    for(var i = 0; i < 16; i++){
        image.ctx.moveTo(i*grid,0);
        image.ctx.lineTo(i*grid,imageSize)
        image.ctx.moveTo(0,i*grid);
        image.ctx.lineTo(imageSize,i*grid)
    }
    image.ctx.moveTo(0,imageSize-1);  
    image.ctx.lineTo(imageSize,imageSize-1)
    image.ctx.moveTo(imageSize-1,0);
    image.ctx.lineTo(imageSize-1,imageSize)
    image.ctx.stroke()
});


var timer = 0;
var rate = 0.05
// Quality 0.5 is sub pixel high quality
//         1 is pixel quality
//         2 is every 2 pixels
var quality = 1.5; // quality at OK

function update(){
    timer += rate;
    var effectX = Math.sin(timer)*(imageSize/4);
    pinchBuldge(effectX,quality,image,result);
    ctx.drawImage(result,0,0);
    setTimeout(update,10); // do the next one in 100 milliseconds
}
update();
.canC {
    width:256px;
    height:256px;
}
<canvas class="canC" id="canV" width=256 height=256></canvas>
like image 58
Blindman67 Avatar answered Nov 15 '22 04:11

Blindman67