0%

在可视化大屏布局中相对单位和calc()的应用

在构建可视化大屏的过程中,首先要先布局组件位置和大小。上两篇文章《关于各种像素概念和前端长度单位的理解》《CSS数学表达式calc()的规范草案翻译》学习了 CSS 相对单位 vwvhremclac(),本文以上两篇内容为基础,加上 mentor 仕春哥的启发,分析前端构建可视化大屏的一种布局方式。

大屏系统的内在原理

硬件

大屏系统实际上是由任意 n × m 块窄边显示器组成,具体如下图示例所示:

该例子中左下信号源的两台电脑通过显卡计算显示被投屏的内容,然后输出到大屏接收器中的矩阵切换器。矩阵切换器将两台电脑中的多路信号转换成单路信号。这个过程中播放控制设备可以控制两个画面的拼接的位置和模式(比如组合显示、开窗、漫游、叠加)。最后传输给上半部分的 4 × 2 块显示器大屏(拼接墙),最后展示大屏画面。

最佳显示效果

如果想实现最佳显示效果,应该满足下面的条件:

  • 大屏逻辑分辨率(设计稿尺寸)的长宽比 = 大屏实际物理分辨率的长宽比

  • 大屏逻辑分辨率(设计稿尺寸)的长宽比 = 显卡输出分辨率的长宽比

  • 显卡输出分辨率 = 视频矩阵切换器支持分辨率 = 大屏实际物理分辨率

在实际实现时,大屏一般由 n × n 块长宽比 16:9,分辨率为 1920×1080 的液晶屏拼接成,如果是 5 × 5 块大屏,实际物理分辨率已经达到了 9600×5400。但是,不管是显卡还是矩阵切换器,能支持 4k 显示(4096×2160)就很优秀了。所以在设计和前端实现时,一般符合 16:9 的比例的 1920×1080 分辨率,就可以高质量的进行大屏展示。

设计稿分割

设计规则

大屏的排版布局一般遵循下面的规则,主中间,次两边,附加各种动效和下钻:

实例

在谷歌上搜索了一个大屏设计稿的例子——中国移动全球基站管理,示例图片如下:

这个例子很符合主中间,次两边的规则,而且很显然中间的 3D 地球应该是有动效,两侧的卡片是可以下钻的。将图片大致分割一些,应该主要由以下部分构成:

前端布局

由于大屏的每个组件卡片排布方式都不同,普通的定位很难适配所有情况,所以这里使用 position: absolute 绝对定位脱离文档流,用 topleftwidthheight 来定位组件卡片。

定义基本量和卡片名

首先把基本量的长度先定义下来,这里的基本量可以复用,用画图做一个巨丑的简单示意图如下:

标题的高度为 heightHeader,正文主体部分的外边距分别为 marginMainTopmarginMainRightmarginMainBotommarginMainLeft,正文中每个小卡片的外边距marginCardTopBottommarginCardLeftRight(这里的边距并不是真正的 margin,只是表达类似的概念)。与 x 轴相关的长度统一用相对单位 vw,与 y 轴相关的长度统一用 vw,基础变量设置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 基础变量的设定
*/
// 标题部分的高度
const heightHeader = '8vh';
// 正文部分的外边距
const marginMainTop = '0.7vh';
const marginMainBottom = '0.7vh';
const marginMainLeft = '0.7vw';
const marginMainRight = '0.7vw';
// 每个小卡片的外边距
const marginCardTopBottom = '0.5vh';
const marginCardLeftRight = '0.5vw';

将每个卡片的中文标题翻译为英文简称,命名如下:

名称 英文翻译 简称
标题 header HD
信号处理情况 Signal processing situation SPS
动态 3D 地球 Dynamic 3D earth D3E
基站信息数据统计 Base station information data statistics BSIDS
国家基站变化对比 Comparison of changes in national base stations CCNBS
基站总体变化 Overall change of base station OCBS

使用 calc()计算

接下来就是最核心的地方,新建一个 config.js 文件,存放 calc()模板,给每个卡片都计算自己的 topleftwidthheight 值。

y 方向举例,“标题”只需要计算 height,,但是计算“信号处理情况”的 top 就需要在 heightHD 的基础上加上正文上边距 marginMainTop 和卡片上边距 marginCardTopBottom

另外,最下面和最右面的组件卡片 widthheight 需要利用 100%来减,完成自适应效果。还是以 y 方向举例,“基站总体变化”卡片,height 计算时,需要用 100%减去正文下边距 marginMainBottom、卡片下边距 marginCardTopBottom 和卡片自身的 toptopOCBS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 计算每个部分的top、left、height、width
*/

// 标题 header HD
const topHD = `0vh`;
const heightHD = `calc(${heightHeader})`;
const leftHD = `0vw`;
const widthHD = `100%`;
// 信号处理情况 Signal processing situation SPS
const topSPS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom})`;
const heightSPS = `50vh`;
const leftSPS = `calc(${marginMainLeft} + ${marginCardLeftRight})`;
const widthSPS = `15vw`;
// 动态3D地球 Dynamic 3D earth D3E
const topD3E = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom})`;
const heightD3E = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topD3E})`;
const leftD3E = `calc(${marginMainLeft} + ${marginCardLeftRight})`;
const widthD3E = `70vw`;
// 基站信息数据统计 Base station information data statistics BSIDS
const topBSIDS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom})`;
const heightBSIDS = `35vh`;
const leftBSIDS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight})`;
const widthBSIDS = `calc(100% - ${leftBSIDS} - ${marginCardLeftRight} - ${marginMainRight})`;
// 国家基站变化对比 Comparison of changes in national base stations CCNBS
const topCCNBS = `calc(${topBSIDS} + ${heightBSIDS} + ${marginCardTopBottom})`;
const heightCCNBS = `35vh`;
const leftCCNBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight})`;
const widthCCNBS = `calc(100% - ${leftCCNBS} - ${marginCardLeftRight} - ${marginMainRight})`;
// 基站总体变化 Overall change of base station OCBS
const topOCBS = `calc(${topCCNBS} + ${heightCCNBS} + ${marginCardTopBottom})`;
const heightOCBS = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topOCBS})`;
const leftOCBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight})`;
const widthOCBS = `calc(100% - ${leftOCBS} - ${marginCardLeftRight} - ${marginMainRight})`;

配置输出

把上述计算的配置项输出,使用简称键名,需要配置的 CSS 属性键值

比较特殊的两项是“信号处理情况”和“动态 3D 地球”。这两个组件相互叠加,需要把上层组件和下层组件设置不同的 z-index 来区分高低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @description 生成配置对象
* @param {}
* @returns {Object} 每个id对应的需要配置的style
*/
let generatorConfigLayout = () => {
return {
HD: {
top: topHD,
left: leftHD,
height: heightHD,
width: widthHD
},
SPS: {
top: topSPS,
left: leftSPS,
height: heightSPS,
width: widthSPS,
// 类似左侧的底图,需要加个z-index
'z-index': 2
},
D3E: {
top: topD3E,
left: leftD3E,
height: heightD3E,
width: widthD3E,
// 类似左侧的底图,需要加个z-index
'z-index': 1
},
BSIDS: {
top: topBSIDS,
left: leftBSIDS,
height: heightBSIDS,
width: widthBSIDS
},
CCNBS: {
top: topCCNBS,
left: leftCCNBS,
height: heightCCNBS,
width: widthCCNBS
},
OCBS: {
top: topOCBS,
left: leftOCBS,
height: heightOCBS,
width: widthOCBS
}
};
};
// 最后导出配置函数
export default generatorConfigLayout;

页面样式

html

页面的样式就比较简单,首先新建一个 index.html,然后要给 htmlbody 设置一个基础样式,z-index 是为了把 body 放在最底层,overflow: hidden 防止出现滚动条。设置根元素字体大小时使用相对大小,让文字在不同分辨率中视觉效果尽量统一(注意:Chrome 最小字体大小为 12px)。

每个组件的标签都有自己的 idid 为简称。在通用.layout 样式中,给每个组件设置绝对定位。为了容易区分,给每个标签设置了不同的背景颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>可视化大屏布局</title>
<style>
/* */
html,
body {
/* 布局必需start */
margin: 0;
padding: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
/* 布局必需end */
/* 让字体使用相对大小 */
font-size: calc(100vw / 120);
background-color: #fdffdf;
}
p {
margin: 0;
}
.layout {
/* 布局必需start */
position: absolute;
overflow: hidden;
/* 布局必需end */
/* flex start */
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
/* flex end */
/* border: 1px solid bisque; */
}
</style>
</head>
<body>
<div id="HD" class="layout" style="background-color: #EFCEE8;">
标题
</div>
<div id="SPS" class="layout" style="background-color:#F0F0F0;">
<p>信号处理情况</p>
</div>
<div id="D3E" class="layout" style="background-color:#F3D7B5;">
<p>动态3D地球</p>
</div>
<div id="BSIDS" class="layout" style="background-color:#DAF9CA;">
<p>基站信息数据统计</p>
</div>
<div id="CCNBS" class="layout" style="background-color:#C7B3E5;">
<p>国家基站变化对比</p>
</div>
<div id="OCBS" class="layout" style="background-color:#A79496;">
<p>基站总体变化</p>
</div>
</body>
</html>

配置导入

为了让浏览器能使用 ES6 的导入导出,需要在 script 标签中添加 type="module"

从上节的配置文件中导入 generatorConfigLayout 函数,执行函数生成布局变量 configLayout。遍历 configLayout,每次执行 setStyle()函数,将 css 样式设置给相对应的组件标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script type="module">
import generatorConfigLayout from './config.js';
/**
* @description 根据id和对应配置项config给dom添加style
* @param {String} id id
* @param {Object} config 每个id对应配置项
*/
const setStyle = (id, config) => {
let dom = document.querySelector(`#${id}`);
Object.keys(config).forEach(item => {
dom.style[item] = config[item];
dom.innerHTML += `<p>${item} : ${config[item]}</p>`;
});
};
// 循环每个id,给每个标签都设置style
let configLayout = generatorConfigLayout();
Object.keys(configLayout).forEach(key => {
setStyle(key, configLayout[key]);
});
</script>

最后生成的布局结果如下所示,完成!

全部代码

全部代码如下:
config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* 基础变量的设定
*/
// 标题部分的高度
const heightHeader = '8vh';
// 正文部分的外边距
const marginMainTop = '0.7vh';
const marginMainBottom = '0.7vh';
const marginMainLeft = '0.7vw';
const marginMainRight = '0.7vw';
// 每个小卡片的外边距
const marginCardTopBottom = '0.5vh';
const marginCardLeftRight = '0.5vw';

/**
* 计算每个部分的top、left、height、width
*/

// 标题 header HD
const topHD = `0vh`;
const heightHD = `calc(${heightHeader})`;
const leftHD = `0vw`;
const widthHD = `100%`;
// 信号处理情况 Signal processing situation SPS
const topSPS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom})`;
const heightSPS = `50vh`;
const leftSPS = `calc(${marginMainLeft} + ${marginCardLeftRight})`;
const widthSPS = `15vw`;
// 动态3D地球 Dynamic 3D earth D3E
const topD3E = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom})`;
const heightD3E = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topD3E})`;
const leftD3E = `calc(${marginMainLeft} + ${marginCardLeftRight})`;
const widthD3E = `70vw`;
// 基站信息数据统计 Base station information data statistics BSIDS
const topBSIDS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom})`;
const heightBSIDS = `35vh`;
const leftBSIDS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight})`;
const widthBSIDS = `calc(100% - ${leftBSIDS} - ${marginCardLeftRight} - ${marginMainRight})`;
// 国家基站变化对比 Comparison of changes in national base stations CCNBS
const topCCNBS = `calc(${topBSIDS} + ${heightBSIDS} + ${marginCardTopBottom})`;
const heightCCNBS = `35vh`;
const leftCCNBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight})`;
const widthCCNBS = `calc(100% - ${leftCCNBS} - ${marginCardLeftRight} - ${marginMainRight})`;
// 基站总体变化 Overall change of base station OCBS
const topOCBS = `calc(${topCCNBS} + ${heightCCNBS} + ${marginCardTopBottom})`;
const heightOCBS = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topOCBS})`;
const leftOCBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight})`;
const widthOCBS = `calc(100% - ${leftOCBS} - ${marginCardLeftRight} - ${marginMainRight})`;

/**
* @description 生成配置对象
* @param {}
* @returns {Object} 每个id对应的需要配置的style
*/
let generatorConfigLayout = () => {
return {
HD: {
top: topHD,
left: leftHD,
height: heightHD,
width: widthHD
},
SPS: {
top: topSPS,
left: leftSPS,
height: heightSPS,
width: widthSPS,
// 类似左侧的底图,需要加个z-index
'z-index': 2
},
D3E: {
top: topD3E,
left: leftD3E,
height: heightD3E,
width: widthD3E,
// 类似左侧的底图,需要加个z-index
'z-index': 1
},
BSIDS: {
top: topBSIDS,
left: leftBSIDS,
height: heightBSIDS,
width: widthBSIDS
},
CCNBS: {
top: topCCNBS,
left: leftCCNBS,
height: heightCCNBS,
width: widthCCNBS
},
OCBS: {
top: topOCBS,
left: leftOCBS,
height: heightOCBS,
width: widthOCBS
}
};
};
export default generatorConfigLayout;

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>可视化大屏布局</title>
<style>
/* */
html,
body {
/* 布局必需start */
margin: 0;
padding: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
/* 布局必需end */
/* 让字体使用相对大小 */
font-size: calc(100vw / 120);
background-color: #fdffdf;
}
p {
margin: 0;
}
.layout {
/* 布局必需start */
position: absolute;
overflow: hidden;
/* 布局必需end */
/* flex start */
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
/* flex end */
/* border: 1px solid bisque; */
}
</style>
</head>
<body>
<div id="HD" class="layout" style="background-color: #EFCEE8;">
标题
</div>
<div id="SPS" class="layout" style="background-color:#F0F0F0;">
<p>信号处理情况</p>
</div>
<div id="D3E" class="layout" style="background-color:#F3D7B5;">
<p>动态3D地球</p>
</div>
<div id="BSIDS" class="layout" style="background-color:#DAF9CA;">
<p>基站信息数据统计</p>
</div>
<div id="CCNBS" class="layout" style="background-color:#C7B3E5;">
<p>国家基站变化对比</p>
</div>
<div id="OCBS" class="layout" style="background-color:#A79496;">
<p>基站总体变化</p>
</div>
<script type="module">
import generatorConfigLayout from './config.js';
/**
* @description 根据id和对应配置项config给dom添加style
* @param {String} id id
* @param {Object} config 每个id对应配置项
*/
const setStyle = (id, config) => {
let dom = document.querySelector(`#${id}`);
Object.keys(config).forEach(item => {
dom.style[item] = config[item];
dom.innerHTML += `<p>${item} : ${config[item]}</p>`;
});
};
// 循环每个id,给每个标签都设置style
let configLayout = generatorConfigLayout();
Object.keys(configLayout).forEach(key => {
setStyle(key, configLayout[key]);
});
</script>
</body>
</html>
👆 全文结束,棒槌时间到 👇