I'm playing around with the <canvas> element, drawing lines and such.
I've noticed that my diagonal lines are antialiased. I'd prefer the jaggy look for what I'm doing - is there any way of turning this feature off?
For images there's now context.imageSmoothingEnabled= false.
However, there's nothing that explicitly controls line drawing. You may need to draw your own lines (the hard way) using getImageData and putImageData.
Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which results in two lines that get drawn on the canvas.
A nice trick to not always need to add the 0.5 to the actual coordinate you want to draw over if you've got a lot of one-pixel lines, is to ctx.translate(0.5, 0.5) your whole canvas at the beginning.
It can be done in Mozilla Firefox. Add this to your code:
contextXYZ.mozImageSmoothingEnabled = false;
In Opera it's currently a feature request, but hopefully it will be added soon.
It must antialias vector graphics
Antialiasing is required for correct plotting of vector graphics that involves non-integer coordinates (0.4, 0.4), which all but very few clients do.
When given non-integer coordinates, the canvas has two options:
Antialias - paint the pixels around the coordinate based on how far the integer coordinate is from non-integer one (ie, the rounding error).
Round - apply some rounding function to the non-integer coordinate (so 1.4 will become 1, for example).
The later strategy will work for static graphics, although for small graphics (a circle with radius of 2) curves will show clear steps rather than a smooth curve.
The real problem is when the graphics is translated (moved) - the jumps between one pixel and another (1.6 => 2, 1.4 => 1), mean that the origin of the shape may jump with relation to the parent container (constantly shifting 1 pixel up/down and left/right).
Some tips
Tip #1: You can soften (or harden) antialiasing by scaling the canvas (say by x) then apply the reciprocal scale (1/x) to the geometries yourself (not using the canvas).
Compare (no scaling):
with (canvas scale: 0.75; manual scale: 1.33):
and (canvas scale: 1.33; manual scale: 0.75):
Tip #2: If a jaggy look is really what you're after, try to draw each shape a few times (without erasing). With each draw, the antialiasing pixels get darker.
Compare. After drawing once:
After drawing thrice:
Try something like canvas { image-rendering: pixelated; }.
This might not work if you're trying to only make one line not antialiased.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillRect(4, 4, 2, 2);
canvas {
image-rendering: pixelated;
width: 100px;
height: 100px; /* Scale 10x */
}
<html>
<head></head>
<body>
<canvas width="10" height="10">Canvas unsupported</canvas>
</body>
</html>
I haven't tested this on many browsers though.
I would draw everything using a custom line algorithm such as Bresenham's line algorithm. Check out this javascript implementation:
http://members.chello.at/easyfilter/canvas.html
I think this will definitely solve your problems.
Adding this:
image-rendering: pixelated; image-rendering: crisp-edges;
to the style attribute of the canvas element helped to draw crisp pixels on the canvas. Discovered via this great article:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Crisp_pixel_art_look
I discovered a better way to disable antialiasing on path / shape rendering using the context's filter property:
The magic / TL;DR:
ctx = canvas.getContext('2d');
// make canvas context render without antialiasing
ctx.filter = "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)";
Demystified:
The data url is a reference to an SVG containing a single filter:
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="filter" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="identity"/>
<feFuncG type="identity"/>
<feFuncB type="identity"/>
<feFuncA type="discrete" tableValues="0 1"/>
</feComponentTransfer>
</filter>
</svg>
Then at the very end of the url is an id reference to that #filter:
"url(data:image/svg+...Zz4=#filter)";
The SVG filter uses a discrete transform on the alpha channel, selecting only completely transparent or completely opaque on a 50% boundary when rendering. This can be tweaked to add some anti-aliasing back in if needed, e.g.:
...
<feFuncA type="discrete" tableValues="0 0 0.25 0.75 1"/>
...
Cons / Notes / Gotchas
Note, I didn't test this method with images, but I can presume it would affect semi-transparent parts of images. I can also guess that it probably would not prevent antialiasing on images at differing color boundaries. It isn't a 'nearest color' solution but rather a binary transparency solution. It seems to work best with path / shape rendering since alpha is the only channel antialiased with paths.
Also, using a minimum lineWidth of 1 is safe. Thinner lines become sparse or may often disappear completely.
Edit:
I've discovered that, in Firefox, setting filter to a dataurl does not work immediately / synchronously: the dataurl has to 'load' first.
e.g. The following will not work in Firefox:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
But waiting till the next JS frame works fine:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
setTimeout(() => {
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
}, 0);
I want to add that I had trouble when downsizing an image and drawing on canvas, it was still using smoothing, even though it wasn't using when upscaling.
I solved using this:
function setpixelated(context){
context['imageSmoothingEnabled'] = false; /* standard */
context['mozImageSmoothingEnabled'] = false; /* Firefox */
context['oImageSmoothingEnabled'] = false; /* Opera */
context['webkitImageSmoothingEnabled'] = false; /* Safari */
context['msImageSmoothingEnabled'] = false; /* IE */
}
You can use this function like this:
var canvas = document.getElementById('mycanvas')
setpixelated(canvas.getContext('2d'))
Maybe this is useful for someone.
ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;
With this combo I can draw nice 1px thin lines.
While we still don't have proper shapeSmoothingEnabled or shapeSmoothingQuality options on the 2D context (I'll advocate for this and hope it makes its way in the near future), we now have ways to approximate a "no-antialiasing" behavior, thanks to SVGFilters, which can be applied to the context through its .filter property.
So, to be clear, it won't deactivate antialiasing per se, but provides a cheap way both in term of implementation and of performances (?, it should be hardware accelerated, which should be better than a home-made Bresenham on the CPU) in order to remove all semi-transparent pixels while drawing, but it may also create some blobs of pixels, and may not preserve the original input color.
For this we can use a <feComponentTransfer> node to grab only fully opaque pixels.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = "url(#remove-alpha)";
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
}
<svg width="0" height="0" style="position:absolute;z-index:-1;">
<defs>
<filter id="remove-alpha" x="0" y="0" width="100%" height="100%">
<feComponentTransfer>
<feFuncA type="discrete" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</defs>
</svg>
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
For the ones that don't like to append an <svg> element in their DOM, and who live in the near future (or with experimental flags on), the CanvasFilter interface we're working on should allow to do this without a DOM (so from Worker too):
if (!("CanvasFilter" in globalThis)) {
throw new Error("Not Supported", "Please enable experimental web platform features, or wait a bit");
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = new CanvasFilter([
{
filter: "componentTransfer",
funcA: {
type: "discrete",
tableValues: [ 0, 1 ]
}
}
]);
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
};
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
Or you can also save the SVG as an external file and set the filter property to path/to/svg_file.svg#remove-alpha.
Notice a very limited trick. If you want to create a 2 colors image, you may draw any shape you want with color #010101 on a background with color #000000. Once this is done, you may test each pixel in the imageData.data[] and set to 0xFF whatever value is not 0x00 :
imageData = context2d.getImageData (0, 0, g.width, g.height);
for (i = 0; i != imageData.data.length; i ++) {
if (imageData.data[i] != 0x00)
imageData.data[i] = 0xFF;
}
context2d.putImageData (imageData, 0, 0);
The result will be a non-antialiased black & white picture. This will not be perfect, since some antialiasing will take place, but this antialiasing will be very limited, the color of the shape being very much like the color of the background.
Here is a basic implementation of Bresenham's algorithm in JavaScript. It's based on the integer-arithmetic version described in this wikipedia article: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function range(f=0, l) {
var list = [];
const lower = Math.min(f, l);
const higher = Math.max(f, l);
for (var i = lower; i <= higher; i++) {
list.push(i);
}
return list;
}
//Don't ask me.
//https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function bresenhamLinePoints(start, end) {
let points = [];
if(start.x === end.x) {
return range(f=start.y, l=end.y)
.map(yIdx => {
return {x: start.x, y: yIdx};
});
} else if (start.y === end.y) {
return range(f=start.x, l=end.x)
.map(xIdx => {
return {x: xIdx, y: start.y};
});
}
let dx = Math.abs(end.x - start.x);
let sx = start.x < end.x ? 1 : -1;
let dy = -1*Math.abs(end.y - start.y);
let sy = start.y < end.y ? 1 : - 1;
let err = dx + dy;
let currX = start.x;
let currY = start.y;
while(true) {
points.push({x: currX, y: currY});
if(currX === end.x && currY === end.y) break;
let e2 = 2*err;
if (e2 >= dy) {
err += dy;
currX += sx;
}
if(e2 <= dx) {
err += dx;
currY += sy;
}
}
return points;
}
For those who still looking for answers. here is my solution.
Assumming image is 1 channel gray. I just thresholded after ctx.stroke().
ctx.beginPath();
ctx.moveTo(some_x, some_y);
ctx.lineTo(some_x, some_y);
...
ctx.closePath();
ctx.fill();
ctx.stroke();
let image = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
for(let x=0; x < ctx.canvas.width; x++) {
for(let y=0; y < ctx.canvas.height; y++) {
if(image.data[x*image.height + y] < 128) {
image.data[x*image.height + y] = 0;
} else {
image.data[x*image.height + y] = 255;
}
}
}
if your image channel is 3 or 4. you need to modify the array index like
x*image.height*number_channel + y*number_channel + channel
Just two notes on StashOfCode's answer:
It only works for a grayscale, opaque canvas (fillRect with white then draw with black, or viceversa)
It may fail when lines are thin (~1px line width)
It's better to do this instead:
Stroke and fill with #FFFFFF, then do this:
imageData.data[i] = (imageData.data[i] >> 7) * 0xFF
That solves it for lines with 1px width.
Other than that, StashOfCode's solution is perfect because it doesn't require to write your own rasterization functions (think not only lines but beziers, circular arcs, filled polygons with holes, etc...)
According to MDN docs, Scaling for high resolution displays, "You may find that canvas items appear blurry on higher-resolution displays. While many solutions may exist, a simple first step is to scale the canvas size up and down simultaneously, using its attributes, styling, and its context's scale."
Ignoring the apparent paradox in their statement, this worked in my case, sharpening edges which had previously been unacceptably fuzzy.
// Get the DPR and size of the canvas
const dpr = window.devicePixelRatio;
const rect = canvas.getBoundingClientRect();
// Set the "actual" size of the canvas
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Scale the context to ensure correct drawing operations
ctx.scale(dpr, dpr);
// Set the "drawn" size of the canvas
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
I been stuck on getting the waves to look just like I want. I'm trying to figure out how to get the base of the wave to be the color I need it. I can do my desired color but it blocks the background. I can not see anything behind it because I was using like a reflection. Maybe someone can figure it out cause I'm having difficulties getting it to work... I plan on making the wave drop and rise. Here is a link to the code pen: HERE
Here is where I have the vertical reflection:
var x = $.cx - $.length / 2 + $.length / $.count * i,
y = height + $.simplex.noise2D($.xoff, $.yoff) * amp + sway;
$.ctx[i === 0 ? 'moveTo' : 'lineTo'](x, y);
}
$.ctx.lineTo($.w, $.h); // -$.h - Vertically reflection
$.ctx.lineTo(0, $.h); // -$.h - Vertically reflection
$.ctx.closePath();
$.ctx.fillStyle = color;
if (comp) {
$.ctx.globalCompositeOperation = comp;
}
$.ctx.fill();
My desired look for the waves is below:
Here is what I got with a successful transparent top, just not the right coloring:
Your problem is that the screen blending of the three colors generates a solid white color, so all the bottom of your canvas becomes white.
Here I simplified a lot the situation, with just 3 rectangles. Your bottom of canvas is my central white square:
const c2 = canvas.cloneNode();
const ctx = canvas.getContext("2d");
ctx.globalCompositeOperation = 'screen';
ctx.fillStyle = '#fb0000';
ctx.fillRect(0,0,50,50);
ctx.fillStyle = "#00ff8e";
ctx.fillRect(12,12,50,50);
ctx.fillStyle = "#6F33FF";
ctx.fillRect(25,25,50,50);
body {
background: #CCC;
}
<canvas id="canvas"></canvas>
So what we need, is a way to make this central square transparent so that we can draw our background behind.
To do this, we will need to draw our shapes at least two times:
once in normal compositing mode, so that we get the full overlap.
once again as source-in compositing mode, so that we get only where all our shapes do overlap.
const ctx = canvas.getContext("2d");
function drawShapes(mode) {
ctx.globalCompositeOperation = mode;
ctx.fillStyle = '#fb0000';
ctx.fillRect(0,0,50,50);
ctx.fillStyle = "#00ff8e";
ctx.fillRect(12,12,50,50);
ctx.fillStyle = "#6F33FF";
ctx.fillRect(25,25,50,50);
}
drawShapes('screen');
drawShapes('source-in');
body {
background: #CCC;
}
<canvas id="canvas"></canvas>
Now we have our overlapping area, we will be able to use it as a cutting shape in a third operation. But to do it, we will need a second, off-screen canvas to perform the compositing of the two states:
const c2 = canvas.cloneNode();
const ctx = canvas.getContext("2d");
const ctx2 = c2.getContext("2d");
function drawShapes(ctx, comp) {
ctx.globalCompositeOperation = comp;
ctx.fillStyle = '#fb0000';
ctx.fillRect(0, 0, 50, 50);
ctx.fillStyle = "#00ff8e";
ctx.fillRect(12, 12, 50, 50);
ctx.fillStyle = "#6F33FF";
ctx.fillRect(25, 25, 50, 50);
}
// first draw our screen, with unwanted white square
drawShapes(ctx, 'screen');
// draw it on the offscreen canvas
ctx2.drawImage(ctx.canvas, 0, 0)
// draw the shapes once again on the offscreen canvas to get the cutting shape
drawShapes(ctx2, 'source-in');
// cut the visible canvas
ctx.globalCompositeOperation = 'destination-out'
ctx.drawImage(ctx2.canvas, 0, 0);
body {
background: #CCC
}
<canvas id="canvas"></canvas>
And voilà, our white square is now transparent, we can draw whatever we want behind our scene using the destination-over composite operation.
I am new to canvas, I have an image myimg.jpg, I have converted this image into canvas and i am trying to apply some pattern image for heel.
I am not able to do it. Here is my screenshot:
How can I get it done.
<div id="myId">
<canvas id="canvaswrapper" width="660" height="540"></canvas>
</div>
function drawImage(){
var ctx = $("canvas")[0].getContext("2d"),
img = new Image();
img.onload = function(){
ctx.drawImage(img, 0, 0, 500, 500);
ctx.beginPath();
var img2= new Image();
var w;
var h;
img2.src = "http://www.gravatar.com/avatar/e555bd971bc2f4910893cd5b785c30ff?s=128&d=identicon&r=PG";
var pattern = ctx.createPattern(img2, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, w, h);
ctx.arc(300,305,50,0,2*Math.PI);
ctx.fill();
ctx.stroke();
};
img.src = "myimg.jpg";
}
drawImage();
You can define the area you want to fill using an image mask that fits on top of your image - this step is something for Photoshop/GIMP.
For example, having your shoe as-is:
Create a mask for it leaving the heal in the original position (it makes it easier to draw it back in - you can always crop it and draw it using an offset instead). Important: background must be transparent:
Then super-impose the pattern using these steps:
Load the pattern and define is as a fill-pattern
Draw the mask into the empty canvas
Optional step: Adjust transformations if needed (translate, scale)
Choose composite mode "source-atop"
Fill the canvas
Choose composite mode "destination-atop"
Draw the main image on top (which will show behind the mask/pattern)
Optional step: draw in original mask image using blending mode "multiply" to add shadow and highlights (does not work in IE). This will help creating an illusion of depth. For IE, drawing it on top using a reduced alpha or a separate image only containing shadows etc. can be an option
Result
Example
var iShoe = new Image, iMask = new Image, iPatt = new Image, count = 3;
iShoe.onload = iMask.onload = iPatt.onload = loader;
iShoe.src = "http://i.stack.imgur.com/hqL1C.png";
iMask.src = "http://i.stack.imgur.com/k5XWN.png";
iPatt.src = "http://i.stack.imgur.com/CEQ10.png";
function loader() {
if (--count) return; // wait until all images has loaded
var ctx = document.querySelector("canvas").getContext("2d"),
pattern = ctx.createPattern(iPatt, "repeat");
// draw in mask
ctx.drawImage(iMask, 0, 0);
// change comp mode
ctx.globalCompositeOperation = "source-atop";
// fill mask
ctx.scale(0.5, 0.5); // scale: 0.5
ctx.fillStyle = pattern; // remember to double the area to fill:
ctx.fillRect(0, 0, ctx.canvas.width*2, ctx.canvas.height*2);
ctx.setTransform(1,0,0,1,0,0); // reset transform
// draw shoe behind mask
ctx.globalCompositeOperation = "destination-atop";
ctx.drawImage(iShoe, 0, 0);
// to make it more realistic, add mask in blend mode (does not work in IE):
ctx.globalCompositeOperation = "multiply";
if (ctx.globalCompositeOperation === "multiply") {
ctx.drawImage(iMask, 0, 0);
}
}
<canvas width=281 height=340></canvas>
I'm looking for a solution to change the texture/pattern for a product.
At this moment i have:
A .png picture of a couch with a transparent background
A .png picture of a texture
With the following code:
<canvas id="a" width="800" height="500">Canvas not supported on your browser</canvas>
var width = $(window).width();
var height = $(window).height();
var c = document.getElementById("a");
var ctx = c.getContext("2d");
var can2 = document.createElement('canvas');
document.body.appendChild(can2)
can2.width = c.width;
can2.height = c.height;
var ctx2 = can2.getContext("2d");
var test = new Image();
test.src = "Images/newBank.png";
test.onload = function () {
ctx2.drawImage(test, 0, 0);
};
var img = new Image();
img.src = "Images/texturetrans.png";
img.onload = function () {
ctx2.globalCompositeOperation = 'source-in';
var ptrn = ctx2.createPattern(img, 'repeat');
ctx2.fillStyle = ptrn;
ctx2.fillRect(0, 0, can2.width, can2.height);
}
`
I get this result:
As you can see, the whole object is filled with my texture. No definitions of the pillows etc. are visible anymore. Is it possible to let my texture be a sort of transparent mask?
I'm already able to change the color of the couch:
But I'd like to be able to also add a pattern to my couch!
Any help will be appreciated and I'm already very sorry for my bad English.
If you're just after an illustrative approximation you can use a combination of blending and composition modes.
First thing is to make sure your main image has transparency - this is important for composition to work (I made a rough cut-off in the following demo).
Main steps:
Draw the pattern
Draw the main image on top with blending mode multiply
Draw the main image on top with compositing mode destination-in - this will make a cut-out
If you want to reduce the size of the pattern you can either do this by using a smaller version of the image, draw to a temporary canvas at a smaller size and use that as pattern, or use the new transform methods on the pattern itself.
Demo
var img1 = new Image, img2 = new Image, cnt = 2,
canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
// image loading for demo (ignore)
img1.onload = img2.onload = function() {if (!--cnt) go()};
img1.src = "//i.imgur.com/8WqH9v4.png"; // sofa
img2.src = "//i.stack.imgur.com/sQlu8.png"; // pattern
// MAIN CODE ---
function go() {
// create a pattern
ctx.fillStyle = ctx.createPattern(img2, "repeat");
// fill canvas with pattern
ctx.fillRect(0, 0, canvas.width, canvas.height);
// use blending mode multiply
ctx.globalCompositeOperation = "multiply";
// draw sofa on top
ctx.drawImage(img1, 0, 0, img1.width*.5, img1.height*.5);
// change composition mode
ctx.globalCompositeOperation = "destination-in";
// draw to cut-out sofa
ctx.drawImage(img1, 0, 0, img1.width*.5, img1.height*.5);
}
<canvas id="canvas" width=600 height=400></canvas>
You can also reverse the order of which image is drawn etc., if you prefer. This is just an example of one way.
If you need accurate texture then there is no way around to either take photos or use a 3D software, or hand-drawn the textures.
NOTE: IE does not support multiply - For this you need to manually iterate through the pixels and multiply each component with each other.
You can test for support this way:
ctx.globalCompositeOperation = "multiply";
if (ctx.globalCompositeOperation === "multiply") {
// blend as above
}
else {
// iterate and blend manually
}
Blending mode luminosity is mentioned in comments and this can be used too of course. I just want to point a couple of things to consider. The first is that this is a non-separable blending mode meaning it depends on all components as it goes through the HSL color model. This makes it a bit more compute intensive.
The second is that if you end up having to do this manually (in for example IE) the code is a bit more complex to emulate, and will be noticeably slower.
I'd like to give a sprite an outline when the character gets healed/damaged/whatever but I can't think of a way to code this using the 2d canvas. If it were possible, I'd think it would be a global composite operation, but I can't think of a way to achieve it with one of them.
I did find this stackoverflow answer that recommends creating a fatter, solid color version of the original and put the original on top of it. That would give it an outline, but it seems like a lot of extra work especially considering I'm using placeholder art. Is there an easier way?
This question is different from the one linked because this is specifically about the HTML5 2D canvas. It may have a solution not available to the other question.
For what it's worth, I don't mind if the outline creates a wider border or keeps the sprite the same size, I just want the outline look.
Just draw your original image in 8 position around the original image
Change composite mode to source-in and fill with the outline color
Change composite mode back to source-over and draw in the original image at correct location
This will create a clean sharp outline with equal border thickness on every side. It is not so suited for thick outlines however. Image drawing is fast, especially when image is not scaled so performance is not an issues unless you need to draw a bunch (which in that case you would cache the drawings or use a sprite-sheet anyways).
Example:
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = "http://i.stack.imgur.com/UFBxY.png";
function draw() {
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 2, // scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width, canvas.height);
// draw original image in normal mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
<canvas id=canvas width=500 height=500></canvas>
Maybe it would be worth trying this :
• build a canvas 1.1 time bigger than the original sprite
• fill it with the outline color
• draw the sprite scaled by 1.1 on the canvas using destination-in globalCompositeOperation.
Then you have a bigger 'shadow' of your sprite in the outline color.
When you want to draw the outline :
• draw the 'shadow' (centered)
• draw your sprite within the shadow.
Depending on the convexity of your sprite, this will work more or less nicely, but i think it's worth trying since it avoids you doubling the number of input graphic files.
I just did a short try as proof-of-concept and it quite works :
http://jsbin.com/dogoroxelupo/1/edit?js,output
Before :
After :
html
<html>
<body>
<image src='http://www.gifwave.com/media/463554/cartoons-comics-video-games-sprites-scott-pilgrim-paul-robertson_200s.gif' id='spr'></image>
<canvas id='cv' width = 500 height= 500 ></canvas>
</body>
</html>
code
window.onload=function() {
var spr = document.getElementById('spr');
var margin = 4;
var gh = createGhost(spr, '#F80', margin);
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
var outlined = true;
setInterval(function() {
ctx.clearRect(0,0,cv.width, cv.height);
if (outlined)
ctx.drawImage(gh, 0, 0)
ctx.drawImage(spr, 0, 0)
outlined = !outlined;
}, 400);
}
function createGhost (img, color, margin) {
var cv= document.createElement('canvas');
cv.width = img.width+2*margin;
cv.height = img.height + 2*margin;
var ctx = cv.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0,0, cv.width, cv.height);
ctx.save();
ctx.globalCompositeOperation = 'destination-in';
var scale = cv.width/spr.width;
ctx.scale(cv.width/spr.width, cv.height/spr.height);
ctx.drawImage(img, -margin, -margin);
ctx.restore();
return cv;
}
You could use strokeRect method to outline the sprite after drawing it. It should be asy if you know your sprite's dimensions...