From 12a2b35b286ac45a49ff2ef9e297f1d9afc2cd3b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 3 Sep 2020 08:11:27 +0200 Subject: [PATCH] :tada: New colorpicker --- .../styles/main/partials/colorpicker.scss | 195 ++++++++++++++++ .../app/main/ui/workspace/colorpicker.cljs | 221 +++++++++++++++++- frontend/src/app/util/color.cljs | 14 ++ frontend/src/app/util/dom.cljs | 3 + 4 files changed, 428 insertions(+), 5 deletions(-) diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index f143d882b6..757d51dbd1 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -5,6 +5,201 @@ // Copyright (c) 2015-2016 Andrey Antukh // Copyright (c) 2015-2016 Juan de la Cruz +.colorpicker-v2 { + display: flex; + flex-direction: column; + width: 13rem; + padding: 0.5rem; + background-color: $color-white; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + + .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; + width: 100%; + + .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; + } + + .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; + } + + .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); + left: 50%; + cursor: pointer; + } + + .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%; + height: 100%; + } + + .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; + + 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: scroll; + 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; + } + } + } +} + .color-picker { display: flex; flex-direction: column; diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 31ec33d0de..ff8ba0a4bc 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -9,20 +9,231 @@ (:require [rumext.alpha :as mf] [app.main.store :as st] - [app.main.ui.colorpicker :as cp])) + [app.main.ui.colorpicker :as cp] + + [cuerdas.core :as str] + [app.util.dom :as dom] + [app.util.color :as uc] + [app.main.ui.icons :as i] + [app.common.math :as math])) ;; --- Color Picker Modal +(mf/defc value-selector [{:keys [saturation luminance on-change]}] + (let [dragging? (mf/use-state false)] + [:div.value-selector + {:on-mouse-down #(reset! dragging? true) + :on-mouse-up #(reset! dragging? false) + :on-mouse-move + (fn [ev] + (when @dragging? + (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) + {:keys [x y]} (-> ev dom/get-client-position) + px (/ (- x left) (- right left)) + py (/ (- y top) (- bottom top)) + + luminance (* (- 1.0 py) (- 1 (* 0.5 px)))] + + (on-change px luminance))))} + [:div.handler {:style {:pointer-events "none" + :left (str (* saturation 100) "%") + :top (str (* (- 1 (/ luminance (- 1 (* 0.5 saturation))) ) 100) "%")}}]])) + +(mf/defc hue-selector [{:keys [hue on-change]}] + (let [dragging? (mf/use-state false)] + [:div.hue-selector + {:on-mouse-down #(reset! dragging? true) + :on-mouse-up #(reset! dragging? false) + :on-mouse-move + (fn [ev] + (when @dragging? + (let [{:keys [left right]} (-> ev dom/get-target dom/get-bounding-rect) + {:keys [x]} (-> ev dom/get-client-position) + px (/ (- x left) (- right left))] + (on-change (* px 360)))))} + [: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)] + [:div.opacity-selector + {:on-mouse-down #(reset! dragging? true) + :on-mouse-up #(reset! dragging? false) + :on-mouse-move + (fn [ev] + (when @dragging? + (let [{:keys [left right]} (-> ev dom/get-target dom/get-bounding-rect) + {:keys [x]} (-> ev dom/get-client-position) + px (/ (- x left) (- right left))] + (on-change px))))} + [:div.handler {:style {:pointer-events "none" + :left (str (* opacity 100) "%")}}]])) + +(defn as-color-state [value opacity] + (let [[r g b] (uc/hex->rgb (or value "000000")) + [h s l] (uc/hex->hsl (or value "000000"))] + {:hex (or value "000000") + :alpha (or opacity 1) + :r r + :g g + :b b + :h h + :s s + :l l})) + +(mf/defc colorpicker + [{:keys [value opacity]}] + (let [state (mf/use-state (as-color-state value opacity)) + ref-picker (mf/use-ref)] + + (mf/use-effect (mf/deps value opacity) + (fn [] + (reset! state (as-color-state value opacity)))) + + (mf/use-effect (mf/deps state) + (fn [] (let [node (mf/ref-val ref-picker) + rgb [(:r @state) (:g @state) (:b @state)] + hue-rgb (uc/hsl->rgb (:h @state) 1.0 0.5)] + (dom/set-css-property node "--color" (str/join ", " rgb)) + (dom/set-css-property node "--hue" (str/join ", " hue-rgb))))) + [:div.colorpicker-v2 {:ref ref-picker} + [:& value-selector {:luminance (:l @state) + :saturation (:s @state) + :on-change (fn [s l] + (let [hex (uc/hsl->hex (:h @state) s l) + [r g b] (uc/hex->rgb hex)] + (swap! state assoc + :hex hex + :r r :g g :b b + :s s :l l)))}] + [:div.shade-selector + [:div.color-bullet] + [:& hue-selector {:hue (:h @state) + :on-change (fn [h] + (let [hex (uc/hsl->hex h (:s @state) (:l @state)) + [r g b] (uc/hex->rgb hex)] + (swap! state assoc + :hex hex + :r r :g g :b b + :h h )))}] + [:& opacity-selector {:opacity (:alpha @state) + :on-change (fn [alpha] + (swap! state assoc + :alpha alpha))}]] + + [:div.color-values + [:input.hex-value {:id "hex-value" + :value (:hex @state) + :on-change (fn [e] + (let [val (-> e dom/get-target dom/get-value) + val (if (= (first val) \#) val (str \# val))] + (swap! state assoc :hex val) + (when (uc/hex? val) + (let [[r g b] (uc/hex->rgb val) + [h s l] (uc/hex->hsl val)] + (swap! state assoc + :r r :g g :b b + :h h :s s :l l)))))}] + [:input.red-value {:id "red-value" + :type "number" + :min 0 + :max 255 + :value (:r @state) + :on-change (fn [e] + (let [val (-> e dom/get-target dom/get-value) + val (if (> val 255) 255 val) + val (if (< val 0) 0 val)] + (swap! state assoc :r val) + (when (not (nil? val)) + (let [{:keys [g b]} @state + hex (uc/rgb->hex [val g b]) + [h s l] (uc/hex->hsl hex)] + (swap! state assoc + :hex hex + :h h :s s :l l)))))}] + [:input.green-value {:id "green-value" + :type "number" + :min 0 + :max 255 + :value (:g @state) + :on-change (fn [e] + (let [val (-> e dom/get-target dom/get-value) + val (if (> val 255) 255 val) + val (if (< val 0) 0 val)] + (swap! state assoc :g val) + (when (not (nil? val)) + (let [{:keys [r b]} @state + hex (uc/rgb->hex [r val b]) + [h s l] (uc/hex->hsl hex)] + (swap! state assoc + :hex hex + :h h :s s :l l)))))}] + [:input.blue-value {:id "blue-value" + :type "number" + :min 0 + :max 255 + :value (:b @state) + :on-change (fn [e] + (let [val (-> e dom/get-target dom/get-value) + val (if (> val 255) 255 val) + val (if (< val 0) 0 val)] + (swap! state assoc :b val) + (when (not (nil? val)) + (let [{:keys [r g]} @state + hex (uc/rgb->hex [r g val]) + [h s l] (uc/hex->hsl hex)] + (swap! state assoc + :hex hex + :h h :s s :l l)))))}] + [:input.alpha-value {:id "alpha-value" + :type "number" + :min 0 + :step 0.1 + :max 1 + :value (math/precision (:alpha @state) 2) + :on-change (fn [e] + (let [val (-> e dom/get-target dom/get-value) + val (if (> val 1) 1 val) + val (if (< val 0) 0 val)] + (swap! state assoc :alpha 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 + [:option {:value :recent} "Recent colors"] + [:option {:value :file} "File library"] + [:option {:value #uuid "f5d51910-ab23-11ea-ac38-e1abed64181a" } "TAIGA library"]] + + [:div.selected-colors + [:div.color-bullet.button.plus-button {:style {:background-color "white"}} + i/plus] + + [:div.color-bullet.button {:style {:background-color "white"}} + i/palette] + + #_(for [j (range 0 40)] + [:div.color-bullet {:style {:background-color "#E8E9EA"}}])]]]) + ) + (mf/defc colorpicker-modal [{:keys [x y default value opacity page on-change disable-opacity] :as props}] [:div.modal-overlay.transparent [:div.colorpicker-tooltip {:style {:left (str (- x 270) "px") :top (str (- y 50) "px")}} - [:& cp/colorpicker {:value (or value default) + #_[:& cp/colorpicker {:value (or value default) :opacity (or opacity 1) :colors (into-array @cp/most-used-colors) :on-change on-change - :disable-opacity disable-opacity}]]]) - - + :disable-opacity disable-opacity}] + [:& colorpicker {:value (or value default) + :opacity (or opacity 1) + :colors (into-array @cp/most-used-colors) + :on-change on-change + :disable-opacity disable-opacity}] + ] + ]) diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index e861701fdc..3ce0401a62 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -52,6 +52,20 @@ (-> (hex->rgb data) (conj opacity))) +(defn hex->hsl [hex] + (try + (into [] (gcolor/hexToHsl hex)) + (catch :default e (do + (.log js/console e) + [0 0 0])))) + +(defn hsl->rgb + [h s l] + (gcolor/hslToRgb h s l)) + +(defn hsl->hex [h s l] + (gcolor/hslToHex h s l)) + (defn hex? [v] (and (string? v) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 8b28b400f1..1cb1673cd6 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -203,3 +203,6 @@ [b] {:pre [(blob? b)]} (js/URL.createObjectURL b)) + +(defn set-css-property [node property value] + (.setProperty (.-style node) property value))