Skip to content

Instantly share code, notes, and snippets.

@gilmoreorless
Last active September 3, 2016 10:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gilmoreorless/9ce39d5db8067a58eb2ed2a7ef5aad43 to your computer and use it in GitHub Desktop.
Save gilmoreorless/9ce39d5db8067a58eb2ed2a7ef5aad43 to your computer and use it in GitHub Desktop.
Arc fill clipping
license: cc-by-4.0

Playing around with using canvas path clipping to produce a specific shape with a fill pattern of many paths.

An arc clipping path is generated using D3’s arc generator and scales (for the arc parameters). Then a fill is made by drawing a bunch of concentric circles.

This turns out to be yet another area where browsers differ in their rendering. Chrome’s (and Safari’s) canvas clip path doesn’t use anti-aliasing at all, resulting in harsh, jagged edges. Firefox’s clip path seems to attempt to do some anti-aliasing, but ends up drawing a kind of border around the shape.

Comparison between Chrome and Firefox rendering


This is part three of a 4-part series of experiments:

1. Gradient line to circle | 2. Flatten a circle | 3. Arc fill clipping | 4. Twist a gradient-filled circle

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: sans-serif;
margin: auto;
position: relative;
width: 600px;
}
.controls {
left: 0;
position: absolute;
top: 2em;
}
label {
display: block;
margin-top: 0.25em;
}
label span {
display: inline-block;
width: 7em;
}
input[type=range] {
width: 300px;
}
</style>
<body>
<div class="controls" id="controls">
<label><span>Arc radius:</span> <input type="range" id="arcRadius" min="0" max="1" step="0.01" value="0" /></label>
<label><span>Arc thickness:</span> <input type="range" id="arcThickness" min="0" max="1" step="0.01" value="0" /></label>
<label><span>Corner radius:</span> <input type="range" id="cornerRadius" min="0" max="1" step="0.01" value="0.5" /></label>
<label><input type="checkbox" id="animate" /> Animate</label>
</div>
<canvas id="drawing"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// Config
var width = 600,
height = 500,
arcRadiusRange = [80, 300],
arcThicknessRange = [20, 60],
relativeArcLength = 0.5,
colourSegments = 10;
// Derived values
var TAU = Math.PI * 2,
TAU_4 = TAU / 4,
baseArcLength = TAU * relativeArcLength,
circumference = arcRadiusRange[0] * baseArcLength,
cx = width / 2 - 90,
cy = height / 2 + 50;
// State
var isAnimating = false,
curPos = {
arcRadius: 0,
arcThickness: 0,
cornerRadius: 0.5
}
// Re-usable interpolators
var _posDomain = [0, 1];
var scaleArc = d3.scalePow()
.clamp(true)
.exponent(2)
.domain(_posDomain)
.range(arcRadiusRange);
var scaleThickness = d3.scaleLinear()
.clamp(true)
.domain(_posDomain)
.range(arcThicknessRange);
var scaleCorner = d3.scaleLinear()
.clamp(true)
.domain(_posDomain); // range is filled dynamically
var scaleFill = d3.scaleLinear()
.clamp(true)
.domain(_posDomain)
.range([10, arcRadiusRange[0] * 1.5]);
// Canvas setup
var canvas = document.getElementById('drawing');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
ctx.translate(cx, cy);
function hue(h) {
return 'hsl(' + h + ', 70%, 50%)';
}
function clamp(num, min, max) {
return Math.min(Math.max(min, +num), max);
}
/**
* Draw a clipped annular arc segment based on radius, thickness and corner variables
*/
function drawArc() {
// Core values based on range sliders
var outerRadius = scaleArc(curPos.arcRadius);
var thickness = scaleThickness(curPos.arcThickness);
var innerRadius = outerRadius - thickness;
scaleCorner.range([0, thickness / 2]);
var cornerRadius = scaleCorner(curPos.cornerRadius);
// Derived values
var baseHeight = arcRadiusRange[0];
var yOffset = baseHeight - outerRadius;
var baseAngle = TAU / 2;
var derivedCircumference = outerRadius * relativeArcLength;
var circumferenceDiff = derivedCircumference / circumference;
var circumferenceAngleOffset = relativeArcLength / circumferenceDiff / 2;
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.cornerRadius(cornerRadius)
.context(ctx);
ctx.save();
// Draw arc-shaped clipping path
ctx.translate(0, yOffset);
ctx.beginPath();
arc({
startAngle: baseAngle - circumferenceAngleOffset,
endAngle: baseAngle + circumferenceAngleOffset
});
ctx.clip();
ctx.translate(0, -yOffset);
// Draw different coloured shapes
var i, r;
for (i = colourSegments - 1; i >= 0; i--) {
r = scaleFill(i / (colourSegments - 1));
ctx.fillStyle = hue(i / colourSegments * 360);
ctx.beginPath();
ctx.arc(0, baseHeight, r, 0, TAU);
ctx.fill();
}
ctx.restore();
// Show reference point
ctx.save();
ctx.strokeStyle = '#666';
ctx.fillStyle = '#666';
ctx.beginPath();
ctx.moveTo(-baseHeight, baseHeight + .5);
ctx.lineTo(baseHeight, baseHeight + .5);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, baseHeight + .5, 2, 0, TAU);
ctx.fill();
ctx.restore();
}
var slider = document.getElementById('flattery');
var ease = d3.easeLinear;
var duration = {
arcRadius: 4567,
arcThickness: 3210,
cornerRadius: 1234,
};
var dom = {};
Object.keys(curPos).forEach(function (key) {
dom[key] = document.getElementById(key);
});
var timer;
function showIt() {
ctx.clearRect(-width / 2, -height / 2, width * 2, height);
drawArc();
Object.keys(curPos).forEach(function (key) {
dom[key].value = curPos[key];
});
}
function tick(elapsed) {
Object.keys(curPos).forEach(function (key) {
var d = duration[key];
curPos[key] = ease(1 - Math.abs((elapsed % d) / d - .5) * 2);
});
showIt();
}
// Handle controls
document.getElementById('controls').addEventListener('input', function (e) {
var input = e.target;
if (input.type === 'range' && input.id) {
curPos[input.id] = +input.value || 0;
showIt();
}
}, false);
document.getElementById('animate').addEventListener('click', function (e) {
isAnimating = this.checked;
if (isAnimating) {
if (!timer) {
timer = d3.timer(tick);
} else {
timer.restart(tick);
}
} else {
timer && timer.stop();
}
}, false);
showIt();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment