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

в 11:53, , рубрики: html5 video, javascript, linux, pygtk, python, Веб-разработка, Программирование

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

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

Но вот беда: авторы собрали её только для 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
Впоследствии ещё дорабатывал адаптивность стиля для корректного отображения в маленьком окне, получилось неплохо, мне нравится.
Можно считать, что день программиста удался.

Видео можно найти по магнет-ссылке.

Автор: tima_tey

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js