"""
File: twinkleStarSketch.py
Date: 2026-09-24
Author: klp (standalone rewrite of PWPointSketch2.py)
Purpose: Twinkling star field — single-file Processing.py sketch with no external
         class dependencies (PWColor, PWPoint, PWSinusoid, PWGrid are not needed).

=== What This Sketch Does ===
    Paints a deep-purple night sky filled with 100 randomly placed yellow-family
    stars.  Clicking the canvas toggles a sinusoidal twinkling effect: each star's
    alpha (opacity) follows a sine wave with a unique phase offset so the stars
    sparkle independently rather than pulsing all at once.

=== Key Concepts for Novices ===

    1. HSB Color Mode
       colorMode(HSB, 360, 100, 100, 100) tells Processing to interpret every
       color as four components:
         Hue        (H) — 0–360  position on the color wheel (red=0, yellow=60,
                                 green=120, cyan=180, blue=240, magenta=300)
         Saturation (S) — 0–100  how vivid the color is  (0 = gray, 100 = pure color)
         Brightness (B) — 0–100  how much light is in the color (0=black, 100=full)
         Alpha      (A) — 0–100  how opaque the color is  (0=invisible, 100=solid)
       After colorMode() is set, EVERY call to stroke(), fill(), background(), and
       color() uses those same ranges for the rest of the sketch.

    2. Storing Stars as Python Dictionaries
       Each star is a plain dict instead of a class instance.  This keeps the
       sketch self-contained.  star['sw'] is the stroke weight (visual size);
       star['offset'] is that star's individual position on the sine wave.

    3. Sinusoidal Twinkling — the Math
       We want each star's alpha to oscillate smoothly between a dim value and a
       bright value over time.  A sine wave naturally does this:

           a = A * sin(B * t) + C

       where:
         t      = the current "time" input: frameCount + the star's unique offset
         A      = amplitude  = (max - min) / 2   — half the swing of the wave
         C      = center     = (max + min) / 2   — the value the wave oscillates around
         B      = angular frequency = TWO_PI / period — controls how fast it cycles

       Example with min=30, max=100, period=90:
         A = (100 - 30) / 2 = 35          (alpha swings ±35 from the center)
         C = (100 + 30) / 2 = 65          (center alpha is 65)
         B = TWO_PI / 90   ≈ 0.0698       (one full cycle every 90 frames)
         → alpha oscillates between 30 (dim) and 100 (bright) every ~3 seconds at 30 fps

    4. Phase Offsets
       Every star stores a random offset (0–89 frames).  Adding this to frameCount
       before evaluating the sine places each star at a different point in the wave
       cycle, so they never all dim or brighten simultaneously.

=== How to Run ===
    Open Processing, select Python mode, paste or open this file, and press Run.
    No additional files or imports are required.
"""

# ── Canvas ────────────────────────────────────────────────────────────────────
CW = 810    # canvas width  in pixels
CH = 700    # canvas height in pixels

# ── Night-sky background color ────────────────────────────────────────────────
# Deep purple, equivalent to hex #5a1485  (R=90, G=20, B=133).
# HSB conversion:  H ≈ 277°,  S ≈ 85%,  B ≈ 52%
SKY_H = 277
SKY_S = 85
SKY_B = 52

# ── Star appearance ───────────────────────────────────────────────────────────
NUM_STARS     = 100   # total stars in the sky
MIN_STAR_SIZE =   3   # smallest star (stroke weight in pixels)
MAX_STAR_SIZE =   8   # largest  star (stroke weight in pixels)

# All stars share the yellow hue (60° on the color wheel).
# Randomising saturation and brightness makes some look warm-white,
# others deep gold, and others vivid yellow.
STAR_HUE = 60

# ── Twinkling sine-wave parameters ────────────────────────────────────────────
# Alpha oscillates between these two extremes when twinkling is on.
TWINKLE_ALPHA_MIN = 30    # dimmest  point  (0–100)
TWINKLE_ALPHA_MAX = 100   # brightest point (0–100)

# Number of frames for one complete twinkle cycle.
# At 30 fps, period=90 means each star completes one pulse every ~3 seconds.
TWINKLE_PERIOD = 90

# ── Sketch-level state (mutated at runtime) ────────────────────────────────────
stars          = []      # list of star dicts; built in setup()
should_twinkle = False   # toggled on/off by mouse click
font_size      = 20

# Sine-wave coefficients derived in setup() from the constants above.
# Declared here so draw() can see them without needing 'global' on every frame.
twinkle_A = 0.0    # amplitude  = (max - min) / 2
twinkle_C = 0.0    # center     = (max + min) / 2
twinkle_B = 0.0    # angular frequency = TWO_PI / period


# =============================================================================
# SETUP — called once when the sketch starts
# =============================================================================
def setup():
    global stars, twinkle_A, twinkle_C, twinkle_B

    print("...setup...")
    size(CW, CH)

    # HSB color mode with scale (360, 100, 100, 100).
    # Must be called before any drawing or color commands so Processing knows
    # how to interpret every (h, s, b, a) tuple in this sketch.
    colorMode(HSB, 360, 100, 100, 100)

    frameRate(30)
    textSize(font_size)
    textAlign(LEFT)

    # ── Derive sinusoid coefficients from the desired alpha range ─────────────
    # These are computed once here rather than every frame to avoid repeating
    # the arithmetic 30 times per second.
    twinkle_A = (TWINKLE_ALPHA_MAX - TWINKLE_ALPHA_MIN) / 2.0   # 35.0
    twinkle_C = (TWINKLE_ALPHA_MAX + TWINKLE_ALPHA_MIN) / 2.0   # 65.0
    twinkle_B = TWO_PI / TWINKLE_PERIOD                          # ≈ 0.0698 rad/frame

    # ── Create the star list ──────────────────────────────────────────────────
    # Each star is a plain Python dict.  Using named keys makes the code
    # readable without requiring a class:
    #   'x', 'y'     — fixed screen position chosen randomly at startup
    #   'sw'         — stroke weight (the star's apparent size in pixels)
    #   'h','s','b'  — HSB color; hue is fixed at STAR_HUE (yellow family)
    #   'offset'     — random phase shift so each star twinkles independently
    stars = []
    for i in range(NUM_STARS):
        star = {
            'x':      random(CW),
            'y':      random(CH),
            'sw':     random(MIN_STAR_SIZE, MAX_STAR_SIZE),
            'h':      STAR_HUE,
            's':      random(0, 100),          # saturation: 0=pale white, 100=vivid yellow
            'b':      random(80, 100),         # brightness: always fairly lit
            'offset': int(random(TWINKLE_PERIOD))   # 0 to TWINKLE_PERIOD-1
        }
        stars.append(star)
    #end for loop
#end setup


# =============================================================================
# DRAW — called 30 times per second; redraws the full canvas each frame
# =============================================================================
def draw():

    # Clear the canvas with the night-sky color each frame.
    # Without this, every new star position would be painted ON TOP of the last,
    # leaving a permanent smear of dots instead of a clean twinkle.
    background(SKY_H, SKY_S, SKY_B)

    for star in stars:
        if should_twinkle:
            # ── Sinusoidal alpha ─────────────────────────────────────────────
            # Sample the wave at (frameCount + offset).
            # Adding the offset is what makes each star land at a different
            # point in the cycle: star A might be brightening while star B
            # is dimming, even though they share the same wave shape.
            alpha = twinkle_A * sin(twinkle_B * (frameCount + star['offset'])) + twinkle_C
        else:
            # Twinkling disabled → every star is fully opaque
            alpha = 100

        # Set the stroke color with this frame's computed alpha.
        # Each star keeps its hue, saturation, and brightness constant;
        # only the alpha changes per frame.
        stroke(star['h'], star['s'], star['b'], alpha)
        strokeWeight(star['sw'])
        point(star['x'], star['y'])   # draw a single dot at the star's position
    #end for loop

    # ── Reset drawing state before the text overlay ───────────────────────────
    # If we leave strokeWeight at a large value, any text rendering that happens
    # to use stroke() would also be thick.  Resetting to 1 is defensive housekeeping.
    noStroke()
    strokeWeight(1)

    # White text: in HSB mode, hue=0 + saturation=0 = achromatic; brightness=100 = white.
    fill(0, 0, 100, 100)
    text("Click canvas to toggle star twinkling!", 10, font_size * 1.5)
    text("Star twinkling: " + str(should_twinkle), 10, font_size * 3)
#end draw


# =============================================================================
# MOUSE INPUT — toggles twinkling each time the canvas is clicked
# =============================================================================
def mousePressed():
    global should_twinkle

    print("...mousePressed...")

    # Guard: only respond to clicks inside the canvas boundary.
    # mouseX and mouseY are Processing globals tracking the current mouse position.
    if 0 <= mouseX <= CW and 0 <= mouseY <= CH:
        should_twinkle = not should_twinkle   # flip the boolean
        print("...toggling should_twinkle: " + str(should_twinkle))
#end mousePressed
