|
<!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> |