Customer Banners (Ads) - SpiceUp. AX and SpotfireX Disclaimer



If you find this site useful and you want to support, buy me a coffee   to keep this site alive and without ads.

Zoom images

Add a magnifier to zoom images. The resolution of the magnifier depends on the original image's resolution.


  • Mouse wheel (no modifier): Fine zoom adjustment (±0.2x increments)
  • Ctrl + Mouse wheel: Integer zoom steps (±1x increments)
  • Shift + Mouse wheel: Lens size adjustment (±10px increments)
  • Alt + Mouse wheel: Lens roundness adjustment (±5% increments)
  • Ctrl + Shift + Mouse wheel: Toggle auto-hide lens behavior
  • Double-click lens: Export settings dialog

html

<img id="pcb "class="zoomable" src="60e80de9886443b1b4b61f7325a1e3a9.jpg" style="height: 160px;"/>


JavaScript

(()=>{
    class Magnifier {
      static lensCounter = 0;
      constructor(img) {
        this.img = img;
        this.img.style.cursor = 'crosshair';
        this.zoom = 2;
        this.lensSize = 120;
        this.lensRoundness = 50; // percent, 0=square, 50=circle
        this.dragging = false;
        this.dragOffset = {x:0, y:0};
        this.lastMouse = null;
        this.autoHideLens = true;
        this.tooltipTimeout = null;


        // Create container
        this.container = document.createElement('div');
        this.container.className = 'magnifier-container';
        img.parentNode.insertBefore(this.container, img);
        this.container.appendChild(img);


        // Create lens with unique id - use image id if available, otherwise generate one
        if (this.img.id) {
          this.lensId = `magnifier-lens-${this.img.id}`;
        } else {
          Magnifier.lensCounter += 1;
          this.lensId = `magnifier-lens-${Magnifier.lensCounter}`;
        }
        this.storageKey = `magnifier-${this.lensId}`; // Set storage key AFTER lensId is created
        this.lens = document.createElement('div');
        this.lens.className = 'magnifier-lens';
        this.lens.id = this.lensId;
        this.lens.style.width = this.lens.style.height = this.lensSize + 'px';
        this.lens.style.borderRadius = this.lensRoundness + '%';
        this.lens.style.opacity = 1;
        this.lens.style.pointerEvents = 'auto';
        this.lens.style.overflow = 'hidden';
       
        // Append lens to Spotfire visualization area instead of container
        const spotfireElement = document.querySelector(".sf-element.sf-element-visualization-area")?.firstElementChild;
        if (spotfireElement) {
          spotfireElement.appendChild(this.lens);
        } else {
          // Fallback to original container if Spotfire element not found
          this.container.appendChild(this.lens);
        }


        // Create canvas for lens
        this.lensCanvas = document.createElement('canvas');
        this.lensCanvas.width = this.lensSize;
        this.lensCanvas.height = this.lensSize;
        this.lensCanvas.style.width = this.lensSize + 'px';
        this.lensCanvas.style.height = this.lensSize + 'px';
        this.lensCanvas.style.borderRadius = this.lensRoundness + '%';
        this.lens.appendChild(this.lensCanvas);
        this.lensCtx = this.lensCanvas.getContext('2d');


        // Create tooltip
        this.tooltip = document.createElement('div');
        this.tooltip.className = 'zoom-tooltip';
        this.tooltip.textContent = '2x';
        this.lens.appendChild(this.tooltip);


        this.loadSettings(); // Load saved settings from localStorage BEFORE centering
        this.centerLens();
        this.updateZoomTooltip();
        this.showLens();
        // Set initial mouse position to image center for lens background
        this.lastMouse = {
          x: this.img.naturalWidth ? this.img.naturalWidth / 2 : this.img.width / 2,
          y: this.img.naturalHeight ? this.img.naturalHeight / 2 : this.img.height / 2
        };
        // Wait for image to load before updating lens background
        if (!this.img.complete) {
          this.img.addEventListener('load', () => {
            this.lastMouse = {
              x: this.img.naturalWidth / 2,
              y: this.img.naturalHeight / 2
            };
            this.centerLens();
            this.updateLensBackground();
          });
        } else {
          this.updateLensBackground();
        }
        this.addEvents();
      }


      centerLens() {
        // Only center if no position was loaded from localStorage
        if (!this.positionLoaded) {
          this.lens.style.left = (this.img.width / 2 - this.lensSize / 2) + 'px';
          this.lens.style.top = (this.img.height / 2 - this.lensSize / 2) + 'px';
        }
      }


      showLens() {
        this.lens.style.opacity = 1;
        this.lens.style.pointerEvents = 'auto';
        this.lens.style.display = 'block';
      }
      hideLens() {
        if (!this.autoHideLens) return;
        this.lens.style.opacity = 0;
        this.lens.style.pointerEvents = 'auto';
      }


      addEvents() {
        this.img.addEventListener('mouseenter', () => this.showLens());
        this.img.addEventListener('mouseleave', () => {
          if (!this.lens.matches(':hover')) this.hideLens();
        });
        this.lens.addEventListener('mouseenter', () => {
          this.showLens();
          this.tooltip.style.opacity = 1;
          // Clear tooltip timeout since mouse is over lens
          if (this.tooltipTimeout) {
            clearTimeout(this.tooltipTimeout);
            this.tooltipTimeout = null;
          }
        });
        this.lens.addEventListener('mouseleave', () => {
          this.tooltip.style.opacity = 0;
          if (!this.img.matches(':hover')) this.hideLens();
        });


        // Drag logic scoped to this instance
        this.lens.addEventListener('mousedown', (e) => {
          this.dragging = true;
          this.lens.style.cursor = 'grabbing';
          const lensRect = this.lens.getBoundingClientRect();
          this.dragOffset.x = e.clientX - lensRect.left;
          this.dragOffset.y = e.clientY - lensRect.top;
          document.body.style.userSelect = 'none';
          e.preventDefault();


          // Add mousemove and mouseup listeners for drag only while dragging
          const onMove = (ev) => {
            if (this.dragging) {
              const containerRect = this.container.getBoundingClientRect();
              let newLeft = ev.clientX - containerRect.left - this.dragOffset.x;
              let newTop = ev.clientY - containerRect.top - this.dragOffset.y;
              this.lens.style.left = newLeft + 'px';
              this.lens.style.top = newTop + 'px';
            }
          };
          const onUp = () => {
            this.dragging = false;
            this.lens.style.cursor = 'crosshair';
            document.body.style.userSelect = '';
            window.removeEventListener('mousemove', onMove);
            window.removeEventListener('mouseup', onUp);
            this.saveSettings(); // Save position after dragging
          };
          window.addEventListener('mousemove', onMove);
          window.addEventListener('mouseup', onUp);
        });


        this.lens.addEventListener('wheel', (e) => this.handleZoomWheel(e));
        this.img.addEventListener('wheel', (e) => this.handleZoomWheel(e));
        this.img.addEventListener('mousemove', (e) => this.handleMouseMove(e));
       
        // Double-click to show settings export dialog
        this.lens.addEventListener('dblclick', (e) => {
          e.preventDefault();
          e.stopPropagation();
          showSettingsDialog();
        });
      }


      updateZoomTooltip(mode) {
        if (mode === 'size') {
          this.tooltip.textContent = `Size: ${this.lensSize}px`;
        } else if (mode === 'round') {
          this.tooltip.textContent = `Round: ${this.lensRoundness}%`;
        } else if (mode === 'autoHide') {
          this.tooltip.textContent = `Auto-hide: ${this.autoHideLens ? 'ON' : 'OFF'}`;
        } else {
          if (Number.isInteger(this.zoom)) {
            this.tooltip.textContent = `${this.zoom}x`;
          } else {
            this.tooltip.textContent = `${this.zoom.toFixed(1)}x`;
          }
        }
       
        // Toast the tooltip when called from zoom/size/roundness/autoHide changes
        if (mode === 'size' || mode === 'round' || mode === 'autoHide' || mode === undefined) {
          this.toastTooltip();
        }
      }


      toastTooltip() {
        // Clear any existing timeout
        if (this.tooltipTimeout) {
          clearTimeout(this.tooltipTimeout);
        }
       
        // Show tooltip immediately
        this.tooltip.style.opacity = 1;
       
        // Hide after 1.5 seconds unless mouse is over lens
        this.tooltipTimeout = setTimeout(() => {
          if (!this.lens.matches(':hover')) {
            this.tooltip.style.opacity = 0;
          }
          this.tooltipTimeout = null;
        }, 1500);
      }


      loadSettings() {
        try {
          // First check for settings in the span tag
          const spanSettings = readSettingsFromSpan();
          let settings = null;
         
          // Get image ID for lookup
          const imageId = this.img.id || this.lensId.replace('magnifier-lens-', '');
         
          if (spanSettings && spanSettings[imageId]) {
            settings = spanSettings[imageId];
            console.log(`Loading settings for ${imageId} from span:`, settings);
          } else {
            // Fall back to localStorage
            const saved = localStorage.getItem(this.storageKey);
            if (saved) {
              settings = JSON.parse(saved);
              console.log(`Loading settings for ${imageId} from localStorage:`, settings);
            }
          }
         
          if (settings) {
            // Restore zoom level
            if (settings.zoom !== undefined) {
              this.zoom = Math.max(0.2, Math.min(20, settings.zoom));
            }
           
            // Restore lens size
            if (settings.lensSize !== undefined) {
              this.lensSize = Math.max(40, Math.min(400, settings.lensSize));
              this.lens.style.width = this.lens.style.height = this.lensSize + 'px';
              this.lensCanvas.width = this.lensCanvas.height = this.lensSize;
              this.lensCanvas.style.width = this.lensCanvas.style.height = this.lensSize + 'px';
            }
           
            // Restore roundness
            if (settings.lensRoundness !== undefined) {
              this.lensRoundness = Math.max(0, Math.min(50, settings.lensRoundness));
              this.lens.style.borderRadius = this.lensRoundness + '%';
              this.lensCanvas.style.borderRadius = this.lensRoundness + '%';
            }
           
            // Restore autoHideLens setting
            if (settings.autoHideLens !== undefined) {
              this.autoHideLens = Boolean(settings.autoHideLens);
            }
           
            // Restore position - flag that position was loaded to prevent centering
            if (settings.position && settings.position.x !== undefined && settings.position.y !== undefined) {
              this.lens.style.left = settings.position.x + 'px';
              this.lens.style.top = settings.position.y + 'px';
              this.positionLoaded = true; // Flag to prevent centering
            }
           
            // Update tooltip with loaded zoom
            this.updateZoomTooltip();
          }
        } catch (e) {
          console.warn('Failed to load magnifier settings:', e);
        }
      }


      saveSettings() {
        try {
          const settings = {
            zoom: this.zoom,
            lensSize: this.lensSize,
            lensRoundness: this.lensRoundness,
            autoHideLens: this.autoHideLens,
            position: {
              x: parseInt(this.lens.style.left) || 0,
              y: parseInt(this.lens.style.top) || 0
            }
          };
          localStorage.setItem(this.storageKey, JSON.stringify(settings));
        } catch (e) {
          console.warn('Failed to save magnifier settings:', e);
        }
      }


      handleZoomWheel(e) {
        e.preventDefault();
        e.stopPropagation();
        if (!this.dragging) {
          let updateLens = false;
          // Shift+wheel: lens size only
          if (e.shiftKey && !e.ctrlKey && !e.altKey) {
            if (e.deltaY < 0) {
              this.lensSize = Math.min(this.lensSize + 10, 400);
            } else {
              this.lensSize = Math.max(this.lensSize - 10, 40);
            }
            this.lens.style.width = this.lens.style.height = this.lensSize + 'px';
            this.lensCanvas.width = this.lensCanvas.height = this.lensSize;
            this.lensCanvas.style.width = this.lensCanvas.style.height = this.lensSize + 'px';
            // this.centerLens();
            this.updateZoomTooltip('size');
            updateLens = true;
            this.saveSettings(); // Save lens size change
          }
          // Alt+wheel: border-radius only
          if (e.altKey && !e.ctrlKey && !e.shiftKey) {
            this.lensRoundness = Math.max(0, Math.min(50, this.lensRoundness + (e.deltaY < 0 ? 5 : -5)));
            this.lens.style.borderRadius = this.lensRoundness + '%';
            this.lensCanvas.style.borderRadius = this.lensRoundness + '%';
            this.updateZoomTooltip('round');
            this.saveSettings(); // Save roundness change
          }
          // Ctrl+Shift+wheel: toggle autoHideLens
          if (e.ctrlKey && e.shiftKey && !e.altKey) {
            this.autoHideLens = !this.autoHideLens;
            this.updateZoomTooltip('autoHide');
            this.saveSettings(); // Save autoHideLens change
          }
          // Ctrl+wheel: zoom (integer)
          if (e.ctrlKey && !e.shiftKey && !e.altKey) {
            if (e.deltaY < 0) {
              this.zoom = Math.min(Math.round(this.zoom + 1), 20);
            } else {
              this.zoom = Math.max(Math.round(this.zoom - 1), 0);
            }
            this.updateZoomTooltip();
            updateLens = true;
            this.saveSettings(); // Save zoom change
          }
          // No modifier: zoom (fine)
          if (!e.ctrlKey && !e.shiftKey && !e.altKey) {
            if (e.deltaY < 0) {
              this.zoom = Math.min(this.zoom + 0.2, 10);
            } else {
              this.zoom = Math.max(this.zoom - 0.2, 0);
            }
            this.updateZoomTooltip();
            updateLens = true;
            this.saveSettings(); // Save zoom change
          }
          // Always update lens background if lens size or zoom changed
          if (updateLens && this.lastMouse) {
            this.updateLensBackground();
          }
        }
      }


      handleMouseMove(e) {
        if (!this.dragging) {
          const rect = this.img.getBoundingClientRect();
          // Mouse position relative to image (CSS pixels)
          const x_css = e.clientX - rect.left;
          const y_css = e.clientY - rect.top;
          // Map CSS pixel to natural image pixel
          const scaleX = this.img.naturalWidth / rect.width;
          const scaleY = this.img.naturalHeight / rect.height;
          const x = x_css * scaleX;
          const y = y_css * scaleY;
          this.lastMouse = { x, y };
          // Do NOT move lens position here; only update magnified region
          this.updateLensBackground();
        }
      }


      updateLensBackground() {
        if (this.lastMouse && !this.dragging) {
          // Clear lens canvas
          this.lensCtx.clearRect(0, 0, this.lensSize, this.lensSize);
          // Calculate source region in image so mouse is at center of lens
          const sw = this.lensSize / this.zoom;
          const sh = this.lensSize / this.zoom;
          let sx = this.lastMouse.x - sw / 2;
          let sy = this.lastMouse.y - sh / 2;
          // Clamp to image bounds
          sx = Math.max(0, Math.min(this.img.naturalWidth - sw, sx));
          sy = Math.max(0, Math.min(this.img.naturalHeight - sh, sy));
          // Draw zoomed region to lens canvas
          this.lensCtx.drawImage(
            this.img,
            sx, sy, sw, sh,
            0, 0, this.lensSize, this.lensSize
          );
        }
      }
    }


    // Function to read settings from the span or pre tag
    function readSettingsFromSpan() {
      const settingsElement = document.getElementById('zoomable-settings');
      if (settingsElement && settingsElement.innerHTML.trim()) {
        try {
          const content = settingsElement.innerHTML.trim();
          // Expected format: "zoomable-settings:{json}"
          if (content.startsWith('zoomable-settings:')) {
            const jsonStr = content.substring('zoomable-settings:'.length);
            return JSON.parse(jsonStr);
          }
        } catch (e) {
          console.warn('Failed to parse settings from element:', e);
        }
      }
      return null;
    }


    // Function to collect all current settings from all magnifiers
    function collectAllSettings() {
      const allSettings = {};
      document.querySelectorAll('.magnifier-lens').forEach(lens => {
        const lensId = lens.id;
        if (lensId.startsWith('magnifier-lens-')) {
          const imageId = lensId.replace('magnifier-lens-', '');
          const storageKey = `magnifier-${lensId}`;
          try {
            const saved = localStorage.getItem(storageKey);
            if (saved) {
              allSettings[imageId] = JSON.parse(saved);
            }
          } catch (e) {
            console.warn(`Failed to read settings for ${imageId}:`, e);
          }
        }
      });
      return allSettings;
    }


    // Function to show settings dialog
    function showSettingsDialog() {
      // Check if dialog is already open
      if (settingsDialog.style.display === 'block') {
        return; // Don't open multiple instances
      }
     
      const allSettings = collectAllSettings();
      const settingsJson = JSON.stringify(allSettings, null, 2);
      const htmlCode = `<pre id="zoomable-settings" hidden>\nzoomable-settings:${settingsJson}\n</pre>`;
     
      const textarea = settingsDialog.querySelector('#settings-output');
      textarea.value = htmlCode;
     
      // Show overlay and dialog
      modalOverlay.style.display = 'block';
      settingsDialog.style.display = 'block';
    }


    // Create modal overlay
    const modalOverlay = document.createElement('div');
    modalOverlay.className = 'modal-overlay';
    document.body.appendChild(modalOverlay);


    // Create global settings dialog (shared by all magnifiers)
    const settingsDialog = document.createElement('div');
    settingsDialog.className = 'settings-dialog';
    settingsDialog.innerHTML = `
      <button class="close-btn">&times;</button>
      <h3>Export Magnifier Settings</h3>
      <div class="message">Copy the code below and paste it into your HTML to preserve these magnifier settings:</div>
      <textarea id="settings-output" readonly></textarea>
      <button class="copy-btn">Copy to Clipboard</button>
    `;
    document.body.appendChild(settingsDialog);


    // Settings dialog event handlers
    function closeDialog() {
      settingsDialog.style.display = 'none';
      modalOverlay.style.display = 'none';
    }


    settingsDialog.querySelector('.close-btn').addEventListener('click', closeDialog);
    modalOverlay.addEventListener('click', closeDialog);
   
    // Prevent dialog from closing when clicking inside the dialog
    settingsDialog.addEventListener('click', (e) => {
      e.stopPropagation();
    });
    settingsDialog.querySelector('.copy-btn').addEventListener('click', () => {
      const textarea = settingsDialog.querySelector('#settings-output');
      textarea.select();
      document.execCommand('copy');
      const btn = settingsDialog.querySelector('.copy-btn');
      const originalText = btn.textContent;
      btn.textContent = 'Copied!';
      setTimeout(() => btn.textContent = originalText, 1000);
    });


    // Enhance all images with class 'zoomable'
    document.querySelectorAll('img.zoomable').forEach(img => {
      new Magnifier(img);
    });


    // Embed CSS as a string and inject next to the first zoomable image
    const magnifierCSS = `
      <style>
        .magnifier-container {
          position: relative;
          display: inline-block;
        }
        .magnifier-lens {
          position: absolute;
          border: 2px solid #333;
          border-radius: 50%;
          width: 120px;
          height: 120px;
          pointer-events: auto;
          display: none;
          box-shadow: 0 2px 8px rgba(0,0,0,0.2);
          background-repeat: no-repeat;
          background-size: 1200px 1200px;
          cursor: grab;
          overflow: hidden;
          opacity: 1;
          transition: opacity 2s;
        }
        .zoom-tooltip {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          background: rgba(0,0,0,0.7);
          color: #fff;
          padding: 2px 8px;
          border-radius: 8px;
          font-size: 16px;
          pointer-events: none;
          opacity: 0;
          transition: opacity 0.2s;
          z-index: 20;
          white-space: nowrap;
        }
        .settings-dialog {
          position: fixed;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          background: white;
          border: 2px solid #333;
          border-radius: 8px;
          box-shadow: 0 4px 20px rgba(0,0,0,0.3);
          z-index: 1000;
          max-width: 600px;
          width: 90%;
          padding: 20px;
          font-family: Arial, sans-serif;
          display: none;
        }
        .settings-dialog h3 {
          margin: 0 0 15px 0;
          color: #333;
          font-size: 18px;
        }
        .settings-dialog .close-btn {
          position: absolute;
          top: 10px;
          right: 15px;
          background: none;
          border: none;
          font-size: 24px;
          cursor: pointer;
          color: #666;
        }
        .settings-dialog .close-btn:hover {
          color: #333;
        }
        .settings-dialog .message {
          margin-bottom: 15px;
          color: #666;
          font-size: 14px;
        }
        .settings-dialog textarea {
          width: 100%;
          height: 150px;
          font-family: 'Courier New', monospace;
          font-size: 12px;
          border: 1px solid #ccc;
          border-radius: 4px;
          padding: 10px;
          resize: vertical;
          background: #f9f9f9;
        }
        .settings-dialog .copy-btn {
          margin: 10px;
          padding: 8px 16px;
          background: #007acc;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          font-size: 14px;
        }
        .settings-dialog .copy-btn:hover {
          background: #005a9e;
        }
        .modal-overlay {
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background: rgba(0, 0, 0, 0.5);
          z-index: 999;
          display: none;
        }
      </style>
    `;
    const firstZoomable = document.querySelector('img.zoomable');
    if (firstZoomable) {
      firstZoomable.insertAdjacentHTML('beforebegin', magnifierCSS);
    }

})()

No comments: