diff --git a/common/app/common/geom/point.cljc b/common/app/common/geom/point.cljc index 54f3f59f4..65b453b56 100644 --- a/common/app/common/geom/point.cljc +++ b/common/app/common/geom/point.cljc @@ -211,6 +211,7 @@ (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 diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc index d49bcf42c..9125c7c35 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 000000000..c108e2812 --- /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 000000000..2218c82a1 --- /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 000000000..0e078a017 --- /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 f486028b4..be86a1808 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 2ff6badc4..9b3d268e4 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 34c34f550..4e28fc2ca 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/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 85d58aeb3..a8901ba2e 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 index dad8b2099..0fc1e92a8 100644 --- a/frontend/src/app/main/ui/workspace/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/gradients.cljs @@ -91,6 +91,8 @@ (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) diff --git a/frontend/src/app/main/ui/workspace/selection.cljs b/frontend/src/app/main/ui/workspace/selection.cljs index 41480a39a..b5e281510 100644 --- a/frontend/src/app/main/ui/workspace/selection.cljs +++ b/frontend/src/app/main/ui/workspace/selection.cljs @@ -212,7 +212,7 @@ :resize-point [:> resize-point-handler props] :resize-side [:> resize-side-handler props]))) - (when (= :rect (:type shape)) + #_(when (= :rect (:type shape)) [:& gradient-handlers {:shape tr-shape :zoom zoom}])])))