Skip to content

Commit

Permalink
feat: save/load drawings
Browse files Browse the repository at this point in the history
- Added favicon
- Added some confirm/alert prompts for actions
  • Loading branch information
egilsster committed Jan 14, 2024
1 parent 9c7b91a commit 04cf6b1
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 33 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
bun-version: latest
- run: bun install
- run: bun check
- run: bun test
- run: bun run build
- name: Upload Pages Artifact
uses: actions/upload-pages-artifact@v2
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
preload = "./happydom.ts"
3 changes: 3 additions & 0 deletions happydom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GlobalRegistrator } from "@happy-dom/global-registrator";

GlobalRegistrator.register();
157 changes: 139 additions & 18 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,42 @@

<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />

<link href="img/favicon.ico" rel="shortcut icon" />
<link href="img/apple-touch-icon.png" rel="apple-touch-icon-precomposed" />

<title>Canvas</title>
</head>

<body>
<main id="app-root" class="hidden h-screen w-screen md:flex flex-col">
<dialog
id="load-canvas-modal"
class="backdrop-blur-sm shadow-lg ring-1 ring-slate-700/25 pt-2 pb-4 px-4 w-[450px] h-[400px] overflow-hidden rounded-sm top-1/4"
>
<div class="flex flex-col h-full">
<div
class="border-b pb-2 flex flex-row justify-between items-center m-0.5 mb-2"
>
<b class="text-gray-500">Load a drawing</b>
<form method="dialog">
<button
title="Close"
class="text-xl text-gray-500 h-5 w-5 flex justify-center items-center"
>
&times;
</button>
</form>
</div>

<ul
id="saved-list"
class="w-full grid grid-cols-1 items-start content-start overflow-scroll h-full"
>
<!-- `li` items go here -->
</ul>
</div>
</dialog>

<header
class="flex flex-row basis-14 w-full bg-neutral-50 border-b border-gray-300 items-center justify-between py-2 px-2"
>
Expand Down Expand Up @@ -58,25 +89,115 @@
</div>
</div>

<div id="action-buttons" class="text-white">
<button
class="transition-colors rounded-sm bg-blue-600 hover:bg-blue-500 active:bg-blue-700 font-semibold py-1 px-3 h-8"
id="undo"
>
<a href="#">Undo</a>
</button>
<button
class="transition-colors rounded-sm bg-blue-600 hover:bg-blue-500 active:bg-blue-700 font-semibold py-1 px-3 h-8"
id="redo"
>
<a href="#">Redo</a>
</button>
<button
class="transition-colors rounded-sm bg-red-600 hover:bg-red-500 active:bg-red-700 font-semibold py-1 px-3 h-8"
id="clear-canvas"
<div id="action-buttons" class="text-white flex flex-row">
<div
class="grid grid-cols-5 divide-x-2 divide-white rounded-sm overflow-hidden h-8"
>
<a href="#">Clear</a>
</button>
<button
class="transition-colors bg-red-600 hover:bg-red-500 active:bg-red-700 font-semibold p-1"
id="clear-canvas"
title="Clear canvas"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.49997 12.8995C2.71892 13.6805 2.71892 14.9468 3.49997 15.7279L7.35785 19.5858H4.08576C3.53347 19.5858 3.08576 20.0335 3.08576 20.5858C3.08576 21.1381 3.53347 21.5858 4.08576 21.5858H20.0858C20.638 21.5858 21.0858 21.1381 21.0858 20.5858C21.0858 20.0335 20.638 19.5858 20.0858 19.5858H10.9558L20.4705 10.071C21.2516 9.28999 21.2516 8.02366 20.4705 7.24261L16.2279 2.99997C15.4468 2.21892 14.1805 2.21892 13.3995 2.99997L3.49997 12.8995ZM7.82579 11.4021L4.91418 14.3137L9.15683 18.5563L12.0684 15.6447L7.82579 11.4021ZM9.24 9.98787L13.4826 14.2305L19.0563 8.65683L14.8137 4.41418L9.24 9.98787Z"
fill="currentColor"
/>
</svg>
</button>

<button
class="transition-colors bg-blue-600 hover:bg-blue-500 active:bg-blue-700 font-semibold p-1"
id="undo"
title="Undo last change"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.6276 14.7219L9.21641 16.1391L2.83875 9.78892L9.18897 3.41125L10.6062 4.82242L6.82971 8.61526L17.1353 8.59304C19.3445 8.58828 21.1392 10.3753 21.144 12.5844L21.1612 20.5844L19.1612 20.5887L19.144 12.5887C19.1416 11.4841 18.2442 10.5907 17.1396 10.593L6.50391 10.616L10.6276 14.7219Z"
fill="currentColor"
/>
</svg>
</button>

<button
class="transition-colors bg-blue-600 hover:bg-blue-500 active:bg-blue-700 font-semibold p-1"
id="redo"
title="Redo last change"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.3724 14.7219L14.7835 16.1391L21.1612 9.78892L14.811 3.41125L13.3937 4.82242L17.1702 8.61526L6.86461 8.59304C4.65547 8.58828 2.86076 10.3753 2.85599 12.5844L2.83875 20.5844L4.83874 20.5887L4.85599 12.5887C4.85837 11.4841 5.75573 10.5907 6.8603 10.593L17.496 10.616L13.3724 14.7219Z"
fill="currentColor"
/>
</svg>
</button>

<button
class="transition-colors bg-sky-600 hover:bg-sky-500 active:bg-sky-700 font-semibold p-1"
id="save-canvas"
title="Save drawing"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 18V16H8V14H10V12H12V14H14V16H12V18H10Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6 2C4.34315 2 3 3.34315 3 5V19C3 20.6569 4.34315 22 6 22H18C19.6569 22 21 20.6569 21 19V9C21 5.13401 17.866 2 14 2H6ZM6 4H13V9H19V19C19 19.5523 18.5523 20 18 20H6C5.44772 20 5 19.5523 5 19V5C5 4.44772 5.44772 4 6 4ZM15 4.10002C16.6113 4.4271 17.9413 5.52906 18.584 7H15V4.10002Z"
fill="currentColor"
/>
</svg>
</button>

<button
class="transition-colors bg-sky-600 hover:bg-sky-500 active:bg-sky-700 font-semibold p-1"
id="load-canvas"
title="Load saved drawings"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4 1.5C2.89543 1.5 2 2.39543 2 3.5V4.5C2 4.55666 2.00236 4.61278 2.00698 4.66825C0.838141 5.07811 0 6.19118 0 7.5V19.5C0 21.1569 1.34315 22.5 3 22.5H21C22.6569 22.5 24 21.1569 24 19.5V7.5C24 5.84315 22.6569 4.5 21 4.5H11.874C11.4299 2.77477 9.86384 1.5 8 1.5H4ZM9.73244 4.5C9.38663 3.9022 8.74028 3.5 8 3.5H4V4.5H9.73244ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V19.5C2 20.0523 2.44772 20.5 3 20.5H21C21.5523 20.5 22 20.0523 22 19.5V7.5C22 6.94772 21.5523 6.5 21 6.5H3Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
</header>

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"test": "bun test",
"format": "biome format --write .",
"check": "biome check .",
"fix": "biome check --apply ."
Expand All @@ -21,6 +22,8 @@
},
"devDependencies": {
"@biomejs/biome": "^1.5.1",
"@happy-dom/global-registrator": "^13.0.6",
"@types/bun": "^1.0.1",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
Expand Down
Binary file added public/img/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/favicon.ico
Binary file not shown.
142 changes: 136 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "./style.css";
import Canvas from "./utils/canvas";
import { getConfig } from "./utils/config";
import ResizeCanvas from "./utils/resizer";
import { clear, load, save } from "./utils/storage";

(() => {
const penShapeElement = document.querySelector(
Expand All @@ -22,6 +23,19 @@ import ResizeCanvas from "./utils/resizer";
const canvasElement = document.querySelector(
"#my-canvas",
) as HTMLCanvasElement;
const loadCanvasDialog = document.querySelector(
"#load-canvas-modal",
) as HTMLDialogElement;

const closeDialog = () => {
loadCanvasDialog.open = false;
};

loadCanvasDialog.onkeydown = (evt) => {
if (evt.key === "Escape") {
closeDialog();
}
};

const config = getConfig();

Expand All @@ -32,7 +46,7 @@ import ResizeCanvas from "./utils/resizer";
ResizeCanvas(canvasElement);
window.addEventListener("resize", () => ResizeCanvas(canvasElement));

const canvas: Canvas = new Canvas(canvasElement);
const canvas = new Canvas(canvasElement);

penColorElement.style.background = config.penColor;
new Picker({
Expand All @@ -54,15 +68,131 @@ import ResizeCanvas from "./utils/resizer";
canvas.penSize = Number(target?.value) || canvas.penSize;
});

document
.querySelector("#clear-canvas")
?.addEventListener("click", () => canvas.clearCanvas());
document.querySelector("#clear-canvas")?.addEventListener("click", () => {
const ok = confirm("Are you sure you want to clear everything?");
if (ok) {
canvas.clearCanvas();
}
});
document
.querySelector("#undo")
?.addEventListener("click", () => canvas.redoShape());
document
.querySelector("#redo")
?.addEventListener("click", () => canvas.undoShape());
document
.querySelector("#save-canvas")
?.addEventListener("click", async () => {
if (canvas.shapes.length === 0) {
alert("Maybe a bit too minimal.. draw something first.");
return;
}
const name = prompt("What's should your art be called?");
if (!name) {
return;
}
await save(name, canvas.shapes);
});
document
.querySelector("#load-canvas")
?.addEventListener("click", async () => {
// Open dialog and put it in focus
loadCanvasDialog.open = true;
loadCanvasDialog.focus();

const savedDrawingsUl = document.getElementById(
"saved-list",
) as HTMLUListElement;
savedDrawingsUl.textContent = "";

const savedEntries = Object.entries(await load());
if (savedEntries.length === 0) {
// todo
const placeholder = document.createElement("div");
placeholder.className =
"text-gray-500 flex w-full h-full justify-center mt-20";
placeholder.innerText = "Your drawings have been stolen!";
savedDrawingsUl.appendChild(placeholder);
} else {
for (const [name, { data, date }] of savedEntries) {
const listItem = document.createElement("li");
listItem.className =
"transition-colors flex justify-between cursor-pointer py-1 ring-1 ring-gray-500/25 m-0.5 px-1.5 rounded-sm hover:bg-gray-100 items-center";

const nameEl = document.createElement("span");
nameEl.className = "truncate w-1/2";
nameEl.textContent = name;
nameEl.title = `Load "${name}"`;

// YYYY-MM-DD, HH:MM:SS
const formattedDate = new Intl.DateTimeFormat("en-UK", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(new Date(date));

const dateEl = document.createElement("span");
dateEl.textContent = formattedDate;
dateEl.title = `Saved ${formattedDate}`;
dateEl.className = "text-gray-500 text-sm";

const deleteButton = document.createElement("button");
deleteButton.className =
"transition-colors text-red-600 hover:text-red-500 active:text-red-700 h-4 w-4 ml-2";
deleteButton.title = `Delete "${name}"`;
deleteButton.onclick = async (evt) => {
evt.stopPropagation();
await clear(name);
closeDialog();
};
deleteButton.innerHTML = `<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M17 5V4C17 2.89543 16.1046 2 15 2H9C7.89543 2 7 2.89543 7 4V5H4C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H5V18C5 19.6569 6.34315 21 8 21H16C17.6569 21 19 19.6569 19 18V7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H17ZM15 4H9V5H15V4ZM17 7H7V18C7 18.5523 7.44772 19 8 19H16C16.5523 19 17 18.5523 17 18V7Z"
fill="currentColor"
/>
<path d="M9 9H11V17H9V9Z" fill="currentColor" />
<path d="M13 9H15V17H13V9Z" fill="currentColor" />
</svg>`;

const rightSide = document.createElement("div");
rightSide.className = "flex flex-row items-center";

rightSide.appendChild(dateEl);
rightSide.appendChild(deleteButton);

listItem.appendChild(nameEl);
listItem.appendChild(rightSide);

listItem.onclick = () => {
canvas.clearCanvas();
canvas.drawLoaded(data);
closeDialog();
};

savedDrawingsUl.appendChild(listItem);
}
}
});

// Remove and clear each entry
document
.querySelector("#clear-saved")
?.addEventListener("click", async () => {
const ok = confirm("Are you sure you want to clear saved drawings?");
if (!ok) {
return;
}
await clear();
});

canvasElement.addEventListener("mousedown", (ev): void => {
const target = ev.target as HTMLCanvasElement | null;
Expand Down Expand Up @@ -118,7 +248,7 @@ import ResizeCanvas from "./utils/resizer";
document.querySelector(".text-spawner") as HTMLTextAreaElement
).addEventListener("keydown", (ev): void => {
if (canvas.isDrawing) {
if (ev.which === 13) {
if (ev.key === "Enter") {
const currShape = canvas.currentShape as Text;
currShape.setText = canvas.currentInputBox.value;

Expand All @@ -127,7 +257,7 @@ import ResizeCanvas from "./utils/resizer";

canvas.isDrawing = false;
canvas.currentInputBox.remove();
} else if (ev.which === 27) {
} else if (ev.key === "Escape") {
canvas.isDrawing = false;
canvas.currentInputBox.remove();
}
Expand Down
Loading

0 comments on commit 04cf6b1

Please sign in to comment.