#
Component
By using the Victor::Component
base class, you can compose complex SVG images
by having one component call and reference other components.
#
Target Image
In the example below, we are creating this output, using Cell
, Piece
and
Board
components.
#
Usage Pattern
- Create a class that inherits from
Victor::Component
- Implement
#width
and#height
, either as public methods or instance variables. - Implement
#body
, and use it to add SVG elements and/or embed other components. - Optionally, implement
#style
, and use it to return a CSS hash, which will be merged to any hosting component. - Optionally, if you want to provide host components with the ability to
control
x
andy
, provide them as public methods or instance variables.
#
Notes
- Components are always generated with 100% width and height, and with a
viewBox
that is determined by your x, y, width, height properties (x and y default to 0). - Once a component was rendered (
#render
,#to_s
) or saved (#save
), the#body
method will be called once and once only. This means that at this point the SVG can no longer be altered. - Each component is also a standalone SVG, that can be saved or rendered independently.
#
Code
require 'victor'
class Cell < Victor::Component
attr_reader :dark
def initialize(x: 0, y: 0, dark: false)
@dark = dark
# These 4 do not need attr_reader, Victor::Component handles that.
# While x and y are optional, width and height are required.
@x = x
@y = y
@width = 100
@height = 100
end
# This method defines the SVG body.
# Use add.* to add any SVG element.
def body
add.rect class: css_class, x: x, y: y, width: width, height: height
end
# This optional method returns a CSS hash. It will be merged into any host
# component automatically.
def style
{
'.cell': { stroke: :white, stroke_width: 1 },
'.light.cell': { fill: '#d4a76f' },
'.dark.cell': { fill: '#6a3f2b' }
}
end
private
def css_class
@css_class ||= (dark ? 'dark cell' : 'light cell')
end
end
require 'victor'
class Piece < Victor::Component
attr_reader :dark
def initialize(x: 0, y: 0, dark: false)
@dark = dark
@x = x
@y = y
end
# Width and height can bn provided as methods, or as instance variables in
# the #initialize method.
def width = @width ||= 80
def height = @height ||= 80
# This method defines the SVG body.
# Use add.* to add any SVG element.
def body
add.circle class: css_class,
cx: x + width / 2, cy: y + height / 2,
r: [width, height].min / 2
end
# This optional method returns a CSS hash. It will be merged into any host
# component automatically.
def style
{
'.piece': {
filter: 'drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5))',
stoke: :none
},
'.light.piece': {
fill: '#f5f5f5',
},
'.dark.piece': {
fill: '#333',
}
}
end
private
def css_class
@css_class ||= (dark ? 'dark piece' : 'light piece')
end
end
require 'victor'
class Board < Victor::Component
# Since all components provide Width and height, we can use these properties
# to calculate this component's dimensions dynamically.
def width = @width ||= (cell_width * 8) + (board_margin * 2)
def height = @height ||= (cell_height * 8) + (board_margin * 2)
# Separate the SVG generation to logical units.
def body
add_frame
add_cells
add_pieces
end
# Provide a public interface for consumers to add pieces after creating
# the board instance. Note that once the component was rendered, its SVG
# is frozen - meaning this method must be called before rendering.
# For this reason, we only remember the added pieces in an array for later
# use.
def add_piece(row, col, dark: false)
piece = Piece.new dark: dark,
x: col * cell_width + board_margin + 10,
y: row * cell_height + board_margin + 10
pieces.push piece
end
def style
{
'.board-frame': {
fill: '#d1bfa7',
stroke: '#6a3f2b'
},
'.inner-frame': {
fill: '#6a3f2b',
stroke: '#6a3f2b',
stroke_width: (board_margin * 0.7).round
},
}
end
private
def board_margin = @board_margin ||= 50
def cell_width = @cell_width ||= sample_cell.width
def cell_height = @cell_height ||= sample_cell.height
def sample_cell = @sample_cell ||= Cell.new
def pieces = @pieces ||= []
# Add a double-rectangle board frame
def add_frame
add.rect class: 'board-frame', x: 0, y: 0, width: width, height: height
add.rect class: 'inner-frame', x: board_margin, y: board_margin,
width: width - (board_margin * 2),
height: height - (board_margin * 2)
end
# Create the 8x8 grid.
def add_cells
8.times do |row|
dark = row.even?
8.times do |col|
cell = Cell.new dark: dark,
x: col * cell_width + board_margin,
y: row * cell_height + board_margin
embed cell
dark = !dark
end
end
end
# Add all pieces as requested by #add_piece
def add_pieces
pieces.each { |piece| embed piece }
end
end