Qix- / color-convert

Plain color conversion functions in JavaScript
MIT License
746 stars 96 forks source link

Preval proof of concept. #63

Closed wtgtybhertgeghgtwtg closed 3 years ago

wtgtybhertgeghgtwtg commented 5 years ago

Builds off #59.

Qix- commented 5 years ago

Interesting! Thank you for doing this, this is really cool.

This dumps the generated graph at build time but still runs everything through the linker, which does the composition upon require. How difficult would it be to basically map all of the functions at build time, too?

wtgtybhertgeghgtwtg commented 5 years ago

How difficult would it be to basically map all of the functions at build time, too?

At the very least, it wouldn't be easy. I think the best I could do would be to get the path (i.e. ['hex', 'rgb', 'hsl'] or whatever) for each node at build-time, but it'll still have to compose the actual functions. So something like

// `modelPaths` would probably actually be an array to skip the runtime `Object.entries` or whatever.
for (const [fromModel, paths] of Object.entries(modelPaths)) {
    convert[fromModel] = {};

    Object.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels});
    Object.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels});

    for (const [toModel, path] of Object.entries(paths)) {
        // Takes ['hex', 'rgb', 'hsl'] and returns the function chaining that together.
        const fn = composeConversion(path)

        convert[fromModel][toModel] = wrapRounded(fn);
        convert[fromModel][toModel].raw = wrapRaw(fn);
    }
}
wtgtybhertgeghgtwtg commented 5 years ago

Updated with a proof of concept of pushing the graph traversal to build time. I want to see if I can cut down on the main loop.

wtgtybhertgeghgtwtg commented 5 years ago

Sorry for the delay. Updated with prevaling away some of the initial Object.defineProperty'ing. I think this might be a controversial change, but it sets up the groundwork of tree shaking in a potential major.

Qix- commented 5 years ago

I think my point is still being missed. This still builds things up at run time. Why not use the script I mentioned in the other ticket to pre-evaluate the build graph and emit new JavaScript?

wtgtybhertgeghgtwtg commented 5 years ago

If I'm understanding correctly, this does create the build graph. At run time, it's a constant.

const models = [["rgb", {
  "channels": 3,
  "labels": "rgb",
  "paths": [["hsl"], ["hsv"], ["hwb"], ["cmyk"], ["xyz"], ["lab"], ["lab", "lch"], ["hex"], ["keyword"], ["ansi16"], ["ansi256"], ["hcg"], ["apple"], ["gray"]]
}], ["hsl", {
  "channels": 3,
  "labels": "hsl",
  "paths": [["rgb"], ["hsv"], ["hcg", "hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["hsv", "ansi16"], ["rgb", "ansi256"], ["hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["hsv", {
  "channels": 3,
  "labels": "hsv",
  "paths": [["rgb"], ["hsl"], ["hcg", "hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["ansi16"], ["rgb", "ansi256"], ["hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["hwb", {
  "channels": 3,
  "labels": "hwb",
  "paths": [["rgb"], ["hcg", "hsl"], ["hcg", "hsv"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["rgb", "ansi16"], ["rgb", "ansi256"], ["hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["cmyk", {
  "channels": 4,
  "labels": "cmyk",
  "paths": [["rgb"], ["rgb", "hsl"], ["rgb", "hsv"], ["rgb", "hwb"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["rgb", "ansi16"], ["rgb", "ansi256"], ["rgb", "hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["xyz", {
  "channels": 3,
  "labels": "xyz",
  "paths": [["rgb"], ["rgb", "hsl"], ["rgb", "hsv"], ["rgb", "hwb"], ["rgb", "cmyk"], ["lab"], ["lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["rgb", "ansi16"], ["rgb", "ansi256"], ["rgb", "hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["lab", {
  "channels": 3,
  "labels": "lab",
  "paths": [["xyz", "rgb"], ["xyz", "rgb", "hsl"], ["xyz", "rgb", "hsv"], ["xyz", "rgb", "hwb"], ["xyz", "rgb", "cmyk"], ["xyz"], ["lch"], ["xyz", "rgb", "hex"], ["xyz", "rgb", "keyword"], ["xyz", "rgb", "ansi16"], ["xyz", "rgb", "ansi256"], ["xyz", "rgb", "hcg"], ["xyz", "rgb", "apple"], ["xyz", "rgb", "gray"]]
}], ["lch", {
  "channels": 3,
  "labels": "lch",
  "paths": [["lab", "xyz", "rgb"], ["lab", "xyz", "rgb", "hsl"], ["lab", "xyz", "rgb", "hsv"], ["lab", "xyz", "rgb", "hwb"], ["lab", "xyz", "rgb", "cmyk"], ["lab", "xyz"], ["lab"], ["lab", "xyz", "rgb", "hex"], ["lab", "xyz", "rgb", "keyword"], ["lab", "xyz", "rgb", "ansi16"], ["lab", "xyz", "rgb", "ansi256"], ["lab", "xyz", "rgb", "hcg"], ["lab", "xyz", "rgb", "apple"], ["lab", "xyz", "rgb", "gray"]]
}], ["hex", {
  "channels": 1,
  "labels": ["hex"],
  "paths": [["rgb"], ["rgb", "hsl"], ["rgb", "hsv"], ["rgb", "hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "keyword"], ["rgb", "ansi16"], ["rgb", "ansi256"], ["rgb", "hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["keyword", {
  "channels": 1,
  "labels": ["keyword"],
  "paths": [["rgb"], ["rgb", "hsl"], ["rgb", "hsv"], ["rgb", "hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "ansi16"], ["rgb", "ansi256"], ["rgb", "hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["ansi16", {
  "channels": 1,
  "labels": ["ansi16"],
  "paths": [["rgb"], ["rgb", "hsl"], ["rgb", "hsv"], ["rgb", "hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["rgb", "ansi256"], ["rgb", "hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["ansi256", {
  "channels": 1,
  "labels": ["ansi256"],
  "paths": [["rgb"], ["rgb", "hsl"], ["rgb", "hsv"], ["rgb", "hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["rgb", "ansi16"], ["rgb", "hcg"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["hcg", {
  "channels": 3,
  "labels": ["h", "c", "g"],
  "paths": [["rgb"], ["hsl"], ["hsv"], ["hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["hsv", "ansi16"], ["rgb", "ansi256"], ["rgb", "apple"], ["rgb", "gray"]]
}], ["apple", {
  "channels": 3,
  "labels": ["r16", "g16", "b16"],
  "paths": [["rgb"], ["rgb", "hsl"], ["rgb", "hsv"], ["rgb", "hwb"], ["rgb", "cmyk"], ["rgb", "xyz"], ["rgb", "lab"], ["rgb", "lab", "lch"], ["rgb", "hex"], ["rgb", "keyword"], ["rgb", "ansi16"], ["rgb", "ansi256"], ["rgb", "hcg"], ["rgb", "gray"]]
}], ["gray", {
  "channels": 1,
  "labels": ["gray"],
  "paths": [["rgb"], ["hsl"], ["hsv"], ["hwb"], ["cmyk"], ["lab", "xyz"], ["lab"], ["lab", "lch"], ["hex"], ["rgb", "keyword"], ["hsv", "ansi16"], ["rgb", "ansi256"], ["hsl", "hcg"], ["rgb", "apple"]]
}]];

It's enumerated at run time as the functions are created, though.

for (const [fromModel, {
  channels,
  labels,
  paths
}] of models) {
  convert[fromModel] = {};
  Object.defineProperty(convert[fromModel], 'channels', {
    value: channels
  });
  Object.defineProperty(convert[fromModel], 'labels', {
    value: labels
  });

  for (const path of paths) {
    const toModel = path[path.length - 1];
    const fn = composeConversion([fromModel, ...path]);
    convert[fromModel][toModel] = wrapRounded(fn);
    convert[fromModel][toModel].raw = wrapRaw(fn);
  }
}
wtgtybhertgeghgtwtg commented 5 years ago

Nevermind, I'm dumb. Locally, I got it to generate output like

const hwbansi16fn = args => rgb.ansi16(hwb.rgb(args));

convert.hwb.ansi16 = wrapRounded(hwbansi16fn);
convert.hwb.ansi16.raw = wrapRaw(hwbansi16fn);

const hwbansi256fn = args => rgb.ansi256(hwb.rgb(args));

convert.hwb.ansi256 = wrapRounded(hwbansi256fn);
convert.hwb.ansi256.raw = wrapRaw(hwbansi256fn);

const hwbhcgfn = args => hwb.hcg(args);

convert.hwb.hcg = wrapRounded(hwbhcgfn);
convert.hwb.hcg.raw = wrapRaw(hwbhcgfn);

const hwbapplefn = args => rgb.apple(hwb.rgb(args));

convert.hwb.apple = wrapRounded(hwbapplefn);
convert.hwb.apple.raw = wrapRaw(hwbapplefn);

const hwbgrayfn = args => rgb.gray(hwb.rgb(args));

convert.hwb.gray = wrapRounded(hwbgrayfn);
convert.hwb.gray.raw = wrapRaw(hwbgrayfn);
convert.cmyk = {};
Object.defineProperty(convert.cmyk, 'channels', {
  value: 4
});
Object.defineProperty(convert.cmyk, 'labels', {
  value: 'cmyk'
});

I don't think it's particularly stable, but is that what you meant?

wtgtybhertgeghgtwtg commented 5 years ago

Restarted everyone from master to make the diff less horrific. I got it to a relatively low number of lines of code. Setting up the functions is now inline.

convert.hwb = {
  rgb: wrapFn(hwb.rgb),
  hsl: wrapFn(args => hcg.hsl(hwb.hcg(args))),
  hsv: wrapFn(args => hcg.hsv(hwb.hcg(args))),
  cmyk: wrapFn(args => rgb.cmyk(hwb.rgb(args))),
  xyz: wrapFn(args => rgb.xyz(hwb.rgb(args))),
  lab: wrapFn(args => rgb.lab(hwb.rgb(args))),
  lch: wrapFn(args => lab.lch(rgb.lab(hwb.rgb(args)))),
  hex: wrapFn(args => rgb.hex(hwb.rgb(args))),
  keyword: wrapFn(args => rgb.keyword(hwb.rgb(args))),
  ansi16: wrapFn(args => rgb.ansi16(hwb.rgb(args))),
  ansi256: wrapFn(args => rgb.ansi256(hwb.rgb(args))),
  hcg: wrapFn(hwb.hcg),
  apple: wrapFn(args => rgb.apple(hwb.rgb(args))),
  gray: wrapFn(args => rgb.gray(hwb.rgb(args)))
};
Object.defineProperty(convert.hwb, 'channels', {
  value: 3
});
Object.defineProperty(convert.hwb, 'labels', {
  value: ['hwb']
});
Qix- commented 3 years ago

Hi there, sorry for the delay. I'm writing an Acorn-based build script for this right now, which will be a bit lighter and faster and achieve a faster startup time overall.

I appreciate the work done here, though :)