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('pointerdown', (e) => mousedown(e, state, context));
context.canvas.addEventListener('pointermove', (e) => mousemove(e, state, context));
context.canvas.addEventListener('pointerup', (e) => mouseup(e, state, context));
context.canvas.addEventListener('pointerleave', (e) => mouseup(e, state, context));
context.canvas.addEventListener('pointerleave', (e) => mouseleave(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));
debug_panel_init(state, context);
}
function debug_panel_init(state, context) {
document.getElementById('debug-red').checked = state.debug.red;
document.getElementById('debug-do-prepass').checked = state.debug.do_prepass;
document.getElementById('debug-limit-from').checked = state.debug.limit_from;
document.getElementById('debug-limit-to').checked = state.debug.limit_to;
document.getElementById('debug-red').addEventListener('change', (e) => {
state.debug.red = e.target.checked;
schedule_draw(state, context);
});
document.getElementById('debug-do-prepass').addEventListener('change', (e) => {
state.debug.do_prepass = e.target.checked;
schedule_draw(state, context);
});
document.getElementById('debug-limit-from').addEventListener('change', (e) => {
state.debug.limit_from = e.target.checked;
schedule_draw(state, context);
});
document.getElementById('debug-limit-to').addEventListener('change', (e) => {
state.debug.limit_to = e.target.checked;
schedule_draw(state, context);
});
document.getElementById('debug-render-from').addEventListener('input', (e) => {
state.debug.render_from = parseInt(e.target.value);
schedule_draw(state, context);
});
document.getElementById('debug-render-to').addEventListener('input', (e) => {
state.debug.render_to = parseInt(e.target.value);
schedule_draw(state, context);
});
document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => {
state.canvas.zoom_level = config.benchmark.zoom_level;
state.canvas.offset.x = config.benchmark.offset.x;
state.canvas.offset.y = config.benchmark.offset.y;
const dz = (state.canvas.zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.zoom = Math.pow(1.0 + dz, Math.abs(state.canvas.zoom_level))
state.debug.benchmark_mode = true;
const origin_x = state.canvas.offset.x;
const origin_y = state.canvas.offset.y;
const original_button_text = e.target.innerText;
let frame = 0;
state.debug.on_benchmark = () => {
if (frame >= config.benchmark.frames) {
state.debug.benchmark_mode = false;
e.target.disabled = false;
e.target.innerText = original_button_text;
return false;
}
state.canvas.offset.x = origin_x + Math.round(100 * Math.cos(frame / 360));
state.canvas.offset.y = origin_y + Math.round(100 * Math.sin(frame / 360));
frame += 1;
return true;
}
e.target.disabled = true;
e.target.innerText = 'Benchmark in progress...';
schedule_draw(state, context);
});
}
function cancel(e) {
e.preventDefault();
return false;
}
function zenmode() {
document.querySelector('.pallete-wrapper').classList.toggle('hidden');
document.querySelector('.top-wrapper').classList.toggle('hidden');
}
function enter_picker_mode(state, context) {
if (state.tools.active === 'pencil') { // or other drawing tools
document.querySelector('canvas').classList.add('picker');
document.querySelector('.picker-preview-outer').classList.remove('dhide');
document.querySelector('.brush-dom').classList.add('dhide');
state.colorpicking = true;
const canvasp = screen_to_canvas(state, state.cursor);
update_color_picker_color(state, context, canvasp);
}
}
function exit_picker_mode(state) {
if (state.colorpicking) {
document.querySelector('canvas').classList.remove('picker');
document.querySelector('.picker-preview-outer').classList.add('dhide');
document.querySelector('.brush-dom').classList.remove('dhide');
state.colorpicking = false;
}
}
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();
} else if (e.code === 'ControlLeft' || e.paddingcode === 'ControlRight') {
enter_picker_mode(state, context);
} else if (e.code === 'KeyD') {
document.querySelector('.debug-window').classList.toggle('dhide');
} else if (e.code === 'KeyZ') {
if (e.ctrlKey) {
queue_event(state, undo_event(state));
} else {
state.zoomdown = true;
}
}
}
function keyup(e, state, context) {
if (e.code === 'Space' && state.spacedown) {
state.spacedown = false;
state.moving = false;
context.canvas.classList.remove('movemode');
} else if (e.code === 'ControlLeft' || e.code === 'ControlRight') {
exit_picker_mode(state);exit_picker_mode
} else if (e.code === 'KeyZ') {
state.zoomdown = false;
}
}
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 !== 0 && e.button !== 1) {
return;
}
if (state.zoomdown) {
state.zooming = true;
state.canvas.zoom_screenp = screenp;
return;
}
if (state.colorpicking) {
const color_u32 = color_to_u32(state.color_picked.substring(1));
state.players[state.me].color = color_u32;
update_cursor(state);
fire_event(state, color_event(color_u32));
return;
}
if (state.spacedown || e.button === 1) {
state.moving = true;
context.canvas.classList.add('moving');
if (e.button === 1) {
context.canvas.classList.add('mousemoving');
}
return;
}
if (state.tools.active === 'pencil') {
canvasp.pressure = Math.ceil(e.pressure * 255);
geometry_clear_player(state, context, state.me);
geometry_add_point(state, context, state.me, canvasp);
state.drawing = true;
state.active_image = null;
schedule_draw(state, context);
} else if (state.tools.active === 'ruler') {
} else if (state.tools.active === 'eraser') {
state.erasing = true;
} else if (state.tools.active === 'pointer') {
state.imagescaling = false;
state.imagemoving = false;
if (state.active_image !== null) {
// Check for resize first, because it supports
// clicking slightly outside of the image
const image = get_image(context, state.active_image);
const corner = image_corner(state, image, canvasp);
if (corner !== null) {
// Resize
state.imagescaling = true;
state.scaling_corner = corner;
document.querySelector('canvas').classList.remove('resize-topleft');
document.querySelector('canvas').classList.remove('resize-topright');
if (corner === 0 || corner === 2) {
document.querySelector('canvas').classList.add('resize-topleft');
} else if (corner === 1 || corner === 3) {
document.querySelector('canvas').classList.add('resize-topright');
}
}
}
// Only do picking logic if we haven't started imagescaling already
if (!state.imagescaling) {
const image = image_at(context, canvasp.x, canvasp.y);
if (image !== null) {
state.active_image = image.key;
// Allow immediately moving
state.imagemoving = true;
} else {
state.active_image = null;
}
}
schedule_draw(state, context);
}
}
function update_color_picker_color(state, context, canvasp) {
const stroke_index = bvh_point(state, canvasp);
let color_under_cursor = color_from_rgbdict(context.bgcolor);
if (stroke_index != null) {
color_under_cursor = color_from_u32(state.events[stroke_index].color);
}
document.querySelector('.picker-preview-inner').style.background = color_under_cursor;
state.color_picked = color_under_cursor;
}
function mousemove(e, state, context) {
e.preventDefault();
let do_draw = false;
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY};
const canvasp = screen_to_canvas(state, screenp);
if (state.tools.active === 'pointer') {
if (state.active_image !== null) {
const image = get_image(context, state.active_image);
const corner = image_corner(state, image, canvasp);
if (state.scaling_corner === null) {
document.querySelector('canvas').classList.remove('resize-topleft');
document.querySelector('canvas').classList.remove('resize-topright');
if (corner === 0 || corner === 2) {
document.querySelector('canvas').classList.add('resize-topleft');
} else if (corner === 1 || corner === 3) {
document.querySelector('canvas').classList.add('resize-topright');
}
}
}
}
if (state.me in state.players) {
const me = state.players[state.me];
const width = Math.max(me.width * state.canvas.zoom, 2.0);
const radius = Math.round(width / 2);
const brush_x = screenp.x - radius - 2;
const brush_y = screenp.y - radius - 2;
document.querySelector('.brush-dom').style.transform = `translate(${brush_x}px, ${brush_y}px)`;
}
if (state.me in state.players && dist_v2(state.players[state.me].cursor, canvasp) > 5) {
state.players[state.me].cursor = canvasp;
fire_event(state, movecursor_event(canvasp.x, canvasp.y));
}
if (state.colorpicking) {
update_color_picker_color(state, context, canvasp);
}
if (state.zooming) {
const zooming_in = e.movementY > 0;
const zooming_out = e.movementY < 0;
let zoom_level = null;
if (zooming_in) {
zoom_level = state.canvas.zoom_level + 1
} else if (zooming_out) {
zoom_level = state.canvas.zoom_level - 1;
} else {
return;
}
if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) {
return;
}
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.zoom_level = zoom_level;
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
do_draw = true;
}
if (state.moving) {
state.canvas.offset.x += e.movementX;
state.canvas.offset.y += e.movementY;
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
fire_event(state, movecanvas_event(state));
draw_html(state, context);
do_draw = true;
}
if (state.imagescaling) {
const image = get_image(context, state.active_image);
scale_image(context, image, state.scaling_corner, canvasp);
do_draw = true;
}
if (state.imagemoving) {
const image = get_image(context, state.active_image);
if (image !== null) {
const dx = e.movementX / state.canvas.zoom;
const dy = e.movementY / state.canvas.zoom;
image.at.x += dx;
image.at.y += dy;
do_draw = true;
}
}
if (state.drawing) {
canvasp.pressure = Math.ceil(e.pressure * 255);
geometry_add_point(state, context, state.me, canvasp, e.pointerType === "pen");
fire_event(state, predraw_event(canvasp.x, canvasp.y));
do_draw = true;
}
if (state.erasing) {
const me = state.players[state.me];
const radius = Math.round(me.width / 2);
const cursor_bbox = {
'x1': canvasp.x - radius,
'y1': canvasp.y - radius,
'x2': canvasp.x + radius,
'y2': canvasp.y + radius,
};
tv_ensure(state.erase_candidates, round_to_pow2(state.stroke_count, 4096));
tv_clear(state.erase_candidates);
bvh_intersect_quad(state, state.bvh, cursor_bbox, state.erase_candidates);
for (let i = 0; i < state.erase_candidates.size; ++i) {
const stroke_id = state.erase_candidates.data[i];
const stroke = state.events[stroke_id];
if (!stroke.deleted && stroke_intersects_cursor(state, stroke, canvasp, radius)) {
stroke.deleted = true;
bvh_delete_stroke(state, stroke);
do_draw = true;
}
}
}
if (do_draw) {
schedule_draw(state, context);
}
state.cursor = screenp;
return false;
}
function mouseup(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 !== 0 && e.button !== 1) {
return;
}
if (state.zooming) {
state.zooming = false;
return;
}
if (state.imagemoving) {
state.imagemoving = false;
const image = get_image(context, state.active_image);
queue_event(state, image_move_event(state.active_image, image.at.x, image.at.y));
schedule_draw(state, context);
return;
}
if (state.imagescaling) {
queue_event(state, image_scale_event(state.active_image, state.scaling_corner, canvasp.x, canvasp.y));
state.imagescaling = false;
state.scaling_corner = null;
return;
}
if (state.moving || e.button === 1) {
state.moving = false;
context.canvas.classList.remove('moving');
if (e.button === 1) {
context.canvas.classList.remove('mousemoving');
}
return;
}
if (state.drawing) {
const stroke = geometry_prepare_stroke(state);
if (stroke) {
// TODO: be able to add a baked stroke locally
//geometry_add_stroke(state, context, stroke, 0);
queue_event(state, stroke_event(state));
//geometry_clear_player(state, context, state.me);
schedule_draw(state, context);
}
state.drawing = false;
return;
}
if (state.erasing) {
state.erasing = false;
return;
}
}
function mouseleave(e, state, context) {
if (state.moving) {
state.moving = false;
context.canvas.classList.remove('movemode');
}
//exit_picker_mode(state);
// something else?
}
function update_cursor(state) {
if (!(state.me in state.players)) {
// we not ready yet
return;
}
const me = state.players[state.me];
const width = Math.max(me.width * state.canvas.zoom, 2.0);
const radius = Math.round(width / 2);
let svg;
if (state.tools.active === 'pencil') {
const current_color = color_from_u32(me.color);
const stroke = (me.color === 0xFFFFFF ? 'black' : 'white');
svg = ``.replaceAll('\n', ' ');
} else if (state.tools.active === 'eraser') {
const current_color = '#ffffff';
const stroke = '#000000';
svg = ``.replaceAll('\n', ' ');
}
document.querySelector('.brush-dom').innerHTML = svg;
const brush_x = state.cursor.x - width / 2 - 2;
const brush_y = state.cursor.y - width / 2 - 2;
document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`;
}
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 zooming_in = e.deltaY < 0;
const zoom_level = zooming_in ? state.canvas.zoom_level + 2 : state.canvas.zoom_level - 2;
if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) {
return;
}
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.zoom_level = zoom_level;
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
state.canvas.zoom_screenp = screenp;
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
fire_event(state, zoomcanvas_event(state, canvasp.x, canvasp.y));
schedule_draw(state, context);
}
function start_move(e, state, context) {
// two touch identifiers are expected to be pushed into state.touch.ids at this point
geometry_clear_player(state, context, state.me); // Hide predraws of this stroke that is not means to be
fire_event(state, clear_event(state)); // Tell others to hide predraws of this stroke
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;
}
}
}
function touchstart(e, state, context) {
e.preventDefault();
// 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);
state.touch.drawing = true;
setTimeout(() => {
state.touch.waiting_for_second_finger = false;
}, config.second_finger_timeout);
} else {
state.touch.ids.push(e.changedTouches[0].identifier);
state.touch.ids.push(e.changedTouches[1].identifier);
start_move(e, state, context);
}
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);
start_move(e, state, context);
}
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;
}
state.touch.moves += 1;
if (state.touch.moves > config.buffer_first_touchmoves) {
// At this point touch with second finger will NOT start a pan
state.touch.waiting_for_second_finger = false;
}
canvasp.pressure = 128; // TODO: check out touch devices' e.pressure
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;
}
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
state.touch.first_finger_position = first_finger_position;
state.touch.second_finger_position = second_finger_position;
fire_event(state, movecanvas_event(state));
draw_html(state, context);
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 stroke = geometry_prepare_stroke(state);
if (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);
return false;
}