HTML and SVG how to move only one endpoint of a line with mouse click? - javascript

I've been trying to figure out a way to move just one end of a line, but it keep changing the container and thus redrawing the entire line and moving it.
Here's a js fiddle: https://jsfiddle.net/h2nwygu8/1/
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Review Race</title>
</head>
<body>
<div class="wrapper">
<section>
<h1>Line Test</h1>
<div id="canvas" style="width: 960px; height:410px; border: 4px solid lime;">
<svg id="svgContainer" xmlns="http://www.w3.org/2000/svg" version="1.1" style="position: absolute; top:95px; width:960px; height:410px;">
<line id="start_line" x1="10" y1="0" x2="10" y2="960" style="stroke:rgb(255,0,0);stroke-width:3;" />
</svg>
</div>
</section>
</div>
</body>
</html>
and javascript:
function placeLine(e) {
var d = document.getElementById('start_line');
posY = e.clientY;
posX = e.clientX;
d.setAttribute("y1", posY)
d.setAttribute("x1", posX)
}
document.addEventListener("click", placeLine);
what I would like to do is have the top of the red line move around over the green box to the mouse x,y position. The bottom of the line should not move. However with each click the containers are changing and moving the entire line. You can also see there are a few issues with the offsets with the mouse x,y and the line. The only restriction is that the line needs to be able to layer over the top of the container with the green boundary.
What's the best way to do this? Thanks.

You've mixed up the height and width with regards to the x2 and y2 properties. It should start working more like you expect if you change y2 to be equal to your height (410px).
See: https://jsfiddle.net/zukrtpyg/.
HTML:
<div id="canvas" style="width: 960px; height:410px; border: 4px solid lime;">
<svg id="svgContainer" xmlns="http://www.w3.org/2000/svg" version="1.1" style="width:960px; height:410px;">
<line id="start_line" x1="0" y1="0" x2="0" y2="410" style="stroke:rgb(255,0,0);stroke-width:3;" />
</svg>
</div>
Javascript:
function placeLine(e) {
var d = document.getElementById('start_line');
posY = e.offsetY;
posX = e.offsetX;
d.setAttribute("y1", posY)
d.setAttribute("x1", posX)
}
document.getElementById('canvas').addEventListener("click", placeLine);
I've also removed the top and position declarations on the canvas element, these were causing the line to extend past the green border on the bottom.
Lastly, I've changed the click listener to be on the canvas element, that way you can use the events' offsetX and offsetY to position the line in relation to the canvas element, which is what the path coordinates are mapped to in the first place.
Using the clientX and clientY positions tries to place it in relation to the page, which doesn't map up with the position you want it to be on the canvas.
You can actually still use the clientX and clientY positions, you just have to compensate with the x and y position of the canvas element in relation to the page. You can find these out using Element.getBoundingClientRect(). See https://jsfiddle.net/th0aLcx0/ for an example.

Related

Any workaround to this Firefox bug? SVGElement.getScreenCTM incorrect when parent element has a transform

The Firefox bug in question is https://bugzilla.mozilla.org/show_bug.cgi?id=1610093
It's a long-standing issue whereby the getScreenCTM method of an SVG element returns incorrect values when a parent above the root SVG element has a transform applied.
The following snippet should show the black circle directly under the mouse pointer inside the red box. This works correctly in webkit/blink where the css transform on the parent is properly calculated. In Firefox this bug causes the black circle to be offset by the same values as the css transform on the parent div element.
I've avoided the issue up until now by ensuring there are no transforms further up the dom tree, but I'm now developing this as a component where I won't have control over the parent dom.
Does anyone know an easy workaround for getting a correct transform matrix?
const svgEl = document.querySelector(`svg#test-1`);
const circleEl = document.querySelector(`svg#test-1 circle`);
const handleMouseMove = (e) => {
// transform screen to SVG coordinate space
const mousePoint = svgEl.createSVGPoint();
mousePoint.x = e.clientX;
mousePoint.y = e.clientY;
const mouseCoordsInCanvasSpace = mousePoint.matrixTransform(svgEl.getScreenCTM().inverse())
// set circle to coords, should be mouse center in SVG coord space
circleEl.setAttributeNS(null, 'cx', mouseCoordsInCanvasSpace.x);
circleEl.setAttributeNS(null, 'cy', mouseCoordsInCanvasSpace.y);
}
window.addEventListener('mousemove', handleMouseMove);
body{
padding: 0px;
margin: 0px;
}
svg {
border: 2px solid red;
margin-left: 0px;
}
<!DOCTYPE html>
<html>
<body>
<div style="transform: translate(50px, 50px);">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100" id="test-1">
<circle r="10" />
</svg>
</div>
</body>
</html>
That is an annoying bug! My suggestion is to use an <object> element. The source/data for the object should be the SVG.
In the following example I'm using a data URL as a data source for the <object>, so that we at least can see something here on ST. But I imagine that the SVG should be dynamic somehow. So, to make this work you will need to load the SVG as a source of a file (data="/file.svg"). If you do that you have access to the contentDocument property on <object> and in that way you have direct access to the DOM of the SVG from the "outside". It needs to run from a web server and on the same domain as well.
body{
padding: 0px;
margin: 0px;
}
object {
border: 2px solid red;
margin-left: 0px;
}
<div style="transform: translate(50px, 50px);">
<object width="100" height="100" type="image/svg+xml" data="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgaWQ9InRlc3QtMSI+CjxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0Ij4KPCFbQ0RBVEFbCnZhciBjaXJjbGVFbDsKY29uc3QgaGFuZGxlTW91c2VNb3ZlID0gZSA9PiB7CiAgY29uc3QgbW91c2VQb2ludCA9IG5ldyBET01Qb2ludChlLmNsaWVudFgsIGUuY2xpZW50WSk7ICAKICBjb25zdCBtb3VzZUNvb3Jkc0luQ2FudmFzU3BhY2UgPSBtb3VzZVBvaW50Lm1hdHJpeFRyYW5zZm9ybShlLnRhcmdldC5nZXRTY3JlZW5DVE0oKS5pbnZlcnNlKCkpOwogIC8vIHNldCBjaXJjbGUgdG8gY29vcmRzLCBzaG91bGQgYmUgbW91c2UgY2VudGVyIGluIFNWRyBjb29yZCBzcGFjZSAKICBjaXJjbGVFbC5zZXRBdHRyaWJ1dGVOUyhudWxsLCAnY3gnLCBtb3VzZUNvb3Jkc0luQ2FudmFzU3BhY2UueCk7CiAgY2lyY2xlRWwuc2V0QXR0cmlidXRlTlMobnVsbCwgJ2N5JywgbW91c2VDb29yZHNJbkNhbnZhc1NwYWNlLnkpOwp9OwoKZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignRE9NQ29udGVudExvYWRlZCcsIGUgPT4gewogIGNpcmNsZUVsID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2MxJyk7CiAgZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignbW91c2Vtb3ZlJywgaGFuZGxlTW91c2VNb3ZlKTsKfSk7Cl1dPgo8L3NjcmlwdD4KPGNpcmNsZSBpZD0iYzEiIHI9IjEwIiAvPgo8L3N2Zz4="></object>
</div>
Here is the SVG I use in the <object>:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<script type="text/javascript">
<![CDATA[
var circleEl;
const handleMouseMove = e => {
const mousePoint = new DOMPoint(e.clientX, e.clientY);
const mouseCoordsInCanvasSpace = mousePoint.matrixTransform(e.target.getScreenCTM().inverse());
circleEl.setAttributeNS(null, 'cx', mouseCoordsInCanvasSpace.x);
circleEl.setAttributeNS(null, 'cy', mouseCoordsInCanvasSpace.y);
};
document.addEventListener('DOMContentLoaded', e => {
circleEl = document.getElementById('c1');
document.addEventListener('mousemove', handleMouseMove);
});
]]>
</script>
<circle id="c1" r="10" />
</svg>

why custom svg cursor is vanishing after certain value of height and width is reached

I want to display a custom circular shape as cursor whose sizes are dynamic.
general logic i'm following is, if radius = some value and svg height and width is twice the radius
i.e for r="64" ---> width="128" height="128"
Problem statement
I found 128 is the threshold height and width.
if it is increased just by one i.e 129 then cursor will no longer appear.
How to increase cursor side beyond 128 i.e radius is beyond 64?
see below code example,
var one = document.getElementById('one');
one.style.cursor = `url('data:image/svg+xml;utf8,<svg fill="%23FF0000" width="128" height="128" xmlns="http://www.w3.org/2000/svg"><circle cx="50%" cy="50%" r="64" fill="yellow" /></svg>') 32 32, auto`;
var two = document.getElementById('two');
two.style.cursor = `url('data:image/svg+xml;utf8,<svg fill="%23FF0000" width="129" height="129" xmlns="http://www.w3.org/2000/svg"><circle cx="50%" cy="50%" r="64" fill="yellow" /></svg>') 32 32, auto`;
<!DOCTYPE html>
<html>
<body>
<div>
<div id="one" style="padding: 50px; background-color:green">one</div>
<div id="two" style="padding: 50px; background-color:red">two</div>
</div>
</body>
</html>

Curved Text inside a circle and center to bottom center

I have a circle, and i need to have text inside the circle, that follows the circles path. I have found this other question: Wrapping a text around a circular element
However, i was not able to achieve what i'm trying to do. My text has a dynamic length, and i need it to be always centered to the bottom center of the circle, and wrap "upwards" on both sides, if that makes sense. I have tried the svg approach, but when the text is too long, it gets cut off. What would be the best approach to do this?
The text would be orange here (dynamic length), and it needs to centered to the bottom of the circle (blue dot)
Having tried positioning text with middle in the svg I found it cut the text if the text was following a path - as in the question.
Could not find a solution for this in SVG and/or CSS so resorted to a method with JS and show it here just in case of use as an interim measure.
In this snippet, the width of the text is found and the amount the SVG needs to rotate in order to put the mid point of the text at the bottom center of the circle is calculated:
const textPath = document.querySelector('textPath');
const temp = document.createElement('div');
temp.innerHTML = textPath.innerHTML;
temp.style.display = 'inline-block';
temp.style.fontSize = '20px';
document.body.append(temp);
const w = temp.offsetWidth;
document.body.removeChild(temp);
const circumference = Math.PI * 200;
document.querySelector('svg').style.transform = 'rotate(' + Number((180 * w / circumference) - 90) + 'deg)';
.qr--label {
font-size: 20px;
margin: 0 0 -15px 0;
text-align: center;
}
<div class="qr">
<div>
<svg width="220" height="220">
<path fill="white" d="M0,110a110,110 0 1,0 220,0a110,110 0 1,0 -220,0"/>
<path fill="none" id="innerCircle" d="M10,110a100,100 0 1,0 200,0a100,100 0 1,0 -200,0"/>
<text>
<textPath xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#innerCircle" class="qr--label">
Some dynamic text here
</textPath>
</text>
</svg>
</div>
</div>

How to measure width of a string in SVG before creating the actual SVG element (in Angular)

I want to create a SVG element, which include a list of node, each node is a rectangle with a text inside:
<svg class="chart" width="420" height="150" aria-labelledby="title" role="img">
<ng-container *ngFor="let item of listNode">
<g viewBox="0 0 56 18">
<rect [attr.width]="item.w" [attr.height]="2*item.h" fill="white"></rect>
<text [attr.x]="item.x" [attr.y]="item.y" text-anchor="middle" font-family="Meiryo" font-size="item.h"
fill="black">{{item.label}}</text>
</g>
</ng-container>
</svg>
But I want to put the text is in center of the rectangle (both vertically and horizontally). To do this, I think I need to measure the width of text before creating the SVG element.
I tried to use the below function to measure text size:
getTextWidth(text, fontSize, fontFace) {
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
context.font = fontSize + 'px ' + fontFace;
return context.measureText(text).width;
}
But the result is not correct.
So, Is there any way to measure size of SVG text element before creating the real element (like my getTextWidth function), or is there any other way to create rectangle with text is exactly in center.
There are many ways to measure the text width and height but if you have the text itself and its font properties, you already have it.
1) Let's suppose you don't want to do the math, you can use HTMLElement properties.
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
const span = document.createElement('span')
document.body.append(span)
span.style = 'font-family: Arial; font-size: 14px'
span.innerHTML = 'TEXT'
document.body.textContent =
`width: ${span.offsetWidth} height: ${span.offsetHeight}`
span.remove()
</script>
</body>
</html>
Note: don't forget to append your element.
2) Use those dimensions to set your rect dimensions (add extra if you want padding) and set your text in the middle like so:
<text
style="font-family: Arial; font-size: 14px"
x="50%"
y="50%"
dominant-baseline="middle"
text-anchor="middle">TEXT</text>
Note: use always the same style in your text.
Hope this help :)

Partially fill a shape's border with colour

I am trying to create a progress effect whereby colour fills a DOM object's border (or possibly background). The image attached should give you a better idea of what I'm going for. I have achieved the current result by adding an object with a solid background colour over the grey lines and setting its height. This object has mix-blend-mode: color-burn; applied to it which is why it only colours the grey lines underneath it.
This works okay, but ruins the anti aliasing around the circle, and also the produced colour is unpredictable (changes depending on the colour of the lines).
I feel there must be a better way of achieving this, perhaps with the canvas element. Could someone point me in the right direction please?
Thanks in advance!
This should be possible to do with Canvas and may even be possible with CSS itself by playing with multiple elements etc but I would definitely recommend you to use SVG. SVG offers a lot of benefits in terms of how easy it is to code, maintain and also produce responsive outputs (unlike Canvas which tends to become pixelated when scaled).
The following are the components:
A rect element which is the same size as the parent svg and has a linear-gradient fill. The gradient has two colors - one is the base (light gray) and the other is the progress (cyan-ish).
A mask which is applied on the rect element. The mask has a path which is nothing but the line and the circle. When the mask is applied to the rect, only this path would show through the actual background (or fill) of the rect, the rest of the area would be masked out by the other rect which is added inside the mask.
The mask also has a text element to show the progress value.
The linear-gradient has the stop offset set in such a way that it is equal to the progress. By changing the offset, we can always make sure that the path shows the progress fill only for the required length and the base (light gray) for the rest.
window.onload = function() {
var progress = document.querySelector('#progress'),
base = document.querySelector('#base'),
prgText = document.querySelector('#prg-text'),
prgInput = document.querySelector('#prg-input');
prgInput.addEventListener('change', function() {
prgText.textContent = this.value + '%';
progress.setAttribute('offset', this.value + '%');
base.setAttribute('offset', this.value + '%');
});
}
svg {
width: 200px;
height: 300px;
}
path {
stroke-width: 4;
}
#rect {
fill: url(#grad);
mask: url(#path);
}
/* just for demo */
.controls {
position: absolute;
top: 0;
right: 0;
height: 100px;
line-height: 100px;
border: 1px solid;
}
.controls * {
vertical-align: middle;
}
body {
background-image: radial-gradient(circle, #3F9CBA 0%, #153346 100%);
}
<svg viewBox='0 0 200 300' id='shape-container'>
<linearGradient id='grad' gradientTransform='rotate(90 0 0)'>
<stop offset='50%' stop-color='rgb(0,218,235)' id='progress' />
<stop offset='50%' stop-color='rgb(238,238,238)' id='base' />
</linearGradient>
<mask id='path' maskUnits='userSpaceOnUse' x='0' y='0' width='200' height='300'>
<rect x='0' y='0' width='200' height='300' fill='black' />
<path d='M100,0 100,100 A50,50 0 0,0 100,200 L100,300 M100,200 A50,50 0 1,0 100,100' stroke='white' />
<text id='prg-text' x='100' y='155' font-size='20' text-anchor='middle' fill='white'>50%</text>
</mask>
<rect id='rect' x='0' y='0' width='200' height='300' />
</svg>
<!-- just for demo -->
<div class='controls'>
<label>Set Progress:</label>
<input type='range' id='prg-input' min='0' max='100' value='50' />
</div>
If you are new to SVG you can refer to the MDN Docs (links provided below) for more information about the elements, their attributes and values.
SVG Mask Element
SVG Tutorial on Paths

Categories

Resources