iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Modern Web

微光採集瓶:用 Vue 3 打造每日美好記錄網頁系列 第 20 篇

【Day 20】拒絕千篇一律:顏色佔比演算法與色卡調整

  • 分享至 

  • xImage
  •  

昨天做完開瓶跟光珠飛出去的動畫後,回頭看了一下產生的色卡,我發現一個很明顯的盲點:昨天的做法是用 new Set() 把有出現過的顏色抓出來,然後每個顏色都給一個固定的光斑半徑(65%)。這會導致就算整個月存了 29 顆金色、只有 1 顆粉色,兩者在畫面上分到的面積是完全一樣大的。

真實生活是有比重的。如果不把「出現次數」轉成畫布上的「比例」,每張卡片看起來都差不多,就失去了每個月獨一無二的意義。

所以今天主要來搞定這幾件事:

  1. 補測昨天沒測的「隔月 1 號補領」功能,確認跨月時提示橫幅有正常跳出來、點開也能對到上個月。
  2. 改掉 Set 邏輯:算好每種顏色的實際數量與百分比,讓出現最多次的「主導色」決定底色,並依照佔比動態放大光暈半徑。
  3. 色票補上數值標籤:把原本單純的圓點改加上清楚的百分比數字,一眼就能看懂當月的心情組成。

一、補測:隔月 1 號補領月度總結色卡

在動手改演算法前,我們先解決昨天留下的跨月測試。要測試「1 號補領橫幅」,我們需要:

  1. 今天是 1 號。
  2. 瓶子裡必須有「上個月」的光珠。

因為我的瓶子裡本來就有九月的光珠了,如果你的瓶子目前是空的或是只有十月的,要依照下列步驟加入光珠,若本來就有九月的光珠可以直接進行日期修改的步驟就好。

在瀏覽器 Console 貼上這段測試指令,放入幾顆上個月的光珠:

const testGems = JSON.parse(localStorage.getItem('store_gems') || '[]')
testGems.push(
  { id: 1, date: '2026-09-10', color: '#E3B341', note: '九月達成目標', offsetX: 0, offsetY: 0 },
  { id: 2, date: '2026-09-20', color: '#64B5F6', note: '夜晚吹風散步很舒服', offsetX: 0, offsetY: 0 }
)
localStorage.setItem('store_gems', JSON.stringify(testGems))

接著暫時將程式碼裡的日期假裝設成 10 月 1 號(Vue 的GemJar.vue):

// 原本是:const todayStr = getLogicalDate()
// 暫時改為測試日期:
const todayStr = '2026-10-01'

重新整理網頁後,頂部會順利浮現黃色橫幅:

「新月份開始了!上個月的記憶已經總結。」

https://ithelp.ithome.com.tw/upload/images/20261004/20178067mWPEbptx6p.png

點擊「回顧上月色卡」,彈出的卡片目標月份也顯示在 2026-09。

https://ithelp.ithome.com.tw/upload/images/20261004/20178067eaYwjmemqv.png

補領測試通過,測試完記得將 todayStr 改回原本的 getLogicalDate() !

二、演算法實作:情緒佔比與動態光暈

這裡要特別說明一下,isAutoGenerated 這個標記,在專案裡有兩種不同的使用方式:

  • 判斷「今天能不能再採集」時,會排除它。因為系統自動補的留白光珠,不代表使用者今天已經手動寫過東西,不應該因此鎖住輸入框。
  • 統計月度色卡時,不排除它。因為色卡要誠實呈現這個月真實的樣貌,包括那些忙到忘記寫的日子。這些留白,本來就是生活的一部分,不需要被隱藏或排除在回顧之外。

1. 計算各色彩出現頻率與百分比

統計各顏色的次數與百分比,並由多到少排序:

// 計算目標月份各色彩的統計數據(顆數與百分比)
const colorStats = computed(() => {
  const total = targetMonthGems.value.length
  if (total === 0) return []

  const counts = {}
  targetMonthGems.value.forEach(g => {
    counts[g.color] = (counts[g.color] || 0) + 1
  })

  return Object.entries(counts)
    .map(([color, count]) => ({
      color,
      count,
      ratio: Math.round((count / total) * 100)
    }))
    .sort((a, b) => b.count - a.count) // 出現最多的排在第一個
})

2. 改寫 monthlyGradientStyle:動態調整光暈擴散半徑

讓出現比例最高的「主導色」擁有最大的擴散半徑,偶爾出現的顏色則化作角落的小點綴:

const monthlyGradientStyle = computed(() => {
  const stats = colorStats.value
  if (stats.length === 0) {
    return { background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)' }
  }

  const dominant = stats[0]
  const anchorPoints = ['25% 25%', '80% 75%', '75% 25%', '20% 80%', '50% 50%']

  const radialLayers = stats.map((item, idx) => {
    const pos = anchorPoints[idx % anchorPoints.length]
    const renderColor = item.color.toLowerCase() === '#ffffff' 
      ? 'rgba(255, 255, 255, 0.9)' 
      : item.color
    
    // 依佔比動態擴大半徑(50% ~ 90%)
    const radius = Math.min(90, Math.max(50, item.ratio + 30))
    return `radial-gradient(circle at ${pos}, ${renderColor} 0%, rgba(255, 255, 255, 0) ${radius}%)`
  })

  return {
    backgroundImage: radialLayers.join(', '),
    backgroundColor: dominant.color // 直接以當月主色打底
  }
})

3. <template>:色票清單加入比例標籤

在拍立得色卡下方,將原本單純的色票圓點,變為帶有百分比數值的標籤:

<!--- 下方留白區 -->
<div class="card-meta">
  <h3 class="meta-title">月度微光色卡</h3>
  <p class="meta-stats">{{ cardMonthTarget }} 共採集了 {{ targetMonthGems.length }} 道微光</p>

  <!-- 顯示顏色與百分比 -->
  <div class="palette-swatches">
    <div 
      v-for="(item, index) in colorStats" 
      :key="index" 
      class="swatch-item"
    >
      <span class="swatch-bubble" :style="{ backgroundColor: item.color }"></span>
      <span class="swatch-ratio">{{ item.ratio }}%</span>
    </div>
  </div>

4. <style scoped>:微調色票標籤排版

為百分比色票補上排版:

.palette-swatches {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}

.swatch-item {
  display: flex;
  align-items: center;
  gap: 5px;
}

.swatch-bubble {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
}

.swatch-ratio {
  font-size: 0.72rem;
  font-weight: 600;
  color: #57606a;
}

這邊我有將昨天寫的程式碼做一個小小的修正。

原本是想用 rgba(0, 0, 0, 0.25) 做出色卡四周微微暗角的復古效果,但遇到淺色或金色時,這層黑影在視覺上反而有種髒髒的感覺。

於是把原本的:

.aurora-inner-glow {
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.25);
}

改成:

.aurora-inner-glow {
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 30px rgba(255, 255, 255, 0.2);
}

這樣會讓顏色看起來融合得比較自然。

三、成果驗證

為了驗證「情緒佔比演算法」與動態光暈的效果,我們使用兩組比例不同的假資料進行對比:

  • 情境 A:

    1. 色彩佔比:金色 70%、粉色 20%、藍色 10%。
    2. 視覺呈現:整張色卡直接以金色作為基底,最中間是顯眼的金黃色,粉色與藍色在比較邊邊的位子。

    在 Console 輸入這段程式碼:

    const gems70 = [
      ...Array(7).fill(null).map((_, i) => ({ id: `g-${i}`, date: '2026-10-01', color: '#E3B341', note: '成就', offsetX: 0, offsetY: 0 })),
      ...Array(2).fill(null).map((_, i) => ({ id: `p-${i}`, date: '2026-10-02', color: '#F48FB1', note: '溫暖', offsetX: 0, offsetY: 0 })),
      { id: 'b-1', date: '2026-10-03', color: '#64B5F6', note: '平靜', offsetX: 0, offsetY: 0 }
    ]
    localStorage.setItem('store_gems', JSON.stringify(gems70))
    location.reload()
    

https://ithelp.ithome.com.tw/upload/images/20261004/20178067uJkML5pKS5.png

https://ithelp.ithome.com.tw/upload/images/20261004/20178067vjP33x1xJl.png

  • 情境 B:

    1. 色彩佔比:藍色 60%、純白留白 40%。
    2. 視覺呈現:畫布變成天藍色佔比最多,融合了近一半的白色。

    在 Console 輸入這段程式碼:

    const gems60 = [
      // 6 顆藍色(10/01)
      ...Array(6).fill(null).map((_, i) => ({ id: `b-${i}`, date: '2026-10-01', color: '#64B5F6', note: '平靜', offsetX: 0, offsetY: 0 })),
      // 2 顆留白(10/02)
      ...Array(2).fill(null).map((_, i) => ({ id: `w1-${i}`, date: '2026-10-02', color: '#FFFFFF', note: '留白', offsetX: 0, offsetY: 0, isAutoGenerated: true })),
      // 2 顆留白(10/03)
      ...Array(2).fill(null).map((_, i) => ({ id: `w2-${i}`, date: '2026-10-03', color: '#FFFFFF', note: '留白', offsetX: 0, offsetY: 0, isAutoGenerated: true }))
    ]
    
    localStorage.setItem('store_gems', JSON.stringify(gems60))
    location.reload()
    

拍立得下方的色票也精確標上了 70%、20% 等百分比圓點標籤,讓使用者可以在每個月的最後知道自己的哪些情緒佔了生活的多少。

https://ithelp.ithome.com.tw/upload/images/20261004/20178067Cj3Ub9i1Co.png

https://ithelp.ithome.com.tw/upload/images/20261004/20178067ODuw9Gfzpp.png

備註:以上測試資料是以「今天為 10/04」為基準設計,若是在其他日期測試,記得將日期銜接好,避免觸發先前實作的「未登入自動補發白色微光」,導致總顆數與比例出現偏差。

今日小結

  1. 加權設計取代平均分配:算出每種顏色出現的次數跟比例,讓出現最多次的顏色去決定底色和最大的光斑,畫面才看得出當月的心情重點。
  2. 色票加上百分比:直接把趴數標出來,一眼就知道各個情緒佔了多少。
  3. 忘記寫的日子也算進去:統計時沒有特別把系統補發的白色排除掉,畢竟那些沒記心情的日子,本來也就是這個月的一部分。

明日預告

每個月呈現出來色卡這麼漂亮,如果看過一次關掉就找不到了,是不是有些可惜呢?
明天(Day 21),我們要打造「時光畫廊」,把每個月總結出來的微光色卡一張張收納在專屬的展覽頁面中,讓你可以隨時回顧過去每一趟光陰的色彩軌跡!


上一篇
【Day 19】封存月份的美好:月度色卡的誕生及儀式感動畫
系列文
微光採集瓶:用 Vue 3 打造每日美好記錄網頁 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言