pen.js 30 KB

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