Web frame: rotation does not apply to the iframe DOM overlay #103

Open
opened 2026-04-29 10:32:46 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

Rotating a webframe rotates the Konva placeholder card on the canvas but the iframe DOM overlay (the live website) stays axis-aligned. The placeholder and the iframe end up at different angles, and the iframe lands in a wrong screen rect because rotation around the group's origin moves the corners of its bounding box.

Steps to reproduce

  1. Open a board and place a webframe pointing at any URL. Wait for the iframe to load.
  2. Select the webframe and drag the rotation handle on the canvas transformer.

Expected

The iframe DOM overlay rotates to match the Konva placeholder, staying visually aligned with the header bar and the dark background.

Actual

The Konva placeholder rotates but the iframe overlay stays axis-aligned at its original screen rect — the two visually separate. After deselecting and reselecting, the iframe is still axis-aligned regardless of the canvas rotation.

Root cause

webframe.js::updateOverlayPosition writes only wrapper.style.left / top / width / height. Konva's group.rotation() is never transferred to the DOM overlay, and the wrapper has no CSS transform: rotate(...). The screen rect calculation also assumes axis-aligned geometry, so even the left/top are wrong once a rotation is applied.

Suggested fix

In updateOverlayPosition:

  1. Read group.rotation() (in degrees).
  2. Compute the wrapper's screen position as it would be without rotation (current logic).
  3. Apply the rotation via CSS: set wrapper.style.transformOrigin = '0 0' and wrapper.style.transform = 'rotate(<deg>deg)'. Use 0 0 so the rotation pivot matches Konva's group origin.
  4. Reset transform to none (or omit) when the rotation is zero so existing axis-aligned webframes are unchanged.

Stage zoom is already applied via the width/height math; rotation composes cleanly on top because Konva groups rotate around their own origin, which matches transformOrigin: 0 0 on the wrapper.

Acceptance criteria

  • Rotating the webframe via the canvas transformer rotates the iframe overlay in lock-step.
  • Resetting rotation to 0 returns the iframe to axis-aligned with no leftover transform.
  • Pan, zoom, drag, resize continue to work correctly with a rotated webframe.
  • Sync (#101) still works: a remote rotation update is reflected on the receiver after applySyncUpdate writes the new rotation and refreshOverlay runs.
  • No regression to non-webframe objects.
## Summary Rotating a webframe rotates the Konva placeholder card on the canvas but the iframe DOM overlay (the live website) stays axis-aligned. The placeholder and the iframe end up at different angles, and the iframe lands in a wrong screen rect because rotation around the group's origin moves the corners of its bounding box. ## Steps to reproduce 1. Open a board and place a webframe pointing at any URL. Wait for the iframe to load. 2. Select the webframe and drag the rotation handle on the canvas transformer. ## Expected The iframe DOM overlay rotates to match the Konva placeholder, staying visually aligned with the header bar and the dark background. ## Actual The Konva placeholder rotates but the iframe overlay stays axis-aligned at its original screen rect — the two visually separate. After deselecting and reselecting, the iframe is still axis-aligned regardless of the canvas rotation. ## Root cause `webframe.js::updateOverlayPosition` writes only `wrapper.style.left / top / width / height`. Konva's `group.rotation()` is never transferred to the DOM overlay, and the wrapper has no CSS `transform: rotate(...)`. The screen rect calculation also assumes axis-aligned geometry, so even the `left/top` are wrong once a rotation is applied. ## Suggested fix In `updateOverlayPosition`: 1. Read `group.rotation()` (in degrees). 2. Compute the wrapper's screen position as it would be without rotation (current logic). 3. Apply the rotation via CSS: set `wrapper.style.transformOrigin = '0 0'` and `wrapper.style.transform = 'rotate(<deg>deg)'`. Use `0 0` so the rotation pivot matches Konva's group origin. 4. Reset `transform` to `none` (or omit) when the rotation is zero so existing axis-aligned webframes are unchanged. Stage zoom is already applied via the `width`/`height` math; rotation composes cleanly on top because Konva groups rotate around their own origin, which matches `transformOrigin: 0 0` on the wrapper. ## Acceptance criteria - [ ] Rotating the webframe via the canvas transformer rotates the iframe overlay in lock-step. - [ ] Resetting rotation to 0 returns the iframe to axis-aligned with no leftover transform. - [ ] Pan, zoom, drag, resize continue to work correctly with a rotated webframe. - [ ] Sync (#101) still works: a remote rotation update is reflected on the receiver after `applySyncUpdate` writes the new rotation and `refreshOverlay` runs. - [ ] No regression to non-webframe objects.
Author
Member

Spec — Issue #103: Webframe rotation must propagate to the iframe overlay

Objective

Apply the Konva group's rotation to the iframe DOM overlay so the live website rotates in lock-step with the placeholder card.

Root cause

webframe.js::updateOverlayPosition writes only wrapper.style.left/top/width/height. group.rotation() is never read, and the wrapper has no CSS transform.

Fix

In updateOverlayPosition, after the size/position writes:

  • Read group.rotation() (degrees).
  • Konva rotates around the group's local origin (0, 0). In screen coords that pivot is (screenX, screenY - headerH * scale) — i.e. (0px, -headerH * scale) relative to the wrapper's top-left.
  • Set wrapper.style.transformOrigin = '0px ' + (-headerH * scale) + 'px' and wrapper.style.transform = 'rotate(<deg>deg)' when rotation is non-zero; reset to 'none' otherwise so previously-rotated overlays are restored cleanly.

The remote-rotation path is already handled: applySyncUpdate writes node.rotation(obj.rotation) (sync.js:532) before the per-type branches, and the webframe branch (added in #101) calls refreshOverlay(strId), which calls updateOverlayPosition. So once the function applies rotation, sync of rotation is free.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js — only file touched.

Acceptance Criteria

  • Rotating the webframe via the canvas transformer rotates the iframe overlay in lock-step.
  • Returning rotation to 0 fully resets the overlay's CSS transform (no leftover rotation).
  • Pan, zoom, drag, resize continue to work correctly, including with non-zero rotation.
  • Sync (#101) still works: a remote rotation update is reflected on the receiver.
  • No regression to non-webframe objects.

Notes

  • Stage-level rotation is not used in this codebase; only stage zoom and pan compose with the group rotation. The fix accounts for zoom (already in headerH * scale) and pan (already in screenX/Y offsets).
  • CSS transform-origin is set in pixel coords relative to the wrapper's top-left, which correctly tracks zoom (since headerH * scale is the screen-pixel offset).
# Spec — Issue #103: Webframe rotation must propagate to the iframe overlay ## Objective Apply the Konva group's rotation to the iframe DOM overlay so the live website rotates in lock-step with the placeholder card. ## Root cause `webframe.js::updateOverlayPosition` writes only `wrapper.style.left/top/width/height`. `group.rotation()` is never read, and the wrapper has no CSS `transform`. ## Fix In `updateOverlayPosition`, after the size/position writes: - Read `group.rotation()` (degrees). - Konva rotates around the group's local origin `(0, 0)`. In screen coords that pivot is `(screenX, screenY - headerH * scale)` — i.e. `(0px, -headerH * scale)` relative to the wrapper's top-left. - Set `wrapper.style.transformOrigin = '0px ' + (-headerH * scale) + 'px'` and `wrapper.style.transform = 'rotate(<deg>deg)'` when rotation is non-zero; reset to `'none'` otherwise so previously-rotated overlays are restored cleanly. The remote-rotation path is already handled: `applySyncUpdate` writes `node.rotation(obj.rotation)` (sync.js:532) before the per-type branches, and the webframe branch (added in #101) calls `refreshOverlay(strId)`, which calls `updateOverlayPosition`. So once the function applies rotation, sync of rotation is free. ## Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js` — only file touched. ## Acceptance Criteria - [ ] Rotating the webframe via the canvas transformer rotates the iframe overlay in lock-step. - [ ] Returning rotation to 0 fully resets the overlay's CSS transform (no leftover rotation). - [ ] Pan, zoom, drag, resize continue to work correctly, including with non-zero rotation. - [ ] Sync (#101) still works: a remote rotation update is reflected on the receiver. - [ ] No regression to non-webframe objects. ## Notes - Stage-level rotation is not used in this codebase; only stage zoom and pan compose with the group rotation. The fix accounts for zoom (already in `headerH * scale`) and pan (already in `screenX/Y` offsets). - CSS `transform-origin` is set in pixel coords relative to the wrapper's top-left, which correctly tracks zoom (since `headerH * scale` is the screen-pixel offset).
Author
Member

Test Results

  • cargo fmt --all -- --check: pass
  • cargo check --workspace: pass
  • node --check webframe.js: pass
## Test Results - cargo fmt --all -- --check: pass - cargo check --workspace: pass - node --check webframe.js: pass
Author
Member

Implementation Summary

One file changed (webframe.js), +9/-0. Adds CSS transform: rotate(...) with a transform-origin that matches Konva's group-local pivot.

Root cause

updateOverlayPosition wrote only left/top/width/height. group.rotation() was never transferred to the DOM, so rotating the webframe rotated only the Konva placeholder.

crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js

After the existing size/position writes:

var rotation = group.rotation() || 0;
if (rotation) {
    overlay.wrapper.style.transformOrigin = '0px ' + (-headerH * scale) + 'px';
    overlay.wrapper.style.transform = 'rotate(' + rotation + 'deg)';
} else {
    overlay.wrapper.style.transform = 'none';
}

Konva rotates the group around its local origin (0, 0). In screen coords that pivot sits at (screenX, screenY - headerH * scale), which is (0px, -headerH * scale) relative to the wrapper's top-left.

Remote rotation works for free

applySyncUpdate already writes node.rotation(obj.rotation) before per-type branches, and #101's refreshOverlay call in the webframe branch invokes updateOverlayPosition after the rotation is set. So a remote rotation update reaches the iframe automatically.

Verification

  • cargo fmt --all -- --check: clean
  • cargo check --workspace: clean
  • node --check webframe.js: clean

Manual smoke test

  1. Open a board, place a webframe and let the iframe load.
  2. Select it and drag the rotation handle. Both the placeholder and the iframe rotate together.
  3. Reset rotation to 0. Iframe is axis-aligned again, no leftover transform.
  4. Two-window: rotate in tab A; tab B's iframe rotates to match within the WS-throttle window.
  5. Pan, zoom, drag, resize work as before regardless of rotation.

Notes

  • Stage-level rotation is not used in this codebase, so only group rotation is composed with the existing zoom/pan math.
  • transform-origin is in pixel coords relative to the wrapper's top-left and tracks zoom via headerH * scale.
## Implementation Summary One file changed (`webframe.js`), +9/-0. Adds CSS `transform: rotate(...)` with a `transform-origin` that matches Konva's group-local pivot. ### Root cause `updateOverlayPosition` wrote only `left/top/width/height`. `group.rotation()` was never transferred to the DOM, so rotating the webframe rotated only the Konva placeholder. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js` After the existing size/position writes: ```js var rotation = group.rotation() || 0; if (rotation) { overlay.wrapper.style.transformOrigin = '0px ' + (-headerH * scale) + 'px'; overlay.wrapper.style.transform = 'rotate(' + rotation + 'deg)'; } else { overlay.wrapper.style.transform = 'none'; } ``` Konva rotates the group around its local origin `(0, 0)`. In screen coords that pivot sits at `(screenX, screenY - headerH * scale)`, which is `(0px, -headerH * scale)` relative to the wrapper's top-left. ### Remote rotation works for free `applySyncUpdate` already writes `node.rotation(obj.rotation)` before per-type branches, and #101's `refreshOverlay` call in the webframe branch invokes `updateOverlayPosition` after the rotation is set. So a remote rotation update reaches the iframe automatically. ### Verification - `cargo fmt --all -- --check`: clean - `cargo check --workspace`: clean - `node --check webframe.js`: clean ### Manual smoke test 1. Open a board, place a webframe and let the iframe load. 2. Select it and drag the rotation handle. Both the placeholder and the iframe rotate together. 3. Reset rotation to 0. Iframe is axis-aligned again, no leftover transform. 4. Two-window: rotate in tab A; tab B's iframe rotates to match within the WS-throttle window. 5. Pan, zoom, drag, resize work as before regardless of rotation. ### Notes - Stage-level rotation is not used in this codebase, so only group rotation is composed with the existing zoom/pan math. - `transform-origin` is in pixel coords relative to the wrapper's top-left and tracks zoom via `headerH * scale`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#103
No description provided.