2021-09-07 17:36:50 +03:00
|
|
|
package rui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2021-10-04 17:58:17 +03:00
|
|
|
"math"
|
2021-09-07 17:36:50 +03:00
|
|
|
"strconv"
|
2021-10-04 17:58:17 +03:00
|
|
|
"strings"
|
2021-09-07 17:36:50 +03:00
|
|
|
)
|
|
|
|
|
2024-09-12 14:05:11 +03:00
|
|
|
// Constants which related to view's animation
|
2021-10-04 17:58:17 +03:00
|
|
|
const (
|
|
|
|
// AnimationTag is the constant for the "animation" property tag.
|
|
|
|
// The "animation" property sets and starts animations.
|
|
|
|
// Valid types of value are []Animation and Animation
|
|
|
|
AnimationTag = "animation"
|
|
|
|
|
|
|
|
// AnimationPause is the constant for the "animation-pause" property tag.
|
|
|
|
// The "animation-pause" property sets whether an animation is running or paused.
|
|
|
|
AnimationPaused = "animation-paused"
|
|
|
|
|
|
|
|
// TransitionTag is the constant for the "transition" property tag.
|
|
|
|
// The "transition" property sets transition animation of view properties.
|
|
|
|
// Valid type of "transition" property value is Params. Valid type of Params value is Animation.
|
|
|
|
Transition = "transition"
|
|
|
|
|
|
|
|
// PropertyTag is the constant for the "property" animation property tag.
|
|
|
|
// The "property" property describes a scenario for changing a View property.
|
|
|
|
// Valid types of value are []AnimatedProperty and AnimatedProperty
|
|
|
|
PropertyTag = "property"
|
|
|
|
|
|
|
|
// Duration is the constant for the "duration" animation property tag.
|
|
|
|
// The "duration" float property sets the length of time in seconds that an animation takes to complete one cycle.
|
|
|
|
Duration = "duration"
|
|
|
|
|
|
|
|
// Delay is the constant for the "delay" animation property tag.
|
|
|
|
// The "delay" float property specifies the amount of time in seconds to wait from applying
|
|
|
|
// the animation to an element before beginning to perform the animation. The animation can start later,
|
|
|
|
// immediately from its beginning, or immediately and partway through the animation.
|
|
|
|
Delay = "delay"
|
|
|
|
|
|
|
|
// TimingFunction is the constant for the "timing-function" animation property tag.
|
|
|
|
// The "timing-function" property sets how an animation progresses through the duration of each cycle.
|
|
|
|
TimingFunction = "timing-function"
|
|
|
|
|
|
|
|
// IterationCount is the constant for the "iteration-count" animation property tag.
|
|
|
|
// The "iteration-count" int property sets the number of times an animation sequence
|
|
|
|
// should be played before stopping.
|
|
|
|
IterationCount = "iteration-count"
|
|
|
|
|
|
|
|
// AnimationDirection is the constant for the "animation-direction" animation property tag.
|
|
|
|
//The "animation-direction" property sets whether an animation should play forward, backward,
|
|
|
|
// or alternate back and forth between playing the sequence forward and backward.
|
|
|
|
AnimationDirection = "animation-direction"
|
|
|
|
|
|
|
|
// NormalAnimation is value of the "animation-direction" property.
|
|
|
|
// The animation plays forwards each cycle. In other words, each time the animation cycles,
|
|
|
|
// the animation will reset to the beginning state and start over again. This is the default value.
|
|
|
|
NormalAnimation = 0
|
2022-08-10 15:36:38 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
// ReverseAnimation is value of the "animation-direction" property.
|
|
|
|
// The animation plays backwards each cycle. In other words, each time the animation cycles,
|
|
|
|
// the animation will reset to the end state and start over again. Animation steps are performed
|
|
|
|
// backwards, and timing functions are also reversed.
|
|
|
|
// For example, an "ease-in" timing function becomes "ease-out".
|
|
|
|
ReverseAnimation = 1
|
2022-08-10 15:36:38 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
// AlternateAnimation is value of the "animation-direction" property.
|
|
|
|
// The animation reverses direction each cycle, with the first iteration being played forwards.
|
|
|
|
// The count to determine if a cycle is even or odd starts at one.
|
|
|
|
AlternateAnimation = 2
|
2022-08-10 15:36:38 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
// AlternateReverseAnimation is value of the "animation-direction" property.
|
|
|
|
// The animation reverses direction each cycle, with the first iteration being played backwards.
|
|
|
|
// The count to determine if a cycle is even or odd starts at one.
|
|
|
|
AlternateReverseAnimation = 3
|
|
|
|
|
|
|
|
// EaseTiming - a timing function which increases in velocity towards the middle of the transition, slowing back down at the end
|
|
|
|
EaseTiming = "ease"
|
|
|
|
// EaseInTiming - a timing function which starts off slowly, with the transition speed increasing until complete
|
|
|
|
EaseInTiming = "ease-in"
|
|
|
|
// EaseOutTiming - a timing function which starts transitioning quickly, slowing down the transition continues.
|
|
|
|
EaseOutTiming = "ease-out"
|
|
|
|
// EaseInOutTiming - a timing function which starts transitioning slowly, speeds up, and then slows down again.
|
|
|
|
EaseInOutTiming = "ease-in-out"
|
|
|
|
// LinearTiming - a timing function at an even speed
|
|
|
|
LinearTiming = "linear"
|
|
|
|
)
|
|
|
|
|
2022-11-23 15:10:29 +03:00
|
|
|
// StepsTiming return a timing function along stepCount stops along the transition, displaying each stop for equal lengths of time
|
2021-10-04 17:58:17 +03:00
|
|
|
func StepsTiming(stepCount int) string {
|
|
|
|
return "steps(" + strconv.Itoa(stepCount) + ")"
|
|
|
|
}
|
|
|
|
|
|
|
|
// CubicBezierTiming return a cubic-Bezier curve timing function. x1 and x2 must be in the range [0, 1].
|
|
|
|
func CubicBezierTiming(x1, y1, x2, y2 float64) string {
|
|
|
|
if x1 < 0 {
|
|
|
|
x1 = 0
|
|
|
|
} else if x1 > 1 {
|
|
|
|
x1 = 1
|
|
|
|
}
|
|
|
|
if x2 < 0 {
|
|
|
|
x2 = 0
|
|
|
|
} else if x2 > 1 {
|
|
|
|
x2 = 1
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("cubic-bezier(%g, %g, %g, %g)", x1, y1, x2, y2)
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
// AnimatedProperty describes the change script of one property
|
|
|
|
type AnimatedProperty struct {
|
|
|
|
// Tag is the name of the property
|
|
|
|
Tag string
|
|
|
|
// From is the initial value of the property
|
2022-07-26 18:36:00 +03:00
|
|
|
From any
|
2021-10-04 17:58:17 +03:00
|
|
|
// To is the final value of the property
|
2022-07-26 18:36:00 +03:00
|
|
|
To any
|
2021-10-04 17:58:17 +03:00
|
|
|
// KeyFrames is intermediate property values
|
2022-07-26 18:36:00 +03:00
|
|
|
KeyFrames map[int]any
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
type animationData struct {
|
|
|
|
propertyList
|
|
|
|
keyFramesName string
|
2024-07-01 19:17:03 +03:00
|
|
|
usageCounter int
|
2024-07-05 16:41:07 +03:00
|
|
|
view View
|
|
|
|
listener func(view View, animation Animation, event string)
|
|
|
|
oldListeners map[string][]func(View, string)
|
|
|
|
oldAnimation []Animation
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Animation interface is used to set animation parameters. Used properties:
|
|
|
|
// "property", "id", "duration", "delay", "timing-function", "iteration-count", and "animation-direction"
|
|
|
|
type Animation interface {
|
|
|
|
Properties
|
2021-09-07 17:36:50 +03:00
|
|
|
fmt.Stringer
|
2024-07-05 16:41:07 +03:00
|
|
|
|
|
|
|
// Start starts the animation for the view specified by the first argument.
|
|
|
|
// The second argument specifies the animation event listener (can be nil)
|
|
|
|
Start(view View, listener func(view View, animation Animation, event string)) bool
|
|
|
|
// Stop stops the animation
|
|
|
|
Stop()
|
|
|
|
// Pause pauses the animation
|
|
|
|
Pause()
|
|
|
|
// Resume resumes an animation that was stopped using the Pause method
|
|
|
|
Resume()
|
|
|
|
|
2022-05-22 12:54:02 +03:00
|
|
|
writeTransitionString(tag string, buffer *strings.Builder)
|
2021-10-04 17:58:17 +03:00
|
|
|
animationCSS(session Session) string
|
|
|
|
transitionCSS(buffer *strings.Builder, session Session)
|
2022-11-23 15:10:29 +03:00
|
|
|
hasAnimatedProperty() bool
|
2021-10-04 17:58:17 +03:00
|
|
|
animationName() string
|
2024-07-01 19:17:03 +03:00
|
|
|
used()
|
|
|
|
unused(session Session)
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func parseAnimation(obj DataObject) Animation {
|
|
|
|
animation := new(animationData)
|
|
|
|
animation.init()
|
|
|
|
|
|
|
|
for i := 0; i < obj.PropertyCount(); i++ {
|
|
|
|
if node := obj.Property(i); node != nil {
|
|
|
|
if node.Type() == TextNode {
|
|
|
|
animation.Set(node.Tag(), node.Text())
|
|
|
|
} else {
|
|
|
|
animation.Set(node.Tag(), node)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return animation
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2024-09-12 14:05:11 +03:00
|
|
|
// NewAnimation creates a new animation object and return its interface
|
2021-10-04 17:58:17 +03:00
|
|
|
func NewAnimation(params Params) Animation {
|
|
|
|
animation := new(animationData)
|
|
|
|
animation.init()
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
for tag, value := range params {
|
|
|
|
animation.Set(tag, value)
|
|
|
|
}
|
|
|
|
return animation
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2024-07-05 16:41:07 +03:00
|
|
|
func (animation *animationData) animatedProperties() []AnimatedProperty {
|
|
|
|
value := animation.getRaw(PropertyTag)
|
|
|
|
if value == nil {
|
2021-10-04 17:58:17 +03:00
|
|
|
ErrorLog("There are no animated properties.")
|
2024-07-05 16:41:07 +03:00
|
|
|
return nil
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2024-07-05 16:41:07 +03:00
|
|
|
props, ok := value.([]AnimatedProperty)
|
|
|
|
if !ok {
|
2021-10-04 17:58:17 +03:00
|
|
|
ErrorLog("Invalid animated properties.")
|
2024-07-05 16:41:07 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(props) == 0 {
|
|
|
|
ErrorLog("There are no animated properties.")
|
|
|
|
return nil
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2024-07-05 16:41:07 +03:00
|
|
|
return props
|
|
|
|
}
|
|
|
|
|
|
|
|
func (animation *animationData) hasAnimatedProperty() bool {
|
|
|
|
return animation.animatedProperties() != nil
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func (animation *animationData) animationName() string {
|
|
|
|
return animation.keyFramesName
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2024-07-01 19:17:03 +03:00
|
|
|
func (animation *animationData) used() {
|
|
|
|
animation.usageCounter++
|
|
|
|
}
|
|
|
|
|
|
|
|
func (animation *animationData) unused(session Session) {
|
|
|
|
animation.usageCounter--
|
|
|
|
if animation.usageCounter <= 0 && animation.keyFramesName != "" {
|
|
|
|
session.removeAnimation(animation.keyFramesName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-22 12:54:02 +03:00
|
|
|
func (animation *animationData) normalizeTag(tag string) string {
|
|
|
|
tag = strings.ToLower(tag)
|
|
|
|
if tag == Direction {
|
|
|
|
return AnimationDirection
|
|
|
|
}
|
|
|
|
return tag
|
|
|
|
}
|
|
|
|
|
2022-07-26 18:36:00 +03:00
|
|
|
func (animation *animationData) Set(tag string, value any) bool {
|
2021-10-04 17:58:17 +03:00
|
|
|
if value == nil {
|
|
|
|
animation.Remove(tag)
|
|
|
|
return true
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2022-05-22 12:54:02 +03:00
|
|
|
switch tag = animation.normalizeTag(tag); tag {
|
2021-10-04 17:58:17 +03:00
|
|
|
case ID:
|
|
|
|
if text, ok := value.(string); ok {
|
|
|
|
text = strings.Trim(text, " \t\n\r")
|
|
|
|
if text == "" {
|
|
|
|
delete(animation.properties, tag)
|
|
|
|
} else {
|
|
|
|
animation.properties[tag] = text
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
notCompatibleType(tag, value)
|
|
|
|
return false
|
|
|
|
|
|
|
|
case PropertyTag:
|
|
|
|
switch value := value.(type) {
|
|
|
|
case AnimatedProperty:
|
|
|
|
if value.From == nil && value.KeyFrames != nil {
|
|
|
|
if val, ok := value.KeyFrames[0]; ok {
|
|
|
|
value.From = val
|
|
|
|
delete(value.KeyFrames, 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if value.To == nil && value.KeyFrames != nil {
|
|
|
|
if val, ok := value.KeyFrames[100]; ok {
|
|
|
|
value.To = val
|
|
|
|
delete(value.KeyFrames, 100)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if value.From == nil {
|
|
|
|
ErrorLog("AnimatedProperty.From is nil")
|
|
|
|
} else if value.To == nil {
|
|
|
|
ErrorLog("AnimatedProperty.To is nil")
|
|
|
|
} else {
|
|
|
|
animation.properties[tag] = []AnimatedProperty{value}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
case []AnimatedProperty:
|
|
|
|
props := []AnimatedProperty{}
|
|
|
|
for _, val := range value {
|
|
|
|
if val.From == nil && val.KeyFrames != nil {
|
|
|
|
if v, ok := val.KeyFrames[0]; ok {
|
|
|
|
val.From = v
|
|
|
|
delete(val.KeyFrames, 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if val.To == nil && val.KeyFrames != nil {
|
|
|
|
if v, ok := val.KeyFrames[100]; ok {
|
|
|
|
val.To = v
|
|
|
|
delete(val.KeyFrames, 100)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if val.From == nil {
|
|
|
|
ErrorLog("AnimatedProperty.From is nil")
|
|
|
|
} else if val.To == nil {
|
|
|
|
ErrorLog("AnimatedProperty.To is nil")
|
|
|
|
} else {
|
|
|
|
props = append(props, val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(props) > 0 {
|
|
|
|
animation.properties[tag] = props
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
ErrorLog("[]AnimatedProperty is empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
case DataNode:
|
|
|
|
parseObject := func(obj DataObject) (AnimatedProperty, bool) {
|
|
|
|
result := AnimatedProperty{}
|
|
|
|
for i := 0; i < obj.PropertyCount(); i++ {
|
|
|
|
if node := obj.Property(i); node.Type() == TextNode {
|
|
|
|
propTag := strings.ToLower(node.Tag())
|
|
|
|
switch propTag {
|
|
|
|
case "from", "0", "0%":
|
|
|
|
result.From = node.Text()
|
|
|
|
|
|
|
|
case "to", "100", "100%":
|
|
|
|
result.To = node.Text()
|
|
|
|
|
|
|
|
default:
|
|
|
|
tagLen := len(propTag)
|
|
|
|
if tagLen > 0 && propTag[tagLen-1] == '%' {
|
|
|
|
propTag = propTag[:tagLen-1]
|
|
|
|
}
|
|
|
|
n, err := strconv.Atoi(propTag)
|
|
|
|
if err != nil {
|
|
|
|
ErrorLog(err.Error())
|
|
|
|
} else if n < 0 || n > 100 {
|
|
|
|
ErrorLogF(`key-frame "%d" is out of range`, n)
|
|
|
|
} else {
|
|
|
|
if result.KeyFrames == nil {
|
2022-07-26 18:36:00 +03:00
|
|
|
result.KeyFrames = map[int]any{n: node.Text()}
|
2021-10-04 17:58:17 +03:00
|
|
|
} else {
|
|
|
|
result.KeyFrames[n] = node.Text()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if result.From != nil && result.To != nil {
|
|
|
|
return result, true
|
|
|
|
}
|
|
|
|
return result, false
|
|
|
|
}
|
|
|
|
|
|
|
|
switch value.Type() {
|
|
|
|
case ObjectNode:
|
|
|
|
if prop, ok := parseObject(value.Object()); ok {
|
|
|
|
animation.properties[tag] = []AnimatedProperty{prop}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
case ArrayNode:
|
|
|
|
props := []AnimatedProperty{}
|
|
|
|
for _, val := range value.ArrayElements() {
|
|
|
|
if val.IsObject() {
|
|
|
|
if prop, ok := parseObject(val.Object()); ok {
|
|
|
|
props = append(props, prop)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
notCompatibleType(tag, val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(props) > 0 {
|
|
|
|
animation.properties[tag] = props
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
notCompatibleType(tag, value)
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
notCompatibleType(tag, value)
|
|
|
|
}
|
|
|
|
|
|
|
|
case Duration:
|
|
|
|
return animation.setFloatProperty(tag, value, 0, math.MaxFloat64)
|
|
|
|
|
|
|
|
case Delay:
|
|
|
|
return animation.setFloatProperty(tag, value, -math.MaxFloat64, math.MaxFloat64)
|
|
|
|
|
|
|
|
case TimingFunction:
|
|
|
|
if text, ok := value.(string); ok {
|
|
|
|
animation.properties[tag] = text
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
case IterationCount:
|
|
|
|
return animation.setIntProperty(tag, value)
|
|
|
|
|
2022-05-22 12:54:02 +03:00
|
|
|
case AnimationDirection:
|
2021-10-04 17:58:17 +03:00
|
|
|
return animation.setEnumProperty(AnimationDirection, value, enumProperties[AnimationDirection].values)
|
|
|
|
|
|
|
|
default:
|
|
|
|
ErrorLogF(`The "%s" property is not supported by Animation`, tag)
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (animation *animationData) Remove(tag string) {
|
2022-05-22 12:54:02 +03:00
|
|
|
delete(animation.properties, animation.normalizeTag(tag))
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2022-07-26 18:36:00 +03:00
|
|
|
func (animation *animationData) Get(tag string) any {
|
2022-05-22 12:54:02 +03:00
|
|
|
return animation.getRaw(animation.normalizeTag(tag))
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func (animation *animationData) String() string {
|
2022-05-22 12:54:02 +03:00
|
|
|
buffer := allocStringBuilder()
|
|
|
|
defer freeStringBuilder(buffer)
|
|
|
|
|
|
|
|
buffer.WriteString("animation {")
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2024-07-06 13:04:12 +03:00
|
|
|
for _, tag := range animation.AllTags() {
|
|
|
|
if tag != PropertyTag {
|
|
|
|
if value, ok := animation.properties[tag]; ok && value != nil {
|
|
|
|
buffer.WriteString("\n\t")
|
|
|
|
buffer.WriteString(tag)
|
|
|
|
buffer.WriteString(" = ")
|
|
|
|
writePropertyValue(buffer, tag, value, "\t")
|
|
|
|
buffer.WriteRune(',')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
writeProperty := func(prop AnimatedProperty, indent string) {
|
|
|
|
buffer.WriteString(prop.Tag)
|
|
|
|
buffer.WriteString("{\n")
|
|
|
|
buffer.WriteString(indent)
|
|
|
|
buffer.WriteString("from = ")
|
|
|
|
writePropertyValue(buffer, "from", prop.From, indent)
|
|
|
|
buffer.WriteString(",\n")
|
|
|
|
buffer.WriteString(indent)
|
|
|
|
buffer.WriteString("to = ")
|
|
|
|
writePropertyValue(buffer, "to", prop.To, indent)
|
|
|
|
for key, value := range prop.KeyFrames {
|
|
|
|
buffer.WriteString(",\n")
|
|
|
|
buffer.WriteString(indent)
|
|
|
|
tag := strconv.Itoa(key) + "%"
|
|
|
|
buffer.WriteString(tag)
|
|
|
|
buffer.WriteString(" = ")
|
|
|
|
writePropertyValue(buffer, tag, value, indent)
|
|
|
|
}
|
|
|
|
buffer.WriteString("\n")
|
|
|
|
buffer.WriteString(indent[1:])
|
|
|
|
buffer.WriteString("}")
|
|
|
|
}
|
|
|
|
|
|
|
|
if props := animation.animatedProperties(); len(props) > 0 {
|
|
|
|
|
|
|
|
buffer.WriteString("\n\t")
|
|
|
|
buffer.WriteString(PropertyTag)
|
|
|
|
buffer.WriteString(" = ")
|
|
|
|
if len(props) > 1 {
|
|
|
|
buffer.WriteString("[\n")
|
|
|
|
for _, prop := range props {
|
|
|
|
buffer.WriteString("\t\t")
|
|
|
|
writeProperty(prop, "\t\t\t")
|
|
|
|
buffer.WriteString(",\n")
|
|
|
|
}
|
|
|
|
buffer.WriteString("\t]")
|
|
|
|
} else {
|
|
|
|
writeProperty(props[0], "\t\t")
|
|
|
|
}
|
|
|
|
}
|
2022-05-22 12:54:02 +03:00
|
|
|
|
2024-07-06 13:04:12 +03:00
|
|
|
buffer.WriteString("\n}")
|
2022-05-22 12:54:02 +03:00
|
|
|
return buffer.String()
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func (animation *animationData) animationCSS(session Session) string {
|
|
|
|
if animation.keyFramesName == "" {
|
2024-07-05 16:41:07 +03:00
|
|
|
if props := animation.animatedProperties(); props != nil {
|
|
|
|
animation.keyFramesName = session.registerAnimation(props)
|
|
|
|
} else {
|
2021-10-04 17:58:17 +03:00
|
|
|
return ""
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
buffer := allocStringBuilder()
|
|
|
|
defer freeStringBuilder(buffer)
|
|
|
|
|
|
|
|
buffer.WriteString(animation.keyFramesName)
|
|
|
|
|
2022-08-18 18:18:36 +03:00
|
|
|
if duration, ok := floatProperty(animation, Duration, session, 1); ok && duration > 0 {
|
2021-10-04 17:58:17 +03:00
|
|
|
buffer.WriteString(fmt.Sprintf(" %gs ", duration))
|
|
|
|
} else {
|
|
|
|
buffer.WriteString(" 1s ")
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer.WriteString(animation.timingFunctionCSS(session))
|
|
|
|
|
2022-08-18 18:18:36 +03:00
|
|
|
if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 {
|
2021-10-04 17:58:17 +03:00
|
|
|
buffer.WriteString(fmt.Sprintf(" %gs", delay))
|
|
|
|
} else {
|
|
|
|
buffer.WriteString(" 0s")
|
|
|
|
}
|
|
|
|
|
|
|
|
if iterationCount, _ := intProperty(animation, IterationCount, session, 0); iterationCount >= 0 {
|
|
|
|
if iterationCount == 0 {
|
|
|
|
iterationCount = 1
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
buffer.WriteString(fmt.Sprintf(" %d ", iterationCount))
|
|
|
|
} else {
|
|
|
|
buffer.WriteString(" infinite ")
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
direction, _ := enumProperty(animation, AnimationDirection, session, 0)
|
|
|
|
values := enumProperties[AnimationDirection].cssValues
|
|
|
|
if direction < 0 || direction >= len(values) {
|
|
|
|
direction = 0
|
|
|
|
}
|
|
|
|
buffer.WriteString(values[direction])
|
|
|
|
|
|
|
|
// TODO "animation-fill-mode"
|
|
|
|
buffer.WriteString(" forwards")
|
|
|
|
|
|
|
|
return buffer.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (animation *animationData) transitionCSS(buffer *strings.Builder, session Session) {
|
|
|
|
|
2022-08-18 18:18:36 +03:00
|
|
|
if duration, ok := floatProperty(animation, Duration, session, 1); ok && duration > 0 {
|
2021-10-04 17:58:17 +03:00
|
|
|
buffer.WriteString(fmt.Sprintf(" %gs ", duration))
|
|
|
|
} else {
|
|
|
|
buffer.WriteString(" 1s ")
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer.WriteString(animation.timingFunctionCSS(session))
|
|
|
|
|
2022-08-18 18:18:36 +03:00
|
|
|
if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 {
|
2021-10-04 17:58:17 +03:00
|
|
|
buffer.WriteString(fmt.Sprintf(" %gs", delay))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-22 12:54:02 +03:00
|
|
|
func (animation *animationData) writeTransitionString(tag string, buffer *strings.Builder) {
|
|
|
|
buffer.WriteString(tag)
|
|
|
|
buffer.WriteString("{")
|
|
|
|
lead := " "
|
|
|
|
|
|
|
|
writeFloatProperty := func(name string) bool {
|
|
|
|
if value := animation.getRaw(name); value != nil {
|
|
|
|
buffer.WriteString(lead)
|
|
|
|
buffer.WriteString(name)
|
|
|
|
buffer.WriteString(" = ")
|
|
|
|
writePropertyValue(buffer, name, value, "")
|
|
|
|
lead = ", "
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if !writeFloatProperty(Duration) {
|
|
|
|
buffer.WriteString(" duration = 1")
|
|
|
|
lead = ", "
|
|
|
|
}
|
|
|
|
|
|
|
|
writeFloatProperty(Delay)
|
|
|
|
|
|
|
|
if value := animation.getRaw(TimingFunction); value != nil {
|
|
|
|
if timingFunction, ok := value.(string); ok && timingFunction != "" {
|
|
|
|
buffer.WriteString(lead)
|
|
|
|
buffer.WriteString(TimingFunction)
|
|
|
|
buffer.WriteString(" = ")
|
|
|
|
if strings.ContainsAny(timingFunction, " ,()") {
|
|
|
|
buffer.WriteRune('"')
|
|
|
|
buffer.WriteString(timingFunction)
|
|
|
|
buffer.WriteRune('"')
|
2022-08-17 16:23:45 +03:00
|
|
|
} else {
|
|
|
|
buffer.WriteString(timingFunction)
|
2022-05-22 12:54:02 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer.WriteString(" }")
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func (animation *animationData) timingFunctionCSS(session Session) string {
|
|
|
|
if timingFunction, ok := stringProperty(animation, TimingFunction, session); ok {
|
2022-08-11 19:18:36 +03:00
|
|
|
if timingFunction, ok = session.resolveConstants(timingFunction); ok && isTimingFunctionValid(timingFunction) {
|
2021-10-04 17:58:17 +03:00
|
|
|
return timingFunction
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
return ("ease")
|
|
|
|
}
|
|
|
|
|
2022-08-11 19:18:36 +03:00
|
|
|
func isTimingFunctionValid(timingFunction string) bool {
|
2021-10-04 17:58:17 +03:00
|
|
|
switch timingFunction {
|
|
|
|
case "", EaseTiming, EaseInTiming, EaseOutTiming, EaseInOutTiming, LinearTiming:
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-08-11 19:18:36 +03:00
|
|
|
size := len(timingFunction)
|
|
|
|
if size > 0 && timingFunction[size-1] == ')' {
|
|
|
|
if index := strings.IndexRune(timingFunction, '('); index > 0 {
|
|
|
|
args := timingFunction[index+1 : size-1]
|
|
|
|
switch timingFunction[:index] {
|
|
|
|
case "steps":
|
|
|
|
if _, err := strconv.Atoi(strings.Trim(args, " \t\n")); err == nil {
|
|
|
|
return true
|
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
|
2022-08-11 19:18:36 +03:00
|
|
|
case "cubic-bezier":
|
|
|
|
if params := strings.Split(args, ","); len(params) == 4 {
|
|
|
|
for _, param := range params {
|
|
|
|
if _, err := strconv.ParseFloat(strings.Trim(param, " \t\n"), 64); err != nil {
|
|
|
|
return false
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
}
|
2022-08-11 19:18:36 +03:00
|
|
|
return true
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-08-11 19:18:36 +03:00
|
|
|
// IsTimingFunctionValid returns "true" if the "timingFunction" argument is the valid timing function.
|
|
|
|
func IsTimingFunctionValid(timingFunction string, session Session) bool {
|
|
|
|
if timingFunc, ok := session.resolveConstants(strings.Trim(timingFunction, " \t\n")); ok {
|
|
|
|
return isTimingFunctionValid(timingFunc)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func (session *sessionData) registerAnimation(props []AnimatedProperty) string {
|
|
|
|
|
|
|
|
session.animationCounter++
|
|
|
|
name := fmt.Sprintf("kf%06d", session.animationCounter)
|
|
|
|
|
|
|
|
var cssBuilder cssStyleBuilder
|
|
|
|
|
2024-07-01 19:17:03 +03:00
|
|
|
cssBuilder.init(0)
|
2021-10-04 17:58:17 +03:00
|
|
|
cssBuilder.startAnimation(name)
|
|
|
|
|
|
|
|
fromParams := Params{}
|
|
|
|
toParams := Params{}
|
|
|
|
frames := []int{}
|
|
|
|
|
|
|
|
for _, prop := range props {
|
|
|
|
fromParams[prop.Tag] = prop.From
|
|
|
|
toParams[prop.Tag] = prop.To
|
|
|
|
if len(prop.KeyFrames) > 0 {
|
|
|
|
for frame := range prop.KeyFrames {
|
|
|
|
needAppend := true
|
|
|
|
for i, n := range frames {
|
|
|
|
if n == frame {
|
|
|
|
needAppend = false
|
|
|
|
break
|
|
|
|
} else if frame < n {
|
|
|
|
needAppend = false
|
|
|
|
frames = append(append(frames[:i], frame), frames[i+1:]...)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if needAppend {
|
|
|
|
frames = append(frames, frame)
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
cssBuilder.startAnimationFrame("from")
|
|
|
|
NewViewStyle(fromParams).cssViewStyle(&cssBuilder, session)
|
|
|
|
cssBuilder.endAnimationFrame()
|
|
|
|
|
|
|
|
if len(frames) > 0 {
|
|
|
|
for _, frame := range frames {
|
|
|
|
params := Params{}
|
|
|
|
for _, prop := range props {
|
|
|
|
if prop.KeyFrames != nil {
|
|
|
|
if value, ok := prop.KeyFrames[frame]; ok {
|
|
|
|
params[prop.Tag] = value
|
|
|
|
}
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
|
|
|
|
if len(params) > 0 {
|
|
|
|
cssBuilder.startAnimationFrame(strconv.Itoa(frame) + "%")
|
|
|
|
NewViewStyle(params).cssViewStyle(&cssBuilder, session)
|
|
|
|
cssBuilder.endAnimationFrame()
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
cssBuilder.startAnimationFrame("to")
|
|
|
|
NewViewStyle(toParams).cssViewStyle(&cssBuilder, session)
|
|
|
|
cssBuilder.endAnimationFrame()
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
cssBuilder.endAnimation()
|
2024-07-01 19:17:03 +03:00
|
|
|
session.addAnimationCSS(cssBuilder.finish())
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
return name
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2022-07-26 18:36:00 +03:00
|
|
|
func (view *viewData) SetAnimated(tag string, value any, animation Animation) bool {
|
2021-10-04 17:58:17 +03:00
|
|
|
if animation == nil {
|
|
|
|
return view.Set(tag, value)
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2022-07-22 13:10:55 +03:00
|
|
|
session := view.Session()
|
|
|
|
htmlID := view.htmlID()
|
|
|
|
session.startUpdateScript(htmlID)
|
|
|
|
|
2022-10-30 17:22:33 +03:00
|
|
|
session.updateProperty(htmlID, "ontransitionend", "transitionEndEvent(this, event)")
|
|
|
|
session.updateProperty(htmlID, "ontransitioncancel", "transitionCancelEvent(this, event)")
|
2021-10-04 17:58:17 +03:00
|
|
|
|
|
|
|
if prevAnimation, ok := view.transitions[tag]; ok {
|
|
|
|
view.singleTransition[tag] = prevAnimation
|
|
|
|
} else {
|
|
|
|
view.singleTransition[tag] = nil
|
|
|
|
}
|
|
|
|
view.transitions[tag] = animation
|
|
|
|
view.updateTransitionCSS()
|
|
|
|
|
2022-07-22 13:10:55 +03:00
|
|
|
session.finishUpdateScript(htmlID)
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
result := view.Set(tag, value)
|
|
|
|
if !result {
|
|
|
|
delete(view.singleTransition, tag)
|
|
|
|
view.updateTransitionCSS()
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func (style *viewStyle) animationCSS(session Session) string {
|
|
|
|
if value := style.getRaw(AnimationTag); value != nil {
|
|
|
|
if animations, ok := value.([]Animation); ok {
|
|
|
|
buffer := allocStringBuilder()
|
|
|
|
defer freeStringBuilder(buffer)
|
|
|
|
|
|
|
|
for _, animation := range animations {
|
|
|
|
if css := animation.animationCSS(session); css != "" {
|
|
|
|
if buffer.Len() > 0 {
|
|
|
|
buffer.WriteString(", ")
|
|
|
|
}
|
|
|
|
buffer.WriteString(css)
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
return buffer.String()
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
|
2021-09-07 17:36:50 +03:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func (style *viewStyle) transitionCSS(session Session) string {
|
|
|
|
buffer := allocStringBuilder()
|
|
|
|
defer freeStringBuilder(buffer)
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2022-08-17 16:58:07 +03:00
|
|
|
convert := map[string]string{
|
|
|
|
CellHeight: "grid-template-rows",
|
|
|
|
CellWidth: "grid-template-columns",
|
|
|
|
Row: "grid-row",
|
|
|
|
Column: "grid-column",
|
|
|
|
Clip: "clip-path",
|
|
|
|
Shadow: "box-shadow",
|
|
|
|
ColumnSeparator: "column-rule",
|
|
|
|
FontName: "font",
|
|
|
|
TextSize: "font-size",
|
|
|
|
TextLineThickness: "text-decoration-thickness",
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
for tag, animation := range style.transitions {
|
|
|
|
if buffer.Len() > 0 {
|
|
|
|
buffer.WriteString(", ")
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
2022-08-17 16:58:07 +03:00
|
|
|
|
|
|
|
if cssTag, ok := convert[tag]; ok {
|
|
|
|
buffer.WriteString(cssTag)
|
|
|
|
} else {
|
|
|
|
buffer.WriteString(tag)
|
|
|
|
}
|
2021-10-04 17:58:17 +03:00
|
|
|
animation.transitionCSS(buffer, session)
|
|
|
|
}
|
|
|
|
return buffer.String()
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
func (view *viewData) updateTransitionCSS() {
|
2022-10-30 17:22:33 +03:00
|
|
|
view.session.updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(view.session))
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2022-08-10 15:36:38 +03:00
|
|
|
func (style *viewStyle) Transition(tag string) Animation {
|
|
|
|
if style.transitions != nil {
|
|
|
|
if anim, ok := style.transitions[tag]; ok {
|
|
|
|
return anim
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (style *viewStyle) Transitions() map[string]Animation {
|
|
|
|
result := map[string]Animation{}
|
|
|
|
for tag, animation := range style.transitions {
|
2021-10-04 17:58:17 +03:00
|
|
|
result[tag] = animation
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2022-08-10 15:36:38 +03:00
|
|
|
func (style *viewStyle) SetTransition(tag string, animation Animation) {
|
|
|
|
if animation == nil {
|
|
|
|
delete(style.transitions, tag)
|
|
|
|
} else {
|
|
|
|
style.transitions[tag] = animation
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (view *viewData) SetTransition(tag string, animation Animation) {
|
|
|
|
view.viewStyle.SetTransition(tag, animation)
|
|
|
|
if view.created {
|
2022-10-30 17:22:33 +03:00
|
|
|
view.session.updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(view.session))
|
2022-08-10 15:36:38 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
// SetAnimated sets the property with name "tag" of the "rootView" subview with "viewID" id by value. Result:
|
2021-11-04 21:13:34 +03:00
|
|
|
// true - success,
|
|
|
|
// false - error (incompatible type or invalid format of a string value, see AppLog).
|
2022-07-26 18:36:00 +03:00
|
|
|
func SetAnimated(rootView View, viewID, tag string, value any, animation Animation) bool {
|
2021-10-04 17:58:17 +03:00
|
|
|
if view := ViewByID(rootView, viewID); view != nil {
|
|
|
|
return view.SetAnimated(tag, value, animation)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsAnimationPaused returns "true" if an animation of the subview is paused, "false" otherwise.
|
2022-08-31 22:17:46 +03:00
|
|
|
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
|
|
|
|
func IsAnimationPaused(view View, subviewID ...string) bool {
|
2022-07-28 12:11:27 +03:00
|
|
|
return boolStyledProperty(view, subviewID, AnimationPaused, false)
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
2021-09-07 17:36:50 +03:00
|
|
|
|
2022-08-10 15:36:38 +03:00
|
|
|
// GetTransitions returns the subview transitions. The result is always non-nil.
|
2022-08-31 22:17:46 +03:00
|
|
|
// If the second argument (subviewID) is not specified or it is "" then transitions of the first argument (view) is returned
|
|
|
|
func GetTransitions(view View, subviewID ...string) map[string]Animation {
|
|
|
|
if len(subviewID) > 0 && subviewID[0] != "" {
|
|
|
|
view = ViewByID(view, subviewID[0])
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if view != nil {
|
2022-08-10 15:36:38 +03:00
|
|
|
return view.Transitions()
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
2022-08-10 15:36:38 +03:00
|
|
|
return map[string]Animation{}
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
2022-08-10 15:36:38 +03:00
|
|
|
// GetTransition returns the subview property transition. If there is no transition for the given property then nil is returned.
|
2022-08-31 22:17:46 +03:00
|
|
|
// If the second argument (subviewID) is not specified or it is "" then transitions of the first argument (view) is returned
|
2022-08-10 15:36:38 +03:00
|
|
|
func GetTransition(view View, subviewID, tag string) Animation {
|
2021-10-04 17:58:17 +03:00
|
|
|
if subviewID != "" {
|
|
|
|
view = ViewByID(view, subviewID)
|
|
|
|
}
|
|
|
|
|
2022-08-10 15:36:38 +03:00
|
|
|
if view != nil {
|
|
|
|
return view.Transition(tag)
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
2022-08-10 15:36:38 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddTransition adds the transition for the subview property.
|
2022-08-31 22:17:46 +03:00
|
|
|
// If the second argument (subviewID) is not specified or it is "" then the transition is added to the first argument (view)
|
2022-08-10 15:36:38 +03:00
|
|
|
func AddTransition(view View, subviewID, tag string, animation Animation) bool {
|
|
|
|
if tag != "" {
|
|
|
|
if subviewID != "" {
|
|
|
|
view = ViewByID(view, subviewID)
|
|
|
|
}
|
|
|
|
|
|
|
|
if view != nil {
|
|
|
|
view.SetTransition(tag, animation)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetAnimation returns the subview animations. The result is always non-nil.
|
2022-08-31 22:17:46 +03:00
|
|
|
// If the second argument (subviewID) is not specified or it is "" then transitions of the first argument (view) is returned
|
|
|
|
func GetAnimation(view View, subviewID ...string) []Animation {
|
|
|
|
if len(subviewID) > 0 && subviewID[0] != "" {
|
|
|
|
view = ViewByID(view, subviewID[0])
|
2021-10-04 17:58:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if view != nil {
|
|
|
|
if value := view.getRaw(AnimationTag); value != nil {
|
|
|
|
if animations, ok := value.([]Animation); ok && animations != nil {
|
|
|
|
return animations
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:58:17 +03:00
|
|
|
return []Animation{}
|
2021-09-07 17:36:50 +03:00
|
|
|
}
|