Filter Fest! 各種濾鏡大亂鬥?

閱讀時間約 21 分鐘


什麼是 Codepen Challenge?

"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 filterSVG filterJavascript filter。 接著就是個人認為所有練習中最有趣卻也最難的地方,也就是:「所以我要做什麼?」

先來參考一下這週官方給的範例吧,是一個具有圖片濾鏡切換功能的頁面。

raw-image
  • 頂部 header 的圖片邊框不規則是套用 svg 的 filter 屬性,並在內部定義名為 <feTurbulence> 又稱「湍流」的效果。
  • 透過選單選取 CSS filter 的屬性,藉由 JS 中 filter 方法將該屬性套用在底下的圖片上。

官方範例一共使用了三種 filter 完成這個頁面。



所以要做什麼?

第二週的 SVG Filter ,找到一篇文章對於濾鏡的資料寫得蠻詳細的,因此幫助我完成了該週的挑戰。在該篇文章底下有示範了如何做出水面倒影以及雲朵的效果,因此...

raw-image

直接先貼成果:(如果無法顯示,可以點這裡


決定把這週挑戰主題設定在「雲朵」,構想大概會是:

  • 類似玩具總動員安迪的房間牆面的外觀
  • 載入頁面時在隨機位置產生 N 個雲朵且有動畫效果(SVG filter)
  • 頁面有 N 個按鈕,使用者可以切換不同濾鏡效果(CSS filter, JS filter)



正式開始

HTML

首先處理 HTML 的架構。構想是模擬藍天白雲的效果,因此背景會是天藍色,而雲朵將會利用 SVG filter 來處理。

<div class="cloud-layer"></div>
  • 新增一個區塊作為雲層,裡面將會新增N個“雲朵”子容器。
<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 的設定,避免影響到容器長寬的計算。

svg 會把相同層級的元素擠出去容器

svg 會把相同層級的元素擠出去容器


<filter> 裡面使用了兩種濾鏡效果(svg filter 標籤是前綴 "fe" 代表 "filter effect"):

  1. <feTurbulence> :湍流,意指混亂的氣流。採用名為 Perlin 的公式計算出特色為不規則的圖形。(至於背後詳細計算公式可參考這裡
    type 有兩種屬性值可選:fractalNoiseturbulence。前者類似毛玻璃,後者是預設值,類似所謂的琉璃。雖然不太理解,但是實際以雲朵擬真度比較兩者,確實前者會是優於後者的。(見下圖)
    baseFrequency 表示噪點的密集程度,預設值為 0 ,數值越高越密集。可以放兩個參數,對應 x 跟 y 軸。數值低者特徵較明顯,若 x 跟 y 軸數值差異越大,則拉伸程度越明顯。(見下圖)
    numOctaves 類似音樂中的八度,其參數將會與 baseFrequency 進行倍數計算,數值越高圖形噪點程度將更細緻。然而數值越高代表需要更多計算,將會影響效能。(見下圖)
    seed 建立不同的雜訊種類,讓每次造訪頁面時產生隨機的“種子”。數值可填入 0 ~ 9999999。
  2. <feDisplacementMap>:失真,具體來說是位置替換的概念,改變元素和圖形的像素位置。可參考MDN官方文件
    in 表示輸入的圖形來源。"SourceGraphic" 代表將會對於應用濾鏡的目標元素進行失真效果。(這段其實不太懂,大概就是決定要失真的對象是指向誰吧)
    scale 縮放比例,接受正負值。值越大,偏移程度越大。


<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>
  • 新增一個作為切換濾鏡的區塊。新增四個類型為 radio 的 input,分別為 None、Blur、Invert、Drop-shadow 等分別對應 CSS filter 的屬性。



SCSS

body

body {
background-color: #87ceeb;
color: $black;
font-family: "Quicksand", sans-serif;
overflow: hidden;
}
  • 在 body 設置 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;
}
}
  • filter 區塊用 position: fixed 以及設定絕對定位固定在視窗右下角。
  • 並且設置 opacity: 0.3 半透明度,只有在滑鼠 hover 到上方才顯示。同時也用了一種叫做 backdrop-filter 的濾鏡屬性,特性是只會將效果應用於元素背後。因此元素本身不會因為濾鏡效果而影響。


.cloud-layer:雲層

.cloud {
&-layer {
z-index: -1;
width: 100%;
height: 100vh;
position: relative;
}
}
  • 在cloud-layer區塊,也就是“雲層”。因為希望雲朵是分佈在整個雲層各處,所以需要佔滿整個頁面,因此設置 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%;
}
}
  • 由於後續會利用 JS 隨機產生位置,所以這裡要加上絕對定位 (absolute)。
  • 雲朵本身是將區塊利用背景設置為 background:radial-gradient() ,由中心點為起始點往結束點進行漸變。closest-side 結束點為距離中心點最近的垂直/水平的邊切齊,設定在 20% 為白色 -> 60% 透明度0.5的白色 -> 80% 透明度0的白。(如下圖)
沒套上 filter 的漸變效果

沒套上 filter 的漸變效果

  • 套用 filter: url(#cloudRandom);,在 url 括號內放入我們寫好的 svg 濾鏡 id ,如此就能把濾鏡效果套用在雲朵上。
  • 幫加上雲朵動畫,於是加了一個會飄走的 float 動畫。希望整個過程在 10 秒內跑完,所以在 3 秒 (30%) 的時候透明度變為 1,在第 10 秒時離開視窗。
  • 為了更加擬真雲朵狀態,所以利用延遲秒數的方式讓每個雲朵是個別移動。透過 SCSS 的 @for 迴圈語法,除了為每個子元素都加上動畫屬性,利用變數 ($i * 0.333s) 遞增延遲秒數,如此節省了要寫很多重複的程式碼。(官方文件



JS

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;
  • 首先在外面宣告 4 個變數,分別為「cloudLayer 取得“雲層”元素」、「filter 取得“濾鏡選擇器”元素」、「filters 濾鏡參數陣列」及「clouds 雲朵數量」。


DOMContentLoaded 事件:監聽 DOM 載入事件並加入雲朵元素

document.addEventListener("DOMContentLoaded", createClouds(clouds));
  • 雲朵是利用 JS 呼叫函數建立元素並加入 DOM,因此建立監聽器來監聽 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 };
}
  • 為了取得雲朵位置,以 x 軸為例。使用 Math.random() 產生隨機數乘以視窗內部寬度 (document.documentElement.clientWidth - 150) ,最後再利用 Math.floor() 將數值回傳為整數。
  • clientWidthclientHeight 都是計算頁面內部高度(包含 padding,但不包含 border、margin及卷軸)。
  • 將寬高都先各自扣除 150 的用意是讓雲朵整體更靠近頁面可見範圍。


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
  • 透過這方法利用前面取得 input 狀態為 checked 的值與陣列 filters 裡面相符的值,並進行解構。
  • 最後以樣板字面值的方式組合成濾鏡屬性內容,例如:blur(10px);



寫在最後:

「何とかなる!」

首先就是:「寫文章真的好難啊...」

中途真的是好幾次差點放棄,不過最後還是撐過來了!先給自己一點鼓勵。

其實「寫文章」很早就在很多地方看過推薦可以寫部落格紀錄學習成果。大概從三月開始寫 Codepen 挑戰,到了四月底決定把這些挑戰寫文章看看。但是真的開始動筆的時候卻千頭萬緒而停滯。大概是受到從小周遭環境的影響,會覺得如果沒有足夠好的東西就不要拿出來。所以會對自己寫的東西沒有自信,開始假設被其他人看到一定會被笑的情況。

直到最近看了「用大腦喜歡的方式 1 人學習」這本書,提到了「量造就質」的概念,也就是透過持續進行累積學習的份量,最終的結果會比「只做一個最好的作品」還要好。套用在這裡的話,大概就是持續寫作品、持續寫文章吧。

過程中也發現當時沒注意到的細節,再去找文件看一遍。也就是在發佈文章前,對於自己寫的東西又做一次掃描的感覺,並且透過文字的方式確認自己知不知道這段在寫什麼。再者,文筆爛就爛、爛扣就爛扣,但我就是菜雞沒錯啊。如果因此被指正也是幫助自己成長的一種管道。

於是最後把「寫文章」的重點歸納為:文章是寫給自己看的。經過心態調適之後,就比較有動力寫下去了。



如果有幸看到這裡的你,不論是前輩還是同樣對前端有興趣的同學,如果發現任何錯誤或是建議,歡迎在下方留言~謝謝!

1會員
1內容數
台灣人|晨型人|日文及程式學習者|音樂取向雜食性,這世界上有搖滾樂跟Red Velvet,真的太幸福了
留言0
查看全部
發表第一個留言支持創作者!
你可能也想看
菲斯特雷角(Fisterra)英文名為Cape Finisterre,源自拉丁語,意為地球的盡頭,風浪非常強勁,在邊緣凝視著黑暗之海。
Thumbnail
avatar
Ku shali
2024-05-20
西班牙朝聖之路 | Fisterrea-Santiago -Madrid | 22 SEP 2023(在西班牙的第十天)移動回馬德里 Fisterrea-Santiago de compostela-Madrid 從Fisterrea回Santiago的巴士是下午三點,這天我們悠閒地在飯店睡到九點才慢慢收拾行李,到中午才退房。 我們決定把握機會看海看個夠!
Thumbnail
avatar
HARU CHENG
2024-03-15
西班牙朝聖之路 | Pedrouzo-Sandiago -Fisterra | 21 SEP 2023(步行第九天)Pedrouzo-Sandiago de compostela約20km(預計走6小時)-搭巴士去Fisterra 原本想3:30醒來,但振動鬧鐘似乎沒有作用。4點的鬧鐘一響我躡手躡腳拿出背包跟要穿的衣服到樓梯間著裝。聽到一樓餐廳區還有人在聊天,才發現昨天鬧騰的孩子們居然好幾人沒有睡
Thumbnail
avatar
HARU CHENG
2024-03-14
Excel技巧|如何一秒分類表格資料?FILTER 函數教學,新手也能輕鬆上手你是否曾經遇到這樣的情況?手上有一張表格,需要根據某個欄位進行分類,但表格又很繁雜,如果手動一個個查找,就需要花費大量時間才能找到想要的資料,這樣實在是太沒效率又容易眼花。 今天,我就來教你一個FILTER 函數快速分類技巧,讓你輕鬆掌握數據,節省時間。
Thumbnail
avatar
小資女の職場生存之道
2024-01-24
Filter Ingratity Test, FIT在藥廠常見的濾心種類不外乎就是... 空氣濾心、減菌濾心、無菌濾心,這三種不同的濾心使用功能不太一樣,而濾心完整性測試則會發生在...
Thumbnail
avatar
LoMei
2023-01-11
〈filter bubbles〉當每個氣室都被完好的薄膜包覆著 呼吸著屬於自己的空氣 當眼前各種玻璃窗的背後 都是用推薦名單和使用者習慣 砌成的風景,試圖……
Thumbnail
avatar
螺旋迴夢
2019-12-18
【Body Fest 2018體祭台中站】之六:落幕之後二○一八年四月二十九日,體祭正式落幕,以Kiwi和惠敏的舞蹈演出《和好如一 Begin with Reconciliation》作為句點。 體季如何落幕,如何延續,又如何讓身體與自己「和好如一」。
Thumbnail
avatar
紙飛機生活誌
2018-05-01
【Body Fest 2018體祭台中站】之五:體祭限時說說Day3最終回【Body Fest2018體祭台中站】限時說說來到最終回啦!一起來跟《體祭》策展團隊玩心理測驗!
Thumbnail
avatar
紙飛機生活誌
2018-04-26