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