diff --git a/CHANGES.md b/CHANGES.md
index 50c63dd70..22ea5e5da 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,7 @@
### :sparkles: New features
- Add contrast between component select color and shape select color [Taiga #2121](https://tree.taiga.io/project/penpot/issue/2121).
+- Add animations in interactions [Taiga #2244](https://tree.taiga.io/project/penpot/us/2244).
### :bug: Bugs fixed
diff --git a/common/src/app/common/types/interactions.cljc b/common/src/app/common/types/interactions.cljc
index 66ad8dea8..5903662c3 100644
--- a/common/src/app/common/types/interactions.cljc
+++ b/common/src/app/common/types/interactions.cljc
@@ -42,6 +42,46 @@
(s/def ::event-opts
(s/multi-spec event-opts-spec ::event-type))
+;; -- Animation options
+
+(s/def ::animation-type #{:dissolve
+ :slide
+ :push})
+(s/def ::duration ::us/safe-integer)
+(s/def ::easing #{:linear
+ :ease
+ :ease-in
+ :ease-out
+ :ease-in-out})
+(s/def ::way #{:in
+ :out})
+(s/def ::direction #{:right
+ :left
+ :up
+ :down})
+(s/def ::offset-effect ::us/boolean)
+
+(defmulti animation-spec :animation-type)
+
+(defmethod animation-spec :dissolve [_]
+ (s/keys :req-un [::duration
+ ::easing]))
+
+(defmethod animation-spec :slide [_]
+ (s/keys :req-un [::duration
+ ::easing
+ ::way
+ ::direction
+ ::offset-effect]))
+
+(defmethod animation-spec :push [_]
+ (s/keys :req-un [::duration
+ ::easing
+ ::direction]))
+
+(s/def ::animation
+ (s/multi-spec animation-spec ::animation-type))
+
;; -- Options depending on action type
(s/def ::action-type #{:navigate
@@ -69,24 +109,29 @@
(defmulti action-opts-spec :action-type)
(defmethod action-opts-spec :navigate [_]
- (s/keys :opt-un [::destination ::preserve-scroll]))
+ (s/keys :opt-un [::destination
+ ::preserve-scroll
+ ::animation]))
(defmethod action-opts-spec :open-overlay [_]
(s/keys :req-un [::overlay-position
::overlay-pos-type]
:opt-un [::destination
::close-click-outside
- ::background-overlay]))
+ ::background-overlay
+ ::animation]))
(defmethod action-opts-spec :toggle-overlay [_]
(s/keys :req-un [::overlay-position
::overlay-pos-type]
:opt-un [::destination
::close-click-outside
- ::background-overlay]))
+ ::background-overlay
+ ::animation]))
(defmethod action-opts-spec :close-overlay [_]
- (s/keys :opt-un [::destination]))
+ (s/keys :opt-un [::destination
+ ::animation]))
(defmethod action-opts-spec :prev-screen [_]
(s/keys :req-un []))
@@ -122,6 +167,7 @@
;; -- Helpers for interaction
(declare calc-overlay-pos-initial)
+(declare allowed-animation?)
(defn set-event-type
[interaction event-type shape]
@@ -141,42 +187,46 @@
(assoc interaction
:event-type event-type))))
-
(defn set-action-type
[interaction action-type]
(us/verify ::interaction interaction)
(us/verify ::action-type action-type)
- (if (= (:action-type interaction) action-type)
- interaction
- (case action-type
+ (let [new-interaction
+ (if (= (:action-type interaction) action-type)
+ interaction
+ (case action-type
+ :navigate
+ (assoc interaction
+ :action-type action-type
+ :destination (get interaction :destination)
+ :preserve-scroll (get interaction :preserve-scroll false))
- :navigate
- (assoc interaction
- :action-type action-type
- :destination (get interaction :destination)
- :preserve-scroll (get interaction :preserve-scroll false))
+ (:open-overlay :toggle-overlay)
+ (let [overlay-pos-type (get interaction :overlay-pos-type :center)
+ overlay-position (get interaction :overlay-position (gpt/point 0 0))]
+ (assoc interaction
+ :action-type action-type
+ :overlay-pos-type overlay-pos-type
+ :overlay-position overlay-position))
- (:open-overlay :toggle-overlay)
- (let [overlay-pos-type (get interaction :overlay-pos-type :center)
- overlay-position (get interaction :overlay-position (gpt/point 0 0))]
- (assoc interaction
- :action-type action-type
- :overlay-pos-type overlay-pos-type
- :overlay-position overlay-position))
+ :close-overlay
+ (assoc interaction
+ :action-type action-type
+ :destination (get interaction :destination))
- :close-overlay
- (assoc interaction
- :action-type action-type
- :destination (get interaction :destination))
+ :prev-screen
+ (assoc interaction
+ :action-type action-type)
- :prev-screen
- (assoc interaction
- :action-type action-type)
+ :open-url
+ (assoc interaction
+ :action-type action-type
+ :url (get interaction :url ""))))]
- :open-url
- (assoc interaction
- :action-type action-type
- :url (get interaction :url "")))))
+ (cond-> new-interaction
+ (not (allowed-animation? action-type
+ (-> new-interaction :animation :animation-type)))
+ (dissoc :animation-type :animation))))
(defn has-delay
[interaction]
@@ -339,6 +389,129 @@
:manual
(gpt/add (:overlay-position interaction) frame-offset)))))
+(defn has-animation?
+ [interaction]
+ (#{:navigate :open-overlay :close-overlay :toggle-overlay} (:action-type interaction)))
+
+(defn allow-push?
+ [action-type]
+ ; Push animation is not allowed for overlay actions
+ (= :navigate action-type))
+
+(defn allowed-animation?
+ [action-type animation-type]
+ ; Some specific combinations are forbidden, but may occur if the action type
+ ; is changed from a type that allows the animation to another one that doesn't.
+ ; Currently the only case is an overlay action with push animation.
+ (or (not= animation-type :push)
+ (allow-push? action-type)))
+
+(defn set-animation-type
+ [interaction animation-type]
+ (us/verify ::interaction interaction)
+ (us/verify (s/nilable ::animation-type) animation-type)
+ (assert (has-animation? interaction))
+ (assert (allowed-animation? (:action-type interaction) animation-type))
+ (if (= (-> interaction :animation :animation-type) animation-type)
+ interaction
+ (if (nil? animation-type)
+ (dissoc interaction :animation)
+ (cond-> interaction
+ :always
+ (update :animation assoc :animation-type animation-type)
+
+ (= animation-type :dissolve)
+ (update :animation assoc
+ :duration (get-in interaction [:animation :duration] 300)
+ :easing (get-in interaction [:animation :easing] :linear))
+
+ (= animation-type :slide)
+ (update :animation assoc
+ :duration (get-in interaction [:animation :duration] 300)
+ :easing (get-in interaction [:animation :easing] :linear)
+ :way (get-in interaction [:animation :way] :in)
+ :direction (get-in interaction [:animation :direction] :right)
+ :offset-effect (get-in interaction [:animation :offset-effect] false))
+
+ (= animation-type :push)
+ (update :animation assoc
+ :duration (get-in interaction [:animation :duration] 300)
+ :easing (get-in interaction [:animation :easing] :linear)
+ :direction (get-in interaction [:animation :direction] :right))))))
+
+(defn has-duration?
+ [interaction]
+ (#{:dissolve :slide :push} (-> interaction :animation :animation-type)))
+
+(defn set-duration
+ [interaction duration]
+ (us/verify ::interaction interaction)
+ (us/verify ::duration duration)
+ (assert (has-duration? interaction))
+ (update interaction :animation assoc :duration duration))
+
+(defn has-easing?
+ [interaction]
+ (#{:dissolve :slide :push} (-> interaction :animation :animation-type)))
+
+(defn set-easing
+ [interaction easing]
+ (us/verify ::interaction interaction)
+ (us/verify ::easing easing)
+ (assert (has-easing? interaction))
+ (update interaction :animation assoc :easing easing))
+
+(defn has-way?
+ [interaction]
+ ; Way is ignored in slide animations of overlay actions
+ (and (= (:action-type interaction) :navigate)
+ (= (-> interaction :animation :animation-type) :slide)))
+
+(defn set-way
+ [interaction way]
+ (us/verify ::interaction interaction)
+ (us/verify ::way way)
+ (assert (has-way? interaction))
+ (update interaction :animation assoc :way way))
+
+(defn has-direction?
+ [interaction]
+ (#{:slide :push} (-> interaction :animation :animation-type)))
+
+(defn set-direction
+ [interaction direction]
+ (us/verify ::interaction interaction)
+ (us/verify ::direction direction)
+ (assert (has-direction? interaction))
+ (update interaction :animation assoc :direction direction))
+
+(defn invert-direction
+ [animation]
+ (us/verify (s/nilable ::animation) animation)
+ (case (:direction animation)
+ :right
+ (assoc animation :direction :left)
+ :left
+ (assoc animation :direction :right)
+ :up
+ (assoc animation :direction :down)
+ :down
+ (assoc animation :direction :up)
+ animation))
+
+(defn has-offset-effect?
+ [interaction]
+ ; Offset-effect is ignored in slide animations of overlay actions
+ (and (= (:action-type interaction) :navigate)
+ (= (-> interaction :animation :animation-type) :slide)))
+
+(defn set-offset-effect
+ [interaction offset-effect]
+ (us/verify ::interaction interaction)
+ (us/verify ::offset-effect offset-effect)
+ (assert (has-offset-effect? interaction))
+ (update interaction :animation assoc :offset-effect offset-effect))
+
;; -- Helpers for interactions
(defn add-interaction
diff --git a/common/test/app/common/types_interactions_test.cljc b/common/test/app/common/types_interactions_test.cljc
index 11df41948..f5df8c448 100644
--- a/common/test/app/common/types_interactions_test.cljc
+++ b/common/test/app/common/types_interactions_test.cljc
@@ -269,12 +269,252 @@
(t/testing "Set background-overlay"
(let [new-interaction (cti/set-background-overlay i3 true)]
(t/is (not (:background-overlay i3)))
- (t/is (:background-overlay new-interaction))))
-
- ))
+ (t/is (:background-overlay new-interaction))))))
-(t/deftest interactions
+(t/deftest animation-checks
+ (let [i1 cti/default-interaction
+ i2 (cti/set-action-type i1 :open-overlay)
+ i3 (cti/set-action-type i1 :toggle-overlay)
+ i4 (cti/set-action-type i1 :close-overlay)
+ i5 (cti/set-action-type i1 :prev-screen)
+ i6 (cti/set-action-type i1 :open-url)]
+
+ (t/testing "Has animation?"
+ (t/is (cti/has-animation? i1))
+ (t/is (cti/has-animation? i2))
+ (t/is (cti/has-animation? i3))
+ (t/is (cti/has-animation? i4))
+ (t/is (not (cti/has-animation? i5)))
+ (t/is (not (cti/has-animation? i6))))
+
+ (t/testing "Valid push?"
+ (t/is (cti/allow-push? (:action-type i1)))
+ (t/is (not (cti/allow-push? (:action-type i2))))
+ (t/is (not (cti/allow-push? (:action-type i3))))
+ (t/is (not (cti/allow-push? (:action-type i4))))
+ (t/is (not (cti/allow-push? (:action-type i5))))
+ (t/is (not (cti/allow-push? (:action-type i6)))))))
+
+
+(t/deftest set-animation-type
+ (let [i1 cti/default-interaction
+ i2 (cti/set-animation-type i1 :dissolve)]
+
+ (t/testing "Set animation type nil"
+ (let [new-interaction
+ (cti/set-animation-type i1 nil)]
+ (t/is (nil? (-> new-interaction :animation :animation-type)))))
+
+ (t/testing "Set animation type unchanged"
+ (let [new-interaction
+ (cti/set-animation-type i2 :dissolve)]
+ (t/is (= :dissolve (-> new-interaction :animation :animation-type)))))
+
+ (t/testing "Set animation type changed"
+ (let [new-interaction
+ (cti/set-animation-type i2 :slide)]
+ (t/is (= :slide (-> new-interaction :animation :animation-type)))))
+
+ (t/testing "Set animation type reset"
+ (let [new-interaction
+ (cti/set-animation-type i2 nil)]
+ (t/is (nil? (-> new-interaction :animation)))))
+
+ (t/testing "Set animation type dissolve"
+ (let [new-interaction
+ (cti/set-animation-type i1 :dissolve)]
+ (t/is (= :dissolve (-> new-interaction :animation :animation-type)))
+ (t/is (= 300 (-> new-interaction :animation :duration)))
+ (t/is (= :linear (-> new-interaction :animation :easing)))))
+
+ (t/testing "Set animation type dissolve with previous data"
+ (let [interaction (assoc i1 :animation {:animation-type :slide
+ :duration 1000
+ :easing :ease-out
+ :way :out
+ :direction :left
+ :offset-effect true})
+ new-interaction
+ (cti/set-animation-type interaction :dissolve)]
+ (t/is (= :dissolve (-> new-interaction :animation :animation-type)))
+ (t/is (= 1000 (-> new-interaction :animation :duration)))
+ (t/is (= :ease-out (-> new-interaction :animation :easing)))))
+
+ (t/testing "Set animation type slide"
+ (let [new-interaction
+ (cti/set-animation-type i1 :slide)]
+ (t/is (= :slide (-> new-interaction :animation :animation-type)))
+ (t/is (= 300 (-> new-interaction :animation :duration)))
+ (t/is (= :linear (-> new-interaction :animation :easing)))
+ (t/is (= :in (-> new-interaction :animation :way)))
+ (t/is (= :right (-> new-interaction :animation :direction)))
+ (t/is (= false (-> new-interaction :animation :offset-effect)))))
+
+ (t/testing "Set animation type slide with previous data"
+ (let [interaction (assoc i1 :animation {:animation-type :dissolve
+ :duration 1000
+ :easing :ease-out
+ :way :out
+ :direction :left
+ :offset-effect true})
+ new-interaction
+ (cti/set-animation-type interaction :slide)]
+ (t/is (= :slide (-> new-interaction :animation :animation-type)))
+ (t/is (= 1000 (-> new-interaction :animation :duration)))
+ (t/is (= :ease-out (-> new-interaction :animation :easing)))
+ (t/is (= :out (-> new-interaction :animation :way)))
+ (t/is (= :left (-> new-interaction :animation :direction)))
+ (t/is (= true (-> new-interaction :animation :offset-effect)))))
+
+ (t/testing "Set animation type push"
+ (let [new-interaction
+ (cti/set-animation-type i1 :push)]
+ (t/is (= :push (-> new-interaction :animation :animation-type)))
+ (t/is (= 300 (-> new-interaction :animation :duration)))
+ (t/is (= :linear (-> new-interaction :animation :easing)))
+ (t/is (= :right (-> new-interaction :animation :direction)))))
+
+ (t/testing "Set animation type push with previous data"
+ (let [interaction (assoc i1 :animation {:animation-type :slide
+ :duration 1000
+ :easing :ease-out
+ :way :out
+ :direction :left
+ :offset-effect true})
+ new-interaction
+ (cti/set-animation-type interaction :push)]
+ (t/is (= :push (-> new-interaction :animation :animation-type)))
+ (t/is (= 1000 (-> new-interaction :animation :duration)))
+ (t/is (= :ease-out (-> new-interaction :animation :easing)))
+ (t/is (= :left (-> new-interaction :animation :direction)))))))
+
+
+(t/deftest allowed-animation
+ (let [i1 (cti/set-action-type cti/default-interaction :open-overlay)
+ i2 (cti/set-action-type cti/default-interaction :close-overlay)
+ i3 (cti/set-action-type cti/default-interaction :toggle-overlay)]
+
+ (t/testing "Cannot use animation push for an overlay action"
+ (let [bad-interaction-1 (assoc i1 :animation {:animation-type :push
+ :duration 1000
+ :easing :ease-out
+ :direction :left})
+ bad-interaction-2 (assoc i2 :animation {:animation-type :push
+ :duration 1000
+ :easing :ease-out
+ :direction :left})
+ bad-interaction-3 (assoc i3 :animation {:animation-type :push
+ :duration 1000
+ :easing :ease-out
+ :direction :left})]
+ (t/is (not (cti/allowed-animation? (:action-type bad-interaction-1)
+ (-> bad-interaction-1 :animation :animation-type))))
+ (t/is (not (cti/allowed-animation? (:action-type bad-interaction-2)
+ (-> bad-interaction-1 :animation :animation-type))))
+ (t/is (not (cti/allowed-animation? (:action-type bad-interaction-3)
+ (-> bad-interaction-1 :animation :animation-type))))))
+
+ (t/testing "Remove animation if moving to an forbidden state"
+ (let [interaction (cti/set-animation-type cti/default-interaction :push)
+ new-interaction (cti/set-action-type interaction :open-overlay)]
+ (t/is (nil? (:animation new-interaction)))))))
+
+
+(t/deftest option-duration
+ (let [i1 cti/default-interaction
+ i2 (cti/set-animation-type cti/default-interaction :dissolve)]
+
+ (t/testing "Has duration?"
+ (t/is (not (cti/has-duration? i1)))
+ (t/is (cti/has-duration? i2)))
+
+ (t/testing "Set duration"
+ (let [new-interaction (cti/set-duration i2 1000)]
+ (t/is (= 1000 (-> new-interaction :animation :duration)))))))
+
+
+(t/deftest option-easing
+ (let [i1 cti/default-interaction
+ i2 (cti/set-animation-type cti/default-interaction :dissolve)]
+
+ (t/testing "Has easing?"
+ (t/is (not (cti/has-easing? i1)))
+ (t/is (cti/has-easing? i2)))
+
+ (t/testing "Set easing"
+ (let [new-interaction (cti/set-easing i2 :ease-in)]
+ (t/is (= :ease-in (-> new-interaction :animation :easing)))))))
+
+
+(t/deftest option-way
+ (let [i1 cti/default-interaction
+ i2 (cti/set-animation-type cti/default-interaction :slide)
+ i3 (cti/set-action-type i2 :open-overlay)]
+
+ (t/testing "Has way?"
+ (t/is (not (cti/has-way? i1)))
+ (t/is (cti/has-way? i2))
+ (t/is (not (cti/has-way? i3)))
+ (t/is (some? (-> i3 :animation :way)))) ; <- it exists but is ignored
+
+ (t/testing "Set way"
+ (let [new-interaction (cti/set-way i2 :out)]
+ (t/is (= :out (-> new-interaction :animation :way)))))))
+
+
+(t/deftest option-direction
+ (let [i1 cti/default-interaction
+ i2 (cti/set-animation-type cti/default-interaction :push)
+ i3 (cti/set-animation-type cti/default-interaction :dissolve)]
+
+ (t/testing "Has direction?"
+ (t/is (not (cti/has-direction? i1)))
+ (t/is (cti/has-direction? i2)))
+
+ (t/testing "Set direction"
+ (let [new-interaction (cti/set-direction i2 :left)]
+ (t/is (= :left (-> new-interaction :animation :direction)))))
+
+ (t/testing "Invert direction"
+ (let [a-none (:animation i3)
+ a-right (:animation i2)
+ a-left (assoc a-right :direction :left)
+ a-up (assoc a-right :direction :up)
+ a-down (assoc a-right :direction :down)
+
+ a-nil' (cti/invert-direction nil)
+ a-none' (cti/invert-direction a-none)
+ a-right' (cti/invert-direction a-right)
+ a-left' (cti/invert-direction a-left)
+ a-up' (cti/invert-direction a-up)
+ a-down' (cti/invert-direction a-down)]
+
+ (t/is (nil? a-nil'))
+ (t/is (nil? (:direction a-none')))
+ (t/is (= :left (:direction a-right')))
+ (t/is (= :right (:direction a-left')))
+ (t/is (= :down (:direction a-up')))
+ (t/is (= :up (:direction a-down')))))))
+
+
+(t/deftest option-offset-effect
+ (let [i1 cti/default-interaction
+ i2 (cti/set-animation-type cti/default-interaction :slide)
+ i3 (cti/set-action-type i2 :open-overlay)]
+
+ (t/testing "Has offset-effect"
+ (t/is (not (cti/has-offset-effect? i1)))
+ (t/is (cti/has-offset-effect? i2))
+ (t/is (not (cti/has-offset-effect? i3)))
+ (t/is (some? (-> i3 :animation :offset-effect)))) ; <- it exists but is ignored
+
+ (t/testing "Set offset-effect"
+ (let [new-interaction (cti/set-offset-effect i2 true)]
+ (t/is (= true (-> new-interaction :animation :offset-effect)))))))
+
+
+(t/deftest modify-interactions
(let [i1 (cti/set-action-type cti/default-interaction :open-overlay)
i2 (cti/set-action-type cti/default-interaction :close-overlay)
i3 (cti/set-action-type cti/default-interaction :prev-screen)
@@ -298,7 +538,37 @@
(t/testing "Update interaction"
(let [new-interactions (cti/update-interaction interactions 1 #(cti/set-action-type % :open-url))]
(t/is (= (count new-interactions) 2))
- (t/is (= (:action-type (last new-interactions)) :open-url))))
+ (t/is (= (:action-type (last new-interactions)) :open-url))))))
- ))
+
+(t/deftest remap-interactions
+ (let [frame1 (cpi/make-minimal-shape :frame)
+ frame2 (cpi/make-minimal-shape :frame)
+ frame3 (cpi/make-minimal-shape :frame)
+ frame4 (cpi/make-minimal-shape :frame)
+ frame5 (cpi/make-minimal-shape :frame)
+ frame6 (cpi/make-minimal-shape :frame)
+
+ objects {(:id frame3) frame3
+ (:id frame4) frame4
+ (:id frame5) frame5}
+
+ ids-map {(:id frame1) (:id frame4)
+ (:id frame2) (:id frame5)}
+
+ i1 (cti/set-destination cti/default-interaction (:id frame1))
+ i2 (cti/set-destination cti/default-interaction (:id frame2))
+ i3 (cti/set-destination cti/default-interaction (:id frame3))
+ i4 (cti/set-destination cti/default-interaction nil)
+ i5 (cti/set-destination cti/default-interaction (:id frame6))
+
+ interactions [i1 i2 i3 i4 i5]]
+
+ (t/testing "Remap interactions"
+ (let [new-interactions (cti/remap-interactions interactions ids-map objects)]
+ (t/is (= (count new-interactions) 4))
+ (t/is (= (:id frame4) (:destination (get new-interactions 0))))
+ (t/is (= (:id frame5) (:destination (get new-interactions 1))))
+ (t/is (= (:id frame3) (:destination (get new-interactions 2))))
+ (t/is (nil? (:destination (get new-interactions 3))))))))
diff --git a/frontend/resources/images/icons/animate-down.svg b/frontend/resources/images/icons/animate-down.svg
new file mode 100644
index 000000000..6eb7a3501
--- /dev/null
+++ b/frontend/resources/images/icons/animate-down.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/animate-left.svg b/frontend/resources/images/icons/animate-left.svg
new file mode 100644
index 000000000..39318e4cc
--- /dev/null
+++ b/frontend/resources/images/icons/animate-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/animate-right.svg b/frontend/resources/images/icons/animate-right.svg
new file mode 100644
index 000000000..aadf5a05b
--- /dev/null
+++ b/frontend/resources/images/icons/animate-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/animate-up.svg b/frontend/resources/images/icons/animate-up.svg
new file mode 100644
index 000000000..31e877423
--- /dev/null
+++ b/frontend/resources/images/icons/animate-up.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/easing-ease-in-out.svg b/frontend/resources/images/icons/easing-ease-in-out.svg
new file mode 100644
index 000000000..3ada98e19
--- /dev/null
+++ b/frontend/resources/images/icons/easing-ease-in-out.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/easing-ease-in.svg b/frontend/resources/images/icons/easing-ease-in.svg
new file mode 100644
index 000000000..40d2bbd24
--- /dev/null
+++ b/frontend/resources/images/icons/easing-ease-in.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/easing-ease-out.svg b/frontend/resources/images/icons/easing-ease-out.svg
new file mode 100644
index 000000000..9bf6cd73f
--- /dev/null
+++ b/frontend/resources/images/icons/easing-ease-out.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/easing-ease.svg b/frontend/resources/images/icons/easing-ease.svg
new file mode 100644
index 000000000..7760f329d
--- /dev/null
+++ b/frontend/resources/images/icons/easing-ease.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/easing-linear.svg b/frontend/resources/images/icons/easing-linear.svg
new file mode 100644
index 000000000..f3ab49784
--- /dev/null
+++ b/frontend/resources/images/icons/easing-linear.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss
index ee2edb90f..f60fcaa3a 100644
--- a/frontend/resources/styles/common/framework.scss
+++ b/frontend/resources/styles/common/framework.scss
@@ -597,6 +597,7 @@ input.element-name {
label{
cursor: pointer;
display: flex;
+ align-items: center;
margin-right: 15px;
font-size: $fs12;
diff --git a/frontend/resources/styles/main/partials/sidebar-interactions.scss b/frontend/resources/styles/main/partials/sidebar-interactions.scss
index 568dd76e1..7a4dcabf7 100644
--- a/frontend/resources/styles/main/partials/sidebar-interactions.scss
+++ b/frontend/resources/styles/main/partials/sidebar-interactions.scss
@@ -80,6 +80,63 @@
}
}
+.interactions-way-buttons {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ & .input-radio {
+ margin-bottom: 0;
+
+ & label {
+ color: $color-gray-20;
+
+ &:before {
+ background-color: unset;
+ }
+ }
+
+ & input[type=radio]:checked {
+ & + label {
+ &:before {
+ background-color: $color-primary;
+ box-shadow: inset 0 0 0 5px $color-gray-50;
+ }
+ }
+ }
+ }
+}
+
+.interactions-direction-buttons {
+ margin-top: $size-2;
+ padding-top: $size-2;
+ padding-bottom: $size-2;
+ justify-content: space-around;
+
+ .element-set-actions-button {
+ min-width: 40px;
+ min-height: 13px;
+ }
+
+ svg {
+ height: 13px;
+ width: 13px;
+ }
+}
+
+.interactions-easing-icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-width: 30px;
+ min-height: 30px;
+
+ & svg {
+ width: 12px;
+ height: 12px;
+ stroke: $color-gray-20;
+ }
+}
+
.flow-element {
display: flex;
align-items: center;
diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss
index 34e2c5779..c5fc1b30b 100644
--- a/frontend/resources/styles/main/partials/viewer.scss
+++ b/frontend/resources/styles/main/partials/viewer.scss
@@ -12,19 +12,33 @@
grid-row: 1 / span 2;
grid-column: 1 / span 1;
- overflow: auto;
-
display: flex;
justify-content: center;
align-items: center;
flex-flow: wrap;
- .empty-state {
- justify-content: center;
- align-items: center;
- }
+ overflow: auto;
- svg {
- transform-origin: center;
+ & .viewer-wrapper {
+ display: grid;
+ grid-template-rows: 1fr;
+ grid-template-columns: 1fr;
+ justify-items: center;
+ align-items: center;
+ overflow: hidden;
+
+ .empty-state {
+ justify-content: center;
+ align-items: center;
+ }
+
+ svg {
+ transform-origin: center;
+ }
}
}
+
+.viewport-container {
+ grid-column: 1 / 1;
+ grid-row: 1 / 1;
+}
diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs
index b87a398fe..63bd8c34b 100644
--- a/frontend/src/app/main/data/viewer.cljs
+++ b/frontend/src/app/main/data/viewer.cljs
@@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.pages :as cp]
[app.common.spec :as us]
+ [app.common.types.interactions :as cti]
[app.common.uuid :as uuid]
[app.main.constants :as c]
[app.main.data.comments :as dcm]
@@ -316,6 +317,12 @@
(update [_ state]
(d/dissoc-in state [:viewer-local :nav-scroll]))))
+(defn complete-animation
+ []
+ (ptk/reify ::complete-animation
+ ptk/UpdateEvent
+ (update [_ state]
+ (d/dissoc-in state [:viewer-local :current-animation]))))
;; --- Navigation inside page
@@ -335,23 +342,38 @@
(rx/of (rt/nav screen pparams (assoc qparams :index index)))))))
(defn go-to-frame
- [frame-id]
- (us/verify ::us/uuid frame-id)
- (ptk/reify ::go-to-frame
- ptk/UpdateEvent
- (update [_ state]
- (assoc-in state [:viewer-local :overlays] []))
+ ([frame-id] (go-to-frame frame-id nil))
+ ([frame-id animation]
+ (us/verify ::us/uuid frame-id)
+ (us/verify (s/nilable ::cti/animation) animation)
+ (ptk/reify ::go-to-frame
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [route (:route state)
+ qparams (:query-params route)
+ page-id (:page-id qparams)
+ index (:index qparams)
+ frames (get-in state [:viewer :pages page-id :frames])
+ frame (get frames index)]
+ (cond-> state
+ :always
+ (assoc-in [:viewer-local :overlays] [])
- ptk/WatchEvent
- (watch [_ state _]
- (let [route (:route state)
- qparams (:query-params route)
- page-id (:page-id qparams)
+ (some? animation)
+ (assoc-in [:viewer-local :current-animation]
+ {:kind :go-to-frame
+ :orig-frame-id (:id frame)
+ :animation animation}))))
- frames (get-in state [:viewer :pages page-id :frames])
- index (d/index-of-pred frames #(= (:id %) frame-id))]
- (when index
- (rx/of (go-to-frame-by-index index)))))))
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [route (:route state)
+ qparams (:query-params route)
+ page-id (:page-id qparams)
+ frames (get-in state [:viewer :pages page-id :frames])
+ index (d/index-of-pred frames #(= (:id %) frame-id))]
+ (when index
+ (rx/of (go-to-frame-by-index index))))))))
(defn go-to-frame-auto
[]
@@ -383,12 +405,39 @@
;; --- Overlays
+(defn- do-open-overlay
+ [state frame position close-click-outside background-overlay animation]
+ (cond-> state
+ :always
+ (update-in [:viewer-local :overlays] conj
+ {:frame frame
+ :position position
+ :close-click-outside close-click-outside
+ :background-overlay background-overlay})
+ (some? animation)
+ (assoc-in [:viewer-local :current-animation]
+ {:kind :open-overlay
+ :overlay-id (:id frame)
+ :animation animation})))
+
+(defn- do-close-overlay
+ [state frame-id animation]
+ (if (nil? animation)
+ (update-in state [:viewer-local :overlays]
+ (fn [overlays]
+ (d/removev #(= (:id (:frame %)) frame-id) overlays)))
+ (assoc-in state [:viewer-local :current-animation]
+ {:kind :close-overlay
+ :overlay-id frame-id
+ :animation animation})))
+
(defn open-overlay
- [frame-id position close-click-outside background-overlay]
+ [frame-id position close-click-outside background-overlay animation]
(us/verify ::us/uuid frame-id)
(us/verify ::us/point position)
(us/verify (s/nilable ::us/boolean) close-click-outside)
(us/verify (s/nilable ::us/boolean) background-overlay)
+ (us/verify (s/nilable ::cti/animation) animation)
(ptk/reify ::open-overlay
ptk/UpdateEvent
(update [_ state]
@@ -399,19 +448,21 @@
frame (d/seek #(= (:id %) frame-id) frames)
overlays (get-in state [:viewer-local :overlays])]
(if-not (some #(= (:frame %) frame) overlays)
- (update-in state [:viewer-local :overlays] conj
- {:frame frame
- :position position
- :close-click-outside close-click-outside
- :background-overlay background-overlay})
+ (do-open-overlay state
+ frame
+ position
+ close-click-outside
+ background-overlay
+ animation)
state)))))
(defn toggle-overlay
- [frame-id position close-click-outside background-overlay]
+ [frame-id position close-click-outside background-overlay animation]
(us/verify ::us/uuid frame-id)
(us/verify ::us/point position)
(us/verify (s/nilable ::us/boolean) close-click-outside)
(us/verify (s/nilable ::us/boolean) background-overlay)
+ (us/verify (s/nilable ::cti/animation) animation)
(ptk/reify ::toggle-overlay
ptk/UpdateEvent
(update [_ state]
@@ -422,23 +473,27 @@
frame (d/seek #(= (:id %) frame-id) frames)
overlays (get-in state [:viewer-local :overlays])]
(if-not (some #(= (:frame %) frame) overlays)
- (update-in state [:viewer-local :overlays] conj
- {:frame frame
- :position position
- :close-click-outside close-click-outside
- :background-overlay background-overlay})
- (update-in state [:viewer-local :overlays]
- (fn [overlays]
- (d/removev #(= (:id (:frame %)) frame-id) overlays))))))))
+ (do-open-overlay state
+ frame
+ position
+ close-click-outside
+ background-overlay
+ animation)
+ (do-close-overlay state
+ (:id frame)
+ (cti/invert-direction animation)))))))
(defn close-overlay
- [frame-id]
- (ptk/reify ::close-overlay
- ptk/UpdateEvent
- (update [_ state]
- (update-in state [:viewer-local :overlays]
- (fn [overlays]
- (d/removev #(= (:id (:frame %)) frame-id) overlays))))))
+ ([frame-id] (close-overlay frame-id nil))
+ ([frame-id animation]
+ (us/verify ::us/uuid frame-id)
+ (us/verify (s/nilable ::cti/animation) animation)
+ (ptk/reify ::close-overlay
+ ptk/UpdateEvent
+ (update [_ state]
+ (do-close-overlay state
+ frame-id
+ animation)))))
;; --- Objects selection
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index fce2cdc65..2a2c7b5d8 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -17,6 +17,10 @@
(def align-middle (icon-xref :align-middle))
(def align-top (icon-xref :align-top))
(def alignment (icon-xref :alignment))
+(def animate-down (icon-xref :animate-down))
+(def animate-left (icon-xref :animate-left))
+(def animate-right (icon-xref :animate-right))
+(def animate-up (icon-xref :animate-up))
(def arrow-down (icon-xref :arrow-down))
(def arrow-end (icon-xref :arrow-end))
(def arrow-slide (icon-xref :arrow-slide))
@@ -42,6 +46,11 @@
(def copy (icon-xref :copy))
(def curve (icon-xref :curve))
(def download (icon-xref :download))
+(def easing-linear (icon-xref :easing-linear))
+(def easing-ease (icon-xref :easing-ease))
+(def easing-ease-in (icon-xref :easing-ease-in))
+(def easing-ease-out (icon-xref :easing-ease-out))
+(def easing-ease-in-out (icon-xref :easing-ease-in-out))
(def exit (icon-xref :exit))
(def export (icon-xref :export))
(def eye (icon-xref :eye))
diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs
index 18d2485be..429085899 100644
--- a/frontend/src/app/main/ui/viewer.cljs
+++ b/frontend/src/app/main/ui/viewer.cljs
@@ -6,6 +6,7 @@
(ns app.main.ui.viewer
(:require
+ [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.main.data.comments :as dcm]
@@ -31,9 +32,22 @@
(defn- calculate-size
[frame zoom]
(let [{:keys [_ _ width height]} (filters/get-filters-bounds frame)]
- {:width (* width zoom)
- :height (* height zoom)
- :vbox (str "0 0 " width " " height)}))
+ {:base-width width
+ :base-height height
+ :width (* width zoom)
+ :height (* height zoom)
+ :vbox (str "0 0 " width " " height)}))
+
+(defn- calculate-wrapper
+ [size1 size2 zoom]
+ (cond
+ (nil? size1) size2
+ (nil? size2) size1
+ :else (let [width (max (:base-width size1) (:base-width size2))
+ height (max (:base-height size1) (:base-height size2))]
+ {:width (* width zoom)
+ :height (* height zoom)
+ :vbox (str "0 0 " width " " height)})))
(mf/defc viewer
[{:keys [params data]}]
@@ -41,24 +55,41 @@
(let [{:keys [page-id section index]} params
{:keys [file users project permissions]} data
- local (mf/deref refs/viewer-local)
+ local (mf/deref refs/viewer-local)
nav-scroll (:nav-scroll local)
+ orig-viewport-ref (mf/use-ref nil)
+ current-viewport-ref (mf/use-ref nil)
+ current-animation (:current-animation local)
page-id (or page-id (-> file :data :pages first))
- page (mf/use-memo
- (mf/deps data page-id)
- (fn []
- (get-in data [:pages page-id])))
+ page (mf/use-memo
+ (mf/deps data page-id)
+ (fn []
+ (get-in data [:pages page-id])))
- zoom (:zoom local)
- frames (:frames page)
- frame (get frames index)
+ zoom (:zoom local)
+ frames (:frames page)
+ frame (get frames index)
- size (mf/use-memo
- (mf/deps frame zoom)
- (fn [] (calculate-size frame zoom)))
+ overlays (:overlays local)
+
+ orig-frame
+ (when (:orig-frame-id current-animation)
+ (d/seek #(= (:id %) (:orig-frame-id current-animation)) frames))
+
+ size (mf/use-memo
+ (mf/deps frame zoom)
+ (fn [] (calculate-size frame zoom)))
+
+ orig-size (mf/use-memo
+ (mf/deps orig-frame zoom)
+ (fn [] (when orig-frame (calculate-size orig-frame zoom))))
+
+ wrapper-size (mf/use-memo
+ (mf/deps size orig-size zoom)
+ (fn [] (calculate-wrapper size orig-size zoom)))
interactions-mode
(:interactions-mode local)
@@ -96,11 +127,67 @@
(mf/use-layout-effect
(mf/deps nav-scroll)
(fn []
+ ;; Set scroll position after navigate
(when (number? nav-scroll)
(let [viewer-section (dom/get-element "viewer-section")]
(st/emit! (dv/reset-nav-scroll))
(dom/set-scroll-pos! viewer-section nav-scroll)))))
+ (mf/use-layout-effect
+ (mf/deps index)
+ (fn []
+ ;; Navigate animation needs to be started after navigation
+ ;; is complete, and we have the next page index.
+ (when (and current-animation
+ (= (:kind current-animation) :go-to-frame))
+ (let [orig-viewport (mf/ref-val orig-viewport-ref)
+ current-viewport (mf/ref-val current-viewport-ref)]
+ (interactions/animate-go-to-frame
+ (:animation current-animation)
+ current-viewport
+ orig-viewport
+ size
+ orig-size
+ wrapper-size)))))
+
+ (mf/use-layout-effect
+ (mf/deps current-animation)
+ (fn []
+ ;; Overlay animations may be started when needed.
+ (when current-animation
+ (case (:kind current-animation)
+
+ :open-overlay
+ (let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation))))
+ overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation))
+ overlays)
+ overlay-size (calculate-size (:frame overlay) zoom)
+ overlay-position {:x (* (:x (:position overlay)) zoom)
+ :y (* (:y (:position overlay)) zoom)}]
+ (interactions/animate-open-overlay
+ (:animation current-animation)
+ overlay-viewport
+ wrapper-size
+ overlay-size
+ overlay-position))
+
+ :close-overlay
+ (let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation))))
+ overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation))
+ overlays)
+ overlay-size (calculate-size (:frame overlay) zoom)
+ overlay-position {:x (* (:x (:position overlay)) zoom)
+ :y (* (:y (:position overlay)) zoom)}]
+ (interactions/animate-close-overlay
+ (:animation current-animation)
+ overlay-viewport
+ wrapper-size
+ overlay-size
+ overlay-position
+ (:id (:frame overlay))))
+
+ nil))))
+
[:div {:class (dom/classnames
:force-visible (:show-thumbnails local)
:viewer-layout (not= section :handoff)
@@ -139,57 +226,81 @@
:section section
:local local}]
- [:div.viewport-container
- {:style {:width (:width size)
- :height (:height size)
- :position "relative"}}
+ [:*
+ [:div.viewer-wrapper
+ {:style {:width (:width wrapper-size)
+ :height (:height wrapper-size)}}
- (when (= section :comments)
- [:& comments-layer {:file file
- :users users
- :frame frame
- :page page
- :zoom zoom}])
+ (when orig-frame
+ [:div.viewport-container
+ {:ref orig-viewport-ref
+ :style {:width (:width orig-size)
+ :height (:height orig-size)
+ :position "relative"}}
- [:& interactions/viewport
- {:frame frame
- :base-frame frame
- :frame-offset (gpt/point 0 0)
- :size size
- :page page
- :file file
- :users users
- :interactions-mode interactions-mode}]
+ [:& interactions/viewport
+ {:frame orig-frame
+ :base-frame orig-frame
+ :frame-offset (gpt/point 0 0)
+ :size orig-size
+ :page page
+ :file file
+ :users users
+ :interactions-mode :hide}]])
- (for [overlay (:overlays local)]
- (let [size-over (calculate-size (:frame overlay) zoom)]
- [:*
- (when (or (:close-click-outside overlay)
- (:background-overlay overlay))
- [:div.viewer-overlay-background
- {:class (dom/classnames
- :visible (:background-overlay overlay))
- :style {:width (:width frame)
- :height (:height frame)
- :position "absolute"
- :left 0
- :top 0}
- :on-click #(when (:close-click-outside overlay)
- (close-overlay (:frame overlay)))}])
- [:div.viewport-container.viewer-overlay
- {:style {:width (:width size-over)
- :height (:height size-over)
- :left (* (:x (:position overlay)) zoom)
- :top (* (:y (:position overlay)) zoom)}}
- [:& interactions/viewport
- {:frame (:frame overlay)
- :base-frame frame
- :frame-offset (:position overlay)
- :size size-over
- :page page
- :file file
- :users users
- :interactions-mode interactions-mode}]]]))]))]]]))
+ [:div.viewport-container
+ {:ref current-viewport-ref
+ :style {:width (:width size)
+ :height (:height size)
+ :position "relative"}
+ }
+ (when (= section :comments)
+ [:& comments-layer {:file file
+ :users users
+ :frame frame
+ :page page
+ :zoom zoom}])
+
+ [:& interactions/viewport
+ {:frame frame
+ :base-frame frame
+ :frame-offset (gpt/point 0 0)
+ :size size
+ :page page
+ :file file
+ :users users
+ :interactions-mode interactions-mode}]
+
+ (for [overlay overlays]
+ (let [size-over (calculate-size (:frame overlay) zoom)]
+ [:*
+ (when (or (:close-click-outside overlay)
+ (:background-overlay overlay))
+ [:div.viewer-overlay-background
+ {:class (dom/classnames
+ :visible (:background-overlay overlay))
+ :style {:width (:width wrapper-size)
+ :height (:height wrapper-size)
+ :position "absolute"
+ :left 0
+ :top 0}
+ :on-click #(when (:close-click-outside overlay)
+ (close-overlay (:frame overlay)))}])
+ [:div.viewport-container.viewer-overlay
+ {:id (str "overlay-" (str (:id (:frame overlay))))
+ :style {:width (:width size-over)
+ :height (:height size-over)
+ :left (* (:x (:position overlay)) zoom)
+ :top (* (:y (:position overlay)) zoom)}}
+ [:& interactions/viewport
+ {:frame (:frame overlay)
+ :base-frame frame
+ :frame-offset (:position overlay)
+ :size size-over
+ :page page
+ :file file
+ :users users
+ :interactions-mode interactions-mode}]]]))]]]))]]]))
;; --- Component: Viewer Page
diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs
index f0534aa95..1258e49f2 100644
--- a/frontend/src/app/main/ui/viewer/interactions.cljs
+++ b/frontend/src/app/main/ui/viewer/interactions.cljs
@@ -169,3 +169,338 @@
[:span.icon i/tick]
[:span.label (tr "viewer.header.show-interactions-on-click")]]]]]))
+
+(defn animate-go-to-frame
+ [animation current-viewport orig-viewport current-size orig-size wrapper-size]
+ (case (:animation-type animation)
+
+ :dissolve
+ (do (dom/animate! orig-viewport
+ [#js {:opacity "100"}
+ #js {:opacity "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (dom/animate! current-viewport
+ [#js {:opacity "0"}
+ #js {:opacity "100"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}))
+
+ :slide
+ (case (:way animation)
+
+ :in
+ (case (:direction animation)
+
+ :right
+ (let [offset (+ (:width current-size)
+ (/ (- (:width wrapper-size) (:width current-size)) 2))]
+ (dom/animate! current-viewport
+ [#js {:left (str "-" offset "px")}
+ #js {:left "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! orig-viewport
+ [#js {:left "0"
+ :opacity "100%"}
+ #js {:left (str (* offset 0.2) "px")
+ :opacity "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))
+
+ :left
+ (let [offset (+ (:width current-size)
+ (/ (- (:width wrapper-size) (:width current-size)) 2))]
+ (dom/animate! current-viewport
+ [#js {:right (str "-" offset "px")}
+ #js {:right "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! orig-viewport
+ [#js {:right "0"
+ :opacity "100%"}
+ #js {:right (str (* offset 0.2) "px")
+ :opacity "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))
+
+ :up
+ (let [offset (+ (:height current-size)
+ (/ (- (:height wrapper-size) (:height current-size)) 2))]
+ (dom/animate! current-viewport
+ [#js {:bottom (str "-" offset "px")}
+ #js {:bottom "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! orig-viewport
+ [#js {:bottom "0"
+ :opacity "100%"}
+ #js {:bottom (str (* offset 0.2) "px")
+ :opacity "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))
+
+ :down
+ (let [offset (+ (:height current-size)
+ (/ (- (:height wrapper-size) (:height current-size)) 2))]
+ (dom/animate! current-viewport
+ [#js {:top (str "-" offset "px")}
+ #js {:top "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! orig-viewport
+ [#js {:top "0"
+ :opacity "100%"}
+ #js {:top (str (* offset 0.2) "px")
+ :opacity "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}))))
+
+ :out
+ (case (:direction animation)
+
+ :right
+ (let [offset (+ (:width orig-size)
+ (/ (- (:width wrapper-size) (:width orig-size)) 2))]
+ (dom/set-css-property! orig-viewport "z-index" 10000)
+ (dom/animate! orig-viewport
+ [#js {:right "0"}
+ #js {:right (str "-" offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! current-viewport
+ [#js {:right (str (* offset 0.2) "px")
+ :opacity "0"}
+ #js {:right "0"
+ :opacity "100%"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))
+
+ :left
+ (let [offset (+ (:width orig-size)
+ (/ (- (:width wrapper-size) (:width orig-size)) 2))]
+ (dom/set-css-property! orig-viewport "z-index" 10000)
+ (dom/animate! orig-viewport
+ [#js {:left "0"}
+ #js {:left (str "-" offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! current-viewport
+ [#js {:left (str (* offset 0.2) "px")
+ :opacity "0"}
+ #js {:left "0"
+ :opacity "100%"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))
+
+ :up
+ (let [offset (+ (:height orig-size)
+ (/ (- (:height wrapper-size) (:height orig-size)) 2))]
+ (dom/set-css-property! orig-viewport "z-index" 10000)
+ (dom/animate! orig-viewport
+ [#js {:top "0"}
+ #js {:top (str "-" offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! current-viewport
+ [#js {:top (str (* offset 0.2) "px")
+ :opacity "0"}
+ #js {:top "0"
+ :opacity "100%"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))
+
+ :down
+ (let [offset (+ (:height orig-size)
+ (/ (- (:height wrapper-size) (:height orig-size)) 2))]
+ (dom/set-css-property! orig-viewport "z-index" 10000)
+ (dom/animate! orig-viewport
+ [#js {:bottom "0"}
+ #js {:bottom (str "-" offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (when (:offset-effect animation)
+ (dom/animate! current-viewport
+ [#js {:bottom (str (* offset 0.2) "px")
+ :opacity "0"}
+ #js {:bottom "0"
+ :opacity "100%"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))))
+
+ :push
+ (case (:direction animation)
+
+ :right
+ (let [offset (:width wrapper-size)]
+ (dom/animate! current-viewport
+ [#js {:left (str "-" offset "px")}
+ #js {:left "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (dom/animate! orig-viewport
+ [#js {:left "0"}
+ #js {:left (str offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}))
+
+ :left
+ (let [offset (:width wrapper-size)]
+ (dom/animate! current-viewport
+ [#js {:right (str "-" offset "px")}
+ #js {:right "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (dom/animate! orig-viewport
+ [#js {:right "0"}
+ #js {:right (str offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}))
+
+ :up
+ (let [offset (:height wrapper-size)]
+ (dom/animate! current-viewport
+ [#js {:bottom (str "-" offset "px")}
+ #js {:bottom "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (dom/animate! orig-viewport
+ [#js {:bottom "0"}
+ #js {:bottom (str offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}))
+
+ :down
+ (let [offset (:height wrapper-size)]
+ (dom/animate! current-viewport
+ [#js {:top (str "-" offset "px")}
+ #js {:top "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+ (dom/animate! orig-viewport
+ [#js {:top "0"}
+ #js {:top (str offset "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))})))))
+
+(defn animate-open-overlay
+ [animation overlay-viewport
+ wrapper-size overlay-size overlay-position]
+ (case (:animation-type animation)
+
+ :dissolve
+ (dom/animate! overlay-viewport
+ [#js {:opacity "0"}
+ #js {:opacity "100"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+
+ :slide
+ (case (:direction animation) ;; way and offset-effect are ignored
+
+ :right
+ (dom/animate! overlay-viewport
+ [#js {:left (str "-" (:width overlay-size) "px")}
+ #js {:left (str (:x overlay-position) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+
+ :left
+ (dom/animate! overlay-viewport
+ [#js {:left (str (:width wrapper-size) "px")}
+ #js {:left (str (:x overlay-position) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+
+ :up
+ (dom/animate! overlay-viewport
+ [#js {:top (str (:height wrapper-size) "px")}
+ #js {:top (str (:y overlay-position) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)))
+
+ :down
+ (dom/animate! overlay-viewport
+ [#js {:top (str "-" (:height overlay-size) "px")}
+ #js {:top (str (:y overlay-position) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation))))))
+
+(defn animate-close-overlay
+ [animation overlay-viewport
+ wrapper-size overlay-size overlay-position overlay-id]
+ (case (:animation-type animation)
+
+ :dissolve
+ (dom/animate! overlay-viewport
+ [#js {:opacity "100"}
+ #js {:opacity "0"}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)
+ (dv/close-overlay overlay-id)))
+
+ :slide
+ (case (:direction animation) ;; way and offset-effect are ignored
+
+ :right
+ (dom/animate! overlay-viewport
+ [#js {:left (str (:x overlay-position) "px")}
+ #js {:left (str (:width wrapper-size) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)
+ (dv/close-overlay overlay-id)))
+
+ :left
+ (dom/animate! overlay-viewport
+ [#js {:left (str (:x overlay-position) "px")}
+ #js {:left (str "-" (:width overlay-size) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)
+ (dv/close-overlay overlay-id)))
+
+ :up
+ (dom/animate! overlay-viewport
+ [#js {:top (str (:y overlay-position) "px")}
+ #js {:top (str "-" (:height overlay-size) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)
+ (dv/close-overlay overlay-id)))
+
+ :down
+ (dom/animate! overlay-viewport
+ [#js {:top (str (:y overlay-position) "px")}
+ #js {:top (str (:height wrapper-size) "px")}]
+ #js {:duration (:duration animation)
+ :easing (name (:easing animation))}
+ #(st/emit! (dv/complete-animation)
+ (dv/close-overlay overlay-id))))))
+
diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs
index 175e8b5ef..38c628c4f 100644
--- a/frontend/src/app/main/ui/viewer/shapes.cljs
+++ b/frontend/src/app/main/ui/viewer/shapes.cljs
@@ -38,7 +38,7 @@
(def viewer-interactions-show?
(l/derived :interactions-show? refs/viewer-local))
-(defn activate-interaction
+(defn- activate-interaction
[interaction shape base-frame frame-offset objects]
(case (:action-type interaction)
:navigate
@@ -48,7 +48,7 @@
(dom/get-scroll-pos viewer-section)
0)]
(st/emit! (dv/set-nav-scroll scroll)
- (dv/go-to-frame frame-id))))
+ (dv/go-to-frame frame-id (:animation interaction)))))
:open-overlay
(let [dest-frame-id (:destination interaction)
@@ -64,7 +64,8 @@
(st/emit! (dv/open-overlay dest-frame-id
position
close-click-outside
- background-overlay))))
+ background-overlay
+ (:animation interaction)))))
:toggle-overlay
(let [frame-id (:destination interaction)
@@ -75,14 +76,15 @@
(st/emit! (dv/toggle-overlay frame-id
position
close-click-outside
- background-overlay))))
+ background-overlay
+ (:animation interaction)))))
:close-overlay
(let [frame-id (or (:destination interaction)
(if (= (:type shape) :frame)
(:id shape)
(:frame-id shape)))]
- (st/emit! (dv/close-overlay frame-id)))
+ (st/emit! (dv/close-overlay frame-id (:animation interaction))))
:prev-screen
(st/emit! (rt/nav-back-local))
@@ -93,7 +95,7 @@
nil))
;; Perform the opposite action of an interaction, if possible
-(defn deactivate-interaction
+(defn- deactivate-interaction
[interaction shape base-frame frame-offset objects]
(case (:action-type interaction)
:open-overlay
@@ -112,7 +114,8 @@
(st/emit! (dv/toggle-overlay frame-id
position
close-click-outside
- background-overlay))))
+ background-overlay
+ (:animation interaction)))))
:close-overlay
(let [dest-frame-id (:destination interaction)
@@ -128,10 +131,11 @@
(st/emit! (dv/open-overlay dest-frame-id
position
close-click-outside
- background-overlay))))
+ background-overlay
+ (:animation interaction)))))
nil))
-(defn on-mouse-down
+(defn- on-mouse-down
[event shape base-frame frame-offset objects]
(let [interactions (->> (:interactions shape)
(filter #(or (= (:event-type %) :click)
@@ -141,7 +145,7 @@
(doseq [interaction interactions]
(activate-interaction interaction shape base-frame frame-offset objects)))))
-(defn on-mouse-up
+(defn- on-mouse-up
[event shape base-frame frame-offset objects]
(let [interactions (->> (:interactions shape)
(filter #(= (:event-type %) :mouse-press)))]
@@ -150,7 +154,7 @@
(doseq [interaction interactions]
(deactivate-interaction interaction shape base-frame frame-offset objects)))))
-(defn on-mouse-enter
+(defn- on-mouse-enter
[event shape base-frame frame-offset objects]
(let [interactions (->> (:interactions shape)
(filter #(or (= (:event-type %) :mouse-enter)
@@ -160,7 +164,7 @@
(doseq [interaction interactions]
(activate-interaction interaction shape base-frame frame-offset objects)))))
-(defn on-mouse-leave
+(defn- on-mouse-leave
[event shape base-frame frame-offset objects]
(let [interactions (->> (:interactions shape)
(filter #(= (:event-type %) :mouse-leave)))
@@ -173,7 +177,7 @@
(doseq [interaction interactions-inv]
(deactivate-interaction interaction shape base-frame frame-offset objects)))))
-(defn on-load
+(defn- on-load
[shape base-frame frame-offset objects]
(let [interactions (->> (:interactions shape)
(filter #(= (:event-type %) :after-delay)))]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
index 4eb073320..29988793e 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
@@ -73,6 +73,23 @@
:bottom-right (tr "workspace.options.interaction-pos-bottom-right")
:bottom-center (tr "workspace.options.interaction-pos-bottom-center")})
+(defn- animation-type-names
+ [interaction]
+ (cond->
+ {:dissolve (tr "workspace.options.interaction-animation-dissolve")
+ :slide (tr "workspace.options.interaction-animation-slide")}
+
+ (cti/allow-push? (:action-type interaction))
+ (assoc :push (tr "workspace.options.interaction-animation-push"))))
+
+(defn- easing-names
+ []
+ {:linear (tr "workspace.options.interaction-easing-linear")
+ :ease (tr "workspace.options.interaction-easing-ease")
+ :ease-in (tr "workspace.options.interaction-easing-ease-in")
+ :ease-out (tr "workspace.options.interaction-easing-ease-out")
+ :ease-in-out (tr "workspace.options.interaction-easing-ease-in-out")})
+
(def flow-for-rename-ref
(l/derived (l/in [:workspace-local :flow-for-rename]) st/state))
@@ -170,10 +187,13 @@
close-click-outside? (:close-click-outside interaction false)
background-overlay? (:background-overlay interaction false)
preserve-scroll? (:preserve-scroll interaction false)
+ way (-> interaction :animation :way)
+ direction (-> interaction :animation :direction)
extended-open? (mf/use-state false)
ext-delay-ref (mf/use-ref nil)
+ ext-duration-ref (mf/use-ref nil)
select-text
(fn [ref] (fn [_] (dom/select-text! (mf/ref-val ref))))
@@ -237,7 +257,36 @@
change-background-overlay
(fn [event]
(let [value (-> event dom/get-target dom/checked?)]
- (update-interaction index #(cti/set-background-overlay % value))))]
+ (update-interaction index #(cti/set-background-overlay % value))))
+
+ change-animation-type
+ (fn [event]
+ (let [value (-> event dom/get-target dom/get-value d/read-string)]
+ (update-interaction index #(cti/set-animation-type % value))))
+
+ change-duration
+ (fn [value]
+ (update-interaction index #(cti/set-duration % value)))
+
+ change-easing
+ (fn [event]
+ (let [value (-> event dom/get-target dom/get-value d/read-string)]
+ (update-interaction index #(cti/set-easing % value))))
+
+ change-way
+ (fn [event]
+ (let [value (-> event dom/get-target dom/get-value d/read-string)]
+ (update-interaction index #(cti/set-way % value))))
+
+ change-direction
+ (fn [value]
+ (update-interaction index #(cti/set-direction % value)))
+
+ change-offset-effect
+ (fn [event]
+ (let [value (-> event dom/get-target dom/checked?)]
+ (update-interaction index #(cti/set-offset-effect % value))))
+ ]
[:*
[:div.element-set-options-group {:class (dom/classnames
@@ -382,7 +431,97 @@
:checked background-overlay?
:on-change change-background-overlay}]
[:label {:for (str "background-" index)}
- (tr "workspace.options.interaction-background")]]]])])]]))
+ (tr "workspace.options.interaction-background")]]]])
+
+ ; Animation select
+ [:div.interactions-element.separator
+ [:span.element-set-subtitle.wide (tr "workspace.options.interaction-animation")]
+ [:select.input-select
+ {:value (str (-> interaction :animation :animation-type))
+ :on-change change-animation-type}
+ [:option {:value ""} (tr "workspace.options.interaction-animation-none")]
+ (for [[value name] (animation-type-names interaction)]
+ [:option {:value (str value)} name])]]
+
+ ; Direction
+ (when (cti/has-way? interaction)
+ [:div.interactions-element.interactions-way-buttons
+ [:div.input-radio
+ [:input {:type "radio"
+ :id "way-in"
+ :checked (= :in way)
+ :name "animation-way"
+ :value ":in"
+ :on-change change-way}]
+ [:label {:for "way-in"} (tr "workspace.options.interaction-in")]]
+ [:div.input-radio
+ [:input {:type "radio"
+ :id "way-out"
+ :checked (= :out way)
+ :name "animation-way"
+ :value ":out"
+ :on-change change-way}]
+ [:label {:for "way-out"} (tr "workspace.options.interaction-out")]]])
+
+ ; Direction
+ (when (cti/has-direction? interaction)
+ [:div.interactions-element.interactions-direction-buttons
+ [:div.element-set-actions-button
+ {:class (dom/classnames :active (= direction :right))
+ :on-click #(change-direction :right)}
+ i/animate-right]
+ [:div.element-set-actions-button
+ {:class (dom/classnames :active (= direction :down))
+ :on-click #(change-direction :down)}
+ i/animate-down]
+ [:div.element-set-actions-button
+ {:class (dom/classnames :active (= direction :left))
+ :on-click #(change-direction :left)}
+ i/animate-left]
+ [:div.element-set-actions-button
+ {:class (dom/classnames :active (= direction :up))
+ :on-click #(change-direction :up)}
+ i/animate-up]])
+
+ ; Duration
+ (when (cti/has-duration? interaction)
+ [:div.interactions-element
+ [:span.element-set-subtitle.wide (tr "workspace.options.interaction-duration")]
+ [:div.input-element {:title (tr "workspace.options.interaction-ms")}
+ [:> numeric-input {:ref ext-duration-ref
+ :on-click (select-text ext-duration-ref)
+ :on-change change-duration
+ :value (-> interaction :animation :duration)
+ :title (tr "workspace.options.interaction-ms")}]
+ [:span.after (tr "workspace.options.interaction-ms")]]])
+
+ ; Easing
+ (when (cti/has-easing? interaction)
+ [:div.interactions-element
+ [:span.element-set-subtitle.wide (tr "workspace.options.interaction-easing")]
+ [:select.input-select
+ {:value (str (-> interaction :animation :easing))
+ :on-change change-easing}
+ (for [[value name] (easing-names)]
+ [:option {:value (str value)} name])]
+ [:div.interactions-easing-icon
+ (case (-> interaction :animation :easing)
+ :linear i/easing-linear
+ :ease i/easing-ease
+ :ease-in i/easing-ease-in
+ :ease-out i/easing-ease-out
+ :ease-in-out i/easing-ease-in-out)]])
+
+ ; Offset effect
+ (when (cti/has-offset-effect? interaction)
+ [:div.interactions-element
+ [:div.input-checkbox
+ [:input {:type "checkbox"
+ :id (str "offset-effect-" index)
+ :checked (-> interaction :animation :offset-effect)
+ :on-change change-offset-effect}]
+ [:label {:for (str "offset-effect-" index)}
+ (tr "workspace.options.interaction-offset-effect")]]])])]]))
(mf/defc interactions-menu
[{:keys [shape] :as props}]
diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs
index dc09ea5bf..cf9650acc 100644
--- a/frontend/src/app/util/dom.cljs
+++ b/frontend/src/app/util/dom.cljs
@@ -425,3 +425,10 @@
[]
(.back (.-history js/window)))
+(defn animate!
+ ([item keyframes duration] (animate! item keyframes duration nil))
+ ([item keyframes duration onfinish]
+ (let [animation (.animate item keyframes duration)]
+ (when onfinish
+ (set! (.-onfinish animation) onfinish)))))
+
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 4963d624f..07d5b03d7 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -2390,6 +2390,26 @@ msgstr "Action"
msgid "workspace.options.interaction-after-delay"
msgstr "After delay"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation"
+msgstr "Animation"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-none"
+msgstr "None"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-dissolve"
+msgstr "Dissolve"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-slide"
+msgstr "Slide"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-push"
+msgstr "Push"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-background"
msgstr "Add background overlay"
@@ -2414,6 +2434,42 @@ msgstr "Delay"
msgid "workspace.options.interaction-destination"
msgstr "Destination"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-duration"
+msgstr "Duration"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing"
+msgstr "Easing"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-linear"
+msgstr "Linear"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease"
+msgstr "Ease"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in"
+msgstr "Ease in"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-out"
+msgstr "Ease out"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in-out"
+msgstr "Ease in out"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-in"
+msgstr "In"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-offset-effect"
+msgstr "Offset effect"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-mouse-enter"
msgstr "Mouse enter"
@@ -2454,6 +2510,10 @@ msgstr "Open overlay: %s"
msgid "workspace.options.interaction-open-url"
msgstr "Open url"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-out"
+msgstr "Out"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-pos-bottom-center"
msgstr "Bottom center"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index bc0028286..763d5c62a 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -2391,6 +2391,22 @@ msgstr "Acción"
msgid "workspace.options.interaction-after-delay"
msgstr "Tiempo"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation"
+msgstr "Animación"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-dissolve"
+msgstr "Disolver"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-slide"
+msgstr "Deslizar"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-push"
+msgstr "Empujar"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-background"
msgstr "Añadir sombreado de fondo"
@@ -2415,6 +2431,42 @@ msgstr "Tiempo"
msgid "workspace.options.interaction-destination"
msgstr "Destino"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-duration"
+msgstr "Duración"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing"
+msgstr "Easing"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-linear"
+msgstr "Linear"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease"
+msgstr "Ease"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in"
+msgstr "Ease in"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-out"
+msgstr "Ease out"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in-out"
+msgstr "Ease in out"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-in"
+msgstr "Dentro"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-offset-effect"
+msgstr "Offset effect"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-mouse-enter"
msgstr "Pasar encima"
@@ -2455,6 +2507,10 @@ msgstr "Superposición: %s"
msgid "workspace.options.interaction-open-url"
msgstr "Abrir url"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-out"
+msgstr "Fuera"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-pos-bottom-center"
msgstr "Abajo centro"
@@ -3248,4 +3304,4 @@ msgid "workspace.updates.update"
msgstr "Actualizar"
msgid "workspace.viewport.click-to-close-path"
-msgstr "Pulsar para cerrar la ruta"
\ No newline at end of file
+msgstr "Pulsar para cerrar la ruta"