Rplus / rplus.github.io

my blog~
http://rplus.github.io/
29 stars 4 forks source link

[POST] Introduction for 《Pokédex in CSS grid》 #38

Open Rplus opened 7 years ago

Rplus commented 7 years ago

0. 前言

這是上週六(12/25) 網友線下聚會(?) 時,我所負責撰寫的 UI 部份 這次玩得挺開心的, 不論是大家拼命調整開發環境、或是對細節精雕細磨都超有趣的 XD 不過更棒的是, 最後大家在分享時都非常熱心地介紹自己使用的技巧,也都樂於提出建議~ 希望下次還可以繼續一起約出來玩~

至於我負責的畫面可以先看看實作完的 demo: Pokédex in CSS grid http://codepen.io/Rplus/pen/MbddMe?editors=1100 [[[pen slug-hash='MbddMe' height='300' embed-version='2']]]

ps: 這篇效果應用了些許新的 CSS 規格,不保證所有瀏覽器皆正常顯示 建議直接使用 Canary Chrome 瀏覽 或如果你的 Chrome 看不到 grid 的話,可以依 Google 開發者的這篇文章 去開啟 flags


1. Grid system

說實話, 要完成這樣的格狀排版並不是太困難的任務 不管是 float, inline-block, flexbox 或是直接 table 都可以達成! 而這次我想再繼續練習 grid 的排版方式,所以就選用 grid 囉~

grid 相關設定大多都直接寫在 .wall 裡,

.wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5em, 1fr));
  grid-gap: .5em;

  @media (min-width: 360px) {
    grid-template-columns: repeat(auto-fill, minmax(15vmin, 1fr));
    grid-gap: 1vmin;
  }
}

SCSS 原始碼 也就兩三行在定義而已,看起來挺簡單的不是嗎~ 是的,grid 設定起來就是非常輕巧,初學者並不需要太害怕它 XD 只是要稍微多去認識、並找機會實作嘗試 (我也是在嘗試而已~)

grid-template-columns: repeat(auto-fill, minmax(15vmin, 1fr)) 其中這段的定義就是讓 grid 的欄寬自動填滿,寬度最小 15vmin,最大 1fr (fr 是指自動的欄寬) 所以實際跑出來就會變成每欄等寬,最小 15vmin。

因為 100 / 15 = 6.66… ,所以一個版面較短的那側大概都會塞到 6 格 (塞太多格會太小) 而又因使用 vmin 的緣故,所以打螢幕打橫還是可以維持差不多的格數 大家有興趣可以自己用 devtool 隨便拖拉改變螢幕尺寸試玩看看

這邊也有加一個唯一的 @media query 來控制更窄畫面的情況 畫面太小時,就改用較硬性的單位 em 來控制可讀性了

@supports not (display: grid) 這區塊則是目前 grid 較常見的 fallback 使用方式 這處就定義若不支援就改用其它種排版方式處理

ps: 目前這邊的 grid 寫法在 Firefox 好像是 GG 的狀況, 原因有空再查了… 囧

2. Pokemon 的圖片

這張合併圖是用 Pokemon-GO-App-Assets-and-Images 裡的素材, 再用 ImageMagick 接起來的 (花好多時間在弄素材 = =)

ps: pokemon 的圖不管是有顏色還是剪影,都是用這張 sprite 圖完成的唷~

原本圖片是可以直接設定在 .pm 的背景圖上 不過這樣弄剪影效果會影響到不必要的其它元素 所以獨立到 pseudo-element 上來設定背景圖 => 原始碼

圖片就是應用很常見的 CSS image sprite,藉由載入一張大圖來減少 request 數量 這裡有另外的優化是讓它透過 image CDN 來緩存 ( 不然圖床 imgur 有時會 ban codepen 的 request… )

這裡用的 CDN 是 https://images.weserv.nl/ 滿簡單易用的,可以透過網址幫忙調整大小、裁切,還有這次也有用上的 progressive image format <3

而因為 grid size 會變來變去的關係 所以這邊的 image sprite 的 background-position 也要調整為百分比形式才能好好地完成 RWD 的定位 而百分比定位跟原本的計算方式有些不同,不熟的可以參考這篇文章: How to Use Responsive Background Image Sprites – CSS Tutorial 簡單地說,就是 100% * $index / #{$grid-count - 1} ,格數減一就對了~

至於背景圖位置 background-position: var(--bgp) 則是用了 CSS variables, 這是因為背景圖是設定在 pseudo-element 上, 而 HTML inline style 沒法塞到 pseudo-element 身上 所以這邊應用了 CSS variables 的 scope 特性,讓該變數僅在該 grid 生效 便可達成正確的 sprite 定位 :p

如果這邊不使用 CSS variables 的話, 利用關鍵字 inherit 的特性也是能完成在 parent 層自訂 pseudo-element 屬性值的效果,在這邊就不示範了~

ps: 其實 image sprite 不是太難,就是手刻起來會有點繁瑣,不知不覺就變這麼多字…

3. Pokemon 的剪影

// pm avatar shadow image effect
.tile[data-state="seen"] &::before {
  filter: contrast(0) opacity(20%);
  mix-blend-mode: difference;
}

=> 原始碼

剪影效果的 code 其實超短 不過 filter 的想像力沒那麼熟時,還是得慢慢去試效果 contrast 調到 0 基本上就是把顏色都打掉變成灰灰的, opacity 是為了調淡一些、並帶上一些背景色調 而一直記不起每個屬性效果的 mix-blend-mode 則是在 devtool 裡上上下下切換直接看效果比較快 XDD

ps: 背景圖<img> 套用 filter: contrast(0) 的效果會不一樣唷~

剪影這一段的效果其實一開始也沒把握實作出來 不過胡搞瞎搞後的成果還挺趣味的~ 哈哈

4. Pokemon 狀態

其實就三種 已捕獲(caugth)、見過(seen)、未知(unknown) 這三種的數量其實是有一個大約比例的 大約是 7:2:1

這邊的處理方式是使用 HTML preprocessor: pug 寫個 mixin 來處理 其實就是 JS function 而已 只是 Codepen 現在把 jade 換成了 pug2,所以有支援比較新的 es6 syntax~

var states = (() => {
  var stateMap = {
    'unknown': 1,
    'seen': 2,
    'caught': 7
  };
  return Object.keys(stateMap).reduce(
    (summary, curState) => summary.concat(new Array(stateMap[curState]).fill(curState)),
    []
  );
})();

var randomState = () => {
  return states[~~(Math.random() * 100) % states.length];
};

=> 原始碼

基本上就是先做一個陣列,內容是以比例分配好狀態出現的次數 再隨機挑一個 0 ~ 99 的餘數 所以可能會有些不準,不過大致的隨機效果可以做到了~ 這邊隨機數好像應該用 ~~(Math.random() * states.length) 會比較正確一些 XDD

而 CSS 部份就只是使用一些 :not() 選擇器去控制哪些東西不要出現

5. CSS counter

其實頂端的計數我一開始是用 data-attr 寫死在 .subtitle 上的 後來實作完隨機狀態後,才回來想這塊要怎麼修正 XD

這部份的作法若用 client 端的 script (javascript) 來處理的話 應該也是一塊小蛋糕而已 不過前面寫這麼多都沒用到 JS,實在不太想在這邊破功吶 XDD

所以我改用 CSS counter 來自動計數~ 在 code 裡可以看到這部份的設定:

.tile {
  &[data-state="caught"] {
    // caught must have seen
    counter-increment: pm-seen pm-caught pm-counter;
  }

  &[data-state="seen"] {
    counter-increment: pm-seen pm-counter;
  }
}

=> 原始碼

.subtitle {
  &::before {
    content: 'caught: ' counter(pm-caught);
  }

  &::after {
    content: 'seen: ' counter(pm-seen);
  }
}

=> 原始碼

counter-increment 設定在各個需要計數的位置 最後再讓 pseudo-element 吃下這個 counter 就能顯示正確的計數囉~

可以發現這邊用的是多重 counter 有的甚至同時觸發 3 個,這在計算不同狀態時非常好用

只是這邊有個比較需要留意的部份是 counter 要印出來會看 DOM 的順序 所以本來 .header 是寫在 .wall 之前的 為了這效果,才把 header 搬進 wall 裡頭 這部份也剛好 header 的定位是 fixed on top,所以 DOM 擺哪對排版比較不會有副作用 所以有些時候技術的選用也要看天時地利配不配合

而因為 counter 就是這麼吃 DOM 結構 所以原本寫在 .pm 上的 pokemon 狀態硬是拉升到 .tile 上去設定 使用上有較多限制,需要特別留心~


主要的效果基本上就介紹到這邊 相信用到的其實都不是很難的屬性 只是需要平時多接觸、多查找、多練習就能慢慢累積一些能牢記的知識 在需要用上時,就能笑笑地回:

『江湖一點訣』

Rplus commented 7 years ago

Sync to Codepen post: http://codepen.io/Rplus/post/introduction-for-pok-dex-in-css-grid

Rplus commented 7 years ago
montage -tile 12x -background none -geometry 100x100+0+0 $(ls -1 *.png | sort -n) sprite.png

輸出 12 欄的 image sprite 每格 100px,間距 0

Rplus commented 7 years ago

更正第五點 5. CSS counter 可以略過 DOM 結構 但需要將 counter-reset 往上層拉

看這篇時,發現他也有遇到同樣的問題~ ref: Using CSS to detect and counting Prime Numbers https://github.com/xieranmaya/blog/issues/12