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 @@

Changelog

0.3.1

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

Upgrade to plugin architecture, pull out some non-core functions as plugins, register as needed, and reduce the overall volume.

0.2.24

diff --git a/web/src/pages/Doc/en/utils/index.md b/web/src/pages/Doc/en/utils/index.md index 0e4eb955..da9cb28a 100644 --- a/web/src/pages/Doc/en/utils/index.md +++ b/web/src/pages/Doc/en/utils/index.md @@ -1,14 +1,16 @@ # Utility Methods +## Base utility Methods + Reference: ```js import {walk, ...} from 'simple-mind-map/src/utils' ``` -## Methods +### Methods -### walk(root, parent, beforeCallback, afterCallback, isRoot, layerIndex = 0, index = 0) +#### walk(root, parent, beforeCallback, afterCallback, isRoot, layerIndex = 0, index = 0) Depth-first traversal of a tree @@ -34,11 +36,11 @@ Example: walk(tree, null, () => {}, () => {}, false, 0, 0); ``` -### bfsWalk(root, callback) +#### bfsWalk(root, callback) Breadth-first traversal of a tree -### resizeImgSize(width, height, maxWidth, maxHeight) +#### resizeImgSize(width, height, maxWidth, maxHeight) Resize image size @@ -52,17 +54,17 @@ Resize image size `maxWidth` and `maxHeight` can both be passed, or only one of them can be passed -### resizeImg(imgUrl, maxWidth, maxHeight) +#### resizeImg(imgUrl, maxWidth, maxHeight) Resize image, internally loads the image first, then calls the `resizeImgSize` method, and returns a `promise` -### simpleDeepClone(data) +#### simpleDeepClone(data) Extremely simple deep copy method, can only be used for objects that are all basic data, otherwise it will throw an error -### copyRenderTree(tree, root) +#### copyRenderTree(tree, root) Copy render tree data, example: @@ -70,7 +72,7 @@ Copy render tree data, example: copyRenderTree({}, this.mindMap.renderer.renderTree); ``` -### copyNodeTree(tree, root) +#### copyNodeTree(tree, root) Copy node tree data, mainly eliminating the reference `node` instance `_node` and copying the `data` of the data object, example: @@ -79,30 +81,60 @@ and copying the `data` of the data object, example: copyNodeTree({}, node); ``` -### imgToDataUrl(src) +#### imgToDataUrl(src) Convert image to dataURL -### downloadFile(file, fileName) +#### downloadFile(file, fileName) Download file -### throttle(fn, time = 300, ctx) +#### throttle(fn, time = 300, ctx) Throttle function -### asyncRun(taskList, callback = () => {}) +#### asyncRun(taskList, callback = () => {}) Run tasks in task list asynchronously, tasks are run synchronously without order -### degToRad(deg) +#### degToRad(deg) > v0.2.24+ Angle to radian -### camelCaseToHyphen(str) +#### camelCaseToHyphen(str) > v0.2.24+ -CamelCase to hyphen \ No newline at end of file +CamelCase to hyphen + +## Simulate CSS background in Canvas + +Import: + +```js +import drawBackgroundImageToCanvas from 'simple-mind-map/src/utils/simulateCSSBackgroundInCanvas' +``` + +Usage: + +```js +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 + } +}) +``` \ No newline at end of file diff --git a/web/src/pages/Doc/en/utils/index.vue b/web/src/pages/Doc/en/utils/index.vue index 28a28315..5715a903 100644 --- a/web/src/pages/Doc/en/utils/index.vue +++ b/web/src/pages/Doc/en/utils/index.vue @@ -1,11 +1,12 @@ diff --git a/web/src/pages/Doc/zh/changelog/index.md b/web/src/pages/Doc/zh/changelog/index.md index aae6eb8b..f2fab071 100644 --- a/web/src/pages/Doc/zh/changelog/index.md +++ b/web/src/pages/Doc/zh/changelog/index.md @@ -4,7 +4,7 @@ 修复:1.删除背景图片不生效的问题;2.节点拖拽到根节点时连接线跑到根节点上方的问题。 -新增:背景图片展示增加位置和大小设置。 +新增:背景图片展示增加位置和大小设置。导出的图片也同步支持该设置。 ## 0.3.0 diff --git a/web/src/pages/Doc/zh/changelog/index.vue b/web/src/pages/Doc/zh/changelog/index.vue index bf05a0a7..972ad46b 100644 --- a/web/src/pages/Doc/zh/changelog/index.vue +++ b/web/src/pages/Doc/zh/changelog/index.vue @@ -3,7 +3,7 @@

Changelog

0.3.1

修复:1.删除背景图片不生效的问题;2.节点拖拽到根节点时连接线跑到根节点上方的问题。

-

新增:背景图片展示增加位置和大小设置。

+

新增:背景图片展示增加位置和大小设置。导出的图片也同步支持该设置。

0.3.0

升级为插件化架构,将一些非核心功能抽离出来作为插件,按需注册,减小整体体积。

0.2.24

diff --git a/web/src/pages/Doc/zh/utils/index.md b/web/src/pages/Doc/zh/utils/index.md index a3cd621a..bdb8a731 100644 --- a/web/src/pages/Doc/zh/utils/index.md +++ b/web/src/pages/Doc/zh/utils/index.md @@ -1,14 +1,16 @@ # 内置工具方法 +## 基础工具方法 + 引用: ```js import {walk, ...} from 'simple-mind-map/src/utils' ``` -## 方法 +### 方法 -### walk(root, parent, beforeCallback, afterCallback, isRoot, layerIndex = 0, index = 0) +#### walk(root, parent, beforeCallback, afterCallback, isRoot, layerIndex = 0, index = 0) 深度优先遍历树 @@ -32,11 +34,11 @@ import {walk, ...} from 'simple-mind-map/src/utils' walk(tree, null, () => {}, () => {}, false, 0, 0) ``` -### bfsWalk(root, callback) +#### bfsWalk(root, callback) 广度优先遍历树 -### resizeImgSize(width, height, maxWidth, maxHeight) +#### resizeImgSize(width, height, maxWidth, maxHeight) 缩放图片的尺寸 @@ -50,15 +52,15 @@ walk(tree, null, () => {}, () => {}, false, 0, 0) `maxWidth`和`maxHeight`可以同时都传,也可以只传一个 -### resizeImg(imgUrl, maxWidth, maxHeight) +#### resizeImg(imgUrl, maxWidth, maxHeight) 缩放图片,内部先加载图片,然后调用`resizeImgSize`方法,返回一个`promise` -### simpleDeepClone(data) +#### simpleDeepClone(data) 极简的深拷贝方法,只能针对全是基本数据的对象,否则会报错 -### copyRenderTree(tree, root) +#### copyRenderTree(tree, root) 复制渲染树数据,示例: @@ -66,7 +68,7 @@ walk(tree, null, () => {}, () => {}, false, 0, 0) copyRenderTree({}, this.mindMap.renderer.renderTree) ``` -### copyNodeTree(tree, root) +#### copyNodeTree(tree, root) 复制节点树数据,主要是剔除其中的引用`node`实例的`_node`,然后复制`data`对象的数据,示例: @@ -74,30 +76,60 @@ copyRenderTree({}, this.mindMap.renderer.renderTree) copyNodeTree({}, node) ``` -### imgToDataUrl(src) +#### imgToDataUrl(src) 图片转成dataURL -### downloadFile(file, fileName) +#### downloadFile(file, fileName) 下载文件 -### throttle(fn, time = 300, ctx) +#### throttle(fn, time = 300, ctx) 节流函数 -### asyncRun(taskList, callback = () => {}) +#### asyncRun(taskList, callback = () => {}) 异步执行任务队列,多个任务是同步执行的,没有先后顺序 -### degToRad(deg) +#### degToRad(deg) > v0.2.24+ 角度转弧度 -### camelCaseToHyphen(str) +#### camelCaseToHyphen(str) > v0.2.24+ -驼峰转连字符 \ No newline at end of file +驼峰转连字符 + +## 在canvas中模拟css的背景属性 + +引入: + +```js +import drawBackgroundImageToCanvas from 'simple-mind-map/src/utils/simulateCSSBackgroundInCanvas' +``` + +使用: + +```js +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 { + // 成功 + } +}) +``` diff --git a/web/src/pages/Doc/zh/utils/index.vue b/web/src/pages/Doc/zh/utils/index.vue index d2fecc51..6fa5825f 100644 --- a/web/src/pages/Doc/zh/utils/index.vue +++ b/web/src/pages/Doc/zh/utils/index.vue @@ -1,11 +1,12 @@