Fixes problems with pixel picker

This commit is contained in:
alonso.torres 2020-10-14 18:12:15 +02:00
parent 245c39b1f6
commit 9f0a443b5c
10 changed files with 248 additions and 245 deletions

View file

@ -297,10 +297,10 @@
(s/def :internal.color/name ::string)
(s/def :internal.color/value ::string)
(s/def :internal.color/color ::string)
(s/def :internal.color/opacity ::safe-number)
(s/def :internal.color/gradient ::gradient)
(s/def :internal.color/value (s/nilable ::string))
(s/def :internal.color/color (s/nilable ::string))
(s/def :internal.color/opacity (s/nilable ::safe-number))
(s/def :internal.color/gradient (s/nilable ::gradient))
(s/def ::color
(s/keys :req-un [::id

View file

@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
<path fill="#fff" d="M.607 13.076v2.401l2.224.025 7.86-7.405s-.05-.885-.253-1.087c-.202-.202-2.25-1.744-2.25-1.744z"/>
<path fill="#000" d="M.343 15.974a.514.514 0 01-.317-.321c-.023-.07-.026-.23-.026-1.43 0-1.468-.001-1.445.09-1.586.02-.032 1.703-1.724 3.74-3.759a596.805 596.805 0 003.7-3.716c0-.009-.367-.384-.816-.833a29.9 29.9 0 01-.817-.833c0-.01.474-.49 1.054-1.07l1.053-1.053.948.946.947.947 1.417-1.413C12.366.806 12.765.418 12.856.357c.238-.161.52-.28.792-.334.17-.034.586-.03.76.008.801.173 1.41.794 1.57 1.603.03.15.03.569 0 .718a2.227 2.227 0 01-.334.793c-.061.09-.45.49-1.496 1.54L12.734 6.1l.947.948.947.947-1.053 1.054c-.58.58-1.061 1.054-1.07 1.054-.01 0-.384-.368-.833-.817-.45-.45-.824-.817-.834-.817-.009 0-1.68 1.666-3.716 3.701a493.093 493.093 0 01-3.759 3.74c-.14.091-.117.09-1.59.089-1.187 0-1.366-.004-1.43-.027zm6.024-4.633a592.723 592.723 0 003.663-3.68c0-.02-1.67-1.69-1.69-1.69-.01 0-1.666 1.648-3.68 3.663L.996 13.297v.834c0 .627.005.839.02.854.015.014.227.02.854.02h.833l3.664-3.664z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 981 B

Before After
Before After

View file

@ -29,7 +29,7 @@
border: none;
cursor: pointer;
&.active,
&.active svg,
&:hover svg {
fill: $color-primary;
}

View file

@ -133,36 +133,6 @@
(map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids)
(dwc/update-shapes shape-ids update-fn))))))))
#_(defn change-fill
([ids color id file-id]
(change-fill ids color 1 id file-id))
([ids color opacity id file-id]
(ptk/reify ::change-fill
ptk/WatchEvent
(watch [_ state s]
(let [pid (:current-page-id state)
objects (get-in state [:workspace-data :pages-index pid :objects])
children (mapcat #(cph/get-children % objects) ids)
ids (into ids children)
is-text? #(= :text (:type (get objects %)))
text-ids (filter is-text? ids)
shape-ids (filter (comp not is-text?) ids)
attrs (cond-> {:fill-color color
:fill-color-ref-id id
:fill-color-ref-file file-id}
(and opacity (not= opacity :multiple)) (assoc :fill-opacity opacity))
update-fn (fn [shape] (merge shape attrs))
editors (get-in state [:workspace-local :editors])
reduce-fn (fn [state id]
(update-in state [:workspace-data :pages-index pid :objects id] update-fn))]
(rx/from (conj
(map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids)
(dwc/update-shapes shape-ids update-fn))))))))
(defn change-stroke [ids color]
(ptk/reify ::change-stroke
ptk/WatchEvent
@ -185,49 +155,44 @@
:stroke-opacity 1)))]
(rx/of (dwc/update-shapes ids update-fn))))))
#_(defn change-stroke [ids color id file-id]
(ptk/reify ::change-stroke
ptk/WatchEvent
(watch [_ state s]
(let [objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects])
children (mapcat #(cph/get-children % objects) ids)
ids (into ids children)
update-fn (fn [s]
(cond-> s
true
(assoc :stroke-color color
:stroke-color-ref-id id
:stroke-color-ref-file file-id)
(= (:stroke-style s) :none)
(assoc :stroke-style :solid
:stroke-width 1
:stroke-opacity 1)))]
(rx/of (dwc/update-shapes ids update-fn))))))
(defn picker-for-selected-shape []
;; TODO: replace st/emit! by a subject push and set that in the WatchEvent
(let [handle-change-color
(fn [color shift?]
(let [ids (get-in @st/state [:workspace-local :selected])]
(st/emit!
(if shift?
(change-stroke ids color)
(change-fill ids color))
(md/hide))))]
(ptk/reify ::start-picker
(let [sub (rx/subject)]
(ptk/reify ::picker-for-selected-shape
ptk/WatchEvent
(watch [_ state stream]
(let [ids (get-in state [:workspace-local :selected])
stop? (->> stream
(rx/filter (ptk/type? ::stop-picker)))
update-events (fn [[color shift?]]
(rx/of (if shift?
(change-stroke ids color)
(change-fill ids color))
(stop-picker)))]
(rx/merge
;; Stream that updates the stroke/width and stops if `esc` pressed
(->> sub
(rx/take-until stop?)
(rx/flat-map update-events))
;; Hide the modal if the stop event is emitted
(->> stop?
(rx/first)
(rx/map #(md/hide))))))
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :picking-color?] true)
(assoc ::md/modal {:id (random-uuid)
:type :colorpicker
:props {:on-change handle-change-color}
:allow-click-outside true}))))))
(let [handle-change-color (fn [color shift?] (rx/push! sub [color shift?]))]
(-> state
(assoc-in [:workspace-local :picking-color?] true)
(assoc ::md/modal {:id (random-uuid)
:data {:color "#000000" :opacity 1}
:type :colorpicker
:props {:on-change handle-change-color}
:allow-click-outside true})))))))
(defn select-gradient-stop [spot]
(ptk/reify ::start-picker
(ptk/reify ::select-gradient-stop
ptk/UpdateEvent
(update [_ state]
(-> state

View file

@ -50,7 +50,9 @@
(ptk/reify ::update-modal
ptk/UpdateEvent
(update [_ state]
(c/update state ::modal merge options))))
(cond-> state
(::modal state)
(c/update ::modal merge options)))))
(defn show!
[type props]

View file

@ -20,10 +20,10 @@
(:import goog.events.EventType))
(defn- on-esc-clicked
[event]
(when (k/esc? event)
(st/emit! (dm/hide))
(dom/stop-propagation event)))
[event allow-click-outside]
(when (and (k/esc? event) (not allow-click-outside))
(do (dom/stop-propagation event)
(st/emit! (dm/hide)))))
(defn- on-pop-state
[event]
@ -62,16 +62,23 @@
(let [data (unchecked-get props "data")
wrapper-ref (mf/use-ref nil)
allow-click-outside (:allow-click-outside data)
handle-click-outside
(fn [event]
(on-click-outside event wrapper-ref (:type data) (:allow-click-outside data)))]
(on-click-outside event wrapper-ref (:type data) allow-click-outside))
handle-keydown
(fn [event]
(on-esc-clicked event allow-click-outside))]
(mf/use-layout-effect
(mf/deps allow-click-outside)
(fn []
(let [keys [(events/listen js/document EventType.KEYDOWN on-esc-clicked)
(let [keys [(events/listen js/document EventType.KEYDOWN handle-keydown)
(events/listen js/window EventType.POPSTATE on-pop-state)
(events/listen js/document EventType.CLICK handle-click-outside)]]
#(for [key keys]
#(doseq [key keys]
(events/unlistenByKey key)))))
[:div.modal-wrapper {:ref wrapper-ref}

View file

@ -121,6 +121,9 @@
ref-picker (mf/use-ref)
dirty? (mf/use-var false)
last-color (mf/use-var data)
picking-color? (mf/deref picking-color?)
picked-color (mf/deref picked-color)
picked-color-select (mf/deref picked-color-select)
@ -128,8 +131,6 @@
editing-spot-state (mf/deref editing-spot-state-ref)
;; data-ref (mf/use-var data)
current-color (:current-color @state)
change-tab
@ -140,15 +141,9 @@
(fn [changes]
(let [editing-stop (:editing-stop @state)]
(swap! state #(cond-> %
true (update :current-color merge changes)
editing-stop (update-in [:stops editing-stop] merge changes)))
#_(when (:hex changes)
(reset! value-ref (:hex changes)))
;; TODO: CHANGE TO SUPPORT GRADIENTS
#_(on-change (:hex changes (:hex current-color))
(:alpha changes (:alpha current-color)))))
true (update :current-color merge changes)
editing-stop (update-in [:stops editing-stop] merge changes)))
(reset! dirty? true)))
handle-change-stop
(fn [offset]
@ -160,11 +155,15 @@
(st/emit! (dc/select-gradient-stop offset)))))
on-select-library-color
(fn [color] (prn "color" color))
(fn [color] (reset! state (data->state color)))
on-add-library-color
(fn [color] (st/emit! (dwl/add-color (state->data @state))))
on-activate-gradient
(fn [type]
(fn []
(reset! dirty? true)
(if (= type (:type @state))
(do
(swap! state assoc :type :color)
@ -179,13 +178,6 @@
1 (-> (:current-color @state)
(assoc :alpha 0))}))))))]
;; Update state when there is a change in the props upstream
;; TODO: Review for gradients
#_(mf/use-effect
(mf/deps value opacity)
(fn []
(swap! state assoc current-color (as-color-components value opacity))))
;; Updates the CSS color variable when there is a change in the color
(mf/use-effect
(mf/deps current-color)
@ -208,48 +200,47 @@
;; When closing the modal we update the recent-color list
(mf/use-effect
(fn [] (fn []
(st/emit! (dc/stop-picker))
(st/emit! (dwl/add-recent-color (state->data @state))))))
#(fn []
(st/emit! (dc/stop-picker))
(when @last-color
(st/emit! (dwl/add-recent-color @last-color)))))
;; Updates color when used el pixel picker
(mf/use-effect
(mf/deps picking-color? picked-color)
(mf/deps picking-color? picked-color-select)
(fn []
(when picking-color?
(let [[r g b] (or picked-color [0 0 0])
(when (and picking-color? picked-color-select)
(let [[r g b alpha] picked-color
hex (uc/rgb->hex [r g b])
[h s v] (uc/hex->hsv hex)]
(handle-change-color {:hex hex
:r r :g g :b b
:h h :s s :v v
:alpha alpha})))))
(swap! state update :current-color assoc
:r r :g g :b b
:h h :s s :v v
:hex hex)
;; TODO: UPDATE TO USE GRADIENTS
#_(reset! value-ref hex)
#_(when picked-color-select
(on-change hex (:alpha current-color) nil nil picked-shift?))))))
;; TODO: UPDATE TO USE GRADIENTS
#_(mf/use-effect
(mf/deps picking-color? picked-color-select)
(fn [] (when (and picking-color? picked-color-select)
(on-change (:hex current-color) (:alpha current-color) nil nil picked-shift?))))
;; Changes when another gradient handler is selected
(mf/use-effect
(mf/deps editing-spot-state)
#(when (not= editing-spot-state (:editing-stop @state))
(handle-change-stop (or editing-spot-state 0))))
;; Changes on the viewport when moving a gradient handler
(mf/use-effect
(mf/deps data)
#(if-let [gradient-data (-> data data->state :gradient-data)]
(swap! state assoc :gradient-data gradient-data)))
(do
(reset! dirty? true)
(swap! state assoc :gradient-data gradient-data))))
;; Send the properties to the store
(mf/use-effect
(mf/deps @state)
(fn []
(on-change (state->data @state))))
(if @dirty?
(let [color (state->data @state)]
(reset! dirty? false)
(reset! last-color color)
(on-change color)))))
[:div.colorpicker {:ref ref-picker}
[:div.colorpicker-content
@ -306,7 +297,8 @@
:on-change handle-change-color}]
[:& libraries {:current-color current-color
:on-select-color on-select-library-color}]
:on-select-color on-select-library-color
:on-add-library-color on-add-library-color}]
(when on-accept
[:div.actions

View file

@ -32,7 +32,7 @@
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]]
[app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]]))
(mf/defc libraries [{:keys [current-color on-select-color]}]
(mf/defc libraries [{:keys [current-color on-select-color on-add-library-color]}]
(let [selected-library (mf/use-state "recent")
current-library-colors (mf/use-state [])
@ -69,7 +69,7 @@
(mf/use-effect
(mf/deps file-colors)
(fn [] (when (= @selected-library "file")
(let [colors (map #(select-keys % [:id :value]) (vals file-colors))]
(let [colors (vals file-colors)]
(reset! current-library-colors (into [] colors))))))
@ -88,7 +88,7 @@
[:div.selected-colors
(when (= "file" @selected-library)
[:div.color-bullet.button.plus-button {:style {:background-color "white"}
:on-click #(st/emit! (dwl/add-color current-color))}
:on-click on-add-library-color}
i/plus])
[:div.color-bullet.button {:style {:background-color "white"}

View file

@ -10,20 +10,157 @@
(ns app.main.ui.workspace.colorpicker.pixel-overlay
(:require
[rumext.alpha :as mf]
[okulary.core :as l]
[cuerdas.core :as str]
[app.common.geom.point :as gpt]
[app.common.math :as math]
[app.common.uuid :refer [uuid]]
[okulary.core :as l]
[promesa.core :as p]
[goog.events :as events]
[app.common.uuid :as uuid]
[app.util.timers :as timers]
[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 dc]
[app.main.data.colors :as dwc]
[app.main.data.fetch :as mdf]
[app.main.data.modal :as modal]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [t]]))
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as muc]
[app.main.ui.cursors :as cur]
[app.main.ui.keyboard :as kbd]
[app.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]])
(:import goog.events.EventType))
(defn format-viewbox [vbox]
(str/join " " [(+ (:x vbox 0) (:left-offset vbox 0))
(:y vbox 0)
(:width vbox 0)
(:height vbox 0)]))
(mf/defc overlay-frames
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[]
(let [data (mf/deref refs/workspace-page)
objects (:objects data)
root (get objects uuid/zero)
shapes (->> (:shapes root) (map #(get objects %)))]
[:*
[:g.shapes
(for [item shapes]
(if (= (:type item) :frame)
[:& frame-wrapper {:shape item
:key (:id item)
:objects objects}]
[:& shape-wrapper {:shape item
:key (:id item)}]))]]))
(mf/defc pixel-overlay
{::mf/wrap-props false}
[props]
(let [vport (unchecked-get props "vport")
vbox (unchecked-get props "vbox")
viewport-ref (unchecked-get props "viewport-ref")
options (unchecked-get props "options")
svg-ref (mf/use-ref nil)
canvas-ref (mf/use-ref nil)
fetch-pending (mf/deref (mdf/pending-ref))
handle-keydown
(fn [event]
(when (and (kbd/esc? event))
(do (dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dwc/stop-picker))
(modal/disallow-click-outside!))))
on-mouse-move-picker
(fn [event]
(when-let [zoom-view-node (.getElementById js/document "picker-detail")]
(let [{brx :left bry :top} (dom/get-bounding-rect (mf/ref-val viewport-ref))
x (- (.-clientX event) brx)
y (- (.-clientY event) bry)
zoom-context (.getContext zoom-view-node "2d")
canvas-node (mf/ref-val canvas-ref)
canvas-context (.getContext canvas-node "2d")
pixel-data (.getImageData canvas-context x y 1 1)
rgba (.-data pixel-data)
r (obj/get rgba 0)
g (obj/get rgba 1)
b (obj/get rgba 2)
a (obj/get rgba 3)
area-data (.getImageData canvas-context (- x 25) (- y 20) 50 40)]
(-> (js/createImageBitmap area-data)
(p/then (fn [image]
;; Draw area
(obj/set! zoom-context "imageSmoothingEnabled" false)
(.drawImage zoom-context image 0 0 200 160))))
(st/emit! (dwc/pick-color [r g b a])))))
on-mouse-down-picker
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dwc/pick-color-select true (kbd/shift? event))))
on-mouse-up-picker
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dwc/stop-picker))
(modal/disallow-click-outside!))]
(mf/use-effect
(fn []
(let [listener (events/listen js/document EventType.KEYDOWN handle-keydown)]
#(events/unlistenByKey listener))))
(mf/use-effect
;; Everytime we finish retrieving a new URL we redraw the canvas
;; so even if we're not finished the user can start to pick basic
;; shapes
(mf/deps fetch-pending)
(fn []
(try
(let [canvas-node (mf/ref-val canvas-ref)
canvas-context (.getContext canvas-node "2d")
svg-node (mf/ref-val svg-ref)]
(timers/schedule 100
#(let [xml (.serializeToString (js/XMLSerializer.) svg-node)
img-src (str "data:image/svg+xml;base64,"
(-> xml js/encodeURIComponent js/unescape js/btoa))
img (js/Image.)
on-error (fn [err] (.error js/console "ERROR" err))
on-load (fn [] (.drawImage canvas-context img 0 0))]
(.addEventListener img "error" on-error)
(.addEventListener img "load" on-load)
(obj/set! img "src" img-src))))
(catch :default e (.error js/console e)))))
[:*
[:div.overlay
{:tab-index 0
:style {:position "absolute"
:top 0
:left 0
:width "100%"
:height "100%"
:cursor cur/picker}
:on-mouse-down on-mouse-down-picker
:on-mouse-up on-mouse-up-picker
:on-mouse-move on-mouse-move-picker}]
[:canvas {:ref canvas-ref
:width (:width vport 0)
:height (:height vport 0)
:style {:display "none"}}]
[:& (mf/provider muc/embed-ctx) {:value true}
[:svg.viewport
{:ref svg-ref
:preserveAspectRatio "xMidYMid meet"
:width (:width vport 0)
:height (:height vport 0)
:view-box (format-viewbox vbox)
:style {:display "none"
:background-color (get options :background "#E8E9EA")}}
[:& overlay-frames]]]]))

View file

@ -41,6 +41,7 @@
[app.main.ui.workspace.frame-grid :refer [frame-grid]]
[app.main.ui.workspace.shapes.outline :refer [outline]]
[app.main.ui.workspace.gradients :refer [gradient-handlers]]
[app.main.ui.workspace.colorpicker.pixel-overlay :refer [pixel-overlay]]
[app.common.math :as mth]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
@ -185,104 +186,6 @@
(:width vbox 0)
(:height vbox 0)]))
(mf/defc pixel-picker-overlay
{::mf/wrap-props false}
[props]
(let [vport (unchecked-get props "vport")
vbox (unchecked-get props "vbox")
viewport-ref (unchecked-get props "viewport-ref")
options (unchecked-get props "options")
svg-ref (mf/use-ref nil)
canvas-ref (mf/use-ref nil)
fetch-pending (mf/deref (mdf/pending-ref))
on-mouse-move-picker
(fn [event]
(when-let [zoom-view-node (.getElementById js/document "picker-detail")]
(let [{brx :left bry :top} (dom/get-bounding-rect (mf/ref-val viewport-ref))
x (- (.-clientX event) brx)
y (- (.-clientY event) bry)
zoom-context (.getContext zoom-view-node "2d")
canvas-node (mf/ref-val canvas-ref)
canvas-context (.getContext canvas-node "2d")
pixel-data (.getImageData canvas-context x y 1 1)
rgba (.-data pixel-data)
r (obj/get rgba 0)
g (obj/get rgba 1)
b (obj/get rgba 2)
a (obj/get rgba 3)
area-data (.getImageData canvas-context (- x 25) (- y 20) 50 40)]
(-> (js/createImageBitmap area-data)
(p/then (fn [image]
;; Draw area
(obj/set! zoom-context "imageSmoothingEnabled" false)
(.drawImage zoom-context image 0 0 200 160))))
(st/emit! (dwc/pick-color [r g b a])))))
on-mouse-down-picker
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dwc/pick-color-select true (kbd/shift? event))))
on-mouse-up-picker
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dwc/stop-picker))
(modal/disallow-click-outside!))]
(mf/use-effect
;; Everytime we finish retrieving a new URL we redraw the canvas
;; so even if we're not finished the user can start to pick basic
;; shapes
(mf/deps fetch-pending)
(fn []
(try
(let [canvas-node (mf/ref-val canvas-ref)
canvas-context (.getContext canvas-node "2d")
svg-node (mf/ref-val svg-ref)]
(timers/schedule 100
#(let [xml (.serializeToString (js/XMLSerializer.) svg-node)
img-src (str "data:image/svg+xml;base64,"
(-> xml js/encodeURIComponent js/unescape js/btoa))
img (js/Image.)
on-error (fn [err] (.error js/console "ERROR" err))
on-load (fn [] (.drawImage canvas-context img 0 0))]
(.addEventListener img "error" on-error)
(.addEventListener img "load" on-load)
(obj/set! img "src" img-src))))
(catch :default e (.error js/console e)))))
[:*
[:div.overlay
{:style {:position "absolute"
:top 0
:left 0
:width "100%"
:height "100%"
:cursor cur/picker}
:on-mouse-down on-mouse-down-picker
:on-mouse-up on-mouse-up-picker
:on-mouse-move on-mouse-move-picker}]
[:canvas {:ref canvas-ref
:width (:width vport 0)
:height (:height vport 0)
:style {:display "none"}}]
[:& (mf/provider muc/embed-ctx) {:value true}
[:svg.viewport
{:ref svg-ref
:preserveAspectRatio "xMidYMid meet"
:width (:width vport 0)
:height (:height vport 0)
:view-box (format-viewbox vbox)
:style {:display "none"
:background-color (get options :background "#E8E9EA")}}
[:& frames]]]]))
(mf/defc viewport
[{:keys [page-id page local layout] :as props}]
(let [{:keys [options-mode
@ -310,8 +213,6 @@
drawing-tool (:tool drawing)
drawing-obj (:object drawing)
pick-color (mf/use-state [255 255 255 255])
zoom (or zoom 1)
on-mouse-down
@ -587,11 +488,11 @@
[:*
(when picking-color?
[:& pixel-picker-overlay {:vport vport
:vbox vbox
:viewport-ref viewport-ref
:options options
:layout layout}])
[:& pixel-overlay {:vport vport
:vbox vbox
:viewport-ref viewport-ref
:options options
:layout layout}])
[:svg.viewport
{:preserveAspectRatio "xMidYMid meet"