From 66e15931f5ffd25f86e68a71e44fff05c90febbb Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 10 Sep 2026 18:39:47 -0700 Subject: [PATCH] Add a direct-link abstract color study with fixed-hue saturation controls. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- public/color-rings.css | 20 ++++++++++++++++ public/color-rings.html | 38 ++++++++++++++++++++++++++++++ public/color-rings.js | 45 ++++++++++++++++++++++++++++++++++++ scripts/color-rings.test.mjs | 37 +++++++++++++++++++++++++++++ src/index.tsx | 7 ++++++ 5 files changed, 147 insertions(+) create mode 100644 public/color-rings.css create mode 100644 public/color-rings.html create mode 100644 public/color-rings.js create mode 100644 scripts/color-rings.test.mjs diff --git a/public/color-rings.css b/public/color-rings.css new file mode 100644 index 0000000..fecae95 --- /dev/null +++ b/public/color-rings.css @@ -0,0 +1,20 @@ +:root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; color: #202020; background: #fff; } +* { box-sizing: border-box; } +body { margin: 0; } +.color-rings { width: min(100% - 40px, 640px); margin: 0 auto; padding: 44px 0 36px; } +.color-rings h1 { margin: 0; font-size: 18px; font-weight: 500; letter-spacing: -.025em; } +.color-rings header p { margin: 8px 0 0; color: #666; font-size: 13px; } +.rings { display: block; width: min(100%, 560px); height: auto; margin: 20px auto 24px; } +.controls { max-width: 460px; margin: auto; } +.control-heading { display: flex; align-items: center; gap: 16px; font-size: 14px; } +.control-heading output { margin-left: auto; font-variant-numeric: tabular-nums; } +.controls button { font: inherit; color: inherit; background: white; border: 1px solid #bbb; border-radius: 4px; min-height: 44px; padding: 0 14px; cursor: pointer; } +.controls input { display: block; width: 100%; height: 44px; margin: 8px 0 0; accent-color: #252525; cursor: pointer; } +.controls :focus-visible { outline: 2px solid #202020; outline-offset: 4px; } +.range-ends { display: flex; justify-content: space-between; color: #666; font-size: 12px; } +.palette { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 30px 0 20px; } +.palette dt { font-size: 12px; color: #666; } +.palette dd { margin: 6px 0 0; font-size: 14px; } +.palette code { font-variant-numeric: tabular-nums; } +#color-help, noscript p { font-size: 12px; line-height: 1.6; color: #666; margin: 0; } +@media (max-width: 400px) { .color-rings { padding-top: 28px; } .rings { margin: 20px auto; } .palette dt { font-size: 11px; } } diff --git a/public/color-rings.html b/public/color-rings.html new file mode 100644 index 0000000..65045a1 --- /dev/null +++ b/public/color-rings.html @@ -0,0 +1,38 @@ + + + + + + + + Color rings + + + + +
+

Color rings

Three colors. One quiet experiment.

+ + Three concentric color rings + Clay on the outside, ochre in the middle, blue-gray patina inside. The intensity control changes their saturation. + + + + + + +
+
125%
+ + +
+
Clay · outer
#D37E6C
+
Ochre · middle
#E2BB6D
+
Patina · inner
#90ADA9
+
+

100% is the original palette. Hue and HSL lightness stay fixed; saturation stops at 100%. Readouts are the exact sRGB hex colors shown.

+ +
+
+ + diff --git a/public/color-rings.js b/public/color-rings.js new file mode 100644 index 0000000..d5637b4 --- /dev/null +++ b/public/color-rings.js @@ -0,0 +1,45 @@ +(() => { + const slider = document.querySelector('#intensity'); + const output = document.querySelector('#intensity-value'); + const bases = { clay: '#C98576', ochre: '#D6B779', patina: '#93AAA7' }; + + function toHsl(hex) { + const [r, g, b] = hex.match(/[a-f\d]{2}/gi).map(v => parseInt(v, 16) / 255); + const max = Math.max(r, g, b), min = Math.min(r, g, b); + const delta = max - min, l = (max + min) / 2; + if (!delta) return [0, 0, l]; + const s = delta / (1 - Math.abs(2 * l - 1)); + let h = max === r ? (g - b) / delta : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4; + return [((h * 60) + 360) % 360, s, l]; + } + + function toHex([h, s, l]) { + const a = s * Math.min(l, 1 - l); + const channel = n => { + const k = (n + h / 30) % 12; + const value = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); + return Math.round(value * 255).toString(16).padStart(2, '0'); + }; + return `#${channel(0)}${channel(8)}${channel(4)}`.toUpperCase(); + } + + function render() { + const intensity = Number(slider.value); + output.value = `${intensity}%`; + slider.setAttribute('aria-valuetext', `${intensity}% of original saturation`); + for (const [name, base] of Object.entries(bases)) { + const [h, s, l] = toHsl(base); + const hex = toHex([h, Math.min(1, s * intensity / 100), l]); + document.querySelector(`#ring-${name}`).setAttribute('stroke', hex); + document.querySelector(`#hex-${name}`).textContent = hex; + } + } + + slider.addEventListener('input', render); + document.querySelector('.controls').addEventListener('reset', event => { + event.preventDefault(); + slider.value = slider.defaultValue; + render(); + }); + render(); +})(); diff --git a/scripts/color-rings.test.mjs b/scripts/color-rings.test.mjs new file mode 100644 index 0000000..686dba6 --- /dev/null +++ b/scripts/color-rings.test.mjs @@ -0,0 +1,37 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import vm from 'node:vm'; + +test('color rings preserve the base palette, stay in sRGB, and reset', () => { + const elements = new Map(); + const element = selector => { + if (!elements.has(selector)) elements.set(selector, { + value: '125', defaultValue: '125', attributes: {}, listeners: {}, + setAttribute(key, value) { this.attributes[key] = value; }, + addEventListener(key, handler) { this.listeners[key] = handler; }, + }); + return elements.get(selector); + }; + vm.runInNewContext(readFileSync(new URL('../public/color-rings.js', import.meta.url), 'utf8'), { + document: { querySelector: element }, + }); + const slider = element('#intensity'); + const colors = () => ['clay', 'ochre', 'patina'].map(name => { + const hex = element(`#hex-${name}`).textContent; + assert.equal(element(`#ring-${name}`).attributes.stroke, hex); + return hex; + }); + const initial = colors(); + for (let value = 0; value <= 220; value++) { + slider.value = String(value); + slider.listeners.input(); + for (const hex of colors()) assert.match(hex, /^#[0-9A-F]{6}$/); + if (value === 0) for (const hex of colors()) assert.match(hex, /^#([0-9A-F]{2})\1\1$/); + if (value === 100) assert.deepEqual(colors(), ['#C98576', '#D6B779', '#93AAA7']); + } + element('.controls').listeners.reset({ preventDefault() {} }); + assert.equal(slider.value, '125'); + assert.deepEqual(colors(), initial); + assert.equal(element('#intensity-value').value, '125%'); +}); diff --git a/src/index.tsx b/src/index.tsx index 71f7681..75c28c8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -72,6 +72,13 @@ app.get("/apple-touch-icon.png", (c) => c.redirect("/public/apple-touch-icon.png app.get("/healthz", (c) => c.json({ status: "ok" })); +// Direct-link abstract art; intentionally absent from indexes and navigation. +app.use("/artifacts/color-rings", async (c, next) => { + c.header("X-Robots-Tag", "noindex, nofollow, noarchive"); + await next(); +}); +app.get("/artifacts/color-rings", serveStatic({ path: "./public/color-rings.html" })); + // Direct-link design experiment: deliberately outside the site shell and navigation. app.use("/mindco", async (c, next) => { c.header("X-Robots-Tag", "noindex, nofollow"); -- 2.51.2