Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a Raphael fill pattern move with the element? (background image position relative to the element)

How can I give a Raphael element a fill that moves as the element moves, like how the CSS background-image of a position: absolute; HTML element would keep the same position relative to its location as it moved?

Here's an example demo: how can I make the background image pattern of the Raphael element (the triangular path) behave the same while dragged as the HTML element (the square div)?

http://jsbin.com/oxuyeq/8/edit


This question is essentially the opposite of How to make a pattern “fixed” in Raphael.js / IE? with the polarised glasses simulator - I want to make the IE-specific behaviour they were trying to avoid happen consistently, in all browsers (including IE8).

As detailed in that other question, in IE8 (VML) only, the Raphael element behaves how I want; but even this is erratic: various things like calling setSize on the paper element or re-defining the fill (essentially, anything forcing a redraw) cause it to switch to the other behaviour.


There's a question similar to this for pure SVG, but without any good answers at time of writing, and certainly none that work for Raphael.


Edit 2: Watching what happens in SVG mode, it seems that every Raphael transform also automatically does the same matrix transform to the svg <pattern> element. I think this is what causing the behaviour I'm trying to avoid - I think patternContentUnits and patternUnits are unrelated. There's an unresolved issue that seems related here (highlighting the same problem with clip-rect, next to the line this.pattern && updatePosition(this);) - https://github.com/DmitryBaranovskiy/raphael/issues/638

So one possibility might be to define a custom attribute that applies a transform to the element without also applying it to the pattern. Sounds difficult - might require hacking Raphael or duplicating lots of Raphael transform code. Hopefully there's some other way. And god help us making this work in VML...


Edit 3: Some potentially relevant information, it's not just path fills that have this problem. Raphael image elements created with paper.image() don't have this problem in SVG mode but have a very similar problem that is really bad in IE8 VML mode. Here's a demo of several ways to make image elements move, and here's a side-by-side comparison showing how they all work flawlessly in non-IE and all fail in IE:

enter image description here

like image 473
user56reinstatemonica8 Avatar asked Jul 22 '13 15:07

user56reinstatemonica8


2 Answers

I recently stumbled on a similar problem. I couldn't find a working solution anywhere, so I came up with my own. I used the answer by @user568458 as a starting point. First I changed the updatePosition function like so:

 updatePosition = function (o) {
    if(!o.data("relativeFill")) { //data is a custom store for user's properties
        var bbox = o.getBBox(1);
        $(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"});
    }
},

I also changed the fill case of setFillAndStroke function like so:

var relativeFill = o.data("relativeFill"),
isURL = Str(value).match(R._ISURL);
if (isURL) {
    el = $("pattern");
    var ig = $("image");
    el.id = R.createUUID();
    $(el, {x: 0, y: 0, patternUnits: relativeFill ? "objectBoundingBox" : "userSpaceOnUse", height: 1, width: 1});
    $(ig, {x: 0, y: 0, "xlink:href": isURL[1]});
    el.appendChild(ig);

    (function (el) {
        R._preload(isURL[1], function () {
            var w = this.offsetWidth,
                h = this.offsetHeight,
                bbox = o.getBBox();
            $(el, {width: 1, height: 1});
            $(ig, {width: relativeFill ? bbox.width : w, height: relativeFill ? bbox.height : h});
            o.paper.safari();
        });
    })(el);
    o.paper.defs.appendChild(el);
    $(node, {fill: "url(#" + el.id + ")"});
    o.pattern = el;
    o.pattern && updatePosition(o);
    break;
}

Then whenever you want to use it you have to set yourElement.data({ relativeFill: true })

Here's a working example with relative and repeated fill: https://codepen.io/mmazur/pen/Gmebrj?editors=0010


I agree with @user568458, this solution is very ugly and might stop working with a future Raphael update. However, Raphael hasn't been changing much lately so I'm fine with taking this risk.


EDIT: Fixed a bug described here: Image blurry when using url fill pattern for svg circle

like image 126
Michal Mazur Avatar answered Oct 20 '22 06:10

Michal Mazur


Edit:

There's a rough workaround for this that doesn't require hacking Raphael core code, but does depend on a not completely stable IE bug, described https://github.com/DmitryBaranovskiy/raphael/issues/177

It works by hiding the SVG pattern object from Raphael by renaming it in the Raphael object, causing updatePosition() to not be called. This stops Raphael moving the background image in SVG mode. In VML mode, IE8 has a bug whereby, even though the code moves the <fill> element, this isn't updated on the screen until a redraw is forced.

path.patternTmp = path.pattern;
delete path.pattern;

This has the same other downside as the suggestion below that in VML mode it relies on an IE bug. But it works for simple cases. I haven't tested fully enough to see if there are other side effects of this fix, but there shouldn't be - looked at Raphael's source code, it looks like updatePosition() is the only Raphael function that uses element.pattern.

http://jsbin.com/oxuyeq/10/edit

Note that path.pattern has to be renamed like this every time an image fill is applied - so if you reapply the fill for any reason, you need to re-do this fix - and that re-applying the fill will 'fix' the IE bug we depend on causing the image to slip out of sync with the object.


Original answer: Here's a sort-of fix. It's ugly in two ways:

  • For SVG, it relies on hacking Raphael core code (but only a small hack)
  • For VML, it relies on an Internet Explorer redraw bug, and will fail if your code causes a redraw

But you can get relative positions, so long as your code doesn't trigger an IE redraw (e.g. by re-setting the element's fill).

Ideally, there would be a way of doing this that works robustly in IE VML rather than relying on a bug.


You can do this ugly almost-fix by turning off the Raphael function updatePosition() somehow. The behaviour described above isn't an SVG default, it's a Raphael feature defined in the updatePosition() function which appears to be designed to make Raphael SVG elements' behaviour match how VML elements would behave if Internet Explorer wasn't a bundle of bugs held together with bugs. Note that the updatePosition() function is only called in SVG mode - VML fills are supposed to behave this way by default (but don't reliably because of the redraw bug described in the above linked question...).

My (ugly) way of doing this is to modify the updatePosition() function code to this:

updatePosition = function (o) {
    if(!o.relativeFill) {  // <<< added this condition
        var bbox = o.getBBox(1);
        $(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"});          
    }
},

...then, for each element you want to have a relative fill, you set someElement.relativeFill = true;


Note that this isn't good practice - e.g. a future Raphael update might use the relativeFill namespace for something else. It's just an example of a sticky plaster fix that (just about) works.


re. VML mode: In theory, you might be able to make this fix robust and not bug dependent by adding a similar condition to the place in the VML-mode Raphael functions setFillAndStroke and/or setCoords that set fill.position to a dynamic calculation. These seem to be the VML equivalent of updatePosition(). In theory, VML should by default set the fill relative to the shape so long as alignshape = true for the fill element, which in theory should be true by default.

But, it doesn't seem to work like that in practice. From my testing, relying on the IE bug as above actually seems to be more robust than trying to get IE VML to work as documented. In my testing, it's quite difficult to get IE8 to redraw the fill: only resetting the fill seems to do it.

like image 1
user56reinstatemonica8 Avatar answered Oct 20 '22 08:10

user56reinstatemonica8