Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(getColor): improves custom color scale generation #1798

Open
wants to merge 2 commits into
base: next
Choose a base branch
from

Conversation

ze-flo
Copy link
Contributor

@ze-flo ze-flo commented Apr 25, 2024

Description

  • Creates 2 color utilities based on a custom and lightweight build of chroma-js:
    • getCustomColorScale: generates a color scale from hue that closely aligns with the desired ratio for the offset.
    • isValidColor: an alias for chroma.valid
  • Refactors getColor to use getCustomColorScale
  • Reduces @zendeskgarden/react-theming bundle size by selecting parts of chroma-js that are relevant to our needs.

Detail

Cherry-picking only the parts needed to validate a color and generate a multistep color scale helped reduce chroma-js's footprint considerably.

BEFORE

Screen Shot 2024-04-25 at 8 49 42 AM

AFTER

Screen Shot 2024-04-25 at 7 59 52 AM

Before (KB) After (KB) Diff
chroma-js 15.44 3.95 -74.42%

Garden's color palette were generated using Leonardo using the following offset-based contrast ratios.

const targetRatios = {
      100: 1.08,
      200: 1.2,
      300: 1.35,
      400: 2,
      500: 2.8,
      600: 3.3,
      700: 5,
      800: 10,
      900: 13,
      1000: 16,
      1100: 17.5,
      1200: 19.5
    };

To generate a color scale with the precise contrast ratios listed above, @adobe/leonardo-contrast-colors was considered. However at 30kb gzipped, it isn't a good option. Instead, by leveraging chroma-js and importing only the parts needed to generate a color scale, I was able to reproduce a palette very similar to one created by Leonardo.

This sandbox highlights different methods for generating colors.

Screen Shot 2024-04-25 at 9 43 06 AM

I implemented getCustomColorScale following the Chroma 100 Cherry-picked method (see sandbox). It aligns contrast ratios closely with Leonardo without incurring the additional bundle cost. Using a binary search to find the nearest ratio per offset (Chroma 48 Find nearest) could improve accuracy but is a much slower algorithm.

Update

Generating a 100 color scale with Color2K and finding the nearest match using a binary search offered excellent results while only adding 791b to the bundle. πŸ”₯

Screen Shot 2024-04-30 at 3 15 25 PM

The new demo includes a description, pros, and cons for each solution. Also notice a delta between closest match and offset-pinned target contrast ratio for color step.

Checklist

  • πŸ‘Œ design updates will be Garden Designer approved (add the designer as a reviewer)
  • 🌐 demo is up-to-date (npm start)
  • ⬅️ renders as expected with reversed (RTL) direction
  • 🀘 renders as expected with Bedrock CSS (?bedrock)
  • πŸ’‚β€β™‚οΈ includes new unit tests. Maintain existing coverage (always >= 96%)
  • β™Ώ tested for WCAG 2.1 AA accessibility compliance
  • πŸ“ tested in Chrome, Firefox, Safari, and Edge

@ze-flo ze-flo requested a review from a team as a code owner April 25, 2024 19:45
Comment on lines -96 to -106
const _colors = scale([PALETTE.white, _hue, PALETTE.black])
.correctLightness()
.colors(PALETTE_SIZE + 2); // add 2 to account for the white and black endpoints removed below

_hue = _colors.reduce<Record<number, string>>((_retVal, color, index) => {
if (index > 0 && index <= PALETTE_SIZE) {
_retVal[index * 100] = color;
}

return _retVal;
}, {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this style of hue generation to the sandbox for comparison? If it isn't a compelling difference, I'd prefer to retain this for being more straightforward and imperative with respect to Garden's palette size. Garden design will be able to help us evaluate the trade-offs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the Sandbox under Chroma 12 Current.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. Thanks so much.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ze-flo did you look at https://color2k.com/#get-scale or rule it out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent suggestion! πŸ’― πŸ”₯ πŸš€

Using a binary search and a 100 color scale, color2k outputs excellent results despite its tiny footprint. I think we have a winner! 🍾 πŸŽ‰ πŸ₯³

Screen Shot 2024-04-30 at 3 11 18 PM

Screen Shot 2024-04-30 at 3 15 25 PM

Check out the new and improved demo.


import type { ChromaStatic } from 'chroma-js';

// @ts-expect-error Ignoring missing type definition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it's preferable to add a chroma-js.d.ts under /utils/build and update the tsconfig.json accordingly.

*
* @returns An offset-based color palette.
*/
export function getCustomColorScale(hue: string) {
Copy link
Member

@jzempel jzempel Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, I'm less interested in exposing another theming utility and more about containing the chroma-js import gnarliness to a dedicated location (under types, or a new chroma-js folder, or something 🀷). For example, here's a compact version of a file that deals with the valid function...

/**
 * Copyright Zendesk, Inc.
 *
 * Use of this source code is governed under the Apache License, Version 2.0
 * found at http://www.apache.org/licenses/LICENSE-2.0.
 */

import type { ChromaStatic } from 'chroma-js';
import { default as _valid } from 'chroma-js/src/utils/valid';
import 'chroma-js/src/io/hex';

export const valid = _valid as ChromaStatic['valid'];

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the chroma-js folder idea. In that case, I would move getCustomColorScale to getColor.

  1. Where would you recommend I create it?
  2. Would chroma-js.d.ts live in that new folder or /utils/build?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool πŸ™Œ . Here's what I'd recommend...

  • add a chroma-js folder under theming/src/utils that looks something like the screenshot below
  • in getColor simply import { scale, valid } from './chroma-js';
  • add the new chroma-js.d.ts under the project /utils/build with simple module declarations that clear the TS errors
  • depending on design feedback, either leave getColor (nearly) as it is on next, or add a local toScale function similar to the other local functions that invokes scale and portions out the hue shades based on your target contrast ratios algorithm. I want to avoid the notion that Garden could provide a dedicated color scale utility outside of the getColor internals
Screenshot 2024-04-30 at 12 22 38β€―PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

None yet

2 participants