Extract canvas blur to separate component

This commit is contained in:
Sam Becker 2023-09-22 13:41:28 -05:00
parent 93a1571128
commit 2fc2fdf5ba
2 changed files with 92 additions and 56 deletions

View File

@ -0,0 +1,76 @@
'use client';
import { useEffect, useRef } from 'react';
export default function CanvasBlurCapture({
imageUrl,
onCapture,
width,
height,
hidden = true,
edgeCompensation = 10,
scale = 0.5,
quality = 0.9,
}: {
imageUrl: string
onCapture: (blurData: string) => void
width: number
height: number
hidden?: boolean
edgeCompensation?: number
scale?: number
quality?: number
}) {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let timeout: NodeJS.Timeout;
const image = new Image();
image.crossOrigin = 'anonymous';
image.src = imageUrl;
image.onload = () => {
timeout = setTimeout(() => {
const canvas = ref.current;
if (canvas) {
canvas.width = width * scale;
canvas.height = height * scale;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const context = ref.current?.getContext('2d');
if (context) {
context.scale(scale, scale);
context.filter =
'contrast(1.2) saturate(1.2)' +
`blur(${scale * 10}px)`;
context.drawImage(
image,
-edgeCompensation,
-edgeCompensation,
width + edgeCompensation * 2,
width * image.height / image.width
+ edgeCompensation * 2,
);
onCapture(canvas.toDataURL('image/jpeg', quality));
}
} else {
console.error('Cannot generate blur data: canvas not found');
}
}, 2000);
};
return () => clearTimeout(timeout);
}, [
imageUrl,
onCapture,
width,
height,
edgeCompensation,
scale,
quality,
]);
return (
<canvas ref={ref} className={hidden ? 'hidden' : undefined} />
);
}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useCallback, useState } from 'react';
import { import {
FORM_METADATA_ENTRIES, FORM_METADATA_ENTRIES,
PhotoFormData, PhotoFormData,
@ -11,12 +11,10 @@ import { createPhotoAction, updatePhotoAction } from './actions';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import Link from 'next/link'; import Link from 'next/link';
import { cc } from '@/utility/css'; import { cc } from '@/utility/css';
import CanvasBlurCapture from '@/components/CanvasBlurCapture';
const THUMBNAIL_WIDTH = 300; const THUMBNAIL_WIDTH = 300;
const THUMBNAIL_HEIGHT = 200; const THUMBNAIL_HEIGHT = 200;
const EDGE_BLUR_COMPENSATION = 10;
const BLUR_SCALE = 0.5;
const BLUE_JPEG_QUALITY = 0.9;
export default function PhotoForm({ export default function PhotoForm({
initialPhotoForm, initialPhotoForm,
@ -30,55 +28,16 @@ export default function PhotoForm({
const [formData, setFormData] = const [formData, setFormData] =
useState<Partial<PhotoFormData>>(initialPhotoForm); useState<Partial<PhotoFormData>>(initialPhotoForm);
const [showBlur, setShowBlur] = useState(debugBlur);
const canvasRef = useRef<HTMLCanvasElement>(null);
const url = formData.url ?? ''; const url = formData.url ?? '';
useEffect(() => { const updateBlurData = useCallback((blurData: string) => {
let timeout: NodeJS.Timeout;
const image = new Image();
image.crossOrigin = 'anonymous';
image.src = url;
image.onload = () => {
timeout = setTimeout(() => {
const canvas = canvasRef.current;
if (canvas) {
canvas.width = THUMBNAIL_WIDTH * BLUR_SCALE;
canvas.height = THUMBNAIL_HEIGHT * BLUR_SCALE;
canvas.style.width = `${THUMBNAIL_WIDTH}px`;
canvas.style.height = `${THUMBNAIL_HEIGHT}px`;
const context = canvasRef.current?.getContext('2d');
if (context) {
context.scale(BLUR_SCALE, BLUR_SCALE);
context.filter =
'contrast(1.2) saturate(1.2)' +
`blur(${BLUR_SCALE * 10}px)`;
context.drawImage(
image,
-EDGE_BLUR_COMPENSATION,
-EDGE_BLUR_COMPENSATION,
THUMBNAIL_WIDTH + EDGE_BLUR_COMPENSATION * 2,
THUMBNAIL_WIDTH * image.height / image.width
+ EDGE_BLUR_COMPENSATION * 2,
);
if (type === 'create') { if (type === 'create') {
setFormData(data => ({ setFormData(data => ({
...data, ...data,
blurData: canvas.toDataURL('image/jpeg', BLUE_JPEG_QUALITY), blurData,
})); }));
} }
} }, [type]);
} else {
console.error('Cannot generate blur data: canvas not found');
}
}, 2000);
};
return () => clearTimeout(timeout);
}, [url, type]);
const isFormValid = FORM_METADATA_ENTRIES.every(([key, { required }]) => const isFormValid = FORM_METADATA_ENTRIES.every(([key, { required }]) =>
!required || Boolean(formData[key])); !required || Boolean(formData[key]));
@ -96,12 +55,13 @@ export default function PhotoForm({
height={THUMBNAIL_HEIGHT} height={THUMBNAIL_HEIGHT}
priority priority
/> />
<canvas <CanvasBlurCapture
ref={canvasRef} imageUrl={url}
className="hidden" width={THUMBNAIL_WIDTH}
onClick={() => setShowBlur(!showBlur)} height={THUMBNAIL_HEIGHT}
onCapture={updateBlurData}
/> />
{showBlur && formData.blurData && {debugBlur && formData.blurData &&
<img <img
alt="blur" alt="blur"
src={formData.blurData} src={formData.blurData}