@@ -30,9 +30,9 @@
TightWide
-
-
- FlushInset
+
+
+ SmallLarge
@@ -63,7 +63,7 @@
Ochre · middle#E2BB6D
Slate blue · inner#8AA8BA
- 100% intensity is the original palette. Hue and HSL lightness stay fixed; saturation stops at 100%. Gap compresses or expands the space between rings; margin sets the white space between the outer ring and the canvas edge, independent of thickness; thickness changes stroke width; rotation spins the whole mark; order reassigns which color sits on each ring. Readouts are the exact sRGB hex colors shown. Export downloads a standalone SVG with a white background circle.
+ 100% intensity is the original palette. Hue and HSL lightness stay fixed; saturation stops at 100%. Gap compresses or expands the space between rings; scale zooms the whole mark about its center so the band spacing stays even at any size; thickness changes stroke width; rotation spins the whole mark; order reassigns which color sits on each ring. Readouts are the exact sRGB hex colors shown. Export downloads a standalone SVG with a white background circle.
diff --git a/public/color-rings.js b/public/color-rings.js
index 487e480..667d731 100644
--- a/public/color-rings.js
+++ b/public/color-rings.js
@@ -3,8 +3,8 @@
const intensityOut = document.querySelector('#intensity-value');
const gap = document.querySelector('#gap');
const gapOut = document.querySelector('#gap-value');
- const margin = document.querySelector('#margin');
- const marginOut = document.querySelector('#margin-value');
+ const scale = document.querySelector('#scale');
+ const scaleOut = document.querySelector('#scale-value');
const weight = document.querySelector('#weight');
const weightOut = document.querySelector('#weight-value');
const rotation = document.querySelector('#rotation');
@@ -16,11 +16,11 @@
const ringsSvg = document.querySelector('.rings');
const bases = { clay: '#C98576', ochre: '#D6B779', slate: '#8FA7B5' };
- // Deployed mark: gap=10, margin=38, weight=24 gives radii 250/180/110 with
+ // Deployed mark: gap=10, scale=100, weight=24 gives radii 250/180/110 with
// open space between each color and the ring edges, like the ring sigil.
// The DOM keeps clay/ochre/slate element ids stable; the order select
// reassigns radii by position, outer to inner.
- const CANVAS_R = 300;
+ const OUTER_R = 250;
const MIDDLE_R = 180;
const INNER_R = 110;
const BASE_GAP = 10;
@@ -49,31 +49,37 @@
return order.value.split(',');
}
+ // Radial geometry, shared by the live render and the SVG export. The extra
+ // gap compresses the middle and inner rings inward so the mark never clips
+ // the viewBox. Scale zooms every ring about the shared center, so the band
+ // spacing stays even at any size; thickness is independent and only changes
+ // the stroke width. Radii are rounded to hundredths so slider steps never
+ // leak float noise (e.g. 180 * 0.8) into the SVG.
+ function ringRadii(gapValue, scaleValue) {
+ const extra = gapValue - BASE_GAP;
+ const k = scaleValue / 100;
+ const outer = OUTER_R * k;
+ const middle = Math.max(INNER_R / 2, MIDDLE_R - extra) * k;
+ const inner = Math.max(INNER_R / 4, INNER_R - 2 * extra) * k;
+ return [outer, middle, inner].map(v => Math.round(v * 100) / 100);
+ }
+
function render() {
const pct = Number(intensity.value);
const gapValue = Number(gap.value);
- const marginValue = Number(margin.value);
+ const scaleValue = Number(scale.value);
const weightValue = Number(weight.value);
const rotationValue = Number(rotation.value);
const names = orderedNames();
intensityOut.value = `${pct}%`;
gapOut.value = String(gapValue);
- marginOut.value = String(marginValue);
+ scaleOut.value = `${scaleValue}%`;
weightOut.value = String(weightValue);
rotationOut.value = `${rotationValue}°`;
intensity.setAttribute('aria-valuetext', `${pct}% of original saturation`);
- // Radial geometry: the extra gap compresses the middle and inner rings
- // inward so the mark never clips the viewBox. The margin is the visible
- // white space between the outer ring's edge and the canvas edge; it is
- // independent of thickness, so weight changes move the radius instead.
- // The floor keeps the outer ring from crossing the middle ring.
- const extra = gapValue - BASE_GAP;
- const middle = Math.max(INNER_R / 2, MIDDLE_R - extra);
- const inner = Math.max(INNER_R / 4, INNER_R - 2 * extra);
- const outer = Math.max(CANVAS_R - marginValue - weightValue / 2, middle + weightValue);
- const radiusByPosition = [outer, middle, inner];
+ const radiusByPosition = ringRadii(gapValue, scaleValue);
ringsGroup.setAttribute('stroke-width', String(weightValue));
ringsGroup.setAttribute('transform', `rotate(${rotationValue} 300 300)`);
@@ -103,14 +109,10 @@
const names = orderedNames();
const pct = Number(intensity.value);
const gapValue = Number(gap.value);
- const marginValue = Number(margin.value);
+ const scaleValue = Number(scale.value);
const weightValue = Number(weight.value);
const rotationValue = Number(rotation.value);
- const extra = gapValue - BASE_GAP;
- const middle = Math.max(INNER_R / 2, MIDDLE_R - extra);
- const inner = Math.max(INNER_R / 4, INNER_R - 2 * extra);
- const outer = Math.max(CANVAS_R - marginValue - weightValue / 2, middle + weightValue);
- const radiusByPosition = [outer, middle, inner];
+ const radiusByPosition = ringRadii(gapValue, scaleValue);
const circles = names.map(name => {
const [h, s, l] = toHsl(bases[name]);
@@ -139,7 +141,7 @@ ${circles}
URL.revokeObjectURL(url);
}
- for (const input of [intensity, gap, margin, weight, rotation, order]) {
+ for (const input of [intensity, gap, scale, weight, rotation, order]) {
input.addEventListener('input', render);
}
theme.addEventListener('click', () => {
@@ -149,7 +151,7 @@ ${circles}
exportButton.addEventListener('click', exportSvg);
document.querySelector('.controls').addEventListener('reset', event => {
event.preventDefault();
- for (const input of [intensity, gap, margin, weight, rotation, order]) {
+ for (const input of [intensity, gap, scale, weight, rotation, order]) {
input.value = input.defaultValue;
}
theme.value = theme.defaultValue;
diff --git a/scripts/color-rings.test.mjs b/scripts/color-rings.test.mjs
index 7d71a3a..4d9d270 100644
--- a/scripts/color-rings.test.mjs
+++ b/scripts/color-rings.test.mjs
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
-const INPUTS = ['#intensity', '#gap', '#margin', '#weight', '#rotation'];
+const INPUTS = ['#intensity', '#gap', '#scale', '#weight', '#rotation'];
function makeHarness() {
const elements = new Map();
@@ -17,7 +17,7 @@ function makeHarness() {
return elements.get(selector);
};
const defaults = {
- '#intensity': '125', '#gap': '10', '#margin': '38', '#weight': '24', '#rotation': '0',
+ '#intensity': '125', '#gap': '10', '#scale': '100', '#weight': '24', '#rotation': '0',
};
for (const [sel, val] of Object.entries(defaults)) {
element(sel).value = val;
@@ -85,43 +85,49 @@ test('gap slider moves inner rings while the outer ring stays fixed', () => {
assert.equal(element('#ring-slate').attributes.r, '110');
});
-test('margin slider insets the outer ring independently of thickness', () => {
+test('scale slider zooms the whole mark, keeping band spacing even', () => {
const { element } = makeHarness();
- const margin = element('#margin');
- // Default margin 38, weight 24: outer radius 300 - 38 - 12 = 250.
+ const scale = element('#scale');
+ // Default scale 100: deployed radii 250/180/110.
assert.equal(element('#ring-clay').attributes.r, '250');
-
- // More margin insets the outer ring; middle and inner rings stay put.
- margin.value = '80';
- margin.listeners.input();
- assert.equal(element('#ring-clay').attributes.r, '208');
assert.equal(element('#ring-ochre').attributes.r, '180');
assert.equal(element('#ring-slate').attributes.r, '110');
- assert.equal(element('#margin-value').value, '80');
- // Margin is visible white space, independent of thickness. Back at the
- // default margin 38, weight 40 moves the radius to 300 - 38 - 20 = 242 so
- // the outer edge stays at 262, still 38 from the canvas edge.
- margin.value = '38';
- margin.listeners.input();
+ // Scale 80 zooms every ring about the shared center. The radius step
+ // scales with it (70 -> 56), so the band spacing stays even everywhere
+ // instead of the outer band collapsing onto the middle one.
+ scale.value = '80';
+ scale.listeners.input();
+ assert.equal(element('#ring-clay').attributes.r, '200');
+ assert.equal(element('#ring-ochre').attributes.r, '144');
+ assert.equal(element('#ring-slate').attributes.r, '88');
+ assert.equal(element('#scale-value').value, '80%');
+
+ // Scale is independent of thickness: changing the weight does not move
+ // any radius, so both band gaps stay equal (56 - 40 = 16 each here).
const weight = element('#weight');
weight.value = '40';
weight.listeners.input();
- assert.equal(element('#ring-clay').attributes.r, '242');
+ assert.equal(element('#ring-clay').attributes.r, '200');
+ assert.equal(element('#ring-ochre').attributes.r, '144');
+ assert.equal(element('#ring-slate').attributes.r, '88');
- // Flush margin puts the outer edge at the canvas edge.
- margin.value = '0';
- margin.listeners.input();
- assert.equal(element('#ring-clay').attributes.r, '280');
+ // Scale past 100 grows the mark toward the canvas edge.
+ scale.value = '110';
+ scale.listeners.input();
+ assert.equal(element('#ring-clay').attributes.r, '275');
+ assert.equal(element('#ring-ochre').attributes.r, '198');
+ assert.equal(element('#ring-slate').attributes.r, '121');
- // At extreme margin + thickness the floor keeps the outer band from
- // crossing the middle band instead of shrinking under it.
- margin.value = '80';
- margin.listeners.input();
- assert.equal(element('#ring-clay').attributes.r, '220');
+ // Gap adjustments compose with scale: the gap-compressed radii then zoom.
+ const gap = element('#gap');
+ gap.value = '30';
+ gap.listeners.input();
+ assert.equal(element('#ring-ochre').attributes.r, '176');
+ assert.equal(element('#ring-slate').attributes.r, '77');
element('.controls').listeners.reset({ preventDefault() {} });
- assert.equal(margin.value, '38');
+ assert.equal(scale.value, '100');
assert.equal(element('#ring-clay').attributes.r, '250');
});