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 相关文章推荐
MultiSelect左右选择控件的设计与实现介绍
Jun 08 Javascript
JavaScript中的setUTCDate()方法使用详解
Jun 11 Javascript
理解jquery事件冒泡
Jan 03 Javascript
老司机带你解读jQuery插件开发流程
May 16 Javascript
简单实现js倒计时功能
Feb 13 Javascript
Vue.directive自定义指令的使用详解
Mar 10 Javascript
微信小程序之选项卡的实现方法
Sep 29 Javascript
vue 系列——vue2-webpack2框架搭建踩坑之路
Dec 22 Javascript
js调用设备摄像头的方法
Jul 19 Javascript
微信小程序入口场景的问题集合与相关解决方法
Jun 26 Javascript
小程序自定义导航栏兼容适配所有机型(附完整案例)
Apr 26 Javascript
关于Node.js中频繁修改代码重启服务器的问题
Oct 15 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数字每三位加逗号的功能函数
2015/10/22 PHP
php微信开发之关注事件
2018/06/14 PHP
php并发加锁问题分析与设计代码实例讲解
2021/02/26 PHP
刷新时清空文本框内容的js代码
2007/04/23 Javascript
几个javascript操作word的参考代码
2009/10/26 Javascript
javascript中的toFixed固定小数位数 简单实例分享
2013/07/12 Javascript
Javascript中call与apply的学习笔记
2014/09/22 Javascript
一个JavaScript去除字符串末尾的空白实例代码
2014/09/22 Javascript
JQuery菜单效果的两个实例讲解(3)
2015/09/17 Javascript
每天一篇javascript学习小结(String对象)
2015/11/18 Javascript
JavaScript的String字符串对象常用操作总结
2016/05/26 Javascript
Bootstrap table分页问题汇总
2016/05/30 Javascript
AngularJS基础 ng-hide 指令用法及示例代码
2016/08/01 Javascript
JavaScript SHA1加密算法实现详细代码
2016/10/06 Javascript
Bootstrap 下拉多选框插件Bootstrap Multiselect
2017/01/22 Javascript
浅谈js使用in和hasOwnProperty获取对象属性的区别
2017/04/27 Javascript
微信小程序实现图片懒加载的示例代码
2017/12/13 Javascript
详解webpack4升级指南以及从webpack3.x迁移
2018/06/12 Javascript
jQuery实现简单评论功能
2020/08/19 jQuery
[51:34]Ti4主赛事胜者组 DK vs EG 2
2014/07/19 DOTA
python避免死锁方法实例分析
2015/06/04 Python
Python语法快速入门指南
2015/10/12 Python
python从入门到精通(DAY 2)
2015/12/20 Python
Python 爬虫的工具列表大全
2016/01/31 Python
python模拟登录并且保持cookie的方法详解
2017/04/04 Python
Python爬虫番外篇之Cookie和Session详解
2017/12/27 Python
使用pandas中的DataFrame数据绘制柱状图的方法
2018/04/10 Python
关于python下cv.waitKey无响应的原因及解决方法
2019/01/10 Python
Python使用Selenium模拟浏览器自动操作功能
2020/09/08 Python
澳大利亚窗帘商店:Curtain Wonderland
2019/12/01 全球购物
一套.net面试题及答案
2016/11/02 面试题
学生个人自我鉴定
2014/03/26 职场文书
第一批党的群众路线教育实践活动总结报告
2014/07/03 职场文书
医学专业大学生职业生涯规划书
2014/10/25 职场文书
千与千寻观后感
2015/06/04 职场文书
nginx实现多geoserver服务的负载均衡
2022/05/15 Servers