JSON canvas initial
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
// view.js — pan and zoom.
|
||||
//
|
||||
// The whole scene lives in one transformed <div>, so panning and zooming cost a
|
||||
// single CSS transform: no relayout, no re-render of node content, nothing that
|
||||
// touches the DOM per node. That is what keeps a few hundred nodes usable on
|
||||
// the mobile/VPN path.
|
||||
//
|
||||
// The dot grid is painted on the (untransformed) surface and its size/offset
|
||||
// follow the transform, which keeps it crisp at every zoom level instead of
|
||||
// scaling a bitmap.
|
||||
window.Canvas = window.Canvas || {};
|
||||
|
||||
Canvas.View = (function () {
|
||||
var MIN_SCALE = 0.05;
|
||||
var MAX_SCALE = 4;
|
||||
var GRID = 20;
|
||||
|
||||
var surface = null;
|
||||
var world = null;
|
||||
var tx = 0, ty = 0, scale = 1;
|
||||
var listeners = [];
|
||||
|
||||
function apply() {
|
||||
world.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + scale + ')';
|
||||
var step = GRID * scale;
|
||||
surface.style.backgroundSize = step + 'px ' + step + 'px';
|
||||
surface.style.backgroundPosition = tx + 'px ' + ty + 'px';
|
||||
// Below a few pixels apart the dots stop reading as a grid and start
|
||||
// reading as noise, so zooming far out drops them entirely.
|
||||
surface.classList.toggle('is-grid-hidden', step < 6);
|
||||
for (var i = 0; i < listeners.length; i++) listeners[i]();
|
||||
}
|
||||
|
||||
function clampScale(s) {
|
||||
return Math.max(MIN_SCALE, Math.min(MAX_SCALE, s));
|
||||
}
|
||||
|
||||
// zoomAt scales about a screen point, so the world coordinate under the
|
||||
// cursor stays put — the behaviour that makes wheel-zoom feel anchored
|
||||
// rather than lurching toward the origin.
|
||||
function zoomAt(clientX, clientY, factor) {
|
||||
var next = clampScale(scale * factor);
|
||||
if (next === scale) return;
|
||||
var r = surface.getBoundingClientRect();
|
||||
var px = clientX - r.left;
|
||||
var py = clientY - r.top;
|
||||
var ratio = next / scale;
|
||||
tx = px - (px - tx) * ratio;
|
||||
ty = py - (py - ty) * ratio;
|
||||
scale = next;
|
||||
apply();
|
||||
}
|
||||
|
||||
function bounds(nodes) {
|
||||
if (!nodes.length) return null;
|
||||
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
nodes.forEach(function (n) {
|
||||
minX = Math.min(minX, n.x);
|
||||
minY = Math.min(minY, n.y);
|
||||
maxX = Math.max(maxX, n.x + n.width);
|
||||
maxY = Math.max(maxY, n.y + n.height);
|
||||
});
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
|
||||
return {
|
||||
GRID: GRID,
|
||||
|
||||
init: function (surfaceEl, worldEl) {
|
||||
surface = surfaceEl;
|
||||
world = worldEl;
|
||||
apply();
|
||||
},
|
||||
|
||||
get: function () { return { x: tx, y: ty, scale: scale }; },
|
||||
scale: function () { return scale; },
|
||||
|
||||
toWorld: function (clientX, clientY) {
|
||||
var r = surface.getBoundingClientRect();
|
||||
return {
|
||||
x: (clientX - r.left - tx) / scale,
|
||||
y: (clientY - r.top - ty) / scale
|
||||
};
|
||||
},
|
||||
|
||||
toScreen: function (x, y) {
|
||||
var r = surface.getBoundingClientRect();
|
||||
return { x: x * scale + tx + r.left, y: y * scale + ty + r.top };
|
||||
},
|
||||
|
||||
pan: function (dx, dy) {
|
||||
tx += dx;
|
||||
ty += dy;
|
||||
apply();
|
||||
},
|
||||
|
||||
zoomAt: zoomAt,
|
||||
|
||||
// zoomBy scales about the viewport centre, for the toolbar buttons and
|
||||
// the +/- shortcuts where there is no cursor to anchor to.
|
||||
zoomBy: function (factor) {
|
||||
var r = surface.getBoundingClientRect();
|
||||
zoomAt(r.left + r.width / 2, r.top + r.height / 2, factor);
|
||||
},
|
||||
|
||||
resetZoom: function () {
|
||||
var r = surface.getBoundingClientRect();
|
||||
zoomAt(r.left + r.width / 2, r.top + r.height / 2, 1 / scale);
|
||||
},
|
||||
|
||||
// fit frames the whole document with a margin, never zooming past 1:1 —
|
||||
// a canvas with three small nodes should not open comically magnified.
|
||||
fit: function (nodes) {
|
||||
var b = bounds(nodes);
|
||||
var r = surface.getBoundingClientRect();
|
||||
if (!b || !r.width || !r.height) {
|
||||
tx = 0; ty = 0; scale = 1;
|
||||
apply();
|
||||
return;
|
||||
}
|
||||
var pad = 60;
|
||||
var s = clampScale(Math.min(
|
||||
(r.width - pad * 2) / Math.max(b.width, 1),
|
||||
(r.height - pad * 2) / Math.max(b.height, 1),
|
||||
1
|
||||
));
|
||||
scale = s;
|
||||
tx = r.width / 2 - (b.x + b.width / 2) * s;
|
||||
ty = r.height / 2 - (b.y + b.height / 2) * s;
|
||||
apply();
|
||||
},
|
||||
|
||||
// centreOfViewport is where a newly added node goes: wherever the user
|
||||
// is currently looking.
|
||||
centreOfViewport: function () {
|
||||
var r = surface.getBoundingClientRect();
|
||||
return {
|
||||
x: (r.width / 2 - tx) / scale,
|
||||
y: (r.height / 2 - ty) / scale
|
||||
};
|
||||
},
|
||||
|
||||
bounds: bounds,
|
||||
onChange: function (fn) { listeners.push(fn); },
|
||||
refresh: apply
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user