Трета задача

Предадени решения

Краен срок:
23.12.2013 00:05
Точки:
6

Срокът за предаване на решения е отминал

Растеризация

В тази задача ще се опитаме да създадем мини графична библиотека. Тя ще ни позволи да чертаем много прости фигури като точки, линии и правоъгълници на пано (canvas).

Паната ще могат да бъдат визуализирани посредством т. нар. "renderers" – неща, които работят взаимно с пано и създават неговото визуално представяне. В тази задача ще дефинираме два такива.

Изолация

Всички класове и модули, дефинирани в нашата библиотека, ще са капсулирани в един модул, наречен Graphics. По-надолу в условието може да споменаваме класове, без да указваме изрично, че са дефинирани в този модул, тъй като това ще се подразбира.

Например, когато говорим за класа Point, ще подразбираме Graphics::Point и така нататък.

Непроменяемост

Ще се стремим обектите от нашата библиотека да са непроменяеми, т.е. immutable. Това означава, че ако сме създали точка, линия, правоъгълник или друг обект, не трябва да има публичен интерфейс, позволяващ на потребителя да променя полетата или вътрешното състояние на обектите. Затова внимавайте с публичните методи, които вашите класове предоставят на външния свят.

Единственото очевидно изключение от това правило е обектът "пано", по което ще можем да рисуваме.

Пано

Паното представлява правоъгълник от пиксели в двуизмерна координатна система. Всеки пиксел има точно два условни цвята – "прозрачен" и "запълнен". Паното има ширина и височина.

В координатната система, с която ще работим, ще можем да задаваме координати само с цели числа и за опростяване на заданието, координатите няма да бъдат отрицателни числа. Нулата на координатната система се намира в горния ѝ ляв ъгъл.

Пано с ширина 1 и височина 1 разполага само с един пиксел, чийто координати са съответно 0, 0. Когато говорим за координати, ще приемаме, че първата координата, да я кръстим x, е координата по хоризонтала (абсциса), а втората координата – y – ще е по вертикала (ордината).

Класът, моделиращ пано в библиотеката ни, ще се казва Canvas. Той трябва да предоставя следния интерфейс:

  • Конструктор, приемащ два задължителни позиционни аргумента – ширина и височина, цели числа.
  • Методи width и height, връщащи съответните числови стойности.
  • Метод set_pixel(x, y), позволяващ да укажем, че пикселът, идентифициран от координатите x и y е "запълнен".
  • Предикат pixel_at?(x, y), връщащ истина, ако пикселът в съответната точка е "запълнен" и лъжа, ако не е.
  • Метод draw, приемащ един аргумент – фигура. Това води до "изчертаване" на съответната фигура върху паното. Вижте примера в края на условието за това как трябва да можем да извикваме този метод.
  • Метод render_as, приемащ един аргумент – "renderer" и връщащ "резултата" от операцията по рендериране. По-надолу в условието ще има повече детайли за този метод.

Рендериране

Искаме да можем да изобразяваме паната по някакъв начин. За целта ще въведем концепцията "renderer" (ще ползваме и "рендерер") и ще дефинираме два такива. И двата ще се намират в именуваното пространство Graphics::Renderers.

ASCII-текст

Името на този рендерер е Graphics::Renderers::Ascii. Няма специфични ограничения какво трябва да стои зад тази константа. Достатъчно е когато тя се подаде на метода render_as на пано, да се визуализира съответното пано, използвайки следните правила:

  • Връща толкова реда текст, колкото е височината на паното. За пано, високо 10 пиксела, трбява да върне 10 реда текст. Редовете текст са разделени със символ за нов ред \n. След последния ред не трябва да има такъв символ.
  • Всички редове са с еднаква дължина, която съответства на ширината на паното. Например, за пано с ширина 20, всеки ред трябва да има точно 20 символа, съответстващи на пикселите на паното.
  • На всеки "запълнен" пиксел съотетства символът @, а на останалите символът -.

Ето пример за използване на този рендерер:

canvas = Graphics::Canvas.new 5, 5

canvas.set_pixel 0, 0
canvas.set_pixel 1, 1
canvas.set_pixel 2, 2

puts canvas.render_as(Graphics::Renderers::Ascii)

Кодът по-горе трябва да изведе на екрана следното:

@----
-@---
--@--
-----
-----

Не забравяйте, че координата 0, 0 съответства на горния ляв ъгъл на паното.

HTML

Името на този рендерер е Graphics::Renderers::Html. Той наподобява много Ascii. Разликата е, че резултатът от рендериране с него продуцира HTML код, който може да бъде визуализиран в браузър. Подчинява се на следните правила:

  • "Запълнени" пиксели се заместват с <b></b>, а прозрачни такива с <i></i>.
  • Редовете от паното се разделят с <br>.
  • Резултат от него трябва да започва и завършва с малко HTML, за да се получи оформлението.

    Всеки резултат трябва да започва със следния HTML код:

      <!DOCTYPE html>
      <html>
      <head>
        <title>Rendered Canvas</title>
        <style type="text/css">
          .canvas {
            font-size: 1px;
            line-height: 1px;
          }
          .canvas * {
            display: inline-block;
            width: 10px;
            height: 10px;
            border-radius: 5px;
          }
          .canvas i {
            background-color: #eee;
          }
          .canvas b {
            background-color: #333;
          }
        </style>
      </head>
      <body>
        <div class="canvas">
    

    И трябва да завършва с този код:

        </div>
      </body>
      </html>
    
  • Когато проверяваме резултата от този рендерер, ще игнорираме whitespace-а, който връщате, така че може да аранжирате HTML кода си както желаете.

Нека рендерираме същото пано като показаното в примера за Ascii, но с този рендерер:

puts canvas.render_as(Graphics::Renderers::Html)

Тогава трябва да получим следния резултат:

<!DOCTYPE html>
<html>
<head>
  <title>Rendered Canvas</title>
  <style type="text/css">
    .canvas {
      font-size: 1px;
      line-height: 1px;
    }
    .canvas * {
      display: inline-block;
      width: 10px;
      height: 10px;
      border-radius: 5px;
    }
    .canvas i {
      background-color: #eee;
    }
    .canvas b {
      background-color: #333;
    }
  </style>
</head>
<body>
  <div class="canvas">
    <b></b><i></i><i></i><i></i><i></i><br>
    <i></i><b></b><i></i><i></i><i></i><br>
    <i></i><i></i><b></b><i></i><i></i><br>
    <i></i><i></i><i></i><i></i><i></i><br>
    <i></i><i></i><i></i><i></i><i></i>
  </div>
</body>
</html>

Отново обръщаме внимание, че подредбата на кода по отношение на whitespace и идентация е само за прегледност в примера и е без значение.

Ако отворите горния код в браузър, би трябвало да видите нещо такова:

Small HTML rendering

Автентичен Пикасо.

Геометрични фигури

Ще дефинираме три геометрични фигури – точка, линия и правоъгълник.

Равенство

Ще искаме методите == и eql? да работят по смислен начин за всеки от трите вида геометрични обекти. Два различни обекта точки, например, имащи еднакви координати, трябва да се смятат за равни. В нашия контекст е допустимо двата метода да са синоними. За всеки геометричен обект ще бъде уточнено допълнително кога две инстанции се считат за равни.

Освен това, ще искаме да дефинирате метод hash на всеки обект по такъв начин, че два равни обекта да имат две еднакви стойности на hash, а два различни – съответно различни. За имплементацията на hash е допустимо и да стъпите на hash методите на класове от Ruby.

Тези две правила се комбинират добре с нашето желание да държим обектите си непроменяеми (immutable).

Точка

Най-простият геометричен обект в нашата система е представен от класа Point, който:

  • Има конструктор, приемащ два аргумента – x и y, съответстващи на координатите на точката.
  • Има методи x и y, връщащо съответните координати на точката.

Точката се представя на пано като един пиксел с координати x и y.

Две точки се считат за еднакви, ако координатите им са равни.

Линия

Една линия се представя от двете ѝ крайни точки, които ние ще наричаме from и to. Тези точки са обекти от тип Point. Линиите нямат посока. Представляват се от класа Line, който:

  • Има конструктор, приемащ два аргумента – точки, представляващи двата края на линията.
  • Метод from, връщащ лявата крайна точка на линията. Ако линията е вертикална, този метод трябва да върне горната крайна точка.
  • Метод to, връщащ дясната крайна точка на линията. За вертикални линии, този метод връща съответно долната крайна точка на линията.

Очевидно, две линии се считат за равни, ако крайните им точки са еднакви. Не забравяйте, че линиите нямат посока.

Растеризирането на линия на пано не е съвсем тривиална задача. Свободни сте да използвате какъвто алгоритъм желаете за целта. Един възможен алгоритъм е Брезенхам. Нашето решение използва него, затова ви го препоръчваме.

Ако двата края на линията са една и съща точка, линията трябва да се изчертае като един пиксел.

Правоъгълник

Последната фигура, която ще има представяне в нашата библиотека, е правоъгълникът, криещ се зад класа Rectangle. Той има следния публичен интерфейс:

  • Конструктор, приемащ две точки, инстанции на Point. Правоъгълник може да бъде начертан между произволни две точки.
  • Метод left, връщащ лявата (или горната) точка, по аналогия на метода Line#from.
  • Метод right, връщащ дясната (или долната) точка, по аналогия на метода Line#to.
  • Методи top_left, top_right, bottom_left и bottom_right, връщащи точките на горния ляв, горния десен, долния ляв и долния десен връх на правоъгълника, съответно.

Очевидно е, че два правоъгълника ще считаме за равни, ако имат еднакви върхове, без значение с кои два от тях са дефинирани.

Правоъгълникът се изчертава като четири линии и не е запълнен. Той може да има и нулева височина или нулева ширина. Тогава се изчертава като линия. Ако пък двете му точки са еднакви, трябва да се изчертае като един пиксел.

Пример

Ето един малко по-пълен пример, демонстриращ работа с всички обекти в нашата мини графична библиотека:

module Graphics
  canvas = Canvas.new 30, 30

  # Door frame and window
  canvas.draw Rectangle.new(Point.new(3, 3), Point.new(18, 12))
  canvas.draw Rectangle.new(Point.new(1, 1), Point.new(20, 28))

  # Door knob
  canvas.draw Line.new(Point.new(4, 15), Point.new(7, 15))
  canvas.draw Point.new(4, 16)

  # Big "R"
  canvas.draw Line.new(Point.new(8, 5), Point.new(8, 10))
  canvas.draw Line.new(Point.new(9, 5), Point.new(12, 5))
  canvas.draw Line.new(Point.new(9, 7), Point.new(12, 7))
  canvas.draw Point.new(13, 6)
  canvas.draw Line.new(Point.new(12, 8), Point.new(13, 10))

  puts canvas.render_as(Renderers::Ascii)
end

Така полученото пано ще изглежда по следния начин след рендериране като Html и визуализиране в браузър:

Html rendering

А в терминала трябва да видим следния изход:

------------------------------
-@@@@@@@@@@@@@@@@@@@@---------
-@------------------@---------
-@-@@@@@@@@@@@@@@@@-@---------
-@-@--------------@-@---------
-@-@----@@@@@-----@-@---------
-@-@----@----@----@-@---------
-@-@----@@@@@-----@-@---------
-@-@----@---@-----@-@---------
-@-@----@----@----@-@---------
-@-@----@----@----@-@---------
-@-@--------------@-@---------
-@-@@@@@@@@@@@@@@@@-@---------
-@------------------@---------
-@------------------@---------
-@--@@@@------------@---------
-@--@---------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@------------------@---------
-@@@@@@@@@@@@@@@@@@@@---------
------------------------------

Бележки

Смятайте, че ще подаваме само неотрицателни цели числа за координати там, където се очакват такива. Не е нужно да проверявате типовете на подаваните ви аргументи за коректност.

Когато в условието се говори за точка или връх, се има предвид обект от тип Point. Аналогично за линия и правоъгълник.

Ограничения

Тази задача има следните ограничения:

  • Най-много 90 символа на ред
  • Най-много 6 реда на метод
  • Най-много 2 нива на влагане

Ако искате да проверите дали задачата ви спазва ограниченията, следвайте инструкциите в описанието на хранилището за домашните.

Няма да приемаме решения, които не спазват ограниченията. Изпълнявайте rubocop редовно, докато пишете кода. Ако смятате, че rubocop греши по някакъв начин, пишете ни на fmi@ruby.bg, заедно с прикачен код или линк към такъв като private gist. Ако пуснете кода си публично (например във форумите), ще смятаме това за преписване.