aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2026-05-28 20:47:08 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2026-05-28 20:47:08 +0200
commitcd7883d16956d80c67a91c4e07be5335f4de5c39 (patch)
tree633631b3bbe3f6beb16859bcf0eb4c088eb571ae
parent35fb9361b2113f4ebc7179b9421f5f3b8a98a6c7 (diff)
first sketch of pinch zoom
-rw-r--r--src/main.zig3
-rw-r--r--src/root.zig3
-rw-r--r--src/web/component/timeline/index.css9
-rw-r--r--src/web/component/timeline/index.js54
-rw-r--r--src/web/index.css1
-rw-r--r--src/web/pinch.js60
6 files changed, 116 insertions, 14 deletions
diff --git a/src/main.zig b/src/main.zig
index 82f5824..4b2b025 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -17,6 +17,7 @@ const document: html.Element = .native(.html, .{}, .children(&.{
.name = "viewport",
.content = "width=device-width, initial-scale=1.0",
}, .void),
+ z.script.load(@embedFile("web/pinch.js")),
z.style.load(@embedFile("web/index.css")),
z.env(components),
})),
@@ -26,7 +27,7 @@ const document: html.Element = .native(.html, .{}, .children(&.{
}));
pub fn main(init: std.process.Init) !void {
- var server: http.Server = try .init(init.gpa, try .parseLiteral("127.0.0.1:8080"));
+ var server: http.Server = try .init(init.gpa, try .parseLiteral("0.0.0.0:8080"));
try server.serve(comptime document.toDocument());
}
diff --git a/src/root.zig b/src/root.zig
index 8d85eb4..0cd7c71 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -2,5 +2,6 @@
pub const db = @import("db");
pub const api = @import("api");
-pub const frontend = @import("frontend");
+pub const html = @import("html");
+pub const z = @import("z");
pub const http = @import("http");
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();
+ }
+}