I have a piechart with labels and legend which looks fine in SVG but as soon as I use canvg to transform it to canvas some formats are missing / get wrong.
SVG:
Canvas:
I already inline the CSS to apply all CSS settings but still the format does not match.
Any ideas?
Is this a (canvg) bug or am I doing s.th. wrong?
var columns = ['data11', 'data2', 'data347', 'data40098'];
var data = [150, 250, 300, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
/**
* global C3Styles object
*/
var C3Styles = null;
var legendData = [];
var sumTotal = 0
//prepare pie data
var columnData = [];
var columnNames = {};
for (i = 0; i < columns.length; i++) {
columnData.push([columns[i]].concat(data[i]));
var val = (Array.isArray(data[i])) ? data[i].reduce(function(pv, cv) {
return pv + cv;
}, 0) : data[i];
sumTotal += val;
legendData.push({
id: columns[i],
value: val,
ratio: 0.0
});
}
legendData.forEach(function(el, i) {
el.ratio = el.value / sumTotal
columnNames[el.id] = [el.id, d3.format(",.0f")(el.value), d3.format(",.1%")(el.ratio)].join(';');
});
var chart = c3.generate({
bindto: d3.select('#chart'),
data: {
columns: [
[columns[0]].concat(data[0])
],
names: columnNames,
type: 'pie',
},
legend: {
position: 'right',
show: true
},
pie: {
label: {
threshold: 0.001,
format: function(value, ratio, id) {
return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
}
}
},
color: {
pattern: colors
},
onrendered: function() {
redrawLabelBackgrounds();
redrawLegend();
}
});
function addLabelBackground(index) {
/*d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-')+".c3-chart-arc")
.insert("rect", "text")
.style("fill", colors[index]);*/
var p = d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-') + ".c3-chart-arc");
var g = p.append("g");
g.append("rect")
.style("fill", colors[index]);
g.append(function() {
return p.select("text").remove().node();
});
}
for (var i = 0; i < columns.length; i++) {
if (i > 0) {
setTimeout(function(column) {
chart.load({
columns: [
columnData[column],
]
});
//chart.data.names(columnNames[column])
addLabelBackground(column);
}, (i * 5000 / columnData.length), i);
} else {
addLabelBackground(i);
}
}
function redrawLegend() {
d3.select('#chart').selectAll(".c3-legend-item > text").each(function() {
// get d3 node
var legendItem = d3.select(this);
var legendItemNode = legendItem.node();
//check if label is drawn
if (legendItemNode) {
if (legendItemNode.childElementCount === 0 && legendItemNode.innerHTML.length > 0) {
//build data
var data = legendItemNode.innerHTML.split(';');
legendItem.text("");
//TODO format legend dynamically depending on text
legendItem.append("tspan")
.text(data[0] + ": ")
.attr("class", "id-row")
.attr("text-anchor", "start");
legendItem.append("tspan")
.text(data[1] + " = ")
.attr("class", "value-row")
.attr("x", 160)
.attr("text-anchor", "end");
legendItem.append("tspan")
.text(data[2])
.attr("class", "ratio-row")
.attr("x", 190)
.attr("text-anchor", "end");
}
}
});
d3.select('#chart').selectAll(".c3-legend-item > rect").each(function() {
var legendItem = d3.select(this);
legendItem.attr("width", 190);
});
}
function redrawLabelBackgrounds() {
//for all label texts drawn yet
//for all label texts drawn yet
d3.select('#chart').selectAll(".c3-chart-arc > g > text").each(function(v) {
// get d3 node
var label = d3.select(this);
var labelNode = label.node();
//check if label is drawn
if (labelNode) {
if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
//build data
var data = labelNode.innerHTML.split(';');
label.text("");
data.forEach(function(i, n) {
label.append("tspan")
.text(i)
.attr("dy", (n === 0) ? 0 : "1.2em")
.attr("x", 0)
.attr("text-anchor", "middle");
}, label);
}
//check if element is visible
if (d3.select(labelNode.parentNode).style("display") !== 'none') {
//get pos of the label text
var pos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
if (pos) {
// TODO: mofify the pos of the text
// pos[0] = (pos[0]/h*90000);
// pos[1] = (pos[1]/h*90000);
// remove dy and move label
//d3.select(this).attr("dy", 0);
//d3.select(this).attr("transform", "translate(" + pos[0] + "," + pos[1] + ")");
//get surrounding box of the label
var bbox = labelNode.getBBox();
//now draw and move the rects
d3.select(labelNode.parentNode).select("rect")
.attr("transform", "translate(" + (pos[0] - (bbox.width + padding) / 2) +
"," + (pos[1] - bbox.height / labelNode.childElementCount) + ")")
.attr("width", bbox.width + padding)
.attr("height", bbox.height + padding);
}
}
}
});
}
document.getElementById("exportButton").onclick = function() {
exportChartToImage();
};
function exportChartToImage() {
var createImagePromise = new Promise(function(resolve, reject) {
var images = [];
d3.selectAll('svg').each(function() {
if (this.parentNode) {
images.push(getSvgImage(this.parentNode, true));
}
});
if (images.length > 0)
resolve(images);
else
reject(images);
});
createImagePromise.then(function(images) {
/*images.forEach(function(img, n) {
img.toBlob(function(blob) {
saveAs(blob, "image_" + (n + 1) + ".png");
});
});*/
})
.catch(function(error) {
throw error;
});
};
/**
* Converts a SVG-Chart to a canvas and returns it.
*/
function getSvgImage(svgContainer) {
var svgEl = d3.select(svgContainer).select('svg').node();
var svgCopyEl = svgEl.cloneNode(true);
if (!svgCopyEl)
return;
d3.select("#svgCopyEl").selectAll("*").remove();
d3.select("#svgCopyEl").node().append(svgCopyEl);
//apply all CSS styles to SVG
/* taken from https://gist.github.com/aendrew/1ad2eed6afa29e30d52e#file-exportchart-js
and changed from, angular to D3 functions
*/
/* Take styles from CSS and put as inline SVG attributes so that Canvg
can properly parse them. */
if (!C3Styles) {
var chartStyle;
// Get rules from c3.css
var styleSheets = document.styleSheets;
for (var i = 0; i <= styleSheets.length - 1; i++) {
if (styleSheets[i].href && (styleSheets[i].href.indexOf('c3.min.css') !== -1 || styleSheets[i].href.indexOf('c3.css') !== -1)) {
try {
if (styleSheets[i].rules !== undefined) {
chartStyle = styleSheets[i].rules;
} else {
chartStyle = styleSheets[i].cssRules;
}
break;
}
//Note that SecurityError exception is specific to Firefox.
catch (e) {
if (e.name == 'SecurityError') {
console.log("SecurityError. Cant read: " + styleSheets[i].href);
continue;
}
}
}
if (chartStyle !== null && chartStyle !== undefined) {
C3Styles = {};
var selector;
// Inline apply all the CSS rules as inline
for (i = 0; i < chartStyle.length; i++) {
if (chartStyle[i].type === 1) {
selector = chartStyle[i].selectorText;
var styleDec = chartStyle[i].style;
for (var s = 0; s < styleDec.length; s++) {
C3Styles[styleDec[s]] = styleDec[styleDec[s]];
}
}
}
}
}
}
if (C3Styles) {
d3.select(svgCopyEl).selectAll('.c3:not(.c3-chart):not(path)').style(C3Styles);
}
// SVG doesn't use CSS visibility and opacity is an attribute, not a style property. Change hidden stuff to "display: none"
d3.select(svgCopyEl).selectAll('*')
.filter(function(d) {
return d && d.style && (d.style('visibility') === 'hidden' || d.style('opacity') === '0');
})
.style('display', 'none');
//fix weird back fill
d3.select(svgCopyEl).selectAll("path").attr("fill", "none");
//fix no axes
d3.select(svgCopyEl).selectAll("path.domain").attr("stroke", "black");
//fix no tick
d3.select(svgCopyEl).selectAll(".tick line").attr("stroke", "black");
//apply svg text fill, set color
d3.select(svgCopyEl).selectAll("text:not(.c3-empty):not(.c3-axis)").attr("opacity", 1);
var canvasComputed = d3.select("#canvasComputed").node();
// transform SVG to canvas using external canvg
canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));
return canvasComputed;
}
.c3-chart-arc.c3-target text {
color: white;
fill: white;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script>
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.5/canvg.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h4>
SVG
</h4>
<div id="chart" class "c3">
</div>
<h4>
copy SVG
</h4>
<div id ="svgCopyEl">
</div>
<div>
<h4>
canvas
</h4>
<canvas id="canvasComputed"></canvas>
</div>
<button type="button" id="exportButton">
export to Canvas
</button>
Basically it was a combination of lack knowledge and a bug.
First of all, all relevant CSS styles have to be inlined when using canvg. Second, there was a bug in canvg using tspans and text-anchors.
Here is the (more or less) working code:
var columns = ['data11', 'data2', 'data347', 'data40098'];
var data = [150, 250, 300, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
/**
* global C3Styles object
*/
var C3Styles = null;
var legendData = [];
var sumTotal = 0
//prepare pie data
var columnData = [];
var columnNames = {};
for (i = 0; i < columns.length; i++) {
columnData.push([columns[i]].concat(data[i]));
var val = (Array.isArray(data[i])) ? data[i].reduce(function(pv, cv) {
return pv + cv;
}, 0) : data[i];
sumTotal += val;
legendData.push({
id: columns[i],
value: val,
ratio: 0.0
});
}
legendData.forEach(function(el, i) {
el.ratio = el.value / sumTotal
columnNames[el.id] = [el.id, d3.format(",.0f")(el.value), d3.format(",.1%")(el.ratio)].join(';');
});
var chart = c3.generate({
bindto: d3.select('#chart'),
data: {
columns: [
[columns[0]].concat(data[0])
],
names: columnNames,
type: 'pie',
},
legend: {
position: 'right',
show: true
},
pie: {
label: {
threshold: 0.001,
format: function(value, ratio, id) {
return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
}
}
},
color: {
pattern: colors
},
onrendered: function() {
redrawLabelBackgrounds();
redrawLegend();
}
});
function addLabelBackground(index) {
/*d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-')+".c3-chart-arc")
.insert("rect", "text")
.style("fill", colors[index]);*/
var p = d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-') + ".c3-chart-arc");
var g = p.append("g");
g.append("rect")
.style("fill", colors[index]);
g.append(function() {
return p.select("text").remove().node();
});
}
for (var i = 0; i < columns.length; i++) {
if (i > 0) {
setTimeout(function(column) {
chart.load({
columns: [
columnData[column],
]
});
//chart.data.names(columnNames[column])
addLabelBackground(column);
}, (i * 5000 / columnData.length), i);
} else {
addLabelBackground(i);
}
}
function redrawLegend() {
d3.select('#chart').selectAll(".c3-legend-item > text").each(function() {
// get d3 node
var legendItem = d3.select(this);
var legendItemNode = legendItem.node();
//check if label is drawn
if (legendItemNode) {
if (legendItemNode.childElementCount === 0 && legendItemNode.innerHTML.length > 0) {
//build data
var data = legendItemNode.innerHTML.split(';');
legendItem.text("");
//TODO format legend dynamically depending on text
legendItem.append("tspan")
.text(data[0] + ": ")
.attr("class", "id-row")
.attr("text-anchor", "start");
legendItem.append("tspan")
.text(data[1] + " = ")
.attr("class", "value-row")
.attr("x", 160)
.attr("text-anchor", "end");
legendItem.append("tspan")
.text(data[2])
.attr("class", "ratio-row")
.attr("x", 190)
.attr("text-anchor", "end");
}
}
});
d3.select('#chart').selectAll(".c3-legend-item > rect").each(function() {
var legendItem = d3.select(this);
legendItem.attr("width", 190);
});
}
function redrawLabelBackgrounds() {
//for all label texts drawn yet
//for all label texts drawn yet
d3.select('#chart').selectAll(".c3-chart-arc > g > text").each(function(v) {
// get d3 node
var label = d3.select(this);
var labelNode = label.node();
//check if label is drawn
if (labelNode) {
var bbox = labelNode.getBBox();
var labelTextHeight = bbox.height;
if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
//build data
var data = labelNode.innerHTML.split(';');
label.html('')
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle");
data.forEach(function(i, n) {
label.append("tspan")
.text(i)
.attr("dy", (n === 0) ? 0 : "1.2em")
.attr("x", 0);
}, label);
}
//check if element is visible
if (d3.select(labelNode.parentNode).style("display") !== 'none') {
//get pos of the label text
var pos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
if (pos) {
// TODO: mofify the pos of the text
// pos[0] = (pos[0]/h*90000);
// pos[1] = (pos[1]/h*90000);
// remove dy and move label
//d3.select(this).attr("dy", 0);
//d3.select(this).attr("transform", "translate(" + pos[0] + "," + pos[1] + ")");
//get surrounding box of the label
bbox = labelNode.getBBox();
//now draw and move the rects
d3.select(labelNode.parentNode).select("rect")
.attr("transform", "translate(" + (pos[0] - bbox.width / 2 - padding) +
"," + (pos[1] - labelTextHeight/2 - padding)+")")
.attr("width", bbox.width + 2*padding)
.attr("height", bbox.height + 2*padding);
}
}
}
});
}
document.getElementById("exportButton").onclick = function() {
exportChartToImage();
};
function exportChartToImage() {
var createImagePromise = new Promise(function(resolve, reject) {
var images = [];
d3.selectAll('svg').each(function() {
if (this.parentNode) {
images.push(getSvgImage(this.parentNode, true));
}
});
if (images.length > 0)
resolve(images);
else
reject(images);
});
createImagePromise.then(function(images) {
/*images.forEach(function(img, n) {
img.toBlob(function(blob) {
saveAs(blob, "image_" + (n + 1) + ".png");
});
});*/
})
.catch(function(error) {
throw error;
});
};
/**
* Converts a SVG-Chart to a canvas and returns it.
*/
function getSvgImage(svgContainer) {
var svgEl = d3.select(svgContainer).select('svg').node();
var svgCopyEl = svgEl.cloneNode(true);
if (!svgCopyEl)
return;
d3.select("#svgCopyEl").selectAll("*").remove();
d3.select("#svgCopyEl").node().append(svgCopyEl); //.transition().duration(0);
//apply C3 CSS styles to SVG
// SVG doesn't use CSS visibility and opacity is an attribute, not a style property. Change hidden stuff to "display: none"
d3.select("#svgCopyEl").selectAll('*')
.filter(function(d) {
return d && d.style && (d.style('visibility') === 'hidden' || d.style('opacity') === '0');
})
.style('display', 'none');
d3.select("#svgCopyEl").selectAll('.c3-chart path')
.filter(function(d) {
return d && d.style('fill') === 'none';
})
.attr('fill', 'none');
d3.select("#svgCopyEl").selectAll('.c3-chart path')
.filter(function(d) {
return d && d.style('fill') !== 'none';
})
.attr('fill', function(d) {
return d.style('fill');
});
//set c3 default font
d3.select("#svgCopyEl").selectAll('.c3 svg')
.style('font', 'sans-serif')
.style('font-size', '10px');
//set c3 legend font
d3.select("#svgCopyEl").selectAll('.c3-legend-item > text')
.style('font', 'sans-serif')
.style('font-size', '12px');
d3.select("#svgCopyEl").selectAll('.c3-legend-item > text > tspan')
.style('font', 'sans-serif')
.style('font-size', '12px');
//set c3 arc shapes
d3.select("#svgCopyEl").selectAll('.c3-chart-arc path,rect')
.style('stroke', '#fff');
d3.select("#svgCopyEl").selectAll('.c3-chart-arc text')
.attr('fill', '#fff')
.style('font', 'sans-serif')
.style('font-size', '13px');
//fix weird back fill
d3.select("#svgCopyEl").selectAll("path").attr("fill", "none");
//fix no axes
d3.select("#svgCopyEl").selectAll("path.domain").attr("stroke", "black");
//fix no tick
d3.select("#svgCopyEl").selectAll(".tick line").attr("stroke", "black");
var canvasComputed = d3.select("#canvasComputed").node();
// transform SVG to canvas using external canvg
canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));
return canvasComputed;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script> -->
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdn.jsdelivr.net/npm/canvg#2.0.0-beta.1/dist/browser/canvg.min.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h4>
SVG
</h4>
<div id="chart" class "c3">
</div>
<h4>
copy SVG
</h4>
<div id="svgCopyEl">
</div>
<div>
<h4>
canvas
</h4>
<canvas id="canvasComputed"></canvas>
</div>
<button type="button" id="exportButton">
export to Canvas
</button>
I want to create a waveform with progress bars that fill with another color from left to right.
Right now it looks:
I want it to look like this. Yellow is buffered audio, orange is playing right now. I already have these values.
The main question is how do I fill each rect with color by currentTime of audio?
Here's my code:
const elementWidth = 1100
const elementHeight = 64
const duration = 160
const currentTime = 20
const buffered = 140
// here's data that i get from web audio api
// bar length in seconds would be:
// duration / renderData = how much seconds in one bar
const renderData = [
[-0.015067690176936956, 0.015065840696712662],
[-0.009963374263646985, 0.009960838406137254],
[-0.0329772714073922, 0.032922178973984494],
[-0.02010780853750818, 0.020192897509204638],
[-0.029141768346505944, 0.02913273608186522],
[-0.03390369982419367, 0.033888949138664096],
[-0.05309944789682607, 0.053106191954295334],
[-0.017992382356680794, 0.0179506794436456],
[-0.04118192967225779, 0.04120773269527067],
[-0.032132343283569134, 0.03223372926977138],
[-0.04340663941189386, 0.043317410948806916],
[-0.026866048759920942, 0.02695383570549558],
[-0.041548487297645216, 0.04142889765358134],
[-0.0512541217270734, 0.05128097373670754],
[-0.02645596673127562, 0.026461825339764114],
[-0.03276659370022165, 0.032869462727325334],
[-0.02983164709570332, 0.02965126735342542],
[-0.06186988270590101, 0.06228762507639405],
[-0.037202475771159274, 0.03684529067849468],
[-0.04496168984286248, 0.044984343262096924],
[-0.02961698097048877, 0.029580527280458145],
[-0.06637895360455075, 0.06584970915134748],
[-0.03966561332234608, 0.04028105442218536],
[-0.04888827685580639, 0.04879637577182824],
[-0.034440279218927505, 0.03448690299802526],
[-0.04076603383847427, 0.04087949817166488],
[-0.03422100968150345, 0.03407137586231854],
[-0.03420552026962888, 0.034233479991186845],
[-0.06124921943975816, 0.06133406711072517],
[-0.08080063612343565, 0.08052139740352077],
[-0.052296123826832304, 0.05245498821828788],
[-0.07728568068325997, 0.0772439557897976],
[-0.04070025960953707, 0.04072465208052425],
[-0.016598400103531252, 0.01673240062886387],
[-0.0495708419979178, 0.04952405213368158],
[-0.03402468183819489, 0.03404496946468417],
[-0.04719791564971553, 0.04716565090961255],
[-0.024305039710776202, 0.024425998358774473],
[-0.04539290174457686, 0.0453603392364138],
[-0.04291280211166326, 0.042803252613569195],
[-0.03237617188947045, 0.032430479168267405],
[-0.046939414609483046, 0.046991124408919255],
[-0.037727014544829074, 0.03756628029896137],
[-0.05813820211592722, 0.058137499737658825],
[-0.03306609736616569, 0.03332803022833292],
[-0.03706343131822335, 0.03699838219166897],
[-0.031640843865570666, 0.03150685332686255],
[-0.07978720110560034, 0.07982405111308474],
[-0.04565408283291298, 0.04548542047551325],
[-0.03838929844552628, 0.0386080775422541],
[-0.0349069030273341, 0.03516624962570975],
[-0.05791808093217102, 0.057646960595115364],
[-0.040111244425499945, 0.040190047578908046],
[-0.0421531094659709, 0.04210734133509555],
[-0.04358563889018587, 0.043380678911277275],
[-0.024025454017633886, 0.024179111399202893],
[-0.039038574013751944, 0.03889745017750074],
[-0.02962543563292595, 0.02975662299643922],
[-0.07215596460653108, 0.07225534620830149],
[-0.0845103969948925, 0.08417566858032748],
[-0.05029865141667644, 0.05110349428845409],
[-0.06766253837563593, 0.06680008803627584],
[-0.05413748268128195, 0.054261121431710246],
[-0.04702217202288801, 0.04710783667779247],
[-0.047177278676382065, 0.047241381909344966],
[-0.04949906253183499, 0.049358880485210296],
[-0.06384145451618915, 0.06398437795989458],
[-0.0532812223855561, 0.05336013656088595],
[-0.055032831282645335, 0.055131815418379866],
[-0.05771727930777607, 0.05743980672281111],
[-0.06865421948220482, 0.06896493506959074],
[-0.05163944571854085, 0.05129081551014095],
[-0.04546664828758613, 0.04549366890782257],
[-0.02196073923070452, 0.022119579288034315],
[-0.026824862238895183, 0.026915318981447094],
[-0.04771898452983383, 0.04768769589918763],
[-0.05221904154341058, 0.05202229643239835],
[-0.04034726803191834, 0.040288317010035164],
[-0.04252634158686052, 0.04275796625513488],
[-0.055381424446109724, 0.05515857756430962],
[-0.06160043085044191, 0.06143890271068376],
[-0.04579617210990365, 0.04612433751815954],
[-0.039244869887493206, 0.03927668403684328],
[-0.03426885260996771, 0.03423936180141113],
[-0.03516869910983574, 0.035127711830890515],
[-0.026964357386084752, 0.02699723933039285],
[-0.03816966714682839, 0.03778890745758835],
[-0.04777519168041681, 0.04824239079542675],
[-0.07617805358108933, 0.07612545525147858],
[-0.047140552370394925, 0.04744151736320112],
[-0.05137018378775051, 0.051114804207469784],
[-0.03259493948312707, 0.0325308332802452],
[-0.05715909221362399, 0.05709963073119724],
[-0.04835633252739353, 0.04849600527981289],
[-0.0433886628912617, 0.04331087342221564],
[-0.05191740499328957, 0.05183144200010501],
[-0.022690824730811025, 0.02281282548488598],
[-0.021657892287654815, 0.02160585204290785],
[-0.019911292276869504, 0.01990373441321122],
[-0.05252214322669061, 0.052514338488489534],
[-0.045757900781809524, 0.04581189437809006],
[-0.02396372548560904, 0.023788207356191405],
[-0.053426097224355276, 0.05348064888976746],
[-0.05394891160261981, 0.05421456735805457],
[-0.05251658416178273, 0.05238904616093791],
[-0.04774168806444406, 0.047755594530669916],
[-0.03506924339896615, 0.035076784816174336],
[-0.044288649573623336, 0.044337743067559894],
[-0.05109649028135573, 0.050986769978167874],
[-0.03986396401411081, 0.03992226520835857],
[-0.06271544843396921, 0.0628629998182233],
[-0.060325113831802425, 0.06014867491287253],
[-0.06409607265208252, 0.06426716029136537],
[-0.02890807357828784, 0.02879981209701445],
[-0.0579076968762734, 0.058055472378755635],
[-0.0788244096514242, 0.07889209396389751],
[-0.05489594835332056, 0.054304463238473114],
[-0.05066376350430718, 0.051136225666937284],
[-0.04324084422009672, 0.043106921303429975],
[-0.03618639085199314, 0.03630391952984575],
[-0.03229893887218463, 0.032254130211298596],
[-0.040388961018727465, 0.04034166483632292],
[-0.06891322548088202, 0.06894551548689337],
[-0.05708462516274434, 0.05713687370165375],
[-0.0908320094478539, 0.09053809343169553],
[-0.06997210675874246, 0.07036387396569341],
[-0.027676689451677956, 0.02757377175784071],
[-0.02882633060378825, 0.029207481257562274],
[-0.0414701765332311, 0.04136630655327525],
[-0.05308296364144847, 0.0526747543606357],
[-0.02724146501450132, 0.027406581699254588],
[-0.04265844625269343, 0.04270290902986972],
[-0.03899306746018118, 0.038745252551468795],
[-0.0552804734553083, 0.05535944558193926],
[-0.02309096284644189, 0.023040044134232315],
[-0.0507964500028555, 0.05096013747702334],
[-0.04123972706510699, 0.041359046982264745],
[-0.03236153261658939, 0.032179960855430505],
[-0.02858521671477931, 0.028570736354436077],
[-0.03515761112679279, 0.03513507691850391],
[-0.049852204843317816, 0.04984858000374448],
[-0.038280519845162314, 0.038365751907998916],
[-0.05489151074836156, 0.054958999808454506],
[-0.02552547302215947, 0.025555844960312334],
[-0.06393766191228746, 0.0638978766928521],
[-0.04140103340243134, 0.04113465467714282],
[-0.04647459357809104, 0.04654619117779597],
[-0.03293849813553063, 0.03301029011724379],
[-0.04428244235309984, 0.04433992273438912],
[-0.047489538949244604, 0.04755256034371833],
[-0.047176763166566854, 0.04719291045558167],
[-0.06353201748860114, 0.06380784207550017],
[-0.07775209195691819, 0.0773872824070752],
[-0.054300174262817344, 0.054476381979975085],
[-0.08808678703605805, 0.0879414485377677],
[-0.04016286323725983, 0.04007725752721749],
[-0.01889086923709467, 0.018989486049242103]
]
const height = d3.scaleLinear()
.domain(d3.extent(renderData.map(e => e[1] - e[0])))
.range([0, elementHeight])
d3.select(document.getElementById('app'))
.append('svg')
.attr('class', 'd3')
.attr('width', elementWidth)
.attr('height', elementHeight)
.selectAll('.bar')
.data(renderData)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('fill', '#E0E0E0')
.attr('x', (d, i) => (i * 2 + i))
.attr('y', d => elementHeight - height(d[1] - d[0]))
.transition()
.duration(300)
.ease(d3.easeLinear)
.attr('width', 2)
.attr('height', d => height(d[1] - d[0]))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id='app' />
</body>
</html>
Its really fun to play with svg :)
This is what i made, have a look.
const elementWidth = 1100
const elementHeight = 64
var audioTotalTime = 120.0000; // in secound
var currentTime = 0;
var currentBuffer = 0;
const renderData = [
[-0.015067690176936956, 0.015065840696712662],
[-0.009963374263646985, 0.009960838406137254],
[-0.0329772714073922, 0.032922178973984494],
[-0.02010780853750818, 0.020192897509204638],
[-0.029141768346505944, 0.02913273608186522],
[-0.03390369982419367, 0.033888949138664096],
[-0.05309944789682607, 0.053106191954295334],
[-0.017992382356680794, 0.0179506794436456],
[-0.04118192967225779, 0.04120773269527067],
[-0.032132343283569134, 0.03223372926977138],
[-0.04340663941189386, 0.043317410948806916],
[-0.026866048759920942, 0.02695383570549558],
[-0.041548487297645216, 0.04142889765358134],
[-0.0512541217270734, 0.05128097373670754],
[-0.02645596673127562, 0.026461825339764114],
[-0.03276659370022165, 0.032869462727325334],
[-0.02983164709570332, 0.02965126735342542],
[-0.06186988270590101, 0.06228762507639405],
[-0.037202475771159274, 0.03684529067849468],
[-0.04496168984286248, 0.044984343262096924],
[-0.02961698097048877, 0.029580527280458145],
[-0.06637895360455075, 0.06584970915134748],
[-0.03966561332234608, 0.04028105442218536],
[-0.04888827685580639, 0.04879637577182824],
[-0.034440279218927505, 0.03448690299802526],
[-0.04076603383847427, 0.04087949817166488],
[-0.03422100968150345, 0.03407137586231854],
[-0.03420552026962888, 0.034233479991186845],
[-0.06124921943975816, 0.06133406711072517],
[-0.08080063612343565, 0.08052139740352077],
[-0.052296123826832304, 0.05245498821828788],
[-0.07728568068325997, 0.0772439557897976],
[-0.04070025960953707, 0.04072465208052425],
[-0.016598400103531252, 0.01673240062886387],
[-0.0495708419979178, 0.04952405213368158],
[-0.03402468183819489, 0.03404496946468417],
[-0.04719791564971553, 0.04716565090961255],
[-0.024305039710776202, 0.024425998358774473],
[-0.04539290174457686, 0.0453603392364138],
[-0.04291280211166326, 0.042803252613569195],
[-0.03237617188947045, 0.032430479168267405],
[-0.046939414609483046, 0.046991124408919255],
[-0.037727014544829074, 0.03756628029896137],
[-0.05813820211592722, 0.058137499737658825],
[-0.03306609736616569, 0.03332803022833292],
[-0.03706343131822335, 0.03699838219166897],
[-0.031640843865570666, 0.03150685332686255],
[-0.07978720110560034, 0.07982405111308474],
[-0.04565408283291298, 0.04548542047551325],
[-0.03838929844552628, 0.0386080775422541],
[-0.0349069030273341, 0.03516624962570975],
[-0.05791808093217102, 0.057646960595115364],
[-0.040111244425499945, 0.040190047578908046],
[-0.0421531094659709, 0.04210734133509555],
[-0.04358563889018587, 0.043380678911277275],
[-0.024025454017633886, 0.024179111399202893],
[-0.039038574013751944, 0.03889745017750074],
[-0.02962543563292595, 0.02975662299643922],
[-0.07215596460653108, 0.07225534620830149],
[-0.0845103969948925, 0.08417566858032748],
[-0.05029865141667644, 0.05110349428845409],
[-0.06766253837563593, 0.06680008803627584],
[-0.05413748268128195, 0.054261121431710246],
[-0.04702217202288801, 0.04710783667779247],
[-0.047177278676382065, 0.047241381909344966],
[-0.04949906253183499, 0.049358880485210296],
[-0.06384145451618915, 0.06398437795989458],
[-0.0532812223855561, 0.05336013656088595],
[-0.055032831282645335, 0.055131815418379866],
[-0.05771727930777607, 0.05743980672281111],
[-0.06865421948220482, 0.06896493506959074],
[-0.05163944571854085, 0.05129081551014095],
[-0.04546664828758613, 0.04549366890782257],
[-0.02196073923070452, 0.022119579288034315],
[-0.026824862238895183, 0.026915318981447094],
[-0.04771898452983383, 0.04768769589918763],
[-0.05221904154341058, 0.05202229643239835],
[-0.04034726803191834, 0.040288317010035164],
[-0.04252634158686052, 0.04275796625513488],
[-0.055381424446109724, 0.05515857756430962],
[-0.06160043085044191, 0.06143890271068376],
[-0.04579617210990365, 0.04612433751815954],
[-0.039244869887493206, 0.03927668403684328],
[-0.03426885260996771, 0.03423936180141113],
[-0.03516869910983574, 0.035127711830890515],
[-0.026964357386084752, 0.02699723933039285],
[-0.03816966714682839, 0.03778890745758835],
[-0.04777519168041681, 0.04824239079542675],
[-0.07617805358108933, 0.07612545525147858],
[-0.047140552370394925, 0.04744151736320112],
[-0.05137018378775051, 0.051114804207469784],
[-0.03259493948312707, 0.0325308332802452],
[-0.05715909221362399, 0.05709963073119724],
[-0.04835633252739353, 0.04849600527981289],
[-0.0433886628912617, 0.04331087342221564],
[-0.05191740499328957, 0.05183144200010501],
[-0.022690824730811025, 0.02281282548488598],
[-0.021657892287654815, 0.02160585204290785],
[-0.019911292276869504, 0.01990373441321122],
[-0.05252214322669061, 0.052514338488489534],
[-0.045757900781809524, 0.04581189437809006],
[-0.02396372548560904, 0.023788207356191405],
[-0.053426097224355276, 0.05348064888976746],
[-0.05394891160261981, 0.05421456735805457],
[-0.05251658416178273, 0.05238904616093791],
[-0.04774168806444406, 0.047755594530669916],
[-0.03506924339896615, 0.035076784816174336],
[-0.044288649573623336, 0.044337743067559894],
[-0.05109649028135573, 0.050986769978167874],
[-0.03986396401411081, 0.03992226520835857],
[-0.06271544843396921, 0.0628629998182233],
[-0.060325113831802425, 0.06014867491287253],
[-0.06409607265208252, 0.06426716029136537],
[-0.02890807357828784, 0.02879981209701445],
[-0.0579076968762734, 0.058055472378755635],
[-0.0788244096514242, 0.07889209396389751],
[-0.05489594835332056, 0.054304463238473114],
[-0.05066376350430718, 0.051136225666937284],
[-0.04324084422009672, 0.043106921303429975],
[-0.03618639085199314, 0.03630391952984575],
[-0.03229893887218463, 0.032254130211298596],
[-0.040388961018727465, 0.04034166483632292],
[-0.06891322548088202, 0.06894551548689337],
[-0.05708462516274434, 0.05713687370165375],
[-0.0908320094478539, 0.09053809343169553],
[-0.06997210675874246, 0.07036387396569341],
[-0.027676689451677956, 0.02757377175784071],
[-0.02882633060378825, 0.029207481257562274],
[-0.0414701765332311, 0.04136630655327525],
[-0.05308296364144847, 0.0526747543606357],
[-0.02724146501450132, 0.027406581699254588],
[-0.04265844625269343, 0.04270290902986972],
[-0.03899306746018118, 0.038745252551468795],
[-0.0552804734553083, 0.05535944558193926],
[-0.02309096284644189, 0.023040044134232315],
[-0.0507964500028555, 0.05096013747702334],
[-0.04123972706510699, 0.041359046982264745],
[-0.03236153261658939, 0.032179960855430505],
[-0.02858521671477931, 0.028570736354436077],
[-0.03515761112679279, 0.03513507691850391],
[-0.049852204843317816, 0.04984858000374448],
[-0.038280519845162314, 0.038365751907998916],
[-0.05489151074836156, 0.054958999808454506],
[-0.02552547302215947, 0.025555844960312334],
[-0.06393766191228746, 0.0638978766928521],
[-0.04140103340243134, 0.04113465467714282],
[-0.04647459357809104, 0.04654619117779597],
[-0.03293849813553063, 0.03301029011724379],
[-0.04428244235309984, 0.04433992273438912],
[-0.047489538949244604, 0.04755256034371833],
[-0.047176763166566854, 0.04719291045558167],
[-0.06353201748860114, 0.06380784207550017],
[-0.07775209195691819, 0.0773872824070752],
[-0.054300174262817344, 0.054476381979975085],
[-0.08808678703605805, 0.0879414485377677],
[-0.04016286323725983, 0.04007725752721749],
[-0.01889086923709467, 0.018989486049242103]
]
const height = d3.scaleLinear()
.domain(d3.extent(renderData.map(e => e[1] - e[0])))
.range([0, elementHeight])
d3.select(document.getElementById('app'))
.append('svg')
.attr('class', 'd3')
.attr('width', elementWidth)
.attr('height', elementHeight)
.selectAll('.bar')
.data(renderData)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('fill', '#E0E0E0')
.attr('x', (d, i) => (i * 2 + i))
.attr('y', d => elementHeight - height(d[1] - d[0]))
.transition()
.duration(300)
.ease(d3.easeLinear)
.attr('width', 2)
.attr('height', d => height(d[1] - d[0]))
var svg = $(".d3");
var lng = svg.find("rect").length;
function update(){
var selectedRect = Math.floor(((currentTime ) / ( lng )) * (audioTotalTime + 86));
var selectedBufferRect = Math.floor(((currentBuffer ) / ( lng )) * (audioTotalTime + 86));
// this is the best i could do, but you understand the ide
var playingColor = "red";
var bufferColor = "green";
// buffer Progress
$.each (svg.find("rect"), function(index, i){ // buffer
if (index<= selectedBufferRect &&$(this).attr("fill") != playingColor )
$(this).attr("fill", bufferColor);
});
// Playing Progress
$.each (svg.find("rect"), function(index, i){ // Playing
if (index<= selectedRect)
$(this).attr("fill", playingColor);
});
}
function PlayingSimulator(){
currentTime += 1
currentBuffer +=3;
if (currentTime>= audioTotalTime){
update();
return false;
}
update();
setTimeout(PlayingSimulator, 60);
}
PlayingSimulator();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id='app' />
</body>
</html>
Just add some simple logic in function paint() to determine the fill color. Then you need need to decide how often to re-render (I guess at least once every second for updating currentTime).
const elementWidth = 1100;
const elementHeight = 64;
const duration = 160;
let currentTime = 0;
const buffered = 140;
const renderData = [[-0.015067690176936956, 0.015065840696712662],[-0.009963374263646985, 0.009960838406137254],[-0.0329772714073922, 0.032922178973984494],[-0.02010780853750818, 0.020192897509204638],[-0.029141768346505944, 0.02913273608186522],[-0.03390369982419367, 0.033888949138664096],[-0.05309944789682607, 0.053106191954295334],[-0.017992382356680794, 0.0179506794436456],[-0.04118192967225779, 0.04120773269527067],[-0.032132343283569134, 0.03223372926977138],[-0.04340663941189386, 0.043317410948806916],[-0.026866048759920942, 0.02695383570549558],[-0.041548487297645216, 0.04142889765358134],[-0.0512541217270734, 0.05128097373670754],[-0.02645596673127562, 0.026461825339764114],[-0.03276659370022165, 0.032869462727325334],[-0.02983164709570332, 0.02965126735342542],[-0.06186988270590101, 0.06228762507639405],[-0.037202475771159274, 0.03684529067849468],[-0.04496168984286248, 0.044984343262096924],[-0.02961698097048877, 0.029580527280458145],[-0.06637895360455075, 0.06584970915134748],[-0.03966561332234608, 0.04028105442218536],[-0.04888827685580639, 0.04879637577182824],[-0.034440279218927505, 0.03448690299802526],[-0.04076603383847427, 0.04087949817166488],[-0.03422100968150345, 0.03407137586231854],[-0.03420552026962888, 0.034233479991186845],[-0.06124921943975816, 0.06133406711072517],[-0.08080063612343565, 0.08052139740352077],[-0.052296123826832304, 0.05245498821828788],[-0.07728568068325997, 0.0772439557897976],[-0.04070025960953707, 0.04072465208052425],[-0.016598400103531252, 0.01673240062886387],[-0.0495708419979178, 0.04952405213368158],[-0.03402468183819489, 0.03404496946468417],[-0.04719791564971553, 0.04716565090961255],[-0.024305039710776202, 0.024425998358774473],[-0.04539290174457686, 0.0453603392364138],[-0.04291280211166326, 0.042803252613569195],[-0.03237617188947045, 0.032430479168267405],[-0.046939414609483046, 0.046991124408919255],[-0.037727014544829074, 0.03756628029896137],[-0.05813820211592722, 0.058137499737658825],[-0.03306609736616569, 0.03332803022833292],[-0.03706343131822335, 0.03699838219166897],[-0.031640843865570666, 0.03150685332686255],[-0.07978720110560034, 0.07982405111308474],[-0.04565408283291298, 0.04548542047551325],[-0.03838929844552628, 0.0386080775422541],[-0.0349069030273341, 0.03516624962570975],[-0.05791808093217102, 0.057646960595115364],[-0.040111244425499945, 0.040190047578908046],[-0.0421531094659709, 0.04210734133509555],[-0.04358563889018587, 0.043380678911277275],[-0.024025454017633886, 0.024179111399202893],[-0.039038574013751944, 0.03889745017750074],[-0.02962543563292595, 0.02975662299643922],[-0.07215596460653108, 0.07225534620830149],[-0.0845103969948925, 0.08417566858032748],[-0.05029865141667644, 0.05110349428845409],[-0.06766253837563593, 0.06680008803627584],[-0.05413748268128195, 0.054261121431710246],[-0.04702217202288801, 0.04710783667779247],[-0.047177278676382065, 0.047241381909344966],[-0.04949906253183499, 0.049358880485210296],[-0.06384145451618915, 0.06398437795989458],[-0.0532812223855561, 0.05336013656088595],[-0.055032831282645335, 0.055131815418379866],[-0.05771727930777607, 0.05743980672281111],[-0.06865421948220482, 0.06896493506959074],[-0.05163944571854085, 0.05129081551014095],[-0.04546664828758613, 0.04549366890782257],[-0.02196073923070452, 0.022119579288034315],[-0.026824862238895183, 0.026915318981447094],[-0.04771898452983383, 0.04768769589918763],[-0.05221904154341058, 0.05202229643239835],[-0.04034726803191834, 0.040288317010035164],[-0.04252634158686052, 0.04275796625513488],[-0.055381424446109724, 0.05515857756430962],[-0.06160043085044191, 0.06143890271068376],[-0.04579617210990365, 0.04612433751815954],[-0.039244869887493206, 0.03927668403684328],[-0.03426885260996771, 0.03423936180141113],[-0.03516869910983574, 0.035127711830890515],[-0.026964357386084752, 0.02699723933039285],[-0.03816966714682839, 0.03778890745758835],[-0.04777519168041681, 0.04824239079542675],[-0.07617805358108933, 0.07612545525147858],[-0.047140552370394925, 0.04744151736320112],[-0.05137018378775051, 0.051114804207469784],[-0.03259493948312707, 0.0325308332802452],[-0.05715909221362399, 0.05709963073119724],[-0.04835633252739353, 0.04849600527981289],[-0.0433886628912617, 0.04331087342221564],[-0.05191740499328957, 0.05183144200010501],[-0.022690824730811025, 0.02281282548488598],[-0.021657892287654815, 0.02160585204290785],[-0.019911292276869504, 0.01990373441321122],[-0.05252214322669061, 0.052514338488489534],[-0.045757900781809524, 0.04581189437809006],[-0.02396372548560904, 0.023788207356191405],[-0.053426097224355276, 0.05348064888976746],[-0.05394891160261981, 0.05421456735805457],[-0.05251658416178273, 0.05238904616093791],[-0.04774168806444406, 0.047755594530669916],[-0.03506924339896615, 0.035076784816174336],[-0.044288649573623336, 0.044337743067559894],[-0.05109649028135573, 0.050986769978167874],[-0.03986396401411081, 0.03992226520835857],[-0.06271544843396921, 0.0628629998182233],[-0.060325113831802425, 0.06014867491287253],[-0.06409607265208252, 0.06426716029136537],[-0.02890807357828784, 0.02879981209701445],[-0.0579076968762734, 0.058055472378755635],[-0.0788244096514242, 0.07889209396389751],[-0.05489594835332056, 0.054304463238473114],[-0.05066376350430718, 0.051136225666937284],[-0.04324084422009672, 0.043106921303429975],[-0.03618639085199314, 0.03630391952984575],[-0.03229893887218463, 0.032254130211298596],[-0.040388961018727465, 0.04034166483632292],[-0.06891322548088202, 0.06894551548689337],[-0.05708462516274434, 0.05713687370165375],[-0.0908320094478539, 0.09053809343169553],[-0.06997210675874246, 0.07036387396569341],[-0.027676689451677956, 0.02757377175784071],[-0.02882633060378825, 0.029207481257562274],[-0.0414701765332311, 0.04136630655327525],[-0.05308296364144847, 0.0526747543606357],[-0.02724146501450132, 0.027406581699254588],[-0.04265844625269343, 0.04270290902986972],[-0.03899306746018118, 0.038745252551468795],[-0.0552804734553083, 0.05535944558193926],[-0.02309096284644189, 0.023040044134232315],[-0.0507964500028555, 0.05096013747702334],[-0.04123972706510699, 0.041359046982264745],[-0.03236153261658939, 0.032179960855430505],[-0.02858521671477931, 0.028570736354436077],[-0.03515761112679279, 0.03513507691850391],[-0.049852204843317816, 0.04984858000374448],[-0.038280519845162314, 0.038365751907998916],[-0.05489151074836156, 0.054958999808454506],[-0.02552547302215947, 0.025555844960312334],[-0.06393766191228746, 0.0638978766928521],[-0.04140103340243134, 0.04113465467714282],[-0.04647459357809104, 0.04654619117779597],[-0.03293849813553063, 0.03301029011724379],[-0.04428244235309984, 0.04433992273438912],[-0.047489538949244604, 0.04755256034371833],[-0.047176763166566854, 0.04719291045558167],[-0.06353201748860114, 0.06380784207550017],[-0.07775209195691819, 0.0773872824070752],[-0.054300174262817344, 0.054476381979975085],[-0.08808678703605805, 0.0879414485377677],[-0.04016286323725983, 0.04007725752721749],[-0.01889086923709467, 0.018989486049242103]];
const height = d3
.scaleLinear()
.domain(d3.extent(renderData.map(e => e[1] - e[0])))
.range([0, elementHeight]);
const svg = d3
.select("#app")
.append("svg")
.attr("class", "d3")
.attr("width", elementWidth)
.attr("height", elementHeight);
const bar = svg.selectAll(".bar").data(renderData);
const barEnter = bar
.enter()
.append("rect")
.attr("class", "bar")
.attr("fill", (d, i) => {
return paint(i);
})
.attr("x", (d, i) => i * 2 + i)
.attr("y", d => elementHeight - height(d[1] - d[0]))
.attr("width", 2)
.attr("height", d => height(d[1] - d[0]));
function reRender() {
currentTime += 1
barEnter.attr("fill", (d, i) => {
return paint(i);
})
}
window.setInterval(function () {
reRender()
}, 1000);
function paint(i) {
barPos = i / renderData.length * duration;
if (barPos <= currentTime) {
return "red";
} else if (barPos <= buffered) {
return "orange";
} else {
return "grey";
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id='app' />
</body>
</html>
Codepen