[{"data":1,"prerenderedAt":663},["ShallowReactive",2],{"content-/shogi-board-keyboard-svg":3,"all-pages-for-dir":661,"og-image-/shogi-board-keyboard-svg":662},{"id":4,"title":5,"body":6,"category":645,"description":646,"extension":647,"meta":648,"navigation":239,"ogImage":649,"path":650,"project_name":651,"published":652,"publishedAt":653,"seo":654,"stem":655,"tags":656,"todo":649,"unpublished":652,"updatedAt":649,"__hash__":660},"pages/2026-05/2026-05-09/shogi-board-keyboard-svg.md","将棋盤デモにキーボードショートカットと持ち駒ハイライト・SVG駒を実装",{"type":7,"value":8,"toc":633},"minimark",[9,13,21,24,27,30,35,65,307,312,316,331,424,439,443,461,543,557,560,573,576,579,601,604,629],[10,11,12],"h2",{"id":12},"やったこと",[14,15,16,20],"p",{},[17,18,19],"code",{},"apps/web/app/pages/shogi-board-demo.vue"," に3つの機能を追加した。矢印キーで局面を進ませ、持ち駒を選んだら打てる候補マスに青枠を浮かばせ、文字で書いていた持ち駒をSVG駒画像に置き換えた。3つの機能を1ファイル内で順に積み上げ、HMRが拾うのを目で確認しながら進めた。",[10,22,23],{"id":23},"背景",[14,25,26],{},"棋譜を1手ずつ送るたびにマウスでボタンへ手を伸ばしていた。指がキーボードから離れると目線も切れる。さらに持ち駒を選択しても、どこに打てるかは盤面を眺めて推測するしかなかった。持ち駒の表示自体も「角」「飛」と文字で書いていて、盤上のSVG駒と並ぶと違和感が残った。3つの不満を1ファイルにまとめて潰す。",[10,28,29],{"id":29},"実装の流れ",[31,32,34],"h3",{"id":33},"_1-矢印キーで局面を進める","1. 矢印キーで局面を進める",[14,36,37,40,41,44,45,48,49,52,53,56,57,60,61,64],{},[17,38,39],{},"onMounted"," で ",[17,42,43],{},"keydown"," リスナーを登録し、",[17,46,47],{},"onBeforeUnmount"," で解除する。",[17,50,51],{},"←"," と ",[17,54,55],{},"→"," を ",[17,58,59],{},"prev()"," / ",[17,62,63],{},"next()"," に割り当てる。",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-ts shiki shiki-themes vitesse-light vitesse-light","const onKeydown = (e: KeyboardEvent) => {\n  if (e.key === \"ArrowRight\") {\n    e.preventDefault()\n    next()\n  } else if (e.key === \"ArrowLeft\") {\n    e.preventDefault()\n    prev()\n  }\n}\n\nonMounted(() => window.addEventListener(\"keydown\", onKeydown))\nonBeforeUnmount(() => window.removeEventListener(\"keydown\", onKeydown))\n","ts","",[17,73,74,114,148,162,170,203,214,222,228,234,241,277],{"__ignoreMap":71},[75,76,79,83,87,91,94,98,101,105,108,111],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"stQ0i","const ",[75,84,86],{"class":85},"senZ8","onKeydown",[75,88,90],{"class":89},"shFtX"," =",[75,92,93],{"class":89}," (",[75,95,97],{"class":96},"s4oTP","e",[75,99,100],{"class":89},": ",[75,102,104],{"class":103},"sSkh3","KeyboardEvent",[75,106,107],{"class":89},")",[75,109,110],{"class":89}," =>",[75,112,113],{"class":89}," {\n",[75,115,117,121,123,125,128,131,134,138,142,144,146],{"class":77,"line":116},2,[75,118,120],{"class":119},"sHkkW","  if",[75,122,93],{"class":89},[75,124,97],{"class":96},[75,126,127],{"class":89},".",[75,129,130],{"class":96},"key",[75,132,133],{"class":81}," === ",[75,135,137],{"class":136},"sMJiu","\"",[75,139,141],{"class":140},"sdGka","ArrowRight",[75,143,137],{"class":136},[75,145,107],{"class":89},[75,147,113],{"class":89},[75,149,151,154,156,159],{"class":77,"line":150},3,[75,152,153],{"class":96},"    e",[75,155,127],{"class":89},[75,157,158],{"class":85},"preventDefault",[75,160,161],{"class":89},"()\n",[75,163,165,168],{"class":77,"line":164},4,[75,166,167],{"class":85},"    next",[75,169,161],{"class":89},[75,171,173,176,179,182,184,186,188,190,192,194,197,199,201],{"class":77,"line":172},5,[75,174,175],{"class":89},"  }",[75,177,178],{"class":119}," else",[75,180,181],{"class":119}," if",[75,183,93],{"class":89},[75,185,97],{"class":96},[75,187,127],{"class":89},[75,189,130],{"class":96},[75,191,133],{"class":81},[75,193,137],{"class":136},[75,195,196],{"class":140},"ArrowLeft",[75,198,137],{"class":136},[75,200,107],{"class":89},[75,202,113],{"class":89},[75,204,206,208,210,212],{"class":77,"line":205},6,[75,207,153],{"class":96},[75,209,127],{"class":89},[75,211,158],{"class":85},[75,213,161],{"class":89},[75,215,217,220],{"class":77,"line":216},7,[75,218,219],{"class":85},"    prev",[75,221,161],{"class":89},[75,223,225],{"class":77,"line":224},8,[75,226,227],{"class":89},"  }\n",[75,229,231],{"class":77,"line":230},9,[75,232,233],{"class":89},"}\n",[75,235,237],{"class":77,"line":236},10,[75,238,240],{"emptyLinePlaceholder":239},true,"\n",[75,242,244,246,249,251,254,256,259,262,264,266,268,271,274],{"class":77,"line":243},11,[75,245,39],{"class":85},[75,247,248],{"class":89},"(()",[75,250,110],{"class":89},[75,252,253],{"class":96}," window",[75,255,127],{"class":89},[75,257,258],{"class":85},"addEventListener",[75,260,261],{"class":89},"(",[75,263,137],{"class":136},[75,265,43],{"class":140},[75,267,137],{"class":136},[75,269,270],{"class":89},",",[75,272,273],{"class":96}," onKeydown",[75,275,276],{"class":89},"))\n",[75,278,280,282,284,286,288,290,293,295,297,299,301,303,305],{"class":77,"line":279},12,[75,281,47],{"class":85},[75,283,248],{"class":89},[75,285,110],{"class":89},[75,287,253],{"class":96},[75,289,127],{"class":89},[75,291,292],{"class":85},"removeEventListener",[75,294,261],{"class":89},[75,296,137],{"class":136},[75,298,43],{"class":140},[75,300,137],{"class":136},[75,302,270],{"class":89},[75,304,273],{"class":96},[75,306,276],{"class":89},[14,308,309,311],{},[17,310,158],{}," を入れないと、ページ全体が横スクロールに巻き込まれる場面があった。リスナー登録と解除のペアを忘れると、ページ遷移後もキーが効き続けてバグになる。",[31,313,315],{"id":314},"_2-持ち駒の打ち予告ハイライト","2. 持ち駒の打ち予告ハイライト",[14,317,318,319,322,323,326,327,330],{},"選択中の持ち駒を ",[17,320,321],{},"selectedHand"," で保持し、打てるマスを ",[17,324,325],{},"computed"," で算出する。盤面側のセルに ",[17,328,329],{},"is-droppable"," クラスを付けて青枠を出す。",[66,332,334],{"className":68,"code":333,"language":70,"meta":71,"style":71},"const droppableSquares = computed(() => {\n  if (!selectedHand.value) return new Set\u003Cstring>()\n  return computeDroppableSquares(board.value, selectedHand.value)\n})\n",[17,335,336,354,390,419],{"__ignoreMap":71},[75,337,338,340,343,345,348,350,352],{"class":77,"line":78},[75,339,82],{"class":81},[75,341,342],{"class":96},"droppableSquares",[75,344,90],{"class":89},[75,346,347],{"class":85}," computed",[75,349,248],{"class":89},[75,351,110],{"class":89},[75,353,113],{"class":89},[75,355,356,358,360,363,365,367,370,372,375,378,381,384,387],{"class":77,"line":116},[75,357,120],{"class":119},[75,359,93],{"class":89},[75,361,362],{"class":81},"!",[75,364,321],{"class":96},[75,366,127],{"class":89},[75,368,369],{"class":96},"value",[75,371,107],{"class":89},[75,373,374],{"class":119}," return",[75,376,377],{"class":81}," new ",[75,379,380],{"class":85},"Set",[75,382,383],{"class":89},"\u003C",[75,385,386],{"class":103},"string",[75,388,389],{"class":89},">()\n",[75,391,392,395,398,400,403,405,407,409,412,414,416],{"class":77,"line":150},[75,393,394],{"class":119},"  return",[75,396,397],{"class":85}," computeDroppableSquares",[75,399,261],{"class":89},[75,401,402],{"class":96},"board",[75,404,127],{"class":89},[75,406,369],{"class":96},[75,408,270],{"class":89},[75,410,411],{"class":96}," selectedHand",[75,413,127],{"class":89},[75,415,369],{"class":96},[75,417,418],{"class":89},")\n",[75,420,421],{"class":77,"line":164},[75,422,423],{"class":89},"})\n",[14,425,426,427,430,431,434,435,438],{},"純粋関数 ",[17,428,429],{},"computeDroppableSquares"," に板状態と駒種を渡し、Setで返す。レンダリング側は ",[17,432,433],{},"droppableSquares.value.has(key)"," で即座に判定する。最初は配列で返していたが、マスごとに ",[17,436,437],{},"includes"," を回すと81マス分のループが走るため、Setに変えた。",[31,440,442],{"id":441},"_3-持ち駒をsvg駒に置き換え","3. 持ち駒をSVG駒に置き換え",[14,444,445,446,449,450,52,453,456,457,460],{},"持ち駒UIで ",[17,447,448],{},"\u003Cspan>角\u003C/span>"," のように描いていた箇所を、盤上と同じSVGコンポーネントへ差し替えた。CSSがSVGに合わなかったので、",[17,451,452],{},"width: 100%",[17,454,455],{},"height: auto"," を当て、親の ",[17,458,459],{},"display: flex"," で中央寄せに直した。",[66,462,466],{"className":463,"code":464,"language":465,"meta":71,"style":71},"language-vue shiki shiki-themes vitesse-light vitesse-light","\u003Cbutton class=\"hand-piece\" :class=\"{ selected: selectedHand === piece }\">\n  \u003CShogiPieceSvg :piece=\"piece\" />\n  \u003Cspan class=\"count\">×{{ count }}\u003C/span>\n\u003C/button>\n","vue",[17,467,468,523,529,534],{"__ignoreMap":71},[75,469,470,472,475,478,481,483,486,488,491,494,496,498,501,505,507,509,512,515,518,520],{"class":77,"line":78},[75,471,383],{"class":89},[75,473,474],{"class":119},"button",[75,476,477],{"class":96}," class",[75,479,480],{"class":89},"=",[75,482,137],{"class":136},[75,484,485],{"class":140},"hand-piece",[75,487,137],{"class":136},[75,489,490],{"class":89}," :",[75,492,493],{"class":85},"class",[75,495,480],{"class":89},[75,497,137],{"class":89},[75,499,500],{"class":89},"{ ",[75,502,504],{"class":503},"sz8Xr","selected",[75,506,100],{"class":89},[75,508,321],{"class":96},[75,510,511],{"class":81}," ===",[75,513,514],{"class":96}," piece",[75,516,517],{"class":89}," }",[75,519,137],{"class":89},[75,521,522],{"class":89},">\n",[75,524,525],{"class":77,"line":116},[75,526,528],{"class":527},"sG7-3","  \u003CShogiPieceSvg :piece=\"piece\" />\n",[75,530,531],{"class":77,"line":150},[75,532,533],{"class":527},"  \u003Cspan class=\"count\">×{{ count }}\u003C/span>\n",[75,535,536,539,541],{"class":77,"line":164},[75,537,538],{"class":89},"\u003C/",[75,540,474],{"class":119},[75,542,522],{"class":89},[14,544,545,546,548,549,552,553,556],{},"文字幅基準で組んでいた CSS のままだとSVGが小さく潰れた。",[17,547,485],{}," の ",[17,550,551],{},"min-width"," を SVG のアスペクト比に合わせて広げ、選択中は ",[17,554,555],{},"outline: 2px solid #2563eb"," で青枠を浮かばせる。盤上のハイライトと色を揃えると、視線の往復が滑らかになる。",[10,558,559],{"id":559},"動作確認",[14,561,562,563,566,567,569,570,572],{},"Chrome DevTools MCP で ",[17,564,565],{},"localhost:3000/pages/shogi-board-demo"," を開き、HMRが反映されたタイミングで目視確認した。先手の持ち駒「角」をクリックすると青枠が乗り、盤上の打てるマスも同じ青で浮き上がった。",[17,568,55],{}," キーを押すと棋譜が1手進み、",[17,571,51],{}," で戻った。SVG駒は文字版より小さくなりすぎず、盤上の駒と同じ筆致で並んだ。",[14,574,575],{},"agent-browser ではなく Chrome DevTools MCP を選んだのは、ローカルの Vite dev サーバーに直接 Devtools プロトコルで繋いだ方がHMRイベントも観察できるため。",[10,577,578],{"id":578},"学びメモ",[580,581,582,590,595,598],"ul",{},[583,584,585,548,587,589],"li",{},[17,586,43],{},[17,588,158],{}," を入れ忘れると、ページが横スクロールを始めて盤面が画面外へ滑った",[583,591,592,594],{},[17,593,342],{}," を配列ではなくSetで返したら、81マスのレンダリング時間が指でわかるレベルで縮んだ",[583,596,597],{},"文字とSVGを混在させていた持ち駒UIは、片方をSVGに揃えるだけで盤面との視線の引っかかりが消えた",[583,599,600],{},"盤面の打ち予告と持ち駒の選択枠を同じ青色にしたら、「この駒をここに打つ」の対応関係が一目で繋がった",[10,602,603],{"id":603},"次にやること",[580,605,608,617,623],{"className":606},[607],"contains-task-list",[583,609,612,616],{"className":610},[611],"task-list-item",[613,614],"input",{"disabled":239,"type":615},"checkbox"," 数字キー（1〜9）で段選択、a〜iで筋選択のショートカットを試す",[583,618,620,622],{"className":619},[611],[613,621],{"disabled":239,"type":615}," 持ち駒が0枚のときは選択不可にする（現状は0枚でもクリックできてしまう）",[583,624,626,628],{"className":625},[611],[613,627],{"disabled":239,"type":615}," 打ち予告ハイライトに二歩・打ち歩詰めの除外ロジックを足す",[630,631,632],"style",{},"html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sSkh3, html code.shiki .sSkh3{--shiki-default:#2E8F82;--shiki-dark:#2E8F82}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}",{"title":71,"searchDepth":116,"depth":116,"links":634},[635,636,637,642,643,644],{"id":12,"depth":116,"text":12},{"id":23,"depth":116,"text":23},{"id":29,"depth":116,"text":29,"children":638},[639,640,641],{"id":33,"depth":150,"text":34},{"id":314,"depth":150,"text":315},{"id":441,"depth":150,"text":442},{"id":559,"depth":116,"text":559},{"id":578,"depth":116,"text":578},{"id":603,"depth":116,"text":603},"dev","shogi-board-demo.vueに矢印キー操作、持ち駒選択時の打ち予告ハイライト、持ち駒のSVG駒画像化を追加した実装ログ。","md",{},null,"/shogi-board-keyboard-svg","mdx-playground",false,"2026-05-09T00:00:00.000Z",{"title":5,"description":646},"2026-05/2026-05-09/shogi-board-keyboard-svg",[657,658,659],"Vue","将棋","UX改善","N6SsXuLZyuhp568sIkDSdLUAMWQ9BOiobvXjTd7Kmdg",[],"https://log.eurekapu.com/og/blog/shogi-board-keyboard-svg.png?v=2026-05-09T00%3A00%3A00.000Z&title=%E5%B0%86%E6%A3%8B%E7%9B%A4%E3%83%87%E3%83%A2%E3%81%AB%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88%E3%81%A8%E6%8C%81%E3%81%A1%E9%A7%92%E3%83%8F%E3%82%A4%E3%83%A9%E3%82%A4%E3%83%88%E3%83%BBSVG%E9%A7%92%E3%82%92%E5%AE%9F%E8%A3%85&author=Kei%20Komatsu&sig=d3f8041f9d950e98",1782528834631]