I am trying to scale/move an SVG path created with the Raphael api. I want the path to fit neatly within a container, no matter the size of the container. I have searched the reference, the web and I'm still struggling to get this to work.
If anyone can tell me why this isn't working, I would be very happy.
This fiddle shows you what I'm doing: http://jsfiddle.net/tolund/3XPxL/5/
JavaScript:
var draw = function(size) {
var $container = $("#container").empty();
$container.css("height",size+"px").css("width",size+"px");
var paper = Raphael("container");
var pathStr = "M11.166,23.963L22.359,17.5c1.43-0.824,1.43-2.175,"+
"0-3L11.166,8.037c-1.429-0.826-2.598-0.15-2.598,"+
"1.5v12.926C8.568,24.113,9.737,24.789,11.166,23.963z";
// set the viewbox to same size as container
paper.setViewBox(0, 0, $container.width(), $container.height(), true);
// create the path
var path = paper.path(pathStr)
.attr({ fill: "#000", "stroke-width": 0, "stroke-linejoin": "round", opacity: 1 });
// get the path outline box (may be different size than view box.
var box = path.getBBox();
// move the path as much to the top/left as possible within container
path.transform("t" + 0 + "," + 0);
// get the width/height based on path box relative to paper (same size as container)
var w = (paper.width) / box.width;
var h = (paper.height) / box.height;
// scale the path to the container (use "..." to compound transforms)
path.transform('...S' + w + ',' + h + ',0,0');
}
$(function() {
var currentSize = 30;
draw(currentSize);
$("#smaller").click(function(){
currentSize = currentSize < 10 ? currentSize : currentSize * 0.5;
draw(currentSize);
});
$("#bigger").click(function(){
currentSize = 300 < currentSize ? currentSize : currentSize * 2;
draw(currentSize);
});
});
HTML:
<button id="smaller">-</button>
<button id="bigger">+</button>
<div id="container" style="border: 1px #ddd solid; margin: 30px">
</div>
Thanks,
Torgeir.
I think your problem is a fundamental misunderstanding of what the viewbox is useful for. In your code, you're attempting to set the viewbox of the svg element so that it matches the coordinate space of the screen, and then transform the path to match that coordinate space. There's no technical reason you can't do this, but it does effectively take the "Scalable" out of "Scalable Vector Graphics." The entire point of the viewbox is to make the translation between the vector coordinate space and the screen relative.
The best way to solve your problem, then, is not to transform the path to match the SVG element, but to use the viewbox to let SVG's intrinsic scalability do this for you.
First things first: create your path so we have an object to work with. We don't care what the viewbox is at this point.
var pathStr = "..."; // The content of the path and its coordinates are completely immaterial
var path = paper.path(pathStr)
.attr({ fill: "#000", "stroke-width": 0, "stroke-linejoin": "round", opacity: 1 });
Now all we need to do is use the viewbox to "focus" the SVG on the coordinate space we're interested in.
var box = path.getBBox();
var margin = Math.max( box.width, box.height ) * 0.1; // because white space always looks nice ;-)
paper.setViewBox(box.x - margin, box.y - margin, box.width + margin * 2, box.height + margin * 2);
And that's it. The SVG (regardless of its size) will translate from the internal coordinates specified in the viewbox to its physical coordinates on screen.
Here's a fork of your fiddle as proof-of-concept.
Related
I need to position an svg path element at the click position
My example code is as follows
const svg = document.querySelector('svg');
const viewBox = svg.getAttribute('viewBox').split(' ');
const offset = svg.getBoundingClientRect();
svg.addEventListener('click', (e) => {
const width = svg.clientWidth;
const ratio = viewBox[2] / width;
const x = (e.pageX - offset.x) * ratio ;
const y = (e.pageY - offset.y) * ratio;
const tp = document.createElementNS('http://www.w3.org/2000/svg', 'path');
tp.setAttribute('fill', 'F7931E');
tp.setAttribute('stroke', '#000');
tp.setAttribute('d', 'M37,17v15H14V17z M50,0H0v50h50z');
tp.setAttribute('transform', `scale(1) translate(${x}, ${y })`);
svg.appendChild(tp);
});
DEMO
As you can see there is a lot to take into account in order to possition the path where you click. Now, I would like to scale the path
tp.setAttribute('transform', `scale(.3) translate(${x}, ${y})`);
This, unfortunately, changes everything; DEMO with scale = .3
So the question is, how can I compute the correct transform values in this case ?
Probably a bit related, as you can see, if scaling is off, the path is placed such that the mouse pointer is at the top-left corner. I would like to have it at its center. Any guidinance would be appreciated!
One final note, I'm looking for a solution which is generic, meaning it works for any shape
When I create text elements with Snap, say, the letter "a", the surrounding region that captures click events is far larger than the bounding box, by a scaling factor of at least 10. This means that if I create a second text element "b" that is close but visually separated from "a", the interaction regions have a large intersection, so that which element get clicked is mostly determined by which element is in an earlier/later layer.
Is it possible to shrink the interaction region to match the bounding box as closely as possible, so that layering doesn't affect which element's click event is triggered?
Here's a jsfiddle example, where the font size is initially set to 0.1px, which is on the order of magnitude I'm working with. (The size of the interaction area relative to the bounding box seems to increase as font size decreases.) Note that since "b" is created after "a", the only way to trigger "a" is clicking almost at the left edge of the canvas. What I was hoping to do is have a series of elements like these with similar spacing between them.
var s = Snap("#svg");
var fontsize = 0.1;
var make_text_box = function(string, fontsize, color) {
var text = s.text(0, 0, string);
text.attr({
'font-size': fontsize + 'px'
});
text.click(
function() {
text.attr({
fill: color
});
}
);
var bbx = text.getBBox().x;
var bby = text.getBBox().y;
var bbw = text.getBBox().w;
var bbh = text.getBBox().h;
var strokewidth = fontsize / 10;
var bbox = s.rect(bbx, bby, bbw, bbh)
.attr({
stroke: "black",
strokeWidth: strokewidth,
fill: "none"
});
var text_and_box = s.g(text, bbox);
return {
g: text_and_box,
x: bbx,
y: bby
};
};
var a_box = make_text_box("a", fontsize, "red");
var b_box = make_text_box("b", fontsize, "limegreen");
b_box.g.transform("translate(" + (2 * fontsize) + ", 0)");
s.attr({
viewBox: (a_box.x - 4 * fontsize) + ", " + (a_box.y - 4 * fontsize) + ", " + (30 * fontsize) + ", " + (10 * fontsize)
});
<script src="https://cdn.jsdelivr.net/snap.svg/0.1.0/snap.svg-min.js"></script>
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>
I have a feeling you may need a bit of a hacky workaround for this, as I don't know of a way to do what you want. I think there's several bugs out there, or maybe it's unclear what should happen with small font-sizes. Someone like Robert may have a better idea though.
If you leave the font-size to be bigger, but then scale down the element, you may have more success. So something like...
var fontsize = 10;
var text = s.text(0, 0, string);
text.attr({
'font-size': fontsize + 'px',
'transform': 's0.05'
});
jsfiddle
That's about as small as I can get it and still be able to see it and click in it. You may want to play a bit with shape-rendering and text-rendering as well to make it a bit sharper.
I'm making a game in Phaser using some large images that I want to scale down in the actual game:
create() {
//Create the sprite group and scale it down to 30%
this.pieces = this.add.group(undefined, "pieces", true);
this.pieces.scale.x = 0.3;
this.pieces.scale.y = 0.3;
//Add the players to the middle of the stage and add them to the 'pieces' group
var middle = new Phaser.Point( game.stage.width/2, game.stage.height/2);
var player_two = this.add.sprite(middle.x - 50, middle.y, 'image1', undefined, this.pieces);
var player_one = this.add.sprite(middle.x, middle.y-50, 'image2', undefined, this.pieces);
}
However because the sprites are scaled in size, their starting location is also scaled, so instead appearing in the middle of the stage, they appear only 30% of the distance to the middle.
How do I scale the sprite image without it affecting their location?
(The code is incidentally in Typescript but I think this particular sample is also javascript so that's probably irrelevant)
Set Sprite.anchor to 0.5 so the physics body is centered on the Sprite, scale the sprite image without it affecting their location.
this.image.anchor.setTo(0.5, 0.5);
Doc Phaser.Image
Anchor example
You can scale your sprite like so:
var scaleX = 2;
var scaleY = 2;
sprite.scale.set(scaleX, scaleY);
then calculate position of sprite:
var positionX = 100;
var positionY = 100;
sprite.x = positionX / scaleX;
sprite.y = positionY / scaleY;
Now your sprite is at position (100, 100).
The problem is that sprite.x got multiplied by with scaleX.
Regarding Phaser, I'd like to add that in the specific case of weapon.bullets or other groups you create yourself you're going to have to do it this way instead:
weapon.bullets.setAll('scale.x', 0.5);
weapon.bullets.setAll('scale.y', 0.5);
I got stuck on this and ended up in this thread, which is closed but in my case just not what I needed. Others will hopefully have some use out of this :)
im looking for an implementation of a card layout:
given a fixed width canvas, an arbitrary number of rectangles will be laid out, with some margins initially. fixed Y coordinate.
if the number of rectangles doesnt fit the canvas width, each rectangle will be partly overlayed by the next one. the last reactangle showing its whole width
wanted to have the layout auto adjust as I add a new rectangle
if there is such thing exist already or similar, pls share. was hoping i dont have to write it.
Like #puggsoy, I can't think of a library that does this, but here's a simple function that should do what you need.
function updateLayout(canvasWidth:Number, rects:Array, rectWidth:Number, margin:Number):void
{
var totalWidth:Number = rects.length * rectWidth + ((rects.length-1)*margin);
var offset:Number = rectWidth + margin;
if(totalWidth > canvasWidth)
{
offset = (canvasWidth - rectWidth) / (rects.length-1);
}
var currX:Number = 0;
for each(var rect:DisplayObject in rects)
{
rect.x = currX;
currX += offset;
}
}
This assumes that the DisplayObjects in the rects array are ordered by their childIndex.
I want to animate a path (actually a set of paths, but I'll get to that) along a curved path.
RaphaelJS 2 removed the animateAlong method, for reasons I haven't been able to discern. Digging into the Raphael documentation's gears demo as abstracted by Zevan, I have got this far:
//adding a custom attribute to Raphael
(function() {
Raphael.fn.addGuides = function() {
this.ca.guide = function(g) {
return {
guide: g
};
};
this.ca.along = function(percent) {
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "t" + [point.x, point.y]
};
return t;
};
};
})();
var paper = Raphael("container", 600, 600);
paper.addGuides();
// the paths
var circ1 = paper.circle(50, 150, 40);
var circ2 = paper.circle(150, 150, 40);
var circ3 = paper.circle(250, 150, 40);
var circ4 = paper.circle(350, 150, 40);
var arc1 = paper.path("M179,204c22.667-7,37,5,38,9").attr({'stroke-width': '2', 'stroke': 'red'});
// the animation
// works but not at the right place
circ3.attr({guide : arc1, along : 1})
.animate({along : 0}, 2000, "linear");
http://jsfiddle.net/hKGLG/4/
I want the third circle to animate along the red path. It is animating now, but at a distance from the red path equal to the third circle's original coordinates. The weird thing is that this happens whether the transform translate in the along object is relative (lowercase "t") or absolute (uppercase "T"). It also always animates in the same spot, even if I nudge it with a transform translation just before the animate call.
Any help very appreciated. I just got off the boat here in vector-land. Pointers are helpful--a working fiddle is even better.
You're just a hop, skip, and jump away from the functionality that you want. The confusion here concerns the interaction between transformations and object properties -- specifically, that transformations do not modify the original object properties. Translating simply adds to, rather than replaces, the original coordinates of your circles.
The solution is extremely straightforward. In your along method:
this.ca.along = function(percent) {
var box = this.getBBox( false ); // determine the fundamental location of the object before transformation occurs
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "...T" + [point.x - ( box.x + ( box.width / 2 ) ), point.y - ( box.y + ( box.height / 2 ) )] // subtract the center coordinates of the object from the translation offset at this point in the guide.
};
return t;
Obviously, there's some room for optimization here (i.e., it might make sense to create all your circles at 0,0 and then translate them to the display coordinates you want, avoiding a lot of iterative math). But it's functional... see here.
One other caveat: the ...T translation won't effect any other transforms that have already been applied to a given circle. This implementation is not guaranteed to play nicely with other transforms.