Logo Zephyrnet

Một CSS Slinky trong 3D? Đã chấp nhận thử thách!

Ngày:

Braydon Coyer gần đây đã ra mắt một tháng Thử thách nghệ thuật CSS. Anh ấy thực sự đã liên hệ với tôi về việc tặng một bản sao cuốn sách của tôi Di chuyển mọi thứ với CSS để sử dụng làm giải thưởng cho người chiến thắng trong thử thách - điều mà tôi rất vui khi làm được điều đó!

Thử thách của tháng đầu tiên? Mùa xuân. Và khi nghĩ đến việc phải làm gì cho thử thách, Slinkys ngay lập tức nghĩ đến. Bạn biết Slinkys, phải không? Đồ chơi cổ điển đó bạn đập xuống cầu thang và nó di chuyển theo động lực của riêng mình.

Một slinky slinky

Chúng ta có thể tạo một Slinky đi xuống cầu thang như vậy trong CSS không? Đó chính xác là loại thử thách mà tôi thích, vì vậy tôi nghĩ chúng ta có thể giải quyết nó cùng nhau trong bài viết này. Chuẩn bị để lăn? (Dự định chơi chữ.)

Thiết lập HTML Slinky

Hãy làm cho điều này linh hoạt. (Không có ý định chơi chữ.) Ý tôi muốn nói là chúng tôi muốn có thể kiểm soát hành vi của Slinky thông qua các thuộc tính tùy chỉnh CSS, mang lại cho chúng tôi sự linh hoạt trong việc hoán đổi các giá trị khi cần.

Đây là cách tôi sắp đặt bối cảnh, viết bằng Pug cho ngắn gọn:

- const RING_COUNT = 10;
.container
  .scene
    .plane(style=`--ring-count: ${RING_COUNT}`)
      - let rings = 0;
      while rings < RING_COUNT
        .ring(style=`--index: ${rings};`)
        - rings++;

Các thuộc tính tùy chỉnh nội tuyến đó là một cách dễ dàng để chúng tôi cập nhật số lượng vòng và sẽ rất hữu ích khi chúng tôi tiến sâu hơn vào thử thách này. Đoạn mã trên cung cấp cho chúng tôi 10 vòng với HTML trông một cái gì đó như thế này khi biên dịch:

<div class="container">
  <div class="scene">
    <div class="plane" style="--ring-count: 10">
      <div class="ring" style="--index: 0;"></div>
      <div class="ring" style="--index: 1;"></div>
      <div class="ring" style="--index: 2;"></div>
      <div class="ring" style="--index: 3;"></div>
      <div class="ring" style="--index: 4;"></div>
      <div class="ring" style="--index: 5;"></div>
      <div class="ring" style="--index: 6;"></div>
      <div class="ring" style="--index: 7;"></div>
      <div class="ring" style="--index: 8;"></div>
      <div class="ring" style="--index: 9;"></div>
    </div>
  </div>
</div>

CSS Slinky ban đầu

Chúng tôi sẽ cần một số phong cách! Những gì chúng tôi muốn là một cảnh ba chiều. Tôi lưu ý đến một số điều chúng ta có thể muốn làm sau này, vì vậy đó là suy nghĩ đằng sau việc có thêm một thành phần trình bao bọc với .scene lớp học.

Hãy bắt đầu bằng cách xác định một số thuộc tính cho cảnh “infini-slinky” của chúng ta:

:root {
  --border-width: 1.2vmin;
  --depth: 20vmin;
  --stack-height: 6vmin;
  --scene-size: 20vmin;
  --ring-size: calc(var(--scene-size) * 0.6);
  --plane: radial-gradient(rgb(0 0 0 / 0.1) 50%, transparent 65%);
  --ring-shadow: rgb(0 0 0 / 0.5);
  --hue-one: 320;
  --hue-two: 210;
  --blur: 10px;
  --speed: 1.2s;
  --bg: #fafafa;
  --ring-filter: brightness(1) drop-shadow(0 0 0 var(--accent));
}

Các thuộc tính này xác định các đặc điểm của Slinky và khung cảnh của chúng ta. Với phần lớn các cảnh 3D CSS, chúng tôi sẽ thiết lập transform-style mọi mặt:

* {
  box-sizing: border-box;
  transform-style: preserve-3d;
}

Bây giờ chúng tôi cần phong cách cho .scene. Bí quyết là dịch .plane vì vậy, có vẻ như CSS Slinky của chúng tôi đang di chuyển vô hạn xuống một bậc thang. Tôi đã phải chơi xung quanh để có được mọi thứ chính xác theo cách tôi muốn, vì vậy hãy chấp nhận con số kỳ diệu ngay bây giờ, vì chúng sẽ có ý nghĩa sau này.

.container {
  /* Define the scene's dimensions */
  height: var(--scene-size);
  width: var(--scene-size);
  /* Add depth to the scene */
  transform:
    translate3d(0, 0, 100vmin)
    rotateX(-24deg) rotateY(32deg)
    rotateX(90deg)
    translateZ(calc((var(--depth) + var(--stack-height)) * -1))
    rotate(0deg);
}
.scene,
.plane {
  /* Ensure our container take up the full .container */
  height: 100%;
  width: 100%;
  position: relative;
}
.scene {
  /* Color is arbitrary */
  background: rgb(162 25 230 / 0.25);
}
.plane {
  /* Color is arbitrary */
  background: rgb(25 161 230 / 0.25);
  /* Overrides the previous selector */
  transform: translateZ(var(--depth));
}

Có một chút công bằng đang diễn ra ở đây với .container sự biến đổi. Đặc biệt:

  • translate3d(0, 0, 100vmin): Điều này mang lại .container chuyển tiếp và ngăn công việc 3D của chúng tôi không bị cắt bỏ bởi cơ thể. Chúng tôi không sử dụng perspective ở cấp độ này, vì vậy chúng tôi có thể thoát khỏi nó.
  • rotateX(-24deg) rotateY(32deg): Điều này xoay cảnh dựa trên sở thích của chúng tôi.
  • rotateX(90deg): Điều này xoay .container bằng một phần tư lượt, điều này làm phẳng .scene.plane theo mặc định, Nếu không, hai lớp sẽ giống như trên cùng và dưới cùng của một hình khối 3D.
  • translate3d(0, 0, calc((var(--depth) + var(--stack-height)) * -1)): Chúng ta có thể sử dụng điều này để di chuyển cảnh và căn giữa nó trên trục y (thực ra là trục z). Đây là con mắt của nhà thiết kế. Ở đây, chúng tôi đang sử dụng --depth--stack-height để tập trung mọi thứ.
  • rotate(0deg): Mặc dù hiện tại không được sử dụng, chúng tôi có thể muốn xoay cảnh hoặc tạo hoạt ảnh cho việc quay của cảnh sau đó.

Để hình dung những gì đang xảy ra với .container, hãy kiểm tra bản trình diễn này và nhấn vào bất kỳ đâu để xem transform đã áp dụng (xin lỗi, chỉ Chromium. 😭):

Bây giờ chúng ta có một cảnh theo kiểu! 💪

Tạo kiểu cho nhẫn của Slinky

Đây là nơi các thuộc tính tùy chỉnh CSS đó sẽ đóng vai trò của chúng. Chúng tôi có các thuộc tính nội tuyến --index--ring-count từ HTML của chúng tôi. Chúng tôi cũng có các thuộc tính được xác định trước trong CSS mà chúng tôi đã thấy trước đó trên :root.

Các thuộc tính nội tuyến sẽ đóng một phần trong việc định vị mỗi vòng:

.ring {
  --origin-z:
    calc(
      var(--stack-height) - (var(--stack-height) / var(--ring-count))
      * var(--index)
    );
  --hue: var(--hue-one);
  --accent: hsl(var(--hue) 100% 55%);
  height: var(--ring-size);
  width: var(--ring-size);
  border-radius: 50%;
  border: var(--border-width) solid var(--accent);
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%;
  transform:
    translate3d(-50%, -50%, var(--origin-z))
    translateZ(0)
    rotateY(0deg);
}
.ring:nth-of-type(odd) {
  --hue: var(--hue-two);
}

Hãy lưu ý về cách chúng tôi đang tính toán --origin-z giá trị cũng như cách chúng tôi định vị mỗi vòng với transform bất động sản. Điều đó xảy ra sau khi định vị mỗi vòng bằng position: absolute .

Cũng cần lưu ý cách chúng ta xen kẽ màu của từng vòng trong bộ quy tắc cuối cùng đó. Khi tôi lần đầu tiên thực hiện điều này, tôi muốn tạo ra một cầu vồng lấp lánh nơi các vòng đi qua các màu sắc. Nhưng điều đó làm tăng thêm một chút phức tạp cho hiệu ứng.

Bây giờ chúng tôi có một số chiếc nhẫn được huy động .plane:

Biến đổi các vòng Slinky

Đã đến lúc mọi thứ chuyển động! Bạn có thể nhận thấy rằng chúng tôi đặt một transform-origin trên mỗi .ring như thế này:

.ring {
  transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%;
}

Điều này dựa trên .scene kích thước. Điều đó 0.2 giá trị là một nửa kích thước có sẵn còn lại của .scene sau .ring được định vị.

Chắc chắn chúng tôi có thể dọn dẹp điều này một chút!

:root {
  --ring-percentage: 0.6;
  --ring-size: calc(var(--scene-size) * var(--ring-percentage));
  --ring-transform:
    calc(
      100%
      + (var(--scene-size) * ((1 - var(--ring-percentage)) * 0.5))
    ) 50%;
}

.ring {
  transform-origin: var(--ring-transform);
}

Tại sao điều đó transform-origin? Chà, chúng ta cần chiếc nhẫn giống như đang di chuyển lệch tâm. Chơi với transform của một chiếc nhẫn riêng lẻ là một cách tốt để tìm ra transform chúng tôi muốn áp dụng. Di chuyển thanh trượt trên bản trình diễn này để xem vòng lật:

Thêm tất cả các vòng trở lại và chúng ta có thể lật toàn bộ ngăn xếp!

Hmm, nhưng họ không rơi xuống cầu thang tiếp theo. Làm thế nào chúng ta có thể làm cho mỗi vòng rơi xuống đúng vị trí?

Vâng, chúng tôi có một tính toán --origin-z, vậy hãy tính toán --destination-z vì vậy độ sâu thay đổi khi các vòng transform. Nếu chúng ta có một chiếc nhẫn ở trên cùng của ngăn xếp, nó sẽ cuộn lại ở dưới cùng sau khi nó rơi xuống. Chúng tôi có thể sử dụng các thuộc tính tùy chỉnh của mình để xác định một điểm đến cho mỗi vòng:

ring {
  --destination-z: calc(
    (
      (var(--depth) + var(--origin-z))
      - (var(--stack-height) - var(--origin-z))
    ) * -1
  );
  transform-origin: var(--ring-transform);
  transform:
    translate3d(-50%, -50%, var(--origin-z))
    translateZ(calc(var(--destination-z) * var(--flipped, 0)))
    rotateY(calc(var(--flipped, 0) * 180deg));
}

Bây giờ hãy thử di chuyển ngăn xếp! Chúng tôi đang tới đó. 🙌

Hoạt hình những chiếc nhẫn

Chúng ta muốn chiếc nhẫn của chúng ta lật và sau đó rơi xuống. Lần thử đầu tiên có thể trông giống như sau:

.ring {
  animation-name: slink;
  animation-duration: 2s;
  animation-fill-mode: both;
  animation-iteration-count: infinite;
}

@keyframes slink {
  0%, 5% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(0deg);
  }
  25% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(180deg);
  }
  45%, 100% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(var(--destination-z))
      rotateY(180deg);
  }
}

Oof, điều đó không đúng chút nào!

Nhưng đó chỉ là vì chúng tôi không sử dụng animation-delay. Tất cả những chiếc nhẫn là, ừm, sự trượt dài đồng thời. Hãy giới thiệu một animation-delay dựa trên --index của vòng để chúng trượt liên tiếp.

.ring {
  animation-delay: calc(var(--index) * 0.1s);
}

OK, điều đó thực sự là "tốt hơn." Nhưng thời gian vẫn còn sai lệch. Tuy nhiên, điều nổi bật hơn cả là khuyết điểm của animation-delay. Nó chỉ được áp dụng cho lần lặp lại hoạt ảnh đầu tiên. Sau đó, chúng tôi mất tác dụng.

Tại thời điểm này, hãy tô màu các vòng để chúng tiến triển qua bánh xe màu. Điều này sẽ giúp bạn dễ dàng xem những gì đang diễn ra.

.ring {
  --hue: calc((360 / var(--ring-count)) * var(--index));
}

Cái đó tốt hơn! ✨

Quay lại vấn đề. Bởi vì chúng tôi không thể chỉ định độ trễ áp dụng cho mỗi lần lặp, chúng tôi cũng không thể đạt được hiệu quả như mong muốn. Đối với Slinky của chúng tôi, nếu chúng tôi có thể có một animation-delay, chúng tôi có thể đạt được hiệu quả mà chúng tôi muốn. Và chúng tôi có thể sử dụng một khung hình chính trong khi dựa vào các thuộc tính tùy chỉnh trong phạm vi của chúng tôi. Ngay cả một animation-repeat-delay có thể là một bổ sung thú vị.

Chức năng này có sẵn trong các giải pháp hoạt ảnh JavaScript. Ví dụ: GreenSock cho phép bạn chỉ định một delayrepeatDelay.

Tuy nhiên, ví dụ Slinky của chúng tôi không phải là điều dễ dàng nhất để minh họa vấn đề này. Hãy chia điều này thành một ví dụ cơ bản. Hãy xem xét hai hộp. Và bạn muốn chúng quay luân phiên.

Làm cách nào để chúng ta thực hiện điều này với CSS và không có "thủ thuật"? Một ý tưởng là thêm độ trễ vào một trong các hộp:

.box {
  animation: spin 1s var(--delay, 0s) infinite;
}
.box:nth-of-type(2) {
  --delay: 1s;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

Nhưng, điều đó sẽ không hiệu quả vì hộp màu đỏ sẽ tiếp tục quay. Và màu xanh lam sau chữ đầu tiên của nó cũng vậy animation-delay.

Với một cái gì đó như Vớ xanhtuy nhiên, chúng ta có thể đạt được hiệu quả mà chúng ta muốn một cách tương đối dễ dàng:

import gsap from 'https://cdn.skypack.dev/gsap'

gsap.to('.box', {
  rotate: 360,
  /**
   * A function based value, means that the first box has a delay of 0 and
   * the second has a delay of 1
  */
  delay: (index) > index,
  repeatDelay: 1,
  repeat: -1,
  ease: 'power1.inOut',
})

Và nó đây!

Nhưng làm thế nào chúng ta có thể làm điều này nếu không có JavaScript?

Chà, chúng ta phải "hack" @keyframes và hoàn toàn loại bỏ animation-delay. Thay vào đó, chúng tôi sẽ đưa ra @keyframes với không gian trống. Điều này đi kèm với nhiều câu hỏi khác nhau, nhưng hãy tiếp tục và xây dựng một khung hình chính mới trước. Thao tác này sẽ xoay hoàn toàn phần tử hai lần:

@keyframes spin {
  50%, 100% {
    transform: rotate(360deg);
  }
}

Nó giống như chúng tôi đã cắt khung hình chính thành một nửa. Và bây giờ chúng ta sẽ phải tăng gấp đôi animation-duration để có được cùng một tốc độ. Không cần sử dụng animation-delay, chúng tôi có thể thử thiết lập animation-direction: reverse trên hộp thứ hai:

.box {
  animation: spin 2s infinite;
}

.box:nth-of-type(2) {
  animation-direction: reverse;
}

Hầu hết.

Vòng quay là vòng quay sai cách. Chúng tôi có thể sử dụng một phần tử wrapper và xoay nó, nhưng điều đó có thể trở nên phức tạp vì có nhiều thứ cần cân bằng hơn. Cách tiếp cận khác là tạo hai khung hình chính thay vì một khung hình:

@keyframes box-one {
  50%, 100% {
    transform: rotate(360deg);
  }
}
@keyframes box-two {
  0%, 50% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

Và chúng tôi đã có nó:

Điều này sẽ dễ dàng hơn rất nhiều nếu chúng ta có một cách để chỉ định độ trễ lặp lại bằng một cái gì đó như sau:

/* Hypothetical! */
animation: spin 1s 0s 1s infinite;

Hoặc nếu độ trễ lặp lại phù hợp với độ trễ ban đầu, chúng ta có thể có một bộ tổ hợp cho nó:

/* Hypothetical! */
animation: spin 1s 1s+ infinite;

Chắc chắn nó sẽ tạo nên một sự bổ sung thú vị!

Vì vậy, chúng ta cần keyframe cho tất cả những chiếc nhẫn đó?

Có, đó là, nếu chúng ta muốn có một độ trễ nhất quán. Và chúng ta cần làm điều đó dựa trên những gì chúng ta sẽ sử dụng làm cửa sổ hoạt ảnh. Tất cả các vòng cần phải được "slinked" và cố định trước khi các khung hình chính lặp lại.

Điều này sẽ rất kinh khủng nếu viết ra bằng tay. Nhưng đây là lý do tại sao chúng ta có các bộ tiền xử lý CSS, phải không? Chà, ít nhất là cho đến khi chúng tôi nhận được các vòng lặp và một số tính năng thuộc tính tùy chỉnh bổ sung trên web. 😉

Vũ khí được lựa chọn hôm nay sẽ là bút Stylus. Đó là bộ tiền xử lý CSS yêu thích của tôi và đã tồn tại một thời gian. Thói quen có nghĩa là tôi chưa chuyển đến Sass. Thêm vào đó, tôi thích sự thiếu ngữ pháp và tính linh hoạt cần thiết của Stylus.

Điều tốt là chúng tôi chỉ cần viết điều này một lần:

// STYLUS GENERATED KEYFRAMES BE HERE...
$ring-count = 10
$animation-window = 50
$animation-step = $animation-window / $ring-count

for $ring in (0..$ring-count)
  // Generate a set of keyframes based on the ring index
  // index is the ring
  $start = $animation-step * ($ring + 1)
  @keyframes slink-{$ring} {
    // In here is where we need to generate the keyframe steps based on ring count and window.
    0%, {$start * 1%} {
      transform
        translate3d(-50%, -50%, var(--origin-z))
        translateZ(0)
        rotateY(0deg)
    }
    // Flip without falling
    {($start + ($animation-window * 0.75)) * 1%} {
      transform
        translate3d(-50%, -50%, var(--origin-z))
        translateZ(0)
        rotateY(180deg)
    }
    // Fall until the cut-off point
    {($start + $animation-window) * 1%}, 100% {
      transform
        translate3d(-50%, -50%, var(--origin-z))
        translateZ(var(--destination-z))
        rotateY(180deg)
    }
  }

Đây là ý nghĩa của các biến đó:

  • $ring-count: Số lượng các vòng trong slinky của chúng tôi.
  • $animation-window: Đây là phần trăm khung hình chính mà chúng ta có thể liên kết. Trong ví dụ của chúng tôi, chúng tôi nói rằng chúng tôi muốn liên kết lại 50% của khung hình chính. Phần còn lại 50% nên được sử dụng cho sự chậm trễ.
  • $animation-step: Đây là khoảng trống được tính toán cho mỗi vòng. Chúng tôi có thể sử dụng điều này để tính toán phần trăm khung hình chính duy nhất cho mỗi vòng.

Đây là cách nó biên dịch sang CSS, ít nhất là trong một vài lần lặp đầu tiên:

Xem mã đầy đủ
@keyframes slink-0 {
  0%, 4.5% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(0deg);
  }
  38.25% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(180deg);
  }
  49.5%, 100% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(var(--destination-z))
      rotateY(180deg);
  }
}
@keyframes slink-1 {
  0%, 9% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(0deg);
  }
  42.75% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(180deg);
  }
  54%, 100% {
    transform:
       translate3d(-50%, -50%, var(--origin-z))
       translateZ(var(--destination-z))
       rotateY(180deg);
  }
}

Điều cuối cùng cần làm là áp dụng từng bộ khung hình chính cho mỗi vòng. Chúng tôi có thể làm điều này bằng cách sử dụng đánh dấu của mình nếu chúng tôi muốn bằng cách cập nhật nó để xác định cả hai --index a --name:

- const RING_COUNT = 10;
.container
  .scene
    .plane(style=`--ring-count: ${RING_COUNT}`)
      - let rings = 0;
      while rings < RING_COUNT
        .ring(style=`--index: ${rings}; --name: slink-${rings};`)
        - rings++;

Điều này mang lại cho chúng tôi điều này khi được biên dịch:

<div class="container">
  <div class="scene">
    <div class="plane" style="--ring-count: 10">
      <div class="ring" style="--index: 0; --name: slink-0;"></div>
      <div class="ring" style="--index: 1; --name: slink-1;"></div>
      <div class="ring" style="--index: 2; --name: slink-2;"></div>
      <div class="ring" style="--index: 3; --name: slink-3;"></div>
      <div class="ring" style="--index: 4; --name: slink-4;"></div>
      <div class="ring" style="--index: 5; --name: slink-5;"></div>
      <div class="ring" style="--index: 6; --name: slink-6;"></div>
      <div class="ring" style="--index: 7; --name: slink-7;"></div>
      <div class="ring" style="--index: 8; --name: slink-8;"></div>
      <div class="ring" style="--index: 9; --name: slink-9;"></div>
    </div>
  </div>
</div>

Và sau đó, kiểu dáng của chúng tôi có thể được cập nhật cho phù hợp:

.ring {
  animation: var(--name) var(--speed) both infinite cubic-bezier(0.25, 0, 1, 1);
}

Thời gian là tất cả. Vì vậy, chúng tôi đã bỏ mặc định animation-timing-function và chúng tôi đang sử dụng một cubic-bezier. Chúng tôi cũng đang sử dụng --speed thuộc tính tùy chỉnh mà chúng tôi đã xác định ở đầu.

Được rồi. Bây giờ chúng ta có một CSS Slinky nhấp nháy! Chơi với một số biến trong mã và xem bạn có thể mang lại những hành vi khác nhau nào.

Tạo hoạt ảnh vô hạn

Bây giờ chúng ta có phần khó khăn nhất, chúng ta có thể thực hiện điều này đến nơi mà hoạt ảnh lặp lại vô hạn. Để làm điều này, chúng tôi sẽ dịch cảnh khi Slinky của chúng tôi nhấp nháy để có vẻ như nó đang nhấp nháy trở lại vị trí ban đầu.

.scene {
  animation: step-up var(--speed) infinite linear both;
}

@keyframes step-up {
  to {
    transform: translate3d(-100%, 0, var(--depth));
  }
}

Chà, điều đó tốn rất ít nỗ lực!

Chúng tôi có thể xóa màu nền khỏi .scene.plane để ngăn hoạt ảnh quá chói tai:

Sắp xong! Điều cuối cùng cần giải quyết là chồng các vòng sẽ lật trước khi nó trượt lại. Đây là nơi chúng tôi đã đề cập trước đó rằng việc sử dụng màu sắc sẽ có ích. Thay đổi số lượng vòng thành một số lẻ, như 11và chuyển về xen kẽ màu vòng:

Bùm! Chúng tôi có một CSS slinky đang hoạt động! Nó cũng có thể cấu hình!

Các biến thể thú vị

Còn về hiệu ứng “flip flop” thì sao? Theo đó, ý tôi là làm cho Slink chuyển sang các cách khác nhau. Nếu chúng tôi thêm một phần tử bao bọc bổ sung vào cảnh, chúng tôi có thể xoay cảnh bằng cách 180deg trên mỗi slink.

- const RING_COUNT = 11;
.container
  .flipper
    .scene
      .plane(style=`--ring-count: ${RING_COUNT}`)
        - let rings = 0;
        while rings < RING_COUNT
          .ring(style=`--index: ${rings}; --name: slink-${rings};`)
          - rings++;

Đối với hoạt ảnh, chúng ta có thể sử dụng steps() chức năng thời gian và sử dụng hai lần --speed:

.flipper {
  animation: flip-flop calc(var(--speed) * 2) infinite steps(1);
  height: 100%;
  width: 100%;
}

@keyframes flip-flop {
  0% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(180deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

Cuối cùng, nhưng không kém phần quan trọng, hãy thay đổi cách .scene nguyên tố của step-up các tác phẩm hoạt hình. Nó không cần di chuyển trên trục x nữa.

@keyframes step-up {
  0% {
    transform: translate3d(-50%, 0, 0);
  }
  100% {
    transform: translate3d(-50%, 0, var(--depth));
  }
}

Lưu ý animation-timing-function mà chúng tôi sử dụng. Việc sử dụng steps(1) là những gì làm cho nó có thể.

Nếu bạn muốn một cách sử dụng thú vị khác steps(), kiểm tra này #SpeedyCSSTMẹo!

Để có thêm cảm ứng, chúng tôi có thể xoay chậm toàn bộ cảnh:

.container {
  animation: rotate calc(var(--speed) * 40) infinite linear;
}
@keyframes rotate {
  to {
    transform:
      translate3d(0, 0, 100vmin)
      rotateX(-24deg)
      rotateY(-32deg)
      rotateX(90deg)
      translateZ(calc((var(--depth) + var(--stack-height)) * -1))
      rotate(360deg);
  }
}

Tôi thích nó! Tất nhiên, kiểu dáng là chủ quan… vì vậy, tôi đã tạo một ứng dụng nhỏ mà bạn có thể sử dụng để định cấu hình Slinky của mình:

Và đây là phiên bản “Original” và “Flip-Flop” mà tôi đã chụp xa hơn một chút với bóng và chủ đề.

Bản trình diễn cuối cùng

Đó là nó!

Đó là ít nhất một cách để tạo một CSS Slinky thuần túy vừa 3D vừa có thể định cấu hình. Chắc chắn, bạn có thể không tiếp cận với những thứ như thế này hàng ngày, nhưng nó mang đến các kỹ thuật hoạt ảnh CSS thú vị. Nó cũng đặt ra câu hỏi liệu có animation-repeat-delay thuộc tính trong CSS sẽ hữu ích. Bạn nghĩ sao? Bạn có nghĩ rằng sẽ có một số trường hợp sử dụng tốt cho nó? Tôi muốn biết.

Hãy chắc chắn chơi với mã - tất cả đều có sẵn trong Bộ sưu tập CodePen này!

tại chỗ_img

Tin tức mới nhất

tại chỗ_img