I'm trying to create a data visualization with pure JS. I can't find any examples of how to do this that don't rely on third-party libraries.
Let's say you have some data:
data = {
green = 20%;
yellow = 30%;
red = 50%;
}
And you have created a rectangle in HTML that you can fill with color.
How can I use JS to fill the colors according to the percentage of each color? As you'll see in my code below, right now I've just hard-coded in the color percentages. But I want it to be dynamic so it updates when the data changes.
Here's what I have so far:
const drawColors = () => {
const colorBar = document.getElementById("color-bar");
const context = colorBar.getContext('2d');
// Hard coding percentages
context.beginPath();
context.fillStyle = "green";
context.fillRect(0, 0, 100, 100);
context.fillStyle = "yellow"
context.fillRect(100, 0, 100, 100);
context.fillStyle = "red"
context.fillRect(200, 0, 100, 100);
}
drawColors();
<canvas id="color-bar" width="300" height="100">
Assuming you validate somewhere else that percentages values all add up to 100, iterate through the percentages object and fill like this:
const percentages = {
green: 50,
yellow: 30,
red: 20
}
const drawColors = () => {
const colorBar = document.getElementById("color-bar")
const context = colorBar.getContext('2d')
//Calculate pixels from percentage based on canvas width.
const toPixels = percentage => (percentage*colorBar.width)/100
context.beginPath()
let addedColors = 0 //To set the starting point of each fill.
Object.keys(percentages).forEach(color => {
let widthToPaint = toPixels(percentages[color])
context.fillStyle = color
context.fillRect(addedColors, 0, widthToPaint , 100)
addedColors+= widthToPaint
})
}
drawColors()
<canvas id="color-bar" width="300" height="100">
We can use createLinearGradient() for this. Just converted 20% to 0.2 etc (sum has to be 1 or less);
data = {
green : 0.2,
yellow : 0.3,
red : 0.5
}
drawColorBar('canvas',data);
function drawColorBar(selector, data){
const canvas = document.querySelector(selector);
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0,0,canvas.width,0);
let start = 0;
for(const color in data){
gradient.addColorStop(start, color);
start += data[color];
gradient.addColorStop(start, color);
}
ctx.fillStyle = gradient;
ctx.fillRect(0,0,canvas.width,canvas.height);
}
<canvas></canvas>
Related
I can really use some help, my goal is to add few lines to the canvas in different places, it means to use the function drawText few times, for some reason if I don't use drawText inside the onload of drawImage the text does not rendering I am really stuck and can use some help, my main target it to make website that can edit pictures and make memes (e.g.: https://imgflip.com/memegenerator)and adding text line is what i am getting stuck on because I don't understand how to render new text line while save the old one, every time i start new line its like the image rendering all over again and start from the beginning.
// Function to load image on the canvas
function drawImg(url) {
gCurrLineIdx = getCurrLineIdx();
let currLine = gMeme.lines[gCurrLineIdx];
const img = new Image();
img.src = url;
img.onload = () => {
gCtx.drawImage(img, 0, 0, gElCanvas.width, gElCanvas.height);
//If you dont call drawText the text does not render to the canvas
drawText(currLine.txt, currLine.size, currLine.fontColor, currLine.strokeColor, currLine.align, currLine.font, gElCanvas.width / 2, currLine.y);
};
}
// Function to add text on the canvas
function drawText(text = '', fontSize = 20, fontColor = 'white', strokeColor = 'black', align = 'center', font = "ariel", x = gElCanvas.width / 2, y = 20) {
gCtx.strokeStyle = strokeColor;
gCtx.fillStyle = fontColor;
gCtx.font = `${fontSize}px ${font}`;
gCtx.textAlign = align;
gCtx.fillText(text, x, y);
gCtx.strokeText(text, x, y);
}
//Function to add new text line on the canvas.
function onAddTextLine() {
let textInput = document.getElementById('text-input');
textInput.value = '';
addTextLine();
gCurrLineIdx = getCurrLineIdx();
var currLine = gMeme.lines[gCurrLineIdx];
drawText(currLine.txt, currLine.size, currLine.fontColor, currLine.strokeColor, currLine.align, currLine.font, gElCanvas.width / 2, currLine.y);
}
The question I see here is:
how to render new text line while save the old one
Looks like in your code you are drawing things independently, below is a different approach the class Meme has a few functions to add text and image but every time anything change the entire canvas is cleared (clearRect) and every element is re-drawn, the magic is in the this.draw(), that is the call to the common function that does all the drawing.
To keep my sample small I hard-coded many of the things you had as parameters, it should be easy for you to reintroduce them if you need
class Meme {
constructor(ctx, width, height) {
this.ctx = ctx;
this.ctx.font = '80px Arial';
this.ctx.fillStyle = "blue"
this.ctx.textAlign = "center"
this.width = width;
this.height = height;
}
addHeadText(text) {
this.htext = text;
this.draw();
}
addFootText(text) {
this.ftext = text;
this.draw();
}
addImage(image_src) {
this.image = new Image();
this.image.src = image_src;
this.image.onload = () => {
this.draw()
};
}
draw() {
this.ctx.beginPath();
this.ctx.clearRect(0, 0, this.width, this.height)
if (this.image) {
this.ctx.drawImage(this.image, 0, 0, this.width, this.height);
}
if (this.htext) {
this.ctx.textBaseline = "top";
this.ctx.fillText(this.htext, this.width / 2, 0);
}
if (this.ftext) {
this.ctx.textBaseline = "bottom";
this.ctx.fillText(this.ftext, this.width / 2, this.height);
}
this.ctx.closePath();
}
}
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
meme = new Meme(ctx, canvas.width, canvas.height)
meme.addHeadText("Hello");
meme.addFootText("World");
meme.addImage("http://i.stack.imgur.com/UFBxY.png");
document.getElementById('htext').onkeyup = (event) => {
meme.addHeadText(event.srcElement.value);
};
document.getElementById('ftext').onkeyup = (event) => {
meme.addFootText(event.srcElement.value);
};
Head Text:<input type="text" id="htext" name="ftext" style="width: 140px;" value="Hello"><br>
Foot Text:<input type="text" id="ftext" name="ftext" style="width: 140px;" value="World"><br>
<canvas id="c" width=280 height=380 style="border:2px solid red;"></canvas>
I want save canvas in every 2 minutes.
document.getElementById('canvasUrl').value = canvas.toDataURL();
I try:
document.getElementById('canvasUrl').value += canvas.toDataURL();
//or
var canvasValue += canvas.toDataURL();
document.getElementById('canvasUrl').value = canvasValue;
both can't work! Why?
You can but you need to combine the image data first prior to using a dataURL here is an example. Please note it's very verbose I imagine you'd want multiple canvases in an array or something along those lines.
How this works is it takes the data from the first 3 canvases then draws them to the 4th, the 4th canvases dataURL is what's used which has the data from the previous 3 canvases combined.
const canvas1 = document.querySelector('#first');
const ctx1 = canvas1.getContext('2d');
const canvas2 = document.querySelector('#second');
const ctx2 = canvas2.getContext('2d');
const canvas3 = document.querySelector('#third');
const ctx3 = canvas3.getContext('2d');
ctx1.fillStyle = 'red';
ctx1.fillRect(0, 0, 100, 100);
ctx2.fillStyle = 'blue';
ctx2.fillRect(100, 100, 100, 100);
ctx3.fillStyle = 'green';
ctx3.fillRect(10, 10, 100, 10);
const saveCanvas = document.querySelector('#saved');
const ctxSave = saveCanvas.getContext('2d');
// Draw each of our other canvases onto the canvas we're going to save from.
ctxSave.drawImage(canvas1, 0, 0);
ctxSave.drawImage(canvas2, 0, 0);
ctxSave.drawImage(canvas3, 0, 0);
document.getElementById('canvasUrl').value = saveCanvas.toDataURL();
canvas {
border: 1px solid black;
}
<div>
<label>Saved Canvas Data</label>
<input id="canvasUrl" type="text">
</div>
<div>
<canvas id="first"></canvas>
<canvas id="second"></canvas>
<canvas id="third"></canvas>
<canvas id="saved"></canvas>
</div>
I have 2 images of a pirate, one with the shape, and one with the pirate itself. I want to merge that 2 pictures together to create an image with transparant borders to use into my canvas. The only problem is i cant find a way to actually manage this.
The images itself are naturally not transparant at all, so i played with the Composite options in javascript, to try to make it transparant with the 'lighter' option, but i cant find a way to get rid of the white borders around the image.
This is a part of the code i currently have:
ctx.canvas.width = pirate_shape.width;
ctx.canvas.height = pirate_shape.height;
ctx.clearRect(0, 0, pirate_shape.width, pirate_shape.height);
// Draw the shape in reversed colors so i can draw the picture inside the shape.
ctx.drawImage(pirate_shape, 0, 0);
ctx.globalCompositeOperation='difference';
ctx.fillStyle='white';
ctx.fillRect(0, 0, pirate_shape.width, pirate_shape.height);
// Draw the pirate itself
ctx.globalCompositeOperation = 'lighter';
ctx.drawImage(pirate, 0, 0);
The complete code: https://jsfiddle.net/0kbj2Leg/
The result im trying to get:
https://imgur.com/uL0Pf4T
The 2 images i used:
(the pirate)
https://imgur.com/8R1qutU
(the shape)
https://imgur.com/qemOzU2
Do you guys know maybe an way to get that result?
Thanks!
You want to use compositing, not blending.
To do this, you need transparency. Your images don't contain any.
Since your border image is black and white, there is actually an easy way in modern browsers to convert the white to transparent pixels: svg filters.
Once done, we can use compositing to achieve our goal.
// just the assets loader
(async () => {
const [face, border, back] = await Promise.all(
[
'https://i.imgur.com/8R1qutU.png',
'https://imgur.com/qemOzU2.png',
'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png'
].map(url => {
const img = new Image();
img.src = url;
return new Promise(res => {
img.onload = e => res(img);
});
})
);
const ctx = document.getElementById('canvas').getContext('2d');
// generate the border transparency based on luminance
// use the brightness to control how much red we keep
ctx.filter = 'brightness(180%) url(#trans)';
ctx.drawImage(border, 10, 10);
ctx.filter = 'none';
await wait(1000); // only for demo
// draw the face only where we already have the border drawn
ctx.globalCompositeOperation = 'source-in';
ctx.drawImage(face, 10, 10);
await wait(1000); // only for demo
// draw the background behind
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(back, -100,0, 400,300);
// reset
ctx.globalCompositeOperation = 'source-over';
})();
// only for demo, so we can see the different steps
function wait(ms) { return new Promise(res => setTimeout(res, ms)) }
canvas {
background-color: ivory;
border: 1px solid lightgray;
}
<canvas id="canvas" width="200" height="200"></canvas>
<svg style="width:0px;height:0px;position:absolute;z-index:-1">
<defs>
<filter id="trans">
<feColorMatrix type="luminanceToAlpha"/>
</filter>
</defs>
</svg>
Now, it would obviously be a lot easier if you can prepare your assets correctly from the beginning:
// just the assets loader
(async () => {
const [face, border, back] = await Promise.all(
[
'https://i.imgur.com/8R1qutU.png',
'https://i.stack.imgur.com/778ZM.png', // already wth transparency
'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png'
].map(url => {
const img = new Image();
img.src = url;
return new Promise(res => {
img.onload = e => res(img);
});
})
);
const ctx = document.getElementById('canvas').getContext('2d');
ctx.drawImage(border, 10, 10);
// draw the face only where we already have the border drawn
ctx.globalCompositeOperation = 'source-in';
ctx.drawImage(face, 10, 10);
// draw the background behind
ctx.globalCompositeOperation = 'destination-over';
ctx.drawImage(back, -100,0, 400,300);
// reset
ctx.globalCompositeOperation = 'source-over';
})();
canvas {
background-color: ivory;
border: 1px solid lightgray;
}
<canvas id="canvas" width="200" height="200"></canvas>
But you could probably even get rid of the border image entirely by drawing a shadow over a well prepared pirate image:
// just the assets loader
(async () => {
const [face,back] = await Promise.all(
[
'https://i.stack.imgur.com/1hzcD.png',
'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png'
].map(url => {
const img = new Image();
img.src = url;
return new Promise(res => {
img.onload = e => res(img);
});
})
);
const ctx = document.getElementById('canvas').getContext('2d');
// draw the background
ctx.drawImage(back, -100,0, 400,300);
// draw the face with a shadow
ctx.shadowBlur = 15;
ctx.shadowColor = "red";
// little hack to make shadow more opaque
// first move your shadow away from what you wish to draw
ctx.shadowOffsetX = face.width + 10; // + 10 because we'll draw the last one at x:10
ctx.shadowOffsetY = face.height + 10;
// now draw your shape outside of the visible area
ctx.drawImage(face, -face.width, -face.height);
await wait(1000); // just for demo
ctx.drawImage(face, -face.width, -face.height);
// we now have to shadows overlapping but not our shape
await wait(1000); // just for demo
// reset the shadowOffset
ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
// adn draw the last one
ctx.drawImage(face, 10, 10);
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
})();
// only for demo, so we can see the different steps
function wait(ms) { return new Promise(res => setTimeout(res, ms)) }
canvas {
background-color: ivory;
border: 1px solid lightgray;
}
<canvas id="canvas" width="200" height="200"></canvas>
I'm trying to create an app that lets you create a background gradient with two different colors,I'm using React. The first color of the gradient looks as it should, but the second color of the gradient is more of a solid color, with a jagged border. Heres a picture to demonstrate:
My goal is to get something that closer resembles to this:
Where the colors blend.
I'm referring to the MDN docs , and have messed around with the radius and x, y properties. I'm changing the canvas based on prop changes from the parent component, here's my code:
import React, { Component } from "react";
class Canvas extends Component {
componentDidMount() {
const { gradientOne, gradientTwo } = this.props.canvasState.backgroundColor;
this.ctx = this.canvas.getContext("2d");
this.radialGradient = this.ctx.createRadialGradient(
0,
0,
300,
260,
160,
100
);
this.ctx.fillStyle = this.radialGradient;
this.ctx.rect(0, 0, this.canvas.width, this.canvas.height);
this.radialGradient.addColorStop(0, gradientOne);
this.radialGradient.addColorStop(1, gradientTwo);
this.ctx.fill();
}
componentDidUpdate(prevProps, prevState) {
const { gradientOne, gradientTwo } = this.props.canvasState.backgroundColor;
if (prevProps.canvasState.backgroundColor.gradientOne !== gradientOne) {
this.ctx.fillStyle = this.radialGradient;
this.radialGradient.addColorStop(0, gradientOne);
this.ctx.fill();
} else if (
prevProps.canvasState.backgroundColor.gradientTwo !== gradientTwo
) {
this.ctx.fillStyle = this.radialGradient;
this.radialGradient.addColorStop(1, gradientTwo);
this.ctx.fill();
}
}
render() {
return (
<main className="canvasContainer">
<canvas ref={ref => (this.canvas = ref)} id="canvas">
YOUR BROWSER DOESN'T SUPPORT THIS FEATURE :(
</canvas>
</main>
);
}
}
export default Canvas;
Thanks for any help!
Color stops of CanvasGradient (be it linear or radial) can't be modified nor removed.
When you add a new color stop, at the same index than a previous one, it gets placed right after that previous one. So instead of having only two color stops, you have four.
This means that if you have an original gradient like so
<-red -------------------------------------------- green->
and that you add two new color stops blue and yellow at position 0 and 1, you will have something like
<-red[blue -------------------------------- green]yellow->
That is, no gradient between red and blue, nor between yellow and green:
const ctx = canvas.getContext('2d');
ctx.strokeStyle = 'white';
// an horizontal gradient
// 0 is at pixel 50, and 1 at pixel 250 on the x axis
const grad = ctx.createLinearGradient(50,0,250,0);
grad.addColorStop(0, 'red');
grad.addColorStop(1, 'green');
ctx.fillStyle = grad;
// top is two color stops version
ctx.fillRect(0,0,300,70);
// bottom is four color stops version
grad.addColorStop(0, 'blue');
grad.addColorStop(1, 'yellow');
ctx.fillStyle = grad;
ctx.fillRect(0,80,300,70);
// mark color stops
ctx.moveTo(49.5,0);
ctx.lineTo(49.5,150);
ctx.moveTo(249.5,0);
ctx.lineTo(249.5,150);
ctx.stroke();
canvas { border: 1px solid };
<canvas id="canvas"></canvas>
That's what you are doing in your code, since you add two color stops in componentDidMount, and then add more in componentDidUpdate.
To avoid this, simply overwrite your gradient property in componentDidUpdate, so that you start everytime with a new gradient.
const ctx = canvas.getContext('2d');
let radialGradient;
function didMount() {
radialGradient = ctx.createRadialGradient(
0,0,300,
260,160,100
);
radialGradient.addColorStop(0, c1.value);
radialGradient.addColorStop(1, c2.value);
ctx.fillStyle = radialGradient;
ctx.fillRect(0,0,canvas.width,canvas.height);
}
function didUpdate() {
// reset radialGradient to a new one
radialGradient = ctx.createRadialGradient(
0,0,300,
260,160,100
);
radialGradient.addColorStop(0, c1.value);
radialGradient.addColorStop(1, c2.value);
ctx.fillStyle = radialGradient;
ctx.fillRect(0,0,canvas.width,canvas.height);
}
didMount();
c1.oninput = c2.oninput = didUpdate;
canvas { border: 1px solid };
<input id="c1" type="color" value="#22CC22">
<input id="c2" type="color" value="#FF2222">
<canvas id="canvas"></canvas>
I have a canvas on the page, I draw an image into it, now I would like to apply filters like brightness, contrast, sepia etc...
The problem comes when a user chooses filters, cause the user needs to be able to select and un-select filters.
So if for example, the user chooses sepia and contrast filter, I will apply both filters to the canvas, but if he un-select sepia I need to remove the sepia filter from the canvas (kind reset filters and apply only contrast)... and so on for all the available filters.
How can I achieve this?
Is there some easy way?
What I tried is quite awful at the moment:
function applyFilter(filter) {
var canvas = document.getElementById('canvas')
, context = canvas.getContext('2d');
if (filter === 'sepia') {
context.sepia(1);
}
if (filter === 'contrast') {
context.contrast(1.3);
}
}
function removeFilter(filter) {
var canvas = document.getElementById('canvas')
, context = canvas.getContext('2d');
if (filter === 'sepia') {
context.sepia(0);
}
if (filter === 'contrast') {
context.contrast(1);
}
}
You could clone the canvas to a hidden buffer and have 2 versions of the canvas then use the buffer to reset when removing a filter?
http://jsfiddle.net/grzuo9he/
var canvas = document.getElementById('canvas'),
buffer = document.getElementById('buffer'),
context = canvas.getContext("2d"),
bufferContext = buffer.getContext("2d"),
activeFilters = [];
context.moveTo(20, 20);
context.lineTo(100, 20);
context.fillStyle = "#999";
context.beginPath();
context.arc(100,100,75,0,2*Math.PI);
context.fill();
//Make buffer a clone of canvas
bufferContext.drawImage(canvas, 0, 0);
function applyFilter(filter) {
if(filter){
activeFilters.push(filter);
}
if (activeFilters.indexOf('sepia') > -1) {
context.sepia(1);
}
if (activeFilters.indexOf('contrast') > -1) {
context.contrast(1.3);
}
}
function removeFilter(filter) {
//Reset the canvas
context.drawImage(buffer, 0, 0);
//Remove this filter froma active filters
activeFilters.splice(activeFilters.indexOf(filter), 1);
applyFilter(false);
}
You could use 'none' property
none : No filter is applied. Initial value.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = document.getElementById('source');
ctx.filter = 'contrast(1.4)';
ctx.drawImage(image, 10, 10, 180, 120);
ctx.filter = 'none';
https://developer.mozilla.org/fr/docs/Web/API/CanvasRenderingContext2D/filter