What would be a good solution for fitting text to a circle on a website, so it flows with the curves of the circle, instead of a rectangular bounding box?
Here's what I'm trying to achieve:
There are a number of black circles (of a fixed size) on a page, with a textarea next to each of them.
When text is entered into the textarea, it appears in the black circle, where it is centered on both axes.
If so much text is entered that line becomes longer than the radius of the circle, minus a specified value for margin, the line will break like you would expect from regular wrapping, with the block of text still being centered.
Lines nearer the top or bottom will, of course, be shorter than the ones near the middle.
The text will have a fixed size and when the circle is filled with text, the extra content should not be shown (like overflow hidden).
The black circles with the text are really speech bubbles, which are meant to be printed and glued onto a poster.
Do any of the fantastic SVG/Canvas libraries support this or will I have to figure our a method from scratch?
There is a proposed CSS feature call "exclusions" that would make it possible to flow text inside defined areas: http://www.w3.org/TR/css3-exclusions/
This means that SVG and Canvas paths would likely be defined as containers and text would flow/wrap inside the containers.
I did say "proposed" -- it's a ways off from being a reality in browsers.
However...
You can fairly easily wrap text inside a circle using html canvas
The width available to display text on any line changes as you move down the circle.
Here’s how to determine the maximum available width of any horizontal line on a circle
// var r is the radius of the circle
// var h is the distance from the top of the circle to the horizontal line you’ll put text on
var maxWidth=2*Math.sqrt(h*(2*r-h));
You fit text to the line by measuring the width of text—adding one word at a time, until you’ve used up all the available width of that line.
Here’s how to use canvas to measure any text using the current context.font:
var width=ctx.measureText(“This is some test text.”).width;
The rest is just adding text to each line up to the maximum line width and then starting a new line.
If you prefer SVG, you can do similar in SVG using the element.getComputedTextLength method for text metrics.
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/upq6L/
<!doctype html>
<html lang="en">
<head>
<style>
body{ background-color: ivory; padding:20px; }
canvas{ border:1px solid red;}
</style>
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script>
$(function() {
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var text = "'Twas the night before Christmas, when all through the house, Not a creature was stirring, not even a mouse. And so begins the story of the day of Christmas";
var font="12pt verdana";
var textHeight=15;
var lineHeight=textHeight+5;
var lines=[];
var cx=150;
var cy=150;
var r=100;
initLines();
wrapText();
ctx.beginPath();
ctx.arc(cx,cy,r,0,Math.PI*2,false);
ctx.closePath();
ctx.strokeStyle="skyblue";
ctx.lineWidth=2;
ctx.stroke();
// pre-calculate width of each horizontal chord of the circle
// This is the max width allowed for text
function initLines(){
for(var y=r*.90; y>-r; y-=lineHeight){
var h=Math.abs(r-y);
if(y-lineHeight<0){ h+=20; }
var length=2*Math.sqrt(h*(2*r-h));
if(length && length>10){
lines.push({ y:y, maxLength:length });
}
}
}
// draw text on each line of the circle
function wrapText(){
var i=0;
var words=text.split(" ");
while(i<lines.length && words.length>0){
line=lines[i++];
var lineData=calcAllowableWords(line.maxLength,words);
ctx.fillText(lineData.text, cx-lineData.width/2, cy-line.y+textHeight);
words.splice(0,lineData.count);
};
}
// calculate how many words will fit on a line
function calcAllowableWords(maxWidth,words){
var wordCount=0;
var testLine="";
var spacer="";
var fittedWidth=0;
var fittedText="";
ctx.font=font;
for(var i=0;i<words.length; i++){
testLine+=spacer+words[i];
spacer=" ";
var width=ctx.measureText(testLine).width;
if(width>maxWidth){
return({
count:i,
width:fittedWidth,
text:fittedText
});
}
fittedWidth=width;
fittedText=testLine;
}
}
}); // end $(function(){});
</script>
</head>
<body>
<p>Text wrapped and clipped inside a circle</p>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
Related
I have two overlayed rectangles:
I'm trying to fade them out uniformly as if they were one object.
The problem:
When I animate their opacity from 1 to 0, the top rectangle becomes transparent and reveals the edges of the rectangle beneath it.
Here is my code:
var paper = Raphael(50,50,250,350)
var rect = paper.rect (20,40,200,200).attr({"fill":"red","stroke":"black"})
var rect2 = paper.rect (100,140,200,200).attr({"fill":"red","stroke":"black"})
var set=paper.set()
set.push(rect)
set.push(rect2)
set.click(function () {fadeOut()})
function fadeOut() {
rect.animate({"opacity":0},3000)
rect2.animate({"opacity":0},3000)
setTimeout(function () {
rect.attr({"opacity":1})
rect2.attr({"opacity":1})
},3100)
}
When the set is clicked, the rectangles fade out in 3 seconds. (look at the red rectangles in my fiddle, it will clarify my problem)
https://jsfiddle.net/apoL5rfp/1/
In my fiddle I also create a similar looking green path that performs the fade out CORRECTLY.
I can I achieve the same type of fadeout with multiple objects?
I think this is quite difficult in Raphael alone.
Few options spring to mind. Don't use Raphael, use something like Snap, put them in a group and change opacity in the group.
var g = paper.g(rect, rect2);
g.click(function () { fadeOut( this )} )
function fadeOut( el ) {
el.animate({"opacity":0},3000)
setTimeout(function () {
el.attr({"opacity":1})
},3100)
}
jsfiddle
However, you may be tied to Raphael, which makes things a bit tricky, as it doesn't support groups. You could place an 'blank' object over it (which matches same as background) and animate its opacity in the opposite way, like this..(note the disabling of clicks on top object in css)
var rectBlank = paper.rect(18,20,250,330).attr({ fill: 'white', stroke: "white", opacity: 0 });
var set=paper.set()
....
rectBlank.animate({"opacity":1},3000)
jsfiddle
Otherwise I think you may need to use a filter, which may help a bit. SO question
Please could someone suggest how I can transform a loaded rectangle into a mask for an image? I want to be able to create rectangles my svg in illustrator - so the size and position is setup prior to loading the svg
I think this could work in the following steps.
Step1: Add jpeg image to svg
Step2: Group rect and jpeg
Step3: Turn rect into mask/clipping mask
Thanks
David
The code that you look for is element.attr({mask:maskelement});.
In this case, element is the image element, referable e.g. by its id in the page. As is maskelement the element that would be your rectangle. Keep in mind that the color of the rectangle has influence on the transparency of the masked part, white making it 0% transparent, black making it 100% transparent.
As a simple example:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Snap test</title>
<script src="snap/snap.svg.js"></script>
<script>
window.onload = function () {
var s = Snap(800, 600);
Snap.load("myexample.svg", function (f) {
var element = f.select("#myimage");
var maskelement = f.select("#mymask");
element.attr({mask:maskelement});
g = f.select("svg");
s.append(g);
});
};
</script>
</head>
</html>
Keep in mind that the SVG in this case contains the PNG and the masking rectangle. The PNG must have the id that you use in your code, as does the masking rectangle.
Edit: here you see the previous PNG:
And the SVG sourcecode can be found here.
I create a svg area. As things happen in the document, I want to display text strings in the svg area in xy coordinates within the svg area where I click.
How can I append a new text element to given xy coordinates in the svg area as these things happen?
var svg = document.getElementById("id-of-my-svg");
var text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", clickX);
text.setAttribute("y", clickY);
text.appendChild( document.createTextNode("Some text") );
svg.appendChild(text);
Demo here
I love KineticJS, its speed, marriage with GSAP, but what is making my head spin is there a way to freely transform KineticJS objects like the way in FabricJS? Here is the link reference to what I am trying to say: http://fabricjs.com/customization/
I don't want to use FabricJs as its really slow, and its low performance evident from various unit tests.
I am really looking forward to finding a way to be able to freely transform object in KineticJS as it would make life so much easier.
Is there a way to do it?
Thanks for your help,
Praney
Like markE said, this tutorial on Eric's (creator of KineticJS) tutorial site is the basis for all free transforming within KineticJS.
I'll go into detail about the actual free transform logic, there are 2 main functions:
function addAnchor(group, x, y, name) {
var stage = group.getStage();
var layer = group.getLayer();
//Create the anchor shape
var anchor = new Kinetic.Circle({
x: x,
y: y,
stroke: '#666',
fill: '#ddd',
strokeWidth: 2,
radius: 8,
name: name,
draggable: true,
dragOnTop: false
});
//Calls the update function which handles the transform logic
anchor.on('dragmove', function() {
update(this);
layer.draw();
});
//When the anchor is selected, we want to turn dragging off for the group
//This is so that only the anchor is draggable, and we can transform instead of drag
anchor.on('mousedown touchstart', function() {
group.setDraggable(false);
this.moveToTop();
});
//Turn back on draggable for the group
anchor.on('dragend', function() {
group.setDraggable(true);
layer.draw();
});
// add hover styling
anchor.on('mouseover', function() {
var layer = this.getLayer();
document.body.style.cursor = 'pointer';
this.setStrokeWidth(4);
layer.draw();
});
anchor.on('mouseout', function() {
var layer = this.getLayer();
document.body.style.cursor = 'default';
this.setStrokeWidth(2);
layer.draw();
});
group.add(anchor);
}
The addAnchor function, like the name says, adds a single anchor (or free transform handle) to a Kinetic.Group. By default it uses a Kinetic.Circle but really you can use any shape you want.
group - the group to add the anchor to
x - the x position of the anchor
y - the y position of the anchor
name - name of the anchor (usually describe which position the anchor represents, like topLeft or bottomRight
You'll notice a bunch of events attached to the newly created anchor, most of these are pretty straight-forward but the one you want to pay attention to is the dragmove event - this event is the one that calls the update function which handles all the logic for transforming the group/node. It's useful to note that the update function is called for every pixel that you are dragging an anchor.
The tutorial uses 4 corner anchors (thus calling addAnchor 4 times for each group/node), but if you wanted 8 anchors (4 corners - 4 sides) then you just have to adjust the logic to position the anchors correctly and to move the anchors properly when transforming.
By the way, the reason we're adding Anchors to a Group, is because we need them to group with the node in question, and stick with each node through dragging and transforming.
The second method is the update function:
function update(activeAnchor) {
var group = activeAnchor.getParent();
//Get each anchor inside the group, by name. Keep a standard set of names for every anchor you use and note they have to be names not ids because there will be multiple anchors named .topLeft in your app
var topLeft = group.get('.topLeft')[0];
var topRight = group.get('.topRight')[0];
var bottomRight = group.get('.bottomRight')[0];
var bottomLeft = group.get('.bottomLeft')[0];
var image = group.get('.image')[0];
var anchorX = activeAnchor.getX();
var anchorY = activeAnchor.getY();
// update anchor positions
switch (activeAnchor.getName()) {
case 'topLeft':
//When topLeft is being dragged, topRight has to update in the Y-axis
//And bottomLeft has to update in the X-axis
topRight.setY(anchorY);
bottomLeft.setX(anchorX);
break;
case 'topRight':
topLeft.setY(anchorY);
bottomRight.setX(anchorX);
break;
case 'bottomRight':
bottomLeft.setY(anchorY);
topRight.setX(anchorX);
break;
case 'bottomLeft':
bottomRight.setY(anchorY);
topLeft.setX(anchorX);
break;
}
image.setPosition(topLeft.getPosition());
//New height and width are calculated with a little math
//by calculating the distance between the update anchor positions.
var width = topRight.getX() - topLeft.getX();
var height = bottomLeft.getY() - topLeft.getY();
if(width && height) {
image.setSize(width, height);
}
}
The update function only takes one argument: the activeAnchor which is the anchor being dragged.
After that, it selects the other anchors within the group (using static names that you need to give each node and keep consistent throughout your app) so that we can translate their positions while the activeAnchor is being dragged.
The switch statement can get pretty large if you use 8 anchors instead of 4. This is because you need to consider translating almost all the other anchors while dragging one of them.
For an 8 anchor example: If you drag the topLeft anchor, you need to update the y position of the topRight anchor, the x position of the bottomLeft anchor, and for the topMid and leftMid anchors you need to adjust both the x,y values to stay in between the other anchors.
After updating the anchor position, the function handles the logic to resize the shape. Notice that the shape is selected by var image = group.get('.image')[0]; But, what you can do is use the get function to select by type and do something like:
var shape = group.get('Shape')[0];
Obviously this will work best if you just have 1 shape per group (to transform) + 4 or 8 anchors.
Let me know if you have any other questions or comments! Good luck!
this project adds a transfrom tool (resize) and some nice handlers which can be used as a basis:
https://github.com/soloproyectos/jquery.transformtool
Hi please see my previous answer about this. just search my name. hope it will be a bid help for you :) Transform (Move/Scale/Rotate) shapes with KineticJS
I forgot to include the html tags there so here it is... :)``
<!-- INSIDE THE HEAD TAGS -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- I am using jquery transform tool for kineticjs -->
<script type="text/javascript" src="../lib/jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="../lib/jquery.timer-1.0.3.js"></script>
<script type="text/javascript" src="../lib/kinetic-v4.7.4.min.js"></script>
<script type="text/javascript" src="../src/jquery.transformtool-1.0.2.js"></script>
<script type="text/javascript">
//PUT THE JAVSCRIPT SNIPPET HERE FROM THE LINK THAT I PROVIDED ABOVE
</script>
//THE ADD div element with id = canvas and file element with id = files and name files[] INSIDE THE BODY
I have a huge svg 3200*1800. I only want to show a part of that image something like 400*1000, ensuring that the width is the dominant attribute and having a scroll bar for the height but when I set viewbox it increase the width to display the added height.
viewBox="900 550 400 1000"
Is their a way to stop this happening?
I worked it out you need to increase the height relative to the viewbox for example I ended up with something like this:
width="1400"
height="4000"
viewBox="966 555 350 1000"
Compared to what I used to have:
width="350"
height="1000"
viewBox="966 555 350 1000"
You just set 'preserveAspectRatio' to "none" along with your 'viewBox' attribute, then your problem is solved.
This answer builds on Shane's answer (which does not cater to variable window sizes)...
To have width-dominant overflows:
Let the 'viewbox' define the portion of the graphic to display (any known aspect ratio)
Let the svg element have default width and height (100%)
With javascript, dynamically set the height of the svg element every time the window resizes
The code below works for my learning project and is NOT production code.
In the head element:
<script type="application/javascript">
var svgRatio = ${viewboxRatio}; // ratio must be known
// From http://stackoverflow.com/a/13651455
if(window.attachEvent) {
window.attachEvent('onresize', resizeSvg);
}
else if(window.addEventListener) {
window.addEventListener('resize', resizeSvg, true);
}
else {
//The browser does not support Javascript event binding
}
function resizeSvg() {
var height = window.innerWidth * svgRatio;
var svg = document.getElementsByTagName('svg')[0];
svg.setAttribute("height", height.toString());
}
</script>
At the end of the body:
<script type="application/javascript">
resizeSvg();
</script>