From 6a3a46020312f43d6778ac3974ae90c0a7c627f2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 19 Apr 2022 16:19:14 +0200 Subject: [PATCH] :zap: Advanced frame thumbnail handling --- common/src/app/common/pages/helpers.cljc | 6 + frontend/src/app/main/data/workspace.cljs | 8 +- .../app/main/data/workspace/persistence.cljs | 135 +----- .../app/main/data/workspace/selection.cljs | 22 +- .../app/main/data/workspace/thumbnails.cljs | 166 ++++++++ frontend/src/app/main/refs.cljs | 15 + frontend/src/app/main/ui/shapes/frame.cljs | 60 +-- .../app/main/ui/workspace/shapes/frame.cljs | 399 ++++-------------- .../shapes/frame/dynamic_modifiers.cljs | 63 +++ .../ui/workspace/shapes/frame/node_store.cljs | 47 +++ .../shapes/frame/thumbnail_render.cljs | 119 ++++++ .../app/main/ui/workspace/shapes/group.cljs | 2 +- .../app/main/ui/workspace/shapes/svg_raw.cljs | 4 +- .../workspace/shapes/text/viewport_texts.cljs | 12 +- .../src/app/main/ui/workspace/viewport.cljs | 1 - .../app/main/ui/workspace/viewport/hooks.cljs | 20 +- .../viewport/thumbnail_renderer.cljs | 161 ------- .../app/main/ui/workspace/viewport/utils.cljs | 2 +- frontend/src/app/util/dom.cljs | 33 +- 19 files changed, 613 insertions(+), 662 deletions(-) create mode 100644 frontend/src/app/main/data/workspace/thumbnails.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs delete mode 100644 frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index b042d1c2d..943a7f2fc 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -468,3 +468,9 @@ (let [path-split (split-path path)] (merge-path-item (first path-split) name))) + +(defn get-frame-objects + "Retrieves a new objects map only with the objects under frame-id (with frame-id)" + [objects frame-id] + (let [ids (concat [frame-id] (get-children-ids objects frame-id))] + (select-keys objects ids))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index bbed408dd..eabcaced0 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -43,6 +43,7 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.svg-upload :as svg] + [app.main.data.workspace.thumbnails :as dwth] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.zoom :as dwz] @@ -195,7 +196,8 @@ ptk/WatchEvent (watch [_ state _] (if (contains? (get-in state [:workspace-data :pages-index]) page-id) - (rx/of (dwp/preload-data-uris)) + (rx/of (dwp/preload-data-uris) + (dwth/watch-state-changes)) (let [default-page-id (get-in state [:workspace-data :pages 0])] (rx/of (go-to-page default-page-id))))) @@ -1767,3 +1769,7 @@ (dm/export dwz/decrease-zoom) (dm/export dwz/increase-zoom) (dm/export dwz/set-zoom) + +;; Thumbnails +(dm/export dwth/update-thumbnail) + diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 1069d00a8..c7e57e890 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.pages :as cp] - [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.spec.change :as spec.change] [app.common.spec.file :as spec.file] @@ -26,7 +25,6 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.svg-upload :as svg] - [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.util.http :as http] @@ -35,7 +33,6 @@ [app.util.uri :as uu] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] [cuerdas.core :as str] [potok.core :as ptk] [promesa.core :as p] @@ -53,7 +50,7 @@ (ptk/reify ::initialize-persistence ptk/EffectEvent (effect [_ _ stream] - (let [stoper (rx/filter #(= ::finalize %) stream) + (let [stoper (rx/filter #(= :app.main.data.workspace/finalize %) stream) forcer (rx/filter #(= ::force-persist %) stream) notifier (->> stream (rx/filter dch/commit-changes?) @@ -552,136 +549,6 @@ (update-in [:workspace-file :pages] #(filterv (partial not= id) %)) (update :workspace-pages dissoc id))) -(def update-frame-thumbnail? (ptk/type? ::update-frame-thumbnail)) - -(defn remove-thumbnails - [ids] - (ptk/reify ::remove-thumbnails - ptk/WatchEvent - (watch [_ state _] - ;; Removes the thumbnail while it's regenerated - (let [moving? (= :move (get-in state [:workspace-local :transform])) - selected? (wsh/lookup-selected state) - ;; When we're moving the current frame it's safe to keep the thumbnail - ;; if it's resize we need to remove it immeditely - ids (cond->> ids moving? (remove selected?))] - - (if (empty? ids) - (rx/empty) - (rx/of (dch/update-shapes ids #(dissoc % :thumbnail) {:save-undo? false}))))))) - -(defn update-frame-thumbnail - [frame-id] - (ptk/event ::update-frame-thumbnail {:frame-id frame-id})) - -(defn update-shape-thumbnail - "An event that is succeptible to be executed out of the main flow, so - it need to correctly handle the situation that there are no page-id - or file-is loaded." - [shape-id thumbnail-data] - (ptk/reify ::update-shape-thumbnail - ptk/WatchEvent - (watch [_ state _] - (when (and (dwc/initialized? state) - (uuid? shape-id)) - (rx/of (dch/update-shapes [shape-id] - #(assoc % :thumbnail thumbnail-data) - {:save-undo? false})))))) - -(defn- extract-frame-changes - "Process a changes set in a commit to extract the frames that are changing" - [[event [old-objects new-objects]]] - (let [changes (-> event deref :changes) - - extract-ids - (fn [{type :type :as change}] - (case type - :add-obj [(:id change)] - :mod-obj [(:id change)] - :del-obj [(:id change)] - :reg-objects (:shapes change) - :mov-objects (:shapes change) - [])) - - get-frame-id - (fn [id] - (let [shape (or (get new-objects id) - (get old-objects id))] - - (or (and (= :frame (:type shape)) id) - (:frame-id shape)))) - - ;; Extracts the frames and then removes nils and the root frame - xform (comp (mapcat extract-ids) - (map get-frame-id) - (remove nil?) - (filter #(not= uuid/zero %)) - (filter #(contains? new-objects %)))] - - (into #{} xform changes))) - -(defn thumbnail-change? - "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" - [event] - (let [changes (-> event deref :changes) - - is-thumbnail-op? - (fn [{type :type attr :attr}] - (and (= type :set) - (= attr :thumbnail))) - - is-thumbnail-change? - (fn [change] - (and (= (:type change) :mod-obj) - (->> change :operations (every? is-thumbnail-op?))))] - - (->> changes (every? is-thumbnail-change?)))) - -(defn watch-state-changes [] - (ptk/reify ::watch-state-changes - ptk/WatchEvent - (watch [_ state stream] - (let [stopper (->> stream - (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) - (= ::watch-state-changes (ptk/type %))))) - - objects-stream (->> (rx/concat - (rx/of nil) - (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})) - ;; We need to keep the old-objects so we can check the frame for the - ;; deleted objects - (rx/buffer 2 1)) - - frame-changes (->> stream - (rx/filter dch/commit-changes?) - - ;; Async so we wait for additional side-effects of commit-changes - (rx/observe-on :async) - (rx/filter (comp not thumbnail-change?)) - (rx/with-latest-from objects-stream) - (rx/map extract-frame-changes) - (rx/share)) - - frames (-> state wsh/lookup-page-objects cph/get-frames) - no-thumb-frames (->> frames - (filter (comp nil? :thumbnail)) - (mapv :id))] - - (rx/concat - (->> (rx/from no-thumb-frames) - (rx/map #(update-frame-thumbnail %))) - - ;; We remove the thumbnails immediately but defer their generation - (rx/merge - (->> frame-changes - (rx/take-until stopper) - (rx/map #(remove-thumbnails %))) - - (->> frame-changes - (rx/take-until stopper) - (rx/buffer-until (->> frame-changes (rx/debounce 1000))) - (rx/flat-map #(reduce set/union %)) - (rx/map #(update-frame-thumbnail %))))))))) (defn preload-data-uris "Preloads the image data so it's ready when necesary" diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index ff79bcb7e..a47d62592 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -21,6 +21,7 @@ [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.zoom :as dwz] [app.main.refs :as refs] [app.main.streams :as ms] @@ -495,18 +496,29 @@ id-original (first selected) - selected (->> changes + new-selected (->> changes :redo-changes (filter #(= (:type %) :add-obj)) (filter #(selected (:old-id %))) (map #(get-in % [:obj :id])) (into (d/ordered-set))) - id-duplicated (first selected)] + dup-frames (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) + (filter #(selected (:old-id %))) + (filter #(= :frame (get-in % [:obj :type]))) + (map #(vector (:old-id %) (get-in % [:obj :id])))) + + id-duplicated (first new-selected)] ;; Warning: This order is important for the focus mode. - (rx/of (dch/commit-changes changes) - (select-shapes selected) - (memorize-duplicated id-original id-duplicated))))))))) + (rx/merge + (->> (rx/from dup-frames) + (rx/map (fn [[old-id new-id]] (dwt/duplicate-thumbnail old-id new-id)))) + (rx/of (dch/commit-changes changes) + (select-shapes new-selected) + (memorize-duplicated id-original id-duplicated)) + )))))))) (defn change-hover-state [id value] diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs new file mode 100644 index 000000000..78b860e3e --- /dev/null +++ b/frontend/src/app/main/data/workspace/thumbnails.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) UXBOX Labs SL + +(ns app.main.data.workspace.thumbnails + (:require + [app.common.data :as d] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.refs :as refs] + [app.main.repo :as rp] + [app.main.store :as st] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn force-render-stream [id] + (->> st/stream + (rx/filter (ptk/type? ::force-render)) + (rx/map deref) + (rx/filter #(= % id)) + (rx/take 1))) + +(defn update-thumbnail + [id data] + (let [lock (uuid/next)] + (ptk/reify ::update-thumbnail + IDeref + (-deref [_] {:id id :data data}) + + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-file :thumbnails id] data) + (cond-> (nil? (get-in state [::update-thumbnail-lock id])) + (assoc-in [::update-thumbnail-lock id] lock)))) + + ptk/WatchEvent + (watch [_ state stream] + (when (= lock (get-in state [::update-thumbnail-lock id])) + (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize))) + params {:file-id (:current-file-id state) + :object-id id}] + (rx/merge + (->> stream + (rx/take-until stopper) + (rx/filter (ptk/type? ::update-thumbnail)) + (rx/filter #(= id (:id (deref %)))) + (rx/debounce 2000) + (rx/first) + (rx/flat-map + (fn [event] + (let [data (:data @event)] + (rp/mutation! :upsert-file-object-thumbnail (assoc params :data data))))) + + (rx/map #(fn [state] (d/dissoc-in state [::update-thumbnail-lock id])))) + + (->> (rx/of (update-thumbnail id data)) + (rx/observe-on :async))))))))) + +(defn remove-thumbnail + [id] + (ptk/reify ::remove-thumbnail + ptk/UpdateEvent + (update [_ state] + (-> state (d/dissoc-in [:workspace-file :thumbnails id]))) + + ptk/WatchEvent + (watch [_ state _] + (let [params {:file-id (:current-file-id state) + :object-id id + :data nil}] + (->> (rp/mutation! :upsert-file-object-thumbnail params) + (rx/ignore)))))) + +(defn- extract-frame-changes + "Process a changes set in a commit to extract the frames that are changing" + [[event [old-objects new-objects]]] + (let [changes (-> event deref :changes) + + extract-ids + (fn [{type :type :as change}] + (case type + :add-obj [(:id change)] + :mod-obj [(:id change)] + :del-obj [(:id change)] + :reg-objects (:shapes change) + :mov-objects (:shapes change) + [])) + + get-frame-id + (fn [id] + (let [shape (or (get new-objects id) + (get old-objects id))] + + (or (and (= :frame (:type shape)) id) + (:frame-id shape)))) + + ;; Extracts the frames and then removes nils and the root frame + xform (comp (mapcat extract-ids) + (map get-frame-id) + (remove nil?) + (filter #(not= uuid/zero %)) + (filter #(contains? new-objects %)))] + + (into #{} xform changes))) + +(defn thumbnail-change? + "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" + [event] + (let [changes (-> event deref :changes) + + is-thumbnail-op? + (fn [{type :type attr :attr}] + (and (= type :set) + (= attr :thumbnail))) + + is-thumbnail-change? + (fn [change] + (and (= (:type change) :mod-obj) + (->> change :operations (every? is-thumbnail-op?))))] + + (->> changes (every? is-thumbnail-change?)))) + +(defn watch-state-changes [] + (ptk/reify ::watch-state-changes + ptk/WatchEvent + (watch [_ _ stream] + (let [stopper (->> stream + (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (= ::watch-state-changes (ptk/type %))))) + + objects-stream (->> (rx/concat + (rx/of nil) + (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})) + ;; We need to keep the old-objects so we can check the frame for the + ;; deleted objects + (rx/buffer 2 1)) + + frame-changes (->> stream + (rx/filter dch/commit-changes?) + + ;; Async so we wait for additional side-effects of commit-changes + (rx/observe-on :async) + (rx/filter (comp not thumbnail-change?)) + (rx/with-latest-from objects-stream) + (rx/map extract-frame-changes) + (rx/share))] + + (->> frame-changes + (rx/take-until stopper) + (rx/flat-map + (fn [ids] + (->> (rx/from ids) + (rx/map #(ptk/data-event ::force-render %)))))))))) + +(defn duplicate-thumbnail + [old-id new-id] + (ptk/reify ::duplicate-thumbnail + ptk/UpdateEvent + (update [_ state] + (let [old-shape-thumbnail (get-in state [:workspace-file :thumbnails old-id])] + (-> state (assoc-in [:workspace-file :thumbnails new-id] old-shape-thumbnail)))))) + + diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 97a800449..4c12f464f 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -263,6 +263,14 @@ [ids] (l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects =)) +(defn children-objects + [id] + (l/derived + (fn [objects] + (let [children-ids (get-in objects [id :shapes])] + (into [] (keep (d/getf objects)) children-ids))) + workspace-page-objects =)) + (def workspace-page-options (l/derived :options workspace-page)) @@ -386,3 +394,10 @@ (l/derived (fn [state] (dm/get-in state [:viewer-local :fullscreen?])) st/state)) + +(def thumbnail-data + (l/derived #(get-in % [:workspace-file :thumbnails] {}) st/state)) + +(defn thumbnail-frame-data + [frame-id] + (l/derived #(get % frame-id) thumbnail-data)) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 5208555d7..0d371ce49 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -58,38 +58,38 @@ (defn frame-shape [shape-wrapper] (mf/fnc frame-shape - {::mf/wrap-props false} - [props] - (let [childs (unchecked-get props "childs") - shape (unchecked-get props "shape") - {:keys [x y width height]} shape + {::mf/wrap-props false} + [props] + (let [childs (unchecked-get props "childs") + shape (unchecked-get props "shape") + {:keys [x y width height]} shape - transform (gsh/transform-matrix shape) + transform (gsh/transform-matrix shape) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :transform (str transform) - :width width - :height height - :className "frame-background"})) - path? (some? (.-d props)) - render-id (mf/use-ctx muc/render-ctx)] + props (-> (attrs/extract-style-attrs shape) + (obj/merge! + #js {:x x + :y y + :transform (str transform) + :width width + :height height + :className "frame-background"})) + path? (some? (.-d props)) + render-id (mf/use-ctx muc/render-ctx)] - [:* - [:g {:clip-path (frame-clip-url shape render-id)} - [:* - [:& shape-fills {:shape shape} - (if path? - [:> :path props] - [:> :rect props])] + [:* + [:g {:clip-path (frame-clip-url shape render-id)} + [:* + [:& shape-fills {:shape shape} + (if path? + [:> :path props] + [:> :rect props])] - (for [item childs] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}]) - [:& shape-strokes {:shape shape} - (if path? - [:> :path props] - [:> :rect props])]]]]))) + (for [item childs] + [:& shape-wrapper {:shape item + :key (dm/str (:id item))}]) + [:& shape-strokes {:shape shape} + (if path? + [:> :path props] + [:> :rect props])]]]]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 8142c059b..fa7f9c06a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,343 +6,110 @@ (ns app.main.ui.workspace.shapes.frame (:require - [app.common.colors :as cc] [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] - [app.common.math :as mth] + [app.common.pages.helpers :as cph] + [app.main.data.workspace.thumbnails :as dwt] [app.main.refs :as refs] [app.main.ui.hooks :as hooks] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text.fontfaces :as ff] - [app.main.ui.workspace.viewport.utils :as utils] - [app.util.globals :as globals] - [app.util.object :as obj] - [app.util.timers :as ts] + [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as fdm] + [app.main.ui.workspace.shapes.frame.node-store :as fns] + [app.main.ui.workspace.shapes.frame.thumbnail-render :as ftr] [beicon.core :as rx] [rumext.alpha :as mf])) -(defn check-frame-props - "Checks for changes in the props of a frame" - [new-props old-props] - (let [new-shape (unchecked-get new-props "shape") - old-shape (unchecked-get old-props "shape") - - new-thumbnail? (unchecked-get new-props "thumbnail?") - old-thumbnail? (unchecked-get old-props "thumbnail?") - - new-objects (unchecked-get new-props "objects") - old-objects (unchecked-get old-props "objects") - - new-children (->> new-shape :shapes (mapv #(get new-objects %))) - old-children (->> old-shape :shapes (mapv #(get old-objects %)))] - (and (= new-shape old-shape) - (= new-thumbnail? old-thumbnail?) - (= new-children old-children)))) - -(mf/defc frame-placeholder - {::mf/wrap-props false} - [props] - (let [{:keys [x y width height fill-color] :as shape} (obj/get props "shape")] - (if (some? (:thumbnail shape)) - [:& frame/frame-thumbnail {:shape shape}] - [:rect.frame-thumbnail {:x x :y y :width width :height height :style {:fill (or fill-color cc/white)}}]))) - -(defn custom-deferred - [component] - (mf/fnc deferred - {::mf/wrap-props false} - [props] - (let [shape (-> (obj/get props "shape") - (select-keys [:x :y :width :height]) - (hooks/use-equal-memo)) - - tmp (mf/useState false) - ^boolean render? (aget tmp 0) - ^js set-render (aget tmp 1) - prev-shape-ref (mf/use-ref shape)] - - (mf/use-effect - (mf/deps shape) - (fn [] - (mf/set-ref-val! prev-shape-ref shape) - (set-render false))) - - (mf/use-effect - (mf/deps render? shape) - (fn [] - (when-not render? - (let [sem (ts/schedule-on-idle #(set-render true))] - #(rx/dispose! sem))))) - - (if (and render? (= shape (mf/ref-val prev-shape-ref))) - (mf/jsx component props mf/undefined) - (mf/jsx frame-placeholder props mf/undefined))))) - -(defn use-node-store - [thumbnail? node-ref rendered?] - - (let [;; when `true` the node is in memory - in-memory? (mf/use-var nil) - - ;; State just for re-rendering - re-render (mf/use-state 0) - - parent-ref (mf/use-var nil) - - on-frame-load - (mf/use-callback - (fn [node] - (when (and (some? node) (nil? @node-ref)) - (let [content (.createElementNS globals/document "http://www.w3.org/2000/svg" "g")] - (.appendChild node content) - (reset! node-ref content) - (reset! parent-ref node) - (swap! re-render inc)))))] - - (mf/use-effect - (mf/deps thumbnail?) - (fn [] - (when (and (some? @parent-ref) (some? @node-ref) @rendered? thumbnail?) - (.removeChild @parent-ref @node-ref) - (reset! in-memory? true)) - - (when (and (some? @node-ref) @in-memory? (not thumbnail?)) - (.appendChild @parent-ref @node-ref) - (reset! in-memory? false)))) - - on-frame-load)) - -(defn use-render-thumbnail - [{:keys [x y width height] :as shape} node-ref rendered? thumbnail? thumbnail-data] - - (let [frame-canvas-ref (mf/use-ref nil) - frame-image-ref (mf/use-ref nil) - - fixed-width (mth/clamp (:width shape) 250 2000) - fixed-height (/ (* (:height shape) fixed-width) (:width shape)) - - image-url (mf/use-state nil) - observer-ref (mf/use-var nil) - - shape-ref (hooks/use-update-var shape) - - on-image-load - (mf/use-callback - (fn [] - (let [canvas-node (mf/ref-val frame-canvas-ref) - img-node (mf/ref-val frame-image-ref) - - canvas-context (.getContext canvas-node "2d") - canvas-width (.-width canvas-node) - canvas-height (.-height canvas-node)] - (.clearRect canvas-context 0 0 canvas-width canvas-height) - (.rect canvas-context 0 0 canvas-width canvas-height) - (set! (.-fillStyle canvas-context) "#FFFFFF") - (.fill canvas-context) - (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) - - (let [data (.toDataURL canvas-node "image/jpg" 1)] - (reset! thumbnail-data data)) - (reset! image-url nil)))) - - on-change - (mf/use-callback - (fn [] - (when (some? @node-ref) - (let [node @node-ref] - (ts/schedule-on-idle - #(let [frame-html (-> (js/XMLSerializer.) - (.serializeToString node)) - - {:keys [x y width height]} @shape-ref - svg-node (.createElementNS js/document "http://www.w3.org/2000/svg" "svg") - _ (.setAttribute svg-node "version" "1.1") - _ (.setAttribute svg-node "viewBox" (dm/str x " " y " " width " " height)) - _ (.setAttribute svg-node "width" width) - _ (.setAttribute svg-node "height" height) - _ (unchecked-set svg-node "innerHTML" frame-html) - - xml (-> (js/XMLSerializer.) - (.serializeToString svg-node) - js/encodeURIComponent - js/unescape - js/btoa) - - img-src (str "data:image/svg+xml;base64," xml)] - (reset! image-url img-src))))))) - - on-load-frame-dom - (mf/use-callback - (fn [node] - (when (and (some? node) (nil? @observer-ref)) - (let [observer (js/MutationObserver. on-change)] - (.observe observer node #js {:childList true :attributes true :characterData true :subtree true}) - (reset! observer-ref observer))) - - ;; First time rendered if the thumbnail is not present we create it - (when (not thumbnail?) (on-change []))))] - - (mf/use-effect - (fn [] - #(when (and (some? @node-ref) @rendered?) - (mf/unmount @node-ref) - (reset! node-ref nil) - (reset! rendered? false) - (when (some? @observer-ref) - (.disconnect @observer-ref) - (reset! observer-ref nil))))) - - [on-load-frame-dom - (when (some? @image-url) - (mf/html - [:g.thumbnail-rendering - [:foreignObject {:opacity 0 :x x :y y :width width :height height} - [:canvas {:ref frame-canvas-ref - :width fixed-width - :height fixed-height}]] - - [:image {:opacity 0 - :ref frame-image-ref - :x (:x shape) - :y (:y shape) - :xlinkHref @image-url - :width (:width shape) - :height (:height shape) - :on-load on-image-load}]]))])) - -(defn use-dynamic-modifiers - [shape objects node-ref] - - (let [frame-modifiers-ref - (mf/use-memo - (mf/deps (:id shape)) - #(refs/workspace-modifiers-by-frame-id (:id shape))) - - modifiers (mf/deref frame-modifiers-ref) - - transforms - (mf/use-memo - (mf/deps modifiers) - (fn [] - (when (some? modifiers) - (d/mapm (fn [id {modifiers :modifiers}] - (let [center (gsh/center-shape (get objects id))] - (gsh/modifiers->transform center modifiers))) - modifiers)))) - - shapes - (mf/use-memo - (mf/deps transforms) - (fn [] - (->> (keys transforms) - (mapv (d/getf objects))))) - - prev-shapes (mf/use-var nil) - prev-modifiers (mf/use-var nil) - prev-transforms (mf/use-var nil)] - - (mf/use-layout-effect - (mf/deps transforms) - (fn [] - (when (and (nil? @prev-transforms) - (some? transforms)) - (utils/start-transform! @node-ref shapes)) - - (when (some? modifiers) - (utils/update-transform! @node-ref shapes transforms modifiers)) - - (when (and (some? @prev-modifiers) - (empty? modifiers)) - (utils/remove-transform! @node-ref @prev-shapes)) - - (reset! prev-modifiers modifiers) - (reset! prev-transforms transforms) - (reset! prev-shapes shapes))))) - -(defn frame-shape-factory-roots +(defn frame-shape-factory [shape-wrapper] - (let [frame-shape (frame/frame-shape shape-wrapper)] - (mf/fnc inner-frame-shape - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "childs" "fonts" "thumbnail?"]))] - ::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs") - thumbnail? (unchecked-get props "thumbnail?") - fonts (unchecked-get props "fonts") - objects (unchecked-get props "objects") + (mf/fnc frame-shape-inner + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "fonts"]))] + ::mf/wrap-props false + ::mf/forward-ref true} + [props ref] - thumbnail-data (mf/use-state nil) + (let [shape (unchecked-get props "shape") + fonts (unchecked-get props "fonts") + childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) + childs (mf/deref childs-ref)] - thumbnail? (and thumbnail? - (or (some? (:thumbnail shape)) - (some? @thumbnail-data))) - - - ;; References to the current rendered node and the its parentn - node-ref (mf/use-var nil) - - ;; when `true` we've called the mount for the frame - rendered? (mf/use-var false) - - [on-load-frame-dom thumb-renderer] - (use-render-thumbnail shape node-ref rendered? thumbnail? thumbnail-data) - - on-frame-load - (use-node-store thumbnail? node-ref rendered?)] - - (use-dynamic-modifiers shape objects node-ref) - - (when (and (some? @node-ref) (or @rendered? (not thumbnail?))) - (mf/mount - (mf/html - [:& (mf/provider embed/context) {:value true} - [:> shape-container #js {:shape shape :ref on-load-frame-dom} - [:& ff/fontfaces-style {:fonts fonts}] - [:> frame-shape {:shape shape - :childs childs} ]]]) - @node-ref) - (when (not @rendered?) (reset! rendered? true))) - - [:* - (when thumbnail? - [:> frame/frame-thumbnail {:shape (cond-> shape - (some? @thumbnail-data) - (assoc :thumbnail @thumbnail-data))}]) - - [:g.frame-container {:key "frame-container" - :ref on-frame-load}] - thumb-renderer])))) + [:& (mf/provider embed/context) {:value true} + [:> shape-container #js {:shape shape :ref ref} + [:& ff/fontfaces-style {:fonts fonts}] + [:> frame-shape {:shape shape :childs childs} ]]])))) (defn frame-wrapper-factory [shape-wrapper] - (let [frame-shape (frame-shape-factory-roots shape-wrapper)] + + (let [frame-shape (frame-shape-factory shape-wrapper)] (mf/fnc frame-wrapper - {::mf/wrap [#(mf/memo' % check-frame-props)] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "thumbnail?" "objects"]))] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - objects (unchecked-get props "objects") - thumbnail? (unchecked-get props "thumbnail?") + (let [shape (unchecked-get props "shape") + thumbnail? (unchecked-get props "thumbnail?") + objects (unchecked-get props "objects") - children - (-> (mapv (d/getf objects) (:shapes shape)) - (hooks/use-equal-memo)) + objects (mf/use-memo + (mf/deps objects) + #(cph/get-frame-objects objects (:id shape))) - fonts - (-> (ff/frame->fonts shape objects) - (hooks/use-equal-memo))] + objects (hooks/use-equal-memo objects) - [:g.frame-wrapper {:display (when (:hidden shape) "none")} - [:& frame-shape - {:key (str (:id shape)) - :shape shape - :fonts fonts - :childs children - :objects objects - :thumbnail? thumbnail?}]])))) + fonts (mf/use-memo (mf/deps shape objects) #(ff/frame->fonts shape objects)) + fonts (-> fonts (hooks/use-equal-memo)) + + force-render (mf/use-state false) + + ;; Thumbnail data + frame-id (:id shape) + thumbnail-data-ref (mf/use-memo (mf/deps frame-id) #(refs/thumbnail-frame-data frame-id)) + thumbnail-data (mf/deref thumbnail-data-ref) + thumbnail? (and thumbnail? (or (some? (:thumbnail shape)) (some? thumbnail-data))) + + ;; References to the current rendered node and the its parentn + node-ref (mf/use-var nil) + + ;; when `true` we've called the mount for the frame + rendered? (mf/use-var false) + + modifiers (fdm/use-dynamic-modifiers shape objects node-ref) + + disable? (d/not-empty? (get-in modifiers [(:id shape) :modifiers])) + + [on-load-frame-dom thumb-renderer] + (ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable?) + + on-frame-load + (fns/use-node-store thumbnail? node-ref rendered?)] + + (mf/use-effect + (fn [] + ;; When a change in the data is received a "force-render" event is emited + ;; that will force the component to be mounted in memory + (->> (dwt/force-render-stream (:id shape)) + (rx/take-while #(not @rendered?)) + (rx/subs #(reset! force-render true))))) + + (mf/use-effect + (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render) + (fn [] + (when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render)) + (mf/mount + (mf/element frame-shape + #js {:ref on-load-frame-dom :shape shape :fonts fonts}) + + @node-ref) + (when (not @rendered?) (reset! rendered? true))))) + + [:g.frame-container {:key "frame-container" + :ref on-frame-load} + thumb-renderer + + [:g.frame-thumbnail + [:> frame/frame-thumbnail {:shape (cond-> shape + (some? thumbnail-data) + (assoc :thumbnail thumbnail-data))}]]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs new file mode 100644 index 000000000..1306b6cbb --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -0,0 +1,63 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.frame.dynamic-modifiers + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.main.refs :as refs] + [app.main.ui.workspace.viewport.utils :as utils] + [rumext.alpha :as mf])) + +(defn use-dynamic-modifiers + [shape objects node-ref] + + (let [frame-modifiers-ref + (mf/use-memo + (mf/deps (:id shape)) + #(refs/workspace-modifiers-by-frame-id (:id shape))) + + modifiers (mf/deref frame-modifiers-ref) + + transforms + (mf/use-memo + (mf/deps modifiers) + (fn [] + (when (some? modifiers) + (d/mapm (fn [id {modifiers :modifiers}] + (let [center (gsh/center-shape (get objects id))] + (gsh/modifiers->transform center modifiers))) + modifiers)))) + + shapes + (mf/use-memo + (mf/deps transforms) + (fn [] + (->> (keys transforms) + (mapv (d/getf objects))))) + + prev-shapes (mf/use-var nil) + prev-modifiers (mf/use-var nil) + prev-transforms (mf/use-var nil)] + + (mf/use-layout-effect + (mf/deps transforms) + (fn [] + (when (and (nil? @prev-transforms) + (some? transforms)) + (utils/start-transform! @node-ref shapes)) + + (when (some? modifiers) + (utils/update-transform! @node-ref shapes transforms modifiers)) + + (when (and (some? @prev-modifiers) + (empty? modifiers)) + (utils/remove-transform! @node-ref @prev-shapes)) + + (reset! prev-modifiers modifiers) + (reset! prev-transforms transforms) + (reset! prev-shapes shapes))) + modifiers)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs new file mode 100644 index 000000000..f5b73c68f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs @@ -0,0 +1,47 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.frame.node-store + (:require + [app.util.dom :as dom] + [app.util.globals :as globals] + [rumext.alpha :as mf])) + +(defn use-node-store + "Hook responsible of storing the rendered DOM node in memory while not being used" + [thumbnail? node-ref rendered?] + + (let [;; when `true` the node is in memory + in-memory? (mf/use-var true) + + ;; State just for re-rendering + re-render (mf/use-state 0) + + parent-ref (mf/use-var nil) + + on-frame-load + (mf/use-callback + (fn [node] + (when (and (some? node) (nil? @node-ref)) + (let [content (-> (.createElementNS globals/document "http://www.w3.org/2000/svg" "g") + (dom/add-class! "frame-content"))] + ;;(.appendChild node content) + (reset! node-ref content) + (reset! parent-ref node) + (swap! re-render inc)))))] + + (mf/use-effect + (mf/deps thumbnail?) + (fn [] + (when (and (some? @parent-ref) (some? @node-ref) @rendered? thumbnail?) + (.removeChild @parent-ref @node-ref) + (reset! in-memory? true)) + + (when (and (some? @node-ref) @in-memory? (not thumbnail?)) + (.appendChild @parent-ref @node-ref) + (reset! in-memory? false)))) + + on-frame-load)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs new file mode 100644 index 000000000..0fd75996f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -0,0 +1,119 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.frame.thumbnail-render + (:require + [app.common.data.macros :as dm] + [app.common.math :as mth] + [app.main.data.workspace :as dw] + [app.main.store :as st] + [app.main.ui.hooks :as hooks] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.util.timers :as ts] + [rumext.alpha :as mf])) + +(defn- draw-thumbnail-canvas + [canvas-node img-node] + (let [canvas-context (.getContext canvas-node "2d") + canvas-width (.-width canvas-node) + canvas-height (.-height canvas-node)] + (.clearRect canvas-context 0 0 canvas-width canvas-height) + (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) + (.toDataURL canvas-node "image/jpg" 1))) + +(defn use-render-thumbnail + "Hook that will create the thumbnail thata" + [{:keys [id x y width height] :as shape} node-ref rendered? thumbnail? disable?] + + (let [frame-canvas-ref (mf/use-ref nil) + frame-image-ref (mf/use-ref nil) + + disable-ref? (mf/use-var disable?) + + fixed-width (mth/clamp (:width shape) 250 2000) + fixed-height (/ (* (:height shape) fixed-width) (:width shape)) + + image-url (mf/use-state nil) + observer-ref (mf/use-var nil) + + shape-ref (hooks/use-update-var shape) + + thumbnail-ref? (mf/use-var thumbnail?) + + on-image-load + (mf/use-callback + (fn [] + (let [canvas-node (mf/ref-val frame-canvas-ref) + img-node (mf/ref-val frame-image-ref) + thumb-data (draw-thumbnail-canvas canvas-node img-node)] + (st/emit! (dw/update-thumbnail id thumb-data)) + (reset! image-url nil)))) + + on-change + (mf/use-callback + (fn [] + (when (and (some? @node-ref) (not @disable-ref?)) + (let [node @node-ref] + (ts/schedule-on-idle + #(let [frame-html (dom/node->xml node) + {:keys [x y width height]} @shape-ref + svg-node + (-> (dom/make-node "http://www.w3.org/2000/svg" "svg") + (dom/set-property! "version" "1.1") + (dom/set-property! "viewBox" (dm/str x " " y " " width " " height)) + (dom/set-property! "width" width) + (dom/set-property! "height" height) + (obj/set! "innerHTML" frame-html)) + + img-src (-> svg-node dom/node->xml dom/svg->data-uri)] + (reset! image-url img-src))))))) + + on-load-frame-dom + (mf/use-callback + (fn [node] + (when (and (some? node) (nil? @observer-ref)) + (on-change []) + (let [observer (js/MutationObserver. on-change)] + (.observe observer node #js {:childList true :attributes true :characterData true :subtree true}) + (reset! observer-ref observer)))))] + + (mf/use-effect + (mf/deps disable?) + (fn [] + (reset! disable-ref? disable?))) + + (mf/use-effect + (mf/deps thumbnail?) + (fn [] + (reset! thumbnail-ref? thumbnail?))) + + (mf/use-effect + (fn [] + #(when (and (some? @node-ref) @rendered?) + (mf/unmount @node-ref) + (reset! node-ref nil) + (reset! rendered? false) + (when (some? @observer-ref) + (.disconnect @observer-ref) + (reset! observer-ref nil))))) + + [on-load-frame-dom + (when (some? @image-url) + (mf/html + [:g.thumbnail-rendering {:opacity 0} + [:foreignObject {:x x :y y :width width :height height} + [:canvas {:ref frame-canvas-ref + :width fixed-width + :height fixed-height}]] + + [:image {:ref frame-image-ref + :x (:x shape) + :y (:y shape) + :xlinkHref @image-url + :width (:width shape) + :height (:height shape) + :on-load on-image-load}]]))])) diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index 0e7c38883..ff83b4228 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -31,7 +31,7 @@ ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) + childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) childs (mf/deref childs-ref)] [:> shape-container {:shape shape} diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index 7a3d7b1a4..2a09d2bfc 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -20,9 +20,9 @@ ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) + childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) childs (mf/deref childs-ref) - svg-tag (get-in shape [:content :tag])] + svg-tag (get-in shape [:content :tag])] (if (contains? usvg/svg-group-safe-tags svg-tag) [:> shape-container {:shape shape} [:& svg-raw-shape {:shape shape diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs index dd9761822..440188c1b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs @@ -21,6 +21,7 @@ [app.util.object :as obj] [app.util.text-editor :as ted] [app.util.text-svg-position :as utp] + [app.util.timers :as ts] [rumext.alpha :as mf])) (defn- update-with-editor-state @@ -39,7 +40,8 @@ (mf/defc text-container {::mf/wrap-props false - ::mf/wrap [mf/memo]} + ::mf/wrap [mf/memo + #(mf/deferred % ts/idle-then-raf)]} [props] (let [shape (obj/get props "shape") @@ -97,9 +99,11 @@ (dissoc :position-data)))) changed-texts - (->> (keys text-shapes) - (filter text-change?) - (map (d/getf text-shapes)))] + (mf/use-memo + (mf/deps text-shapes) + #(->> (keys text-shapes) + (filter text-change?) + (map (d/getf text-shapes))))] (for [{:keys [id] :as shape} changed-texts] [:& text-container {:shape (dissoc shape :transform :transform-inverse) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 94313a3b2..16941ad06 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -181,7 +181,6 @@ (hooks/setup-keyboard alt? mod? space?) (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids @hover-disabled? focus zoom) (hooks/setup-viewport-modifiers modifiers base-objects) - (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames base-objects vbox hover active-frames zoom) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index d0eae1ddf..7fe09003c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -10,6 +10,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.uuid :as uuid] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] [app.main.data.workspace.path.shortcuts :as psc] @@ -198,7 +199,22 @@ (defn setup-viewport-modifiers [modifiers objects] - (let [transforms + + (let [root-frame-ids + (mf/use-memo + (mf/deps objects) + #(->> objects + (vals) + (filter (fn [{:keys [type frame-id]}] + (and + (not= :frame type) + (= uuid/zero frame-id)))) + (map :id))) + + objects (select-keys objects root-frame-ids) + modifiers (select-keys modifiers root-frame-ids) + + transforms (mf/use-memo (mf/deps modifiers) (fn [] @@ -231,7 +247,7 @@ (when (some? modifiers) (utils/update-transform! globals/document shapes transforms modifiers)) - + (when (and (some? @prev-modifiers) (not (some? modifiers))) (utils/remove-transform! globals/document @prev-shapes)) diff --git a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs deleted file mode 100644 index 634c54c89..000000000 --- a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs +++ /dev/null @@ -1,161 +0,0 @@ -;; 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) UXBOX Labs SL - -(ns app.main.ui.workspace.viewport.thumbnail-renderer - (:require - [app.common.math :as mth] - [app.main.data.workspace.persistence :as dwp] - [app.main.store :as st] - [app.util.dom :as dom] - [app.util.object :as obj] - [app.util.timers :as timers] - [beicon.core :as rx] - [rumext.alpha :as mf])) - -(mf/defc frame-thumbnail - "Renders the canvas and image for a frame thumbnail and stores its value into the shape" - [{:keys [shape background on-thumbnail-data on-frame-not-found]}] - - (let [thumbnail-img (mf/use-ref nil) - thumbnail-canvas (mf/use-ref nil) - - {:keys [width height]} shape - fixed-width (mth/clamp width 250 2000) - fixed-height (/ (* height fixed-width) width) - - on-dom-rendered - (mf/use-callback - (mf/deps (:id shape)) - (fn [node] - (when node - (let [img-node (mf/ref-val thumbnail-img)] - (timers/schedule-on-idle - #(let [frame-node (dom/get-element (str "shape-" (:id shape))) - thumb-node (dom/query frame-node ".frame-thumbnail") - loading-node (dom/query frame-node "[data-loading=\"true\"]")] - (if (and (some? frame-node) - ;; Not render if the thumbnail is in display - (nil? thumb-node) - ;; Not render if some image is still loading - (nil? loading-node)) - (let [frame-html (-> (js/XMLSerializer.) - (.serializeToString frame-node)) - - ;; We need to wrap the group node into a SVG with a viewbox that matches the selrect of the frame - svg-node (.createElementNS js/document "http://www.w3.org/2000/svg" "svg") - _ (.setAttribute svg-node "version" "1.1") - _ (.setAttribute svg-node "viewBox" (str (:x shape) " " (:y shape) " " (:width shape) " " (:height shape))) - _ (.setAttribute svg-node "width" (:width shape)) - _ (.setAttribute svg-node "height" (:height shape)) - _ (unchecked-set svg-node "innerHTML" frame-html) - xml (-> (js/XMLSerializer.) - (.serializeToString svg-node) - js/encodeURIComponent - js/unescape - js/btoa) - img-src (str "data:image/svg+xml;base64," xml)] - (obj/set! img-node "src" img-src)) - - (on-frame-not-found (:id shape))))))))) - - on-image-load - (mf/use-callback - (mf/deps on-thumbnail-data background) - (fn [] - (let [canvas-node (mf/ref-val thumbnail-canvas) - img-node (mf/ref-val thumbnail-img) - - canvas-context (.getContext canvas-node "2d") - canvas-width (.-width canvas-node) - canvas-height (.-height canvas-node) - - _ (.clearRect canvas-context 0 0 canvas-width canvas-height) - _ (.rect canvas-context 0 0 canvas-width canvas-height) - _ (set! (.-fillStyle canvas-context) background) - _ (.fill canvas-context) - _ (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) - - data (.toDataURL canvas-node "image/jpg" 1)] - (on-thumbnail-data data))))] - - [:div.frame-renderer {:ref on-dom-rendered - :style {:display "none"}} - [:img.thumbnail-img - {:ref thumbnail-img - :width width - :height height - :on-load on-image-load}] - - [:canvas.thumbnail-canvas - {:ref thumbnail-canvas - :width fixed-width - :height fixed-height}]])) - -(mf/defc frame-renderer - "Component in charge of creating thumbnails and storing them" - {::mf/wrap-props false} - [props] - (let [objects (obj/get props "objects") - background (obj/get props "background") - - ;; Id of the current frame being rendered - shape-id (mf/use-state nil) - - ;; This subject will emit a value every time there is a free "slot" to render - ;; a thumbnail - next (mf/use-memo #(rx/behavior-subject :next)) - - render-frame - (mf/use-callback - (fn [frame-id] - (reset! shape-id frame-id))) - - updates-stream - (mf/use-memo - #(let [update-events (rx/filter dwp/update-frame-thumbnail? st/stream)] - (->> (rx/zip update-events next) - (rx/map first)))) - - on-thumbnail-data - (mf/use-callback - (mf/deps @shape-id) - (fn [data] - (reset! shape-id nil) - (timers/schedule - (fn [] - (st/emit! (dwp/update-shape-thumbnail @shape-id data)) - (rx/push! next :next))))) - - on-frame-not-found - (mf/use-callback - (fn [frame-id] - ;; If we couldn't find the frame maybe is still rendering. We push the event again - ;; after a time - (reset! shape-id nil) - (rx/push! next :next) - (timers/schedule-on-idle - 100 - (st/emitf (dwp/update-frame-thumbnail frame-id)))))] - - (mf/use-effect - (mf/deps render-frame) - (fn [] - (let [sub (->> updates-stream - (rx/subs #(render-frame (-> (deref %) :frame-id))))] - - #(rx/dispose! sub)))) - - (mf/use-layout-effect - (fn [] - (timers/schedule-on-idle - #(st/emit! (dwp/watch-state-changes))))) - - (when (and (some? @shape-id) (contains? objects @shape-id)) - [:& frame-thumbnail {:key (str "thumbnail-" @shape-id) - :shape (get objects @shape-id) - :background background - :on-thumbnail-data on-thumbnail-data - :on-frame-not-found on-frame-not-found}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 8d88e5c27..c2d0e8a3c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -86,7 +86,7 @@ mask? (and group? masked-group?) ;; When the shape is a frame we maybe need to move its thumbnail - thumb-node (when frame? (dom/query base-node (str "#thumbnail-" id)))] + thumb-node (when frame? (dom/query (str "#thumbnail-" id)))] (cond frame? diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 76afe90a5..470b302ff 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -341,17 +341,41 @@ {:pre [(blob? b)]} (js/URL.createObjectURL b)) +(defn make-node + ([namespace name] + (.createElementNS globals/document namespace name)) + + ([name] + (.createElement globals/document name))) + +(defn node->xml + [node] + (-> (js/XMLSerializer.) + (.serializeToString node))) + +(defn svg->data-uri + [svg] + (assert (string? svg)) + (let [b64 (-> svg + js/encodeURIComponent + js/unescape + js/btoa)] + (dm/str "data:image/svg+xml;base64," b64))) + (defn set-property! [^js node property value] (when (some? node) - (.setAttribute node property value))) + (.setAttribute node property value)) + node) (defn set-text! [^js node text] (when (some? node) - (set! (.-textContent node) text))) + (set! (.-textContent node) text)) + node) (defn set-css-property! [^js node property value] (when (some? node) - (.setProperty (.-style ^js node) property value))) + (.setProperty (.-style ^js node) property value)) + node) (defn capture-pointer [^js event] (when (some? event) @@ -382,7 +406,8 @@ (defn add-class! [^js node class-name] (when (some? node) (let [class-list (.-classList ^js node)] - (.add ^js class-list class-name)))) + (.add ^js class-list class-name))) + node) (defn remove-class! [^js node class-name] (when (some? node)