Jake Vanderwerf
2026-05-12 c32ed859f4abd1591c882f4f2a6ee16b1ec275e2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class Swiper {
    constructor() {
        this.isInitialized = false;
        this.initSubscribers();
        this.initHandlers();
        this.swipe = {
            startX: null,
            endX: null,
            startY: null,
            endY: null,
            minSwipe: 50
        };
 
        this.pinch = {
            active: false,
            startDistance: 0,
            lastDistance: 0,
            scale: 1
        };
    }
 
    /*********************************************************************
     LISTENERS
     *********************************************************************/
    initHandlers() {
        this.touchStartHandler  = this.handleTouchStart.bind(this);
        this.touchMoveHandler   = this.handleTouchMove.bind(this);
        this.touchEndHandler    = this.handleTouchEnd.bind(this);
    }
    initListeners() {
        if (this.isInitialized) {
            return;
        }
        this.isInitialized = true;
        document.addEventListener('touchstart', this.touchStartHandler);
        document.addEventListener('touchmove', this.touchMoveHandler);
        document.addEventListener('touchend', this.touchEndHandler);
    }
    cleanupListeners() {
        if (this.subscribers.size > 0) return;
        this.isInitialized = false;
        document.removeEventListener('touchstart', this.touchStartHandler);
        document.removeEventListener('touchmove', this.touchMoveHandler);
        document.removeEventListener('touchend', this.touchEndHandler);
    }
    handleTouchStart(e) {
        if (e.touches.length === 2) {
            // Two-finger pinch start
            const dx = e.touches[0].clientX - e.touches[1].clientX;
            const dy = e.touches[0].clientY - e.touches[1].clientY;
            const distance = Math.sqrt(dx*dx + dy*dy);
 
            this.pinch.active = true;
            this.pinch.startDistance = this.pinch.lastDistance = distance;
            this.notify('pinch-start', { distance });
 
            return; // Don't treat this as a swipe
        }
 
        this.swipe.startX = e.touches[0].clientX;
        this.swipe.startY = e.touches[0].clientY;
    }
    handleTouchMove(e) {
        if (this.pinch.active && e.touches.length === 2) {
            const dx = e.touches[0].clientX - e.touches[1].clientX;
            const dy = e.touches[0].clientY - e.touches[1].clientY;
            const distance = Math.sqrt(dx*dx + dy*dy);
 
            const scale = distance / this.pinch.startDistance;
 
            this.pinch.lastDistance = distance;
            this.pinch.scale = scale;
 
            this.notify('pinch-move', { e, distance, scale });
 
            // Direction
            if (distance > this.pinch.startDistance) {
                this.notify('pinch-out', { scale });
            } else {
                this.notify('pinch-in', { scale });
            }
 
            return; // Do not fire swipe logic
        }
        this.swipe.endX = e.touches[0].clientX;
        this.swipe.endY = e.touches[0].clientY;
    }
    handleTouchEnd(e) {
        // Finish pinch
        if (this.pinch.active) {
            this.notify('pinch-end', {
                finalScale: this.pinch.scale
            });
            this.pinch.active = false;
            return;
        }
 
        if ((!this.swipe.startX || !this.swipe.endX) ||(!this.swipe.startY || !this.swipe.endY)) return;
 
        const distanceX = this.swipe.startX - this.swipe.endX;
        const distanceY = this.swipe.startY - this.swipe.endY;
 
        if (Math.abs(distanceX) > this.swipe.minSwipe) {
            let direction = distanceX > 0 ? 'swipe-right' : 'swipe-left';
            this.notify(direction);
        }
        if (Math.abs(distanceY) > this.swipe.minSwipe) {
            let direction = distanceY > 0 ? 'swipe-up' : 'swipe-down';
            this.notify(direction);
        }
        this.swipe.startX = this.swipe.startY = this.swipe.endX = this.swipe.endY = null;
    }
    /*********************************************************************
     SUBSCRIBERS
     *********************************************************************/
    initSubscribers() {
        this.subscribers = new Set();
    }
 
    subscribe(callback) {
        if (!this.isInitialized) {
            this.initListeners();
        }
        this.subscribers.add(callback);
        return () => this.subscribers.delete(callback);
    }
    unsubscribe(callback) {
        this.subscribers.delete(callback);
        if (this.subscribers.size === 0) {
            this.cleanupListeners();
        }
    }
 
    notify(event, data = {}) {
        this.subscribers.forEach( callback => {
            try {
                callback(event, data);
            } catch (error) {
                console.error('Subscriber error:', error);
            }
        });
    }
 
    /******************************************************************
     CLEANUP
     ******************************************************************/
 
    destroy() {
        this.subscribers.clear();
        this.cleanupListeners();
    }
}
 
window.jvbSwiper = new Swiper();