[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fopenlayers-image-adjust":3},{"id":4,"title":5,"body":6,"category":601,"date":602,"description":603,"draft":604,"extension":605,"meta":606,"navigation":100,"path":607,"readingTime":116,"seo":608,"stem":609,"tags":610,"__hash__":614},"blog\u002Fblog\u002Fopenlayers-image-adjust.md","OpenLayers 影像調整：CSS filter 與彈窗拖曳的互動問題",{"type":7,"value":8,"toc":589},"minimark",[9,13,18,21,24,188,191,250,253,276,280,287,290,293,297,300,303,306,365,368,384,387,390,393,546,556,559,562,568,571,585],[10,11,12],"p",{},"工作上有一個查看影像的彈窗，裡面用 OpenLayers 顯示影像，使用者可以平移和縮放。這次要加上調整明暗、對比和飽和度的功能，同時也修掉一個彈窗互動的問題。",[14,15,17],"h2",{"id":16},"用-css-filter-調整影像","用 CSS filter 調整影像",[10,19,20],{},"OpenLayers 預設用 canvas 繪製圖層。要改變影像外觀，我一開始想到的是讀取像素、重新計算，但這樣要處理每個圖磚，平移和縮放時還要重來。",[10,22,23],{},"其實需求只是「看起來」不同，不需要改變資料，所以直接在 canvas 元素上套用 CSS filter 就好：",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-js shiki shiki-themes github-dark","updateFilterStyle() {\n  const canvas = document.querySelector('#imageMap .ol-layer canvas')\n  if (!canvas) return\n\n  canvas.style.filter = `\n    contrast(${this.contrast \u002F 100})\n    saturate(${this.saturation \u002F 100})\n    brightness(${this.exposure \u002F 100})\n  `\n}\n","js","",[32,33,34,47,77,95,102,114,138,157,176,182],"code",{"__ignoreMap":30},[35,36,39,43],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"svObZ","updateFilterStyle",[35,44,46],{"class":45},"s95oV","() {\n",[35,48,50,54,58,61,64,67,70,74],{"class":37,"line":49},2,[35,51,53],{"class":52},"snl16","  const",[35,55,57],{"class":56},"sDLfK"," canvas",[35,59,60],{"class":52}," =",[35,62,63],{"class":45}," document.",[35,65,66],{"class":41},"querySelector",[35,68,69],{"class":45},"(",[35,71,73],{"class":72},"sU2Wk","'#imageMap .ol-layer canvas'",[35,75,76],{"class":45},")\n",[35,78,80,83,86,89,92],{"class":37,"line":79},3,[35,81,82],{"class":52},"  if",[35,84,85],{"class":45}," (",[35,87,88],{"class":52},"!",[35,90,91],{"class":45},"canvas) ",[35,93,94],{"class":52},"return\n",[35,96,98],{"class":37,"line":97},4,[35,99,101],{"emptyLinePlaceholder":100},true,"\n",[35,103,105,108,111],{"class":37,"line":104},5,[35,106,107],{"class":45},"  canvas.style.filter ",[35,109,110],{"class":52},"=",[35,112,113],{"class":72}," `\n",[35,115,117,120,123,126,129,132,135],{"class":37,"line":116},6,[35,118,119],{"class":72},"    contrast(${",[35,121,122],{"class":56},"this",[35,124,125],{"class":72},".",[35,127,128],{"class":45},"contrast",[35,130,131],{"class":52}," \u002F",[35,133,134],{"class":56}," 100",[35,136,137],{"class":72},"})\n",[35,139,141,144,146,148,151,153,155],{"class":37,"line":140},7,[35,142,143],{"class":72},"    saturate(${",[35,145,122],{"class":56},[35,147,125],{"class":72},[35,149,150],{"class":45},"saturation",[35,152,131],{"class":52},[35,154,134],{"class":56},[35,156,137],{"class":72},[35,158,160,163,165,167,170,172,174],{"class":37,"line":159},8,[35,161,162],{"class":72},"    brightness(${",[35,164,122],{"class":56},[35,166,125],{"class":72},[35,168,169],{"class":45},"exposure",[35,171,131],{"class":52},[35,173,134],{"class":56},[35,175,137],{"class":72},[35,177,179],{"class":37,"line":178},9,[35,180,181],{"class":72},"  `\n",[35,183,185],{"class":37,"line":184},10,[35,186,187],{"class":45},"}\n",[10,189,190],{},"介面用滑桿綁定數值，範圍 50 到 200，預設 100：",[25,192,196],{"className":193,"code":194,"language":195,"meta":30,"style":30},"language-html shiki shiki-themes github-dark","\u003Cv-slider v-model=\"contrast\" min=\"50\" max=\"200\" step=\"10\" @input=\"updateFilterStyle\" \u002F>\n","html",[32,197,198],{"__ignoreMap":30},[35,199,200,203,207,210,212,215,218,220,223,226,228,231,234,236,239,242,244,247],{"class":37,"line":38},[35,201,202],{"class":45},"\u003C",[35,204,206],{"class":205},"s4JwU","v-slider",[35,208,209],{"class":41}," v-model",[35,211,110],{"class":45},[35,213,214],{"class":72},"\"contrast\"",[35,216,217],{"class":41}," min",[35,219,110],{"class":45},[35,221,222],{"class":72},"\"50\"",[35,224,225],{"class":41}," max",[35,227,110],{"class":45},[35,229,230],{"class":72},"\"200\"",[35,232,233],{"class":41}," step",[35,235,110],{"class":45},[35,237,238],{"class":72},"\"10\"",[35,240,241],{"class":41}," @input",[35,243,110],{"class":45},[35,245,246],{"class":72},"\"updateFilterStyle\"",[35,248,249],{"class":45}," \u002F>\n",[10,251,252],{},"這個做法有幾個好處：",[254,255,256,264,270],"ul",{},[257,258,259,263],"li",{},[260,261,262],"strong",{},"效能好","：濾鏡由瀏覽器處理，不用重新繪製圖磚。",[257,265,266,269],{},[260,267,268],{},"不影響資料","：原始影像完全不變，重設數值就能還原。",[257,271,272,275],{},[260,273,274],{},"互動即時","：拖動滑桿時馬上看到結果。",[277,278,279],"h3",{"id":279},"範圍要限定",[10,281,282,283,286],{},"選擇器一定要指定到彈窗內的地圖。頁面上還有主地圖，如果只寫 ",[32,284,285],{},".ol-layer canvas","，就可能改到別的地圖。",[277,288,289],{"id":289},"什麼時候不適合",[10,291,292],{},"CSS filter 只改變顯示結果。如果要匯出調整後的影像，或是做像素分析，就要用 Canvas API 或 OpenLayers 的圖層處理。另外它會作用在整個 canvas 上，同一張 canvas 裡的所有圖層都會受到影響。",[14,294,296],{"id":295},"修-bug操作地圖時後面的視窗跟著動","修 Bug：操作地圖時，後面的視窗跟著動",[10,298,299],{},"這個專案有好幾個可拖曳的彈窗。使用者先開啟「訂單」視窗，再開啟「查看影像」視窗，然後在地圖上拖曳時，後面的訂單視窗竟然也跟著移動；拖曳比例尺的縮放工具時也一樣。",[277,301,302],{"id":302},"原因",[10,304,305],{},"拖曳功能是這樣註冊的：",[25,307,309],{"className":27,"code":308,"language":29,"meta":30,"style":30},"document.addEventListener('mousedown', (e) => {\n  if (e.target.tagName !== 'CANVAS') dragMouseDown(e)\n})\n",[32,310,311,340,361],{"__ignoreMap":30},[35,312,313,316,319,321,324,327,331,334,337],{"class":37,"line":38},[35,314,315],{"class":45},"document.",[35,317,318],{"class":41},"addEventListener",[35,320,69],{"class":45},[35,322,323],{"class":72},"'mousedown'",[35,325,326],{"class":45},", (",[35,328,330],{"class":329},"s9osk","e",[35,332,333],{"class":45},") ",[35,335,336],{"class":52},"=>",[35,338,339],{"class":45}," {\n",[35,341,342,344,347,350,353,355,358],{"class":37,"line":49},[35,343,82],{"class":52},[35,345,346],{"class":45}," (e.target.tagName ",[35,348,349],{"class":52},"!==",[35,351,352],{"class":72}," 'CANVAS'",[35,354,333],{"class":45},[35,356,357],{"class":41},"dragMouseDown",[35,359,360],{"class":45},"(e)\n",[35,362,363],{"class":37,"line":79},[35,364,137],{"class":45},[10,366,367],{},"問題有兩個：",[254,369,370,381],{},[257,371,372,373,376,377,380],{},"監聽器掛在 ",[32,374,375],{},"document","，",[260,378,379],{},"頁面上任何位置按下滑鼠都會觸發","，不只是彈窗本身。",[257,382,383],{},"判斷條件只排除了 canvas。地圖上的縮放工具不是 canvas，按下去就被當成要拖曳視窗。",[10,385,386],{},"換句話說，程式檢查的是「按到的不是什麼」，而不是「按到的是不是我」。",[277,388,389],{"id":389},"修正",[10,391,392],{},"改成確認事件真的發生在這個彈窗內，而且不是在地圖上：",[25,394,396],{"className":27,"code":395,"language":29,"meta":30,"style":30},"document.addEventListener('mousedown', (e) => {\n  const popup = document.getElementById('orderPopup')\n  const imageMap = document.querySelector('.imageMap')\n\n  const isInPopup = popup?.contains(e.target)\n  const isInMap = imageMap?.contains(e.target)\n\n  if (isInPopup && !isInMap && e.target.tagName !== 'CANVAS') {\n    dragMouseDown(e)\n  }\n})\n",[32,397,398,418,439,459,463,481,497,501,529,536,541],{"__ignoreMap":30},[35,399,400,402,404,406,408,410,412,414,416],{"class":37,"line":38},[35,401,315],{"class":45},[35,403,318],{"class":41},[35,405,69],{"class":45},[35,407,323],{"class":72},[35,409,326],{"class":45},[35,411,330],{"class":329},[35,413,333],{"class":45},[35,415,336],{"class":52},[35,417,339],{"class":45},[35,419,420,422,425,427,429,432,434,437],{"class":37,"line":49},[35,421,53],{"class":52},[35,423,424],{"class":56}," popup",[35,426,60],{"class":52},[35,428,63],{"class":45},[35,430,431],{"class":41},"getElementById",[35,433,69],{"class":45},[35,435,436],{"class":72},"'orderPopup'",[35,438,76],{"class":45},[35,440,441,443,446,448,450,452,454,457],{"class":37,"line":79},[35,442,53],{"class":52},[35,444,445],{"class":56}," imageMap",[35,447,60],{"class":52},[35,449,63],{"class":45},[35,451,66],{"class":41},[35,453,69],{"class":45},[35,455,456],{"class":72},"'.imageMap'",[35,458,76],{"class":45},[35,460,461],{"class":37,"line":97},[35,462,101],{"emptyLinePlaceholder":100},[35,464,465,467,470,472,475,478],{"class":37,"line":104},[35,466,53],{"class":52},[35,468,469],{"class":56}," isInPopup",[35,471,60],{"class":52},[35,473,474],{"class":45}," popup?.",[35,476,477],{"class":41},"contains",[35,479,480],{"class":45},"(e.target)\n",[35,482,483,485,488,490,493,495],{"class":37,"line":116},[35,484,53],{"class":52},[35,486,487],{"class":56}," isInMap",[35,489,60],{"class":52},[35,491,492],{"class":45}," imageMap?.",[35,494,477],{"class":41},[35,496,480],{"class":45},[35,498,499],{"class":37,"line":140},[35,500,101],{"emptyLinePlaceholder":100},[35,502,503,505,508,511,514,517,519,522,524,526],{"class":37,"line":159},[35,504,82],{"class":52},[35,506,507],{"class":45}," (isInPopup ",[35,509,510],{"class":52},"&&",[35,512,513],{"class":52}," !",[35,515,516],{"class":45},"isInMap ",[35,518,510],{"class":52},[35,520,521],{"class":45}," e.target.tagName ",[35,523,349],{"class":52},[35,525,352],{"class":72},[35,527,528],{"class":45},") {\n",[35,530,531,534],{"class":37,"line":178},[35,532,533],{"class":41},"    dragMouseDown",[35,535,360],{"class":45},[35,537,538],{"class":37,"line":184},[35,539,540],{"class":45},"  }\n",[35,542,544],{"class":37,"line":543},11,[35,545,137],{"class":45},[10,547,548,551,552,555],{},[32,549,550],{},"element.contains(target)"," 可以判斷點擊的目標是否在某個元素裡面，比單看 ",[32,553,554],{},"tagName"," 可靠。",[10,557,558],{},"這次修正後，地圖的平移和縮放就和視窗拖曳分開了，使用者調整影像時，後面的視窗不會再亂跑。",[277,560,561],{"id":561},"更好的寫法",[10,563,564,565,567],{},"如果重新設計，我不會把監聽器掛在 ",[32,566,375],{},"，而是掛在彈窗的標題列，只有抓著標題列才能拖曳。這樣根本不用判斷目標，也能避免監聽器在元件關閉後還留著。",[14,569,570],{"id":570},"這次學到的事",[254,572,573,576,582],{},[257,574,575],{},"先想需求是改變資料，還是只改變顯示。後者通常有更簡單的做法。",[257,577,578,579,581],{},"事件綁在 ",[32,580,375],{}," 時，要特別小心影響範圍。",[257,583,584],{},"條件寫成「確認是我」，比「排除不是我的」安全，因為排除清單永遠列不完。",[586,587,588],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":30,"searchDepth":49,"depth":49,"links":590},[591,595,600],{"id":16,"depth":49,"text":17,"children":592},[593,594],{"id":279,"depth":79,"text":279},{"id":289,"depth":79,"text":289},{"id":295,"depth":49,"text":296,"children":596},[597,598,599],{"id":302,"depth":79,"text":302},{"id":389,"depth":79,"text":389},{"id":561,"depth":79,"text":561},{"id":570,"depth":49,"text":570},"case","2025-12-01","在 OpenLayers 地圖上用 CSS filter 做即時的明暗、對比、飽和調整，並記錄彈窗內操作地圖時，後方視窗跟著移動的問題如何修正。",false,"md",{},"\u002Fblog\u002Fopenlayers-image-adjust",{"title":5,"description":603},"blog\u002Fopenlayers-image-adjust",[611,612,613],"OpenLayers","Canvas","Events","gwt78IkkoRd9FfjnkME3_P5nuwVi0Y3w5xDT54o1K3I"]