pen.js 24 KB

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