I want to simulate an human drawing effect starting from a photo, using javascript.
I've been searching trough several js libraries that do image manipulation (mostly on canvas). But seems that no one even attempted what I'm looking for.
I don't think it's impossible to achieve such effects with javascript. So I wonder why I can't find anything already done.
On the native side there are several alternatives to photoshop to achieve such effects, as seen in several apps in the App Store:
Here's other examples of possible result (from Artist's Touch App):
Alright so I found a great explanation of the algorithm used here and adapted it to JS and canvas.
Live Demo
CodePen Demo with controls to mess with the effect
How it works is you average all the pixels around your center pixel, then you multiply that average by the intensity level you want, you then divide it by 255. Then you increment the r/g/b's related to the intensity level. Then you check which intensity level was most common among the pixels neighbors, and assign the target pixel that intensity level.
edit worked on it a bit more and rewrote a lot of it, gained some really huge performance gains, works with decent sized images now pretty well.
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
img = new Image();
img.addEventListener('load', function () {
canvas.width = this.width;
canvas.height = this.height;
ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
oilPaintEffect(canvas, 4, 55);
});
img.crossOrigin = "Anonymous";
img.src = "https://fbcdn-sphotos-h-a.akamaihd.net/hphotos-ak-xpa1/v/t1.0-9/1379992_10202357787410559_1075078295_n.jpg?oh=5b001e9848796dd942f47a0b2f3df6af&oe=542F3FEF&__gda__=1412145968_4dbb7f75b385770ecc3f4b88105cb0f8";
function oilPaintEffect(canvas, radius, intensity) {
var width = canvas.width,
height = canvas.height,
imgData = ctx.getImageData(0, 0, width, height),
pixData = imgData.data,
destCanvas = document.createElement("canvas"),
dCtx = destCanvas.getContext("2d"),
pixelIntensityCount = [];
destCanvas.width = width;
destCanvas.height = height;
// for demo purposes, remove this to modify the original canvas
document.body.appendChild(destCanvas);
var destImageData = dCtx.createImageData(width, height),
destPixData = destImageData.data,
intensityLUT = [],
rgbLUT = [];
for (var y = 0; y < height; y++) {
intensityLUT[y] = [];
rgbLUT[y] = [];
for (var x = 0; x < width; x++) {
var idx = (y * width + x) * 4,
r = pixData[idx],
g = pixData[idx + 1],
b = pixData[idx + 2],
avg = (r + g + b) / 3;
intensityLUT[y][x] = Math.round((avg * intensity) / 255);
rgbLUT[y][x] = {
r: r,
g: g,
b: b
};
}
}
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
pixelIntensityCount = [];
// Find intensities of nearest pixels within radius.
for (var yy = -radius; yy <= radius; yy++) {
for (var xx = -radius; xx <= radius; xx++) {
if (y + yy > 0 && y + yy < height && x + xx > 0 && x + xx < width) {
var intensityVal = intensityLUT[y + yy][x + xx];
if (!pixelIntensityCount[intensityVal]) {
pixelIntensityCount[intensityVal] = {
val: 1,
r: rgbLUT[y + yy][x + xx].r,
g: rgbLUT[y + yy][x + xx].g,
b: rgbLUT[y + yy][x + xx].b
}
} else {
pixelIntensityCount[intensityVal].val++;
pixelIntensityCount[intensityVal].r += rgbLUT[y + yy][x + xx].r;
pixelIntensityCount[intensityVal].g += rgbLUT[y + yy][x + xx].g;
pixelIntensityCount[intensityVal].b += rgbLUT[y + yy][x + xx].b;
}
}
}
}
pixelIntensityCount.sort(function (a, b) {
return b.val - a.val;
});
var curMax = pixelIntensityCount[0].val,
dIdx = (y * width + x) * 4;
destPixData[dIdx] = ~~ (pixelIntensityCount[0].r / curMax);
destPixData[dIdx + 1] = ~~ (pixelIntensityCount[0].g / curMax);
destPixData[dIdx + 2] = ~~ (pixelIntensityCount[0].b / curMax);
destPixData[dIdx + 3] = 255;
}
}
// change this to ctx to instead put the data on the original canvas
dCtx.putImageData(destImageData, 0, 0);
}
Paste a demo of @Loktar's answer here for someone looking for this algorithm.
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
img = new Image(),
effectEl = document.getElementById("effect"),
settings = {
radius : 4,
intensity : 25,
ApplyFilter : function(){
doOilPaintEffect();
}
}
img.addEventListener('load', function () {
// reduced the size by half for pen and performance.
canvas.width = (this.width/2);
canvas.height = (this.height/2);
ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
doOilPaintEffect();
});
img.crossOrigin = "Anonymous";
img.src = "https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2013/02/ImageTechniques.jpg";
var gui = new dat.GUI();
gui.add(settings, 'intensity');
gui.add(settings, 'radius');
gui.add(settings, 'ApplyFilter');
function doOilPaintEffect(){
oilPaintEffect(canvas, settings.radius, settings.intensity);
}
function oilPaintEffect(canvas, radius, intensity) {
var width = canvas.width,
height = canvas.height,
imgData = ctx.getImageData(0, 0, width, height),
pixData = imgData.data,
// change to createElement getting added element just for the demo
destCanvas = document.getElementById("dest-canvas"),
dCtx = destCanvas.getContext("2d"),
pixelIntensityCount = [];
destCanvas.width = width;
destCanvas.height = height;
var destImageData = dCtx.createImageData(width, height),
destPixData = destImageData.data,
intensityLUT = [],
rgbLUT = [];
for (var y = 0; y < height; y++) {
intensityLUT[y] = [];
rgbLUT[y] = [];
for (var x = 0; x < width; x++) {
var idx = (y * width + x) * 4,
r = pixData[idx],
g = pixData[idx + 1],
b = pixData[idx + 2],
avg = (r + g + b) / 3;
intensityLUT[y][x] = Math.round((avg * intensity) / 255);
rgbLUT[y][x] = {
r: r,
g: g,
b: b
};
}
}
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
pixelIntensityCount = [];
// Find intensities of nearest pixels within radius.
for (var yy = -radius; yy <= radius; yy++) {
for (var xx = -radius; xx <= radius; xx++) {
if (y + yy > 0 && y + yy < height && x + xx > 0 && x + xx < width) {
var intensityVal = intensityLUT[y + yy][x + xx];
if (!pixelIntensityCount[intensityVal]) {
pixelIntensityCount[intensityVal] = {
val: 1,
r: rgbLUT[y + yy][x + xx].r,
g: rgbLUT[y + yy][x + xx].g,
b: rgbLUT[y + yy][x + xx].b
}
} else {
pixelIntensityCount[intensityVal].val++;
pixelIntensityCount[intensityVal].r += rgbLUT[y + yy][x + xx].r;
pixelIntensityCount[intensityVal].g += rgbLUT[y + yy][x + xx].g;
pixelIntensityCount[intensityVal].b += rgbLUT[y + yy][x + xx].b;
}
}
}
}
pixelIntensityCount.sort(function (a, b) {
return b.val - a.val;
});
var curMax = pixelIntensityCount[0].val,
dIdx = (y * width + x) * 4;
destPixData[dIdx] = ~~ (pixelIntensityCount[0].r / curMax);
destPixData[dIdx + 1] = ~~ (pixelIntensityCount[0].g / curMax);
destPixData[dIdx + 2] = ~~ (pixelIntensityCount[0].b / curMax);
destPixData[dIdx + 3] = 255;
}
}
// change this to ctx to instead put the data on the original canvas
dCtx.putImageData(destImageData, 0, 0);
}
body{text-align:center;background:#ececec;font-family:Tahoma, Geneva, sans-serif}
section{display:inline-block}
canvas{border:1px solid #000}
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script>
<section>
<h2>Original</h2>
<canvas id="canvas"></canvas>
</section>
<section>
<h2>Oil Painting Effect</h2>
<canvas id="dest-canvas"></canvas>
</section>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With