From 919e7327dbd0517788cea8dfb6a6544cbbf265c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Roche?= Date: Tue, 20 Aug 2024 14:21:56 +0200 Subject: [PATCH] color palette --- plugins/dither/src/App.css | 91 +++++++++- plugins/dither/src/App.old.tsx | 161 ------------------ plugins/dither/src/App.tsx | 42 ++--- plugins/dither/src/materials/ordered.tsx | 149 ++++++++++------ plugins/dither/src/palette.tsx | 97 +++++++++++ plugins/dither/src/use-gradient-texture.ts | 33 ++++ .../src/use-ordered-dithering-texture.ts | 4 +- plugins/dither/src/worker/tsconfig.json | 6 - plugins/dither/src/worker/worker.ts | 49 ------ 9 files changed, 340 insertions(+), 292 deletions(-) delete mode 100644 plugins/dither/src/App.old.tsx create mode 100644 plugins/dither/src/palette.tsx create mode 100644 plugins/dither/src/use-gradient-texture.ts delete mode 100644 plugins/dither/src/worker/tsconfig.json delete mode 100644 plugins/dither/src/worker/worker.ts diff --git a/plugins/dither/src/App.css b/plugins/dither/src/App.css index be061288..1a971b86 100644 --- a/plugins/dither/src/App.css +++ b/plugins/dither/src/App.css @@ -1,7 +1,7 @@ .container { display: flex; align-items: center; - height: 100%; + /* height: 100%; */ flex-direction: column; /* gap: 10px; */ padding: 0 15px 15px 15px; @@ -42,6 +42,15 @@ input[type="range"] { width: 100%; } +body[data-framer-theme=dark] .gui-row .gui-label { + color: #ccc; +} + +.gui { + margin-top: 5px; + margin-bottom: 5px; +} + .gui-row { position: relative; @@ -66,4 +75,82 @@ input[type="range"] { .gui-row .gui-select { grid-column: 2 / -1; width: 100%; -} \ No newline at end of file +} + +.gui-row .gui-palette { + width: 100%; + grid-column: 2 / -1; + gap: 10px; + display: flex; + flex-direction: column; +} + +.gui-row .gui-palette .color { + /* width: 100%; */ + + /* display: grid; */ + /* grid-template-columns: repeat(2, minmax(0, 1fr)); */ + /* column-gap: 10px; */ + /* display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; */ + /* gap: 10px; */ + + display: flex; + align-items: center; + gap: 10px; + +} + +.gui-row .gui-palette .color input{ + /* grid-column: 1 / span 1; + width: 100%; */ + width: auto; + flex-grow: 1; +} + +/* .gui-palette .color { + width: 100%; +} */ + +/* .gui-palette input { */ + /* width: 100%; */ + /* width: 30px; */ +/* } */ + +/* .gui-palette .list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 5px; + width: 100%; +}*/ + +.gui-palette .controls { + /* width: 100%; */ + display: flex; + /* gap: 10px; */ + align-items: stretch; + /* justify-content: flex-end; */ +} + +.gui-palette .controls > * { + display: flex; + align-items: center; +} + +.gui-palette .add { + height: 30px; + background-color: var(--framer-color-bg-secondary); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + cursor: pointer; +} + +/* .gui-row .gui-palette input { + width: 30px; + aspect-ratio: 1; +} */ \ No newline at end of file diff --git a/plugins/dither/src/App.old.tsx b/plugins/dither/src/App.old.tsx deleted file mode 100644 index 804a3d4a..00000000 --- a/plugins/dither/src/App.old.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import * as comlink from "comlink" -import { ImageAsset, framer } from "framer-plugin" -import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react" -import "./App.css" -import { Spinner } from "./Spinner" -import { assert, bytesFromCanvas } from "./utils" -import type { CanvasWorker } from "./worker/worker" -import Worker from "./worker/worker?worker" - -const WorkerBase = comlink.wrap(new Worker()) - -void framer.showUI({ position: "top left", width: 280, height: 260 }) - -function useSelectedImage() { - const [image, setImage] = useState(null) - - console.log("image", image) - - useEffect(() => { - return framer.subscribeToImage(setImage) - }, []) - - return image -} - -export function App() { - const image = useSelectedImage() - - if (!image) { - return ( -
-

Select an Image

-
- ) - } - - return -} - -const debounce = (fn: Function, ms = 300) => { - let timeoutId: ReturnType - return function (this: any, ...args: any[]) { - clearTimeout(timeoutId) - timeoutId = setTimeout(() => fn.apply(this, args), ms) - } -} - -function ThresholdImage({ image, maxWidth, maxHeight }: { image: ImageAsset; maxWidth: number; maxHeight: number }) { - const [threshold, setThreshold] = useState(127) - const canvasRef = useRef(null) - const [hasPainted, setHasPainted] = useState(false) - - const handleSaveImage = async () => { - const ctx = canvasRef.current?.getContext("2d") - assert(ctx) - - const originalImage = await image.getData() - - assert(canvasRef.current) - const nextBytes = await bytesFromCanvas(canvasRef.current) - assert(nextBytes) - - const start = performance.now() - - framer.hideUI() - await framer.setImage({ - image: { - bytes: nextBytes, - mimeType: originalImage.mimeType, - }, - }) - - void framer.closePlugin("Image saved...") - - console.log("total duration", performance.now() - start) - } - - const updateCanvas = useMemo( - () => - debounce(async (nextThreshold: number) => { - const worker = await new WorkerBase() - - const bitmap = await image.loadBitmap() - - const canvas = canvasRef.current - assert(canvas) - const ctx = canvas.getContext("2d") - assert(ctx) - - const result = await worker.draw(bitmap, nextThreshold) - - assert(result) - - let displayWidth: number, displayHeight: number - - // Calculate the aspect ratios based on max dimensions - const widthRatio = maxWidth / bitmap.width - const heightRatio = maxHeight / bitmap.height - - if (widthRatio < heightRatio) { - // Width ratio is smaller, so we'll base dimensions on width to prevent going over maxWidth - displayWidth = maxWidth - displayHeight = bitmap.height * widthRatio - } else { - // Base dimensions on height - displayHeight = maxHeight - displayWidth = bitmap.width * heightRatio - } - - assert(ctx) - - canvas.width = displayWidth - canvas.height = displayHeight - - framer.showUI({ - position: "top left", - width: 280, - height: displayHeight + 95, - }) - - ctx.drawImage(result, 0, 0, displayWidth, displayHeight) - - setHasPainted(true) - }, 20), - [image] - ) - - const handleThresholdChange = useCallback( - (nextValue: number) => { - startTransition(() => { - setThreshold(nextValue) - void updateCanvas(nextValue) - }) - }, - [updateCanvas] - ) - - useEffect(() => { - // Start in the middle between 0-255 - void updateCanvas(127) - }, [image]) - - return ( -
-
- - {!hasPainted && } -
- - handleThresholdChange(Number(event.target.value))} - /> - - -
- ) -} diff --git a/plugins/dither/src/App.tsx b/plugins/dither/src/App.tsx index 11ee9b61..701f03a7 100644 --- a/plugins/dither/src/App.tsx +++ b/plugins/dither/src/App.tsx @@ -1,27 +1,27 @@ -import * as comlink from "comlink" +// import * as comlink from "comlink" import { ImageAsset, framer } from "framer-plugin" import { - forwardRef, - startTransition, + // forwardRef, + // startTransition, useCallback, useEffect, - useImperativeHandle, - useMemo, + // useImperativeHandle, + // useMemo, useRef, useState, } from "react" import "./App.css" -import { Spinner } from "./Spinner" +// import { Spinner } from "./Spinner" import { assert, bytesFromCanvas } from "./utils" -import type { CanvasWorker } from "./worker/worker" -import Worker from "./worker/worker?worker" +// import type { CanvasWorker } from "./worker/worker" +// import Worker from "./worker/worker?worker" import { Renderer, Camera, Transform, Plane, Program, Mesh, Texture } from "ogl" -import { RandomDither } from "./materials/random" +// import { RandomDither } from "./materials/random" import { OrderedDither } from "./materials/ordered" // const WorkerBase = comlink.wrap(new Worker()) -void framer.showUI({ position: "top left", width: 280, height: Infinity }) +void framer.showUI({ position: "top left", width: 280, height: 1000 }) function useSelectedImage() { const [image, setImage] = useState(null) @@ -90,7 +90,7 @@ function DitherImage({ image }: { image: ImageAsset }) { const [scene] = useState(() => new Transform()) const [geometry] = useState(() => new Plane(gl)) - const [type, setType] = useState(1) + // const [type, setType] = useState(1) const [program, setProgram] = useState(() => new Program(gl, {})) const [mesh] = useState(() => new Mesh(gl, { geometry, program })) @@ -223,16 +223,16 @@ function DitherImage({ image }: { image: ImageAsset }) { texture={texture} /> )} */} - {type === 1 && ( - { - // TODO: fix this type - setProgram(node?.program) - }} - gl={gl} - texture={texture} - /> - )} + {/* {type === 1 && ( */} + { + // TODO: fix this type + setProgram(node?.program) + }} + gl={gl} + texture={texture} + /> + {/* )} */} diff --git a/plugins/dither/src/materials/ordered.tsx b/plugins/dither/src/materials/ordered.tsx index 5dd1598e..27f7bb87 100644 --- a/plugins/dither/src/materials/ordered.tsx +++ b/plugins/dither/src/materials/ordered.tsx @@ -3,9 +3,11 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from "react" import { GLSL } from "../glsl" import { ORDERED_DITHERING_MATRICES } from "../ordered-dithering-matrices" import { useOrderedDitheringTexture } from "../use-ordered-dithering-texture" +import { Palette } from "../palette" +import { useGradientTexture } from "../use-gradient-texture" export class OrderedDitherMaterial extends Program { - constructor(gl: OGLRenderingContext, texture: Texture, ditherTexture: Texture) { + constructor(gl: OGLRenderingContext, texture: Texture, ditherTexture: Texture, paletteTexture: Texture) { super(gl, { vertex: /*glsl*/ `#version 300 es precision lowp float; @@ -34,6 +36,7 @@ export class OrderedDitherMaterial extends Program { uniform sampler2D uTexture; uniform sampler2D uDitherTexture; + uniform sampler2D uPaletteTexture; uniform int uMode; uniform vec2 uResolution; uniform float uPixelSize; @@ -58,50 +61,76 @@ export class OrderedDitherMaterial extends Program { float threshold = uRandom == 1 ? random(uv) : texture(uDitherTexture, vec2(x, y)).r; - if (uColorMode == 0) { // Black and White - if (luma(rgb) >= threshold) { - color = vec3(1.0); - } - } else if (uColorMode == 1) { // RGB - if(rgb.r >= threshold) { - color.r = 1.0; - } - - if(rgb.g >= threshold) { - color.g = 1.0; - } - - if(rgb.b >= threshold) { - color.b = 1.0; - } - } else if (uColorMode == 2) { // Grayscale + // if (uColorMode == 0) { // Grayscale + // if (luma(rgb) >= threshold) { + // color = vec3(1.0); + // } + // } else if (uColorMode == 1) { // RGB + // if(rgb.r >= threshold) { + // color.r = 1.0; + // } + + // if(rgb.g >= threshold) { + // color.g = 1.0; + // } + + // if(rgb.b >= threshold) { + // color.b = 1.0; + // } + // } else + + if (uColorMode == 0) { // Grayscale color.rgb = vec3(luma(rgb)); + threshold -= 0.44; // arbitraty threshold adjustment - if(color.r >= threshold) { - color.r = quantize(color.r, uQuantization); - } + // if(color.r >= threshold) { + // if(luma(rgb) >= threshold) { + color.r = quantize(color.r + threshold, uQuantization); + // } - if(color.g >= threshold) { - color.g = quantize(color.g, uQuantization); - } + // if(color.g >= threshold) { + color.g = quantize(color.g + threshold, uQuantization); + // } - if(color.b >= threshold) { - color.b = quantize(color.b, uQuantization); - } - } else if (uColorMode == 3) { // True Colors + // if(color.b >= threshold) { + color.b = quantize(color.b + threshold, uQuantization); + // } + } else if (uColorMode == 1) { // True Colors color.rgb = rgb; + threshold -= 0.44; // arbitraty threshold adjustment - if(rgb.r >= threshold) { - color.r = quantize(color.r, uQuantization); - } + // if(rgb.r >= threshold) { + color.r = quantize(color.r + threshold, uQuantization); + // } - if(rgb.g >= threshold) { - color.g = quantize(color.g, uQuantization); - } + // if(rgb.g >= threshold) { + color.g = quantize(color.g + threshold, uQuantization); + // } + + // if(rgb.b >= threshold) { + color.b = quantize(color.b + threshold, uQuantization); + // } + } else if (uColorMode == 2) { // Custom Palette + // color.rgb = vec3(luma(rgb)); + threshold -= 0.44; // arbitraty threshold adjustment + + ivec2 paletteTextureSize = textureSize(uDitherTexture, 0); + + color.rgb = texture(uPaletteTexture, vec2(quantize(1. - (luma(rgb) + threshold), paletteTextureSize.x), 0.0)).rgb; + + // if(color.r >= threshold) { + // if(luma(rgb) >= threshold) { + // float x = quantize(color.r + threshold, uQuantization); + // } + + // if(color.g >= threshold) { + // color.g = quantize(color.g + threshold, uQuantization); + // } + + // if(color.b >= threshold) { + // color.b = quantize(color.b + threshold, uQuantization); + // } - if(rgb.b >= threshold) { - color.b = quantize(color.b, uQuantization); - } } @@ -125,6 +154,8 @@ export class OrderedDitherMaterial extends Program { fragColor = vec4(orderedDither(color.rgb, pixelizedUv), color.a); + // fragColor = texture(uPaletteTexture, vUv); + // fragColor = color; } `, @@ -132,6 +163,7 @@ export class OrderedDitherMaterial extends Program { uTexture: { value: texture }, uResolution: { value: new Vec2(1, 1) }, uDitherTexture: { value: ditherTexture }, + uPaletteTexture: { value: paletteTexture }, uPixelSize: { value: 1 }, uColorMode: { value: 0 }, uQuantization: { value: 8 }, @@ -159,7 +191,7 @@ export class OrderedDitherMaterial extends Program { } set quantization(value: number) { - this.uniforms.uQuantization.value = value + this.uniforms.uQuantization.value = Math.floor(value) } set isRandom(value: boolean) { @@ -176,14 +208,18 @@ export const OrderedDither = forwardRef(function RandomDither( ref ) { const [mode, setMode] = useState("BAYER_4x4") - const [colorMode, setColorMode] = useState(0) - const [quantization, setQuantization] = useState(8) + const [colorMode, setColorMode] = useState(1) + const [quantization, setQuantization] = useState(3) const [isRandom, setIsRandom] = useState(false) const [pixelSize, setPixelSize] = useState(2) + const [colors, setColors] = useState([] as string[]) + + console.log(quantization) // const [pixelSize, setPixelSize] = useState(1) // const [ditherPixelSize, setDitherPixelSize] = useState(1) const { texture: ditherTexture, canvas } = useOrderedDitheringTexture(gl, ORDERED_DITHERING_MATRICES[mode]) + const { texture: paletteTexture } = useGradientTexture(gl, colors) useEffect(() => { // document.body.appendChild(canvas) @@ -199,7 +235,7 @@ export const OrderedDither = forwardRef(function RandomDither( ` }, [ditherTexture, canvas]) - const [program] = useState(() => new OrderedDitherMaterial(gl, texture, ditherTexture)) + const [program] = useState(() => new OrderedDitherMaterial(gl, texture, ditherTexture, paletteTexture)) useEffect(() => { program.colorMode = colorMode @@ -224,7 +260,7 @@ export const OrderedDither = forwardRef(function RandomDither( useImperativeHandle(ref, () => ({ program }), [program]) return ( -
+
- {[2, 3].includes(colorMode) && ( + {[0, 1].includes(colorMode) && (
setQuantization(parseInt(e.target.value))} className="gui-select" />
)} + {[2].includes(colorMode) && ( +
+ + { + setColors(colors) + }} + /> +
+ )}
) }) diff --git a/plugins/dither/src/palette.tsx b/plugins/dither/src/palette.tsx new file mode 100644 index 00000000..1438e0d0 --- /dev/null +++ b/plugins/dither/src/palette.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react" + +const DEFAULT_COLORS = ["#ccff33", "#9ef01a", "#70e000", "#38b000", "#008000", "#006400", "#004b23"] + +export function Palette({ onChange }: { onChange?: (colors: string[]) => void }) { + const [colors, setColors] = useState(DEFAULT_COLORS) + // const [count, setCount] = useState(2) + + useEffect(() => { + onChange?.(colors) + }, [colors]) + + // useEffect(() => { + // setColors(colors => { + // const newColors = [...colors] + // newColors.length = count + // return newColors + // }) + // }, [count]) + + return ( +
+ {colors.map((color, i) => ( +
+ { + const color = e.target.value + setColors(colors => { + const newColors = [...colors] + newColors[i] = color + return newColors + }) + }} + // ref={node => { + // if (!node) return + + // const color = node.value + + // if (colors[i] === color) return + + // setColors(colors => { + // const newColors = [...colors] + // newColors[i] = color + // return newColors + // }) + // }} + /> + {colors.length > 2 && ( +
+
+ setColors(() => { + const newColors = [...colors] + newColors.splice(i, 1) + return newColors + }) + } + > + + + + +
+
+ )} +
+ ))} +
+ setColors(() => { + const newColors = [...colors] + newColors.push(DEFAULT_COLORS[newColors.length]) + return newColors + }) + } + className="add" + > + Add Color +
+
+ ) +} diff --git a/plugins/dither/src/use-gradient-texture.ts b/plugins/dither/src/use-gradient-texture.ts new file mode 100644 index 00000000..a6739113 --- /dev/null +++ b/plugins/dither/src/use-gradient-texture.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react" +import { OGLRenderingContext, Texture, Color } from "ogl" + +export function useGradientTexture(gl: OGLRenderingContext, colors: string[]) { + const [texture] = useState(() => new Texture(gl, { minFilter: gl.NEAREST, magFilter: gl.NEAREST })) + const [canvas] = useState(() => document.createElement("canvas")) + + useEffect(() => { + if (!colors.length) return + + const pixels = new Uint8ClampedArray(colors.length * 4) + for (let i = 0; i < pixels.length; i += 4) { + const color = new Color(colors[i / 4]) + pixels[i] = color.r * 255 + pixels[i + 1] = color.g * 255 + pixels[i + 2] = color.b * 255 + pixels[i + 3] = 255 + } + + canvas.width = colors.length + canvas.height = 1 + const ctx = canvas.getContext("2d") + // ctx.putImageData(pixels, 0, 0) + ctx?.putImageData(new ImageData(pixels, colors.length, 1), 0, 0) + + texture.image = pixels + texture.width = colors.length + texture.height = 1 + texture.update() + }, [texture, canvas, colors]) + + return { texture } +} diff --git a/plugins/dither/src/use-ordered-dithering-texture.ts b/plugins/dither/src/use-ordered-dithering-texture.ts index c1ec0ae2..01c120b1 100644 --- a/plugins/dither/src/use-ordered-dithering-texture.ts +++ b/plugins/dither/src/use-ordered-dithering-texture.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react" -import { OGLRenderingContext, Renderer, Texture } from "ogl" +import { OGLRenderingContext, Texture } from "ogl" export function useOrderedDitheringTexture(gl: OGLRenderingContext, orderedDithering) { const [texture] = useState(() => new Texture(gl, { minFilter: gl.NEAREST, magFilter: gl.NEAREST })) @@ -18,8 +18,6 @@ export function useOrderedDitheringTexture(gl: OGLRenderingContext, orderedDithe pixels[i + 3] = 255 } - console.log(x, y, pixels) - canvas.width = x canvas.height = y const ctx = canvas.getContext("2d") diff --git a/plugins/dither/src/worker/tsconfig.json b/plugins/dither/src/worker/tsconfig.json deleted file mode 100644 index 09ead3ba..00000000 --- a/plugins/dither/src/worker/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "lib": ["ES2020", "Webworker"] - } -} diff --git a/plugins/dither/src/worker/worker.ts b/plugins/dither/src/worker/worker.ts deleted file mode 100644 index 9ecba2fc..00000000 --- a/plugins/dither/src/worker/worker.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Endpoint, expose, transfer } from "comlink" - -export class CanvasWorker { - private offscreenCanvas: OffscreenCanvas - private ctx: OffscreenCanvasRenderingContext2D - - constructor() { - this.offscreenCanvas = new OffscreenCanvas(0, 0) - const ctx = this.offscreenCanvas.getContext("2d") - - if (!ctx) { - throw new Error("WTF") - } - - this.ctx = ctx - } - - async draw(bitmap: ImageBitmap, threshold: number) { - const { width, height } = bitmap - - this.offscreenCanvas.width = width - this.offscreenCanvas.height = height - - const ctx = this.ctx - - // Draw the ImageBitmap onto the canvas - ctx.drawImage(bitmap, 0, 0) - - const imageData = ctx.getImageData(0, 0, width, height) - - if (!imageData) return - - const imgData = imageData.data - - for (let i = 0; i < imgData.length; i += 4) { - const grayscale = imgData[i] * 0.3 + imgData[i + 1] * 0.59 + imgData[i + 2] * 0.11 - const binaryColor = grayscale < threshold ? 0 : 255 - imgData[i] = imgData[i + 1] = imgData[i + 2] = binaryColor - } - - ctx.putImageData(imageData, 0, 0) - - const resultBitmap = await createImageBitmap(imageData) - - return transfer(resultBitmap, [resultBitmap]) - } -} - -expose(CanvasWorker, self as Endpoint)