Showing posts with label Python. Show all posts
Showing posts with label Python. Show all posts

Saturday, 7 November 2026

Molecular Structure Visualization in Python Using RDKit

 

Image

Molecular Structure Visualization in Python Using RDKit

Introduction

Python is widely used in scientific computing, artificial intelligence, machine learning, and data science. But did you know that Python can also help you visualize chemical molecules and explore their structures?

One of the most powerful libraries for this purpose is RDKit.

RDKit is an open-source cheminformatics toolkit that allows developers, researchers, and computational chemists to work with molecular structures, chemical fingerprints, molecular descriptors, and chemical data.

With just a few lines of Python code, you can convert a chemical representation into a visual molecular structure.

In this tutorial, we will learn how to install RDKit, understand SMILES notation, and generate a molecular structure image using Python.

What Is RDKit?

RDKit is an open-source software toolkit designed for cheminformatics and molecular data processing.

It provides Python APIs for working with chemical information and molecular structures.

Key Features of RDKit

  • Molecular Visualization: Generate 2D molecular structure diagrams.

  • SMILES Processing: Convert SMILES strings into molecular objects.

  • Molecular Descriptors: Calculate molecular properties and descriptors.

  • Chemical Fingerprints: Generate molecular fingerprints for similarity analysis.

  • Substructure Searching: Find specific chemical patterns inside molecules.

  • Molecular Manipulation: Create, modify, and analyze chemical structures.

RDKit is especially useful in computational chemistry, pharmaceutical research, cheminformatics, and AI-driven drug discovery.

1. Install RDKit

First, install RDKit using pip.

%pip install rdkit

If you are working in Jupyter Notebook, execute this command in a notebook cell.

2. Import Required Libraries

Next, import the required modules.

from rdkit import Chem
from rdkit.Chem import Draw

Understanding the imports:

  • Chem: Provides tools for creating and manipulating molecular structures.

  • Draw: Provides functionality for generating molecular structure images.

3. Create a Molecular Structure Using SMILES

SMILES stands for Simplified Molecular Input Line Entry System.

It is a text-based notation used to represent chemical structures.

Instead of manually drawing a molecule, we can describe its atoms, bonds, and connectivity using a SMILES string.

Let's use the following SMILES representation:

smiles = "CC(=O)OC1=CC=CC=C1C(=O)O"

This represents Aspirin (Acetylsalicylic Acid).

Now convert the SMILES string into an RDKit molecular object.

mol = Chem.MolFromSmiles(smiles)

The MolFromSmiles() function parses the SMILES string and creates a molecular object.

4. Visualize the Molecular Structure

RDKit provides MolToImage() to generate a 2D molecular diagram.

from IPython.display import display

display(
    Draw.MolToImage(
        mol,
        size=(450, 300)
    )
)

This generates a visual representation of the aspirin molecule.

The diagram displays:

  • Carbon atoms and their connectivity

  • Oxygen atoms

  • Single and double chemical bonds

  • The benzene ring

  • Functional groups

You can change the image dimensions using the size parameter.

5. Complete Python Code

Here is the complete implementation in one place:

from rdkit import Chem
from rdkit.Chem import Draw
from IPython.display import display

# SMILES representation of Aspirin
smiles = "CC(=O)OC1=CC=CC=C1C(=O)O"

# Convert SMILES into a molecular object
mol = Chem.MolFromSmiles(smiles)

# Check whether the molecule was parsed
if mol is not None:
    display(Draw.MolToImage(mol, size=(450, 300)))
else:
    print("Invalid SMILES string")

This code can be executed directly in Jupyter Notebook.

6. Important RDKit Functions

Here are some useful RDKit functions worth exploring:

FunctionPurpose
Chem.MolFromSmiles()Create a molecule from SMILES
Chem.MolToSmiles()Convert a molecule into SMILES
Draw.MolToImage()Generate a 2D molecular image
Chem.AddHs()Add explicit hydrogen atoms
Chem.RemoveHs()Remove explicit hydrogen atoms
Chem.MolFromMolBlock()Read a molecule from a MolBlock
Chem.FindMolChiralCenters()Identify chiral centers

7. Real-World Applications of RDKit

RDKit is useful across several scientific and technological fields.

Drug Discovery

Researchers use molecular representations and descriptors to investigate potential drug candidates.

Computational Chemistry

Chemists can process and analyze molecular structures programmatically.

Molecular Similarity

Chemical fingerprints can help compare molecules and identify structurally similar compounds.

Machine Learning

Molecular descriptors and fingerprints can be used as input features for machine learning models.

Artificial Intelligence in Chemistry

RDKit can be integrated into AI pipelines for molecular property prediction, chemical data processing, and generative molecular research.

8. Why Python Developers Should Learn RDKit

Python is not limited to web development, automation, or data analysis.

Its scientific ecosystem allows developers to work in specialized fields such as chemistry, biology, healthcare, and pharmaceutical research.

Learning RDKit introduces developers to:

  • Scientific Python programming

  • Molecular data processing

  • Chemical structure visualization

  • Computational chemistry

  • Molecular machine learning

  • AI-assisted drug discovery

It is an excellent library for developers interested in combining Python with scientific research.

Conclusion

RDKit demonstrates how powerful Python can be beyond traditional programming applications.

With only a few lines of code, we can convert a SMILES string into a molecular object and generate a 2D chemical structure diagram.

Whether you are a Python developer, data scientist, computational chemist, or AI researcher, RDKit is a valuable tool to explore.

Start experimenting with molecular structures and discover how Python connects programming with the world of chemistry!

Explore more Python libraries and scientific computing resources at CLCODING.

๐ŸŒ https://whatsapp.com/channel/0029Va5BbiT9xVJXygonSX0G

Wednesday, 4 November 2026

Flatten Nested Lists in Python 3.15 Using * – A Cleaner Way to Write Python Code

 

Image

Flatten Nested Lists in Python 3.15 Using * – A Cleaner Way to Write Python Code

Python 3.15 introduces an interesting improvement to Python comprehensions: unpacking directly inside list comprehensions using the * operator.

If you frequently work with nested lists, this new syntax provides a cleaner alternative to traditional nested list comprehensions.

Let's understand how it works with a simple example.

1. Understanding Nested Lists

A nested list is a list that contains other lists as its elements.

Consider the following example:

daily_weather = [
    ["☀️", "☁️"],
    ["๐ŸŒง️"],
    ["☀️", "๐ŸŒฉ️"]
]

print(daily_weather)

Output:

[['☀️', '☁️'], ['๐ŸŒง️'], ['☀️', '๐ŸŒฉ️']]

Here, we have three separate lists containing weather conditions.

What if we want to combine all these elements into one single list?

This process is called flattening a list.

2. Traditional Approach (Before Python 3.15)

Before Python 3.15, we commonly used nested list comprehensions.

daily_weather = [
    ["☀️", "☁️"],
    ["๐ŸŒง️"],
    ["☀️", "๐ŸŒฉ️"]
]

result = [
    weather
    for day in daily_weather
    for weather in day
]

print(result)

Output:

['☀️', '☁️', '๐ŸŒง️', '☀️', '๐ŸŒฉ️']

How does it work?

  • The first for loop iterates through each inner list.

  • The second for loop accesses every element inside that list.

  • Each element is added to the resulting list.

This approach works perfectly, but Python 3.15 introduces a more compact alternative.

3. Python 3.15: Flatten Lists Using *

Python 3.15 introduces unpacking in comprehensions through PEP 798.

Now, you can use the * operator directly inside a list comprehension.

daily_weather = [
    ["☀️", "☁️"],
    ["๐ŸŒง️"],
    ["☀️", "๐ŸŒฉ️"]
]

result = [*day for day in daily_weather]

print(result)

Output:

['☀️', '☁️', '๐ŸŒง️', '☀️', '๐ŸŒฉ️']

That's it!

The * operator unpacks each inner iterable, adding its individual elements to the resulting list.

4. Before vs. Python 3.15

Traditional approachPython 3.15
[x for d in data for x in d][*d for d in data]
Nested iterationIterable unpacking
More explicitMore compact

Both approaches produce the same flattened list for this example.

5. More Examples

The new syntax works with other iterable types too.

Example 1: Flatten a list of numbers

numbers = [
    [1, 2, 3],
    [4, 5],
    [6, 7, 8]
]

result = [*group for group in numbers]

print(result)

Output:

[1, 2, 3, 4, 5, 6, 7, 8]

Example 2: Flatten a list containing strings

words = [
    ["Python", "Java"],
    ["C++", "JavaScript"]
]

result = [*group for group in words]

print(result)

Output:

['Python', 'Java', 'C++', 'JavaScript']

Example 3: Flatten a list containing empty lists

data = [
    [10, 20],
    [],
    [30, 40]
]

result = [*group for group in data]

print(result)

Output:

[10, 20, 30, 40]

Empty inner iterables simply contribute no elements.

6. Important Things to Remember

  1. This syntax was introduced in Python 3.15 through PEP 798.

  2. It works with list and set comprehensions, dictionary comprehensions, and generator expressions.

  3. The * operator unpacks an iterable into individual elements.

  4. It flattens one level of nesting at a time.

  5. It does not recursively flatten deeply nested lists.

  6. Python 3.14 and earlier do not support this new comprehension syntax.

Important: This is not a recursive flattening solution. If your list contains multiple levels of nested lists, you will need a recursive function or another suitable approach.

7. Conclusion

Python continues to evolve with features that make code more expressive and easier to read.

The new unpacking syntax in Python 3.15 provides another way to flatten nested lists without writing multiple for clauses.

Traditional approach:

[x for group in data for x in group]

Python 3.15 approach:

[*group for group in data]

Both are useful, but the new syntax gives developers another compact option.

Keep learning, keep coding!

Follow CLCODING for more Python tutorials, coding challenges, and programming updates.



Frequently Asked Questions

Q1. What is list flattening in Python?

List flattening is the process of converting a list containing other lists into a single list containing their individual elements.

Q2. Which Python version supports unpacking in comprehensions?

Python 3.15 introduced this feature through PEP 798.

Q3. Does [*group for group in data] flatten multiple nested levels?

No. It flattens only one level. Deeply nested lists require a recursive or multi-level approach.

Q4. Is the traditional nested list comprehension still supported?

Yes. The traditional syntax remains valid and works in earlier Python versions.

Q5. Is the new syntax faster?

It provides a more concise way to express flattening. Do not assume it is faster in every situation; benchmark both approaches for performance-sensitive applications.

Monday, 2 November 2026

Pydantic Settings: Manage Python Application Configuration Like a Pro

 

Image


Pydantic Settings: Manage Python Application Configuration Like a Pro

SEO Title: Pydantic Settings in Python: Manage Environment Variables and Application Configuration

Meta Description: Learn how to use pydantic-settings in Python to manage environment variables, validate application configuration, load .env files, and build cleaner applications with practical examples.

Keywords: Pydantic Settings, Python environment variables, pydantic-settings tutorial, Python configuration management, Python backend development, Python libraries


Introduction

When building Python applications, configuration management is an important part of writing clean and maintainable code.

You might have different configuration values for:

  • Application names

  • Database connections

  • API keys

  • Debug mode

  • Server ports

  • External service URLs

A common beginner approach is to hardcode these values directly into Python files.

APP_NAME = "CLCODING"
DEBUG = True
PORT = 8000

This works for small projects, but it becomes difficult to maintain as your application grows.

What if you need different configurations for development, testing, and production?

This is where Pydantic Settings becomes useful.

What is Pydantic Settings?

pydantic-settings is a Python library that helps you manage application configuration using environment variables, .env files, and other settings sources.

It builds on Pydantic's validation capabilities, allowing you to define configuration using Python type hints.

Instead of manually reading and converting environment variables, you can define a settings class and let the library handle loading and validation.

Key benefits:

  1. Load configuration from environment variables.

  2. Validate configuration values using Python types.

  3. Support .env files.

  4. Organize application settings in one place.

  5. Reduce hardcoded configuration.

  6. Make applications easier to deploy and maintain.

Installation

Install the package using pip:

pip install pydantic-settings

Your First Pydantic Settings Example

Let's create a simple application configuration.

import os
from pydantic_settings import BaseSettings, SettingsConfigDict

# Set environment variables for demonstration
os.environ["APP_NAME"] = "CLCODING"
os.environ["APP_DEBUG"] = "true"
os.environ["APP_PORT"] = "9000"


class Settings(BaseSettings):

    model_config = SettingsConfigDict(
        env_prefix="APP_"
    )

    name: str = "My Python App"
    debug: bool = False
    port: int = 8000


settings = Settings()

print("App:", settings.name)
print("Debug:", settings.debug)
print("Port:", settings.port)

Expected Output

App: CLCODING
Debug: True
Port: 9000

How does this code work?

1. Import the required classes

from pydantic_settings import BaseSettings, SettingsConfigDict

BaseSettings provides the settings functionality, while SettingsConfigDict lets you configure how settings are loaded.

2. Define environment variables

os.environ["APP_NAME"] = "CLCODING"
os.environ["APP_DEBUG"] = "true"
os.environ["APP_PORT"] = "9000"

These simulate environment variables that would normally be configured outside your application.

3. Create a Settings class

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_")

    name: str = "My Python App"
    debug: bool = False
    port: int = 8000

The APP_ prefix tells Pydantic Settings to look for:

Python fieldEnvironment variable
nameAPP_NAME
debugAPP_DEBUG
portAPP_PORT

4. Create the settings object

settings = Settings()

The library reads matching environment variables and validates their values against the declared types.

For example, "9000" becomes the integer 9000, and "true" becomes the boolean True.

If an environment variable is not provided, the field's default value is used.

Using a .env File

For larger projects, you may prefer to keep configuration in a .env file during local development.

Create a file named .env:

APP_NAME=CLCODING
APP_DEBUG=true
APP_PORT=9000

Now update your Python code:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):

    model_config = SettingsConfigDict(
        env_prefix="APP_",
        env_file=".env",
        env_file_encoding="utf-8"
    )

    name: str = "My Python App"
    debug: bool = False
    port: int = 8000


settings = Settings()

print(settings.model_dump())

Expected Output

{
    'name': 'CLCODING',
    'debug': True,
    'port': 9000
}

Important: Install python-dotenv when using dotenv file support:

pip install python-dotenv

Environment variables take priority over values loaded from a .env file. Keep real credentials out of source control and add .env to your .gitignore.

Practical Example: Database Configuration

You can also use Pydantic Settings to manage database configuration.

from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseSettings(BaseSettings):

    model_config = SettingsConfigDict(
        env_prefix="DB_"
    )

    host: str = "localhost"
    port: int = 5432
    name: str = "my_database"
    user: str = "admin"


db = DatabaseSettings()

print("Database Host:", db.host)
print("Database Port:", db.port)
print("Database Name:", db.name)
print("Database User:", db.user)

You can override these values through environment variables without changing your Python source code.

For example:

DB_HOST=production.example.com
DB_PORT=5432
DB_NAME=production_db
DB_USER=app_user

This makes it easier to use the same application code across multiple environments.

Where Can You Use Pydantic Settings?

Pydantic Settings is particularly useful for:

1. FastAPI Applications

Manage application configuration, database connections, and external service settings.

2. Machine Learning Projects

Configure model paths, experiment parameters, API endpoints, and storage locations.

3. AI Applications

Manage LLM provider settings, API endpoints, model names, and service configuration.

4. Backend Development

Separate application configuration from business logic.

5. Production Deployment

Provide environment-specific configuration without changing application source code.

Best Practices

  • Avoid hardcoding API keys and passwords.

  • Use environment variables for deployment configuration.

  • Keep local .env files out of Git.

  • Define appropriate Python types for settings.

  • Use sensible defaults only for non-sensitive configuration.

  • Validate required settings instead of silently accepting missing values.

  • Use a secrets manager for sensitive production credentials.

Remember: Pydantic Settings helps load and validate configuration. It does not automatically encrypt or secure your secrets.

Conclusion

Configuration management is an essential part of professional Python development.

pydantic-settings makes this process easier by combining environment variable loading, type validation, and structured configuration into a simple Python interface.

If you are building APIs, backend services, machine learning applications, or AI-powered tools, this library is worth learning.

Try it in your next Python project!

Useful Resources

Follow CLCODING for more Python tutorials, libraries, coding tips, and practical developer resources.

From Code to Light: A Beautiful Diwali Diya Animation in Python

 

Image

from __future__ import annotations

import math import time from dataclasses import dataclass from typing import Optional, Tuple import numpy as np TAU = 2.0 * math.pi # ============================================================================= # 1. CONFIGURATION (everything here can also be set from the command line) # ============================================================================= @dataclass class Config: fps: float = 30.0 # target frames per second speed: float = 1.0 # global animation speed (also speeds up the intro) intro: float = 1.0 # multiplies the length of the intro stages (1-4) density: float = 1.0 # particle density multiplier glow: float = 1.0 # strength of the surrounding ambient glow accent: float = 1.0 # amount of cyan / electric-blue accents (0 = none) cols: int = 0 # canvas width in characters (0 = whole terminal) rows: int = 0 # canvas height in lines (0 = whole terminal) color_mode: str = "auto" # auto | truecolor | 256 seed: Optional[int] = None # fixed random seed for a repeatable animation duration: float = 0.0 # stop automatically after N seconds (0 = never) # Colour ramps: (heat 0..1, (r, g, b)). Edit these to re-colour the whole piece. FIRE_STOPS = [ (0.00, (35, 4, 28)), # deep plum (0.12, (125, 8, 38)), # ruby red (0.27, (220, 30, 42)), # vivid crimson (0.42, (255, 76, 35)), # coral (0.58, (255, 137, 25)), # bright orange (0.73, (255, 194, 45)), # amber (0.88, (255, 235, 105)), # golden yellow (1.00, (255, 255, 230)), # warm-white core ] ACCENT_STOPS = [ (0.00, (35, 12, 110)), # royal purple (0.24, (88, 35, 210)), # violet (0.48, (0, 115, 255)), # electric blue (0.70, (0, 220, 220)), # turquoise (0.86, (70, 255, 170)), # emerald (1.00, (235, 255, 255)), # icy highlight ] FIRE_STEPS = 48 # number of distinct fire colours ACCENT_STEPS = 20 # number of distinct accent colours # Glyphs. Index into GLYPHS is what the frame buffers store (0 = blank). GLYPHS = " .,:;!|/\\-_=+*xX#%@" LEVEL_GROUPS = [".,", ":;", "+*", "xX#", "%@"] # very low .. maximum intensity LEVEL_EDGES = np.array([0.07, 0.20, 0.36, 0.58, 0.80]) # heat thresholds between groups # ============================================================================= # 2. SMALL HELPERS # ============================================================================= def smoothstep(e0, e1, x): t = np.clip((x - e0) / (e1 - e0), 0.0, 1.0) return t * t * (3.0 - 2.0 * t) def flow(a, b, t): """Cheap smooth pseudo-noise in [-1, 1] built from travelling sine waves.""" return (np.sin(a * 1.7 + b * 1.1 + t * 1.9) + np.sin(a * 3.3 - b * 2.1 - t * 2.7 + 1.3) + np.sin(a * 5.9 + b * 3.7 + t * 3.9 + 2.1)) * (1.0 / 3.0) def build_ramp(stops, steps): pos = np.array([s[0] for s in stops], dtype=np.float64) rgb = np.array([s[1] for s in stops], dtype=np.float64) xs = np.linspace(0.0, 1.0, steps) cols = [np.interp(xs, pos, rgb[:, c]) for c in range(3)] return np.stack(cols, axis=1).round().astype(np.uint8) def rgb_to_xterm256(r: int, g: int, b: int) -> int: """Nearest xterm-256 colour (6x6x6 cube or greyscale ramp).""" levels = (0, 95, 135, 175, 215, 255) def nearest(v): return min(range(6), key=lambda i: abs(levels[i] - v)) ri, gi_, bi = nearest(r), nearest(g), nearest(b) cube = (levels[ri], levels[gi_], levels[bi]) cube_idx = 16 + 36 * ri + 6 * gi_ + bi gray_i = min(23, max(0, int(round((((r + g + b) / 3.0) - 8) / 10.0)))) gv = 8 + 10 * gray_i d_cube = sum((a - c) ** 2 for a, c in zip((r, g, b), cube)) d_gray = sum((a - gv) ** 2 for a in (r, g, b)) return 232 + gray_i if d_gray < d_cube else cube_idx FIRE_RAMP = build_ramp(FIRE_STOPS, FIRE_STEPS) ACCENT_RAMP = build_ramp(ACCENT_STOPS, ACCENT_STEPS) COLOR_TABLE = np.vstack([np.zeros((1, 3), np.uint8), FIRE_RAMP, ACCENT_RAMP]) ACCENT_BASE = 1 + FIRE_STEPS # first accent index in COLOR_TABLE def _glyph(c: str) -> int: return GLYPHS.index(c) GROUP_IDX = np.array( [[_glyph(g[i % len(g)]) for i in range(4)] for g in LEVEL_GROUPS], dtype=np.int32 ) # shape (5 intensity levels, 4 interchangeable variants) def shade(h, k, accent): """Heat (float), variant (int 0-3) and accent flag arrays -> (glyph, colour) index arrays.""" lvl = np.searchsorted(LEVEL_EDGES, h, side="right") # 0 = blank, 1..5 glyph = np.where(lvl > 0, GROUP_IDX[np.maximum(lvl - 1, 0), k], 0) hc = np.clip(h, 0.0, 1.0) fire = 1 + (hc * (FIRE_STEPS - 1) + 0.5).astype(np.int32) acc = ACCENT_BASE + (np.clip(h * 1.15, 0.0, 1.0) * (ACCENT_STEPS - 1) + 0.5).astype(np.int32) col = np.where(accent, acc, fire) return glyph.astype(np.uint8), col.astype(np.uint8) # ============================================================================= # 4. DIYA GEOMETRY # ============================================================================= class DiyaShape: """ Static geometry of the lamp, evaluated on the character grid. Coordinates are "normalised": X right, Y up, 1.0 = half the canvas height, and one terminal column counts as half a row so shapes keep their true proportions. """ Y_RIM = -0.20 # height of the rim ellipse centre R = 0.78 # half width of the bowl D = 0.46 # depth of the bowl below the rim P = 2.10 # super-ellipse power of the bowl profile RIM_B = 0.10 # half height of the rim ellipse (seen slightly from above) FOOT_TOP = -0.64 FOOT_BOT = -0.77 Y_WICK = Y_RIM + 0.02 def __init__(self, W: int, H: int, rng: np.random.Generator): self.W, self.H = W, H self.unit = max(4.0, min(H / 2.0, W / 4.0)) * 0.97 self.cx = (W - 1) / 2.0 self.cy = (H - 1) / 2.0 xs = (np.arange(W) - self.cx) * 0.5 / self.unit ys = (self.cy - np.arange(H)) / self.unit self.X, self.Y = np.meshgrid(xs, ys) # per-cell random constants (visibility, glyph variants, twinkle phases ...) self.r1 = rng.random((H, W)) self.r2 = rng.random((H, W)) self.r3 = rng.random((H, W)) self.phase = rng.uniform(0.0, TAU, (H, W)) self.rate = rng.uniform(1.5, 7.0, (H, W)) # glyph substitution speed self.rate2 = rng.uniform(0.7, 3.2, (H, W)) # twinkle speed inside, heat, oil, _ = self.evaluate(self.X, self.Y) self.bowl_in, self.oil = inside, oil self._build_edges(inside) # outline cells are drawn dark red / dark orange, the rest keeps its lighting self.bowl_heat = np.where(self.edge, 0.24 + 0.36 * heat, heat) * inside # soft radial illumination around the flame self.glow = np.exp(-((self.X / 0.80) ** 2 + ((self.Y - 0.10) / 0.85) ** 2)) # how late each cell is revealed while the diya forms (centre first, ragged edge) dist = np.hypot(self.X * 0.85, self.Y + 0.15) / 1.05 self.rv = 0.65 * np.clip(dist, 0.0, 1.3) + 0.35 * self.r1 # ---- analytic description of the lamp ------------------------------ def evaluate(self, X, Y): """Return (inside, static heat, oil mask, sampling weight) for arbitrary coordinates.""" R, yr = self.R, self.Y_RIM ax = np.abs(X) below = Y <= yr g = (ax / R) ** self.P + (np.maximum(yr - Y, 0.0) / self.D) ** 2 body = below & (g <= 1.0) ell = (ax / (0.98 * R)) ** 1.8 + ((Y - yr) / self.RIM_B) ** 2 rim = ell <= 1.0 foot_t = np.clip((self.FOOT_TOP - Y) / (self.FOOT_TOP - self.FOOT_BOT), 0.0, 1.0) foot_hw = 0.22 + 0.15 * foot_t ** 1.6 foot = (Y <= self.FOOT_TOP) & (Y >= self.FOOT_BOT) & (ax <= foot_hw) inside = body | rim | foot # lighting of the bowl body: brighter near the rim, highlights on the shoulders ybot = yr - self.D v = np.clip((Y - ybot) / self.D, 0.0, 1.0) heat = 0.40 + 0.34 * v ** 1.2 + 0.12 * v * np.exp(-(X / 0.5) ** 2) hl1 = np.exp(-(((X + 0.42 * R) / 0.22) ** 2 + ((Y - (yr - 0.14)) / 0.06) ** 2)) hl2 = np.exp(-(((X - 0.50 * R) / 0.15) ** 2 + ((Y - (yr - 0.20)) / 0.045) ** 2)) heat = heat + 0.40 * hl1 + 0.18 * hl2 # decorative band: a row of glowing pearls and a darker line above it yb = yr - 0.21 pearl = (np.abs(Y - yb) < 0.026) & (np.abs((X / 0.105) % 1.0 - 0.5) < 0.21) & (g < 0.72) heat = np.where(pearl, 0.92, heat) line = (np.abs(Y - (yb + 0.075)) < 0.011) & (g < 0.85) heat = np.where(line, heat * 0.55, heat) # foot foot_h = 0.30 + 0.28 * (1.0 - foot_t) heat = np.where(foot & ~body, foot_h, heat) # rim ellipse: bright front lip, softer back lip, glowing oil inside ring = rim & (ell > 0.60) heat = np.where(ring, np.where(below, 0.90, 0.55), heat) oil = rim & (ell <= 0.60) heat = np.where(oil, 0.80 + 0.18 * np.exp(-(X / 0.2) ** 2), heat) edge_b = np.exp(-((g - 0.93) / 0.14) ** 2) * below edge_e = np.exp(-((ell - 0.85) / 0.20) ** 2) weight = inside * (0.30 + 0.70 * np.maximum(np.maximum(edge_b, edge_e), foot * 0.6)) return inside, heat * inside, oil, weight def _build_edges(self, inside): """Outline cells and the glyph ( / \\ | - _ ) that follows the outline direction.""" pad = np.pad(inside, 1) eroded = inside & pad[:-2, 1:-1] & pad[2:, 1:-1] & pad[1:-1, :-2] & pad[1:-1, 2:] self.edge = inside & ~eroded def blur(a): p = np.pad(a, 1) return (4 * a + 2 * (p[:-2, 1:-1] + p[2:, 1:-1] + p[1:-1, :-2] + p[1:-1, 2:]) + (p[:-2, :-2] + p[:-2, 2:] + p[2:, :-2] + p[2:, 2:])) / 16.0 s = blur(blur(inside.astype(np.float64))) gy, gx = np.gradient(s) # d/drow, d/dcol th = np.arctan2(2.0 * gx, gy) % math.pi # direction of the outline in screen space glyph = np.full(inside.shape, _glyph("|"), np.uint8) top = gy > 0 # inside lies below -> upper outline horiz = (th < math.pi / 8) | (th >= 7 * math.pi / 8) slash = (th >= math.pi / 8) & (th < 3 * math.pi / 8) back = (th >= 5 * math.pi / 8) & (th < 7 * math.pi / 8) glyph[slash] = _glyph("/") glyph[back] = _glyph("\\") glyph[horiz & top] = _glyph("-") glyph[horiz & ~top] = _glyph("_") self.edge_glyph = glyph self.edge_sel = self.edge & (self.r3 < 0.88) # a few gaps keep it organic # ---- sampling / mapping -------------------------------------------- def to_cell(self, X, Y): c = np.rint(self.cx + 2.0 * self.unit * X).astype(np.int64) r = np.rint(self.cy - self.unit * Y).astype(np.int64) return r, c def sample_bowl(self, n: int, rng: np.random.Generator): """n home positions on the lamp (outline weighted) plus their static heat.""" xs, ys, hs = [], [], [] got = 0 while got < n: m = (n - got) * 4 + 64 X = rng.uniform(-self.R * 1.02, self.R * 1.02, m) Y = rng.uniform(self.FOOT_BOT, self.Y_RIM + self.RIM_B, m) _, heat, _, w = self.evaluate(X, Y) keep = rng.random(m) < w xs.append(X[keep]) ys.append(Y[keep]) hs.append(heat[keep]) got += int(keep.sum()) return np.concatenate(xs)[:n], np.concatenate(ys)[:n], np.concatenate(hs)[:n] # ============================================================================= # 5. FLAME # ============================================================================= class FireSimulation: """Procedural, continuously changing flame (shape, sway, flicker, heat).""" H0 = 0.98 # full flame height (normalised units) W0 = 0.20 # full flame half width def __init__(self, shape: DiyaShape): self.shape = shape self.y0 = shape.Y_WICK self.burst = 0.0 # set by the controller for "glowing bursts" def height(self, t, grow): f = (1.0 + 0.055 * math.sin(TAU * 1.9 * t + 0.5) + 0.040 * math.sin(TAU * 3.7 * t + 1.7) + 0.025 * math.sin(TAU * 7.3 * t + 0.2)) return self.H0 * grow * f * (1.0 + 0.10 * self.burst) def sway(self, s, t, grow): sc = np.clip(s, 0.0, 1.0) return grow * sc ** 1.5 * (0.050 * np.sin(TAU * 0.55 * t + 2.4 * sc) + 0.032 * np.sin(TAU * 1.35 * t + 4.3 * sc + 1.0) + 0.016 * np.sin(TAU * 3.10 * t + 7.0 * sc + 2.0)) def half_width(self, s, t, grow): sc = np.clip(s, 0.0, 1.0) body = np.maximum(np.sin(math.pi * sc ** 0.62), 0.0) ** 0.9 wob = 1.0 + 0.14 * flow(sc * 3.0, 0.0, t * 2.3) return np.maximum(self.W0 * grow ** 0.8 * body * wob, 0.010) @staticmethod def profile(u, s): """Flame heat for a point at |u| half-widths from the centre line, s along its height.""" sc = np.clip(s, 0.0, 1.0) body = np.exp(-(u / 0.98) ** 4) # defined flame edge halo = np.exp(-(u / 1.7) ** 2) # soft red-orange fringe core = np.exp(-(u / 0.50) ** 2) * (1.0 - 0.75 * sc) return (0.74 * body + 0.20 * halo + 0.34 * core) * (1.0 - 0.25 * sc ** 2) def place(self, s, u, t, grow): """Map flame coordinates (s along height, u across width) to normalised X, Y.""" Y = self.y0 + s * self.height(t, grow) X = self.sway(s, t, grow) + u * self.half_width(s, t, grow) return X, Y def field(self, t, grow, energy): """Flame heat (0..1) for every character cell.""" sh = self.shape X, Y = sh.X, sh.Y hf = max(self.height(t, grow), 1e-3) s = (Y - self.y0) / hf sc = np.clip(s, 0.0, 1.0) w = self.half_width(sc, t, grow) u = np.abs(X - self.sway(sc, t, grow)) / w u = u * (1.0 + 0.22 * flow(X * 6.0, Y * 6.5 - t * 2.6, t * 1.3)) heat = self.profile(u, sc) env = smoothstep(-0.03, 0.10, s) * (1.0 - smoothstep(0.93, 1.12, s)) bands = 0.88 + 0.12 * flow(X * 4.5, Y * 9.0 - t * 4.2, t * 0.7) return np.clip(heat * env * bands * energy, 0.0, 1.0) # ============================================================================= # 6. PARTICLES # ============================================================================= @dataclass class Particle: """Snapshot of one particle. ParticleSystem stores the same fields as numpy columns (struct-of-arrays) so thousands of them can be updated per frame.""" x: float = 0.0 y: float = 0.0 vx: float = 0.0 vy: float = 0.0 intensity: float = 1.0 life: float = 1.0 max_life: float = 1.0 flicker: float = 4.0 # Hz accent: bool = False char: str = " " color: Tuple[int, int, int] = (0, 0, 0) class Controls: """Everything the animation stages drive (see AnimationController.controls).""" stage = 1 gain = 0.0 # fraction of particles that are visible radius = 0.05 # size of the initial spark cloud structure = 0.0 # how far particles have moved to their place in the diya flame_grow = 0.12 # size of the flame (0..1) field_gain = 0.0 # how much of the solid, dense diya texture is revealed glow_gain = 0.0 # ambient radial glow accent_gain = 0.0 # cyan / blue accents allowed core_heat = 0.55 # temperature of the middle of the spark cloud flick_depth = 0.95 # how strongly particles blink on/off alive = False class ParticleSystem: """ Three kinds of "former" particles (bowl, flame, ambient) start as a spark cloud and drift to their place in the diya, plus a pool of free-flying sparks that rise from the flame. """ C0 = (0.0, 0.0) # centre of the first sparks (centre of the terminal) def __init__(self, shape: DiyaShape, fire: FireSimulation, cfg: Config, rng: np.random.Generator): self.shape, self.fire, self.cfg, self.rng = shape, fire, cfg, rng cells = shape.W * shape.H d = max(cfg.density, 0.05) nA, nB, nC = int(0.14 * cells * d), int(0.10 * cells * d), int(0.30 * cells * d) nS = int(0.045 * cells * d) + 24 n = nA + nB + nC self.n, self.nS = n, nS self.sl_bowl = slice(0, nA) self.sl_flame = slice(nA, nA + nB) self.sl_amb = slice(nA + nB, n) # --- common properties of the formers scale = np.array([0.75, 0.55]) self.sc_dir = np.clip(rng.standard_normal((n, 2)), -2.4, 2.4) * scale self.appear = rng.random(n) self.delay = rng.random(n) self.intensity = 0.55 + 0.45 * rng.random(n) self.flick = rng.uniform(1.5, 9.0, n) self.phase = rng.uniform(0.0, TAU, n) self.accent = np.zeros(n, bool) # --- bowl particles: sampled on the lamp self.bowl_hx, self.bowl_hy, self.bowl_heat = shape.sample_bowl(nA, rng) self.accent[self.sl_bowl] = rng.random(nA) < 0.02 * cfg.accent # --- flame particles: coordinates inside the (moving) flame s = np.empty(0) while s.size < nB: c = rng.random(nB * 3 + 32) keep = rng.random(c.size) < np.sqrt(np.maximum(np.sin(math.pi * c ** 0.62), 0.0)) s = np.concatenate([s, c[keep]]) self.fs = s[:nB] self.fu = np.clip(rng.normal(0.0, 0.55, nB), -1.5, 1.5) self.accent[self.sl_flame] = (np.abs(self.fu) > 1.0) & (rng.random(nB) < 0.35 * cfg.accent) # --- ambient particles: a halo around the flame with different behaviours r = np.abs(rng.standard_normal(nC)) * 0.44 a = rng.uniform(0.0, TAU, nC) self.amb_hx = r * np.cos(a) * 1.35 self.amb_hy = 0.06 + r * np.sin(a) * 0.95 self.beh = rng.integers(0, 4, nC) # 0 still, 1 drifts sideways, 2 rises, 3 pulses self.beh_rate = rng.uniform(0.08, 0.35, nC) self.accent[self.sl_amb] = rng.random(nC) < 0.09 * cfg.accent self._hx = np.empty(n) self._hy = np.empty(n) self._home = np.empty(n) self._hx[self.sl_bowl] = self.bowl_hx self._hy[self.sl_bowl] = self.bowl_hy # --- free-flying sparks self.s_x = np.zeros(nS) self.s_y = np.zeros(nS) self.s_vx = np.zeros(nS) self.s_vy = np.zeros(nS) self.s_life = np.zeros(nS) self.s_max = np.ones(nS) self.s_int = np.ones(nS) self.s_acc = np.zeros(nS, bool) self.s_rate = rng.uniform(2.0, 8.0, nS) self.s_ph = rng.uniform(0.0, TAU, nS) self.spark_rate = nS / 1.8 # emissions per second at full flame # ---- sparks --------------------------------------------------------- def emit_sparks(self, count: int, t: float, grow: float, strength: float = 1.0): free = np.flatnonzero(self.s_life <= 0.0)[:count] m = free.size if m == 0: return rng = self.rng s = rng.uniform(0.05, 0.70, m) u = rng.normal(0.0, 0.55, m) x, y = self.fire.place(s, u, t, grow) life = rng.uniform(0.9, 2.8, m) * (0.8 + 0.4 * strength) self.s_x[free], self.s_y[free] = x, y self.s_vx[free] = rng.normal(0.0, 0.035 * strength, m) self.s_vy[free] = rng.uniform(0.10, 0.50, m) * strength self.s_life[free] = life self.s_max[free] = life self.s_int[free] = rng.uniform(0.75, 1.0, m) self.s_acc[free] = rng.random(m) < 0.22 * self.cfg.accent def _update_sparks(self, t, dt, ctl): self.s_life -= dt np.maximum(self.s_life, -1.0, out=self.s_life) self.s_x += (self.s_vx + 0.045 * np.sin(TAU * self.s_rate * t + self.s_ph)) * dt self.s_y += self.s_vy * dt self.s_vx *= max(0.0, 1.0 - 0.25 * dt) if ctl.stage >= 4 and ctl.flame_grow > 0.3: n = self.rng.poisson(self.spark_rate * ctl.flame_grow ** 2 * dt) if n: self.emit_sparks(int(n), t, ctl.flame_grow) def snapshot(self, i: int) -> Particle: """Return spark i as a Particle record (handy for debugging / extensions).""" life = float(self.s_life[i]) h = float(self.s_int[i] * max(life, 0.0) / self.s_max[i]) ch, col = shade(np.array([h]), np.zeros(1, np.int64), np.array([bool(self.s_acc[i])])) r, g, b = COLOR_TABLE[int(col[0])].tolist() return Particle(float(self.s_x[i]), float(self.s_y[i]), float(self.s_vx[i]), float(self.s_vy[i]), float(self.s_int[i]), life, float(self.s_max[i]), float(self.s_rate[i]), bool(self.s_acc[i]), GLYPHS[int(ch[0])], (r, g, b)) # ---- per-frame state ------------------------------------------------- def render(self, t, dt, ctl: Controls): """Advance the simulation and return per-cell (heat, glyph variant, accent) layers.""" sh = self.shape H, W = sh.H, sh.W heat_buf = np.zeros((H, W)) var_buf = np.zeros((H, W), np.int64) acc_buf = np.zeros((H, W), bool) if ctl.stage < 2: return heat_buf, var_buf, acc_buf self._update_sparks(t, dt, ctl) n = self.n k = smoothstep(0.0, 1.0, np.clip((ctl.structure - 0.6 * self.delay) / 0.4, 0.0, 1.0)) ph = self.phase # cloud ("scatter") positions and heat sx = self.C0[0] + self.sc_dir[:, 0] * ctl.radius + 0.006 * np.sin(TAU * 0.7 * t + ph) sy = self.C0[1] + self.sc_dir[:, 1] * ctl.radius + 0.006 * np.cos(TAU * 0.6 * t + ph) d = np.hypot(sx - self.C0[0], (sy - self.C0[1]) * 1.3) scat = (0.10 + ctl.core_heat * np.exp(-(d / (0.22 + 0.55 * ctl.radius)) ** 2)) * self.intensity # home positions / heat hx, hy, home = self._hx, self._hy, self._home b, f, a = self.sl_bowl, self.sl_flame, self.sl_amb wave_x = 0.010 * np.sin(TAU * 0.31 * t + 3.1 * self.bowl_hy + ph[b]) hx[b] = self.bowl_hx + wave_x hy[b] = self.bowl_hy + 0.006 * np.cos(TAU * 0.27 * t + ph[b]) home[b] = self.bowl_heat * (0.80 + 0.20 * np.sin(TAU * self.flick[b] * 0.4 * t + ph[b])) fx, fy = self.fire.place(self.fs, self.fu, t, ctl.flame_grow) hx[f], hy[f] = fx, fy home[f] = self.fire.profile(np.abs(self.fu), self.fs) * (0.80 + 0.20 * self.intensity[f]) pa, beh, br = ph[a], self.beh, self.beh_rate rise_ph = (t * br * 0.5 + pa / TAU) % 1.0 drift = np.where(beh == 1, 0.07 * np.sin(TAU * br * t + pa), 0.0) rise = np.where(beh == 2, rise_ph * 0.55, 0.0) ax = self.amb_hx + 0.014 * np.sin(TAU * 0.31 * t + 3.1 * self.amb_hy + pa) + drift ay = self.amb_hy + 0.011 * np.cos(TAU * 0.27 * t + 2.3 * self.amb_hx + pa) + rise hx[a], hy[a] = ax, ay illum = np.exp(-((ax / 0.95) ** 2 + ((ay - 0.12) / 0.90) ** 2)) mod = np.ones(a.stop - a.start) mod = np.where(beh == 2, (1.0 - rise_ph) ** 0.7 * smoothstep(0.0, 0.06, rise_ph), mod) mod = np.where(beh == 3, 0.25 + 0.75 * np.maximum(0.0, np.sin(TAU * br * t + pa)) ** 3, mod) home[a] = (0.14 + 0.52 * illum) * self.intensity[a] * mod * (0.4 + 0.6 * ctl.glow_gain) # blend cloud -> home, then apply appear-over-time and on/off flickering x = sx * (1.0 - k) + hx * k y = sy * (1.0 - k) + hy * k heat = scat * (1.0 - k) + home * k vis = np.clip((ctl.gain - self.appear) * 20.0, 0.0, 1.0) raw = 0.5 + 0.5 * np.sin(TAU * self.flick * t + ph) fl = (1.0 - ctl.flick_depth) + ctl.flick_depth * smoothstep(0.15, 0.40, raw) heat = heat * vis * fl accent = self.accent & (ctl.accent_gain > 0.5) var = ((t * self.flick * 0.35 + ph).astype(np.int64)) & 3 # sparks alive = self.s_life > 0.0 shp = np.clip(self.s_life / self.s_max, 0.0, 1.0) ** 0.85 sheat = self.s_int * shp * (0.82 + 0.18 * np.sin(TAU * self.s_rate * t + self.s_ph)) * alive svar = ((t * self.s_rate * 0.5 + self.s_ph).astype(np.int64)) & 3 X = np.concatenate([x, self.s_x]) Y = np.concatenate([y, self.s_y]) Hh = np.concatenate([heat, sheat]) V = np.concatenate([var, svar]) A = np.concatenate([accent, self.s_acc]) r, c = sh.to_cell(X, Y) ok = (Hh > LEVEL_EDGES[0]) & (r >= 0) & (r < H) & (c >= 0) & (c < W) r, c, Hh, V, A = r[ok], c[ok], Hh[ok], V[ok], A[ok] order = np.argsort(Hh) # hottest particle written last wins the cell r, c = r[order], c[order] heat_buf[r, c] = Hh[order] var_buf[r, c] = V[order] acc_buf[r, c] = A[order] return heat_buf, var_buf, acc_buf # ============================================================================= # 7. ANIMATION CONTROLLER # ============================================================================= class AnimationController: """Runs the five stages and composes flame, lamp, glow and particles into one frame.""" # end of each stage in seconds at speed 1 / intro 1: # 1 darkness, 2 first sparks, 3 expansion, 4 formation, then 5 living artwork STAGE_ENDS = (1.0, 4.0, 8.5, 15.0) def __init__(self, cfg: Config, W: int, H: int): self.cfg = cfg self.rng = np.random.default_rng(cfg.seed) self.bounds = [e * max(cfg.intro, 0.05) for e in self.STAGE_ENDS] self.burst = 0.0 self.resize(W, H) def resize(self, W: int, H: int): self.W, self.H = W, H self.shape = DiyaShape(W, H, self.rng) self.fire = FireSimulation(self.shape) self.particles = ParticleSystem(self.shape, self.fire, self.cfg, self.rng) # ---- stage logic ---------------------------------------------------- def controls(self, t: float) -> Controls: b0, b1, b2, b3 = self.bounds c = Controls() if t < b0: # stage 1: darkness return c if t < b1: # stage 2: first sparks e = smoothstep(0.0, 1.0, (t - b0) / (b1 - b0)) c.stage, c.gain = 2, 0.002 + 0.028 * e c.radius, c.core_heat = 0.05 + 0.10 * e, 0.50 + 0.12 * e elif t < b2: # stage 3: particle expansion p = (t - b1) / (b2 - b1) e = smoothstep(0.0, 1.0, p) c.stage, c.gain = 3, 0.03 + 0.72 * p ** 1.5 c.radius, c.structure = 0.15 + 0.85 * e, 0.34 * e c.core_heat = 0.62 + 0.30 * e c.glow_gain = 0.30 * smoothstep(0.4, 1.0, p) c.accent_gain, c.flick_depth = p, 0.95 - 0.10 * p elif t < b3: # stage 4: diya formation p = (t - b2) / (b3 - b2) e = smoothstep(0.0, 1.0, p) c.stage, c.gain = 4, 0.75 + 0.25 * e c.radius, c.structure = 1.0, 0.34 + 0.80 * e c.flame_grow = 0.12 + 0.88 * smoothstep(0.05, 0.95, p) c.field_gain = smoothstep(0.25, 1.0, p) c.glow_gain, c.accent_gain = 0.30 + 0.70 * e, 1.0 c.core_heat, c.flick_depth = 0.95, 0.85 - 0.50 * e else: # stage 5: living artwork c.stage, c.gain, c.radius, c.structure = 5, 1.0, 1.0, 1.14 c.flame_grow, c.field_gain, c.glow_gain, c.accent_gain = 1.0, 1.0, 1.0, 1.0 c.core_heat, c.flick_depth, c.alive = 0.95, 0.35, True return c def _tick_bursts(self, t, dt, ctl): self.burst *= math.exp(-dt / 0.55) if ctl.alive and self.rng.random() < 0.30 * dt: # occasional glowing burst self.burst = 1.0 self.particles.emit_sparks(max(8, int(0.10 * self.particles.nS)), t, ctl.flame_grow, strength=1.5) self.fire.burst = self.burst # ---- composition ------------------------------------------------------ def frame(self, t: float, dt: float) -> Tuple[np.ndarray, np.ndarray]: ctl = self.controls(t) sh = self.shape if ctl.stage < 2: z = np.zeros((sh.H, sh.W), np.uint8) return z, z self._tick_bursts(t, dt, ctl) energy = 1.0 + 0.16 * self.burst flick = (0.90 + 0.05 * math.sin(TAU * 3.3 * t + 1.0) + 0.03 * math.sin(TAU * 7.9 * t) + 0.02 * math.sin(TAU * 13.1 * t + 2.0)) # solid layers: flame + lamp, revealed from the centre outwards flame = self.fire.field(t, ctl.flame_grow, energy) shimmer = 0.84 + 0.16 * np.sin(t * sh.rate + sh.phase) light = float(np.clip(flick + 0.10 * self.burst, 0.8, 1.15)) bowl = sh.bowl_heat * light * shimmer rev = np.clip((ctl.field_gain * 1.35 - sh.rv) / 0.22, 0.0, 1.0) field = np.maximum(flame, bowl) * rev field = field * (0.92 + 0.08 * np.sin(t * sh.rate * 1.3 + sh.phase * 2.0)) # ambient glow: a dithered, twinkling halo g = sh.glow * ctl.glow_gain * self.cfg.glow * (0.82 + 0.18 * flick + 0.25 * self.burst) tw = 0.5 + 0.5 * np.sin(t * sh.rate2 + sh.phase) glow_h = np.where(sh.r2 < np.clip(g * 1.25, 0.0, 1.0), g * 0.40 * (0.40 + 0.75 * tw), 0.0) field = np.maximum(field, glow_h) # digital accents: a few cyan/blue cells twinkling in the glow acc_f = (sh.r3 > 1.0 - 0.020 * self.cfg.accent) & (g > 0.10) & (tw > 0.80) & (field < 0.30) field = np.where(acc_f, 0.30 + 0.5 * tw, field) var_f = ((t * sh.rate + sh.phase * 2.0).astype(np.int64)) & 3 # particle layer on top hp, vp, ap = self.particles.render(t, dt, ctl) use_p = hp > field heat = np.where(use_p, hp, field) var = np.where(use_p, vp, var_f) acc = np.where(use_p, ap, acc_f) ch, col = shade(heat, var, acc) # outline glyphs that follow the curve of the lamp edge = sh.edge_sel & ~use_p & (rev > 0.5) & (ch != 0) ch = np.where(edge, sh.edge_glyph, ch).astype(np.uint8) return ch, col # JUPYTER NOTEBOOK RENDERER from IPython.display import display, HTML, clear_output from html import escape def frame_to_html(ch, col): rows = [] for rr in range(ch.shape[0]): pieces, run, last_color = [], [], None for cc in range(ch.shape[1]): gid = int(ch[rr, cc]) if gid == 0: char, color = " ", None else: char = GLYPHS[gid] rgb = COLOR_TABLE[int(col[rr, cc])] color = "#{:02x}{:02x}{:02x}".format(*map(int, rgb)) if color != last_color and run: txt = escape("".join(run)).replace(" ", "&nbsp;") pieces.append(txt if last_color is None else '<span style="color:{}">{}</span>'.format(last_color, txt)) run = [] run.append(char) last_color = color if run: txt = escape("".join(run)).replace(" ", "&nbsp;") pieces.append(txt if last_color is None else '<span style="color:{}">{}</span>'.format(last_color, txt)) rows.append("".join(pieces)) return "<br>".join(rows) def run_notebook(duration=25, fps=12, width=84, height=30, speed=1.0, density=0.85, glow=1.0, accent=1.0, seed=None): cfg = Config(fps=fps, speed=speed, intro=1.0, density=density, glow=glow, accent=accent, cols=width, rows=height, color_mode="truecolor", seed=seed, duration=duration) controller = AnimationController(cfg, width, height) frame_dt = 1.0 / max(1, fps) start = last = time.perf_counter() next_frame = start try: while True: now = time.perf_counter() elapsed = now - start if duration > 0 and elapsed >= duration: break dt = min(now - last, 0.1) ch, col = controller.frame(elapsed * speed, dt * speed) artwork = frame_to_html(ch, col) clear_output(wait=True) html = ('<div style="background:radial-gradient(ellipse at 50% 48%, #241044 0%, #090817 48%, #020207 100%);padding:18px 12px;border:1px solid #49306f;border-radius:16px;text-align:center;box-shadow:0 0 28px rgba(130,70,255,.22),inset 0 0 28px rgba(0,220,220,.08);">' '<div style="background:linear-gradient(90deg,#ff5d8f,#ffd166,#5ce1e6,#b58cff);-webkit-background-clip:text;color:transparent;font:bold 23px Arial;margin-bottom:12px;">HAPPY DIWALI ✨</div>' '<div style="display:inline-block;text-align:left;color:white;line-height:1.05;' 'font:13px Consolas, Courier New, monospace;white-space:nowrap;">' + artwork + '</div>' '<div style="background:linear-gradient(90deg,#ff8fab,#ffd166,#6ee7d8);-webkit-background-clip:text;color:transparent;font:bold 14px Arial;margin-top:12px;">' 'May your life shine bright! | CLCODING</div></div>') display(HTML(html)) last = now next_frame += frame_dt delay = next_frame - time.perf_counter() if delay > 0: time.sleep(delay) else: next_frame = time.perf_counter() except KeyboardInterrupt: pass finally: clear_output(wait=True) display(HTML('<h3 style="color:#e6a700">Shubh Deepavali! — CLCODING</h3>')) # Run the animation. Adjust these settings as needed. run_notebook(duration=25, fps=12, width=84, height=30)

Popular Posts

Categories

100 Python Programs for Beginner (119) AI (359) Android (25) AngularJS (1) API (2) Assembly Language (2) aws (31) Azure (12) BI (10) book (1) Books (359) Bootcamp (20) C (78) C# (12) C++ (83) cloud (1) Course (97) Coursera (307) Cybersecurity (37) data (10) Data Analysis (48) Data Analytics (31) data management (16) Data Science (443) Data Strucures (21) Deep Learning (228) Django (17) Downloads (3) edx (21) Engineering (15) Euron (30) Events (9) Excel (24) Finance (13) flask (4) flutter (1) FPL (17) Gadgets (1) Generative AI (80) Git (14) Google (55) Hadoop (3) HTML Quiz (1) HTML&CSS (48) IBM (43) IoT (3) IS (25) Java (99) Leet Code (4) Machine Learning (417) Meta (24) MICHIGAN (5) microsoft (13) Nvidia (8) Pandas (16) PHP (20) Projects (36) Python (1405) Python Bug (1) Python Coding Challenge (1303) Python Library (29) Python Mathematics (24) Python Mistakes (51) Python Pattern Challenge (38) Python Quiz (685) Python Tips (115) Python Turtle (1) Questions (3) R (72) React (7) Scripting (3) security (5) Selenium Webdriver (4) Software (21) SQL (56) Udemy (24) UX Research (1) web application (11) Web development (9) web scraping (3)

Followers

Python Coding for Kids ( Free Demo for Everyone)