普通视图
阿塞拜疆总理强调重视发展对华关系
天津:引导社会资本设立一批面向人工智能领域的天使投资基金、创业投资基金、产业投资基金
朱雀二号改进型遥二运载火箭发射成功
国轩高科宣布已建成全固态电池中试线
我国首个脑机接口临床与转化病房成立
华为云超节点创新算力基础设施
遥望科技将在5月底启动首批五到六个内容直播改革试点
得帆发布新一代AI战略
天津:加强核心算法技术研发
端午假期首日火车票开售 中短途线路成热门
从this丢失到精准绑定:用一个例子彻底搞懂bind的「救命」场景
前几天写代码时遇到个怪事:用setTimeout
调用对象方法,结果this.name
打印出undefined
。折腾半小时才发现是this
指向问题——直到用bind
修复后,代码才正常运行。今天就用这个真实案例,结合call
、apply
、bind
的核心差异,带大家彻底搞懂「绑定this」的底层逻辑。
先看一个「翻车现场」:setTimeout里的this丢失
先看用户提供的代码(稍作修改更直观):
var obj = {
name: 'cherry',
func1: function() {
console.log("当前this指向:", this);
console.log("打印name:", this.name);
},
func2: function() {
setTimeout(function() {
this.func1(); // 目标:调用obj的func1
}, 1000);
}
};
obj.func2();
// 1秒后输出:
// 当前this指向: Window {...}(浏览器环境)
// 打印name: undefined
为什么会翻车?
setTimeout
的回调函数是独立调用的。在JavaScript中,函数的this
在调用时动态绑定,规则是:
- 如果函数作为对象的方法调用(如
obj.func2()
),this
指向该对象; - 如果函数独立调用(如
function() {}
),this
默认指向全局对象(浏览器是window
,严格模式是undefined
)。
上面的代码中,setTimeout
的回调函数是独立调用的,所以this
指向window
。而window
没有func1
方法(除非你全局定义过),自然会报错this.func1 is not a function
(或打印undefined
)。
用bind「拯救」this:用户代码的正确写法
用户提供的修复方案是用bind(obj)
绑定this
:
var obj = {
name: 'cherry',
func1: function() {
console.log("当前this指向:", this);
console.log("打印name:", this.name);
},
func2: function() {
setTimeout(function() {
this.func1();
}.bind(obj), 1000); // 关键:用bind绑定obj作为this
}
};
obj.func2();
// 1秒后输出:
// 当前this指向: {name: 'cherry', func1: ƒ, func2: ƒ}
// 打印name: cherry
为什么bind能解决问题?
bind(obj)
做了两件事:
-
返回一个新函数:这个新函数的
this
被永久绑定为obj
,无论它在哪里调用; -
不立即执行:
bind
不会像call/apply
那样立即执行原函数,而是等待setTimeout
触发时执行新函数。
经过1秒后再出现:
对比call和apply:为什么不能用它们?
如果尝试用call
或apply
:
// 错误示范:用call
setTimeout(function() {
this.func1();
}.call(obj), 1000);
// 结果:立即执行(不会等1秒),且setTimeout的第一个参数变成了undefined(因为call立即执行函数,返回值是undefined)
call
和apply
会立即执行函数,导致两个问题:
- 函数会在
setTimeout
注册时立即执行,而不是延迟1秒; -
setTimeout
的第一个参数需要是函数(或字符串),但call
执行后的返回值是func1
的执行结果(这里是undefined
),导致定时器无效。
使用call和apply都是以下效果,cherry都是立即执行:
深入理解bind:它到底做了什么?
1. 绑定this,忽略后续调用的this
用bind
绑定this
后,无论新函数如何调用,this
都会指向绑定的值:
const boundFunc = function() {
console.log(this.name);
}.bind({ name: '绑定对象' });
boundFunc(); // 输出:绑定对象(即使独立调用)
boundFunc.call({ name: 'call的this' }); // 输出:绑定对象(call无法覆盖bind的绑定)
2. 预填充参数,创建「偏函数」
bind
可以提前传递部分参数,后续调用时只需传剩余参数:
function add(a, b) {
return a + b;
}
const add5 = add.bind(null, 5); // 预填充第一个参数为5
console.log(add5(3)); // 8(相当于add(5,3))
3. 被new操作符覆盖的绑定
如果用bind
绑定的函数作为构造函数(用new
调用),bind
的this
会被new
创建的新对象覆盖:
const Person = function(name) {
this.name = name;
};
const BoundPerson = Person.bind({}); // 绑定this为{}
const p = new BoundPerson('张三');
console.log(p.name); // 张三(this指向新创建的p对象)
开发中最常用的5种场景(含用户案例)
1. 解决定时器/回调函数的this丢失(用户案例)
setTimeout
、setInterval
的回调函数常因独立调用导致this
丢失,bind
是最直接的解决方案:
// 用户代码解析:
// 1. obj.func2调用时,this指向obj;
// 2. setTimeout的回调函数通过.bind(obj)绑定this为obj;
// 3. 1秒后回调执行时,this指向obj,成功调用func1。
2. 继承父类构造函数(经典OOP)
用构造函数实现继承时,call
可以调用父类构造函数,绑定子类实例的this
:
function Animal(name) {
this.name = name;
}
function Dog(name, age) {
// 用call调用父类,绑定Dog实例的this
Animal.call(this, name);
this.age = age;
}
const dog = new Dog('小黑', 3);
console.log(dog); // { name: '小黑', age: 3 }
3. 数组方法应用于类数组(apply经典用法)
类数组对象(如arguments
、DOM节点集合)可以用apply
调用数组方法:
function sum() {
// arguments是类数组对象(有length但无数组方法)
// 用apply调用Array.prototype.reduce,计算总和
return Array.prototype.reduce.apply(arguments, [function(acc, cur) {
return acc + cur;
}, 0]);
}
console.log(sum(1, 2, 3)); // 6
5. 求数组的最大值/最小值(apply简化代码)
Math.max
需要多个参数,apply
可以将数组展开为参数列表:
const scores = [95, 88, 92, 100, 85];
const maxScore = Math.max.apply(null, scores); // 100(等价于Math.max(95,88,92,100,85))
总结:记住这3个关键点
-
call和apply:立即执行函数,
call
传参数列表,apply
传数组; -
bind:返回绑定后的新函数,不立即执行,适合延迟调用(如
setTimeout
、事件绑定); -
核心价值:解决
this
指向问题,让函数在指定上下文中执行。
回到用户的例子,bind(obj)
就像给setTimeout
的回调函数装了一个「定位器」,无论它在哪里执行,this
都会精准指向obj
。这就是bind
最核心的作用——固定this,让函数的执行上下文可控。
下次遇到this
丢失的问题,先想想:是需要立即执行(用call/apply),还是延迟执行(用bind)?想清楚这一点,就能在开发中灵活选择啦!
华泰证券:看好海外燃气轮机主机的量价齐升
清华大学五道口金融学院院长焦捷:“五篇大文章”构成中国金融高质量发展新驱动力
魏牌全新高山正式上市
计算机图形学中的齐次坐标:从基础到应用
一、齐次坐标的基本概念
在计算机图形学中,齐次坐标是一种用 N+1 维向量表示 N 维空间点的方法。在二维空间中,我们通常用 (x, y) 表示一个点,但在齐次坐标中,这个点被表示为 (x, y, w)。其中,w 是一个额外的坐标分量,当 w ≠ 0 时,对应的二维笛卡尔坐标为 (x/w, y/w)。
齐次坐标的优势在于它能够统一处理点和向量,并且能够简洁地表示平移、旋转、缩放等变换。
为什么需要齐次坐标?
在二维空间中,我们可以用矩阵乘法来表示旋转和缩放变换,但平移变换无法直接用 2×2 矩阵表示。齐次坐标通过增加一个维度,让我们能够用 3×3 矩阵统一表示所有的仿射变换(平移、旋转、缩放、剪切等)。
二、齐次坐标的表示
在齐次坐标中:
- 二维点表示为 (x, y, 1)
- 二维向量表示为 (x, y, 0)
- 当 w ≠ 1 时,点 (x, y, w) 对应的笛卡尔坐标为 (x/w, y/w)
向量的 w 分量为 0,这意味着向量在平移变换下不会改变,而点会受到平移的影响。
三、齐次坐标的变换矩阵
在齐次坐标系统中,二维变换可以用 3×3 矩阵表示。下面我们用 JavaScript 实现一个简单的齐次坐标和变换矩阵库。
JavaScript 实现
下面是一个基于齐次坐标的二维变换库的实现,包含了点和向量的表示以及基本变换矩阵的创建和应用。
class Vector3 {
constructor(x, y, w) {
this.x = x;
this.y = y;
this.w = w !== undefined ? w : 1; // 默认 w 为 1,表示点
}
// 转换为笛卡尔坐标
toCartesian() {
if (this.w === 0) {
return new Vector3(this.x, this.y, 0); // 向量保持不变
}
return new Vector3(this.x / this.w, this.y / this.w, 1);
}
// 应用变换矩阵
applyMatrix(matrix) {
const { x, y, w } = this;
const m = matrix.values;
return new Vector3(
m[0][0] * x + m[0][1] * y + m[0][2] * w,
m[1][0] * x + m[1][1] * y + m[1][2] * w,
m[2][0] * x + m[2][1] * y + m[2][2] * w
);
}
// 向量加法(仅用于 w=0 的向量)
add(v) {
if (this.w !== 0 || v.w !== 0) {
throw new Error('Vector addition is only defined for vectors (w=0)');
}
return new Vector3(this.x + v.x, this.y + v.y, 0);
}
// 向量点积(仅用于 w=0 的向量)
dot(v) {
if (this.w !== 0 || v.w !== 0) {
throw new Error('Dot product is only defined for vectors (w=0)');
}
return this.x * v.x + this.y * v.y;
}
// 向量长度(仅用于 w=0 的向量)
length() {
if (this.w !== 0) {
throw new Error('Length is only defined for vectors (w=0)');
}
return Math.sqrt(this.dot(this));
}
}
class Matrix3 {
constructor() {
// 初始化为单位矩阵
this.values = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
];
}
// 设置为平移矩阵
setTranslation(tx, ty) {
this.values = [
[1, 0, tx],
[0, 1, ty],
[0, 0, 1]
];
return this;
}
// 设置为旋转变换矩阵
setRotation(angleInRadians) {
const c = Math.cos(angleInRadians);
const s = Math.sin(angleInRadians);
this.values = [
[c, -s, 0],
[s, c, 0],
[0, 0, 1]
];
return this;
}
// 设置为缩放变换矩阵
setScaling(sx, sy) {
this.values = [
[sx, 0, 0],
[0, sy, 0],
[0, 0, 1]
];
return this;
}
// 矩阵乘法
multiply(matrix) {
const result = new Matrix3();
const a = this.values;
const b = matrix.values;
const c = result.values;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
}
}
return result;
}
// 应用于点或向量
transform(point) {
return point.applyMatrix(this);
}
}
四、基本变换的实现
1. 平移变换
平移变换将点 (x, y) 移动到 (x + tx, y + ty)。在齐次坐标中,平移矩阵为:
// 创建平移矩阵
const translationMatrix = new Matrix3();
translationMatrix.setTranslation(50, 100); // 沿 x 轴平移 50,沿 y 轴平移 100
// 创建点 (10, 20)
const point = new Vector3(10, 20);
// 应用平移变换
const translatedPoint = translationMatrix.transform(point);
console.log(translatedPoint.toCartesian()); // 输出: (60, 120, 1)
2. 旋转变换
旋转变换将点绕原点旋转一定角度。在齐次坐标中,旋转矩阵为:
// 创建旋转变换矩阵(绕原点旋转 90 度)
const rotationMatrix = new Matrix3();
rotationMatrix.setRotation(Math.PI / 2); // 90 度 = π/2 弧度
// 创建点 (10, 0)
const point = new Vector3(10, 0);
// 应用旋转变换
const rotatedPoint = rotationMatrix.transform(point);
console.log(rotatedPoint.toCartesian()); // 输出: (0, 10, 1)
3. 缩放变换
缩放变换改变点的大小。在齐次坐标中,缩放矩阵为:
// 创建缩放变换矩阵(x 方向缩放 2 倍,y 方向缩放 0.5 倍)
const scalingMatrix = new Matrix3();
scalingMatrix.setScaling(2, 0.5);
// 创建点 (10, 20)
const point = new Vector3(10, 20);
// 应用缩放变换
const scaledPoint = scalingMatrix.transform(point);
console.log(scaledPoint.toCartesian()); // 输出: (20, 10, 1)
五、变换的组合
在计算机图形学中,我们经常需要组合多个变换。例如,先旋转,再平移,最后缩放。变换的组合顺序非常重要,因为矩阵乘法不满足交换律。
变换顺序示例
下面的例子展示了不同变换顺序的效果:
// 创建一个点
const point = new Vector3(10, 0);
// 创建变换矩阵
const translationMatrix = new Matrix3().setTranslation(50, 0);
const rotationMatrix = new Matrix3().setRotation(Math.PI / 2);
// 顺序 1: 先旋转后平移
const transform1 = translationMatrix.multiply(rotationMatrix);
const result1 = transform1.transform(point);
console.log("先旋转后平移:", result1.toCartesian()); // 输出: (50, 10, 1)
// 顺序 2: 先平移后旋转
const transform2 = rotationMatrix.multiply(translationMatrix);
const result2 = transform2.transform(point);
console.log("先平移后旋转:", result2.toCartesian()); // 输出: (-10, 60, 1)
六、齐次坐标在透视投影中的应用
齐次坐标在透视投影中也有重要应用。透视投影是模拟人眼视觉的一种投影方式,远处的物体看起来比近处的小。
在透视投影中,我们可以通过修改齐次坐标的 w 分量来实现这种效果。例如,将点 (x, y, z) 投影到 z=0 的平面上:
// 透视投影矩阵
function createPerspectiveMatrix(d) {
const matrix = new Matrix3();
matrix.values = [ [1, 0, 0],
[0, 1, 0],
[0, 0, 1/d]
];
return matrix;
}
// 创建透视投影矩阵(d 是投影平面到视点的距离)
const perspectiveMatrix = createPerspectiveMatrix(100);
// 创建三维点 (50, 0, 50) 在齐次坐标中表示为 (50, 0, 50, 1)
// 注意:我们的 Vector3 类可以处理这种情况,w 分量默认为 1
const point3D = new Vector3(50, 0, 50);
// 应用透视投影
const projectedPoint = perspectiveMatrix.transform(point3D);
console.log("投影后的点:", projectedPoint.toCartesian()); // 输出: (100, 0, 1)
七、应用实例:实现一个简单的图形变换工具
下面我们用 HTML5 Canvas 和上面实现的齐次坐标库来创建一个简单的图形变换工具,支持平移、旋转和缩放。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>齐次坐标图形变换示例</title>
<style>
canvas {
border: 1px solid #ccc;
}
.controls {
margin-top: 10px;
}
</style>
</head>
<body>
<canvas id="canvas" width="500" height="400"></canvas>
<div class="controls">
<button id="translateBtn">平移</button>
<button id="rotateBtn">旋转</button>
<button id="scaleBtn">缩放</button>
<button id="resetBtn">重置</button>
</div>
<script>
// 前面定义的 Vector3 和 Matrix3 类的代码放在这里
// 获取 canvas 和绘图上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 创建一个简单的图形(三角形)
const originalPoints = [
new Vector3(100, 50),
new Vector3(200, 150),
new Vector3(50, 150)
];
// 当前变换矩阵
let transformMatrix = new Matrix3();
// 绘制图形
function draw() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 应用变换
const transformedPoints = originalPoints.map(point =>
transformMatrix.transform(point).toCartesian()
);
// 绘制坐标系
ctx.strokeStyle = '#ccc';
ctx.beginPath();
ctx.moveTo(0, canvas.height / 2);
ctx.lineTo(canvas.width, canvas.height / 2);
ctx.moveTo(canvas.width / 2, 0);
ctx.lineTo(canvas.width / 2, canvas.height);
ctx.stroke();
// 绘制变换后的图形
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.beginPath();
ctx.moveTo(transformedPoints[0].x, transformedPoints[0].y);
for (let i = 1; i < transformedPoints.length; i++) {
ctx.lineTo(transformedPoints[i].x, transformedPoints[i].y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
// 绘制变换后的点
ctx.fillStyle = 'blue';
transformedPoints.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
ctx.fill();
});
}
// 初始化绘制
draw();
// 添加按钮事件
document.getElementById('translateBtn').addEventListener('click', () => {
// 创建平移矩阵并与当前矩阵相乘
const translateMatrix = new Matrix3().setTranslation(50, 30);
transformMatrix = translateMatrix.multiply(transformMatrix);
draw();
});
document.getElementById('rotateBtn').addEventListener('click', () => {
// 创建旋转变换矩阵并与当前矩阵相乘
const rotateMatrix = new Matrix3().setRotation(Math.PI / 12); // 15度
transformMatrix = rotateMatrix.multiply(transformMatrix);
draw();
});
document.getElementById('scaleBtn').addEventListener('click', () => {
// 创建缩放变换矩阵并与当前矩阵相乘
const scaleMatrix = new Matrix3().setScaling(1.2, 1.2);
transformMatrix = scaleMatrix.multiply(transformMatrix);
draw();
});
document.getElementById('resetBtn').addEventListener('click', () => {
// 重置变换矩阵
transformMatrix = new Matrix3();
draw();
});
</script>
</body>
</html>
八、总结
齐次坐标是计算机图形学中一个非常重要的概念,它通过增加一个维度,统一了点和向量的表示,并且能够用矩阵乘法简洁地表示各种变换。掌握齐次坐标和变换矩阵是理解和实现更复杂图形算法的基础。
通过本文的介绍和示例代码,你应该对齐次坐标有了基本的理解,并且能够实现简单的图形变换。在实际应用中,齐次坐标还广泛应用于 3D 图形、计算机视觉和机器人学等领域。
81.爬楼梯
假设你正在爬楼梯。需要
n
阶你才能到达楼顶。每次你可以爬
1
或2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
解法1 暴力递归和记忆化递归
思路
这本质上就是一个斐波那契数列,所以当 n
走到只有 1
个台阶的时候,只有一种解法。
然后递归去处理。到达 n
这个台阶,有两种方法,要么是走 1
步,要么是走 2
步,所以返回结果是 (n - 1) + (n - 2)
。
但是这个方法存在大量的计算,所以无法通过题解。如果这个解法要想通过题解,必须要保存已经计算过的步数,下面给出两种方法。
代码
朴素计算
function climbStairs(n: number): number {
if (n <= 1) return 1;
return climbStairs(n - 1) + climbStairs(n - 2);
};
memo缓存
function climbStairs(n: number): number {
const memo = new Array(n + 1).fill(-1); // -1 代表未计算
memo[0] = 1;
memo[1] = 1;
function dfs(i: number): number {
if (memo[i] !== -1) return memo[i];
memo[i] = dfs(i - 1) + dfs(i - 2);
return memo[i];
}
return dfs(n);
}
时空复杂度
时间复杂度:朴素 O(2^n)
,记忆化O(n)
空间复杂度:朴素 O(n)
,记忆化O(n)
解法2 动态规划
思路
其实上面已经将问题分解成了子问题,到达这个台阶只有两种方法,要么 n - 1
,要么 n - 2
。
所以其实只需要两个变量就可计算完毕当前台阶,即 prev
和 cur
。当前台阶就是 prev + cur
,然后再更新 prev
和 cur
。
代码
function climbStairs(n: number): number {
if (n <= 1) return 1;
let prev = 1;
let cur = 1;
for (let i = 2; i <= n; i++) {
const temp = prev + cur;
prev = cur;
cur = temp;
}
return cur;
}
时空复杂度
时间复杂度:O(n)
空间复杂度:O(1)
three.js 字体使用全解析
在 3D 可视化项目中,文字是传递信息的重要元素。Three.js 作为强大的 3D 库,提供了多种添加和处理文字的方式。本文将深入探讨 Three.js 中字体的使用方法,从基础文字渲染到高级文字动画,帮助你在 3D 场景中完美呈现文字内容。
一、Three.js 中字体的基本概念
在 Three.js 中使用字体,主要有两种方式:
- 基于 Canvas 的 2D 文字渲染:使用 Canvas 绘制文字,然后将其作为纹理应用到 3D 平面上。
- 3D 文字几何体:使用 Three.js 提供的 TextGeometry 创建具有厚度和深度的 3D 文字模型。
这两种方式各有优缺点,适用于不同的场景。下面我们将详细介绍这两种方式的实现方法。
二、使用 Canvas 生成文字纹理
基础实现方法
通过 Canvas 生成文字纹理是一种简单且灵活的方法,特别适合需要动态更新文字内容的场景。
// 创建Canvas文字纹理函数
function createTextTexture(text, parameters = {}) {
parameters = Object.assign({
fontface: "Arial",
fontsize: 72,
backgroundColor: { r: 0, g: 0, b: 0, a: 0 },
textColor: { r: 255, g: 255, b: 255, a: 255 }
}, parameters);
// 创建Canvas元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 设置Canvas尺寸
context.font = `${parameters.fontsize}px ${parameters.fontface}`;
const textWidth = context.measureText(text).width;
// 设置Canvas尺寸,考虑文字宽度和高度
canvas.width = textWidth + parameters.fontsize;
canvas.height = parameters.fontsize * 2;
// 重置字体设置,因为Canvas尺寸改变后可能会重置
context.font = `${parameters.fontsize}px ${parameters.fontface}`;
context.textBaseline = 'middle';
context.textAlign = 'center';
// 绘制背景
context.fillStyle = `rgba(${parameters.backgroundColor.r}, ${parameters.backgroundColor.g}, ${parameters.backgroundColor.b}, ${parameters.backgroundColor.a})`;
context.fillRect(0, 0, canvas.width, canvas.height);
// 绘制文字
context.fillStyle = `rgba(${parameters.textColor.r}, ${parameters.textColor.g}, ${parameters.textColor.b}, ${parameters.textColor.a})`;
context.fillText(text, canvas.width / 2, canvas.height / 2);
// 创建纹理
const texture = new THREE.CanvasTexture(canvas);
return texture;
}
// 在Three.js场景中使用Canvas生成的文字纹理
function addTextToScene() {
// 创建文字纹理
const texture = createTextTexture("Hello Three.js", {
fontface: "Arial",
fontsize: 48,
textColor: { r: 255, g: 255, b: 255 },
backgroundColor: { r: 0, g: 0, b: 0, a: 0 }
});
// 创建材质和平面几何体
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true });
const geometry = new THREE.PlaneGeometry(2, 1);
const textMesh = new THREE.Mesh(geometry, material);
// 添加到场景
scene.add(textMesh);
}
高级应用:动态更新文字内容
Canvas 纹理的一个重要优势是可以动态更新文字内容。以下是一个实现动态更新文字的示例:
// 动态更新文字内容
function updateText(text) {
// 重新生成纹理
const texture = createTextTexture(text);
// 更新材质的纹理
textMesh.material.map.dispose();
textMesh.material.map = texture;
textMesh.material.needsUpdate = true;
}
// 在动画循环中更新文字
let counter = 0;
function animate() {
requestAnimationFrame(animate);
// 每100帧更新一次文字
if (counter % 100 === 0) {
updateText(`Frame: ${counter}`);
}
counter++;
renderer.render(scene, camera);
}
三、使用 TextGeometry 创建 3D 文字
准备字体文件
使用 TextGeometry 需要先加载字体文件。Three.js 使用 JSON 格式的字体文件,这些文件可以通过 Three.js 提供的字体工具生成。
以下是加载字体并创建 3D 文字的示例:
// 加载字体并创建3D文字
function loadFontAndCreateText() {
const loader = new THREE.FontLoader();
// 加载字体文件
loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
// 创建文字几何体
const geometry = new THREE.TextGeometry('Three.js 3D Text', {
font: font,
size: 0.5,
height: 0.1, // 文字厚度
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5
});
// 计算边界框以居中文字
geometry.computeBoundingBox();
const centerOffset = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x);
// 创建材质
const material = new THREE.MeshPhongMaterial({
color: 0x44aa88,
specular: 0x111111
});
// 创建网格
const textMesh = new THREE.Mesh(geometry, material);
// 设置位置
textMesh.position.x = centerOffset;
textMesh.position.y = 0.2;
textMesh.position.z = 0;
// 添加到场景
scene.add(textMesh);
});
}
优化 3D 文字性能
对于复杂场景中的大量文字,性能可能成为问题。以下是一些优化建议:
- 合并几何体:如果有多个静态文字,可以使用BufferGeometryUtils.mergeBufferGeometries合并它们以减少绘制调用。
- 降低细节:减少curveSegments和bevelSegments的值可以显著提高性能。
- 使用实例化:对于重复的文字,可以使用THREE.InstancedMesh。
四、文字的高级效果与动画
文字材质与光照效果
使用不同的材质可以为文字带来不同的视觉效果:
// 使用不同材质的文字示例
function createTextWithMaterials() {
// 加载字体
const loader = new THREE.FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
// 创建基础文字几何体
const geometry = new THREE.TextGeometry('Materials', {
font: font,
size: 0.4,
height: 0.1
});
// 不同材质的文字
const materials = [
new THREE.MeshBasicMaterial({ color: 0xff0000 }),
new THREE.MeshLambertMaterial({ color: 0x00ff00 }),
new THREE.MeshPhongMaterial({ color: 0x0000ff, shininess: 100 }),
new THREE.MeshStandardMaterial({ color: 0xffff00, metalness: 0.5, roughness: 0.5 })
];
// 为每种材质创建一个文字实例
materials.forEach((material, index) => {
const textMesh = new THREE.Mesh(geometry.clone(), material);
textMesh.position.set(-2 + index * 1.5, 0, 0);
scene.add(textMesh);
});
});
}
文字动画效果
以下是一个文字波浪动画的实现:
// 文字波浪动画
function createWavyText() {
const loader = new THREE.FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
const geometry = new THREE.TextGeometry('Wave Animation', {
font: font,
size: 0.5,
height: 0.1
});
// 创建顶点材质
const material = new THREE.ShaderMaterial({
vertexShader: `
uniform float time;
varying vec3 vPosition;
void main() {
vPosition = position;
// 添加波浪效果
float offset = sin(position.x * 5.0 + time) * 0.05;
vec3 newPosition = position + normal * offset;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
`,
fragmentShader: `
varying vec3 vPosition;
void main() {
// 基于位置创建颜色渐变
vec3 color = mix(vec3(0.2, 0.2, 1.0), vec3(1.0, 0.2, 0.2), vPosition.y + 0.5);
gl_FragColor = vec4(color, 1.0);
}
`,
uniforms: {
time: { value: 0.0 }
},
side: THREE.DoubleSide,
transparent: false
});
const textMesh = new THREE.Mesh(geometry, material);
scene.add(textMesh);
// 动画循环
function animate() {
requestAnimationFrame(animate);
material.uniforms.time.value += 0.05;
renderer.render(scene, camera);
}
animate();
});
}
五、性能优化与最佳实践
字体文件管理
- 对于小型项目,可以直接使用 Three.js 提供的内置字体。
- 对于大型项目,考虑创建自定义字体以减少文件大小。
- 使用字体压缩工具减小字体文件体积。
动态文字的性能考量
- 频繁更新 Canvas 纹理会影响性能,尽量减少更新频率。
- 对于实时数据显示,考虑使用数字精灵而非完整文字。
渲染性能优化
- 对于远距离可见的文字,考虑使用低精度几何体。
- 使用THREE.LOD(Level of Detail)根据距离动态切换文字精度。
六、实际应用案例
游戏中的 3D 文字
在游戏中,3D 文字可用于显示玩家名称、分数或游戏状态:
// 游戏中的玩家名称显示
function createPlayerNameTag(player) {
const loader = new THREE.FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
const geometry = new THREE.TextGeometry(player.name, {
font: font,
size: 0.2,
height: 0.02
});
const material = new THREE.MeshPhongMaterial({ color: player.color });
const nameTag = new THREE.Mesh(geometry, material);
// 设置文字位置,使其始终面向相机
nameTag.position.set(0, 2, 0);
player.add(nameTag);
// 确保文字始终面向相机
scene.add(new THREE.PointLight(0xffffff, 1, 100));
scene.add(new THREE.PointLight(0xffffff, 0.5, 100));
});
}
数据可视化中的文字标签
在数据可视化中,文字标签用于标识数据点:
// 数据可视化中的标签
function addDataLabels(data) {
data.forEach((item, index) => {
const texture = createTextTexture(item.label, {
fontsize: 36,
textColor: { r: 255, g: 255, b: 255 }
});
const material = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(material);
// 设置位置
sprite.position.set(item.x, item.y, item.z);
sprite.scale.set(1, 0.5, 1);
scene.add(sprite);
});
}
七、常见问题与解决方案
文字模糊问题
- 原因:Canvas 尺寸过小或纹理过滤设置不当。
- 解决方案:增加 Canvas 尺寸,或设置纹理的minFilter和magFilter为THREE.NearestFilter。
3D 文字渲染不完整
- 原因:相机的near值设置过大,或文字超出了视锥体。
- 解决方案:减小相机的near值,或调整文字位置使其在视锥体内。
性能问题
- 原因:过多的文字对象或复杂的文字几何体。
- 解决方案:合并几何体、使用实例化、降低细节级别或使用 Canvas 纹理替代 3D 文字。
八、总结
Three.js 提供了丰富的字体使用方式,从简单的 2D 文字纹理到复杂的 3D 文字几何体,能够满足各种场景的需求。在实际应用中,应根据具体需求选择合适的方法,并注意性能优化。通过合理使用材质、动画和交互效果,可以为 3D 场景增添丰富的信息和生动的视觉体验。
希望本文能帮助你掌握 Three.js 中字体的使用技巧,创造出更加精彩的 3D 项目!
父子都有点击事件,阻止冒泡event.stopPropagation()
为什么需要阻止冒泡?
假设结构是这样包裹的,两层都有点击事件,
<button @click="handleContinue">继续学习</button>
</div>
在点击内层的按钮时,会导致向父元素冒泡外层元素的点击事件同时也被触发,从而可能导致多个 $router.push()
被调用,导致两次路由跳转 → 而第一次被取消,控制台就会报错。
阻止冒泡的正确使用:
1、结构中事件中携带event:
@click="handleContinue($event)" > 继续学习 </el-button>
2、 阻止冒泡event.stopPropagation()
methods: {
handleContinue(event) {
event.stopPropagation(); // 阻止冒泡
console.log('跳转继续学习 /my-courses');
this.$router.push('/my-courses');
},
bannerClick() {
this.$router.push('/my-content');
}
}
这样就能避免因事件冒泡导致的重复导航和控制台警告了。
event.stopPropagation()方法用于阻止事件继续传播,适用于需要精确控制事件影响范围的场景,如防止点击一个元素时触发其父元素的特定行为。 event.preventDefault()方法用于阻止事件的默认行为,适用于需要自定义处理某些操作的场景,如表单提交、链接导航等。