I render an SVG line that has a new point added to it every 50ms. After a few seconds, the line is off the page. How can I make my SVG keep the most recent part of the line in view, while moving the older part out of view
var $series = $('#series');
var data = [];
setInterval(function() {
// Add random data step
var newValue = Math.floor(Math.random() * 10) - 5;
data.push(newValue)
// Rerender line
var newPoints = 'M10 50 ';
data.forEach(function(y) {
console.log(y);
newPoints += "l 10 " + y + " ";
});
console.log(newPoints);
$('#series').attr('d', newPoints);
}, 50);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg width="1000" height="500" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path id="series" d="M10 250 l 10 10 l 10 20 l 10 50 l 10 -20" stroke="red" stroke-width="2" fill="none" />
</svg>
var $series = $('#series');
var chart = $('#chartdiv').find('svg')[0];
var data = [];
var xmin = 0;
setInterval(function() {
// Add random data step
xmin = xmin + 10;
var newValue = Math.floor(Math.random() * 10) - 5;
data.push(newValue)
// Rerender line
var newPoints = 'M10 50 ';
data.forEach(function(y) {
//console.log(y);
newPoints += "l 10 " + y + " ";
});
//console.log(newPoints);
$('#series').attr('d', newPoints);
if (xmin > 1000 )
{
var gmin = xmin - 1000;
chart.setAttribute('viewBox', gmin + " 0 1000 500");
}
}, 50);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='chartdiv'>
<svg viewBox="0 0 1000 500" width="1000" height="500" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path id="series" d="M10 250 l 10 10 l 10 20 l 10 50 l 10 -20" stroke="red" stroke-width="2" fill="none" />
</svg>
</div>
Try this snippet above. I added a viewBox and increment the viewBox "x" value in each loop to move the viewing area live as you draw.
Related
I have an <svg> included in my HTML file with a bunch of <path> elements.
My desired behavior is to be able to randomly shuffle the positioning of the <path> elements, and then to subsequently sort them back into their proper position.
Example: if I have 3 <path>s at positions 1, 2, and 3. For the shuffle functionality, I move path 1 to position 3, path 2 to position 1, and path 3 to position 2. Then I do some kind of visual sort (e.g. insertion sort), where I swap two <path>s' positions at a time until the <path>s are back in their proper place and the SVG looks normal again.
If these were "normal" HTML elements I would just set the x and y properties, but based on my research <path> elements don't have those properties, so I've resorted to using the transform: translate(x y).
With my current approach, the first swap works fine. But any subsequent swaps get way out of whack, and go too far in both directions.
If I'm just swapping two <path>s back and forth, I can get it to work consistently by keeping track of which element is in which position (e.g. elem.setAttribute('currPos', otherElem.id)), and when currPos == currElem.id, setting transform: translate(0 0), but when I start adding more elements, they end up moving to places where there previously wasn’t a <path> element.
My current code is below. For some reason the CSS transition isn’t working properly here but it works elsewhere (edit: it works fine on desktop just not on my phone)
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getPos(elem) {
let rect = elem.getBoundingClientRect();
let x = rect.left + (rect.right - rect.left) / 2;
let y = rect.top + (rect.bottom - rect.top) / 2;
return [x, y];
}
async function swap(e1, e2, delayMs = 3000) {
let e1Pos = getPos(e1);
let e2Pos = getPos(e2);
console.log(e1Pos, e2Pos);
e2.setAttribute('transform', `translate(${e1Pos[0]-e2Pos[0]}, ${e1Pos[1]-e2Pos[1]})`);
e1.setAttribute('transform', `translate(${e2Pos[0]-e1Pos[0]}, ${e2Pos[1]-e1Pos[1]})`);
if (delayMs) {
await delay(delayMs);
}
}
let blackSquare = document.getElementById('black-square');
let redSquare = document.getElementById('red-square');
swap(blackSquare, redSquare)
.then(() => swap(blackSquare, redSquare))
.then(() => swap(blackSquare, redSquare));
* {
position: absolute;
}
path {
transition: transform 3s
}
<svg width="500" height="800" xmlns="http://www.w3.org/2000/svg">
<path id="black-square" d="M 10 10 H 90 V 90 H 10 L 10 10" fill="black" />
<path id="red-square" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" />
<path id="green-square" d="M 20 120 h 80 v 80 h -80 v -80" fill="green" />
</svg>
You could achieve this by applying multiple translate transformations.
Lets say, the red square should be positioned at the black square position:
<path transform="translate(-130 -70) translate(10 10)" id="redSquare" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" ></path>
translate(-130 -70) negates the square's original x,y offset and moves this element to the svg's coordinate origin.
The second transformation translate(10 10) will move this element to the black square's position.
const paths = document.querySelectorAll("path");
function shuffleEls(paths) {
/**
* get current positions and save to data attribute
* skip this step if data attribute is already set
*/
if(!paths[0].getAttribute('data-pos')){
paths.forEach((path) => {
posToDataAtt(path);
});
}
// shuffle path element array
const shuffledPaths = shuffleArr([...paths]);
for (let i = 0; i < shuffledPaths.length; i += 1) {
let len = shuffledPaths.length;
//let el1 = i>0 ? shuffledPaths[i-1] : shuffledPaths[len-1] ;
let el1 = shuffledPaths[i];
let el2 = paths[i];
copyPosFrom(el1, el2);
}
}
function posToDataAtt(el) {
let bb = el.getBBox();
let [x, y, width, height] = [bb.x, bb.y, bb.width, bb.height].map((val) => {
return +val.toFixed(2);
});
el.dataset.pos = [x, y].join(" ");
}
function copyPosFrom(el1, el2) {
let [x1, y1] = el1.dataset.pos.split(" ").map((val) => {
return +val;
});
let [x2, y2] = el2.dataset.pos.split(" ").map((val) => {
return +val;
});
/**
* original position is negated by negative x/y offsets
* new position copied from 2nd element
*/
el1.setAttribute(
"transform",
`translate(-${x1} -${y1}) translate(${x2} ${y2})`
);
}
function shuffleArr(arr) {
const newArr = arr.slice();
for (let i = newArr.length - 1; i > 0; i--) {
const rand = Math.floor(Math.random() * (i + 1));
[newArr[i], newArr[rand]] = [newArr[rand], newArr[i]];
}
return newArr;
}
svg{
border:1px solid red;
}
path{
transition: 0.5s;
}
<p>
<button onclick="shuffleEls(paths)">shuffleAll()</button>
</p>
<svg width="500" height="800" xmlns="http://www.w3.org/2000/svg">
<path id="blackSquare" d="M 10 10 H 90 V 90 H 10 L 10 10" fill="black" />
<path id="redSquare" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" />
<path id="greenSquare" d="M 20 120 h 80 v 80 h -80 v -80" fill="green" />
<path id="purpleSquare" d="M 250 10 h 80 v 80 h -80 v -80" fill="purple" />
</svg>
How it works
shuffleEls() gets each path's position via getBBox() and saves x and y coordinates to a data attribute
We shuffle the path element array
each path inherits the position from it's shuffled counterpart
swap positions:
let el1 = shuffledPaths[i];
let el2 = paths[i];
copyPosFrom(el1, el2);
Update: include previous transformations
If a <path> element is already transformed (e.g rotated), you probably want to retain it.
const paths = document.querySelectorAll(".pathToshuffle");
function revertShuffling(paths) {
if (paths[0].getAttribute('data-pos')) {
paths.forEach((path) => {
copyPosFrom(path, path);
});
}
}
//shuffleEls(paths)
function shuffleEls(paths) {
/**
* get current positions and save to data attribute
* skip this step if data attribute is already set
*/
if (!paths[0].getAttribute('data-pos')) {
paths.forEach((path) => {
posToDataAtt(path);
});
}
// shuffle path element array
const shuffledPaths = shuffleArr([...paths]);
let shuffledElCount = 0;
for (let i = 0; i < shuffledPaths.length; i += 1) {
let el1 = shuffledPaths[i];
let el2 = paths[i];
shuffledElCount += copyPosFrom(el1, el2);
}
// repeat shuffling if result is identical to previous one
if (shuffledElCount < 1) {
shuffleEls(paths);
}
}
function posToDataAtt(el) {
let bb = el.getBBox();
let [x, y, width, height] = [bb.x, bb.y, bb.width, bb.height].map((val) => {
return +val.toFixed(2);
});
// include other transformations
let style = window.getComputedStyle(el);
let matrix = style.transform != 'none' ? style.transform : '';
el.dataset.pos = [x + width / 2, y + height / 2, matrix].join("|");
}
function copyPosFrom(el1, el2) {
let [x1, y1, matrix1] = el1.dataset.pos.split("|");
let [x2, y2, matrix2] = el2.dataset.pos.split("|");
/**
* original position is negated by negative x/y offsets
* new position copied from 2nd element
*/
let transformAtt = `translate(-${x1} -${y1}) translate(${x2} ${y2}) ${matrix1}`;
// compare previous transformations to prevent identical/non-shuffled results
let transFormChange = el1.getAttribute('transform') != transformAtt ? 1 : 0;
el1.setAttribute("transform", transformAtt);
return transFormChange;
}
function shuffleArr(arr) {
let newArr = arr.slice();
for (let i = newArr.length - 1; i > 0; i--) {
const rand = Math.floor(Math.random() * (i + 1));
[newArr[i], newArr[rand]] = [newArr[rand], newArr[i]];
}
return newArr;
}
svg {
border: 1px solid red;
}
path {
transition: 0.5s;
}
<p>
<button onclick="shuffleEls(paths)">shuffleAll()</button>
<button onclick="revertShuffling(paths)">revertShuffling()</button>
</p>
<svg width="500" height="800" xmlns="http://www.w3.org/2000/svg">
<path class="pathToshuffle" id="blackSquare" d="M 20 30 h 60 v 40 h -60 z" fill="#999" />
<path class="pathToshuffle" id="redSquare" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" />
<path class="pathToshuffle" id="greenSquare" d="M 20 120 h 80 v 80 h -80 v -80" fill="green" />
<path class="pathToshuffle" transform="rotate(45 275 35)" id="purpleSquare" d="M 250 10 h 50 v 50 h -50 v -50" fill="purple" />
<path id="bg" d="M 10 10 H 90 V 90 H 10 L 10 10z
M 130 70 h 80 v 80 h -80 v -80z
M 20 120 h 80 v 80 h -80 v -80z
M 250 10 h 50 v 50 h -50 v -50z" fill="none" stroke="#000" stroke-width="1" stroke-dasharray="1 2"/>
</svg>
We can append a matrix() to the data attribute.
If paths have different sizes or aspect ratios, you can also set centered x/y coordinates according the the actual bounding box.
This way, all shuffled elements will be positioned around the same center points.
let style = window.getComputedStyle(el);
let matrix = style.transform!='none' ? style.transform : '';
el.dataset.pos = [x+width/2, y+height/2, matrix].join("|");
I think that is is easier to keep track of the positions if all the <path> elements have the same starting point (so, the same distance to 0,0) and then use transform/translate to position them. You can use elements transform matrix to find the position.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getPos(elem) {
let x = elem.transform.baseVal[0].matrix.e;
let y = elem.transform.baseVal[0].matrix.f;
return [x,y];
}
async function swap(e1, e2, delayMs = 3000) {
let e1Pos = getPos(e1);
let e2Pos = getPos(e2);
e2.setAttribute('transform', `translate(${e1Pos[0]} ${e1Pos[1]})`);
e1.setAttribute('transform', `translate(${e2Pos[0]} ${e2Pos[1]})`);
if (delayMs) {
await delay(delayMs);
}
}
let blackSquare = document.getElementById('black-square');
let redSquare = document.getElementById('red-square');
let greenSquare = document.getElementById('green-square');
swap(blackSquare, redSquare)
.then(() => swap(blackSquare, redSquare))
.then(() => swap(blackSquare, greenSquare));
path {
transition: transform 3s
}
<svg viewBox="0 0 500 400" width="500"
xmlns="http://www.w3.org/2000/svg">
<path id="black-square" d="M 0 0 H 80 V 80 H 0 Z"
fill="black" transform="translate(200 50)" />
<path id="red-square" d="M 0 0 H 80 V 80 H 0 Z"
fill="red" transform="translate(50 20)" />
<path id="green-square" d="M 0 0 H 80 V 80 H 0 Z"
fill="green" transform="translate(100 120)" />
</svg>
I am trying to implement the Gauss Formula Visually so I am having a little bit of problems with JavaScript since I never worked with it before. My question would be how do I color certain squares of my grid
{
const w = width, h = w * .6, mx = w/2 * margin, my = h/2 * margin;
const s = DOM.svg(w, h);
const path = `M${mx},${my}${gridPath(count+1, count, w - mx * 2, h - my * 2, false)}`;
s.appendChild(svg`<path stroke=#555 d="${path}">`);
s[0].sty
return s;
}
and this is the function that computes the grid
function gridPath(cols, rows, width = 1, height = 1, initPosition = true) {
// Line distances.
const sx = width / cols, sy = height / rows;
// Horizontal and vertical path segments, joined by relative move commands.
const px = Array(rows+1).fill(`h${width}`).join(`m${-width},${sy}`);
const py = Array(cols+1).fill(`v${height}`).join(`m${sx},${-height}`);
// Paths require an initial move command. It can be set either by this function
// or appended to the returned path.
return `${initPosition ? 'M0,0' : ''}${px}m${-width}${-height}${py}`;
}
You need to create solid rectangles that could be filled.
Your script generates an array of lines – so you don't get any surface area.
Besides, all lines are currently defined as concatenated commands in a single <path> element.
That's great for optimizing filesize – but you won't be able to select individual sub paths (grid lines).
Example 1: Svg markup for a 3x3 grid changing fill color on hover
svg {
display: block;
overflow: visible;
width: 75vmin
}
path {
fill: #fff;
stroke-width: 1;
stroke: #000
}
path:hover {
fill: red
}
<svg viewBox="0 0 90 90">
<!-- 1. row -->
<path d="M 0 0 h30 v30 h-30z" />
<path d="M 30 0 h30 v30 h-30z" />
<path d="M 60 0 h30 v30 h-30z" />
<!-- 2. row -->
<path d="M 0 30 h30 v30 h-30z" />
<path d="M 30 30 h30 v30 h-30z" />
<path d="M 60 30 h30 v30 h-30z" />
<!-- 3. row -->
<path d="M 0 60 h30 v30 h-30z" />
<path d="M 30 60 h30 v30 h-30z" />
<path d="M 60 60 h30 v30 h-30z" />
</svg>
Example 2: Create grid svg dynamically. Set fill colors by class
let svg = document.querySelector('svg');
// generate grid svg
let gridSVG1 = gridPaths(4, 4, 100, 100, 0.25, '#ccc', true);
// highlight columns
highlightColumns(gridSVG1, ['1-2', '2-3', '3-4']);
function highlightColumns(svg, col_array) {
col_array.forEach(function(colIndex, i) {
// select
let currentCol = svg.querySelector('.col-' + colIndex);
currentCol.classList.add('highlight');
})
}
function gridPaths(cols, rows, width = 1, height = 1, strokeWidth = 1, strokeColor = '#000', render = false) {
let gridSvg = `<g fill="#fff" stroke-width="${strokeWidth}" stroke="${strokeColor}">\n`;
let offsetX = width / cols;
let offsetY = height / rows;
let currentRow = 1;
let currentCol = 1;
// add initial y/x offset according to stroke width to avoid cropped outer strokes
let shiftX = 0;
let shiftY = 0;
let cellCount = cols * rows;
for (let i = 0; i < cellCount; i++) {
// if current index is divisible by columns – move to next row
if (i > 0 && i % (cols) === 0) {
shiftX = 0;
shiftY += offsetY;
currentCol = 1;
currentRow++;
}
// add classnames for rows and columns to make each cell selectable
let className = `col row-${currentRow} col-${currentCol} col-${currentRow}-${currentCol}`;
// add new cell to output
gridSvg += `<path class="${className}" d="M ${shiftX} ${shiftY} h${offsetX} v${offsetY} h${-offsetX}z"/>\n`;
// increment x offset for next column
shiftX += offsetX;
currentCol++;
}
//close group
gridSvg += '\n</g>';
// create new svg
let nameSpace = 'http://www.w3.org/2000/svg';
let xlink = 'http://www.w3.org/1999/xlink';
let gridSvgEl = document.createElementNS(nameSpace, 'svg');
gridSvgEl.setAttribute('xmlns', nameSpace);
gridSvgEl.setAttribute('xmlns:xlink', xlink);
gridSvgEl.setAttribute('overflow', 'visible');
// add stroke width to viewBox to avoid cropped outer strokes
gridSvgEl.setAttribute('viewBox', [0, 0, width, height].join(' '));
gridSvgEl.innerHTML = gridSvg;
// optional: render grid
if (render) {
document.body.appendChild(gridSvgEl);
}
return gridSvgEl;
}
svg {
display: block;
overflow: visible;
width: 75vmin
}
.row-3 {
fill: #eee
}
.col-2 {
fill: #fee
}
.col:hover,
.highlight {
fill: lime
}
The gridPaths() function loops through the total amount of columns/cells. It also adds class names according to the current row and column index.
This way you can select individual columns and rows in css or js.
All paths are grouped in a <g> element for styling – you could also style each column in css alone.
Using the answer from this thread I was able to draw a semicircle (arc):
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
console.log(d)
return d;
}
window.onload = function() {
document.getElementById("arc1").setAttribute("d", describeArc(100, 100, 50, -90, 90));
};
<svg width="1000" height="1000">
<path id="arc1" fill="red" stroke="#446688" stroke-width="2" />
</svg>
What I'm trying to achieve is to be able to draw an SVG as a path consistent with many arcs (semicircles) and be able to set fill on them.
Something like this:
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 A 10 10 0 0 1 100 100 M 100 100 A 10 10 0 0 1 150 100 M 150 100 A 10 10 0 0 1 200 100 M 200 100 A 10 10 0 0 1 250 100" fill="red" stroke="blue" stroke-width="3" />
</svg>
Is there a better way to achieve a simpler path? For now, it looks like this:
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 A 10 10 0 0 1 100 100 M 100 100 A 10 10 0 0 1 150 100 M 150 100 A 10 10 0 0 1 200 100 M 200 100 A 10 10 0 0 1 250 100" fill="red" stroke="blue" stroke-width="3" />
</svg>
Or do I have to generate a longer and longer path when there are, let's say, 30 semicircles?
Edit: the IE9+ support is required. Also, those elements will be clickable, draggable and controllable. By controllable I mean that their number and size will change when mouse clicking/moving.
I choose my first approach with a dynamic very long path.
Thanks!
For this I would use lower case commands. For example this is drawing the arc you need: an arc with a radius of 25 and an ending point 50 units ( 2 * 25 ) away from the starting point of the arc.
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 a 25 25 0 0 1 50 0" fill="red" stroke="blue" stroke-width="3" />
</svg>
In order to get a path of 4 arcs you need to repeat the arc (a 25 25 0 0 1 50 0) 4 times something like this:
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 a 25 25 0 0 1 50 0
a 25 25 0 0 1 50 0
a 25 25 0 0 1 50 0
a 25 25 0 0 1 50 0 " fill="red" stroke="blue" stroke-width="3" />
</svg>
It's easy to see how you can use javascript to generate the d attribute you need:
let d ="M 50 100";
for(let i=0; i<4;i++){d +="a 25 25 0 0 1 50 0 "}
document.querySelector("path").setAttribute("d",d);
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100" fill="red" stroke="blue" stroke-width="3" />
</svg>
You can use a vanilla JavaScript Web Component (supported in all modern Browsers) to create the SVG
Your Custom Element <svg-arcs repeat="7"></svg-arcs> then creates:
<style>
svg { background: pink }
svg path { stroke-width: 3 }
</style>
<svg-arcs repeat="30"></svg-arcs>
<script>
customElements.define("svg-arcs", class extends HTMLElement {
connectedCallback() {
let repeat = this.getAttribute("repeat") || 5;
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
for (let x = 0; x < repeat; x++) {
let path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", `M${3 + 50*x} 100 A 10 10 0 0 1 ${50+50*x} 100`);
path.setAttribute("fill", "red");
path.setAttribute("stroke", "blue");
svg.append(path);
}
svg.setAttribute("viewBox", `0 0 ${50*repeat + 3} 150`);
this.append(svg);
}
})
</script>
For more dynamic control over individual arcs see the Web Component in SO post:
Firefox: shadow-DOM compatibility
You could use a pattern and size your patterned object appropriately. Here is one that accomodates 4 iterations.
Edit & Update:
If you want those arcs to be independently clickable/draggable, then they need to be separately addressable in the DOM. The "use" element might be what you're looking for.
svg {
background: grey;
}
<svg width="800px" height="600px">
<defs>
<path id="arc-template" d="M1.5 50 a 10 10 0 0 1 97 0" fill="red" stroke="blue" stroke-width="3" />
</defs>
<use id="arc1" href="#arc-template" x="50" y="100"/>
<use id="arc2" href="#arc-template" x="150" y="100"/>
<use id="arc3" href="#arc-template" x="250" y="100"/>
<use id="arc4" href="#arc-template" x="350" y="100"/>
</svg>
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 am following a tutorial from netuts about raphael js and I dont understand one of the examples, could some one possibly explain this to me in plainer english. I know I should learn more about javascript first.
for(var i = 0; i < 5; i+=1) {
var multiplier = i*5;
paper.circle(250 + (2*multiplier), 100 + multiplier, 50 - multiplier); }
Thanks! Very Much
The code will create five circles
for(var i = 0; i < 5; i+=1) { // loop five times => create five circles
var multiplier = i*5; // multiply i to increase the effect in the next lines
paper.circle( 250 + (2*multiplier), // the x coordinate of the new circle
100 + multiplier, // the y coordinate
50 - multiplier); // the radius
}
Results in this SVG element:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="556" height="109">
<desc>Created with Raphaël</desc>
<defs/>
<circle cx="250" cy="100" r="50" fill="none" stroke="#000"/>
<circle cx="260" cy="105" r="45" fill="none" stroke="#000"/>
<circle cx="270" cy="110" r="40" fill="none" stroke="#000"/>
<circle cx="280" cy="115" r="35" fill="none" stroke="#000"/>
<circle cx="290" cy="120" r="30" fill="none" stroke="#000"/>
</svg>
for(var i = 0; i < 5; i+=1) {
Iterate 5 times. Store the number of times iterated so far in the variable i. The "{" begins the loop.
var multiplier = i * 5;
Multiply i by 5 and store in a variable called multiplier.
paper.circle(250 + (2*multiplier), 100 + multiplier, 50 - multiplier);
Draw a circle with an x coordinate at 250 plus twice the multiplier, a y coordinate at 100 plus the multiplier and with a radius of 50 minus the multiplier. (Essentially a fancy way of getting distinct circles.)
}
End the loop.