Created
May 22, 2022 10:57
-
-
Save trappedinspacetime/e69a87e414d4a52c70d425369d7d20f0 to your computer and use it in GitHub Desktop.
all in all pycairo example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
################################################################################ | |
##3456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 | |
## | |
## Info: | |
## python-port of cairo-based countdown effect | |
## | |
## Run: | |
## chmod +x countdown.py | |
## ./countdown.py | |
## | |
## Copyright 2014 Mirco Mueller | |
## | |
## Author: | |
## Mirco "MacSlow" Mueller <[email protected]> | |
## | |
## This program is free software: you can redistribute it and/or modify it | |
## under the terms of the GNU General Public License version 3, as published | |
## by the Free Software Foundation. | |
## | |
## This program is distributed in the hope that it will be useful, but | |
## WITHOUT ANY WARRANTY; without even the implied warranties of | |
## MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | |
## PURPOSE. See the GNU General Public License for more details. | |
## | |
## You should have received a copy of the GNU General Public License along | |
## with this program. If not, see <http://www.gnu.org/licenses/>. | |
## | |
################################################################################ | |
from ctypes import * | |
import time | |
import math | |
import cairo | |
from gi.repository import GLib, Gdk, Gtk, Pango, PangoCairo | |
HEIGHT_FRACTION = 2.5 | |
COUNTDOWN_STEPS = 5 | |
FPS = 24 | |
LIBPIXMAN_NAME = "libpixman-1.so" | |
PIXMAN_FILTER_CONVOLUTION = 5 | |
PIXMAN_OP_SRC = 1 | |
PIXMAN_a8r8g8b8 = 0x20028888 | |
class Countdown: | |
def setupPixman (self, name): | |
libpixman = cdll.LoadLibrary (name) | |
self.pixman_image_create_bits = libpixman.pixman_image_create_bits | |
self.pixman_image_set_filter = libpixman.pixman_image_set_filter | |
self.pixman_image_composite = libpixman.pixman_image_composite | |
self.pixman_image_unref = libpixman.pixman_image_unref | |
def drawNumber (self, cr, width, height, number): | |
# create pango desc/layout | |
if (self.layout == None): | |
self.layout = PangoCairo.create_layout (cr) | |
self.desc = Pango.font_description_from_string ("Ubuntu Mono") | |
self.desc.set_absolute_size (0.75 * height * Pango.SCALE) | |
self.desc.set_weight (Pango.Weight.NORMAL) | |
self.desc.set_style (Pango.Style.NORMAL) | |
self.layout.set_font_description (self.desc) | |
# print and layout string (pango-wise) | |
self.layout.set_text (str (number), -1) | |
# determine center position for number | |
rects = self.layout.get_extents () | |
x = width / 2 - rects[0].x / Pango.SCALE - rects[0].width / Pango.SCALE / 2; | |
y = height / 2 - rects[0].y / Pango.SCALE - rects[0].height / Pango.SCALE / 2; | |
# draw text | |
cr.move_to (x, y) | |
PangoCairo.layout_path (cr, self.layout) | |
cr.set_operator (cairo.OPERATOR_SOURCE) | |
cr.set_source_rgb (0.0, 0.0, 0.0) | |
cr.fill () | |
def createGaussianBlurKerne1D (self, radius, sigma): | |
scale2 = 2.0 * sigma * sigma | |
scale1 = 1.0 / (math.pi * scale2) | |
size = 2 * radius + 1; | |
n_params = size | |
tmp = (c_double * n_params)() | |
tmpSum = 0.0 | |
i = 0 | |
# caluclate gaussian kernel in floating point format | |
for x in range (-radius, radius + 1): | |
u = x * x | |
tmp[i] = scale1 * math.exp (-u / scale2) | |
tmpSum += tmp[i] | |
i += 1 | |
# normalize gaussian kernel and convert to fixed point format | |
params = (c_int32 * (n_params + 2))() | |
params[0] = size << 16 | |
params[1] = 1 << 16 | |
for i in range (n_params): | |
params[2 + i] = int ((tmp[i] / tmpSum) * 65536.0) | |
n_params += 2 | |
return params, n_params | |
def blurSurface (self, surface, data, radius, sigma): | |
width = surface.get_width () | |
height = surface.get_height () | |
stride = surface.get_stride () | |
format = surface.get_format () | |
# create pixman image for cairo image surface | |
src = self.pixman_image_create_bits (PIXMAN_a8r8g8b8, width, height, data, stride) | |
# attach gaussian kernel to pixman image | |
params, n_params = self.createGaussianBlurKerne1D (radius, sigma) | |
self.pixman_image_set_filter (src, PIXMAN_FILTER_CONVOLUTION, params, n_params) | |
# render blured image to new pixman image | |
pass1Data = (c_uint32 * stride * height)() | |
pass1 = self.pixman_image_create_bits (PIXMAN_a8r8g8b8, width, height, pass1Data, stride) | |
self.pixman_image_composite (PIXMAN_OP_SRC, src, None, pass1, 0, 0, 0, 0, 0, 0, width, height) | |
self.pixman_image_unref (src) | |
# flip the 1D kernel | |
tmp = params[0] | |
params[0] = params[1] | |
params[1] = tmp | |
self.pixman_image_set_filter (pass1, PIXMAN_FILTER_CONVOLUTION, params, n_params) | |
pass2Data = (c_ubyte * stride * height)() | |
pass2 = self.pixman_image_create_bits (PIXMAN_a8r8g8b8, width, height, pass2Data, stride) | |
self.pixman_image_composite (PIXMAN_OP_SRC, pass1, None, pass2, 0, 0, 0, 0, 0, 0, width, height) | |
self.pixman_image_unref (pass1) | |
# create new cairo image for blured pixman image | |
surface = cairo.ImageSurface.create_for_data (pass2Data, format, width, height, stride) | |
self.pixman_image_unref (pass2) | |
return surface | |
def createDropShadow (self, width, height, blurRadius): | |
stride = 4 * width | |
format = cairo.FORMAT_ARGB32 | |
data = (c_uint32 * stride * height)() | |
surface = cairo.ImageSurface.create_for_data (data, format, width, height) | |
cr = cairo.Context (surface) | |
# clear context | |
cr.scale (width, height) | |
cr.set_operator (cairo.OPERATOR_CLEAR) | |
cr.paint () | |
# drop-shadow | |
cr.set_operator (cairo.OPERATOR_SOURCE) | |
cr.set_source_rgba (0.35, 0.35, 0.35, 0.8) | |
cr.move_to (0.5, 0.5) | |
cr.arc (0.5, 0.5, 0.4125, 0.0, math.pi / 180.0 * 360.0) | |
cr.close_path () | |
cr.fill () | |
# blur surface | |
blurredSurface = self.blurSurface (surface, data, 10, 3.0) | |
return blurredSurface | |
def render (self, cr, width, height, angle, number): | |
# clear context | |
cr.set_operator (cairo.OPERATOR_CLEAR) | |
cr.paint () | |
cr.set_operator (cairo.OPERATOR_SOURCE) | |
# "drop-shadow" with blurrrrrrr!!! | |
if (self.dropShadow): | |
cr.set_source_surface (self.dropShadow, 0.0, 0.0) | |
cr.paint () | |
cr.save () | |
cr.set_operator (cairo.OPERATOR_SOURCE) | |
cr.scale (width, height) | |
# draw "tinted" area | |
cr.set_source_rgba (0.65, 0.65, 0.65, 0.9) | |
cr.move_to (0.5, 0.5) | |
fillRadian = -math.pi / 180.0 * (angle + 90.0); | |
cr.arc (0.5, 0.5, 0.40375, math.pi / 180.0 * 270.0, fillRadian) | |
cr.close_path () | |
cr.fill () | |
# draw black "grid" | |
cr.set_line_join (cairo.LINE_JOIN_ROUND) | |
cr.set_line_cap (cairo.LINE_CAP_BUTT) | |
cr.set_line_width (0.0025) | |
cr.set_source_rgb (0.0, 0.0, 0.0) | |
cr.move_to (0.5 - 0.40375, 0.5) | |
cr.rel_line_to (2.0 * 0.40375, 0.0) | |
cr.move_to (0.5, 0.5 - 0.40375) | |
cr.rel_line_to (0.0, 2.0 * 0.40375) | |
cr.stroke () | |
# draw two thick black lines | |
strokeRadian = math.pi / 180.0 * (angle - 180.0) | |
x = math.sin (strokeRadian) * 0.40375 | |
y = math.cos (strokeRadian) * 0.40375 | |
cr.set_line_width (0.0125) | |
cr.move_to (0.5, 0.5 - 0.40375) | |
cr.line_to (0.5, 0.5) | |
cr.rel_line_to (x, y) | |
cr.stroke () | |
# draw two white circles | |
cr.set_source_rgb (1.0, 1.0, 1.0) | |
cr.set_line_width (0.0085) | |
cr.arc (0.5, 0.5, 0.4, 0.0, 2 * math.pi) | |
cr.stroke () | |
cr.arc (0.5, 0.5, 0.35, 0.0, 2 * math.pi) | |
cr.stroke () | |
cr.restore () | |
self.drawNumber (cr, width, height, number) | |
def onDraw (self, widget, cr, number): | |
secondsf = time.time () - self.starttime | |
seconds = math.trunc (secondsf) % number | |
angle = (math.trunc (secondsf) - secondsf) * 360.0 | |
self.render (cr, widget.get_allocated_width (), widget.get_allocated_height (), angle, number - seconds) | |
if (number - seconds == 1): | |
widget.set_opacity (1.0 + (math.trunc (secondsf) - secondsf)) | |
else: | |
widget.set_opacity (1.0) | |
return True | |
def onScreenChanged (self, widget, oldScreen): | |
screen = widget.get_screen () | |
visual = screen.get_rgba_visual () | |
if (visual == None): | |
visual = screen.get_system_visual () | |
widget.set_visual (visual) | |
def onDelete (self, widget, event): | |
Gtk.main_quit () | |
return False | |
def onTimeout (self, widget): | |
widget.queue_draw () | |
return True | |
def __init__(self): | |
# create gtk-window for drawing | |
self.window = Gtk.Window (Gtk.WindowType.TOPLEVEL) | |
self.window.set_app_paintable (True) | |
self.window.set_decorated (False) | |
self.window.set_title ("cairo-countdown") | |
self.window.set_keep_above (True) | |
self.window.set_focus_on_map (False) | |
self.window.set_accept_focus (False) | |
self.window.set_skip_pager_hint (True) | |
self.window.set_skip_taskbar_hint (True) | |
# make window click-through, this needs pycairo 1.10.0 for python3 | |
# to work | |
rect = cairo.RectangleInt (0, 0, 1, 1) | |
region = cairo.Region (rect) | |
if (not region.is_empty ()): | |
self.window.input_shape_combine_region (None) | |
self.window.input_shape_combine_region (region) | |
# make sure that gtk-window opens with a RGBA-visual | |
self.onScreenChanged (self.window, None) | |
self.window.realize () | |
self.window.set_type_hint (Gdk.WindowTypeHint.DOCK) | |
transparent = Gdk.RGBA (0.0, 0.0, 0.0, 0.0) | |
gdkwindow = self.window.get_window () | |
gdkwindow.set_background_rgba (transparent) | |
# center the gtk-window on the screen | |
screen = self.window.get_screen () | |
monitor = screen.get_monitor_at_window (gdkwindow) | |
geo = screen.get_monitor_geometry (monitor) | |
size = geo.height / HEIGHT_FRACTION | |
self.window.set_size_request (size, size) | |
self.window.set_position (Gtk.WindowPosition.CENTER) | |
# now it's time to show it | |
self.window.show_all () | |
# setup libpixman via ctypes | |
self.setupPixman (LIBPIXMAN_NAME) | |
# start timer and timeout | |
self.timeoutId = GLib.timeout_add (1000 / FPS, self.onTimeout, self.window) | |
self.layout = None | |
self.desc = None | |
self.starttime = time.time () | |
self.dropShadow = self.createDropShadow (int (size), int (size), 10) | |
# hook up signal-callbacks | |
self.window.connect ("screen-changed", self.onScreenChanged) | |
self.window.connect ("draw", self.onDraw, COUNTDOWN_STEPS) | |
self.window.connect ("delete-event", self.onDelete) | |
countdown = Countdown () | |
Gtk.main () | |
self.layout.unref () | |
self.desc.free () |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment