package Gtk2::GoBoard; use Scalar::Util; use KGS::Constants; use KGS::Game::Board; use POSIX qw(ceil); use Gtk2; use Glib::Object::Subclass Gtk2::AspectFrame, properties => [ Glib::ParamSpec->IV ( "size", "Board Size", "The Go Board size, 2..38", 2, 38, 19, [qw(construct-only writable readable)], ), ]; sub TRAD_WIDTH (){ 42.42 } # traditional board width sub TRAD_HEIGHT (){ 45.45 } # traditional board height sub TRAD_SIZE_B (){ 2.18 } # traditional black stone size sub TRAD_SIZE_W (){ 2.12 } # traditional white stone size sub SHADOW (){ 0.07 } sub INIT_INSTANCE { my $self = shift; $self->double_buffered (0); $self->set (border_width => 0, shadow_type => 'none', obey_child => 0, ratio => TRAD_WIDTH / TRAD_HEIGHT); $self->add ($self->{canvas} = new Gtk2::DrawingArea); $self->{canvas}->signal_connect (configure_event => sub { $self->configure_event ($_[1]) }); } sub FINALIZE_INSTANCE { my $self = shift; die "FINALIZE board\n";#d# } sub configure_event { my ($self, $event) = @_; $self->{window} = $self->{canvas}->window; my $drawable = $self->{window}; $drawable->set_back_pixmap (undef, 0); #my $gc = new Gtk2::Gdk::GC $drawable; #$gc->set_rgb_fg_color (new Gtk2::Gdk::Color 0xe0e0, 0xb2b2, 0x5e5e); #$drawable->draw_rectangle ($gc, 1, 0, 0, $self->allocation->width, $self->allocation->height); # remove Glib::Source $self->{idle}; $self->{idle} ||= add Glib::Idle sub { delete $self->{stack}; $self->{width} = $self->{canvas}->allocation->width; $self->{height} = $self->{canvas}->allocation->height; $self->draw_board (); $self->draw_stones (delete $self->{board}, 0) if $self->{board}; $self->{window}->clear_area (0, 0, $self->{width}, $self->{height}); delete $self->{idle}; 0; }; 1; } sub set_board { my ($self, $board) = @_; $self->draw_stones ($board, 1); } sub new_pixbuf { my ($w, $h, $alpha, $fill) = @_; my $pixbuf = new Gtk2::Gdk::Pixbuf 'rgb', $alpha, 8, $w, $h; $pixbuf->fill ($fill) if defined $fill; $pixbuf; } sub scale_pixbuf { my ($src, $w, $h, $mode, $alpha) = @_; my $dst = new_pixbuf $w, $h, $alpha; $src->scale( $dst, 0, 0, $w, $h, 0, 0, $w / $src->get_width, $h / $src->get_height, $mode, ); $dst; } sub pixbuf_rect { my ($pb, $colour, $x1, $y1, $x2, $y2, $alpha) = @_; # we fake lines by... a horrible method :/ my $colour_pb = new_pixbuf 1, 1, 0, $colour; $colour_pb->composite ($pb, $x1, $y1, $x2 - $x1 + 1, $y2 - $y1 + 1, $x1, $y1, $x2 + 1, $y2 + 1, 'nearest', $alpha); } sub center_text { my ($self, $drawable, $colour, $x, $y, $size, $text) = @_; # could be optimized by caching quite a bit my $context = $self->get_pango_context; my $font = $context->get_font_description; $font->set_size ($size * Gtk2::Pango->scale); my $layout = new Gtk2::Pango::Layout $context; $layout->set_text ($text); my ($w, $h) = $layout->get_pixel_size; my $gc = new Gtk2::Gdk::GC $drawable; my $r = (($colour >> 24) & 255) * 65535 / 255; my $g = (($colour >> 16) & 255) * 65535 / 255; my $b = (($colour >> 8) & 255) * 65535 / 255; $gc->set_rgb_fg_color (new Gtk2::Gdk::Color $r, $g, $b); $drawable->draw_layout ($gc, $x - $w*0.5, $y - $h*0.5, $layout); } # draw an empty board and attach the bg pixmap sub draw_board { my ($self) = @_; my $canvas = $self->{canvas}; my $size = $self->{size}; my $w = $self->{width}; my $h = $self->{height}; my $pixmap = new Gtk2::Gdk::Pixmap $self->window, $w, $h, -1; my $gridcolour = 0x88444400; # black is traditional, but only with overlapping stones my $labelcolour = 0x88444400; # we leave enough space for the shadows.. I like smaller stones, and we # do no need to do the nifty recursive screen updates that cgoban2 does # TODO: smaller == buggy, want visual perfectness my $borderw = int ($w / ($size + 3) * 0.5); my $borderh = $borderw; my $w2 = $w - $borderw * 2; my $h2 = $h - $borderh * 2; my $edge = $self->{edge} = ceil ($w2 / ($size + 1)); my $ofs = $edge * 0.5; my @kx = map int ($w2 * $_ / ($size+1) + $borderw + 0.5), 0 .. $size; $self->{kx} = \@kx; my @ky = map int ($h2 * $_ / ($size+1) + $borderh + 0.5), 0 .. $size; $self->{ky} = \@ky; my $pixbuf; my ($bw, $bh) = ($::board_img->get_width, $::board_img->get_height); if ($w < $bw && $h < $bh) { $pixbuf = new_pixbuf $w, $h, 0; $::board_img->copy_area (0, 0, $w, $h, $pixbuf, 0, 0); } else { $pixbuf = scale_pixbuf $::board_img, $w, $h, $::config->{speed} ? 'nearest' : 'bilinear', 0; } my $linew = int ($w / 40 / $size); # ornamental border... we have time to waste :/ pixbuf_rect $pixbuf, 0xffcc7700, 0, 0, $w-1, $linew, 255; pixbuf_rect $pixbuf, 0xffcc7700, 0, 0, $linew, $h-1, 255; pixbuf_rect $pixbuf, 0xffcc7700, $w-$linew-1, 0, $w-1, $h-1, 255; pixbuf_rect $pixbuf, 0xffcc7700, 0, $h-$linew-1, $w-1, $h-1, 255; for my $i (1 .. $size) { pixbuf_rect $pixbuf, $gridcolour, $kx[$i] - $linew, $ky[1] - $linew, $kx[$i] + $linew, $ky[$size] + $linew, 255; pixbuf_rect $pixbuf, $gridcolour, $kx[1] - $linew, $ky[$i] - $linew, $kx[$size] + $linew, $ky[$i] + $linew, 255; } # hoshi points my $hoshi = sub { my ($x, $y) = @_; my $hs = 1 | int $edge / 4; $hs = 5 if $hs < 5; $x = $kx[$x] - $hs / 2; $y = $ky[$y] - $hs / 2; # we use the shadow mask... not perfect, but I want to finish this $::shadow_img->composite ($pixbuf, $x, $y, ($hs + 1) x2, $x, $y, $hs / $::shadow_img->get_width, $hs / $::shadow_img->get_height, 'bilinear', 255); }; if ($size > 6) { my $h1 = $size < 10 ? 3 : 4; # corner / edge offset $hoshi->($h1, $h1); $hoshi->($size - $h1 + 1, $h1); $hoshi->($h1, $size - $h1 + 1); $hoshi->($size - $h1 + 1, $size - $h1 + 1); if ($size % 2) { # on odd boards, also the remaining 5 my $h2 = ($size + 1) / 2; if ($size > 10) { $hoshi->($h1, $h2); $hoshi->($size - $h1 + 1, $h2); $hoshi->($h2, $size - $h1 + 1); $hoshi->($h2, $h1); } # the tengen $hoshi->($h2, $h2); } } # now we have a board sans text $pixmap->draw_pixbuf ($self->style->white_gc, $pixbuf, 0, 0, 0, 0, $w, $h, "normal", 0, 0); # now draw the labels for my $i (1 .. $size) { # 38 max, but we allow a bit more my $label = (qw(- A B C D E F G H J K L M N O P Q R S T U V W X Y Z AA BB CC DD EE FF GG HH JJ KK LL MM NN OO PP QQ RR SS TT UU VV WW XX YY ZZ))[$i]; $self->center_text ($pixmap, $labelcolour, $kx[$i], $borderh, $ofs * 0.7, $label); $self->center_text ($pixmap, $labelcolour, $kx[$i], $h2 + $borderh, $ofs * 0.7, $label); $self->center_text ($pixmap, $labelcolour, $borderw, $ky[$i], $ofs * 0.7, $size - $i + 1); $self->center_text ($pixmap, $labelcolour, $w2 + $borderw, $ky[$i], $ofs * 0.7, $size - $i + 1); } $self->{window}->set_back_pixmap ($pixmap, 0); $self->{backgroundpm} = $pixmap; $self->{backgroundpb} = $pixbuf; } # create a stack of stones sub get_stack { my ($self, $mark, $size) = @_; $self->{stack}{$mark} ||= do { my @stack; my $csize = ceil $size; my $shadow = $size * SHADOW; for my $stone ($mark & MARK_W ? @::white_img : @::black_img) { my $base = new_pixbuf +(ceil $size + $shadow) x2, 1, 0x00000000; # zeroeth the shadow if (~$mark & MARK_GRAYED and $mark & (MARK_B | MARK_W)) { $::shadow_img->composite ( $base, $shadow, $shadow, $csize, $csize, $shadow, $shadow, $size / $::shadow_img->get_width, $size / $::shadow_img->get_height, 'bilinear', 128 ); } for ([MARK_B, $mark & MARK_GRAYED ? 96 : 255, 1], [MARK_W, $mark & MARK_GRAYED ? 160 : 255, TRAD_SIZE_W / TRAD_SIZE_B]) { my ($mask, $alpha, $scale) = @$_; if ($mark & $mask) { $stone->composite ( $base, 0, 0, $csize, $csize, ($size * (1 - $scale) * 0.5 ) x2, $size * $scale / $stone->get_width, $size * $scale / $stone->get_height, 'bilinear', $alpha ); } } # then the small stones for ([MARK_SMALL_B, $::black_img[$rand % @::black_img]], [MARK_SMALL_W, $::white_img[$rand % @::white_img]]) { my ($mask, $img) = @$_; if ($mark & $mask) { $img->composite ( $base, (int $size / 4) x2, (ceil $size / 2 + 1) x2, ($size / 4) x2, $size / $img->get_width / 2, $size / $img->get_height / 2, 'bilinear', 255 ); } } # and lastly any markers my $dark_bg = ! ! ($mark & (MARK_B | MARK_GRAY_B)); for ([MARK_CIRCLE, $::circle_img[$dark_bg]], [MARK_TRIANGLE, $::triangle_img[$dark_bg]], [MARK_SQUARE, $::square_img[$dark_bg]]) { my ($mask, $img) = @$_; if ($mark & $mask) { $img->composite ( $base, 0, 0, $size, $size, 0, 0, $size / $img->get_width, $size / $img->get_height, 'bilinear', 176 ); } } push @stack, $base; } \@stack; } } sub draw_stones { my ($self, $board, $dopaint) = @_; if ($self->{backgroundpb}) { my @areas; my $old = delete $self->{board}; my $size = $self->{size}; my $edge = int ($self->{edge}) | 1; my $ofs = $edge * 0.5; my $kx = $self->{kx}; my $ky = $self->{ky}; for my $x (1 .. $size) { for my $y (1 .. $size) { my $mark = $board->{board}[$x-1][$y-1]; my $mold = $old->{board}[$x-1][$y-1]; if ($mold != $mark) { my $rand = $x ^ $y; my $shadow = $edge * SHADOW; my @area = ($kx->[$x] - $ofs, $ky->[$y] - $ofs, $edge + $shadow, $edge + $shadow); my @areai = ((ceil $area[0]), (ceil $area[1]), (int $area[2]), (int $area[3])); # area, integer my $pb = new_pixbuf @areai[2,3]; $self->{backgroundpb}->copy_area (@areai, $pb, 0, 0); my $put_stack = sub { my ($x, $y, $dx, $dy, $ox, $oy) = @_; my $mark = $board->{board}[$x-1][$y-1]; if ($mark & ~MARK_LABEL) { my $stack = $self->get_stack ($mark, $edge); $stack->[$rand % @$stack] ->composite ($pb, $ox, $oy, $areai[2] + $dx - $ox, $areai[3] + $dy - $oy, $dx + $ox, $dy + $oy, 1, 1, 'nearest', 255); } }; $put_stack->($x-1, $y, $kx->[$x-1] - $kx->[$x], 0, 0, 0) if $x > 1; $put_stack->($x, $y-1, 0, $ky->[$y-1] - $ky->[$y], 0, 0) if $y > 1; $put_stack->($x , $y , 0, 0); $put_stack->($x+1, $y, 0, 0, $kx->[$x+1] - $kx->[$x], 0) if $x < $size; $put_stack->($x, $y+1, 0, 0, 0, $ky->[$y+1] - $ky->[$y]) if $y < $size; # speed none, normal, max $self->{backgroundpm}->draw_pixbuf ($self->style->black_gc, $pb, 0, 0, @areai, 'max', 0, 0); # labels are handled here because they are quite rare if ($mark & MARK_LABEL) { my $white = $mark & (MARK_W | MARK_GRAY_W) ? 0 : 0xffffff00; if ($white) { $self->center_text ($self->{backgroundpm}, 0, $area[0] + $ofs * 1.1, $area[1] + $ofs * 1.1, $ofs * 0.7, $self->{board}{label}[$x-1][$y-1]); } $self->center_text ($self->{backgroundpm}, $white, $area[0] + $ofs, $area[1] + $ofs, $ofs * 0.7, $self->{board}{label}[$x-1][$y-1]); } # a single full clear_area is way faster than many single calls here push @areas, \@areai if $dopaint; #my $redraw; $redraw = sub { # my ($x, $y) = @_; # return if $old->{board}[$x][$y] & MARK_REDRAW; # $old->{board}[$x][$y] |= MARK_REDRAW; # $redraw->($x+1, $y) if $old->{board}[$x+1][$y] & (MARK_W | MARK_B | MARK_GREY_W | MARK_GREY_B); # $redraw->($x, $y+1) if $old->{board}[$x][$y+1] & (MARK_W | MARK_B | MARK_GREY_W | MARK_GREY_B); #}; # #$redraw->($x-1, $y-1); } } } if (@areas) { # the "cut-off" point is arbitrary if (@areas > 16) { # update a single rectangle only my $rect = new Gtk2::Gdk::Rectangle @{pop @areas}; $rect = $rect->union (new Gtk2::Gdk::Rectangle @$_) for @areas; $self->{window}->clear_area ($rect->values); } else { # update all the affected rectangles $self->{window}->clear_area (@$_) for @areas; } } } $self->{board} = $board; #d# save #Storable::nstore { board => $self->{board}, size => $self->{size}, path => $self->{path}}, "testboard.storable"; } sub FINALIZE { warn "FINALIZE(@_)\n";#d# my ($self) = @_; $self->{userpanel}[$_] && (delete $self->{userpanel}[$_])->destroy for BLACK, WHITE; $self->SUPER::destroy; delete $self->{app}{gamelist}{game}{$self->{channel}}; } 1;