painter.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. import QRCode from '../../../libs/util/qrcode.js';
  2. import { isGradient, doGradient } from './gradient.js';
  3. export default class Painter {
  4. constructor(ctx, data, pixelRatio = 2) {
  5. this.ctx = ctx;
  6. this.height = data.height
  7. this.width = data.width
  8. this.views = data.views
  9. this.background = data.background
  10. this.pixelRatio = pixelRatio;
  11. }
  12. draw(callback) {
  13. this.style = {
  14. width: this.width,
  15. height: this.height
  16. };
  17. this._background();
  18. for (const view of this.views) {
  19. this._drawAbsolute(view);
  20. }
  21. this.ctx.draw(false, () => {
  22. if (callback) {
  23. callback();
  24. }
  25. });
  26. }
  27. _background() {
  28. this.ctx.save();
  29. const { width, height } = this.style;
  30. const bg = this.background;
  31. this.ctx.translate(width / 2, height / 2);
  32. this._doClip(this.borderRadius, width, height);
  33. if (!bg) {
  34. // 如果未设置背景,则默认使用透明色
  35. this.ctx.fillStyle = 'transparent';
  36. this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
  37. } else if (bg.startsWith('#') || bg.startsWith('rgba') || bg.toLowerCase() === 'transparent') {
  38. // 背景填充颜色
  39. this.ctx.fillStyle = bg;
  40. this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
  41. } else if (isGradient(bg)) {
  42. doGradient(bg, width, height, this.ctx);
  43. this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
  44. } else {
  45. // 背景填充图片
  46. this.ctx.drawImage(bg, -(width / 2), -(height / 2), width, height);
  47. }
  48. this.ctx.restore();
  49. }
  50. _drawAbsolute(view) {
  51. if (!(view && view.type)) {
  52. // 过滤无效 view
  53. return;
  54. }
  55. // 证明 style 为数组形式,需要合并
  56. if (view.style && view.style.length) {
  57. /* eslint-disable no-param-reassign */
  58. view.style = Object.assign(...view.style);
  59. }
  60. switch (view.type) {
  61. case 'image':
  62. this._drawAbsImage(view);
  63. break;
  64. case 'text':
  65. this._fillAbsText(view);
  66. break;
  67. case 'rect':
  68. this._drawAbsRect(view);
  69. break;
  70. case 'qrcode':
  71. this._drawQRCode(view);
  72. break;
  73. default:
  74. break;
  75. }
  76. }
  77. _border({ borderRadius = 0, width, height, borderWidth = 0, borderStyle = 'solid' }) {
  78. let r1 = 0;
  79. let r2 = 0;
  80. let r3 = 0;
  81. let r4 = 0;
  82. const minSize = Math.min(width, height);
  83. if (borderRadius) {
  84. if (Array.isArray(borderRadius)) {
  85. if (borderRadius.length === 4) {
  86. r1 = Math.min(borderRadius[0], width / 2, height / 2);
  87. r2 = Math.min(borderRadius[1], width / 2, height / 2);
  88. r3 = Math.min(borderRadius[2], width / 2, height / 2);
  89. r4 = Math.min(borderRadius[3], width / 2, height / 2);
  90. } else {
  91. r1 = r2 = r3 = r4 = Math.min(borderRadius[0] || 0, width / 2, height / 2);
  92. }
  93. } else {
  94. r1 = r2 = r3 = r4 = Math.min(borderRadius || 0, width / 2, height / 2);
  95. }
  96. }
  97. const lineWidth = borderWidth || 0;
  98. this.ctx.lineWidth = lineWidth;
  99. if (borderStyle === 'dashed') {
  100. this.ctx.setLineDash([(lineWidth * 4) / 3, (lineWidth * 4) / 3]);
  101. // this.ctx.lineDashOffset = 2 * lineWidth
  102. } else if (borderStyle === 'dotted') {
  103. this.ctx.setLineDash([lineWidth, lineWidth]);
  104. }
  105. const notSolid = borderStyle !== 'solid';
  106. this.ctx.beginPath();
  107. if (notSolid && r1 === 0) {
  108. this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2);
  109. } // 顶边虚线规避重叠规则
  110. if (r1 !== 0) {
  111. this.ctx.arc(-width / 2 + r1, -height / 2 + r1, r1 + lineWidth / 2, 1 * Math.PI, 1.5 * Math.PI);
  112. } //左上角圆弧
  113. this.ctx.lineTo(r2 === 0 ? (notSolid ? width / 2 : width / 2 + lineWidth / 2) : width / 2 - r2, -height / 2 - lineWidth / 2); // 顶边线
  114. if (notSolid && r2 === 0) {
  115. this.ctx.moveTo(width / 2 + lineWidth / 2, -height / 2 - lineWidth);
  116. } // 右边虚线规避重叠规则
  117. if (r2 !== 0) {
  118. this.ctx.arc(width / 2 - r2, -height / 2 + r2, r2 + lineWidth / 2, 1.5 * Math.PI, 2 * Math.PI);
  119. } // 右上角圆弧
  120. this.ctx.lineTo(width / 2 + lineWidth / 2, r3 === 0 ? (notSolid ? height / 2 : height / 2 + lineWidth / 2) : height / 2 - r3); // 右边线
  121. if (notSolid && r3 === 0) {
  122. this.ctx.moveTo(width / 2 + lineWidth, height / 2 + lineWidth / 2);
  123. } // 底边虚线规避重叠规则
  124. if (r3 !== 0) {
  125. this.ctx.arc(width / 2 - r3, height / 2 - r3, r3 + lineWidth / 2, 0, 0.5 * Math.PI);
  126. } // 右下角圆弧
  127. this.ctx.lineTo(r4 === 0 ? (notSolid ? -width / 2 : -width / 2 - lineWidth / 2) : -width / 2 + r4, height / 2 + lineWidth / 2); // 底边线
  128. if (notSolid && r4 === 0) {
  129. this.ctx.moveTo(-width / 2 - lineWidth / 2, height / 2 + lineWidth);
  130. } // 左边虚线规避重叠规则
  131. if (r4 !== 0) {
  132. this.ctx.arc(-width / 2 + r4, height / 2 - r4, r4 + lineWidth / 2, 0.5 * Math.PI, 1 * Math.PI);
  133. } // 左下角圆弧
  134. this.ctx.lineTo(-width / 2 - lineWidth / 2, r1 === 0 ? (notSolid ? -height / 2 : -height / 2 - lineWidth / 2) : -height / 2 + r1); // 左边线
  135. if (notSolid && r1 === 0) {
  136. this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2);
  137. } // 顶边虚线规避重叠规则
  138. if (!notSolid) {
  139. this.ctx.closePath();
  140. }
  141. }
  142. /**
  143. * 根据 borderRadius 进行裁减
  144. */
  145. _doClip(borderRadius, width, height, borderStyle) {
  146. if (borderRadius && width && height) {
  147. // 防止在某些机型上周边有黑框现象,此处如果直接设置 fillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会
  148. // globalAlpha 在 1.9.90 起支持,低版本下无效,但把 fillStyle 设为了 white,相对默认的 black 要好点
  149. this.ctx.globalAlpha = 0;
  150. this.ctx.fillStyle = 'white';
  151. this._border({
  152. borderRadius,
  153. width,
  154. height,
  155. borderStyle
  156. });
  157. this.ctx.fill();
  158. // 在 ios 的 6.6.6 版本上 clip 有 bug,禁掉此类型上的 clip,也就意味着,在此版本微信的 ios 设备下无法使用 border 属性
  159. if (!(getApp().globalData.systemInfo && getApp().globalData.systemInfo.version <= '6.6.6' && getApp().globalData.systemInfo.platform === 'ios')) {
  160. this.ctx.clip();
  161. }
  162. this.ctx.globalAlpha = 1;
  163. }
  164. }
  165. /**
  166. * 画边框
  167. */
  168. _doBorder(view, width, height) {
  169. if (!view.style) {
  170. return;
  171. }
  172. const { borderRadius, borderWidth, borderColor, borderStyle } = view.style;
  173. if (!borderWidth) {
  174. return;
  175. }
  176. this.ctx.save();
  177. this._preProcess(view, true);
  178. this.ctx.strokeStyle = borderColor || 'black';
  179. this._border({
  180. borderRadius,
  181. width,
  182. height,
  183. borderWidth,
  184. borderStyle
  185. });
  186. this.ctx.stroke();
  187. this.ctx.restore();
  188. }
  189. _preProcess(view, notClip) {
  190. let width = 0;
  191. let height;
  192. let extra;
  193. const paddings = this._doPaddings(view);
  194. switch (view.type) {
  195. case 'text': {
  196. const textArray = String(view.text).split('\n');
  197. // 处理多个连续的'\n'
  198. for (let i = 0; i < textArray.length; ++i) {
  199. if (textArray[i] === '') {
  200. textArray[i] = ' ';
  201. }
  202. }
  203. const fontWeight = view.style.fontWeight || '400';
  204. const textStyle = view.style.textStyle || 'normal';
  205. if (!view.style.fontSize) {
  206. view.style.fontSize = 15;
  207. }
  208. this.ctx.font = `${textStyle} ${fontWeight} ${view.style.fontSize}px "${view.style.fontFamily || 'sans-serif'}"`;
  209. // 计算行数
  210. let lines = 0;
  211. const linesArray = [];
  212. for (let i = 0; i < textArray.length; ++i) {
  213. const textLength = this.ctx.measureText(textArray[i]).width;
  214. const minWidth = view.style.fontSize + paddings[1] + paddings[3];
  215. let partWidth = view.style.width ? view.style.width - paddings[1] - paddings[3] : textLength;
  216. if (partWidth < minWidth) {
  217. partWidth = minWidth;
  218. }
  219. const calLines = Math.ceil(textLength / partWidth);
  220. // 取最长的作为 width
  221. width = partWidth > width ? partWidth : width;
  222. lines += calLines;
  223. linesArray[i] = calLines;
  224. }
  225. lines = view.style.maxLines < lines ? view.style.maxLines : lines;
  226. const fontSize = view.style.fontSize;
  227. const lineHeight = view.style.lineHeight ? fontSize * view.style.lineHeight * 1.2 : fontSize;
  228. height = lineHeight * lines;
  229. extra = {
  230. lines: lines,
  231. lineHeight: lineHeight,
  232. textArray: textArray,
  233. linesArray: linesArray
  234. };
  235. break;
  236. }
  237. case 'image': {
  238. // image的长宽设置成auto的逻辑处理
  239. const ratio = this.pixelRatio;
  240. // 有style却未设置width或height,则默认为auto
  241. if (view.style) {
  242. if (!view.style.width) {
  243. view.style.width = 'auto';
  244. }
  245. if (!view.style.height) {
  246. view.style.height = 'auto';
  247. }
  248. }
  249. if (!view.style || (view.style.width === 'auto' && view.style.height === 'auto')) {
  250. width = Math.round(view.sWidth / ratio);
  251. height = Math.round(view.sHeight / ratio);
  252. } else if (view.style.width === 'auto') {
  253. height = view.style.height;
  254. width = (view.sWidth / view.sHeight) * height;
  255. } else if (view.style.height === 'auto') {
  256. width = view.style.width;
  257. height = (view.sHeight / view.sWidth) * width;
  258. } else {
  259. width = view.style.width;
  260. height = view.style.height;
  261. }
  262. break;
  263. }
  264. default:
  265. if (!(view.style.width && view.style.height)) {
  266. console.error('You should set width and height');
  267. return;
  268. }
  269. width = view.style.width;
  270. height = view.style.height;
  271. break;
  272. }
  273. let x;
  274. if (view.style && view.style.right) {
  275. if (typeof view.style.right === 'number') {
  276. x = this.style.width - view.style.right;
  277. } else {
  278. // 数组方式暂不支持,使用第一个值作为right值
  279. const rights = view.style.right;
  280. x = this.style.width - rights[0];
  281. }
  282. } else if (view.style && view.style.left) {
  283. if (typeof view.style.left === 'number') {
  284. x = view.style.left;
  285. } else {
  286. // 数组方式暂不支持,使用第一个值作为left值
  287. const lefts = view.style.left;
  288. x = lefts[0];
  289. }
  290. } else {
  291. x = 0;
  292. }
  293. //const y = view.style && view.style.bottom ? this.style.height - height - view.style.bottom.toPx(true) : (view.style && view.style.top ? view.style.top.toPx(true) : 0);
  294. let y;
  295. if (view.style && view.style.bottom) {
  296. y = this.style.height - height - view.style.bottom;
  297. } else {
  298. if (view.style && view.style.top) {
  299. if (typeof view.style.top === 'number') {
  300. y = view.style.top;
  301. } else {
  302. // 数组方式暂不支持,使用第一个值作为top值
  303. const tops = view.style.top;
  304. y = tops[0];
  305. }
  306. } else {
  307. y = 0;
  308. }
  309. }
  310. const angle = view.style && view.style.rotate ? this._getAngle(view.style.rotate) : 0;
  311. // 当设置了 right 时,默认 align 用 right,反之用 left
  312. const align = view.style && view.style.align ? view.style.align : view.style && view.style.right ? 'right' : 'left';
  313. const verticalAlign = view.style && view.style.verticalAlign ? view.style.verticalAlign : 'top';
  314. // 记录绘制时的画布
  315. let xa = 0;
  316. switch (align) {
  317. case 'center':
  318. xa = x;
  319. break;
  320. case 'right':
  321. xa = x - width / 2;
  322. break;
  323. default:
  324. xa = x + width / 2;
  325. break;
  326. }
  327. let ya = 0;
  328. switch (verticalAlign) {
  329. case 'center':
  330. ya = y;
  331. break;
  332. case 'bottom':
  333. ya = y - height / 2;
  334. break;
  335. default:
  336. ya = y + height / 2;
  337. break;
  338. }
  339. this.ctx.translate(xa, ya);
  340. // 记录该 view 的有效点击区域
  341. // TODO ,旋转和裁剪的判断
  342. // 记录在真实画布上的左侧
  343. let left = x;
  344. if (align === 'center') {
  345. left = x - width / 2;
  346. } else if (align === 'right') {
  347. left = x - width;
  348. }
  349. var top = y;
  350. if (verticalAlign === 'center') {
  351. top = y - height / 2;
  352. } else if (verticalAlign === 'bottom') {
  353. top = y - height;
  354. }
  355. if (view.rect) {
  356. view.rect.left = left;
  357. view.rect.top = top;
  358. view.rect.right = left + width;
  359. view.rect.bottom = top + height;
  360. view.rect.x = view.style && view.style.right ? x - width : x;
  361. view.rect.y = y;
  362. } else {
  363. view.rect = {
  364. left: left,
  365. top: top,
  366. right: left + width,
  367. bottom: top + height,
  368. x: view.style && view.style.right ? x - width : x,
  369. y: y
  370. };
  371. }
  372. view.rect.left = view.rect.left - paddings[3];
  373. view.rect.top = view.rect.top - paddings[0];
  374. view.rect.right = view.rect.right + paddings[1];
  375. view.rect.bottom = view.rect.bottom + paddings[2];
  376. if (view.type === 'text') {
  377. view.rect.minWidth = view.style.fontSize + paddings[1] + paddings[3];
  378. }
  379. this.ctx.rotate(angle);
  380. if (!notClip && view.style && view.style.borderRadius && view.type !== 'rect') {
  381. this._doClip(view.style.borderRadius, width, height, view.style.borderStyle);
  382. }
  383. this._doShadow(view);
  384. return {
  385. width: width,
  386. height: height,
  387. x: x,
  388. y: y,
  389. extra: extra
  390. };
  391. }
  392. _doPaddings(view) {
  393. const { padding } = view.style ? view.style : {};
  394. let pd = [0, 0, 0, 0];
  395. if (padding) {
  396. if (typeof padding === 'string') {
  397. const pdg = padding.split(/\s+/);
  398. if (pdg.length === 1) {
  399. const x = parseFloat(pdg[0]);
  400. pd = [x, x, x, x];
  401. }
  402. if (pdg.length === 2) {
  403. const x = parseFloat(pdg[0]);
  404. const y = parseFloat(pdg[1]);
  405. pd = [x, y, x, y];
  406. }
  407. if (pdg.length === 3) {
  408. const x = parseFloat(pdg[0]);
  409. const y = parseFloat(pdg[1]);
  410. const z = parseFloat(pdg[2]);
  411. pd = [x, y, z, y];
  412. }
  413. if (pdg.length === 4) {
  414. const x = parseFloat(pdg[0]);
  415. const y = parseFloat(pdg[1]);
  416. const z = parseFloat(pdg[2]);
  417. const a = parseFloat(pdg[3]);
  418. pd = [x, y, z, a];
  419. }
  420. } else if (Array.isArray(padding)) {
  421. if (padding.length === 1) {
  422. pd = [padding[0], padding[0], padding[0], padding[0]];
  423. } else if (padding.length === 2) {
  424. pd = [padding[0], padding[1], padding[0], padding[1]];
  425. } else if (padding.length === 3) {
  426. pd = [padding[0], padding[1], padding[2], padding[1]];
  427. } else if (padding.length === 4) {
  428. pd = [padding[0], padding[1], padding[2], padding[3]];
  429. }
  430. }
  431. }
  432. return pd;
  433. }
  434. // 画文字的背景图片
  435. _doBackground(view) {
  436. this.ctx.save();
  437. const { width: rawWidth, height: rawHeight } = this._preProcess(view, true);
  438. const { background } = view.style;
  439. let pd = this._doPaddings(view);
  440. const width = rawWidth + pd[1] + pd[3];
  441. const height = rawHeight + pd[0] + pd[2];
  442. this._doClip(view.style.borderRadius, width, height, view.style.borderStyle);
  443. if (isGradient(background)) {
  444. doGradient(background, width, height, this.ctx);
  445. } else {
  446. this.ctx.fillStyle = background;
  447. }
  448. this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
  449. this.ctx.restore();
  450. }
  451. _drawQRCode(view) {
  452. this.ctx.save();
  453. const { width, height } = this._preProcess(view);
  454. new QRCode({
  455. canvas: this.ctx,
  456. text: view.content,
  457. size: width,
  458. offsetX: width / 2,
  459. offsetY: height / 2,
  460. background: view.style.background,
  461. foreground: view.style.color,
  462. draw: false
  463. });
  464. this.ctx.restore();
  465. this._doBorder(view, width, height);
  466. }
  467. _drawAbsImage(view) {
  468. if (!view.url) {
  469. return;
  470. }
  471. this.ctx.save();
  472. const { width, height } = this._preProcess(view);
  473. // 获得缩放到图片大小级别的裁减框
  474. let rWidth = view.sWidth;
  475. let rHeight = view.sHeight;
  476. let startX = 0;
  477. let startY = 0;
  478. // 绘画区域比例
  479. const cp = width / height;
  480. // 原图比例
  481. const op = view.sWidth / view.sHeight;
  482. if (cp >= op) {
  483. rHeight = rWidth / cp;
  484. startY = Math.round((view.sHeight - rHeight) / 2);
  485. } else {
  486. rWidth = rHeight * cp;
  487. startX = Math.round((view.sWidth - rWidth) / 2);
  488. }
  489. if (view.style && view.style.mode === 'scaleToFill') {
  490. this.ctx.drawImage(view.url, -(width / 2), -(height / 2), width, height);
  491. } else {
  492. this.ctx.drawImage(view.url, startX, startY, rWidth, rHeight, -(width / 2), -(height / 2), width, height);
  493. view.rect.startX = startX / view.sWidth;
  494. view.rect.startY = startY / view.sHeight;
  495. view.rect.endX = (startX + rWidth) / view.sWidth;
  496. view.rect.endY = (startY + rHeight) / view.sHeight;
  497. }
  498. this.ctx.restore();
  499. this._doBorder(view, width, height);
  500. }
  501. _fillAbsText(view) {
  502. if (!view.text) {
  503. return;
  504. }
  505. if (view.style.background) {
  506. // 生成背景
  507. this._doBackground(view);
  508. }
  509. this.ctx.save();
  510. const { width, height, extra } = this._preProcess(view, view.style.background && view.style.borderRadius);
  511. this.ctx.fillStyle = view.style.color || 'black';
  512. const { lines, lineHeight, textArray, linesArray } = extra;
  513. // 如果设置了id,则保留 text 的长度
  514. if (view.id) {
  515. let textWidth = 0;
  516. for (let i = 0; i < textArray.length; ++i) {
  517. const _w = this.ctx.measureText(textArray[i]).width;
  518. textWidth = _w > textWidth ? _w : textWidth;
  519. }
  520. }
  521. let lineIndex = 0;
  522. for (let j = 0; j < textArray.length; ++j) {
  523. const preLineLength = Math.ceil(textArray[j].length / linesArray[j]);
  524. let start = 0;
  525. let alreadyCount = 0;
  526. for (let i = 0; i < linesArray[j]; ++i) {
  527. // 绘制行数大于最大行数,则直接跳出循环
  528. if (lineIndex >= lines) {
  529. break;
  530. }
  531. alreadyCount = preLineLength;
  532. let text = textArray[j].substr(start, alreadyCount);
  533. let measuredWith = this.ctx.measureText(text).width;
  534. // 如果测量大小小于width一个字符的大小,则进行补齐,如果测量大小超出 width,则进行减除
  535. // 如果已经到文本末尾,也不要进行该循环
  536. while (start + alreadyCount <= textArray[j].length && (width - measuredWith > view.style.fontSize || measuredWith - width > view.style.fontSize)) {
  537. if (measuredWith < width) {
  538. text = textArray[j].substr(start, ++alreadyCount);
  539. } else {
  540. if (text.length <= 1) {
  541. // 如果只有一个字符时,直接跳出循环
  542. break;
  543. }
  544. text = textArray[j].substr(start, --alreadyCount);
  545. // break;
  546. }
  547. measuredWith = this.ctx.measureText(text).width;
  548. }
  549. start += text.length;
  550. // 如果是最后一行了,发现还有未绘制完的内容,则加...
  551. if (lineIndex === lines - 1 && (j < textArray.length - 1 || start < textArray[j].length)) {
  552. while (this.ctx.measureText(`${text}...`).width > width) {
  553. if (text.length <= 1) {
  554. // 如果只有一个字符时,直接跳出循环
  555. break;
  556. }
  557. text = text.substring(0, text.length - 1);
  558. }
  559. text += '...';
  560. measuredWith = this.ctx.measureText(text).width;
  561. }
  562. this.ctx.textAlign = view.style.textAlign ? view.style.textAlign : 'left';
  563. let x;
  564. let lineX;
  565. switch (view.style.textAlign) {
  566. case 'center':
  567. x = 0;
  568. lineX = x - measuredWith / 2;
  569. break;
  570. case 'right':
  571. x = width / 2;
  572. lineX = x - measuredWith;
  573. break;
  574. default:
  575. x = -(width / 2);
  576. lineX = x;
  577. break;
  578. }
  579. const y = -(height / 2) + (lineIndex === 0 ? view.style.fontSize : view.style.fontSize + lineIndex * lineHeight);
  580. lineIndex++;
  581. if (view.style.textStyle === 'stroke') {
  582. this.ctx.strokeText(text, x, y, measuredWith);
  583. } else {
  584. this.ctx.fillText(text, x, y, measuredWith);
  585. }
  586. const fontSize = view.style.fontSize;
  587. let textDecoration;
  588. if (view.style.textDecoration) {
  589. this.ctx.lineWidth = fontSize / 13;
  590. this.ctx.beginPath();
  591. if (/\bunderline\b/.test(view.style.textDecoration)) {
  592. this.ctx.moveTo(lineX, y);
  593. this.ctx.lineTo(lineX + measuredWith, y);
  594. textDecoration = {
  595. moveTo: [lineX, y],
  596. lineTo: [lineX + measuredWith, y]
  597. };
  598. }
  599. if (/\boverline\b/.test(view.style.textDecoration)) {
  600. this.ctx.moveTo(lineX, y - fontSize);
  601. this.ctx.lineTo(lineX + measuredWith, y - fontSize);
  602. textDecoration = {
  603. moveTo: [lineX, y - fontSize],
  604. lineTo: [lineX + measuredWith, y - fontSize]
  605. };
  606. }
  607. if (/\bline-through\b/.test(view.style.textDecoration)) {
  608. this.ctx.moveTo(lineX, y - fontSize / 3);
  609. this.ctx.lineTo(lineX + measuredWith, y - fontSize / 3);
  610. textDecoration = {
  611. moveTo: [lineX, y - fontSize / 3],
  612. lineTo: [lineX + measuredWith, y - fontSize / 3]
  613. };
  614. }
  615. this.ctx.closePath();
  616. this.ctx.strokeStyle = view.style.color;
  617. this.ctx.stroke();
  618. }
  619. }
  620. }
  621. this.ctx.restore();
  622. this._doBorder(view, width, height);
  623. }
  624. _drawAbsRect(view) {
  625. this.ctx.save();
  626. const { width, height } = this._preProcess(view);
  627. if (isGradient(view.style.color)) {
  628. doGradient(view.style.color, width, height, this.ctx);
  629. } else {
  630. this.ctx.fillStyle = view.style.color;
  631. }
  632. const { borderRadius, borderStyle, borderWidth } = view.style;
  633. this._border({
  634. borderRadius,
  635. width,
  636. height,
  637. borderWidth,
  638. borderStyle
  639. });
  640. this.ctx.fill();
  641. this.ctx.restore();
  642. this._doBorder(view, width, height);
  643. }
  644. // shadow 支持 (x, y, blur, color), 不支持 spread
  645. // shadow:0px 0px 10px rgba(0,0,0,0.1);
  646. _doShadow(view) {
  647. if (!view.style || !view.style.shadow) {
  648. return;
  649. }
  650. const box = view.style.shadow.replace(/,\s+/g, ',').split(/\s+/);
  651. if (box.length > 4) {
  652. console.error("shadow don't spread option");
  653. return;
  654. }
  655. this.ctx.shadowOffsetX = parseInt(box[0], 10);
  656. this.ctx.shadowOffsetY = parseInt(box[1], 10);
  657. this.ctx.shadowBlur = parseInt(box[2], 10);
  658. this.ctx.shadowColor = box[3];
  659. }
  660. _getAngle(angle) {
  661. return (Number(angle) * Math.PI) / 180;
  662. }
  663. }