Cairo 图形指南 (6) —— 透明
这一篇讲述有关透明的一些基本知识,并提供两个有趣的透明效果。
透明是透过某种材质的可见度。理解透明最简单的方式就是想像一下玻璃或者水。从技术上讲,光线可以穿过玻璃,因此我们可以看到玻璃之后的物体。
在计算机图形学中,可以使用alpha 混合方式来实现透明效果。Alpha 混合,是通过将图像与背景组合实现部分透明的视觉效果。混合过程中使用了一种叫做 alpha 通道的东西。Alpha 通道在图形文件格式中是一个 8 位的层,用于表示图片的透明性。每个像素所包含的这个各额外的 8 位数字提供了一个蒙板,可以表达 256 个层次的透明度。
透明的矩形
第一个例子,绘制了 10 个不同透明程度的矩形。
#include <gtk/gtk.h>
static gboolean
on_expose_event (GtkWidget * widget,
GdkEventExpose * event, gpointer data)
{
cairo_t *cr;
cr = gdk_cairo_create (widget->window);
gint i;
for (i = 1; i <= 10; i++) {
cairo_set_source_rgba (cr, 0, 0, 1, i * 0.1);
cairo_rectangle (cr, 50 * i, 20, 40, 40);
cairo_fill (cr);
}
cairo_destroy (cr);
return FALSE;
}
int
main (int argc, char *argv[])
{
GtkWidget *window;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
g_signal_connect (G_OBJECT (window), "expose-event",
G_CALLBACK (on_expose_event), NULL);
g_signal_connect (G_OBJECT (window), "destroy",
G_CALLBACK (gtk_main_quit), NULL);
gtk_window_set_position (GTK_WINDOW (window),
GTK_WIN_POS_CENTER);
gtk_window_set_default_size (GTK_WINDOW (window), 590, 90);
gtk_window_set_title (GTK_WINDOW (window), "transparency");
gtk_widget_set_app_paintable (window, TRUE);
gtk_widget_show_all (window);
gtk_main ();
return 0;
}
cairo_set_source_rgba () 函数有一个可选的 alpha 参数,用于提供透明色支持。
for (i = 1; i <= 10; i++) {
cairo_set_source_rgba (cr, 0, 0, 1, i * 0.1);
cairo_rectangle (cr, 50 * i, 20, 40, 40);
cairo_fill (cr);
}
这段代码创建 10 个矩形,其 alpha 值从 0.1 递增到 1。
淡出的效果
在下一个示例中,实现对一幅图片的淡出处理。这幅图片会逐渐变得越来越透明直至看不见。
#include <gtk/gtk.h>
cairo_surface_t *image;
static gboolean
on_expose_event (GtkWidget * widget,
GdkEventExpose * event, gpointer data)
{
cairo_t *cr;
cr = gdk_cairo_create (widget->window);
static double alpha = 1;
double const delta = 0.01;
cairo_set_source_surface (cr, image, 10, 10);
cairo_paint_with_alpha (cr, alpha);
alpha -= delta;
if (alpha <= 0)
timer = FALSE;
cairo_destroy (cr);
return FALSE;
}
static gboolean
time_handler (GtkWidget * widget)
{
if (widget->window == NULL)
return FALSE;
gtk_widget_queue_draw (widget);
return TRUE;
}
int
main (int argc, char *argv[])
{
GtkWidget *window;
GtkWidget *darea;
gint width, height;
image = cairo_image_surface_create_from_png ("tuz.png");
width = cairo_image_surface_get_width (image);
height = cairo_image_surface_get_height (image);
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
darea = gtk_drawing_area_new ();
gtk_container_add (GTK_CONTAINER (window), darea);
g_signal_connect (darea, "expose-event",
G_CALLBACK (on_expose_event), NULL);
g_signal_connect (window, "destroy",
G_CALLBACK (gtk_main_quit), NULL);
gtk_window_set_position (GTK_WINDOW (window),
GTK_WIN_POS_CENTER);
gtk_window_set_default_size (GTK_WINDOW (window), width + 20,
height + 20);
gtk_window_set_title (GTK_WINDOW (window), "fade out");
g_timeout_add (50, (GSourceFunc) time_handler,
(gpointer) window);
gtk_widget_show_all (window);
gtk_main ();
cairo_surface_destroy (image);
return 0;
}
在这一示例中,显示了一幅渐渐淡出的图片。所使用的图片是 Linux kernel 的新 Logo,详见 LinuxToy 网站的介绍。
出于对程序运行效率的考虑,图像外观的创建在主函数中进行。
将载入的图片设置为源 (source)。
调用函数 cairo_paint_with_alpha () 来实现图片淡出效果,该函数使用透明度作为蒙板。
在每次响应 expose 事件的处理过程中,进行透明度的值递减。
alpha = 1;
当透明度为 0 时,意味着图片是完全透明的。这时,再将透明度置为 1,即将图片复原为不透明状态。这样做的目的是让图片周而复始、痛不欲生的淡出着。
创建一个计时器,其回调函数为 time_handler (),每隔 50ms 便会被调用一次。
time_handler (GtkWidget * widget)
{
if (widget->window == NULL)
return FALSE;
gtk_widget_queue_draw (widget);
return TRUE;
}
time_handler () 函数的实现,它的主要作用是调用 gtk_widget_queue_draw () 函数,实现窗口的重绘 (发出 expose 事件)。当该函数返回值为 FALSE 时,计时器便停止工作。
return FALSE;
这两行代码看上去有点多余。但是考虑在计时器的时间间隔很小的情况下 (譬如 5ms),如果关闭窗口,这时进程尚未完全销毁,不过窗口已经被 destroy 了。这时,这两行代码可以防止计时器回调函数继续工作而引起的程序崩溃。
“等待”的演示
下面的示例,使用透明效果制作了一个“等待”状态的演示,是通过绘制 8 条依次消隐淡出的线,模拟一条线的运动过程。像这样的效果通常被用于通知用户耐心地等待一个隐藏在屏幕之后漫长的任务的完成。在网上看视频时,经常可以看到类似的“等待”。
#include <gtk/gtk.h>
#include <math.h>
static gushort count = 0;
static gboolean
on_expose_event (GtkWidget * widget,
GdkEventExpose * event, gpointer data)
{
cairo_t *cr;
cr = gdk_cairo_create (widget->window);
static gdouble const trs[8][8] = {
{0.0, 0.15, 0.30, 0.5, 0.65, 0.80, 0.9, 1.0},
{1.0, 0.0, 0.15, 0.30, 0.5, 0.65, 0.8, 0.9},
{0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65, 0.8},
{0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65},
{0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5},
{0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3},
{0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15},
{0.15, 0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0,}
};
gint width, height;
gtk_window_get_size (GTK_WINDOW (widget), &width, &height);
cairo_translate (cr, width / 2, height / 2);
gint i = 0;
for (i = 0; i < 8; i++) {
cairo_set_line_width (cr, 3);
cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
cairo_set_source_rgba (cr, 0, 0, 0, trs[count % 8][i]);
cairo_move_to (cr, 0.0, -10.0);
cairo_line_to (cr, 0.0, -40.0);
cairo_rotate (cr, M_PI / 4);
cairo_stroke (cr);
}
cairo_destroy (cr);
return FALSE;
}
static gboolean
time_handler (GtkWidget * widget)
{
count += 1;
gtk_widget_queue_draw (widget);
return TRUE;
}
int
main (int argc, char *argv[])
{
GtkWidget *window;
GtkWidget *darea;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
g_signal_connect (G_OBJECT (window), "expose-event",
G_CALLBACK (on_expose_event), NULL);
g_signal_connect (G_OBJECT (window), "destroy",
G_CALLBACK (gtk_main_quit), NULL);
gtk_window_set_position (GTK_WINDOW (window),
GTK_WIN_POS_CENTER);
gtk_window_set_default_size (GTK_WINDOW (window), 250, 150);
gtk_window_set_title (GTK_WINDOW (window), "waiting demo");
gtk_widget_set_app_paintable (window, TRUE);
gtk_widget_show_all (window);
g_timeout_add (100, (GSourceFunc) time_handler,
(gpointer) window);
gtk_main ();
return 0;
}
方法比较笨,就是绘制 8 条不同透明度的线。
{0.0, 0.15, 0.30, 0.5, 0.65, 0.80, 0.9, 1.0},
{1.0, 0.0, 0.15, 0.30, 0.5, 0.65, 0.8, 0.9},
{0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65, 0.8},
{0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65},
{0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5},
{0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3},
{0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15},
{0.15, 0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0,}
};
这 8 条线的透明度就是这样被活生生地定义出来的,每一行表示一条线的透明度候选集。
cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
设置线宽与线帽。
每条线的透明度就是这样活生生地变化出来的。
cairo_line_to (cr, 0.0, -40.0);
cairo_rotate (cr, M_PI / 4);
这里有个从未讲过的技巧,就是 cairo 的旋转变换。每次都在前一次旋转的基础上继续逆时针旋转 角度。这样,8 次循环之后,恰好绕了一圈。
又是计时器。