SCanvasView.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. /*
  2. * *********************************************************************************************************************
  3. *
  4. * !!
  5. * .F88X
  6. * X8888Y
  7. * .}888888N;
  8. * i888888N; .:! .I$WI:
  9. * R888888I .'N88~ i8}+8Y&8"l8i$8>8W~'>W8}8]KW+8IIN"8&
  10. * .R888888I .;N8888~ .X8' "8I.!,/8" !%NY8`"8I8~~8>,88I
  11. * +888888N; .8888888Y "&&8Y.}8,
  12. * ./888888N; .R888888Y .'}~ .>}'.`+> i}! "i' +/' .'i~ !11,.:">, .~]! .i}i
  13. * ~888888%: .I888888l .]88~`1/iY88Ii+1'.R$8$8]"888888888> Y8$ W8E X8E W8888'188Il}Y88$*
  14. * 18888888 E8888881 .]W%8$`R8X'&8%++N8i,8N%N8+l8%` .}8N:.R$RE%N88N%N$K$R 188,FE$8%~Y88I
  15. * .E888888I .i8888888' .:$8I;88+`E8R:/8N,.>881.`$8E/1/]N8X.Y8N`"KF&&FK!'88*."88K./$88%RN888+~
  16. * 8888888I .,N888888~ ~88i"8W,!N8*.I88.}888%F,i$88"F88" 888:E8X.>88!i88>`888*.}Fl1]*}1YKi'
  17. * i888888N' I888Y ]88;/EX*IFKFK88X K8R .l8W 88Y ~88}'88E&%8W.X8N``]88!.$8K .:W8I
  18. * .i888888N; I8Y .&8$ .X88! i881.:%888>I88 ;88] +88+.';;;;:.Y88X 18N.,88l .+88/
  19. * .:R888888I
  20. * .&888888I Copyright (c) 2009-2020. 博锐尚格科技股份有限公司
  21. * ~8888'
  22. * .!88~ All rights reserved.
  23. *
  24. * *********************************************************************************************************************
  25. */
  26. import {
  27. SMouseButton,
  28. SMouseEvent,
  29. SNetUtil,
  30. SObject,
  31. STouchState
  32. } from "@persagy-web/base";
  33. import { SSvgPaintEngine } from "./engines/SSvgPaintEngine";
  34. import { SPoint } from "./types/SPoint";
  35. import { SPainter } from "./SPainter";
  36. /**
  37. * Canvas 视图基类
  38. *
  39. * @author 庞利祥 <sybotan@126.com>
  40. */
  41. export class SCanvasView extends SObject {
  42. /** 需要绘制 */
  43. private _needDraw = true;
  44. /** canvas 视图 */
  45. protected readonly canvasView: HTMLCanvasElement;
  46. get canvas(): CanvasRenderingContext2D | null {
  47. return this.canvasView.getContext("2d") as CanvasRenderingContext2D;
  48. }
  49. /** 宽度 */
  50. get width(): number {
  51. return this.canvasView.width;
  52. }
  53. /** 高度 */
  54. get height(): number {
  55. return this.canvasView.height;
  56. }
  57. /** 原点坐标 */
  58. private _origin = new SPoint();
  59. get origin(): SPoint {
  60. return this._origin;
  61. }
  62. set origin(v: SPoint) {
  63. // 如果不可移动
  64. if (!this.moveable) {
  65. return;
  66. }
  67. this._origin.x = v.x;
  68. this._origin.y = v.y;
  69. this._needDraw = true;
  70. }
  71. /** 可否进行平移操作 */
  72. moveable = true;
  73. /** 缩放比例 */
  74. private _scale: number = 1;
  75. get scale(): number {
  76. return this._scale;
  77. }
  78. set scale(v: number) {
  79. // 如果不可缩放
  80. if (!this.scalable) {
  81. return;
  82. }
  83. this._scale = v;
  84. this._needDraw = true;
  85. }
  86. /** 可否执行绽放操作 */
  87. scalable = true;
  88. /** 鼠标滚轮缩放速度 */
  89. wheelZoom = 1.05;
  90. /** 最小缩放比例 */
  91. minScale = 0.000001;
  92. /** 最大缩放比例 */
  93. maxScale = 1000000;
  94. /** 最后一次更新时间 */
  95. private _lastFrameTime = 0;
  96. /** 鼠标中键按下时位置 */
  97. private _midKeyPos = new SPoint();
  98. /** 手执状态 */
  99. private _touchState = STouchState.None;
  100. /** 手指拖动点 */
  101. private _touchPoint = new SPoint();
  102. /** 未缩放时二指间距离 */
  103. private _beforeLength = 0;
  104. /** 缩放后二指间距离 */
  105. private _afterLength = 0;
  106. /** 鼠标样式 */
  107. set cursor(v: string) {
  108. this.canvasView.style.cursor = v;
  109. }
  110. /**
  111. * 构造函数
  112. *
  113. * @param id 画布对象 ID
  114. */
  115. constructor(id: string) {
  116. super();
  117. this.canvasView = document.getElementById(id) as HTMLCanvasElement;
  118. this.bindEvent(this.canvasView);
  119. this.requestAnimationFrame(this.loop.bind(this));
  120. }
  121. /**
  122. * 更新视图
  123. */
  124. update(): void {
  125. this._needDraw = true;
  126. }
  127. /**
  128. * 保存图像
  129. *
  130. * @param name 名称
  131. * @param type 图像类型
  132. */
  133. saveImage(name: string, type: string): void {
  134. let url = this.canvasView.toDataURL(`image/${type}`);
  135. SNetUtil.downLoad(name, url);
  136. }
  137. /**
  138. * 图像的 URL
  139. *
  140. * @param type 图像类型
  141. * @return 图像的 URL
  142. */
  143. imageUrl(type: string): string {
  144. return this.canvasView.toDataURL(`image/${type}`);
  145. }
  146. /**
  147. * 视图内容保存为 SVG 文件
  148. *
  149. * @param name 文件名
  150. */
  151. saveSvg(name: string): void {
  152. let url = URL.createObjectURL(
  153. new Blob([this.svgData()], { type: "text/xml,charset=UTF-8" })
  154. );
  155. SNetUtil.downLoad(name, url);
  156. }
  157. /**
  158. * 视图 SVG 图形的数据
  159. *
  160. * @return URL 地址
  161. */
  162. svgData(): string {
  163. let engine = new SSvgPaintEngine(this.width, this.height);
  164. let painter = new SPainter(engine);
  165. this.onDraw(painter);
  166. return engine.toSvg();
  167. }
  168. /**
  169. * 缩放视图时计算视图的位置与缩放比例
  170. *
  171. * @param zoom 缩放比例
  172. * @param x0 缩放计算的中心点 X 坐标
  173. * @param y0 缩放计算的中心点 Y 坐标
  174. */
  175. scaleByPoint(zoom: number, x0: number, y0: number): void {
  176. // 如果不可缩放
  177. if (!this.scalable) {
  178. return;
  179. }
  180. let z = zoom;
  181. // 缩放比例大于等于最大缩放比例
  182. if (this.scale * zoom >= this.maxScale) {
  183. z = this.maxScale / this.scale;
  184. this.scale = this.maxScale;
  185. } else if (this.scale * zoom <= this.minScale) {
  186. // 缩放比例小于等于最小缩放比例
  187. z = this.minScale / this.scale;
  188. this.scale = this.minScale;
  189. } else {
  190. // 缩放比例在最小比例和最大比例范围内
  191. this.scale *= zoom;
  192. }
  193. this.origin.x = x0 - (x0 - this.origin.x) * z;
  194. this.origin.y = y0 - (y0 - this.origin.y) * z;
  195. }
  196. /**
  197. * 循环
  198. */
  199. protected loop(): void {
  200. // 存在 canvas 对象,并且需要刷新
  201. if (null != this.canvas && this._needDraw) {
  202. this._needDraw = false;
  203. let painter = new SPainter(this);
  204. this.onDraw(painter);
  205. }
  206. this.requestAnimationFrame(this.loop.bind(this));
  207. }
  208. /**
  209. * 绘制视图
  210. *
  211. * @param painter painter 对象
  212. */
  213. protected onDraw(painter: SPainter): void {}
  214. /**
  215. * 鼠标单击事件
  216. *
  217. * @param event 事件参数
  218. */
  219. protected onClick(event: MouseEvent): void {}
  220. /**
  221. * 鼠标双击事件
  222. *
  223. * @param event 事件参数
  224. */
  225. protected onDoubleClick(event: MouseEvent): void {}
  226. /**
  227. * 鼠标按下事件
  228. *
  229. * @param event 事件参数
  230. */
  231. protected onMouseDown(event: MouseEvent): void {
  232. let se = new SMouseEvent(event);
  233. // 按下鼠标滚轮
  234. if (se.buttons & SMouseButton.MidButton) {
  235. this._midKeyPos.x = se.x;
  236. this._midKeyPos.y = se.y;
  237. }
  238. }
  239. /**
  240. * 鼠标移动事件
  241. *
  242. * @param event 事件参数
  243. */
  244. protected onMouseMove(event: MouseEvent): void {
  245. // 如果可以移动
  246. if (this.moveable) {
  247. // 按中键移动
  248. let se = new SMouseEvent(event);
  249. // 按下鼠标滚轮
  250. if (se.buttons & SMouseButton.MidButton) {
  251. this.origin.x += se.x - this._midKeyPos.x;
  252. this.origin.y += se.y - this._midKeyPos.y;
  253. this._midKeyPos.x = se.x;
  254. this._midKeyPos.y = se.y;
  255. this.$emit("midMouseMove", se);
  256. this.update();
  257. return;
  258. }
  259. }
  260. }
  261. /**
  262. * 鼠标松开事件
  263. *
  264. * @param event 事件参数
  265. */
  266. protected onMouseUp(event: MouseEvent): void {}
  267. /**
  268. * 上下文菜单事件
  269. *
  270. * @param event 事件参数
  271. */
  272. protected onContextMenu(event: MouseEvent): void {}
  273. /**
  274. * 鼠标滚轮事件
  275. *
  276. * @param event 事件参数
  277. */
  278. protected onMouseWheel(event: WheelEvent): void {
  279. if (event.deltaY < 0) {
  280. // 向上滑动
  281. this.scaleByPoint(this.wheelZoom, event.offsetX, event.offsetY);
  282. } else {
  283. //向上滑动
  284. this.scaleByPoint(1 / this.wheelZoom, event.offsetX, event.offsetY);
  285. }
  286. this.$emit("onMouseWheel", this.scale, event.deltaY < 0);
  287. }
  288. /**
  289. * 按键按下事件
  290. *
  291. * @param event 事件参数
  292. */
  293. protected onKeyDown(event: KeyboardEvent): void {}
  294. /**
  295. * 按键press事件
  296. *
  297. * @param event 事件参数
  298. */
  299. protected onKeyPress(event: KeyboardEvent): void {}
  300. /**
  301. * 按键松开事件
  302. *
  303. * @param event 事件参数
  304. */
  305. protected onKeyUp(event: KeyboardEvent): void {}
  306. /**
  307. * 开始触摸事件
  308. *
  309. * @param event 事件参数
  310. */
  311. protected onTouchStart(event: TouchEvent): void {
  312. let touches = event.touches;
  313. // 单点触摸
  314. if (touches.length == 1) {
  315. this._touchPoint.x = event.touches[0].clientX;
  316. this._touchPoint.y = event.touches[0].clientY;
  317. }
  318. // 两点触摸
  319. if (touches.length == 2) {
  320. this._touchState = STouchState.Zoom;
  321. this._beforeLength = this.getDistance(event);
  322. }
  323. }
  324. /**
  325. * 触摸移动事件
  326. *
  327. * @param event 事件参数
  328. */
  329. protected onTouchMove(event: TouchEvent): void {
  330. let touches = event.touches;
  331. // 单点触摸移动
  332. if (touches.length == 1) {
  333. this.origin.x += event.touches[0].clientX - this._touchPoint.x;
  334. this.origin.y += event.touches[0].clientY - this._touchPoint.y;
  335. this._touchPoint.x = event.touches[0].clientX;
  336. this._touchPoint.y = event.touches[0].clientY;
  337. }
  338. // 两点触摸缩放
  339. if (touches.length == 2) {
  340. this.viewZoom(event);
  341. }
  342. }
  343. /**
  344. * 结束触摸事件
  345. *
  346. * @param event 事件参数
  347. */
  348. protected onTouchEnd(event: TouchEvent): void {
  349. this._touchState = STouchState.None;
  350. }
  351. /**
  352. * View大小变更事件
  353. *
  354. * @param event 事件参数
  355. */
  356. protected onResize(event: UIEvent): void {}
  357. /**
  358. * 绑定事件
  359. *
  360. * @param element 要绑定事件的元素
  361. */
  362. private bindEvent(element: HTMLElement): void {
  363. // 绑定鼠标事件
  364. element.onclick = this.onClick.bind(this);
  365. element.ondblclick = this.onDoubleClick.bind(this);
  366. element.onmousedown = this.onMouseDown.bind(this);
  367. element.onmousemove = this.onMouseMove.bind(this);
  368. element.onmouseup = this.onMouseUp.bind(this);
  369. element.oncontextmenu = this.onContextMenu.bind(this);
  370. element.onwheel = this.onMouseWheel.bind(this);
  371. // 绑定按键事件
  372. element.onkeydown = this.onKeyDown.bind(this);
  373. element.onkeypress = this.onKeyPress.bind(this);
  374. element.onkeyup = this.onKeyUp.bind(this);
  375. // 触摸事件
  376. element.ontouchstart = this.onTouchStart.bind(this);
  377. element.ontouchmove = this.onTouchMove.bind(this);
  378. element.ontouchend = this.onTouchEnd.bind(this);
  379. // 绑定窗口事件
  380. element.onresize = this.onResize.bind(this);
  381. }
  382. /**
  383. * 启动动画帧(即指定时间执行回调函数)
  384. *
  385. * @param callback 回调函数
  386. */
  387. private requestAnimationFrame(callback: FrameRequestCallback): number {
  388. let currTime = new Date().getTime();
  389. let timeToCall = Math.max(0, 16 - (currTime - this._lastFrameTime));
  390. let id = setTimeout(function(): void {
  391. callback(currTime + timeToCall);
  392. }, timeToCall);
  393. this._lastFrameTime = currTime + timeToCall;
  394. return id;
  395. }
  396. /**
  397. * 缩放视图
  398. *
  399. * @param event 触摸事件
  400. */
  401. private viewZoom(event: TouchEvent): boolean {
  402. // 如果是缩放
  403. if (this._touchState == STouchState.Zoom) {
  404. // 获取两点的距离
  405. this._afterLength = this.getDistance(event);
  406. // 变化的长度;
  407. let gapLenght = this._afterLength - this._beforeLength;
  408. // 两指操作变化距离超过 5 缩放(防抖)
  409. if (Math.abs(gapLenght) > 5 && this._beforeLength != 0.0) {
  410. // 求的缩放的比例
  411. let tempScale = this._afterLength / this._beforeLength;
  412. let p = this.getMiddlePoint(event);
  413. // 重设置
  414. this.scaleByPoint(tempScale, p.x, p.y);
  415. this._beforeLength = this._afterLength;
  416. }
  417. }
  418. return true;
  419. }
  420. /**
  421. * 获取两手指之间的距离
  422. *
  423. * @param event 触摸事件
  424. * @return 两手指之间的距离
  425. */
  426. private getDistance(event: TouchEvent): number {
  427. // 两指操作距离小于 2 不做处理
  428. if (event.touches.length < 2) {
  429. return 0;
  430. }
  431. let dx = event.touches[0].clientX - event.touches[1].clientX;
  432. let dy = event.touches[0].clientY - event.touches[1].clientY;
  433. return Math.sqrt(dx * dx + dy * dy);
  434. }
  435. /**
  436. * 获得两个手指触摸点的中点坐标
  437. *
  438. * @param event 触摸事件
  439. * @return 得到视图的 x 坐标中点
  440. */
  441. private getMiddlePoint(event: TouchEvent): SPoint {
  442. return new SPoint(
  443. (event.touches[0].clientX + event.touches[1].clientX) / 2,
  444. (event.touches[0].clientY + event.touches[1].clientY) / 2
  445. );
  446. }
  447. }