Related
I have a set of nested For loops in my script (vanilla javascript, no jquery) that could go on for quite a while (30+ seconds). During this time, the browser is obviously unresponsive and the DOM isn't updating because the loop is blocking - just as one would expect.
Inside the nested For loops, I'm calling another function and generating a SVG circle with the parameters being passed and then adding the circles to an svg element and returning to the loop.
Question: what strategy can be used to update the DOM and actually show some of the SVG circles being created?
There are a lot of questions on S/O asking for similar solutions, but most of the examples use setTimeout(function, 0) and although I have tried using this, the DOM doesn't update probably because I'm not sure which function I should set the Timeout for?
I've also found some examples of webworkers, but can't quite wrap my head around how I could actually use those in this project.
Using this basic example - could someone show me how I can update the DOM while the loop is processing?
<button onclick="randomCircles()">
make circles
</button>
<svg
id="cont" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0" y="0" viewBox="0, 0, 900, 600">
<style>svg { background-color: black;}</style>
</svg>
<script>
function randomCircles(){
var x_end = 900;
var y_end = 675;
for(x=0; x<=x_end; x += 5){
for(y=0; y<=y_end; y += 5){
var new_circle = Math.random();
drawCircle(x,y,new_circle);
}
}
}
function drawCircle(x,y,circle_radius){
var svgns = "http://www.w3.org/2000/svg";
var container = document.getElementById('cont');
var circle = document.createElementNS(svgns, 'circle');
var circle_to_draw = circle_radius * 2;
circle.setAttributeNS(null, 'cx', x);
circle.setAttributeNS(null, 'cy', y);
circle.setAttributeNS(null, 'r', circle_to_draw);
circle.setAttributeNS(null, 'style', 'fill: white; stroke: none;' );
container.appendChild(circle);
return;
}
</script>
Very basic example (fiddle: https://jsfiddle.net/wx67uyzv/2/)
Here is an updated fiddle with the solution, thanks to Joel!
https://jsfiddle.net/198znrc0/
You're going to need to use setInterval().
// sample values
var loopSpeed = 25; // How often the window will refresh, in milliseconds
var loopEnd = 75;
var loopStart = 5;
var loopIncrement = 5;
var i = loopStart;
var intervalId = setInterval(() => {
if (i === loopEnd) {
// wrap up
clearInterval(intervalId);
return;
}
// do loop stuff
i += loopIncrement;
}, loopSpeed);
You need to split your code into multiple chunks which you can run without blocking browser for long time. Here is simple example of how it can be done, tried to keep it as close as possible to your original code. Basically you draw certain number of circles circlesCount per cycle and then use setTimeout to let browser update DOM.
function randomCircles() {
var x_end = 900;
var y_end = 675;
var x = 0;
var y = 0;
var circlesCount = 20;
function randomCirclesIteration() {
var drawnCircles = 0;
for(; x<=x_end; x += 5){
for(; y<=y_end; y += 5){
new_circle = Math.random();
drawCircle(x, y, new_circle);
drawnCircles++;
if (drawnCircles == circlesCount) {
setTimeout(randomCirclesIteration, 0);
return;
}
}
y = 0;
}
}
randomCirclesIteration();
}
function drawCircle(x, y, circle_radius){
var svgns = "http://www.w3.org/2000/svg";
var container = document.getElementById('cont');
var circle = document.createElementNS(svgns, 'circle');
var new_circle = (circle_radius * 2).toFixed(2);
circle.setAttributeNS(null, 'cx', x);
circle.setAttributeNS(null, 'cy', y);
circle.setAttributeNS(null, 'r', parseFloat(new_circle));
circle.setAttributeNS(null, 'style', 'fill: white; stroke: none;' );
container.appendChild(circle);
}
I'm looking for a way to zoom from place to place on a globe in D3 v4 (v4 is important). What I'm looking for is basically exactly this: https://www.jasondavies.com/maps/zoom/ My problem is that Jason Davies obfuscated his code, so I can't read it, and I can't find a bl.ock containing that project or anything similar to it. I'll provide a link to what I've got here: http://plnkr.co/edit/0mjyR3ovTfkDXB8FTG0j?p=preview
The relevant is probably inside the .tween():
.tween("rotate", function () {
var p = d3.geoCentroid(points[i]),
r = d3.geoInterpolate(projection.rotate(), [-p[0], -p[1]]);
return function (t) {
projection.rotate(r(t));
convertedLongLats = [projection(points[0].coordinates), projection(points[1].coordinates)]
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
for (var j = 0; j < points.length; j++) {
var textCoords = projection(points[j].coordinates);
c.fillStyle = textColors, c.textAlign = "center", c.font = "18px FontAwesome", c.fillText(points[j].icon, textCoords[0], textCoords[1]);
textCoords[0] += 15;
c.textAlign = "left", c.font = " 12px Roboto", c.fillText(points[j].location, textCoords[0], textCoords[1]);
}
c.strokeStyle = textColors, c.lineWidth = 4, c.setLineDash([10, 10]), c.beginPath(), c.moveTo(convertedLongLats[0][0], convertedLongLats[0][1]), c.lineTo(convertedLongLats[1][0], convertedLongLats[1][1]), c.stroke();
};
})
Basically, I want what I've got now but I want it to zoom in, pretty much exactly like it is in the Animated World Zoom example I provided above. I'm not really looking for code, I'd rather someone point me in the right direction with an example or something (it's worth noting that I'm fairly new to d3 and that this project is heavily based on World Tour by mbostock, so it uses canvas). Thank you all in advance!
Based on your plunker and comment, a challenge in zooming out between two points in a transition is that the interpolator will only interpolate between two values. The solution in your plunker relies on two interpolators, one for zooming in and zooming out. This method has added un-needed complexity and somewhere along the line, as you note, it jumps to an incorrect scale. You could simplify this:
Take an interpolator that interpolates between -1 and 1, and weight each scale according to the absolute value of the interpolator. At zero, the zoom should be out all the way, while at -1,1, you should be zoomed in:
var s = d3.interpolate(-1,1);
// get the appropriate scale:
scale = Math.abs(0-s(t))*startEndScale + (1-Mat.abs(0-s(t)))*middleScale
This is a little clunky as it goes from zooming out to zooming in rather abruptly, so you could ease it with a sine type easing:
var s = d3.interpolate(0.0000001,Math.PI);
// get the appropriate scale:
scale = (1-Math.abs(Math.sin(s(t))))*startEndScale + Math.abs(Math.sin(s(t)))*middleScale
I've applied this to your plunker here.
For a simple and minimal example using the example that I suggested and your two points and path (and using your plunkr as a base), stripping out the animated line and icons, I would probably put together something like (plunker, snippet below best viewed on full screen):
var width = 600,
height = 600;
var points = [{
type: "Point",
coordinates: [-74.2582011, 40.7058316],
location: "Your Location",
icon: "\uF015"
}, {
type: "Point",
coordinates: [34.8887969, 32.4406351],
location: "Caribe Royale Orlando",
icon: "\uF236"
}];
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var c = canvas.node().getContext("2d");
var point = points[0].coordinates;
var projection = d3.geoOrthographic()
.translate([width / 2, height / 2])
.scale(width / 2)
.clipAngle(90)
.precision(0.6)
.rotate([-point[0], -point[1]]);
var path = d3.geoPath()
.projection(projection)
.context(c);
var colorLand = "#4d4f51",
colorGlobe = "#2e3133",
textColors = "#fff";
d3.json("https://unpkg.com/world-atlas#1/world/110m.json", function (error, world) {
if (error) throw error;
var sphere = { type: "Sphere" };
var land = topojson.feature(world, world.objects.land);
var i = 0;
var scaleMiddle = width/2;
var scaleStartEnd = width * 2;
loop();
function loop() {
d3.transition()
.tween("rotate",function() {
var flightPath = {
type: 'Feature',
geometry: {
type: "LineString",
coordinates: [points[i++%2].coordinates, points[i%2].coordinates]
}
};
// next point:
var p = points[i%2].coordinates;
// current rotation:
var currentRotation = projection.rotate();
// next rotation:
var nextRotation = projection.rotate([-p[0],-p[1]]).rotate();
// Interpolaters:
var r = d3.geoInterpolate(currentRotation,nextRotation);
var s = d3.interpolate(0.0000001,Math.PI);
return function(t) {
// apply interpolated values
projection.rotate(r(t))
.scale( (1-Math.abs(Math.sin(s(t))))*scaleStartEnd + Math.abs(Math.sin(s(t)))*scaleMiddle ) ;
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
c.beginPath(), path(flightPath), c.globalAlpha = 0.5, c.shadowColor = "#fff", c.shadowBlur = 5, c.lineWidth = 0.5, c.strokeStyle = "#fff", c.stroke(), c.shadowBlur = 0, c.globalAlpha = 1;
}
})
.duration(3000)
.on("end", function() { loop(); })
}
});
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
I use javascript (not jQuery).
In short
Filled circle
Transparent behind the circle
Number centered in the middle
These values need to be easy to change:
Radius
Background color
Font size
Font family
It should not care if the number will break the circle.
Reason
The reason for me needing this is I have a Google map. It has a lot of markers with different numbers and colors. I found a script that works (in Chrome) but it needed D3 in order to work.
It seems like this is a simple task. That's why I hope I could leave D3 in this case.
Update
Here is my current code. I've read some more and maybe it would be possible to do it with SVG. What ever works.
var karta = (function () {
var fn = {};
var map;
var latitude;
var longitude;
var zoom;
var cache = {};
var colors = [];
var height = 75;
var width = 75;
// Init
fn.init = function(options) {
console.log(options);
markers = options.markers;
latitude = options.latitude;
longitude = options.longitude;
zoom = options.zoom;
colors = fn.setColors();
fn.setMap();
fn.setMarkers();
console.log(options);
};
// Set map
fn.setMap = function() {
map = new google.maps.Map(document.getElementById('map_canvas'), {
zoom: zoom,
center: {
lat: latitude,
lng: longitude
},
mapTypeId: 'satellite',
mapTypeControl: false,
zoomControl: false,
streetViewControl: false,
});
};
// Set markers
fn.setMarkers = function() {
var mytest = '<svg height="32" width="32"><foreignObject width="32" height="32" x="16" y="16" transform="translate(-16,-16)"><div class="circle" style="background: blue; border-radius: 100%; text-align: center; line-height: 32px; font-size: 12px;"><span style="display: inline-block; vertical-align: middle; color: #fff; font-weight: bold;">180</span></div></foreignObject></svg>';
var myurl = 'data:image/svg+xml;charset=UTF-8;base64,' + btoa(mytest);
console.log(myurl);
markers.forEach( function( point ) {
fn.icon( point[0], function(src) {
new google.maps.Marker({
position: new google.maps.LatLng( point[1], point[2] ),
map: map,
icon: {
url: myurl,
anchor: new google.maps.Point(25, 25),
origin: new google.maps.Point(0, 0),
scaledSize: new google.maps.Size(50, 50)
}
});
});
});
};
// Set colors
fn.setColors = function() {
colors[65] = "E50000";
colors[80] = "E52500";
colors[95] = "E54A00";
colors[110] = "E56F00";
colors[125] = "E59400";
colors[140] = "B79B00";
colors[155] = "89A200";
colors[170] = "5CA900";
colors[185] = "2EB000";
colors[250] = "00B700";
return colors;
};
// Set circle
fn.setCircle = function(svg, number) {
var circles = svg.append('circle')
.attr('cx', '27.2')
.attr('cy', '27.2')
.attr('r', '12')
.style('fill', colors[number]);
return circles;
};
// Set label
fn.setLabel = function(svg, number) {
var label = svg.append('text')
.attr('dx', 27)
.attr('dy', 32)
.attr('text-anchor', 'middle')
.attr('style', 'font-size: 12px; fill: #FFFFFF; font-family: Arial, Verdana; font-weight: bold')
.text(number);
return label;
};
// Set svg
fn.setSvg = function(number) {
var svg = d3.select(document.createElement('div')).append('svg')
.attr('viewBox', '0 0 54.4 54.4')
.append('g')
fn.setCircle(svg, number);
fn.setLabel(svg, number);
return svg;
};
// Set image
fn.setImage = function(number, node, callback) {
var image = new Image();
image.onload = (function(width, height) {
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var dataURL;
d3.select(canvas)
.attr('width', width)
.attr('height', height);
context.drawImage(image, 0, 0, width, height);
dataURL = canvas.toDataURL();
generateIconCache[number] = dataURL;
callback(dataURL);
}).bind(this, width, height);
var xmlSource = (new XMLSerializer()).serializeToString(node);
image.src = 'data:image/svg+xml;base64,' + btoa(encodeURIComponent(xmlSource).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
};
// Icon
fn.icon = function( number, callback ) {
if( cache[number] !== undefined ) {
callback( cache[number] );
}
var svg = fn.setSvg(number);
var node = svg.node().parentNode.cloneNode(true);
d3.select(node).select('clippath').remove();
fn.setImage(number, node, callback);
}
return fn;
})();
Scaling text to fit a circle.
To scale text to fit a circle you need to find the angle of the diagonal line across the text. To get that you need the font height and the font width. The font height is in the font attribute of the 2D context. Eg "18px Arial" and the font width can be calculated using the 2D context textWidth = ctx.measureText(text).width.
once you have the font width and the font height you can get the diagonal angle with textDiag = Math.atan(fontheight / fontWidth) . This will be the angle where on the circle the corner of the text will touch the side.
We can draw a line from that point on the circle circumference to the horizontal line through the middle of the circle to give us the location of right edge of the text. You get that with halfWidth = Math.cos(textDiag) * radius The total text width should be twice that.
As it is a real pain to find a point size to match a given text width, it is easier just to scale the text using the context transformation matrix.
The scale is simply the required text width divided by then known text width. fontScale = halfWidth * 2) / textWidth then set the transform ctx.setTransform(fontScale,0,0,fontScale,r,r) (r is the radius or center of circle) and draw the text ctx.filltext(text,0,0) with the textAlign and textBaseline 2d context attributes set to "center" and "middle" respectively
The example show this in action by creating a simple text circle img that can be drawn on another canvas, or just added to the DOM as an image. The two properties number and radius set the number to display and the size in pixels. The demo shows it being created and drawn onto a canvas with the number increasing to show how it scales.
// creates a circle image object
// Function arguments.
// radius is the you know ....
// number is the value to display in the circle
// style is this style
// style.color the circle colour. Defaults to "red";
// style.font the font. Defaults to '18px arial';
// style.fontColor is the font colour. Defaults to "white";
// style.fit if true then the number is made to fit te circle as best it can. Defaults to true
// style.decimals number of decimal places to display. Defaults to 0
//
// returns
// Html canvas element with the following custom attributes
// ctx the 2D context
// number the value to display
// radius the radius
// displayStyle the referance to the style arrgument passed when createCircleNumberImage
// was called
// draw the function used to render the image. This function has no arguments
// use the displayStyle atrribute to set the style
function createCircleNumberImage(radius,number,style){
// create HTML 5 image element
img = document.createElement("canvas");
// size it
img.width = Math.ceil(radius * 2);
img.height = Math.ceil(radius * 2);
// get a drawing context
img.ctx = img.getContext("2d");
// set custom attributes
img.radius = radius;
img.number = number;
img.displayStyle = style;
// set defaults
style.color = style.color ? style.color : "red";
style.font = style.font ? style.font : '18px arial';
style.fontColor = style.fontColor ? style.fontColor : "white";
style.fit = style.fit === undefined ? true : style.fit;
style.decimals = style.decimals === undefined || isNaN(style.decimals) ? 0 : style.decimals;
// add draw function
img.draw = function(){
var fontScale, fontWidth, fontSize, number;
// resize
this.width = Math.ceil(this.radius * 2);
this.height = Math.ceil(this.radius * 2);
// clear (incase resize did not do it)
this.ctx.clearRect(0,0,this.width,this.height);
// draw the circle
this.ctx.fillStyle = this.displayStyle.color;
this.ctx.beginPath();
this.ctx.arc(radius,radius,radius,0,Math.PI * 2);
this.ctx.fill();
// setup the font styles
this.ctx.font = this.displayStyle.font;
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.fillStyle = this.displayStyle.fontColor;
// get the value to display
number = this.number.toFixed(this.displayStyle.decimals);
// get the font size
fontSize = Number(/[0-9\.]+/.exec(this.ctx.font)[0]);
if(!this.displayStyle.fit || isNaN(fontSize)){ // Dont fit text or font height unknown
this.ctx.fillText(number,radius,radius);
}else{
// fit font as based on the angle from text center to bottom right
fontWidth = this.ctx.measureText(number).width;
fontScale = Math.cos(Math.atan(fontSize/fontWidth)) * this.radius * 2 / fontWidth;
this.ctx.setTransform(fontScale,0,0,fontScale,this.radius,this.radius);
this.ctx.fillText(number,0,0);
this.ctx.setTransform(1,0,0,1,0,0); // restor the transform
}
if(!this.displayStyle.fit || isNaN(fontSize)){ // Dont fit text or font height unknown
this.ctx.fillText(number,radius,radius);
}else{
fontScale = Math.cos(Math.atan(fontSize/fontWidth)) * this.radius * 2 / fontWidth;
this.ctx.setTransform(fontScale,0,0,fontScale,this.radius,this.radius);
this.ctx.fillText(number,0,0);
this.ctx.setTransform(1,0,0,1,0,0); // restor the transform
}
// return this so you can call the draw function from within a canvas drawImage function
return this;
}
// draw first time
img.draw()
// return new image
return img;
}
var canvas = document.createElement("canvas");
canvas.width = 320;
canvas.height = 200;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
// set comments above the function declaration for help
var sun = createCircleNumberImage (
60,1,{
fontColor : "white",
font : "24px arial",
color : "#EE0",
fit : true,
decimals : 0,
}
)
function doAgain(){
ctx.fillStyle = "black";
ctx.fillRect(0,0,canvas.width,canvas.height/2)
ctx.fillStyle = "Red";
ctx.fillRect(0,canvas.height /2 ,canvas.width,canvas.height/2)
if(sun.number > 5000000000){
sun.number = 0;
}
sun.number = sun.number + Math.floor(sun.number/10) + 1;
ctx.drawImage(sun.draw(),ctx.canvas.width / 2 - sun.radius,ctx.canvas.height / 2 - sun.radius);
setTimeout(doAgain,200);
}
doAgain();
I have a SVG file that I want to store in a database. It would be very helpful to store all the objects as polygons and store the coordinates. However the file I work with was exported from Ilustrator. It does contain polygons but also transformed rectangles and other forms. The elements don't have to be rendered with absolute precision so rounding errors are not an issue.
How can I convert the transformed rects?
This is ok:
<polygon points="2694.423,2972.209 2685.76,2982.961 2702.282,2996.274
2710.938,2985.526 "/
and can be stored as a list of coordinates.
This is problematic
<rect x="4316.474" y="2236.01" transform="matrix(-0.3208 -0.9471 0.9471 -0.3208 3591.1353 7063.0771)" width="22.991" height="15.92"/>
and should be converted to a polygon as in the first example.
Eventually I would like to use a html5 canvas to display the shapes.
To render the polyon data I've use the kinecticjs framework in the following way:
function poly (layer, points)
{
var poly = new Kinetic.Line({
points: points,
fill: 'black',
stroke: 'black',
strokeWidth: 0,
closed: true
});
// add the shape to the layer
layer.add(poly);
return poly;
}
Are there know performance issues with canvas vs svg?. I have about 3000 clickable objects to render.
As to the question:
Are there know performance issues with canvas vs svg?. I have about
3000 clickable objects to render.
The 3000 elements are no problem. But you cannot include events on the individual canvas elements.
The apply-transforms normaliser step could be achieved using http://petercollingridge.appspot.com/svg_optimiser.
As for the second step, you may have to add your own code for that.
In many cases it is meaningful to return certain svg elements(line, rect, circle, ellipse, polygon, polyline, and path) to their screen x,y values following transformations. This is accomplished using getCTM, and matrixTransform
Note: Use vector-effect="non-scaling-stroke" for elements with stroke(*not available in IE).
The following is code to return screen points for various transformed svg elemens:
//----build a generic document SVG root to hold svg point---
function screenLine(line,svg)
{
var sCTM = line.getCTM()
var x1=parseFloat(line.getAttribute("x1"))
var y1=parseFloat(line.getAttribute("y1"))
var x2=parseFloat(line.getAttribute("x2"))
var y2=parseFloat(line.getAttribute("y2"))
var mySVGPoint1 = svg.createSVGPoint();
mySVGPoint1.x = x1
mySVGPoint1.y = y1
mySVGPointTrans1 = mySVGPoint1.matrixTransform(sCTM)
line.setAttribute("x1",mySVGPointTrans1.x)
line.setAttribute("y1",mySVGPointTrans1.y)
var mySVGPoint2 = svg.createSVGPoint();
mySVGPoint2.x = x2
mySVGPoint2.y = y2
mySVGPointTrans2= mySVGPoint2.matrixTransform(sCTM)
line.setAttribute("x2",mySVGPointTrans2.x)
line.setAttribute("y2",mySVGPointTrans2.y)
//---force removal of transform--
line.setAttribute("transform","")
line.removeAttribute("transform")
}
function screenCircle(circle,svg)
{
var sCTM = circle.getCTM()
var scaleX = sCTM.a;
var cx=parseFloat(circle.getAttribute("cx"))
var cy=parseFloat(circle.getAttribute("cy"))
var r=parseFloat(circle.getAttribute("r"))
var mySVGPointC = svg.createSVGPoint();
mySVGPointC.x = cx
mySVGPointC.y = cy
mySVGPointTransC = mySVGPointC.matrixTransform(sCTM)
circle.setAttribute("cx",mySVGPointTransC.x)
circle.setAttribute("cy",mySVGPointTransC.y)
circle.setAttribute("r",r*scaleX)
//---force removal of transform--
circle.setAttribute("transform","")
circle.removeAttribute("transform")
}
function screenEllipse(ellipse,svg)
{
var sCTM = ellipse.getCTM()
var scaleX = sCTM.a;
var scaleY = sCTM.d;
var cx=parseFloat(ellipse.getAttribute("cx"))
var cy=parseFloat(ellipse.getAttribute("cy"))
var rx=parseFloat(ellipse.getAttribute("rx"))
var ry=parseFloat(ellipse.getAttribute("ry"))
var mySVGPointC = svg.createSVGPoint();
mySVGPointC.x = cx
mySVGPointC.y = cy
mySVGPointTransC = mySVGPointC.matrixTransform(sCTM)
ellipse.setAttribute("cx",mySVGPointTransC.x)
ellipse.setAttribute("cy",mySVGPointTransC.y)
ellipse.setAttribute("rx",rx*scaleX)
ellipse.setAttribute("ry",ry*scaleY)
//---force removal of transform--
ellipse.setAttribute("transform","")
ellipse.removeAttribute("transform")
}
function screenRect(rect,svg)
{
var sCTM = rect.getCTM()
var scaleX = sCTM.a;
var scaleY = sCTM.d;
var x=parseFloat(rect.getAttribute("x"))
var y=parseFloat(rect.getAttribute("y"))
var width=parseFloat(rect.getAttribute("width"))
var height=parseFloat(rect.getAttribute("height"))
var mySVGPoint = svg.createSVGPoint();
mySVGPoint.x = x
mySVGPoint.y = y
mySVGPointTrans = mySVGPoint.matrixTransform(sCTM)
rect.setAttribute("x",mySVGPointTrans.x)
rect.setAttribute("y",mySVGPointTrans.y)
rect.setAttribute("width",width*scaleX)
rect.setAttribute("height",height*scaleY)
//---force removal of transform--
rect.setAttribute("transform","")
rect.removeAttribute("transform")
}
function screenPolyline(myPoly,svg)
{
var sCTM = myPoly.getCTM()
var pointsList = myPoly.points;
var n = pointsList.numberOfItems;
for(var m=0;m<n;m++)
{
var mySVGPoint = mySVG.createSVGPoint();
mySVGPoint.x = pointsList.getItem(m).x
mySVGPoint.y = pointsList.getItem(m).y
mySVGPointTrans = mySVGPoint.matrixTransform(sCTM)
pointsList.getItem(m).x=mySVGPointTrans.x
pointsList.getItem(m).y=mySVGPointTrans.y
}
//---force removal of transform--
myPoly.setAttribute("transform","")
myPoly.removeAttribute("transform")
}
function screenPath(path,svg)
{
var sCTM = path.getCTM()
var scaleX = sCTM.a;
var scaleY = sCTM.d;
var segList=path.pathSegList
var segs=segList.numberOfItems
//---change segObj values
for(var k=0;k<segs;k++)
{
var segObj=segList.getItem(k)
if(segObj.x && segObj.y )
{
var mySVGPoint = svg.createSVGPoint();
mySVGPoint.x = segObj.x
mySVGPoint.y = segObj.y
mySVGPointTrans = mySVGPoint.matrixTransform(sCTM)
segObj.x=mySVGPointTrans.x
segObj.y=mySVGPointTrans.y
}
if(segObj.x1 && segObj.y1)
{
var mySVGPoint1 = svg.createSVGPoint();
mySVGPoint1.x = segObj.x1
mySVGPoint1.y = segObj.y1
mySVGPointTrans1 = mySVGPoint1.matrixTransform(sCTM)
segObj.x1=mySVGPointTrans1.x
segObj.y1=mySVGPointTrans1.y
}
if(segObj.x2 && segObj.y2)
{
var mySVGPoint2 = svg.createSVGPoint();
mySVGPoint2.x = segObj.x2
mySVGPoint2.y = segObj.y2
mySVGPointTrans2 = mySVGPoint2.matrixTransform(sCTM)
segObj.x2=mySVGPointTrans2.x
segObj.y2=mySVGPointTrans2.y
}
if(segObj.r1)segObj.r1=segObj.r1*scaleX
if(segObj.r2)segObj.r2=segObj.r2*scaleX
}
//---force removal of transform--
path.setAttribute("transform","")
path.removeAttribute("transform")
}
//---changes all transformed points to screen points---
function screenPolygon(myPoly,mySVG)
{
var sCTM = myPoly.getCTM()
var pointsList = myPoly.points;
var n = pointsList.numberOfItems;
for(var m=0;m<n;m++)
{
var mySVGPoint = mySVG.createSVGPoint();
mySVGPoint.x = pointsList.getItem(m).x
mySVGPoint.y = pointsList.getItem(m).y
mySVGPointTrans = mySVGPoint.matrixTransform(sCTM)
pointsList.getItem(m).x=mySVGPointTrans.x
pointsList.getItem(m).y=mySVGPointTrans.y
}
//---force removal of transform--
myPoly.setAttribute("transform","")
myPoly.removeAttribute("transform")
}
Given:
Your untransformed SVG points
And your SVG transformation matrix
You can mathematically apply the transform to those points.
The resulting points are the transformed XY--the "normalized" XY.
The resulting points will draw to canvas in the same locations as the transformed SVG points.
The resulting points won't require the SVG transformation to be draw in the correct locations.
Here's the code that applies a transformation matrix to "normalize" a point:
function simplifyTransform(point,matrix){
simpleX = point.x * matrix[0] + point.y * matrix[2] + matrix[4];
simpleY = point.x * matrix[1] + point.y * matrix[3] + matrix[5];
return({x:simpleX,y:simpleY});
}
Here's an illustration:
Here's code and a Demo: http://jsfiddle.net/m1erickson/ushWr/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
#canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var x=50;
var y=50;
var h=30;
var w=50;
var matrix=[1.25,.75,.75,1.25,20,20];
ctx.save();
ctx.transform(matrix[0],matrix[1],matrix[2],matrix[3],matrix[4],matrix[5],matrix[6]);
ctx.fillStyle="red";
ctx.fillRect(x,y,w,h);
ctx.restore();
// make an array of the 4 corners of the rectangle
var points=[];
points.push({x:x,y:y});
points.push({x:x+w,y:y});
points.push({x:x+w,y:y+h});
points.push({x:x,y:y+h});
// get the transformed rectangle's corners in untransformed space
var rect=simplifyPoly(points,matrix);
// stroke the untransformed rectangle
ctx.save();
ctx.strokeStyle="green";
ctx.lineWidth=2;
ctx.moveTo(rect[0].x,rect[0].y);
for(var i=1;i<rect.length;i++){
ctx.lineTo(rect[i].x,rect[i].y);
}
ctx.closePath();
ctx.stroke();
ctx.restore();
function simplifyTransform(point,matrix){
simpleX = point.x * matrix[0] + point.y * matrix[2] + matrix[4];
simpleY = point.x * matrix[1] + point.y * matrix[3] + matrix[5];
return({x:simpleX,y:simpleY});
}
function simplifyPoly(points,matrix){
var simplePoints=[];
for(var i=0;i<points.length;i++){
simplePoints.push(simplifyTransform(points[i],matrix));
}
return(simplePoints);
}
}); // end $(function(){});
</script>
</head>
<body>
<h4>The red fill is drawn using untransformed points plus a transform<br>The green stroke is drawn using the "simplified" points--no transform involved.</h4>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
After a more elaborate search I'd found Peter Collingridge experiments on http://petercollingridge.appspot.com/svg-editor/
In Illustrator it is possible to change transformed rects in poly's with Object/Path/Simplify
I'm trying to animatate a spinning star icon:
var star = this._paper.path("M26.522,12.293l-5.024-0.73c-1.089-0.158-2.378-1.095-2.864-2.081l-2.249-4.554c-0.487-0.986-1.284-0.986-1.771,0l-2.247,4.554c-0.487,0.986-1.776,1.923-2.864,2.081l-5.026,0.73c-1.088,0.158-1.334,0.916-0.547,1.684l3.637,3.544c0.788,0.769,1.28,2.283,1.094,3.368l-0.858,5.004c-0.186,1.085,0.458,1.553,1.432,1.041l4.495-2.363c0.974-0.512,2.566-0.512,3.541,0l4.495,2.363c0.974,0.512,1.618,0.044,1.433-1.041l-0.859-5.004c-0.186-1.085,0.307-2.6,1.095-3.368l3.636-3.544C27.857,13.209,27.611,12.452,26.522,12.293zM22.037,16.089c-1.266,1.232-1.966,3.394-1.67,5.137l0.514,2.984l-2.679-1.409c-0.757-0.396-1.715-0.612-2.702-0.612s-1.945,0.216-2.7,0.61l-2.679,1.409l0.511-2.982c0.297-1.743-0.404-3.905-1.671-5.137l-2.166-2.112l2.995-0.435c1.754-0.255,3.592-1.591,4.373-3.175L15.5,7.652l1.342,2.716c0.781,1.583,2.617,2.92,4.369,3.173l2.992,0.435L22.037,16.089z")
.attr({ fill: "darkred", stroke: "none" })
.transform("t"+starX+","+starY);
var a0 = Raphael.animation({ transform: "r360"}, 2000);
star.animate(a0.repeat(Infinity));
If I remove the translate I get a nice animation. But with the translate the animation is weird.
The parameters for the animation should include the translation as well, since they are the end attributes of the object, not just the difference between the start and the end ones. See the example: http://jsfiddle.net/b9akz/32/.
var paper = new Raphael('holder');
var starX = 100, starY = 100;
var star = paper.path("M26.522,12.293l-5.024-0.73c-1.089-0.158-2.378-1.095-2.864-2.081l-2.249-4.554c-0.487-0.986-1.284-0.986-1.771,0l-2.247,4.554c-0.487,0.986-1.776,1.923-2.864,2.081l-5.026,0.73c-1.088,0.158-1.334,0.916-0.547,1.684l3.637,3.544c0.788,0.769,1.28,2.283,1.094,3.368l-0.858,5.004c-0.186,1.085,0.458,1.553,1.432,1.041l4.495-2.363c0.974-0.512,2.566-0.512,3.541,0l4.495,2.363c0.974,0.512,1.618,0.044,1.433-1.041l-0.859-5.004c-0.186-1.085,0.307-2.6,1.095-3.368l3.636-3.544C27.857,13.209,27.611,12.452,26.522,12.293zM22.037,16.089c-1.266,1.232-1.966,3.394-1.67,5.137l0.514,2.984l-2.679-1.409c-0.757-0.396-1.715-0.612-2.702-0.612s-1.945,0.216-2.7,0.61l-2.679,1.409l0.511-2.982c0.297-1.743-0.404-3.905-1.671-5.137l-2.166-2.112l2.995-0.435c1.754-0.255,3.592-1.591,4.373-3.175L15.5,7.652l1.342,2.716c0.781,1.583,2.617,2.92,4.369,3.173l2.992,0.435L22.037,16.089z")
.attr({ fill: "darkred", stroke: "none" })
.translate(starX,starY);
var a0 = Raphael.animation({ transform: "t"+starX + "," + starY + " r360"}, 2000);
star.animate(a0.repeat(Infinity));
You need to factor in the translation in the animate as follows:
var paper = Raphael(0,0,500,500);
var starX = 50;
var starY = 50;
// code for your path here as its rather long! Including your translate
// using the starX and starY
// then the animation
var a0 = Raphael.animation({ transform: "t"+starX+","+starY+"r360"}, 2000);
star.animate(a0.repeat(Infinity));
I also made a jsfiddle example here for you to see.