From c9a0772c5b6d871455f899034582cfd401f26a91 Mon Sep 17 00:00:00 2001 From: Dan Burzo Date: Mon, 13 May 2019 23:19:24 +0300 Subject: [PATCH 1/7] WIP support for color stops in interpolate() --- benchmark/tests/interpolate-speed.js | 10 ++++++++ package.json | 3 ++- src/interpolate/interpolate.js | 36 +++++++++++++++++++++++++++- src/util/normalizeOffsets.js | 31 ++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 src/util/normalizeOffsets.js diff --git a/benchmark/tests/interpolate-speed.js b/benchmark/tests/interpolate-speed.js index 4e6c6c71..1015ae7e 100644 --- a/benchmark/tests/interpolate-speed.js +++ b/benchmark/tests/interpolate-speed.js @@ -77,6 +77,16 @@ benchmark('culori: multiple colors in RGB', () => { } }); +benchmark('culori: multiple colors in RGB (cached)', () => { + let lerp = culori.interpolate(colors); + for (var i = 0; i < iterations; i++) { + culori + .samples(count) + .map(lerp) + .map(hex); + } +}); + let scale2 = chroma.scale(colors); benchmark('chroma: multiple colors in RGB (cached)', () => { for (var i = 0; i < iterations; i++) { diff --git a/package.json b/package.json index 07965711..5229a45f 100644 --- a/package.json +++ b/package.json @@ -71,5 +71,6 @@ "jsxBracketSameLine": false, "arrowParens": "avoid", "printWidth": 80 - } + }, + "dependencies": {} } diff --git a/src/interpolate/interpolate.js b/src/interpolate/interpolate.js index 597b6db5..3f42cb73 100644 --- a/src/interpolate/interpolate.js +++ b/src/interpolate/interpolate.js @@ -1,10 +1,28 @@ import converter from '../converter'; import { getModeDefinition } from '../modes'; +import normalizeOffsets from '../util/normalizeOffsets'; +import samples from '../samples'; export default (colors, mode = 'rgb', interpolations) => { let def = getModeDefinition(mode); + let conv = converter(mode); - let converted = colors.map(converter(mode)); + let converted = []; + let offsets = []; + + colors.forEach(c => { + if (Array.isArray(c)) { + converted.push(conv(c[0])); + offsets.push(c[1]); + } else if (typeof c === 'number') { + // TODO: support for color hints + } else { + converted.push(conv(c)); + offsets.push(undefined); + } + }); + + normalizeOffsets(offsets); let zipped = def.channels.reduce((res, channel) => { res[channel] = converted.map(color => color[channel]); @@ -28,6 +46,22 @@ export default (colors, mode = 'rgb', interpolations) => { // clamp t to the [0, 1] interval t = Math.min(Math.max(0, t), 1); + if (t < offsets[0]) { + return converted[0]; + } + + if (t > offsets[offsets.length - 1]) { + return converted[converted.length - 1]; + } + + // TODO: figure this out + // let idx = offsets.findIndex(offset => offset > t); + // let start = offsets[idx - 1]; + // let end = offsets[idx]; + // let pc = start + (end - start) + // let start_range = (idx-1) / (offsets.length - 1); + // let end_range = idx / (offsets.length - 1); + return def.channels.reduce( (res, channel) => { let val = interpolators[channel](t); diff --git a/src/util/normalizeOffsets.js b/src/util/normalizeOffsets.js new file mode 100644 index 00000000..7ecf93d4 --- /dev/null +++ b/src/util/normalizeOffsets.js @@ -0,0 +1,31 @@ +/* + Note: this method does not make a defensive copy of the array. + Instead, it adjusts the offsets in place. + */ +export default arr => { + // let arr = offsets.slice(); + if (arr[0] === undefined) arr[0] = 0; + if (arr[arr.length - 1] === undefined) arr[arr.length - 1] = 1; + let i = 1, + j, + start, + start_offset, + increment; + while (i < arr.length) { + if (arr[i] === undefined) { + start = i; + start_offset = arr[i - 1]; + j = i; + while (arr[j] === undefined) j++; + increment = (arr[j] - start_offset) / (j - i + 1); + while (i < j) { + arr[i] = start_offset + (i - start + 1) * increment; + i++; + } + } else if (arr[i] < arr[i - 1]) { + arr[i] = arr[i - 1]; + } + i++; + } + // return arr; +}; From 0696b33a93c4b1a1aca0c732985efb65e267fb26 Mon Sep 17 00:00:00 2001 From: Dan Burzo Date: Tue, 14 May 2019 00:05:34 +0300 Subject: [PATCH 2/7] Wrap up formulas --- benchmark/index.js | 4 ++-- src/interpolate/interpolate.js | 38 +++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/benchmark/index.js b/benchmark/index.js index 6cbd9c02..82f1b664 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -1,3 +1,3 @@ -require('./tests/rgb-parse-speed.js'); -require('./tests/namedcolors-parse-speed.js'); +// require('./tests/rgb-parse-speed.js'); +// require('./tests/namedcolors-parse-speed.js'); require('./tests/interpolate-speed.js'); diff --git a/src/interpolate/interpolate.js b/src/interpolate/interpolate.js index 3f42cb73..3bf94646 100644 --- a/src/interpolate/interpolate.js +++ b/src/interpolate/interpolate.js @@ -10,14 +10,14 @@ export default (colors, mode = 'rgb', interpolations) => { let converted = []; let offsets = []; - colors.forEach(c => { - if (Array.isArray(c)) { - converted.push(conv(c[0])); - offsets.push(c[1]); - } else if (typeof c === 'number') { + colors.forEach(color => { + if (Array.isArray(color)) { + converted.push(conv(color[0])); + offsets.push(color[1]); + } else if (typeof color === 'number') { // TODO: support for color hints } else { - converted.push(conv(c)); + converted.push(conv(color)); offsets.push(undefined); } }); @@ -42,29 +42,33 @@ export default (colors, mode = 'rgb', interpolations) => { } ); + let n = converted.length - 1; + return t => { // clamp t to the [0, 1] interval t = Math.min(Math.max(0, t), 1); - if (t < offsets[0]) { + if (t <= offsets[0]) { return converted[0]; } - if (t > offsets[offsets.length - 1]) { - return converted[converted.length - 1]; + if (t > offsets[n]) { + return converted[n]; } - // TODO: figure this out - // let idx = offsets.findIndex(offset => offset > t); - // let start = offsets[idx - 1]; - // let end = offsets[idx]; - // let pc = start + (end - start) - // let start_range = (idx-1) / (offsets.length - 1); - // let end_range = idx / (offsets.length - 1); + // Convert `t` from [0, 1] to `t0` between the appropriate two colors. + // First, look for the two colors between which `t` is located. + // Note: this can be optimized by searching for the index + // through bisection instead of start-to-end. + let idx = 0; + while (offsets[idx] < t) idx++; + let start = offsets[idx - 1]; + let end = offsets[idx]; + let t0 = (idx - 1 + (t - start) / (end - start)) / n; return def.channels.reduce( (res, channel) => { - let val = interpolators[channel](t); + let val = interpolators[channel](t0); if (val !== undefined) { res[channel] = val; } From 6c24b507b6f48272e154290cf524259352b62383 Mon Sep 17 00:00:00 2001 From: Dan Burzo Date: Tue, 14 May 2019 00:24:45 +0300 Subject: [PATCH 3/7] Cleanup --- src/interpolate/interpolate.js | 36 ++++++++++++------------ src/util/normalizeOffsets.js | 31 --------------------- src/util/normalizePositions.js | 50 ++++++++++++++++++++++++++++++++++ test/interpolate.test.js | 3 +- 4 files changed, 69 insertions(+), 51 deletions(-) delete mode 100644 src/util/normalizeOffsets.js create mode 100644 src/util/normalizePositions.js diff --git a/src/interpolate/interpolate.js b/src/interpolate/interpolate.js index 3bf94646..e413ea1a 100644 --- a/src/interpolate/interpolate.js +++ b/src/interpolate/interpolate.js @@ -1,31 +1,31 @@ import converter from '../converter'; import { getModeDefinition } from '../modes'; -import normalizeOffsets from '../util/normalizeOffsets'; +import normalizePositions from '../util/normalizePositions'; import samples from '../samples'; export default (colors, mode = 'rgb', interpolations) => { let def = getModeDefinition(mode); let conv = converter(mode); - let converted = []; - let offsets = []; + let conv_colors = []; + let positions = []; colors.forEach(color => { if (Array.isArray(color)) { - converted.push(conv(color[0])); - offsets.push(color[1]); + conv_colors.push(conv(color[0])); + positions.push(color[1]); } else if (typeof color === 'number') { - // TODO: support for color hints + // TODO: add support for color hints } else { - converted.push(conv(color)); - offsets.push(undefined); + conv_colors.push(conv(color)); + positions.push(undefined); } }); - normalizeOffsets(offsets); + normalizePositions(positions); let zipped = def.channels.reduce((res, channel) => { - res[channel] = converted.map(color => color[channel]); + res[channel] = conv_colors.map(color => color[channel]); return res; }, {}); @@ -42,18 +42,18 @@ export default (colors, mode = 'rgb', interpolations) => { } ); - let n = converted.length - 1; + let n = conv_colors.length - 1; return t => { // clamp t to the [0, 1] interval t = Math.min(Math.max(0, t), 1); - if (t <= offsets[0]) { - return converted[0]; + if (t <= positions[0]) { + return conv_colors[0]; } - if (t > offsets[n]) { - return converted[n]; + if (t > positions[n]) { + return conv_colors[n]; } // Convert `t` from [0, 1] to `t0` between the appropriate two colors. @@ -61,9 +61,9 @@ export default (colors, mode = 'rgb', interpolations) => { // Note: this can be optimized by searching for the index // through bisection instead of start-to-end. let idx = 0; - while (offsets[idx] < t) idx++; - let start = offsets[idx - 1]; - let end = offsets[idx]; + while (positions[idx] < t) idx++; + let start = positions[idx - 1]; + let end = positions[idx]; let t0 = (idx - 1 + (t - start) / (end - start)) / n; return def.channels.reduce( diff --git a/src/util/normalizeOffsets.js b/src/util/normalizeOffsets.js deleted file mode 100644 index 7ecf93d4..00000000 --- a/src/util/normalizeOffsets.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - Note: this method does not make a defensive copy of the array. - Instead, it adjusts the offsets in place. - */ -export default arr => { - // let arr = offsets.slice(); - if (arr[0] === undefined) arr[0] = 0; - if (arr[arr.length - 1] === undefined) arr[arr.length - 1] = 1; - let i = 1, - j, - start, - start_offset, - increment; - while (i < arr.length) { - if (arr[i] === undefined) { - start = i; - start_offset = arr[i - 1]; - j = i; - while (arr[j] === undefined) j++; - increment = (arr[j] - start_offset) / (j - i + 1); - while (i < j) { - arr[i] = start_offset + (i - start + 1) * increment; - i++; - } - } else if (arr[i] < arr[i - 1]) { - arr[i] = arr[i - 1]; - } - i++; - } - // return arr; -}; diff --git a/src/util/normalizePositions.js b/src/util/normalizePositions.js new file mode 100644 index 00000000..be82201b --- /dev/null +++ b/src/util/normalizePositions.js @@ -0,0 +1,50 @@ +/* + Normalize an array of color stop positions for a gradient + based on the rules defined in the CSS Images Module 4 spec: + + 1. make the first position 0 and the last position 1 if missing + 2. sequences of unpositioned color stops should be spread out evenly + 3. no position can be smaller than any of the ones preceding it + + Reference: https://drafts.csswg.org/css-images-4/#color-stop-fixup + + Note: this method does not make a defensive copy of the array + it receives as argument. Instead, it adjusts the values in-place. + */ +export default arr => { + // 1. fix up first/last position if missing + if (arr[0] === undefined) { + arr[0] = 0; + } + if (arr[arr.length - 1] === undefined) { + arr[arr.length - 1] = 1; + } + + let i = 1; + let j; + let from_idx; + let from_pos; + let inc; + while (i < arr.length) { + // 2. fill up undefined positions + if (arr[i] === undefined) { + from_idx = i; + from_pos = arr[i - 1]; + j = i; + + // find end of `undefined` sequence... + while (arr[j] === undefined) j++; + + // ...and add evenly-spread positions + inc = (arr[j] - from_pos) / (j - i + 1); + while (i < j) { + arr[i] = from_pos + (i + 1 - from_idx) * inc; + i++; + } + } else if (arr[i] < arr[i - 1]) { + // 3. make positions increase + arr[i] = arr[i - 1]; + } + i++; + } +}; diff --git a/test/interpolate.test.js b/test/interpolate.test.js index 03e6a507..6ba27431 100644 --- a/test/interpolate.test.js +++ b/test/interpolate.test.js @@ -108,8 +108,7 @@ tape('interpolate between black and white in RGB/RGBA', function(test) { r: 1, g: 1, b: 1, - mode: 'rgb', - alpha: 1 + mode: 'rgb' }); test.deepEqual(rgb(grays(0.1)), { r: 0.9, From 257e02562f41a517104b6986b1f61483ce30efe1 Mon Sep 17 00:00:00 2001 From: Dan Burzo Date: Tue, 14 May 2019 10:15:03 +0300 Subject: [PATCH 4/7] tape tests with es modules import --- package.json | 8 ++++---- test/blend.test.js | 6 ++---- test/clamp.test.js | 12 ++++++------ test/css.test.js | 5 ++--- test/cubehelix.test.js | 4 +--- test/difference.test.js | 7 +++---- test/displayable.test.js | 16 ++++++++-------- test/formatter.test.js | 12 ++++++------ test/hsi.test.js | 5 ++--- test/hsl.test.js | 5 ++--- test/hsv.test.js | 5 ++--- test/hwb.test.js | 5 ++--- test/interpolate.test.js | 5 ++--- test/interpolateHue.test.js | 5 ++--- test/lch.test.js | 5 ++--- test/nearest.test.js | 5 ++--- test/parse.test.js | 5 ++--- test/random.test.js | 6 ++---- test/rgb.test.js | 5 ++--- test/samples.test.js | 5 ++--- test/yiq.test.js | 5 ++--- yarn.lock | 5 +++++ 22 files changed, 63 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index 5229a45f..2678a1f1 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ ], "devDependencies": { "eslint": "^5.7.0", + "esm": "^3.2.22", "gh-pages": "^2.0.1", "husky": "^1.1.2", "nyc": "^13.1.0", @@ -45,12 +46,11 @@ }, "scripts": { "build": "rollup -c", - "pretest": "yarn build", - "test": "tape test/*.js | tap-spec", + "test": "tape -r esm test/*.js | tap-spec", "benchmark": "node benchmark/index.js", - "prepublishOnly": "yarn test", + "prepublishOnly": "yarn build && yarn test", "coverage:report": "nyc report --reporter=lcov", - "coverage:test": "nyc --produce-source-map tape test/*.js | tap-spec", + "coverage:test": "nyc --produce-source-map tape -r esm test/*.js | tap-spec", "docs:start": "hugo serve --source docs", "docs:build": "hugo --source docs", "docs:deploy": "yarn docs:build && gh-pages -d docs/public", diff --git a/test/blend.test.js b/test/blend.test.js index ff7dfc48..f7a7bae0 100644 --- a/test/blend.test.js +++ b/test/blend.test.js @@ -1,7 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); - -let { blend } = culori; +import tape from 'tape'; +import { blend } from '../src/index'; tape('blendNormal', function(test) { test.deepEqual(blend(['white', 'rgba(0, 0, 0, 0.5)']), { diff --git a/test/clamp.test.js b/test/clamp.test.js index 95927ad1..963761ba 100644 --- a/test/clamp.test.js +++ b/test/clamp.test.js @@ -1,12 +1,12 @@ -let tape = require('tape'); -let culori = require('../'); +import tape from 'tape'; +import { clamp } from '../src/index'; -let clamp = culori.clamp('lch'); +let _clamp = clamp('lch'); tape('RGB', function(test) { - test.deepEqual(clamp('red'), { mode: 'rgb', r: 1, g: 0, b: 0 }); + test.deepEqual(_clamp('red'), { mode: 'rgb', r: 1, g: 0, b: 0 }); - test.deepEqual(clamp('rgb(300, 255, 255)'), { + test.deepEqual(_clamp('rgb(300, 255, 255)'), { mode: 'rgb', r: 1, g: 1, @@ -17,7 +17,7 @@ tape('RGB', function(test) { }); tape('LCh', function(test) { - test.deepEqual(clamp('lch(50 120 5)'), { + test.deepEqual(_clamp('lch(50 120 5)'), { mode: 'lch', l: 50, c: 77.48291015625, diff --git a/test/css.test.js b/test/css.test.js index d2be59d6..9ff117e9 100644 --- a/test/css.test.js +++ b/test/css.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { formatter, rgb } = culori; +import tape from 'tape'; +import { formatter, rgb } from '../src/index'; tape('formatter()', function(test) { test.deepEqual( diff --git a/test/cubehelix.test.js b/test/cubehelix.test.js index b07e4472..5c96fcff 100644 --- a/test/cubehelix.test.js +++ b/test/cubehelix.test.js @@ -1,6 +1,4 @@ -let tape = require('tape'); -// let culori = require('../'); -// let { cubehelix, rgb } = culori; +import tape from 'tape'; tape('RGB -> Cubehelix', function(test) { // test.deepEqual( diff --git a/test/difference.test.js b/test/difference.test.js index 68091276..07c4e7ba 100644 --- a/test/difference.test.js +++ b/test/difference.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { +import tape from 'tape'; +import { differenceEuclidean, differenceCie76, differenceCie94, @@ -11,7 +10,7 @@ let { lab, round, hsl -} = culori; +} from '../src/index'; tape('euclidean distance in RGB', function(test) { let delta = differenceEuclidean(); diff --git a/test/displayable.test.js b/test/displayable.test.js index 67b95124..5e804519 100644 --- a/test/displayable.test.js +++ b/test/displayable.test.js @@ -1,16 +1,16 @@ -let tape = require('tape'); -let culori = require('../'); +import tape from 'tape'; +import { displayable } from '../src/index'; tape('RGB', function(test) { - test.equal(culori.displayable({ mode: 'rgb', r: 0, g: 0, b: 0 }), true); + test.equal(displayable({ mode: 'rgb', r: 0, g: 0, b: 0 }), true); test.equal( - culori.displayable({ mode: 'rgb', r: 1, g: 1, b: 1, alpha: 0.5 }), + displayable({ mode: 'rgb', r: 1, g: 1, b: 1, alpha: 0.5 }), true ); test.equal( - culori.displayable({ mode: 'rgb', r: 1.1, g: 1, b: 1, alpha: 0.5 }), + displayable({ mode: 'rgb', r: 1.1, g: 1, b: 1, alpha: 0.5 }), false ); @@ -18,11 +18,11 @@ tape('RGB', function(test) { }); tape('LCh', function(test) { - test.equal(culori.displayable('lch(50 0 0)'), true); + test.equal(displayable('lch(50 0 0)'), true); - test.equal(culori.displayable('lch(50 -100 0)'), true); + test.equal(displayable('lch(50 -100 0)'), true); - test.equal(culori.displayable('lch(120 -100 0)'), false); + test.equal(displayable('lch(120 -100 0)'), false); test.end(); }); diff --git a/test/formatter.test.js b/test/formatter.test.js index ac7c7cf0..cf4c8a9d 100644 --- a/test/formatter.test.js +++ b/test/formatter.test.js @@ -1,8 +1,8 @@ -let tape = require('tape'); -let culori = require('../'); +import tape from 'tape'; +import { formatter, rgb } from '../src/index'; tape('formatter(hex)', function(test) { - let hex = culori.formatter('hex'); + let hex = formatter('hex'); test.equal(hex('tomato'), '#ff6347'); @@ -10,10 +10,10 @@ tape('formatter(hex)', function(test) { }); tape('formatter(rgb)', function(test) { - let rgb = culori.formatter('rgb'); + let torgb = formatter('rgb'); - test.equal(rgb(culori.rgb('#f0f0f0f0')), 'rgba(240, 240, 240, 0.94)'); - test.equal(rgb('#f0f0f0f0'), 'rgba(240, 240, 240, 0.94)'); + test.equal(torgb(rgb('#f0f0f0f0')), 'rgba(240, 240, 240, 0.94)'); + test.equal(torgb('#f0f0f0f0'), 'rgba(240, 240, 240, 0.94)'); test.end(); }); diff --git a/test/hsi.test.js b/test/hsi.test.js index 56eb8cfd..4370ca4c 100644 --- a/test/hsi.test.js +++ b/test/hsi.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { hsi, rgb } = culori; +import tape from 'tape'; +import { hsi, rgb } from '../src/index'; tape('rgb() converts from HSI to RGB', function(test) { test.deepEqual( diff --git a/test/hsl.test.js b/test/hsl.test.js index daa32ca6..c4217eba 100644 --- a/test/hsl.test.js +++ b/test/hsl.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { hsl, rgb } = culori; +import tape from 'tape'; +import { hsl, rgb } from '../src/index'; tape('rgb() converts from HSL to RGB', function(test) { test.deepEqual( diff --git a/test/hsv.test.js b/test/hsv.test.js index bd7beead..30793867 100644 --- a/test/hsv.test.js +++ b/test/hsv.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { hsv, rgb } = culori; +import tape from 'tape'; +import { hsv, rgb } from '../src/index'; tape('rgb() converts from HSV to RGB', function(test) { test.deepEqual( diff --git a/test/hwb.test.js b/test/hwb.test.js index a9c487ed..699b357c 100644 --- a/test/hwb.test.js +++ b/test/hwb.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { hwb } = culori; +import tape from 'tape'; +import { hwb } from '../src/index'; tape('hwb() parses hwb CSS strings', function(test) { test.deepEqual( diff --git a/test/interpolate.test.js b/test/interpolate.test.js index 6ba27431..d5f7f381 100644 --- a/test/interpolate.test.js +++ b/test/interpolate.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { interpolate, formatter, rgb, samples } = culori; +import tape from 'tape'; +import { interpolate, formatter, rgb, samples } from '../src/index'; let hex = formatter('hex'); diff --git a/test/interpolateHue.test.js b/test/interpolateHue.test.js index a76de27d..92dffd89 100644 --- a/test/interpolateHue.test.js +++ b/test/interpolateHue.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { interpolateHue, formatter } = culori; +import tape from 'tape'; +import { interpolateHue, formatter } from '../src/index'; tape('interpolate/hue', function(test) { test.deepEqual(interpolateHue([0, 360]), [0, 0], '[0, 360]'); diff --git a/test/lch.test.js b/test/lch.test.js index 1f62c958..c3a36aa2 100644 --- a/test/lch.test.js +++ b/test/lch.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -// let culori = require('../'); -// let { round, rgb, convert, lch } = culori; +import tape from 'tape'; +import { nearest, colorsNamed } from '../src/index'; tape('convert(lch)', function(test) { // test.deepEqual( diff --git a/test/nearest.test.js b/test/nearest.test.js index b70fa991..504b7885 100644 --- a/test/nearest.test.js +++ b/test/nearest.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { nearest, colorsNamed } = culori; +import tape from 'tape'; +import { nearest, colorsNamed } from '../src/index'; let nearestNamedColor = nearest(Object.keys(colorsNamed)); diff --git a/test/parse.test.js b/test/parse.test.js index cde91771..753b3e56 100644 --- a/test/parse.test.js +++ b/test/parse.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { parse } = culori; +import tape from 'tape'; +import { parse } from '../src/index'; tape('named colors', function(test) { test.deepEqual( diff --git a/test/random.test.js b/test/random.test.js index 86ced915..9be7eef0 100644 --- a/test/random.test.js +++ b/test/random.test.js @@ -1,7 +1,5 @@ -let tape = require('tape'); -let culori = require('..'); - -let { random } = culori; +import tape from 'tape'; +import { random } from '../src/index'; tape('random (rgb)', test => { let c1 = random(); diff --git a/test/rgb.test.js b/test/rgb.test.js index 9a45f065..5720ceca 100644 --- a/test/rgb.test.js +++ b/test/rgb.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { rgb, formatter } = culori; +import tape from 'tape'; +import { rgb, formatter } from '../src/index'; tape('rgb(Specifier)', function(test) { test.deepEqual(formatter('hex')(rgb('#ffffff')), '#ffffff'); diff --git a/test/samples.test.js b/test/samples.test.js index 6e755ef9..f5533a28 100644 --- a/test/samples.test.js +++ b/test/samples.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { interpolate, samples, formatter } = culori; +import tape from 'tape'; +import { interpolate, samples, formatter } from '../src/index'; let hex = formatter('hex'); diff --git a/test/yiq.test.js b/test/yiq.test.js index d602c282..ebf7223a 100644 --- a/test/yiq.test.js +++ b/test/yiq.test.js @@ -1,6 +1,5 @@ -let tape = require('tape'); -let culori = require('../'); -let { yiq, formatter } = culori; +import tape from 'tape'; +import { yiq, formatter } from '../src/index'; let rgb = formatter('rgb'); diff --git a/yarn.lock b/yarn.lock index 84d17d87..cfda5cfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -541,6 +541,11 @@ eslint@^5.7.0: table "^5.0.2" text-table "^0.2.0" +esm@^3.2.22: + version "3.2.22" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.22.tgz#5062c2e22fee3ccfee4e8f20da768330da90d6e3" + integrity sha512-z8YG7U44L82j1XrdEJcqZOLUnjxco8pO453gKOlaMD1/md1n/5QrscAmYG+oKUspsmDLuBFZrpbxI6aQ67yRxA== + espree@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634" From 76b1b9f20cf1379d226b6fb6e9ab97a4df296042 Mon Sep 17 00:00:00 2001 From: Dan Burzo Date: Tue, 14 May 2019 13:25:04 +0300 Subject: [PATCH 5/7] Update docs --- .github/evenly-spaced-vs-positions.png | Bin 0 -> 30566 bytes README.md | 38 +++++++++++++++++++++++-- benchmark/index.js | 4 +-- benchmark/package.json | 2 +- benchmark/yarn.lock | 7 +++-- 5 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 .github/evenly-spaced-vs-positions.png diff --git a/.github/evenly-spaced-vs-positions.png b/.github/evenly-spaced-vs-positions.png new file mode 100644 index 0000000000000000000000000000000000000000..806af4d561820798e94b49d11e9299cb6bd709d5 GIT binary patch literal 30566 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 b*b_OotMbVO55bRfh0JbQ-K@OvDEhwuNG{yx literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 73141b08..8394617f 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,25 @@ let custom_interpolator = culori.interpolate(['blue', 'red'], 'lch', { There are a few interpolation methods available, listed below. Depending on the channel, the numeric values can be interpreted/interpolated in various _modes_. The hue channel, for example, is interpolated by taking into account the _shortest path around the hue circle_ (`interpolateHue`). And the `interpolateAlpha` mode assumes an _undefined_ alpha is `1`. +#### Color stop positions + +You can specify positions of color stops to interpolate in the way they're defined in the [CSS Images Module Level 4][css-images-4] specification: + +```js +culori.interpolate(['red', ['green', 0.25], 'blue']); +``` + +In the image below, you can see the effect of interpolating with evenly-spaced colors (1) vs. positioned colors stops (2): + + + +To specify a positioned color stop, use an array that contains the color followed by its position. For omitted (implicit) positions, we apply the rules from the spec: + +1. if the first color doesn't have a position, it's assumed to be `0`; if the last color doesn't have a position, it's assumed to be `1`; +2. any other color stops that don't have a position will be evenly distributed along the gradient line. + +Color stop positions should be in increasing order. + #### Interpolation methods You'll use these methods when you want to override how colors get interpolated in a specific color space, or when defining the default interpolation for custom color spaces. @@ -466,7 +485,11 @@ culori.random(); // => { mode: 'rgb', r: 0.75, g: 0.12, b: 0.99 } ``` -You can specify constraints for each individual channel in the color space, as either a _fixed number_ or an _interval_: +#### Specifying constraints + +Random colors are, by definition, all over the color space and not all of them will look particularly nice. Some color spaces, such as HSL or HSV, are also biased towards colors close to black and/or white, because of the way these color spaces stretch the RGB cube into cyllinders. + +For more control on how the colors are generated, you can specify constraints for each individual channel in the color space. Constraints can be either a _constant number_ or an _interval_ from where to pick the channel value: ```js culori.random('hsv', { @@ -475,9 +498,17 @@ culori.random('hsv', { }); ``` -The resulting color will not include an _alpha_ value, unless you include it in the list of constraints. +The _alpha_ channel is excluded by default. To obtain colors with random alpha values, include a constraint for `alpha`: + +```js +culori.random('lrgb', { alpha: [0, 1] }); +``` + +#### Displayable random colors + +The value for any channel in the color space for which there are no constraints will be picked from the entire range of that channel. However, some color spaces, such as LAB or LCH, don't have explicit ranges for certain channels; for these, some approximate ranges [have been pre-computed](https://github.com/evercoder/culori/blob/master/tools/ranges.js) as the limits of the displayable sRGB gamut. -The value for any channel in the color space for which there are no constraints will be picked from the entire range of that channel. However, some color spaces, such as LAB or LCH, don't have explicit ranges for certain channels; for these, some approximate ranges [have been pre-computed](https://github.com/evercoder/culori/blob/master/tools/ranges.js) as the limits of the displayable sRGB gamut. Even with these ranges in place, a combination of channel values may not be displayable. You can use [`culori.displayable()`](#culoriDisplayable) to check this, and [`culori.clamp()`](#culoriClamp) to obtain a displayable version. +Even with these ranges in place, a combination of channel values may not be displayable. Check if that's the case with [`culori.displayable()`](#culoriDisplayable), and pass the color through [`culori.clamp()`](#culoriClamp) to obtain a displayable version. ### Extending culori @@ -753,3 +784,4 @@ _Please suggest more interesting projects._ [din99ode]: https://de.wikipedia.org/wiki/DIN99-Farbraum#Farbabstandsformel [kotsarekno-ramos]: http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf [yiq]: https://en.wikipedia.org/wiki/YIQ +[css-images-4]: https://drafts.csswg.org/css-images-4/#color-stop-syntax diff --git a/benchmark/index.js b/benchmark/index.js index 82f1b664..6cbd9c02 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -1,3 +1,3 @@ -// require('./tests/rgb-parse-speed.js'); -// require('./tests/namedcolors-parse-speed.js'); +require('./tests/rgb-parse-speed.js'); +require('./tests/namedcolors-parse-speed.js'); require('./tests/interpolate-speed.js'); diff --git a/benchmark/package.json b/benchmark/package.json index 94953bf8..b4301e09 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MIT", "devDependencies": { - "chroma-js": "^1.4.0", + "chroma-js": "^2.0.3", "d3-color": "^1.2.3", "d3-interpolate": "^1.3.2", "tinycolor2": "^1.4.1" diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index 9a0a3e11..fe07a5a4 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -2,9 +2,10 @@ # yarn lockfile v1 -chroma-js@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-1.4.0.tgz#695c52e7c97617e5f687db31913503d410481ae4" +chroma-js@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.0.3.tgz#2521d4f80c4e786e00064c4a62824e38ff6557c4" + integrity sha512-2kTvZZOFSV1O81/rm99t9vmkh9jQxsHqsRRoZevDVz/VCC3yKMyPuMK8M5yHG+UMg2tV6cRoqtZtgcD92udcBw== d3-color@1: version "1.0.3" From 7d113065e3fe24fd30d343e630bd6b59176d3955 Mon Sep 17 00:00:00 2001 From: Dan Burzo Date: Tue, 14 May 2019 15:25:33 +0300 Subject: [PATCH 6/7] WIP color interpolation hints --- README.md | 2 +- src/interpolate/interpolate.js | 27 ++++++++++++++++++--------- src/util/normalizePositions.js | 1 + test/interpolate.test.js | 18 ++++++++++++++++++ test/normalizePositions.test.js | 18 ++++++++++++++++++ 5 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 test/normalizePositions.test.js diff --git a/README.md b/README.md index 8394617f..46a86f26 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ In the image below, you can see the effect of interpolating with evenly-spaced c -To specify a positioned color stop, use an array that contains the color followed by its position. For omitted (implicit) positions, we apply the rules from the spec: +To specify a positioned color stop, use an array that contains the color followed by its position. For omitted (implicit) positions, we apply the rules [from the spec][css-images-4]: 1. if the first color doesn't have a position, it's assumed to be `0`; if the last color doesn't have a position, it's assumed to be `1`; 2. any other color stops that don't have a position will be evenly distributed along the gradient line. diff --git a/src/interpolate/interpolate.js b/src/interpolate/interpolate.js index e413ea1a..bf280bbe 100644 --- a/src/interpolate/interpolate.js +++ b/src/interpolate/interpolate.js @@ -9,15 +9,17 @@ export default (colors, mode = 'rgb', interpolations) => { let conv_colors = []; let positions = []; + let hints = {}; - colors.forEach(color => { - if (Array.isArray(color)) { - conv_colors.push(conv(color[0])); - positions.push(color[1]); - } else if (typeof color === 'number') { - // TODO: add support for color hints + colors.forEach(val => { + if (Array.isArray(val)) { + conv_colors.push(conv(val[0])); + positions.push(val[1]); + } else if (typeof val === 'number') { + // Interpret numbers as color interpolation hints + hints[positions.length] = val; } else { - conv_colors.push(conv(color)); + conv_colors.push(conv(val)); positions.push(undefined); } }); @@ -63,8 +65,15 @@ export default (colors, mode = 'rgb', interpolations) => { let idx = 0; while (positions[idx] < t) idx++; let start = positions[idx - 1]; - let end = positions[idx]; - let t0 = (idx - 1 + (t - start) / (end - start)) / n; + let delta = positions[idx] - start; + + let P = (t - start) / delta; + if (hints[idx]) { + let H = (hints[idx] - start) / delta; + P = Math.pow(P, Math.log(0.5) / Math.log(H)); + } + + let t0 = (idx - 1 + P) / n; return def.channels.reduce( (res, channel) => { diff --git a/src/util/normalizePositions.js b/src/util/normalizePositions.js index be82201b..3ed2bdd4 100644 --- a/src/util/normalizePositions.js +++ b/src/util/normalizePositions.js @@ -47,4 +47,5 @@ export default arr => { } i++; } + return arr; }; diff --git a/test/interpolate.test.js b/test/interpolate.test.js index d5f7f381..248fc384 100644 --- a/test/interpolate.test.js +++ b/test/interpolate.test.js @@ -191,3 +191,21 @@ tape('bug checking', function(test) { ); test.end(); }); + +tape('color interpolation hints', t => { + [0, 0.1, 0.2, 0.5, 0.8, 1].forEach(t0 => { + t.deepEqual( + interpolate(['red', 0.5, 'green'])(t0), + interpolate(['red', 'green'])(t0) + ); + }); + + t.deepEqual(interpolate(['red', 0.2, 'green'])(0.5), { + mode: 'rgb', + r: 0.25808621995139014, + g: 0.37241162292636104, + b: 0 + }); + + t.end(); +}); diff --git a/test/normalizePositions.test.js b/test/normalizePositions.test.js new file mode 100644 index 00000000..20a08738 --- /dev/null +++ b/test/normalizePositions.test.js @@ -0,0 +1,18 @@ +import tape from 'tape'; +import normalize from '../src/util/normalizePositions'; + +tape('util: normalizePositions', t => { + t.deepEqual( + normalize([undefined, undefined, undefined, undefined, undefined]), + [0, 0.25, 0.5, 0.75, 1] + ); + + t.deepEqual(normalize([0.2, undefined, undefined, 0.8]), [ + 0.2, + 0.4, + 0.6000000000000001, + 0.8 + ]); + + t.end(); +}); From a3a918c566d37bc592fdf603fbf97a013060fba7 Mon Sep 17 00:00:00 2001 From: Dan Burzo Date: Wed, 15 May 2019 10:57:47 +0300 Subject: [PATCH 7/7] Support easing functions inside gradient --- src/interpolate/interpolate.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/interpolate/interpolate.js b/src/interpolate/interpolate.js index bf280bbe..ed3682c0 100644 --- a/src/interpolate/interpolate.js +++ b/src/interpolate/interpolate.js @@ -3,21 +3,25 @@ import { getModeDefinition } from '../modes'; import normalizePositions from '../util/normalizePositions'; import samples from '../samples'; +// Color interpolation hint exponential function +let hint = (P, H) => + H === 0 ? 1 : H === 1 ? 0 : Math.pow(P, Math.log(0.5) / Math.log(H)); + export default (colors, mode = 'rgb', interpolations) => { let def = getModeDefinition(mode); let conv = converter(mode); let conv_colors = []; let positions = []; - let hints = {}; + let fns = {}; colors.forEach(val => { if (Array.isArray(val)) { conv_colors.push(conv(val[0])); positions.push(val[1]); - } else if (typeof val === 'number') { - // Interpret numbers as color interpolation hints - hints[positions.length] = val; + } else if (typeof val === 'number' || typeof val === 'function') { + // Color interpolation hint or easing function + fns[positions.length] = val; } else { conv_colors.push(conv(val)); positions.push(undefined); @@ -68,9 +72,9 @@ export default (colors, mode = 'rgb', interpolations) => { let delta = positions[idx] - start; let P = (t - start) / delta; - if (hints[idx]) { - let H = (hints[idx] - start) / delta; - P = Math.pow(P, Math.log(0.5) / Math.log(H)); + let fn = fns[idx]; + if (fn !== undefined) { + P = typeof fn === 'number' ? hint(P, (fn - start) / delta) : fn(P); } let t0 = (idx - 1 + P) / n;