上面的圖是題目,題目本身是一個動態的由各種動畫組成的樣子,而我們要做出幾乎一樣的樣子,題目中還有附上出題官方的CodePen,也有附上給我們解題用的template,當我們真的不會的時候,還是可以參考他們的寫法,所以沒有想像中困難。
我做好的此題CSS Challeage解答
那麼我們就開始吧。
題目要求我們製作一個圓形的日出日落場景,包含天空、太陽、金字塔及其陰影等元素,並使用 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
隱藏超出的部分。
.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
,透出底下的黑色,然後循環播放。.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
裁切技術,只要前面變數跟彼此之間的邏輯掌控好,後面動畫部分其實沒有非常困難:
clip-path
與 polygon
裁切的應用。transform
和 rotate
動畫設計。transform-origin
控制太陽的運動軌跡,並與陰影的變化同步運作。那今天就先到這裡,明天我們再繼續來玩下一題。