从文本HTML画布中提取路径

12

有没有办法在HTML5中从文本字母中提取路径,然后获取沿着该路径的(x,y)坐标,以便可以沿着该字母路径形成圆圈来呈现字母?

我想采用x,y坐标并在它们的位置上应用一种形状,以便以“像素化”的格式呈现文本字符串,然后再添加一些动画效果。

如何获得沿着画布上字符路径的某种x,y坐标的任何建议都将非常棒。

编辑:我实际上正在尝试自动生成坐标,以执行类似于此示例的操作:http://www.html5canvastutorials.com/labs/html5-canvas-google-bouncing-balls/


可能是 Html5 画布文本交集 的重复问题。 - Paul Sweatte
2个回答

21

像素缩放

一个简单的方法是按照以下步骤执行:

  • 使用小字体,使用纯色绘制文本
  • 迭代所有像素。任何 alpha = 255 的像素,都将其与x和y一起按直径进行缩放并存储到数组中

现在您有了一个大致的“球”数组来表示文本,可以进行动画处理。对于字母间距,它的准确性不是很高,但对于给定目的应该足够(您始终可以测量每个字母,并在分隔点处使用附加的增量值增加结束x值)。

较大的字体大小可以提高质量,但也会生成更多的点。与下面演示中使用的通用字体不同的字体类型可能对总体外观有所好处(请尝试!)。您还可以调整 alpha 阈值值以包含不完全坚实但有影响力的像素。

最后,不同的浏览器以不同的方式渲染文本,因此您可能需要注意这一点(请参见上面关于测量每个字母以添加它们之间的额外空间的内容)。

演示

snapshot

var ctx = document.querySelector("canvas").getContext("2d"),
    inp = document.querySelector("input"),
    w = ctx.canvas.width,
    h = ctx.canvas.height,
    balls = [];                                     // global ball array

ctx.fillStyle = "rgb(0, 154, 253)";                 // fill must be a solid color
generate(inp.value)                                 // init default text
inp.onkeyup = function() {generate(this.value)};    // get some text to demo

function generate(txt) {
  var i, radius = 5,                                // ball radius
      data32;                                       // we'll use uint32 for speed
  
  balls = [];                                       // clear ball array
  ctx.clearRect(0, 0, w, h);                        // clear canvas so we can
  ctx.fillText(txt.toUpperCase(), 0, 10);           // draw the text (default 10px)
  
  // get a Uint32 representation of the bitmap:
  data32 = new Uint32Array(ctx.getImageData(0, 0, w, h).data.buffer);
  
  // loop through each pixel. We will only store the ones with alpha = 255
  for(i = 0; i < data32.length; i++) {
    if (data32[i] & 0xff000000) {             // check alpha mask
      balls.push({                            // add new ball if a solid pixel
        x: (i % w) * radius * 2 + radius,     // use position and radius to
        y: ((i / w)|0) * radius * 2 + radius, //  pre-calc final position and size
        radius: radius,
        a: (Math.random() * 250)|0            // just to demo animation capability
      });
    }
  }
  // return array - here we'll animate it directly to show the resulting objects:
}

(function animate() {
  ctx.clearRect(0, 0, w, h);
  ctx.beginPath();
  for(var i = 0, ball; ball = balls[i]; i++) {
    var dx = Math.sin(ball.a * 0.2) + ball.radius,   // do something funky
        dy = Math.cos(ball.a++ * 0.2) + ball.radius;
    ctx.moveTo(ball.x + ball.radius + dx, ball.y + dy);
    ctx.arc(ball.x + dx, ball.y + dy, ball.radius, 0, 6.28);
    ctx.closePath();
  }
  ctx.fill();
  requestAnimationFrame(animate);
})();
body {font:bold 16px sans-serif}
<label>Type some text: <input value="PIXELS"></label><br>
<canvas width=1024></canvas>


12

手动沿字母路径放置圆圈是一项艰巨的任务。

即使没有人工干预,自动地(自动化!)也更难实现。

以下是如何自动排列圆圈以形成字母的方法。

答案分为两部分...

  1. 找到“字形”,

  2. 创建圆圈以填充和轮廓字形。

1. 艰难的部分

Frederik De Bleser编写了一个名为opentype.js的很好的库,它使用二次曲线在画布上解析 .ttf 字体文件并提取出任何指定字符的字形轮廓:https://github.com/nodebox/opentype.js

2. 稍微简单一些的部分

对于每个字母:

  • Find "many" points on each quadratic curve. Here's the algorithm to calculate an [x,y] on the curve at an interval T. T will range from 0.00 at the start of the curve to 1.00 at the end of the curve. T will not produce evenly spaced [x,y]'s along the curve so you will need to oversample (So "many" might mean 1000 values of T between 0.00 and 1.00).

    function getQuadraticBezierXYatT(startPt,controlPt,endPt,T) {
        var x = Math.pow(1-T,2) * startPt.x + 2 * (1-T) * T * controlPt.x + Math.pow(T,2) * endPt.x; 
        var y = Math.pow(1-T,2) * startPt.y + 2 * (1-T) * T * controlPt.y + Math.pow(T,2) * endPt.y; 
        return( {x:x,y:y} );
    }
    
  • Find the angle that is tangent to the curve's angle at those points. (Basically calculate what would be a right angle to the curve). You can do that with the next derivative of the quadratic formula:

    function quadraticBezierTangentAngle(t, p0, p2, p1) {
        var tt = 1 - t;
        var dx = (tt * p1.x + t * p2.x) - (tt * p0.x + t * p1.x);
        var dy = (tt * p1.y + t * p2.y) - (tt * p0.y + t * p1.y);
        return Math.tan(Math.atan2(dy,dx));
    }
    
  • Starting at the beginning of the curve, calculate each distance from the current [x,y] to the next [x,y]. You can do this with the Pythagorean Theorem:

    var dx=nextX-currentX;
    var dy=nextY-currentY;
    var distance=Math.sqrt(dx*dx+dy*dy);
    
  • De-duplicate the array so that all the remaining [x,y] elements are 1px distant from the previous [x,y] element. You can do this by filling a second array with values from the first where parseInt( nextInOriginalArray - lastDistanceInNewArray)==1;

  • Decide on a radius for your circles that will make up each letter. This is actually harder than it might seem. For "blocky" fonts, you can draw the letter "I" on the canvas. Then fetch all pixles using getImageData. Calculate the width of the "I"'s vertical stroke by searching for the count of opaque pixels running horizontally at the vertical middle of the letter. For blocky fonts, var radius = horizontalOpaquePixelCount/2;. For fonts with variable width strokes, you'll have to be inventive. Maybe var radius = horizontalOpaquePixelCount/3; or var radius = horizontalOpaquePixelCount/4;.

  • Iterate through the points array and define a new circle every radius*2 pixels. You calculate the center point for each circle using the tangent angle and trigonometry like this:

    var centerX = curvePointX + radius*Math.cos(tangentAngle);
    var centerY = curvePointY + radius*Math.sin(tangentAngle);
    
  • While creating circles, at some point the letter's curves will turn back upon themselves, so you must check each new circle you create to be sure it won't overlap an existing circle. You can calculate whether a new circle will intersect each existing circle like this:

    var dx = newCircleCenterX - existingCircleCenterX;
    var dy = newCircleCenterY - existingCircleCenterY;
    var distance=Math.sqrt(dx*dx+dy*dy);
    var circlesAreIntersecting=(distance<=newCircleRadius+existingCircleRadius);
    
微调:在某些字母路径的端点附近,您会发现下一个完整的圆形半径将溢出字母形状。如果发生这种情况,您可以缩小一些圆的半径以适应字母形状。如果您只想为圆形使用固定半径,则可以根据所有圆的平均半径重新计算所有圆的固定半径-包括您必须“缩小”以适应字母形状的圆。
例如,这是由15个圆形成的字母“L”。
但是,2个红色圆圈脱离了它的字母形状。您可以(1)缩小红色圆圈以适应字母形状或(2)根据适合字母形状的平均半径重新计算新的固定圆半径。
var total=0;
total += greenRadii * 13;
total += verticalRedRadiusResizedToFitInsideLetterform;
total += horizontalRedRadiusResizedToFitInsideLetterform;
var newRadius = total / 15;

您可以通过计算两条线的交点来计算适合字形的红色半径的长度:(1)连接最后一个绿色圆圈中心和红色圆圈中心形成的线段,(2)从曲线上的最后一个点垂直形成的线。以下是计算两条线的交点的算法:

// Get interseting point of 2 line segments (if any)
// Attribution: http://paulbourke.net/geometry/pointlineplane/
function line2lineIntersection(p0,p1,p2,p3) {

    var unknownA = (p3.x-p2.x) * (p0.y-p2.y) - (p3.y-p2.y) * (p0.x-p2.x);
    var unknownB = (p1.x-p0.x) * (p0.y-p2.y) - (p1.y-p0.y) * (p0.x-p2.x);
    var denominator  = (p3.y-p2.y) * (p1.x-p0.x) - (p3.x-p2.x) * (p1.y-p0.y);        

    // Test if Coincident
    // If the denominator and numerator for the ua and ub are 0
    //    then the two lines are coincident.    
    if(unknownA==0 && unknownB==0 && denominator==0){return(null);}

    // Test if Parallel 
    // If the denominator for the equations for ua and ub is 0
    //     then the two lines are parallel. 
    if (denominator == 0) return null;

    // If the intersection of line segments is required 
    // then it is only necessary to test if ua and ub lie between 0 and 1.
    // Whichever one lies within that range then the corresponding
    // line segment contains the intersection point. 
    // If both lie within the range of 0 to 1 then 
    // the intersection point is within both line segments. 
    unknownA /= denominator;
    unknownB /= denominator;

    var isIntersecting=(unknownA>=0 && unknownA<=1 && unknownB>=0 && unknownB<=1)

    if(!isIntersecting){return(null);}

    return({
        x: p0.x + unknownA * (p1.x-p0.x),
        y: p0.y + unknownA * (p1.y-p0.y)
    });
}

你知道有没有实现你所描述的算法的任何实例吗? - seltzlab
这更像是一个应用程序而不是算法——一系列算法。我已经完成了大约75%的这个应用程序。我还没有时间完成它。 :-) - markE
更进一步,可以创建一个基本的反锯齿效果,方法如下:对于每个圆(像素),计算0-255范围内有多少比例的圆面积落在字形之外,其中“0”的值完全在字形内,“127”是50%在内,“255”完全在字形之外,然后使用该数字将该像素分配为灰度值。例如,对于值为“109”,灰色的RGB值将为(109, 109, 109)或(#6d6d6d)。它可能在小字体大小时看起来不太好,但在大字体大小上应该效果良好。 - ColdCold

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