SVG Foreign Object sizing inconsistent - svg

I'm trying to make 2 html objects in SVGs and further use them inside Vis.js graphs. My first svg (Button) works as intended and looks good. My problem is that when I try to insert the table div the width/height are not what I have set them to be.
Here's what I get:
As you can see the button is larger than the red box even though the red box has a larger width and height (1000px x 800px versus 220px x 68px)!
Here's my JavaScript:
// THE RED BOX
const tableComponent =
`<svg xmlns="http://www.w3.org/2000/svg" width="1000px" height="800px">
<foreignObject x="0" y="0" width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="height: 100%; width: 100%; background-color: red;">
<p>Here is a paragraph that requires word wrap</p>
</div>
</foreignObject>
</svg>`;
// THE ORANGE BUTTON
const buttonComponent =
`<svg xmlns="http://www.w3.org/2000/svg" width="220px" height="68px">
<foreignObject x="0" y="0" width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="height: 100%; width: 100%;">
<button style="width: 100%; height: 100%; font-size: 20px; color: white; background-color: #FF9700; border-radius: 8px; border: none;">Order Filling</button>
</div>
</foreignObject>
</svg>`;
const tableComponentUrl = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(tableComponent);
const buttonComponentUrl = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(buttonComponent);
const nodes = new DataSet([
{ id: 1, image: buttonComponentUrl, shape: 'image' },
{ id: 2, image: tableComponentUrl, shape: 'image' },
{ id: 3, label: 'Node 3', shape: 'box' }
]);
// create an array with edges
const edges = new DataSet([
{ from: 1, to: 2, arrows: 'to' },
{ from: 1, to: 3, arrows: 'to' }
]);
// create a network
const container = document.querySelector('.data-source-container');
const data = {
nodes: nodes,
edges: edges
};
const options = {
edges: { smooth: false }
};
const network = new Network(container, data, options);

Well, SVG is scalable vector graphics, the inner sizes has nothing to do with the sizes a user sees (more accurately: x dimensions of objects that user sees are x_orig*scale where x_orig is the x dimension inside SVG and scale is a factor set by the x dimension of the whole SVG element).
In other words (assuming the other parts work and if they do, you have invented an interesting hack to extend vis.js' possibilities of inserting html) you have to set the dimensions of your pictures. Try to use size, scale or shapeProperties.useImageSize in corresponding nodes' options. I can't see an option to adjust aspect ratio, though, so this may require additional tweaking inside SVG itself (like setting its dimensions even).
Let me know how it goes, that's quite an interesting approach that you have.

Related

SVG elements to zoom whole SVG group on click or mouseover

I would like to use the circles within my SVG file to trigger a zoom in centred on the circle. I have got it working with a div acting as the trigger for the zoom but if I instead apply id="pin" to one of the circle elements within the SVG it no longer zooms in. Can anyone tell me why this is?
Is there a better way for me to achieve what I am trying to do? Ideally, I would like it to be possible to click to zoom and then to access other interactivity within the SVG while zoomed in.
If this is not possible is there a simple way to zoom and pan an SVG and to be able to access SVG interactivity while zoomed?
If I have missed something obvious please forgive me, I’m very much still learning the basics!
Rough example:
CodePen link
<div id="pin">click to trigger zoom</div>
<div class="map" id="mapFrame">
<svg class="image" id="mapSVG" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1920 1442.5"" style="
enable-background:new 0 0 1920 924.9;" xml:space="preserve">
<g id="Layer_7" data-name="Layer 7">
<image width="1800" height="1350" transform="translate(0) scale(1.069)" opacity="0.3"
xlink:href="https://media.npr.org/assets/img/2020/07/04/seamus-coronavirus-d3-world-map-20200323_wide-a3888a851b91a905e9ad054ea03e177e23620015.png" />
</g>
<g id="one">
<circle cx="929.664" cy="944.287" r="81.191"/>
</g>
<g id="two">
<circle cx="638.164" cy="456.863" r="81.191" />
</g>
<g id="three">
<circle cx="1266.164" cy="498.868" r="81.191" />
</g>
</svg>
</div>
<script src="app.js"></script>
svg {
width: 100%;
height: auto;
}
#pin {
position: absolute;
height: 65px;
width: 75px;
top: 300px;
left: 550px;
padding: 10px;
background-color: yellow;
}
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#pin');
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});
When you click on the circle, you are also clicking on the background image as well, triggering two events which is essentially cancelling the zoom. You can see this if you place alert('click 1'); and alert('click 2'); in your listeners.
This doesn't happen on the #pin element because it's outside background div and avoids the event bubbling up. This is solved by adding event.stopPropagation();
Code from your CodePen:
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#one'); //changed to #one
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
event.stopPropagation(); //added to prevent bubbling
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});

Fit text into svg without using js

It is necessary that the text fills the svg as much as possible as with bacground-size: contain. The svg parent can be resized dynamically. The text is always single line. Now such a solution is used using js, but it is desirable to do it using the svg itself.
const $svg = document.querySelector(`svg`);
const $text = $svg.querySelector('text');
const $input = document.querySelector('input');
$input.addEventListener('input', e => {
$text.textContent = e.target.value;
const box = $text.getBBox();
$svg.setAttribute('viewBox', `${box.x} ${box.y} ${box.width} ${box.height}`);
});
$input.dispatchEvent(new Event('input'))
svg {
border: 1px solid red;
height: 10em;
width: 100%;
}
<svg preserveAspectRatio="xMaxYMid meet">
<text alignment-baseline="baseline" dominant-baseline="middle" text-anchor="middle" fill="black"></text>
</svg>
<input type='text' value='Random single line text'>
Can you not just use a parent div? Add a no wrap attribute (word-break or flex-wrap for a flex container) to the text and that will stretch the div to full text width. Then make the svg width: 100% within the div.

How can I set an Overlay on top when I click on it

I have an Openlayers map with a lot of overlays (Point-coordinates).
These overlays are often very close to each other or overlapping.
When I click on an existing Overlay I want the Overlay to be set on top, so that it is fully seen, not behind any other Overlay.
So far I have only seen that the Layers can be set with an z-index. Is it possible to do that with overlays, too?
I would like to do something like that:
map.setLayerIndex(markers, 99);
but with an overlay
Overlays are controls, which are positioned on an coordinate instead of being in a fixed place. They are basically nothing more but regular html div elements and change position with the map.
This also means, you can apply normal CSS styling and use z-index on them.
var layer = new ol.layer.Tile({
source: new ol.source.OSM()
});
var map = new ol.Map({
layers: [layer],
target: 'map',
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
// Vienna marker
var marker1 = new ol.Overlay({
position: ol.proj.fromLonLat([16.3725, 48.208889]),
positioning: 'center-center',
element: document.getElementById('marker1'),
stopEvent: false,
className: 'm1 ol ol-overlay-container ol-selectable'
});
map.addOverlay(marker1);
marker2 = new ol.Overlay({
position: ol.proj.fromLonLat([23.3725, 48.208889]),
positioning: 'center-center',
element: document.getElementById('marker2'),
stopEvent: false,
className: 'm2 ol ol-overlay-container ol-selectable'
});
map.addOverlay(marker2);
function clicked(selector) {
console.log('clicked overlay', selector);
document.querySelectorAll(".ol").forEach(function(el){
el.classList.remove('active');
});
document.querySelector(selector).classList.add('active');
}
html, body, .map {
min-height: 50px;
min-width: 50px;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.marker {
width: 30px;
height: 30px;
border: 1px solid #088;
border-radius: 15px;
background-color: #0FF;
}
.m1 .marker {
background-color: #FF0;
}
.active {
z-index: 1234782904789;
}
.active .marker {
background-color: red;
}
<link href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#main/dist/en/v7.0.0/legacy/ol.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#main/dist/en/v7.0.0/legacy/ol.js"></script>
<div id="map" class="map"></div>
<div id="marker1" title="Marker" class="marker" onclick="clicked('.m1')"></div>
<div id="marker2" title="Marker" class="marker" onclick="clicked('.m2')"></div>
The existing answer works, but it doesn't preserve the z-order of the overlays, it only guarantees that the clicked one will be on top. Since it is the only element with a z-index in this stacking context, the z-order of the other elements will be random.
Here is a solution that brings the clicked overlay to the front, while preserving the current z-order of all the other ones:
export function bringToFront (map: PluggableMap, clickedOverlayElement: HTMLElement) {
const overlays = map.getOverlays().sort(zIndexComparator);
overlays.forEach((overlay, i) => {
const element = overlay.get('element');
const container = pointInfo.closest('.ol-overlay-container') as HTMLElement;
container.style.zIndex = element === clickedOverlayElement ? overlays.length.toFixed() : i.toFixed();
});
}
function getOverlayContainer (overlay: Overlay) {
return overlay.get('element').closest('.ol-overlay-container') as HTMLElement;
}
function zIndexComparator (a: Overlay, b: Overlay) {
return (getOverlayContainer(a).style.zIndex > getOverlayContainer(b).style.zIndex)
? 1
: -1;
}
Just call the bringToFront() function when your overlay element is clicked.

How do I make fabric.js output svg including alpha color?

I need to output the result of a fabric canvas drawing to an SVG file.
I'm using fabric.js version 1.7.6 and when I have a path drawn to a canvas with an rgba fill like rgba(255,0,0,.15) the resulting SVG has a fill of rgb(0,0,0). Is there some setting I need to enable to make it output the alpha chanel?
In my sample code the purple circle converts to SVG properly, but the rectangle just shows up as black.
Sample HTML:
<html>
<head>
<script src="fabric.js"></script>
</head>
<body>
<div id="canvasHolder" style="border: 3px solid black;">
<canvas id="canvasElement" width="400" height="400" />
</div>
<div id="svgHolder" style="border: 3px solid blue;">
</div>
</body>
<script>
var canvas = new fabric.Canvas('canvasElement');
var rect = new fabric.Path('M,0,0,h,100,v,100,h,-100,z',{
top:100,
left:100,
stroke: 'green',
fill: 'rgba(255,0,0,.15)'
});
canvas.add(rect);
var circ = new fabric.Circle({
radius: 30,
top:30,
left:30,
stroke: 'blue',
fill: 'purple'
});
canvas.add(circ);
canvas.renderAll();
// Make an SVG object out of the fabric canvas
var SVG = canvas.toSVG();
document.getElementById('svgHolder').innerHTML = SVG;
</script>
</html>
Output SVG:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="400" height="400" viewBox="0 0 400 400" xml:space="preserve">
<desc>Created with Fabric.js 1.7.6</desc>
<defs>
</defs>
<path d="M 0 0 h 100 v 100 h -100 z" style="stroke: rgb(0,128,0); stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform="translate(150.5 150.5) translate(-50, -50) " stroke-linecap="round"></path>
</svg>
As I said in comment, this looks like a bug, that you should report on the project's issue tracker.
Colors are all converted to rgb() (rgba, hsl, hsla, hex, keywords) and thus don't support alpha channel...
For the time being, here is an heavy workaround :
toSVG accepts an reviver function, which will receive all the svg nodes markups. From there, you can reapply the correct styles, but not so easily.
The only parameter I could find allowing us to identify which object corresponds to the svg markup we get, is the id one.
So first, we will construct a dictionary, which will store our colors, by id.
Then, we will assign the id and colors to our fabric's objects.
Finally, in the reviver, we will parse our markup to convert it to an svg node, check its id atribute, and then change its style.fill and style.stroke properties, before returning the serialization of this modified node.
var canvas = new fabric.Canvas('canvasElement');
var colors_dict = {};
// an helper function to generate and store our colors objects
function getColorId(fill, stroke) {
var id = 'c_' + Math.random() * 10e16;
return colors_dict[id] = {
id: id,
fill: fill || 'black',
stroke: stroke || 'black' // weirdly fabric doesn't support 'none'
}
}
// first ask for the color object of the rectangle
var rect_color = getColorId('hsla(120, 50%, 50%, .5)', 'rgba(0,0,255, .25)');
var rect = new fabric.Path('M,0,0,h,100,v,100,h,-100,z', {
top: 60,
left: 60,
stroke: rect_color.stroke, // set the required stroke
fill: rect_color.fill, // fill
id: rect_color.id // and most importantly, the id
});
canvas.add(rect);
var circ_color = getColorId('rgba(200, 0,200, .7)');
var circ = new fabric.Circle({
radius: 30,
top: 30,
left: 30,
stroke: circ_color.stroke,
fill: circ_color.fill,
id: circ_color.id
});
canvas.add(circ);
canvas.renderAll();
var parser = new DOMParser();
var serializer = new XMLSerializer();
function reviveColors(svg){
// first we parse the markup we get, and extract the node we're interested in
var svg_doc = parser.parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + svg + '</svg>', 'image/svg+xml');
var svg_node = svg_doc.documentElement.firstElementChild;
var id = svg_node.getAttribute('id');
if (id && id in colors_dict) { // is this one of the colored nodes
var col = colors_dict[id]; // get back our color object
svg_node.style.fill = col.fill; // reapply the correct styles
svg_node.style.stroke = col.stroke;
// return the new markup
return serializer.serializeToString(svg_node).replace('xmlns="http://www.w3.org/2000/svg', '');
}
return svg;
}
// Make an SVG object out of the fabric canvas
var SVG = canvas.toSVG(null, reviveColors);
document.getElementById('svgHolder').innerHTML = SVG;
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.12/fabric.js"></script>
<div id="canvasHolder" style="border: 3px solid black;">
<!-- beware canvas tag can't be self-closing -->
<canvas id="canvasElement" width="400" height="200"></canvas>
</div>
<div id="svgHolder" style="border: 3px solid blue;">
The colors are converted to RGB because svg specs wants color in CSS2 format and so rgba is unsupported.
cit: https://www.w3.org/TR/2008/REC-CSS2-20080411/syndata.html#value-def-color
Fabric fulfill the transparency with the fill-opacity rull. The point is that fabric.Color color parser looks like is choking over the .15 notation for the alpha channel.
Please use 0.15 and it will work.
i agree that fabric.Color could be fixed for this.

How to fill the screen with a div, then center it once the screen gets too big (max-height reached)?

Goal:
When the width and height of the window are both small, the div should be the same size as the window;
When the width of the window is too big (>max-width), the div should keep its width as max-width, and be horizontally centered.
When the height of the window is too big (>max-height), the div should keep its height as max-height, and be vertically centered.
The example below has achieved everything, except for the last point.
How to center this div vertically in the window? I.e., I want the red areas to behave like the green ones, but just vertically instead of horizontally.
(This design is intended for a responsive design for mobile devices' screens. No JS involvement if possible.)
<!doctype html>
<html>
<head>
<style>
body,html{
height:100%;
margin:0px;
background:green;
}
#t1{
position:relative;
height:100%;
max-width:640px;
margin:0 auto;
background-color:red;
}
#t1-1{
position:absolute;
height:100%;
max-height:640px;
width:100%;
background-color:#dddddd;
overflow:hidden;/*demo purpose*/
}
/*the following stuff are for demo only*/
img{
position:absolute;
opacity:0.5;
}
img.w{
width:100%;
}
img.h{
height:100%;
}
</style>
</head>
<body>
<div id="t1">
<div id="t1-1">
<img class="h" src="http://www.google.com/images/srpr/logo3w.png" />
<img class="w" src="http://www.google.com/images/srpr/logo3w.png" />
</div>
</div>
</body>
</html>
P.S. In this example, some desktop browsers internally set a min-width value to the whole thing (e.g. 400px in Chrome), unabling the div to keep shrinking horizontally.
You may need a little javascript to make it work:
First of all, you need an <div> element to layout, so I called it mask:
<div id="mask"></div>
Then, style it to fill the entire document, and give a max-width and max-height:
<style>
#mask {
position: fixed;
height: 100%;
max-height: 400px;
width: 100%;
max-width: 400px;
top: 0;
left: 0;
background: red;
}
</style>
This style do not perform the centering work, so you need your javascript to do it, we have a layoutMask function to determine if the div should be centered or not:
var mask = document.getElementById('mask');
function layoutMask() {
// here 400 is the same as the max-width style property
if (window.innerWidth >= 400) {
mask.style.left = '50%';
// to ensure centering, this sould be (max-width / 2)
mask.style.marginLeft = '-200px';
}
else {
mask.style.left = '';
mask.style.marginLeft = '';
}
// the same as width
if (window.innerHeight >= 400) {
mask.style.top = '50%';
mask.style.marginTop = '-200px';
}
else {
mask.style.top = '';
mask.style.marginTop = '';
}
}
At last, assign this function to the resize event, and execute immediately to ensure the <div> got layed correctly on first load:
if (window.addEventListener) {
window.addEventListener('resize', layoutMask);
}
else {
window.attachEvent('onresize', layoutMask);
}
layoutMask();
I tried this on my chrome, but I'm sure it does not work under IE6 since IE6 doesn't support the position: fixed; style, but it should work in most browsers.
I've made a jsfiddle for test.
As per my knowledge, with height:100% it is not possible. You need to use <center> to keep it in center horizontally and vertically. You may need to use margins also. Like:
margin-top:18%;
margin-left:40%;
You can add a #media query to achieve this effect
#media (min-height: 640px) {
#t1-1 {
top: 50%;
margin-top: -320px;
}
}
See JSFiddle for testing.

Resources