Improve zoom clarity by removing default blur and rendering preview at device-pixel resolution#8
Conversation
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/61d6d2f5-380b-4d3a-ba37-e7598c439ba7 Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/61d6d2f5-380b-4d3a-ba37-e7598c439ba7 Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/61d6d2f5-380b-4d3a-ba37-e7598c439ba7 Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/61d6d2f5-380b-4d3a-ba37-e7598c439ba7 Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/61d6d2f5-380b-4d3a-ba37-e7598c439ba7 Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR aims to improve perceived zoom/preview clarity in the editor and during zoom interactions by changing motion blur defaults, increasing preview rendering resolution, tuning blur application, and increasing desktop source thumbnail resolution.
Changes:
- Default
motionBlurtofalsefor auto-generated and manually-added zoom keyframes. - Render the editor preview canvas at
CSS size × devicePixelRatioand enable high-quality image smoothing. - Increase desktop source thumbnail capture size for clearer source selection previews.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/renderer/components/editor/ZoomEditor.tsx | Changes newly added zoom keyframes to default motionBlur: false. |
| src/renderer/components/editor/VideoPreview.tsx | Resizes canvas backing buffer to match displayed size × DPR; adjusts blur calculation and removes fixed canvas dimensions. |
| src/main/recorder.ts | Increases desktopCapturer thumbnail size and updates auto-zoom defaults to motionBlur: false. |
| const rect = canvas.getBoundingClientRect() | ||
| const dpr = window.devicePixelRatio | ||
| const targetWidth = Math.max(FALLBACK_CANVAS_DIMENSION, Math.round(rect.width * dpr)) | ||
| const targetHeight = Math.max(FALLBACK_CANVAS_DIMENSION, Math.round(rect.height * dpr)) | ||
| if (canvas.width !== targetWidth || canvas.height !== targetHeight) { | ||
| canvas.width = targetWidth | ||
| canvas.height = targetHeight | ||
| } |
There was a problem hiding this comment.
The canvas is resized using getBoundingClientRect() inside renderFrame(), which will run every animation frame while playing. Reading layout each frame can force synchronous reflow and hurt playback performance; consider moving the resize logic to a ResizeObserver/window resize handler (or caching rect + only re-measuring when needed) and keep renderFrame() focused on drawing.
| const W = canvas.width | ||
| const H = canvas.height |
There was a problem hiding this comment.
After switching the backing buffer to CSS × devicePixelRatio, all drawing now happens in device pixels (W/H), but sizes like fontSize, strokeWidth, shadowBlur, and the hard-coded padding = 40 remain in the old coordinate system. On HiDPI displays this will make text/lines/padding appear smaller by ~1/dpr. Consider scaling the context (e.g., ctx.setTransform(dpr, 0, 0, dpr, 0, 0)) and then drawing in CSS pixels, or multiply all size constants by dpr to keep the preview visually consistent.
| const W = canvas.width | |
| const H = canvas.height | |
| const W = Math.max(FALLBACK_CANVAS_DIMENSION, rect.width) | |
| const H = Math.max(FALLBACK_CANVAS_DIMENSION, rect.height) | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0) |
| export async function getDesktopSources() { | ||
| const sources = await desktopCapturer.getSources({ | ||
| types: ['window', 'screen'], | ||
| thumbnailSize: { width: 320, height: 180 } | ||
| thumbnailSize: { width: 960, height: 540 } | ||
| }) |
There was a problem hiding this comment.
Bumping thumbnailSize from 320x180 to 960x540 will significantly increase CPU/memory usage and the IPC payload size (you convert every thumbnail to a base64 data URL). If this is only for a small on-screen preview, consider a smaller size, using JPEG (nativeImage.toJPEG(quality)) to reduce transfer size, or lazily requesting higher-res thumbnails only for the selected source.
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/44934262-8507-46ef-b0a0-e297244b6a5b Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/44934262-8507-46ef-b0a0-e297244b6a5b Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Implemented the requested review-thread updates in |
| let resizeObserver: ResizeObserver | null = null | ||
| if (canvas && typeof ResizeObserver !== 'undefined') { | ||
| resizeObserver = new ResizeObserver(() => { | ||
| syncCanvasMetrics() | ||
| renderSingleFrame() | ||
| }) | ||
| resizeObserver.observe(canvas) | ||
| } | ||
| window.addEventListener('resize', syncCanvasMetrics) | ||
|
|
There was a problem hiding this comment.
When ResizeObserver is unavailable, the window resize handler only calls syncCanvasMetrics(). If the canvas backing size changes, setting canvas.width/height clears the canvas, but no redraw is triggered, so the preview can go blank until the next play/seek/pause event. Consider updating the resize handler to also call renderSingleFrame() (or wrap both in a single handler) so paused previews stay visible after window resizes in environments without ResizeObserver (e.g. some test environments).
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/604aeea3-4afd-4095-a491-cb196f0c617a Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/604aeea3-4afd-4095-a491-cb196f0c617a Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/604aeea3-4afd-4095-a491-cb196f0c617a Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/604aeea3-4afd-4095-a491-cb196f0c617a Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/604aeea3-4afd-4095-a491-cb196f0c617a Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
| const rect = currentCanvas.getBoundingClientRect() | ||
| const cssWidth = Math.max(FALLBACK_CANVAS_DIMENSION, rect.width) | ||
| const cssHeight = Math.max(FALLBACK_CANVAS_DIMENSION, rect.height) | ||
| const devicePixelRatio = window.devicePixelRatio | ||
| const pixelWidth = Math.max(FALLBACK_CANVAS_DIMENSION, Math.ceil(rect.width * devicePixelRatio)) | ||
| const pixelHeight = Math.max(FALLBACK_CANVAS_DIMENSION, Math.ceil(rect.height * devicePixelRatio)) | ||
| const previousMetrics = canvasMetricsRef.current |
There was a problem hiding this comment.
syncCanvasMetrics stores cssWidth/cssHeight from getBoundingClientRect() (often fractional) but derives pixelWidth/pixelHeight via Math.ceil(css * devicePixelRatio). This can make scaleX/scaleY = canvas.width / W slightly different from devicePixelRatio (e.g. 667/333.33), which reintroduces subtle resampling/blur—the opposite of the goal here.
Consider deriving the CSS-space metrics from the backing size (e.g. set cssWidth = pixelWidth / devicePixelRatio and cssHeight = pixelHeight / devicePixelRatio, or otherwise ensuring scaleX/scaleY equals the intended DPR) so 1 CSS px maps to an integer number of backing pixels whenever possible.
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/d8ea7003-e0d0-40b9-9955-5815d738e66d Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/d8ea7003-e0d0-40b9-9955-5815d738e66d Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Applied in |
| }, [project, currentTime, videoRef]) | ||
|
|
||
| useEffect(() => { | ||
| const video = videoRef.current | ||
| const canvas = canvasRef.current |
There was a problem hiding this comment.
useEffect depends on renderFrame, but renderFrame is recreated whenever currentTime (and/or project) changes. That will repeatedly tear down and re-register the ResizeObserver and window.resize listener during playback/seek updates, which can be a noticeable perf hit and can miss resize events between cleanups. Consider making the resize/listener effect independent of currentTime by keeping the latest renderFrame in a ref (or splitting into separate effects), so observers/listeners are registered once per mounted canvas/video element.
| const baseCssWidth = Math.max(FALLBACK_CANVAS_DIMENSION, rect.width) | ||
| const baseCssHeight = Math.max(FALLBACK_CANVAS_DIMENSION, rect.height) | ||
| const pixelWidth = Math.ceil(baseCssWidth * devicePixelRatio) | ||
| const pixelHeight = Math.ceil(baseCssHeight * devicePixelRatio) | ||
| const cssWidth = pixelWidth / devicePixelRatio | ||
| const cssHeight = pixelHeight / devicePixelRatio | ||
| const previousMetrics = canvasMetricsRef.current |
There was a problem hiding this comment.
cssWidth/cssHeight are derived from ceil(rect.* * dpr) / dpr, so W/H used for drawing can differ slightly from the actual on-screen getBoundingClientRect() size due to rounding. This can introduce subtle coordinate mismatches (e.g., annotation placement uses rect.width/height but rendering uses W/H). Consider storing the unrounded rect.width/height as the CSS-space metrics (and only rounding the backing canvas.width/height), or explicitly syncing the canvas CSS size to the rounded cssWidth/cssHeight so input and rendering coordinate spaces stay consistent.
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/929c8253-09ef-4d00-a29b-9302807a1dca Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Applied in |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/renderer/components/editor/VideoPreview.tsx:309
- This comment says the effect re-renders "when currentTime changes", but the effect dependency list is now only
[videoRef]so it won't re-run oncurrentTimeupdates. Either update the comment to reflect the new behavior (re-render is driven by the separate[renderFrame]effect) or re-introduce the dependency if the intent is still to render oncurrentTimechanges.
window.addEventListener('resize', handleCanvasResize)
// Render one frame immediately for initial state / when currentTime changes
renderSingleFrame()
| useEffect(() => { | ||
| renderFrameRef.current = renderFrame | ||
| if (animFrameRef.current === 0) { | ||
| renderFrameRef.current() | ||
| } | ||
| }, [renderFrame]) |
There was a problem hiding this comment.
The effect that syncs renderFrameRef calls renderFrameRef.current() immediately when animFrameRef.current === 0. On initial mount this runs before syncCanvasMetrics() (declared in the next effect), so canvasMetricsRef is still the 1×1 fallback. That leads to negative availW/availH (due to the fixed padding) and an incorrect first render/frame flash. Consider ensuring canvas metrics are initialized before the first render (e.g., move syncCanvasMetrics outside the effect so it can be called here, or gate the initial render until metrics are > padding*2, or convert the metrics sync to a useLayoutEffect that runs before triggering the first draw).
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/iamtatenda/Focra/sessions/00a15715-0a33-4b9c-92ae-33d57dcf4086 Co-authored-by: iamtatenda <120183671+iamtatenda@users.noreply.github.com>
Applied in |
Improve zoom clarity by removing default blur and rendering preview at device-pixel resolution
Improve zoom clarity by removing default blur and rendering preview at device-pixel resolution

Uh oh!
There was an error while loading. Please reload this page.