[{"data":1,"prerenderedAt":2283},["ShallowReactive",2],{"content-query-NweA5xABCV":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"tags":11,"rowTypeId":18,"sitemap":19,"body":20,"_type":2277,"_id":2278,"_source":2279,"_file":2280,"_stem":2281,"_extension":2282},"/articles/tech/development/yominchu-vertical-canvas","development",false,"","【解説】Canvas で縦書き日本語組版を実装する！詠み人の描画方法","和歌・短歌・俳句を画像化するツール「詠み人」で、CSS writing-mode を使わず Canvas で縦書きを実装した理由と、句読点や括弧などの特殊文字の扱いをまとめます。","2026-04-16",[12,13,14,15,16,17],"Canvas","日本語組版","縦書き","TypeScript","フォント","詠み人",1,{"loc":4,"lastmod":10,"priority":18},{"type":21,"children":22,"toc":2263},"root",[23,31,53,74,91,96,174,187,192,204,209,214,833,853,873,879,899,912,1718,1724,1744,1750,1777,1789,1794,1806,1811,1988,2014,2019,2038,2051,2182,2203,2208,2252,2257],{"type":24,"tag":25,"props":26,"children":28},"element","h2",{"id":27},"はじめに",[29],{"type":30,"value":27},"text",{"type":24,"tag":32,"props":33,"children":34},"p",{},[35,37,43,45,51],{"type":30,"value":36},"和歌・短歌・俳句を縦書きの美しい画像にして SNS に投稿できるツール ",{"type":24,"tag":38,"props":39,"children":41},"a",{"href":40},"/tools/yominchu",[42],{"type":30,"value":17},{"type":30,"value":44}," を作ったとき、最大の悩みどころは「",{"type":24,"tag":46,"props":47,"children":48},"strong",{},[49],{"type":30,"value":50},"縦書きレイアウトをどう描画するか",{"type":30,"value":52},"」でした。",{"type":24,"tag":32,"props":54,"children":55},{},[56,58,65,67,72],{"type":30,"value":57},"ブラウザには ",{"type":24,"tag":59,"props":60,"children":62},"code",{"className":61},[],[63],{"type":30,"value":64},"writing-mode: vertical-rl",{"type":30,"value":66}," という CSS プロパティがあり、HTML 上で縦書きを表現することはできます。けれども詠み人では最終的に ",{"type":24,"tag":46,"props":68,"children":69},{},[70],{"type":30,"value":71},"Canvas で1文字ずつ描画する",{"type":30,"value":73}," 方針にしました。この記事では、その判断と、ハマった特殊文字の扱いをまとめます。",{"type":24,"tag":75,"props":76,"children":77},"summary-box",{},[78],{"type":24,"tag":32,"props":79,"children":80},{},[81,83,89],{"type":30,"value":82},"CSS ",{"type":24,"tag":59,"props":84,"children":86},{"className":85},[],[87],{"type":30,"value":88},"writing-mode",{"type":30,"value":90}," ではなく Canvas 描画を選んだのは、「最終成果物が画像である」という用途の特殊性からでした。句読点の位置調整、長音・括弧の90度回転、フォント読み込み待機など、日本語組版固有の細かいルールを自前でコントロールする必要があり、結果的に Canvas のほうが制御しやすかったです。",{"type":24,"tag":25,"props":92,"children":94},{"id":93},"環境",[95],{"type":30,"value":93},{"type":24,"tag":97,"props":98,"children":99},"table",{},[100,119],{"type":24,"tag":101,"props":102,"children":103},"thead",{},[104],{"type":24,"tag":105,"props":106,"children":107},"tr",{},[108,114],{"type":24,"tag":109,"props":110,"children":111},"th",{},[112],{"type":30,"value":113},"項目",{"type":24,"tag":109,"props":115,"children":116},{},[117],{"type":30,"value":118},"技術",{"type":24,"tag":120,"props":121,"children":122},"tbody",{},[123,137,149,162],{"type":24,"tag":105,"props":124,"children":125},{},[126,132],{"type":24,"tag":127,"props":128,"children":129},"td",{},[130],{"type":30,"value":131},"フレームワーク",{"type":24,"tag":127,"props":133,"children":134},{},[135],{"type":30,"value":136},"Nuxt 4 + Vue 3",{"type":24,"tag":105,"props":138,"children":139},{},[140,145],{"type":24,"tag":127,"props":141,"children":142},{},[143],{"type":30,"value":144},"言語",{"type":24,"tag":127,"props":146,"children":147},{},[148],{"type":30,"value":15},{"type":24,"tag":105,"props":150,"children":151},{},[152,157],{"type":24,"tag":127,"props":153,"children":154},{},[155],{"type":30,"value":156},"描画",{"type":24,"tag":127,"props":158,"children":159},{},[160],{"type":30,"value":161},"HTML Canvas 2D",{"type":24,"tag":105,"props":163,"children":164},{},[165,169],{"type":24,"tag":127,"props":166,"children":167},{},[168],{"type":30,"value":16},{"type":24,"tag":127,"props":170,"children":171},{},[172],{"type":30,"value":173},"Noto Serif JP + 同梱フォント（力弱・行書・隷書）",{"type":24,"tag":25,"props":175,"children":177},{"id":176},"なぜ-css-writing-mode-を選ばなかったのか",[178,180,185],{"type":30,"value":179},"なぜ CSS ",{"type":24,"tag":59,"props":181,"children":183},{"className":182},[],[184],{"type":30,"value":88},{"type":30,"value":186}," を選ばなかったのか",{"type":24,"tag":32,"props":188,"children":189},{},[190],{"type":30,"value":191},"詠み人は最終的に「1枚の PNG 画像」を生成するツールです。ユーザーはそれを Twitter/X にシェアしたり、画像としてダウンロードしたりします。",{"type":24,"tag":32,"props":193,"children":194},{},[195,197,202],{"type":30,"value":196},"「",{"type":24,"tag":46,"props":198,"children":199},{},[200],{"type":30,"value":201},"実装者が描画の全てをコントロールしたい",{"type":30,"value":203},"」という要望にいちばん素直に応えてくれたのが、Canvas で1文字ずつ fillText する方式でした。後々背景を追加するなどを考えた際にも Canvas は扱いやすいと考えました。",{"type":24,"tag":25,"props":205,"children":207},{"id":206},"基本のカラムレイアウト",[208],{"type":30,"value":206},{"type":24,"tag":32,"props":210,"children":211},{},[212],{"type":30,"value":213},"縦書きは「右から左へ」行が並びます。詠み人ではこれを次のように実装しています。",{"type":24,"tag":215,"props":216,"children":220},"pre",{"className":217,"code":218,"language":219,"meta":7,"style":7},"language-typescript shiki shiki-themes vitesse-dark","validLines.forEach((line, ci) => {\n  const x = poemStartX - ci * colW\n  const chars = [...line]\n  const fs = Math.min(46, Math.floor((H - 130) / Math.max(chars.length, 1)))\n  const lh = fs + 6\n\n  ctx.fillStyle = style.text\n  ctx.font = `400 ${fs}px ${fontFamily}`\n  ctx.textAlign = 'center'\n\n  chars.forEach((ch, ci2) => {\n    drawVerticalChar(ctx, ch, x, startY + ci2 * lh, fs, fontId)\n  })\n})\n","typescript",[221],{"type":24,"tag":59,"props":222,"children":223},{"__ignoreMap":7},[224,281,326,357,489,521,531,567,638,674,682,729,815,824],{"type":24,"tag":225,"props":226,"children":228},"span",{"class":227,"line":18},"line",[229,235,241,247,252,256,261,266,271,276],{"type":24,"tag":225,"props":230,"children":232},{"style":231},"--shiki-default:#BD976A",[233],{"type":30,"value":234},"validLines",{"type":24,"tag":225,"props":236,"children":238},{"style":237},"--shiki-default:#666666",[239],{"type":30,"value":240},".",{"type":24,"tag":225,"props":242,"children":244},{"style":243},"--shiki-default:#80A665",[245],{"type":30,"value":246},"forEach",{"type":24,"tag":225,"props":248,"children":249},{"style":237},[250],{"type":30,"value":251},"((",{"type":24,"tag":225,"props":253,"children":254},{"style":231},[255],{"type":30,"value":227},{"type":24,"tag":225,"props":257,"children":258},{"style":237},[259],{"type":30,"value":260},",",{"type":24,"tag":225,"props":262,"children":263},{"style":231},[264],{"type":30,"value":265}," ci",{"type":24,"tag":225,"props":267,"children":268},{"style":237},[269],{"type":30,"value":270},")",{"type":24,"tag":225,"props":272,"children":273},{"style":237},[274],{"type":30,"value":275}," =>",{"type":24,"tag":225,"props":277,"children":278},{"style":237},[279],{"type":30,"value":280}," {\n",{"type":24,"tag":225,"props":282,"children":284},{"class":227,"line":283},2,[285,291,296,301,306,311,316,321],{"type":24,"tag":225,"props":286,"children":288},{"style":287},"--shiki-default:#CB7676",[289],{"type":30,"value":290},"  const ",{"type":24,"tag":225,"props":292,"children":293},{"style":231},[294],{"type":30,"value":295},"x",{"type":24,"tag":225,"props":297,"children":298},{"style":237},[299],{"type":30,"value":300}," =",{"type":24,"tag":225,"props":302,"children":303},{"style":231},[304],{"type":30,"value":305}," poemStartX",{"type":24,"tag":225,"props":307,"children":308},{"style":287},[309],{"type":30,"value":310}," - ",{"type":24,"tag":225,"props":312,"children":313},{"style":231},[314],{"type":30,"value":315},"ci",{"type":24,"tag":225,"props":317,"children":318},{"style":287},[319],{"type":30,"value":320}," * ",{"type":24,"tag":225,"props":322,"children":323},{"style":231},[324],{"type":30,"value":325},"colW\n",{"type":24,"tag":225,"props":327,"children":329},{"class":227,"line":328},3,[330,334,339,343,348,352],{"type":24,"tag":225,"props":331,"children":332},{"style":287},[333],{"type":30,"value":290},{"type":24,"tag":225,"props":335,"children":336},{"style":231},[337],{"type":30,"value":338},"chars",{"type":24,"tag":225,"props":340,"children":341},{"style":237},[342],{"type":30,"value":300},{"type":24,"tag":225,"props":344,"children":345},{"style":237},[346],{"type":30,"value":347}," [...",{"type":24,"tag":225,"props":349,"children":350},{"style":231},[351],{"type":30,"value":227},{"type":24,"tag":225,"props":353,"children":354},{"style":237},[355],{"type":30,"value":356},"]\n",{"type":24,"tag":225,"props":358,"children":360},{"class":227,"line":359},4,[361,365,370,374,379,383,388,393,399,403,407,411,416,420,425,429,434,438,443,448,452,457,461,465,469,475,479,484],{"type":24,"tag":225,"props":362,"children":363},{"style":287},[364],{"type":30,"value":290},{"type":24,"tag":225,"props":366,"children":367},{"style":231},[368],{"type":30,"value":369},"fs",{"type":24,"tag":225,"props":371,"children":372},{"style":237},[373],{"type":30,"value":300},{"type":24,"tag":225,"props":375,"children":376},{"style":231},[377],{"type":30,"value":378}," Math",{"type":24,"tag":225,"props":380,"children":381},{"style":237},[382],{"type":30,"value":240},{"type":24,"tag":225,"props":384,"children":385},{"style":243},[386],{"type":30,"value":387},"min",{"type":24,"tag":225,"props":389,"children":390},{"style":237},[391],{"type":30,"value":392},"(",{"type":24,"tag":225,"props":394,"children":396},{"style":395},"--shiki-default:#4C9A91",[397],{"type":30,"value":398},"46",{"type":24,"tag":225,"props":400,"children":401},{"style":237},[402],{"type":30,"value":260},{"type":24,"tag":225,"props":404,"children":405},{"style":231},[406],{"type":30,"value":378},{"type":24,"tag":225,"props":408,"children":409},{"style":237},[410],{"type":30,"value":240},{"type":24,"tag":225,"props":412,"children":413},{"style":243},[414],{"type":30,"value":415},"floor",{"type":24,"tag":225,"props":417,"children":418},{"style":237},[419],{"type":30,"value":251},{"type":24,"tag":225,"props":421,"children":422},{"style":231},[423],{"type":30,"value":424},"H",{"type":24,"tag":225,"props":426,"children":427},{"style":287},[428],{"type":30,"value":310},{"type":24,"tag":225,"props":430,"children":431},{"style":395},[432],{"type":30,"value":433},"130",{"type":24,"tag":225,"props":435,"children":436},{"style":237},[437],{"type":30,"value":270},{"type":24,"tag":225,"props":439,"children":440},{"style":287},[441],{"type":30,"value":442}," / ",{"type":24,"tag":225,"props":444,"children":445},{"style":231},[446],{"type":30,"value":447},"Math",{"type":24,"tag":225,"props":449,"children":450},{"style":237},[451],{"type":30,"value":240},{"type":24,"tag":225,"props":453,"children":454},{"style":243},[455],{"type":30,"value":456},"max",{"type":24,"tag":225,"props":458,"children":459},{"style":237},[460],{"type":30,"value":392},{"type":24,"tag":225,"props":462,"children":463},{"style":231},[464],{"type":30,"value":338},{"type":24,"tag":225,"props":466,"children":467},{"style":237},[468],{"type":30,"value":240},{"type":24,"tag":225,"props":470,"children":472},{"style":471},"--shiki-default:#B8A965",[473],{"type":30,"value":474},"length",{"type":24,"tag":225,"props":476,"children":477},{"style":237},[478],{"type":30,"value":260},{"type":24,"tag":225,"props":480,"children":481},{"style":395},[482],{"type":30,"value":483}," 1",{"type":24,"tag":225,"props":485,"children":486},{"style":237},[487],{"type":30,"value":488},")))\n",{"type":24,"tag":225,"props":490,"children":492},{"class":227,"line":491},5,[493,497,502,506,511,516],{"type":24,"tag":225,"props":494,"children":495},{"style":287},[496],{"type":30,"value":290},{"type":24,"tag":225,"props":498,"children":499},{"style":231},[500],{"type":30,"value":501},"lh",{"type":24,"tag":225,"props":503,"children":504},{"style":237},[505],{"type":30,"value":300},{"type":24,"tag":225,"props":507,"children":508},{"style":231},[509],{"type":30,"value":510}," fs",{"type":24,"tag":225,"props":512,"children":513},{"style":287},[514],{"type":30,"value":515}," + ",{"type":24,"tag":225,"props":517,"children":518},{"style":395},[519],{"type":30,"value":520},"6\n",{"type":24,"tag":225,"props":522,"children":524},{"class":227,"line":523},6,[525],{"type":24,"tag":225,"props":526,"children":528},{"emptyLinePlaceholder":527},true,[529],{"type":30,"value":530},"\n",{"type":24,"tag":225,"props":532,"children":534},{"class":227,"line":533},7,[535,540,544,549,553,558,562],{"type":24,"tag":225,"props":536,"children":537},{"style":231},[538],{"type":30,"value":539},"  ctx",{"type":24,"tag":225,"props":541,"children":542},{"style":237},[543],{"type":30,"value":240},{"type":24,"tag":225,"props":545,"children":546},{"style":231},[547],{"type":30,"value":548},"fillStyle",{"type":24,"tag":225,"props":550,"children":551},{"style":237},[552],{"type":30,"value":300},{"type":24,"tag":225,"props":554,"children":555},{"style":231},[556],{"type":30,"value":557}," style",{"type":24,"tag":225,"props":559,"children":560},{"style":237},[561],{"type":30,"value":240},{"type":24,"tag":225,"props":563,"children":564},{"style":231},[565],{"type":30,"value":566},"text\n",{"type":24,"tag":225,"props":568,"children":570},{"class":227,"line":569},8,[571,575,579,584,588,594,600,606,610,615,620,624,629,633],{"type":24,"tag":225,"props":572,"children":573},{"style":231},[574],{"type":30,"value":539},{"type":24,"tag":225,"props":576,"children":577},{"style":237},[578],{"type":30,"value":240},{"type":24,"tag":225,"props":580,"children":581},{"style":231},[582],{"type":30,"value":583},"font",{"type":24,"tag":225,"props":585,"children":586},{"style":237},[587],{"type":30,"value":300},{"type":24,"tag":225,"props":589,"children":591},{"style":590},"--shiki-default:#C98A7D77",[592],{"type":30,"value":593}," `",{"type":24,"tag":225,"props":595,"children":597},{"style":596},"--shiki-default:#C98A7D",[598],{"type":30,"value":599},"400 ",{"type":24,"tag":225,"props":601,"children":603},{"style":602},"--shiki-default:#4D9375",[604],{"type":30,"value":605},"${",{"type":24,"tag":225,"props":607,"children":608},{"style":596},[609],{"type":30,"value":369},{"type":24,"tag":225,"props":611,"children":612},{"style":602},[613],{"type":30,"value":614},"}",{"type":24,"tag":225,"props":616,"children":617},{"style":596},[618],{"type":30,"value":619},"px ",{"type":24,"tag":225,"props":621,"children":622},{"style":602},[623],{"type":30,"value":605},{"type":24,"tag":225,"props":625,"children":626},{"style":596},[627],{"type":30,"value":628},"fontFamily",{"type":24,"tag":225,"props":630,"children":631},{"style":602},[632],{"type":30,"value":614},{"type":24,"tag":225,"props":634,"children":635},{"style":590},[636],{"type":30,"value":637},"`\n",{"type":24,"tag":225,"props":639,"children":641},{"class":227,"line":640},9,[642,646,650,655,659,664,669],{"type":24,"tag":225,"props":643,"children":644},{"style":231},[645],{"type":30,"value":539},{"type":24,"tag":225,"props":647,"children":648},{"style":237},[649],{"type":30,"value":240},{"type":24,"tag":225,"props":651,"children":652},{"style":231},[653],{"type":30,"value":654},"textAlign",{"type":24,"tag":225,"props":656,"children":657},{"style":237},[658],{"type":30,"value":300},{"type":24,"tag":225,"props":660,"children":661},{"style":590},[662],{"type":30,"value":663}," '",{"type":24,"tag":225,"props":665,"children":666},{"style":596},[667],{"type":30,"value":668},"center",{"type":24,"tag":225,"props":670,"children":671},{"style":590},[672],{"type":30,"value":673},"'\n",{"type":24,"tag":225,"props":675,"children":677},{"class":227,"line":676},10,[678],{"type":24,"tag":225,"props":679,"children":680},{"emptyLinePlaceholder":527},[681],{"type":30,"value":530},{"type":24,"tag":225,"props":683,"children":685},{"class":227,"line":684},11,[686,691,695,699,703,708,712,717,721,725],{"type":24,"tag":225,"props":687,"children":688},{"style":231},[689],{"type":30,"value":690},"  chars",{"type":24,"tag":225,"props":692,"children":693},{"style":237},[694],{"type":30,"value":240},{"type":24,"tag":225,"props":696,"children":697},{"style":243},[698],{"type":30,"value":246},{"type":24,"tag":225,"props":700,"children":701},{"style":237},[702],{"type":30,"value":251},{"type":24,"tag":225,"props":704,"children":705},{"style":231},[706],{"type":30,"value":707},"ch",{"type":24,"tag":225,"props":709,"children":710},{"style":237},[711],{"type":30,"value":260},{"type":24,"tag":225,"props":713,"children":714},{"style":231},[715],{"type":30,"value":716}," ci2",{"type":24,"tag":225,"props":718,"children":719},{"style":237},[720],{"type":30,"value":270},{"type":24,"tag":225,"props":722,"children":723},{"style":237},[724],{"type":30,"value":275},{"type":24,"tag":225,"props":726,"children":727},{"style":237},[728],{"type":30,"value":280},{"type":24,"tag":225,"props":730,"children":732},{"class":227,"line":731},12,[733,738,742,747,751,756,760,765,769,774,779,783,788,793,797,801,805,810],{"type":24,"tag":225,"props":734,"children":735},{"style":243},[736],{"type":30,"value":737},"    drawVerticalChar",{"type":24,"tag":225,"props":739,"children":740},{"style":237},[741],{"type":30,"value":392},{"type":24,"tag":225,"props":743,"children":744},{"style":231},[745],{"type":30,"value":746},"ctx",{"type":24,"tag":225,"props":748,"children":749},{"style":237},[750],{"type":30,"value":260},{"type":24,"tag":225,"props":752,"children":753},{"style":231},[754],{"type":30,"value":755}," ch",{"type":24,"tag":225,"props":757,"children":758},{"style":237},[759],{"type":30,"value":260},{"type":24,"tag":225,"props":761,"children":762},{"style":231},[763],{"type":30,"value":764}," x",{"type":24,"tag":225,"props":766,"children":767},{"style":237},[768],{"type":30,"value":260},{"type":24,"tag":225,"props":770,"children":771},{"style":231},[772],{"type":30,"value":773}," startY",{"type":24,"tag":225,"props":775,"children":776},{"style":287},[777],{"type":30,"value":778}," +",{"type":24,"tag":225,"props":780,"children":781},{"style":231},[782],{"type":30,"value":716},{"type":24,"tag":225,"props":784,"children":785},{"style":287},[786],{"type":30,"value":787}," *",{"type":24,"tag":225,"props":789,"children":790},{"style":231},[791],{"type":30,"value":792}," lh",{"type":24,"tag":225,"props":794,"children":795},{"style":237},[796],{"type":30,"value":260},{"type":24,"tag":225,"props":798,"children":799},{"style":231},[800],{"type":30,"value":510},{"type":24,"tag":225,"props":802,"children":803},{"style":237},[804],{"type":30,"value":260},{"type":24,"tag":225,"props":806,"children":807},{"style":231},[808],{"type":30,"value":809}," fontId",{"type":24,"tag":225,"props":811,"children":812},{"style":237},[813],{"type":30,"value":814},")\n",{"type":24,"tag":225,"props":816,"children":818},{"class":227,"line":817},13,[819],{"type":24,"tag":225,"props":820,"children":821},{"style":237},[822],{"type":30,"value":823},"  })\n",{"type":24,"tag":225,"props":825,"children":827},{"class":227,"line":826},14,[828],{"type":24,"tag":225,"props":829,"children":830},{"style":237},[831],{"type":30,"value":832},"})\n",{"type":24,"tag":32,"props":834,"children":835},{},[836,838,844,846,851],{"type":30,"value":837},"行ごとに x座標を ",{"type":24,"tag":59,"props":839,"children":841},{"className":840},[],[842],{"type":30,"value":843},"colW",{"type":30,"value":845}," だけ左にずらしていくのが「右から左へ」の実現です。各列の中では、文字を上から下に ",{"type":24,"tag":59,"props":847,"children":849},{"className":848},[],[850],{"type":30,"value":501},{"type":30,"value":852},"（= フォントサイズ + 6px）の間隔で配置していきます。",{"type":24,"tag":32,"props":854,"children":855},{},[856,858,864,866,871],{"type":30,"value":857},"行の文字数に応じてフォントサイズを ",{"type":24,"tag":59,"props":859,"children":861},{"className":860},[],[862],{"type":30,"value":863},"Math.floor((H - 130) / chars.length)",{"type":30,"value":865}," で動的に決めているのもポイントです。長い句でも必ずキャンバス内に収まるように、",{"type":24,"tag":46,"props":867,"children":868},{},[869],{"type":30,"value":870},"フォントサイズのほうを縮める",{"type":30,"value":872}," 方針にしました。",{"type":24,"tag":25,"props":874,"children":876},{"id":875},"特殊文字の扱い-詠み人最大のハマりどころ",[877],{"type":30,"value":878},"特殊文字の扱い — 詠み人最大のハマりどころ",{"type":24,"tag":32,"props":880,"children":881},{},[882,884,889,891,897],{"type":30,"value":883},"縦書き日本語組版で一筋縄でいかないのが、",{"type":24,"tag":46,"props":885,"children":886},{},[887],{"type":30,"value":888},"句読点・括弧・長音符",{"type":30,"value":890}," などの特殊文字です。これらを素直に ",{"type":24,"tag":59,"props":892,"children":894},{"className":893},[],[895],{"type":30,"value":896},"fillText",{"type":30,"value":898}," すると、向きや位置が崩れて読めない画像になります。",{"type":24,"tag":32,"props":900,"children":901},{},[902,904,910],{"type":30,"value":903},"詠み人では ",{"type":24,"tag":59,"props":905,"children":907},{"className":906},[],[908],{"type":30,"value":909},"drawVerticalChar",{"type":30,"value":911}," の中でこれらを個別処理しています。",{"type":24,"tag":215,"props":913,"children":915},{"className":217,"code":914,"language":219,"meta":7,"style":7},"const ROTATE_CHARS = new Set('ー〜「」『』…・（）')\nconst TOP_RIGHT_CHARS = new Set('、。')\n\nfunction drawVerticalChar(ctx, ch, x, y, fontSize, fontId) {\n  if (TOP_RIGHT_CHARS.has(ch)) {\n    // 句読点: 右上に小さめに配置\n    ctx.save()\n    const size = fontSize * 0.5\n    ctx.font = ctx.font.replace(/\\d+px/, `${size}px`)\n    ctx.fillText(ch, x + fontSize * 0.35, y - fontSize * 0.3)\n    ctx.restore()\n  } else if (ROTATE_CHARS.has(ch)) {\n    // 長音・括弧等: 90度回転\n    ctx.save()\n    ctx.translate(x, y - fontSize * 0.35)\n    ctx.rotate(Math.PI / 2)\n    ctx.fillText(ch, 0, 0)\n    ctx.restore()\n  } else {\n    ctx.fillText(ch, x, y)\n  }\n}\n",[916],{"type":24,"tag":59,"props":917,"children":918},{"__ignoreMap":7},[919,968,1013,1020,1091,1134,1143,1165,1195,1299,1377,1397,1447,1455,1474,1527,1575,1620,1640,1656,1700,1709],{"type":24,"tag":225,"props":920,"children":921},{"class":227,"line":18},[922,927,932,936,941,946,950,955,960,964],{"type":24,"tag":225,"props":923,"children":924},{"style":287},[925],{"type":30,"value":926},"const ",{"type":24,"tag":225,"props":928,"children":929},{"style":231},[930],{"type":30,"value":931},"ROTATE_CHARS",{"type":24,"tag":225,"props":933,"children":934},{"style":237},[935],{"type":30,"value":300},{"type":24,"tag":225,"props":937,"children":938},{"style":287},[939],{"type":30,"value":940}," new ",{"type":24,"tag":225,"props":942,"children":943},{"style":243},[944],{"type":30,"value":945},"Set",{"type":24,"tag":225,"props":947,"children":948},{"style":237},[949],{"type":30,"value":392},{"type":24,"tag":225,"props":951,"children":952},{"style":590},[953],{"type":30,"value":954},"'",{"type":24,"tag":225,"props":956,"children":957},{"style":596},[958],{"type":30,"value":959},"ー〜「」『』…・（）",{"type":24,"tag":225,"props":961,"children":962},{"style":590},[963],{"type":30,"value":954},{"type":24,"tag":225,"props":965,"children":966},{"style":237},[967],{"type":30,"value":814},{"type":24,"tag":225,"props":969,"children":970},{"class":227,"line":283},[971,975,980,984,988,992,996,1000,1005,1009],{"type":24,"tag":225,"props":972,"children":973},{"style":287},[974],{"type":30,"value":926},{"type":24,"tag":225,"props":976,"children":977},{"style":231},[978],{"type":30,"value":979},"TOP_RIGHT_CHARS",{"type":24,"tag":225,"props":981,"children":982},{"style":237},[983],{"type":30,"value":300},{"type":24,"tag":225,"props":985,"children":986},{"style":287},[987],{"type":30,"value":940},{"type":24,"tag":225,"props":989,"children":990},{"style":243},[991],{"type":30,"value":945},{"type":24,"tag":225,"props":993,"children":994},{"style":237},[995],{"type":30,"value":392},{"type":24,"tag":225,"props":997,"children":998},{"style":590},[999],{"type":30,"value":954},{"type":24,"tag":225,"props":1001,"children":1002},{"style":596},[1003],{"type":30,"value":1004},"、。",{"type":24,"tag":225,"props":1006,"children":1007},{"style":590},[1008],{"type":30,"value":954},{"type":24,"tag":225,"props":1010,"children":1011},{"style":237},[1012],{"type":30,"value":814},{"type":24,"tag":225,"props":1014,"children":1015},{"class":227,"line":328},[1016],{"type":24,"tag":225,"props":1017,"children":1018},{"emptyLinePlaceholder":527},[1019],{"type":30,"value":530},{"type":24,"tag":225,"props":1021,"children":1022},{"class":227,"line":359},[1023,1028,1033,1037,1041,1045,1049,1053,1057,1061,1066,1070,1075,1079,1083,1087],{"type":24,"tag":225,"props":1024,"children":1025},{"style":287},[1026],{"type":30,"value":1027},"function",{"type":24,"tag":225,"props":1029,"children":1030},{"style":243},[1031],{"type":30,"value":1032}," drawVerticalChar",{"type":24,"tag":225,"props":1034,"children":1035},{"style":237},[1036],{"type":30,"value":392},{"type":24,"tag":225,"props":1038,"children":1039},{"style":231},[1040],{"type":30,"value":746},{"type":24,"tag":225,"props":1042,"children":1043},{"style":237},[1044],{"type":30,"value":260},{"type":24,"tag":225,"props":1046,"children":1047},{"style":231},[1048],{"type":30,"value":755},{"type":24,"tag":225,"props":1050,"children":1051},{"style":237},[1052],{"type":30,"value":260},{"type":24,"tag":225,"props":1054,"children":1055},{"style":231},[1056],{"type":30,"value":764},{"type":24,"tag":225,"props":1058,"children":1059},{"style":237},[1060],{"type":30,"value":260},{"type":24,"tag":225,"props":1062,"children":1063},{"style":231},[1064],{"type":30,"value":1065}," y",{"type":24,"tag":225,"props":1067,"children":1068},{"style":237},[1069],{"type":30,"value":260},{"type":24,"tag":225,"props":1071,"children":1072},{"style":231},[1073],{"type":30,"value":1074}," fontSize",{"type":24,"tag":225,"props":1076,"children":1077},{"style":237},[1078],{"type":30,"value":260},{"type":24,"tag":225,"props":1080,"children":1081},{"style":231},[1082],{"type":30,"value":809},{"type":24,"tag":225,"props":1084,"children":1085},{"style":237},[1086],{"type":30,"value":270},{"type":24,"tag":225,"props":1088,"children":1089},{"style":237},[1090],{"type":30,"value":280},{"type":24,"tag":225,"props":1092,"children":1093},{"class":227,"line":491},[1094,1099,1104,1108,1112,1117,1121,1125,1130],{"type":24,"tag":225,"props":1095,"children":1096},{"style":602},[1097],{"type":30,"value":1098},"  if",{"type":24,"tag":225,"props":1100,"children":1101},{"style":237},[1102],{"type":30,"value":1103}," (",{"type":24,"tag":225,"props":1105,"children":1106},{"style":231},[1107],{"type":30,"value":979},{"type":24,"tag":225,"props":1109,"children":1110},{"style":237},[1111],{"type":30,"value":240},{"type":24,"tag":225,"props":1113,"children":1114},{"style":243},[1115],{"type":30,"value":1116},"has",{"type":24,"tag":225,"props":1118,"children":1119},{"style":237},[1120],{"type":30,"value":392},{"type":24,"tag":225,"props":1122,"children":1123},{"style":231},[1124],{"type":30,"value":707},{"type":24,"tag":225,"props":1126,"children":1127},{"style":237},[1128],{"type":30,"value":1129},"))",{"type":24,"tag":225,"props":1131,"children":1132},{"style":237},[1133],{"type":30,"value":280},{"type":24,"tag":225,"props":1135,"children":1136},{"class":227,"line":523},[1137],{"type":24,"tag":225,"props":1138,"children":1140},{"style":1139},"--shiki-default:#758575DD",[1141],{"type":30,"value":1142},"    // 句読点: 右上に小さめに配置\n",{"type":24,"tag":225,"props":1144,"children":1145},{"class":227,"line":533},[1146,1151,1155,1160],{"type":24,"tag":225,"props":1147,"children":1148},{"style":231},[1149],{"type":30,"value":1150},"    ctx",{"type":24,"tag":225,"props":1152,"children":1153},{"style":237},[1154],{"type":30,"value":240},{"type":24,"tag":225,"props":1156,"children":1157},{"style":243},[1158],{"type":30,"value":1159},"save",{"type":24,"tag":225,"props":1161,"children":1162},{"style":237},[1163],{"type":30,"value":1164},"()\n",{"type":24,"tag":225,"props":1166,"children":1167},{"class":227,"line":569},[1168,1173,1178,1182,1186,1190],{"type":24,"tag":225,"props":1169,"children":1170},{"style":287},[1171],{"type":30,"value":1172},"    const ",{"type":24,"tag":225,"props":1174,"children":1175},{"style":231},[1176],{"type":30,"value":1177},"size",{"type":24,"tag":225,"props":1179,"children":1180},{"style":237},[1181],{"type":30,"value":300},{"type":24,"tag":225,"props":1183,"children":1184},{"style":231},[1185],{"type":30,"value":1074},{"type":24,"tag":225,"props":1187,"children":1188},{"style":287},[1189],{"type":30,"value":320},{"type":24,"tag":225,"props":1191,"children":1192},{"style":395},[1193],{"type":30,"value":1194},"0.5\n",{"type":24,"tag":225,"props":1196,"children":1197},{"class":227,"line":640},[1198,1202,1206,1210,1214,1219,1223,1227,1231,1236,1240,1245,1251,1256,1262,1266,1270,1274,1278,1282,1286,1290,1295],{"type":24,"tag":225,"props":1199,"children":1200},{"style":231},[1201],{"type":30,"value":1150},{"type":24,"tag":225,"props":1203,"children":1204},{"style":237},[1205],{"type":30,"value":240},{"type":24,"tag":225,"props":1207,"children":1208},{"style":231},[1209],{"type":30,"value":583},{"type":24,"tag":225,"props":1211,"children":1212},{"style":237},[1213],{"type":30,"value":300},{"type":24,"tag":225,"props":1215,"children":1216},{"style":231},[1217],{"type":30,"value":1218}," ctx",{"type":24,"tag":225,"props":1220,"children":1221},{"style":237},[1222],{"type":30,"value":240},{"type":24,"tag":225,"props":1224,"children":1225},{"style":231},[1226],{"type":30,"value":583},{"type":24,"tag":225,"props":1228,"children":1229},{"style":237},[1230],{"type":30,"value":240},{"type":24,"tag":225,"props":1232,"children":1233},{"style":243},[1234],{"type":30,"value":1235},"replace",{"type":24,"tag":225,"props":1237,"children":1238},{"style":237},[1239],{"type":30,"value":392},{"type":24,"tag":225,"props":1241,"children":1242},{"style":590},[1243],{"type":30,"value":1244},"/",{"type":24,"tag":225,"props":1246,"children":1248},{"style":1247},"--shiki-default:#6872AB",[1249],{"type":30,"value":1250},"\\d",{"type":24,"tag":225,"props":1252,"children":1253},{"style":395},[1254],{"type":30,"value":1255},"+",{"type":24,"tag":225,"props":1257,"children":1259},{"style":1258},"--shiki-default:#C4704F",[1260],{"type":30,"value":1261},"px",{"type":24,"tag":225,"props":1263,"children":1264},{"style":590},[1265],{"type":30,"value":1244},{"type":24,"tag":225,"props":1267,"children":1268},{"style":237},[1269],{"type":30,"value":260},{"type":24,"tag":225,"props":1271,"children":1272},{"style":590},[1273],{"type":30,"value":593},{"type":24,"tag":225,"props":1275,"children":1276},{"style":602},[1277],{"type":30,"value":605},{"type":24,"tag":225,"props":1279,"children":1280},{"style":596},[1281],{"type":30,"value":1177},{"type":24,"tag":225,"props":1283,"children":1284},{"style":602},[1285],{"type":30,"value":614},{"type":24,"tag":225,"props":1287,"children":1288},{"style":596},[1289],{"type":30,"value":1261},{"type":24,"tag":225,"props":1291,"children":1292},{"style":590},[1293],{"type":30,"value":1294},"`",{"type":24,"tag":225,"props":1296,"children":1297},{"style":237},[1298],{"type":30,"value":814},{"type":24,"tag":225,"props":1300,"children":1301},{"class":227,"line":676},[1302,1306,1310,1314,1318,1322,1326,1330,1334,1338,1342,1347,1351,1355,1360,1364,1368,1373],{"type":24,"tag":225,"props":1303,"children":1304},{"style":231},[1305],{"type":30,"value":1150},{"type":24,"tag":225,"props":1307,"children":1308},{"style":237},[1309],{"type":30,"value":240},{"type":24,"tag":225,"props":1311,"children":1312},{"style":243},[1313],{"type":30,"value":896},{"type":24,"tag":225,"props":1315,"children":1316},{"style":237},[1317],{"type":30,"value":392},{"type":24,"tag":225,"props":1319,"children":1320},{"style":231},[1321],{"type":30,"value":707},{"type":24,"tag":225,"props":1323,"children":1324},{"style":237},[1325],{"type":30,"value":260},{"type":24,"tag":225,"props":1327,"children":1328},{"style":231},[1329],{"type":30,"value":764},{"type":24,"tag":225,"props":1331,"children":1332},{"style":287},[1333],{"type":30,"value":778},{"type":24,"tag":225,"props":1335,"children":1336},{"style":231},[1337],{"type":30,"value":1074},{"type":24,"tag":225,"props":1339,"children":1340},{"style":287},[1341],{"type":30,"value":787},{"type":24,"tag":225,"props":1343,"children":1344},{"style":395},[1345],{"type":30,"value":1346}," 0.35",{"type":24,"tag":225,"props":1348,"children":1349},{"style":237},[1350],{"type":30,"value":260},{"type":24,"tag":225,"props":1352,"children":1353},{"style":231},[1354],{"type":30,"value":1065},{"type":24,"tag":225,"props":1356,"children":1357},{"style":287},[1358],{"type":30,"value":1359}," -",{"type":24,"tag":225,"props":1361,"children":1362},{"style":231},[1363],{"type":30,"value":1074},{"type":24,"tag":225,"props":1365,"children":1366},{"style":287},[1367],{"type":30,"value":787},{"type":24,"tag":225,"props":1369,"children":1370},{"style":395},[1371],{"type":30,"value":1372}," 0.3",{"type":24,"tag":225,"props":1374,"children":1375},{"style":237},[1376],{"type":30,"value":814},{"type":24,"tag":225,"props":1378,"children":1379},{"class":227,"line":684},[1380,1384,1388,1393],{"type":24,"tag":225,"props":1381,"children":1382},{"style":231},[1383],{"type":30,"value":1150},{"type":24,"tag":225,"props":1385,"children":1386},{"style":237},[1387],{"type":30,"value":240},{"type":24,"tag":225,"props":1389,"children":1390},{"style":243},[1391],{"type":30,"value":1392},"restore",{"type":24,"tag":225,"props":1394,"children":1395},{"style":237},[1396],{"type":30,"value":1164},{"type":24,"tag":225,"props":1398,"children":1399},{"class":227,"line":731},[1400,1405,1410,1415,1419,1423,1427,1431,1435,1439,1443],{"type":24,"tag":225,"props":1401,"children":1402},{"style":237},[1403],{"type":30,"value":1404},"  }",{"type":24,"tag":225,"props":1406,"children":1407},{"style":602},[1408],{"type":30,"value":1409}," else",{"type":24,"tag":225,"props":1411,"children":1412},{"style":602},[1413],{"type":30,"value":1414}," if",{"type":24,"tag":225,"props":1416,"children":1417},{"style":237},[1418],{"type":30,"value":1103},{"type":24,"tag":225,"props":1420,"children":1421},{"style":231},[1422],{"type":30,"value":931},{"type":24,"tag":225,"props":1424,"children":1425},{"style":237},[1426],{"type":30,"value":240},{"type":24,"tag":225,"props":1428,"children":1429},{"style":243},[1430],{"type":30,"value":1116},{"type":24,"tag":225,"props":1432,"children":1433},{"style":237},[1434],{"type":30,"value":392},{"type":24,"tag":225,"props":1436,"children":1437},{"style":231},[1438],{"type":30,"value":707},{"type":24,"tag":225,"props":1440,"children":1441},{"style":237},[1442],{"type":30,"value":1129},{"type":24,"tag":225,"props":1444,"children":1445},{"style":237},[1446],{"type":30,"value":280},{"type":24,"tag":225,"props":1448,"children":1449},{"class":227,"line":817},[1450],{"type":24,"tag":225,"props":1451,"children":1452},{"style":1139},[1453],{"type":30,"value":1454},"    // 長音・括弧等: 90度回転\n",{"type":24,"tag":225,"props":1456,"children":1457},{"class":227,"line":826},[1458,1462,1466,1470],{"type":24,"tag":225,"props":1459,"children":1460},{"style":231},[1461],{"type":30,"value":1150},{"type":24,"tag":225,"props":1463,"children":1464},{"style":237},[1465],{"type":30,"value":240},{"type":24,"tag":225,"props":1467,"children":1468},{"style":243},[1469],{"type":30,"value":1159},{"type":24,"tag":225,"props":1471,"children":1472},{"style":237},[1473],{"type":30,"value":1164},{"type":24,"tag":225,"props":1475,"children":1477},{"class":227,"line":1476},15,[1478,1482,1486,1491,1495,1499,1503,1507,1511,1515,1519,1523],{"type":24,"tag":225,"props":1479,"children":1480},{"style":231},[1481],{"type":30,"value":1150},{"type":24,"tag":225,"props":1483,"children":1484},{"style":237},[1485],{"type":30,"value":240},{"type":24,"tag":225,"props":1487,"children":1488},{"style":243},[1489],{"type":30,"value":1490},"translate",{"type":24,"tag":225,"props":1492,"children":1493},{"style":237},[1494],{"type":30,"value":392},{"type":24,"tag":225,"props":1496,"children":1497},{"style":231},[1498],{"type":30,"value":295},{"type":24,"tag":225,"props":1500,"children":1501},{"style":237},[1502],{"type":30,"value":260},{"type":24,"tag":225,"props":1504,"children":1505},{"style":231},[1506],{"type":30,"value":1065},{"type":24,"tag":225,"props":1508,"children":1509},{"style":287},[1510],{"type":30,"value":1359},{"type":24,"tag":225,"props":1512,"children":1513},{"style":231},[1514],{"type":30,"value":1074},{"type":24,"tag":225,"props":1516,"children":1517},{"style":287},[1518],{"type":30,"value":787},{"type":24,"tag":225,"props":1520,"children":1521},{"style":395},[1522],{"type":30,"value":1346},{"type":24,"tag":225,"props":1524,"children":1525},{"style":237},[1526],{"type":30,"value":814},{"type":24,"tag":225,"props":1528,"children":1530},{"class":227,"line":1529},16,[1531,1535,1539,1544,1548,1552,1556,1561,1566,1571],{"type":24,"tag":225,"props":1532,"children":1533},{"style":231},[1534],{"type":30,"value":1150},{"type":24,"tag":225,"props":1536,"children":1537},{"style":237},[1538],{"type":30,"value":240},{"type":24,"tag":225,"props":1540,"children":1541},{"style":243},[1542],{"type":30,"value":1543},"rotate",{"type":24,"tag":225,"props":1545,"children":1546},{"style":237},[1547],{"type":30,"value":392},{"type":24,"tag":225,"props":1549,"children":1550},{"style":231},[1551],{"type":30,"value":447},{"type":24,"tag":225,"props":1553,"children":1554},{"style":237},[1555],{"type":30,"value":240},{"type":24,"tag":225,"props":1557,"children":1558},{"style":231},[1559],{"type":30,"value":1560},"PI",{"type":24,"tag":225,"props":1562,"children":1563},{"style":287},[1564],{"type":30,"value":1565}," /",{"type":24,"tag":225,"props":1567,"children":1568},{"style":395},[1569],{"type":30,"value":1570}," 2",{"type":24,"tag":225,"props":1572,"children":1573},{"style":237},[1574],{"type":30,"value":814},{"type":24,"tag":225,"props":1576,"children":1578},{"class":227,"line":1577},17,[1579,1583,1587,1591,1595,1599,1603,1608,1612,1616],{"type":24,"tag":225,"props":1580,"children":1581},{"style":231},[1582],{"type":30,"value":1150},{"type":24,"tag":225,"props":1584,"children":1585},{"style":237},[1586],{"type":30,"value":240},{"type":24,"tag":225,"props":1588,"children":1589},{"style":243},[1590],{"type":30,"value":896},{"type":24,"tag":225,"props":1592,"children":1593},{"style":237},[1594],{"type":30,"value":392},{"type":24,"tag":225,"props":1596,"children":1597},{"style":231},[1598],{"type":30,"value":707},{"type":24,"tag":225,"props":1600,"children":1601},{"style":237},[1602],{"type":30,"value":260},{"type":24,"tag":225,"props":1604,"children":1605},{"style":395},[1606],{"type":30,"value":1607}," 0",{"type":24,"tag":225,"props":1609,"children":1610},{"style":237},[1611],{"type":30,"value":260},{"type":24,"tag":225,"props":1613,"children":1614},{"style":395},[1615],{"type":30,"value":1607},{"type":24,"tag":225,"props":1617,"children":1618},{"style":237},[1619],{"type":30,"value":814},{"type":24,"tag":225,"props":1621,"children":1623},{"class":227,"line":1622},18,[1624,1628,1632,1636],{"type":24,"tag":225,"props":1625,"children":1626},{"style":231},[1627],{"type":30,"value":1150},{"type":24,"tag":225,"props":1629,"children":1630},{"style":237},[1631],{"type":30,"value":240},{"type":24,"tag":225,"props":1633,"children":1634},{"style":243},[1635],{"type":30,"value":1392},{"type":24,"tag":225,"props":1637,"children":1638},{"style":237},[1639],{"type":30,"value":1164},{"type":24,"tag":225,"props":1641,"children":1643},{"class":227,"line":1642},19,[1644,1648,1652],{"type":24,"tag":225,"props":1645,"children":1646},{"style":237},[1647],{"type":30,"value":1404},{"type":24,"tag":225,"props":1649,"children":1650},{"style":602},[1651],{"type":30,"value":1409},{"type":24,"tag":225,"props":1653,"children":1654},{"style":237},[1655],{"type":30,"value":280},{"type":24,"tag":225,"props":1657,"children":1659},{"class":227,"line":1658},20,[1660,1664,1668,1672,1676,1680,1684,1688,1692,1696],{"type":24,"tag":225,"props":1661,"children":1662},{"style":231},[1663],{"type":30,"value":1150},{"type":24,"tag":225,"props":1665,"children":1666},{"style":237},[1667],{"type":30,"value":240},{"type":24,"tag":225,"props":1669,"children":1670},{"style":243},[1671],{"type":30,"value":896},{"type":24,"tag":225,"props":1673,"children":1674},{"style":237},[1675],{"type":30,"value":392},{"type":24,"tag":225,"props":1677,"children":1678},{"style":231},[1679],{"type":30,"value":707},{"type":24,"tag":225,"props":1681,"children":1682},{"style":237},[1683],{"type":30,"value":260},{"type":24,"tag":225,"props":1685,"children":1686},{"style":231},[1687],{"type":30,"value":764},{"type":24,"tag":225,"props":1689,"children":1690},{"style":237},[1691],{"type":30,"value":260},{"type":24,"tag":225,"props":1693,"children":1694},{"style":231},[1695],{"type":30,"value":1065},{"type":24,"tag":225,"props":1697,"children":1698},{"style":237},[1699],{"type":30,"value":814},{"type":24,"tag":225,"props":1701,"children":1703},{"class":227,"line":1702},21,[1704],{"type":24,"tag":225,"props":1705,"children":1706},{"style":237},[1707],{"type":30,"value":1708},"  }\n",{"type":24,"tag":225,"props":1710,"children":1712},{"class":227,"line":1711},22,[1713],{"type":24,"tag":225,"props":1714,"children":1715},{"style":237},[1716],{"type":30,"value":1717},"}\n",{"type":24,"tag":1719,"props":1720,"children":1722},"h3",{"id":1721},"句読点は右上に半分サイズで置く",[1723],{"type":30,"value":1721},{"type":24,"tag":32,"props":1725,"children":1726},{},[1727,1729,1734,1736,1742],{"type":30,"value":1728},"日本の伝統的な縦書き組版では、「、」と「。」は ",{"type":24,"tag":46,"props":1730,"children":1731},{},[1732],{"type":30,"value":1733},"マス目の右上に小さく",{"type":30,"value":1735}," 配置します。詠み人ではサイズを 50%、座標を ",{"type":24,"tag":59,"props":1737,"children":1739},{"className":1738},[],[1740],{"type":30,"value":1741},"(x + fontSize * 0.35, y - fontSize * 0.3)",{"type":30,"value":1743}," に補正することで、この配置を再現しました。",{"type":24,"tag":1719,"props":1745,"children":1747},{"id":1746},"長音括弧は90度回転",[1748],{"type":30,"value":1749},"長音・括弧は90度回転",{"type":24,"tag":32,"props":1751,"children":1752},{},[1753,1755,1761,1763,1768,1770,1775],{"type":30,"value":1754},"「ー」「〜」「「」「（）」などは、横書きでそのまま描くと縦書きの文脈で読みにくくなります。詠み人では ",{"type":24,"tag":59,"props":1756,"children":1758},{"className":1757},[],[1759],{"type":30,"value":1760},"ctx.rotate(Math.PI / 2)",{"type":30,"value":1762}," で ",{"type":24,"tag":46,"props":1764,"children":1765},{},[1766],{"type":30,"value":1767},"90度時計回りに回転",{"type":30,"value":1769}," して描画しています。",{"type":24,"tag":59,"props":1771,"children":1773},{"className":1772},[],[1774],{"type":30,"value":1490},{"type":30,"value":1776}," で座標を中心にずらしてから回転させるのがコツです。",{"type":24,"tag":32,"props":1778,"children":1779},{},[1780,1782,1787],{"type":30,"value":1781},"このあたりは日本語組版の JIS X 4051 や W3C の日本語組版要件を全部実装しようとすると際限がないので、",{"type":24,"tag":46,"props":1783,"children":1784},{},[1785],{"type":30,"value":1786},"詠み人に実際に入力される文字種に限って割り切る",{"type":30,"value":1788}," 方針を取りました。",{"type":24,"tag":1719,"props":1790,"children":1792},{"id":1791},"フォントによって位置が異なる",[1793],{"type":30,"value":1791},{"type":24,"tag":32,"props":1795,"children":1796},{},[1797,1799,1804],{"type":30,"value":1798},"回転処理をさらに厄介にするのが、",{"type":24,"tag":46,"props":1800,"children":1801},{},[1802],{"type":30,"value":1803},"フォントごとにグリフの中心位置が微妙にずれる",{"type":30,"value":1805}," という問題です。詠み人では明朝（Noto Serif JP）・力弱（851CHIKARA-YOWAKU）・行書（AoyagiKouzanT）・隷書（AoyagiReisyosimo）の4フォントを選択できますが、このうち隷書だけは回転文字の描画位置がずれます。",{"type":24,"tag":32,"props":1807,"children":1808},{},[1809],{"type":30,"value":1810},"実装ではフォント ID を見て隷書のときだけ X 軸方向にオフセットを入れています。",{"type":24,"tag":215,"props":1812,"children":1814},{"className":217,"code":1813,"language":219,"meta":7,"style":7},"const offsetX = fontId === 'reisyo' ? -fontSize * 0.15 : 0\nctx.translate(x + offsetX, y - fontSize * 0.35)\nctx.rotate(Math.PI / 2)\n",[1815],{"type":24,"tag":59,"props":1816,"children":1817},{"__ignoreMap":7},[1818,1885,1945],{"type":24,"tag":225,"props":1819,"children":1820},{"class":227,"line":18},[1821,1825,1830,1834,1838,1843,1847,1852,1856,1861,1866,1870,1875,1880],{"type":24,"tag":225,"props":1822,"children":1823},{"style":287},[1824],{"type":30,"value":926},{"type":24,"tag":225,"props":1826,"children":1827},{"style":231},[1828],{"type":30,"value":1829},"offsetX",{"type":24,"tag":225,"props":1831,"children":1832},{"style":237},[1833],{"type":30,"value":300},{"type":24,"tag":225,"props":1835,"children":1836},{"style":231},[1837],{"type":30,"value":809},{"type":24,"tag":225,"props":1839,"children":1840},{"style":287},[1841],{"type":30,"value":1842}," === ",{"type":24,"tag":225,"props":1844,"children":1845},{"style":590},[1846],{"type":30,"value":954},{"type":24,"tag":225,"props":1848,"children":1849},{"style":596},[1850],{"type":30,"value":1851},"reisyo",{"type":24,"tag":225,"props":1853,"children":1854},{"style":590},[1855],{"type":30,"value":954},{"type":24,"tag":225,"props":1857,"children":1858},{"style":287},[1859],{"type":30,"value":1860}," ? -",{"type":24,"tag":225,"props":1862,"children":1863},{"style":231},[1864],{"type":30,"value":1865},"fontSize",{"type":24,"tag":225,"props":1867,"children":1868},{"style":287},[1869],{"type":30,"value":320},{"type":24,"tag":225,"props":1871,"children":1872},{"style":395},[1873],{"type":30,"value":1874},"0.15",{"type":24,"tag":225,"props":1876,"children":1877},{"style":287},[1878],{"type":30,"value":1879}," : ",{"type":24,"tag":225,"props":1881,"children":1882},{"style":395},[1883],{"type":30,"value":1884},"0\n",{"type":24,"tag":225,"props":1886,"children":1887},{"class":227,"line":283},[1888,1892,1896,1900,1904,1908,1912,1917,1921,1925,1929,1933,1937,1941],{"type":24,"tag":225,"props":1889,"children":1890},{"style":231},[1891],{"type":30,"value":746},{"type":24,"tag":225,"props":1893,"children":1894},{"style":237},[1895],{"type":30,"value":240},{"type":24,"tag":225,"props":1897,"children":1898},{"style":243},[1899],{"type":30,"value":1490},{"type":24,"tag":225,"props":1901,"children":1902},{"style":237},[1903],{"type":30,"value":392},{"type":24,"tag":225,"props":1905,"children":1906},{"style":231},[1907],{"type":30,"value":295},{"type":24,"tag":225,"props":1909,"children":1910},{"style":287},[1911],{"type":30,"value":778},{"type":24,"tag":225,"props":1913,"children":1914},{"style":231},[1915],{"type":30,"value":1916}," offsetX",{"type":24,"tag":225,"props":1918,"children":1919},{"style":237},[1920],{"type":30,"value":260},{"type":24,"tag":225,"props":1922,"children":1923},{"style":231},[1924],{"type":30,"value":1065},{"type":24,"tag":225,"props":1926,"children":1927},{"style":287},[1928],{"type":30,"value":1359},{"type":24,"tag":225,"props":1930,"children":1931},{"style":231},[1932],{"type":30,"value":1074},{"type":24,"tag":225,"props":1934,"children":1935},{"style":287},[1936],{"type":30,"value":787},{"type":24,"tag":225,"props":1938,"children":1939},{"style":395},[1940],{"type":30,"value":1346},{"type":24,"tag":225,"props":1942,"children":1943},{"style":237},[1944],{"type":30,"value":814},{"type":24,"tag":225,"props":1946,"children":1947},{"class":227,"line":328},[1948,1952,1956,1960,1964,1968,1972,1976,1980,1984],{"type":24,"tag":225,"props":1949,"children":1950},{"style":231},[1951],{"type":30,"value":746},{"type":24,"tag":225,"props":1953,"children":1954},{"style":237},[1955],{"type":30,"value":240},{"type":24,"tag":225,"props":1957,"children":1958},{"style":243},[1959],{"type":30,"value":1543},{"type":24,"tag":225,"props":1961,"children":1962},{"style":237},[1963],{"type":30,"value":392},{"type":24,"tag":225,"props":1965,"children":1966},{"style":231},[1967],{"type":30,"value":447},{"type":24,"tag":225,"props":1969,"children":1970},{"style":237},[1971],{"type":30,"value":240},{"type":24,"tag":225,"props":1973,"children":1974},{"style":231},[1975],{"type":30,"value":1560},{"type":24,"tag":225,"props":1977,"children":1978},{"style":287},[1979],{"type":30,"value":1565},{"type":24,"tag":225,"props":1981,"children":1982},{"style":395},[1983],{"type":30,"value":1570},{"type":24,"tag":225,"props":1985,"children":1986},{"style":237},[1987],{"type":30,"value":814},{"type":24,"tag":32,"props":1989,"children":1990},{},[1991,1997,1999,2005,2007,2012],{"type":24,"tag":59,"props":1992,"children":1994},{"className":1993},[],[1995],{"type":30,"value":1996},"-fontSize * 0.15",{"type":30,"value":1998}," という補正値は、隷書フォントの「ー」や「〜」を実際に描画しながら目視で合わせた数値です。フォント間でグリフメトリクスが統一されていないため、汎用的な計算式を導出するのは難しく、フォントを追加するたびにこの分岐が増える可能性があります。現状は4フォント固定なので ",{"type":24,"tag":59,"props":2000,"children":2002},{"className":2001},[],[2003],{"type":30,"value":2004},"fontId === 'reisyo'",{"type":30,"value":2006}," の1分岐で収まっていますが、フォント選択肢を増やす場合はオフセットのテーブル化が必要になるでしょう。私は ",{"type":24,"tag":46,"props":2008,"children":2009},{},[2010],{"type":30,"value":2011},"必要最低限で実装する",{"type":30,"value":2013}," という原則で実装を進めています。現時点で複雑にならないことが最優先です。",{"type":24,"tag":25,"props":2015,"children":2017},{"id":2016},"フォントの読み込み待機を忘れずに",[2018],{"type":30,"value":2016},{"type":24,"tag":32,"props":2020,"children":2021},{},[2022,2024,2029,2031,2036],{"type":30,"value":2023},"Canvas 描画で地味にハマるのが「",{"type":24,"tag":46,"props":2025,"children":2026},{},[2027],{"type":30,"value":2028},"指定したフォントがまだ読み込まれていないとデフォルトフォントで描画される",{"type":30,"value":2030},"」問題です。特に Google Fonts のような外部フォントを使う場合、初回描画時に ",{"type":24,"tag":59,"props":2032,"children":2034},{"className":2033},[],[2035],{"type":30,"value":896},{"type":30,"value":2037}," が意図しないフォントで動き、2回目以降は正常、という不安定な挙動を起こします。",{"type":24,"tag":32,"props":2039,"children":2040},{},[2041,2043,2049],{"type":30,"value":2042},"詠み人では描画開始前に ",{"type":24,"tag":59,"props":2044,"children":2046},{"className":2045},[],[2047],{"type":30,"value":2048},"document.fonts.load()",{"type":30,"value":2050}," を明示的に待機しています。",{"type":24,"tag":215,"props":2052,"children":2054},{"className":217,"code":2053,"language":219,"meta":7,"style":7},"await document.fonts.load(`400 46px ${fontFamily}`)\nawait document.fonts.load(`300 13px ${fontFamily}`)\n",[2055],{"type":24,"tag":59,"props":2056,"children":2057},{"__ignoreMap":7},[2058,2122],{"type":24,"tag":225,"props":2059,"children":2060},{"class":227,"line":18},[2061,2066,2071,2075,2080,2084,2089,2093,2097,2102,2106,2110,2114,2118],{"type":24,"tag":225,"props":2062,"children":2063},{"style":602},[2064],{"type":30,"value":2065},"await",{"type":24,"tag":225,"props":2067,"children":2068},{"style":231},[2069],{"type":30,"value":2070}," document",{"type":24,"tag":225,"props":2072,"children":2073},{"style":237},[2074],{"type":30,"value":240},{"type":24,"tag":225,"props":2076,"children":2077},{"style":231},[2078],{"type":30,"value":2079},"fonts",{"type":24,"tag":225,"props":2081,"children":2082},{"style":237},[2083],{"type":30,"value":240},{"type":24,"tag":225,"props":2085,"children":2086},{"style":243},[2087],{"type":30,"value":2088},"load",{"type":24,"tag":225,"props":2090,"children":2091},{"style":237},[2092],{"type":30,"value":392},{"type":24,"tag":225,"props":2094,"children":2095},{"style":590},[2096],{"type":30,"value":1294},{"type":24,"tag":225,"props":2098,"children":2099},{"style":596},[2100],{"type":30,"value":2101},"400 46px ",{"type":24,"tag":225,"props":2103,"children":2104},{"style":602},[2105],{"type":30,"value":605},{"type":24,"tag":225,"props":2107,"children":2108},{"style":596},[2109],{"type":30,"value":628},{"type":24,"tag":225,"props":2111,"children":2112},{"style":602},[2113],{"type":30,"value":614},{"type":24,"tag":225,"props":2115,"children":2116},{"style":590},[2117],{"type":30,"value":1294},{"type":24,"tag":225,"props":2119,"children":2120},{"style":237},[2121],{"type":30,"value":814},{"type":24,"tag":225,"props":2123,"children":2124},{"class":227,"line":283},[2125,2129,2133,2137,2141,2145,2149,2153,2157,2162,2166,2170,2174,2178],{"type":24,"tag":225,"props":2126,"children":2127},{"style":602},[2128],{"type":30,"value":2065},{"type":24,"tag":225,"props":2130,"children":2131},{"style":231},[2132],{"type":30,"value":2070},{"type":24,"tag":225,"props":2134,"children":2135},{"style":237},[2136],{"type":30,"value":240},{"type":24,"tag":225,"props":2138,"children":2139},{"style":231},[2140],{"type":30,"value":2079},{"type":24,"tag":225,"props":2142,"children":2143},{"style":237},[2144],{"type":30,"value":240},{"type":24,"tag":225,"props":2146,"children":2147},{"style":243},[2148],{"type":30,"value":2088},{"type":24,"tag":225,"props":2150,"children":2151},{"style":237},[2152],{"type":30,"value":392},{"type":24,"tag":225,"props":2154,"children":2155},{"style":590},[2156],{"type":30,"value":1294},{"type":24,"tag":225,"props":2158,"children":2159},{"style":596},[2160],{"type":30,"value":2161},"300 13px ",{"type":24,"tag":225,"props":2163,"children":2164},{"style":602},[2165],{"type":30,"value":605},{"type":24,"tag":225,"props":2167,"children":2168},{"style":596},[2169],{"type":30,"value":628},{"type":24,"tag":225,"props":2171,"children":2172},{"style":602},[2173],{"type":30,"value":614},{"type":24,"tag":225,"props":2175,"children":2176},{"style":590},[2177],{"type":30,"value":1294},{"type":24,"tag":225,"props":2179,"children":2180},{"style":237},[2181],{"type":30,"value":814},{"type":24,"tag":32,"props":2183,"children":2184},{},[2185,2187,2193,2195,2201],{"type":30,"value":2186},"本文用（46px）と作者名用（13px）の両方をロードしないと、作者名だけがデフォルトフォントになる、という微妙なバグが出ます。フォントウェイトが異なる（本文は ",{"type":24,"tag":59,"props":2188,"children":2190},{"className":2189},[],[2191],{"type":30,"value":2192},"400",{"type":30,"value":2194},"、作者名は ",{"type":24,"tag":59,"props":2196,"children":2198},{"className":2197},[],[2199],{"type":30,"value":2200},"300",{"type":30,"value":2202},"）ため、それぞれ個別に await しています。",{"type":24,"tag":25,"props":2204,"children":2206},{"id":2205},"まとめ",[2207],{"type":30,"value":2205},{"type":24,"tag":2209,"props":2210,"children":2211},"ul",{},[2212,2225,2230,2242],{"type":24,"tag":2213,"props":2214,"children":2215},"li",{},[2216,2218,2223],{"type":30,"value":2217},"Canvas での縦書き実装は、",{"type":24,"tag":46,"props":2219,"children":2220},{},[2221],{"type":30,"value":2222},"画像生成ツール",{"type":30,"value":2224}," という用途では CSS より素直",{"type":24,"tag":2213,"props":2226,"children":2227},{},[2228],{"type":30,"value":2229},"句読点は 50% サイズで右上配置、長音・括弧は 90° 回転、という特殊文字処理が必須",{"type":24,"tag":2213,"props":2231,"children":2232},{},[2233,2235,2240],{"type":30,"value":2234},"日本語組版の全仕様を追うのではなく、",{"type":24,"tag":46,"props":2236,"children":2237},{},[2238],{"type":30,"value":2239},"自分のツールに実際に入力される文字",{"type":30,"value":2241}," に絞って割り切る",{"type":24,"tag":2213,"props":2243,"children":2244},{},[2245,2250],{"type":24,"tag":59,"props":2246,"children":2248},{"className":2247},[],[2249],{"type":30,"value":2048},{"type":30,"value":2251}," を複数サイズで await することで、フォント未ロード起因のバグを回避",{"type":24,"tag":32,"props":2253,"children":2254},{},[2255],{"type":30,"value":2256},"縦書きは奥が深い世界ですが、画像生成用途に限れば Canvas で十分戦えます。",{"type":24,"tag":2258,"props":2259,"children":2260},"style",{},[2261],{"type":30,"value":2262},"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);}",{"title":7,"searchDepth":283,"depth":283,"links":2264},[2265,2266,2267,2269,2270,2275,2276],{"id":27,"depth":283,"text":27},{"id":93,"depth":283,"text":93},{"id":176,"depth":283,"text":2268},"なぜ CSS writing-mode を選ばなかったのか",{"id":206,"depth":283,"text":206},{"id":875,"depth":283,"text":878,"children":2271},[2272,2273,2274],{"id":1721,"depth":328,"text":1721},{"id":1746,"depth":328,"text":1749},{"id":1791,"depth":328,"text":1791},{"id":2016,"depth":283,"text":2016},{"id":2205,"depth":283,"text":2205},"markdown","content:articles:tech:development:yominchu-vertical-canvas.md","content","articles/tech/development/yominchu-vertical-canvas.md","articles/tech/development/yominchu-vertical-canvas","md",1776356306737]