|
|
|
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));
|
|
|
|
|
|
|
|
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-force-clip-off').checked = state.debug.force_clip_off;
|
|
|
|
document.getElementById('debug-draw-bvh').checked = state.debug.draw_bvh;
|
|
|
|
|
|
|
|
document.getElementById('debug-draw-bvh').addEventListener('change', (e) => {
|
|
|
|
state.debug.draw_bvh = e.target.checked;
|
|
|
|
schedule_draw(state, context);
|
|
|
|
});
|
|
|
|
|
|
|
|
document.getElementById('debug-force-clip-off').addEventListener('change', (e) => {
|
|
|
|
state.debug.force_clip_off = e.target.checked;
|
|
|
|
schedule_draw(state, context);
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
} else if (e.code === 'KeyZ') {
|
|
|
|
const topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
|
|
|
|
const bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height});
|
|
|
|
|
|
|
|
for (let i = 0; i < state.events.length; ++i) {
|
|
|
|
const event = state.events[i];
|
|
|
|
|
|
|
|
if (event.type === EVENT.STROKE) {
|
|
|
|
let on_screen = false;
|
|
|
|
|
|
|
|
for (const p of event.points) {
|
|
|
|
if (topleft.x <= p.x && p.x <= bottomright.x && topleft.y <= p.y && p.y <= bottomright.y) {
|
|
|
|
on_screen = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (on_screen) {
|
|
|
|
console.log(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (e.code === 'KeyD') {
|
|
|
|
document.querySelector('.debug-window').classList.toggle('dhide');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state.tools.active === 'pencil') {
|
|
|
|
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);
|
|
|
|
} else if (state.tools.active === 'ruler') {
|
|
|
|
|
|
|
|
} else if (state.tools.active === 'eraser') {
|
|
|
|
state.erasing = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
if (state.drawing) {
|
|
|
|
geometry_add_point(state, context, state.me, canvasp);
|
|
|
|
fire_event(state, predraw_event(canvasp.x, canvasp.y));
|
|
|
|
do_draw = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state.erasing) {
|
|
|
|
const p1 = screen_to_canvas(state, state.cursor);
|
|
|
|
const p2 = { 'x': canvasp.x, 'y': canvasp.y };
|
|
|
|
const erased = strokes_intersect_line(state, p1, p2);
|
|
|
|
|
|
|
|
for (const index of erased) {
|
|
|
|
if (!state.events[index].deleted) {
|
|
|
|
state.events[index].deleted = true;
|
|
|
|
do_draw = true;
|
|
|
|
geometry_delete_stroke(state, context, index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (do_draw) {
|
|
|
|
schedule_draw(state, context);
|
|
|
|
}
|
|
|
|
|
|
|
|
state.cursor = screenp;
|
|
|
|
|
|
|
|
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, 0); // TODO: stroke index?
|
|
|
|
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 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 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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, 0); // TODO: stroke index
|
|
|
|
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;
|
|
|
|
}
|