diff --git a/demos/image.json b/demos/image.json new file mode 100644 index 0000000..b9194fd --- /dev/null +++ b/demos/image.json @@ -0,0 +1,30 @@ +{ + "displays": { + "default": { + "sType": "DoubleVerticalRGB", + "iWidth": 100, + "iHeight": 100, + "iMaxFPS": 30 + } + }, + "routines": { + "image": { + "sType": "RGBImage", + "iPriority": 0, + "aParameters": { + "sPath": "images/slipped_disc_1.ppm" + } + } + }, + "events": [ + { + "at": 0.0, + "on": "routine/image", + "do": "enable" + }, + { + "at": 0.1, + "do": "end" + } + ] +} diff --git a/demos/images/slipped_disc_1.ppm b/demos/images/slipped_disc_1.ppm new file mode 100644 index 0000000..a43cff8 Binary files /dev/null and b/demos/images/slipped_disc_1.ppm differ diff --git a/src/classmap.php b/src/classmap.php index a35e702..598ba30 100644 --- a/src/classmap.php +++ b/src/classmap.php @@ -23,9 +23,12 @@ 'ABadCafe\\PDE\\Routine\\SimpleLine' => '/routine/SimpleLine.php', 'ABadCafe\\PDE\\Routine\\RGBPulse' => '/routine/RGBPulse.php', 'ABadCafe\\PDE\\Routine\\Toroid' => '/routine/Toroid.php', + 'ABadCafe\\PDE\\Routine\\RGBImage' => '/routine/RGBImage.php', 'ABadCafe\\PDE\\Routine\\Factory' => '/routine/Factory.php', 'ABadCafe\\PDE\\Routine\\RGBPersistence' => '/routine/RGBPersistence.php', 'ABadCafe\\PDE\\Routine\\StaticNoise' => '/routine/StaticNoise.php', + 'ABadCafe\\PDE\\Routine\\IResourceLoader' => '/routine/common/IResourceLoader.php', + 'ABadCafe\\PDE\\Routine\\TResourceLoader' => '/routine/common/TResourceLoader.php', 'ABadCafe\\PDE\\Routine\\Base' => '/routine/common/Base.php', 'ABadCafe\\PDE\\System\\ILoader' => '/system/ILoader.php', 'ABadCafe\\PDE\\System\\IRateLimiter' => '/system/IRateLimiter.php', diff --git a/src/display/DoubleVerticalRGB.php b/src/display/DoubleVerticalRGB.php index 5de43d5..cd83a15 100644 --- a/src/display/DoubleVerticalRGB.php +++ b/src/display/DoubleVerticalRGB.php @@ -92,6 +92,33 @@ private function subprocessRenderLoop() { $iExpectSize = $this->iWidth * $this->iHeight * 4; $sTemplate = IANSIControl::ATTR_BG_RGB_TPL; $iShortReads = 0; + + $aTemplates = [ + // Everything changed + 0 => IANSIControl::ATTR_FG_RGB_TPL . IANSIControl::ATTR_BG_RGB_TPL . ICustomChars::MAP[0x80], + + // Foreground and Background are equal but changed + 1 => IANSIControl::ATTR_BG_RGB_TPL . ' ', + + // Foreground and Background unequal, foreground unchanged + 2 => IANSIControl::ATTR_BG_RGB_TPL . ICustomChars::MAP[0x80], + + // Foreground and Background equal, foreground unchanged + 3 => IANSIControl::ATTR_BG_RGB_TPL . ' ', + + // Foreground and Background unequal, background unchanged + 4 => IANSIControl::ATTR_FG_RGB_TPL . ICustomChars::MAP[0x80], + + // Foreground and Backgrounc equal, foreground unchanged + 5 => IANSIControl::ATTR_BG_RGB_TPL . ' ', + + // Foreground and Background unequal, unchanged + 6 => ICustomChars::MAP[0x80], + + // Foreground and background unequal, unchanged + 7 => ' ' + ]; + while (($sInput = $this->receivePixelData($iExpectSize))) { $iGotSize = strlen($sInput); @@ -108,23 +135,56 @@ private function subprocessRenderLoop() { $iEvenOffset = 0; $iOddOffset = $this->iWidth; - // Todo optimise for cases where either value is unchanged - $sTemplate = IANSIControl::ATTR_FG_RGB_TPL . IANSIControl::ATTR_BG_RGB_TPL . ICustomChars::MAP[0x80]; + $iLastBackRGB = 0; + $iLastForeRGB = 0; for ($iRow = 0; $iRow < $this->iHeight; $iRow += 2) { $i = $this->iWidth; while ($i--) { - $iForeRGB = $aPixels[$iEvenOffset++]; - $iBackRGB = $aPixels[$iOddOffset++]; - $sRawBuffer .= sprintf( - $sTemplate, - $iForeRGB >> 16, - ($iForeRGB >> 8) & 0xFF, - ($iForeRGB & 0xFF), - $iBackRGB >> 16, - ($iBackRGB >> 8) & 0xFF, - ($iBackRGB & 0xFF) - ); + $iForeRGB = $aPixels[$iEvenOffset++]; + $iBackRGB = $aPixels[$iOddOffset++]; + $iCase = (int)($iForeRGB == $iBackRGB) | (int)($iForeRGB == $iLastForeRGB) << 1 | (int)($iBackRGB == $iLastBackRGB) << 2; + $sTemplate = $aTemplates[$iCase]; + switch ($iCase) { + case 1: + //case 2: //TODO - why does this glitch? + case 3: + case 5: + $sRawBuffer .= sprintf( + $sTemplate, + $iBackRGB >> 16, + ($iBackRGB >> 8) & 0xFF, + ($iBackRGB & 0xFF) + ); + break; + case 4: + $sRawBuffer .= sprintf( + $sTemplate, + $iForeRGB >> 16, + ($iForeRGB >> 8) & 0xFF, + ($iForeRGB & 0xFF) + ); + break; + case 6: + case 7: + $sRawBuffer .= $sTemplate; + break; + case 0: + default: + $sRawBuffer .= sprintf( + $aTemplates[0], + $iForeRGB >> 16, + ($iForeRGB >> 8) & 0xFF, + ($iForeRGB & 0xFF), + $iBackRGB >> 16, + ($iBackRGB >> 8) & 0xFF, + ($iBackRGB & 0xFF) + ); + + } + $iLastForeRGB = $iForeRGB; + $iLastBackRGB = $iBackRGB; + } $iEvenOffset += $this->iWidth; $iOddOffset += $this->iWidth; diff --git a/src/routine/Factory.php b/src/routine/Factory.php index 5fd7989..c7820a8 100644 --- a/src/routine/Factory.php +++ b/src/routine/Factory.php @@ -36,6 +36,7 @@ class Factory { 'Toroid' => Toroid::class, 'RGBPulse' => RGBPulse::class, 'RGBPersistence' => RGBPersistence::class, + 'RGBImage' => RGBImage::class, ]; private static ?self $oInstance = null; diff --git a/src/routine/RGBImage.php b/src/routine/RGBImage.php new file mode 100644 index 0000000..7665a7f --- /dev/null +++ b/src/routine/RGBImage.php @@ -0,0 +1,103 @@ + 'required' + ]; + + public function preload() : self { + $this->loadPNM($this->oParameters->sPath); + return $this; + } + + /** + * @inheritDoc + */ + public function setDisplay(PDE\IDisplay $oDisplay) : self { + $this->bCanRender = ($oDisplay instanceof PDE\Display\IPixelled); + $this->oDisplay = $oDisplay; + $this->iViewWidth = $oDisplay->getWidth(); + $this->iViewHeight = $oDisplay->getHeight(); + return $this; + } + + /** + * @inheritDoc + */ + public function render(int $iFrameNumber, float $fTimeIndex) : self { + if ($this->canRender($iFrameNumber, $fTimeIndex)) { + $oBuffer = $this->oDisplay->getPixelBuffer(); + if ($this->iWidth == $this->iViewWidth && $this->iHeight == $this->iViewHeight) { + foreach ($oBuffer as $i => $iBufferRGB) { + $oBuffer[$i] = $this->oPixels[$i]; + } + } + } + return $this; + } + + /** + * @inheritDoc + */ + protected function parameterChange() { + } + + /** + * Load a PNM image + */ + protected function loadPNM(string $sPath) { + $sRaw = $this->loadFile($sPath); + if (preg_match('/^(\d+)\s+(\d+)$/m', $sRaw, $aMatches)) { + $this->iWidth = (int)$aMatches[1]; + $this->iHeight = (int)$aMatches[2]; + $iArea = $this->iWidth * $this->iHeight; + $this->oPixels = new SPLFixedArray($iArea); + $sData = substr($sRaw, ($iArea * -3)); + $iDataOffset = 0; + for ($i = 0; $i < $iArea; ++$i) { + $this->oPixels[$i] = + (ord($sData[$iDataOffset++]) << 16) | + (ord($sData[$iDataOffset++]) << 8) | + (ord($sData[$iDataOffset++])); + } + } else { + throw new \Exception('Invalid PNM Format'); + } + } + +} diff --git a/src/routine/common/IResourceLoader.php b/src/routine/common/IResourceLoader.php new file mode 100644 index 0000000..fdf1e1e --- /dev/null +++ b/src/routine/common/IResourceLoader.php @@ -0,0 +1,46 @@ +sBasePath = $sBasePath; + return $this; + } + + /** + * Load a file. + * + * @param string $sRelativePath + * @return string + * @throws \Exception + */ + private function loadFile(string $sRelativePath) : string { + $sPath = $this->sBasePath . $sRelativePath; + if (file_exists($sPath) && is_readable($sPath)) { + return file_get_contents($sPath); + } + throw new \Exception($sPath . ' could not be read'); + } +} diff --git a/src/system/Context.php b/src/system/Context.php index 94e34e1..fb92353 100644 --- a/src/system/Context.php +++ b/src/system/Context.php @@ -71,7 +71,7 @@ class Context { */ public function __construct(ILoader $oLoader) { $this->initialiseDisplays($oLoader->getDisplays()); - $this->initialiseRoutines($oLoader->getRoutines()); + $this->initialiseRoutines($oLoader->getRoutines(), $oLoader->getBasePath()); $this->initialiseTimeline($oLoader->getEvents()); } @@ -120,18 +120,23 @@ private function initialiseDisplays(array $aDisplayDefinitions) { * * @param Definition\Routine[] $aRoutineDefinitions */ - private function initialiseRoutines(array $aRoutineDefinitions) { + private function initialiseRoutines(array $aRoutineDefinitions, string $sBasePath) { $oRoutineFactory = PDE\Routine\Factory::get(); foreach ($aRoutineDefinitions as $sIdentity => $oRoutineDefinition) { $sIdentity = self::NS_ROUTINE . $sIdentity; if (isset($this->aRoutineInstances[$sIdentity])) { throw new \Exception('Duplicate routine identity ' . $sIdentity); } - $this->aRoutineInstances[$sIdentity] = $oRoutineFactory->create( + $this->aRoutineInstances[$sIdentity] = $oRoutine = $oRoutineFactory->create( $oRoutineDefinition->sType, $this->oDisplay, $oRoutineDefinition->aParameters ); + if ($oRoutine instanceof PDE\Routine\IResourceLoader) { + $oRoutine + ->setBasePath($sBasePath) + ->preload(); + } $this->aRoutinePriorities[$sIdentity] = $oRoutineDefinition->iPriority; } asort($this->aRoutinePriorities, SORT_NUMERIC); diff --git a/src/system/ILoader.php b/src/system/ILoader.php index 0a1e12b..7513709 100644 --- a/src/system/ILoader.php +++ b/src/system/ILoader.php @@ -34,6 +34,13 @@ interface ILoader { */ public function __construct(string $sFilePath); + /** + * Obtain the base path, i.e. the directory in which the demo file is located. + * + * @return string + */ + public function getBasePath() : string; + /** * Return an associative array of the Display definitions in file. * diff --git a/src/system/loader/JSON.php b/src/system/loader/JSON.php index ba3de44..fb53d62 100644 --- a/src/system/loader/JSON.php +++ b/src/system/loader/JSON.php @@ -42,6 +42,8 @@ class JSON implements System\ILoader { */ private array $aEvents; + private string $sBasePath; + /** * @inheritDoc */ @@ -54,6 +56,8 @@ public function __construct(string $sFilePath) { throw new \Exception('Unable to parse ' . $sFilePath . ', invalid JSON?'); } + $this->sBasePath = dirname($sFilePath) . '/'; + if ( !isset($oDocument->displays) || !is_object($oDocument->displays) || @@ -87,6 +91,13 @@ public function __construct(string $sFilePath) { } } + /** + * @inheritDoc + */ + public function getBasePath() : string { + return $this->sBasePath; + } + /** * @inheritDoc */