Skip to content

Commit 3b4291f

Browse files
committed
feat: Add draw text
1 parent cc25e7b commit 3b4291f

File tree

11 files changed

+688
-200
lines changed

11 files changed

+688
-200
lines changed

apps/collabydraw/canvas-engine/CanvasEngine.ts

Lines changed: 264 additions & 43 deletions
Large diffs are not rendered by default.

apps/collabydraw/canvas-engine/SelectionController.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Shape } from "@/types/canvas";
2+
import { getFontSize } from "@/utils/textUtils";
23

34
type Tool = Shape;
45

@@ -122,6 +123,16 @@ export class SelectionController {
122123
bounds.width = maxX - minX + shape.strokeWidth * 2 + 40;
123124
bounds.height = maxY - minY + shape.strokeWidth * 2 + 40;
124125
break;
126+
127+
case "text":
128+
const calFontSize = getFontSize(shape.fontSize, 100);
129+
this.ctx.font = `${calFontSize}px/1.2 ${shape.fontFamily === "normal" ? "Arial" : shape.fontFamily === "hand-drawn" ? "Excalifont, Xiaolai" : "Assistant"}`;
130+
const metrics = this.ctx.measureText(shape.text || "");
131+
bounds.x = shape.x - 10;
132+
bounds.y = shape.y - 10;
133+
bounds.width = metrics.width + 20;
134+
bounds.height = 48;
135+
break;
125136
}
126137

127138
return bounds;

apps/collabydraw/components/StyleConfigurator.tsx

Lines changed: 158 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"use client"
22

33
import type React from "react"
4-
import { BgFill, FillStyle, RoughStyle, StrokeEdge, StrokeFill, StrokeStyle, StrokeWidth, ToolType } from "@/types/canvas"
4+
import { cn } from "@/lib/utils";
5+
import { BgFill, FillStyle, FontFamily, FontSize, RoughStyle, StrokeEdge, StrokeFill, StrokeStyle, StrokeWidth, TextAlign, ToolType } from "@/types/canvas"
6+
import { fillStyleIcons, fillStyleLabels, fontFamilyIcons, fontFamilyLabels, fontSizeIcons, fontSizeLabels, roughStyleIcons, roughStyleLabels, strokeEdgeIcons, strokeEdgeLabels, strokeStyleIcons, strokeStyleLabels, textAlignIcons, textAlignLabels } from "@/config/canvasTypeMappings";
7+
import { Input } from "./ui/input";
58
import { ColorBoard } from "./color-board"
69
import ItemLabel from "./ItemLabel";
7-
import { Input } from "./ui/input";
8-
import { cn } from "@/lib/utils";
9-
import { fillStyleIcons, fillStyleLabels, roughStyleIcons, roughStyleLabels, strokeEdgeIcons, strokeEdgeLabels, strokeStyleIcons, strokeStyleLabels } from "@/config/canvasTypeMappings";
1010

1111
interface StyleConfiguratorProps {
1212
activeTool: ToolType;
@@ -24,6 +24,12 @@ interface StyleConfiguratorProps {
2424
setRoughStyle: React.Dispatch<React.SetStateAction<RoughStyle>>;
2525
fillStyle: FillStyle;
2626
setFillStyle: React.Dispatch<React.SetStateAction<FillStyle>>;
27+
fontFamily: FontFamily;
28+
setFontFamily: React.Dispatch<React.SetStateAction<FontFamily>>;
29+
fontSize: FontSize;
30+
setFontSize: React.Dispatch<React.SetStateAction<FontSize>>;
31+
textAlign: TextAlign;
32+
setTextAlign: React.Dispatch<React.SetStateAction<TextAlign>>;
2733
isMobile?: boolean
2834
}
2935

@@ -43,6 +49,12 @@ export function StyleConfigurator({
4349
setRoughStyle,
4450
fillStyle,
4551
setFillStyle,
52+
fontFamily,
53+
setFontFamily,
54+
fontSize,
55+
setFontSize,
56+
textAlign,
57+
setTextAlign,
4658
isMobile
4759
}: StyleConfiguratorProps) {
4860

@@ -51,6 +63,9 @@ export function StyleConfigurator({
5163
const edgeStyleOptions: StrokeStyle[] = ["solid", "dashed", "dotted"]
5264
const roughStyleOptions: RoughStyle[] = [0, 1, 2]
5365
const fillStyleOptions: FillStyle[] = ['hachure', 'cross-hatch', 'dashed', 'dots', 'zigzag', 'zigzag-line', 'solid']
66+
const fontFamilyOptions: FontFamily[] = ['hand-drawn', 'normal', 'code']
67+
const fontSizeOptions: FontSize[] = ['Small', 'Medium', 'Large']
68+
const textAlignOptions: TextAlign[] = ['left', 'center', 'right']
5469

5570
if (activeTool === "eraser" || activeTool === "grab" || activeTool === "selection") {
5671
return;
@@ -61,7 +76,7 @@ export function StyleConfigurator({
6176
isMobile ? "" : "absolute top-full w-56 h-[calc(100vh-150px)] bg-background dark:bg-w-bg rounded-lg Island"
6277
)}>
6378
<h2 className="sr-only">Selected shape actions</h2>
64-
<div className="flex flex-col gap-y-3">
79+
<div className="ColorBoard flex flex-col gap-y-3">
6580
<ColorBoard
6681
mode="Shape"
6782
bgFill={bgFill}
@@ -71,22 +86,8 @@ export function StyleConfigurator({
7186
activeTool={activeTool}
7287
/>
7388

74-
<div className="">
75-
<ItemLabel label="Stroke width" />
76-
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
77-
{lineThicknessOptions.map((sw, index) => (
78-
<StrokeWidthSelector
79-
key={index}
80-
strokeWidth={strokeWidth}
81-
strokeWidthProp={sw}
82-
onClick={() => setStrokeWidth(sw)}
83-
/>
84-
))}
85-
</div>
86-
</div>
87-
88-
{(activeTool === "rectangle" || activeTool === "diamond" || activeTool === 'ellipse') && (
89-
<div className="">
89+
{(activeTool === "rectangle" || activeTool === 'ellipse' || activeTool === "diamond" || activeTool === 'line' || activeTool === 'pen') && (
90+
<div className="Fill-Style-Selector">
9091
<ItemLabel label="Fill" />
9192
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
9293
{fillStyleOptions.map((fs, index) => (
@@ -101,8 +102,24 @@ export function StyleConfigurator({
101102
</div>
102103
)}
103104

105+
{(activeTool !== "text") && (
106+
<div className="Stroke-Width-Selector">
107+
<ItemLabel label="Stroke width" />
108+
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
109+
{lineThicknessOptions.map((sw, index) => (
110+
<StrokeWidthSelector
111+
key={index}
112+
strokeWidth={strokeWidth}
113+
strokeWidthProp={sw}
114+
onClick={() => setStrokeWidth(sw)}
115+
/>
116+
))}
117+
</div>
118+
</div>
119+
)}
120+
104121
{(activeTool === "rectangle" || activeTool === "diamond") && (
105-
<div className="">
122+
<div className="Edge-Style-Selector">
106123
<ItemLabel label="Edges" />
107124
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
108125
{edgeRoundnessOptions.map((sw, index) => (
@@ -117,39 +134,131 @@ export function StyleConfigurator({
117134
</div>
118135
)}
119136

120-
<div className="">
121-
<ItemLabel label="Sloppiness" />
122-
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
123-
{roughStyleOptions.map((rs, index) => (
124-
<RoughStyleSelector
125-
key={index}
126-
roughStyle={roughStyle}
127-
roughStyleProp={rs}
128-
onClick={() => setRoughStyle(rs)}
129-
/>
130-
))}
131-
</div>
132-
</div>
133-
134-
<div className="">
135-
<ItemLabel label="Stroke Style" />
136-
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
137-
{edgeStyleOptions.map((sw, index) => (
138-
<StrokeStyleSelector
139-
key={index}
140-
strokeStyle={strokeStyle}
141-
strokeStyleProp={sw}
142-
onClick={() => setStrokeStyle(sw)}
143-
/>
144-
))}
145-
</div>
146-
</div>
137+
{(activeTool !== "pen" && activeTool !== 'text') && (
138+
<>
139+
<div className="Rough-Style-Selector">
140+
<ItemLabel label="Sloppiness" />
141+
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
142+
{roughStyleOptions.map((rs, index) => (
143+
<RoughStyleSelector
144+
key={index}
145+
roughStyle={roughStyle}
146+
roughStyleProp={rs}
147+
onClick={() => setRoughStyle(rs)}
148+
/>
149+
))}
150+
</div>
151+
</div>
152+
153+
<div className="Stroke-Style-Selector">
154+
<ItemLabel label="Stroke Style" />
155+
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
156+
{edgeStyleOptions.map((sw, index) => (
157+
<StrokeStyleSelector
158+
key={index}
159+
strokeStyle={strokeStyle}
160+
strokeStyleProp={sw}
161+
onClick={() => setStrokeStyle(sw)}
162+
/>
163+
))}
164+
</div>
165+
</div>
166+
</>
167+
)}
168+
169+
{(activeTool === "text") && (
170+
<>
171+
<div className="Font-Family-Selector">
172+
<ItemLabel label="Font family" />
173+
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
174+
{fontFamilyOptions.map((ff, index) => (
175+
<FontFamilySelector
176+
key={index}
177+
fontFamily={fontFamily}
178+
fontFamilyProp={ff}
179+
onClick={() => setFontFamily(ff)}
180+
/>
181+
))}
182+
</div>
183+
</div>
184+
185+
<div className="Font-Size-Selector">
186+
<ItemLabel label="Font size" />
187+
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
188+
{fontSizeOptions.map((fs, index) => (
189+
<FontSizeSelector
190+
key={index}
191+
fontSize={fontSize}
192+
fontSizeProp={fs}
193+
onClick={() => setFontSize(fs)}
194+
/>
195+
))}
196+
</div>
197+
</div>
198+
199+
<div className="Text-Align-Selector">
200+
<ItemLabel label="Text align" />
201+
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
202+
{textAlignOptions.map((a, index) => (
203+
<TextAlignSelector
204+
key={index}
205+
textAlign={textAlign}
206+
textAlignProp={a}
207+
onClick={() => setTextAlign(a)}
208+
/>
209+
))}
210+
</div>
211+
</div>
212+
</>
213+
)}
147214
</div>
148215
</section>
149216
</>
150217
)
151218
}
152219

220+
const TextAlignSelector = ({ textAlign, textAlignProp, onClick }: { textAlign: TextAlign, textAlignProp: TextAlign, onClick?: () => void }) => {
221+
return (
222+
<label className={cn("active flex justify-center items-center w-8 h-8 p-0 box-border border border-default-border-color rounded-lg cursor-pointer bg-light-btn-bg2 text-text-primary-color dark:bg-w-button-hover-bg dark:hover:bg-tool-btn-bg-hover-dark dark:text-text-primary-color dark:border-w-button-hover-bg focus-within:shadow-shadow-tool-focus",
223+
textAlign === textAlignProp ? 'bg-selected-tool-bg-light dark:bg-selected-tool-bg-dark dark:border-selected-tool-bg-dark' : ''
224+
)}
225+
title={textAlignLabels[textAlignProp]}
226+
onClick={onClick}
227+
>
228+
<Input type="radio" checked={textAlign === textAlignProp} onChange={() => onClick?.()} name="textAlign" className="opacity-0 absolute pointer-events-none" />
229+
{textAlignIcons[textAlignProp]}
230+
</label>
231+
)
232+
}
233+
234+
const FontSizeSelector = ({ fontSize, fontSizeProp, onClick }: { fontSize: FontSize, fontSizeProp: FontSize, onClick?: () => void }) => {
235+
return (
236+
<label className={cn("active flex justify-center items-center w-8 h-8 p-0 box-border border border-default-border-color rounded-lg cursor-pointer bg-light-btn-bg2 text-text-primary-color dark:bg-w-button-hover-bg dark:hover:bg-tool-btn-bg-hover-dark dark:text-text-primary-color dark:border-w-button-hover-bg focus-within:shadow-shadow-tool-focus",
237+
fontSize === fontSizeProp ? 'bg-selected-tool-bg-light dark:bg-selected-tool-bg-dark dark:border-selected-tool-bg-dark' : ''
238+
)}
239+
title={fontSizeLabels[fontSizeProp]}
240+
onClick={onClick}
241+
>
242+
<Input type="radio" checked={fontSize === fontSizeProp} onChange={() => onClick?.()} name="fontSize" className="opacity-0 absolute pointer-events-none" />
243+
{fontSizeIcons[fontSizeProp]}
244+
</label>
245+
)
246+
}
247+
248+
const FontFamilySelector = ({ fontFamily, fontFamilyProp, onClick }: { fontFamily: FontFamily, fontFamilyProp: FontFamily, onClick?: () => void }) => {
249+
return (
250+
<label className={cn("active flex justify-center items-center w-8 h-8 p-0 box-border border border-default-border-color rounded-lg cursor-pointer bg-light-btn-bg2 text-text-primary-color dark:bg-w-button-hover-bg dark:hover:bg-tool-btn-bg-hover-dark dark:text-text-primary-color dark:border-w-button-hover-bg focus-within:shadow-shadow-tool-focus",
251+
fontFamily === fontFamilyProp ? 'bg-selected-tool-bg-light dark:bg-selected-tool-bg-dark dark:border-selected-tool-bg-dark' : ''
252+
)}
253+
title={fontFamilyLabels[fontFamilyProp]}
254+
onClick={onClick}
255+
>
256+
<Input type="radio" checked={fontFamily === fontFamilyProp} onChange={() => onClick?.()} name="fontFamily" className="opacity-0 absolute pointer-events-none" />
257+
{fontFamilyIcons[fontFamilyProp]}
258+
</label>
259+
)
260+
}
261+
153262
const StrokeWidthSelector = ({ strokeWidth, strokeWidthProp, onClick }: { strokeWidth: StrokeWidth, strokeWidthProp: StrokeWidth, onClick?: () => void }) => {
154263
return (
155264
<label className={cn("active flex justify-center items-center w-8 h-8 p-0 box-border border border-default-border-color rounded-lg cursor-pointer bg-light-btn-bg2 text-text-primary-color dark:bg-w-button-hover-bg dark:hover:bg-tool-btn-bg-hover-dark dark:text-text-primary-color dark:border-w-button-hover-bg focus-within:shadow-shadow-tool-focus",

apps/collabydraw/components/SvgIcons.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,19 @@ export function ListBullet({ className }: { className: string }) {
2222
<path fillRule="evenodd" d="M2.625 6.75a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Zm4.875 0A.75.75 0 0 1 8.25 6h12a.75.75 0 0 1 0 1.5h-12a.75.75 0 0 1-.75-.75ZM2.625 12a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0ZM7.5 12a.75.75 0 0 1 .75-.75h12a.75.75 0 0 1 0 1.5h-12A.75.75 0 0 1 7.5 12Zm-4.875 5.25a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Zm4.875 0a.75.75 0 0 1 .75-.75h12a.75.75 0 0 1 0 1.5h-12a.75.75 0 0 1-.75-.75Z" clipRule="evenodd" />
2323
</svg>
2424
)
25+
}
26+
27+
export function TextIcon({ className }: { className: string }) {
28+
return (
29+
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 24 24" className={cn("size-6", className)} fill="none" strokeWidth="2" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
30+
<g strokeWidth="1.5">
31+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
32+
<line x1="4" y1="20" x2="7" y2="20"></line>
33+
<line x1="14" y1="20" x2="21" y2="20"></line>
34+
<line x1="6.9" y1="15" x2="13.8" y2="15"></line>
35+
<line x1="10.2" y1="6.3" x2="16" y2="20"></line>
36+
<polyline points="5 20 11 4 13 4 20 20"></polyline>
37+
</g>
38+
</svg>
39+
)
2540
}

0 commit comments

Comments
 (0)