implemented Canvas and FileWriter for PPM
This commit is contained in:
parent
dda4ba31e0
commit
1a3e0d31db
4 changed files with 364 additions and 0 deletions
85
src/Canvas/Canvas.php
Normal file
85
src/Canvas/Canvas.php
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ItsAMirko\RayTracer\Canvas;
|
||||||
|
|
||||||
|
use ItsAMirko\RayTracer\Primitives\Color;
|
||||||
|
use OutOfBoundsException;
|
||||||
|
|
||||||
|
class Canvas
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Color[][]
|
||||||
|
*/
|
||||||
|
private $pixels;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canvas constructor.
|
||||||
|
*
|
||||||
|
* @param int $width
|
||||||
|
* @param int $height
|
||||||
|
*/
|
||||||
|
public function __construct(int $width, int $height)
|
||||||
|
{
|
||||||
|
$this->width = $width;
|
||||||
|
$this->height = $height;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function width(): int
|
||||||
|
{
|
||||||
|
return $this->width;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function height(): int
|
||||||
|
{
|
||||||
|
return $this->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Color[][]
|
||||||
|
*/
|
||||||
|
public function pixels(): array
|
||||||
|
{
|
||||||
|
return $this->pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $x
|
||||||
|
* @param int $y
|
||||||
|
* @param Color $color
|
||||||
|
*/
|
||||||
|
public function addPixel(int $x, int $y, Color $color): void
|
||||||
|
{
|
||||||
|
if ($x < 0 || $x >= $this->width) {
|
||||||
|
throw new OutOfBoundsException('Invalid position for x provided. Expected integer between 0 and '
|
||||||
|
. ($this->width - 1) . ', but got ' . $x);
|
||||||
|
} elseif ($y < 0 || $y >= $this->height) {
|
||||||
|
throw new OutOfBoundsException('Invalid position for y provided. Expected integer between 0 and '
|
||||||
|
. ($this->height - 1) . ', but got ' . $y);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pixels[$x][$y] = $color;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/Canvas/CanvasPpmFileWriter.php
Normal file
99
src/Canvas/CanvasPpmFileWriter.php
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ItsAMirko\RayTracer\Canvas;
|
||||||
|
|
||||||
|
use League\Flysystem\FilesystemInterface;
|
||||||
|
use function strlen;
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CanvasPpmFileWriter
|
||||||
|
*
|
||||||
|
* @package ItsAMirko\RayTracer\Canvas
|
||||||
|
*/
|
||||||
|
class CanvasPpmFileWriter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Defines the maximal number of chars per line in the file.
|
||||||
|
*/
|
||||||
|
private const MAX_CHARS_PER_LINE = 70;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var FilesystemInterface
|
||||||
|
*/
|
||||||
|
private $filesystem;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CanvasFileWriter constructor.
|
||||||
|
*
|
||||||
|
* @param FilesystemInterface $filesystem
|
||||||
|
*/
|
||||||
|
public function __construct(FilesystemInterface $filesystem)
|
||||||
|
{
|
||||||
|
$this->filesystem = $filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Canvas $canvas
|
||||||
|
* @param string $filename
|
||||||
|
*/
|
||||||
|
public function createFile(Canvas $canvas, string $filename): void
|
||||||
|
{
|
||||||
|
$content = $this->getFileHeader($canvas);
|
||||||
|
$content .= $this->getImageContent($canvas);
|
||||||
|
|
||||||
|
$this->filesystem->put($filename, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Canvas $canvas
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getFileHeader(Canvas $canvas): string
|
||||||
|
{
|
||||||
|
return 'P3' . PHP_EOL . $canvas->width() . ' ' . $canvas->height() . PHP_EOL . '255' . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Canvas $canvas
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getImageContent(Canvas $canvas): string
|
||||||
|
{
|
||||||
|
$content = '';
|
||||||
|
$pixels = $canvas->pixels();
|
||||||
|
|
||||||
|
for ($y = 0; $y < $canvas->height(); $y++) {
|
||||||
|
$rowContent = '';
|
||||||
|
for ($x = 0; $x < $canvas->width(); $x++) {
|
||||||
|
foreach (['red', 'green', 'blue'] as $color) {
|
||||||
|
$colorHex = '0';
|
||||||
|
if (isset($pixels[$x][$y]) && $color === 'red') {
|
||||||
|
$colorHex = $pixels[$x][$y]->redAsHex();
|
||||||
|
} elseif (isset($pixels[$x][$y]) && $color === 'green') {
|
||||||
|
$colorHex = $pixels[$x][$y]->greenAsHex();
|
||||||
|
} elseif (isset($pixels[$x][$y]) && $color === 'blue') {
|
||||||
|
$colorHex = $pixels[$x][$y]->blueAsHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($rowContent . $colorHex) > self::MAX_CHARS_PER_LINE) {
|
||||||
|
$content .= trim($rowContent) . PHP_EOL;
|
||||||
|
$rowContent = '';
|
||||||
|
}
|
||||||
|
$rowContent .= $colorHex . ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content .= trim($rowContent) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
122
tests/Canvas/CanvasPpmFileWriterTest.php
Normal file
122
tests/Canvas/CanvasPpmFileWriterTest.php
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ItsAMirko\RayTracer\Canvas;
|
||||||
|
|
||||||
|
use ItsAMirko\RayTracer\Primitives\Color;
|
||||||
|
use League\Flysystem\FilesystemInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use function explode;
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
|
class CanvasPpmFileWriterTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var FilesystemInterface
|
||||||
|
*/
|
||||||
|
private $filesystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var CanvasPpmFileWriter
|
||||||
|
*/
|
||||||
|
private $fileWriter;
|
||||||
|
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->filesystem = $this->createMock(FilesystemInterface::class);
|
||||||
|
|
||||||
|
$this->fileWriter = new CanvasPpmFileWriter($this->filesystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testWritesPPMHeader(): void
|
||||||
|
{
|
||||||
|
$expectedFilename = 'my_canvas.ppm';
|
||||||
|
$expectedContentLines = [
|
||||||
|
0 => 'P3',
|
||||||
|
1 => '5 3',
|
||||||
|
2 => '255',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->filesystem->expects($this->once())
|
||||||
|
->method('put')
|
||||||
|
->with($expectedFilename, $this->checkFileContentCallback($expectedContentLines));
|
||||||
|
|
||||||
|
$canvas = new Canvas(5, 3);
|
||||||
|
$this->fileWriter->createFile($canvas, $expectedFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testWritesPixels(): void
|
||||||
|
{
|
||||||
|
$expectedFilename = 'my_canvas.ppm';
|
||||||
|
$expectedContentLines = [
|
||||||
|
3 => '255 0 0 0 0 0 0 0 0 0 0 0 0 0 0',
|
||||||
|
4 => '0 0 0 0 0 0 0 128 0 0 0 0 0 0 0',
|
||||||
|
5 => '0 0 0 0 0 0 0 0 0 0 0 0 0 0 255',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->filesystem->expects($this->once())
|
||||||
|
->method('put')
|
||||||
|
->with($expectedFilename, $this->checkFileContentCallback($expectedContentLines));
|
||||||
|
|
||||||
|
$canvas = new Canvas(5, 3);
|
||||||
|
$canvas->addPixel(0, 0, new Color(1.5, 0.0, 0.0));
|
||||||
|
$canvas->addPixel(2, 1, new Color(0.0, 0.5, 0.0));
|
||||||
|
$canvas->addPixel(4, 2, new Color(-0.5, 0.0, 1.0));
|
||||||
|
|
||||||
|
$this->fileWriter->createFile($canvas, $expectedFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testSplitsLongLinesOfPixels(): void
|
||||||
|
{
|
||||||
|
$expectedFilename = 'my_canvas.ppm';
|
||||||
|
$expectedContentLines = [
|
||||||
|
3 => '255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204',
|
||||||
|
4 => '153 255 204 153 255 204 153 255 204 153 255 204 153',
|
||||||
|
5 => '255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204',
|
||||||
|
6 => '153 255 204 153 255 204 153 255 204 153 255 204 153',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->filesystem->expects($this->once())
|
||||||
|
->method('put')
|
||||||
|
->with($expectedFilename, $this->checkFileContentCallback($expectedContentLines));
|
||||||
|
|
||||||
|
$canvas = new Canvas(10, 2);
|
||||||
|
for ($i = 0; $i < 10; $i++) {
|
||||||
|
$canvas->addPixel($i, 0, new Color(1, 0.8, 0.6));
|
||||||
|
$canvas->addPixel($i, 1, new Color(1, 0.8, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fileWriter->createFile($canvas, $expectedFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a callback to check the content of the PPM file
|
||||||
|
*
|
||||||
|
* @param array $expectedContentLines
|
||||||
|
*
|
||||||
|
* @return callable
|
||||||
|
*/
|
||||||
|
private function checkFileContentCallback(array $expectedContentLines)
|
||||||
|
{
|
||||||
|
return $this->callback(function (
|
||||||
|
string $content
|
||||||
|
) use ($expectedContentLines): bool {
|
||||||
|
$actualContentLines = explode(PHP_EOL, $content);
|
||||||
|
foreach ($expectedContentLines as $line => $expectedContent) {
|
||||||
|
if ($expectedContent !== $actualContentLines[$line]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
58
tests/Canvas/CanvasTest.php
Normal file
58
tests/Canvas/CanvasTest.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ItsAMirko\RayTracer\Canvas;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use ItsAMirko\RayTracer\Primitives\Color;
|
||||||
|
use OutOfBoundsException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use function random_int;
|
||||||
|
|
||||||
|
class CanvasTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testProvidesDimensions(): void
|
||||||
|
{
|
||||||
|
$width = random_int(1, 100);
|
||||||
|
$height = random_int(1, 100);
|
||||||
|
|
||||||
|
$canvas = new Canvas($width, $height);
|
||||||
|
|
||||||
|
$this->assertSame($width, $canvas->width());
|
||||||
|
$this->assertSame($height, $canvas->height());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testProvidesItsPixels(): void
|
||||||
|
{
|
||||||
|
$canvas = new Canvas(2, 2);
|
||||||
|
$canvas->addPixel(0, 0, new Color(1.0, 0.0, 0.0));
|
||||||
|
$canvas->addPixel(0, 1, new Color(0.0, 1.0, 0.0));
|
||||||
|
$canvas->addPixel(1, 1, new Color(0.0, 0.0, 1.0));
|
||||||
|
|
||||||
|
$expectedPixels = [
|
||||||
|
0 => [
|
||||||
|
0 => new Color(1.0, 0.0, 0.0),
|
||||||
|
1 => new Color(0.0, 1.0, 0.0),
|
||||||
|
],
|
||||||
|
1 => [
|
||||||
|
1 => new Color(0.0, 0.0, 1.0),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expectedPixels, $canvas->pixels());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testThrowsExceptionIfPixelToAddIsOutOfBounds(): void
|
||||||
|
{
|
||||||
|
$this->expectException(OutOfBoundsException::class);
|
||||||
|
|
||||||
|
$canvas = new Canvas(1, 1);
|
||||||
|
$canvas->addPixel(1, 1, new Color(1.0, 0.0, 0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue