关于节日
圣诞节, 元旦, 看大家 (情侣) 在朋友圈里发各种庆祝的或者祝福的话语, 甚是感动, 然后悄悄拉黑了. 作为单身狗, 我们也有自己庆祝节日的方式, 今天我们就来实现一些祝福的效果.
需要说明的是, 所有的效果都是利用 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 下没有卡顿, 原因未知.
预览效果 https://shiyangzhaoa.github.io/canvas2xx/ - 本地 Chrome 下打开很卡, 火狐, Safari 正常
圣诞树
早先的时候是圣诞节的时候, 看到各种用字符组成圣诞树的形式, 于是自己就去试了下, 还是比较简单的.
这段用的是项目里的 JS 代码, 不过一看就是不可执行的, 因为我是按照空格分割的.
需要注意的点是:
因为是处理文件, 所以我们需要借助 node
怎样处理图片, 生成相应的代码
如何让切割后的代码仍然可以执行
对于上面的几点, 做以下分析:
关于第一点和第二点, 和上面的例子一样, 我们还是需要 canvas,node 环境并没有 canvas 这个 element, 需要借助第三方的库 node-canvas( https://www.npmjs.com/package/canvas ) 例子:
绘制好图片, 我们就能像上面一样拿到需要的 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() {}
- }
完全是两个函数, 后面在网上看了下, 发现了芋头大大很久以前写过一篇类似的, 地址 https://zhuanlan.zhihu.com/p/24506119 , 有兴趣的小伙伴可以看看, 这块不做过多说明, 实现还是有点麻烦的
会动的字符
上面说了字符和图片, 自然而然的, 下面说的应该就是视频了. 视频的话, 也是非常简单的, 因为视频是由连续的图片组成的, 也就是不断变化的图片, 就是所谓的 "帧". 也就是, 如果我们能拿到视频所有定格的图片, 就能作出相应的动画效果.
需要把视频 "拆成" 图片, 需要借助第三方的工具, https://ffmpeg.org/ffmpeg.html , 功能比较强大, 具体不做说明, 需要安装到全局, 利用 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 包,, 用着也还可以, 我刚开始用了, 但是感觉功能不能满足, 而且使用这个包的前提是你全局安装了 FFMPEG...
总结
GitHub 源码 https://github.com/shiyangzhaoa/canvas2xx
这个我拖了比较久, 有的东西有点记不清楚, 可能有些东西表达的不好, 说的不是很细, 一些 API 的说明我都省略了, 这些 MDN 上都有, 就没做过多说明, 文档, 本来自己还想做些有趣的东西, 但后面没啥时间, 就没继续做下去了, 希望有兴趣的朋友可以去尝试一波, 还是很有意思的.
来源: https://juejin.im/post/5c2b766051882575f560553b