d3 / d3-zoom

Pan and zoom SVG, HTML or Canvas using mouse or touch input.
https://d3js.org/d3-zoom
ISC License
505 stars 143 forks source link

Support flipped svgs #146

Closed charleslaw closed 5 years ago

charleslaw commented 6 years ago

I am using this lib for a web page that is drawing SVGs that can have flipped x or y coordinates (the g transform attr is set to scale(1, -1) for example).

I was able to in a not terrible way, add support for this in a local copy of the non minified code - the diff is below. Basically, k represents scale always, but a kxy param maps k to the scaling in x and y. The change means that any new Transform has to carry the kxy param, and the toString() function needs to take kxy into account, the rest basically just works. I wanted to share but am not sure what is needed to make this into a mergeable change - does this look like something that can be mergeable one day?

diff --git a/d3-zoom.v1.js b/d3-zoom.v1.js
index eb97b7d..6128cba 100644
--- a/d3-zoom.v1.js
+++ b/d3-zoom.v1.js
@@ -17,19 +17,21 @@ function ZoomEvent(target, type, transform) {
   this.transform = transform;
 }

-function Transform(k, x, y) {
+function Transform(k, x, y, kxy) {
   this.k = k;
   this.x = x;
   this.y = y;
+  //This can be undfined, it's handled later
+  this.kxy = kxy;
 }

 Transform.prototype = {
   constructor: Transform,
   scale: function(k) {
-    return k === 1 ? this : new Transform(this.k * k, this.x, this.y);
+    return k === 1 ? this : new Transform(this.k * k, this.x, this.y, this.kxy);
   },
   translate: function(x, y) {
-    return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y);
+    return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y, this.kxy);
   },
   apply: function(point) {
     return [point[0] * this.k + this.x, point[1] * this.k + this.y];
@@ -56,12 +58,51 @@ Transform.prototype = {
     return y.copy().domain(y.range().map(this.invertY, this).map(y.invert, y));
   },
   toString: function() {
-    return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")";
+    var scale = "scale(";
+    if (this.kxy) {
+        //TODO: Matrix?
+        scale += this.kxy[0]*this.k+","+this.kxy[1]*this.k;
+    } else {
+        scale += this.k;
+    }
+    scale += ")";
+    return "translate(" + this.x + "," + this.y + ") " + scale;
   }
 };

 var identity = new Transform(1, 0, 0);

+var parse = function(a){
+    //https://stackoverflow.com/a/17838403/557406
+    var b = {};
+    for (var i in a = a.match(/(\w+\((\-?\d+\.?\d*e?\-?\d*,?)+\))+/g)){
+        var c = a[i].match(/[\w\.\-]+/g);
+        b[c.shift()] = c.map(parseFloat);
+    }
+    return b;
+}
+
+var parseTransform = function (transformString){
+    if (!transformString) {
+        return identity;
+    }
+    var params = parse(transformString);
+    if (!params.translate) {
+        params.translate = [0,0];
+    }
+    if (!params.scale) {
+        params.scale = [1];
+    }
+    var k = Math.abs(params.scale[0]);
+    //NOTE: Parse will not have attr empty strings, instead it's just undefined
+    //We only use a pure k if scale is not flipped in x or y
+    if (k === params.scale[0] && (params.scale.length <2 || params.scale[1] === k)){
+        return new Transform(k, params.translate[0], params.translate[1]);
+    }
+    var kxy = [params.scale[0] / k, params.scale[1] / k];
+    return new Transform(k, params.translate[0], params.translate[1], kxy);
+}
+
 transform.prototype = Transform.prototype;

 function transform(node) {
@@ -136,9 +177,9 @@ var zoom = function() {
       wheelDelay = 150,
       clickDistance2 = 0;

-  function zoom(selection) {
+  function zoom(selection, parsedTransform) {
     selection
-        .property("__zoom", defaultTransform)
+        .property("__zoom", parsedTransform || defaultTransform)
         .on("wheel.zoom", wheeled)
         .on("mousedown.zoom", mousedowned)
         .on("dblclick.zoom", dblclicked)
@@ -207,12 +248,12 @@ var zoom = function() {

   function scale(transform$$1, k) {
     k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));
-    return k === transform$$1.k ? transform$$1 : new Transform(k, transform$$1.x, transform$$1.y);
+    return k === transform$$1.k ? transform$$1 : new Transform(k, transform$$1.x, transform$$1.y, transform$$1.kxy);
   }

   function translate(transform$$1, p0, p1) {
     var x = p0[0] - p1[0] * transform$$1.k, y = p0[1] - p1[1] * transform$$1.k;
-    return x === transform$$1.x && y === transform$$1.y ? transform$$1 : new Transform(transform$$1.k, x, y);
+    return x === transform$$1.x && y === transform$$1.y ? transform$$1 : new Transform(transform$$1.k, x, y, transform$$1.kxy);
   }

   function centroid(extent) {
@@ -235,7 +276,7 @@ var zoom = function() {
               i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));
           return function(t) {
             if (t === 1) t = b; // Avoid rounding error on end.
-            else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); }
+            else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k, a.kxy); }
             g.zoom(null, t);
           };
         });
@@ -283,15 +324,17 @@ var zoom = function() {
       return this;
     },
     emit: function(type) {
+      //TODO: Fix array if both elements are the same
       d3Selection.customEvent(new ZoomEvent(zoom, type, this.that.__zoom), listeners.apply, listeners, [type, this.that, this.args]);
     }
   };

   function wheeled() {
     if (!filter.apply(this, arguments)) return;
+    //Make scale an array for everything here
     var g = gesture(this, arguments),
         t = this.__zoom,
-        k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),
+        k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k  * Math.pow(2, wheelDelta.apply(this, arguments)))),
         p = d3Selection.mouse(this);

     // If the mouse is in the same location as before, reuse it.
@@ -303,7 +346,7 @@ var zoom = function() {
       clearTimeout(g.wheel);
     }

-    // If this wheel event won’t trigger a transform change, ignore it.
+    // If this wheel event won't trigger a transform change, ignore it.
     else if (t.k === k) return;

     // Otherwise, capture the mouse point and location at the start.
@@ -496,6 +539,7 @@ var zoom = function() {
 exports.zoom = zoom;
 exports.zoomTransform = transform;
 exports.zoomIdentity = identity;
+exports.parseTransform = parseTransform;

 Object.defineProperty(exports, '__esModule', { value: true });
charleslaw commented 6 years ago

https://stackoverflow.com/a/38230545/557406 would be better for parsing the string, though this is svg specific too

mbostock commented 5 years ago

It sounds like this would be supported by #48.