Compare commits
27 Commits
Author | SHA1 | Date |
---|---|---|
A.Olokhtonov | ad9cded350 | 2 years ago |
A.Olokhtonov | 6f446b7d13 | 2 years ago |
A.Olokhtonov | 8b3f28337e | 2 years ago |
A.Olokhtonov | 8557c5d47e | 2 years ago |
A.Olokhtonov | 0271e38dbc | 2 years ago |
A.Olokhtonov | 31b18e69a0 | 2 years ago |
A.Olokhtonov | 3b8232e196 | 2 years ago |
A.Olokhtonov | 1edcc6725b | 2 years ago |
A.Olokhtonov | ac0d8f7605 | 2 years ago |
A.Olokhtonov | 33898ab27a | 2 years ago |
A.Olokhtonov | dec07b4edc | 2 years ago |
A.Olokhtonov | cb783db614 | 2 years ago |
A.Olokhtonov | 165d9235ce | 2 years ago |
A.Olokhtonov | 7c2ba5ff72 | 2 years ago |
A.Olokhtonov | 110afe123f | 2 years ago |
A.Olokhtonov | 45c3af9c67 | 2 years ago |
A.Olokhtonov | bf273a9424 | 2 years ago |
A.Olokhtonov | 59cb197e58 | 2 years ago |
A.Olokhtonov | 343008c0af | 2 years ago |
A.Olokhtonov | f24e8d386b | 2 years ago |
A.Olokhtonov | 29f697dceb | 2 years ago |
A.Olokhtonov | 04c11e23f3 | 2 years ago |
A.Olokhtonov | 5c0d9e1537 | 2 years ago |
A.Olokhtonov | 5593536485 | 2 years ago |
A.Olokhtonov | ab152b2d0a | 2 years ago |
A.Olokhtonov | e056d6c698 | 2 years ago |
A.Olokhtonov | 7011cc86be | 2 years ago |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
function ui_offline() { |
||||
document.body.classList.add('offline'); |
||||
document.querySelector('.offline-toast').classList.remove('hidden'); |
||||
} |
||||
|
||||
function ui_online() { |
||||
document.body.classList.remove('offline'); |
||||
document.querySelector('.offline-toast').classList.add('hidden'); |
||||
} |
||||
|
||||
async function insert_image(state, context, file) { |
||||
const bitmap = await createImageBitmap(file); |
||||
|
||||
const p = { 'x': state.cursor.x, 'y': state.cursor.y }; |
||||
const canvasp = screen_to_canvas(state, p); |
||||
|
||||
canvasp.x -= bitmap.width / 2; |
||||
canvasp.y -= bitmap.height / 2; |
||||
|
||||
const form_data = new FormData(); |
||||
form_data.append('file', file); |
||||
|
||||
const resp = await fetch(`/api/image?deskId=${state.desk_id}`, { |
||||
method: 'post', |
||||
body: form_data, |
||||
}) |
||||
|
||||
if (resp.ok) { |
||||
const image_id = await resp.text(); |
||||
const event = image_event(image_id, canvasp.x, canvasp.y); |
||||
await queue_event(state, event); |
||||
} |
||||
} |
||||
|
||||
function event_size(event) { |
||||
let size = 1 + 3; // type + padding
|
||||
|
||||
switch (event.type) { |
||||
case EVENT.PREDRAW: { |
||||
size += 4 * 2; |
||||
break; |
||||
} |
||||
|
||||
case EVENT.SET_COLOR: { |
||||
size += 4; |
||||
break; |
||||
} |
||||
|
||||
case EVENT.SET_WIDTH: { |
||||
size += 2; |
||||
break; |
||||
} |
||||
|
||||
case EVENT.STROKE: { |
||||
size += 4 + 2 + 2 + 4 + event.points.length * 4 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (f32, f32) points
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.UNDO: |
||||
case EVENT.REDO: { |
||||
break; |
||||
} |
||||
|
||||
case EVENT.IMAGE: |
||||
case EVENT.IMAGE_MOVE: { |
||||
size += 4 + 4 + 4; // file id + x + y
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.ERASER: { |
||||
size += 4; // stroke id
|
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
} |
||||
} |
||||
|
||||
return size; |
||||
} |
||||
|
||||
function find_touch(touchlist, id) { |
||||
for (const touch of touchlist) { |
||||
if (touch.identifier === id) { |
||||
return touch; |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
function find_image(state, image_id) { |
||||
for (let i = state.events.length - 1; i >= 0; --i) { |
||||
const event = state.events[i]; |
||||
if (event.type === EVENT.IMAGE && !event.deleted && event.image_id === image_id) { |
||||
return event; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,412 @@
@@ -0,0 +1,412 @@
|
||||
function deserializer_create(buffer, dataview) { |
||||
return { |
||||
'offset': 0, |
||||
'size': buffer.byteLength, |
||||
'buffer': buffer, |
||||
'view': dataview, |
||||
'strview': new Uint8Array(buffer), |
||||
}; |
||||
} |
||||
|
||||
function des_u8(d) { |
||||
const value = d.view.getUint8(d.offset); |
||||
d.offset += 1; |
||||
return value; |
||||
} |
||||
|
||||
function des_u16(d) { |
||||
const value = d.view.getUint16(d.offset, true); |
||||
d.offset += 2; |
||||
return value; |
||||
} |
||||
|
||||
function des_u32(d) { |
||||
const value = d.view.getUint32(d.offset, true); |
||||
d.offset += 4; |
||||
return value; |
||||
} |
||||
|
||||
function des_f32(d) { |
||||
const value = d.view.getFloat32(d.offset, true); |
||||
d.offset += 4; |
||||
return value; |
||||
} |
||||
|
||||
function des_f32array(d, count) { |
||||
const result = []; |
||||
|
||||
for (let i = 0; i < count; ++i) { |
||||
const item = d.view.getFloat32(d.offset, true); |
||||
d.offset += 4; |
||||
result.push(item); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
function des_event(d) { |
||||
const event = {}; |
||||
|
||||
event.type = des_u8(d); |
||||
event.user_id = des_u32(d); |
||||
|
||||
switch (event.type) { |
||||
case EVENT.PREDRAW: { |
||||
event.x = des_f32(d); |
||||
event.y = des_f32(d); |
||||
break; |
||||
} |
||||
|
||||
case EVENT.SET_COLOR: { |
||||
event.color = des_u32(d); |
||||
break; |
||||
} |
||||
|
||||
case EVENT.SET_WIDTH: { |
||||
event.width = des_u16(d); |
||||
break; |
||||
} |
||||
|
||||
case EVENT.STROKE: { |
||||
const stroke_id = des_u32(d); |
||||
const point_count = des_u16(d); |
||||
const width = des_u16(d); |
||||
const color = des_u32(d); |
||||
const coords = des_f32array(d, point_count * 2); |
||||
|
||||
event.stroke_id = stroke_id; |
||||
event.points = []; |
||||
|
||||
for (let i = 0; i < point_count; ++i) { |
||||
const x = coords[2 * i + 0]; |
||||
const y = coords[2 * i + 1]; |
||||
event.points.push({'x': x, 'y': y}); |
||||
} |
||||
|
||||
event.color = color; |
||||
event.width = width; |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.IMAGE: |
||||
case EVENT.IMAGE_MOVE: { |
||||
event.image_id = des_u32(d); |
||||
event.x = des_f32(d); |
||||
event.y = des_f32(d); |
||||
break; |
||||
} |
||||
|
||||
case EVENT.UNDO: |
||||
case EVENT.REDO: { |
||||
break; |
||||
} |
||||
|
||||
case EVENT.ERASER: { |
||||
event.stroke_id = des_u32(d); |
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
} |
||||
} |
||||
|
||||
return event; |
||||
} |
||||
|
||||
function bitmap_bbox(event) { |
||||
const bbox = { |
||||
'xmin': event.x, |
||||
'xmax': event.x + event.bitmap.width, |
||||
'ymin': event.y, |
||||
'ymax': event.y + event.bitmap.height |
||||
}; |
||||
|
||||
return bbox; |
||||
} |
||||
|
||||
function init_player_defaults(state, player_id, color = config.default_color, width = config.default_width) { |
||||
state.players[player_id] = { |
||||
'color': color, |
||||
'width': width, |
||||
'points': [], |
||||
}; |
||||
} |
||||
|
||||
function handle_event(state, context, event) { |
||||
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); |
||||
|
||||
let need_draw = false; |
||||
|
||||
if (!(event.user_id in state.players)) { |
||||
init_player_defaults(state, event.user_id); |
||||
} |
||||
|
||||
switch (event.type) { |
||||
case EVENT.PREDRAW: { |
||||
geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y}); |
||||
need_draw = true; |
||||
break; |
||||
} |
||||
|
||||
case EVENT.SET_COLOR: { |
||||
state.players[event.user_id].color = event.color; |
||||
break; |
||||
} |
||||
|
||||
case EVENT.SET_WIDTH: { |
||||
state.players[event.user_id].width = event.width; |
||||
break; |
||||
} |
||||
|
||||
case EVENT.STROKE: { |
||||
if (event.user_id != state.me) { |
||||
geometry_clear_player(state, context, event.user_id); |
||||
need_draw = true; |
||||
} |
||||
|
||||
geometry_add_stroke(state, context, event); |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.UNDO: { |
||||
need_draw = true; |
||||
console.error('todo'); |
||||
// for (let i = state.events.length - 1; i >=0; --i) {
|
||||
// const other_event = state.events[i];
|
||||
|
||||
// // Users can only undo their own, undeleted (not already undone) events
|
||||
// if (other_event.user_id === event.user_id && !other_event.deleted) {
|
||||
// if (other_event.type === EVENT.STROKE) {
|
||||
// other_event.deleted = true;
|
||||
// const stats = stroke_stats(other_event.points, state.cursor.width);
|
||||
// redraw_region(stats.bbox);
|
||||
// break;
|
||||
// } else if (other_event.type === EVENT.IMAGE) {
|
||||
// other_event.deleted = true;
|
||||
// const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`);
|
||||
// if (item) item.remove();
|
||||
// break;
|
||||
// } else if (other_event.type === EVENT.ERASER) {
|
||||
// other_event.deleted = true;
|
||||
// const erased = find_stroke_backwards(other_event.stroke_id);
|
||||
// if (erased) {
|
||||
// erased.deleted = false;
|
||||
// const stats = stroke_stats(erased.points, state.cursor.width);
|
||||
// redraw_region(stats.bbox);
|
||||
// }
|
||||
// break;
|
||||
// } else if (other_event.type === EVENT.IMAGE_MOVE) {
|
||||
// const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`);
|
||||
|
||||
// const ix = state.images[other_event.image_id].x -= other_event.x;
|
||||
// const iy = state.images[other_event.image_id].y -= other_event.y;
|
||||
|
||||
// item.style.transform = `translate(${ix}px, ${iy}px)`;
|
||||
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.IMAGE: { |
||||
try { |
||||
(async () => { |
||||
const url = config.image_url + event.image_id; |
||||
const r = await fetch(config.image_url + event.image_id); |
||||
const blob = await r.blob(); |
||||
const bitmap = await createImageBitmap(blob); |
||||
const p = {'x': event.x, 'y': event.y}; |
||||
|
||||
event.width = bitmap.width; |
||||
event.height = bitmap.height; |
||||
|
||||
add_image(context, event.image_id, bitmap, p); |
||||
|
||||
// God knows when this will actually complete (it loads the image from the server)
|
||||
// so do not set need_draw. Instead just schedule the draw ourselves when done
|
||||
schedule_draw(state, context); |
||||
})(); |
||||
} catch (e) { |
||||
console.log('Could not load image bitmap:', e); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.IMAGE_MOVE: { |
||||
// Already moved due to local prediction
|
||||
if (event.user_id !== state.me) { |
||||
const image_id = event.image_id; |
||||
const image_event = find_image(state, image_id); |
||||
|
||||
if (image_event) { |
||||
// if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y);
|
||||
image_event.x = event.x; |
||||
image_event.y = event.y; |
||||
move_image(context, image_event); |
||||
need_draw = true; |
||||
} |
||||
} |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.ERASER: { |
||||
need_draw = true; |
||||
console.error('todo'); |
||||
// if (event.deleted) {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// for (const other_event of state.events) {
|
||||
// if (other_event.type === EVENT.STROKE && other_event.stroke_id === event.stroke_id) {
|
||||
// // Might already be deleted because of local prediction
|
||||
// if (!other_event.deleted) {
|
||||
// other_event.deleted = true;
|
||||
// const stats = stroke_stats(other_event.points, state.cursor.width);
|
||||
// redraw_region(stats.bbox);
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
} |
||||
} |
||||
|
||||
return need_draw; |
||||
} |
||||
|
||||
async function handle_message(state, context, d) { |
||||
const message_type = des_u8(d); |
||||
let do_draw = false; |
||||
|
||||
// if (config.debug_print) console.debug(message_type);
|
||||
|
||||
switch (message_type) { |
||||
case MESSAGE.JOIN: |
||||
case MESSAGE.INIT: { |
||||
state.server_lsn = des_u32(d); |
||||
state.online = true; |
||||
|
||||
if (state.server_lsn > state.lsn) { |
||||
// Server knows something that we don't
|
||||
state.lsn = state.server_lsn; |
||||
} |
||||
|
||||
let color = config.default_color; |
||||
let width = config.default_width; |
||||
|
||||
if (message_type === MESSAGE.JOIN) { |
||||
localStorage.setItem('sessionId', des_u32(d)); |
||||
if (config.debug_print) console.debug('join in'); |
||||
} else { |
||||
color = des_u32(d); |
||||
width = des_u16(d); |
||||
state.me = parseInt(localStorage.getItem('sessionId')); |
||||
if (config.debug_print) console.debug('init in'); |
||||
} |
||||
|
||||
init_player_defaults(state, state.me); |
||||
|
||||
const color_code = color_from_u32(color).substring(1); |
||||
|
||||
switch_color(state, document.querySelector(`.color[data-color="${color_code}"]`)); |
||||
document.querySelector('#stroke-width').value = width; |
||||
fire_event(state, width_event(width)); |
||||
|
||||
const event_count = des_u32(d); |
||||
const user_count = des_u32(d); |
||||
|
||||
if (config.debug_print) console.debug(`${event_count} events in init`); |
||||
|
||||
state.events.length = 0; |
||||
|
||||
for (let i = 0; i < user_count; ++i) { |
||||
const user_id = des_u32(d); |
||||
const user_color = des_u32(d); |
||||
const user_width = des_u16(d); |
||||
|
||||
init_player_defaults(state, user_id, user_color, user_width); |
||||
} |
||||
|
||||
for (let i = 0; i < event_count; ++i) { |
||||
const event = des_event(d); |
||||
handle_event(state, context, event); |
||||
state.events.push(event); |
||||
} |
||||
|
||||
do_draw = true; |
||||
|
||||
send_ack(event_count); |
||||
sync_queue(state); |
||||
|
||||
break; |
||||
} |
||||
|
||||
case MESSAGE.FIRE: { |
||||
const event = des_event(d); |
||||
const need_draw = handle_event(state, context, event); |
||||
|
||||
do_draw = do_draw || need_draw; |
||||
|
||||
break; |
||||
} |
||||
|
||||
case MESSAGE.ACK: { |
||||
const lsn = des_u32(d); |
||||
|
||||
if (config.debug_print) console.debug(`ack ${lsn} in`); |
||||
|
||||
if (lsn > state.server_lsn) { |
||||
// ACKs may arrive out of order
|
||||
state.server_lsn = lsn; |
||||
} |
||||
|
||||
break; |
||||
} |
||||
|
||||
case MESSAGE.SYN: { |
||||
const sn = des_u32(d); |
||||
const count = des_u32(d); |
||||
|
||||
const we_expect = sn - state.sn; |
||||
const first = count - we_expect; |
||||
|
||||
if (config.debug_print) console.debug(`syn ${sn} in`); |
||||
|
||||
for (let i = 0; i < count; ++i) { |
||||
const event = des_event(d); |
||||
if (i >= first) { |
||||
const need_draw = handle_event(state, context, event); |
||||
do_draw = do_draw || need_draw; |
||||
state.events.push(event); |
||||
} |
||||
} |
||||
|
||||
state.sn = sn; |
||||
|
||||
send_ack(sn); // await?
|
||||
|
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
if (do_draw) { |
||||
schedule_draw(state, context); |
||||
} |
||||
} |
@ -1,66 +0,0 @@
@@ -1,66 +0,0 @@
|
||||
function draw_stroke(stroke) { |
||||
const points = stroke.points; |
||||
|
||||
if (points.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
// if (config.debug_print) console.debug(points)
|
||||
|
||||
storage.ctx0.beginPath(); |
||||
storage.ctx0.moveTo(points[0].x, points[0].y); |
||||
storage.ctx0.strokeStyle = color_from_u32(stroke.color); |
||||
storage.ctx0.lineWidth = stroke.width; |
||||
|
||||
for (let i = 1; i < points.length; ++i) { |
||||
const p = points[i]; |
||||
storage.ctx0.lineTo(p.x, p.y); |
||||
} |
||||
|
||||
storage.ctx0.stroke(); |
||||
} |
||||
|
||||
function redraw_predraw() { |
||||
storage.ctx1.clearRect(0, 0, storage.ctx1.canvas.width, storage.ctx1.canvas.height); |
||||
} |
||||
|
||||
function predraw_user(user_id, event) { |
||||
if (!(user_id in storage.predraw)) { |
||||
storage.predraw[user_id] = []; |
||||
} |
||||
|
||||
storage.ctx1.beginPath(); |
||||
if (storage.predraw[user_id].length > 0) { |
||||
const last = storage.predraw[user_id][storage.predraw[user_id].length - 1]; |
||||
storage.ctx1.moveTo(last.x, last.y); |
||||
storage.ctx1.lineTo(event.x, event.y); |
||||
} else { |
||||
storage.ctx1.moveTo(event.x, event.y); |
||||
} |
||||
storage.ctx1.stroke(); |
||||
|
||||
storage.predraw[user_id].push({ 'x': event.x, 'y': event.y }); |
||||
} |
||||
|
||||
function redraw_region(bbox) { |
||||
if (bbox.xmin === bbox.xmax || bbox.ymin === bbox.ymax) { |
||||
return; |
||||
} |
||||
|
||||
storage.ctx0.save(); |
||||
storage.ctx0.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
||||
|
||||
storage.ctx0.beginPath(); |
||||
storage.ctx0.rect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
||||
storage.ctx0.clip(); |
||||
|
||||
for (const event of storage.events) { |
||||
if (event.type === EVENT.STROKE && !event.deleted) { |
||||
if (stroke_intersects_region(event.points, bbox)) { |
||||
draw_stroke(event); |
||||
} |
||||
} |
||||
} |
||||
|
||||
storage.ctx0.restore(); |
||||
} |
Before Width: | Height: | Size: 671 B |
Before Width: | Height: | Size: 957 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 500 B |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 691 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 457 B |
@ -1,345 +0,0 @@
@@ -1,345 +0,0 @@
|
||||
function deserializer_create(buffer, dataview) { |
||||
return { |
||||
'offset': 0, |
||||
'size': buffer.byteLength, |
||||
'buffer': buffer, |
||||
'view': dataview, |
||||
'strview': new Uint8Array(buffer), |
||||
}; |
||||
} |
||||
|
||||
function des_u8(d) { |
||||
const value = d.view.getUint8(d.offset); |
||||
d.offset += 1; |
||||
return value; |
||||
} |
||||
|
||||
function des_u16(d) { |
||||
const value = d.view.getUint16(d.offset, true); |
||||
d.offset += 2; |
||||
return value; |
||||
} |
||||
|
||||
function des_s16(d) { |
||||
const value = d.view.getInt16(d.offset, true); |
||||
d.offset += 2; |
||||
return value; |
||||
} |
||||
|
||||
function des_u32(d) { |
||||
const value = d.view.getUint32(d.offset, true); |
||||
d.offset += 4; |
||||
return value; |
||||
} |
||||
|
||||
function des_u16array(d, count) { |
||||
const result = []; |
||||
|
||||
for (let i = 0; i < count; ++i) { |
||||
const item = d.view.getUint16(d.offset, true); |
||||
d.offset += 2; |
||||
result.push(item); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
function des_event(d) { |
||||
const event = {}; |
||||
|
||||
event.type = des_u8(d); |
||||
event.user_id = des_u32(d); |
||||
|
||||
switch (event.type) { |
||||
case EVENT.PREDRAW: { |
||||
event.x = des_u16(d); |
||||
event.y = des_u16(d); |
||||
break; |
||||
} |
||||
|
||||
case EVENT.STROKE: { |
||||
const stroke_id = des_u32(d); |
||||
const point_count = des_u16(d); |
||||
const width = des_u16(d); |
||||
const color = des_u32(d); |
||||
const coords = des_u16array(d, point_count * 2); |
||||
|
||||
event.stroke_id = stroke_id; |
||||
event.points = []; |
||||
|
||||
for (let i = 0; i < point_count; ++i) { |
||||
const x = coords[2 * i + 0]; |
||||
const y = coords[2 * i + 1]; |
||||
event.points.push({'x': x, 'y': y}); |
||||
} |
||||
|
||||
event.color = color; |
||||
event.width = width; |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.IMAGE: |
||||
case EVENT.IMAGE_MOVE: { |
||||
event.image_id = des_u32(d); |
||||
event.x = des_s16(d); // stored as u16, but actually is s16
|
||||
event.y = des_s16(d); // stored as u16, but actually is s16
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.UNDO: |
||||
case EVENT.REDO: { |
||||
break; |
||||
} |
||||
|
||||
case EVENT.ERASER: { |
||||
event.stroke_id = des_u32(d); |
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
} |
||||
} |
||||
|
||||
return event; |
||||
} |
||||
|
||||
function bitmap_bbox(event) { |
||||
const x = (event.x <= storage.canvas.width ? event.x : event.x - 65536); |
||||
const y = (event.y <= storage.canvas.height ? event.y : event.y - 65536); |
||||
|
||||
const bbox = { |
||||
'xmin': x, |
||||
'xmax': x + event.bitmap.width, |
||||
'ymin': y, |
||||
'ymax': y + event.bitmap.height |
||||
}; |
||||
|
||||
return bbox; |
||||
} |
||||
|
||||
async function handle_event(event) { |
||||
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); |
||||
|
||||
// TODO(@speed): do not handle locally predicted events
|
||||
|
||||
switch (event.type) { |
||||
case EVENT.STROKE: { |
||||
if (event.user_id in storage.predraw || event.user_id === storage.me.id) { |
||||
storage.predraw[event.user_id] = []; |
||||
redraw_predraw(); |
||||
} |
||||
|
||||
draw_stroke(event); |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.UNDO: { |
||||
for (let i = storage.events.length - 1; i >=0; --i) { |
||||
const other_event = storage.events[i]; |
||||
|
||||
// Users can only undo their own, undeleted (not already undone) events
|
||||
if (other_event.user_id === event.user_id && !other_event.deleted) { |
||||
if (other_event.type === EVENT.STROKE) { |
||||
other_event.deleted = true; |
||||
const stats = stroke_stats(other_event.points, storage.cursor.width); |
||||
redraw_region(stats.bbox); |
||||
break; |
||||
} else if (other_event.type === EVENT.IMAGE) { |
||||
other_event.deleted = true; |
||||
const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); |
||||
if (item) item.remove(); |
||||
break; |
||||
} else if (other_event.type === EVENT.ERASER) { |
||||
other_event.deleted = true; |
||||
const erased = find_stroke_backwards(other_event.stroke_id); |
||||
if (erased) { |
||||
erased.deleted = false; |
||||
const stats = stroke_stats(erased.points, storage.cursor.width); |
||||
redraw_region(stats.bbox); |
||||
} |
||||
break; |
||||
} else if (other_event.type === EVENT.IMAGE_MOVE) { |
||||
const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); |
||||
|
||||
const ix = storage.images[other_event.image_id].x -= other_event.x; |
||||
const iy = storage.images[other_event.image_id].y -= other_event.y; |
||||
|
||||
item.style.transform = `translate(${ix}px, ${iy}px)`; |
||||
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.IMAGE: { |
||||
const url = config.image_url + event.image_id; |
||||
const item = document.createElement('img'); |
||||
|
||||
item.classList.add('floating-image'); |
||||
item.style['z-index'] = storage.events.length; |
||||
item.setAttribute('data-image-id', event.image_id); |
||||
item.setAttribute('src', url); |
||||
item.style.transform = `translate(${event.x}px, ${event.y}px)`; |
||||
|
||||
elements.images.appendChild(item); |
||||
storage.images[event.image_id] = { |
||||
'x': event.x, 'y': event.y |
||||
}; |
||||
// const r = await fetch(config.image_url + event.image_id);
|
||||
// const blob = await r.blob();
|
||||
// const bitmap = await createImageBitmap(blob);
|
||||
// event.bitmap = bitmap;
|
||||
// const bbox = bitmap_bbox(event);
|
||||
// storage.ctx0.drawImage(bitmap, bbox.xmin, bbox.ymin);
|
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.IMAGE_MOVE: { |
||||
// Already moved due to local prediction
|
||||
if (event.user_id !== storage.me.id) { |
||||
const image_id = event.image_id; |
||||
const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`); |
||||
|
||||
const ix = storage.images[event.image_id].x += event.x; |
||||
const iy = storage.images[event.image_id].y += event.y; |
||||
|
||||
if (item) { |
||||
item.style.transform = `translate(${ix}px, ${iy}px)`; |
||||
} |
||||
} |
||||
|
||||
break; |
||||
} |
||||
|
||||
case EVENT.ERASER: { |
||||
if (event.deleted) { |
||||
break; |
||||
} |
||||
|
||||
for (const other_event of storage.events) { |
||||
if (other_event.type === EVENT.STROKE && other_event.stroke_id === event.stroke_id) { |
||||
// Might already be deleted because of local prediction
|
||||
if (!other_event.deleted) { |
||||
other_event.deleted = true; |
||||
const stats = stroke_stats(other_event.points, storage.cursor.width); |
||||
redraw_region(stats.bbox); |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async function handle_message(d) { |
||||
const message_type = des_u8(d); |
||||
|
||||
if (config.debug_print) console.debug(message_type); |
||||
|
||||
switch (message_type) { |
||||
case MESSAGE.JOIN: |
||||
case MESSAGE.INIT: { |
||||
elements.canvas0.classList.add('white'); |
||||
|
||||
storage.me.id = des_u32(d); |
||||
storage.server_lsn = des_u32(d); |
||||
|
||||
if (storage.server_lsn > storage.lsn) { |
||||
// Server knows something that we don't
|
||||
storage.lsn = storage.server_lsn; |
||||
} |
||||
|
||||
if (message_type === MESSAGE.JOIN) { |
||||
ls.setItem('sessionId', des_u32(d)); |
||||
if (config.debug_print) console.debug('join in'); |
||||
} else { |
||||
if (config.debug_print) console.debug('init in'); |
||||
} |
||||
|
||||
const event_count = des_u32(d); |
||||
|
||||
if (config.debug_print) console.debug(`${event_count} events in init`); |
||||
|
||||
storage.ctx0.clearRect(0, 0, storage.ctx0.canvas.width, storage.ctx0.canvas.height); |
||||
elements.images.innerHTML = ''; |
||||
storage.events.length = 0; |
||||
|
||||
for (let i = 0; i < event_count; ++i) { |
||||
const event = des_event(d); |
||||
await handle_event(event); |
||||
storage.events.push(event); |
||||
} |
||||
|
||||
elements.canvas0.classList.remove('white'); |
||||
|
||||
send_ack(event_count); |
||||
|
||||
sync_queue(); |
||||
|
||||
break; |
||||
} |
||||
|
||||
case MESSAGE.FIRE: { |
||||
const user_id = des_u32(d); |
||||
const predraw_event = des_event(d); |
||||
|
||||
predraw_user(user_id, predraw_event); |
||||
|
||||
break; |
||||
} |
||||
|
||||
case MESSAGE.ACK: { |
||||
const lsn = des_u32(d); |
||||
|
||||
if (config.debug_print) console.debug(`ack ${lsn} in`); |
||||
|
||||
if (lsn > storage.server_lsn) { |
||||
// ACKs may arrive out of order
|
||||
storage.server_lsn = lsn; |
||||
} |
||||
|
||||
break; |
||||
} |
||||
|
||||
case MESSAGE.SYN: { |
||||
const sn = des_u32(d); |
||||
const count = des_u32(d); |
||||
|
||||
const we_expect = sn - storage.sn; |
||||
const first = count - we_expect; |
||||
|
||||
if (config.debug_print) console.debug(`syn ${sn} in`); |
||||
|
||||
for (let i = 0; i < count; ++i) { |
||||
const event = des_event(d); |
||||
if (i >= first) { |
||||
handle_event(event); |
||||
storage.events.push(event); |
||||
} |
||||
} |
||||
|
||||
storage.sn = sn; |
||||
|
||||
await send_ack(sn); |
||||
|
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
return; |
||||
} |
||||
} |
||||
} |
@ -1,23 +1,86 @@
@@ -1,23 +1,86 @@
|
||||
function tools_switch(tool) { |
||||
if (storage.tools.active_element) { |
||||
storage.tools.active_element.classList.remove('active'); |
||||
function switch_tool(state, item) { |
||||
const tool = item.getAttribute('data-tool'); |
||||
|
||||
if (tool === 'undo') { |
||||
return; |
||||
} |
||||
|
||||
if (state.tools.active_element) { |
||||
state.tools.active_element.classList.remove('active'); |
||||
} |
||||
|
||||
state.tools.active = tool; |
||||
state.tools.active_element = item; |
||||
state.tools.active_element.classList.add('active'); |
||||
} |
||||
|
||||
function switch_color(state, item) { |
||||
const color = item.getAttribute('data-color'); |
||||
|
||||
if (state.colors.active_element) { |
||||
state.colors.active_element.classList.remove('active'); |
||||
} |
||||
|
||||
if (state.me in state.players) { |
||||
const color_u32 = color_to_u32(color); |
||||
state.players[state.me].color = color_u32 |
||||
fire_event(state, color_event(color_u32)); |
||||
} |
||||
|
||||
state.colors.active_element = item; |
||||
state.colors.active_element.classList.add('active'); |
||||
} |
||||
|
||||
function show_stroke_preview(state, size) { |
||||
const preview = document.querySelector('#stroke-preview'); |
||||
|
||||
preview.style.width = size * state.canvas.zoom + 'px'; |
||||
preview.style.height = size * state.canvas.zoom + 'px'; |
||||
preview.style.background = color_from_u32(state.players[state.me].color); |
||||
|
||||
preview.classList.remove('dhide'); |
||||
} |
||||
|
||||
function hide_stroke_preview() { |
||||
document.querySelector('#stroke-preview').classList.add('dhide'); |
||||
} |
||||
|
||||
function switch_stroke_width(e, state) { |
||||
if (!state.online) return; |
||||
|
||||
const value = e.target.value; |
||||
|
||||
state.players[state.me].width = value; |
||||
show_stroke_preview(state, value); |
||||
|
||||
if (state.hide_preview) { |
||||
clearTimeout(state.hide_preview); |
||||
} |
||||
|
||||
state.hide_preview = setTimeout(hide_stroke_preview, config.brush_preview_timeout); |
||||
} |
||||
|
||||
storage.tools.active = tool; |
||||
storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"]`); |
||||
storage.tools.active_element.classList.add('active'); |
||||
function broadcast_stroke_width(e, state) { |
||||
const value = e.target.value; |
||||
fire_event(state, width_event(value)); |
||||
} |
||||
|
||||
function tools_init() { |
||||
const pencil = document.querySelector('.tool[data-tool="pencil"]'); |
||||
const ruler = document.querySelector('.tool[data-tool="ruler"]'); |
||||
const eraser = document.querySelector('.tool[data-tool="eraser"]'); |
||||
const undo = document.querySelector('.tool[data-tool="undo"]'); |
||||
function init_tools(state) { |
||||
const tools = document.querySelectorAll('.tools .tool'); |
||||
const colors = document.querySelectorAll('.pallete .color'); |
||||
|
||||
tools.forEach((item) => { item.addEventListener('click', () => switch_tool(state, item)); }); |
||||
colors.forEach((item) => { item.addEventListener('click', () => switch_color(state, item)); }); |
||||
|
||||
// TODO: from localstorage
|
||||
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]')); |
||||
switch_color(state, document.querySelector('.color[data-color="000000"]')); |
||||
|
||||
const slider = document.querySelector('#stroke-width'); |
||||
|
||||
pencil.addEventListener('click', () => tools_switch('pencil')); |
||||
ruler.addEventListener('click', () => tools_switch('ruler')); |
||||
eraser.addEventListener('click', () => tools_switch('eraser')); |
||||
undo.addEventListener('click', queue_undo); |
||||
// slider.value = state.players[state.me].width;
|
||||
slider.addEventListener('input', (e) => switch_stroke_width(e, state)); |
||||
slider.addEventListener('change', (e) => broadcast_stroke_width(e, state)); |
||||
|
||||
tools_switch('pencil'); |
||||
document.querySelector('.phone-extra-controls').addEventListener('click', zenmode); |
||||
} |
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
function schedule_draw(state, context) { |
||||
if (!state.timers.raf) { |
||||
window.requestAnimationFrame(() => draw(state, context)); |
||||
state.timers.raf = true; |
||||
} |
||||
} |
||||
|
||||
function draw(state, context) { |
||||
state.timers.raf = false; |
||||
|
||||
const gl = context.gl; |
||||
const width = window.innerWidth; |
||||
const height = window.innerHeight; |
||||
|
||||
let locations; |
||||
let buffers; |
||||
|
||||
gl.viewport(0, 0, context.canvas.width, context.canvas.height); |
||||
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); |
||||
gl.clear(gl.COLOR_BUFFER_BIT); |
||||
|
||||
// Images
|
||||
locations = context.locations['quad']; |
||||
buffers = context.buffers['quad']; |
||||
|
||||
gl.useProgram(context.programs['quad']); |
||||
|
||||
gl.enableVertexAttribArray(locations['a_pos']); |
||||
gl.enableVertexAttribArray(locations['a_texcoord']); |
||||
|
||||
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
gl.uniform1i(locations['u_texture'], 0); |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']); |
||||
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0); |
||||
gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_DRAW); |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']); |
||||
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); |
||||
gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW); |
||||
|
||||
const count = Object.keys(context.textures).length; |
||||
let active_image_index = -1; |
||||
|
||||
gl.uniform1i(locations['u_outline'], 0); |
||||
|
||||
for (let key = 0; key < count; ++key) { |
||||
if (context.textures[key].image_id === context.active_image) { |
||||
active_image_index = key; |
||||
continue; |
||||
} |
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, context.textures[key].texture); |
||||
gl.drawArrays(gl.TRIANGLES, key * 6, 6); |
||||
} |
||||
|
||||
if (active_image_index !== -1) { |
||||
gl.uniform1i(locations['u_outline'], 1); |
||||
gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture); |
||||
gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); |
||||
} |
||||
|
||||
// Strokes
|
||||
locations = context.locations['stroke']; |
||||
buffers = context.buffers['stroke']; |
||||
|
||||
gl.useProgram(context.programs['stroke']); |
||||
|
||||
gl.enableVertexAttribArray(locations['a_type']); |
||||
gl.enableVertexAttribArray(locations['a_pos']); |
||||
gl.enableVertexAttribArray(locations['a_texcoord']); |
||||
gl.enableVertexAttribArray(locations['a_color']); |
||||
|
||||
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); |
||||
|
||||
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, config.bytes_per_point, 0); |
||||
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, config.bytes_per_point, 8); |
||||
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 16); |
||||
gl.vertexAttribPointer(locations['a_type'], 1, gl.UNSIGNED_BYTE, false, config.bytes_per_point, 19); |
||||
|
||||
gl.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW); |
||||
gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point); |
||||
|
||||
gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_stroke_serializer.buffer, gl.STATIC_DRAW); |
||||
gl.drawArrays(gl.TRIANGLES, 0, context.dynamic_stroke_serializer.offset / config.bytes_per_point); |
||||
} |
@ -0,0 +1,244 @@
@@ -0,0 +1,244 @@
|
||||
function push_point(s, x, y, u, v, r, g, b, type) { |
||||
ser_f32(s, x); |
||||
ser_f32(s, y); |
||||
ser_f32(s, u); |
||||
ser_f32(s, v); |
||||
// ser_u8(s, Math.floor(Math.random() * 255));
|
||||
// ser_u8(s, Math.floor(Math.random() * 255));
|
||||
// ser_u8(s, Math.floor(Math.random() * 255));
|
||||
ser_u8(s, r); |
||||
ser_u8(s, g); |
||||
ser_u8(s, b); |
||||
ser_u8(s, type); |
||||
} |
||||
|
||||
function push_circle(s, cx, cy, radius, r, g, b) { |
||||
push_point(s, cx - radius, cy - radius, 0, 0, r, g, b, 1); |
||||
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1); |
||||
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1); |
||||
|
||||
push_point(s, cx + radius, cy + radius, 1, 1, r, g, b, 1); |
||||
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1); |
||||
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1); |
||||
} |
||||
|
||||
function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, r, g, b) { |
||||
push_point(s, p1x, p1y, 0, 0, r, g, b, 0); |
||||
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); |
||||
push_point(s, p3x, p3y, 1, 0, r, g, b, 0); |
||||
|
||||
push_point(s, p4x, p4y, 1, 1, r, g, b, 0); |
||||
push_point(s, p3x, p3y, 1, 0, r, g, b, 0); |
||||
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); |
||||
} |
||||
|
||||
function push_stroke(s, stroke) { |
||||
const stroke_width = stroke.width; |
||||
const points = stroke.points; |
||||
const color_u32 = stroke.color; |
||||
|
||||
const r = (color_u32 >> 16) & 0xFF; |
||||
const g = (color_u32 >> 8) & 0xFF; |
||||
const b = color_u32 & 0xFF; |
||||
|
||||
if (points.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
if (points.length === 1) { |
||||
push_circle(s, points[0].x, points[0].y, stroke_width / 2, r, g, b); |
||||
return; |
||||
} |
||||
|
||||
for (let i = 0; i < points.length - 1; ++i) { |
||||
const px = points[i].x; |
||||
const py = points[i].y; |
||||
|
||||
const nextpx = points[i + 1].x; |
||||
const nextpy = points[i + 1].y; |
||||
|
||||
const d1x = nextpx - px; |
||||
const d1y = nextpy - py; |
||||
|
||||
// Perpendicular to (d1x, d1y), points to the LEFT
|
||||
let perp1x = -d1y; |
||||
let perp1y = d1x; |
||||
|
||||
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); |
||||
|
||||
perp1x /= perpnorm1; |
||||
perp1y /= perpnorm1; |
||||
|
||||
const s1x = px + perp1x * stroke_width / 2; |
||||
const s1y = py + perp1y * stroke_width / 2; |
||||
const s2x = px - perp1x * stroke_width / 2; |
||||
const s2y = py - perp1y * stroke_width / 2; |
||||
|
||||
const s3x = nextpx + perp1x * stroke_width / 2; |
||||
const s3y = nextpy + perp1y * stroke_width / 2; |
||||
const s4x = nextpx - perp1x * stroke_width / 2; |
||||
const s4y = nextpy - perp1y * stroke_width / 2; |
||||
|
||||
push_quad(s, s2x, s2y, s1x, s1y, s4x, s4y, s3x, s3y, r, g, b); |
||||
push_circle(s, px, py, stroke_width / 2, r, g, b); |
||||
} |
||||
|
||||
const lastp = points[points.length - 1]; |
||||
|
||||
push_circle(s, lastp.x, lastp.y, stroke_width / 2, r, g, b); |
||||
} |
||||
|
||||
function geometry_prepare_stroke(state) { |
||||
if (!state.online) { |
||||
return null; |
||||
} |
||||
|
||||
return { |
||||
'color': state.players[state.me].color, |
||||
'width': state.players[state.me].width, |
||||
'points': process_stroke(state, state.players[state.me].points), |
||||
'user_id': state.me, |
||||
}; |
||||
} |
||||
|
||||
function geometry_add_stroke(state, context, stroke) { |
||||
if (!state.online || !stroke) return; |
||||
|
||||
const bytes_left = context.static_stroke_serializer.size - context.static_stroke_serializer.offset; |
||||
const bytes_needed = (stroke.points.length * 12 + 6) * config.bytes_per_point; |
||||
|
||||
if (bytes_left < bytes_needed) { |
||||
const old_view = context.static_stroke_serializer.strview; |
||||
const old_offset = context.static_stroke_serializer.offset; |
||||
|
||||
const new_size = Math.ceil((context.static_stroke_serializer.size + bytes_needed) * 1.62); |
||||
|
||||
context.static_stroke_serializer = serializer_create(new_size); |
||||
context.static_stroke_serializer.strview.set(old_view); |
||||
context.static_stroke_serializer.offset = old_offset; |
||||
} |
||||
|
||||
push_stroke(context.static_stroke_serializer, stroke); |
||||
} |
||||
|
||||
function recompute_dynamic_data(state, context) { |
||||
let bytes_needed = 0; |
||||
|
||||
for (const player_id in state.players) { |
||||
const player = state.players[player_id]; |
||||
if (player.points.length > 0) { |
||||
bytes_needed += (player.points.length * 12 + 6) * config.bytes_per_point; |
||||
} |
||||
} |
||||
|
||||
if (bytes_needed > context.dynamic_stroke_serializer.size) { |
||||
context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62)); |
||||
} else { |
||||
context.dynamic_stroke_serializer.offset = 0; |
||||
} |
||||
|
||||
for (const player_id in state.players) { |
||||
// player has the same data as their current stroke: points, color, width
|
||||
const player = state.players[player_id]; |
||||
if (player.points.length > 0) { |
||||
push_stroke(context.dynamic_stroke_serializer, player); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function geometry_add_point(state, context, player_id, point) { |
||||
if (!state.online) return; |
||||
state.players[player_id].points.push(point); |
||||
recompute_dynamic_data(state, context); |
||||
} |
||||
|
||||
function geometry_clear_player(state, context, player_id) { |
||||
if (!state.online) return; |
||||
state.players[player_id].points.length = 0; |
||||
recompute_dynamic_data(state, context); |
||||
} |
||||
|
||||
function add_image(context, image_id, bitmap, p) { |
||||
const x = p.x; |
||||
const y = p.y; |
||||
const gl = context.gl; |
||||
const id = Object.keys(context.textures).length; |
||||
|
||||
context.textures[id] = { |
||||
'texture': gl.createTexture(), |
||||
'image_id': image_id |
||||
}; |
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture); |
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, bitmap); |
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); |
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); |
||||
|
||||
context.quad_positions.push(...[ |
||||
x, y, |
||||
x, y + bitmap.height, |
||||
x + bitmap.width, y + bitmap.height, |
||||
|
||||
x + bitmap.width, y, |
||||
x, y, |
||||
x + bitmap.width, y + bitmap.height, |
||||
]); |
||||
|
||||
context.quad_texcoords.push(...[ |
||||
0, 0, |
||||
0, 1, |
||||
1, 1, |
||||
1, 0, |
||||
0, 0, |
||||
1, 1, |
||||
]); |
||||
|
||||
context.quad_positions_f32 = new Float32Array(context.quad_positions); |
||||
context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords); |
||||
} |
||||
|
||||
function move_image(context, image_event) { |
||||
const x = image_event.x; |
||||
const y = image_event.y; |
||||
|
||||
const count = Object.keys(context.textures).length; |
||||
|
||||
for (let id = 0; id < count; ++id) { |
||||
const image = context.textures[id]; |
||||
if (image.image_id === image_event.image_id) { |
||||
context.quad_positions[id * 12 + 0] = x; |
||||
context.quad_positions[id * 12 + 1] = y; |
||||
context.quad_positions[id * 12 + 2] = x; |
||||
context.quad_positions[id * 12 + 3] = y + image_event.height; |
||||
context.quad_positions[id * 12 + 4] = x + image_event.width; |
||||
context.quad_positions[id * 12 + 5] = y + image_event.height; |
||||
|
||||
context.quad_positions[id * 12 + 6] = x + image_event.width; |
||||
context.quad_positions[id * 12 + 7] = y; |
||||
context.quad_positions[id * 12 + 8] = x; |
||||
context.quad_positions[id * 12 + 9] = y; |
||||
context.quad_positions[id * 12 + 10] = x + image_event.width; |
||||
context.quad_positions[id * 12 + 11] = y + image_event.height; |
||||
|
||||
context.quad_positions_f32 = new Float32Array(context.quad_positions); |
||||
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
function image_at(state, x, y) { |
||||
for (let i = state.events.length - 1; i >= 0; --i) { |
||||
const event = state.events[i]; |
||||
if (event.type === EVENT.IMAGE && !event.deleted) { |
||||
if ('height' in event && 'width' in event) { |
||||
if (event.x <= x && x <= event.x + event.width && event.y <= y && y <= event.y + event.height) { |
||||
return event; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
@ -0,0 +1,410 @@
@@ -0,0 +1,410 @@
|
||||
function init_listeners(state, context) { |
||||
window.addEventListener('keydown', (e) => keydown(e, state, context)); |
||||
window.addEventListener('keyup', (e) => keyup(e, state, context)); |
||||
window.addEventListener('paste', (e) => paste(e, state, context)); |
||||
|
||||
context.canvas.addEventListener('mousedown', (e) => mousedown(e, state, context)); |
||||
context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context)); |
||||
context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context)); |
||||
context.canvas.addEventListener('mouseleave', (e) => mouseup(e, state, context)); |
||||
context.canvas.addEventListener('contextmenu', cancel); |
||||
context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); |
||||
|
||||
context.canvas.addEventListener('touchstart', (e) => touchstart(e, state, context)); |
||||
context.canvas.addEventListener('touchmove', (e) => touchmove(e, state, context)); |
||||
context.canvas.addEventListener('touchend', (e) => touchend(e, state, context)); |
||||
context.canvas.addEventListener('touchcancel', (e) => touchend(e, state, context)); |
||||
|
||||
context.canvas.addEventListener('drop', (e) => on_drop(e, state, context)); |
||||
context.canvas.addEventListener('dragover', (e) => mousemove(e, state, context)); |
||||
} |
||||
|
||||
function cancel(e) { |
||||
e.preventDefault(); |
||||
return false; |
||||
} |
||||
|
||||
function zenmode() { |
||||
document.querySelector('.pallete-wrapper').classList.toggle('hidden'); |
||||
document.querySelector('.sizer-wrapper').classList.toggle('hidden'); |
||||
} |
||||
|
||||
async function paste(e, state, context) { |
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items; |
||||
for (const item of items) { |
||||
if (item.kind === 'file') { |
||||
const file = item.getAsFile(); |
||||
await insert_image(state, context, file); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function keydown(e, state, context) { |
||||
if (e.code === 'Space' && !state.drawing) { |
||||
state.spacedown = true; |
||||
context.canvas.classList.add('movemode'); |
||||
} else if (e.code === 'Tab') { |
||||
e.preventDefault(); |
||||
zenmode(); |
||||
} |
||||
} |
||||
|
||||
function keyup(e, state, context) { |
||||
if (e.code === 'Space' && state.spacedown) { |
||||
state.spacedown = false; |
||||
state.moving = false; |
||||
context.canvas.classList.remove('movemode'); |
||||
} |
||||
} |
||||
|
||||
function mousedown(e, state, context) { |
||||
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
||||
const canvasp = screen_to_canvas(state, screenp); |
||||
|
||||
if (e.button === 2) { |
||||
// Right click on image to select it
|
||||
const image_event = image_at(state, canvasp.x, canvasp.y); |
||||
|
||||
if (image_event) { |
||||
context.active_image = image_event.image_id; |
||||
} else { |
||||
context.active_image = null; |
||||
} |
||||
|
||||
schedule_draw(state, context); |
||||
|
||||
return; |
||||
} |
||||
|
||||
if (e.button !== 0) { |
||||
return; |
||||
} |
||||
|
||||
if (context.active_image) { |
||||
// Move selected image with left click
|
||||
const image_event = image_at(state, canvasp.x, canvasp.y); |
||||
if (image_event && image_event.image_id === context.active_image) { |
||||
state.moving_image = image_event; |
||||
return; |
||||
} |
||||
} |
||||
|
||||
if (state.spacedown) { |
||||
state.moving = true; |
||||
context.canvas.classList.add('moving'); |
||||
return; |
||||
} |
||||
|
||||
geometry_clear_player(state, context, state.me); |
||||
geometry_add_point(state, context, state.me, canvasp); |
||||
|
||||
state.drawing = true; |
||||
context.active_image = null; |
||||
|
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
function mousemove(e, state, context) { |
||||
e.preventDefault(); |
||||
|
||||
let do_draw = false; |
||||
|
||||
if (state.moving) { |
||||
state.canvas.offset.x += e.movementX; |
||||
state.canvas.offset.y += e.movementY; |
||||
do_draw = true; |
||||
} |
||||
|
||||
if (state.moving_image) { |
||||
state.moving_image.x += e.movementX / state.canvas.zoom; |
||||
state.moving_image.y += e.movementY / state.canvas.zoom; |
||||
move_image(context, state.moving_image); |
||||
do_draw = true; |
||||
} |
||||
|
||||
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
||||
const canvasp = screen_to_canvas(state, screenp); |
||||
|
||||
state.cursor = screenp; |
||||
|
||||
if (state.drawing) { |
||||
geometry_add_point(state, context, state.me, canvasp); |
||||
fire_event(state, predraw_event(canvasp.x, canvasp.y)); |
||||
do_draw = true; |
||||
} |
||||
|
||||
if (do_draw) { |
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
function mouseup(e, state, context) { |
||||
if (e.button !== 0) { |
||||
return; |
||||
} |
||||
|
||||
if (state.moving_image) { |
||||
schedule_draw(state, context); |
||||
queue_event(state, image_move_event(context.active_image, state.moving_image.x, state.moving_image.y)); |
||||
state.moving_image = null; |
||||
return; |
||||
} |
||||
|
||||
if (state.moving) { |
||||
state.moving = false; |
||||
context.canvas.classList.remove('moving'); |
||||
return; |
||||
} |
||||
|
||||
if (state.drawing) { |
||||
const stroke = geometry_prepare_stroke(state); |
||||
|
||||
if (stroke) { |
||||
geometry_add_stroke(state, context, stroke); |
||||
queue_event(state, stroke_event(state)); |
||||
geometry_clear_player(state, context, state.me); |
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
state.drawing = false; |
||||
|
||||
return; |
||||
} |
||||
} |
||||
|
||||
function wheel(e, state, context) { |
||||
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
||||
const canvasp = screen_to_canvas(state, screenp); |
||||
|
||||
const dz = (e.deltaY < 0 ? 0.1 : -0.1); |
||||
const old_zoom = state.canvas.zoom; |
||||
|
||||
state.canvas.zoom *= (1.0 + dz); |
||||
|
||||
if (state.canvas.zoom > config.max_zoom) { |
||||
state.canvas.zoom = old_zoom; |
||||
return; |
||||
} |
||||
|
||||
if (state.canvas.zoom < config.min_zoom) { |
||||
state.canvas.zoom = old_zoom; |
||||
return; |
||||
} |
||||
|
||||
const zoom_offset_x = Math.round((dz * old_zoom) * canvasp.x); |
||||
const zoom_offset_y = Math.round((dz * old_zoom) * canvasp.y); |
||||
|
||||
state.canvas.offset.x -= zoom_offset_x; |
||||
state.canvas.offset.y -= zoom_offset_y; |
||||
|
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
function touchstart(e, state) { |
||||
e.preventDefault(); |
||||
|
||||
if (state.touch.drawing) { |
||||
// Ingore subsequent touches if we are already drawing
|
||||
return; |
||||
} |
||||
|
||||
// First finger(s) down?
|
||||
if (state.touch.ids.length === 0) { |
||||
if (e.changedTouches.length === 1) { |
||||
|
||||
// We give a bit of time to add a second finger
|
||||
state.touch.waiting_for_second_finger = true; |
||||
state.touch.moves = 0; |
||||
state.touch.buffered.length = 0; |
||||
state.touch.ids.push(e.changedTouches[0].identifier); |
||||
|
||||
setTimeout(() => { |
||||
state.touch.waiting_for_second_finger = false; |
||||
}, config.second_finger_timeout); |
||||
} else { |
||||
console.error('Two touchstarts at the same time are not yet supported'); |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
// There are touches already
|
||||
if (state.touch.waiting_for_second_finger) { |
||||
if (e.changedTouches.length === 1) { |
||||
state.touch.ids.push(e.changedTouches[0].identifier); |
||||
|
||||
for (const touch of e.touches) { |
||||
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
||||
|
||||
if (touch.identifier === state.touch.ids[0]) { |
||||
state.touch.first_finger_position = screenp; |
||||
} else if (touch.identifier === state.touch.ids[1]) { |
||||
state.touch.second_finger_position = screenp; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return; |
||||
} |
||||
} |
||||
|
||||
function touchmove(e, state, context) { |
||||
if (state.touch.ids.length === 1) { |
||||
const touch = find_touch(e.changedTouches, state.touch.ids[0]); |
||||
|
||||
if (!touch) { |
||||
return; |
||||
} |
||||
|
||||
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
||||
const canvasp = screen_to_canvas(state, screenp); |
||||
|
||||
if (state.touch.moving) { |
||||
// Can happen if we have been panning the canvas and lifted one finger,
|
||||
// but not the second one
|
||||
return; |
||||
} |
||||
|
||||
if (!state.touch.drawing) { |
||||
// Buffer this move
|
||||
state.touch.moves += 1; |
||||
|
||||
if (state.touch.moves > config.buffer_first_touchmoves) { |
||||
// Start drawing, no more waiting
|
||||
state.touch.waiting_for_second_finger = false; |
||||
state.touch.drawing = true; |
||||
} else { |
||||
state.touch.buffered.push(canvasp); |
||||
} |
||||
} else { |
||||
// Handle buffered moves
|
||||
if (state.touch.buffered.length > 0) { |
||||
geometry_clear_player(state, context, state.me); |
||||
|
||||
for (const p of state.touch.buffered) { |
||||
geometry_add_point(state, context, state.me, p); |
||||
fire_event(state, predraw_event(p.x, p.y)); |
||||
} |
||||
|
||||
state.touch.buffered.length = 0; |
||||
} |
||||
|
||||
geometry_add_point(state, context, state.me, canvasp); |
||||
fire_event(state, predraw_event(canvasp.x, canvasp.y)); |
||||
|
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
if (state.touch.ids.length === 2) { |
||||
state.touch.moving = true; |
||||
|
||||
let first_finger_position = null; |
||||
let second_finger_position = null; |
||||
|
||||
// A separate loop because touches might be in different order ? (question mark)
|
||||
// IMPORTANT: e.touches, not e.changedTouches!
|
||||
for (const touch of e.touches) { |
||||
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
||||
|
||||
if (touch.identifier === state.touch.ids[0]) { |
||||
first_finger_position = screenp; |
||||
} else if (touch.identifier === state.touch.ids[1]) { |
||||
second_finger_position = screenp; |
||||
} |
||||
} |
||||
|
||||
const old_finger_midpoint = mid_v2(state.touch.first_finger_position, state.touch.second_finger_position); |
||||
const new_finger_midpoint = mid_v2(first_finger_position, second_finger_position); |
||||
|
||||
const new_finger_midpoint_canvas = mid_v2( |
||||
screen_to_canvas(state, first_finger_position), |
||||
screen_to_canvas(state, second_finger_position) |
||||
); |
||||
|
||||
const old_finger_distance = dist_v2(state.touch.first_finger_position, state.touch.second_finger_position); |
||||
const new_finger_distance = dist_v2(first_finger_position, second_finger_position); |
||||
|
||||
const dx = new_finger_midpoint.x - old_finger_midpoint.x; |
||||
const dy = new_finger_midpoint.y - old_finger_midpoint.y; |
||||
|
||||
const old_zoom = state.canvas.zoom; |
||||
|
||||
state.canvas.offset.x += dx; |
||||
state.canvas.offset.y += dy; |
||||
|
||||
// console.log(new_finger_distance, state.touch.finger_distance);
|
||||
|
||||
const scale_by = new_finger_distance / old_finger_distance; |
||||
const dz = state.canvas.zoom * (scale_by - 1.0); |
||||
|
||||
const zoom_offset_x = dz * new_finger_midpoint_canvas.x; |
||||
const zoom_offset_y = dz * new_finger_midpoint_canvas.y; |
||||
|
||||
if (config.min_zoom <= state.canvas.zoom * scale_by && state.canvas.zoom * scale_by <= config.max_zoom) { |
||||
state.canvas.zoom *= scale_by; |
||||
state.canvas.offset.x -= zoom_offset_x; |
||||
state.canvas.offset.y -= zoom_offset_y; |
||||
} |
||||
|
||||
state.touch.first_finger_position = first_finger_position; |
||||
state.touch.second_finger_position = second_finger_position; |
||||
|
||||
schedule_draw(state, context); |
||||
|
||||
return; |
||||
} |
||||
} |
||||
|
||||
function touchend(e, state, context) { |
||||
for (const touch of e.changedTouches) { |
||||
if (state.touch.drawing) { |
||||
if (state.touch.ids[0] == touch.identifier) { |
||||
// const event = stroke_event();
|
||||
// await queue_event(event);
|
||||
|
||||
const stroke = geometry_prepare_stroke(state); |
||||
|
||||
if (stroke) { |
||||
geometry_add_stroke(state, context, stroke); |
||||
queue_event(state, stroke_event(state)); |
||||
geometry_clear_player(state, context, state.me); |
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
state.touch.drawing = false; |
||||
} |
||||
} |
||||
|
||||
const index = state.touch.ids.indexOf(touch.identifier); |
||||
|
||||
if (index !== -1) { |
||||
state.touch.ids.splice(index, 1); |
||||
} |
||||
} |
||||
|
||||
if (state.touch.ids.length === 0) { |
||||
// Only allow drawing again when ALL fingers have been lifted
|
||||
state.touch.moving = false; |
||||
waiting_for_second_finger = false; |
||||
} |
||||
} |
||||
|
||||
async function on_drop(e, state, context) { |
||||
e.preventDefault(); |
||||
|
||||
if (e.dataTransfer.files.length !== 1) { |
||||
return; |
||||
} |
||||
|
||||
const file = e.dataTransfer.files[0]; |
||||
await insert_image(state, context, file); |
||||
|
||||
schedule_draw(state, context); |
||||
|
||||
return false; |
||||
} |
@ -0,0 +1,193 @@
@@ -0,0 +1,193 @@
|
||||
const stroke_vs_src = ` |
||||
attribute float a_type; |
||||
attribute vec2 a_pos; |
||||
attribute vec2 a_texcoord; |
||||
attribute vec3 a_color; |
||||
|
||||
uniform vec2 u_scale; |
||||
uniform vec2 u_res; |
||||
uniform vec2 u_translation; |
||||
|
||||
varying vec3 v_color; |
||||
varying vec2 v_texcoord; |
||||
varying float v_type; |
||||
|
||||
void main() { |
||||
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
||||
vec2 screen02 = screen01 * 2.0; |
||||
|
||||
screen02.y = 2.0 - screen02.y; |
||||
|
||||
v_color = a_color; |
||||
v_texcoord = a_texcoord; |
||||
v_type = a_type; |
||||
|
||||
gl_Position = vec4(screen02 - 1.0, 0, 1); |
||||
} |
||||
`;
|
||||
|
||||
const stroke_fs_src = ` |
||||
#extension GL_OES_standard_derivatives : enable |
||||
|
||||
precision mediump float; |
||||
|
||||
varying vec3 v_color; |
||||
varying vec2 v_texcoord; |
||||
varying float v_type; |
||||
|
||||
void main() { |
||||
vec2 uv = v_texcoord * 2.0 - 1.0; |
||||
float sdf = 1.0 - mix(abs(uv.y), length(uv), v_type); |
||||
float pd = fwidth(sdf); |
||||
float alpha = 1.0 - smoothstep(pd, 0.0, sdf); |
||||
|
||||
gl_FragColor = vec4(v_color * alpha, alpha); |
||||
} |
||||
`;
|
||||
|
||||
const tquad_vs_src = ` |
||||
attribute vec2 a_pos; |
||||
attribute vec2 a_texcoord; |
||||
|
||||
uniform vec2 u_scale; |
||||
uniform vec2 u_res; |
||||
uniform vec2 u_translation; |
||||
|
||||
varying vec2 v_texcoord; |
||||
|
||||
void main() { |
||||
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
||||
vec2 screen02 = screen01 * 2.0; |
||||
screen02.y = 2.0 - screen02.y; |
||||
vec2 screen11 = screen02 - 1.0; |
||||
v_texcoord = a_texcoord; |
||||
gl_Position = vec4(screen11, 0, 1); |
||||
} |
||||
`;
|
||||
|
||||
const tquad_fs_src = ` |
||||
precision mediump float; |
||||
|
||||
varying vec2 v_texcoord; |
||||
|
||||
uniform sampler2D u_texture; |
||||
uniform bool u_outline; |
||||
|
||||
void main() { |
||||
if (!u_outline) { |
||||
gl_FragColor = texture2D(u_texture, v_texcoord); |
||||
} else { |
||||
gl_FragColor = mix(texture2D(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); |
||||
} |
||||
} |
||||
`;
|
||||
|
||||
function init_webgl(state, context) { |
||||
context.canvas = document.querySelector('#c'); |
||||
context.gl = context.canvas.getContext('webgl', { |
||||
'preserveDrawingBuffer': true, |
||||
'desynchronized': true, |
||||
'antialias': false, |
||||
}); |
||||
|
||||
const gl = context.gl; |
||||
|
||||
gl.enable(gl.BLEND); |
||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); |
||||
gl.getExtension('OES_standard_derivatives'); |
||||
|
||||
const stroke_vs = create_shader(gl, gl.VERTEX_SHADER, stroke_vs_src); |
||||
const stroke_fs = create_shader(gl, gl.FRAGMENT_SHADER, stroke_fs_src); |
||||
|
||||
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); |
||||
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); |
||||
|
||||
context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs); |
||||
context.programs['quad'] = create_program(gl, quad_vs, quad_fs); |
||||
|
||||
context.locations['stroke'] = { |
||||
'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'), |
||||
'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'), |
||||
'a_texcoord': gl.getAttribLocation(context.programs['stroke'], 'a_texcoord'), |
||||
'a_color': gl.getAttribLocation(context.programs['stroke'], 'a_color'), |
||||
|
||||
'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'), |
||||
'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'), |
||||
'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'), |
||||
}; |
||||
|
||||
context.locations['quad'] = { |
||||
'a_pos': gl.getAttribLocation(context.programs['quad'], 'a_pos'), |
||||
'a_texcoord': gl.getAttribLocation(context.programs['quad'], 'a_texcoord'), |
||||
'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'), |
||||
'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'), |
||||
'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), |
||||
'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), |
||||
'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'), |
||||
}; |
||||
|
||||
context.buffers['stroke'] = { |
||||
'b_packed': context.gl.createBuffer(), |
||||
}; |
||||
|
||||
context.buffers['quad'] = { |
||||
'b_pos': context.gl.createBuffer(), |
||||
'b_texcoord': context.gl.createBuffer(), |
||||
}; |
||||
|
||||
const resize_canvas = (entries) => { |
||||
// https://www.khronos.org/webgl/wiki/HandlingHighDPI
|
||||
const entry = entries[0]; |
||||
|
||||
let width; |
||||
let height; |
||||
|
||||
if (entry.devicePixelContentBoxSize) { |
||||
width = entry.devicePixelContentBoxSize[0].inlineSize; |
||||
height = entry.devicePixelContentBoxSize[0].blockSize; |
||||
} else if (entry.contentBoxSize) { |
||||
// fallback for Safari that will not always be correct
|
||||
width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio); |
||||
height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio); |
||||
} |
||||
|
||||
context.canvas.width = width; |
||||
context.canvas.height = height; |
||||
|
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
const resize_observer = new ResizeObserver(resize_canvas); |
||||
resize_observer.observe(context.canvas); |
||||
} |
||||
|
||||
function create_shader(gl, type, source) { |
||||
const shader = gl.createShader(type); |
||||
|
||||
gl.shaderSource(shader, source); |
||||
gl.compileShader(shader); |
||||
|
||||
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { |
||||
return shader; |
||||
} |
||||
|
||||
console.error(type, ':', gl.getShaderInfoLog(shader)); |
||||
|
||||
gl.deleteShader(shader); |
||||
} |
||||
|
||||
function create_program(gl, vs, fs) { |
||||
const program = gl.createProgram(); |
||||
|
||||
gl.attachShader(program, vs); |
||||
gl.attachShader(program, fs); |
||||
gl.linkProgram(program); |
||||
|
||||
if (gl.getProgramParameter(program, gl.LINK_STATUS)) { |
||||
return program; |
||||
} |
||||
|
||||
console.error('link:', gl.getProgramInfoLog(program)); |
||||
|
||||
gl.deleteProgram(program); |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.4.13) 16 APR 2023 21:20 |
||||
entering extended mode |
||||
restricted \write18 enabled. |
||||
%&-line parsing enabled. |
||||
** |
||||
|
||||
! Emergency stop. |
||||
<*> |
||||
|
||||
End of file on the terminal! |
||||
|
||||
|
||||
Here is how much of TeX's memory you used: |
||||
3 strings out of 476091 |
||||
111 string characters out of 5794081 |
||||
1849330 words of memory out of 5000000 |
||||
20488 multiletter control sequences out of 15000+600000 |
||||
512287 words of font info for 32 fonts, out of 8000000 for 9000 |
||||
1141 hyphenation exceptions out of 8191 |
||||
0i,0n,0p,1b,6s stack positions out of 10000i,1000n,20000p,200000b,200000s |
||||
! ==> Fatal error occurred, no output PDF file produced! |