macaw

Project / Source Code Repository
https://gitlab.com/jcroisant/macaw
Issue Tracker
https://gitlab.com/jcroisant/macaw/issues
Maintainer
John Croisant
License
BSD 2-Clause

Macaw provides efficient color types, math operations, and color space conversion. It is primarily meant for computer graphics, data visualization, image processing, games, etc. For a more scientific library, see the color egg.

Macaw provides a variety of arithmetic, blending, and compositing operations using linear RGB (floats), perceptual sRGB (bytes), and perceptual HSL (floats). More operations and color types may be added in the future.

Color objects can either be self-contained for convenience, or can point to locations in memory to efficiently work with large blocks of image data. Low-level operations are also provided which directly modify memory, without needing to create color objects.

Table of Contents

  1. macaw
    1. Color types
      1. Generic color procedures
      2. rgb
      3. rgb8
      4. hsl
    2. Array types
      1. Generic array procedures
      2. rgb-array
      3. rgb8-array
      4. hsl-array
    3. Color math
      1. In-place color math
      2. Gamma
    4. Low-level
      1. Caution!
      2. Colors from memory
      3. Arrays from memory
      4. Low-level array operations
      5. Low-level color math
    5. Compatibility with other libraries
      1. Generic conversion
      2. sdl2
      3. web-colors
    6. Version history

Color types

Generic color procedures

[procedure] (color? x) → boolean

Returns #t if x is a color of any type defined in this library, or #f if it is anything else.

[procedure] (color->rgb color_1) → color_1 or new rgb
[procedure] (color->rgb8 color_1) → color_1 or new rgb8
[procedure] (color->hsl color_1) → color_1 or new hsl

Converts a color of any type into the target type. If color_1 is already the target type, it is returned without copying. Otherwise, a newly allocated color of the target type is returned.

[procedure] (color->rgb/new color_1) → new rgb
[procedure] (color->rgb8/new color_1) → new rgb8
[procedure] (color->hsl/new color_1) → new hsl

Like color->rgb etc. except it always returns a newly allocated color, even if color_1 is already the target type. Equivalent to e.g.

(if (rgb? color_1)
    (rgb-copy color_1)
    (color->rgb color_1))

rgb

The rgb type represents colors in linear RGB space. It is not gamma compressed. Its alpha is not premultiplied. It has red, green, blue, and alpha (opacity) components stored as 32-bit floating-point numbers (float), which are usually in the interval [0,1]. Values outside that interval are allowed, but type conversion procedures may behave as if the values were clamped to that interval.

[procedure] (rgb r g b #!optional (a 1.0)) → new rgb

Creates a new rgb color. All values are floats, usually in the interval [0,1].

[procedure] (rgb-r rgb_1) → float
[procedure] (rgb-g rgb_1) → float
[procedure] (rgb-b rgb_1) → float
[procedure] (rgb-a rgb_1) → float
[setter] (rgb-r-set! rgb_1 r) → rgb_1
[setter] (rgb-g-set! rgb_1 g) → rgb_1
[setter] (rgb-b-set! rgb_1 b) → rgb_1
[setter] (rgb-a-set! rgb_1 a) → rgb_1
[setter] (set! (rgb-r rgb_1) r) → rgb_1
[setter] (set! (rgb-g rgb_1) g) → rgb_1
[setter] (set! (rgb-b rgb_1) b) → rgb_1
[setter] (set! (rgb-a rgb_1) a) → rgb_1

Gets or sets a component of the rgb color. All values are floats, usually in the interval [0,1]. The setters return the modified color.

[procedure] (rgb-set! rgb_1 r g b a) → rgb_1

Sets every component of the rgb color, then returns the modified color. This is more efficient than setting each component individually.

[procedure] (rgb? x) → boolean

Returns #t if x is an rgb color, or #f if it is anything else.

[procedure] (rgb= rgb_1 rgb_2) → boolean

Returns #t if rgb_1 and rgb_2 have exactly the same values. This uses floating point equality, which is notoriously fickle due to rounding errors. Consider using rgb-near? instead.

[procedure] (rgb-near? rgb_1 rgb_2 #!optional (e 1e-5)) → boolean

Returns #t if rgb_1 and rgb_2 have approximately the same values. This checks if each value of rgb_1 is within ±e of the value of rgb_2. This is slower than rgb= but more resilient against rounding errors.

[procedure] (rgb-copy rgb_1) → new rgb

Returns a new copy of the rgb color.

[procedure] (rgb-copy! rgb_src rgb_dst) → rgb_dst

Copies data from rgb_src to rgb_dst. Modifies and returns rgb_dst.

[procedure] (rgb-normalize rgb_1) → new rgb
[procedure] (rgb-normalize! rgb_1) → rgb_1

Normalizes the rgb color by clamping its components to the interval [0,1]. rgb-normalize returns a new color. rgb-normalize! modifies and returns its argument.

[procedure] (rgb->list rgb_1) → list of 4 floats

Returns a list of the rgb color's values, (r g b a).

[procedure] (rgb->values rgb_1) → 4 float values

Returns the rgb color's values as multiple values.

[procedure] (rgb->rgb8 rgb_1) → new rgb8
[procedure] (rgb->hsl rgb_1) → new hsl

Converts the rgb color into a new color of a different type.

rgb8

The rgb8 type represents colors in perceptual (non-linear) sRGB space. It is gamma compressed. Its alpha is not premultiplied. It has red, green, blue, and alpha (opacity) components stored as unsigned 8-bit integers (unsigned char), which are limited to the interval [0,255]. Values outside that interval are not allowed.

[procedure] (rgb8 r g b #!optional (a 255)) → new rgb8

Creates a new rgb8 color. All arguments must be integers in the interval [0,255].

[procedure] (rgb8-r rgb8_1) → integer
[procedure] (rgb8-g rgb8_1) → integer
[procedure] (rgb8-b rgb8_1) → integer
[procedure] (rgb8-a rgb8_1) → integer
[setter] (rgb8-r-set! rgb8_1 r) → rgb8_1
[setter] (rgb8-g-set! rgb8_1 g) → rgb8_1
[setter] (rgb8-b-set! rgb8_1 b) → rgb8_1
[setter] (rgb8-a-set! rgb8_1 a) → rgb8_1
[setter] (set! (rgb8-r rgb8_1) r) → rgb8_1
[setter] (set! (rgb8-g rgb8_1) g) → rgb8_1
[setter] (set! (rgb8-b rgb8_1) b) → rgb8_1
[setter] (set! (rgb8-a rgb8_1) a) → rgb8_1

Gets or sets a component of the rgb8 color. Values are exact integers, and must be in the interval [0,255]. The setters return the modified color.

[procedure] (rgb8-set! rgb8_1 r g b a) → rgb8_1

Sets every component of the rgb8 color, then returns the modified color. This is more efficient than setting each component individually.

[procedure] (rgb8? x) → boolean

Returns #t if x is an rgb8 color, or #f if it is anything else.

[procedure] (rgb8= rgb8_1 rgb8_2) → boolean

Returns #t if rgb8_1 and rgb8_2 have exactly the same values.

[procedure] (rgb8-copy rgb8_1) → new rgb8

Returns a new copy of the rgb8 color.

[procedure] (rgb8-copy! rgb8_src rgb8_dst) → rgb8_dst

Copies data from rgb8_src to rgb8_dst. Modifies and returns rgb8_dst.

[procedure] (rgb8->list rgb8_1) → list of 4 integers

Returns a list of the rgb8 color's values, (r g b a).

[procedure] (rgb8->values rgb8_1) → 4 integer values

Returns the rgb8 color's values as multiple values.

[procedure] (rgb8->rgb rgb8_1) → new rgb
[procedure] (rgb8->hsl rgb8_1) → new hsl

Converts the rgb8 color into a new color of a different type.

hsl

The hsl type represents colors in cylindrical HSL space. It has hue, saturation, lightness, and alpha (opacity) components stored as 32-bit floating-point numbers (float). The lightness component is effectively gamma compressed. Hue is usually in the interval [0,360), and the other components are usually in the interval [0,1]. Values outside that interval are allowed, but type conversion procedures may behave as if values were wrapped (hue) or clamped (other components) to those intervals.

[procedure] (hsl h s l #!optional (a 1.0)) → new hsl

Creates a new hsl color. h is a float, usually in the interval [0,360). Other components are floats, usually in the interval [0,1].

[procedure] (hsl-h hsl_1) → float
[procedure] (hsl-s hsl_1) → float
[procedure] (hsl-l hsl_1) → float
[procedure] (hsl-a hsl_1) → float
[setter] (hsl-h-set! hsl_1 h) → hsl_1
[setter] (hsl-s-set! hsl_1 s) → hsl_1
[setter] (hsl-l-set! hsl_1 l) → hsl_1
[setter] (hsl-a-set! hsl_1 a) → hsl_1
[setter] (set! (hsl-h hsl_1) h) → hsl_1
[setter] (set! (hsl-s hsl_1) s) → hsl_1
[setter] (set! (hsl-l hsl_1) l) → hsl_1
[setter] (set! (hsl-a hsl_1) a) → hsl_1

Gets or sets a component of the hsl color. h is a float, usually in the interval [0,360). Other components are floats, usually in the interval [0,1].

[procedure] (hsl-set! hsl_1 h s l a) → hsl_1

Sets every component of the hsl color, then returns the modified color. This is more efficient than setting each component individually.

[procedure] (hsl? x) → boolean

Returns #t if x is an hsl color, or #f if it is anything else.

[procedure] (hsl= hsl_1 hsl_2) → boolean

Returns #t if hsl_1 and hsl_2 have exactly the same values. This uses floating point equality, which is notoriously fickle due to rounding errors. Consider using hsl-near? instead.

[procedure] (hsl-near? hsl_1 hsl_2 #!optional (e 1e-5)) → boolean

Returns #t if hsl_1 and hsl_2 have approximately the same values. This checks if each value of hsl_1 is within ±e of the value of hsl_2. This is slower than hsl= but more resilient against rounding errors.

[procedure] (hsl-copy hsl_1) → new hsl

Returns a new copy of the hsl color.

[procedure] (hsl-copy! hsl_src hsl_dst) → hsl_dst

Copies data from hsl_src to hsl_dst. Modifies and returns hsl_dst.

[procedure] (hsl-normalize hsl_1) → new hsl
[procedure] (hsl-normalize! hsl_1) → hsl_1

Normalizes the hsl color by wrapping its hue to the interval [0,360) and clamping its other components to the interval [0,1]. hsl-normalize returns a new color. hsl-normalize! modifies and returns its argument.

[procedure] (hsl->list hsl_1) → list of 4 floats

Returns a list of the hsl color's values, (h s l a).

[procedure] (hsl->values hsl_1) → 4 float values

Returns the hsl color's values as multiple values.

[procedure] (hsl->rgb hsl_1) → new rgb
[procedure] (hsl->rgb8 hsl_1) → new rgb8

Converts the hsl color into a new color of a different type.

Array types

Arrays represent a two-dimensional grid of colors, such as pixels in an image. The color data is stored in a contiguous block of memory, which is more efficient than storing a list or vector of color objects. It also allows arrays to directly access blocks of pixel data from other sources. For example, see the sdl2 section for how to use rgb8-array to access the pixels of an sdl2:surface.

Generic array procedures

[procedure] (array? x) → boolean

Returns #t if x is an array of any type defined in this library, or #f if it is anything else.

[procedure] (array-width array_1) → integer
[procedure] (array-height array_1) → integer
[procedure] (array-pitch array_1) → integer

Returns the width, height, or pitch of an array of any type.

[procedure] (array-ref array_1 x y) → color

Returns an color object which directly accesses the memory of the array at the coordinates x, y. The returned color type depends on the given array type (e.g. giving an hsl-array will result in an hsl color). Modifying the color will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory.

Signals an exception if x or y are out of bounds.

[procedure] (array-for-each proc array ...)

Calls proc once for each color in the given array(s). If the arrays are different sizes, iterates over the overlapping area (i.e. the smallest width and smallest height).

The iteration proceeds in row-major order. proc is called with the x coordinate, y coordinate, and the corresponding color from each given array. Modifying the color will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory.

The arrays can be different types. The color types passed to proc depend on the given array types (e.g. if the first array is an hsl-array, the first color argument to proc will be an hsl color). If all the arrays are the same type, or if there is only one array, it is more efficient to use the type-specific procedures such as rgb-array-for-each and hsl-array-for-each.

(array-for-each
  (lambda (x y rgb8_1 hsl_2)
    (rgb8-lerp! rgb8_1 hsl_2 (/ x 100.0)))
  rgb8-array_1 hsl-array_2)

rgb-array

[procedure] (make-rgb-array width height #!optional pitch) → new rgb-array

Allocates and returns a new rgb-array with the given width and height.

pitch is the number of bytes per row, including any padding at the end. If pitch is omitted, a pitch is automatically chosen that is at least (* width 16). Use rgb-array-pitch to get the pitch that was chosen.

[procedure] (rgb-array? x) → boolean

Returns #t if x is an rgb-array, otherwise #f.

[procedure] (rgb-array-width rgb-array_1) → integer
[procedure] (rgb-array-height rgb-array_1) → integer
[procedure] (rgb-array-pitch rgb-array_1) → integer

Returns the width, height, or pitch of the rgb-array.

[procedure] (rgb-array-ref rgb-array_1 x y) → rgb

Returns an rgb color object which directly accesses the memory of the array at the coordinates x, y. Modifying the color will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory. See rgb-parent.

Signals an exception if x or y are out of bounds.

[procedure] (rgb-array-for-each proc rgb-array ...)

Calls proc once for each color in the given rgb-array(s). If the arrays are different sizes, it iterates over the overlapping area (i.e. the smallest width and smallest height).

The iteration proceeds in row-major order. proc is called with the x coordinate, y coordinate, and the corresponding rgb color from each given array. Modifying the color(s) will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory.

(rgb-array-for-each
  (lambda (x y rgb_1 rgb_2)
    (rgb-lerp! rgb_1 rgb_2 (/ x 100.0)))
  rgb-array_1 rgb-array_2)

rgb8-array

[procedure] (make-rgb8-array width height #!optional pitch) → new rgb8-array

Allocates and returns a new rgb8-array with the given width and height.

pitch is the number of bytes per row, including any padding at the end. If pitch is omitted, a pitch is automatically chosen that is at least (* width 4). Use rgb8-array-pitch to get the pitch that was chosen.

[procedure] (rgb8-array? x) → boolean

Returns #t if x is an rgb8-array, otherwise #f.

[procedure] (rgb8-array-width rgb8-array_1) → integer
[procedure] (rgb8-array-height rgb8-array_1) → integer
[procedure] (rgb8-array-pitch rgb8-array_1) → integer

Returns the width, height, or pitch of the rgb8-array.

[procedure] (rgb8-array-ref rgb8-array_1 x y) → rgb8

Returns an rgb8 color object which directly accesses the memory of the array at the coordinates x, y. Modifying the color will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory. See rgb8-parent.

Signals an exception if x or y are out of bounds.

[procedure] (rgb8-array-for-each proc rgb8-array ...)

Calls proc once for each color in the given rgb8-array(s). If the arrays are different sizes, it iterates over the overlapping area (i.e. the smallest width and smallest height).

The iteration proceeds in row-major order. proc is called with the x coordinate, y coordinate, and the corresponding rgb8 color from each given array. Modifying the color(s) will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory.

(rgb8-array-for-each
  (lambda (x y rgb8_1 rgb8_2)
    (rgb8-lerp! rgb8_1 rgb8_2 (/ x 100.0)))
  rgb8-array_1 rgb8-array_2)

hsl-array

[procedure] (make-hsl-array width height #!optional pitch) → new hsl-array

Allocates and returns a new hsl-array with the given width and height.

pitch is the number of bytes per row, including any padding at the end. If pitch is omitted, a pitch is automatically chosen that is at least (* width 16). Use hsl-array-pitch to get the pitch that was chosen.

[procedure] (hsl-array? x) → boolean

Returns #t if x is an hsl-array, otherwise #f.

[procedure] (hsl-array-width hsl-array_1) → integer
[procedure] (hsl-array-height hsl-array_1) → integer
[procedure] (hsl-array-pitch hsl-array_1) → integer

Returns the width, height, or pitch of the hsl-array.

[procedure] (hsl-array-ref hsl-array_1 x y) → hsl

Returns an hsl color object which directly accesses the memory of the array at the coordinates x, y. Modifying the color will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory. See hsl-parent.

Signals an exception if x or y are out of bounds.

[procedure] (hsl-array-for-each proc hsl-array ...)

Calls proc once for each color in the given hsl-array(s). If the arrays are different sizes, it iterates over the overlapping area (i.e. the smallest width and smallest height).

The iteration proceeds in row-major order. proc is called with the x coordinate, y coordinate, and the corresponding hsl color from each given array. Modifying the color(s) will modify the array data. The color's parent will be automatically set to the array, so that the array will not be garbage collected while the color is using its memory.

(hsl-array-for-each
  (lambda (x y hsl_1 hsl_2)
    (hsl-lerp! hsl_1 hsl_2 (/ x 100.0)))
  hsl-array_1 hsl-array_2)

Color math

This library provides many color math operations. Many of them are equivalent to layer blend modes found in image editing software such as GIMP or Adobe Photoshop. Most operations treat the first argument as if it were the bottom layer, and later arguments as if they were above it.

Each operation has multiple versions, one per color type. Every version accepts arguments of any color type, but they operate in different color spaces, and return different color types.

For example, you can call rgb-lerp with an rgb8 color and a hsl color. The arguments will be automatically converted to rgb, then interpolated in linear RGB space, and an rgb color will be returned. If you called hsl-lerp with the same arguments, they would be converted to hsl, then interpolated in HSL space, which could result in a very different color.

Math in HSL space can be counterintuitive. For example, if you use hsl-add to add dark dull yellow (hsl 60 0.5 0.25) with dark dull green (hsl 120 0.5 0.25), the result will be a bright saturated cyan (hsl 180 1.0 0.5). But this behavior can also be useful. For example, (hsl-add color (hsl 180 0 0)) will give the complement (opposite hue) of color, and (hsl-mul color (hsl 1 0 1)) will give a desaturated (grayscale) version of color.

See also the In-place color math section for versions of these operations which modify in-place (destructively), and the Low-level color math section for versions of these operations which work directly on pointers or locatives.

[procedure] (rgb-add color_1 color ...) → new rgb
[procedure] (rgb8-add color_1 color ...) → new rgb8
[procedure] (hsl-add color_1 color ...) → new hsl

Add zero or more colors to color_1, similar to the "addition" or "linear dodge" layer blend mode in image editing software.

The result type and color space used depends on the procedure. The result will have the same alpha as color_1. The alphas of the other colors determine how much effect they have.

[procedure] (rgb-sub color_1 color ...) → new rgb
[procedure] (rgb8-sub color_1 color ...) → new rgb8
[procedure] (hsl-sub color_1 color ...) → new hsl

Subtract zero or more colors from color_1, similar to the "subtract" layer blend mode in image editing software.

The result type and color space used depends on the procedure. The result will have the same alpha as color_1. The alphas of the other colors determine how much effect they have.

[procedure] (rgb-mul color_1 color ...) → new rgb
[procedure] (rgb8-mul color_1 color ...) → new rgb8
[procedure] (hsl-mul color_1 color ...) → new hsl

Multiply each non-alpha component of color_1 by the corresponding component of zero or more colors, similar to the "multiply" layer blend mode in image editing software.

Be aware that rgb8-mul behaves as if the components were divided by 255, i.e. multiplying by white (rgb8 255 255 255) has no effect. Also, rgb8-mul truncates each component to an integer.

The result type and color space used depends on the procedure. The result will have the same alpha as color_1. The alphas of the other colors determine how much effect they have.

[procedure] (rgb-scale color_1 n) → new rgb
[procedure] (rgb8-scale color_1 n) → new rgb8
[procedure] (hsl-scale color_1 n) → new hsl

Multiply each non-alpha component of color by the real number n. rgb8-scale truncates each component to an integer. Unlike rgb8-mul, scaling by 1.0 (not 255) has no effect.

The result type and color space used depends on the procedure. The result will have the same alpha as color_1.

[procedure] (rgb-lerp color_1 color_2 t) → new rgb
[procedure] (rgb8-lerp color_1 color_2 t) → new rgb8
[procedure] (hsl-lerp color_1 color_2 t) → new hsl

Mix color_1 and color_2 using linear interpolation. rgb8-lerp truncates each component to an integer. The result type and color space used depends on the procedure. The alpha values are interpolated.

t is the interpolation factor, which controls the mix proportion. 0 means use only color_1, 1 means use only color_2, 0.5 means halfway between color_1 and color_2. You can also perform linear extrapolation by passing a t less than 0 or greater than 1.

[procedure] (rgb-mix color-list #!optional weight-list) → new rgb
[procedure] (rgb8-mix color-list #!optional weight-list) → new rgb8
[procedure] (hsl-mix color-list #!optional weight-list) → new hsl

Proportionally mixes one or more colors, similar to the "convolution matrix" operation in image editing software. Returns a color whose components (including alpha) are the weighted sum of the given colors' components.

color-list is a list of one or more colors of any type. They are automatically converted to the target type before mixing.

weight-list is an optional list of real numbers, giving the weight of each color. It must be the same length as color-list. Usually the weights should total 1, but that is not required. Negative weights and weights greater than 1 are allowed. If weight-list is omitted, the colors are mixed equally.

[procedure] (rgb-under color_1 color ...) → new rgb
[procedure] (rgb8-under color_1 color ...) → new rgb8
[procedure] (hsl-under color_1 color ...) → new hsl

Composite zero or more colors over color_1, similar to "normal" layer blend mode in image editing software. Same as rgb-over etc. except the order is reversed: the first color is on bottom and each later color is higher up.

The result type and color space used depends on the procedure. The result's alpha will be based on the inputs.

[procedure] (rgb-over color_1 color ...) → new rgb
[procedure] (rgb8-over color_1 color ...) → new rgb8
[procedure] (hsl-over color_1 color ...) → new hsl

Composite color_1 over zero or more colors, similar to "normal" layer blend mode in image editing software. Same as rgb-under etc. except the order is reversed: the first color is on top and each later color is lower down.

The result type and color space used depends on the procedure. The result's alpha will be based on the inputs.

In-place color math

These procedures produce the same result as the color math operations described above, but they modify the first argument in-place (destructively) and return it, instead of allocating a new color object.

Unlike the operations described above, the first argument to an in-place operation must already be the target type. For example, the first argument to hsl-add! must be an hsl color, and the first argument to rgb8-add! must be an rgb8 color. Later color arguments can be any color type.

[procedure] (rgb-add! rgb_1 color ...) → rgb_1
[procedure] (rgb8-add! rgb8_1 color ...) → rgb8_1
[procedure] (hsl-add! hsl_1 color ...) → hsl_1

In-place versions of rgb-add etc.

[procedure] (rgb-sub! rgb_1 color ...) → rgb_1
[procedure] (rgb8-sub! rgb8_1 color ...) → rgb8_1
[procedure] (hsl-sub! hsl_1 color ...) → hsl_1

In-place versions of rgb-sub etc.

[procedure] (rgb-mul! rgb_1 color ...) → rgb_1
[procedure] (rgb8-mul! rgb8_1 color ...) → rgb8_1
[procedure] (hsl-mul! hsl_1 color ...) → hsl_1

In-place versions of rgb-mul etc.

[procedure] (rgb-scale! rgb_1 n) → rgb_1
[procedure] (rgb8-scale! rgb8_1 n) → rgb8_1
[procedure] (hsl-scale! hsl_1 n) → hsl_1

In-place versions of rgb-scale etc.

[procedure] (rgb-lerp! rgb_1 color_2 t) → rgb_1
[procedure] (rgb8-lerp! rgb8_1 color_2 t) → rgb8_1
[procedure] (hsl-lerp! hsl_1 color_2 t) → hsl_1

In-place versions of rgb-lerp etc.

[procedure] (rgb-mix! color-list #!optional weight-list) → car of color-list
[procedure] (rgb8-mix! color-list #!optional weight-list) → car of color-list
[procedure] (hsl-mix! color-list #!optional weight-list) → car of color-list

In-place versions of rgb-mix etc. The first color in color-list must already be the target type. That color will be modified and returned.

[procedure] (rgb-under! rgb_1 color ...) → rgb_1
[procedure] (rgb8-under! rgb8_1 color ...) → rgb8_1
[procedure] (hsl-under! hsl_1 color ...) → hsl_1

In-place versions of rgb-under etc.

[procedure] (rgb-over! rgb_1 color ...) → rgb_1
[procedure] (rgb8-over! rgb8_1 color ...) → rgb8_1
[procedure] (hsl-over! hsl_1 color ...) → hsl_1

In-place versions of rgb-over etc.

Gamma

Gamma compression is a process of transforming RGB color data to match the way humans perceive light. This allows computers to efficiently store and display images, but math performed with gamma compressed colors will not produce correct results. For more information about gamma, read "What every coder should know about gamma" by John Novak.

In this library, the rgb8 type is gamma compressed, and the rgb type is not. The hsl type's lightness component is effectively gamma compressed. The color type conversion procedures such as rgb->rgb8 handle gamma automatically. You can also use gamma-compress and gamma-expand (see below) to handle gamma manually.

For the best looking and most correct results, you should perform math operations using rgb, not rgb8. However, rgb uses more memory and CPU, so you might want to use rgb8 in cases where maximimum performance is more important than appearance or correctness. You should also use rgb8 if you want to match the output of software that does not support linear RGB.

[procedure] (gamma-compress n) → float

Performs gamma compression (encoding) of the real number n, converting from linear RGB space to sRGB space. This implements the sRGB forward transfer function, which approximates a gamma of 2.2.

n is usually in the interval [0,1], and the result is usually in the interval [0,1]. To convert from a linear RGB component to an 8-bit sRGB component, do this: (truncate (* 255 (gamma-compress n)))

[procedure] (gamma-expand n) → float

Performs gamma expansion (decoding) of the real number n, converting from sRGB space to linear RGB space. This implements the sRGB reverse transfer function, which approximates a gamma of 2.2.

n is usually in the interval [0,1], and the result is usually in the interval [0,1]. To convert from an 8-bit sRGB component to a linear RGB component, do this: (gamma-expand (/ n 255.0))

Low-level

For advanced users, macaw provides low-level versions of many procedures. These low-level procedures work directly with pointers or locatives. They give you more control to optimize performance-critical parts of your code, but they are less convenient and less safe.

Caution!

Pointers and locatives must be used with caution to avoid buffer overflows, segmentation faults, and other serious problems! These problems can crash your program or cause security vulnerabilities!

You must ensure that pointers and locatives refer to data of the correct type and size. For pointers and weak locatives, you must also ensure that the memory has not been garbage collected or freed.

For colors, you can get a compatible pointer from a color object using rgb-pointer etc. Or you can use a pointer to static memory, a locative of a blob, etc. The argument name indicates what type is required:

For arrays, the size of memory required depends on the array's pitch (number of bytes per row, including padding), and the array's height (number of rows). The exact requirements are described below for each array type.

Colors from memory

The color types in this library usually hold a locative of a SRFI-4 numeric vector (e.g. f32vector). But advanced users can use the procedures below to create colors from pointers to static memory, locatives of blobs, etc. This must be used with caution!

For example, this can be used to efficiently access parts of a large block of pixel data in memory. The color will directly read and write to the memory, without the expense of copying back and forth.

[procedure] (rgb-at float*_pointer #!optional parent) → rgb
[procedure] (rgb-pointer rgb_1) → pointer or locative
[setter] (set! (rgb-pointer rgb_1) float*_pointer)
[procedure] (rgb-parent rgb_1) → any
[setter] (set! (rgb-parent rgb_1) parent)

rgb-at creates an rgb color which reads and writes to the memory at pointer, which can be a pointer or a locative. The memory starting at pointer must contain four 32-bit floats in the order r, g, b, a. You can use rgb-pointer to get or set the pointer.

The optional parent can be any object that the color depends on. This reference prevents the parent from being garbage collected while the color is still accessing the parent's memory.

[procedure] (rgb8-at uchar*_pointer #!optional parent) → rgb8
[procedure] (rgb8-pointer rgb8_1) → pointer or locative
[setter] (set! (rgb8-pointer rgb8_1) uchar*_pointer)
[procedure] (rgb8-parent rgb8_1) → any
[setter] (set! (rgb8-parent rgb8_1) parent)

rgb8-at creates an rgb8 color which reads and writes to the memory at pointer, which can be a pointer or a locative. The memory starting at pointer must contain four unsigned 8-bit integers in the order r, g, b, a. You can use rgb8-pointer to get or set the pointer.

The optional parent can be any object that the color depends on. This reference prevents the parent from being garbage collected while the color is still accessing the parent's memory.

[procedure] (hsl-at float*_pointer #!optional parent) → hsl
[procedure] (hsl-pointer hsl_1) → pointer or locative
[setter] (set! (hsl-pointer hsl_1) float*_pointer)
[procedure] (hsl-parent hsl_1) → any
[setter] (set! (hsl-parent hsl_1) parent)

hsl-at creates an hsl color which reads and writes to the memory at pointer, which can be a pointer or a locative. The memory starting at pointer must contain four 32-bit floats in the order h, s, l, a. You can use hsl-pointer to get or set the pointer.

The optional parent can be any object that the color depends on. This reference prevents the parent from being garbage collected while the color is still accessing the parent's memory.

Arrays from memory

The array types in this library usually hold a locative of a SRFI-4 numeric vector (e.g. f32vector). But advanced users can use the procedures below to create arrays from pointers to static memory, locatives of blobs, etc. This must be used with caution!

For example, this can be used to efficiently access a large block of pixel data in memory. The array will directly read and write to the memory, without the expense of copying back and forth.

[procedure] (rgb-array-at pointer width height #!optional pitch) → rgb-array
[procedure] (rgb-array-parent rgb-array_1) → any
[setter] (set! (rgb-array-parent rgb-array_1) parent)

rgb-array-at creates an rgb-array which reads and writes to the memory at pointer, which can be a pointer or a locative. If pitch is omitted, it defaults to (* width 16). The memory starting at pointer must be at least (* pitch height) bytes long, containing groups of four 32-bit floats in the order r, g, b, a.

rgb-array-parent gets or sets the rgb-array's parent, which can be any object that the array depends on. This reference prevents the parent from being garbage collected while the array is still accessing the parent's memory.

[procedure] (rgb8-array-at pointer width height #!optional pitch) → rgb8-array
[procedure] (rgb8-array-parent rgb8-array_1) → any
[setter] (set! (rgb8-array-parent rgb8-array_1) parent)

rgb8-array-at creates an rgb8-array which reads and writes to the memory at pointer, which can be a pointer or a locative. If pitch is omitted, it defaults to (* width 4). The memory starting at pointer must be at least (* pitch height) bytes long, containing groups of four unsigned 8-bit integers in the order r, g, b, a.

rgb8-array-parent gets or sets the rgb8-array's parent, which can be any object that the array depends on. This reference prevents the parent from being garbage collected while the array is still accessing the parent's memory.

[procedure] (hsl-array-at pointer width height #!optional pitch) → hsl-array
[procedure] (hsl-array-parent hsl-array_1) → any
[setter] (set! (hsl-array-parent hsl-array_1) parent)

hsl-array-at creates an hsl-array which reads and writes to the memory at pointer, which can be a pointer or a locative. If pitch is omitted, it defaults to (* width 16). The memory starting at pointer must be at least (* pitch height) bytes long, containing groups of four 32-bit floats in the order h, s, l, a.

hsl-array-parent gets or sets the hsl-array's parent, which can be any object that the array depends on. This reference prevents the parent from being garbage collected while the array is still accessing the parent's memory.

Low-level array operations

[procedure] (array-ref-pointer array_1 x y) → pointer or locative
[procedure] (rgb-array-ref-pointer rgb-array_1 x y) → pointer or locative
[procedure] (rgb8-array-ref-pointer rgb8-array_1 x y) → pointer or locative
[procedure] (hsl-array-ref-pointer hsl-array_1 x y) → pointer or locative

Like array-ref etc. but returns a pointer or locative instead of a color object. This must be used with caution! The return value will be a pointer if the array holds a pointer, or a locative if the array holds a locative.

These procedures are equivalent to e.g. (rgb-pointer (rgb-array-ref rgb-array_1 x y)) but much more efficient. They are useful for optimization when using low-level color math procedures.

[procedure] (array-for-each-pointer proc array ...)
[procedure] (rgb-array-for-each-pointer proc rgb-array ...)
[procedure] (rgb8-array-for-each-pointer proc rgb8-array ...)
[procedure] (hsl-array-for-each-pointer proc hsl-array ...)

Like array-for-each etc. but proc is called with pointers or locatives instead of color objects. This must be used with caution! The proc will be called with a pointer if the array holds a pointer, or a locative if the array holds a locative.

(array-for-each-pointer
  (lambda (x y hsl-pointer_1 rgb8-pointer_2)
    (low-hsl->rgb8! hsl-pointer_1 rgb8-pointer_2))
  hsl-array_1 rgb8-array_2)

Low-level color math

These procedures perform the same computations as the color math procedures, but they operate directly on pointers or locatives. These procedures must be used with caution!

The last argument to every procedure below is the output pointer, where the result of the operation will be written. The output pointer can be the same as an input pointer, which will cause the input to be overwritten with the result.

[procedure] (low-rgb->rgb8! float*_in uchar*_out)
[procedure] (low-rgb->hsl! float*_in float*_out)
[procedure] (low-rgb8->rgb! uchar*_in float*_out)
[procedure] (low-rgb8->hsl! uchar*_in float*_out)
[procedure] (low-hsl->rgb! float*_in float*_out)
[procedure] (low-hsl->rgb8! float*_in uchar*_out)

Low-level versions of rgb->rgb8 etc.

[procedure] (low-rgb-add! float*_in1 float*_in2 float*_out)
[procedure] (low-rgb8-add! uchar*_in1 uchar*_in2 uchar*_out)
[procedure] (low-hsl-add! float*_in1 float*_in2 float*_out)

Low-level versions of rgb-add etc.

[procedure] (low-rgb-sub! float*_in1 float*_in2 float*_out)
[procedure] (low-rgb8-sub! uchar*_in1 uchar*_in2 uchar*_out)
[procedure] (low-hsl-sub! float-in1 float*_in2 float*_out)

Low-level versions of rgb-sub etc.

[procedure] (low-rgb-mul! float*_in1 float*_in2 float*_out)
[procedure] (low-rgb8-mul! uchar*_in1 uchar*_in2 uchar*_out)
[procedure] (low-hsl-mul! float*_in1 float*_in2 float*_out)

Low-level versions of rgb-mul etc.

[procedure] (low-rgb-scale! float*_in float_n float*_out)
[procedure] (low-rgb8-scale! uchar*_in float_n uchar*_out)
[procedure] (low-hsl-scale! float*_in float_n float*_out)

Low-level versions of rgb-scale etc.

[procedure] (low-rgb-lerp! float*_in1 float*_in2 float_t float*_out)
[procedure] (low-rgb8-lerp! uchar*_in1 uchar*_in2 float_t uchar*_out)
[procedure] (low-hsl-lerp! float*_in1 float*_in2 float_t float*_out)

Low-level versions of rgb-lerp etc.

[procedure] (low-hsl-mix! float*_accum float*_color float_weight float*_out)
[procedure] (low-rgb-mix! float*_accum float*_color float_weight float*_out)
[procedure] (low-rgb8-mix! uchar*_accum uchar*_color float_weight uchar*_out)

Low-level versions of rgb-mix etc.

This performs a weighted addition of color onto accum, writing the result to out (which is usually the same pointer as accum). Normally accum should start with all values set to zero, and the procedure should be called once per color being mixed.

[procedure] (low-rgb-under! float*_in1 float*_in2 float*_out)
[procedure] (low-rgb8-under! uchar*_in1 uchar*_in2 uchar*_out)
[procedure] (low-hsl-under! float*_in1 float*_in2 float*_out)

Low-level versions of rgb-under etc.

[procedure] (low-rgb-over! float*_in1 float*_in2 float*_out)
[procedure] (low-rgb8-over! uchar*_in1 uchar*_in2 uchar*_out)
[procedure] (low-hsl-over! float*_in1 float*_in2 float*_out)

Low-level versions of rgb-over etc.

Compatibility with other libraries

Macaw is designed to be easy to use with other libraries. Explanations and helper procedures are provided below. This code is released by its author(s) to the public domain.

Generic conversion

Most color libraries have an equivalent of macaw's rgb8 type, with red, green, blue, and possibly alpha, represented as integers from 0 to 255. You can convert any macaw color into rgb8 using color->rgb8, then destructure it using rgb8->values, then call the other library's constructor, like so:

(import (prefix macaw "macaw:"))

;;; Converts any macaw color to rgb8, destructures it, then calls
;;; make-foo with the r g b a values.
(define (macaw->foo color)
  (let-values (((r g b a) (macaw:rgb8->values
                           (macaw:color->rgb8 color))))
    (make-foo r g b a)))

;;; Converts a foo color to a macaw rgb8, assuming that foo->list
;;; returns an (r g b) or (r g b a) list.
(define (foo->rgb8 color)
  (apply macaw:rgb8 (foo->list color)))

sdl2

The generic approach described above can be used with the sdl2 egg, but much greater performance is possible because the rgb8 type has the same memory layout as sdl2:color. Likewise, the rgb8-array type has the same memory layout as sdl2:surface pixel data with the abgr8888 pixel format on little-endian computers (most common), or the rgba8888 pixel format on big-endian computers (less common).

This means that macaw and sdl2 can directly access the same memory, with no destructuring or copying, and only minimal allocation of memory to create a new record object.

You can use these helper procedures to interoperate between macaw and sdl2:

;;; This code is released by its author(s) to the public domain.

(import (prefix macaw "macaw:")
        (prefix sdl2 "sdl2:")
        (prefix (only sdl2-internals
                      wrap-color
                      unwrap-color)
                "sdl2:"))

;;; Creates a new macaw:rgb8 copied from an sdl2:color.
(define (sdl2->rgb8 c)
  (macaw:rgb8-at (sdl2:unwrap-color (sdl2:color-copy c))))

;;; Creates a macaw:rgb8 which shares memory with an sdl2:color.
(define (sdl2->rgb8/shared c)
  (macaw:rgb8-at (sdl2:unwrap-color c) c))

;;; Creates a new sdl2:color copied from any type of macaw color.
(define (macaw->sdl2 c)
  (sdl2:wrap-color (macaw:rgb8-pointer (macaw:color->rgb8/new c))))

;;; Creates an sdl2:color which shares memory with a macaw:rgb8.
;;; You must prevent the rgb8 from being garbage collected while
;;; the sdl2:color is still using its memory.
(define (rgb8->sdl2/shared c)
  (sdl2:wrap-color (macaw:rgb8-pointer c)))

;;; Create an sdl2:surface with a pixel format that is compatible with
;;; macaw:rgb8-array. The compatible pixel format depends on whether
;;; this computer is little-endian (most common) or big-endian.
(define (make-rgb8-surface width height)
  (let-values (((bpp rmask gmask bmask amask)
                (sdl2:pixel-format-enum-to-masks
                 (cond-expand
                  (little-endian 'abgr8888)
                  (else 'rgba8888)))))
    (sdl2:create-rgb-surface*
     0 width height
     bpp rmask gmask bmask amask)))

;;; Creates a macaw:rgb8-array that accesses the surface's pixel data.
;;; The surface must have the correct pixel format for this computer.
(define (surface->rgb8-array/shared surface)
  (assert (eq? (sdl2:pixel-format-format
                (sdl2:surface-format surface))
               (cond-expand
                (little-endian 'abgr8888)
                (else 'rgba8888)))
          "surface pixel format not compatible with rgb8-array"
          (sdl2:pixel-format-format
           (sdl2:surface-format surface)))
  (let ((array (macaw:rgb8-array-at (sdl2:surface-pixels-raw surface)
                                    (sdl2:surface-w surface)
                                    (sdl2:surface-h surface)
                                    (sdl2:surface-pitch surface))))
    ;; Set surface as array's parent to prevent surface from being
    ;; garbage collected while array is still using its memory.
    (set! (macaw:rgb8-array-parent array) surface)
    array))

;;; Creates an sdl2:surface that accesses the rgb8-array's memory.
;;; You must prevent the rgb8-array from being garbage collected
;;; while the surface is still using its memory.
(define (rgb8-array->surface/shared array)
  (let-values (((bpp rmask gmask bmask amask)
                (sdl2:pixel-format-enum-to-masks
                 (cond-expand
                  (little-endian 'abgr8888)
                  (else 'rgba8888)))))
    (sdl2:create-rgb-surface-from*
     (macaw:rgb8-array-pointer array)
     (macaw:rgb8-array-width array)
     (macaw:rgb8-array-height array)
     bpp
     (macaw:rgb8-array-pitch array)
     rmask gmask bmask amask)))

web-colors

Macaw colors are easily converted to lists compatible with the web-colors egg. This process is similar to the generic conversion process except that web colors always use an alpha range of 0.0 to 1.0, whereas macaw's rgb8 type uses an alpha range of 0 to 255.

You can use these helper procedures to interoperate between macaw and web-colors:

;;; This code is released by its author(s) to the public domain.

(import (prefix macaw "macaw:")
        (prefix web-colors "wc:"))

;;; Creates a new macaw color from any web color list.
(define (web-color->macaw wc)
  (case (and (wc:color-list? wc) (car wc))
    ((rgb)  (macaw:rgb8 (list-ref wc 1)
                        (list-ref wc 2)
                        (list-ref wc 3)
                        (inexact->exact (floor (* 255 (list-ref wc 4))))))
    ((rgb%) (apply macaw:rgb (map exact->inexact (cdr wc))))
    ((hsl)  (apply macaw:hsl (map exact->inexact (cdr wc))))
    (else   (error "Not a web-color list" wc))))

;;; Creates a new web color list from any macaw color.
(define (macaw->web-color c)
  (cond
   ((macaw:rgb8? c)
    (let-values (((r g b a) (macaw:rgb8->values c)))
      (list 'rgb r g b (/ a 255.0))))
   ((macaw:rgb? c)
    (cons 'rgb% (macaw:rgb->list c)))
   ((macaw:hsl? c)
    (cons 'hsl (macaw:hsl->list c)))
   (else
    (error "Not a macaw color" c))))


;;; Attempts to parse the given web color string and return it as
;;; a new macaw color. Raises an exception if parsing fails.
(define (color-string->macaw s)
  (web-color->macaw (wc:parse-web-color s)))

;;; Returns a web color string from any macaw color.
;;; Values are rounded to specified precision to compensate for
;;; floating point rounding error, so the output is more friendly.
(define (macaw->color-string c #!optional (precision 3))
  (define (round-float n)
    (/ (round (* n (expt 10 precision)))
       (expt 10 precision)))
  (let ((wc (macaw->web-color c)))
    (wc:web-color->string
     (cons (car wc)
           (map round-float (cdr wc))))))

;;; Returns a #RRGGBB or #RRGGBBAA string from any macaw color.
;;; The macaw color is converted to rgb8.
(define (macaw->hex-string c)
  (let-values (((r g b a) (macaw:rgb8->values (macaw:color->rgb8 c))))
    (wc:rgb-color->hex-string (list 'rgb r g b (/ a 255.0)))))

Version history

0.1.0 (2020-04-20)
Initial release. rgb, rgb8, and hsl color types. rgb-array, rgb8-array, and hsl-array types. add, sub, mul, scale, lerp, mix, under, and over math operations.