svg#demo
View Compiled
$demo-dark: #333;
$demo-light: #ccc;
$demo-ll: #95a;
$demo-hl: #f90;

body {
	display: flex;
	margin: 0;
	height: 100vh
}

svg {
	display: block;
	width: 100vw;
	color: $demo-light
}

path { fill: none }

circle {
	fill: currentcolor;
	stroke: $demo-dark;
	stroke-width: 5px
}

[id='end'] path {
	fill: #b53
}

.curves {
	stroke: transparent;
	stroke-width: 98px;
	cursor: pointer;
	color: $demo-dark;
	
	use {
		stroke: currentcolor;
		stroke-width: 10px;
		pointer-events: none
	}
}

.guides {
	stroke: $demo-ll;
	stroke-width: 4px;
	pointer-events: none;
	
	path { opacity: 0 }
}

.points {	
	.end { cursor: pointer }
	
	.ctrl { opacity: 0 }
}

#demo .hl {
	opacity: 1;
	color: $demo-hl;
	transition: .3s
}
const _SVG = document.getElementById('demo'), 
			D = 1000 /* viewBox size */, 
			O = -.5*D /* viewBox top left offset */, 
			NS_URI = 'http://www.w3.org/2000/svg', 
			XLink_NS = 'http://www.w3.org/1999/xlink', 
			S = [], N = 2, 
			P = 5, NCUB = 3, NT = P*NCUB, 
			PR = .025*D, PR1 = .5*PR, 
			ACT = ['add', 'remove'];

let hl = null;

function getStarCoord(f = .5) {
	const RCO = f*D /* outer (pentagram) circumradius*/, 
				BAS = 2*(2*Math.PI/P) /* base angle for star poly */, 
				BAC = 2*Math.PI/P /* base angle for convex poly */, 
				RI = RCO*Math.cos(.5*BAS) /*pentagram/ inner pentagon inradius */, 
				RCI = RI/Math.cos(.5*BAC) /* inner pentagon circumradius */, 
				ND = 2*P /* total number of distinct points we need to get */, 
				BAD = 2*Math.PI/ND /* base angle for point distribution */, 
				PTS = [] /* array we fill with point coordinates */;

	for(let i = 0; i < ND; i++) {
		let /* radius at end point (inner)/ control point (outer) */
				cr = i%2 ? RCI : RCO, 
				/* angle of radial segment from origin to current point */
				ca = i*BAD + .5*Math.PI, 
				x = Math.round(cr*Math.cos(ca)), 
				y = Math.round(cr*Math.sin(ca));

		PTS.push([x, y]);
		/* for even indices double it, control points coincide here */
		if(!(i%2)) PTS.push([x, y]);
	}

	return PTS
};

function getHeartCoord(f = .25) {
	const R = f*D /* helper circle radius  */, 
				RC = Math.round(R/Math.SQRT2) /* circumradius of square of edge R */, 
				XT = 0, YT = -RC /* coords of point T */, 
				XA = 2*RC, YA = -RC /* coords of A points (x in abs value) */, 
				XB = 2*RC, YB = RC /* coords of B points (x in abs value) */, 
				XC = 0, YC = 3*RC /* coords of point C */, 
				XD = RC, YD = -2*RC /* coords of D points (x in abs value) */, 
				XE = 3*RC, YE = 0 /* coords of E points (x in abs value) */, 
				/* const for cubic curve approx of quarter circle */
				C = .551915, 
        CC = 1 - C, 
        /* coords of ctrl points on TD segs */
				XTD = Math.round(CC*XT + C*XD), YTD = Math.round(CC*YT + C*YD), 
        /* coords of ctrl points on AD segs */
				XAD = Math.round(CC*XA + C*XD), YAD = Math.round(CC*YA + C*YD), 
        /* coords of ctrl points on AE segs */
				XAE = Math.round(CC*XA + C*XE), YAE = Math.round(CC*YA + C*YE), 
        /* coords of ctrl points on BE segs */
				XBE = Math.round(CC*XB + C*XE), YBE = Math.round(CC*YB + C*YE);
	
	return [
		[XC, YC], [XC, YC], [-XB, YB], 
		[-XBE, YBE], [-XAE, YAE], [-XA, YA], 
		[-XAD, YAD], [-XTD, YTD], [XT, YT], 
		[XTD, YTD], [XAD, YAD], [XA, YA], 
		[XAE, YAE], [XBE, YBE], [XB, YB]
	].map(([x, y]) => [x + D, y - .09*R]);
};

function createShapes() {
	let f = document.createDocumentFragment();
	
	S.push(...[
		{ coord: getStarCoord() }, 
		{ coord: getHeartCoord() }
	]);
	
	for(let i = 0; i < N; i++) {
		let _curves, _guides, _points;
		
		S[i].g = document.createElementNS(NS_URI, 'g');
		S[i].g.classList.add('shape');
		
		_curves = document.createElementNS(NS_URI, 'g');
		_curves.classList.add('curves');
		S[i].c = [];
		
		_guides = document.createElementNS(NS_URI, 'g');
		_guides.classList.add('guides');
		
		_points = document.createElementNS(NS_URI, 'g');
		_points.classList.add('points');
				
		for(let j = 0; j < P; j++) {
			let sidx = j*NCUB, 
					tmp = S[i].coord.slice(sidx, sidx + NCUB), 
					unique = (tmp[0][0] == tmp[1][0] && tmp[0][1] == tmp[1][1]) ? 
										tmp.slice(1) : tmp;
			
			S[i].c.push({
				curve: document.createElementNS(NS_URI, 'path'), 
				ccopy: document.createElementNS(NS_URI, 'use'), 
				guide: document.createElementNS(NS_URI, 'path'), 
				ends: [
					j ? S[i].c[j - 1].ends[1] : null, 
					document.createElementNS(NS_URI, 'circle')
				],
				ctrl: unique.slice(0, unique.length - 1)
										.map(_ => document.createElementNS(NS_URI, 'circle'))
			});
			
			S[i].c[j].curve.id = `curve-${i}-${j}`;
			S[i].c[j].curve.setAttribute(...[
				'd',
				`M${S[i].coord[(sidx + NT - 1)%NT]}
				 C${tmp}`
			]);	
			S[i].c[j].ccopy.setAttributeNS(...[
				XLink_NS, 
				'xlink:href', 
				`#${S[i].c[j].curve.id}`
			]);
			_curves.appendChild(S[i].c[j].curve);
			_curves.appendChild(S[i].c[j].ccopy);
			
			S[i].c[j].guide.setAttribute(...[
				'd',
				`M${S[i].coord[(sidx + NT - 1)%NT]}
				  ${unique}`
			]);
			_guides.appendChild(S[i].c[j].guide);
			
			S[i].c[j].ctrl.forEach((c, k) => {
				c.classList.add('ctrl');
				c.setAttribute('r', PR1);
				c.setAttribute('cx', S[i].coord[sidx + k][0]);
				c.setAttribute('cy', S[i].coord[sidx + k][1]);
				_points.appendChild(c);
			});
			
			S[i].c[j].ends[1].id = `point-${i}-${(j + 1)%P}`
			S[i].c[j].ends[1].classList.add('end');
			S[i].c[j].ends[1].setAttribute('r', PR);
			S[i].c[j].ends[1].setAttribute('cx', S[i].coord[sidx + NCUB - 1][0]);
			S[i].c[j].ends[1].setAttribute('cy', S[i].coord[sidx + NCUB - 1][1]);
			_points.appendChild(S[i].c[j].ends[1]);
		}
		
		S[i].c[0].ends[0] = S[i].c[P - 1].ends[1];
		
		S[i].g.appendChild(_curves);
		S[i].g.appendChild(_guides);
		S[i].g.appendChild(_points);
		f.appendChild(S[i].g)
	}
	
	_SVG.appendChild(f)
};

function highlight(hl_obj, add = 1) {
	const CURR_ACT = ACT[.5*(1 - add)];
	
	for(let i = 0; i < N; i++) {
		if(hl_obj.typ === 'curve') {			
			S[i].c[hl_obj.idx].ccopy.classList[CURR_ACT]('hl');
			S[i].c[hl_obj.idx].guide.classList[CURR_ACT]('hl');
			S[i].c[hl_obj.idx].ctrl.forEach(c => c.classList[CURR_ACT]('hl'));
			S[i].c[hl_obj.idx].ends.forEach(c => c.classList[CURR_ACT]('hl'));
		}
		else if(hl_obj.typ === 'point') {
			S[i].c[hl_obj.idx].ends[0].classList[CURR_ACT]('hl');
		}
	}
	
	hl = add > 0 ? hl_obj : null;
};

(function init() {
	_SVG.setAttribute('viewBox', [O, O, 2*D, 1.05*D].join(' '));
	
	createShapes();
	
	addEventListener('click', e => {
		const _T = e.target;
		
		if(hl) highlight(hl, -1);
		
		if(_T.id && /(point)|(curve)/.test(_T.id)) {
			const DATA = _T.id.split('-');
			
			highlight({typ: DATA[0], idx: +DATA[2]});
		}
	}, false);
})();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.