From 43d1d127dc622e1817965b8e10af970d7d384a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 15 Jan 2026 14:23:20 +0100 Subject: [PATCH] :tada: Apply blur effect to previous canvas pixels while setting wasm objects --- .../main/ui/workspace/sidebar/sitemap.cljs | 13 +- .../app/main/ui/workspace/viewport_wasm.cljs | 6 + frontend/src/app/render_wasm/api.cljs | 41 ++--- frontend/src/app/render_wasm/api/webgl.cljs | 166 ++++++++++++++++++ frontend/src/app/render_wasm/wasm.cljs | 3 + 5 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 frontend/src/app/render_wasm/api/webgl.cljs diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 040b1055b4..d578ccfbc5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -23,6 +23,7 @@ [app.main.ui.hooks :as hooks] [app.main.ui.icons :as deprecated-icon] [app.main.ui.notifications.badge :refer [badge-notification]] + [app.render-wasm.api :as wasm.api] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -54,12 +55,7 @@ refs/workspace-data =)) -(defn- apply-canvas-blur - "Apply blur filter to the viewport canvas element immediately. - This is used to provide visual feedback during page navigation." - [] - (when-let [canvas (dom/get-element "render")] - (dom/set-style! canvas "filter" "blur(8px)"))) + ;; --- Page Item @@ -79,8 +75,9 @@ ;; when using the wasm renderer, apply a blur effect to the viewport canvas (if (features/active-feature? @st/state "render-wasm/v1") (do - (apply-canvas-blur) - ;; NOTE: it seems we need double RAF so the blur is actually applied and visible + (wasm.api/capture-canvas-pixels) + (wasm.api/apply-canvas-blur) + ;; NOTE: it seems we need two RAF so the blur is actually applied and visible ;; in the canvas :( (timers/raf (fn [] diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 645cba9b15..0530b21a06 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -312,6 +312,11 @@ (js/console.error "Error initializing canvas context:" e) false))] (reset! canvas-init? init?) + (when init? + ;; Restore previous canvas pixels immediately after context initialization + ;; This happens before initialize-viewport is called + (wasm.api/apply-canvas-blur) + (wasm.api/restore-previous-canvas-pixels)) (when-not init? (js/alert "WebGL not supported") (st/emit! (dcm/go-to-dashboard-recent)))))))) @@ -340,6 +345,7 @@ (mf/with-effect [@canvas-init? zoom vbox background] (when (and @canvas-init? (not @initialized?)) + (wasm.api/clear-canvas-pixels) (wasm.api/initialize-viewport base-objects zoom vbox background) (reset! initialized? true))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 3059462cd6..13b74427f7 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -29,6 +29,7 @@ [app.main.worker :as mw] [app.render-wasm.api.fonts :as f] [app.render-wasm.api.texts :as t] + [app.render-wasm.api.webgl :as webgl] [app.render-wasm.deserializers :as dr] [app.render-wasm.helpers :as h] [app.render-wasm.mem :as mem] @@ -37,7 +38,6 @@ [app.render-wasm.serializers :as sr] [app.render-wasm.serializers.color :as sr-clr] [app.render-wasm.svg-filters :as svg-filters] - ;; FIXME: rename; confunsing name [app.render-wasm.wasm :as wasm] [app.util.debug :as dbg] [app.util.dom :as dom] @@ -279,30 +279,6 @@ [string] (+ (count string) 1)) -(defn- create-webgl-texture-from-image - "Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object" - [gl image-element] - (let [texture (.createTexture ^js gl)] - (.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)) - (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl)) - (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl)) - (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl)) - (.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element) - (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil) - texture)) - -(defn- get-webgl-context - "Gets the WebGL context from the WASM module" - [] - (when wasm/context-initialized? - (let [gl-obj (unchecked-get wasm/internal-module "GL")] - (when gl-obj - ;; Get the current WebGL context from Emscripten - ;; The GL object has a currentContext property that contains the context handle - (let [current-ctx (.-currentContext ^js gl-obj)] - (when current-ctx - (.-GLctx ^js current-ctx))))))) (defn- get-texture-id-for-gl-object "Registers a WebGL texture with Emscripten's GL object system and returns its ID" @@ -332,8 +308,8 @@ (->> (retrieve-image url) (rx/map (fn [img] - (when-let [gl (get-webgl-context)] - (let [texture (create-webgl-texture-from-image gl img) + (when-let [gl (webgl/get-webgl-context)] + (let [texture (webgl/create-webgl-texture-from-image gl img) texture-id (get-texture-id-for-gl-object texture) width (.-width ^js img) height (.-height ^js img) @@ -1448,6 +1424,12 @@ result))) +(defn apply-canvas-blur + [] + (when wasm/canvas + (dom/set-style! wasm/canvas "filter" "blur(4px)"))) + + (defn init-wasm-module [module] (let [default-fn (unchecked-get module "default") @@ -1469,3 +1451,8 @@ (js/console.error cause) (p/resolved false))))) (p/resolved false)))) + +;; Re-export public WebGL functions +(def capture-canvas-pixels webgl/capture-canvas-pixels) +(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels) +(def clear-canvas-pixels webgl/clear-canvas-pixels) diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs new file mode 100644 index 0000000000..764da6dfa4 --- /dev/null +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -0,0 +1,166 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.render-wasm.api.webgl + "WebGL utilities for pixel capture and rendering" + (:require + [app.common.logging :as log] + [app.render-wasm.wasm :as wasm] + [app.util.dom :as dom])) + +(defn get-webgl-context + "Gets the WebGL context from the WASM module" + [] + (when wasm/context-initialized? + (let [gl-obj (unchecked-get wasm/internal-module "GL")] + (when gl-obj + ;; Get the current WebGL context from Emscripten + ;; The GL object has a currentContext property that contains the context handle + (let [current-ctx (.-currentContext ^js gl-obj)] + (when current-ctx + (.-GLctx ^js current-ctx))))))) + +(defn create-webgl-texture-from-image + "Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object" + [gl image-element] + (let [texture (.createTexture ^js gl)] + (.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)) + (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl)) + (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl)) + (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl)) + (.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element) + (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil) + texture)) + +;; FIXME: temporary function until we are able to keep the same across pages. +(defn- draw-imagedata-to-webgl + "Draws ImageData to a WebGL2 context by creating a texture" + [gl image-data] + (let [width (.-width ^js image-data) + height (.-height ^js image-data) + texture (.createTexture ^js gl)] + ;; 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)) + (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl)) + (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl)) + (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl)) + (.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-data) + + ;; Set up viewport + (.viewport ^js gl 0 0 width height) + + ;; Vertex & Fragment shaders + ;; Since we are only calling this function once (on page switch), we don't need + ;; to cache the compiled shaders somewhere else (cannot be reused in a differen context). + (let [vertex-shader-source "#version 300 es +in vec2 a_position; +in vec2 a_texCoord; +out vec2 v_texCoord; +void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; +}" + fragment-shader-source "#version 300 es +precision highp float; +in vec2 v_texCoord; +uniform sampler2D u_texture; +out vec4 fragColor; +void main() { + fragColor = texture(u_texture, v_texCoord); +}" + vertex-shader (.createShader ^js gl (.-VERTEX_SHADER ^js gl)) + fragment-shader (.createShader ^js gl (.-FRAGMENT_SHADER ^js gl)) + program (.createProgram ^js gl)] + (.shaderSource ^js gl vertex-shader vertex-shader-source) + (.compileShader ^js gl vertex-shader) + (when-not (.getShaderParameter ^js gl vertex-shader (.-COMPILE_STATUS ^js gl)) + (log/error :hint "Vertex shader compilation failed" + :log (.getShaderInfoLog ^js gl vertex-shader))) + + (.shaderSource ^js gl fragment-shader fragment-shader-source) + (.compileShader ^js gl fragment-shader) + (when-not (.getShaderParameter ^js gl fragment-shader (.-COMPILE_STATUS ^js gl)) + (log/error :hint "Fragment shader compilation failed" + :log (.getShaderInfoLog ^js gl fragment-shader))) + + (.attachShader ^js gl program vertex-shader) + (.attachShader ^js gl program fragment-shader) + (.linkProgram ^js gl program) + + (if (.getProgramParameter ^js gl program (.-LINK_STATUS ^js gl)) + (do + (.useProgram ^js gl program) + + ;; Create full-screen quad vertices (normalized device coordinates) + (let [position-location (.getAttribLocation ^js gl program "a_position") + texcoord-location (.getAttribLocation ^js gl program "a_texCoord") + position-buffer (.createBuffer ^js gl) + texcoord-buffer (.createBuffer ^js gl) + positions #js [-1.0 -1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0] + texcoords #js [0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0 1.0]] + ;; Set up position buffer + (.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) position-buffer) + (.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. positions) (.-STATIC_DRAW ^js gl)) + (.enableVertexAttribArray ^js gl position-location) + (.vertexAttribPointer ^js gl position-location 2 (.-FLOAT ^js gl) false 0 0) + ;; Set up texcoord buffer + (.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) texcoord-buffer) + (.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. texcoords) (.-STATIC_DRAW ^js gl)) + (.enableVertexAttribArray ^js gl texcoord-location) + (.vertexAttribPointer ^js gl texcoord-location 2 (.-FLOAT ^js gl) false 0 0) + ;; Set texture uniform + (.activeTexture ^js gl (.-TEXTURE0 ^js gl)) + (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture) + (let [texture-location (.getUniformLocation ^js gl program "u_texture")] + (.uniform1i ^js gl texture-location 0)) + + ;; draw + (.drawArrays ^js gl (.-TRIANGLES ^js gl) 0 6) + + ;; cleanup + (.deleteBuffer ^js gl position-buffer) + (.deleteBuffer ^js gl texcoord-buffer) + (.deleteShader ^js gl vertex-shader) + (.deleteShader ^js gl fragment-shader) + (.deleteProgram ^js gl program))) + (log/error :hint "Program linking failed" + :log (.getProgramInfoLog ^js gl program))) + + (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil) + (.deleteTexture ^js gl texture)))) + +(defn restore-previous-canvas-pixels + "Restores previous canvas pixels into the new canvas" + [] + (when-let [previous-canvas-pixels wasm/canvas-pixels] + (when-let [gl wasm/gl-context] + (draw-imagedata-to-webgl gl previous-canvas-pixels) + (set! wasm/canvas-pixels nil)))) + +(defn clear-canvas-pixels + [] + (when wasm/canvas + (let [context wasm/gl-context] + (.clearColor ^js context 0 0 0 0.0) + (.clear ^js context (.-COLOR_BUFFER_BIT ^js context)) + (.clear ^js context (.-DEPTH_BUFFER_BIT ^js context)) + (.clear ^js context (.-STENCIL_BUFFER_BIT ^js context))) + (dom/set-style! wasm/canvas "filter" "none") + (set! wasm/canvas-pixels nil))) + +(defn capture-canvas-pixels + "Captures the pixels of the viewport canvas" + [] + (when wasm/canvas + (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)))) diff --git a/frontend/src/app/render_wasm/wasm.cljs b/frontend/src/app/render_wasm/wasm.cljs index 0f6a0f6c5f..6547d27b9a 100644 --- a/frontend/src/app/render_wasm/wasm.cljs +++ b/frontend/src/app/render_wasm/wasm.cljs @@ -12,6 +12,8 @@ ;; Reference to the HTML canvas element. (defonce canvas nil) +;; Reference to the captured pixels of the canvas (for page switching effect) +(defonce canvas-pixels nil) ;; Reference to the Emscripten GL context wrapper. (defonce gl-context-handle nil) @@ -56,3 +58,4 @@ :stroke-linecap shared/RawStrokeLineCap :stroke-linejoin shared/RawStrokeLineJoin :fill-rule shared/RawFillRule}) +