12

无论如何从html 5中的文本字母中提取路径,然后沿该路径抓取(x,y)坐标,以便字母可以由沿该字母路径的圆圈形成?

我想获取 x,y 坐标并在它们的位置应用一个形状,使其类似于“像素化”格式的文本字符串,然后是一些动画效果。

任何关于沿着画布上的字符路径获取某种 x,y 坐标的建议都会很棒。

编辑:我基本上是在尝试自动生成坐标来做类似的事情:http ://www.html5canvastutorials.com/labs/html5-canvas-google-bouncing-balls/

4

2 回答 2

18

像素缩放

一个简单的方法是执行以下操作:

  • 使用小字体,使用纯色绘制文本
  • 迭代所有像素。任何 alpha = 255 的像素,存储到一个数组,但 x 和 y 随直径缩放

现在你有一个粗略的代表文本的“球”数组并且可以动画。当涉及到字母间距时,它不是非常准确,但它应该用于给定的目的(您总是可以测量每个字母并在分离点使用额外的增量值增加结束 x 值)。

较大的字体大小可以提高质量,但也会产生更多的点。与下面演示中使用的通用字体类型不同的字体类型也可能有利于整体外观(实验!)。您还可以调整 alpha 阈值以包含不完全实心但有影响的像素。

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

演示

快照

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>

于 2015-05-13T02:47:53.003 回答
11

通过在字母路径上直观地放置圆圈来手动完成这是一项艰巨的任务。

在没有人工干预的情况下自动(自动!)变得更加困难。

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

答案分为两部分...

  1. 寻找“字母”,

  2. 创建圆圈以填充和勾勒字体。

1.困难的部分

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

2.唯一稍微不那么硬的部分

对于每个字母:

  • 在每条二次曲线上找到“许多”点。这是在曲线上以间隔 T 计算 [x,y] 的算法。T 的范围从曲线开始处的 0.00 到曲线结束处的 1.00。T 不会沿曲线产生均匀间隔的 [x,y],因此您需要过采样(因此“许多”可能意味着 1000 个 T 值在 0.00 和 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} );
    }
    
  • 找到在这些点处与曲线角度相切的角度。(基本上计算与曲线成直角的角度)。您可以使用二次公式的下一个导数来做到这一点:

    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));
    }
    
  • 从曲线的开头开始,计算从当前 [x,y] 到下一个 [x,y] 的每个距离。你可以用勾股定理做到这一点:

    var dx=nextX-currentX;
    var dy=nextY-currentY;
    var distance=Math.sqrt(dx*dx+dy*dy);
    
  • 对数组进行重复数据删除,以使所有剩余的 [x,y] 元素与前一个 [x,y] 元素的距离为 1px。您可以通过使用第一个数组中的值填充第二个数组来做到这一点parseInt( nextInOriginalArray - lastDistanceInNewArray)==1;

  • 确定构成每个字母的圆圈的半径。这实际上比看起来更难。对于“块状”字体,您可以在画布上绘制字母“I”。然后使用getImageData. 通过搜索在字母垂直中间水平运行的不透明像素的计数来计算“I”的垂直笔划的宽度。对于块状字体,var radius = horizontalOpaquePixelCount/2;. 对于具有可变宽度笔画的字体,您必须具有创造性。也许var radius = horizontalOpaquePixelCount/3;var radius = horizontalOpaquePixelCount/4;

  • 遍历点数组并为每个radius*2像素定义一个新圆。您可以使用切角和三角函数计算每个圆的中心点,如下所示:

    var centerX = curvePointX + radius*Math.cos(tangentAngle);
    var centerY = curvePointY + radius*Math.sin(tangentAngle);
    
  • 在创建圆圈时,字母的曲线会在某些时候自动返回,因此您必须检查您创建的每个新圆圈,以确保它不会与现有圆圈重叠。您可以计算一个新圆是否会与每个现有圆相交,如下所示:

    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;

您可以通过计算 2 条线的交点来计算适合字母形状的红色半径的长度:(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)
    });
}
于 2015-04-17T19:08:22.833 回答