diff --git a/simple-mind-map/src/Export.js b/simple-mind-map/src/Export.js index df7be3e3..765dcd8e 100644 --- a/simple-mind-map/src/Export.js +++ b/simple-mind-map/src/Export.js @@ -1,6 +1,7 @@ import { imgToDataUrl, downloadFile } from './utils' import JsPDF from 'jspdf' import { SVG } from '@svgdotjs/svg.js' +import drawBackgroundImageToCanvas from './utils/simulateCSSBackgroundInCanvas' const URL = window.URL || window.webkitURL || window // 导出类 @@ -85,7 +86,9 @@ class Export { let { backgroundColor = '#fff', backgroundImage, - backgroundRepeat = 'repeat' + backgroundRepeat = 'no-repeat', + backgroundPosition = 'center center', + backgroundSize = 'cover', } = this.mindMap.themeConfig // 背景颜色 ctx.save() @@ -96,19 +99,18 @@ class Export { // 背景图片 if (backgroundImage && backgroundImage !== 'none') { ctx.save() - let img = new Image() - img.src = backgroundImage - img.onload = () => { - let pat = ctx.createPattern(img, backgroundRepeat) - ctx.rect(0, 0, width, height) - ctx.fillStyle = pat - ctx.fill() + drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, { + backgroundRepeat, + backgroundPosition, + backgroundSize + }, (err) => { + if (err) { + reject(err) + } else { + resolve() + } ctx.restore() - resolve() - } - img.onerror = e => { - reject(e) - } + }) } else { resolve() } diff --git a/simple-mind-map/src/utils/simulateCSSBackgroundInCanvas.js b/simple-mind-map/src/utils/simulateCSSBackgroundInCanvas.js new file mode 100644 index 00000000..5f33b3bb --- /dev/null +++ b/simple-mind-map/src/utils/simulateCSSBackgroundInCanvas.js @@ -0,0 +1,354 @@ +// 将以空格分隔的字符串值转换成成数字/单位/值数组 +const getNumberValueFromStr = value => { + let arr = String(value).split(/\s+/) + return arr.map(item => { + if (/^[\d.]+/.test(item)) { + // 数字+单位 + let res = /^([\d.]+)(.*)$/.exec(item) + return [Number(res[1]), res[2]] + } else { + // 单个值 + return item + } + }) +} + +// 缩放宽度 +const zoomWidth = (ratio, height) => { + // w / height = ratio + return ratio * height +} + +// 缩放高度 +const zoomHeight = (ratio, width) => { + // width / h = ratio + return width / ratio +} + +// 关键词到百分比值的映射 +const keyWordToPercentageMap = { + left: 0, + top: 0, + center: 50, + bottom: 100, + right: 100 +} + +// 模拟background-size +const handleBackgroundSize = ({ + backgroundSize, + drawOpt, + imageRatio, + canvasWidth, + canvasHeight, + canvasRatio +}) => { + if (backgroundSize) { + // 将值转换成数组 + let backgroundSizeValueArr = getNumberValueFromStr(backgroundSize) + // 两个值都为auto,那就相当于不设置 + if ( + backgroundSizeValueArr[0] === 'auto' && + backgroundSizeValueArr[1] === 'auto' + ) { + return + } + // 值为cover + if (backgroundSizeValueArr[0] === 'cover') { + if (imageRatio > canvasRatio) { + // 图片的宽高比大于canvas的宽高比,那么图片高度缩放到和canvas的高度一致,宽度自适应 + drawOpt.height = canvasHeight + drawOpt.width = zoomWidth(imageRatio, canvasHeight) + } else { + // 否则图片宽度缩放到和canvas的宽度一致,高度自适应 + drawOpt.width = canvasWidth + drawOpt.height = zoomHeight(imageRatio, canvasWidth) + } + return + } + // 值为contain + if (backgroundSizeValueArr[0] === 'contain') { + if (imageRatio > canvasRatio) { + // 图片的宽高比大于canvas的宽高比,那么图片宽度缩放到和canvas的宽度一致,高度自适应 + drawOpt.width = canvasWidth + drawOpt.height = zoomHeight(imageRatio, canvasWidth) + } else { + // 否则图片高度缩放到和canvas的高度一致,宽度自适应 + drawOpt.height = canvasHeight + drawOpt.width = zoomWidth(imageRatio, canvasHeight) + } + return + } + // 图片宽度 + let newNumberWidth = -1 + if (backgroundSizeValueArr[0]) { + if (Array.isArray(backgroundSizeValueArr[0])) { + // 数字+单位类型 + if (backgroundSizeValueArr[0][1] === '%') { + // %单位 + drawOpt.width = (backgroundSizeValueArr[0][0] / 100) * canvasWidth + newNumberWidth = drawOpt.width + } else { + // 其他都认为是px单位 + drawOpt.width = backgroundSizeValueArr[0][0] + newNumberWidth = backgroundSizeValueArr[0][0] + } + } else if (backgroundSizeValueArr[0] === 'auto') { + // auto类型,那么根据设置的新高度以图片原宽高比进行自适应 + if (backgroundSizeValueArr[1]) { + if (backgroundSizeValueArr[1][1] === '%') { + // 高度为%单位 + drawOpt.width = zoomWidth( + imageRatio, + (backgroundSizeValueArr[1][0] / 100) * canvasHeight + ) + } else { + // 其他都认为是px单位 + drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0]) + } + } + } + } + // 设置了图片高度 + if (backgroundSizeValueArr[1] && Array.isArray(backgroundSizeValueArr[1])) { + // 数字+单位类型 + if (backgroundSizeValueArr[1][1] === '%') { + // 高度为%单位 + drawOpt.height = (backgroundSizeValueArr[1][0] / 100) * canvasHeight + } else { + // 其他都认为是px单位 + drawOpt.height = backgroundSizeValueArr[1][0] + } + } else if (newNumberWidth !== -1) { + // 没有设置图片高度或者设置为auto,那么根据设置的新宽度以图片原宽高比进行自适应 + drawOpt.height = zoomHeight(imageRatio, newNumberWidth) + } + } +} + +// 模拟background-position +const handleBackgroundPosition = ({ + backgroundPosition, + drawOpt, + imgWidth, + imgHeight, + canvasWidth, + canvasHeight +}) => { + if (backgroundPosition) { + // 将值转换成数组 + let backgroundPositionValueArr = getNumberValueFromStr(backgroundPosition) + // 将关键词转为百分比 + backgroundPositionValueArr = backgroundPositionValueArr.map(item => { + if (typeof item === 'string') { + return keyWordToPercentageMap[item] !== undefined + ? [keyWordToPercentageMap[item], '%'] + : item + } + return item + }) + if (Array.isArray(backgroundPositionValueArr[0])) { + if (backgroundPositionValueArr.length === 1) { + // 如果只设置了一个值,第二个默认为50% + backgroundPositionValueArr.push([50, '%']) + } + // 水平位置 + if (backgroundPositionValueArr[0][1] === '%') { + // 单位为% + let canvasX = (backgroundPositionValueArr[0][0] / 100) * canvasWidth + let imgX = (backgroundPositionValueArr[0][0] / 100) * imgWidth + // 计算差值 + drawOpt.x = canvasX - imgX + } else { + // 其他单位默认都为px + drawOpt.x = backgroundPositionValueArr[0][0] + } + // 垂直位置 + if (backgroundPositionValueArr[1][1] === '%') { + // 单位为% + let canvasY = (backgroundPositionValueArr[1][0] / 100) * canvasHeight + let imgY = (backgroundPositionValueArr[1][0] / 100) * imgHeight + // 计算差值 + drawOpt.y = canvasY - imgY + } else { + // 其他单位默认都为px + drawOpt.y = backgroundPositionValueArr[1][0] + } + } + } +} + +// 模拟background-repeat +const handleBackgroundRepeat = ({ + ctx, + image, + backgroundRepeat, + drawOpt, + imgWidth, + imgHeight, + canvasWidth, + canvasHeight +}) => { + if (backgroundRepeat) { + // 保存在handleBackgroundPosition中计算出来的x、y + let ox = drawOpt.x + let oy = drawOpt.y + // 计算ox和oy能平铺的图片数量 + let oxRepeatNum = Math.ceil(ox / imgWidth) + let oyRepeatNum = Math.ceil(oy / imgHeight) + // 计算ox和oy第一张图片的位置 + let oxRepeatX = ox - oxRepeatNum * imgWidth + let oxRepeatY = oy - oyRepeatNum * imgHeight + // 将值转换成数组 + let backgroundRepeatValueArr = getNumberValueFromStr(backgroundRepeat) + // 不处理 + if ( + backgroundRepeatValueArr[0] === 'no-repeat' || + (imgWidth >= canvasWidth && imgHeight >= canvasHeight) + ) { + return + } + // 水平平铺 + if (backgroundRepeatValueArr[0] === 'repeat-x') { + if (canvasWidth > imgWidth) { + let x = oxRepeatX + while (x < canvasWidth) { + drawImage(ctx, image, { + ...drawOpt, + x + }) + x += imgWidth + } + return true + } + } + // 垂直平铺 + if (backgroundRepeatValueArr[0] === 'repeat-y') { + if (canvasHeight > imgHeight) { + let y = oxRepeatY + while (y < canvasHeight) { + drawImage(ctx, image, { + ...drawOpt, + y + }) + y += imgHeight + } + return true + } + } + // 平铺 + if (backgroundRepeatValueArr[0] === 'repeat') { + let x = oxRepeatX + while (x < canvasWidth) { + if (canvasHeight > imgHeight) { + let y = oxRepeatY + while (y < canvasHeight) { + drawImage(ctx, image, { + ...drawOpt, + x, + y + }) + y += imgHeight + } + } + x += imgWidth + } + return true + } + } +} + +// 根据参数绘制图片 +const drawImage = (ctx, image, drawOpt) => { + ctx.drawImage( + image, + drawOpt.sx, + drawOpt.sy, + drawOpt.swidth, + drawOpt.sheight, + drawOpt.x, + drawOpt.y, + drawOpt.width, + drawOpt.height + ) +} + +const drawBackgroundImageToCanvas = ( + ctx, + width, + height, + img, + { backgroundSize, backgroundPosition, backgroundRepeat }, + callback = () => {} +) => { + // 画布的长宽比 + let canvasRatio = width / height + // 加载图片 + let image = new Image() + image.src = img + image.onload = () => { + // 图片的宽度及长宽比 + let imgWidth = image.width + let imgHeight = image.height + let imageRatio = imgWidth / imgHeight + // 绘制图片 + // drawImage方法的参数值 + let drawOpt = { + sx: 0, + sy: 0, + swidth: imgWidth, + sheight: imgHeight, + x: 0, + y: 0, + width: imgWidth, + height: imgHeight + } + // 模拟background-size + handleBackgroundSize({ + backgroundSize, + drawOpt, + imageRatio, + canvasWidth: width, + canvasHeight: height, + canvasRatio + }) + + // 模拟background-position + handleBackgroundPosition({ + backgroundPosition, + drawOpt, + imgWidth: drawOpt.width, + imgHeight: drawOpt.height, + imageRatio, + canvasWidth: width, + canvasHeight: height, + canvasRatio + }) + + // 模拟background-repeat + let notNeedDraw = handleBackgroundRepeat({ + ctx, + image, + backgroundRepeat, + drawOpt, + imgWidth: drawOpt.width, + imgHeight: drawOpt.height, + imageRatio, + canvasWidth: width, + canvasHeight: height, + canvasRatio + }) + + // 绘制图片 + if (!notNeedDraw) { + drawImage(ctx, image, drawOpt) + } + + callback() + } + image.onerror = e => { + callback(e) + } +} + +export default drawBackgroundImageToCanvas \ No newline at end of file diff --git a/web/src/pages/Doc/en/changelog/index.md b/web/src/pages/Doc/en/changelog/index.md index daad2b20..472d6ddf 100644 --- a/web/src/pages/Doc/en/changelog/index.md +++ b/web/src/pages/Doc/en/changelog/index.md @@ -4,7 +4,7 @@ Fix: 1.The problem that deleting the background image does not take effect; 2.The problem that the connector runs above the root node when the node is dragged to the root node. -New: Add position and size settings for background image display. +New: Add position and size settings for background image display. This setting is also supported for exported pictures. ## 0.3.0 diff --git a/web/src/pages/Doc/en/changelog/index.vue b/web/src/pages/Doc/en/changelog/index.vue index a2a7cf30..c781161e 100644 --- a/web/src/pages/Doc/en/changelog/index.vue +++ b/web/src/pages/Doc/en/changelog/index.vue @@ -3,7 +3,7 @@
Fix: 1.The problem that deleting the background image does not take effect; 2.The problem that the connector runs above the root node when the node is dragged to the root node.
-New: Add position and size settings for background image display.
+New: Add position and size settings for background image display. This setting is also supported for exported pictures.
Upgrade to plugin architecture, pull out some non-core functions as plugins, register as needed, and reduce the overall volume.
Reference:
import {walk, ...} from 'simple-mind-map/src/utils'
-Depth-first traversal of a tree
root: the root node of the tree to be traversed
parent: parent node
Example:
walk(tree, null, () => {}, () => {}, false, 0, 0);
-Breadth-first traversal of a tree
-Resize image size
width: original width of the image
height: original height of the image
maxWidth: the width to resize to
maxHeight: the height to resize to
maxWidth and maxHeight can both be passed, or only one of them can be passed
Resize image, internally loads the image first, then calls the resizeImgSize
method, and returns a promise
Extremely simple deep copy method, can only be used for objects that are all basic data, otherwise it will throw an error
-Copy render tree data, example:
copyRenderTree({}, this.mindMap.renderer.renderTree);
-Copy node tree data, mainly eliminating the reference node instance _node
and copying the data of the data object, example:
copyNodeTree({}, node);
-Convert image to dataURL
-Download file
-Throttle function
-Run tasks in task list asynchronously, tasks are run synchronously without order
-v0.2.24+
Angle to radian
-v0.2.24+
CamelCase to hyphen
+Import:
+import drawBackgroundImageToCanvas from 'simple-mind-map/src/utils/simulateCSSBackgroundInCanvas'
+
+Usage:
+let width = 500
+let height = 500
+let img = '/1.jpg'
+let canvas = document.createElement('canvas')
+canvas.width = width
+canvas.height = height
+drawBackgroundImageToCanvas(ctx, width, height, img, {
+ backgroundRepeat: 'repeat-y',
+ backgroundSize: '60%',
+ backgroundPosition: 'center center'
+}, (err) => {
+ if (err) {
+ // fail
+ } else {
+ // success
+ }
+})
+
修复:1.删除背景图片不生效的问题;2.节点拖拽到根节点时连接线跑到根节点上方的问题。
-新增:背景图片展示增加位置和大小设置。
+新增:背景图片展示增加位置和大小设置。导出的图片也同步支持该设置。
升级为插件化架构,将一些非核心功能抽离出来作为插件,按需注册,减小整体体积。
引用:
import {walk, ...} from 'simple-mind-map/src/utils'
-深度优先遍历树
root:要遍历的树的根节点
parent:父节点
示例:
walk(tree, null, () => {}, () => {}, false, 0, 0)
-广度优先遍历树
-缩放图片的尺寸
width:图片原本的宽
height:图片原本的高
maxWidth:要缩放到的宽
maxHeight:要缩放到的高
maxWidth和maxHeight可以同时都传,也可以只传一个
缩放图片,内部先加载图片,然后调用resizeImgSize方法,返回一个promise
极简的深拷贝方法,只能针对全是基本数据的对象,否则会报错
-复制渲染树数据,示例:
copyRenderTree({}, this.mindMap.renderer.renderTree)
-复制节点树数据,主要是剔除其中的引用node实例的_node,然后复制data对象的数据,示例:
copyNodeTree({}, node)
-图片转成dataURL
-下载文件
-节流函数
-异步执行任务队列,多个任务是同步执行的,没有先后顺序
-v0.2.24+
角度转弧度
-v0.2.24+
驼峰转连字符
+引入:
+import drawBackgroundImageToCanvas from 'simple-mind-map/src/utils/simulateCSSBackgroundInCanvas'
+
+使用:
+let width = 500
+let height = 500
+let img = '/1.jpg'
+let canvas = document.createElement('canvas')
+canvas.width = width
+canvas.height = height
+drawBackgroundImageToCanvas(ctx, width, height, img, {
+ backgroundRepeat: 'repeat-y',
+ backgroundSize: '60%',
+ backgroundPosition: 'center center'
+}, (err) => {
+ if (err) {
+ // 失败
+ } else {
+ // 成功
+ }
+})
+