123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- /* eslint-disable */
- const PROPERTIES = ['hover-class', 'hover-start-time', 'space', 'src']
- const COMPUTED_STYLE = [
- 'color',
- 'font-size',
- 'font-weight',
- 'font-family',
- 'backgroundColor',
- 'border',
- 'border-radius',
- 'box-sizing',
- 'line-height',
- ]
- const DEFAULT_BORDER = '0px none rgb(0, 0, 0)'
- const DEFAULT_BORDER_RADIUS = '0px'
- // default z-index??
- const DEFAULT_RANK = {
- view: 0,
- image: 1,
- text: 2,
- }
- const drawWrapper = (context, data) => {
- const { backgroundColor, width, height } = data
- context.setFillStyle(backgroundColor)
- context.fillRect(0, 0, width, height)
- }
- // todo: do more for different language
- const strLen = str => {
- let count = 0
- for (let i = 0, len = str.length; i < len; i++) {
- count += str.charCodeAt(i) < 256 ? 1 : 2
- }
- return count / 2
- }
- const isMuitlpleLine = (data, text) => {
- const { 'font-size': letterWidth, width } = data
- const length = strLen(text)
- const rowlineLength = length * parseInt(letterWidth, 10)
- return rowlineLength > width
- }
- const drawMutipleLine = (context, data, text) => {
- const {
- 'font-size': letterWidth,
- width,
- left,
- top,
- 'line-height': lineHeightAttr,
- } = data
- const lineHieght = lineHeightAttr === 'normal' ? Math.round(1.2 * letterWidth) : lineHeightAttr
- const rowLetterCount = Math.floor(width / parseInt(letterWidth, 10))
- const length = strLen(text)
- for (let i = 0; i < length; i += rowLetterCount) {
- const lineText = text.substring(i, i + rowLetterCount)
- const rowNumber = Math.floor(i / rowLetterCount)
- const rowTop = top + rowNumber * parseInt(lineHieght, 10)
- context.fillText(lineText, left, rowTop)
- }
- }
- // enable color, font, for now only support chinese
- const drawText = (context, data) => {
- const {
- dataset: { text },
- left,
- top,
- color,
- 'font-weight': fontWeight,
- 'font-size': fontSize,
- 'font-family': fontFamily,
- } = data
- const canvasText = Array.isArray(text) ? text[0] : text
- context.font = `${fontWeight} ${Math.round(
- parseFloat(fontSize),
- )}px ${fontFamily}`
- context.setFillStyle(color)
- if (isMuitlpleLine(data, canvasText)) {
- drawMutipleLine(context, data, canvasText)
- } else {
- context.fillText(canvasText, left, top)
- }
- context.restore()
- }
- const getImgInfo = src =>
- new Promise((resolve, reject) => {
- wx.getImageInfo({
- src,
- success(res) {
- resolve(res)
- },
- })
- })
- const hasBorder = border => border !== DEFAULT_BORDER
- const hasBorderRadius = borderRadius => borderRadius !== DEFAULT_BORDER_RADIUS
- const getBorderAttributes = border => {
- let borderColor, borderStyle
- let borderWidth = 0
- if (hasBorder) {
- borderWidth = parseInt(border.split(/\s/)[0], 10)
- borderStyle = border.split(/\s/)[1]
- borderColor = border.match(/(rgb).*/gi)[0]
- }
- return {
- borderWidth,
- borderStyle,
- borderColor,
- }
- }
- const getImgRect = (imgData, borderWidth) => {
- const { width, height, left, top } = imgData
- const imgWidth = width - 2 * borderWidth
- const imgHeight = height - 2 * borderWidth
- const imgLeft = left + borderWidth
- const imgTop = top + borderWidth
- return {
- imgWidth,
- imgHeight,
- imgLeft,
- imgTop,
- }
- }
- const getArcCenterPosition = imgData => {
- const { width, height, left, top } = imgData
- const coordX = width / 2 + left
- const coordY = height / 2 + top
- return {
- coordX,
- coordY,
- }
- }
- const getArcRadius = (imgData, borderWidth = 0) => {
- const { width } = imgData
- return width / 2 - borderWidth / 2
- }
- const getCalculatedImagePosition = (imgData, naturalWidth, naturalHeight) => {
- const { border } = imgData
- const { borderWidth } = getBorderAttributes(border)
- const { imgWidth, imgHeight, imgLeft, imgTop } = getImgRect(
- imgData,
- borderWidth,
- )
- const ratio = naturalWidth / naturalHeight
- // tweak for real width and position => center center
- const realWidth = ratio > 0 ? imgWidth : imgHeight * ratio
- const realHeight = ratio > 0 ? imgWidth * (1 / ratio) : imgHeight
- const offsetLeft = ratio > 0 ? 0 : (imgWidth - realWidth) / 2
- const offsetTop = ratio > 0 ? (imgHeight - realHeight) / 2 : 0
- return {
- realWidth,
- realHeight,
- left: imgLeft + offsetLeft,
- top: imgTop + offsetTop,
- }
- }
- const drawArcImage = (context, imgData) => {
- const { src } = imgData
- const { coordX, coordY } = getArcCenterPosition(imgData)
- return getImgInfo(src).then(res => {
- const { width: naturalWidth, height: naturalHeight } = res
- const arcRadius = getArcRadius(imgData)
- context.save()
- context.beginPath()
- context.arc(coordX, coordY, arcRadius, 0, 2 * Math.PI)
- context.closePath()
- context.clip()
- const { left, top, realWidth, realHeight } = getCalculatedImagePosition(
- imgData,
- naturalWidth,
- naturalHeight,
- )
- context.drawImage(
- src,
- 0,
- 0,
- naturalWidth,
- naturalHeight,
- left,
- top,
- realWidth,
- realHeight,
- )
- context.restore()
- })
- }
- const drawRectImage = (context, imgData) => {
- const { src, width, height, left, top } = imgData
- return getImgInfo(src).then(res => {
- const { width: naturalWidth, height: naturalHeight } = res
- context.save()
- context.beginPath()
- context.rect(left, top, width, height)
- context.closePath()
- context.clip()
- const {
- left: realLeft,
- top: realTop,
- realWidth,
- realHeight,
- } = getCalculatedImagePosition(imgData, naturalWidth, naturalHeight)
- context.drawImage(
- src,
- 0,
- 0,
- naturalWidth,
- naturalHeight,
- realLeft,
- realTop,
- realWidth,
- realHeight,
- )
- context.restore()
- })
- }
- const drawArcBorder = (context, imgData) => {
- const { border } = imgData
- const { coordX, coordY } = getArcCenterPosition(imgData)
- const { borderWidth, borderColor } = getBorderAttributes(border)
- const arcRadius = getArcRadius(imgData, borderWidth)
- context.save()
- context.beginPath()
- context.setLineWidth(borderWidth)
- context.setStrokeStyle(borderColor)
- context.arc(coordX, coordY, arcRadius, 0, 2 * Math.PI)
- context.stroke()
- context.restore()
- }
- const drawRectBorder = (context, imgData) => {
- const { border } = imgData
- const { left, top, width, height } = imgData
- const { borderWidth, borderColor } = getBorderAttributes(border)
- const correctedBorderWidth = borderWidth + 1 // draw may cause empty 0.5 space
- context.save()
- context.beginPath()
- context.setLineWidth(correctedBorderWidth)
- context.setStrokeStyle(borderColor)
- context.rect(
- left + borderWidth / 2,
- top + borderWidth / 2,
- width - borderWidth,
- height - borderWidth,
- )
- context.stroke()
- context.restore()
- }
- // image, enable border-radius: 50%, border, bgColor
- const drawImage = (context, imgData) => {
- const { border, 'border-radius': borderRadius } = imgData
- let drawImagePromise
- if (hasBorderRadius(borderRadius)) {
- drawImagePromise = drawArcImage(context, imgData)
- } else {
- drawImagePromise = drawRectImage(context, imgData)
- }
- return drawImagePromise.then(() => {
- if (hasBorder(border)) {
- if (hasBorderRadius(borderRadius)) {
- return drawArcBorder(context, imgData)
- } else {
- return drawRectBorder(context, imgData)
- }
- }
- return Promise.resolve()
- })
- }
- // e.g. 10%, 4px
- const getBorderRadius = imgData => {
- const { width, height, 'border-radius': borderRadiusAttr } = imgData
- const borderRadius = parseInt(borderRadiusAttr, 10)
- if (borderRadiusAttr.indexOf('%') !== -1) {
- const borderRadiusX = parseInt(borderRadius / 100 * width, 10)
- const borderRadiusY = parseInt(borderRadius / 100 * height, 10)
- return {
- isCircle: borderRadiusX === borderRadiusY,
- borderRadius: borderRadiusX,
- borderRadiusX,
- borderRadiusY,
- }
- } else {
- return {
- isCircle: true,
- borderRadius,
- }
- }
- }
- const drawViewArcBorder = (context, imgData) => {
- const { width, height, left, top, backgroundColor, border } = imgData
- const { borderRadius } = getBorderRadius(imgData)
- const { borderWidth, borderColor } = getBorderAttributes(border)
- // console.log('🐞-imgData', imgData)
- context.beginPath()
- context.moveTo(left + borderRadius, top)
- context.lineTo(left + width - borderRadius, top)
- context.arcTo(
- left + width,
- top,
- left + width,
- top + borderRadius,
- borderRadius,
- )
- context.lineTo(left + width, top + height - borderRadius)
- context.arcTo(
- left + width,
- top + height,
- left + width - borderRadius,
- top + height,
- borderRadius,
- )
- context.lineTo(left + borderRadius, top + height)
- context.arcTo(
- left,
- top + height,
- left,
- top + height - borderRadius,
- borderRadius,
- )
- context.lineTo(left, top + borderRadius)
- context.arcTo(left, top, left + borderRadius, top, borderRadius)
- context.closePath()
- if (backgroundColor) {
- context.setFillStyle(backgroundColor)
- context.fill()
- }
- if (borderColor && borderWidth) {
- context.setLineWidth(borderWidth)
- context.setStrokeStyle(borderColor)
- context.stroke()
- }
- }
- const drawViewBezierBorder = (context, imgData) => {
- const { width, height, left, top, backgroundColor, border } = imgData
- const { borderWidth, borderColor } = getBorderAttributes(border)
- const { borderRadiusX, borderRadiusY } = getBorderRadius(imgData)
- context.beginPath()
- context.moveTo(left + borderRadiusX, top)
- context.lineTo(left + width - borderRadiusX, top)
- context.quadraticCurveTo(left + width, top, left + width, top + borderRadiusY)
- context.lineTo(left + width, top + height - borderRadiusY)
- context.quadraticCurveTo(
- left + width,
- top + height,
- left + width - borderRadiusX,
- top + height,
- )
- context.lineTo(left + borderRadiusX, top + height)
- context.quadraticCurveTo(
- left,
- top + height,
- left,
- top + height - borderRadiusY,
- )
- context.lineTo(left, top + borderRadiusY)
- context.quadraticCurveTo(left, top, left + borderRadiusX, top)
- context.closePath()
- if (backgroundColor) {
- context.setFillStyle(backgroundColor)
- context.fill()
- }
- if (borderColor && borderWidth) {
- context.setLineWidth(borderWidth)
- context.setStrokeStyle(borderColor)
- context.stroke()
- }
- }
- // enable border, border-radius, bgColor, position
- const drawView = (context, imgData) => {
- const { isCircle } = getBorderRadius(imgData)
- if (isCircle) {
- drawViewArcBorder(context, imgData)
- } else {
- drawViewBezierBorder(context, imgData)
- }
- }
- const isTextElement = item => {
- const { dataset: { text }, type } = item
- return Boolean(text) || type === 'text'
- }
- const isImageElement = item => {
- const { src, type } = item
- return Boolean(src) || type === 'image'
- }
- const isViewElement = item => {
- const { type } = item
- return type === 'view'
- }
- const formatElementData = elements =>
- elements.map(element => {
- if (isTextElement(element)) {
- element.type = 'text'
- element.rank = DEFAULT_RANK.text
- } else if (isImageElement(element)) {
- element.type = 'image'
- element.rank = DEFAULT_RANK.image
- } else {
- element.type = 'view'
- element.rank = DEFAULT_RANK.view
- }
- return element
- })
- // todo: use z-index as order to draw??
- const getSortedElementsData = elements =>
- elements.sort((a, b) => {
- if (a.rank < b.rank) {
- return -1
- } else if (a.rank > b.rank) {
- return 1
- }
- return 0
- })
- const drawElements = (context, storeItems) => {
- const itemPromise = []
- storeItems.forEach(item => {
- if (isTextElement(item)) {
- const text = drawText(context, item)
- itemPromise.push(text)
- } else if (isImageElement(item)) {
- const image = drawImage(context, item)
- itemPromise.push(image)
- } else {
- const view = drawView(context, item)
- itemPromise.push(view)
- }
- })
- return itemPromise
- }
- // storeObject: { 0: [...], 1: [...] }
- // chain call promise based on Object key
- const drawElementBaseOnIndex = (context, storeObject, key = 0, drawPromise) => {
- if (typeof drawPromise === 'undefined') {
- drawPromise = Promise.resolve()
- }
- const objectKey = key // note: key is changing when execute promise then
- const chainPromise = drawPromise.then(() => {
- const nextPromise = storeObject[objectKey]
- ? Promise.all(drawElements(context, storeObject[objectKey]))
- : Promise.resolve()
- return nextPromise
- })
- if (key >= Object.keys(storeObject).length) {
- return chainPromise
- } else {
- return drawElementBaseOnIndex(context, storeObject, key + 1, chainPromise)
- }
- }
- const drawCanvas = (canvasId, wrapperData, innerData) => {
- const context = wx.createCanvasContext(canvasId)
- context.setTextBaseline('top')
- // todo: use this after weixin fix stupid clip can't work bug in fillRect
- // for now, just set canvas background as a compromise
- drawWrapper(context, wrapperData[0])
- const storeObject = {}
- const sortedElementData = getSortedElementsData(formatElementData(innerData)) // fake z-index
- sortedElementData.forEach(item => {
- if (!storeObject[item.rank]) {
- // initialize
- storeObject[item.rank] = []
- }
- if (isTextElement(item) || isImageElement(item) || isViewElement(item)) {
- storeObject[item.rank].push(item)
- }
- })
- // note: draw is async
- return drawElementBaseOnIndex(context, storeObject).then(
- () =>
- new Promise((resolve, reject) => {
- context.draw(true, () => {
- resolve()
- })
- }),
- )
- }
- const wxSelectorQuery = element =>
- new Promise((resolve, reject) => {
- try {
- wx
- .createSelectorQuery()
- .selectAll(element)
- .fields(
- {
- dataset: true,
- size: true,
- rect: true,
- properties: PROPERTIES,
- computedStyle: COMPUTED_STYLE,
- },
- res => {
- resolve(res)
- },
- )
- .exec()
- } catch (error) {
- reject(error)
- }
- })
- const wxml2canvas = (wrapperId, elementsClass, canvasId) => {
- const getWrapperElement = wxSelectorQuery(wrapperId)
- const getInnerElements = wxSelectorQuery(elementsClass)
- return Promise.all([getWrapperElement, getInnerElements]).then(data => {
- return drawCanvas(canvasId, data[0], data[1])
- })
- }
- // export default wxml2canvas
- module.exports = wxml2canvas
|