Continuous gradient along a HTML5 canvas path - javascript

I am trying to draw a continous gradient along a path of points, where each point has a it's own color, using the HTML5 canvas API.
See http://bl.ocks.org/rveciana/10743959 for inspiration, where that effect is achieved with D3.
There doesn't seem to be a way to add multiple linear gradients for a single canvas path, so I resorted to something like this: http://jsfiddle.net/51toapv2/
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var pts = [[100, 100, "red"], [150, 150, "green"], [200, 100, "yellow"]];
ctx.lineWidth = 20;
ctx.lineJoin = "round";
ctx.lineCap = "round";
for (var i = 0; i < pts.length - 1; i++) {
var begin = pts[i];
var end = pts[i + 1];
ctx.beginPath();
var grad = ctx.createLinearGradient(begin[0], begin[1], end[0], end[1]);
grad.addColorStop(0, begin[2]);
grad.addColorStop(1, end[2]);
ctx.strokeStyle = grad;
ctx.moveTo(begin[0], begin[1]);
ctx.lineTo(end[0], end[1]);
ctx.stroke();
}
As you can see it produces a subpar effect as the paths aren't merged and the "line joins" are clearly visible.
Is it possible to achieve the effect I'm looking for with the canvas API?

Here's a slight modification of your original idea that makes the joins blend nicely.
Original: Draw a gradient line from the start to end of a line segment.
This causes the line joins to overlap and produces a noticeable & undesired transition.
Modification: Draw a gradient line that doesn't extend to the start / endpoints.
With this modification, the line joins will always be solid colors rather than be partially gradiented. As a result, the line joins will transition nicely between line segments.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var lines = [
{x:100, y:050,color:'red'},
{x:150, y:100,color:'green'},
{x:200, y:050,color:'gold'},
{x:275, y:150,color:'blue'}
];
var linewidth=20;
ctx.lineCap='round';
ctx.lineJoint='round';
for(var i=1;i<lines.length;i++){
// calculate the smaller part of the line segment over
// which the gradient will run
var p0=lines[i-1];
var p1=lines[i];
var dx=p1.x-p0.x;
var dy=p1.y-p0.y;
var angle=Math.atan2(dy,dx);
var p0x=p0.x+linewidth*Math.cos(angle);
var p0y=p0.y+linewidth*Math.sin(angle);
var p1x=p1.x+linewidth*Math.cos(angle+Math.PI);
var p1y=p1.y+linewidth*Math.sin(angle+Math.PI);
// determine where the gradient starts and ends
if(i==1){
var g=ctx.createLinearGradient(p0.x,p0.y,p1x,p1y);
}else if(i==lines.length-1){
var g=ctx.createLinearGradient(p0x,p0y,p1.x,p1.y);
}else{
var g=ctx.createLinearGradient(p0x,p0y,p1x,p1y);
}
// add the gradient color stops
// and draw the gradient line from p0 to p1
g.addColorStop(0,p0.color);
g.addColorStop(1,p1.color);
ctx.beginPath();
ctx.moveTo(p0.x,p0.y);
ctx.lineTo(p1.x,p1.y);
ctx.strokeStyle=g;
ctx.lineWidth=linewidth;
ctx.stroke();
}
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=350 height=200></canvas>

You can do a simple approach interpolating two colors along a line. If you need smooth/shared gradients where two lines joins at steeper angles, you would need to calculate and basically implement a line drawing algorithm from (almost) scratch. This would be out of scope for SO, so here is a simpler approach.
That being said - the example in the link is not actually a line but several plots of squares of different colors. The issues it would have too is "hidden" by its subtle variations.
Example
This approach requires two main functions:
Line interpolate function which draws each segment in a line from previous mouse position to current position
Color interpolate function which takes an array of colors and interpolate between two current colors depending on length, position and segment size.
Tweak parameters such as segment size, number of colors in the array etc. to get the optimal result.
Line interpolate function
function plotLine(ctx, x1, y1, x2, y2) {
var diffX = Math.abs(x2 - x1), // get line length
diffY = Math.abs(y2 - y1),
dist = Math.sqrt(diffX * diffX + diffY * diffY),
step = dist / 10, // define some resolution
i = 0, t, b, x, y;
while (i <= dist) { // render circles along the line
t = Math.min(1, i / dist);
x = x1 + (x2 - x1) * t;
y = y1 + (y2 - y1) * t;
ctx.fillStyle = getColor(); // get current color
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI*2);
ctx.fill();
i += step;
}
Color interpolate function
function getColor() {
var r, g, b, t, c1, c2;
c1 = colors[cIndex]; // get current color from array
c2 = colors[(cIndex + 1) % maxColors]; // get next color
t = Math.min(1, total / segment); // calculate t
if (++total > segment) { // rotate segment
total = 0;
if (++cIndex >= maxColors) cIndex = 0; // rotate color array
}
r = c1.r + (c2.r - c1.r) * t; // interpolate color
g = c1.g + (c2.g - c1.g) * t;
b = c1.b + (c2.b - c1.b) * t;
return "rgb(" + (r|0) + "," + (g|0) + "," + (b|0) + ")";
}
Demo
Putting it all together will allow you to draw gradient lines. If you don't want to draw them manually simply call the plotLine() function whenever needed.
// Some setup code
var c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
colors = [
{r: 255, g: 0, b: 0},
{r: 255, g: 255, b: 0},
{r: 0, g: 255, b: 0},
{r: 0, g: 255, b: 255},
{r: 0, g: 0, b: 255},
{r: 255, g: 0, b: 255},
{r: 0, g: 255, b: 255},
{r: 0, g: 255, b: 0},
{r: 255, g: 255, b: 0},
],
cIndex = 0, maxColors = colors.length,
total = 0, segment = 500,
isDown = false, px, py;
setSize();
c.onmousedown = c.ontouchstart = function(e) {
isDown = true;
var pos = getPos(e);
px = pos.x;
py = pos.y;
};
window.onmousemove = window.ontouchmove = function(e) {if (isDown) plot(e)};
window.onmouseup = window.ontouchend = function(e) {
e.preventDefault();
isDown = false
};
function getPos(e) {
e.preventDefault();
if (e.touches) e = e.touches[0];
var r = c.getBoundingClientRect();
return {
x: e.clientX - r.left,
y: e.clientY - r.top
}
}
function plot(e) {
var pos = getPos(e);
plotLine(ctx, px, py, pos.x, pos.y);
px = pos.x;
py = pos.y;
}
function plotLine(ctx, x1, y1, x2, y2) {
var diffX = Math.abs(x2 - x1),
diffY = Math.abs(y2 - y1),
dist = Math.sqrt(diffX * diffX + diffY * diffY),
step = dist / 50,
i = 0,
t, b, x, y;
while (i <= dist) {
t = Math.min(1, i / dist);
x = x1 + (x2 - x1) * t;
y = y1 + (y2 - y1) * t;
ctx.fillStyle = getColor();
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI*2);
ctx.fill();
i += step;
}
function getColor() {
var r, g, b, t, c1, c2;
c1 = colors[cIndex];
c2 = colors[(cIndex + 1) % maxColors];
t = Math.min(1, total / segment);
if (++total > segment) {
total = 0;
if (++cIndex >= maxColors) cIndex = 0;
}
r = c1.r + (c2.r - c1.r) * t;
g = c1.g + (c2.g - c1.g) * t;
b = c1.b + (c2.b - c1.b) * t;
return "rgb(" + (r|0) + "," + (g|0) + "," + (b|0) + ")";
}
}
window.onresize = setSize;
function setSize() {
c.width = window.innerWidth;
c.height = window.innerHeight;
}
document.querySelector("button").onclick = function() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
};
html, body {background:#777; margin:0; overflow:hidden}
canvas {position:fixed;left:0;top:0;background: #333}
button {position:fixed;left:10px;top:10px}
<canvas></canvas>
<button>Clear</button>
TIPS:
The gradient values can be pre-populated / cached beforehand
The step for position in gradient can be bound to length to get even spread independent of draw speed
You can easily replace the brush with other path/figures/shapes, even combine image based brushes which is composited with current color

Related

How to create upload in each hexagon shape in canvas?

I create multi shape with canvas, but I want to upload a photo by clicking on each hexagon.
How to create with jquery?
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const a = 2 * Math.PI / 6;
const r = 50;
// 1st
x = r;
y = r;
drawHexagon(x, y);
// 2nd
x = x + r + r * Math.cos(a);
y = y + r * Math.sin(a);
drawHexagon(x, y);
// 3rd
x = x + r + r * Math.cos(a);
y = y - r * Math.sin(a);
drawHexagon(x, y);
// 4th
x = x + r + r * Math.cos(a);
y = y + r * Math.sin(a);
drawHexagon(x, y);
function drawHexagon(x, y) {
ctx.beginPath();
for (var i = 0; i < 6; i++) {
ctx.lineTo(x + r * Math.cos(a * i), y + r * Math.sin(a * i));
}
ctx.closePath();
ctx.stroke();
}
<canvas id="canvas" width="800" height="500" />
I'm going to answer the first part of your problem...
The comment from ggorlen is spot on, you have a complex situation, you must break it down into multiple questions and tackle each individually.
So how do we detect a mouse event over a particular shape in a canvas?
My recommendation use Path2D:
https://developer.mozilla.org/en-US/docs/Web/API/Path2D
It comes with a handy function isPointInPath to detect if a point is in or path:
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
Sample code below, I'm using the mouse move event but you can use any other:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
class Hexagon {
constructor(x, y, r, a) {
this.path = new Path2D()
for (var i = a; i < a+Math.PI*2; i+=Math.PI/3) {
this.path.lineTo(x + r * Math.cos(i), y + r * Math.sin(i));
}
}
draw(evt) {
ctx.beginPath()
var rect = canvas.getBoundingClientRect()
var x = evt.clientX - rect.left
var y = evt.clientY - rect.top
ctx.fillStyle = ctx.isPointInPath(this.path, x, y) ? "red" : "green"
ctx.fill(this.path)
}
}
shapes = []
shapes.push(new Hexagon( 50, 50, 40, 0))
shapes.push(new Hexagon(125, 90, 45, 0.5))
shapes.push(new Hexagon(200, 50, 30, 0.8))
shapes.push(new Hexagon(275, 90, 53, 4.1))
canvas.addEventListener("mousemove", function(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
shapes.forEach((s) => s.draw(evt))
}
)
shapes.forEach((s) => s.draw({ clientX: 0, clientY: 0 }))
<canvas id="canvas" width="400" height="180" />
I hardcoded your constants to keep this example as small as possible
Also my class Hexagon takes the radius and angle as parameters that way we can have different Hexagons see: constructor(x, y, r, a)

draw arc between 2 lines & draw a curve 2 points

Framework: fabricjs
My first problem is to draw a angle betweens 2 Lines. My code is working but i'm not happy with the result.
My second problem is to draw a curve between 2 Points.
My Code for the first problem.
I have 3 Points:
A , B , C
2 Lines:
AB , BC
With this information i calculate distance points with distance 10.
let angle = this.calcAngle(A,B,C);
let distanceAB = this.calcCornerPoint(A, B, 10);
let distanceBC = this.calcCornerPoint(C, B, 10);
Calc Angle:
calcAngle(A, B, C, final, finalAddon = "°") {
var nx1 = A.x - B.x;
var ny1 = A.y - B.y;
var nx2 = C.x - B.x;
var ny2 = C.y - B.y;
this.lineAngle1 = Math.atan2(ny1, nx1);
this.lineAngle2 = Math.atan2(ny2, nx2);
if (nx1 * ny2 - ny1 * nx2 < 0) {
const t = lineAngle2;
this.lineAngle2 = this.lineAngle1;
this.lineAngle1 = t;
}
// if angle 1 is behind then move ahead
if (lineAngle1 < lineAngle2) {
this.lineAngle1 += Math.PI * 2;
}
}
Than draw a Path with:
this.drawAngleTrapez(distanceAB, distanceBC, B);
drawAngleTrapez(AB, BC, B) {
let points = [AB, BC, B];
let path = "";
if (this.trapezObjs[this.iterator]) {
this.canvas.remove(this.trapezObjs[this.iterator]);
}
path += "M " + Math.round(points[0].x) + " " + Math.round(points[0].y) + "";
for (let i = 1; i < points.length; i++) {
path += " L " + Math.round(points[i].x) + " " + Math.round(points[i].y) + "";
}
this.currentTrapez = this.trapezObjs[this.iterator] = new fabric.Path(path, {
selectable: false,
hasControls: false,
hasBorders: false,
hoverCursor: 'default',
fill: '#ccc',
strokeWidth: this.strokeWidth,
});
this.canvas.add(this.trapezObjs[this.iterator]);
}
And than i draw a Circle:
drawAnglePoint(B,d = 10) {
this.currentCorner = new fabric.Circle({
left: B.x,
top: B.y,
startAngle: this.lineAngle1,
endAngle: this.lineAngle2,
radius: 10,
fill: '#ccc',
selectable: false,
hasControls: false,
hasBorders: false,
hoverCursor: 'default',
});
this.canvas.add(this.currentCorner);
}
But the result ist not beautiful:
And the blue point is not on the end of the line, mabye here also a little fix.
this.startPoint.set({ left: C.x, top: C.y });
Second Problem solved: was an error in my calculation.
The problem is , its not a beautiful curve:
Rather than draw the central 'wedge' as 2 shapes - a triangle and a portion of a circle, you should instead draw it as the 2-sided figure that it is.
Circles are drawn by supplying the origin. Therefore, to draw the blue point on the end of the line, you should specify the same coordinates for the end-point as you do for the circle-centre.
The below code will re-create your first image with the exception of the text.
While I've drawn the swept-angle indicator transparently, ontop of the lines, you'd probably want to change the draw order, colour and opacity.
(I used 0x88/0xFF = 136/255 = 53.3%),
"use strict";
function newEl(tag){return document.createElement(tag)}
function byId(id){return document.getElementById(id)}
class vec2
{
constructor(x,y){this.x = x;this.y = y;}
get x(){ return this._x; }
set x(newVal){ this._x = newVal; }
get y(){ return this._y; }
set y(newVal){ this._y = newVal; }
get length(){return Math.hypot(this.x,this.y);}
set length(len){var invLen = len/this.length; this.timesEquals(invLen);}
add(other){return new vec2(this.x+other.x, this.y+other.y);}
sub(other){return new vec2(this.x-other.x, this.y-other.y);}
plusEquals(other){this.x+=other.x;this.y+=other.y;return this;}
minusEquals(other){this.x-=other.x;this.y-=other.y;return this;}
timesEquals(scalar){this.x*=scalar;this.y*=scalar;return this;}
divByEquals(scalar){this.x/=scalar;this.y/=scalar;return this;}
setTo(other){this.x=other.x;this.y=other.y;}
toString(){return `vec2 {x: ${this.x}, y: ${this.y}}` }
toStringN(n){ return `vec2 {x: ${this.x.toFixed(n)}, y: ${this.y.toFixed(n)}}` }
dotProd(other){return this.x*other.x + this.y*other.y;}
timesEquals(scalar){this.x *= scalar;this.y *= scalar;return this;}
normalize(){let len = this.length;this.x /= len;this.y /= len;return this;}
static clone(other){let result = new vec2(other.x, other.y);return result;}
clone(){return vec2.clone(this);}
};
window.addEventListener('load', onWindowLoaded, false);
function onWindowLoaded(evt)
{
var can = byId('output');
let A = new vec2(172,602), B = new vec2(734,602), C = new vec2(847,194);
myTest(can, [A,B,C]);
}
function circle(canvas, x, y, radius)
{
let ctx = canvas.getContext('2d');
ctx.moveTo(x,y);
ctx.beginPath();
ctx.ellipse(x,y, radius,radius, 0, 0, 2*Math.PI);
ctx.closePath();
ctx.fill();
}
function getAngle(origin, pt)
{
let delta = pt.sub(origin);
let angle = Math.atan2( delta.y, delta.x );
return angle;
}
function myTest(canvas, points)
{
let ctx = canvas.getContext('2d');
// background colour
//
ctx.fillStyle = '#ebedf0';
ctx.fillRect(0,0,canvas.width,canvas.height);
// white square grid
//
//60,33 = intersection of first
//115 = square-size
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 12;
for (let x=60; x<canvas.width; x+=112)
{
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
ctx.closePath();
}
for (let y=33; y<canvas.height; y+=112)
{
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
ctx.closePath();
}
// wedge indicating swept angle
let angle1 = getAngle(points[1], points[2]);
let angle2 = getAngle(points[1], points[0]);
ctx.beginPath();
ctx.moveTo(points[1].x,points[1].y);
ctx.arc(points[1].x,points[1].y, 70, angle1,angle2, true);
ctx.fillStyle = '#cccccc88';
ctx.fill();
console.log(angle1, angle2);
// lines
//
ctx.lineWidth = 9;
ctx.strokeStyle = '#c3874c';
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.lineTo(points[2].x, points[2].y);
ctx.stroke();
ctx.closePath();
// points
//
ctx.fillStyle = '#3b89c9';
ctx.beginPath();
points.forEach( function(pt){circle(canvas, pt.x,pt.y, 10);} );
ctx.closePath();
}
canvas
{
zoom: 67%;
}
<canvas id='output' width='996' height='730'></canvas>

Fastest way to draw and fill a NOT anti-aliasing circle in HTML5Canvas

I need to draw and fill a not anti-aliased circle for a basic drawing app in HTML5Canvas, because fill bucket tools algorithms dont fill anti-aliased shapes border nicely.
I took the javascript algorithm of this page https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
and implement it to draw filled circle, but its very slow.
canvas = document.getElementById("canvas");
const CHANNELS_PER_PIXEL = 4; //rgba
function drawCircle (x0, y0, radius, canvas) {
var x = radius-1;
var y = 0;
var dx = 1;
var dy = 1;
var decisionOver2 = dx - (radius << 1); // Decision criterion divided by 2 evaluated at x=r, y=0
var imageWidth = canvas.width;
var imageHeight = canvas.height;
var context = canvas.getContext('2d');
var imageData = context.getImageData(0, 0, imageWidth, imageHeight);
var pixelData = imageData.data;
var makePixelIndexer = function (width) {
return function (i, j) {
var index = CHANNELS_PER_PIXEL * (j * width + i);
//index points to the Red channel of pixel
//at column i and row j calculated from top left
return index;
};
};
var pixelIndexer = makePixelIndexer(imageWidth);
var drawPixel = function (x, y) {
var idx = pixelIndexer(x,y);
pixelData[idx] = 152; //red
pixelData[idx + 1] = 152; //green
pixelData[idx + 2] = 152;//blue
pixelData[idx + 3] = 255;//alpha
};
while (x >= y) {
if(x0 + x>=0){drawPixel(x0 + x, y0 + y);}
if(x0 + y>=0){drawPixel(x0 + y, y0 + x);}
if(x0 - x>=0){drawPixel(x0 - x, y0 + y);}
if(x0 - y>=0){drawPixel(x0 - y, y0 + x);}
if(x0 - x>=0){drawPixel(x0 - x, y0 - y);}
if(x0 - y>=0){drawPixel(x0 - y, y0 - x);}
if(x0 + x>=0){drawPixel(x0 + x, y0 - y);}
if(x0 + y>=0){drawPixel(x0 + y, y0 - x);}
//fill circle code
var x1=x0-x;
var x2=x0+x;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0+y);
}
}
var x1=x0-y;
var x2=x0+y;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0+x);
}
}
var x1=x0-x;
var x2=x0+x;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0-y);
}
}
var x1=x0-y;
var x2=x0+y;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0-x);
}
}
//fill end
if (decisionOver2 <= 0)
{
y++;
decisionOver2 += dy; // Change in decision criterion for y -> y+1
dy += 2;
}
if (decisionOver2 > 0)
{
x--;
dx += 2;
decisionOver2 += (-radius << 1) + dx; // Change for y -> y+1, x -> x-1
}
}
context.putImageData(imageData, 0, 0);
}
Also,
context.translate(0.5, 0.5);
and
context.imageSmoothingEnabled = !1;
dont work for a circle.
Do you have better functions or do you know how to compress and concatenate this circle algorithm ?
Thanks
I made this modified version of the Breseham circle algorithm to fill "aliased" circles a while back for a "retro" project.
The modification is taking the values from the 8 slices and converts them to 4 lines. We can use rect() to create a line but have to convert the absolute (x2,y2) coordinate to width and height instead.
The method simply add rect's to the path which is pretty fast and you don't have to go via the slow getImageData()/putImageData() (and doesn't get inflicted by CORS issues). And at the end one single fill operation is invoked. This means you can also use this directly on the canvas without having to be worry about existing content in most cases.
It's important that the translate and the given values are integer values, and that radius > 0.
To force integer values simply shift the value 0:
xc = xc|0; // you can add these to the function below
yc = yc|0;
r = r|0;
(If you should want to make an outline ("stroked") version you would have to use all 8 slices' positions and change width for rect() to 1.)
Demo
var ctx = c.getContext("2d");
ctx.fillStyle = "#09f";
aliasedCircle(ctx, 200, 200, 180);
ctx.fill();
function aliasedCircle(ctx, xc, yc, r) { // NOTE: for fill only!
var x = r, y = 0, cd = 0;
// middle line
ctx.rect(xc - x, yc, r<<1, 1);
while (x > y) {
cd -= (--x) - (++y);
if (cd < 0) cd += x++;
ctx.rect(xc - y, yc - x, y<<1, 1); // upper 1/4
ctx.rect(xc - x, yc - y, x<<1, 1); // upper 2/4
ctx.rect(xc - x, yc + y, x<<1, 1); // lower 3/4
ctx.rect(xc - y, yc + x, y<<1, 1); // lower 4/4
}
}
<canvas id=c width=400 height=400></canvas>
Zoomed-in demo:
var ctx = c.getContext("2d");
ctx.scale(4,4);
ctx.fillStyle = "#09f";
aliasedCircle(ctx, 50, 50, 45);
ctx.fill();
ctx.font = "6px sans-serif";
ctx.fillText("4x", 2, 8);
function aliasedCircle(ctx, xc, yc, r) {
var x = r, y = 0, cd = 0;
// middle line
ctx.rect(xc - x, yc, r<<1, 1);
while (x > y) {
cd -= (--x) - (++y);
if (cd < 0) cd += x++;
ctx.rect(xc - y, yc - x, y<<1, 1); // upper 1/4
ctx.rect(xc - x, yc - y, x<<1, 1); // upper 2/4
ctx.rect(xc - x, yc + y, x<<1, 1); // lower 3/4
ctx.rect(xc - y, yc + x, y<<1, 1); // lower 4/4
}
}
<canvas id=c width=400 height=400></canvas>

HTML5 Canvas thick lineWidth ellipse has strange blank

I'm making a drawing app with html5 canvas.
User can draw ellipses and select both line color and fill color.
(including transparent colors)
When selected color is not transparent, it works fine.
But when transparent color is selected and border line width is thick, there are problems.(Q1 and Q2)
This is the image
http://tinypic.com/view.php?pic=28ry4z&s=9#.VoRs7U8jHSg
I'm using drawEllipse() method from below.
the relation of the bezier Curve and ellipse?
Does anyone solve this problems?
Any help will be greatly appreciated.
[Q1]
When the lineWidth is larger than the ellipse's width, there is a strange blank in the ellipse, and lineWidth is strangely thin.
Internet Explorer works fine, but both Firefox and Safari web browsers have this problem.
How can I change the blank area to be blue?
[Q2]
I'm using transparent colors and I want to draw the ellipse with 2 colors.
(stroke is blue and fill is red)
But the stroke color and the fill color are mixed and there is magenta area in the ellipse.
How can I draw the ellipse with 2 colors?
(I want to change the magenta area to blue)
One time fill is preferred when possible.
Here is my code
// this method is from
// https://stackoverflow.com/questions/14169234/the-relation-of-the-bezier-curve-and-ellipse
function _drawEllipse(ctx, x, y, w, h) {
var width_over_2 = w / 2;
var width_two_thirds = w * 2 / 3;
var height_over_2 = h / 2;
ctx.beginPath();
ctx.moveTo(x, y - height_over_2);
ctx.bezierCurveTo(x + width_two_thirds, y - height_over_2, x + width_two_thirds, y + height_over_2, x, y + height_over_2);
ctx.bezierCurveTo(x - width_two_thirds, y + height_over_2, x - width_two_thirds, y - height_over_2, x, y - height_over_2);
ctx.closePath();
ctx.stroke();
}
function ellipse_test() {
var canvas = document.getElementById('sample1');
var ctx = canvas.getContext('2d');
var x = 100;
var y = 100;
var w = 40;
var h = 100;
ctx.lineWidth = 30;
ctx.fillStyle = "rgba(255,0,0,0.5)";
ctx.strokeStyle = "rgba(0,0,255,0.5)";
ctx.globalCompositeOperation = "source-over";
for (var r = 0; r < 50; r++) {
_drawEllipse(ctx, x, y, r, r * 2);
ctx.fill();
x += 60;
if (x > 1000) {
x = 100;
y += 200;
}
}
}
ellipse_test();
<canvas id="sample1" style="border:1px solid blue; background:black;" width="1200" height="800"></canvas>
this is the image on firefox
Both problems are caused by the fact that multiple strokes/fills of semi-transparent colors over an area will cause that area to become a blend of colors (much like an artist blends multiple colors).
You can resolve both problems by converting semi-transparent colors into opaque colors:
function RGBAtoRGB(r, g, b, a, backgroundR,backgroundG,backgroundB){
var r3 = Math.round(((1 - a) * backgroundR) + (a * r))
var g3 = Math.round(((1 - a) * backgroundG) + (a * g))
var b3 = Math.round(((1 - a) * backgroundB) + (a * b))
return "rgb("+r3+","+g3+","+b3+")";
}
// convert 50%-red foreground fill + 100% black background into opaque (=="red-brownish")
ctx.fillStyle = RGBAtoRGB(255,0,0,0.50, 0,0,0,1); // "rgba(255,0,0,0.5)";
// convert 50%-blue foreground stroke + 100% black background into opaque (=="blueish")
ctx.strokeStyle = RGBAtoRGB(0,0,255,0.50, 0,0,0,1); // "rgba(0,0,255,0.5)";
Example code refactored to use opaque fills & strokes:
ellipse_test();
// this method is from
// http://stackoverflow.com/questions/14169234/the-relation-of-the-bezier-curve-and-ellipse
function _drawEllipse(ctx, x, y, w, h) {
var width_over_2 = w / 2;
var width_two_thirds = w * 2 / 3;
var height_over_2 = h / 2;
ctx.beginPath();
ctx.moveTo(x, y - height_over_2);
ctx.bezierCurveTo(x + width_two_thirds, y - height_over_2, x + width_two_thirds, y + height_over_2, x, y + height_over_2);
ctx.bezierCurveTo(x - width_two_thirds, y + height_over_2, x - width_two_thirds, y - height_over_2, x, y - height_over_2);
ctx.closePath();
}
function ellipse_test() {
var canvas = document.getElementById('sample1');
var ctx = canvas.getContext('2d');
var x = 100;
var y = 100;
var w = 40;
var h = 100;
ctx.lineWidth = 30;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = RGBAtoRGB(255, 0, 0, 0.50, 0, 0, 0, 1); // "rgba(255,0,0,0.5)";
ctx.strokeStyle = RGBAtoRGB(0, 0, 255, 0.50, 0, 0, 0, 1); // "rgba(0,0,255,0.5)";
ctx.globalCompositeOperation = "source-over";
for (var r = 0; r < 50; r++) {
_drawEllipse(ctx, x, y, r, r * 2);
ctx.stroke();
ctx.fill();
x += 60;
if (x > 1000) {
x = 100;
y += 200;
}
}
}
function RGBAtoRGB(r, g, b, a, backgroundR, backgroundG, backgroundB) {
var r3 = Math.round(((1 - a) * backgroundR) + (a * r))
var g3 = Math.round(((1 - a) * backgroundG) + (a * g))
var b3 = Math.round(((1 - a) * backgroundB) + (a * b))
return "rgb(" + r3 + "," + g3 + "," + b3 + ")";
}
body {
background-color: ivory;
}
#canvas {
border: 1px solid red;
background-color=black;
}
<canvas id="sample1" width=1200 height=800></canvas>
Overlapping
...And obviously if you draw your ellipses very close together they will eventually overlap. That's what's causing your Q1-line thinning.

Javascript Canvas apply Radial Gradient to Segment?

I am trying to create a shadow system for my 2D Game in a HTML5 Canvas. Right now, I am rendering my shadows like so:
function drawShadows(x, y, width) {
if (shadowSprite == null) {
shadowSprite = document.createElement('canvas');
var tmpCtx = shadowSprite.getContext('2d');
var shadowBlur = 20;
shadowSprite.width = shadowResolution;
shadowSprite.height = shadowResolution;
var grd = tmpCtx.createLinearGradient(-(shadowResolution / 4), 0,
shadowResolution, 0);
grd.addColorStop(0, "rgba(0, 0, 0, 0.1)");
grd.addColorStop(1, "rgba(0, 0, 0, 0)");
tmpCtx.fillStyle = grd;
tmpCtx.shadowBlur = shadowBlur;
tmpCtx.shadowColor = "#000";
tmpCtx.fillRect(0, 0, shadowResolution, shadowResolution);
}
graph.save();
graph.rotate(sun.getDir(x, y));
graph.drawImage(shadowSprite, 0, -(width / 2), sun.getDist(x, y), width);
graph.restore();
}
This renders a cube with a linear gradient that fades from black to alpha 0.
This however does not produce a realistic result, since it will always be a rectangle. Here is an illustration to describe the problem:
Sorry i'm not very artistic. It would not be an issue to draw the trapezoid shape. (Seen in blue). The issue is that I still there to be a gradient. Is it possible to draw a shape like that with a gradient?
The canvas is very flexible. Almost anything is possible. This example draws the light being cast. But it can just as easily be the reverse. Draw the shadows as a gradient.
If you are after realism then instead of rendering a gradient for the lighting (or shadows) use the shape created to set a clipping area and then render a accurate lighting and shadow solution.
With lineTo and gradients you can create any shape and gradient you my wish. Also to get the best results use globalCompositeOperation as they have a large variety of filters.
The demo just shows how to mix a gradient and a shadow map. (Very basic no recursion implemented, and shadows are just approximations.)
var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");
var mouse = {
x:0,
y:0,
};
function mouseMove(event){
mouse.x = event.offsetX; mouse.y = event.offsetY;
if(mouse.x === undefined){ mouse.x = event.clientX; mouse.y = event.clientY;}
}
// add mouse controls
canvas.addEventListener('mousemove',mouseMove);
var boundSize = 10000; // a number....
var createImage = function(w,h){ // create an image
var image;
image = document.createElement("canvas");
image.width = w;
image.height = h;
image.ctx = image.getContext("2d");
return image;
}
var directionC = function(x,y,xx,yy){ // this should be inLine but the angles were messing with my head
var a; // so moved it out here
a = Math.atan2(yy - y, xx - x); // for clarity and the health of my sanity
return (a + Math.PI * 2) % (Math.PI * 2); // Dont like negative angles.
}
// Create background image
var back = createImage(20, 20);
back.ctx.fillStyle = "#333";
back.ctx.fillRect(0, 0, 20, 20);
// Create background image
var backLight = createImage(20, 20);
backLight .ctx.fillStyle = "#ACD";
backLight .ctx.fillRect(0, 0, 20, 20);
// create circle image
var circle = createImage(64, 64);
circle.ctx.fillStyle = "red";
circle.ctx.beginPath();
circle.ctx.arc(32, 32, 30, 0, Math.PI * 2);
circle.ctx.fill();
// create some circles semi random
var circles = [];
circles.push({
x : 200 * Math.random(),
y : 200 * Math.random(),
scale : Math.random() * 0.8 + 0.3,
});
circles.push({
x : 200 * Math.random() + 200,
y : 200 * Math.random(),
scale : Math.random() * 0.8 + 0.3,
});
circles.push({
x : 200 * Math.random() + 200,
y : 200 * Math.random() + 200,
scale : Math.random() * 0.8 + 0.3,
});
circles.push({
x : 200 * Math.random(),
y : 200 * Math.random() + 200,
scale : Math.random() * 0.8 + 0.3,
});
// shadows on for each circle;
var shadows = [{},{},{},{}];
var update = function(){
var c, dir, dist, x, y, x1, y1, x2, y2, dir1, dir2, aAdd, i, j, s, s1 ,nextDir, rev, revId;
rev = false; // if inside a circle reverse the rendering.
// set up the gradient at the mouse pos
var g = ctx.createRadialGradient(mouse.x, mouse.y, canvas.width * 1.6, mouse.x, mouse.y, 2);
// do each circle and work out the two shadow lines coming from it.
for(var i = 0; i < circles.length; i++){
c = circles[i];
dir = directionC(mouse.x, mouse.y, c.x, c.y);
dist = Math.hypot(mouse.x - c.x, mouse.y - c.y);
// cludge factor. Could not be bother with the math as the light sourse nears an object
if(dist < 30* c.scale){
rev = true;
revId = i;
}
aAdd = (Math.PI / 2) * (0.5 / (dist - 30 * c.scale));
x1 = Math.cos(dir - (Math.PI / 2 + aAdd)) * 30 * c.scale;
y1 = Math.sin(dir - (Math.PI / 2 + aAdd)) * 30 * c.scale;
x2 = Math.cos(dir + (Math.PI / 2 + aAdd)) * 30 * c.scale;
y2 = Math.sin(dir + (Math.PI / 2 + aAdd)) * 30 * c.scale;
// direction of both shadow lines
dir1 = directionC(mouse.x, mouse.y, c.x + x1, c.y + y1);
dir2 = directionC(mouse.x, mouse.y, c.x + x2, c.y + y2);
// create the shadow object to hold details
shadows[i].dir = dir;
shadows[i].d1 = dir1;
if (dir2 < dir1) { // make sure second line is always greater
dir2 += Math.PI * 2;
}
shadows[i].d2 = dir2;
shadows[i].x1 = (c.x + x1); // set the shadow start pos
shadows[i].y1 = (c.y + y1);
shadows[i].x2 = (c.x + x2); // for both lines
shadows[i].y2 = (c.y + y2);
shadows[i].circle = c; // ref the circle
shadows[i].dist = dist; // set dist from light
shadows[i].branch1 = undefined; //.A very basic tree for shadows that interspet other object
shadows[i].branch2 = undefined; //
shadows[i].branch1Dist = undefined;
shadows[i].branch2Dist = undefined;
shadows[i].active = true; // false if the shadow is in a shadow
shadows[i].id = i;
}
shadows.sort(function(a,b){ // sort by distance from light
return a.dist - b.dist;
});
// cull shdows with in shadows and connect circles with joined shadows
for(i = 0; i < shadows.length; i++){
s = shadows[i];
for(j = i + 1; j < shadows.length; j++){
s1 = shadows[j];
if(s1.d1 > s.d1 && s1.d2 < s.d2){ // if shadow in side another
s1.active = false; // cull it
}else
if(s.d1 > s1.d1 && s.d1 < s1.d2){ // if shodow intercepts going twards light
s1.branch1 = s;
s.branch1Dist = s1.dist - s.dist;
s.active = false;
}else
if(s.d2 > s1.d1 && s.d2 < s1.d2){ // away from light
s.branch2 = s1;
s.branch2Dist = s1.dist - s.dist;
s1.active = false;
}
}
}
// keep it quick so not using filter
// filter culled shadows
var shadowsShort = [];
for (i = 0; i < shadows.length; i++) {
if ((shadows[i].active && !rev) || (rev && shadows[i].id === revId)) { // to much hard work makeng shadow from inside the circles. Was a good idea at the time. But this i just an example after all;
shadowsShort.push(shadows[i])
}
}
// sort shadows in clock wise render order
if(rev){
g.addColorStop(0.3, "rgba(210,210,210,0)");
g.addColorStop(0.6, "rgba(128,128,128,0.5)");
g.addColorStop(1, "rgba(0,0,0,0.9)");
shadowsShort.sort(function(a,b){
return b.dir - a.dir;
});
// clear by drawing background image.
ctx.drawImage(backLight, 0, 0, canvas.width, canvas.height);
}else{
g.addColorStop(0.3, "rgba(0,0,0,0)");
g.addColorStop(0.6, "rgba(128,128,128,0.5)");
g.addColorStop(1, "rgba(215,215,215,0.9)");
shadowsShort.sort(function(a,b){
return a.dir - b.dir;
});
// clear by drawing background image.
ctx.drawImage(back, 0, 0, canvas.width, canvas.height);
}
// begin drawin the light area
ctx.fillStyle = g; // set the gradient as the light
ctx.beginPath();
for(i = 0; i < shadowsShort.length; i++){ // for each shadow move in to the light across the circle and then back out away from the light
s = shadowsShort[i];
x = s.x1 + Math.cos(s.d1) * boundSize;
y = s.y1 + Math.sin(s.d1) * boundSize;
if (i === 0) { // if the start move to..
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
ctx.lineTo(s.x1, s.y1);
if (s.branch1 !== undefined) { // if braching. (NOTE this is not recursive. the correct solution would to math this a function and use recursion to climb in an out)
s = s.branch1;
x = s.x1 + Math.cos(s.d1) * s.branch1Dist;
y = s.y1 + Math.sin(s.d1) * s.branch1Dist;
ctx.lineTo(x, y);
ctx.lineTo(s.x1, s.y1);
}
ctx.lineTo(s.x2, s.y2);
if (s.branch2 !== undefined) {
x = s.x2 + Math.cos(s.d2) * s.branch2Dist;
y = s.y2 + Math.sin(s.d2) * s.branch2Dist;
ctx.lineTo(x, y);
s = s.branch2;
ctx.lineTo(s.x2, s.y2);
}
x = s.x2 + Math.cos(s.d2) * boundSize;
y = s.y2 + Math.sin(s.d2) * boundSize;
ctx.lineTo(x, y);
// now fill in the light between shadows
s1 = shadowsShort[(i + 1) % shadowsShort.length];
nextDir = s1.d1;
if(rev){
if (nextDir > s.d2) {
nextDir -= Math.PI * 2
}
}else{
if (nextDir < s.d2) {
nextDir += Math.PI * 2
}
}
x = Math.cos((nextDir+s.d2)/2) * boundSize + canvas.width / 2;
y = Math.sin((nextDir+s.d2)/2) * boundSize + canvas.height / 2;
ctx.lineTo(x, y);
}
// close the path.
ctx.closePath();
// set the comp to lighten or multiply
if(rev){
ctx.globalCompositeOperation ="multiply";
}else{
ctx.globalCompositeOperation ="lighter";
}
// draw the gradient
ctx.fill()
ctx.globalCompositeOperation ="source-over";
// draw the circles
for (i = 0; i < circles.length; i++) {
c = circles[i];
ctx.drawImage(circle, c.x - 32 * c.scale, c.y - 32 * c.scale, 64 * c.scale, 64 * c.scale);
}
// feed the herbervors.
window.requestAnimationFrame(update);
}
update();
.canC { width:400px; height:400px;}
<canvas class="canC" id="canV" width=400 height=400></canvas>

Categories

Resources