diff --git a/README.md b/README.md index 11428b0..a2acee2 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ At the moment, there are three implementations of animations: - [Color Flow](#color-flow) - you can apply this animation to any widget You can configure this animation to make your button hover smoothly or change it into a rainbow! - [Movement](#move) - moves DrawCursor, emulating moving an object (aka `giu.Widget`). +- [Resize](#resize) - resize a widget. Lets shortly discuss particular types of animations: @@ -117,6 +118,23 @@ there are two additional methods of `MoveAnimation`. - another method is tu simply call `DefaultStartPos` method. It takes no arguments and acts like most users would like to use `StartPos` - it returns `Step(startPos)`. +#### Resize + +```go +func Resize[T giu.Widget]( + w resizable2D[T], + sizes ...imgui.Vec2, +) *ResizeAnimation[T] {...} +``` + +This will resize `w`. + +It is the first animation that requires a [Type Parameter](https://go.dev/tour/generics/1). +It is because first of arguments - a widget `w` needs to implement one extra method - +the `Size(w, h float32)`. + +Second argument is a list of sizes to apply as Key Frames. + ### Easing These are some additional ways of controlling the flow of animation: diff --git a/_examples/main.go b/_examples/main.go index 8ada750..0604883 100644 --- a/_examples/main.go +++ b/_examples/main.go @@ -1,23 +1,51 @@ +// Package main contains an example of using giu-animations. package main import ( "time" - "golang.org/x/image/colornames" - "github.com/AllenDang/giu" "github.com/AllenDang/imgui-go" + "golang.org/x/image/colornames" animations "github.com/gucio321/giu-animations/v2" ) +//nolint:gochecknoglobals // it's an example var ( - easingAlg = animations.EasingAlgNone - playOnHover bool + easingAlg = animations.EasingAlgNone + moveOnHover bool + resizeOnHover bool + resizeFlags = struct { + BeforeX, + BeforeY, + AfterX, + AfterY bool + }{true, true, false, false} + sameline bool ) +//nolint:funlen // it's an example func loop() { a := int32(easingAlg) + resizeTrick := animations.TrickNever + + if resizeFlags.BeforeX { + resizeTrick |= animations.TrickCursorBeforeX + } + + if resizeFlags.BeforeY { + resizeTrick |= animations.TrickCursorBeforeY + } + + if resizeFlags.AfterX { + resizeTrick |= animations.TrickCursorAfterX + } + + if resizeFlags.AfterY { + resizeTrick |= animations.TrickCursorAfterY + } + animations.Animator( animations.Transition( func(starterFunc animations.StarterFunc) { @@ -51,25 +79,32 @@ func loop() { starterFunc.StartCycle(1, animations.PlayForward) }), ), - giu.Checkbox("Play on hover", &playOnHover), + giu.Checkbox("Move on hover", &moveOnHover), + giu.Checkbox("Resize on hover", &resizeOnHover), animations.Animator( animations.Move(func(starter animations.StarterFunc) giu.Widget { - return giu.Child().Layout( - giu.Row( - giu.Label("Set easing alg:"), - giu.SliderInt(&a, 0, int32(animations.EasingAlgMax-1)).Size(100).OnChange(func() { - easingAlg = animations.EasingAlgorithmType(a) - }), - ), - giu.Row( - giu.Button("play backwards").OnClick(func() { - starter.Start(animations.PlayBackward) - }), - giu.Button("move me!").OnClick(func() { - starter.Start(animations.PlayForward) - }), + return animations.Animator(animations.Resize[*giu.ChildWidget]( + giu.Child().Layout( + giu.Row( + giu.Label("Set easing alg:"), + giu.SliderInt(&a, 0, int32(animations.EasingAlgMax-1)).Size(100).OnChange(func() { + easingAlg = animations.EasingAlgorithmType(a) + }), + ), + giu.Row( + giu.Button("play backwards").OnClick(func() { + starter.Start(animations.PlayBackward) + }), + giu.Button("move me!").OnClick(func() { + starter.Start(animations.PlayForward) + }), + ), ), - ).Size(200, 80) + imgui.Vec2{X: 200, Y: 80}, + imgui.Vec2{X: 250, Y: 130}, + ).TrickCursor(animations.TrickNever)).Trigger(animations.TriggerOnChange, animations.PlayForward, func() bool { + return imgui.IsItemHovered() && resizeOnHover + }) }, animations.Step(20, 100). Bezier(imgui.Vec2{X: 20, Y: 20}, imgui.Vec2{X: 90}), @@ -78,8 +113,30 @@ func loop() { FPS(120). EasingAlgorithm(easingAlg). Trigger(animations.TriggerOnTrue, animations.PlayForward, func() bool { - return playOnHover && giu.IsItemHovered() + return moveOnHover && giu.IsItemHovered() }), + animations.Animator(animations.Resize[*giu.ButtonWidget]( + giu.Button("Resize me!"), + imgui.Vec2{X: 150, Y: 150}, + imgui.Vec2{X: 200, Y: 200}, + imgui.Vec2{X: 250, Y: 250}, + imgui.Vec2{X: 300, Y: 300}, + ).TrickCursor(resizeTrick)).Trigger(animations.TriggerOnChange, animations.PlayForward, imgui.IsItemHovered). + EasingAlgorithm(animations.EasingAlgOutBounce), + + giu.Custom(func() { + if sameline { + imgui.SameLine() + } + }), + giu.TreeNode("Resize flags").Layout( + giu.Checkbox("Before X", &resizeFlags.BeforeX), + giu.Checkbox("Before Y", &resizeFlags.BeforeY), + giu.Checkbox("After X", &resizeFlags.AfterX), + giu.Checkbox("After Y", &resizeFlags.AfterY), + giu.Separator(), + giu.Checkbox("Put me in the same line with resizable button", &sameline), + ), ) }, func(starterFunc animations.StarterFunc) { diff --git a/animation.go b/animation.go index 5524450..2d3f191 100644 --- a/animation.go +++ b/animation.go @@ -15,7 +15,9 @@ type Animation interface { // BuildNormal is called every frame when animation is not running // starter is animation link to Animator.Start - BuildNormal(currentKeyFrame KeyFrame, starterFunc StarterFunc) + // triggerCheck should be placed where the animator should check for trigger. If not called - Animator will do that + // after rendering the widget (after calling BuildNormal). + BuildNormal(currentKeyFrame KeyFrame, starterFunc StarterFunc, triggerCheck func()) // BuildAnimation is called when running an animation. // It receives several important arguments: // - animationPercentage after applying specified by Animator diff --git a/animator.go b/animator.go index ed271b7..a6e9b9d 100644 --- a/animator.go +++ b/animator.go @@ -274,10 +274,17 @@ func (a *AnimatorWidget) Build() { return } - a.animation.BuildNormal(cf, a) + wasCalled := false + triggerValue := false + a.animation.BuildNormal(cf, a, func() { + wasCalled = true + triggerValue = triggerValue || a.triggerFunc() + }) if a.triggerFunc != nil { - triggerValue := a.triggerFunc() + if !wasCalled { + triggerValue = a.triggerFunc() + } switch a.triggerType { case TriggerNever: diff --git a/color_flow.go b/color_flow.go index ff22e3e..471f29b 100644 --- a/color_flow.go +++ b/color_flow.go @@ -91,7 +91,7 @@ func (c *ColorFlowAnimation) KeyFramesCount() int { } // BuildNormal builds animation in normal, not-triggered state. -func (c *ColorFlowAnimation) BuildNormal(currentKeyFrame KeyFrame, _ StarterFunc) { +func (c *ColorFlowAnimation) BuildNormal(currentKeyFrame KeyFrame, _ StarterFunc, _ func()) { normalColor := c.color[currentKeyFrame]() c.build(normalColor) diff --git a/move.go b/move.go index 6ee3cfb..295b851 100644 --- a/move.go +++ b/move.go @@ -66,7 +66,7 @@ func (m *MoveAnimation) KeyFramesCount() int { } // BuildNormal implements Animation. -func (m *MoveAnimation) BuildNormal(currentKF KeyFrame, starter StarterFunc) { +func (m *MoveAnimation) BuildNormal(currentKF KeyFrame, starter StarterFunc, _ func()) { imgui.SetCursorPos(m.getPosition(currentKF)) m.widget(starter).Build() diff --git a/resize_animation.go b/resize_animation.go new file mode 100644 index 0000000..7fbeb0d --- /dev/null +++ b/resize_animation.go @@ -0,0 +1,157 @@ +package animations + +import ( + "github.com/AllenDang/giu" + "github.com/AllenDang/imgui-go" +) + +// TrickCursor allows to enable special Drawing Cursor behavior. +// If enabled, Drawing Cursor is moved before rendering ResizeAnimation. +// It allows to reach an effect of resizing a UI element from its center. +// NOTE: Be careful as this feature (especially TrickCursorAfterX) may break imgui.Sameline() mechanism. +// NOTE: You can test it in _examples/main.go. +type TrickCursor byte + +// These constants work as a bitmask. Feel free to use it like TrickCursorBeforeX | TrickCursorAfterY. +const ( + // TrickNever allows to disable TrickCursor. + TrickNever TrickCursor = 1 << iota + + // TrickCursorBeforeX allows to move Drawing Cursor before rendering ResizeAnimation on X axis. + TrickCursorBeforeX + // TrickCursorBeforeY allows to move Drawing Cursor before rendering ResizeAnimation on Y axis. + TrickCursorBeforeY + // TrickCursorAfterX allows to move Drawing Cursor after rendering ResizeAnimation on X axis. + // IMPORTANT: Applying this will interfere with imgui.Sameline() mechanism. + TrickCursorAfterX + // TrickCursorAfterY allows to move Drawing Cursor after rendering ResizeAnimation on Y axis. + TrickCursorAfterY + + // TrickCursorBefore allows to move Drawing Cursor before rendering ResizeAnimation on both axes. + TrickCursorBefore = TrickCursorBeforeX | TrickCursorBeforeY + // TrickCursorAfter allows to move Drawing Cursor after rendering ResizeAnimation on both axes. + TrickCursorAfter = TrickCursorAfterX | TrickCursorAfterY + // TrickCursorAlways allows to move Drawing Cursor before and after rendering ResizeAnimation on both axes. + TrickCursorAlways = TrickCursorBefore | TrickCursorAfter +) + +// ResizeAnimation allows to resize a UI element. +type ResizeAnimation[T giu.Widget] struct { + widget resizable2D[T] + sizes []imgui.Vec2 + id string + trickCursor TrickCursor +} + +// Resize creates ResizeAnimation. +// It requires Widget type parameter (e.g. *giu.ButtonWidget). +func Resize[T giu.Widget](w resizable2D[T], sizes ...imgui.Vec2) *ResizeAnimation[T] { + return &ResizeAnimation[T]{ + id: giu.GenAutoID("giu-animations-ResizeAnimation"), + widget: w, + sizes: sizes, + trickCursor: TrickCursorBeforeY, + } +} + +// ID allows to set ID manually. +func (r *ResizeAnimation[T]) ID(id string) { + r.id = id +} + +// TrickCursor allows to set TrickCursor. +// Default: TrickCursorBefore. +func (r *ResizeAnimation[T]) TrickCursor(t TrickCursor) *ResizeAnimation[T] { + r.trickCursor = t + + return r +} + +// Init implements Animation interface. +func (r *ResizeAnimation[T]) Init() { + // noop +} + +// Reset implements Animation. +func (r *ResizeAnimation[T]) Reset() { + // noop +} + +// KeyFramesCount implements Animation. +func (r *ResizeAnimation[T]) KeyFramesCount() int { + return len(r.sizes) +} + +// BuildNormal implements Animation. +func (r *ResizeAnimation[T]) BuildNormal(currentKeyFrame KeyFrame, _ StarterFunc, triggerCheck func()) { + // This may happen if user forgot to pass size vectors. In this case just allow to build unchanged widget. + if int(currentKeyFrame) > len(r.sizes)-1 { + r.widget.Build() + + return + } + + r.trickCursorBefore(r.sizes[currentKeyFrame], imgui.Vec2{}) + + r.widget.Size(r.sizes[currentKeyFrame].X, r.sizes[currentKeyFrame].Y).Build() + triggerCheck() + + r.trickCursorAfter(r.sizes[currentKeyFrame]) +} + +// BuildAnimation implements Animation. +func (r *ResizeAnimation[T]) BuildAnimation( + animationPercentage, _ float32, + baseKeyFrame, destinationKeyFrame KeyFrame, + _ PlayMode, _ StarterFunc, +) { + delta := imgui.Vec2{ + X: (r.sizes[destinationKeyFrame].X - r.sizes[baseKeyFrame].X) * animationPercentage, + Y: (r.sizes[destinationKeyFrame].Y - r.sizes[baseKeyFrame].Y) * animationPercentage, + } + + r.trickCursorBefore(r.sizes[baseKeyFrame], delta) + + newSize := imgui.Vec2{ + X: r.sizes[baseKeyFrame].X + delta.X, + Y: r.sizes[baseKeyFrame].Y + delta.Y, + } + + r.widget.Size(newSize.X, newSize.Y).Build() + + r.trickCursorAfter(newSize) +} + +func (r *ResizeAnimation[T]) trickCursorBefore(current, delta imgui.Vec2) { + move := imgui.Vec2{} + + if r.trickCursor&TrickCursorBeforeX != 0 { + move.X -= (current.X + delta.X - r.sizes[0].X) / 2 + } + + if r.trickCursor&TrickCursorBeforeY != 0 { + move.Y -= (current.Y + delta.Y - r.sizes[0].Y) / 2 + } + + imgui.Sameline() + imgui.Dummy(move) +} + +func (r *ResizeAnimation[T]) trickCursorAfter(currentSize imgui.Vec2) { + move := imgui.Vec2{} + + if r.trickCursor&TrickCursorAfterX != 0 { + move.X -= (currentSize.X - r.sizes[0].X) / 2 + } + + if r.trickCursor&TrickCursorAfterY != 0 { + move.Y -= (currentSize.Y - r.sizes[0].Y) / 2 + } + + imgui.Dummy(move) +} + +type resizable2D[T giu.Widget] interface { + Size(w, h float32) T + giu.Widget +} diff --git a/transition.go b/transition.go index 5d74bf0..c2c2939 100644 --- a/transition.go +++ b/transition.go @@ -35,7 +35,7 @@ func (t *TransitionAnimation) Init() { } // BuildNormal implements Animation interface. -func (t *TransitionAnimation) BuildNormal(f KeyFrame, starter StarterFunc) { +func (t *TransitionAnimation) BuildNormal(f KeyFrame, starter StarterFunc, _ func()) { t.renderers[f](starter) }