More work on interactions.

This commit is contained in:
Andrey Antukh 2016-07-30 16:35:19 +03:00
parent eb62b106f3
commit fbbffe0167
No known key found for this signature in database
GPG Key ID: 4DFEBCB8316A8B95
5 changed files with 276 additions and 77 deletions

View File

@ -48,14 +48,13 @@
:hover "Hover" :hover "Hover"
:mousein "Mouse In" :mousein "Mouse In"
:mouseout "Mouse Out" :mouseout "Mouse Out"
:swiperight "Swipe Right" ;; :swiperight "Swipe Right"
:swipeleft "Swipe Left" ;; :swipeleft "Swipe Left"
:swipedown "Swipe Down" ;; :swipedown "Swipe Down"
:touchandhold "Touch and Hold" ;; :touchandhold "Touch and Hold"
:holdrelease "Hold release" ;; :holdrelease "Hold release"
(pr-str trigger))) (pr-str trigger)))
(defn- interactions-list-render (defn- interactions-list-render
[own shape form-ref] [own shape form-ref]
(letfn [(on-edit [item event] (letfn [(on-edit [item event]
@ -105,11 +104,11 @@
[:option {:value ":hover"} "Hover"] [:option {:value ":hover"} "Hover"]
[:option {:value ":mousein"} "Mouse in"] [:option {:value ":mousein"} "Mouse in"]
[:option {:value ":mouseout"} "Mouse out"] [:option {:value ":mouseout"} "Mouse out"]
[:option {:value ":swiperight"} "Swipe right"] #_[:option {:value ":swiperight"} "Swipe right"]
[:option {:value ":swipeleft"} "Swipe left"] #_[:option {:value ":swipeleft"} "Swipe left"]
[:option {:value ":swipedown"} "Swipe dpwn"] #_[:option {:value ":swipedown"} "Swipe dpwn"]
[:option {:value ":touchandhold"} "Touch and hold"] #_[:option {:value ":touchandhold"} "Touch and hold"]
[:option {:value ":holdrelease"} "Hold release"] #_[:option {:value ":holdrelease"} "Hold release"]
#_[:option {:value ":keypress"} "Key press"] #_[:option {:value ":keypress"} "Key press"]
#_[:option {:value ":pageisloaded"} "Page is loaded"] #_[:option {:value ":pageisloaded"} "Page is loaded"]
#_[:option {:value ":windowscroll"} "Window is scrolled to"]]]])) #_[:option {:value ":windowscroll"} "Window is scrolled to"]]]]))
@ -236,12 +235,12 @@
{:placeholder "X" {:placeholder "X"
:on-change (partial on-change form-ref :moveto-x) :on-change (partial on-change form-ref :moveto-x)
:type "number" :type "number"
:value (:moveto-x @form-ref)}] :value (:moveto-x @form-ref "")}]
[:input.input-text [:input.input-text
{:placeholder "Y" {:placeholder "Y"
:on-change (partial on-change form-ref :moveto-y) :on-change (partial on-change form-ref :moveto-y)
:type "number" :type "number"
:value (:moveto-y @form-ref)}]]])) :value (:moveto-y @form-ref "")}]]]))
(def moveto-input (def moveto-input
(mx/component (mx/component
@ -264,12 +263,12 @@
{:placeholder "X" {:placeholder "X"
:on-change (partial on-change form-ref :moveby-x) :on-change (partial on-change form-ref :moveby-x)
:type "number" :type "number"
:value (:moveby-x @form-ref)}] :value (:moveby-x @form-ref "")}]
[:input.input-text [:input.input-text
{:placeholder "Y" {:placeholder "Y"
:on-change (partial on-change form-ref :moveby-y) :on-change (partial on-change form-ref :moveby-y)
:type "number" :type "number"
:value (:moveby-y @form-ref)}]]])) :value (:moveby-y @form-ref "")}]]]))
(def moveby-input (def moveby-input
(mx/component (mx/component
@ -292,7 +291,7 @@
:min "0" :min "0"
:max "100" :max "100"
:type "number" :type "number"
:value (:opacity @form-ref)}]]])) :value (:opacity @form-ref "")}]]]))
(def opacity-input (def opacity-input
(mx/component (mx/component
@ -308,10 +307,10 @@
[:span "Rotate (dg)"] [:span "Rotate (dg)"]
[:div.row-flex [:div.row-flex
[:input.input-text [:input.input-text
{:placeholder "px" {:placeholder "dg"
:on-change (partial on-change form-ref :rotate) :on-change (partial on-change form-ref :rotation)
:type "number" :type "number"
:value ""}]]])) :value (:rotation @form-ref "")}]]]))
(def rotate-input (def rotate-input
(mx/component (mx/component
@ -330,12 +329,12 @@
{:placeholder "Width" {:placeholder "Width"
:on-change (partial on-change form-ref :resize-width) :on-change (partial on-change form-ref :resize-width)
:type "number" :type "number"
:value ""}] :value (:resize-width @form-ref "")}]
[:input.input-text [:input.input-text
{:placeholder "Height" {:placeholder "Height"
:on-change (partial on-change form-ref :resize-height) :on-change (partial on-change form-ref :resize-height)
:type "number" :type "number"
:value ""}]]])) :value (:resize-height @form-ref "")}]]]))
(def resize-input (def resize-input
(mx/component (mx/component
@ -416,7 +415,7 @@
(defn- easing-input-render (defn- easing-input-render
[own form-ref] [own form-ref]
(when-not (:easing @form-ref) (when-not (:easing @form-ref)
(swap! form-ref assoc :easing :none)) (swap! form-ref assoc :easing :linear))
(html (html
[:div [:div
[:span "Easing"] [:span "Easing"]
@ -425,7 +424,6 @@
{:placeholder "Easing" {:placeholder "Easing"
:on-change (partial on-change form-ref :easing) :on-change (partial on-change form-ref :easing)
:value (pr-str (:easing @form-ref))} :value (pr-str (:easing @form-ref))}
[:option {:value ":none"} "None"]
[:option {:value ":linear"} "Linear"] [:option {:value ":linear"} "Linear"]
[:option {:value ":easein"} "Ease in"] [:option {:value ":easein"} "Ease in"]
[:option {:value ":easeout"} "Ease out"] [:option {:value ":easeout"} "Ease out"]
@ -486,12 +484,12 @@
[:option {:value ":show"} "Show"] [:option {:value ":show"} "Show"]
[:option {:value ":hide"} "Hide"] [:option {:value ":hide"} "Hide"]
[:option {:value ":toggle"} "Toggle"] [:option {:value ":toggle"} "Toggle"]
[:option {:value ":moveto"} "Move to"] ;; [:option {:value ":moveto"} "Move to"]
[:option {:value ":moveby"} "Move by"] [:option {:value ":moveby"} "Move by"]
[:option {:value ":opacity"} "Opacity"] [:option {:value ":opacity"} "Opacity"]
[:option {:value ":size"} "Size"] [:option {:value ":size"} "Size"]
[:option {:value ":color"} "Color"] [:option {:value ":color"} "Color"]
[:option {:value ":rotate"} "Rotate"] ;; [:option {:value ":rotate"} "Rotate"]
[:option {:value ":gotopage"} "Go to page"] [:option {:value ":gotopage"} "Go to page"]
[:option {:value ":gotourl"} "Go to URL"] [:option {:value ":gotourl"} "Go to URL"]
#_[:option {:value ":goback"} "Go back"] #_[:option {:value ":goback"} "Go back"]

View File

@ -30,6 +30,10 @@
([classname node] ([classname node]
(dom/getElementByClass classname node))) (dom/getElementByClass classname node)))
(defn get-element
[id]
(dom/getElement id))
(defn stop-propagation (defn stop-propagation
[e] [e]
(when e (when e

View File

@ -45,3 +45,4 @@
(def ref-node rum/ref-node) (def ref-node rum/ref-node)
(def react rum/react) (def react rum/react)
(def reactive rum/reactive) (def reactive rum/reactive)
(def dom-node rum/dom-node)

View File

@ -5,82 +5,259 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.view.ui.viewer.interactions (ns uxbox.view.ui.viewer.interactions
(:require [uxbox.util.dom :as dom] (:require [promesa.core :as p]
[vendor.animejs])) [uxbox.util.dom :as dom]
[uxbox.main.geom :as geom]
[uxbox.main.geom.matrix :as gmt]
[uxbox.main.geom.point :as gpt]
[uxbox.main.state :as st]
[vendor.snapsvg])
(:import goog.events.EventType))
(defn- translate-trigger (defn- translate-trigger
"Translates the interaction trigger name (keyword) into "Translates the interaction trigger name (keyword) into
approriate dom event name (keyword)." approriate dom event name (keyword)."
[trigger] [trigger]
{:pre [(keyword? trigger)] {:pre [(keyword? trigger)]}
:post [(keyword? %)]}
(case trigger (case trigger
:click :on-click :click EventType.CLICK
:hover :on-hover :doubleclick EventType.DBLCLICK
:doubleclick :on-double-click :rightclick EventType.CONTEXTMENU
:rightclick :on-context-menu :mousein EventType.MOUSEENTER
:mousein :on-mouse-enter :mouseout EventType.MOUSELEAVE
:mouseout :on-mouse-leave :hover ::hover
(throw (ex-info "not supported at this moment" {:trigger trigger})))) (throw (ex-info "not supported at this moment" {:trigger trigger}))))
(defn- translate-ease (defn- translate-ease
"Translates the uxbox ease settings to one "Translates the uxbox ease settings to one
that are compatible with anime.js library." that are compatible with anime.js library."
[ease] [ease]
{:pre [(keyword? ease)] {:pre [(keyword? ease)]}
:post [(string? %)]}
(case ease (case ease
:easein "easeInCubic" :linear js/mina.linear
:easeout "easeOutCubic" :easein js/mina.easin
:easeinout "easeInOutCubic" :easeout js/mina.easout
(name ease))) :easeinout js/mina.easeinout
(throw (ex-info "invalid ease value" {:ease ease}))))
(defn- animate
[& opts]
(js/anime (clj->js (apply hash-map opts))))
(defn- animate*
[dom {:keys [delay duration easing] :as opts}]
(let [props (dissoc opts :delay :duration :easing)
snap (js/Snap. dom)]
(p/schedule delay #(.animate snap (clj->js props) duration easing))))
;; --- Interactions to Animation Compilation ;; --- Interactions to Animation Compilation
(defn- build-moveby-interaction (defn- run-moveby-interaction
[{:keys [element moveby-x moveby-y easing delay duration]}] [{:keys [element moveby-x moveby-y easing delay duration direction]}]
(let [opts (clj->js {:targets [(str "#shape-" element)] (let [dom (dom/get-element (str "shape-" element))]
:translateX (str moveby-x "px") (if (= direction :reverse)
:translateY (str moveby-y "px") (animate* dom {:transform (str "translate(" (- moveby-x)" " (- moveby-y) ")")
:easing (translate-ease easing) :easing (translate-ease easing)
:delay delay :delay delay
:duration duration :duration duration})
:loop false})] (animate* dom {:transform (str "translate(" moveby-x " " moveby-y ")")
#(js/anime opts))) :easing (translate-ease easing)
:delay delay
:duration duration}))))
(defn- build-gotourl-interaction (declare run-hide-interaction)
(defn- run-show-interaction
[{:keys [element easing delay duration
animation direction] :as itx}]
(let [dom (dom/get-element (str "shape-" element))]
(if (= direction :reverse)
(run-hide-interaction (dissoc itx :direction))
(animate* dom {:fillOpacity "1"
:strokeOpacity "1"
:easing (translate-ease easing)
:delay delay
:duration duration}))))
(defn- run-hide-interaction
[{:keys [element easing delay duration
animation direction] :as itx}]
(let [dom (dom/get-element (str "shape-" element))]
(if (= direction :reverse)
(run-show-interaction (dissoc itx :direction))
(animate* dom {:fillOpacity "0"
:strokeOpacity "0"
:easing (translate-ease easing)
:delay delay
:duration duration}))))
(defn- run-opacity-interaction
[{:keys [element opacity easing delay
duration animation direction]}]
(let [shape (get-in @st/state [:shapes-by-id element])
dom (dom/get-element (str "shape-" element))]
(if (= direction :reverse)
(animate* dom {:fillOpacity (:fill-opacity shape "1")
:strokeOpacity (:stroke-opacity shape "1")
:easing (translate-ease easing)
:delay delay
:duration duration})
(animate* dom {:fillOpacity opacity
:strokeOpacity opacity
:easing (translate-ease easing)
:delay delay
:duration duration}))))
;; (defn- run-size-interaction
;; [{:keys [element resize-width resize-height
;; easing delay duration] :as opts}]
;; (let [shape (get-in @st/state [:shapes-by-id element])
;; tf (geom/transformation-matrix
;; (geom/resize shape (gpt/point resize-width resize-height)))]
;; (animate :targets [(str "#shape-" element)]
;; :transform (str tf)
;; :easing (translate-ease easing)
;; :delay delay
;; :duration duration
;; :loop false)))
(defn- run-size-interaction-icon
[{:keys [x1 y1 view-box rotation] :as shape}
{:keys [resize-width resize-height easing
element delay duration direction] :as opts}]
(if (= direction :reverse)
(let [end (geom/transformation-matrix shape)]
(animate :targets [(str "#shape-" element)]
:transform (str end)
:easing (translate-ease easing)
:delay delay
:duration duration
:loop false))
(let [orig-width (nth view-box 2)
orig-height (nth view-box 3)
scale-x (/ resize-width orig-width)
scale-y (/ resize-height orig-height)
center-x (- resize-width (/ resize-width 2))
center-y (- resize-height (/ resize-height 2))
end (-> (gmt/matrix)
(gmt/translate x1 y1)
(gmt/translate center-x center-y)
(gmt/rotate rotation)
(gmt/translate (- center-x) (- center-y))
(gmt/scale scale-x scale-y))
dom (dom/get-element (str "shape-" element))]
(animate* dom {:transform (str end)
:easing (translate-ease easing)
:delay delay
:duration duration}))))
(defn- run-size-interaction-rect
[{:keys [x1 y1 rotation] :as shape}
{:keys [resize-width resize-height easing
element delay duration direction] :as opts}]
(if (= direction :reverse)
(let [end (geom/transformation-matrix shape)]
(animate :targets [(str "#shape-" element)]
:transform (str end)
:easing (translate-ease easing)
:delay delay
:duration duration
:loop false))
(let [dom (dom/get-element (str "shape-" element))]
(animate* dom {:easing (translate-ease easing)
:delay delay
:duration duration
:width resize-width
:height resize-height}))))
(defn- run-size-interaction
[{:keys [element] :as opts}]
(let [shape (get-in @st/state [:shapes-by-id element])]
(case (:type shape)
:icon (run-size-interaction-icon shape opts)
:rect (run-size-interaction-rect shape opts))))
(defn- run-gotourl-interaction
[{:keys [url]}] [{:keys [url]}]
#(set! (.-href js/location) url)) (set! (.-href js/location) url))
(defn- build-interaction (defn- run-color-interaction
[{:keys [element fill-color stroke-color direction easing delay duration]}]
(let [shape (get-in @st/state [:shapes-by-id element])
dom (dom/get-element (str "shape-" element))]
(if (= direction :reverse)
(animate* dom {:easing (translate-ease easing)
:delay delay
:duration duration
:fill (:fill shape "#000000")
:stroke (:stroke shape "#000000")})
(animate* dom {:easing (translate-ease easing)
:delay delay
:duration duration
:fill fill-color
:stroke stroke-color}))))
(defn- run-rotate-interaction
[{:keys [element rotation direction easing delay duration] :as opts}]
(let [shape (get-in @st/state [:shapes-by-id element])
dom (dom/get-element (str "shape-" element))
mtx1 (geom/transformation-matrix (update shape :rotation + rotation))
mtx2 (geom/transformation-matrix shape)]
(if (= direction :reverse)
(animate* dom {:easing (translate-ease easing)
:delay delay
:duration duration
:transform (str mtx2)})
(animate* dom {:easing (translate-ease easing)
:delay delay
:duration duration
:transform (str mtx1)}))))
(defn- run-interaction
"Given an interaction data structure return "Given an interaction data structure return
a precompiled animation." a precompiled animation."
[{:keys [action] :as itx}] [{:keys [action] :as itx}]
(case action (case action
:moveby (build-moveby-interaction itx) :moveby (run-moveby-interaction itx)
:gotourl (build-gotourl-interaction itx) :show (run-show-interaction itx)
:hide (run-hide-interaction itx)
:size (run-size-interaction itx)
:opacity (run-opacity-interaction itx)
:gotourl (run-gotourl-interaction itx)
:color (run-color-interaction itx)
:rotate (run-rotate-interaction itx)
(throw (ex-info "undefined interaction" {:action action})))) (throw (ex-info "undefined interaction" {:action action}))))
;; --- Main Api ;; --- Main Api
(defn- default-callback (defn- build-hover-evt
"A default user action callback that prevents default event." "A special case for hover event."
[itx event] [itx]
(dom/prevent-default event) (letfn [(on-mouse-enter [event]
(itx)) (dom/prevent-default event)
(run-interaction itx))
(on-mouse-leave [event]
(dom/prevent-default event)
(run-interaction (assoc itx :direction :reverse)))]
[[EventType.MOUSEENTER on-mouse-enter]
[EventType.MOUSELEAVE on-mouse-leave]]))
(defn- build-attr (defn- build-generic-evt
"A reducer function that compiles interaction data structures "A reducer function that compiles interaction data structures
into apropriate event handler attributes." into apropriate event handler attributes."
[acc {:keys [trigger] :as interaction}] [evt itx]
(let [evt (translate-trigger trigger) (letfn [(on-event [event]
itx (build-interaction interaction)] (dom/prevent-default event)
(assoc acc evt (partial default-callback itx)))) (run-interaction itx))]
[[evt on-event]]))
(defn build-attrs (defn build-events
"Compile a sequence of interactions into a hash-map of event-handlers." "Compile a sequence of interactions into a hash-map of event-handlers."
[items] [shape]
(reduce build-attr {} items)) (reduce (fn [acc itx]
(let [evt (translate-trigger (:trigger itx))]
(if (= evt ::hover)
(into acc (build-hover-evt itx))
(into acc (build-generic-evt evt itx)))))
[]
(vals (:interactions shape))))

View File

@ -5,7 +5,8 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.view.ui.viewer.shapes (ns uxbox.view.ui.viewer.shapes
(:require [uxbox.util.mixins :as mx :include-macros true] (:require [goog.events :as events]
[uxbox.util.mixins :as mx :include-macros true]
[uxbox.main.state :as st] [uxbox.main.state :as st]
[uxbox.main.ui.shapes.rect :refer (rect-shape)] [uxbox.main.ui.shapes.rect :refer (rect-shape)]
[uxbox.main.ui.shapes.icon :refer (icon-shape)] [uxbox.main.ui.shapes.icon :refer (icon-shape)]
@ -13,15 +14,33 @@
[uxbox.main.ui.shapes.group :refer (group-shape)] [uxbox.main.ui.shapes.group :refer (group-shape)]
[uxbox.main.ui.shapes.line :refer (line-shape)] [uxbox.main.ui.shapes.line :refer (line-shape)]
[uxbox.main.ui.shapes.circle :refer (circle-shape)] [uxbox.main.ui.shapes.circle :refer (circle-shape)]
[uxbox.view.ui.viewer.interactions :as itx :refer (build-attrs)])) [uxbox.view.ui.viewer.interactions :as itx])
(:import goog.events.EventType))
;; --- Interactions Wrapper ;; --- Interactions Wrapper
(defn- interactions-wrapper-did-mount
[own]
(let [dom (mx/dom-node own)
shape (first (:rum/args own))
evnts (itx/build-events shape)
keys (reduce (fn [acc [evt callback]]
(conj acc (events/listen dom evt callback)))
[]
evnts)]
(assoc own ::keys keys)))
(defn- interactions-wrapper-will-unmount
[own]
(let [keys (::keys own)]
(run! #(events/unlistenByKey %) keys)
(dissoc own ::keys)))
(mx/defc interactions-wrapper (mx/defc interactions-wrapper
{:did-mount interactions-wrapper-did-mount
:will-unmount interactions-wrapper-will-unmount}
[shape factory] [shape factory]
(let [interactions (vals (:interactions shape)) [:g {:id (str "itx-" (:id shape))} (factory shape)])
attrs (itx/build-attrs interactions)]
[:g attrs (factory shape)]))
;; --- Shapes ;; --- Shapes