2
\$\begingroup\$

I wrote a small program in Javascript that, when given a certain number of sides, will draw a polygon with that number of sides (i.e. 3 for a triangle, 5 for a pentagon, 8 for octagon, etc.).

The program is set on an interval that will, every second, increment the number of sides, redraw the canvas, and then draw the new polygon with the new number of sides.

The code works as intended, though I'm not entirely sure if it's working most efficiently. Was wondering if there were better ways to manage variables, draw to the screen, etc.

Here is the jsFiddle

---HTML---

<html>
<head>
    <title>Circles</title>
    <style>
        body * {
            margin: 5px;
        }

        canvas {
            border: 2px solid black;
        }
    </style>
</head>
<body>
    <p>Sides: <span id='noSides'></span></p>
    <input type='button' class='dir' id='backward' value='BACKWARD' />
    <input type='button' class='dir' id='forward' value='FOWARD' />
    <input type='button' id='start' value='Start' />
    <input type='button' id='stop' value='Stop' />
    <input type='button' id='reset' value='Reset' />
    <br />
    <canvas id='canvas' width='500' height='500'></canvas>
    <script type="text/javascript" src="index.js"></script>
</body>
</html>

---Javascript---

class Vector {
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}

class Circle {
  constructor(x, y, radius) {
    this.center = new Vector(x, y);
    this.radius = radius;
  }
}

const input = document.getElementById("sides");
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
const center = {
  x: canvas.width / 2,
  y: canvas.height / 2
};
const rotateAround = 3 * Math.PI / 2;

drawCanvas();

let reverse = false;
let sides = 0;
let radius = center.y * 0.8;
let shapes = [];

function reset() {
  context.rotate(-(3 * Math.PI / 2));
  context.translate(-center.y, -center.y);
  drawCanvas();
  context.translate(center.y, center.y);
  context.rotate(3 * Math.PI / 2);
}

function draw() {
  reset();
  if (reverse) {
    if (sides != 0) {
      sides -= 1;
    }
  } else {
    sides += 1;
  }
  document.getElementById('noSides').innerText = sides;
  if (sides > 0) {
    shapes = [];
    for (let i = 0; i < sides; i++) {
      shapes.push(new Circle(0, 0, 20));
    }

    for (let i = 0; i < shapes.length; i++) {
      let x1 = radius * Math.cos(Math.PI * (i / (shapes.length / 2)));
      let y1 = radius * Math.sin(Math.PI * (i / (shapes.length / 2)));

      shapes[i].center.x = x1;
      shapes[i].center.y = y1;
    }

    for (let i = 0; i < shapes.length; i++) {
      for (let j = i + 1; j < shapes.length; j++) {
        context.beginPath();
        context.lineWidth = 0.5;
        context.moveTo(shapes[i].center.x, shapes[i].center.y);
        context.lineTo(shapes[j].center.x, shapes[j].center.y);
        context.stroke();
        context.closePath();
      }
    }
  }
}

function drawCanvas() {
  context.fillStyle = "lightgrey";
  context.fillRect(0, 0, canvas.width, canvas.height);
}

let interval;
let started = false;

function update() {
  context.translate(center.y, center.y);
  context.rotate(rotateAround);

  drawCanvas();
}

update();

document.getElementById('start').onclick = function() {
  if (started === false) {
    started = true;
    interval = setInterval(draw, 1000);
  }
}

document.getElementById('stop').onclick = function() {
  clearInterval(interval);
  started = false;
}

document.getElementById('forward').onclick = function() {
  reverse = false;
}

document.getElementById('backward').onclick = function() {
  reverse = true;
}

document.getElementById('reset').onclick = function() {
    clearInterval(interval);
    reset();
    sides = 0;
}
\$\endgroup\$
1
\$\begingroup\$

closePath is not complement of beginPath

You are using ctx.closePath() incorrectly. ctx.closePath() is a path function, similar to ctx.moveTo(x, y). It creates a line from the current position to previous ctx.moveTo(x, y). It is not the complement of ctx.beginPath(). There is no need to use it in this case.

Keep it Simple.

Circle & Vector

You have defined Circle, and Vector objects.

When you create the circle you pass a x, y coordinate that you use to create a Vector and assign to circle.center.

Rather pass a Vector to the Circle. eg

constructor(center, radius) {
    this.center = center;
    ...

circle = new Circle(new Vector(x, y), 10);

Just data stores

Both the Circle and Vector objects are just data stores, they do nothing else but hold some numbers. You can simplify both to much simpler forms

const Vector = (x = 0, y = 0) = ({x, y});
const Circle = (center = Vector(), radius = 20) = ({center, radius});

This saves you 11 lines of code, without changing the behavior.

Irrelevant data.

The function draw does some odd things. You create circles yet only ever use centers. As code in Code Review questions is considered to be complete. Using circle is superfluous, just use the vectors.

Cache data

Your code recalculates all the points each time you redraw, this is a waste of time. You can either

  • Store all the points for each shape and use existing path points or create them if they have not yet been created.
  • Store the 2D path calls as a Path2D.

2D rendering

When rendering to 2D canvas context always group path calls that use the same state (color, line width, etc) should be inside the same beginPath. beginPath and stroke require GPU state changes, which is where the rendering bottle neck is, on low end devices this bottle neck is very small making even simple renders slow due to poor state change calls.

The draw call has too many roles. Setting number of sides, displaying the number of sides, generating the points, all should be part of other functions. draw should only draw nothing more. Create the shape in another function and pass that data to the draw call. Displaying side count, and changing side shape can be done at a higher level.

Avoid setInterval

You should always avoid setInterval as it often makes managing timers more complex. Use setTimeout as there is no need to hold handles as it can be stopped via semaphore.

Avoid repetition

A lot of the code has very similar parts. Good code has very little repetition. This is done by using functions to do repetitive tasks.

More often than not the repetitive code will be such over many project. Writing functions for repeated code also creates a resource that can be moved from project to project saving you hours of time typing out the same thing over and over.

JavaScript supports modules that makes code reuse easy. The rewrite shows some examples, DOM and Maths utils that would normally be in modules and imported. See rewrite comments

UI

Make the UI smart, buttons that can not or should not be used should be disabled. The disabled status of buttons also give the user feedback as to the state of the app.

The rewrite maps the buttons to an object called actions which is in effect the interface to the app. Not only can The user call actions, the code also uses actions to reset, and stop (when sides < 1)

Rewrite

At the top & bottom there is some commented code that shows how the utilities would be imported. The DOM utils are medium level code, if you have questions use the CR comments.

"use strict"; // not required when using modules
setTimeout(() => { // timeout only for CR snippet so that code is parse before execution
                   // Note I did not indent timeout as there is not much room for snipper 

/* To use modules the imports would be. See bottom of snippet */
// import {query, listener, elements} from "./DOM/utils.jsm";
// import {constants} from "./Maths/constants.jsm";
// import {Vec2} from "./Maths/constants.jsm";

const ctx = query("#canvas").getContext("2d");
const center = Vector(ctx.canvas.width / 2, ctx.canvas.height / 2);

const rotate = - Math.PI / 2;
const shapes = [];
var sidesStep = 1;
var sides = 1;
var radius = center.y * 0.8;
var started = false;

const actions = {
    updateBtnStates() {
        btns.start.disabled = started;
        btns.stop.disabled = !started;
        btns.forward.disabled = sidesStep > 0;
        btns.backward.disabled = sidesStep < 0 && sides > 1;  
    },
    start() {
        if (!started) {
            started = true;
            update();
        }
        actions.updateBtnStates();
    },
    stop() {         
        started = false;
        actions.updateBtnStates();
    },
    reverse() { 
        sides -= sidesStep;
        sidesStep = -sidesStep; 
        actions.updateBtnStates();
    },
    forward() { actions.reverse() },
    backward() { actions.reverse() },
    reset() { 
        sidesStep = 1;
        sides = 1;
        actions.stop();
        update(true);
    },    
}
const btns = elements({
    start: "#startBtn",
    stop: "#stopBtn",
    forward: "#forwardBtn",
    backward: "#backwardBtn",
    reset: "#resetBtn",
});
Object.entries(btns).forEach(([name, el]) => listener(el, "click", actions[name]));

actions.reset();

function createShape(sides, r = radius) {
    const shape = [];
    var i = sides;
    while (i-- > 0) {        
        const ang = (i / sides) * TAU;
        shape.push(Vector(Math.cos(ang) * r, Math.sin(ang) * r));
    }
    return shape;
}
    
function pathShape(points) {
    const path = new Path2D();
    var i = sides, j, p1, p2;
    while (i-- > 0) {        
        j = i;
        p1 = points[i];
        while (j-- > 0) {
            p2 = points[j];
            path.moveTo(p1.x, p1.y);
            path.lineTo(p2.x, p2.y);
        }
    }
    return path;
}
function drawShape(ctx, path, pos, rotate, lineWidth = 0.5, col = "#000") {
    ctx.setTransform(1, 0, 0, 1, pos.x, pos.y);
    ctx.rotate(rotate);
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = col;
    ctx.stroke(path);
    ctx.setTransform(1, 0, 0, 1, 0, 0); // default
}
function clearCanvas(color = "lightgrey") {
    ctx.fillStyle = "lightgrey";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}    

function update(once = false) {
    if (started || once) { 
        !once && setTimeout(update, 1000);
        if (!shapes[sides]) { shapes[sides] = pathShape(createShape(sides)) }
        clearCanvas();
        drawShape(ctx, shapes[sides], center, rotate);
        numSidesEl.textContent = sides;
        sides += sidesStep;
        if (sides < 1) { 
            sides = 1;
            sidesStep = 1;
            actions.stop();
        }
    }
}
},0); // End of timeout



/* The following would be a  Module file /DOM/utils.jsm */
const qryEl = qryEl => typeof qryEl === "string" ? query(qryEl) : qryEl;
const query = (qStr, el = document) => el.querySelector(qStr);
const listener = (eq, name, call, opt = {}) => ((eq = qryEl(eq)).addEventListener(name, call, opt), eq);
const elements = obj => (Object.entries(obj).forEach(([n, q]) => obj[n] = query(q)), obj);
// export {query, listener, elements};

/* The following would be a Module file /Maths/constants.jsm  */    
// const constants.TAU = Math.PI * 2;
// export {constants};

/* The following would be a Module file  /Maths/Vec2.jsm */ 
// const Vec2 = (x = 0, y = 0) => ({x, y});
// export {Vec2};

/* To use modules the imports would be */
// import {query, listener, elements} from "./DOM/utils.jsm";
// import {constants} from "./Maths/constants.jsm";
// import {Vec2} from "./Maths/constants.jsm";


/* Replacements for imports constants, Vec2 */
const TAU = Math.PI * 2;
const Vector = (x = 0, y = 0) => ({x, y});
<p>Sides: <span id='numSidesEl'></span></p>
    <button class='dir' id='backwardBtn'>BACKWARD</button>
    <button class='dir' id='forwardBtn'>FOWARD</button>
    <button id='startBtn'>Start</button>
    <button id='stopBtn'>Stop</button>
    <button id='resetBtn'>Reset</button>
    <br>
    <canvas id='canvas' width='500' height='500'></canvas>

Warning Let

let in global scope is very bad.

General rule for let is. Only use it inside { blocks }, never use it in global scope.

Sorry OP the following is a little overly detailed and is in lue of the inevitable comments defending let.

A <script> is NOT a block, though obvious (no {}), the general intuition is that it is (who uses global let and puts {} around a script?)

The counter intuitive behavior of a global let.

For example a script...

<script > 
    let b = {log() { console.log("Hi") }}; // b can not be trusted
    setTimeout(() => b.log(), 200);

</script> 

...in 200ms expect "Hi" in console.

Another (diligent) script will crash

<script>
    "use strict";  // doin the right thing
     let b = 5;    // throws "b already declared" What the!!!!
</script> 

Another (sloppy) script can reuse b

<script>  b = 5 </script> 

Not crashing this script, but when the timeout (first script) callback calls b.log which is no longer a function it will throw an error.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.