From 6bf9b98232eb48e9cb0767a04ea99f9b0c736b61 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 9 Sep 2020 08:40:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(=F0=9F=8E=A8):=20color=20worklets=20(#336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Colors.ts | 226 ++++++++++++++++++++++++++++++++++ src/Coordinates.ts | 2 +- src/{Vector.ts => Vectors.ts} | 0 src/index.ts | 3 +- 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 src/Colors.ts rename src/{Vector.ts => Vectors.ts} (100%) diff --git a/src/Colors.ts b/src/Colors.ts new file mode 100644 index 00000000..3243fe4f --- /dev/null +++ b/src/Colors.ts @@ -0,0 +1,226 @@ +import { Platform } from "react-native"; +import { + interpolate, + Extrapolate, + processColor, +} from "react-native-reanimated"; + +import { clamp, mix } from "./Math"; + +declare let _WORKLET: boolean; + +export type Color = string | number; +export enum ColorSpace { + RGB, + HSV, +} + +const fract = (v: number) => { + "worklet"; + return v - Math.floor(v); +}; + +export const opacity = (c: number) => { + "worklet"; + return ((c >> 24) & 255) / 255; +}; + +export const red = (c: number) => { + "worklet"; + return (c >> 16) & 255; +}; + +export const green = (c: number) => { + "worklet"; + return (c >> 8) & 255; +}; + +export const blue = (c: number) => { + "worklet"; + return c & 255; +}; + +export const color = (r: number, g: number, b: number, alpha = 1) => { + "worklet"; + if (Platform.OS === "web" || !_WORKLET) { + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } + const a = alpha * 255; + const c = + a * (1 << 24) + + Math.round(r) * (1 << 16) + + Math.round(g) * (1 << 8) + + Math.round(b); + if (Platform.OS === "android") { + // on Android color is represented as signed 32 bit int + return c < (1 << 31) >>> 0 ? c : c - Math.pow(2, 32); + } + return c; +}; + +export const hsv2rgb = (h: number, s: number, v: number) => { + "worklet"; + // vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + const K = { + x: 1, + y: 2 / 3, + z: 1 / 3, + w: 3, + }; + // vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + const p = { + x: Math.abs(fract(h + K.x) * 6 - K.w), + y: Math.abs(fract(h + K.y) * 6 - K.w), + z: Math.abs(fract(h + K.z) * 6 - K.w), + }; + // return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); + const rgb = { + x: v * mix(s, K.x, clamp(p.x - K.x, 0, 1)), + y: v * mix(s, K.x, clamp(p.y - K.x, 0, 1)), + z: v * mix(s, K.x, clamp(p.z - K.x, 0, 1)), + }; + return { + r: Math.round(rgb.x * 255), + g: Math.round(rgb.y * 255), + b: Math.round(rgb.z * 255), + }; +}; + +export const hsv2color = (h: number, s: number, v: number) => { + "worklet"; + const { r, g, b } = hsv2rgb(h, s, v); + return color(r, g, b); +}; + +export const colorForBackground = (r: number, g: number, b: number) => { + "worklet"; + const L = 0.299 * r + 0.587 * g + 0.114 * b; + return L > 186 ? 0x000000ff : 0xffffffff; +}; + +const rgbToHsv = (c: number) => { + "worklet"; + const r = red(c) / 255; + const g = green(c) / 255; + const b = blue(c) / 255; + + const ma = Math.max(r, g, b); + const mi = Math.min(r, g, b); + let h = 0; + const v = ma; + + const d = ma - mi; + const s = ma === 0 ? 0 : d / ma; + if (ma === mi) { + h = 0; // achromatic + } else { + switch (ma) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + default: // do nothing + } + h /= 6; + } + return { h, s, v }; +}; + +const interpolateColorsHSV = ( + value: number, + inputRange: number[], + colors: number[] +) => { + "worklet"; + const colorsAsHSV = colors.map((c) => rgbToHsv(c)); + const h = interpolate( + value, + inputRange, + colorsAsHSV.map((c) => c.h), + Extrapolate.CLAMP + ); + const s = interpolate( + value, + inputRange, + colorsAsHSV.map((c) => c.s), + Extrapolate.CLAMP + ); + const v = interpolate( + value, + inputRange, + colorsAsHSV.map((c) => c.v), + Extrapolate.CLAMP + ); + return hsv2color(h, s, v); +}; + +const interpolateColorsRGB = ( + value: number, + inputRange: number[], + colors: number[] +) => { + "worklet"; + const r = Math.round( + interpolate( + value, + inputRange, + colors.map((c) => red(c)), + Extrapolate.CLAMP + ) + ); + const g = Math.round( + interpolate( + value, + inputRange, + colors.map((c) => green(c)), + Extrapolate.CLAMP + ) + ); + const b = Math.round( + interpolate( + value, + inputRange, + colors.map((c) => blue(c)), + Extrapolate.CLAMP + ) + ); + const a = interpolate( + value, + inputRange, + colors.map((c) => opacity(c)), + Extrapolate.CLAMP + ); + return color(r, g, b, a); +}; + +export const interpolateColor = ( + value: number, + inputRange: number[], + rawOutputRange: Color[], + colorSpace: ColorSpace = ColorSpace.RGB +) => { + "worklet"; + const outputRange = rawOutputRange.map((c) => + typeof c === "number" ? c : processColor(c) + ); + if (colorSpace === ColorSpace.HSV) { + return interpolateColorsHSV(value, inputRange, outputRange); + } + const result = interpolateColorsRGB(value, inputRange, outputRange); + return result; +}; + +export const mixColor = ( + value: number, + color1: Color, + color2: Color, + colorSpace: ColorSpace = ColorSpace.RGB +) => { + "worklet"; + return interpolateColor(value, [0, 1], [color1, color2], colorSpace); +}; diff --git a/src/Coordinates.ts b/src/Coordinates.ts index 827e49fb..226738bd 100644 --- a/src/Coordinates.ts +++ b/src/Coordinates.ts @@ -1,4 +1,4 @@ -import { Vector } from "./Vector"; +import { Vector } from "./Vectors"; export interface PolarPoint { theta: number; diff --git a/src/Vector.ts b/src/Vectors.ts similarity index 100% rename from src/Vector.ts rename to src/Vectors.ts diff --git a/src/index.ts b/src/index.ts index 890dda67..671f215f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,6 @@ export * from "./Animations"; export * from "./Coordinates"; export * from "./Transitions"; export * from "./Math"; -export * from "./Vector"; +export * from "./Vectors"; +export * from "./Colors"; export { default as ReText } from "./ReText";