143 lines
3.4 KiB
Svelte
143 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import type { Annotation } from '$lib/types';
|
|
import AnnotationEditOverlay from './AnnotationEditOverlay.svelte';
|
|
|
|
let {
|
|
annotation,
|
|
isHovered,
|
|
isActive,
|
|
faded = false,
|
|
dimmed = false,
|
|
blockNumber = undefined,
|
|
isFlashing = false,
|
|
isResizable = false,
|
|
onclick,
|
|
onpointerenter,
|
|
onpointerleave
|
|
}: {
|
|
annotation: Annotation;
|
|
isHovered: boolean;
|
|
isActive: boolean;
|
|
faded?: boolean;
|
|
dimmed?: boolean;
|
|
blockNumber?: number | undefined;
|
|
isFlashing?: boolean;
|
|
isResizable?: boolean;
|
|
onclick: () => void;
|
|
onpointerenter: () => void;
|
|
onpointerleave: () => void;
|
|
} = $props();
|
|
|
|
function hexToRgba(hex: string, alpha: number): string {
|
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
}
|
|
|
|
let clipPath = $derived.by(() => {
|
|
if (!annotation.polygon || annotation.polygon.length !== 4) return 'none';
|
|
const points = annotation.polygon
|
|
.map(([px, py]) => {
|
|
const cx = ((px - annotation.x) / annotation.width) * 100;
|
|
const cy = ((py - annotation.y) / annotation.height) * 100;
|
|
return `${cx}% ${cy}%`;
|
|
})
|
|
.join(', ');
|
|
return `polygon(${points})`;
|
|
});
|
|
|
|
let bgAlpha = $derived(dimmed ? 0.3 : isHovered || isActive ? 0.5 : 0.3);
|
|
|
|
let boxShadow = $derived.by(() => {
|
|
if (dimmed) return 'none';
|
|
if (isActive || isHovered) return `inset 0 0 0 2px ${hexToRgba(annotation.color, 0.8)}`;
|
|
return 'none';
|
|
});
|
|
|
|
let opacity = $derived(dimmed ? 1 : faded ? 0.3 : 1);
|
|
|
|
let shapeStyle = $derived(
|
|
`position: absolute;` +
|
|
` left: ${annotation.x * 100}%;` +
|
|
` top: ${annotation.y * 100}%;` +
|
|
` width: ${annotation.width * 100}%;` +
|
|
` height: ${annotation.height * 100}%;` +
|
|
` background-color: ${hexToRgba(annotation.color, bgAlpha)};` +
|
|
` box-shadow: ${boxShadow};` +
|
|
` opacity: ${opacity};` +
|
|
` pointer-events: auto;` +
|
|
` cursor: pointer;` +
|
|
` transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.3s ease;` +
|
|
(clipPath !== 'none' ? ` clip-path: ${clipPath};` : '')
|
|
);
|
|
</script>
|
|
|
|
<div
|
|
data-testid="annotation-{annotation.id}"
|
|
data-annotation
|
|
class:annotation-flash={isFlashing}
|
|
role="button"
|
|
tabindex="0"
|
|
aria-label="Block anzeigen"
|
|
onclick={onclick}
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') onclick();
|
|
}}
|
|
onpointerenter={onpointerenter}
|
|
onpointerleave={onpointerleave}
|
|
style={shapeStyle}
|
|
>
|
|
{#if !dimmed && blockNumber}
|
|
<div
|
|
style="
|
|
position: absolute;
|
|
top: -8px;
|
|
left: -8px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background-color: {annotation.color};
|
|
color: white;
|
|
font-size: 12px;
|
|
font-family: sans-serif;
|
|
font-weight: 700;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
pointer-events: none;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
"
|
|
>
|
|
{blockNumber}
|
|
</div>
|
|
{/if}
|
|
{#if isResizable}
|
|
<AnnotationEditOverlay annotation={annotation} />
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes annotation-flash-anim {
|
|
0% {
|
|
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 80%, transparent);
|
|
outline-offset: 0px;
|
|
}
|
|
100% {
|
|
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 0%, transparent);
|
|
outline-offset: 2px;
|
|
}
|
|
}
|
|
|
|
.annotation-flash {
|
|
animation: annotation-flash-anim 1.5s ease-out;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.annotation-flash {
|
|
animation: none;
|
|
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 80%, transparent);
|
|
}
|
|
}
|
|
</style>
|