119 lines
4.7 KiB
GDScript3
119 lines
4.7 KiB
GDScript3
|
|
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)
|