Panel-Magic 是一个基于 AngularX+ 并面向设计师或运营人员的可视化搭建平台,目前仅可用于快速生成微信小程序应用,具备与 Photoshop 类似的交互体验!!html
好了,吹完以后接下来开始从技术角度剖析其中主要的实现原理git
在此以前说明该平台的定位,目的不是给技术人员编辑完以后进行二次开发或代码的定制化。关于这个定位问题我我的的想法是,code 问题不可能彻底交托给可视化编辑、除非是相似传统的简单的企业介绍页等还有可能彻底代替,但仍是比不上直接代码生成的工具,因此 Panel-Magic 一开始的定位就是给设计师或运营人员使用,生成的产物再也不是 code。github
关键是中间的数据模型的建模过程以及可视化界面的建立,生成的新数据和源数据都是约定好固定格式的 JSON 描述文件,其包含固定的 key 字段和对应的 value 值类型,生成小程序的过程在生成完新数据以后typescript
目前源数据约定的数据格式为shell
{ "app_id": "", "cata_data": [ { "group": "默认组", "pages": [ { "title": "首页", "name": "首页", "router": "page10001", "isEdit": false, "uniqueId": 1556693791081, }, ], "isEdit": false, "uniqueId": 1556693791066, } ] } 复制代码
更为完整的约定格式在 MockModel.ts编程
src
├── app
│ ├── appdata // AppData 根服务,数据模型 AppDataModel 的核心服务
│ ├── base-class // 基类
│ ├── core // HttpClient 服务
│ ├── panel-extend // 可视化搭建交互部分
│ │ ├── model // 数据模型
│ │ ├── panel-assist-arbor // 右侧可操做区域如对齐、图层、前进后退等操做入口
│ │ ├── panel-catalogue // 页面分组管理
│ │ ├── panel-event // 事件管理
│ │ ├── panel-layer // 图层列表管理
│ │ ├── panel-scaleplate // 标尺管理
│ │ ├── panel-scope-enchantment // 核心拖拽部分,包括辅助线、轮廓描述等
│ │ ├── panel-senior-vessel-edit // 容器组合管理
│ │ ├── panel-shell // “手机壳”区域管理
│ │ ├── panel-soul // 左侧组件库管理
│ │ ├── panel-widget // 每一个部分组件如按钮、文字等
│ │ │ ├── all-widget-container
│ │ │ │ ├── auxiliaryline-widget
│ │ │ │ ├── button-widget
│ │ │ │ ├── linkrange-widget
│ │ │ │ ├── picture-widget
│ │ │ │ ├── rect-widget
│ │ │ │ └── text-widget
│ │ │ ├── all-widget-unit
│ │ │ │ ├── map-view
│ │ │ │ ├── navigation-bar-view
│ │ │ │ ├── rich-text-view
│ │ │ │ ├── slideshow-picture-view
│ │ │ │ └── tab-bar-view
│ │ │ ├── all-widget-vessel
│ │ │ │ └── senior-vessel-widget
│ │ │ └── model
│ │ ├── panel-widget-appearance // “设置”管理
│ │ │ ├── model
│ │ │ ├── panel-widget-animation
│ │ │ ├── panel-widget-clip-path
│ │ │ ├── panel-widget-facade
│ │ │ ├── panel-widget-filter
│ │ │ ├── panel-widget-picture
│ │ │ ├── panel-widget-shadow
│ │ │ └── panel-widget-text
│ │ ├── panel-widget-appearance-site // 每一个部分组件的专属“设置”
│ │ │ ├── panel-button-site
│ │ │ ├── panel-combination-site
│ │ │ ├── panel-line-site
│ │ │ ├── panel-linkrange-site
│ │ │ ├── panel-map-site
│ │ │ ├── panel-picture-site
│ │ │ ├── panel-rect-site
│ │ │ ├── panel-slideshow-picture-site
│ │ │ └── panel-text-site
│ │ └── panel-widget-details // 弹出来的“设置”管理界面
│ ├── public // 公共组件
│ │ ├── directive
│ │ ├── image-gallery
│ │ ├── my-color-picker
│ │ ├── ng-thumb-auto
│ │ ├── pipe
│ │ ├── theme
│ │ ├── top-navbar
│ │ └── util
│ ├── service // 服务端 service
│ │ ├── hs-files
│ │ └── hs-xcx
│ └── share
├── assets // 资源文件
复制代码
为了实现更好的自由布局排版,绝对定位是个人首选选择,也更能匹配像素级别的定制编辑小程序
除了定位数据之外,每一个组件其实都具备通用的样式数据,如边框设置、阴影设置、文本设置、定位设置等通用元素,甚至也具备通用的事件设置,而后对于编辑来讲,组件同时也具备如选中时的轮廓样式数据等,因此咱们定义一个基本组件数据模型,让全部组件都继承这个模型,那就是 PanelWidgetModel.ts微信小程序
拿 button 按钮组件举例来讲,它位于 src/app/panel-extend/panel-widget/all-widget-container/button-widget
;浏览器
├── button-widget.component.html
├── button-widget.component.ts
└── button-widget.data.ts
复制代码
其中
button-widget.data.ts
文件是用于在左侧拖拽组件到中间编辑区域时候的默认样式和事件数据,它是直接实例化了 PanelWidgetModel 并导出bash
其中 component 部分为:
import { Component, OnInit, Input } from "@angular/core"; import { PanelWidgetModel } from "../../model"; @Component({ selector: "app-button-widget", templateUrl: "./button-widget.component.html", styles: [""], }) export class ButtonWidgetComponent implements OnInit { private _widget: PanelWidgetModel; @Input() public get widget(): PanelWidgetModel { return this._widget; } public set widget(v: PanelWidgetModel) { this._widget = v; } constructor() {} ngOnInit() {} } 复制代码
而后在渲染的时候双向绑定里面的文本数据
<p *ngIf="!widget.isHiddenText" class="text-overflow-hidden">{{ widget.autoWidget.content }}</p> 复制代码
对于简单的组件 PanelWidgetModel
提供的基本数据模型足矣;
稍微复杂的组件如 map
地图组件则能够在 component 文件里自行拓展 PanelWidgetModel
类;
有了 PanelWidgetModel
以后,咱们来看看渲染组件的核心代码部分 👇;
<div class="zoom-area" [ngStyle]="{ 'background-color': panelInfo.bgColor }"> <ng-container *ngFor="let widget of widgetList$ | async"> <div class="widget-shell" [ngStyle]="widget.profileModel.styleContent"> <app-panel-widget [widget]="widget" [isSimpleFunc]="false"></app-panel-widget> </div> </ng-container> </div> 复制代码
在模版中异步循环渲染 widgetList$
里的组件并传递数据给 app-panel-widget 组件;
其中 widgetList$
定义为;
public get widgetList$(): BehaviorSubject<Array<PanelWidgetModel>> { return this.panelExtendService.widgetList$; } // 在 panelExtendService 服务里 public widgetList$: BehaviorSubject<Array<PanelWidgetModel>> = new BehaviorSubject<Array<PanelWidgetModel>>([]); 复制代码
就是上述提到的 PanelWidgetModel 类列表;
而 app-panel-widget 组件位于 src/app/panel-extend/panel-widget/panel-widget.component.ts
它负责接收 widgetList$
里的每个不一样组件并根据 type
类型负责渲染对应的组件;
<div class="widget-main" [nrIsStopPropagation]="true" nrDraggable [nrIdBody]="'#free-panel-main'" (launchMouseIncrement)="acceptDraggableIncrement($event)" nrMouseMoveOut (dblclick)="acceptDoubleClick()" (mousedown)="acceptWidgetChecked($event)" (emitMouseType)="acceptMouseMoveOut($event)" (contextmenu)="acceptWidgetRightClick($event)" > <ng-container *ngIf="widget.autoWidget"> <div class="widget-content {{ widget.type }}" *ngIf="widget.type != 'combination'" [ngStyle]="widgetStyle"> <ng-container [ngSwitch]="widget.type"> <!-- more ... --> <!-- 按钮 --> <ng-container *ngSwitchCase="'button'"> <app-button-widget [widget]="widget"></app-button-widget> </ng-container> <!-- more ... --> </ng-container> </div> </ng-container> </div> 复制代码
nrDraggable
: 指定该组件是可拖拽组件launchMouseIncrement
: 由 public 里的 DraggableDirective 指令提供,用于返回鼠标事件的 movementY 和 movementXnrMouseMoveOut
: 由 public 里的 MousemoveoutDirective 指令提供,用于返回鼠标的移入和移出事件监听emitMouseType
: 由 public 里的 MousemoveoutDirective 指令提供,返回鼠标是移入仍是移出事件contextmenu
: 右键事件因此,当你在面板中选中某个组件的时候,不仅仅只是一个简单的 click 事件组成,是由鼠标的移入、鼠标按下、鼠标弹起等分解步骤来完成;
咱们先看看 mousedown
事件, 它执行的方法为 acceptWidgetChecked
;
public acceptWidgetChecked(event: MouseEvent): void { if (!this.isSimpleFunc) { event.stopPropagation(); event.preventDefault(); if ( !this.panelScopeEnchantmentService.scopeEnchantmentModel.outerSphereInsetWidgetList$.value.some( w => w.uniqueId == this.widget.uniqueId ) ) { event.shiftKey == true ? this.panelScopeEnchantmentService.toggleOuterSphereInsetWidget(this.widget) : this.panelScopeEnchantmentService.onlyOuterSphereInsetWidget(this.widget); } else { if (event.shiftKey == true) this.panelScopeEnchantmentService.toggleOuterSphereInsetWidget(this.widget); } this.openMouseMoveLaunch(); } } 复制代码
这里先补充一下,panelScopeEnchantmentService
服务负责管理拖拽时的辅助线计算、轮廓描边生成以及右键事件等核心编辑服务,该服务的 ScopeEnchantmentModel
就是用于生成组件轮廓数据和拖拽点的数据模型类;
所谓'轮廓描述',就是计算多个或单个组件的最长、最高的描边
回到 acceptWidgetChecked
, 这里当鼠标按下的时候并非直接生成该组件的轮廓描述,而是多了 shiftKey
键盘事件的判断,用于按住 shiftKey
的时候多选多个组件并将生成的轮廓描边包含出多个组件,如
其中生成轮廓的逻辑核心部分在 panelScopeEnchantmentService
里的 handleFromWidgetListToProfileOuterSphere
方法,👇
public handleFromWidgetListToProfileOuterSphere(arg: { isLaunch?: boolean } = { isLaunch: true }): void { const oriArr = this.scopeEnchantmentModel.outerSphereInsetWidgetList$.value.map(e => { e.profileModel.isCheck = true; // 根据当前位置从新设置mousecoord e.profileModel.setMouseCoord([e.profileModel.left, e.profileModel.top]); return e.profileModel; }); if (oriArr.length > 0) { // 计算出最小的left,最小的top,最大的width和height const calcResult = this.calcProfileOuterSphereInfo(); // 若是insetWidget数量大于一个则不容许开启旋转,且旋转角度重置 if (oriArr.length == 1) { calcResult.isRotate = true; calcResult.rotate = oriArr[0].rotate; } else { calcResult.isRotate = false; } // 赋值 this.scopeEnchantmentModel.launchProfileOuterSphere(calcResult, arg.isLaunch); // 同时生成八个方位坐标点,若是被选组件大于一个则不生成 this.scopeEnchantmentModel.handleCreateErightCornerPin(); } } 复制代码
其中 calcProfileOuterSphereInfo
是计算大小和位置的核心
public calcProfileOuterSphereInfo(): OuterSphereHasAuxlModel { const insetWidget = this.scopeEnchantmentModel.outerSphereInsetWidgetList$.value; let outerSphere = new OuterSphereHasAuxlModel().setData({ left: Infinity, top: Infinity, width: -Infinity, height: -Infinity, rotate: 0, }); let maxWidth = null; let maxHeight = null; let minWidthEmpty = Infinity; let minHeightEmpty = Infinity; insetWidget.forEach(e => { let offsetCoord = { left: 0, top: 0 }; if (e.profileModel.rotate != 0 && insetWidget.length > 1) { offsetCoord = this.handleOuterSphereRotateOffsetCoord(e.profileModel); } outerSphere.left = Math.min(outerSphere.left, e.profileModel.left + offsetCoord.left); outerSphere.top = Math.min(outerSphere.top, e.profileModel.top + offsetCoord.top); maxWidth = Math.max(maxWidth, e.profileModel.left + e.profileModel.width + offsetCoord.left * -1); maxHeight = Math.max(maxHeight, e.profileModel.top + e.profileModel.height + offsetCoord.top * -1); if (e.profileModel.left + e.profileModel.width < 0) { minWidthEmpty = Math.min(minWidthEmpty, Math.abs(e.profileModel.left) - e.profileModel.width); } else { minWidthEmpty = 0; } if (e.profileModel.top + e.profileModel.height < 0) { minHeightEmpty = Math.min(minHeightEmpty, Math.abs(e.profileModel.top) - e.profileModel.height); } else { minHeightEmpty = 0; } }); outerSphere.width = Math.abs(maxWidth - outerSphere.left) - minWidthEmpty; outerSphere.height = Math.abs(maxHeight - outerSphere.top) - minHeightEmpty; outerSphere.setMouseCoord([outerSphere.left, outerSphere.top]); return outerSphere; } 复制代码
更为完整的逻辑在 panelScopeEnchantmentService
服务里;
接下来就是拖拽事件,拖拽的组件并不仅仅是某个组件,而是轮廓包含在内的全部被选中组件,核心代码在 src/app/panel-extend/panel-scope-enchantment/model/scope-enchantment.model.ts
的 handleLocationInsetWidget
方法里;
/** * 根据主轮廓的位置计算轮廓内被选组件的位置 */ public handleLocationInsetWidget( increment: DraggablePort, allWidget: Array<PanelWidgetModel> = this.outerSphereInsetWidgetList$.value ): void { if (Array.isArray(allWidget)) { const pro = this.valueProfileOuterSphere; // 全部轮廓内的组件计算位置 allWidget.forEach(w => { w.profileModel.mouseCoord[0] += increment.left; w.profileModel.mouseCoord[1] += increment.top; let obj = { left: w.profileModel.mouseCoord[0], top: w.profileModel.mouseCoord[1] }; if (!(pro.lLine || pro.rLine || pro.vcLine)) { obj.left = w.profileModel.mouseCoord[0]; pro.left = pro.mouseCoord[0]; } else { obj.left += pro.left - pro.mouseCoord[0]; } if (!(pro.tLine || pro.bLine || pro.hcLine)) { obj.top = w.profileModel.mouseCoord[1]; pro.top = pro.mouseCoord[1]; } else { obj.top += pro.top - pro.mouseCoord[1]; } w.profileModel.setData(obj); /** * 若是被选的全部组件当中有组合组件combination,则须要从新计算其子集的全部widget轮廓数值 */ if (w.type == "combination") { this.handleLocationInsetWidget(increment, w.autoWidget.content); } }); } } 复制代码
注:因为拖拽的过程中,改变的是每一个组件自身的位置信息数据,而轮廓描述是由 calcProfileOuterSphereInfo
计算生成的,全部在拖拽的过程中还须要实时计算主轮廓数据;
小结:
PanelWidgetModel
类ScopeEnchantmentModel
类,用于描述边框信息,八个方位拖拽点数据等,拖拽组件的过程其实就是将该类选中的全部组件批量改变位置信息默认状况下因此依赖于 PanelWidgetModel
类的组件均可以进行旋转,但就是由于这个旋转角度,所影响的问题包括了拖拽边框拉伸、多选组件一块儿拉伸、对齐辅助线计算不许确等一系列问题,因此在旋转以后须要计算与不旋转时候的差值增量,具体计算方式能够看我另外一篇水文 12.拖拽拉伸加上旋转角度的数学原理
核心函数位于 src/app/panel-extend/panel-scope-enchantment/panel-scope-enchantment.service.ts
的 handleOuterSphereRotateOffsetCoord
;
public handleOuterSphereRotateOffsetCoord( arg: ProfileModel, type: "lt" | "rt" | "lb" | "rb" = "lt" ): { left: number; top: number } | undefined { const fourCoord = this.conversionRotateToOffsetLeftTop({ width: arg.width, height: arg.height, rotate: arg.rotate, }); if (fourCoord) { let min = Infinity; let max = -Infinity; for (let e in fourCoord) { min = Math.min(min, fourCoord[e][0]); max = Math.max(max, fourCoord[e][1]); } const typeObj = { lt: [min, max], rt: [-min, max], lb: [min, -max], rb: [-min, -max], }; if (typeObj[type]) { return { left: Math.round(arg.width / 2 + typeObj[type][0]), top: Math.round(arg.height / 2 - typeObj[type][1]), }; } } return; } /// more... public conversionRotateToOffsetLeftTop(arg: { width: number; height: number; rotate: number; }): { lt: number[]; rt: number[]; lb: number[]; rb: number[]; } { // 转化角度使其成0~360的范围 arg.rotate = this.conversionRotateOneCircle(arg.rotate); let result = { lt: [(arg.width / 2) * -1, arg.height / 2], rt: [arg.width / 2, arg.height / 2], lb: [(arg.width / 2) * -1, (arg.height / 2) * -1], rb: [arg.width / 2, (arg.height / 2) * -1], }; let convRotate = this.conversionRotateToMathDegree(arg.rotate); let calcX = (x, y) => <any>(x * Math.cos(convRotate) + y * Math.sin(convRotate)) * 1; let calcY = (x, y) => <any>(y * Math.cos(convRotate) - x * Math.sin(convRotate)) * 1; result.lt = [calcX(result.lt[0], result.lt[1]), calcY(result.lt[0], result.lt[1])]; result.rt = [calcX(result.rt[0], result.rt[1]), calcY(result.rt[0], result.rt[1])]; result.lb = [result.rt[0] * -1, result.rt[1] * -1]; result.rb = [result.lt[0] * -1, result.lt[1] * -1]; return result; } 复制代码
具体的边框拉伸计算方式核心都在 DraggableTensileCursorService 服务
若是只选中一个组件对其进行边框拉伸是很好计算的,即便有个旋转角度也很好的计算,假若选中的是多个组件一块儿呢?
个人解决方案就是;
拖拽边框拉伸改变的其实不是组件自己的边框,而是主轮廓
ScopeEnchantmentModel
的边框,只是顺便
计算一下这个轮廓内部全部被选中的组件相对于轮廓来讲的位置比例
而已
核心代码位于 src/app/panel-extend/panel-scope-enchantment/model/profile.model.ts
;
/** * 根据传入的主轮廓数据计算该组件在主轮廓里的位置比例 */ public recordInsetProOuterSphereFourProportion(pro: ProfileModel, widget: ProfileModel = this): void { this.insetProOuterSphereFourProportion = { left: (widget.left - pro.left) / pro.width, top: (widget.top - pro.top) / pro.height, right: (widget.left - pro.left + widget.width) / pro.width, bottom: Math.abs(widget.top - pro.top + widget.height) / pro.height, }; } 复制代码
PS: ProfileModel
类是 PanelWidgetModel
类里的用于描述组件自己的轮廓数据类
这样一来全部被选中的组件都有了相对于主轮廓来讲的位置比例,在进行拉伸计算的时候,将组件本身的宽高和主轮廓的宽高比例保持一致,便可
先看看对齐辅助线效果;
用过 PS 的蛇鸡丝应该对这个功能不会陌生,我我的也很喜欢这么牛逼的辅助线对齐;
咱们先看看对齐辅助线渲染的模版文件,它位于 src/app/panel-extend/panel-scope-enchantment/panel-scope-enchantment.component.html
;
<!-- 辅助线 --> <div class="auxiliary-container"> <ng-container *ngIf="scopeEnchantment.profileOuterSphere$ | async"> <div class="v v-left" *ngIf="(scopeEnchantment.profileOuterSphere$ | async).lLine" [ngStyle]="{ left: (scopeEnchantment.profileOuterSphere$ | async).left + (scopeEnchantment.profileOuterSphere$ | async).offsetAmount.left + 'px' }" ></div> <div class="v v-center" *ngIf="(scopeEnchantment.profileOuterSphere$ | async).vcLine" [ngStyle]="{ left: (scopeEnchantment.profileOuterSphere$ | async).vCenterStyle + 'px' }" ></div> <div class="v v-right" *ngIf="(scopeEnchantment.profileOuterSphere$ | async).rLine" [ngStyle]="{ left: (scopeEnchantment.profileOuterSphere$ | async).rightStyle - (scopeEnchantment.profileOuterSphere$ | async).offsetAmount.left + 'px' }" ></div> <div class="h h-top" *ngIf="(scopeEnchantment.profileOuterSphere$ | async).tLine" [ngStyle]="{ top: (scopeEnchantment.profileOuterSphere$ | async).top + (scopeEnchantment.profileOuterSphere$ | async).offsetAmount.top + 'px' }" ></div> <div class="h h-center" *ngIf="(scopeEnchantment.profileOuterSphere$ | async).hcLine" [ngStyle]="{ top: (scopeEnchantment.profileOuterSphere$ | async).hCenterStyle + 'px' }" ></div> <div class="h h-bottom" *ngIf="(scopeEnchantment.profileOuterSphere$ | async).bLine" [ngStyle]="{ top: (scopeEnchantment.profileOuterSphere$ | async).bottomStyle - (scopeEnchantment.profileOuterSphere$ | async).offsetAmount.top + 'px' }" ></div> </ng-container> </div> 复制代码
辅助线数据依赖于 ScopeEnchantmentModel
里的 profileOuterSphere$
, 其实就是描述主轮廓的可观察类, 定义以下;
public profileOuterSphere$: BehaviorSubject<OuterSphereHasAuxlModel> = new BehaviorSubject(null); 复制代码
其中 OuterSphereHasAuxlModel 就是包含了对齐辅助线的全部位置数据
大体思路就是
在点击主轮廓正准备拖拽的时刻,计算好不在主轮廓内的其余外部组件的全部位置数据信息并记录在某个变量里,完了以后在拖拽的过程中,计算主轮廓的位置信息与这个变量内的数据差值是否达到了临界点,从而决定是否显示对齐辅助线和改变位置;
在 src/app/panel-extend/panel-scope-enchantment/panel-scope-enchantment.component.ts
这个组件下开启对主轮廓的订阅
// 生成完主轮廓以后计算其他组件的横线和竖线状况并保存起来 this.profileOuterSphereRX$ = this.scopeEnchantment.profileOuterSphere$.pipe().subscribe(value => { const insetW = this.panelScopeEnchantmentService.scopeEnchantmentModel.outerSphereInsetWidgetList$.value; if (value) { this.createAllLineSave(); // 主轮廓建立完成就开启角度值监听 this.openRotateSubject(value); // 根据角度计算主轮廓的offset坐标增量 const cValue = cloneDeep(value); const offsetCoord = this.panelScopeEnchantmentService.handleOuterSphereRotateOffsetCoord(cValue); value.setOffsetAmount(offsetCoord); // 开始记录全部被选组件的位置比例 insetW.forEach(w => { w.profileModel.recordInsetProOuterSphereFourProportion(value); }); } this.panelScopeEnchantmentService.panelScopeTextEditorModel$.next(null); this.clipPathService.emptyClipPath(); }); 复制代码
而后拖拽过程当中限流的计算位置信息
/** * 计算辅助线的显示与否状况 * 分为6种状况 * 辅助线只会显示在主轮廓的4条边以及2条中线 * 遍历时先寻找离四条边最近的4个数值 * 参数target表示除了用于计算最外主轮廓之外还能计算其余的辅助线状况,(例如左侧的组件库里的待建立的组件) */ public handleAuxlineCalculate( target: OuterSphereHasAuxlModel = this.scopeEnchantmentModel.valueProfileOuterSphere ): void { const outerSphere = target; const offsetAmount = outerSphere.offsetAmount; const aux = this.auxliLineModel$.value; const mouseCoord = outerSphere.mouseCoord; // 差量达到多少范围内开始对齐 const diffNum: number = 4; outerSphere.resetAuxl(); if (mouseCoord) { for (let i: number = 0, l: number = aux.vLineList.length; i < l; i++) { if (Math.abs(aux.vLineList[i] - mouseCoord[0] + offsetAmount.left * -1) <= diffNum) { outerSphere.left = aux.vLineList[i] + offsetAmount.left * -1; outerSphere.lLine = true; } if (Math.abs(aux.vLineList[i] - (mouseCoord[0] + outerSphere.width) + offsetAmount.left) <= diffNum) { outerSphere.left = aux.vLineList[i] - outerSphere.width + offsetAmount.left; outerSphere.rLine = true; } if (outerSphere.lLine == true && outerSphere.rLine == true) break; } for (let i: number = 0, l: number = aux.hLineList.length; i < l; i++) { if (Math.abs(aux.hLineList[i] - mouseCoord[1] + offsetAmount.top * -1) <= diffNum) { outerSphere.top = aux.hLineList[i] + offsetAmount.top * -1; outerSphere.tLine = true; } if (Math.abs(aux.hLineList[i] - (mouseCoord[1] + outerSphere.height) + offsetAmount.top) <= diffNum) { outerSphere.top = aux.hLineList[i] - outerSphere.height + offsetAmount.top; outerSphere.bLine = true; } if (outerSphere.tLine == true && outerSphere.bLine == true) break; } for (let i: number = 0, l: number = aux.hcLineList.length; i < l; i++) { if (Math.abs(aux.hcLineList[i] - (mouseCoord[1] + outerSphere.height / 2)) <= diffNum) { outerSphere.top = aux.hcLineList[i] - outerSphere.height / 2; outerSphere.hcLine = true; break; } } for (let i: number = 0, l: number = aux.vcLineList.length; i < l; i++) { if (Math.abs(aux.vcLineList[i] - (mouseCoord[0] + outerSphere.width / 2)) <= diffNum) { outerSphere.left = aux.vcLineList[i] - outerSphere.width / 2; outerSphere.vcLine = true; break; } } } } 复制代码
关于前进与后退能够看我另外一篇水文 富交互Web应用中的撤销和前进;
实现原理比较简单粗暴,就是把每一次你认为须要记录下来的操做存一份数据到浏览器的 IndexedDB 里,前进就是在表里面查找最新保存的状态并渲染,后退就是查找上一次状态并渲染
我特别喜欢剪贴蒙版部分,在写它的过程中感受就像是作了好几道初中数学大题!
咱们先看看它的效果
它的核心其实就是依赖于一个 CSS 的属性 clip-path
而展现出来的几个固定剪贴蒙版本质上就是在计算组件的 clip-path
对应的不一样属性值
核心文件在 clip-path-mask.model.ts
总体的搭建从架构方面来讲并不复杂,生成的小程序代码包也没那么的神秘,其中花费时间较多的天然就是在处理各类极致交互体验的技术细节上,在实现功能以前建好数据模型是一个良好的习惯,Panel-Magic 还有不少比较复杂的功能点,感兴趣的能够去 Star 一下😉