33 private PointF _centerWorldPx;
34 private float _radiusWorldPx;
35 private Color _lightColor;
36 private float _intensity = 1f;
37 private float _hotspotRadiusRatio = 0.06f;
38 private float _midpointRadiusRatio = 0.55f;
39 private float _midpointIntensityRatio = 0.35f;
40 private bool _isAntialias =
true;
41 private SKBlendMode _blendMode = SKBlendMode.Screen;
43 private bool _flickerEnabled;
44 private float _flickerAmount = 0.08f;
45 private int _flickerRefreshHz = 12;
46 private float _flickerPhaseSec;
47 private float _flickerMultiplier = 1f;
48 private long _lastFlickerRefreshTick;
59 public event Action<DirectRadialLight>?
Changed;
76 string? nickname =
null)
83 worldBounds: BoundsFromCenter(centerWorldPx, radiusWorldPx),
86 _lightColor = lightColor;
87 _centerWorldPx = centerWorldPx;
88 _radiusWorldPx = Math.Max(1f, radiusWorldPx);
96 get => _centerWorldPx;
99 if (ApproximatelyEqual(_centerWorldPx, value))
102 _centerWorldPx = value;
115 get => _radiusWorldPx;
118 var clamped = Math.Max(1f, value);
119 if (Math.Abs(_radiusWorldPx - clamped) < 0.001f)
122 _radiusWorldPx = clamped;
135 if (_lightColor.ToArgb() == value.ToArgb())
152 var clamped = Clamp01(value);
153 if (Math.Abs(_intensity - clamped) < 0.001f)
156 _intensity = clamped;
175 if (_blendMode == value)
188 get => _hotspotRadiusRatio;
191 var clamped = Clamp01(value);
192 if (Math.Abs(_hotspotRadiusRatio - clamped) < 0.001f)
195 _hotspotRadiusRatio = clamped;
205 get => _midpointRadiusRatio;
208 var clamped = Clamp01(value);
209 if (Math.Abs(_midpointRadiusRatio - clamped) < 0.001f)
212 _midpointRadiusRatio = clamped;
222 get => _midpointIntensityRatio;
225 var clamped = Clamp01(value);
226 if (Math.Abs(_midpointIntensityRatio - clamped) < 0.001f)
229 _midpointIntensityRatio = clamped;
242 if (_isAntialias == value)
245 _isAntialias = value;
260 get => _flickerEnabled;
263 if (_flickerEnabled == value)
266 _flickerEnabled = value;
267 _flickerMultiplier = 1f;
268 _lastFlickerRefreshTick = 0;
279 get => _flickerAmount;
282 var clamped = Math.Clamp(value, 0f, 1f);
283 if (Math.Abs(_flickerAmount - clamped) < 0.001f)
286 _flickerAmount = clamped;
297 get => _flickerRefreshHz;
298 set => _flickerRefreshHz = Math.Max(1, value);
344 if (destRectScreen.Width <= 0f || destRectScreen.Height <= 0f)
347 var canvas = backbuffer.
Canvas;
350 if (worldBounds.Width <= 0 || worldBounds.Height <= 0)
353 var rect =
new SKRect(destRectScreen.Left, destRectScreen.Top, destRectScreen.Right, destRectScreen.Bottom);
354 float scaleX = destRectScreen.Width / worldBounds.Width;
355 float scaleY = destRectScreen.Height / worldBounds.Height;
361 var center =
new SKPoint(
362 destRectScreen.Left + (_centerWorldPx.X - worldBounds.Left) * scaleX,
363 destRectScreen.Top + (_centerWorldPx.Y - worldBounds.Top) * scaleY);
365 var radius = Math.Max(Math.Abs(_radiusWorldPx * scaleX), Math.Abs(_radiusWorldPx * scaleY));
370 var hotspot = Math.Clamp(_hotspotRadiusRatio, 0f, 0.95f);
371 var midpoint = Math.Clamp(_midpointRadiusRatio, hotspot + 0.001f, 0.999f);
374 var inner = ToSkColor(_lightColor, effectiveIntensity);
375 var middle = ToSkColor(_lightColor, effectiveIntensity * _midpointIntensityRatio);
376 var outer = ToSkColor(_lightColor, 0f);
378 using var shader = SKShader.CreateRadialGradient(
381 new[] { inner, inner, middle, outer },
382 new[] { 0f, hotspot, midpoint, 1f },
383 SKShaderTileMode.Clamp);
385 using var paint =
new SKPaint
388 Style = SKPaintStyle.Fill,
393 canvas.DrawRect(rect, paint);
396 private void UpdateFlicker(
long tick)
398 if (!_flickerEnabled)
401 if (_lastFlickerRefreshTick == 0)
403 _lastFlickerRefreshTick = tick;
407 if (tick <= _lastFlickerRefreshTick)
410 var dt = HighResTimer.GetDuration(_lastFlickerRefreshTick, tick);
411 var refreshIntervalSec = 1f / _flickerRefreshHz;
413 if (dt < refreshIntervalSec)
416 _flickerPhaseSec += dt;
418 var primary = 0.5f + 0.5f * MathF.Sin(_flickerPhaseSec * MathF.Tau * _flickerRefreshHz);
419 var secondary = 0.5f + 0.5f * MathF.Sin(_flickerPhaseSec * MathF.Tau * (_flickerRefreshHz * 0.37f + 0.73f));
420 var mixed = primary * 0.65f + secondary * 0.35f;
422 _flickerMultiplier = 1f - _flickerAmount + mixed * _flickerAmount * 2f;
423 _lastFlickerRefreshTick = tick;
429 private void UpdateWorldBounds()
431 WorldBounds = BoundsFromCenter(_centerWorldPx, _radiusWorldPx);
435 private void OnChanged()
440 private static Rectangle BoundsFromCenter(PointF centerWorldPx,
float radiusWorldPx)
442 var radius = Math.Max(1f, radiusWorldPx);
443 var left = (int)MathF.Floor(centerWorldPx.X - radius);
444 var top = (int)MathF.Floor(centerWorldPx.Y - radius);
445 var right = (int)MathF.Ceiling(centerWorldPx.X + radius);
446 var bottom = (int)MathF.Ceiling(centerWorldPx.Y + radius);
448 return Rectangle.FromLTRB(left, top, right, bottom);
451 private static SKColor ToSkColor(Color color,
float intensity)
453 var alpha = (byte)Math.Clamp(color.A * Clamp01(intensity), 0f, 255f);
454 return new SKColor(color.R, color.G, color.B, alpha);
457 private static float Clamp01(
float value) => Math.Clamp(value, 0f, 1f);
459 private static bool ApproximatelyEqual(PointF left, PointF right)
461 return Math.Abs(left.X - right.X) < 0.001f &&
462 Math.Abs(left.Y - right.Y) < 0.001f;
SceneLayer? SceneLayer
Gets the scene layer to which this direct drawing is attached, or null if the drawing is in view mode...
Rectangle WorldBounds
Gets or sets the world-space bounds of this direct drawing in pixels.
DirectDrawingBase(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, SceneLayer? sceneLayer, View? view, Rectangle? screenBounds, Rectangle? worldBounds, string? nickname=null)
Initializes a new instance of the DirectDrawingBase class.
DirectRadialLight SetRadius(float radiusWorldPx)
Sets the light radius and returns this instance for chaining.
float FlickerAmount
Gets or sets how much flicker may alter the base intensity. Recommended range: 0 to 0....
int FlickerRefreshHz
Gets or sets how often flicker is recomputed and dirtied.
float MidpointIntensityRatio
Gets or sets the middle stop intensity multiplier from 0 to 1.
Action< DirectRadialLight >? Changed
Occurs when this light changes in a way tracked darkness overlays care about.
float EffectiveIntensity
Gets the current intensity after flicker has been applied.
bool IsAntialias
Gets or sets whether Skia antialiasing is enabled for the light draw.
float RadiusWorldPx
Gets the light radius in world pixels.
DirectRadialLight SetIntensity(float intensity)
Sets the light intensity and returns this instance for chaining.
override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
Color LightColor
Gets or sets the color and base alpha of the light.
DirectRadialLight(Color lightColor, RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, PointF centerWorldPx, float radiusWorldPx, string? nickname=null)
Initializes a new scene-layer radial light.
SKBlendMode BlendMode
Gets or sets the blend mode used when compositing the light over scene content.
float MidpointRadiusRatio
Gets or sets the radius ratio for the middle gradient stop.
float HotspotRadiusRatio
Gets or sets the radius ratio for the hottest inner portion of the gradient.
bool FlickerEnabled
Gets or sets whether the light should periodically vary its intensity.
override void Update(long tick)
Performs per-frame update logic for this direct drawing, including fade and reveal animations....
float Intensity
Gets or sets the overall intensity multiplier from 0 to 1.
PointF CenterWorldPx
Gets the light center in world pixels.
DirectRadialLight MoveTo(PointF centerWorldPx)
Moves the light center and returns this instance for chaining.
Represents a base class for managing a graphical backbuffer, and is the in-memory surface where rende...
SKCanvas Canvas
Gets the SkiaSharp canvas used for drawing operations on this backbuffer.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.