Skip to content

Instantly share code, notes, and snippets.

@goulu
Last active June 11, 2018 08:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save goulu/91cc96fccb98552a8a24cb034306c866 to your computer and use it in GitHub Desktop.
Save goulu/91cc96fccb98552a8a24cb034306c866 to your computer and use it in GitHub Desktop.
D3 zoomable canvas class
const margin = {top: 20, right: 10, bottom: 20, left: 60};
class Figure {
constructor(div,width,height) {
this.width = width;
this.height = height;
// Canvas is drawn first, and then SVG over the top.
this.canvas = div.append("canvas")
.attr("width",width)
.attr("height", 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")
.style("z-index",0)
.on('click', () => {
// Get coordinates of click relative to the canvas element
let pos = d3.mouse(this.canvas.node());
console.log('canvas click ' + pos);
});
this.svg = div.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 + ")")
.style("position", "absolute")
.style("z-index",1)
.on('click', () => {
// Get coordinates of click relative to the canvas element
let pos = d3.mouse(this.svg.node());
console.log('svg click ' + pos);
});
// We make an invisible rectangle to intercept mouse events
this.rect=this.svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "000")
.style("opacity", 1e-6)
.style("position", "absolute")
.style("z-index",2)
.on("click", () => {
let pos = d3.mouse(this.rect.node());
console.log('rect click '+pos);
})
console.log('Figure created')
}
loaded(image) {
this.image=image;
let width = this.width;
let height = this.height;
this.x = d3.scale.linear()
.domain([0, image.width])
.range([0, width]);
this.y = d3.scale.linear()
.domain([0, image.height])
.range([height, 0]);
let x = this.x;
let y = this.y;
this.xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
this.yAxis = d3.svg.axis()
.scale(y)
.orient("left");
this.zoom = d3.behavior.zoom()
.x(x)
.y(y)
// .scaleExtent([1, 10])
.on("zoom", this.refresh.bind(this));
this.canvas.attr("width", image.width)
.attr("height", image.height);
// this.draw();
this.rect.call(this.zoom);
console.log('rect zoom assigned')
this.svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(this.xAxis)
.call(removeZero);
this.svg.append("g")
.attr("class", "y axis")
.call(this.yAxis)
.call(removeZero);
this.draw()
}
draw() {
this.context = this.canvas.node().getContext("2d");
this.context.globalAlpha = 1;
this.context.drawImage(this.image, 0, 0);
console.log('image drawn')
}
// 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.
refresh() {
let zoom=this.zoom;
let width = this.width;
let height = this.height;
let x = this.x;
let y = this.y;
let xmin = x.domain()[0];
let xmax = x.domain()[1];
let ymin = y.domain()[0];
let ymax = y.domain()[1];
let t = zoom.translate();
let s = zoom.scale();
let tx = t[0],
ty = t[1];
let xdom = x.domain();
let reset_s = 0;
if ((xdom[1] - xdom[0]) >= (xmax - xmin)) {
zoom.x(x.domain([xmin, xmax]));
xdom = x.domain();
reset_s = 1;
}
let 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]);
let image = this.image;
this.context.drawImage(image,
tx * image.width / width, ty * image.height / height,
image.width * s, image.height * s);
this.svg.select(".x.axis").call(this.xAxis).call(removeZero);
this.svg.select(".y.axis").call(this.yAxis).call(removeZero);
}
}
function removeZero(axis)
{
axis.selectAll("g").filter(function (d) {
return !d;
}).remove();
}
<!DOCTYPE html>
<meta charset="utf-8">
<title>Canvas image zoom</title>
<link rel="stylesheet" href="style.css">
<script src="https://d3js.org/d3.v3.min.js"></script>
<body>
<div id="figure" style="float:left; position:relative;"></div>
</body>
<script src="image.js" type="text/javascript"></script>
<script>
let masterImg = new Image();
let figure = new Figure(d3.select("#figure"), 640, 400);
masterImg.src = "http://www.dieselstation.com/wallpapers/albums/Lincoln/Mark-X-Concept-2004/Lincoln-Mark-X-Concept-2004-widescreen-03.jpg";
masterImg.onload = function() {
figure.loaded(masterImg)
};
</script>
body {
position: relative;
}
svg,
canvas {
position: absolute;
}
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment