I am making a sketch in P5.js that should have a pixel density of 1. The current state of this sketch it should be generating a tile that is 100x100 pixels of randomized brightness (like cable static) and displaying as a tile in the top left. Something seems off with the pixel density of the tile image I created though. there are large black lines appearing between each line of random pixels.
function writeColor(image, x, y, red, green, blue, alpha) {
let index = (x + y * width) * 4;
image.pixels[index] = red;
image.pixels[index + 1] = green;
image.pixels[index + 2] = blue;
image.pixels[index + 3] = alpha;
}
function randomTile(size_x, size_y){
let result = createImage(size_x, size_y);
result.loadPixels();
for (var y = 0; y < size_y; y++){
for (var x= 0; x < size_x; x++){
let brt = floor(random(256));
writeColor(result, x, y, brt, brt, brt, 255);
}
}
result.updatePixels();
return result;
}
function setup() {
createCanvas(600, 400);
pixelDensity(1);
background(0);
let tile = randomTile(100, 100);
image(tile, 0, 0);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
output: enter image description here
I have tried messing with the pixel density and looking up pixel density specific to images but I'm not really getting what is going on here.
As mentioned in a comment, your indexing for a tile's pixels should be relative to that tile's width (size_y) rather than the p5 canvas width.
function writeColor(image, x, y, w, red, green, blue, alpha) {
const index = (x + y * w) * 4;
image.pixels[index] = red;
image.pixels[index + 1] = green;
image.pixels[index + 2] = blue;
image.pixels[index + 3] = alpha;
}
function randomTile(sizeX, sizeY) {
const result = createImage(sizeX, sizeY);
result.loadPixels();
for (let y = 0; y < sizeY; y++) {
for (let x = 0; x < sizeX; x++) {
const brt = floor(random(256));
writeColor(result, x, y, sizeY, brt, brt, brt, 255);
}
}
result.updatePixels();
return result;
}
function setup() {
createCanvas(600, 400);
pixelDensity(1);
background(0);
const tile = randomTile(100, 100);
image(tile, 0, 0);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Unrelated suggestions:
Use camelCase rather than snake_case in JS.
Use const when you don't need to reassign a variable (which is most of the time) and let everywhere else. Avoid var.
When you have long argument lists, consider using an object to name the parameters (left as an exercise if performance allows it).
Related
I have been doing some work on P5.js using perlin noise. I have already done a cube with perlin noise texture all over it. But my question is: Can I separate each face's texture?
You may be wondering why would I need this. I need to make a cubemap with it, so I need to take each face's texture and build the image.
I think I will make this crystal clear with images.
This is my cube.
I need to make something like this
Now I will show you my code
function setup() {
createCanvas(500,500, WEBGL);
angleMode(DEGREES)
noiseDetail(1)
//noLoop();
}
function draw() {
background(30);
noStroke();
translate(0,0, -width)
rotateX(frameCount * 3)
rotateY(frameCount * 3)
translate(-width/2, -height/2, -width/2);
let space = width / 20;
let indexX = 0;
for (let x = 0; x < width; x += space) {
let indexY = 0;
for(let y = 0; y < height; y += space) {
let indexZ = 0
for (let z = 0; z < width; z += space) {
push();
let h = noise(indexX, indexY, indexZ) * 255;
fill(h);
translate(x,y,z)
box(space);
pop();
indexZ += 0.1;
}
indexY += 0.1;
}
indexX += 0.1;
}
}
Whenever you calculate h, you should save that along with its indexX, indexY, indexZ (or x,y,z? I'm not entirely sure which variable denotes the position of each pixel in your code) in an array. You could end up with something like:
const myArray =
[
{
h: 123123,
indexX: 234,
indexY: 345,
indexZ: 456,
},
...
]
Now you have information on what h you want to place on each location and you only need to find out how to map each point on the cube surface to a 2 dimensional map like the one you linked.
I have some code which generates collages from sets of photos, im doing this by averaging the pixels' colors of the images themselves and eventually, after a certain manipulation i just point(x,y) with the averaged color.
The only problem is that when I zoom on retina screens (above certain resolution) it is very visible that this is indeed a bunch of points on the screen and not 1 complete image.
I guess it has something to do with pixelDensity() but after a lot of experimentations with that, it didn't help as well.
attached here is an example of a zoomed crop -
The main loop which combines the pixels is very basic and looks like this -
for (let y = 0; y <= imgOne.height; y++) {
for (let x = 0; x <= imgOne.width; x++) {
// Get the colors.
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo) ) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo) ) / 2;
stroke(avgRed,avgGreen,avgBlue);
point(x, y);
}
}
The point function is specifically going to draw a tiny round shape with a diameter equal to the pixelDensity. When you scale up the canvas either by CSS transform or by using your browsers zoom function you are going to start to see the sub-pixel artifacts of this. There are two ways to make sure your "points" of color are square and completely fill the plane even when zoomed in: 1) use the set() function to explicitly draw pixels, 2) use the square() or rect() functions to deliberately draw a square. (Theoretically you could also directly manipulate the pixels array, but this would be significantly more complicated).
Here is an example that demonstrates the original issue, as well as the different solutions.
// only show a portion of the image.
const W = 120;
const H = 120;
let imgOne;
let imgTwo;
function preload() {
// "Recursive raytrace of a sphere" by Tim Babb is licensed under CC BY-SA 4.0
// https://creativecommons.org/licenses/by-sa/4.0/
imgOne = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/32/Recursive_raytrace_of_a_sphere.png/240px-Recursive_raytrace_of_a_sphere.png");
//
imgTwo = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Rainbow-gradient-fully-saturated.svg/240px-Rainbow-gradient-fully-saturated.svg.png");
}
function setup() {
createCanvas(W * 3, H);
noLoop();
}
function draw() {
background(220);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
// Get the colors.
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo)) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo)) / 2;
stroke(avgRed, avgGreen, avgBlue);
point(x, y);
}
}
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo)) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo)) / 2;
set(x + W, y, color(avgRed, avgGreen, avgBlue));
}
}
updatePixels();
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo)) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo)) / 2;
fill(avgRed, avgGreen, avgBlue);
noStroke();
square(x + W * 2, y, 1);
}
}
}
// This work is licensed under a CC BY-SA 4.0 License.
// https://creativecommons.org/licenses/by-sa/4.0/
// Author: Paul Wheeler
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
transform-origin: top left;
transform: scale(4);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
In order for the problem to be reproduced you need to use your browsers zoom capability first and then run the code. I'm not sure exactly why the behavior differs when you run the code and then zoom.
I am trying to assign a random color to each circle once, and then get that color to progress through the color spectrum. I am a beginner and cannot work out how to stop the random color in draw function from resetting every loop without pulling it into setup function which then makes all the circles start from the same random color. Code below of what I have so far. It also seems to be changing the colors in a slight wave at the moment, I assume this is to do with the 'for'. All i want is a grid of circles that are assigned a random color once and then go on to progress their color through the color wheel at the same speed. Thanks in advance!
let intervals = [10];
let count;
let frameWidth;
let distance;
let diam1;
let col1;
let sat1;
let bri1;
let speed;
function setup() {
createCanvas(800, 800);
colorMode(HSB, 360, 100, 100, 1);
//initiate draw variables
count = random(intervals);
frameWidth = (width*0.8);
distance = (frameWidth/count);
col1 = random(360);
diam1 = distance*0.5;
speed = 0.005;
}
function draw() {
background(0, 0, 0, 1);
// draw grid of circles
for (let x = width * 0.1; x <= width * 0.9; x += distance) {
for (let y = height * 0.1; y <= height * 0.9; y += distance) {
sat1 = 100;
bri1 = 100;
noStroke();
fill(col1, sat1, bri1, 1);
ellipse(x,y,diam1);
col1 = col1 + speed
if (col1 >= 360) {
col1 = 0
}
}
}
}
You have to create a grid of random colors in setup:
let speed, colors, rows, columns;
function setup() {
// [...]
columns = Math.round(frameWidth / distance) + 1;
rows = Math.round(frameWidth / distance) + 1;
colors = [];
for (let i = 0; i < columns; i ++) {
colors.push([]);
for (let j = 0; j < rows; j ++) {
colors[i].push(random(360));
}
}
speed = 1;
}
Use the the colors in the draw function. Complete example:
let intervals = [10];
let count, frameWidth, distance;
let sat1, bri1;
let speed, colors, rows, columns, diameters;
function setup() {
createCanvas(800, 800);
colorMode(HSB, 360, 100, 100, 1);
//initiate draw variables
count = random(intervals);
frameWidth = (width*0.8);
distance = (frameWidth/count);
columns = Math.round(frameWidth / distance) + 1;
rows = Math.round(frameWidth / distance) + 1;
colors = [];
diameters = []
for (let i = 0; i < columns; i ++) {
colors.push([]);
diameters.push([]);
for (let j = 0; j < rows; j ++) {
colors[i].push(random(360));
diameters[i].push(random(TWO_PI));
}
}
speed = 1;
}
function draw() {
background(0, 0, 0, 1);
// draw grid of circles
for (let i = 0; i < columns; i ++) {
for (let j = 0; j < rows; j ++) {
sat1 = 100;
bri1 = 100;
noStroke();
fill(colors[i][j], sat1, bri1, 1);
let d = distance * (0.5 + 0.4 * sin(diameters[i][j]));
ellipse(width * 0.1 + i * distance, height * 0.1 + j * distance, d);
colors[i][j] = (colors[i][j] + speed) % 360;
diameters[i][j] = diameters[i][j] + 0.05;
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
The ui will be a 2d grid of circles.
The data structure will be a 2d array of colour values. The data structure never needs to be updated; to get a circle's current colour we only need to know its initial colour, and the amount of time that has passed!
I'm going to use the hsla() css colour directive to define the colours, as it makes shifting the hue trivial.
The colourShiftingCircles function takes a canvas context and some display parameters, and will maintain an animating (colour-shifting) grid of circles on the provided canvas context.
The function signature is:
colourShiftingCircles({ ctx, rad, numX, numY, saturation=100, lightness=50, huePerSec=30 })
ctx: the canvas context
rad: circle radius in pixels
numX: number of circles across
numY: number of circles down (total of numX x numY circles)
saturation: control how "colourful" or "nongrey" the colours are
lightness: control how shifted towards black/white the colours are
huePerSec: how quickly to cycle hues
let canvas = document.querySelector('canvas');
// This just ensures the canvas is a reasonable size; it's optional
let resize = () => {
let { width, height } = canvas.parentNode.getBoundingClientRect();
if (width.toString() !== canvas.getAttribute('width')) canvas.setAttribute('width', width);
if (height.toString() !== canvas.getAttribute('height')) canvas.setAttribute('height', height);
};
resize();
window.addEventListener('resize', resize);
let colourShiftingCircles = ({ ctx, rad, numX, numY, saturation=100, lightness=50, huePerSec=30 }) => {
// This value allows the animation to be ended at some point in the future
let control = { run: true, end: () => control.run = false };
// Animation loop runs inside an `async` function:
(async () => {
// An array of size `numX` x `numY` items, defining initial hue values for circles
let data = [ ...new Array(numX) ].map(() => [ ...new Array(numY) ].map(() => {
return { hue: Math.random() * 360 };
}));
// Mark the start time; this will allow us to tell how much time has passed later
let startTime = performance.now();
while (control.run) {
// Wait for animation frame; this is a basic principle of canvas animations
await new Promise(r => requestAnimationFrame(r));
// Figure out how much the hue has shifted by at this point in time
let totalHueShift = ((performance.now() - startTime) / 1000) * huePerSec;
// Draw every circle:
for (let y = 0; y < numY; y++) { for (let x = 0; x < numX; x++) {
// Calculate the hue based on the circle's initial colour, and the amount
// of hue shifted due to passage of time
let hue = data[x][y].hue + totalHueShift;
// Simply draw a circle with an hsla fill colour
ctx.beginPath();
ctx.fillStyle = `hsla(${Math.floor(hue) % 360}deg ${saturation}% ${lightness}%)`;
ctx.arc((rad * 2 * x) + rad, (rad * 2 * y) + rad, rad, 0, Math.PI * 2);
ctx.fill();
}}
}
})();
return control;
};
let { end } = colourShiftingCircles({ ctx: canvas.getContext('2d'), rad: 20, numX: 5, numY: 5 });
// If you ever want to end the animation just call `end()`!
/* Just getting the canvas to display in a reasonable way; this is optional */
html, body, canvas { position: absolute; width: 100%; height: 100%; margin: 0; padding: 0; }
canvas { outline: 1px solid black; }
<canvas></canvas>
first of all, i am a beginner on js and p5.js. My aim on this program is a smoothly morphing random shape. I was satisfied with the calculateShape()-function and the drawShape()-function, but when it comes to morphing (updateShape()) it gets really ugly. I thought it might be a good idea to save my current array into a temporary array, then loop over the array and add a random number to each x and y of each index and then replace the old x and y at this index. The main problem is, that it is always adding new shapes on the screen instead of changing the values of the vertices of the existing shape. Can anybody of you please give me a hint or point out my mistake(s)? THANK YOU IN ADVANCE!
var c1;
var c2;
var c3;
var centerX;
var centerY;
var fb;
var radius;
var angle;
var shape = [];
var temp;
/*function to calculate the inital shape*/
function calculateShape() {
//calculate coordinates and save into array
for (var i = 0; i < fb; i++) {
var x = cos(angle * i) * radius + random(-77,77);
var y = sin(angle * i) * radius + random(-77,77);
var v = createVector(x, y);
shape.push(v);
}
}
/*function for morphing the shape*/
function updateShape() {
var temp = shape;
for (var i = 0; i < shape.length - 1; i++) {
var x = temp[i].x + random(-1, 1);
var y = temp[i].y + random(-1, 1);
var p = temp[i];
var v = createVector(x, y);
shape.splice(p,1);
shape.push(v);
}
}
/*function for drawing the shape on the screen*/
function createShape(){
beginShape();
curveVertex(shape[shape.length-1].x, shape[shape.length-1].y);
for (var i = 0; i < shape.length; i++){
curveVertex(shape[i].x, shape[i].y);
}
curveVertex(shape[0].x, shape[0].y);
endShape(CLOSE);
}
function setup() {
createCanvas(windowWidth, windowHeight);
smooth();
background(250);
//frameRate(2);
// defining possible colors
c1 = color(0, 196, 181, 235);
c2 = color(50, 227, 232, 235);
c3 = color(248, 49, 62, 255);
var colors = [c1, c2, c3];
//center of the window
centerX = windowWidth/2;
centerY = windowHeight/2;
//defining all variables
fb = 8;
angle = radians(360 / fb);
radius = random(120, 140);
//calling thefunction that initalises the shape
calculateShape();
}
function draw() {
translate(centerX, centerY);
blendMode(BLEND);
fill(c3);
noStroke();
createShape();
updateShape();
}
The main problem is, that it is always adding new shapes on the screen instead of changing the values of the vertices of the existing shape.
Sure, you just need to clear the screen before drawing again. So, reset the background with the background(250) from setup, in draw.
for this project http://biduleohm.free.fr/ledohm/ (sorry, the user interface is in french but the code is in english) I need an angular gradient but it doesn't exists in native so I've implemented it using a linear gradient on a line and I draw the lines more and more longer to form a triangle. The result is graphically OK but the speed isn't really good (1850 ms for 125 triangles). It's in the tab [RĂ©partition], it redraws if there is a keyup event on one of the inputs, don't be afraid of the apparent slowness, I've limited to maximum one redraw every 2000 ms.
Before I used a simple linear gradient on the whole triangle (but this doesn't match the reality) and the speed was OK, it draws thousands of triangles in less than a second. This function was used :
drawFrontLightForColor : function(x, y, w, h, color) {
var x2 = x - w;
var x3 = x + w;
var gradient = Distri.frontCanvas.createLinearGradient(x2, y, x3, y);
gradient.addColorStop(0, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
gradient.addColorStop(0.5, 'rgba(' + color + ', ' + (color == Distri.lightColors.cw ? Distri.lightCenterAlphaCw : Distri.lightCenterAlphaOther) + ')');
gradient.addColorStop(1, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
Distri.frontCanvas.fillStyle = gradient;
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x, y);
Distri.frontCanvas.lineTo(x2, (y + h));
Distri.frontCanvas.lineTo(x3, (y + h));
Distri.frontCanvas.lineTo(x, y);
Distri.frontCanvas.fill();
Distri.frontCanvas.closePath();
},
Then I switched to this function :
drawFrontLightForColor : function(x, y, w, h, centerColor, edgeColor) {
var ratio = w / h;
var tmpY;
var tmpW;
var x2;
var x3;
var gradient;
Distri.frontCanvas.lineWidth = 1;
for (var tmpH = 0; tmpH < h; tmpH++) {
tmpY = y + tmpH;
tmpW = Math.round(tmpH * ratio);
x2 = x - tmpW;
x3 = x + tmpW;
gradient = Distri.frontCanvas.createLinearGradient(x2, tmpY, x3, tmpY);
gradient.addColorStop(0, edgeColor);
gradient.addColorStop(0.5, centerColor);
gradient.addColorStop(1, edgeColor);
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x2, tmpY);
Distri.frontCanvas.lineTo(x, tmpY);
Distri.frontCanvas.lineTo(x3, tmpY);
Distri.frontCanvas.strokeStyle = gradient;
Distri.frontCanvas.stroke();
Distri.frontCanvas.closePath();
}
},
You can find the whole source here
I can't put the beginPath, stroke, closePath out of the loop because of the gradient which is changing every iteration (I've tried but it used the last gradient for every line (which, ironically, is identical to the first function...) which is understandable but not what I want).
I accept any advice (including redo the whole function and modify his caller to outsource some code) to improve the speed let's say 5x (ideally more).
I think you took the wrong way from the start : when doing so much changes of color, you have better operate at the pixel level.
So yes that could be with a webgl pixel shader, but you'll have to fight just to get the boilerplate running ok on all platform (or get a lib to do that for you).
And anyway there's a solution perfect for your need, and fast enough (a few ms) : use raw pixel data, update them one by one with the relevant function, then draw the result.
The steps to do that are :
- create a buffer same size as the canvas.
- iterate through it's pixel, keeping track of the x,y of the point.
- normalize the coordinates so they match your 'space'.
- compute the value for the normalized (x,y) out of all the data that you have.
- write a color (in my example i choose greyscale) out of that value.
- draw the whole buffer to canvas.
I did a jsfiddle, and here's the result with 4 data points :
fiddle is here :
http://jsfiddle.net/gamealchemist/KsM9c/3/
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
var width = canvas.width,
height = canvas.height;
// builds an image for the target canvas
function buildImage(targetCanvas, valueForXY, someData) {
var width = targetCanvas.width;
var height = targetCanvas.height;
var tempImg = ctx.createImageData(width, height);
var buffer = tempImg.data;
var offset = 0;
var xy = [0, 0];
function normalizeXY(xy) {
xy[0] = xy[0] / width ;
xy[1] = xy[1] / height;
}
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++, offset += 4) {
xy[0] = x; xy[1]=y;
normalizeXY(xy);
var val = Math.floor(valueForXY(xy, someData) * 255);
buffer[offset] = val;
buffer[offset + 1] = val;
buffer[offset + 2] = val;
buffer[offset + 3] = 255;
}
ctx.putImageData(tempImg, 0, 0);
}
// return normalized (0->1) value for x,y and
// provided data.
// xy is a 2 elements array
function someValueForXY(xy, someData) {
var res = 0;
for (var i = 0; i < someData.length; i++) {
var thisData = someData[i];
var dist = Math.pow(sq(thisData[0] - xy[0]) + sq(thisData[1] - xy[1]), -0.55);
localRes = 0.04 * dist;
res += localRes;
}
if (res > 1) res = 1;
return res;
}
var someData = [
[0.6, 0.2],
[0.35, 0.8],
[0.2, 0.5],
[0.6, 0.75]
];
buildImage(canvas, someValueForXY, someData);
// ------------------------
function sq(x) {
return x * x
}
In fact the GameAlchemist's solution isn't fast or I do something really wrong. I've implemented this algo only for the top view because the front view is much more complex.
For 120 lights the top view take 100-105 ms with the old code and it take 1650-1700 ms with this code (and moreover it still lacks a few things in the new code like the color for example):
drawTopLightForColor_ : function(canvasW, canvasD, rampX, rampY, rampZ, ledsArrays, color) {
function sq(x) {
return x * x;
}
var tmpImg = Distri.topCanvasCtx.createImageData(canvasW, canvasD);
var rawData = tmpImg.data;
var ledsArray = ledsArrays[color];
var len = ledsArray.length;
var i = 0;
for (var y = 0; y < canvasD; y++) {
for (var x = 0; x < canvasW; x++, i += 4) {
var intensity = 0;
for (var j = 0; j < len; j++) {
intensity += 2 * Math.pow(
sq((rampX + ledsArray[j].x) - x) +
sq((rampZ + ledsArray[j].y) - y),
-0.5
);
}
if (intensity > 1) {
intensity = 1;
}
intensity = Math.round(intensity * 255);
rawData[i] = intensity;
rawData[i + 1] = intensity;
rawData[i + 2] = intensity;
rawData[i + 3] = 255;
}
}
Distri.topCanvasCtx.putImageData(tmpImg, 0, 0);
},
Am I doing something wrong?