0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-10 00:58:26 -05:00

Advanced frame thumbnail handling

This commit is contained in:
alonso.torres 2022-04-19 16:19:14 +02:00
parent b576ef02af
commit 6a3a460203
19 changed files with 613 additions and 662 deletions

View file

@ -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)))

View file

@ -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)

View file

@ -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"

View file

@ -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]

View file

@ -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))))))

View file

@ -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))

View file

@ -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])]]]])))

View file

@ -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))}]]]))))

View file

@ -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))

View file

@ -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))

View file

@ -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}]]))]))

View file

@ -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}

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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}])))

View file

@ -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?

View file

@ -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)