Media

ImageCompare

Before and after pictures one over the other, with a divider to drag (or move with the keys) between them.

#media#image#drag#interactive

Demo

Preview
A cottage on a hill (Night)A cottage on a hill (Day)

The divider is at 50%.

The hill, before and after the rain (after)The hill, before and after the rain (before)

Drag anywhere on a picture, or Tab to it and use the arrow keys.

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/image_compare.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/image_compare.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/image_compare.qwe
// The same scene drawn twice in SVG, by day and by night, so the demo needs no image files.fn image_compare_demo_scene(sky: str, sun: str, hill: str, lit: bool) -> str {    let windows = lit ? `<rect x="540" y="330" width="24" height="24" fill="#ffd36b"/><rect x="590" y="330" width="24" height="24" fill="#ffd36b"/>` : ""    let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 675" width="1200" height="675">`        + `<rect width="1200" height="675" fill="${sky}"/><circle cx="900" cy="170" r="70" fill="${sun}"/>`        + `<path d="M0 470 Q 300 330 600 450 T 1200 420 V 675 H 0 Z" fill="${hill}"/>`        + `<rect x="520" y="300" width="120" height="110" fill="#8a5a3c"/><path d="M505 305 L580 240 L655 305 Z" fill="#5b3a2a"/>${windows}</svg>`    `data:image/svg+xml,${url_encode(svg)}`}component ImageCompareDemoLive() {    state split = 50    <div.demo-stack>        <ImageCompare before={image_compare_demo_scene("#8fc8f2", "#ffd36b", "#4f8f4a", false)}                      after={image_compare_demo_scene("#16203f", "#f5f0d0", "#1f3326", true)}                      alt="A cottage on a hill" before_label="Day" after_label="Night"                      on_change={(p) => { split = p }}/>        <p.demo-note id="image-compare-demo-note">The divider is at {round(split)}%.</p>    </div>}view ImageCompareDemo() =    <div.demo-stack>        <ImageCompareDemoLive/>        <div style="max-width: 360px">            <ImageCompare before={image_compare_demo_scene("#f3a46b", "#ff6b4a", "#7a5a3a", false)}                          after={image_compare_demo_scene("#f3a46b", "#ff6b4a", "#3f7a3a", false)}                          alt="The hill, before and after the rain" at={25} ratio="4/3" before_label={null} after_label={null}/>        </div>        <p.demo-note>Drag anywhere on a picture, or Tab to it and use the arrow keys.</p>    </div>

Source

ui/image_compare.qwe
// ImageCompare: two pictures of the same thing, before and after, one over the// other, with a divider to drag across to show more of one or the other. The// divider is a real range input stretched over the pictures, so a click or a// drag anywhere moves it, and the arrow keys, Home and End do too.////   <ImageCompare before="/img/old-kitchen.jpg" after="/img/new-kitchen.jpg" alt="The kitchen"/>//   <ImageCompare before={raw_photo} after={edited} before_label="Original" after_label="Edited"//                 at={30} ratio="4/3" on_change={(pct) => { split = pct }}/>//// before, after: the two pictures (the same size and framing); before shows at// the left of the divider, after at the right. alt: what they show; each// picture's alt is it with its label. before_label, after_label: the tags in// the corners ("Before", "After"); null hides them. at: where the divider// starts, in percent from the left (50). ratio: the pictures' shape ("16/9").// label: what screen readers call the divider ("Divider between before and after").// on_change(pct): inside one of your components, called with the divider's place.import ui/theme.qwe use ui_clslet image_compare_css = css`.ui-image-compare {  position: relative; overflow: hidden; width: 100%; aspect-ratio: var(--r, 16/9); border-radius: var(--ui-radius);  background: var(--ui-subtle); font: 600 12px/1 var(--ui-font); user-select: none; touch-action: pan-y;}.ui-image-compare img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; pointer-events: none; -webkit-user-drag: none; }.ui-image-compare-before { clip-path: inset(0 calc(100% - var(--at, 50%)) 0 0); }.ui-image-compare-line { position: absolute; top: 0; bottom: 0; left: var(--at, 50%); width: 2px; margin-left: -1px; background: #fff; box-shadow: 0 0 4px rgba(0, 0, 0, .4); pointer-events: none; }.ui-image-compare-knob {  position: absolute; top: 50%; left: 50%; display: grid; place-items: center; width: 40px; height: 40px; transform: translate(-50%, -50%);  border: 2px solid #fff; border-radius: 50%; background: rgba(0, 0, 0, .45); color: #fff; box-shadow: var(--ui-shadow);}.ui-image-compare-knob svg { width: 22px; height: 22px; }.ui-image-compare-tag { position: absolute; top: 12px; padding: 5px 9px; border-radius: 999px; background: rgba(0, 0, 0, .55); color: #fff; letter-spacing: .04em; text-transform: uppercase; pointer-events: none; }.ui-image-compare-tag-before { left: 12px; }.ui-image-compare-tag-after { right: 12px; }.ui-image-compare input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: ew-resize; appearance: none; }.ui-image-compare input::-webkit-slider-thumb { appearance: none; width: 44px; height: 100vh; }.ui-image-compare:has(input:focus-visible) .ui-image-compare-knob { box-shadow: 0 0 0 3px #fff, 0 0 0 6px var(--ui-accent); }`component ImageCompare(before: str, after: str, alt: str = "", before_label: str? = "Before", after_label: str? = "After",                       at: num = 50, ratio: str = "16/9", label: str = "Divider between before and after", on_change = null,                       class: str? = null) {    style global image_compare_css    state pos = at < 0 ? 0 : at > 100 ? 100 : at    let name = (tag: str?, fallback: str) => alt != "" ? `${alt} (${tag ?? fallback})` : ""    <div class={ui_cls(["ui-image-compare", class])} style={`--r: ${ratio}; --at: ${pos}%`}>        <img.ui-image-compare-after src={after} alt={name(after_label, "after")} draggable="false"/>        <img.ui-image-compare-before src={before} alt={name(before_label, "before")} draggable="false"/>        {before_label != null && <span.ui-image-compare-tag.ui-image-compare-tag-before aria-hidden="true">{before_label}</span>}        {after_label != null && <span.ui-image-compare-tag.ui-image-compare-tag-after aria-hidden="true">{after_label}</span>}        <div.ui-image-compare-line aria-hidden="true">            <span.ui-image-compare-knob>                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                     stroke-linejoin="round"><path d="m9 6-6 6 6 6M15 6l6 6-6 6"/></svg>            </span>        </div>        <input type="range" min="0" max="100" step="0.5" value={pos} aria-label={label}               aria-valuetext={`${round(pos)}% ${before_label ?? "before"}, ${round(100 - pos)}% ${after_label ?? "after"}`}               @input={(e) => {                   pos = float(e.value)                   on_change && on_change(pos)               }}/>    </div>}
Thread
69Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.