UI drawing panels for a soccer match. PitchCanvas draws the striped pitch and markings, GoalCanvas draws both goals, FootballerCanvas draws one player from procedural pose parts, and GoalReplayCanvas renders a captured goal replay with its own camera.
using Sandbox;
using Sandbox.UI;
using Socer;
using System;
using System.Collections.Generic;
using System.Linq;
// Same broadcast projection and procedural poses; no per-line/per-limb layout nodes.
public class PitchCanvas : Panel
{
public float Pixels { get; set; }
public MatchController Game { get; set; }
static readonly PitchPerspective.Line[] Markings=PitchPerspective.Markings().ToArray();
internal static Vector2 Project(Point point,float pixels,float height=0)
{
var p=PitchPerspective.Project(point);
return new(p.X*pixels,p.Y*pixels*HumanFigure.GroundScale-height*pixels*.77f*PitchPerspective.Scale(point));
}
public override void OnDraw(Painter painter)
{
using var drawingScope=painter.Scope();
painter.Scale(ScaleToScreen);
painter.Stroke=Stroke.None;
for(int i=0;i<10;i++)
{
painter.Fill=Color.Parse(i%2==0?"#24634d":"#286c53")??Color.Green;
painter.Quad(Project(new(i*10.5f,0),Pixels),Project(new((i+1)*10.5f,0),Pixels),
Project(new((i+1)*10.5f,68),Pixels),Project(new(i*10.5f,68),Pixels));
}
painter.Fill=Fill.None;painter.Stroke=Stroke.Solid(Color.Parse("#c2d8bb")??Color.White,1.5f);
foreach(var line in Markings)painter.Line(Project(line.A,Pixels),Project(line.B,Pixels));
painter.Stroke=Stroke.None;painter.Fill=Color.Parse("#d6e5c9")??Color.White;
painter.Circle(Project(new(11,34),Pixels),2);painter.Circle(Project(new(94,34),Pixels),2);painter.Circle(Project(new(52.5f,34),Pixels),2);
}
}
public class GoalCanvas : Panel
{
public float Pixels { get; set; }
static readonly PitchPerspective.Line[] Edges=PitchPerspective.Goal(0).Concat(PitchPerspective.Goal(105)).ToArray();
public override void OnDraw(Painter painter)
{
using var drawingScope=painter.Scope();
painter.Scale(ScaleToScreen);
painter.Fill=Fill.None;
foreach(var edge in Edges)
{
painter.Stroke=Stroke.Solid(Color.Parse(edge.Net?"#dae5df65":"#f1f3df")??Color.White,edge.Net?1:2.5f);
painter.Line(PitchCanvas.Project(edge.A,Pixels,edge.ZA),PitchCanvas.Project(edge.B,Pixels,edge.ZB));
}
}
}
public class FootballerCanvas : Panel
{
public Footballer Player { get; set; }
public MatchSimulation Match { get; set; }
public float Pixels { get; set; }
public bool Referee { get; set; }
readonly List<HumanFigure.Part> parts=new(24);
readonly List<ReplayFigure.Part> worldParts=new(24);
static readonly Dictionary<string,Color> Colors=new();
static Color Ink(string value)
{
if(!Colors.TryGetValue(value,out var color))Colors[value]=color=Color.Parse(value)??Color.White;
return color;
}
public override void OnDraw(Painter painter)
{
using var drawingScope=painter.Scope();
painter.Scale(ScaleToScreen);
if(Player==null || Match==null)return;
painter.Stroke=Stroke.None;
foreach(var part in HumanFigure.Pose(Player,Match,Referee,parts,worldParts))
{
var delta=part.B-part.A;float width=Math.Max(1.2f,part.Width*Pixels);
float length=Math.Max(width,delta.Length*Pixels);
using var scope=painter.Scope();
painter.Translate(12.5f+part.A.X*Pixels,12.5f+part.A.Y*Pixels);
painter.Rotate(MathF.Atan2(delta.Y,delta.X)*180/MathF.PI);
painter.Fill=Ink(part.Color);
painter.Rect(new Rect(0,-width/2,length,width),width*.45f);
if(part.Color=="#17282b")
{
painter.Fill=Color.White.WithAlpha(.25f);
painter.Rect(new Rect(length*.48f,-width*.26f,length*.35f,width*.12f),width*.06f);
}
// A soft rim and narrow reflection give the same silhouette a rounded
// surface without extra panels, textures, or per-player materials.
if(width>=2f && length>width*1.15f)
{
painter.Fill=Color.Black.WithAlpha(.14f);
painter.Rect(new Rect(width*.16f,width*.18f,length-width*.32f,width*.19f),width*.09f);
painter.Fill=Color.White.WithAlpha(.17f);
painter.Rect(new Rect(width*.18f,-width*.33f,length-width*.36f,width*.16f),width*.08f);
}
}
}
}
// The replay has its own perspective and immutable captured poses. It never
// replaces the live match camera or asks the simulation to run backwards.
public class GoalReplayCanvas : Panel
{
public GoalReplay Replay { get; set; }
public float WidthPx { get; set; }
public float HeightPx { get; set; }
static readonly PitchPerspective.Line[] Markings=PitchPerspective.Markings().ToArray();
static readonly Dictionary<string,Color> Colors=new();
static Vector2 V(Point p)=>new(p.X,p.Y);
static Color Ink(string code)
{
if(!Colors.TryGetValue(code,out var value))Colors[code]=value=Color.Parse(code)??Color.White;
return value;
}
public override void OnDraw(Painter painter)
{
using var drawingScope=painter.Scope();
painter.Scale(ScaleToScreen);
float w=Math.Max(1,WidthPx),h=Math.Max(1,HeightPx);
painter.Stroke=Stroke.None;painter.Fill=Ink("#1e4f40");painter.Rect(new Rect(0,0,w,h));
if(Replay==null || !Replay.Sample(out var a,out var b,out float t))return;
var scorer=GoalReplay.Mix(a.Actors[Replay.ScorerId].Position,b.Actors[Replay.ScorerId].Position,t);
float afterGoal=Math.Clamp((Replay.PlaybackTime-Replay.GoalTime-.15f)/1.45f,0,1);
afterGoal=afterGoal*afterGoal*(3-2*afterGoal);
var camera=new GoalReplayCamera(scorer,Replay.AttackDirection,w,h,afterGoal);
int direction=Replay.AttackDirection;
// Turf bands are drawn in world space, so they converge toward the goal.
for(int i=0;i<10;i++)
{
float x0=i*10.5f,x1=(i+1)*10.5f;
var p0=new Point(x0,0);var p1=new Point(x1,0);
var p2=new Point(x1,68);var p3=new Point(x0,68);
if(Math.Min(Math.Min(camera.Depth(p0),camera.Depth(p1)),Math.Min(camera.Depth(p2),camera.Depth(p3)))<.8f)continue;
painter.Fill=Ink(i%2==0?"#286850":"#2b7155");
painter.Quad(V(camera.Project(p0)),V(camera.Project(p1)),V(camera.Project(p2)),V(camera.Project(p3)));
}
painter.Fill=Fill.None;painter.Stroke=Stroke.Solid(Ink("#bdd7ba"),1.5f);
foreach(var edge in Markings)
{
if(camera.Depth(edge.A)<.8f || camera.Depth(edge.B)<.8f)continue;
painter.Line(V(camera.Project(edge.A)),V(camera.Project(edge.B)));
}
float goalX=direction>0?105:0;
foreach(var edge in PitchPerspective.Goal(goalX))
{
painter.Stroke=Stroke.Solid(Ink(edge.Net?"#b8d5c591":"#f5f5e1"),edge.Net?1f:3f);
painter.Line(V(camera.Project(edge.A,edge.ZA)),V(camera.Project(edge.B,edge.ZB)));
}
painter.Stroke=Stroke.None;
foreach(int i in Enumerable.Range(0,a.Actors.Length).OrderByDescending(i=>camera.Depth(a.Actors[i].Position)))
{
var first=a.Actors[i];var second=b.Actors[i];
if(!first.Visible || camera.Depth(first.Position)<.8f)continue;
Point position=GoalReplay.Mix(first.Position,second.Position,t);
float scale=camera.Scale(position);var basePoint=camera.Project(position);
if(basePoint.X< -150 || basePoint.X>w+150 || basePoint.Y< -150 || basePoint.Y>h+200)continue;
painter.Fill=Color.Black.WithAlpha(.24f);
painter.Rect(new Rect(basePoint.X-scale*.38f,basePoint.Y-scale*.07f,scale*.76f,scale*.14f),scale*.07f);
int count=Math.Min(first.Count,second.Count);
for(int j=0;j<count;j++)
{
var pa=first.Parts[j];var pb=second.Parts[j];
var worldFrom=GoalReplay.Mix(pa.A,pb.A,t);
var worldTo=GoalReplay.Mix(pa.B,pb.B,t);
float zFrom=pa.ZA+(pb.ZA-pa.ZA)*t,zTo=pa.ZB+(pb.ZB-pa.ZB)*t;
Point from=camera.Project(worldFrom,zFrom);
Point to=camera.Project(worldTo,zTo);
float width=Math.Max(1.5f,pa.Width*camera.Scale(GoalReplay.Mix(worldFrom,worldTo,.5f)));
var delta=to-from;
using var partScope=painter.Scope();
painter.Translate(from.X,from.Y);
painter.Rotate(MathF.Atan2(delta.Y,delta.X)*180/MathF.PI);
float length=Math.Max(width,delta.Length);
if(j>=count-2)
{
painter.Fill=Color.Black.WithAlpha(.32f);painter.Circle(new Vector2(0,0),width*.5f+1);
painter.Fill=Ink(pa.Color);painter.Circle(new Vector2(0,0),width*.5f);
continue;
}
painter.Fill=Color.Black.WithAlpha(.32f);
painter.Rect(new Rect(-1,-width*.5f-1,length+2,width+2),width*.48f);
painter.Fill=Ink(pa.Color);
painter.Rect(new Rect(0,-width*.5f,length,width),width*.45f);
if(pa.Color=="#17282b")
{
painter.Fill=Color.White.WithAlpha(.3f);
painter.Rect(new Rect(length*.46f,-width*.27f,length*.36f,width*.13f),width*.07f);
}
if(width>3 && length>width*1.15f)
{
painter.Fill=Color.Black.WithAlpha(.14f);
painter.Rect(new Rect(width*.16f,width*.18f,length-width*.32f,width*.19f),width*.09f);
painter.Fill=Color.White.WithAlpha(.17f);
painter.Rect(new Rect(width*.18f,-width*.33f,length-width*.36f,width*.16f),width*.08f);
}
}
}
var ball=GoalReplay.Mix(a.Ball,b.Ball,t);
float ballHeight=a.BallHeight+(b.BallHeight-a.BallHeight)*t;
if(camera.Depth(ball)>.8f)
{
var shadow=camera.Project(ball);var at=camera.Project(ball,ballHeight);
painter.Fill=Color.Black.WithAlpha(.32f);painter.Circle(V(shadow),Math.Max(2,camera.Scale(ball)*.13f));
painter.Fill=Color.White;painter.Circle(V(at),Math.Max(3,camera.Scale(ball)*.11f));
}
}
}