-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Animated icons #272
Comments
I have to say until now so haven’t really given much thought about animated svg icons (which is why you haven’t found any built-in way to achieve it) but I'm definitely open to it. Would you mind sharing the code you used to make this working? |
Sure, I did a little example with a button using a MouseListener and a timer. FPS is also adjustable changing the corresponding value. Icon is loaded as usual, and the listener remembers the time when the animation stopped so it won't reset and continue from where it left off instead just like in my GIF up there. button.addMouseListener(new MouseAdapter() {
java.util.Timer timer;
double time;
int fps = 60;
@Override
public void mouseEntered(MouseEvent e) {
Icon icon = button.getIcon();
if (icon instanceof DarkSVGIcon) {
DarkSVGIcon darkSVGIcon = (DarkSVGIcon) icon;
darkSVGIcon.setDirectRenderingMode(true);
SVGUniverse universe = darkSVGIcon.getSVGIcon().getSvgUniverse();
long start = System.currentTimeMillis();
double offset = time;
timer = new java.util.Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
time = offset + (System.currentTimeMillis() - start)/1000d;
universe.setCurTime(time);
universe.updateTime();
button.repaint();
} catch (SVGException ex) {
ex.printStackTrace();
}
}
}, 0, 1000/fps);
}
}
@Override
public void mouseExited(MouseEvent e) {
Icon icon = button.getIcon();
if (icon instanceof DarkSVGIcon) {
SVGUniverse universe = ((DarkSVGIcon) icon).getSVGIcon().getSvgUniverse();
time = universe.getCurTime();
}
if (timer != null)
timer.cancel();
}
}); |
I just thought of something else. Remembering the animation time is good for animations that loop indefinitely and freeze like the one in my example, but not for those that play once or a limited number of times. Maybe that should also be a flag so in mouseExited: if (rememberTime)
time = universe.getCurTime();
else
time = 0; Edit: I'll try to get a more elegant solution that's able to handle this, and can be applied more easily to components and post it here. |
Okay, I got it working and I think it's good enough, now it can also play the animation backwards on mouse exit (I'll include an example GIF below). Should be easy enough to implement in any given component that uses an SVG icon as an image source. Now, there's only a little problem: In order for the reverse to work, it needs to know the full animation time and whether or not it's an infinite loop. I hard-coded it for the time being, but it could either be received by the function as an argument or extracted from the SVG (however, extracting the animation duration from the SVG could maybe be a little bit tricky with chained animations and that stuff?). For whatever reason sometimes it flickers, but it's uncommon and lasts so little that I decided not to worry about it. Here's the full example code (GUIUtils.loadIcon() just returns an Icon using the icon loader): public static void main(String[] args) {
LafManager.install(new OneDarkTheme());
DarkSVGIcon icon = (DarkSVGIcon) GUIUtils.loadIcon("heart.svg", 200, 200);
JPanel animatedPanel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int size = Math.min(getWidth(), getHeight());
int x = (getWidth() - size)/2, y = (getHeight() - size)/2;
g.drawImage(icon.createImage(size, size), x, y, null);
}
};
animatedPanel.setPreferredSize(new Dimension(200, 200));
setComponentAnimated(() -> icon, animatedPanel, false, true, 0.5);
testComponent(animatedPanel);
}
private static void testComponent(Component component) {
JFrame frame = new JFrame();
JPanel panel = new JPanel(new BorderLayout());
panel.add(component, BorderLayout.CENTER);
frame.setContentPane(panel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
static int fps = 60;
private static MouseListener createAnimationListener(Supplier<Icon> supplier, JComponent component,
boolean rememberTime, boolean reverseOnExit, double reverseDelay) {
return new MouseAdapter() {
private java.util.Timer timer;
private DarkSVGIcon svgIcon;
@Override
public void mouseEntered(MouseEvent e) {
Icon icon = supplier.get();
if (icon instanceof DarkSVGIcon) {
if (timer != null)
timer.cancel();
svgIcon = (DarkSVGIcon) icon;
svgIcon.setDirectRenderingMode(true);
SVGUniverse universe = svgIcon.getSVGIcon().getSvgUniverse();
long start = System.currentTimeMillis();
double offset = universe.getCurTime();
timer = new java.util.Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
universe.setCurTime(offset + (System.currentTimeMillis() - start)/1000d);
universe.updateTime();
SwingUtilities.invokeLater(component::repaint);
} catch (SVGException ex) {
ex.printStackTrace();
}
}
}, 0, 1000/fps);
}
}
@Override
public void mouseExited(MouseEvent e) {
if (timer != null)
timer.cancel();
if (!rememberTime) {
SVGUniverse universe = svgIcon.getSVGIcon().getSvgUniverse();
if (reverseOnExit) {
double animTime = 2.5;
boolean infiniteLoop = false;
double delta = infiniteLoop ? universe.getCurTime()%animTime : Math.min(universe.getCurTime(), animTime + reverseDelay);
long start = System.currentTimeMillis();
timer = new java.util.Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
double newTime;
newTime = delta - (System.currentTimeMillis() - start)/1000d;
if (newTime < 0) {
newTime = 0;
timer.cancel();
}
universe.setCurTime(newTime);
try {
universe.updateTime();
SwingUtilities.invokeLater(component::repaint);
} catch (SVGException ex) {
ex.printStackTrace();
}
}
}, 0, 1000/fps);
} else {
universe.setCurTime(0);
try {
universe.updateTime();
} catch (SVGException ex) {
ex.printStackTrace();
}
component.repaint();
}
}
}
};
}
public static void setComponentAnimated(Supplier<Icon> iconSupplier, JComponent component, boolean rememberTime,
boolean reverseOnExit, double reverseDelay) {
component.addMouseListener(createAnimationListener(iconSupplier, component, rememberTime, reverseOnExit, reverseDelay));
} The SVG I used in the example: <svg viewBox="0 0 530 530" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(9, 37)" stroke="#31E8FF" stroke-width="10" fill="#31E8FF" fill-opacity="0" stroke-dasharray="1570" stroke-dashoffset="0" d="m471.382812 44.578125c-26.503906-28.746094-62.871093-44.578125-102.410156-44.578125-29.554687 0-56.621094 9.34375-80.449218 27.769531-12.023438 9.300781-22.917969 20.679688-32.523438 33.960938-9.601562-13.277344-20.5-24.660157-32.527344-33.960938-23.824218-18.425781-50.890625-27.769531-80.445312-27.769531-39.539063 0-75.910156 15.832031-102.414063 44.578125-26.1875 28.410156-40.613281 67.222656-40.613281 109.292969 0 43.300781 16.136719 82.9375 50.78125 124.742187 30.992188 37.394531 75.535156 75.355469 127.117188 119.3125 17.613281 15.011719 37.578124 32.027344 58.308593 50.152344 5.476563 4.796875 12.503907 7.4375 19.792969 7.4375 7.285156 0 14.316406-2.640625 19.785156-7.429687 20.730469-18.128907 40.707032-35.152344 58.328125-50.171876 51.574219-43.949218 96.117188-81.90625 127.109375-119.304687 34.644532-41.800781 50.777344-81.4375 50.777344-124.742187 0-42.066407-14.425781-80.878907-40.617188-109.289063zm0 0">
<animate attributeName="stroke-dashoffset" from="0" to="1570" dur="2s" fill="freeze"/>
<animate attributeName="fill-opacity" from="0" to="1" dur="0.5s" begin="2s" fill="freeze"/>
</path>
</svg> The icon did flicker during the demo, but due to the GIF's framerate it doesn't show. I'll try to fix the flickering and update the code if I manage to find a solution. |
Sorry I haven’t gotten around looking into this, but your progress looks great! You might want to look into the Animator class for the time handling. It supports pausing (suspending) and reverting the timer. As for the issue with the need for hard-coding the animation duration I don’t really want to give a convenience api just now (it’s probably possible to compute it from the svg dom). I plan on replacing the underlying svg implementation in the future and don’t want to implement something which I may not be able to support then. |
I'll look into it and see what I can do. Also, I'll move the duration to the function params so it shouldn't be a problem. |
Thank you very much for your efforts! |
I'm just doing my part to contribute to this amazing library. |
I have implemented a new version of the import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashSet;
import java.util.function.Supplier;
import javax.swing.*;
import com.github.weisj.darklaf.properties.icons.DarkSVGIcon;
import com.kitfox.svg.SVGException;
import com.kitfox.svg.SVGUniverse;
public class SVGAnimator extends Animator {
public enum Event {
onClick,
onHover
}
// Keep a single instance for easy removal on component detaching
private final AnimationListener listener;
private final Supplier<Icon> iconSupplier;
private DarkSVGIcon darkSVGIcon;
private volatile SVGUniverse universe;
private final HashSet<JComponent> attachedTo;
private Event trigger;
private boolean reverseOnExit;
private int mouseButton = MouseEvent.BUTTON1;
private final float animationDuration;
public SVGAnimator(DarkSVGIcon icon, float duration, float delay, RepeatMode repeatMode, Event trigger) {
this(() -> icon, duration, delay, repeatMode, trigger);
}
// Set a null trigger in order to make it manual (via play(), forward() or backward()) only.
public SVGAnimator(Supplier<Icon> iconSupplier, float durationInSeconds, float delayInSeconds,
RepeatMode repeatMode, Event trigger) {
super(Math.round(durationInSeconds * 1000), Math.round(delayInSeconds * 1000), DefaultInterpolator.LINEAR,
repeatMode);
this.iconSupplier = iconSupplier;
attachedTo = new HashSet<>(1); // Most likely used for single components
listener = new AnimationListener();
animationDuration = durationInSeconds;
this.trigger = trigger;
updateIcon(false);
}
// These two functions only work if the Animator was given a supplier instead of a static icon
// ======================
public void updateIcon() {
updateIcon(true);
}
/*
* I included the option to not turn back to non-direct rendering in case the icon is used somewhere
* else. I'm not completely sure, but I believe darklaf keeps the icons themselves cached, if that's
* the case, other component using the animated icon will suddenly stop being animated. Anyway it
* doesn't hurt.
*/
/**
* Retrieves the new icon from the supplier and updates the universe.
*
* @param updateRenderingMode If true, the old icon will revert to non-direct rendering mode,
* enabling the cache.
*/
public void updateIcon(boolean updateRenderingMode) {
Icon icon = iconSupplier.get();
if (icon instanceof DarkSVGIcon) {
updateIcon0((DarkSVGIcon) icon, updateRenderingMode);
}
}
// ==================================================================================================================
// These two will take an icon and update it directly
// ===============================================================
public void updateIcon(Icon icon) {
updateIcon(icon, true);
}
/**
* Changes the icon for a new one. Supplier is unaffected, so the previous icon can potentially be
* recovered by calling {@code updateIcon()}
*
* @param icon New icon
* @param updateRenderingMode If true, the old icon will revert to non-direct rendering mode,
* enabling the cache.
*/
public void updateIcon(Icon icon, boolean updateRenderingMode) {
if (icon instanceof DarkSVGIcon)
updateIcon0((DarkSVGIcon) icon, updateRenderingMode);
}
// ==================================================================================================================
private void updateIcon0(DarkSVGIcon icon, boolean updateRenderingMode) {
if (!icon.equals(darkSVGIcon)) {
if (updateRenderingMode && darkSVGIcon != null)
darkSVGIcon.setDirectRenderingMode(false);
darkSVGIcon = icon;
darkSVGIcon.setDirectRenderingMode(true);
universe = new SVGUniverse();
darkSVGIcon.getSVGIcon().setSvgUniverse(universe);
}
}
/**
* Adds the component to the collection of components to be repainted each frame, also adds the
* animation listener to the component. Essentially, the component becomes both a trigger and a
* display (assuming the component paints the icon in some way) for the icon after calling this
* method.
*
* @see SVGAnimator#detachFrom(JComponent)
* @param c Component
*/
public void attachTo(JComponent c) {
synchronized (attachedTo) {
attachedTo.add(c);
}
c.addMouseListener(listener);
}
/**
* The opposite of {@code attachTo()}. Removes the component from the collection and the animation
* listener from the component, which means the component stops triggering the icon and being
* repainted. However, if the component still displays the icon, and other component triggers the
* animation, it will still reflect the changes after repainting.
*
* @see SVGAnimator#attachTo(JComponent)
* @param c Component
*/
public void detachFrom(JComponent c) {
synchronized (attachedTo) {
attachedTo.remove(c);
}
c.removeMouseListener(listener);
}
/**
* Sets the mouse button to trigger the animation when the {@code trigger} variable is set to
* {@code Event.onClick}, it has no effect for hovering animations.
*
* @see Event
* @see AnimationListener#mouseClicked(MouseEvent)
* @param mouseButton Which mouse button will trigger the animation on clicking the component.
*/
public void setMouseButton(int mouseButton) {
this.mouseButton = mouseButton;
}
/**
* If the {@code trigger} variable is set to {@code Event.onHover}, determines whether the animation
* will play backwards on mouse exiting the component.
*
* @see Event
* @see AnimationListener#mouseExited(MouseEvent)
* @param reverseOnExit Animation reverses on mouse exit if true
*/
public void setReverseOnExit(boolean reverseOnExit) {
this.reverseOnExit = reverseOnExit;
// Indicates that the fraction should be kept at 1 after finishing the animation, so it can be
// reversed afterwards. No effect on repeating animations.
setRepeatMode(RepeatMode.DO_NOT_REPEAT_FREEZE);
// I should be able to use keepEndState as a reversing indicator too, but I'll keep the other
// variable for
// clarity
}
/**
* Sets the trigger of the animation.
*
* @see Event
* @param trigger A value from the {@code Event} enum
*/
public void setTrigger(Event trigger) {
this.trigger = trigger;
}
@Override
protected void paintAnimationFrame(float fraction) {
universe.setCurTime(fraction * animationDuration);
try {
universe.updateTime();
synchronized (attachedTo) {
for (JComponent c : attachedTo)
c.repaint();
}
} catch (SVGException e) {
e.printStackTrace();
}
}
private class AnimationListener extends MouseAdapter {
@Override
public void mouseEntered(MouseEvent e) {
if (Event.onHover.equals(trigger)) {
playForwards();
}
}
@Override
public void mouseExited(MouseEvent e) {
if (Event.onHover.equals(trigger)) {
if (reverseOnExit) {
// If the animation is a loop but is set to reverse, it will reverse only to the starting point
// instead of redoing all the loops backwards.
playBackwards(isRunning());
} else if (repeatMode().isRepeating())
pause();
else {
stop();
}
}
}
@Override
public void mouseClicked(MouseEvent e) {
if (Event.onClick.equals(trigger) && e.getButton() == mouseButton) playForwards(false);
}
}
} |
That's so nice, thank you! If you don't mind the question, do you think the new SVG implementation will support filters? I've been dying to use some of them with my icons. |
I absolutely don’t mind the question. Support for filters in the new implementation is definitely planned: https://github.com/weisJ/jsvg |
That's wonderful. I'll totally check it out when I can. |
Just to give an update for this issue. I am at the point where I can replace all current usage of svgSalamander in darklaf with jsvg. Though I still haven't gotten around implementing animation support, which will probably be an considerate amount work. If you want to accelerate the progress on this issue I would highly appreciate you contributing to jsvg. The same goes for any filters which you want to see implemented :) |
I'd love to contribute, but I'm not sure if I can be of much help. In any case I'll take a look at it later and see if there's anything I can help with. |
Follow up to my previous comment. Sorry I wasn't able to help before, I've been pretty busy with studies and some other stuff. However, I just had one of those eureka moments and I think I came up with an idea for an animator that will keep all features of the one I posted here (easily reverse the animation, pause it, etc), while also implementing something very important that I skipped (pun absolutely intended): frame-skipping. The basic idea is that it will account for slower (or faster, if it happened, for whatever reason) executions and increase (or decrease) the fraction delta accordingly, so it will catch up. I'd just have to figure out a couple conditions and organize the code properly. Using this 'hybrid' approach makes it easy to reverse the animation by just changing the sign of the delta, just like before, while also avoiding all the hassle of dealing with times. :D I'm not very familiarized with image rendering, so I doubt I can be of much help with actually drawing the SVGs, but I think at least I can help with the animator to speed up development a little bit. Anyway, let me know what you think and I'll get to it as soon as I can. |
General description
First of all, I don't know if this was already implemented somehow, but I couldn't find anything, maybe I'm just blind. But I dug through the code of both darklaf and svgSalamander and found that it was actually possible to animate it on mouse hover, but it was kinda difficult to get it working. So, my question is if it'd be possible to include it somehow so it's easier to do? Like a property or something that determines if the component will animate the icon or not when hovered. Direct rendering mode needs to be true in the DarkSVGIcon for this to work, so I assume it would cause a slight performance penalty since it's sacrificing the cache, but I figured that wasn't necessarily a problem if direct rendering mode is enabled only when animations were enabled.
This suggestion is:
For visual suggestions
This is how it looks while running. I also tested changing theme and theme colors and it didn't seem to have any issues with that.
For behavioral suggestions
The icon animates when the mouse hovers the component. The animation is given in the SVG file, of course.
The text was updated successfully, but these errors were encountered: