pokesprite/includes/growingpacker.php
2016-12-11 01:19:09 +01:00

206 lines
6.3 KiB
PHP

<?php
// PokéSprite
// ----------
// The use of this source code is governed by the MIT license.
// See the COPYRIGHT file for more information.
namespace PkSpr;
/**
* Growing Bin Packer algorithm for positioning icons in a spritesheet
* using space as efficiently as possible.
*
* This code is a rewritten version of Jake Gordon's GrowingPacker algorithm.
* See <http://codeincomplete.com/posts/2011/5/7/bin_packing/> for more info.
* The algorithm has been adapted from Javascript to PHP and modified lightly.
*/
class GrowingPacker
{
public $root;
private $permit_growth_w;
private $permit_growth_h;
private $img_padding;
/**
* Fits size sorted blocks using a binary tree packing algorithm.
*
* Note that $blocks needs to be converted to a StdObject temporarily
* as arrays cannot be properly passed by reference. This algorithm
* cannot work in PHP using arrays.
*
* For the best effects, $blocks needs to be sorted by max(width, height),
* descending (with the largest elements appearing first).
* The algorithm fails if a block is larger than the previous one.
*
* If no extra arguments are passed, the initial size is determined from
* the first element, and growth is permitted in both directions.
*
* @param mixed[] $blocks Blocks to fit.
* @param ?int $initial_w Initial width.
* @param ?int $initial_h Initial height.
* @param ?boolean $permit_growth_w Permits the stack to grow in width.
* @param ?boolean $permit_growth_h Permits the stack to grow in height.
*/
public function fit(&$blocks, $initial_w=null, $initial_h=null, $permit_growth_w=true, $permit_growth_h=true)
{
$len = count($blocks);
$first_block = @reset($blocks);
$this->permit_growth_w = $permit_growth_w;
$this->permit_growth_h = $permit_growth_h;
$this->img_padding = Settings::get('pkmn_img_padding');
// Set the initial sizes.
$w = isset($first_block) ? $first_block->w + $this->img_padding : 0;
$h = isset($first_block) ? $first_block->h + $this->img_padding : 0;
$w = isset($initial_w) ? $initial_w : $w;
$h = isset($initial_h) ? $initial_h : $h;
// Create the root object.
$this->root = (object)array(
'x' => 0,
'y' => 0,
'w' => $w,
'h' => $h,
'used' => false,
);
// Iterate over all our blocks and then either split or grow
// nodes on the tree.
foreach ($blocks as &$block) {
$block_w = $block->w + $this->img_padding;
$block_h = $block->h + $this->img_padding;
$node = $this->find_node($this->root, $block_w, $block_h);
if ($node) {
$block->fit = $this->split_node($node, $block_w, $block_h);
}
else {
$block->fit = $this->grow_node($block_w, $block_h);
}
}
}
private function find_node(&$root, $w, $h)
{
if (@$root->used) {
$node = $this->find_node($root->right, $w, $h);
if ($node) {
return $node;
}
$node = $this->find_node($root->down, $w, $h);
if ($node) {
return $node;
}
}
else
if (($w <= $root->w) && ($h <= $root->h)) {
return $root;
}
else {
return null;
}
}
private function split_node(&$node, $w, $h)
{
$node->used = true;
$node->down = (object)array(
'x' => $node->x,
'y' => $node->y + $h,
'w' => $node->w,
'h' => $node->h - $h,
);
$node->right = (object)array(
'x' => $node->x + $w,
'y' => $node->y,
'w' => $node->w - $w,
'h' => $h,
);
return $node;
}
private function grow_node($w, $h)
{
$can_grow_down = ($w <= $this->root->w) && $this->permit_growth_h !== false;
$can_grow_right = ($h <= $this->root->h) && $this->permit_growth_w !== false;
$should_grow_right = $can_grow_right && ($this->root->h >= ($this->root->w + $w));
$should_grow_down = $can_grow_down && ($this->root->w >= ($this->root->h + $h));
if ($should_grow_right) {
return $this->grow_right($w, $h);
}
else
if ($should_grow_down) {
return $this->grow_down($w, $h);
}
else
if ($can_grow_right) {
return $this->grow_right($w, $h);
}
else
if ($can_grow_down) {
return $this->grow_down($w, $h);
}
else {
// Error: couldn't grow. This is likely because the input blocks
// weren't sorted by size. They must be sorted by (preferably)
// max(width, height), with the largest elements appearing first.
return null;
}
}
private function grow($args, $w, $h)
{
$this->root = $args;
$node = $this->find_node($this->root, $w, $h);
if ($node) {
return $this->split_node($node, $w, $h);
} else {
return null;
}
}
private function grow_right($w, $h)
{
$args = (object)array(
'used' => true,
'x' => 0,
'y' => 0,
'w' => $this->root->w + $w,
'h' => $this->root->h,
'down' => $this->root,
'right' => (object)array(
'x' => $this->root->w,
'y' => 0,
'w' => $w,
'h' => $this->root->h,
)
);
return $this->grow($args, $w, $h);
}
private function grow_down($w, $h)
{
$args = (object)array(
'used' => true,
'x' => 0,
'y' => 0,
'w' => $this->root->w,
'h' => $this->root->h + $h,
'down' => $this->root,
'right' => (object)array(
'x' => 0,
'y' => $this->root->h,
'w' => $this->root->w,
'h' => $h,
)
);
return $this->grow($args, $w, $h);
}
}