diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index ce00c06d60..3283050d23 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -88,7 +88,7 @@ (p/resolved nil) ;; Blur with Skia, then capture the already-blurred frame. (do (wasm.api/render-blurred-snapshot!) - (wasm.api/capture-canvas-snapshot-url))) + (wasm.api/capture-canvas-snapshot))) (p/finally (fn [] (wasm.api/apply-canvas-blur) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 2972b58ead..9f23d33485 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -117,6 +117,44 @@ (uuid? outlined-frame-id) (conj outlined-frame-id) (uuid? edition) (disj edition)))) +(mf/defc transition-overlay* + "Frozen-frame overlay shown while switching pages or recovering from WebGL + context loss. `image` is either an `ImageBitmap` snapshot of the canvas or + a data-url string placeholder (initial load)." + [{:keys [image clip-path]}] + (let [canvas-ref (mf/use-ref nil) + bitmap? (not (string? image))] + (mf/with-effect [image] + (when bitmap? + (when-let [canvas (mf/ref-val canvas-ref)] + ;; A closed ImageBitmap reports zero size; skip instead of throwing. + (when (pos? (.-width ^js image)) + (set! (.-width ^js canvas) (.-width ^js image)) + (set! (.-height ^js canvas) (.-height ^js image)) + (let [ctx (.getContext ^js canvas "2d")] + (.drawImage ^js ctx image 0 0)))))) + (if bitmap? + ;; Full-bleed so the snapshot overlays the canvas 1:1. + [:canvas {:data-testid "canvas-wasm-transition" + :ref canvas-ref + :style {:position "absolute" + :inset 0 + :width "100%" + :height "100%" + :object-fit "cover" + :pointer-events "none" + :clip-path clip-path}}] + [:img {:data-testid "canvas-wasm-transition" + :src image + :draggable false + :style {:position "absolute" + :inset 0 + :width "100%" + :height "100%" + :object-fit "cover" + :pointer-events "none" + :clip-path clip-path}}]))) + (mf/defc viewport* [{:keys [selected wglobal layout file page palete-size]}] (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check @@ -375,7 +413,7 @@ preview-blend (-> refs/workspace-preview-blend (mf/deref)) shapes-loading? (mf/deref wasm.api/shapes-loading?) - transition-image-url (mf/deref wasm.api/transition-image-url*)] + transition-image (mf/deref wasm.api/transition-image*)] ;; NOTE: We need this page-id dependency to react to it and reset the ;; canvas, even though we are not using `page-id` inside the hook. @@ -593,25 +631,16 @@ ;; Show the transition image when switching pages or recovering from WebGL context loss. (when (and (or page-transition? context-loss-overlay?) - (some? transition-image-url)) - (let [src transition-image-url] - [:img {:data-testid "canvas-wasm-transition" - :src src - :draggable false - ;; Full-bleed so the snapshot overlays the canvas 1:1. - :style {:position "absolute" - :inset 0 - :width "100%" - :height "100%" - :object-fit "cover" - :pointer-events "none" - ;; Initial load: clip to the live canvas frame (rounded - ;; corner + ruler strips when present) so it shows - ;; through. No frame in hide-UI mode -> no clip. - :clip-path (when (and transition-reveal-rulers? frame-visible?) - (let [strip (if show-rulers? rulers/ruler-area-size 0)] - (dm/str "inset(" strip "px 0 0 " strip "px round " - rulers/canvas-border-radius "px)")))}}])) + (some? transition-image)) + [:> transition-overlay* + {:image transition-image + ;; Initial load: clip to the live canvas frame (rounded + ;; corner + ruler strips when present) so it shows + ;; through. No frame in hide-UI mode -> no clip. + :clip-path (when (and transition-reveal-rulers? frame-visible?) + (let [strip (if show-rulers? rulers/ruler-area-size 0)] + (dm/str "inset(" strip "px 0 0 " strip "px round " + rulers/canvas-border-radius "px)")))}]) [:svg.viewport-controls diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index a1a0a2f6bf..cefdb08dc1 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -70,9 +70,9 @@ ;; `penpot:wasm:tiles-complete`. ;; ;; - `page-transition?`: true while the overlay should be considered active. -;; - `transition-image-url*`: URL used by the UI overlay (usually `blob:` from the -;; current WebGL canvas snapshot; on initial load it may be a tiny SVG data-url -;; derived from the page background color). +;; - `transition-image*`: image shown by the UI overlay (usually an `ImageBitmap` +;; snapshot of the WebGL canvas; on initial load it may be a tiny SVG data-url +;; string derived from the page background color). ;; - `transition-epoch*`: monotonic counter used to ignore stale async work/events ;; when the user clicks pages rapidly (A -> B -> C). ;; - `transition-tiles-handler*`: the currently installed DOM event handler for @@ -83,7 +83,7 @@ ;; rulers show through. False (page switch / context loss) keeps the snapshot's ;; baked-in rulers full-bleed to avoid a blank-strip flicker on canvas remount. (defonce transition-reveal-rulers? (atom false)) -(defonce transition-image-url* (atom nil)) +(defonce transition-image* (atom nil)) (defonce transition-epoch* (atom 0)) (defonce transition-tiles-handler* (atom nil)) (defonce snapshot-tiles-handler* (atom nil)) @@ -100,13 +100,13 @@ (defn set-transition-image-from-background! - "Sets `transition-image-url*` to a data URL representing a solid background color." + "Sets `transition-image*` to a data URL representing a solid background color." [background] (when (string? background) (let [svg (str "" "" "")] - (reset! transition-image-url* + (reset! transition-image* (str "data:image/svg+xml;charset=utf-8," (js/encodeURIComponent svg)))))) (defn begin-page-transition! @@ -120,7 +120,7 @@ (when-let [prev @transition-tiles-handler*] (.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev)) (reset! transition-tiles-handler* nil) - (reset! transition-image-url* nil)) + (reset! transition-image* nil)) (defn- set-transition-tiles-complete-handler! "Installs a tiles-complete handler bound to the current transition epoch. @@ -146,7 +146,7 @@ (reset! transition-reveal-rulers? true) ; reveal the live rulers ;; If something already toggled `page-transition?` (e.g. legacy init code paths), ;; ensure we still have a deterministic placeholder on initial load. - (when (or (not @page-transition?) (nil? @transition-image-url*)) + (when (or (not @page-transition?) (nil? @transition-image*)) (set-transition-image-from-background! background)) (when-not @page-transition? ;; Start transition + bind the tiles-complete handler to this epoch. @@ -161,7 +161,7 @@ [] (reset! context-loss-overlay? false) (when-not @page-transition? - (reset! transition-image-url* nil))) + (reset! transition-image* nil))) (defn listen-tiles-render-complete-once! "Registers a one-shot listener for `penpot:wasm:tiles-complete`, dispatched from WASM @@ -173,12 +173,28 @@ (f)) #js {:once true})) +(defn capture-canvas-snapshot + "Captures the viewport canvas into `wasm/canvas-snapshot` (an `ImageBitmap`) + and closes the replaced snapshot unless the transition overlay is still + showing it (a replaced snapshot can never become displayed again, so closing + it is safe). Returns a promise resolving to the bitmap (or nil)." + [] + (let [^js prev wasm/canvas-snapshot] + (-> (webgl/capture-canvas-snapshot) + (p/then (fn [^js bitmap] + (when (and (some? prev) + (some? bitmap) + (not (identical? prev bitmap)) + (not (identical? prev @transition-image*))) + (.close prev)) + bitmap))))) + (defonce ^:private schedule-canvas-snapshot-capture! (fns/debounce (fn [] (when (and (initialized?) (some? wasm/canvas)) - (-> (webgl/capture-canvas-snapshot-url) + (-> (capture-canvas-snapshot) (p/catch (fn [_] nil))))) snapshot-capture-debounce-ms)) @@ -239,7 +255,6 @@ (def ^:const TEXT_EDITOR_EVENT_NEEDS_LAYOUT 4) ;; Re-export public WebGL functions -(def capture-canvas-snapshot-url webgl/capture-canvas-snapshot-url) (def draw-thumbnail-to-canvas webgl/draw-thumbnail-to-canvas) ;; Re-export public text editor functions @@ -432,7 +447,7 @@ (defn render-blurred-snapshot! "Blurs the current page into the canvas so a following - `capture-canvas-snapshot-url` grabs an already-blurred transition frame." + `capture-canvas-snapshot` grabs an already-blurred transition frame." [] (when (and wasm/context-initialized? (not @wasm/context-lost?)) (h/call wasm/internal-module "_render_blurred_snapshot" TRANSITION_BLUR_RADIUS))) @@ -1994,9 +2009,8 @@ ;; Keep the last rendered pixels visible while context is lost/recovering. (reset! transition-reveal-rulers? false) ; snapshot has rulers baked in (start-context-loss-overlay!) - (when-let [url wasm/canvas-snapshot-url] - (when (string? url) - (reset! transition-image-url* url))) + (when-let [snapshot wasm/canvas-snapshot] + (reset! transition-image* snapshot)) (reset! wasm/context-lost? true) (st/async-emit! (ntf/show {:content (tr "webgl.webgl-context-lost.toast") @@ -2415,12 +2429,11 @@ ;; Lock the snapshot for the whole transition: if the user clicks to another page ;; while the transition is active, keep showing the original page snapshot until ;; the final target page finishes rendering. The caller (sitemap on-click) is - ;; responsible for ensuring `wasm/canvas-snapshot-url` was freshly captured + ;; responsible for ensuring `wasm/canvas-snapshot` was freshly captured ;; before invoking us. (when-not already? - (when-let [url wasm/canvas-snapshot-url] - (when (string? url) - (reset! transition-image-url* url)))))) + (when-let [snapshot wasm/canvas-snapshot] + (reset! transition-image* snapshot))))) (defn render-shape-pixels [shape-id scale] diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs index 853370335e..23b8d57810 100644 --- a/frontend/src/app/render_wasm/api/webgl.cljs +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -134,28 +134,20 @@ 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`. +(defn capture-canvas-snapshot + "Captures the current viewport canvas as an `ImageBitmap` and stores it in + `wasm/canvas-snapshot`. Unlike `canvas.toBlob` (which does a synchronous GPU + readback plus PNG encoding on the main thread), `createImageBitmap` resolves + asynchronously and stays on the GPU in accelerated browsers. - Returns a promise resolving to the URL string (or nil)." + Returns a promise resolving to the ImageBitmap (or nil)." [] (if-let [^js canvas wasm/canvas] - (p/create - (fn [resolve _reject] - ;; Revoke previous snapshot to avoid leaking blob URLs. - (when-let [prev wasm/canvas-snapshot-url] - (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"))) + (-> (js/createImageBitmap canvas) + (p/then (fn [^js bitmap] + (set! wasm/canvas-snapshot bitmap) + bitmap)) + (p/catch (fn [_] nil))) (p/resolved nil))) (defn draw-thumbnail-to-canvas diff --git a/frontend/src/app/render_wasm/wasm.cljs b/frontend/src/app/render_wasm/wasm.cljs index caf3d3a15a..22403cc823 100644 --- a/frontend/src/app/render_wasm/wasm.cljs +++ b/frontend/src/app/render_wasm/wasm.cljs @@ -13,9 +13,10 @@ ;; Reference to the HTML canvas element. (defonce canvas nil) -;; Snapshot of the current canvas suitable for `` overlays. -;; This is typically a `blob:` URL created via `canvas.toBlob`. -(defonce canvas-snapshot-url nil) +;; Snapshot of the current canvas as an `ImageBitmap`, suitable for painting +;; into an overlay canvas. Created via `createImageBitmap` so capturing never +;; encodes pixels on the main thread. +(defonce canvas-snapshot nil) ;; Reference to the Emscripten GL context wrapper. (defonce gl-context-handle nil) @@ -38,7 +39,7 @@ [] (set! internal-frame-id nil) (set! canvas nil) - (set! canvas-snapshot-url nil) + (set! canvas-snapshot nil) (set! gl-context-handle nil) (set! gl-context nil) (set! context-initialized? false)