概要
ルビのある文言をcanvas要素に表示したかった。
KonvaではTextを使うと、widthをみて自動的に改行をしてくれるなどの機能はあるが、ルビをふる機能はない。
ライブラリを探したところ、Canvas Text Template v1.1が出てきたが、これはやりたいことに対してリッチすぎた。
konvajs.org - Rich_Text.htmlを参考に、html2canvasを使用することで、ちょっと強引だが解決できた。
実装
const Preview: React.FC<{
form: UseFormReturn<ActionCardEditFormData>
}> = ({ form }) => {
const card = form.getValues()
const ref = useRef<HTMLDivElement>(null)
const [url, setUrl] = useState('')
const [image] = useImage(url)
const watchFlavor = form.watch('flavor')
useEffect(() => {
;(async () => {
if (ref.current == null) return
// そのままだとぼやけるので、2倍に出力して0.5に縮小する
const canvas = await html2canvas(ref.current, { width: 210, scale: 2 })
setUrl(canvas.toDataURL('img/png'))
})()
}, [watchFlavor])
return (
<div>
<div style={{ width: '1px', height: '1px', overflow: 'hidden' }}>
<div
style={{
whiteSpace: 'pre-line',
width: '210px',
backgroundColor: 'white',
color: 'black',
fontSize: '14px',
fontFamily: family.serif,
}}
ref={ref}
dangerouslySetInnerHTML={{
__html: textToIncludeRubyTagsTextSnitized(watchFlavor),
}}
></div>
</div>
<BaseCard>
<CardType text="アクション" />
<CardName name={card.name} ruby={card.nameRuby} />
<Image x={30} y={50} image={image} scaleX={0.5} scaleY={0.5} />
<RightBottom value="icon: Material Design icons" />
</BaseCard>
</div>
)
}
useImage
import React from 'react'
type Status = 'loading' | 'loaded' | 'failed'
export const useImage = (
url: string,
crossOrigin?: 'anonymous' | 'use-credentials',
) => {
const statusRef = React.useRef<Status>('loading')
const imageRef = React.useRef<HTMLImageElement>()
const [_, setStateToken] = React.useState(0)
const oldUrl = React.useRef<string>()
const oldCrossOrigin = React.useRef<string>()
if (oldUrl.current !== url || oldCrossOrigin.current !== crossOrigin) {
statusRef.current = 'loading'
imageRef.current = undefined
oldUrl.current = url
oldCrossOrigin.current = crossOrigin
}
React.useLayoutEffect(
function () {
if (!url) return
const img = document.createElement('img')
function onload() {
statusRef.current = 'loaded'
imageRef.current = img
setStateToken(Math.random())
}
function onerror() {
statusRef.current = 'failed'
imageRef.current = undefined
setStateToken(Math.random())
}
img.addEventListener('load', onload)
img.addEventListener('error', onerror)
crossOrigin && (img.crossOrigin = crossOrigin)
img.src = url
return function cleanup() {
img.removeEventListener('load', onload)
img.removeEventListener('error', onerror)
}
},
[url, crossOrigin],
)
return [imageRef.current, statusRef.current] as const
}
textToRubyTag
import DOMPurify from 'dompurify'
const textToRubyTag = (text: string) =>
text
/* 半角または全角の縦棒以降の文字をベーステキスト、括弧内の文字をルビテキスト。 */
.replace(/[||](.+?)((.+?))/g, '<ruby>$1<rt>$2</rt></ruby>')
.replace(/[||](.+?)\((.+?)\)/g, '<ruby>$1<rt>$2</rt></ruby>')
.replace(/[||]((.+?))/g, '($1)')
export const textToIncludeRubyTagsTextSnitized = (text = '') =>
DOMPurify.sanitize(textToRubyTag(text))