Как стать автором
Обновить

Dropclock для xscreensaver или как верстальщик писал заставку под Linux

Время на прочтение8 мин
Количество просмотров27K
Не помню, где первый раз увидел, но был очарован заставкой DropClock, о которой уже упоминалось на Хабре.

=>

Но вот беда: авторы собрали её только для Win и Mac. Несмотря на это, желание было сильнее ограничений, и я решил во что бы то ни стало собрать собственную реализацию.

Первая мысль: попытаться реализовать программный рендер воды и брызги.
Так как моё основное направление — веб, я начал искать реализации webGL и эффектов воды. В результате изысканий пришёл к WebGL Water.

Почитал немного документации, попробовал helloworld'ы, но это не моё.

Беглое чтение исходников и попытки правок не привнесли ясности в предмет исследований. Было принято решение посмотреть, а из чего же состоит оригинал.

Скачал с торрентов Win версию, на вайне не пошла, поставил на виртуалку.

На деле оказалось, что это флеш со вшитыми роликами: 10 с цифрами на чёрном фоне и 10 на белом, каждый длится минуту.
Распаковщики swf их не потянули, но мне помог конвертер SWF to Video (win). Дальше уже в родной системе перекодировал в mp4/x264 ffmpeg-ом.

Далее дело оставалось за малым — заставить это работать в браузере. Для разнообразия ещё добавил подгрузку погоды с OpenWeatherMap.

Получилась вот такая разметка:

	<div class="all">
		<div class="d d0"><video src="dropclock_media/b0.mp4" autoplay="autoplay" loop="loop" id="d0" data-d="0"></video></div>
		<div class="d d1"><video src="dropclock_media/b0.mp4" autoplay="autoplay" loop="loop" id="d1" data-d="1"></video></div>
		<div class="d s"><div class="sep"></div></div>
		<div class="d d2"><video src="dropclock_media/b0.mp4" autoplay="autoplay" loop="loop" id="d2" data-d="2"></video></div>
		<div class="d d3"><video src="dropclock_media/b0.mp4" autoplay="autoplay" loop="loop" id="d3" data-d="3"></video></div>
		<div class="inspector">
			<div class="time" id="info"></div>
			<div class="weather"></div>
		</div>
	</div>

Стили (итоговые)
		html,body{height:100%;width:100%;margin:0;padding:0;overflow:hidden;}
		body{text-align:center;background-color:#000;color:#fff;font-family:Arial;}
		body.save{cursor:none;}
		.all{
			width: 100%;
			max-width: 1375px;
			height: calc(100% - 100px);
			display:inline-block;white-space: nowrap;text-align:center;
			position: relative;
			padding: 0; margin:0 auto;
			}
	.d{
		display:inline-block;line-height:0;/*width:312px;*/max-width:calc((100% - 115px) / 4 - 2px);height:100%;
		vertical-align:top;overflow:hidden;border:1px solid #000;/*background:#fff;*/
		 }
	.d:after{display:block;width:100%;height:2px;content:"";background:#000;margin-top:-2px;position:relative;z-index:100;}
		.s{width:115px;overflow:visible;}
		.d .sep{height:100%;animation: blick 1s ease infinite;}
		.r .sep{-webkit-animation:rotate  60s ease infinite;}
		.sep{position:relative;overflow:visible;}
		.sep:after,.sep:before{
			position:absolute;top:40%;left:39%;z-index:1000;content:"";
			display:block;width:22%;height:20%;
			-webkit-animation:blick 1s ease infinite;
			/*background-color: red;*/
			background-image: url("data:image/gif;base64,R0lGODlhAQABAIABAMzMzP///yH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAAQABAAACAkQBADs=");
			background-repeat:no-repeat;background-size:contain;
			}
		.sep:after{margin-top:94%;}
		.sep:before{}
		video{height:100%;max-height:700px;width:calc(100% + 2px);max-width:315px;margin:-2px -1px 0;position:relative;z-index:50;}
		#info{/*display:none;*/text-align:right;}
		@-webkit-keyframes blick{
			0% {opacity:1;}
			50% {opacity:.7;}
			70% {opacity:.3;}
			100% {opacity:1;}
			}
		@-webkit-keyframes rotate{
			0 %{-webkit-transform: rotate(0deg);}
			0.2%{ -webkit-transform: rotate(-30deg); }
			0.8%{ -webkit-transform: rotate(390deg); }
			1%{ -webkit-transform: rotate(360deg); }
			100% {-webkit-transform: rotate(360deg);}
		}
		
		@keyframes blick{0% {opacity:1;}50% {opacity:.7;}70% {opacity:.3;}100% {opacity:1;}}
	
	.inspector{line-height:50px;padding:0 25px;}
	.time{float:right;}
	.weather{float:left;}
	.weather img{vertical-align:top;}
	.arrow{display:inline-block;width:20px;height:50px;}

@media screen and (min-height:775px){
	.all{
		top: calc((100% - 755px) / 2);
		height: calc(100% - 202px);
	}
}
@media screen and (max-height:775px){
	.all{
		top:25px;
	} 
}

Скрипты (js и немного jquery)
		var digits = [0,0,0,0];
		var msmove = timenow();
		var city = 1485357;
		var lastrequesttime = 0;
		function timenow(){ return new Date * 1; }
		function getRandomArbitary(min, max){return Math.random() * (max - min) + min;}
		function extra0(d,e){
			if(e===undefined)e=1;for(var i=0; i<e; i++){if(d<Math.pow(10,(i+1))) d='0'+d;}return d;}
		function step(){
			var	now = new Date(),hors = now.getHours(),mins = now.getMinutes(),ndig = new Array(4),secs = now.getSeconds(),mili = now.getMilliseconds();
			ndig[0]=Math.floor(hors/10);
			ndig[1]=hors-ndig[0]*10;
			ndig[2]=Math.floor(mins/10);
			ndig[3]=mins-ndig[2]*10;
			//ndig[4]
			// from last mouse move
			var flmm = timenow() - msmove;
			if(flmm > 2000)
//				if(!$('body').hasClass('save'))
				$('body').addClass('save');
			// print info
			var	info = document.getElementById('info');
			info.innerHTML = 
//				timenow()+' / '+
//				msmove+' / '+
//				flmm+' / '+
				extra0(hors)+':'+extra0(mins)+':'+extra0(secs)+
//				'.'+extra0(mili,2)+
				'';
			// replace
			for(var i in ndig){
				if(ndig[i] != digits[i]){
					if(lastrequesttime + getRandomArbitary(256,2048) <  timenow())
						updateDigit(i,ndig[i]);
					//document.getElementById('d'+i).src='b'+ndig[i]+'.mp4';
					if(i==3){
						updateWeather();
						}
					}
				}
			setTimeout(step,getRandomArbitary(256,768));
			}
		function updateDigit(i,n){
			var d = $('#d'+i).clone().attr('src','dropclock_media/b'+n+'.mp4').appendTo('.d'+i);
			//setTimeout(function(){
			//	var i = d[0].dataset.d;
				$('#d'+i+':first').remove();
			//	},15000);
			
					
			//document.getElementById('d'+i).src='b'+n+'.mp4';
			lastrequesttime = timenow();
			digits[i] = n;
			}
		function updateWeather(){
			// weather
			$.getJSON('http://api.openweathermap.org/data/2.5/weather?id='+city,function(data){
				console.dir(data);
				var	icon = 'http://openweathermap.org/img/w/'+data.weather[0].icon+'.png',
						temp = Math.floor(data.main.temp - 273.15),
						arrst= 'transform: rotate('+data.wind.deg+'deg);-webkit-transform: rotate('+data.wind.deg+'deg);',
					w_html = 
//					'Погода:'+
					data.name+' '+
					data.sys.country+' '+
					'<img src="'+icon+'">'+
					data.weather[0].main+', '+
					data.weather[0].description+' '+
					((temp>0)?'+':'')+temp+'° '+
					'; Wind '+data.wind.speed+'m/s <div class="arrow" style="'+arrst+'">↓</div>'+
					'';
				$('.weather').html(w_html);
				});
			}

		$(function(){
			// timer
			step();
			$(window).mousemove(function(){
				if(document.body.classList.contains('save'))
					document.body.classList.remove('save');
				msmove = timenow();
				});
			});		




В firefox видео притормаживало и дребезжало, поэтому изначально я был ориентирован на webkit.
Добавил задержку падения цифр, чтобы не исчезали все сразу.
Вроде работает, красиво, но нужно запускать это как самостоятельный файл.

Нашёл у себя в убунте системный мини-браузер webbrowser-app и, покапавшись в его параметрах, написал вот такой лаунчер:

#/bin/sh
webbrowser-app --chromeless --fullscreen /var/www/vhosts/localhost/dropclock/index.html

Но xscreensaver на него сильно ругался и я понял, что что-то делаю не так…

Шло время, желание иметь красивую заставку никак не утолялось. 13 сентября, во всероссийский День Программиста, работа была отодвинута, а внимание снова сосредоточено на достижении призрачной цели. До этого видел оконные приложения на Python и выбор пал на него, как впоследствии оказалось, не зря. Раньше никогда ничего на нём не писал. Начал разбирать базовые примеры, добавил webkit.
Ура! Заработало!
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pygtk
pygtk.require('2.0')
import os, argparse, gtk, webkit
from gtk import gdk

class DropClock:
	
	def createParser():
		self.parser = argparse.ArgumentParser()
		self.parser.add_argument ('-r', '--root', action='store_const', const=True, default=False)
		self.namespace = self.parser.parse_args()

	def delete_event(self, widget, event, data=None):
		print "Вызван сигнал удаления"
		return False
	
	def destroy(self, widget, data=None):
		print "Вызван сигнал уничтожения"
		gtk.main_quit()

	def __init__(self):
		self.parser = argparse.ArgumentParser()
		self.parser.add_argument ('-r', '--root', action='store_const', const=True, default=False)
		self.parser.add_argument ('-w', '-window-id', action='store_const', const=True, default='')
		self.namespace = self.parser.parse_args()

		url = 'file://' + os.path.realpath('./index.html')
		self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
		self.window.set_default_size(1600, 768)
		self.window.set_position(gtk.WIN_POS_CENTER)
		self.window.connect("delete_event", self.delete_event)
		self.window.connect("destroy", self.destroy)
		
		self.browser = webkit.WebView()
		self.browser.props.settings.props.enable_default_context_menu = False
		self.browser.load_uri(url)
		
		self.window.add(self.browser)
		self.window.show_all()
#		if self.namespace.root:
#			self.window.fullscreen();
		
		
		
	
	def main(self):
		gtk.main()
#		self.createParser();

if __name__ == "__main__":
	hello = DropClock()
	hello.main()


А вдруг можно и к xscreensaver'у его прикрутить? Был найден пример заставки на питоне, на базе которого написан свой.
Ничего сверхъестественного
#!/usr/bin/python

import os
import sys

import gtk, webkit
from gtk import gdk

# the secret sauce is to get the "window id" out of $XSCREENSAVER_WINDOW
# code comes from these two places:
# 1) http://pastebin.com/nSCiq1P3
# 2) http://stackoverflow.com/questions/4598581/python-clutter-set-display

class ScreenSaverWindow(gtk.Window):
	
	def __init__(self):
		gtk.Window.__init__(self)
		pass

	def delete_event(self, widget, event, data=None):
		return False
		
	def destroy(self, widget, data=None):
		gtk.main_quit()

	def realize(self):
		if self.flags() & gtk.REALIZED:
			return
		ident = os.environ.get('XSCREENSAVER_WINDOW')
		if not ident is None:
			print 'if not ident is None:'
			self.window = gtk.gdk.window_foreign_new(int(ident, 16))
			self.window.set_events (gdk.EXPOSURE_MASK | gdk.STRUCTURE_MASK)
			# added by aja
			x, y, w, h, depth = self.window.get_geometry()
			# self.size_allocate(gtk.gdk.Rectangle(x, y, w, h))
			self.set_default_size(w, h)
			self.move(x, y)
			self.set_decorated(False)
			# aja - more
			self.window.set_user_data(self)
			self.style.attach(self.window)
			self.set_flags(self.flags() | gtk.REALIZED)
			#self.window.connect("destroy", self.destroy)

		if self.window == None:
			print 'self.window == None:'
			self.window = gdk.Window(None, 1024, 768, gdk.WINDOW_TOPLEVEL,(gdk.EXPOSURE_MASK | gdk.STRUCTURE_MASK),gdk.INPUT_OUTPUT)
#			self.window.set_title("DropClock")

		if self.window != None:
			print 'self.window != None:'
			#self.window.add_filter(lambda *args: self.filter_event(args))
			self.set_flags(self.flags() | gtk.REALIZED)
			
		self.browser = webkit.WebView()
		url = 'file://' + os.path.join(os.path.dirname(__file__) + '/index.html')
		self.browser.load_uri(url)
		self.add(self.browser)
		self.browser.show()

window = ScreenSaverWindow()
window.set_title('DropClock')
window.connect('delete-event', gtk.main_quit)
window.set_default_size(1024, 768)
window.realize()

window.modify_bg(gtk.STATE_NORMAL, gdk.color_parse("black"))

window.show()

gtk.main()

В ~/.xscreensaver под списком добавлена строка:
- "Drop Clock" /var/www/vhosts/localhost/dropclock/dropclock_xss.py \n\
Впоследствии ещё дорабатывал адаптивность стиля для корректного отображения в маленьком окне, получилось неплохо, мне нравится.

Можно считать, что день программиста удался.

UPD: немного кропнул видео, т.к. в некоторых разрешениях и на демках появлялись белые полосы вверху и внизу ролика
UPD: тем, кому лениво собирать самому, ссылка на обменник; запустите сначала из терминала, почитайте какие ошибки возвращает
Теги:
Хабы:
Всего голосов 39: ↑35 и ↓4+31
Комментарии19

Публикации

Истории

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург