🎉 Pixel picker

This commit is contained in:
alonso.torres 2020-09-09 12:17:32 +02:00 committed by Alonso Torres
parent 8aad43883f
commit f8b3baef3f
18 changed files with 494 additions and 199 deletions

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24px" height="24px">
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1743,19 +1743,19 @@
}
},
"workspace.libraries.colors.file-library" : {
"used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:277", "src/app/main/ui/workspace/colorpalette.cljs:149" ],
"used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:314", "src/app/main/ui/workspace/colorpalette.cljs:149" ],
"translations" : {
"en" : "File library"
}
},
"workspace.libraries.colors.recent-colors" : {
"used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:276", "src/app/main/ui/workspace/colorpalette.cljs:159" ],
"used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:313", "src/app/main/ui/workspace/colorpalette.cljs:159" ],
"translations" : {
"en" : "Recent colors"
}
},
"workspace.libraries.colors.save-color" : {
"used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:312" ],
"used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:349" ],
"translations" : {
"en" : "Save color"
}

View file

@ -8,11 +8,54 @@
.colorpicker {
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);
& > * {
width: 200px;
}
.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;
}
}
}
.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;
}
.handler {
position: absolute;
width: 12px;
@ -25,7 +68,6 @@
background-color: rgba(var(--hue));
position: relative;
height: 6.75rem;
width: 100%;
cursor: pointer;
.handler {
@ -137,6 +179,7 @@
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);
@ -162,7 +205,8 @@
grid-template-columns: repeat(8, 1fr);
justify-content: space-between;
margin-right: -8px;
overflow: scroll;
overflow-x: hidden;
overflow-y: auto;
max-height: 5.5rem;
}

View file

@ -129,3 +129,31 @@
(-> state
(update :workspace-layout conj :colorpalette)
(assoc-in [:workspace-local :selected-palette] selected)))))
(defn start-picker []
(ptk/reify ::start-picker
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :picking-color?] true)))))
(defn stop-picker []
(ptk/reify ::stop-picker
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :picking-color?] false)))))
(defn pick-color [rgba]
(ptk/reify ::pick-color
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :picked-color] rgba)))))
(defn pick-color-select [value]
(ptk/reify ::pick-color
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :picked-color-select] value)))))

View file

@ -0,0 +1,22 @@
;; 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.data.fetch
(:require
[promesa.core :as p]
[app.util.object :as obj]))
(defn fetch-as-data-uri [url]
(-> (js/fetch url)
(p/then (fn [res] (.blob res)))
(p/then (fn [blob]
(let [reader (js/FileReader.)]
(p/create (fn [resolve reject]
(obj/set! reader "onload" #(resolve [url (.-result reader)]))
(.readAsDataURL reader blob))))))))

View file

@ -102,7 +102,10 @@
:right-sidebar? true
:color-for-rename nil
:selected-palette :recent
:selected-palette-size :big})
:selected-palette-size :big
:picking-color? false
:picked-color nil
:picked-color-select false})
(def initialize-layout
(ptk/reify ::initialize-layout

View file

@ -114,6 +114,11 @@
(unchecked-set node "type" "text/css")
node))
(defn gfont-url [family variants]
(let [base (str "https://fonts.googleapis.com/css?family=" family)
variants (str/join "," (map :id variants))]
(str base ":" variants "&display=block")))
(defmulti ^:private load-font :backend)
(defmethod load-font :builtin
@ -126,10 +131,7 @@
[{:keys [id family variants ::on-loaded] :as font}]
(when (exists? js/window)
(js/console.log "[debug:fonts]: loading google font" id)
(let [base (str "https://fonts.googleapis.com/css?family=" family)
variants (str/join "," (map :id variants))
uri (str base ":" variants "&display=block")
node (create-link-node uri)]
(let [node (create-link-node (gfont-url family variants))]
(.addEventListener node "load" (fn [event] (when (fn? on-loaded)
(on-loaded id))))
(.append (.-head js/document) node)

View file

@ -80,6 +80,14 @@
(def current-hover
(l/derived :hover workspace-local))
(def picking-color?
(l/derived :picking-color? workspace-local))
(def picked-color
(l/derived :picked-color workspace-local))
(def picked-color-select
(l/derived :picked-color-select workspace-local))
(def workspace-layout
(l/derived :workspace-layout st/state))

View file

@ -32,6 +32,7 @@
(def resize-ns (cursor-fn :resize-h 90))
(def rotate (cursor-fn :rotate 90))
(def text (cursor-ref :text))
(def picker (cursor-ref :picker 0 0 24))
(mf/defc debug-preview
{::mf/wrap-props false}

View file

@ -10,6 +10,7 @@
(:import goog.events.EventType))
(defonce state (atom nil))
(defonce can-click-outside (atom false))
(defn show!
[component props]
@ -25,13 +26,12 @@
(reset! state nil)
(dom/stop-propagation event)))
(defn- on-parent-clicked
[event parent-ref]
(let [parent (mf/ref-val parent-ref)
(defn- on-click
[event wrapper-ref]
(let [wrapper (mf/ref-val wrapper-ref)
current (dom/get-target event)]
;; (js/console.log current (.-className ^js current))
(when (and (dom/equals? (.-firstElementChild ^js parent) current)
(str/includes? (.-className ^js current) "modal-overlay"))
(when (and (not @can-click-outside) (not (.contains wrapper current)))
(dom/stop-propagation event)
(dom/prevent-default event)
(reset! state nil))))
@ -39,15 +39,19 @@
(mf/defc modal-wrapper
[{:keys [component props]}]
(let [wrapper-ref (mf/use-ref nil)]
(mf/use-effect
(fn []
(let [key (events/listen js/document EventType.KEYDOWN on-esc-clicked)]
#(events/unlistenByKey %))))
#(events/unlistenByKey key))))
(mf/use-effect
(fn []
(let [key (events/listen js/document EventType.CLICK #(on-click % wrapper-ref))]
#(events/unlistenByKey key))))
(let [ref (mf/use-ref nil)]
[:div.modal-wrapper
{:ref ref
:on-click #(on-parent-clicked % ref)}
{:ref wrapper-ref}
[:& component props]]))
(mf/defc modal
@ -58,4 +62,8 @@
:key (random-uuid)}]))
(defn allow-click-outside! []
(reset! can-click-outside true))
(defn disallow-click-outside! []
(reset! can-click-outside false))

View file

@ -13,23 +13,43 @@
[app.config :as cfg]
[app.common.geom.shapes :as geom]
[app.main.ui.shapes.attrs :as attrs]
[app.util.object :as obj]))
[app.util.object :as obj]
[app.main.data.fetch :as df]
[promesa.core :as p]))
(mf/defc image-shape
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
{:keys [id x y width height rotation metadata]} shape
transform (geom/transform-matrix shape)
uri (cfg/resolve-media-path (:path metadata))
data-uri (mf/use-state nil)]
(mf/use-effect
(mf/deps shape)
(fn []
(-> (df/fetch-as-data-uri uri)
(p/then #(reset! data-uri (second %))))))
(let [transform (geom/transform-matrix shape)
props (-> (attrs/extract-style-attrs shape)
(obj/merge!
#js {:x x
:y y
:transform transform
:id (str "shape-" id)
:preserveAspectRatio "none"
:xlinkHref uri
:width width
:height height}))]
[:> "image" props]))
:height height
:preserveAspectRatio "none"}))]
(if (nil? @data-uri)
[:> "rect" (obj/merge!
props
#js {:fill "#E8E9EA"
:stroke "#000000"})]
[:> "image" (obj/merge!
props
#js {:xlinkHref @data-uri})]))
))

View file

@ -6,12 +6,16 @@
(ns app.main.ui.shapes.text
(:require
[promesa.core :as p]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.data.fetch :as df]
[app.common.data :as d]
[app.common.geom.shapes :as geom]
[app.common.geom.matrix :as gmt]
[app.main.fonts :as fonts]
[app.util.object :as obj]))
[app.util.object :as obj]
[clojure.set :as set]))
;; --- Text Editor Rendering
@ -55,7 +59,8 @@
base #js {:textDecoration text-decoration
:color fill
:opacity opacity
:textTransform text-transform}]
:textTransform text-transform
:lineHeight "inherit"}]
(when (and (string? letter-spacing)
(pos? (alength letter-spacing)))
@ -83,10 +88,42 @@
base))
(defn get-all-fonts [node]
(let [current-font (if (not (nil? (:font-id node)))
#{(:font-id node)}
#{})
children-font (map get-all-fonts (:children node))]
(reduce set/union (conj children-font current-font))))
(defn fetch-font [font-id]
(let [{:keys [family variants]} (get @fonts/fontsdb font-id)]
(-> (js/fetch (fonts/gfont-url family variants))
(p/then (fn [res] (.text res))))))
(defn embed-font [font-id]
(p/let [font-text (fetch-font font-id)
url-to-data (->> font-text
(re-seq #"url\(([^)]+)\)")
(map second)
(map df/fetch-as-data-uri)
(p/all))]
(reduce (fn [text [url data]] (str/replace text url data)) font-text url-to-data)))
(defn- render-text-node
([node] (render-text-node 0 node))
([index {:keys [type text children] :as node}]
(mf/html
(let [embeded-fonts (mf/use-state nil)]
(mf/use-effect
(mf/deps node)
(fn []
(when (= type "root")
(let [font-to-embed (get-all-fonts node)
embeded (map embed-font font-to-embed)]
(-> (p/all embeded)
(p/then (fn [result] (reset! embeded-fonts (str/join "\n" result)))))))))
(if (string? text)
(let [style (generate-text-styles (clj->js node))]
[:span {:style style :key index} text])
@ -94,11 +131,15 @@
(case type
"root"
(let [style (generate-root-styles (clj->js node))]
[:div.root.rich-text
{:key index
:style style
:xmlns "http://www.w3.org/1999/xhtml"}
children])
[:*
(when (not (nil? @embeded-fonts))
[:style @embeded-fonts])
children]])
"paragraph-set"
(let [style #js {:display "inline-block"
@ -109,7 +150,7 @@
(let [style (generate-paragraph-styles (clj->js node))]
[:p {:key index :style style} children])
nil))))))
nil)))))))
(mf/defc text-content
{::mf/wrap-props false

View file

@ -102,6 +102,10 @@
shared-libs (mf/deref refs/workspace-libraries)
recent-colors (mf/deref refs/workspace-recent-colors)
picking-color? (mf/deref refs/picking-color?)
picked-color (mf/deref refs/picked-color)
picked-color-select (mf/deref refs/picked-color-select)
locale (mf/deref i18n/locale)
value-ref (mf/use-var value)
@ -154,9 +158,41 @@
;; When closing the modal we update the recent-color list
(mf/use-effect
(fn [] #(st/emit! (dwl/add-recent-color @value-ref))))
(fn [] #(st/emit! (dwc/stop-picker)
(dwl/add-recent-color @value-ref))))
(mf/use-effect
(mf/deps picking-color? picked-color)
(fn [] (when picking-color?
(let [[r g b] picked-color
hex (uc/rgb->hex [r g b])
[h s v] (uc/hex->hsv hex)]
(swap! current-color assoc
:r r :g g :b b
:h h :s s :v v
:hex hex)
(when picked-color-select
(on-change hex (:alpha @current-color)))))))
(mf/use-effect
(mf/deps picking-color? picked-color-select)
(fn [] (when picking-color?
(on-change (:hex @current-color) (:alpha @current-color)))))
[: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]]
(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)
@ -168,7 +204,8 @@
:r r :g g :b b
:s s :v v)
(reset! value-ref hex)
(on-change hex (:alpha @current-color))))}]
(on-change hex (:alpha @current-color))))}])
(when (not picking-color?)
[:div.shade-selector
[:div.color-bullet]
[:& hue-selector {:hue (:h @current-color)
@ -184,7 +221,7 @@
[:& opacity-selector {:opacity (:alpha @current-color)
:on-change (fn [alpha]
(swap! current-color assoc :alpha alpha)
(on-change (:hex @current-color) alpha))}]]
(on-change (:hex @current-color) alpha))}]])
[:div.color-values
[:input.hex-value {:id "hex-value"
@ -320,12 +357,11 @@
:top (str (- y 50) "px")}
:right {:left (str (+ x 24) "px")
:top (str (- y 50) "px")})]
[:div.modal-overlay.transparent
[:div.colorpicker-tooltip
{:style (clj->js style)}
[:& colorpicker {:value (or value default)
:opacity (or opacity 1)
:on-change on-change
:on-accept on-accept
:disable-opacity disable-opacity}]]]))
:disable-opacity disable-opacity}]]))

View file

@ -33,9 +33,10 @@
(defn draw-rule!
[dctx {:keys [zoom size start count type] :or {count 200}}]
(when start
(let [txfm (- (* (- 0 start) zoom) 20)
minv (mth/round start)
maxv (mth/round (+ start (/ size zoom)))
minv (max (mth/round start) -10000)
maxv (min (mth/round (+ start (/ size zoom))) 10000)
step (calculate-step-size zoom)]
(if (= type :horizontal)
@ -75,7 +76,7 @@
(do
(.moveTo path 17 pos)
(.lineTo path 20 pos))))
(recur (inc i))))))))
(recur (inc i)))))))))
(mf/defc horizontal-rule

View file

@ -119,7 +119,8 @@
base #js {:textDecoration text-decoration
:color fill
:opacity opacity
:textTransform text-transform}]
:textTransform text-transform
:lineHeight "inherit"}]
(when (and (string? letter-spacing)
(pos? (alength letter-spacing)))

View file

@ -36,7 +36,7 @@
(if (= value :multiple) "transparent" value))
(defn remove-hash [value]
(if (= value :multiple) "" (subs value 1)))
(if (or (nil? value) (= value :multiple)) "" (subs value 1)))
(defn append-hash [value]
(str "#" value))

View file

@ -15,12 +15,15 @@
[goog.events :as events]
[potok.core :as ptk]
[rumext.alpha :as mf]
[promesa.core :as p]
[app.main.ui.icons :as i]
[app.main.ui.cursors :as cur]
[app.main.ui.modal :as modal]
[app.common.data :as d]
[app.main.constants :as c]
[app.main.data.workspace :as dw]
[app.main.data.workspace.drawing :as dd]
[app.main.data.colors :as dwc]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.streams :as ms]
@ -168,15 +171,20 @@
edition
tooltip
selected
panning]} local
panning
picking-color?]} local
file (mf/deref refs/workspace-file)
viewport-ref (mf/use-ref nil)
canvas-ref (mf/use-ref nil)
zoom-view-ref (mf/use-ref nil)
last-position (mf/use-var nil)
drawing (mf/deref refs/workspace-drawing)
drawing-tool (:tool drawing)
drawing-obj (:object drawing)
pick-color (mf/use-state [255 255 255 255])
zoom (or zoom 1)
on-mouse-down
@ -410,7 +418,47 @@
prnt (dom/get-parent node)]
(st/emit! (dw/update-viewport-size (dom/get-client-size prnt)))))
options (mf/deref refs/workspace-page-options)]
options (mf/deref refs/workspace-page-options)
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)))
on-mouse-up-picker
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dwc/pick-color-select false)
(dwc/stop-picker))
(modal/disallow-click-outside!))]
(mf/use-layout-effect
(fn []
@ -434,6 +482,36 @@
(mf/use-layout-effect (mf/deps layout) on-resize)
(mf/use-effect
(mf/deps props)
(fn []
(when picking-color?
(try
(let [svg-node (mf/ref-val viewport-ref)
canvas-node (mf/ref-val canvas-ref)
canvas-context (.getContext canvas-node "2d")
xml (.serializeToString (js/XMLSerializer.) svg-node)
content (str "data:image/svg+xml;base64," (js/btoa xml))
img (js/Image.)]
(obj/set! img "onload"
(fn []
(.drawImage canvas-context img 0 0)))
(obj/set! img "src" content))
(catch :default e (.error js/console e))))))
[:*
(when picking-color?
[:canvas {:ref canvas-ref
:width (:width vport 0)
:height (:height vport 0)
:on-mouse-down on-mouse-down-picker
:on-mouse-up on-mouse-up-picker
:on-mouse-move on-mouse-move-picker
:style {:position "absolute"
:top 0
:left 0
:cursor cur/picker}}])
[:svg.viewport
{:preserveAspectRatio "xMidYMid meet"
:width (:width vport 0)
@ -502,5 +580,5 @@
[:& presence/active-cursors {:page-id page-id}]
[:& selection-rect {:data (:selrect local)}]
(when (= options-mode :prototype)
[:& interactions {:selected selected}])]))
[:& interactions {:selected selected}])]]))

View file

@ -51,9 +51,7 @@
(defn hex->hsl [hex]
(try
(into [] (gcolor/hexToHsl hex))
(catch :default e (do
(.log js/console e)
[0 0 0]))))
(catch :default e [0 0 0])))
(defn hsl->rgb
[[h s l]]