First incomplete but working version

This commit is contained in:
Stephan Egli 2025-01-09 18:12:30 +01:00
commit 777d27a5d7
10 changed files with 2045 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
index.html Normal file
View File

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Local-First Geometry App</title>
<link rel="stylesheet" href="src/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<!-- Bereich für Buttons, um neue Formen anzulegen -->
<div id="controls">
<button id="addCircleButton">Kreis hinzufügen</button>
<button id="addTriangleButton">Dreieck hinzufügen</button>
</div>
<!-- Canvas, auf dem wir die Formen zeichnen -->
<canvas id="shapesCanvas" width="600" height="400"></canvas>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1681
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "automerge-tutorial",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^6.0.5"
},
"dependencies": {
"@automerge/automerge-repo": "^1.2.1",
"@automerge/automerge-repo-network-websocket": "^1.2.1",
"@automerge/automerge-repo-storage-indexeddb": "^1.2.1",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.4.1"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

50
src/canvasManager.js Normal file
View File

@ -0,0 +1,50 @@
// js/canvasManager.js
/**
* Zeichnet alle Shapes auf dem Canvas neu
* @param {HTMLCanvasElement} canvas
* @param {Array} shapesArray - Array der Shapes aus dem Dokument
*/
export function drawAllShapesOnCanvas(canvas, shapesArray) {
const ctx = canvas.getContext("2d");
// Canvas leerräumen
ctx.clearRect(0, 0, canvas.width, canvas.height);
shapesArray.forEach(shape => {
if (shape.type === "circle") {
drawCircle(ctx, shape);
} else if (shape.type === "triangle") {
drawTriangle(ctx, shape);
}
});
}
/**
* Zeichnet einen Kreis
* @param {CanvasRenderingContext2D} ctx
* @param {Object} circle
*/
function drawCircle(ctx, circle) {
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI);
ctx.fillStyle = circle.color || "black";
ctx.fill();
}
/**
* Zeichnet ein Dreieck
* @param {CanvasRenderingContext2D} ctx
* @param {Object} triangle
*/
function drawTriangle(ctx, triangle) {
const [p1, p2, p3] = triangle.coordinates;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.lineTo(p3.x, p3.y);
ctx.closePath();
ctx.fillStyle = triangle.color || "black";
ctx.fill();
}

86
src/docManager.js Normal file
View File

@ -0,0 +1,86 @@
// docManager.js (Automerge 2.x style)
import { Repo } from "@automerge/automerge-repo"
// For IndexedDB storage
import { IndexedDBStorageAdapter } from '@automerge/automerge-repo-storage-indexeddb'
import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket"
let handle
export function initRepo() {
const repo = new Repo({
// Choose a network adapter or omit if youre doing pure local
network: [new BrowserWebSocketClientAdapter("wss://automerge.rheinheim.fraction.ch")],
// Use IndexedDB instead of localStorage
storage: new IndexedDBStorageAdapter({
// optionally specify a database name (defaults to "automerge")
databaseName: "myAutomergeDB"
})
})
// Now create or open a document
handle = repo.create()
// Listen for 'change' events
handle.on("change", ({doc}) => {
console.log("Document changed!")
// The up-to-date document is accessible via handle.doc
console.log("Current doc state:", doc)
})
// Initialize the doc with some data
handle.change(doc => {
doc.shapes = []
})
return handle
}
/**
* Add a new circle to the document
*/
export function addCircle(x, y, radius) {
handle.change(doc => {
doc.shapes.push({
id: Date.now(),
type: "circle",
x,
y,
radius,
color: "red"
})
})
}
/**
* Fügt dem Dokument ein Dreieck hinzu
*/
export function addTriangle(x1, y1, x2, y2, x3, y3) {
handle.change(doc => {
doc.shapes.push({
id: Date.now(),
type: "triangle",
coordinates: [
{ x: x1, y: y1 },
{ x: x2, y: y2 },
{ x: x3, y: y3 },
],
color: "blue"
});
});
}
/**
* Move a shape in the document
*/
export function moveShape(shapeId, newX, newY) {
handle.change(doc => {
const shape = doc.shapes.find(s => s.id === shapeId)
if (shape) {
shape.x = newX
shape.y = newY
}
})
}

35
src/main.js Normal file
View File

@ -0,0 +1,35 @@
// main.js
import { initRepo, addCircle, addTriangle, moveShape } from "./docManager.js"
import { drawAllShapesOnCanvas } from "./canvasManager.js"
const canvas = document.getElementById("shapesCanvas")
// 1. Init the repo & document handle
const handle = initRepo()
// TODO is the whenReady() needed ?
// await handle.whenReady()
// 2. Subscribe so we can re-render whenever doc changes
// Listen for 'change' events
handle.on("change", ({doc}) => {
console.log("Document changed! Drawallshapes", doc)
drawAllShapesOnCanvas(canvas, doc.shapes)
})
// 3. Buttons holen
const addCircleButton = document.getElementById("addCircleButton");
const addTriangleButton = document.getElementById("addTriangleButton");
// 4. Event Listener für Buttons
addCircleButton.addEventListener("click", () => {
// z. B. zufällige Position/Radius
addCircle(100, 100, 30);
// TODO render();
});
addTriangleButton.addEventListener("click", () => {
// z. B. zufällige Koordinaten
addTriangle( 200, 200, 220, 220, 180, 220);
// TODO render();
});

104
src/style.css Normal file
View File

@ -0,0 +1,104 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
canvas {
border: 1px solid #ccc;
}
#controls {
margin-bottom: 1em;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #f7df1eaa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

19
vite.config.ts Normal file
View File

@ -0,0 +1,19 @@
import { defineConfig } from "vite"
import wasm from "vite-plugin-wasm"
import topLevelAwait from 'vite-plugin-top-level-await'
export default defineConfig({
// customize this to your repo name for github pages deploy
base: "",
build: {
target: "esnext",
},
plugins: [wasm(), topLevelAwait()],
worker: {
format: "es",
plugins: () => [wasm()],
},
})