Media

ZoomImage

A picture that zooms in where the pointer is; a click holds it, and the keyboard zooms and moves around too.

#media#image#commerce#interactive

Demo

Preview

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/zoom_image.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/zoom_image.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/zoom_image.qwe
// A woven pattern drawn in SVG, fine enough to be worth zooming, so the demo needs no image files.fn zoom_image_demo_weave(a: str, b: str) -> str {    let cells = join(for i in range(0, 20) => join(for j in range(0, 15) =>        `<rect x="${i * 40}" y="${j * 40}" width="40" height="40" fill="${(i + j) % 2 == 0 ? a : b}"/><circle cx="${i * 40 + 20}" cy="${j * 40 + 20}" r="4" fill="#fff" opacity=".6"/>`, ""), "")    let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600">${cells}`        + `<text x="400" y="320" font-family="Georgia, serif" font-size="14" text-anchor="middle" fill="#fff">Made in Porto · 100% linen</text></svg>`    `data:image/svg+xml,${url_encode(svg)}`}view ZoomImageDemo() =    <div.demo-row style="align-items: flex-start">        <div style="width: 360px; max-width: 100%">            <ZoomImage src={zoom_image_demo_weave("#5b4bdb", "#3f8f8a")} alt="Linen fabric with a small label"/>        </div>        <div style="width: 240px; max-width: 100%">            <ZoomImage src={zoom_image_demo_weave("#c62a2f", "#a35200")} alt="Red fabric, square" zoom={4} ratio="1"                       large="https://picsum.photos/seed/weave/1200/1200" hint="Zoom ×4"/>        </div>    </div>

Source

ui/zoom_image.qwe
// ZoomImage: a picture that zooms in where the pointer is while it rests on it,// to see the weave of a fabric or the print on a label. A click (or a tap) holds// the zoom on and off; with the keyboard, Enter zooms, the arrow keys move// around and Escape zooms out.////   <ZoomImage src="/img/sofa.jpg" alt="The sofa's fabric"/>//   <ZoomImage src={product.image} alt={product.name} zoom={3} ratio="1"/>//// src, alt: the picture. zoom: how much it grows (2.5). ratio: the frame's shape// ("4/3"); the picture fills it. large: a bigger picture to show while zoomed// (src by default), loaded only when it is first wanted. hint: the line shown// in a corner ("Hover to zoom"); null hides it.import ui/theme.qwe use ui_clslet zoom_image_css = css`.ui-zoom-image {  position: relative; display: block; overflow: hidden; width: 100%; aspect-ratio: var(--r, 4/3); border-radius: var(--ui-radius);  background: var(--ui-subtle); cursor: zoom-in; outline: none; font: 12px/1 var(--ui-font);}.ui-zoom-image:focus-visible { box-shadow: var(--ui-focus); }.ui-zoom-image img {  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transform-origin: var(--x, 50%) var(--y, 50%);  transition: transform .2s ease-out; pointer-events: none; -webkit-user-drag: none;}.ui-zoom-image-on { cursor: zoom-out; }.ui-zoom-image-on img { transform: scale(var(--z, 2.5)); transition: transform .2s ease-out, transform-origin .05s linear; }.ui-zoom-image-hint {  position: absolute; right: 10px; bottom: 10px; display: inline-flex; align-items: center; gap: 6px; padding: 6px 9px;  border-radius: 999px; background: rgba(0, 0, 0, .55); color: #fff; pointer-events: none; transition: opacity .2s;}.ui-zoom-image-hint svg { width: 14px; height: 14px; }.ui-zoom-image-on .ui-zoom-image-hint { opacity: 0; }@media (prefers-reduced-motion: reduce) { .ui-zoom-image img, .ui-zoom-image-on img, .ui-zoom-image-hint { transition: none; } }`component ZoomImage(src: str, alt: str = "", zoom: num = 2.5, ratio: str = "4/3", large: str? = null,                    hint: str? = "Hover to zoom", class: str? = null) {    style global zoom_image_css    state on = false    state held = false    state x = 50    state y = 50    state wanted = false    let shown = wanted && large != null ? large : src    let clamp = (v: num) => v < 0 ? 0 : v > 100 ? 100 : v    let follow = (e) => {        let r = e.target.getBoundingClientRect()        x = clamp(round((e.clientX - r.left) * 100 / r.width, 2))        y = clamp(round((e.clientY - r.top) * 100 / r.height, 2))    }    <div class={ui_cls(["ui-zoom-image", on && "ui-zoom-image-on", class])} style={`--r: ${ratio}; --z: ${zoom}; --x: ${x}%; --y: ${y}%`}         role="img" aria-label={alt} tabindex="0"         aria-description={on ? `Zoomed ${zoom} times. Arrow keys move, Escape zooms out.` : "Press Enter to zoom."}         @pointerenter={(e) => { if e.pointerType == "mouse" { wanted = true; on = true; follow(e) } }}         @pointermove={(e) => { if on { follow(e) } }}         @pointerleave={(e) => { if e.pointerType == "mouse" && !held { on = false } }}         @click={(e) => {             wanted = true             held = !held || !on             on = held             if on { follow(e) }         }}         @keydown={(e) => {             let step = 10             let done = match e.key {                 "Enter" | " " => { wanted = true; on = !on; held = on; x = 50; y = 50; true }                 "Escape" => { if on { on = false; held = false; true } else { false } }                 "ArrowLeft" => { if on { x = clamp(x - step) }; on }                 "ArrowRight" => { if on { x = clamp(x + step) }; on }                 "ArrowUp" => { if on { y = clamp(y - step) }; on }                 "ArrowDown" => { if on { y = clamp(y + step) }; on }                 _ => false             }             if done { e.preventDefault() }         }}>        <img src={shown} alt="" draggable="false"/>        {hint != null && <span.ui-zoom-image-hint aria-hidden="true">            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                 stroke-linejoin="round"><path d="M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM21 21l-4.35-4.35M11 8v6M8 11h6"/></svg>            {hint}        </span>}    </div>}
Thread
64Useful? Like it, or add to its thread below.

Thread 0 posts

On ZoomImage: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.