乐趣区

庆祝新年?画一颗圣诞树?还是…

关于节日
圣诞节,元旦,看大家 (情侣) 在朋友圈里发各种庆祝的或者祝福的话语,甚是感动,然后悄悄拉黑了。作为单身狗,我们也有自己庆祝节日的方式,今天我们就来实现一些祝福的效果。
需要说明的是,所有的效果都是利用 canvas 来实现的。
祝福话语

偷了朋友的图,很基本的庆祝方式,展示不同的文字,一段时间切换一次,普普通通,但是对于低像素来说,是最好的方法了,也是庆祝节日用的最多的了,我们这里做个效果多一点的版本效果展示:
基本原理是这样的:

在 canvas 中把字画出来,渐变色效果,通过 canvas 的相关 API 获取 imageData,就是像素点信息,同 rgba。
遍历 imageData,生成相关 dom。
设置定时,因为渲染不同的文字效果,当然,有过渡效果。

过程对应的代码:

在 canvas 里写字,且渐变效果:
// 像素点的单位长度
const rectWidth =
parseFloat(document.documentElement.style.getPropertyValue(‘–rect-width’));
const canvas = document.createElement(‘canvas’);
canvas.width = 100;
canvas.height = 20;

const ctx = canvas.getContext(‘2d’);
ctx.font = ‘100 18px monospace’;
ctx.textBaseline = ‘top’; // 设置文字基线
ctx.textAlign = ‘center’;
// 将区域内所有像素点设置成透明
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 渐变效果
const gradient = ctx.createLinearGradient(10, 0, canvas.width – 10, 0);
gradient.addColorStop(0, ‘red’);
gradient.addColorStop(1 / 6, ‘orange’);
gradient.addColorStop(2 / 6, ‘yellow’);
gradient.addColorStop(3 / 6, ‘green’);
gradient.addColorStop(4 / 6, ‘blue’);
gradient.addColorStop(5 / 6, ‘indigo’);
gradient.addColorStop(1, ‘violet’);
ctx.fillStyle = gradient;

// y 设置 2,是因为火狐浏览器下效果有异常 …
ctx.fillText(‘ 这是测试 ’, canvas.width / 2, 2);
// 插入
document.body.appendChild(canvas);

像素点过多会卡顿,所以这里尽量用少的点去完成效果
获取 imageData,生成相关 dom
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 打印一下
console.log(imageData);
imageData 包含三个属性,data,width 和 height,data 是一个一维数组,[[0-255], [0-255], [0-255], [0-255]],长度是 4 的倍数,4 个算一小组,相当于 rgba,只不过透明度范围也是 0~255,width 和 height 相当于长宽,像素点数量 = (高 宽) 4
{
let i = 2000;
const fragment = document.createDocumentFragment();
while (i– > 0) {
fragment.appendChild(document.createElement(‘li’));
}
ul.appendChild(fragment);
}
let iLi = 0;
for (let column = 0; column < imageData.width; column++) {
for (let row = 0; row < imageData.height; row++) {
// 第几个像素点起始位置,肯定是 4 的倍数
const idx = ((row * imageData.width) + column) * 4;
if (imageData.data[idx + 3] > 0) {
const li = ul.children[iLi++];
li.style.opacity = ‘1’;
// 观察 css 你会发现,所有显示的点初始位置都是在中心
li.style.transform = `translate(
${column * rectWidth}px,
${row * rectWidth}px)
scale(1.5)`;
// 这里 scale 完全是为了好看
li.style.background =
`rgba(${imageData.data[idx]},${imageData.data[idx + 1]},${imageData.data[idx + 2]},${imageData.data[idx + 3] / 255})`;
}
}
}
while (iLi < 2000) {
const li = ul.children[iLi++];
li.style.opacity = ‘0’;
}

定时器比较简单,就不写了,具体可以看源码。
注意的点,Chrome 下有点卡顿,Safari 和 Firefox 下没有卡顿,原因未知。
预览效果 - 本地 Chrome 下打开很卡,火狐、safari 正常
圣诞树
早先的时候是圣诞节的时候,看到各种用字符组成圣诞树的形式,于是自己就去试了下,还是比较简单的。

这段用的是项目里的 js 代码,不过一看就是不可执行的,因为我是按照空格分割的。
需要注意的点是:

因为是处理文件,所以我们需要借助 node
怎样处理图片,生成相应的代码
如何让切割后的代码仍然可以执行

对于上面的几点,做以下分析:
关于第一点和第二点,和上面的例子一样,我们还是需要 canvas,node 环境并没有 canvas 这个 element,需要借助第三方的库 node-canvas(npm)例子:
绘制好图片,我们就能像上面一样拿到需要的 ImageData,然后就是写文件,基本上是非常简单了,写的时候考虑到 canvas 的 API 比较多,用了 typescript,不影响阅读,都 9102 年了,你可以不用,你也应该全局装以下 typescript(毕竟如今 typescript 已经成了社交语言,“哎呦,你也在用 typescript 的啊,我也在用呢~”)
先写个简单版本,用 text 格式,展示基本图形
const fs = require(“fs”);
const path = require(‘path’);
const {createCanvas, loadImage} = require(‘canvas’);

const canvas = createCanvas(80, 80)
const ctx: CanvasRenderingContext2D = canvas.getContext(‘2d’)

async function transform(input: string, output: string) {
const image: ImageBitmap = await loadImage(input);

ctx.drawImage(image, 0, 0, 80, 80);

const imageData: ImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const {width, height, data} = imageData;
let outStr = ”;

for (let col = 0; col < height; col++) {
for (let row = 0; row < width; row++) {
const index = ((col * height) + row) * 4;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
const a = data[index + 3];

//“黑色”区间,找的图片不是完全黑色
if (r < 100 && g < 100 && b < 100 && a === 255) {
outStr += ‘+’;
} else {
outStr += ‘ ‘;
}
}

outStr += ‘\n’;
}

console.log(outStr);
fs.writeFileSync(output, outStr);
}

transform(path.join(__dirname, ‘../img/tree.jpg’), path.join(__dirname, ‘../outputs/demo2.txt’));
效果:

关于把 js 代码切割成可执行的样子,这块我想了很久,刚开始只是是想把 js 文件按空格切割成数组,给定一个初始的变量 start,记录到什么位置,因为一些变量名是不能分割,但 js 一些语法特性不好处理,比如说
function test() {
return
function aa() {}
}

function test() {
return function aa() {}
}
完全是两个函数,后面在网上看了下,发现了芋头大大很久以前写过一篇类似的,地址,有兴趣的小伙伴可以看看,这块不做过多说明,实现还是有点麻烦的
会动的字符
上面说了字符和图片,自然而然的,下面说的应该就是视频了。视频的话,也是非常简单的,因为视频是由连续的图片组成的,也就是不断变化的图片,就是所谓的“帧”。也就是,如果我们能拿到视频所有定格的图片,就能作出相应的动画效果。
需要把视频“拆成”图片,需要借助第三方的工具,ffmpeg,功能比较强大,具体不做说明,需要安装到全局,利用 brew,运行 brew install ffmpeg 就好了(大概,我好像是这样装的 233),windows 用户下载要配置环境变量之类的,自己查一下吧。
// 主要代码
const mvPath = path.join(__dirname, ‘../mv/bad-apple.flv’);
const imgPath = path.join(__dirname, ‘../img’);

const setTime = (t: number) => new Promise((resolve) => {
setTimeout(() => resolve(), t);
});

try {
void async function main() {
let img = fs.readdirSync(imgPath);
let len = img.length;
if (len <= 1) {
await execSync(`cd ${imgPath} && ffmpeg -i ${mvPath} -f image2 -vf fps=fps=30 bad-%d.png`);
img = fs.readdirSync(imgPath);
len = img.length;
}
let start = 1;
let count = len;

(async function inter(i: number) {
if (i < count) {
await transform(path.join(__dirname, `../img/bad-${i}.png`));
await setTime(33.33);
await inter(++i);
}
})(start);
}()
} catch (err) {
console.log(err);
}
工具的配置非常多,文档看起来也是很麻烦,有个 npm 包,node-fluent-ffmpeg,用着也还可以,我刚开始用了,但是感觉功能不能满足,而且使用这个包的前提是你全局安装了 ffmpeg…
总结
GitHub 源码
这个我拖了比较久,有的东西有点记不清楚,可能有些东西表达的不好,说的不是很细,一些 api 的说明我都省略了,这些 MDN 上都有,就没做过多说明,当然, 你可以做些更有趣的事情, 文档,本来自己还想做些有趣的东西,但后面没啥时间,就没继续做下去了,希望有兴趣的朋友可以去尝试一波,还是很有意思的。
就酱,感谢阅读~

退出移动版