メインコンテンツまでスキップ

CameraCrop

挙動

CameraCrop

使用ライブラリ

※ cameraはreact-camera-proの方が圧倒的に綺麗に撮れるが、枯れていない上に2024/7/11時点で、warningが出るため未使用。

ソースコード

ソースコード
'use client'

import Webcam from 'react-webcam'
import { useLayoutEffect, useRef, useState } from 'react'
import ReactCrop, { type Crop } from 'react-image-crop'
import NextImage from 'next/image'
import 'react-image-crop/dist/ReactCrop.css'

// 切り取りエリア座標/サイズ作成関数
const createCrop = (width: number, height: number): Crop => {
return {
unit: 'px',
x: width - width * 0.75,
y: height - height * 0.75,
width: width * 0.5,
height: height * 0.5,
}
}

type CameraButtonProps = {
onClick: () => void
text: string
}
const CameraButton = ({ onClick, text }: CameraButtonProps) => (
<button onClick={onClick} className='bg-white text-2xl p-4 rounded-full shadow-md hover:opacity-80'>
{text}
</button>
)

const CameraPage = () => {
const webcamRef = useRef<Webcam>(null)
const [enableCamera, setEnableCamera] = useState(false)
const [currentCaptureImage, setCurrentCaptureImage] = useState<string | null>(null)
const [captureImage, setCaptureImage] = useState<string | null>(null)
const [cameraWidth, setCameraWidth] = useState(0)
const [cameraHeight, setCameraHeight] = useState(0)
const [crop, setCrop] = useState<Crop>(createCrop(0, 0))

// 初期描画前に画面サイズを取得
useLayoutEffect(() => {
const aspectHeight = (window.innerWidth / 4) * 3
const windowHeight = window.innerHeight
setCameraWidth(window.innerWidth)
setCameraHeight(aspectHeight > windowHeight ? windowHeight : aspectHeight)
}, [])

// 撮影処理
const capture = () => {
const screenShot = webcamRef.current?.getScreenshot()
screenShot && setCurrentCaptureImage(screenShot)
}

// 再撮影処理
const resetCapture = () => {
setCurrentCaptureImage(null)
setCrop(createCrop(cameraWidth, cameraHeight))
}

// カメラ起動
const handleEnableCamera = () => {
setEnableCamera(true)
setCrop(createCrop(cameraWidth, cameraHeight))
}

// カメラ非表示
const handleDisableCamera = () => {
setEnableCamera(false)
setCurrentCaptureImage(null)
}

// 画像の切り取り & 保存処理
const onSavaCapture = async () => {
// 画像切り取り
const canvas = document.createElement('canvas')
canvas.width = crop.width
canvas.height = crop.height
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
ctx.beginPath()
ctx.rect(0, 0, canvas.width, canvas.height)
ctx.clip()

// 切り取った画像をcanvasに描画
const img = (await new Promise((resolve) => {
const img = new Image()
img.src = currentCaptureImage!
img.onload = () => resolve(img)
})) as HTMLImageElement
ctx.drawImage(img, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height)

// base64に変換
canvas.toBlob(async (result) => {
if (!result) return
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(result)
reader.onloadend = () => {
if (typeof reader.result === 'string') {
resolve(reader.result)
} else reject(new Error('Failed to convert blob to base64 string'))
}
reader.onerror = reject
})
setCaptureImage(base64)
})

resetCapture()
setEnableCamera(false)
}

return (
<article>
{enableCamera ? (
<section className='fixed inset-0 bg-gray-700'>
<div>
<button className='text-white p-3 hover:opacity-80' onClick={handleDisableCamera}>
閉じる
</button>
{currentCaptureImage ? (
<ReactCrop crop={crop} onChange={setCrop}>
<NextImage src={currentCaptureImage} alt='Screenshot' width={0} height={0} className='w-auto h-auto' />
</ReactCrop>
) : (
<div>
<span
className='absolute border-2 rounded-sm border-white'
style={{
width: cameraWidth * 0.5,
height: cameraHeight * 0.5,
marginLeft: cameraWidth * 0.25,
marginTop: cameraHeight * 0.25,
}}
/>
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat='image/jpeg'
screenshotQuality={1}
width={cameraWidth}
/>
</div>
)}
</div>

<div className='absolute mb-20 w-full flex justify-center bottom-0'>
{currentCaptureImage ? (
<div className='flex justify-around w-full'>
<CameraButton onClick={resetCapture} text='再撮影' />
<CameraButton onClick={onSavaCapture} text='登録' />
</div>
) : (
<CameraButton onClick={capture} text='撮影' />
)}
</div>
</section>
) : (
<section className='p-2'>
{captureImage && (
<figure className='mb-2'>
<figcaption>画像プレビュー</figcaption>
<NextImage
src={captureImage}
alt='captureImage'
width={crop.width}
height={crop.height}
className='w-full h-auto'
/>
</figure>
)}
<CameraButton onClick={handleEnableCamera} text='カメラ起動' />
</section>
)}
</article>
)
}

export default CameraPage

検証ページ

こちら