diff --git a/src/app/recorder.tsx b/src/app/recorder.tsx new file mode 100644 index 0000000..079c713 --- /dev/null +++ b/src/app/recorder.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { useStopWatch } from '@/hooks/use-stop-watch'; +import { useState, useRef, type JSX } from 'react'; +import { toast } from 'sonner'; + +type RecordingControls = { + stop: () => void; +}; + +function record(canvasRef: React.RefObject): RecordingControls | null { + const canvas = canvasRef.current; + console.log('Attempting to record canvas:', canvas); + if (!canvas) { + console.warn('No canvas element found'); + return null; + } + + try { + console.log('Capturing stream from canvas'); + const stream = canvas.captureStream(30); + console.log('Stream obtained:', stream); + const mediaRecorder = new MediaRecorder(stream, { + mimeType: 'video/webm;codecs=vp9', + videoBitsPerSecond: 2_500_000 + }); + + const chunks: Blob[] = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) chunks.push(event.data); + }; + + mediaRecorder.onstop = () => { + const blob = new Blob(chunks, { type: 'video/webm' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `recording-${Date.now()}.webm`; + a.click(); + URL.revokeObjectURL(url); + }; + + mediaRecorder.start(1000); + return { stop: () => mediaRecorder.stop() }; + } catch (error) { + toast.error(`Recording failed: ${error}`); + return null; + } +} + +interface RecorderProps { + className?: string; + canvasRef: React.RefObject; +} + +function Recorder({ className, canvasRef }: RecorderProps): JSX.Element { + const [isRecording, setIsRecording] = useState(false); + const { timeElapsed, startStopWatch, stopStopWatch } = useStopWatch(); + const recordingRef = useRef(null); + + const toggleRecording = () => { + console.log(canvasRef.current); + if (!canvasRef.current) return; + + if (isRecording) { + stopStopWatch(); + recordingRef.current?.stop(); + } else { + startStopWatch(); + recordingRef.current = record(canvasRef); + } + setIsRecording(!isRecording); + }; + + const formatTime = (seconds: number): string => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes.toString().padStart(2, '0')}:${remainingSeconds + .toString() + .padStart(2, '0')}`; + }; + + return ( +
+
+ {formatTime(timeElapsed)} +
+ +
+ ); +} + +function RecordButton({ + isRecording, + toggleRecording, +}: { + isRecording: boolean; + toggleRecording: () => void; +}): JSX.Element { + return ( + + ); +} + +export default Recorder; \ No newline at end of file diff --git a/src/hero/canvas.tsx b/src/hero/canvas.tsx index 1b25034..efc7668 100644 --- a/src/hero/canvas.tsx +++ b/src/hero/canvas.tsx @@ -1,9 +1,8 @@ 'use client'; import { liquidFragSource } from '@/app/hero/liquid-frag'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type Ref, type RefObject} from 'react'; import { toast } from 'sonner'; - // uniform sampler2D u_image_texture; // uniform float u_time; // uniform float u_ratio; @@ -38,10 +37,12 @@ export function Canvas({ imageData, params, processing, + ref }: { imageData: ImageData; params: ShaderParams; processing: boolean; + ref: RefObject }) { const canvasRef = useRef(null); const [gl, setGl] = useState(null); @@ -251,5 +252,11 @@ export function Canvas({ }; }, [gl, uniforms, imageData]); + useEffect(() => { + if(ref && canvasRef.current){ + ref.current = canvasRef.current; + } + },[canvasRef,ref]) + return ; } diff --git a/src/hero/hero.tsx b/src/hero/hero.tsx index 6f11e34..1c7fc85 100644 --- a/src/hero/hero.tsx +++ b/src/hero/hero.tsx @@ -11,6 +11,7 @@ import { toast } from 'sonner'; import { parseLogoImage } from './parse-logo-image'; import { uploadImage } from '@/hero/upload-image'; import isEqual from 'lodash-es/isEqual'; +import Recorder from '@/app/recorder'; interface HeroProps { imageId: string; @@ -35,8 +36,10 @@ export function Hero({ imageId }: HeroProps) { const [imageData, setImageData] = useState(null); const [processing, setProcessing] = useState(true); + const canvasRef = useRef(null); // Check URL for image ID on mount useEffect(() => { + console.log(canvasRef); setProcessing(true); async function updateImageData() { @@ -61,7 +64,7 @@ export function Hero({ imageId }: HeroProps) { } updateImageData(); - }, [imageId]); + }, [imageId, canvasRef]); useEffect(() => { stateRef.current = state; @@ -189,20 +192,25 @@ export function Hero({ imageId }: HeroProps) { handleFiles(files); }} > -
{ - switch (state.background) { - case 'metal': - return 'linear-gradient(to bottom, #eee, #b8b8b8)'; - } - return state.background; - })(), - }} - > -
- {imageData && } +
+
+ +
+
{ + switch (state.background) { + case 'metal': + return 'linear-gradient(to bottom, #eee, #b8b8b8)'; + } + return state.background; + })(), + }} + > +
+ {imageData && } +
diff --git a/src/hooks/use-stop-watch.tsx b/src/hooks/use-stop-watch.tsx new file mode 100644 index 0000000..f8e6b2c --- /dev/null +++ b/src/hooks/use-stop-watch.tsx @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +interface UseStopWatchResult { + timeElapsed: number; + isRunning: boolean; + startStopWatch: () => void; + stopStopWatch: () => void; +} + +export function useStopWatch() { + const [timeElapsed, setTimeElapsed] = useState(0); + const [isRunning, setIsRunning] = useState(false); + const rafRef = useRef(null); + const startTimeRef = useRef(null); + + const updateTime = useCallback(() => { + if(startTimeRef.current !== null) { + const elapsedMS = Date.now() - startTimeRef.current; + setTimeElapsed(Math.floor(elapsedMS / 1000)); + } + setTimeElapsed((prev) => prev + 1); + rafRef.current = requestAnimationFrame(updateTime); + },[]) + + const startStopWatch = useCallback(() => { + if (typeof window === "undefined") return; + if(!isRunning) { + setIsRunning(true); + rafRef.current = requestAnimationFrame(updateTime); + startTimeRef.current = Date.now(); + } + }, [isRunning, updateTime]) + + const stopStopWatch = useCallback(() => { + if(rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + setIsRunning(false); + } + }, []); + + useEffect(() => { + return () => { + if(rafRef.current !== null){ + cancelAnimationFrame(rafRef.current); + } + } + },[]) + + return {timeElapsed, isRunning,startStopWatch, stopStopWatch} +} \ No newline at end of file