diff options
| author | Nathan Reiner <nathan@nathanreiner.xyz> | 2026-05-28 20:47:08 +0200 |
|---|---|---|
| committer | Nathan Reiner <nathan@nathanreiner.xyz> | 2026-05-28 20:47:08 +0200 |
| commit | cd7883d16956d80c67a91c4e07be5335f4de5c39 (patch) | |
| tree | 633631b3bbe3f6beb16859bcf0eb4c088eb571ae /src/web | |
| parent | 35fb9361b2113f4ebc7179b9421f5f3b8a98a6c7 (diff) | |
first sketch of pinch zoom
Diffstat (limited to 'src/web')
| -rw-r--r-- | src/web/component/timeline/index.css | 9 | ||||
| -rw-r--r-- | src/web/component/timeline/index.js | 54 | ||||
| -rw-r--r-- | src/web/index.css | 1 | ||||
| -rw-r--r-- | src/web/pinch.js | 60 |
4 files changed, 112 insertions, 12 deletions
diff --git a/src/web/component/timeline/index.css b/src/web/component/timeline/index.css index 4060deb..8e61fcf 100644 --- a/src/web/component/timeline/index.css +++ b/src/web/component/timeline/index.css @@ -2,6 +2,15 @@ width: 100%; display: inline-grid; gap: 0px; + transition: transform 0.1s ease; +} + +.image { + text-align: center; + align-content: center; + font-weight: bold; + font-size: 0.5em; + overflow: hidden; } #container.tiling .image { diff --git a/src/web/component/timeline/index.js b/src/web/component/timeline/index.js index 0bbbcc2..0e237a1 100644 --- a/src/web/component/timeline/index.js +++ b/src/web/component/timeline/index.js @@ -9,8 +9,9 @@ function getRandomColor() { return color; } -for (let i = 0; i < 100; ++i) { +for (let i = 0; i < 10000; ++i) { container.appendChild(Z.native('div', { + innerText: i, className: 'image', style: { background: getRandomColor(), @@ -19,24 +20,53 @@ for (let i = 0; i < 100; ++i) { })); } -const set_number_of_rows = (n) => { - container.style.gridTemplateColumns = '1fr '.repeat(n); +const zoom = new (class { + constructor() { + this.committed = 5; + this.count = 5; + container.style.gridTemplateColumns = `repeat(5, 1fr)`; + container.classList.toggle('tiling', this.count != 1); + } - container.classList.toggle('tiling', n != 1); -}; + update(delta, point) { + this.count += delta; + this.count = Math.min(Math.max(this.count, 1), 20); + + const scale = this.committed / this.count; + const rects = Z.document.getClientRects()[0]; + + const x = point.x; + const y = point.y - rects.y; -let zoom_state = 5; -set_number_of_rows(zoom_state); + container.style.transformOrigin = `${x}px ${y}px`; + container.style.transform = `scale(${scale})`; + } + + commit() { + console.log('commit'); + this.count = Math.round(this.count); + container.style.transform = 'scale(1)'; + container.style.gridTemplateColumns = `repeat(${this.count}, 1fr)`; + container.classList.toggle('tiling', this.count != 1); + this.committed = this.count; + } +}); Z.document.onwheel = (event) => { if (!event.ctrlKey) { return; } - event.preventDefault(); + zoom.update(event.wheelDeltaY / 120, { x: 0, y: 0 }); + zoom.commit(); +}; - zoom_state += event.wheelDeltaY / 120; - zoom_state = Math.max(zoom_state, 1); - set_number_of_rows(zoom_state); -} +const pinch = new Pinch(Z.document); +pinch.onpinch = (delta, point) => { + zoom.update(delta / 20, point); +}; + +pinch.onfinish = (delta) => { + zoom.commit(); +}; diff --git a/src/web/index.css b/src/web/index.css index b5647b1..3c6b0d6 100644 --- a/src/web/index.css +++ b/src/web/index.css @@ -3,6 +3,7 @@ body { margin: 0; width: 100vw; height: 100vh; + touch-action: pan-y; /* Prevent default touch behavior */ } #timeline { diff --git a/src/web/pinch.js b/src/web/pinch.js new file mode 100644 index 0000000..14f843a --- /dev/null +++ b/src/web/pinch.js @@ -0,0 +1,60 @@ +class Pinch { + constructor(element) { + this.event_cache = []; + this.previous_diff = null; + this.onpinch = () => {}; + this.onfinish = () => {}; + + element.onpointerdown = (e) => this.pointer_down_handler(e); + element.onpointermove = (e) => this.pointer_move_handler(e); + element.onpointerup = (e) => this.pointer_up_handler(e); + element.onpointercancel = (e) => this.pointer_up_handler(e); + element.onpointerout = (e) => this.pointer_up_handler(e); + element.onpointerleave = (e) => this.pointer_up_handler(e); + } + + pointer_down_handler(event) { + this.event_cache.push(event); + } + + pointer_move_handler(event) { + const index = this.event_cache.findIndex( + (cached) => cached.pointerId == event.pointerId, + ); + this.event_cache[index] = event; + + if (this.event_cache.length === 2) { + event.preventDefault(); + + const diff = Math.hypot( + this.event_cache[0].clientX - this.event_cache[1].clientX, + this.event_cache[0].clientY - this.event_cache[1].clientY, + ); + + if (this.previous_diff != null) { + const point = { + x: (this.event_cache[0].clientX + this.event_cache[1].clientX) / 2, + y: (this.event_cache[0].clientY + this.event_cache[1].clientY) / 2, + }; + + const delta = this.previous_diff - diff; + this.onpinch(delta, point); + } + + this.previous_diff = diff; + } + } + + pointer_up_handler(event) { + const index = this.event_cache.findIndex( + (cached) => cached.pointerId === event.pointerId, + ); + this.event_cache.splice(index, 1); + + if (this.event_cache.length < 2) { + this.previous_diff = null; + } + + this.onfinish(); + } +} |