Compare commits
5 Commits
133795f744
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 425660f139 | |||
| d5a744652d | |||
| e9d8a1a818 | |||
| da90cfa891 | |||
| 05ad4128e4 |
@ -7,6 +7,7 @@
|
|||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"start": "node server.js",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
@ -38,6 +39,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"@sveltejs/adapter-static": "^3.0.8"
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
792
pnpm-lock.yaml
generated
792
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
119
server.js
Normal file
119
server.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import http from 'http';
|
||||||
|
import { Server } from 'socket.io';
|
||||||
|
import { handler } from './build/handler.js';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = new Server(server);
|
||||||
|
|
||||||
|
const eventTimestamps = {};
|
||||||
|
const EVENT_LIMIT = 100; // Max events per second per user
|
||||||
|
const TIME_FRAME = 1000; // 1 second in milliseconds
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log('a user connected');
|
||||||
|
|
||||||
|
// Middleware to check for event spamming
|
||||||
|
socket.use((packet, next) => {
|
||||||
|
const userId = socket.id;
|
||||||
|
const eventName = packet[0];
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!eventTimestamps[userId]) {
|
||||||
|
eventTimestamps[userId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove timestamps older than the time frame
|
||||||
|
eventTimestamps[userId] = eventTimestamps[userId].filter(ts => now - ts < TIME_FRAME);
|
||||||
|
|
||||||
|
// Check if the user has exceeded the event limit
|
||||||
|
if (eventTimestamps[userId].length >= EVENT_LIMIT) {
|
||||||
|
console.warn(`User ${userId} is sending too many events (${eventName}). Throttling.`);
|
||||||
|
return next(new Error('Too many events'));
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTimestamps[userId].push(now);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('join', (room) => {
|
||||||
|
socket.join(room);
|
||||||
|
console.log(`User joined room: ${room}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('request-state', (data) => {
|
||||||
|
socket.to(data.room).emit('request-state', { from: socket.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sync-state', (data) => {
|
||||||
|
socket.to(data.to).emit('sync-state', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('card-move', (data) => {
|
||||||
|
socket.to(data.room).emit('card-move', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('card-click', (data) => {
|
||||||
|
socket.to(data.room).emit('card-click', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('card-drop', (data) => {
|
||||||
|
socket.to(data.room).emit('card-drop', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-add', (data) => {
|
||||||
|
socket.to(data.room).emit('sticky-note-add', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-move', (data) => {
|
||||||
|
socket.to(data.room).emit('sticky-note-move', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-update', (data) => {
|
||||||
|
socket.to(data.room).emit('sticky-note-update', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-delete', (data) => {
|
||||||
|
socket.to(data.room).emit('sticky-note-delete', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('deck-change', (data) => {
|
||||||
|
socket.to(data.room).emit('deck-change', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('shuffle-deck', (data) => {
|
||||||
|
socket.to(data.room).emit('shuffle-deck', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('collect-to-deck', (data) => {
|
||||||
|
socket.to(data.room).emit('collect-to-deck', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('deal-cards', (data) => {
|
||||||
|
socket.to(data.room).emit('deal-cards', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('flip-all-cards', (data) => {
|
||||||
|
socket.to(data.room).emit('flip-all-cards', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('shuffle-mode-change', (data) => {
|
||||||
|
socket.to(data.room).emit('shuffle-mode-change', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('import-canvas', (data) => {
|
||||||
|
socket.to(data.room).emit('import-canvas', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('user disconnected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(handler);
|
||||||
|
|
||||||
|
server.listen(3000, () => {
|
||||||
|
console.log('listening on *:3000');
|
||||||
|
});
|
||||||
@ -8,6 +8,7 @@
|
|||||||
y?: number;
|
y?: number;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
canvasOffset?: { x: number; y: number };
|
canvasOffset?: { x: number; y: number };
|
||||||
|
canvasZoom?: number;
|
||||||
onMove?: (id: number, x: number, y: number) => void;
|
onMove?: (id: number, x: number, y: number) => void;
|
||||||
onClick?: (id: number) => void;
|
onClick?: (id: number) => void;
|
||||||
onDrop?: (id: number, x: number, y: number) => void;
|
onDrop?: (id: number, x: number, y: number) => void;
|
||||||
@ -22,6 +23,7 @@
|
|||||||
y = $bindable(0),
|
y = $bindable(0),
|
||||||
zIndex = $bindable(0),
|
zIndex = $bindable(0),
|
||||||
canvasOffset = { x: 0, y: 0 },
|
canvasOffset = { x: 0, y: 0 },
|
||||||
|
canvasZoom = 1,
|
||||||
onMove,
|
onMove,
|
||||||
onClick,
|
onClick,
|
||||||
onDrop
|
onDrop
|
||||||
@ -38,10 +40,11 @@
|
|||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
hasMoved = false;
|
hasMoved = false;
|
||||||
const rect = cardElement.getBoundingClientRect();
|
|
||||||
|
// Calculate offset relative to card position in canvas coordinates
|
||||||
dragOffset = {
|
dragOffset = {
|
||||||
x: event.clientX - rect.left,
|
x: (event.clientX - canvasOffset.x) / canvasZoom - x,
|
||||||
y: event.clientY - rect.top
|
y: (event.clientY - canvasOffset.y) / canvasZoom - y
|
||||||
};
|
};
|
||||||
dragStartPos = {
|
dragStartPos = {
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
@ -61,8 +64,8 @@
|
|||||||
hasMoved = true;
|
hasMoved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newX = event.clientX - dragOffset.x - canvasOffset.x;
|
const newX = (event.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||||
const newY = event.clientY - dragOffset.y - canvasOffset.y;
|
const newY = (event.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||||
|
|
||||||
x = newX;
|
x = newX;
|
||||||
y = newY;
|
y = newY;
|
||||||
@ -84,6 +87,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTouchStart(event: TouchEvent) {
|
||||||
|
if (event.touches.length !== 1) return;
|
||||||
|
event.stopPropagation(); // Prevent canvas panning
|
||||||
|
|
||||||
|
const touch = event.touches[0];
|
||||||
|
isDragging = true;
|
||||||
|
hasMoved = false;
|
||||||
|
|
||||||
|
// Calculate offset relative to card position in canvas coordinates
|
||||||
|
dragOffset = {
|
||||||
|
x: (touch.clientX - canvasOffset.x) / canvasZoom - x,
|
||||||
|
y: (touch.clientY - canvasOffset.y) / canvasZoom - y
|
||||||
|
};
|
||||||
|
dragStartPos = {
|
||||||
|
x: touch.clientX,
|
||||||
|
y: touch.clientY
|
||||||
|
};
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchMove(event: TouchEvent) {
|
||||||
|
if (!isDragging || event.touches.length !== 1) return;
|
||||||
|
|
||||||
|
const touch = event.touches[0];
|
||||||
|
const deltaX = Math.abs(touch.clientX - dragStartPos.x);
|
||||||
|
const deltaY = Math.abs(touch.clientY - dragStartPos.y);
|
||||||
|
|
||||||
|
if (deltaX > 10 || deltaY > 10) {
|
||||||
|
hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newX = (touch.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||||
|
const newY = (touch.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||||
|
|
||||||
|
x = newX;
|
||||||
|
y = newY;
|
||||||
|
|
||||||
|
onMove?.(id, newX, newY);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchEnd(event: TouchEvent) {
|
||||||
|
if (isDragging) {
|
||||||
|
onDrop?.(id, x, y);
|
||||||
|
if (!hasMoved) {
|
||||||
|
flipped = !flipped;
|
||||||
|
onClick?.(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDragging = false;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
@ -104,6 +161,9 @@
|
|||||||
style="left: {x}px; top: {y}px; z-index: {zIndex};"
|
style="left: {x}px; top: {y}px; z-index: {zIndex};"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
onclick={handleClick}
|
onclick={handleClick}
|
||||||
|
ontouchstart={handleTouchStart}
|
||||||
|
ontouchmove={handleTouchMove}
|
||||||
|
ontouchend={handleTouchEnd}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
zIndex: number;
|
zIndex: number;
|
||||||
color: string;
|
color: string;
|
||||||
canvasOffset?: { x: number; y: number };
|
canvasOffset?: { x: number; y: number };
|
||||||
|
canvasZoom?: number;
|
||||||
onMove?: (id: number, x: number, y: number) => void;
|
onMove?: (id: number, x: number, y: number) => void;
|
||||||
onUpdate?: (id: number, text: string) => void;
|
onUpdate?: (id: number, text: string) => void;
|
||||||
onDelete?: (id: number) => void;
|
onDelete?: (id: number) => void;
|
||||||
@ -20,6 +21,7 @@
|
|||||||
zIndex = $bindable(0),
|
zIndex = $bindable(0),
|
||||||
color,
|
color,
|
||||||
canvasOffset = { x: 0, y: 0 },
|
canvasOffset = { x: 0, y: 0 },
|
||||||
|
canvasZoom = 1,
|
||||||
onMove,
|
onMove,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onDelete
|
onDelete
|
||||||
@ -38,10 +40,11 @@
|
|||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
hasMoved = false;
|
hasMoved = false;
|
||||||
const rect = noteElement.getBoundingClientRect();
|
|
||||||
|
// Calculate offset relative to note position in canvas coordinates
|
||||||
dragOffset = {
|
dragOffset = {
|
||||||
x: event.clientX - rect.left,
|
x: (event.clientX - canvasOffset.x) / canvasZoom - x,
|
||||||
y: event.clientY - rect.top
|
y: (event.clientY - canvasOffset.y) / canvasZoom - y
|
||||||
};
|
};
|
||||||
dragStartPos = {
|
dragStartPos = {
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
@ -61,8 +64,8 @@
|
|||||||
hasMoved = true;
|
hasMoved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newX = event.clientX - dragOffset.x - canvasOffset.x;
|
const newX = (event.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||||
const newY = event.clientY - dragOffset.y - canvasOffset.y;
|
const newY = (event.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||||
|
|
||||||
x = newX;
|
x = newX;
|
||||||
y = newY;
|
y = newY;
|
||||||
@ -102,6 +105,59 @@
|
|||||||
onDelete?.(id);
|
onDelete?.(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTouchStart(event: TouchEvent) {
|
||||||
|
if (event.touches.length !== 1) return;
|
||||||
|
event.stopPropagation(); // Prevent canvas panning
|
||||||
|
|
||||||
|
const touch = event.touches[0];
|
||||||
|
isDragging = true;
|
||||||
|
hasMoved = false;
|
||||||
|
|
||||||
|
// Calculate offset relative to note position in canvas coordinates
|
||||||
|
dragOffset = {
|
||||||
|
x: (touch.clientX - canvasOffset.x) / canvasZoom - x,
|
||||||
|
y: (touch.clientY - canvasOffset.y) / canvasZoom - y
|
||||||
|
};
|
||||||
|
dragStartPos = {
|
||||||
|
x: touch.clientX,
|
||||||
|
y: touch.clientY
|
||||||
|
};
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchMove(event: TouchEvent) {
|
||||||
|
if (!isDragging || event.touches.length !== 1) return;
|
||||||
|
|
||||||
|
const touch = event.touches[0];
|
||||||
|
const deltaX = Math.abs(touch.clientX - dragStartPos.x);
|
||||||
|
const deltaY = Math.abs(touch.clientY - dragStartPos.y);
|
||||||
|
|
||||||
|
if (deltaX > 5 || deltaY > 5) {
|
||||||
|
hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newX = (touch.clientX - canvasOffset.x) / canvasZoom - dragOffset.x;
|
||||||
|
const newY = (touch.clientY - canvasOffset.y) / canvasZoom - dragOffset.y;
|
||||||
|
|
||||||
|
x = newX;
|
||||||
|
y = newY;
|
||||||
|
|
||||||
|
onMove?.(id, newX, newY);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchEnd(event: TouchEvent) {
|
||||||
|
if (isDragging) {
|
||||||
|
if (!hasMoved) {
|
||||||
|
isEditing = true;
|
||||||
|
setTimeout(() => textareaElement?.focus(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDragging = false;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
@ -122,6 +178,9 @@
|
|||||||
style="left: {x}px; top: {y}px; z-index: {zIndex}; background-color: {color};"
|
style="left: {x}px; top: {y}px; z-index: {zIndex}; background-color: {color};"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
onclick={handleClick}
|
onclick={handleClick}
|
||||||
|
ontouchstart={handleTouchStart}
|
||||||
|
ontouchmove={handleTouchMove}
|
||||||
|
ontouchend={handleTouchEnd}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Card from '$lib/Card.svelte';
|
import Card from '$lib/Card.svelte';
|
||||||
import StickyNote from '$lib/StickyNote.svelte';
|
import StickyNote from '$lib/StickyNote.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
|
||||||
interface CardData {
|
interface CardData {
|
||||||
id: number;
|
id: number;
|
||||||
@ -21,7 +22,7 @@
|
|||||||
riskCategories?: string[];
|
riskCategories?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StickyNote {
|
interface StickyNoteData {
|
||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
x: number;
|
x: number;
|
||||||
@ -101,17 +102,163 @@
|
|||||||
|
|
||||||
let deckPosition = $state({ x: 50, y: 300 });
|
let deckPosition = $state({ x: 50, y: 300 });
|
||||||
let canvasOffset = $state({ x: 0, y: 0 });
|
let canvasOffset = $state({ x: 0, y: 0 });
|
||||||
|
let canvasZoom = $state(1);
|
||||||
let isDragging = $state(false);
|
let isDragging = $state(false);
|
||||||
let dragStart = $state({ x: 0, y: 0 });
|
let dragStart = $state({ x: 0, y: 0 });
|
||||||
let dragStartOffset = $state({ x: 0, y: 0 });
|
let dragStartOffset = $state({ x: 0, y: 0 });
|
||||||
|
let isPinching = $state(false);
|
||||||
|
let lastPinchDistance = $state(0);
|
||||||
|
let pinchCenter = $state({ x: 0, y: 0 });
|
||||||
|
|
||||||
let cards = $state<CardData[]>([]);
|
let cards = $state<CardData[]>([]);
|
||||||
let maxZIndex = $state(0);
|
let maxZIndex = $state(0);
|
||||||
let stickyNotes = $state<StickyNote[]>([]);
|
let stickyNotes = $state<StickyNoteData[]>([]);
|
||||||
let nextNoteId = $state(1);
|
let nextNoteId = $state(1);
|
||||||
let noteColors = ['#ffeb3b', '#4caf50', '#2196f3', '#ff9800', '#e91e63', '#9c27b0'];
|
let noteColors = ['#ffeb3b', '#4caf50', '#2196f3', '#ff9800', '#e91e63', '#9c27b0'];
|
||||||
|
|
||||||
function initializeDeck() {
|
let socket: Socket;
|
||||||
|
let room: string;
|
||||||
|
let isStateSynced = false;
|
||||||
|
let lastEmitTime = 0;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
room = urlParams.get('sessid') || `room-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
if (!urlParams.has('sessid')) {
|
||||||
|
window.history.replaceState({}, '', `?sessid=${room}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = io();
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
socket.emit('join', room);
|
||||||
|
|
||||||
|
// Request state from other clients, but only if we haven't synced yet
|
||||||
|
if (!isStateSynced) {
|
||||||
|
socket.emit('request-state', { room });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no one sends state within 2 seconds, initialize the deck
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isStateSynced) {
|
||||||
|
initializeDeck();
|
||||||
|
isStateSynced = true;
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
socket.on('request-state', (data) => {
|
||||||
|
const fullState = {
|
||||||
|
cards,
|
||||||
|
stickyNotes,
|
||||||
|
selectedDeck,
|
||||||
|
shuffleMode,
|
||||||
|
deckPosition,
|
||||||
|
maxZIndex,
|
||||||
|
nextNoteId
|
||||||
|
};
|
||||||
|
socket.emit('sync-state', { to: data.from, room, state: fullState });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sync-state', (data) => {
|
||||||
|
if (!isStateSynced) {
|
||||||
|
restoreCanvasState(data.state);
|
||||||
|
isStateSynced = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('card-move', (data) => {
|
||||||
|
const card = cards.find(c => c.id === data.id);
|
||||||
|
if (card) {
|
||||||
|
card.x = data.x;
|
||||||
|
card.y = data.y;
|
||||||
|
card.zIndex = data.zIndex;
|
||||||
|
if (data.zIndex > maxZIndex) maxZIndex = data.zIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('card-click', (data) => {
|
||||||
|
const card = cards.find(c => c.id === data.id);
|
||||||
|
if (card) {
|
||||||
|
card.flipped = !card.flipped;
|
||||||
|
card.zIndex = data.zIndex;
|
||||||
|
if (data.zIndex > maxZIndex) maxZIndex = data.zIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('card-drop', (data) => {
|
||||||
|
const card = cards.find(c => c.id === data.id);
|
||||||
|
if (card) {
|
||||||
|
Object.assign(card, data.cardState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-add', (data) => {
|
||||||
|
stickyNotes.push(data.note);
|
||||||
|
if (data.note.id >= nextNoteId) nextNoteId = data.note.id + 1;
|
||||||
|
if (data.note.zIndex > maxZIndex) maxZIndex = data.note.zIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-move', (data) => {
|
||||||
|
const note = stickyNotes.find(n => n.id === data.id);
|
||||||
|
if (note) {
|
||||||
|
note.x = data.x;
|
||||||
|
note.y = data.y;
|
||||||
|
note.zIndex = data.zIndex;
|
||||||
|
if (data.zIndex > maxZIndex) maxZIndex = data.zIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-update', (data) => {
|
||||||
|
const note = stickyNotes.find(n => n.id === data.id);
|
||||||
|
if (note) {
|
||||||
|
note.text = data.text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('sticky-note-delete', (data) => {
|
||||||
|
const index = stickyNotes.findIndex(n => n.id === data.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
stickyNotes.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('deck-change', (data) => {
|
||||||
|
selectedDeck = data.selectedDeck;
|
||||||
|
initializeDeck(false); // Don't broadcast, just init
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('shuffle-deck', (data) => {
|
||||||
|
cards = data.cards;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('collect-to-deck', (data) => {
|
||||||
|
cards = data.cards;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('deal-cards', (data) => {
|
||||||
|
cards = data.cards;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('flip-all-cards', () => {
|
||||||
|
cards.forEach(card => card.flipped = !card.flipped);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('shuffle-mode-change', (data) => {
|
||||||
|
shuffleMode = data.shuffleMode;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('import-canvas', (data) => {
|
||||||
|
restoreCanvasState(data.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function initializeDeck(broadcast = true) {
|
||||||
const config = deckConfigs[selectedDeck];
|
const config = deckConfigs[selectedDeck];
|
||||||
cards = config.cards.map((image, index) => ({
|
cards = config.cards.map((image, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
@ -124,13 +271,21 @@
|
|||||||
isRiskCategory: selectedDeck === 1 && image.includes('1.png')
|
isRiskCategory: selectedDeck === 1 && image.includes('1.png')
|
||||||
}));
|
}));
|
||||||
maxZIndex = cards.length;
|
maxZIndex = cards.length;
|
||||||
|
stickyNotes = [];
|
||||||
|
nextNoteId = 1;
|
||||||
|
|
||||||
|
if (broadcast && socket) {
|
||||||
|
socket.emit('deck-change', { room, selectedDeck });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
initializeDeck();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleDeckChange() {
|
function handleDeckChange() {
|
||||||
|
if (!confirm('Are you sure you want to change the deck? This will reset the current session.')) {
|
||||||
|
// If the user cancels, revert the selection
|
||||||
|
const previousDeck = cards.length > 0 ? deckConfigs.findIndex(d => d.cards[0] === cards[0].frontImage) : 0;
|
||||||
|
selectedDeck = previousDeck;
|
||||||
|
return;
|
||||||
|
}
|
||||||
initializeDeck();
|
initializeDeck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,17 +295,21 @@
|
|||||||
card.x = x;
|
card.x = x;
|
||||||
card.y = y;
|
card.y = y;
|
||||||
card.zIndex = ++maxZIndex;
|
card.zIndex = ++maxZIndex;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastEmitTime > 16) { // Throttle to ~60fps
|
||||||
|
socket.emit('card-move', { room, id, x, y, zIndex: card.zIndex });
|
||||||
|
lastEmitTime = now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCardDrop(id: number, x: number, y: number) {
|
function handleCardDrop(id: number, x: number, y: number) {
|
||||||
const card = cards.find(c => c.id === id);
|
const card = cards.find(c => c.id === id);
|
||||||
if (card) {
|
if (card) {
|
||||||
// Calculate card center position (card is 220x300px)
|
|
||||||
const cardCenterX = x + 110;
|
const cardCenterX = x + 110;
|
||||||
const cardCenterY = y + 150;
|
const cardCenterY = y + 150;
|
||||||
|
|
||||||
// Check if card center is dropped on deck area
|
|
||||||
if (
|
if (
|
||||||
cardCenterX >= deckPosition.x &&
|
cardCenterX >= deckPosition.x &&
|
||||||
cardCenterX <= deckPosition.x + 220 &&
|
cardCenterX <= deckPosition.x + 220 &&
|
||||||
@ -158,7 +317,6 @@
|
|||||||
cardCenterY <= deckPosition.y + 300 &&
|
cardCenterY <= deckPosition.y + 300 &&
|
||||||
!card.inDeck
|
!card.inDeck
|
||||||
) {
|
) {
|
||||||
// Move card to top of deck
|
|
||||||
const deckCards = cards.filter(c => c.inDeck);
|
const deckCards = cards.filter(c => c.inDeck);
|
||||||
const topZ = deckCards.length > 0 ? Math.max(...deckCards.map(c => c.zIndex)) + 1 : 0;
|
const topZ = deckCards.length > 0 ? Math.max(...deckCards.map(c => c.zIndex)) + 1 : 0;
|
||||||
|
|
||||||
@ -168,9 +326,9 @@
|
|||||||
card.flipped = true;
|
card.flipped = true;
|
||||||
card.inDeck = true;
|
card.inDeck = true;
|
||||||
} else if (card.inDeck) {
|
} else if (card.inDeck) {
|
||||||
// Card was moved out of deck
|
|
||||||
card.inDeck = false;
|
card.inDeck = false;
|
||||||
}
|
}
|
||||||
|
socket.emit('card-drop', { room, id, cardState: { ...card } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,6 +336,7 @@
|
|||||||
const card = cards.find(c => c.id === id);
|
const card = cards.find(c => c.id === id);
|
||||||
if (card) {
|
if (card) {
|
||||||
card.zIndex = ++maxZIndex;
|
card.zIndex = ++maxZIndex;
|
||||||
|
socket.emit('card-click', { room, id, zIndex: card.zIndex });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +355,7 @@
|
|||||||
} else {
|
} else {
|
||||||
normalShuffle(deckCards);
|
normalShuffle(deckCards);
|
||||||
}
|
}
|
||||||
|
socket.emit('shuffle-deck', { room, cards });
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalShuffle(deckCards: CardData[]) {
|
function normalShuffle(deckCards: CardData[]) {
|
||||||
@ -277,6 +437,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectToDeck() {
|
function collectToDeck() {
|
||||||
|
if (!confirm('Are you sure you want to collect all cards to the deck?')) return;
|
||||||
cards.forEach((card, index) => {
|
cards.forEach((card, index) => {
|
||||||
card.x = deckPosition.x + index * 2;
|
card.x = deckPosition.x + index * 2;
|
||||||
card.y = deckPosition.y + index * 2;
|
card.y = deckPosition.y + index * 2;
|
||||||
@ -284,9 +445,11 @@
|
|||||||
card.flipped = true;
|
card.flipped = true;
|
||||||
card.inDeck = true;
|
card.inDeck = true;
|
||||||
});
|
});
|
||||||
|
socket.emit('collect-to-deck', { room, cards });
|
||||||
}
|
}
|
||||||
|
|
||||||
function dealCards() {
|
function dealCards() {
|
||||||
|
if (!confirm('Are you sure you want to deal all cards?')) return;
|
||||||
cards.forEach((card, index) => {
|
cards.forEach((card, index) => {
|
||||||
card.x = 50 + 330 + (index % 6) * 240;
|
card.x = 50 + 330 + (index % 6) * 240;
|
||||||
card.y = 50 + Math.floor(index / 6) * 320;
|
card.y = 50 + Math.floor(index / 6) * 320;
|
||||||
@ -294,22 +457,35 @@
|
|||||||
card.flipped = false;
|
card.flipped = false;
|
||||||
card.inDeck = false;
|
card.inDeck = false;
|
||||||
});
|
});
|
||||||
|
socket.emit('deal-cards', { room, cards });
|
||||||
}
|
}
|
||||||
|
|
||||||
function flipAllCards() {
|
function flipAllCards() {
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
card.flipped = !card.flipped;
|
card.flipped = !card.flipped;
|
||||||
});
|
});
|
||||||
|
socket.emit('flip-all-cards', { room });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCanvasWheel(event: WheelEvent) {
|
function handleCanvasWheel(event: WheelEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
|
||||||
|
const newZoom = Math.max(0.1, Math.min(3, canvasZoom * zoomFactor));
|
||||||
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
const centerX = rect.width / 2;
|
||||||
|
const centerY = rect.height / 2;
|
||||||
|
const canvasCenterX = (centerX - canvasOffset.x) / canvasZoom;
|
||||||
|
const canvasCenterY = (centerY - canvasOffset.y) / canvasZoom;
|
||||||
|
canvasZoom = newZoom;
|
||||||
|
canvasOffset.x = centerX - canvasCenterX * canvasZoom;
|
||||||
|
canvasOffset.y = centerY - canvasCenterY * canvasZoom;
|
||||||
|
} else {
|
||||||
const scrollSpeed = 1;
|
const scrollSpeed = 1;
|
||||||
canvasOffset.x -= event.deltaX * scrollSpeed;
|
canvasOffset.x -= event.deltaX * scrollSpeed;
|
||||||
canvasOffset.y -= event.deltaY * scrollSpeed;
|
canvasOffset.y -= event.deltaY * scrollSpeed;
|
||||||
|
}
|
||||||
canvasOffset.x = Math.max(-2000, Math.min(2000, canvasOffset.x));
|
|
||||||
canvasOffset.y = Math.max(-2000, Math.min(2000, canvasOffset.y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCanvasMouseDown(event: MouseEvent) {
|
function handleCanvasMouseDown(event: MouseEvent) {
|
||||||
@ -328,9 +504,8 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const deltaX = event.clientX - dragStart.x;
|
const deltaX = event.clientX - dragStart.x;
|
||||||
const deltaY = event.clientY - dragStart.y;
|
const deltaY = event.clientY - dragStart.y;
|
||||||
|
canvasOffset.x = dragStartOffset.x + deltaX;
|
||||||
canvasOffset.x = Math.max(-2000, Math.min(2000, dragStartOffset.x + deltaX));
|
canvasOffset.y = dragStartOffset.y + deltaY;
|
||||||
canvasOffset.y = Math.max(-2000, Math.min(2000, dragStartOffset.y + deltaY));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,23 +519,93 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTouchDistance(touches: TouchList) {
|
||||||
|
const touch1 = touches[0];
|
||||||
|
const touch2 = touches[1];
|
||||||
|
return Math.sqrt(
|
||||||
|
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||||
|
Math.pow(touch2.clientY - touch1.clientY, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTouchCenter(touches: TouchList) {
|
||||||
|
const touch1 = touches[0];
|
||||||
|
const touch2 = touches[1];
|
||||||
|
return {
|
||||||
|
x: (touch1.clientX + touch2.clientX) / 2,
|
||||||
|
y: (touch1.clientY + touch2.clientY) / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCanvasTouchStart(event: TouchEvent) {
|
||||||
|
if (event.touches.length >= 2) {
|
||||||
|
isPinching = true;
|
||||||
|
lastPinchDistance = getTouchDistance(event.touches);
|
||||||
|
pinchCenter = getTouchCenter(event.touches);
|
||||||
|
} else if (event.touches.length === 1) {
|
||||||
|
isDragging = true;
|
||||||
|
const touch = event.touches[0];
|
||||||
|
dragStart.x = touch.clientX;
|
||||||
|
dragStart.y = touch.clientY;
|
||||||
|
dragStartOffset.x = canvasOffset.x;
|
||||||
|
dragStartOffset.y = canvasOffset.y;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCanvasTouchMove(event: TouchEvent) {
|
||||||
|
if (event.touches.length === 1 && isDragging && !isPinching) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
const deltaX = touch.clientX - dragStart.x;
|
||||||
|
const deltaY = touch.clientY - dragStart.y;
|
||||||
|
canvasOffset.x = dragStartOffset.x + deltaX;
|
||||||
|
canvasOffset.y = dragStartOffset.y + deltaY;
|
||||||
|
} else if (event.touches.length === 2 && isPinching) {
|
||||||
|
const currentDistance = getTouchDistance(event.touches);
|
||||||
|
const zoomFactor = currentDistance / lastPinchDistance;
|
||||||
|
const newZoom = Math.max(0.1, Math.min(3, canvasZoom * zoomFactor));
|
||||||
|
|
||||||
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
const pinchCenterX = pinchCenter.x - rect.left;
|
||||||
|
const pinchCenterY = pinchCenter.y - rect.top;
|
||||||
|
|
||||||
|
const canvasCenterX = (pinchCenterX - canvasOffset.x) / canvasZoom;
|
||||||
|
const canvasCenterY = (pinchCenterY - canvasOffset.y) / canvasZoom;
|
||||||
|
|
||||||
|
canvasZoom = newZoom;
|
||||||
|
|
||||||
|
canvasOffset.x = pinchCenterX - canvasCenterX * canvasZoom;
|
||||||
|
canvasOffset.y = pinchCenterY - canvasCenterY * canvasZoom;
|
||||||
|
|
||||||
|
lastPinchDistance = currentDistance;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCanvasTouchEnd(event: TouchEvent) {
|
||||||
|
if (event.touches.length < 2) isPinching = false;
|
||||||
|
if (event.touches.length < 1) isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
function addStickyNote() {
|
function addStickyNote() {
|
||||||
const randomColor = noteColors[Math.floor(Math.random() * noteColors.length)];
|
const randomColor = noteColors[Math.floor(Math.random() * noteColors.length)];
|
||||||
const newNote: StickyNote = {
|
const newNote: StickyNoteData = {
|
||||||
id: nextNoteId++,
|
id: nextNoteId++,
|
||||||
text: 'New note',
|
text: '',
|
||||||
x: 400 - canvasOffset.x,
|
x: (400 - canvasOffset.x) / canvasZoom,
|
||||||
y: 200 - canvasOffset.y,
|
y: (200 - canvasOffset.y) / canvasZoom,
|
||||||
zIndex: ++maxZIndex,
|
zIndex: ++maxZIndex,
|
||||||
color: randomColor
|
color: randomColor
|
||||||
};
|
};
|
||||||
stickyNotes.push(newNote);
|
stickyNotes.push(newNote);
|
||||||
|
socket.emit('sticky-note-add', { room, note: newNote });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStickyNote(id: number, text: string) {
|
function updateStickyNote(id: number, text: string) {
|
||||||
const note = stickyNotes.find(n => n.id === id);
|
const note = stickyNotes.find(n => n.id === id);
|
||||||
if (note) {
|
if (note) {
|
||||||
note.text = text;
|
note.text = text;
|
||||||
|
socket.emit('sticky-note-update', { room, id, text });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,6 +615,11 @@
|
|||||||
note.x = x;
|
note.x = x;
|
||||||
note.y = y;
|
note.y = y;
|
||||||
note.zIndex = ++maxZIndex;
|
note.zIndex = ++maxZIndex;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastEmitTime > 16) { // Throttle to ~60fps
|
||||||
|
socket.emit('sticky-note-move', { room, id, x, y, zIndex: note.zIndex });
|
||||||
|
lastEmitTime = now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,6 +627,7 @@
|
|||||||
const index = stickyNotes.findIndex(n => n.id === id);
|
const index = stickyNotes.findIndex(n => n.id === id);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
stickyNotes.splice(index, 1);
|
stickyNotes.splice(index, 1);
|
||||||
|
socket.emit('sticky-note-delete', { room, id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,7 +637,6 @@
|
|||||||
shuffleMode,
|
shuffleMode,
|
||||||
cards: cards.map(card => ({...card})),
|
cards: cards.map(card => ({...card})),
|
||||||
stickyNotes: stickyNotes.map(note => ({...note})),
|
stickyNotes: stickyNotes.map(note => ({...note})),
|
||||||
canvasOffset,
|
|
||||||
deckPosition,
|
deckPosition,
|
||||||
maxZIndex,
|
maxZIndex,
|
||||||
nextNoteId,
|
nextNoteId,
|
||||||
@ -406,6 +656,16 @@
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreCanvasState(state: any) {
|
||||||
|
selectedDeck = state.selectedDeck ?? 0;
|
||||||
|
shuffleMode = state.shuffleMode ?? 'normal';
|
||||||
|
cards = state.cards ?? [];
|
||||||
|
stickyNotes = state.stickyNotes ?? [];
|
||||||
|
deckPosition = state.deckPosition ?? { x: 50, y: 300 };
|
||||||
|
maxZIndex = state.maxZIndex ?? (cards.length + (stickyNotes.length || 0));
|
||||||
|
nextNoteId = state.nextNoteId ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
function importCanvas() {
|
function importCanvas() {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
@ -419,21 +679,15 @@
|
|||||||
try {
|
try {
|
||||||
const canvasState = JSON.parse(e.target?.result as string);
|
const canvasState = JSON.parse(e.target?.result as string);
|
||||||
|
|
||||||
// Validate the imported data structure
|
|
||||||
if (!canvasState.cards || !Array.isArray(canvasState.cards)) {
|
if (!canvasState.cards || !Array.isArray(canvasState.cards)) {
|
||||||
alert('Invalid canvas file format');
|
alert('Invalid canvas file format');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore state
|
restoreCanvasState(canvasState);
|
||||||
selectedDeck = canvasState.selectedDeck || 0;
|
|
||||||
shuffleMode = canvasState.shuffleMode || 'normal';
|
// Broadcast imported state to others
|
||||||
cards = canvasState.cards || [];
|
socket.emit('import-canvas', { room, state: canvasState });
|
||||||
stickyNotes = canvasState.stickyNotes || [];
|
|
||||||
canvasOffset = canvasState.canvasOffset || { x: 0, y: 0 };
|
|
||||||
deckPosition = canvasState.deckPosition || { x: 50, y: 300 };
|
|
||||||
maxZIndex = canvasState.maxZIndex || cards.length;
|
|
||||||
nextNoteId = canvasState.nextNoteId || 1;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error reading canvas file: ' + error);
|
alert('Error reading canvas file: ' + error);
|
||||||
@ -445,6 +699,12 @@
|
|||||||
input.click();
|
input.click();
|
||||||
document.body.removeChild(input);
|
document.body.removeChild(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.emit('shuffle-mode-change', { room, shuffleMode });
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
@ -485,16 +745,20 @@
|
|||||||
onmousemove={handleCanvasMouseMove}
|
onmousemove={handleCanvasMouseMove}
|
||||||
onmouseup={handleCanvasMouseUp}
|
onmouseup={handleCanvasMouseUp}
|
||||||
oncontextmenu={handleCanvasContextMenu}
|
oncontextmenu={handleCanvasContextMenu}
|
||||||
|
ontouchstart={handleCanvasTouchStart}
|
||||||
|
ontouchmove={handleCanvasTouchMove}
|
||||||
|
ontouchend={handleCanvasTouchEnd}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="canvas-content"
|
class="canvas-content"
|
||||||
style="transform: translate({canvasOffset.x}px, {canvasOffset.y}px);"
|
style="transform: translate({canvasOffset.x}px, {canvasOffset.y}px) scale({canvasZoom});"
|
||||||
>
|
>
|
||||||
{#each cards as card (card.id)}
|
{#each cards as card (card.id)}
|
||||||
<Card
|
<Card
|
||||||
{...card}
|
{...card}
|
||||||
backImage={card.isRiskCategory ? 'risks/risikocat_back.png' : backImage}
|
backImage={card.isRiskCategory ? 'risks/risikocat_back.png' : backImage}
|
||||||
canvasOffset={canvasOffset}
|
canvasOffset={canvasOffset}
|
||||||
|
canvasZoom={canvasZoom}
|
||||||
bind:flipped={card.flipped}
|
bind:flipped={card.flipped}
|
||||||
bind:x={card.x}
|
bind:x={card.x}
|
||||||
bind:y={card.y}
|
bind:y={card.y}
|
||||||
@ -509,6 +773,7 @@
|
|||||||
<StickyNote
|
<StickyNote
|
||||||
{...note}
|
{...note}
|
||||||
canvasOffset={canvasOffset}
|
canvasOffset={canvasOffset}
|
||||||
|
canvasZoom={canvasZoom}
|
||||||
bind:text={note.text}
|
bind:text={note.text}
|
||||||
bind:x={note.x}
|
bind:x={note.x}
|
||||||
bind:y={note.y}
|
bind:y={note.y}
|
||||||
@ -597,18 +862,40 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-area:active {
|
.game-area:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.controls {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
gap: 5px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.canvas-content {
|
.canvas-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 500vw;
|
width: 500vw;
|
||||||
height: 500vh;
|
height: 500vh;
|
||||||
min-width: 500vw;
|
min-width: 500vw;
|
||||||
min-height: 500vh;
|
min-height: 500vh;
|
||||||
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deck-area {
|
.deck-area {
|
||||||
|
|||||||
Reference in New Issue
Block a user