# -*- coding: utf-8 -*- """ This module implements a fire 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 import copy from random import randint, random from asciimatics.renderers.base import DynamicRenderer from asciimatics.screen import Screen class Fire(DynamicRenderer): """ Renderer to create a fire effect based on a specified `emitter` that defines the heat source. The implementation here uses the same techniques described in http://freespace.virgin.net/hugo.elias/models/m_fire.htm, although a slightly different implementation. """ _COLOURS_16 = [ (Screen.COLOUR_RED, 0), (Screen.COLOUR_RED, 0), (Screen.COLOUR_RED, 0), (Screen.COLOUR_RED, 0), (Screen.COLOUR_RED, 0), (Screen.COLOUR_RED, 0), (Screen.COLOUR_RED, 0), (Screen.COLOUR_RED, Screen.A_BOLD), (Screen.COLOUR_RED, Screen.A_BOLD), (Screen.COLOUR_RED, Screen.A_BOLD), (Screen.COLOUR_RED, Screen.A_BOLD), (Screen.COLOUR_YELLOW, Screen.A_BOLD), (Screen.COLOUR_YELLOW, Screen.A_BOLD), (Screen.COLOUR_YELLOW, Screen.A_BOLD), (Screen.COLOUR_YELLOW, Screen.A_BOLD), (Screen.COLOUR_WHITE, Screen.A_BOLD), ] _COLOURS_256 = [ (0, 0), (52, 0), (88, 0), (124, 0), (160, 0), (196, 0), (202, 0), (208, 0), (214, 0), (220, 0), (226, 0), (227, 0), (228, 0), (229, 0), (230, 0), (231, 0), ] _CHARS = " ...::$$$&&&@@" def __init__(self, height, width, emitter, intensity, spot, colours, bg=False): """ :param height: Height of the box to contain the flames. :param width: Width of the box to contain the flames. :param emitter: Heat source for the flames. Any non-whitespace character is treated as part of the heat source. :param intensity: The strength of the flames. The bigger the number, the hotter the fire. 0 <= intensity <= 1.0. :param spot: Heat of each spot source. Must be an integer > 0. :param colours: Number of colours the screen supports. :param bg: (Optional) Whether to render background colours only. """ super(Fire, self).__init__(height, width) self._emitter = emitter self._intensity = intensity self._spot_heat = spot self._count = len([c for c in emitter if c not in " \n"]) line = [0 for _ in range(self._canvas.width)] self._buffer = [copy.deepcopy(line) for _ in range(self._canvas.width * 2)] self._colours = self._COLOURS_256 if colours >= 256 else \ self._COLOURS_16 self._bg_too = bg # Figure out offset of emitter to centre at the bottom of the buffer e_width = 0 e_height = 0 for line in self._emitter.split("\n"): e_width = max(e_width, len(line)) e_height += 1 self._x = (width - e_width) // 2 self._y = height - e_height def _render_now(self): # First make the fire rise with convection for y in range(len(self._buffer) - 1): self._buffer[y] = self._buffer[y + 1] self._buffer[len(self._buffer) - 1] = [0 for _ in range(self._canvas.width)] # Seed new hot spots x = self._x y = self._y for c in self._emitter: if c not in " \n" and random() < self._intensity: self._buffer[y][x] += randint(1, self._spot_heat) if c == "\n": x = self._x y += 1 else: x += 1 # Seed a few cooler spots for _ in range(self._canvas.width // 2): self._buffer[randint(0, self._canvas.height - 1)][ randint(0, self._canvas.width - 1)] -= 10 # Simulate cooling effect of the resulting environment. for y in range(len(self._buffer)): for x in range(self._canvas.width): new_val = self._buffer[y][x] if y < len(self._buffer) - 1: new_val += self._buffer[y + 1][x] if x > 0: new_val += self._buffer[y][x - 1] if x < self._canvas.width - 1: new_val += self._buffer[y][x + 1] self._buffer[y][x] = new_val // 4 # Now build the rendered text from the simulated flames. self._clear() for x in range(self._canvas.width): for y in range(len(self._buffer)): if self._buffer[y][x] > 0: colour = self._colours[min(len(self._colours) - 1, self._buffer[y][x])] if self._bg_too: char = " " bg = colour[0] else: char = self._CHARS[min(len(self._CHARS) - 1, self._buffer[y][x])] bg = 0 self._write(char, x, y, colour[0], colour[1], bg) return self._plain_image, self._colour_map