Weird pattern of Heat Equation Simulation on 2D Canvas - javascript

I'm playing with a Heat Equation on 2D Canvas with additional heat source in a form of a donut. The result I got some "acidic" pattern around this donut.
const width = 200; // width of the grid
const height = 200; // height of the grid
const dt = 0.25; // time step
const dx = 1; // space step in the x-direction
const dy = 1; // space step in the y-direction
const alpha = 0.25; // thermal diffusivity
const Q = [];
// Heat of the heat source
const Q0 = 80;
const r1 = 8;
const r2 = 12;
for (let i = 0; i < width - 1; i++) {
Q[i] = [];
for (let j = 0; j < height - 1; j++) {
// Calculate the distance from the center of the region
const r = Math.sqrt((i - width / 2) ** 2 + (j - height / 2) ** 2);
Q[i][j] = (r1 < r && r < r2) ? Q0 : 0;
}
}
let grid = []; // array to store the grid
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Initialize the grid with random temperatures
for (let i = 0; i < width; i++) {
grid[i] = [];
for (let j = 0; j < height; j++) {
grid[i][j] = 50
}
}
function updateGrid() {
// Update the temperature of each cell based on the heat equation
for (let i = 1; i < width - 1; i++) {
for (let j = 1; j < height - 1; j++) {
const d2Tdx2 = (grid[i + 1][j] - 2 * grid[i][j] + grid[i - 1][j]) / (dx ** 2);
const d2Tdy2 = (grid[i][j + 1] - 2 * grid[i][j] + grid[i][j - 1]) / (dy ** 2);
grid[i][j] = grid[i][j] + alpha * dt * (d2Tdx2 + d2Tdy2) + (Q[i][j] * dt);
}
}
}
// This function is called repeatedly to update the grid and render it
function main() {
updateGrid();
renderGrid();
requestAnimationFrame(main);
}
// This function render the grid
function renderGrid() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Iterate over the grid and draw each cell
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
let hue = ((100 - grid[i][j]) / 100) * 240;
//ctx.fillStyle = `rgb(${temp}, ${temp}, ${temp})`;
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
ctx.fillRect(i * dx, j * dy, dx, dy);
}
}
}
// Start the simulation
main();
Tried different approaches, like playing with hsl colors, initial parameters but something definitely missing there.
What I aslo noticed is that during debuging, some values seems to be blowing up and I think that's the root of the problem but can't find the source of it, I have tried to found more information about this behaviour of differential equiations and why this can happen but couldn't apply it to the source code.

during calculation values in grid can go over 100, thus making formula ((100 - grid[i][j]) / 100) * 240; produce negative values
the simples way to fix is to limit values:
grid[i][j] = Math.min(100, grid[i][j] + alpha * dt * (d2Tdx2 + d2Tdy2) + (Q[i][j] * dt));
const width = 200; // width of the grid
const height = 200; // height of the grid
const dt = 0.25; // time step
const dx = 1; // space step in the x-direction
const dy = 1; // space step in the y-direction
const alpha = 0.25; // thermal diffusivity
const Q = [];
// Heat of the heat source
const Q0 = 80;
const r1 = 8;
const r2 = 12;
for (let i = 0; i < width - 1; i++) {
Q[i] = [];
for (let j = 0; j < height - 1; j++) {
// Calculate the distance from the center of the region
const r = Math.sqrt((i - width / 2) ** 2 + (j - height / 2) ** 2);
Q[i][j] = (r1 < r && r < r2) ? Q0 : 0;
}
}
let grid = []; // array to store the grid
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Initialize the grid with random temperatures
for (let i = 0; i < width; i++) {
grid[i] = [];
for (let j = 0; j < height; j++) {
grid[i][j] = 50
}
}
function updateGrid() {
// Update the temperature of each cell based on the heat equation
for (let i = 1; i < width - 1; i++) {
for (let j = 1; j < height - 1; j++) {
const d2Tdx2 = (grid[i + 1][j] - 2 * grid[i][j] + grid[i - 1][j]) / (dx ** 2);
const d2Tdy2 = (grid[i][j + 1] - 2 * grid[i][j] + grid[i][j - 1]) / (dy ** 2);
grid[i][j] = Math.min(100, grid[i][j] + alpha * dt * (d2Tdx2 + d2Tdy2) + (Q[i][j] * dt));
}
}
}
// This function is called repeatedly to update the grid and render it
function main() {
updateGrid();
renderGrid();
requestAnimationFrame(main);
}
// This function render the grid
function renderGrid() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Iterate over the grid and draw each cell
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
let hue = (100 - grid[i][j]) / 100 * 240;
//ctx.fillStyle = `rgb(${temp}, ${temp}, ${temp})`;
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
ctx.fillRect(i * dx, j * dy, dx, dy);
}
}
}
// Start the simulation
main();
<canvas width="500" height="500" id="canvas"></canvas>

Related

How to change the rows or columns of a grid without messing up the content

I want to be able to implement more rows or columns to the grid depending on the format. (For example if i want to use the sketch on a horizontal 16x9 screen, there would have to be more rows than columns) Right now it only works if the format/canvas is square.
As soon as I change the number of tiles or the size of the canvas, the elements jump around.
Here is my sketch:
let colors = [
"#F7F7F7",
"#141414",
"#FB2576",
"#F48668",
"#67339E",
"#00A6A6",
"#78FFD6"
];
function drawSubdividedCircle(x, y, size, segments, layers) {
segments = random (1,13);
layers = random (1,13);
const r = 360 / segments;
for (let i = 0; i < segments; i++) {
for (let j = 0; j < layers; j++) {
fill(random(colors));
const s = map(j, 0, layers, size, 0);
arc(
x + size / 2,
y + size / 2,
s,
s,
radians(r * i),
radians(r * (i + 1)));
}
}
}
function setup() {
createCanvas(500, 500);
frameRate(2);
}
function draw() {
noStroke();
let tilesX = 5;
let tilesY = 5;
let tileW = width / tilesX;
let tileH = height / tilesY;
const tileSize = width / tilesX;
for (let x = 0; x < tilesX; x++) {
for (let y = 0; y < tilesY; y++) {
rect(x*tileW, y*tileH, tileW, tileH);
fill(random(colors));
push();
for (let x = 0; x < tilesX; x++) {
for (let y = 0; y < tilesY; y++) {
let r = random(1);
if (r < 0.5) {
ellipseMode(CORNER);
ellipse(x*tileW, y*tileH, tileW, tileH);
fill(random(colors));
} else {
ellipseMode(CENTER);
drawSubdividedCircle(x * tileSize, y * tileSize, tileSize);
fill(random(colors));
pop();
}
}
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
I changed your code a little.. Removed two redundant for-loops, it is much faster now.
When you want to make your program responsive, you have to make a choice. Do you want the circles to scale when you change the resolution or maybe do you want the amount of rows/columns to change when the resolution changes? There are several possibilities, I made it now to keep the amount of tiles, and use the smaller value of the 'tileW' and 'tileH' to remain the circle shape.
let colors = [
"#F7F7F7",
"#141414",
"#FB2576",
"#F48668",
"#67339E",
"#00A6A6",
"#78FFD6"
];
var tilesX = 5;
var tilesY = 5;
var tileW;
var tileH;
var tileSize;
function drawSubdividedCircle(x, y, size, segments, layers)
{
segments = random (1,13);
layers = random (1,13);
const r = 360 / segments;
for (let i = 0; i < segments; i++)
{
for (let j = 0; j < layers; j++)
{
fill(random(colors));
const s = map(j, 0, layers, size, 0);
arc(
x + size / 2,
y + size / 2,
s,
s,
radians(r * i),
radians(r * (i + 1)));
}
}
}
function setup()
{
createCanvas(550, 500);
ellipseMode(CENTER);
//frameRate(2);
tileW = width / tilesX;
tileH = height / tilesY;
tileSize = min(tileW, tileH);
}
function draw()
{
background(0);
noStroke();
for (let x = 0; x < tilesX; x++)
{
for (let y = 0; y < tilesY; y++)
{
rect(x * tileSize, y * tileSize, tileSize, tileSize);
fill(random(colors));
let r = random(1);
if (r < 0.5)
{
ellipse((x - 0.5) * tileSize, (y - 0.5) * tileSize, tileSize, tileSize);
fill(random(colors));
}
else
{
drawSubdividedCircle(x * tileSize, y * tileSize, tileSize);
fill(random(colors));
}
}
}
}

What's causing this tunnel effect to render incorrectly?

I'm currently attempting to create a sort of 3D tunnel effect in Javascript using the canvas tag.
It works, in the sense that it actually renders something, but it looks like this:
Not only is that not a tunnel, but it also takes up only a quarter of the canvas for some reason.
I wish I knew how to solve this, but I'm not getting any errors from the console, and the script itself seems to be running just fine, so I don't know where to start.
var texWidth = 256;
var texHeight = 256;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var texture = new Array(texHeight);
var distanceTable = new Array(h);
var angleTable = new Array(h);
var imageData = ctx.createImageData(canvas.width, canvas.height);
var w = imageData.width;
var h = imageData.height;
for (i = 0; i < texHeight; i++) {
texture[i] = new Array(texWidth);
}
for (j = 0; j < h; j++) {
distanceTable[j] = new Array(w);
angleTable[j] = new Array(w);
}
for (y = 0; y < texHeight; y++) {
for (x = 0; x < texHeight; x++) {
texture[y][x] = x ^ y;
}
}
for (y2 = 0; y2 < h; y2++) {
for (x2 = 0; x2 < w; x2++) {
let angle;
let distance;
let ratio = 32.0;
distance = ~~(ratio * texHeight / Math.sqrt((x2 - w / 2.0) * (x2 - w / 2.0) + (y2 - h / 2.0) * (y2 - h / 2.0))) % texHeight;
angle = Math.abs(~~(0.5 * texWidth * Math.atan2(y2 - h / 2.0, x2 - w / 2.0) / Math.PI));
distanceTable[y2][x2] = distance;
angleTable[y2][x2] = angle;
}
}
var animation = 0;
function draw() {
animation = animation + 0.01;
let shiftX = texWidth * 1.0 * animation;
let shiftY = texHeight * 0.25 * animation;
for (y3 = 0; y3 < h; y3++) {
for (x3 = 0; x3 < w; x3++) {
let id = y3 * w + x3 * 4;
let c = texture[~~(distanceTable[y3][x3] + shiftX) % texWidth][~~(angleTable[y3][x3] + shiftY) % texHeight];
imageData.data[id] = 0;
imageData.data[id+1] = 0;
imageData.data[id+2] = c;
imageData.data[id+3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
window.requestAnimationFrame(draw);
}
window.requestAnimationFrame(draw);
Since I have very little else to work with, I tried modifying some of the variables. That produces some interesting strobe-like effects, but not much else.

How to make a grid of ellipses with subdivisions

I have created a circle with subdivisions (function CircleSubDivs) in p5.js and now want to generate a grid filled with those. Ideally the ellipse would also be disproportionate if the width and height of a tile is not the same, or if the amount of tiles were to be controlled by the mouse position the ellipse would move flexibly.
This was my inspiration
This is my code so far:
// let colors = [
// "#F48668 ",
// "#5D2E8C",
// "#F7F7F7"
// ];
function CircleSubDivs(x, y, size) {
let amount = 13;
let r = 360 / amount;
for (let j = 0; j < 10; j++) {
for (let i = 0; i < amount; i++) {
fill(random(255));
let s = map(j, 0, 8, width, 100);
arc(width / 2, height / 2, s, s, radians(r * i), radians(r * (i + 1)));
}
}
}
function setup() {
createCanvas(500, 500);
frameRate(1);
}
function draw() {
background("#0F0F0F");
noStroke();
let tilesX = 3;
let tilesY = 2;
let tileW = width / tilesX;
let tileH = height / tilesY;
for (let x = 0; x < tilesX; x++) {
for (let y = 0; y < tilesY; y++) {
CircleSubDivs(x * tileW, y * tileH, tileW, tileH);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
As you can see I have created a grid and tried to store my function for the ellipse with divisions in there, but it just shows me one single ellipse when I run it. I would really appreciate some help :)
Your parameters to your function are unused, so the function does the same thing every time.
You might try something like:
function drawSubdividedCircle(x, y, size, segments=13, layers=10) {
const r = 360 / segments;
for (let i = 0; i < segments; i++) {
for (let j = 0; j < layers; j++) {
fill(random(255));
const s = map(j, 0, layers, size, 0);
arc(
x + size / 2,
y + size / 2,
s,
s,
radians(r * i),
radians(r * (i + 1))
);
}
}
}
function setup() {
createCanvas(500, 500);
frameRate(1);
}
function draw() {
background("#0F0F0F");
noStroke();
const tilesX = 3;
const tilesY = 2;
const tileSize = width / tilesX;
for (let x = 0; x < tilesX; x++) {
for (let y = 0; y < tilesY; y++) {
drawSubdividedCircle(x * tileSize, y * tileSize, tileSize);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>

draw a hexagon of hexagons in p5.js

I am trying to create a hexagon made out of hexagon cells in p5.js.
I would like the algorithm to receive the following as input:
number of hexagon cells in the first row
total hexagon cells to draw
This is the code I have so far but it is not drawing the shape the way I want to.
// declare fixed variables
let diam = 30;
let total_hex = 30;
let diagonal = 7;
let first_row = 4;
let side = first_row;
// declare dependent variables
let radius = diam / 2;
let upper_half = side - 1;
let rows = upper_half + side;
function setup() {
createCanvas(700, 700);
}
// draw
function draw() {
background("#ffae5");
noFill();
strokeWeight(2);
stroke("#ffb703");
// start counters
// row_counter = rows;
counter = 0;
row_cells = first_row;
push();
translate(200, 200);
for (let j = 0; j < rows; j++) {
for (let i = 0; i < row_cells; i++) {
push();
translate(-j * (radius + radius / 2 + radius / 4), 0);
if (diagonal > i) {
console.log(diagonal, i);
push();
translate(i * 2 * diam - (i * radius) / 2, 3 * j * (diam / 2));
polygon(0, 0, diam, 6);
pop();
}
pop();
}
if (rows < j) {
row_cells -= 1;
}
if (rows > j) {
row_cells += 1;
}
}
pop();
}
function polygon(x, y, radius, npoints) {
let angle = TWO_PI / npoints;
beginShape();
for (let a = 0; a < TWO_PI; a += angle) {
let sx = x + sin(a) * radius;
let sy = y + cos(a) * radius;
vertex(sx, sy);
}
endShape(CLOSE);
}
Specifically, the hexagons from the bottom half of the shape appear to be shifted by some parameter I still can't figure out. Here's the link to the p5.js online editor.
What am I missing here? Thanks!
I logged i, j at each cell to determine the position.
Now if you look at the cells you want to remove, you will see that these cells are: i < 3 && j > 3 && j - i > 3.
So if you simply want to remove these cells, you could change your if (diagonal > i) to if (diagonal > i && !(i < 3 && j > 3 && j - i > 3))) or parametrize that 3 to be a dependent variable to Math.floor(diagonal/2) to make something like:
//declare fixed variables
let diagonal = 7;
...
//declare dependent variables
const bottomLeftThreshold = Math.floor(diagonal/2);
...
const bottomLeft = i < bottomLeftThreshold && j > bottomLeftThreshold && j - i > bottomLeftThreshold
if (diagonal > i && !(bottomLeft))
...
Now the code is much more readable and modularized.
I did some tweaks to #cSharp's code snippet (see first answer) here and there, and ended up making the code flexible enough so it can adapt to any number of hexagons and sizes.
Below are the "fixed" and "dependent" variables I declared to draw the hexagon as well as the draw() function. I had to do some auxiliary calculations to get the diagonal and side sizes here.
// declare data variables
let year_1920 = 21;
let year_2019 = 73;
// declare fixed variables
let diam = 30;
let total_hex = year_2019;
let diagonal = 11;
let first_row = 6;
let side = first_row;
// declare dependent variables
let radius = diam / 2;
let upper_half = side - 1;
let rows = upper_half + side;
function draw() {
background("#ffae5");
noFill();
strokeWeight(1.2);
stroke("#ffb703");
// start counters
yellow = 0;
counter = 0;
row_cells = first_row;
push();
translate(200, 100);
for (let j = 0; j < rows; j++) {
for (let i = 0; i < row_cells; i++) {
if (yellow >= year_1920) {
stroke(255);
fill("#ffb703");
}
yellow++;
if (counter >= total_hex) {
noStroke();
noFill();
}
push();
translate(-j * (radius + radius / 2 + radius / 4), 0);
if (
diagonal > i &&
!(i < upper_half && j > upper_half && j - i > upper_half)
) {
push();
translate(i * 2 * diam - (i * radius) / 2, 3 * j * (diam / 2));
polygon(0, 0, diam, 6);
pop();
counter++;
}
pop();
}
if (rows > j) {
row_cells++;
}
}
pop();
}

p5.js - Low FPS for some basic animations

I'm having really bad performance on a project i wrote in Javascript (with the p5.js library)
Here is the code:
const fps = 60;
const _width = 400;
const _height = 300;
const firePixelChance = 1;
const coolingRate = 1;
const heatSourceSize = 10;
const noiseIncrement = 0.02;
const fireColor = [255, 100, 0, 255];
const bufferWidth = _width;
const bufferHeight = _height;
let buffer1;
let buffer2;
let coolingBuffer;
let ystart = 0.0;
function setup() {
createCanvas(_width, _height);
frameRate(fps);
buffer1 = createGraphics(bufferWidth, bufferHeight);
buffer2 = createGraphics(bufferWidth, bufferHeight);
coolingBuffer = createGraphics(bufferWidth, bufferHeight);
}
// Draw a line at the bottom
function heatSource(buffer, rows, _color) {
const start = bufferHeight - rows;
for (let x = 0; x < bufferWidth; x++) {
for (let y = start; y < bufferHeight; y++) {
if(Math.random() >= firePixelChance)
continue;
buffer.pixels[(x + (y * bufferWidth)) * 4] = _color[0]; // Red
buffer.pixels[(x + (y * bufferWidth)) * 4 +1] = _color[1]; // Green
buffer.pixels[(x + (y * bufferWidth)) * 4 +2] = _color[2]; // Blue
buffer.pixels[(x + (y * bufferWidth)) * 4 +3] = 255; // Alpha
}
}
}
// Produces the 'smoke'
function coolingMap(buffer){
let xoff = 0.0;
for(x = 0; x < bufferWidth; x++){
xoff += noiseIncrement;
yoff = ystart;
for(y = 0; y < bufferHeight; y++){
yoff += noiseIncrement;
n = noise(xoff, yoff);
bright = pow(n, 3) * 20;
buffer.pixels[(x + (y * bufferWidth)) * 4] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +1] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +2] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +3] = bright;
}
}
ystart += noiseIncrement;
}
// Change color of a pixel so it looks like its smooth
function smoothing(buffer, _buffer2, _coolingBuffer) {
for (let x = 0; x < bufferWidth; x++) {
for (let y = 0; y < bufferHeight; y++) {
// Get all 4 neighbouring pixels
const left = getColorFromPixelPosition(x+1,y,buffer.pixels);
const right = getColorFromPixelPosition(x-1,y,buffer.pixels);
const bottom = getColorFromPixelPosition(x,y+1,buffer.pixels);
const top = getColorFromPixelPosition(x,y-1,buffer.pixels);
// Set this pixel to the average of those neighbours
let sumRed = left[0] + right[0] + bottom[0] + top[0];
let sumGreen = left[1] + right[1] + bottom[1] + top[1];
let sumBlue = left[2] + right[2] + bottom[2] + top[2];
let sumAlpha = left[3] + right[3] + bottom[3] + top[3];
// "Cool down" color
const coolingMapColor = getColorFromPixelPosition(x,y,_coolingBuffer.pixels)
sumRed = (sumRed / 4) - (Math.random() * coolingRate) - coolingMapColor[0];
sumGreen = (sumGreen / 4) - (Math.random() * coolingRate) - coolingMapColor[1];
sumBlue = (sumBlue / 4) - (Math.random() * coolingRate) - coolingMapColor[2];
sumAlpha = (sumAlpha / 4) - (Math.random() * coolingRate) - coolingMapColor[3];
// Make sure we dont get negative numbers
sumRed = sumRed > 0 ? sumRed : 0;
sumGreen = sumGreen > 0 ? sumGreen : 0;
sumBlue = sumBlue > 0 ? sumBlue : 0;
sumAlpha = sumAlpha > 0 ? sumAlpha : 0;
// Update this pixel
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4] = sumRed; // Red
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +1] = sumGreen; // Green
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +2] = sumBlue; // Blue
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +3] = sumAlpha; // Alpha
}
}
}
function draw() {
background(0);
text("FPS: "+Math.floor(frameRate()), 10, 20);
fill(0,255,0,255);
buffer1.loadPixels();
buffer2.loadPixels();
coolingBuffer.loadPixels();
heatSource(buffer1, heatSourceSize, fireColor);
coolingMap(coolingBuffer);
smoothing(buffer1, buffer2, coolingBuffer);
buffer1.updatePixels();
buffer2.updatePixels();
coolingBuffer.updatePixels();
let temp = buffer1;
buffer1 = buffer2;
buffer2 = temp;
image(buffer2, 0, 0); // Draw buffer to screen
// image(coolingBuffer, 0, bufferHeight); // Draw buffer to screen
}
function mousePressed() {
buffer1.fill(fireColor);
buffer1.noStroke();
buffer1.ellipse(mouseX, mouseY, 100, 100);
}
function getColorFromPixelPosition(x, y, pixels) {
let _color = [];
for (let i = 0; i < 4; i++)
_color[i] = pixels[(x + (y * bufferWidth)) * 4 + i];
return _color;
}
function getRandomColorValue() {
return Math.floor(Math.random() * 255);
}
I'm getting ~12 FPS on chrome and ~1 FPS on any other browser and i cant figure out why..
Resizing my canvas to make it bigger also impacts the fps negatively...
In the devtools performance tab i noticed that both my smoothing and coolingMap functions are the things slowing it down, but i cant figure out what part of them are so heavy..
You've pretty much answered this for yourself already:
i'm starting to think this is normal and i should work on caching stuff and maybe use pixel groups instead of single pixels
Like you're discovering, doing some calculation for every single pixel is pretty slow. Computers only have finite resources, and there's going to be a limit to what you can throw at them.
In your case, you might consider drawing the whole thing to a canvas once at startup, and then moving the canvas up over the life of the program.

Categories

Resources