背景:
为了美观,图形编辑软件一般都有线条和图片的阴影效果,阴影表现为深灰色的模糊图形,与原图形的形状一致。而GTK并没有内置的阴影效果,因此需要自己实现。
目的:
利用GTK函数实现阴影效果。
整体思路:
阴影效果即一个位图,先画它,然后再画主图,就组成了阴影效果。首先,创建一个cairo_p_w_picpath_surface,然后用灰色在上面画图形。然后获取cairo_p_w_picpath_surface的像素数据,对其进行box-blur。最后,把位图和主图都画出来即可。
优化策略:
一, 时间优化,每个图形保存一份自己的阴影,只有在图形改变时才需要重绘阴影,其他时候(如平移、缩放)不用重新计算阴影,节省计算时间。
二, 空间优化,对每个图形进行box-blur时,可以共用一个缓存进行,这个缓存的大小是固定的。因为缓存大小固定,所以如果图形大小超过了缓存大小,则需要对图形进行分割,然后对分割后的每一块进行box-blur,再把box-blur的结果考回阴影位图。
实现:
主要复杂点在于上述优化策略中的分割策略实现,下面用图figure1.1阐释:
Figure1.1阴影整体流程:1)shadowSurface为图形阴影的像素数据,比如用cairo画一些线条在上面。当这个阴影数据大于缓存shadowBuf时,就需要分块,shadowBuf和shadowBuf2是预先分配的固定大小的缓存,专门用来box-blur。关于如何分块后面会具体描述。2)将一个块拷贝到shadowBuf中。3)对这个块box-blur。4)把对这个块box-blur后的结果拷贝到tmpBluredSurfaceData中。5)当所有块都拷贝到tmpBluredSurfaceData中后,把最终结果考回shadowSurface中,整个过程完成。
整体流程中第1)步的分块和第4)步的拷贝是需要特别说明的,见图Figure1.2
Figure1.2阴影分块:shadowSurface为需要box-blur的数据。shadowBuf的大小为固定的,红色虚线所示。shadowSurface里面每个蓝色块为切分(cut)大小,红色虚线为挖出来(dig)的大小,之所以有cut和dig这两个概念,是因为box-blur会使每个块的边界像素失真。导致失真的原因是box-blur会对每个像素的周围像素求均值,那么处于边界的像素周围包含了空像素,即值为0的像素,求均值后就被0值“污染”了,因此边界像素会失真。为了避免失真,就需要额外dig一些像素,例如想对(A0,C2)块进行box-blur,如果不额外dig的话,C和2的边界像素就会被“0污染”,结果就会出在阴影中现浅色的分块线。正确的方法是取到(A0,D3)的区域,box-blur后,再考回(A0,C2)区域。注:shadowBuf的大小是能容纳最大的块的,如(B1,G6)。在shadowBuf中dig区域(红虚线)的最大值已经预先分配好了,那么如何确定中间的cut区域(蓝色区域)的大小呢,即如何确定左上右下四个方向红虚线边到蓝区域边的距离,这个距离即需要额外dig的像素?以左边距离为例,其他方向类似,应该是在水平方向box-blur的次数*box-blur的半径,这是因为进行n次box-blur就会使边界内n*box-blur范围内的像素被“0污染”,所以额外dig这些像素,box-blur完当做废料扔掉就好了。例如,要对(A0,C2)进行box-blur,x方向cut大小为AC,dig大小为CD,y方向cut大小为02,dig大小为23,总的额外dig出来的区域为(C0,D3)与(A2,D3)的并集,(A0,D3)作为box-blur的输入,box-blur后数据在shadowBuf中,然后拷贝到tmpBluredSurfaceData中,拷贝的区域是等同于(A0,C2)大小的区域,dig区域即被污染的区域扔掉了。
下图为阴影结果的效果图:
Figure1.3阴影效果
核心代码参考实现如list1-1所示,为图形类MyShape的画阴影函数。List1-2为工具函数。List1-3为box-blur函数。
List1-1:
void my_shape_draw_self_shadow (MyShape* self, ApplicationState*appState) {
my_debug("my_shape_draw_self_shadow ...");
MyShapePrivate*priv = MY_SHAPE_GET_PRIVATE (self);
cairo_t*copy = appState->cr;
cairo_t*c;
cairo_surface_t*sur;
cairo_t*cr_window;
int cut_width_byte;
int cut_height_byte;
int dig_extra_left_byte;
int dig_extra_top_byte;
int dig_extra_right_byte;
int dig_extra_bottom_byte;
int width;
int height;
int stride;
int blockCountX;
int blockCountY;
int blockIndexX;
int blockIndexY;
int blockX;
int blockY;
int block_width_byte;
int block_height_byte;
int block_inner_left_byte;
int block_inner_top_byte;
int block_inner_right_byte;
int block_inner_bottom_byte;
if(self->isShowing && self->isShadowing) {
MY_SHAPE_GET_CLASS(self)->update_shadow_rect (self);
if(self->isShadowDirty) { // dirty,需要重新blur shadow
//let sub class draw shadow
if(self->shadowSurface) {
cairo_surface_destroy(self->shadowSurface);
}
width= self->shadowWidth;
height= self->shadowHeight;
sur= cairo_p_w_picpath_surface_create (CAIRO_FORMAT_ARGB32,
width,
height);
self->shadowSurface= sur;
stride= cairo_p_w_picpath_surface_get_stride (sur);
self->shadowStride= stride;
c= cairo_create (sur);
appState->cr= c;
cairo_set_source_rgb(c, 0.5, 0.5, 0.5);
cairo_set_dash(c, self->dashes, self->dashCount, self->dashOffset);
cairo_set_line_width(c, self->strokeWidth * appState->scale);
MY_SHAPE_GET_CLASS(self)->draw_self_shadow (self, appState);
cairo_destroy(c);
appState->cr= copy;
//let us blur
cairo_format_tformat = cairo_p_w_picpath_surface_get_format (sur);
assert(format == CAIRO_FORMAT_ARGB32);
unsignedchar *surfaceData = cairo_p_w_picpath_surface_get_data (sur);
if(stride * height <= appState->shadowBufSize) { // buf够大,不用分块blur
memset(appState->shadowBuf, 0, appState->shadowBufSize);
my_box_blur_horizontal (surfaceData, appState->shadowBuf, width,height, stride, self->boxRadius);
my_box_blur_horizontal (appState->shadowBuf, surfaceData, width,height, stride, self->boxRadius);
my_box_blur_vertical (surfaceData, appState->shadowBuf,width, height, stride, self->boxRadius);
my_box_blur_vertical (appState->shadowBuf, surfaceData,width, height, stride, self->boxRadius);
}else { // buf太小,需要分块blur
unsignedchar *tmpBluredSurfaceData = g_malloc0( sizeof(unsigned char) * stride *height);
memset(appState->shadowBuf, 0, appState->shadowBufSize);
memset(appState->shadowBuf2, 0, appState->shadowBufSize);
intblurTimes = 1; // 各方向blur次数
dig_extra_left_byte = self->boxRadius * 4 *blurTimes;
dig_extra_top_byte = self->boxRadius *blurTimes;
dig_extra_right_byte = self->boxRadius * 4 * blurTimes;
dig_extra_bottom_byte = self->boxRadius * blurTimes;
cut_width_byte = appState->shadowBufStride -dig_extra_left_byte - dig_extra_right_byte;
cut_height_byte = appState->shadowBufHeight -dig_extra_top_byte - dig_extra_bottom_byte;
assert(cut_width_byte > 0);
assert(cut_width_byte > 0);
blockCountX= ceil ((double) stride / cut_width_byte);
blockCountY= ceil ((double) height / cut_height_byte);
for(blockIndexY = 0; blockIndexY < blockCountY; blockIndexY++) {
for(blockIndexX = 0; blockIndexX < blockCountX; blockIndexX++) {
my_util_block_position_in_buffer(surfaceData,
stride,
height,
blockIndexX,
blockIndexY,
cut_width_byte,
cut_height_byte,
dig_extra_left_byte,
dig_extra_top_byte,
dig_extra_right_byte,
dig_extra_bottom_byte,
&blockX,
&blockY,
&block_width_byte,
&block_height_byte,
&block_inner_left_byte,
&block_inner_top_byte,
&block_inner_right_byte,
&block_inner_bottom_byte);
my_util_memcpy_box_to_continuous(surfaceData,
stride,
height,
blockX,
blockY,
block_width_byte,
block_height_byte,
appState->shadowBuf,
0);
//每个方向必须严格blur 2次
my_box_blur_horizontal (appState->shadowBuf,appState->shadowBuf2, block_width_byte / 4, block_height_byte,block_width_byte, self->boxRadius);
// my_box_blur_horizontal (appState->shadowBuf2,appState->shadowBuf, block_width_byte / 4, block_height_byte,block_width_byte, self->boxRadius);
// my_box_blur_vertical (appState->shadowBuf,appState->shadowBuf2, block_width_byte / 4, block_height_byte,block_width_byte, self->boxRadius);
my_box_blur_vertical (appState->shadowBuf2, appState->shadowBuf,block_width_byte / 4, block_height_byte, block_width_byte, self->boxRadius);
my_util_memcpy_box_to_box(appState->shadowBuf,
block_width_byte,
block_height_byte,
block_inner_left_byte,
block_inner_top_byte,
block_width_byte- block_inner_left_byte - block_inner_right_byte,
block_height_byte- block_inner_top_byte - block_inner_bottom_byte,
tmpBluredSurfaceData,
blockX+ block_inner_left_byte,
blockY+ block_inner_top_byte,
stride,
height);
}
}
memcpy(surfaceData, tmpBluredSurfaceData, sizeof(unsigned char) * stride * height);
g_free(tmpBluredSurfaceData);
}
self->isShadowDirty= FALSE;
}
//let us draw the shadow surface finally
cr_window= gdk_cairo_create (appState->pixmap);
cairo_rectangle(cr_window,
self->shadowX+ self->shadowDeltaX + appState->orignX,
self->shadowY+ self->shadowDeltaY + appState->orignY,
self->shadowWidth,
self->shadowHeight);
cairo_clip(cr_window);
cairo_set_source_surface(cr_window,
self->shadowSurface,
self->shadowX+ self->shadowDeltaX + appState->orignX,
self->shadowY+ self->shadowDeltaY + appState->orignY);
cairo_set_operator(cr_window, CAIRO_OPERATOR_MULTIPLY);
cairo_paint(cr_window);
//for debug start
#ifndef MY_GUI_NDEBUG
cairo_set_source_rgb(cr_window, 1., 0., 0.);
cairo_rectangle(cr_window,
self->shadowX+ self->shadowDeltaX + appState->orignX,
self->shadowY+ self->shadowDeltaY + appState->orignY,
self->shadowWidth,
self->shadowHeight);
cairo_rectangle(cr_window,
self->shadowX+ self->shadowDeltaX + self->shadowPadding + appState->orignX,
self->shadowY+ self->shadowDeltaY + self->shadowPadding + appState->orignY,
self->shadowWidth- self->shadowPadding * 2,
self->shadowHeight- self->shadowPadding * 2);
cairo_stroke(cr_window);
#endif
//for debug end
cairo_destroy (cr_window);
}
}
List1-2:
void my_util_block_position_in_buffer (unsigned char *buf,
int buf_stride,
int buf_height,
int block_index_x,
int block_index_y,
int block_width_byte,
int block_height_byte,
int extra_left_byte,
int extra_top_byte,
int extra_right_byte,
int extra_bottom_byte,
int *out_x,
int *out_y,
int *out_width_byte,
int *out_height_byte,
int *out_inner_left_byte,
int *out_inner_top_byte,
int *out_inner_right_byte,
int *out_inner_bottom_byte) {
assert(buf_stride >= 0);
assert(buf_height >= 0);
assert(block_width_byte >= 0);
assert(block_height_byte >= 0);
assert(block_index_x >= 0);
assert(block_index_y >= 0);
assert(extra_left_byte >= 0);
assert(extra_top_byte >= 0);
assert(extra_right_byte >= 0);
assert(extra_bottom_byte >= 0);
intx, y, width_byte, height_byte;
//calculate x
x= block_index_x * block_width_byte;
assert(x < buf_stride);
if(x + block_width_byte < buf_stride) {
width_byte= block_width_byte;
}else {
width_byte= buf_stride - x;
}
if(x - extra_left_byte >= 0) {
x-= extra_left_byte;
width_byte+= extra_left_byte;
*out_inner_left_byte= extra_left_byte;
}else {
*out_inner_left_byte= 0;
}
if(x + width_byte + extra_right_byte <= buf_stride) {
width_byte+= extra_right_byte;
*out_inner_right_byte= extra_right_byte;
}else {
*out_inner_right_byte= 0;
}
//calculate y
y= block_index_y * block_height_byte;
assert(y < buf_height);
if(y + block_height_byte < buf_height) {
height_byte= block_height_byte;
}else {
height_byte= buf_height - y;
}
if(y - extra_top_byte >= 0) {
y-= extra_top_byte;
height_byte+= extra_top_byte;
*out_inner_top_byte= extra_top_byte;
}else {
*out_inner_top_byte= 0;
}
if(y + height_byte + extra_bottom_byte <= buf_height) {
height_byte+= extra_bottom_byte;
*out_inner_bottom_byte= extra_bottom_byte;
}else {
*out_inner_bottom_byte= 0;
}
*out_x= x;
*out_y= y;
*out_width_byte= width_byte;
*out_height_byte= height_byte;
}
void my_util_memcpy_box_to_box (unsigned char *src,
int src_stride,
int src_height_byte,
int box_src_x,
int box_src_y,
int box_width_byte,
int box_height_byte,
unsigned char *dst,
int box_dst_x,
int box_dst_y,
int dst_stride,
int dst_height_byte) {
assert(box_src_x >= 0);
assert(box_src_y >= 0);
assert(box_src_x + box_width_byte <= src_stride);
assert(box_src_y + box_height_byte <= src_height_byte);
assert(box_dst_x >= 0);
assert(box_dst_y >= 0);
assert(box_dst_x + box_width_byte <= dst_stride);
assert(box_dst_y + box_height_byte <= dst_height_byte);
inti, j;
unsignedchar *p; // points to start address of eachrow of src
unsignedchar *q; // points to start address ofeach row of dst
p= src + box_src_y * src_stride + box_src_x;
q= dst + box_dst_y * dst_stride + box_dst_x;
for(i = 0; i < box_height_byte; i++) {
for(j = 0; j < box_width_byte; j++) {
q[j]= p[j];
}
p+= src_stride;
q+= dst_stride;
}
}
void my_util_memcpy_box_to_continuous (unsigned char *src,
int src_stride,
int src_height_byte,
int box_x,
int box_y,
int box_width_byte,
int box_height_byte,
unsigned char *dst,
int dst_offset) {
assert(box_x >= 0);
assert(box_y >= 0);
assert(box_x + box_width_byte <= src_stride);
assert(box_y + box_height_byte <= src_height_byte);
assert(dst_offset >= 0);
inti, j;
unsignedchar *p; // points to start address ofeach row of src
unsignedchar *q; // points to address of dst
p= src + box_y * src_stride + box_x;
q= dst + dst_offset;
for(i = 0; i < box_height_byte; i++) {
for(j = 0; j < box_width_byte; j++) {
*q= p[j];
q++;
}
p+= src_stride;
}
}
List1-3:
void my_box_blur_horizontal (unsigned char * src,
unsigned char * dst,
int width,
int height,
int stride,
int radius){
my_debug("my_box_blur_horizontal from %#x to %#x, %d %d %d", src, dst,stride, rowCount, radius);
g_assert(radius >= 1);
intx, y, k; // pixel coordinary
intboxSize = radius * 2 + 1;
unsignedint totalR, totalG, totalB, totalA;
unsignedchar *p, *q;
for(y = 0; y < height; y++) {
totalR= 0;
totalG= 0;
totalB= 0;
totalA= 0;
for(k = 0; k < min (width, radius); k++) {
p= src + y * stride + k * 4;
totalR+= p[0];
totalG+= p[1];
totalB+= p[2];
totalA+= p[3];
}
for(x = 0; x < width; x++) {
if(x - radius - 1 >= 0) {
p= src + y * stride + (x - radius - 1) * 4;
totalR-= p[0];
totalG-= p[1];
totalB-= p[2];
totalA-= p[3];
}
if(x + radius < width) {
p= src + y * stride + (x + radius) * 4;
totalR+= p[0];
totalG+= p[1];
totalB+= p[2];
totalA+= p[3];
}
q= dst + y * stride + x * 4;
q[0]= totalR / boxSize;
q[1]= totalG / boxSize;
q[2]= totalB / boxSize;
q[3]= totalA / boxSize;
}
}
}
void my_box_blur_vertical (unsigned char*src,
unsigned char*dst,
int width,
int height,
int stride,
int radius){
intx, y, k;
intboxSize = radius * 2 + 1;
unsignedint totalR, totalG, totalB, totalA;
unsignedchar *p, // src pointer, points toaddress of pixel
*q; // dst pointer
for(x = 0; x < width; x++) {
totalR= 0;
totalG= 0;
totalB= 0;
totalA= 0;
for(k = 0; k < min (height, radius); k++) {
p= src + k * stride + x * 4;
totalR+= p[0];
totalG+= p[1];
totalB+= p[2];
totalA+= p[3];
}
for(y = 0; y < height; y++) {
if(y - radius - 1 >= 0) {
p= src + (y - radius - 1) * stride + x * 4;
totalR-= p[0];
totalG-= p[1];
totalB-= p[2];
totalA-= p[3];
}
if(y + radius < height) {
p= src + (y + radius) * stride + x * 4;
totalR+= p[0];
totalG+= p[1];
totalB+= p[2];
totalA+= p[3];
}
q= dst + y * stride + x * 4;
q[0]= totalR / boxSize;
q[1]= totalG / boxSize;
q[2]= totalB / boxSize;
q[3]= totalA / boxSize;
}
}
}
结语:
综上,讲述了如何利用GTK实现图形的阴影效果,按此思路完全可以推广的任何平台,并不局限于GTK。才疏学浅,难免有所纰漏,欢迎批评指正。
谢谢观赏!