网站首页 > 技术文章 正文
数控被锁怎么解锁▉▉▉【一电一 17154833762-】▉▉▉虽然我们讲了这么多个章节,但其实目前为止就只有一个 Rect 能用,略显单调。于是乎,为了让整个画布稍微生动一些,这个章节我们来尝试增加一个图片类,如果你以后需要扩展一个物体类,也可以用同样的方法。另外有时候我们还希望在物体属性改变时或者画布创建后做一些额外的事情,这个时候事件系统就派上用场啦,也就是我们常说的发布订阅,我觉的这是前端应用最广的设计模式没有之一了。
FabricImage 图片类
话不多说,开撸走起。先来看看 FabricImage 图片类的实现,我们可以想一下一个图片类应该具备什么样的功能,可以看看下面图片类代码的调用方式找找灵感:
FabricImage.fromURL(
'https://p26-passport.byteacctimg.com/img/user-avatar/7470b65342454dd6699a6cf772652260~300x300.image',
(img) => { canvas.add(img) }, // 这里需要手动回调添加物体
{ width: 200, height: 200, left: 300, top: 300 }
);
FabricImage.fromURL(
'./src/beidaihe.jpeg',
(img) => { canvas.add(img) }, // 这里需要手动回调添加物体
{ width: 200, height: 200, left: 600, top: 400 }
);
复制代码
上面代码展示了两种最常用的图片加载方式,一个是远程链接,一个是本地图片,调用方式看起来有些特殊,不过我们先不管这个,直接来实现它就行。既然要绘制图片,那肯定要先加载好才能用,这也是图片类特殊的地方,它是异步的,并且加载图片的方法是通用的,所以我们把它写在 Util 这里,来简单看下加载图片的代码(也许你在面试中遇见过):
class Util {
static loadImage(url) {
return new Promise((resolve, reject) => { // 方便链式调用,promise 这玩意多写多熟悉就懂了
const img = document.createElement('img');
img.onload = () => { // 先进行事件监听,要在请求图片前
img.onload = img.onerror = null;
resolve(img);
};;
img.onerror = () => {
reject(new Error('Error loading ' + img.src));
};
img.src = url; // 这个才是真正去请求图片
});
}
}
复制代码
代码不多也不难理解,那接下来就要看如何绘制了。在 canvas 中要想绘制图片也不难,大体过程就是把图片变成 img 标签,当做参数传给 ctx.drawImage 这个画布专用绘制方法,稍微要注意点的就是图片的宽高设置,我们会先取传入参数 options 中间的宽高作为图片的大小,没传参数的话再取图片自身的宽高(因为此时图片已经加载完成,所以可以取到图片的信息),同样的来简单看下代码实现:
class FabricImage extends FabricObject { // 继承基类是必须的
public type: string = 'image'; // 类型标识
public _element: HTMLImageElement;
/** 默认通过 img 标签来绘制,因为最终都是要通过该标签绘制的 */
constructor(element: HTMLImageElement, options) {
super(options);
this._initElement(element, options);
}
_initElement(element: HTMLImageElement, options) {
this._element = element;
this.setOptions(options);
this._setWidthHeight(options);
return this;
}
/** 设置图像大小 */
_setWidthHeight(options) {
this.width = 'width' in options ? options.width : this.getElement() ? this.getElement().width || 0 : 0;
this.height = 'height' in options ? options.height : this.getElement() ? this.getElement().height || 0 : 0;
}
/** 核心:直接调用 drawImage 绘制图像 */
_render(ctx: CanvasRenderingContext2D) {
const x = -this.width / 2;
const y = -this.height / 2;
const elementToDraw = this._element;
elementToDraw && ctx.drawImage(elementToDraw, x, y, this.width, this.height);
}
getElement() {
return this._element;
}
/** 如果是根据 url 或者本地路径加载图像,本质都是取加载图片完成之后在转成 img 标签 */
static fromURL(url, callback, imgOptions) {
Util.loadImage(url).then((img) => {
callback && callback(new FabricImage(img as HTMLImageElement, imgOptions));
});
}
}
复制代码
看完上面的代码,你应该理解了前面为什么要那样调用,虽然看起来有点繁琐。然后。。。一个简简单单的 FabricImage 类就写好啦。不过这里我再补充两个小点:
- 一个是我们可以将图片素材缓存起来,这样如果用到多张相同的图片就不用重复发请求啦;
- 另一个就是 imageSmoothingEnabled 属性,这个是 canvas 用来设置图片是否平滑的属性,默认值为 true,表示平滑,false 只是表示图片不平滑。比如将一张 50*50 的图像放大 3 倍的时候,canvas 会默认做一些抗锯齿处理使之平滑,如果不需要的话可以将其设置成 false,也算是种优化,具体可以看看 mdn 上这个具体例子,这里就作为知识点简单了解下,当然我也截了个示意图意思一下(仔细看,一定能看出差别的):
其实扩展一个类还是非常简单的,你只需要知道这个类会有哪些独特的自有属性,并搞定 _render() 方法即可。
事件派发
因为这个章节内容比较少,所以我就把事件派发的内容也放在这里讲解了。
有时候我们希望在物体初始化前后、状态改变前后、一些交互前后,能够触发相应的事件来实现自己的需求,比如画布被点击了我想...,物体被移动了我想...,这个就是典型的发布订阅模式,前端应用最广泛的设计模式,没有之一(当然只是我觉得),比如:
- html 中的 addEventListener
- vue 中的 EventBus
- 各种库和插件暴露的一些钩子函数(或者说是生命周期) 早前这玩意我也没真正理解,总是看了就忘,因为总感觉这东西很抽象,说不上来这到底是个什么东西,所以这里我希望把它具象化,以便于理解。发布订阅它其实可以理解成一个简单的对象,就像下面这样:
// key 就是事件名,key 存储的值就是一堆回调函数
const eventObj = {
eventName1: [cb1, cb2, ... ],
eventName2: [cb1, cb2, cb3, ... ],
...
// 比如下面这些常见的事件名
click: [cb1, cb2, ... ],
created: [cb1, cb2, cb3, ... ],
mounted: [cb1, cb2, ... ],
}
复制代码
我们最终要构造的就是这样一个对象,eventObj 相当于一个事件管理中心,当我们触发相应条件 eventName 的事件发布时(发布),就会找到 eventObj 里面 eventName 对应的那个数组,然后将里面的回调函数 cb 挨个遍历执行即可。那我们怎么想 eventObj 添加事件回调呢,很简单就是找到 eventName 对应的数组往里 push 就行(订阅),当然为了操作方便我们需要提供 eventObj.on、eventObj.off、eventObj.emit 等方法方便我们添加、触发和删除事件。
下面我们来看看具体实现,这东西写多了就是很简单的一件事情,写法也比较固定,写好了之后也基本不用改,实在不行 copy 也行:
/**
* 发布订阅,事件中心
* 应用场景:可以在特定的时间点触发一系列事件(在本文主要就是渲染前后、初始化物体前后、物体状态改变时)
*/
export class EventCenter {
private __eventListeners; // 就是上面说的 eventObj 那个对象
/** 往某个事件里面添加回调,找到事件名所对应的数组往里push */
on(eventName, handler) {
if (!this.__eventListeners) {
this.__eventListeners = {};
}
if (!this.__eventListeners[eventName]) {
this.__eventListeners[eventName] = [];
}
this.__eventListeners[eventName].push(handler);
return this;
}
/** 触发某个事件回调,找到事件名对应的数组拿出来遍历执行 */
emit(eventName, options = {}) {
if (!this.__eventListeners) {
return this;
}
let listenersForEvent = this.__eventListeners[eventName];
if (!listenersForEvent) {
return this;
}
for (let i = 0, len = listenersForEvent.length; i < len; i++) {
listenersForEvent[i] && listenersForEvent[i].call(this, options);
}
this.__eventListeners[eventName] = listenersForEvent.filter((value) => value !== false);
return this;
}
/** 删除某个事件回调 */
off(eventName, handler) {
if (!this.__eventListeners) {
return this;
}
if (arguments.length === 0) {
// 如果没有参数,就是解绑所有事件
for (eventName in this.__eventListeners) {
this._removeEventListener.call(this, eventName);
}
} else {
// 解绑单个事件
this._removeEventListener.call(this, eventName, handler);
}
return this;
}
_removeEventListener(eventName, handler) {
if (!this.__eventListeners[eventName]) {
return;
}
let eventListener = this.__eventListeners[eventName];
// 注意:这里我们删除监听一般都是置为 null 或者 false
// 当然也可以用 splice 删除,不过 splice 会改变数组长度,这点要尤为注意
if (handler) {
eventListener[eventListener.indexOf(handler)] = false;
} else {
eventListener.fill(false);
}
}
}
复制代码
希望这种模式大家能够达到默写的水平,对我们日后代码的理解也确实是很有帮助的。
然后接下来要做什么呢?很简单,就是让需要事件的类继承至这个事件类就可以了,然后在有需要的地方触发就行了,这里我们以画布为例,看下下面的代码你就知道这种套路了
猜你喜欢
- 2024-11-11 一行JS代码实现一个简单的模板字符串替换「实践」
- 2024-11-11 14个 JavaScript 代码优化技巧 页面代码优化方法及技巧
- 2024-11-11 Web 端实时防挡脸弹幕(基于机器学习)
- 2024-11-11 聊聊苹果营销页中几个有趣的交互动画
- 2024-11-11 由浅入深,66条JavaScript面试知识点(一)
- 2024-11-11 前端JS实现字符串/图片/excel文件下载
- 2024-11-11 吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】
- 2024-11-11 「前端进阶」高性能渲染十万条数据(时间分片)
- 2024-11-11 我是如何零基础入门前端开发的(2021 版)
- 2024-11-11 简单实现一个虚拟形象系统 虚拟形象制作软件下载
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-