I am trying to curve image along the path.
Here what I got so long.
I did this by cutting image into parts, placing them on a certain point on the line, and rotating them by tangent angle of that point.
Everything is great, except if you look closely there are cracks between each image section, although each image begins exactly where previous ends.
Can anybody help to get rid of those cracks.
Here is jsBin.
I am trying to curve image along the path.
Here what I got so long.
I did this by cutting image into parts, placing them on a certain point on the line, and rotating them by tangent angle of that point.
Everything is great, except if you look closely there are cracks between each image section, although each image begins exactly where previous ends.
Can anybody help to get rid of those cracks.
Here is jsBin.
Share Improve this question edited Mar 4, 2022 at 20:15 Zoe - Save the data dump 28.3k22 gold badges128 silver badges160 bronze badges asked Dec 14, 2016 at 13:17 ZhirayrZhirayr 4232 gold badges6 silver badges10 bronze badges 8- While there is some alpha in the image you will not be able to get rid of the seams using your method. To do this with any quality on the 2D canvas you will need to write a scan line render. For each pixel, find the closest point on the curve, this will give you the x image coordinate, the distance the pixel is from the curve will give you the y image coordinate. This is very slow. For this type of thing you are best using webGL, create mesh along the curve, map the texture coords, and render. Very fast maybe even quicker than rendering the curve using 2D (once you have the mesh) – Blindman67 Commented Dec 15, 2016 at 2:59
- Depending on how accurate you need it to be, you could use simple 2-directional displacement instead of webgl, but it won't as accurate and of course still slower. You may be able to use it for animation though even if it has some limitations (a simple example). – user1693593 Commented Dec 15, 2016 at 7:29
- Nice done , maybe blur effect can help you . – Nikola Lukic Commented Dec 15, 2016 at 8:17
- @K3N Nice example, but I don't think Displacement would do the job here. – Zhirayr Commented Dec 15, 2016 at 14:07
- @Blindman67 there are seams, even if Image got no Alpha. – Zhirayr Commented Dec 19, 2016 at 8:43
1 Answer
Reset to default 8Bezier 2nd & 3rd order ScanLine rendering
Drawing an image with opacity in sections will not work as there will always be some pixels overlapping. The result will be seams.
Quality and quick, webGL
The easiest approch is to use WebGL and render the curve as a set of polygons. It is quick, and can be rendered offscreen.
Scanline rendering
First I must point out this is very SLOW and not for animation.
The alternative is to create a scan line render that scans the pixels one row at a time. For each pixel you find the closest point on the curve as the bezier position 0-1 and the distance from the curve. This gives you the x and y mapping coordinate of the image. You also need to find which side of the curve you are. This can be found by puting the tangent at the point on the curve and using the cross product of the tangent and pixel to find which side of the line you are.
This method will work for most curves but breaks down when the curve is self intersecting or the width of the source image causes pixels to overlap. As scan line rendering ensures no pixels will be written twice the only artifacts generated will be seams along lines where the distance to the curve abruptly changes.
The advantage of scan line rendering is that you can create very high quality rendering (trading off time) using super sampling.
Workers
Scanline rendering is ideal for parallel processing techniques. Using workers to do parts of the scan will give a nearly linear performance boost. On some browsers you can find the number of available processing cores with window.clientInformation.hardwareConcurrency
creating any more workers than this value will not give you improvement but will start to reduce performance. If you can not find the number of cores it is best to keep an eye on performance and not spawn any more workers if throughput is not increasing.
Demo
The following is the most basic scan line render of a curve without any super sampling. The function at the heart of the method getPosNearBezier
finds the position via brute force. It samples all the points along the curve to find the closest. As is this method is VERY slow, but there is plenty of room for optimisation and you should be able to double or triple the performance with some extra smarts.
// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
// setup canvas
var canvas = createImage(400,400);
var ctx = canvas.ctx;
document.body.appendChild(canvas);
ctx.clearRect(0,0,canvas.width,canvas.height);
document.body.style.background = "#999";
const quality = 500; // this value should be greater than the approx length
// of the bezier curve in pixels.
// create source image with gradient alpha 0 to 1 to 0
var sWidth = 300;
var sHeight = 100;
var checkerSize = 20;
var darkG = ctx.createLinearGradient(0,0,0,sHeight);
var lightG = ctx.createLinearGradient(0,0,0,sHeight);
for(var i = 0; i <= 1; i += 1/20){
darkG.addColorStop(i,"rgba(0,0,0,"+Math.pow(Math.sin(i * Math.PI),2)+")");
lightG.addColorStop(i,"rgba(255,255,255,"+Math.pow(Math.sin(i * Math.PI),2)+")");
}
// draw checker pattern on source image
var testImage = createImage(sWidth,sHeight);
for(var i = 0; i < sHeight; i += checkerSize){
for(var j = 0; j < sWidth; j += checkerSize){
if(((i/checkerSize+j/checkerSize) % 2) === 0){
testImage.ctx.fillStyle = darkG;
}else{
testImage.ctx.fillStyle = lightG;
}
testImage.ctx.fillRect(j,i,checkerSize,checkerSize);
}
}
// ctx.drawImage(testImage,0,0);
// get source image as 32bit pixels (note Endian of this word does not effect the result)
var sourcePixels = new Uint32Array(testImage.ctx.getImageData(0,0,testImage.width,testImage.height).data.buffer);
var pixelData;
// variables for bezier functions.
// keep these outside the function as creating them inside will have a performance/GC hit
var x = 0;
var y = 0;
var v1 = {x,y};
var v2 = {x,y};
var v3 = {x,y};
var v4 = {x,y};
var tng = {x,y};
var p = {x,y};
var curvePos = {x,y};
var c1,u1,u,b1,a,b,c,d,e,vx,vy;
var bez = {};
bez.p1 = {x : 40, y : 40}; // start
bez.p2 = {x : 360, y : 360}; // end
bez.cp1 = {x : 360, y : 40}; // first control point
bez.cp2 = {x : 40, y : 360}; // second control point if undefined then this is a quadratic
// This is a search and is thus very very slow.
// get the unit pos on the bezier that is closest to the point point
// resolution is the search steps (default 100)
// pos is a estimate of the pos, if given then a higher resolution search is done around this pos
function getPosNearBezier(point,resolution,pos){
// translate curve to make vec the origin
v1.x = bez.p1.x - point.x;
v1.y = bez.p1.y - point.y;
v2.x = bez.p2.x - point.x;
v2.y = bez.p2.y - point.y;
v3.x = bez.cp1.x - point.x;
v3.y = bez.cp1.y - point.y;
if(bez.cp2 !== undefined){
v4.x = bez.cp2.x - point.x;
v4.y = bez.cp2.y - point.y;
}
if(resolution === undefined){
resolution = 100;
}
c1 = 1/resolution;
u1 = 1 + c1/2;
var s = 0;
if(pos !== undefined){
s = pos - c1 * 2;
u1 = pos + c1 * 2;
c1 = (c1 * 4) / resolution;
}
d = Infinity;
if(bez.cp2 === undefined){
for(var i = s; i <= u1; i += c1){
a = (1 - i);
c = i * i;
b = a*2*i;
a *= a;
vx = v1.x * a + v3.x * b + v2.x * c;
vy = v1.y * a + v3.y * b + v2.y * c;
e = Math.sqrt(vx*vx+vy*vy);
if(e < d ){
pos = i;
d = e;
curvePos.x = vx;
curvePos.y = vy;
}
}
}else{
for(var i = s; i <= u1; i += c1){
a = (1 - i);
c = i * i;
b = 3 * a * a * i;
b1 = 3 * c * a;
a = a*a*a;
c *= i;
vx = v1.x * a + v3.x * b + v4.x * b1 + v2.x * c;
vy = v1.y * a + v3.y * b + v4.y * b1 + v2.y * c;
e = Math.sqrt(vx*vx+vy*vy);
if(e < d ){
pos = i;
d = e;
curvePos.x = vx + point.x;
curvePos.y = vy + point.y;
}
}
}
return pos;
};
function tangentAt( position) { // returns the normalised tangent at position
if(bez.cp2 === undefined){
a = (1-position) * 2;
b = position * 2;
tng.x = a * (bez.cp1.x - bez.p1.x) + b * (bez.p2.x - bez.cp1.x);
tng.y = a * (bez.cp1.y - bez.p1.y) + b * (bez.p2.y - bez.cp1.y);
}else{
a = (1-position)
b = 6 * a * position; // (6*(1-t)*t)
a *= 3 * a; // 3 * ( 1 - t) ^ 2
c = 3 * position * position; // 3 * t ^ 2
tng.x = -bez.p1.x * a + bez.cp1.x * (a - b) + bez.cp2.x * (b - c) + bez.p2.x * c;
tng.y = -bez.p1.y * a + bez.cp1.y * (a - b) + bez.cp2.y * (b - c) + bez.p2.y * c;
}
u = Math.sqrt(tng.x * tng.x + tng.y * tng.y);
tng.x /= u;
tng.y /= u;
return tng;
}
function getRow(y){
pixelData = ctx.getImageData(0,y,canvas.width,1)
return new Uint32Array(pixelData.data.buffer);
}
function setRow(y,data){
return ctx.putImageData(pixelData,0,y);
}
// scans a single line
function scanLine(y){
var pixels = getRow(y);
for(var x = 0; x < canvas.width; x += 1){
p.x = x;
p.y = y;
var bp = getPosNearBezier(p,quality);
if(bp >= 0 && bp <= 1){ // is along curve
tng = tangentAt(bp); // get tangent so that we can find what side of the curve we are
vx = curvePos.x - x;
vy = curvePos.y - y;
var dist = Math.sqrt(vx * vx + vy * vy);
dist *= Math.sign(vx* tng.y - vy*tng.x)
dist += sHeight /2
if(dist >= 0 && dist <= sHeight){
var srcIndex = Math.round(bp * sWidth) + Math.round(dist) * sWidth;
if(sourcePixels[srcIndex] !== 0){
pixels[x] = sourcePixels[srcIndex];
}
}
}
}
setRow(y,pixels);
}
var scanY = 0;
// scan all pixels on canvas
function scan(){
scanLine(scanY);
scanY += 1;
if(scanY < canvas.height){
setTimeout(scan,1);
}
}
// draw curve
ctx.fillStyle = "blue";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(bez.p1.x,bez.p1.y);
ctx.bezierCurveTo(bez.cp1.x,bez.cp1.y,bez.cp2.x,bez.cp2.y,bez.p2.x,bez.p2.y);
ctx.stroke();
//start scan
scan();
WebGL example
This example just renders the bezier onto an offscreen canvas using webGL and then renders that canvas onto the 2D canvas, so you still have full use of the 2D API.
Its a bit of a mess. But from your bin you know what you are doing so hopefully this will help.
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
var createCanvas=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;return i;}
var can,gl; // canvas and webGL context
var canvas = createImage(512,512);
var ctx = canvas.ctx;
document.body.appendChild(canvas);
document.body.style.background = "#999";
var x = 0;
var y = 0;
var v1 = {x,y};
var v2 = {x,y};
var v3 = {x,y};
var v4 = {x,y};
var tng = {x,y};
var p = {x,y};
var curvePos = {x,y};
var c1,u1,b1,a,b,c,d,e,vx,vy;
// the bez we are using
var bez = {};
bez.p1 = {x : 50, y : 50}; // start
bez.p2 = {x : 350, y : 350}; // end
bez.cp1 = {x : 300, y : 50}; // first control point
bez.cp2 = {x : 50, y : 310}; // second control point if undefined then this is a quadratic
function getBezierAt(bez,pos){
if(bez.cp2 === undefined){
a = (1 - pos);
c = i * pos;
b = a*2*pos;
a *= a;
curvePos.x = bez.p1.x * a + bez.cp1.x * b + bez.p2.x * c;
curvePos.y = bez.p1.y * a + bez.cp1.y * b + bez.p2.y * c;
}else{
a = (1 - pos);
c = pos * pos;
b = 3 * a * a * pos;
b1 = 3 * c * a;
a = a*a*a;
c *= pos;
curvePos.x = bez.p1.x * a + bez.cp1.x * b + bez.cp2.x * b1 + bez.p2.x * c;
curvePos.y = bez.p1.y * a + bez.cp1.y * b + bez.cp2.y * b1 + bez.p2.y * c;
}
return curvePos;
};
function tangentAt(bez, position) { // returns the normalised tangent at position
if(bez.cp2 === undefined){
a = (1-position) * 2;
b = position * 2;
tng.x = a * (bez.cp1.x - bez.p1.x) + b * (bez.p2.x - bez.cp1.x);
tng.y = a * (bez.cp1.y - bez.p1.y) + b * (bez.p2.y - bez.cp1.y);
}else{
a = (1-position)
b = 6 * a * position; // (6*(1-t)*t)
a *= 3 * a; // 3 * ( 1 - t) ^ 2
c = 3 * position * position; // 3 * t ^ 2
tng.x = -bez.p1.x * a + bez.cp1.x * (a - b) + bez.cp2.x * (b - c) + bez.p2.x * c;
tng.y = -bez.p1.y * a + bez.cp1.y * (a - b) + bez.cp2.y * (b - c) + bez.p2.y * c;
}
var u = Math.sqrt(tng.x * tng.x + tng.y * tng.y);
tng.x /= u;
tng.y /= u;
return tng;
}
function createTestImage(w,h,checkerSize,c1,c2){
var testImage = createImage(w,h);
var darkG = testImage.ctx.createLinearGradient(0,0,0,h);
var lightG = testImage.ctx.createLinearGradient(0,0,0,h);
for(var i = 0; i <= 1; i += 1/20){
darkG.addColorStop(i,"rgba("+c1.join(",")+","+(Math.pow(Math.sin(i * Math.PI),5))+")");
lightG.addColorStop(i,"rgba("+c2.join(",")+","+Math.pow(Math.sin(i * Math.PI),5)+")");
}
for(var i = 0; i < h; i += checkerSize){
for(var j = 0; j < w; j += checkerSize){
if(((i/checkerSize+j/checkerSize) % 2) === 0){
testImage.ctx.fillStyle = darkG;
}else{
testImage.ctx.fillStyle = lightG;
}
testImage.ctx.fillRect(j,i,checkerSize,checkerSize);
}
}
return testImage;
}
// Creates a mesh with texture coords for webGL to render
function createBezierMesh(bezier,steps,tWidth,tHeight){
var i,x,y,tx,ty;
var array = [];
var step = 1/steps;
for(var i = 0; i < 1 + step/2; i += step){
if(i > 1){ // sometimes there is a slight error
i = 1;
}
curvePos = getBezierAt(bezier,i);
tng = tangentAt(bezier,i);
x = curvePos.x - tng.y * (tHeight/2);
y = curvePos.y + tng.x * (tHeight/2);
tx = i;
ty = 0;
array.push({x,y,tx,ty})
x = curvePos.x + tng.y * (tHeight/2);
y = curvePos.y - tng.x * (tHeight/2);
ty = 1;
array.push({x,y,tx,ty})
}
return array;
}
function createShaders(){
var fShaderSrc = `
precision mediump float;
uniform sampler2D image; // texture to draw
varying vec2 texCoord; // holds text coordinates
void main() {
gl_FragColor = texture2D(image,texCoord);
}`;
var vShaderSrc = `
attribute vec4 vert; // holds a vert with pos as xy textures as zw
varying vec2 texCoord; // holds text coordinates
void main(){
gl_Position = vec4(vert.x,vert.y,0.0,1.0); // seperate out the position
texCoord = vec2(vert.z,vert.w); // and texture coordinate
}`;
var fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader, fShaderSrc);
gl.pileShader(fShader);
var vShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vShader, vShaderSrc);
gl.pileShader(vShader);
var program = gl.createProgram();
gl.attachShader(program, fShader);
gl.attachShader(program, vShader);
gl.linkProgram(program);
gl.useProgram(program);
program.vertAtr = gl.getAttribLocation(program, "vert"); // save location of verts
gl.enableVertexAttribArray(program.vertAtr); // turn em on
return program;
}
function createTextureFromImage(image){
var texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.bindTexture(gl.TEXTURE_2D, null);
return texture;
}
function createMesh(array,vertSize) {
var meshBuf ;
var w = gl.canvas.width;
var h = gl.canvas.height;
var verts = [];
for(var i = 0; i < array.length; i += 1){
var v = array[i];
verts.push((v.x - w / 2) / w * 2 , -(v.y - h / 2) / h * 2, v.tx, v.ty);
}
verts = new Float32Array(verts);
gl.bindBuffer(gl.ARRAY_BUFFER, meshBuf = gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
meshBuf.vertSize = vertSize;
meshBuf.numVerts = array.length ;
return {verts,meshBuf}
}
function drawMesh(mesh){
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(mesh.program);
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.meshBuf);
gl.bufferData(gl.ARRAY_BUFFER, mesh.verts, gl.STATIC_DRAW);
gl.vertexAttribPointer(mesh.program.vertAtr, mesh.meshBuf.vertSize, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, mesh.texture);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, mesh.meshBuf.numVerts);
}
function startWebGL(imgW,imgH){
can = createCanvas(canvas.width,canvas.height);
gl = can.getContext("webgl");
gl.viewportWidth = can.width;
gl.viewportHeight = can.height;
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
var mesh = createMesh(createBezierMesh(bez,50,imgW,imgH),4);
mesh.program = createShaders();
mesh.W = imgW;
mesh.H = imgH;
mesh.texture = createTextureFromImage(createTestImage(imgW,imgH,imgH/4,[255,255,255],[0,255,0]));
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0,0,0,0);
drawMesh(mesh)
return mesh;
}
// recreates bezier mesh and draws it
function updateBezier(bezier,mesh){
var array = createBezierMesh(bezier,50,mesh.W,mesh.H);
var index = 0;
var w = gl.canvas.width;
var h = gl.canvas.height;
for(var i = 0; i < array.length; i += 1){
var v = array[i];
mesh.verts[index ++] = (v.x - w / 2) / w * 2;
mesh.verts[index ++] = -(v.y - h / 2) / h * 2;
mesh.verts[index ++] = v.tx;
mesh.verts[index ++] = v.ty;
}
drawMesh(mesh);
}
ctx.font = "26px arial";
// main update function
function update(timer){
var w = canvas.width;
var h = canvas.height;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,canvas.width,canvas.height);
var x= Math.cos(timer / 1000) * 100;
var y= Math.sin(timer / 1000) * 100;
bez.p1.x = 50 + x;
bez.p1.y = 50 + y;
var x= Math.cos(timer / 2000) * 100;
var y= Math.sin(timer / 2000) * 100;
bez.p2.x = 350 + x;
bez.p2.y = 350 + y;
updateBezier(bez,glMesh)
ctx.drawImage(can,0,0);
ctx.fillText("WebGL rendered to 2D canvas.",10,30)
requestAnimationFrame(update);
}
var glMesh = startWebGL(512,64);
requestAnimationFrame(update);
Note Both examples use ES6 syntax, use babel if you want IE11 support.