"Challenges are fun opportunities for leveling up your skills by building things. Each week, you’ll get a new prompt surrounding a monthly theme to riff on. The best Pens get picked and featured on the homepage!" - Codepen 官網
Codepen 會在每個月推出一個主題,於每週推出一項與該主題相關的挑戰。任何人都可以加入這項挑戰。每次都有設定基本門檻要求,然而官方都會提供一個範本。可以照著範本重刻或是自己發想都可以。如果作品出色的話,有可能會被官方挑選放在首頁喔。
作為 2024 年 5 月的最後一週,主題取名為「Filter fest!」。其要求如下:
“create a Pen that includes at least two types of front-end filters”
也就是說這個 Pen 至少要使用到兩種以上的 "filter" 就對了。前幾週的挑戰練習了前端不同的 filter,例如:CSS filter、SVG filter、Javascript filter。 接著就是個人認為所有練習中最有趣卻也最難的地方,也就是:「所以我要做什麼?」
先來參考一下這週官方給的範例吧,是一個具有圖片濾鏡切換功能的頁面。
<feTurbulence>
又稱「湍流」的效果。官方範例一共使用了三種 filter 完成這個頁面。
第二週的 SVG Filter ,找到一篇文章對於濾鏡的資料寫得蠻詳細的,因此幫助我完成了該週的挑戰。在該篇文章底下有示範了如何做出水面倒影以及雲朵的效果,因此...
直接先貼成果:(如果無法顯示,可以點這裡)
決定把這週挑戰主題設定在「雲朵」,構想大概會是:
首先處理 HTML 的架構。構想是模擬藍天白雲的效果,因此背景會是天藍色,而雲朵將會利用 SVG filter 來處理。
<div class="cloud-layer"></div>
<svg height='0' width='0'>
<filter id='cloudRandom'>
<feTurbulence type='fractalNoise' baseFrequency='0.012' numOctaves='4' seed='25' />
<feDisplacementMap in='SourceGraphic' scale='200' />
</filter>
</svg>
<svg>
,並加入 <filter>
標籤。並且要記得寫id
,才能從 CSS 取用。筆記:
若 svg 標籤沒明確指示長寬設定,預設會是 300x150px。然而這裡的 svg 作為濾鏡參數使用,因此加入長寬為 0 的設定,避免影響到容器長寬的計算。
在 <filter>
裡面使用了兩種濾鏡效果(svg filter 標籤是前綴 "fe" 代表 "filter effect"):
<feTurbulence>
:湍流,意指混亂的氣流。採用名為 Perlin 的公式計算出特色為不規則的圖形。(至於背後詳細計算公式可參考這裡)fractalNoise
和turbulence
。前者類似毛玻璃,後者是預設值,類似所謂的琉璃。雖然不太理解,但是實際以雲朵擬真度比較兩者,確實前者會是優於後者的。(見下圖)<feDisplacementMap>
:失真,具體來說是位置替換的概念,改變元素和圖形的像素位置。可參考MDN官方文件。<div class="filter">
<div class="radio">
<input name='filterType' type="radio" id="none" value='none' checked>
<label for="none">None</label>
</div>
<div class="radio">
<input name='filterType' type="radio" id="blur" value='blur'>
<label for="blur">Blur</label>
</div>
<div class="radio">
<input name='filterType' type="radio" id='invert' value='invert'>
<label for="invert">Invert</label>
</div>
<div class="radio">
<input name='filterType' type="radio" id='drop-shadow' value='drop-shadow'>
<label for="drop-shadow">Drop-shadow</label>
</div>
</div>
body
body {
background-color: #87ceeb;
color: $black;
font-family: "Quicksand", sans-serif;
overflow: hidden;
}
overflow: hidden
讓視窗不會出現卷軸.filter:濾鏡切換器
.filter {
position: fixed;
bottom: 0.25rem;
right: 0.25rem;
padding: 0.5rem 1rem;
font-size: 1.25rem;
border-radius: 1rem;
opacity: 0.3;
transition: all 0.5s;
&:hover {
opacity: 1;
backdrop-filter: blur(8px);
}
}
.radio {
// 設定每個 input 的間距
&:not(:last-child) {
margin-bottom: 0.25rem;
}
}
position: fixed
以及設定絕對定位固定在視窗右下角。opacity: 0.3
半透明度,只有在滑鼠 hover 到上方才顯示。同時也用了一種叫做 backdrop-filter
的濾鏡屬性,特性是只會將效果應用於元素背後。因此元素本身不會因為濾鏡效果而影響。.cloud-layer:雲層
.cloud {
&-layer {
z-index: -1;
width: 100%;
height: 100vh;
position: relative;
}
}
width: 100%; height: 100vh;
。另外設置相對定位 (relative),讓雲朵可以這層元素作為定位點。z-index: -1
讓雲層作為背景層級的元素。.cloud:雲朵
.cloud {
width: 650px;
height: 350px;
position: absolute;
opacity: 0;
background: radial-gradient(
closest-side,
#fff 20%,
rgba(#fff, 0.5) 60%,
rgba(#fff, 0) 80%
);
// 套用 svg 濾鏡
filter: url(#cloudRandom);
@for $i from 1 through 20 {
&:nth-of-type(#{$i}) {
animation: 10s float linear infinite ($i * 0.333s);
}
}
}
// 雲朵動畫
@keyframes float {
30% {
opacity: 1;
}
100% {
right: -100%;
}
}
background:radial-gradient()
,由中心點為起始點往結束點進行漸變。closest-side
結束點為距離中心點最近的垂直/水平的邊切齊,設定在 20% 為白色 -> 60% 透明度0.5的白色 -> 80% 透明度0的白。(如下圖)filter: url(#cloudRandom);
,在 url 括號內放入我們寫好的 svg 濾鏡 id ,如此就能把濾鏡效果套用在雲朵上。float
動畫。希望整個過程在 10 秒內跑完,所以在 3 秒 (30%) 的時候透明度變為 1,在第 10 秒時離開視窗。@for
迴圈語法,除了為每個子元素都加上動畫屬性,利用變數 ($i * 0.333s)
遞增延遲秒數,如此節省了要寫很多重複的程式碼。(官方文件)const cloudLayer = document.querySelector(".cloud-layer");
const filter = document.querySelector(".filter");
const filters = [
{
name: "blur",
value: "10px"
},
{
name: "invert",
value: "50%"
},
{
name: "drop-shadow",
value: "16px 16px 20px pink"
}
];
const clouds = 20;
DOMContentLoaded 事件:監聽 DOM 載入事件並加入雲朵元素
document.addEventListener("DOMContentLoaded", createClouds(clouds));
DOMContentLoaded
事件。另一個類似的事件類型是 load
,差異在於調用函數的時機是整個頁面所有資料(包含圖片、樣式)都載入完成後才會進行函數調用,而DOMContentLoaded
則是在 DOM 結構都解析載入後就進行函數調用。(參考文章)createClouds()
帶入引數 clouds
createClouds( num ):建立“雲朵”元素
function createClouds(num) {
// 建立一個獨立的節點集合,減少多次操作 DOM 的情況。
const frag = document.createDocumentFragment();
// 建立一組長度為 num ,並且透過函數 getRandomPos() 產生一個物件內容加入陣列
const positions = Array.from({ length: num }, () => getRandomPos());
// 迴圈遍歷次數為 num 次,每次新增一個“雲朵”元素加入 frag。
for (let i = 0; i < num; i++) {
// 解構 postitions 陣列作為 x 軸與 y 軸
const { x, y } = positions[i];
// 建立一個 div 元素
const newCloud = document.createElement("div");
// 加入 class
newCloud.classList.add("cloud");
// 將 x,y 軸加入雲朵的樣式內容,分別對應距離右邊及底部的位置
newCloud.style.cssText = `right: ${x}px; bottom: ${y}px;`;
// 將新增的元素內容加入到 frag 中
frag.append(newCloud);
}
// 將 frag 內容加入“雲層”
cloudLayer.append(frag);
}
關於DocumentFragment:
DocumentFragment
是 DOM 節點(Nodes)。他們不會成為 DOM 主幹的一部份。最常見的作法是先建立文本片段 (document fragment),然後將元素 (element) 加入文本片段中,最後再將文本片段加入 DOM 樹中。在 DOM 樹中,文本片段將會被他所有的子元素取代。
正因為文本片段是存在記憶體中,並且不是 DOM 主幹的一部分,增加子元素並不會導致網頁重刷 (reflow)(重新計算元素的位置和幾何)。因此採用文本片段通常會有比較好的效能表現 (better performance)。(MDN)
getRandomPos( ):隨機產生(位置)數值
function getRandomPos() {
// 產生 x 軸
const x = Math.floor(
Math.random() * (document.documentElement.clientWidth - 150)
);
// 產生 y 軸
const y = Math.floor(
Math.random() * (document.documentElement.clientHeight - 150)
);
return { x, y };
}
Math.random()
產生隨機數乘以視窗內部寬度 (document.documentElement.clientWidth - 150)
,最後再利用 Math.floor()
將數值回傳為整數。clientWidth
及 clientHeight
都是計算頁面內部高度(包含 padding,但不包含 border、margin及卷軸)。change 事件:監聽濾鏡切換
filter.addEventListener("change", function (e) {
// 如果點擊的目標元素不是 input 就 return
if (!e.target.matches("input")) return;
// 取得被點選的濾鏡切換器的值
const selected_value = filter.querySelector(
"input[name='filterType']:checked"
).value;
// 如果 selected_value 不為「none」加入 filter 屬性,否則復歸預設值
if (selected_value != "none") {
// 解構 filters 回傳的值
const { name, value } = filters.find(
(obj) => obj["name"] === selected_value
);
// 加入雲層的 CSS filter 屬性
cloudLayer.style.filter = `${name}(${value})`;
} else {
// 預設值,filter: none;
cloudLayer.style.filter = "none";
}
});
find()
方法會回傳第一個滿足所提供之測試函式的元素值,否則回傳 undefined
。(MDN)blur(10px);
寫在最後:
「何とかなる!」
首先就是:「寫文章真的好難啊...」
中途真的是好幾次差點放棄,不過最後還是撐過來了!先給自己一點鼓勵。
其實「寫文章」很早就在很多地方看過推薦可以寫部落格紀錄學習成果。大概從三月開始寫 Codepen 挑戰,到了四月底決定把這些挑戰寫文章看看。但是真的開始動筆的時候卻千頭萬緒而停滯。大概是受到從小周遭環境的影響,會覺得如果沒有足夠好的東西就不要拿出來。所以會對自己寫的東西沒有自信,開始假設被其他人看到一定會被笑的情況。
直到最近看了「用大腦喜歡的方式 1 人學習」這本書,提到了「量造就質」的概念,也就是透過持續進行累積學習的份量,最終的結果會比「只做一個最好的作品」還要好。套用在這裡的話,大概就是持續寫作品、持續寫文章吧。
過程中也發現當時沒注意到的細節,再去找文件看一遍。也就是在發佈文章前,對於自己寫的東西又做一次掃描的感覺,並且透過文字的方式確認自己知不知道這段在寫什麼。再者,文筆爛就爛、爛扣就爛扣,但我就是菜雞沒錯啊。如果因此被指正也是幫助自己成長的一種管道。
於是最後把「寫文章」的重點歸納為:文章是寫給自己看的。經過心態調適之後,就比較有動力寫下去了。
如果有幸看到這裡的你,不論是前輩還是同樣對前端有興趣的同學,如果發現任何錯誤或是建議,歡迎在下方留言~謝謝!