Skip to content

Instantly share code, notes, and snippets.

@gilmoreorless
Last active September 3, 2016 10:15
Show Gist options
  • Save gilmoreorless/7f8124f51180197bcbce3790e51c29be to your computer and use it in GitHub Desktop.
Save gilmoreorless/7f8124f51180197bcbce3790e51c29be to your computer and use it in GitHub Desktop.
Flatten a circle
license: cc-by-4.0

An experiment in transitioning a circle to a flat line while keeping its fill gradient. The transition shapes are rectangles with rounded corners, with only the height and corner radius changing.

The “stretch” mode also transitions the width of the line, ultimately becoming the length of the original circle’s circumference.


This is part two 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;
}
#flattery {
width: 300px;
}
</style>
<body>
<div class="controls">
<label>Flatness: <input type="range" id="flattery" min="0" max="1" step="0.01" value="0" /></label>
<label><input type="checkbox" id="animate" /> Animate</label>
<label><input type="checkbox" id="stretch-width" /> Stretch width</label>
</div>
<canvas id="drawing"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// Config
var width = 600,
height = 500,
mainRadius = 80,
lineThickness = 40,
gradientSegments = 180;
// Derived values
var TAU = Math.PI * 2,
TAU_4 = TAU / 4,
circumference = mainRadius * TAU,
cx = width / 2 - 120,
cy = height / 2 + 50;
// State
var isAnimating = false,
shouldStretch = false;
// Re-usable interpolators
var _flatnessDomain = [0, 1];
var scaleHeight = d3.scaleLinear()
.domain(_flatnessDomain)
.range([mainRadius * 2, lineThickness]);
var scaleWidth = d3.scaleLinear()
.domain(_flatnessDomain)
.range([mainRadius * 2, circumference]);
var scaleXOffset = d3.scaleLinear()
.domain(_flatnessDomain)
.range([0, circumference * 0.25]);
var scaleRadius = d3.scaleLinear()
.domain(_flatnessDomain)
.range([mainRadius, 0]);
// 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 fixed-width shape with full hue gradient from left to right.
* The shape morphs from a straight line to a full circle.
*
* `flatness` is a float in range [0, 1] that defines how flat the shape is.
* 0 = no flatness (full circle)
* 1 = full flatness (straight horizontal line)
*/
function drawMainShape(flatness) {
flatness = clamp(flatness, 0, 1);
if (flatness > 0.999) flatness = 1;
// Derived values
var w = shouldStretch ? scaleWidth(flatness) : mainRadius * 2;
var h = scaleHeight(flatness);
var w_2 = w / 2;
var h_2 = h / 2;
var cornerRadius = scaleRadius(flatness);
var x2 = w_2 - cornerRadius;
var x1 = -x2;
var y2 = h_2 - cornerRadius;
var y1 = -y2;
var xOffset = shouldStretch ? scaleXOffset(flatness) : 0;
var grad, i, x, y, p1, p2;
ctx.save();
grad = ctx.createLinearGradient(-w_2, 0, w_2, 0);
for (i = 0; i < gradientSegments; i++) {
p1 = i / gradientSegments;
p2 = (i + 1) / gradientSegments;
grad.addColorStop(p1, hue(p1 * 360));
grad.addColorStop(p2, hue(p2 * 360));
}
ctx.translate(xOffset, mainRadius - h_2);
ctx.beginPath();
ctx.fillStyle = grad;
if (flatness < 1) {
for (i = 0; i < 4; i++) {
x = (i === 0 || i === 3) ? x2 : x1;
y = (i < 2) ? y2 : y1;
ctx.arc(x, y, cornerRadius, TAU_4 * i, TAU_4 * (i + 1));
}
ctx.fill();
} else {
ctx.fillRect(-w_2, -h_2, w, h);
}
ctx.restore();
// Show reference point
ctx.save();
ctx.strokeStyle = '#666';
ctx.fillStyle = '#666';
ctx.beginPath();
ctx.moveTo(-mainRadius, mainRadius + .5);
ctx.lineTo(mainRadius, mainRadius + .5);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, mainRadius + .5, 2, 0, TAU);
ctx.fill();
ctx.restore();
}
var slider = document.getElementById('flattery');
var ease = d3.easeCubicInOut;
var duration = 4000;
var timer;
function showIt(t) {
ctx.clearRect(-width / 2, -height / 2, width * 2, height);
drawMainShape(t);
ctx.fillText(t, 0, mainRadius + 20);
slider.value = t;
}
function tick(elapsed) {
var t = ease(1 - Math.abs((elapsed % duration) / duration - .5) * 2);
showIt(t);
}
// Handle controls
slider.addEventListener('input', function (e) {
showIt(this.value);
}, 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);
document.getElementById('stretch-width').addEventListener('click', function (e) {
shouldStretch = this.checked;
if (!isAnimating) {
showIt(slider.value);
}
});
// Setup
showIt(0);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment