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-tabsid="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
来避免变形。