如何获得像素完美的线性缩放 UI

Avatar of Georgi Nikoloff
Georgi Nikoloff

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

根据视口宽度动态缩放 CSS 值并不是什么新鲜话题。您可以在 CSS-Tricks 上找到许多深入的报道,例如 这篇这篇

但是,大多数这些示例使用 相对 CSS 单位无单位值 来实现流畅缩放。这会丢失像素完美,并且通常会在屏幕低于或高于某个阈值时导致文本换行和布局偏移。

但是,如果我们真的想要像素完美怎么办?例如,如果我们正在开发一个复杂的实时分析仪表板,以便在会议室的大型电视上观看,或者作为一些仅在移动设备和平板电脑上打开的 PWA,而不是文本密集型博客和新闻网站?在这些情况下,我们需要更高的精度。

换句话说,如果我们想统一缩放设计怎么办?当然,可以根据可用宽度使用 CSS 转换来 缩放 内容,如 本文 中所述——这样,就可以保持正确的比例。

但是,我们也可以使用 CSS 中的像素值来实现流畅的比例缩放 UI。它们会根据设备屏幕尺寸进行适当缩放,同时保持其像素完美比例。此外,如果使用像素更舒适或更熟悉,我们仍然可以使用像素值并将其自动转换为相对 CSS 单位。

缩放我们的 UI

让我们尝试实现这个由 Craftwork 提供的 很棒的仪表板。我们需要以一种使其完美缩放并保留所有文本行数、边距、图像大小等的方式来制作它。

让我们使用 CSS 像素值并在 SCSS 中工作以提高速度和便利性。因此,如果我们要定位其中一个卡片小部件的标题,我们的 SCSS 可能如下所示

.cardWidget {
  .cardHeading {
    font-size: 16px;
  }
}

没什么花哨的。我们以前从未见过。作为像素值,它不会缩放。

此设计是使用宽度为 1600px 的容器创建的。让我们假设在 1600px 时,卡片标题的理想字体大小应为 16px,因为这就是它的设计方式。

现在我们有了此宽度的“理想”容器宽度字体大小,让我们使用当前*视口宽度相应地缩放我们的 CSS 像素值

/*
  1600px is the ideal viewport width that the UI designers who
  created the dashboard used when designing their Figma artboards

  Please not we are not using pixel units here, treating it purely
  as a numeric value.
*/
--ideal-viewport-width: 1600;
/*
  The actual width of the user device
*/
--current-viewport-width: 100vw;

.cardWidget {
  .cardHeading {
    /*
      16px is the ideal font size that the UI designers want for
      1600px viewport width.

      Please note that we are not using pixel units here,
      treating it purely as a numeric value.
    */
    --ideal-font-size: 16;
    /*
      Calculate the actual font size:

      We take our idealFontSize and multiply it by the difference
      between the current viewport width and the ideal viewport width.
    */
    font-size: calc(
      var(--ideal-font-size) * (var(--current-viewport-width) / var(--ideal-viewport-width)
    );
  }
}

如您所见,我们将从设计中获得的理想字体大小视为基准,并将其乘以当前和理想视口宽度之间的差值。从数学上讲,这看起来如何?假设我们在与模型具有完全相同宽度的屏幕上查看此 Web 应用程序

--current-device-width: 100vw; // represents 1600px or full width of the screen
--ideal-viewport-width: 1600; // notice that the ideal and current width match
--ideal-font-size: 16;
// this evaluates to:
font-size: calc(16 * 1600px / 1600);
// same as:
font-size: calc(16 * 1px);
// final result:
font-size: 16px;

因此,由于我们的视口宽度完全匹配,因此我们的 font-size1600px 的理想视口宽度下最终正好为 16px

再举一个例子,假设我们在宽度为 1366px 的较小笔记本电脑屏幕上查看 Web 应用程序。以下是更新后的数学计算

font-size: calc(16 * 1366px / 1600);
// same as:
font-size: calc(16 * 0.85375px);
// final result:
font-size: 13.66px;

或者假设我们在宽度为 1920px 的全高清显示屏上查看它

font-size: calc(16 * 1920px / 1600);
// same as:
font-size: calc(16 * 1.2px);
// final result:
font-size: 19.2px;

您可以亲眼看到,即使我们使用像素值作为参考,我们实际上也能够根据理想视口大小和当前视口大小之间的宽度差来按比例缩放我们的 CSS 值。

我构建了一个小型演示来说明此技术

为了方便起见,这里有一个视频

限制最小和最大视口宽度

使用这种当前方法,设计会缩放以匹配视口大小,无论视口大小如何。我们可以使用 CSS clamp() 防止这种情况,它允许我们设置 350px 的最小宽度和 3840px 的最大宽度。这意味着如果我们要在宽度为 5000px 的设备上打开 Web 应用程序,我们的布局将保持锁定在 3840px

--ideal-viewport-width: 1600;
--current-viewport-width: 100vw;
/*
  Set our minimum and maximum allowed layout widths:
*/
--min-viewport-width: 350px;
--max-viewport-width: 3840px;

.cardWidget {
  .cardHeading {
    --ideal-font-size: 16;
    font-size: calc(
      /*
        The clamp() function takes three comma separated expressions
        as its parameter, in the order of minimum value, preferred value
        and maximum value:
      */
      --clamped-viewport-width: clamp(var(--min-viewport-width), var(--current-viewport-width), var(--max-viewport-width);
      /*
        Use the clamped viewport width in our calculation
      */
      var(--ideal-font-size) * var(--clamped-viewport-width) / var(--ideal-viewport-width)
    );
  }
}

让我们为单位转换创建一个辅助函数

我们的代码非常冗长。让我们编写一个简单的 SCSS 函数,将我们的值从像素转换为相对单位。这样,我们就可以在任何地方导入和重用它,而无需重复太多

/*
  Declare a SCSS function that takes a value to be scaled and
  ideal viewport width:
*/
@function scaleValue(
  $value,
  $idealViewportWidth: 1600px,
  $min: 350px,
  $max: 3840px
) {
  @return calc(
    #{$value} * (clamp(#{$min}, 100vw, #{$max}) / #{$idealViewportWidth})
  );
}

/*
  We can then apply it on any numeric CSS value.

  Please note we are passing not pixel based, but numeric values:
*/
.myElement {
  width: #{scaleValue(500)};
  height: #{scaleValue(500)};
  box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
  font-size: #{scaleValue(24)};
}

移植到 JavaScript

有时 CSS 不够用,我们必须使用 JavaScript 来调整组件的大小。假设我们正在 动态构建 SVG,并且我们需要根据理想的设计宽度调整其 widthheight 属性的大小。以下是如何实现此目的的 JavaScript 代码

/*
  Our helper method to scale a value based on the device width
*/
const scaleValue = (value, idealViewportWidth = 1600) => {
  return value * (window.innerWidth / idealViewportWidth)
}

/*
  Create a SVG element and set its width, height and viewbox properties
*/
const IDEAL_SVG_WIDTH = 512
const IDEAL_SVG_HEIGHT = 512

const svgEl = document.createElement('svg')
/* Scale the width and height */
svgEl.setAttribute('width', scaleValue(IDEAL_SVG_WIDTH))
svgEl.setAttribute('height', scaleValue(IDEAL_SVG_WIDTH))

/*
  We don't really need to scale the viewBox property because it will
  perfectly match the ratio of the scaled width and height
*/
svg.setAttribute('viewBox', `0 0 ${IDEAL_SVG_WIDTH} ${IDEAL_SVG_HEIGHT}`)

此技术的缺点

此解决方案并不完美。例如,一个主要的缺点是 UI **不再可缩放**。无论用户放大多少,设计都会保持锁定,就像在 100% 放大状态下查看一样。

也就是说,我们可以轻松地使用传统的媒体查询,在其中为不同的视口宽度设置不同的理想数值

.myElement {
  width: #{scaleValue(500)};
  height: #{scaleValue(500)};
  box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
  font-size: #{scaleValue(24)};
  @media (min-width: 64em) {
    width: #{scaleValue(800)};
    font-size: #{scaleValue(42)};
  }
}

现在,我们可以同时受益于媒体查询和我们的像素完美线性缩放。

总结

所有这些都是实现流畅 UI 的另一种方法。我们将像素完美值视为纯数值,并将其乘以当前视口宽度和设计中的“理想”视口宽度之间的差值。

我在自己的工作中广泛使用了这种技术,希望您也能从中找到一些用途。