You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
786 lines
27 KiB
786 lines
27 KiB
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('do-snap').checked = state.snap !== null; |
|
|
|
document.getElementById('debug-red').addEventListener('change', (e) => { |
|
state.debug.red = e.target.checked; |
|
schedule_draw(state, context); |
|
}); |
|
|
|
document.getElementById('do-snap').addEventListener('change', (e) => { |
|
state.snap = e.target.checked ? 'grid' : null; |
|
}); |
|
|
|
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); |
|
const raw_canvasp = {...canvasp}; |
|
|
|
if (state.snap === 'grid') { |
|
const step = grid_snap_step(state); |
|
canvasp.x = Math.round(canvasp.x / step) * step; |
|
canvasp.y = Math.round(canvasp.y / step) * step; |
|
} |
|
|
|
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') { |
|
state.linedrawing = true; |
|
state.ruler_origin = canvasp; |
|
} 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, raw_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, raw_canvasp.x, raw_canvasp.y); |
|
if (image !== null) { |
|
state.active_image = image.key; |
|
// Allow immediately moving |
|
state.imagemoving = true; |
|
image.raw_at.x = image.at.x; |
|
image.raw_at.y = image.at.y; |
|
} 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); |
|
const raw_canvasp = {...canvasp}; |
|
|
|
if (state.snap === 'grid') { |
|
const step = grid_snap_step(state); |
|
canvasp.x = Math.round(canvasp.x / step) * step; |
|
canvasp.y = Math.round(canvasp.y / step) * step; |
|
} |
|
|
|
if (state.tools.active === 'pointer') { |
|
if (state.active_image !== null) { |
|
const image = get_image(context, state.active_image); |
|
const corner = image_corner(state, image, raw_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_screen = canvas_to_screen(state, canvasp); |
|
const brush_x = brush_screen.x - radius - 2; |
|
const brush_y = brush_screen.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.raw_at.x += dx; |
|
image.raw_at.y += dy; |
|
|
|
if (state.snap === 'grid') { |
|
const step = grid_snap_step(state); |
|
image.at.x = Math.round(image.raw_at.x / step) * step; |
|
image.at.y = Math.round(image.raw_at.y / step) * step; |
|
} else if (state.snap === null) { |
|
image.at.x = image.raw_at.x; |
|
image.at.y = image.raw_at.y; |
|
} |
|
|
|
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 last_canvasp = screen_to_canvas(state, state.cursor); |
|
|
|
const cursor_bbox = { |
|
'x1': Math.min(canvasp.x, last_canvasp.x) - radius, |
|
'y1': Math.min(canvasp.y, last_canvasp.y) - radius, |
|
'x2': Math.max(canvasp.x, last_canvasp.x) + radius, |
|
'y2': Math.max(canvasp.y, last_canvasp.y) + radius, |
|
}; |
|
|
|
tv_ensure(state.erase_candidates, round_to_pow2(state.stroke_count, 4096)); |
|
tv_clear(state.erase_candidates); |
|
|
|
// Rough pass, not all of these might actually need to be erased |
|
bvh_intersect_quad(state, state.bvh, cursor_bbox, state.erase_candidates); |
|
|
|
// Fine pass, actually run expensive capsule vs capsule intersection tests |
|
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_capsule(state, stroke, last_canvasp, canvasp, radius)) { |
|
stroke.deleted = true; |
|
bvh_delete_stroke(state, stroke); |
|
queue_event(state, eraser_event(stroke_id)); |
|
do_draw = true; |
|
} |
|
} |
|
} |
|
|
|
if (state.linedrawing) { |
|
// TODO: we should do something different when we allow multiple dynamic strokes per player |
|
geometry_clear_player(state, context, state.me); |
|
|
|
const p1 = {'x': state.ruler_origin.x, 'y': state.ruler_origin.y, 'pressure': 128}; |
|
const p2 = {'x': canvasp.x, 'y': canvasp.y, 'pressure': 128}; |
|
|
|
geometry_add_point(state, context, state.me, p1, false, true); |
|
geometry_add_point(state, context, state.me, p2, false, true); |
|
|
|
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); |
|
const raw_canvasp = {...canvasp}; |
|
|
|
if (state.snap === 'grid') { |
|
const step = grid_snap_step(state); |
|
canvasp.x = Math.round(canvasp.x / step) * step; |
|
canvasp.y = Math.round(canvasp.y / step) * step; |
|
} |
|
|
|
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); |
|
image.raw_at.x = image.at.x; |
|
image.raw_at.y = image.at.y; |
|
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; |
|
} |
|
|
|
if (state.linedrawing) { |
|
state.linedrawing = false; |
|
queue_event(state, stroke_event(state)); |
|
schedule_draw(state, context); |
|
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' || state.tools.active === 'ruler') { |
|
const current_color = color_from_u32(me.color); |
|
const stroke = (me.color === 0xFFFFFF ? 'black' : 'white'); |
|
|
|
svg = `<svg style="display: block" xmlns="http://www.w3.org/2000/svg" width="${width + 4}" height="${width + 4}"> |
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="${stroke}" fill="none" stroke-width="3"/> |
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="none" fill="${current_color}" stroke-width="1"/> |
|
</svg>`.replaceAll('\n', ' '); |
|
} else if (state.tools.active === 'eraser') { |
|
const current_color = '#ffffff'; |
|
const stroke = '#000000'; |
|
svg = `<svg style="display: block" xmlns="http://www.w3.org/2000/svg" width="${width + 4}" height="${width + 4}"> |
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="${stroke}" fill="none" stroke-width="3"/> |
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="none" fill="${current_color}" stroke-width="1"/> |
|
</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; |
|
}
|
|
|