背景
深交所在 2024 年请技术口全体人员参加名为“程序员的浪漫——新春送祝福”的活动。真不错!我喜欢这个活动,它很好地激发了我创作的热情。
经过策划,我决定用 vue.js 来演示思维导图,以全面宣传深交所。问题来了,如何实现?我搜索了全网,实在找不到一篇教程,教人如何用 vue.js 或 JavaScript 来演示思维导图中的每一个节点。面对这样的困难,我想过要不裁剪出每一个节点来播放吧?这样做起来就简单多了。哦,不!这样做太投机取巧了!毕竟我想夺取实施难度大的巧夺天工奖呢!因此,我决定做全网第一人,公开发表 vue.js 如何演示一张思维导图。
思维导图
下面这张思维导图,告诉我们深交所所有的宣传标语。我需要在活动作品中演示它的每一个节点。
思路
首先,要知道这张图非常大,况且它只能被放在页面中一个指定的组件或容器中。其次,存放和演示思维导图的容器的尺寸是有限的,无法 100% 显示一个节点。因此,演示的时候,思维导图应缩放到特定的尺寸,使得容器恰好只显示一个节点,而其余节点则被隐藏或遮挡了。
我可以利用以下 CSS 特性(关注粗体字):
-
background-repeat
:定义背景图像的重复方式。背景图像可以沿着水平轴,垂直轴,两个轴重复,或者根本不重复。 -
background-size
:设置背景图片大小。图片可以保有其原有的尺寸,或者拉伸到新的尺寸,或者在保持其原有比例的同时缩放到元素的可用空间的尺寸。 -
background-position-x
:设置水平方向的位置。 -
background-position-y
:用于设置初始状态时背景图片在垂直方向上的位置。
我应先获取每个节点的位置和尺寸,所需的数据如下:
属性名 | 属性含义 |
---|---|
x | 横坐标 |
y | 纵坐标 |
w | 宽度 |
h | 高度 |
接下来,用伪代码描述下放置和缩放图片的(核心)思路:
if 节点宽度 <= 容器最大宽度
容器宽度 = 节点宽度
else
容器宽度 = 容器最大宽度
演示缩放比 = 容器宽度 / 节点宽度
容器高度 = 节点高度 * 演示缩放比
if 容器高度 > 容器最大高度
容器宽度 = 容器宽度 * 容器最大高度 / 容器高度
演示缩放比 = 容器宽度 / 节点宽度
容器高度 = 节点高度 * 演示缩放比
background-size = 图片原始宽度 / 节点宽度
background-position-x = 0 - 节点横坐标 * 演示缩放比
background-position-y = 0 - 节点纵坐标 * 演示缩放比
实现
1、底层依赖
我选择的vue.js的版本是 2.7.7
。
2、容器
要知道,我们既可能在 14 寸电脑显示器上演示思维导图,也可能在 12 寸电脑显示器上演示,也可能在大屏电视机上演示,所以,我打算使容器的尺寸适应不同的显示器,小屏就小图显示,大屏则大图显示。
看以下代码:
<template>
<div id="imgContainer" :style="imgContainerStyle"></div>
</template>
<script>
export default {
name: 'MindMapping',
data() {
return {
// 图片的原始尺寸
imgSize: {
w: 4195,
h: 2037
},
imgContainerStyle: {
top: 0,
bottom: 0,
left: 0,
right: 0,
margin: 'auto',
width: '0px',
height: '0px',
backgroundSize: 'cover'
}
}
},
computed: {
// 图片最大的宽度,希望不要被其它元素遮挡
getMaxShowWidth() {
return window.innerWidth - 605;
},
// 图片最大的高度,希望不要越出浏览器
getMaxShowHeight() {
return Math.floor(window.innerHeight * 0.7);
}
},
mounted() {
// 计算思维导图的尺寸,应保持图片的宽高比,否则会变形
let h = Math.ceil(this.getMaxShowWidth * (this.imgSize.h / this.imgSize.w));
this.imgContainerStyle.width = `${this.getMaxShowWidth}px`;
this.imgContainerStyle.height = `${h}px`;
}
}
</script>
<style scoped>
#imgContainer {
display: inline-block;
position: absolute;
background-image: url(images/深交所宣传标语.v3.png);
background-repeat: no-repeat;
}
</style>
一点点解释下:
- 我使容器以绝对布局显示,这样就可以水平和垂直居中显示了。
- 我使用
background-xxx
的方式,这样有助于只显示图片的一部分。 -
getMaxShowWidth
和getMaxShowHeight
是我自己的计算,你可以用其它方式计算。 - 首次加载思维导图时,计算容器最多可以多少像素来显示,然后为了保持宽高比,计算容器的高度,最后使图片铺满整个容器。
效果图:
3、准备每个节点的坐标和尺寸信息
正如思路中所阐述的,我需要准备每个节点的坐标和尺寸信息。ImageGlass软件可以起到作用。
信息如下所示:
// 思维导图的演示路径
showPath: [{// 中心主题
x: 1870, y: 850, w: 837, h: 258
}, {// 价值观
x: 2640, y: 0, w: 800, h: 230
}, {// 使命
x: 2640, y: 178, w: 800, h: 280
}, {// 愿景
x: 2640, y: 395, w: 1000, h: 270
}, {// 深交所“十四五”发展战略规划与“优一”建设纲要的总体原则
x: 2640, y: 623, w: 1440, h: 555
}, {// 深交所“十四五”发展战略规划的主要目标
x: 2640, y: 1164, w: 1440, h: 492
}, {// “优一”建设纲要的主要目标
x: 2640, y: 1662, w: 1558, h: 375
}, {// 十四五IT发展战略目标
x: 0, y: 0, w: 1890, h: 2037
}, {// 战略目标之使命&愿景
x: 326, y: 140, w: 978, h: 586
}, {// 战略目标之原则&策略
x: 244, y: 718, w: 982, h: 660
}, {// 战略目标之战略目标
x: 42, y: 1364, w: 1188, h: 256
}, {// 战略目标之重点任务
x: 432, y: 1600, w: 794, h: 390
}]
我把这些信息放在数组里,这样我就可以使程序依次演示每个节点了。
4、实现伪代码
思路中的伪代码是此篇文章的核心算法,我需要实现它。
getShowProp(x, y, w, h) {
let displayWidth, displayHeight, displayX, displayY, scale;
if (w > this.getMaxShowWidth) {
displayWidth = this.getMaxShowWidth;
} else {
displayWidth = w;
}
scale = Math.round(displayWidth / w * 10000) / 10000;
displayHeight = Math.floor(h * scale);
if (displayHeight > this.getMaxShowHeight) {
displayWidth = parseInt(displayWidth * this.getMaxShowHeight / displayHeight);
scale = Math.round(displayWidth / w * 10000) / 10000;
}
displayHeight = Math.floor(h * scale);
displayX = 0 - Math.floor(x * scale);
displayY = 0 - Math.floor(y * scale);
return {
x: displayX,
y: displayY,
w: displayWidth,
h: displayHeight
};
}
有了这个函数,我就可以用它的返回值来设置容器的宽度、高度和 background-position
了。等等,background-size
值怎么设置?还需要以下实现代码:
getBackgroundSize(w) {
let scale = Math.floor(this.imgSize.w / w * 100);
return `${scale}%`;
}
我只需要把节点的宽度值传给这个函数,就可以用它的返回值设置 background-size
了。
5、演示函数
有了以上核心实现,我就可以执行起来了。此时我需要执行核心代码的代码——演示函数。
// 演示节点
internalShow() {
const next = this.showPath[this.nextPath];
const showProp = this.getShowProp(next.x, next.y, next.w, next.h);
// 使节点进场
this.$nextTick(() => {
this.imgContainerStyle.width = `${showProp.w}px`;
this.imgContainerStyle.height = `${showProp.h}px`;
this.imgContainerStyle.backgroundPosition = `${showProp.x}px ${showProp.y}px`;
this.imgContainerStyle.backgroundSize = this.getBackgroundSize(next.w);
this.imgContainerStyle.left = 0;
this.imgContainerStyle.right = 0;
this.imgContainerStyle.top = 0;
this.imgContainerStyle.bottom = 0;
this.imgContainerStyle.margin = 'auto';
});
}
其中,nextPath
是演示节点的指示器,它在 data
里设置。初始设置如下:
nextPath: -1
到此,我可以迫不及待地测试一下。我可以在 mounted
里添加如下代码:
setTimeout(() => {
this.nextPath = 0;
this.internalShow();
}, 1000);
以上代码表示页面加载 1 秒后,就演示第 1 个节点。亲测成功!
6、完善演示函数
我希望程序能像 PPT 一样,既可以演示下一个节点,也可以演示上一个节点。而且,我还需要检查 nextPath
值,使它不能越界,也就是说,当演示到最后一个节点时,就不能再往下演示了,当回退到第一个节点时,就不能再回退了。于是,有了以下代码:
// 演示下一个节点
showNext() {
// 如果演示到最后一个节点,就不再往后演示
if (++this.nextPath === this.showPath.length) {
this.nextPath--;
return;
}
this.internalShow();
},
// 演示上一个节点
showPrevious() {
// 如果后退到第一个节点,就不再后退
if (--this.nextPath < 0) {
this.nextPath++;
return;
}
this.internalShow();
}
在 mounted
里使用以下代码测试下:
setTimeout(() => {
this.showNext();
setTimeout(() => {
this.showNext();
setTimeout(this.showPrevious, 500);
}, 500);
}, 1000);
7、键盘指令
有没有发现,我刚陷入了回调地狱中?因为 setTimeout
嵌套太复杂了,让人抓狂!
所以,为了演示每个节点,我决定使用键盘指令——按下方向左右键——来指示程序演示,所以我应绑定键盘事件。
在 mounted
里加入以下代码:
window.addEventListener('keyup', event => {
if ('ArrowRight' === event.key) {
// 按下`right`键时,使思维导图向后演示
this.showNext();
} else if ('ArrowLeft' === event.key) {
// 按下`left`键时,使思维导图向前演示
this.showPrevious();
}
});
到此,按下方向右键时,程序就演示下一个节点,按下方向左键时,程序就回到上一个节点。
8、最终效果
更多
嘿!你可能认为这样做效果太简陋了,应当要加入动画啥的。我已经考虑过,也加入了。只是因为这些增强效果不在本文范围内,所以我就不写了。
我的全部代码都放在 GitHub 里,欢迎来看看。