Related
I have a building floor map in SVG whith these size attributes:
width="594.75pt" height="841.5pt"
The size of the map, is in meters : 40x52.
What is the correct way to convert meters to points ?
Here is what I've tried so far :
XmlTextReader reader = new XmlTextReader(pathToSvg);
while (reader.Read() && string.IsNullOrEmpty(width) && string.IsNullOrEmpty(height))
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
if (reader.Name == #"svg")
{
width = reader.GetAttribute(#"width");
height = reader.GetAttribute(#"height");
}
break;
}
}
// Remove pt from width and height strings
width = width.Replace("pt", string.Empty);
height = height.Replace("pt", string.Empty);
// Convert to double values
double widthInPoint = double.Parse(width, CultureInfo.InvariantCulture);
double heightInPoint = double.Parse(height, CultureInfo.InvariantCulture);
// compute the ratio meters/points in both dimensions <=== Is this section right ??
// 594.75pt => 40 meters
// 1pt => X meters
double ratioX = mapHorizontalMeterSize / widthInPoint;
double ratioY = mapVerticalMeterSize / heightInPoint;
// Compute the Beacon position in points
double radiusInPoint = Math.Round(radius / ratioX, 2);
double beaconXPositionOnMapInPt = Math.Round((customMapX / ratioX) - (radius / ratioX), 2);
double yPos = Math.Round((customMapY / ratioY) - (radius / ratioY), 2);
// SVG positioning is top left corner by default, we are bottom left (originCornerId == 0)
double beaconYPositionOnMapInPt = originCornerId == 0 ? heightInPoint - yPos : yPos;
string tmpPath;
var reaaderSettings = new XmlReaderSettings();
reaaderSettings.DtdProcessing = DtdProcessing.Parse;
using (var svgReader = XmlReader.Create(path, reaaderSettings))
{
XDocument doc = XDocument.Load(svgReader);
var xmlns = doc.Root.GetDefaultNamespace();
var xlinkns = doc.Root.GetNamespaceOfPrefix("xlink");
// Add the circle
doc.Root.Add(new XElement(xmlns + "circle",
new XAttribute("stroke", "blue"),
new XAttribute("stroke-width", "3"),
new XAttribute("cx", $"{beaconXPositionOnMapInPt.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute("cy", $"{beaconYPositionOnMapInPt.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute("r", $"{radiusInPoint.ToString(CultureInfo.InvariantCulture)}"),
new XAttribute("fill-opacity", "0.1")
));
// Add the beacon image
//XNamespace xlinkns = "https://www.w3.org/1999/xlink";
doc.Root.Add(new XElement(xmlns + "image",
new XAttribute("x", $"{beaconXPositionOnMapInPt.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute("y", $"{yPos.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute(xlinkns + "href", $"data:image/{iconFormat};base64,{icon}")
));
tmpPath = FileHelpers.NextAvailableFilename(path);
doc.Save(tmpPath);
}
The result is absolutely not what I'm expecting.
The svg file is almost 3Mb in size and I can't show it here.
If you need to add new elements with dimensions specified in meters you could use a conversion helper function to find the right scaling multiplier/divisor
If your svg's dimensions are 594.8 × 841.5 user units
594.8 × 841.5 pt (real life print format - A4)
containing a map that is 40×52m in real life:
Meter to point multiplier: 2834.64388 (for converting meters to points/user units)
Scaling divisor: 40*2834.64388 / 594.8
Js example
I'm adding a rectangle at x/y=20m; width/height=10m; using my helper function
m2UserUnits('20m', scaleDivisor) that will convert meter to user units.
let svg = document.querySelector('svg');
let realWidth = '40m';
let userWidth = '594.8pt';
let m2PtMultiplier = 2834.64388;
let scaleDivisor = parseFloat(realWidth)*m2PtMultiplier / parseFloat(userWidth);
//append to svg
let ns ='http://www.w3.org/2000/svg';
let rect = document.createElementNS(ns, 'rect');
rect.setAttribute('x', m2UserUnits('20m', scaleDivisor) );
rect.setAttribute('y', m2UserUnits('20m', scaleDivisor) );
rect.setAttribute('width', m2UserUnits('10m', scaleDivisor) );
rect.setAttribute('height', m2UserUnits('10m', scaleDivisor) );
svg.appendChild(rect)
let circle = document.createElementNS(ns, 'circle');
circle.setAttribute('cx', m2UserUnits('5m', scaleDivisor) );
circle.setAttribute('cy', m2UserUnits('5m', scaleDivisor) );
circle.setAttribute('r', m2UserUnits('5m', scaleDivisor) );
svg.appendChild(circle)
//unit conversion
function m2UserUnits(val, scaleDivisor){
let valNum = parseFloat(val);
if(val.indexOf('m')!==-1){
valNum *= 2834.64388;
}
return valNum/scaleDivisor;
}
svg{
border: 1px solid red;
width:40%;
}
text{
font-size:32px
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 594.8 841.5" >
<rect id="bg-A4" fill="#FFFFFF" width="594.8" height="841.5"/>
<rect id="map-40x52m" fill="#EEEEEE" width="594.8" height="773.2"/>
<text x="50%" y="90%" text-anchor="middle">Map area: 40×52m (real life)</text>
<text x="50%" y="98%" text-anchor="middle">svg viewport: 594.8 × 841.5 user units</text>
</svg>
So I was spending some time playing around with pure (no external libraries) SVG elements dragging.
In general all works, but there is this nasty issue for fast moving mouse:
- when user mousedowns a draggable SVG element close to its edge
- then drags (mousemove) such draggable too fast
- the mouse "loses" the draggable
Here the issue is described in more details:
http://www.svgopen.org/2005/papers/AdvancedMouseEventModelForSVG-1/index.html#S3.2
Also here the author tried to fix UX by leveraging mouseout event:
http://nuclearprojects.com/blog/svg-click-and-drag-object-with-mouse-code/
I copied the above code snippet here: http://codepen.io/cmer41k/pen/zNGwpa
The question I have is:
Is there no other way (provided by pure SVG) to prevent such "loss" of SVG element while mouse moves too fast?
My attempt to solve this was:
- detect (somehow) that mouseout event happened without finishing the dragging.
- and if so (we sort of detected "disconnect") - reconnect the SVG element with current mouse position.
Is there a reason why this wouldn't work?
Code:
var click=false; // flag to indicate when shape has been clicked
var clickX, clickY; // stores cursor location upon first click
var moveX=0, moveY=0; // keeps track of overall transformation
var lastMoveX=0, lastMoveY=0; // stores previous transformation (move)
function mouseDown(evt){
evt.preventDefault(); // Needed for Firefox to allow dragging correctly
click=true;
clickX = evt.clientX;
clickY = evt.clientY;
evt.target.setAttribute("fill","green");
}
function move(evt){
evt.preventDefault();
if(click){
moveX = lastMoveX + ( evt.clientX – clickX );
moveY = lastMoveY + ( evt.clientY – clickY );
evt.target.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
}
}
function endMove(evt){
click=false;
lastMoveX = moveX;
lastMoveY = moveY;
evt.target.setAttribute("fill","gray");
}
The most important part of your code is missing, namely how or more specifically on which element you register the events.
What you basically do to prevent this problem is to register the mousemove and mouseup events on the outermost svg element, and not on the element you want to drag.
svg.addEventListener("mousemove", move)
svg.addEventListener("mouseup", endMove)
When starting the drag, register the events on the svg element, and when done unregister them.
svg.removeEventListener("mousemove", move)
svg.removeListener("mouseup", endMove)
you have to store the element you are currently dragging, so it is available in the other event handlers.
what i additionally do is to set pointer-events to "none" on the dragged
element so that you can react to mouse events underneath the dragged element (f.e. finding the drop target...)
evt.target.setAttribute("pointer-events", "none")
but don't forget to set it back to something sensible when dragging is done
evt.target.setAttribute("pointer-events", "all")
var click = false; // flag to indicate when shape has been clicked
var clickX, clickY; // stores cursor location upon first click
var moveX = 0,
moveY = 0; // keeps track of overall transformation
var lastMoveX = 0,
lastMoveY = 0; // stores previous transformation (move)
var currentTarget = null
function mouseDown(evt) {
evt.preventDefault(); // Needed for Firefox to allow dragging correctly
click = true;
clickX = evt.clientX;
clickY = evt.clientY;
evt.target.setAttribute("fill", "green");
// register move events on outermost SVG Element
currentTarget = evt.target
svg.addEventListener("mousemove", move)
svg.addEventListener("mouseup", endMove)
evt.target.setAttribute("pointer-events", "none")
}
function move(evt) {
evt.preventDefault();
if (click) {
moveX = lastMoveX + (evt.clientX - clickX);
moveY = lastMoveY + (evt.clientY - clickY);
currentTarget.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
}
}
function endMove(evt) {
click = false;
lastMoveX = moveX;
lastMoveY = moveY;
currentTarget.setAttribute("fill", "gray");
svg.removeEventListener("mousemove", move)
svg.removeEventListener("mouseup", endMove)
currentTarget.setAttribute("pointer-events", "all")
}
<svg id="svg" width="800" height="600" style="border: 1px solid black; background: #E0FFFF;">
<rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
<circle id="mycirc" cx="60" cy="60" r="22" onmousedown="mouseDown(evt)" />
</svg>
more advanced
there are still two things not so well with this code.
it does not work for viewBoxed SVGs nor for elements inside
transformed parents.
all the globals are bad coding practice.
here is how to fix those:
Nr. 1 is solved by converting mouse coordinates into local coordinates using the inverse of getScreenCTM (CTM = Current Transformation Matrix).
function globalToLocalCoords(x, y) {
var p = elem.ownerSVGElement.createSVGPoint()
var m = elem.parentNode.getScreenCTM()
p.x = x
p.y = y
return p.matrixTransform(m.inverse())
}
For nr. 2 see this implementation:
var dre = document.querySelectorAll(".draggable")
for (var i = 0; i < dre.length; i++) {
var o = new Draggable(dre[i])
}
function Draggable(elem) {
this.target = elem
this.clickPoint = this.target.ownerSVGElement.createSVGPoint()
this.lastMove = this.target.ownerSVGElement.createSVGPoint()
this.currentMove = this.target.ownerSVGElement.createSVGPoint()
this.target.addEventListener("mousedown", this)
this.handleEvent = function(evt) {
evt.preventDefault()
this.clickPoint = globalToLocalCoords(evt.clientX, evt.clientY)
this.target.classList.add("dragged")
this.target.setAttribute("pointer-events", "none")
this.target.ownerSVGElement.addEventListener("mousemove", this.move)
this.target.ownerSVGElement.addEventListener("mouseup", this.endMove)
}
this.move = function(evt) {
var p = globalToLocalCoords(evt.clientX, evt.clientY)
this.currentMove.x = this.lastMove.x + (p.x - this.clickPoint.x)
this.currentMove.y = this.lastMove.y + (p.y - this.clickPoint.y)
this.target.setAttribute("transform", "translate(" + this.currentMove.x + "," + this.currentMove.y + ")")
}.bind(this)
this.endMove = function(evt) {
this.lastMove.x = this.currentMove.x
this.lastMove.y = this.currentMove.y
this.target.classList.remove("dragged")
this.target.setAttribute("pointer-events", "all")
this.target.ownerSVGElement.removeEventListener("mousemove", this.move)
this.target.ownerSVGElement.removeEventListener("mouseup", this.endMove)
}.bind(this)
function globalToLocalCoords(x, y) {
var p = elem.ownerSVGElement.createSVGPoint()
var m = elem.parentNode.getScreenCTM()
p.x = x
p.y = y
return p.matrixTransform(m.inverse())
}
}
.dragged {
fill-opacity: 0.5;
stroke-width: 0.5px;
stroke: black;
stroke-dasharray: 1 1;
}
.draggable{cursor:move}
<svg id="svg" viewBox="0 0 800 600" style="border: 1px solid black; background: #E0FFFF;">
<rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
<circle class="draggable" id="mycirc" cx="60" cy="60" r="22" fill="blue" />
<g transform="rotate(45,175,75)">
<rect class="draggable" id="mycirc" x="160" y="60" width="30" height="30" fill="green" />
</g>
<g transform="translate(200 200) scale(2 2)">
<g class="draggable">
<circle cx="0" cy="0" r="30" fill="yellow"/>
<text text-anchor="middle" x="0" y="0" fill="red">I'm draggable</text>
</g>
</g>
</svg>
<div id="out"></div>
In a word game I am trying to draw score as white numbers above a blue (or red) rectangle:
For example, in the above screenshot it is the number "13".
Here is my entire class Score.js (with currently hardcoded WIDTH and HEIGHT):
"use strict";
function Score(color) {
PIXI.Container.call(this);
this.interactive = false;
this.buttonMode = false;
this.visible = false;
this.bgGraphics = new PIXI.Graphics();
this.bgGraphics.beginFill(color, 1);
this.bgGraphics.drawRect(0, 0, Score.WIDTH, Score.HEIGHT);
this.bgGraphics.endFill();
this.addChild(this.bgGraphics);
this.text = new PIXI.Text('XXX', {font: '20px Arial', fill: 0xFFFFFF});
this.text.x = 1;
this.text.y = 1;
this.addChild(this.text);
}
Score.prototype = Object.create(PIXI.Container.prototype);
Score.prototype.constructor = Score;
Score.WIDTH = 36;
Score.HEIGHT = 24;
Score.prototype.setText = function(str) {
this.text.text = str;
}
I wonder, how to modify my setText() function, so that a new rectangle is drawn on each call - as a bounding rectangle for the str argument?
I have looked at the PIXI.Text.getBounds() method, but it returns a Matrix and not a Rectangle...
I think you can just use this.text.width. This has historically had some bugs associated with it, but it should be working right in the latest version.
Is there a syntax that will allow for varied line heights (lineHeight) within one IText object? I've attempted to do this by adding lineHeight to the styles property, but it doesn't seem to be interpreting the different lineHeight.
styles: {
0: {
0: { textDecoration: 'underline', fontSize: 80, lineHeight: 1 },
1: { textBackgroundColor: 'red', lineHeight: 1 }
},
Rendered with a lineHeight = 1:
http://jsfiddle.net/xmfw65qg/44/
Rendered at lineHeight = 2.5:
http://jsfiddle.net/xmfw65qg/43/
Note, they render identically and do not interpret the line height. Is there a different way to do this?
Once there was an "unsupported feature" that allowed you to set the fontSize of the newline character to a big fontSize to emulate a higher lineHeight.
Now with code refactoring this procedure does not work anymore.
You can still
1) update to latest fabricjs version
2) override the _getLineHeight() the method as in the snippet
3) set in your style object a big fontSize for the last character in the line.
the override method consist to extend the for loop from "1 to < len" to "1 to = len" and nothing else.
var text = {"type":"i-text","originX":"left","originY":"top","left":1,"top":1,"width":230.05,"height":235.94,"fill":"#333","stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"text":"lorem ipsum\ndolor\nsit Amet\nconsectetur","fontSize":40,"fontWeight":"normal","fontFamily":"Helvetica","fontStyle":"","lineHeight":1.16,"textDecoration":"","textAlign":"left","textBackgroundColor":"","styles":{"0":{"0":{"fill":"red","fontSize":20,"fontFamily":"Helvetica","fontWeight":"normal","fontStyle":""},"1":{"fill":"red","fontSize":30,"fontFamily":"Helvetica","fontWeight":"normal","fontStyle":""},"2":{"fill":"red","fontSize":40,"fontFamily":"Helvetica","fontWeight":"normal","fontStyle":""},"3":{"fill":"red","fontSize":50,"fontFamily":"Helvetica","fontWeight":"normal","fontStyle":""},"4":{"fill":"red","fontSize":60,"fontFamily":"Helvetica","fontWeight":"normal","fontStyle":""},"6":{"textBackgroundColor":"yellow"},"7":{"textBackgroundColor":"yellow"},"8":{"textBackgroundColor":"yellow"},"9":{"textBackgroundColor":"yellow","fontFamily":"Helvetica","fontSize":40,"fontWeight":"normal","fontStyle":""},"11":{"fontSize":80}},"1":{"0":{"textDecoration":"underline"},"1":{"textDecoration":"underline","fontFamily":"Helvetica","fontSize":40,"fontWeight":"normal","fontStyle":""},"2":{"fill":"green","fontStyle":"italic","textDecoration":"underline"},"3":{"fill":"green","fontStyle":"italic","textDecoration":"underline"},"4":{"fill":"green","fontStyle":"italic","textDecoration":"underline","fontFamily":"Helvetica","fontSize":40,"fontWeight":"normal"}},"2":{"0":{"fill":"blue","fontWeight":"bold"},"1":{"fill":"blue","fontWeight":"bold"},"2":{"fill":"blue","fontWeight":"bold","fontFamily":"Helvetica","fontSize":40,"fontStyle":""},"4":{"fontFamily":"Courier","textDecoration":"line-through"},"5":{"fontFamily":"Courier","textDecoration":"line-through"},"6":{"fontFamily":"Courier","textDecoration":"line-through"},"7":{"fontFamily":"Courier","textDecoration":"line-through","fontSize":40,"fontWeight":"normal","fontStyle":""}, "8":{"fontSize":100}},"3":{"0":{"fontFamily":"Impact","fill":"#666","textDecoration":"line-through"},"1":{"fontFamily":"Impact","fill":"#666","textDecoration":"line-through"},"2":{"fontFamily":"Impact","fill":"#666","textDecoration":"line-through"},"3":{"fontFamily":"Impact","fill":"#666","textDecoration":"line-through"},"4":{"fontFamily":"Impact","fill":"#666","textDecoration":"line-through","fontSize":40,"fontWeight":"normal","fontStyle":""}}}};
fabric.IText.prototype._getHeightOfLine = function(ctx, lineIndex) {
if (this.__lineHeights[lineIndex]) {
return this.__lineHeights[lineIndex];
}
var line = this._textLines[lineIndex],
maxHeight = this._getHeightOfChar(ctx, lineIndex, 0);
for (var i = 1, len = line.length; i <= len; i++) {
var currentCharHeight = this._getHeightOfChar(ctx, lineIndex, i);
if (currentCharHeight > maxHeight) {
maxHeight = currentCharHeight;
}
}
this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult;
return this.__lineHeights[lineIndex];
};
var canvas = new fabric.Canvas('canvas');
canvas.add(new fabric.IText.fromObject(text));
<script src="http://www.deltalink.it/andreab/fabric/fabric.js" ></script>
<canvas id="canvas" width=500 height=400 style="height:500px;width:500px;"></canvas>
I'm drawing text labels in SVG. I have a fixed width available (say 200px). When the text is too long, how can I trim it ?
The ideal solution would also add ellipsis (...) where the text is cut. But I can also live without it.
Using d3 library
a wrapper function for overflowing text:
function wrap() {
var self = d3.select(this),
textLength = self.node().getComputedTextLength(),
text = self.text();
while (textLength > (width - 2 * padding) && text.length > 0) {
text = text.slice(0, -1);
self.text(text + '...');
textLength = self.node().getComputedTextLength();
}
}
usage:
text.append('tspan').text(function(d) { return d.name; }).each(wrap);
One way to do this is to use a textPath element, since all characters that fall off the path will be clipped away automatically. See the text-path examples from the SVG testsuite.
Another way is to use CSS3 text-overflow on svg text elements, an example here. Opera 11 supports that, but you'll likely find that the other browsers support it only on html elements at this time.
You can also measure the text strings and insert the ellipsis yourself with script, I'd suggest using the getSubStringLength method on the text element, increasing the nchars parameter until you find a length that is suitable.
Implementing Erik's 3rd suggestion I came up with something like this:
//places textString in textObj, adds an ellipsis if text can't fit in width
function placeTextWithEllipsis(textObj,textString,width){
textObj.textContent=textString;
//ellipsis is needed
if (textObj.getSubStringLength(0,textString.length)>=width){
for (var x=textString.length-3;x>0;x-=3){
if (textObj.getSubStringLength(0,x)<=width){
textObj.textContent=textString.substring(0,x)+"...";
return;
}
}
textObj.textContent="..."; //can't place at all
}
}
Seems to do the trick :)
#user2846569 show me how to do it ( yes, using d3.js ). But, I have to make some little changes to work:
function wrap( d ) {
var self = d3.select(this),
textLength = self.node().getComputedTextLength(),
text = self.text();
while ( ( textLength > self.attr('width') )&& text.length > 0) {
text = text.slice(0, -1);
self.text(text + '...');
textLength = self.node().getComputedTextLength();
}
}
svg.append('text')
.append('tspan')
.text(function(d) { return d; })
.attr('width', 200 )
.each( wrap );
The linearGradient element can be used to produce a pure SVG solution. This example fades out the truncated text (no ellipsis):
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="0" x2="200" y1="0" y2="0" id="truncateText">
<stop offset="90%" stop-opacity="1" />
<stop offset="100%" stop-opacity="0" />
</linearGradient>
<linearGradient id="truncateLegendText0" gradientTransform="translate(0)" xlink:href="#truncateText" />
<linearGradient id="truncateLegendText1" gradientTransform="translate(200)" xlink:href="#truncateText" />
</defs>
<text fill="url(#truncateLegendText0)" font-size="50" x="0" y="50">0123456789</text>
<text fill="url(#truncateLegendText1)" font-size="50" x="200" y="150">0123456789</text>
</svg>
(I had to use linear gradients to solve this because the SVG renderer I was using does not support the textPath solution.)
Try this one, I use this function in my chart library:
function textEllipsis(el, text, width) {
if (typeof el.getSubStringLength !== "undefined") {
el.textContent = text;
var len = text.length;
while (el.getSubStringLength(0, len--) > width) {}
el.textContent = text.slice(0, len) + "...";
} else if (typeof el.getComputedTextLength !== "undefined") {
while (el.getComputedTextLength() > width) {
text = text.slice(0,-1);
el.textContent = text + "...";
}
} else {
// the last fallback
while (el.getBBox().width > width) {
text = text.slice(0,-1);
// we need to update the textContent to update the boundary width
el.textContent = text + "...";
}
}
}
There is several variants using d3 and loops for search smaller text that fit. This can be achieved without loops and it work faster. textNode - d3 node.
clipText(textNode, maxWidth, postfix) {
const textWidth = textNode.getComputedTextLength();
if (textWidth > maxWidth) {
let text = textNode.textContent;
const newLength = Math.round(text.length * (1 - (textWidth - maxWidth) / textWidth));
text = text.substring(0, newLength);
textNode.textContent = text.trim() + postfix;
}
}
My approach was similar to OpherV's, but I tried doing this using JQuery
function getWidthOfText(text, fontSize, fontFamily) {
var span = $('<span></span>');
span.css({
'font-family': fontFamily,
'font-size' : fontSize
}).text(text);
$('body').append(span);
var w = span.width();
span.remove();
return w;
}
function getStringForSize(text, size, fontSize, fontFamily) {
var curSize = getWidthOfText(text, fontSize, fontFamily);
if(curSize > size)
{
var curText = text.substring(0,text.length-5) + '...';
return getStringForSize(curText, size, fontSize, fontFamily);
}
else
{
return text;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Now when calling getStringForSize('asdfasdfasdfasdfasdfasdf', 110, '13px','OpenSans-Light') you'll get "asdfasdfasdfasd..."