tag:crieit.net,2005:https://crieit.net/tags/canvas/feed 「canvas」の記事 - Crieit Crieitでタグ「canvas」に投稿された最近の記事 2020-09-20T11:34:07+09:00 https://crieit.net/tags/canvas/feed tag:crieit.net,2005:PublicArticle/16063 2020-09-20T00:57:20+09:00 2020-09-20T11:34:07+09:00 https://crieit.net/posts/9607755c4800bec007d1a912ce8f42dc 草野球のライブ配信を補助するツールを作った話 <h1 id="草野球放送局"><a href="#%E8%8D%89%E9%87%8E%E7%90%83%E6%94%BE%E9%80%81%E5%B1%80">草野球放送局</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://baseball-broadcast.vercel.app/">https://baseball-broadcast.vercel.app/</a></p> <p><a href="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f66295a77955.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f66295a77955.jpg?mw=700" alt="" /></a></p> <h1 id="仕組み"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF">仕組み</a></h1> <p>「ツイキャスゲームズ」などのスクリーン配信に対応したサービスで、<br /> スマホの画面を直接ストリーミング配信できることを利用して、<br /> スマホ側でカメラを起動して画面を合成したらいいじゃないかという発想に。<br /> react-webcamというライブラリ(canvasで描画している)とNextJSを組み合わせました。</p> <h1 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h1> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/EjPa1nqVwAk" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h1 id="使った技術"><a href="#%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">使った技術</a></h1> <h2 id="NextJS"><a href="#NextJS">NextJS</a></h2> <p>最低でもQA画面と設定画面、合成画面が必要になると思っていたので、<br /> NextJSを選択しました。</p> <h2 id="Vercel"><a href="#Vercel">Vercel</a></h2> <p>デプロイ用のプラットフォーム</p> <h2 id="react-webcam"><a href="#react-webcam">react-webcam</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/mozmorris/react-webcam">react-webcam</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ozota/items/0fc56f600955c6380dde">react(nativeでない)でカメラを使ってみる</a></li> </ul> <h1 id="苦労したこと"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">苦労したこと</a></h1> <h2 id="カメラのデバッグ"><a href="#%E3%82%AB%E3%83%A1%E3%83%A9%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0">カメラのデバッグ</a></h2> <p>セキュリティ的にカメラの起動条件がlocalhostか、https通信下だったので、最初はvercelに上げながら動作確認していましたが、途中からcodesandboxを使っていました。</p> <p><strong>iPhone/Chromeではカメラが起動しない</strong>とか。</p> <h2 id="スマホの向きを変えたときの描画"><a href="#%E3%82%B9%E3%83%9E%E3%83%9B%E3%81%AE%E5%90%91%E3%81%8D%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E6%8F%8F%E7%94%BB">スマホの向きを変えたときの描画</a></h2> <p>縦横の向きを変えたときにリサイズする処理を色々試したのですが、</p> <ul> <li>addEventListener('resize')</li> <li>再描画回避のためにuseRefを使う</li> <li>縦横のサイズを入れ替える</li> </ul> <p>どれも結局上手くいかず、stackoverflowなどを見ながら最終的にたどり着いたのはCSSでの表示制御でした。えぇぇぇぇぇ.....。</p> <h2 id="UI詰め過ぎ問題"><a href="#UI%E8%A9%B0%E3%82%81%E9%81%8E%E3%81%8E%E5%95%8F%E9%A1%8C">UI詰め過ぎ問題</a></h2> <p>スコアボードのUIを詰め過ぎてタップしづらいという問題が発生。<br /> <code>transform: scale(1.3)</code>でコンポーネントごと1.3倍にして逃げました。<br /> <a href="https://crieit.net/boards/web1week-202005/WEB-UI#パーツ%28コンポーネント%29の組み合わせ">秘伝のたれを思いついた瞬間</a></p> <pre><code class="javascript">import Webcam from "react-webcam"; import NHKBoard from "./NHKBoard"; const Home = () => { const videoConstraints = { facingMode: { exact: "environment" }, aspectRatio: 1.78 }; return ( <div style=<span>{</span><span>{</span> width:'95vw', height:'95vh', position:'relative' <span>}</span><span>}</span> > <Webcam style=<span>{</span><span>{</span> position:'absolute', top:0, left:0, width:'95vw', height:'95vh', <span>}</span><span>}</span> videoConstraints={videoConstraints} /> <div style=<span>{</span><span>{</span> transform:'scale(1.3)', position:'absolute', bottom:0, right:0 <span>}</span><span>}</span> > <NHKBoard/> </div> </div> ) } export default Home; </code></pre> <h1 id="IKEN"><a href="#IKEN">IKEN</a></h1> <p>スコアボードの種類を選べるようにするとか、選手紹介を表示するとか機能拡張は考えていますが、一旦最小構成でリリースしますのでフィードバックいただけるとうれしいです!</p> <p><a target="_blank" rel="nofollow noopener" href="https://ikens.net/ckoshien_tech/baseball-broadcast">https://ikens.net/ckoshien_tech/baseball-broadcast</a></p> ckoshien tag:crieit.net,2005:PublicArticle/16031 2020-08-16T06:52:03+09:00 2022-07-11T19:07:52+09:00 https://crieit.net/posts/music-waves-visualizer WebAudioAPIとcanvasで音声波形動画を出力するサイトを作ってみた <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>YouTubeなどで見かけるよくあるミュージックビデオに音声波形が動いて表示されたりするのがあります。<br /> そういう動画は調べてみると、大抵はWindowsのAviUtlという無料の動画編集ソフトの拡張機能で作られてるようです。<br /> 最近では、daniwell氏により<a target="_blank" rel="nofollow noopener" href="https://aidn.jp/mvg/">Music Visualization Generator</a>がWindows/Macのフリーソフトがリリースされ、簡単に音声波形動画が作れるようになりました。<br /> そういった中で、Web上でも作って共有できたら便利だなと思い、調べてみたところ、どうやらHTML5のWebAPIとJavascriptでできるようなのでやってみました。</p> <h2 id="作ったもの"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">作ったもの</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://komura-c.github.io/music-waves-visualizer/">Music Waves Visualizer</a></p> <p><a href="https://crieit.now.sh/upload_images/d4815e4367e4f03b703f791d465c9a6f5f3858b51166f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d4815e4367e4f03b703f791d465c9a6f5f3858b51166f.png?mw=700" alt="スクリーンショット 2020-08-14 19.41.42.png" /></a><br /> <img src="https://i.gyazo.com/1e8b95de8bd4f40e203ca3d25c42acbd.gif" alt="" /></p> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/komura-c/music-waves-visualizer/">komura-c/music-waves-visualizer</a></p> <h2 id="実装した機能"><a href="#%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F%E6%A9%9F%E8%83%BD">実装した機能</a></h2> <p>WebAPIとCanvas、Next.jsを駆使して以下を実装しました。<br /> ・画像と音声の読み込み、表示<br /> ・音声波形表示<br /> ・動画として出力</p> <h2 id="画像と音声の読み込み"><a href="#%E7%94%BB%E5%83%8F%E3%81%A8%E9%9F%B3%E5%A3%B0%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF">画像と音声の読み込み</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload">FileReader.onload - Web API | MDN</a><br /> を参考にして、画像を読み込むと同時にcanvasに描画しています。<br /> 音声も同様ですが、読み込み時にbase64としてデコードすることでWebAudioAPIに渡した上で、FileReaderに読み込ませています。</p> <pre><code class="ts">// audioLoad const LoadSample = (audioCtx, audioDataUrl) => { fetch(audioDataUrl) .then((response) => { return response.arrayBuffer(); }) .then((arrayBuffer) => { audioCtx.decodeAudioData(arrayBuffer).then((decodedData) => { buffer = decodedData; }); }) }; </code></pre> <h2 id="音声波形表示"><a href="#%E9%9F%B3%E5%A3%B0%E6%B3%A2%E5%BD%A2%E8%A1%A8%E7%A4%BA">音声波形表示</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Drawing_graphics_with_canvas">canvas に絵を描く - Web API | MDN</a>を参考にしました。<br /> モード別に描画方法を変えることで周波数バー・折れ線・円形を実現しています。</p> <h2 id="動画として出力"><a href="#%E5%8B%95%E7%94%BB%E3%81%A8%E3%81%97%E3%81%A6%E5%87%BA%E5%8A%9B">動画として出力</a></h2> <p>以下の記事を参考に実装しました。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/ru_shalm/items/0930aedad12c4e100446">MediaRecorder APIをつかってCanvas/WebAudioなゲーム画面を録画する</a><br /> <a target="_blank" rel="nofollow noopener" href="https://blog.ver001.com/javascript-canvas-mediarecorder/">JavaScriptでcanvasを録画して動画ファイルに保存する方法</a></p> <p>また、上記の対応だとwebpでしか動画の出力ができませんでした。<br /> そのため、ffmpeg.wasmを使いmp4で書き出せるように対応しました。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/ffmpegwasm/ffmpeg.wasm">https://github.com/ffmpegwasm/ffmpeg.wasm</a></p> <pre><code class="ts">import { createFFmpeg } from "@ffmpeg/ffmpeg"; export async function generateMp4Video( binaryData: Uint8Array, webmName: string, mp4Name: string ) { const ffmpeg = createFFmpeg({ log: true }); await ffmpeg.load(); ffmpeg.FS("writeFile", webmName, binaryData); await ffmpeg.run("-i", webmName, "-vcodec", "copy", mp4Name); return ffmpeg.FS("readFile", mp4Name); } </code></pre> <h2 id="今後の課題"><a href="#%E4%BB%8A%E5%BE%8C%E3%81%AE%E8%AA%B2%E9%A1%8C">今後の課題</a></h2> <ul> <li>canvasの性質上、モバイルへの対応が難しいので知見を深める</li> </ul> <h2 id="参考記事"><a href="#%E5%8F%82%E8%80%83%E8%A8%98%E4%BA%8B">参考記事</a></h2> <h3 id="WebAudioAPIについて"><a href="#WebAudioAPI%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">WebAudioAPIについて</a></h3> <p>使い方は以下を参考にしました。<br /> <a target="_blank" rel="nofollow noopener" href="https://webmusicdevelopers.appspot.com/codelabs/webaudio/index.html?ja-jp">Web Audio API | Codelab</a><br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API#Example">Web Audio API - Web API | MDN</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.html5rocks.com/ja/tutorials/webaudio/intro/">Web Audio API の基礎</a></p> <p>WebAudioAPIで詰まったところは、以下のように仕様書を日本語訳してくれている方がいらっしゃるので辞書代わりに参考にしました。<br /> <a target="_blank" rel="nofollow noopener" href="https://g200kg.github.io/web-audio-api-ja/">Web Audio API ( 日本語訳 )</a></p> こむら tag:crieit.net,2005:PublicArticle/15887 2020-05-06T15:52:28+09:00 2020-05-06T15:52:28+09:00 https://crieit.net/posts/Canvas-Konva-js-OGP-Nuxt Canvasのライブラリ「Konva.js」でOGP生成(Nuxtアプリ) <p>Nuxt.jsとFirebaseで作っていた<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">Webサービス</a>を7月末にリリースして、はや10ヶ月。。<br /> 登録総額1億円を突破したので、記念企画として<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">積読レビュー</a>機能をリリースしました!!</p> <p>そこで<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>というライブラリを使ってOGP画像を生成したので、<br /> その時に調べたことをまとめてみました。</p> <h3 id="積読・読書前でも書ける『積読レビュー』"><a href="#%E7%A9%8D%E8%AA%AD%E3%83%BB%E8%AA%AD%E6%9B%B8%E5%89%8D%E3%81%A7%E3%82%82%E6%9B%B8%E3%81%91%E3%82%8B%E3%80%8E%E7%A9%8D%E8%AA%AD%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">積読・読書前でも書ける『積読レビュー』</a></h3> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p> <p>読書のレビューというと、読書後の感想を書くものですが、<br /> 積ん読が多いとなかなか書く機会がないです。。</p> <p><strong>「感想は読書後だけではないのでは?」</strong><br /> ということで、読書前でも書けるレビューを2つ用意してみました。</p> <h4 id="妄想で書く『妄想レビュー』"><a href="#%E5%A6%84%E6%83%B3%E3%81%A7%E6%9B%B8%E3%81%8F%E3%80%8E%E5%A6%84%E6%83%B3%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">妄想で書く『妄想レビュー』</a></h4> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/8e8a9968-c44f-64c8-93d2-0f8eb1f3172a.png" width="600px"/></p> <p><strong>まだ読んでないけど、本の表紙や帯の印象から妄想で書いてみる『妄想レビュー』</strong></p> <p>「中身じゃなく外観だけで、読書後のレビューを書いてみるとどうだろう。。?」<br /> というネタ的なレビューです。</p> <p>いかに読んでる醸し出せるかをチャレンジしてみるとおもしろいかも(<em>´ω`</em>)</p> <p>他の使い方としては、<strong>読む前に書いておいて、読んだ後との感想を比較してみる</strong>のもたのしいかも?<br /> 読書前後で感想が同じになっても違っていても、新たな発見があるかもです。</p> <h4 id="きっかけを書く『きっかけレビュー』"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%82%92%E6%9B%B8%E3%81%8F%E3%80%8E%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">きっかけを書く『きっかけレビュー』</a></h4> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e740910f-2687-0484-e3d2-bebdd0d95bf6.png" width="600px"/></p> <p><strong>買ったときのきっかけや意気込み・ワクワク感を書いてみる『きっかけレビュー』</strong></p> <p>「『なぜ買うのか、どうして買いたいのか』という気持ちを残しておくのもどうだろう。。?」<br /> と思いつきつくってみました。</p> <p>買ったときの気持ちも大事で、<br /> * 技術書やビジネス書なら「こうなりたい!ここを強くしたい!」<br /> * 小説やマンガなら「ひさびさの新刊!たのしみ!」<br /> という思いがあるはず・・</p> <p><strong>それを記録として残しておくと、いつか見返したときにたのしいかもしれません。</strong></p> <h2 id="なぜKonva.js?"><a href="#%E3%81%AA%E3%81%9CKonva.js%EF%BC%9F">なぜKonva.js?</a></h2> <p>この企画では、本を選んで、レビューを書いていくのですが、<br /> 本の表紙など画像を埋め込む必要があります。</p> <p>以前、<a target="_blank" rel="nofollow noopener" href="http://hen-ai.net/">別のサービス</a>で、<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/01/24/110000">SVGでOGP用の画像を生成</a>してみたのですが、端末によってはうまくいかなかったりと、画像を埋め込むのがかなり大変でした。。</p> <p>なので、SVGではなく、Canvasで試してみようと調べたところ、<br /> <a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>と<a target="_blank" rel="nofollow noopener" href="http://fabricjs.com/">Fabric.js</a>が見つけ、<br /> 文字の折返しなどのガイドがあった<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>を選んでみました。</p> <h2 id="Konvaで画像生成"><a href="#Konva%E3%81%A7%E7%94%BB%E5%83%8F%E7%94%9F%E6%88%90">Konvaで画像生成</a></h2> <p>ここからが本題。やっと本題。。</p> <h3 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h3> <h4 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h4> <pre><code class="shell">$ npm install vue-konva konva --save </code></pre> <p>今回は、Nuxt/Vueで使うので、KonvaのVueライブラリ(<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/vue/index.html">vue-konva</a>)もインストール</p> <h4 id="プラグインの作成"><a href="#%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90">プラグインの作成</a></h4> <p>vue-konvaを使えるようプラグインを作成。</p> <pre><code class="ts">// ~/plugins/vue-konva.ts import Vue from "vue"; import VueKonva from "vue-konva"; Vue.use(VueKonva); </code></pre> <p>そして、作ったプラグインをnuxt.config.tsに追加。</p> <pre><code class="ts">// nuxt.config.js import { Configuration } from "@nuxt/types"; const config: Configuration = { mode: "spa", // 略 plugins: [ { src: "~/plugins/vue-konva", ssr: false }, // 略 ], // 略 }; </code></pre> <p>これで準備はOK</p> <h3 id="KonvaでCanvasを描いてみる"><a href="#Konva%E3%81%A7Canvas%E3%82%92%E6%8F%8F%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">KonvaでCanvasを描いてみる</a></h3> <h4 id="画像を表示する(v-image)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28v-image%29">画像を表示する(v-image)</a></h4> <p>まずは、フレームとなる以下の背景画像を表示してみます。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/9033133e-55f5-17eb-a8bc-85ce000a68ef.png" width="600px"/></p> <pre><code class="html"><template> <div> <v-stage :config="configKonva"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; @Component export default class ReviewPreviewSample extends Vue { // konva全体の設定(v-stageに対応) private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT }; // 背景画像の設定(v-imageに対応) private configBg: { image: HTMLImageElement | null } = { image: null }; async mounted() { // マウントされたら、背景画像を読み込んで設定する this.setupBg().then(); } // 背景画像の設定 private async setupBg() { const image = await this.getImage("/img/review_paple.png"); this.configBg.image = image; } // 共通処理: 画像の読み込み private async getImage(src: string): Promise<HTMLImageElement> { return new Promise((resolve, reject) => { const image = new window.Image(); image.onload = () => resolve(image); image.src = src; }); } } </script> </code></pre> <p>こんな感じ。</p> <h5 id="基本的な構成"><a href="#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E6%A7%8B%E6%88%90">基本的な構成</a></h5> <p>基本的な構成は、こんな感じで、<br /> <code>v-stage > v-layer > その他諸々</code><br /> で配置していく。</p> <pre><code class="html"><v-stage :config="configKonva"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> </v-stage> </code></pre> <p>必ずrootが<code>v-stage</code>で、その直下が<code>v-layer</code>。<br /> <code>v-layer</code>はいくつも作れるけど、3〜4までがよいっぽい。(waringがでた)</p> <p>位置やサイズ、表示する画像は<code>:config</code>を介して、Kanva.js自体の設定値を付与していく。</p> <p>どんな設定値があるかは、<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>を見ていく感じで、<br /> Imageだと<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.Image.html">ここ(Class: Image)をみる</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/">チュートリアル</a>もあるので、それを見つつ、<br /> 具体的な設定値は<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>参照という流れで進めた。</p> <h5 id="画像の表示には読み込みが必要"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AB%E3%81%AF%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%8C%E5%BF%85%E8%A6%81">画像の表示には読み込みが必要</a></h5> <p>画像の表示は少しめんどくさくて、</p> <ol> <li>HTMLImageElementをつくって</li> <li>HTMLImageElementで画像をロードして</li> <li>ロード後のHTMLImageElementをconfigに設定</li> </ol> <p>という流れ。。</p> <p>それをしているのがこのあたり。<br /> 画像読み込みは何度も使うけど、Promiseじゃないので、<br /> ロードが終わったらHTMLImageElementを返す処理を共通化</p> <pre><code class="ts">async mounted() { // マウントされたら、背景画像を読み込んで設定する this.setupBg().then(); } // 背景画像の設定 private async setupBg() { const image = await this.getImage("/img/review_paple.png"); this.configBg.image = image; } // 共通処理: 画像の読み込み private async getImage(src: string): Promise<HTMLImageElement> { return new Promise((resolve, reject) => { const image = new window.Image(); image.onload = () => resolve(image); image.src = src; }); } </code></pre> <p>これでできるのがこんな感じ。</p> <p><img width="600px" alt="スクリーンショット 2020-05-06 12.46.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b295f2ff-7b56-5ab4-1e9c-055ef7ff4314.png"></p> <p>でかい。。<br /> CanvasのサイズをOGP画像のサイズ(1200x630)にしているもんね。。</p> <h5 id="画像を画面サイズにあわせる"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E3%81%82%E3%82%8F%E3%81%9B%E3%82%8B">画像を画面サイズにあわせる</a></h5> <p>ほんとはこんな感じにしたい。。</p> <p><img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e3210e3f-c4b0-6e6c-1a11-e791dc90dd05.gif" /></p> <p>ので、Windowサイズの変更を検知して、CSSのscaleで調整してみる。</p> <p>こんな感じ。</p> <pre><code class="html"><template> <!-- サイズ計算用にidが必要なので追加 --> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; @Component export default class ReviewPreviewSample extends Vue { private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT }; private configBg: { image: HTMLImageElement | null } = { image: null }; private scale: number = 0; async mounted() { this.setupBg().then(); // 初回のスケール計算処理を走らせる this.$nextTick(() => this.handleResize()); // イベントリスナーにスケール計算処理を登録 window.addEventListener("resize", this.handleResize); } beforeDestroy() { // Destroy前に解放 window.removeEventListener("resize", this.handleResize); } private get style() { // transformのscaleを使って、サイズ調整 return { transform: `scale(${this.scale})`, "-webkit-transform": `scale(${this.scale})`, "transform-origin": "0 0" }; } // スケール計算用の処理: 対象IDのサイズを取得して、scaleを計算 private handleResize() { const elm = document.getElementById("preview-wrapper"); if (!elm) return; const rect = elm.getBoundingClientRect(); this.scale = rect.width / IMAGE_WIDTH; } private async setupBg() { // 略 } private async getImage(src: string): Promise<HTMLImageElement> { // 略 } } </script> <style lang="scss" scoped> /* 画像のアスペクト比は固定なので、あらかじめ高さをCSSで調整 */ .preview-wrapper { position: relative; width: 100%; height: auto; } .preview-wrapper { &:before { content: ""; display: block; padding-top: 52.5%; /* 630 / 1200 x 100 */ } } .preview-content { position: absolute; top: 0; left: 0; } </style> </code></pre> <p>これでOK。Konva.jsの話に戻る</p> <h4 id="画像を加工する(サイズ変更)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E3%82%B5%E3%82%A4%E3%82%BA%E5%A4%89%E6%9B%B4%29">画像を加工する(サイズ変更)</a></h4> <p>次は書影を読み込んで、配置する部分。<br /> 書影は本ごとにサイズが違うので、加工が必要。。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_13_06_55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/733c10bc-58e3-ffd4-45cd-09d344c9eb4f.png"></p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> <v-layer> <!-- 書影の画像 --> <v-image :config="configBookImg"></v-image> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; const BOOK_IMAGE_WIDTH = 464; const BOOK_IMAGE_HEIGHT = 470; @Component export default class ReviewPreviewSample extends Vue { // 略 private configBookImg: any | null = null; async mounted() { this.setupBg().then(); // 略 } // 略 private async setupBookImage() { // 画像の読み込み const imageBook = await this.getImage("/img/notfound_paple.png"); // v-imageのconfig const scale = BOOK_IMAGE_WIDTH / imageBook.width; this.configBookImg = { image: imageBook, x: 48, // 左上からの位置(x) y: 112, // 左上からの位置(y) width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横) height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦) crop: { // 切り取り処理 x: 0, // 切り取り位置(x) y: 0, // 切り取り位置(y) width: imageBook.width, // 切り取りサイズ(横) height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦) } }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p>こんな感じ。切り取りは、v-imageのcropを指定すればOK。</p> <p>ここで若干ハマった。。</p> <h5 id="【ハマり1】cropとscaleを同時に指定するとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A1%E3%80%91crop%E3%81%A8scale%E3%82%92%E5%90%8C%E6%99%82%E3%81%AB%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%80%82%E3%80%82">【ハマり1】cropとscaleを同時に指定するとうまくいかない。。</a></h5> <p>サイズを調整しようと、いろいろしていたときに、<br /> スケールを変化させる<code>scale</code>と切り取り処理の<code>crop</code>を同時に指定してみたけど、<br /> あまりうまくいかず、cropのみで対応した。。</p> <h5 id="【ハマり2】computedだとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A2%E3%80%91computed%E3%81%A0%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%80%82%E3%80%82">【ハマり2】computedだとうまくいかない。。</a></h5> <p>最初は、以下のような感じで、computedにしてたけど、<br /> cropなど、配下のobjectが変わっても変更が反映されなかったので、<br /> dataのconfigBookImgを用意する形にしてみた。</p> <pre><code class="ts">get configBookImg() { // 画像の読み込み const imageBook = await this.getImage("/img/notfound_paple.png"); // v-imageのconfig const scale = BOOK_IMAGE_WIDTH / imageBook.width; return { image: imageBook, x: 48, // 左上からの位置(x) y: 112, // 左上からの位置(y) width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横) height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦) crop: { // 切り取り処理 x: 0, // 切り取り位置(x) y: 0, // 切り取り位置(y) width: imageBook.width, // 切り取りサイズ(横) height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦) } }; } </code></pre> <h4 id="画像を加工する(丸く切り取る)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E4%B8%B8%E3%81%8F%E5%88%87%E3%82%8A%E5%8F%96%E3%82%8B%29">画像を加工する(丸く切り取る)</a></h4> <p>次はこれ。ユーザのアイコン画像。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_14_08_51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/80f51e08-b4f4-5fd5-fa3e-35ef560b7197.png"></p> <p>もともとは、こんな四角い画像だけど、アイコンっぽく丸く切り取りたい。</p> <p><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/62f0de4a-a307-82be-9755-a1a0963b9e2e.png" /></p> <p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/clipping/Clipping_Function.html">チュートリアル</a>を見てみると、<code>v-group</code>を使うとできるっぽい。</p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <!-- 略 --> <v-layer> <v-group :config="configUserImageGroup"> <v-image :config="configUserImg"></v-image> </v-group> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; const BOOK_IMAGE_WIDTH = 464; const BOOK_IMAGE_HEIGHT = 470; const USER_IMAGE_SIZE = 76; @Component export default class ReviewPreviewSample extends Vue { // 略 private configUserImg: any | null = null; async mounted() { // 略 this.setupUserImage().then(); // 略 } // 略 private get configUserImageGroup() { return { // 複雑な切り取り処理: CanvasRenderingContext2Dのarcを使う clipFunc: ctx => { const r = USER_IMAGE_SIZE / 2; ctx.arc(1092 + r, 538 + r, r, 0, Math.PI * 2, false); }, // ドラッグを無効にする draggable: false }; } // ユーザ画像の読み込み処理: 本の画像とかとだいたい同じ private async setupUserImage() { const image = await this.getImage("/img/kira_puka.png"); this.configUserImg = { image: image, x: 1092, y: 538, width: USER_IMAGE_SIZE, height: USER_IMAGE_SIZE }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p><code>v-group</code>の<code>clipFunc</code>を使うと、Canvaを操作できるようで、<br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/arc">CanvasRenderingContext2Dのarc</a>を使って、丸く切り取り。</p> <p>注意点は、原点がCanvas(v-stage)の左上であること。<br /> 画像の左上が原点だと思っていたので、全然切り取られなかった。。</p> <h4 id="1行の文字を表示する(省略あり)"><a href="#1%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E3%81%82%E3%82%8A%29">1行の文字を表示する(省略あり)</a></h4> <p>画像はここまで。次は文字。本のタイトルを表示したい。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_14_20_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/5042be5a-3901-780e-34eb-3616558ed1e8.png"></p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <!-- 略 --> <v-layer> <v-text :config="configBookTitle"></v-text> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; // 略 @Component export default class ReviewPreviewSample extends Vue { // 略 private get configBookTitle() { return { // 表示する文字 text: "選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル", x: 200, y: 28, width: IMAGE_WIDTH - 400, fill: "#FFFFFF", // 文字の色 fontSize: 36, // フォントサイズ fontFamily: "Noto Sans JP", // フォントの種類 fontStyle: "bold", // フォントのスタイル align: "center", // 揃え位置(中央揃え) // 折り返しなし(wrap)を指定すると、省略(ellipsis)を設定可能に wrap: "none", ellipsis: true }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p>文字を表示するには、<code>v-text</code>を使えばOK</p> <p>CSSのtext-overflowと同じ感じで、折返しをnoneにすれば、省略記号の表示もできる。</p> <h4 id="複数行の文字を表示する(省略不可)"><a href="#%E8%A4%87%E6%95%B0%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E4%B8%8D%E5%8F%AF%29">複数行の文字を表示する(省略不可)</a></h4> <p>次はレビューの文字を表示。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_14_30_22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/75fc126e-1179-e8d1-a2ab-6aa1aaf44974.png"></p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <!-- 略 --> <v-layer> <v-text :config="configReview"></v-text> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; // 略 @Component export default class ReviewPreviewSample extends Vue { // 略 private get configReview() { return { text: "レビューの内容れびゅーのないよう\n\nレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないよう", x: 596, y: 188, width: 542, height: 270, fill: "#443a33", fontSize: 30, fontFamily: "Noto Sans JP" // 行の高さ lineHeight: 1.2, }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p>同じく<code>v-text</code>を使う感じ。<br /> <code>wrap</code>を指定しなければ、そのまま複数行に。</p> <p>改行文字を入れれば改行もしてくれて、widthを指定すれば折返しも自動。</p> <p>ただ、残念ながら、<code>-webkit-line-clamp</code>みたいに複数行の省略はないみたい。。</p> <h3 id="Canvasを画像にエクスポート"><a href="#Canvas%E3%82%92%E7%94%BB%E5%83%8F%E3%81%AB%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88">Canvasを画像にエクスポート</a></h3> <p>最後に作った画像をエクスポート!!</p> <p><img width="500" alt="スクリーンショット_2020-05-06_14_41_16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/57be76de-b8c2-3a34-31f8-5b85079b621c.png"></p> <pre><code class="html"><template> <div> <div id="preview-wrapper" class="preview-wrapper"> <!-- v-stageが取得できるようにrefを追加--> <v-stage ref="stage" :config="configKonva" class="preview-content" :style="style" > <!-- 略 --> </v-stage> </div> <!-- ボタンを追加 --> <div class="has-text-centered"> <b-button type="is-primary" size="is-medium" @click="download"> <span>作成する</span> </b-button> </div> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; // 略 @Component export default class ReviewPreviewSample extends Vue { // 略 // ダウンロード処理 private async download() { // stageの取得: 大きいサイズを取得するときは、'pixelRatio'で倍率を指定 const stage = (this.$refs.stage as any).getStage({ pixelRatio: 1 }); // stageからDataURLを取得 const dataURL = stage.toDataURL(); // ダウンロード処理: プログラムでリンクを作ってクリック const link = document.createElement("a"); link.download = "ogp.png"; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } </script> <style lang="scss" scoped> /** 略 **/ </style> </code></pre> <p>こんな感じ。Konvaでエクスポートするときには、<code>v-stage</code>から取得する。<br /> DataURL以外にJSONとかでもエクスポートできるので、保存したりもOK</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/data_and_serialization/Stage_Data_URL.html">HTML5 Canvas Stage Data URL Tutorial | Konva - JavaScript 2d canvas library</a></li> </ul> <p>開発しているサービスでは、FirebaseのCloud Storageに保存しているので、DataURL形式でOK。</p> <p>ハマリポイントは、外部URLの画像を使っているとき。。エクスポートに失敗する。。<br /> Cavans自体の仕様で外部URLの画像がある場合、汚染されたとみなされエラーになるとのこと。。</p> <p>対処法は以下に書かれている感じで、CORS対応をすればOK<br /> ・<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/posts/Tainted_Canvas.html">Resolving "Tainted canvases may not be exported" with Konva | Konva - JavaScript 2d canvas library</a></p> <h2 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h2> <p>SVGでも外部URLは問題があったけど、Konva.jsでも。。<br /> でも、サクサクできたのと、ドラッグで位置を変えれるみたいなので、いろいろできそう(<em>´ω`</em>)</p> <p>積ん読レビューはできたての機能なので、ぜひぜひさわってみてくださいヽ(=´▽`=)ノ<br /> ■<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">どっちでレビューする?妄想レビュ・きっかけレビュー</a></p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p> きらぷか@積読ハウマッチ/SSSAPIなど