Relógio Analógico show U2

10 respostas
Fox_McCloud

Disponibilizo aqui pro pessoal um relógio analógico 100% funcional, em Java (claro).

Embora eu já tenha "brincado" com relógios analógicos antes, esse foi inspirado no relógio que apareceu no telão do show do U2 - 360º após a abertura, enquanto aguardávamos o U2 entrar.

Em anexo o jar auto-executável e no zip o fonte, o properties e as imagens (para criar o projeto no Eclipse cole o conteúdo do src.zip na pasta src).

Ao executar ele não cria um ícone na barra do menu iniciar (já que se trata de um JDialog), mas cria um ícone ao lado do relógio do sistema que permite selecionar se ficará "always on top" (sempre visível) e sair.

A janelinha do relógio pode ser arrastada. ESC ou ALT+F4 (no Windows) também finalizam a aplicação.

Executa em Windows e Linux, não testei em outros sistemas operacionais, mas é esperado que funcione.

Esse fonte utiliza técnicas de animação, as mesmas utilizadas em engines de jogos, otimizando o processamento com um Thread simples e acelerando as imagens na memória de vídeo.

O FPS (taxa de frames por segundo) é calculado como 75% do refresh rate da configuração de vídeo do usuário, resultando em uma animação suave para esse tipo de aplicação.

As setas, traços e grid do fundo são desenhados em tempo de execução. Os desenhos estáticos são desenhados apenas uma vez no início da execução.

Divirtam-se e me dêem crédito se distribuirem ;-)

8)

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.CheckboxMenuItem;
import java.awt.Color;
import java.awt.Composite;
import java.awt.DisplayMode;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.MenuItem;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.SystemTray;
import java.awt.Transparency;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Properties;

import javax.imageio.ImageIO;
import javax.swing.JDialog;

/**
 * Relógio analógico em Java com animação suave e imagens aceleradas em memória de vídeo.
 * 
 * Inspirado no relógio analógico exibido no show do U2 - 360º antes de a banda entrar.
 * 
 * Distribuição livre, apenas me dêem o crédito.
 * 
 * @author Cláudio Loureiro ([email removido])
 */
public class RelogioAnalogico extends JDialog implements Runnable {

	private static final long serialVersionUID = 1L;
	private static final RelogioAnalogico instance;

	public static final GraphicsEnvironment G_ENV;
	public static final GraphicsDevice G_DEV;
	public static final GraphicsConfiguration G_CONF;
	public static final DisplayMode D_MODE;
	
	static {
		G_ENV = GraphicsEnvironment.getLocalGraphicsEnvironment();
		G_DEV = G_ENV.getDefaultScreenDevice();
		G_CONF = G_DEV.getDefaultConfiguration();
		D_MODE = G_DEV.getDisplayMode();
		instance = new RelogioAnalogico();
	}
	
	public static void main(String[] args) {
		try {
			RelogioAnalogico relogio = RelogioAnalogico.getInstance();
			relogio.inicializa();
			relogio.inicia();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	private int tamanho;
	private Color corTracoMenor;
	private Color corTracoMaior;
	private Color corPonteiroHoras;
	private Color corPonteiroMinutos;
	private Color corPonteiroSegundos;
	private BufferedImage javaLogo;
	private BufferedImage imagemFundo;
	private BufferedImage moldura;
	private BufferedImage pontoCentral;
	private int espessuraLinha;
	
	private int posXCentro;
	private int posYCentro;

	private double incrementoRaio;
	private BufferedImage fundo;
	private BufferedImage ponteiroSegundos;
	private BufferedImage ponteiroMinutos;
	private BufferedImage ponteiroHoras;
	
	private BufferedImage bufferDeTras;
	private BufferedImage bufferDaFrente;
	
	private Calendar horario;
	private volatile boolean executando;
	
	private int posicaoXClique;
	private int posicaoYClique;
	
	private RelogioAnalogico() {
		super();
		horario = new GregorianCalendar();
	}
	
	private void inicializa() throws Exception {
		Properties propriedades = new Properties();
		propriedades.load(this.getClass().getResourceAsStream("/config.properties"));
		tamanho = Integer.parseInt(propriedades.getProperty("tamanho"));
		corTracoMenor = new Color(Integer.parseInt(propriedades.getProperty("cor_traco_menor"), 16));
		corTracoMaior = new Color(Integer.parseInt(propriedades.getProperty("cor_traco_maior"), 16));
		corPonteiroHoras = new Color(Integer.parseInt(propriedades.getProperty("cor_ponteiro_horas"), 16));
		corPonteiroMinutos = new Color(Integer.parseInt(propriedades.getProperty("cor_ponteiro_minutos"), 16));
		corPonteiroSegundos = new Color(Integer.parseInt(propriedades.getProperty("cor_ponteiro_segundos"), 16));
		javaLogo = acelera(ImageIO.read(this.getClass().getResourceAsStream(propriedades.getProperty("java_logo"))));
		imagemFundo = acelera(ImageIO.read(this.getClass().getResourceAsStream(propriedades.getProperty("imagem_fundo"))));
		moldura = acelera(ImageIO.read(this.getClass().getResourceAsStream(propriedades.getProperty("moldura"))));
		pontoCentral = acelera(ImageIO.read(this.getClass().getResourceAsStream(propriedades.getProperty("ponto_central"))));
		espessuraLinha = Integer.parseInt(propriedades.getProperty("espessura_linha"));

		incrementoRaio = 0.035 * tamanho;
		bufferDeTras = G_CONF.createCompatibleImage(tamanho, tamanho, Transparency.OPAQUE);
		bufferDaFrente = G_CONF.createCompatibleImage(tamanho, tamanho, Transparency.OPAQUE);
		
		posXCentro = posYCentro = tamanho / 2;
		
		criaImagens();
		
		setSize(tamanho, tamanho);
		setUndecorated(true);
		setResizable(false);
		setLocation(D_MODE.getWidth() - (tamanho + 5), 5);
		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		setAlwaysOnTop(true);
		setFocusable(true);
		
		addFocusListener(new FocusAdapter(){
			@Override
			public void focusLost(FocusEvent e) {
				RelogioAnalogico.this.requestFocus();
			}
		});
		addMouseListener(new MouseAdapter(){
			@Override
			public void mousePressed(MouseEvent e) {
				posicaoXClique = e.getX();
				posicaoYClique = e.getY();
			}
		});
		addMouseMotionListener(new MouseMotionAdapter(){
			@Override
			public void mouseDragged(MouseEvent e) {
				int posicaoXArrasto = e.getX();
				int posicaoYArrasto = e.getY();
				Point location = RelogioAnalogico.this.getLocation();
				RelogioAnalogico.this.setLocation(location.x + (posicaoXArrasto - posicaoXClique), location.y + (posicaoYArrasto - posicaoYClique));
			}
		});
		addKeyListener(new KeyAdapter(){
			@Override
			public void keyPressed(KeyEvent e) {
				if(KeyEvent.VK_ESCAPE == e.getKeyCode()) {
					RelogioAnalogico.this.executando = false;
				}
			}
		});
		addWindowListener(new WindowAdapter(){
			@Override
			public void windowClosing(WindowEvent e) {
				RelogioAnalogico.this.executando = false;
			}
		});
		if(SystemTray.isSupported()) {
			PopupMenu menu = new PopupMenu();
			
			final CheckboxMenuItem mItem1 = new CheckboxMenuItem("Sempre Visível", true);
			mItem1.addItemListener(new ItemListener(){
				@Override
				public void itemStateChanged(ItemEvent e) {
					RelogioAnalogico.this.setAlwaysOnTop(mItem1.getState());
				}
			});
			menu.add(mItem1);
			
			MenuItem mItem2 = new MenuItem("Fechar");
			mItem2.addActionListener(new ActionListener(){
				@Override
				public void actionPerformed(ActionEvent e) {
					RelogioAnalogico.this.executando = false;
				}
			});
			menu.add(mItem2);
			
			TrayIcon trayIcon = new TrayIcon(javaLogo, "Relógio Analógico", menu);
			trayIcon.setImageAutoSize(true);
			trayIcon.addMouseListener(new MouseAdapter(){
				@Override
				public void mousePressed(MouseEvent e) {
					RelogioAnalogico.this.toFront();
				}
			});
			
			SystemTray.getSystemTray().add(trayIcon);
		}
	}
	
	private void criaImagens() {
		/* FUNDO */
		fundo = G_CONF.createCompatibleImage(tamanho, tamanho, Transparency.OPAQUE);
		Graphics2D g2 = fundo.createGraphics();
		g2.setRenderingHints(ConfiguracoesGraficas.getInstance());
		Stroke strokeOriginal = g2.getStroke();
		Composite compositeOriginal = g2.getComposite();
		
		g2.drawImage(imagemFundo, 0, 0, null);
		
		double raioDiagonal = Math.sqrt(2 * Math.pow(tamanho / 2, 2));
		double raioInternoMaior = 0.9 * (tamanho / 2);
		double menorRaio = incrementoRaio * 3;
		
		g2.setColor(corTracoMenor);
		g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.15F));
		for(int angulo = 0; angulo < 360; angulo += (360 / 60)) {
			double posXInterna = posXCentro + menorRaio * Math.sin(Math.toRadians(angulo));
			double posYInterna = posYCentro + menorRaio * Math.cos(Math.toRadians(angulo));
			double posXExterna = posXCentro + raioInternoMaior * Math.sin(Math.toRadians(angulo));
			double posYExterna = posYCentro + raioInternoMaior * Math.cos(Math.toRadians(angulo));
			g2.drawLine((int)posXInterna, (int)posYInterna, (int)posXExterna, (int)posYExterna);
		}
		
		for(int angulo = 0; angulo < 360; angulo += (360 / 120)) {
			double posXInterna = posXCentro + raioInternoMaior * Math.sin(Math.toRadians(angulo));
			double posYInterna = posYCentro + raioInternoMaior * Math.cos(Math.toRadians(angulo));
			double posXExterna = posXCentro + raioDiagonal * Math.sin(Math.toRadians(angulo));
			double posYExterna = posYCentro + raioDiagonal * Math.cos(Math.toRadians(angulo));
			g2.drawLine((int)posXInterna, (int)posYInterna, (int)posXExterna, (int)posYExterna);
		}

		double raioCirculo;
		for(raioCirculo = raioDiagonal; raioCirculo >= raioInternoMaior; raioCirculo -= incrementoRaio) {
			g2.drawOval((int)(posXCentro - raioCirculo), (int)(posYCentro - raioCirculo), (int)(2 * raioCirculo), (int)(2 * raioCirculo));
		}
		
		List<Double> listaRaiosInternos = new ArrayList<Double>();
		for(; raioCirculo >= menorRaio; raioCirculo -= incrementoRaio) {
			listaRaiosInternos.add(raioCirculo);
			g2.drawOval((int)(posXCentro - raioCirculo), (int)(posYCentro - raioCirculo), (int)(2 * raioCirculo), (int)(2 * raioCirculo));
		}
		Double[] raiosInternos = listaRaiosInternos.toArray(new Double[]{});

		g2.setComposite(compositeOriginal);
		g2.setStroke(new BasicStroke(espessuraLinha));
		
		BufferedImage barraMenor = criaBarra(corTracoMenor, (int)raiosInternos[0].doubleValue(), (int)raiosInternos[1].doubleValue());
		BufferedImage barraMaior = criaBarra(corTracoMaior, (int)raiosInternos[0].doubleValue(), (int)raiosInternos[2].doubleValue());
		
		int posXBarraMenor = (tamanho - barraMenor.getWidth()) / 2;
		int posYBarraMenor = (tamanho / 2) - barraMaior.getHeight();
		int posXBarraMaior = (tamanho - barraMaior.getWidth()) / 2;
		int posYBarraMaior = (tamanho / 2) - barraMaior.getHeight();
		for(double angulo = 270, controle = 0; controle < 360; angulo += (360/60), controle += (360 / 60)) {
			AffineTransform transformOriginal = g2.getTransform();
			AffineTransform rotacao = new AffineTransform();
			rotacao.rotate(Math.toRadians(angulo), posXCentro, posYCentro);
			g2.setTransform(rotacao);
			if(angulo % 30 == 0) { // barra maior
				g2.drawImage(barraMaior, posXBarraMaior, posYBarraMaior, null);
			} else { // barra menor
				g2.drawImage(barraMenor, posXBarraMenor, posYBarraMenor, null);
			}
			g2.setTransform(transformOriginal);
		}
		g2.setStroke(strokeOriginal);
		g2.dispose();
		
		/* PONTEIROS */
		ponteiroSegundos = criaPonteiro(corPonteiroSegundos, (int)raiosInternos[5].doubleValue(), (int)(2.5 * incrementoRaio));
		ponteiroMinutos = criaPonteiro(corPonteiroMinutos, (int)raiosInternos[6].doubleValue(), (int)(2.5 * incrementoRaio));
		ponteiroHoras = criaPonteiro(corPonteiroHoras, (int)raiosInternos[7].doubleValue(), (int)(2.5 * incrementoRaio));
	}
	
	private BufferedImage criaPonteiro(
			Color cor,
			int alturaBarra,
			int alturaSeta) {
		BufferedImage image = G_CONF.createCompatibleImage(30, (alturaBarra + alturaSeta), Transparency.TRANSLUCENT);
		Graphics2D g2 = image.createGraphics();
		g2.setRenderingHints(ConfiguracoesGraficas.getInstance());
		Stroke strokeOriginal = g2.getStroke();
		
		g2.setStroke(new BasicStroke(espessuraLinha));
		g2.setColor(cor);

		// Para barra grossa
		int posXCentro = (image.getWidth() - espessuraLinha) / 2;
		
		alturaBarra += (alturaSeta / 2);
		g2.drawLine(posXCentro, image.getHeight(), posXCentro, (image.getHeight() - alturaBarra));
		
		g2.setStroke(strokeOriginal);
		
		// Para seta
		double posXTopo = (image.getWidth() / 2);
		double posYTopo = 0;
		
		double angulo = 270 + 180; // Ponta-cabeça
		
		double posXBase1 = posXTopo + alturaSeta * Math.cos(Math.toRadians(angulo + 15));
		double posYBase1 = posYTopo + alturaSeta * Math.sin(Math.toRadians(angulo + 15));
		
		double posXBase2 = posXTopo + alturaSeta * Math.cos(Math.toRadians(angulo - 15));
		double posYBase2 = posYTopo + alturaSeta * Math.sin(Math.toRadians(angulo - 15));
		
		g2.fillPolygon(
				new int[]{(int)posXTopo, (int)posXBase1, (int)posXBase2},
				new int[]{(int)posYTopo, (int)posYBase1, (int)posYBase2},
				3);
		
		g2.dispose();
		return image;
	}
	
	private BufferedImage criaBarra(Color cor, int alturaTopo, int alturaBase) {
		BufferedImage image = G_CONF.createCompatibleImage(30, alturaTopo, Transparency.TRANSLUCENT);
		Graphics2D g2 = image.createGraphics();
		g2.setRenderingHints(ConfiguracoesGraficas.getInstance());
		Stroke strokeOriginal = g2.getStroke();
		
		g2.setStroke(new BasicStroke(espessuraLinha));
		g2.setColor(cor);
		
		int posX = (image.getWidth() - espessuraLinha) / 2;
		int posYTopo = image.getHeight() - alturaTopo;
		int posYBase = image.getHeight() - alturaBase;
		
		g2.drawLine(posX, posYBase, posX, posYTopo);
		
		g2.setStroke(strokeOriginal);
		g2.dispose();
		
		return image;
	}

	private void inicia() throws Exception {
		new Thread(this).start();
	}
	
	@Override
	public void run() {
		setVisible(true);
		executando = true;
		while(executando) {
			double tempo = System.nanoTime();
			repaint();
			atualiza();
			desenha();
			trocaBuffer();
			dorme(tempo);
		}
		this.dispose();
		System.exit(0);
	}
	
	@Override
	public void paint(Graphics g) {
		g.drawImage(bufferDaFrente, 0, 0, null);
		g.dispose();
	}

	private void atualiza() {
		horario.setTime(new Date());
	}
	
	private void desenha() {
		Graphics2D g2 = bufferDeTras.createGraphics();
		g2.setRenderingHints(ConfiguracoesGraficas.getInstance());
		
		g2.drawImage(fundo, 0, 0, null);
		
		int posXCentro, posYCentro;
		posXCentro = posYCentro = tamanho / 2;
		
		double hora = horario.get(Calendar.HOUR);
		double minuto = horario.get(Calendar.MINUTE);
		double segundo = horario.get(Calendar.SECOND);
		double milisegundo = horario.get(Calendar.MILLISECOND);
		
		double anguloSegundo = 360 * ((segundo + milisegundo / 1000) / 60);
		double anguloMinuto = 360 * ((minuto + segundo / 60 + milisegundo / 60000) / 60);
		double anguloHora = 360 * ((hora + minuto / 60 + segundo / 3600 + milisegundo / 3600000) / 12);
		
		AffineTransform transformOriginal = g2.getTransform();
		
		AffineTransform rotacaoSegundos = new AffineTransform();
		rotacaoSegundos.rotate(Math.toRadians(anguloSegundo), posXCentro, posYCentro);
		g2.setTransform(rotacaoSegundos);
		g2.drawImage(ponteiroSegundos, (tamanho - ponteiroSegundos.getWidth()) / 2, (tamanho / 2) - ponteiroSegundos.getHeight(), null);
		g2.setTransform(transformOriginal);
		
		AffineTransform rotacaoMinutos = new AffineTransform();
		rotacaoMinutos.rotate(Math.toRadians(anguloMinuto), posXCentro, posYCentro);
		g2.setTransform(rotacaoMinutos);
		g2.drawImage(ponteiroMinutos, (tamanho - ponteiroMinutos.getWidth()) / 2, (tamanho / 2) - ponteiroMinutos.getHeight(), null);
		g2.setTransform(transformOriginal);
		
		AffineTransform rotacaoHoras = new AffineTransform();
		rotacaoHoras.rotate(Math.toRadians(anguloHora), posXCentro, posYCentro);
		g2.setTransform(rotacaoHoras);
		g2.drawImage(ponteiroHoras, (tamanho - ponteiroHoras.getWidth()) / 2, (tamanho / 2) - ponteiroHoras.getHeight(), null);
		g2.setTransform(transformOriginal);
		
		g2.drawImage(moldura, 0, 0, null);
		g2.drawImage(pontoCentral, (tamanho - pontoCentral.getWidth()) / 2, (tamanho - pontoCentral.getHeight()) / 2, null);
		
		g2.dispose();
	}
	
	private void trocaBuffer() {
		BufferedImage temp = bufferDaFrente;
		bufferDaFrente = bufferDeTras;
		bufferDeTras = temp;
	}
	
	private void dorme(double tempo) {
		double fps = 0.75 * D_MODE.getRefreshRate();
		double periodo = 1000 / fps;
		long tempoDormencia = (long)(periodo - (System.nanoTime() - tempo) / 1000000.0); // divisão por 1000000 para converter de ns para ms
		if(tempoDormencia > 0) {
			try {
				Thread.sleep(tempoDormencia);
			} catch (InterruptedException e) {}
		} else {
			Thread.yield();
		}
	}

	private BufferedImage acelera(BufferedImage imagem) {
		BufferedImage acelerada = G_CONF.createCompatibleImage(imagem.getWidth(), imagem.getHeight(), Transparency.TRANSLUCENT);
		Graphics2D g2 = acelerada.createGraphics();
		g2.setRenderingHints(ConfiguracoesGraficas.getInstance());
		g2.drawImage(imagem, 0, 0, null);
		g2.dispose();
		return acelerada;
	}
	
	private static class ConfiguracoesGraficas extends RenderingHints {
		
		private static final ConfiguracoesGraficas instance;
		
		static {
			instance = new ConfiguracoesGraficas();
		}
		
		private ConfiguracoesGraficas() {
			super(null);
			put(KEY_ALPHA_INTERPOLATION,	VALUE_ALPHA_INTERPOLATION_QUALITY);
			put(KEY_ANTIALIASING,			VALUE_ANTIALIAS_ON);
			put(KEY_COLOR_RENDERING,		VALUE_COLOR_RENDER_QUALITY);
			put(KEY_DITHERING,				VALUE_DITHER_DISABLE);
			put(KEY_FRACTIONALMETRICS,		VALUE_FRACTIONALMETRICS_ON);
			put(KEY_INTERPOLATION,			VALUE_INTERPOLATION_BICUBIC);
			put(KEY_RENDERING,				VALUE_RENDER_QUALITY);
			put(KEY_STROKE_CONTROL,			VALUE_STROKE_NORMALIZE);
			put(KEY_TEXT_ANTIALIASING,		VALUE_TEXT_ANTIALIAS_ON);
		}

		public static ConfiguracoesGraficas getInstance() {
			return instance;
		}
		
	}

	public static RelogioAnalogico getInstance() {
		return instance;
	}
	
}

10 Respostas

Polverini

show de bola, ta de parabens =)

alexvingg

Muito bom !!

R

Muito bom cara.
Só um porém [pra ficar 101%]: No relógio do show, que eu lembro não tinha as setas.

Fox_McCloud

RonanVargas:
Muito bom cara.
Só um porém [pra ficar 101%]: No relógio do show, que eu lembro não tinha as setas.

Hehehehehe… Eu observei o relógio do show, as setas eram losangos (estilo rosa-dos-ventos).

MAS é apenas inspirado naquele :wink:

Alexandre_Saudate

Fox McCloud:
RonanVargas:
Muito bom cara.
Só um porém [pra ficar 101%]: No relógio do show, que eu lembro não tinha as setas.

Hehehehehe… Eu observei o relógio do show, as setas eram losangos (estilo rosa-dos-ventos).

MAS é apenas inspirado naquele :wink:

[chato mode=on]
E o relógio adiantava. E ele se autodestruía no final.
[/chato]

[]'s

Fox_McCloud

asaudate:
Fox McCloud:
RonanVargas:
Muito bom cara.
Só um porém [pra ficar 101%]: No relógio do show, que eu lembro não tinha as setas.

Hehehehehe… Eu observei o relógio do show, as setas eram losangos (estilo rosa-dos-ventos).

MAS é apenas inspirado naquele :wink:

[chato mode=on]
E o relógio adiantava. E ele se autodestruía no final.
[/chato]

[]'s


Hauhauhauhauahuah…

Na quarta ele tava uma hora atrasado!

No mínimo deve ser o note de alguém da equipe em outro fuso-horário xD

Luiz_Aguiar

Coloca os fontes no Github amigo.

[]s

ViniGodoy

Dica, se tiver uma classe de vetores encapsulada, poderá eliminar boa parte dessa matematiqueira aí no meio do seu código:
http://bit.ly/usovetores

Marky.Vasconcelos

Que legal!

++

Fox_McCloud

Marky.Vasconcelos:
Que legal!

++


Opa, gostei! Vou ver isso depois do trabalho, rs…

Criado 19 de abril de 2011
Ultima resposta 19 de abr. de 2011
Respostas 10
Participantes 8