旋转一组包含文本的形状,同时保持文本居中和水平

Rotate a group of shapes containing text whilst keeping text centered and horizontal

这可能只是数学。

我正在使用 Konva 动态生成形状,并将其存储为标签。所以有一个标签,其中包含一个 textElement 和一个矩形。我想确保该矩形中的文本始终是 a) 水平和垂直居中并且 b) 朝向正确的方向。

所以矩形可以任意旋转,但我始终希望文本居中并面向正确的方向。

创建代码;宽度、高度、旋转、x 和 y 都有从数据库中提取的值。

var table = new Konva.Label({
                x: pos_x,
                y: pos_y,
                width: tableWidth,
                height: tableHeight,
                draggable:true
              });

table.add(new Konva.Rect({
                    width: tableWidth,
                    height: tableHeight,
                    rotation: rotation,
                    fill: fillColor,
                    stroke: strokeColor,
                    strokeWidth: 4
                }));

table.add(new Konva.Text({
                width: tableWidth,
                height: tableHeight,
                x: pos_x, //Defaults to zero
                y: pos_y, //Default to zero
                text: tableNumber, 
                verticalAlign: 'middle',
                align: 'center',
                fontSize: 30,
                fontFamily: 'Calibri',
                fill: 'black'
            }))

tableLayer.add(table);

问题是,如果旋转到位,文本会偏离中心,如下图所示:

在某些情况下我会手动更正 - 例如如果旋转 = 45 度:

pos_x = -tableWidth/2;
pos_y = tableHeight/5;

但这不是长久之计。我希望文本的 x 和 y 坐标位于形状本身的中心点。

我尝试了几种方法(例如对 Label 本身应用旋转,然后对文本应用负旋转值)

此代码片段说明了一个解决方案。它是从我的 复制和修改的,当时我正在寻找一种围绕任意点旋转的稳健方法 - 请注意,我认为这是一个与我原来的问题略有不同的问题,所以我没有暗示这是一个重复。不同之处在于需要使用更复杂的分组形状并保持该组中的某些元素不旋转。

不在 OP 的问题中,但我通过将文本分组来将背景矩形设置到文本中。这样做的目的是表明文本矩形将在某些旋转点延伸到标签矩形之外。这不是一个关键问题,但看到它发生是很有用的。

编码人员面临的根本挑战是了解形状在旋转时如何移动,因为我们通常希望将它们围绕其中心旋转,但 Konva(以及所有 HTML5 canvas wrappers) 接下来是从 top-left 角旋转,至少对于问题中的每个形状的矩形。 'is' 可以移动旋转点(称为偏移量),但这对开发人员来说也是一个概念上的挑战,对任何试图支持代码的人来说都是一个很好的陷阱。

此答案中有很多代码用于设置一些动态的东西,您可以使用它来可视化正在发生的事情。然而,症结在于:

  // This is the important call ! Cross is the rotation point as illustrated by crosshairs.
  rotateAroundPoint(shape, rotateBy, {x: cross.x(), y: cross.y()});
  
  // The label is a special case because we need to keep the text unrotated.
  if (shape.name() === 'label'){
    let text = shape.find('.text')[0];
    rotateAroundPoint(text, -1 * rotateBy, {x: text.getClientRect().width/2, y: text.getClientRect().height/2});
  }

rotateAroundPoint() 函数将要旋转的 Konva 形状、顺时针旋转角度(不是弧度,好角度)以及旋转点在 canvas / 上的 x 和 y 位置作为参数parent.

我构建了一组形状作为我的标签,由一个矩形和一个文本形状组成。我将其命名为 'label'。实际上,我将文本形状切换为另一组矩形 + 文本,这样我就可以显示文本所在的矩形。你可以省略额外的组。我将其命名为 'text'.

第一次调用 rotateAroundPoint() 会旋转名为 'label' 的组。所以小组在 canvas 上轮换。由于 'text' 是 'label' 组的 child,这会使 'text' 旋转,因此下一行检查我们是否正在使用 'label'组,如果是这样,我们需要掌握 'text' 形状,这就是这条线的作用:

let text = shape.find('.text')[0];

在 Konva 中,find() 的结果是一个列表,所以我们取列表中的第一个。现在我剩下要做的就是通过将负旋转度应用到其中心点来再次旋转 'label' 组上的文本。下面的代码实现了这一点。

rotateAroundPoint(text, -1 * rotateBy, {x: text.getClientRect().width/2, y: text.getClientRect().height/2});

值得一提的一点 - 我为我的 'text' 形状使用了一组。 Konva 组自然没有宽度或高度 - 它更像是一种将形状收集在一起但没有 'physical' 容器的方法。因此,为了获得中心点计算的宽度和高度,我使用了 group.getClientRect() 方法,该方法给出了包含组中所有形状的最小边界框的大小,并生成了一个 object作为{宽度:,高度:}。

第二个注意事项 - 第一次使用 rotateAroundPoint() 会影响 'label' 组,该组的 parent 为 canvas。该函数的第二次使用会影响 'text' 组,该组将 'label' 组作为其 parent。它微妙但值得了解。

这是片段。我强烈建议您 运行 全屏并围绕几个不同的点旋转一些形状。

// Code to illustrate rotation of a shape around any given point. The important functions here is rotateAroundPoint() which does the rotation and movement math ! 

let 
    angle = 0, // display value of angle
    startPos = {x: 80, y: 45},    
    shapes = [],    // array of shape ghosts / tails
    rotateBy = 20,  // per-step angle of rotation 
    shapeName = $('#shapeName').val(),  // what shape are we drawing
    shape = null,
    ghostLimit = 10,

    // Set up a stage
    stage = new Konva.Stage({
        container: 'container',
        width: window.innerWidth,
        height: window.innerHeight
      }),

  
    // add a layer to draw on
    layer = new Konva.Layer(),
    
    // create the rotation target point cross-hair marker
    lineV = new Konva.Line({points: [0, -20, 0, 20], stroke: 'lime', strokeWidth: 1}),
    lineH = new Konva.Line({points: [-20, 0,  20, 0], stroke: 'lime', strokeWidth: 1}),
    circle = new Konva.Circle({x: 0, y: 0, radius: 10, fill: 'transparent', stroke: 'lime', strokeWidth: 1}),
    cross = new Konva.Group({draggable: true, x: startPos.x, y: startPos.y}),
    labelRect, labelText;

// Add the elements to the cross-hair group
cross.add(lineV, lineH, circle);
layer.add(cross);

// Add the layer to the stage
stage.add(layer);


$('#shapeName').on('change', function(){
  shapeName = $('#shapeName').val();
  shape.destroy();
  shape = null;
  reset();
})


// Draw whatever shape the user selected
function drawShape(){
  
    // Add a shape to rotate
    if (shape !== null){
      shape.destroy();
    }
   
    switch (shapeName){
      case "rectangle":
        shape = new Konva.Rect({x: startPos.x, y: startPos.y, width: 120, height: 80, fill: 'magenta', stroke: 'black', strokeWidth: 4});
        break;

      case "hexagon":
        shape = new Konva.RegularPolygon({x: startPos.x, y: startPos.y, sides: 6, radius: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4}); 
        break;
        
      case "ellipse":
        shape = new Konva.Ellipse({x: startPos.x, y: startPos.y, radiusX: 40, radiusY: 20, fill: 'magenta', stroke: 'black', strokeWidth: 4});     
        break;
        
      case "circle":
        shape = new Konva.Ellipse({x: startPos.x, y: startPos.y, radiusX: 40, radiusY: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});     
        break;

      case "star":
        shape = new Konva.Star({x: startPos.x, y: startPos.y, numPoints: 5, innerRadius: 20, outerRadius: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});     
        break;        
        
       case "label":
         shape = new Konva.Group({name: 'label'});
        labelRect = new Konva.Rect({x: 0, y: 0, width: 120, height: 80, fill: 'magenta', stroke: 'black', strokeWidth: 4, name: 'rect'})
        shape.add(labelRect);
        labelText = new Konva.Group({name: 'text'});
        labelText.add(new Konva.Rect({x: 0, y: 0, width: 100, height: 40, fill: 'cyan', stroke: 'black', strokeWidth: 2}))
        labelText.add(new Konva.Text({x: 0, y: 0, width: 100, height: 40, text: 'Wombat',fontSize: 20, fontFamily: 'Calibri', align: 'center', padding: 10}))
        
        shape.add(labelText)
        
        labelText.position({x: (labelRect.width() - labelText.getClientRect().width) /2, y:  (labelRect.height() - labelText.getClientRect().height) /2})
        
        
        
        break;
       
    };
    layer.add(shape);
    
    cross.moveToTop();

  }



// Reset the shape position etc.
function reset(){

  drawShape();  // draw the current shape
  
  // Set to starting position, etc.
  shape.position(startPos)
  cross.position(startPos);
  angle = 0;
  $('#angle').html(angle);
  $('#position').html('(' + shape.x() + ', ' + shape.y() + ')');
  
  clearTails(); // clear the tail shapes
  
  stage.draw();  // refresh / draw the stage.
}




// Click the stage to move the rotation point
stage.on('click', function (e) {
  cross.position(stage.getPointerPosition());
  stage.draw();
});

// Rotate a shape around any point.
// shape is a Konva shape
// angleRadians is the angle to rotate by, in radians
// point is an object {x: posX, y: posY}
function rotateAroundPoint(shape, angleDegrees, point) {
  let angleRadians = angleDegrees * Math.PI / 180; // sin + cos require radians
  
  const x =
    point.x +
    (shape.x() - point.x) * Math.cos(angleRadians) -
    (shape.y() - point.y) * Math.sin(angleRadians);
  const y =
    point.y +
    (shape.x() - point.x) * Math.sin(angleRadians) +
    (shape.y() - point.y) * Math.cos(angleRadians);
   
  shape.rotation(shape.rotation() + angleDegrees); // rotate the shape in place
  shape.x(x);  // move the rotated shape in relation to the rotation point.
  shape.y(y);
  
  shape.moveToTop(); // 
}



$('#rotate').on('click', function(){
  
  let newShape = shape.clone();
  shapes.push(newShape);
  layer.add(newShape);
  
  // This ghost / tails stuff is just for fun.
  if (shapes.length >= ghostLimit){
    shapes[0].destroy();   
    shapes = shapes.slice(1);
  }
  for (var i = shapes.length - 1; i >= 0; i--){
    shapes[i].opacity((i + 1) * (1/(shapes.length + 2)))
  };

  // This is the important call ! Cross is the rotation point as illustrated by crosshairs.
  rotateAroundPoint(shape, rotateBy, {x: cross.x(), y: cross.y()});
  
  // The label is a special case because we need to keep the text unrotated.
  if (shape.name() === 'label'){
    let text = shape.find('.text')[0];
    rotateAroundPoint(text, -1 * rotateBy, {x: text.getClientRect().width/2, y: text.getClientRect().height/2});
  }

  
  cross.moveToTop();
  
  stage.draw();
  
  angle = angle + 10;
  $('#angle').html(angle);
  $('#position').html('(' + Math.round(shape.x() * 10) / 10 + ', ' + Math.round(shape.y() * 10) / 10 + ')');
})



// Function to clear the ghost / tail shapes
function clearTails(){

  for (var i = shapes.length - 1; i >= 0; i--){
    shapes[i].destroy();
  };
  shapes = [];
  
}

// User cicks the reset button.
$('#reset').on('click', function(){

  reset();

})

// Force first draw!
reset();
body {
  margin: 10;
  padding: 10;
  overflow: hidden;
  background-color: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva@^3/konva.min.js"></script>
<p>1. Click the rotate button to see what happens when rotating around shape origin.</p>
<p>2. Reset then click stage to move rotation point and click rotate button again - rinse & repeat</p>
<p>
<button id = 'rotate'>Rotate</button>
  <button id = 'reset'>Reset</button> 
  <select id='shapeName'>
    <option value='label' selected='selected'>Label</option>
    <option value='rectangle'>Rectangle</option>
    <option value='hexagon'>Polygon</option>
    <option value='ellipse' >Ellipse</option>
    <option value='circle' >Circle</option>
    <option value='star'>Star</option>    
  </select>
  Angle :    <span id='angle'>0</span>
Position :   <span id='position'></span>
</p>
<div id="container"></div>