How to visualize a sine wave with custom frequency and amplitude? - javascript

I am trying to plot a sine wave into the canvas: I want to control the frequency like if I say:
10 freq, I want to see 10 periods of oscillating waves with the same amplitude etc.
Currently 10 freq works ok, but If I say 49 frequency, I see a weird change in amplitude. What is causing this and what is my error?
In the example I overlap two different waves to save on space.
let element = document.getElementById('app');
let canvas = document.createElement('canvas');
canvas.width = 320;
canvas.height = 180;
let bounds = element.getBoundingClientRect();
canvas.style.width = bounds.width + 'px';
canvas.style.height = bounds.height + 'px';
element.appendChild(canvas);
let ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 320, 180);
let points = [];
function fSin(freq, amplitude) {
return x =>
Math.sin(x / 100 * freq * Math.PI * 2) * amplitude;
}
function generate(ff) {
let res = [];
for (let i = 0; i < 100; i++) {
res.push([i, ff(i)]);
}
return res;
}
function drawPoints(points, xscale = 1, yscale = 1) {
ctx.moveTo(points[0][0] * xscale, 90 - points[0][1] * yscale);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0] * xscale, 90 - points[i][1] * yscale);
}
ctx.stroke();
}
points = generate(fSin(10, 10));
points = points.concat(generate(fSin(49, 10)));
drawPoints(points, 3.2, 2);
#app {
width: 90vw;
height: 90vh;
margin: auto;
}
#app canvas {
margin: auto;
}
<div id="app"></div>

What you're seeing is just an illusion due to aliasing, the interplay between the sampling rate (where the points are captured) and the sine function. If you were to zoom in and add more in-between points, you'd see that it's still a regular sine function. The omission of the in-between points due to sampling and connecting those samples with lines distorts the actual curve.
For fun, I modified your code a bit and added a slider. In this version, single pixel points are drawn instead, and the slider lets to interactively adjust the frequency. You can see as you increase/decrease the frequency how the samples seem to form weird patterns.
let element = document.getElementById('app');
let canvas = document.createElement('canvas');
canvas.width = 320;
canvas.height = 180;
canvas.style.width = '320px';
canvas.style.height = '180px';
let ctx = canvas.getContext('2d');
element.appendChild(canvas);
function fSin(freq, amplitude) {
return x => Math.sin(x / 100 * freq * Math.PI * 2) * amplitude;
}
let slider = document.createElement('input');
slider.type = 'range';
slider.min = 0;
slider.max = 100;
slider.addEventListener('change', () => {
const frequency = slider.value;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 320, 180);
ctx.strokeStyle = 'red';
ctx.fillStyle = 'red';
const ff = fSin(frequency, 10);
for (let i = 0; i < canvas.width; i++) {
ctx.fillRect(i, 90 - ff(i) * 2, 1, 1);
}
});
element.appendChild(slider);
#app {
width: 90vw;
height: 90vh;
margin: auto;
}
#app canvas {
margin: auto;
}
<div id="app"></div>

Related

Javascript pseudo-zoom around mouse position in canvas

I would like to "zoom" (mouse wheel) a grid I have, around the mouse position in canvas (like Desmos does).
By zooming, I mean redrawing the lines inside the canvas to look like a zoom effect, NOT performing an actual zoom.
And I would like to only use vanilla javascript and no libraries (so that I can learn).
At this point, I set up a very basic magnification effect that only multiplies the distance between the gridlines, based on the mouse wheel values.
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(-this.width/2, -this.height/2, this.width, this.height);
// Horizontal lines
this.xSubdivs = Math.floor(this.height / this.cellSize);
for (let i = 0; i <= this.xSubdivs; i++) {this.setHorizontalLines(i);}
// Vertical lines
this.ySubdivs = Math.floor(this.width / this.cellSize);
for (let i = 0; i <= this.ySubdivs; i++) {this.setVerticalLines(i) ;}
// Axis
this.setAxis();
}
setHorizontalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % this.cellBlock == 0) {
// light lines
context.strokeStyle = gridCellColor;
}
else{
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * i) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * i) - this.height/2);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % cellBlock == 0) {
// Light lines
context.strokeStyle = gridCellColor;
}
else {
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo((this.cellSize * i) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * i) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
// Axis are separated from the line loops so that they remain on
// top of them (cosmetic measure)
setAxis(){
// Style x Axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw x Axis
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.stroke();
context.closePath();
// Style y axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw y axis
context.beginPath();
context.moveTo((this.cellSize * xAxisOffset ) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * xAxisOffset ) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Initialize coordinates in the middle of the canvas
context.translate(canvasWidth/2,canvasHeight/2)
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom(){
grid.cellSize = grid.cellSize + zoom;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<span>Use mouse wheel to zoom</span>
<canvas id="canvas"></canvas>
This works fine but, as expected, it magnifies the grid from the top left corner not from the mouse position.
So, I thought about detecting the mouse position and then modifying all the "moveTo" and "lineTo" parts. The goal would be to offset the magnified grid so that everything is displaced except the 2 lines intersecting the current mouse coordinates.
For instance, it feels to me that instead of this:
context.moveTo(
(this.cellSize * i) - this.width/2,
-this.height/2
);
I should have something like this:
context.moveTo(
(this.cellSize * i) - this.width/2 + OFFSET BASED ON MOUSE COORDS,
-this.height/2
);
But after hours of trials and errors, I'm stuck. So, any help would be appreciated.
FYI: I already coded a functional panning system that took me days to achieve (that I stripped from the code here for clarity), so I would like to keep most of the logic I have so far, if possible.
Unless my code is total nonsense or has performance issues, of course.
Thank you.
You're already tracking the mouse position in pixels. If you transform the mouse position to the coordinate system of your grid, you can define which grid cell it's hovering.
After zooming in, you can again calculate the mouse's coordinate. When zooming in around another center point, you'll see the coordinate shift.
You can undo that shift by translating the grid's center in the opposite direction.
Here's the main part of it:
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
// Make the zoom happen: update the cellSize
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
To make this easier to work with, I introduced a grid.centerX and .centerY. I updated your draw method to take the center in to account (and kind of butchered it along the way, but I think you'll manage to improve that a bit).
Here's the complete example:
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs,
this.centerX = canvasWidth / 2,
this.centerY = canvasHeight / 2
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(0, 0, this.width, this.height);
// Horizontal lines
const minIY = -Math.ceil(this.centerY / this.cellSize);
const maxIY = Math.ceil((this.height - this.centerY) / this.cellSize);
for (let i = minIY; i <= maxIY; i++) {this.setHorizontalLines(i);}
// Vertical lines
const minIX = -Math.ceil(this.centerX / this.cellSize);
const maxIX = Math.ceil((this.width - this.centerX) / this.cellSize);
for (let i = minIX; i <= maxIX; i++) {this.setVerticalLines(i) ;}
this.setVerticalLines(0);
this.setHorizontalLines(0);
}
setLineStyle(i) {
if (i === 0) {
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
} else if (i % cellBlock == 0) {
// Light lines
context.lineWidth = 0.5;
context.strokeStyle = gridCellColor;
} else {
// Dark lines
context.lineWidth = 0.5;
context.strokeStyle = gridBlockColor;
}
}
setHorizontalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const y = this.centerY + this.cellSize * i;
context.beginPath();
context.moveTo(0, y);
context.lineTo(this.width, y);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const x = this.centerX + this.cellSize * i;
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, this.height);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<canvas id="canvas"></canvas>

How to Improve Html5 Canvas Performance

So I have this project I have been working on and the goal of it is to randomly generate terrain on a 2D plane, and put rain in the background, and I chose to use the html5 canvas element to accomplish this goal. After creating it I am happy with the result but I am having performance issues and could use some advice on how to fix it. So far I have tried to only clear the bit of the canvas that is needed, which is above the rectangles I drew under the terrain to fill it in, but because of this I have to redraw the circles. The rn(rain number) has already been lowered by about 2 times and it still lags, any suggestions?
Note - The code in the snippet does not lag due to it's small size, but if I was to run it in full screen with the actual rain number(800), it would lag. I have shrunk the values to fit the snippet.
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
for (i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
c.fillRect(x, y, 4, canvas.height - y);
}
}
function gameloop() {
//clearing canvas
for (i = 0; i < tn; i++) {
c.clearRect(tp[i].x - 2, 0, 2, tp[i].y);
}
for (i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain drawing
c.fillRect(rp[i].x, rp[i].y, rp[i].w, 6);
}
for (i = 0; i < tn; i++) {
//terrain drawing
c.beginPath();
c.arc(tp[i].x, tp[i].y, 6, 0, 7);
c.fill();
}
}
setup();
setInterval(gameloop, 1000 / 60);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<html>
<head>
<link rel="stylesheet" href="index.css">
<title>A Snowy Night</title>
</head>
<body id="body"> <canvas id="gamecanvas"></canvas>
<script src="index.js"></script>
</body>
</html>
Superimposing canvas
Like I suggested in my comment, the use of a second canvas point is to only have to draw the terrain once, and hence it could enhance the performance of your animation by saving a redraw on each new frame. This can be done with CSS by positioning one on the other (like layers).
#canvasBase {
position: relative;
}
#canvasLayer1 {
position: absolute;
top: 0;
left: 0;
}
#canvasLayer2 {
position: absolute;
top: 0;
left: 0;
}
// etc...
Also I advise you to use requestAnimationFrame over setinterval (see why).
requestAnimationFrame
However, by using requestAnimationFrame, we don't control the refresh rate, it's tied to the client hardware. So we need to handle it and for that, we will use the DOMHighResTimeStamp which is passed as an argument to our callback method.
The idea is to let it run at native speed and manage the fps by updating the logic (our calculs) only at desired time. For exemple, if we need a fps = 60; that means we need to update our logic every 1000 / 60 = ~16,67 ms. So we check if the deltaTime with the time of the last frame is equal or superior than ~16,67ms. If not enough time elapsed, we call a new frame & we return (important, otherwise the control we just did is useless as the code keeps going whatever the outcome of it).
let fps = 60;
/* Check if we need to update the logic */
/* if not request a new frame & return */
if(deltaLastUpdate <= 1000 / fps){ // 1000 / 60 = ~16,67ms
requestAnimationFrame(animate);
return;
}
Clearing canvas
As you need to erase all the past rain drops, the simplest & cheapest in ressources in to clear the whole context in one swoop.
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
Path2D
As your drawing use the same color for the rain drops, you can as well group all these in one path:
rainPath = new Path2D();
...
So you will need only one instruction to draw them (same ressources saving type as the clearRect):
ctxRain.fill(rainPath);
Result
/* CANVAS "Terrain" */
const terrainCanvas = document.getElementById('gameTerrain');
const ctxTerrain = terrainCanvas.getContext('2d');
terrainCanvas.height = window.innerHeight;
terrainCanvas.width = window.innerWidth;
/* CANVAS "Rain" */
const rainCanvas = document.getElementById('gameRain');
const ctxRain = rainCanvas.getContext('2d');
rainCanvas.height = window.innerHeight;
rainCanvas.width = window.innerWidth;
/* Game Constants */
const wind = 5;
const rainMaxParticules = 100;
const rain = [];
let rainPath;
const terrainMaxParticules = terrainCanvas.width + 20;
const terrain = [];
let terrainPath;
/* Maths help */
const ma = Math.random;
const mo = Math.round;
/* Clear */
function clearTerrain(){
ctxTerrain.clearRect(0, 0, terrainCanvas.width, terrainCanvas.height);
}
function clearRain(){
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
}
/* Logic */
function initTerrain(){
terrain[0] = { x: -2, y: terrainCanvas.height - 50 };
for (let i = 1; i <= terrainMaxParticules; i++) {
let x = terrain[i - 1].x + 2;
let y = terrain[i - 1].y + (ma() * 20) - 10;
if (y > terrainCanvas.height - 50) {
y = terrain[i - 1].y -= 1;
}
if (y < terrainCanvas.height - 100) {
y = terrain[i - 1].y += 1;
}
terrain[i] = { x, y };
}
}
function initRain(){
for (let i = 0; i < rainMaxParticules; i++) {
let x = mo(ma() * rainCanvas.width);
let y = mo(ma() * rainCanvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rain[i] = { x, y, w, s };
}
}
function init(){
initTerrain();
initRain();
}
function updateTerrain(){
terrainPath = new Path2D();
for(let i = 0; i < terrain.length; i++){
terrainPath.arc(terrain[i].x, terrain[i].y, 6, Math.PI/2, 5*Math.PI/2);
}
terrainPath.lineTo(terrainCanvas.width, terrainCanvas.height);
terrainPath.lineTo(0, terrainCanvas.height);
}
function updateRain(){
rainPath = new Path2D();
for (let i = 0; i < rain.length; i++) {
// Rain looping
if (rain[i].y > rainCanvas.height + 5) {
rain[i].y = -5;
}
if (rain[i].x > rainCanvas.width + 5) {
rain[i].x = -5;
}
// Rain movement
rain[i].y += rain[i].s;
rain[i].x += wind;
// Path containing all the drops
rainPath.rect(rain[i].x, rain[i].y, rain[i].w, 6);
}
}
/* Drawing */
function drawTerrain(){
ctxTerrain.fillStyle = 'black';
ctxTerrain.fill(terrainPath);
}
function drawRain(){
ctxRain.fillStyle = 'black';
ctxRain.fill(rainPath);
}
/* Animation Constant */
const fps = 60;
let lastTimestampUpdate;
let terrainDrawn = false;
/* Game loop */
function animate(timestamp){
/* Initialize rain & terrain particules */
if(rain.length === 0 || terrain.length === 0){
init();
}
/* Define "lastTimestampUpdate" from the first call */
if (lastTimestampUpdate === undefined){
lastTimestampUpdate = timestamp;
}
/* Check if we need to update the logic & the drawing, if not, request a new frame & return */
if(timestamp - lastTimestampUpdate <= 1000 / fps){
requestAnimationFrame(animate);
return;
}
if(!terrainDrawn){
/* Terrain --------------------- */
/* Clear */
clearTerrain();
/* Logic */
updateTerrain();
/* Draw */
drawTerrain();
/* ----------------------------- */
terrainDrawn = true;
}
/* --- Rain -------------------- */
/* Clear */
clearRain();
/* Logic */
updateRain();
/* Draw */
drawRain();
/* ----------------------------- */
/* Request another frame */
lastTimestampUpdate = timestamp;
requestAnimationFrame(animate);
}
/* Start the animation */
requestAnimationFrame(animate);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
#gameTerrain {
position: relative;
}
#gameRain {
position: absolute;
top: 0;
left: 0;
}
<body>
<canvas id="gameTerrain"></canvas>
<canvas id="gameRain"></canvas>
</body>
Aside
This won't affect performance, however I encourage you to use const & let over var (What's the difference between using “let” and “var”?).
Generally, having more paint instructions will be what costs the most, the complexity of these paint instructions only comes to play when it's really complex.
Here you are spamming the GPU with paint instructions:
(canvas.width) + 20 calls to clearRect(). clearRect() is a paint instruction, and not a cheap one. Use it sporadically, but actually, you should use it only to clear the whole context.
One fillRect() per rain drop.. They're all the same color, they can be merged in a single sub-path and drawn in a single draw call.
One fill per circle composing the terrain.
So instead of this huge number of draw calls, we could make it in only two draw calls:
One clearRect, one fill() of one big subpath containing both the drops and
the terrain.
However it's certainly more practical to keep the terrain and the rain separated, so let's make it three draw calls, by keeping the terrain in its own Path2D object, which is more friendly for the CPU:
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
// this will hold our Path2D object
// which will hold the full terrain drawing
// set a 'let' because we will set it again on resize
let terrain;
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (let i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
terrain = new Path2D();
for (let i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
terrain.rect(x, y, 4, canvas.height - y);
terrain.arc(x, y, 6, 0, Math.PI*2);
}
}
function gameloop() {
// clear the whole canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// start a new sub-path for the rain
c.beginPath();
for (let i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain tracing
c.rect(rp[i].x, rp[i].y, rp[i].w, 6);
}
// paint all the drops in a single op
c.fill();
// paint the whole terrain in a single op
c.fill(terrain);
// loop at screen refresh frequency
requestAnimationFrame(gameloop);
}
setup();
requestAnimationFrame(gameloop);
onresize = () => setup();
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<canvas id="gamecanvas"></canvas>
Further possible improvements:
Instead of making our terrain path a set of rectangles, using only lineTo to trace the actual outline would probably help a bit, some more calculations at init, but it's done only once in a while.
If the terrain becomes more complex, with more details, or with various colors and shadows etc. then consider painting it only once, and then produce an ImageBitmap from the canvas. Then in gameLoop you'll just have to drawImage that ImageBitmap (drawing bitmaps is super fast, but storing it consumes memory, so remember to .close() the ImageBitmap when you don't need it anymore).

Placing a letter randomly in a canvas offset by a % from border

I have this canvas where I place a letter randomly within the canvas.
var w = Math.random() * canvas.width;
var h = Math.random() * canvas.height;
drawRandomCircle(canvas,w,h);
function drawRandomCircle(canvas,w,h)
{
var fontSize = '35';
var ctx = canvas.getContext("2d");
var color = 'rgba(245, 66, 66,1.0)';
ctx.fillStyle = color;
ctx.font = fontSize + 'pt Arial';
ctx.fillText('O', w, h);
}
The results:
I would like to improve further on the function to include an offset in % from the canvas boundary to limit where the letter will appear.
The results would be similar to something similar to this.
Any ideas?
You need to take into account the 10% on the borders.
Try the following which uses this principle... but also remember that the co-ordinates for the canvas are top-left based... but when you do the font it will go UP (not down) so you have to take that into account as well.
var canvas = document.getElementsByTagName("canvas")[0];
var fontSizePx = 35;
// Get 10% of the width/height
var cw = (canvas.width / 10);
var ch = (canvas.height / 10);
// Get 80% of the width/height but minus the size of the font
var cw80 = (cw * 8) - fontSizePx;
var ch80 = (ch * 8) - fontSizePx;
for(var i = 0; i < 10; i++) {
// Get random value within center 80%
var w = (Math.random() * cw80) + cw;
// Add on the size of the font to move it down
var h = (Math.random() * ch80) + ch + fontSizePx;
drawRandomCircle(canvas,w,h);
}
function drawRandomCircle(canvas,w,h) {
var ctx = canvas.getContext("2d");
var color = 'rgba(245, 66, 66,1.0)';
ctx.fillStyle = color;
ctx.font = fontSizePx.toString() + 'px Arial';
ctx.fillText('O', w, h);
}
canvas {
border:1px solid black;
}
<canvas></canvas>

Canvas - Create random flakes on image

Can anyone give me a hint how I can create something like that with javascript:
The requirement is that I can set the density of the flakes. and add up to 5 different colors.
I do know how to create a canvas and put pixels in there, but I don't know how to create the "flakes".
Is there a way to create random shapes like this?
You can tessellate a simple shape and draw it at some random point.
The example below will create a 3 sided point, testate it randomly to a detail level of about 2 pixels and then add it to a path.
Then the path is filled with a color and another set of shapes are added.
function testate(amp, points) {
const p = [];
var i = points.length - 2, x1, y1, x2, y2;
p.push(x1 = points[i++]);
p.push(y1 = points[i]);
i = 0;
while (i < points.length) {
x2 = points[i++];
y2 = points[i++];
const dx = x2 - x1;
const dy = y2 - y1;
const r = (Math.random() - 0.5) * 2 * amp;
p.push(x1 + dx / 2 - dy * r);
p.push(y1 + dy / 2 + dx * r);
p.push(x1 = x2);
p.push(y1 = y2);
}
return p;
}
function drawFlake(ctx, size, x, y, noise) {
const a = Math.random() * Math.PI;
var points = [];
const step = Math.PI * (2/3);
var i = 0;
while (i < 3) {
const r = (Math.random() * size + size) / 2;
points.push(Math.cos(a + i * step) * r);
points.push(Math.sin(a + i * step) * r);
i++;
}
while (size > 2) {
points = testate(noise, points);
size >>= 1;
}
i = 0;
ctx.setTransform(1,0,0,1,x,y);
ctx.moveTo(points[i++], points[i++]);
while (i < points.length) {
ctx.lineTo(points[i++], points[i++]);
}
}
function drawRandomFlakes(ctx, count, col, min, max, noise) {
ctx.fillStyle = col;
ctx.beginPath();
while (count-- > 0) {
const x = Math.random() * ctx.canvas.width;
const y = Math.random() * ctx.canvas.height;
const size = min + Math.random() * (max- min);
drawFlake(ctx, size, x, y, noise);
}
ctx.fill();
}
const ctx = canvas.getContext("2d");
canvas.addEventListener("click",drawFlakes);
drawFlakes();
function drawFlakes(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#341";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height)
const noise = Math.random() * 0.3 + 0.3;
drawRandomFlakes(ctx, 500, "#572", 5, 10, noise)
drawRandomFlakes(ctx, 200, "#421", 10, 15, noise)
drawRandomFlakes(ctx, 25, "#257", 15, 30, noise)
}
body { background: #341 }
div {
position: absolute;
top: 20px;
left: 20px;
color: white;
}
<canvas id="canvas" width = "600" height = "512"></canvas>
<div>Click to redraw</div>
You'll need a certain noise algorithm.
In this example I used Perlin noise, but you can use any noise algorithm that fits your needs. By using Perlin noise we can define a blob as an area where the noise value is above a certain threshold.
I used a library that I found here and based my code on the sample code. The minified code is just a small portion of it (I cut out simplex and perlin 3D).
LICENSE
You can tweek it by changing the following parameters
Math.abs(noise.perlin2(x / 25, y / 25))
Changing the 25 to a higher value will zoom in, lower will zoom out
if (value > 0.4){
Changing the 0.4 to a lower value will increase blob size, higher will decrease blob size.
!function(n){var t=n.noise={};function e(n,t,e){this.x=n,this.y=t,this.z=e}e.prototype.dot2=function(n,t){return this.x*n+this.y*t},e.prototype.dot3=function(n,t,e){return this.x*n+this.y*t+this.z*e};var r=[new e(1,1,0),new e(-1,1,0),new e(1,-1,0),new e(-1,-1,0),new e(1,0,1),new e(-1,0,1),new e(1,0,-1),new e(-1,0,-1),new e(0,1,1),new e(0,-1,1),new e(0,1,-1),new e(0,-1,-1)],o=[151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180],i=new Array(512),w=new Array(512);function u(n){return n*n*n*(n*(6*n-15)+10)}function f(n,t,e){return(1-e)*n+e*t}t.seed=function(n){n>0&&n<1&&(n*=65536),(n=Math.floor(n))<256&&(n|=n<<8);for(var t=0;t<256;t++){var e;e=1&t?o[t]^255&n:o[t]^n>>8&255,i[t]=i[t+256]=e,w[t]=w[t+256]=r[e%12]}},t.seed(0),t.perlin2=function(n,t){var e=Math.floor(n),r=Math.floor(t);n-=e,t-=r;var o=w[(e&=255)+i[r&=255]].dot2(n,t),h=w[e+i[r+1]].dot2(n,t-1),s=w[e+1+i[r]].dot2(n-1,t),a=w[e+1+i[r+1]].dot2(n-1,t-1),c=u(n);return f(f(o,s,c),f(h,a,c),u(t))}}(this);
const c = document.getElementById("canvas");
const cc = c.getContext("2d");
noise.seed(Math.random());
let image = cc.createImageData(canvas.width, canvas.height);
let data = image.data;
for (let x = 0; x < c.width; x++){
for (let y = 0; y < c.height; y++){
const value = Math.abs(noise.perlin2(x / 25, y / 25));
const cell = (x + y * c.width) * 4;
if (value > 0.4){
data[cell] = 256;
data[cell + 1] = 0;
data[cell + 2] = 0;
data[cell + 3] = 256;
}
else {
data[cell] = 0;
data[cell + 1] = 0;
data[cell + 2] = 0;
data[cell + 3] = 0;
}
}
}
cc.putImageData(image, 0, 0);
<canvas id="canvas" width=500 height=500></canvas>

Canvas arc on Retina is too sharp at some points

I am trying to create animated arc that doesn't look blurry on HiDPI devices.
This is how my arc looks on iPhone 5s:
you can see that near 0, 90, 180 deg arc becomes too sharp. How can I prevent this?
This is my code:
// Canvas arc progress
const can = document.getElementById('canvas');
const ctx = can.getContext('2d');
const circ = Math.PI * 2;
const quart = Math.PI / 2;
const canvasSize = can.offsetWidth;
const halfCanvasSize = canvasSize / 2;
let start = 0,
finish = 70,
animRequestId = null;
// Get pixel ratio
const ratio = (function() {
const dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
})();
// Set canvas h & w
can.width = can.height = canvasSize * ratio;
can.style.width = can.style.height = canvasSize + 'px';
ctx.scale(ratio, ratio)
ctx.beginPath();
ctx.strokeStyle = 'rgb(120,159,194)';
ctx.lineCap = 'square';
ctx.lineWidth = 8.0;
ctx.arc(halfCanvasSize, halfCanvasSize, halfCanvasSize - 4, 0, circ, false);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.strokeStyle = 'rgb(244,247,255)';
ctx.lineCap = 'round';
ctx.lineWidth = 8.0;
ctx.closePath();
let imd = ctx.getImageData(0, 0, canvasSize, canvasSize);
const draw = (current) => {
ctx.putImageData(imd, 0, 0);
ctx.beginPath();
ctx.arc(halfCanvasSize, halfCanvasSize, halfCanvasSize - 4, -(quart), ((circ) * current) - quart, false);
ctx.stroke();
};
(function animateArcProgress() {
animRequestId = requestAnimationFrame(animateArcProgress);
if (start <= finish) {
draw(start / 100);
start += 2;
} else {
cancelAnimationFrame(animRequestId);
}
})();
body {
margin: 0;
}
div {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
widht: 300px;
background: #85b1d7;
}
canvas {
height: 250px;
width: 250px;
}
<div>
<canvas id='canvas'></canvas>
</div>
You can soften the edge by drawing 1/2 pixel inside as done in your code below.
I rendered the arc 3 times at 8, 7.5 and 7 pixel width width alpha colour values 0.25, 0.5 and 1 respectively.
You can make it as soft as you want.
BTW using putImageData is very slow, why not just render the background to another canvas and draw that via ctx.drawImage(offScreencanvas,0,0) that way you will use the GPU to render the background rather than the CPU via the graphics port bus.
I added a bit more code to show the different softening FX you can get and added a mouse zoom so you can see the pixels a little better.
const can = document.getElementById('canvas');
const can2 = document.createElement("canvas"); // off screen canvas
const can3 = document.createElement("canvas"); // off screen canvas
const ctx = can.getContext('2d');
const ctx2 = can2.getContext('2d');
const ctx3 = can3.getContext('2d');
const circ = Math.PI * 2;
const quart = Math.PI / 2;
const canvasSize = can.offsetWidth;
const halfCanvasSize = canvasSize / 2;
const mouse = {x : null, y : null};
can.addEventListener("mousemove",function(e){
var bounds = can.getBoundingClientRect();
mouse.x = e.clientX - bounds.left;
mouse.y = e.clientY - bounds.top;
});
let start = 0,
finish = 70,
animRequestId = null;
// Get pixel ratio
const ratio = (function() {
const dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
})();
// Set canvas h & w
can2.height = can3.height = can2.width = can3.width = can.width = can.height = canvasSize * ratio;
can.style.width = can.style.height = canvasSize + 'px';
ctx.scale(ratio, ratio)
ctx2.scale(ratio, ratio)
ctx3.scale(ratio, ratio)
ctx2.beginPath();
ctx2.strokeStyle = 'rgb(120,159,194)';
ctx2.lineCap = 'square';
ctx2.lineWidth = 8.0;
ctx2.arc(halfCanvasSize, halfCanvasSize, halfCanvasSize - 4, 0, circ, false);
ctx2.stroke();
ctx2.closePath();
ctx2.beginPath();
ctx2.strokeStyle = 'rgb(244,247,255)';
ctx2.lineCap = 'round';
ctx2.lineWidth = 8.0;
ctx2.closePath();
const draw = (current) => {
ctx3.clearRect(0,0,canvas.width,canvas.height);
ctx3.drawImage(can2,0,0);
var rad = halfCanvasSize - 4;
const drawArc = () => {
ctx3.beginPath();
ctx3.arc(halfCanvasSize, halfCanvasSize, rad, -(quart), ((circ) * current) - quart, false);
ctx3.stroke();
}
// draw soft
ctx3.strokeStyle = 'rgb(244,247,255)';
ctx3.lineWidth = 8.5;
ctx3.globalAlpha = 0.25;
drawArc();;
ctx3.lineWidth = 7.0;
ctx3.globalAlpha = 0.5;
drawArc();;
ctx3.lineWidth = 6.5;
ctx3.globalAlpha = 1;
drawArc();
// draw normal
rad -= 12;
ctx3.lineWidth = 8.0;
ctx3.globalAlpha = 1;
drawArc();;
// draw ultra soft
rad -= 12;
ctx3.strokeStyle = 'rgb(244,247,255)';
ctx3.lineWidth = 9.0;
ctx3.globalAlpha = 0.1;
drawArc();
ctx3.lineWidth = 8.0;
ctx3.globalAlpha = 0.2;
drawArc();;
ctx3.lineWidth = 7.5;
ctx3.globalAlpha = 0.5;
drawArc();
ctx3.lineWidth = 6;
ctx3.globalAlpha = 1;
drawArc();
};
const zoomW = 30;
const zoomAmount = 5;
const drawZoom = () => {
ctx.drawImage(can3,0,0);
var width = zoomW * zoomAmount;
var cx = mouse.x - width / 2;
var cy = mouse.y - width / 2;
var c1x = mouse.x - zoomW / 2;
var c1y = mouse.y - zoomW / 2;
ctx.strokeStyle = 'rgb(244,247,255)';
ctx.lineWidth = 4;
ctx.strokeRect(cx,cy,width,width);
ctx.clearRect(cx,cy,width,width);
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.drawImage(can3,c1x,c1y,zoomW,zoomW,cx,cy,width,width);
ctx.imageSmoothingEnabled = true;
ctx.mozImageSmoothingEnabled = true;
}
function keepUpdating(){
ctx.clearRect(0,0,can.width,can.height);
drawZoom();
requestAnimationFrame(keepUpdating);
}
(function animateArcProgress() {
ctx.clearRect(0,0,can.width,can.height);
draw(start / 100);
drawZoom();
if (start <= finish) {
start += 0.5;
requestAnimationFrame(animateArcProgress);
} else {
requestAnimationFrame(keepUpdating);
}
})();
body {
margin: 0;
}
div {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
widht: 300px;
background: #85b1d7;
}
canvas {
height: 250px;
width: 250px;
}
<div>
<canvas id='canvas'></canvas>
</div>

Categories

Resources