omgovich / colord

👑 A tiny yet powerful tool for high-performance color manipulations and conversions
https://colord.omgovich.ru
MIT License
1.69k stars 51 forks source link

The first concept #1

Closed omgovich closed 3 years ago

omgovich commented 3 years ago

It's been a long time, @molefrog @rschristian!

I decided to use our experience from react-colorful to create a new color conversion and manipulation tool. It's called colord (🎨Color + 👑Lord).

Actually, the color tool market is quite crowded already, but several packages have most of the market: https://www.npmjs.com/package/color-convert (37M downloads weekly) — 13 KB, has 1 dependency https://www.npmjs.com/package/color (11M downloads weekly) — 22.4 KB, has 2 dependencies https://www.npmjs.com/package/tinycolor2 (3M downloads weekly) — 14,4 KB

The idea is to create a tool that will support most of their features but will be better, lighter, faster, and TS-oriented.

This PR is the first concept that I would like to share with you to hear your thoughts.

The API is going to be similar to tinycolor2's one and the library won't be tree-shakeable (like all popular color conversion tools).

import colord from 'colord'
colord("#F00").grayscale().toRgbaString() // "rgba(128, 128, 128, 1)"

Tinycolor2 is not the most popular color tool, but I like their API. It's pretty simple to understand.

Why I'm not going to create a tree-shakeable library? Because nobody likes them. Example: I know 2 tree-shakeable color conversion tools: https://www.npmjs.com/package/color-fns https://www.npmjs.com/package/@swiftcarrot/color-fns Just 2k downloads weekly just because nobody wants to dig into color models and do extra work. Only geeks like me can use libraries of this kind =)

rgbaToHex(grayscale(parseRgba(rgba))) // the code nobody wants to write
magick(rgba).grayscale().toHex() // the cope people like

So I think our way is to copy the API of the popular libraries (it means to be not tree-shakeable), but make the library faster/lighter, and provide better DX.

Current bundle size: 3KB min, 1.14 KB gzip.

Any feedback is welcome ❤️

rschristian commented 3 years ago

Hello once again!

For me, if I'm understanding you correctly, a non-treeshakeable library is a non-starter. I've copied functions from tinycolor2 and I likely will do again because I refuse to install that bloatware.

Because nobody likes them.

I refuse to use anything that isn't, personally. If it's not treeshakable, it's a net-negative to any application I use. I'm better off manually copying functions 100% of the time.

Those other libraries didn't fail to pick up because they weren't treeshakeable.

rschristian commented 3 years ago

You can provide chainable functions that are tree-shakeable and offer that API. It shouldn't be hard (theoretically)

omgovich commented 3 years ago

The thing I noticed when I've been replacing react-color with react-colorful is that most of the developers don't want to think about input formats at all. They love tools that accept any input.

I also want to provide a universal value parser as popular libraries do.

But a universal parser will pull in about 70% of all library sources (all color model parsers and many conversion algorithms). That means that splitting the library into separate methods (to make it tree-shakeable) won't save many kilobytes, but makes DX way worse.

I believe that we can create a library that will cost 3-4 KB gzip. That's not much. I mean it doesn't feel like tree-shaking is needed for such a small library. I would prefer to provide better and familiar DX than trying to win less than 1 KB.

omgovich commented 3 years ago

I would prefer to create a tree-shakeable solution, but I don't believe that somebody will use the library if they have to write code like that:

import { flow, parseAnyColor, saturate, toHsla } from 'colord' // ~2,5–3 KB

const processColor = flow(parseAnyColor, [saturate, 10], toHsla);
processColor('#F00')

instead of

import colord from 'colord' // 4 KB

colord('#F00').saturate(10).toHsla()

So the idea I have is to be similar to the popular libraries but be lighter and faster: optimize everything we can optimize.

omgovich commented 3 years ago

Maybe I'm not right, but it just feels like a tree-shakeable version won't work. I mean it won't be popular because it's too complicated for most of the developers.

omgovich commented 3 years ago

Combined way. We could go with the dayjs's approach:

// default behavior
import { colord } from 'colord' // 2KB
colord('red') // can't parse

// extend
import { colord, extend } from 'colord' // 2 KB + ~0,2KB
import { colorNamesPlugin } from '@colord/names' // 1,5 KB
extend(colorNamesPlugin)
colord('red') // ok
rschristian commented 3 years ago

I mean if that's what you're aiming for then that's what you're aiming for. I certainly have no place to say anything in that regard.

But for my use, and use in my workplace, I've turned down libraries a lot smaller than 3-4kb if I thought it could be done better.

omgovich commented 3 years ago

Actually, you've got me thinking about making the library extendable (see example above). It might make the library unique in terms of DX and size. Thanks!

import { colord, extend } from 'colord'
import { namesPlugin } from 'colord-names'
import { cmykPlugin } from 'colord-cmyk'

extend(namesPlugin, cmykPlugin)

colord('red').toCmyk()
molefrog commented 3 years ago

Would that be an option to make the library semi-tree shakable? I mean, having just one method to parse all the color models, but also provide a set of methods that can be used in a composable rather than a chainable fashion:

import { parse, grayscale, saturate  } from "colord"

saturate(grayscale(parse("red")))

// additionally
import { parseRgba  } from "colord"
parseRgba("...")

But in that case we might need to come up with some smart indermediate object format.

omgovich commented 3 years ago

What if a developer needs to parse any color except the color name? I think it's a common case.

parseHexOrRgbOrHslOrHsv(HEX|RGB|HSL) // 1 KB
parse(NAME|HEX|RGB|HSL) // 2,5 KB

Most of the projects I've seen don't know which color they receive but that's probably not a color name. That means that they need to run:

const color = parseHex(something) || parseRgb(something) || // ...
saturate(grayscale(color))

It's now an option. I would rather have one method that can't be extendable this way:

import colord from 'colord' // default parsers and methods
colord(HEX|RGB|HSL).toRgba()
// +
import 'colord-names' // extends colord's parsers, public methods and types
colord(NAME|HEX|RGB|HSL).toName()

I like how dayjs works. It's chainable (which is super clear and comfortable to use) but provides plugins to extend functionality. Yeah, it's not 100% tree-shakeable, but still. It feels like DX is more important here than 300 extra bytes