From b2a23b3e7bb4878bc471b05ccb8b7394fad61747 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Sat, 7 Dec 2024 11:39:37 -0800 Subject: [PATCH] Add magnification when editing pet appearance I couldn't find a library for this functionality that didn't require jQuery, and I don't want to be adding *more* jQuery requirements. So, I decided to throw together my own! The `` component copies its contents into a "lens" element, then uses basic JS to track mouse position, then uses CSS to move the lens and its contents into a helpful position. One thing I noticed here is that the zoom is a bit crunchy because we're using PNG images, and it's hard to zoom in even further than we already are. I might try switching this UI to use the SVG images by default instead? --- app/assets/javascripts/magic-magnifier.js | 33 +++++++++++++++ .../pet_states/support-outfit-viewer.js | 12 ++++-- .../application/magic-magnifier.sass | 40 +++++++++++++++++++ .../pet_states/support-outfit-viewer.sass | 5 +++ .../_support_outfit_viewer.html.haml | 4 +- app/views/pet_states/edit.html.haml | 2 + 6 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/magic-magnifier.js create mode 100644 app/assets/stylesheets/application/magic-magnifier.sass diff --git a/app/assets/javascripts/magic-magnifier.js b/app/assets/javascripts/magic-magnifier.js new file mode 100644 index 00000000..2d3ed82f --- /dev/null +++ b/app/assets/javascripts/magic-magnifier.js @@ -0,0 +1,33 @@ +class MagicMagnifier extends HTMLElement { + connectedCallback() { + setTimeout(() => this.#attachLens(), 0); + this.addEventListener("mousemove", this.#onMouseMove); + } + + #attachLens() { + const lens = document.createElement("magic-magnifier-lens"); + lens.inert = true; + lens.useContent(this.children); + this.appendChild(lens); + } + + #onMouseMove(e) { + const lens = this.querySelector("magic-magnifier-lens"); + const rect = this.getBoundingClientRect(); + const x = e.pageX - rect.left; + const y = e.pageY - rect.top; + lens.style.setProperty("--magic-magnifier-x", x + "px"); + lens.style.setProperty("--magic-magnifier-y", y + "px"); + } +} + +class MagicMagnifierLens extends HTMLElement { + useContent(contentNodes) { + for (const contentNode of contentNodes) { + this.appendChild(contentNode.cloneNode(true)); + } + } +} + +customElements.define("magic-magnifier", MagicMagnifier); +customElements.define("magic-magnifier-lens", MagicMagnifierLens); diff --git a/app/assets/javascripts/pet_states/support-outfit-viewer.js b/app/assets/javascripts/pet_states/support-outfit-viewer.js index 07211ee8..b8d4798f 100644 --- a/app/assets/javascripts/pet_states/support-outfit-viewer.js +++ b/app/assets/javascripts/pet_states/support-outfit-viewer.js @@ -13,10 +13,12 @@ class SupportOutfitViewer extends HTMLElement { if (!e.target.matches("tr")) return; const id = e.target.querySelector("[data-field=id]").innerText; - const layer = this.querySelector( + const layers = this.querySelectorAll( `outfit-viewer [data-asset-id="${CSS.escape(id)}"]`, ); - layer.setAttribute("highlighted", ""); + for (const layer of layers) { + layer.setAttribute("highlighted", ""); + } } // When a row is unhovered, unhighlight its corresponding outfit viewer layer. @@ -24,10 +26,12 @@ class SupportOutfitViewer extends HTMLElement { if (!e.target.matches("tr")) return; const id = e.target.querySelector("[data-field=id]").innerText; - const layer = this.querySelector( + const layers = this.querySelectorAll( `outfit-viewer [data-asset-id="${CSS.escape(id)}"]`, ); - layer.removeAttribute("highlighted"); + for (const layer of layers) { + layer.removeAttribute("highlighted"); + } } // When clicking a row, redirect the click to the first link. diff --git a/app/assets/stylesheets/application/magic-magnifier.sass b/app/assets/stylesheets/application/magic-magnifier.sass new file mode 100644 index 00000000..79752e76 --- /dev/null +++ b/app/assets/stylesheets/application/magic-magnifier.sass @@ -0,0 +1,40 @@ +magic-magnifier + position: relative + + &:not(:hover) + magic-magnifier-lens + display: none + +magic-magnifier-lens + width: var(--magic-magnifier-lens-width, 100px) + height: var(--magic-magnifier-lens-height, 100px) + overflow: hidden + border-radius: 100% + + background: white + border: 2px solid black + box-shadow: 3px 3px 3px rgba(0, 0, 0, .5) + + position: absolute + left: var(--magic-magnifier-x, 0px) + top: var(--magic-magnifier-y, 0px) + + > * + // Translations are applied in the opposite of the order they're specified. + // So, here's what we're doing: + // + // 1. Translate the content left by --magic-magnifier-x and up by + // --magic-magnifier-y, to align the target location with the lens's + // top-right corner. + // 2. Zoom in by --magic-magnifier-scale. + // 3. Translate the content right by half of --magic-magnifier-lens-width, + // and down by half of --magic-magnifier-lens-height, to align the + // target location with the lens's center. + // + // Note that it *is* possible to specify transforms relative to the center, + // rather than the top-left corner—this is in fact the default!—but that + // gets confusing fast with scale in play. I think this is easier to reason + // about with the top-left corner in terms of math, and center it after the + // fact. + transform: translateX(calc(var(--magic-magnifier-lens-width, 100px) / 2)) translateY(calc(var(--magic-magnifier-lens-height, 100px) / 2)) scale(var(--magic-magnifier-scale, 2)) translateX(calc(-1 * var(--magic-magnifier-x, 0px))) translateY(calc(-1 * var(--magic-magnifier-y, 0px))) + transform-origin: left top diff --git a/app/assets/stylesheets/pet_states/support-outfit-viewer.sass b/app/assets/stylesheets/pet_states/support-outfit-viewer.sass index 4624ada6..9184a86b 100644 --- a/app/assets/stylesheets/pet_states/support-outfit-viewer.sass +++ b/app/assets/stylesheets/pet_states/support-outfit-viewer.sass @@ -37,3 +37,8 @@ support-outfit-viewer cursor: zoom-in &:hover background: $module-bg-color + + magic-magnifier + --magic-magnifier-lens-width: 100px + --magic-magnifier-lens-height: 100px + --magic-magnifier-scale: 2.5 diff --git a/app/views/pet_states/_support_outfit_viewer.html.haml b/app/views/pet_states/_support_outfit_viewer.html.haml index e9df8126..9bcb935e 100644 --- a/app/views/pet_states/_support_outfit_viewer.html.haml +++ b/app/views/pet_states/_support_outfit_viewer.html.haml @@ -1,5 +1,7 @@ = content_tag "support-outfit-viewer", **html_options do - = outfit_viewer outfit + %magic-magnifier + = outfit_viewer outfit + %table %thead %tr diff --git a/app/views/pet_states/edit.html.haml b/app/views/pet_states/edit.html.haml index a118bf82..fc55f6aa 100644 --- a/app/views/pet_states/edit.html.haml +++ b/app/views/pet_states/edit.html.haml @@ -45,11 +45,13 @@ - content_for :stylesheets do = stylesheet_link_tag "application/breadcrumbs" + = stylesheet_link_tag "application/magic-magnifier" = stylesheet_link_tag "application/outfit-viewer" = stylesheet_link_tag "application/support-form" = stylesheet_link_tag "pet_states/support-outfit-viewer" = page_stylesheet_link_tag "pet_states/edit" - content_for :javascripts do + = javascript_include_tag "magic-magnifier" = javascript_include_tag "outfit-viewer" = javascript_include_tag "pet_states/support-outfit-viewer"