draw.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. import { toPx, isNumber, getImageInfo } from './utils'
  2. import { GD } from './gradient'
  3. import QR from './qrcode'
  4. export class Draw {
  5. constructor(context, canvas, use2dCanvas = false, isH5PathToBase64 = false, sleep) {
  6. this.ctx = context
  7. this.canvas = canvas || null
  8. this.use2dCanvas = use2dCanvas
  9. this.isH5PathToBase64 = isH5PathToBase64
  10. this.sleep = sleep
  11. }
  12. roundRect(x, y, w, h, r, fill = false, stroke = false, ) {
  13. if (r < 0) return
  14. const {ctx} = this
  15. ctx.beginPath()
  16. if(!r) {
  17. ctx.rect(x, y, w, h)
  18. } else if(typeof r === 'number' && [0,1,-1].includes(w - r * 2) && [0, 1, -1].includes(h - r * 2)) {
  19. ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 2)
  20. } else {
  21. let {
  22. borderTopLeftRadius: tl = r || 0,
  23. borderTopRightRadius: tr = r || 0,
  24. borderBottomRightRadius: br = r || 0,
  25. borderBottomLeftRadius: bl = r || 0
  26. } = r || {r,r,r,r}
  27. // 右下角
  28. ctx.arc(x + w - br, y + h - br, br, 0, Math.PI * 0.5)
  29. ctx.lineTo(x + bl, y + h)
  30. // 左下角
  31. ctx.arc(x + bl, y + h - bl, bl, Math.PI * 0.5, Math.PI)
  32. ctx.lineTo(x, y + tl)
  33. // 左上角
  34. ctx.arc(x + tl, y + tl, tl, Math.PI, Math.PI * 1.5)
  35. ctx.lineTo(x + w - tr, y)
  36. // 右上角
  37. ctx.arc(x + w - tr, y + tr, tr, Math.PI * 1.5, Math.PI * 2)
  38. ctx.lineTo(x + w, y + h - br)
  39. }
  40. ctx.closePath()
  41. if (stroke) ctx.stroke()
  42. if (fill) ctx.fill()
  43. }
  44. setTransform(box, {transform}) {
  45. const {ctx} = this
  46. const {
  47. scaleX = 1,
  48. scaleY = 1,
  49. translateX = 0,
  50. translateY = 0,
  51. rotate = 0,
  52. skewX = 0,
  53. skewY = 0
  54. } = transform || {}
  55. let {
  56. left: x,
  57. top: y,
  58. width: w,
  59. height: h
  60. } = box
  61. ctx.scale(scaleX, scaleY)
  62. ctx.translate(
  63. w * (scaleX > 0 ? 1 : -1) / 2 + (x + translateX) / scaleX,
  64. h * (scaleY > 0 ? 1 : -1) / 2 + (y + translateY) / scaleY)
  65. if(rotate) {
  66. ctx.rotate(rotate * Math.PI / 180)
  67. }
  68. if(skewX || skewY) {
  69. ctx.transform(1, Math.tan(skewY * Math.PI/180), Math.tan(skewX * Math.PI/180), 1 , 0, 0)
  70. }
  71. }
  72. setBackground(bd, w, h) {
  73. const {ctx} = this
  74. if (!bd) {
  75. // #ifndef MP-TOUTIAO || MP-BAIDU
  76. ctx.setFillStyle('transparent')
  77. // #endif
  78. // #ifdef MP-TOUTIAO || MP-BAIDU
  79. ctx.setFillStyle('rgba(0,0,0,0)')
  80. // #endif
  81. } else if(GD.isGradient(bd)) {
  82. GD.doGradient(bd, w, h, ctx);
  83. } else {
  84. ctx.setFillStyle(bd)
  85. }
  86. }
  87. setShadow({boxShadow: bs = []}) {
  88. const {ctx} = this
  89. if (bs.length) {
  90. const [x, y, b, c] = bs
  91. ctx.setShadow(x, y, b, c)
  92. }
  93. }
  94. setBorder(box, style) {
  95. const {ctx} = this
  96. let {
  97. left: x,
  98. top: y,
  99. width: w,
  100. height: h
  101. } = box
  102. const {border, borderBottom, borderTop, borderRight, borderLeft, borderRadius: r, opacity = 1} = style;
  103. const {
  104. borderWidth : bw = 0,
  105. borderStyle : bs,
  106. borderColor : bc,
  107. } = border || {}
  108. const {
  109. borderBottomWidth : bbw = bw,
  110. borderBottomStyle : bbs = bs,
  111. borderBottomColor : bbc= bc,
  112. } = borderBottom || {}
  113. const {
  114. borderTopWidth: btw = bw,
  115. borderTopStyle: bts = bs,
  116. borderTopColor: btc = bc,
  117. } = borderTop || {}
  118. const {
  119. borderRightWidth: brw = bw,
  120. borderRightStyle: brs = bs,
  121. borderRightColor: brc = bc,
  122. } = borderRight || {}
  123. const {
  124. borderLeftWidth: blw = bw,
  125. borderLeftStyle: bls = bs,
  126. borderLeftColor: blc = bc,
  127. } = borderLeft || {}
  128. let {
  129. borderTopLeftRadius: tl = r || 0,
  130. borderTopRightRadius: tr = r || 0,
  131. borderBottomRightRadius: br = r || 0,
  132. borderBottomLeftRadius: bl = r || 0
  133. } = r || {r,r,r,r}
  134. if(!borderBottom && !borderLeft && !borderTop && !borderRight && !border) return;
  135. const _borderType = (w, s, c) => {
  136. // #ifndef APP-NVUE
  137. if (s == 'dashed') {
  138. // #ifdef MP
  139. ctx.setLineDash([Math.ceil(w * 4 / 3), Math.ceil(w * 4 / 3)])
  140. // #endif
  141. // #ifndef MP
  142. ctx.setLineDash([Math.ceil(w * 6), Math.ceil(w * 6)])
  143. // #endif
  144. } else if (s == 'dotted') {
  145. ctx.setLineDash([w, w])
  146. }
  147. // #endif
  148. ctx.setStrokeStyle(c)
  149. }
  150. const _setBorder = (x1, y1, x2, y2, x3, y3, r1, r2, p1, p2, p3, bw, bs, bc) => {
  151. ctx.save()
  152. this.setOpacity(style)
  153. this.setTransform(box, style)
  154. ctx.setLineWidth(bw)
  155. _borderType(bw, bs, bc)
  156. ctx.beginPath()
  157. ctx.arc(x1, y1, r1, Math.PI * p1, Math.PI * p2)
  158. ctx.lineTo(x2, y2)
  159. ctx.arc(x3, y3, r2, Math.PI * p2, Math.PI * p3)
  160. ctx.stroke()
  161. ctx.restore()
  162. }
  163. if(border) {
  164. ctx.save()
  165. this.setOpacity(style)
  166. this.setTransform(box, style)
  167. ctx.setLineWidth(bw)
  168. _borderType(bw, bs, bc)
  169. this.roundRect(-w/2, -h/2, w, h, r, false, bc ? true : false)
  170. ctx.restore()
  171. }
  172. x = -w/2
  173. y = -h/2
  174. if(borderBottom) {
  175. _setBorder(x + w - br, y + h - br, x + bl, y + h, x + bl, y + h - bl, br, bl, 0.25, 0.5, 0.75, bbw, bbs, bbc)
  176. }
  177. if(borderLeft) {
  178. // 左下角
  179. _setBorder(x + bl, y + h - bl, x, y + tl, x + tl, y + tl, bl, tl, 0.75, 1, 1.25, blw, bls, blc)
  180. }
  181. if(borderTop) {
  182. // 左上角
  183. _setBorder(x + tl, y + tl, x + w - tr, y, x + w - tr, y + tr, tl, tr, 1.25, 1.5, 1.75, btw, bts, btc)
  184. }
  185. if(borderRight) {
  186. // 右上角
  187. _setBorder(x + w - tr, y + tr, x + w, y + h - br, x + w - br, y + h - br, tr, br, 1.75, 2, 0.25, btw, bts, btc)
  188. }
  189. }
  190. setOpacity({opacity = 1}) {
  191. this.ctx.setGlobalAlpha(opacity)
  192. }
  193. drawView(box, style) {
  194. const {ctx} = this
  195. const {
  196. left: x,
  197. top: y,
  198. width: w,
  199. height: h
  200. } = box
  201. let {
  202. borderRadius = 0,
  203. border,
  204. borderTop,
  205. borderBottom,
  206. borderLeft,
  207. borderRight,
  208. color = '#000000',
  209. backgroundColor: bg,
  210. rotate,
  211. shadow
  212. } = style || {}
  213. ctx.save()
  214. this.setOpacity(style)
  215. this.setTransform(box, style)
  216. this.setShadow(style)
  217. this.setBackground(bg, w, h)
  218. this.roundRect(-w/2, -h/2, w, h, borderRadius, true, false)
  219. ctx.restore()
  220. this.setBorder(box, style)
  221. }
  222. async drawImage(img, box = {}, style = {}, custom = true) {
  223. await new Promise(async (resolve, reject) => {
  224. const {ctx} = this
  225. const canvas = this.canvas
  226. const {
  227. borderRadius = 0,
  228. mode,
  229. padding = {},
  230. backgroundColor: bg,
  231. } = style
  232. const {paddingTop: pt = 0, paddingLeft: pl= 0, paddingRight: pr= 0, paddingBottom: pb = 0} = padding
  233. let {
  234. left: x,
  235. top: y,
  236. width: w,
  237. height: h
  238. } = box
  239. ctx.save()
  240. if(!custom) {
  241. this.setOpacity(style)
  242. this.setTransform(box, style)
  243. this.setBackground(bg, w, h)
  244. this.setShadow(style)
  245. x = -w/2
  246. y = -h/2
  247. this.roundRect(x, y, w, h, borderRadius, true, false)
  248. }
  249. ctx.clip()
  250. const _modeImage = (img) => {
  251. x += pl
  252. y += pt
  253. w = w - pl - pr
  254. h = h - pt - pb
  255. // 获得图片原始大小
  256. let rWidth = img.width
  257. let rHeight = img.height
  258. let startX = 0
  259. let startY = 0
  260. // 绘画区域比例
  261. const cp = w / h
  262. // 原图比例
  263. // 如果大于1 是宽度长
  264. // 如果小于1 是高度长
  265. const op = rWidth / rHeight
  266. if (mode === 'scaleToFill' || !img.width) {
  267. ctx.drawImage(img.src, x, y, w, h);
  268. } else if(mode === 'aspectFit') {
  269. if(cp >= op) {
  270. rWidth = h * op;
  271. rHeight = h
  272. startX = x + Math.round(w - rWidth) / 2
  273. startY = y
  274. } else {
  275. rWidth = w
  276. rHeight = w / op;
  277. startX = x
  278. startY = y + Math.round(h - rHeight) / 2
  279. }
  280. ctx.drawImage(img.src, startX, startY, rWidth, rHeight);
  281. } else {
  282. if (cp >= op) {
  283. rHeight = rWidth / cp;
  284. // startY = Math.round((h - rHeight) / 2)
  285. } else {
  286. rWidth = rHeight * cp;
  287. startX = Math.round(((img.width || w) - rWidth) / 2)
  288. }
  289. // 百度小程序 开发工具 顺序有问题 暂不知晓真机
  290. // #ifdef MP-BAIDU
  291. ctx.drawImage(img.src, x, y, w, h, startX, startY, rWidth, rHeight)
  292. // #endif
  293. // #ifndef MP-BAIDU
  294. ctx.drawImage(img.src, startX, startY, rWidth, rHeight, x, y, w, h)
  295. // #endif
  296. }
  297. }
  298. const _drawImage = (img) => {
  299. if (this.use2dCanvas) {
  300. const Image = canvas.createImage()
  301. Image.onload = () => {
  302. img.src = Image
  303. _modeImage(img)
  304. _restore()
  305. }
  306. Image.onerror = () => {
  307. console.error(`createImage fail: ${img}`)
  308. reject(new Error(`createImage fail: ${img}`))
  309. }
  310. Image.src = img.src
  311. } else {
  312. _modeImage(img)
  313. _restore()
  314. }
  315. }
  316. const _restore = () => {
  317. ctx.restore()
  318. this.setBorder(box, style)
  319. setTimeout(() => {
  320. resolve(true)
  321. }, this.sleep)
  322. }
  323. if(typeof img === 'string') {
  324. const {path: src, width, height} = await getImageInfo(img, this.isH5PathToBase64)
  325. _drawImage({src, width, height})
  326. } else {
  327. _drawImage(img)
  328. }
  329. })
  330. }
  331. drawText(text, box, style, rules) {
  332. const {ctx} = this
  333. let {
  334. left: x,
  335. top: y,
  336. width: w,
  337. height: h,
  338. offsetLeft: ol = 0
  339. } = box
  340. let {
  341. color = '#000000',
  342. lineHeight = '1.4em',
  343. fontSize = 14,
  344. fontWeight,
  345. fontFamily,
  346. textStyle,
  347. textAlign = 'left',
  348. verticalAlign: va = 'top',
  349. backgroundColor: bg,
  350. maxLines,
  351. display,
  352. padding = {},
  353. borderRadius = 0,
  354. textDecoration: td
  355. } = style
  356. const {paddingTop: pt = 0, paddingLeft: pl = 0} = padding
  357. lineHeight = toPx(lineHeight, fontSize)
  358. if (!text) return
  359. ctx.save()
  360. this.setOpacity(style)
  361. this.setTransform(box, style)
  362. x = -w/2
  363. y = -h/2
  364. ctx.setTextBaseline(va)
  365. ctx.setFont({fontFamily, fontSize, fontWeight, textStyle})
  366. ctx.setTextAlign(textAlign)
  367. if(bg) {
  368. this.setBackground(bg, w, h)
  369. this.roundRect(x, y, w, h, borderRadius, 1, 0)
  370. }
  371. if(display && display.includes('lock')) {
  372. x += pl
  373. y += pt
  374. }
  375. this.setShadow(style)
  376. ctx.setFillStyle(color)
  377. let rulesObj = {};
  378. if(rules) {
  379. if (rules.word.length > 0) {
  380. for (let i = 0; i < rules.word.length; i++) {
  381. let startIndex = 0,
  382. index;
  383. while ((index = text.indexOf(rules.word[i], startIndex)) > -1) {
  384. rulesObj[index] = {
  385. reset: true
  386. };
  387. for (let j = 0; j < rules.word[i].length; j++) {
  388. rulesObj[index + j] = {
  389. reset: true
  390. };
  391. }
  392. startIndex = index + 1;
  393. }
  394. }
  395. }
  396. }
  397. // 水平布局
  398. switch (textAlign) {
  399. case 'left':
  400. break
  401. case 'center':
  402. x += 0.5 * w
  403. break
  404. case 'right':
  405. x += w
  406. break
  407. default:
  408. break
  409. }
  410. const textWidth = ctx.measureText(text, fontSize).width
  411. const actualHeight = Math.ceil(textWidth / w) * lineHeight
  412. let paddingTop = Math.ceil((h - actualHeight) / 2)
  413. if (paddingTop < 0) paddingTop = 0
  414. // 垂直布局
  415. switch (va) {
  416. case 'top':
  417. break
  418. case 'middle':
  419. y += fontSize / 2
  420. break
  421. case 'bottom':
  422. y += fontSize
  423. break
  424. default:
  425. break
  426. }
  427. // 绘线
  428. const _drawLine = (x, y, textWidth) => {
  429. const { system } = uni.getSystemInfoSync()
  430. if(/win|mac/.test(system)){
  431. y += (fontSize / 3)
  432. }
  433. // 垂直布局
  434. switch (va) {
  435. case 'top':
  436. break
  437. case 'middle':
  438. y -= fontSize / 2
  439. break
  440. case 'bottom':
  441. y -= fontSize
  442. break
  443. default:
  444. break
  445. }
  446. let to = x
  447. switch (textAlign) {
  448. case 'left':
  449. x = x
  450. to+= textWidth
  451. break
  452. case 'center':
  453. x = x - textWidth / 2
  454. to = x + textWidth
  455. break
  456. case 'right':
  457. to = x
  458. x = x - textWidth
  459. break
  460. default:
  461. break
  462. }
  463. if(td) {
  464. ctx.setLineWidth(fontSize / 13);
  465. ctx.beginPath();
  466. if (/\bunderline\b/.test(td)) {
  467. y -= inlinePaddingTop * 0.8
  468. ctx.moveTo(x, y);
  469. ctx.lineTo(to, y);
  470. }
  471. if (/\boverline\b/.test(td)) {
  472. y += inlinePaddingTop
  473. ctx.moveTo(x, y - lineHeight);
  474. ctx.lineTo(to, y - lineHeight);
  475. }
  476. if (/\bline-through\b/.test(td)) {
  477. ctx.moveTo(x , y - lineHeight / 2 );
  478. ctx.lineTo(to, y - lineHeight /2 );
  479. }
  480. ctx.closePath();
  481. ctx.setStrokeStyle(color);
  482. ctx.stroke();
  483. }
  484. }
  485. const _reset = (text, x, y) => {
  486. const rs = Object.keys(rulesObj)
  487. for (let i = 0; i < rs.length; i++) {
  488. const item = rulesObj[rs[i]]
  489. // ctx.globalCompositeOperation = "destination-out";
  490. ctx.save();
  491. ctx.setFillStyle(rules.color);
  492. if(item.char) {
  493. ctx.fillText(item.char, item.x , item.y)
  494. }
  495. ctx.restore();
  496. }
  497. }
  498. const _setText = (isReset, char) => {
  499. if(isReset) {
  500. const t1 = Math.round(ctx.measureText('\u0020', fontSize).width)
  501. const t2 = Math.round(ctx.measureText('\u3000', fontSize).width)
  502. const t3 = Math.round(ctx.measureText(char, fontSize).width)
  503. let _char = ''
  504. let _num = 1
  505. if(t3 == t2){
  506. _char ='\u3000'
  507. _num = 1
  508. } else {
  509. _char = '\u0020'
  510. _num = Math.ceil(t3 / t1)
  511. }
  512. return new Array(_num).fill(_char).join('')
  513. } else {
  514. return char
  515. }
  516. }
  517. const _setRulesObj = (text, index, x, y) => {
  518. rulesObj[index].x = x
  519. rulesObj[index].y = y
  520. rulesObj[index].char = text
  521. }
  522. const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
  523. // 不超过一行
  524. if (textWidth + ol <= w && !text.includes('\n')) {
  525. x = x + ol
  526. const rs = Object.keys(rulesObj)
  527. let _text = text.split('')
  528. if(rs) {
  529. for (let i = 0; i < rs.length; i++) {
  530. const index = rs[i]
  531. const t = _text[index]
  532. let char = _setText(rulesObj[index], t)
  533. _text[index] = char
  534. _setRulesObj(t, index, x + ctx.measureText(text.substring(0, index), fontSize).width, y + inlinePaddingTop)
  535. }
  536. _reset()
  537. }
  538. ctx.fillText(_text.join(''), x, y + inlinePaddingTop)
  539. y += lineHeight
  540. _drawLine(x, y, textWidth)
  541. ctx.restore()
  542. this.setBorder(box, style)
  543. return
  544. }
  545. // 多行文本
  546. const chars = text.split('')
  547. const _y = y
  548. let _x = x
  549. // 逐行绘制
  550. let line = ''
  551. let lineIndex = 0
  552. for(let index = 0 ; index <= chars.length; index++){
  553. let ch = chars[index] || ''
  554. const isLine = ch === '\n'
  555. const isRight = ch == ''// index == chars.length
  556. ch = isLine ? '' : ch;
  557. let textline = line + _setText(rulesObj[index], ch)
  558. let textWidth = ctx.measureText(textline, fontSize).width
  559. // 绘制行数大于最大行数,则直接跳出循环
  560. if (lineIndex >= maxLines) {
  561. break;
  562. }
  563. if(lineIndex == 0) {
  564. textWidth = textWidth + ol
  565. _x = x + ol
  566. } else {
  567. textWidth = textWidth
  568. _x = x
  569. }
  570. if(rulesObj[index]) {
  571. _setRulesObj(ch, index, _x + ctx.measureText(line, fontSize).width, y + inlinePaddingTop)
  572. }
  573. if (textWidth > w || isLine || isRight) {
  574. lineIndex++
  575. line = isRight && textWidth <= w ? textline : line
  576. if(lineIndex === maxLines && textWidth > w) {
  577. while( ctx.measureText(`${line}...`, fontSize).width > w) {
  578. if (line.length <= 1) {
  579. // 如果只有一个字符时,直接跳出循环
  580. break;
  581. }
  582. line = line.substring(0, line.length - 1);
  583. if(rulesObj[index - 1]) {
  584. rulesObj[index - 1].char = ''
  585. }
  586. }
  587. line += '...'
  588. }
  589. ctx.fillText(line, _x, y + inlinePaddingTop)
  590. y += lineHeight
  591. _drawLine(_x, y, textWidth)
  592. line = ch
  593. if ((y + lineHeight) > (_y + h)) break
  594. } else {
  595. line = textline
  596. }
  597. }
  598. const rs = Object.keys(rulesObj)
  599. if(rs) {
  600. _reset()
  601. }
  602. ctx.restore()
  603. this.setBorder(box, style)
  604. }
  605. async drawNode(element) {
  606. const {
  607. layoutBox,
  608. computedStyle,
  609. name,
  610. rules
  611. } = element
  612. const {
  613. src,
  614. text
  615. } = element.attributes
  616. if (name === 'view') {
  617. this.drawView(layoutBox, computedStyle)
  618. } else if (name === 'image' && src) {
  619. await this.drawImage(element.attributes, layoutBox, computedStyle, false)
  620. } else if (name === 'text') {
  621. this.drawText(text, layoutBox, computedStyle, rules)
  622. } else if (name === 'qrcode') {
  623. QR.api.draw(text, this, layoutBox, computedStyle)
  624. }
  625. if (!element.children) return
  626. const childs = Object.values ? Object.values(element.children) : Object.keys(element.children).map((key) => element.children[key]);
  627. for (const child of childs) {
  628. await this.drawNode(child)
  629. }
  630. }
  631. }