mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
Animation: Add animation demos
This commit is contained in:
parent
bc3600cde8
commit
4274b835f6
18 changed files with 2138 additions and 323 deletions
900
examples/GUI/AnimatorsDemo.h
Normal file
900
examples/GUI/AnimatorsDemo.h
Normal file
|
|
@ -0,0 +1,900 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework examples.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
to use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: AnimatorsDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Application demonstrating how the JUCE provided Animator
|
||||
classes can be used to create dynamic, composable, smooth
|
||||
animations.
|
||||
|
||||
dependencies: juce_core, juce_data_structures, juce_events, juce_graphics,
|
||||
juce_gui_basics, juce_animation
|
||||
exporters: xcode_mac, vs2022, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: AnimatorsDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
struct Paintable
|
||||
{
|
||||
virtual ~Paintable() = default;
|
||||
virtual void paint (Graphics& g) const = 0;
|
||||
};
|
||||
|
||||
namespace Shapes
|
||||
{
|
||||
|
||||
class Arc final : public Paintable
|
||||
{
|
||||
static constexpr auto pi = MathConstants<float>::pi;
|
||||
static constexpr auto arcStart = pi / 2.0f;
|
||||
|
||||
public:
|
||||
Arc() = default;
|
||||
|
||||
Arc (Point<float> centreIn, float radiusIn, float thicknessIn, Colour colourIn)
|
||||
: centre (centreIn),
|
||||
initialRadius (radiusIn - thicknessIn / 2),
|
||||
initialThickness (thicknessIn),
|
||||
colour (colourIn)
|
||||
{
|
||||
}
|
||||
|
||||
void paint (Graphics& g) const override
|
||||
{
|
||||
if (! active)
|
||||
return;
|
||||
|
||||
const Graphics::ScopedSaveState s { g };
|
||||
|
||||
Path p;
|
||||
p.addCentredArc (centre.getX(),
|
||||
centre.getY(),
|
||||
radius,
|
||||
radius,
|
||||
0.0f,
|
||||
arcStart,
|
||||
arcStart + sweepAngle,
|
||||
true);
|
||||
|
||||
g.setColour (colour);
|
||||
g.strokePath (p, { thickness, PathStrokeType::mitered });
|
||||
}
|
||||
|
||||
void setActive (bool activeIn)
|
||||
{
|
||||
active = activeIn;
|
||||
}
|
||||
|
||||
auto getFanoutAnimator() const { return fanoutAnimator; }
|
||||
|
||||
auto getFillAnimator() const { return fillAnimator; }
|
||||
|
||||
Point<float> centre;
|
||||
float initialRadius = 0.0f, initialThickness = 0.0f;
|
||||
Colour colour = Colours::white;
|
||||
|
||||
private:
|
||||
bool active = false;
|
||||
float radius = 0.0f, thickness = 0.0f, sweepAngle = 0.0f;
|
||||
|
||||
void reset()
|
||||
{
|
||||
sweepAngle = 0.0f;
|
||||
radius = initialRadius;
|
||||
thickness = initialThickness;
|
||||
}
|
||||
|
||||
Animator fanoutAnimator = ValueAnimatorBuilder{}
|
||||
.withOnStartCallback ([this]
|
||||
{
|
||||
reset();
|
||||
active = true;
|
||||
})
|
||||
.withValueChangedCallback ([this] (auto value)
|
||||
{
|
||||
sweepAngle = makeAnimationLimits (2.1f * MathConstants<float>::pi).lerp (value);
|
||||
})
|
||||
.withEasing (Easings::createLinear())
|
||||
.build();
|
||||
|
||||
Animator fillAnimator = ValueAnimatorBuilder{}
|
||||
.withOnStartReturningValueChangedCallback (
|
||||
[this]
|
||||
{
|
||||
const auto thicknessChange = radius - 5.0f;
|
||||
const std::tuple begin { initialRadius, initialThickness };
|
||||
const std::tuple end { initialRadius - thicknessChange / 2.0f, initialThickness + thicknessChange };
|
||||
const auto limits = makeAnimationLimits (begin, end);
|
||||
|
||||
return [this, limits] (auto value)
|
||||
{
|
||||
std::tie (radius, thickness) = limits.lerp (value);
|
||||
};
|
||||
})
|
||||
.withEasing (Easings::createLinear())
|
||||
.build();
|
||||
};
|
||||
|
||||
class Circle : public Paintable
|
||||
{
|
||||
public:
|
||||
Circle (Point<float> centreIn, float radiusIn, Colour colourIn)
|
||||
: centre (centreIn), radius (radiusIn), colour (colourIn)
|
||||
{
|
||||
}
|
||||
|
||||
void paint (Graphics& g) const override
|
||||
{
|
||||
if (! active)
|
||||
return;
|
||||
|
||||
const Graphics::ScopedSaveState s { g };
|
||||
|
||||
g.setColour (colour);
|
||||
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, 2.0f * radius, 2.0f * radius);
|
||||
}
|
||||
|
||||
void setActive (bool activeIn) { active = activeIn; }
|
||||
|
||||
private:
|
||||
Point<float> centre;
|
||||
float radius = 0.0f;
|
||||
Colour colour;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
/* Can return a subpath based on a proportion between [0, 1.0]. Useful for creating an animation
|
||||
where a path is drawn over time.
|
||||
*/
|
||||
class PartialPath
|
||||
{
|
||||
public:
|
||||
explicit PartialPath (std::initializer_list<Point<float>> points)
|
||||
{
|
||||
bool pathIsEmpty = false;
|
||||
|
||||
for (auto p : points)
|
||||
{
|
||||
if (std::exchange (pathIsEmpty, true) == false)
|
||||
{
|
||||
path.startNewSubPath (p);
|
||||
pts.push_back (std::make_pair (p, 0.0f));
|
||||
}
|
||||
else
|
||||
{
|
||||
path.lineTo (p);
|
||||
pts.push_back (std::make_pair (p, path.getLength()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Path getPartialPath (float proportion) const
|
||||
{
|
||||
proportion = jmin (1.0f, proportion);
|
||||
|
||||
Path partialPath;
|
||||
bool pathIsEmpty = false;
|
||||
|
||||
if (pts.size() < 2)
|
||||
return partialPath;
|
||||
|
||||
const auto proportionalLength = path.getLength() * proportion;
|
||||
|
||||
const auto lineTo = [&partialPath, &pathIsEmpty] (Point<float> p)
|
||||
{
|
||||
if (std::exchange (pathIsEmpty, true) == false)
|
||||
partialPath.startNewSubPath (p);
|
||||
else
|
||||
partialPath.lineTo (p);
|
||||
};
|
||||
|
||||
for (const auto& point : pts)
|
||||
{
|
||||
if (point.second > proportionalLength)
|
||||
{
|
||||
lineTo (path.getPointAlongPath (proportionalLength));
|
||||
break;
|
||||
}
|
||||
|
||||
lineTo (point.first);
|
||||
}
|
||||
|
||||
return partialPath;
|
||||
}
|
||||
|
||||
private:
|
||||
Path path;
|
||||
std::vector<std::pair<Point<float>, float>> pts;
|
||||
};
|
||||
|
||||
class Checkmark : public Paintable
|
||||
{
|
||||
public:
|
||||
Checkmark (Rectangle<float> placementIn, float thicknessIn)
|
||||
: placement (placementIn),
|
||||
partialPath { { placement.getX(), placement.getY() + 0.7f * placement.getHeight() },
|
||||
{ placement.getX() + 0.4f * placement.getWidth(), placement.getBottom() },
|
||||
{ placement.getRight(), placement.getY() + 0.2f * placement.getHeight() } },
|
||||
thickness (thicknessIn)
|
||||
{
|
||||
}
|
||||
|
||||
void paint (Graphics& g) const override
|
||||
{
|
||||
if (exactlyEqual (progress, 0.0f))
|
||||
return;
|
||||
|
||||
const Graphics::ScopedSaveState s { g };
|
||||
|
||||
g.setColour (colour);
|
||||
|
||||
const auto p = partialPath.getPartialPath (progress);
|
||||
g.strokePath (p, PathStrokeType { thickness, PathStrokeType::JointStyle::curved, PathStrokeType::EndCapStyle::rounded });
|
||||
}
|
||||
|
||||
void setProgress (float p) { progress = p; }
|
||||
|
||||
private:
|
||||
Rectangle<float> placement;
|
||||
PartialPath partialPath;
|
||||
float progress = 0.0f;
|
||||
float thickness = 10.0f;
|
||||
Colour colour { Colours::white };
|
||||
};
|
||||
|
||||
} // namespace Shapes
|
||||
|
||||
inline auto createComponentMover (Component& component, Rectangle<int> targetBounds)
|
||||
{
|
||||
const auto boundsToTuple = [] (auto b)
|
||||
{
|
||||
return std::make_tuple (b.getX(), b.getY(), b.getWidth(), b.getHeight());
|
||||
};
|
||||
|
||||
const auto begin = boundsToTuple (component.getBoundsInParent());
|
||||
const auto end = boundsToTuple (targetBounds);
|
||||
const auto limits = makeAnimationLimits (begin, end);
|
||||
|
||||
return [&component, limits] (auto v)
|
||||
{
|
||||
const auto [x, y, w, h] = limits.lerp (v);
|
||||
component.setBounds (x, y, w, h);
|
||||
};
|
||||
}
|
||||
|
||||
class AnimatedCheckmark : public Paintable
|
||||
{
|
||||
public:
|
||||
AnimatedCheckmark (Point<float> centre, float radius, float thickness, Colour colour)
|
||||
: arc (centre, radius, thickness, colour),
|
||||
circle (centre, radius, colour),
|
||||
checkmark (Rectangle<float> { centre.getX() - radius,
|
||||
centre.getY() - radius,
|
||||
2.0f * radius,
|
||||
2.0f * radius }.reduced (radius * 0.4f),
|
||||
thickness)
|
||||
{
|
||||
}
|
||||
|
||||
void paint (Graphics& g) const override
|
||||
{
|
||||
arc.paint (g);
|
||||
circle.paint (g);
|
||||
checkmark.paint (g);
|
||||
}
|
||||
|
||||
auto getAnimator() const { return animator; }
|
||||
|
||||
private:
|
||||
Shapes::Arc arc;
|
||||
Shapes::Circle circle;
|
||||
Shapes::Checkmark checkmark;
|
||||
|
||||
Animator animator = [&]
|
||||
{
|
||||
circle.setActive (false);
|
||||
|
||||
const auto checkmarkAnimator = ValueAnimatorBuilder{}
|
||||
.withEasing (Easings::createEaseOutBack())
|
||||
.withDurationMs (450)
|
||||
.withValueChangedCallback ([this] (auto value)
|
||||
{
|
||||
checkmark.setProgress (value);
|
||||
})
|
||||
.build();
|
||||
|
||||
return AnimatorSetBuilder (arc.getFanoutAnimator())
|
||||
.followedBy (arc.getFillAnimator())
|
||||
.followedBy ([this]
|
||||
{
|
||||
arc.setActive (false);
|
||||
circle.setActive (true);
|
||||
})
|
||||
.followedBy (checkmarkAnimator)
|
||||
.build();
|
||||
}();
|
||||
};
|
||||
|
||||
class FallingBall : public Component
|
||||
{
|
||||
private:
|
||||
template <typename T>
|
||||
auto getDimensions (Rectangle<T> r)
|
||||
{
|
||||
return std::make_tuple (r.getX(), r.getY(), r.getWidth(), r.getHeight());
|
||||
}
|
||||
|
||||
public:
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.setColour (Colour { 0xff179af0 });
|
||||
const auto [x, y, width, height] = getDimensions (getLocalBounds().toFloat());
|
||||
g.fillEllipse (x, y, width, height);
|
||||
}
|
||||
|
||||
Animator updateAndGetAnimator()
|
||||
{
|
||||
newBounds = getBoundsInParent().withY (getParentHeight() - getHeight());
|
||||
return fallAnimator;
|
||||
}
|
||||
|
||||
private:
|
||||
Rectangle<int> newBounds;
|
||||
|
||||
Animator fallAnimator = ValueAnimatorBuilder{}
|
||||
.withOnStartReturningValueChangedCallback ([this] { return createComponentMover (*this, newBounds); })
|
||||
.withEasing (Easings::createBounce())
|
||||
.withDurationMs (600)
|
||||
.build();
|
||||
};
|
||||
|
||||
class PulsingCheckmark : public Component
|
||||
{
|
||||
public:
|
||||
PulsingCheckmark (Point<float> centre, float radius)
|
||||
: checkmark ({ radius, radius }, radius, radius / 6.25f, Colour { 0xff1bc211 })
|
||||
{
|
||||
const auto widthAndHeight = 2 * (int) radius;
|
||||
const auto x = (int) (centre.getX() - radius);
|
||||
const auto y = (int) (centre.getY() - radius);
|
||||
|
||||
setBounds (x, y, widthAndHeight, widthAndHeight);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
checkmark.paint (g);
|
||||
}
|
||||
|
||||
void mouseDown (const MouseEvent&) override
|
||||
{
|
||||
animator.complete();
|
||||
}
|
||||
|
||||
Animator getAnimator() const { return animator; }
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AnimatedCheckmark checkmark;
|
||||
|
||||
Animator animator = [&]
|
||||
{
|
||||
const auto checkmarkAnimator = checkmark.getAnimator();
|
||||
const auto checkmarkDuration = checkmarkAnimator.getDurationMs();
|
||||
|
||||
const auto pulseAnimator = ValueAnimatorBuilder{}
|
||||
.withEasing (Easings::createOnOffRamp())
|
||||
.withOnStartReturningValueChangedCallback (
|
||||
[this]
|
||||
{
|
||||
const auto radius = (float) getWidth() / 2.0f;
|
||||
const auto centreInParent = getBoundsInParent().toFloat().getPosition()
|
||||
+ Point<float> { radius, radius };
|
||||
|
||||
return [this, centreInParent] (auto value)
|
||||
{
|
||||
setTransform (AffineTransform::translation (-centreInParent)
|
||||
.followedBy (AffineTransform::scale (1.0f + 0.2f * value))
|
||||
.followedBy (AffineTransform::translation (centreInParent)));
|
||||
};
|
||||
})
|
||||
.build();
|
||||
|
||||
const auto timeBeforePulseAnimation = checkmarkDuration - pulseAnimator.getDurationMs();
|
||||
|
||||
const auto repaintAnimator = ValueAnimatorBuilder{}
|
||||
.withValueChangedCallback ([this] (auto) { repaint(); })
|
||||
.runningInfinitely()
|
||||
.build();
|
||||
|
||||
return AnimatorSetBuilder (checkmarkAnimator)
|
||||
.togetherWith (repaintAnimator)
|
||||
.togetherWith (timeBeforePulseAnimation)
|
||||
.followedBy (pulseAnimator)
|
||||
.followedBy ([repaintAnimator] { repaintAnimator.complete(); })
|
||||
.build();
|
||||
}();
|
||||
};
|
||||
|
||||
// Displays the PulsingCheckmark as it looks when its animation is complete
|
||||
class CompletedCheckmark : public Component
|
||||
{
|
||||
public:
|
||||
explicit CompletedCheckmark (std::function<void()> onClickIn)
|
||||
: onClick (std::move (onClickIn)) {}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
const auto getCentre = [this]
|
||||
{
|
||||
return Point<float> { (float) getWidth() / 2, (float) getHeight() / 2 };
|
||||
};
|
||||
|
||||
const auto radius = (float) std::min (getWidth(), getHeight()) / 2.0f;
|
||||
checkmark = std::make_unique<PulsingCheckmark> (getCentre(), radius);
|
||||
checkmark->setInterceptsMouseClicks (false, false);
|
||||
addAndMakeVisible (*checkmark);
|
||||
checkmark->getAnimator().start();
|
||||
checkmark->getAnimator().complete();
|
||||
checkmark->getAnimator().update (0);
|
||||
}
|
||||
|
||||
void mouseDown (const MouseEvent&) override
|
||||
{
|
||||
NullCheckedInvocation::invoke (onClick);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<PulsingCheckmark> checkmark;
|
||||
std::function<void()> onClick;
|
||||
};
|
||||
|
||||
class BallToolComponent : public Component
|
||||
{
|
||||
public:
|
||||
explicit BallToolComponent (std::function<void()> onClickIn)
|
||||
: onClick (std::move (onClickIn)) {}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.setColour (Colour { 0xff179af0 });
|
||||
g.fillEllipse (getLocalBounds().toFloat());
|
||||
}
|
||||
|
||||
void mouseDown (const MouseEvent&) override
|
||||
{
|
||||
NullCheckedInvocation::invoke (onClick);
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void()> onClick;
|
||||
};
|
||||
|
||||
class AnimatorsDemo : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
AnimatorsDemo()
|
||||
{
|
||||
welcomeComponent.setOnAnimatedClickEnd ([this]
|
||||
{
|
||||
toolsPanel.open();
|
||||
});
|
||||
addAndMakeVisible (welcomeComponent);
|
||||
|
||||
toolsPanel.onClose = [this] { welcomeComponent.reset(); };
|
||||
toolsPanel.addToolComponent (std::make_unique<CompletedCheckmark> ([this] { selectedTool = SelectedTool::checkmark; }));
|
||||
toolsPanel.addToolComponent (std::make_unique<BallToolComponent> ([this]
|
||||
{
|
||||
selectedTool = SelectedTool::ball;
|
||||
}));
|
||||
addAndMakeVisible (toolsPanel);
|
||||
|
||||
setSize (600, 400);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
|
||||
|
||||
for (const auto& paintable : objects)
|
||||
paintable.second->paint (g);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
welcomeComponent.setBounds (getLocalBounds());
|
||||
}
|
||||
|
||||
void mouseDown (const MouseEvent& event) override
|
||||
{
|
||||
switch (selectedTool)
|
||||
{
|
||||
case SelectedTool::checkmark:
|
||||
makeCheckmark (event.getPosition().toFloat());
|
||||
break;
|
||||
|
||||
case SelectedTool::ball:
|
||||
makeBall (event.getPosition().toFloat());
|
||||
break;
|
||||
|
||||
case SelectedTool::none:
|
||||
toolsPanel.wobbleLabel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void makeCheckmark (Point<float> centre)
|
||||
{
|
||||
auto checkmark = std::make_unique<PulsingCheckmark> (centre, 50.0f);
|
||||
updater.addAnimator (checkmark->getAnimator(), [this] { toolComponent.reset(); });
|
||||
checkmark->getAnimator().start();
|
||||
addAndMakeVisible (*checkmark, 0);
|
||||
toolComponent = std::move (checkmark);
|
||||
}
|
||||
|
||||
void makeBall (Point<float> centre)
|
||||
{
|
||||
auto ball = std::make_unique<FallingBall>();
|
||||
addAndMakeVisible (*ball, 0);
|
||||
const auto radius = 50.0f;
|
||||
const Rectangle<float> bounds { centre.getX() - radius, centre.getY() - radius, 2 * radius, 2 * radius };
|
||||
ball->setBounds (bounds.toNearestInt());
|
||||
auto animator = ball->updateAndGetAnimator();
|
||||
updater.addAnimator (animator, [this] { toolComponent.reset(); });
|
||||
animator.start();
|
||||
toolComponent = std::move (ball);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
VBlankAnimatorUpdater updater { this };
|
||||
|
||||
struct WelcomeComponent : public Component
|
||||
{
|
||||
WelcomeComponent()
|
||||
{
|
||||
startButton.onClick = [this]
|
||||
{
|
||||
animateForward = true;
|
||||
buttonAnimator.start();
|
||||
updater.addAnimator (buttonAnimator, [this] { updater.removeAnimator (buttonAnimator); });
|
||||
};
|
||||
|
||||
addAndMakeVisible (startButton);
|
||||
}
|
||||
|
||||
void setOnAnimatedClickEnd (std::function<void()> onClickIn)
|
||||
{
|
||||
onClick = std::move (onClickIn);
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
animateForward = false;
|
||||
buttonAnimator.start();
|
||||
updater.addAnimator (buttonAnimator, [this] { updater.removeAnimator (buttonAnimator); });
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
startButton.setBounds (getLocalBounds().withSizeKeepingCentre (140, 40));
|
||||
}
|
||||
|
||||
private:
|
||||
TextButton startButton { "Start demo" };
|
||||
bool animateForward = false;
|
||||
Animator buttonAnimator = [&]
|
||||
{
|
||||
return ValueAnimatorBuilder{}
|
||||
.withOnStartCallback ([this] { setVisible (true); })
|
||||
.withValueChangedCallback ([this] (auto value)
|
||||
{
|
||||
const auto progress = animateForward ? value : (1.0 - value);
|
||||
setAlpha (1.0f - (float) progress);
|
||||
})
|
||||
.withOnCompleteCallback ([this]
|
||||
{
|
||||
setVisible (! animateForward);
|
||||
|
||||
if (animateForward)
|
||||
NullCheckedInvocation::invoke (onClick);
|
||||
})
|
||||
.build();
|
||||
}();
|
||||
|
||||
VBlankAnimatorUpdater updater { this };
|
||||
std::function<void()> onClick;
|
||||
};
|
||||
|
||||
class WobblyLabel : public Component
|
||||
{
|
||||
public:
|
||||
explicit WobblyLabel (const String& text)
|
||||
: label ({}, text)
|
||||
{
|
||||
label.setJustificationType (Justification::right);
|
||||
addAndMakeVisible (label);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
label.setBounds (getLocalBounds().withX ((int) offset));
|
||||
}
|
||||
|
||||
void wobble()
|
||||
{
|
||||
updater.addAnimator (animator, [this] { updater.removeAnimator (animator); });
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private:
|
||||
float offset = 0.0f;
|
||||
Label label;
|
||||
Animator animator = ValueAnimatorBuilder{}.withValueChangedCallback (
|
||||
[this] (float progress)
|
||||
{
|
||||
offset = 10.0f * std::sin (progress * 20.0f) * (1.0f - progress);
|
||||
resized();
|
||||
})
|
||||
.withDurationMs (600)
|
||||
.build();
|
||||
VBlankAnimatorUpdater updater { this };
|
||||
};
|
||||
|
||||
struct ToolsPanel : public Component
|
||||
{
|
||||
static constexpr auto margin = 15;
|
||||
|
||||
ToolsPanel()
|
||||
{
|
||||
shadower.setOwner (this);
|
||||
closeButton.onClick = [this] { close(); };
|
||||
addAndMakeVisible (closeButton);
|
||||
addAndMakeVisible (label);
|
||||
addAndMakeVisible (instructions);
|
||||
addChildComponent (selectionComponent, 0);
|
||||
|
||||
instructions.setJustificationType (Justification::centred);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId).brighter (0.1f));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
closeButton.setBounds (getLocalBounds().removeFromTop (40)
|
||||
.removeFromRight (40)
|
||||
.reduced (5));
|
||||
|
||||
auto bounds = getLocalBounds();
|
||||
instructions.setBounds (bounds.removeFromBottom (30));
|
||||
|
||||
FlexBox flexBox;
|
||||
flexBox.flexDirection = FlexBox::Direction::row;
|
||||
flexBox.flexWrap = FlexBox::Wrap::noWrap;
|
||||
flexBox.justifyContent = FlexBox::JustifyContent::center;
|
||||
flexBox.alignItems = FlexBox::AlignItems::center;
|
||||
|
||||
const auto height = (float) (bounds.getHeight() - 2 * margin);
|
||||
|
||||
flexBox.items.add (FlexItem (label).withWidth (200.0f).withHeight (height));
|
||||
|
||||
for (auto& c : toolComponents)
|
||||
flexBox.items.add (FlexItem (*c).withWidth (height).withHeight (height).withMargin (margin));
|
||||
|
||||
flexBox.performLayout (bounds);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void open()
|
||||
{
|
||||
shouldOpen = true;
|
||||
updater.addAnimator (slideInAnimator, [this] { updater.removeAnimator (slideInAnimator); });
|
||||
slideInAnimator.start();
|
||||
}
|
||||
|
||||
void close()
|
||||
{
|
||||
shouldOpen = false;
|
||||
updater.addAnimator (slideInAnimator, [this]
|
||||
{
|
||||
NullCheckedInvocation::invoke (onClose);
|
||||
updater.removeAnimator (slideInAnimator);
|
||||
});
|
||||
slideInAnimator.start();
|
||||
}
|
||||
|
||||
void addToolComponent (std::unique_ptr<Component> component)
|
||||
{
|
||||
addAndMakeVisible (*component);
|
||||
component->addMouseListener (this, false);
|
||||
toolComponents.push_back (std::move (component));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void mouseUp (const MouseEvent& event) override
|
||||
{
|
||||
if (event.originalComponent == this)
|
||||
return;
|
||||
|
||||
const auto targetBounds = event.originalComponent->getBounds().expanded (std::min (10, margin));
|
||||
|
||||
if (! selectionComponent.isVisible())
|
||||
{
|
||||
selectionComponent.setBounds (targetBounds);
|
||||
selectionComponent.appear (updater);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectionComponent.moveTo (updater, targetBounds);
|
||||
}
|
||||
}
|
||||
|
||||
void wobbleLabel()
|
||||
{
|
||||
label.wobble();
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
class SelectionComponent : public Component
|
||||
{
|
||||
public:
|
||||
SelectionComponent()
|
||||
{
|
||||
setVisible (false);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
arc.paint (g);
|
||||
}
|
||||
|
||||
void appear (VBlankAnimatorUpdater& animatorUpdater)
|
||||
{
|
||||
appearAnimator.start();
|
||||
animatorUpdater.addAnimator (appearAnimator,
|
||||
[this, &animatorUpdater]
|
||||
{
|
||||
animatorUpdater.removeAnimator (appearAnimator);
|
||||
});
|
||||
}
|
||||
|
||||
void moveTo (VBlankAnimatorUpdater& animatorUpdater, Rectangle<int> newBoundsIn)
|
||||
{
|
||||
newBounds = newBoundsIn;
|
||||
moveToNewBoundsAnimator.start();
|
||||
animatorUpdater.addAnimator (moveToNewBoundsAnimator,
|
||||
[this, &animatorUpdater]
|
||||
{
|
||||
animatorUpdater.removeAnimator (moveToNewBoundsAnimator);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
Shapes::Arc arc;
|
||||
Rectangle<int> newBounds;
|
||||
|
||||
Animator appearAnimator = [&]
|
||||
{
|
||||
const auto repaintAnimator =
|
||||
ValueAnimatorBuilder{}
|
||||
.withValueChangedCallback ([this] (auto) { repaint(); })
|
||||
.runningInfinitely()
|
||||
.build();
|
||||
|
||||
return AnimatorSetBuilder { [this]
|
||||
{
|
||||
setVisible (true);
|
||||
arc.centre = Point { getWidth(), getHeight() }.toFloat() / 2;
|
||||
arc.initialRadius = (float) (getWidth()) / 2 - 2.0f;
|
||||
arc.initialThickness = 4.0f;
|
||||
} }
|
||||
.followedBy (repaintAnimator)
|
||||
.togetherWith (arc.getFanoutAnimator())
|
||||
.followedBy ([repaintAnimator] { repaintAnimator.complete(); })
|
||||
.withTimeTransform ([] (auto v) { return 1.5 * v; })
|
||||
.build();
|
||||
}();
|
||||
|
||||
Animator moveToNewBoundsAnimator = ValueAnimatorBuilder{}
|
||||
.withOnStartReturningValueChangedCallback ([this] { return createComponentMover (*this, newBounds); })
|
||||
.withEasing (Easings::createSpring())
|
||||
.withDurationMs (600)
|
||||
.build();
|
||||
};
|
||||
|
||||
WobblyLabel label { "Select animation:" };
|
||||
Label instructions { "", "Click below to animate" };
|
||||
SelectionComponent selectionComponent;
|
||||
std::vector<std::unique_ptr<Component>> toolComponents;
|
||||
DropShadow shadow { Colour { 0x90000000 }, 12, {} };
|
||||
DropShadower shadower { shadow };
|
||||
TextButton closeButton { "X" };
|
||||
|
||||
Animator slideInAnimator = ValueAnimatorBuilder{}
|
||||
.withEasing (Easings::createEaseInOutCubic())
|
||||
.withOnStartReturningValueChangedCallback (
|
||||
[this]
|
||||
{
|
||||
const auto width = getParentWidth() - 2 * margin;
|
||||
const auto height = 130;
|
||||
setBounds (-width, margin, width, height);
|
||||
setVisible (true);
|
||||
|
||||
const auto limits = makeAnimationLimits (-width, margin);
|
||||
|
||||
return [this, limits] (auto value)
|
||||
{
|
||||
const auto progress = std::clamp (shouldOpen ? value : 1.0 - value, 0.0, 1.0);
|
||||
setTopLeftPosition (roundToInt (limits.lerp ((float) progress)), margin);
|
||||
};
|
||||
})
|
||||
.withDurationMs (500)
|
||||
.build();
|
||||
|
||||
VBlankAnimatorUpdater updater { this };
|
||||
bool shouldOpen = true;
|
||||
|
||||
public:
|
||||
std::function<void()> onClose;
|
||||
};
|
||||
|
||||
enum class SelectedTool
|
||||
{
|
||||
none,
|
||||
checkmark,
|
||||
ball
|
||||
};
|
||||
|
||||
WelcomeComponent welcomeComponent;
|
||||
ToolsPanel toolsPanel;
|
||||
SelectedTool selectedTool = SelectedTool::none;
|
||||
std::unique_ptr<Component> toolComponent;
|
||||
std::map<Paintable*, std::unique_ptr<Paintable>> objects;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnimatorsDemo)
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue