diff --git a/sites/postcss-preset-env/blog/relative-color-syntax-channel-values.md b/sites/postcss-preset-env/blog/relative-color-syntax-channel-values.md
new file mode 100644
index 000000000..065fdcab8
--- /dev/null
+++ b/sites/postcss-preset-env/blog/relative-color-syntax-channel-values.md
@@ -0,0 +1,137 @@
+---
+title: 'Relative Color Syntax : Channel Values'
+description: Inspect the channel values for the origin color.
+date: 2024-03-04
+---
+
+With relative color syntax it can become confusing what the exact values are at each step and how they can be used in math expressions.
+This little tool aims to show the channel values at each step of the process.
+
+
+
+Channel values for:
+
+
+
+
+
l 0,1
+
c 0,0.4
+
h 0,360
+
alpha
+
+
+
+
Origin color
+
0.628
+
0.258
+
29.234
+
1.000
+
+
+
+
Expressions
+
calc(l / 2)
+
0.268
+
h
+
1.000
+
+
+
+
Output color
+
0.314
+
0.268
+
29.234
+
1.000
+
+
+
+
+The indicated ranges of values correspond to 0% and 100% for each channel.
+They are not the minimum and maximum allowed values, you can use values beyond these ranges.
+
+{% block scripts %}{% endblock %}
+
+
diff --git a/sites/postcss-preset-env/rollup.config.mjs b/sites/postcss-preset-env/rollup.config.mjs
index 92b118633..b996a05d9 100644
--- a/sites/postcss-preset-env/rollup.config.mjs
+++ b/sites/postcss-preset-env/rollup.config.mjs
@@ -14,6 +14,7 @@ export default [
'blog_color_parser_2023_03_27',
'blog_color_mix_2023_03_27',
'blog_relative_color_syntax_2023_05_22',
+ 'blog_relative_color_syntax_channel_values_2024_03_02',
].map(name => ({
input: `src/static/js/${name}.js`,
output: [
diff --git a/sites/postcss-preset-env/src/static/js/blog_relative_color_syntax_channel_values_2024_03_02.js b/sites/postcss-preset-env/src/static/js/blog_relative_color_syntax_channel_values_2024_03_02.js
new file mode 100644
index 000000000..088bd578e
--- /dev/null
+++ b/sites/postcss-preset-env/src/static/js/blog_relative_color_syntax_channel_values_2024_03_02.js
@@ -0,0 +1,274 @@
+import { color, serializeOKLCH } from '@csstools/css-color-parser';
+import { isCommentNode, isFunctionNode, isTokenNode, isWhitespaceNode, parseComponentValue } from '@csstools/css-parser-algorithms';
+import { tokenize } from '@csstools/css-tokenizer';
+
+function renderResult() {
+ document.querySelectorAll('.color-input').forEach((inputEl) => {
+ const [
+ outputChannel1Name,
+ outputChannel1Input,
+ outputChannel1Calc,
+ outputChannel1Output,
+ outputChannel2Name,
+ outputChannel2Input,
+ outputChannel2Calc,
+ outputChannel2Output,
+ outputChannel3Name,
+ outputChannel3Input,
+ outputChannel3Calc,
+ outputChannel3Output,
+ outputChannel4Name,
+ outputChannel4Input,
+ outputChannel4Calc,
+ outputChannel4Output,
+ ] = [
+ document.querySelector('[channel~="1"][channel~="name"]'),
+ document.querySelector('[channel~="1"][channel~="input"]'),
+ document.querySelector('[channel~="1"][channel~="calc"]'),
+ document.querySelector('[channel~="1"][channel~="output"]'),
+ document.querySelector('[channel~="2"][channel~="name"]'),
+ document.querySelector('[channel~="2"][channel~="input"]'),
+ document.querySelector('[channel~="2"][channel~="calc"]'),
+ document.querySelector('[channel~="2"][channel~="output"]'),
+ document.querySelector('[channel~="3"][channel~="name"]'),
+ document.querySelector('[channel~="3"][channel~="input"]'),
+ document.querySelector('[channel~="3"][channel~="calc"]'),
+ document.querySelector('[channel~="3"][channel~="output"]'),
+ document.querySelector('[channel~="4"][channel~="name"]'),
+ document.querySelector('[channel~="4"][channel~="input"]'),
+ document.querySelector('[channel~="4"][channel~="calc"]'),
+ document.querySelector('[channel~="4"][channel~="output"]'),
+ ];
+
+ if (
+ !outputChannel1Name ||
+ !outputChannel1Input ||
+ !outputChannel1Calc ||
+ !outputChannel1Output ||
+ !outputChannel2Name ||
+ !outputChannel2Input ||
+ !outputChannel2Calc ||
+ !outputChannel2Output ||
+ !outputChannel3Name ||
+ !outputChannel3Input ||
+ !outputChannel3Calc ||
+ !outputChannel3Output ||
+ !outputChannel4Name ||
+ !outputChannel4Input ||
+ !outputChannel4Calc ||
+ !outputChannel4Output
+ ) {
+ return;
+ }
+
+ const value = inputEl.value;
+ if (!value) {
+ return;
+ }
+
+ const componentValue = parseComponentValue(tokenize({ css: value.trim() }));
+ if (!componentValue || !isFunctionNode(componentValue)) {
+ inputEl.style.outline = '2px solid rgb(255 0 0 / 70%)';
+ return;
+ }
+
+ const functionName = componentValue.getName().toLowerCase();
+
+ const originColor = (() => {
+ let didSeeFrom = false;
+
+ for (let i = 0; i < componentValue.value.length; i++) {
+ const node = componentValue.value[i];
+ if (isCommentNode(node) || isWhitespaceNode(node)) {
+ continue;
+ }
+
+ if (isTokenNode(node) && node.toString().toLowerCase() === 'from') {
+ didSeeFrom = true;
+ continue;
+ }
+
+ if (!didSeeFrom) {
+ return;
+ }
+
+ return node;
+ }
+ })();
+
+ if (!originColor) {
+ inputEl.style.outline = '2px solid rgb(255 0 0 / 70%)';
+ return;
+ }
+
+ let { channel1, channel2, channel3, alpha, colorSpace } = (() => {
+ let didSeeFrom = false;
+ let didSeeOrigin = false;
+
+ let nodes = [];
+ let expectColorSpace = functionName === 'color';
+ let colorSpace = null;
+
+ for (let i = 0; i < componentValue.value.length; i++) {
+ const node = componentValue.value[i];
+ if (isCommentNode(node) || isWhitespaceNode(node)) {
+ continue;
+ }
+
+ if (isTokenNode(node) && node.toString().toLowerCase() === 'from') {
+ didSeeFrom = true;
+ continue;
+ }
+
+ if (!didSeeFrom) {
+ return {};
+ }
+
+ if (!didSeeOrigin) {
+ didSeeOrigin = true;
+ continue;
+ }
+
+ nodes.push(node);
+ }
+
+ if (expectColorSpace) {
+ colorSpace = nodes.splice(0, 1);
+ }
+
+ if (nodes.length > 3) {
+ const slash = nodes[3];
+ if (!isTokenNode(slash) || slash.toString() !== '/') {
+ return {};
+ }
+
+ nodes.splice(3, 1);
+ }
+
+ if (nodes.length < 3 || nodes.length > 4) {
+ return {};
+ }
+
+ return {
+ channel1: nodes[0],
+ channel2: nodes[1],
+ channel3: nodes[2],
+ alpha: nodes[3],
+ colorSpace,
+ };
+ })();
+
+ if (!channel1 || !channel2 || !channel3) {
+ inputEl.style.outline = '2px solid rgb(255 0 0 / 70%)';
+ return;
+ }
+
+ let passThroughColor = originColor;
+ let channelNames = ['-', '-', '-'];
+ let channelPercentageReferenceValues = ['', '', ''];
+ switch (functionName) {
+ case 'rgb':
+ passThroughColor = parseComponentValue(tokenize({ css: `rgb(from ${originColor.toString()} r g b / alpha)` }));
+ channelNames = ['r', 'g', 'b'];
+ channelPercentageReferenceValues = ['0,255', '0,255', '0,255'];
+ break;
+ case 'hsl':
+ passThroughColor = parseComponentValue(tokenize({ css: `hsl(from ${originColor.toString()} h s l / alpha)` }));
+ channelNames = ['h', 's', 'l'];
+ channelPercentageReferenceValues = ['0,360', '0,100', '0,100'];
+ break;
+ case 'hwb':
+ passThroughColor = parseComponentValue(tokenize({ css: `hwb(from ${originColor.toString()} h w b / alpha)` }));
+ channelNames = ['h', 'w', 'b'];
+ channelPercentageReferenceValues = ['0,360', '0,100', '0,100'];
+ break;
+ case 'lab':
+ passThroughColor = parseComponentValue(tokenize({ css: `lab(from ${originColor.toString()} l a b / alpha)` }));
+ channelNames = ['l', 'a', 'b'];
+ channelPercentageReferenceValues = ['0,100', '-125,125', '-125,125'];
+ break;
+ case 'oklab':
+ passThroughColor = parseComponentValue(tokenize({ css: `oklab(from ${originColor.toString()} l a b / alpha)` }));
+ channelNames = ['l', 'a', 'b'];
+ channelPercentageReferenceValues = ['0,1', '-0.4,0.4', '-0.4,0.4'];
+ break;
+ case 'lch':
+ passThroughColor = parseComponentValue(tokenize({ css: `lch(from ${originColor.toString()} l c h / alpha)` }));
+ channelNames = ['l', 'c', 'h'];
+ channelPercentageReferenceValues = ['0,100', '0,150', '0,360'];
+ break;
+ case 'oklch':
+ passThroughColor = parseComponentValue(tokenize({ css: `oklch(from ${originColor.toString()} l c h / alpha)` }));
+ channelNames = ['l', 'c', 'h'];
+ channelPercentageReferenceValues = ['0,1', '0,0.4', '0,360'];
+ break;
+ case 'color':
+ if (colorSpace?.toString().includes('xyz')) {
+ passThroughColor = parseComponentValue(tokenize({ css: `color(from ${originColor.toString()} ${colorSpace.toString()} x y z / alpha)` }));
+ channelNames = ['x', 'y', 'z'];
+ channelPercentageReferenceValues = ['0,1', '0,1', '0,1'];
+ } else if (colorSpace) {
+ passThroughColor = parseComponentValue(tokenize({ css: `color(from ${originColor.toString()} ${colorSpace.toString()} r g b / alpha)` }));
+ channelNames = ['r', 'g', 'b'];
+ channelPercentageReferenceValues = ['0,1', '0,1', '0,1'];
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ outputChannel1Name.innerHTML = channelNames[0] + ` ${channelPercentageReferenceValues[0]} `;
+ outputChannel2Name.innerHTML = channelNames[1] + ` ${channelPercentageReferenceValues[1]} `;
+ outputChannel3Name.innerHTML = channelNames[2] + ` ${channelPercentageReferenceValues[2]} `;
+ outputChannel4Name.innerHTML = 'alpha';
+
+ const passThroughColorValue = color(passThroughColor);
+ if (!passThroughColorValue) {
+ inputEl.style.outline = '2px solid rgb(255 0 0 / 70%)';
+ return;
+ }
+
+ outputChannel1Calc.value = channel1.toString();
+ outputChannel2Calc.value = channel2.toString();
+ outputChannel3Calc.value = channel3.toString();
+ outputChannel4Calc.value = alpha ? alpha.toString() : round(passThroughColorValue.alpha);
+
+ const outputColorValue = color(componentValue);
+ if (!outputColorValue) {
+ inputEl.style.outline = '2px solid rgb(255 0 0 / 70%)';
+ return;
+ }
+
+ const outputColorValueStr = serializeOKLCH(outputColorValue).toString();
+
+ inputEl.style.outline = 'none';
+
+ if (passThroughColorValue.colorNotation === 'rgb' || passThroughColorValue.colorNotation === 'hex') {
+ passThroughColorValue.channels = passThroughColorValue.channels.map((channel) => channel * 255);
+ }
+
+ outputChannel1Input.value = round(passThroughColorValue.channels[0]);
+ outputChannel2Input.value = round(passThroughColorValue.channels[1]);
+ outputChannel3Input.value = round(passThroughColorValue.channels[2]);
+ outputChannel4Input.value = round(passThroughColorValue.alpha);
+
+ if (outputColorValue.colorNotation === 'rgb' || outputColorValue.colorNotation === 'hex') {
+ outputColorValue.channels = outputColorValue.channels.map((channel) => channel * 255);
+ }
+
+ outputChannel1Output.value = round(outputColorValue.channels[0]);
+ outputChannel2Output.value = round(outputColorValue.channels[1]);
+ outputChannel3Output.value = round(outputColorValue.channels[2]);
+ outputChannel4Output.value = round(outputColorValue.alpha);
+
+ document.getElementById('color-input-label-1').style.setProperty('--color', outputColorValueStr);
+ });
+}
+
+function round(x) {
+ return Math.round(x * 1000) / 1000;
+}
+
+addEventListener('change', renderResult);
+addEventListener('keyup', renderResult);