thi-ng / umbrella

⛱ Broadly scoped ecosystem & mono-repository of 199 TypeScript projects (and ~180 examples) for general purpose, functional, data driven development
https://thi.ng
Apache License 2.0
3.35k stars 149 forks source link

[@thi.ng/color] mix() updates input color #387

Closed cdaein closed 1 year ago

cdaein commented 1 year ago

Hi, I am using mix() in @thi.ng/color to manually lerp colors. After using it, when I check the original input color, it has been transformed.

  import { hsv, mix } from "@thi.ng/color";

  const col1 = hsv(0, 0.0, 1);
  const col2 = hsv(0.4, 1, 1);

  const num = 4;

  console.log(col1.buf); // [0, 0, 1, 1]
  for (let i = 0; i < num; i++) {
    // use null
    const colMid = mix(null, col1, col2, i / (num - 1));
  }
  console.log(col1.buf); // [0.4, 1, 1, 1] - it should be [0, 0, 1, 1]

It works fine if I reuse the same color object:

  let colMid = hsv();
  console.log(col1.buf); // [0, 0, 1, 1]
  for (let i = 0; i < num; i++) {
    // reuse
    mix(colMid, col1, col2, i / (num - 1));
  }
  console.log(col1.buf); // [0, 0, 1, 1] correct value

I'm wondering if I am missing anything. Thank you!

postspectacular commented 1 year ago

Sorry, @cdaein - this needs to be added to the docs! Since the colors are all "vector" based, the behavior of this function (and possibly others in the color package) is the same as most of the functions in the https://thi.ng/vectors package:

Each operation producing a vector result takes an output vector as first argument. If null, the vector given as 2nd argument will (usually) be used as output (i.e. for mutation).

So in your case this would mean if the 1st arg to mix() is null, then the 2nd arg (col1) will be used to store the result. The main reason for this design decision is that it allows the same set of functions to be used for in-place updates or to produce new results... The vectors pkg already has 900+ functions/operations, without that behavior it'd have to be double that! 🤯

Another important addition for these functions in the colors package (e.g. also analog(), clamp() etc.): If you want those functions to write its result into a new color object, always provide one of the same mode:

const a = hsv(0,1,1);
const b = hsv(0.5, 0.5, 0.5);

// result written to a (i.e. res === a)
const res = mix(null, a, b, 0.1);

// result written to a new hsv color
const res = mix(hsv(), a, b, 0.1);

// result written to new untyped raw color array, then wrapped as hsv
const res = hsv(mix([], a, b, 0.1));

// result written to a new color w/ same mode as `a`
// (useful if you don't know what mode/space a is in originally)
const res = mix(a.empty(), a, b, 0.1);

// similar to previous, but using ICopy interface instead
const res = mix(a.copy(), a, b, 0.1);

Btw. The last two examples are making use of the general ICopy and IEmpty interfaces, which are also implemented by many other types in various umbrella packages...

Hth! Will add docs for this ASAP - sorry for the confusion!

cdaein commented 1 year ago

Oh, I have missed that.. Thank you very much for the clarification and the clear examples! 🙏

postspectacular commented 1 year ago

Not your fault to miss that little note! Just pushed a commit (cda14bf16) making this behavior (hopefully) more obvious in the doc strings for all relevant functions... More pkg updates incoming (oklch support and some other improvements), new version soon!