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>
2 Answers
Answers 1
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>
Answers 2
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>
0 comments:
Post a Comment