Spaces:
Running
Running
<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, // label1 | |
is_point ? 0 : Math.round(x2 * scale_x), | |
is_point ? 0 : Math.round(y2 * scale_y), | |
is_point ? 4 : 3, // label2 | |
]); | |
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; | |
// Draw foreground points. | |
ctx.beginPath(); | |
ctx.fillStyle = "rgba(0, 255, 255, 1.0)"; // Cyan. | |
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(); | |
// Draw background points. | |
ctx.beginPath(); | |
ctx.fillStyle = "rgba(255, 192, 203, 1.0)"; // Pink. | |
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> | |