forked from mbk-lab/rui_orig
2
0
Fork 0

Added "animation" and "transition" properties

This commit is contained in:
Alexei Anoshenko 2021-10-04 17:58:17 +03:00
parent c16ea89679
commit be954df7c7
23 changed files with 2141 additions and 494 deletions

View File

@ -1792,7 +1792,7 @@ radius необходимо передать nil
func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent) func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent)
func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent) func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent)
## События указателя ### События указателя
Указатель - это аппаратно-независимое представление устройств ввода (таких как мышь, перо Указатель - это аппаратно-независимое представление устройств ввода (таких как мышь, перо
или точка контакта на сенсорной поверхности). Указатель может указывать на конкретную координату или точка контакта на сенсорной поверхности). Указатель может указывать на конкретную координату
@ -3793,6 +3793,296 @@ MediaPlayer имеет ряд методов для управления пар
где view - корневой View, playerID - id of AudioPlayer or VideoPlayer где view - корневой View, playerID - id of AudioPlayer or VideoPlayer
## Анимация
Библиотека поддерживает два вида анимации:
* Анимированое изменения значения свойства (далее "анимация перехода")
* Сценарий анимированного изменения одного или нескольких свойств (далее просто "сценарий анимации")
### Интерфейс Animation
Для задания параметров анимации мспользуется интерфейс Animation. Он расширяет интерфейс Properties.
Интерфейс создается с помощью функции:
func NewAnimation(params Params) Animation
Часть свойств интерфейса Animation используется в обоих типах анимации, остальные используются
только в сценариях анимации.
Общими свойствами являются
| Свойство | Константа | Тип | По умолчанию | Описание |
|-------------------|----------------|---------|--------------|-------------------------------------------------|
| "duration" | Duration | float64 | 1 | Длительность анимации в секундах |
| "delay" | Delay | float64 | 0 | Длительность задержки перед анимации в секундах |
| "timing-function" | TimingFunction | string | "ease" | Функция изменения скорости анимации |
Свойства используемые только в сценариях анимации будут описаны ниже
#### Свойство "timing-function"
Свойство "timing-function" описывает в текстовом виде функция изменения скорости анимации. Функции
могут быть разделены на 2 вида: простые функции и функции с параметрами.
Простые функции
| Функция | Константа | Описание |
|---------------|-----------------|-------------------------------------------------------------------------------|
| "ease" | EaseTiming | скорость увеличивается к середине, а в конце замедляется. |
| "ease-in" | EaseInTiming | скорость вначале медленная, но в конце увеличивается. |
| "ease-out" | EaseOutTiming | скорость вначале быстрая, но быстро снижается. Большая часть медленная |
| "ease-in-out" | EaseInOutTiming | скорость вначале быстрая, но быстро снижается, а в конце снова увеличивается. |
| "linear" | LinearTiming | постоянная скорость |
И есть две функции с параметрами:
* "steps(N)" - дискретная функция, где N - целое число задающее количество шагов. Вы можете задавать
данную функцию или в виде текста или использую функцию:
func StepsTiming(stepCount int) string
Например
animation := rui.NewAnimation(rui.Params{
rui.TimingFunction: rui.StepsTiming(10),
})
эквивалентно
animation := rui.NewAnimation(rui.Params{
rui.TimingFunction: "steps(10)",
})
* "cubic-bezier(x1, y1, x2, y2)" - временная функция кубической кривой Безье. x1, y1, x2, y2 имеют тип float64.
x1 и x2 должны быть в диапазоне [0, 1]. Вы можете задавать данную функцию или в виде текста или использую функцию:
func CubicBezierTiming(x1, y1, x2, y2 float64) string
### Анимация перехода
Анимация перехода может применяться к свойствам типа: SizeUnit, Color, AngleUnit, float64 и составным свойсвам
в составе которых имеются элементы перечисленных типов (например "shadow", "border" и т.д.).
При попытке применить анимацию к свойствам других типов (например, bool, string) ошибки не произойдет,
просто анимации не будет.
Анимация перехода бывает двух видов:
* аднократная;
* постоянная;
Однократная анимация запускается с помощью функции SetAnimated интерфейса View. Данная функция имеет следующее
описание:
SetAnimated(tag string, value interface{}, animation Animation) bool
Она присваивает свойству новое значение, при этом изменение происходит с использованием заданной анимации.
Например,
view.SetAnimated(rui.Width, rui.Px(400), rui.NewAnimation(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
}))
Есть также глобальная функция для анимированного однократного изменения значения свойства дочернего View
func SetAnimated(rootView View, viewID, tag string, value interface{}, animation Animation) bool
Постоянная анимация запускается каждый раз когда изменяется значение свойства. Для задания постоянной
анимации перехода используется свойство "transition" (константа Transition). В качества значения данному
свойству присваивается rui.Params, где в качестве ключа должно быть имя свойства, а значение - интерфейс Animation.
Например,
view.Set(rui.Transition, rui.Params{
rui.Height: rui.NewAnimation(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
},
rui.BackgroundColor: rui.NewAnimation(rui.Params{
rui.Duration: 1.5,
rui.Delay: 0.5,
rui.TimingFunction: rui.Linear,
},
})
Вызов функции SetAnimated не меняет значение свойства "transition".
Для получения текущего списка постоянных анимаций перехода используется функция
func GetTransition(view View, subviewID string) Params
Добавлять новые анимации перехода рекомендуется с помощью функции
func AddTransition(view View, subviewID, tag string, animation Animation) bool
Вызов данной функции эквивалентен следующему коду
transitions := rui.GetTransition(view, subviewID)
transitions[tag] = animation
rui.Set(view, subviewID, rui.Transition, transitions)
#### События анимации перехода
Анимация перехода генерирует следующие события
| Событие | Константа | Описание |
|---------------------------|-----------------------|----------------------------------------------------|
| "transition-run-event" | TransitionRunEvent | Цикл анимации перехода стартовал, т.е. до задержки |
| "transition-start-event" | TransitionStartEvent | Анимация перехода действительно стартовала, т.е. после задержки |
| "transition-end-event" | TransitionEndEvent | Анимация перехода закончена |
| "transition-cancel-event" | TransitionCancelEvent | Анимация перехода прервана |
Основной слушатель данных событий имеет следующий формат:
func(View, string)
где второй аргумент это имя свойства.
Можно также использовать слушателя следующего формата:
func()
func(string)
func(View)
Получить списки слушателей событий анимации перехода с помощью функций:
func GetTransitionRunListeners(view View, subviewID string) []func(View, string)
func GetTransitionStartListeners(view View, subviewID string) []func(View, string)
func GetTransitionEndListeners(view View, subviewID string) []func(View, string)
func GetTransitionCancelListeners(view View, subviewID string) []func(View, string)
### Cценарий анимации
енарий анимации описывает более сложную анимацию, по сравнению с анимацией перехода. Для этого
в Animation добавляются дополнительные свойства:
#### Свойство "property"
Свойство "property" (константа PropertyTag) описывает изменения свойств. В качестве значения ему присваивается
[]AnimatedProperty. Структура AnimatedProperty описывает изменение одного свойства. Она описана как
type AnimatedProperty struct {
Tag string
From, To interface{}
KeyFrames map[int]interface{}
}
где Tag - имя свойства, From - начальное значение свойства, To - конечное значение свойства,
KeyFrames - промежуточные значения свойства (ключевые кадры).
Обязательными являются поля Tag, From, To. Поле KeyFrames опционально, может быть nil.
Поле KeyFrames описывает ключавые кадры. В качестве ключа типа int используется процент времени
прошедший с начала анимации (именно начала самой анимации, время заданное свойством "delay" исключается).
А значание это значение свойства в данный момент времени. Например
prop := rui.AnimatedProperty {
Tag: rui.Width,
From: rui.Px(100),
To: rui.Px(200),
KeyFrames: map[int]interface{
90: rui.Px(220),
}
}
В данном примере свойство "width" 90% времени будет увеличиваться со 100px до 220px. В оставшиеся
10% времени - будет уменьшаться с 220px до 200px.
Свойству "property" присваивается []AnimatedProperty, а значит можно анимаровать сразу несколько свойств.
Вы должны задать хотя бы один эдемент "property", иначе анимация будет игнорироваться.
#### Свойство "id"
Свойство "id" (константа ID) типа string задает идентификатор анимации. Передается в качестве параметра слушателю
события анимации. Если вы не планируете использовать слушателей событий для анимации, то данное свойство
можно не задавать.
#### Свойство "iteration-count"
Свойство "iteration-count" (константа IterationCount) типа int задает количество повторений анимации.
Значение по умолчанию 1. Значение меньше нуля заставляет повторяться анимацию бесконечно.
#### Свойство "animation-direction"
Свойство "animation-direction" (константа AnimationDirection) типа int устанавливает, должна ли анимация
воспроизводиться вперед, назад или поочередно вперед и назад между воспроизведением
последовательности вперед и назад. Может принимать следующие значения:
| Значение | Константа | Описание |
|:--------:|---------------------------|-----------------------------------------------------------------------|
| 0 | NormalAnimation | Анимация проигрывается вперёд каждую итерацию, то есть, когда анимация заканчивается, она сразу сбрасывается в начальное положение и снова проигрывается. |
| 1 | ReverseAnimation | Анимация проигрывается наоборот, с последнего положения до первого и потом снова сбрасывается в конечное положение и снова проигрывается. |
| 2 | AlternateAnimation | Анимация меняет направление в каждом цикле, то есть в первом цикле она начинает с начального положения, доходит до конечного, а во втором цикле продолжает с конечного и доходит до начального и так далее |
| 3 | AlternateReverseAnimation | Анимация начинает проигрываться с конечного положения и доходит до начального, а в следующем цикле продолжая с начального переходит в конечное |
#### Запуск анимации
Для запуска сценария анимации необходимо созданный Animation интерфейс присвоить свойству "animation"
(константа AnimationTag). Если View уже отображается на экране, то анимация запускается сразу (с учетом
заданной задержки), в противоположном случае анимация запускается как только View отобразится на экране.
Свойству "animation" можно присваивать Animation и []Animation, т.е. можно запускать несколько анимаций
одновременно для одного View
Пример,
prop := rui.AnimatedProperty {
Tag: rui.Width,
From: rui.Px(100),
To: rui.Px(200),
KeyFrames: map[int]interface{
90: rui.Px(220),
}
}
animation := rui.NewAnimation(rui.Params{
rui.PropertyTag: []rui.AnimatedProperty{prop},
rui.Duration: 2,
rui.TimingFunction: LinearTiming,
})
rui.Set(view, "subview", rui.AnimationTag, animation)
#### Свойство "animation-paused"
Свойство "animation-paused" (константа AnimationPaused) типа bool позволяет приостановить анимацию.
Для того чтобы поставить анимацию на паузу необходимо данному свойству присвоить значение true, а
для возобновления - false.
Внимание. В момент присваивания значения свойству "animation" cвойство "animation-paused" сбрасывается в false.
#### События анимации
Сценарий анимации генерирует следующие события
| Событие | Константа | Описание |
|-----------------------------|-------------------------|----------------------------------|
| "animation-start-event" | AnimationStartEvent | Анимация стартовала |
| "animation-end-event" | AnimationEndEvent | Анимация закончена |
| "animation-cancel-event" | AnimationCancelEvent | Анимация прервана |
| "animation-iteration-event" | AnimationIterationEvent | Началась новая итерация анимации |
Внимание! Не все браузеры поддерживают событие "animation-cancel-event". В данное время это только
Safari и Firefox.
Основной слушатель данных событий имеет следующий формат:
func(View, string)
где второй аргумент это id анимации.
Можно также использовать слушателя следующего формата:
func()
func(string)
func(View)
Получить списки слушателей событий анимации с помощью функций:
func GetAnimationStartListeners(view View, subviewID string) []func(View, string)
func GetAnimationEndListeners(view View, subviewID string) []func(View, string)
func GetAnimationCancelListeners(view View, subviewID string) []func(View, string)
func GetAnimationIterationListeners(view View, subviewID string) []func(View, string)
## Сессия ## Сессия
Когда клиент создает соединение с сервером, то для этого соединения создается интерфейс Session. Когда клиент создает соединение с сервером, то для этого соединения создается интерфейс Session.

292
README.md
View File

@ -1765,7 +1765,7 @@ You can get lists of listeners for mouse events using the functions:
func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent) func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent)
func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent) func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent)
## Pointer Events ### Pointer Events
A pointer is a device-independent representation of input devices (such as a mouse, pen, A pointer is a device-independent representation of input devices (such as a mouse, pen,
or point of contact on a touch surface). A pointer can point to a specific coordinate or point of contact on a touch surface). A pointer can point to a specific coordinate
@ -3761,6 +3761,296 @@ For quick access to these methods, there are global functions:
where view is the root View, playerID is the id of AudioPlayer or VideoPlayer where view is the root View, playerID is the id of AudioPlayer or VideoPlayer
## Animation
The library supports two types of animation:
* Animated property value changes (hereinafter "transition animation")
* Script animated change of one or more properties (hereinafter simply "animation script")
### Animation interface
The Animation interface is used to set animation parameters. It extends the Properties interface.
The interface is created using the function:
func NewAnimation(params Params) Animation
Some of the properties of the Animation interface are used in both types of animation, the rest are used only
in animation scripts.
Common properties are
| Property | Constant | Type | Default | Description |
|-------------------|----------------|---------|--------------|----------------------------------------------|
| "duration" | Duration | float64 | 1 | Animation duration in seconds |
| "delay" | Delay | float64 | 0 | Delay before animation in seconds |
| "timing-function" | TimingFunction | string | "ease" | The function of changing the animation speed |
Properties used only in animation scripts will be described below.
#### "timing-function" property
The "timing-function" property describes in text the function of changing the speed of the animation.
Functions can be divided into 2 types: simple functions and functions with parameters.
Simple functions
| Function | Constant | Description |
|---------------|-----------------|-------------------------------------------------------------------|
| "ease" | EaseTiming | the speed increases towards the middle and slows down at the end. |
| "ease-in" | EaseInTiming | the speed is slow at first, but increases in the end. |
| "ease-out" | EaseOutTiming | speed is fast at first, but decreases rapidly. Most of the slow |
| "ease-in-out" | EaseInOutTiming | the speed is fast at first, but quickly decreases, and at the end it increases again. |
| "linear" | LinearTiming | constant speed |
And there are two functions with parameters:
* "steps(N)" - discrete function, where N is an integer specifying the number of steps.
You can specify this function either as text or using the function:
func StepsTiming(stepCount int) string
For example
animation := rui.NewAnimation(rui.Params{
rui.TimingFunction: rui.StepsTiming(10),
})
equivalent to
animation := rui.NewAnimation(rui.Params{
rui.TimingFunction: "steps(10)",
})
* "cubic-bezier(x1, y1, x2, y2)" - time function of a cubic Bezier curve. x1, y1, x2, y2 are of type float64.
x1 and x2 must be in the range [0...1]. You can specify this function either as text or using the function:
func CubicBezierTiming(x1, y1, x2, y2 float64) string
### Transition animation
Transition animation can be applied to properties of the type: SizeUnit, Color, AngleUnit, float64 and composite properties that contain elements of the listed types (for example, "shadow", "border", etc.).
If you try to animate other types of properties (for example, bool, string), no error will occur,
there will simply be no animation.
There are two types of transition animations:
* single-fold;
* constant;
A one-time animation is triggered using the SetAnimated function of the View interface.
This function has the following description:
SetAnimated(tag string, value interface{}, animation Animation) bool
It assigns a new value to the property, and the change occurs using the specified animation.
For example,
view.SetAnimated(rui.Width, rui.Px(400), rui.NewAnimation(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
}))
There is also a global function for animated one-time change of the property value of the child View
func SetAnimated(rootView View, viewID, tag string, value interface{}, animation Animation) bool
A persistent animation runs every time the property value changes.
To set the constant animation of the transition, use the "transition" property (the Transition constant).
As a value, this property is assigned rui.Params, where the property name should be the key,
and the value should be the Animation interface.
For example,
view.Set(rui.Transition, rui.Params{
rui.Height: rui.NewAnimation(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
},
rui.BackgroundColor: rui.NewAnimation(rui.Params{
rui.Duration: 1.5,
rui.Delay: 0.5,
rui.TimingFunction: rui.Linear,
},
})
Calling the SetAnimated function does not change the value of the "transition" property.
To get the current list of permanent transition animations, use the function
func GetTransition(view View, subviewID string) Params
It is recommended to add new transition animations using the function
func AddTransition(view View, subviewID, tag string, animation Animation) bool
Calling this function is equivalent to the following code
transitions := rui.GetTransition(view, subviewID)
transitions[tag] = animation
rui.Set(view, subviewID, rui.Transition, transitions)
#### Transition animation events
The transition animation generates the following events
| Event | Constant | Description |
|---------------------------|-----------------------|------------------------------------------------------------------|
| "transition-run-event" | TransitionRunEvent | The transition animation loop has started, i.e. before the delay |
| "transition-start-event" | TransitionStartEvent | The transition animation has actually started, i.e. after delay |
| "transition-end-event" | TransitionEndEvent | Transition animation finished |
| "transition-cancel-event" | TransitionCancelEvent | Transition animation interrupted |
The main event listener has the following format:
func(View, string)
where the second argument is the name of the property.
You can also use a listener in the following format:
func()
func(string)
func(View)
Get lists of listeners for transition animation events using functions:
func GetTransitionRunListeners(view View, subviewID string) []func(View, string)
func GetTransitionStartListeners(view View, subviewID string) []func(View, string)
func GetTransitionEndListeners(view View, subviewID string) []func(View, string)
func GetTransitionCancelListeners(view View, subviewID string) []func(View, string)
### Animation script
An animation script describes a more complex animation than a transition animation. To do this, additional properties are added to Animation:
#### "property" property
The "property" property (constant PropertyTag) describes property changes.
[]AnimatedProperty or AnimatedProperty is assigned as a value. The AnimatedProperty structure describes
the change script of one property. She is described as
type AnimatedProperty struct {
Tag string
From, To interface{}
KeyFrames map[int]interface{}
}
where Tag is the name of the property, From is the initial value of the property,
To is the final value of the property, KeyFrames is intermediate property values (keyframes).
The required fields are Tag, From, To. The KeyFrames field is optional, it can be nil.
The KeyFrames field describes key frames. As a key of type int, the percentage of time elapsed
since the beginning of the animation is used (exactly the beginning of the animation itself,
the time specified by the "delay" property is excluded).
And the value is the value of the property at a given moment in time. For example
prop := rui.AnimatedProperty {
Tag: rui.Width,
From: rui.Px(100),
To: rui.Px(200),
KeyFrames: map[int]interface{
90: rui.Px(220),
}
}
In this example, the "width" property will grow from 100px to 220px 90% of the time.
In the remaining 10% of the time, it will decrease from 220px to 200px.
The "property" property is assigned to []AnimatedProperty, which means that you can animate several properties at once.
You must set at least one "property" element, otherwise the animation will be ignored.
#### "id" property
The "id" string property (constant ID) specifies the animation identifier.
Passed as a parameter to the animation event listener. If you do not plan to use event listeners for animation,
then you do not need to set this property.
#### "iteration-count" property
The "iteration-count" int property (constant IterationCount) specifies the number of animation repetitions.
The default is 1. A value less than zero causes the animation to repeat indefinitely.
#### "animation-direction" property
The "animation-direction" int property (an AnimationDirection constant) specifies whether
the animation should play forward, backward, or alternately forward and backward between forward and
backward playback of the sequence. It can take the following values:
| Value | Constant | Description |
|:--------:|---------------------------|-----------------------------------------------------------------------|
| 0 | NormalAnimation | The animation plays forward every iteration, that is, when the animation ends, it is immediately reset to its starting position and played again. |
| 1 | ReverseAnimation | The animation plays backwards, from the last position to the first, and then resets to the final position and plays again. |
| 2 | AlternateAnimation | The animation changes direction in each cycle, that is, in the first cycle, it starts from the start position, reaches the end position, and in the second cycle, it continues from the end position and reaches the start position, and so on. |
| 3 | AlternateReverseAnimation | The animation starts playing from the end position and reaches the start position, and in the next cycle, continuing from the start position, it goes to the end position. |
#### Animation start
To start the animation script, you must assign the interface created by Animation to the "animation" property
(the AnimationTag constant). If the View is already displayed on the screen, then the animation starts immediately
(taking into account the specified delay), otherwise the animation starts as soon as the View is displayed
on the screen.
The "animation" property can be assigned Animation and [] Animation, ie. you can run several animations
at the same time for one View
Example,
prop := rui.AnimatedProperty {
Tag: rui.Width,
From: rui.Px(100),
To: rui.Px(200),
KeyFrames: map[int]interface{
90: rui.Px(220),
}
}
animation := rui.NewAnimation(rui.Params{
rui.PropertyTag: []rui.AnimatedProperty{prop},
rui.Duration: 2,
rui.TimingFunction: LinearTiming,
})
rui.Set(view, "subview", rui.AnimationTag, animation)
#### "animation-paused" property
The "animation-paused" bool property of View (AnimationPaused constant) allows the animation to be paused.
In order to pause the animation, set this property to "true", and to resume to "false".
Attention. When you assign a value to the "animation" property, the "animation-paused" property is set to false.
#### Animation events
The animation script generates the following events
| Event | Constant | Description |
|-----------------------------|-------------------------|----------------------------------------|
| "animation-start-event" | AnimationStartEvent | Animation started |
| "animation-end-event" | AnimationEndEvent | Animation finished |
| "animation-cancel-event" | AnimationCancelEvent | Animation interrupted |
| "animation-iteration-event" | AnimationIterationEvent | A new iteration of animation has begun |
Attention! Not all browsers support the "animation-cancel-event" event. This is currently only Safari and Firefox.
The main event data listener has the following format:
func(View, string)
where the second argument is the id of the animation.
You can also use a listener in the following format:
func()
func(string)
func(View)
Get lists of animation event listeners using functions:
func GetAnimationStartListeners(view View, subviewID string) []func(View, string)
func GetAnimationEndListeners(view View, subviewID string) []func(View, string)
func GetAnimationCancelListeners(view View, subviewID string) []func(View, string)
func GetAnimationIterationListeners(view View, subviewID string) []func(View, string)
## Session ## Session
When a client creates a connection to a server, a Session interface is created for that connection. When a client creates a connection to a server, a Session interface is created for that connection.

View File

@ -1,229 +1,710 @@
package rui package rui
/*
import ( import (
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings"
) )
type AnimationTags struct { const (
Tag string // AnimationTag is the constant for the "animation" property tag.
Start, End interface{} // 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
// 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
// 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
// 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"
)
// StepsTiming return a timing function along stepCount stops along the transition, diplaying each stop for equal lengths of time
func StepsTiming(stepCount int) string {
return "steps(" + strconv.Itoa(stepCount) + ")"
} }
type AnimationKeyFrame struct { // CubicBezierTiming return a cubic-Bezier curve timing function. x1 and x2 must be in the range [0, 1].
KeyFrame int func CubicBezierTiming(x1, y1, x2, y2 float64) string {
TimingFunction string if x1 < 0 {
Params Params 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)
} }
type AnimationScenario interface { // 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
From interface{}
// To is the final value of the property
To interface{}
// KeyFrames is intermediate property values
KeyFrames map[int]interface{}
}
type animationData struct {
propertyList
keyFramesName string
}
// 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
fmt.Stringer fmt.Stringer
ruiStringer ruiStringer
Name() string animationCSS(session Session) string
cssString(session Session) string transitionCSS(buffer *strings.Builder, session Session)
hasAnimatedPropery() bool
animationName() string
} }
type animationScenario struct { func parseAnimation(obj DataObject) Animation {
name string animation := new(animationData)
tags []AnimationTags animation.init()
keyFrames []AnimationKeyFrame
cssText string
}
var animationScenarios = []string{} for i := 0; i < obj.PropertyCount(); i++ {
if node := obj.Property(i); node != nil {
func addAnimationScenario(name string) string { if node.Type() == TextNode {
animationScenarios = append(animationScenarios, name) animation.Set(node.Tag(), node.Text())
return name } else {
} animation.Set(node.Tag(), node)
func registerAnimationScenario() string {
find := func(text string) bool {
for _, scenario := range animationScenarios {
if scenario == text {
return true
} }
} }
return false
} }
n := 1
name := fmt.Sprintf("scenario%08d", n)
for find(name) {
n++
name = fmt.Sprintf("scenario%08d", n)
}
animationScenarios = append(animationScenarios, name)
return name
}
func NewAnimationScenario(tags []AnimationTags, keyFrames []AnimationKeyFrame) AnimationScenario {
if tags == nil {
ErrorLog(`Nil "tags" argument is not allowed.`)
return nil
}
if len(tags) == 0 {
ErrorLog(`An empty "tags" argument is not allowed.`)
return nil
}
animation := new(animationScenario)
animation.tags = tags
if keyFrames == nil && len(keyFrames) > 0 {
animation.keyFrames = keyFrames
}
animation.name = registerAnimationScenario()
return animation return animation
} }
func (animation *animationScenario) Name() string { func NewAnimation(params Params) Animation {
return animation.name animation := new(animationData)
animation.init()
for tag, value := range params {
animation.Set(tag, value)
}
return animation
} }
func (animation *animationScenario) String() string { func (animation *animationData) hasAnimatedPropery() bool {
props := animation.getRaw(PropertyTag)
if props == nil {
ErrorLog("There are no animated properties.")
return false
}
if _, ok := props.([]AnimatedProperty); !ok {
ErrorLog("Invalid animated properties.")
return false
}
return true
}
func (animation *animationData) animationName() string {
return animation.keyFramesName
}
func (animation *animationData) Set(tag string, value interface{}) bool {
if value == nil {
animation.Remove(tag)
return true
}
switch tag = strings.ToLower(tag); tag {
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 {
result.KeyFrames = map[int]interface{}{n: node.Text()}
} 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)
case AnimationDirection, Direction:
return animation.setEnumProperty(AnimationDirection, value, enumProperties[AnimationDirection].values)
default:
ErrorLogF(`The "%s" property is not supported by Animation`, tag)
}
return false
}
func (animation *animationData) Remove(tag string) {
tag = strings.ToLower(tag)
if tag == Direction {
tag = AnimationDirection
}
delete(animation.properties, tag)
}
func (animation *animationData) Get(tag string) interface{} {
tag = strings.ToLower(tag)
if tag == Direction {
tag = AnimationDirection
}
return animation.getRaw(tag)
}
func (animation *animationData) String() string {
writer := newRUIWriter() writer := newRUIWriter()
animation.ruiString(writer) animation.ruiString(writer)
return writer.finish() return writer.finish()
} }
func (animation *animationScenario) ruiString(writer ruiWriter) { func (animation *animationData) ruiString(writer ruiWriter) {
writer.startObject("animation")
// TODO // TODO
writer.endObject()
} }
func valueToCSS(tag string, value interface{}, session Session) string { func (animation *animationData) animationCSS(session Session) string {
if value == nil { if animation.keyFramesName == "" {
return "" props := animation.getRaw(PropertyTag)
} if props == nil {
ErrorLog("There are no animated properties.")
convertFloat := func(val float64) string { return ""
if _, ok := sizeProperties[tag]; ok {
return fmt.Sprintf("%gpx", val)
} }
return fmt.Sprintf("%g", val)
}
switch value := value.(type) { animatedProps, ok := props.([]AnimatedProperty)
case string:
value, ok := session.resolveConstants(value)
if !ok { if !ok {
ErrorLog("Invalid animated properties.")
return "" return ""
} }
if _, ok := sizeProperties[tag]; ok {
var size SizeUnit animation.keyFramesName = session.registerAnimation(animatedProps)
if size.SetValue(value) { }
return size.cssString("auto")
} buffer := allocStringBuilder()
return "" defer freeStringBuilder(buffer)
buffer.WriteString(animation.keyFramesName)
if duration, _ := floatProperty(animation, Duration, session, 1); duration > 0 {
buffer.WriteString(fmt.Sprintf(" %gs ", duration))
} else {
buffer.WriteString(" 1s ")
}
buffer.WriteString(animation.timingFunctionCSS(session))
if delay, _ := floatProperty(animation, Delay, session, 0); delay > 0 {
buffer.WriteString(fmt.Sprintf(" %gs", delay))
} else {
buffer.WriteString(" 0s")
}
if iterationCount, _ := intProperty(animation, IterationCount, session, 0); iterationCount >= 0 {
if iterationCount == 0 {
iterationCount = 1
} }
if isPropertyInList(tag, colorProperties) { buffer.WriteString(fmt.Sprintf(" %d ", iterationCount))
var color Color } else {
if color.SetValue(value) { buffer.WriteString(" infinite ")
return color.cssString() }
}
return ""
}
if isPropertyInList(tag, angleProperties) {
var angle AngleUnit
if angle.SetValue(value) {
return angle.cssString()
}
return ""
}
if _, ok := enumProperties[tag]; ok {
var size SizeUnit
if size.SetValue(value) {
return size.cssString("auto")
}
return ""
}
return value
case SizeUnit: direction, _ := enumProperty(animation, AnimationDirection, session, 0)
return value.cssString("auto") values := enumProperties[AnimationDirection].cssValues
if direction < 0 || direction >= len(values) {
direction = 0
}
buffer.WriteString(values[direction])
case AngleUnit: // TODO "animation-fill-mode"
return value.cssString() buffer.WriteString(" forwards")
case Color: return buffer.String()
return value.cssString() }
case float32: func (animation *animationData) transitionCSS(buffer *strings.Builder, session Session) {
return convertFloat(float64(value))
case float64: if duration, _ := floatProperty(animation, Duration, session, 1); duration > 0 {
return convertFloat(value) buffer.WriteString(fmt.Sprintf(" %gs ", duration))
} else {
buffer.WriteString(" 1s ")
}
default: buffer.WriteString(animation.timingFunctionCSS(session))
if n, ok := isInt(value); ok {
if prop, ok := enumProperties[tag]; ok {
values := prop.cssValues
if n >= 0 && n < len(values) {
return values[n]
}
return ""
}
return convertFloat(float64(n)) if delay, _ := floatProperty(animation, Delay, session, 0); delay > 0 {
buffer.WriteString(fmt.Sprintf(" %gs", delay))
}
}
func (animation *animationData) timingFunctionCSS(session Session) string {
if timingFunction, ok := stringProperty(animation, TimingFunction, session); ok {
if timingFunction, ok = session.resolveConstants(timingFunction); ok && validateTimingFunction(timingFunction) {
return timingFunction
} }
} }
return ("ease")
}
func validateTimingFunction(timingFunction string) bool {
switch timingFunction {
case "", EaseTiming, EaseInTiming, EaseOutTiming, EaseInOutTiming, LinearTiming:
return true
}
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
}
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
}
}
return true
}
}
}
}
return false
}
func (session *sessionData) registerAnimation(props []AnimatedProperty) string {
session.animationCounter++
name := fmt.Sprintf("kf%06d", session.animationCounter)
var cssBuilder cssStyleBuilder
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)
}
}
}
}
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
}
}
}
if len(params) > 0 {
cssBuilder.startAnimationFrame(strconv.Itoa(frame) + "%")
NewViewStyle(params).cssViewStyle(&cssBuilder, session)
cssBuilder.endAnimationFrame()
}
}
}
cssBuilder.startAnimationFrame("to")
NewViewStyle(toParams).cssViewStyle(&cssBuilder, session)
cssBuilder.endAnimationFrame()
cssBuilder.endAnimation()
style := strings.ReplaceAll(cssBuilder.finish(), "\n", `\n`)
session.runScript(`document.querySelector('style').textContent += "` + style + `"`)
return name
}
func (view *viewData) SetAnimated(tag string, value interface{}, animation Animation) bool {
if animation == nil {
return view.Set(tag, value)
}
updateProperty(view.htmlID(), "ontransitionend", "transitionEndEvent(this, event)", view.session)
updateProperty(view.htmlID(), "ontransitioncancel", "transitionCancelEvent(this, event)", view.session)
if prevAnimation, ok := view.transitions[tag]; ok {
view.singleTransition[tag] = prevAnimation
} else {
view.singleTransition[tag] = nil
}
view.transitions[tag] = animation
view.updateTransitionCSS()
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)
}
}
return buffer.String()
}
}
return "" return ""
} }
func (animation *animationScenario) cssString(session Session) string { func (style *viewStyle) transitionCSS(session Session) string {
if animation.cssText != "" { buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer := allocStringBuilder() for tag, animation := range style.transitions {
defer freeStringBuilder(buffer) if buffer.Len() > 0 {
buffer.WriteString(", ")
writeValue := func(tag string, value interface{}) {
if cssValue := valueToCSS(tag, value); cssValue != "" {
buffer.WriteString(" ")
buffer.WriteString(tag)
buffer.WriteString(": ")
buffer.WriteString(cssValue)
buffer.WriteString(";\n")
}
} }
buffer.WriteString(tag)
animation.transitionCSS(buffer, session)
}
return buffer.String()
}
buffer.WriteString(`@keyframes `) func (view *viewData) updateTransitionCSS() {
buffer.WriteString(animation.name) updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(view.Session()), view.Session())
}
buffer.WriteString(" {\n from {\n") func (view *viewData) getTransitions() Params {
for _, property := range animation.tags { result := Params{}
writeValue(property.Tag, property.Start) for tag, animation := range view.transitions {
result[tag] = animation
}
return result
}
// SetAnimated sets the property with name "tag" of the "rootView" subview with "viewID" id by value. Result:
// true - success,
// false - error (incompatible type or invalid format of a string value, see AppLog).
func SetAnimated(rootView View, viewID, tag string, value interface{}, animation Animation) bool {
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.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func IsAnimationPaused(view View, subviewID string) bool {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := boolStyledProperty(view, AnimationPaused); ok {
return result
} }
}
return false
}
buffer.WriteString(" }\n to {\n") // GetTransition returns the subview transitions. The result is always non-nil.
for _, property := range animation.tags { // If the second argument (subviewID) is "" then transitions of the first argument (view) is returned
writeValue(property.Tag, property.End) func GetTransition(view View, subviewID string) Params {
} if subviewID != "" {
buffer.WriteString(" }\n") view = ViewByID(view, subviewID)
if animation.keyFrames != nil {
for _, keyFrame := range animation.keyFrames {
if keyFrame.KeyFrame > 0 && keyFrame.KeyFrame < 100 &&
keyFrame.Params != nil && len(keyFrame.Params) > 0 {
buffer.WriteString(" ")
buffer.WriteString(strconv.Itoa(keyFrame.KeyFrame))
buffer.WriteString("% {\n")
for tag, value := range keyFrame.Params {
writeValue(tag, value)
}
buffer.WriteString(" }\n")
}
}
}
buffer.WriteString("}\n")
animation.cssText = buffer.String()
} }
return animation.cssText if view != nil {
return view.getTransitions()
}
return Params{}
}
// AddTransition adds the transition for the subview property.
// If the second argument (subviewID) is "" then the transition is added to the first argument (view)
func AddTransition(view View, subviewID, tag string, animation Animation) bool {
if tag == "" {
return false
}
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return false
}
transitions := view.getTransitions()
transitions[tag] = animation
return view.Set(Transition, transitions)
}
// GetAnimation returns the subview animations. The result is always non-nil.
// If the second argument (subviewID) is "" then transitions of the first argument (view) is returned
func GetAnimation(view View, subviewID string) []Animation {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.getRaw(AnimationTag); value != nil {
if animations, ok := value.([]Animation); ok && animations != nil {
return animations
}
}
}
return []Animation{}
} }
*/

375
animationEvents.go Normal file
View File

@ -0,0 +1,375 @@
package rui
import "strings"
const (
// TransitionRunEvent is the constant for "transition-run-event" property tag
// The "transition-run-event" is fired when a transition is first created,
// i.e. before any transition delay has begun.
TransitionRunEvent = "transition-run-event"
// TransitionStartEvent is the constant for "transition-end-event" property tag
// The "transition-start-event" is fired when a transition has actually started,
// i.e., after "delay" has ended.
TransitionStartEvent = "transition-start-event"
// TransitionEndEvent is the constant for "transition-end-event" property tag
// The "transition-end-event" is fired when a transition has completed.
TransitionEndEvent = "transition-end-event"
// TransitionCancelEvent is the constant for "transition-cancel-event" property tag
// The "transition-cancel-event" is fired when a transition is cancelled. The transition is cancelled when:
// * A new property transition has begun.
// * The "visibility" property is set to "gone".
// * The transition is stopped before it has run to completion, e.g. by moving the mouse off a hover-transitioning view.
TransitionCancelEvent = "transition-cancel-event"
// AnimationStartEvent is the constant for "animation-start-event" property tag.
// The "animation-start-event" is fired when an animation has started.
// If there is an animation-delay, this event will fire once the delay period has expired.
AnimationStartEvent = "animation-start-event"
// AnimationEndEvent is the constant for "animation-end-event" property tag.
// The "animation-end-event" is fired when aт фnimation has completed.
// If the animation aborts before reaching completion, such as if the element is removed
// or the animation is removed from the element, the "animation-end-event" is not fired.
AnimationEndEvent = "animation-end-event"
// AnimationCancelEvent is the constant for "animation-cancel-event" property tag.
// The "animation-cancel-event" is fired when an animation unexpectedly aborts.
// In other words, any time it stops running without sending the "animation-end-event".
// This might happen when the animation-name is changed such that the animation is removed,
// or when the animating view is hidden. Therefore, either directly or because any of its
// containing views are hidden.
// The event is not supported by all browsers.
AnimationCancelEvent = "animation-cancel-event"
// AnimationIterationEvent is the constant for "animation-iteration-event" property tag.
// The "animation-iteration-event" is fired when an iteration of an animation ends,
// and another one begins. This event does not occur at the same time as the animationend event,
// and therefore does not occur for animations with an "iteration-count" of one.
AnimationIterationEvent = "animation-iteration-event"
)
func valueToAnimationListeners(value interface{}) ([]func(View, string), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(View, string):
return []func(View, string){value}, true
case func(string):
fn := func(view View, event string) {
value(event)
}
return []func(View, string){fn}, true
case func(View):
fn := func(view View, event string) {
value(view)
}
return []func(View, string){fn}, true
case func():
fn := func(view View, event string) {
value()
}
return []func(View, string){fn}, true
case []func(View, string):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(string):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, string), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event string) {
v(event)
}
}
return listeners, true
case []func(View):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, string), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event string) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, string), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event string) {
v()
}
}
return listeners, true
case []interface{}:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, string), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(View, string):
listeners[i] = v
case func(string):
listeners[i] = func(view View, event string) {
v(event)
}
case func(View):
listeners[i] = func(view View, event string) {
v(view)
}
case func():
listeners[i] = func(view View, event string) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var transitionEvents = map[string]struct{ jsEvent, jsFunc string }{
TransitionRunEvent: {jsEvent: "ontransitionrun", jsFunc: "transitionRunEvent"},
TransitionStartEvent: {jsEvent: "ontransitionstart", jsFunc: "transitionStartEvent"},
TransitionEndEvent: {jsEvent: "ontransitionend", jsFunc: "transitionEndEvent"},
TransitionCancelEvent: {jsEvent: "ontransitioncancel", jsFunc: "transitionCancelEvent"},
}
func (view *viewData) setTransitionListener(tag string, value interface{}) bool {
listeners, ok := valueToAnimationListeners(value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeTransitionListener(tag)
} else if js, ok := transitionEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeTransitionListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := transitionEvents[tag]; ok {
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
}
}
}
func getAnimationListeners(view View, subviewID string, tag string) []func(View, string) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View, string)); ok {
return result
}
}
}
return []func(View, string){}
}
func transitionEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range transitionEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func (view *viewData) handleTransitionEvents(tag string, data DataObject) {
if property, ok := data.PropertyValue("property"); ok {
if tag == TransitionEndEvent || tag == TransitionCancelEvent {
if animation, ok := view.singleTransition[property]; ok {
delete(view.singleTransition, property)
if animation != nil {
view.transitions[property] = animation
} else {
delete(view.transitions, property)
}
view.updateTransitionCSS()
}
}
for _, listener := range getAnimationListeners(view, "", tag) {
listener(view, property)
}
}
}
var animationEvents = map[string]struct{ jsEvent, jsFunc string }{
AnimationStartEvent: {jsEvent: "onanimationstart", jsFunc: "animationStartEvent"},
AnimationEndEvent: {jsEvent: "onanimationend", jsFunc: "animationEndEvent"},
AnimationIterationEvent: {jsEvent: "onanimationiteration", jsFunc: "animationIterationEvent"},
AnimationCancelEvent: {jsEvent: "onanimationcancel", jsFunc: "animationCancelEvent"},
}
func (view *viewData) setAnimationListener(tag string, value interface{}) bool {
listeners, ok := valueToAnimationListeners(value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeAnimationListener(tag)
} else if js, ok := animationEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeAnimationListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := animationEvents[tag]; ok {
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
}
}
}
func animationEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range animationEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func (view *viewData) handleAnimationEvents(tag string, data DataObject) {
if listeners := getAnimationListeners(view, "", tag); len(listeners) > 0 {
id := ""
if name, ok := data.PropertyValue("name"); ok {
for _, animation := range GetAnimation(view, "") {
if name == animation.animationName() {
id, _ = stringProperty(animation, ID, view.Session())
}
}
}
for _, listener := range listeners {
listener(view, id)
}
}
}
// GetTransitionRunListeners returns the "transition-run-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTransitionRunListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, TransitionRunEvent)
}
// GetTransitionStartListeners returns the "transition-start-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTransitionStartListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, TransitionStartEvent)
}
// GetTransitionEndListeners returns the "transition-end-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTransitionEndListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, TransitionEndEvent)
}
// GetTransitionCancelListeners returns the "transition-cancel-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTransitionCancelListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, TransitionCancelEvent)
}
// GetAnimationStartListeners returns the "animation-start-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetAnimationStartListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, AnimationStartEvent)
}
// GetAnimationEndListeners returns the "animation-end-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetAnimationEndListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, AnimationEndEvent)
}
// GetAnimationCancelListeners returns the "animation-cancel-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetAnimationCancelListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, AnimationCancelEvent)
}
// GetAnimationIterationListeners returns the "animation-iteration-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetAnimationIterationListeners(view View, subviewID string) []func(View, string) {
return getAnimationListeners(view, subviewID, AnimationIterationEvent)
}

View File

@ -1037,8 +1037,26 @@ function startResize(element, mx, my, event) {
} }
} }
function transitionStartEvent(element, event) {
var message = "transition-start-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) {
message += ",property=" + event.propertyName
}
sendMessage(message + "}");
event.stopPropagation();
}
function transitionRunEvent(element, event) {
var message = "transition-run-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) {
message += ",property=" + event.propertyName
}
sendMessage(message + "}");
event.stopPropagation();
}
function transitionEndEvent(element, event) { function transitionEndEvent(element, event) {
var message = "transitionEnd{session=" + sessionID + ",id=" + element.id; var message = "transition-end-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) { if (event.propertyName) {
message += ",property=" + event.propertyName message += ",property=" + event.propertyName
} }
@ -1047,7 +1065,7 @@ function transitionEndEvent(element, event) {
} }
function transitionCancelEvent(element, event) { function transitionCancelEvent(element, event) {
var message = "transitionEnd{session=" + sessionID + ",id=" + element.id; var message = "transition-cancel-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) { if (event.propertyName) {
message += ",property=" + event.propertyName message += ",property=" + event.propertyName
} }
@ -1055,8 +1073,44 @@ function transitionCancelEvent(element, event) {
event.stopPropagation(); event.stopPropagation();
} }
function animationStartEvent(element, event) {
var message = "animation-start-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
sendMessage(message + "}");
event.stopPropagation();
}
function animationEndEvent(element, event) {
var message = "animation-end-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
sendMessage(message + "}");
event.stopPropagation();
}
function animationCancelEvent(element, event) {
var message = "animation-cancel-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
sendMessage(message + "}");
event.stopPropagation();
}
function animationIterationEvent(element, event) {
var message = "animation-iteration-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
sendMessage(message + "}");
event.stopPropagation();
}
function stackTransitionEndEvent(stackId, propertyName, event) { function stackTransitionEndEvent(stackId, propertyName, event) {
sendMessage("transitionEnd{session=" + sessionID + ",id=" + stackId + ",property=" + propertyName + "}"); sendMessage("transition-end-event{session=" + sessionID + ",id=" + stackId + ",property=" + propertyName + "}");
event.stopPropagation(); event.stopPropagation();
} }

View File

@ -102,7 +102,7 @@ const (
// BackgroundElement describes the background element. // BackgroundElement describes the background element.
type BackgroundElement interface { type BackgroundElement interface {
Properties Properties
cssStyle(view View) string cssStyle(session Session) string
Tag() string Tag() string
} }
@ -238,8 +238,7 @@ func (image *backgroundImage) Get(tag string) interface{} {
return image.backgroundElement.Get(image.normalizeTag(tag)) return image.backgroundElement.Get(image.normalizeTag(tag))
} }
func (image *backgroundImage) cssStyle(view View) string { func (image *backgroundImage) cssStyle(session Session) string {
session := view.Session()
if src, ok := stringProperty(image, Source, session); ok && src != "" { if src, ok := stringProperty(image, Source, session); ok && src != "" {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
@ -444,7 +443,7 @@ func (point *BackgroundGradientPoint) setValue(value string) bool {
return false return false
} }
func (gradient *backgroundGradient) writeGradient(view View, buffer *strings.Builder) bool { func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient] value, ok := gradient.properties[Gradient]
if !ok { if !ok {
@ -455,7 +454,7 @@ func (gradient *backgroundGradient) writeGradient(view View, buffer *strings.Bui
switch value := value.(type) { switch value := value.(type) {
case string: case string:
if text, ok := view.Session().resolveConstants(value); ok && text != "" { if text, ok := session.resolveConstants(value); ok && text != "" {
elements := strings.Split(text, `,`) elements := strings.Split(text, `,`)
points := make([]BackgroundGradientPoint, len(elements)) points := make([]BackgroundGradientPoint, len(elements))
for i, element := range elements { for i, element := range elements {
@ -477,7 +476,7 @@ func (gradient *backgroundGradient) writeGradient(view View, buffer *strings.Bui
for i, element := range value { for i, element := range value {
switch element := element.(type) { switch element := element.(type) {
case string: case string:
if text, ok := view.Session().resolveConstants(element); ok && text != "" { if text, ok := session.resolveConstants(element); ok && text != "" {
if !points[i].setValue(text) { if !points[i].setValue(text) {
ErrorLogF(`Invalid gradient point #%d: "%s"`, i, text) ErrorLogF(`Invalid gradient point #%d: "%s"`, i, text)
return false return false
@ -535,11 +534,10 @@ func (gradient *backgroundLinearGradient) Set(tag string, value interface{}) boo
return gradient.backgroundGradient.Set(tag, value) return gradient.backgroundGradient.Set(tag, value)
} }
func (gradient *backgroundLinearGradient) cssStyle(view View) string { func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
session := view.Session()
if repeating, _ := boolProperty(gradient, Repeating, session); repeating { if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-linear-gradient(`) buffer.WriteString(`repeating-linear-gradient(`)
} else { } else {
@ -581,7 +579,7 @@ func (gradient *backgroundLinearGradient) cssStyle(view View) string {
} }
} }
if !gradient.writeGradient(view, buffer) { if !gradient.writeGradient(session, buffer) {
return "" return ""
} }
@ -642,11 +640,10 @@ func (gradient *backgroundRadialGradient) Get(tag string) interface{} {
return gradient.backgroundGradient.Get(gradient.normalizeTag(tag)) return gradient.backgroundGradient.Get(gradient.normalizeTag(tag))
} }
func (gradient *backgroundRadialGradient) cssStyle(view View) string { func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
session := view.Session()
if repeating, _ := boolProperty(gradient, Repeating, session); repeating { if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-radial-gradient(`) buffer.WriteString(`repeating-radial-gradient(`)
} else { } else {
@ -706,7 +703,7 @@ func (gradient *backgroundRadialGradient) cssStyle(view View) string {
} }
buffer.WriteString(", ") buffer.WriteString(", ")
if !gradient.writeGradient(view, buffer) { if !gradient.writeGradient(session, buffer) {
return "" return ""
} }

View File

@ -218,6 +218,42 @@ func (builder *cssStyleBuilder) endStyle() {
builder.buffer.WriteString(`}\n`) builder.buffer.WriteString(`}\n`)
} }
func (builder *cssStyleBuilder) startAnimation(name string) {
if builder.buffer == nil {
builder.init()
}
builder.media = true
builder.buffer.WriteString(`\n@keyframes `)
builder.buffer.WriteString(name)
builder.buffer.WriteString(` {\n`)
}
func (builder *cssStyleBuilder) endAnimation() {
if builder.buffer == nil {
builder.init()
}
builder.buffer.WriteString(`}\n`)
builder.media = false
}
func (builder *cssStyleBuilder) startAnimationFrame(name string) {
if builder.buffer == nil {
builder.init()
}
builder.buffer.WriteString(`\t`)
builder.buffer.WriteString(name)
builder.buffer.WriteString(` {\n`)
}
func (builder *cssStyleBuilder) endAnimationFrame() {
if builder.buffer == nil {
builder.init()
}
builder.buffer.WriteString(`\t}\n`)
}
func (builder *cssStyleBuilder) add(key, value string) { func (builder *cssStyleBuilder) add(key, value string) {
if value != "" { if value != "" {
if builder.buffer == nil { if builder.buffer == nil {

View File

@ -259,3 +259,10 @@ func (customView *CustomViewData) setScroll(x, y, width, height float64) {
customView.superView.setScroll(x, y, width, height) customView.superView.setScroll(x, y, width, height)
} }
} }
func (customView *CustomViewData) getTransitions() Params {
if customView.superView != nil {
return customView.superView.getTransitions()
}
return Params{}
}

View File

@ -1,16 +0,0 @@
package main
import (
"github.com/anoshenko/rui"
)
const absoluteLayoutDemoText = `
AbsoluteLayout {
width = 100%, height = 100%,
content = [ View { id = view1, width = 32px, height = 32px, left = 100px, top = 200px, background-color = #FF0000FF } ]
}
`
func createAbsoluteLayoutDemo(session rui.Session) rui.View {
return rui.CreateViewFromText(session, absoluteLayoutDemoText)
}

142
demo/animationDemo.go Normal file
View File

@ -0,0 +1,142 @@
package main
import "github.com/anoshenko/rui"
const animationDemoText = `
GridLayout {
style = demoPage,
content = [
AbsoluteLayout {
id = animationContainer, width = 100%, height = 100%,
content = [
View {
id = animatedView1, width = 32px, height = 32px, left = 16px, top = 16px, background-color = #FF0000FF
}
]
},
ListLayout {
style = optionsPanel,
content = [
GridLayout {
style = optionsTable,
content = [
TextView { row = 0, text = "Duration" },
DropDownList { row = 0, column = 1, id = animationDuration, current = 0, items = ["4s", "8s", "12s"]},
TextView { row = 1, text = "Delay" },
DropDownList { row = 1, column = 1, id = animationDelay, current = 0, items = ["0s", "1s", "2s"]},
TextView { row = 2, text = "Timing function" },
DropDownList { row = 2, column = 1, id = animationTimingFunction, current = 0, items = ["ease", "linear", "steps(40)"]},
TextView { row = 3, text = "Iteration Count" },
DropDownList { row = 3, column = 1, id = animationIterationCount, current = 0, items = ["1", "3", "infinite"]},
TextView { row = 4, text = "Direction" },
DropDownList { row = 4, column = 1, id = animationDirection, current = 0, items = ["normal", "reverse", "alternate", "alternate-reverse"]},
Button { row = 5, column = 0:1, id = animationStart, content = Start },
Button { row = 6, column = 0:1, id = animationPause, content = Pause },
]
}
]
}
]
}`
func createAnimationDemo(session rui.Session) rui.View {
view := rui.CreateViewFromText(session, animationDemoText)
if view == nil {
return nil
}
rui.Set(view, "animationStart", rui.ClickEvent, func() {
frame := rui.GetViewFrame(view, "animationContainer")
prop1 := rui.AnimatedProperty{
Tag: rui.Left,
From: rui.Px(16),
To: rui.Px(16),
KeyFrames: map[int]interface{}{
25: rui.Px(frame.Width - 48),
50: rui.Px(frame.Width - 48),
75: rui.Px(16),
},
}
prop2 := rui.AnimatedProperty{
Tag: rui.Top,
From: rui.Px(16),
To: rui.Px(16),
KeyFrames: map[int]interface{}{
25: rui.Px(16),
50: rui.Px(frame.Height - 48),
75: rui.Px(frame.Height - 48),
},
}
prop3 := rui.AnimatedProperty{
Tag: rui.Rotate,
From: rui.Deg(0),
To: rui.Deg(360),
KeyFrames: map[int]interface{}{
25: rui.Deg(90),
50: rui.Deg(180),
75: rui.Deg(270),
},
}
params := rui.Params{
rui.PropertyTag: []rui.AnimatedProperty{prop1, prop2, prop3},
rui.Duration: rui.GetDropDownCurrent(view, "animationDuration") * 4,
rui.Delay: rui.GetDropDownCurrent(view, "animationDelay"),
rui.AnimationDirection: rui.GetDropDownCurrent(view, "animationDirection"),
}
switch rui.GetDropDownCurrent(view, "animationTimingFunction") {
case 0:
params[rui.TimingFunction] = rui.EaseTiming
case 1:
params[rui.TimingFunction] = rui.LinearTiming
case 2:
params[rui.TimingFunction] = rui.StepsTiming(40)
}
switch rui.GetDropDownCurrent(view, "animationIterationCount") {
case 0:
params[rui.IterationCount] = 1
case 1:
params[rui.IterationCount] = 3
case 2:
params[rui.IterationCount] = -1
}
rui.Set(view, "animatedView1", rui.AnimationTag, rui.NewAnimation(params))
})
rui.Set(view, "animationPause", rui.ClickEvent, func() {
if rui.IsAnimationPaused(view, "animatedView1") {
rui.Set(view, "animatedView1", rui.AnimationPaused, false)
rui.Set(view, "animationPause", rui.Content, "Pause")
} else {
rui.Set(view, "animatedView1", rui.AnimationPaused, true)
rui.Set(view, "animationPause", rui.Content, "Resume")
}
})
rui.Set(view, "animatedView1", rui.AnimationStartEvent, func() {
rui.Set(view, "animatedView1", rui.AnimationPaused, false)
rui.Set(view, "animationPause", rui.Content, "Pause")
})
rui.Set(view, "animatedView1", rui.AnimationEndEvent, func() {
rui.Set(view, "animatedView1", rui.AnimationPaused, false)
rui.Set(view, "animationPause", rui.Content, "Pause")
})
rui.Set(view, "animatedView1", rui.AnimationCancelEvent, func() {
rui.Set(view, "animatedView1", rui.AnimationPaused, false)
rui.Set(view, "animationPause", rui.Content, "Pause")
})
rui.Set(view, "animatedView1", rui.AnimationIterationEvent, func() {
})
return view
}

View File

@ -78,7 +78,6 @@ func createDemo(session rui.Session) rui.SessionContent {
{"GridLayout", createGridLayoutDemo, nil}, {"GridLayout", createGridLayoutDemo, nil},
{"ColumnLayout", createColumnLayoutDemo, nil}, {"ColumnLayout", createColumnLayoutDemo, nil},
{"StackLayout", createStackLayoutDemo, nil}, {"StackLayout", createStackLayoutDemo, nil},
{"AbsoluteLayout", createAbsoluteLayoutDemo, nil},
{"Resizable", createResizableDemo, nil}, {"Resizable", createResizableDemo, nil},
{"ListView", createListViewDemo, nil}, {"ListView", createListViewDemo, nil},
{"Checkbox", createCheckboxDemo, nil}, {"Checkbox", createCheckboxDemo, nil},
@ -93,6 +92,7 @@ func createDemo(session rui.Session) rui.SessionContent {
{"Filter", createFilterDemo, nil}, {"Filter", createFilterDemo, nil},
{"Clip", createClipDemo, nil}, {"Clip", createClipDemo, nil},
{"Transform", transformDemo, nil}, {"Transform", transformDemo, nil},
{"Animation", createAnimationDemo, nil},
{"Transition", createTransitionDemo, nil}, {"Transition", createTransitionDemo, nil},
{"Key events", createKeyEventsDemo, nil}, {"Key events", createKeyEventsDemo, nil},
{"Mouse events", createMouseEventsDemo, nil}, {"Mouse events", createMouseEventsDemo, nil},

View File

@ -43,24 +43,23 @@ func createTransitionDemo(session rui.Session) rui.View {
} }
rui.Set(view, "startTransition", rui.ClickEvent, func(button rui.View) { rui.Set(view, "startTransition", rui.ClickEvent, func(button rui.View) {
for id, timing := range bars { for id, timing := range bars {
animation := rui.NewAnimation(rui.Params{
rui.Duration: 2,
rui.TimingFunction: timing,
})
if bar := rui.ViewByID(view, id); bar != nil { if bar := rui.ViewByID(view, id); bar != nil {
if rui.GetWidth(bar, "").Value == 100 { if rui.GetWidth(bar, "").Value == 100 {
bar.SetAnimated(rui.Width, rui.Percent(20), rui.Animation{ bar.Remove(rui.TransitionEndEvent)
Duration: 2, bar.SetAnimated(rui.Width, rui.Percent(20), animation)
TimingFunction: timing,
})
} else { } else {
bar.SetAnimated(rui.Width, rui.Percent(100), rui.Animation{ bar.Set(rui.TransitionEndEvent, func(v rui.View, tag string) {
Duration: 2, bar.Remove(rui.TransitionEndEvent)
TimingFunction: timing, bar.SetAnimated(rui.Width, rui.Percent(20), animation)
FinishListener: rui.AnimationFinishedFunc(func(v rui.View, tag string) {
bar.SetAnimated(rui.Width, rui.Percent(20), rui.Animation{
Duration: 2,
TimingFunction: bars[v.ID()],
})
}),
}) })
bar.SetAnimated(rui.Width, rui.Percent(100), animation)
} }
} }
} }

View File

@ -54,6 +54,7 @@ var boolProperties = []string{
Controls, Controls,
Loop, Loop,
Muted, Muted,
AnimationPaused,
} }
var intProperties = []string{ var intProperties = []string{
@ -378,6 +379,11 @@ var enumProperties = map[string]struct {
"", "",
[]string{"to top", "to right top", "to right", "to right bottom", "to bottom", "to left bottom", "to left", "to left top"}, []string{"to top", "to right top", "to right", "to right bottom", "to bottom", "to left bottom", "to left", "to left top"},
}, },
AnimationDirection: {
[]string{"normal", "reverse", "alternate", "alternate-reverse"},
"",
[]string{"normal", "reverse", "alternate", "alternate-reverse"},
},
RadialGradientShape: { RadialGradientShape: {
[]string{"ellipse", "circle"}, []string{"ellipse", "circle"},
"", "",

View File

@ -53,6 +53,8 @@ type Session interface {
// a description of the error is written to the log // a description of the error is written to the log
Set(viewID, tag string, value interface{}) bool Set(viewID, tag string, value interface{}) bool
registerAnimation(props []AnimatedProperty) string
resolveConstants(value string) (string, bool) resolveConstants(value string) (string, bool)
checkboxOffImage() string checkboxOffImage() string
checkboxOnImage() string checkboxOnImage() string
@ -88,27 +90,28 @@ type Session interface {
} }
type sessionData struct { type sessionData struct {
customTheme *theme customTheme *theme
darkTheme bool darkTheme bool
touchScreen bool touchScreen bool
textDirection int textDirection int
pixelRatio float64 pixelRatio float64
language string language string
languages []string languages []string
checkboxOff string checkboxOff string
checkboxOn string checkboxOn string
radiobuttonOff string radiobuttonOff string
radiobuttonOn string radiobuttonOn string
app Application app Application
sessionID int sessionID int
viewCounter int viewCounter int
content SessionContent content SessionContent
rootView View rootView View
ignoreUpdates bool ignoreUpdates bool
popups *popupManager popups *popupManager
images *imageManager images *imageManager
brige WebBrige brige WebBrige
events chan DataObject events chan DataObject
animationCounter int
} }
func newSession(app Application, id int, customTheme string, params DataObject) Session { func newSession(app Application, id int, customTheme string, params DataObject) Session {
@ -122,6 +125,7 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.languages = []string{} session.languages = []string{}
session.viewCounter = 0 session.viewCounter = 0
session.ignoreUpdates = false session.ignoreUpdates = false
session.animationCounter = 0
if customTheme != "" { if customTheme != "" {
if theme, ok := newTheme(customTheme); ok { if theme, ok := newTheme(customTheme); ok {

View File

@ -55,11 +55,11 @@ func (layout *stackLayoutData) Init(session Session) {
layout.viewsContainerData.Init(session) layout.viewsContainerData.Init(session)
layout.tag = "StackLayout" layout.tag = "StackLayout"
layout.systemClass = "ruiStackLayout" layout.systemClass = "ruiStackLayout"
layout.properties[TransitionEndEvent] = []func(View, string){layout.pushFinished, layout.popFinished}
} }
func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) { func (layout *stackLayoutData) pushFinished(view View, tag string) {
switch tag { if tag == "ruiPush" {
case "ruiPush":
if layout.pushView != nil { if layout.pushView != nil {
layout.pushView = nil layout.pushView = nil
count := len(layout.views) count := len(layout.views)
@ -70,13 +70,17 @@ func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) {
} }
updateInnerHTML(layout.htmlID(), layout.session) updateInnerHTML(layout.htmlID(), layout.session)
} }
if layout.onPushFinished != nil { if layout.onPushFinished != nil {
onPushFinished := layout.onPushFinished onPushFinished := layout.onPushFinished
layout.onPushFinished = nil layout.onPushFinished = nil
onPushFinished() onPushFinished()
} }
}
}
case "ruiPop": func (layout *stackLayoutData) popFinished(view View, tag string) {
if tag == "ruiPop" {
popView := layout.popView popView := layout.popView
layout.popView = nil layout.popView = nil
updateInnerHTML(layout.htmlID(), layout.session) updateInnerHTML(layout.htmlID(), layout.session)
@ -88,6 +92,27 @@ func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) {
} }
} }
func (layout *stackLayoutData) Set(tag string, value interface{}) bool {
if strings.ToLower(tag) == TransitionEndEvent {
listeners, ok := valueToAnimationListeners(value)
if ok {
listeners = append(listeners, layout.pushFinished)
listeners = append(listeners, layout.popFinished)
layout.properties[TransitionEndEvent] = listeners
}
return ok
}
return layout.viewsContainerData.Set(tag, value)
}
func (layout *stackLayoutData) Remove(tag string) {
if strings.ToLower(tag) == TransitionEndEvent {
layout.properties[TransitionEndEvent] = []func(View, string){layout.pushFinished, layout.popFinished}
} else {
layout.viewsContainerData.Remove(tag)
}
}
func (layout *stackLayoutData) Peek() View { func (layout *stackLayoutData) Peek() View {
if int(layout.peek) < len(layout.views) { if int(layout.peek) < len(layout.views) {
return layout.views[layout.peek] return layout.views[layout.peek]
@ -174,7 +199,7 @@ func (layout *stackLayoutData) Push(view View, animation int, onPushFinished fun
layout.pushView = view layout.pushView = view
layout.animationType = animation layout.animationType = animation
layout.animation["ruiPush"] = Animation{FinishListener: layout} //layout.animation["ruiPush"] = Animation{FinishListener: layout}
layout.onPushFinished = onPushFinished layout.onPushFinished = onPushFinished
htmlID := layout.htmlID() htmlID := layout.htmlID()
@ -226,7 +251,7 @@ func (layout *stackLayoutData) Pop(animation int, onPopFinished func(View)) bool
layout.RemoveView(layout.peek) layout.RemoveView(layout.peek)
layout.animationType = animation layout.animationType = animation
layout.animation["ruiPop"] = Animation{FinishListener: layout} //layout.animation["ruiPop"] = Animation{FinishListener: layout}
layout.onPopFinished = onPopFinished layout.onPopFinished = onPopFinished
htmlID := layout.htmlID() htmlID := layout.htmlID()

View File

@ -804,7 +804,7 @@ func (table *tableViewData) getCellBorder() BorderProperty {
} }
func (table *tableViewData) cssStyle(self View, builder cssBuilder) { func (table *tableViewData) cssStyle(self View, builder cssBuilder) {
table.viewData.cssViewStyle(builder, table.Session(), self) table.viewData.cssViewStyle(builder, table.Session())
gap, ok := sizeProperty(table, Gap, table.Session()) gap, ok := sizeProperty(table, Gap, table.Session())
if !ok || gap.Type == Auto || gap.Value <= 0 { if !ok || gap.Type == Auto || gap.Value <= 0 {
@ -834,7 +834,7 @@ func (cell *tableCellView) set(tag string, value interface{}) bool {
func (cell *tableCellView) cssStyle(self View, builder cssBuilder) { func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
session := cell.Session() session := cell.Session()
cell.viewData.cssViewStyle(builder, session, self) cell.viewData.cssViewStyle(builder, session)
if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok { if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok {
builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value]) builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value])

View File

@ -191,7 +191,7 @@ func (theme *theme) cssText(session Session) string {
style.init() style.init()
parseProperties(&style, obj) parseProperties(&style, obj)
builder.startStyle(tag) builder.startStyle(tag)
style.cssViewStyle(&builder, session, nil) style.cssViewStyle(&builder, session)
builder.endStyle() builder.endStyle()
} }
@ -202,7 +202,7 @@ func (theme *theme) cssText(session Session) string {
style.init() style.init()
parseProperties(&style, obj) parseProperties(&style, obj)
builder.startStyle(tag) builder.startStyle(tag)
style.cssViewStyle(&builder, session, nil) style.cssViewStyle(&builder, session)
builder.endStyle() builder.endStyle()
} }
builder.endMedia() builder.endMedia()

84
view.go
View File

@ -82,6 +82,8 @@ type View interface {
cssStyle(self View, builder cssBuilder) cssStyle(self View, builder cssBuilder)
addToCSSStyle(addCSS map[string]string) addToCSSStyle(addCSS map[string]string)
getTransitions() Params
onResize(self View, x, y, width, height float64) onResize(self View, x, y, width, height float64)
onItemResize(self View, index int, x, y, width, height float64) onItemResize(self View, index int, x, y, width, height float64)
setNoResizeEvent() setNoResizeEvent()
@ -92,18 +94,18 @@ type View interface {
// viewData - base implementation of View interface // viewData - base implementation of View interface
type viewData struct { type viewData struct {
viewStyle viewStyle
session Session session Session
tag string tag string
viewID string viewID string
_htmlID string _htmlID string
parentID string parentID string
systemClass string systemClass string
animation map[string]Animation singleTransition map[string]Animation
addCSS map[string]string addCSS map[string]string
frame Frame frame Frame
scroll Frame scroll Frame
noResizeEvent bool noResizeEvent bool
created bool created bool
//animation map[string]AnimationEndListener //animation map[string]AnimationEndListener
} }
@ -142,7 +144,7 @@ func (view *viewData) Init(session Session) {
view.session = session view.session = session
view.addCSS = map[string]string{} view.addCSS = map[string]string{}
//view.animation = map[string]AnimationEndListener{} //view.animation = map[string]AnimationEndListener{}
view.animation = map[string]Animation{} view.singleTransition = map[string]Animation{}
view.noResizeEvent = false view.noResizeEvent = false
view.created = false view.created = false
} }
@ -215,6 +217,12 @@ func (view *viewData) remove(tag string) {
case TouchStart, TouchEnd, TouchMove, TouchCancel: case TouchStart, TouchEnd, TouchMove, TouchCancel:
view.removeTouchListener(tag) view.removeTouchListener(tag)
case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent:
view.removeTransitionListener(tag)
case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent:
view.removeAnimationListener(tag)
case ResizeEvent, ScrollEvent: case ResizeEvent, ScrollEvent:
delete(view.properties, tag) delete(view.properties, tag)
@ -276,6 +284,12 @@ func (view *viewData) set(tag string, value interface{}) bool {
case TouchStart, TouchEnd, TouchMove, TouchCancel: case TouchStart, TouchEnd, TouchMove, TouchCancel:
return view.setTouchListener(tag, value) return view.setTouchListener(tag, value)
case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent:
return view.setTransitionListener(tag, value)
case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent:
return view.setAnimationListener(tag, value)
case ResizeEvent, ScrollEvent: case ResizeEvent, ScrollEvent:
return view.setFrameListener(tag, value) return view.setFrameListener(tag, value)
} }
@ -304,7 +318,7 @@ func (view *viewData) propertyChanged(tag string) {
updateInnerHTML(view.parentHTMLID(), session) updateInnerHTML(view.parentHTMLID(), session)
case Background: case Background:
updateCSSProperty(htmlID, Background, view.backgroundCSS(view), session) updateCSSProperty(htmlID, Background, view.backgroundCSS(session), session)
return return
case Border: case Border:
@ -428,6 +442,7 @@ func (view *viewData) propertyChanged(tag string) {
} else { } else {
updateCSSProperty(htmlID, "font-style", "", session) updateCSSProperty(htmlID, "font-style", "", session)
} }
return
case SmallCaps: case SmallCaps:
if state, ok := boolProperty(view, tag, session); ok { if state, ok := boolProperty(view, tag, session); ok {
@ -439,13 +454,33 @@ func (view *viewData) propertyChanged(tag string) {
} else { } else {
updateCSSProperty(htmlID, "font-variant", "", session) updateCSSProperty(htmlID, "font-variant", "", session)
} }
return
case Strikethrough, Overline, Underline: case Strikethrough, Overline, Underline:
updateCSSProperty(htmlID, "text-decoration", view.cssTextDecoration(session), session) updateCSSProperty(htmlID, "text-decoration", view.cssTextDecoration(session), session)
for _, tag2 := range []string{TextLineColor, TextLineStyle, TextLineThickness} { for _, tag2 := range []string{TextLineColor, TextLineStyle, TextLineThickness} {
view.propertyChanged(tag2) view.propertyChanged(tag2)
} }
return
case Transition:
view.updateTransitionCSS()
return
case AnimationTag:
updateCSSProperty(htmlID, AnimationTag, view.animationCSS(session), session)
return
case AnimationPaused:
paused, ok := boolProperty(view, AnimationPaused, session)
if !ok {
updateCSSProperty(htmlID, `animation-play-state`, ``, session)
} else if paused {
updateCSSProperty(htmlID, `animation-play-state`, `paused`, session)
} else {
updateCSSProperty(htmlID, `animation-play-state`, `running`, session)
}
return
} }
if cssTag, ok := sizeProperties[tag]; ok { if cssTag, ok := sizeProperties[tag]; ok {
@ -521,7 +556,7 @@ func (view *viewData) addToCSSStyle(addCSS map[string]string) {
} }
func (view *viewData) cssStyle(self View, builder cssBuilder) { func (view *viewData) cssStyle(self View, builder cssBuilder) {
view.viewStyle.cssViewStyle(builder, view.session, self) view.viewStyle.cssViewStyle(builder, view.session)
switch GetVisibility(view, "") { switch GetVisibility(view, "") {
case Invisible: case Invisible:
builder.add(`visibility`, `hidden`) builder.add(`visibility`, `hidden`)
@ -600,6 +635,8 @@ func viewHTML(view View, buffer *strings.Builder) {
pointerEventsHtml(view, buffer) pointerEventsHtml(view, buffer)
touchEventsHtml(view, buffer) touchEventsHtml(view, buffer)
focusEventsHtml(view, buffer) focusEventsHtml(view, buffer)
transitionEventsHtml(view, buffer)
animationEventsHtml(view, buffer)
buffer.WriteRune('>') buffer.WriteRune('>')
view.htmlSubviews(view, buffer) view.htmlSubviews(view, buffer)
@ -654,6 +691,12 @@ func (view *viewData) handleCommand(self View, command string, data DataObject)
listener(self) listener(self)
} }
case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent:
view.handleTransitionEvents(command, data)
case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent:
view.handleAnimationEvents(command, data)
case "scroll": case "scroll":
view.onScroll(view, dataFloatProperty(data, "x"), dataFloatProperty(data, "y"), dataFloatProperty(data, "width"), dataFloatProperty(data, "height")) view.onScroll(view, dataFloatProperty(data, "x"), dataFloatProperty(data, "y"), dataFloatProperty(data, "width"), dataFloatProperty(data, "height"))
@ -671,17 +714,6 @@ func (view *viewData) handleCommand(self View, command string, data DataObject)
} }
} }
case "transitionEnd":
if property, ok := data.PropertyValue("property"); ok {
if animation, ok := view.animation[property]; ok {
delete(view.animation, property)
view.updateTransitionCSS()
if animation.FinishListener != nil {
animation.FinishListener.OnAnimationFinished(self, property)
}
}
return true
}
/* /*
case "resize": case "resize":
floatProperty := func(tag string) float64 { floatProperty := func(tag string) float64 {

View File

@ -1,170 +0,0 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
const (
// 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"
)
// StepsTiming return a timing function along stepCount stops along the transition, diplaying each stop for equal lengths of time
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)
}
// AnimationFinishedListener describes the end of an animation event handler
type AnimationFinishedListener interface {
// OnAnimationFinished is called when a property animation is finished
OnAnimationFinished(view View, property string)
}
type Animation struct {
// Duration defines the time in seconds an animation should take to complete
Duration float64
// TimingFunction defines how intermediate values are calculated for a property being affected
// by an animation effect. If the value is "" then the "ease" function is used
TimingFunction string
// Delay defines the duration in seconds to wait before starting a property's animation.
Delay float64
// FinishListener defines the end of an animation event handler
FinishListener AnimationFinishedListener
}
type animationFinishedFunc struct {
finishFunc func(View, string)
}
func (listener animationFinishedFunc) OnAnimationFinished(view View, property string) {
if listener.finishFunc != nil {
listener.finishFunc(view, property)
}
}
func AnimationFinishedFunc(finishFunc func(View, string)) AnimationFinishedListener {
listener := new(animationFinishedFunc)
listener.finishFunc = finishFunc
return listener
}
func validateTimingFunction(timingFunction string) bool {
switch timingFunction {
case "", EaseTiming, EaseInTiming, EaseOutTiming, EaseInOutTiming, LinearTiming:
return true
}
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
}
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
}
}
return true
}
}
}
}
return false
}
func (view *viewData) SetAnimated(tag string, value interface{}, animation Animation) bool {
timingFunction, ok := view.session.resolveConstants(animation.TimingFunction)
if !ok || animation.Duration <= 0 || !validateTimingFunction(timingFunction) {
if view.Set(tag, value) {
if animation.FinishListener != nil {
animation.FinishListener.OnAnimationFinished(view, tag)
}
return true
}
return false
}
updateProperty(view.htmlID(), "ontransitionend", "transitionEndEvent(this, event)", view.session)
updateProperty(view.htmlID(), "ontransitioncancel", "transitionCancelEvent(this, event)", view.session)
animation.TimingFunction = timingFunction
view.animation[tag] = animation
view.updateTransitionCSS()
result := view.Set(tag, value)
if !result {
delete(view.animation, tag)
view.updateTransitionCSS()
}
return result
}
func (view *viewData) transitionCSS() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for tag, animation := range view.animation {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(fmt.Sprintf(" %gs", animation.Duration))
if animation.TimingFunction != "" {
buffer.WriteRune(' ')
buffer.WriteString(animation.TimingFunction)
}
if animation.Delay > 0 {
if animation.TimingFunction == "" {
buffer.WriteString(" ease")
}
buffer.WriteString(fmt.Sprintf(" %gs", animation.Delay))
}
}
return buffer.String()
}
func (view *viewData) updateTransitionCSS() {
updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(), view.Session())
}
// SetAnimated sets the property with name "tag" of the "rootView" subview with "viewID" id by value. Result:
// true - success,
// false - error (incompatible type or invalid format of a string value, see AppLog).
func SetAnimated(rootView View, viewID, tag string, value interface{}, animation Animation) bool {
if view := ViewByID(rootView, viewID); view != nil {
return view.SetAnimated(tag, value, animation)
}
return false
}

View File

@ -9,12 +9,12 @@ import (
// ViewStyle interface of the style of view // ViewStyle interface of the style of view
type ViewStyle interface { type ViewStyle interface {
Properties Properties
cssViewStyle(buffer cssBuilder, session Session, view View) cssViewStyle(buffer cssBuilder, session Session)
} }
type viewStyle struct { type viewStyle struct {
propertyList propertyList
//transitions map[string]ViewTransition transitions map[string]Animation
} }
// Range defines range limits. The First and Last value are included in the range // Range defines range limits. The First and Last value are included in the range
@ -60,7 +60,7 @@ func (r *Range) setValue(value string) bool {
func (style *viewStyle) init() { func (style *viewStyle) init() {
style.propertyList.init() style.propertyList.init()
//style.shadows = []ViewShadow{} //style.shadows = []ViewShadow{}
//style.transitions = map[string]ViewTransition{} style.transitions = map[string]Animation{}
} }
// NewViewStyle create new ViewStyle object // NewViewStyle create new ViewStyle object
@ -132,14 +132,14 @@ func split4Values(text string) []string {
return []string{} return []string{}
} }
func (style *viewStyle) backgroundCSS(view View) string { func (style *viewStyle) backgroundCSS(session Session) string {
if value, ok := style.properties[Background]; ok { if value, ok := style.properties[Background]; ok {
if backgrounds, ok := value.([]BackgroundElement); ok { if backgrounds, ok := value.([]BackgroundElement); ok {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
for _, background := range backgrounds { for _, background := range backgrounds {
if value := background.cssStyle(view); value != "" { if value := background.cssStyle(session); value != "" {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteString(", ") buffer.WriteString(", ")
} }
@ -155,7 +155,7 @@ func (style *viewStyle) backgroundCSS(view View) string {
return "" return ""
} }
func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session, view View) { func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
if margin, ok := boundsProperty(style, Margin, session); ok { if margin, ok := boundsProperty(style, Margin, session); ok {
margin.cssValue(Margin, builder) margin.cssValue(Margin, builder)
@ -219,7 +219,7 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session, view V
builder.add(BackgroundClip, enumProperties[BackgroundClip].values[value]) builder.add(BackgroundClip, enumProperties[BackgroundClip].values[value])
} }
if background := style.backgroundCSS(view); background != "" { if background := style.backgroundCSS(session); background != "" {
builder.add("background", background) builder.add("background", background)
} }
@ -399,24 +399,20 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session, view V
} }
} }
} }
/*
if len(style.transitions) > 0 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for property, transition := range style.transitions { if transition := style.transitionCSS(session); transition != "" {
if buffer.Len() > 0 { builder.add(`transition`, transition)
buffer.WriteString(`, `) }
}
buffer.WriteString(property)
transition.cssWrite(buffer, session)
}
if buffer.Len() > 0 { if animation := style.animationCSS(session); animation != "" {
builder.add(`transition`, buffer.String()) builder.add(AnimationTag, animation)
} }
if pause, ok := boolProperty(style, AnimationPaused, session); ok {
if pause {
builder.add(`animation-play-state`, `paused`)
} else {
builder.add(`animation-play-state`, `running`)
} }
*/ }
// TODO text-shadow
} }

View File

@ -267,6 +267,101 @@ func (style *viewStyle) set(tag string, value interface{}) bool {
case Filter: case Filter:
return style.setFilter(value) return style.setFilter(value)
case Transition:
setObject := func(obj DataObject) bool {
if obj != nil {
switch obj.Tag() {
case "", "_":
ErrorLog("Invalid transition property name")
default:
style.transitions[obj.Tag()] = parseAnimation(obj)
return true
}
}
return false
}
switch value := value.(type) {
case Params:
result := false
for tag, val := range value {
if animation, ok := val.(Animation); ok {
tag = strings.ToLower(tag)
if animation == nil || tag == "" {
ErrorLog("Invalid transition property name")
} else {
style.transitions[tag] = animation
result = true
}
} else {
notCompatibleType(Transition, val)
}
}
return result
case DataObject:
return setObject(value)
case DataNode:
switch value.Type() {
case ObjectNode:
return setObject(value.Object())
case ArrayNode:
result := true
for i := 0; i < value.ArraySize(); i++ {
if obj := value.ArrayElement(i).Object(); obj != nil {
result = setObject(obj) && result
} else {
notCompatibleType(tag, value.ArrayElement(i))
result = false
}
}
return result
}
}
notCompatibleType(tag, value)
return false
case AnimationTag:
switch value := value.(type) {
case Animation:
style.properties[tag] = []Animation{value}
return true
case []Animation:
style.properties[tag] = value
return true
case DataObject:
if animation := parseAnimation(value); animation.hasAnimatedPropery() {
style.properties[tag] = []Animation{animation}
return true
}
case DataNode:
animations := []Animation{}
result := true
for i := 0; i < value.ArraySize(); i++ {
if obj := value.ArrayElement(i).Object(); obj != nil {
if anim := parseAnimation(obj); anim.hasAnimatedPropery() {
animations = append(animations, anim)
} else {
result = false
}
} else {
notCompatibleType(tag, value.ArrayElement(i))
result = false
}
}
if result && len(animations) > 0 {
style.properties[tag] = animations
}
return result
}
} }
return style.propertyList.set(tag, value) return style.propertyList.set(tag, value)

View File

@ -91,10 +91,10 @@ func getOrigin(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit)
return x, y, z return x, y, z
} }
func getSkew(style Properties, session Session) (AngleUnit, AngleUnit) { func getSkew(style Properties, session Session) (AngleUnit, AngleUnit, bool) {
skewX, _ := angleProperty(style, SkewX, session) skewX, okX := angleProperty(style, SkewX, session)
skewY, _ := angleProperty(style, SkewY, session) skewY, okY := angleProperty(style, SkewY, session)
return skewX, skewY return skewX, skewY, okX || okY
} }
func getTranslate(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) { func getTranslate(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) {
@ -104,19 +104,18 @@ func getTranslate(style Properties, session Session) (SizeUnit, SizeUnit, SizeUn
return x, y, z return x, y, z
} }
func getScale(style Properties, session Session) (float64, float64, float64) { func getScale(style Properties, session Session) (float64, float64, float64, bool) {
scaleX, _ := floatProperty(style, ScaleX, session, 1) scaleX, okX := floatProperty(style, ScaleX, session, 1)
scaleY, _ := floatProperty(style, ScaleY, session, 1) scaleY, okY := floatProperty(style, ScaleY, session, 1)
scaleZ, _ := floatProperty(style, ScaleZ, session, 1) scaleZ, okZ := floatProperty(style, ScaleZ, session, 1)
return scaleX, scaleY, scaleZ return scaleX, scaleY, scaleZ, okX || okY || okZ
} }
func getRotate(style Properties, session Session) (float64, float64, float64, AngleUnit) { func getRotateVector(style Properties, session Session) (float64, float64, float64) {
rotateX, _ := floatProperty(style, RotateX, session, 1) rotateX, _ := floatProperty(style, RotateX, session, 1)
rotateY, _ := floatProperty(style, RotateY, session, 1) rotateY, _ := floatProperty(style, RotateY, session, 1)
rotateZ, _ := floatProperty(style, RotateZ, session, 1) rotateZ, _ := floatProperty(style, RotateZ, session, 1)
angle, _ := angleProperty(style, Rotate, session) return rotateX, rotateY, rotateZ
return rotateX, rotateY, rotateZ, angle
} }
func (style *viewStyle) transform(session Session) string { func (style *viewStyle) transform(session Session) string {
@ -124,8 +123,8 @@ func (style *viewStyle) transform(session Session) string {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
skewX, skewY := getSkew(style, session) skewX, skewY, skewOK := getSkew(style, session)
if skewX.Value != 0 || skewY.Value != 0 { if skewOK {
buffer.WriteString(`skew(`) buffer.WriteString(`skew(`)
buffer.WriteString(skewX.cssString()) buffer.WriteString(skewX.cssString())
buffer.WriteRune(',') buffer.WriteRune(',')
@ -134,9 +133,9 @@ func (style *viewStyle) transform(session Session) string {
} }
x, y, z := getTranslate(style, session) x, y, z := getTranslate(style, session)
scaleX, scaleY, scaleZ := getScale(style, session) scaleX, scaleY, scaleZ, scaleOK := getScale(style, session)
if getTransform3D(style, session) { if getTransform3D(style, session) {
if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) || (z.Type != Auto && z.Value != 0) { if x.Type != Auto || y.Type != Auto || z.Type != Auto {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteRune(' ') buffer.WriteRune(' ')
} }
@ -149,7 +148,7 @@ func (style *viewStyle) transform(session Session) string {
buffer.WriteRune(')') buffer.WriteRune(')')
} }
if scaleX != 1 || scaleY != 1 || scaleZ != 1 { if scaleOK {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteRune(' ') buffer.WriteRune(' ')
} }
@ -162,8 +161,8 @@ func (style *viewStyle) transform(session Session) string {
buffer.WriteRune(')') buffer.WriteRune(')')
} }
rotateX, rotateY, rotateZ, angle := getRotate(style, session) if angle, ok := angleProperty(style, Rotate, session); ok {
if angle.Value != 0 && (rotateX != 0 || rotateY != 0 || rotateZ != 0) { rotateX, rotateY, rotateZ := getRotateVector(style, session)
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteRune(' ') buffer.WriteRune(' ')
} }
@ -177,8 +176,9 @@ func (style *viewStyle) transform(session Session) string {
buffer.WriteString(angle.cssString()) buffer.WriteString(angle.cssString())
buffer.WriteRune(')') buffer.WriteRune(')')
} }
} else { } else {
if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) { if x.Type != Auto || y.Type != Auto {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteRune(' ') buffer.WriteRune(' ')
} }
@ -189,7 +189,7 @@ func (style *viewStyle) transform(session Session) string {
buffer.WriteRune(')') buffer.WriteRune(')')
} }
if scaleX != 1 || scaleY != 1 { if scaleOK {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteRune(' ') buffer.WriteRune(' ')
} }
@ -200,8 +200,7 @@ func (style *viewStyle) transform(session Session) string {
buffer.WriteRune(')') buffer.WriteRune(')')
} }
angle, _ := angleProperty(style, Rotate, session) if angle, ok := angleProperty(style, Rotate, session); ok {
if angle.Value != 0 {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteRune(' ') buffer.WriteRune(' ')
} }

View File

@ -854,7 +854,8 @@ func GetSkew(view View, subviewID string) (AngleUnit, AngleUnit) {
if view == nil { if view == nil {
return AngleUnit{Value: 0, Type: Radian}, AngleUnit{Value: 0, Type: Radian} return AngleUnit{Value: 0, Type: Radian}, AngleUnit{Value: 0, Type: Radian}
} }
return getSkew(view, view.Session()) x, y, _ := getSkew(view, view.Session())
return x, y
} }
// GetScale returns a x-, y-, and z-axis scaling value of a 2D/3D scale. The default value is 1. // GetScale returns a x-, y-, and z-axis scaling value of a 2D/3D scale. The default value is 1.
@ -866,7 +867,8 @@ func GetScale(view View, subviewID string) (float64, float64, float64) {
if view == nil { if view == nil {
return 1, 1, 1 return 1, 1, 1
} }
return getScale(view, view.Session()) x, y, z, _ := getScale(view, view.Session())
return x, y, z
} }
// GetRotate returns a x-, y, z-coordinate of the vector denoting the axis of rotation, and the angle of the view rotation // GetRotate returns a x-, y, z-coordinate of the vector denoting the axis of rotation, and the angle of the view rotation
@ -878,7 +880,10 @@ func GetRotate(view View, subviewID string) (float64, float64, float64, AngleUni
if view == nil { if view == nil {
return 0, 0, 0, AngleUnit{Value: 0, Type: Radian} return 0, 0, 0, AngleUnit{Value: 0, Type: Radian}
} }
return getRotate(view, view.Session())
angle, _ := angleProperty(view, Rotate, view.Session())
rotateX, rotateY, rotateZ := getRotateVector(view, view.Session())
return rotateX, rotateY, rotateZ, angle
} }
// GetAvoidBreak returns "true" if avoids any break from being inserted within the principal box, // GetAvoidBreak returns "true" if avoids any break from being inserted within the principal box,