## ZooUI - Zooming User Interface
## Copyright (C) 2009 David Roberts <d@vidr.cc>
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License
## as published by the Free Software Foundation; either version 3
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <https://www.gnu.org/licenses/>.
"""Dynamic tile provider for Barnsley's fern."""
import math
import random
from typing import TYPE_CHECKING, Any
from PIL import Image
from .dynamictileprovider import DynamicTileProvider
## Performance optimization note:
## Phase 2 optimizations replace 2**x with math.exp2(x) (1.85x faster)
## and math.log(x, 2) with math.log2(x) (2x faster) throughout the codebase.
## These changes are performance-critical for zoom operations.
if TYPE_CHECKING:
from PIL.Image import Image as PILImage
TileID = tuple[str, int, int, int] # type: ignore[misc]
[docs]
class FernTileProvider(DynamicTileProvider):
"""
Constructor :
FernTileProvider(tilecache)
Parameters :
tilecache : TileCache
FernTileProvider(tilecache) --> FernTileProvider
FernTileProvider objects are used for generating tiles of Barnsley's
fern iterated function system.
This dynamic tile provider generates fractal images of Barnsley's fern
using an iterated function system (IFS). The fern is created by randomly
applying one of four affine transformations to points, with each
transformation having a specific probability of being chosen.
Implementation Notes:
- Inherits from DynamicTileProvider
- Uses four affine transformations with different probabilities:
* rachis (stem): 1% probability
* left hand first pinna: 7% probability
* right hand first pinna: 7% probability
* body of fern: 85% probability
- Generates up to max_iterations (50000) points
- Stops after max_points (10000) points are drawn on tile
- Tiles are 256x256 pixels saved as PNG
- Color is RGB (100, 170, 0) for a green fern appearance
"""
def __init__(self, tilecache: Any) -> None:
"""
Constructor :
FernTileProvider(tilecache)
Parameters :
tilecache : Any
FernTileProvider(tilecache) --> None
Create a new FernTileProvider with the given TileCache.
The tilecache parameter is the TileCache instance that this provider
will use to store generated fern fractal tiles.
"""
DynamicTileProvider.__init__(self, tilecache)
filext = "png"
tilesize = 256
aspect_ratio = 1.0
max_iterations = 50000
max_points = 10000
transformations = [
## (probability, (a, b, c, d, e, f))
## x_n+1 = a*x_n + b*y_n + c
## y_n+1 = d*x_n + e*y_n + f
## for details about the transformations, see:
## <http://en.wikipedia.org/wiki/Barnsley's_fern>
## <http://books.google.com/books?id=oh7NoePgmOIC
## &printsec=frontcover#PPA86,M1>
## <http://mathworld.wolfram.com/BarnsleysFern.html>
## <http://www.home.aone.net.au/~byzantium/ferns/fractal.html>
## rachis
(0.01, (0.00, 0.00, 0.00, 0.00, 0.16, 0.00)),
## left hand first pinna
(0.07, (0.20, -0.26, 0.00, 0.23, 0.22, 1.60)),
## right hand first pinna
(0.07, (-0.15, 0.28, 0.00, 0.26, 0.24, 0.44)),
## body of fern
(0.85, (0.85, 0.04, 0.00, -0.04, 0.85, 1.60)),
]
color = (100, 170, 0)
def __choose_transformation(self) -> tuple[float, float, float, float, float, float]:
"""
Method :
FernTileProvider.__choose_transformation()
Parameters :
None
FernTileProvider.__choose_transformation() --> Tuple[float, float, float, float, float, float]
Randomly choose a transformation based on the probability of each
transformation being chosen.
This method implements probabilistic selection from the four fern
transformations. A random value between 0 and 1 is generated, and
transformations are tested in order until one is selected based on
cumulative probability.
Implementation Notes:
- Generates random float in range [0, 1)
- Iterates through transformations in order
- Subtracts each probability from random value
- Returns transformation when random value <= probability
- Returns 6-tuple: (a, b, c, d, e, f) for affine transform
"""
n = random.uniform(0, 1)
chosen_transformation = self.transformations[0][1] # default to first
for probability, transformation in self.transformations:
if n <= probability:
chosen_transformation = transformation
break
else:
n -= probability
return chosen_transformation
def __transform(self, x: float, y: float) -> tuple[float, float]:
"""
Method :
FernTileProvider.__transform(x, y)
Parameters :
x : float
y : float
FernTileProvider.__transform(x, y) --> Tuple[float, float]
Randomly choose a transformation and apply it to x and y, returning
the result as a tuple.
This method applies one iteration of the fern's iterated function system.
It selects a transformation using __choose_transformation() and applies
the affine transformation to the input coordinates.
Implementation Notes:
- Calls __choose_transformation() to get transformation coefficients
- Applies affine transformation: x' = a*x + b*y + c
- Applies affine transformation: y' = d*x + e*y + f
- Returns new coordinates as tuple (x', y')
"""
t = self.__choose_transformation()
x_new = t[0] * x + t[1] * y + t[2]
y_new = t[3] * x + t[4] * y + t[5]
return (x_new, y_new)
def __draw_point(self, tile: "PILImage", x: float, y: float, tilesize_units: float) -> None:
"""
Method :
FernTileProvider.__draw_point(tile, x, y, tilesize_units)
Parameters :
tile : Image
x : float
y : float
tilesize_units : float
FernTileProvider.__draw_point(tile, x, y, tilesize_units) --> None
Draw the given point on the given tile.
Converts fern coordinate space to pixel coordinates and draws a single
pixel on the tile. The y-coordinate is inverted to match image coordinate
system (origin at top-left).
Preconditions:
- 0.0 <= x <= tilesize_units
- 0.0 <= y <= tilesize_units
Implementation Notes:
- Scales x from [0, tilesize_units] to [0, tilesize]
- Clamps x to valid pixel range [0, tilesize-1]
- Scales y from [0, tilesize_units] to [0, tilesize]
- Inverts y-coordinate: pixel_y = tilesize - scaled_y
- Clamps y to valid pixel range [0, tilesize-1]
- Sets pixel color using self.color (green)
"""
x = x * self.tilesize / tilesize_units
x = min(int(x), self.tilesize - 1)
y = y * self.tilesize / tilesize_units
y = min(int(self.tilesize - y), self.tilesize - 1)
tile.putpixel((x, y), self.color)
[docs]
def _load_dynamic(self, tile_id: TileID, outfile: str) -> None:
"""
Method :
FernTileProvider._load_dynamic(tile_id, outfile)
Parameters :
tile_id : Tuple[str, int, int, int]
outfile : str
FernTileProvider._load_dynamic(tile_id, outfile) --> None
Generate a tile of Barnsley's fern and save it to outfile.
This method generates a fractal fern image for the specified tile by
iterating the fern's IFS transformations. The tile coordinates (row, col)
and tilelevel determine which portion of the fern to render. Points are
generated starting from (0, 0) and transformed repeatedly, with points
falling within the tile's boundaries being drawn.
Implementation Notes:
- Returns early if row/col are out of range for the tilelevel
- Calculates tile boundaries in fern coordinate space (-5 to 5 for x, 0 to 10 for y)
- Creates a black RGB image of size tilesize x tilesize
- Iterates up to max_iterations times, starting from origin (0, 0)
- Only draws points that fall within the tile boundaries
- Stops after max_points are drawn to the tile
- Saves the resulting tile as PNG to outfile
"""
_media_id, tilelevel, row, col = tile_id
if row < 0 or col < 0 or row > 2**tilelevel - 1 or col > 2**tilelevel - 1:
## row,col out of range
return
tilesize_units = 10.0 * math.exp2(-tilelevel)
x = col * tilesize_units
y = row * tilesize_units
## the corners of the tile are:
## (x1,y2) +----+ (x2,y2)
## | |
## (x1,y1) +----+ (x2,y1)
x1 = x - 5.0
y2 = 10.0 - y
x2 = x1 + tilesize_units
y1 = y2 - tilesize_units
tile = Image.new("RGB", (self.tilesize, self.tilesize))
num_points = 0
x = 0.0
y = 0.0
for _i in range(self.max_iterations):
if x1 <= x <= x2 and y1 <= y <= y2:
self.__draw_point(tile, x - x1, y - y1, tilesize_units)
num_points += 1
if num_points > self.max_points:
break
x, y = self.__transform(x, y)
tile.save(outfile)