From 1ba43c863a0163ac9b69021dfbe6dcd17b2312fa Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Thu, 11 Jun 2026 12:23:51 +0200 Subject: [PATCH] :zap: Capture snapshot only when needed and downscaled --- frontend/src/app/render_wasm/api.cljs | 31 ------------- frontend/src/app/render_wasm/api/webgl.cljs | 49 ++++++++++++++++----- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index a1a0a2f6bf..811a215300 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -86,10 +86,6 @@ (defonce transition-image-url* (atom nil)) (defonce transition-epoch* (atom 0)) (defonce transition-tiles-handler* (atom nil)) -(defonce snapshot-tiles-handler* (atom nil)) - -(def ^:private snapshot-capture-debounce-ms 250) - (defn initialized? "True when the WASM render context is ready to receive design-state @@ -173,31 +169,6 @@ (f)) #js {:once true})) -(defonce ^:private schedule-canvas-snapshot-capture! - (fns/debounce - (fn [] - (when (and (initialized?) - (some? wasm/canvas)) - (-> (webgl/capture-canvas-snapshot-url) - (p/catch (fn [_] nil))))) - snapshot-capture-debounce-ms)) - -(defn- start-canvas-snapshot-listener! - [] - (when-let [prev @snapshot-tiles-handler*] - (.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev)) - (let [handler (fn [_] (schedule-canvas-snapshot-capture!))] - (reset! snapshot-tiles-handler* handler) - (.addEventListener ^js ug/document "penpot:wasm:tiles-complete" handler))) - -(defn- stop-canvas-snapshot-listener! - [] - (when-let [prev @snapshot-tiles-handler*] - (.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev)) - (reset! snapshot-tiles-handler* nil) - (when-let [cancel (unchecked-get schedule-canvas-snapshot-capture! "cancel")] - (cancel))) - (defn text-editor-wasm? [] (or (contains? cf/flags :feature-text-editor-wasm) @@ -2072,7 +2043,6 @@ (when can-listen? (.addEventListener canvas "webglcontextlost" on-webgl-context-lost) (.addEventListener canvas "webglcontextrestored" on-webgl-context-restored)) - (start-canvas-snapshot-listener!) (reset! wasm/context-lost? false) (set! wasm/context-initialized? true))) @@ -2099,7 +2069,6 @@ (when wasm/canvas (.removeEventListener wasm/canvas "webglcontextlost" on-webgl-context-lost) (.removeEventListener wasm/canvas "webglcontextrestored" on-webgl-context-restored)) - (stop-canvas-snapshot-listener!) (when (wasm/module-ready?) (free-gpu-resources) diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs index 853370335e..f46f66ac19 100644 --- a/frontend/src/app/render_wasm/api/webgl.cljs +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -134,11 +134,24 @@ void main() { (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil) (.deleteTexture ^js gl texture)))) -(defn capture-canvas-snapshot-url - "Captures the current viewport canvas as a PNG `blob:` URL and stores it in - `wasm/canvas-snapshot-url`. +;; Codec for the transition snapshot. The snapshot is only ever shown as a +;; heavily-blurred overlay (see TRANSITION_BLUR_RADIUS), so a lossy, alpha-capable +;; codec is fine and encodes much faster than lossless PNG -- on a full-viewport +;; canvas, PNG `toBlob` of millions of pixels costs ~1s+ on the main thread. +;; WebP keeps the alpha channel (unlike JPEG) and encodes in a fraction of that. +(def ^:private SNAPSHOT_MIME "image/webp") +(def ^:private SNAPSHOT_QUALITY 0.6) - Returns a promise resolving to the URL string (or nil)." +;; The snapshot is only shown heavily blurred, so it is downscaled to this max +;; side before encoding to keep the WebP encode cheap on big/high-DPI canvases. +(def ^:private SNAPSHOT_MAX_DIM 1024) + +(defonce ^:private snapshot-scratch-canvas + (delay (js/document.createElement "canvas"))) + +(defn capture-canvas-snapshot-url + "Captures the current viewport canvas as a downscaled WebP `blob:` URL and + stores it in `wasm/canvas-snapshot-url`. Returns a promise of the URL or nil." [] (if-let [^js canvas wasm/canvas] (p/create @@ -148,14 +161,26 @@ void main() { (when (and (string? prev) (.startsWith ^js prev "blob:")) (js/URL.revokeObjectURL prev))) (set! wasm/canvas-snapshot-url nil) - (.toBlob canvas - (fn [^js blob] - (if blob - (let [url (js/URL.createObjectURL blob)] - (set! wasm/canvas-snapshot-url url) - (resolve url)) - (resolve nil))) - "image/png"))) + (let [cw (.-width canvas) + ch (.-height canvas) + ;; Cap the longest side to SNAPSHOT_MAX_DIM (never upscale). + scale (min 1.0 (/ SNAPSHOT_MAX_DIM (max 1 cw ch))) + tw (max 1 (js/Math.round (* cw scale))) + th (max 1 (js/Math.round (* ch scale))) + ^js sc @snapshot-scratch-canvas + ^js ctx (.getContext sc "2d")] + (set! (.-width sc) tw) + (set! (.-height sc) th) + (.drawImage ctx canvas 0 0 tw th) + (.toBlob sc + (fn [^js blob] + (if blob + (let [url (js/URL.createObjectURL blob)] + (set! wasm/canvas-snapshot-url url) + (resolve url)) + (resolve nil))) + SNAPSHOT_MIME + SNAPSHOT_QUALITY)))) (p/resolved nil))) (defn draw-thumbnail-to-canvas