From 8b2e671ea459f46854893339c3ffde892a8246d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Wed, 15 Apr 2026 16:00:10 +0900 Subject: [PATCH 1/8] Reduce system usage during slider drag and idle rendering by batching RAF dispatches, memoizing expensive computations, guarding hidden-tab animation loops, and eliminating unnecessary re-renders and event listener churn. --- src/App.tsx | 2 +- src/components/adjustments/Basic.tsx | 7 +- src/components/adjustments/Color.tsx | 7 +- src/components/adjustments/Curves.tsx | 11 +- src/components/adjustments/Details.tsx | 6 +- src/components/adjustments/Effects.tsx | 6 +- src/components/panel/editor/Waveform.tsx | 93 +++++++------ src/components/ui/Slider.tsx | 26 +++- src/hooks/useHistoryState.tsx | 11 +- src/hooks/useKeyboardShortcuts.tsx | 162 ++++++++--------------- src/hooks/useThumbnails.tsx | 6 + 11 files changed, 178 insertions(+), 159 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2843527a0..1c58840cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1540,7 +1540,7 @@ function App() { isInteractive: dragging, targetResolution: targetRes || null, roi: roi || null, - computeWaveform: !!isWaveformVisible, + computeWaveform: !!isWaveformVisible && !dragging, activeWaveformChannel: activeWaveformChannelRef.current || null, }); diff --git a/src/components/adjustments/Basic.tsx b/src/components/adjustments/Basic.tsx index 94da62967..0c9ff04d1 100644 --- a/src/components/adjustments/Basic.tsx +++ b/src/components/adjustments/Basic.tsx @@ -2,7 +2,7 @@ import { motion } from 'framer-motion'; import clsx from 'clsx'; import Slider from '../ui/Slider'; import { Adjustments, BasicAdjustment } from '../../utils/adjustments'; -import { useEffect, useRef, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; interface BasicAdjustmentsProps { adjustments: Adjustments; @@ -140,7 +140,7 @@ const ToneMapperSwitch = ({ ); }; -export default function BasicAdjustments({ +function BasicAdjustments({ adjustments, setAdjustments, isForMask = false, @@ -237,3 +237,6 @@ export default function BasicAdjustments({ ); } + +export default memo(BasicAdjustments); + diff --git a/src/components/adjustments/Color.tsx b/src/components/adjustments/Color.tsx index ae8733fab..c86abb84f 100644 --- a/src/components/adjustments/Color.tsx +++ b/src/components/adjustments/Color.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { memo, useState } from 'react'; import { Pipette, Sliders } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import Slider from '../ui/Slider'; @@ -385,7 +385,7 @@ const ColorCalibrationPanel = ({ adjustments, setAdjustments, onDragStateChange ); }; -export default function ColorPanel({ +function ColorPanel({ adjustments, setAdjustments, appSettings, @@ -553,3 +553,6 @@ export default function ColorPanel({ ); } + +export default memo(ColorPanel); + diff --git a/src/components/adjustments/Curves.tsx b/src/components/adjustments/Curves.tsx index 0a0ef5514..d46b14d8d 100644 --- a/src/components/adjustments/Curves.tsx +++ b/src/components/adjustments/Curves.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react'; +import { memo, useMemo, useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { RotateCcw, Copy, ClipboardPaste } from 'lucide-react'; import { ActiveChannel, Adjustments, Coord } from '../../utils/adjustments'; @@ -149,7 +149,7 @@ function isDefaultCurve(points: Array | undefined) { return p1.x === 0 && p1.y === 0 && p2.x === 255 && p2.y === 255; } -export default function CurveGraph({ +function CurveGraph({ adjustments, setAdjustments, histogram, @@ -288,6 +288,8 @@ export default function CurveGraph({ const points = localPoints ?? propPoints; const { color, data: histogramData } = channelConfig[activeChannel]; + const curvePath = useMemo(() => (points ? getCurvePath(points) : ''), [points]); + if (!propPoints || !points) { return ( - + {points.map((p: Coord, i: number) => ( ); } + +export default memo(CurveGraph); + diff --git a/src/components/adjustments/Details.tsx b/src/components/adjustments/Details.tsx index 32852cb81..b87b0e84a 100644 --- a/src/components/adjustments/Details.tsx +++ b/src/components/adjustments/Details.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import Slider from '../ui/Slider'; import { Adjustments, DetailsAdjustment } from '../../utils/adjustments'; import { AppSettings } from '../ui/AppProperties'; @@ -12,7 +13,7 @@ interface DetailsPanelProps { onDragStateChange?: (isDragging: boolean) => void; } -export default function DetailsPanel({ +function DetailsPanel({ adjustments, setAdjustments, appSettings, @@ -145,3 +146,6 @@ export default function DetailsPanel({ ); } + +export default memo(DetailsPanel); + diff --git a/src/components/adjustments/Effects.tsx b/src/components/adjustments/Effects.tsx index a1d2dc322..1295265e4 100644 --- a/src/components/adjustments/Effects.tsx +++ b/src/components/adjustments/Effects.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import Slider from '../ui/Slider'; import { Adjustments, Effect, CreativeAdjustment } from '../../utils/adjustments'; import LUTControl from '../ui/LUTControl'; @@ -14,7 +15,7 @@ interface EffectsPanelProps { onDragStateChange?: (isDragging: boolean) => void; } -export default function EffectsPanel({ +function EffectsPanel({ adjustments, setAdjustments, isForMask = false, @@ -187,3 +188,6 @@ export default function EffectsPanel({ ); } + +export default memo(EffectsPanel); + diff --git a/src/components/panel/editor/Waveform.tsx b/src/components/panel/editor/Waveform.tsx index fab02e5c3..27bdb2daf 100644 --- a/src/components/panel/editor/Waveform.tsx +++ b/src/components/panel/editor/Waveform.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import { motion, AnimatePresence, LayoutGroup } from 'framer-motion'; import { AlertOctagon } from 'lucide-react'; import { WaveformData } from '../../ui/AppProperties'; @@ -47,27 +47,31 @@ const modeButtons = [ ]; const HistogramView = ({ histogram }: { histogram: any }) => { - if (!histogram || !histogram.red || !histogram.green || !histogram.blue) return null; + const channels = useMemo(() => { + if (!histogram?.red || !histogram?.green || !histogram?.blue) return null; - const redMax = Math.max(...(histogram.red || [0])); - const greenMax = Math.max(...(histogram.green || [0])); - const blueMax = Math.max(...(histogram.blue || [0])); - const globalMax = Math.max(redMax, greenMax, blueMax, 1); + const redMax = Math.max(...(histogram.red as number[])); + const greenMax = Math.max(...(histogram.green as number[])); + const blueMax = Math.max(...(histogram.blue as number[])); + const globalMax = Math.max(redMax, greenMax, blueMax, 1); - const getFill = (data: number[]) => { - const pathData = data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); - return `M0,255 L${pathData} L255,255 Z`; - }; + const getFill = (data: number[]) => { + const pathData = data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); + return `M0,255 L${pathData} L255,255 Z`; + }; - const getLine = (data: number[]) => { - return 'M' + data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); - }; + const getLine = (data: number[]) => { + return 'M' + data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); + }; + + return [ + { key: 'red', color: '#FF6B6B', fill: getFill(histogram.red), line: getLine(histogram.red) }, + { key: 'green', color: '#6BCB77', fill: getFill(histogram.green), line: getLine(histogram.green) }, + { key: 'blue', color: '#4D96FF', fill: getFill(histogram.blue), line: getLine(histogram.blue) }, + ]; + }, [histogram?.red, histogram?.green, histogram?.blue]); - const channels = [ - { key: 'red', color: '#FF6B6B', data: histogram.red }, - { key: 'green', color: '#6BCB77', data: histogram.green }, - { key: 'blue', color: '#4D96FF', data: histogram.blue }, - ]; + if (!channels) return null; return ( { className="w-full h-full overflow-visible pointer-events-none" preserveAspectRatio="none" > - {channels.map((ch) => { - if (!ch.data || ch.data.length === 0) return null; - return ( - - - - - ); - })} + {channels.map((ch) => ( + + + + + ))} ); }; @@ -110,13 +111,17 @@ const FakeHistogramLoader = () => { let lastTime = 0; const ANIMATION_SPEED = 1.0; + const FRAME_INTERVAL = 1000 / 30; const render = (currentTime: number) => { - if (lastTime === 0) lastTime = currentTime; - - let dt = (currentTime - lastTime) / 1000; + animationFrameId = requestAnimationFrame(render); + if (document.hidden) return; + if (lastTime === 0) lastTime = currentTime - FRAME_INTERVAL; + const elapsed = currentTime - lastTime; + if (elapsed < FRAME_INTERVAL) return; lastTime = currentTime; + let dt = elapsed / 1000; if (dt > 0.05) dt = 0.05; time += dt * ANIMATION_SPEED; @@ -153,8 +158,6 @@ const FakeHistogramLoader = () => { drawChannel('rgba(255, 107, 107, 0.55)', 'rgba(255, 107, 107, 0.3)', 0, 5, 0.8); drawChannel('rgba(107, 203, 119, 0.55)', 'rgba(107, 203, 119, 0.3)', 2, 4, -1.0); drawChannel('rgba(77, 150, 255, 0.55)', 'rgba(77, 150, 255, 0.3)', 4, 6, 0.6); - - animationFrameId = requestAnimationFrame(render); }; animationFrameId = requestAnimationFrame(render); @@ -285,12 +288,17 @@ const FakeWaveformLoader = ({ mode }: { mode: string }) => { } let animationFrameId: number; + const FRAME_INTERVAL = 1000 / 30; const render = (time: number) => { - if (lastTimeRef.current === 0) lastTimeRef.current = time; - let dt = (time - lastTimeRef.current) / 1000; + animationFrameId = requestAnimationFrame(render); + if (document.hidden) return; + if (lastTimeRef.current === 0) lastTimeRef.current = time - FRAME_INTERVAL; + const elapsed = time - lastTimeRef.current; + if (elapsed < FRAME_INTERVAL) return; lastTimeRef.current = time; + let dt = elapsed / 1000; if (dt > 0.05) dt = 0.05; let frameDt = dt; @@ -428,7 +436,6 @@ const FakeWaveformLoader = ({ mode }: { mode: string }) => { } ctx.putImageData(imgData, 0, 0); - animationFrameId = requestAnimationFrame(render); }; animationFrameId = requestAnimationFrame(render); diff --git a/src/components/ui/Slider.tsx b/src/components/ui/Slider.tsx index 184bca13c..5add023f9 100644 --- a/src/components/ui/Slider.tsx +++ b/src/components/ui/Slider.tsx @@ -63,6 +63,9 @@ const Slider = ({ snapToStepRef.current = snapToStep; rangeRef.current = { min, max }; + const pendingDispatchRef = useRef(null); + const dispatchRafRef = useRef(null); + useEffect(() => { onDragStateChange(isDragging); }, [isDragging, onDragStateChange]); @@ -137,10 +140,27 @@ const Slider = ({ const snappedValue = snapToStepRef.current(accumulatedValueRef.current); setDisplayValue(snappedValue); - onChangeRef.current({ target: { value: snappedValue } }); + pendingDispatchRef.current = snappedValue; + if (dispatchRafRef.current === null) { + dispatchRafRef.current = requestAnimationFrame(() => { + if (pendingDispatchRef.current !== null) { + onChangeRef.current({ target: { value: pendingDispatchRef.current } }); + } + dispatchRafRef.current = null; + }); + } }; const handlePointerUp = () => { + // Flush any pending dispatch immediately on release + if (dispatchRafRef.current !== null) { + cancelAnimationFrame(dispatchRafRef.current); + dispatchRafRef.current = null; + } + if (pendingDispatchRef.current !== null) { + onChangeRef.current({ target: { value: pendingDispatchRef.current } }); + pendingDispatchRef.current = null; + } lastUpTime.current = Date.now(); setIsDragging(false); }; @@ -155,6 +175,10 @@ const Slider = ({ window.removeEventListener('mouseup', handlePointerUp); window.removeEventListener('touchmove', handlePointerMove); window.removeEventListener('touchend', handlePointerUp); + if (dispatchRafRef.current !== null) { + cancelAnimationFrame(dispatchRafRef.current); + dispatchRafRef.current = null; + } }; }, [isDragging]); diff --git a/src/hooks/useHistoryState.tsx b/src/hooks/useHistoryState.tsx index e71cd516a..c1485fd34 100644 --- a/src/hooks/useHistoryState.tsx +++ b/src/hooks/useHistoryState.tsx @@ -1,5 +1,14 @@ import { useState, useMemo, useCallback } from 'react'; +function shallowEqual(a: any, b: any): boolean { + if (a === b) return true; + if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false; + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + return keysA.every((k) => a[k] === b[k]); +} + export const useHistoryState = (initialState: any) => { const [history, setHistory] = useState([initialState]); const [index, setIndex] = useState(0); @@ -8,7 +17,7 @@ export const useHistoryState = (initialState: any) => { const setState = useCallback( (newState: any) => { const resolvedState = typeof newState === 'function' ? newState(history[index]) : newState; - if (JSON.stringify(resolvedState) === JSON.stringify(history[index])) { + if (shallowEqual(resolvedState, history[index])) { return; } const newHistory = history.slice(0, index + 1); diff --git a/src/hooks/useKeyboardShortcuts.tsx b/src/hooks/useKeyboardShortcuts.tsx index 606c58204..afe6b3930 100644 --- a/src/hooks/useKeyboardShortcuts.tsx +++ b/src/hooks/useKeyboardShortcuts.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { ImageFile, Panel, SelectedImage } from '../components/ui/AppProperties'; import { BrushSettings } from '../components/ui/AppProperties'; @@ -55,60 +55,64 @@ interface KeyboardShortcutsProps { setBrushSettings: (settings: BrushSettings) => void; } -export const useKeyboardShortcuts = ({ - activeAiPatchContainerId, - activeAiSubMaskId, - activeMaskContainerId, - activeMaskId, - activeRightPanel, - osPlatform, - canRedo, - canUndo, - copiedFilePaths, - customEscapeHandler, - handleBackToLibrary, - handleCopyAdjustments, - handleDeleteAiPatch, - handleDeleteMaskContainer, - handleDeleteSelected, - handleImageSelect, - handlePasteAdjustments, - handlePasteFiles, - handleRate, - handleRightPanelSelect, - handleRotate, - handleSetColorLabel, - handleToggleFullScreen, - handleZoomChange, - isFullScreen, - isModalOpen, - isStraightenActive, - isViewLoading, - libraryActivePath, - multiSelectedPaths, - onSelectPatchContainer, - redo, - selectedImage, - setActiveAiSubMaskId, - setActiveMaskContainerId, - setActiveMaskId, - setCopiedFilePaths, - setIsStraightenActive, - setIsWaveformVisible, - setLibraryActivePath, - setMultiSelectedPaths, - setShowOriginal, - sortedImageList, - undo, - zoom, - displaySize, - baseRenderSize, - originalSize, - brushSettings, - setBrushSettings, -}: KeyboardShortcutsProps) => { +export const useKeyboardShortcuts = (props: KeyboardShortcutsProps) => { + const propsRef = useRef(props); + propsRef.current = props; + useEffect(() => { const handleKeyDown = (event: any) => { + const { + activeAiPatchContainerId, + activeAiSubMaskId, + activeMaskContainerId, + activeMaskId, + activeRightPanel, + osPlatform, + canRedo, + canUndo, + copiedFilePaths, + customEscapeHandler, + handleBackToLibrary, + handleCopyAdjustments, + handleDeleteAiPatch, + handleDeleteMaskContainer, + handleDeleteSelected, + handleImageSelect, + handlePasteAdjustments, + handlePasteFiles, + handleRate, + handleRightPanelSelect, + handleRotate, + handleSetColorLabel, + handleToggleFullScreen, + handleZoomChange, + isFullScreen, + isModalOpen, + isStraightenActive, + isViewLoading, + libraryActivePath, + multiSelectedPaths, + onSelectPatchContainer, + redo, + selectedImage, + setActiveAiSubMaskId, + setActiveMaskContainerId, + setActiveMaskId, + setCopiedFilePaths, + setIsStraightenActive, + setIsWaveformVisible, + setLibraryActivePath, + setMultiSelectedPaths, + setShowOriginal, + sortedImageList, + undo, + zoom, + displaySize, + baseRenderSize, + originalSize, + brushSettings, + setBrushSettings, + } = propsRef.current; if (isModalOpen) { return; } @@ -454,55 +458,5 @@ export const useKeyboardShortcuts = ({ return () => { window.removeEventListener('keydown', handleKeyDown); }; - }, [ - activeAiPatchContainerId, - activeAiSubMaskId, - activeMaskContainerId, - activeMaskId, - activeRightPanel, - osPlatform, - canRedo, - canUndo, - copiedFilePaths, - customEscapeHandler, - handleBackToLibrary, - handleCopyAdjustments, - handleDeleteAiPatch, - handleDeleteMaskContainer, - handleDeleteSelected, - handleImageSelect, - handlePasteAdjustments, - handlePasteFiles, - handleRate, - handleRightPanelSelect, - handleRotate, - handleSetColorLabel, - handleToggleFullScreen, - handleZoomChange, - isFullScreen, - isStraightenActive, - isViewLoading, - libraryActivePath, - multiSelectedPaths, - onSelectPatchContainer, - redo, - selectedImage, - setActiveAiSubMaskId, - setActiveMaskContainerId, - setActiveMaskId, - setCopiedFilePaths, - setIsStraightenActive, - setIsWaveformVisible, - setLibraryActivePath, - setMultiSelectedPaths, - setShowOriginal, - sortedImageList, - undo, - zoom, - displaySize, - baseRenderSize, - originalSize, - brushSettings, - setBrushSettings, - ]); + }, []); }; diff --git a/src/hooks/useThumbnails.tsx b/src/hooks/useThumbnails.tsx index 2aa3b7f8d..71ff1d283 100644 --- a/src/hooks/useThumbnails.tsx +++ b/src/hooks/useThumbnails.tsx @@ -12,6 +12,12 @@ export function useThumbnails() { if (!processorRef.current) { processorRef.current = setInterval(() => { + if (visiblePathsRef.current.size === 0) { + clearInterval(processorRef.current!); + processorRef.current = null; + return; + } + const pathsToRequest = Array.from(visiblePathsRef.current).filter((p) => !requestedPathsRef.current.has(p)); if (pathsToRequest.length > 0) { From 379baa413528089f34655b89458dc85b63a1e5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Wed, 15 Apr 2026 16:02:38 +0900 Subject: [PATCH 2/8] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df6e5b964..99bd8cc29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: build +name: build on: workflow_call: From 75542c68e5e32fee60a46c85443f71461c27128d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Wed, 15 Apr 2026 16:02:45 +0900 Subject: [PATCH 3/8] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99bd8cc29..df6e5b964 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: build +name: build on: workflow_call: From f526fe8b5851f4bca7efe861eeb1be568ca01b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Wed, 15 Apr 2026 16:46:00 +0900 Subject: [PATCH 4/8] update --- src/components/panel/editor/Waveform.tsx | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/components/panel/editor/Waveform.tsx b/src/components/panel/editor/Waveform.tsx index 27bdb2daf..91f77402e 100644 --- a/src/components/panel/editor/Waveform.tsx +++ b/src/components/panel/editor/Waveform.tsx @@ -111,17 +111,13 @@ const FakeHistogramLoader = () => { let lastTime = 0; const ANIMATION_SPEED = 1.0; - const FRAME_INTERVAL = 1000 / 30; const render = (currentTime: number) => { - animationFrameId = requestAnimationFrame(render); - if (document.hidden) return; - if (lastTime === 0) lastTime = currentTime - FRAME_INTERVAL; - const elapsed = currentTime - lastTime; - if (elapsed < FRAME_INTERVAL) return; + if (lastTime === 0) lastTime = currentTime; + + let dt = (currentTime - lastTime) / 1000; lastTime = currentTime; - let dt = elapsed / 1000; if (dt > 0.05) dt = 0.05; time += dt * ANIMATION_SPEED; @@ -158,6 +154,8 @@ const FakeHistogramLoader = () => { drawChannel('rgba(255, 107, 107, 0.55)', 'rgba(255, 107, 107, 0.3)', 0, 5, 0.8); drawChannel('rgba(107, 203, 119, 0.55)', 'rgba(107, 203, 119, 0.3)', 2, 4, -1.0); drawChannel('rgba(77, 150, 255, 0.55)', 'rgba(77, 150, 255, 0.3)', 4, 6, 0.6); + + animationFrameId = requestAnimationFrame(render); }; animationFrameId = requestAnimationFrame(render); @@ -288,17 +286,12 @@ const FakeWaveformLoader = ({ mode }: { mode: string }) => { } let animationFrameId: number; - const FRAME_INTERVAL = 1000 / 30; const render = (time: number) => { - animationFrameId = requestAnimationFrame(render); - if (document.hidden) return; - if (lastTimeRef.current === 0) lastTimeRef.current = time - FRAME_INTERVAL; - const elapsed = time - lastTimeRef.current; - if (elapsed < FRAME_INTERVAL) return; + if (lastTimeRef.current === 0) lastTimeRef.current = time; + let dt = (time - lastTimeRef.current) / 1000; lastTimeRef.current = time; - let dt = elapsed / 1000; if (dt > 0.05) dt = 0.05; let frameDt = dt; @@ -436,6 +429,7 @@ const FakeWaveformLoader = ({ mode }: { mode: string }) => { } ctx.putImageData(imgData, 0, 0); + animationFrameId = requestAnimationFrame(render); }; animationFrameId = requestAnimationFrame(render); From 02e6cddd11169d49882f1464da5674000e21ec61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Thu, 16 Apr 2026 01:47:31 +0900 Subject: [PATCH 5/8] Update Waveform.tsx --- src/components/panel/editor/Waveform.tsx | 71 ++++++++++++------------ 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/src/components/panel/editor/Waveform.tsx b/src/components/panel/editor/Waveform.tsx index 91f77402e..fab02e5c3 100644 --- a/src/components/panel/editor/Waveform.tsx +++ b/src/components/panel/editor/Waveform.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence, LayoutGroup } from 'framer-motion'; import { AlertOctagon } from 'lucide-react'; import { WaveformData } from '../../ui/AppProperties'; @@ -47,31 +47,27 @@ const modeButtons = [ ]; const HistogramView = ({ histogram }: { histogram: any }) => { - const channels = useMemo(() => { - if (!histogram?.red || !histogram?.green || !histogram?.blue) return null; + if (!histogram || !histogram.red || !histogram.green || !histogram.blue) return null; - const redMax = Math.max(...(histogram.red as number[])); - const greenMax = Math.max(...(histogram.green as number[])); - const blueMax = Math.max(...(histogram.blue as number[])); - const globalMax = Math.max(redMax, greenMax, blueMax, 1); + const redMax = Math.max(...(histogram.red || [0])); + const greenMax = Math.max(...(histogram.green || [0])); + const blueMax = Math.max(...(histogram.blue || [0])); + const globalMax = Math.max(redMax, greenMax, blueMax, 1); - const getFill = (data: number[]) => { - const pathData = data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); - return `M0,255 L${pathData} L255,255 Z`; - }; - - const getLine = (data: number[]) => { - return 'M' + data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); - }; + const getFill = (data: number[]) => { + const pathData = data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); + return `M0,255 L${pathData} L255,255 Z`; + }; - return [ - { key: 'red', color: '#FF6B6B', fill: getFill(histogram.red), line: getLine(histogram.red) }, - { key: 'green', color: '#6BCB77', fill: getFill(histogram.green), line: getLine(histogram.green) }, - { key: 'blue', color: '#4D96FF', fill: getFill(histogram.blue), line: getLine(histogram.blue) }, - ]; - }, [histogram?.red, histogram?.green, histogram?.blue]); + const getLine = (data: number[]) => { + return 'M' + data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); + }; - if (!channels) return null; + const channels = [ + { key: 'red', color: '#FF6B6B', data: histogram.red }, + { key: 'green', color: '#6BCB77', data: histogram.green }, + { key: 'blue', color: '#4D96FF', data: histogram.blue }, + ]; return ( { className="w-full h-full overflow-visible pointer-events-none" preserveAspectRatio="none" > - {channels.map((ch) => ( - - - - - ))} + {channels.map((ch) => { + if (!ch.data || ch.data.length === 0) return null; + return ( + + + + + ); + })} ); }; From b5ee6ad2db2521ec7d37d0f29d8fe0660bcab554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Thu, 16 Apr 2026 02:03:09 +0900 Subject: [PATCH 6/8] update --- .claude/settings.local.json | 8 +++ src/components/panel/editor/Waveform.tsx | 91 ++++++++++++++---------- 2 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..2eb0fdc1b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(grep -v \"//.*as \")", + "Bash(grep -v \"^.*//.*as \")" + ] + } +} diff --git a/src/components/panel/editor/Waveform.tsx b/src/components/panel/editor/Waveform.tsx index fab02e5c3..4dc73a1a0 100644 --- a/src/components/panel/editor/Waveform.tsx +++ b/src/components/panel/editor/Waveform.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo, memo } from 'react'; import { motion, AnimatePresence, LayoutGroup } from 'framer-motion'; import { AlertOctagon } from 'lucide-react'; import { WaveformData } from '../../ui/AppProperties'; @@ -46,28 +46,48 @@ const modeButtons = [ }, ]; -const HistogramView = ({ histogram }: { histogram: any }) => { - if (!histogram || !histogram.red || !histogram.green || !histogram.blue) return null; +const HistogramView = memo(({ histogram }: { histogram: any }) => { + const channels = useMemo(() => { + if (!histogram?.red || !histogram?.green || !histogram?.blue) return null; - const redMax = Math.max(...(histogram.red || [0])); - const greenMax = Math.max(...(histogram.green || [0])); - const blueMax = Math.max(...(histogram.blue || [0])); - const globalMax = Math.max(redMax, greenMax, blueMax, 1); + const getSafeMax = (arr: number[]) => { + let max = 0; + for (let i = 0; i < arr.length; i++) { + if (arr[i] > max) max = arr[i]; + } + return max || 1; + }; - const getFill = (data: number[]) => { - const pathData = data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); - return `M0,255 L${pathData} L255,255 Z`; - }; + const redMax = getSafeMax(histogram.red); + const greenMax = getSafeMax(histogram.green); + const blueMax = getSafeMax(histogram.blue); + const globalMax = Math.max(redMax, greenMax, blueMax); - const getLine = (data: number[]) => { - return 'M' + data.map((val, i) => `${(i / 255) * 255},${255 - (val / globalMax) * 255}`).join(' L'); - }; + const generatePaths = (data: number[]) => { + if (!data.length) return { fill: '', line: '' }; - const channels = [ - { key: 'red', color: '#FF6B6B', data: histogram.red }, - { key: 'green', color: '#6BCB77', data: histogram.green }, - { key: 'blue', color: '#4D96FF', data: histogram.blue }, - ]; + let pathPoints = ''; + const len = data.length; + for (let i = 0; i < len; i++) { + const x = (i / (len - 1)) * 255; + const y = 255 - (data[i] / globalMax) * 255; + pathPoints += (i === 0 ? '' : ' L') + `${x},${y}`; + } + + return { + fill: `M0,255 L${pathPoints} L255,255 Z`, + line: `M${pathPoints}`, + }; + }; + + return [ + { key: 'red', color: '#FF6B6B', ...generatePaths(histogram.red) }, + { key: 'green', color: '#6BCB77', ...generatePaths(histogram.green) }, + { key: 'blue', color: '#4D96FF', ...generatePaths(histogram.blue) }, + ]; + }, [histogram]); + + if (!channels) return null; return ( { className="w-full h-full overflow-visible pointer-events-none" preserveAspectRatio="none" > - {channels.map((ch) => { - if (!ch.data || ch.data.length === 0) return null; - return ( - - - - - ); - })} + {channels.map((ch) => ( + + + + + ))} ); -}; +}); const FakeHistogramLoader = () => { const canvasRef = useRef(null); From d2805e7c0f4c7ac30c072a248e1838de970974bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Thu, 16 Apr 2026 02:07:10 +0900 Subject: [PATCH 7/8] Delete .claude directory --- .claude/settings.local.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 2eb0fdc1b..000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(grep -v \"//.*as \")", - "Bash(grep -v \"^.*//.*as \")" - ] - } -} From 5b19df57e4f51b664922d82494f15f42abb93f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=ED=93=A8=EB=A6=AC=F0=9F=90=BE?= Date: Thu, 16 Apr 2026 13:51:21 +0900 Subject: [PATCH 8/8] Update App.tsx --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index be444567b..ab1dcc547 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1540,7 +1540,7 @@ function App() { isInteractive: dragging, targetResolution: targetRes || null, roi: roi || null, - computeWaveform: !!isWaveformVisible && !dragging, + computeWaveform: !!isWaveformVisible, activeWaveformChannel: activeWaveformChannelRef.current || null, });