The Goal
An often seen effect in illustrations and other graphic works is a gradient between two colors, which is provided with a grain/noise and thus gets a very special effect. (especially example 3.)
My research has shown many solutions how to achieve this effect in software like Illustrator, but I would like to recreate it with p5js or the vanilla js canvas.
(source: /@stefanhrlemann/how-to-create-noisy-risograph-style-gradients-and-textures-in-photoshop-in-3-ways-394d6012a93a)
My attempt
I have already tried to create a gradient and set a random noise with a specific color using the pixel array on top of it. Which only partially leads to the desired effect:
function draw() {
setGradient(0, 0, width, height, color(0, 0, 0), color(255, 255 ,255));
setNoise();
}
function setNoise() {
loadPixels();
for (let x = 0; x < width; x++ ) {
for (let y = 0; y < height; y++ ) {
if (random(1) > 0.9) {
const index = (x + y * width) * 4;
pixels[index] = 255;
pixels[index + 1] = 255;
pixels[index + 2] = 255;
pixels[index + 3] = 255;
}
}
}
updatePixels();
}
function setGradient(x, y, w, h, c1, c2) {
noFill();
for (let i = y; i <= y + h; i++) {
let inter = map(i, y, y + h, 0, 1);
let c = lerpColor(c1, c2, inter);
stroke(c);
line(x, i, x + w, i);
}
}
what do you think? is this the right approach or is there a better / simpler solution?
The Goal
An often seen effect in illustrations and other graphic works is a gradient between two colors, which is provided with a grain/noise and thus gets a very special effect. (especially example 3.)
My research has shown many solutions how to achieve this effect in software like Illustrator, but I would like to recreate it with p5js or the vanilla js canvas.
(source: https://medium./@stefanhrlemann/how-to-create-noisy-risograph-style-gradients-and-textures-in-photoshop-in-3-ways-394d6012a93a)
My attempt
I have already tried to create a gradient and set a random noise with a specific color using the pixel array on top of it. Which only partially leads to the desired effect:
function draw() {
setGradient(0, 0, width, height, color(0, 0, 0), color(255, 255 ,255));
setNoise();
}
function setNoise() {
loadPixels();
for (let x = 0; x < width; x++ ) {
for (let y = 0; y < height; y++ ) {
if (random(1) > 0.9) {
const index = (x + y * width) * 4;
pixels[index] = 255;
pixels[index + 1] = 255;
pixels[index + 2] = 255;
pixels[index + 3] = 255;
}
}
}
updatePixels();
}
function setGradient(x, y, w, h, c1, c2) {
noFill();
for (let i = y; i <= y + h; i++) {
let inter = map(i, y, y + h, 0, 1);
let c = lerpColor(c1, c2, inter);
stroke(c);
line(x, i, x + w, i);
}
}
what do you think? is this the right approach or is there a better / simpler solution?
Share Improve this question edited May 2, 2020 at 19:40 iamrobin. asked May 2, 2020 at 19:33 iamrobin.iamrobin. 1,6343 gold badges20 silver badges31 bronze badges1 Answer
Reset to default 5You need to blend your noise with the gradient. You can't blend directly an ImageData, you need to first transform it to a bitmap (putting it on a canvas can do). So instead of the overlay blending your tutorial talked about, you may prefer the hard-light
one, which will invert the order of our layers.
const w = 300;
const h = 300;
const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
// some colored noise
const data = Uint32Array.from( {length: w*h }, () => Math.random() * 0xFFFFFFFF );
const img = new ImageData( new Uint8ClampedArray( data.buffer ), w, h );
ctx.putImageData( img, 0, 0 );
// first pass to convert our noise to black and transparent
ctx.globalCompositeOperation = "color";
ctx.fillRect( 0, 0, w, h );
ctx.globalCompositeOperation = "hard-light";
ctx.fillStyle = ctx.createLinearGradient( 0, 0, 0, h );
ctx.fillStyle.addColorStop( 0.1, 'white' );
ctx.fillStyle.addColorStop( 0.9, 'black' );
ctx.fillRect( 0, 0, w, h );
canvas { background: lime; }
<canvas id="canvas" height="300"></canvas>
But blending requires you have an opaque scene. If you want to have transparency, then you'd have to use positing too:
const w = 300;
const h = 300;
const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
// some black and transparent noise
const data = Uint32Array.from( {length: w*h }, () => Math.random() > 0.5 ? 0xFF000000 : 0 );
const img = new ImageData( new Uint8ClampedArray( data.buffer ), w, h );
ctx.putImageData( img, 0, 0 );
ctx.fillStyle = ctx.createLinearGradient( 0, 0, 0, h );
ctx.fillStyle.addColorStop( 0.1, 'transparent' );
ctx.fillStyle.addColorStop( 0.9, 'black' );
// apply transparency gradient on noise (dim top)
ctx.globalCompositeOperation = "destination-in";
ctx.fillRect( 0, 0, w, h );
// apply black of the gradient on noise (darken bottom)
ctx.globalCompositeOperation = "multiply";
ctx.fillRect( 0, 0, w, h );
// optionally change the color of the noise
ctx.globalCompositeOperation = "source-atop";
ctx.fillStyle = "red";
ctx.fillRect( 0, 0, w, h );
canvas { background: lime; }
<canvas id="canvas" height="300"></canvas>