JS前端轻量fabric.js系列之画布初始化


Posted in Javascript onAugust 05, 2022

前言

从这个章节开始我们就步入正题了,那一开始要做啥子呢,回忆下上个章节中 fabric.js 的使用过程,先是创建画布,再添加物体,然后开始动画和交互。显然画布是一切物体的开端?,所以首先要搞定的就是它,也就是 const canvas = new fabric.Canvas('canvas') 这一步要做的事情。

画布的前置知识

在说 fabric.js 如何初始化画布之前,先巩固下画布的相关知识点。创建画布要做的事情通常比较简单,就是单纯的获取画布(或动态创建画布)并重新设置画布宽高,就像下面这个样子:

const canvas = document.getElementById('canvas') || document.createElement('canvas');
const width = canvas.width;
const height = canvas.height;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';

为什么要重新设置宽高,这是个很容易混淆的点。看看下面的代码??:

#canvas { width: 200px; height: 100px; }
<canvas id="canvas" width="100" height="100"></canvas>

可以看到上面的 canvas 有两个宽高大小,一个是 canvas 上的属性值,一个是 css 的样式值,那应该以哪个为准呢??

我们可以先抛弃 css 大小的概念,请记住:所有的绘图操作都是在 canvas 这个画布大小上进行的,就上面的代码来说不论你绘制什么东西,都是在 100*100 的画布中进行的,当你在 canvas 绘制完所有东西之后要在页面上某个区域渲染了,才和 css 大小有关,就上面的例子来说就是你要把 100*100 的 canvas 画布放到页面上 200*100 的区域,但是它们大小不一致要怎么处理呢?

你可以把 canvas 绘制的内容想象成一张大小固定的照片,把 css 大小想象成一个容器,不管 css 尺寸如何,这张照片都会铺满整个容器(机制就是这样,没有为啥?)。所以如果长宽比例相同就会等比缩放;

如果长宽比例不同就会拉伸变形;如果大小一样就刚刚好。就我们的例子来说 100*100 的绘制内容水平方向会被拉伸成 200*100,就产生了变形,因此通常情况下需要把 canvas 和 css 设置成一样大,确保不拉伸变形,看下面的示意图能帮你加深理解:

JS前端轻量fabric.js系列之画布初始化

另外还有一个常见问题就是设备像素比(devicePixelRatio)的影响,如果不处理在高清屏上就会导致模糊(比如 Mac 电脑),大家应该有看过类似问题的文章,但大多都是各种名词词汇,看完就忘的那种。

关于这个问题我在另一篇文章?关于 canvas 模糊的问题(高清图解)有解释过,有需要的可以去看下,这里就简单介绍下(温馨提示:实在不好记可以跳过这一趴?,因为它并不妨碍我们进行接下来的开发)。我们知道画的东西最终是要展现在屏幕上的,而屏幕又是由很多小格子构成的,通常情况下:

  • 如果 dpr = 1,就说明 1px 对应屏幕上的 1 个小格子(亦即 1 个 css 像素对应 1 个物理像素)

如果 dpr = 2,就说明 1px 对应屏幕上的 2 个小格子(亦即 1 个 css 像素对应 1 个物理像素) 顺便看下图解??:

JS前端轻量fabric.js系列之画布初始化

图没看懂??那就来看看文字解说:假设我们现在 canvas 和 css 的大小都是 10 * 10,那么 canvas 画完的照片中就会有 100 个(像素)点,也就是只有 100 个点的信息;但是到了高清屏中(如 dpr = 2),我们需要 400 个点的信息,原来的点不够用怎么办?

于是就会有一套算法来自动生成这些点的信息,从而造成了模糊。那应该怎么办呢??我们需要更多的点,所以可以这样子搞,把画布放大 dpr 倍,也就是把 canvas 的宽高都乘以 dpr(css 的大小还是不变的),接下来的绘制都是在宽为width*dpr、高为 height*dpr的画布大小上进行的,这样一折腾,点就变多了。

但是要注意什么呢,画布变大了,相应的绘制操作(画圆、画矩形等)也需要相应放大,这个我会在最后一章加上这个功能,一开始有个印象就行,不然容易犯晕?。

画布初始化

在 fabric.js 中我们总共会创建两个画布,一个是上层画布(upper-canvas),一个是下层画布(lower-canvas),两个画布是一样大的,还有一个外层 div 将这两个 canvas 包起来。

  • 上层画布主要用于处理一些交互事件,比如鼠标事件、涂鸦模式(画板)、左键拖拽产生的框选区域等;
  • 下层画布则单纯的用于绘制所有物体,简单粗暴的遍历所有物体进行绘制,没有其他多余的操作。

如果通过上层画布的交互后,某些物体的某些属性值被改变了,这时候就会清空下层画布,重新绘制所有物体,两层画布各司其职,典型的数据驱动视图。

除了职责分明还有一点点单向数据流的味道,上层的交互改变了数据,数据的改变传到下层画布,下层画布就单纯的重新绘制;

但是反过来,下层画布并不会影响上层画布也不会影响数据,这样问题排查起来也方便些。相信大家都用过 vue2,如果我们要修改 props 中的值,就需要用 $emit 把数据传出去,修改父元素的值才行;

但如果 props 是个对象,我们其实可以在子元素中直接修改 props 的属性值,虽然方便但不是很好的写法,关系就乱了,如果你有踩过这个坑的话。

扯远了,回过头来,实际上 fabric.js 一共创建了三层画布,还有一个是 cacheCanvasEl,我们就把它叫做缓冲层画布吧,它和另外两个画布一样大,但并没有在页面中显示,所以也可以叫离屏 canvas,它主要用来提供一个临时绘制环境,以便不时之需,后面章节会说道它的用途,这里先知道有这么个东西就行。

JS前端轻量fabric.js系列之画布初始化

顺便给些示例代码,简单瞟一瞟就行:

/** 画布类 */
class Canvas {
    /** 画布宽度 */
    public width: number;
    /** 画布高度 */
    public height: number;
    /** 包围 canvas 的外层 div 容器 */
    public wrapperEl: HTMLElement;
    /** 下层 canvas 画布,主要用于绘制所有物体 */
    public lowerCanvasEl: HTMLCanvasElement;
    /** 上层 canvas,主要用于监听鼠标事件、涂鸦模式、左键点击拖蓝框选区域 */
    public upperCanvasEl: HTMLCanvasElement;
    /** 缓冲层画布 */
    public cacheCanvasEl: HTMLCanvasElement;
    /** 上层画布环境 */
    public contextTop: CanvasRenderingContext2D;
    /** 下层画布环境 */
    public contextContainer: CanvasRenderingContext2D;
    /** 缓冲层画布环境 */
    public contextCache: CanvasRenderingContext2D;
    /** 整个画布到上面和左边的偏移量 */
    private _offset: Offset;
    /** 画布中所有添加的物体 */
    private _objects: FabricObject[];
    constructor(el: HTMLCanvasElement, options) {
        // 初始化下层画布 lower-canvas
        this._initStatic(el, options);
        // 初始化上层画布 upper-canvas
        this._initInteractive();
        // 初始化缓冲层画布
        this._createCacheCanvas();
    }
    // 下层画布初始化:参数赋值、重置宽高,并赋予样式
    _initStatic(el: HTMLCanvasElement, options) {
        this.lowerCanvasEl = el;
        Util.addClass(this.lowerCanvasEl, 'lower-canvas');
        this._applyCanvasStyle(this.lowerCanvasEl);
        this.contextContainer = this.lowerCanvasEl.getContext('2d');
        for (let prop in options) {
            this[prop] = options[prop];
        }
        this.width = +this.lowerCanvasEl.width;
        this.height = +this.lowerCanvasEl.height;
        this.lowerCanvasEl.style.width = this.width + 'px';
        this.lowerCanvasEl.style.height = this.height + 'px';
    }
    // 其余两个画布同理
}

上面的代码简单用到了 Util 这个工具类,里面主要就是封装一些独立的、常用的方法,大部分都比较简单,下面简单的列举几种:

const PiBy180 = Math.PI / 180; // 写在这里相当于缓存,因为会频繁调用
class Util {
     /** 单纯的创建一个新的 canvas 元素 */
    static createCanvasElement() {
        const canvas = document.createElement('canvas');
        return canvas;
    }
    /** 角度转弧度,注意 canvas 中用的都是弧度,但是角度对我们来说比较直观 */
    static degreesToRadians(degrees: number): number {
        return degrees * PiBy180;
    }
    /** 弧度转角度,注意 canvas 中用的都是弧度,但是角度对我们来说比较直观 */
    static radiansToDegrees(radians: number): number {
        return radians / PiBy180;
    }
    /** 从数组中溢出某个元素 */
    static removeFromArray(array: any[], value: any) {
        let idx = array.indexOf(value);
        if (idx !== -1) {
            array.splice(idx, 1);
        }
        return array;
    }
    static clone(obj) {
        if (!obj || typeof obj !== 'object') return obj;
        let temp = new obj.constructor();
        for (let key in obj) {
            if (!obj[key] || typeof obj[key] !== 'object') {
                temp[key] = obj[key];
            } else {
                temp[key] = Util.clone(obj[key]);
            }
        }
        return temp;
    }
    static loadImage(url, options: any = {}) {
        return new Promise(function (resolve, reject) {
            let img = document.createElement('img');
            let done = () => {
                img.onload = img.onerror = null;
                resolve(img);
            };
            if (url) {
                img.onload = done;
                img.onerror = () => {
                    reject(new Error('Error loading ' + img.src));
                };
                options && options.crossOrigin && (img.crossOrigin = options.crossOrigin);
                img.src = url;
            } else {
                done();
            }
        });
    }
}

诸如此类,大家可以自己去看下 Util 这个工具类,后面就不再赘述了,当然有些比较麻烦点的方法(比如 animate 和一些计算)可以先跳过,后面的用到的时候会再展开。

变换练习

同样的这个章节内容不多也不难,所以这里先为下一篇文章(物体基类)做一些热身练习,讲一些变换的基础内容,也就是 transform(translate、rotate、scale),功能和 css 的 transform 类似。

以绘制一个红色矩形为例 ctx.fillRect(0, 0, 50, 50),让我们看看这几个东西分别会产生什么影响:

translate 的影响

JS前端轻量fabric.js系列之画布初始化

rotate 的影响

JS前端轻量fabric.js系列之画布初始化

scale 的影响

JS前端轻量fabric.js系列之画布初始化

这里对 scale 做一些补充,scale 的结果是对坐标系做了缩放,但是理解起来不是很直观,所以你可以认为 scale 其实是对坐标轴的刻度做了缩放,比如本来画布的一段固定长度代表 50,scale(2, 2) 之后,同样的固定长度就只能代表 25,所以还需要再来一个固定长度才能表示 50,视觉上就是放大的效果。

好了,以上这几种变换的结果本质都是对坐标系的变换,translate 改变了坐标系原点的位置,rotate 将坐标系进行了旋转,scale 则将坐标轴的刻度进行了缩放,而画布的视窗大小(也就是上面图中的 canvas 框)是不变的(可以想象成一个镜头),我们并不会改动到画布的宽高,不要混淆了。

单个内容的变换还是比较好理解的,但是混在一起就会有点变扭了,比如要画下面这样一个图形(两个箭头和等边三角形):

JS前端轻量fabric.js系列之画布初始化

大家可以用这三种变换画一下上面的图形,能画出来应该就有点感觉了(这些变换效果是会累加的哦)。建议多动手练练,因为下个章节会用上。

小结

这里是本章的知识点小结,记住这些就可以了:

  • 我们共创建了三个 canvas,每个 canvas 都是一样大的,但功能各不相同
  • 逻辑和绘制是分离的,上层画布用来改逻辑和改数据,下层画布则用来绘制
  • 原点始终都是在画布左上角,x 轴水平向右为正,y 轴竖直向下为正? 然后这里还是先给个简版 fabric.js 的代码链接吧,有需要的可以参考看看,会随着文章更新不断完善。好啦,今天的分享就到这里,有什么问题欢迎点赞评论留言,我们下期再见,拜拜 ? ?

实现一个轻量 fabric.js 系列一(概览)? 

以上就是JS前端轻量fabric.js系列之画布初始化的详细内容,更多关于fabric.js画布初始化的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
解析img图片没找到onerror事件 Stack overflow at line: 0
Dec 23 Javascript
js和jquery设置disabled属性为true使按钮失效
Aug 07 Javascript
JS实现固定在右下角可展开收缩DIV层的方法
Feb 13 Javascript
JS+CSS实现滑动切换tab菜单效果
Aug 25 Javascript
js实现tab切换效果实例
Sep 16 Javascript
javascript实现省市区三级联动下拉框菜单
Nov 17 Javascript
1秒50万字!js实现关键词匹配
Aug 01 Javascript
JS实现Ajax的方法分析
Dec 20 Javascript
解决Vue 浏览器后退无法触发beforeRouteLeave的问题
Dec 24 Javascript
浅谈在Vue.js中如何实现时间转换指令
Jan 06 Javascript
jquery实现手风琴案例
May 04 jQuery
微信小程序 接入腾讯地图的两种写法
Jan 12 Javascript
vue3 自定义图片放大器效果的示例代码
Jul 23 #Vue.js
JavaScript parseInt0.0000005打印5原理解析
Jul 23 #Javascript
JavaScript实现一键复制内容剪贴板
Jul 23 #Javascript
从原生JavaScript到React深入理解
Jul 23 #Javascript
JS前端监控采集用户行为的N种姿势
Jul 23 #Javascript
JS前端可扩展的低代码UI框架Sunmao使用详解
Jul 23 #Javascript
uniapp引入支付宝原生扫码插件步骤详解
Jul 23 #Javascript
You might like
PHP 验证码的实现代码
2011/07/17 PHP
WordPress中用于获取文章作者与分类信息的方法整理
2015/12/17 PHP
jquery 简短几句代码实现给元素动态添加及获取提示信息
2011/09/01 Javascript
分享XmlHttpRequest调用Webservice的一点心得
2012/07/20 Javascript
jquery文字上下滚动的实现方法
2013/03/22 Javascript
js设置cookie过期及清除浏览器对应名称的cookie
2013/10/24 Javascript
jQuery中的ajax async同步和异步详解
2015/09/29 Javascript
javascript实现tab响应式切换特效
2016/01/29 Javascript
Node.js Express 框架 POST方法详解
2017/01/23 Javascript
vue+swiper实现侧滑菜单效果
2017/12/28 Javascript
JavaScript面试技巧之数组的一些不low操作
2019/03/22 Javascript
vue滚动插件better-scroll使用详解
2019/10/18 Javascript
Python logging模块学习笔记
2014/05/24 Python
Python with的用法
2014/08/22 Python
python图片验证码生成代码
2016/07/02 Python
python利用微信公众号实现报警功能
2018/06/10 Python
对Python3 解析html的几种操作方式小结
2019/02/16 Python
Python multiprocess pool模块报错pickling error问题解决方法分析
2019/03/20 Python
如何通过雪花算法用Python实现一个简单的发号器
2019/07/03 Python
Python把图片转化为pdf代码实例
2020/07/28 Python
python 模拟登陆github的示例
2020/12/04 Python
CSS3 media queries + jQuery实现响应式导航
2016/09/30 HTML / CSS
什么是虚拟内存?虚拟内存有什么优势?
2012/02/19 面试题
代码中finally中的代码会不会执行
2012/02/06 面试题
宿舍卫生检讨书
2014/01/16 职场文书
乡镇干部十八大感言
2014/02/17 职场文书
初中军训感想300字
2014/03/05 职场文书
竞选生活委员演讲稿
2014/04/28 职场文书
五四青年节演讲稿
2014/05/26 职场文书
酒店节能减排方案
2014/05/26 职场文书
大学生学习计划书
2014/09/15 职场文书
党员转正意见怎么写
2015/06/03 职场文书
悬崖上的金鱼姬观后感
2015/06/15 职场文书
2016党性教育学习心得体会
2016/01/21 职场文书
《游戏公平》教学反思
2016/02/20 职场文书
python3实现无权最短路径的方法
2021/05/12 Python