一: 应用场景
在一些C端的活动中,我们可能会需要实现一个动态拼字的效果,例如下面这样:
Canvas动态拼字效果视频
3d动态拼字效果视频
二:实现思路:
上面视频中的文本点位信息我们可以通过canvas来获取:
- 将文本绘制在canvas画布上。
- 通过canvas的2d上下文调用getImageData来拿到像素数据。
- 对像素数据进行过滤拿到文本位置的像素信息。
我们在黑色背景的canvas画布上绘制白色的文本,然后循环canvas上的每一个像素,通过像素的颜色值来判断该像素点是属于文本还是背景。
三: 关键代码
1. 简单版:
// 获取canvas的2d上下文
this.canvas = document.getElementById("myCanvas");
this.ctx = this.canvas.getContext("2d");
// 存储文本点位数据
this.textPointData = [];
// 调用getImageData方法获取canvas区域的像素数据
let imgData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
let pxl = imgData.data;
// 循环canvas每个像素点,过滤出文字部分的像素信息
for (let i = 0; i < this.canvas.width * this.canvas.height; i++) {
let alphaIndex = i * 4 + 3;
if (pxl[i * 4 + 3] > 0) {
// 根据alpha值获得像素的线性位置。
let linearPosition = alphaIndex / 4;
// 将线性位置除以宽度以得到Y坐标
let y = Math.floor(linearPosition / this.canvas.width);
// 将y坐标乘以宽度,并从线性位置减去整个值以得到X坐标。
let x = linearPosition - (Math.floor(linearPosition / this.canvas.width) * this.canvas.width);
this.textPointData.push({
x,
y,
z: 0,
})
}
}
2. 升级版
增加功能:
- 文本是否填充。
- 相邻点的间距设置。
- 自定义像素过滤条件。
/** 获取文本点位信息 */
export default class TextToPoints {
/** 采样的文本 */
str: string = "神小夜";
/** 是否填充 */
fill: boolean = false;
/** 相邻点位的间距 */
separation: number = 2;
/** 像素的过滤函数 */
pixelCallback: any;
canvas: any;
constructor(canvas, str, fill, separation?, pixelCallback?) {
this.canvas = canvas;
this.str = str;
this.fill = fill;
this.separation = separation || 2;
this.pixelCallback = pixelCallback || this.defaultPixelCallback;
}
/** 默认pixelCallback:检查是否白色文本在黑色背景 */
defaultPixelCallback(color) {
return ((color[0] + color[1] + color[2]) / 3 > 50);
}
/**
* 获取每个像素点颜色
*/
checkPixel(i, imageData) {
if (i < 0 || i > imageData.length) return false;
const red = imageData[i];
const green = imageData[i + 1];
const blue = imageData[i + 2];
const alpha = imageData[i + 3];
const color = [red, green, blue, alpha];
return this.pixelCallback(color);
};
/**
* 获取separation间隔的点位数据
* @Param separation 相邻点位的间隔距离
* @Param width canvas宽
* @Param height canvas高
* @Param context canvas 2d ctx
*/
scan(separation, width, height, context) {
const imageData = context.getImageData(0, 0, width, height).data;
const points = [];
const map = {};
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
// 拿到每个像素点数据第一位的索引
const pos = (i * width + j) * 4;
// 过滤获取点位
if (this.checkPixel(pos, imageData) && (
this.fill ||
i == 0 ||
i == height - 1 ||
j == 0 ||
j == width - 1 ||
!this.checkPixel(pos - 4, imageData) ||
!this.checkPixel(pos + 4, imageData) ||
!this.checkPixel(pos - width * 4, imageData) ||
!this.checkPixel(pos + width * 4, imageData))
) {
let alreadyAdded = false;
// 循环(i,j)点 的 separation范围内的点
for (let inner_i = i - separation; inner_i < i + separation; inner_i++) {
for (let inner_j = j - separation; inner_j < j + separation; inner_j++) {
if (inner_j == j && inner_i == i) continue;
if (map[inner_i + "_" + inner_j]) {
alreadyAdded = true;
break;
}
}
if (alreadyAdded) break;
}
// 已经添加的点位,不再push进数组
if (!alreadyAdded) {
points.push({x: j, y: i});
map[i + "_" + j] = true;
}
}
}
}
return points;
};
/** 获取点位数据 */
getPoints() {
const canvas_temp = document.createElement("canvas");
const context_temp = canvas_temp.getContext('2d');
context_temp.font = "200px Verdana";
// 拿到绘制的文本 的width, 并设置height
const text_measure: any = context_temp.measureText(this.str);
text_measure.height = 300;
canvas_temp.width = text_measure.width;
canvas_temp.height = text_measure.height;
context_temp.fillStyle = "black";
context_temp.fillRect(0, 0, this.canvas.width, this.canvas.height);
context_temp.fillStyle = "white";
context_temp.font = "200px Verdana";
context_temp.textAlign = "center";
context_temp.textBaseline = "middle";
context_temp.fillText(this.str, canvas_temp.width / 2, canvas_temp.height / 2);
const points = this.scan(this.separation, canvas_temp.width, canvas_temp.height, context_temp);
return {
points,
textW: text_measure.width,
textH: text_measure.height,
}
};
}
获取点位数据
const data = new TextToPoints(this.canvas, "神小夜", false).getPoints();
四: 结语
上面的实现方法并不局限于获取文本点位信息,只要是画在canvas上的东西都是同理的。比如logo,头像等等。