picture-annotation.py
Python script, ASCII text executable
1from pyscript import document, fetch as pyfetch 2from pyscript.ffi import create_proxy 3import asyncio 4import json 5 6document.getElementById("shape-options").style.display = "flex" 7 8image = document.getElementById("annotation-image") 9zone = document.getElementById("annotation-zone") 10confirm_button = document.getElementById("annotation-confirm") 11cancel_button = document.getElementById("annotation-cancel") 12backspace_button = document.getElementById("annotation-backspace") 13delete_button = document.getElementById("annotation-delete") 14previous_button = document.getElementById("annotation-previous") 15next_button = document.getElementById("annotation-next") 16save_button = document.getElementById("annotation-save") 17 18object_list = document.getElementById("object-types") 19object_list_content = document.getElementById("object-types-content") 20object_list_filter = document.getElementById("filter-object-types") 21 22confirm_button.style.display = "none" 23cancel_button.style.display = "none" 24backspace_button.style.display = "none" 25delete_button.style.display = "none" 26previous_button.style.display = "none" 27next_button.style.display = "none" 28shape_type = "" 29bbox_pos = None 30new_shape = None 31selected_shape = None 32 33 34def make_shape_container(): 35shape = document.createElementNS("http://www.w3.org/2000/svg", "svg") 36shape.setAttribute("width", "100%") 37shape.setAttribute("height", "100%") 38shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}") 39shape.classList.add("shape-container") 40 41return shape 42 43 44async def get_all_objects(): 45response = await pyfetch("/api/object-types") 46if response.ok: 47return await response.json() 48 49 50def follow_cursor(event): 51rect = zone.getBoundingClientRect() 52x = event.clientX - rect.left 53y = event.clientY - rect.top 54vertical_ruler.style.left = str(x) + "px" 55horizontal_ruler.style.top = str(y) + "px" 56 57 58def change_object_type(event): 59global selected_shape 60if selected_shape is None: 61return 62selected_shape.setAttribute("data-object-type", event.currentTarget.value) 63 64 65change_object_type_proxy = create_proxy(change_object_type) 66 67 68def list_shapes(): 69shapes = list(zone.getElementsByClassName("shape")) 70json_shapes = [] 71for shape in shapes: 72shape_dict = {} 73if shape.tagName == "rect": 74shape_dict["type"] = "bbox" 75shape_dict["shape"] = { 76"x": float(shape.getAttribute("x")) / image.naturalWidth, 77"y": float(shape.getAttribute("y")) / image.naturalHeight, 78"w": float(shape.getAttribute("width")) / image.naturalWidth, 79"h": float(shape.getAttribute("height")) / image.naturalHeight 80} 81elif shape.tagName == "polygon" or shape.tagName == "polyline": 82if shape.tagName == "polygon": 83shape_dict["type"] = "polygon" 84elif shape.tagName == "polyline": 85shape_dict["type"] = "polyline" 86 87points = shape.getAttribute("points").split(" ") 88json_points = [] 89for point in points: 90x, y = point.split(",") 91x, y = float(x), float(y) 92json_points.append({ 93"x": x / image.naturalWidth, 94"y": y / image.naturalHeight 95}) 96 97shape_dict["shape"] = json_points 98elif shape.tagName == "circle" and shape.classList.contains("shape-point"): 99shape_dict["type"] = "point" 100shape_dict["shape"] = { 101"x": float(shape.getAttribute("cx")) / image.naturalWidth, 102"y": float(shape.getAttribute("cy")) / image.naturalHeight 103} 104else: 105continue 106 107shape_dict["object"] = shape.getAttribute("data-object-type") 108json_shapes.append(shape_dict) 109 110return json_shapes 111 112 113def put_shapes(json_shapes): 114for shape in json_shapes: 115new_shape = make_shape_container() 116zone_rect = zone.getBoundingClientRect() 117 118if shape["type"] == "bbox": 119rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect") 120rectangle.setAttribute("x", str(shape["shape"]["x"] * image.naturalWidth)) 121rectangle.setAttribute("y", str(shape["shape"]["y"] * image.naturalHeight)) 122rectangle.setAttribute("width", str(shape["shape"]["w"] * image.naturalWidth)) 123rectangle.setAttribute("height", str(shape["shape"]["h"] * image.naturalHeight)) 124rectangle.setAttribute("fill", "none") 125rectangle.setAttribute("data-object-type", shape["object"] or "") 126rectangle.classList.add("shape-bbox") 127rectangle.classList.add("shape") 128new_shape.appendChild(rectangle) 129elif shape["type"] == "polygon" or shape["type"] == "polyline": 130polygon = document.createElementNS("http://www.w3.org/2000/svg", shape["type"]) 131points = " ".join( 132[f"{point['x'] * image.naturalWidth},{point['y'] * image.naturalHeight}" for point in shape["shape"]]) 133polygon.setAttribute("points", points) 134polygon.setAttribute("fill", "none") 135polygon.setAttribute("data-object-type", shape["object"] or "") 136polygon.classList.add(f"shape-{shape['type']}") 137polygon.classList.add("shape") 138new_shape.appendChild(polygon) 139elif shape["type"] == "point": 140point = document.createElementNS("http://www.w3.org/2000/svg", "circle") 141point.setAttribute("cx", str(shape["shape"]["x"] * image.naturalWidth)) 142point.setAttribute("cy", str(shape["shape"]["y"] * image.naturalHeight)) 143point.setAttribute("r", "0") 144point.classList.add("shape-point") 145point.classList.add("shape") 146point.setAttribute("data-object-type", shape["object"] or "") 147new_shape.appendChild(point) 148 149zone.appendChild(new_shape) 150 151 152async def load_shapes(): 153resource_id = document.getElementById("resource-id").value 154response = await pyfetch(f"/picture/{resource_id}/get-annotations") 155if response.ok: 156shapes = await response.json() 157return shapes 158 159 160async def save_shapes(event): 161shapes = list_shapes() 162resource_id = document.getElementById("resource-id").value 163print("Saving shapes:", shapes) 164response = await pyfetch(f"/picture/{resource_id}/save-annotations", 165method="POST", 166headers={ 167"Content-Type": "application/json" 168}, 169body=json.dumps(shapes) 170) 171if response.ok: 172return await response 173 174 175save_shapes_proxy = create_proxy(save_shapes) 176save_button.addEventListener("click", save_shapes_proxy) 177delete_shape_key_proxy = create_proxy(lambda event: event.key == "Delete" and delete_shape_proxy(None)) 178next_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowRight" and next_shape_proxy(None)) 179previous_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowLeft" and previous_shape_proxy(None)) 180 181 182def get_centre(shape): 183if shape.tagName == "rect": 184x = float(shape.getAttribute("x")) + float(shape.getAttribute("width")) / 2 185y = float(shape.getAttribute("y")) + float(shape.getAttribute("height")) / 2 186elif shape.tagName == "polygon": 187points = shape.getAttribute("points").split(" ") 188# Average of the extreme points 189sorted_x = sorted([float(point.split(",")[0]) for point in points]) 190sorted_y = sorted([float(point.split(",")[1]) for point in points]) 191top = sorted_y[0] 192bottom = sorted_y[-1] 193left = sorted_x[0] 194right = sorted_x[-1] 195 196x = (left + right) / 2 197y = (top + bottom) / 2 198elif shape.tagName == "polyline": 199points = shape.getAttribute("points").split(" ") 200# Median point 201x = float(points[len(points) // 2].split(",")[0]) 202y = float(points[len(points) // 2].split(",")[1]) 203elif shape.tagName == "circle" and shape.classList.contains("shape-point"): 204x = float(shape.getAttribute("cx")) 205y = float(shape.getAttribute("cy")) 206else: 207return None 208 209return x, y 210 211 212async def update_object_list_filter(event): 213filter_text = event.currentTarget.value 214for label in object_list_content.children: 215if label.innerText.lower().find(filter_text.lower()) == -1: 216label.style.display = "none" 217else: 218label.style.display = "flex" 219 220 221async def focus_shape(shape): 222global selected_shape 223 224if shape_type != "select": 225return 226if selected_shape is not None: 227selected_shape.classList.remove("selected") 228 229selected_shape = shape 230 231selected_shape.classList.add("selected") 232 233objects = await get_all_objects() 234 235delete_button.style.display = "flex" 236next_button.style.display = "flex" 237previous_button.style.display = "flex" 238document.addEventListener("keydown", delete_shape_key_proxy) 239document.addEventListener("keydown", next_shape_key_proxy) 240document.addEventListener("keydown", previous_shape_key_proxy) 241 242object_list_content.innerHTML = "" 243object_list_filter.value = "" 244new_radio = document.createElement("input") 245new_radio.setAttribute("type", "radio") 246new_radio.setAttribute("name", "object-type") 247new_radio.setAttribute("tabindex", "1") # Focus first 248new_radio.setAttribute("value", "") 249new_label = document.createElement("label") 250new_label.appendChild(new_radio) 251new_label.append("Undefined") 252object_list_content.appendChild(new_label) 253new_radio.addEventListener("change", change_object_type_proxy) 254 255selected_object = selected_shape.getAttribute("data-object-type") 256if not selected_object: 257new_radio.setAttribute("checked", "") 258 259for object, description in objects.items(): 260new_radio = document.createElement("input") 261new_radio.setAttribute("type", "radio") 262new_radio.setAttribute("name", "object-type") 263new_radio.setAttribute("value", object) 264if selected_object == object: 265new_radio.setAttribute("checked", "") 266new_label = document.createElement("label") 267new_label.appendChild(new_radio) 268new_label.append(object) 269object_list_content.appendChild(new_label) 270new_radio.addEventListener("change", change_object_type_proxy) 271 272object_list.style.display = "flex" 273object_list_filter.focus() 274object_list_filter.addEventListener("input", update_object_list_filter) 275object_list.style.left = str(get_centre(shape)[0] / image.naturalWidth * 100) + "%" 276object_list.style.top = str(get_centre(shape)[1] / image.naturalHeight * 100) + "%" 277object_list.style.right = "auto" 278object_list.style.bottom = "auto" 279object_list.style.maxHeight = str(image.height - get_centre(shape)[1] / image.naturalHeight * image.height) + "px" 280object_list.style.maxWidth = str(image.width - get_centre(shape)[0] / image.naturalWidth * image.width) + "px" 281 282if get_centre(shape)[0] / image.naturalWidth * image.width + object_list.offsetWidth > image.width: 283object_list.style.left = "auto" 284object_list.style.right = str((image.naturalWidth - get_centre(shape)[0]) / image.naturalWidth * 100) + "%" 285object_list.style.maxWidth = str(get_centre(shape)[0] / image.naturalWidth * image.width) + "px" 286 287if get_centre(shape)[1] / image.naturalHeight * image.height + object_list.offsetHeight > image.height: 288object_list.style.top = "auto" 289object_list.style.bottom = str((image.naturalHeight - get_centre(shape)[1]) / image.naturalHeight * 100) + "%" 290object_list.style.maxHeight = str(get_centre(shape)[1] / image.naturalHeight * image.height) + "px" 291 292 293async def select_shape(event): 294await focus_shape(event.target) 295 296 297async def next_shape(event): 298global selected_shape 299if selected_shape is None: 300return 301 302selected_svg = selected_shape.parentNode 303 304while selected_svg is not None: 305next_sibling = selected_svg.nextElementSibling 306if next_sibling and next_sibling.classList.contains("shape-container"): 307selected_svg = next_sibling 308break 309elif next_sibling is None: 310# If no more siblings, loop back to the first child 311selected_svg = selected_svg.parentNode.firstElementChild 312while selected_svg is not None and not selected_svg.classList.contains( 313"shape-container"): 314selected_svg = selected_svg.nextElementSibling 315break 316else: 317selected_svg = next_sibling 318 319if selected_svg: 320shape = selected_svg.firstElementChild 321await focus_shape(shape) 322 323event.preventDefault() 324 325 326async def previous_shape(event): 327global selected_shape 328if selected_shape is None: 329return 330 331selected_svg = selected_shape.parentNode 332 333while selected_svg is not None: 334next_sibling = selected_svg.previousElementSibling 335if next_sibling and next_sibling.classList.contains("shape-container"): 336selected_svg = next_sibling 337break 338elif next_sibling is None: 339# If no more siblings, loop back to the last child 340selected_svg = selected_svg.parentNode.lastElementChild 341while selected_svg is not None and not selected_svg.classList.contains( 342"shape-container"): 343selected_svg = selected_svg.previousElementSibling 344break 345else: 346selected_svg = next_sibling 347 348if selected_svg: 349shape = selected_svg.firstElementChild 350await focus_shape(shape) 351 352event.preventDefault() 353 354 355def unselect_shape(event): 356global selected_shape 357 358if selected_shape is not None: 359selected_shape.classList.remove("selected") 360selected_shape = None 361 362object_list_content.innerHTML = "" 363object_list.style.display = "none" 364delete_button.style.display = "none" 365next_button.style.display = "none" 366previous_button.style.display = "none" 367document.removeEventListener("keydown", delete_shape_key_proxy) 368document.removeEventListener("keydown", next_shape_key_proxy) 369document.removeEventListener("keydown", previous_shape_key_proxy) 370 371 372def delete_shape(event): 373global selected_shape 374if selected_shape is None: 375return 376# Shape is SVG shape inside SVG so we need to remove the parent SVG 377selected_shape.parentNode.remove() 378selected_shape = None 379object_list_content.innerHTML = "" 380object_list.style.display = "none" 381delete_button.style.display = "none" 382next_button.style.display = "none" 383previous_button.style.display = "none" 384document.removeEventListener("keydown", delete_shape_key_proxy) 385document.removeEventListener("keydown", next_shape_key_proxy) 386document.removeEventListener("keydown", previous_shape_key_proxy) 387 388 389select_shape_proxy = create_proxy(select_shape) 390unselect_shape_proxy = create_proxy(unselect_shape) 391delete_shape_proxy = create_proxy(delete_shape) 392next_shape_proxy = create_proxy(next_shape) 393previous_shape_proxy = create_proxy(previous_shape) 394 395delete_button.addEventListener("click", delete_shape_proxy) 396next_button.addEventListener("click", next_shape_proxy) 397previous_button.addEventListener("click", previous_shape_proxy) 398 399# These are functions usable in JS 400cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event)) 401make_bbox_proxy = create_proxy(lambda event: make_bbox(event)) 402make_polygon_proxy = create_proxy(lambda event: make_polygon(event)) 403follow_cursor_proxy = create_proxy(follow_cursor) 404 405 406def switch_shape(event): 407global shape_type 408object_list_content.innerHTML = "" 409unselect_shape(None) 410shape = event.currentTarget.id 411shape_type = shape 412for button in list(document.getElementById("shape-selector").children): 413if not button.classList.contains("button-flat"): 414button.classList.add("button-flat") 415event.currentTarget.classList.remove("button-flat") 416if shape_type == "select": 417# Add event listeners to existing shapes 418print(len(list(document.getElementsByClassName("shape"))), "shapes found") 419for shape in document.getElementsByClassName("shape"): 420print("Adding event listener to shape:", shape) 421shape.addEventListener("click", select_shape_proxy) 422image.addEventListener("click", unselect_shape_proxy) 423helper_message.innerText = "Click on a shape to select" 424# Cancel the current shape creation 425if shape_type == "shape-bbox": 426cancel_bbox(None) 427elif shape_type == "shape-polygon": 428cancel_polygon(None) 429elif shape_type == "shape-polyline": 430cancel_polygon(None) 431else: 432# Remove event listeners for selection 433for shape in document.getElementsByClassName("shape"): 434print("Removing event listener from shape:", shape) 435shape.removeEventListener("click", select_shape_proxy) 436image.removeEventListener("click", unselect_shape_proxy) 437helper_message.innerText = "Select a shape type then click on the image to begin defining it" 438print("Shape is now of type:", shape) 439 440 441def select_mode(): 442global shape_type 443document.getElementById("select").click() 444 445 446vertical_ruler = document.getElementById("annotation-ruler-vertical") 447horizontal_ruler = document.getElementById("annotation-ruler-horizontal") 448vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary") 449horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary") 450helper_message = document.getElementById("annotation-helper-message") 451 452helper_message.innerText = "Select a shape type then click on the image to begin defining it" 453 454 455def cancel_bbox(event): 456global bbox_pos, new_shape 457 458# Key must be ESCAPE 459if event is not None and hasattr(event, "key") and event.key != "Escape": 460return 461 462if new_shape is not None and event is not None: 463# Require event so the shape is kept when it ends normally 464new_shape.remove() 465zone.removeEventListener("click", make_bbox_proxy) 466document.removeEventListener("keydown", cancel_bbox_proxy) 467cancel_button.removeEventListener("click", cancel_bbox_proxy) 468 469bbox_pos = None 470vertical_ruler.style.display = "none" 471horizontal_ruler.style.display = "none" 472vertical_ruler_2.style.display = "none" 473horizontal_ruler_2.style.display = "none" 474zone.style.cursor = "auto" 475cancel_button.style.display = "none" 476helper_message.innerText = "Select a shape type then click on the image to begin defining it" 477new_shape = None 478 479 480def make_bbox(event): 481global new_shape, bbox_pos 482zone_rect = zone.getBoundingClientRect() 483 484if bbox_pos is None: 485helper_message.innerText = "Now define the second point" 486 487bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width, 488(event.clientY - zone_rect.top) / zone_rect.height] 489vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%" 490horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%" 491vertical_ruler_2.style.display = "block" 492horizontal_ruler_2.style.display = "block" 493 494else: 495x0, y0 = bbox_pos.copy() 496x1 = (event.clientX - zone_rect.left) / zone_rect.width 497y1 = (event.clientY - zone_rect.top) / zone_rect.height 498 499rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect") 500 501new_shape = make_shape_container() 502zone_rect = zone.getBoundingClientRect() 503 504new_shape.appendChild(rectangle) 505zone.appendChild(new_shape) 506 507minx = min(x0, x1) 508miny = min(y0, y1) 509maxx = max(x0, x1) 510maxy = max(y0, y1) 511 512rectangle.setAttribute("x", str(minx * image.naturalWidth)) 513rectangle.setAttribute("y", str(miny * image.naturalHeight)) 514rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth)) 515rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight)) 516rectangle.setAttribute("fill", "none") 517rectangle.setAttribute("data-object-type", "") 518rectangle.classList.add("shape-bbox") 519rectangle.classList.add("shape") 520 521# Add event listeners to the new shape 522rectangle.addEventListener("click", select_shape_proxy) 523 524cancel_bbox(None) 525 526 527polygon_points = [] 528 529 530def make_polygon(event): 531global new_shape, polygon_points 532 533polygon = new_shape.children[0] 534 535zone_rect = zone.getBoundingClientRect() 536 537polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width, 538(event.clientY - zone_rect.top) / zone_rect.height)) 539 540# Update the polygon 541polygon.setAttribute("points", " ".join( 542[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in 543polygon_points])) 544 545 546def reset_polygon(): 547global new_shape, polygon_points 548 549zone.removeEventListener("click", make_polygon_proxy) 550document.removeEventListener("keydown", close_polygon_proxy) 551document.removeEventListener("keydown", cancel_polygon_proxy) 552document.removeEventListener("keydown", backspace_polygon_proxy) 553confirm_button.style.display = "none" 554cancel_button.style.display = "none" 555backspace_button.style.display = "none" 556confirm_button.removeEventListener("click", close_polygon_proxy) 557cancel_button.removeEventListener("click", cancel_polygon_proxy) 558backspace_button.removeEventListener("click", backspace_polygon_proxy) 559polygon_points.clear() 560 561zone.style.cursor = "auto" 562new_shape = None 563 564 565def close_polygon(event): 566if event is not None and hasattr(event, "key") and event.key != "Enter": 567return 568# Polygon is already there, but we need to remove the events 569reset_polygon() 570 571 572def cancel_polygon(event): 573if event is not None and hasattr(event, "key") and event.key != "Escape": 574return 575# Delete the polygon 576new_shape.remove() 577reset_polygon() 578 579 580def backspace_polygon(event): 581if event is not None and hasattr(event, "key") and event.key != "Backspace": 582return 583if not polygon_points: 584return 585polygon_points.pop() 586polygon = new_shape.children[0] 587polygon.setAttribute("points", " ".join( 588[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in 589polygon_points])) 590 591 592close_polygon_proxy = create_proxy(close_polygon) 593cancel_polygon_proxy = create_proxy(cancel_polygon) 594backspace_polygon_proxy = create_proxy(backspace_polygon) 595 596 597def open_shape(event): 598global new_shape, bbox_pos 599if bbox_pos or shape_type == "select": 600return 601print("Creating a new shape of type:", shape_type) 602 603if shape_type == "shape-bbox": 604helper_message.innerText = ("Define the first point at the intersection of the lines " 605"by clicking on the image, or click the cross to cancel") 606 607cancel_button.addEventListener("click", cancel_bbox_proxy) 608document.addEventListener("keydown", cancel_bbox_proxy) 609cancel_button.style.display = "flex" 610bbox_pos = None 611zone.addEventListener("click", make_bbox_proxy) 612vertical_ruler.style.display = "block" 613horizontal_ruler.style.display = "block" 614zone.style.cursor = "crosshair" 615elif shape_type == "shape-polygon" or shape_type == "shape-polyline": 616if shape_type == "shape-polygon": 617helper_message.innerText = ("Click on the image to define the points of the polygon, " 618"press escape to cancel, enter to close, or backspace to " 619"remove the last point") 620elif shape_type == "shape-polyline": 621helper_message.innerText = ("Click on the image to define the points of the polyline, " 622"press escape to cancel, enter to finish, or backspace to " 623"remove the last point") 624 625if not polygon_points and not new_shape: 626new_shape = make_shape_container() 627zone_rect = zone.getBoundingClientRect() 628 629if not polygon_points and int(new_shape.children.length) == 0: 630zone.addEventListener("click", make_polygon_proxy) 631document.addEventListener("keydown", close_polygon_proxy) 632document.addEventListener("keydown", cancel_polygon_proxy) 633document.addEventListener("keydown", backspace_polygon_proxy) 634cancel_button.addEventListener("click", cancel_polygon_proxy) 635cancel_button.style.display = "flex" 636confirm_button.addEventListener("click", close_polygon_proxy) 637confirm_button.style.display = "flex" 638backspace_button.addEventListener("click", backspace_polygon_proxy) 639backspace_button.style.display = "flex" 640if shape_type == "shape-polygon": 641polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon") 642polygon.classList.add("shape-polygon") 643elif shape_type == "shape-polyline": 644polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline") 645polygon.classList.add("shape-polyline") 646polygon.setAttribute("fill", "none") 647polygon.setAttribute("data-object-type", "") 648polygon.classList.add("shape") 649new_shape.appendChild(polygon) 650zone.appendChild(new_shape) 651zone.style.cursor = "crosshair" 652elif shape_type == "shape-point": 653point = document.createElementNS("http://www.w3.org/2000/svg", "circle") 654zone_rect = zone.getBoundingClientRect() 655point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth)) 656point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight)) 657point.setAttribute("r", "0") 658point.classList.add("shape-point") 659point.classList.add("shape") 660point.setAttribute("data-object-type", "") 661 662new_shape = make_shape_container() 663zone_rect = zone.getBoundingClientRect() 664 665new_shape.appendChild(point) 666zone.appendChild(new_shape) 667 668new_shape = None 669 670 671 672for button in list(document.getElementById("shape-selector").children): 673button.addEventListener("click", create_proxy(switch_shape)) 674print("Shape", button.id, "is available") 675 676zone.addEventListener("mousemove", follow_cursor_proxy) 677zone.addEventListener("click", create_proxy(open_shape)) 678 679# Load existing annotations, if any 680put_shapes(await load_shapes()) 681print("Ready!") 682