diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc
index b523dae2c5..65b453b561 100644
--- a/common/app/common/geom/point.cljc
+++ b/common/app/common/geom/point.cljc
@@ -199,3 +199,27 @@
(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
+ "Returns the distance from a point to a line defined by two points"
+ [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/common/app/common/math.cljc b/common/app/common/math.cljc
index d49bcf42cf..9125c7c351 100644
--- a/common/app/common/math.cljc
+++ b/common/app/common/math.cljc
@@ -12,6 +12,10 @@
#?(:cljs
(:require [goog.math :as math])))
+(def PI
+ #?(:cljs (.-PI js/Math)
+ :clj Math/PI))
+
(defn nan?
[v]
#?(:cljs (js/isNaN v)
diff --git a/frontend/resources/images/icons/picker-harmony.svg b/frontend/resources/images/icons/picker-harmony.svg
new file mode 100644
index 0000000000..c108e28127
--- /dev/null
+++ b/frontend/resources/images/icons/picker-harmony.svg
@@ -0,0 +1,2 @@
+
diff --git a/frontend/resources/images/icons/picker-hsv.svg b/frontend/resources/images/icons/picker-hsv.svg
new file mode 100644
index 0000000000..2218c82a17
--- /dev/null
+++ b/frontend/resources/images/icons/picker-hsv.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/resources/images/icons/picker-ramp.svg b/frontend/resources/images/icons/picker-ramp.svg
new file mode 100644
index 0000000000..0e078a017e
--- /dev/null
+++ b/frontend/resources/images/icons/picker-ramp.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/resources/images/icons/picker.svg b/frontend/resources/images/icons/picker.svg
index f486028b4b..be86a18081 100644
--- a/frontend/resources/images/icons/picker.svg
+++ b/frontend/resources/images/icons/picker.svg
@@ -1 +1,4 @@
-
\ No newline at end of file
+
diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss
index 2ff6badc4c..9b3d268e44 100644
--- a/frontend/resources/styles/main/partials/colorpicker.scss
+++ b/frontend/resources/styles/main/partials/colorpicker.scss
@@ -6,257 +6,487 @@
// Copyright (c) 2015-2016 Juan de la Cruz
.colorpicker {
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ background-color: $color-white;
+}
+
+.colorpicker-content {
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem;
+
+ & > * {
+ width: 200px;
+ }
+
+ .top-actions {
display: flex;
- flex-direction: column;
- padding: 0.5rem;
- background-color: $color-white;
- box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ margin-bottom: 0.25rem;
+ justify-content: space-between;
- & > * {
- width: 200px;
+ .picker-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+
+ &.active,
+ &:hover svg {
+ fill: $color-primary;
+ }
+
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+ }
+ }
+
+ .gradients-buttons {
+ .gradient {
+ cursor: pointer;
+ width: 15px;
+ height: 15px;
+ padding: 0;
+ margin: 0;
+ border: 1px solid $color-gray-20;
+ border-radius: 2px;
+ margin-left: 0.25rem;
}
- .top-actions {
- display: flex;
- margin-bottom: 0.25rem;
-
- .picker-btn {
- background: none;
- border: none;
- cursor: pointer;
-
- &.active,
- &:hover svg {
- fill: $color-primary;
- }
-
- svg {
- width: 14px;
- height: 14px;
- }
- }
+ .active {
+ border-color: $color-primary;
}
- .picker-detail-wrapper {
- position: relative;
+ .linear-gradient {
+ background: linear-gradient(180deg, $color-gray-20, transparent);
+ }
- .center-circle {
- width: 14px;
- height: 14px;
- border: 2px solid $color-white;
- border-radius: 8px;
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-7px, -7px);
- filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
- }
+ .radial-gradient {
+ background: radial-gradient(transparent, $color-gray-20);
}
- #picker-detail {
- border: 1px solid $color-gray-10;
+ }
+
+ .gradient-stops {
+ height: 10px;
+ display: flex;
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+
+ .gradient-background {
+ height: 100%;
+ width: 100%;
+ border: 1px solid $color-gray-10;
}
+ .gradient-stop-wrapper {
+ position: absolute;
+ width: calc(100% - 2rem);
+ margin-left: 0.5rem;
+ }
+
+ .gradient-stop {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ border-radius: 2px;
+ border: 1px solid $color-gray-20;
+ margin-top: -2px;
+ margin-left: -7px;
+ box-shadow: 0 2px 2px rgb(0 0 0 / 15%);
+
+ .selected {
+ border-color: $color-primary;
+ }
+ }
+ }
+
+ .picker-detail-wrapper {
+ position: relative;
+
+ .center-circle {
+ width: 14px;
+ height: 14px;
+ border: 2px solid $color-white;
+ border-radius: 8px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-7px, -7px);
+ filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
+ }
+ }
+
+ #picker-detail {
+ border: 1px solid $color-gray-10;
+ }
+
+ .slider-selector {
+ --gradient-direction: 90deg;
+ --background-repeat: left;
+
+ &.vertical {
+ --gradient-direction: 0deg;
+ --background-repeat: top;
+ }
+
+ border: 1px solid $color-gray-10;
+
+ background: linear-gradient(var(--gradient-direction), rgba(var(--color), 0) 0%, rgba(var(--color), 1.0) 100%);
+ align-self: center;
+ position: relative;
+ cursor: pointer;
+
+ width: 100%;
+ height: calc(0.5rem + 1px);
+
+ &.vertical {
+ width: calc(0.5rem + 1px);
+ height: 100%;
+ }
+
+ &.hue {
+ background: linear-gradient(
+ var(--gradient-direction),
+ #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%,
+ #00f 67%, #f0f 83%, #f00 100%);
+ }
+
+ &.saturation {
+ background: linear-gradient(
+ var(--gradient-direction),
+ var(--saturation-grad-from) 0%,
+ var(--saturation-grad-to) 100%
+ )
+ }
+
+ &.opacity {
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") var(--background-repeat) center;
+
+ &::after {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(var(--gradient-direction), rgba(var(--color), 0) 0%, rgba(var(--color), 1.0) 100%);
+ }
+
+ }
+
+ &.value {
+ background: linear-gradient(var(--gradient-direction), #FFF 0%, #000 100%);
+ }
+
+
.handler {
+ background-color: $color-white;;
+ box-shadow: rgba(0, 0, 0, 0.37) 0px 1px 4px 0px;
+ transform: translate(-6px, -2px);
+ left: 50%;
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ border-radius: 6px;
+ z-index: 1;
+ }
+
+ &.vertical .handler {
+ transform: translate(-6px, 6px);
+ }
+ }
+
+ .value-saturation-selector {
+ background-color: rgba(var(--hue-rgb));
+ position: relative;
+ height: 6.75rem;
+ cursor: pointer;
+
+ .handler {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ border-radius: 6px;
+ z-index: 1;
+ border: 1px solid $color-white;
+ box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset, rgb(0 0 0 / 0.25) 0px 4px 4px inset, rgb(0 0 0 / 0.25) 0px 4px 4px;
+ transform: translate(-6px, -6px);
+ left: 50%;
+ top: 50%;
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(to right, #fff, rgba(255,255,255,0));
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(to top, #000, rgba(0,0,0,0));
+ }
+ }
+
+ .color-bullet {
+ grid-area: color;
+ width: 20px;
+ height: 20px;
+ background-color: rgba(var(--color));
+ border-radius: 12px;
+ border: 1px solid $color-gray-10;
+ }
+
+ .shade-selector {
+ display: grid;
+ justify-items: center;
+ align-items: center;
+ grid-template-areas: "color hue"
+ "color opacity";
+ grid-template-columns: 2.5rem 1fr;
+ height: 3.5rem;
+ grid-row-gap: 0.5rem;
+ cursor: pointer;
+ margin-bottom: 0.25rem;
+
+ .slider-selector.hue {
+ grid-area: "hue";
+ align-self: end;
+ }
+
+ .slider-selector.opacity {
+ grid-area: "opacity";
+ align-self: start;
+ }
+ }
+
+ .color-values {
+ display: grid;
+ grid-template-columns: 3.5rem repeat(4, 1fr);
+ grid-row-gap: 0.25rem;
+ justify-items: center;
+ grid-column-gap: 0.25rem;
+
+ input {
+ width: 100%;
+ margin: 0;
+ border: 1px solid $color-gray-10;
+ border-radius: 2px;
+ font-size: $fs11;
+ height: 1.5rem;
+ padding: 0 $x-small;
+ color: $color-gray-40;
+ }
+
+ label {
+ font-size: $fs11;
+ }
+ }
+
+ .libraries {
+ border-top: 1px solid $color-gray-10;
+ padding-top: 0.5rem;
+ margin-top: 0.25rem;
+ width: 200px;
+
+ select {
+ background-image: url(/images/icons/arrow-down.svg);
+ background-repeat: no-repeat;
+ background-position: 95% 48%;
+ background-size: 10px;
+ margin: 0;
+ margin-bottom: 0.5rem;
+ width: 100%;
+ padding: 2px 0.25rem;
+ font-size: 0.75rem;
+ color: $color-gray-40;
+ border-color: $color-gray-10;
+ border-radius: 2px;
+
+ option {
+ padding: 0;
+ }
+ }
+
+ .selected-colors {
+ display: grid;
+ grid-template-columns: repeat(8, 1fr);
+ justify-content: space-between;
+ margin-right: -8px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ max-height: 5.5rem;
+ }
+
+
+ .selected-colors::after {
+ content: "";
+ flex: auto;
+ }
+
+ .selected-colors .color-bullet {
+ grid-area: auto;
+ margin-bottom: 0.25rem;
+ cursor: pointer;
+
+ &:hover {
+ border-color: $color-primary;
+ }
+
+ &.button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &.button svg {
+ width: 12px;
+ height: 12px;
+ fill: $color-gray-30;
+ }
+
+ &.plus-button svg {
+ width: 8px;
+ height: 8px;
+ fill: $color-black;
+ }
+ }
+ }
+
+ .actions {
+ margin-top: 0.5rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+
+ .btn-primary {
+ height: 1.5rem;
+ padding: 0 2.5rem;
+ font-size: $fs12;
+ }
+ }
+
+ .harmony-selector {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 0.5rem;
+
+ .hue-wheel-wrapper {
+ position: relative;
+
+ .hue-wheel {
+ width: 152px;
+ height: 152px;
+ }
+
+ .handler {
position: absolute;
width: 12px;
height: 12px;
border-radius: 6px;
z-index: 1;
- }
-
- .value-selector {
- background-color: rgba(var(--hue));
- position: relative;
- height: 6.75rem;
- cursor: pointer;
-
- .handler {
- box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset;
- transform: translate(-6px, -6px);
- left: 50%;
- top: 50%;
- }
- }
-
- .value-selector::before {
- content: "";
- position: absolute;
- width: 100%;
- height: 100%;
- background: linear-gradient(to right, #fff, rgba(255,255,255,0));
- }
-
- .value-selector::after {
- content: "";
- position: absolute;
- width: 100%;
- height: 100%;
- background: linear-gradient(to top, #000, rgba(0,0,0,0));
- }
-
- .shade-selector {
- display: grid;
- justify-items: center;
- align-items: center;
- grid-template-areas: "color hue" "color opacity";
- grid-template-columns: 2.5rem 1fr;
- height: 3.5rem;
- grid-row-gap: 0.5rem;
- cursor: pointer;
- }
-
- .color-bullet {
- grid-area: color;
- width: 20px;
- height: 20px;
- background-color: rgba(var(--color));
- border-radius: 12px;
- border: 1px solid $color-gray-10;
- }
-
- .hue-selector {
- align-self: end;
- grid-area: hue;
- height: 0.5rem;
- width: 100%;
- background: linear-gradient(
- to right,
- #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%,
- #00f 67%, #f0f 83%, #f00 100%);
- position: relative;
- cursor: pointer;
- }
-
- .hue-selector .handler,
- .opacity-selector .handler {
- background-color: rgb(248, 248, 248);
- box-shadow: rgba(0, 0, 0, 0.37) 0px 1px 4px 0px;
- transform: translate(-6px, -2px);
+ border: 1px solid $color-white;
+ box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset, rgb(0 0 0 / 0.25) 0px 4px 4px inset, rgb(0 0 0 / 0.25) 0px 4px 4px;
+ transform: translate(-6px, -6px);
left: 50%;
+ top: 50%;
+ }
+
+ .handler.complement {
+ background-color: $color-white;
+ box-shadow: rgb(0 0 0 / 0.25) 0px 4px 4px;
+ }
}
- .opacity-selector {
- align-self: start;
- grid-area: opacity;
- height: 0.5rem;
- width: 100%;
- position: relative;
- background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") left center;
- }
-
- .opacity-selector::after {
- content: "";
- background: linear-gradient(to right, rgba(var(--color), 0) 0%, rgba(var(--color), 1.0) 100%);
- position: absolute;
- width: 100%;
+ .handlers-wrapper {
+ height: 152px;
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ justify-content: space-around;
+ padding-top: 0.5rem;
+
+ & > * {
height: 100%;
+ }
+ }
+ }
+
+ .hsva-selector {
+ display: grid;
+ padding: 0.25rem;
+ grid-template-columns: 20px 1fr;
+ grid-template-rows: repeat(4, 2rem);
+ grid-row-gap: 0.5rem;
+ margin-bottom: 0.5rem;
+
+ .hue,
+ .saturation,
+ .value,
+ .opacity {
+ border-radius: 10px;
}
- .color-values {
- display: grid;
- grid-template-columns: 3.5rem repeat(4, 1fr);
- grid-row-gap: 0.25rem;
- justify-items: center;
- grid-column-gap: 0.25rem;
+ .hsva-selector-label {
+ grid-column: 1;
+ align-self: center;
+ }
+ }
+}
- input {
- width: 100%;
- margin: 0;
- border: 1px solid $color-gray-10;
- border-radius: 2px;
- font-size: $fs11;
- height: 1.5rem;
- padding: 0 $x-small;
- color: $color-gray-40;
- }
+.colorpicker-tooltip {
+ border-radius: $br-small;
+ display: flex;
+ flex-direction: column;
+ left: 1400px;
+ top: 100px;
+ position: absolute;
+ z-index: 11;
+ width: auto;
- label {
- font-size: $fs11;
- }
+ span {
+ color: $color-gray-20;
+ font-size: $fs12;
+ }
+
+ .inputs-area {
+
+ .input-text {
+ color: $color-gray-60;
+ font-size: $fs13;
+ margin: 5px;
+ padding: 5px;
+ width: 100%;
}
- .libraries {
- border-top: 1px solid $color-gray-10;
- padding-top: 0.5rem;
- margin-top: 0.25rem;
- width: 200px;
-
- select {
- background-image: url(/images/icons/arrow-down.svg);
- background-repeat: no-repeat;
- background-position: 95% 48%;
- background-size: 10px;
- margin: 0;
- margin-bottom: 0.5rem;
- width: 100%;
- padding: 2px 0.25rem;
- font-size: 0.75rem;
- color: $color-gray-40;
- border-color: $color-gray-10;
- border-radius: 2px;
+ }
- option {
- padding: 0;
- }
- }
+ .colorpicker-tabs {
+ display: flex;
+ margin-top: 0.25rem;
+ height: 2rem;
+ background-color: $color-gray-10;
- .selected-colors {
- display: grid;
- grid-template-columns: repeat(8, 1fr);
- justify-content: space-between;
- margin-right: -8px;
- overflow-x: hidden;
- overflow-y: auto;
- max-height: 5.5rem;
- }
-
-
- .selected-colors::after {
- content: "";
- flex: auto;
- }
-
- .selected-colors .color-bullet {
- grid-area: auto;
- margin-bottom: 0.25rem;
- cursor: pointer;
-
- &:hover {
- border-color: $color-primary;
- }
-
- &.button {
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- &.button svg {
- width: 12px;
- height: 12px;
- fill: $color-gray-30;
- }
-
- &.plus-button svg {
- width: 8px;
- height: 8px;
- fill: $color-black;
- }
- }
+ .active {
+ background-color: $color-white;
}
- .actions {
- margin-top: 0.5rem;
- display: flex;
- flex-direction: row;
- justify-content: center;
+ .colorpicker-tab {
+ cursor: pointer;
+ display: flex;
+ flex-grow: 1;
+ justify-content: center;
+ align-items: center;
- .btn-primary {
- height: 1.5rem;
- padding: 0 2.5rem;
- font-size: $fs12;
- }
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $color-gray-30;
+ }
}
+ }
}
.color-data {
@@ -265,8 +495,8 @@
position: relative;
.color-name {
- font-size: $fs13;
- margin: 5px 6px 0px 6px;
+ font-size: $fs13;
+ margin: 5px 6px 0px 6px;
}
.color-info {
@@ -310,30 +540,3 @@
}
}
-.colorpicker-tooltip {
- border-radius: $br-small;
- display: flex;
- flex-direction: column;
- left: 1400px;
- top: 100px;
- position: absolute;
- z-index: 11;
- width: auto;
-
- span {
- color: $color-gray-20;
- font-size: $fs12;
- }
-
- .inputs-area {
-
- .input-text {
- color: $color-gray-60;
- font-size: $fs13;
- margin: 5px;
- padding: 5px;
- width: 100%;
- }
-
- }
-}
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index 34c34f550c..4e28fc2cad 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -122,6 +122,9 @@
(def uppercase (icon-xref :uppercase))
(def user (icon-xref :user))
(def tick (icon-xref :tick))
+(def picker-harmony (icon-xref :picker-harmony))
+(def picker-hsv (icon-xref :picker-hsv))
+(def picker-ramp (icon-xref :picker-ramp))
(def loader-pencil
(mf/html
diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs
index 52c0281703..185c5e1f49 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 0000000000..f86b025b0e
--- /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 a1a7c8457f..5cac4b6b38 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/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs
index 85d58aeb34..a8901ba2e1 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs
@@ -10,18 +10,20 @@
(ns app.main.ui.workspace.colorpicker
(:require
[rumext.alpha :as mf]
- [app.main.store :as st]
+ [okulary.core :as l]
[cuerdas.core :as str]
- [app.util.dom :as dom]
- [app.util.color :as uc]
- [app.main.ui.icons :as i]
+ [app.common.geom.point :as gpt]
[app.common.math :as math]
[app.common.uuid :refer [uuid]]
+ [app.util.dom :as dom]
+ [app.util.color :as uc]
+ [app.util.object :as obj]
+ [app.main.store :as st]
+ [app.main.refs :as refs]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.colors :as dwc]
[app.main.data.modal :as modal]
- [okulary.core :as l]
- [app.main.refs :as refs]
+ [app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [t]]))
;; --- Refs
@@ -44,7 +46,7 @@
;; --- Color Picker Modal
-(mf/defc value-selector [{:keys [hue saturation value on-change]}]
+(mf/defc value-saturation-selector [{:keys [hue saturation value on-change]}]
(let [dragging? (mf/use-state false)
calculate-pos
(fn [ev]
@@ -53,7 +55,7 @@
px (math/clamp (/ (- x left) (- right left)) 0 1)
py (* 255 (- 1 (math/clamp (/ (- y top) (- bottom top)) 0 1)))]
(on-change px py)))]
- [:div.value-selector
+ [:div.value-saturation-selector
{:on-mouse-down #(reset! dragging? true)
:on-mouse-up #(reset! dragging? false)
:on-pointer-down (partial dom/capture-pointer)
@@ -64,41 +66,389 @@
:left (str (* 100 saturation) "%")
:top (str (* 100 (- 1 (/ value 255))) "%")}}]]))
-(mf/defc hue-selector [{:keys [hue on-change]}]
- (let [dragging? (mf/use-state false)
- calculate-pos
- (fn [ev]
- (let [{:keys [left right]} (-> ev dom/get-target dom/get-bounding-rect)
- {:keys [x]} (-> ev dom/get-client-position)
- px (math/clamp (/ (- x left) (- right left)) 0 1)]
- (on-change (* px 360))))]
- [:div.hue-selector
- {:on-mouse-down #(reset! dragging? true)
- :on-mouse-up #(reset! dragging? false)
- :on-pointer-down (partial dom/capture-pointer)
- :on-pointer-up (partial dom/release-pointer)
- :on-click calculate-pos
- :on-mouse-move #(when @dragging? (calculate-pos %))}
- [:div.handler {:style {:pointer-events "none"
- :left (str (* (/ hue 360) 100) "%")}}]]))
-(mf/defc opacity-selector [{:keys [opacity on-change]}]
- (let [dragging? (mf/use-state false)
+(mf/defc slider-selector [{:keys [value class min-value max-value vertical? reverse? on-change]}]
+ (let [min-value (or min-value 0)
+ max-value (or max-value 1)
+ dragging? (mf/use-state false)
calculate-pos
(fn [ev]
- (let [{:keys [left right]} (-> ev dom/get-target dom/get-bounding-rect)
- {:keys [x]} (-> ev dom/get-client-position)
- px (math/clamp (/ (- x left) (- right left)) 0 1)]
- (on-change px)))]
- [:div.opacity-selector
- {:on-mouse-down #(reset! dragging? true)
+ (when on-change
+ (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect)
+ {:keys [x y]} (-> ev dom/get-client-position)
+ unit-value (if vertical?
+ (math/clamp (/ (- bottom y) (- bottom top)) 0 1)
+ (math/clamp (/ (- x left) (- right left)) 0 1))
+ unit-value (if reverse?
+ (math/abs (- unit-value 1.0))
+ unit-value)
+ value (+ min-value (* unit-value (- max-value min-value)))]
+ (on-change value))))]
+
+ [:div.slider-selector
+ {:class (str (if vertical? "vertical " "") class)
+ :on-mouse-down #(reset! dragging? true)
:on-mouse-up #(reset! dragging? false)
:on-pointer-down (partial dom/capture-pointer)
:on-pointer-up (partial dom/release-pointer)
:on-click calculate-pos
:on-mouse-move #(when @dragging? (calculate-pos %))}
- [:div.handler {:style {:pointer-events "none"
- :left (str (* opacity 100) "%")}}]]))
+
+ (let [value-percent (* (/ (- value min-value)
+ (- max-value min-value)) 100)
+
+ value-percent (if reverse?
+ (math/abs (- value-percent 100))
+ value-percent)
+ value-percent-str (str value-percent "%")
+
+ style-common #js {:pointerEvents "none"}
+ style-horizontal (obj/merge! #js {:left value-percent-str} style-common)
+ style-vertical (obj/merge! #js {:bottom value-percent-str} style-common)]
+ [:div.handler {:style (if vertical? style-vertical style-horizontal)}])]))
+
+
+(defn create-color-wheel
+ [canvas-node]
+ (let [ctx (.getContext canvas-node "2d")
+ width (obj/get canvas-node "width")
+ height (obj/get canvas-node "height")
+ radius (/ width 2)
+ cx (/ width 2)
+ cy (/ width 2)
+ step 0.2]
+
+ (.clearRect ctx 0 0 width height)
+
+ (doseq [degrees (range 0 360 step)]
+ (let [degrees-rad (math/radians degrees)
+ x (* radius (math/cos (- degrees-rad)))
+ y (* radius (math/sin (- degrees-rad)))]
+ (obj/set! ctx "strokeStyle" (str/format "hsl(%s, 100%, 50%)" degrees))
+ (.beginPath ctx)
+ (.moveTo ctx cx cy)
+ (.lineTo ctx (+ cx x) (+ cy y))
+ (.stroke ctx)))
+
+ (let [grd (.createRadialGradient ctx cx cy 0 cx cx radius)]
+ (.addColorStop grd 0 "white")
+ (.addColorStop grd 1 "rgba(255, 255, 255, 0")
+ (obj/set! ctx "fillStyle" grd)
+
+ (.beginPath ctx)
+ (.arc ctx cx cy radius 0 (* 2 math/PI) true)
+ (.closePath ctx)
+ (.fill ctx))))
+
+(mf/defc ramp-selector [{:keys [color on-change]}]
+ (let [{hue :h saturation :s value :v alpha :alpha} color
+
+ on-change-value-saturation
+ (fn [new-saturation new-value]
+ (let [hex (uc/hsv->hex [hue new-saturation new-value])
+ [r g b] (uc/hex->rgb hex)]
+ (on-change {:hex hex
+ :r r :g g :b b
+ :s new-saturation
+ :v new-value})))
+
+ on-change-hue
+ (fn [new-hue]
+ (let [hex (uc/hsv->hex [new-hue saturation value])
+ [r g b] (uc/hex->rgb hex)]
+ (on-change {:hex hex
+ :r r :g g :b b
+ :h new-hue} )))
+
+ on-change-opacity
+ (fn [new-opacity]
+ (on-change {:alpha new-opacity} ))]
+ [:*
+ [:& value-saturation-selector
+ {:hue hue
+ :saturation saturation
+ :value value
+ :on-change on-change-value-saturation}]
+
+ [:div.shade-selector
+ [:div.color-bullet]
+ [:& slider-selector {:class "hue"
+ :max-value 360
+ :value hue
+ :on-change on-change-hue}]
+
+ [:& slider-selector {:class "opacity"
+ :max-value 1
+ :value alpha
+ :on-change on-change-opacity}]]]))
+
+(defn color->point
+ [canvas-side hue saturation]
+ (let [hue-rad (math/radians (- hue))
+ comp-x (* saturation (math/cos hue-rad))
+ comp-y (* saturation (math/sin hue-rad))
+ x (+ (/ canvas-side 2) (* comp-x (/ canvas-side 2)))
+ y (+ (/ canvas-side 2) (* comp-y (/ canvas-side 2)))]
+ (gpt/point x y)))
+
+(mf/defc harmony-selector [{:keys [color on-change]}]
+ (let [canvas-ref (mf/use-ref nil)
+ {hue :h saturation :s value :v alpha :alpha} color
+
+ canvas-side 152
+ pos-current (color->point canvas-side hue saturation)
+ pos-complement (color->point canvas-side (mod (+ hue 180) 360) saturation)
+ dragging? (mf/use-state false)
+
+ calculate-pos (fn [ev]
+ (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect)
+ {:keys [x y]} (-> ev dom/get-client-position)
+ px (math/clamp (/ (- x left) (- right left)) 0 1)
+ py (math/clamp (/ (- y top) (- bottom top)) 0 1)
+
+ px (- (* 2 px) 1)
+ py (- (* 2 py) 1)
+
+ angle (math/degrees (math/atan2 px py))
+ new-hue (math/precision (mod (- angle 90 ) 360) 2)
+ new-saturation (math/clamp (math/distance [px py] [0 0]) 0 1)
+ hex (uc/hsv->hex [new-hue new-saturation value])
+ [r g b] (uc/hex->rgb hex)]
+ (on-change {:hex hex
+ :r r :g g :b b
+ :h new-hue
+ :s new-saturation})))
+
+ on-change-value (fn [new-value]
+ (let [hex (uc/hsv->hex [hue saturation new-value])
+ [r g b] (uc/hex->rgb hex)]
+ (on-change {:hex hex
+ :r r :g g :b b
+ :v new-value})))
+ on-complement-click (fn [ev]
+ (let [new-hue (mod (+ hue 180) 360)
+ hex (uc/hsv->hex [new-hue saturation value])
+ [r g b] (uc/hex->rgb hex)]
+ (on-change {:hex hex
+ :r r :g g :b b
+ :h new-hue
+ :s saturation})))
+
+ on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))]
+
+ (mf/use-effect
+ (mf/deps canvas-ref)
+ (fn [] (when canvas-ref
+ (create-color-wheel (mf/ref-val canvas-ref)))))
+
+ [:div.harmony-selector
+ [:div.hue-wheel-wrapper
+ [:canvas.hue-wheel
+ {:ref canvas-ref
+ :width canvas-side
+ :height canvas-side
+ :on-mouse-down #(reset! dragging? true)
+ :on-mouse-up #(reset! dragging? false)
+ :on-pointer-down (partial dom/capture-pointer)
+ :on-pointer-up (partial dom/release-pointer)
+ :on-click calculate-pos
+ :on-mouse-move #(when @dragging? (calculate-pos %))}]
+ [:div.handler {:style {:pointer-events "none"
+ :left (:x pos-current)
+ :top (:y pos-current)}}]
+ [:div.handler.complement {:style {:left (:x pos-complement)
+ :top (:y pos-complement)
+ :cursor "pointer"}
+ :on-click on-complement-click}]]
+ [:div.handlers-wrapper
+ [:& slider-selector {:class "value"
+ :vertical? true
+ :reverse? true
+ :value value
+ :max-value 255
+ :vertical true
+ :on-change on-change-value}]
+ [:& slider-selector {:class "opacity"
+ :vertical? true
+ :value alpha
+ :max-value 1
+ :vertical true
+ :on-change on-change-opacity}]]]))
+
+(mf/defc hsva-selector [{:keys [color on-change]}]
+ (let [{hue :h saturation :s value :v alpha :alpha} color
+ handle-change-slider (fn [key]
+ (fn [new-value]
+ (let [change (hash-map key new-value)
+ {:keys [h s v]} (merge color change)
+ hex (uc/hsv->hex [h s v])
+ [r g b] (uc/hex->rgb hex)]
+ (on-change (merge change
+ {:hex hex
+ :r r :g g :b b})))))
+ on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))]
+ [:div.hsva-selector
+ [:span.hsva-selector-label "H"]
+ [:& slider-selector
+ {:class "hue" :max-value 360 :value hue :on-change (handle-change-slider :h)}]
+
+ [:span.hsva-selector-label "S"]
+ [:& slider-selector
+ {:class "saturation" :max-value 1 :value saturation :on-change (handle-change-slider :s)}]
+
+ [:span.hsva-selector-label "V"]
+ [:& slider-selector
+ {:class "value" :reverse? true :max-value 255 :value value :on-change (handle-change-slider :v)}]
+
+ [:span.hsva-selector-label "A"]
+ [:& slider-selector
+ {:class "opacity" :max-value 1 :value alpha :on-change on-change-opacity}]]))
+
+(mf/defc color-inputs [{:keys [type color on-change]}]
+ (let [{red :r green :g blue :b
+ hue :h saturation :s value :v
+ hex :hex alpha :alpha} color
+
+ parse-hex (fn [val] (if (= (first val) \#) val (str \# val)))
+
+ refs {:hex (mf/use-ref nil)
+ :r (mf/use-ref nil)
+ :g (mf/use-ref nil)
+ :b (mf/use-ref nil)
+ :h (mf/use-ref nil)
+ :s (mf/use-ref nil)
+ :v (mf/use-ref nil)
+ :alpha (mf/use-ref nil)}
+
+ on-change-hex
+ (fn [e]
+ (let [val (-> e dom/get-target-val parse-hex)]
+ (when (uc/hex? val)
+ (let [[r g b] (uc/hex->rgb val)
+ [h s v] (uc/hex->hsv hex)]
+ (on-change {:hex val
+ :h h :s s :v v
+ :r r :g g :b b})))))
+
+ on-change-property
+ (fn [property max-value]
+ (fn [e]
+ (let [val (-> e dom/get-target-val (math/clamp 0 max-value))
+ val (if (#{:s} property) (/ val 100) val)]
+ (when (not (nil? val))
+ (if (#{:r :g :b} property)
+ (let [{:keys [r g b]} (merge color (hash-map property val))
+ hex (uc/rgb->hex [r g b])
+ [h s v] (uc/hex->hsv hex)]
+ (on-change {:hex hex
+ :h h :s s :v v
+ :r r :g g :b b}))
+
+ (let [{:keys [h s v]} (merge color (hash-map property val))
+ hex (uc/hsv->hex [h s v])
+ [r g b] (uc/hex->rgb hex)]
+ (on-change {:hex hex
+ :h h :s s :v v
+ :r r :g g :b b})))))))
+
+ on-change-opacity
+ (fn [e]
+ (when-let [new-alpha (-> e dom/get-target-val (math/clamp 0 100) (/ 100))]
+ (on-change {:alpha new-alpha})))]
+
+
+ ;; Updates the inputs values when a property is changed in the parent
+ (mf/use-effect
+ (mf/deps color type)
+ (fn []
+ (doseq [ref-key (keys refs)]
+ (let [property-val (get color ref-key)
+ property-ref (get refs ref-key)]
+ (when (and property-val property-ref)
+ (when-let [node (mf/ref-val property-ref)]
+ (case ref-key
+ (:s :alpha) (dom/set-value! node (math/round (* property-val 100)))
+ :hex (dom/set-value! node property-val)
+ (dom/set-value! node (math/round property-val)))))))))
+
+ [:div.color-values
+ [:input {:id "hex-value"
+ :ref (:hex refs)
+ :default-value hex
+ :on-change on-change-hex}]
+
+ (if (= type :rgb)
+ [:*
+ [:input {:id "red-value"
+ :ref (:r refs)
+ :type "number"
+ :min 0
+ :max 255
+ :default-value red
+ :on-change (on-change-property :r 255)}]
+
+ [:input {:id "green-value"
+ :ref (:g refs)
+ :type "number"
+ :min 0
+ :max 255
+ :default-value green
+ :on-change (on-change-property :g 255)}]
+
+ [:input {:id "blue-value"
+ :ref (:b refs)
+ :type "number"
+ :min 0
+ :max 255
+ :default-value blue
+ :on-change (on-change-property :b 255)}]]
+ [:*
+ [:input {:id "hue-value"
+ :ref (:h refs)
+ :type "number"
+ :min 0
+ :max 360
+ :default-value hue
+ :on-change (on-change-property :h 360)}]
+
+ [:input {:id "saturation-value"
+ :ref (:s refs)
+ :type "number"
+ :min 0
+ :max 100
+ :step 1
+ :default-value saturation
+ :on-change (on-change-property :s 100)}]
+
+ [:input {:id "value-value"
+ :ref (:v refs)
+ :type "number"
+ :min 0
+ :max 255
+ :default-value value
+ :on-change (on-change-property :v 255)}]])
+
+ [:input.alpha-value {:id "alpha-value"
+ :ref (:alpha refs)
+ :type "number"
+ :min 0
+ :step 1
+ :max 100
+ :default-value (if (= alpha :multiple) "" (math/precision alpha 2))
+ :on-change on-change-opacity}]
+
+ [:label.hex-label {:for "hex-value"} "HEX"]
+ (if (= type :rgb)
+ [:*
+ [:label.red-label {:for "red-value"} "R"]
+ [:label.green-label {:for "green-value"} "G"]
+ [:label.blue-label {:for "blue-value"} "B"]]
+ [:*
+ [:label.red-label {:for "hue-value"} "H"]
+ [:label.green-label {:for "saturation-value"} "S"]
+ [:label.blue-label {:for "value-value"} "V"]])
+ [:label.alpha-label {:for "alpha-value"} "A"]]))
+
(defn as-color-components [value opacity]
(let [value (if (uc/hex? value) value "#000000")
@@ -108,12 +458,13 @@
{:hex (or value "000000")
:alpha (or opacity 1)
:r r :g g :b b
- :h h :s s :v v}
- ))
+ :h h :s s :v v}))
(mf/defc colorpicker
[{:keys [value opacity on-change on-accept]}]
(let [current-color (mf/use-state (as-color-components value opacity))
+
+ active-tab (mf/use-state :ramp #_:harmony #_:hsva)
selected-library (mf/use-state "recent")
current-library-colors (mf/use-state [])
ref-picker (mf/use-ref)
@@ -136,7 +487,16 @@
parse-selected (fn [selected]
(if (#{"recent" "file"} selected)
(keyword selected)
- (uuid selected)) )]
+ (uuid selected)) )
+
+ change-tab (fn [tab] #(reset! active-tab tab))
+
+ handle-change-color (fn [changes]
+ (swap! current-color merge changes)
+ (when (:hex changes)
+ (reset! value-ref (:hex changes)))
+ (on-change (:hex changes (:hex @current-color))
+ (:alpha changes (:alpha @current-color))))]
;; Update state when there is a change in the props upstream
(mf/use-effect
@@ -149,9 +509,19 @@
(mf/deps @current-color)
(fn [] (let [node (mf/ref-val ref-picker)
rgb [(:r @current-color) (:g @current-color) (:b @current-color)]
- hue-rgb (uc/hsv->rgb [(:h @current-color) 1.0 255])]
+ hue-rgb (uc/hsv->rgb [(:h @current-color) 1.0 255])
+ hsl-from (uc/hsv->hsl [(:h @current-color) 0 (:v @current-color)])
+ hsl-to (uc/hsv->hsl [(:h @current-color) 1 (:v @current-color)])
+
+ format-hsl (fn [[h s l]]
+ (str/fmt "hsl(%s, %s, %s)"
+ h
+ (str (* s 100) "%")
+ (str (* l 100) "%")))]
(dom/set-css-property node "--color" (str/join ", " rgb))
- (dom/set-css-property node "--hue" (str/join ", " hue-rgb)))))
+ (dom/set-css-property node "--hue-rgb" (str/join ", " hue-rgb))
+ (dom/set-css-property node "--saturation-grad-from" (format-hsl hsl-from))
+ (dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to)))))
;; Load library colors when the select is changed
(mf/use-effect
@@ -204,168 +574,78 @@
(on-change (:hex @current-color) (:alpha @current-color) nil nil picked-shift?))))
[:div.colorpicker {:ref ref-picker}
- [:div.top-actions
- [:button.picker-btn
- {:class (when picking-color? "active")
- :on-click (fn []
- (modal/allow-click-outside!)
- (st/emit! (dwc/start-picker)))}
- i/picker]]
+ [:div.colorpicker-content
+ [:div.top-actions
+ [:button.picker-btn
+ {:class (when picking-color? "active")
+ :on-click (fn []
+ (modal/allow-click-outside!)
+ (st/emit! (dwc/start-picker)))}
+ i/picker]
- (if picking-color?
- [:div.picker-detail-wrapper
- [:div.center-circle]
- [:canvas#picker-detail {:width 200
- :height 160}]]
- [:& value-selector {:hue (:h @current-color)
- :saturation (:s @current-color)
- :value (:v @current-color)
- :on-change (fn [s v]
- (let [hex (uc/hsv->hex [(:h @current-color) s v])
- [r g b] (uc/hex->rgb hex)]
- (swap! current-color assoc
- :hex hex
- :r r :g g :b b
- :s s :v v)
- (reset! value-ref hex)
- (on-change hex (:alpha @current-color))))}])
- (when (not picking-color?)
- [:div.shade-selector
- [:div.color-bullet]
- [:& hue-selector {:hue (:h @current-color)
- :on-change (fn [h]
- (let [hex (uc/hsv->hex [h (:s @current-color) (:v @current-color)])
- [r g b] (uc/hex->rgb hex)]
- (swap! current-color assoc
- :hex hex
- :r r :g g :b b
- :h h )
- (reset! value-ref hex)
- (on-change hex (:alpha @current-color))))}]
- [:& opacity-selector {:opacity (:alpha @current-color)
- :on-change (fn [alpha]
- (swap! current-color assoc :alpha alpha)
- (on-change (:hex @current-color) alpha))}]])
+ [:div.gradients-buttons
+ [:button.gradient.linear-gradient #_{:class "active"}]
+ [:button.gradient.radial-gradient]]]
- [:div.color-values
- [:input.hex-value {:id "hex-value"
- :value (:hex @current-color)
- :on-change (fn [e]
- (let [val (-> e dom/get-target dom/get-value)
- val (if (= (first val) \#) val (str \# val))]
- (swap! current-color assoc :hex val)
- (when (uc/hex? val)
- (reset! value-ref val)
- (let [[r g b] (uc/hex->rgb val)
- [h s v] (uc/hex->hsv val)]
+ #_[:div.gradient-stops
+ [:div.gradient-background {:style {:background "linear-gradient(90deg, #EC0BE5, #CDCDCD)" }}]
+ [:div.gradient-stop-wrapper
+ [:div.gradient-stop.start {:style {:background-color "#EC0BE5"}}]
+ [:div.gradient-stop.end {:style {:background-color "#CDCDCD"
+ :left "100%"}}]]]
+
+ (if picking-color?
+ [:div.picker-detail-wrapper
+ [:div.center-circle]
+ [:canvas#picker-detail {:width 200 :height 160}]]
+ (case @active-tab
+ :ramp [:& ramp-selector {:color @current-color :on-change handle-change-color}]
+ :harmony [:& harmony-selector {:color @current-color :on-change handle-change-color}]
+ :hsva [:& hsva-selector {:color @current-color :on-change handle-change-color}]
+ nil))
+
+ [:& color-inputs {:type (if (= @active-tab :hsva) :hsv :rgb) :color @current-color :on-change handle-change-color}]
+
+ [:div.libraries
+ [:select {:on-change (fn [e]
+ (let [val (-> e dom/get-target dom/get-value)]
+ (reset! selected-library val)))
+ :value @selected-library}
+ [:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")]
+ [:option {:value "file"} (t locale "workspace.libraries.colors.file-library")]
+ (for [[_ {:keys [name id]}] shared-libs]
+ [:option {:key id
+ :value id} name])]
+
+ [:div.selected-colors
+ (when (= "file" @selected-library)
+ [:div.color-bullet.button.plus-button {:style {:background-color "white"}
+ :on-click #(st/emit! (dwl/add-color (:hex @current-color)))}
+ i/plus])
+
+ [:div.color-bullet.button {:style {:background-color "white"}
+ :on-click #(st/emit! (dwc/show-palette (parse-selected @selected-library)))}
+ i/palette]
+
+ (for [[idx {:keys [id file-id value]}] (map-indexed vector @current-library-colors)]
+ [:div.color-bullet {:key (str "color-" idx)
+ :on-click (fn []
+ (swap! current-color assoc :hex value)
+ (reset! value-ref value)
+ (let [[r g b] (uc/hex->rgb value)
+ [h s v] (uc/hex->hsv value)]
(swap! current-color assoc
:r r :g g :b b
:h h :s s :v v)
- (on-change val (:alpha @current-color))))))}]
- [:input.red-value {:id "red-value"
- :type "number"
- :min 0
- :max 255
- :value (:r @current-color)
- :on-change (fn [e]
- (let [val (-> e dom/get-target dom/get-value (math/clamp 0 255))]
- (swap! current-color assoc :r val)
- (when (not (nil? val))
- (let [{:keys [g b]} @current-color
- hex (uc/rgb->hex [val g b])
- [h s v] (uc/hex->hsv hex)]
- (reset! value-ref hex)
- (swap! current-color assoc
- :hex hex
- :h h :s s :v v)
- (on-change hex (:alpha @current-color))))))}]
- [:input.green-value {:id "green-value"
- :type "number"
- :min 0
- :max 255
- :value (:g @current-color)
- :on-change (fn [e]
- (let [val (-> e dom/get-target dom/get-value (math/clamp 0 255))]
- (swap! current-color assoc :g val)
- (when (not (nil? val))
- (let [{:keys [r b]} @current-color
- hex (uc/rgb->hex [r val b])
- [h s v] (uc/hex->hsv hex)]
- (reset! value-ref hex)
- (swap! current-color assoc
- :hex hex
- :h h :s s :v v)
- (on-change hex (:alpha @current-color))))))}]
- [:input.blue-value {:id "blue-value"
- :type "number"
- :min 0
- :max 255
- :value (:b @current-color)
- :on-change (fn [e]
- (let [val (-> e dom/get-target dom/get-value (math/clamp 0 255))]
- (swap! current-color assoc :b val)
- (when (not (nil? val))
- (let [{:keys [r g]} @current-color
- hex (uc/rgb->hex [r g val])
- [h s v] (uc/hex->hsv hex)]
- (reset! value-ref hex)
- (swap! current-color assoc
- :hex hex
- :h h :s s :v v)
- (on-change hex (:alpha @current-color))))))}]
- [:input.alpha-value {:id "alpha-value"
- :type "number"
- :min 0
- :step 0.1
- :max 1
- :value (if (= (:alpha @current-color) :multiple)
- ""
- (math/precision (:alpha @current-color) 2))
- :on-change (fn [e]
- (let [val (-> e dom/get-target dom/get-value (math/clamp 0 1))]
- (swap! current-color assoc :alpha val)
- (on-change (:hex @current-color) val)))}]
- [:label.hex-label {:for "hex-value"} "HEX"]
- [:label.red-label {:for "red-value"} "R"]
- [:label.green-label {:for "green-value"} "G"]
- [:label.blue-label {:for "blue-value"} "B"]
- [:label.alpha-label {:for "alpha-value"} "A"]]
-
- [:div.libraries
- [:select {:on-change (fn [e]
- (let [val (-> e dom/get-target dom/get-value)]
- (reset! selected-library val)))
- :value @selected-library}
- [:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")]
- [:option {:value "file"} (t locale "workspace.libraries.colors.file-library")]
- (for [[_ {:keys [name id]}] shared-libs]
- [:option {:key id
- :value id} name])]
-
- [:div.selected-colors
- (when (= "file" @selected-library)
- [:div.color-bullet.button.plus-button {:style {:background-color "white"}
- :on-click #(st/emit! (dwl/add-color (:hex @current-color)))}
- i/plus])
-
- [:div.color-bullet.button {:style {:background-color "white"}
- :on-click #(st/emit! (dwc/show-palette (parse-selected @selected-library)))}
- i/palette]
-
- (for [[idx {:keys [id file-id value]}] (map-indexed vector @current-library-colors)]
- [:div.color-bullet {:key (str "color-" idx)
- :on-click (fn []
- (swap! current-color assoc :hex value)
- (reset! value-ref value)
- (let [[r g b] (uc/hex->rgb value)
- [h s v] (uc/hex->hsv value)]
- (swap! current-color assoc
- :r r :g g :b b
- :h h :s s :v v)
- (on-change value (:alpha @current-color) id file-id)))
- :style {:background-color value}}])]
-
- ]
+ (on-change value (:alpha @current-color) id file-id)))
+ :style {:background-color value}}])]]]
+ [:div.colorpicker-tabs
+ [:div.colorpicker-tab {:class (when (= @active-tab :ramp) "active")
+ :on-click (change-tab :ramp)} i/picker-ramp]
+ [:div.colorpicker-tab {:class (when (= @active-tab :harmony) "active")
+ :on-click (change-tab :harmony)} i/picker-harmony]
+ [:div.colorpicker-tab {:class (when (= @active-tab :hsva) "active")
+ :on-click (change-tab :hsva)} i/picker-hsv]]
(when on-accept
[:div.actions
[:button.btn-primary.btn-large
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 0000000000..0fc1e92a8c
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/gradients.cljs
@@ -0,0 +1,292 @@
+;; 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=")
+
+#_(def checkboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
+
+(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 8e54075e58..b5e281510b 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