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 `<magic-magnifier>` 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?
This commit is contained in:
Emi Matchu 2024-12-07 11:39:37 -08:00
parent b3f3b39aa0
commit b2a23b3e7b
6 changed files with 91 additions and 5 deletions

View file

@ -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);

View file

@ -13,22 +13,26 @@ class SupportOutfitViewer extends HTMLElement {
if (!e.target.matches("tr")) return; if (!e.target.matches("tr")) return;
const id = e.target.querySelector("[data-field=id]").innerText; 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)}"]`, `outfit-viewer [data-asset-id="${CSS.escape(id)}"]`,
); );
for (const layer of layers) {
layer.setAttribute("highlighted", ""); layer.setAttribute("highlighted", "");
} }
}
// When a row is unhovered, unhighlight its corresponding outfit viewer layer. // When a row is unhovered, unhighlight its corresponding outfit viewer layer.
#onMouseLeave(e) { #onMouseLeave(e) {
if (!e.target.matches("tr")) return; if (!e.target.matches("tr")) return;
const id = e.target.querySelector("[data-field=id]").innerText; 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)}"]`, `outfit-viewer [data-asset-id="${CSS.escape(id)}"]`,
); );
for (const layer of layers) {
layer.removeAttribute("highlighted"); layer.removeAttribute("highlighted");
} }
}
// When clicking a row, redirect the click to the first link. // When clicking a row, redirect the click to the first link.
#onClick(e) { #onClick(e) {

View file

@ -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 cornerthis 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

View file

@ -37,3 +37,8 @@ support-outfit-viewer
cursor: zoom-in cursor: zoom-in
&:hover &:hover
background: $module-bg-color background: $module-bg-color
magic-magnifier
--magic-magnifier-lens-width: 100px
--magic-magnifier-lens-height: 100px
--magic-magnifier-scale: 2.5

View file

@ -1,5 +1,7 @@
= content_tag "support-outfit-viewer", **html_options do = content_tag "support-outfit-viewer", **html_options do
%magic-magnifier
= outfit_viewer outfit = outfit_viewer outfit
%table %table
%thead %thead
%tr %tr

View file

@ -45,11 +45,13 @@
- content_for :stylesheets do - content_for :stylesheets do
= stylesheet_link_tag "application/breadcrumbs" = stylesheet_link_tag "application/breadcrumbs"
= stylesheet_link_tag "application/magic-magnifier"
= stylesheet_link_tag "application/outfit-viewer" = stylesheet_link_tag "application/outfit-viewer"
= stylesheet_link_tag "application/support-form" = stylesheet_link_tag "application/support-form"
= stylesheet_link_tag "pet_states/support-outfit-viewer" = stylesheet_link_tag "pet_states/support-outfit-viewer"
= page_stylesheet_link_tag "pet_states/edit" = page_stylesheet_link_tag "pet_states/edit"
- content_for :javascripts do - content_for :javascripts do
= javascript_include_tag "magic-magnifier"
= javascript_include_tag "outfit-viewer" = javascript_include_tag "outfit-viewer"
= javascript_include_tag "pet_states/support-outfit-viewer" = javascript_include_tag "pet_states/support-outfit-viewer"