如何围绕椭圆旋转文本?

How do I rotate text around an oval?

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

到目前为止我的方法是:

  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>

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

在椭圆的某些点,构成像素的圆内的线在某些点比其他点长。我不确定如何解释得好,但我能解释的最好方法是使用与电子游戏中 strafing 相同的概念。

Credits to Dan Violet Sagmiller (Game Developer)

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

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

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

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

我没有任何解决方案,只有降低效果的方法,您可以通过以下方式看到:

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

  • 保持椭圆的比例彼此相似;通过保持宽度和高度彼此接近,字母之间存在间距的效果将会降低。

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

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

    currentOffset += textWidth(sourceCharacters[i]);

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

正确的实现方式包括在绘制每个字符的点处找到椭圆的切线,使用该切线的斜率来确定每个字符的角度,然后确定旋转的度数space 下一个字母必须使字形边界框的角接触。找到那个字母间距角度似乎很难精确地做到,但我有一个应该工作得相当好的 hack。

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>