手把手帶你使用純CSS繪製一箇中國結,並新增動畫效果!

2022-01-27 13:00:47
本篇文章手把手教大家,一步步使用純CSS繪製一箇中國結,並給這個中國結新增紅包雨動畫效果,希望對大家有所幫助!

春節是中國人最重要的節日,春節期間的習俗也非常多,東西南北各不相同。 為了增添年味,過年時家家戶戶會置辦各種年貨和裝飾品,把家裡營造得紅紅火火,紅燈籠,紅對聯,紅福字,以及紅色的中國結。

1.png

中國結原材料是簡簡單單的紅繩,經過古人的巧妙構思,編織成一個菱形網格的樣子。網格上的繩線緊緊連線在一起,象徵一家人團結和睦,幸福美滿。

2.png

那麼如何用CSS來實現一箇中國結呢? 先看最終效果。

3.png

4.gif

線上預覽Codepen地址:
https://codepen.io/cliea/pen/LYOPbBr

如此 Amazing 的效果,你也可以做出來,下面讓我們開始吧!

一、編碼之前

1. 蒐集素材,越簡潔越好

先從網上搜一張中國結的圖片,中國結的樣式不止一種,我們選擇一種最經典的中國結的編織樣式。 圖片的品質決定了最後成品的品質,下面就是一張比較整潔,結構清晰的中國結圖片。供我們寫CSS時參考使用。

5.png

2. 觀察細節,構思實現的可能性

有了圖片就可以開始寫程式碼了嗎?當然不是。

首先回想一下現在要做的事:用CSS畫箇中國結。

你真的想好了嗎?這是一個可以實現的目標嗎?想象一下,當你的領導給你佈置一個任務:讓手機殼根據APP的主題色而變色。你會直接開始寫程式碼嗎?

你會想兩個問題:

  • APP作為一個軟體,是否有和手機殼互動的介面

  • 手機殼如果接收到色值,如何變色

這是一個比較極端的例子,上面兩條都無法實現。回到CSS和這張中國結的圖片。我們首先要想的是,我們應該用哪些CSS技術,來實現這個圖片。你現在回過頭仔細觀察一下上面的圖片。

經過短暫的觀察,我們發現這樣一些要點

  • 中國結的繩子是由漸變色組成,深紅,淺紅,深紅

  • 中間的主體部分由22根相互交叉的繩子組合而成,而且每過一個交叉點就會交換一下層級順序

  • 有一些環狀結構,顏色漸變的過程與直線相同

  • 整體都是紅色,以黃色點綴

然後就是預想一下實現原理

  • 直線的顏色漸變,使用 linear-gradient 或者 repeating-linear-gradient

  • 環狀漸變,使用 radial-gradient

  • 網格的交叉,使用 mask 遮罩來達到交叉效果

  • 四分之三環以及底部兩根彎曲的繩子使用 clip-path 來裁剪

  • 為了使編碼更方便,採用 SCSS

  • 許多地方可以使用 ::before ::after 實現,減少html程式碼

3. 結構拆分,化整為零

上面是從技術角度從整體觀察,下面就是對整個圖片進行拆分,先確定其 html 結構。

  • 中間像棋盤一樣的網格結構,可以作為一個 html 標籤

6.png

  • 四周16個小半圓,使用16個標籤定位實現

7.png

  • 兩個四分之三圓,放在一組裡,使用相同的樣式,第二個基於第一個旋轉180deg

8.png

  • 兩個十字結,樣式一樣,所以也放在一組裡

9.png

  • 頂部三個小結構,放在一組,外層命名為header

10.png

  • 底部左右兩部分高度相似,也放在一組,命名為footer

11.png

這樣我們得到了 html 的結構

<div class="chinese-knot">
  <div class="grid"></div>
  <div class="ring-small">
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
  </div>
  <div class="ring-big">
    <i><b></b></i>
    <i><b></b></i>
  </div>
  <div class="cross-node">
    <div class="node">
      <i></i>
      <i></i>
      <i></i>
      <i></i>
    </div>
    <div class="node">
      <i></i>
      <i></i>
      <i></i>
      <i></i>
    </div>
  </div>
  <div class="header">
    <i></i>
    <b></b>
    <span></span>
  </div>
  <div class="footer">
    <b></b>
    <b></b>
    <div class="tassels">
      <i></i>
      <i></i>
    </div>
  </div>
</div>

實際編碼當中,html 並不是一次寫成,而是經過不斷調整才成為上面這個樣子。

二、CSS逐個實現中國結部件

1. 網格

網格最終效果是個菱形,也就是正方形旋轉了45deg,我們先不旋轉,看看它是什麼樣子

12.png

先設定一個變數,表示繩子的寬度,我們設為--width,這個尺寸很重要,後面所有尺寸都是基於這個寬度,這樣後面我們調整整個圖形的大小,只要改這一個--width就行了。

:root {
  --width: 1.7vh;
}

垂直和水平都各有11根繩,繩子之間的間隙約為繩子寬度的 0.5 倍,所以可以得到網格的寬高都為 11 + 0.5 * 10 = 16 倍的繩子寬度,所以我們可以這樣寫:

:root {
  --width: 1.7vh;
  --grid-width: calc(var(--width) * 16);
}
.grid {
  width: var(--grid-width);
  height: var(--grid-width);
}

body 加上一些樣式,讓盒子居中,再加一個深色背景

body{
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background: #1d1e22;
  overflow: hidden;
}

再給 .grid 也加一個白色背景色,測試一下:

13.png

這樣螢幕正中間就出現了一個白色方塊,下面我們把白色背景改成11根線的樣式:

:root{
  --width: 1.7vh;
  --red-1: #f40001;
  --red-2: #d40000;
  --red-3: #8c0703;
  --rope: 
    var(--red-3), 
    var(--red-2) calc(var(--width) * 0.25), 
    var(--red-1) calc(var(--width) * 0.45), 
    var(--red-1) calc(var(--width) * 0.55), 
    var(--red-2) calc(var(--width) * 0.75), 
    var(--red-3) var(--width);
  --grid-width: calc(var(--width) * 16);
  --bg-line: linear-gradient(90deg, var(--rope), transparent var(--width)) 0 0 / calc(var(--width) * 1.5) calc(var(--width) * 1.5);
}
.grid{
  width: var(--grid-width);
  height: var(--grid-width);
  background: var(--bg-line);
}

就得到了下面的效果:

14.png

可能你有點蒙圈。發生了什麼事情?

還是讓事情變得簡單點,我們先畫一根不帶漸變的紅線:

.grid{
  background: linear-gradient(
    90deg, 
    var(--red-1), 
    var(--red-1) var(--width), 
    transparent var(--width)
  );
}

15.png

先是一個線性漸變 linear-gradient,然後旋轉角度設為90deg,讓它從左到右漸變(預設是從下往上),然後起始值設為--red-1(你問我red-1red-3哪來的?效果圖上吸來的),在--width處也設定為--red-1,這樣就得到了一根寬為 --width 的紅線。但這還沒完,得接著在--width 處加一個透明transpanrent,這樣從--width直到圖形的最右側就都不填充顏色了。

但這不太像根繩子,讓紅線漸變起來:

.grid{
  background: linear-gradient(
    90deg, 
    var(--red-3), 
    var(--red-2) calc(var(--width) * 0.25), 
    var(--red-1) calc(var(--width) * 0.45), 
    var(--red-1) calc(var(--width) * 0.55), 
    var(--red-2) calc(var(--width) * 0.75), 
    var(--red-3) var(--width), 
    transparent var(--width)
  );
}

16.png

這樣就得到了一根有一點點立體效果的繩子。可是怎麼讓它橫向重複11次,並且間隔0.5倍的--width呢?看下面的程式碼:

.grid{
  background: linear-gradient(
    90deg, 
    var(--red-3), 
    var(--red-2) calc(var(--width) * 0.25), 
    var(--red-1) calc(var(--width) * 0.45), 
    var(--red-1) calc(var(--width) * 0.55), 
    var(--red-2) calc(var(--width) * 0.75), 
    var(--red-3) var(--width), 
    transparent var(--width)
  ) 0 0 / calc(var(--width) * 1.5) calc(var(--width) * 1.5);
}

大家來找茬:這段程式碼和上一段有什麼不同?眼尖的你可能已經看出來了,多了這一行:

0 0 / calc(var(--width) * 1.5) calc(var(--width) * 1.5)

/為分界線,左邊的含義是background-positoin,右邊的含義是background-size

0 0也就是左上角。calc(var(--width) * 1.5) calc(var(--width) * 1.5) 也就是一個正方形,寬度為1.5倍繩寬。

17.png

這樣一個小方塊,在垂直和水平方向上重複,就得了我們想要的結果:

18.png

可是我們想要的是網格,現在頂多也就算個柵格。

那就使用偽類複製一份,並且旋轉90deg

:root{
  --width: 1.7vh;
  --red-1: #f40001;
  --red-2: #d40000;
  --red-3: #8c0703;
  --rope: 
    var(--red-3), 
    var(--red-2) calc(var(--width) * 0.25), 
    var(--red-1) calc(var(--width) * 0.45), 
    var(--red-1) calc(var(--width) * 0.55), 
    var(--red-2) calc(var(--width) * 0.75), 
    var(--red-3) var(--width);
  --grid-width: calc(var(--width) * 16);
  --bg-line: linear-gradient(90deg, var(--rope), transparent var(--width)) 0 0 / calc(var(--width) * 1.5) calc(var(--width) * 1.5);
}
.grid {
  width: var(--grid-width);
  height: var(--grid-width);
  background: var(--bg-line);
  &:after {
    content: "";
    display: block;
    width: var(--grid-width);
    height: var(--grid-width);
    background: var(--bg-line);
    transform: rotate(90deg);
  }
}

19.png

對比一下參考圖片:

20.png

不能說完全不相干,但是人家一看就經過了能工巧匠的編織,咱們這隻能算簡單的疊加,怎麼才能讓上面變成下面呢?

經過仔細的觀察,發現只要把上面一層橫著的線,稍加一些遮擋就能實現交叉編織的效果。用哪個css屬性實現呢?那就只有mask 了。

下圖藍色框是需要遮擋的部分,綠色框是需要重複的部分。

21.png

仔細分析一下綠框的構成:

22.png

本質上是在一個3×3的正方形上挖兩個1×1的小洞,位置分別是0 01.5 1.5。我們要如何畫這樣一張圖?並把這張圖應用到mask上呢?

mask是通過傳入的圖片進行遮罩處理,而背景圖除了傳入一張png以外,CSS還內建了幾個生成背景圖的函數:

  • linear-gradient:線性漸變
  • repeating-linear-gradient:重複線性漸變
  • radial-gradient:徑向漸變
  • conic-gradient:圓錐漸變

這些函數都可以和mask配合。這裡我們使用conic-gradient實現上面的圖形。

conic-gradient 實現上圖,思路要反著來:不是在方形上挖孔,而是用多個矩形將要渲染的部分填充顏色,剩下的部分自然就是透明的:

23.png

CSS實現如下:

:root{
    ...
    --conic: #000 0 90deg, transparent 0 100%;
}

.grid {
  ...
  &:after {
    ...
    -webkit-mask: conic-gradient(from 0deg at var(--width) calc(var(--width) * 1.5), var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3),
      conic-gradient(from 90deg at calc(var(--width) * 2.5) 0, var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3),
      conic-gradient(from 180deg at calc(var(--width) * 1.5) var(--width), var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3),
      conic-gradient(from 90deg at 0 calc(var(--width) * 2.5), var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3);
  }
}

預覽效果

24.png

目前為止完整程式碼

:root{
  --width: 1.7vh;
  --red-1: #f40001;
  --red-2: #d40000;
  --red-3: #8c0703;
  --rope: 
    var(--red-3), 
    var(--red-2) calc(var(--width) * 0.25), 
    var(--red-1) calc(var(--width) * 0.45), 
    var(--red-1) calc(var(--width) * 0.55), 
    var(--red-2) calc(var(--width) * 0.75), 
    var(--red-3) var(--width);
  --grid-width: calc(var(--width) * 16);
  --bg-line: linear-gradient(90deg, var(--rope), transparent var(--width)) 0 0 / calc(var(--width) * 1.5) calc(var(--width) * 1.5);
  --conic: #000 0 90deg, transparent 0 100%;
}
body{
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background: #1d1e22;
  overflow: hidden;
}
.grid {
  width: var(--grid-width);
  height: var(--grid-width);
  background: var(--bg-line);
  &:after {
    content: "";
    display: block;
    width: var(--grid-width);
    height: var(--grid-width);
    background: var(--bg-line);
    transform: rotate(90deg);
    -webkit-mask: conic-gradient(from 0deg at var(--width) calc(var(--width) * 1.5), var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3),
      conic-gradient(from 90deg at calc(var(--width) * 2.5) 0, var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3),
      conic-gradient(from 180deg at calc(var(--width) * 1.5) var(--width), var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3),
      conic-gradient(from 90deg at 0 calc(var(--width) * 2.5), var(--conic)) 0 0 / calc(var(--width) * 3) calc(var(--width) * 3);
  }
}
<div class="grid"></div>

沒錯,這個圖形,只用了.grid這一個標籤!

但是隻有網格還不夠,讓我們繼續。

2. 半圓環

回頭看一下參考圖:

25.png

嗯,環形漸變,那就是radial-gradient了:

  <div class="ring-small">
    <i></i>
  </div>
.ring-small {
  i {
    position: absolute;
    width: calc(var(--width) * 2.5);
    height: calc(var(--width) * 1.5);
    background: radial-gradient(
        circle at 50% 100%, 
        transparent calc(var(--width) * 0.25), 
        var(--red-3) calc(var(--width) * 0.25), 
        var(--red-2) calc(var(--width) * (0.25 + 0.25)),
        var(--red-1) calc(var(--width) * (0.25 + 0.45)), 
        var(--red-1) calc(var(--width) * (0.25 + 0.55)), 
        var(--red-2) calc(var(--width) * (0.25 + 0.75)),
        var(--red-3) calc(var(--width) * (0.25 + 1)), 
        transparent calc(var(--width) * (0.25 + 1))
    );
  }
}

26.png

這樣就得到了半個環形圖,讓我們使用定位把它和網格結合看看

/* 先給最外層加個相對定位,後面的絕對定位都相對這一層 */
.chinese-knot {
  width: var(--grid-width);
  height: var(--grid-width);
  position: relative;
}
.ring-small {
  i {
    position: absolute;
    top: calc(var(--width) * -1.5);
    left: calc(var(--width) * 3);
  }
}

27.png

對比素材圖,發現環形不是直接緊貼在網格上的,而是先延伸了一小段直線,再接的曲線。那我們就給它增個高吧:

.ring-small {
  i {
    &:before,
    &:after {
      content: "";
      position: absolute;
      bottom: calc(var(--width) * -0.5 + 1px);
      width: var(--width);
      height: calc(var(--width) * 0.5);
      background: var(--bg-line);
    }
    &:after {
      right: 0;
    }
  }
}

上面使用兩個偽類,為半圓環加了兩截高度為 0.5--width的增高墊,效果如下圖

28.png

接著複製16個這樣的圖形,分別定位到各自的位置上:

<div class="chinese-knot">
  <div class="grid"></div>
  <div class="ring-small">
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
    <i></i>
  </div>
</div>
.ring-small {
  i {
    position: absolute;
    width: calc(var(--width) * 2.5);
    height: calc(var(--width) * 1.5);
    background: radial-gradient(
        circle at 50% 100%, 
        transparent calc(var(--width) * 0.25), 
        var(--red-3) calc(var(--width) * 0.25), 
        var(--red-2) calc(var(--width) * (0.25 + 0.25)),
        var(--red-1) calc(var(--width) * (0.25 + 0.45)), 
        var(--red-1) calc(var(--width) * (0.25 + 0.55)), 
        var(--red-2) calc(var(--width) * (0.25 + 0.75)),
        var(--red-3) calc(var(--width) * (0.25 + 1)), 
        transparent calc(var(--width) * (0.25 + 1))
    );
    &:before,
    &:after {
      content: "";
      position: absolute;
      bottom: calc(var(--width) * -0.5 + 1px);
      width: var(--width);
      height: calc(var(--width) * 0.5);
      background: var(--bg-line);
    }
    &:after {
      right: 0;
    }
    &:nth-child(-n + 4) {
      top: calc(var(--width) * -2 + 2px);
    }
    &:nth-child(1) {
      left: calc(var(--width) * 3);
    }
    &:nth-child(2) {
      left: calc(var(--width) * 6);
    }
    &:nth-child(3) {
      left: calc(var(--width) * 9);
    }
    &:nth-child(4) {
      left: calc(var(--width) * 12);
    }
    &:nth-child(-n + 8):nth-child(n + 5) {
      bottom: calc(var(--width) * -2 + 2px);
      transform: rotate(180deg);
    }
    &:nth-child(5) {
      left: calc(var(--width) * 1.5);
    }
    &:nth-child(6) {
      left: calc(var(--width) * 4.5);
    }
    &:nth-child(7) {
      left: calc(var(--width) * 7.5);
    }
    &:nth-child(8) {
      left: calc(var(--width) * 10.5);
    }
    &:nth-child(-n + 12):nth-child(n + 9) {
      left: calc(var(--width) * -2.5 + 2px);
      transform: rotate(-90deg);
    }
    &:nth-child(9) {
      top: calc(var(--width) * 3.5);
    }
    &:nth-child(10) {
      top: calc(var(--width) * 6.5);
    }
    &:nth-child(11) {
      top: calc(var(--width) * 9.5);
    }
    &:nth-child(12) {
      top: calc(var(--width) * 12.5);
    }
    &:nth-child(-n + 16):nth-child(n + 13) {
      right: calc(var(--width) * -2.5 + 2px);
      transform: rotate(90deg);
    }
    &:nth-child(13) {
      top: calc(var(--width) * 2);
    }
    &:nth-child(14) {
      top: calc(var(--width) * 5);
    }
    &:nth-child(15) {
      top: calc(var(--width) * 8);
    }
    &:nth-child(16) {
      top: calc(var(--width) * 11);
    }
  }
}

就得到了這樣的效果

29.png

哈哈,很像下水管道~

3. 四分之三圓環

還是先看素材:

30.png

嗯,不得不懷疑網易雲的 LOGO 的靈感是不是就是中國結。

30-1.png

單個環形已經實現了,兩個環也不難吧:

<div class="ring-big">
    <i><b></b></i>
</div>
.ring-big {
  i {
    position: absolute;
    width: calc(var(--width) * 6);
    height: calc(var(--width) * 6);
    b {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background: radial-gradient(
        circle at 50% 50%,
        transparent calc(var(--width) * 0.5),
        var(--red-3) calc(var(--width) * 0.5),
        var(--red-2) calc(var(--width) * (0.5 + 0.25)),
        var(--red-1) calc(var(--width) * (0.5 + 0.45)),
        var(--red-1) calc(var(--width) * (0.5 + 0.55)),
        var(--red-2) calc(var(--width) * (0.5 + 0.75)),
        var(--red-3) calc(var(--width) * (0.5 + 1)),
        transparent calc(var(--width) * (0.5 + 1)),
        transparent calc(var(--width) * 2),
        var(--red-3) calc(var(--width) * 2),
        var(--red-2) calc(var(--width) * (2 + 0.25)),
        var(--red-1) calc(var(--width) * (2 + 0.45)),
        var(--red-1) calc(var(--width) * (2 + 0.55)),
        var(--red-2) calc(var(--width) * (2 + 0.75)),
        var(--red-3) calc(var(--width) * (2 + 1)),
        transparent calc(var(--width) * (2 + 1))
      );
    }
  }
}

31.png

為什麼 <i> 標籤裡要再套一個標籤呢,因為接下來我們要執行 clip-path,還要給圓環增高,而clip-path 會給增高的部分也裁剪掉,所以只能再套一層,讓內層的 <b> 自己 clip,增高則使用 <i> 的偽類實現。下面就是將圓環右下角 1/4 裁剪掉並且加一個增高墊的程式碼:

.ring-big {
  i {
    ...
    b {
      ...
      clip-path: polygon(0 0, 100% 0, 100% 50%, 50% 50%, 50% 100%, 0 100%);
    }
    &:before,
    &:after {
      content: "";
      position: absolute;
      top: calc(var(--width) * 3 - 1px);
      left: calc(var(--width) * 3.5);
      width: calc(var(--width) * 2.5);
      height: calc(var(--width) * 0.5 + 2px);
      background: repeating-linear-gradient(
        90deg, 
        var(--red-3), 
        var(--red-2) calc(var(--width) * 0.25), 
        var(--red-1) calc(var(--width) * 0.45), 
        var(--red-1) calc(var(--width) * 0.55), 
        var(--red-2) calc(var(--width) * 0.75), 
        var(--red-3) var(--width), 
        transparent var(--width), 
        transparent calc(var(--width) * 1.5)
      );
    }
    &:after {
      transform: rotate(90deg);
      transform-origin: left top;
      top: calc(var(--width) * 3.5);
      left: calc(var(--width) * 3.5 + 1px);
    }
  }
}

32.png

複製一份並定位:

.ring-big {
  i {
    ...
    &:nth-child(1) {
      left: calc(var(--width) * -3.5);
      top: calc(var(--width) * -3.5);
    }
    &:nth-child(2) {
      left: auto;
      top: auto;
      right: calc(var(--width) * -3.5);
      bottom: calc(var(--width) * -3.5);
      transform: rotate(180deg);
    }
  }
}

33.png

到這裡,工作的一半就已經完成了~繼續

4. 十字結

34.png

這個圖形,相對於上面幾個,已經沒什麼難度了,五個1×1 的正方形,中間的漸變方向和周圍四個垂直。

中間的正方形,用父級本身實現,裡面周圍四個,用四個子<i>標籤實現:

<div class="cross-node">
    <div class="node">
      <i></i>
      <i></i>
      <i></i>
      <i></i>
    </div>
    <div class="node">
      <i></i>
      <i></i>
      <i></i>
      <i></i>
    </div>
  </div>
.cross-node {
  .node {
    position: absolute;
    z-index: 2;
    width: var(--width);
    height: var(--width);
    background: var(--bg-line);
    i {
      position: absolute;
      width: var(--width);
      height: var(--width);
      background: var(--bg-line);
      transform: rotate(90deg);
      &:nth-child(1) {
        left: calc(var(--width) * -1);
      }
      &:nth-child(2) {
        left: var(--width);
      }
      &:nth-child(3) {
        top: calc(var(--width) * -1);
      }
      &:nth-child(4) {
        top: var(--width);
      }
    }
    &:nth-child(1) {
      right: calc(var(--width) * -1);
      top: calc(var(--width) * -1);
    }
    &:nth-child(2) {
      left: calc(var(--width) * -1);
      bottom: calc(var(--width) * -1);
    }
  }
}

35.png

36.png

5. 掛繩

前面我們都是讓中國結處於一個斜躺的姿態,寫頭部和尾部之前,讓我們先把它擺正:

.chinese-knot {
  ...
  transform: rotate(-45deg) translate(calc(var(--width) * 4), calc(var(--width) * -4));
}

37.png

回頭看素材圖:

38.png

先確定一下html結構:

<div class="header">
    <i></i>
    <b></b>
    <span></span>
</div>

i 是上面的吊繩,b 是圓環,span 是銜接處的短繩,帶點黃色裝飾。為了方便調整定位,我們從下往上實現,先寫短繩:

:root {
  --yellow-1: #fced00;
  --yellow-2: #f28a00;
  --yellow-3: #da571b;
  --bg-yellow: linear-gradient(
    90deg,
    var(--yellow-3),
    var(--yellow-2) 20%,
    var(--yellow-1) 40%,
    var(--yellow-1) 60%,
    var(--yellow-2) 80%,
    var(--yellow-3) 100%
  );
}
.header {
  position: absolute;
  right: 0;
  top: 0;
  transform: rotate(45deg);
  i {
    position: absolute;
    bottom: calc(var(--width) * 1);
    left: calc(var(--width) * -0.5);
    width: calc(var(--width) * 1);
    height: calc(var(--width) * 2);
    background: var(--bg-line);
    &:before {
      content: "";
      display: block;
      height: calc(var(--width) * 0.5);
      background: var(--bg-yellow);
    }
  }
}

然後是圓環:

.header {
  ...
  b {
    position: absolute;
    bottom: calc(var(--width) * 3);
    left: calc(var(--width) * -1.5);
    width: calc(var(--width) * 3);
    height: calc(var(--width) * 3);
    background: radial-gradient(
      circle at 50%,
      transparent calc(var(--width) * 0.75),
      var(--red-3) calc(var(--width) * 0.75),
      var(--red-2) calc(var(--width) * (0.75 + 0.15)),
      var(--red-1) calc(var(--width) * (0.75 + 0.3)),
      var(--red-1) calc(var(--width) * (0.75 + 0.45)),
      var(--red-2) calc(var(--width) * (0.75 + 0.6)),
      var(--red-3) calc(var(--width) * (0.75 + 0.75)),
      transparent calc(var(--width) * (0.75 + 0.75))
    );
  }
}

最後是長的吊繩:

.header {
  ...
  span {
    position: absolute;
    bottom: calc(var(--width) * 5);
    left: calc(var(--width) * -0.25);
    width: calc(var(--width) * 0.5);
    height: calc(var(--width) * 30);
    background: linear-gradient(90deg, var(--red-2), var(--red-1) 20%, var(--red-2) 70%, var(--red-3));
    border-radius: calc(var(--width) * 0.25);
  }
}

單獨效果

39.png

整體效果

40.png

6. 流蘇

41.png

確定html結構:

<div class="footer">
    <b></b>
    <b></b>
    <div class="tassels">
      <i></i>
      <i></i>
    </div>
</div>

可以看到,流蘇部分,有兩個彎曲的 1/8 環,我們用兩個b 標籤來表示。這個形狀依然還是先畫一個完整的環,然後裁剪來實現:

.footer {
  position: absolute;
  left: 0;
  bottom: 0;
  b {
    position: absolute;
    width: calc(var(--width) * 15);
    height: calc(var(--width) * 15);
    background: radial-gradient(
      circle at 50%,
      transparent calc(var(--width) * 6.5),
      var(--red-3) calc(var(--width) * 6.5),
      var(--red-2) calc(var(--width) * (6.5 + 0.25)),
      var(--red-1) calc(var(--width) * (6.5 + 0.45)),
      var(--red-1) calc(var(--width) * (6.5 + 0.55)),
      var(--red-2) calc(var(--width) * (6.5 + 0.75)),
      var(--red-3) calc(var(--width) * (6.5 + 1)),
      transparent calc(var(--width) * (6.5 + 1))
    );
  }
}

42.png

加上裁剪並定位:

.footer {
  ...
  b {
    ...
    &:nth-child(1) {
      left: calc(var(--width) * -8.5);
      top: calc(var(--width) * 1);
      clip-path: polygon(50% 0, 50% 50%, 10% 0);
    }
    &:nth-child(2) {
      left: calc(var(--width) * -16);
      top: calc(var(--width) * -6.5);
      clip-path: polygon(100% 50%, 50% 50%, 100% 90%);
    }
  }
}

43.png

兩個小尾巴就實現了。

最後是流蘇。先畫一下背景上的垂直細線,這裡我們用 repeating-linear-gradient 實現,每隔 2px 畫一條 1px 寬的透明度為 0.2 的黑線:

.footer {
  .tassels {
    i {
      position: absolute;
      width: calc(var(--width) * 2.5);
      height: calc(var(--width) * 14);
      background: var(--red-2) repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 3px) 50% 50% / 3px 1px;}
  }
}

44.png

再蒙上一層黃色的裝飾:

.footer {
  .tassels {
    i {
      ...
      &:before {
        content: "";
        position: absolute;
        top: calc(var(--width) * 0.5);
        width: 100%;
        height: calc(var(--width) * 3.6);
        background: var(--bg-yellow);
        clip-path: polygon(0 0, 100% 0, 100% 10%, 0 10%, 0 15%, 100% 15%, 100% 85%, 0 85%, 0 90%, 100% 90%, 100% 100%, 0 100%, 0 0);
      }
  }
}

45.png

上面程式碼中使用 clip-path 對黃色背景裁剪,露出兩條紅線,裁剪路徑可以用下圖表示:

46.png

最終效果:

47.png

三、加點動畫

本來到這裡就應該結束了。但是我想讓這個中國結有點實際用途,比如加點互動什麼的。

紅包也是春節的習俗之一,那就加一個拉一下中國結掉落紅包雨的特效吧~

1. 拉一下

給中國結在:active狀態下加個位移即可實現:

.chinese-knot {
  width: var(--grid-width);
  height: var(--grid-width);
  position: relative;
  transform: rotate(-45deg) translate(calc(var(--width) * 4), calc(var(--width) * -4));
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: all 0.5s;
  &:active {
    transform: rotate(-45deg) translate(calc(var(--width) * 2), calc(var(--width) * -2));
  }
}

48.gif

2. 畫個紅包

先搜尋一個紅包素材:

49.png

觀察一下紅包結構,深紅背景,淺紅弧形開口,加一個黃色圓形封口,上面寫著一個繁體的開字。

我們可以先確定 html 結構。.rain 作為外層,代表整個紅包雨,一個i標籤代表一個紅包:

<div class="rain">
  <i></i>
</div>

一個標籤怎麼實現上面提到的三種元素呢?看程式碼:

.rain {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-around;
  i {
    position: relative;
    display: block;
    width: calc(var(--width) * 5);
    height: calc(var(--width) * 8);
    background: var(--red-3);
    border-radius: calc(var(--width) * 0.4);
    overflow: hidden;
    box-shadow: 0 calc(var(--width) * 1) calc(var(--width) * 1) rgba(0, 0, 0, 0.3);
    &:before {
      content: "";
      position: absolute;
      left: 50%;
      transform: translate(-50%, -50%);
      width: calc(var(--width) * 8);
      height: calc(var(--width) * 8);
      background: var(--red-1);
      opacity: 0.5;
      border-radius: 50%;
    }
    &:after {
      content: "開";
      position: absolute;
      left: 50%;
      transform: translate(-50%, 140%);
      width: calc(var(--width) * 2);
      height: calc(var(--width) * 2);
      background: var(--yellow-2);
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-style: normal;
      font-size: calc(var(--width) * 0.5);
      color: var(--yellow-1);
      text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.1);
    }
  }
}

50.png

使用i標籤自身實現紅包主體,:before 偽類實現弧形的開口,:after 偽類實現黃色圓形封口,在content中寫上字。

一個紅包完成了,再複製 9 個:

<div class="rain">
  <i></i>
  <i></i>
  <i></i>
  <i></i>
  <i></i>
  <i></i>
  <i></i>
  <i></i>
  <i></i>
  <i></i>
</div>

51.png

這樣就得到了 10 個固定在頂部,並且整齊排列的紅包了。

3. 紅包雨動畫

下雨嘛,從上往下運動就好了:

.rain {
  ...
  i {
    ...
    animation: fall 3s ease-in infinite;
  }
}
@keyframes fall {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(0, 100vh);
  }
}

52.gif

聰明的你估計已經猜到了這樣的結果:誰家的雨是這樣齊刷刷的下來的?

那我們就紅包的垂直位置錯落一點,使用 sassrandom 函數來實現隨機:

.rain {
  ...
  i {
    ...
    @for $i from 1 through 10 {
      &:nth-child(#{$i}) {
        top: random(60) + vh;
      }
    }
  }
}

53.gif

額,效果怎麼和想象的不一樣。依舊還是齊刷刷下落,只不過是"錯落"的齊刷刷。

那我們讓每個紅包的開始時間也隨機不就行了嘛:

.rain {
  ...
  i {
    ...
    @for $i from 1 through 10 {
      &:nth-child(#{$i}) {
        top: random(60) + vh;
        animation-delay: random(30) * 0.1s;
      }
    }
  }
}

54.gif

嗯,好了一點點。但是有一個問題,螢幕上的雨點,有時候很多,有時候很少,不夠均勻。那我們把動畫的持續時間也隨機會怎麼樣呢?

.rain {
  ...
  i {
    ...
    @for $i from 1 through 10 {
      &:nth-child(#{$i}) {
        top: random(60) + vh;
        animation-delay: random(30) * 0.1s;
        animation-duration: random(10) * 0.1s + 2s; /* 2s ~ 3s 之間隨機 */
      }
    }
  }
}

55.gif

終於更像雨了~

但是現在雨滴是憑空出現的,很生硬,我們只要把開始的位置挪到負一屏,然後讓它下落到正二屏就行了:

.rain {
  ...
  top: -100vh;
}
@keyframes fall {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(0, 200vh);
  }
}

56.gif

這樣就有了源源不斷下落的效果。

4. 拉一下觸發紅包雨

CSS 不是 JS ,怎麼觸發點選事件呢?

我們就要運用 CSS 本身的特性了,checkbox 核取方塊有個選中狀態 :checked,而核取方塊可以用點選切換這個狀態,再使用 CSS 的兄弟選擇器 element ~ element 即可實現點選新增樣式的效果。

樣式可以觸發了,那如何觸發動畫呢?

animation 屬性新增到元素上後,播放狀態預設是 running ,我們需要先把初始播放狀態改為 paused (暫停), 然後通過上面的方法,把元素的播放狀態改回 running 來實現播放動畫的效果:

<input type="checkbox" id="switch">
<label class="chinese-knot" for="switch">...</label>
<div class="rain">...</div>
.rain {
  ...
  i {
    ...
    animation: fall 3s ease-in infinite;
    /* 預設不播放動畫 */
    animation-play-state: paused;
  }
}

#switch {
  visibility: hidden;
  pointer-events: none;
}
/* checkbox 選中時播放動畫 */
#switch:checked ~ .rain i {
  animation-play-state: running;
}
/* 點選時重置動畫,否則取消checkbox選中狀態,動畫會中止並停留在當前位置 */
.chinese-knot:active ~ .rain i {
  animation: none;
}

上面的 html 中,我們讓.chinese-knotdiv 改為 label 來指向 checkbox,方法是 labelforcheckboxid 設為相同的值。

57.gif

效果很不錯,我們再給紅包雨下落時加個背景,以提醒使用者當前的狀態。並且下紅包雨時,調低中國結的透明度,以突出紅包的存在感。

<input type="checkbox" id="switch">
<div class="bg"></div>
<label class="chinese-knot" for="switch">...</label>
<div class="rain">...</div>
.bg {
  position: absolute;
  left: 0;
  top: 0;
  height: 100vh;
  width: 100vw;
  background: linear-gradient(0deg, #171a4b, #96367f);
  opacity: 0;
  transition: all 0.5s;
}
#switch:checked ~ .bg {
  opacity: 1;
}
#switch:checked ~ .chinese-knot {
  opacity: 0.2;
  &:hover {
    opacity: 0.5;
  }
}

58.gif

完結撒花~~~

總結

這篇文章整理了我從蒐集素材開始,創作一個作品的全部過程,程式碼寫了一天,這篇文章寫了半天。希望能讓 CSS 初學者對 CSS 燃起興趣,也希望讓接觸了一段時間 CSS 的朋友獲得一些靈感和幫助。

(學習視訊分享:)

以上就是手把手帶你使用純CSS繪製一箇中國結,並新增動畫效果!的詳細內容,更多請關注TW511.COM其它相關文章!