HTML5 Canvas:沿路径

时间:2016-12-14 13:17:03

标签: javascript html5 canvas curve aliasing

我试图沿路径弯曲图像。

这里我得到了这么久。 enter image description here

我这样做是通过将图像切割成零件,将它们放在线上的某个点上,然后通过该点的切线角度旋转它们。

一切都很好,除非你仔细观察,每个图像部分之间都有裂缝,尽管每个图像都在前一个结束的地方开始。

任何人都可以帮助摆脱这些裂缝。

这是jsBin

1 个答案:

答案 0 :(得分:5)

Bezier 2nd& 3阶ScanLine渲染

在剖面中绘制具有不透明度的图像将不起作用,因为总会有一些像素重叠。结果将是接缝。

质量和快速,webGL

最简单的方法是使用WebGL并将曲线渲染为一组多边形。它很快,可以在屏幕外呈现。

扫描线渲染

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

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

此方法适用于大多数曲线,但在曲线自相交或光源图像的宽度导致像素重叠时会发生故障。由于扫描线渲染确保没有像素被写入两次,因此生成的唯一伪像将沿着与曲线的距离突然改变的线接缝。

扫描线渲染的优势在于您可以使用超级采样创建非常高质量的渲染(交易时间)。

扫描线渲染是并行处理技术的理想选择。使用工作人员进行部分扫描将提供近乎线性的性能提升。在某些浏览器上,您可以找到window.clientInformation.hardwareConcurrency可用处理核心的数量,创建比此值更多的工作人员不会给您带来改进,但会开始降低性能。如果您无法找到核心数量,最好关注性能,如果吞吐量没有增加,则不会产生更多的工作人员。

演示

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



// 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();
&#13;
&#13;
&#13;

WebGL示例

此示例仅使用webGL将bezier渲染到屏幕外画布上,然后将该画布渲染到2D画布上,因此您仍然可以充分利用2D API。

有点乱。但是从你的箱子里你知道你在做什么,所以希望这会有所帮助。

&#13;
&#13;
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);
&#13;
&#13;
&#13;

  

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