JS中实现物体的绘制的步骤和知识点是什么
Admin 2022-09-13 群英技术资讯 291 次浏览
在上个章节中我们已经创建了画布,接下来就可以进行物体的绘制了,那具体要怎么画呢?根据文章标题可以猜到应该是要抽象出一个物体基类,归纳出一些它们的共性,那它们能有啥共性呢,毕竟每个物体好像都是各画各的。对于这个问题大家可以先简单思考几秒钟再往下看????。。。
我们要绘制某个物体,那不就是在画布的某个位置(top、left值)根据某些属性(宽高大小等)画上某个物体(比如矩形、多边形、图片或者路径等等)吗,并且之后还可以对每个物体进行一些交互操作(主要就是平移+旋转+缩放)。这么一说,是不是好像已经把物体的挺多共性给抽离出来呢(真的是万物皆对象啊,前端同学在 canvas 中尤其能体会到这个思想)。
那么,自然而然的我们就需要抽象出一个物体基类(FabricObject),其它物体(如 Rect)只需要继承这个物体基类,就能够很方便的拥有一些通用能力,对于日后的维护和扩展也都是很友好的,看下面的代码理解起来应该会更清晰:
class FabricObject {
/** 物体类型标识 */
public type: string = 'object';
/** 是否可见 */
public visible: boolean = true;
/** 是否处于激活态,也就是是否被选中 */
public active: boolean = false;
/** 物体位置的 top 值,就是 y */
public top: number = 0;
/** 物体位置的 left 值,就是 x */
public left: number = 0;
/** 物体的原始宽度 */
public width: number = 0;
/** 物体的原始高度 */
public height: number = 0;
/** 物体当前的缩放倍数 x */
public scaleX: number = 1;
/** 物体当前的缩放倍数 y */
public scaleY: number = 1;
/** 物体当前的旋转角度 */
public angle: number = 0;
/** 默认水平变换中心 left | right | center */
public originX: string = 'center';
/** 默认垂直变换中心 top | bottom | center */
public originY: string = 'center';
/** 列举常用的属性 */
public stateProperties: string[] = ('top left width height scaleX scaleY ' + 'angle fill originX originY ' + 'stroke strokeWidth ' + 'borderWidth visible').split(' ');
...
constructor(options) {
this.initialize(options); // 初始化各种属性,就是简单的赋值
}
initialize(options) {
options && this.setOptions(options);
}
render() {} // 绘制物体的方法
...
}
上面代码中有几个比较容易混淆的点,就是 originX、originY 和 top、left,以及为啥不用 x、y 来表示物体位置呢?
解答之前,我们先来思考一个问题,如果要在画布的 (x, y) 处绘制一个 100*100
的矩形,这句话会有什么歧义吗?em。。。有的,看下下面这张图:
你会发现两种画法好像都没错,也都挺符合直觉,主要就是因为它们所定义的中心点不一样,所以就有了 originX 和 originY。
物体最重要的一个方法就是 render 了,但是每个物体有各自独特的绘制方法,能抽象出什么呢?想想好像没啥能抽的。确实是这样,所以我们尝试先直接绘制几个普通物体,再通过它们看看能不能倒推出一些通用的东西。
假设要在 (100, 100) 的地方绘制一个 50*50
的矩形,并将其放大 2 倍,之后旋转 45°,该怎么画呢?正常来说我们需要简单计算一下,就像这样:
100*100
ctx.fillRect(-width/2, -height/2, width, height);
,其中 width=50,height=50,然后就尽量不去动它。那怎么画出缩放和旋转的效果,并且画在点 (100, 100) 的地方呢?就是用到之前说的变换坐标系,简单看下代码:ctx.save(); // 之前提到过了,你要修改 ctx 上的一些配置或者画一个物体,最好先 save 一下,这是个好习惯
ctx.translate(100, 100); // 此时原点已经变到了 (100, 100) 的地方
ctx.scale(2, 2); // 坐标系放大两倍
ctx.rotate(Util.degreesToRadians(45)); // 注意 canvas 中用的都是弧度(弧度 / 2 * Math.PI = 角度 / 360),所以需要简单换算下
ctx.fillRect(-width/2, height/2, width, height); // 绘制矩形的方法固定不变,宽高一般也不会去修改
ctx.restore(); // 画完之后还原 ctx 状态,这是个好习惯
再来看看第二个例子,在左下角画一个边长为 100 的等边三角形△,我们要做的就是先把原点移到三角形的某个顶点上(这里我们当然拿左下角的顶点啦),然后通过不断旋转坐标系绘制三条边,看下代码:
ctx.save();
ctx.translate(0, 画布高度); // 左下角变为(0, 0) 点了
ctx.rotate(Util.degreesToRadians(30)); // 准备画左边这条边
ctx.moveTo(0, 0);
ctx.lineTo(100, 0);
ctx.rotate(Util.degreesToRadians(120)); // 准备画右边这条边
ctx.lineTo(100, 0);
ctx.rotate(Util.degreesToRadians(120)); // 准备画下面这条边
ctx.lineTo(100, 0);
ctx.restore();
大家可以在此基础上画一画正多边形,就能够体会到旋转的意思了。 至于第三个画圆的例子,这里也简单放下代码:
ctx.save();
ctx.translate(100, 100);
ctx.scale(2, 2);
ctx.arc(0, 0, r, 0, 2 * Math.PI); // 画圆的方法始终不变
ctx.fill();
ctx.restore();
我们不再把物体上面的变换用于物体自身,而是用于坐标系,从而简化了计算量和绘图操作。
但可能还是不好看出来能抽象出什么(其实就只抽出了变换),所以让我们来看看代码吧:
class FabricObject {
/** 渲染物体的通用流程 */
render(ctx: CanvasRenderingContext2D) {
// 看不见的物体不绘制
if (this.width === 0 || this.height === 0 || !this.visible) return;
// 凡是要变换坐标系或者设置画笔属性都需要用先用 save 保存和再用 restore 还原,避免影响到其他东西的绘制
ctx.save();
// 1、坐标变换
this.transform(ctx);
// 2、绘制物体
this._render(ctx);
ctx.restore();
}
transform(ctx: CanvasRenderingContext2D) {
ctx.translate(this.left, this.top);
ctx.rotate(Util.degreesToRadians(this.angle));
ctx.scale(this.scaleX, this.scaleY);
}
/** 具体由子类来实现,因为这确实是每个子类物体所独有的 */
_render(ctx: CanvasRenderingContext2D) {}
}
从上面的代码中可以看到物体的绘制被分成了两步:transform
和 _render
。
对于 transform
建议大家可以拿正多边形和折线来找找感觉,本质就是 n 条线段通过 translate 来不断改变线段起始位置,通过 rotate 改变方向,通过 scale 来改变线段长度,而绘制期间线段自身的长度其实并没有改变,然后画之前在脑海里想一下每一条线段的效果,看看画的是否与想的一致。记住核心思路(重要的事情说三遍):
transform
还要注意的是:transform
里面的函数换个写法而已,我们用矩阵的形式 matrix(a, b, c, d, tx, ty)
也能达到同样的效果,但是矩阵更加强大并统一了写法,而且除了三种基本的变换,还能达到其他效果,比如斜切 skew。关于矩阵的概念和写法我们会在这个系列的最后几个章节单独讲一下,目前我们可以暂且认为这三种变换和矩阵是等价的。scale 是沿着坐标轴放大,并不一定是水平或竖直方向,假如物体旋转了,就是沿着旋转之后的坐标轴方向放大,如下图所示:
说完了 transform
,我们再来看看 _render
,这个就真没啥共性了,需要由子类自己实现。
接下来就趁热打铁,我们以一个最简单也最常用的 Rect 矩形类为例子来看看子类又是怎么操作的,这里直接上代码,因为确实简单:
/** 矩形类 */
class Rect extends FabricObject {
/** 矩形标识 */
public type: string = 'rect';
/** 圆角 rx */
public rx: number = 0;
/** 圆角 ry */
public ry: number = 0;
constructor(options) {
super(options);
this._initStateProperties();
this._initRxRy(options);
}
/** 一些共有的和独有的属性 */
_initStateProperties() {
this.stateProperties = this.stateProperties.concat(['rx', 'ry']);
}
/** 初始化圆角值 */
_initRxRy(options) {
this.rx = options.rx || 0;
this.ry = options.ry || 0;
}
/** 单纯的绘制一个普普通通的矩形 */
_render(ctx: CanvasRenderingContext2D) {
let rx = this.rx || 0,
ry = this.ry || 0,
x = -this.width / 2,
y = -this.height / 2,
w = this.width,
h = this.height;
// 绘制一个新的东西,大部分情况下都要开启一个新路径,要养成习惯
ctx.beginPath();
// 从左上角开始向右顺时针画一个矩形,这里就是单纯的绘制一个规规矩矩的矩形
// 不考虑旋转缩放啥的,因为旋转缩放会在调用 _render 函数之前处理
// 另外这里考虑了圆角的实现,所以用到了贝塞尔曲线,不然你可以直接画成四条线段,再懒一点可以直接调用原生方法 fillRect 和 strokeRect
// 不过自己写的话自由度更高,也方便扩展
ctx.moveTo(x + rx, y);
ctx.lineTo(x + w - rx, y);
ctx.bezierCurveTo(x + w, y, x + w, y + ry, x + w, y + ry);
ctx.lineTo(x + w, y + h - ry);
ctx.bezierCurveTo(x + w, y + h, x + w - rx, y + h, x + w - rx, y + h);
ctx.lineTo(x + rx, y + h);
ctx.bezierCurveTo(x, y + h, x, y + h - ry, x, y + h - ry);
ctx.lineTo(x, y + ry);
ctx.bezierCurveTo(x, y, x + rx, y, x + rx, y);
ctx.closePath();
if (this.fill) ctx.fill();
if (this.stroke) ctx.stroke();
}
}
现在我们已经有了一个最基础也最为重要的一个物体:矩形。于是就可以将它添加到画布中,我们在上一章节的 Canvas 类中加一个 add 方法,如下代码所示:
class Canvas {
/**
* 添加元素
* 目前的模式是调用 add 添加物体的时候就立马渲染,如果一次性加入大量元素,就会做很多无用功
* 所以可以优化一下,就是先批量添加元素(需要加一个变量标识),最后再统一渲染(手动调用 renderAll 函数即可),这里先了解即可
*/
add(...args): Canvas {
this._objects.push(...args);
this.renderAll();
return this;
}
/** 在下层画布上绘制所有物体 */
renderAll(): Canvas {
// 获取下层画布
const ctx = this.contextContainer;
// 清除画布
this.clearContext(ctx);
// 简单粗暴的遍历渲染
this._objects.forEach(object => {
// render = transfrom + _render
object.render(ctx);
})
return this;
}
}
现在我们只需要传入不同的参数就能在画布中创建形形色色的矩形了,而子类里面的 _render
方法一般写好了就行,很少会去动它。
大家可以类比一下浏览器的盒模型,其实就是四四方方的矩形,然后用 css 中的 transfrom 做各种变换,也能达到各种效果,而元素的宽高大小并没与改变。如果不理解为什么要拆成 transform 和 _render
两部分,大家可以先记住,后面会体会到它的好。
当然你可以能还有其他疑问,比如我们就直接遍历所有物体嘛,绘制的物体一多这样写不会有问题吗?关于这类问题我会在后面的性能优化章节中讲到,敬请期待,哈哈
这里就本章的内容进行一些小的总结,这个章节我们主要学习了如何写一个物体基类 FabricObject 以及最简单的子类实现 Rect,一般物体的绘制大体可分为两步:
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
猜你喜欢
方法:1、使用“new Date(year, month,0)”语句根据指定年份和月份来创建日期对象;2、使用“日期对象.getDate()”语句处理日期对象,返回指定月份的最后一天,即可知道指定月份有多少天。
JavaScript中数组的方法种类众多,在ES3-ES7不同版本时期都有新方法;并且数组的方法还有原型方法和从object继承的方法,本文介绍了JavaScript数组及非数组对象的深浅克隆,希望读者能从中有所收获
微信小程序实现拍照打卡功能 本文实例为大家分享了微信小程序实现拍照打卡的具体代码,供大家参考,具体内容如下 由于拍照组件是相当于一个块,用隐藏显示的方法不太好,为了更好的用户交互,选择了在一个新的页面调用相机组件,上传图片并保存打卡数据的方式. 小程序端 签到页面wxml <view class="signBtn" bindtap="signSubmit"> <view>{{signTime}}</view> <view>打卡签 ...
我用这些方法主要从五个方面去学习。1.方法是干什么的。2.谁能用 (当然是数组啦)。3.有没有返回值,返回值是什么。4.修不修改原来的对象。5.如何使用。接下来就是干货了。1.p
这篇文章主要介绍了解决VantUI popup 弹窗不弹出或无蒙层的问题。具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所有
增值电信经营许可证 : B1.B2-20140078 粤ICP备09006778号 域名注册商资质 粤 D3.1-20240008