Skip to content

Commit

Permalink
Fix Hue wheel
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Nov 17, 2024
1 parent 5dd74a0 commit 64d75ee
Show file tree
Hide file tree
Showing 23 changed files with 1,048 additions and 1,827 deletions.
2 changes: 1 addition & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"baseUrl": ".",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"moduleResolution": "nodenext",
"outDir": "./dist/"
},
"include": ["src", "test"]
Expand Down
6 changes: 3 additions & 3 deletions packages/icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"@rollup/plugin-typescript": "^12.1.1",
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"react": "19.0.0-rc-cae764ce-20241025",
"react-dom": "19.0.0-rc-cae764ce-20241025",
"rollup": "^4.26.0",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"rollup": "^4.27.2",
"rollup-plugin-import-css": "^3.5.6",
"types-react": "19.0.0-rc.1",
"types-react-dom": "19.0.0-rc.1"
Expand Down
4 changes: 3 additions & 1 deletion packages/icons/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"extends": "../../tsconfig.react.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist"
"outDir": "dist",
"module": "NodeNext",
"moduleResolution": "nodenext"
},
"include": ["src"]
}
2 changes: 1 addition & 1 deletion packages/plugin-tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"devDependencies": {
"@terrazzo/cli": "workspace:^",
"@terrazzo/parser": "workspace:^",
"tailwindcss": "^3.4.14",
"tailwindcss": "^3.4.15",
"yaml": "^2.6.0"
}
}
6 changes: 3 additions & 3 deletions packages/react-color-picker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@
"chokidar-cli": "^3.0.0",
"culori": "^4.0.1",
"jsdom": "^24.1.3",
"react": "19.0.0-rc-cae764ce-20241025",
"react-dom": "19.0.0-rc-cae764ce-20241025",
"rollup": "^4.26.0",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"rollup": "^4.27.2",
"rollup-plugin-import-css": "^3.5.6",
"size-limit": "^11.1.6",
"types-react": "19.0.0-rc.1",
Expand Down
22 changes: 11 additions & 11 deletions packages/react-color-picker/src/components/ColorChannelSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Slider } from '@terrazzo/tiles';
import { COLORSPACES, type ColorOutput, formatCss, type default as useColor } from '@terrazzo/use-color';
import { type ColorOutput, type Oklab, formatCss, type default as useColor, withAlpha } from '@terrazzo/use-color';
import { modeLrgb, modeOklab, modeRgb, useMode } from 'culori';
import { type ReactElement, useMemo } from 'react';
import { calculateBounds } from '../lib/color.js';
import type { WebGLColor } from '../lib/webgl.js';
import HueWheel from './HueWheel.js';
import TrueGradient from './TrueGradient.js';
import './ColorChannelSlider.css';

useMode(modeRgb);
useMode(modeLrgb);
const toOklab = useMode(modeOklab);

/** size, in px, to pad inner track */
export const TRACK_PADDING = 4;
/** CSS class to add to body */
Expand Down Expand Up @@ -84,18 +88,14 @@ function ColorChannelBG({ channel, color, displayMin, displayMax, min, max }: Co
}

const range = (displayMax ?? max) - (displayMin ?? min);
let leftColor = { ...color.original, [channel]: min, alpha: 1 } as WebGLColor;
if (!RGB_COLORSPACES.includes(color.original.mode)) {
leftColor = COLORSPACES.rec2020.converter(leftColor);
}
let rightColor = { ...color.original, [channel]: max, alpha: 1 } as WebGLColor;
if (!RGB_COLORSPACES.includes(color.original.mode)) {
rightColor = COLORSPACES.rec2020.converter(rightColor);
}
const leftColor = { ...color.original, [channel]: displayMin ?? min };
const rightColor = { ...color.original, [channel]: displayMax ?? max };
const leftOklab = useMemo(() => withAlpha(toOklab(leftColor) as Oklab) as Oklab, [color.css]);
const rightOklab = useMemo(() => withAlpha(toOklab(rightColor) as Oklab) as Oklab, [color.css]);

return (
<div className='tz-color-channel-slider-bg-wrapper'>
<TrueGradient className='tz-color-channel-slider-bg' start={leftColor} end={rightColor} />
<TrueGradient className='tz-color-channel-slider-bg' start={leftOklab} end={rightOklab} />
{typeof displayMin === 'number' && displayMin < min && (
<div
className='tz-color-channel-slider-overlay tz-color-channel-slider-overlay__min'
Expand Down
17 changes: 9 additions & 8 deletions packages/react-color-picker/src/components/TrueGradient.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import type { Oklab } from '@terrazzo/use-color';
import { type ComponentProps, useEffect, useRef, useState } from 'react';
import { GradientRGB, type WebGLColor } from '../lib/webgl.js';
import { GradientOklab } from '../lib/webgl.js';

export interface TrueGradientProps extends ComponentProps<'canvas'> {
start: WebGLColor;
end: WebGLColor;
start: Oklab;
end: Oklab;
}

function TrueGradient({ start, end, ...rest }: TrueGradientProps) {
function TrueGradient({ start, end, ...props }: TrueGradientProps) {
const canvasEl = useRef<HTMLCanvasElement>(null);
const [webgl, setWebgl] = useState<GradientRGB | undefined>();
const [webgl, setWebgl] = useState<GradientOklab | undefined>();

// initialize
useEffect(() => {
if (webgl || !canvasEl.current) {
return;
}
setWebgl(new GradientRGB({ canvas: canvasEl.current, startColor: start, endColor: end }));
}, [canvasEl.current, webgl]);
setWebgl(new GradientOklab({ canvas: canvasEl.current, startColor: start, endColor: end }));
}, [webgl]);

// update color
useEffect(() => {
Expand All @@ -25,7 +26,7 @@ function TrueGradient({ start, end, ...rest }: TrueGradientProps) {
}
}, [start, end, webgl]);

return <canvas ref={canvasEl} {...rest} />;
return <canvas {...props} ref={canvasEl} />;
}

export default TrueGradient;
4 changes: 3 additions & 1 deletion packages/react-color-picker/src/lib/rgb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ vec4 linear_rgb_to_srgb(vec4 linear_rgb) {
return vec4(srgb_transfer_fn(linear_rgb.x), srgb_transfer_fn(linear_rgb.y), srgb_transfer_fn(linear_rgb.z), linear_rgb.w);
}
// Blend 2 vec4 colors together
vec4 avg_vec4(vec4 a, vec4 b, float w) {
return a * (1.0 - w) + b * w;
float _w = 1.0 - w;
return (a * _w) + (b * w);
}
vec4 blend_srgb(vec4 a, vec4 b, float w) {
Expand Down
51 changes: 17 additions & 34 deletions packages/react-color-picker/src/lib/webgl.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import type { A98, Lrgb, P3, Prophoto, Rec2020, Rgb } from '@terrazzo/use-color';
import type { Oklab } from '@terrazzo/use-color';
import { OKLAB } from './oklab.js';
import { LINEAR_RGB } from './rgb.js';

/** RGB-based colorspaces */
export type WebGLColor = A98 | Lrgb | Rgb | P3 | Rec2020 | Prophoto;

/** create a WebGL2 rendering context and throw errors if needed */
export function createRenderingContext(canvas: HTMLCanvasElement): WebGL2RenderingContext {
// init GL
Expand Down Expand Up @@ -96,17 +93,21 @@ in vec4 v_end_color;
out vec4 f_color;
${LINEAR_RGB}
${OKLAB}
void main() {
float a = vec2(gl_FragCoord.xy / v_resolution).x;
f_color = blend_srgb(v_start_color, v_end_color, a);
f_color = linear_rgb_to_srgb(avg_vec4(oklab_to_linear_rgb(v_start_color), oklab_to_linear_rgb(v_end_color), a));
}
`;

export class GradientRGB {
/**
* Create a gradient from A to B, blended in Oklab space.
*/
export class GradientOklab {
gl: WebGL2RenderingContext;
startColor: WebGLColor;
endColor: WebGLColor;
startColor: Oklab;
endColor: Oklab;
program: WebGLProgram;
attr: Record<keyof typeof GRADIENT_RGB_SHADERS.attrs, number> = {
a_position: -1,
Expand All @@ -118,11 +119,7 @@ export class GradientRGB {

private lastFrame: number | undefined;

constructor({
canvas,
startColor,
endColor,
}: { canvas: HTMLCanvasElement; startColor: WebGLColor; endColor: WebGLColor }) {
constructor({ canvas, startColor, endColor }: { canvas: HTMLCanvasElement; startColor: Oklab; endColor: Oklab }) {
this.gl = createRenderingContext(canvas);
this.program = createProgram({
gl: this.gl,
Expand Down Expand Up @@ -153,26 +150,13 @@ export class GradientRGB {
this.render();
}

setColors(startColor: WebGLColor, endColor: WebGLColor) {
setColors(startColor: Oklab, endColor: Oklab) {
this.startColor = startColor;
this.endColor = endColor;
// note: `drawingBufferColorSpace` is ignored in Firefox, but it shouldn’t throw an error
if (
endColor.mode === 'a98' ||
endColor.mode === 'p3' ||
endColor.mode === 'rec2020' ||
endColor.mode === 'prophoto' ||
startColor.mode === 'a98' ||
startColor.mode === 'p3' ||
startColor.mode === 'rec2020' ||
startColor.mode === 'prophoto'
) {
this.gl.drawingBufferColorSpace = 'display-p3';
} else {
this.gl.drawingBufferColorSpace = 'srgb';
}
this.gl.vertexAttrib4f(this.attr.a_start_color, startColor.r, startColor.g, startColor.b, 1);
this.gl.vertexAttrib4f(this.attr.a_end_color, endColor.r, endColor.g, endColor.b, 1);
this.gl.drawingBufferColorSpace = 'display-p3';
this.gl.vertexAttrib4f(this.attr.a_start_color, startColor.l, startColor.a, startColor.b, 1);
this.gl.vertexAttrib4f(this.attr.a_end_color, endColor.l, endColor.a, endColor.b, 1);
this.render();
}

Expand Down Expand Up @@ -206,7 +190,7 @@ export class GradientRGB {
}

/**
* Generate a perfect rainbow hue wheel in Oklab colorspace with WebGL
* Generate a perceptually-uniform rainbow gradient in the Oklab space.
*/
export const HUE_SHADERS = {
attrs: { a_position: 'a_position', a_resolution: 'a_resolution' },
Expand Down Expand Up @@ -243,12 +227,12 @@ void main() {
// 3 = projection toward point, hue dependent
// 4 = adaptive Lightness, hue independent
// 5 = adaptive Lightness, hue dependent
int clamp_mode = 2;
int clamp_mode = 3;
float hue_norm = vec2(gl_FragCoord.xy / v_resolution).x;
float hue = 360.0 * hue_norm;
f_color = oklch_to_srgb(vec4(0.8, 0.4, hue, 1.0), clamp_mode);
f_color = oklch_to_srgb(vec4(0.7, 0.4, hue, 1.0), clamp_mode);
}
`;

Expand Down Expand Up @@ -288,7 +272,6 @@ export class HueWheel {
if (gamut !== 'srgb' && gamut !== 'p3') {
throw new Error(`Unsupported gamut: "${gamut}"`);
}
// this.gl.drawingBufferColorSpace = gamut === 'p3' ? 'display-p3' : 'srgb';
this.gl.drawingBufferColorSpace = 'display-p3';
}

Expand Down
4 changes: 4 additions & 0 deletions packages/react-color-picker/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "nodenext"
},
"exclude": ["**/__test__/**", "**/*.test.*"]
}
5 changes: 4 additions & 1 deletion packages/react-color-picker/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"extends": "../../tsconfig.react.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist"
"outDir": "dist",
"module": "NodeNext",
"moduleResolution": "nodenext",
"skipLibCheck": true
},
"include": ["src"]
}
16 changes: 8 additions & 8 deletions packages/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
"culori": "^4.0.1"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.3.6",
"@storybook/react": "^8.3.6",
"@storybook/react-vite": "^8.3.6",
"@storybook/test": "^8.3.6",
"@storybook/addon-essentials": "^8.4.4",
"@storybook/react": "^8.4.4",
"@storybook/react-vite": "^8.4.4",
"@storybook/test": "^8.4.4",
"@terrazzo/fonts": "workspace:^",
"@terrazzo/icons": "workspace:^",
"@terrazzo/react-color-picker": "workspace:^",
Expand All @@ -31,12 +31,12 @@
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"@vitejs/plugin-react-swc": "^3.7.1",
"react": "19.0.0-rc-cae764ce-20241025",
"react-dom": "19.0.0-rc-cae764ce-20241025",
"storybook": "^8.3.6",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"storybook": "^8.4.4",
"types-react": "19.0.0-rc.1",
"types-react-dom": "19.0.0-rc.1",
"vite": "^5.4.10"
"vite": "^5.4.11"
},
"overrides": {
"@types/react": "npm:types-react@rc",
Expand Down
12 changes: 6 additions & 6 deletions packages/storybook/src/TrueGradient.stories.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { TrueGradient } from '@terrazzo/react-color-picker';
import { modeP3, modeRgb, useMode } from 'culori/fn';

const toRgb = useMode(modeRgb);
useMode(modeP3);
import useColor from '@terrazzo/use-color';

export default {
title: 'Components/Display/TrueGradient',
Expand All @@ -18,14 +15,17 @@ export const Overview = {
end: 'color(srgb 0 1 0)',
},
render(args) {
const [start] = useColor(args.start);
const [end] = useColor(args.end);

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
Good
<TrueGradient start={toRgb(args.start)} end={toRgb(args.end)} style={{ width: '16rem', height: '1.5rem' }} />
<TrueGradient start={start.oklab} end={end.oklab} style={{ width: '16rem', height: '1.5rem' }} />
Bad
<div
style={{
background: `linear-gradient(to right, ${args.start}, ${args.end})`,
background: `linear-gradient(to right, ${start.css}, ${end.css})`,
width: '16rem',
height: '1.5rem',
}}
Expand Down
8 changes: 4 additions & 4 deletions packages/tiles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@use-gesture/react": "^10.3.1",
"clsx": "^2.1.1",
"culori": "^4.0.1",
"shiki": "^1.22.2",
"shiki": "^1.23.0",
"vite": "^5.4.11"
},
"devDependencies": {
Expand All @@ -52,9 +52,9 @@
"@types/react-dom": "npm:types-react-dom@rc",
"@vitejs/plugin-react-swc": "^3.7.1",
"chokidar-cli": "^3.0.0",
"react": "19.0.0-rc-cae764ce-20241025",
"react-dom": "19.0.0-rc-cae764ce-20241025",
"rollup": "^4.26.0",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"rollup": "^4.27.2",
"rollup-plugin-import-css": "^3.5.6",
"size-limit": "^11.1.6",
"types-react": "19.0.0-rc.1",
Expand Down
4 changes: 4 additions & 0 deletions packages/tiles/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "nodenext"
},
"exclude": ["**/__test__/**", "**/*.test.*"]
}
5 changes: 4 additions & 1 deletion packages/tiles/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"extends": "../../tsconfig.react.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist"
"outDir": "dist",
"module": "NodeNext",
"moduleResolution": "nodenext",
"skipLibCheck": true
},
"include": ["src"]
}
Loading

0 comments on commit 64d75ee

Please sign in to comment.