class_name DimensionalBreach extends Node2D # #125: the hole itself, in world space. # # The prologue's warp was a few lines turning around a point. This is built # on the same idiom as BlackHoleVisual - an empty core with an accretion # disc of counter-rotating ARC SEGMENTS around it, hotter and thicker toward # the centre - because that is the part of the black-hole planet that reads # well, and a breach and a black hole should look like the same kind of # thing. # # What it adds over that planet is growth and infall. A planet's disc is # steady; this one opens from nothing, drags streamers in from well outside # its own disc, and keeps accelerating, so the picture says "this is # happening now" rather than "this has always been here". # # grow() drives everything from one 0..1 number, so the director never has # to know about rings or streamers - it says how far along the breach is and # this decides what that looks like. const CORE_RADIUS: float = 54.0 const RING_COUNT: int = 6 const RING_STEP: float = 19.0 const DISC_COLOR: Color = Color(0.55, 0.15, 0.85) const DISC_HOT_COLOR: Color = Color(0.92, 0.62, 1.0) const RIM_COLOR: Color = Color(0.98, 0.88, 1.0) # Streamers of matter falling in from outside the disc. They are what makes # the hole look like it is PULLING rather than merely spinning. const STREAMER_COUNT: int = 14 const STREAMER_REACH: float = 5.2 const STREAMER_STEPS: int = 14 # 0 = closed, 1 = fully open. Everything below is scaled from it. var progress: float = 0.0 var disc_phase: float = 0.0 func grow(value: float) -> void: progress = clampf(value, 0.0, 1.0) queue_redraw() func horizon_radius() -> float: return CORE_RADIUS * progress func disc_radius() -> float: return (CORE_RADIUS + float(RING_COUNT - 1) * RING_STEP) * progress func _process(delta: float) -> void: # The spin accelerates as it opens - a hole that turned at a constant # rate while growing read as a picture being scaled up. disc_phase += delta * (0.6 + progress * 2.4) queue_redraw() func _draw() -> void: if progress <= 0.001: return var horizon: float = horizon_radius() # The core is drawn as genuinely empty - an opaque disc of nothing, not a # dark tint. Same decision as BlackHoleVisual, and for the same reason: a # black hole with something visible in the middle stops reading as one. draw_circle(Vector2.ZERO, horizon, Color(0.0, 0.0, 0.0, 1.0)) _draw_streamers(horizon) for i in range(RING_COUNT): var radius: float = (CORE_RADIUS + float(i) * RING_STEP) * progress # 1.0 at the innermost ring, fading outward - drives colour, weight # and spin, exactly as the planet's disc does. var t: float = 1.0 - float(i) / float(maxi(RING_COUNT - 1, 1)) var ring_color: Color = DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.7) # Alternating direction per ring reads as turbulence rather than as # one disc rotating rigidly. var spin: float = disc_phase * (0.3 + t * 0.9) * (1.0 if i % 2 == 0 else -1.0) for arc_i in range(2): var start: float = spin + float(arc_i) * PI + 0.15 var end: float = start + PI - 0.3 var points := PackedVector2Array() for j in range(25): var angle: float = lerpf(start, end, float(j) / 24.0) points.append(Vector2(cos(angle), sin(angle)) * radius) NeonDraw.draw_glowing_path(self, points, ring_color, (0.5 + t * 0.5) * progress, 0.45 + t * 0.4) # The rim: a bright, thin circle right on the horizon. It is what gives # the void an edge instead of a fade, and it is drawn last so nothing # else washes over it. if horizon > 2.0: draw_arc(Vector2.ZERO, horizon, 0.0, TAU, 48, Color(RIM_COLOR, 0.85 * progress), 3.0, true) draw_arc(Vector2.ZERO, horizon * 1.06, 0.0, TAU, 48, Color(DISC_HOT_COLOR, 0.4 * progress), 7.0, true) func _draw_streamers(horizon: float) -> void: # Each streamer is a spiral falling from outside the disc down onto the # horizon. They wind tighter as the breach opens, so the pull visibly # strengthens rather than the picture merely getting bigger. var outer: float = disc_radius() * STREAMER_REACH if outer <= horizon + 1.0: return for i in range(STREAMER_COUNT): var lead: float = disc_phase * 1.6 + TAU * float(i) / float(STREAMER_COUNT) var points := PackedVector2Array() for j in range(STREAMER_STEPS + 1): var t: float = float(j) / float(STREAMER_STEPS) # Cubic, so a streamer spends most of its length far out and # whips through the last stretch - which is what infall does. var radius: float = lerpf(outer, horizon, t * t * t) var angle: float = lead + t * TAU * (0.55 + progress * 0.85) points.append(Vector2(cos(angle), sin(angle)) * radius) var fade: float = (0.16 + 0.5 * progress) * (0.5 + 0.5 * sin(lead * 3.0)) NeonDraw.draw_glowing_path(self, points, DISC_COLOR.lerp(DISC_HOT_COLOR, 0.35), maxf(fade, 0.08), 0.3)