I am using typescript to draw canvas element. I would like to make my canvas element responsive to the screen size. So that I can match the canvas size to the parent div element.
I have tried to use this removing the size in canvas and providing it in .css. But this doesn't help because I have two fixed elements. I have found this solution which is 8 years old canvas resize.
How can I approach this problem ? The answer which I have posted distortes the image quality way too much. Is there any other approach ?
export class bar {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private width_canvas: number;
private height_canvas: number;
constructor(canvas: HTMLCanvasElement) {
this.canvas = < HTMLCanvasElement > canvas;
this.ctx = < CanvasRenderingContext2D > canvas.getContext('2d');
this.width_canvas = this.canvas.width;
this.height_canvas = this.canvas.height;
window.requestAnimationFrame(() => this.draw());
};
draw() {
let wid_bar: number = this.width_canvas - 400;
this.value.forEach((val, idx) => {
// draw bar background
this.ctx.save();
this.ctx.beginPath();
this.ctx.rect(200, (idx * (80)), wid_bar, 30);
this.ctx.fillStyle = yellow;
this.ctx.fill();
window.requestAnimationFrame(() => this.draw());
};
}
<div class="bar">
<canvas id="canvas" width="1200" height="200"> This does not work </canvas>
</div>
try (if you use CSS width only without redrawing then picture it will lose quality) instead input value read screen size
function change(inp) {
can.width = inp.value;
can.height = inp.value/4;
draw(can, [5,10,15,20])
}
function draw(canvas,value) {
let ctx = can.getContext('2d');
let wid_bar = canvas.width - 400;
value.forEach((val, idx) => {
// draw bar background
ctx.save();
ctx.beginPath();
ctx.rect(200, idx * 80, wid_bar, 30);
ctx.fillStyle = 'yellow';
ctx.fill();
});
}
change(audio);
canvas { background: black; }
<input id="audio" type="range" min="100" max="600" oninput="change(this)" value=150 /><br>
<canvas id="can"></canvas>
Related
Introduction
I'm trying to deal with blurry visuals on my canvas animation. The blurriness is especially prevalent on mobile-devices, retina and high-dpi (dots-per-inch) screens.
I'm looking for a way to ensure the pixels that are drawn using the canvas look their best on low-dpi screens and high-dpi screens. As a solution to this problem I red multiple articles about canvas-down-scaling and followed this tutorial:
https://www.kirupa.com/canvas/canvas_high_dpi_retina.htm
Integrating down-scaling in the project
The project in which I want to implement down-scaling can be found below and consists of a few important features:
There is a (big) main canvas. (Performance optimization)
There are multiple (pre-rendered) smaller canvasses that are used to draw and load a image into. (Performance optimization)
The canvas is animated. (In the code snippet, there is no visible animation but the animation function is intergrated.)
Question
What im trying to achieve: The problem I'm facing seems quite simple. When the website (with the canvas) is opened on a mobile device (eg. an Iphone, with more pixels per inch then a regular desktop). The images appear more blurry. What I'm actually trying to achieve is to remove this blurriness from the images. I red this and it stated that blurriness can be removed by downsampling. I tried to incorporate this technique in the code provided, but it did not work completely. The images just became larger and I was unable to scale the images back to the original size. snippet it is not implemented correctly, the output is still blurry. What did I do wrong and how am I able to fix this issue?
Explanation of the code snippet
The variable devicePixelRatio is set to 2 to simulate a high-dpi phone screen, low-dpi screens have a devicePixelRatio of 1.
Multiple pre-rendered canvasses generated is the function spawn is the snippet there are 5 different canvasses, but on the production environment there are 10's.
If there are any pieces of information missing or questions about this post, please let me know. Thanks a lot!
Code Snippet
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d' );
var circles = [];
//Simulate Retina screen = 2, Normal screen = 1
let devicePixelRatio = 2
function mainCanvasPixelRatio() {
// get current size of the canvas
let rect = canvas.getBoundingClientRect();
// increase the actual size of our canvas
canvas.width = rect.width * devicePixelRatio;
canvas.height = rect.height * devicePixelRatio;
// ensure all drawing operations are scaled
c.scale(devicePixelRatio, devicePixelRatio);
// scale everything down using CSS
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
}
// Initial Spawn
function spawn() {
for (let i = 0; i < 2; i++) {
//Set Radius
let radius = parseInt(i*30);
//Give position
let x = Math.round((canvas.width/devicePixelRatio) / 2);
let y = Math.round((canvas.height /devicePixelRatio) / 2);
//Begin Prerender canvas
let PreRenderCanvas = document.createElement('canvas');
const tmp = PreRenderCanvas.getContext("2d");
//Set PreRenderCanvas width and height
let PreRenderCanvasWidth = ((radius*2)*1.5)+1;
let PreRenderCanvasHeight = ((radius*2)*1.5)+1;
//Increase the actual size of PreRenderCanvas
PreRenderCanvas.width = PreRenderCanvasWidth * devicePixelRatio;
PreRenderCanvas.height = PreRenderCanvasHeight * devicePixelRatio;
//Scale PreRenderCanvas down using CSS
PreRenderCanvas.style.width = PreRenderCanvasWidth + 'px';
PreRenderCanvas.style.height = PreRenderCanvasHeight + 'px';
//Ensure PreRenderCanvas drawing operations are scaled
tmp.scale(devicePixelRatio, devicePixelRatio);
//Init image
const image= new Image();
//Get center of PreRenderCanvas
let m_canvasCenterX = (PreRenderCanvas.width/devicePixelRatio) * .5;
let m_canvasCenterY = (PreRenderCanvas.height/devicePixelRatio) * .5;
//Draw red circle on PreRenderCanvas
tmp.strokeStyle = "red";
tmp.beginPath();
tmp.arc((m_canvasCenterX), (m_canvasCenterY), ((PreRenderCanvas.width/devicePixelRatio)/3) , 0, 2 * Math.PI);
tmp.lineWidth = 2;
tmp.stroke();
tmp.restore();
tmp.closePath()
//Set Image
image .src= "https://play-lh.googleusercontent.com/IeNJWoKYx1waOhfWF6TiuSiWBLfqLb18lmZYXSgsH1fvb8v1IYiZr5aYWe0Gxu-pVZX3"
//Get padding
let paddingX = (PreRenderCanvas.width/devicePixelRatio)/5;
let paddingY = (PreRenderCanvas.height/devicePixelRatio)/5;
//Load image
image.onload = function () {
tmp.beginPath()
tmp.drawImage(image, paddingX,paddingY, (PreRenderCanvas.width/devicePixelRatio)-(paddingX*2),(PreRenderCanvas.height/devicePixelRatio)-(paddingY*2));
tmp.closePath()
}
let circle = new Circle(x, y, c ,PreRenderCanvas);
circles.push(circle)
}
}
// Circle parameters
function Circle(x, y, c ,m_canvas) {
this.x = x;
this.y = y;
this.c = c;
this.m_canvas = m_canvas;
}
//Draw circle on canvas
Circle.prototype = {
//Draw circle on canvas
draw: function () {
this.c.drawImage( this.m_canvas, (this.x - (this.m_canvas.width)/2), (this.y - this.m_canvas.height/2));
}
};
// Animate
function animate() {
//Clear canvas each time
c.clearRect(0, 0, (canvas.width /devicePixelRatio), (canvas.height /devicePixelRatio));
//Draw in reverse for info overlap
circles.slice().reverse().forEach(function( circle ) {
circle.draw();
});
requestAnimationFrame(animate);
}
mainCanvasPixelRatio()
spawn()
animate()
#mainCanvas {
background:blue;
}
<canvas id="mainCanvas"></canvas>
<br>
<!DOCTYPE html>
<html>
<body>
<p>Image to use:</p>
<img id="scream" width="220" height="277"
src="pic_the_scream.jpg" alt="The Scream">
<p>Canvas:</p>
<canvas id="myCanvas" width="240" height="297"
style="border:1px solid #d3d3d3;">
</canvas>
<script>
window.onload = function() {
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var img = document.getElementById("scream");
ctx.drawImage(img, 10, 10);
};
</script>
</body>
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'm quite new to svelte and I'm trying to get a canvas to render on the full screen using svelte. Sounds quite easy to do, but I can't get it to work properly. I'm binding a width and height variable to the clientWidth/clientHeight of the parent and using these variables to set the dimensions of the canvas. The issue now is that when onMount is called, the width and height variables are set but they are not applied to the canvas element yet. This means when the canvas renders for the first time, it still has the initial dimensions and not the ones of the parent. Only when I render it a second time it has the proper dimensions. How can get the canvas to have the right dimensions on the first render or render the canvas again when it has the proper dimensions?
Here you can find a "working" version.
<script>
import { onMount } from "svelte";
let canvas;
let ctx;
let width = 1007;
let height = 1140;
const draw = () => {
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
ctx.moveTo(width/2 - 50, height/2);
ctx.arc(width/2, height/2, 50, 0, 2 * Math.PI);
ctx.fill();
}
onMount(() => {
ctx = canvas.getContext("2d");
draw();
setTimeout(draw, 5000);
});
</script>
<style>
.container {
width: 100%;
height: 100%;
}
</style>
<div
class="container"
bind:clientWidth={width}
bind:clientHeight={height}>
<canvas bind:this={canvas} {width} {height} />
</div>
You can solve this by waiting for the next 'tick'
import { onMount, tick } from 'svelte';
onMount(async () => {
ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
await tick()
draw();
});
What await tick() does is to effectively pause execution until all current changes have been applied to the dom
tick in the docs
I've been stuck for days on a problem and I can't seem to find a solution. There is a gap between the cursor and the drawing and I don't know how to fix it.
I've set the height/width of the canvas in the HTML instead of in the CSS file, I've tried different ways to get the cursor position and also adding/removing some pixels to correct the gap on the lineTo line but it doesn't change anything.
For information, my canvas is located next to a map in a container with a width of 40%, I don't know if this is the problem but I don't want to change this.
class Canvas {
constructor(canvasId) {
this.canvas = canvasId;
this.ctx = this.canvas.getContext('2d');
this.signing = false;
}
load() {
this.start();
this.draw();
this.stop();
}
start() {
this.canvas.addEventListener('mousedown', () => {
this.signing = true;
const rectangle = this.canvas.getBoundingClientRect(e);
this.ctx.moveTo(e.clientX - rectangle.left, e.clientY - rectangle.top);
this.beginPath();
});
}
stop() {
this.canvas.addEventListener('mouseup', () => {
this.signing = false;
this.ctx.beginPath();
});
}
draw() {
this.canvas.addEventListener('mousemove', (e) => {
if (this.signing === true) {
this.ctx.lineWidth = '2';
this.ctx.lineCap = "round";
this.ctx.lineTo(e.clientX - this.canvas.offsetLeft - 65, e.clientY - this.canvas.offsetTop - 40);
this.ctx.stroke();
}
});
}
};
const signature = new Canvas(document.getElementById('to-sign'));
signature.load();
canvas {
border: solid;
}
<canvas id="to-sign" class="full-width" width="620" height="160">
Non supporté sur votre navigateur
</canvas>
This issue arises if you have a padding on your canvas. getBoundingClientRect returns the position and size of the element's border box. This includes the padding. See this Introduction to the CSS Box Model and this reference of getBoundingClientRect
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>