shadowColor (transparent by default), shadowBlur, shadowOffsetX and shadowOffsetY cast a shadow under everything drawn next, and globalAlpha scales the opacity of later drawing. ctx.filter takes CSS filters such as blur() and grayscale(), but Safari 10 lacks it (not Baseline), so treat it as an enhancement.
<canvas id="c" style="width:600px; height:108px"></canvas><script src="hidpi.js"></script>
<script>
const ctx = hidpi(c), r = devicePixelRatio;
function cover(x, color, title) {
ctx.fillStyle = color; ctx.fillRect(x, 6, 80, 92); ctx.font = 'bold 13px Georgia';
ctx.fillStyle = '#fff3e0'; ctx.fillText(title, x + 8, 30);
}
ctx.save();
ctx.shadowColor = 'rgb(0 0 0 / 0.45)'; ctx.shadowBlur = 8 * r; // shadows ignore
ctx.shadowOffsetX = ctx.shadowOffsetY = 5 * r; // the transform: scale
cover(20, '#1f5f8b', 'Harbor'); ctx.restore();
ctx.globalAlpha = 0.5; cover(170, '#5b3f99', 'Deep Web'); ctx.globalAlpha = 1;
cover(320, '#e09a10', 'Saffron');
ctx.filter = 'grayscale(1) opacity(0.6)'; cover(470, '#e09a10', 'Saffron');
</script>
Shadow offsets and blur ignore the transform, so under hidpi() multiply them by devicePixelRatio. Wrap shadows in save() and restore(), or every later shape grows one, as the title here does.
<!doctype html>
<style>
body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
canvas { width: 100%; max-width: 600px; height: 150px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<label>shadowBlur <input type="range" id="blur" min="0" max="20" value="8"></label>
<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 ctx = hidpi(document.getElementById('c')), r = devicePixelRatio;
function cover(x, color, title, label) {
ctx.fillStyle = color; ctx.fillRect(x, 12, 90, 110);
ctx.font = 'bold 14px Georgia'; ctx.fillStyle = '#fff3e0'; ctx.fillText(title, x + 8, 36);
ctx.save(); ctx.shadowColor = 'transparent'; ctx.globalAlpha = 1; ctx.filter = 'none';
ctx.font = '11px sans-serif'; ctx.fillStyle = '#263238'; ctx.fillText(label, x, 140);
ctx.restore();
}
function draw(blur) {
ctx.clearRect(0, 0, 600, 150);
// Shadows ignore the transform, so multiply by devicePixelRatio under hidpi()
ctx.save();
ctx.shadowColor = 'rgb(0 0 0 / 0.45)'; ctx.shadowBlur = blur * r;
ctx.shadowOffsetX = ctx.shadowOffsetY = 5 * r;
cover(20, '#1f5f8b', 'Harbor', 'shadow');
ctx.restore(); // or every later shape gets one
ctx.globalAlpha = 0.5; cover(170, '#5b3f99', 'Deep Web', 'globalAlpha 0.5');
ctx.globalAlpha = 1;
cover(320, '#e09a10', 'Saffron', 'plain');
// ctx.filter takes CSS filters (not in Safari: treat as an enhancement)
ctx.filter = 'grayscale(1) opacity(0.6)'; cover(470, '#e09a10', 'Saffron', 'filter: grayscale');
ctx.filter = 'none';
}
const slider = document.getElementById('blur');
slider.addEventListener('input', () => draw(+slider.value));
draw(8);
</script>