概要
以前の記事の応用です。
https://qiita.com/Yoshihiro-Hirose/items/82b9dccdf270551db887
動画ファイルを選択してプレビュー表示して、その一部をマウスでクリックして画像に切り出します。
動作サンプル
CodePenで動作するサンプルを用意しました。
狭いので右上のロゴから別ウィンドウを推奨します。
適当な動画を選択して、マウスでクリックしてみてください。
※CodePen 上で動かすと座標が多少ずれます
See the Pen SelectVideoPointClip by Yoshihiro Hirose (@yoshihiro-hirose) on CodePen.
処理の流れ
- マウスカーソルの画像を用意
- 動画ファイル選択時に表示する video 要素にクリックイベントを登録
- クリック時の座標を計算して canvas に書き出す
マウスカーソルの画像を用意
クリップする感を出すためにカーソル用の画像を用意します。
枠があって中が透過されていれば OK。

static ディレクトリに画像を配置して、vue ファイルにもスタイルを定義して video 要素から参照させます。
<style lang="sass" scoped>
.clipable
cursor: url('/clip_cursol.png'), crosshair
</style>
動画ファイル選択時に表示する video 要素にクリックイベントを登録
@click で関数を指定してビデオ内のクリックを捕捉します。
再生ボタンや早送りなどのコントロールはクリックイベントの対象外です。
<video
:
:
:
@click="clip"
>
クリック時の座標を計算して canvas に書き出す
画像を書き出す canvas を用意しておきます。
<canvas
id="c"
:width="matching_image_side"
:height="matching_image_side"
></canvas>
ビデオクリック時の関数は以下のようになりました。
clip(event) {
const video = document.getElementById('upload-video')
const canvas = document.getElementById('c')
// 実際のビデオサイズ
const orgVideoWidth = video.videoWidth
const orgVideoHeight = video.videoHeight
// ブラウザ上の表示サイズ
const eleVideoWidth = video.width
const eleVideoHeight = video.height
// 割合を計算して処理していく
const widthRatio = orgVideoWidth / eleVideoWidth
const heightRatio = orgVideoHeight / eleVideoHeight
// カーソルの枠分ピクセルを調整
const pointX = event.offsetX + 3
const pointY = event.offsetY + 3
const srcX = pointX * widthRatio
const srcY = pointY * heightRatio
canvas
.getContext('2d')
.drawImage(
video,
srcX,
srcY,
this.matching_image_side,
this.matching_image_side,
0,
0,
this.matching_image_side,
this.matching_image_side
)
},
以下工夫ポイント
- クリックした座標からマウスカーソルの枠分を考慮して調整
- drawImage で video から書き出す際、実際の動画のサイズが基準になっている。
- ブラウザ上で表示した場合に比べて差があるので、割合を計算して調整している。
おまけ
canvas に書き出した画像を API からアップロードしたい場合、Blob を生成して multipart で送れます。
詳細は全体のコードを確認してください。
ファイル全体
<template>
<v-row justify="center">
<v-col sm="12" md="11" lg="9" xl="6">
<v-sheet class="pa-3">
<h1>動画のマウスクリックによる一部切り出し</h1>
<v-form ref="form">
<video
v-if="uploadVideoUrl"
id="upload-video"
class="clipable"
controls
width="640"
height="360"
@click="clip"
>
<source :src="uploadVideoUrl" />
このブラウザではビデオ表示がサポートされていません
</video>
<v-file-input
v-model="work_video"
accept="video/*"
show-size
label="適当な動画ファイルを選択してください"
prepend-icon="mdi-video"
@change="onVideoPicked"
></v-file-input>
<canvas
id="c"
:width="matching_image_side"
:height="matching_image_side"
></canvas>
<span v-if="uploadVideoUrl"
>※動画をクリックして選択箇所を画像に切り出します</span
>
</v-form>
<v-row align="center" justify="center" class="mt-12 mb-4 px-5">
<v-btn outlined color="iconcolor" rounded block @click="post"
>サーバにPOSTする予定ボタン</v-btn
>
</v-row>
</v-sheet>
</v-col>
</v-row>
</template>
<script>
export default {
data() {
return {
work_video: null,
matching_image_side: '28',
matching_image: null,
uploadVideoUrl: ''
}
},
methods: {
onVideoPicked(file) {
if (file !== undefined && file !== null) {
if (file.name.lastIndexOf('.') <= 0) {
return
}
const fr = new FileReader()
fr.readAsDataURL(file)
fr.addEventListener('load', () => {
this.uploadVideoUrl = fr.result
})
} else {
this.uploadVideoUrl = ''
}
},
clip(event) {
const video = document.getElementById('upload-video')
const canvas = document.getElementById('c')
// 実際のビデオサイズ
const orgVideoWidth = video.videoWidth
const orgVideoHeight = video.videoHeight
// ブラウザ上の表示サイズ
const eleVideoWidth = video.width
const eleVideoHeight = video.height
// 割合を計算して処理していく
const widthRatio = orgVideoWidth / eleVideoWidth
const heightRatio = orgVideoHeight / eleVideoHeight
// カーソルの枠分ピクセルを調整
const pointX = event.offsetX + 3
const pointY = event.offsetY + 3
const srcX = pointX * widthRatio
const srcY = pointY * heightRatio
canvas
.getContext('2d')
.drawImage(
video,
srcX,
srcY,
this.matching_image_side,
this.matching_image_side,
0,
0,
this.matching_image_side,
this.matching_image_side
)
},
async post() {
const blobPromise = new Promise(function(resolve, reject) {
const canvas = document.getElementById('c')
canvas.toBlob(
function(blob) {
resolve(blob)
},
'image/jpeg',
1.0
)
})
const formData = new FormData()
formData.append('image', await blobPromise, 'test.jpg')
// API で切り抜いた画像をアップロードできる
// await this.$axios.post('/api/test', formData, {
// headers: {
// 'Content-Type': 'multipart/form-data'
// }
// })
}
}
}
</script>
<style lang="sass" scoped>
.clipable
cursor: url('/clip_cursol.png'), crosshair
</style>
最後に
指摘事項や質問ありましたらお気軽にコメントください!