From f4fabbc776f12542e550aa77fca2ef870db33904 Mon Sep 17 00:00:00 2001 From: Yehor Date: Thu, 16 Nov 2023 18:52:18 +0200 Subject: [PATCH] Colorizer (v0.0.1-alpha.5) (#18) --- bun.lockb | Bin 167794 -> 168683 bytes package.json | 4 +- .../Generator/CanvasSection/CanvasSection.tsx | 107 ++++++++++-------- .../Gradient/ColorPicker/ColorPicker.tsx | 40 +++++++ .../Gradient/ColorPicker/index.ts | 1 + .../CanvasSection/Gradient/Gradient.tsx | 104 +++++++++++++++++ .../Generator/CanvasSection/Gradient/index.ts | 1 + .../CanvasSection/utils/drawColor.ts | 35 ++++++ .../CanvasSection/utils/getCtx2dFromRef.ts | 11 ++ src/types/index.ts | 15 +++ src/utils/colors.test.ts | 12 +- src/utils/colors.ts | 35 ++---- 12 files changed, 292 insertions(+), 73 deletions(-) create mode 100644 src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/ColorPicker.tsx create mode 100644 src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/index.ts create mode 100644 src/components/pages/Generator/CanvasSection/Gradient/Gradient.tsx create mode 100644 src/components/pages/Generator/CanvasSection/Gradient/index.ts create mode 100644 src/components/pages/Generator/CanvasSection/utils/drawColor.ts create mode 100644 src/components/pages/Generator/CanvasSection/utils/getCtx2dFromRef.ts diff --git a/bun.lockb b/bun.lockb index 74697c61776ef4ce81b4814b710036502ae5bdff..4875a9961fd90f2b2fc0fd4d8c855035b9f03332 100755 GIT binary patch delta 31257 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 E0lS@6jQ{`u delta 30510 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 G;r|CYpQZo+ diff --git a/package.json b/package.json index 58d3bff..834bde1 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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": { diff --git a/src/components/pages/Generator/CanvasSection/CanvasSection.tsx b/src/components/pages/Generator/CanvasSection/CanvasSection.tsx index 618277a..a633b0f 100644 --- a/src/components/pages/Generator/CanvasSection/CanvasSection.tsx +++ b/src/components/pages/Generator/CanvasSection/CanvasSection.tsx @@ -7,11 +7,16 @@ 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(false); const width = is8k ? 8192 : 4096; @@ -19,18 +24,19 @@ export function CanvasSection() { const [isPristine, setIsPristine] = useState(true); const [isRendering, setIsRendering] = useState(false); - const [isNormalPreview, setIsNormalPreview] = useState(false); + const [previewType, setPreviewType] = useState('original'); const [renderTimeMs, setRenderTimeMs] = useState(); const canvasRef = useRef(null); const canvasOriginalPreviewDataUrl = useRef(undefined); + const gradientCanvasRef = useRef(null); const render = () => { setIsPristine(false); setIsRendering(true); - setIsNormalPreview(false); + setPreviewType('original'); - const ctx2d = getCtx2d(canvasRef); + const ctx2d = getCtx2dFromRef(canvasRef); const { iterations, @@ -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; @@ -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 ( @@ -231,18 +241,35 @@ export function CanvasSection() {
+
+
+ +
); } @@ -281,15 +308,3 @@ function Canvas({canvasRef, width, height, isRendering}: CanvasProps) { ); } - -const getCtx2d = ( - canvasRef: React.RefObject, -): 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; -}; diff --git a/src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/ColorPicker.tsx b/src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/ColorPicker.tsx new file mode 100644 index 0000000..b9bc210 --- /dev/null +++ b/src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/ColorPicker.tsx @@ -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(null); + const [popoverOpen, setPopoverOpen] = useState(false); + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, []); + useOnClickOutside(popoverRef, closePopover); + + return ( +
+
{ + setPopoverOpen(true); + }} + > + {popoverOpen && ( +
+ +
+ )} +
+ {colorHex} +
+ ); +} diff --git a/src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/index.ts b/src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/index.ts new file mode 100644 index 0000000..d99937e --- /dev/null +++ b/src/components/pages/Generator/CanvasSection/Gradient/ColorPicker/index.ts @@ -0,0 +1 @@ +export {ColorPicker} from './ColorPicker'; diff --git a/src/components/pages/Generator/CanvasSection/Gradient/Gradient.tsx b/src/components/pages/Generator/CanvasSection/Gradient/Gradient.tsx new file mode 100644 index 0000000..a44c5af --- /dev/null +++ b/src/components/pages/Generator/CanvasSection/Gradient/Gradient.tsx @@ -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((_, forwardedRef) => { + const canvasRef = useRef(null); + useImperativeHandle(forwardedRef, () => { + if (!canvasRef.current) { + throw new TypeError('Canvas ref is not set'); + } + + return canvasRef.current; + }); + + const [colors, setColors] = useState(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 ( +
+

Gradient

+
+
+ +
+
+
+ {colors.map((color, index) => ( +
+ + +
+ ))} +
+
+ +
+
+
+
+ ); +}); diff --git a/src/components/pages/Generator/CanvasSection/Gradient/index.ts b/src/components/pages/Generator/CanvasSection/Gradient/index.ts new file mode 100644 index 0000000..78b5d40 --- /dev/null +++ b/src/components/pages/Generator/CanvasSection/Gradient/index.ts @@ -0,0 +1 @@ +export {Gradient} from './Gradient'; diff --git a/src/components/pages/Generator/CanvasSection/utils/drawColor.ts b/src/components/pages/Generator/CanvasSection/utils/drawColor.ts new file mode 100644 index 0000000..a99c71c --- /dev/null +++ b/src/components/pages/Generator/CanvasSection/utils/drawColor.ts @@ -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); +}; diff --git a/src/components/pages/Generator/CanvasSection/utils/getCtx2dFromRef.ts b/src/components/pages/Generator/CanvasSection/utils/getCtx2dFromRef.ts new file mode 100644 index 0000000..eeaae63 --- /dev/null +++ b/src/components/pages/Generator/CanvasSection/utils/getCtx2dFromRef.ts @@ -0,0 +1,11 @@ +export const getCtx2dFromRef = ( + canvasRef: React.RefObject, +): 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; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 5c7d519..2f6eeb5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1 +1,16 @@ export type NumberDual = [number, number]; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export type ColorRGB = { + r: number; + g: number; + b: number; +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export type ColorRGBA = { + r: number; + g: number; + b: number; + a: number; +}; diff --git a/src/utils/colors.test.ts b/src/utils/colors.test.ts index ffd1573..849919a 100644 --- a/src/utils/colors.test.ts +++ b/src/utils/colors.test.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from 'vitest'; -import {rgb, rgba, xxx, xxxa} from './colors'; +import {rgb, rgba, rgbToHex, xxx, xxxa} from './colors'; describe('colors', () => { describe('rgb', () => { @@ -18,6 +18,16 @@ describe('colors', () => { }); }); + describe('rgbToHex', () => { + it('works', () => { + expect(rgbToHex({r: 0, g: 0, b: 0})).toBe('#000000'); + expect(rgbToHex({r: 12, g: 0, b: 0})).toBe('#0c0000'); + expect(rgbToHex({r: 16, g: 132, b: 0})).toBe('#108400'); + expect(rgbToHex({r: 0, g: 192, b: 255})).toBe('#00c0ff'); + expect(rgbToHex({r: 255, g: 254, b: 10})).toBe('#fffe0a'); + }); + }); + describe('xxx', () => { it('works', () => { expect(xxx({x: 0})).toBe('rgb(0,0,0)'); diff --git a/src/utils/colors.ts b/src/utils/colors.ts index f1b2322..0ed027b 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -1,31 +1,16 @@ -/** - * Converts RGB (0 - 255) numbers to string color in `rgb(r,g,b)` format. - */ -export const rgb = ({r, g, b}: {r: number; g: number; b: number}): string => - `rgb(${r},${g},${b})`; +import {type ColorRGB, type ColorRGBA} from '@/types'; -/** - * Converts RGB (0 - 255) + A (0 - 100) numbers to string color in `rgb(r,g,b,a)` format. - */ -export const rgba = ({ - r, - g, - b, - a, -}: { - r: number; - g: number; - b: number; - a: number; -}): string => `rgb(${r},${g},${b},${a / 100})`; +export const rgb = ({r, g, b}: ColorRGB): string => `rgb(${r},${g},${b})`; + +export const rgba = ({r, g, b, a}: ColorRGBA): string => + `rgb(${r},${g},${b},${a / 100})`; + +export const rgbToHex = ({r, g, b}: ColorRGB): string => { + const toHex = (x: number): string => x.toString(16).padStart(2, '0'); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +}; -/** - * Converts X (0 - 255) number to grayscale string color in `rgb(x,x,x)` format. - */ export const xxx = ({x}: {x: number}): string => rgb({r: x, g: x, b: x}); -/** - * Converts X (0 - 255) + A (0 - 100) numbers to grayscale string color in `rgb(x,x,x,a)` format. - */ export const xxxa = ({x, a}: {x: number; a: number}): string => rgba({r: x, g: x, b: x, a});