Skip to content

Commit

Permalink
Colorizer (v0.0.1-alpha.5) (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
satelllte authored Nov 16, 2023
1 parent a1777ae commit f4fabbc
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 73 deletions.
Binary file modified bun.lockb
Binary file not shown.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "displacementx",
"version": "0.0.1-alpha.4",
"version": "0.0.1-alpha.5",
"scripts": {
"dev": "next dev",
"build": "next build",
Expand All @@ -18,7 +18,9 @@
"geist": "1.0.0",
"next": "14.0.1",
"react": "18.2.0",
"react-colorful": "5.6.1",
"react-dom": "18.2.0",
"usehooks-ts": "2.9.1",
"zustand": "4.4.6"
},
"devDependencies": {
Expand Down
107 changes: 61 additions & 46 deletions src/components/pages/Generator/CanvasSection/CanvasSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,36 @@ import {SectionTitle} from '../SectionTitle';
import {saveImage} from './utils/saveImage';
import {draw} from './utils/draw';
import {Switch} from '@/components/ui/Switch';
import {Gradient} from './Gradient';
import {getCtx2dFromRef} from './utils/getCtx2dFromRef';
import {getCanvasDimensions} from './utils/getCanvasDimensions';
import {clearCanvas} from './utils/clearCanvas';
import {drawNormal} from './utils/drawNormal';
import {drawColor} from './utils/drawColor';
import {drawInvert} from './utils/drawInvert';

type PreviewType = 'original' | 'normal' | 'color';

export function CanvasSection() {
const [is8k, setIs8k] = useState<boolean>(false);
const width = is8k ? 8192 : 4096;
const height = is8k ? 8192 : 4096;

const [isPristine, setIsPristine] = useState<boolean>(true);
const [isRendering, setIsRendering] = useState<boolean>(false);
const [isNormalPreview, setIsNormalPreview] = useState<boolean>(false);
const [previewType, setPreviewType] = useState<PreviewType>('original');
const [renderTimeMs, setRenderTimeMs] = useState<number | undefined>();

const canvasRef = useRef<HTMLCanvasElement>(null);
const canvasOriginalPreviewDataUrl = useRef<string | undefined>(undefined);
const gradientCanvasRef = useRef<HTMLCanvasElement>(null);

const render = () => {
setIsPristine(false);
setIsRendering(true);
setIsNormalPreview(false);
setPreviewType('original');

const ctx2d = getCtx2d(canvasRef);
const ctx2d = getCtx2dFromRef(canvasRef);

const {
iterations,
Expand Down Expand Up @@ -134,46 +140,56 @@ export function CanvasSection() {
};

const onIs8kChange = (is8k: boolean) => {
const ctx2d = getCtx2d(canvasRef);
const ctx2d = getCtx2dFromRef(canvasRef);

clearCanvas(ctx2d);

setIsPristine(true);
setIsNormalPreview(false);
setPreviewType('original');
setRenderTimeMs(undefined);
setIs8k(is8k);
};

const invert = () => {
const quickRender = (callback: () => void) => {
const renderTimeStartMs: number = performance.now();
setIsRendering(true);
setIsNormalPreview(false);

const updateCanvas = () => {
const ctx2d = getCtx2d(canvasRef);
drawInvert(ctx2d);
};

// Put a small timeout to allow the UI to update before canvas takes the main thread over
setTimeout(() => {
updateCanvas();
callback();
setIsRendering(false);
setRenderTimeMs(performance.now() - renderTimeStartMs);
}, 20);
};

const toggleNormalPreview = () => {
const isNormalPreviewNew = !isNormalPreview;
const renderTimeStartMs: number = performance.now();
setIsRendering(true);
const invert = () => {
quickRender(() => {
const ctx2d = getCtx2dFromRef(canvasRef);
drawInvert(ctx2d);
});
};

const updateCanvas = () => {
const ctx2d = getCtx2d(canvasRef);
const togglePreviewFor = (type: PreviewType) => () => {
quickRender(() => {
const shouldDrawNonOriginal = previewType === 'original';

if (isNormalPreviewNew) {
// Draw normal preview
const ctx2d = getCtx2dFromRef(canvasRef);
const ctx2dGradient = getCtx2dFromRef(gradientCanvasRef);

if (shouldDrawNonOriginal) {
// Save original preview
canvasOriginalPreviewDataUrl.current = ctx2d.canvas.toDataURL();
drawNormal(ctx2d);
// Draw preview based on type
switch (type) {
case 'normal':
drawNormal(ctx2d);
break;
case 'color':
drawColor({ctx2d, ctx2dGradient});
break;
default:
break;
}
} else {
// Restore original preview
const dataUrl = canvasOriginalPreviewDataUrl.current;
Expand All @@ -188,15 +204,9 @@ export function CanvasSection() {
};
}
}
};

// Put a small timeout to allow the UI to update before canvas takes the main thread over
setTimeout(() => {
updateCanvas();
setIsNormalPreview(isNormalPreviewNew);
setIsRendering(false);
setRenderTimeMs(performance.now() - renderTimeStartMs);
}, 20);
setPreviewType(shouldDrawNonOriginal ? type : 'original');
});
};

return (
Expand Down Expand Up @@ -231,18 +241,35 @@ export function CanvasSection() {
</div>
<div className='flex flex-wrap gap-1 pt-2'>
<Button
disabled={isPristine || isRendering || isNormalPreview}
disabled={isPristine || isRendering || previewType !== 'original'}
onClick={invert}
>
Invert
</Button>
<Button
disabled={isPristine || isRendering}
onClick={toggleNormalPreview}
disabled={
isPristine ||
isRendering ||
(previewType !== 'normal' && previewType !== 'original')
}
onClick={togglePreviewFor('normal')}
>
Preview {previewType === 'normal' ? 'original' : 'normal'}
</Button>
<Button
disabled={
isPristine ||
isRendering ||
(previewType !== 'color' && previewType !== 'original')
}
onClick={togglePreviewFor('color')}
>
Preview {isNormalPreview ? 'original' : 'normal'}
Preview {previewType === 'color' ? 'original' : 'color'}
</Button>
</div>
<div className='mt-2 pt-2'>
<Gradient ref={gradientCanvasRef} />
</div>
</section>
);
}
Expand Down Expand Up @@ -281,15 +308,3 @@ function Canvas({canvasRef, width, height, isRendering}: CanvasProps) {
</div>
);
}

const getCtx2d = (
canvasRef: React.RefObject<HTMLCanvasElement>,
): CanvasRenderingContext2D => {
const canvas = canvasRef.current;
if (!canvas) throw new TypeError('Canvas not found in ref');

const ctx2d = canvas.getContext('2d');
if (!ctx2d) throw new TypeError('Error getting 2d context from canvas');

return ctx2d;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {useCallback, useRef, useState} from 'react';
import {useOnClickOutside} from 'usehooks-ts';
import {rgbToHex} from '@/utils/colors';
import {type ColorRGB} from '@/types';
import {RgbColorPicker} from 'react-colorful';

type ColorPickerProps = {
readonly color: ColorRGB;
readonly setColor: (newColor: ColorRGB) => void;
};

export function ColorPicker({color, setColor}: ColorPickerProps) {
const colorHex = rgbToHex(color);

const popoverRef = useRef<HTMLDivElement>(null);
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
const closePopover = useCallback(() => {
setPopoverOpen(false);
}, []);
useOnClickOutside(popoverRef, closePopover);

return (
<div className='flex items-center gap-2'>
<div
className='relative h-8 w-8 rounded-sm border border-white'
style={{backgroundColor: colorHex}}
onClick={() => {
setPopoverOpen(true);
}}
>
{popoverOpen && (
<div ref={popoverRef} className='absolute bottom-full left-0 z-10'>
<RgbColorPicker color={color} onChange={setColor} />
</div>
)}
</div>
<span className='w-16 text-sm text-white/75'>{colorHex}</span>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {ColorPicker} from './ColorPicker';
104 changes: 104 additions & 0 deletions src/components/pages/Generator/CanvasSection/Gradient/Gradient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import {Button} from '@/components/ui/Button';
import {rgb} from '@/utils/colors';
import {type ColorRGB} from '@/types';
import {getCtx2dFromRef} from '../utils/getCtx2dFromRef';
import {getCanvasDimensions} from '../utils/getCanvasDimensions';
import {ColorPicker} from './ColorPicker';

const colorsMin = 2;
const colorsMax = 20;
const colorsDefault: ColorRGB[] = [
{r: 0, g: 255, b: 255},
{r: 149, g: 0, b: 255},
{r: 255, g: 229, b: 0},
];

export const Gradient = forwardRef<HTMLCanvasElement>((_, forwardedRef) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useImperativeHandle(forwardedRef, () => {
if (!canvasRef.current) {
throw new TypeError('Canvas ref is not set');
}

return canvasRef.current;
});

const [colors, setColors] = useState<ColorRGB[]>(colorsDefault);

const addColor = () => {
setColors([...colors, {r: 0, g: 0, b: 0}]);
};

const setColorForIndex = (index: number) => (newColor: ColorRGB) => {
const newColors = [...colors];
newColors[index] = newColor;
setColors(newColors);
};

const deleteColorForIndex = (index: number) => () => {
const newColors = [...colors];
newColors.splice(index, 1);
setColors(newColors);
};

useEffect(() => {
const ctx2d = getCtx2dFromRef(canvasRef);
const {w, h} = getCanvasDimensions(ctx2d);

ctx2d.clearRect(0, 0, w, h);

const gradient = ctx2d.createLinearGradient(0, 0, w, 0);
for (let i = 0; i < colors.length; i++) {
gradient.addColorStop(i / Math.max(colors.length - 1, 1), rgb(colors[i]));
}

ctx2d.fillStyle = gradient;
ctx2d.fillRect(0, 0, w, h);
}, [colors]);

return (
<div>
<h2>Gradient</h2>
<div className='flex flex-col gap-2 sm:flex-row'>
<div className='pt-1'>
<canvas
ref={canvasRef}
className='h-36 w-36 border border-dashed border-white'
width={256}
height={256}
/>
</div>
<div>
<div className='flex flex-col gap-1 pt-1'>
{colors.map((color, index) => (
<div
key={index} // eslint-disable-line react/no-array-index-key
className='flex gap-2'
>
<ColorPicker color={color} setColor={setColorForIndex(index)} />
<Button
disabled={colors.length <= colorsMin}
onClick={deleteColorForIndex(index)}
>
Delete
</Button>
</div>
))}
</div>
<div className='pt-2'>
<Button disabled={colors.length >= colorsMax} onClick={addColor}>
Add stop
</Button>
</div>
</div>
</div>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {Gradient} from './Gradient';
35 changes: 35 additions & 0 deletions src/components/pages/Generator/CanvasSection/utils/drawColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {getCanvasDimensions} from './getCanvasDimensions';

export const drawColor = ({
ctx2d,
ctx2dGradient,
}: {
ctx2d: CanvasRenderingContext2D;
ctx2dGradient: CanvasRenderingContext2D;
}): void => {
const {w, h} = getCanvasDimensions(ctx2d);
const {w: wGradient} = getCanvasDimensions(ctx2dGradient);

const source = ctx2d.getImageData(0, 0, w, h);
const sourceGradient = ctx2dGradient.getImageData(0, 0, wGradient, 1);
const destination = ctx2d.createImageData(w, h);

const paletteR: number[] = [];
const paletteG: number[] = [];
const paletteB: number[] = [];

for (let i = 0; i < wGradient * 4; i += 4) {
paletteR.push(sourceGradient.data[i]);
paletteG.push(sourceGradient.data[i + 1]);
paletteB.push(sourceGradient.data[i + 2]);
}

for (let i = 0; i < source.data.length; i += 4) {
destination.data[i] = paletteR[source.data[i]];
destination.data[i + 1] = paletteG[source.data[i + 1]];
destination.data[i + 2] = paletteB[source.data[i + 2]];
destination.data[i + 3] = source.data[i + 3];
}

ctx2d.putImageData(destination, 0, 0);
};
Loading

0 comments on commit f4fabbc

Please sign in to comment.