How to simplify this code? - javascript

I’ve just started learning canvas and have tried so far a couple of exercises, but my code is always way too long and most probably unnecessarily complicated. I have the following code of a four leaf clover drawing and would like to know how to simplify it. Any suggestions?
Thank you in advance!
var clover = document.getElementById("clover");
var ctx = clover.getContext("2d");
//style:
ctx.strokeStyle = "#006600";
ctx.lineWidth = 0.3;
ctx.beginPath();
ctx.moveTo(115,80);
ctx.bezierCurveTo(20,100,200,100,235,135);
ctx.stroke();
//First leaf:
ctx.strokeStyle = "black";
ctx.lineWidth = 0.8;
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(55,70);
ctx.quadraticCurveTo(20,100,115,80);
ctx.stroke();
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(55,70);
ctx.quadraticCurveTo(40,30,115,80);
ctx.stroke();
ctx.closePath();
ctx.fill();
// Second leaf:
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(115,80);
ctx.quadraticCurveTo(80,20,130,50);
ctx.stroke();
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(115,80);
ctx.quadraticCurveTo(200,40,130,50);
ctx.stroke();
ctx.closePath();
ctx.fill();
// Third leaf:
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(115,80);
ctx.quadraticCurveTo(235,60,185,85);
ctx.stroke();
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(115,80);
ctx.quadraticCurveTo(190,115,185,85);
ctx.stroke();
ctx.closePath();
ctx.fill();
// Fourth leaf:
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(115,80);
ctx.quadraticCurveTo(180,135,110,115);
ctx.stroke();
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#7BA32D";
ctx.beginPath();
ctx.moveTo(115,80);
ctx.quadraticCurveTo(60,130,110,115);
ctx.stroke();
ctx.closePath();
ctx.fill();
// lines on the leaves:
ctx.strokeStyle = "#006600";
ctx.lineWidth = 0.3;
ctx.beginPath();
ctx.moveTo(115, 80);
ctx.lineTo(65, 71);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(115, 80);
ctx.lineTo(127, 55);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(115, 80);
ctx.lineTo(175, 85);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(115, 80);
ctx.lineTo(110, 110);
ctx.stroke();
ctx.closePath();

Write one or more functions that do the things you are repeating. Figure out what parameters they need to take to be able to handle the slightly different cases. Then call the functions with the right parameters. For example, your code of the form
ctx.beginPath();
ctx.moveTo(115, 80);
ctx.lineTo(110, 110);
ctx.stroke();
ctx.closePath();
would be written as the function
function line(x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.closePath();
}
and called as
line(115, 80, 110, 110);

Expressive language
Javascript is known for its expressive flexibility and can allow coding styles not found in most other languages.
Use Path2D
The first thought for your code was to use the Path2D object to define the draw commands. It uses similar syntax to SVG path command.
But why not create your own command list.
Named styles
First let's find all the styles and name them
var styles = {
dGreen : {
strokeStyle : "#006600",
lineWidth : 0.3,
},
black : {
strokeStyle : "black",
fillStyle : "#7BA32D",
lineWidth : 0.8,
}
}
Now you can use the named style and if you name the properties the same as used by the 2D API it is very easy to set a style
function setStyle(style){
Object.keys(style).forEach(prop => ctx[prop] = style[prop]);
},
setStyle(styles.black); // sets the style black
If you want to use properties you did not think of at the time you dont have to code it just set the property and you are done
styles.black.lineJoin = "round";
setStyle(styles.black); // sets the style black
Custom command list.
For the drawing commands you are doing the same set of operations many times. In SVG the commands are single characters "M" for moveto followed by x, y coordinates.
We can do the same. The commands will be a string, separated by "," which is then split into an array. You shift from the array each command as needed.
First the command object that has a function for each command. In this case M for moveTo and "L" for lineTo. It takes the array which it uses to get the coordinates from.
var commands = {
M(array){
ctx.moveTo(array.shift(),array.shift());
},
L(array){
ctx.lineTo(array.shift(),array.shift());
}
}
Then Define a path with our new commands move to 10,10 then line to 100,100
var path = "M,10,10,L,100,100";
Now we just need to parse and interpret the path
function drawPath(path){
// split the command string into parts
var commandList = path.split(",");
// while there are commands
while(commandList.length > 0){
// use the next command to index the command
// and call the function it names passing the command list so
// it can get the data it needs
commands[commandList.shift()](commandList);
} // do that until there is nothing on the command list
}
Now all you need to do is supply the command strings to draw what you need. Because you can define the commands you can create commands as complex or simple as you want.
A little more complex
The following is the command functions and draw function I created to draw your image
// define draw commands
var drawFuncs = {
getN(a,count){ return a.splice(0,count); }, // gets values from array
M(a){ ctx.moveTo(...this.getN(a,2)); }, // move to
C(a){ ctx.bezierCurveTo(...this.getN(a,6)); }, // bezier curve
Q(a){ ctx.quadraticCurveTo(...this.getN(a,4)); },// quad curve
S(){ ctx.stroke(); }, // stroke
P(){ ctx.closePath(); }, // close path
F(){ ctx.fill(); }, // fill
B(){ ctx.beginPath(); }, // begin path
l(a) { // line segment
ctx.beginPath();
ctx.moveTo(...this.getN(a,2));
ctx.lineTo(...this.getN(a,2));
ctx.stroke();
},
St(a){ // set style
var style = styles[a.shift()];
Object.keys(style).forEach(prop=>ctx[prop] = style[prop]);
},
}
// Takes command string and draws what is in it
function draw(shape){
var a = shape.split(",");
while(a.length > 0){
drawFuncs[a.shift()](a);
}
}
You can put that code into a separate library and forget about it, while you concentrate on the rendering
Now you can render via your own custom made declarative language
Define the styles
// define named styles
var styles = {
dGreen : {
strokeStyle : "#006600",
lineWidth : 0.3,
},
black : {
strokeStyle : "black",
fillStyle : "#7BA32D",
lineWidth : 0.8,
}
}
Create command list and draw
draw([
"St,dGreen,B,M,115,80,C,20,100,200,100,235,135,S",
"St,black,B,M,55,70,Q,20,100,115,80",
"M,55,70,Q,40,30,115,80",
"M,115,80,Q,80,20,130,50",
"M,115,80,Q,200,40,130,50",
"M,115,80,Q,235,60,185,85",
"M,115,80,Q,190,115,185,85",
"M,115,80,Q,180,135,110,115",
"M,115,80,Q,60,130,110,115,S,P,F",
"St,dGreen",
"l,115,80,65,71",
"l,115,80,127,55",
"l,115,80,175,85",
"l,115,80,110,110",
].join(","));
Demo
Note: All the code is written in ES6 and will need Babel (or similar) to work on legacy browsers.
// define draw commands
var drawFuncs = {
getN(a,count){ return a.splice(0,count); }, // gets values from array
M(a){ ctx.moveTo(...this.getN(a,2)); }, // move to
C(a){ ctx.bezierCurveTo(...this.getN(a,6)); }, // bezier curve
Q(a){ ctx.quadraticCurveTo(...this.getN(a,4)); },// quad curve
S(){ ctx.stroke(); }, // stroke
P(){ ctx.closePath(); }, // close path
F(){ ctx.fill(); }, // fill
B(){ ctx.beginPath(); }, // begin path
l(a) { // line segment
ctx.beginPath();
ctx.moveTo(...this.getN(a,2));
ctx.lineTo(...this.getN(a,2));
ctx.stroke();
},
St(a){ // set style
var style = styles[a.shift()];
Object.keys(style).forEach(prop=>ctx[prop] = style[prop]);
},
}
// Takes command string and draws what is in it
function draw(shape){
var a = shape.split(",");
while(a.length > 0){
drawFuncs[a.shift()](a);
}
}
// create canvas and add to DOM
var canvas = document.createElement("canvas");
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
// define named styles
var styles = {
dGreen : {
strokeStyle : "#006600",
lineWidth : 0.3,
},
black : {
strokeStyle : "black",
fillStyle : "#7BA32D",
lineWidth : 0.8,
}
}
// draw it all
ctx.clearRect(0,0,canvas.width,canvas.height);
draw([
"St,dGreen,B,M,115,80,C,20,100,200,100,235,135,S",
"St,black,B,M,55,70,Q,20,100,115,80",
"M,55,70,Q,40,30,115,80",
"M,115,80,Q,80,20,130,50",
"M,115,80,Q,200,40,130,50",
"M,115,80,Q,235,60,185,85",
"M,115,80,Q,190,115,185,85",
"M,115,80,Q,180,135,110,115",
"M,115,80,Q,60,130,110,115,S,P,F",
"St,dGreen",
"l,115,80,65, 71",
"l,115,80,127, 55",
"l,115,80,175, 85",
"l,115,80,110, 110",
].join(","));

Related

Canvas object: semi circle leaking colors in JavaScript

Hi I want to understand the essentials of opening and closing function of canvas .
What I noticed was when I used only one opening and closing of function of canvas for 2 separate object a rectangle and a semi circle. The colour started leaking as shown:
What I expect is as below figure 2. With one opening and closing canvas function:
The code is mentioned below
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.rect(30, 30, 50, 60);
ctx.fillStyle = "#FF0000";
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.arc(350,200,20,0,Math.PI*2,false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath();
The function ctx.beginPath create a new path object by deleting all existing paths points and strokes
The function ctx.closePath creates a line from the last point added to the current path to the previous ctx.moveTo or ctx.beginPath it is unrelated to the ctx.beginPath function and does nothing if followed by ctx.beginPath
The function ctx.closePath is the same as ctx.lineTo in the following
ctx.beginPath();
ctx.moveTo(100,100);
ctx.lineTo(200,200);
ctx.lineTo(100,200);
ctx.lineTo(100,100); // back to start
Same as
ctx.beginPath();
ctx.moveTo(100,100);
ctx.lineTo(200,200);
ctx.lineTo(100,200);
ctx.closePath(); // does the same as ctx.lineTo(100,100); // back to start
Some comments in your code.
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.rect(30, 30, 50, 60);
ctx.fillStyle = "#FF0000";
ctx.fill();
ctx.closePath(); // <<< Not needed as you have already called fill
ctx.beginPath(); // <<< this deletes the previous path
ctx.arc(350,200,20,0,Math.PI*2,false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath(); // <<< not needed

Prevent canvas element from manipulating others depending on order

I have been experimenting with canvas and things were going pretty well. However when it came time to rotate an object I had the hardest time getting it to rotate. When I finally did I found that depending on the order the objects being initiated it. The object with the rotation attributes would get passed on to objects being called after it.
All my objects were put into their own functions, and then called by a draw function. I am curious if this is always true and I have to be conscious of my objects order of initiation or if there is something I am missing.
Below if you move Obj1 and Obj2 before or after each other you will see the results are different. My goal is to have Obj1 before Obj2 and only have Obj1 rotated.
window.onload = function(){
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
x = 470,
y = 260;
canvas.width = 500;
canvas.height = 500;
function Obj1(){
ctx.beginPath();
ctx.translate(100,0);
ctx.rotate(20 * Math.PI / 180);
ctx.fillRect(x, y + 10, 20, 45);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.closePath();
}
function Obj2(){
ctx.beginPath();
ctx.rect(0,300,100, 200);
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
}
function draw(){
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
Obj1();
Obj2();
}
draw();
}
<canvas id="canvas"></canvas>
You need to reset the rotation matrix after rotating the object
Whats happening is that the transforms are cascading .
Rendering happens as a stack not a per element basis .
The order of operations is
TRANSFORM
DRAW
RESET
DRAW
This is also a recursive process .
You can say rotate the world scene / view port and also individual object .
TRANSFORM
TRANSFORM
DRAW
RESET
TRANSFORM
DRAW
RESET
RESET
If you post a simple code example I can review it for you
Have a look at
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Transformations
function Obj1(){
ctx.save();
ctx.beginPath();
ctx.translate(100,0);
ctx.rotate(20 * Math.PI / 180);
ctx.fillRect(x, y + 10, 20, 45);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.closePath();
ctx.restore();
}
I believe off the top of my head .

CanvasRenderingContext2D.fill() does not seem to work

I'm making a simply polygon draw method in HTML5 canvas. It will successfully outline the shape, but it will not fill it, even though it has been instructed to:
function PhysicsObj(Position /*Vector*/,Vertices /*Vector Array*/)
{
this.Position = Position
this.Vertices = Vertices
this.Velocity = new Vector(0,0);
this.Colour = "rgb(100,100,100)";
this.draw = function()
{
ctx.beginPath();
for(var point=0; point<Vertices.length-1; point++)
{
ctx.moveTo(Vertices[point].X+Position.X,Vertices[point].Y+Position.Y);
ctx.lineTo(Vertices[point+1].X+Position.X,Vertices[point+1].Y+Position.Y);
}
ctx.moveTo(Vertices[point].X+Position.X,Vertices[point].Y+Position.Y);
ctx.lineTo(Vertices[0].X+Position.X,Vertices[0].Y+Position.Y);
ctx.closePath();
ctx.fillStyle = this.Colour;
ctx.fill();
ctx.strokeStyle = this.Colour;
ctx.stroke();
}
}
var Polygon = new PhysicsObj(new Vector(100,100),[new Vector(50,50),new Vector(-50,50), new Vector(0,125)]);
Polygon.draw();
The method simply takes several vertices, and connects them into a path. It simply will not fill; I cannot figure out how to use the fill method.
Eliminate the extra moveTo commands:
var PositionX=50;
var PositionY=50;
var Vertices=[
{X:10,Y:10},
{X:100,Y:10},
{X:50,Y:50}
];
ctx.beginPath();
ctx.moveTo(Vertices[0].X+PositionX,Vertices[0].Y+PositionY);
for(var point=1; point<Vertices.length; point++)
{
ctx.lineTo(Vertices[point].X+PositionX,Vertices[point].Y+PositionY);
}
ctx.lineTo(Vertices[0].X+PositionX,Vertices[0].Y+PositionY);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
ctx.strokeStyle = "black";
ctx.stroke();

Draw overlap of 3 circles on a canvas

I need to draw the following image on an HTML5 canvas without a temporary canvas:
With a temporary canvas it's easy, because I can handle the overlaps independently like you can see here:
Check this jsFiddle.
// init
var canvas = document.getElementById('canvas');
var tempCanvas = document.getElementById('tempCanvas');
var ctx = canvas.getContext('2d');
var tempCtx = tempCanvas.getContext('2d');
// draw circle function
var drawCircle = function( c, color ) {
ctx.beginPath();
ctx.arc( c.x, c.y, 50, 0, 2 * Math.PI, false );
ctx.fillStyle = color;
ctx.fill();
}
// draw overlap function
var drawOverlap = function( c1, c2, color ) {
tempCtx.clearRect( 0, 0, 300, 300 );
// first circle
tempCtx.globalCompositeOperation = 'source-over';
tempCtx.beginPath();
tempCtx.arc( c1.x, c1.y, 50, 0, 2 * Math.PI, false );
tempCtx.fillStyle = color;
tempCtx.fill();
// second circle
tempCtx.globalCompositeOperation = 'destination-in';
tempCtx.beginPath();
tempCtx.arc( c2.x, c2.y, 50, 0, 2 * Math.PI, false );
tempCtx.fill();
// draw on main canvas
ctx.drawImage( tempCanvas, 0, 0 );
}
// circle objects
var c1 = { x:100, y: 200 };
var c2 = { x:180, y: 200 };
var c3 = { x:140, y: 140 };
// draw background
ctx.beginPath();
ctx.rect( 0, 0, 300, 300 );
ctx.fillStyle = 'black';
ctx.fill();
// draw circles
drawCircle( c1, 'grey' );
drawCircle( c2, 'white' );
drawCircle( c3, 'white' );
// draw overlaps
drawOverlap( c1, c2, 'red' );
drawOverlap( c1, c3, 'blue' );
drawOverlap( c2, c3, 'blue' );
Do you know a way to draw this without a second canvas? Thanks a lot.
EDIT:
I solved it thanks to #user13500. There are still ugly borders but it's very fast:
Check this jsFiddle
Ai, my head hurts.
Not a quite a solution. Not one at all I guess. But if we do not care about the background we have this:
Fiddle Updated to new version as below.
Giving us this:
Tried to solve this using only globalCompositeOperation ;-P
Edit:
OK. Moving away from it for a few minutes and here we go again. This time with this as result. There is still the issue with stray lines around the circle red:
Though it might not be what you are after, it is here ;-) #markE is in another realm when it comes to authority on the subject.
Fiddle.
Code:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var ct = [
'source-over', // 0
'source-in', // 1
'source-out', // 2
'source-atop', // 3
'destination-over', // 4
'destination-in', // 5
'destination-out', // 6
'destination-atop', // 7
'lighter', // 8
'darker', // 9
'copy', // 10
'xor' // 11
];
ctx.beginPath();
ctx.globalCompositeOperation = ct[0];
ctx.fillStyle = "#888";
ctx.arc(100,200,50,0,2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.globalCompositeOperation = ct[6];
ctx.fillStyle = "#fff";
ctx.arc(180,200,50,0,2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.globalCompositeOperation = ct[11];
ctx.fillStyle = "#f00";
ctx.arc(100,200,50,0,2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.globalCompositeOperation = ct[4];
ctx.fillStyle = "#888";
ctx.arc(100,200,50,0,2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.globalCompositeOperation = ct[9];
ctx.fillStyle = "#fff";
ctx.arc(180,200,50,0,2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.globalCompositeOperation = ct[11];
ctx.fillStyle = "#fff";
ctx.arc(140,140,50,0,2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.globalCompositeOperation = ct[4];
ctx.fillStyle = "#00f";
ctx.arc(140,140,50,0,2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.globalCompositeOperation = ct[4];
ctx.rect( 0, 0, 300, 300 );
ctx.fillStyle = '#000';
ctx.fill();
You can do this without a temporary canvas using clipping regions -- context.clip()
If you need, I can code the solution but since you know how to do compositing you can probably figure it out quickly ;)
But more importantly! ...
Why are you disabling your tool choices by not using a temporary canvas?
You could do document.createElement("canvas") to create a temporary canvas that's not even visible on the screen.

Unable to apply fill for path drawn rectangle

I am trying to form a cell object using paths in HTML5 Canvas. But I am unable to apply fill for the path I made. Tried out several things but not able to solve it.
class Cell {
constructor({ i, j }) {
this.i = i;
this.j = j;
this.visited = false;
this.walls = [true, true, true, true]; // top right bottom left
this.show = canvasContext => this._show(canvasContext);
}
_show(ctx) {
const x = this.j * cellSize;
const y = this.i * cellSize;
if (!this.visited) {
ctx.fillStyle = "green";
} else {
ctx.fillStyle = "yellow";
}
// ctx.fillStyle = "green";
ctx.strokeStyle = "black";
ctx.beginPath();
if (this.walls[0]) {
ctx.moveTo(x, y);
ctx.lineTo(x + cellSize, y); // top
}
if (this.walls[3]) {
ctx.moveTo(x, y);
ctx.lineTo(x, y + cellSize); // left
}
if (this.walls[1]) {
ctx.moveTo(x + cellSize, y);
ctx.lineTo(x + cellSize, y + cellSize); // right
}
if (this.walls[2]) {
ctx.moveTo(x, y + cellSize);
ctx.lineTo(x + cellSize, y + cellSize); // bottom
}
ctx.fill();
ctx.stroke();
// ctx.fill();
}
}
See the Pen Maze Generator by Dhanushu (#dhanushuUzumaki) on CodePen.
The problem appears to be that you aren't creating a closed shape, you are just creating a series of lines that don't form a shape.
This is a closed path. If you took a pencil and drew the points, you'd see that it would be closed.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#F00"; // red
ctx.strokeStyle = "#0F0"; // green
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(100, 50);
ctx.lineTo(100, 100);
ctx.lineTo(50, 100);
ctx.closePath();
ctx.fill();
ctx.stroke();
canvas {
border: 1px solid #000;
}
<canvas />
This is an open path, who's stroke looks the same:
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#F00"; // red
ctx.strokeStyle = "#0F0"; // green
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(100, 50);
ctx.moveTo(100, 50);
ctx.lineTo(100, 100);
ctx.moveTo(100, 100);
ctx.lineTo(50, 100);
ctx.moveTo(50, 100);
ctx.lineTo(50, 50);
ctx.closePath();
ctx.fill();
ctx.stroke();
canvas {
border: 1px solid #000;
}
<canvas />
By calling moveTo() you are essentially picking the pencil up, and breaking the shape. There is nothing to fill, because it's not one contiguous path. It's just 4 separate lines that are close to one another.
For your particular problem, I see two ways to solve this. The first would be to stop using moveTo() so often and just draw the lines. However, for your particular problem, that could be tricky.
Instead, what you might draw to do is draw individual rectangles to fill in each block of your grid. It seems like that logic might be easier to work out.

Categories

Resources