From bf98e89a44f8696e8d8be809b6372428b9fb4079 Mon Sep 17 00:00:00 2001 From: kurkle Date: Tue, 29 Mar 2022 17:32:01 +0300 Subject: [PATCH] Add interpolate method --- .editorconfig | 14 ++++++++++++++ src/color.js | 52 +++++++++++++++++++++++++++++++++------------------ src/rgb.js | 14 ++++++++++++++ src/srgb.js | 26 ++++++++++++++++++++++++++ test/index.js | 8 ++++++++ 5 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 .editorconfig create mode 100644 src/srgb.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..063c99a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# https://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.html] +indent_style = tab +indent_size = 4 diff --git a/src/color.js b/src/color.js index bb16799..cb7cedd 100644 --- a/src/color.js +++ b/src/color.js @@ -3,11 +3,12 @@ * @module index */ +import {b2n, n2b, round} from './byte'; import {hexParse, hexString} from './hex'; -import {rgbParse, rgbString} from './rgb'; -import {hueParse, hsl2rgb, rgb2hsl, rotate, hslString} from './hue'; +import {hsl2rgb, hslString, hueParse, rgb2hsl, rotate} from './hue'; import {nameParse} from './names'; -import {b2n, n2b, round} from './byte'; +import {rgbMix, rgbParse, rgbString} from './rgb'; +import {interpolate} from './srgb'; /** * @typedef {import('./index.js').RGBA} RGBA @@ -151,29 +152,31 @@ export default class Color { * Mix another color to this color. * @param {Color} color - Color to mix in * @param {number} weight - 0..1 + * @returns {Color} */ mix(color, weight) { - const me = this; if (color) { - const c1 = me.rgb; - const c2 = color.rgb; - let w2; // using instead of undefined in the next line - const p = weight === w2 ? 0.5 : weight; - const w = 2 * p - 1; - const a = c1.a - c2.a; - const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0; - w2 = 1 - w1; - c1.r = 0xFF & w1 * c1.r + w2 * c2.r + 0.5; - c1.g = 0xFF & w1 * c1.g + w2 * c2.g + 0.5; - c1.b = 0xFF & w1 * c1.b + w2 * c2.b + 0.5; - c1.a = p * c1.a + (1 - p) * c2.a; - me.rgb = c1; + rgbMix(this._rgb, color._rgb, weight); + } + return this; + } + + /** + * Interpolate a color value between this and `color` + * @param {Color} color + * @param {number} t - 0..1 + * @returns {Color} + */ + interpolate(color, t) { + if (color) { + this._rgb = interpolate(this._rgb, color._rgb, t); } - return me; + return this; } /** * Clone + * @returns {Color} */ clone() { return new Color(this.rgb); @@ -182,6 +185,7 @@ export default class Color { /** * Set aplha * @param {number} a - the alpha [0..1] + * @returns {Color} */ alpha(a) { this._rgb.a = n2b(a); @@ -191,6 +195,7 @@ export default class Color { /** * Make clearer * @param {number} ratio - ratio [0..1] + * @returns {Color} */ clearer(ratio) { const rgb = this._rgb; @@ -200,6 +205,7 @@ export default class Color { /** * Convert to grayscale + * @returns {Color} */ greyscale() { const rgb = this._rgb; @@ -212,6 +218,7 @@ export default class Color { /** * Opaquer * @param {number} ratio - ratio [0..1] + * @returns {Color} */ opaquer(ratio) { const rgb = this._rgb; @@ -219,6 +226,10 @@ export default class Color { return this; } + /** + * Negates the rgb value + * @returns {Color} + */ negate() { const v = this._rgb; v.r = 255 - v.r; @@ -230,6 +241,7 @@ export default class Color { /** * Lighten * @param {number} ratio - ratio [0..1] + * @returns {Color} */ lighten(ratio) { modHSL(this._rgb, 2, ratio); @@ -239,6 +251,7 @@ export default class Color { /** * Darken * @param {number} ratio - ratio [0..1] + * @returns {Color} */ darken(ratio) { modHSL(this._rgb, 2, -ratio); @@ -248,6 +261,7 @@ export default class Color { /** * Saturate * @param {number} ratio - ratio [0..1] + * @returns {Color} */ saturate(ratio) { modHSL(this._rgb, 1, ratio); @@ -257,6 +271,7 @@ export default class Color { /** * Desaturate * @param {number} ratio - ratio [0..1] + * @returns {Color} */ desaturate(ratio) { modHSL(this._rgb, 1, -ratio); @@ -266,6 +281,7 @@ export default class Color { /** * Rotate * @param {number} deg - degrees to rotate + * @returns {Color} */ rotate(deg) { rotate(this._rgb, deg); diff --git a/src/rgb.js b/src/rgb.js index 3561740..d8f25e6 100644 --- a/src/rgb.js +++ b/src/rgb.js @@ -60,3 +60,17 @@ export function rgbString(v) { : `rgb(${v.r}, ${v.g}, ${v.b})` ); } + +/** + * Mix rgb2 to rgb1 by percent ratio (in place). + * @param {RGBA} rgb1 - the color (also the return value) + * @param {RGBA} rgb2 - the mixed color + * @param {number} t - 0..1 (default 0.5) + */ +export function rgbMix(rgb1, rgb2, t = 0.5) { + t = 1 - t; + rgb1.r = 0xFF & rgb1.r + t * (rgb2.r - rgb1.r) + 0.5; + rgb1.g = 0xFF & rgb1.g + t * (rgb2.g - rgb1.g) + 0.5; + rgb1.b = 0xFF & rgb1.b + t * (rgb2.b - rgb1.b) + 0.5; + rgb1.a = rgb1.a + t * (rgb2.a - rgb1.a); +} diff --git a/src/srgb.js b/src/srgb.js new file mode 100644 index 0000000..de50c18 --- /dev/null +++ b/src/srgb.js @@ -0,0 +1,26 @@ +import {b2n, n2b} from './byte'; + +/** + * @typedef {import('./index.js').RGBA} RGBA + */ + +const to = v => v <= 0.0031308 ? v * 12.92 : Math.pow(v, 1.0 / 2.4) * 1.055 - 0.055; +const from = v => v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + +/** + * @param {RGBA} rgb1 from color + * @param {RGBA} rgb2 to color + * @param {number} t 0..1 + * @returns {RGBA} interpolaced + */ +export function interpolate(rgb1, rgb2, t) { + const r = from(b2n(rgb1.r)); + const g = from(b2n(rgb1.g)); + const b = from(b2n(rgb1.b)); + return { + r: n2b(to(r + t * (from(b2n(rgb2.r)) - r))), + g: n2b(to(g + t * (from(b2n(rgb2.g)) - g))), + b: n2b(to(b + t * (from(b2n(rgb2.b)) - b))), + a: rgb1.a + t * (rgb2.a - rgb1.a) + }; +} diff --git a/test/index.js b/test/index.js index 46964ef..9d72264 100644 --- a/test/index.js +++ b/test/index.js @@ -92,3 +92,11 @@ assert.strictEqual(new Color('rgba(10, 10, 10, 0.8)').clone().rgbString(), 'rgba assert.strictEqual(new Color('invalid-color-value').lighten(0.1).hslString(), undefined); assert.strictEqual(new Color('invalid-color-value').mix(undefined, 0.5).rgbString(), undefined); + +assert.strictEqual(new Color('red').interpolate(new Color('green'), 0).rgbString(), 'rgb(255, 0, 0)'); +assert.strictEqual(new Color('red').interpolate(new Color('green'), 0.25).rgbString(), 'rgb(225, 65, 0)'); +assert.strictEqual(new Color('red').interpolate(new Color('green'), 0.5).rgbString(), 'rgb(188, 92, 0)'); +assert.strictEqual(new Color('red').interpolate(new Color('green'), 0.75).rgbString(), 'rgb(137, 112, 0)'); +assert.strictEqual(new Color('red').interpolate(new Color('green'), 1).rgbString(), 'rgb(0, 128, 0)'); + +assert.strictEqual(new Color('#fefa').interpolate(new Color('#ced5'), 0.6).hexString(), '#E2EDEC77');