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 相关文章推荐
jQuery 连续列表实现代码
Dec 21 Javascript
鼠标滚轮控制网页横向移动实现思路
Mar 22 Javascript
JavaScript实现的双向跨域插件分享
Jan 31 Javascript
jQuery插件编写步骤详解
Jun 03 Javascript
关于List.ToArray()方法的效率测试
Sep 30 Javascript
jQuery Validate 校验多个相同name的方法
May 18 jQuery
Angular2安装angular-cli
May 21 Javascript
深入理解Vue router的部分高级用法
Aug 15 Javascript
JS实现倒计时图文效果
Nov 17 Javascript
vue 实现路由跳转时更改页面title
Nov 05 Javascript
vue封装自定义指令之动态显示title操作(溢出显示,不溢出不显示)
Nov 12 Javascript
使用typescript快速开发一个cli的实现示例
Dec 09 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基于SQLite实现的分页功能示例
2017/06/21 PHP
thinkPHP框架中执行事务的方法示例
2018/05/31 PHP
模拟用户操作Input元素,不会触发相应事件
2007/05/11 Javascript
jQuery源码分析-05异步队列 Deferred 使用介绍
2011/11/14 Javascript
jQuery滚动加载图片效果的实现
2013/03/06 Javascript
js Dialog 去掉右上角的X关闭功能
2014/04/23 Javascript
深入解析JavaScript中的立即执行函数
2016/05/21 Javascript
AngularJS基础 ng-readonly 指令简单示例
2016/08/02 Javascript
javascript 分号总结及详细介绍
2016/09/24 Javascript
使用bootstrapValidator插件进行动态添加表单元素并校验
2016/09/28 Javascript
JS当前页面登录注册框,固定DIV,底层阴影的实例代码
2016/09/29 Javascript
Java  Spring 事务回滚详解
2016/10/17 Javascript
jQuery层级选择器_动力节点节点Java学院整理
2017/07/04 jQuery
浅谈Vue.js应用的四种AJAX请求数据模式
2017/08/30 Javascript
JS实现打砖块游戏
2020/02/14 Javascript
JavaScript对象字面量和构造函数原理与用法详解
2020/04/18 Javascript
原生JS实现拖拽功能
2020/12/16 Javascript
[04:03]2014DOTA2西雅图国际邀请赛 LGD战队巡礼
2014/07/07 DOTA
[29:59]完美世界DOTA2联赛PWL S3 Forest vs access 第二场 12.11
2020/12/13 DOTA
Win7上搭建Cocos2d-x 3.1.1开发环境
2014/07/03 Python
详解在Python程序中自定义异常的方法
2015/10/16 Python
Python的Socket编程过程中实现UDP端口复用的实例分享
2016/03/19 Python
python机器人运动范围问题的解答
2019/04/29 Python
Python中请不要再用re.compile了
2019/06/30 Python
GNC健安喜官方海外旗舰店:美国著名保健品牌
2017/01/04 全球购物
巴西食品补充剂在线零售商:Músculos na Web
2017/08/07 全球购物
Nordgreen台湾官网:极简北欧设计手表
2019/08/21 全球购物
店长岗位职责
2013/11/21 职场文书
咖啡店的创业计划书,让你hold不住
2014/01/03 职场文书
教师师德承诺书
2014/03/26 职场文书
人力资源管理专业毕业生自荐书
2014/05/25 职场文书
大学毕业生求职自荐书
2014/06/05 职场文书
贷款委托书怎么写
2014/08/02 职场文书
秋季运动会演讲稿
2014/09/16 职场文书
2015年保送生自荐信
2015/03/24 职场文书
Python实战之实现康威生命游戏
2021/04/26 Python