-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Hue wheel #351
Fix Hue wheel #351
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 */ | ||
|
@@ -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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The I may just ditch it in favor of raw Culori methods, but for now this is a bandaid to not refactor that. I’m honestly really happy with the performance of |
||
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' | ||
|
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 | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another design problem of the WebGL code was trying to make it magically handle all color spaces, all gamuts. This is far too much responsibility for it, and also isn’t necessary since at any given time, we’re only operating in one colorspace! Instead, if we simply restrict the WebGL code to operate in the Oklab space alone, we don’t lose any performance benefits. It does what it does best—performs crazy-fast translations between Oklab <> Linear RGB. And the only time we have to calculate colorspaces on the main thread in JS is just 2 times—start and end color, which is trivial. Keeping WebGL in Oklab still reaps all the benefits but sheds a ton of complexity and potential bugs. |
||
endColor: Oklab; | ||
program: WebGLProgram; | ||
attr: Record<keyof typeof GRADIENT_RGB_SHADERS.attrs, number> = { | ||
a_position: -1, | ||
|
@@ -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, | ||
|
@@ -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(); | ||
} | ||
|
||
|
@@ -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' }, | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are quite a few changes in the WebGL code, but this was basically a fix—a one-liner. I was halfway to experimenting with some alternate methods of generating the hue wheel, but ultimately we should use the thing that looks best, and that’s changing the clamp method |
||
|
||
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); | ||
} | ||
`; | ||
|
||
|
@@ -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'; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
{ | ||
"extends": "./tsconfig.json", | ||
"compilerOptions": { | ||
"module": "NodeNext", | ||
"moduleResolution": "nodenext" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bug with the Rollup TS plugin I’m too lazy to fix right now—it doesn’t do a good job of tracing the |
||
}, | ||
"exclude": ["**/__test__/**", "**/*.test.*"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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": { | ||
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RC gettin closer! 🎉 |
||
"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", | ||
|
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.*"] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VS Code would consistently autocomplete to
NodeNext
before, but today it is consistently recommendingnodenext
. Stylistic change from TS itself? Dunno.