我是一個務實的人,買手機的時候從來只講規格、不看顏色。
「為什麼要選顏色?顏色會影響 CPU 嗎?會影響 RAM 嗎? 功能才重要,顏色無關緊要。」
所以如果同機型有任何一個顏色稍微便宜一點點,我就會選便宜的。
或是如果某些顏色缺貨要等,我就會直接買有現貨的。
「花樣顏色哪有什麼差,功能還不是都一樣!」
但這僅限對我個人而言。
如果我要送別人東西,我一定會精挑細選最好的圖樣顏色。
因為這種視覺上的效果,是會實質影響效用的!
所以如果我要做一個功能給別人用,
功能固然要顧,外觀也不能馬虎。
今天早上,我看著 GASO 的學習地圖,突然意識到一個問題:雖然我們已經有了中古世紀地圖的整體風格,但節點本身還是使用 Graphviz 的預設樣式。這些節點看起來太「現代化」了,與我們精心設計的古樸風格格格不入。
就像在一張古老的羊皮紙地圖上,卻貼著現代化的彩色標籤一樣,總覺得哪裡不對勁。
於是,我決定給節點來一場徹底的視覺革命。
一開始,我思考著什麼樣的節點形狀最適合中古世紀地圖的風格。
橢圓形 vs 矩形:
我選擇了矩形,因為它更符合古代地圖的視覺語言。
// 修改預設節點形狀
nodeShape: localStorage.getItem("gv_nodeShape") || "box",
同時,我也調整了節點的尺寸設定,讓它們自動適應文字內容,而不是使用固定的寬高。
在 Graphviz 中,節點預設會有邊框。我想要移除這些邊框,創造更簡潔的視覺效果。
第一次嘗試:使用 CSS
#zoomInner svg g.node rect,
#zoomInner svg g.node ellipse,
#zoomInner svg g.node polygon {
stroke: none !important;
}
但這並不完全有效,因為 Graphviz 生成的 SVG 會有內聯樣式。
第二次嘗試:直接修改 SVG 屬性
// 直接修改所有節點的 fill 屬性為漸層
const nodeElements = svg.querySelectorAll('g.node polygon, g.node rect, g.node ellipse');
nodeElements.forEach(element => {
element.setAttribute('fill', 'url(#nodeGradient)');
element.setAttribute('stroke', 'none');
});
這次成功了!直接修改 SVG 元素的屬性比 CSS 更有效。
這是最有趣的部分。我最初想要實現半透明效果,但發現 Graphviz 的 fillcolor
屬性只支援實色,不支援透明度。
問題分析:
fillcolor="#8b4513"
只能設定實色rgba()
或透明度解決方案:
我決定使用 SVG 的漸層功能,並在 JavaScript 中動態創建漸層定義:
// 創建漸層定義
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gradient.setAttribute('id', 'nodeGradient');
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '100%');
gradient.setAttribute('y2', '100%');
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', 'rgba(139, 69, 19, 0.8)');
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop2.setAttribute('offset', '100%');
stop2.setAttribute('stop-color', 'rgba(160, 82, 45, 0.6)');
最初的線性漸層效果不錯,但總覺得缺少了什麼。我開始思考如何讓節點更有立體感和吸引力。
線性漸層的問題:
放射狀漸層的優勢:
// 從 linearGradient 改為 radialGradient
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient');
gradient.setAttribute('id', 'nodeGradient');
gradient.setAttribute('cx', '50%'); // 中心 X 座標
gradient.setAttribute('cy', '50%'); // 中心 Y 座標
gradient.setAttribute('r', '50%'); // 半徑
在實現放射狀漸層後,我開始微調顏色和透明度,以達到最佳的視覺效果。
第一次調整:
rgba(160, 82, 45, 0.9)
- 較亮的棕色,90% 不透明度rgba(139, 69, 19, 0.4)
- 較深的棕色,40% 不透明度最終調整(根據使用者反饋):
rgba(160, 82, 45, 1.0)
- 完全不透明rgba(139, 69, 19, 0)
- 完全透明這個調整創造了更強烈的對比效果,讓節點看起來像發光的標記。
在 SVG 中,漸層是通過 <defs>
元素定義的,然後通過 url(#gradientId)
引用:
<defs>
<radialGradient id="nodeGradient" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(160, 82, 45, 1.0)"/>
<stop offset="100%" stop-color="rgba(139, 69, 19, 0)"/>
</radialGradient>
</defs>
在半透明背景上,文字的可讀性是一個重要問題。我採用了以下策略:
#zoomInner svg g.node text {
fill: white !important;
font-weight: bold !important;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5) !important;
}
問題:Graphviz 生成的 SVG 會有內聯的 fill
屬性,CSS 的 !important
無法覆蓋。
解決方案:使用 JavaScript 直接修改 SVG 元素的屬性。
element.setAttribute('fill', 'url(#nodeGradient)');
問題:每次重新渲染圖形時,可能會重複創建漸層定義。
解決方案:檢查漸層是否已存在,避免重複創建。
if (!defs.querySelector('#nodeGradient')) {
// 創建漸層定義
}
問題:在錯誤處理的分支中也需要應用相同的漸層效果。
解決方案:將漸層創建和應用的邏輯複製到錯誤處理分支中。
這次的節點樣式進化主要從以下幾個方面提升了使用者體驗:
這次的開發讓我深刻體會到,技術實現只是手段,視覺效果才是目標。我們需要不斷地調整和優化,直到達到理想的視覺效果。
通過這次的開發,我對 SVG 的漸層技術有了更深入的理解:
最終的透明度調整(中心完全不透明,邊緣完全透明)是根據使用者的反饋進行的。這讓我意識到,使用者的直觀感受往往比技術理論更重要。
今天的開發過程讓我深刻體會到,細節決定體驗。一個看似簡單的節點樣式調整,實際上涉及了 SVG 技術、視覺設計、使用者體驗等多個層面。
從實色到半透明,從線性漸層到放射狀漸層,每一次調整都讓 GASO 的學習地圖變得更加精緻和吸引人。
現在,當使用者看到這些發光的節點時,他們會感受到一種探索的興奮感,就像古代探險家在地圖上發現了新的標記一樣。
因為學習 Google Apps Script,真的就像一場偉大的探險,而每一個節點,都是這場探險中的一個重要地標。
明天,我們將繼續探索 GASO 的可能性,讓這個學習地圖變得更加豐富和有趣。敬請期待!
如果想要看一些我的鐵人賽花邊心得,
也歡迎追蹤我的 Threads 和 Facebook