Animate marquee on SVG curve - javascript

I have a text that is wrapping around an SVG circle which is scaling depending on window size – thanks to user enxaneta https://stackoverflow.com/a/56036245/10727821. I want to animate the text so that it would be revolving around the center like a marquee. For this, my code currently looks like this:
function Init(){
let wrap = document.getElementById('wrap');
let thePath = document.getElementById('thePath');
let ellipse = document.getElementById('ellipse');
let w = wrap.clientWidth;
let h = wrap.clientHeight;
ellipse.setAttributeNS(null,"viewBox",`0 0 ${w} ${h}`);
let d = `M${w/10},${h/2}A${4*w/10},${4*h/10} 0 0 0 ${9*w/10} ${5*h/10} A${4*w/10},${4*h/10} 0 0 0 ${w/10} ${5*h/10}`
thePath.setAttributeNS(null,"d", d)
}
window.setTimeout(function() {
Init();
window.addEventListener('resize', Init, false);
}, 15);
let so = 0
function Marquee(){
let tp = document.getElementById('tp');
requestAnimationFrame(Marquee)
tp.setAttributeNS(null,"startOffset",so+"%");
if(so >= 50){so = 0;}
so+= .05
}
Marquee()
<div id="wrap">
<svg id="ellipse" version="1.1" viewBox="0 0 1000 1000" preserveAspectRatio="none">
<path id="thePath" fill="transparent" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500" />
<text stroke="black" font-size="20">
<textPath xlink:href="#thePath" dy="5" id="tp">
Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
</textPath>
</text>
</svg>
</div>
This is working well, except the text gets "swallowed" at the end of the curve (see attached image). I'd like to have it make a full rotation without any interruption. I have tried changing the so variable to a negative value, but this ends up in the text being too far offset so it would only slowly creep onto the page. I was thinking to prepend a text fragment after a certain time, but this wouldn't take into account the startOffset movement and would probably not work…
Thankful for any hints, also those using JS libraries or plugins!

The main idea is that the path has to coil twice. And when the startOffset is at 50% you make it 0. Also because the length of the path is changing when you resize the window you need to recalculate the font-size. I hope it helps.
function Init() {
let w = wrap.clientWidth;
let h = wrap.clientHeight;
ellipse.setAttributeNS(null, "viewBox", `0 0 ${w} ${h}`);
let d = `M${w / 10},${h / 2}A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 *
w /
10} ${5 * h / 10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${w / 10} ${5 *
h /
10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 * w / 10} ${5 * h / 10} A${4 *
w /
10},${4 * h / 10} 0 0 0 ${w / 10} ${5 * h / 10}`;
thePath.setAttributeNS(null, "d", d);
let paths_length = thePath.getTotalLength();
tp.style.fontSize = paths_length / 205;
}
window.setTimeout(function() {
Init();
window.addEventListener("resize", Init, false);
}, 15);
let so = 0;
function Marquee() {
requestAnimationFrame(Marquee);
tp.setAttributeNS(null, "startOffset", so + "%");
if (so >= 50) {
so = 0;
}
so += 0.05;
}
Marquee();
#wrap{width:100vw; height:100vh}
svg {
background:#eee;
}
<div id="wrap">
<svg id="ellipse" version="1.1" viewBox="0 0 1000 1000">
<path id="thePath" fill="gold" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500 A400,400 0 0 0 900 500 A400,400 0 0 0 100 500" />
<text stroke="#000000" >
<textPath xlink:href="#thePath" dy="5" id="tp">
Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •
</textPath>
</text>
</svg>
</div>
UPDATE
The OP is commenting:
This snippet seems to be working fine for the use case, but when I try to apply it to another font family, the dimensions are off and the two loops start overlapping
One easy fix would be setting the attribute textLength equal to the length of the path divided by 2 (since the path is coiling twice - is twice as long as it should be). Also you need tu use lengthAdjust="spacingAndGlyphs" that is controlling how the text is stretched or compressed into that length.
function Init() {
let w = wrap.clientWidth;
let h = wrap.clientHeight;
ellipse.setAttributeNS(null, "viewBox", `0 0 ${w} ${h}`);
let d = `M${w / 10},${h / 2}A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 *
w /
10} ${5 * h / 10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${w / 10} ${5 *
h /
10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 * w / 10} ${5 * h / 10} A${4 *
w /
10},${4 * h / 10} 0 0 0 ${w / 10} ${5 * h / 10}`;
thePath.setAttributeNS(null, "d", d);
let path_length = thePath.getTotalLength();
//////////////////////////////////////////////////
tp.setAttributeNS(null,"textLength",path_length/2)
//////////////////////////////////////////////////
tp.style.fontSize = path_length / 200;
}
window.setTimeout(function() {
Init();
window.addEventListener("resize", Init, false);
}, 15);
let so = 0;
function Marquee() {
requestAnimationFrame(Marquee);
tp.setAttributeNS(null, "startOffset", so + "%");
if (so >= 50) {
so = 0;
}
so += 0.05;
}
Marquee();
#wrap{width:100vw; height:100vh}
svg {
background:#eee;
font-family:consolas;
}
<div id="wrap">
<svg id="ellipse" version="1.1" viewBox="0 0 1000 1000">
<path id="thePath" fill="gold" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500 A400,400 0 0 0 900 500 A400,400 0 0 0 100 500" />
<text stroke="#000000" >
<textPath xlink:href="#thePath" id="tp" lengthAdjust="spacingAndGlyphs">Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • </textPath>
</text>
</svg>
</div>
You may also need to add/remove some Coming Soon • if the text becomes too stretched / squished.
UPDATE 2
Apparently the last solution do not work on Firefox. This is another solution to this problem.
Initially I'm setting the font size much bigger than needed. Next I check if the text length is bigger than half path length, and if so I'm reducing the font size. I'm doing this in a while loop:
function Init() {
let w = wrap.clientWidth;
let h = wrap.clientHeight;
ellipse.setAttributeNS(null, "viewBox", `0 0 ${w} ${h}`);
let d = `M${w / 10},${h / 2}A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 *
w /
10} ${5 * h / 10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${w / 10} ${5 *
h /
10} A${4 * w / 10},${4 * h / 10} 0 0 0 ${9 * w / 10} ${5 * h / 10} A${4 *
w /
10},${4 * h / 10} 0 0 0 ${w / 10} ${5 * h / 10}`;
thePath.setAttributeNS(null, "d", d);
let path_length = thePath.getTotalLength();
//begin at a bigger size than needed
let font_size = 100;
ellipse.style.fontSize = font_size+"px";
// while the text length is bigger than half path length
while(tp.getComputedTextLength() > path_length / 2 ){
//reduce the font size
font_size -=.25;
//reset the font size
ellipse.style.fontSize = font_size+"px";
}
}
window.setTimeout(function() {
Init();
window.addEventListener("resize", Init, false);
}, 15);
let so = 0;
function Marquee() {
requestAnimationFrame(Marquee);
tp.setAttributeNS(null, "startOffset", so + "%");
if (so >= 50) {
so = 0;
}
so += 0.02;
}
Marquee();
html, body {
margin: 0;
height: 100%;
width: 100%;
}
body {
font-family: "Arimo", sans-serif;
}
#wrap{
width:100%;
height:100%;
position: fixed;
top: 0;
left: 0;
}
text {
text-transform: uppercase;
font-weight: lighter;
}
<div id="wrap">
<svg id="ellipse" version="1.1" viewBox="0 0 1000 1000">
<path id="thePath" fill="transparent" d="M100,500A400,400 0 0 0 900 500 A400,400 0 0 0 100 500" />
<text stroke="black">
<textPath xlink:href="#thePath" dy="5" id="tp" lengthAdjust="spacingAndGlyphs">Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon • Coming Soon •</textPath>
</text>
</svg>
</div>

Related

How to swap the position of two <path> elements inside an SVG

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>

Color squares of a grid JavaScript

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.

How to add a Fade out effect in an HTML canvas

I am trying to add a fade out effect (that happens after half a second after it has been drawn) to an arch in HTML Canvas.
I am able to draw a circle.
This is my code to draw the arch.
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.arc(Xcoord, Ycoord, 50, 0, 2 * Math.PI, false);
ctx.fill();
ctx.lineWidth = "4";
ctx.strokeStyle = "#000000";
ctx.stroke();
ctx.closePath();
Is there a way I can extend this to add a fade out effect that starts 0.5 seconds after it has been drawn.
My solution is based on pure JavaScript.
Set an interval.
Set opacity if none.
Reduce opacity until 0.
Clear interval after opacity reaches 0.
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#FF0000";
ctx.fillRect(0,0,200,100);
setTimeout(function(){
var fadeTarget = document.getElementById("myCanvas");
var fadeEffect = setInterval(function () {
if (!fadeTarget.style.opacity) {
fadeTarget.style.opacity = 1;
}
if (fadeTarget.style.opacity > 0) {
fadeTarget.style.opacity -= 0.02;
} else {
clearInterval(fadeEffect);
}
}, 20);
},0.5);
<canvas id="myCanvas" width="200" height="100"
style="border:1px solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
A great canvas 2d resource is MDN's CanvasRenderingContext2D
This is the second question on the same topic I have answered recently. The following answer on how to fade content on the canvas will explain how to render animated (fading) content.
Timing
The example below using the time passed to the mainLoop function by requestAnimationFrame creates the various time related animated FX. Time is stored in globalTime and used as needed
The animation timing is in the bottom function drawContent and uses the variables animateStart (how long before starting) and animateLen (how long the animation is)
Then from that a value from 0-1 (in animPos) is passed to the function donut.draw and used to animate. donut.draw also uses the globalTime to change the icing color.
var globalTime; // set in mainLoop
var startTime; // undefined means it is reset
var animateStart = 2500; // in ms time till animation starts 2.5 seconds
var animateLen = 10000; // in ms length of animation 10 seconds
function drawContent() {
var animPos = 0; // default anim pos
// check if animation need reset. If so set start time
if (startTime === undefined) { startTime = globalTime + animateStart }
// use the dif between global and start times to get the animation pos
const t = globalTime - startTime;
if (t >= 0 && t < animateLen) {
animPos = t / (animateLen / 2);
animPos = animPos > 1 ? 1- (animPos - 1) : animPos;
} else if(t >= animateLen) { startTime = undefined } // resets animation
donut.draw(Math.easeInOut(animPos));
}
Fading
The fading is performed on the sprinkles using globalAlpha. and there is also some fading on the shadow cast on the donut by the icing. (using globalAlpha and globalCompositeOperation = "source-atop" )
The highlight line on the icing is also faded by using the CSS color format rgba(255,255,255, 0.7) where the last value is the alpha amount (from 0 - 1).
Why the over the top demo?
I am not a fan of SVG and as Vivek Raju's SVG example is so pretty it may sway people away from the better canvas 2D or WebGL solutions, which can be far more dynamic and interesting.
So the example below recreates the same content using pure JS and the CanvasRenderingContext2D API.
Demo
Math.TAU = Math.PI * 2; // set up 2 PI
Math.PI90 = Math.PI / 2; // 90 deg
Math.easeInOut = (v, p = 2) => v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p)
Math.rand = (val) => Math.random() * val;
Math.randI = (val) => Math.random() * val | 0;
requestAnimationFrame(mainLoop);
CanvasRenderingContext2D.prototype.setStyle = function(style) {
for(const [key, value] of Object.entries(style)) { this[key] = value }
}
const ctx = canvas.getContext("2d");
const W = canvas.width = innerWidth; // size canvas to page
const H = canvas.height = innerHeight * 1.5;
const sprinkle = (x, y, col, size, ang) => ({x,y,col,size,ang});
const donut = {
styles: {
main: { // the icing
lineWidth: 3,
strokeStyle: "#000",
fillStyle: "#FCC",
},
inner: { // the donut
lineWidth: 2,
strokeStyle: "#000",
fillStyle: "#DB8",
},
highlight: {
lineWidth: 2,
strokeStyle: "rgba(255,255,255,0.8)",
},
sprinkle: {
lineWidth: 2,
strokeStyle: "#000",
}
},
x: W * (1/2),
y: H * (1/3) - H * (1/16) ,
r: Math.min(W * (1 / 2.5), H * (1 / 4)),
highlightOffset: 4,
drips:11, // best that this value is odd and >= 3
sprinkleCount: 7,
resetDrips: 0,
init() {
var i = 0;
this.dripLens = [];
this.sprinkles = [];
while (i < this.drips) {
this.dripLens.push(Math.rand(1) + ((i + 1) % 2));
i++;
}
i = 0;
const dr = this.r / this.drips;
const innerR = this.r * (1/3);
while (i < this.sprinkleCount) {
const a = (i / (this.sprinkleCount - 1)) * (Math.PI + 1) + Math.PI - 0.5;
const dist = Math.rand(this.r - innerR) * 0.4;
const col = "#" + Math.randI(0xFFF).toString(16).padStart(3,"0").replace(/[0-9]/g,"A");
const ang = Math.rand(Math.TAU);
const x = Math.cos(a) * (dist + innerR * 1.75) + this.x;
const y = Math.sin(a) * (dist + innerR * 1.75) + this.y;
this.sprinkles.push(sprinkle(x,y,col, dr*0.85, ang));
i++;
}
},
draw(dripScale) {
const y = this.y + this.r * (1/1.5);
const dr = this.r / this.drips;
const dripLen = this.r * (1/2.3) * dripScale;
const innerR = this.r * (1/3);
const ho = this.highlightOffset;
const lw = this.styles.main.lineWidth;
var x = this.x + this.r - dr, xx = x - dr * 2;
var i = 0;
ctx.setStyle(this.styles.inner);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - lw, 0, Math.TAU);
ctx.arc(this.x, this.y, innerR + lw, 0, Math.TAU);
ctx.fill("evenodd");
ctx.stroke();
ctx.setStyle(this.styles.main);
const lum = Math.sin(globalTime / (250 * 90)) * 20 + 50;
ctx.fillStyle = "hsl("+(globalTime / 250) % 360 +",100%,"+lum+"%)"
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, Math.PI, Math.TAU);
while (i < this.drips) {
const dy = this.dripLens[i++] * dripLen;
if (i % 2) {
ctx.arc(x, y + dy, dr, 0, Math.PI);
} else {
ctx.arc(x, y + dy, dr,0, -Math.PI, true);
}
x -= dr * 2;
}
ctx.closePath();
ctx.moveTo(this.x + innerR, this.y);
ctx.arc(this.x, this.y, innerR, 0, Math.TAU);
ctx.globalCompositeOperation = "source-atop";
ctx.globalAlpha = 0.25;
ctx.lineWidth = this.styles.main.lineWidth * 3;
ctx.stroke();
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.lineWidth = this.styles.main.lineWidth;
ctx.fill("evenodd");
ctx.stroke();
ctx.setStyle(this.styles.highlight);
ctx.beginPath();
ctx.arc(this.x, this.y, innerR + ho, 0, Math.PI90);
i = 0;
while (i < this.drips - 1) {
const dy1 = this.dripLens[i++] * dripLen;
const dy2 = this.dripLens[i++] * dripLen;
ctx.moveTo(xx + dr + ho, y + dy1);
ctx.arc(xx, y + dy2, dr + ho, 0, -Math.PI90, true);
xx -= dr * 4;
}
const dy1 = this.dripLens[i] * dripLen;
ctx.moveTo(xx + dr + ho, y + dy1);
ctx.arc(this.x, this.y, this.r - ho, Math.PI , Math.PI + Math.PI90);
ctx.stroke();
i = 4;
ctx.setStyle(this.styles.sprinkle);
for (const spr of this.sprinkles) {
const dy = ((spr.y - (this.y - this.r)) / this.r) * 5 * dripScale;
ctx.globalAlpha = Math.sin(globalTime / ((1 + i) * 200)) * 0.4 + 0.6;
ctx.setTransform(1,0,0,1, spr.x, spr.y + dy);
ctx.rotate(spr.ang + dy * 0.2 * (i++ % 2 ? -1 : 1));
ctx.fillStyle = spr.col;
ctx.beginPath();
ctx.arc(0, -spr.size, spr.size / 2, Math.PI, Math.TAU);
ctx.arc(0, spr.size, spr.size / 2, 0, Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
if (this.resetDrips === 0 && dripScale < 0.01) {
this.resetDrips = 1;
i = 0;
while (i < this.dripLens.length) {
this.dripLens[i] = Math.rand(1) + 0.1 + ((i + 1) % 2);
i++;
}
} else if (this.resetDrips === 1 && dripScale > 0.9) {
this.resetDrips = 0;
}
}
};
donut.init();
var globalTime;
var startTime;
var animateStart = 2500; // 2.5 seconds
var animateLen = 10000; // 10 seconds
function mainLoop(time) {
globalTime = time;
ctx.globalAlpha = 1;
ctx.clearRect(0, 0, W, H);
drawContent();
requestAnimationFrame(mainLoop);
}
function drawContent() {
var animPos = 0;
if (startTime === undefined) { startTime = globalTime + animateStart }
const t = globalTime - startTime;
if (t >= 0 && t < animateLen) {
animPos = t / (animateLen / 2);
animPos = animPos > 1 ? 1- (animPos - 1) : animPos;
} else if(t >= animateLen) { startTime = undefined } // resets animation
donut.draw(Math.easeInOut(animPos));
}
body {
padding: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
Hover to the SVG file:
body {
text-align: center;
}
#donut-icing {
fill: #FA9CB6;
transition: fill 3s ease-out;
}
#donut:hover {
cursor: pointer;
}
#donut:hover #donut-icing {
fill: #4a8af4;
}
<html>
<head>
<title>SVG Donut Animated on Hover with CSS / Sass</title>
</head>
<body>
<svg id="donut" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="204" height="203" viewBox="0 0 204 203">
<defs>
<linearGradient id="donut-a" x1="50%" x2="50%" y1="59.542%" y2="98.874%">
<stop offset="0%" stop-color="#F8F5EA"/>
<stop offset="100%" stop-color="#E1D6B6"/>
</linearGradient>
<path id="donut-icing" fill="#FA9CB6" d="M200,100 C200,44.771525 155.228475,0 100,0 C44.771525,0 0,44.771525 0,100 M0,100 L0,156.203125 C0,159.048145 2.12004279,161.359375 4.63917526,161.359375 C7.35102845,161.359375 9.47049623,159.052855 9.27835052,156.203125 L9.27835052,148.46875 C9.4934916,145.61902 11.6018043,143.3125 14.4329897,143.3125 C16.8052584,143.3125 18.9141431,145.62373 19.0721649,148.46875 L19.0721649,156.203125 L19.0721649,168.0625 C18.9141431,170.90752 21.0230278,173.21875 23.7113402,173.21875 C26.2264819,173.21875 28.3347946,170.91223 28.3505155,168.0625 L28.3505155,161.359375 L28.3505155,154.140625 C28.3347946,151.291053 30.4431073,148.984375 32.9896907,148.984375 C35.6465615,148.984375 37.7554461,151.295764 37.628866,154.140625 L37.628866,193.84375 C37.7554461,196.68877 39.8643308,199 42.2680412,199 C45.067785,199 47.1760977,196.69348 47.4226804,193.84375 L47.4226804,131.453125 C47.1760977,128.603395 49.2844103,126.296875 52.0618557,126.296875 C54.4878645,126.296875 56.5967492,128.608105 56.7010309,131.453125 L56.7010309,176.3125 C56.5967492,179.15752 58.7056338,181.46875 61.3402062,181.46875 C63.909088,181.46875 66.0174007,179.16223 65.9793814,176.3125 L65.9793814,141.765625 C66.0174007,138.916053 68.1257134,136.609375 70.6185567,136.609375 C73.3291675,136.609375 75.4380522,138.920764 75.257732,141.765625 L75.257732,160.84375 C75.4380522,163.688611 77.5469369,166 79.8969072,166 C82.750391,166 84.8587037,163.693322 85.0515464,160.84375 L85.0515464,153.625 C84.8587037,150.775428 86.9670164,148.46875 89.6907216,148.46875 C92.1704706,148.46875 94.2793552,150.780139 94.3298969,153.625 L94.8453608,187.140625 C94.8027248,189.985486 97.14593,192.296875 100,192.296875 C102.927546,192.296875 105.270115,189.990197 105.154639,187.140625 L104.639175,149.5 C104.746746,145.61902 106.855058,143.3125 109.278351,143.3125 C112.058513,143.3125 114.167397,145.62373 113.917526,148.46875 L113.917526,168.0625 C114.167397,170.90752 116.276282,173.21875 119.072165,173.21875 C121.479736,173.21875 123.588049,170.91223 123.71134,168.0625 L123.71134,161.359375 L123.71134,154.140625 C123.588049,151.291053 125.696362,148.984375 128.350515,148.984375 C130.899816,148.984375 133.0087,151.295764 132.989691,154.140625 L132.989691,193.84375 C133.0087,196.68877 135.117585,199 137.628866,199 C140.321039,199 142.429352,196.69348 142.268041,193.84375 L142.268041,138.671875 L142.268041,131.453125 C142.429352,128.603395 144.537665,126.296875 146.907216,126.296875 C149.741119,126.296875 151.850003,128.608105 152.061856,131.453125 L152.061856,139.1875 L152.061856,171.15625 C151.850003,174.00127 153.958888,176.3125 156.701031,176.3125 C159.162342,176.3125 161.270655,174.00598 161.340206,171.15625 L161.340206,144.859375 L161.340206,141.765625 C161.270655,138.916053 163.378968,136.609375 165.979381,136.609375 C168.582422,136.609375 170.691306,138.920764 170.618557,141.765625 L170.618557,144.859375 L170.618557,176.3125 C170.629257,179.157361 172.738142,181.46875 175.257732,181.46875 C177.941596,181.46875 180.049909,179.162072 179.896907,176.3125 L179.896907,153.625 C180.111958,150.775428 182.220271,148.46875 185.051546,148.46875 C187.423725,148.46875 189.532609,150.780139 189.690722,153.625 L189.690722,163.421875 C189.532609,166.266736 191.875815,168.578125 194.845361,168.578125 C197.65743,168.578125 200,166.271447 200,163.421875 L200,100"/>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(2 2)">
<circle cx="100" cy="100" r="100" fill="url(#donut-a)"/>
<path stroke="#302434" stroke-linecap="square" stroke-width="2" d="M180,160 C180.163288,159.782085 185.314605,152.363686 187,149 L180,160 Z M48,185 C63.1555058,194.519493 80.9387639,200 99.9750338,200 C111.534055,200 122.652906,197.969392 133,194.15209"/>
<mask id="donut-c" fill="#fff">
<use xlink:href="#donut-icing"/>
</mask>
<use fill="#FA9CB6" xlink:href="#donut-icing"/>
<path d="M200,100 C200,44.771525 155.228475,0 100,0 C44.771525,0 0,44.771525 0,100 M0,100 L0,156.203125 C0,159.048145 2.12004279,161.359375 4.63917526,161.359375 C7.35102845,161.359375 9.47049623,159.052855 9.27835052,156.203125 L9.27835052,148.46875 C9.4934916,145.61902 11.6018043,143.3125 14.4329897,143.3125 C16.8052584,143.3125 18.9141431,145.62373 19.0721649,148.46875 L19.0721649,156.203125 L19.0721649,168.0625 C18.9141431,170.90752 21.0230278,173.21875 23.7113402,173.21875 C26.2264819,173.21875 28.3347946,170.91223 28.3505155,168.0625 L28.3505155,161.359375 L28.3505155,154.140625 C28.3347946,151.291053 30.4431073,148.984375 32.9896907,148.984375 C35.6465615,148.984375 37.7554461,151.295764 37.628866,154.140625 L37.628866,193.84375 C37.7554461,196.68877 39.8643308,199 42.2680412,199 C45.067785,199 47.1760977,196.69348 47.4226804,193.84375 L47.4226804,131.453125 C47.1760977,128.603395 49.2844103,126.296875 52.0618557,126.296875 C54.4878645,126.296875 56.5967492,128.608105 56.7010309,131.453125 L56.7010309,176.3125 C56.5967492,179.15752 58.7056338,181.46875 61.3402062,181.46875 C63.909088,181.46875 66.0174007,179.16223 65.9793814,176.3125 L65.9793814,141.765625 C66.0174007,138.916053 68.1257134,136.609375 70.6185567,136.609375 C73.3291675,136.609375 75.4380522,138.920764 75.257732,141.765625 L75.257732,160.84375 C75.4380522,163.688611 77.5469369,166 79.8969072,166 C82.750391,166 84.8587037,163.693322 85.0515464,160.84375 L85.0515464,153.625 C84.8587037,150.775428 86.9670164,148.46875 89.6907216,148.46875 C92.1704706,148.46875 94.2793552,150.780139 94.3298969,153.625 L94.8453608,187.140625 C94.8027248,189.985486 97.14593,192.296875 100,192.296875 C102.927546,192.296875 105.270115,189.990197 105.154639,187.140625 L104.639175,149.5 C104.746746,145.61902 106.855058,143.3125 109.278351,143.3125 C112.058513,143.3125 114.167397,145.62373 113.917526,148.46875 L113.917526,168.0625 C114.167397,170.90752 116.276282,173.21875 119.072165,173.21875 C121.479736,173.21875 123.588049,170.91223 123.71134,168.0625 L123.71134,161.359375 L123.71134,154.140625 C123.588049,151.291053 125.696362,148.984375 128.350515,148.984375 C130.899816,148.984375 133.0087,151.295764 132.989691,154.140625 L132.989691,193.84375 C133.0087,196.68877 135.117585,199 137.628866,199 C140.321039,199 142.429352,196.69348 142.268041,193.84375 L142.268041,138.671875 L142.268041,131.453125 C142.429352,128.603395 144.537665,126.296875 146.907216,126.296875 C149.741119,126.296875 151.850003,128.608105 152.061856,131.453125 L152.061856,139.1875 L152.061856,171.15625 C151.850003,174.00127 153.958888,176.3125 156.701031,176.3125 C159.162342,176.3125 161.270655,174.00598 161.340206,171.15625 L161.340206,144.859375 L161.340206,141.765625 C161.270655,138.916053 163.378968,136.609375 165.979381,136.609375 C168.582422,136.609375 170.691306,138.920764 170.618557,141.765625 L170.618557,144.859375 L170.618557,176.3125 C170.629257,179.157361 172.738142,181.46875 175.257732,181.46875 C177.941596,181.46875 180.049909,179.162072 179.896907,176.3125 L179.896907,153.625 C180.111958,150.775428 182.220271,148.46875 185.051546,148.46875 C187.423725,148.46875 189.532609,150.780139 189.690722,153.625 L189.690722,163.421875 C189.532609,166.266736 191.875815,168.578125 194.845361,168.578125 C197.65743,168.578125 200,166.271447 200,163.421875 L200,100" mask="url(#donut-c)"/>
<path stroke="#302434" stroke-linecap="square" stroke-width="3" d="M200 100C200 44.771525 155.228475 0 100 0 44.771525 0 0 44.771525 0 100M0 100L0 156.203125C0 159.048145 2.12004279 161.359375 4.63917526 161.359375 7.35102845 161.359375 9.47049623 159.052855 9.27835052 156.203125L9.27835052 148.46875C9.4934916 145.61902 11.6018043 143.3125 14.4329897 143.3125 16.8052584 143.3125 18.9141431 145.62373 19.0721649 148.46875L19.0721649 156.203125 19.0721649 168.0625C18.9141431 170.90752 21.0230278 173.21875 23.7113402 173.21875 26.2264819 173.21875 28.3347946 170.91223 28.3505155 168.0625L28.3505155 161.359375 28.3505155 154.140625C28.3347946 151.291053 30.4431073 148.984375 32.9896907 148.984375 35.6465615 148.984375 37.7554461 151.295764 37.628866 154.140625L37.628866 193.84375C37.7554461 196.68877 39.8643308 199 42.2680412 199 45.067785 199 47.1760977 196.69348 47.4226804 193.84375L47.4226804 131.453125C47.1760977 128.603395 49.2844103 126.296875 52.0618557 126.296875 54.4878645 126.296875 56.5967492 128.608105 56.7010309 131.453125L56.7010309 176.3125C56.5967492 179.15752 58.7056338 181.46875 61.3402062 181.46875 63.909088 181.46875 66.0174007 179.16223 65.9793814 176.3125L65.9793814 141.765625C66.0174007 138.916053 68.1257134 136.609375 70.6185567 136.609375 73.3291675 136.609375 75.4380522 138.920764 75.257732 141.765625L75.257732 160.84375C75.4380522 163.688611 77.5469369 166 79.8969072 166 82.750391 166 84.8587037 163.693322 85.0515464 160.84375L85.0515464 153.625C84.8587037 150.775428 86.9670164 148.46875 89.6907216 148.46875 92.1704706 148.46875 94.2793552 150.780139 94.3298969 153.625L94.8453608 187.140625C94.8027248 189.985486 97.14593 192.296875 100 192.296875 102.927546 192.296875 105.270115 189.990197 105.154639 187.140625L104.639175 149.5C104.746746 145.61902 106.855058 143.3125 109.278351 143.3125 112.058513 143.3125 114.167397 145.62373 113.917526 148.46875L113.917526 168.0625C114.167397 170.90752 116.276282 173.21875 119.072165 173.21875 121.479736 173.21875 123.588049 170.91223 123.71134 168.0625L123.71134 161.359375 123.71134 154.140625C123.588049 151.291053 125.696362 148.984375 128.350515 148.984375 130.899816 148.984375 133.0087 151.295764 132.989691 154.140625L132.989691 193.84375C133.0087 196.68877 135.117585 199 137.628866 199 140.321039 199 142.429352 196.69348 142.268041 193.84375L142.268041 138.671875 142.268041 131.453125C142.429352 128.603395 144.537665 126.296875 146.907216 126.296875 149.741119 126.296875 151.850003 128.608105 152.061856 131.453125L152.061856 139.1875 152.061856 171.15625C151.850003 174.00127 153.958888 176.3125 156.701031 176.3125 159.162342 176.3125 161.270655 174.00598 161.340206 171.15625L161.340206 144.859375 161.340206 141.765625C161.270655 138.916053 163.378968 136.609375 165.979381 136.609375 168.582422 136.609375 170.691306 138.920764 170.618557 141.765625L170.618557 144.859375 170.618557 176.3125C170.629257 179.157361 172.738142 181.46875 175.257732 181.46875 177.941596 181.46875 180.049909 179.162072 179.896907 176.3125L179.896907 153.625C180.111958 150.775428 182.220271 148.46875 185.051546 148.46875 187.423725 148.46875 189.532609 150.780139 189.690722 153.625L189.690722 163.421875C189.532609 166.266736 191.875815 168.578125 194.845361 168.578125 197.65743 168.578125 200 166.271447 200 163.421875L200 100"/>
<circle cx="100.5" cy="100.5" r="26.5" fill="#FFF" stroke="#302434" stroke-linecap="square" stroke-width="3"/>
<path stroke="#FFF" stroke-linecap="round" stroke-width="2" d="M103,132 L103,132 C119.016258,132 132,119.016258 132,103" opacity=".7"/>
<path fill="#F7DA71" stroke="#302434" stroke-width="2.062" d="M168.854871,80.8314322 L168.854871,81.1126307 C168.854871,82.6700566 170.123055,83.9411037 171.680144,83.9411037 L183.967742,83.9411037 C185.519686,83.9411037 186.793015,82.6697696 186.793015,81.1126307 L186.793015,80.8314322 C186.793015,79.2740064 185.52483,78.0029593 183.967742,78.0029593 L171.680144,78.0029593 C170.1282,78.0029593 168.854871,79.2742934 168.854871,80.8314322 Z" transform="rotate(-134 177.824 80.972)"/>
<path fill="#FFAD38" stroke="#302434" stroke-width="2.062" d="M170.784479,122.710815 L170.784479,122.992013 C170.784479,124.549439 172.052663,125.820486 173.609752,125.820486 L185.89735,125.820486 C187.449294,125.820486 188.722623,124.549152 188.722623,122.992013 L188.722623,122.710815 C188.722623,121.153389 187.454438,119.882342 185.89735,119.882342 L173.609752,119.882342 C172.057808,119.882342 170.784479,121.153676 170.784479,122.710815 Z" transform="rotate(25 179.754 122.851)"/>
<path fill="#9FC1EA" stroke="#302434" stroke-width="2.062" d="M137.779864,103.591935 L137.779864,103.873134 C137.779864,105.430559 139.048049,106.701607 140.605137,106.701607 L152.892735,106.701607 C154.444679,106.701607 155.718008,105.430272 155.718008,103.873134 L155.718008,103.591935 C155.718008,102.034509 154.449824,100.763462 152.892735,100.763462 L140.605137,100.763462 C139.053193,100.763462 137.779864,102.034796 137.779864,103.591935 Z" transform="rotate(-73 146.749 103.733)"/>
<path fill="#FFAD38" stroke="#302434" stroke-width="2.062" d="M69.2022598,19.4388294 L69.2022598,19.7200279 C69.2022598,21.2774537 70.4704445,22.5485008 72.0275331,22.5485008 L84.3151307,22.5485008 C85.8670745,22.5485008 87.1404041,21.2771667 87.1404041,19.7200279 L87.1404041,19.4388294 C87.1404041,17.8814035 85.8722194,16.6103564 84.3151307,16.6103564 L72.0275331,16.6103564 C70.4755893,16.6103564 69.2022598,17.8816905 69.2022598,19.4388294 Z" transform="rotate(-41 78.171 19.58)"/>
<path fill="#F45467" stroke="#302434" stroke-width="2.062" d="M63.4327156,63.1481419 L63.4327156,63.4293404 C63.4327156,64.9867663 64.7009003,66.2578133 66.2579889,66.2578133 L78.5455866,66.2578133 C80.0975304,66.2578133 81.3708599,64.9864793 81.3708599,63.4293404 L81.3708599,63.1481419 C81.3708599,61.5907161 80.1026752,60.319669 78.5455866,60.319669 L66.2579889,60.319669 C64.7060452,60.319669 63.4327156,61.5910031 63.4327156,63.1481419 Z" transform="rotate(-51 72.402 63.289)"/>
<path fill="#9FC1EA" stroke="#302434" stroke-width="2.062" d="M93.4950295,44.5196548 L93.4950295,44.8008533 C93.4950295,46.3582791 94.7632142,47.6293262 96.3203028,47.6293262 L108.6079,47.6293262 C110.159844,47.6293262 111.433174,46.3579921 111.433174,44.8008533 L111.433174,44.5196548 C111.433174,42.9622289 110.164989,41.6911819 108.6079,41.6911819 L96.3203028,41.6911819 C94.768359,41.6911819 93.4950295,42.962516 93.4950295,44.5196548 Z" transform="rotate(-120 102.464 44.66)"/>
<path fill="#F45467" stroke="#302434" stroke-width="2.062" d="M116.763462,17.6083368 L116.763462,17.8895353 C116.763462,19.4469612 118.031647,20.7180082 119.588736,20.7180082 L131.876333,20.7180082 C133.428277,20.7180082 134.701607,19.4466741 134.701607,17.8895353 L134.701607,17.6083368 C134.701607,16.050911 133.433422,14.7798639 131.876333,14.7798639 L119.588736,14.7798639 C118.036792,14.7798639 116.763462,16.051198 116.763462,17.6083368 Z" transform="rotate(-163 125.733 17.749)"/>
<path fill="#9FC1EA" stroke="#302434" stroke-width="2.062" d="M31.4950295,42.5196548 L31.4950295,42.8008533 C31.4950295,44.3582791 32.7632142,45.6293262 34.3203028,45.6293262 L46.6079004,45.6293262 C48.1598442,45.6293262 49.4331738,44.3579921 49.4331738,42.8008533 L49.4331738,42.5196548 C49.4331738,40.9622289 48.1649891,39.6911819 46.6079004,39.6911819 L34.3203028,39.6911819 C32.768359,39.6911819 31.4950295,40.962516 31.4950295,42.5196548 Z" transform="rotate(-120 40.464 42.66)"/>
<path fill="#9FC1EA" stroke="#302434" stroke-width="2.062" d="M7.00295932,122.683344 L7.00295932,122.964542 C7.00295932,124.521968 8.27114404,125.793015 9.82823266,125.793015 L22.1158303,125.793015 C23.6677741,125.793015 24.9411037,124.521681 24.9411037,122.964542 L24.9411037,122.683344 C24.9411037,121.125918 23.6729189,119.854871 22.1158303,119.854871 L9.82823266,119.854871 C8.27628888,119.854871 7.00295932,121.126205 7.00295932,122.683344 Z" transform="rotate(44 15.972 122.824)"/>
<path fill="#9FC1EA" stroke="#302434" stroke-width="2.062" d="M143.573598,38.5351135 L143.573598,38.816312 C143.573598,40.3737379 144.841783,41.644785 146.398871,41.644785 L158.686469,41.644785 C160.238413,41.644785 161.511742,40.3734509 161.511742,38.816312 L161.511742,38.5351135 C161.511742,36.9776877 160.243558,35.7066406 158.686469,35.7066406 L146.398871,35.7066406 C144.846928,35.7066406 143.573598,36.9779747 143.573598,38.5351135 Z" transform="rotate(10 152.543 38.676)"/>
<path fill="#FFAD38" stroke="#302434" stroke-width="2.062" d="M13.7798639,78.5919351 L13.7798639,78.8731336 C13.7798639,80.4305595 15.0480486,81.7016065 16.6051372,81.7016065 L28.8927349,81.7016065 C30.4446787,81.7016065 31.7180082,80.4302725 31.7180082,78.8731336 L31.7180082,78.5919351 C31.7180082,77.0345093 30.4498235,75.7634622 28.8927349,75.7634622 L16.6051372,75.7634622 C15.0531935,75.7634622 13.7798639,77.0347963 13.7798639,78.5919351 Z" transform="rotate(-73 22.749 78.733)"/>
<path fill="#FFAD38" stroke="#302434" stroke-width="2.062" d="M133.763462,61.6083368 L133.763462,61.8895353 C133.763462,63.4469612 135.031647,64.7180082 136.588736,64.7180082 L148.876333,64.7180082 C150.428277,64.7180082 151.701607,63.4466741 151.701607,61.8895353 L151.701607,61.6083368 C151.701607,60.050911 150.433422,58.7798639 148.876333,58.7798639 L136.588736,58.7798639 C135.036792,58.7798639 133.763462,60.051198 133.763462,61.6083368 Z" transform="rotate(-17 142.733 61.749)"/>
<path fill="#F7DA71" stroke="#302434" stroke-width="2.062" d="M40.1448483,100.300712 L40.1448483,100.58191 C40.1448483,102.139336 41.4130331,103.410383 42.9701217,103.410383 L55.2577193,103.410383 C56.8096631,103.410383 58.0829927,102.139049 58.0829927,100.58191 L58.0829927,100.300712 C58.0829927,98.7432859 56.8148079,97.4722388 55.2577193,97.4722388 L42.9701217,97.4722388 C41.4181779,97.4722388 40.1448483,98.7435729 40.1448483,100.300712 Z" transform="rotate(54 49.114 100.441)"/>
<path stroke="#302434" stroke-linecap="square" stroke-width="2" d="M143 190C153.043627 185.195415 162.158445 178.746675 170 171M28.7212226 170.713933C31.3439197 173.284559 34.1076215 175.717271 37 178M11.5217511 145.997463C13.5503966 149.823796 15.8170146 153.505818 18.3020045 157.024023"/>
<path stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 140C20.5567785 140 22.8296876 142.321236 23 145.178571L23 152.946429 23 169M37 146C39.5567785 146 41.8296876 148.311968 42 151.157895L42 158.894737 42 195M56 123C58.5567785 123 60.8296876 125.305227 61 128.142857L61 135.857143 61 177M151 122C153.556779 122 155.829688 124.311968 156 127.157895L156 134.894737 156 171M74 133C76.5567785 133 78.8296876 135.3242 79 138.185185L79 145.962963 79 161M94 145C96.5567785 145 98.8296876 147.3242 99 150.185185L99 157.962963 99 187M113 139C115.556779 139 117.829688 141.280512 118 144.087719L118 151.719298 118 168M132 145C134.556779 145 136.829688 147.311968 137 150.157895L137 157.894737 137 194M170 133C172.301101 133 174.346719 135.293314 174.5 138.116279L174.5 145.790698 175 177M189 144C191.556779 144 193.829688 146.301766 194 149.135135L194 156.837838 194 163" opacity=".7"/>
</g>
</svg>
</body>
</html>

Uncaught TypeError: Cannot read property 'setAttribute' of null

Still learning JS since I'm a noob so bear with me.
I have a web app with 2 circular SVG gauges that currently work and I receive this issue before users log-in.
My Problem: I get
"Uncaught TypeError: Cannot read property 'setAttribute' of null" firing off like crazy
for pathElementTwo.setAttribute('d', describeArc(26, 0, arcTwo)); in console because the area where that specific arc only needs to load when users log in. This only fires off like crazy in console before login, after login it disappears.
How can I fix this issue so console doesn't fire off like crazy before users log in?
Any help is gladly appreciated. Thanks!
JS
function describeArc(radius, startAngle, endAngle) {
// Helper function, used to convert the (startAngle, endAngle) arc
// dexcription into cartesian coordinates that are used for the
// SVG arc descriptor.
function polarToCartesian(radius, angle) {
return {
x: radius * Math.cos(angle),
y: radius * Math.sin(angle),
};
}
// Generate cartesian coordinates for the start and end of the arc.
var start = polarToCartesian(radius, endAngle);
var end = polarToCartesian(radius, startAngle);
// Determine if we're drawing an arc that's larger than a 1/2 circle.
var largeArcFlag = endAngle - startAngle <= Math.PI ? 0 : 1;
// Generate the SVG arc descriptor.
var d = [
'M', start.x, start.y,
'A', radius, radius, 1, largeArcFlag, 0, end.x, end.y
].join(' ');
return d;
}
var arc = 0;
var arcTwo = 0;
setInterval(function() {
// Update the ticker progress.
arc += Math.PI / 1000;
arcTwo += Math.PI / 1000;
if (arc >= 2 * Math.PI) { arc = 0; }
if (arcTwo >= 2 * Math.PI) { arcTwo = 0; }
// Update the SVG arc descriptor.
var pathElement = document.getElementById('arc-path');
var pathElementTwo = document.getElementById('arc-path-two');
pathElement.setAttribute('d', describeArc(26, 0, arc));
pathElementTwo.setAttribute('d', describeArc(26, 0, arcTwo));
}, 400 / 0)
HTML
<div class="ticker-body">
<svg viewBox="19, -19 65 35" class="gauge-background"
fill="none">
<circle r="10"/>
</svg>
<svg viewBox="-39, -39 700 75" class="gauge" fill="none">
<path id="arc-path" transform="rotate(-90)" stroke-
linecap="circle" />
</svg>
</div>
<div class="overlay-collect"></div>
<div class="hot-offer-btn"></div>
<div class="ticker-body-two">
<svg viewBox="4, -19 65 35" class="gauge-background-
two" fill="none">
<circle r="10"/>
</svg>
<svg viewBox="-51, -34 450 75" class="gauge-two"
fill="none">
<path id="arc-path-two" transform="rotate(-90)"
stroke-linecap="circle" />
</svg>
</div>
Basically, you just need to add something to short circuit the build if you aren't ready for it.
One simple way with the code you have would be to just return if !pathElement.
setInterval(function() {
// Update the SVG arc descriptor.
var pathElement = document.getElementById('arc-path');
var pathElementTwo = document.getElementById('arc-path-two');
if (!pathElement || !pathElementTwo) {
return; // don't do the rest
}
// Update the ticker progress.
arc += Math.PI / 1000;
arcTwo += Math.PI / 1000;
if (arc >= 2 * Math.PI) { arc = 0; }
if (arcTwo >= 2 * Math.PI) { arcTwo = 0; }
pathElement.setAttribute('d', describeArc(26, 0, arc));
pathElementTwo.setAttribute('d', describeArc(26, 0, arcTwo));
}, 400 / 0)
Now, if pathElement or pathElementTwo are null, it'll just return out of the function and stop doing things.
I also pulled the variables up to the top of the function for two reasons.
First, it's just good convention to declare all variables for a scope at the top, for readability and to help avoid potential errors.
The other reason, for this case in particular, is so you can jump out as early as possible. No need to do the other math if we aren't going to be able to do anything with it.

How to draw the path of a rounded rectangle with optional border sides? [duplicate]

I have the following SVG:
<svg>
<g>
<path id="k9ffd8001" d="M64.5 45.5 82.5 45.5 82.5 64.5 64.5 64.5 z" stroke="#808600" stroke-width="0" transform="rotate(0 0 0)" stroke-linecap="square" stroke-linejoin="round" fill-opacity="1" stroke-opacity="1" fill="#a0a700"></path>
<path id="kb8000001" d="M64.5 45.5 82.5 45.5 82.5 64.5 64.5 64.5 z" stroke="#808600" stroke-width="0" transform="rotate(0 0 0)" stroke-linecap="square" stroke-linejoin="round" fill-opacity="1" stroke-opacity="1" fill="url(#k9ffb0001)"></path>
</g>
</svg>
I want to get a CSS-like border-top-right-radius and border-top-bottom-radius effect.
How can I achieve that rounded corner effect?
Here is how you can create a rounded rectangle with SVG Path:
<path d="M100,100 h200 a20,20 0 0 1 20,20 v200 a20,20 0 0 1 -20,20 h-200 a20,20 0 0 1 -20,-20 v-200 a20,20 0 0 1 20,-20 z" />
Explanation
m100,100: move to point(100,100)
h200: draw a 200px horizontal line from where we are
a20,20 0 0 1 20,20: draw an arc with 20px X radius, 20px Y radius, clockwise, to a point with 20px difference in X and Y axis
v200: draw a 200px vertical line from where we are
a20,20 0 0 1 -20,20: draw an arc with 20px X and Y radius, clockwise, to a point with -20px difference in X and 20px difference in Y axis
h-200: draw a -200px horizontal line from where we are
a20,20 0 0 1 -20,-20: draw an arc with 20px X and Y radius, clockwise, to a point with -20px difference in X and -20px difference in Y axis
v-200: draw a -200px vertical line from where we are
a20,20 0 0 1 20,-20: draw an arc with 20px X and Y radius, clockwise, to a point with 20px difference in X and -20px difference in Y axis
z: close the path
<svg width="440" height="440">
<path d="M100,100 h200 a20,20 0 0 1 20,20 v200 a20,20 0 0 1 -20,20 h-200 a20,20 0 0 1 -20,-20 v-200 a20,20 0 0 1 20,-20 z" fill="none" stroke="black" stroke-width="3" />
</svg>
Not sure why nobody posted an actual SVG answer. Here is an SVG rectangle with rounded corners (radius 3) on the top:
<path d="M0,0 L0,27 A3,3 0 0,0 3,30 L7,30 A3,3 0 0,0 10,27 L10,0 Z" />
This is a Move To (M), Line To (L), Arc To (A), Line To (L), Arc To (A), Line To (L), Close Path (Z).
The comma-delimited numbers are absolute coordinates. The arcs are defined with additional parameters specifying the radius and type of arc. This could also be accomplished with relative coordinates (use lower-case letters for L and A).
The complete reference for those commands is on the W3C SVG Paths page, and additional reference material on SVG paths can be found in this article.
As referenced in my answer to Applying rounded corners to paths/polygons, I have written a routine in javascript for generically rounding corners of SVG paths, with examples, here: http://plnkr.co/edit/kGnGGyoOCKil02k04snu.
It will work independently from any stroke effects you may have. To use, include the rounding.js file from the Plnkr and call the function like so:
roundPathCorners(pathString, radius, useFractionalRadius)
The result will be the rounded path.
The results look like this:
You have explicitly set your stroke-linejoin to round but your stroke-width to 0, so of course you're not going to see rounded corners if you have no stroke to round.
Here's a modified example with rounded corners made through strokes:
http://jsfiddle.net/8uxqK/1/
<path d="M64.5 45.5 82.5 45.5 82.5 64.5 64.5 64.5 z"
stroke-width="5"
stroke-linejoin="round"
stroke="#808600"
fill="#a0a700" />
Otherwise—if you need an actual rounded shape fill and not just a rounded fatty stroke—you must do what #Jlange says and make an actual rounded shape.
I'd also consider using a plain old <rect> which provides the rx and ry attributes
MDN SVG docs <- note the second drawn rect element
I've happened upon this problem today myself and managed to solve it by writing a small JavaScript function.
From what I can tell, there is no easy way to give a path element in an SVG rounded corners except if you only need the borders to be rounded, in which case the (CSS) attributes stroke, stroke-width and most importantly stroke-linejoin="round" are perfectly sufficient.
However, in my case I used a path object to create custom shapes with n corners that are filled out with a certain color and don't have visible borders, much like this:
I managed to write a quick function that takes an array of coordinates for an SVG path and returns the finished path string to put in the d attribute of the path html element. The resulting shape will then look something like this:
Here is the function:
/**
* Creates a coordinate path for the Path SVG element with rounded corners
* #param pathCoords - An array of coordinates in the form [{x: Number, y: Number}, ...]
*/
function createRoundedPathString(pathCoords) {
const path = [];
const curveRadius = 3;
// Reset indexes, so there are no gaps
pathCoords = pathCoords.slice();
for (let i = 0; i < pathCoords.length; i++) {
// 1. Get current coord and the next two (startpoint, cornerpoint, endpoint) to calculate rounded curve
const c2Index = ((i + 1) > pathCoords.length - 1) ? (i + 1) % pathCoords.length : i + 1;
const c3Index = ((i + 2) > pathCoords.length - 1) ? (i + 2) % pathCoords.length : i + 2;
const c1 = pathCoords[i];
const c2 = pathCoords[c2Index];
const c3 = pathCoords[c3Index];
// 2. For each 3 coords, enter two new path commands: Line to start of curve, bezier curve around corner.
// Calculate curvePoint c1 -> c2
const c1c2Distance = Math.sqrt(Math.pow(c1.x - c2.x, 2) + Math.pow(c1.y - c2.y, 2));
const c1c2DistanceRatio = (c1c2Distance - curveRadius) / c1c2Distance;
const c1c2CurvePoint = [
((1 - c1c2DistanceRatio) * c1.x + c1c2DistanceRatio * c2.x).toFixed(1),
((1 - c1c2DistanceRatio) * c1.y + c1c2DistanceRatio * c2.y).toFixed(1)
];
// Calculate curvePoint c2 -> c3
const c2c3Distance = Math.sqrt(Math.pow(c2.x - c3.x, 2) + Math.pow(c2.y - c3.y, 2));
const c2c3DistanceRatio = curveRadius / c2c3Distance;
const c2c3CurvePoint = [
((1 - c2c3DistanceRatio) * c2.x + c2c3DistanceRatio * c3.x).toFixed(1),
((1 - c2c3DistanceRatio) * c2.y + c2c3DistanceRatio * c3.y).toFixed(1)
];
// If at last coord of polygon, also save that as starting point
if (i === pathCoords.length - 1) {
path.unshift('M' + c2c3CurvePoint.join(','));
}
// Line to start of curve (L endcoord)
path.push('L' + c1c2CurvePoint.join(','));
// Bezier line around curve (Q controlcoord endcoord)
path.push('Q' + c2.x + ',' + c2.y + ',' + c2c3CurvePoint.join(','));
}
// Logically connect path to starting point again (shouldn't be necessary as path ends there anyway, but seems cleaner)
path.push('Z');
return path.join(' ');
}
You can determine the rounding strength by setting the curveRadius variable at the top. The default is 3 for a 100x100 (viewport) coordinate system, but depending on the size of your SVG, you may need to adjust this.
For my case I need to radius begin and end of path:
With stroke-linecap: round; I change it to what I want:
This question is the first result for Googling "svg rounded corners path". Phrogz suggestion to use stroke has some limitations (namely, that I cannot use stroke for other purposes, and that the dimensions have to be corrected for the stroke width).
Jlange suggestion to use a curve is better, but not very concrete. I ended up using quadratic Bézier curves for drawing rounded corners. Consider this picture of a corner marked with a blue dot and two red points on adjacent edges:
The two lines could be made with the L command. To turn this sharp corner into a rounded corner, start drawing a curve from the left red point (use M x,y to move to that point). Now a quadratic Bézier curve has just a single control point which you must set on the blue point. Set the end of the curve at the right red point. As the tangent at the two red points are in the direction of the previous lines, you will see a fluent transition, "rounded corners".
Now to continue the shape after the rounded corner, a straight line in a Bézier curve can be achieved by setting the control point between on the line between the two corners.
To help me with determining the path, I wrote this Python script that accepts edges and a radius. Vector math makes this actually very easy. The resulting image from the output:
#!/usr/bin/env python
# Given some vectors and a border-radius, output a SVG path with rounded
# corners.
#
# Copyright (C) Peter Wu <peter#lekensteyn.nl>
from math import sqrt
class Vector(object):
def __init__(self, x, y):
self.x = x
self.y = y
def sub(self, vec):
return Vector(self.x - vec.x, self.y - vec.y)
def add(self, vec):
return Vector(self.x + vec.x, self.y + vec.y)
def scale(self, n):
return Vector(self.x * n, self.y * n)
def length(self):
return sqrt(self.x**2 + self.y**2)
def normal(self):
length = self.length()
return Vector(self.x / length, self.y / length)
def __str__(self):
x = round(self.x, 2)
y = round(self.y, 2)
return '{},{}'.format(x, y)
# A line from vec_from to vec_to
def line(vec_from, vec_to):
half_vec = vec_from.add(vec_to.sub(vec_from).scale(.5))
return '{} {}'.format(half_vec, vec_to)
# Adds 'n' units to vec_from pointing in direction vec_to
def vecDir(vec_from, vec_to, n):
return vec_from.add(vec_to.sub(vec_from).normal().scale(n))
# Draws a line, but skips 'r' units from the begin and end
def lineR(vec_from, vec_to, r):
vec = vec_to.sub(vec_from).normal().scale(r)
return line(vec_from.add(vec), vec_to.sub(vec))
# An edge in vec_from, to vec_to with radius r
def edge(vec_from, vec_to, r):
v = vecDir(vec_from, vec_to, r)
return '{} {}'.format(vec_from, v)
# Hard-coded border-radius and vectors
r = 5
a = Vector( 0, 60)
b = Vector(100, 0)
c = Vector(100, 200)
d = Vector( 0, 200 - 60)
path = []
# Start below top-left edge
path.append('M {} Q'.format(a.add(Vector(0, r))))
# top-left edge...
path.append(edge(a, b, r))
path.append(lineR(a, b, r))
path.append(edge(b, c, r))
path.append(lineR(b, c, r))
path.append(edge(c, d, r))
path.append(lineR(c, d, r))
path.append(edge(d, a, r))
path.append(lineR(d, a, r))
# Show results that can be pushed into a <path d="..." />
for part in path:
print(part)
Here are some paths for tabs:
https://codepen.io/mochime/pen/VxxzMW
<!-- left tab -->
<div>
<svg width="60" height="60">
<path d="M10,10
a10 10 0 0 1 10 -10
h 50
v 47
h -50
a10 10 0 0 1 -10 -10
z"
fill="#ff3600"></path>
</svg>
</div>
<!-- right tab -->
<div>
<svg width="60" height="60">
<path d="M10 0
h 40
a10 10 0 0 1 10 10
v 27
a10 10 0 0 1 -10 10
h -40
z"
fill="#ff3600"></path>
</svg>
</div>
<!-- tab tab :) -->
<div>
<svg width="60" height="60">
<path d="M10,40
v -30
a10 10 0 0 1 10 -10
h 30
a10 10 0 0 1 10 10
v 30
z"
fill="#ff3600"></path>
</svg>
</div>
The other answers explained the mechanics. I especially liked hossein-maktoobian's answer.
The paths in the pen do the brunt of the work, the values can be modified to suite whatever desired dimensions.
This basically does the same as Mvins answer, but is a more compressed down and simplified version. It works by going back the distance of the radius of the lines adjacent to the corner and connecting both ends with a bezier curve whose control point is at the original corner point.
function createRoundedPath(coords, radius, close) {
let path = ""
const length = coords.length + (close ? 1 : -1)
for (let i = 0; i < length; i++) {
const a = coords[i % coords.length]
const b = coords[(i + 1) % coords.length]
const t = Math.min(radius / Math.hypot(b.x - a.x, b.y - a.y), 0.5)
if (i > 0) path += `Q${a.x},${a.y} ${a.x * (1 - t) + b.x * t},${a.y * (1 - t) + b.y * t}`
if (!close && i == 0) path += `M${a.x},${a.y}`
else if (i == 0) path += `M${a.x * (1 - t) + b.x * t},${a.y * (1 - t) + b.y * t}`
if (!close && i == length - 1) path += `L${b.x},${b.y}`
else if (i < length - 1) path += `L${a.x * t + b.x * (1 - t)},${a.y * t + b.y * (1 - t)}`
}
if (close) path += "Z"
return path
}
Here’s a piece of react code to generate rectangles with different corner radiuses:
const Rect = ({width, height, tl, tr, br, bl}) => {
const top = width - tl - tr;
const right = height - tr - br;
const bottom = width - br - bl;
const left = height - bl - tl;
const d = `
M${tl},0
h${top}
a${tr},${tr} 0 0 1 ${tr},${tr}
v${right}
a${br},${br} 0 0 1 -${br},${br}
h-${bottom}
a${bl},${bl} 0 0 1 -${bl},-${bl}
v-${left}
a${tl},${tl} 0 0 1 ${tl},-${tl}
z
`;
return (
<svg width={width} height={height}>
<path d={d} fill="black" />
</svg>
);
};
ReactDOM.render(
<Rect width={200} height={100} tl={20} tr={0} br={20} bl={60} />,
document.querySelector('#app'),
);
https://jsfiddle.net/v1Ljpxh7/
Just to simplify implementing answer of #hmak.me, here's a commented piece of React code to generate rounded rectangles.
const Rect = ({width, height, round, strokeWidth}) => {
// overhang over given width and height that we get due to stroke width
const s = strokeWidth / 2;
// how many pixels do we need to cut from vertical and horizontal parts
// due to rounded corners and stroke width
const over = 2 * round + strokeWidth;
// lengths of straight lines
const w = width - over;
const h = height - over;
// beware that extra spaces will not be minified
// they are added for clarity
const d = `
M${round + s},${s}
h${w}
a${round},${round} 0 0 1 ${round},${round}
v${h}
a${round},${round} 0 0 1 -${round},${round}
h-${w}
a${round},${round} 0 0 1 -${round},-${round}
v-${h}
a${round},${round} 0 0 1 ${round},-${round}
z
`;
return (
<svg width={width} height={height}>
<path d={d} fill="none" stroke="black" strokeWidth={strokeWidth} />
</svg>
);
};
ReactDOM.render(
<Rect width={64} height={32} strokeWidth={2} round={4} />,
document.querySelector('#app'),
);
Jsfiddle link.
I wrote this little typescript function so I can dynamically create the path for a complex rounded rectangle that function similar to a div with border-radius.
export function roundedRectPath(
x: number,
y: number,
width: number,
height: number,
bevel: [number, number, number, number] = [3, 3, 3, 3]
): string {
return "M" + x + "," + y
+ `m 0 ${bevel[0]}`
+ `q 0 -${bevel[0]} ${bevel[0]} -${bevel[0]}`
+ `l ${width - bevel[0] - bevel[1]} 0`
+ `q ${bevel[1]} 0 ${bevel[1]} ${bevel[1]}`
+ `l 0 ${height - bevel[1] - bevel[2]}`
+ `q 0 ${bevel[2]} -${bevel[2]} ${bevel[2]}`
+ `l -${width - bevel[2] - bevel[3]} 0`
+ `q -${bevel[3]} 0 -${bevel[3]} -${bevel[3]}`
+ `z`;
}
I found a solution but it is a bit hacky so it may not always work. I found that if you have an arc (A or a) with really small values it forces it to create a curve in one spot thus forming a rounded comer...
<svg viewBox="0 0 1 0.6" stroke="black" fill="grey" style="stroke-width:0.05px;">
<path d="M0.7 0.2 L0.1 0.1 A0.0001 0.0001 0 0 0 0.099 0.101 L0.5 0.5Z"></path>
</svg>
You are using a path element, why don't you just give the path a curve? See here for how to make curves using path elements: http://www.w3.org/TR/SVG/paths.html#PathDataCurveCommands

Categories

Resources