diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index b523dae2c..54f3f59f4 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -199,3 +199,26 @@ (defn center-points [points] (let [k (point (count points))] (reduce #(add %1 (divide %2 k)) (point) points))) + +(defn normal-left + "Returns the normal unit vector on the left side" + [{:keys [x y]}] + (unit (point (- y) x))) + +(defn normal-right + "Returns the normal unit vector on the right side" + [{:keys [x y]}] + (unit (point y (- x)))) + +(defn point-line-distance + [point line-point1 line-point2] + (let [{x0 :x y0 :y} point + {x1 :x y1 :y} line-point1 + {x2 :x y2 :y} line-point2 + num (mth/abs + (+ (* x0 (- y2 y1)) + (- (* y0 (- x2 x1))) + (* x2 y1) + (- (* y2 x1)))) + dist (distance line-point2 line-point1)] + (/ num dist))) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 52c028170..185c5e1f4 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -8,7 +8,9 @@ ;; Copyright (c) 2016-2020 UXBOX Labs SL (ns app.main.ui.shapes.attrs - (:require [app.util.object :as obj])) + (:require + [cuerdas.core :as str] + [app.util.object :as obj])) (defn- stroke-type->dasharray [style] @@ -19,16 +21,20 @@ nil)) (defn extract-style-attrs - [shape] - (let [stroke-style (:stroke-style shape :none) - attrs #js {:fill (or (:fill-color shape) "transparent") - :fillOpacity (:fill-opacity shape nil) - :rx (:rx shape nil) - :ry (:ry shape nil)}] - (when (not= stroke-style :none) - (obj/merge! attrs - #js {:stroke (:stroke-color shape nil) - :strokeWidth (:stroke-width shape 1) - :strokeOpacity (:stroke-opacity shape nil) - :strokeDasharray (stroke-type->dasharray stroke-style)})) - attrs)) + ([shape] (extract-style-attrs shape nil)) + ([shape gradient-id] + (let [stroke-style (:stroke-style shape :none) + attrs #js {:rx (:rx shape nil) + :ry (:ry shape nil)} + attrs (obj/merge! attrs + (if gradient-id + #js {:fill (str/format "url(#%s)" gradient-id)} + #js {:fill (or (:fill-color shape) "transparent") + :fillOpacity (:fill-opacity shape nil)}))] + (when (not= stroke-style :none) + (obj/merge! attrs + #js {:stroke (:stroke-color shape nil) + :strokeWidth (:stroke-width shape 1) + :strokeOpacity (:stroke-opacity shape nil) + :strokeDasharray (stroke-type->dasharray stroke-style)})) + attrs))) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs new file mode 100644 index 000000000..f86b025b0 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -0,0 +1,78 @@ +;; 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.gradients + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [goog.object :as gobj] + [app.common.uuid :as uuid] + [app.common.geom.point :as gpt])) + +(mf/defc linear-gradient [{:keys [id shape gradient]}] + (let [{:keys [x y width height]} shape] + [:defs + [:linearGradient {:id id + :x1 (:start-x gradient) + :y1 (:start-y gradient) + :x2 (:end-x gradient) + :y2 (:end-y gradient)} + (for [{:keys [offset color opacity]} (:stops gradient)] + [:stop {:key (str id "-stop-" offset) + :offset (or offset 0) + :stop-color color + :stop-opacity opacity}])]])) + +(mf/defc radial-gradient [{:keys [id shape gradient]}] + (let [{:keys [x y width height]} shape] + [:defs + (let [translate-vec (gpt/point (+ x (* width (:start-x gradient))) + (+ y (* height (:start-y gradient)))) + + + gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient)) + (* height (:start-y gradient))) + (gpt/point (* width (:end-x gradient)) + (* height (:end-y gradient)))) + + angle (gpt/angle gradient-vec + (gpt/point 1 0)) + + shape-height-vec (gpt/point 0 (/ height 2)) + + scale-factor-y (/ (gpt/length gradient-vec) (/ height 2)) + scale-factor-x (* scale-factor-y (:width gradient)) + + scale-vec (gpt/point (* scale-factor-y (/ height 2)) + (* scale-factor-x (/ width 2)) + ) + tr-translate (str/fmt "translate(%s, %s)" (:x translate-vec) (:y translate-vec)) + tr-rotate (str/fmt "rotate(%s)" angle) + tr-scale (str/fmt "scale(%s, %s)" (:x scale-vec) (:y scale-vec)) + transform (str/fmt "%s %s %s" tr-translate tr-rotate tr-scale)] + [:radialGradient {:id id + :cx 0 + :cy 0 + :r 1 + :gradientUnits "userSpaceOnUse" + :gradientTransform transform} + (for [{:keys [offset color opacity]} (:stops gradient)] + [:stop {:key (str id "-stop-" offset) + :offset (or offset 0) + :stop-color color + :stop-opacity opacity}])])])) + +(mf/defc gradient + {::mf/wrap-props false} + [props] + (let [gradient (gobj/get props "gradient")] + (case (:type gradient) + :linear [:> linear-gradient props] + :radial [:> radial-gradient props] + nil))) diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs index a1a7c8457..5cac4b6b3 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -13,7 +13,12 @@ [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] [app.common.geom.shapes :as geom] - [app.util.object :as obj])) + [app.util.object :as obj] + [app.main.ui.shapes.gradients :refer [gradient]] + + [cuerdas.core :as str] + [app.common.uuid :as uuid] + [app.common.geom.point :as gpt])) (mf/defc rect-shape {::mf/wrap-props false} @@ -21,7 +26,10 @@ (let [shape (unchecked-get props "shape") {:keys [id x y width height]} shape transform (geom/transform-matrix shape) - props (-> (attrs/extract-style-attrs shape) + + gradient-id (when (:fill-color-gradient shape) (str (uuid/next))) + + props (-> (attrs/extract-style-attrs shape gradient-id) (obj/merge! #js {:x x :y y @@ -30,7 +38,12 @@ :width width :height height}))] - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "rect"}])) + [:* + (when gradient-id + [:& gradient {:id gradient-id + :shape shape + :gradient (:fill-color-gradient shape)}]) + [:& shape-custom-stroke {:shape shape + :base-props props + :elem-name "rect"}]])) diff --git a/frontend/src/app/main/ui/workspace/gradients.cljs b/frontend/src/app/main/ui/workspace/gradients.cljs new file mode 100644 index 000000000..dad8b2099 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/gradients.cljs @@ -0,0 +1,290 @@ +;; 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.gradients + "Gradients handlers and renders" + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [beicon.core :as rx] + [app.main.data.workspace.common :as dwc] + [app.main.store :as st] + [app.main.streams :as ms] + [app.common.math :as mth] + [app.util.dom :as dom] + [app.common.geom.point :as gpt] + [app.common.geom.matrix :as gmt])) + +(def gradient-line-stroke-width 2) +(def gradient-line-stroke-color "white") +(def gradient-square-width 15) +(def gradient-square-radius 2) +(def gradient-square-stroke-width 2) +(def gradient-width-handler-radius 5) +(def gradient-width-handler-color "white") +(def gradient-square-stroke-color "white") +(def gradient-square-stroke-color-selected "#1FDEA7") + +(mf/defc shadow [{:keys [id x y width height offset]}] + [:filter {:id id + :x x + :y y + :width width + :height 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"}] + [:feOffset {:dy offset}] + [:feGaussianBlur {:stdDeviation "1"}] + [:feColorMatrix {:type "matrix" :values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"}] + [:feBlend {:mode "normal" :in2 "BackgroundImageFix" :result id}] + [:feBlend {:mode "normal" :in "SourceGraphic" :in2 id :result "shape"}]]) + +(mf/defc gradient-line-drop-shadow-filter [{:keys [id zoom from-p to-p]}] + [:& shadow + {:id id + :x (min (- (:x from-p) (/ 2 zoom)) + (- (:x to-p) (/ 2 zoom))) + :y (min (- (:y from-p) (/ 2 zoom)) + (- (:y to-p) (/ 2 zoom))) + :width (+ (mth/abs (- (:x to-p) (:x from-p))) (/ 4 zoom)) + :height (+ (mth/abs (- (:y to-p) (:y from-p))) (/ 4 zoom)) + :offset (/ 2 zoom)}]) + + +(mf/defc gradient-square-drop-shadow-filter [{:keys [id zoom point]}] + [:& shadow + {:id id + :x (- (:x point) (/ gradient-square-width zoom 2) 2) + :y (- (:y point) (/ gradient-square-width zoom 2) 2) + :width (+ (/ gradient-square-width zoom) (/ 2 zoom) 4) + :height (+ (/ gradient-square-width zoom) (/ 2 zoom) 4) + :offset (/ 2 zoom)}]) + +(mf/defc gradient-width-handler-shadow-filter [{:keys [id zoom point]}] + [:& shadow + {:id id + :x (- (:x point) (/ gradient-width-handler-radius zoom) 2) + :y (- (:y point) (/ gradient-width-handler-radius zoom) 2) + :width (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4) + :height (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4) + :offset (/ 2 zoom)}]) + +(def default-gradient + {:type :linear + :start-x 0.5 :start-y 0.5 + :end-x 0.5 :end-y 1 + :width 1.0 + :stops [{:offset 0 + :color "#FF0000" + :opacity 1} + {:offset 1 + :color "#FF0000" + :opacity 0.2}]}) + +(def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAACvUlEQVQoFQGyAk39AeLi4gAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB////AAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjScaa0cU7nIAAAAASUVORK5CYII=") + +(mf/defc gradient-color-handler + [{:keys [filter-id zoom point color angle on-click on-mouse-down on-mouse-up]}] + [:g {:filter (str/fmt "url(#%s)" filter-id) + :transform (gmt/rotate-matrix angle point)} + + [:image {:href checkboard + :x (- (:x point) (/ gradient-square-width 2 zoom)) + :y (- (:y point) (/ gradient-square-width 2 zoom)) + :width (/ gradient-square-width zoom) + :height (/ gradient-square-width zoom)}] + + [:rect {:x (- (:x point) (/ gradient-square-width 2 zoom)) + :y (- (:y point) (/ gradient-square-width 2 zoom)) + :rx (/ gradient-square-radius zoom) + :width (/ gradient-square-width zoom 2) + :height (/ gradient-square-width zoom) + :fill (:value color) + :on-click (partial on-click :to-p) + :on-mouse-down (partial on-mouse-down :to-p) + :on-mouse-up (partial on-mouse-up :to-p)}] + + [:rect {:x (- (:x point) (/ gradient-square-width 2 zoom)) + :y (- (:y point) (/ gradient-square-width 2 zoom)) + :rx (/ gradient-square-radius zoom) + :width (/ gradient-square-width zoom) + :height (/ gradient-square-width zoom) + :stroke "white" + :stroke-width (/ gradient-square-stroke-width zoom) + :fill (:value color) + :fill-opacity (:opacity color) + :on-click on-click + :on-mouse-down on-mouse-down + :on-mouse-up on-mouse-up}]]) + +(mf/defc gradient-handler-transformed + [{:keys [from-p to-p width-p from-color to-color zoom on-change-start on-change-finish on-change-width on-change-stop-color]}] + (let [moving-point (mf/use-var nil) + angle (+ 90 (gpt/angle from-p to-p)) + + on-click (fn [position event] + (dom/stop-propagation event) + (dom/prevent-default event)) + + on-mouse-down (fn [position event] + (dom/stop-propagation event) + (dom/prevent-default event) + (reset! moving-point position)) + + on-mouse-up (fn [position event] + (dom/stop-propagation event) + (dom/prevent-default event) + (reset! moving-point nil))] + + (mf/use-effect + (mf/deps @moving-point from-p to-p width-p) + (fn [] + (let [subs (->> st/stream + (rx/filter ms/pointer-event?) + (rx/filter #(= :viewport (:source %))) + (rx/map :pt) + (rx/subs #(case @moving-point + :from-p (when on-change-start (on-change-start %)) + :to-p (when on-change-finish (on-change-finish %)) + :width-p (when on-change-width + (let [width-v (gpt/unit (gpt/to-vec from-p width-p)) + distance (gpt/point-line-distance % from-p to-p) + new-width-p (gpt/add + from-p + (gpt/multiply width-v (gpt/point distance)))] + (on-change-width new-width-p))) + nil)))] + (fn [] (rx/dispose! subs))))) + [:g.gradient-handlers + [:defs + [:& gradient-line-drop-shadow-filter {:id "gradient_line_drop_shadow" :from-p from-p :to-p to-p :zoom zoom}] + [:& gradient-line-drop-shadow-filter {:id "gradient_widh_line_drop_shadow" :from-p from-p :to-p width-p :zoom zoom}] + [:& gradient-square-drop-shadow-filter {:id "gradient_square_from_drop_shadow" :point from-p :zoom zoom}] + [:& gradient-square-drop-shadow-filter {:id "gradient_square_to_drop_shadow" :point to-p :zoom zoom}] + [:& gradient-width-handler-shadow-filter {:id "gradient_width_handler_drop_shadow" :point width-p :zoom zoom}]] + + [:g {:filter "url(#gradient_line_drop_shadow)"} + [:line {:x1 (:x from-p) + :y1 (:y from-p) + :x2 (:x to-p) + :y2 (:y to-p) + :stroke gradient-line-stroke-color + :stroke-width (/ gradient-line-stroke-width zoom)}]] + + (when width-p + [:g {:filter "url(#gradient_widh_line_drop_shadow)"} + [:line {:x1 (:x from-p) + :y1 (:y from-p) + :x2 (:x width-p) + :y2 (:y width-p) + :stroke gradient-line-stroke-color + :stroke-width (/ gradient-line-stroke-width zoom)}]]) + + (when width-p + [:g {:filter "url(#gradient_width_handler_drop_shadow)"} + [:circle {:cx (:x width-p) + :cy (:y width-p) + :r (/ gradient-width-handler-radius zoom) + :fill gradient-width-handler-color + :on-mouse-down (partial on-mouse-down :width-p) + :on-mouse-up (partial on-mouse-up :width-p)}]]) + + [:& gradient-color-handler + {:filter-id "gradient_square_from_drop_shadow" + :zoom zoom + :point from-p + :color from-color + :angle angle + :on-click (partial on-click :from-p) + :on-mouse-down (partial on-mouse-down :from-p) + :on-mouse-up (partial on-mouse-up :from-p)}] + + [:& gradient-color-handler + {:filter-id "gradient_square_to_drop_shadow" + :zoom zoom + :point to-p + :color to-color + :angle angle + :on-click (partial on-click :to-p) + :on-mouse-down (partial on-mouse-down :to-p) + :on-mouse-up (partial on-mouse-up :to-p)}]])) + +(mf/defc gradient-handlers + [{:keys [shape zoom]}] + (let [{:keys [x y width height] :as sr} (:selrect shape) + + state (mf/use-state (:fill-color-gradient shape default-gradient)) + + [{start-color :color start-opacity :opacity} + {end-color :color end-opacity :opacity}] (:stops @state) + + from-p (gpt/point (+ x (* width (:start-x @state))) + (+ y (* height (:start-y @state)))) + + to-p (gpt/point (+ x (* width (:end-x @state))) + (+ y (* height (:end-y @state)))) + + gradient-vec (gpt/to-vec from-p to-p) + gradient-length (gpt/length gradient-vec) + + width-v (-> gradient-vec + (gpt/normal-left) + (gpt/multiply (gpt/point (* (:width @state) (/ gradient-length (/ height 2) )))) + (gpt/multiply (gpt/point (/ width 2)))) + + width-p (gpt/add from-p width-v) + + on-change-start (fn [point] + (let [start-x (/ (- (:x point) x) width) + start-y (/ (- (:y point) y) height)] + (swap! state assoc + :start-x start-x + :start-y start-y ))) + + on-change-finish (fn [point] + (let [end-x (/ (- (:x point) x) width) + end-y (/ (- (:y point) y) height)] + (swap! state assoc + :end-x end-x + :end-y end-y))) + + on-change-width (fn [point] + (let [scale-factor-y (/ gradient-length (/ height 2)) + norm-dist (/ (gpt/distance point from-p) + (* (/ width 2) scale-factor-y))] + (swap! state assoc :width norm-dist))) + + on-change-stop-color (fn [offset color opacity] (println "change-color"))] + + (mf/use-effect + (mf/deps shape) + (fn [] + (reset! state (:fill-color-gradient shape default-gradient)))) + + (mf/use-effect + (mf/deps @state) + (fn [] + (when (not= (:fill-color-gradient shape) @state) + (st/emit! (dwc/update-shapes + [(:id shape)] + #(assoc % :fill-color-gradient @state)))))) + + [:& gradient-handler-transformed + {:from-p from-p + :to-p to-p + :width-p (when (= :radial (:type @state)) width-p) + :from-color {:value start-color :opacity start-opacity} + :to-color {:value end-color :opacity end-opacity} + :zoom zoom + :on-change-start on-change-start + :on-change-finish on-change-finish + :on-change-width on-change-width + :on-change-stop-color on-change-stop-color}])) diff --git a/frontend/src/app/main/ui/workspace/selection.cljs b/frontend/src/app/main/ui/workspace/selection.cljs index 8e54075e5..41480a39a 100644 --- a/frontend/src/app/main/ui/workspace/selection.cljs +++ b/frontend/src/app/main/ui/workspace/selection.cljs @@ -16,18 +16,20 @@ [rumext.alpha :as mf] [rumext.util :refer [map->obj]] [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.cursors :as cur] + [app.common.math :as mth] [app.util.dom :as dom] [app.util.object :as obj] [app.common.geom.shapes :as geom] [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] [app.util.debug :refer [debug?]] - [app.main.ui.workspace.shapes.outline :refer [outline]])) - + [app.main.ui.workspace.shapes.outline :refer [outline]] + [app.main.ui.workspace.gradients :refer [gradient-handlers]])) (def rotation-handler-size 25) (def resize-point-radius 4) @@ -168,7 +170,6 @@ :cursor (if (#{:left :right} position) (cur/resize-ew rotation) (cur/resize-ns rotation)) }}])) - (mf/defc controls {::mf/wrap-props false} [props] @@ -180,7 +181,9 @@ current-transform (mf/deref refs/current-transform) selrect (geom/shape->rect-shape shape) - transform (geom/transform-matrix shape)] + transform (geom/transform-matrix shape) + + tr-shape (geom/transform-shape shape)] (when (not (#{:move :rotate} current-transform)) [:g.controls @@ -190,8 +193,7 @@ :transform transform :zoom zoom :color color}] - [:& outline {:shape (geom/transform-shape shape) - :color color}] + [:& outline {:shape tr-shape :color color}] ;; Handlers (for [{:keys [type position props]} (handlers-for-selection selrect)] @@ -208,7 +210,11 @@ (case type :rotation (when (not= :frame (:type shape)) [:> rotation-handler props]) :resize-point [:> resize-point-handler props] - :resize-side [:> resize-side-handler props])))]))) + :resize-side [:> resize-side-handler props]))) + + (when (= :rect (:type shape)) + [:& gradient-handlers {:shape tr-shape + :zoom zoom}])]))) ;; --- Selection Handlers (Component) (mf/defc path-edition-selection-handlers