mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 19:06:18 +00:00
⚡ Replace toBlob to capture snapshots without blocking the main thread
This commit is contained in:
parent
045f177a1f
commit
4a86431dfd
@ -88,7 +88,7 @@
|
|||||||
(p/resolved nil)
|
(p/resolved nil)
|
||||||
;; Blur with Skia, then capture the already-blurred frame.
|
;; Blur with Skia, then capture the already-blurred frame.
|
||||||
(do (wasm.api/render-blurred-snapshot!)
|
(do (wasm.api/render-blurred-snapshot!)
|
||||||
(wasm.api/capture-canvas-snapshot-url)))
|
(wasm.api/capture-canvas-snapshot)))
|
||||||
(p/finally
|
(p/finally
|
||||||
(fn []
|
(fn []
|
||||||
(wasm.api/apply-canvas-blur)
|
(wasm.api/apply-canvas-blur)
|
||||||
|
|||||||
@ -117,6 +117,44 @@
|
|||||||
(uuid? outlined-frame-id) (conj outlined-frame-id)
|
(uuid? outlined-frame-id) (conj outlined-frame-id)
|
||||||
(uuid? edition) (disj edition))))
|
(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*
|
(mf/defc viewport*
|
||||||
[{:keys [selected wglobal layout file page palete-size]}]
|
[{:keys [selected wglobal layout file page palete-size]}]
|
||||||
(let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
|
(let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
|
||||||
@ -375,7 +413,7 @@
|
|||||||
preview-blend (-> refs/workspace-preview-blend
|
preview-blend (-> refs/workspace-preview-blend
|
||||||
(mf/deref))
|
(mf/deref))
|
||||||
shapes-loading? (mf/deref wasm.api/shapes-loading?)
|
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
|
;; 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.
|
;; 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.
|
;; Show the transition image when switching pages or recovering from WebGL context loss.
|
||||||
(when (and (or page-transition? context-loss-overlay?)
|
(when (and (or page-transition? context-loss-overlay?)
|
||||||
(some? transition-image-url))
|
(some? transition-image))
|
||||||
(let [src transition-image-url]
|
[:> transition-overlay*
|
||||||
[:img {:data-testid "canvas-wasm-transition"
|
{:image transition-image
|
||||||
:src src
|
;; Initial load: clip to the live canvas frame (rounded
|
||||||
:draggable false
|
;; corner + ruler strips when present) so it shows
|
||||||
;; Full-bleed so the snapshot overlays the canvas 1:1.
|
;; through. No frame in hide-UI mode -> no clip.
|
||||||
:style {:position "absolute"
|
:clip-path (when (and transition-reveal-rulers? frame-visible?)
|
||||||
:inset 0
|
(let [strip (if show-rulers? rulers/ruler-area-size 0)]
|
||||||
:width "100%"
|
(dm/str "inset(" strip "px 0 0 " strip "px round "
|
||||||
:height "100%"
|
rulers/canvas-border-radius "px)")))}])
|
||||||
: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)")))}}]))
|
|
||||||
|
|
||||||
|
|
||||||
[:svg.viewport-controls
|
[:svg.viewport-controls
|
||||||
|
|||||||
@ -70,9 +70,9 @@
|
|||||||
;; `penpot:wasm:tiles-complete`.
|
;; `penpot:wasm:tiles-complete`.
|
||||||
;;
|
;;
|
||||||
;; - `page-transition?`: true while the overlay should be considered active.
|
;; - `page-transition?`: true while the overlay should be considered active.
|
||||||
;; - `transition-image-url*`: URL used by the UI overlay (usually `blob:` from the
|
;; - `transition-image*`: image shown by the UI overlay (usually an `ImageBitmap`
|
||||||
;; current WebGL canvas snapshot; on initial load it may be a tiny SVG data-url
|
;; snapshot of the WebGL canvas; on initial load it may be a tiny SVG data-url
|
||||||
;; derived from the page background color).
|
;; string derived from the page background color).
|
||||||
;; - `transition-epoch*`: monotonic counter used to ignore stale async work/events
|
;; - `transition-epoch*`: monotonic counter used to ignore stale async work/events
|
||||||
;; when the user clicks pages rapidly (A -> B -> C).
|
;; when the user clicks pages rapidly (A -> B -> C).
|
||||||
;; - `transition-tiles-handler*`: the currently installed DOM event handler for
|
;; - `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
|
;; 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.
|
;; baked-in rulers full-bleed to avoid a blank-strip flicker on canvas remount.
|
||||||
(defonce transition-reveal-rulers? (atom false))
|
(defonce transition-reveal-rulers? (atom false))
|
||||||
(defonce transition-image-url* (atom nil))
|
(defonce transition-image* (atom nil))
|
||||||
(defonce transition-epoch* (atom 0))
|
(defonce transition-epoch* (atom 0))
|
||||||
(defonce transition-tiles-handler* (atom nil))
|
(defonce transition-tiles-handler* (atom nil))
|
||||||
(defonce snapshot-tiles-handler* (atom nil))
|
(defonce snapshot-tiles-handler* (atom nil))
|
||||||
@ -100,13 +100,13 @@
|
|||||||
|
|
||||||
|
|
||||||
(defn set-transition-image-from-background!
|
(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]
|
[background]
|
||||||
(when (string? background)
|
(when (string? background)
|
||||||
(let [svg (str "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'>"
|
(let [svg (str "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'>"
|
||||||
"<rect width='1' height='1' fill='" background "'/>"
|
"<rect width='1' height='1' fill='" background "'/>"
|
||||||
"</svg>")]
|
"</svg>")]
|
||||||
(reset! transition-image-url*
|
(reset! transition-image*
|
||||||
(str "data:image/svg+xml;charset=utf-8," (js/encodeURIComponent svg))))))
|
(str "data:image/svg+xml;charset=utf-8," (js/encodeURIComponent svg))))))
|
||||||
|
|
||||||
(defn begin-page-transition!
|
(defn begin-page-transition!
|
||||||
@ -120,7 +120,7 @@
|
|||||||
(when-let [prev @transition-tiles-handler*]
|
(when-let [prev @transition-tiles-handler*]
|
||||||
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||||
(reset! transition-tiles-handler* nil)
|
(reset! transition-tiles-handler* nil)
|
||||||
(reset! transition-image-url* nil))
|
(reset! transition-image* nil))
|
||||||
|
|
||||||
(defn- set-transition-tiles-complete-handler!
|
(defn- set-transition-tiles-complete-handler!
|
||||||
"Installs a tiles-complete handler bound to the current transition epoch.
|
"Installs a tiles-complete handler bound to the current transition epoch.
|
||||||
@ -146,7 +146,7 @@
|
|||||||
(reset! transition-reveal-rulers? true) ; reveal the live rulers
|
(reset! transition-reveal-rulers? true) ; reveal the live rulers
|
||||||
;; If something already toggled `page-transition?` (e.g. legacy init code paths),
|
;; If something already toggled `page-transition?` (e.g. legacy init code paths),
|
||||||
;; ensure we still have a deterministic placeholder on initial load.
|
;; 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))
|
(set-transition-image-from-background! background))
|
||||||
(when-not @page-transition?
|
(when-not @page-transition?
|
||||||
;; Start transition + bind the tiles-complete handler to this epoch.
|
;; Start transition + bind the tiles-complete handler to this epoch.
|
||||||
@ -161,7 +161,7 @@
|
|||||||
[]
|
[]
|
||||||
(reset! context-loss-overlay? false)
|
(reset! context-loss-overlay? false)
|
||||||
(when-not @page-transition?
|
(when-not @page-transition?
|
||||||
(reset! transition-image-url* nil)))
|
(reset! transition-image* nil)))
|
||||||
|
|
||||||
(defn listen-tiles-render-complete-once!
|
(defn listen-tiles-render-complete-once!
|
||||||
"Registers a one-shot listener for `penpot:wasm:tiles-complete`, dispatched from WASM
|
"Registers a one-shot listener for `penpot:wasm:tiles-complete`, dispatched from WASM
|
||||||
@ -173,12 +173,28 @@
|
|||||||
(f))
|
(f))
|
||||||
#js {:once true}))
|
#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!
|
(defonce ^:private schedule-canvas-snapshot-capture!
|
||||||
(fns/debounce
|
(fns/debounce
|
||||||
(fn []
|
(fn []
|
||||||
(when (and (initialized?)
|
(when (and (initialized?)
|
||||||
(some? wasm/canvas))
|
(some? wasm/canvas))
|
||||||
(-> (webgl/capture-canvas-snapshot-url)
|
(-> (capture-canvas-snapshot)
|
||||||
(p/catch (fn [_] nil)))))
|
(p/catch (fn [_] nil)))))
|
||||||
snapshot-capture-debounce-ms))
|
snapshot-capture-debounce-ms))
|
||||||
|
|
||||||
@ -239,7 +255,6 @@
|
|||||||
(def ^:const TEXT_EDITOR_EVENT_NEEDS_LAYOUT 4)
|
(def ^:const TEXT_EDITOR_EVENT_NEEDS_LAYOUT 4)
|
||||||
|
|
||||||
;; Re-export public WebGL functions
|
;; 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)
|
(def draw-thumbnail-to-canvas webgl/draw-thumbnail-to-canvas)
|
||||||
|
|
||||||
;; Re-export public text editor functions
|
;; Re-export public text editor functions
|
||||||
@ -432,7 +447,7 @@
|
|||||||
|
|
||||||
(defn render-blurred-snapshot!
|
(defn render-blurred-snapshot!
|
||||||
"Blurs the current page into the canvas so a following
|
"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?))
|
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||||
(h/call wasm/internal-module "_render_blurred_snapshot" TRANSITION_BLUR_RADIUS)))
|
(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.
|
;; Keep the last rendered pixels visible while context is lost/recovering.
|
||||||
(reset! transition-reveal-rulers? false) ; snapshot has rulers baked in
|
(reset! transition-reveal-rulers? false) ; snapshot has rulers baked in
|
||||||
(start-context-loss-overlay!)
|
(start-context-loss-overlay!)
|
||||||
(when-let [url wasm/canvas-snapshot-url]
|
(when-let [snapshot wasm/canvas-snapshot]
|
||||||
(when (string? url)
|
(reset! transition-image* snapshot))
|
||||||
(reset! transition-image-url* url)))
|
|
||||||
(reset! wasm/context-lost? true)
|
(reset! wasm/context-lost? true)
|
||||||
(st/async-emit!
|
(st/async-emit!
|
||||||
(ntf/show {:content (tr "webgl.webgl-context-lost.toast")
|
(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
|
;; 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
|
;; while the transition is active, keep showing the original page snapshot until
|
||||||
;; the final target page finishes rendering. The caller (sitemap on-click) is
|
;; 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.
|
;; before invoking us.
|
||||||
(when-not already?
|
(when-not already?
|
||||||
(when-let [url wasm/canvas-snapshot-url]
|
(when-let [snapshot wasm/canvas-snapshot]
|
||||||
(when (string? url)
|
(reset! transition-image* snapshot)))))
|
||||||
(reset! transition-image-url* url))))))
|
|
||||||
|
|
||||||
(defn render-shape-pixels
|
(defn render-shape-pixels
|
||||||
[shape-id scale]
|
[shape-id scale]
|
||||||
|
|||||||
@ -134,28 +134,20 @@ void main() {
|
|||||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||||
(.deleteTexture ^js gl texture))))
|
(.deleteTexture ^js gl texture))))
|
||||||
|
|
||||||
(defn capture-canvas-snapshot-url
|
(defn capture-canvas-snapshot
|
||||||
"Captures the current viewport canvas as a PNG `blob:` URL and stores it in
|
"Captures the current viewport canvas as an `ImageBitmap` and stores it in
|
||||||
`wasm/canvas-snapshot-url`.
|
`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]
|
(if-let [^js canvas wasm/canvas]
|
||||||
(p/create
|
(-> (js/createImageBitmap canvas)
|
||||||
(fn [resolve _reject]
|
(p/then (fn [^js bitmap]
|
||||||
;; Revoke previous snapshot to avoid leaking blob URLs.
|
(set! wasm/canvas-snapshot bitmap)
|
||||||
(when-let [prev wasm/canvas-snapshot-url]
|
bitmap))
|
||||||
(when (and (string? prev) (.startsWith ^js prev "blob:"))
|
(p/catch (fn [_] nil)))
|
||||||
(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")))
|
|
||||||
(p/resolved nil)))
|
(p/resolved nil)))
|
||||||
|
|
||||||
(defn draw-thumbnail-to-canvas
|
(defn draw-thumbnail-to-canvas
|
||||||
|
|||||||
@ -13,9 +13,10 @@
|
|||||||
|
|
||||||
;; Reference to the HTML canvas element.
|
;; Reference to the HTML canvas element.
|
||||||
(defonce canvas nil)
|
(defonce canvas nil)
|
||||||
;; Snapshot of the current canvas suitable for `<img src=...>` overlays.
|
;; Snapshot of the current canvas as an `ImageBitmap`, suitable for painting
|
||||||
;; This is typically a `blob:` URL created via `canvas.toBlob`.
|
;; into an overlay canvas. Created via `createImageBitmap` so capturing never
|
||||||
(defonce canvas-snapshot-url nil)
|
;; encodes pixels on the main thread.
|
||||||
|
(defonce canvas-snapshot nil)
|
||||||
|
|
||||||
;; Reference to the Emscripten GL context wrapper.
|
;; Reference to the Emscripten GL context wrapper.
|
||||||
(defonce gl-context-handle nil)
|
(defonce gl-context-handle nil)
|
||||||
@ -38,7 +39,7 @@
|
|||||||
[]
|
[]
|
||||||
(set! internal-frame-id nil)
|
(set! internal-frame-id nil)
|
||||||
(set! canvas nil)
|
(set! canvas nil)
|
||||||
(set! canvas-snapshot-url nil)
|
(set! canvas-snapshot nil)
|
||||||
(set! gl-context-handle nil)
|
(set! gl-context-handle nil)
|
||||||
(set! gl-context nil)
|
(set! gl-context nil)
|
||||||
(set! context-initialized? false)
|
(set! context-initialized? false)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user