diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66a5be4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +server/images diff --git a/Caddyfile b/Caddyfile index 1d84dad..f4509ce 100644 --- a/Caddyfile +++ b/Caddyfile @@ -2,6 +2,11 @@ desk.local { redir /ws /ws/ redir /desk /desk/ + handle_path /images/* { + root * /code/desk2/server/images + file_server + } + handle /ws/* { reverse_proxy 127.0.0.1:3003 } diff --git a/client/cursor.js b/client/cursor.js index 7afc77a..23ae529 100644 --- a/client/cursor.js +++ b/client/cursor.js @@ -1,7 +1,8 @@ function on_down(e) { if (e.button === 1) { - const event = undo_event(); - queue_event(event); + elements.cursor.classList.add('dhide'); + elements.canvas0.classList.add('moving'); + storage.state.moving = true; return; } @@ -9,6 +10,10 @@ function on_down(e) { return; } + if (storage.state.moving) { + return; + } + const x = Math.round(e.clientX + window.scrollX); const y = Math.round(e.clientY + window.scrollY); @@ -33,11 +38,10 @@ function on_move(e) { const x = storage.cursor.x = Math.round(e.clientX + window.scrollX); const y = storage.cursor.y = Math.round(e.clientY + window.scrollY); - const width = storage.cursor.width; - - elements.cursor.style.transform = `translate3D(${Math.round(x - width / 2)}px, ${Math.round(y - width / 2)}px, 0)`; if (storage.state.drawing) { + const width = storage.cursor.width; + storage.ctx1.beginPath(); storage.ctx1.moveTo(last_x, last_y); @@ -49,19 +53,36 @@ function on_move(e) { storage.current_stroke.push(predraw); fire_event(predraw); + } else if (storage.state.moving) { + storage.canvas.offset_x -= e.movementX; + storage.canvas.offset_y -= e.movementY; + + if (window.scrollX !== storage.canvas.offset_x || window.scrollY !== storage.canvas.offset_y) { + window.scrollTo(storage.canvas.offset_x, storage.canvas.offset_y); + } + + if (storage.canvas.offset_x > storage.canvas.max_scroll_x) storage.canvas.offset_x = storage.canvas.max_scroll_x; + if (storage.canvas.offset_x < 0) storage.canvas.offset_x = 0; + if (storage.canvas.offset_y > storage.canvas.max_scroll_y) storage.canvas.offset_y = storage.canvas.max_scroll_y; + if (storage.canvas.offset_y < 0) storage.canvas.offset_y = 0; } + + e.preventDefault(); } async function on_up(e) { - if (e.button != 0) { - return; + if (storage.state.moving && e.button === 1) { + elements.cursor.classList.remove('dhide'); + elements.canvas0.classList.remove('moving'); + storage.state.moving = false; } - storage.state.drawing = false; - - const event = stroke_event(); - storage.current_stroke = []; - await queue_event(event); + if (storage.state.drawing && e.button === 0) { + storage.state.drawing = false; + const event = stroke_event(); + storage.current_stroke = []; + await queue_event(event); + } } function on_keydown(e) { @@ -81,16 +102,47 @@ function on_keyup(e) { } function on_leave(e) { - if (storage.state.drawing) { - on_up(e); - storage.state.drawing = false; - return; - } - if (storage.state.moving) { elements.cursor.classList.remove('dhide'); elements.canvas0.classList.remove('moving'); storage.state.moving = false; return; } +} + +function on_resize(e) { + storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth; + storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight; +} + +async function on_drop(e) { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + const bitmap = await createImageBitmap(file); + + const x = storage.cursor.x - Math.round(bitmap.width / 2); + const y = storage.cursor.y - Math.round(bitmap.height / 2); + + // storage.ctx0.drawImage(bitmap, x, y); + + const form_data = new FormData(); + form_data.append('file', file); + + const resp = await fetch(`/api/image?deskId=${storage.desk_id}`, { + method: 'post', + body: form_data, + }) + + if (resp.ok) { + const image_id = await resp.text(); + const event = image_event(image_id, x, y); + await queue_event(event); + } + + return false; +} + +function cancel(e) { + e.preventDefault(); + return false; } \ No newline at end of file diff --git a/client/default.css b/client/default.css index 39069ff..ea5c4e9 100644 --- a/client/default.css +++ b/client/default.css @@ -1,6 +1,7 @@ html, body { margin: 0; padding: 0; + overflow: hidden; } .dhide { diff --git a/client/index.js b/client/index.js index b9ee6e3..2c78ff8 100644 --- a/client/index.js +++ b/client/index.js @@ -8,7 +8,9 @@ const EVENT = Object.freeze({ STROKE: 20, UNDO: 30, REDO: 31, + IMAGE: 40, }); + const MESSAGE = Object.freeze({ INIT: 100, SYN: 101, @@ -20,6 +22,7 @@ const MESSAGE = Object.freeze({ const config = { ws_url: 'wss://desk.local/ws/', + image_url: 'https://desk.local/images/', sync_timeout: 1000, ws_reconnect_timeout: 2000, }; @@ -81,9 +84,13 @@ function event_size(event) { break; } + case EVENT.IMAGE: { + size += 4 + 2 + 2; // file id + x + y + break; + } + default: { console.error('fuck'); - process.exit(1); } } @@ -113,6 +120,15 @@ function redo_event() { return { 'type': EVENT.REDO }; } +function image_event(image_id, x, y) { + return { + 'type': EVENT.IMAGE, + 'image_id': image_id, + 'x': x, + 'y': y, + } +} + function main() { const url = new URL(window.location.href); const parts = url.pathname.split('/'); @@ -127,7 +143,11 @@ function main() { elements.cursor.style.width = storage.cursor.width + 'px'; elements.cursor.style.height = storage.cursor.width + 'px'; - storage.canvas.rect = elements.canvas0.getBoundingClientRect(); + storage.canvas.offset_x = window.scrollX; + storage.canvas.offset_y = window.scrollY; + + storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth; + storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight; storage.ctx0 = elements.canvas0.getContext('2d'); storage.ctx1 = elements.canvas1.getContext('2d'); @@ -144,5 +164,9 @@ function main() { window.addEventListener('touchstart', (e) => e.preventDefault()); window.addEventListener('keydown', on_keydown); window.addEventListener('keyup', on_keyup); - // window.addEventListener('pointerleave', on_leave); + window.addEventListener('resize', on_resize); + + elements.canvas0.addEventListener('dragover', on_move); + elements.canvas0.addEventListener('drop', on_drop); + elements.canvas0.addEventListener('pointerleave', on_leave); } diff --git a/client/recv.js b/client/recv.js index 94859d9..e18a832 100644 --- a/client/recv.js +++ b/client/recv.js @@ -66,6 +66,13 @@ function des_event(d) { break; } + case EVENT.IMAGE: { + event.image_id = des_u32(d); + event.x = des_u16(d); + event.y = des_u16(d); + break; + } + case EVENT.UNDO: case EVENT.REDO: { break; @@ -98,7 +105,7 @@ function redraw_region(bbox) { storage.ctx0.restore(); } -function handle_event(event) { +async function handle_event(event) { console.debug(`event type ${event.type} from user ${event.user_id}`); switch (event.type) { @@ -127,6 +134,19 @@ function handle_event(event) { break; } + case EVENT.IMAGE: { + const r = await fetch(config.image_url + event.image_id); + const blob = await r.blob(); + const bitmap = await createImageBitmap(blob); + + + const x = (event.x <= storage.canvas.width ? event.x : event.x - 65536); + const y = (event.y <= storage.canvas.height ? event.y : event.y - 65536); + + storage.ctx0.drawImage(bitmap, x, y); + break; + } + default: { console.error('fuck'); } diff --git a/client/send.js b/client/send.js index 3960122..eab012f 100644 --- a/client/send.js +++ b/client/send.js @@ -48,6 +48,14 @@ function ser_event(s, event) { break; } + case EVENT.IMAGE: { + const image_id = parseInt(event.image_id); + ser_u32(s, image_id); + ser_u16(s, event.x); + ser_u16(s, event.y); + break; + } + case EVENT.UNDO: case EVENT.REDO: { break; @@ -55,7 +63,6 @@ function ser_event(s, event) { default: { console.error('fuck'); - process.exit(1); } } } @@ -120,11 +127,16 @@ function push_event(event) { break; } + case EVENT.IMAGE: case EVENT.UNDO: case EVENT.REDO: { storage.queue.push(event); break; } + + default: { + console.error('fuck'); + } } } diff --git a/server/config.js b/server/config.js index 92eda4c..885c236 100644 --- a/server/config.js +++ b/server/config.js @@ -1,4 +1,5 @@ export const HOST = '127.0.0.1'; export const PORT = 3003; export const DATADIR = 'data'; -export const SYNC_TIMEOUT = 1000; \ No newline at end of file +export const SYNC_TIMEOUT = 1000; +export const IMAGEDIR = 'images'; \ No newline at end of file diff --git a/server/data/db.sqlite b/server/data/db.sqlite index bb57799..965b935 100644 Binary files a/server/data/db.sqlite and b/server/data/db.sqlite differ diff --git a/server/deserializer.js b/server/deserializer.js index 606ed79..399402a 100644 --- a/server/deserializer.js +++ b/server/deserializer.js @@ -51,6 +51,13 @@ export function event(d) { break; } + case EVENT.IMAGE: { + event.image_id = u32(d); + event.x = u16(d); + event.y = u16(d); + break; + } + case EVENT.UNDO: case EVENT.REDO: { break; diff --git a/server/enums.js b/server/enums.js index 7265373..cdc6e08 100644 --- a/server/enums.js +++ b/server/enums.js @@ -9,6 +9,7 @@ export const EVENT = Object.freeze({ STROKE: 20, UNDO: 30, REDO: 31, + IMAGE: 40, }); export const MESSAGE = Object.freeze({ diff --git a/server/http.js b/server/http.js index e0d641f..bd5f6c3 100644 --- a/server/http.js +++ b/server/http.js @@ -1,4 +1,22 @@ -export function route(req) { - console.log('HTTP:', req.url); - return new Response(req.url); +import * as config from './config'; +import * as math from './math'; +import * as storage from './storage'; + +export async function route(req) { + const url = new URL(req.url); + + console.log(url.pathname); + + if (url.pathname === '/api/image') { + const desk_id = url.searchParams.get('deskId') || '0'; + const formData = await req.formData(); + const file = formData.get('file'); + const image_id = math.fast_random32(); + + await Bun.write(config.IMAGEDIR + '/' + image_id, file); + + storage.put_image(image_id, desk_id); + + return new Response(image_id); + } } \ No newline at end of file diff --git a/server/recv.js b/server/recv.js index c84247c..c16de4a 100644 --- a/server/recv.js +++ b/server/recv.js @@ -25,6 +25,7 @@ function handle_event(session, event) { break; } + case EVENT.IMAGE: case EVENT.UNDO: { storage.put_event(event); break; diff --git a/server/send.js b/server/send.js index ac825cd..375f127 100644 --- a/server/send.js +++ b/server/send.js @@ -21,6 +21,11 @@ function event_size(event) { break; } + case EVENT.IMAGE: { + size += 4 + 2 + 2; // file id + x + y + break; + } + case EVENT.UNDO: case EVENT.REDO: { break; diff --git a/server/serializer.js b/server/serializer.js index 9fd289c..054e8d3 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -49,6 +49,13 @@ export function event(s, event) { break; } + case EVENT.IMAGE: { + u32(s, event.image_id); + u16(s, event.x); + u16(s, event.y); + break; + } + case EVENT.UNDO: case EVENT.REDO: { break; diff --git a/server/storage.js b/server/storage.js index 8fd1a5d..0d2730e 100644 --- a/server/storage.js +++ b/server/storage.js @@ -42,6 +42,16 @@ export function startup() { ON UPDATE NO ACTION );`).run(); + db.query(`CREATE TABLE IF NOT EXISTS images ( + id INTEGER PRIMARY KEY, + desk_id INTEGER, + + FOREIGN KEY (desk_id) + REFERENCES desks (id) + ON DELETE CASCADE + ON UPDATE NO ACTION + );`).run(); + db.query(`CREATE TABLE IF NOT EXISTS strokes ( id INTEGER PRIMARY KEY, desk_id INTEGER, @@ -59,13 +69,14 @@ export function startup() { desk_id INTEGER, user_id INTEGER, stroke_id INTEGER, + image_id INTEGER, x INTEGER, y INTEGER, FOREIGN KEY (desk_id) REFERENCES desks (id) ON DELETE CASCADE - ON UPDATE NO ACTION, + ON UPDATE NO ACTION FOREIGN KEY (user_id) REFERENCES users (id) @@ -76,6 +87,11 @@ export function startup() { REFERENCES strokes (id) ON DELETE CASCADE ON UPDATE NO ACTION + + FOREIGN KEY (image_id) + REFERENCES images (id) + ON DELETE CASCADE + ON UPDATE NO ACTION );`).run(); db.query(`CREATE INDEX IF NOT EXISTS idx_events_desk_id @@ -91,9 +107,11 @@ export function startup() { const res3 = db.query('SELECT COUNT(id) as count FROM strokes').get(); const res4 = db.query('SELECT COUNT(id) as count FROM users').get(); const res5 = db.query('SELECT COUNT(id) as count FROM sessions').get(); + const res6 = db.query('SELECT COUNT(id) as count FROM images').get(); + queries.desks = db.query('SELECT id, sn FROM desks'); - queries.events = db.query('SELECT id, desk_id, user_id, stroke_id, type, x, y FROM events'); + queries.events = db.query('SELECT * FROM events'); queries.sessions = db.query('SELECT id, lsn, user_id, desk_id FROM sessions'); queries.strokes = db.query('SELECT id, points FROM strokes'); queries.empty_desk = db.query('INSERT INTO desks (id, title, sn) VALUES (?1, ?2, 0)'); @@ -104,7 +122,8 @@ export function startup() { queries.save_session_lsn = db.query('UPDATE sessions SET lsn = ?1 WHERE id = ?2'); queries.create_session = db.query('INSERT INTO sessions (id, lsn, user_id, desk_id) VALUES (?1, 0, ?2, ?3)'); queries.create_user = db.query('INSERT INTO users (id, login) VALUES (?1, ?2)'); - queries.put_event = db.query('INSERT INTO events (type, desk_id, user_id, stroke_id, x, y) VALUES (?1, ?2, ?3, ?4, ?5, ?6)'); + queries.put_event = db.query('INSERT INTO events (type, desk_id, user_id, stroke_id, image_id, x, y) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)'); + queries.put_image = db.query('INSERT INTO images (id, desk_id) VALUES (?1, ?2)'); console.log(`Storing data in ${path}`); console.log(`Entity count at startup: @@ -112,7 +131,8 @@ export function startup() { ${res2.count} events ${res3.count} strokes ${res4.count} users - ${res5.count} sessions` + ${res5.count} sessions + ${res6.count} images` ); const stored_desks = get_desks(); @@ -169,7 +189,7 @@ export function get_desk_strokes(desk_id) { } export function put_event(event) { - return queries.put_event.get(event.type, event.desk_id || 0, event.user_id || 0, event.stroke_id || 0, event.x || 0, event.y || 0); + return queries.put_event.get(event.type, event.desk_id || 0, event.user_id || 0, event.stroke_id || 0, event.image_id || 0, event.x || 0, event.y || 0); } export function put_stroke(stroke_id, desk_id, points) { @@ -198,4 +218,8 @@ export function create_user(user) { export function save_session_lsn(session_id, lsn) { return queries.save_session_lsn.get(lsn, session_id); +} + +export function put_image(image_id, desk_id) { + return queries.put_image.get(image_id, desk_id); } \ No newline at end of file