Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions javascript/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,15 @@ <h1>Doyle Spiral Studio</h1>
<details class="advanced-settings">
<summary>Advanced settings</summary>
<div class="advanced-settings-content">
<div class="field-group">
<div class="checkbox-row">
<label>
<input type="checkbox" id="shadeByAngle" name="shade_by_angle" />
Colour 2D regions by line angle (grayscale)
</label>
</div>
<p class="hint">Fills each arc group using its normalised line angle (modulo 180°) mapped from black to white.</p>
</div>
<div class="field-group">
<label for="renderTimeoutSeconds">Render timeout (seconds)</label>
<input
Expand Down
1 change: 1 addition & 0 deletions javascript/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const DEFAULTS = {
fill_pattern_type: 'lines',
fill_pattern_rect_width: 2,
fill_pattern_animation: 'radial_bloom',
shade_by_angle: false,
highlight_rim_width: 1.2,
group_outline_width: 0.6,
pattern_stroke_width: 0.5,
Expand Down
72 changes: 67 additions & 5 deletions javascript/js/doyle_spiral_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,8 @@ class ArcGroup {
lineSettings = [3, 0],
drawOutline = true,
lineOffset = 0,
solidFill = null,
solidFillOpacity = 1.0,
patternType = 'lines',
rectWidth = 2,
patternSpacingOverride = null,
Expand All @@ -1498,6 +1500,17 @@ class ArcGroup {
});
return;
}
if (solidFill) {
const stroke = drawOutline ? '#000000' : 'none';
context.drawGroupOutline(outline, {
fill: solidFill,
stroke,
strokeWidth: outlineStrokeWidth,
fillOpacity: solidFillOpacity,
drawOutline,
});
return;
}
if (patternFill) {
const stroke = this.debugStroke || '#000000';
const [lineSpacingRaw, lineAngleDeg] = lineSettings;
Expand Down Expand Up @@ -2631,7 +2644,16 @@ class DoyleSpiralEngine {
};
}

_createArcGroupsForCircles(radiusToRing, spiralCenter, debugGroups, addFillPattern, drawGroupOutline, context, outlineStrokeWidth = 0) {
_createArcGroupsForCircles(
radiusToRing,
spiralCenter,
debugGroups,
addFillPattern,
drawGroupOutline,
context,
outlineStrokeWidth = 0,
shadeByAngle = false,
) {
for (const circle of this.circles) {
if (circle.intersections.length !== 6) {
continue;
Expand All @@ -2655,7 +2677,7 @@ class DoyleSpiralEngine {
const end = circle.intersections[j][0];
const steps = estimateArcSteps(circle, start, end);
const arc = new ArcElement(circle, start, end, steps, true);
if (!addFillPattern && drawGroupOutline) {
if (!addFillPattern && drawGroupOutline && !shadeByAngle) {
context.drawScaledArc(arc, { color: '#000000', width: outlineStrokeWidth });
}
group.addArc(arc);
Expand Down Expand Up @@ -2864,6 +2886,7 @@ class DoyleSpiralEngine {
fillPatternOffset = 0.0,
fillPatternType = 'lines',
fillPatternRectWidth = 2.0,
shadeByAngle = false,
highlightRimWidth = 1.2,
groupOutlineWidth = 0.6,
patternStrokeWidth = 0.5,
Expand All @@ -2885,6 +2908,7 @@ class DoyleSpiralEngine {
const patternStroke = Number.isFinite(patternStrokeWidth)
? Math.max(0, patternStrokeWidth)
: 0;
const shouldPatternFill = addFillPattern && !shadeByAngle;

const spiralCenter = Complex.ZERO;
const radiusToRing = this._computeRingIndices();
Expand All @@ -2893,16 +2917,17 @@ class DoyleSpiralEngine {
radiusToRing,
spiralCenter,
debugGroups,
addFillPattern,
shouldPatternFill,
drawGroupOutline,
context,
outlineStrokeWidth,
shadeByAngle,
);
this._drawOuterClosureArcs(
spiralCenter,
debugGroups,
redOutline,
addFillPattern,
shouldPatternFill,
drawGroupOutline,
context,
outlineStrokeWidth,
Expand Down Expand Up @@ -2956,7 +2981,7 @@ class DoyleSpiralEngine {
}
}

if (addFillPattern) {
if (shouldPatternFill) {
for (const [key, group] of this.arcGroups.entries()) {
if (key.startsWith('outer_')) {
continue;
Expand All @@ -2981,6 +3006,41 @@ class DoyleSpiralEngine {
}
}

if (shadeByAngle) {
for (const [key, group] of this.arcGroups.entries()) {
if (key.startsWith('outer_')) {
continue;
}
const ringIdx = group.ringIndex ?? 0;
const patternAngle = Number.isFinite(group.primaryPatternAngle)
? group.primaryPatternAngle
: ringIdx * fillPatternAngle;
const referenceArc = group.arcs.find(arc => arc?.start && arc?.end);
let baseAngle = Number.isFinite(patternAngle) ? patternAngle : null;
if (!Number.isFinite(baseAngle) && referenceArc) {
const dx = referenceArc.end.re - referenceArc.start.re;
const dy = referenceArc.end.im - referenceArc.start.im;
if (Number.isFinite(dx) && Number.isFinite(dy) && Math.hypot(dx, dy) > 1e-9) {
baseAngle = (Math.atan2(dy, dx) * 180) / Math.PI;
}
}
if (!Number.isFinite(baseAngle)) {
baseAngle = 0;
}
const normalizedAngle = ((baseAngle % 180) + 180) % 180;
const intensity = clamp(normalizedAngle / 179.999, 0, 1);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using 180 instead of 179.999 for normalization would be clearer and avoid a magic number. Since normalizedAngle is always in the range [0, 180), dividing by 180 will produce a value in [0, 1). When multiplied by 255 and rounded, this will correctly map to the [0, 255] integer range for the color channel, effectively covering the full black-to-white spectrum as intended.

Suggested change
const intensity = clamp(normalizedAngle / 179.999, 0, 1);
const intensity = clamp(normalizedAngle / 180, 0, 1);

const channel = Math.round(intensity * 255);
const hex = channel.toString(16).padStart(2, '0');
const fillColor = `#${hex}${hex}${hex}`;
group.toSVGFill(context, {
solidFill: fillColor,
solidFillOpacity: 1,
drawOutline: drawGroupOutline,
outlineStrokeWidth: outlineStrokeWidth,
});
}
}

if (redOutline && maxIndex !== null) {
for (const [key, group] of this.arcGroups.entries()) {
if (!key.startsWith('circle_')) {
Expand Down Expand Up @@ -3160,6 +3220,7 @@ function normaliseParams(params = {}) {
fill_pattern_type: fillPatternType,
fill_pattern_rect_width: rectWidthMm,
fill_pattern_animation: normalisePatternAnimationId(params.fill_pattern_animation),
shade_by_angle: Boolean(params.shade_by_angle ?? false),
highlight_rim_width: highlightRimWidth,
group_outline_width: groupOutlineWidth,
pattern_stroke_width: patternStrokeWidth,
Expand Down Expand Up @@ -3191,6 +3252,7 @@ function renderSpiral(params = {}, overrideMode = null) {
fillPatternType: opts.fill_pattern_type,
fillPatternRectWidth: opts.fill_pattern_rect_width,
fillPatternAnimation: opts.fill_pattern_animation,
shadeByAngle: opts.shade_by_angle,
highlightRimWidth: opts.highlight_rim_width,
groupOutlineWidth: opts.group_outline_width,
patternStrokeWidth: opts.pattern_stroke_width,
Expand Down