Color API

A Collection of Interesting Ideas,

This version:
https://wicg.github.io/color-api
Issue Tracking:
GitHub
Inline In Spec
Editors:
Chris Lilley (W3C)
Lea Verou (Invited Expert)
Tab Atkins Jr. (Google)

Abstract

A color space agnostic class for color specification, manipulation, and conversion.

1. Introduction

Many APIs on the Web platform need to be able to accept color input and provide color output in a format more structured than CSS <color> strings. Furthermore, authors often need to perform computations on color values, such as manipulating components in a variety of color spaces, computing color differences, evaluating whether color pairs have sufficient contrast, or interpolating two colors (regardless of what color space they are specified in).

The API presented in this document aims to address all of the common use cases, and provide extensibility points to enable more complex use cases to be expressed.

wicg/color-api/1Expand introduction

2. Color class

Color objects represent ...TBD

[Exposed=*]
interface Color {
  // Default constructor
  constructor(
    (CSSOMString or ColorSpace) colorSpace,
    sequence<double> coords,
    optional double alpha = 1
  );

  // Parse CSS color
  constructor(CSSOMString cssText);

  // Clone color instance or create from JSON-style object
  constructor((Color or CSSColorValue or ColorJSON) color);

  attribute ColorSpace colorSpace;
  attribute ObservableArray<double> coords;
  attribute double alpha;

  // Get/set coordinates (in this or other color spaces)
  double get((CSSOMString or ColorSpace) colorSpace, (CSSOMString or unsigned short) coord);
  double get((CSSOMString or unsigned short) coord);

  Color set(
    (CSSOMString or ColorSpace) colorSpace,
    (CSSOMString or unsigned short) coord,
    (double or relativeCoordCallback) value
  );
  Color set(
    (CSSOMString or unsigned short) coord,
    (double or relativeCoordCallback) value
  );
  Color set((CSSOMString or ColorSpace) colorSpace, object values);
  Color set(object values);

  // Convert to another color space
  Color to(CSSOMString colorspace);

  // Check whether a color is in gamut of a given color space
  boolean inGamut(optional CSSOMString colorspace);

  // Bring a color into gamut of a given colorspace
  Color toGamut(
    optional (CSSOMString or ColorSpace) colorSpace,
    optional ToGamutOptions options = {}
  );

  stringifier;

  ColorJSON toJSON();

  // Color difference
  double deltaE(Color color, optional DeltaEMethod method);
};

dictionary ToGamutOptions {
  coordReference? method = "lch.c";
};

dictionary coordReference {
  (CSSOMString or ColorSpace) colorSpace;
  CSSOMString name;
};

// TODO: we want authors to be able to extend this
// If we keep it an enum, the only way to add custom deltaE methods
// is a separate method.
enum DeltaEMethod {
  "76",    // fast, but limited accuracy
  "2000",  // slower, but accurate
};

dictionary ColorJSON {
  (CSSOMString or ColorSpace) colorSpace;
  sequence<double> coords;
  double alpha;
};

callback relativeCoordCallback = double (double coord);
The new Color(colorspace, coords, alpha) constructor steps are:
  1. Look up the ColorSpace object in the registry using the colorspace parameter.

    1. If the result is null, throw a TypeError

    2. Otherwise, set the color’s color space to it

  2. If coords is not provided, create an array of zeroes with length equal to the number of coordinates in colorspace.

  3. If coords is provided:

    1. If it’s not an array, throw a TypeError.

    2. Create a clone of the array.

    3. If its length is greater than the number of coordinates in the color space, trim the excess numbers from the end.

    4. If its length is smaller than the number of coordinates in the color space, pad it with zeroes

    5. Set the color’s coords to the cloned array.

  4. If alpha is not a number, coerce it to a number, then set the color’s alpha to this number.

The get(coord) method of Color objects must, when called on this:
  1. If there are two arguments, set refSpace to ColorSpace.get(colorSpace).

  2. If there is only one argument, set refSpace to ColorSpace.get(this.colorSpace)

  3. Let color = this.to(refSpace)

  4. Return color.coords[refSpace.coord].

The to(colorSpace) method of Color objects must, when called on this:
  1. Look up the color space object from the current object’s colorSpace specifier.

The inGamut(colorSpace) method, when called, must perform the following steps:
  1. Let colorSpace be the color space object from the current object’s colorSpace specifier.

  2. While the color space does not have an inGamut method, set colorSpace = ColorSpace.get(colorSpace.base)

  3. If colorSpace has an inGamut method, return colorSpace.inGamut(this.coords)

  4. Otherwise, return true.

The toGamut(colorSpace) method, when called, must perform the following steps:
  1. If colorSpace is not specified, it is set to this.colorSpace.

  2. Set colorSpace to the result of ColorSpace.get(colorSpace).

  3. If this.inGamut(colorSpace) === true, clone the current color and return it.

  4. Lookup the coordinate reference in options.method and

  5. Let color = this.to(colorSpace)

  6. While the color space does not have an inGamut method, set colorSpace = ColorSpace.get(colorSpace.base)

  7. If colorSpace has an inGamut method, return colorSpace.inGamut(this.coords)

  8. Otherwise, return true.

Color space objects should not have a method that is only present sometimes. We need to do this some other way.

The deltaE(color, deltaEMethod) method, when called, must perform the following steps:
  1. Calculate the color difference between this and color using the method specified by deltaEMethod (see prose below)

  2. Return the result.

The value "76" corresponds to the deltaE 76 method, which is a fast but inaccurate method. The value "2000" corresponds to the deltaE 2000 method, which is a slower but more accurate method. Future versions of this specification may add additional methods, or allow authors to define their own methods.

3. ColorSpace class

[Exposed=*]
interface ColorSpace {
  readonly attribute CSSOMString name;
  readonly attribute ColorSpaceWhitePoint white;
  readonly attribute ColorSpace? base;
  readonly attribute sequence<CSSOMString> coords;

  constructor(CSSOMString name, ColorSpaceOptions options);

  // Register a ColorSpace object
  static undefined register(ColorSpace colorspace);

  // Creates a new ColorSpace object and registers it
  static ColorSpace create(CSSOMString name, ColorSpaceOptions options);

  // Array of names for all registered color spaces
  static readonly attribute FrozenArray<CSSOMString> names;

  // Lookup ColorSpace object by name
  static ColorSpace get((CSSOMString or ColorSpace) name);

  // Load ICC profile and create a ColorSpace object from it
  static Promise<ColorSpace> fromICCProfile((Response or CSSOMString) resource, ReducedColorSpaceOptions options);
};

// White point x and y chromaticities
interface ColorSpaceWhitePoint {
  constructor(double x, double y);

  readonly attribute double x;
  readonly attribute double y;

  static readonly attribute ColorSpaceWhitePoint D65;
  static readonly attribute ColorSpaceWhitePoint D50;
};

dictionary ReducedColorSpaceOptions {
  // Coordinate names and optional metadata
  record<DOMString, ColorSpaceCoordinate> coords;
};

dictionary ColorSpaceOptions : ReducedColorSpaceOptions {
  (ColorSpaceWhitePoint or object) white;

  inGamutCallback inGamut;

  // Base color space, if this is a transformation
  (CSSOMString or ColorSpace)? base;
  toBaseCallback toBase;
  fromBaseCallback fromBase;
};

dictionary ColorSpaceCoordinate {
  ColorSpaceCoordinateType? type = "number";

  // Gamut limits
  double? min;
  double? max;

  // Reference range
  double? refMin;
  double? refMax;
};

enum ColorSpaceCoordinateType { "angle", "number" };

callback inGamutCallback = boolean (sequence<double> coords);
callback toBaseCallback = sequence<double> (sequence<double> coords);
callback fromBaseCallback = sequence<double> (sequence<double> coords);

Color space coordinates must specify at least one of:

wicg/color-api/7Matrix-based extensibility

The ColorSpaceWhitePoint interface represents the xy chromaticity coordinates of a white point. For convenience, D50 and D65 are predefined as follows:

The get(name) function:
  1. If the argument is a ColorSpace object, return it

  2. If the argument is a string, look up that string in the internal registry of ColorSpace objects.

    1. If a ColorSpace object is found, return it.

    2. Otherwise, throw a ReferenceError (Color space does not exist)

4. Algorithms

4.1. Getting and setting coordinates

The color.get() and color.set() methods allow authors to read/write coordinates in the current color space or even other color spaces. Color spaces can be provided either as a string (color space id) or a CoorSpace object. Coordinates can be specified either as a name, or as a numerical index.

color.set(coord, value) also accepts a value. If the value is a function, it is invoked immediately, with the result of color.get(coord) being passed as the first argument. If the result is a number, the corresponding coordinate is set to it.

4.2. Color space lookup

Color spaces can be looked up either by ColorSpace object, or by name. Implementations are expected to maintain an internal Map registry of color space names to objects, for fast lookups.

To look up a color space, follow the following steps:

  1. If needle is a ColorSpace object, let needle = needle.name

  2. If needle is a USVString, look up if there is an entry with that key in the internal Map of color names to ColorSpace objects.

  3. Return the ColorSpace object, or null, if none is found

4.3. Converting between color spaces

To convert a color from color space A to color space B, perform the following steps:

  1. If A.name === B.name, clone the color and return it

  2. Let coords = A.toBase(color.coords). If A.base === B.name, return new Color(B, coords, color.alpha)

  3. While A.base !== "xyz":

    1. Let coords = A.toBase(color.coords).

    2. If A.base === B.name, return new Color(B, coords, color.alpha)

    3. Otherwise, let A = ColorSpace.get(A.base)

  4. Follow B’s base chain until "xyz" as well, and store the result in an array.

  5. Starting from the end, let coords = B.fromBase(coords) on each of these colorspaces

  6. Return new Color(B, coords, color.alpha)

wicg/color-api/11Clarify later steps of colorspace conversion

Note: While this seems complicated in the general case, in virtually every real case the base chain has a length of max 3, so the algorithm would end very quickly.

4.4. Registering a color space

TBD. Should throw if base chain doesn’t resolve to "xyz" eventually. Should throw if name exists.

5. Security Considerations

There are no known security issues introduced by these features.

6. Privacy Considerations

There are no known privacy issues introduced by these features.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-TYPED-OM-1]
Shane Stephens; Tab Atkins Jr.; Naina Raisinghani. CSS Typed OM Level 1. URL: https://drafts.css-houdini.org/css-typed-om-1/
[CSSOM-1]
Daniel Glazman; Emilio Cobos Álvarez. CSS Object Model (CSSOM). URL: https://drafts.csswg.org/cssom/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

IDL Index

[Exposed=*]
interface Color {
  // Default constructor
  constructor(
    (CSSOMString or ColorSpace) colorSpace,
    sequence<double> coords,
    optional double alpha = 1
  );

  // Parse CSS color
  constructor(CSSOMString cssText);

  // Clone color instance or create from JSON-style object
  constructor((Color or CSSColorValue or ColorJSON) color);

  attribute ColorSpace colorSpace;
  attribute ObservableArray<double> coords;
  attribute double alpha;

  // Get/set coordinates (in this or other color spaces)
  double get((CSSOMString or ColorSpace) colorSpace, (CSSOMString or unsigned short) coord);
  double get((CSSOMString or unsigned short) coord);

  Color set(
    (CSSOMString or ColorSpace) colorSpace,
    (CSSOMString or unsigned short) coord,
    (double or relativeCoordCallback) value
  );
  Color set(
    (CSSOMString or unsigned short) coord,
    (double or relativeCoordCallback) value
  );
  Color set((CSSOMString or ColorSpace) colorSpace, object values);
  Color set(object values);

  // Convert to another color space
  Color to(CSSOMString colorspace);

  // Check whether a color is in gamut of a given color space
  boolean inGamut(optional CSSOMString colorspace);

  // Bring a color into gamut of a given colorspace
  Color toGamut(
    optional (CSSOMString or ColorSpace) colorSpace,
    optional ToGamutOptions options = {}
  );

  stringifier;

  ColorJSON toJSON();

  // Color difference
  double deltaE(Color color, optional DeltaEMethod method);
};

dictionary ToGamutOptions {
  coordReference? method = "lch.c";
};

dictionary coordReference {
  (CSSOMString or ColorSpace) colorSpace;
  CSSOMString name;
};

// TODO: we want authors to be able to extend this
// If we keep it an enum, the only way to add custom deltaE methods
// is a separate method.
enum DeltaEMethod {
  "76",    // fast, but limited accuracy
  "2000",  // slower, but accurate
};

dictionary ColorJSON {
  (CSSOMString or ColorSpace) colorSpace;
  sequence<double> coords;
  double alpha;
};

callback relativeCoordCallback = double (double coord);

[Exposed=*]
interface ColorSpace {
  readonly attribute CSSOMString name;
  readonly attribute ColorSpaceWhitePoint white;
  readonly attribute ColorSpace? base;
  readonly attribute sequence<CSSOMString> coords;

  constructor(CSSOMString name, ColorSpaceOptions options);

  // Register a ColorSpace object
  static undefined register(ColorSpace colorspace);

  // Creates a new ColorSpace object and registers it
  static ColorSpace create(CSSOMString name, ColorSpaceOptions options);

  // Array of names for all registered color spaces
  static readonly attribute FrozenArray<CSSOMString> names;

  // Lookup ColorSpace object by name
  static ColorSpace get((CSSOMString or ColorSpace) name);

  // Load ICC profile and create a ColorSpace object from it
  static Promise<ColorSpace> fromICCProfile((Response or CSSOMString) resource, ReducedColorSpaceOptions options);
};

// White point x and y chromaticities
interface ColorSpaceWhitePoint {
  constructor(double x, double y);

  readonly attribute double x;
  readonly attribute double y;

  static readonly attribute ColorSpaceWhitePoint D65;
  static readonly attribute ColorSpaceWhitePoint D50;
};

dictionary ReducedColorSpaceOptions {
  // Coordinate names and optional metadata
  record<DOMString, ColorSpaceCoordinate> coords;
};

dictionary ColorSpaceOptions : ReducedColorSpaceOptions {
  (ColorSpaceWhitePoint or object) white;

  inGamutCallback inGamut;

  // Base color space, if this is a transformation
  (CSSOMString or ColorSpace)? base;
  toBaseCallback toBase;
  fromBaseCallback fromBase;
};

dictionary ColorSpaceCoordinate {
  ColorSpaceCoordinateType? type = "number";

  // Gamut limits
  double? min;
  double? max;

  // Reference range
  double? refMin;
  double? refMax;
};

enum ColorSpaceCoordinateType { "angle", "number" };

callback inGamutCallback = boolean (sequence<double> coords);
callback toBaseCallback = sequence<double> (sequence<double> coords);
callback fromBaseCallback = sequence<double> (sequence<double> coords);


Issues Index

wicg/color-api/1Expand introduction
wicg/color-api/3Describe the other constructor signatures
Color space objects should not have a method that is only present sometimes. We need to do this some other way.
wicg/color-api/7Matrix-based extensibility
wicg/color-api/11Clarify later steps of colorspace conversion