iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0
Modern Web

Dive into CSS Challenge:從問題到解決方案的實踐之旅系列 第 4

Day 4 - CSS Challenge #3:金字塔的日出日落

  • 分享至 

  • xImage
  •  

題目

CSS Challenge Day3
https://ithelp.ithome.com.tw/upload/images/20240917/20169403xmpVNwzhwT.png

上面的圖是題目,題目本身是一個動態的由各種動畫組成的樣子,而我們要做出幾乎一樣的樣子,題目中還有附上出題官方的CodePen,也有附上給我們解題用的template,當我們真的不會的時候,還是可以參考他們的寫法,所以沒有想像中困難。

我做好的此題CSS Challeage解答

那麼我們就開始吧。

題目分析

題目要求我們製作一個圓形的日出日落場景,包含天空、太陽、金字塔及其陰影等元素,並使用 CSS 製作動畫效果。這是一個需要動態變化的場景,要求所有元素平滑過渡並同步運作。

  1. 以圓形為畫布,呈現整個場景,包含天空、太陽、金字塔和地面。
  2. 太陽下落時,影子隨之變長,並且背景從亮色變成深色,所有動畫需保持節奏一致。
  3. 當太陽運動時,金字塔和地面的陰影會依據太陽的角度同步改變,創造出逼真的效果。

注意要點

  1. 動畫停頓與過渡:例如在 30% 處,太陽位於左上角並暫停一下,陰影隨後開始拉長。
  2. 陰影的長度控制:陰影根據太陽的運動進行拉伸與縮短,對應太陽從天頂到地平線的位置。
  3. 數值計算:多個動畫場景需要完美的混合在一起,考驗對 CSS 排版的能力。

開始解題

基礎框架設計

<div class="frame">
  <div class="center">
    <div class="circle">
      <div class="sky"></div>
      <div class="sun"></div>
      <div class="pyramidLeftSide"></div>
      <div class="pyramidRightSide"></div>
      <div class="shadow"></div>
      <div class="sand"></div>
    </div>
  </div>
</div>

我先使用題目template提供的基礎的版型,將物件全部置中,然後在裡面依序放入要製作的場景。
首先是 .circle ,用它來代表圓形內的東西,這裡用 border-radius: 100% 來創建圓形視窗,,並通過 overflow: hidden 隱藏超出的部分。
接著依序把所有會用到的場景,從底層到最上層依序擺放,使用 div 來分別構成天空、太陽、金字塔的左側,金字塔的右側、金字塔的陰影和沙地。


變數設定

$circleWidthHeight: 180px;
$sunWidthHeight: 34px;
$horizon: 125px;
$sandHeight: ($circleWidthHeight - $horizon);
$sunLeft: ($circleWidthHeight/2)-($sunWidthHeight/2);
$pyramidLeftWidth: 116px;
$pyramidHeight: ($pyramidLeftWidth / 2);
$pyramidLeft: ($circleWidthHeight/2)-($pyramidLeftWidth/2);

$shadowWidth: 400px;
$shadowTransX: ($shadowWidth / 2) - ($circleWidthHeight / 2);
$shadowLeft: ($shadowWidth / 2)-($pyramidLeftWidth / 2);
$shadowRight: $shadowLeft + $pyramidLeftWidth;

製作動畫的話,我習慣用變數,會比較方便好計算,這邊大概講一下我的變數怎麼做的。

  • $circleWidthHeight:這是整個圓形場景的寬高,是個正圓,所以我使用同一個數值。
  • sunWidthHeight:這是太陽的寬高。
  • $horizon:這是天空的高度,一直算到地平線的所在位置。
  • sandHeight:這是沙子的高度,我使用圓形場景的高度去剪掉地平線(天空)的高度,剩下的就是沙子的高度。
  • sunLeft:太陽的初始值,我預設他是在天頂,所以他應該要在圓形場景左右置中的位置,這樣我在設定他的 transform-origin 圓心的時候,他才會是沿著正確的軌道運行。
  • pyramidLeftWidth:金字塔左側的寬度,這邊我其實設定的是一整個三角形,而不是只有左側,因為右側等等可以再蓋上去就好,在這邊我為了定位方便,我直接放了一個等腰三角形在圖的正中間。
  • pyramidHeight:金字塔的高度,這邊我直接把金字塔的寬度除2做成高度,所以剛剛金字塔的寬度我設定了一個可以被整除的數字。
  • pyramidLeft:這是為了定位金字塔左側的空間,所以我就使用圓形場景除以2,再剪掉金字塔寬度除以2,剩下的就會是左側的空隙。
  • shadowWidth:因為金字塔的影子可能因為太陽移動,而超出圓形場景,所以我將他的寬度直接設定成跟整個 .frame 一樣寬,這樣才有超出的空間。
  • shadowTransX:這邊是因為目前影子的部分,他的 left: 0 會是在圓型場景的最左側,而不是整個 .frame 的最左側,為了設定讓剛剛設定的寬度的 400 可以從 .frame 的最左側開始算,這樣整個影子的寬度才會跟 .frame 對齊,所以把整個影子的寬度除以2,去剪掉圓型場景除以2,就會得到左半段那黑色的區域,我們再把 left 設定成負數,就可以把場景往左放了。
  • shadowLeft:這是用來設定 clip-path: polygon 內金字塔陰影的左邊那個節點的 x (這邊 y 是 0,貼到沙子最上方,金字塔的最底),我的計算方式是使用整個陰影寬度除以2,再去剪掉金字塔寬度除以2,就會剩下他左側的空隙。
  • shadowRight:這是用來設定 clip-path: polygon 內金字塔陰影的右邊那個節點的 x (這邊 y 是 0,貼到沙子最上方,金字塔的最底),計算方式是金字塔的寬,加上剛剛算出來的 shadowLeft ,就可以得出右邊節點的 x

圓形視窗設計

.circle {
  position: relative;
  width: $circleWidthHeight;
  height: $circleWidthHeight;
  overflow: hidden;
  border-radius: 100%;
}

這裡用 border-radius: 100% 來創建圓形視窗,這是我們整個動畫的容器,所有內容(太陽、金字塔、陰影、沙地)都會在這個圓形內呈現,並通過 overflow: hidden 隱藏超出的部分。


天空與太陽動畫設計

1. 天空的變化

.sky {
  position: absolute;
  height: $horizon;
  z-index: 1;
  top: 0;
  bottom: $horizon;
  left: 0;
  right: 0;
  background-color: #7DDFFC;
  animation: animate-fade 5s infinite;
}

動畫部分:

@keyframes animate-fade {
  0% {
    opacity: 0;
  }
  30% {
    opacity: 1;
  }
  70% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
  • 天空顏色從 30% 開始變亮,模擬日出。當太陽接近地平線時(70%),天空開始逐漸變暗,最終在 100% 完全進入夜晚。
  • animate-fade 會讓天空在 5 秒內由亮藍色轉為 opacity: 0,透出底下的黑色,然後循環播放。

2. 太陽的運動

.sun {
  width: $sunWidthHeight;
  height: $sunWidthHeight;
  border-radius: 100%;
  position: absolute;
  z-index: 2;
  background-color: #FFEF00;
  top: 10px;
  left: $sunLeft;
  transform-origin: 50% 400%;
  animation: animate-sun 5s infinite;
}
  • 太陽的動畫是通過 transform: rotate() 實現的,軸心點設置為 transform-origin: 50% 400%,這意味著太陽會繞著一個遠離自身中心的點進行旋轉,模擬了太陽沿著天空運行的軌跡。
  • 關鍵節點:在 0%,太陽位於左下方,並在 30% 的時候暫停於左上角。此時陰影最短。隨後,太陽繼續旋轉,直到 100% 完全落下。題目的做法有修改太陽的顏色,但我在製作的時候發現那個顏色其實在視覺上不太明顯,所以我就沒有做。

金字塔與陰影設計

金字塔

.pyramidLeftSide {
  position: absolute;
  z-index: 2;
  width: $pyramidLeftWidth;
  height: $pyramidHeight;
  left: $pyramidLeft;
  bottom: ($sandHeight - 1);
  clip-path: polygon(100% 100%,50% 0%,0% 100%);
  background-color: #F4F4F4;
  animation: animate-pyramid-left 5s infinite;
}
.pyramidRightSide {
	position: absolute;
	z-index: 3;
	width: $pyramidHeight;
	height: $pyramidHeight;
	right: $pyramidLeft;
	bottom: ($sandHeight - 1);
	clip-path: polygon(100% 100%,0% 0%,20% 100%);
	background-color: #DDDADA;
	animation: animate-pyramid-right 5s infinite;
}
  • 使用 clip-path 來裁切元素的屬性,這裡使用 polygon() 創造出三角形的金字塔側面。每個點的座標是根據百分比來定義的,100% 100% 是右下角,50% 0% 是頂端,0% 100% 是左下角。
  • 金字塔的陰影顏色會根據太陽的運動逐漸變化,當太陽位於左上角(30%)時,陰影顏色最淺。當太陽接近地平線時,陰影變深,這模擬了太陽照射角度的改變。
  • 我除了將顏色隨著太陽變化而改變以外,也做了 opacity:0 去透出底下的黑色,製造出整體的 fade-out 感覺的動畫。

金字塔的陰影動畫

.shadow {
  position: absolute;
  z-index: 5;
  width: $shadowWidth;
  height: 30px;
  transform-origin: 50% 0%;
  top: ($horizon - 1);
  left: -$shadowTransX;
  background-color: black;
  opacity: 0.2;
  clip-path: polygon($shadowLeft 0%,$shadowRight 0%,100% 100%);
  animation: animate-shadow 5s infinite;
}

動畫部分

@keyframes animate-shadow {
	0% {
		transform: scaleY(0);
		clip-path: polygon( // x,y
			$shadowLeft 0%,$shadowRight 0%,80% 100%);
	}
	30% {
		transform: scaleY(1);
		clip-path: polygon( // x,y
			$shadowLeft 0%,$shadowRight 0%,75% 80%);
	}
	55% {
		transform: scaleY(0.6);
	}
	70% {
		transform: scaleY(0.9);
	}
	100% {
		transform: scaleY(0);
		clip-path: polygon( // x,y
			$shadowLeft 0%,$shadowRight 0%,20% 100%);
	}
}
  • 這裡使用 clip-path 來裁切陰影的形狀,polygon() 定義了一個不規則的多邊形,模擬陰影的形狀。隨著太陽下落,陰影的形狀會逐漸擴大,且對應到太陽的角度變化。
  • 隨著太陽的變化,我 clip-path 裁切的節點也跟著變化,病使用動畫去漸變。
  • transform-origin: 50% 0% 定義了陰影的變形原點為其上邊緣的中點,這樣可以模擬陰影隨著太陽的下落而拉長。在太陽運行至 30% 左上角時,陰影最短。隨著太陽下落,陰影逐漸變長,並在 55% 時達到最大值。這個變化通過 transform: scaleY() 控制,模擬了光照下陰影的自然變化。
  • 我在 top: ($horizon - 1) 刻意多做了 -1 ,是為了視覺上,不要讓影子跟沙地的邊緣有細細的空隙而刻意做的填補。

沙地與背景變化

.sand {
  position: absolute;
  width: 100%;
  height: $sandHeight;
  z-index: 4;
  bottom: 0;
  background-color: #F0DE75;
  animation: animate-fade 5s infinite;
}
  • 沙地的顏色隨著背景漸漸變暗。animate-fade 控制沙地的顏色變化,由淺色沙漠轉為透出底下的黑色。

技巧總結

這次挑戰的核心技巧在於動畫之間的協作及細緻的 clip-path 裁切技術,只要前面變數跟彼此之間的邏輯掌控好,後面動畫部分其實沒有非常困難:

  1. clip-pathpolygon 裁切的應用。
  2. transformrotate 動畫設計。
  3. 透過 transform-origin 控制太陽的運動軌跡,並與陰影的變化同步運作。
  4. 多個動畫的協同同步,將所有動畫都設定為 5 秒一個循環,確保太陽、陰影、天空、沙地等元素都保持一致的動畫節奏,形成一個整體場景的流暢過渡。
  5. 變數的應用與各個元素之間的計算,考驗CSS的排版能力。

Wrap up and go home

那今天就先到這裡,明天我們再繼續來玩下一題。


上一篇
Day 3 - CSS Challenge #2:MenuIcon動畫的實現與解題分析
下一篇
Day 5 - CSS Challenge #4:三層圓形的漸變動畫
系列文
Dive into CSS Challenge:從問題到解決方案的實踐之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言