Skip to content

Commit

Permalink
feat: animated icons, first POC
Browse files Browse the repository at this point in the history
  • Loading branch information
toinux committed Mar 28, 2024
1 parent cfb15ff commit 75ccc53
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.antonus.anothertime.model;

public record AnimatedFrame(int index, int[] frame) {
}
22 changes: 22 additions & 0 deletions src/main/java/org/antonus/anothertime/model/AnimatedIcon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.antonus.anothertime.model;

import java.util.TreeMap;

public class AnimatedIcon {
int duration = 0;
TreeMap<Integer, AnimatedFrame> frames = new TreeMap<>();

public void addFrame(int index, int delay, int[] frame) {
frames.put(duration, new AnimatedFrame(index, frame));
duration += delay;
}

public AnimatedFrame getFrame() {
if (duration == 0) {
return frames.floorEntry(0).getValue();
}
// TODO : renvoyer l'entry directement pour avoir la clef que l'on pourrait utiliser dans le cache du dimmed icon ?
return frames.floorEntry((int)(System.currentTimeMillis() % duration)).getValue();
}

}
60 changes: 56 additions & 4 deletions src/main/java/org/antonus/anothertime/service/AwtrixService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.antonus.anothertime.model.AnimatedIcon;
import org.antonus.anothertime.model.AwtrixSettings;
import org.antonus.anothertime.model.AwtrixStats;
import org.antonus.anothertime.rest.AwtrixClient;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.PixelGrabber;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;

import static org.antonus.anothertime.utils.ColorUtils.rgb888;

Expand All @@ -36,16 +46,58 @@ public class AwtrixService {
private String currentApp = "anothertime";

@Cacheable(value = "icons", sync = true)
public int[] getIcon(String iconName, String defaultIcon) {
public AnimatedIcon getIcon(String iconName, String defaultIcon) {
if (null == iconName || iconName.isBlank() || iconName.equalsIgnoreCase("default")) {
return getDefaultIcon(defaultIcon);
AnimatedIcon defaultAnimatedIcon = new AnimatedIcon();
// TODO : animate default icon too
defaultAnimatedIcon.addFrame(0, 0, getDefaultIcon(defaultIcon));
return defaultAnimatedIcon;
// return getDefaultIcon(defaultIcon);
}
try {
return imageToBmp(ImageIO.read(new ByteArrayInputStream(awtrixClient.getIcon(iconName))));

ImageReader ir = ImageIO.getImageReadersBySuffix("gif").next();
ImageInputStream is = ImageIO.createImageInputStream(new ByteArrayInputStream(awtrixClient.getIcon(iconName)));
ir.setInput(is, false);
int numFrames = ir.getNumImages(true);
AnimatedIcon animatedIcon = new AnimatedIcon();
for (int i = 0; i < numFrames; i++) {
animatedIcon.addFrame(i, getFrameDelay(ir, i), imageToBmp(ir.read(i)));
}
return animatedIcon;

// return imageToBmp(ImageIO.read(new ByteArrayInputStream(awtrixClient.getIcon(iconName))));
} catch (Exception e) {
log.info("could not load icon {}, loading default icon {} instead", iconName, defaultIcon);
return getDefaultIcon(defaultIcon);
AnimatedIcon defaultAnimatedIcon = new AnimatedIcon();
// TODO : animate default icon too
defaultAnimatedIcon.addFrame(0,0, getDefaultIcon(defaultIcon));
return defaultAnimatedIcon;
//return getDefaultIcon(defaultIcon);
}
}

private static int getFrameDelay(ImageReader reader, int frameIndex) throws IOException {
// Get the metadata of the current frame
int delay = 0;
int imageMetadataIndex = reader.getMinIndex() + frameIndex;
IIOMetadata imageMetadata = reader.getImageMetadata(imageMetadataIndex);
String metaFormatName = imageMetadata.getNativeMetadataFormatName();

IIOMetadataNode root = (IIOMetadataNode) imageMetadata.getAsTree(metaFormatName);
NodeList children = root.getElementsByTagName("GraphicControlExtension");

// Loop through GraphicControlExtension nodes to find delay time
for (int i = 0; i < children.getLength(); i++) {
Node nodeItem = children.item(i);
NamedNodeMap attr = nodeItem.getAttributes();
Node delayNode = attr.getNamedItem("delayTime");
if (delayNode != null) {
delay = Integer.parseInt(delayNode.getNodeValue()) * 10; // Convert to milliseconds
break;
}
}
return delay;
}

private int[] getDefaultIcon(String defaultIcon) {
Expand Down
38 changes: 27 additions & 11 deletions src/main/java/org/antonus/anothertime/service/IconsService.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package org.antonus.anothertime.service;

import lombok.RequiredArgsConstructor;
import org.antonus.anothertime.model.AnimatedFrame;
import org.antonus.anothertime.model.AnimatedIcon;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.awt.*;
import java.math.BigDecimal;
import java.math.RoundingMode;

import static org.antonus.anothertime.utils.ColorUtils.*;

Expand All @@ -13,22 +19,32 @@
public class IconsService {

private final AwtrixService awtrixService;
private final CacheManager cacheManager;

public int[] getIcon(String iconName, String defaultIcon) {
return awtrixService.getIcon(iconName, defaultIcon);
}

@Cacheable(value = "icons", keyGenerator = "dimmedIconKeyGenerator")
// TODO : trouver un moyer de mettre en cache avec en plus l'index de la frame
// @Cacheable(value = "icons", keyGenerator = "dimmedIconKeyGenerator")
public int[] getDimmedIcon(String iconName, String defaultIcon, float dim) {
int[] icon = getIcon(iconName, defaultIcon);
Cache cache = cacheManager.getCache("icons");
assert cache != null;

// AnimatedIcon animatedIcon = awtrixService.getIcon(iconName, defaultIcon);

AnimatedFrame animatedFrame = awtrixService.getIcon(iconName, defaultIcon).getFrame();
int[] icon = animatedFrame.frame();
if (dim >= 1 || null == icon) {
return icon;
}
int[] result = new int[icon.length];
for (int i = 0; i < icon.length ; i++) {
result[i] = rgb888(dimColor(Color.decode(Integer.toString(icon[i])), dim));
}
return result;

BigDecimal rounded = (new BigDecimal(dim)).setScale(2, RoundingMode.FLOOR);
String cache_key = iconName+"_"+defaultIcon+"_"+rounded+"_"+animatedFrame.index();
return cache.get(cache_key, () -> {
int[] result = new int[icon.length];
for (int i = 0; i < icon.length ; i++) {
result[i] = rgb888(dimColor(Color.decode(Integer.toString(icon[i])), dim));
}
return result;
});

}

public Color defaultColorIfNull(Color color, Color defaultColor) {
Expand Down

0 comments on commit 75ccc53

Please sign in to comment.