pen.js 29 KB

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