Script Mode

A .sevgi file is ordinary Ruby run by the sevgi command. Before evaluating the file, the runner installs Sevgi's DSL words in the script's top-level scope.

Applications embedding the same runner should use the result-oriented Execution API.

Script shape

A typical script has:

In the checkers example, a Ruby hash stores the piece positions. A callable module holds the drawing steps, and the SVG block invokes them with Call.

Callable modules can put argument-independent SVG in base blocks. Sevgi runs inherited bases from parent to child, then local bases in registration order. Name a single drawing method call; give multiple methods names that describe their drawing steps.

#!/usr/bin/env -S ruby -S sevgi
# frozen_string_literal: true

module CheckerBoard
  extend SVG::Module

  BOARD_MARGIN = 50
  CELL_SIZE = 100
  PIECE_SIZE = 80
  SIZE = (CELL_SIZE * 8) + (BOARD_MARGIN * 2)

  STYLE = {
    ".board-frame" => { fill: "#d1bfa7", stroke: "#6a3f2b" },
    ".inner-frame" => {
      fill: "#6a3f2b",
      stroke: "#6a3f2b",
      "stroke-width": (BOARD_MARGIN * 0.7).round
    },
    ".cell" => { stroke: "white", "stroke-width": 1 },
    ".light.cell" => { fill: "#d4a76f" },
    ".dark.cell" => { fill: "#6a3f2b" },
    ".piece" => {
      filter: "drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5))",
      stroke: "none"
    },
    ".light.piece" => { fill: "#f5f5f5" },
    ".dark.piece" => { fill: "#333" }
  }.freeze

  base do # Draw the board.
    css STYLE

    square class: "board-frame", length: SIZE
    square class: "inner-frame", x: BOARD_MARGIN, y: BOARD_MARGIN,
           length: SIZE - (BOARD_MARGIN * 2)

    8.times do |row|
      8.times do |col|
        square class: "#{(row + col).even? ? "dark" : "light"} cell",
               x: (col * CELL_SIZE) + BOARD_MARGIN,
               y: (row * CELL_SIZE) + BOARD_MARGIN,
               length: CELL_SIZE
      end
    end
  end

  def call(placement)
    center = BOARD_MARGIN + (CELL_SIZE / 2)

    placement.each do |tone, rows|
      rows.each do |row, cols|
        cols.each do |col|
          circle class: "#{tone} piece",
                 cx: (col * CELL_SIZE) + center,
                 cy: (row * CELL_SIZE) + center,
                 r: PIECE_SIZE / 2
        end
      end
    end
  end
end

placement = {
  dark: {
    0 => [1, 3, 5, 7],
    1 => [0, 2, 4, 6]
  },
  light: {
    6 => [1, 3, 5, 7],
    7 => [0, 2, 4, 6]
  }
}

SVG :minimal,
    width: CheckerBoard::SIZE,
    height: CheckerBoard::SIZE,
    viewBox: "0 0 #{CheckerBoard::SIZE} #{CheckerBoard::SIZE}" do
  Call CheckerBoard, placement
end.Save
#!/usr/bin/env -S ruby -S sevgi
# frozen_string_literal: true

module CheckerBoard
  extend SVG::Module

  BOARD_MARGIN = 50
  CELL_SIZE = 100
  PIECE_SIZE = 80
  SIZE = (CELL_SIZE * 8) + (BOARD_MARGIN * 2)

  STYLE = {
    ".board-frame" => { fill: "#705f4b", stroke: "#d3a778" },
    ".inner-frame" => {
      fill: "#d3a778",
      stroke: "#d3a778",
      "stroke-width": (BOARD_MARGIN * 0.7).round
    },
    ".cell" => { stroke: "white", "stroke-width": 1 },
    ".light.cell" => { fill: "#b88a55" },
    ".dark.cell" => { fill: "#d3a778" },
    ".piece" => {
      filter: "drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5))",
      stroke: "none"
    },
    ".light.piece" => { fill: "#e8e2dc" },
    ".dark.piece" => { fill: "#161616" }
  }.freeze

  base do # Draw the board.
    css STYLE

    square class: "board-frame", length: SIZE
    square class: "inner-frame", x: BOARD_MARGIN, y: BOARD_MARGIN,
           length: SIZE - (BOARD_MARGIN * 2)

    8.times do |row|
      8.times do |col|
        square class: "#{(row + col).even? ? "dark" : "light"} cell",
               x: (col * CELL_SIZE) + BOARD_MARGIN,
               y: (row * CELL_SIZE) + BOARD_MARGIN,
               length: CELL_SIZE
      end
    end
  end

  def call(placement)
    center = BOARD_MARGIN + (CELL_SIZE / 2)

    placement.each do |tone, rows|
      rows.each do |row, cols|
        cols.each do |col|
          circle class: "#{tone} piece",
                 cx: (col * CELL_SIZE) + center,
                 cy: (row * CELL_SIZE) + center,
                 r: PIECE_SIZE / 2
        end
      end
    end
  end
end

placement = {
  dark: {
    0 => [1, 3, 5, 7],
    1 => [0, 2, 4, 6]
  },
  light: {
    6 => [1, 3, 5, 7],
    7 => [0, 2, 4, 6]
  }
}

SVG :minimal,
    width: CheckerBoard::SIZE,
    height: CheckerBoard::SIZE,
    viewBox: "0 0 #{CheckerBoard::SIZE} #{CheckerBoard::SIZE}" do
  Call CheckerBoard, placement
end.Save
<svg width="900" height="900" viewBox="0 0 900 900">
  <style type="text/css">
    <![CDATA[
      .board-frame {
        fill: #d1bfa7;
        stroke: #6a3f2b;
      }
      .inner-frame {
        fill: #6a3f2b;
        stroke: #6a3f2b;
        stroke-width: 35;
      }
      .cell {
        stroke: white;
        stroke-width: 1;
      }
      .light.cell {
        fill: #d4a76f;
      }
      .dark.cell {
        fill: #6a3f2b;
      }
      .piece {
        filter: drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5));
        stroke: none;
      }
      .light.piece {
        fill: #f5f5f5;
      }
      .dark.piece {
        fill: #333;
      }
    ]]>
  </style>
  <rect width="900" height="900" class="board-frame"/>
  <rect width="800" height="800" class="inner-frame" x="50" y="50"/>
  <rect width="100" height="100" class="dark cell" x="50" y="50"/>
  <rect width="100" height="100" class="light cell" x="150" y="50"/>
  <rect width="100" height="100" class="dark cell" x="250" y="50"/>
  <rect width="100" height="100" class="light cell" x="350" y="50"/>
  <rect width="100" height="100" class="dark cell" x="450" y="50"/>
  <rect width="100" height="100" class="light cell" x="550" y="50"/>
  <rect width="100" height="100" class="dark cell" x="650" y="50"/>
  <rect width="100" height="100" class="light cell" x="750" y="50"/>
  <rect width="100" height="100" class="light cell" x="50" y="150"/>
  <rect width="100" height="100" class="dark cell" x="150" y="150"/>
  <rect width="100" height="100" class="light cell" x="250" y="150"/>
  <rect width="100" height="100" class="dark cell" x="350" y="150"/>
  <rect width="100" height="100" class="light cell" x="450" y="150"/>
  <rect width="100" height="100" class="dark cell" x="550" y="150"/>
  <rect width="100" height="100" class="light cell" x="650" y="150"/>
  <rect width="100" height="100" class="dark cell" x="750" y="150"/>
  <rect width="100" height="100" class="dark cell" x="50" y="250"/>
  <rect width="100" height="100" class="light cell" x="150" y="250"/>
  <rect width="100" height="100" class="dark cell" x="250" y="250"/>
  <rect width="100" height="100" class="light cell" x="350" y="250"/>
  <rect width="100" height="100" class="dark cell" x="450" y="250"/>
  <rect width="100" height="100" class="light cell" x="550" y="250"/>
  <rect width="100" height="100" class="dark cell" x="650" y="250"/>
  <rect width="100" height="100" class="light cell" x="750" y="250"/>
  <rect width="100" height="100" class="light cell" x="50" y="350"/>
  <rect width="100" height="100" class="dark cell" x="150" y="350"/>
  <rect width="100" height="100" class="light cell" x="250" y="350"/>
  <rect width="100" height="100" class="dark cell" x="350" y="350"/>
  <rect width="100" height="100" class="light cell" x="450" y="350"/>
  <rect width="100" height="100" class="dark cell" x="550" y="350"/>
  <rect width="100" height="100" class="light cell" x="650" y="350"/>
  <rect width="100" height="100" class="dark cell" x="750" y="350"/>
  <rect width="100" height="100" class="dark cell" x="50" y="450"/>
  <rect width="100" height="100" class="light cell" x="150" y="450"/>
  <rect width="100" height="100" class="dark cell" x="250" y="450"/>
  <rect width="100" height="100" class="light cell" x="350" y="450"/>
  <rect width="100" height="100" class="dark cell" x="450" y="450"/>
  <rect width="100" height="100" class="light cell" x="550" y="450"/>
  <rect width="100" height="100" class="dark cell" x="650" y="450"/>
  <rect width="100" height="100" class="light cell" x="750" y="450"/>
  <rect width="100" height="100" class="light cell" x="50" y="550"/>
  <rect width="100" height="100" class="dark cell" x="150" y="550"/>
  <rect width="100" height="100" class="light cell" x="250" y="550"/>
  <rect width="100" height="100" class="dark cell" x="350" y="550"/>
  <rect width="100" height="100" class="light cell" x="450" y="550"/>
  <rect width="100" height="100" class="dark cell" x="550" y="550"/>
  <rect width="100" height="100" class="light cell" x="650" y="550"/>
  <rect width="100" height="100" class="dark cell" x="750" y="550"/>
  <rect width="100" height="100" class="dark cell" x="50" y="650"/>
  <rect width="100" height="100" class="light cell" x="150" y="650"/>
  <rect width="100" height="100" class="dark cell" x="250" y="650"/>
  <rect width="100" height="100" class="light cell" x="350" y="650"/>
  <rect width="100" height="100" class="dark cell" x="450" y="650"/>
  <rect width="100" height="100" class="light cell" x="550" y="650"/>
  <rect width="100" height="100" class="dark cell" x="650" y="650"/>
  <rect width="100" height="100" class="light cell" x="750" y="650"/>
  <rect width="100" height="100" class="light cell" x="50" y="750"/>
  <rect width="100" height="100" class="dark cell" x="150" y="750"/>
  <rect width="100" height="100" class="light cell" x="250" y="750"/>
  <rect width="100" height="100" class="dark cell" x="350" y="750"/>
  <rect width="100" height="100" class="light cell" x="450" y="750"/>
  <rect width="100" height="100" class="dark cell" x="550" y="750"/>
  <rect width="100" height="100" class="light cell" x="650" y="750"/>
  <rect width="100" height="100" class="dark cell" x="750" y="750"/>
  <circle class="dark piece" cx="200" cy="100" r="40"/>
  <circle class="dark piece" cx="400" cy="100" r="40"/>
  <circle class="dark piece" cx="600" cy="100" r="40"/>
  <circle class="dark piece" cx="800" cy="100" r="40"/>
  <circle class="dark piece" cx="100" cy="200" r="40"/>
  <circle class="dark piece" cx="300" cy="200" r="40"/>
  <circle class="dark piece" cx="500" cy="200" r="40"/>
  <circle class="dark piece" cx="700" cy="200" r="40"/>
  <circle class="light piece" cx="200" cy="700" r="40"/>
  <circle class="light piece" cx="400" cy="700" r="40"/>
  <circle class="light piece" cx="600" cy="700" r="40"/>
  <circle class="light piece" cx="800" cy="700" r="40"/>
  <circle class="light piece" cx="100" cy="800" r="40"/>
  <circle class="light piece" cx="300" cy="800" r="40"/>
  <circle class="light piece" cx="500" cy="800" r="40"/>
  <circle class="light piece" cx="700" cy="800" r="40"/>
</svg>
<svg width="900" height="900" viewBox="0 0 900 900">
  <style type="text/css">
    <![CDATA[
      .board-frame {
        fill: #705f4b;
        stroke: #d3a778;
      }
      .inner-frame {
        fill: #d3a778;
        stroke: #d3a778;
        stroke-width: 35;
      }
      .cell {
        stroke: white;
        stroke-width: 1;
      }
      .light.cell {
        fill: #b88a55;
      }
      .dark.cell {
        fill: #d3a778;
      }
      .piece {
        filter: drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5));
        stroke: none;
      }
      .light.piece {
        fill: #e8e2dc;
      }
      .dark.piece {
        fill: #161616;
      }
    ]]>
  </style>
  <rect width="900" height="900" class="board-frame"/>
  <rect width="800" height="800" class="inner-frame" x="50" y="50"/>
  <rect width="100" height="100" class="dark cell" x="50" y="50"/>
  <rect width="100" height="100" class="light cell" x="150" y="50"/>
  <rect width="100" height="100" class="dark cell" x="250" y="50"/>
  <rect width="100" height="100" class="light cell" x="350" y="50"/>
  <rect width="100" height="100" class="dark cell" x="450" y="50"/>
  <rect width="100" height="100" class="light cell" x="550" y="50"/>
  <rect width="100" height="100" class="dark cell" x="650" y="50"/>
  <rect width="100" height="100" class="light cell" x="750" y="50"/>
  <rect width="100" height="100" class="light cell" x="50" y="150"/>
  <rect width="100" height="100" class="dark cell" x="150" y="150"/>
  <rect width="100" height="100" class="light cell" x="250" y="150"/>
  <rect width="100" height="100" class="dark cell" x="350" y="150"/>
  <rect width="100" height="100" class="light cell" x="450" y="150"/>
  <rect width="100" height="100" class="dark cell" x="550" y="150"/>
  <rect width="100" height="100" class="light cell" x="650" y="150"/>
  <rect width="100" height="100" class="dark cell" x="750" y="150"/>
  <rect width="100" height="100" class="dark cell" x="50" y="250"/>
  <rect width="100" height="100" class="light cell" x="150" y="250"/>
  <rect width="100" height="100" class="dark cell" x="250" y="250"/>
  <rect width="100" height="100" class="light cell" x="350" y="250"/>
  <rect width="100" height="100" class="dark cell" x="450" y="250"/>
  <rect width="100" height="100" class="light cell" x="550" y="250"/>
  <rect width="100" height="100" class="dark cell" x="650" y="250"/>
  <rect width="100" height="100" class="light cell" x="750" y="250"/>
  <rect width="100" height="100" class="light cell" x="50" y="350"/>
  <rect width="100" height="100" class="dark cell" x="150" y="350"/>
  <rect width="100" height="100" class="light cell" x="250" y="350"/>
  <rect width="100" height="100" class="dark cell" x="350" y="350"/>
  <rect width="100" height="100" class="light cell" x="450" y="350"/>
  <rect width="100" height="100" class="dark cell" x="550" y="350"/>
  <rect width="100" height="100" class="light cell" x="650" y="350"/>
  <rect width="100" height="100" class="dark cell" x="750" y="350"/>
  <rect width="100" height="100" class="dark cell" x="50" y="450"/>
  <rect width="100" height="100" class="light cell" x="150" y="450"/>
  <rect width="100" height="100" class="dark cell" x="250" y="450"/>
  <rect width="100" height="100" class="light cell" x="350" y="450"/>
  <rect width="100" height="100" class="dark cell" x="450" y="450"/>
  <rect width="100" height="100" class="light cell" x="550" y="450"/>
  <rect width="100" height="100" class="dark cell" x="650" y="450"/>
  <rect width="100" height="100" class="light cell" x="750" y="450"/>
  <rect width="100" height="100" class="light cell" x="50" y="550"/>
  <rect width="100" height="100" class="dark cell" x="150" y="550"/>
  <rect width="100" height="100" class="light cell" x="250" y="550"/>
  <rect width="100" height="100" class="dark cell" x="350" y="550"/>
  <rect width="100" height="100" class="light cell" x="450" y="550"/>
  <rect width="100" height="100" class="dark cell" x="550" y="550"/>
  <rect width="100" height="100" class="light cell" x="650" y="550"/>
  <rect width="100" height="100" class="dark cell" x="750" y="550"/>
  <rect width="100" height="100" class="dark cell" x="50" y="650"/>
  <rect width="100" height="100" class="light cell" x="150" y="650"/>
  <rect width="100" height="100" class="dark cell" x="250" y="650"/>
  <rect width="100" height="100" class="light cell" x="350" y="650"/>
  <rect width="100" height="100" class="dark cell" x="450" y="650"/>
  <rect width="100" height="100" class="light cell" x="550" y="650"/>
  <rect width="100" height="100" class="dark cell" x="650" y="650"/>
  <rect width="100" height="100" class="light cell" x="750" y="650"/>
  <rect width="100" height="100" class="light cell" x="50" y="750"/>
  <rect width="100" height="100" class="dark cell" x="150" y="750"/>
  <rect width="100" height="100" class="light cell" x="250" y="750"/>
  <rect width="100" height="100" class="dark cell" x="350" y="750"/>
  <rect width="100" height="100" class="light cell" x="450" y="750"/>
  <rect width="100" height="100" class="dark cell" x="550" y="750"/>
  <rect width="100" height="100" class="light cell" x="650" y="750"/>
  <rect width="100" height="100" class="dark cell" x="750" y="750"/>
  <circle class="dark piece" cx="200" cy="100" r="40"/>
  <circle class="dark piece" cx="400" cy="100" r="40"/>
  <circle class="dark piece" cx="600" cy="100" r="40"/>
  <circle class="dark piece" cx="800" cy="100" r="40"/>
  <circle class="dark piece" cx="100" cy="200" r="40"/>
  <circle class="dark piece" cx="300" cy="200" r="40"/>
  <circle class="dark piece" cx="500" cy="200" r="40"/>
  <circle class="dark piece" cx="700" cy="200" r="40"/>
  <circle class="light piece" cx="200" cy="700" r="40"/>
  <circle class="light piece" cx="400" cy="700" r="40"/>
  <circle class="light piece" cx="600" cy="700" r="40"/>
  <circle class="light piece" cx="800" cy="700" r="40"/>
  <circle class="light piece" cx="100" cy="800" r="40"/>
  <circle class="light piece" cx="300" cy="800" r="40"/>
  <circle class="light piece" cx="500" cy="800" r="40"/>
  <circle class="light piece" cx="700" cy="800" r="40"/>
</svg>

Output methods

Save writes next to the script with the same base name and an .svg extension. The showcase files use this form.

Write(path) writes to a specific destination.

Out prints SVG to standard output, which suits shell pipelines and tests.

The runner reads a file operand, -, or standard input when no file is given. Input from standard input has the logical name output.sevgi, so an implicit Save, PDF, or PNG writes output.svg, output.pdf, or output.png. Use --as NAME when the pipeline has a more useful identity. NAME is a basename, not a path:

sevgi --as badge < drawing.sevgi

That command evaluates the input as badge.sevgi, making an implicit Save write badge.svg. The option also works with a file operand and keeps the file's directory, so relative Load calls still resolve beside the source:

sevgi --as proof drawings/card.sevgi

Here an implicit Save writes drawings/proof.svg. Explicit destinations such as Save "build/card.svg" and an explicit default: always take precedence over the logical input name.

Top-level DSL scope

In a script, call SVG, SVG elements such as rect and circle, and capitalized operations such as Canvas, Paper, and TileX as plain DSL words. There is little reason to spell out Sevgi's object graph in a file whose job is to draw.

It is still Ruby. Use hashes, loops, calculations, and helper objects wherever they make the drawing easier to read.

The runner installs the full top-level API in a managed scope. The script needs neither require "sevgi" nor an SVG. facade receiver:

Paper 85, 55, :card

SVG :minimal, :card do
  rect width: "100%", height: "100%", rx: 3
end.Save

Library code uses the same words through the facade: the example above becomes SVG.Paper(...) followed by the same SVG(...) block. Types and callable-module contracts keep their double-colon spelling in both modes, such as SVG::Canvas and SVG::Module.

Load

Load "palette" evaluates palette.sevgi relative to the active script, not the process working directory. You can move a drawing split across several files as one directory. If loading fails, the executor error keeps the source stack and points back to the file that caused it.

Top-level API

Script mode exposes the document entry points SVG, Canvas, Document, Document!, Paper, Paper!, Mixin, Grid, and Load. Its Derender entry points are Decompile, Derender, Evaluate, and EvaluateChildren; append File to any of those names when the input is a file path. Drawing words such as Rotate live inside an SVG block. The DSL Catalog records the context for every word, including script-only helpers.

Rake

Require sevgi/binaries/rake in a Rakefile to run a script without spawning a shell:

require "sevgi/binaries/rake"

file "card.svg" => "card.sevgi" do
  sevgi "card", "front", theme: :dark
end

Positional arguments arrive as ARGA; keyword arguments arrive as ARGH.