src/components/CameraView.tsx 2.4 K raw
1
import { useRef, useMemo, useEffect, useState } from "react";
2
import { useCamera } from "../hooks/useCamera";
3
import { useAnimationLoop } from "../hooks/useAnimationLoop";
4
import { buildCSSFilter, buildLUT } from "../lib/filters";
5
import type { FilterState } from "../lib/types";
6
7
interface CameraViewProps {
8
  filterState: FilterState;
9
}
10
11
export function CameraView({ filterState }: CameraViewProps) {
12
  const { videoRef, error, isReady } = useCamera();
13
  const canvasRef = useRef<HTMLCanvasElement>(null);
14
  const [videoDimensions, setVideoDimensions] = useState({ w: 1280, h: 720 });
15
16
  useEffect(() => {
17
    const video = videoRef.current;
18
    if (!video) return;
19
20
    function onMeta() {
21
      setVideoDimensions({ w: video!.videoWidth, h: video!.videoHeight });
22
    }
23
    video.addEventListener("loadedmetadata", onMeta);
24
    return () => video.removeEventListener("loadedmetadata", onMeta);
25
  }, [videoRef]);
26
27
  const cssFilter = useMemo(() => buildCSSFilter(filterState.basic), [filterState.basic]);
28
  const lut = useMemo(
29
    () => buildLUT(filterState.basic, filterState.curves),
30
    [filterState.basic, filterState.curves],
31
  );
32
33
  useAnimationLoop(() => {
34
    const video = videoRef.current;
35
    const canvas = canvasRef.current;
36
    if (!video || !canvas || !isReady || video.readyState < 2) return;
37
38
    const ctx = canvas.getContext("2d", { willReadFrequently: !lut.isIdentity });
39
    if (!ctx) return;
40
41
    canvas.width = videoDimensions.w;
42
    canvas.height = videoDimensions.h;
43
44
    ctx.filter = cssFilter;
45
    ctx.drawImage(video, 0, 0, videoDimensions.w, videoDimensions.h);
46
47
    if (!lut.isIdentity) {
48
      ctx.filter = "none";
49
      const imageData = ctx.getImageData(0, 0, videoDimensions.w, videoDimensions.h);
50
      const d = imageData.data;
51
      const lr = lut.r;
52
      const lg = lut.g;
53
      const lb = lut.b;
54
      for (let i = 0; i < d.length; i += 4) {
55
        d[i] = lr[d[i]];
56
        d[i + 1] = lg[d[i + 1]];
57
        d[i + 2] = lb[d[i + 2]];
58
      }
59
      ctx.putImageData(imageData, 0, 0);
60
    }
61
  });
62
63
  if (error) {
64
    return (
65
      <div className="camera-error">
66
        <p>{error}</p>
67
      </div>
68
    );
69
  }
70
71
  return (
72
    <div className="camera-view">
73
      <video
74
        ref={videoRef}
75
        autoPlay
76
        playsInline
77
        muted
78
        style={{ position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" }}
79
      />
80
      <canvas ref={canvasRef} className="camera-canvas" />
81
    </div>
82
  );
83
}