UNPKG

magnitude-core

Version:
153 lines (152 loc) 8 kB
import { retryOnErrorIsSuccess } from "@/common"; import logger from "@/logger"; export class CursorVisual { /** * Manages the visual indicator for actions on a page */ page; visualElementId = 'action-visual-indicator'; lastPosition = null; constructor() { //this.page = page; } async setActivePage(page) { this.page = page; page.on('load', async () => { await retryOnErrorIsSuccess(this.setupOnPage.bind(this), { mode: 'retry_all', delayMs: 200, retryLimit: 10 }); }); await retryOnErrorIsSuccess(this.setupOnPage.bind(this), { mode: 'retry_all', delayMs: 200, retryLimit: 5 }); } async move(x, y) { // Store the position this.lastPosition = { x, y }; // Create or update the mouse pointer visual, showing the click effect await this._drawVisual(x, y, false); // The pointer visual takes 0.3s on the transition, but awaiting script evaluation does not wait for this to complete. // So we wait 300ms manually. await this.page.waitForTimeout(300); } async setupOnPage() { if (this.lastPosition) { // Redraw the visual without the click effect await this._drawVisual(this.lastPosition.x, this.lastPosition.y, false); } } // Internal method to handle the actual drawing logic async _drawVisual(x, y, showClickEffect) { try { await this.page.evaluate(({ x, y, id, showClickEffect }) => { // Use viewport coordinates directly (no scroll adjustment for fixed positioning) const viewportX = x; const viewportY = y; // Document coordinates for the click effect circle (which uses absolute positioning) const docX = x + window.scrollX; const docY = y + window.scrollY; // --- Create Expanding/Fading Circle (Optional) --- if (showClickEffect) { const circle = document.createElement('div'); circle.style.position = 'absolute'; circle.style.left = `${docX}px`; circle.style.top = `${docY}px`; circle.style.borderRadius = '50%'; circle.style.backgroundColor = '#026aa1'; // Blue color circle.style.width = '0px'; circle.style.height = '0px'; circle.style.transform = 'translate(-50%, -50%)'; // Center on (x, y) circle.style.pointerEvents = 'none'; circle.style.zIndex = '9998'; // Below the pointer circle.style.opacity = '0.7'; // Initial opacity document.body.appendChild(circle); // Animate the circle const animation = circle.animate([ { width: '0px', height: '0px', opacity: 0.7 }, // Start state { width: '50px', height: '50px', opacity: 0 } // End state ], { duration: 500, // 500ms duration easing: 'ease-out' }); // Remove circle after animation animation.onfinish = () => { circle.remove(); }; } // --- Pointer Logic (Always runs) --- // Check if the visual indicator already exists let pointerElement = document.getElementById(id); // If it doesn't exist, create it with all necessary styling if (!pointerElement) { pointerElement = document.createElement('div'); pointerElement.id = id; pointerElement.style.position = 'fixed'; // Use fixed positioning for viewport-relative pointerElement.style.width = '32px'; pointerElement.style.height = '32px'; pointerElement.style.zIndex = '2147483647'; // Max z-index pointerElement.style.pointerEvents = 'none'; // Don't interfere with actual clicks // Notice that transition is 300ms pointerElement.style.transition = 'left 0.3s cubic-bezier(0.25, 0.1, 0.25, 1), top 0.3s cubic-bezier(0.25, 0.1, 0.25, 1)'; // Create SVG using DOM methods to avoid Trusted Types issues const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '32'); svg.setAttribute('height', '32'); svg.setAttribute('viewBox', '0 0 113.50408 99.837555'); const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); g.setAttribute('transform', 'translate(-413.10686,-501.19661)'); const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path1.setAttribute('style', 'fill:#026aa1;fill-opacity:1;stroke:#000000;stroke-width:0'); path1.setAttribute('d', 'm 416.1069,504.1966 52.47697,93.83813 8.33253,-57.61019 z'); const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path2.setAttribute('style', 'fill:#0384c7;fill-opacity:1;stroke:#000000;stroke-width:0'); path2.setAttribute('d', 'm 416.1069,504.1966 60.8095,36.22794 46.69517,-34.75524 z'); const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path3.setAttribute('style', 'fill:#0384c7;fill-opacity:0;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round'); path3.setAttribute('d', 'm 416.1069,504.19658 52.47698,93.83813 8.33252,-57.61019 46.69517,-34.75521 -107.50467,-1.47273'); g.appendChild(path1); g.appendChild(path2); g.appendChild(path3); svg.appendChild(g); pointerElement.appendChild(svg); document.body.appendChild(pointerElement); } //pointerElement.style.display = 'none'; // Update position - use viewport coordinates for fixed positioning // Set the top-left corner to (viewportX, viewportY) and then translate by (-1px, -3px) // to align the pointer tip (approx. at 1.27, 4.17 within the SVG) with the click point. pointerElement.style.left = `${viewportX}px`; pointerElement.style.top = `${viewportY}px`; pointerElement.style.transform = 'translate(-1px, -3px)'; }, { x, y, id: this.visualElementId, showClickEffect }); } catch (error) { // For example when: // TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment. logger.trace(`Failed to draw visual: ${error.message}`); } } async hide() { try { await this.page.evaluate((id) => { const element = document.getElementById(id); if (element) { element.style.display = 'none'; } }, this.visualElementId); } catch { logger.trace(`Failed to hide pointer`); } } async show() { try { await this.page.evaluate((id) => { const element = document.getElementById(id); if (element) { // Revert to the default display value (usually 'block' for a div) element.style.display = ''; } }, this.visualElementId); } catch { logger.trace(`Failed to show pointer`); } } }