Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/adjustments/Basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -140,7 +140,7 @@ const ToneMapperSwitch = ({
);
};

export default function BasicAdjustments({
function BasicAdjustments({
adjustments,
setAdjustments,
isForMask = false,
Expand Down Expand Up @@ -237,3 +237,6 @@ export default function BasicAdjustments({
</div>
);
}

export default memo(BasicAdjustments);

7 changes: 5 additions & 2 deletions src/components/adjustments/Color.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -385,7 +385,7 @@ const ColorCalibrationPanel = ({ adjustments, setAdjustments, onDragStateChange
);
};

export default function ColorPanel({
function ColorPanel({
adjustments,
setAdjustments,
appSettings,
Expand Down Expand Up @@ -553,3 +553,6 @@ export default function ColorPanel({
</div>
);
}

export default memo(ColorPanel);

11 changes: 8 additions & 3 deletions src/components/adjustments/Curves.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -149,7 +149,7 @@ function isDefaultCurve(points: Array<Coord> | undefined) {
return p1.x === 0 && p1.y === 0 && p2.x === 255 && p2.y === 255;
}

export default function CurveGraph({
function CurveGraph({
adjustments,
setAdjustments,
histogram,
Expand Down Expand Up @@ -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 (
<Text
Expand Down Expand Up @@ -536,7 +538,7 @@ export default function CurveGraph({

<line x1="0" y1="255" x2="255" y2="0" stroke="rgba(255,255,255,0.2)" strokeWidth="1" strokeDasharray="2 2" />

<path d={getCurvePath(points)} fill="none" stroke={color} strokeWidth="2.5" />
<path d={curvePath} fill="none" stroke={color} strokeWidth="2.5" />

{points.map((p: Coord, i: number) => (
<circle
Expand All @@ -557,3 +559,6 @@ export default function CurveGraph({
</div>
);
}

export default memo(CurveGraph);

6 changes: 5 additions & 1 deletion src/components/adjustments/Details.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo } from 'react';
import Slider from '../ui/Slider';
import { Adjustments, DetailsAdjustment } from '../../utils/adjustments';
import { AppSettings } from '../ui/AppProperties';
Expand All @@ -12,7 +13,7 @@ interface DetailsPanelProps {
onDragStateChange?: (isDragging: boolean) => void;
}

export default function DetailsPanel({
function DetailsPanel({
adjustments,
setAdjustments,
appSettings,
Expand Down Expand Up @@ -145,3 +146,6 @@ export default function DetailsPanel({
</div>
);
}

export default memo(DetailsPanel);

6 changes: 5 additions & 1 deletion src/components/adjustments/Effects.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,7 +15,7 @@ interface EffectsPanelProps {
onDragStateChange?: (isDragging: boolean) => void;
}

export default function EffectsPanel({
function EffectsPanel({
adjustments,
setAdjustments,
isForMask = false,
Expand Down Expand Up @@ -191,3 +192,6 @@ export default function EffectsPanel({
</div>
);
}

export default memo(EffectsPanel);

91 changes: 54 additions & 37 deletions src/components/panel/editor/Waveform.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -46,55 +46,72 @@ 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 (
<svg
viewBox="0 0 255 255"
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 (
<g key={ch.key} style={{ mixBlendMode: 'lighten' }}>
<path d={getFill(ch.data)} fill={ch.color} fillOpacity={0.4} />
<path
d={getLine(ch.data)}
fill="none"
stroke={ch.color}
strokeWidth={1.5}
strokeOpacity={1.8}
vectorEffect="non-scaling-stroke"
strokeLinejoin="round"
/>
</g>
);
})}
{channels.map((ch) => (
<g key={ch.key} style={{ mixBlendMode: 'lighten' }}>
<path d={ch.fill} fill={ch.color} fillOpacity={0.4} />
<path
d={ch.line}
fill="none"
stroke={ch.color}
strokeWidth={1.5}
strokeOpacity={0.8}
vectorEffect="non-scaling-stroke"
strokeLinejoin="round"
/>
</g>
))}
</svg>
);
};
});

const FakeHistogramLoader = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
Expand Down
26 changes: 25 additions & 1 deletion src/components/ui/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const Slider = ({
snapToStepRef.current = snapToStep;
rangeRef.current = { min, max };

const pendingDispatchRef = useRef<number | null>(null);
const dispatchRafRef = useRef<number | null>(null);

useEffect(() => {
onDragStateChange(isDragging);
}, [isDragging, onDragStateChange]);
Expand Down Expand Up @@ -147,10 +150,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);
};
Expand All @@ -165,6 +185,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]);

Expand Down
11 changes: 10 additions & 1 deletion src/hooks/useHistoryState.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand Down
Loading
Loading