Add draggable curved text support
- Wrap TextPath in Group container to enable dragging of curved text
Previously curved text couldn't be moved on canvas. Now users can:
• Add curved text
• Move it by dragging
• Update position is saved correctly
- Confirm curved text is per-element: Each text element has its own
curvedPath property, so you can mix curved and straight text on the
same design (Font 1: circle, Font 2: wave, Font 3: straight, etc.)
- TextPath uses listening={false} since Group handles all interactions
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
859956b9d8
commit
af4f21938d
+166
-30
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState, useEffect, useLayoutEffect, useCallback } from 'react';
|
||||
import { Stage, Layer, Text, Image as KonvaImage, Transformer, Rect } from 'react-konva';
|
||||
import { Stage, Layer, Text, TextPath, Image as KonvaImage, Transformer, Rect, Group } from 'react-konva';
|
||||
import type Konva from 'konva';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ProductMockup from './ProductMockup';
|
||||
@@ -324,35 +324,62 @@ function PrintSurface({
|
||||
/>
|
||||
{elements.map((el) =>
|
||||
el.type === 'text' ? (
|
||||
<Text
|
||||
key={el.id}
|
||||
ref={(node) => {
|
||||
if (node) nodeRefs.current.set(el.id, node);
|
||||
}}
|
||||
x={el.x}
|
||||
y={el.y}
|
||||
text={el.text}
|
||||
fontFamily={el.fontFamily}
|
||||
fontSize={el.fontSize}
|
||||
fill={el.fill}
|
||||
rotation={el.rotation}
|
||||
draggable
|
||||
listening={true}
|
||||
onClick={() => setSelectedId(el.id)}
|
||||
onTap={() => setSelectedId(el.id)}
|
||||
onDragEnd={(e) => updateElement(el.id, { x: e.target.x(), y: e.target.y() })}
|
||||
onTransformEnd={(e) => {
|
||||
const node = e.target;
|
||||
updateElement(el.id, {
|
||||
x: node.x(),
|
||||
y: node.y(),
|
||||
rotation: node.rotation(),
|
||||
fontSize: Math.max(8, el.fontSize * node.scaleX()),
|
||||
});
|
||||
node.scaleX(1);
|
||||
node.scaleY(1);
|
||||
}}
|
||||
/>
|
||||
el.curvedPath ? (
|
||||
// Curved text path wrapped in a draggable Group
|
||||
<Group
|
||||
key={el.id}
|
||||
ref={(node) => {
|
||||
if (node) nodeRefs.current.set(el.id, node);
|
||||
}}
|
||||
x={el.x}
|
||||
y={el.y}
|
||||
draggable
|
||||
listening={true}
|
||||
onClick={() => setSelectedId(el.id)}
|
||||
onTap={() => setSelectedId(el.id)}
|
||||
onDragEnd={(e) => updateElement(el.id, { x: e.target.x(), y: e.target.y() })}
|
||||
>
|
||||
<TextPath
|
||||
data={generateCurvePath(el.curvedPath.type, el.curvedPath.radius, el.curvedPath.reverse)}
|
||||
text={el.text}
|
||||
fontFamily={el.fontFamily}
|
||||
fontSize={el.fontSize}
|
||||
fill={el.fill}
|
||||
listening={false}
|
||||
/>
|
||||
</Group>
|
||||
) : (
|
||||
// Regular straight text
|
||||
<Text
|
||||
key={el.id}
|
||||
ref={(node) => {
|
||||
if (node) nodeRefs.current.set(el.id, node);
|
||||
}}
|
||||
x={el.x}
|
||||
y={el.y}
|
||||
text={el.text}
|
||||
fontFamily={el.fontFamily}
|
||||
fontSize={el.fontSize}
|
||||
fill={el.fill}
|
||||
rotation={el.rotation}
|
||||
draggable
|
||||
listening={true}
|
||||
onClick={() => setSelectedId(el.id)}
|
||||
onTap={() => setSelectedId(el.id)}
|
||||
onDragEnd={(e) => updateElement(el.id, { x: e.target.x(), y: e.target.y() })}
|
||||
onTransformEnd={(e) => {
|
||||
const node = e.target;
|
||||
updateElement(el.id, {
|
||||
x: node.x(),
|
||||
y: node.y(),
|
||||
rotation: node.rotation(),
|
||||
fontSize: Math.max(8, el.fontSize * node.scaleX()),
|
||||
});
|
||||
node.scaleX(1);
|
||||
node.scaleY(1);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<UploadedImage
|
||||
key={el.id}
|
||||
@@ -391,6 +418,59 @@ function PrintSurface({
|
||||
);
|
||||
}
|
||||
|
||||
// Generate SVG path for curved text based on type and radius
|
||||
function generateCurvePath(type: string, radius: number, reverse: boolean = false): string {
|
||||
const cx = 280; // center x (middle of 560px canvas)
|
||||
const cy = 280; // center y
|
||||
const r = radius;
|
||||
|
||||
let path = '';
|
||||
|
||||
switch (type) {
|
||||
case 'circle':
|
||||
// Full circle arc from bottom-left to top-right
|
||||
const large = 0; // arc flag
|
||||
const sweep = reverse ? 0 : 1;
|
||||
path = `M ${cx - r} ${cy} A ${r} ${r} 0 ${large} ${sweep} ${cx + r} ${cy}`;
|
||||
break;
|
||||
|
||||
case 'wave':
|
||||
// Wave pattern
|
||||
const waveHeight = radius / 3;
|
||||
const waveLength = radius * 1.5;
|
||||
path = `M ${cx - waveLength} ${cy}`;
|
||||
for (let x = cx - waveLength; x <= cx + waveLength; x += 10) {
|
||||
const y = cy + Math.sin((x - (cx - waveLength)) / waveLength * Math.PI * 2) * waveHeight;
|
||||
path += ` L ${x} ${y}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'spiral':
|
||||
// Spiral arc
|
||||
const spiralTurns = 1.5;
|
||||
const spiralSteps = 100;
|
||||
path = `M ${cx} ${cy - radius}`;
|
||||
for (let i = 1; i <= spiralSteps; i++) {
|
||||
const angle = (i / spiralSteps) * Math.PI * 2 * spiralTurns;
|
||||
const r = radius * (1 - i / spiralSteps);
|
||||
const x = cx + r * Math.cos(angle);
|
||||
const y = cy - r * Math.sin(angle);
|
||||
path += ` L ${x} ${y}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'arc':
|
||||
// Simple arc
|
||||
path = `M ${cx - radius} ${cy} A ${radius} ${radius} 0 0 ${reverse ? 0 : 1} ${cx + radius} ${cy}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
path = `M 0 ${cy} L 560 ${cy}`; // fallback: straight line
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
const [color, setColor] = useState(product.colors[0]);
|
||||
const [size, setSize] = useState<string | null>(product.sizes[0] ?? null);
|
||||
@@ -1199,6 +1279,62 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
||||
<span className="w-12 text-right font-mono text-xs">{text.rotation || 0}°</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Curved Text Controls */}
|
||||
<div className="border-t border-line pt-3">
|
||||
<label className="flex items-center gap-2 text-sm text-muted mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={text.curvedPath ? true : false}
|
||||
onChange={(e) => updateElement(text.id, { curvedPath: e.target.checked ? { type: 'circle', radius: 80 } : null })}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
Curved text
|
||||
</label>
|
||||
|
||||
{text.curvedPath && (
|
||||
<div className="space-y-3">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted">Curve type</span>
|
||||
<select
|
||||
value={text.curvedPath.type || 'circle'}
|
||||
onChange={(e) => updateElement(text.id, { curvedPath: { ...text.curvedPath, type: e.target.value as any } })}
|
||||
className="border border-line bg-paper px-2 py-1 text-xs"
|
||||
>
|
||||
<option value="circle">Circle</option>
|
||||
<option value="wave">Wave</option>
|
||||
<option value="spiral">Spiral</option>
|
||||
<option value="arc">Arc</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted">Radius / Intensity</span>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="range"
|
||||
min={20}
|
||||
max={200}
|
||||
value={text.curvedPath.radius || 80}
|
||||
onChange={(e) => updateElement(text.id, { curvedPath: { ...text.curvedPath, radius: Number(e.target.value) } })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-right font-mono text-xs">{text.curvedPath.radius || 80}</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={text.curvedPath.reverse ? true : false}
|
||||
onChange={(e) => updateElement(text.id, { curvedPath: { ...text.curvedPath, reverse: e.target.checked } })}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="text-muted">Reverse direction</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user