4
本文翻译自 [CSS Findings From Photoshop Web Version
](https://ishadeed.com/article/photoshop-web-css),作者:Ahmad, 略有删改。

几周前,Adobe发布了一个Web版的Photoshop,它是用WebAssembly、Web组件、P3颜色等网络技术构建的。

Photoshop是我14岁时学会的第一个专业设计应用程序。这是我成为设计师并最终成为前端开发人员的原因之一。正因为如此,我认为看看CSS是如何助力像Photoshop这样的大型应用开发会很有趣。

在这篇文章中,我将分享在Web版的Photoshop中我觉得有趣的CSS发现。

Photoshop 旧版Logo

我注意到的第一件事是在浏览器控制台中使用Photoshop(1990-1991)的旧Logo。

你会对这样的东西是如何制作的感到好奇吗?以下是代码:

console.info(
  "%c %cAdobe %cPhotoshop Web%c  %c2023.22.0.0%c  %c56043548b47",
  "padding-left: 36px; line-height: 36px; background-image: url('data:image/gif;base64,R0lGODlhIAAgAPEBAAAAAP///wAAAAAAACH5BAEAAAIALAAAAAAgACAAAAKkhC+py3zfopxGvIsztsCGD4La2FVAGBoBuZnox45WcqLsum5KDWdvf1nwTo+fLbgDqo7LFCJJbOY0oidt6ozVKrtib0qaCnlYcJh7rf5iK6HZaM64VeS6L+pWf89WT+6vRAUBBVQ1gpOTJ4IYdxCnOBSJ8ZhkZNekk5ZSxpTpt6Y1eEVm00j3VALDmBXVyPEJB2r2ShoLh2ASqvU60dsr5yuBUQAAOw=='); background-size: 32px; background-repeat: no-repeat; background-position: 2px 2px", "background: #666; border-radius:0.5em 0 0 0.5em; padding:0.2em 0em 0.1em 0.5em; color: white; font-weight: bold", "background: #666; border-radius:0 0.5em 0.5em 0; padding:0.2em 0.5em 0.1em 0em; color: white;", "", "background: #c3a650; border-radius:0.5em; padding:0.2em 0.5em 0.1em 0.5em; color: white;", "", "background: #15889f; border-radius:0.5em; padding:0.2em 0.5em 0.1em 0.5em; color: white;");

body 元素

要让Photoshop这样的应用在Web网页上有真实应用的感觉,首先需要防止滚动。为了实现这一点,<body>元素设置了position: fixedoverflow: 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

#appView {
  background-color: var(--editor-background-color);
  color: var(--spectrum-global-color-gray-800);
  display: flex;
  flex-direction: column;
}

几乎都是 Flexbox 布局

当构建一个web应用程序时,使用flexbox有很多好处。当我想到Flexbox和Photoshop一起出现时,我的感觉是很复杂。

Photoshop是一个著名的设计软件,是许多人进入设计领域的第一款软件。另一方面使用Flexbox构建组件变得更容易,对CSS对新手来说更容易。

无需使用clearfix清除浮动,只需添加display: flex,然后根据需要设置子项的样式。让我们探索Photoshop中的相关Flexbox使用情况。

导航栏

我喜欢这里的命名。他们使用“start”,“center”和“end”,而不是使用“leftcenterright”,。

对于可以从左到右(LTR)或从右到左(RTL)工作的应用程序来说,这种逻辑命名是正确的。

Context Bar

在构建像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实现。

这里是层组件的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()表示层组件
  • 如果有HTML属性存在indent=1,则更改第一列的padding-right
CSS :host 是一个伪类选择器,它用于选择当前组件的宿主元素。:host 选择器只能在 Shadow DOM 中使用,因为它选择的是组件的根元素,而不是组件内部的子元素。
:host([dir="ltr"][indent="1"]) #first-column {
  padding-right: var(--spectrum-global-dimension-size-200);
}

如果是indent=2,则通过CSS calc()函数将padding-right的值乘以2。

:host([dir="ltr"][indent="2"]) #first-column {
  padding-right: calc(2 * var(--spectrum-global-dimension-size-200));
}

在浏览器中,我尝试嵌套到第6级。下面是一张真实的截图:

当看到这个的时候,我检查Figma背后的CSS实现。他们使用了一个间隔组件来增加嵌套层的间距。

有趣的是,两个主要的设计应用程序使用了不同的技术来实现相同的目标。

关于 CSS 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行的主网格。第一行是auto,第二行跨越可用空间。

:host {
  display: grid;
  grid-template-columns: 100%;
}

:host(:not([direction^="vertical"])) {
  grid-template-rows: auto 1fr;
}

这里有几件事:

  • 使用CSS:not()选择器
  • 使用[attr^=value]选择器排除属性direction的值以vertical开头的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);
}

这里使用的技术称为命名网格线。这个想法是你命名每个列或网格,然后定义其宽度。列和行的宽度为automin-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。锁定按钮被放置在网格的中心,但它需要在lefttop位置稍微偏移一些。

.lock-button {
  grid-area: horizontal / size-locks / horizontal / size-locks;
  position: absolute;
  left: 8px;
  top: 22px;
}

Drop-Shadow 输入框

这是许多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网格应该在我们的Web应用程序中更多地使用,但绝对不像下面的例子。

菜单网格

我认为在这里使用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 网格是为了这个小组件而设计的,我认为在这里使用 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变量

我真的很喜欢CSS变量如何用来改变UI。我将着重指出这方面的多个例子。

更改图层缩略图的大小

如果您熟悉Photoshop,则可以控制缩略图大小并使其更小。当您有很多层,并希望在更少的空间中查看更多层时,这很有用。

首先层面板的主容器上有一个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);
}

Loading 进度条

管理组件的大小是通过使用属性size来完成的,CSS变量根据大小而变化。

: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属性quite存在,则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
  );
  /* And a lot more styles that I removed for the purpose of keeping the article clean. */
}

单选按钮

在这个例子中,团队使用CSS变量根据size HTML属性更改单选按钮的大小。

<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
  );
  /* And a lot more styles that I removed for the purpose of keeping the article clean. */
}

当菜单处于活动状态时锁定页面

当主菜单处于活动状态时,有一个“保持器”元素填充整个屏幕,位于菜单下方。

#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 viewport单元的用途。混合模式菜单的最大高度为55vh

sp-menu {
  max-height: 55vh;
  --mod-menu-item-min-height: auto;
}

::slotted(*) {
  overscroll-behavior: contain;
}

overscroll-behavior: contain也有用到。这是一个很好的功能,可以避免滚动正文内容。

注释组件

用户可以在画布上的任何地方钉上注释或绘图。我检查了组件,以了解它是如何构建的。

我喜欢动态定位和颜色的CSS变量

为了将每个评论放置在用户选择的位置,团队使用了通过JS提供的CSS变量来处理。

<div
  data-html2canvas-ignore="true"
  class="Pin__component ccx-annotation"
  style="
    --offset-x: 570.359375px;
    --offset-y: 74.23046875px;
    --ccx-comments-pin-color: #16878C;
  "
></div>
.Pin__component {
  --pin-diameter: 24px;
  left: calc(var(--offset-x) - var(--pin-diameter) / 2);
  top: calc(var(--offset-y) - var(--pin-diameter) / 2);
  position: absolute;
  height: var(--pin-diameter);
  width: var(--pin-diameter);
  border-radius: var(--pin-diameter);
  border: 1px solid white;
  background: var(--ccx-comments-pin-color);
}

使用SVG进行工程图标注

当你将图片缩小时,SVG笔划不会调整大小,而且看起来很粗。

据我所知,这可以通过添加vector-effect: non-scaling-stroke来解决。但我没试过。

对图层缩略图使用 Object-Fit: Contain

在图层面板中,缩略图使用object-fit: contain以避免失真。

最后

文章到此就结束了,介绍了Photoshop Web版本使用的一些CSS技术。与国内常见的CSS技术相比,有许多不同之处,其中很多部分值得学习和借鉴。当然这只是其中的一部分,如果你感兴趣,可以查看他们的源代码来深入研究。


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


南城FE
2.2k 声望571 粉丝