From d8874ff1d1eea250ea482d29a2b74d546b2aa942 Mon Sep 17 00:00:00 2001 From: Karim <127413175+pywolf1503@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:09:57 +0000 Subject: [PATCH 1/4] add "stop watch" hook --- src/hooks/use-stop-watch.tsx | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/hooks/use-stop-watch.tsx 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 From 8cda3329aeb03879ea041a658ed67d08786eed26 Mon Sep 17 00:00:00 2001 From: Karim <127413175+pywolf1503@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:24:34 +0000 Subject: [PATCH 2/4] add canvas recorder component --- src/app/recorder.tsx | 121 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/app/recorder.tsx diff --git a/src/app/recorder.tsx b/src/app/recorder.tsx new file mode 100644 index 0000000..eb958ae --- /dev/null +++ b/src/app/recorder.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { useStopWatch } from '@/hooks/use-stop-watch'; +import { useState, useRef, type JSX } from 'react'; + +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); + console.log('MediaRecorder started successfully'); + return { stop: () => mediaRecorder.stop() }; + } catch (error) { + console.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 = () => { + 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 From 9857a5c987a8b32cd9f2e7ec8b2db5a592a0e2d6 Mon Sep 17 00:00:00 2001 From: Karim <127413175+pywolf1503@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:22:54 +0000 Subject: [PATCH 3/4] use recorder and canvasref --- src/app/recorder.tsx | 5 +++-- src/hero/canvas.tsx | 11 +++++++++-- src/hero/hero.tsx | 38 +++++++++++++++++++++++--------------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/app/recorder.tsx b/src/app/recorder.tsx index eb958ae..079c713 100644 --- a/src/app/recorder.tsx +++ b/src/app/recorder.tsx @@ -2,6 +2,7 @@ import { useStopWatch } from '@/hooks/use-stop-watch'; import { useState, useRef, type JSX } from 'react'; +import { toast } from 'sonner'; type RecordingControls = { stop: () => void; @@ -41,10 +42,9 @@ function record(canvasRef: React.RefObject): Recording }; mediaRecorder.start(1000); - console.log('MediaRecorder started successfully'); return { stop: () => mediaRecorder.stop() }; } catch (error) { - console.error('Recording failed:', error); + toast.error(`Recording failed: ${error}`); return null; } } @@ -60,6 +60,7 @@ function Recorder({ className, canvasRef }: RecorderProps): JSX.Element { const recordingRef = useRef(null); const toggleRecording = () => { + console.log(canvasRef.current); if (!canvasRef.current) return; if (isRecording) { 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..6d2a562 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 && } +
From 955d8bc3bfc4136d89a294c26c0bdf8fdac18e6e Mon Sep 17 00:00:00 2001 From: Karim <127413175+pywolf1503@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:24:06 +0000 Subject: [PATCH 4/4] height tweaks --- src/hero/hero.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hero/hero.tsx b/src/hero/hero.tsx index 6d2a562..1c7fc85 100644 --- a/src/hero/hero.tsx +++ b/src/hero/hero.tsx @@ -192,12 +192,12 @@ export function Hero({ imageId }: HeroProps) { handleFiles(files); }} > -
+
{ switch (state.background) {