Let the tiles actually grow
The ceiling was a fixed 240 points, which two people in a 520-wide window were already against — so dragging the window to twice the size changed the tiles by six points, and the resizing looked broken because in every way that mattered it was. The ceiling now comes from the wall's share of the height, so a bigger window really does make the faces bigger: three people go from 153 points at 520x700 to 277 at 900x700 to 449 on a 1080p screen. The fixed cap is still there but generous, and only answers on a screen tall enough that the height never binds. Columns are picked by trying every arrangement from one row to one column and keeping the roomiest, because neither axis alone decides it — more columns buy height by spending width, and which is worth more depends on the shape of the window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16df4e0 parent: 4917912 modified
src/frq/av.jolt +40 -37 | @@ -495,11 +495,14 @@ | ||
| 495 | 495 | ;; is people looking at each other, not a contact sheet. |
| 496 | 496 | (def ^:private want-tile 160) |
| 497 | 497 | |
| 498 | -;; A tile narrower than this is not a face, it is a thumbnail of one. The | |
| 499 | -;; ceiling is where a tile stops being a tile and starts being the screen — at | |
| 500 | -;; which point the person wants the picture, not the call. | |
| 498 | +;; A tile narrower than this is not a face, it is a thumbnail of one. | |
| 501 | 499 | (def ^:private min-tile 96) |
| 502 | -(def ^:private max-tile 240) | |
| 500 | + | |
| 501 | +;; And an upper bound, so that one person alone does not become a wall-sized | |
| 502 | +;; portrait in a maximised window. Generous rather than tight — what actually | |
| 503 | +;; stops tiles growing is the height budget below, and this is only here so | |
| 504 | +;; there is an answer on a screen tall enough not to bind. | |
| 505 | +(def ^:private max-tile 480) | |
| 503 | 506 | |
| 504 | 507 | ;; What a tile costs in height beyond its picture: the name under it, and the |
| 505 | 508 | ;; gap to the row below. |
| @@ -510,46 +513,45 @@ | ||
| 510 | 513 | ;; being that and starts being a video app with a chat box attached. |
| 511 | 514 | (def ^:private wall-share 0.34) |
| 512 | 515 | |
| 513 | -(defn- tile-height [width] (+ (long (* width 0.75)) tile-label)) | |
| 514 | - | |
| 515 | 516 | (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols))) |
| 516 | 517 | |
| 517 | 518 | (defn tile-width |
| 518 | - "How wide each of `n` tiles across should be in a window of `width` points. | |
| 519 | - | |
| 520 | - A wider window makes everyone bigger, which is the point of dragging one | |
| 521 | - wider during a call; a second person appearing shrinks the first rather than | |
| 522 | - pushing them off the edge." | |
| 523 | - [width n] | |
| 519 | + "How wide a tile is with `n` across and `rows` down, in a window of `width` | |
| 520 | + by `height` points. | |
| 521 | + | |
| 522 | + Bounded by both axes, and the height is usually the one that binds. That is | |
| 523 | + deliberate: a fixed ceiling meant a window dragged from half the screen to | |
| 524 | + all of it changed the tiles by six points, because they were already against | |
| 525 | + it. Deriving the ceiling from the wall's share of the height means a taller | |
| 526 | + window really does make the faces bigger." | |
| 527 | + [width height n rows] | |
| 524 | 528 | (let [n (max 1 n) |
| 525 | - usable (max 0 (- (or width 0) wall-chrome (* tile-gap (dec n))))] | |
| 526 | - (-> (quot usable n) | |
| 529 | + rows (max 1 rows) | |
| 530 | + across (max 0 (- (or width 0) wall-chrome (* tile-gap (dec n)))) | |
| 531 | + by-width (quot across n) | |
| 532 | + ;; What the row height allows, once the name and the gap are paid for. | |
| 533 | + down (- (quot (long (* (or height 0) wall-share)) rows) tile-gap tile-label) | |
| 534 | + by-height (long (/ (max 0 down) 0.75))] | |
| 535 | + (-> (min by-width by-height) | |
| 527 | 536 | (max min-tile) |
| 528 | 537 | (min max-tile)))) |
| 529 | 538 | |
| 530 | 539 | (defn columns |
| 531 | - "How many tiles to put across a window of `width` by `height` points. | |
| 532 | - | |
| 533 | - Two pressures, and the second is why this is not simply a division. Across: | |
| 534 | - tiles are kept near the size they want rather than crammed to the minimum, | |
| 535 | - because four faces at a hundred points each is worse than two rows of two at | |
| 536 | - twice that. Down: the whole wall has to fit its share of the window, so when | |
| 537 | - the rows that follow from a comfortable width would be taller than that, it | |
| 538 | - takes more columns and smaller tiles instead — which is what stops five | |
| 539 | - people in a narrow window from stacking five rows deep and pushing the | |
| 540 | - conversation off the screen." | |
| 540 | + "How many tiles to put across, for the biggest tiles the window allows. | |
| 541 | + | |
| 542 | + Every arrangement from one row to one column is tried and the roomiest wins, | |
| 543 | + because neither axis alone decides it: more columns buy height by spending | |
| 544 | + width, and which is worth more depends on the shape of the window. Ties go to | |
| 545 | + fewer columns, which is the arrangement with fewer rows." | |
| 541 | 546 | [width height n] |
| 542 | - (let [n (max 1 n) | |
| 543 | - usable (- (or width 0) wall-chrome) | |
| 544 | - want (-> (quot (+ usable tile-gap) (+ want-tile tile-gap)) (max 1) (min n)) | |
| 545 | - budget (* (or height 0) wall-share) | |
| 546 | - fits? (fn [cols] | |
| 547 | - (<= (* (rows-for n cols) (+ (tile-height (tile-width width cols)) tile-gap)) | |
| 548 | - budget))] | |
| 549 | - ;; From the width we would like, widen until the wall fits its share. The | |
| 550 | - ;; last candidate is every tile on one row, which is as short as the wall | |
| 551 | - ;; can be made — if that still does not fit, it is the best on offer. | |
| 552 | - (or (first (filter fits? (range want (inc n)))) n))) | |
| 547 | + (let [n (max 1 n)] | |
| 548 | + (reduce (fn [best cols] | |
| 549 | + (if (> (tile-width width height cols (rows-for n cols)) | |
| 550 | + (tile-width width height best (rows-for n best))) | |
| 551 | + cols | |
| 552 | + best)) | |
| 553 | + 1 | |
| 554 | + (range 2 (inc n))))) | |
| 553 | 555 | |
| 554 | 556 | (defn tile-rows |
| 555 | 557 | "The tiles as `[width [[key ...] ...]]` — one width, and the rows to draw. |
| @@ -559,6 +561,7 @@ | ||
| 559 | 561 | as though something had gone wrong rather than as a layout." |
| 560 | 562 | ([] (tile-rows @window-width @window-height (tiles))) |
| 561 | 563 | ([width height keys] |
| 562 | - (let [cols (columns width height (count keys))] | |
| 563 | - [(tile-width width cols) | |
| 564 | + (let [n (count keys) | |
| 565 | + cols (columns width height n)] | |
| 566 | + [(tile-width width height cols (rows-for n cols)) | |
| 564 | 567 | (mapv vec (partition-all cols keys))]))) |
| @@ -495,11 +495,14 @@ | |||
| 495 | ;; is people looking at each other, not a contact sheet. | 495 | ;; is people looking at each other, not a contact sheet. |
| 496 | (def ^:private want-tile 160) | 496 | (def ^:private want-tile 160) |
| 497 | 497 | ||
| 498 | -;; A tile narrower than this is not a face, it is a thumbnail of one. The | 498 | +;; A tile narrower than this is not a face, it is a thumbnail of one. |
| 499 | -;; ceiling is where a tile stops being a tile and starts being the screen — at | ||
| 500 | -;; which point the person wants the picture, not the call. | ||
| 501 | (def ^:private min-tile 96) | 499 | (def ^:private min-tile 96) |
| 502 | -(def ^:private max-tile 240) | 500 | + |
| 501 | +;; And an upper bound, so that one person alone does not become a wall-sized | ||
| 502 | +;; portrait in a maximised window. Generous rather than tight — what actually | ||
| 503 | +;; stops tiles growing is the height budget below, and this is only here so | ||
| 504 | +;; there is an answer on a screen tall enough not to bind. | ||
| 505 | +(def ^:private max-tile 480) | ||
| 503 | 506 | ||
| 504 | ;; What a tile costs in height beyond its picture: the name under it, and the | 507 | ;; What a tile costs in height beyond its picture: the name under it, and the |
| 505 | ;; gap to the row below. | 508 | ;; gap to the row below. |
| @@ -510,46 +513,45 @@ | |||
| 510 | ;; being that and starts being a video app with a chat box attached. | 513 | ;; being that and starts being a video app with a chat box attached. |
| 511 | (def ^:private wall-share 0.34) | 514 | (def ^:private wall-share 0.34) |
| 512 | 515 | ||
| 513 | -(defn- tile-height [width] (+ (long (* width 0.75)) tile-label)) | ||
| 514 | - | ||
| 515 | (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols))) | 516 | (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols))) |
| 516 | 517 | ||
| 517 | (defn tile-width | 518 | (defn tile-width |
| 518 | - "How wide each of `n` tiles across should be in a window of `width` points. | 519 | + "How wide a tile is with `n` across and `rows` down, in a window of `width` |
| 519 | - | 520 | + by `height` points. |
| 520 | - A wider window makes everyone bigger, which is the point of dragging one | 521 | + |
| 521 | - wider during a call; a second person appearing shrinks the first rather than | 522 | + Bounded by both axes, and the height is usually the one that binds. That is |
| 522 | - pushing them off the edge." | 523 | + deliberate: a fixed ceiling meant a window dragged from half the screen to |
| 523 | - [width n] | 524 | + all of it changed the tiles by six points, because they were already against |
| 525 | + it. Deriving the ceiling from the wall's share of the height means a taller | ||
| 526 | + window really does make the faces bigger." | ||
| 527 | + [width height n rows] | ||
| 524 | (let [n (max 1 n) | 528 | (let [n (max 1 n) |
| 525 | - usable (max 0 (- (or width 0) wall-chrome (* tile-gap (dec n))))] | 529 | + rows (max 1 rows) |
| 526 | - (-> (quot usable n) | 530 | + across (max 0 (- (or width 0) wall-chrome (* tile-gap (dec n)))) |
| 531 | + by-width (quot across n) | ||
| 532 | + ;; What the row height allows, once the name and the gap are paid for. | ||
| 533 | + down (- (quot (long (* (or height 0) wall-share)) rows) tile-gap tile-label) | ||
| 534 | + by-height (long (/ (max 0 down) 0.75))] | ||
| 535 | + (-> (min by-width by-height) | ||
| 527 | (max min-tile) | 536 | (max min-tile) |
| 528 | (min max-tile)))) | 537 | (min max-tile)))) |
| 529 | 538 | ||
| 530 | (defn columns | 539 | (defn columns |
| 531 | - "How many tiles to put across a window of `width` by `height` points. | 540 | + "How many tiles to put across, for the biggest tiles the window allows. |
| 532 | - | 541 | + |
| 533 | - Two pressures, and the second is why this is not simply a division. Across: | 542 | + Every arrangement from one row to one column is tried and the roomiest wins, |
| 534 | - tiles are kept near the size they want rather than crammed to the minimum, | 543 | + because neither axis alone decides it: more columns buy height by spending |
| 535 | - because four faces at a hundred points each is worse than two rows of two at | 544 | + width, and which is worth more depends on the shape of the window. Ties go to |
| 536 | - twice that. Down: the whole wall has to fit its share of the window, so when | 545 | + fewer columns, which is the arrangement with fewer rows." |
| 537 | - the rows that follow from a comfortable width would be taller than that, it | ||
| 538 | - takes more columns and smaller tiles instead — which is what stops five | ||
| 539 | - people in a narrow window from stacking five rows deep and pushing the | ||
| 540 | - conversation off the screen." | ||
| 541 | [width height n] | 546 | [width height n] |
| 542 | - (let [n (max 1 n) | 547 | + (let [n (max 1 n)] |
| 543 | - usable (- (or width 0) wall-chrome) | 548 | + (reduce (fn [best cols] |
| 544 | - want (-> (quot (+ usable tile-gap) (+ want-tile tile-gap)) (max 1) (min n)) | 549 | + (if (> (tile-width width height cols (rows-for n cols)) |
| 545 | - budget (* (or height 0) wall-share) | 550 | + (tile-width width height best (rows-for n best))) |
| 546 | - fits? (fn [cols] | 551 | + cols |
| 547 | - (<= (* (rows-for n cols) (+ (tile-height (tile-width width cols)) tile-gap)) | 552 | + best)) |
| 548 | - budget))] | 553 | + 1 |
| 549 | - ;; From the width we would like, widen until the wall fits its share. The | 554 | + (range 2 (inc n))))) |
| 550 | - ;; last candidate is every tile on one row, which is as short as the wall | ||
| 551 | - ;; can be made — if that still does not fit, it is the best on offer. | ||
| 552 | - (or (first (filter fits? (range want (inc n)))) n))) | ||
| 553 | 555 | ||
| 554 | (defn tile-rows | 556 | (defn tile-rows |
| 555 | "The tiles as `[width [[key ...] ...]]` — one width, and the rows to draw. | 557 | "The tiles as `[width [[key ...] ...]]` — one width, and the rows to draw. |
| @@ -559,6 +561,7 @@ | |||
| 559 | as though something had gone wrong rather than as a layout." | 561 | as though something had gone wrong rather than as a layout." |
| 560 | ([] (tile-rows @window-width @window-height (tiles))) | 562 | ([] (tile-rows @window-width @window-height (tiles))) |
| 561 | ([width height keys] | 563 | ([width height keys] |
| 562 | - (let [cols (columns width height (count keys))] | 564 | + (let [n (count keys) |
| 563 | - [(tile-width width cols) | 565 | + cols (columns width height n)] |
| 566 | + [(tile-width width height cols (rows-for n cols)) | ||
| 564 | (mapv vec (partition-all cols keys))]))) | 567 | (mapv vec (partition-all cols keys))]))) |