Introduction
Quite often, when developing a new web page, we get the requirement to display images that are not rectangle. The easiest way out is to use a format that has transparency (png, gif). However, when there are many such large images within the page, both formats have disadvantages: We either lose on quality, if we use gif, or we lose in responsiveness, when using png, as png is lossless and a large-sized image results in a large file. Here I suggest an alternative way of loading such images, using jpg (which can result in a significantly small file size for the image without compromising too much quality) and a bit of JavaScript, using raphaël and a bit of jquery (although the latter is purely for reducing the lines of code).
Using the Code
The HTML markup and inline JavaScript is only an example of a relatively simple almost circular (but not quite circular) path. To get all those coordinates, I used a vector drawing program (e.g. Inkscape) to first draw the shape I wanted. Then I opened the resulting svg file with a text editor, as svg files are really XML. I searched for the path information. The relevant line is:
<path d="M 85.106383,487.46857 C
81.847017,330.82524 237.24118,217.14379
377.74267,222.77754 527.94414,228.80023
618.60707,399.39619 604.25532,530.02176 589.90357,
660.64733 463.97372,772.57588 311.70213,768.31963
215.82865,765.63981 88.365749,644.1119
85.106383,487.46857 z" id="path2996"/>
We only need the information in the d
attribute. These are instructions on how to reproduce the shape I created in the svg. To see what each instruction means, please refer to the SVG path data reference, or to the more concise raphaël js Path reference.
The problem with the above coordinates is that they are actually relative to the canvas created in the vector program. We don't really want that, we want the path translated to the top-left hand corner of the canvas. This can be a non-trivial task, however. What I did (I am sure there are other methods as well) was the following:
- Convert all spaces into line breaks. This gives the following:
M
85.106383,487.46857
C
81.847017,330.82524
237.24118,217.14379
377.74267,222.77754
527.94414,228.80023
618.60707,399.39619
604.25532,530.02176
589.90357,660.64733
463.97372,772.57588
311.70213,768.31963
215.82865,765.63981
88.365749,644.1119
85.106383,487.46857
z
- Convert all commas into tabs to get the following:
M
85.106383 487.46857
C
81.847017 330.82524
237.24118 217.14379
377.74267 222.77754
527.94414 228.80023
618.60707 399.39619
604.25532 530.02176
589.90357 660.64733
463.97372 772.57588
311.70213 768.31963
215.82865 765.63981
88.365749 644.1119
85.106383 487.46857
z
- Copy-paste the resulting data into a spreadsheet. The data should be presented as a 2-column array.
- Find the minimum of each column and subtract that from all values in that column.
- Get the resulting
string
. It should be as such:
M
3.259366 270.32478
C
0 113.68145
155.394163 0
295.895653 5.633750
446.097123 11.65644
536.760053 182.2524
522.408303 312.87797
508.056553 443.50354
382.126703 555.43209
229.855113 551.17584
133.981633 548.49602
6.518732 426.96811
3.259366 270.32478
z
- and make a JavaScript array out of it.
var MaskPath= [
["M"],
[3.259366, 270.32478],
["C"],
[0, 113.68145],
[155.394163, 0],
[295.895653, 5.63374999999999],
[446.097123, 11.65644],
[536.760053, 182.2524],
[522.408303, 312.87797],
[508.056553, 443.50354],
[382.126703, 555.43209],
[229.855113, 551.17584],
[133.981633, 548.49602],
[6.518732, 426.96811],
[3.259366, 270.32478],
["z"]
];
The rest of the JavaScript code is pretty straight forward.
First, we find the scale - all the above coordinates may need to be scaled down. Our canvas originally is 525x555, and the path just fits in that rectangle. However, the image we want to clip is 363x382, so we need a smaller canvas. We find the ratio of the two rectangles (image vs canvas).
var imageWidth = Number($(div).data('width'));
var imageHeight = Number($(div).data('height'));
var WidthRatio = imageWidth / initialCanvasWidth;
var HeightRatio = imageHeight / initialCanvasHeight;
var ratio = Math.min(WidthRatio, HeightRatio);
and scale down the canvas itself:
var paper = Raphael(id, initialCanvasWidth * ratio, initialCanvasHeight * ratio);
and each coordinate in the array:
[instruction[0] * ratio, instruction[1] * ratio]
Then it's only a matter of applying the image as "fill
" for the scaled path we have created:
fill: "url(" + $(div).data('url') + ")"
All information regarding the image is contained as data in the div
that serves as parent to the svg canvas created by raphaël:
<div id="raph1" class="raphObj" data-url="/image1.jpg" data-width="363" data-height="382"></div>
Points of Interest
The JavaScript code assumes two non-trivial things:
- The image has roughly the same width/height ratio with the original canvas, i.e., the canvas is almost square and so is the image. Trying to use an image that is elongated (e.g. a portrait or a landscape photo), you'll notice that it will be tiled as a background in the path. One can probably mitigate this by creating various path-and-canvas configurations (i.e. one for square or almost square images, one for portrait and one for landscape images).
- The image is smaller than the original canvas. Otherwise the call to...
var ratio = Math.min(WidthRatio, HeightRatio);
... doesn't work properly. One could probably check if the ratios are above or below 1 and choose between Math.min
and Math.max
accordingly.
Also, please make sure you are viewing the HTML file via a web server, otherwise the cdn-based JavaScript libraries (jquery and raphaël) will not load.