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:
parent
b576ef02af
commit
6a3a460203
19 changed files with 613 additions and 662 deletions
|
@ -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)))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
166
frontend/src/app/main/data/workspace/thumbnails.cljs
Normal file
166
frontend/src/app/main/data/workspace/thumbnails.cljs
Normal 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))))))
|
||||
|
||||
|
|
@ -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))
|
||||
|
|
|
@ -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])]]]])))
|
||||
|
||||
|
|
|
@ -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))}]]]))))
|
||||
|
|
|
@ -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))
|
|
@ -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))
|
|
@ -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}]]))]))
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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}])))
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue