mirror of
https://github.com/konvajs/konva.git
synced 2026-02-24 20:26:01 +08:00
feat: add rotateAnchorAngle to Transformer for customizable rotation anchor positioning
This commit is contained in:
@@ -23,6 +23,7 @@ export interface TransformerConfig extends ContainerConfig {
|
||||
rotationSnaps?: Array<number>;
|
||||
rotationSnapTolerance?: number;
|
||||
rotateAnchorOffset?: number;
|
||||
rotateAnchorAngle?: number;
|
||||
rotateAnchorCursor?: string;
|
||||
borderEnabled?: boolean;
|
||||
borderStroke?: string;
|
||||
@@ -55,6 +56,7 @@ const EVENTS_NAME = 'tr-konva';
|
||||
const ATTR_CHANGE_LIST = [
|
||||
'resizeEnabledChange',
|
||||
'rotateAnchorOffsetChange',
|
||||
'rotateAnchorAngleChange',
|
||||
'rotateEnabledChange',
|
||||
'enabledAnchorsChange',
|
||||
'anchorSizeChange',
|
||||
@@ -620,19 +622,54 @@ export class Transformer extends Group {
|
||||
sceneFunc(ctx, shape) {
|
||||
const tr = shape.getParent() as Transformer;
|
||||
const padding = tr.padding();
|
||||
const width = shape.width();
|
||||
const height = shape.height();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(
|
||||
-padding,
|
||||
-padding,
|
||||
shape.width() + padding * 2,
|
||||
shape.height() + padding * 2
|
||||
width + padding * 2,
|
||||
height + padding * 2
|
||||
);
|
||||
ctx.moveTo(shape.width() / 2, -padding);
|
||||
|
||||
if (tr.rotateEnabled() && tr.rotateLineVisible()) {
|
||||
ctx.lineTo(
|
||||
shape.width() / 2,
|
||||
-tr.rotateAnchorOffset() * Util._sign(shape.height()) - padding
|
||||
);
|
||||
// Calculate rotation line position based on rotateAnchorAngle
|
||||
const rotateAnchorAngle = tr.rotateAnchorAngle();
|
||||
const rotateAnchorOffset = tr.rotateAnchorOffset();
|
||||
const rad = Util.degToRad(rotateAnchorAngle);
|
||||
// Direction vector (0 degrees = up/top)
|
||||
const dirX = Math.sin(rad);
|
||||
const dirY = -Math.cos(rad);
|
||||
|
||||
// Center of the box
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
|
||||
// Find intersection with box edge
|
||||
let t = Infinity;
|
||||
if (dirY < 0) {
|
||||
t = Math.min(t, -cy / dirY);
|
||||
} else if (dirY > 0) {
|
||||
t = Math.min(t, (height - cy) / dirY);
|
||||
}
|
||||
if (dirX < 0) {
|
||||
t = Math.min(t, -cx / dirX);
|
||||
} else if (dirX > 0) {
|
||||
t = Math.min(t, (width - cx) / dirX);
|
||||
}
|
||||
|
||||
// Edge point (start of line)
|
||||
const edgeX = cx + dirX * t;
|
||||
const edgeY = cy + dirY * t;
|
||||
|
||||
// End point with offset
|
||||
const sign = Util._sign(height);
|
||||
const endX = edgeX + dirX * rotateAnchorOffset * sign;
|
||||
const endY = edgeY + dirY * rotateAnchorOffset * sign;
|
||||
|
||||
ctx.moveTo(edgeX, edgeY);
|
||||
ctx.lineTo(endX, endY);
|
||||
}
|
||||
|
||||
ctx.fillStrokeShape(shape);
|
||||
@@ -739,8 +776,10 @@ export class Transformer extends Group {
|
||||
x = anchorNode.x() - attrs.width / 2;
|
||||
y = -anchorNode.y() + attrs.height / 2;
|
||||
|
||||
// hor angle is changed?
|
||||
let delta = Math.atan2(-y, x) + Math.PI / 2;
|
||||
// Calculate angle from center to current anchor position
|
||||
// Offset by rotateAnchorAngle so we measure rotation from the anchor's starting position
|
||||
const rotateAnchorAngleRad = Konva.getAngle(this.rotateAnchorAngle());
|
||||
let delta = Math.atan2(-y, x) + Math.PI / 2 - rotateAnchorAngleRad;
|
||||
|
||||
if (attrs.height < 0) {
|
||||
delta -= Math.PI;
|
||||
@@ -1244,9 +1283,47 @@ export class Transformer extends Group {
|
||||
visible: resizeEnabled && enabledAnchors.indexOf('bottom-right') >= 0,
|
||||
});
|
||||
|
||||
// Calculate rotation anchor position based on rotateAnchorAngle
|
||||
const rotateAnchorAngle = this.rotateAnchorAngle();
|
||||
const rotateAnchorOffset = this.rotateAnchorOffset();
|
||||
const rad = Util.degToRad(rotateAnchorAngle);
|
||||
// Direction vector (0 degrees = up/top)
|
||||
const dirX = Math.sin(rad);
|
||||
const dirY = -Math.cos(rad);
|
||||
|
||||
// Center of the box
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
|
||||
// Find intersection with box edge
|
||||
// Calculate time to hit each edge from center
|
||||
let t = Infinity;
|
||||
|
||||
// Handle each direction
|
||||
if (dirY < 0) {
|
||||
// Moving up, check top edge (y = 0)
|
||||
t = Math.min(t, -cy / dirY);
|
||||
} else if (dirY > 0) {
|
||||
// Moving down, check bottom edge (y = height)
|
||||
t = Math.min(t, (height - cy) / dirY);
|
||||
}
|
||||
if (dirX < 0) {
|
||||
// Moving left, check left edge (x = 0)
|
||||
t = Math.min(t, -cx / dirX);
|
||||
} else if (dirX > 0) {
|
||||
// Moving right, check right edge (x = width)
|
||||
t = Math.min(t, (width - cx) / dirX);
|
||||
}
|
||||
|
||||
// Edge point
|
||||
const edgeX = cx + dirX * t;
|
||||
const edgeY = cy + dirY * t;
|
||||
|
||||
// Final position with offset (accounting for height sign and padding)
|
||||
const sign = Util._sign(height);
|
||||
this._batchChangeChild('.rotater', {
|
||||
x: width / 2,
|
||||
y: -this.rotateAnchorOffset() * Util._sign(height) - padding,
|
||||
x: edgeX + dirX * rotateAnchorOffset * sign,
|
||||
y: edgeY + dirY * rotateAnchorOffset * sign - padding * dirY,
|
||||
visible: this.rotateEnabled(),
|
||||
});
|
||||
|
||||
@@ -1332,6 +1409,7 @@ export class Transformer extends Group {
|
||||
rotateEnabled: GetSet<boolean, this>;
|
||||
rotateLineVisible: GetSet<boolean, this>;
|
||||
rotateAnchorOffset: GetSet<number, this>;
|
||||
rotateAnchorAngle: GetSet<number, this>;
|
||||
rotationSnapTolerance: GetSet<number, this>;
|
||||
rotateAnchorCursor: GetSet<string, this>;
|
||||
padding: GetSet<number, this>;
|
||||
@@ -1514,6 +1592,30 @@ Factory.addGetterSetter(
|
||||
getNumberValidator()
|
||||
);
|
||||
|
||||
/**
|
||||
* get/set the angle (in degrees) of the rotation anchor position around the bounding box.
|
||||
* 0 = top-center (default), 90 = middle-right, 180 = bottom-center, -90 = middle-left
|
||||
* @name Konva.Transformer#rotateAnchorAngle
|
||||
* @method
|
||||
* @param {Number} angle
|
||||
* @returns {Number}
|
||||
* @example
|
||||
* // get
|
||||
* var rotateAnchorAngle = transformer.rotateAnchorAngle();
|
||||
*
|
||||
* // set rotation anchor to the right side
|
||||
* transformer.rotateAnchorAngle(90);
|
||||
*
|
||||
* // set rotation anchor to the bottom
|
||||
* transformer.rotateAnchorAngle(180);
|
||||
*/
|
||||
Factory.addGetterSetter(
|
||||
Transformer,
|
||||
'rotateAnchorAngle',
|
||||
0,
|
||||
getNumberValidator()
|
||||
);
|
||||
|
||||
/**
|
||||
* get/set rotation anchor cursor
|
||||
* @name Konva.Transformer#rotateAnchorCursor
|
||||
|
||||
@@ -32,60 +32,25 @@
|
||||
const layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
// const circle = new Konva.Circle({
|
||||
// x: 100,
|
||||
// y: 100,
|
||||
// radius: 10,
|
||||
// fill: 'red',
|
||||
// });
|
||||
// layer.add(circle);
|
||||
// throw new Error('test');
|
||||
const text = new Konva.Text({
|
||||
x: 100,
|
||||
y: 100,
|
||||
text: 'Hello, world!',
|
||||
fontSize: 20,
|
||||
fontFamily: 'Arial',
|
||||
fill: 'black',
|
||||
});
|
||||
layer.add(text);
|
||||
|
||||
// Given gridRows and gridCols, compute maximum possible radius and position circles
|
||||
const gridRows = 1000;
|
||||
const gridCols = 1000;
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
const transformer = new Konva.Transformer({
|
||||
nodes: [text],
|
||||
});
|
||||
layer.add(transformer);
|
||||
|
||||
// Calculate spacing and radius so circles fit entire window
|
||||
// There are (gridCols - 1) spaces between centers horizontally, and (gridRows - 1) vertically
|
||||
// The available width is "width", so (gridCols) circles and (gridCols-1) spaces between
|
||||
// The diameter must fit: gridCols * diameter + (gridCols - 1) * gap <= width
|
||||
// But to maximize, gap should equal to at least 0, so best layout is circles tangent
|
||||
|
||||
// So the max diameter horizontally is width / gridCols
|
||||
// Likewise for vertical
|
||||
const cellWidth = width / gridCols;
|
||||
const cellHeight = height / gridRows;
|
||||
const radius = Math.min(cellWidth, cellHeight) / 2;
|
||||
|
||||
const circles = [];
|
||||
|
||||
// Create grid of circles - centers in each cell's center
|
||||
console.time('create grid of circles');
|
||||
for (let row = 0; row < gridRows; row++) {
|
||||
for (let col = 0; col < gridCols; col++) {
|
||||
const cx = cellWidth * col + cellWidth / 2;
|
||||
const cy = cellHeight * row + cellHeight / 2;
|
||||
const c = new Konva.Circle({
|
||||
x: cx,
|
||||
y: cy,
|
||||
radius: radius,
|
||||
fill: Konva.Util.getRandomColor(),
|
||||
id: `circle-${row}-${col}`,
|
||||
});
|
||||
circles.push({
|
||||
node: c,
|
||||
x: cx,
|
||||
y: cy,
|
||||
row,
|
||||
col,
|
||||
});
|
||||
layer.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd('create grid of circles');
|
||||
const anim = new Konva.Animation((frame) => {
|
||||
transformer.rotateAnchorAngle(frame.time * 0.01);
|
||||
}, layer);
|
||||
anim.start();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5435,4 +5435,151 @@ describe('Transformer', function () {
|
||||
'Back should be draggable when at least one node is draggable'
|
||||
);
|
||||
});
|
||||
|
||||
// ======================================================
|
||||
// rotateAnchorAngle tests
|
||||
// ======================================================
|
||||
|
||||
it('rotateAnchorAngle positions rotater correctly at different angles', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
var rect = new Konva.Rect({
|
||||
x: 50,
|
||||
y: 50,
|
||||
draggable: true,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: 'yellow',
|
||||
});
|
||||
layer.add(rect);
|
||||
|
||||
var tr = new Konva.Transformer({
|
||||
nodes: [rect],
|
||||
});
|
||||
layer.add(tr);
|
||||
layer.draw();
|
||||
|
||||
var rotater = tr.findOne('.rotater')!;
|
||||
var offset = tr.rotateAnchorOffset();
|
||||
|
||||
// 0 degrees (default): top-center
|
||||
assertAlmostEqual(rotater.x(), 50);
|
||||
assertAlmostEqual(rotater.y(), -offset);
|
||||
|
||||
// 90 degrees: middle-right
|
||||
tr.rotateAnchorAngle(90);
|
||||
assertAlmostEqual(rotater.x(), 100 + offset);
|
||||
assertAlmostEqual(rotater.y(), 50);
|
||||
|
||||
// 180 degrees: bottom-center
|
||||
tr.rotateAnchorAngle(180);
|
||||
assertAlmostEqual(rotater.x(), 50);
|
||||
assertAlmostEqual(rotater.y(), 100 + offset);
|
||||
|
||||
// -90 degrees: middle-left
|
||||
tr.rotateAnchorAngle(-90);
|
||||
assertAlmostEqual(rotater.x(), -offset);
|
||||
assertAlmostEqual(rotater.y(), 50);
|
||||
});
|
||||
|
||||
it('drag rotation works with rotateAnchorAngle at 90 (right side)', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
// Position rect so rotater at right side stays within stage bounds (578x200)
|
||||
var rect = new Konva.Rect({
|
||||
x: 50,
|
||||
y: 50,
|
||||
draggable: true,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: 'yellow',
|
||||
});
|
||||
layer.add(rect);
|
||||
|
||||
var tr = new Konva.Transformer({
|
||||
nodes: [rect],
|
||||
rotateAnchorAngle: 90,
|
||||
});
|
||||
layer.add(tr);
|
||||
layer.draw();
|
||||
|
||||
var rotater = tr.findOne('.rotater')!;
|
||||
var pos = rotater.getAbsolutePosition();
|
||||
|
||||
simulateMouseDown(tr, { x: pos.x, y: pos.y });
|
||||
simulateMouseMove(tr, { x: pos.x - 100, y: pos.y + 100 });
|
||||
simulateMouseUp(tr, { x: pos.x - 100, y: pos.y + 100 });
|
||||
|
||||
assert.equal(rect.rotation(), 90);
|
||||
});
|
||||
|
||||
it('drag rotation works with rotateAnchorAngle at -90 (left side)', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
// Position rect so rotater at left side stays within stage bounds
|
||||
var rect = new Konva.Rect({
|
||||
x: 150,
|
||||
y: 50,
|
||||
draggable: true,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: 'yellow',
|
||||
});
|
||||
layer.add(rect);
|
||||
|
||||
var tr = new Konva.Transformer({
|
||||
nodes: [rect],
|
||||
rotateAnchorAngle: -90,
|
||||
});
|
||||
layer.add(tr);
|
||||
layer.draw();
|
||||
|
||||
var rotater = tr.findOne('.rotater')!;
|
||||
var pos = rotater.getAbsolutePosition();
|
||||
|
||||
simulateMouseDown(tr, { x: pos.x, y: pos.y });
|
||||
simulateMouseMove(tr, { x: pos.x + 100, y: pos.y - 100 });
|
||||
simulateMouseUp(tr, { x: pos.x + 100, y: pos.y - 100 });
|
||||
|
||||
assert.equal(rect.rotation(), 90);
|
||||
});
|
||||
|
||||
it('drag rotation works with rotateAnchorAngle at 180 (bottom)', function () {
|
||||
var stage = addStage();
|
||||
var layer = new Konva.Layer();
|
||||
stage.add(layer);
|
||||
|
||||
// Position rect so rotater at bottom stays within stage bounds (height=200)
|
||||
var rect = new Konva.Rect({
|
||||
x: 50,
|
||||
y: 10,
|
||||
draggable: true,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: 'yellow',
|
||||
});
|
||||
layer.add(rect);
|
||||
|
||||
var tr = new Konva.Transformer({
|
||||
nodes: [rect],
|
||||
rotateAnchorAngle: 180,
|
||||
});
|
||||
layer.add(tr);
|
||||
layer.draw();
|
||||
|
||||
var rotater = tr.findOne('.rotater')!;
|
||||
var pos = rotater.getAbsolutePosition();
|
||||
|
||||
simulateMouseDown(tr, { x: pos.x, y: pos.y });
|
||||
simulateMouseMove(tr, { x: pos.x - 100, y: pos.y - 100 });
|
||||
simulateMouseUp(tr, { x: pos.x - 100, y: pos.y - 100 });
|
||||
|
||||
assertAlmostEqual(rect.rotation(), 90);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user