Skip to content

Instantly share code, notes, and snippets.

@tommct tommct/README.md
Last active Jun 11, 2018

Embed
What would you like to do?
D3 Constrained Zoom Canvas Image

Implements constrained zooming of an image put onto an HTML5 Canvas.

<!DOCTYPE html>
<meta charset="utf-8">
<title>Canvas image zoom</title>
<style>
body {
position: relative;
}
svg,
canvas {
position: absolute;
}
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<body>
<script>
var imageObj = new Image();
imageObj.src = "http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg";
var margin = {top: 20, right: 10, bottom: 20, left: 60},
width = imageObj.width,
height = imageObj.height;
var ctx;
var x = d3.scale.linear()
.domain([0, imageObj.width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, imageObj.height])
.range([height, 0]);
var xmin = x.domain()[0];
var xmax = x.domain()[1];
var ymin = y.domain()[0];
var ymax = y.domain()[1];
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", refresh);
// Canvas is drawn first, and then SVG over the top.
var canvas = d3.select("body").append("canvas")
.attr("width", imageObj.width)
.attr("height", imageObj.height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("left", margin.left + "px")
.style("top", margin.top + "px")
.style("width", width + "px")
.style("height", height + "px")
.style("position", "absolute")
.call(drawImage);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// We make an invisible rectangle to intercept mouse events for zooming.
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "000")
.style("opacity", 1e-6)
.call(zoom);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.call(removeZero);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.call(removeZero);
function drawImage(canvas) {
ctx = canvas.node().getContext("2d");
ctx.drawImage(imageObj, 0, 0);
}
// Keep an eye out for "translateExtent" or "xExtent" methods that may be
// added at some point to bound the limits of zooming and panning. Until then,
// this works.
function refresh() {
var t = zoom.translate();
var s = zoom.scale();
var tx = t[0],
ty = t[1];
var xdom = x.domain();
var reset_s = 0;
if ((xdom[1] - xdom[0]) >= (xmax - xmin)) {
zoom.x(x.domain([xmin, xmax]));
xdom = x.domain();
reset_s = 1;
}
var ydom = y.domain();
if ((ydom[1] - ydom[0]) >= (ymax - ymin)) {
zoom.y(y.domain([ymin, ymax]));
ydom = y.domain();
reset_s += 1;
}
if (reset_s == 2) { // Both axes are full resolution. Reset.
zoom.scale(1);
tx = 0;
ty = 0;
}
else {
if (xdom[0] < xmin) {
tx = 0;
x.domain([xmin, xdom[1] - xdom[0] + xmin]);
xdom = x.domain();
}
if (xdom[1] > xmax) {
xdom[0] -= xdom[1] - xmax;
tx = -xdom[0]*width/(xmax-xmin)*s;
x.domain([xdom[0], xmax]);
}
if (ydom[0] < ymin) {
y.domain([ymin, ydom[1] - ydom[0] + ymin]);
ydom = y.domain();
ty = -(ymax-ydom[1])*height/(ymax-ymin)*s;
}
if (ydom[1] > ymax) {
ydom[0] -= ydom[1] - ymax;
ty = 0;
y.domain([ydom[0], ymax]);
}
}
// Reset (possibly) if hit an edge so that next focus event starts correctly.
zoom.translate([tx, ty]);
ctx.drawImage(imageObj,
tx*imageObj.width/width, ty*imageObj.height/height,
imageObj.width*s, imageObj.height*s);
svg.select(".x.axis").call(xAxis).call(removeZero);
svg.select(".y.axis").call(yAxis).call(removeZero);
}
function removeZero(axis) {
axis.selectAll("g").filter(function(d) { return !d; }).remove();
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.