Snap to grid and flag export
This commit is contained in:
@@ -53,8 +53,8 @@
|
|||||||
if (!files || files.length === 0) return;
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
const rect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
const dropX = (e.clientX - rect.left - state.viewport.x) / state.viewport.zoom;
|
const dropX = state.snap((e.clientX - rect.left - state.viewport.x) / state.viewport.zoom);
|
||||||
const dropY = (e.clientY - rect.top - state.viewport.y) / state.viewport.zoom;
|
const dropY = state.snap((e.clientY - rect.top - state.viewport.y) / state.viewport.zoom);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
handleFile(file, dropX, dropY);
|
handleFile(file, dropX, dropY);
|
||||||
|
|||||||
@@ -100,8 +100,8 @@
|
|||||||
const dy = (e.clientY - dragStart.y) / appState.viewport.zoom;
|
const dy = (e.clientY - dragStart.y) / appState.viewport.zoom;
|
||||||
for (const [id, startPos] of dragStartPositions) {
|
for (const [id, startPos] of dragStartPositions) {
|
||||||
appState.updateItem(id, {
|
appState.updateItem(id, {
|
||||||
x: startPos.x + dx,
|
x: appState.snap(startPos.x + dx),
|
||||||
y: startPos.y + dy
|
y: appState.snap(startPos.y + dy)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (isResizing) {
|
} else if (isResizing) {
|
||||||
@@ -126,15 +126,17 @@
|
|||||||
newHeight = constrained.height;
|
newHeight = constrained.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deltaWidth = newWidth - resizeStart.width;
|
const snappedWidth = appState.snap(newWidth);
|
||||||
const deltaHeight = newHeight - resizeStart.height;
|
const snappedHeight = appState.snap(newHeight);
|
||||||
|
const deltaWidth = snappedWidth - resizeStart.width;
|
||||||
|
const deltaHeight = snappedHeight - resizeStart.height;
|
||||||
const offset = calculateCenterOffset(resizeStart.corner, deltaWidth, deltaHeight, item.rotation);
|
const offset = calculateCenterOffset(resizeStart.corner, deltaWidth, deltaHeight, item.rotation);
|
||||||
|
|
||||||
appState.updateItem(item.id, {
|
appState.updateItem(item.id, {
|
||||||
width: newWidth,
|
width: snappedWidth,
|
||||||
height: newHeight,
|
height: snappedHeight,
|
||||||
x: resizeStart.itemX + offset.x,
|
x: appState.snap(resizeStart.itemX + offset.x),
|
||||||
y: resizeStart.itemY + offset.y
|
y: appState.snap(resizeStart.itemY + offset.y)
|
||||||
});
|
});
|
||||||
} else if (isRotating) {
|
} else if (isRotating) {
|
||||||
const el = document.querySelector(`[data-item-id="${item.id}"]`);
|
const el = document.querySelector(`[data-item-id="${item.id}"]`);
|
||||||
|
|||||||
@@ -8,14 +8,22 @@
|
|||||||
let soundInput: HTMLInputElement;
|
let soundInput: HTMLInputElement;
|
||||||
let videoInput: HTMLInputElement;
|
let videoInput: HTMLInputElement;
|
||||||
|
|
||||||
|
function getSpawnPosition() {
|
||||||
|
return {
|
||||||
|
x: state.snap(-state.viewport.x / state.viewport.zoom + 400),
|
||||||
|
y: state.snap(-state.viewport.y / state.viewport.zoom + 300)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function addTile() {
|
function addTile() {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
const pos = getSpawnPosition();
|
||||||
state.addItem({
|
state.addItem({
|
||||||
id,
|
id,
|
||||||
html: '',
|
html: '',
|
||||||
css: '',
|
css: '',
|
||||||
x: -state.viewport.x / state.viewport.zoom + 400,
|
x: pos.x,
|
||||||
y: -state.viewport.y / state.viewport.zoom + 300,
|
y: pos.y,
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -26,6 +34,7 @@
|
|||||||
|
|
||||||
function addText() {
|
function addText() {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
const pos = getSpawnPosition();
|
||||||
state.addItem({
|
state.addItem({
|
||||||
id,
|
id,
|
||||||
html: '<p>Text</p>',
|
html: '<p>Text</p>',
|
||||||
@@ -34,8 +43,8 @@
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-family: 'Departure Mono', monospace;
|
font-family: 'Departure Mono', monospace;
|
||||||
}`,
|
}`,
|
||||||
x: -state.viewport.x / state.viewport.zoom + 400,
|
x: pos.x,
|
||||||
y: -state.viewport.y / state.viewport.zoom + 300,
|
y: pos.y,
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 50,
|
height: 50,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -48,6 +57,7 @@
|
|||||||
const url = prompt('Enter URL to embed:');
|
const url = prompt('Enter URL to embed:');
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
const pos = getSpawnPosition();
|
||||||
state.addItem({
|
state.addItem({
|
||||||
id,
|
id,
|
||||||
html: `<iframe src="${url}" frameborder="0" allowfullscreen></iframe>`,
|
html: `<iframe src="${url}" frameborder="0" allowfullscreen></iframe>`,
|
||||||
@@ -56,8 +66,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
}`,
|
}`,
|
||||||
x: -state.viewport.x / state.viewport.zoom + 400,
|
x: pos.x,
|
||||||
y: -state.viewport.y / state.viewport.zoom + 300,
|
y: pos.y,
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 480,
|
height: 480,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -91,8 +101,7 @@
|
|||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const assetId = crypto.randomUUID();
|
const assetId = crypto.randomUUID();
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const x = -state.viewport.x / state.viewport.zoom + 400;
|
const pos = getSpawnPosition();
|
||||||
const y = -state.viewport.y / state.viewport.zoom + 300;
|
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
@@ -106,8 +115,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}`,
|
}`,
|
||||||
x,
|
x: pos.x,
|
||||||
y,
|
y: pos.y,
|
||||||
width: img.naturalWidth,
|
width: img.naturalWidth,
|
||||||
height: img.naturalHeight,
|
height: img.naturalHeight,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -122,8 +131,7 @@
|
|||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const assetId = crypto.randomUUID();
|
const assetId = crypto.randomUUID();
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const x = -state.viewport.x / state.viewport.zoom + 400;
|
const pos = getSpawnPosition();
|
||||||
const y = -state.viewport.y / state.viewport.zoom + 300;
|
|
||||||
|
|
||||||
state.addAsset(assetId, { blob: file, url, filename: file.name });
|
state.addAsset(assetId, { blob: file, url, filename: file.name });
|
||||||
state.addItem({
|
state.addItem({
|
||||||
@@ -133,8 +141,8 @@
|
|||||||
css: `audio {
|
css: `audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}`,
|
}`,
|
||||||
x,
|
x: pos.x,
|
||||||
y,
|
y: pos.y,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 54,
|
height: 54,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -147,8 +155,7 @@
|
|||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const assetId = crypto.randomUUID();
|
const assetId = crypto.randomUUID();
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const x = -state.viewport.x / state.viewport.zoom + 400;
|
const pos = getSpawnPosition();
|
||||||
const y = -state.viewport.y / state.viewport.zoom + 300;
|
|
||||||
|
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.onloadedmetadata = () => {
|
video.onloadedmetadata = () => {
|
||||||
@@ -161,8 +168,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}`,
|
}`,
|
||||||
x,
|
x: pos.x,
|
||||||
y,
|
y: pos.y,
|
||||||
width: video.videoWidth || 640,
|
width: video.videoWidth || 640,
|
||||||
height: video.videoHeight || 360,
|
height: video.videoHeight || 360,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Upload, Download, Paintbrush, Trash2, EyeOff, ZoomIn, Combine, Lock, Unlock } from 'lucide-svelte';
|
import { Upload, Download, Paintbrush, Trash2, EyeOff, ZoomIn, Combine, Lock, Unlock, Grid3x3 } from 'lucide-svelte';
|
||||||
import { exportBoard, importBoard, mergeBoard } from './io';
|
import { exportBoard, importBoard, mergeBoard } from './io';
|
||||||
import { state } from './state.svelte';
|
import { state } from './state.svelte';
|
||||||
import Palette from './Palette.svelte';
|
import Palette from './Palette.svelte';
|
||||||
@@ -116,6 +116,13 @@
|
|||||||
<button onclick={handleImportClick} title="Import" disabled={state.locked}><Upload size={14} /></button>
|
<button onclick={handleImportClick} title="Import" disabled={state.locked}><Upload size={14} /></button>
|
||||||
<button onclick={handleMergeClick} title="Merge" disabled={state.locked}><Combine size={14} /></button>
|
<button onclick={handleMergeClick} title="Merge" disabled={state.locked}><Combine size={14} /></button>
|
||||||
<button onclick={handleExport} title="Export"><Download size={14} /></button>
|
<button onclick={handleExport} title="Export"><Download size={14} /></button>
|
||||||
|
<button
|
||||||
|
class:active={state.snapToGrid}
|
||||||
|
onclick={() => state.setSnapToGrid(!state.snapToGrid)}
|
||||||
|
title={state.snapToGrid ? 'Disable snap to grid' : 'Snap to grid'}
|
||||||
|
>
|
||||||
|
<Grid3x3 size={14} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class:active={state.locked}
|
class:active={state.locked}
|
||||||
onclick={() => state.setLocked(!state.locked)}
|
onclick={() => state.setLocked(!state.locked)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export async function exportBoard(): Promise<{
|
|||||||
items: state.manifest.items.map((item) => ({ ...item })),
|
items: state.manifest.items.map((item) => ({ ...item })),
|
||||||
sharedCss: state.manifest.sharedCss,
|
sharedCss: state.manifest.sharedCss,
|
||||||
appCss: state.manifest.appCss,
|
appCss: state.manifest.appCss,
|
||||||
|
flags: state.manifest.flags ?? {},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const item of exportManifest.items) {
|
for (const item of exportManifest.items) {
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ function createState() {
|
|||||||
let focusedId = $state<string | null>(null);
|
let focusedId = $state<string | null>(null);
|
||||||
let clipboard = $state<Omit<Item, 'id'>[]>([]);
|
let clipboard = $state<Omit<Item, 'id'>[]>([]);
|
||||||
let locked = $state<boolean>(false);
|
let locked = $state<boolean>(false);
|
||||||
|
let snapToGrid = $state<boolean>(false);
|
||||||
|
|
||||||
|
const GRID_SIZE = 20;
|
||||||
|
|
||||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let animationId: number | null = null;
|
let animationId: number | null = null;
|
||||||
@@ -220,6 +223,14 @@ function createState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSnapToGrid(value: boolean) {
|
||||||
|
snapToGrid = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snap(value: number): number {
|
||||||
|
return snapToGrid ? Math.round(value / GRID_SIZE) * GRID_SIZE : value;
|
||||||
|
}
|
||||||
|
|
||||||
function focus(id: string | null) {
|
function focus(id: string | null) {
|
||||||
focusedId = id;
|
focusedId = id;
|
||||||
}
|
}
|
||||||
@@ -445,6 +456,9 @@ function createState() {
|
|||||||
get locked() {
|
get locked() {
|
||||||
return locked;
|
return locked;
|
||||||
},
|
},
|
||||||
|
get snapToGrid() {
|
||||||
|
return snapToGrid;
|
||||||
|
},
|
||||||
get maxZIndex() {
|
get maxZIndex() {
|
||||||
return maxZIndex;
|
return maxZIndex;
|
||||||
},
|
},
|
||||||
@@ -461,6 +475,8 @@ function createState() {
|
|||||||
edit,
|
edit,
|
||||||
editGlobal,
|
editGlobal,
|
||||||
setLocked,
|
setLocked,
|
||||||
|
setSnapToGrid,
|
||||||
|
snap,
|
||||||
focus,
|
focus,
|
||||||
updateSharedCss,
|
updateSharedCss,
|
||||||
updateAppCss,
|
updateAppCss,
|
||||||
|
|||||||
Reference in New Issue
Block a user