|
| 1 | +--- |
| 2 | +import type { ImageMetadata } from "astro"; |
| 3 | +import { Image } from "astro:assets"; |
| 4 | +
|
| 5 | +type Props = { |
| 6 | + image: ImageMetadata; |
| 7 | + x: number | string; |
| 8 | + y: number | string; |
| 9 | + scale?: number; |
| 10 | + reverse?: boolean; |
| 11 | + vertical?: boolean; |
| 12 | +} & Record<string, any>; |
| 13 | +
|
| 14 | +const { |
| 15 | + image: { width, height }, |
| 16 | + x, |
| 17 | + y, |
| 18 | + scale: rawScale = 1, |
| 19 | + reverse = false, |
| 20 | + vertical = false, |
| 21 | + ...props |
| 22 | +} = Astro.props; |
| 23 | +
|
| 24 | +props.alt ??= ""; |
| 25 | +
|
| 26 | +const defaultSize = { width: 1280, height: 720 }; |
| 27 | +
|
| 28 | +const scale = |
| 29 | + Math.max(width / defaultSize.width, height / defaultSize.height) * rawScale; |
| 30 | +
|
| 31 | +function parseCoord( |
| 32 | + rawX: string | number | null | undefined, |
| 33 | + rawY: string | number | null | undefined, |
| 34 | + width?: number, |
| 35 | + height?: number |
| 36 | +): { x: number; y: number } | undefined { |
| 37 | + const parse = (coord: number | string | null | undefined, max = 1) => |
| 38 | + typeof coord === "string" && coord.endsWith("%") |
| 39 | + ? (Number(coord.substring(0, coord.length - 1)) * max) / 100 |
| 40 | + : Number(coord); |
| 41 | +
|
| 42 | + const x = parse(rawX, width), |
| 43 | + y = parse(rawY, height); |
| 44 | +
|
| 45 | + return !Number.isNaN(x) && !Number.isNaN(y) ? { x, y } : undefined; |
| 46 | +} |
| 47 | +
|
| 48 | +const pointAt = parseCoord(x, y, defaultSize.width, defaultSize.height); |
| 49 | +
|
| 50 | +const xor = (a: boolean, b: boolean) => (a || b) && !(a && b); |
| 51 | +
|
| 52 | +const tl = (xor(reverse, vertical) ? -130 : 130) * scale; // tail length |
| 53 | +const tw = 20 * scale; // tail width |
| 54 | +const hl = (xor(reverse, vertical) ? -70 : 70) * scale; // head length |
| 55 | +const ho = 20 * scale; // head overhang |
| 56 | +
|
| 57 | +const path = |
| 58 | + pointAt !== undefined |
| 59 | + ? [ |
| 60 | + `M ${pointAt.x} ${pointAt.y}`, |
| 61 | + "l", |
| 62 | + vertical ? `${-ho - tw / 2} ${-hl}` : `${-hl} ${-ho - tw / 2}`, |
| 63 | + vertical ? "h" : "v", |
| 64 | + ho, |
| 65 | + vertical ? "v" : "h", |
| 66 | + -tl, |
| 67 | + vertical ? "h" : "v", |
| 68 | + tw, |
| 69 | + vertical ? "v" : "h", |
| 70 | + tl, |
| 71 | + vertical ? "h" : "v", |
| 72 | + ho, |
| 73 | + "Z", |
| 74 | + ].join(" ") |
| 75 | + : ""; |
| 76 | +--- |
| 77 | + |
| 78 | +<div> |
| 79 | + <Image src={Astro.props.image} {...props} /> |
| 80 | + <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${width} ${height}`}> |
| 81 | + <path d={path} fill="#ff3333" stroke="none"></path> |
| 82 | + </svg> |
| 83 | +</div> |
| 84 | + |
| 85 | +<style> |
| 86 | + div { |
| 87 | + position: relative; |
| 88 | + } |
| 89 | + |
| 90 | + div > svg { |
| 91 | + position: absolute; |
| 92 | + top: 0; |
| 93 | + left: 0; |
| 94 | + max-width: 100%; |
| 95 | + max-height: 100%; |
| 96 | + } |
| 97 | +</style> |
0 commit comments