最近在逐步学习 Web 开发,突发奇想决定利用 HTML + CSS 构建一个原神抽卡 UI,在逐步推进的过程中,进一步决定将其变为高度自定义、高度仿真的原神抽卡模拟器。跟随学习进度,前期将使用纯 HTML + CSS 构建,之后再迁移到前端框架,并完善相关逻辑实现。

新手上路,记录内容难免初级愚蠢,大佬请轻喷或绕行。


整体思路

整体分为两个部分:UI 类(包括界面文本及图标、按钮等)、poster 类(中央展示区)。
全局使用 vh 单位,确保相对布局不受缩放影响。

背景采用视频置底,并在后期考虑性能更佳的方案;UI 采用 flex 布局分居上下,利用 padding 创建安全区。poster 整体采用 flex 布局。

响应式布局待后续完善。

前置 CSS

为保证网页在任意缩放下相对布局不受影响,使用 vh 单位。1vh 等于视口高度的 1%,因此无论缩放如何,元素相对于视口的大小和位置保持一致。但在 AI 协助添加竖屏强制横屏时,发现 vh 单位在竖屏时会受到影响,导致布局错乱。故目前全局使用 vmin 单位,确保在竖屏和横屏下都能保持相对布局的一致性。

变量

UI 内主要使用的几个颜色使用变量存储,方便区分与理解:

1
2
3
--primary-color: #d09a52;    // 开发前期的临时颜色,角色元素的主题色
--text-black: #565656; // 深色文本的颜色
--text-white: #ffffff; // 白色文本的颜色

自定义字体

使用 @font-face At-rule,目前暴力加载原神字体的 ttf 文件,引用为 Wenhei。

1
2
3
4
@font-face {  
font-family: 'Wenhei';
src: url('./zh-cn.ttf');
}

复用样式

全局文本分为三种大小,分别设置为 .smaller-text.medium-text.bigger-text。UI 文本具有黑色描边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.bigger-text {  
font-size: 2.4vh;
}
.medium-text {
font-size: 2vh;
}
.smaller-text {
font-size: 1.75vh;
}
.black-shadow {
text-shadow:
-0.1vh -0.1vh 0 rgba(0, 0, 0, 0.25),
0.1vh -0.1vh 0 rgba(0, 0, 0, 0.25),
-0.1vh 0.1vh 0 rgba(0, 0, 0, 0.25),
0.1vh 0.1vh 0 rgba(0, 0, 0, 0.25);
}

仍然可以注意到,游戏内的描边实现与 CSS 的四向文本阴影效果一致。明显的特征为转角处描边有缺口。文本的描边在下面会再次提到。

文本描边效果

优化类

盒模型使用 border-box,方便布局规划与设计,禁止文本选中并设置全局字体。

为按钮清除部分默认样式,忽略 img 的鼠标事件防止图片拖拽(后续仍需优化)。

背景层:#background-video.bg-dark

目前背景采用视频,配合 .bg-dark 类遮罩模拟游戏内 shader。fixed 定位适应性平铺。

考虑到后续可能优化背景,此处不详细记录。

中央展示区 .poster [角色祈愿]

.poster 为 fixed 定位,即相对于 viewport 定位。使用 top 与 left 创建 50% 的 offset 并进行负偏移,达到居中效果。

1
2
3
4
5
6
7
.poster {  
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -48%); // 游戏内并未完全居中
...
}

将其分为 4 个部分:卡片左上角的卡池类型标识 .banner-sign,卡片左侧整体文本 .poster-text-left (flex),卡片展示的立绘 .main-img-mask.side-img,卡池角色的名字标签 .character-label

由于目前 .poster 为角色祈愿的样式,不同祈愿样式有所差别,在后续完善过程中可能修改类名加以区分。

.main-img-mask

五星角色的立绘外采用一层 mask,意图为创建剪切蒙版的效果。mask 使用绝对定位及 100% 宽高,与 .poster 框架大小位置一致。

由于注意到 .banner-sign 有出框效果,若将 overflow: hidden 直接应用与 .poster 会导致 .banner-sign 也被裁切,于是选择为立绘加一个 mask。这一点算是一个小巧思,掉了一根头发,将实现蒙版的思路从“为立绘加一层蒙版”转变为“如何让蒙版外的区域透明”是重点所在。

poster overflow 示意

mask 也是第一次接触的 CSS 属性。以下是来自 MDN 对 mask-image 的解释:

The mask-image CSS property sets the image that is used as the mask layer for an element, hiding sections of the element on which the masking image is set based on the alpha channel of the mask image and, depending on the mask-mode property value, the luminance of the mask image’s colors.

由于 image 的判断是基于 alpha 通道的,因此使用一个从 transparent 到不透明的渐变作为 mask-image 便达到了想要的效果。

.poster-text-left

起初做 .poster-text-left 时还没调试好 .main-img,没有设置它的定位方式。调试完 .main-img 后发现,给 .poster-text-left 设置非 static 定位才能设置 z-index 属性,实现层级调整,故设置为 relative。另一个考虑是使用 relative 而非 absolute 是因为相较于对整个文本区域设置偏移而言,直接对容器设置 padding 更好控制。另外若文本区域本身不设置 offset,它的位置不会变化。

该部分的通用样式为文本颜色(黑色)和白色描边。

内部分为 3 个板块:.gacha-name.describe,剩余时间三部分(剩余时间无额外样式便未分配额外类名,仅套用 .smaller-text)。采用 flex 布局的 column 主轴,justify-content 设置为 space-between 实现顶、底贴边。

poster-text-left 布局示意

中部的 .describe 本质为一个 container,高度 100% 以撑满整个 flex 容器除 .gacha-name 和剩余时间的空间。内部的 .describe-content 也为 flex 的 column 布局,justify-contentflex-start 实现局部的顶部对齐。

“每十次祈愿必出四星或以上物品”是半透明的着色背景,实际遇到的问题则是,在父容器 .poster-text-left 的 flex 布局下,交叉轴方向默认撑满导致着色背景的宽度过大(不能根据文本长度适应)。将相关类添加 align-self: flex-startwidth: fit-content 后整个容器宽度就能根据文本长度适应了。

too long bg color

其次是伪元素的应用。.describe-content 左侧的竖线是通过伪元素实现的,设置为绝对定位并放置在文本左侧。值得一提的是,不应该对 .describe 设置伪元素,而是 .describe-content,否则竖线的长度会被 .describe 的高度撑大,无法自适应。这也是 .describe-content 外套一个容器的另一层原因。

describe 伪元素

.describe 这个容器确实也有两个伪元素,分别为上下的装饰虚线,设置为绝对定位并放置在 .describe 的上下边缘。(游戏内这里在超框时还有一个在虚线处的透明度渐变,暂时没有做)

关于文本描边

描边的实现是从刚开始就考虑的。经历一番 Google 后,了解到了两种常见的实现方式:text-shadowtext-stroke。前者通过在文本周围设置多个阴影来模拟描边效果,后者则是 CSS3 引入的属性,可以直接为文本添加描边。

text-stroke 搜索截图

1
2
3
4
5
text-shadow:
-0.1vmin -0.1vmin 0 rgba(255, 255, 255, 0.8),
0.1vmin -0.1vmin 0 rgba(255, 255, 255, 0.8),
-0.1vmin 0.1vmin 0 rgba(255, 255, 255, 0.8),
0.1vmin 0.1vmin 0 rgba(255, 255, 255, 0.8);

然而实践后发现,text-stroke 的描边是内描边,会导致文本视觉变细,与游戏内不符。在试了四向 text-shadow 后,发现它与游戏内的描边效果惊人地相似,尤其是转角处的缺口特征。因此最终选择了 text-shadow 的方式来实现文本描边。关于这个相似,我也询问了 Gemini,有一些意外收获。

在 Unity 的旧版 UI 系统(uGUI)中,实现描边最简单粗暴的方法是通过一个名为 Outline 的组件。这个组件的底层原理并不是真的画了一圈线,而是把原本的文本网格(Mesh)复制了 4 份,分别向左上、右上、左下、右下偏移了 1 个像素。因为这四个副本是十字形或交叉形偏移的,在拐角处它们无法重合,自然就形成了你看到的那个“缺口”。即便增加到 8 份(加上上下左右),在描边较粗时,边缘依然会显得锯齿感十足。

.character-label

比划一下游戏截图不难看出,标签的位置大致位于 .poster 的 50%, 75% 处。

character-label layout

由于标签内包含图标、底色、小标签、UP! 标识,且相对位置较为复杂,主要还包含一些地方的对齐,所以考虑了 grid 布局。尽管使用 flex 布局也能实现,但 grid 布局或许更适合这种情况,能够更方便地控制元素的位置和对齐。

character-label grid

由于 .name-bg 的宽度范围是从元素图标的 50% 到 UP! 标识的 ~50%,因此 .name-bg 作为单元格,并设置 grid-column 为 1/3,能够让它自动适应图标和 UP! 标识之间包括文字的宽度。高度同理。

.star.signgrid-column 则均为 2/4,这样其宽度可以根据文本长度适应,且不会影响上一行的内容。

然而 .sign-bg 的宽度完全取决于 .sign 本身,而 .sign 单元格的宽度(2/4)是预留或者说溢出的,所以使用单元格并不现实。所以这里仍然使用 .sign 的伪元素来实现,而 .sign 本身设置为 flex 布局,其宽度在较长的单元格内自适应。

UI

UI 分为上中下三部分,分别为 .ui-top.ui-center.ui-bottom。整体均使用 flex 布局来实现上下靠齐。每个部分也分为 2-3 个板块,使用 flex 布局来实现左右靠齐。

.ui-top


To be continued…