🎉 Allow masked groups

This commit is contained in:
Andrés Moya 2020-10-15 10:59:23 +02:00 committed by Alonso Torres
parent ad66955a54
commit ee89b2e7f4
14 changed files with 207 additions and 33 deletions

View file

@ -245,7 +245,8 @@
:internal.shape/height :internal.shape/height
:internal.shape/interactions :internal.shape/interactions
:internal.shape/selrect :internal.shape/selrect
:internal.shape/points])) :internal.shape/points
:internal.shape/masked-group?]))
(def component-sync-attrs {:fill-color :fill-group (def component-sync-attrs {:fill-color :fill-group
:fill-color-ref-file :fill-group :fill-color-ref-file :fill-group
@ -270,7 +271,8 @@
:height :size-group :height :size-group
:proportion :size-group :proportion :size-group
:rx :radius-group :rx :radius-group
:ry :radius-group}) :ry :radius-group
:masked-group? :mask-group})
(s/def ::minimal-shape (s/def ::minimal-shape
(s/keys :req-un [::type ::name] (s/keys :req-un [::type ::name]

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path fill-rule="evenodd" clip-rule="evenodd" d="M452 250c0 150-200 250-200 250S52 400 52 250V75L252 0l200 75zM259 388c43 0 80-27 93-63H165c14 36 50 63 94 63zM102 200c9-29 34-50 63-50s53 21 62 50zm238-50c-29 0-54 21-63 50h125c-9-29-33-50-62-50z"/>
</svg>

After

Width:  |  Height:  |  Size: 320 B

View file

@ -144,6 +144,47 @@
} }
} }
.element-list li.masked {
.element-children {
li:first-child {
position: relative;
&::before {
content: "^";
font-size: $fs18;
font-family: opensans;
font-weight: lighter;
position: absolute;
top: -5px;
left: -5px;
}
}
li:last-child {
border-left: none;
position: relative;
&::before {
content: " ";
border-left: 1px solid $color-gray-40;
height: 1rem;
position: absolute;
top: 0;
left: 0;
}
&::after {
content: " ";
border-bottom: 1px solid $color-gray-40;
width: 0.3rem;
position: absolute;
top: 1rem;
left: 0;
}
}
}
}
.element-icon { .element-icon {
svg { svg {
fill: $color-gray-30; fill: $color-gray-30;

View file

@ -158,7 +158,7 @@ $width-settings-bar: 16rem;
ul { ul {
border-left: 9px solid $color-gray-50; border-left: 9px solid $color-gray-50;
margin: 0; margin: 0 0 0 0.4rem;
li { li {
border-left: 1px solid $color-gray-40; border-left: 1px solid $color-gray-40;

View file

@ -1369,6 +1369,69 @@
(dws/prepare-remove-group page-id group objects)] (dws/prepare-remove-group page-id group objects)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))) (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))))
(def mask-group
(ptk/reify ::mask-group
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
shapes (dws/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; create a new group and set it as masked.
[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dws/prepare-create-group page-id shapes "Group-" true))
rchanges (conj rchanges
{:type :mod-obj
:page-id page-id
:id (:id group)
:operations [{:type :set
:attr :masked-group?
:val true}]})
uchanges (conj rchanges
{:type :mod-obj
:page-id page-id
:id (:id group)
:operations [{:type :set
:attr :masked-group?
:val nil}]})]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(def unmask-group
(ptk/reify ::unmask-group
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])]
(when (= (count selected) 1)
(let [group (get objects (first selected))
rchanges [{:type :mod-obj
:page-id page-id
:id (:id group)
:operations [{:type :set
:attr :masked-group?
:val nil}]}]
uchanges [{:type :mod-obj
:page-id page-id
:id (:id group)
:operations [{:type :set
:attr :masked-group?
:val (:masked-group? group)}]}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactions ;; Interactions
@ -1514,6 +1577,7 @@
"+" #(st/emit! (increase-zoom nil)) "+" #(st/emit! (increase-zoom nil))
"-" #(st/emit! (decrease-zoom nil)) "-" #(st/emit! (decrease-zoom nil))
"ctrl+g" #(st/emit! group-selected) "ctrl+g" #(st/emit! group-selected)
"ctrl+shift+m" #(st/emit! mask-group)
"ctrl+k" #(st/emit! dwl/add-component) "ctrl+k" #(st/emit! dwl/add-component)
"shift+g" #(st/emit! ungroup-selected) "shift+g" #(st/emit! ungroup-selected)
"shift+0" #(st/emit! reset-zoom) "shift+0" #(st/emit! reset-zoom)

View file

@ -69,6 +69,7 @@
(def logo-icon (icon-xref :uxbox-logo-icon)) (def logo-icon (icon-xref :uxbox-logo-icon))
(def lowercase (icon-xref :lowercase)) (def lowercase (icon-xref :lowercase))
(def mail (icon-xref :mail)) (def mail (icon-xref :mail))
(def mask (icon-xref :mask))
(def minus (icon-xref :minus)) (def minus (icon-xref :minus))
(def move (icon-xref :move)) (def move (icon-xref :move))
(def msg-error (icon-xref :msg-error)) (def msg-error (icon-xref :msg-error))

View file

@ -9,6 +9,7 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.main.ui.shapes.group :refer [mask-id-ctx]]
[app.util.object :as obj])) [app.util.object :as obj]))
; The SVG standard does not implement yet the 'stroke-alignment' ; The SVG standard does not implement yet the 'stroke-alignment'
@ -23,13 +24,16 @@
base-props (unchecked-get props "base-props") base-props (unchecked-get props "base-props")
elem-name (unchecked-get props "elem-name") elem-name (unchecked-get props "elem-name")
{:keys [x y width height]} (geom/shape->rect-shape shape) {:keys [x y width height]} (geom/shape->rect-shape shape)
mask-id (mf/use-ctx mask-id-ctx)
stroke-id (mf/use-var (uuid/next)) stroke-id (mf/use-var (uuid/next))
stroke-style (:stroke-style shape :none) stroke-style (:stroke-style shape :none)
stroke-position (:stroke-alignment shape :center)] stroke-position (:stroke-alignment shape :center)]
(cond (cond
;; Center alignment (or no stroke): the default in SVG ;; Center alignment (or no stroke): the default in SVG
(or (= stroke-style :none) (= stroke-position :center)) (or (= stroke-style :none) (= stroke-position :center))
[:> elem-name base-props] [:> elem-name (cond-> (obj/merge! #js {} base-props)
(some? mask-id)
(obj/merge! #js {:mask mask-id}))]
;; Inner alignment: display the shape with double width stroke, ;; Inner alignment: display the shape with double width stroke,
;; and clip the result with the original shape without stroke. ;; and clip the result with the original shape without stroke.
@ -49,10 +53,15 @@
shape-props (-> (obj/merge! #js {} base-props) shape-props (-> (obj/merge! #js {} base-props)
(obj/merge! #js {:strokeWidth (* stroke-width 2) (obj/merge! #js {:strokeWidth (* stroke-width 2)
:clipPath (str "url('#" clip-id "')")}))] :clipPath (str "url('#" clip-id "')")}))]
[:* (if (nil? mask-id)
[:> "clipPath" #js {:id clip-id} [:*
[:> elem-name clip-props]] [:> "clipPath" #js {:id clip-id}
[:> elem-name shape-props]]) [:> elem-name clip-props]]
[:> elem-name shape-props]]
[:g {:mask mask-id}
[:> "clipPath" #js {:id clip-id}
[:> elem-name clip-props]]
[:> elem-name shape-props]]))
;; Outer alingmnent: display the shape in two layers. One ;; Outer alingmnent: display the shape in two layers. One
;; without stroke (only fill), and another one only with stroke ;; without stroke (only fill), and another one only with stroke
@ -61,7 +70,7 @@
;; without stroke ;; without stroke
(= stroke-position :outer) (= stroke-position :outer)
(let [mask-id (str "mask-" @stroke-id) (let [stroke-mask-id (str "mask-" @stroke-id)
stroke-width (.-strokeWidth ^js base-props) stroke-width (.-strokeWidth ^js base-props)
mask-props1 (-> (obj/merge! #js {} base-props) mask-props1 (-> (obj/merge! #js {} base-props)
(obj/merge! #js {:stroke "white" (obj/merge! #js {:stroke "white"
@ -89,11 +98,18 @@
(obj/merge! #js {:strokeWidth (* stroke-width 2) (obj/merge! #js {:strokeWidth (* stroke-width 2)
:fill "none" :fill "none"
:fillOpacity 0 :fillOpacity 0
:mask (str "url('#" mask-id "')")}))] :mask (str "url('#" stroke-mask-id "')")}))]
[:* (if (nil? mask-id)
[:mask {:id mask-id} [:*
[:> elem-name mask-props1] [:mask {:id mask-id}
[:> elem-name mask-props2]] [:> elem-name mask-props1]
[:> elem-name shape-props1] [:> elem-name mask-props2]]
[:> elem-name shape-props2]])))) [:> elem-name shape-props1]
[:> elem-name shape-props2]]
[:g {:mask mask-id}
[:mask {:id stroke-mask-id}
[:> elem-name mask-props1]
[:> elem-name mask-props2]]
[:> elem-name shape-props1]
[:> elem-name shape-props2]])))))

View file

@ -10,10 +10,13 @@
(ns app.main.ui.shapes.group (ns app.main.ui.shapes.group
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str]
[app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.attrs :as attrs]
[app.util.debug :refer [debug?]] [app.util.debug :refer [debug?]]
[app.common.geom.shapes :as geom])) [app.common.geom.shapes :as geom]))
(def mask-id-ctx (mf/create-context nil))
(defn group-shape (defn group-shape
[shape-wrapper] [shape-wrapper]
(mf/fnc group-shape (mf/fnc group-shape
@ -22,14 +25,26 @@
(let [frame (unchecked-get props "frame") (let [frame (unchecked-get props "frame")
shape (unchecked-get props "shape") shape (unchecked-get props "shape")
childs (unchecked-get props "childs") childs (unchecked-get props "childs")
mask (if (:masked-group? shape)
(first childs)
nil)
childs (if (:masked-group? shape)
(rest childs)
childs)
is-child-selected? (unchecked-get props "is-child-selected?") is-child-selected? (unchecked-get props "is-child-selected?")
{:keys [id x y width height]} shape {:keys [id x y width height]} shape
transform (geom/transform-matrix shape)] transform (geom/transform-matrix shape)]
[:g [:g
(for [item childs] (when mask
[:& shape-wrapper {:frame frame [:defs
:shape item [:mask {:id (:id mask)}
:key (:id item)}]) [:& shape-wrapper {:frame frame
:shape mask}]]])
[:& (mf/provider mask-id-ctx) {:value (str/fmt "url(#%s)" (:id mask))}
(for [item childs]
[:& shape-wrapper {:frame frame
:shape item
:key (:id item)}])]
(when (not is-child-selected?) (when (not is-child-selected?)
[:rect {:transform transform [:rect {:transform transform
:x x :x x

View file

@ -12,6 +12,7 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.group :refer [mask-id-ctx]]
[app.util.object :as obj])) [app.util.object :as obj]))
(mf/defc icon-shape (mf/defc icon-shape
@ -20,6 +21,7 @@
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
{:keys [id x y width height metadata rotation content]} shape {:keys [id x y width height metadata rotation content]} shape
mask-id (mf/use-ctx mask-id-ctx)
transform (geom/transform-matrix shape) transform (geom/transform-matrix shape)
vbox (apply str (interpose " " (:view-box metadata))) vbox (apply str (interpose " " (:view-box metadata)))
@ -33,6 +35,7 @@
:height height :height height
:viewBox vbox :viewBox vbox
:preserveAspectRatio "none" :preserveAspectRatio "none"
:mask mask-id
:dangerouslySetInnerHTML #js {:__html content}}))] :dangerouslySetInnerHTML #js {:__html content}}))]
[:g {:transform transform} [:g {:transform transform}
[:> "svg" props]])) [:> "svg" props]]))
@ -41,7 +44,9 @@
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(let [{:keys [content id metadata]} shape (let [{:keys [content id metadata]} shape
view-box (apply str (interpose " " (:view-box metadata))) view-box (apply str (interpose " " (:view-box metadata)))
mask-id (mf/use-ctx mask-id-ctx)
props {:viewBox view-box props {:viewBox view-box
:id (str "shape-" id) :id (str "shape-" id)
:mask mask-id
:dangerouslySetInnerHTML #js {:__html content}}] :dangerouslySetInnerHTML #js {:__html content}}]
[:& "svg" props])) [:& "svg" props]))

View file

@ -13,6 +13,7 @@
[app.config :as cfg] [app.config :as cfg]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.group :refer [mask-id-ctx]]
[app.util.object :as obj] [app.util.object :as obj]
[app.main.ui.context :as muc] [app.main.ui.context :as muc]
[app.main.data.fetch :as df] [app.main.data.fetch :as df]
@ -26,6 +27,7 @@
{:keys [id x y width height rotation metadata]} shape {:keys [id x y width height rotation metadata]} shape
uri (cfg/resolve-media-path (:path metadata)) uri (cfg/resolve-media-path (:path metadata))
embed-resources? (mf/use-ctx muc/embed-ctx) embed-resources? (mf/use-ctx muc/embed-ctx)
mask-id (mf/use-ctx mask-id-ctx)
data-uri (mf/use-state (when (not embed-resources?) uri))] data-uri (mf/use-state (when (not embed-resources?) uri))]
(mf/use-effect (mf/use-effect
@ -44,7 +46,8 @@
:id (str "shape-" id) :id (str "shape-" id)
:width width :width width
:height height :height height
:preserveAspectRatio "none"}))] :preserveAspectRatio "none"
:mask mask-id}))]
(if (nil? @data-uri) (if (nil? @data-uri)
[:> "rect" (obj/merge! [:> "rect" (obj/merge!
props props

View file

@ -13,6 +13,7 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.group :refer [mask-id-ctx]]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.util.object :as obj])) [app.util.object :as obj]))
@ -45,6 +46,7 @@
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
background? (unchecked-get props "background?") background? (unchecked-get props "background?")
{:keys [id x y width height]} (geom/shape->rect-shape shape) {:keys [id x y width height]} (geom/shape->rect-shape shape)
mask-id (mf/use-ctx mask-id-ctx)
transform (geom/transform-matrix shape) transform (geom/transform-matrix shape)
pdata (render-path shape) pdata (render-path shape)
props (-> (attrs/extract-style-attrs shape) props (-> (attrs/extract-style-attrs shape)
@ -53,7 +55,7 @@
:id (str "shape-" id) :id (str "shape-" id)
:d pdata}))] :d pdata}))]
(if background? (if background?
[:g [:g {:mask mask-id}
[:path {:stroke "transparent" [:path {:stroke "transparent"
:fill "transparent" :fill "transparent"
:stroke-width "20px" :stroke-width "20px"
@ -63,5 +65,6 @@
:elem-name "path"}]] :elem-name "path"}]]
[:& shape-custom-stroke {:shape shape [:& shape-custom-stroke {:shape shape
:base-props props :base-props props
:mask mask-id
:elem-name "path"}]))) :elem-name "path"}])))

View file

@ -13,6 +13,7 @@
[app.main.data.fetch :as df] [app.main.data.fetch :as df]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.ui.context :as muc] [app.main.ui.context :as muc]
[app.main.ui.shapes.group :refer [mask-id-ctx]]
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
@ -224,6 +225,7 @@
[props] [props]
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
selected? (unchecked-get props "selected?") selected? (unchecked-get props "selected?")
mask-id (mf/use-ctx mask-id-ctx)
{:keys [id x y width height rotation content]} shape] {:keys [id x y width height rotation content]} shape]
[:foreignObject {:x x [:foreignObject {:x x
:y y :y y
@ -231,6 +233,7 @@
:transform (geom/transform-matrix shape) :transform (geom/transform-matrix shape)
:id (str id) :id (str id)
:width width :width width
:height height} :height height
:mask mask-id}
[:& text-content {:content (:content shape)}]])) [:& text-content {:content (:content shape)}]]))

View file

@ -62,6 +62,8 @@
do-unlock-shape #(st/emit! (dw/update-shape-flags id {:blocked false})) do-unlock-shape #(st/emit! (dw/update-shape-flags id {:blocked false}))
do-create-group #(st/emit! dw/group-selected) do-create-group #(st/emit! dw/group-selected)
do-remove-group #(st/emit! dw/ungroup-selected) do-remove-group #(st/emit! dw/ungroup-selected)
do-mask-group #(st/emit! dw/mask-group)
do-unmask-group #(st/emit! dw/unmask-group)
do-add-component #(st/emit! dwl/add-component) do-add-component #(st/emit! dwl/add-component)
do-detach-component #(st/emit! (dwl/detach-component id)) do-detach-component #(st/emit! (dwl/detach-component id))
do-reset-component #(st/emit! (dwl/reset-component id)) do-reset-component #(st/emit! (dwl/reset-component id))
@ -98,14 +100,26 @@
[:& menu-separator] [:& menu-separator]
(when (> (count selected) 1) (when (> (count selected) 1)
[:& menu-entry {:title "Group" [:*
:shortcut "Ctrl + g" [:& menu-entry {:title "Group"
:on-click do-create-group}]) :shortcut "Ctrl + g"
:on-click do-create-group}]
[:& menu-entry {:title "Mask"
:shortcut "Ctrl + Shift + M"
:on-click do-mask-group}]])
(when (and (= (count selected) 1) (= (:type shape) :group)) (when (and (= (count selected) 1) (= (:type shape) :group))
[:& menu-entry {:title "Ungroup" [:*
:shortcut "Shift + g" [:& menu-entry {:title "Ungroup"
:on-click do-remove-group}]) :shortcut "Shift + g"
:on-click do-remove-group}]
(if (:masked-group? shape)
[:& menu-entry {:title "Unmask"
:shortcut "Ctrl + Shift + M"
:on-click do-unmask-group}]
[:& menu-entry {:title "Mask"
:shortcut "Ctrl + Shift + M"
:on-click do-mask-group}])])
(if (:hidden shape) (if (:hidden shape)
[:& menu-entry {:title "Show" [:& menu-entry {:title "Show"

View file

@ -43,9 +43,11 @@
:rect i/box :rect i/box
:curve i/curve :curve i/curve
:text i/text :text i/text
:group (if (nil? (:component-id shape)) :group (if (some? (:component-id shape))
i/folder i/component
i/component) (if (:masked-group? shape)
i/mask
i/folder))
nil)) nil))
;; --- Layer Name ;; --- Layer Name
@ -196,6 +198,7 @@
:ref dref :ref dref
:class (dom/classnames :class (dom/classnames
:component (not (nil? (:component-id item))) :component (not (nil? (:component-id item)))
:masked (:masked-group? item)
:dnd-over (= (:over dprops) :center) :dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top) :dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot) :dnd-over-bot (= (:over dprops) :bot)
@ -307,7 +310,8 @@
:component-file :component-file
:shape-ref :shape-ref
:touched :touched
:metadata])] :metadata
:masked-group?])]
(persistent! (persistent!
(reduce-kv (fn [res id obj] (reduce-kv (fn [res id obj]
(assoc! res id (strip-data obj))) (assoc! res id (strip-data obj)))