I'm trying to animate the following html elements to implement a functionality similar to a volume wheel.
<svg id="circle_svg" width="200" height="175">
<circle cx="100" cy="85" r="75" stroke="black" stroke-width="2" fill="lightgray"/>
<line id="line_alpha" x1="100" y1="85" x2="100" y2="160" style="stroke:rgb(0,0,255);stroke-width:2"/>
<circle id="dot_alpha" cx="100" cy="160" r="10" stroke="black" stroke-width="2" fill="red"/>
</svg>
The basic idea is that clicking on the red dot and moving the mouse around should result in the following behavior:
The red dot moves along the circle (even if mouse doesn't stay exactly on it).
The end point of the line on the circle follows the red dot.
A number shown somewhere else in the page gets incremented or decremented with the amount of angular displacement.
I found a demo online that allows to drag an svg circle all around the page, by binding the elements of interest to mousedown and mouseup events and rewriting the attribute cx and cy of the circle to the current location of the mouse.
However when testing the code on jsfiddle with my example (or even with the original code) something is not working. Could you please take a look and give me advice on what might be going wrong?
Jsfiddle my settings
Jsfiddle original settings
I was able to find the solution to my question (thanks to a friend) and will post it as reference for others:
The main problem with pasting the code from this online demo into jsfiddle is that the order in which JavaScript libraries and functions is not predictable.
So some binding might be called before the binded function is defined.
Also, the code from the demo is more complicated that what I needed.
Here is the code solution on jsfiddle
Below is a working snippet for SO site
var dragging = false
var updateGraphics = function (e) {
if (dragging) {
var parentOffset = $('#wheel').offset();
var relX = e.pageX - parentOffset.left;
var relY = e.pageY - parentOffset.top;
var cx = +$('#circle').attr('cx')
var cy = +$('#circle').attr('cy')
var r = +$('#circle').attr('r')
var dx = relX - cx
var dy = relY - cy
//var dx = e.clientX - cx
//var dy = e.clientY - cy
console.debug('cx: ' + cx);
console.debug('cy: ' + cy);
console.debug('dx: ' + dx);
console.debug('dy: ' + dy);
console.debug('clientX: ' + e.clientX);
console.debug('clientY: ' + e.clientY);
console.debug('relX: ' + relX);
console.debug('relY: ' + relY);
var a = Math.atan2(dy, dx)
var dotX = cx + r * Math.cos(a)
var dotY = cy + r * Math.sin(a)
$('#dot').attr('cx', dotX);
$('#dot').attr('cy', dotY);
$('#line_2').attr('x2', dotX);
$('#line_2').attr('y2', dotY);
}
}
$('svg').on('mousedown', function (e) {
dragging = true
updateGraphics(e)
})
$('svg').on('mouseup', function (e) {
dragging = false
})
$('svg').on('mousemove', updateGraphics)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg id="wheel" width="200" height="175" style="background-color:lightgreen;">
<circle id="circle" cx="100" cy="85" r="75" stroke="black" stroke-width="2" fill="lightgray"/>
<line id="line_1" x1="100" y1="85" x2="100" y2="160" stroke-dasharray="15,15" style="stroke:rgb(255,0,0);stroke-width:2"/>
<line id="line_2" x1="100" y1="85" x2="100" y2="160" style="stroke:rgb(0,0,255);stroke-width:2"/>
<circle id="dot" cx="100" cy="160" r="10" stroke="black" stroke-width="2" fill="red"/>
</svg>
Related
My goal is to design an arc slider which looks something like that
I have the following structure of the template
<svg width="500" height="300">
<path id="track" stroke="lightgrey" fill="transparent" stroke-width="20" d="
M 50 50
A 90 90 0 0 0 300 50
"/>
<path id="trackFill" fill="cyan" stroke-width="20" d="
M 50 50
A 90 90 0 0 0 [some dynamic value?] [some dynamic value?]
"/>
<circle id="knob" fill="lightblue" cx="[dynamic, initial - 50]" cy="[dynamic, initial - 50]" r="25"/>
</svg>
knob - the control which user is supposed to drag in order to change the value
track - the full arc of the slide
trackFill - the portion of the slider path before the knob
Is it possible to make trackFill cover the portion of the slider before the knob as it is being dragged along the slider curve? If so which APIs or CSS rules will help me to achieve such a result?
Is it something like this you are after?
let svg = document.getElementById("slider");
let trackFill = document.getElementById("trackFill");
let knob = document.getElementById("knob");
let isDragging = false;
let sliderDragOffset = {dx: 0, dy: 0};
let ARC_CENTRE = {x: 175, y: 50};
let ARC_RADIUS = 125;
let sliderValue = 0;
setSliderValue(sliderValue);
function setSliderValue(value)
{
// Limit value to (0..sliderMax)
let sliderMax = track.getTotalLength();
sliderValue = Math.max(0, Math.min(value, sliderMax));
// Calculate new position of knob
let knobRotation = sliderValue * Math.PI / sliderMax;
let knobX = ARC_CENTRE.x - Math.cos(knobRotation) * ARC_RADIUS;
let knobY = ARC_CENTRE.y + Math.sin(knobRotation) * ARC_RADIUS;
// Adjust trackFill dash patter to only draw the portion up to the knob position
trackFill.setAttribute("stroke-dasharray", sliderValue + " " + sliderMax);
// Update the knob position
knob.setAttribute("cx", knobX);
knob.setAttribute("cy", knobY);
}
knob.addEventListener("mousedown", evt => {
isDragging = true;
// Remember where we clicked on knob in order to allow accurate dragging
sliderDragOffset.dx = evt.offsetX - knob.cx.baseVal.value;
sliderDragOffset.dy = evt.offsetY - knob.cy.baseVal.value;
// Attach move event to svg, so that it works if you move outside knob circle
svg.addEventListener("mousemove", knobMove);
// Attach move event to window, so that it works if you move outside svg
window.addEventListener("mouseup", knobRelease);
});
function knobMove(evt)
{
// Calculate adjusted drag position
let x = evt.offsetX + sliderDragOffset.dx;
let y = evt.offsetY + sliderDragOffset.dy;
// Position relative to centre of slider arc
x -= ARC_CENTRE.x;
y -= ARC_CENTRE.y;
// Get angle of drag position relative to slider centre
let angle = Math.atan2(y, -x);
// Positions above arc centre will be negative, so handle them gracefully
// by clamping angle to the nearest end of the arc
angle = (angle < -Math.PI / 2) ? Math.PI : (angle < 0) ? 0 : angle;
// Calculate new slider value from this angle (sliderMaxLength * angle / 180deg)
setSliderValue(angle * track.getTotalLength() / Math.PI);
}
function knobRelease(evt)
{
// Cancel event handlers
svg.removeEventListener("mousemove", knobMove);
window.removeEventListener("mouseup", knobRelease);
isDragging = false;
}
<svg id="slider" width="500" height="300">
<g stroke="lightgrey">
<path id="track" fill="transparent" stroke-width="20" d="
M 50 50
A 125 125 0 0 0 300 50
"/>
</g>
<use id="trackFill" xlink:href="#track" stroke="cyan"/>
<circle id="knob" fill="lightblue" cx="50" cy="50" r="25"/>
</svg>
I've kept this code simple for clarity, but at the expense of some limitations.
It assumes there is only one slider per page. If you need more than that, you will have to keep the slider-specific values (eg sliderValue and, isDragging) separate. You could use data attributes for that. You would also need to switch from accessing the SVG elements via id attributes to another way (eg. class attributes), because id attributes must be unique on the page.
Here is a simple example:
const radius = 50;
const offsetX = 10;
const offsetY = 10;
// 0 <= pos <= 1
const setSliderPos = (svg, pos) => {
const angle = Math.PI * pos;
const x = offsetX + radius - Math.cos(angle) * radius;
const y = offsetY + Math.sin(angle) * radius;
svg.select('.knob').attr('cx', x).attr('cy', y);
svg.select('.first').attr('d', `M ${offsetX},${offsetY} A ${radius},${radius} 0 0 0 ${x},${y}`);
svg.select('.second').attr('d', `M ${x},${y} A ${radius},${radius} 0 0 0 ${offsetX + radius * 2},${offsetY}`);
}
setSliderPos(d3.select('#svg-1'), 0.3);
setSliderPos(d3.select('#svg-2'), 0.6);
setSliderPos(d3.select('#svg-3'), 1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg id="svg-1" width="150" height="80">
<path class="first" stroke-width="5" stroke="lightblue" fill="none"/>
<path class="second" stroke-width="5" stroke="cyan" fill="none"/>
<circle class="knob" r="10" fill="lightblue"/>
</svg>
<svg id="svg-2" width="150" height="80">
<path class="first" stroke-width="5" stroke="lightblue" fill="none"/>
<path class="second" stroke-width="5" stroke="cyan" fill="none"/>
<circle class="knob" r="10" fill="lightblue"/>
</svg>
<svg id="svg-3" width="150" height="80">
<path class="first" stroke-width="5" stroke="lightblue" fill="none"/>
<path class="second" stroke-width="5" stroke="cyan" fill="none"/>
<circle class="knob" r="10" fill="lightblue"/>
</svg>
To mark the progress you can use stroke-dasharray with a percentage; for example
<g stroke="lightgrey">
<path id="track" fill="transparent" stroke-width="20"
stroke-dasharray="40% 60%"
d="M 50 50 A 125 125 0 0 0 300 50"/>
</g>
This will show 40% of the arc and hide 60% of the arc.
If you need to use two colors, for example the whole arc in grey and the progress in black, you need to use two arcs on top of one another; the one at the bottom would be the one you already have, and the one at the top would have a stroke in black and use stroke-dasharray as shown.
I need to check the response of a user by tracking the mouse movement over a moving object (in this case a circle). If the mouse is not over the circle I need to calculate the offset by comparing the mouse coordinates and the circle coordinates.
But whenever I check the circle values, they are not changing and will stay on their initial value.
Here's a simple example:
function clickCircle() {
var circle = document.getElementById("circle");
console.log('baseVal x: ' + circle.cx.baseVal.value);
console.log('animVal x: ' + circle.cx.animVal.value);
}
<p>Click on the moving circle</p>
<svg width="1200" height="1200">
<circle id="circle" cx="60" cy="60" r="20" fill="green" onclick="clickCircle();">
<animateMotion id="ani" dur="10s" repeatCount="indefinite"
path="M20, 60 C20,
-50 180, 150 180,
60 C180-60 20,
150 20, 60 z" />
</circle>
</svg>
Does anybody have any idea on how to get the coordinates from a moving circle that is being animated with animateMotion?
You could drag in an animation Icon, and track its properties
Or with JavaScript you calculate its center x,y position with:
let {width,height} = circle.getBBox();
let {x,y} = circle.getBoundingClientRect();
x = x + width/2;
y = y + height/2;
Also read: https://schneide.blog/2018/03/05/some-tricks-for-working-with-svg-in-javascript/
note! this code below will forever add circle Nodes!
<style> svg { width: 300px } </style>
<svg viewBox="0 0 300 200">
<rect width="100%" height="100%" fill="lightgreen"></rect>
<text x=10 y="20">Click the circle!</text>
<circle id="circle" cx="40" cy="40" r="40" fill="green"
onclick="clickCircle(event)">
<animateMotion id="ani" dur="10s" repeatCount="indefinite" path="m20 40c0-110 160 90 160 0c0-120-160 90-160 0z" />
</circle>
<text id="position" x="200" y="20">21</text>
</svg>
<script>
function clickCircle(evt) {
point("gold");
}
function point(color) {
let circle = document.getElementById("circle");
let {width,height} = circle.getBBox();
let {x,y} = circle.getBoundingClientRect();
let c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
x = x + width/2;
y = y + height/2;
c.setAttribute("cx", x);
c.setAttribute("cy", y);
c.setAttribute("r", color == "black" ? 3 : 6);
c.setAttribute("fill", color);
circle.parentNode.append(c);
position.innerHTML = `${~~x} , ${~~y}`;
}
setInterval(() => point("black"), 250);
</script>
Or try the JSFiddle: https://jsfiddle.net/dannye/ph705b49/
Dan was Authorware toch heel wat makkelijker...
Alles goed?
I am just getting into JS, so I may be missing something. I am trying to animate an SVG rectangle with a mouseover so that the shape appears to be 'fleeing' the mouse. When I try to change x and y by adding to them, the shape disappears. If I subtract, it behaves as expected.
Any help would be greatly appreciated.
HTML
<svg width="1200" height="600">
<rect x="100" y="100" width="100" height="100" id="firstShape" onmouseover="moveShape(firstShape);">
</svg>
Javascript
function moveShape(obj) {
var newX = obj.getAttribute("x") + 5;
var newY = obj.getAttribute("y") + 5;
obj.setAttribute("x", newX);
obj.setAttribute("y", newY);
}
Attributes are strings, and Javascript is very sloppy about the way it handles strings and numbers.
What you were actually doing is adding "5" to "100" and getting "1005" as a result.
If you convert the attributes to integers before modifying them, then your code will work fine.
function moveShape(obj) {
var newX = parseInt(obj.getAttribute("x")) + 5;
var newY = parseInt(obj.getAttribute("y")) + 5;
obj.setAttribute("x", newX);
obj.setAttribute("y", newY);
}
<svg width="1200" height="600">
<rect x="100" y="100" width="100" height="100" id="firstShape" onmouseover="moveShape(firstShape);">
</svg>
So I have 4 SVG circles, which I'm using stroke-dash to mask. And the general idea is that they're supposed to make up one full circle based on their percentage.
I've gotten the length of each segment, and when I rotate them manually I see that it all adds up. But I can't figure out how to calculate the rotation of each segment. Under is a jsbin link to show how far I've gotten:
http://jsbin.com/lutodomujo/1/
Also, if there is better way to solve this, I'd be happy to hear it. The only thing that has to work is the hover effect as shown in the example.
By the way, the following line is a purely wild guess (as you may have noticed):
var rotate = (Math.sin((c-prevRotate)/100) * Math.PI)*100; // ?
And it is, as far as I know, the only one I need help to figure out.
var prevRotate = 0;
$('circle').each(function (i) {
var r = $(this).attr('r');
var val = $(this).data('perc');
var c = Math.PI * (r * 2);
var pct = ((100 - val) / 100) * c;
var rotate = (Math.sin((c-prevRotate)/100) * Math.PI)*100;
$(this).css({
strokeDasharray: c,
strokeDashoffset: pct,
transform: 'rotate(' + rotate + 'deg)'
});
prevRotate += pct;
});
svg { width: 300px; }
circle {
stroke-width: 3;
transform-origin: center;
}
circle:hover {stroke-width: 5}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 164 164">
<circle fill="none" stroke="#A5D2C6" cx="82" cy="82" r="80" data-perc="40"/>
<circle fill="none" stroke="#000000" cx="82" cy="82" r="80" data-perc="30"/>
<circle fill="none" stroke="#EBE6B7" cx="82" cy="82" r="80" data-perc="20"/>
<circle fill="none" stroke="#F1AAA6" cx="82" cy="82" r="80" data-perc="10"/>
</svg>
I made some change and it s work :
var prevRotate = 0;
$('circle').each(function (i) {
var r = $(this).attr('r');
var val = $(this).data('perc');
var c = Math.PI * (r * 2);
var pct = ((100 - val) / 100) * c;
var rotate = prevRotate;
$(this).css({
strokeDasharray: c,
strokeDashoffset: pct,
transform: 'rotate(' + rotate + 'deg)'
});
prevRotate += (360*val/100);
});
I'm trying to manipulate with mouse SVG path which represents symbol of electronics resistor. This symbol requires manipulation with end of the "leads" (if you can picture real resistor); therefore I am trying to achieve draging 1st point arround (2nd is still there) and to all points of path to behave proportionally in when drag the 1st point on new coordinates.
Try to group, try with trigonometry functions...so code is like:
`<g id="r" > <!-- R - group for symbol of electronics resistor -->
<path d="M 200 20 v80 h30 v150 h-60 v-150 h30 m 0 150 v80 "
fill="none" stroke="blue" stroke-width="5" />
<circle cx="200" cy="20" r="10"
fill="blue" />
<circle cx="200" cy="330" r="10"
fill="blue"/>
</g>`
Please, help.
I think I've made roughly what you want: http://dl.dropbox.com/u/169269/resistor.svg
Click and drag on the resistor to scale and rotate it to that mouse position. In this version, the line thickness and circles also scale, but it shouldn't be too difficult to avoid that.
It does require trigonometry and transformations. The key part is the drag function, which I explain in more detail at: http://www.petercollingridge.co.uk/interactive-svg-components/draggable-svg-element
function drag(evt)
{
if(selected_element != 0)
{
var resistor_x = 200;
var resistor_y = 100;
var mouse_x = evt.pageX;
var mouse_y = evt.pageY;
dx = resistor_x - mouse_x;
dy = resistor_y - mouse_y;
d = Math.sqrt(dx*dx + dy*dy); // Find distance to mouse
theta = 90+Math.atan2(dy, dx)*180/Math.PI; //Find angle to mouse in degrees
transform = "translate(200, 100) rotate(" + theta + ") scale(" + d/310 + ")" ;
selected_element.setAttributeNS(null, "transform", transform);
}
}
Note that this code assumes the resistor is 310 pixels long and rotating about (200, 100). Scaling and rotation transformations work centred on (0,0), so you should draw the resistor with the static point at (0,0) and then translate it to where you want.