roundabout,
created on Sunday, 5 January 2025, 21:24:26 (1736112266),
received on Sunday, 5 January 2025, 21:24:29 (1736112269)
Author identity: vlad <vlad.muntoiu@gmail.com>
87febd36d95cac58a7cde0cf4d71f4fd74d61d19
static/picture-annotation.py
@@ -49,8 +49,8 @@ async def get_all_objects():
def follow_cursor(event): rect = zone.getBoundingClientRect() x = event.clientX - rect.lefty = event.clientY - rect.topx = (event.clientX - rect.left) / scale y = (event.clientY - rect.top) / scalevertical_ruler.style.left = str(x) + "px" horizontal_ruler.style.top = str(y) + "px"
@@ -278,16 +278,21 @@ async def focus_shape(shape):
object_list.style.bottom = "auto" object_list.style.maxHeight = str(image.height - get_centre(shape)[1] / image.naturalHeight * image.height) + "px" object_list.style.maxWidth = str(image.width - get_centre(shape)[0] / image.naturalWidth * image.width) + "px" transform_origin = ["top", "left"]if get_centre(shape)[0] / image.naturalWidth * image.width + object_list.offsetWidth > image.width: object_list.style.left = "auto" object_list.style.right = str((image.naturalWidth - get_centre(shape)[0]) / image.naturalWidth * 100) + "%" object_list.style.maxWidth = str(get_centre(shape)[0] / image.naturalWidth * image.width) + "px" transform_origin[1] = "right"if get_centre(shape)[1] / image.naturalHeight * image.height + object_list.offsetHeight > image.height: object_list.style.top = "auto" object_list.style.bottom = str((image.naturalHeight - get_centre(shape)[1]) / image.naturalHeight * 100) + "%" object_list.style.maxHeight = str(get_centre(shape)[1] / image.naturalHeight * image.height) + "px" transform_origin[0] = "bottom" object_list.style.transformOrigin = " ".join(transform_origin)async def select_shape(event):
@@ -475,6 +480,7 @@ def cancel_bbox(event):
cancel_button.style.display = "none" helper_message.innerText = "Select a shape type then click on the image to begin defining it" new_shape = None update_transform()def make_bbox(event):
@@ -561,6 +567,8 @@ def reset_polygon():
zone.style.cursor = "auto" new_shape = None update_transform() def close_polygon(event): if event is not None and hasattr(event, "key") and event.key != "Enter":
@@ -667,6 +675,8 @@ def open_shape(event):
new_shape = None update_transform() for button in list(document.getElementById("shape-selector").children):
@@ -676,6 +686,80 @@ for button in list(document.getElementById("shape-selector").children):
zone.addEventListener("mousemove", follow_cursor_proxy) zone.addEventListener("click", create_proxy(open_shape)) # Zoom zoom_container = document.getElementById("annotation-zoom-container") scale = 1 translate_x = 0 translate_y = 0 def update_transform(): zone.style.transform = f"scale({scale}) translate({translate_x}px, {translate_y}px)" object_list.style.transform = f"scale({1/scale})" # neutralise the zoom vertical_ruler.style.transform = f"scale({1/scale})" horizontal_ruler.style.transform = f"scale({1/scale})" vertical_ruler_2.style.transform = f"scale({1/scale})" horizontal_ruler_2.style.transform = f"scale({1/scale})" for svg in zone.getElementsByClassName("shape-container"): # Because vector-effect="non-scaling-stroke" doesn't work when an HTML ancestor is # scaled, we need to scale the shapes down then scale them back up svg.style.transform = f"scale({1/scale})" svg.style.transformOrigin = "0 0" for shape in svg.children: shape.setAttribute("transform", f"scale({scale})") shape.setAttribute("transform-origin", "0 0") scale_exponent = 0 zoom_offset = (0, 0) def on_wheel(event): global scale, scale_exponent, translate_x, translate_y event.preventDefault() scale_exponent += (-1 if event.deltaY > 0 else 1) / 16 new_scale = 2 ** scale_exponent rect = zoom_container.getBoundingClientRect() pointer_x = event.clientX - rect.left pointer_y = event.clientY - rect.top translate_x = pointer_x - (pointer_x - translate_x) * (new_scale / scale) translate_y = pointer_y - (pointer_y - translate_y) * (new_scale / scale) scale = new_scale update_transform() is_dragging = False start_x = 0 start_y = 0 def on_mouse_down(event): global is_dragging, start_x, start_y is_dragging = True start_x = event.clientX start_y = event.clientY def on_mouse_move(event): global translate_x, translate_y if is_dragging: translate_x += event.movementX / scale translate_y += event.movementY / scale update_transform() def on_mouse_up(event): global is_dragging is_dragging = False zoom_container.addEventListener("wheel", on_wheel) zoom_container.addEventListener("mousedown", on_mouse_down) document.addEventListener("mousemove", on_mouse_move) document.addEventListener("mouseup", on_mouse_up) # Load existing annotations, if any put_shapes(await load_shapes()) print("Ready!")
static/style.css
@@ -66,14 +66,12 @@ iconify-icon {
#annotation-zone, .annotation-zone { position: relative; user-select: none; }.annotation-zone {overflow: hidden; transform-origin: 50% 50%;} #annotation-zone {overflow: hidden;#annotation-zoom-container > #annotation-zone { overflow: visible;} #annotation-image, .annotation-image {
@@ -115,6 +113,7 @@ iconify-icon {
fill: var(--color-accent); fill-opacity: 0.1; stroke-width: 2px; --shape-stroke-width: 2px;vector-effect: non-scaling-stroke; pointer-events: auto; transition: filter 0.25s cubic-bezier(0.61, 1, 0.88, 1),
@@ -131,6 +130,7 @@ iconify-icon {
.shape-point { stroke: var(--color-accent); stroke-width: 2px; --shape-radius: 2px;fill-opacity: 1; r: 2; transition: r 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
@@ -141,12 +141,14 @@ iconify-icon {
.shape.selected { fill-opacity: 0.2; stroke-width: 4px; --shape-stroke-width: 4px;filter: drop-shadow(0 0 2px var(--text-soft)); } .shape-point.selected { fill-opacity: 1; r: 6; --shape-radius: 6px;stroke-width: 6px; }
@@ -154,10 +156,6 @@ iconify-icon {
pointer-events: none; } #annotation-helper-message {min-height: 2lh;}.shape-container-viewonly > .shape, .thumbnail-list > li .shape { fill: var(--color-info); stroke: var(--color-info);
@@ -812,6 +810,44 @@ nav .button-flat .ripple-pad {
} #annotation-helper-message { padding: 1rem;padding: 0.75rem;text-align: center; flex: 1 0 auto; } #annotation-zoom-container { overflow: hidden; } #annotation-zoom-container { flex: 0 1 auto; overflow: scroll; } #annotation-zoom-container > #annotation-zone { transform: scale(1); transition: transform 0.125s ease; max-width: 100%; max-height: 100%; } #annotation-main-area { display: flex; flex-direction: column; flex: 1 1 auto; overflow: auto; } body.fixed-content-area { max-width: 100vw; max-height: 100vh; display: flex; flex-direction: column; } body.fixed-content-area > main { flex: 0 1 auto; overflow: auto; display: flex; flex-direction: column;}
templates/default.html
@@ -14,7 +14,7 @@
} } </style> <body><body {% if fixed_content_area %}class="fixed-content-area"{% endif %}><dialog id="hamburger" class="sheet-left"> <nav> <ul>
templates/picture-annotation.html
@@ -3,6 +3,7 @@
{% block nav_title %}{{ resource.title }}{% endblock %} {% block title %}Annotating picture {{ resource.title }} | {{ site_name }}{% endblock %} {% set back_url = "/picture/" + resource.id|string %} {% set fixed_content_area = true %}{% block content %} <style>
@@ -13,7 +14,7 @@
</style> <script type="module" src="https://pyscript.net/releases/2024.8.2/core.js"></script> <x-frame id="main-area"><x-frame id="annotation-main-area"><x-hbox id="annotation-controls"> <x-buttonbox id="shape-selector"> <button class="button-flat" title="selection" id="select">
@@ -57,16 +58,18 @@
</button> </x-buttonbox> </x-hbox> <div id="annotation-zone"><x-vbox id="object-types" style="--gap-box: 0.25rem; --padding-box: 1rem;"><input type="text" id="filter-object-types" placeholder="Search" aria-label="Search object types"><x-vbox id="object-types-content"></x-vbox></x-vbox><img id="annotation-image" src="/raw/picture/{{ resource.id }}" alt=""><div id="annotation-ruler-vertical" class="annotation-ruler"></div><div id="annotation-ruler-horizontal" class="annotation-ruler"></div><div id="annotation-ruler-vertical-secondary" class="annotation-ruler"></div><div id="annotation-ruler-horizontal-secondary" class="annotation-ruler"></div><div id="annotation-zoom-container"> <div id="annotation-zone"> <x-vbox id="object-types" style="--gap-box: 0.25rem; --padding-box: 1rem;"> <input type="text" id="filter-object-types" placeholder="Search" aria-label="Search object types"> <x-vbox id="object-types-content"></x-vbox> </x-vbox> <img id="annotation-image" src="/raw/picture/{{ resource.id }}" alt=""> <div id="annotation-ruler-vertical" class="annotation-ruler"></div> <div id="annotation-ruler-horizontal" class="annotation-ruler"></div> <div id="annotation-ruler-vertical-secondary" class="annotation-ruler"></div> <div id="annotation-ruler-horizontal-secondary" class="annotation-ruler"></div> </div></div> <p id="annotation-helper-message">Please wait for Python to load...</p> </x-frame>