diff --git a/frontend/resources/styles/common/dependencies/helpers.scss b/frontend/resources/styles/common/dependencies/helpers.scss index 92c0c7494..e23f58b39 100644 --- a/frontend/resources/styles/common/dependencies/helpers.scss +++ b/frontend/resources/styles/common/dependencies/helpers.scss @@ -48,6 +48,11 @@ $br-big: 8px; } } +.row-grid-2 { + display: grid; + grid-template-columns: repeat(2, 1fr); +} + .flex-grow { flex-grow: 1; } diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index 9a725c916..c19e4a8cc 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -364,7 +364,8 @@ ul.slider-dots { position: relative; width: 75px; - &::after { + &::after, + .after { color: $color-gray-20; font-size: $fs12; height: 20px; @@ -374,6 +375,11 @@ ul.slider-dots { width: 20px; } + .after { + width: auto; + right: 6px; + } + // Input amounts &.pixels { diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 229bc2408..4e6201959 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -468,8 +468,8 @@ } .color-th { - background-color: $color-gray-10; - border: 1px solid $color-gray-10; + background-color: $color-gray-30; + border: 1px solid $color-gray-30; border-radius: $br-small; cursor: pointer; display: flex; @@ -765,6 +765,7 @@ top: 0; width: 100%; opacity: 0.4; + z-index: 10; } .element-set-content .advanced-options { @@ -775,6 +776,7 @@ position: relative; top: 2px; width: calc(100% + 16px); + z-index: 20; } .btn-options { @@ -815,7 +817,8 @@ } } -.exports-options { +.exports-options, +.shadow-options{ .element-set-options-group { justify-content: space-between; .delete-icon { @@ -841,3 +844,35 @@ margin-top: 10px; } } + +.shadow-options .color-row-wrap { + margin-left: 6px; + margin-top: 0.5rem; +} + +.element-set-actions-button { + display: flex; + min-width: 30px; + min-height: 30px; + justify-content: center; + align-items: center; + cursor: pointer; + svg { + width: 16px; + height: 16px; + fill: $color-gray-20; + } + + &:hover svg { + fill: $color-primary; + } +} +.element-set-actions { + display: flex; + visibility: hidden; + +} + +.element-set-options-group:hover .element-set-actions { + visibility: visible; +} diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 8a2105d67..89061114f 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -11,6 +11,7 @@ "The main logic for SVG export functionality." (:require [rumext.alpha :as mf] + [cuerdas.core :as str] [app.common.uuid :as uuid] [app.common.pages :as cp] [app.common.pages-helpers :as cph] @@ -18,6 +19,7 @@ [app.common.geom.shapes :as geom] [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] + [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.icon :as icon] @@ -77,18 +79,21 @@ frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (geom/transform-shape frame shape) - opts #js {:shape shape}] - (case (:type shape) - :curve [:> path/path-shape opts] - :text [:> text/text-shape opts] - :icon [:> icon/icon-shape opts] - :rect [:> rect/rect-shape opts] - :path [:> path/path-shape opts] - :image [:> image/image-shape opts] - :circle [:> circle/circle-shape opts] - :frame [:> frame-wrapper {:shape shape}] - :group [:> group-wrapper {:shape shape :frame frame}] - nil)))))) + opts #js {:shape shape} + filter-id (filters/get-filter-id)] + [:g {:filter (filters/filter-str filter-id shape)} + [:& filters/filters {:filter-id filter-id :shape shape}] + (case (:type shape) + :curve [:> path/path-shape opts] + :text [:> text/text-shape opts] + :icon [:> icon/icon-shape opts] + :rect [:> rect/rect-shape opts] + :path [:> path/path-shape opts] + :image [:> image/image-shape opts] + :circle [:> circle/circle-shape opts] + :frame [:> frame-wrapper {:shape shape}] + :group [:> group-wrapper {:shape shape :frame frame}] + nil)]))))) (mf/defc page-svg {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 52c028170..def3de31c 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -21,7 +21,8 @@ (defn extract-style-attrs [shape] (let [stroke-style (:stroke-style shape :none) - attrs #js {:fill (or (:fill-color shape) "transparent") + attrs #js {;:filter (when (not= :frame (:type shape)) (str "url(#filter_" (:id shape) ")")) + :fill (or (:fill-color shape) "transparent") :fillOpacity (:fill-opacity shape nil) :rx (:rx shape nil) :ry (:ry shape nil)}] diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index b2568e59d..ba0443536 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.custom-stroke (:require [rumext.alpha :as mf] + [app.common.uuid :as uuid] [app.common.geom.shapes :as geom] [app.util.object :as obj])) @@ -21,7 +22,8 @@ (let [shape (unchecked-get props "shape") base-props (unchecked-get props "base-props") elem-name (unchecked-get props "elem-name") - {:keys [id x y width height]} (geom/shape->rect-shape shape) + {:keys [x y width height]} (geom/shape->rect-shape shape) + id (uuid/next) stroke-style (:stroke-style shape :none) stroke-position (:stroke-alignment shape :center)] (cond diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs new file mode 100644 index 000000000..d07db2cf7 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -0,0 +1,77 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.shapes.filters + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.util.color :as color] + [app.common.math :as mth] + [app.common.uuid :as uuid])) + +(defn get-filter-id [] + (str "filter_" (uuid/next))) + +(defn filter-str + [filter-id shape] + + (when (seq (:shadow shape)) + (str/fmt "url(#$0)" [filter-id]))) + +(mf/defc drop-shadow-filter + [{:keys [filter-id filter shape]}] + + (let [{:keys [x y width height]} (:selrect shape) + {:keys [fid color opacity offset-x offset-y blur spread]} filter + + filter-x (min x (+ x offset-x (- spread) (- blur) -5)) + filter-y (min y (+ y offset-y (- spread) (- blur) -5)) + filter-width (+ width (mth/abs offset-x) (* spread 2) (* blur 2) 10) + filter-height (+ height (mth/abs offset-x) (* spread 2) (* blur 2) 10) + + [r g b a] (color/hex->rgba color opacity) + [r g b] [(/ r 255) (/ g 255) (/ b 255)] + color-matrix (str/fmt "0 0 0 0 $0 0 0 0 0 $1 0 0 0 0 $2 0 0 0 $3 0" [r g b a])] + [:filter {:id filter-id + :x filter-x :y filter-y + :width filter-width :height filter-height + :filterUnits "userSpaceOnUse" + :color-interpolation-filters "sRGB"} + [:feFlood {:flood-opacity 0 :result "BackgroundImageFix"}] + [:feColorMatrix {:in "SourceAlpha" :type "matrix" + :values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"}] + (when (> spread 0) + [:feMorphology {:radius spread + :operator "dilate" + :in "SourceAlpha" + :result "effect1_dropShadow"}]) + + [:feOffset {:dx offset-x :dy offset-y}] + [:feGaussianBlur {:stdDeviation (/ blur 2)}] + + [:feColorMatrix {:type "matrix" :values color-matrix}] + + [:feBlend {:mode "normal" + :in2 "BackgroundImageFix" + :result "effect1_dropShadow"}] + + [:feBlend {:mode "normal" + :in "SourceGraphic" + :in2 "effect1_dropShadow" + :result "shape"}]])) + +(mf/defc filters + [{:keys [filter-id shape]}] + [:defs + (for [{:keys [id type hidden] :as filter} (:shadow shape)] + (when (not hidden) + [:& drop-shadow-filter {:key id + :filter-id filter-id + :filter filter + :shape shape}]))]) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 35b0a75bf..c29d23f4d 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -11,12 +11,15 @@ "The main container for a frame in viewer mode" (:require [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.common.uuid :as uuid] [app.common.data :as d] [app.common.pages :as cp] [app.common.pages-helpers :as cph] [app.main.data.viewer :as dv] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] @@ -53,10 +56,14 @@ on-mouse-down (mf/use-callback (mf/deps shape) - #(on-mouse-down % shape))] + #(on-mouse-down % shape)) + + filter-id (filters/get-filter-id)] [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer")} + :cursor (when (:interactions shape) "pointer") + :filter (filters/filter-str filter-id shape)} + [:& filters/filters {:filter-id filter-id :shape shape}] [:& component {:shape shape :frame frame :childs childs diff --git a/frontend/src/app/main/ui/workspace/shapes/common.cljs b/frontend/src/app/main/ui/workspace/shapes/common.cljs index 1967e9dbe..662e89805 100644 --- a/frontend/src/app/main/ui/workspace/shapes/common.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/common.cljs @@ -14,6 +14,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.keyboard :as kbd] + [app.main.ui.shapes.filters :as filters] [app.util.dom :as dom] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] @@ -69,9 +70,12 @@ #(on-mouse-down % shape)) on-context-menu (mf/use-callback (mf/deps shape) - #(on-context-menu % shape))] + #(on-context-menu % shape)) + filter-id (filters/get-filter-id)] [:g.shape {:on-mouse-down on-mouse-down - :on-context-menu on-context-menu} + :on-context-menu on-context-menu + :filter (filters/filter-str filter-id shape)} + [:& filters/filters {:filter-id filter-id :shape shape}] [:& component {:shape shape}]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs new file mode 100644 index 000000000..9e3c803df --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs @@ -0,0 +1,24 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.common + (:require + [rumext.alpha :as mf] + [app.util.dom :as dom])) + +(mf/defc advanced-options [{:keys [visible? on-close children]}] + (let [handle-click (fn [event] (when on-close + (do (dom/stop-propagation event) + (on-close))))] + (when visible? + [:* + [:div.focus-overlay {:on-click handle-click}] + [:div.advanced-options {} + children]]))) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs index 3848e7ea5..04e55265d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/frame_grid.cljs @@ -20,6 +20,7 @@ [app.main.data.workspace.grid :as dw] [app.util.geom.grid :as gg] [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] [app.main.ui.components.select :refer [select]] @@ -30,16 +31,6 @@ (def workspace-saved-grids (l/derived :saved-grids refs/workspace-page-options)) -(mf/defc advanced-options [{:keys [visible? on-close children]}] - (when visible? - [:* - [:div.focus-overlay {:on-click #(when on-close - (do - (dom/stop-propagation %) - (on-close)))}] - [:div.advanced-options {} - children]])) - (defn- get-size-options [locale] [{:value :auto :label (t locale "workspace.options.grid.auto")} :separator @@ -254,3 +245,4 @@ :on-remove (handle-remove-grid index) :on-save-grid handle-save-grid}])])])) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs index f74aff236..8e43aeac1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs @@ -12,7 +12,8 @@ [rumext.alpha :as mf] [app.main.ui.workspace.sidebar.options.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]] - [app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]])) + [app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]] + [app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]])) (mf/defc options {::mf/wrap [mf/memo]} @@ -31,5 +32,8 @@ :values fill-values}] [:& stroke-menu {:ids ids :type type - :values stroke-values}]])) + :values stroke-values}] + [:& shadow-menu {:ids ids + :type type + :values (select-keys shape [:shadow])}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shadow.cljs new file mode 100644 index 000000000..47ff7ef24 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shadow.cljs @@ -0,0 +1,195 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.shadow + (:require + [rumext.alpha :as mf] + [app.common.data :as d] + [app.common.uuid :as uuid] + [app.main.data.workspace.common :as dwc] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] + [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] + [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] + [app.util.dom :as dom])) + +(defn create-shadow [] + (let [id (uuid/next)] + {:id id + :style :drop-shadow + :color "#000000" + :opacity 0.2 + :offset-x 4 + :offset-y 4 + :blur 4 + :spread 0 + :hidden false})) + +(defn valid-number? [value] + (or (number? value) (not (js/isNaN (js/parseInt value))))) + +(mf/defc shadow-entry + [{:keys [ids index value]}] + (let [open-shadow (mf/use-state false) + + basic-offset-x-ref (mf/use-ref nil) + basic-offset-y-ref (mf/use-ref nil) + basic-blur-ref (mf/use-ref nil) + + adv-offset-x-ref (mf/use-ref nil) + adv-offset-y-ref (mf/use-ref nil) + adv-blur-ref (mf/use-ref nil) + adv-spread-ref (mf/use-ref nil) + + remove-shadow-by-id + (fn [values id] (->> values (filterv (fn [s] (not= (:id s) id))))) + + + + on-remove-shadow + (fn [id] + (fn [] + (st/emit! (dwc/update-shapes ids #(update % :shadow remove-shadow-by-id id) )))) + + select-text + (fn [ref] (fn [event] (dom/select-text! (mf/ref-val ref)))) + + update-attr + (fn update-attr + ([index attr valid?] + (update-attr index attr valid? nil)) + + ([index attr valid? update-ref] + (fn [event] + (let [value (dom/get-value (dom/get-target event))] + (when (or (not valid?) (valid? value)) + (do + (when update-ref + (dom/set-value! (mf/ref-val update-ref) value)) + (st/emit! (dwc/update-shapes ids #(assoc-in % [:shadow index attr] (js/parseInt value 10)))))))))) + + update-color + (fn [index] + (fn [color opacity] + (st/emit! (dwc/update-shapes + ids + #(-> % + (assoc-in [:shadow index :color] color) + (assoc-in [:shadow index :opacity] opacity)))))) + + toggle-visibility + (fn [index] + (fn [] + (st/emit! (dwc/update-shapes ids #(update-in % [:shadow index :hidden] not)))))] + [:* + [:div.element-set-options-group + + [:div.element-set-actions-button + {:on-click #(reset! open-shadow true)} + i/actions] + + [:input.input-text {:type "number" + :ref basic-offset-x-ref + :on-change (update-attr index :offset-x valid-number?) + :on-click (select-text basic-offset-x-ref) + :default-value (:offset-x value)}] + [:input.input-text {:type "number" + :ref basic-offset-y-ref + :on-change (update-attr index :offset-y valid-number?) + :on-click (select-text basic-offset-y-ref) + :default-value (:offset-y value)}] + [:input.input-text {:type "number" + :ref basic-blur-ref + :on-click (select-text basic-blur-ref) + :on-change (update-attr index :blur valid-number?) + :min 0 + :default-value (:blur value)}] + + [:div.element-set-actions + [:div.element-set-actions-button {:on-click (toggle-visibility index)} + (if (:hidden value) i/eye-closed i/eye)] + [:div.element-set-actions-button {:on-click (on-remove-shadow (:id value))} + i/minus]]] + + [:& advanced-options {:visible? @open-shadow + :on-close #(reset! open-shadow false)} + [:div.row-grid-2 + [:select.input-select + [:option {:value ":drop-shadow"} "Drop shadow"] + #_[:option {:value ":inner-shadow"} "Inner shadow"]]] + + [:div.row-grid-2 + [:div.input-element + [:input.input-text {:type "number" + :ref adv-offset-x-ref + :no-validate true + :placeholder "--" + :on-click (select-text adv-offset-x-ref) + :on-change (update-attr index :offset-x valid-number? basic-offset-x-ref) + :default-value (:offset-x value)}] + [:span.after "X"]] + + [:div.input-element + [:input.input-text {:type "number" + :ref adv-offset-y-ref + :no-validate true + :placeholder "--" + :on-click (select-text adv-offset-y-ref) + :on-change (update-attr index :offset-y valid-number? basic-offset-y-ref) + :default-value (:offset-y value)}] + [:span.after "Y"]]] + + [:div.row-grid-2 + [:div.input-element + [:input.input-text {:type "number" + :ref adv-blur-ref + :no-validate true + :placeholder "--" + :on-click (select-text adv-blur-ref) + :on-change (update-attr index :blur valid-number? basic-blur-ref) + :min 0 + :default-value (:blur value)}] + [:span.after "Blur"]] + + [:div.input-element + [:input.input-text {:type "number" + :ref adv-spread-ref + :no-validate true + :placeholder "--" + :on-click (select-text adv-spread-ref) + :on-change (update-attr index :spread valid-number?) + :min 0 + :default-value (:spread value)}] + [:span.after "Spread"]]] + + [:div.color-row-wrap + [:& color-row {:color {:value (:color value) :opacity (:opacity value)} + :on-change (update-color index) + :on-open #(st/emit! dwc/start-undo-transaction) + :on-close #(st/emit! dwc/commit-undo-transaction)}]]]])) +(mf/defc shadow-menu + [{:keys [ids type values] :as props}] + + (.log js/console "values" (clj->js values)) + (let [on-add-shadow + (fn [] + (st/emit! (dwc/update-shapes ids #(update % :shadow (fnil conj []) (create-shadow)) )))] + [:div.element-set.shadow-options + [:div.element-set-title + [:span "Shadow"] + [:div.add-page {:on-click on-add-shadow} i/close]] + + (when (seq (:shadow values)) + [:div.element-set-content + (for [[index {:keys [id] :as value}] (d/enumerate (:shadow values []))] + [:& shadow-entry {:key (str "shadow-" id) + :ids ids + :value value + :index index}])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs index 29fb7b558..a8927137b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs @@ -70,7 +70,7 @@ :group (t locale "workspace.options.group-stroke") (t locale "workspace.options.stroke")) - show-options (not= (or (:stroke-style values) :none) :none) + show-options (not= (:stroke-style values :none) :none) current-stroke-color {:value (:stroke-color values) :opacity (:stroke-opacity values) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index dbc05d854..57ac2d37c 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -103,6 +103,10 @@ [node] (set! (.-value node) "")) +(defn set-value! + [node value] + (set! (.-value node) value)) + (defn select-text! [node] (.select node))