【fabric.js】『化物語』の閑話休題演出をサクッと作れる「化物語「風」扉絵メーカー」を1.5時間で作ってみた

## はじめに
最近、Youtube動画の演出で「化物語でよく見る閑話休題や黒駒演出取りいれるのはどうだろう」と思い、動画編集ソフト上で実現してみた。
![スクリーンショット 2022-06-07 201114.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/b3bdb848-bdbb-544d-35f5-c6b7c0d24030.png)
場面転換としてなかなか優秀だなと思い、これからこの演出を使っていこうかなと思ったが...

「都度色を考えたり、カラーコードコピーしたりが面倒だ...!!」

というわけで、自分が楽するためにサクッと閑話休題演出を作ってくれる簡単なWebアプリを作ってみた。

なお、1時間ちょっとで作ったver0.5版なので、簡単な操作しかできないが、気が向いたらもう少しブラッシュアップする予定。

## 作った(作っている)モノ

(PC画面しか対応してなくてスマン)
[化物語「風」扉絵メーカー](http://tasokori.net/webapps/monogatari-tobirae-maker/)

こんな感じのモノが出来る。下部のボタンでダウンロードできる。(ブラウザはChromeでしか動作確認していない)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/e097c7c4-79c4-7a18-4b9a-b7dcc6e9a5b4.png)

UIはクソでスマン。後でもう少しマシにする。

## 出来ること

* 画面上のメインテキスト/サブテキストの編集(「化物語 桃駒」の部分とその下の英字)ができる(テキスト中身編集/位置移動回転)
* Select Themeの色アイコンをクリックで、簡単に色テーマを切り替えできる
* FontColor, BackgroundColor 欄に色コード(#333333やwhite など)を指定することで、好きなカラーに変更可能
* Toggle Frameで周辺の白枠表示/非表示コントロールが可能
* Downloadボタンで編集結果の画像をダウンロード可能

## ソース

[Git](https://github.com/hidepy/MonogatariTobiraeMaker)

### ソース本体

```javascript:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script
    src="https://code.jquery.com/jquery-3.6.0.js"
    integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk="
    crossorigin="anonymous"></script>
    <script type="text/javascript" src="js/lib/fabric.min.js"></script>
    <script type="text/javascript"  src="js/lib/FileSaver.js"></script>
    <script type="text/javascript"  src="js/lib/canvas-toBlob.js"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@500&display=swap" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css2?family=Kotta+One&family=Noto+Serif+JP:wght@500&family=Quintessential&display=swap" rel="stylesheet">
    <style>
        #canvas{
	        display: block;
        }
        #meta-wrapper {
            margin-left: auto;
            margin-right: auto;
        }
        #wrapper {
            width: 100%;
            height: 100%;
        }
        .color-preset-changer {
            width: 32px;
            height: 32px;
            border-radius: 16px;
            margin: 8px;
            display: inline-block;
        }
    </style>
    <title>化物語「風」扉絵メーカー v0.5(プロトタイプ)</title>
</head>
<body>
    <div style="display: block">
        <div id="meta-wrapper">
            <div id="wrapper">
                <canvas id="canvas" width="" height=""></canvas>
            </div>
        </div>

        <div id="current-color-wrapper">
            <span class='setting-label'>FontColor: </span>
            <input type="text" id="current-color-fontColor">
            <span class='setting-label'>BackgroundColor: </span>
            <input type="text" id="current-color-bgColor">

            <button id="current-color-change-button">Change Color</button>
        </div>

        <div id="color-preset-wrapper"></div>

        <div id="frame-toggle-wrapper">
            <div class='setting-label'>Toggle Frame: </div>
            <input type="checkbox" id="frame-toggle-check">
        </div>

        <div id="download-image-wrapper">
            <button id="download-image">Download</button>
        </div>

        <input type="hidden" id="current-theme-holder">
    </div>

    <script>
        const COLOR_PRISETS = ["#1f2a3b/#ffffff", "#443830/#ffffff", "#9b459b/#ffffff", "#e0684b/#ffffff", "#dd931b/#ffffff", "#c40c09/#ffffff", "#bfbdbe/#ffffff", "#7d002f/#ffffff", "#e16369/#ffffff", "#e2601b/#ffffff", "#d2bd6b/#ffffff", "#90b932/#ffffff", "#8190ab/#1a1212", "#eaeae8/#8190ab"]

        const elCanvas = document.getElementById("canvas")
        const elWrapper = document.getElementById("wrapper")

        const baseWidth = elWrapper.offsetWidth * 0.8
        const canvasWidth = baseWidth
        const canvasHeight = baseWidth * 9 / 16
        elCanvas.width = canvasWidth
        elCanvas.height =  canvasHeight

        const canvas = new fabric.Canvas("canvas")

        const [defaultBgColor, defaultFontColor] = COLOR_PRISETS[0].split("/")
        const defaultFontFamily = "HGP明朝B, 'HGP明朝B', Noto Serif JP, 'Noto Serif JP', serif"

        $("#current-theme-holder").attr("data-attr-bgcolor", defaultBgColor).attr("data-attr-fontcolor", defaultFontColor)

        const bg = new fabric.Rect({
            left: 0,
            top: 0,
            fill: defaultBgColor,
            width: canvasWidth,
            height: canvasHeight,
            selectable: false,
        })

        const mainText = new fabric.IText("化物語 黒齣", {
            fontWeight: 'normal',
            left: canvasWidth / 2,
            top: canvasHeight / 2 - 48,
            fontFamily: defaultFontFamily,
            charSpacing: 1000,
            fill: defaultFontColor,
            fontSize: 64,
            originX: "center",
            originY: "center",
            selectable: true,
        })

        const subText = new fabric.IText("bakemonogatari                                            kuro", {
            fontWeight: 'normal',
            left: canvasWidth / 2,
            top: canvasHeight / 2 + 24,
            fontFamily: "Kotta One, 'Kotta One', serif",
            fontStyle: 'italic',
            charSpacing: 10,
            fill: defaultFontColor,
            fontSize: 24,
            originX: "center",
            originY: "center",
            selectable: true,
        })

        const rectangleWrap = new fabric.Rect({
            left: 64,
            top: 48,
            fill: "transparent",
            width: canvasWidth - 128,
            height: canvasHeight - 96,
            selectable: false,
            stroke: "transparent",
            strokeWidth: 4,
        })

        const calcFrameColor = color=> $("#frame-toggle-check").prop('checked') ? color : "transparent"

        const changeTheme = (bgColor, fontColor)=> {
            $("#current-theme-holder").attr("data-attr-bgcolor", bgColor).attr("data-attr-fontcolor", fontColor)

            bg.set("fill", bgColor)
            mainText.set("fill", fontColor)
            subText.set("fill", fontColor)
            rectangleWrap.set("stroke", calcFrameColor(fontColor))

            canvas.renderAll()
        }

        $("#color-preset-wrapper").append("<div class='setting-label'>Select Theme: </div>")
        const elColorPresetWrapper = COLOR_PRISETS.forEach(v=> {
            const [bgColor, fontColor] = v.split("/")
            const el = 
                $(`<div id='${v}' class='color-preset-changer' style='background-color: ${bgColor}' data-attr-bgcolor='${bgColor}' data-attr-fontcolor='${fontColor}'></div>`)
                    .on("click", event=> {
                        const bgColor = $(event.target).attr("data-attr-bgcolor")
                        const fontColor = $(event.target).attr("data-attr-fontcolor")

                        changeTheme(bgColor, fontColor)
                    })
            $("#color-preset-wrapper").append(el)
        })
        
        $("#frame-toggle-check").on("click", event=> {
            const fontColor = $("#current-theme-holder").attr("data-attr-fontcolor")
            rectangleWrap.set("stroke", calcFrameColor(fontColor))

            canvas.renderAll()
        })

        $("#current-color-change-button").on("click", event=> {
            const fontColor = $("#current-color-fontColor").val()
            const bgColor = $("#current-color-bgColor").val()

            changeTheme(bgColor, fontColor)
        })

        $("#download-image").on("click", ()=> {
            $("#canvas").get(0).toBlob(function(blob){
		        saveAs(blob, "monogatari-like-cut.png");
            })
        })

        canvas.add(bg)
        canvas.add(rectangleWrap)
        canvas.add(mainText)
        canvas.add(subText)
    </script>
</body>
</html>
```

### ソースざっくり説明

* Webフォントでそれっぽいフォントを使うことにしているよ
* 生Canvasではなくて、fabric.jsを使ってるよ
* ダウンロード処理にはFileSaver.jsとcanvas-toBlob.jsを使ってるよ

後は特記事項無しですね...

## 参考リンク

[fabric.js](http://fabricjs.com/)
[eligrey FileSaver(Git)](https://github.com/eligrey/FileSaver.js)
[eligrey canvas-toBlob(Git)](https://github.com/eligrey/canvas-toBlob.js)