Files
cv-site/doc/ZOOM_IMPLEMENTATION.md
T
2025-11-16 12:48:12 +00:00

8.6 KiB
Raw Blame History

Zoom Implementation - Technical Documentation

Overview

This document describes the technical implementation of the custom zoom feature in the CV application, including architectural decisions, constraints, and solutions to specific challenges.

Implementation Summary

  • Range: 25% - 175% (centered at 100%)
  • Technology: CSS zoom property on wrapper element
  • Fixed Elements: Action bar, zoom control, info button, back-to-top button
  • Viewport Behavior: Horizontal scroll enabled when zoomed > 100%

Technical Architecture

1. Core Zoom Application

Location: static/js/main.js - applyZoom() function

function applyZoom(zoomValue, saveToStorage = true) {
    const zoomWrapper = document.getElementById('zoom-wrapper');
    const zoomLevel = zoomValue / 100;

    // Apply CSS zoom to wrapper
    zoomWrapper.style.zoom = zoomLevel;

    // Allow horizontal expansion when zoomed > 100%
    if (zoomLevel > 1) {
        zoomWrapper.style.minWidth = `${window.innerWidth}px`;
    } else {
        zoomWrapper.style.minWidth = '';
    }

    // Apply inverse zoom to fixed buttons
    const inverseZoom = 1 / zoomLevel;
    document.getElementById('back-to-top').style.zoom = inverseZoom;
    document.getElementById('info-button').style.zoom = inverseZoom;
}

2. HTML Structure

Location: templates/index.html

<!-- Fixed elements (NOT affected by zoom) -->
<div class="action-bar">...</div>
<nav class="navigation-menu">...</nav>

<!-- Zoomable content wrapper -->
<div id="zoom-wrapper" class="zoom-wrapper">
    <div class="cv-container">
        <main class="cv-paper">
            <!-- CV content here -->
        </main>
    </div>
</div>

<!-- Fixed elements (NOT affected by zoom) -->
<footer>...</footer>
<button id="back-to-top">...</button>
<button id="info-button">...</button>
<div id="zoom-control">...</div>

Critical Design Decisions

Decision 1: CSS zoom vs transform: scale()

Choice: CSS zoom property

Why:

  • Layout impact: zoom affects actual layout dimensions, making scrollbars appear naturally
  • Footer positioning: With zoom, footer automatically follows zoomed content
  • Browser behavior: Mimics native browser zoom behavior more closely

Why NOT transform: scale():

  • Only visual transformation, doesn't affect layout
  • Creates infinite gray space below content
  • Requires complex margin calculations to position footer
  • Doesn't trigger natural scrollbar behavior

Decision 2: Inverse Zoom for Fixed Elements

Problem: CSS zoom on parent can affect fixed-position children in some rendering contexts

Solution: Apply inverse zoom to elements that must stay constant size

const inverseZoom = 1 / zoomLevel;
button.style.zoom = inverseZoom;

Example:

  • Content zoom: 150% (1.5)
  • Button inverse zoom: 66.67% (1/1.5)
  • Result: Button appears at 100% (1.5 × 0.667 = 1.0)

Affected Elements:

  • #back-to-top button
  • #info-button button
  • Note: #zoom-control and .action-bar are naturally outside zoom context

Decision 3: Viewport Expansion Strategy

Problem: Elements with width: 100% stay constrained to viewport even when zoomed

Solution: Set min-width on zoom wrapper when zoom > 100%

if (zoomLevel > 1) {
    zoomWrapper.style.minWidth = `${window.innerWidth}px`;
}

Effect:

  • Content can expand beyond viewport width
  • Horizontal scrollbar appears automatically
  • User can scroll to see all zoomed content

CSS Support:

body {
    overflow-x: auto; /* Enable horizontal scroll */
}

Constraints and Limitations

1. Browser Compatibility

CSS zoom property:

  • Chrome, Edge, Safari: Full support
  • ⚠️ Firefox: Supported since Firefox 126 (May 2024)
  • Fallback: Content remains at 100% zoom in unsupported browsers

2. Range Limitations

Current Range: 25% - 175%

Why centered at 100%:

  • Slider midpoint = (25 + 175) / 2 = 100
  • Users expect "reset" to be at slider center
  • Provides balanced zoom-in/zoom-out range

Why NOT 50% - 200%:

  • 50% minimum would make slider center = 125%, confusing UX
  • Current range provides adequate zoom range while maintaining intuitive controls

3. Fixed Element Positioning

Elements that MUST stay outside zoom-wrapper:

  • Action bar (navigation)
  • Zoom control
  • Fixed buttons (info, back-to-top)
  • Footer

If placed inside zoom-wrapper:

  • Would scale with content
  • Requires inverse zoom application (added complexity)
  • Footer positioning becomes unreliable

4. Performance Considerations

CSS zoom Performance:

  • Generally performant (GPU-accelerated in modern browsers)
  • Can cause reflow when changed
  • Mitigated by requestAnimationFrame() wrapper

Optimization:

requestAnimationFrame(() => {
    zoomWrapper.style.zoom = zoomLevel;
    // Other DOM modifications
});

5. Mobile Behavior

Current Implementation: Zoom control hidden on mobile (≤768px viewport)

Rationale:

  • Mobile browsers provide native pinch-to-zoom
  • Custom zoom control redundant on touch devices
  • Saves screen space on small viewports

Code:

function isMobileView() {
    return window.innerWidth <= 768;
}

Edge Cases Handled

1. Zoom Persistence

Behavior: Zoom level saved to localStorage

localStorage.setItem('cv-zoom', zoomValue.toString());

On Page Load: Restore saved zoom level

const savedZoom = localStorage.getItem('cv-zoom');
if (savedZoom) {
    applyZoom(parseInt(savedZoom, 10), false);
}

2. Responsive Resize

Problem: User resizes window while zoomed

Solution: Recalculate min-width on window resize

window.addEventListener('resize', function() {
    // Debounced resize handler
    if (isMobileView()) {
        applyZoom(100, false); // Reset on mobile
    }
});

3. Reset Button Green Indicator

Feature: Reset button turns green when zoom ≠ 100%

Implementation:

if (zoomValue !== 100) {
    resetBtn.classList.add('zoom-not-default');
} else {
    resetBtn.classList.remove('zoom-not-default');
}

CSS:

.zoom-reset-btn.zoom-not-default:hover {
    background: #74aacd;
}

4. Keyboard Shortcuts

Supported Shortcuts:

  • Ctrl/Cmd + Plus: Zoom in (+10%)
  • Ctrl/Cmd + Minus: Zoom out (-10%)
  • Ctrl/Cmd + 0: Reset to 100%

Implementation:

document.addEventListener('keydown', function(e) {
    if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
        if (e.key === '=' || e.key === '+') {
            e.preventDefault();
            incrementZoom(10);
        }
        // ... other shortcuts
    }
});

Bottom Scroll Detection Integration

Feature: Info and back-to-top buttons turn green when at page bottom

Challenge: Zoom affects scroll calculations

Solution: Bottom detection works independently of zoom

const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = document.documentElement.clientHeight;
const isAtBottom = (scrollHeight - currentScroll - clientHeight) < 50;

Why it works: scrollHeight and clientHeight automatically account for zoomed content dimensions

Future Considerations

Potential Enhancements

  1. Smooth Zoom Transitions

    • Current: Instant zoom change
    • Possible: CSS transition on zoom property
    • Trade-off: May feel laggy for large zoom changes
  2. Zoom Presets

    • Current: Free-range slider
    • Possible: Preset buttons (50%, 75%, 100%, 125%, 150%)
    • Trade-off: Less flexible, but faster access
  3. Per-Section Zoom

    • Current: Entire document zooms
    • Possible: Zoom specific sections independently
    • Trade-off: Complex state management

Known Limitations to Monitor

  1. Firefox < 126: No CSS zoom support (falls back to 100%)
  2. Print Behavior: Zoom is reset for print (intentional)
  3. High Zoom Levels: Text rendering quality may degrade > 175%

Testing Checklist

  • Zoom in/out with slider
  • Zoom with keyboard shortcuts
  • Reset button functionality
  • Fixed elements stay constant size
  • Horizontal scroll appears when needed
  • Bottom detection works with zoom
  • Green button indicators work correctly
  • localStorage persistence works
  • Mobile: Zoom control hidden
  • Print: Zoom resets properly

References


Last Updated: 2025-11-12 Author: Development Team Status: Implemented and Stable