/* * ********************************************************************************************************************* * * !! * .F88X * X8888Y * .}888888N; * i888888N; .:! .I$WI: * R888888I .'N88~ i8}+8Y&8"l8i$8>8W~'>W8}8]KW+8IIN"8& * .R888888I .;N8888~ .X8' "8I.!,/8" !%NY8`"8I8~~8>,88I * +888888N; .8888888Y "&&8Y.}8, * ./888888N; .R888888Y .'}~ .>}'.`+> i}! "i' +/' .'i~ !11,.:">, .~]! .i}i * ~888888%: .I888888l .]88~`1/iY88Ii+1'.R$8$8]"888888888> Y8$ W8E X8E W8888'188Il}Y88$* * 18888888 E8888881 .]W%8$`R8X'&8%++N8i,8N%N8+l8%` .}8N:.R$RE%N88N%N$K$R 188,FE$8%~Y88I * .E888888I .i8888888' .:$8I;88+`E8R:/8N,.>881.`$8E/1/]N8X.Y8N`"KF&&FK!'88*."88K./$88%RN888+~ * 8888888I .,N888888~ ~88i"8W,!N8*.I88.}888%F,i$88"F88" 888:E8X.>88!i88>`888*.}Fl1]*}1YKi' * i888888N' I888Y ]88;/EX*IFKFK88X K8R .l8W 88Y ~88}'88E&%8W.X8N``]88!.$8K .:W8I * .i888888N; I8Y .&8$ .X88! i881.:%888>I88 ;88] +88+.';;;;:.Y88X 18N.,88l .+88/ * .:R888888I * .&888888I Copyright (c) 2009-2020. 博锐尚格科技股份有限公司 * ~8888' * .!88~ All rights reserved. * * ********************************************************************************************************************* */ import { SMouseButton, SMouseEvent, SNetUtil, SObject, STouchState } from "@persagy-web/base"; import { SSvgPaintEngine } from "./engines/SSvgPaintEngine"; import { SPoint } from "./types/SPoint"; import { SPainter } from "./SPainter"; /** * Canvas 视图基类 * * @author 庞利祥 */ export class SCanvasView extends SObject { /** 需要绘制 */ private _needDraw = true; /** canvas 视图 */ protected readonly canvasView: HTMLCanvasElement; get canvas(): CanvasRenderingContext2D | null { return this.canvasView.getContext("2d") as CanvasRenderingContext2D; } // Get canvas /** 宽度 */ get width(): number { return this.canvasView.width; } // Get width() /** 高度 */ get height(): number { return this.canvasView.height; } // Get height() /** 原点坐标 */ private _origin = new SPoint(); get origin(): SPoint { return this._origin; } // Get origin set origin(v: SPoint) { if (!this.moveable) { return; } this._origin.x = v.x; this._origin.y = v.y; this._needDraw = true; } // Set origin /** 可否进行平移操作 */ moveable = true; /** 缩放比例 */ private _scale: number = 1; get scale(): number { return this._scale; } // Get scale set scale(v: number) { if (!this.scalable) { return; } this._scale = v; this._needDraw = true; } // Set scale /** 可否执行绽放操作 */ scalable = true; /** 鼠标滚轮缩放速度 */ wheelZoom = 1.05; /** 最小缩放比例 */ minScale = 0.000001; /** 最大缩放比例 */ maxScale = 1000000; /** 最后一次更新时间 */ private _lastFrameTime = 0; /** 鼠标中键按下时位置 */ private _midKeyPos = new SPoint(); /** 手执状态 */ private _touchState = STouchState.None; /** 手指拖动点 */ private _touchPoint = new SPoint(); /** 未缩放时二指间距离 */ private _beforeLength = 0; /** 缩放后二指间距离 */ private _afterLength = 0; /** 鼠标样式 */ set cursor(v: string) { this.canvasView.style.cursor = v; } // Set cursor //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /** * 构造函数 * * @param id 画布对象 ID */ constructor(id: string) { super(); this.canvasView = document.getElementById(id) as HTMLCanvasElement; this.bindEvent(this.canvasView); this.requestAnimationFrame(this.loop.bind(this)); } // Constructor /** * 更新视图 */ update(): void { this._needDraw = true; } // Function update() /** * 保存图像 * * @param name 名称 * @param type 图像类型 */ saveImage(name: string, type: string): void { let url = this.canvasView.toDataURL(`image/${type}`); SNetUtil.downLoad(name, url); } // Function saveImage() /** * 图像的 URL * * @param type 图像类型 * @return 图像的 URL */ imageUrl(type: string): string { return this.canvasView.toDataURL(`image/${type}`); } // Function imageUrl() /** * 视图内容保存为 SVG 文件 * * @param name 文件名 */ saveSvg(name: string): void { let url = URL.createObjectURL( new Blob([this.svgData()], { type: "text/xml,charset=UTF-8" }) ); SNetUtil.downLoad(name, url); } // Function saveSvg() /** * 视图 SVG 图形的数据 * * @return URL 地址 */ svgData(): string { let engine = new SSvgPaintEngine(this.width, this.height); let painter = new SPainter(engine); this.onDraw(painter); return engine.toSvg(); } // Function saveSvg() /** * 缩放视图时计算视图的位置与缩放比例 * * @param zoom 缩放比例 * @param x0 缩放计算的中心点 X 坐标 * @param y0 缩放计算的中心点 Y 坐标 */ scaleByPoint(zoom: number, x0: number, y0: number): void { if (!this.scalable) { return; } let z = zoom; /** * 缩放比例在最小比例和最大比例范围内 */ if (this.scale * zoom >= this.maxScale) { z = this.maxScale / this.scale; this.scale = this.maxScale; } else if (this.scale * zoom <= this.minScale) { z = this.minScale / this.scale; this.scale = this.minScale; } else { this.scale *= zoom; } this.origin.x = x0 - (x0 - this.origin.x) * z; this.origin.y = y0 - (y0 - this.origin.y) * z; } // Function scaleByPoint() //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /** * 循环 */ protected loop(): void { /** painter 对象 */ let ctx = this.canvas; if (null != ctx && this._needDraw) { this._needDraw = false; let painter = new SPainter(this); this.onDraw(painter); } this.requestAnimationFrame(this.loop.bind(this)); } // Function loop() /** * 绘制视图 * * @param painter painter 对象 */ protected onDraw(painter: SPainter): void {} // Function onDraw() /** * 鼠标单击事件 * * @param event 事件参数 */ protected onClick(event: MouseEvent): void {} // Function onClick() /** * 鼠标双击事件 * * @param event 事件参数 */ protected onDoubleClick(event: MouseEvent): void {} // Function onDoubleClick() /** * 鼠标按下事件 * * @param event 事件参数 */ protected onMouseDown(event: MouseEvent): void { let se = new SMouseEvent(event); if (se.buttons & SMouseButton.MidButton) { this._midKeyPos.x = se.x; this._midKeyPos.y = se.y; } } // Function onMouseDown() /** * 鼠标移动事件 * * @param event 事件参数 */ protected onMouseMove(event: MouseEvent): void { // 如果可以移动 if (this.moveable) { // 按中键移动 let se = new SMouseEvent(event); if (se.buttons & SMouseButton.MidButton) { this.origin.x += se.x - this._midKeyPos.x; this.origin.y += se.y - this._midKeyPos.y; this._midKeyPos.x = se.x; this._midKeyPos.y = se.y; this.update(); return; } } } // Function onMouseMove() /** * 鼠标松开事件 * * @param event 事件参数 */ protected onMouseUp(event: MouseEvent): void {} // Function onMouseUp() /** * 上下文菜单事件 * * @param event 事件参数 */ protected onContextMenu(event: MouseEvent): void {} // Function onContextMenu() /** * 鼠标滚轮事件 * * @param event 事件参数 */ protected onMouseWheel(event: WheelEvent): void { if (event.deltaY < 0) { this.scaleByPoint(this.wheelZoom, event.offsetX, event.offsetY); } else { this.scaleByPoint(1 / this.wheelZoom, event.offsetX, event.offsetY); } } // Function onMouseWheel() /** * 按键按下事件 * * @param event 事件参数 */ protected onKeyDown(event: KeyboardEvent): void {} // Function onKeydown() /** * 按键press事件 * * @param event 事件参数 */ protected onKeyPress(event: KeyboardEvent): void {} // Function onKeypress() /** * 按键松开事件 * * @param event 事件参数 */ protected onKeyUp(event: KeyboardEvent): void {} // Function onKeyup() /** * 开始触摸事件 * * @param event 事件参数 */ protected onTouchStart(event: TouchEvent): void { let touches = event.touches; if (touches.length == 1) { this._touchPoint.x = event.touches[0].clientX; this._touchPoint.y = event.touches[0].clientY; } if (touches.length == 2) { this._touchState = STouchState.Zoom; this._beforeLength = this.getDistance(event); } } // Function onTouchStart() /** * 触摸移动事件 * * @param event 事件参数 */ protected onTouchMove(event: TouchEvent): void { let touches = event.touches; if (touches.length == 1) { this.origin.x += event.touches[0].clientX - this._touchPoint.x; this.origin.y += event.touches[0].clientY - this._touchPoint.y; this._touchPoint.x = event.touches[0].clientX; this._touchPoint.y = event.touches[0].clientY; } if (touches.length == 2) { this.viewZoom(event); } } // Function onTouchMove() /** * 结束触摸事件 * * @param event 事件参数 */ protected onTouchEnd(event: TouchEvent): void { this._touchState = STouchState.None; } // Function onTouchEnd() /** * View大小变更事件 * * @param event 事件参数 */ protected onResize(event: UIEvent): void {} // Function onClick() /** * 绑定事件 * * @param element 要绑定事件的元素 */ private bindEvent(element: HTMLElement): void { // 绑定鼠标事件 element.onclick = this.onClick.bind(this); element.ondblclick = this.onDoubleClick.bind(this); element.onmousedown = this.onMouseDown.bind(this); element.onmousemove = this.onMouseMove.bind(this); element.onmouseup = this.onMouseUp.bind(this); element.oncontextmenu = this.onContextMenu.bind(this); element.onwheel = this.onMouseWheel.bind(this); // 绑定按键事件 element.onkeydown = this.onKeyDown.bind(this); element.onkeypress = this.onKeyPress.bind(this); element.onkeyup = this.onKeyUp.bind(this); // 触摸事件 element.ontouchstart = this.onTouchStart.bind(this); element.ontouchmove = this.onTouchMove.bind(this); element.ontouchend = this.onTouchEnd.bind(this); // 绑定窗口事件 element.onresize = this.onResize.bind(this); } // Function bindEvent() /** * 启动动画帧(即指定时间执行回调函数) * * @param callback 回调函数 */ private requestAnimationFrame(callback: FrameRequestCallback): number { let currTime = new Date().getTime(); let timeToCall = Math.max(0, 16 - (currTime - this._lastFrameTime)); let id = setTimeout(function(): void { callback(currTime + timeToCall); }, timeToCall); this._lastFrameTime = currTime + timeToCall; return id; } // Function requestAnimationFrame() /** * 缩放视图 * * @param event 触摸事件 */ private viewZoom(event: TouchEvent): boolean { if (this._touchState == STouchState.Zoom) { // 获取两点的距离 this._afterLength = this.getDistance(event); // 变化的长度; let gapLenght = this._afterLength - this._beforeLength; if (Math.abs(gapLenght) > 5 && this._beforeLength != 0.0) { // 求的缩放的比例 let tempScale = this._afterLength / this._beforeLength; let p = this.getMiddlePoint(event); // 重设置 this.scaleByPoint(tempScale, p.x, p.y); this._beforeLength = this._afterLength; } } return true; } // Function onTouchMove() /** * 获取两手指之间的距离 * * @param event 触摸事件 * @return 两手指之间的距离 */ private getDistance(event: TouchEvent): number { if (event.touches.length < 2) { return 0; } let dx = event.touches[0].clientX - event.touches[1].clientX; let dy = event.touches[0].clientY - event.touches[1].clientY; return Math.sqrt(dx * dx + dy * dy); } // Function getDistance() /** * 获得两个手指触摸点的中点坐标 * * @param event 触摸事件 * @return 得到视图的 x 坐标中点 */ private getMiddlePoint(event: TouchEvent): SPoint { return new SPoint( (event.touches[0].clientX + event.touches[1].clientX) / 2, (event.touches[0].clientY + event.touches[1].clientY) / 2 ); } // Function getMiddlePoint() } // Class SCanvasView