You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Three.js中居中THREE.Points文本网格?

解决文本网格居中显示问题

问题背景

通过SVG路径创建顶点制作文本网格时,无论文本长度如何,字母的起始位置始终固定,无法居中显示;长文本还可能超出屏幕范围,需要实现文本始终居中且完整呈现的效果。

问题分析

当前代码中,文本从SVG的x=0位置开始绘制,再通过固定的svgViewBoxWidth/2偏移尝试居中,但这个固定值无法适配不同长度的文本——短文本会偏左,长文本可能超出可视范围。核心问题是没有基于文本实际的边界尺寸来计算居中偏移量。

解决方案步骤

  • 获取文本实际边界:通过opentype.js的getBoundingBox方法,获取文本的宽度、高度及偏移坐标,这是精准居中的基础。
  • 调整文本绘制起始位置:让文本在SVG画布内先居中,而非从x=0开始绘制。
  • 修正顶点坐标计算:基于文本的实际边界,确保所有顶点最终在Three.js场景中居中。
  • 自适应缩放:根据文本宽度动态调整缩放比例,保证长文本完整显示。

修改后的关键代码

async _setupTextModel() {
  const font = await load(UnDotumBold);

  // 1. 获取文本的实际包围盒
  const fontSize = 1000;
  const textBoundingBox = font.getBoundingBox(text, 0, fontSize, fontSize);
  const textWidth = textBoundingBox.x2 - textBoundingBox.x1;
  const textHeight = textBoundingBox.y2 - textBoundingBox.y1;
  
  // 计算文本在SVG中居中的起始偏移量
  // SVG的y轴向下,需调整y方向偏移保证文本垂直居中
  const textOffsetX = (1920 - textWidth) / 2 - textBoundingBox.x1;
  const textOffsetY = fontSize - (1920 - textHeight) / 2 + textBoundingBox.y1;

  // 创建SVG并生成路径
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
  svg.setAttribute("viewBox", [0, 0, 1920, 1920].join(" "));
  g.setAttribute("fill", "black");

  let pathMarkup = "";
  // 2. 使用计算好的偏移量绘制文本,确保文本在SVG中居中
  const fontPaths = font.getPaths(text, textOffsetX, textOffsetY, fontSize);

  fontPaths.forEach((fontPath) => {
    const path = fontPath.toSVG();
    pathMarkup += path;
  });
  g.insertAdjacentHTML("beforeend", pathMarkup);

  document.body.appendChild(svg);
  svg.appendChild(g);

  const paths = svg.querySelectorAll("path");
  const svgViewBoxWidth = svg.viewBox.baseVal.width;
  const svgViewBoxHeight = svg.viewBox.baseVal.height;

  svg.remove();

  // 生成顶点
  this._textVertices = [];
  const sizes = [];
  const delay = 1;
  const gradient = chroma.scale([
    "ef476f",
    "ffd166",
    "06d6a0",
    "118ab2",
    "073b4c",
  ]);
  const colorRGB = {
    r: Math.random() * 100,
    g: Math.random() * 100,
    b: Math.random() * 100,
  };
  const positionXYZ = {
    x: Math.random() * 3000,
    y: Math.random() * 3000,
    z: Math.random() * 3000,
  };

  const timeline = gsap.timeline({
    onReverseComplete: () => {
      timeline.timeScale(1);
      timeline.play(0);
    },
  });

  paths.forEach((path) => {
    const length = path.getTotalLength();

    for (let i = 0; i < length; i += 30) {
      const pointLength = i;
      const point = path.getPointAtLength(pointLength);

      // 3. 基于SVG viewBox中心计算顶点坐标,此时文本已在SVG中居中,转换后自然居中
      const vector = new THREE.Vector3(
        point.x - svgViewBoxWidth / 2,
        -point.y + svgViewBoxHeight / 2,
        (Math.random() - 0.5) * 15,
      );

      const start = new THREE.Vector3(
        vector.x + (Math.random() - 0.5) * positionXYZ.x,
        vector.y + (Math.random() - 0.5) * positionXYZ.y,
        vector.z + (Math.random() - 0.5) * positionXYZ.z,
      );

      const coloursX =
        point.x / svgViewBoxWidth + (Math.random() - 0.5) * 0.2;
      const color = gradient(coloursX).rgb();

      this._textVertices.push(vector);

      vector.r = 1 - (vector.z + 7.5) / colorRGB.r;
      vector.g = 1 - (vector.z + 7.5) / colorRGB.g;
      vector.b = 1 - (vector.z + 7.5) / colorRGB.b;

      timeline.from(
        vector,
        {
          x: start.x,
          y: start.y,
          z: start.z,
          r: color[0] / 255,
          g: color[1] / 255,
          b: color[2] / 255,
          duration: "random(0.5, 1.5)",
          ease: "power2.out",
        },
        delay * 0.0012,
      );

      sizes.push(25 * this.renderer.getPixelRatio());
    }
  });

  // 创建文本网格
  this._textGeometry = new THREE.BufferGeometry().setFromPoints(
    this._textVertices,
  );

  // 移除原无效的color属性,保留与_tick方法对应的customColor逻辑
  this._textGeometry.setAttribute(
    "size",
    new THREE.Float32BufferAttribute(sizes, 1),
  );

  const textMaterial = new THREE.ShaderMaterial({
    uniforms: {
      pointTexture: {
        value: new THREE.TextureLoader().load(ALPHAMAP.CIRCLE),
      },
    },
    vertexShader: this._vertexShader,
    fragmentShader: this._fragmentShader,
    transparent: true,
    depthTest: false,
  });

  this._textMesh = new THREE.Points(this._textGeometry, textMaterial);
  this._textMesh.scale.set(0.1, 0.1, 0.1);

  // 4. 根据文本宽度自适应缩放,避免长文本超出屏幕
  const maxTextWidth = 1600; // 设定SVG中允许的最大文本宽度
  const scaleFactor = textWidth > maxTextWidth ? maxTextWidth / textWidth : 1;
  this._textMesh.scale.multiplyScalar(scaleFactor);

  this._textGroup = new THREE.Group();
  this._textGroup.add(this._textMesh);
  this._textGroup.position.set(0, 0, -240);

  this._init();
}

额外说明

  • 原代码中colors数组为空,导致color属性无效,已移除该无效代码,保留与_tick方法匹配的customColor更新逻辑。
  • 自适应缩放的maxTextWidth值可根据实际场景调整,确保不同长度的文本都能完整显示。

内容的提问来源于stack exchange,提问作者LSS

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 01:54:14