Enhance SVG viewer with pan feature

This commit is contained in:
ema
2025-07-01 02:11:31 +08:00
parent 682801a8bb
commit 2a94fa155d

View File

@@ -1,43 +1,68 @@
class SvgViewer { // SvgViewer: Provides SVG preview with the following features:
// - Fit SVG to window with no upscaling
// - Mouse wheel zoom in/out (with smooth animation)
// - Double-click to fit SVG to window
// - Mouse drag to pan (only when SVG is larger than the container)
// - Pan is limited to visible overflow area
// - Handles SVGs with or without width/height/viewBox attributes
// - Resets pan on zoom or fit
// - No animation during pan, smooth animation during zoom/fit
class SvgViewer {
constructor() { constructor() {
// Initial scale and scale limits
this.scale = 1; this.scale = 1;
this.minScale = 0.1; this.minScale = 0.1;
this.maxScale = 10; this.maxScale = 10;
this.scaleStep = 1.2; this.scaleStep = 1.2;
this.baseScale = 1; this.baseScale = 1;
// SVG viewBox dimensions
this.viewBoxWidth = null; this.viewBoxWidth = null;
this.viewBoxHeight = null; this.viewBoxHeight = null;
this.svgElement = null; this.svgElement = null;
this.wrapper = document.getElementById('svgWrapper'); this.wrapper = document.getElementById('svgWrapper');
this.transitionEnabled = false; this.transitionEnabled = false;
// Offset for panning
this.offsetX = 0;
this.offsetY = 0;
// Drag state
this.isDragging = false;
this.lastMouseX = 0;
this.lastMouseY = 0;
} }
// Ensure SVG has proper size attributes and extract viewBox dimensions
fixSvgSize(svgElement) { fixSvgSize(svgElement) {
let widthAttr = svgElement.getAttribute('width'); let widthAttr = svgElement.getAttribute('width');
let heightAttr = svgElement.getAttribute('height'); let heightAttr = svgElement.getAttribute('height');
let viewBox = svgElement.getAttribute('viewBox'); let viewBox = svgElement.getAttribute('viewBox');
let viewBoxWidth = null, viewBoxHeight = null; let viewBoxWidth = null, viewBoxHeight = null;
// If viewBox is missing but width/height exist, set viewBox
if (!viewBox && widthAttr && heightAttr && widthAttr.trim() !== '' && heightAttr.trim() !== '') { if (!viewBox && widthAttr && heightAttr && widthAttr.trim() !== '' && heightAttr.trim() !== '') {
svgElement.setAttribute('viewBox', `0 0 ${widthAttr} ${heightAttr}`); svgElement.setAttribute('viewBox', `0 0 ${widthAttr} ${heightAttr}`);
viewBox = svgElement.getAttribute('viewBox'); viewBox = svgElement.getAttribute('viewBox');
} }
// Parse viewBox dimensions
if (viewBox) { if (viewBox) {
const vb = viewBox.split(/\s+/); const vb = viewBox.split(/\s+/);
if (vb.length === 4) { if (vb.length === 4) {
viewBoxWidth = parseFloat(vb[2]); viewBoxWidth = parseFloat(vb[2]);
viewBoxHeight = parseFloat(vb[3]); viewBoxHeight = parseFloat(vb[3]);
} }
} }
// If width or height is missing or empty, try to set from viewBox // If width or height is missing, set from viewBox if possible
if (!widthAttr || widthAttr.trim() === '' || !heightAttr || heightAttr.trim() === '') { if (!widthAttr || widthAttr.trim() === '' || !heightAttr || heightAttr.trim() === '') {
if (viewBoxWidth && viewBoxHeight) { if (viewBoxWidth && viewBoxHeight) {
svgElement.setAttribute('width', viewBoxWidth.toString()); svgElement.setAttribute('width', viewBoxWidth.toString());
svgElement.setAttribute('height', viewBoxHeight.toString()); svgElement.setAttribute('height', viewBoxHeight.toString());
} else { } else {
// fallback: if viewBox is missing or zero, use 100vw // Fallback: use 100vw if no size info
svgElement.style.width = '100vw'; svgElement.style.width = '100vw';
svgElement.style.height = '100vw'; svgElement.style.height = '100vw';
} }
@@ -46,33 +71,62 @@
this.viewBoxHeight = viewBoxHeight; this.viewBoxHeight = viewBoxHeight;
} }
// Calculate the base scale to fit SVG into the container
computeBaseScale() { computeBaseScale() {
if (!this.viewBoxWidth || !this.viewBoxHeight) return 1; if (!this.viewBoxWidth || !this.viewBoxHeight) return 1;
const wrapperRect = document.getElementById('svgContainer').getBoundingClientRect(); const wrapperRect = document.getElementById('svgContainer').getBoundingClientRect();
const scaleX = wrapperRect.width / this.viewBoxWidth; const scaleX = wrapperRect.width / this.viewBoxWidth;
const scaleY = wrapperRect.height / this.viewBoxHeight; const scaleY = wrapperRect.height / this.viewBoxHeight;
return Math.min(scaleX, scaleY, 1); // never upscale by default
return Math.min(scaleX, scaleY, 1); // Never upscale by default
} }
// Update SVG transform for scale and pan
updateTransform() { updateTransform() {
this.svgElement.style.transform = `scale(${this.scale})`; // Calculate actual SVG display size
const container = document.getElementById('svgContainer');
const containerRect = container.getBoundingClientRect();
const svgWidth = this.viewBoxWidth * this.scale;
const svgHeight = this.viewBoxHeight * this.scale;
// Limit pan offset to visible area
let maxOffsetX = Math.max(0, (svgWidth - containerRect.width) / 2);
let maxOffsetY = Math.max(0, (svgHeight - containerRect.height) / 2);
this.offsetX = Math.max(-maxOffsetX, Math.min(this.offsetX, maxOffsetX));
this.offsetY = Math.max(-maxOffsetY, Math.min(this.offsetY, maxOffsetY));
// Only allow pan if SVG is larger than container
if (svgWidth <= containerRect.width) this.offsetX = 0;
if (svgHeight <= containerRect.height) this.offsetY = 0;
this.svgElement.style.transform = `scale(${this.scale}) translate(${this.offsetX / this.scale}px, ${this.offsetY / this.scale}px)`;
this.svgElement.style.transformOrigin = 'center center'; this.svgElement.style.transformOrigin = 'center center';
} }
// Enable transform transition (for zoom)
enableTransition() { enableTransition() {
if (!this.transitionEnabled) {
this.svgElement.style.transition = 'transform 0.1s ease-out'; this.svgElement.style.transition = 'transform 0.1s ease-out';
this.transitionEnabled = true; this.transitionEnabled = true;
} }
// Disable transform transition (for pan)
disableTransition() {
this.svgElement.style.transition = '';
this.transitionEnabled = false;
} }
// Fit SVG to window and reset pan
fitToWindow() { fitToWindow() {
this.baseScale = this.computeBaseScale(); this.baseScale = this.computeBaseScale();
this.scale = this.baseScale; this.scale = this.baseScale;
this.offsetX = 0;
this.offsetY = 0;
this.updateTransform(); this.updateTransform();
} }
// Bind mouse and wheel events for zoom and pan
bindEvents() { bindEvents() {
// Zoom with mouse wheel
this.wrapper.addEventListener("wheel", (e) => { this.wrapper.addEventListener("wheel", (e) => {
this.enableTransition(); this.enableTransition();
e.preventDefault(); e.preventDefault();
@@ -81,14 +135,73 @@
} else { } else {
this.scale = Math.max(this.minScale, this.scale / this.scaleStep); this.scale = Math.max(this.minScale, this.scale / this.scaleStep);
} }
// Reset pan on zoom
this.offsetX = 0;
this.offsetY = 0;
this.updateTransform(); this.updateTransform();
}, { passive: false }); }, { passive: false });
// Double click to fit
this.wrapper.addEventListener('dblclick', () => { this.wrapper.addEventListener('dblclick', () => {
this.enableTransition(); this.enableTransition();
this.fitToWindow(); this.fitToWindow();
}); });
// Start pan on mouse down
this.wrapper.addEventListener('mousedown', (e) => {
// Only left mouse button
if (e.button !== 0) return;
// Only allow pan if SVG is larger than container
const container = document.getElementById('svgContainer');
const containerRect = container.getBoundingClientRect();
const svgWidth = this.viewBoxWidth * this.scale;
const svgHeight = this.viewBoxHeight * this.scale;
if (svgWidth > containerRect.width || svgHeight > containerRect.height) {
this.isDragging = true;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
document.body.style.cursor = 'grab';
this.disableTransition(); // Disable animation while panning
}
});
// Pan on mouse move
window.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
const container = document.getElementById('svgContainer');
const containerRect = container.getBoundingClientRect();
const svgWidth = this.viewBoxWidth * this.scale;
const svgHeight = this.viewBoxHeight * this.scale;
// Only allow pan if SVG is larger than container
if (svgWidth > containerRect.width || svgHeight > containerRect.height) {
let dx = e.clientX - this.lastMouseX;
let dy = e.clientY - this.lastMouseY;
// Only allow pan in directions where SVG is larger
if (svgWidth > containerRect.width) {
this.offsetX += dx;
}
if (svgHeight > containerRect.height) {
this.offsetY += dy;
}
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
this.updateTransform();
}
});
// End pan on mouse up
window.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
document.body.style.cursor = '';
}
});
} }
// Initialize SVG viewer
async init() { async init() {
const rawSvg = await chrome.webview.hostObjects.external.GetSvgContent(); const rawSvg = await chrome.webview.hostObjects.external.GetSvgContent();
const parser = new DOMParser(); const parser = new DOMParser();
@@ -107,4 +220,5 @@
} }
} }
// Create and initialize the SVG viewer
new SvgViewer().init(); new SvgViewer().init();