using SixLabors.Fonts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; namespace KfChatDotNetBot.Commands.Kasino.Roulette; public static class RouletteAnimationGenerator { // European Wheel Sequence private static readonly int[] WheelNumbers = { 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, 24, 16, 33, 1, 20, 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26 }; /// /// Generates an animated roulette wheel that lands on the specified winning number /// /// The number (0-36) that the ball should land on /// A tuple containing the animation duration in seconds and the WebP animation bytes public static (int durationSeconds, byte[] animationBytes) GenerateAnimation(int winningNumber) { if (winningNumber < 0 || winningNumber > 36) { throw new ArgumentOutOfRangeException(nameof(winningNumber), "Winning number must be between 0 and 36"); } using var board = DrawWheelBase(); int fps = 20; int duration = Random.Shared.Next(6, 9); int totalFrames = fps * duration; using var animation = new Image(500, 500); // Find the index of the winning number in the wheel sequence int winningIndex = Array.IndexOf(WheelNumbers, winningNumber); if (winningIndex == -1) { throw new InvalidOperationException($"Winning number {winningNumber} not found in wheel sequence"); } // Set the "Journey" float endWheelRotation = 720f + Random.Shared.Next(0, 360); float sliceAngle = 360f / 37f; // The pocket index 'i' is located at (i * sliceAngle) degrees relative to wheel zero. // Our '0' pocket was drawn at -90 degrees. float pocketOffsetOnWheel = (winningIndex * sliceAngle) - 90; // This is where the ball MUST be at the end of the video float finalBallAngle = endWheelRotation + pocketOffsetOnWheel; // Render frames for (int i = 0; i < totalFrames; i++) { float progress = (float)i / totalFrames; float ease = 1f - MathF.Pow(1f - progress, 3); // Smooth stop // Wheel rotates Clockwise (Adding degrees) float currentWheelAngle = endWheelRotation * ease; // Ball rotates Counter-Clockwise (Starting high and subtracting) // We start with 5 extra laps (1800 degrees) and "go back" to the final angle float startBallAngle = finalBallAngle + 1800f; float currentBallAngle = startBallAngle - ((startBallAngle - finalBallAngle) * ease); var frame = new Image(500, 500); frame.Mutate(ctx => { // Draw Wheel using var rotatedBoard = board.Clone(b => b.Rotate(currentWheelAngle)); int ox = 250 - (rotatedBoard.Width / 2); int oy = 250 - (rotatedBoard.Height / 2); ctx.DrawImage(rotatedBoard, new Point(ox, oy), 1f); // Ball Radius (Physics) float dropT = MathF.Max(0, (progress - 0.7f) / 0.3f); float radius = 230 - (45 * MathF.Pow(dropT, 2)); float rads = currentBallAngle * MathF.PI / 180; float bx = 250 + (radius * MathF.Cos(rads)); float by = 250 + (radius * MathF.Sin(rads)); ctx.Fill(Color.White, new EllipsePolygon(bx, by, 14)); }); frame.Frames.RootFrame.Metadata.GetWebpMetadata().FrameDelay = (uint)(1000 / fps); animation.Frames.AddFrame(frame.Frames.RootFrame); frame.Dispose(); } animation.Frames.RemoveFrame(0); using var ms = new MemoryStream(); animation.SaveAsWebp(ms, new WebpEncoder { FileFormat = WebpFileFormatType.Lossy, Quality = 50 }); return (duration, ms.ToArray()); } private static Image DrawWheelBase() { var img = new Image(500, 500); float centerX = 250, centerY = 250, outerRadius = 245, innerRadius = 170, step = 360f / 37f; img.Mutate(ctx => { for (int i = 0; i < 37; i++) { float startAngle = i * step - (step / 2) - 90; var color = WheelNumbers[i] == 0 ? Color.Green : (i % 2 == 0 ? Color.DarkRed : Color.Black); var path = new PathBuilder().AddArc(centerX, centerY, outerRadius, outerRadius, 0, startAngle, step) .AddArc(centerX, centerY, innerRadius, innerRadius, 0, startAngle + step, -step).Build(); ctx.Fill(color, path); ctx.Draw(Color.Gold, 1, path); string text = WheelNumbers[i].ToString(); float textAngle = (startAngle + (step / 2)) * MathF.PI / 180; float tx = centerX + ((outerRadius + innerRadius) / 2) * MathF.Cos(textAngle); float ty = centerY + ((outerRadius + innerRadius) / 2) * MathF.Sin(textAngle); try { var font = SystemFonts.CreateFont("Arial", 14, FontStyle.Bold); ctx.DrawText( new DrawingOptions { Transform = Matrix3x2Extensions.CreateRotationDegrees(startAngle + (step / 2) + 90, new PointF(tx, ty)) }, text, font, Color.White, new PointF(tx - 6, ty - 9)); } catch { // Font loading failed, skip text rendering } } ctx.Fill(Color.DarkSlateGray, new EllipsePolygon(centerX, centerY, innerRadius - 5)); }); return img; } }