This commit is contained in:
2025-12-01 17:42:42 +01:00
commit e9b0e9d856
26 changed files with 3438 additions and 0 deletions

174
src/lib/Canvas.svelte Normal file
View File

@@ -0,0 +1,174 @@
<script lang="ts">
import { state } from './state.svelte';
import Item from './Item.svelte';
let container: HTMLDivElement;
let isPanning = false;
let lastX = 0;
let lastY = 0;
function handleMouseDown(e: MouseEvent) {
if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
isPanning = true;
lastX = e.clientX;
lastY = e.clientY;
e.preventDefault();
} else if (e.button === 0 && e.target === container) {
state.select(null);
state.focus(null);
}
}
function handleMouseMove(e: MouseEvent) {
if (!isPanning) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX;
lastY = e.clientY;
state.pan(dx, dy);
}
function handleMouseUp() {
isPanning = false;
}
function handleWheel(e: WheelEvent) {
e.preventDefault();
const factor = e.deltaY > 0 ? 0.9 : 1.1;
const rect = container.getBoundingClientRect();
const cx = e.clientX - rect.left;
const cy = e.clientY - rect.top;
state.zoomAt(factor, cx, cy);
}
function handleDrop(e: DragEvent) {
e.preventDefault();
const files = e.dataTransfer?.files;
if (!files || files.length === 0) return;
const rect = container.getBoundingClientRect();
const dropX = (e.clientX - rect.left - state.viewport.x) / state.viewport.zoom;
const dropY = (e.clientY - rect.top - state.viewport.y) / state.viewport.zoom;
for (const file of files) {
handleFile(file, dropX, dropY);
}
}
function handleFile(file: File, x: number, y: number) {
const id = crypto.randomUUID();
const assetId = crypto.randomUUID();
const url = URL.createObjectURL(file);
if (file.type.startsWith('image/')) {
const img = new Image();
img.onload = () => {
state.addAsset(assetId, { blob: file, url, filename: file.name });
state.addItem({
id,
assetId,
html: `<img src="${url}" alt="" />`,
css: `img {
width: 100%;
height: 100%;
object-fit: contain;
}`,
x,
y,
width: img.naturalWidth,
height: img.naturalHeight,
rotation: 0,
zIndex: state.maxZIndex + 1
});
};
img.src = url;
} else if (file.type.startsWith('audio/')) {
state.addAsset(assetId, { blob: file, url, filename: file.name });
state.addItem({
id,
assetId,
html: `<audio src="${url}" controls></audio>`,
css: `audio {
width: 100%;
}`,
x,
y,
width: 300,
height: 54,
rotation: 0,
zIndex: state.maxZIndex + 1
});
} else if (file.type.startsWith('video/')) {
const video = document.createElement('video');
video.onloadedmetadata = () => {
state.addAsset(assetId, { blob: file, url, filename: file.name });
state.addItem({
id,
assetId,
html: `<video src="${url}" controls></video>`,
css: `video {
width: 100%;
height: 100%;
}`,
x,
y,
width: video.videoWidth || 640,
height: video.videoHeight || 360,
rotation: 0,
zIndex: state.maxZIndex + 1
});
};
video.src = url;
}
}
function handleDragOver(e: DragEvent) {
e.preventDefault();
}
</script>
<svelte:window onmouseup={handleMouseUp} onmousemove={handleMouseMove} />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={container}
class="canvas"
onmousedown={handleMouseDown}
onwheel={handleWheel}
ondrop={handleDrop}
ondragover={handleDragOver}
>
<div
class="viewport"
style="transform: translate({state.viewport.x}px, {state.viewport.y}px) scale({state.viewport
.zoom})"
>
{#each state.manifest.items as item (item.id)}
<Item {item} />
{/each}
</div>
</div>
<style>
.canvas {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
background: var(--bg, #1a1a1a);
background-image: radial-gradient(circle, var(--border, #333) 1px, transparent 1px);
background-size: 20px 20px;
cursor: grab;
}
.canvas:active {
cursor: grabbing;
}
.viewport {
transform-origin: 0 0;
position: absolute;
top: 0;
left: 0;
}
</style>