Implements constrained zooming of an image put onto an HTML5 Canvas.
Last active
June 11, 2018 07:24
-
-
Save tommct/8047063 to your computer and use it in GitHub Desktop.
D3 Constrained Zoom Canvas Image
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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