Translating SVG elements for an Isometric view - javascript

I'm working on some JavaScript code to render standard 2D SVG/Canvas elements (drawn with Raphael-JS) in an isometric 3Dish view.
Say we have two rectangles drawn next to each other. I then have them redrawn at the correct angles (basically a 30 degree twist) for an isometric view.
(In the image above I've shown the origin for two corresponding elements.)
My problem is I don't know how to properly translate all the individual elements so they "tile" correctly instead of just overlapping.
While actually using tiles would make things easier as I could just base any given tile's placement on the one before it, tiles won't work in this case. Everything is dynamic and will be more complex than simple x/y planes.
Here is an image of some isometric tiles if there's any confusions as to how I want these objects to be placed.

You shouldn't apply the transformation to the individual elements, but to the source elements as a collection. In Raphael, you could use something like
var s = paper.set();
s.push(square1, square2);
and now do the transformations without too much math, which is supposed to work like this:
// s.clone(); // if you want to keep originals
s.rotate(45, 0, 0).scale(1, .7).translate(100, 0);
(However, scaling of rotated items seems to be broken in RaphaelJS.)
Plain SVG example:
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="-200,-500 1000,1000">
<title>Isometric</title>
<g id="source"> <!-- group -->
<circle cx="-50" cy="-50" r="50"/>
<rect width="100" height="100"/>
<rect width="100" height="100" x="101"/>
<rect width="100" height="100" x="50" y="-200"/>
</g>
<!-- make copy of group and apply transformations -->
<use xlink:href="#source" transform="translate(500) scale(1, .7) rotate(-45)"/>
</svg>

Using Raphel.js 2.0 you can do this using the .transform() method and providing a transform string that rotates 45 degrees and scales vertically 70% (or whatever pitch you want). It's important to pay attention to the position you are rotating and scaling around as well - in this case I'm using 0,0. You will also notice I'm translating 100 over to the right to compensate for the rotation.
Transform strings are also great for this use case because you can simply prepend the projection transformation to the transformation of other objects in the scene and they will all end up in the right place.
For example (see http://jsfiddle.net/k22yG/):
var paper = Raphael(10, 10, 320, 240),
set = paper.set();
// Build a set to make it easier to transform them all at once
set.push(
// Grid of rectangles
paper.rect(0, 0, 50, 50),
paper.rect(60, 0, 50, 50),
paper.rect(0, 60, 50, 50),
paper.rect(60, 60, 50, 50)
);
// Rotate, then scale, then move (describe in "reverse" order)
set.transform('t100,0s1,0.7,0,0r45,0,0');​

Related

How to get bounding coordinates from an SVG path

I'm working on a project that requires bounding draggable elements inside complex shapes, defined ideally by SVG Path elements, using Javascript.
I'm open to other ways of defining the bounds but SVG would tie in better with the source material.
I've tried using pathSegList however this returns undefined, and I've read it's deprecated.
How would I go about getting a list of coordinates from an SVG path element, which I could translate to X/Y coordinates for Javascript?
You can use the .getBBox() method on the path element. Take a look at the snippet:
console.log(document.getElementById('mypath').getBBox());
<svg width="300" height="100" viewBox="0 0 300 100" style="background:#efefef">
<path id="mypath" d="M20,20 L40,20 40,40, 20,40 Z" fill="red" />
</svg>

add mouse event to svg <pattern>, detect grid hover

I have a bit of a complicated question. I'm trying to render a grid via svg and then add event listeners to the grid. Currently I'm rendering the grid via a <pattern> element. I'm open to other avenues for rendering the grid, but it needs to be scalable / performant, as this grid could easily be 10's of thousands of squares. Think of it like a floorplan or a blueprint.
What I want: I'd like to be able to attach an event listener to every square of the grid.
What I've done: I looked up the svg docs, tried a bunch of different things (like onclick handlers, svg attributes like pointer-events... etc) but with no luck. I can probably make it work with mouse client coordinates, however I'd like to avoid using this method if possible as this svg will have zoom and pan functionality.. which can make transforming coordinates a pain.
Base svg code (simplified for this question):
<svg viewBox="0 0 100 100">
<g className="view-control">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="gray" strokeWidth="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</g>
</svg>
Fiddle to play with
I'm not sure what your objection to "mouse client coordinates" is, but it is very simple to do, and meets your requirement.
Add a click handler to the grid rectangle. I've used a click handler for this demo. But if you want to base it on hover, you could use the mousemove event instead.
<rect width="200%" height="200%" fill="url(#grid)" onClick={(e) => handleClick(e)}/>
function handleClick(e) {
var pos = getSVGPosition(e);
createRectAt(pos);
}
The function to convert the mouse coords to svg coords looks like this:
function getSVGPosition(e) {
var svg = e.nativeEvent.target.ownerSVGElement;
var pt = svg.createSVGPoint();
pt.x = e.nativeEvent.clientX;
pt.y = e.nativeEvent.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
return {svg: svg, x: pt.x, y: pt.y};
}
And then for the purposes of the demo, I create a square at the relevant grid position
function createRectAt(pos) {
var rect = document.createElementNS(pos.svg.namespaceURI, "rect");
rect.setAttribute("x", Math.floor(pos.x / 10) * 10);
rect.setAttribute("y", Math.floor(pos.y / 10) * 10);
rect.setAttribute("width", 10);
rect.setAttribute("height", 10);
rect.setAttribute("fill", "green");
pos.svg.appendChild(rect);
}
https://jsfiddle.net/tp530748/31/
If you make the grid a <pattern> then you will not be able to register any mouse events on the pattern. The patterns hitbox will cover the whole <rect>
I recommend calculating the coordinates of the squares yourself based on the size of the <svg>, the viewBox and the spacing of the grid. Attach any mouse events to the SVG element

Animate circle along sine curve

I am trying to replicate the visuals of f.lux using jQuery which looks like this (sorry for the poor quality). Basically, it is a circle following a sine wave path. I have looked at the jQuery.path animations and I tried replicating the animation, but my sine wave is choppy since it is drawn using CSS. Is there a way to draw the sine wave so it is smooth and then animate the circle following the sine wave path? I couldn't get a fiddle working but this is what my animation currently looks like now. My animation also does not loop back to where it starts unlike the f.lux animation.
I am not sure what the limits are for this either, will I need to make f.lux's background (the blue and pink halves of the sine wave) and then lay the animation on top of it or can I make the whole visual using Javascript, CSS, and jQuery?
Does anybody have any ideas or can anyone point me in the right direction?
To make your line smoother, you can use a svg element, like so :
<svg width="190" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M10 80 C 40 10, 65 20, 95 80" stroke="black" fill="#E0E0E0"/>
<path d="M95 80 C 105 100, 150 150, 180 80" stroke="black" fill="#EEEEEE"/>
</svg>
This uses two different bézier curves so you can use different colors for your background. You will have to change the values a bit to match you desired width/height and curvature. There is an explanation on svg paths here : https://developer.mozilla.org/en/docs/Web/SVG/Tutorial/Paths.
You can create an insert a svg element with javascript or jQuery SVG if you don't have access to the HTML source file.

how to explicitly draw svg viewbox coordinates

I have an svg viewbox element to draw waveform as follows:
<svg id="waveform" viewBox="0 -1 2000 2" preserveAspectRatio="none">
<g id="waveform">
<path id="waveform1" d="{{some data}}"/>
</g>
</svg>
It plots the waveform as expected. However it doesn't give me the x and y label of the viewbox, is there any alternate way by which we can add the x and y label of viewbox on webpage?
Your svg is rendering a single path, the waveform. If you want labeled axes to be drawn, you need to render them. For example, inspect the source of these SVG graphs with labeled axes: http://www.goat1000.com/svggraph-titles.php. You'll probably want to use a library to do this.
The svg element is not a "smart" graphing widget, it is a plain canvas.
The viewbox attribute on the svg element sets the coordinate system within which the content of the svg is drawn (its internal dimensions, as opposed to the actual pixel dimensions when it is rendered).

SVG Scaling and Coordinates

This is my first SVG project, and I’m not a programmer, but I dabble in interactive infographics. My previous experience in this area comes from working with ActionScript.
I’m using plain SVG (no Raphael, D3, etc.) and trying to create an interactive barchart. After some initial difficulty with the SVG coordinate system and scaling, I found some code online that handles the postscaling translation:
<text x="x_coord0" y="y_coord0" transform="scale(x_scale, y_scale) translate(-x_coord0*(x_scale-1)/x_scale, -y_coord0*(y_scale-1)/y_scale)" …>text</text>
And I converted it into this JavaScript:
var translationfactor = ((0 - y_position)*(y_scalefactor - 1) / y_scalefactor);
var matrix = "scale(1," + y_scalefactor + ") translate(0," + Number(translationfactor) + ")";
targetbar.setAttribute("transform", matrix);
The problem is that I need the bars “translated” back to the chart’s baseline, not the original locations of their topmost points. Currently the correctly scaled bars are hugging the top of the chart:
http://billgregg.net/miscellany/upsidedown-barchart.png
I’ve tried several fixes, including plugging the bars’ ”missing height” into translationfactor (the bars start out the full height of the chart and get scaled down dynamically). Nothing has worked. Part of my problem is that, besides being new to SVGs, I can stare at that code all day and my brain still can’t parse it. Multiplying negative numbers is too abstract and at a fundamental level I just don’t “get” the math, which of course makes modifying the code difficult.
My questions:
(1) What’s the fix for the code above to position the bars back on the baseline of the chart?
(2) Is there a more transparent, more pedestrian way of accomplishing the translation? My first thought along these lines was that if a bar’s height is reduced to 40% of its original value, then multiplying the original Y coordinate value by 250% should reset the bar to its original location (at least its topmost point), but that doesn’t seem to work.
(3) Is there a way to set a bar’s point of origin to its bottom? In Flash it’s possible, though as far as I know it’s a manual, not a programmatic task.
(4) Is there a method similar to .localToGlobal() in ActionScript that would allow me to avoid having to mess with the local coordinate system at all?
Behind the scenes there is matrix math going on and it can be hard to get your head around the pre and post multiplication of arrays.
It's not entirely clear what you are trying to achieve, but reading between the lines, it sounds like you are wanting to provide graph coordinates in their raw(ish) form and have the SVG scale and position them for you(?)
If that's the case, then I think the solution is simpler than what you think.
Assuming I'm right, we'll start with something that looks like this:
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<g transform="">
<rect x="0" width="1" height="5" fill="red"/>
<rect x="1" width="1" height="11" fill="green"/>
<rect x="2" width="1" height="12" fill="orange"/>
<rect x="3" width="1" height="8" fill="blue"/>
</g>
</svg>
Where x is obvious and the bar length is in height. y defaults to 0, so we don't need it here.
You basically want to know what goes in the transform to scale and position the bars on your page. The fact that your graph is "upside-down" helps a little. Because the origin in an SVG is at the top left.
First apply a scale. Let's make the bars 20 pixels wide, and scale the lengths up by 10.
<g transform="scale(20,10)">
Next you want to position the graph on the page. Let's put the top-left corner at (40,40).
In SVG the transformations are concatenated in order (post-multiplied) so in order for the translation to be what you specify and not be multiplied by the scale, you should put it first.
<g transform="translate(40,40) scale(20,10)">
So the final SVG looks like this:
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<g transform="translate(40,40) scale(20,10)">
<rect x="0" width="1" height="5" fill="red"/>
<rect x="1" width="1" height="11" fill="green"/>
<rect x="2" width="1" height="12" fill="orange"/>
<rect x="3" width="1" height="8" fill="blue"/>
</g>
</svg>
The above has been simplified by assuming you have already subtracted the values from your base 20%. If you wanted to keep the pure raw values, it's possible, but things get a bit trickier. You would need to either tinker with both the y and height value of each bar, or use clipping to hide the part of the bar above 20%.
For "right way up"/normal graphs. All you need to do is make the y scale negative and translate the graph so that the bottom-left is where you want it.
<g transform="translate(40,140) scale(20,-10)">
Hope this helps.

Categories