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 相关文章推荐
js弹出层(jQuery插件形式附带reLoad功能)
Apr 12 Javascript
JS获取URL中的参数数据
Dec 05 Javascript
javascript实现动态侧边栏代码
Feb 19 Javascript
JavaScript怎么判断图片是否加载完成以便获取其尺寸
May 08 Javascript
JavaScript将字符串转换成字符编码列表的方法
Mar 19 Javascript
JavaScript数组的栈方法与队列方法详解
May 26 Javascript
indexedDB bootstrap angularjs之 MVC DOMO (应用示例)
Jun 20 Javascript
AngularJS删除路由中的#符号的方法
Sep 20 Javascript
ES6新特性七:数组的扩充详解
Apr 21 Javascript
原生js实现移动端触摸轮播的示例代码
Dec 22 Javascript
详解Vue项目编译后部署在非网站根目录的解决方案
Apr 26 Javascript
详解从Vue-router到html5的pushState
Jul 21 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上的memcache和memcached两个pecl库
2010/03/29 PHP
调整优化您的LAMP应用程序的5种简单方法
2011/06/26 PHP
PHP实现伪静态方法汇总
2016/01/13 PHP
JavaScript 实现模态对话框 源代码大全
2009/05/02 Javascript
jquery中获取元素的几种方式小结
2011/07/05 Javascript
浅谈jquery中delegate()与live()
2015/06/22 Javascript
jquery分割字符串的方法
2015/06/24 Javascript
JavaScript实现窗口抖动效果
2016/10/19 Javascript
bootstrap栅格系统示例代码分享
2017/05/22 Javascript
AngularJS中控制器函数的定义与使用方法示例
2017/10/10 Javascript
JsChart组件使用详解
2018/03/04 Javascript
vue新vue-cli3环境配置和模拟json数据的实例
2018/09/19 Javascript
React项目动态设置title标题的方法示例
2018/09/26 Javascript
Vue2.x通用编辑组件的封装及应用详解
2019/05/28 Javascript
weui中的picker使用js进行动态绑定数据问题
2019/11/06 Javascript
[09:31]2016国际邀请赛中国区预选赛Yao赛后采访 答题送礼
2016/06/27 DOTA
[02:49]DOTA2完美大师赛首日观众采访
2017/11/23 DOTA
分析Python中设计模式之Decorator装饰器模式的要点
2016/03/02 Python
浅谈Python NLP入门教程
2017/12/25 Python
对Python3中的input函数详解
2018/04/22 Python
Python unittest 简单实现参数化的方法
2018/11/30 Python
Python实现KNN(K-近邻)算法的示例代码
2019/03/05 Python
如何使用Python调整图像大小
2020/09/26 Python
纯CSS实现设置半个字符的样式
2014/07/03 HTML / CSS
int *p=NULL和*p= NULL有什么区别
2014/10/23 面试题
DataList 能否分页,请问如何实现?
2015/05/03 面试题
婚纱摄影师求职信范文
2014/04/17 职场文书
《夹竹桃》教学反思
2014/04/20 职场文书
感情真挚的毕业生求职信
2014/07/19 职场文书
2014党员民主评议个人思想剖析发言
2014/09/19 职场文书
学生实习证明模板汇总
2014/09/25 职场文书
妈妈再爱我一次观后感
2015/06/08 职场文书
2016年12月份红领巾广播稿
2015/12/21 职场文书
研究生学习计划书应该怎么写?
2019/09/10 职场文书
pytorch DataLoader的num_workers参数与设置大小详解
2021/05/28 Python
vue自定义右键菜单之全局实现
2022/04/09 Vue.js