如何围绕椭圆旋转文本?

时间:2021-07-26 11:05:00

标签: p5.js

我想围绕椭圆旋转文本。每个字符都会发生一些轻微的变形,但是椭圆变得越极端(即越不完美的圆)。理想情况下,此方法可以在任意高度和宽度比例的椭圆形,甚至圆角矩形等其他形状上很好地呈现文本。

到目前为止,我的方法是:

  1. 找到椭圆的边缘
  2. 使用三角函数围绕扭曲的椭圆旋转一个点
  3. 在这个位置画text()
  4. 根据它的位置旋转这个 text()
  5. 通过 for 循环执行此操作,循环提供字符串中的每个字符。

字符获得奇怪的间距(即使使用 textWidth)和奇怪的旋转(即使是动态计算的)。您可以在下面的代码片段中看到这一点,特别是在 'spinning' 的 'nn' 和 'piece' 的 'ie' 上。

let canvasWidth = 400;
let canvasHeight = 400;
let spinSpeed = 0.25;
let ellipseWidth = 280;
let ellipseHeight = 200;
let angle = 0;
let sourceText = "A beautiful piece of spinning text. ";
let sourceCharacters = sourceText.toUpperCase().split("");

function setup() {
  createCanvas(canvasWidth, canvasHeight);
  angleMode(DEGREES);
  textSize(18);
  textAlign(LEFT, BASELINE);
}

function draw() {
  background(0);

  // Draw an ellipse
  stroke("rgba(255, 255, 255, 0.15)");
  noFill();
  ellipse(canvasWidth / 2, canvasHeight / 2, ellipseWidth, ellipseHeight);

  // Prepare for operations around circle
  translate(canvasWidth / 2, canvasHeight / 2);

  // Create a revolving angle
  if (angle < 360) {
    angle += spinSpeed;
  } else {
    angle = 0;
  }

  // Set variables for trigonometry
  let widthR = ellipseWidth / 2;
  let heightR = ellipseHeight / 2;
  let dx = widthR * cos(angle);
  let dy = heightR * sin(angle);

  // Set variable for offsetting each character
  let currentOffset = 0;

  // Loop through each chracter and place on oval edge
  for (let i = 0; i < sourceCharacters.length; i++) {
    push();
    dx = widthR * cos(angle + currentOffset);
    dy = heightR * sin(angle + currentOffset);

    translate(dx, dy);
    rotate(angle + currentOffset + 90);

    stroke("rgba(255, 255, 255, 0.25)");
    rect(0, 0, textWidth(sourceCharacters[i]), 10);
    fill("white");
    text(sourceCharacters[i], 0, 0);

    currentOffset += textWidth(sourceCharacters[i]);
    pop();
  }
}
html,
body {
  margin: 0;
  padding: 0;
}

canvas {
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>

2 个答案:

答案 0 :(得分:1)

据我怀疑,字符之间的这种奇怪的间距不是由于代码错误造成的。这很简单,原因与您无法制作出非常好的地球地图的原因相同。在 p5js 中,椭圆和圆并不完美。

在椭圆的某些点上,构成像素的圆内的线在某些点上比其他点长。我不知道如何很好地解释它,但我能解释它的最好方法是使用与视频游戏中的 strafing 相同的概念。

Credits to Dan Violet Sagmiller (Game Developer)

在视频游戏中,游戏开发者有时会降低玩家同时向前和向侧面移动(对角线)的速度。这是因为一些复杂的数学表明扫射会导致玩家移动得更快。

发生扫射的唯一原因是游戏开发者改变玩家移动的方式。他们不使用三角函数或旋转数学与向量,而是使用转换逻辑(+,-)。这通常会提高游戏的性能。根据您的代码,您还使用了转换逻辑:

translate(dx, dy);
rotate(angle + currentOffset + 90);

本质上,当每个字母在程序的每一帧中移动到新位置时,由于扫射的工作方式,有些字母会比其他字母移动得更远。

我没有任何解决方案,只有通过以下方式减少影响的方法:

  • 减小和增大尺寸;您可以增加椭圆的大小或减少文本的大小和长度。

  • 保持椭圆的比例相似;通过保持宽度和高度彼此接近,字母间距的影响会降低。

我希望这能解决您的问题,或者至少有助于减少它的影响。祝你有美好的一天!

答案 1 :(得分:1)

这行代码没有意义,可能只是巧合:

    currentOffset += textWidth(sourceCharacters[i]);

这是使用以像素为单位的字符宽度作为以度为单位的角度(基于在调用 rotate/sin/cos 时使用 currentOffset。

正确的实现包括在您绘制每个字符的点处找到椭圆的切线,使用该切线的斜率来确定每个字符的角度,然后确定所需的旋转度数隔开下一个字母,使字形边界框的角接触。找到那个字母间距角度似乎很难精确地完成,但我有一个应该可以很好地工作的技巧。

let canvasWidth = 400;
let canvasHeight = 400;
let spinSpeed = 0.25;
let ellipseWidth = 280;
let ellipseHeight = 200;
let angle = 0;
let sourceText = "A beautiful piece of spinning text. ";
let sourceCharacters = sourceText.toUpperCase().split("");

function setup() {
  createCanvas(canvasWidth, canvasHeight);
  angleMode(DEGREES);
  textSize(18);
  // drawing each letter from it's center point makes determining the angle and
  // the spacing a little easier I think.
  textAlign(CENTER, BASELINE);
}

function draw() {
  background(0);

  // Draw an ellipse
  stroke("rgba(255, 255, 255, 0.15)");
  noFill();
  ellipse(canvasWidth / 2, canvasHeight / 2, ellipseWidth, ellipseHeight);

  // Prepare for operations around circle
  translate(canvasWidth / 2, canvasHeight / 2);

  // Create a revolving angle
  if (angle < 360) {
    angle += spinSpeed;
  } else {
    angle = 0;
  }

  // Set variables for trigonometry
  const widthR = ellipseWidth / 2;
  const heightR = ellipseHeight / 2;
  let dx, dy;

  // Set variable for offsetting each character
  let currentOffset = 0;

  // Loop through each chracter and place on oval edge
  for (let i = 0; i < sourceCharacters.length; i++) {
    push();
    /* This isn't the best way to convert an angle to a position on an ellipse
     * It causes distortions depending on the tightness of the curve.
    dx = widthR * cos(angle + currentOffset);
    dy = heightR * sin(angle + currentOffset); */

    // This give a more accurate position. See: https://math.stackexchange.com/a/2258243/771335
    let r = widthR * heightR / sqrt(widthR ** 2 * sin(angle + currentOffset) ** 2 + heightR ** 2 * cos(angle + currentOffset) ** 2);
    dx = r * cos(angle + currentOffset);
    dy = r * sin(angle + currentOffset);


    translate(dx, dy);
    // This is the derivative of the equation for an ellipse.
    // Calculating the derivative at X gives us the current slope.
    let tangent = -1 * (heightR ** 2) * dx / (widthR ** 2 * sqrt(heightR ** 2 * (widthR ** 2 - dx ** 2) / widthR ** 2));
    if (dy < 0) {
      tangent *= -1;
    }

    // Use the tangent slope to determine rotation angle
    let rotation = atan2(tangent, 1);
    if (dy > 0) {
      rotation += 180;
    }
    rotate(rotation);

    let charWidth = textWidth(sourceCharacters[i]);
    stroke("rgba(255, 255, 255, 0.25)");
    rect(-charWidth / 2, 0, charWidth, -18);
    fill("white");
    text(sourceCharacters[i], 0, 0);

    // find the angle between the vector to the center of the current letter
    // and the vector to the center of the next letter projected onto the
    // current tangent line. This is a bit of a hack.

    if (i + 1 < sourceCharacters.length) {
      let spacing = (charWidth + textWidth(sourceCharacters[i + 1])) / 2;
      let vCur = createVector(dx, dy);
      let vNext =
        vCur.copy().add(
          createVector(spacing * cos(rotation), spacing * sin(rotation))
        );

      currentOffset += vCur.angleBetween(vNext);
    }
    pop();
  }
}
html,
body {
  margin: 0;
  padding: 0;
}

canvas {
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>

相关问题