写点什么

假如只剩下 canvas 标签

用户头像
执鸢者
关注
发布于: 2021 年 05 月 09 日
假如只剩下canvas标签

笔者正在进行“前端百题斩”的系列文章撰写,让每位前端工程师掌握高频知识点,为工作助力。关注公众号“执鸢者”,用知识武装自己的头脑。

一、背景

如果只剩下 canvas 标签,该如何去绘制页面中的内容呢?这也许是一个伪命题,但是用 canvas 确事能够帮助完成很多事。今天就用 canvas+AST 语法树构建一个信息流样式。

二、绘制流程

将整个绘制流程分为三部分:基本元素、AST 语法树、主函数类。基本元素指的是图片、文字、矩形、圆等;AST 语法树在本处值得就是包含一些属性的 js 对象;主函数类指对外暴露的接口,通过调用实现最终绘制。

2.1 基本元素

不管多么复杂的事物肯定都是由一系列简单的元素组成,例如汽车肯定是通过一些简单的机械零配件组成;电脑也是通过电阻、电容等零配件组成。网页也不例外,也是通过文字、图片、矩形等组成。

2.1.1 加载图片

图片是一个页面中的灵魂元素,在页面中占据绝大部分空间。


class DrawImage {    constructor(ctx, imageObj) {        this.ctx = ctx;        this.imageObj = imageObj;    }
draw() { const {centerX, centerY, src, sx = 1, sy = 1} = this.imageObj; const img = new Image(); img.onload = () => { const imgWidth = img.width; const imgHeight = img.height; this.ctx.save(); this.ctx.scale(sx, sy); this.ctx.drawImage(img, centerX - imgWidth * sx / 2, centerY - imgHeight * sy / 2); this.ctx.restore(); }; img.src = src; }}
复制代码

2.1.2 绘制文字

文字能够提高页面的可读性,让观察该页面的每一个人都能够快速了解该页面的思想。


class DrawText {    constructor(ctx, textObj) {        this.ctx = ctx;        this.textObj = textObj;    }
draw() { const {x, y, font, content, lineHeight = 20, width, fillStyle = '#000000', textAlign = 'start', textBaseline = 'middle'} = this.textObj; const branchsContent = this.getBranchsContent(content, width); this.ctx.save(); this.ctx.fillStyle = fillStyle; this.ctx.textAlign = textAlign; this.ctx.textBaseline = textBaseline; this.ctx.font = font; branchsContent.forEach((branchContent, index) => { this.ctx.fillText(branchContent, x, y + index * lineHeight); }); this.ctx.restore(); }
getBranchsContent(content, width) { if (!width) { return [content]; } const charArr = content.split(''); const branchsContent = []; let tempContent = ''; charArr.forEach(char => { if (this.ctx.measureText(tempContent).width < width && this.ctx.measureText(tempContent + char).width <= width) { tempContent += char; } else { branchsContent.push(tempContent); tempContent = ''; } }); branchsContent.push(tempContent); return branchsContent; }}
复制代码

2.1.3 绘制矩形

通过矩形元素能够与文字等元素配合达到意想不到的效果。


class DrawRect {    constructor(ctx, rectObj) {        this.ctx = ctx;        this.rectObj = rectObj;    }
draw() { const {x, y, width, height, fillStyle, lineWidth = 1} = this.rectObj; this.ctx.save(); this.ctx.fillStyle = fillStyle; this.ctx.lineWidth = lineWidth; this.ctx.fillRect(x, y, width, height); this.ctx.restore(); }}
复制代码

2.1.4 绘制圆

圆与矩形承担的角色一致,也是在页面中比较重要的角色。


class DrawCircle {    constructor(ctx, circleObj) {        this.ctx = ctx;        this.circleObj = circleObj;    }
draw() { const {x, y, R, startAngle = 0, endAngle = Math.PI * 2, lineWidth = 1, fillStyle} = this.circleObj; this.ctx.save(); this.ctx.lineWidth = lineWidth; this.ctx.fillStyle = fillStyle; this.ctx.beginPath(); this.ctx.arc(x, y, R, startAngle, endAngle); this.ctx.closePath(); this.ctx.fill(); this.ctx.restore(); }}
复制代码

2.2 AST 树

AST 抽象语法树是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。例如,在 Vue 中,将模板语法转换为 AST 抽象语法树,然后再将抽象语法树转换为 HTML 结构,咱们在利用 canvas 绘制页面时也利用 AST 抽象语法树来表示页面中的内容,实现的类型有 rect(矩形)、img(图片)、text(文字)、circle(圆)。


本次将绘制的内容包含静态页面部分和动画部分,所以将利用两个 canvas 实现,每个 canvas 将对应一个 AST 树,分别为静态部分 AST 树和动态部分 AST 树。

2.2.1 静态部分 AST 树

本次绘制的页面中静态部分的 AST 树如下所示,包含矩形、图片、文字。


const graphicAst = [    {        type: 'rect',        x: 0,        y: 0,        width: 1400,        height: 400,        fillStyle: '#cec9ae'    },    {        type: 'img',        centerX: 290,        centerY: 200,        sx: 0.9,        sy: 0.9,        src: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Finews.gtimg.com%2Fnewsapp_match%2F0%2F11858683821%2F0.jpg&refer=http%3A%2F%2Finews.gtimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1622015341&t=cc1bd95777dfa37d88c48bb6e179778e'    },    {        type: 'text',        x: 600,        y: 60,        textAlign: 'start',        textBaseline: 'middle',        font: 'normal 40px serif',        lineHeight: 50,        width: 180,        fillStyle: '#000000',        content: '灰太狼是最好的一头狼,它每天都在梦想着吃羊,一直没有实现,但是从不气馁。'    },    {        type: 'text',        x: 600,        y: 170,        textAlign: 'start',        textBaseline: 'middle',        font: 'normal 30px serif',        lineHeight: 50,        width: 180,        fillStyle: '#7F7F7F',        content: '为灰太狼加油、为灰太狼喝彩,😄'    },    {        type: 'text',        x: 1200,        y: 360,        textAlign: 'start',        textBaseline: 'ideographic',        font: 'normal 30px serif',        lineHeight: 50,        width: 180,        fillStyle: '#949494',        content: '阅读'    },    {        type: 'text',        x: 1260,        y: 363,        textAlign: 'start',        textBaseline: 'ideographic',        font: 'normal 30px serif',        lineHeight: 50,        width: 180,        fillStyle: '#949494',        content: '520'    }];
复制代码

2.2.2 动态部分 AST 树

本次绘制的页面中动画部分的 AST 树动态生成,由一系列动态颜色的圆组成。


function getMarqueeAst(startX, endX, count, options = {}) {    const {y = 15, R = 15} = options;    if (!(endX >= startX && count > 0)) {        return [];    }    const interval = (endX - startX) / count;    const marqueeAstArr = [];    for (let i = 0; i < count; i++) {        const RValue = Math.random() * 255;        const GValue = Math.random() * 255;        const BValue = Math.random() * 255;        const fillStyle = `rgb(${RValue}, ${GValue}, ${BValue})`;        marqueeAstArr.push({            type: 'circle',            x: startX + i * interval,            y,            R,            fillStyle        });    }
return marqueeAstArr;}
复制代码

2.3 主函数类

除了上述一些基本元素类,将通过一个主函数类对外进行暴露。


class Draw {    constructor(canvasDom) {        this._canvasDom = canvasDom;        this.ctx = this._canvasDom.getContext('2d');        this.width = this._canvasDom.width;        this.height = this._canvasDom.height;    }
// 绘制函数 draw(ast) { ast.forEach(elementObj => { this.drawFactory(elementObj); const {children} = elementObj; // 递归调用 if (children && Array.isArray(children)) { this.draw(children); } }); }
// 工厂模型绘制对应基本元素 drawFactory(elementObj) { const {type} = elementObj; switch(type) { case 'img': { this.drawImage(elementObj); break; } case 'text': { this.drawText(elementObj); break; } case 'rect': { this.drawRect(elementObj); break; } case 'circle': { this.drawCircle(elementObj); break; } } }
drawImage(imageObj) { const drawImage = new DrawImage(this.ctx, imageObj); drawImage.draw(); }
drawText(textObj) { const drawText = new DrawText(this.ctx, textObj); drawText.draw(); }
drawRect(rectObj) { const drawRect = new DrawRect(this.ctx, rectObj); drawRect.draw(); }
drawCircle(circleObj) { const drawCircle = new DrawCircle(this.ctx, circleObj); drawCircle.draw(); }
clearCanvas() { this.ctx.clearRect(0, 0, this.width, this.height); }}
复制代码

2.4 内容绘制

前面的准备工作已经完成,下面将各个函数和 AST 树联动起来,达到想要的效果。

2.4.1 静态内容绘制

先将静态部分的内容绘制好,作为页面的基石。


const basicCanvasDom = document.getElementById('basicCanvas');const drawBasicInstance = new Draw(basicCanvasDom);drawBasicInstance.draw(graphicAst);
复制代码


2.4.2 绘制动画跑马灯

再给该部分内容来点动画效果,更加激动人心。


const animationCanvasDom = document.getElementById('animationCanvas');const drawAnimationInstance = new Draw(animationCanvasDom);
let renderCount = 0;function animate() { if (renderCount % 5 === 0) { drawAnimationInstance.clearCanvas(); drawAnimationInstance.draw(getMarqueeAst(20, 1440, 22)); drawAnimationInstance.draw(getMarqueeAst(20, 1440, 22, { y: 380 })); } window.requestAnimationFrame(animate); renderCount++;}animate();
复制代码



  1. 本文对应源码,关注公众号“执鸢者”,回复“canvas”获取

  2. 如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

发布于: 2021 年 05 月 09 日阅读数: 18
用户头像

执鸢者

关注

让前端知识变的简单可依赖。 2019.09.05 加入

以脑图分享前端知识,让知识变的简单可依赖。

评论

发布
暂无评论
假如只剩下canvas标签