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 wo
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
.
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:
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.
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