Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change dimension of canvas depending on selected option

I was working on canvas and came across the Idea of changing dimensions of the cube. So, by using HTML5 Canvas I made up this cube which has two squares joined by the lines to make it look like a cube.

What I want is when I select a cube type from select the cube should automatically change itself depending on the length and width of the selected option. The height remains constant. Like if the I select the cube of 5x5 which is by default a cube but when the I select the option of 5x10 the width(front) should not be changed but the length(side) of the cube should expand, and vice versa if I select 10x5 my max option is 25x15. As you can see the canvas I created below is in pixels, first I need to convert these pixels into centimeters(cm) then centimeters to cubic meters.

The whole cube should be aligned in the fixed canvas area specified.

Here is fiddle

var canvas = document.querySelector('canvas');

canvas.width = 500;
canvas.height = 300;

var contxt = canvas.getContext('2d');

//squares
/*
contxt.fillRect(x, y, widht, height);
*/
contxt.strokeStyle = 'grey';
var fillRect = false;
contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
contxt.rect(80, 80, 100, 100);
contxt.rect(120, 40, 100, 100);
if (fillRect) {
  contxt.fill();
}
contxt.stroke();

/*Lines
contxt.beginPath();
contxt.moveTo(x, y);
contxt.lineTo(300, 100);
*/
contxt.beginPath();

contxt.moveTo(80, 80);
contxt.lineTo(120, 40);

contxt.moveTo(180, 80);
contxt.lineTo(220, 40);

contxt.moveTo(80, 180);
contxt.lineTo(120, 140);

contxt.moveTo(180, 180);
contxt.lineTo(220, 140);

contxt.stroke();
canvas {
  border: 1px solid #000;
}
select {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<select>
  <option>5x5</option>
  <option>5x10</option>
  <option>10x5</option>
</select>

<canvas></canvas>
like image 494
Mohammed Wahed Khan Avatar asked May 09 '18 12:05

Mohammed Wahed Khan


2 Answers

Drawing the cube:

To generate a dynamic cube you would have to listen to an onChange event on the <select> element. Every time the selected option changes you would want to redraw your cube.

To redraw the cube you need to create a renderCube function which should take the new dimensions of the cube and as specified an offset for positioning. In this function you have to clear the previously drawn cube and redraw the new one with the given dimensions and offset.

Adding a transition effect:

As you can not apply css transitions to canvas elements you have to implement the transition yourself. You would have to create an animation function which would calculate the dimensions of the cube in the transition phase and rerender it to the screen on each frame.

An implementation of the resizable cube with a transition effect would be:
(if you prefer here is a fiddle too)
(if you do not need the transition effect check the fiddle before it has been implemented)

var canvas = document.querySelector('canvas');
canvas.width = 320;
canvas.height = 150;
var contxt = canvas.getContext('2d');

var currentHeight = 0, currentWidth = 0, currentDepth = 0, animationId = 0;

function renderCube(height, width, depth, offsetX, offsetY) {
  currentHeight = height;
  currentWidth = width;
  currentDepth = depth;

  // Clear possible existing cube
  contxt.clearRect(0, 0, canvas.width, canvas.height);
  contxt.beginPath();

  // Calculate depth, width and height based on given input
  depth = (depth * 10 * 0.8) / 2;
  width = width * 10;
  height = height * 10;

  // Draw 2 squares to the canvas
  contxt.strokeStyle = 'grey';
  var fillRect = false;
  contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
  contxt.rect(offsetX, offsetY, width, height);
  contxt.rect(offsetX + depth, offsetY - depth, width, height);
  if (fillRect) {
    contxt.fill();
  }
  contxt.stroke();


  // An array which specifies where to draw the depth lines between the 2 rects
  // The offset will be applied while drawing the lines
  var depthLineCoordinates = [
    // posX, posY, posX2, posY2
    [0, 0, depth, -depth],
    [width, 0, width + depth, -depth],
    [0, height, depth, height - depth],
    [width, height, width + depth, height - depth]
  ];

  // Draw the depth lines to the canvas
  depthLineCoordinates.forEach(function(element) {
    contxt.moveTo(offsetX + element[0], offsetY + element[1]);
    contxt.lineTo(offsetX + element[2], offsetY + element[3]);
  });
  contxt.stroke();
}

// As requested by OP an example of a transition to the cube
// The transitionDuration may be a double which specifies the transition duration in seconds
function renderCubeWithTransistion(height, width, depth, offsetX, offsetY, transitionDuration) {
  var fps = 60;
  var then = Date.now();
  var startTime = then;
  var finished = false;

  var heightDifference = (height - currentHeight);
  var widthDifference = (width - currentWidth);
  var depthDifference = (depth - currentDepth);

  // Get an "id" for the current animation to prevent multiple animations from running at the same time.
  // Only the last recently started animation will be executed.
  // If a new one should be run, the last one will get aborted.
  var transitionStartMillis = (new Date()).getMilliseconds();
  animationId = transitionStartMillis;

  function animate() {
    // Do not continue rendering the current animation if a new one has been started
    if (transitionStartMillis != animationId) return;
    // request another frame if animation has not been finished
    if (!finished) requestAnimationFrame(animate);

    // Control FPS
    now = Date.now();
    elapsed = now - then;

    if (elapsed > (1000 / fps)) {
      then = now - (elapsed % (1000 / fps));

      // Calculate a linear transition effect
      if (parseInt(currentHeight, 0) != parseInt(height, 0)) currentHeight += heightDifference / (transitionDuration * fps);
      if (parseInt(currentWidth, 0) != parseInt(width, 0)) currentWidth += widthDifference / (transitionDuration * fps);
      if (parseInt(currentDepth, 0) != parseInt(depth, 0)) currentDepth += depthDifference / (transitionDuration * fps);

      // Render the cube
      renderCube(currentHeight, currentWidth, currentDepth, offsetX, offsetY);

      // Check if the current dimensions of the cube are equal to the specified dimensions of the cube
      // If they are the same, finish the transition
      if (parseInt(currentHeight, 0) === parseInt(height, 0) && parseInt(currentWidth, 0) === parseInt(width, 0) && parseInt(currentDepth, 0) === parseInt(depth, 0)) {
        finished = true;
      }
    }
  }

  // Start the animation process
  animate();

  return true;
}

// Draw the cube initially with 5x5
renderCube(5, 5, 5, 80, 70);

// Add the onChange event listener to the select element
var cubeSizeSelector = document.getElementById('cubeSizeSelector');
cubeSizeSelector.onchange = function(e) {
  var cubeSize = e.target.value.split('x');
  renderCubeWithTransistion(5, parseInt(cubeSize[0], 0), parseInt(cubeSize[1], 0), 80, 70, 0.3);
}
canvas {
  border: 1px solid #000;
}
select {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"> </script>
<select id="cubeSizeSelector">
  <option>5x5</option>
  <option>5x10</option>
  <option>10x5</option>
</select>

<canvas></canvas>
like image 69
Bernhard Avatar answered Oct 24 '22 09:10

Bernhard


Drawing an extruded outline. Axonometric

Ideally you would create a generic axonometric renderer that given a floor plan renders the object to the canvas as needed.

You can then link the plan to a selection box and update the view when the selection has changed.

Best as a code example

The example below uses the object renderIsoPlan to render the shape.

Shapes are set via a plan. eg a box has a floor plan [[-1,-1],[1,-1],[1,1],[-1,1]] representing the 4 bottom corners.

The renderIsoPlan has the following properties

  • canvas The canvas that the shape is rendered to. Will not draw until this is set. renderIsoPlan will create a 2D context which will be the same if you have one already
  • height How far up the outline is projected.
  • style Canvas context style object eg {stokeStyle : "red", lineWidth : 2} draws 2 pixel with red lines.
  • plan Set of points for the floor. Points are moved to center automatically. eg [[0,-1],[1,1],[-1,1]] draws a triangle
  • scale Scale say no more
  • rotate Amount to rotate. If not 0 then projection is dimetric else it is trimetric.
  • centerY in unit size of canvas. ie 0.5 is center
  • centerX same as centerY

Call renderIsoPlan.refresh to draw

Note that you can not rotate the projection in the question as it visually appears to warp (change shape) thus if rotate is not 0 then a different projection is used.

Note the object is automatically centered around 0,0 use centerX, centerY to center in the view

setTimeout(start,0); // wait till Javascript parsed and executed
requestAnimationFrame(animate); // Animate checked at start so start anim

// named list of shapes
const boxes = {
  box1By1 : {
    plan : [[-1,-1],[1,-1],[1,1],[-1,1]],
    scale : 35,
    centerY : 0.75,
  },
  box1By2 : {
    plan :  [[-1,-2],[1,-2],[1,2],[-1,2]],
    scale : 30,
    centerY : 0.7,
  },
  box2By2 : {
    plan :  [[-2,-2],[2,-2],[2,2],[-2,2]],
    scale : 25,
    centerY : 0.7,
  },
  box2By1 : {
    plan :  [[-2,-1],[2,-1],[2,1],[-2,1]],
    scale : 30,
    centerY : 0.7,
  },
  box1By3 : {
    plan : [[-1,-3],[1,-3],[1,3],[-1,3]],
    scale : 22,
    centerY : 0.67,
  },
  box1By4 :{
    plan :  [[-1,-4],[1,-4],[1,4],[-1,4]],
    scale : 20,
    centerY : 0.63,
  },
  lShape : {
    plan : [[-2,-4],[0,-4],[0,2],[2,2],[2,4],[-2,4]],
    scale : 20,
    centerY : 0.65,
 },
  current : null,
}
// Sets the renderIsoPlan object to the current selection
function setShape(){
  boxes.current = boxes[boxShape.value];
  Object.assign(renderIsoPlan, boxes.current);
  if (!animateCheckBox.checked) { renderIsoPlan.refresh() }
}
// When ready this is called
function start(){
  renderIsoPlan.canvas = canvas;
  renderIsoPlan.height = 2;
  setShape();
  renderIsoPlan.refresh();
}

// Add event listeners for checkbox and box selection 
boxShape.addEventListener("change", setShape );
animateCheckBox.addEventListener("change",()=>{
  if (animateCheckBox.checked) {
    requestAnimationFrame(animate);
  } else {
    renderIsoPlan.rotate = 0;
    setShape();
  }
});


// Renders animated object
function animate(time){     
  if (animateCheckBox.checked) {
    renderIsoPlan.rotate = time / 1000;
    renderIsoPlan.refresh();
    requestAnimationFrame(animate);
  }
}


// Encasulate Axonometric render.
const renderIsoPlan = (() => {
    var ctx,canvas,plan,cx,cy,w,h,scale,height, rotate;
    height = 50;
    scale = 10;
    rotate = 0;
    const style = {
      strokeStyle : "#000",
      lineWidth : 1,
      lineJoin : "round",
      lineCap : "round",
    };
    const depthScale = (2/3);

    // Transforms then projects the point to 2D
    function transProjPoint(p) {
      const project = rotate !== 0 ? 0 : depthScale;
      const xdx = Math.cos(rotate);
      const xdy = Math.sin(rotate);
      const y = p[0] * xdy + p[1] * xdx;
      const x = p[0] * xdx - p[1] * xdy - y * project;
      return [x,y * depthScale];
    }
    
    // draws the plan        
    function draw() {
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0,0,w,h);      
      ctx.setTransform(scale, 0, 0, scale, cx, cy);
      var i = plan.length;
      ctx.beginPath();
      while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
      ctx.closePath();
      i = plan.length;
      ctx.translate(0,-height);
      ctx.moveTo(...transProjPoint(plan[--i]))
      while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
      ctx.closePath();
      i = plan.length;
      while(i--){
        const [x,y] = transProjPoint(plan[i]);
        ctx.moveTo(x,y);
        ctx.lineTo(x,y + height);
      }
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.stroke();

    }
    // centers the plan view on coordinate 0,0
    function centerPlan(plan){
      var x = 0, y = 0;
      for(const point of plan){
         x += point[0];
         y += point[1];
      }
      x /= plan.length;
      y /= plan.length;
      for(const point of plan){
         point[0] -= x;
         point[1] -= y;
      }
      return plan;
    }    
    
    
    // Sets the style of the rendering
    function setStyle(){
      for(const key of Object.keys(style)){
        if(ctx[key] !== undefined){
          ctx[key] = style[key];
        }
      }
    }


  // define the interface
  const API = {
    // setters allow the use of Object.apply 
    set canvas(c) {
      canvas = c;
      ctx = canvas.getContext("2d");
      w = canvas.width;  // set width and height
      h = canvas.height;
      cx = w / 2 | 0;    // get center
      cy = h / 2 | 0; // move center down because plan is extruded up
    },
    set height(hh) { height = hh },
    set style(s) { Object.assign(style,s) },
    set plan(points) { plan = centerPlan([...points])  },
    set scale(s) { scale = s },
    set rotate(r) { rotate = r },
    set centerY(c) { cy = c * h },
    set centerX(c) { cx = c * w },
    
    // getters not used in the demo
    get height() { return height },
    get style() { return style },
    get plan() { return plan },
    get scale() { return scale },
    get rotate() { return r },
    get centerY() { return cy / h },
    get centerX() { return cx / w },
    
    // Call this to refresh the view
    refresh(){
      if(ctx && plan){
        ctx.save();
        if(style){ setStyle() }
        draw();
        ctx.restore();
      }
    }
  }
  // return the interface
  return API;
})();
canvas { border : 2px solid black; }
<select id="boxShape">
 <option value = "box1By1">1 by 1</option>
 <option value = "box1By2">1 by 2</option>
 <option value = "box2By2">2 by 2</option>
 <option value = "box2By1">2 by 1</option>
 <option value = "box1By3">1 by 3</option>
 <option value = "box1By4">1 by 4</option>
 <option value = "lShape">L shape</option>
</select>
<input type="checkBox" id="animateCheckBox" checked=true>Animate</input><br>
<canvas id="canvas"></canvas>
like image 40
Blindman67 Avatar answered Oct 24 '22 10:10

Blindman67