Изменить размерность холста в зависимости от выбранного параметра

Я работал на холсте и сталкивался с идеей изменения размеров куба. Итак, используя HTML5 Canvas, я составил этот куб, который имеет два квадрата, соединенных линиями, чтобы сделать его похожим на куб.

Я хочу, чтобы, когда я выбираю тип куба, выберите куб, который должен автоматически меняться в зависимости от длины и ширины выбранного параметра. Высота остается постоянной. Например, если я выбираю куб 5x5, который по умолчанию является кубом, но когда я выбираю вариант 5x10, ширина (фронт) не должна изменяться, а длина (сторона) куба должна расширяться, и наоборот, если я выберите 10x5 мой максимальный вариант - 25x15. Поскольку вы можете видеть, что холст, который я создал ниже, находится в пикселях, сначала мне нужно преобразовать эти пиксели в сантиметры (см), а затем в сантиметры в кубические метры.

Весь куб должен быть выровнен в заданной области холста.

Вот скрипка

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>

Ответы

Ответ 1

Рисование куба:

Чтобы создать динамический куб, вам нужно будет прослушать событие onChange в элементе <select>. Каждый раз, когда выбранная опция изменяется, вы захотите перерисовать свой куб.

Чтобы перерисовать куб, вам нужно создать функцию renderCube которая должна принимать новые размеры куба и как указано смещение для позиционирования. В этой функции вы должны очистить ранее нарисованный куб и перерисовать новый с заданными размерами и смещением.

Добавление эффекта перехода:

Поскольку вы не можете применять css-переходы к элементам canvas, вам необходимо реализовать переход самостоятельно. Вам нужно будет создать функцию анимации, которая будет вычислять размеры куба на фазе перехода и повторно отображать его на экране на каждом кадре.

Реализация масштабируемого куба с эффектом перехода будет:
(если вы предпочитаете здесь тоже скрипку)
(если вам не нужен эффект перехода, проверьте скрипт, прежде чем он будет реализован)

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>

Ответ 2

Рисование экструдированного контура. аксонометрической

В идеале вы бы создали общий аксонометрический рендерер, который, учитывая план этажа, визуализирует объект на холсте по мере необходимости.

Затем вы можете связать план с полем выбора и обновить представление, когда выбор изменился.

Лучший пример кода

В приведенном ниже примере объект renderIsoPlan для визуализации фигуры.

Формы задаются по плану. например, поле имеет план этажа [[-1,-1],[1,-1],[1,1],[-1,1]] представляющий 4 нижних угла.

Функция renderIsoPlan обладает следующими свойствами:

  • холст Холст, на который визуализируется форма. Не будет рисовать, пока это не будет установлено. renderIsoPlan создаст 2D-контекст, который будет таким же, если у вас уже есть
  • height Как далеко проецируется контур.
  • стиль объекта стиля контекста Canvas, например {stokeStyle: "red", lineWidth: 2} рисует 2 пикселя с красными линиями.
  • план Набор точек для пола. Очки автоматически перемещаются в центр. например [[0,-1],[1,1],[-1,1]] рисует треугольник
  • не масштаб шкалы больше говорить
  • Вращайте количество для поворота. Если не 0, то проекция является диметрической, иначе она триметрична.
  • centerY в размерном размере холста. т.е. 0,5 является центром
  • centerX такой же, как centerY

Вызов renderIsoPlan.refresh для рисования

Обратите внимание, что вы не можете поворачивать проекцию в вопросе, поскольку она визуально выглядит деформируемой (меняет форму), поэтому, если вращение не равно 0, используется другая проекция.

Обратите внимание, что объект автоматически центрируется вокруг 0,0, используя centerX, centerY чтобы centerY в представлении

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>