96 lines
4.3 KiB
Python
96 lines
4.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
This module implements a kaeldioscope effect renderer.
|
|
"""
|
|
from __future__ import division
|
|
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
from builtins import range
|
|
from math import sin, cos, pi, atan2
|
|
|
|
from asciimatics.renderers.base import DynamicRenderer
|
|
|
|
|
|
class Kaleidoscope(DynamicRenderer):
|
|
"""
|
|
Renderer to create a 2-mirror kaleidoscope effect.
|
|
|
|
This is a chained renderer (i.e. it acts upon the output of another Renderer which is
|
|
passed to it on construction). The other Renderer is used as the cell that is rotated over
|
|
time to create the animation.
|
|
|
|
You can specify the desired rotational symmetry of the kaleidoscope (which determines the
|
|
angle between the mirrors). If you chose values of less than 2, you are effectively removing
|
|
one or both mirrors, thus either getting the original cell or a simple mirrored image of the
|
|
cell.
|
|
|
|
Since this renderer rotates the background cell, it needs operate on square pixels, which
|
|
means each character in the cell is drawn as 2 next to each other on the screen. In other
|
|
words the cell needs to be half the width of the desired output (when measured in text
|
|
characters).
|
|
"""
|
|
|
|
def __init__(self, height, width, cell, symmetry):
|
|
"""
|
|
:param height: Height of the box to contain the kaleidoscope.
|
|
:param width: Width of the box to contain the kaleidoscope.
|
|
:param cell: A Renderer to use as the backing cell for the kaleidoscope.
|
|
:param symmetry: The desired rotational symmetry. Must be a non-negative integer.
|
|
"""
|
|
super(Kaleidoscope, self).__init__(height, width)
|
|
self._symmetry = symmetry
|
|
self._rotation = 0
|
|
self._cell = cell
|
|
|
|
def _render_now(self):
|
|
# Rotate a point (x, y) through an angle theta.
|
|
def _rotate(x, y, theta):
|
|
return x * cos(theta) - y * sin(theta), x * sin(theta) + y * cos(theta)
|
|
|
|
# Reflect a point (x, y) in a line at angle theta
|
|
def _reflect(x, y, theta):
|
|
return x * cos(2 * theta) + y * sin(2 * theta), x * sin(2 * theta) - y * cos(2 * theta)
|
|
|
|
# Get the base cell now - so we can pick out characters as needed.
|
|
text, colour_map = self._cell.rendered_text
|
|
|
|
# Integer maths will result in gaps between characters if you rotate from the starting
|
|
# point to desired end-point. We therefore look for the reverse mapping from the final
|
|
# character and trace-back instead.
|
|
for dx in range(self._canvas.width // 2):
|
|
for dy in range(self._canvas.height):
|
|
# Figure out which segment of the circle we're in, so we know what affine
|
|
# transformations to apply.
|
|
ox = (dx - self._canvas.width / 4)
|
|
oy = dy - self._canvas.height / 2
|
|
segment = round(atan2(oy, ox) * self._symmetry / pi)
|
|
if segment % 2 == 0:
|
|
# Just a rotation required for even segments.
|
|
x1, y1 = _rotate(
|
|
ox, oy, 0 if self._symmetry == 0 else -segment * pi / self._symmetry)
|
|
else:
|
|
# Odd segments require a rotation and then a reflection.
|
|
x1, y1 = _rotate(ox, oy, (1 - segment) * pi / self._symmetry)
|
|
x1, y1 = _reflect(x1, y1, pi / self._symmetry / 2)
|
|
|
|
# Now rotate once more to simulate the rotation of the background cell too.
|
|
x1, y1 = _rotate(x1, y1, self._rotation)
|
|
|
|
# Re-normalize back to the box coordinates and draw the character that we found
|
|
# from the reverse mapping.
|
|
x2 = int(x1 + self._cell.max_width / 2)
|
|
y2 = int(y1 + self._cell.max_height / 2)
|
|
if (0 <= y2 < len(text)) and (0 <= x2 < len(text[y2])):
|
|
self._write(text[y2][x2] + text[y2][x2],
|
|
dx * 2,
|
|
dy,
|
|
colour_map[y2][x2][0],
|
|
colour_map[y2][x2][1],
|
|
colour_map[y2][x2][2])
|
|
|
|
# Now rotate the background cell for the next frame.
|
|
self._rotation += pi / 180
|
|
|
|
return self._plain_image, self._colour_map
|