Change SVG <g/> Coordinates on Hover - javascript

I have array of x and y coordinates:
var coordinateX = [10,20,30,40,50,....];
var coordinateY = [10,20,30,40,50,....]:
Those coordinates are circle centers in which I want to show tooltip when hover over circle.
for (var i = 0; i < circles.length; i++) {
circles[i].addEventListener('mousemove', show);
circles[i].addEventListener('mouseout', hide);
}
Tooltip is represented as g element with rectangle and text inside it
<g id="poligon" visibility="hidden" class="element">
<rect width="80" height="20" fill="white"/>
<text class="tooltip" x="4" y="15" dominant-baseline="centered">Tooltip</text>
</g>
var poligon = svgDoc.getElementById("poligon")
function show(evt) {
poligon.setAttributeNS(null, "transform","translate(" + coordinateX[i] + ", " + coordinatey[i] + ")");
poligon.setAttributeNS(null, "visibility", "visible");
textTooltipVrijednost.data = evt.target.getAttributeNS(null, "class");
}
function hide(){
poligon.setAttributeNS(null, "visibility", "hidden");
}
I tried to translate g element using setAttribute and changing x, y values of translate attribute, but I got only last position value. How to do this dynamically for each position?

As I've commented a better idea would be using an html element as a tooltip. This way you won't have problems when the at small sizes of the svg element when the text may become so small that you can't read it.
The main idea is to detect the position of the mouse over the svg element. If the mouse enter a circle the tooltip becomes visible and takes the position of the mouse. Also in this case is displaying the coords of the circle. When the mouse leaves the circle the tooltip's display goes back to none.
I hope it helps.
const SVG_NS = 'http://www.w3.org/2000/svg';
let coordinates = [{cx:10,cy:7,r:5},{cx:10,cy:30,r:5},{cx:50,cy:10,r:5},{cx:33,cy:25,r:5}]
let circles = []
coordinates.forEach(c=>{
circles.push(drawCircle(c, svg))
})
circles.forEach((c,i)=>{
c.addEventListener("mouseenter",(e)=>{
let m = oMousePos(svg, e);
let cx = coordinates[i].cx;
let cy = coordinates[i].cy;
tooltip.style.display = "block"
tooltip.style.left = m.x+"px";
tooltip.style.top = m.y+"px";
tooltip.innerHTML = `${cx}, ${cy}`
})
c.addEventListener("mouseleave",(e)=>{
tooltip.style.display = "none"
})
})
function drawCircle(o, parent) {
var circle = document.createElementNS(SVG_NS, 'circle');
for (var name in o) {
if (o.hasOwnProperty(name)) {
circle.setAttributeNS(null, name, o[name]);
}
}
parent.appendChild(circle);
return circle;
}
function oMousePos(svg, evt) {
var ClientRect = svg.getBoundingClientRect();
return { //objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
svg {
border: 1px solid;
width:100vh;
}
#wrap {
position: relative;
}
#tooltip {
position: absolute;
width: 80px;
height: 30px;
background: white;
border: 1px solid;
text-align: center;
line-height: 30px;
top: 0;
left: 0;
display: none;
pointer-events: none;
}
<div id="wrap">
<svg id="svg" viewBox="0 0 60 40"></svg>
<div id="tooltip">0,0</div>
</div>

One solution is to just pass i into your show() function.
for (let i = 0; i < circles.length; i++) { // <-- change "var" to "let"
circles[i].addEventListener('mousemove', function() { show(i) });
circles[i].addEventListener('mouseout', hide);
}
function show(i) {
poligon.setAttributeNS(null, "transform","translate(" + coordinateX[i] + ", " + coordinatey[i] + ")");
poligon.setAttributeNS(null, "visibility", "visible");
textTooltipVrijednost.data = evt.target.getAttributeNS(null, "class");
}
var coordinateX = [10,20,30,40,50];
var coordinateY = [10,20,30,40,50];
var svgDoc = document.querySelector("svg");
var poligon = svgDoc.getElementById("poligon")
for (let i = 0; i < coordinateX.length; i++) {
var circle = document.createElementNS(svgDoc.namespaceURI, "circle");
circle.setAttribute("cx", coordinateX[i]);
circle.setAttribute("cy", coordinateY[i]);
circle.setAttribute("r", "5");
svgDoc.appendChild(circle);
circle.addEventListener('mousemove', function() { show(i); } );
circle.addEventListener('mouseout', hide);
}
function show(i) {
poligon.setAttribute("transform","translate(" + coordinateX[i] + ", " + coordinateY[i] + ")");
poligon.setAttribute("visibility", "visible");
}
function hide(){
poligon.setAttributeNS(null, "visibility", "hidden");
}
<svg width="400" height="400" viewBox="0 0 200 200">
<g id="poligon" visibility="hidden" class="element">
<rect width="80" height="20" fill="white"/>
<text class="tooltip" x="4" y="15" dominant-baseline="centered">Tooltip</text>
</g>
</svg>

Related

SVG Shape Lighting

Summary: I'm trying to create a canvas of randomized rock climbing holds using vector graphics generated with properties such as color, rotation, size and path values.
To add depth to these I'm trying to add a sort of randomized shadow to them to show that one or more sides of the shape are raised from the background.
Question:
I've been able to apply this filter onto the svg however as you can see on the below image "Light Filter" I get this white effect bleeding out to the edge of the svg element.
I'd like to find a way to keep that raised effect and have the color show or find a new way to show shadow randomized to each edge of the svg path?
You can find the filter code in the function: addFilter
You can disable the filter effect by commenting out the function addFilter(); and applyFilter();
No Filter:
Light Filter:
//create a filter for the svg copying the rough paper filter and apply it to the svg
var filter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
filter.setAttribute("id", "roughpaper");
filter.setAttribute("x", "0%");
filter.setAttribute("y", "0%");
filter.setAttribute("width", "100%");
filter.setAttribute("height", "100%");
var feDiffuseLighting = document.createElementNS("http://www.w3.org/2000/svg", "feDiffuseLighting");
feDiffuseLighting.setAttribute("in", "noise");
feDiffuseLighting.setAttribute("lighting-color", "#ffffff");
feDiffuseLighting.setAttribute("surfaceScale", "2");
var feDistantLight = document.createElementNS("http://www.w3.org/2000/svg", "feDistantLight");
feDistantLight.setAttribute("azimuth", "45");
feDistantLight.setAttribute("elevation", "60");
feDiffuseLighting.appendChild(feDistantLight);
filter.appendChild(feDiffuseLighting);
document.getElementById("svg-0").appendChild(filter);
body {
background-color: #333;
overflow: hidden;
}
#svg-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.svg-element {
position: absolute;
width: 150px;
height: 150px;
}
<div id="svg-container">
<svg
viewBox="0 0 200 200"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
class="svg-element"
id="svg-0"
filter="url(#roughpaper)"
>
<path
d="M27.8,-30.1C31.8,-29.8,27.8,-17,30.3,-5C32.9,7,42,18.2,38.3,19.7C34.5,21.1,18,12.7,4.6,21.4C-8.9,30.1,-19.3,55.9,-25.4,58.5C-31.5,61.2,-33.3,40.7,-44.1,24.4C-54.8,8,-74.4,-4.3,-75.5,-15.9C-76.6,-27.6,-59.1,-38.6,-43.4,-36.8C-27.7,-34.9,-13.9,-20.3,-1,-19.1C11.9,-17.9,23.9,-30.3,27.8,-30.1Z"
transform="translate(100, 100)"
class="path"
id="path-0"
style="fill: rgb(106, 76, 147)"
></path>
</svg>
</div>
Diffuse light effects should be multiplied with the original, otherwise you'll see the the lighting color rather than the combination of original color + lighting. So just add a feBlend with a multiply - like so. Update: and then add a feComposite/in to "clip to self" - so you don't see the background lit with the light as well.
//create an array with multiple svg paths
var svgPaths = [
{
path: "M27.8,-30.1C31.8,-29.8,27.8,-17,30.3,-5C32.9,7,42,18.2,38.3,19.7C34.5,21.1,18,12.7,4.6,21.4C-8.9,30.1,-19.3,55.9,-25.4,58.5C-31.5,61.2,-33.3,40.7,-44.1,24.4C-54.8,8,-74.4,-4.3,-75.5,-15.9C-76.6,-27.6,-59.1,-38.6,-43.4,-36.8C-27.7,-34.9,-13.9,-20.3,-1,-19.1C11.9,-17.9,23.9,-30.3,27.8,-30.1Z",
},
{
path: "M36.5,-45.3C49.8,-32.4,64.8,-23.2,69,-10.5C73.3,2.2,66.7,18.5,58.3,34.1C49.8,49.8,39.4,64.8,23.8,74.4C8.1,84,-12.7,88.2,-22.9,77.9C-33,67.7,-32.3,43,-35.4,26.1C-38.5,9.1,-45.3,0,-46.2,-10.5C-47.2,-21,-42.3,-32.7,-33.6,-46.4C-24.9,-60.1,-12.5,-75.7,-0.4,-75.2C11.6,-74.7,23.2,-58.1,36.5,-45.3Z",
}
];
var colors = ["#FF595E", "#FFCA3A", "#8AC926", "#1982C4", "#6A4C93"];
//create a function to apply properties to each svg element
function holdProps() {
//based on the id of the svg element, re-position the svg element on the screen
var svgElements = document.getElementsByClassName("svg-element");
//set the position of the svg element to the top left
for (var i = 0; i < svgElements.length; i++) {
svgElements[i].style.position = "absolute";
svgElements[i].style.top = "0";
svgElements[i].style.left = "0";
}
//console log the bounding box of each svg element
for (var i = 0; i < svgElements.length; i++) {
var svg = svgElements[i];
console.log(svg.getBoundingClientRect());
}
}
//create a function to apply properties to each svg elements path value
function holdPaths() {
//create a path and append it to each svg element
$(".svg-element").each(function () {
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
//set attributes to the above created path
path.setAttribute(
"d",
svgPaths[Math.floor(Math.random() * svgPaths.length)].path
);
path.setAttribute("transform", "translate(100, 100)");
path.setAttribute("class", "path");
path.setAttribute("id", "path-" + $(this).attr("id").split("svg-")[1]);
this.appendChild(path);
});
//If a path id is clicked change the path to a random path with a random color
$(".svg-element").click(function () {
var path = svgPaths[Math.floor(Math.random() * svgPaths.length)].path;
var color = colors[Math.floor(Math.random() * colors.length)];
$(this).find("path").attr("d", path);
$(this).find("path").css("fill", color);
});
}
//create a function to apply random colors to each svg element
function colorHold() {
$(".svg-element path").each(function () {
var color = colors[Math.floor(Math.random() * colors.length)];
$(this).css("fill", color);
//stroke white width 5 if the svg is hoverd over
$(this).hover(function () {
$(this).css("stroke", "white");
$(this).css("stroke-width", "5");
}
//reset stroke to black and stroke width to 1 if the svg is not hovered over
, function () {
$(this).css("stroke", "black");
$(this).css("stroke-width", "0");
}
);
});
}
//create feDistantLight and a fePointLight to each svg element
function addFilter() {
var filter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
filter.setAttribute("id", "roughpaper");
filter.setAttribute("x", "0%");
filter.setAttribute("y", "0%");
filter.setAttribute("width", "100%");
filter.setAttribute("height", "100%");
var feGauss= document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
feGauss.setAttribute("stdDeviation", "4");
feGauss.setAttribute("result", "blur");
filter.appendChild(feGauss);
var feDiffuseLighting = document.createElementNS("http://www.w3.org/2000/svg", "feDiffuseLighting");
feDiffuseLighting.setAttribute("in", "blur");
feDiffuseLighting.setAttribute("lighting-color", "#ffffff");
feDiffuseLighting.setAttribute("surfaceScale", "6");
var feDistantLight = document.createElementNS("http://www.w3.org/2000/svg", "feDistantLight");
feDistantLight.setAttribute("azimuth", "235");
feDistantLight.setAttribute("elevation", "50");
feDiffuseLighting.appendChild(feDistantLight);
filter.appendChild(feDiffuseLighting);
var feBlend= document.createElementNS("http://www.w3.org/2000/svg", "feBlend");
feBlend.setAttribute("mode", "multiply");
feBlend.setAttribute("in2", "SourceGraphic");
filter.appendChild(feBlend);
var feComp= document.createElementNS("http://www.w3.org/2000/svg", "feComposite");
feComp.setAttribute("operator", "in");
feComp.setAttribute("in2", "SourceGraphic");
filter.appendChild(feComp);
document.getElementsByTagName("svg")[0].appendChild(filter);
}
//apply the filter to each svg element
function applyFilter() {
var svgElements = document.getElementsByClassName("svg-element");
for (var i = 0; i < svgElements.length; i++) {
svgElements[i].setAttribute("filter", "url(#roughpaper)");
}
}
//on load run the functions
$(document).ready(function () {
//create a path for each svg element
holdPaths();
//apply properties to each svg element
holdProps();
//apply random colors to each svg element
colorHold();
//Disable these two to remove the filter
//create a filter to each svg element
addFilter();
//apply the filter to each svg element
applyFilter();
});
body {
background-color: #333;
overflow: hidden;
}
#svg-container {
/* full size of page*/
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.svg-element {
position: absolute;
}
svg path {
transition: 0.2s;
}
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<div id="svg-container">
<!-- Create SVG -->
<svg id="svg-01" class="svg-element" width="100%" height="100%" viewBox="0 0 200 200" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
</svg>
</div>

Programmatically change SVG classes during runtime

-I want to draw the same SVG onto a canvas multiple times, but each time I want to PROGRAMMATICALLY change the colors of specific classes within that SVG.
For example take this house image below:
The SVG for this House has the following classes:
<style>
.window-class {
fill: lime;
}
.door-class {
fill: blue;
}
.house-class {
fill: tan;
}
.roof-class {
fill: red;
}
</style>
How do I programmatically access these specific Style-Classes so I can change their color values for each new house that I draw?
I’m constructing the SVG by creating an Image object and then drawing that image onto a canvas using the following code:
// 1. Create the CANVAS and the CONTEXT:
var theCanvas = document.getElementById("theCanvas");
var theContext = theCanvas.getContext("2d");
theContext.fillStyle = "lightblue";
theContext.fillRect(0, 0, theCanvas.width, theCanvas.height);
// 2. Define the SVG data:
var imageData = '<svg id="HOUSE" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="240.26" height="311.24" viewBox="0 0 240.26 311.24"><defs><style>.house-class {fill: tan;}.roof-class {fill: red;}.roof-class, .window-class, .door-class {stroke: #000;stroke-miterlimit: 10;}.window-class {fill: lime;}.door-class {fill: blue;}</style></defs><g id="House"><rect class="house-class" x="30.08" y="131.74" width="173.07" height="179"/><path d="M270,242V420H98V242H270m1-1H97V421H271V241Z" transform="translate(-67.39 -109.76)"/></g><polygon id="Roof" class="roof-class" points="1.11 131.74 239.11 131.74 117.11 0.74 1.11 131.74"/><rect id="Window2" class="window-class" x="145.11" y="160.74" width="30" height="42"/><rect id="Window1" class="window-class" x="58.61" y="160.74" width="30" height="42"/><rect id="Door" class="door-class" x="92.11" y="228.74" width="52" height="82"/></svg>';
var DOMURL = window.URL || window.webkitURL || window;
var img = new Image();
var svg = new Blob([imageData], { type: 'image/svg+xml;charset=utf-8' });
var url = DOMURL.createObjectURL(svg);
img.onload = function () {
theContext.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
}
img.src = url;
Ordinarily I'd be able to get at the specific Classes who's colors I want to change by using this:
let nodeList = document.getElementsByClassName("window-class");
And then I would iterate through that nodeList and at each element I find that is styled with this window-class, I would do:
element.style.fill = -whatever-the-next-color-would-be-;
But since I'm creating my image in the manner I showed above, I'm not sure how I can get at specific classes of its SVG.
Any thoughts?
==============================
UPDATE:
It was pointed out that the code for drawing the image/SVG multiple times wasn't included, so here it is:
// GLOBAL VARIABLES:
const TOTAL_IMAGES = 3; // could be 30, or 300
const canvasWidth = 250;
const canvasHeight = 320;
var canvasX, canvasY = 0;
// COLOR VARIABLES:
var colorCounter = 0;
let houseColorsArray = ["fuchsia", "gold", "lighblue"]; // Will have lots more colors for all of these
let windowColorsArray = ["yellow", "pink", "lightgreen"];
let roofColorsArray = ["maroon", "crimson", "darkred"];
let doorColorsArray = ["darkBlue", "purple", "darkslategray"];
// CLASS-NAMES
let classNamesToPaintArray = [".house-class", ".door-class", ".window-class", ".roof-class"];
function designOneHouse(theCanvas) {
console.log("\n\n==========================\n=");
console.log("= =>>In 'designOneHouse()'!\n");
// 1. Create a Color-Scheme:
let houseColor = houseColorsArray[colorCounter];
let doorColor = doorColorsArray[colorCounter];
let windowColor = windowColorsArray[colorCounter];
let roofColor = roofColorsArray[colorCounter];
console.log(" ->Current 'houseColor' = ", houseColor);
console.log(" ->Current 'doorColor' = ", doorColor);
console.log(" ->Current 'windowColor' = ", windowColor);
console.log(" ->Current 'roofColor' = ", roofColor);
let context = theCanvas.getContext("2d");
// Iterate the ColorCounter - making sure we don't overflow the ColorsArrays:
colorCounter++;
if(colorCounter == houseColorsArray.length) {
colorCounter = 0;
}
// Now GET-AT and PAINT the Individual SVG Components.
// STRATEGY:
// 1. Iterate through the Array containing all the CLASS-NAMES who's color I want to change.
// 2. For each of these classes, I'll need to iterate through all the HTML elements that are OF that class type
// (there may be like 10 elements that are all styled by the same Style; I want all of them to be updated!)
//
for(classNameCounter = 0; classNameCounter < classNamesToPaintArray.length; classNameCounter++) {
let currentClassName = classNamesToPaintArray[classNameCounter];
console.log("currentClassName = " + currentClassName);
let nodeList = document.getElementsByClassName(currentClassName);
console.log("nodeList = " + nodeList);
console.log("nodeList LENGTH = " + nodeList.length);
for(var counter = 0; counter < nodeList.length; counter++) {
console.log("\n\n===>>IN FOR LOOP -- Node-COUNTER = " + counter);
let currentNode = nodeList[counter];
console.dir(" > 'childNodes[0]' of 'currentNode' = ");
console.dir(currentNode.childNodes[0]);
let elements = document.querySelectorAll(".door-class");
// Change the text of multiple elements with a loop
elements.forEach(element => {
element.style.fill = "pink";
});
}
}
}
function makeCanvasGrid() {
console.log("\n\n====>In 'makeCanvasGrid()'!\n");
for(var canvasCounter = 0; canvasCounter < TOTAL_IMAGES; canvasCounter++) {
console.log("\n >FOR LOOP - canvasCounter = " + canvasCounter);
// 1. Create a new Canvas Object:
let newCanvas = document.createElement("canvas");
newCanvas.setAttribute("width", canvasWidth);
newCanvas.setAttribute("height", canvasHeight);
newCanvas.setAttribute("id", "newCanvas" + canvasCounter);
// Log-out just to verify the "id" property was set correctly:
console.log(" >newCanvas.id = " + newCanvas.id);
// 2. Place the Canvas at (x,y) (top, left) coordinates:
newCanvas.style.position = "absolute";
newCanvas.style.left = canvasX; //"100px";
newCanvas.style.top = canvasY; //"100px";
designOneHouse(newCanvas);
// Check the current Canvas' (X, Y) coords, and if needed, reset X to 0 and SKIP to the next "ROW" of Canvasses:
if(canvasCounter > 0 && canvasCounter % 3 == 0) {
console.log(" >>NEXT ROW PLEASE!!!! canvasCount = ", canvasCounter);
canvasX = 0;
canvasY += canvasHeight + 20;
}
else {
canvasX += canvasWidth + 10;
}
}
}
makeCanvasGrid();
SO when I run this right now, the console shows me the nodeList is empty:
nodeList LENGTH = 0
So basically this statement isn't working:
let nodeList = document.getElementsByClassName(currentClassName);
To manipulate the house's DOM, the SVG has to be in the DOM. So I've wrapped the SVG in a <div> and hidden the div. I've put it way offscreen, but I could have hidden the div in several other ways.
Once you do that, your next problem is that you are changing the fill of the elements, but that will be overridded by the CSS in your SVG. So you have to remove those CSS styles.
Thirdly, you are creating canvas objects, but you are not attaching them to the DOM.
Also you are getting an error because canvasX isn't initialised. Plus CSS lengths must have units. So you need newCanvas.style.left = canvasX + "px" etc.
You were also looking up your elements wrongly. getElementsByClassName(".hose-class") won't find anything. It needed to be getElementsByClassName(".hose-class").
Finally, I've rewritten the element lookup and colour assignment code. I've bundled each colour scheme up into an array of colour scheme objects. It makes mapping classes to colours much simpler.
// GLOBAL VARIABLES:
const TOTAL_IMAGES = 3; // could be 30, or 300
const canvasWidth = 250;
const canvasHeight = 320;
var canvasX = 0, canvasY = 0;
// COLOR VARIABLES:
var colorCounter = 0;
let houseColorSchemes = [ {".house-class": "fuchsia",
".door-class": "darkblue",
".window-class": "yellow",
".roof-class": "maroon"},
{".house-class": "gold",
".door-class": "purple",
".window-class": "pink",
".roof-class": "crimson"},
{".house-class": "lightblue",
".door-class": "darkslategray",
".window-class": "lightgreen",
".roof-class": "darkred"} ];
// CLASS-NAMES
let classNamesToPaintArray = [".house-class", ".door-class", ".window-class", ".roof-class"];
// SVG template
let houseSVG = document.getElementById("HOUSE");
function designOneHouse(theCanvas) {
console.log("\n\n==========================\n=");
console.log("= =>>In 'designOneHouse()'!\n");
let context = theCanvas.getContext("2d");
// Now GET-AT and PAINT the Individual SVG Components.
// STRATEGY:
// 1. Iterate through the Array containing all the CLASS-NAMES who's color I want to change.
// 2. For each of these classes, I'll need to iterate through all the HTML elements that are OF that class type
// (there may be like 10 elements that are all styled by the same Style; I want all of them to be updated!)
//
let colorScheme = houseColorSchemes[colorCounter];
classNamesToPaintArray.forEach(className => {
let elements = houseSVG.querySelectorAll(className);
elements.forEach(element => element.style.fill = colorScheme[className]);
});
var imageData = houseSVG.outerHTML;
var DOMURL = window.URL || window.webkitURL || window;
var img = new Image();
var svg = new Blob([imageData], { type: 'image/svg+xml;charset=utf-8' });
var url = DOMURL.createObjectURL(svg);
img.onload = function () {
context.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
}
img.src = url;
// Iterate the ColorCounter - making sure we don't overflow the ColorsArrays:
colorCounter++;
if(colorCounter == houseColorSchemes.length) {
colorCounter = 0;
}
}
function makeCanvasGrid() {
console.log("\n\n====>In 'makeCanvasGrid()'!\n");
for(var canvasCounter = 0; canvasCounter < TOTAL_IMAGES; canvasCounter++) {
console.log("\n >FOR LOOP - canvasCounter = " + canvasCounter);
// 1. Create a new Canvas Object:
let newCanvas = document.createElement("canvas");
newCanvas.setAttribute("width", canvasWidth);
newCanvas.setAttribute("height", canvasHeight);
newCanvas.setAttribute("id", "newCanvas" + canvasCounter);
// Log-out just to verify the "id" property was set correctly:
console.log(" >newCanvas.id = " + newCanvas.id);
// 2. Place the Canvas at (x,y) (top, left) coordinates:
newCanvas.style.position = "absolute";
newCanvas.style.left = canvasX + "px"; //"100px";
newCanvas.style.top = canvasY + "px"; //"100px";
document.body.appendChild(newCanvas);
designOneHouse(newCanvas);
// Check the current Canvas' (X, Y) coords, and if needed, reset X to 0 and SKIP to the next "ROW" of Canvasses:
if(canvasCounter > 0 && canvasCounter % 3 == 0) {
console.log(" >>NEXT ROW PLEASE!!!! canvasCount = ", canvasCounter);
canvasX = 0;
canvasY += canvasHeight + 20;
}
else {
canvasX += canvasWidth + 10;
}
}
}
makeCanvasGrid();
#house-template {
position: absolute;
left: -1000px;
}
<div id="house-template">
<svg id="HOUSE" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="240.26" height="311.24" viewBox="0 0 240.26 311.24">
<defs>
<style>
.roof-class, .window-class, .door-class {stroke: #000;stroke-miterlimit: 10;}
</style>
</defs>
<g id="House">
<rect class="house-class" x="30.08" y="131.74" width="173.07" height="179"/>
<path d="M270,242V420H98V242H270m1-1H97V421H271V241Z" transform="translate(-67.39 -109.76)"/>
</g>
<polygon id="Roof" class="roof-class" points="1.11 131.74 239.11 131.74 117.11 0.74 1.11 131.74"/>
<rect id="Window2" class="window-class" x="145.11" y="160.74" width="30" height="42"/>
<rect id="Window1" class="window-class" x="58.61" y="160.74" width="30" height="42"/>
<rect id="Door" class="door-class" x="92.11" y="228.74" width="52" height="82"/>
</svg>
</div>
Below is one way to produce your desired result.
The approach below has the <svg> element in the HTML to be used as a template. That template is cloned, colors applied, converted into an Image and placed into the canvas for each house that has colors.
Note: the structure of the SVG changed. The class attributes are replaced by a custom data- attribute data-part that is used to apply the fill style through a normal CSS selector.
The coordinate positions of each house are in an array of space separated x y coordinates. The array also indicates how many houses are to be drawn
The colors for the house 'parts' are included in an Object that lists the house 'part' and its corresponding color (the count of colors should match the number of houses)
All <canvas> CSS is moved to the stylesheet.
I'll let you deal with sizing the image on the canvas.
const canvas = document.querySelector('canvas');
const context = canvas.getContext("2d");
const housePositions = ["0 10", "85 10", "170 10"];
const parts = {
House: ["fuchsia", "gold", "lightblue"],
Window: ["yellow", "pink", "lightgreen"],
Roof: ["maroon", "crimson", "darkred"],
Door: ["darkBlue", "purple", "darkslategray"]
};
function addHouse(colorIndex, x, y) {
let clonedSvgElement = document.querySelector('svg').cloneNode(true);
Object.keys(parts)
.forEach(part => {
clonedSvgElement.querySelectorAll(`[data-part=${part}]`)
.forEach(item => {
item.style.fill = parts[part][colorIndex];
});
const blob = new Blob([clonedSvgElement.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const blobURL = URL.createObjectURL(blob);
const image = new Image();
image.onload = () => {
context.drawImage(image, x, y, 130, 110);
URL.revokeObjectURL(this.src);
};
image.src = blobURL;
});
}
housePositions.forEach((coordString, index) => {
const [x, y] = coordString.split(' ');
addHouse(index, x, y);
});
canvas {
position: absolute;
left: 10px;
top: 10px;
width: 150px;
height: 80px;
border: 1px solid;
background-color: lightblue;
}
svg {
display: none;
}
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="index.css">
<title>Document</title>
<script defer src="index.js"></script>
</head>
<body>
<canvas></canvas>
<svg id="HOUSE" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="140" height="140" viewBox="0 0 240.26 311.24"><defs></defs><g id="House"><rect data-part="House" x="30.08" y="131.74" width="173.07" height="179"/><path d="M270,242V420H98V242H270m1-1H97V421H271V241Z" transform="translate(-67.39 -109.76)"/></g><polygon data-part="Roof" points="1.11 131.74 239.11 131.74 117.11 0.74 1.11 131.74"/><rect data-part="Window" x="145.11" y="160.74" width="30" height="42"/><rect data-part="Window" x="58.61" y="160.74" width="30" height="42"/><rect data-part="Door" x="92.11" y="228.74" width="52" height="82"/></svg>
</body>
</html>

How to take a screenshot of specific part of webpage just like Firefox screenshot not using HTML canvas?

I need to have a cursor to drag and take screenshot of dragged area on HTML webpage. I tried using HTML canvas but it takes screenshot of specific div not the selected region on HTML webpage.
The new html2canvas version 1 has width, height, x and y options.
You can make use of these options to achieve a cropping feature the Firefox's Screenshot's way.
document.onmousedown = startDrag;
document.onmouseup = endDrag;
document.onmousemove = expandDrag;
var dragging = false,
dragStart = {
x: 0,
y: 0
},
dragEnd = {
x: 0,
y: 0
};
function updateDragger() {
dragger.classList.add('visible');
var s = dragger.style;
s.top = Math.min(dragStart.y, dragEnd.y) + 'px';
s.left = Math.min(dragStart.x, dragEnd.x) + 'px';
s.height = Math.abs(dragStart.y - dragEnd.y) + 'px';
s.width = Math.abs(dragStart.x - dragEnd.x) + 'px';
}
function startDrag(evt) {
evt.preventDefault();
dragging = true;
dragStart.x = dragEnd.x = evt.clientX;
dragStart.y = dragEnd.y = evt.clientY;
updateDragger();
}
function expandDrag(evt) {
if (!dragging) return;
dragEnd.x = evt.clientX;
dragEnd.y = evt.clientY;
updateDragger();
}
function endDrag(evt) {
dragging = false;
dragger.classList.remove('visible');
// here is the important part
html2canvas(document.body, {
width: Math.abs(dragStart.x - dragEnd.x),
height: Math.abs(dragStart.y - dragEnd.y),
x: Math.min(dragStart.x, dragEnd.x),
y: Math.min(dragStart.y, dragEnd.y)
})
.then(function(c) {
document.body.appendChild(c);
});
dragStart.x = dragStart.y = dragEnd.x = dragEnd.y = 0;
}
* {
user-select: none;
}
#dragger {
position: fixed;
background: rgba(0, 0, 0, .5);
border: 1px dashed white;
pointer-events: none;
display: none;
}
#dragger.visible {
display: block;
}
canvas {
border: 1px solid;
}
<script src="https://github.com/niklasvh/html2canvas/releases/download/v1.0.0-alpha.1/html2canvas.js"></script>
<div id="wrapper">
<p> Drag to take a screenshot ...</p>
<img crossOrigin src="https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png" width="120" height="120">
</div>
<div id="dragger" tabindex></div>

D3 JS - Making a Polygon Draggable using hard coded Bounding Box Attributes Does Not Work

I draw a Polygon using D3 mouse events as shown in this fiddle.
Below is the method that get's the polygon's bounding box and sets the polygon's bounding box properties.
function completePolygon() {
d3.select('g.outline').remove();
gPoly = svgCanvas.append('g')
.classed("polygon", true);
polyPoints.splice(polyPoints.length - 1);
polyEl = gPoly.append("polygon")
.attr("points", polyPoints);
for (var i = 0; i < polyPoints.length; i++) {
gPoly.append('circle')
.attr("cx", polyPoints[i][0])
.attr("cy", polyPoints[i][1])
.attr("r", 4)
.call(dragBehavior);
}
isDrawing = false;
isDragging = true;
bbox = polyEl._groups[0][0].getBBox();
var bbox2 = gPoly._groups[0][0].getBBox();
//Altering the bounding box's attribute of polygon
bbox.x = 0;
bbox.y = 0;
bbox.width = 50;
bbox.height = 50;
gPoly.attr("transform", "translate(" + 0 + "," + 0 + ")");
// polyEL.attr("transform", "translate(" + 0 + "," + 0 + ")");
//
// gPoly.call(d3.drag().on("drag", movePolygon(bbox)));
}
I want to make the entire polygon draggable. I tried getting the Bounding Box of the drawn Polygon and setting the X and Y coordinates to 0 then translating it on drag like I did for the circle and rectangle elements in this fiddle but changing any of the polygon's bounding box properties don't seem to have an affect on the polygon element. However translating for the polygon works.
Is there any other way other than looping through the polygon's 2 dimensional array of coordinates and updating all the coordinate points on to implement a draggable polygon?
I'm really not following all this getBBox() stuff. Why don't you drag the element using the traditional way?
gPoly.call(d3.drag().on("drag", function(d) {
d3.select(this).attr("transform", "translate(" +
(d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")")
}));
Here is your code with that change:
d3.select('#poly').on('click', function() {
new Polygon();
});
var w = 600,
h = 500;
var svgCanvas = d3.select('body').append('svg').attr("width", w).attr("height", h);
function Polygon() {
var polyPoints = [];
var gContainer = svgCanvas.append('g').classed("outline", true);
var isDrawing = false;
var isDragging = false;
var linePoint1, linePoint2;
var startPoint;
var bbox;
var boundingRect;
var shape;
var gPoly;
var polyDraw = svgCanvas.on("mousedown", setPoints)
.on("mousemove", drawline)
.on("mouseup", decidePoly);
var dragBehavior = d3.drag().on("drag", alterPolygon);
// var dragPolygon = d3.drag().on("drag", movePolygon(bbox));
//On mousedown - setting points for the polygon
function setPoints() {
if (isDragging) return;
isDrawing = true;
var plod = d3.mouse(this);
linePoint1 = {
x: plod[0],
y: plod[1]
};
polyPoints.push(plod);
var circlePoint = gContainer.append("circle")
.attr("cx", linePoint1.x)
.attr("cy", linePoint1.y)
.attr("r", 4)
.attr("start-point", true)
.classed("handle", true)
.style("cursor", "pointer");
// on setting points if mousedown on a handle
if (d3.event.target.hasAttribute("handle")) {
completePolygon()
}
}
//on mousemove - appending SVG line elements to the points
function drawline() {
if (isDrawing) {
linePoint2 = d3.mouse(this);
gContainer.select('line').remove();
gContainer.append('line')
.attr("x1", linePoint1.x)
.attr("y1", linePoint1.y)
.attr("x2", linePoint2[0] - 2) //arbitary value must be substracted due to circle cursor hover not working
.attr("y2", linePoint2[1] - 2); // arbitary values must be tested
}
}
//On mouseup - Removing the placeholder SVG lines and adding polyline
function decidePoly() {
gContainer.select('line').remove();
gContainer.select('polyline').remove();
var polyline = gContainer.append('polyline').attr('points', polyPoints);
gContainer.selectAll('circle').remove();
for (var i = 0; i < polyPoints.length; i++) {
var circlePoint = gContainer.append('circle')
.attr('cx', polyPoints[i][0])
.attr('cy', polyPoints[i][1])
.attr('r', 4)
.attr("handle", true)
.classed("handle", true);
}
}
//Called on mousedown if mousedown point if a polygon handle
function completePolygon() {
d3.select('g.outline').remove();
gPoly = svgCanvas.append('g')
.classed("polygon", true);
polyPoints.splice(polyPoints.length - 1);
//console.log(polyPoints);
polyEl = gPoly.append("polygon")
.attr("points", polyPoints);
for (var i = 0; i < polyPoints.length; i++) {
gPoly.append('circle')
.attr("cx", polyPoints[i][0])
.attr("cy", polyPoints[i][1])
.attr("r", 4)
.call(dragBehavior);
}
isDrawing = false;
isDragging = true;
bbox = polyEl._groups[0][0].getBBox();
var bbox2 = gPoly._groups[0][0].getBBox();
bbox.x = 0;
bbox.y = 0;
bbox.width = 50;
bbox.height = 50;
// debugger;
gPoly.datum({
x: 0,
y: 0
})
//console.log(bbox);
gPoly.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"
});
// polyEL.attr("transform", "translate(" + 0 + "," + 0 + ")");
//
gPoly.call(d3.drag().on("drag", function(d) {
d3.select(this).attr("transform", "translate(" + (d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")")
}));
}
//Altering polygon coordinates based on handle drag
function alterPolygon() {
if (isDrawing === true) return;
var alteredPoints = [];
var selectedP = d3.select(this);
var parentNode = d3.select(this.parentNode);
//select only the elements belonging to the parent <g> of the selected circle
var circles = d3.select(this.parentNode).selectAll('circle');
var polygon = d3.select(this.parentNode).select('polygon');
var pointCX = d3.event.x;
var pointCY = d3.event.y;
//rendering selected circle on drag
selectedP.attr("cx", pointCX).attr("cy", pointCY);
//loop through the group of circle handles attatched to the polygon and push to new array
for (var i = 0; i < polyPoints.length; i++) {
var circleCoord = d3.select(circles._groups[0][i]);
var pointCoord = [circleCoord.attr("cx"), circleCoord.attr("cy")];
alteredPoints[i] = pointCoord;
}
//re-rendering polygon attributes to fit the handles
polygon.attr("points", alteredPoints);
bbox = parentNode._groups[0][0].getBBox();
console.log(bbox);
}
function movePolygon() {
}
function prepareTransform(bboxVal) {
var originalPosition = {
x: bboxVal.x,
y: bboxVal.y
};
console.log(bboxVal);
console.log(bbox);
bbox.x = 0;
bbox.y = 0;
// //render a bounding box
// shape.rectEl.attr("x", bbox.x).attr("y", bbox.y).attr("height", bbox.height).attr("width", bbox.width);
//
// //drag points
// shape.pointEl1.attr("cx", bbox.x).attr("cy", bbox.y).attr("r", 4);
// shape.pointEl2.attr("cx", (bbox.x + bbox.width)).attr("cy", (bbox.y + bbox.height)).attr("r", 4);
// shape.pointEl3.attr("cx", bbox.x + bbox.width).attr("cy", bbox.y).attr("r", 4);
// shape.pointEl4.attr("cx", bbox.x).attr("cy", bbox.y + bbox.height).attr("r", 4);
return originalPosition;
}
}
h1 {
text-align: center;
}
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 60%;
/* depends on svg ratio, for my zebra height/width = 1.2 so padding-bottom = 50% * 1.2 = 60% */
vertical-align: middle;
/* top | middle | bottom ... do what you want */
}
.my-svg {
/* svg into : object, img or inline */
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
/* only required for <img /> */
z-index: 0;
}
svg {
border: solid 1px rgba(221, 61, 16, 0.71);
}
.rectangle {
fill: lightblue;
stroke: blue;
stroke-width: 2px;
fill-opacity: 0.5;
}
.rectangle-bind {
fill: none;
stroke: #081c4e;
stroke-width: 1px;
stroke-dasharray: 5;
}
circle {
fill: lightgreen;
stroke: green;
stroke-width: 2px;
fill-opacity: 0.5;
}
.rect-active {
fill: #1578db;
}
.pointC-active {
fill: #FF8F00;
}
.circle-active {
fill: #4CAF50;
}
path {
fill: #ffb7b3;
stroke: #ff4736;
stroke-width: 5px;
}
polygon {
fill: #b6eeff;
fill-opacity: 0.5;
stroke: #0067ff;
stroke-width: 2px;
}
line {
fill: none;
stroke: #cd08ff;
stroke-width: 2px;
}
polyline {
fill: none;
stroke: #563aff;
stroke-width: 2px;
}
circle.handle {
fill: yellow;
stroke: #cb9c0f;
stroke-width: 2px;
cursor: pointer;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button id='poly'>Poly</button>
PS: Don't use _groups[0][0]. Use node() instead.

D3.js: How can I put an image on the background of a circle with the remaining parts trimmed and make it resize along with the circle onclick?

I have the following code http://codepen.io/juanf03/pen/ZBYRNK
and I need to put a profile pic on the background, how can I do if the image comes as a squared image and I want to put it within the SVG circle? How do I do to resize the image along with the circle and the text on the click event handler? Do I have to make use add it to the g "elementGroup" I created like I did with the text and the circle? I ask this because of the resolution cause the image itself will not be an SVG but a raster.
//listener that will be executed on setIntervalCheck to update the graphic
setInterval(function(){
console.log(dataset);
moveForwardOnBubbleList();
createElementGroup();
}, 5000);
var profile_pic_url="https://scontent.fsst1-2.fna.fbcdn.net/v/t1.0-9/13680856_103268503450198_1479797031996897052_n.jpg?oh=f43bced91822fb210c8be8a410825da9&oe=58D46460";
var dataset = [{unique_followers: 5, profile_pic:profile_pic_url}, {unique_followers: 10, profile_pic:profile_pic_url},{ unique_followers: 15, profile_pic:profile_pic_url}, { unique_followers: 20, profile_pic:profile_pic_url}, { unique_followers: 25, profile_pic:profile_pic_url}, {unique_followers: 40, profile_pic:profile_pic_url} ];
var w=900,h=600;
var svg=d3.select("body").append("svg")
.attr("width",w)
.attr("height",h);
//1st level:All circles group
var circlesGroup = svg.append("g").classed("general-group",true);
//2nd level: Group of circle and text
var elementGroup;
var circle;
var circleAttributes;
//create g's of existing data
createElementGroup();
elementGroup.on('click', function(d,i){
var that=this;
d3.selectAll('.element-group').each(function(d,i) {
if(this.id!==that.id){
d3.select(this).classed("selected",false);
}
});
d3.select(this).classed("selected", !d3.select(this).classed("selected"));
});
//adding circular background image to the circles
//var circlesSelection=svg.selectAll('circle');
function createElementGroup(){
elementGroup = circlesGroup
.selectAll('.element-group')
.data(dataset, function(d){ return d.unique_followers});
//doesn't work the exit transition
var elementExit = elementGroup.exit().transition().duration(1000).style("opacity", 0).remove();
var elementEnter = elementGroup.enter()
.append("g").classed("element-group",true).style("opacity",0);
elementEnter.merge(elementGroup).attr("transform", function(d,i){
//option 1 generation by mod
if(i%2===0){
return "translate(" + (i*80+45) + "," + h/1.55 + ")";
}else{
return "translate(" + (i*80+45) + "," + h/1.45 + ")";
}
/*
//option 2 random
var random= (Math.random() * (1.6 - 1.45) + 1.45).toFixed(4);
return "translate(" + (i*80+45) + "," + h/random + ")";*/
}).transition().duration(2000).style("opacity", 1.0);
circle=elementEnter.append('circle');
circleAttributes = circle
.attr("r", 20)
.attr("stroke","black")
.attr("fill", "white")
.classed("circle",true);
//text to show
var texts = elementEnter.append("text")
.attr("text-anchor", "middle")
.text(function(d) {
return parseInt(d.unique_followers);
})
.style("pointer-events","none")
.classed('tweet-number', true);
//image to show as background
//element group positioning for the text to be inside circle
}
function addBubbleLast(){
var random=Math.floor(Math.random() * (40)) + 1;
dataset.push({unique_followers: random, profile_pic:profile_pic_url});
}
function removeFirstBubble(){
dataset.shift();
}
function moveForwardOnBubbleList(){
addBubbleLast();
removeFirstBubble();
}
//css
body
{
/*padding-top: 50px;*/
padding-left: 100px;
}
.tweet-number{
opacity:0.25;
}
.circle{
}
.selected *{
transform: scale(2);
transition: all 0.5s ease;
opacity:1.0;
}
.element-group *{
transition: all 0.5s ease;
}
I'm not JS savvy, but in HTML, you can put the image that comes as a squared image within the svg circle using the following code:
<svg width="100" height="100">
<defs>
<pattern id="image" patternUnits="userSpaceOnUse" height="100" width="100">
<image x="0" y="0" height="100" width="100" xlink:href="http://i.imgur.com/7Nlcay7.jpg"></image>
</pattern>
</defs>
<circle id='top' cx="50" cy="50" r="50" fill="url(#image)"/>
</svg>
Hope this helps.
All credits go to Teo Inke

Categories