在画布中收缩/凸起图像

5

如何在画布上收缩/凸起图像的某些区域?

我之前制作了一个太阳系动画,现在我开始重写它。现在,我想为物体添加引力效果。为了使效果可见,我将背景变成了网格并进行修改。

期望的效果类似于这样(在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);

{{链接1:jsFiddle}}


更新:我已经搜索了一些资源,但由于我从未进行过像素操作,因此无法将它们组合起来。

使用HTML5 Canvas进行双线性滤波的像素扭曲 | Splashnology.com(仅限函数)

glfx.js(带演示的WebGL库)

JSFiddle(spherize、zoom、twirl示例)

我想反转的球面化效果可能适用于这项工作。


Canvas只能进行仿射变换(所有变换结果都是平行四边形),因此原生画布变换无法进行捏合变形。为了捏合变形图像的矩形部分,可以将未变形的矩形分成2个(或更多)变形三角形。这里有一个以前的帖子,可以将弯曲的图像还原,但您可以将其用作对未弯曲部分进行弯曲的起点:https://dev59.com/LIvda4cB1Zd3GeqPZnna#30566272 - markE
3个回答

1
更新的答案: 我已经显著提高了性能,但降低了灵活性。
要实现捏合效果,需要使用掩码,然后使用该掩码重新绘制图像。在这种情况下,您使用一个圆形掩码,在缩放原始副本时将其缩小。效果是凸起或捏合。
有一个质量设置,可以为您提供从亚像素渲染到非常粗糙的效果。与这些事情一样,您会为质量而牺牲速度。
我不建议将此作为您需求的最终解决方案,因为硬件和浏览器之间的渲染速度不一致。
要获得一致的结果,您需要使用webGL。如果我有时间,我会编写一个着色器来做到这一点,如果ShaderToy上没有。
所以这是一个纯canvas 2d解决方案。Canvas 2d可以做任何事情,只是不能像webGL那样快,但它可以接近。 更新: 已重写示例以提高速度。现在使用剪辑而不是像素掩码运行得更快。虽然新版本仅限于同时在两个轴上进行捏合凸起。

更多信息请查看代码注释。我已尽力解释,如果您有问题,请问。我希望能给您一个完美的答案,但是canvas 2d API需要更加成熟才能使这些内容更加可靠。

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>


为什么这个程序运行得这么慢?我找到了一些运行更快的演示。你能否检查一下它们,然后写一个简单的演示程序呢?最好使用长变量名,这样我就可以跟踪程序的运行情况。我已经更新了问题。 - akinuri
@akinuri 这很慢是因为“quality”变量根据画布的宽度确定计算次数。这些计算实际上在亚像素级别进行,但画布只在像素级别绘制,因此存在大量冗余 - 即使较低的“quality”值似乎可以抵消莫尔纹。 - Brian Peacock
(哎呀)……莫尔纹效应(从而似乎提高图像质量),它只是通过以更小的单位递增来减缓整个过程,从而使得莫尔纹效应在视觉上不那么明显。请参见此代码片段:https://jsfiddle.net/BnPck/7fzxnfan/1/ - Brian Peacock
@Blindman67 对于回复晚了这么久表示抱歉,我必须休息一下(要忙学校和其他事情)。没有时间来处理这个问题。现在重新审视旧作品,我刚在Github上为这个特定问题创建了一个 repository。如果我们能在那里交流并一起想出解决方案,我会非常感激。 - akinuri

1
我有时间重新审视这个问题并提出了一个解决方案。与其直接解决问题,我首先需要了解计算和像素操作背后的数学原理。 因此,我决定使用“粒子”而不是图像/像素。一个JavaScript对象是我更熟悉的东西,所以很容易操作。 我不会尝试解释这种方法,因为我认为它是不言自明的,并且我尽量保持它简单易懂。

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

canvas.width  = 400;
canvas.height = 400;

var particles = [];

function Particle() {
    this.position = {
        actual : {
            x : 0,
            y : 0
        },
        affected : {
            x : 0,
            y : 0
        },
    };
}

// space between particles
var gridSize = 25;

var columns  = canvas.width / gridSize;
var rows     = canvas.height / gridSize;

// create grid using particles
for (var i = 0; i < rows+1; i++) {
    for (var j = 0; j < canvas.width; j += 2) {
        var p = new Particle();
        p.position.actual.x = j;
        p.position.actual.y = i * gridSize;
        p.position.affected = Object.create(p.position.actual);
        particles.push(p);
    }
}
for (var i = 0; i < columns+1; i++) {
    for (var j = 0; j < canvas.height; j += 2) {
        var p = new Particle();
        p.position.actual.x = i * gridSize;
        p.position.actual.y = j;
        p.position.affected = Object.create(p.position.actual);
        particles.push(p);
    }
}

// track mouse coordinates as it is the source of mass/gravity
var mouse = {
    x : -100,
    y : -100,
};

var effectRadius = 75;
var effectStrength = 50;

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    
    particles.forEach(function (particle) {
        // move the particle to its original position
        particle.position.affected = Object.create(particle.position.actual);
        
        // calculate the effect area
        var a = mouse.y - particle.position.actual.y;
        var b = mouse.x - particle.position.actual.x;
        var dist = Math.sqrt(a*a + b*b);
        
        // check if the particle is in the affected area
        if (dist < effectRadius) {
            
            // angle of the mouse relative to the particle
            var a = angle(particle.position.actual.x, particle.position.actual.y, mouse.x, mouse.y);
            
            // pull is stronger on the closest particle
            var strength = dist.map(0, effectRadius, effectStrength, 0);
            
            if (strength > dist) {
                strength = dist;
            }
            
            // new position for the particle that's affected by gravity
            var p = pos(particle.position.actual.x, particle.position.actual.y, a, strength);
            
            particle.position.affected.x = p.x;
            particle.position.affected.y = p.y;
        }
        
        context.beginPath();
        context.rect(particle.position.affected.x -1, particle.position.affected.y -1, 2, 2);
        context.fill();
    });
}

draw();

window.addEventListener("mousemove", function (e) {
    mouse.x = e.x - canvas.offsetLeft;
    mouse.y = e.y - canvas.offsetTop;
    requestAnimationFrame(draw);
});

function angle(originX, originY, targetX, targetY) {
    var dx = targetX - originX;
    var dy = targetY - originY;
    var theta = Math.atan2(dy, dx) * (180 / Math.PI);
    if (theta < 0) theta = 360 + theta;
    return theta;
}

Number.prototype.map = function (in_min, in_max, out_min, out_max) {
    return (this - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
};

function pos(x, y, angle, length) {
    angle *= Math.PI / 180;
    return {
        x : Math.round(x + length * Math.cos(angle)),
        y : Math.round(y + length * Math.sin(angle)),
    };
}
* {
  margin: 0;
  padding: 0;
  box-sizing: inherit;
  line-height: inherit;
  font-size: inherit;
  font-family: inherit;
}

body {
  font-family: sans-serif;
  box-sizing: border-box;
  background-color: hsl(0, 0%, 90%);
}

canvas {
  display: block;
  background: white;
  box-shadow: 0 0 2px rgba(0, 0, 0, .2), 0 1px 1px rgba(0, 0, 0, .1);
  margin: 20px auto;
}

canvas:hover {
  cursor: none;
}
<canvas id="canvas"></canvas>

我可能会在以后尝试创建旋转效果,并将它们移植到WebGL中以获得更好的性能。


更新:

现在,我正在处理旋转效果,并且已经在一定程度上使其工作。

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

canvas.width  = 400;
canvas.height = 400;

var particles = [];

function Particle() {
    this.position = {
        actual : {
            x : 0,
            y : 0
        },
        affected : {
            x : 0,
            y : 0
        },
    };
}

// space between particles
var gridSize = 25;

var columns  = canvas.width / gridSize;
var rows     = canvas.height / gridSize;

// create grid using particles
for (var i = 0; i < rows+1; i++) {
    for (var j = 0; j < canvas.width; j += 2) {
        var p = new Particle();
        p.position.actual.x = j;
        p.position.actual.y = i * gridSize;
        p.position.affected = Object.create(p.position.actual);
        particles.push(p);
    }
}
for (var i = 0; i < columns+1; i++) {
    for (var j = 0; j < canvas.height; j += 2) {
        var p = new Particle();
        p.position.actual.x = i * gridSize;
        p.position.actual.y = j;
        p.position.affected = Object.create(p.position.actual);
        particles.push(p);
    }
}

// track mouse coordinates as it is the source of mass/gravity
var mouse = {
    x : -100,
    y : -100,
};

var effectRadius = 75;
var twirlAngle   = 90;

function draw(e) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    
    particles.forEach(function (particle) {
        // move the particle to its original position
        particle.position.affected = Object.create(particle.position.actual);
        
        // calculate the effect area
        var a = mouse.y - particle.position.actual.y;
        var b = mouse.x - particle.position.actual.x;
        var dist = Math.sqrt(a*a + b*b);
        
        // check if the particle is in the affected area
        if (dist < effectRadius) {
            
            // angle of the particle relative to the mouse
            var a = angle(mouse.x, mouse.y, particle.position.actual.x, particle.position.actual.y);
            
            var strength = dist.map(0, effectRadius, twirlAngle, 0);
            
            // twirl
            a += strength;
            
            // new position for the particle that's affected by gravity
            var p = rotate(a, dist, mouse.x, mouse.y);
            
            particle.position.affected.x = p.x;
            particle.position.affected.y = p.y;
        }
        
        context.beginPath();
        context.rect(particle.position.affected.x -1, particle.position.affected.y -1, 2, 2);
        context.fillStyle = "black";
        context.fill();
    });
}

draw();

window.addEventListener("mousemove", function (e) {
    mouse.x = e.x - canvas.offsetLeft;
    mouse.y = e.y - canvas.offsetTop;
    requestAnimationFrame(draw);
});

function angle(originX, originY, targetX, targetY) {
    var dx = targetX - originX;
    var dy = targetY - originY;
    var theta = Math.atan2(dy, dx) * (180 / Math.PI);
    if (theta < 0) theta = 360 + theta;
    return theta;
}

Number.prototype.map = function (in_min, in_max, out_min, out_max) {
    return (this - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
};

function pos(x, y, angle, length) {
    angle *= Math.PI / 180;
    return {
        x : Math.round(x + length * Math.cos(angle)),
        y : Math.round(y + length * Math.sin(angle)),
    };
}

function rotate(angle, distance, originX, originY) {
    return {
        x : originX + Math.cos(angle * Math.PI/180) * distance,
        y : originY + Math.sin(angle * Math.PI/180) * distance,
    }
}
* {
  margin: 0;
  padding: 0;
  box-sizing: inherit;
  line-height: inherit;
  font-size: inherit;
  font-family: inherit;
}

body {
  font-family: sans-serif;
  box-sizing: border-box;
  background-color: hsl(0, 0%, 90%);
}

canvas {
  display: block;
  background: white;
  box-shadow: 0 0 2px rgba(0, 0, 0, .2), 0 1px 1px rgba(0, 0, 0, .1);
  margin: 20px auto;
}
<canvas id="canvas"></canvas>

旋转强度的映射存在轻微问题。我使用了与捏合效果相同的map函数,但我认为旋转不使用线性映射,而是缓和映射。将JS版本与PS滤镜进行比较。PS滤镜更加平滑。我需要重写map函数。

enter image description here

更新2:

我已经成功让它像PS滤镜一样工作。使用缓动函数,例如easeOutQuad解决了问题。享受吧 :)

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

canvas.width  = 400;
canvas.height = 400;

var particles = [];

function Particle() {
    this.position = {
        actual : {
            x : 0,
            y : 0
        },
        affected : {
            x : 0,
            y : 0
        },
    };
}

// space between particles
var gridSize = 25;

var columns  = canvas.width / gridSize;
var rows     = canvas.height / gridSize;

// create grid using particles
for (var i = 0; i < rows+1; i++) {
    for (var j = 0; j < canvas.width; j+=2) {
        var p = new Particle();
        p.position.actual.x = j;
        p.position.actual.y = i * gridSize;
        p.position.affected = Object.create(p.position.actual);
        particles.push(p);
    }
}
for (var i = 0; i < columns+1; i++) {
    for (var j = 0; j < canvas.height; j+=2) {
        var p = new Particle();
        p.position.actual.x = i * gridSize;
        p.position.actual.y = j;
        p.position.affected = Object.create(p.position.actual);
        particles.push(p);
    }
}

// track mouse coordinates as it is the source of mass/gravity
var mouse = {
    x : -100,
    y : -100,
};

var effectRadius = 75;
var twirlAngle   = 90;

function draw(e) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    
    particles.forEach(function (particle) {
        // move the particle to its original position
        particle.position.affected = Object.create(particle.position.actual);
        
        // calculate the effect area
        var a = mouse.y - particle.position.actual.y;
        var b = mouse.x - particle.position.actual.x;
        var dist = Math.sqrt(a*a + b*b);
        
        // check if the particle is in the affected area
        if (dist < effectRadius) {
            
            // angle of the particle relative to the mouse
            var a = angle(mouse.x, mouse.y, particle.position.actual.x, particle.position.actual.y);
            
            var strength = twirlAngle - easeOutQuad(dist, 0, twirlAngle, effectRadius);
            
            // twirl
            a += strength;
            
            // new position for the particle that's affected by gravity
            var p = rotate(a, dist, mouse.x, mouse.y);
            
            particle.position.affected.x = p.x;
            particle.position.affected.y = p.y;
        }
        
        context.beginPath();
        context.rect(particle.position.affected.x-1, particle.position.affected.y-1, 2, 2);
        context.fillStyle = "black";
        context.fill();
    });
}

draw();

window.addEventListener("mousemove", function (e) {
    mouse.x = e.x - canvas.offsetLeft;
    mouse.y = e.y - canvas.offsetTop;
    requestAnimationFrame(draw);
});

function easeOutQuad(t, b, c, d) {
    t /= d;
    return -c * t*(t-2) + b;
};

function angle(originX, originY, targetX, targetY) {
    var dx = targetX - originX;
    var dy = targetY - originY;
    var theta = Math.atan2(dy, dx) * (180 / Math.PI);
    if (theta < 0) theta = 360 + theta;
    return theta;
}

Number.prototype.map = function (in_min, in_max, out_min, out_max) {
    return (this - in_min) / (in_max - in_min) * (out_max - out_min) + out_min;
};

function pos(x, y, angle, length) {
    angle *= Math.PI / 180;
    return {
        x : Math.round(x + length * Math.cos(angle)),
        y : Math.round(y + length * Math.sin(angle)),
    };
}

function rotate(angle, distance, originX, originY) {
    return {
        x : originX + Math.cos(angle * Math.PI/180) * distance,
        y : originY + Math.sin(angle * Math.PI/180) * distance,
    }
}
* {
  margin: 0;
  padding: 0;
  box-sizing: inherit;
  line-height: inherit;
  font-size: inherit;
  font-family: inherit;
}

body {
  font-family: sans-serif;
  box-sizing: border-box;
  background-color: hsl(0, 0%, 90%);
}

canvas {
  display: block;
  background: white;
  box-shadow: 0 0 2px rgba(0, 0, 0, .2), 0 1px 1px rgba(0, 0, 0, .1);
  margin: 20px auto;
}
<canvas id="canvas"></canvas>


0

OP在问题底部的更新中提到了glfx.js,但我想在答案中强调一下,因为我错过了它,而且它对我来说是一个完美的解决方案。这里有一个演示:

https://evanw.github.io/glfx.js/demo/#bulgePinch

let canvas = fx.canvas();

// convert the image to a texture
let image = document.querySelector('#input-image');
let texture = canvas.texture(image);

// apply the bulge/pinch
canvas.draw(texture);
canvas.bulgePinch(centerX, centerY, radius, strength);
canvas.update();

// replace the image with the canvas
image.parentNode.insertBefore(canvas, image);
image.parentNode.removeChild(image);

// or get canvas as data url
let dataUrl = canvas.toDataUrl("image/png");

来自文档

Bulges or pinches the image in a circle.

centerX   The x coordinate of the center of the circle of effect.
centerY   The y coordinate of the center of the circle of effect.
radius    The radius of the circle of effect.
strength  -1 to 1 (-1 is strong pinch, 0 is no effect, 1 is strong bulge)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接