两周前,Adobe 发布了Web版 Photoshop🔗,它是使用 WebAssembly、web components、P3 颜色等 Web 技术构建的。本文就来研究一下网页版 Photoshop 上有趣又有用的 CSS 知识!
Photoshop 旧 Logo
首先,在浏览器控制台中使用了 Photoshop 的 Logo(1990-1991)。
这是如何实现的呢?这里是代码:
console.info(
"%c %cAdobe %cPhotoshop Web%c %c2023.20.0.0%c %c1bba617e276",
"padding-left: 36px; line-height: 36px; background-image: url('data:image/gif;base64,R0lGODlhIAAgAPEBAAAAAPw==');"
)
Body 元素
要让像Photoshop这样的应用在Web上感觉像一个真正的应用,第一件事是防止滚动。为了实现这一点,<body>元素具有position: fixed和overflow: hidden。
body,
html {
height: 100%;
}
body {
font-family: adobe-clean, sans-serif;
margin: 0;
overflow: hidden;
position: fixed;
width: 100%;
}
在<body>元素内部,也有多个根元素。
<psw-app>
<psw-app-context>
<ue-video-surface>
<ue-drawer>
<div id="appView">
<psw-app-navbar></psw-app-navbar>
<psw-document-page></psw-document-page>
</div>
</ue-drawer>
</ue-video-surface>
</psw-app-context>
</psw-app>
最内部有一个包含导航和文档页面的元素。
#appView {
background-color: var(--editor-background-color);
color: var(--spectrum-global-color-gray-800);
display: flex;
flex-direction: column;
}
* {
touch-action: manipulation;
}
:host {
position: relative;
}
Flexbox 布局
在构建现代的 Web 应用时,使用 Flex 布局具有很多好处。网页版 Photoshop 就用到了很多 Flex 布局。
使用 Flexbox 使构建组件变得更容易。下面来看几个使用 Flexbox 的例子。
导航栏
我很喜欢这部分的元素类命名,它没有使用 left、center、right,而是使用了start、center、end。
在应用可以从左到右(LTR)或从右到左(RTL)工作时,这种逻辑命名是正确的做法。
操作
在构建像 Photoshop 这样的复杂应用时,嵌套的 Flexbox 容器是很常见的。下图显示了操作栏中的两个容器。
第一个容器用于缩放。第二个容器包含所有的操作和按钮。
.container {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: var(--spectrum-global-dimension-size-50);
}
- 使用gap属性非常有助于定义间距,使用margin或padding来实现这个效果可能会变得混乱。
- .container这个类名太过通用了,但在这里非常适用,因为这是一个Web组件,所以所有的样式都被封装起来了。
图层
图层功能是Photoshop的重要组成部分,仔细观察CSS,发现它们全部都是 Flexbox 布局。
下面是图层组件的 HTML 结构:
<psw-tree-view-item indent="0" layer-visible can-open dir="ltr" open>
<div id="link">
<span id="first-column"></span>
<span id="second-column"></span>
<span id="label"></span>
</div>
</psw-tree-view-item>
这里使用 ID 是完全没问题的,因为这是一个 Web 组件,因此 #first-column ID 在页面上出现多少次并不重要。
#link元素是一个 Flexbox 容器,#label元素也是一个 Flexbox 容器。
<div class="layer-content layer-wrapper selected">
<psw-layer-thumbnail></psw-layer-thumbnail>
<div class="name" title="Layer name">Layer name</div>
<div class="actions"></div>
<overlay-trigger></overlay-trigger>
</div>
下面来举一个例子说明如何完成子层的缩进。
- :host()表示图层组件。
- 这感觉像是条件式 CSS。如果HTML属性indent=1存在,那么就更改第一列的padding-right。
:host([dir="ltr"][indent="1"]) #first-column {
padding-right: var(--spectrum-global-dimension-size-200);
}
如果缩进是两个级别,那么可以通过CSS的calc()函数,将padding-right的值乘以 2。
:host([dir="ltr"][indent="2"]) #first-column {
padding-right: calc(2 * var(--spectrum-global-dimension-size-200));
}
在浏览器中,尝试嵌套到第 6 层:
而知名的设计软件 Figma 是使用间隔组件来增加嵌套层的间距的。
Grid 布局
新文件模式
在创建新的 Photoshop 文件时,可以选择预定义的尺寸列表。为了实现这一点,有一个包含多个选项卡和一个活动面板的布局。
HTML 如下所示:
<sp-tabs
id="tabs"
quiet=""
selected="2"
size="m"
direction="horizontal"
dir="ltr"
focusable=""
>
<div id="list"></div>
<slot name="tab-panel"></slot>
</sp-tabs>
在 CSS 中,有一个 1 列 2 行的主网格。其中,第一行的高度根据内容自适应调整行高,而第二行则占据了所有剩余可用空间。
:host {
display: grid;
grid-template-columns: 100%;
}
:host(:not([direction^="vertical"])) {
grid-template-rows: auto 1fr;
}
这里有几个要点:
- 使用CSS的:not()选择器。
- 使用[attr^=value]选择器来排除具有以vertical开头的值的direction属性的HTML元素。
这些都是条件式 CSS 技术。
尝试将direction属性更改为vertical,结果符合预期。
下面是基于属性变化的CSS:
:host([direction^="vertical"]) {
grid-template-columns: auto 1fr;
}
:host([direction^="vertical-right"]) #list #selection-indicator,
:host([direction^="vertical"]) #list #selection-indicator {
inline-size: var(
--mod-tabs-divider-size,
var(--spectrum-tabs-divider-size)
);
inset-block-start: 0px;
inset-inline-start: 0px;
position: absolute;
}
为了突出显示哪个选项卡是活动状态,有一个相对于选项卡列表定位的#selection-indicator元素。
图层属性
这部分的 CSS 网格用法很有趣,它适合解决对齐网格中许多元素的问题。
深入研究 CSS:
.content {
position: relative;
display: grid;
grid-template-rows: [horizontal] min-content [vertical] min-content [transforms] min-content [end];
grid-template-columns: [size-labels] min-content [size-inputs] auto [size-locks] min-content [space] min-content [position-labels] min-content [position-inputs] auto [end];
row-gap: var(--spectrum-global-dimension-size-150);
}
使用 Firfox 开发者工具来调试这个网格,它会生成模拟的网格布局。当突出显示矩形时,它将显示放置在实际的网格中。
这里使用的技术就是命名网格线,其思想是给每个列或行指定一个名称,然后定义其宽度。列和行的宽度可以是auto
或min-content
,这是创建动态网格的一种很好的方法。
这样,每个网格项都可以在网格内部进行定位:
.horizontal-size-label {
grid-area: horizontal / size-labels / horizontal / size-labels;
}
.vertical-position-input {
grid-area: vertical / position-inputs / vertical / position-inputs;
}
.horizontal-position-input {
grid-area: horizontal / position-inputs / horizontal /
position-inputs;
}
还有一个细节是对网格项使用了position: absolute。锁定按钮位于网格的中心,但它需要略微从左边和顶部位置进行插入。
.lock-button {
grid-area: horizontal / size-locks / horizontal / size-locks;
position: absolute;
left: 8px;
top: 22px;
}
输入
下面来看看使用 CSS 网格来布局输入字段的用例。
:host([editable]) {
display: grid;
grid-template-areas:
"label ."
"slider number";
grid-template-columns: 1fr auto;
}
:host([editable]) #label-container {
grid-area: label / label / label / label;
}
:host([editable]) #label-container + div {
grid-area: slider / slider / slider / slider;
}
:host([editable]) sp-number-field {
grid-area: number / number / number / number;
}
在浏览器中检查时,可以看到网格线名称或网格区域名称。
- 网格区域名称
- 网格线名称
菜单项
在我看来,在这里使用 CSS 网格有点大材小用了。
sp-menu-item {
display: grid;
grid-template-areas:
". chevronAreaCollapsible . iconArea sectionHeadingArea . . ."
"selectedArea chevronAreaCollapsible checkmarkArea iconArea labelArea valueArea actionsArea chevronAreaDrillIn"
". . . . descriptionArea . . ."
". . . . submenuArea . . .";
grid-template-columns: auto auto auto auto 1fr auto auto auto;
grid-template-rows: 1fr auto auto auto;
}
这是一个包含 8列 4行的网格。这里似乎一次只能激活一个网格行,其他行将由于内容为空或HTML元素的缺失而折叠。
有趣的是,上面的 CSS 是简化过的版本。原始版本看起来像这样,使用了grid-template缩写。
下面是在应用中找到的一些菜单项:
CSS 网格是针对这个小组件的,感觉是有点多此一举了。
.checkmark {
align-self: start;
grid-area: checkmarkArea / checkmarkArea / checkmarkArea /
checkmarkArea;
}
#label {
grid-area: labelArea / labelArea / labelArea / labelArea;
}
::slotted([slot="value"]) {
grid-area: valueArea / valueArea / valueArea / valueArea;
}
注意,CSS 网格的暗部分处于非活动状态。它们折叠了,因为没有内容。对于这个例子,也可以这样来实现:
.checkmark {
align-self: start;
grid-area: checkmarkArea;
}
#label {
grid-area: labelArea;
}
::slotted([slot="value"]) {
grid-area: valueArea;
}
当每列和行的值相同时,无需定义它们的开始和结束位置。
CSS变量的广泛使用
更改图层缩略图的大小
Photoshop 可以控制缩略图大小。当有很多图层并且想要在更小的空间中查看更多图层时,这非常有用。
Adobe 团队的构建方式很有趣。首先,图层面板的主容器上有一个 HTML 属性large-thumbs。
<psw-layers-panel large-thumbs></psw-layers-panel>
在CSS中,有一个:host([large-thumbs])选择器,它分配了特定的CSS变量。
:host([large-thumbs]) {
--psw-custom-layer-thumbnail-size: var(
--spectrum-global-dimension-size-800
);
--psw-custom-layer-thumbnail-border-size: var(
--spectrum-global-dimension-size-50
);
}
对于每个图层,都有一个名为 psw-layer-thumbnail 的元素,这是将应用CSS变量的地方。
<psw-layers-panel-item>
<psw-tree-view-item>
<psw-layer-thumbnail class="thumb"></psw-layer-thumbnail>
</psw-tree-view-item>
</psw-layers-panel-item>
这里,CSS 变量被分配给缩略图。
:host {
--layer-thumbnail-size: var(
--psw-custom-layer-thumbnail-size,
var(--spectrum-global-dimension-size-400)
);
--layer-badge-size: var(--spectrum-global-dimension-size-200);
position: relative;
width: var(--layer-thumbnail-size);
min-width: var(--layer-thumbnail-size);
height: var(--layer-thumbnail-size);
}
加载进度
通过使用size属性来管理组件的大小。CSS 变量会根据不同的 size 而变化。
:host([size="m"]) {
--spectrum-progressbar-size-default: var(
--spectrum-progressbar-size-2400
);
--spectrum-progressbar-font-size: var(--spectrum-font-size-75);
--spectrum-progressbar-thickness: var(
--spectrum-progress-bar-thickness-large
);
--spectrum-progressbar-spacing-top-to-text: var(
--spectrum-component-top-to-text-75
);
}
图像控制
如果 HTML 存在属性quiet,则 UI 会更简单(没有边框)。
这也是通过 CSS 变量完成的。
:host([quiet]) {
--spectrum-actionbutton-background-color-default: var(
--system-spectrum-actionbutton-quiet-background-color-default
);
--spectrum-actionbutton-background-color-hover: var(
--system-spectrum-actionbutton-quiet-background-color-hover
);
}
单选按钮
在这个例子中,使用 CSS 变量来根据 HTML 的 size 属性值来改变单选按钮的大小。
<sp-radio size="m" checked="" role="radio"></sp-radio>
:host([size="m"]) {
--spectrum-radio-height: var(--spectrum-component-height-100);
--spectrum-radio-button-control-size: var(
--spectrum-radio-button-control-size-medium
);
}
菜单处于活动状态时锁定页面
当主菜单处于活动状态时,会有一个名为 holder 的元素填充整个屏幕(遮罩层),并位于菜单下方。
#actual[aria-hidden] + #holder {
display: flex;
}
#holder {
display: none;
align-items: center;
justify-content: center;
flex-flow: column;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
此元素是为了防止用户单击或将鼠标悬停在页面的其他部分,以确保用户只能对菜单进行操作,这可能是为了模仿桌面应用而设置的。
混合模式菜单
这里使用了 CSS 视口单位。混合模式菜单的最大高度为 55vh(视口高度的55%)。
sp-menu {
max-height: 55vh;
--mod-menu-item-min-height: auto;
}
::slotted(*) {
overscroll-behavior: contain;
}
这里还使用了 overscroll-behavior: contains,这是避免滚动正文内容的一个很棒的功能。
图层缩略图
在图层面板中,缩略图使用了object-fit: contain来避免变形。