Pinch/pucker canvas 中的图像
Pinch/pucker an image in canvas
如何 pinch/pucker 图像的某些区域 canvas?
前段时间做了一个太阳系动画,开始改写。现在,我想为质量添加重力效果。为了使效果可见,我将背景变成了网格,我将对其进行修改。
想要的效果是这样的(PS制作)
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);
更新:我已经进行了一些谷歌搜索并找到了一些资源,但由于我以前从未进行过像素操作,因此无法将它们放在一起。
Pixel Distortions with Bilinear Filtration in HTML5 Canvas | Splashnology.com(仅函数)
glfx.js(带有演示的 WebGL 库)
JSFiddle(球形化、缩放、旋转示例)
我猜倒置形式的球形效果对这项工作很有帮助。
更新的答案
我显着提高了性能,但降低了灵活性。
要获得捏合效果,您需要使用遮罩,然后使用遮罩重新绘制图像。在这种情况下,您可以使用圆形遮罩,当您绘制放大或缩小原件的副本时,该遮罩会缩小。效果是凸起或收缩。
有一个质量设置可以让您从亚像素渲染到非常粗糙。与这些事情一样,您会牺牲速度来换取质量。
由于硬件和浏览器之间的渲染速度不一致,我不建议将此作为满足您要求的最终解决方案。
要获得一致的结果,您需要使用 webGL。如果我有时间我会写一个着色器来做到这一点,如果 ShaderToy
上还没有
所以这是一个纯 canvas 二维解决方案。 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>
我有时间重新审视这个问题并提出了解决方案。首先,我需要了解计算和像素操作背后的数学原理,而不是直接解决问题。
所以,我没有使用 image/pixels,而是决定使用 particles
。 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
,但我认为 twirl 不使用线性映射,而是使用缓和映射。将 JS 版本与 PS 过滤器进行比较。 PS 滤镜更流畅。我需要重写 map
函数。
更新二:
我设法使它的工作方式与 PS 过滤器相同。使用 ease 函数,即 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>
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");
来自the docs:
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)
如何 pinch/pucker 图像的某些区域 canvas?
前段时间做了一个太阳系动画,开始改写。现在,我想为质量添加重力效果。为了使效果可见,我将背景变成了网格,我将对其进行修改。
想要的效果是这样的(PS制作)
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);
更新:我已经进行了一些谷歌搜索并找到了一些资源,但由于我以前从未进行过像素操作,因此无法将它们放在一起。
Pixel Distortions with Bilinear Filtration in HTML5 Canvas | Splashnology.com(仅函数)
glfx.js(带有演示的 WebGL 库)
JSFiddle(球形化、缩放、旋转示例)
我猜倒置形式的球形效果对这项工作很有帮助。
更新的答案 我显着提高了性能,但降低了灵活性。
要获得捏合效果,您需要使用遮罩,然后使用遮罩重新绘制图像。在这种情况下,您可以使用圆形遮罩,当您绘制放大或缩小原件的副本时,该遮罩会缩小。效果是凸起或收缩。
有一个质量设置可以让您从亚像素渲染到非常粗糙。与这些事情一样,您会牺牲速度来换取质量。
由于硬件和浏览器之间的渲染速度不一致,我不建议将此作为满足您要求的最终解决方案。
要获得一致的结果,您需要使用 webGL。如果我有时间我会写一个着色器来做到这一点,如果 ShaderToy
上还没有所以这是一个纯 canvas 二维解决方案。 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>
我有时间重新审视这个问题并提出了解决方案。首先,我需要了解计算和像素操作背后的数学原理,而不是直接解决问题。
所以,我没有使用 image/pixels,而是决定使用 particles
。 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
,但我认为 twirl 不使用线性映射,而是使用缓和映射。将 JS 版本与 PS 过滤器进行比较。 PS 滤镜更流畅。我需要重写 map
函数。
更新二:
我设法使它的工作方式与 PS 过滤器相同。使用 ease 函数,即 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>
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");
来自the docs:
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)