From be954df7c79f6532f266ee1bb4241cc3176f517e Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko Date: Mon, 4 Oct 2021 17:58:17 +0300 Subject: [PATCH] Added "animation" and "transition" properties --- README-ru.md | 292 ++++++++++++- README.md | 292 ++++++++++++- animation.go | 821 +++++++++++++++++++++++++++++-------- animationEvents.go | 375 +++++++++++++++++ app_scripts.js | 60 ++- background.go | 21 +- cssBuilder.go | 36 ++ customView.go | 7 + demo/absoluteLayoutDemo.go | 16 - demo/animationDemo.go | 142 +++++++ demo/main.go | 2 +- demo/transitionDemo.go | 25 +- propertySet.go | 6 + session.go | 46 ++- stackLayout.go | 37 +- tableView.go | 4 +- theme.go | 4 +- view.go | 84 ++-- viewAnimation.go | 170 -------- viewStyle.go | 44 +- viewStyleSet.go | 95 +++++ viewTransform.go | 45 +- viewUtils.go | 11 +- 23 files changed, 2141 insertions(+), 494 deletions(-) create mode 100644 animationEvents.go delete mode 100644 demo/absoluteLayoutDemo.go create mode 100644 demo/animationDemo.go delete mode 100644 viewAnimation.go diff --git a/README-ru.md b/README-ru.md index 32228f2..e7d9add 100644 --- a/README-ru.md +++ b/README-ru.md @@ -1792,7 +1792,7 @@ radius необходимо передать nil func GetDoubleClickListeners(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 +## Анимация + +Библиотека поддерживает два вида анимации: + +* Анимированое изменения значения свойства (далее "анимация перехода") +* Сценарий анимированного изменения одного или нескольких свойств (далее просто "сценарий анимации") + +### Интерфейс 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ценарий анимации + +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. diff --git a/README.md b/README.md index 6909455..26ccf71 100644 --- a/README.md +++ b/README.md @@ -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 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, 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 +## 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 When a client creates a connection to a server, a Session interface is created for that connection. diff --git a/animation.go b/animation.go index 2b3fcee..1cc134b 100644 --- a/animation.go +++ b/animation.go @@ -1,229 +1,710 @@ package rui -/* import ( "fmt" + "math" "strconv" + "strings" ) -type AnimationTags struct { - Tag string - Start, End interface{} +const ( + // AnimationTag is the constant for the "animation" property tag. + // The "animation" property sets and starts animations. + // Valid types of value are []Animation and Animation + AnimationTag = "animation" + + // AnimationPause is the constant for the "animation-pause" property tag. + // The "animation-pause" property sets whether an animation is running or paused. + AnimationPaused = "animation-paused" + + // TransitionTag is the constant for the "transition" property tag. + // The "transition" property sets transition animation of view properties. + // Valid type of "transition" property value is Params. Valid type of Params value is Animation. + Transition = "transition" + + // PropertyTag is the constant for the "property" animation property tag. + // The "property" property describes a scenario for changing a View property. + // Valid types of value are []AnimatedProperty and AnimatedProperty + PropertyTag = "property" + + // Duration is the constant for the "duration" animation property tag. + // The "duration" float property sets the length of time in seconds that an animation takes to complete one cycle. + Duration = "duration" + + // Delay is the constant for the "delay" animation property tag. + // The "delay" float property specifies the amount of time in seconds to wait from applying + // the animation to an element before beginning to perform the animation. The animation can start later, + // immediately from its beginning, or immediately and partway through the animation. + Delay = "delay" + + // TimingFunction is the constant for the "timing-function" animation property tag. + // The "timing-function" property sets how an animation progresses through the duration of each cycle. + TimingFunction = "timing-function" + + // IterationCount is the constant for the "iteration-count" animation property tag. + // The "iteration-count" int property sets the number of times an animation sequence + // should be played before stopping. + IterationCount = "iteration-count" + + // AnimationDirection is the constant for the "animation-direction" animation property tag. + //The "animation-direction" property sets whether an animation should play forward, backward, + // or alternate back and forth between playing the sequence forward and backward. + AnimationDirection = "animation-direction" + + // NormalAnimation is value of the "animation-direction" property. + // The animation plays forwards each cycle. In other words, each time the animation cycles, + // the animation will reset to the beginning state and start over again. This is the default value. + NormalAnimation = 0 + // 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 { - KeyFrame int - TimingFunction string - Params Params +// 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) } -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 ruiStringer - Name() string - cssString(session Session) string + animationCSS(session Session) string + transitionCSS(buffer *strings.Builder, session Session) + hasAnimatedPropery() bool + animationName() string } -type animationScenario struct { - name string - tags []AnimationTags - keyFrames []AnimationKeyFrame - cssText string -} +func parseAnimation(obj DataObject) Animation { + animation := new(animationData) + animation.init() -var animationScenarios = []string{} - -func addAnimationScenario(name string) string { - animationScenarios = append(animationScenarios, name) - return name -} - -func registerAnimationScenario() string { - find := func(text string) bool { - for _, scenario := range animationScenarios { - if scenario == text { - return true + for i := 0; i < obj.PropertyCount(); i++ { + if node := obj.Property(i); node != nil { + if node.Type() == TextNode { + animation.Set(node.Tag(), node.Text()) + } else { + animation.Set(node.Tag(), node) } } - return 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 } -func (animation *animationScenario) Name() string { - return animation.name +func NewAnimation(params Params) Animation { + 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() animation.ruiString(writer) return writer.finish() } -func (animation *animationScenario) ruiString(writer ruiWriter) { +func (animation *animationData) ruiString(writer ruiWriter) { + writer.startObject("animation") // TODO + writer.endObject() } -func valueToCSS(tag string, value interface{}, session Session) string { - if value == nil { - return "" - } - - convertFloat := func(val float64) string { - if _, ok := sizeProperties[tag]; ok { - return fmt.Sprintf("%gpx", val) +func (animation *animationData) animationCSS(session Session) string { + if animation.keyFramesName == "" { + props := animation.getRaw(PropertyTag) + if props == nil { + ErrorLog("There are no animated properties.") + return "" } - return fmt.Sprintf("%g", val) - } - switch value := value.(type) { - case string: - value, ok := session.resolveConstants(value) + animatedProps, ok := props.([]AnimatedProperty) if !ok { + ErrorLog("Invalid animated properties.") return "" } - if _, ok := sizeProperties[tag]; ok { - var size SizeUnit - if size.SetValue(value) { - return size.cssString("auto") - } - return "" + + animation.keyFramesName = session.registerAnimation(animatedProps) + } + + buffer := allocStringBuilder() + 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) { - var color Color - if color.SetValue(value) { - 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 + buffer.WriteString(fmt.Sprintf(" %d ", iterationCount)) + } else { + buffer.WriteString(" infinite ") + } - case SizeUnit: - return value.cssString("auto") + direction, _ := enumProperty(animation, AnimationDirection, session, 0) + values := enumProperties[AnimationDirection].cssValues + if direction < 0 || direction >= len(values) { + direction = 0 + } + buffer.WriteString(values[direction]) - case AngleUnit: - return value.cssString() + // TODO "animation-fill-mode" + buffer.WriteString(" forwards") - case Color: - return value.cssString() + return buffer.String() +} - case float32: - return convertFloat(float64(value)) +func (animation *animationData) transitionCSS(buffer *strings.Builder, session Session) { - case float64: - return convertFloat(value) + if duration, _ := floatProperty(animation, Duration, session, 1); duration > 0 { + buffer.WriteString(fmt.Sprintf(" %gs ", duration)) + } else { + buffer.WriteString(" 1s ") + } - default: - 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 "" - } + buffer.WriteString(animation.timingFunctionCSS(session)) - 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 "" } -func (animation *animationScenario) cssString(session Session) string { - if animation.cssText != "" { +func (style *viewStyle) transitionCSS(session Session) string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) - buffer := allocStringBuilder() - defer freeStringBuilder(buffer) - - 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") - } + for tag, animation := range style.transitions { + if buffer.Len() > 0 { + buffer.WriteString(", ") } + buffer.WriteString(tag) + animation.transitionCSS(buffer, session) + } + return buffer.String() +} - buffer.WriteString(`@keyframes `) - buffer.WriteString(animation.name) +func (view *viewData) updateTransitionCSS() { + updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(view.Session()), view.Session()) +} - buffer.WriteString(" {\n from {\n") - for _, property := range animation.tags { - writeValue(property.Tag, property.Start) +func (view *viewData) getTransitions() Params { + result := Params{} + 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") - for _, property := range animation.tags { - writeValue(property.Tag, property.End) - } - buffer.WriteString(" }\n") - - 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() +// GetTransition returns the subview transitions. The result is always non-nil. +// If the second argument (subviewID) is "" then transitions of the first argument (view) is returned +func GetTransition(view View, subviewID string) Params { + if subviewID != "" { + view = ViewByID(view, subviewID) } - 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{} } -*/ diff --git a/animationEvents.go b/animationEvents.go new file mode 100644 index 0000000..fe436ed --- /dev/null +++ b/animationEvents.go @@ -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) +} diff --git a/app_scripts.js b/app_scripts.js index f8047d1..096ff97 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -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) { - var message = "transitionEnd{session=" + sessionID + ",id=" + element.id; + var message = "transition-end-event{session=" + sessionID + ",id=" + element.id; if (event.propertyName) { message += ",property=" + event.propertyName } @@ -1047,7 +1065,7 @@ function transitionEndEvent(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) { message += ",property=" + event.propertyName } @@ -1055,8 +1073,44 @@ function transitionCancelEvent(element, event) { 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) { - sendMessage("transitionEnd{session=" + sessionID + ",id=" + stackId + ",property=" + propertyName + "}"); + sendMessage("transition-end-event{session=" + sessionID + ",id=" + stackId + ",property=" + propertyName + "}"); event.stopPropagation(); } diff --git a/background.go b/background.go index 443bd04..b69b4fd 100644 --- a/background.go +++ b/background.go @@ -102,7 +102,7 @@ const ( // BackgroundElement describes the background element. type BackgroundElement interface { Properties - cssStyle(view View) string + cssStyle(session Session) string Tag() string } @@ -238,8 +238,7 @@ func (image *backgroundImage) Get(tag string) interface{} { return image.backgroundElement.Get(image.normalizeTag(tag)) } -func (image *backgroundImage) cssStyle(view View) string { - session := view.Session() +func (image *backgroundImage) cssStyle(session Session) string { if src, ok := stringProperty(image, Source, session); ok && src != "" { buffer := allocStringBuilder() defer freeStringBuilder(buffer) @@ -444,7 +443,7 @@ func (point *BackgroundGradientPoint) setValue(value string) bool { 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] if !ok { @@ -455,7 +454,7 @@ func (gradient *backgroundGradient) writeGradient(view View, buffer *strings.Bui switch value := value.(type) { case string: - if text, ok := view.Session().resolveConstants(value); ok && text != "" { + if text, ok := session.resolveConstants(value); ok && text != "" { elements := strings.Split(text, `,`) points := make([]BackgroundGradientPoint, len(elements)) for i, element := range elements { @@ -477,7 +476,7 @@ func (gradient *backgroundGradient) writeGradient(view View, buffer *strings.Bui for i, element := range value { switch element := element.(type) { case string: - if text, ok := view.Session().resolveConstants(element); ok && text != "" { + if text, ok := session.resolveConstants(element); ok && text != "" { if !points[i].setValue(text) { ErrorLogF(`Invalid gradient point #%d: "%s"`, i, text) return false @@ -535,11 +534,10 @@ func (gradient *backgroundLinearGradient) Set(tag string, value interface{}) boo return gradient.backgroundGradient.Set(tag, value) } -func (gradient *backgroundLinearGradient) cssStyle(view View) string { +func (gradient *backgroundLinearGradient) cssStyle(session Session) string { buffer := allocStringBuilder() defer freeStringBuilder(buffer) - session := view.Session() if repeating, _ := boolProperty(gradient, Repeating, session); repeating { buffer.WriteString(`repeating-linear-gradient(`) } else { @@ -581,7 +579,7 @@ func (gradient *backgroundLinearGradient) cssStyle(view View) string { } } - if !gradient.writeGradient(view, buffer) { + if !gradient.writeGradient(session, buffer) { return "" } @@ -642,11 +640,10 @@ func (gradient *backgroundRadialGradient) Get(tag string) interface{} { return gradient.backgroundGradient.Get(gradient.normalizeTag(tag)) } -func (gradient *backgroundRadialGradient) cssStyle(view View) string { +func (gradient *backgroundRadialGradient) cssStyle(session Session) string { buffer := allocStringBuilder() defer freeStringBuilder(buffer) - session := view.Session() if repeating, _ := boolProperty(gradient, Repeating, session); repeating { buffer.WriteString(`repeating-radial-gradient(`) } else { @@ -706,7 +703,7 @@ func (gradient *backgroundRadialGradient) cssStyle(view View) string { } buffer.WriteString(", ") - if !gradient.writeGradient(view, buffer) { + if !gradient.writeGradient(session, buffer) { return "" } diff --git a/cssBuilder.go b/cssBuilder.go index bd2ac71..852c30d 100644 --- a/cssBuilder.go +++ b/cssBuilder.go @@ -218,6 +218,42 @@ func (builder *cssStyleBuilder) endStyle() { 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) { if value != "" { if builder.buffer == nil { diff --git a/customView.go b/customView.go index a4dc457..a07a435 100644 --- a/customView.go +++ b/customView.go @@ -259,3 +259,10 @@ func (customView *CustomViewData) setScroll(x, y, width, height float64) { customView.superView.setScroll(x, y, width, height) } } + +func (customView *CustomViewData) getTransitions() Params { + if customView.superView != nil { + return customView.superView.getTransitions() + } + return Params{} +} diff --git a/demo/absoluteLayoutDemo.go b/demo/absoluteLayoutDemo.go deleted file mode 100644 index af17eac..0000000 --- a/demo/absoluteLayoutDemo.go +++ /dev/null @@ -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) -} diff --git a/demo/animationDemo.go b/demo/animationDemo.go new file mode 100644 index 0000000..9e5d4ea --- /dev/null +++ b/demo/animationDemo.go @@ -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 +} diff --git a/demo/main.go b/demo/main.go index 1da38ea..2bfb4a9 100644 --- a/demo/main.go +++ b/demo/main.go @@ -78,7 +78,6 @@ func createDemo(session rui.Session) rui.SessionContent { {"GridLayout", createGridLayoutDemo, nil}, {"ColumnLayout", createColumnLayoutDemo, nil}, {"StackLayout", createStackLayoutDemo, nil}, - {"AbsoluteLayout", createAbsoluteLayoutDemo, nil}, {"Resizable", createResizableDemo, nil}, {"ListView", createListViewDemo, nil}, {"Checkbox", createCheckboxDemo, nil}, @@ -93,6 +92,7 @@ func createDemo(session rui.Session) rui.SessionContent { {"Filter", createFilterDemo, nil}, {"Clip", createClipDemo, nil}, {"Transform", transformDemo, nil}, + {"Animation", createAnimationDemo, nil}, {"Transition", createTransitionDemo, nil}, {"Key events", createKeyEventsDemo, nil}, {"Mouse events", createMouseEventsDemo, nil}, diff --git a/demo/transitionDemo.go b/demo/transitionDemo.go index 5ac91fc..09575ac 100644 --- a/demo/transitionDemo.go +++ b/demo/transitionDemo.go @@ -43,24 +43,23 @@ func createTransitionDemo(session rui.Session) rui.View { } rui.Set(view, "startTransition", rui.ClickEvent, func(button rui.View) { + 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 rui.GetWidth(bar, "").Value == 100 { - bar.SetAnimated(rui.Width, rui.Percent(20), rui.Animation{ - Duration: 2, - TimingFunction: timing, - }) + bar.Remove(rui.TransitionEndEvent) + bar.SetAnimated(rui.Width, rui.Percent(20), animation) } else { - bar.SetAnimated(rui.Width, rui.Percent(100), rui.Animation{ - Duration: 2, - TimingFunction: timing, - 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.Set(rui.TransitionEndEvent, func(v rui.View, tag string) { + bar.Remove(rui.TransitionEndEvent) + bar.SetAnimated(rui.Width, rui.Percent(20), animation) }) + bar.SetAnimated(rui.Width, rui.Percent(100), animation) } } } diff --git a/propertySet.go b/propertySet.go index 240f038..a071844 100644 --- a/propertySet.go +++ b/propertySet.go @@ -54,6 +54,7 @@ var boolProperties = []string{ Controls, Loop, Muted, + AnimationPaused, } 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"}, }, + AnimationDirection: { + []string{"normal", "reverse", "alternate", "alternate-reverse"}, + "", + []string{"normal", "reverse", "alternate", "alternate-reverse"}, + }, RadialGradientShape: { []string{"ellipse", "circle"}, "", diff --git a/session.go b/session.go index c6ec066..46e12a4 100644 --- a/session.go +++ b/session.go @@ -53,6 +53,8 @@ type Session interface { // a description of the error is written to the log Set(viewID, tag string, value interface{}) bool + registerAnimation(props []AnimatedProperty) string + resolveConstants(value string) (string, bool) checkboxOffImage() string checkboxOnImage() string @@ -88,27 +90,28 @@ type Session interface { } type sessionData struct { - customTheme *theme - darkTheme bool - touchScreen bool - textDirection int - pixelRatio float64 - language string - languages []string - checkboxOff string - checkboxOn string - radiobuttonOff string - radiobuttonOn string - app Application - sessionID int - viewCounter int - content SessionContent - rootView View - ignoreUpdates bool - popups *popupManager - images *imageManager - brige WebBrige - events chan DataObject + customTheme *theme + darkTheme bool + touchScreen bool + textDirection int + pixelRatio float64 + language string + languages []string + checkboxOff string + checkboxOn string + radiobuttonOff string + radiobuttonOn string + app Application + sessionID int + viewCounter int + content SessionContent + rootView View + ignoreUpdates bool + popups *popupManager + images *imageManager + brige WebBrige + events chan DataObject + animationCounter int } 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.viewCounter = 0 session.ignoreUpdates = false + session.animationCounter = 0 if customTheme != "" { if theme, ok := newTheme(customTheme); ok { diff --git a/stackLayout.go b/stackLayout.go index b9320e4..0861779 100644 --- a/stackLayout.go +++ b/stackLayout.go @@ -55,11 +55,11 @@ func (layout *stackLayoutData) Init(session Session) { layout.viewsContainerData.Init(session) layout.tag = "StackLayout" layout.systemClass = "ruiStackLayout" + layout.properties[TransitionEndEvent] = []func(View, string){layout.pushFinished, layout.popFinished} } -func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) { - switch tag { - case "ruiPush": +func (layout *stackLayoutData) pushFinished(view View, tag string) { + if tag == "ruiPush" { if layout.pushView != nil { layout.pushView = nil count := len(layout.views) @@ -70,13 +70,17 @@ func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) { } updateInnerHTML(layout.htmlID(), layout.session) } + if layout.onPushFinished != nil { onPushFinished := layout.onPushFinished layout.onPushFinished = nil onPushFinished() } + } +} - case "ruiPop": +func (layout *stackLayoutData) popFinished(view View, tag string) { + if tag == "ruiPop" { popView := layout.popView layout.popView = nil 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 { if int(layout.peek) < len(layout.views) { return layout.views[layout.peek] @@ -174,7 +199,7 @@ func (layout *stackLayoutData) Push(view View, animation int, onPushFinished fun layout.pushView = view layout.animationType = animation - layout.animation["ruiPush"] = Animation{FinishListener: layout} + //layout.animation["ruiPush"] = Animation{FinishListener: layout} layout.onPushFinished = onPushFinished htmlID := layout.htmlID() @@ -226,7 +251,7 @@ func (layout *stackLayoutData) Pop(animation int, onPopFinished func(View)) bool layout.RemoveView(layout.peek) layout.animationType = animation - layout.animation["ruiPop"] = Animation{FinishListener: layout} + //layout.animation["ruiPop"] = Animation{FinishListener: layout} layout.onPopFinished = onPopFinished htmlID := layout.htmlID() diff --git a/tableView.go b/tableView.go index e531afa..9b391bc 100644 --- a/tableView.go +++ b/tableView.go @@ -804,7 +804,7 @@ func (table *tableViewData) getCellBorder() BorderProperty { } 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()) 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) { session := cell.Session() - cell.viewData.cssViewStyle(builder, session, self) + cell.viewData.cssViewStyle(builder, session) if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok { builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value]) diff --git a/theme.go b/theme.go index fb0934a..919c1c5 100644 --- a/theme.go +++ b/theme.go @@ -191,7 +191,7 @@ func (theme *theme) cssText(session Session) string { style.init() parseProperties(&style, obj) builder.startStyle(tag) - style.cssViewStyle(&builder, session, nil) + style.cssViewStyle(&builder, session) builder.endStyle() } @@ -202,7 +202,7 @@ func (theme *theme) cssText(session Session) string { style.init() parseProperties(&style, obj) builder.startStyle(tag) - style.cssViewStyle(&builder, session, nil) + style.cssViewStyle(&builder, session) builder.endStyle() } builder.endMedia() diff --git a/view.go b/view.go index f34a01e..2b13844 100644 --- a/view.go +++ b/view.go @@ -82,6 +82,8 @@ type View interface { cssStyle(self View, builder cssBuilder) addToCSSStyle(addCSS map[string]string) + getTransitions() Params + onResize(self View, x, y, width, height float64) onItemResize(self View, index int, x, y, width, height float64) setNoResizeEvent() @@ -92,18 +94,18 @@ type View interface { // viewData - base implementation of View interface type viewData struct { viewStyle - session Session - tag string - viewID string - _htmlID string - parentID string - systemClass string - animation map[string]Animation - addCSS map[string]string - frame Frame - scroll Frame - noResizeEvent bool - created bool + session Session + tag string + viewID string + _htmlID string + parentID string + systemClass string + singleTransition map[string]Animation + addCSS map[string]string + frame Frame + scroll Frame + noResizeEvent bool + created bool //animation map[string]AnimationEndListener } @@ -142,7 +144,7 @@ func (view *viewData) Init(session Session) { view.session = session view.addCSS = map[string]string{} //view.animation = map[string]AnimationEndListener{} - view.animation = map[string]Animation{} + view.singleTransition = map[string]Animation{} view.noResizeEvent = false view.created = false } @@ -215,6 +217,12 @@ func (view *viewData) remove(tag string) { case TouchStart, TouchEnd, TouchMove, TouchCancel: view.removeTouchListener(tag) + case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent: + view.removeTransitionListener(tag) + + case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent: + view.removeAnimationListener(tag) + case ResizeEvent, ScrollEvent: delete(view.properties, tag) @@ -276,6 +284,12 @@ func (view *viewData) set(tag string, value interface{}) bool { case TouchStart, TouchEnd, TouchMove, TouchCancel: 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: return view.setFrameListener(tag, value) } @@ -304,7 +318,7 @@ func (view *viewData) propertyChanged(tag string) { updateInnerHTML(view.parentHTMLID(), session) case Background: - updateCSSProperty(htmlID, Background, view.backgroundCSS(view), session) + updateCSSProperty(htmlID, Background, view.backgroundCSS(session), session) return case Border: @@ -428,6 +442,7 @@ func (view *viewData) propertyChanged(tag string) { } else { updateCSSProperty(htmlID, "font-style", "", session) } + return case SmallCaps: if state, ok := boolProperty(view, tag, session); ok { @@ -439,13 +454,33 @@ func (view *viewData) propertyChanged(tag string) { } else { updateCSSProperty(htmlID, "font-variant", "", session) } + return case Strikethrough, Overline, Underline: updateCSSProperty(htmlID, "text-decoration", view.cssTextDecoration(session), session) for _, tag2 := range []string{TextLineColor, TextLineStyle, TextLineThickness} { 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 { @@ -521,7 +556,7 @@ func (view *viewData) addToCSSStyle(addCSS map[string]string) { } func (view *viewData) cssStyle(self View, builder cssBuilder) { - view.viewStyle.cssViewStyle(builder, view.session, self) + view.viewStyle.cssViewStyle(builder, view.session) switch GetVisibility(view, "") { case Invisible: builder.add(`visibility`, `hidden`) @@ -600,6 +635,8 @@ func viewHTML(view View, buffer *strings.Builder) { pointerEventsHtml(view, buffer) touchEventsHtml(view, buffer) focusEventsHtml(view, buffer) + transitionEventsHtml(view, buffer) + animationEventsHtml(view, buffer) buffer.WriteRune('>') view.htmlSubviews(view, buffer) @@ -654,6 +691,12 @@ func (view *viewData) handleCommand(self View, command string, data DataObject) listener(self) } + case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent: + view.handleTransitionEvents(command, data) + + case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent: + view.handleAnimationEvents(command, data) + case "scroll": 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": floatProperty := func(tag string) float64 { diff --git a/viewAnimation.go b/viewAnimation.go deleted file mode 100644 index cfa46f8..0000000 --- a/viewAnimation.go +++ /dev/null @@ -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 -} diff --git a/viewStyle.go b/viewStyle.go index f76e9a8..98ce8f1 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -9,12 +9,12 @@ import ( // ViewStyle interface of the style of view type ViewStyle interface { Properties - cssViewStyle(buffer cssBuilder, session Session, view View) + cssViewStyle(buffer cssBuilder, session Session) } type viewStyle struct { propertyList - //transitions map[string]ViewTransition + transitions map[string]Animation } // 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() { style.propertyList.init() //style.shadows = []ViewShadow{} - //style.transitions = map[string]ViewTransition{} + style.transitions = map[string]Animation{} } // NewViewStyle create new ViewStyle object @@ -132,14 +132,14 @@ func split4Values(text string) []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 backgrounds, ok := value.([]BackgroundElement); ok { buffer := allocStringBuilder() defer freeStringBuilder(buffer) for _, background := range backgrounds { - if value := background.cssStyle(view); value != "" { + if value := background.cssStyle(session); value != "" { if buffer.Len() > 0 { buffer.WriteString(", ") } @@ -155,7 +155,7 @@ func (style *viewStyle) backgroundCSS(view View) string { 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 { 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]) } - if background := style.backgroundCSS(view); background != "" { + if background := style.backgroundCSS(session); 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 buffer.Len() > 0 { - buffer.WriteString(`, `) - } - buffer.WriteString(property) - transition.cssWrite(buffer, session) - } + if transition := style.transitionCSS(session); transition != "" { + builder.add(`transition`, transition) + } - if buffer.Len() > 0 { - builder.add(`transition`, buffer.String()) - } + if animation := style.animationCSS(session); animation != "" { + 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 + } } diff --git a/viewStyleSet.go b/viewStyleSet.go index 5b1b737..64c9acf 100644 --- a/viewStyleSet.go +++ b/viewStyleSet.go @@ -267,6 +267,101 @@ func (style *viewStyle) set(tag string, value interface{}) bool { case Filter: 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) diff --git a/viewTransform.go b/viewTransform.go index 198bb58..17f6062 100644 --- a/viewTransform.go +++ b/viewTransform.go @@ -91,10 +91,10 @@ func getOrigin(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) return x, y, z } -func getSkew(style Properties, session Session) (AngleUnit, AngleUnit) { - skewX, _ := angleProperty(style, SkewX, session) - skewY, _ := angleProperty(style, SkewY, session) - return skewX, skewY +func getSkew(style Properties, session Session) (AngleUnit, AngleUnit, bool) { + skewX, okX := angleProperty(style, SkewX, session) + skewY, okY := angleProperty(style, SkewY, session) + return skewX, skewY, okX || okY } 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 } -func getScale(style Properties, session Session) (float64, float64, float64) { - scaleX, _ := floatProperty(style, ScaleX, session, 1) - scaleY, _ := floatProperty(style, ScaleY, session, 1) - scaleZ, _ := floatProperty(style, ScaleZ, session, 1) - return scaleX, scaleY, scaleZ +func getScale(style Properties, session Session) (float64, float64, float64, bool) { + scaleX, okX := floatProperty(style, ScaleX, session, 1) + scaleY, okY := floatProperty(style, ScaleY, session, 1) + scaleZ, okZ := floatProperty(style, ScaleZ, session, 1) + 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) rotateY, _ := floatProperty(style, RotateY, session, 1) rotateZ, _ := floatProperty(style, RotateZ, session, 1) - angle, _ := angleProperty(style, Rotate, session) - return rotateX, rotateY, rotateZ, angle + return rotateX, rotateY, rotateZ } func (style *viewStyle) transform(session Session) string { @@ -124,8 +123,8 @@ func (style *viewStyle) transform(session Session) string { buffer := allocStringBuilder() defer freeStringBuilder(buffer) - skewX, skewY := getSkew(style, session) - if skewX.Value != 0 || skewY.Value != 0 { + skewX, skewY, skewOK := getSkew(style, session) + if skewOK { buffer.WriteString(`skew(`) buffer.WriteString(skewX.cssString()) buffer.WriteRune(',') @@ -134,9 +133,9 @@ func (style *viewStyle) transform(session Session) string { } x, y, z := getTranslate(style, session) - scaleX, scaleY, scaleZ := getScale(style, session) + scaleX, scaleY, scaleZ, scaleOK := getScale(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 { buffer.WriteRune(' ') } @@ -149,7 +148,7 @@ func (style *viewStyle) transform(session Session) string { buffer.WriteRune(')') } - if scaleX != 1 || scaleY != 1 || scaleZ != 1 { + if scaleOK { if buffer.Len() > 0 { buffer.WriteRune(' ') } @@ -162,8 +161,8 @@ func (style *viewStyle) transform(session Session) string { buffer.WriteRune(')') } - rotateX, rotateY, rotateZ, angle := getRotate(style, session) - if angle.Value != 0 && (rotateX != 0 || rotateY != 0 || rotateZ != 0) { + if angle, ok := angleProperty(style, Rotate, session); ok { + rotateX, rotateY, rotateZ := getRotateVector(style, session) if buffer.Len() > 0 { buffer.WriteRune(' ') } @@ -177,8 +176,9 @@ func (style *viewStyle) transform(session Session) string { buffer.WriteString(angle.cssString()) buffer.WriteRune(')') } + } 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 { buffer.WriteRune(' ') } @@ -189,7 +189,7 @@ func (style *viewStyle) transform(session Session) string { buffer.WriteRune(')') } - if scaleX != 1 || scaleY != 1 { + if scaleOK { if buffer.Len() > 0 { buffer.WriteRune(' ') } @@ -200,8 +200,7 @@ func (style *viewStyle) transform(session Session) string { buffer.WriteRune(')') } - angle, _ := angleProperty(style, Rotate, session) - if angle.Value != 0 { + if angle, ok := angleProperty(style, Rotate, session); ok { if buffer.Len() > 0 { buffer.WriteRune(' ') } diff --git a/viewUtils.go b/viewUtils.go index e1ca150..77fa9c6 100644 --- a/viewUtils.go +++ b/viewUtils.go @@ -854,7 +854,8 @@ func GetSkew(view View, subviewID string) (AngleUnit, AngleUnit) { if view == nil { 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. @@ -866,7 +867,8 @@ func GetScale(view View, subviewID string) (float64, float64, float64) { if view == nil { 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 @@ -878,7 +880,10 @@ func GetRotate(view View, subviewID string) (float64, float64, float64, AngleUni if view == nil { 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,