ijlyttle / colorpath Goto Github PK
View Code? Open in Web Editor NEWBuild Color Paths
Home Page: https://ijlyttle.github.io/colorpath
License: Other
Build Color Paths
Home Page: https://ijlyttle.github.io/colorpath
License: Other
Maybe also df_hcl - find a good set of points. It will make the examples easier.
i.e. it should figure out which ends of the two palettes meet, then orient the palettes thusly.
A color-path can be thought-of as a special case of a sequential palette:
All paths in a family will have the same luminance for start and end points. This way, parts of different paths can be joined to create a diverging palette.
Change the names and the arguments to make a smoother introduction to a luminance rescaler.
# linear input-rescaler, maps c(0, 1) to `range`
rescaler_linear_input <- function(range) # returns cpath_rescaler
# linear luminance-rescaler, maps c(0, 1) to luminance `range` for `palette`
rescaler_linear_luminance <- function(range, palette) # returns cpath_rescaler
I think it's a bit clunky to have to specify a palette twice to rescale it; provides an opportunity to make a mistake.
In the future, consider a wrapper function.
I think we can remove consideration of the LUV space - we were using it for its Euclidean distance metric. If we calculate distance using farver::compare_colour()
, perhaps we can rescale the input so we are perceputally uniform according to CIE2000.
As well, I think this would solve #25 (inconsistent hue) and #33 (chroma floor), as the root of both of these problems is translation to/from LUV space.
plots should always include the entire luminance domain, as well as the limit where chroma is zero
when building an LUV palette, keep control-points as attribute
accessor function spec_luv()
to get the control points
fn_hue()
becomes the main attraction; fn_max_chroma()
and colors
become attributes.
structure(
fn_hue,
class = ("pth_surface"),
fn_max_chroma = fn_max_chroma,
colors = colors
)
comment to future self from self-who-is-just-trying-to-get-something-working-now
leftover from #54
pth_data_max_chroma()
and pth_layer_max_chroma()
with colorio, we have access to a bunch of color spaces, including CAM16-UCS and Jzazbz, with the hope of "better" distance measurements.
The base color space for colorio is xyz100 - and we have a way to translate to it using farver::decode_colour()
Currently, the internal function .get_distance()
has a signature:
.get_distance <- function(pal_luv, n, method, ...)
Here, method
refers to farver::compare_colour()
; we use a default value of "cie2000"
.
What if our method
argument used a syntax like "farver::cie2000"
, and could also take a value like "colorio::CAM16-UCS"
?
the color spaces have moved, e.g.: colorio$CIELUV
-> colorio$cs$CIELUV
also the conversion to srgb has been renamed: colorio$SrgbLinear()$to_srgb255()
-> colorio$cs$SrgbLinear()$to_rgb255()
Our palette-functions work in LUV space, not in hex-code space.
palette_hex <- function(palette) # returns palette-function that returns hex-codes
finding out for Jzazbz, that out-of-gamut may also produce an error - we can use a try()
block and return a high out-of-gamut value rather than throwing an error.
This would give us a quick indication of what a surface looks like, using colorspace::hclplot()
.
Converting from an HCL data frame, as_mat_luv()
has a default chroma_min
of 0.01
. This is done so that we can preserve the hue information when the chroma is zero.
In practice, we this "gap" will have no perceptual difference.
In theory, we should be able to set the floor much lower, e.g. 1.e-6
. There seems to be some rounding errors introduced by the farver color-conversion functions. Given that HCL <-> LUV is a polar transformation, we should be able to do this ourselves without risk.
This is the mechanism that underpins theming for color scales.
Function with S3 class cpath_rescaler
:
x
(vector, each member between 0 and 1)y
), each member between 0 and 1x
and y
# maps (0, 1) according to equally-spaced points on Bézier spline
rescaler_bezier <- function(luv) # returns cpath_rescaler
# maps (0, 1) to (x0, x1)
rescaler_linear <- function(x0, x1) # returns cpath_rescaler
like altair
Using the original hex-color will be more informative; we can always create an index post-facto.
A rescaler based on perceptual distance:
rescaler_dist <- function(pal_luv, tolerance = 1.e-4, method = "cie2000", ...)
publicize, export, etc.
To rescale a palette, you need also to specify a rescaler function - this feels a bit inefficient.
New functions:
pal_luv_rescale_x <- function(pal_luv, range)
pal_luv_rescale_lum <- function(pal_luv, range)
pth_mat_max_chroma(x, ...) {
# returns matrix with max-chroma for surface, palette, colors
}
Thinking to implement initially for a surface, needs an n_step
argument.
Will also be useful to implement rbind()
for pth_mat
- can bind only if using the same colorspace.
reducing the scope to get this merged sooner.
will create other issues for vignettes to show how functions work.
now the fun begins, again:
a palette refers to a continuous-palette function that, for every input (0 <= x <= 1
), returns a color.
a hue surface refers to a function that, for each input luminance (0 <= lum <= 100
), returns a hue (0 <= hue < 360
).
a chroma trajectory refers to a function that, for each input (0 <= x <= 1
), returns a set of luminance and chroma; it is implemented using a Bézier curve.
We can construct a palette by projecting a chroma trajectory onto a hue surface.
Something that could build on colorspace::hclplot()
I will rarely create issues with titles better than this one. 🎶 What the world needs now...
We want a convenient way to create a matrix of luv values.
#' @param df_hcl `data.frame` with variables `h`, `c`, `l`
luv <- function(df_hcl)
it will make remotes happier
One thing coming into focus for me is that continuous palettes are driven by luminance.
Let's say that we can define a surface in HCL space as the collection of points where
hue <- function(lum) {
h <- hue_0 + (lum / 100) (hue_1 - hue_0)
h %% 360
}
Let's postulate that a family of palettes, lets say "blues", can be defined as being drawn along such a surface defined using hue_0
and hue_1
.
To specify a palette function, we need would only specify control points using luminance and chroma.
To get the entire set of control points, we would need to calculate the hue, given the luminance, for the family.
Need to work in CVD
# returns pth_mat
pth_mat_gamut <- function(n_point = 5, transformer = pth_to_cieluv)
# returns data frame with columns cvd (factor), severity (factor)
pth_cvd_grid <- function(cvd = c("none", "deutan", "protan", "tritan"), severity = 1)
pth_cvd_grid_full <- function(cvd = c("deutan", "protan", "tritan"), severity = c(0, 0.5, 1))
# returns data frame with luminance, chroma, hue, hex, cvd, severity
pth_data_cvd <- function(x, cvd = pth_cvd_grid(), ...)
pth_data_cvd.pth_mat <- function(x, cvd = pth_cvd_grid(), ...)
pth_data_cvd.character <- function(x, cvd = pth_cvd_grid(), transformer = pth_to_cieluv, ...)
pth_data_cvd.pth_palette <- function(x, cvd = pth_cvd_grid(), n_point = 11, ...)
pth_mat_gamut()
pth_cvd_grid()
pth_cvd_grid_full()
pth_data_cvd()
pth_plot_polar()
# returns ggplot
pth_plot_polar <- function(x, ...)
pth_plot_polar.tbl_df <- function(x, ...)
pth_plot_polar.pth_mat <- function(x, cvd = pth_cvd_grid_none(), ...)
pth_plot_polar.character <- function(x, cvd = pth_cvd_grid_none(), transformer = pth_to_cieluv, ...)
pth_plot_polar.pth_palette <- function(x, cvd = pth_cvd_grid_none(), n_point = 11, ...)
Thinking not to do the last one since it would not offer much, if anything, beyond geom_point()
# returns geom_point
pth_layer_polar <- function(x, ...)
pth_layer_polar.tbl_df <- function(x, ...)
pth_layer_polar.pth_palette <- function(x, n_point = 11, ...)
# these return data frames
# if there is more than one set of attributes, it uses negative chroma for the first set
# this will make plotting more straight-forward
# lum, chroma, hue, hex
pth_data_surface_raster <- function(sfc)
# lum, chroma, hue
pth_data_control_points <- function(traj)
# lum, chroma, hue, hex
# use variation in luminance to set chroma?
pth_data_palette <- function(palette, n)
# lum, chroma, hue, hex
# use variation in luminance to set chroma?
pth_data_max_chroma <- function(palette, n)
pth_plot_palette <- function(palette, ...) # methods for pth_palette_path, pth_palette_hex
pth_plot_surface <- function(x, ...) # methods for pth_surface, pth_palette_path, pth_data_surface
pth_layer_control_points <- function(x, ...) # methods for pth_palette_path, pth_data_control_points
pth_layer_max_chroma <- function(x, ...) # methods for pth_data_max_chroma
# need layers for individual colors, palette paths
This is also a re-imagining of the API 😬
I think that we want to focus on a "class" of color spaces that I'll call Lab-100
class. These color spaces have:
Examples of these colorspaces are:
#' @param mat, `matrix`
#' @param hex, `character` with S3 class `pth_hex`, representing a six-character hex code
To convert between different expressions of color:
#' @param color object with class `pth_hex`, `pth_mat`
# @param ... additional parameters needed to define a color space
#' if `NULL`, will look for `color_space` attribute on `color`
pth_to_cielab <- function(color, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cielab`
pth_to_cieluv <- function(color, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cieluv`
pth_to_cam02ucs <- function(color, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cam02ucs`
pth_to_cam16ucs <- function(color, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cam16ucs`
pth_to_jzazbz100 <- function(color, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_jzazbz100`
pth_to_hex <- function(color) # returns `character` with S3 class `pth_hex`
To create a structure:
#' These functions put classes onto existing structures and values.
pth_new_cielab <- function(mat, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cielab`
pth_new_cieluv <- function(mat, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cieluv`
pth_new_cam02ucs <- function(mat, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cam02ucs`
pth_new_cam16ucs <- function(mat, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_cam16ucs`
pth_new_jzazbz100 <- function(mat, ...) # returns `matrix` with S3 classes `pth_mat`, `pth_jzazbz100`
pth_new_hex <- function(hex) # returns `character` with S3 class `pth_hex`, representing a six-character hex code
Will also need helpers:
pth_to_cartesian <- function(mat, chroma_min = 1.e-4) # returns `matrix`
pth_to_polar <- function(mat) # returns `matrix`
Let's think a bit about the S3 classes:
pth_mat
: has three columns representing luminance, blue-yellow, green-red
pth_cieluv
: pth_mat
that uses CIELUV color space,
...
pth_hex
: character
representing a six-character hex code
Given a palette function, a number of intervals, and a name of a method, return a vector (length n
) of perceptual distances.
get_distance <- function(pal_luv, n = 20, method = "cie2000")
This will return a function with S3 class cpath_palette
:
x
(vector, each member between 0 and 1)"l"
, "u"
, "v"
)# given matrix of luv values
palette_bezier <- function(luv) # returns cpath_palette
to support pkgdown site
distance is a Euclidean-like distance measured in a Cartesian color space.
metric is a measurement, e.g. "cie2000", non-Euclidean, triangle inequality does not apply.
#' @param color_a, color_b object with S3 class `character`, `pth_hex` or `pth_mat`.
#' @param transformer `function` that returns a `pth_mat`.
#' @param non_luminance_weight `numeric` non-negative number used to weight the non-luminance dimensions
#' in the distance calculation
#' @param metric `character` indicates which distance metric to use (from the `farver` package).
#'
#' @return `double` perceptual distance
#'
pth_distance <- function(color_a, color_b = NULL, transformer = NULL, non_luminance_weight = 1)
pth_metric <- function(color_a, color_b = NULL, metric = c("cie2000", "cie94", "cie1976", "cmc"))
Colors used to measure:
color_b
is NULL
, we get the distances between consecutive rows of color_a
.color_b
has length 1
, it is repeated to have the length of color_a
.color_b
must have the same length as color_a
, in which case a pairwise comparison is made.A transformer
is a function used to convert a color to a given color space, e.g. pth_to_cieluv
:
transformer
is specified, the distance calculation is made using that color space.transformer
is NULL
:
color_a
and color_b
are both hex-codes, we use pth_to_cieluv
.color_a
and color_b
both use the same color space, we use identity
.color_a
and color_b
use different color spaces, we throw an error.The non_luminance_weight
is an experiment, to be determined if it can be useful or not. It is used to scale the non-luminance dimensions of the colors:
non_luminance_weight
is 1, the standard Euclidean distance is returned.non_luminance_weight
is 0, the distance is the change in luminance only.We will need to determine, for a given color space, the extent of the RGB gamut.
# returns `logical`
pth_in_gamut <- function(color)
# returns `double`, max chroma given luminance and hue, in provided color space
pth_max_chroma <- function(mat)
# returns object of same type, with out-of-gamut colors replaced with max-chroma colors
pth_clip_chroma <- function(color)
We could also use an internal function:
# returns `double`, negative means inside gamut, positive means outside
x_gamut <- function(color)
This would be an S3 generic that would act similarly to pth_transformer()
, in that it would return a function that could create a pth_mat
using a given color space.
I think this would be cleaner than the current pth_mat_replace_data()
; I would look to deprecate that...
pth_new_trajectory()
pth_new_palette_path()
pth_plot_palette_path()
pth_layer_control_points()
pth_layer_palette()
add pkgdown site
We need a set of functions to calculate perceptual distances:
# returns double
pth_distance_euclid(color_a, color_b, transformer, non_luminance_weight, ...)
pth_distance_metric(color_a, color_b, method, ...)
# returns data frame with cols:
# - hex_original_a
# - hex_original_b
# - cvd
# - severity
# - hex_a
# - hex_b
# - distance
#
pth_data_cat_euclid(color, cvd, transformer, non_luminance_weight, ...)
pth_data_cat_metric(color, cvd, method, ...)
pth_data_qnt_euclid(palette, n_step, cvd, transformer, non_luminance_weight, ...)
pth_data_qnt_metric(palette, n_step, cvd, method, ...)
# rescale the input to a palette
rescale_palette <- function(palette, rescaler) # returns cpath_palette
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.