Skip to content
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

Introduce color interpolation based on the LAB color-space #6782

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions apps/common-app/src/examples/ColorInterpolationExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function DefaultInterpolation({

return (
<View>
<Text>Default:</Text>
<Text style={styles.text}>Default:</Text>
<View style={styles.colorContainer}>
<Animated.View style={[styles.bigbox, animatedStyle]} />
<Button
Expand Down Expand Up @@ -72,7 +72,7 @@ function ColorInterpolation({

return (
<View>
<Text>{title}:</Text>
<Text style={styles.text}>{title}:</Text>
<View style={styles.colorContainer}>
<View style={[styles.box, { backgroundColor: color1 }]} />
<View style={styles.spacer} />
Expand Down Expand Up @@ -139,6 +139,11 @@ function hsvStarInterpolation(
return interpolateColor(progress, [0, 1], [color1, color2], 'HSV');
}

function oklabInterpolation(color1: string, color2: string, progress: number) {
'worklet';
return interpolateColor(progress, [0, 1], [color1, color2], 'LAB');
}

export default function ColorInterpolationExample() {
const [color1, setColor1] = useState('#ff0000');
const [color2, setColor2] = useState('#00ffff');
Expand All @@ -163,11 +168,13 @@ export default function ColorInterpolationExample() {
<ScrollView style={styles.container}>
<TextInput
value={color1Text}
style={styles.text}
onChangeText={onChangeColor1}
autoCapitalize="none"
/>
<TextInput
value={color2Text}
style={styles.text}
onChangeText={onChangeColor2}
autoCapitalize="none"
/>
Expand Down Expand Up @@ -196,6 +203,12 @@ export default function ColorInterpolationExample() {
interpolateFunction={hsvStarInterpolation}
title="HSV*"
/>
<ColorInterpolation
color1={color1}
color2={color2}
interpolateFunction={oklabInterpolation}
title="OK L*a*b*"
/>
</ScrollView>
);
}
Expand All @@ -220,4 +233,8 @@ const styles = StyleSheet.create({
spacer: {
width: 10,
},
text: {
color: 'black',
fontWeight: 'bold',
},
});
patrycjakalinska marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ An array of output colors values in form of strings (like `'red'`, `'#ff0000'`,

#### `colorSpace` <Optional />

The color space to use for interpolation. Can be either `'HSV'` or `'RGB'`. Defaults to `'RGB'`.
The color space to use for interpolation. Can be either `'HSV'`, `'RGB'` or [`'LAB'`](https://en.wikipedia.org/wiki/Oklab_color_space). Defaults to `'RGB'`.
d4vidi marked this conversation as resolved.
Show resolved Hide resolved

#### `options` <Optional />

Expand Down
patrycjakalinska marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ function hsvStarInterpolation(
return interpolateColor(progress, [0, 1], [color1, color2], 'HSV');
}

function oklabInterpolation(color1: string, color2: string, progress: number) {
return interpolateColor(progress, [0, 1], [color1, color2], 'LAB');
}

const ProgressBarSection = ({ color1, color2 }) => {
return (
<div className={styles.progressBarSection}>
Expand All @@ -44,6 +48,7 @@ const ProgressBarSection = ({ color1, color2 }) => {
<p>RGB (with gamma correction)</p>
<p>HSV</p>
<p>HSV (with correction)</p>
<p>OK L*a*b*</p>
</div>
<div className={clsx(styles.progressBarSectionPart)}>
<ColorProgressBar
Expand All @@ -66,6 +71,11 @@ const ProgressBarSection = ({ color1, color2 }) => {
color2={color2}
interpolateFunction={hsvStarInterpolation}
/>
<ColorProgressBar
color1={color1}
color2={color2}
interpolateFunction={oklabInterpolation}
/>
</div>
</div>
);
Expand Down
patrycjakalinska marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Animated, {

interface InterpolateColorProps {
outputRange: any[];
colorSpace: 'RGB' | 'HSV';
colorSpace: 'RGB' | 'HSV' | 'OKLAB';
d4vidi marked this conversation as resolved.
Show resolved Hide resolved
options: InterpolationOptions;
}

Expand Down
patrycjakalinska marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
export const ColorSpace = {
RGB: 'RGB',
HSV: 'HSV',
LAB: 'LAB',
};

const initialState = {
Expand All @@ -37,7 +38,7 @@ export default function useInterpolateColorPlayground() {

const [colorBarsSectionCollapsed, setColorBarsSectionCollapsed] =
useState(true);
const [colorSpace, setColorSpace] = useState<'RGB' | 'HSV'>(
const [colorSpace, setColorSpace] = useState<'RGB' | 'HSV' | 'LAB'>(
ColorSpace[initialState.colorSpace]
);
const [gamma, setGamma] = useState(initialState.gamma);
Expand All @@ -59,19 +60,23 @@ export default function useInterpolateColorPlayground() {
setCorrection(() => initialState.correction);
};

// prettier-ignore
const argsCode =
(colorSpace === ColorSpace.RGB
? `gamma: ${gamma},`
: (colorSpace === ColorSpace.HSV
? `useCorrectedHSVInterpolation: ${correction},`
: ''));
// prettier-ignore
const code = `
interpolateColor(
sv.value,
[0, 1],
['${colorLeftBoundary.toUpperCase()}', '${colorRightBoundary.toUpperCase()}']
'${colorSpace}',
{
${
colorSpace === ColorSpace.RGB
? `gamma: ${gamma},`
: `useCorrectedHSVInterpolation: ${correction},`
}
}
${argsCode ? `{
${argsCode}
}` : ''}
)
`;

Expand All @@ -81,7 +86,7 @@ export default function useInterpolateColorPlayground() {
label="Colorspace"
value={colorSpace}
onChange={(changedString) => setColorSpace(ColorSpace[changedString])}
options={['RGB', 'HSV']}
options={['RGB', 'HSV', 'LAB']}
/>
{colorSpace === ColorSpace.RGB && (
<Range
Expand Down
15 changes: 15 additions & 0 deletions packages/react-native-reanimated/src/culori/Colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

export interface LabColor {
l: number;
a: number;
b: number;
alpha?: number;
}

export interface RgbColor {
r: number;
g: number;
b: number;
alpha?: number;
}
12 changes: 12 additions & 0 deletions packages/react-native-reanimated/src/culori/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

/*
* The vast majority of the code exported by this module is a direct copy of the code from
* the culori package (see https://culorijs.org/), which deserves full credit.
*/
patrycjakalinska marked this conversation as resolved.
Show resolved Hide resolved

import oklab from './oklab';

export default {
oklab,
};
47 changes: 47 additions & 0 deletions packages/react-native-reanimated/src/culori/lrgb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';
import type { RgbColor } from './Colors';

const channelFromLrgb = (c = 0) => {
'worklet';
const abs = Math.abs(c);
if (abs > 0.0031308) {
return (Math.sign(c) || 1) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
}
return c * 12.92;
};

const convertLrgbToRgb = ({ r, g, b, alpha }: RgbColor): RgbColor => {
'worklet';
return {
r: channelFromLrgb(r),
g: channelFromLrgb(g),
b: channelFromLrgb(b),
alpha,
};
};

const channelToLrgb = (c = 0) => {
'worklet';
const abs = Math.abs(c);
if (abs <= 0.04045) {
return c / 12.92;
}
return (Math.sign(c) || 1) * Math.pow((abs + 0.055) / 1.055, 2.4);
};

const convertRgbToLrgb = ({ r, g, b, alpha }: RgbColor) => {
'worklet';
return {
r: channelToLrgb(r),
g: channelToLrgb(g),
b: channelToLrgb(b),
alpha,
};
};

export default {
convert: {
fromRgb: convertRgbToLrgb,
toRgb: convertLrgbToRgb,
},
};
101 changes: 101 additions & 0 deletions packages/react-native-reanimated/src/culori/oklab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict';
import type { LabColor, RgbColor } from './Colors';

import lrgb from './lrgb';

function convertLrgbToOklab({
r = 0,
g = 0,
b = 0,
alpha,
}: RgbColor): LabColor {
'worklet';
const L = Math.cbrt(
0.41222147079999993 * r + 0.5363325363 * g + 0.0514459929 * b
);
const M = Math.cbrt(
0.2119034981999999 * r + 0.6806995450999999 * g + 0.1073969566 * b
);
const S = Math.cbrt(
0.08830246189999998 * r + 0.2817188376 * g + 0.6299787005000002 * b
);

return {
l: 0.2104542553 * L + 0.793617785 * M - 0.0040720468 * S,
a: 1.9779984951 * L - 2.428592205 * M + 0.4505937099 * S,
b: 0.0259040371 * L + 0.7827717662 * M - 0.808675766 * S,
alpha,
};
}

function convertRgbToOklab(rgb: RgbColor) {
'worklet';
const lrgbColor = lrgb.convert.fromRgb(rgb);
const result = convertLrgbToOklab(lrgbColor);
if (rgb.r === rgb.b && rgb.b === rgb.g) {
result.a = result.b = 0;
}
return result;
}

function convertOklabToLrgb({
l = 0,
a = 0,
b = 0,
alpha,
}: LabColor): RgbColor {
'worklet';
/* eslint-disable @typescript-eslint/no-loss-of-precision */
const L = Math.pow(
l * 0.99999999845051981432 +
0.39633779217376785678 * a +
0.21580375806075880339 * b,
3
);
const M = Math.pow(
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
l * 1.0000000088817607767 -
0.1055613423236563494 * a -
0.063854174771705903402 * b,
3
);
const S = Math.pow(
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
l * 1.0000000546724109177 -
0.089484182094965759684 * a -
1.2914855378640917399 * b,
3
);
/* eslint-enable */

return {
r: +4.076741661347994 * L - 3.307711590408193 * M + 0.230969928729428 * S,
g:
-1.2684380040921763 * L + 2.6097574006633715 * M - 0.3413193963102197 * S,
b:
-0.004196086541837188 * L -
0.7034186144594493 * M +
1.7076147009309444 * S,
alpha,
};
}

function convertOklabToRgb(labColor: LabColor): RgbColor {
'worklet';
const roundChannel = (channel: number) =>
Math.ceil(channel * 100_000) / 100_000;

const lrgbColor = convertOklabToLrgb(labColor);
const rgbColor = lrgb.convert.toRgb(lrgbColor);
rgbColor.r = roundChannel(rgbColor.r);
rgbColor.g = roundChannel(rgbColor.g);
rgbColor.b = roundChannel(rgbColor.b);
return rgbColor;
}

export default {
convert: {
fromRgb: convertRgbToOklab,
toRgb: convertOklabToRgb,
},
};
Loading