紧密贴合的 SVG 形状:现在和未来

Avatar of Ana Tudor
Ana Tudor 发表

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

我最近发现自己想将 SVG 形状紧密地打包到 HTML 容器中。 紧密打包是指我希望形状的最外点正好位于容器的边缘,没有任何内容超出容器或被裁剪。

以下演示显示了此示例

查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 4 点星形 – 紧密贴合容器

这个四点星形的最外顶点位于其容器的边缘。 我们使 SVG 完全覆盖其父元素,并选择了坐标系和星形点的坐标,以便 SVG 画布的 (0,0) 点正好位于中间,并且星形的最外顶点位于此坐标系的轴线上,即容器的边缘。 下面的演示说明了在这种情况下代码是如何工作的

查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 4 点星形 – 可视化代码说明

现在假设我们不希望它填充颜色,而是使用可变宽度的描边。 拖动滑块以调整星形的 stroke-width

查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 4 点星形 – 可变描边

如上所示,增加 stroke-width 会产生一个问题:描边在四点星形的初始边界内外部都延伸,导致其尺寸增大,不再适合其容器。 很明显,我们需要调整顶点的位置或更改 <svg> 元素上的 viewBox 属性。

解决方案:调整 viewBox

以下演示说明了调整 viewBox 的工作原理

查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 4 点星形 – 调整 viewBox

将滑块滑块向左拖动会减小 viewBox 的宽度和高度,始终保持它们相等,并更改 SVG 画布左上角的坐标,使其 (0, 0) 点始终位于中间。 这就像“放大”效果。

将滑块滑块向右拖动会增加 viewBox 的宽度和高度,始终保持它们相等,并更改 SVG 画布左上角的坐标,使其 (0, 0) 点始终位于中间。 这就像“缩小”效果。

在我们的例子中,为了在增加 stroke-width 后使四点星形适合,我们需要缩小,因此我们需要增加 viewBox 的尺寸。 但是,我们需要将它们增加多少才能使星形刚好接触边缘? 如果我们没有足够地缩小,那么我们的星形仍然会被裁剪。 如果我们缩小太多,那么它将不再接触边缘。 那么,正确的缩放比例的 viewBox 尺寸是多少,我们如何获得它们呢?

为了弄清楚这一点,我们需要选择一个我们想要增加 stroke-width 的精确值——假设是 20。 我们现在将仔细观察在将 stroke-width 增加到 20 后外顶点周围发生了什么

在上图中,我们的星形被表示了两次,分别是在增加 stroke-width 之前和之后。 我们可以看到,在增加 stroke-width 后,我们的星形在所有方向上都超出了初始边界 d 的距离。 这意味着我们应该将 viewbox 尺寸增加此距离 d 的两倍。

好的,但是我们如何获得 d 呢? 好吧,为了做到这一点,我们将专注于一个外顶点,比如 (250,0) 点。

我们将此点称为 P。 我们还将考虑另一个点 Q,它是我们增加 stroke-width 后星形的最右点。 它位于 x 轴上,就像 P 一样,只是在 P 的右侧 d 的距离处。 如果我们通过 P 画一条垂直线到星形的一条边上,我们得到直角三角形 PQR,其中 PQ 等于 dPR 等于我们应用的已知 stroke-width 的一半。

在这个直角三角形中,PQ 是斜边,角度 α 的正弦是 PRPQ 之间的比率。 PR 等于我们已知 stroke-width 的一半(20/2 = 10),而 PQd。 因此,从这里我们得到 d 等于 stroke-width 的一半(10)与角度 α 的正弦之比。

但是,看起来我们只是用另一个未知数替换了一个未知数,因为我们不知道 α。 然而,我们可以计算它。

我们的星形关于坐标轴对称,这意味着任何一条轴都将我们的星形分成两个完全镜像的半部分。 除此之外,当一条横截线与两条或多条平行线相交时,对应角(在每个交点处占据相同位置的角度)相等,如下面的演示所示

查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 对应角

所有这些意味着,在下图中,所有标记的角度都相等。 那些位于 x 轴上方的角度分别等于它们正下方的角度,因为星形是对称的。 所有那些位于轴线一侧的角度再次相等,因为轴线一侧的虚线是平行的(并且横截线是 x 轴)。

由于所有这些角度都相等,我们可以从另一个直角三角形计算 α——由点 P、星形的下一个顶点(我们称之为 S)及其在 x 轴上的投影(我们称之为 T)形成的三角形。 点在一条线上的投影是在该线与通过该点画出的垂直线相交处获得的。

我们知道这三个点的坐标。 从我们用来创建星形的代码中,点 P 位于 (250,0) 处,点 S 位于 (20,20) 处。

T,点 Sx 轴上的投影,具有与 S 相同的 x 坐标,而其 y 坐标为 0。 这意味着 T 位于 (20,0) 处。

知道这些坐标,我们可以计算 STPT 线段的长度。 垂直 ST 线段的长度为 20,而水平 PT 线段的长度为 250 - 20 = 230.

PST 三角形是一个直角三角形,因此 α 的正切是 STPT 线段之比。 这两条线段都是已知的,这意味着我们可以将 α 计算为它们的比率的反正切:atan(20/230)

现在我们已经计算出 α,我们可以将其替换到给我们提供我们正在寻找的距离 d 的关系中:10/sin(atan(20/230)) ≈ 115(顺便说一句,我只是在谷歌上搜索了那个结果)。 这意味着左上角点的坐标将为 (-250 - 115,-250 - 115) = (-365, -365),并且 viewbox 的宽度和高度将为 500 + 2*115 = 500 + 230 = 730. 在这种情况下,在更改 viewBox(并保持其他所有内容不变)后,我们的代码变为

<svg viewBox='-365 -365 730 730'>
  <polygon points='250,0 20,20 0,250 -20,20 -250,0 -20,-20 0,-250 20,-20'/>
</svg>

您可以在下面的笔中看到结果

查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 4 点星形 – 应用 viewBox 修正

问题

您可能已经意识到上面的演示看起来不太对。 星形仍然看起来被裁剪了,即使它现在适合 SVG 画布的可见部分。 这是怎么回事,我们如何解决?

stroke-linejoin

好吧,每当我们有一个由各种线段组成的形状在拐角处相遇时,我们就有几种选择来决定它应该是什么样子。 这些选项由一个名为 stroke-linejoin 的属性控制。 此属性可以取以下四个值之一

  • miter:(默认值)——线段以锐角相交
  • round:拐角被圆角化
  • bevel:看起来有点像 miter,但顶点被切掉
  • inherit:其父组使用的任何值

此笔提供了一些实际示例

查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 stroke-linejoin 值

现在,如果我们仔细查看上面的笔,我们会注意到第一列最后一个连接处的一些有趣的东西。 它具有 stroke-linejoin: miter,但视觉效果与第一列(miter 列)上的其他连接不同。 事实上,它看起来就像我们在最后一列(bevel 列)的最后一个连接处得到的结果。 而且,它看起来就像我们此时在星形上遇到的问题。 我们的线连接应该miter,因为我们没有将 stroke-linejoin 设置为任何其他值。 但是,我们看到的是 bevel

stroke-miterlimit

这是因为一个名为stroke-miterlimit的属性。顾名思义,此属性限制了斜接可以扩展的程度。当超出此限制时,连接会简单地从斜接转换为斜角。

注意:SVG2 规定line-join将获得几个其他可能的值,其中一个是miter-clip,它将使连接在限制值处裁剪,而不是斜角。

下面的演示显示,两段之间的角度越小,miter通常扩展得越多,如果设置的stroke-miterlimit足够高,以至于它永远不会转换为bevel。它还显示了miter转换为斜角的角度如何随着stroke-miterlimit的值增加减小

查看 Ana Tudor 在 CodePen 上的笔 stroke-miterlimit (@thebabydino)。

好的,但是stroke-miterlimit和实际的斜接长度之间有什么关系呢?在之前的演示中,stroke-miterlimit的值范围从215,而实际的斜接长度超过50,可以达到数百。斜接长度还取决于stroke-width,因此很明显,施加的限制不是对实际的斜接长度本身。相反,此限制是对斜接长度与笔划宽度之比的限制。并且,如下面的图像所示,此比率等于连接线的两条线之间的一半角度的正弦的倒数。

锐角 (< 90°) 的正弦随着角度的增大而增大,因此正弦的倒数(我们对其施加stroke-miterlimit设置的限制的比率)将随着角度的减小而增大。

查看 Ana Tudor 在 CodePen 上的笔
How sin(θ) and 1/sin(θ) change with θ
(@thebabydino)。

这在某种程度上解释了连接如何从miter转换为bevel。如果角度非常接近0,则其正弦也非常接近0,使该值的倒数成为一个非常大的数字,并使斜接扩展很多。而且,虽然我没有关于此的任何实际指标,但我认为这对性能肯定不利。

修复星形演示

现在回到我们的星星,它的角度并不那么接近零,为了防止连接转换为斜角,我们需要做的就是在之前计算的 α 角的正弦的倒数处设置stroke-miterlimit。α 为atan(20/230),所以我们想要的值是1/sin(atan(20/230)) ≈ 11.5,为了确保,我们将设置stroke-miterlimit: 12。下面的笔显示了如何简单地将此一行添加到 CSS 中即可解决所有问题。

查看 Ana Tudor 在 CodePen 上的笔 4 point star – viewBox & stroke-mitterlimit corrections applied (@thebabydino)。

注意:如果我们不想进行所有这些计算,那么我们可以简单地将stroke-miterlimit设置为一个非常高的值。到底有多高?好吧,对于连接线的两条线之间的角度,60就足够了,而且我们不太可能经常需要超过这个值。

问题

好的,但是这种通过更改viewBox并在因此缩小来重新调整星形在其容器内的方法也使笔划看起来比我们最初打算的要细。我们可以尝试使用vector-effect: non-scaling-stroke来修复此问题,但这只会产生更多问题,因为当 SVG 的容器以及 SVG 本身的大小发生变化时,笔划宽度将不再缩放。

因此,如果我们想解决这个问题,我们必须保持viewBox不变,只需调整星形顶点的坐标。

解决方案:调整顶点的坐标

我们知道如果设置stroke-width: 20,星形会向外扩展多少。我们已计算出此距离d约为115。这意味着我们必须将最外层的顶点向内移动115,从沿轴线的250250 - 115 = 135。我们不能只更改最外层顶点的坐标,因为这会扭曲我们的星形,同时更改角度会改变斜接扩展的程度,并且星形也不会再紧密排列了。

查看 Ana Tudor 在 CodePen 上的笔 4 point star – adjusting the outer vertices (@thebabydino)。

相反,我们需要做的首先是计算一个缩放因子,该因子将是最外层顶点的新非零坐标与旧坐标之比:135/250 = .54。然后我们将其他顶点的旧坐标乘以此因子以获得其新坐标(20*.54 = 10.8)

查看 Ana Tudor 在 CodePen 上的笔 4 point star – adjusting all vertices (@thebabydino)。

而且……就是这样!您可以在此处查看最终演示

查看 Ana Tudor 在 CodePen 上的笔 4 point star – vertex corrections applied (@thebabydino)。

未来解决方案:stroke-alignment

当然,未来应该会带来很多美好的事物,包括避免所有这些计算可能给某些人带来的痛苦。SVG2 将允许我们指定一个stroke-alignment——没错,控制增加stroke-width是否会使笔划向内、向外或在元素轮廓的两侧扩展将只需一行代码即可实现。

在我们的例子中,那将是stroke-alignment: inner。然后,砰!不需要其他技巧,也不需要通过调整viewBox来缩放,也不需要重新定位我们形状的点……不再需要任何这些了。

但是,在此期间,让我们看看我一直在将它用于什么。

用例:3D!

两年多前,我开始使用 CSS 3D 变换来构建各种 3D 形状。非矩形平面(具有三、五、六或十个边)是通过嵌套 2D 变换的 HTML 元素并使用overflow: hidden剪裁不需要的部分来创建的。在过去的几个月里,我也开始使用 SVG,并开始考虑重新制作这些 3D 形状,这次使用 SVG <polygon>作为非矩形面。

注意:如果您需要复习基本几何知识,此笔解释了多边形是什么,并提供了一些示例。

这就是我遇到上面提出的问题的地方,因为我在 3D 中移动 2D 面积的程度取决于它们的 2D 尺寸。因此,在我看来,最简单的解决方案是在 SVG 的正中间放置我的正多边形的圆周圆(所有顶点都位于其上的圆),并且至少有一个顶点位于边缘上。由于 SVG 元素将具有与其 HTML 容器相同的尺寸,因此这将意味着 HTML 容器的一半尺寸将等于内部 SVG 多边形的圆周半径。

请注意,并非每个多边形都有一个圆周圆(但所有正多边形和所有三角形都有)。

因此,让我们看看我是如何通过创建最简单的 3D 形状——棱柱来处理所有这些的!棱柱是多面体(具有平面和直边的 3D 固体),具有两个n边形底面,由n个四边形面连接而成。为简单起见,我们将所有侧面视为矩形,并且底面为正多边形(所有角度都相等,所有边长都相等)。下面的演示允许创建和展平多个棱柱。

查看 Ana Tudor 在 CodePen 上的笔 build a prism (@thebabydino)。

矩形面很容易获得。HTML 元素默认情况下是矩形,因此我们只需为每个矩形面使用一个元素。但是底面呢?

好吧,对于每个底面,我们将取一个正方形(相等的widthheight,最好使用vmin单位,以便它们随视口缩放)HTML 元素,并在其中包含一个 SVG。<svg>元素完全覆盖其容器(widthheight都设置为100%),并且内部的polygon接触边缘,但没有任何内容被剪裁。由于两个底面相同,因此我们只需定义一次polygon,然后在每个底面中引用它。

<svg height='0' width='0'>
  <defs>
    <polygon id='basepoly'/>
  </defs>
</svg>

<div class='prism'>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>

  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>

  
</div>

但是,在这种情况下,我们最终会在底面内的两个 SVG 元素上设置相同的viewBox,因此此版本的改进之处在于使用<symbol>,这样我们只需在一个地方(在<symbol>元素上)设置viewBox即可。

<svg height='0' width='0'>
  <symbol id='basepoly'>
    <polygon/>
  </symbol>
</svg>

<div class='prism'>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>
  <div class='prism__face--base'>
    <svg>
      <use xlink:href='#basepoly'/>
    </svg>
  </div>

  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>
  <div class='prism__face--lateral'></div>

  <!-- more lateral faces if needed -->
</div>

基本的 CSS 代码会将所有几何元素绝对定位在其容器的中央。我们将 3D 形状的父元素(在本例中为<body>元素)设置为场景,方法是在其上设置透视。由于棱柱是绝对定位的,因此我们还必须为<body>设置height,并且由于我们没有其他内容,因此我们将使 body 覆盖整个视口。我们还将为面元素提供明确的宽度和高度,并通过使用负边距确保它们位于正中间。并且鉴于我们可能希望在 3D 中动画化棱柱本身,因此我们将在其上设置transform-style: preserve-3d

body {
  height: 100vh;
  perspective: 32em;
}

[class*=prism] {
  position: absolute;
  top: 50%; left: 50%;
}

.prism { transform-style: preserve-3d; }

.prism__face--base {
  margin: -13vmin;
  width: 26vmin; height: 26vmin;
}

.prism__face--base svg {
  width: 100%;
  height: 100%;
}

.prism__face--lateral {
  margin: -16vmin -13vmin;
  width: 26vmin; height: 32vmin;
}

现在让我们看看如何获得紧密贴合的正多边形。我们从这样一个事实开始:正多边形的所有顶点都位于一个称为外接圆的圆上。但是我们究竟如何在圆上定位顶点呢?

在任何其他操作之前,我们需要知道圆是什么样子的。一个完整的圆有360°,我们从x轴的正方向(3点钟方向)开始。

查看 Ana Tudor 在 CodePen 上的钢笔 full circle – responsive SVG explanation (@thebabydino)。

如果我们的多边形有n条等长的边,则对应于一条边的弧将具有一个完整圆的度数(360°)除以n。对于等边三角形,n为 3,因此角度为120°。对于正方形,n为 4,因此角度为90°。对于正五边形,n5,因此角度为72°

现在,如果我们从开始,以n步绕圆走,那么在每一步我们都有一个多边形顶点,如下面的演示所示。

查看 Ana Tudor 在 CodePen 上的钢笔 construct regular polygon (@thebabydino)。

对于三角形,n3,因此每一步都是360°/3 = 120°。这将等边三角形的三个顶点置于120°240°。对于正方形,n4,使每一步为360°/4 = 90°,并将四个顶点置于90°180°270°。对于正五边形,n5,角度步长为360°/5 = 72°,顶点位于72°144°216°288°

现在我们需要这些顶点的坐标才能绘制我们的多边形。平面中一个点的坐标由连接该点到原点的线段的长度(在本例中,为我们圆的半径r)以及该线段与x轴之间的角度(在本例中,为角度步长的i倍,其中i是顶点的索引)给出。

xi = r*cos(i*360°/n);
yi = r*sin(i*360°/n);

如果我们知道顶点/边的数量,那么计算角度很容易,但是半径是多少呢?好吧,不考虑<polygon>元素的stroke-width,这将是我们正方形 SVG viewBox尺寸的一半(我们假设为800)。调整stroke-width将意味着从该值中减去斜接长度的一半,就像我们之前对四点星所做的那样。

为此,我们需要计算正多边形的角度,因为斜接长度取决于stroke-width和多边形角度。我们知道三角形中的角度总和为180°(您可以在演示中拖动顶点以玩弄三角形)。

查看 Ana Tudor 在 CodePen 上的钢笔 triangle angles add up to 180° (@thebabydino)。

了解这一点,并知道三角形中有三个角,我们得到正(或等边)三角形的角度为60°。但是其他正多边形呢?好吧,如果我们将多边形的第一个顶点连接到所有其他顶点,我们可以看到我们已将多边形分成n - 2个三角形。

查看 Ana Tudor 在 CodePen 上的钢笔 every convex n-polygon can be split into n-2 triangles (@thebabydino)。

这意味着具有n个顶点/边的多边形的角度总和为(n - 2)*180°。鉴于这个正多边形的全部n个角都相等,我们得到每个角都是(n - 2)*180°/n。如果我们实际进行计算,我们会发现等边三角形为60°,正方形为90°,正五边形为108°,正六边形为120°,依此类推……

现在我们拥有了设置基本多边形points属性所需的所有数据。

var r = 400 /* circumradius, no correction, half of 800 */, 
    sw = 20 /* stroke-width */, 
    n = 3 /* number of edges */, 

    sym = document.getElementById('basepoly'), 
    poly = sym.querySelector('polygon'), 
    vb = [-r, -r, 2*r, 2*r], 
    points_attr_text = '', 

    base_angle = 2*Math.PI/n, 
    poly_angle = (n - 2)*Math.PI/n, 
    correction = .5*sw/Math.sin(.5*poly_angle), 
    r_final = r - correction;

/* set the viewBox */
/* "viewBox", not "viewbox" (won't work) */
sym.setAttribute('viewBox', vb.join(' '));

for(var i = 0; i < n; i++) {
    curr_angle = i*base_angle;
    x = r_final*Math.cos(curr_angle);
    y = r_final*Math.sin(curr_angle);

    points_attr_text += x + ',' + y + ' ';
}

poly.setAttribute('points', points_attr_text);
poly.setAttribute('stroke-width', sw);

您可以在以下钢笔中看到此代码的工作情况。您可以调整边/顶点数、外接半径或stroke-width的值以获得不同的结果。我们可以通过各种方法将这些多边形包装得更紧密,但随后我们将失去对任意数量的边/顶点使用简单一致的方法的优势。

查看 Ana Tudor 在 CodePen 上的钢笔 n-polygon touching the edges of the SVG container (@thebabydino)。

或者,我们可以使用 Jade 获得完全相同的结果

- var n = 3;
- var sw = 20;
- var r = 400;

- var base_angle = 2*Math.PI/n;
- var poly_angle = (n - 2)*Math.PI/n;
- var correction = .5*sw/Math.sin(.5*poly_angle);
- var r_final = r - correction;

mixin polygon(n)
    - var points = '';
    - for(var i = 0; i < n; i++) {
        - var curr_angle = i*base_angle;
        - var x = r_final*Math.cos(curr_angle)
        - var y = r_final*Math.sin(curr_angle);
        - points += ~~x + ',' + ~~y + ' ';
    - }
    polygon(points=points stroke-width='#{sw}')

svg(width='0' height='0')
    symbol(id='basepoly' 
                 viewbox='#{-r} #{-r} #{2*r} #{2*r}')
        +polygon(n)

svg(class='vis')
    use(xlink:href='#basepoly')

我们采取的下一步是将其与我们最初的代码结合起来,并在创建多边形的同时自动生成侧面。您可以在下面的钢笔中看到结果(或者,如果您更喜欢使用 Jade 的方法,请务必查看此版本)。

查看 Ana Tudor 在 CodePen 上的钢笔 generate prism faces (JS) (@thebabydino)。

好的,但此时,我们所有的面都简单地堆叠在屏幕中央的彼此之上,因此我们需要在 3D 中定位它们,以使它们形成棱柱。

让我们从底面开始。目前,它们位于屏幕的垂直平面内,我们希望它们位于垂直于屏幕的水平平面内;一个朝上,另一个朝下。为此,我们将对我们想要朝上的一个应用rotateX(90deg)变换,对我们想要朝下的一个应用rotateX(-90deg)变换。对元素应用旋转也会旋转其坐标系,因此现在z轴(最初是从屏幕出来,朝向我们的轴)分别指向两个旋转面的上下。这些面现在是水平的,但它们在中间切割垂直侧面。因此,我们需要做的是沿垂直方向(一个向上,另一个向下)平移它们,平移量为侧面高度的一半(在本例中我们将其设为32vmin,因此一半为16vmin)。在旋转后沿垂直方向平移意味着沿z轴平移(旋转后使其变为垂直),因此我们必须应用translateY变换。此代码将是

.prism__face--base:nth-child(1) {
  transform: rotateX(90deg) translateZ(16vmin); /* up */
}
.prism__face--base:nth-child(2) {
  transform: rotateX(-90deg) translateZ(16vmin); /* down */
}

添加这两个规则集后,两个底面现在已就位。

查看 Ana Tudor 在 CodePen 上的钢笔 position prism bases (JS) (@thebabydino)。

定位侧面意味着首先围绕其y轴旋转它们(使用rotateY变换),使它们分别面向底面的一个边,然后将其向外平移(使用translateZ())到内切圆半径。但是内切圆半径究竟是什么?好吧,它是圆的半径(称为内切圆),该圆恰好在一个点上与多边形的每条边相切(每条边都与内切圆相切)。就像外接圆一样,并非所有多边形都有内切圆,但所有正多边形(就像我们在棱柱底面情况下处理的多边形)都有。

此外,在正多边形的情况下,外接圆和内切圆的圆心位于同一点,而外接半径和内切半径将多边形的角和相应的边分成两半。以图形方式表示所有这些内容,我们得到类似于此钢笔中的内容

查看 Ana Tudor 在 CodePen 上的钢笔 in/circumcircle of a regular polygon – WORK IN PROGRESS (SVG version) (@thebabydino)。

这里,外接半径是连接中心点到多边形顶点的线段,内切半径是连接中心点到多边形边中点的线段。鉴于多边形的边与内切圆相切,演示中绘制的内切半径垂直于多边形的边。

如果我们取一个由内切半径、外接半径和多边形边的一半组成的三角形,则该三角形是一个直角三角形,并且允许我们计算相对于外接半径和多边形角度的内切半径。

查看 Ana Tudor 在 CodePen 上的钢笔 relation betwen circumradius, inradius, edge length, polygon angle (@thebabydino)。

在上面的演示中,这是OVM三角形。OV是外接半径,OM是内切半径,VM是正多边形边的一半。VMO角是直角(这使得对边OV成为斜边),OVM角是多边形角度的一半。OVM角的正弦是OMOV的比率,我们将使用此关系来计算OM,因为我们已经知道OVOVM角。

外接半径是我们已在 CSS 中设置的基本面的尺寸的一半,我们已经有了多边形的角度,所以这就是我们需要的所有内切半径值。

base_face = prism.querySelector('.prism__face--base');
cradius = .5*getComputedStyle(base_face).width.split('px')[0];
iradius = cradius*Math.sin(.5*poly_angle);

现在我们只需为每个侧面设置正确的变换即可。

curr_y_rot = i*base_angle + .5*poly_angle;
curr_lat_face.style.transform = 
    'rotateY(' + curr_y_rot + 
    'translateZ(' + iradius + 'px)';

好的,这越来越接近了。

查看 Ana Tudor 在 CodePen 上的钢笔 position prism faces stage #1 (JS) (@thebabydino)。

我们仍然存在的问题是侧面的宽度仍然不正确。我们已将其设置为等于基本面的尺寸,但基本面是基本多边形外接半径的两倍,这与基本多边形的边不相等。但我们可以从与内切半径相同的三角形中计算边

edge_len = 2*cradius*Math.cos(.5*poly_angle);

…然后为侧面设置正确的宽度和margin-left。

curr_lat_face.style.width = edge_len + 'px';
curr_lat_face.style.marginLeft = -.5*edge_len + 'px';

现在我们得到了一个漂亮的棱镜,你可以在下面的 Pen 中查看(或者在它的Jade 版本中)。

查看 Ana Tudor 在 CodePen 上创建的 Pen position prism faces full (JS) (@thebabydino)。

最棒的是,更改边的数量就像更改 JS 中的 n 变量一样简单——自己试试吧!还有一些小问题需要解决。我们实际上不需要在每个侧面都内联设置相同的 widthmargin-left——我们可以简单地将它们放入一个样式元素中。此外,在调整大小后,我们需要更新这些值以及侧面上的转换,因为它们取决于底部的像素尺寸,我们已使用视口单位设置了这些尺寸,以便棱镜在调整大小时进行缩放。这个最终的 Pen 修复了所有这些小问题。

查看 Ana Tudor 在 CodePen 上创建的 Pen position prism faces final (JS) (@thebabydino)。