🔧 Add loading between pages using previous thumbnail

This commit is contained in:
Elena Torro 2026-04-14 16:18:05 +02:00
parent 8b06adbb28
commit 47ce2ad6cd
3 changed files with 81 additions and 19 deletions

View File

@ -991,7 +991,11 @@
(letfn [(do-render []
;; Check if context is still initialized before executing
;; to prevent errors when navigating quickly
(when (and wasm/context-initialized? (not @wasm/context-lost?))
(when (and wasm/context-initialized? (not @wasm/context-lost?)
;; Skip during bulk loading — the blurred previous-page
;; preview must stay visible until set-objects finishes
;; and renders synchronously.
(not @shapes-loading?))
(perf/begin-measure "render-finish")
(h/call wasm/internal-module "_set_view_end")
(perf/end-measure "render-finish")
@ -1007,15 +1011,17 @@
(defn set-view-box
[zoom vbox]
(perf/begin-measure "set-view-box")
(h/call wasm/internal-module "_set_view_start")
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(perf/end-measure "set-view-box")
;; Skip during bulk loading — preserve blurred previous-page preview
(when-not @shapes-loading?
(perf/begin-measure "set-view-box")
(h/call wasm/internal-module "_set_view_start")
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(perf/end-measure "set-view-box")
(perf/begin-measure "render-from-cache")
(h/call wasm/internal-module "_render_from_cache" 0)
(render-finish)
(perf/end-measure "render-from-cache"))
(perf/begin-measure "render-from-cache")
(h/call wasm/internal-module "_render_from_cache" 0)
(render-finish)
(perf/end-measure "render-from-cache")))
(defn update-text-rect!
[id]
@ -1204,7 +1210,23 @@
(update-text-layouts text-ids)))
(if render-callback
(render-callback)
(request-render "set-objects-complete"))
;; When on-shapes-ready is set (page transition), use
;; synchronous render so the complete scene appears in
;; one frame right after the blur is cleared. Without
;; this, the async _render path flushes partial tiles
;; progressively, causing visible tile-by-tile loading.
(if on-shapes-ready
(do
;; Cancel the rAF that end-shapes-loading! just
;; scheduled — we are about to render synchronously
;; and don't want a redundant progressive render on
;; the next frame.
(when-let [fid wasm/internal-frame-id]
(js/cancelAnimationFrame fid)
(set! wasm/internal-frame-id nil)
(reset! pending-render false))
(render-sync))
(request-render "set-objects-complete")))
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))
(resolve nil)
@ -1252,11 +1274,25 @@
;; Rebuild the tile index so _render knows which shapes
;; map to which tiles after a page switch.
(h/call wasm/internal-module "_set_view_end")
;; Run text layouts before the first render so text shapes
;; are correctly sized immediately.
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
(when (seq text-ids)
(update-text-layouts text-ids)))
;; When doing a page transition, render immediately with whatever
;; content is available (shapes without images). Images load in
;; the background and trigger a re-render when ready.
(when on-shapes-ready
(render-sync))
(process-pending shapes thumbnails full
(fn []
(if render-callback
(render-callback)
(request-render "set-objects-sync-complete"))
(when-not on-shapes-ready
(if render-callback
(render-callback)
(request-render "set-objects-sync-complete")))
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))
(defn- shapes-in-tree-order

View File

@ -48,11 +48,28 @@
;; FIXME: temporary function until we are able to keep the same <canvas> across pages.
(defn- draw-imagedata-to-webgl
"Draws ImageData to a WebGL2 context by creating a texture"
"Draws ImageData to a WebGL2 context by creating a texture.
After _init, Skia owns the GL context and may have left a non-default
FBO bound plus scissor/stencil/depth enabled. We must reset all
relevant GL state so the full-screen quad actually lands on the
default framebuffer (the visible canvas)."
[gl image-data]
(let [width (.-width ^js image-data)
height (.-height ^js image-data)
texture (.createTexture ^js gl)]
;; Bind the default framebuffer (FBO 0) — Skia may have left one
;; of its offscreen FBOs bound.
(.bindFramebuffer ^js gl (.-FRAMEBUFFER ^js gl) nil)
;; Disable GPU tests that Skia may have enabled — any of these
;; can silently discard our fragments.
(.disable ^js gl (.-SCISSOR_TEST ^js gl))
(.disable ^js gl (.-STENCIL_TEST ^js gl))
(.disable ^js gl (.-DEPTH_TEST ^js gl))
(.disable ^js gl (.-BLEND ^js gl))
(.colorMask ^js gl true true true true)
;; Bind texture and set parameters
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
@ -156,6 +173,9 @@ void main() {
[]
(when wasm/canvas
(let [context wasm/gl-context]
;; Bind default framebuffer so we clear the visible canvas,
;; not whichever offscreen FBO Skia may have left active.
(.bindFramebuffer ^js context (.-FRAMEBUFFER ^js context) nil)
(.clearColor ^js context 0 0 0 0.0)
(.clear ^js context (.-COLOR_BUFFER_BIT ^js context))
(.clear ^js context (.-DEPTH_BUFFER_BIT ^js context))
@ -172,10 +192,11 @@ void main() {
(let [context wasm/gl-context
width (.-width wasm/canvas)
height (.-height wasm/canvas)
buffer (js/Uint8ClampedArray. (* width height 4))
_ (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer)
image-data (js/ImageData. buffer width height)]
(set! wasm/canvas-pixels image-data))))
buffer (js/Uint8ClampedArray. (* width height 4))]
;; Bind default framebuffer — Skia may have left an offscreen FBO active.
(.bindFramebuffer ^js context (.-FRAMEBUFFER ^js context) nil)
(.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer)
(set! wasm/canvas-pixels (js/ImageData. buffer width height)))))
(defn draw-thumbnail-to-canvas
"Loads an image from `uri` and draws it stretched to fill the WebGL canvas.

View File

@ -2779,8 +2779,13 @@ impl RenderState {
}
} else {
performance::begin_measure!("render_shape_tree::uncached");
// Only allow stopping (yielding) if the current tile is NOT visible.
// This ensures all visible tiles render synchronously before showing,
// eliminating empty squares during zoom. Interest-area tiles can still yield.
let tile_is_visible = self.tile_viewbox.is_visible(&current_tile);
let can_stop = allow_stop && !tile_is_visible;
let (is_empty, early_return) = self
.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?;
if early_return {
return Ok(());