Wednesday, September 10, 2008

svg boundingbox of rotated ellipse

Found out last night that safari has a bug in its svg implementation (ok, probably lost most of you right there “who cares about svg?” … but there’s a possibility that this could be useful for flash/actionscript too)

Turns out that if you rotate an ellipse, the getBBox method returns a rather generous bounding box, because it’s using an implicit rectangle wrapped around the ellipse to get the bounding box.

You can see below what the browser bug is and what the correct box is. The red—buggy—implementation is created by the browser basing its calculations on the blue (implicit) rectangle that surrounds the ellipse.

The correct result is the box in green.

I created a javascript workaround for this bug.

It uses calculus. This is the first time I’ve used calculus in programming something that didn’t exist solely to demonstrate calculus.

I first tried to find a solution posted somewhere on the net, but only found a post where someone says it’s solved in the latest version, then reverses himself and says, ‘oh, no, actually it’s firefox that fixed its version of the same bug.’

If someone happens to find the bug # or a posted solution, that’d be cool.

// Trig functions (Math shortcut aliases)
var sin = Math.sin,
    cos = Math.cos,
    tan = Math.tan,
    cot = function(a){return 1/tan(a);},
    sqrt= Math.sqrt,
    acos= Math.acos,

    // our instance of an ellipse .. will become something more like this._shape (if this.type == ELLIPSE)
    ellipse = $('e').getElementsByTagName('ellipse')[0],

    // the properties of our instance of an ellipse
    a = ellipse.getAttribute('rx')*1, //x-radius (major, minor, who knows)
    b = ellipse.getAttribute('ry')*1, //y-radius (major, minor, who cares)
    cx = ellipse.getAttribute('cx')*1, //center's x-coord .. not currently used (assume the ellipse is centered at the origin)
    cy = ellipse.getAttribute('cy')*1, //center's y-coord .. not currently used (assume the ellipse is centered at the origin)
    matrix = ellipse.getCTM(), //the transform matrix
    angle = acos( matrix.a ),

    // the x coordinates where the ROTATED xmax and ymax occur on the ellipse BEFORE ROTATION.
    // these "magical" formulas represent the recombined derivative of the ellipse function
    x1 = (-1*(a*a)*tan(angle)) / sqrt(a*a*tan(angle)*tan(angle) + (b*b)),
    x2 = (   (a*a)*cot(angle)) / sqrt(a*a*cot(angle)*cot(angle) + (b*b)),

    // the actual xmax and ymax, reflecting the above, rotated into position (AFTER ROTATION).
    xmax = (cos(angle) * x2) + (sin(angle) * f(x2)),
    // (using negative-angle because positive screen y-axis is the same as 'normal maths' negative y-axis)
    ymax = (sin(-angle) * x1) + (cos(-angle) * f(x1));

// the f(x) that defines the ellipse (in general form)
function f(x){
    return b * Math.sqrt(1-((x*x)/(a*a)));

//~ console.log(ellipse.getAttribute('transform'));
//~ log(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);

// browser's idea of where the bbox is... (red box)
var box = ellipse.parentNode.getBBox();
re = $("re");
re.setAttribute("x", box.x);
re.setAttribute("y", box.y);
re.setAttribute("width", box.width);
re.setAttribute("height", box.height);
re.setAttribute("stroke", "red");

// my idea of where the bbox is... (green box)
re = $("re2");
re.setAttribute("x", -Math.abs(xmax));
re.setAttribute("y", -Math.abs(ymax));
re.setAttribute("width", Math.abs(xmax)*2);
re.setAttribute("height", Math.abs(ymax)*2);

1 comment:

  1. Not sure, but this is probably still a problem in Safari. It is still a problem in Chrome, and Chrome didn't even exist when I wrote this blogpost!