|
<svelte:options accessors={true} /> |
|
|
|
<script lang="ts"> |
|
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte"; |
|
|
|
const dispatch = createEventDispatcher(); |
|
|
|
export let width = 0; |
|
export let height = 0; |
|
export let natural_width = 0; |
|
export let natural_height = 0; |
|
|
|
let boxes: Array<Array<number>> = []; |
|
let points: Array<Array<number>> = []; |
|
|
|
let canvas_container: HTMLElement; |
|
let canvas: HTMLCanvasElement; |
|
let ctx: CanvasRenderingContext2D | null; |
|
|
|
let mouse_pressing: boolean = false; |
|
let mouse_button: number; |
|
let prev_x: number, prev_y: number; |
|
let cur_x: number, cur_y: number; |
|
|
|
let old_width = 0; |
|
let old_height = 0; |
|
let canvasObserver: ResizeObserver; |
|
|
|
async function set_canvas_size(dimensions: { |
|
width: number; |
|
height: number; |
|
}) { |
|
await tick(); |
|
canvas.width = dimensions.width; |
|
canvas.height = dimensions.height; |
|
canvas.style.width = `${dimensions.width}px`; |
|
canvas.style.height = `${dimensions.height}px`; |
|
canvas.style.marginTop = `-${dimensions.height}px`; |
|
} |
|
|
|
export async function resize_canvas() { |
|
if (width === old_width && height === old_height) return; |
|
await set_canvas_size({ width: width, height: height }); |
|
draw_canvas(); |
|
setTimeout(() => { |
|
old_height = height; |
|
old_width = width; |
|
}, 100); |
|
clear(); |
|
} |
|
|
|
export function clear() { |
|
boxes = []; |
|
points = []; |
|
draw_canvas(); |
|
dispatch("change", points); |
|
return true; |
|
} |
|
|
|
export function undo() { |
|
boxes.pop(); |
|
points.pop(); |
|
draw_canvas(); |
|
dispatch("change", points); |
|
return true; |
|
} |
|
|
|
onMount(async () => { |
|
ctx = canvas.getContext("2d"); |
|
if (ctx) { |
|
(ctx.lineJoin = "round"), (ctx.lineCap = "round"); |
|
ctx.strokeStyle = "#000"; |
|
} |
|
canvasObserver = new ResizeObserver(() => { |
|
resize_canvas(); |
|
}); |
|
canvasObserver.observe(canvas_container); |
|
draw_loop(); |
|
clear(); |
|
}); |
|
|
|
onDestroy(() => { |
|
canvasObserver.unobserve(canvas_container); |
|
}); |
|
|
|
function get_mouse_pos(e: MouseEvent | TouchEvent | FocusEvent) { |
|
const rect = canvas.getBoundingClientRect(); |
|
let screenX, screenY: number; |
|
if (e instanceof MouseEvent) { |
|
screenX = e.clientX; |
|
screenY = e.clientY; |
|
} else if (e instanceof TouchEvent) { |
|
screenX = e.changedTouches[0].clientX; |
|
screenY = e.changedTouches[0].clientY; |
|
} else { |
|
return { x: prev_x, y: prev_y }; |
|
} |
|
return { x: screenX - rect.left, y: screenY - rect.top }; |
|
} |
|
|
|
function handle_draw_start(e: MouseEvent | TouchEvent) { |
|
e.preventDefault(); |
|
(mouse_pressing = true), (mouse_button = 0); |
|
if (e instanceof MouseEvent) mouse_button = e.button; |
|
const { x, y } = get_mouse_pos(e); |
|
(prev_x = x), (prev_y = y); |
|
} |
|
|
|
function handle_draw_move(e: MouseEvent | TouchEvent) { |
|
e.preventDefault(); |
|
const { x, y } = get_mouse_pos(e); |
|
(cur_x = x), (cur_y = y); |
|
} |
|
|
|
function handle_draw_end(e: MouseEvent | TouchEvent | FocusEvent) { |
|
e.preventDefault(); |
|
if (mouse_pressing) { |
|
const { x, y } = get_mouse_pos(e); |
|
let x1 = Math.min(prev_x, x); |
|
let y1 = Math.min(prev_y, y); |
|
let x2 = Math.max(prev_x, x); |
|
let y2 = Math.max(prev_y, y); |
|
boxes.push([x1, y1, x2, y2]); |
|
let scale_x = natural_width / width; |
|
let scale_y = natural_height / height; |
|
let is_point = x1 == x2 && y1 == y2; |
|
points.push([ |
|
Math.round(x1 * scale_x), |
|
Math.round(y1 * scale_y), |
|
is_point ? (mouse_button == 0 ? 1 : 0) : 2, |
|
is_point ? 0 : Math.round(x2 * scale_x), |
|
is_point ? 0 : Math.round(y2 * scale_y), |
|
is_point ? 4 : 3, |
|
]); |
|
dispatch("change", points); |
|
} |
|
mouse_pressing = false; |
|
} |
|
|
|
function draw_loop() { |
|
draw_canvas(); |
|
window.requestAnimationFrame(() => { |
|
draw_loop(); |
|
}); |
|
} |
|
|
|
function draw_canvas() { |
|
if (!ctx) return; |
|
ctx.clearRect(0, 0, width, height); |
|
if (mouse_pressing && cur_x != prev_x && prev_y != cur_y) { |
|
let boxes_temp = boxes.slice(); |
|
boxes_temp.push([prev_x, prev_y, cur_x, cur_y]); |
|
draw_boxes(boxes_temp); |
|
draw_points(boxes); |
|
} else { |
|
draw_boxes(boxes); |
|
draw_points(boxes); |
|
} |
|
} |
|
|
|
function draw_boxes(boxes: Array<Array<number>>) { |
|
if (!ctx) return; |
|
ctx.fillStyle = "rgba(0, 0, 0, 0.1)"; |
|
ctx.beginPath(); |
|
boxes.forEach((box: Array<number>) => { |
|
if (box[0] != box[2] && box[1] != box[3]) { |
|
ctx.rect(box[0], box[1], box[2] - box[0], box[3] - box[1]); |
|
} |
|
}); |
|
ctx.fill(); |
|
ctx.stroke(); |
|
} |
|
|
|
function draw_points(boxes: Array<Array<number>>) { |
|
if (!ctx) return; |
|
|
|
ctx.beginPath(); |
|
ctx.fillStyle = "rgba(0, 255, 255, 1.0)"; |
|
boxes.forEach((box: Array<number>, index: number) => { |
|
if (points[index][2] == 1) { |
|
let radius = Math.sqrt(width * height) * 0.01; |
|
ctx.moveTo(box[0] + radius, box[1]); |
|
ctx.arc(box[0], box[1], radius, 0, 2 * Math.PI, false); |
|
} |
|
}); |
|
ctx.fill(); |
|
ctx.stroke(); |
|
|
|
ctx.beginPath(); |
|
ctx.fillStyle = "rgba(255, 192, 203, 1.0)"; |
|
boxes.forEach((box: Array<number>, index: number) => { |
|
if (points[index][2] == 0) { |
|
let radius = Math.sqrt(width * height) * 0.01; |
|
ctx.moveTo(box[0] + radius, box[1]); |
|
ctx.arc(box[0], box[1], radius, 0, 2 * Math.PI, false); |
|
} |
|
}); |
|
ctx.fill(); |
|
ctx.stroke(); |
|
} |
|
</script> |
|
|
|
<div class="wrap" bind:this={canvas_container}> |
|
<canvas |
|
bind:this={canvas} |
|
on:mousedown={handle_draw_start} |
|
on:mousemove={handle_draw_move} |
|
on:mouseout={handle_draw_move} |
|
on:mouseup={handle_draw_end} |
|
on:touchstart={handle_draw_start} |
|
on:touchmove={handle_draw_move} |
|
on:touchend={handle_draw_end} |
|
on:touchcancel={handle_draw_end} |
|
on:blur={handle_draw_end} |
|
on:click|stopPropagation |
|
style=" z-index: 15" |
|
/> |
|
</div> |
|
|
|
<style> |
|
canvas { |
|
display: block; |
|
position: absolute; |
|
top: 0; |
|
right: 0; |
|
bottom: 0; |
|
left: 0; |
|
margin: auto; |
|
} |
|
|
|
.wrap { |
|
position: relative; |
|
width: var(--size-full); |
|
height: var(--size-full); |
|
touch-action: none; |
|
} |
|
</style> |
|
|