OK
This commit is contained in:
174
src/lib/Canvas.svelte
Normal file
174
src/lib/Canvas.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user