clip() intersects the clipping region with the current path, or with a Path2D and an optional 'evenodd' fill rule, and confines all later drawing to it. Clips only shrink; to widen one, restore() a state saved before it.
<canvas id="c" style="width:600px; height:56px"></canvas><script src="hidpi.js"></script>
<script>
const ctx = hidpi(c);
const star = new Path2D('M12 2.6 14.5 9.2 21.5 9.5 16 13.9 17.9 20.7 12 16.8 6.1 20.7 ' +
'8 13.9 2.5 9.5 9.5 9.2Z'); // the star of Section 1.3.2
const stars = color => [0, 1, 2, 3, 4].forEach(i => {
ctx.save(); ctx.translate(10 + i * 52, 4); ctx.scale(2, 2);
ctx.fillStyle = color; ctx.fill(star); ctx.restore(); });
stars('#e0d8c8'); // pale base row
ctx.save(); ctx.beginPath(); ctx.rect(0, 0, 10 + 4 * 52 + 0.6 * 48, 56); ctx.clip();
stars('#e09a10'); ctx.restore(); // saffron, clipped
ctx.font = '20px Georgia'; ctx.fillText('4.6 The Quiet Harbor', 290, 36);
</script>
The clip spans four stars on a 52-unit pitch plus 60 percent of the fifth star's 48-unit box, the same arithmetic as the SVG clip path of Clipping and Masking. A clip is a hard edge; for a soft fade, use compositing.
<!doctype html>
<style>
body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
canvas { width: 100%; max-width: 600px; height: 190px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<p>Move the pointer across the first row to set a rating.</p>
<script>
function hidpi(c, r = devicePixelRatio) {
c.width = c.clientWidth * r; c.height = c.clientHeight * r;
const ctx = c.getContext('2d'); ctx.scale(r, r); return ctx;
}
const c = document.getElementById('c');
const ctx = hidpi(c);
const star = new Path2D('M12 2.6 14.5 9.2 21.5 9.5 16 13.9 17.9 20.7 12 16.8 6.1 20.7 ' +
'8 13.9 2.5 9.5 9.5 9.2Z'); // a 24-unit star
const books = [['The Quiet Harbor', 4.6], ['Salt and Saffron', 4.8], ['Small Steps to Big Summits', 4.1]];
function stars(y, color) {
for (let i = 0; i < 5; i++) {
ctx.save(); ctx.translate(10 + i * 52, y); ctx.scale(2, 2);
ctx.fillStyle = color; ctx.fill(star); ctx.restore();
}
}
function rating(y, value, title) {
stars(y, '#e0d8c8'); // pale base row
ctx.save();
ctx.beginPath(); // clip to the rated width
const whole = Math.floor(value);
ctx.rect(0, y, 10 + whole * 52 + (value - whole) * 48, 50);
ctx.clip(); // later drawing stays inside
stars(y, '#e09a10');
ctx.restore(); // widen the clip again
ctx.font = '18px Georgia'; ctx.fillStyle = '#263238';
ctx.fillText(`${value.toFixed(1)} ${title}`, 290, y + 32);
}
function draw(first) {
ctx.clearRect(0, 0, 600, 190);
books.forEach(([title, value], i) => rating(6 + i * 60, i === 0 ? first : value, title));
}
c.addEventListener('pointermove', e => {
const b = c.getBoundingClientRect(), x = e.clientX - b.left, y = e.clientY - b.top;
if (y < 60 && x < 270) {
const i = Math.floor((x - 10) / 52), frac = Math.min(1, Math.max(0, (x - 10 - i * 52) / 48));
draw(Math.max(0, Math.min(5, i + frac)));
}
});
draw(4.6);
</script>