Tutorial of using Cairo in Ruby with Glimmer DSL for GTK:
Cairo is the engine behind drawing arbitrary 2D geometric shapes in GTK.
In Glimmer DSL for GTK, you can draw Cairo shapes declaratively in a way similar to how SVG works, but using one language; Ruby, thus being able to utilize Ruby logic (e.g. if statement or each loop) with it effortlessly when needed. Declarative syntax also yields less code that is simpler, not dependent on ordering of nested properties, and more understandable/maintainable.
Below is a quick tutorial consisting of samples inspired and ported from Mohit Sindhwani's blog post "Cairo with Ruby - Samples using RCairo".
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Hello, Drawing Area!'
default_size 256, 256
drawing_area {
# Surface Paint
paint 242.25, 242.25, 242.25
# Set up the parameters
xc = 128.0
yc = 128.0
radius = 100.0
angle1 = 45.0 * (Math::PI/180.0) # angles are specified
angle2 = 180.0 * (Math::PI/180.0) # in radians
# The main arc
arc(xc, yc, radius, angle1, angle2) {
stroke 0, 0, 0
line_width 10
}
# Draw helping lines
# First, the circle at the centre
arc(xc, yc, 10.0, 0, 2*Math::PI) {
fill 255, 51, 51, 0.6
}
# Then, the lines reaching out
path {
arc xc, yc, radius, angle1, angle1
line_to xc, yc
arc xc, yc, radius, angle2, angle2
line_to xc, yc
stroke 255, 51, 51, 0.6
line_width 6
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Arc Negative'
default_size 256, 256
drawing_area {
# Surface Paint
paint 255, 255, 255
# Set up the parameters
xc = 128.0
yc = 128.0
radius = 100.0
angle1 = 45.0 * (Math::PI/180.0) # angles are specified
angle2 = 180.0 * (Math::PI/180.0) # in radians
# The main negative arc
arc_negative(xc, yc, radius, angle1, angle2) {
stroke 0, 0, 0
line_width 10
}
# Draw helping lines
# First, the circle at the centre
arc(xc, yc, 10.0, 0, 2*Math::PI) {
fill 255, 51, 51, 0.6
}
# Then, the lines reaching out
path {
arc(xc, yc, radius, angle1, angle1)
line_to(xc, yc)
arc(xc, yc, radius, angle2, angle2)
line_to(xc, yc)
stroke 255, 51, 51, 0.6
line_width 6
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Clip'
default_size 256, 256
drawing_area {
# Surface Paint
paint 255, 255, 255
# Designate arc as the clipping area
arc(128.0, 128.0, 76.8, 0, 2 * Math::PI) {
clip true
}
# Rectangle will get clipped by arc
rectangle(0, 0, 256, 256) {
fill 0, 0, 0
}
# Path will get clipped by arc
path {
move_to 0, 0
line_to 256, 256
move_to 256, 0
line_to 0, 256
stroke 0, 255, 0
line_width 10
}
}
}.show
require 'glimmer-dsl-gtk'
require 'net/http'
image_content = Net::HTTP.get(URI('https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-gtk/master/images/breaking-blue-wave.png'))
image_file = File.join(Dir.home, 'breaking-blue-wave.png')
File.write(image_file, image_content)
include Glimmer
window {
title 'Clip Image'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
arc(128.0, 128.0, 76.8, 0, 2 * Math::PI) {
clip true # designate arc as the clipping area
}
rectangle(0, 0, 256, 256) {
# Source image is from:
# - https://www.publicdomainpictures.net/en/view-image.php?image=7683&picture=breaking-blue-wave
# Converted to PNG before using it
image = Cairo::ImageSurface.from_png(image_file)
w = image.width
h = image.height
scale 256.0/w, 256.0/h, exclude: :shape # applies scale to fill source image only
fill image, 0, 0
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Curve to'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
x=25.6
y=128.0
x1=102.4
y1=230.4
x2=153.6
y2=25.6
x3=230.4
y3=128.0
path {
move_to x, y
curve_to x1, y1, x2, y2, x3, y3
line_width 10
stroke 0, 0, 0
}
path {
move_to x,y
line_to x1,y1
move_to x2,y2
line_to x3,y3
line_width 6
stroke 255, 51, 51, 0.6
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Dashes'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
dashes = [ 50.0, # ink
10.0, # skip
10.0, # ink
10.0 # skip
]
offset = -50.0
path {
move_to 128.0, 25.6
line_to 230.4, 230.4
rel_line_to -102.4, 0.0
curve_to 51.2, 230.4, 51.2, 128.0, 128.0, 128.0
line_width 10
dash dashes, offset
stroke 0, 0, 0
}
}
}.show
(note: there is no Fill and Stroke 1; this was adopted from Mohit's blog post, which only mentioned Fill and Stroke 2)
samples/cairo/fill_and_stroke2.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Fill and Stroke 2'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
move_to 128.0, 25.6
line_to 230.4, 230.4
rel_line_to -102.4, 0.0
curve_to 51.2, 230.4, 51.2, 128.0, 128.0, 128.0
close_path
fill 0, 0, 255
stroke 0, 0, 0
line_width 10
}
path {
move_to 64.0, 25.6
rel_line_to 51.2, 51.2
rel_line_to -51.2, 51.2
rel_line_to -51.2, -51.2
close_path
fill 0, 0, 255
stroke 0, 0, 0
line_width 10
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Fill Style'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
rectangle 12, 12, 232, 70
path { # sub-path
arc 64, 64, 40, 0, 2*Math::PI
}
path { # sub-path
arc_negative 192, 64, 40, 0, -2*Math::PI
}
fill_rule Cairo::FILL_RULE_EVEN_ODD
line_width 6
fill 0, 178.5, 0
stroke 0, 0, 0
}
path {
rectangle 12, 12, 232, 70
path { # sub-path
arc 64, 64, 40, 0, 2*Math::PI
}
path { # sub-path
arc_negative 192, 64, 40, 0, -2*Math::PI
}
translate 0, 128
fill_rule Cairo::FILL_RULE_WINDING
line_width 6
fill 0, 0, 229.5
stroke 0, 0, 0
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Gradient'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
# Create the Linear Pattern
rectangle(0, 0, 256, 256) {
pat = Cairo::LinearPattern.new(0.0, 0.0, 0.0, 256.0)
pat.add_color_stop_rgba(1, 0, 0, 0, 1)
pat.add_color_stop_rgba(0, 1, 1, 1, 1)
fill pat
}
# Create the radial pattern
arc(128.0, 128.0, 76.8, 0, 2 * Math::PI) {
pat = Cairo::RadialPattern.new(115.2, 102.4, 25.6,
102.4, 102.4, 128.0)
pat.add_color_stop_rgba(0, 1, 1, 1, 1)
pat.add_color_stop_rgba(1, 0, 0, 0, 1)
fill pat
}
}
}.show
require 'glimmer-dsl-gtk'
require 'net/http'
image_content = Net::HTTP.get(URI('https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-gtk/master/images/breaking-blue-wave.png'))
image_file = File.join(Dir.home, 'breaking-blue-wave.png')
File.write(image_file, image_content)
include Glimmer
window {
title 'Image'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
image = Cairo::ImageSurface.from_png(image_file)
w = image.width
h = image.height
translate 128.0, 128.0
rotate 45*Math::PI/180
scale 256.0/w, 256.0/h
translate -0.5*w, -0.5*h
paint image, 0, 0
}
}.show
samples/cairo/image_gradient.rb
require 'glimmer-dsl-gtk'
require 'net/http'
image_content = Net::HTTP.get(URI('https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-gtk/master/images/breaking-blue-wave.png'))
image_file = File.join(Dir.home, 'breaking-blue-wave.png')
File.write(image_file, image_content)
include Glimmer
window {
title 'Image Gradient'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
image = Cairo::ImageSurface.from_png(image_file)
w = image.width
h = image.height
# Load the image as a surface pattern
pattern = Cairo::SurfacePattern.new(image)
pattern.extend = Cairo::EXTEND_REPEAT
# Set up the scale matrix
pattern.matrix = Cairo::Matrix.scale(w/256.0 * 5.0, h/256.0 * 5.0)
rectangle(0, 0, 256, 256) {
translate 128.0, 128.0
rotate Math::PI / 4
scale 1/Math.sqrt(2), 1/Math.sqrt(2)
translate -128.0, -128.0
fill pattern
}
}
}.show
samples/cairo/multi_segment_caps.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Multi Segment Caps'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
move_to 50.0, 75.0
line_to 200.0, 75.0
move_to 50.0, 125.0
line_to 200.0, 125.0
move_to 50.0, 175.0
line_to 200.0, 175.0
line_width 30
line_cap Cairo::LINE_CAP_ROUND
stroke 0, 0, 0
}
}
}.show
samples/cairo/rounded_rectangle.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Rounded Rectangle'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
rounded_rectangle(25.6, 25.6, 204.8, 204.8, 20)
fill 127.5, 127.5, 255
line_width 10.0
stroke 127.5, 0, 0, 0.5
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Set line cap'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
# The main code
path {
move_to 64.0, 50.0
line_to 64.0, 200.0
line_cap Cairo::LINE_CAP_BUTT # default
line_width 30
stroke 0, 0, 0
}
path {
move_to 128.0, 50.0
line_to 128.0, 200.0
line_cap Cairo::LINE_CAP_ROUND
line_width 30
stroke 0, 0, 0
}
path {
move_to 192.0, 50.0
line_to 192.0, 200.0
line_cap Cairo::LINE_CAP_SQUARE
line_width 30
stroke 0, 0, 0
}
# draw helping lines */
path {
move_to 64.0, 50.0
line_to 64.0, 200.0
move_to 128.0, 50.0
line_to 128.0, 200.0
move_to 192.0, 50.0
line_to 192.0, 200.0
line_width 2.56
stroke 255, 51, 51
}
}
}.show
samples/cairo/set_line_join.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Set line join'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
# The main code
path {
move_to 76.8, 84.48
rel_line_to 51.2, -51.2
rel_line_to 51.2, 51.2
line_join Cairo::LINE_JOIN_MITER # default
line_width 40.96
stroke 0, 0, 0
}
path {
move_to 76.8, 161.28
rel_line_to 51.2, -51.2
rel_line_to 51.2, 51.2
line_join Cairo::LINE_JOIN_BEVEL
line_width 40.96
stroke 0, 0, 0
}
path {
move_to 76.8, 238.08
rel_line_to 51.2, -51.2
rel_line_to 51.2, 51.2
line_join Cairo::LINE_JOIN_ROUND
line_width 40.96
stroke 0, 0, 0
}
}
}.show
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Text'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
font_family = OS.linux? ? 'Sans' : (OS.mac? ? 'Helvetica' : 'Arial')
# The main code
path {
move_to 10.0, 135.0
show_text 'Hello'
font_face font_family, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD
font_size 90.0
line_width 2.56
fill 0, 0, 0
stroke 0, 0, 0
}
path {
move_to 70.0, 165.0
text_path 'void'
font_face font_family, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD
font_size 90.0
line_width 2.56
fill 127.5, 127.5, 255
stroke 0, 0, 0
}
# draw helping lines
path {
arc 10.0, 135.0, 5.12, 0, 2*Math::PI
close_path
arc 70.0, 165.0, 5.12, 0, 2*Math::PI
fill 255, 51, 51, 0.6
}
}
}.show
Tetris Screenshot:
Tetris Glimmer DSL for GTK Code:
# From: https://github.com/AndyObtiva/glimmer-dsl-gtk#tetris | |
require 'glimmer-dsl-gtk' | |
require_relative 'tetris/model/game' | |
class Tetris | |
include Glimmer | |
BLOCK_SIZE = 25 | |
BEVEL_CONSTANT = 20 | |
COLOR_GRAY = [192, 192, 192] | |
def initialize | |
@game = Model::Game.new | |
end | |
def launch | |
create_gui | |
register_observers | |
@game.start! | |
@main_window.show | |
end | |
def create_gui | |
@main_window = window { | |
title 'Glimmer Tetris' | |
default_size Model::Game::PLAYFIELD_WIDTH * BLOCK_SIZE, Model::Game::PLAYFIELD_HEIGHT * BLOCK_SIZE # + 98 | |
box(:vertical) { | |
tetris_menu_bar | |
box(:horizontal) { | |
@playfield_blocks = playfield(playfield_width: @game.playfield_width, playfield_height: @game.playfield_height, block_size: BLOCK_SIZE) | |
score_board | |
} | |
} | |
on(:key_press_event) do |widget, key_event| | |
case key_event.keyval | |
when 65364 # down arrow | |
@game.down! | |
when 32 # space | |
@game.down!(instant: true) | |
when 65362 # up arrow | |
case @game.up_arrow_action | |
when :instant_down | |
@game.down!(instant: true) | |
when :rotate_right | |
@game.rotate!(:right) | |
when :rotate_left | |
@game.rotate!(:left) | |
end | |
when 65361 # left arrow | |
@game.left! | |
when 65363 # right arrow | |
@game.right! | |
when 65506 # right shift | |
@game.rotate!(:right) | |
when 65505 # left shift | |
@game.rotate!(:left) | |
else | |
# Do Nothing | |
end | |
end | |
} | |
end | |
def register_observers | |
observe(@game, :game_over) do |game_over| | |
if game_over | |
show_game_over_dialog | |
else | |
start_moving_tetrominos_down | |
end | |
end | |
@game.playfield_height.times do |row| | |
@game.playfield_width.times do |column| | |
observe(@game.playfield[row][column], :color) do |new_color| | |
color = new_color | |
block = @playfield_blocks[row][column] | |
block[:background_square].fill = color | |
block[:top_bevel_edge].fill = [color[0] + 4*BEVEL_CONSTANT, color[1] + 4*BEVEL_CONSTANT, color[2] + 4*BEVEL_CONSTANT] | |
block[:right_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT] | |
block[:bottom_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT] | |
block[:left_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT] | |
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color | |
block[:drawing_area].queue_draw | |
false | |
end | |
end | |
end | |
Model::Game::PREVIEW_PLAYFIELD_HEIGHT.times do |row| | |
Model::Game::PREVIEW_PLAYFIELD_WIDTH.times do |column| | |
observe(@game.preview_playfield[row][column], :color) do |new_color| | |
color = new_color | |
block = @preview_playfield_blocks[row][column] | |
block[:background_square].fill = color | |
block[:top_bevel_edge].fill = [color[0] + 4*BEVEL_CONSTANT, color[1] + 4*BEVEL_CONSTANT, color[2] + 4*BEVEL_CONSTANT] | |
block[:right_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT] | |
block[:bottom_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT] | |
block[:left_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT] | |
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color | |
block[:drawing_area].queue_draw | |
end | |
end | |
end | |
observe(@game, :score) do |new_score| | |
@score_label.text = new_score.to_s | |
end | |
observe(@game, :lines) do |new_lines| | |
@lines_label.text = new_lines.to_s | |
end | |
observe(@game, :level) do |new_level| | |
@level_label.text = new_level.to_s | |
end | |
end | |
def tetris_menu_bar | |
menu_bar { | |
menu_item(label: 'Game') { |mi| | |
m = menu { | |
check_menu_item(label: 'Pause') { | |
on(:activate) do | |
@game.paused = !@game.paused? | |
end | |
} | |
menu_item(label: 'Restart') { | |
on(:activate) do | |
@game.restart! | |
end | |
} | |
separator_menu_item | |
menu_item(label: 'Exit') { | |
on(:activate) do | |
@main_window.close | |
end | |
} | |
} | |
mi.submenu = m.gtk | |
} | |
menu_item(label: 'View') { |mi| | |
m = menu { | |
menu_item(label: 'Show High Scores') { | |
on(:activate) do | |
show_high_score_dialog | |
end | |
} | |
menu_item(label: 'Clear High Scores') { | |
on(:activate) do | |
@game.clear_high_scores! | |
end | |
} | |
} | |
mi.submenu = m.gtk | |
} | |
menu_item(label: 'Options') { |mi| | |
m = menu { | |
rmi = radio_menu_item(nil, 'Instant Down on Up') { | |
on(:activate) do | |
@game.instant_down_on_up! | |
end | |
} | |
default_rmi = radio_menu_item(rmi.group, 'Rotate Right on Up') { | |
on(:activate) do | |
@game.rotate_right_on_up! | |
end | |
} | |
default_rmi.activate | |
radio_menu_item(rmi.group, 'Rotate Left on Up') { | |
on(:activate) do | |
@game.rotate_left_on_up! | |
end | |
} | |
} | |
mi.submenu = m.gtk | |
} | |
menu_item(label: 'Help') { |mi| | |
m = menu { | |
menu_item(label: 'About') { | |
on(:activate) do | |
show_about_dialog | |
end | |
} | |
} | |
mi.submenu = m.gtk | |
} | |
} | |
end | |
def score_board | |
box(:vertical) { | |
label | |
@preview_playfield_blocks = playfield(playfield_width: Model::Game::PREVIEW_PLAYFIELD_WIDTH, playfield_height: Model::Game::PREVIEW_PLAYFIELD_HEIGHT, block_size: BLOCK_SIZE) | |
label | |
label('Score') | |
@score_label = label | |
label | |
label('Lines') | |
@lines_label = label | |
label | |
label('Level') | |
@level_label = label | |
label | |
} | |
end | |
def playfield(playfield_width: , playfield_height: , block_size: , &extra_content) | |
blocks = [] | |
box(:vertical) { | |
playfield_height.times.map do |row| | |
blocks << [] | |
box(:horizontal) { | |
playfield_width.times.map do |column| | |
blocks.last << block(row: row, column: column, block_size: block_size) | |
end | |
} | |
end | |
extra_content&.call | |
} | |
blocks | |
end | |
def block(row: , column: , block_size: , &extra_content) | |
block = {} | |
bevel_pixel_size = 0.16 * block_size.to_f | |
color = Model::Block::COLOR_CLEAR | |
block[:drawing_area] = drawing_area { | |
size_request block_size, block_size | |
block[:background_square] = square(0, 0, block_size) { | |
fill *color | |
} | |
block[:top_bevel_edge] = polygon(0, 0, block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size, bevel_pixel_size) { | |
fill color[0] + 4*BEVEL_CONSTANT, color[1] + 4*BEVEL_CONSTANT, color[2] + 4*BEVEL_CONSTANT | |
} | |
block[:right_bevel_edge] = polygon(block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size, block_size) { | |
fill color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT | |
} | |
block[:bottom_bevel_edge] = polygon(block_size, block_size, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size) { | |
fill color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT | |
} | |
block[:left_bevel_edge] = polygon(0, 0, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size) { | |
fill color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT | |
} | |
block[:border_square] = square(0, 0, block_size) { | |
stroke *COLOR_GRAY | |
} | |
extra_content&.call | |
} | |
block | |
end | |
def start_moving_tetrominos_down | |
unless @tetrominos_start_moving_down | |
@tetrominos_start_moving_down = true | |
GLib::Timeout.add(@game.delay*1000) do | |
@game.down! if !@game.game_over? && !@game.paused? | |
true | |
end | |
end | |
end | |
def show_game_over_dialog | |
message_dialog(@main_window) { |md| | |
title 'Game Over!' | |
text "Score: #{@game.high_scores.first.score}\nLines: #{@game.high_scores.first.lines}\nLevel: #{@game.high_scores.first.level}" | |
on(:response) do | |
md.destroy | |
end | |
}.show | |
@game.restart! | |
false | |
end | |
def show_high_score_dialog | |
game_paused = !!@game.paused | |
@game.paused = true | |
if @game.high_scores.empty? | |
high_scores_string = "No games have been scored yet." | |
else | |
high_scores_string = @game.high_scores.map do |high_score| | |
"#{high_score.name} | Score: #{high_score.score} | Lines: #{high_score.lines} | Level: #{high_score.level}" | |
end.join("\n") | |
end | |
message_dialog(@main_window) { |md| | |
title 'High Scores' | |
text high_scores_string | |
on(:response) do | |
md.destroy | |
end | |
}.show | |
@game.paused = game_paused | |
end | |
def show_about_dialog | |
message_dialog(@main_window) { |md| | |
title 'About' | |
text "Glimmer Tetris\n\nGlimmer DSL for GTK\n\nElaborate Sample\n\nCopyright (c) 2021-2022 Andy Maleh" | |
on(:response) do | |
md.destroy | |
end | |
}.show | |
end | |
end | |
Tetris.new.launch |
No comments:
Post a Comment