Complex shape with rainbow gradient - javascript

I'm trying to draw a figure on a canvas, to be filled with a rainbow-colored gradient. The wanted result is something like this:
Creating the shape itself is pretty easy, just creating a path and drawing the lines. However, actually filling it with a gradient appears to be somewhat more difficult, as it seems only radial and linear gradients are supported.
The closest I have gotten is this:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var gradient=ctx.createLinearGradient(0,0,0,100);
gradient.addColorStop (0, 'red');
gradient.addColorStop (0.25, 'yellow');
gradient.addColorStop (0.5, 'green');
gradient.addColorStop (0.75, 'blue');
gradient.addColorStop (1, 'violet');
ctx.moveTo(0,40);
ctx.lineTo(200,0);
ctx.lineTo(200,100);
ctx.lineTo(0, 50);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
<body onload="draw();">
<canvas id="canvas" width="400" height="300"></canvas>
</body>
The gradient colors and such are correct, but the gradient should of course be more triangular-like, rather than being rectangular and cropped.

Native html5 canvas doesn't have a way to stretch one side of a gradient fill.
But there is a workaround:
Create your stretch gradient by drawing a series of vertical gradient lines with an increasing length.
Then you can use transformations to draw your stretched gradient at your desired angle
Example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var length=200;
var y0=40;
var y1=65
var stops=[
{stop:0.00,color:'red'},
{stop:0.25,color:'yellow'},
{stop:0.50,color:'green'},
{stop:0.75,color:'blue'},
{stop:1.00,color:'violet'},
];
var g=stretchedGradientRect(length,y0,y1,stops);
ctx.translate(50,100);
ctx.rotate(-Math.PI/10);
ctx.drawImage(g,0,0);
function stretchedGradientRect(length,startingHeight,endingHeight,stops){
var y=startingHeight;
var yInc=(endingHeight-startingHeight)/length;
// create a temp canvas to hold the stretched gradient
var c=document.createElement("canvas");
var cctx=c.getContext("2d");
c.width=length;
c.height=endingHeight;
// clip the path to eliminate "jaggies" on the bottom
cctx.beginPath();
cctx.moveTo(0,0);
cctx.lineTo(length,0);
cctx.lineTo(length,endingHeight);
cctx.lineTo(0,startingHeight);
cctx.closePath();
cctx.clip();
// draw a series of vertical gradient lines with increasing height
for(var x=0;x<length;x+=1){
var gradient=cctx.createLinearGradient(0,0,0,y);
for(var i=0;i<stops.length;i++){
gradient.addColorStop(stops[i].stop,stops[i].color);
}
cctx.beginPath();
cctx.moveTo(x,0);
cctx.lineTo(x,y+2);
cctx.strokeStyle=gradient;
cctx.stroke();
y+=yInc;
}
return(c);
}
#canvas{border:1px solid red; margin:0 auto; }
<h4>Stretched gradient made from vertical strokes</h4>
<canvas id="canvas" width=300 height=200></canvas>

Related

How to get true 1-pixel width black color line in p5.js [duplicate]

When i try to draw single pixel black line with the following code:
context.strokeStyle = '#000';
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.lineWidth = 1;
context.stroke();
context.closePath();
I have more then one pixel line with gray border. How to fix it?
Here is an example http://jsfiddle.net/z4VJq/
Call your function with these coordinates instead: drawLine(30,30.5,300,30.5);. Try it in jsFiddle.
The problem is that your color will be at an edge, so the color will be halfway in the pixel above the edge and halfway below the edge. If you set the position of the line in the middle of an integer, it will be drawn within a pixel line.
This picture (from the linked article below) illustrates it:
You can read more about this on Canvas tutorial: A lineWidth example.
You have to use context.translate(.5,.5); to offset everything by half a pixel. Its easy way for fix your problem
var canvas = document.getElementById("canvas1");
var context1 = canvas.getContext('2d');
context1.strokeStyle = '#000';
context1.beginPath();
context1.moveTo(10, 5);
context1.lineTo(300, 5);
context1.stroke();
var canvas2 = document.getElementById("canvas2");
var context2 = canvas2.getContext('2d');
context2.translate(.5,.5);
context2.strokeStyle = '#000';
context2.beginPath();
context2.moveTo(10, 5);
context2.lineTo(300, 5);
context2.stroke();
<div><canvas height='10' width='300' id='canvas1'>Обновите браузер</canvas></div>
<div><canvas height='10' width='300' id='canvas2'>Обновите браузер</canvas></div>

Draw fixed boxes on HTML canvas image [duplicate]

I'm tyring to rotate a diagram in canvas around its center while keeping the letters upright. I'm trying to use ctx.rotate(#) but it's rotating the entire diagram using what appears to be the left side of the canvas as the center.
The following link offers a visual: I want it to look like the green, not the red, as it currently does with my code. Visual Explanation
The following is the JSFiddle: http://jsfiddle.net/ddxarcag/143/
And my code is below:
<script>
$(document).ready(function () {
init();
function init() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
draw(ctx);
}
function draw(ctx) {
// layer1/Line
ctx.rotate(00);
ctx.beginPath();
ctx.moveTo(75.1, 7.7);
ctx.lineTo(162.9, 7.7);
ctx.stroke();
function WordSelector1() {
var word = ['A', 'B', 'C'];
var random = word[Math.floor(Math.random() * word.length)];
return random;
}
var x = WordSelector1();
// layer1/P
ctx.font = "12.0px 'Myriad Pro'";
ctx.rotate(0);
ctx.fillText(x, 60.0, 10.0);
}
});
</script>
Any help would be much appreciated. Thanks!
Drawing rotated graphs in canvas is somewhat easy once you know how to move the origin (0,0) to another point and then rotating the axes around it.
I added some comments in the code as not to repeat code and explanations.
I also moved the functions out of the $(document).ready and changed some numbers for more rounded values.
$(document).ready(function () {
init();
});
function init() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
draw(ctx);
}
function draw(ctx) {
ctx.font = "12.0px 'Myriad Pro'";
var angle = Math.random()*150; //Run more times to see other angles
//This translates the 0,0 to the center of the horizontal line
ctx.translate(100, 100);
//This draws the original straight line
ctx.beginPath();
ctx.moveTo(-50, 0); //The coordinates are relative to the new origin
ctx.lineTo(50, 0);
ctx.stroke();
//Draw the first letter
var x = WordSelector1();
ctx.fillText(x, -60, 0);
//This section draws the rotated line with the text straight
//Rotate the canvas axes by "angle"
ctx.rotate(angle * Math.PI/180);
ctx.beginPath();
ctx.moveTo(-50, 0); //The relative coordinates DO NOT change
ctx.lineTo(50, 0); //This shows that the axes rotate, not the drawing
ctx.stroke();
var x = WordSelector1();
ctx.translate(-60,0); //The origin must now move where the letter is to be placed
ctx.rotate(-angle * Math.PI/180); //Counter-rotate by "-angle"
ctx.fillText(x, 0, 0); //Draw the letter
}
function WordSelector1() {
var word = ['A', 'B', 'C'];
var random = word[Math.floor(Math.random() * word.length)];
return random;
}
canvas{
border: 1px solid;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="200" height="200"></canvas>
One warning: after everything is drawn the axes end parallel to the canvas borders because it was rotated by -angle, but the origin is where the last letter was placed. You may want to use ctx.save() and ctx.restore() to avoid having to revert translations and rotations.

Moving a clipped canvas

How would I go by if I wanted to move my clipped canvas? I need to move them to a specific area, but I'm not sure where to put the coordinates. I'm still very new to this, and would appreciate any help I can get.
I know I can make a "var" for the position, but since I have 6 pieces to move that would mean an awful lot of var's. Is there a simpler way?
Code:
<script>
var can=document.getElementById("NewCanvas");
var Jctx=can.getContext("2d");
var ctx=can.getContext("2d");
var img = new Image();
img.onload = function() {
ctx.moveTo(305,307);
ctx.lineTo(560,152);
ctx.lineTo(450,10);
ctx.lineTo(305,10);
ctx.closePath();
ctx.clip();
ctx.drawImage(img,0,0); // new image is clipped;
}
img.src='Prototype22.png';
</script>
Here's how to move a clipped area to a desired offset.
Put your clipping coordinates in an array:
var clips=[{x:305,y:307},{x:560,y:152},{x:450,y:10},{x:305,y:10}];
Then pass the image, clipping-coordinates & desired offset [x,y] to a function that does this:
Calculates the minimum x & y in the clipping coordinates array.
Translates (moves the whole canvas) to -minX+offsetX & -minY+offsetY. This will both normalize the clipped area to [0,0] and also then move that normalized area to the desired offset coordinate.
Defines the clipping path using the unaltered clipping coordinates.
Creates the clipping area with context.clip
Draws the image at [0,0]
The result will be a clipped area of the image that has been moved to the desired offset.
If you have multiple clips then you can create multiple arrays of clipping coordinates and pass them each into drawClippedImgAtXY.
Here is example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var clips=[{x:305,y:307},{x:560,y:152},{x:450,y:10},{x:305,y:10}];
var img = new Image();
img.onload = function() {
drawClippedImgAtXY(img,clips,100,20)
}
img.src='https://dl.dropboxusercontent.com/u/139992952/stackoverflow/city-q-c-640-480-5.jpg';
function drawClippedImgAtXY(img,clipPts,x,y){
var minX=10000000;
var minY=10000000;
for(var i=0;i<clipPts.length;i++){
var pt=clipPts[i];
if(pt.x<minX){minX=pt.x;}
if(pt.y<minY){minY=pt.y;}
}
ctx.save();
ctx.translate(-minX+x,-minY+y);
ctx.moveTo(clipPts[0].x,clipPts[0].y);
for(var i=1;i<clipPts.length;i++){
ctx.lineTo(clipPts[i].x,clipPts[i].y);
}
ctx.clip();
ctx.drawImage(img,0,0);
ctx.restore();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<h4>Move a clipped area to x==100, y==20</h4>
<canvas id="canvas" width=600 height=400></canvas>

Crop the image in irregular shape and stretch it

I found images that depict what is my problem:
User will able to choose four points on canvas to crop the part of image and than stretch it.
How to do that in HTML5? drawImage function (as I know) works only with rectangles (takes x, y, width and height values) so I can't use irregular shape. The solution have to work in every modern browser, so I don't want things based on webgl or something.
EDIT:
More info: this will be app for editing pictures. I want to let user cut some part of bigger picture and edit that. It will be similar to Paint, so canvas is required to edit pixels.
The effect you're going for is "perspective warping".
Canvas's 2D context cannot do this "out-of-the-box" because it can't turn a rectangle into a trapezoid. Canvas 2D can only do affine transforms which can only form parallelograms.
As user #Canvas says, Canvas 3D (webgl) can do the transforms you're going for.
I did this a while back. It uses Canvas 2d and it redraws an image using 1 pixel wide vertical slices which are stretched to "fake" a perspective warp. You are welcome to use it as a starting point for your project.
Example code and a Demo: http://jsfiddle.net/m1erickson/y4kst2pk/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
#canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var scrollX=$canvas.scrollLeft();
var scrollY=$canvas.scrollTop();
//
var isDown=false;
var PI2=Math.PI*2;
var selectedGuide=-1;
var guides=[];
//
var marginLeft=50;
var marginTop=50;
var iw,ih,cw,ch;
var img=new Image();
img.onload=start;
img.src='https://dl.dropboxusercontent.com/u/139992952/stack1/buildings1.jpg';
function start(){
iw=img.width;
ih=img.height;
canvas.width=iw+100;
canvas.height=ih+100;
cw=canvas.width;
ch=canvas.height;
ctx.strokeStyle="blue";
ctx.fillStyle="blue";
guides.push({x:0,y:0,r:10});
guides.push({x:0,y:ih,r:10});
guides.push({x:iw,y:0,r:10});
guides.push({x:iw,y:ih,r:10});
//
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUp(e);});
$("#canvas").mouseout(function(e){handleMouseOut(e);});
drawAll();
}
function drawAll(){
ctx.clearRect(0,0,cw,ch);
drawGuides();
drawImage();
}
function drawGuides(){
for(var i=0;i<guides.length;i++){
var guide=guides[i];
ctx.beginPath();
ctx.arc(guide.x+marginLeft,guide.y+marginTop,guide.r,0,PI2);
ctx.closePath();
ctx.fill();
}
}
function drawImage(){
// TODO use guides
var x1=guides[0].x;
var y1=guides[0].y;
var x2=guides[2].x;
var y2=guides[2].y;
var x3=guides[1].x;
var y3=guides[1].y;
var x4=guides[3].x;
var y4=guides[3].y;
// calc line equations slope & b (m,b)
var m1=Math.tan( Math.atan2((y2-y1),(x2-x1)) );
var b1=y2-m1*x2;
var m2=Math.tan( Math.atan2((y4-y3),(x4-x3)) );
var b2=y4-m2*x4;
// draw vertical slices
for(var X=0;X<iw;X++){
var yTop=m1*X+b1;
var yBottom=m2*X+b2;
ctx.drawImage( img,X,0,1,ih,
X+marginLeft,yTop+marginTop,1,yBottom-yTop );
}
// outline
ctx.save();
ctx.translate(marginLeft,marginTop);
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x4,y4);
ctx.lineTo(x3,y3);
ctx.closePath();
ctx.strokeStyle="black";
ctx.stroke();
ctx.restore();
}
function handleMouseDown(e){
e.preventDefault();
var mouseX=parseInt(e.clientX-offsetX);
var mouseY=parseInt(e.clientY-offsetY);
// Put your mousedown stuff here
selectedGuide=-1;
for(var i=0;i<guides.length;i++){
var guide=guides[i];
var dx=mouseX-(guide.x+marginLeft);
var dy=mouseY-(guide.y+marginTop);
if(dx*dx+dy*dy<=guide.r*guide.r){
selectedGuide=i;
break;
}
}
isDown=(selectedGuide>=0);
}
function handleMouseUp(e){
e.preventDefault();
isDown=false;
}
function handleMouseOut(e){
e.preventDefault();
isDown=false;
}
function handleMouseMove(e){
if(!isDown){return;}
e.preventDefault();
var x=parseInt(e.clientX-offsetX)-marginLeft;
var y=parseInt(e.clientY-offsetY)-marginTop;
var guide=guides[selectedGuide];
guides[selectedGuide].y=y;
if(selectedGuide==0 && y>guides[1].y){guide.y=guides[1].y;}
if(selectedGuide==1 && y<guides[0].y){guide.y=guides[0].y;}
if(selectedGuide==2 && y>guides[3].y){guide.y=guides[3].y;}
if(selectedGuide==3 && y<guides[2].y){guide.y=guides[2].y;}
drawAll();
}
}); // end $(function(){});
</script>
</head>
<body>
<h4>Perspective Warp by vertically dragging left or right blue guides.</h4>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
So here's a killing trick : You can use the regular drawImage of the context2d to draw a texture inside a triangle.
The constraint is that the texture coordinates must be axis-aligned.
To draw a textured triangle you have to :
• Compute by yourself the transform required to draw the image.
• Clip to a triangle, since drawImage would draw a quad.
• drawImage with the right transform.
So the idea is to split your quad into two triangles, and render both.
But there's one more trick : when drawing the lower-right triangle, texture reading should start from the lower-right part of the texture, then move up and left. This can't be done in Firefox, which accepts only positive arguments to drawImage. So i compute the 'mirror' of the first point vs the two others, and i can draw in the regular direction again -the clipping will ensure only right part is drawn-.
fiddle is here :
http://jsfiddle.net/gamealchemist/zch3gdrx/
function rasterizeTriangle(v1, v2, v3, mirror) {
var fv1 = {
x: 0,
y: 0,
u: 0,
v: 0
};
fv1.x = v1.x;
fv1.y = v1.y;
fv1.u = v1.u;
fv1.v = v1.v;
ctx.save();
// Clip to draw only the triangle
ctx.beginPath();
ctx.moveTo(v1.x, v1.y);
ctx.lineTo(v2.x, v2.y);
ctx.lineTo(v3.x, v3.y);
ctx.clip();
// compute mirror point and flip texture coordinates for lower-right triangle
if (mirror) {
fv1.x = fv1.x + (v3.x - v1.x) + (v2.x - v1.x);
fv1.y = fv1.y + (v3.y - v1.y) + (v2.y - v1.y);
fv1.u = v3.u;
fv1.v = v2.v;
}
//
var angleX = Math.atan2(v2.y - fv1.y, v2.x - fv1.x);
var angleY = Math.atan2(v3.y - fv1.y, v3.x - fv1.x);
var scaleX = lengthP(fv1, v2);
var scaleY = lengthP(fv1, v3);
var cos = Math.cos,
sin = Math.sin;
// ----------------------------------------
// Transforms
// ----------------------------------------
// projection matrix (world relative to center => screen)
var transfMatrix = [];
transfMatrix[0] = cos(angleX) * scaleX;
transfMatrix[1] = sin(angleX) * scaleX;
transfMatrix[2] = cos(angleY) * scaleY;
transfMatrix[3] = sin(angleY) * scaleY;
transfMatrix[4] = fv1.x;
transfMatrix[5] = fv1.y;
ctx.setTransform.apply(ctx, transfMatrix);
// !! draw !!
ctx.drawImage(bunny, fv1.u, fv1.v, v2.u - fv1.u, v3.v - fv1.v,
0, 0, 1, 1);
//
ctx.restore();
};
Edit : i added the relevant comment of #szym , with his example picture :
This only sort of looks right. If there are any straight lines in the
original image you will see that each triangle is warped differently
(2 different affine transforms rather than a perspective transform).
You need to have a container with perspective and perspective-origin set
You need to use rotateY, skewY and change your heights and width on your image
There is probably a lot of math behind this - personally I just fiddle with it in my browser to make it look pretty close to what I need
So here is a fiddle:
http://jsfiddle.net/6egdevwe/1/
#container {
margin: 50px;
perspective: 166px; perspective-origin: 50% 0px; }
#testimage {
transform: rotateY(93.4deg) skewY(34deg);
width: 207px;
height: 195px; }
css3 transform -> rotation or rotationZ
http://www.w3schools.com/cssref/css3_pr_transform.asp

In a 2D canvas, is there a way to give a sprite an outline?

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...

Categories

Resources