Skip to content

Commit cb97039

Browse files
feat(export): add export functionality for canvas as PNG, SVG, and PDF
1 parent 93c7014 commit cb97039

File tree

4 files changed

+104
-3
lines changed

4 files changed

+104
-3
lines changed

client/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@radix-ui/react-slider": "^1.3.6",
1515
"@tailwindcss/vite": "^4.1.14",
1616
"axios": "^1.12.2",
17+
"canvas2svg": "^1.0.16",
1718
"class-variance-authority": "^0.7.1",
1819
"clsx": "^2.1.1",
1920
"jspdf": "^3.0.3",

client/src/components/Canvas.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import { ColorPicker } from "./ColorPicker";
44
import { StrokeControl } from "./StrokeControl";
55
import { toast } from "sonner";
66
import { io } from "socket.io-client";
7+
<<<<<<< Updated upstream
8+
=======
9+
import tinycolor from "tinycolor2";
10+
import { jsPDF } from "jspdf";
11+
import C2S from "canvas2svg";
12+
>>>>>>> Stashed changes
713

814
export const Canvas = () => {
915
const canvasRef = useRef(null);
@@ -453,6 +459,48 @@ export const Canvas = () => {
453459
return "cursor-crosshair";
454460
};
455461

462+
const handleExport = (format) => {
463+
const canvas = canvasRef.current;
464+
if (!canvas) return;
465+
466+
switch (format) {
467+
case "png": {
468+
const link = document.createElement("a");
469+
link.download = "canvas.png";
470+
link.href = canvas.toDataURL("image/png");
471+
link.click();
472+
break;
473+
}
474+
475+
case "pdf": {
476+
const pdf = new jsPDF({
477+
orientation: "landscape",
478+
unit: "px",
479+
format: [canvas.width, canvas.height],
480+
});
481+
const imgData = canvas.toDataURL("image/png");
482+
pdf.addImage(imgData, "PNG", 0, 0, canvas.width, canvas.height);
483+
pdf.save("canvas.pdf");
484+
break;
485+
}
486+
487+
case "svg": {
488+
const svgCtx = new C2S(canvas.width, canvas.height);
489+
const svgData = svgCtx.getSerializedSvg();
490+
const blob = new Blob([svgData], { type: "image/svg+xml" });
491+
const link = document.createElement("a");
492+
link.download = "canvas.svg";
493+
link.href = URL.createObjectURL(blob);
494+
link.click();
495+
break;
496+
}
497+
498+
default:
499+
console.warn("Unsupported format:", format);
500+
}
501+
};
502+
503+
456504
return (
457505
<div className="relative w-full h-screen overflow-hidden bg-canvas">
458506
{/* 🔹 Login / Logout buttons (Unchanged) */}
@@ -511,6 +559,7 @@ export const Canvas = () => {
511559
activeTool={activeTool}
512560
onToolChange={handleToolChange}
513561
onClear={handleClear}
562+
onExport={handleExport}
514563
/>
515564
{joined ? (
516565
<button

client/src/components/Toolbar.jsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,50 @@ export const Toolbar = ({ activeTool, onToolChange, onClear, onExport }) => {
2323
{ type: "rectangle", icon: Square },
2424
{ type: "circle", icon: Circle },
2525
{ type: "line", icon: Minus },
26+
<<<<<<< Updated upstream
27+
=======
28+
29+
];
30+
31+
const handleExport = (format) => {
32+
onExport(format);
33+
}
34+
35+
const brushTypes = [
36+
{
37+
id: "solid",
38+
label: "Solid",
39+
preview: <div className="w-10 h-1 bg-black rounded-full"></div>,
40+
},
41+
{
42+
id: "dashed",
43+
label: "Dashed",
44+
preview: (
45+
<div className="w-10 h-1 border-b-2 border-dashed border-black"></div>
46+
),
47+
},
48+
{
49+
id: "paint",
50+
label: "Paint",
51+
preview: (
52+
<div className="w-10 h-2 bg-gradient-to-r from-blue-500 to-pink-500 rounded-full opacity-80 blur-[1px]"></div>
53+
),
54+
},
55+
{
56+
id: "crayon",
57+
label: "Crayon",
58+
preview: (
59+
<div className="w-10 h-1 bg-black/70 rounded-sm shadow-[0_0_3px_1px_rgba(0,0,0,0.3)]"></div>
60+
),
61+
},
62+
{
63+
id: "oil-pastel",
64+
label: "Oil Pastel",
65+
preview: (
66+
<div className="w-10 h-2 bg-gradient-to-r from-yellow-400 via-red-400 to-purple-400 rounded-full blur-[0.5px] opacity-90"></div>
67+
),
68+
},
69+
>>>>>>> Stashed changes
2670
];
2771

2872
return (
@@ -75,15 +119,15 @@ export const Toolbar = ({ activeTool, onToolChange, onClear, onExport }) => {
75119
</Button>
76120
}
77121
>
78-
<DropdownMenuItem onClick={() => onExport("png")}>
122+
<DropdownMenuItem onClick={() => handleExport("png")}>
79123
<FileImage className="h-4 w-4" />
80124
Export as PNG
81125
</DropdownMenuItem>
82-
<DropdownMenuItem onClick={() => onExport("svg")}>
126+
<DropdownMenuItem onClick={() => handleExport("svg")}>
83127
<FileType className="h-4 w-4" />
84128
Export as SVG
85129
</DropdownMenuItem>
86-
<DropdownMenuItem onClick={() => onExport("pdf")}>
130+
<DropdownMenuItem onClick={() => handleExport("pdf")}>
87131
<FileType className="h-4 w-4" />
88132
Export as PDF
89133
</DropdownMenuItem>

0 commit comments

Comments
 (0)