refactor: Remove offsetX/offsetY from text objects for more reliable positioning
Browse filesBreaking change: Simplifies text positioning by removing offsetX/offsetY properties
and relying on Konva's built-in alignment instead.
Changes:
- Remove offsetX and offsetY from TextObject type definition
- Update all layouts to use proper x/y positioning without offsets
- Remove offset handling from Canvas.tsx (group snapping, single snapping, transforms, textarea positioning)
- Remove offset rendering from TextEditable.tsx
- Update layoutExporter.ts to export fontWeight instead of offsets
Benefits:
- Eliminates complex offset scaling during transformations
- Reduces multiple sources of truth for positioning
- Makes text transformations more predictable and reliable
- Simplifies codebase maintenance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
- src/components/Canvas/Canvas.tsx +12 -32
- src/components/Canvas/TextEditable.tsx +0 -2
- src/data/layouts.ts +4 -8
- src/types/canvas.types.ts +0 -2
- src/utils/layoutExporter.ts +2 -6
|
@@ -322,11 +322,9 @@ export default function Canvas({
|
|
| 322 |
const nodeHeight = selectedNode.height();
|
| 323 |
const nodeX = selectedNode.x();
|
| 324 |
const nodeY = selectedNode.y();
|
| 325 |
-
const offsetX = (selectedNode as any).attrs.offsetX || 0;
|
| 326 |
-
const offsetY = (selectedNode as any).attrs.offsetY || 0;
|
| 327 |
|
| 328 |
-
const left = nodeX
|
| 329 |
-
const top = nodeY
|
| 330 |
const right = left + nodeWidth;
|
| 331 |
const bottom = top + nodeHeight;
|
| 332 |
|
|
@@ -391,11 +389,9 @@ export default function Canvas({
|
|
| 391 |
const nodeX = node.x();
|
| 392 |
const nodeY = node.y();
|
| 393 |
|
| 394 |
-
// Calculate object center position
|
| 395 |
-
const
|
| 396 |
-
const
|
| 397 |
-
const objectCenterX = nodeX + (nodeWidth / 2) - offsetX;
|
| 398 |
-
const objectCenterY = nodeY + (nodeHeight / 2) - offsetY;
|
| 399 |
|
| 400 |
let snappedX = nodeX;
|
| 401 |
let snappedY = nodeY;
|
|
@@ -413,14 +409,14 @@ export default function Canvas({
|
|
| 413 |
// Check horizontal center snapping
|
| 414 |
// If Shift is pressed and moving vertically, don't snap horizontally
|
| 415 |
if (Math.abs(objectCenterX - centerX) < snapThreshold && (!shiftPressed || isHorizontalMovement)) {
|
| 416 |
-
snappedX = centerX - (nodeWidth / 2)
|
| 417 |
showVerticalGuide = true;
|
| 418 |
}
|
| 419 |
|
| 420 |
// Check vertical center snapping
|
| 421 |
// If Shift is pressed and moving horizontally, don't snap vertically
|
| 422 |
if (Math.abs(objectCenterY - centerY) < snapThreshold && (!shiftPressed || !isHorizontalMovement)) {
|
| 423 |
-
snappedY = centerY - (nodeHeight / 2)
|
| 424 |
showHorizontalGuide = true;
|
| 425 |
}
|
| 426 |
|
|
@@ -516,21 +512,15 @@ export default function Canvas({
|
|
| 516 |
rotation: objNode.rotation()
|
| 517 |
};
|
| 518 |
|
| 519 |
-
// Handle text objects: scale fontSize
|
| 520 |
if (obj.type === 'text') {
|
| 521 |
const scaleFactor = Math.min(nodeScaleX, nodeScaleY);
|
| 522 |
const newFontSize = Math.max(10, obj.fontSize * scaleFactor);
|
| 523 |
|
| 524 |
-
// Scale offsetX and offsetY if they exist
|
| 525 |
-
const scaledOffsetX = obj.offsetX ? obj.offsetX * nodeScaleX : undefined;
|
| 526 |
-
const scaledOffsetY = obj.offsetY ? obj.offsetY * nodeScaleY : undefined;
|
| 527 |
-
|
| 528 |
return {
|
| 529 |
...updated,
|
| 530 |
fontSize: newFontSize,
|
| 531 |
-
isFixedSize: true
|
| 532 |
-
offsetX: scaledOffsetX,
|
| 533 |
-
offsetY: scaledOffsetY
|
| 534 |
};
|
| 535 |
}
|
| 536 |
return updated;
|
|
@@ -564,16 +554,10 @@ export default function Canvas({
|
|
| 564 |
const scaleFactor = Math.min(scaleX, scaleY);
|
| 565 |
const newFontSize = Math.max(10, obj.fontSize * scaleFactor);
|
| 566 |
|
| 567 |
-
// Scale offsetX and offsetY if they exist
|
| 568 |
-
const scaledOffsetX = obj.offsetX ? obj.offsetX * scaleX : undefined;
|
| 569 |
-
const scaledOffsetY = obj.offsetY ? obj.offsetY * scaleY : undefined;
|
| 570 |
-
|
| 571 |
return {
|
| 572 |
...updated,
|
| 573 |
fontSize: newFontSize,
|
| 574 |
-
isFixedSize: true
|
| 575 |
-
offsetX: scaledOffsetX,
|
| 576 |
-
offsetY: scaledOffsetY
|
| 577 |
};
|
| 578 |
}
|
| 579 |
return updated;
|
|
@@ -695,13 +679,9 @@ export default function Canvas({
|
|
| 695 |
const container = stage.container();
|
| 696 |
const containerRect = container.getBoundingClientRect();
|
| 697 |
|
| 698 |
-
// Account for offsetX and offsetY when positioning textarea
|
| 699 |
-
const offsetX = editingText.offsetX || 0;
|
| 700 |
-
const offsetY = editingText.offsetY || 0;
|
| 701 |
-
|
| 702 |
return {
|
| 703 |
-
top: containerRect.top + editingText.y
|
| 704 |
-
left: containerRect.left + editingText.x
|
| 705 |
};
|
| 706 |
};
|
| 707 |
|
|
|
|
| 322 |
const nodeHeight = selectedNode.height();
|
| 323 |
const nodeX = selectedNode.x();
|
| 324 |
const nodeY = selectedNode.y();
|
|
|
|
|
|
|
| 325 |
|
| 326 |
+
const left = nodeX;
|
| 327 |
+
const top = nodeY;
|
| 328 |
const right = left + nodeWidth;
|
| 329 |
const bottom = top + nodeHeight;
|
| 330 |
|
|
|
|
| 389 |
const nodeX = node.x();
|
| 390 |
const nodeY = node.y();
|
| 391 |
|
| 392 |
+
// Calculate object center position
|
| 393 |
+
const objectCenterX = nodeX + (nodeWidth / 2);
|
| 394 |
+
const objectCenterY = nodeY + (nodeHeight / 2);
|
|
|
|
|
|
|
| 395 |
|
| 396 |
let snappedX = nodeX;
|
| 397 |
let snappedY = nodeY;
|
|
|
|
| 409 |
// Check horizontal center snapping
|
| 410 |
// If Shift is pressed and moving vertically, don't snap horizontally
|
| 411 |
if (Math.abs(objectCenterX - centerX) < snapThreshold && (!shiftPressed || isHorizontalMovement)) {
|
| 412 |
+
snappedX = centerX - (nodeWidth / 2);
|
| 413 |
showVerticalGuide = true;
|
| 414 |
}
|
| 415 |
|
| 416 |
// Check vertical center snapping
|
| 417 |
// If Shift is pressed and moving horizontally, don't snap vertically
|
| 418 |
if (Math.abs(objectCenterY - centerY) < snapThreshold && (!shiftPressed || !isHorizontalMovement)) {
|
| 419 |
+
snappedY = centerY - (nodeHeight / 2);
|
| 420 |
showHorizontalGuide = true;
|
| 421 |
}
|
| 422 |
|
|
|
|
| 512 |
rotation: objNode.rotation()
|
| 513 |
};
|
| 514 |
|
| 515 |
+
// Handle text objects: scale fontSize
|
| 516 |
if (obj.type === 'text') {
|
| 517 |
const scaleFactor = Math.min(nodeScaleX, nodeScaleY);
|
| 518 |
const newFontSize = Math.max(10, obj.fontSize * scaleFactor);
|
| 519 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
return {
|
| 521 |
...updated,
|
| 522 |
fontSize: newFontSize,
|
| 523 |
+
isFixedSize: true
|
|
|
|
|
|
|
| 524 |
};
|
| 525 |
}
|
| 526 |
return updated;
|
|
|
|
| 554 |
const scaleFactor = Math.min(scaleX, scaleY);
|
| 555 |
const newFontSize = Math.max(10, obj.fontSize * scaleFactor);
|
| 556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
return {
|
| 558 |
...updated,
|
| 559 |
fontSize: newFontSize,
|
| 560 |
+
isFixedSize: true
|
|
|
|
|
|
|
| 561 |
};
|
| 562 |
}
|
| 563 |
return updated;
|
|
|
|
| 679 |
const container = stage.container();
|
| 680 |
const containerRect = container.getBoundingClientRect();
|
| 681 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
return {
|
| 683 |
+
top: containerRect.top + editingText.y,
|
| 684 |
+
left: containerRect.left + editingText.x
|
| 685 |
};
|
| 686 |
};
|
| 687 |
|
|
@@ -125,8 +125,6 @@ export default function TextEditable({
|
|
| 125 |
y={object.y}
|
| 126 |
width={object.width}
|
| 127 |
height={object.height}
|
| 128 |
-
offsetX={object.offsetX || 0}
|
| 129 |
-
offsetY={object.offsetY || 0}
|
| 130 |
text={object.text}
|
| 131 |
fontSize={displayFontSize}
|
| 132 |
fontFamily={object.fontFamily}
|
|
|
|
| 125 |
y={object.y}
|
| 126 |
width={object.width}
|
| 127 |
height={object.height}
|
|
|
|
|
|
|
| 128 |
text={object.text}
|
| 129 |
fontSize={displayFontSize}
|
| 130 |
fontFamily={object.fontFamily}
|
|
@@ -52,7 +52,7 @@ export const LAYOUTS: Record<string, Layout> = {
|
|
| 52 |
{
|
| 53 |
id: 'title-text',
|
| 54 |
type: 'text',
|
| 55 |
-
x:
|
| 56 |
y: 81.58,
|
| 57 |
width: 945.2583618164062,
|
| 58 |
height: 140,
|
|
@@ -65,7 +65,6 @@ export const LAYOUTS: Record<string, Layout> = {
|
|
| 65 |
bold: true,
|
| 66 |
italic: false,
|
| 67 |
align: 'center',
|
| 68 |
-
offsetX: 426,
|
| 69 |
},
|
| 70 |
{
|
| 71 |
id: 'logo-placeholder',
|
|
@@ -131,7 +130,7 @@ export const LAYOUTS: Record<string, Layout> = {
|
|
| 131 |
{
|
| 132 |
id: 'description-text',
|
| 133 |
type: 'text',
|
| 134 |
-
x:
|
| 135 |
y: 503.48,
|
| 136 |
width: 1015,
|
| 137 |
height: 107,
|
|
@@ -145,7 +144,6 @@ export const LAYOUTS: Record<string, Layout> = {
|
|
| 145 |
italic: false,
|
| 146 |
align: 'center',
|
| 147 |
isFixedSize: true,
|
| 148 |
-
offsetX: 507.5,
|
| 149 |
},
|
| 150 |
{
|
| 151 |
id: 'singing-huggy',
|
|
@@ -181,7 +179,7 @@ export const LAYOUTS: Record<string, Layout> = {
|
|
| 181 |
{
|
| 182 |
id: 'title-text',
|
| 183 |
type: 'text',
|
| 184 |
-
x:
|
| 185 |
y: 329.67,
|
| 186 |
width: 724.1570434570312,
|
| 187 |
height: 115.93,
|
|
@@ -195,13 +193,12 @@ export const LAYOUTS: Record<string, Layout> = {
|
|
| 195 |
italic: false,
|
| 196 |
align: 'center',
|
| 197 |
isFixedSize: true,
|
| 198 |
-
offsetX: 321.5,
|
| 199 |
fontWeight: 'black',
|
| 200 |
},
|
| 201 |
{
|
| 202 |
id: 'subtitle-text',
|
| 203 |
type: 'text',
|
| 204 |
-
x:
|
| 205 |
y: 441.535,
|
| 206 |
width: 440.6540222167969,
|
| 207 |
height: 63.93,
|
|
@@ -215,7 +212,6 @@ export const LAYOUTS: Record<string, Layout> = {
|
|
| 215 |
italic: false,
|
| 216 |
align: 'center',
|
| 217 |
isFixedSize: true,
|
| 218 |
-
offsetX: 210.5,
|
| 219 |
},
|
| 220 |
],
|
| 221 |
},
|
|
|
|
| 52 |
{
|
| 53 |
id: 'title-text',
|
| 54 |
type: 'text',
|
| 55 |
+
x: 124,
|
| 56 |
y: 81.58,
|
| 57 |
width: 945.2583618164062,
|
| 58 |
height: 140,
|
|
|
|
| 65 |
bold: true,
|
| 66 |
italic: false,
|
| 67 |
align: 'center',
|
|
|
|
| 68 |
},
|
| 69 |
{
|
| 70 |
id: 'logo-placeholder',
|
|
|
|
| 130 |
{
|
| 131 |
id: 'description-text',
|
| 132 |
type: 'text',
|
| 133 |
+
x: 92.5,
|
| 134 |
y: 503.48,
|
| 135 |
width: 1015,
|
| 136 |
height: 107,
|
|
|
|
| 144 |
italic: false,
|
| 145 |
align: 'center',
|
| 146 |
isFixedSize: true,
|
|
|
|
| 147 |
},
|
| 148 |
{
|
| 149 |
id: 'singing-huggy',
|
|
|
|
| 179 |
{
|
| 180 |
id: 'title-text',
|
| 181 |
type: 'text',
|
| 182 |
+
x: 237.9214782714844,
|
| 183 |
y: 329.67,
|
| 184 |
width: 724.1570434570312,
|
| 185 |
height: 115.93,
|
|
|
|
| 193 |
italic: false,
|
| 194 |
align: 'center',
|
| 195 |
isFixedSize: true,
|
|
|
|
| 196 |
fontWeight: 'black',
|
| 197 |
},
|
| 198 |
{
|
| 199 |
id: 'subtitle-text',
|
| 200 |
type: 'text',
|
| 201 |
+
x: 379.6729888916016,
|
| 202 |
y: 441.535,
|
| 203 |
width: 440.6540222167969,
|
| 204 |
height: 63.93,
|
|
|
|
| 212 |
italic: false,
|
| 213 |
align: 'center',
|
| 214 |
isFixedSize: true,
|
|
|
|
| 215 |
},
|
| 216 |
],
|
| 217 |
},
|
|
@@ -45,8 +45,6 @@ export interface TextObject extends CanvasObjectBase {
|
|
| 45 |
bold: boolean;
|
| 46 |
italic: boolean;
|
| 47 |
align?: 'left' | 'center' | 'right';
|
| 48 |
-
offsetX?: number; // Horizontal offset for centering or custom positioning
|
| 49 |
-
offsetY?: number; // Vertical offset for centering or custom positioning
|
| 50 |
isFixedSize?: boolean; // Track if text box size is fixed (Stage 2) or growing (Stage 1)
|
| 51 |
isEditing?: boolean; // Track if text is currently being edited
|
| 52 |
fontWeight?: 'normal' | 'bold' | 'black'; // Track font weight variation (normal = 400, bold = 700, black = 900)
|
|
|
|
| 45 |
bold: boolean;
|
| 46 |
italic: boolean;
|
| 47 |
align?: 'left' | 'center' | 'right';
|
|
|
|
|
|
|
| 48 |
isFixedSize?: boolean; // Track if text box size is fixed (Stage 2) or growing (Stage 1)
|
| 49 |
isEditing?: boolean; // Track if text is currently being edited
|
| 50 |
fontWeight?: 'normal' | 'bold' | 'black'; // Track font weight variation (normal = 400, bold = 700, black = 900)
|
|
@@ -39,8 +39,7 @@ export function exportCanvasAsLayout(
|
|
| 39 |
lines.push(`${indent} italic: ${obj.italic},`);
|
| 40 |
if (obj.align) lines.push(`${indent} align: '${obj.align}',`);
|
| 41 |
if (obj.isFixedSize) lines.push(`${indent} isFixedSize: ${obj.isFixedSize},`);
|
| 42 |
-
if (obj.
|
| 43 |
-
if (obj.offsetY !== undefined) lines.push(`${indent} offsetY: ${obj.offsetY},`);
|
| 44 |
} else if (obj.type === 'image' || obj.type === 'huggy') {
|
| 45 |
lines.push(`${indent} src: '${obj.src}',`);
|
| 46 |
lines.push(`${indent} name: '${obj.name || 'Untitled'}',`);
|
|
@@ -88,10 +87,7 @@ export function logCanvasObjects(objects: CanvasObject[]): void {
|
|
| 88 |
console.log('Text:', obj.text);
|
| 89 |
console.log('Font:', `${obj.fontSize}px ${obj.fontFamily}`);
|
| 90 |
console.log('Color:', obj.fill);
|
| 91 |
-
console.log('Styles:', { bold: obj.bold, italic: obj.italic });
|
| 92 |
-
if (obj.offsetX !== undefined || obj.offsetY !== undefined) {
|
| 93 |
-
console.log('Offset:', { offsetX: obj.offsetX, offsetY: obj.offsetY });
|
| 94 |
-
}
|
| 95 |
} else if (obj.type === 'image' || obj.type === 'huggy') {
|
| 96 |
console.log('Source:', obj.src);
|
| 97 |
console.log('Name:', obj.name);
|
|
|
|
| 39 |
lines.push(`${indent} italic: ${obj.italic},`);
|
| 40 |
if (obj.align) lines.push(`${indent} align: '${obj.align}',`);
|
| 41 |
if (obj.isFixedSize) lines.push(`${indent} isFixedSize: ${obj.isFixedSize},`);
|
| 42 |
+
if (obj.fontWeight && obj.fontWeight !== 'normal') lines.push(`${indent} fontWeight: '${obj.fontWeight}',`);
|
|
|
|
| 43 |
} else if (obj.type === 'image' || obj.type === 'huggy') {
|
| 44 |
lines.push(`${indent} src: '${obj.src}',`);
|
| 45 |
lines.push(`${indent} name: '${obj.name || 'Untitled'}',`);
|
|
|
|
| 87 |
console.log('Text:', obj.text);
|
| 88 |
console.log('Font:', `${obj.fontSize}px ${obj.fontFamily}`);
|
| 89 |
console.log('Color:', obj.fill);
|
| 90 |
+
console.log('Styles:', { bold: obj.bold, italic: obj.italic, fontWeight: obj.fontWeight });
|
|
|
|
|
|
|
|
|
|
| 91 |
} else if (obj.type === 'image' || obj.type === 'huggy') {
|
| 92 |
console.log('Source:', obj.src);
|
| 93 |
console.log('Name:', obj.name);
|