HTML5 Canvas:路径上的曲线图像 [英] HTML5 Canvas: curve image along the path

查看:74
本文介绍了HTML5 Canvas:路径上的曲线图像的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试沿路径弯曲图像。

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.

这里是 jsBin

推荐答案

Bezier 2nd&三阶ScanLine渲染


绘制具有不透明性的图像的部分将不起作用,因为总会有一些像素重叠。

Bezier 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.

最简单的方法是使用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.

首先,我必须指出这是非常缓慢的,不是动画。

First I must point out this is very SLOW and not for animation.

另一种方法是创建一个扫描线渲染,一次扫描像素一行。对于每个像素,您会在曲线上找到最接近的点,即贝塞尔曲线位置0-1和到曲线的距离。这为您提供了图像的x和y映射坐标。您还需要找到曲线的哪一侧。可以通过计算曲线上该点的切线,并使用切线和像素的叉积来找到您所在的线的哪一侧。

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 computing 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.

Scanline渲染非常适合并行处理技术。使用工人进行扫描的一部分将使性能几乎呈线性增长。在某些浏览器上,您可以找到带有 window.clientInformation.hardwareConcurrency 的可用处理核心数量,创建的工作线程超过此值不会给您带来任何改善,但会开始降低性能。如果找不到内核数,则最好关注性能,并且如果吞吐量没有增加,则不要产生更多的工作线程。

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.

以下是没有任何超级采样的曲线的最基本扫描线渲染。方法 getPosNearBezier 的核心功能是通过蛮力查找位置。它沿曲线采样所有点以找到最接近的点。因为这种方法非常慢,但是有足够的优化空间,您应该可以使用一些额外的功能来使性能提高一倍或两倍。

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将bezier渲染到屏幕外的画布上,然后将该画布渲染到2D画布上,因此您仍然可以充分使用2D API。

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.compileShader(fShader);
    var vShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vShader, vShaderSrc);
    gl.compileShader(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);


注意这两个示例都使用ES6语法,如果需要IE11支持,则使用babel。

Note Both examples use ES6 syntax, use babel if you want IE11 support.

这篇关于HTML5 Canvas:路径上的曲线图像的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆