Jump to content

Animated Spinners

From Rosetta Code
Task
Animated Spinners
You are encouraged to solve this task according to the task description, using any language you may know.
Task

Create and display five spinners. One spinner in the middle and four spinners surrounding it. Each spinner is created by drawing radius lines around a center axis and then looping through the drawing to simulate a moving clock hand. When the loop is sped up, the illusion of a spinner is created. NOTE: The animated GIF example is actually slower than what you should be able to create. A fast animation will fill in more radial lines with a pleasing appearance.

Stretch goal

Extra credit for offsetting the spinners with mouse movement.

Slow Spinners
Slow Spinners
Fast Spinners
Fast Spinners
Spinners offset with mouse movement
Spinners offset with mouse movement

EasyLang

spRadius = 10
angle = 90
background 000
# define the spinners, each element is x, y, color
spData[][] = [ [ 50 50 955 ] [ 25 50 369 ] [ 75 50 309 ] [ 50 25 390 ] [ 50 75 930 ] ]
spCount = len spData[][]
# 
on timer
   clear
   for sp = 1 to spCount
      x = spData[sp][1]
      y = spData[sp][2]
      color spData[sp][3]
      move x y
      line x + spRadius * cos angle y + spRadius * sin angle
      angle -= 1
      if angle < 0
         angle += 360
      .
   .
   timer 0.001
.
timer 0

FreeBASIC

Const SIZE = 380
Const RADIUS = 60
Const SPINNERS = 5
Const SENSITIVITY = 0.5

Type SpinnerType
    x As Single
    y As Single
    angle As Single
    kolor As Integer
End Type

Dim s(0 To SPINNERS-1) As SpinnerType

Screenres SIZE, SIZE, 32
Windowtitle "Move the mouse to offset the spinners"

' Setup spinners with different positions and kolors
s(0).x = SIZE\2      : s(0).y = SIZE\2      : s(0).kolor = Rgb(  0,255,  0) 'lime
s(1).x = SIZE\2 + 50 : s(1).y = SIZE\2 + 50 : s(1).kolor = Rgb(255,128,  0) 'orange
s(2).x = SIZE\2 - 50 : s(2).y = SIZE\2 - 50 : s(2).kolor = Rgb(255,  0,  0) 'red
s(3).x = SIZE\2 + 50 : s(3).y = SIZE\2 - 50 : s(3).kolor = Rgb(255,255,  0) 'yellow
s(4).x = SIZE\2 - 50 : s(4).y = SIZE\2 + 50 : s(4).kolor = Rgb(255,255,255) 'white

Dim As Integer mx, my, i

For i = 0 To SPINNERS-1: s(i).angle = 0: Next

Do
    Getmouse mx, my
    
    Color , Rgb(44,45,42)
    Cls
    
    ' Draw background circle
    Circle (SIZE\2, SIZE\2), SIZE\2-25, Rgb(0,0,0), , , , F
    Circle (SIZE\2, SIZE\2), SIZE\2-25, Rgb(128,128,128)
    
    ' Update and draw each spinner
    For i = 0 To SPINNERS-1
        ' Mouse interaction with smoother movement
        If mx > s(i).x-RADIUS And mx < s(i).x+RADIUS Then
            If my > s(i).y-RADIUS And my < s(i).y+RADIUS Then
                s(i).x += Sgn(s(i).x - mx) * SENSITIVITY
                s(i).y += Sgn(s(i).y - my) * SENSITIVITY
            End If
        End If
        
        ' Draw new line
        Line (s(i).x, s(i).y)-(s(i).x + RADIUS * Cos(s(i).angle), _
        s(i).y + RADIUS * Sin(s(i).angle)), s(i).kolor
        
        ' Update angle
        s(i).angle += 0.25
    Next i
    
    Sleep 1
Loop Until Inkey = Chr(27) 'ESC

FutureBasic

Created with FutureBasic

_Window = 1
_WindowSize = 350

begin globals
  
  
  CAShapeLayerRef   gShapeLayer1
  CALayerRef        gLayer1
  short gLoopCounter1
  short gAngle1 = 1
  
  CAShapeLayerRef   gShapeLayer2
  CALayerRef        gLayer2
  short gLoopCounter2
  short gAngle2 = 1
  
  CAShapeLayerRef   gShapeLayer3
  CALayerRef        gLayer3
  short gLoopCounter3
  short gAngle3 = 1
  
  CAShapeLayerRef   gShapeLayer4
  CALayerRef        gLayer4
  short gLoopCounter4
  short gAngle4 = 1
  
  CAShapeLayerRef   gShapeLayer5
  CALayerRef        gLayer5
  short gLoopCounter5
  short gAngle5 = 1
  
  
  float gMouseX,gMouseY
  
  
end globals



local fn DrawSpinner(x as int,x1 as int,Angle as short, LoopCounter as short, ShapeLayer as CAShapeLayerRef) as short
  
  BezierPathRef     path
  path = fn BezierPathInit
  
  short EndX,EndY
  short RadiusLength = 60
  short CenterX = _WindowSize/2 + x
  short CenterY = _WindowSize/2 + x1
  
  
  // Move the spinner left or right with the horizontal mouse  position
  if (gMouseY > CenterY - RadiusLength & gMouseY < CenterY + RadiusLength )
    if (gMouseX > CenterX && gMouseX < CenterX + RadiusLength)  then CenterX = CenterX - 10
    if gMouseX < CenterX && gMouseX > CenterX - RadiusLength then CenterX = CenterX + 10
  end if
  
  // Move the spinner up or down with the vertical mouse position
  if (gMouseX > CenterX - RadiusLength & gMouseX < CenterX + RadiusLength )
    if (gMouseY > CenterY && gMouseY < CenterY + RadiusLength )  then CenterY = CenterY - 10
    if gMouseY < CenterY && gMouseY > CenterY - RadiusLength then CenterY = CenterY + 10
  end if
  
  
  LoopCounter++
  
  EndX = CenterX
  EndY = CenterY
  
  
  BezierPathMoveToPoint( path, fn CGPointMake(CenterX ,CenterY))
  
  
  select
    case Angle > 0 && Angle <= RadiusLength // top to 90 degrees
      EndX = CenterX + LoopCounter
      EndY = CenterY  - RadiusLength + LoopCounter
    case Angle > 0 && Angle > RadiusLength && Angle <= RadiusLength * 2 // 90 degrees to bottom
      EndX = CenterX + RadiusLength - LoopCounter
      EndY = (CenterY + LoopCounter)
    case Angle > 0 && Angle > RadiusLength * 2 && Angle <= RadiusLength * 3 // bottom to 270 degrees
      EndX = CenterX - LoopCounter
      EndY = (CenterY + RadiusLength - LoopCounter )
    case Angle > 0 && Angle > RadiusLength * 3 && Angle <= RadiusLength * 4 // 270 degrees to top
      EndX = CenterX - RadiusLength + LoopCounter
      EndY = (CenterY -LoopCounter)
  end select
  
  
  BezierPathLineToPoint( path, fn CGPointMake(EndX , EndY ))
  CAShapeLayerSetPath( ShapeLayer, path )
  CAShapeLayerSetLineWidth( ShapeLayer, 2 )
  
  
  if LoopCounter => RadiusLength then LoopCounter = 0
  AppSetProperty( @"Loop", (CFTypeRef) fn StringWithFormat(@"%d",LoopCounter ))
  
  if Angle > RadiusLength * 4 then Angle = 1
  Angle++
  
  
end fn = Angle


local fn DoMouse
  
  CGPoint pt = fn EventLocationInView( _windowContentViewTag )
  gMouseX = pt.x
  gMouseY = pt.y
  
end fn


local fn BuildWindow
  
  CGRect        r
  SInt32        style
  
  style  = NSWindowStyleMaskTitled
  
  r = fn CGRectMake(0,0,_WindowSize,_WindowSize)
  window _window, @"Move the mouse to offset the spinners", r, style
  
  viewsetflipped(_windowContentViewTag,_true)
  WindowSetAcceptsMouseMovedEvents(_window,_true)
  WindowSubclassContentView(_Window)  // need this to detect mouse movement
  ViewSetWantsLayer( _windowContentViewTag, _true )
  
  // Draw an circle around the spinners
  CAShapeLayerRef   shapeLayer
  CALayerRef        layer
  layer = fn ViewLayer( _windowContentViewTag )
  shapeLayer = fn CAShapeLayerInit
  CALayerAddSublayer( layer, shapeLayer )
  CAShapeLayerSetStrokeColor( shapeLayer, fn ColorGray )
  BezierPathRef     path
  r = fn CGRectMake(25,25,_WindowSize - 50,_WindowSize - 50)
  path = fn BezierPathWithRoundedRect(r,_WindowSize - 50,_WindowSize + 150)
  CAShapeLayerSetPath( shapeLayer, path )
  CAShapeLayerSetLineWidth( shapeLayer, 1 )
  
  // Initialize drawing layers for the five spinners
  gLayer1 = fn ViewLayer( _windowContentViewTag )
  gShapeLayer1 = fn CAShapeLayerInit
  CALayerAddSublayer( gLayer1, gShapeLayer1)
  CAShapeLayerSetStrokeColor( gShapeLayer1, fn ColorGreen )
  gAngle1 = fn DrawSpinner( 0, 0, gAngle1, gLoopCounter1, gShapeLayer1)
  gLoopCounter1 = fn StringIntValue(fn AppProperty( @"Loop" ))
  
  gLayer2 = fn ViewLayer( _windowContentViewTag )
  gShapeLayer2 = fn CAShapeLayerInit
  CALayerAddSublayer( gLayer2, gShapeLayer2 )
  CAShapeLayerSetStrokeColor( gShapeLayer2, fn ColorOrange )
  gAngle2 = fn DrawSpinner( 50, 50, gAngle2, gLoopCounter2, gShapeLayer2 )
  gLoopCounter2 = fn StringIntValue(fn AppProperty( @"Loop" ))
  
  gLayer3 = fn ViewLayer( _windowContentViewTag )
  gShapeLayer3 = fn CAShapeLayerInit
  CALayerAddSublayer( gLayer3, gShapeLayer3 )
  CAShapeLayerSetStrokeColor( gShapeLayer3, fn ColorRed )
  gAngle3 = fn DrawSpinner( -50, -50, gAngle3, gLoopCounter3, gShapeLayer3 )
  gLoopCounter3 = fn StringIntValue(fn AppProperty( @"Loop" ))
  
  gLayer4 = fn ViewLayer( _windowContentViewTag )
  gShapeLayer4 = fn CAShapeLayerInit
  CALayerAddSublayer( gLayer4, gShapeLayer4 )
  CAShapeLayerSetStrokeColor( gShapeLayer4, fn ColorYellow )
  gAngle4 = fn DrawSpinner( 50, -50, gAngle4, gLoopCounter4, gShapeLayer4 )
  gLoopCounter4 = fn StringIntValue(fn AppProperty( @"Loop" ))
  
  gLayer5 = fn ViewLayer( _windowContentViewTag )
  gShapeLayer5 = fn CAShapeLayerInit
  CALayerAddSublayer( gLayer5, gShapeLayer5 )
  CAShapeLayerSetStrokeColor( gShapeLayer5, fn ColorWhite )
  gAngle5 = fn DrawSpinner( -50, 50, gAngle5, gLoopCounter5, gShapeLayer5)
  gLoopCounter5= fn StringIntValue(fn AppProperty( @"Loop" ))
  
end fn

void local fn DoDialog( ev as long )
  
  select ( ev )
    case _viewMouseMoved
      fn DoMouse
  end select
  
end fn

fn BuildWindow

on dialog fn DoDialog

local fn DrawLayers
  
  gAngle1 = fn DrawSpinner( 0, 0, gAngle1, gLoopCounter1, gShapeLayer1)
  gLoopCounter1 = fn StringIntValue(fn AppProperty( @"Loop" ))
  gAngle2 = fn DrawSpinner( 50, 50, gAngle2, gLoopCounter2, gShapeLayer2 )
  gLoopCounter2 = fn StringIntValue(fn AppProperty( @"Loop" ))
  gAngle3 = fn DrawSpinner( -50, -50, gAngle3, gLoopCounter3, gShapeLayer3 )
  gLoopCounter3 = fn StringIntValue(fn AppProperty( @"Loop" ))
  gAngle4 = fn DrawSpinner( 50, -50, gAngle4, gLoopCounter4, gShapeLayer4 )
  gLoopCounter4 = fn StringIntValue(fn AppProperty( @"Loop" ))
  gAngle5 = fn DrawSpinner( -50, 50, gAngle5, gLoopCounter5, gShapeLayer5)
  gLoopCounter5= fn StringIntValue(fn AppProperty( @"Loop" ))
  
end fn

fn AppSetTimer( .0001, @fn DrawLayers, _true ) // Determines the spinner speed
// Set the value to .01 to see that the spinners are actually lines rotating around a center axis.
// Set the value to .0001 to see the spinning illusion.

HandleEvents
Output:

Java

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.swing.JFrame;
import javax.swing.JPanel;

public final class AnimatedSpinners extends JPanel implements Runnable {

	public static void main(String[] args) {
		EventQueue.invokeLater( () -> {
			JFrame.setDefaultLookAndFeelDecorated(true);
			JFrame frame = new JFrame("Animated Spinners");
			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);		
			frame.add( new AnimatedSpinners() );
			frame.setLocationByPlatform(true);		
			frame.setResizable(false);
			frame.pack();					
			frame.setVisible(true);				
		} );
	}	
	
	@Override
    public void paintComponent(Graphics graphics) {
    	super.paintComponent(graphics);
    	Graphics2D graphics2D = (Graphics2D) graphics;
        graphics2D.setColor(Color.BLACK);
        graphics2D.fillOval(24, 24, SIZE - 48, SIZE - 48);
        
        spinners.forEach( s -> s.draw(graphics2D) );
    }
	
	@Override
	public void run() {
		repaint();
	}
	
	private AnimatedSpinners() {
		setPreferredSize( new Dimension(SIZE, SIZE) );
		setBackground(Color.DARK_GRAY);
		setDoubleBuffered(true);
		addMouseMotionListener(Spinner.listener);
		
		spinners = List.of( new Spinner(0, 0, Color.GREEN), new Spinner(-120, -120, Color.RED),
						    new Spinner(120, -120, Color.YELLOW), new Spinner(-120, 120, Color.WHITE),
						    new Spinner(120, 120, Color.ORANGE) );
		
		ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
		executorService.scheduleAtFixedRate(this, 0, 20, TimeUnit.MILLISECONDS);
	}
	
	private static final class Spinner {
		
		public Spinner(int aOffsetX, int aOffsetY, Color aColour) {
			centreX = SIZE / 2 + aOffsetX; 
			centreY = SIZE / 2 + aOffsetY;
			colour = aColour;
		}
		
		public void draw(Graphics2D graphics2D) {
			Point delta = mouseMovement();
			centreX += delta.x;
			centreY += delta.y;
			angle = ( angle + ANGLE_INCREMENT ) % TAU;
			final int endX = centreX + (int) ( RADIUS * Math.cos(angle) );
			final int endY = centreY + (int) ( RADIUS * Math.sin(angle) );
			graphics2D.setColor(colour);
			graphics2D.setStroke( new BasicStroke(2.0F) );
			graphics2D.drawLine(centreX, centreY, endX, endY);
		}
		
		private Point mouseMovement() {
			Point delta = new Point(0, 0);
			// Move the spinner left or right with the horizontal mouse  position
	        if ( mouseY > centreY - RADIUS && mouseY < centreY + RADIUS ) {
	            if ( mouseX > centreX && mouseX < centreX + RADIUS ) { delta.x= -10; }
	            if ( mouseX < centreX && mouseX > centreX - RADIUS ) { delta.x = +10; }
	        }
	        
			// Move the spinner up or down with the vertical mouse position
	        if ( mouseX > centreX - RADIUS && mouseX < centreX + RADIUS ) {
	            if ( mouseY < centreY && mouseY > centreY - RADIUS ) { delta.y = +10; }
	            if ( mouseY > centreY && mouseY < centreY + RADIUS ) { delta.y = -10; }
	        }
	        
	        // Ensure that the spinner remains in the black background circle
	        return ( Math.hypot(SIZE / 2 - centreX, SIZE / 2 - centreY) < SIZE / 2 - RADIUS - 24 - 10) ?
	            delta : new Point(0, 0);
		}		
		
		private int centreX, centreY;	
		
		private final Color colour;
		
		private static double angle;
		private static int mouseX, mouseY;
		
		private static final int RADIUS = 60;
		private static final double ANGLE_INCREMENT = Math.PI / 6.0;
		private static final double TAU = 2 * Math.PI;			
		private static final MouseAdapter listener = new MouseAdapter() {
		
			public void mouseMoved(MouseEvent event) {
                mouseX = event.getX();
                mouseY = event.getY();
            }
			
		};	
				
	}

	private List<Spinner> spinners;
	
	private static final int SIZE = 600;

}

Julia

Luxor's backend under Windows for movie creation has trouble with higher frame rates, so the frame rate of the output (nothing else) is modified with Clipchamp for the viewable gif file below.

""" https://rosettacode.org/wiki/Animated_Spinners """

using Luxor

const movie = Movie(600, 600, "lux_spinners")

function frame(scene::Scene, frame_num)
    rulers()
    sethue("grey40")
    rect(Point(-300, -300), 595, 595, action = :fill)
    sethue("black")
    circle(Point(0, 0), 296, action = :fill)
    colors = ["green", "orange", "red", "white", "yellow"]
    xs, ys = [0.0, 120.0, -120.0, 120.0, -120.0], [0.0, 120.0, -120.0, -120.0, 120.0]
    centers = [Point(xs[i], ys[i]) for i in eachindex(xs)]
    angle, r = mod1(frame_num * 7, 360), 84.0
    xs .= getindex.(centers, 1) .+ r * cospi(angle / 180)
    ys .= getindex.(centers, 2) .+ r * sinpi(angle / 180)
    for (i, c) in enumerate(centers)
        sethue(colors[i])
        setline(8)
        line(c, Point(xs[i], ys[i]), action = :stroke)
    end
end

animate(movie, [Scene(movie, frame, 1:720)], framerate = 1000, creategif = true, pathname = "lux_spinners.gif")
Output:

prior version

Sorry, it seems I somehow deleted this when moving Python into the right place...

""" https://rosettacode.org/wiki/Animated_Spinners """

using Gtk, Colors, Graphics, Cairo

""" Parameters for drawing the Gtk spinners app. """
struct Draw5Parameters
    dim::Vector{Int}
    xs::Vector{Float64}
    ys::Vector{Float64}
    deltas::Vector{Float64}
    previous::Vector{Float64}
    fps::Int
    colors::Vector{Colorant}
    Draw5Parameters() = new([800, 800], zeros(5), zeros(5), zeros(2), zeros(2), 150,
        [colorant"lightgreen", colorant"orange", colorant"red", colorant"white", colorant"yellow"])
end
const DP = Draw5Parameters()
const WIN = GtkWindow("Spinners", DP.dim[1], DP.dim[2])
const CANVAS = GtkCanvas()
push!(WIN, CANVAS)

@guarded Gtk.draw(CANVAS) do _
    """ Draw the spinners as a single line position. """
    ctx = getgc(CANVAS)
    h = height(ctx)
    w = width(ctx)
    if all(iszero, DP.xs) || w != DP.dim[1] || h != DP.dim[2]
        DP.dim .= w, h
        DP.xs .= [0.0, 50.0, -50.0, 50.0, -50.0] .* (w / 200) .+ (w / 2)
        DP.ys .= [0, 50, -50, -50, 50] .* (h / 200) .+ (h / 2)
        set_coordinates(ctx, BoundingBox(0, w, 0, h))
        set_source(ctx, colorant"gray")
        rectangle(ctx, 0, 0, w, h)
        fill(ctx)
        set_source(ctx, colorant"black")
        arc(ctx, h / 2, w / 2, w / 2, 0, 2π)
        fill(ctx)
    end
    r = w / 10
    for i in eachindex(DP.xs)
        set_line_width(ctx, 4.0)
        set_source(ctx, colorant"black")
        move_to(ctx, DP.xs[i], DP.ys[i])
        line_to(ctx, DP.xs[i] + r * DP.previous[1] * 1.1, DP.ys[i] + r * DP.previous[2] * 1.1)
        stroke(ctx)
        set_line_width(ctx, 2.0)
        set_source(ctx, DP.colors[i])
        move_to(ctx, DP.xs[i], DP.ys[i])
        line_to(ctx, DP.xs[i] + r * DP.deltas[1], DP.ys[i] + r * DP.deltas[2])
        stroke(ctx)
    end
    DP.previous .= DP.deltas
end

""" Run the spinners by drawing serial rotations in the angle of the lines. """
function spin()
    angle = 0.0
    while true
        angle = mod1(angle + 100 * rand(), 360)
        DP.deltas .= cospi(angle / 180), sinpi(angle / 180)
        draw(CANVAS)
        show(CANVAS)
        sleep(1 / DP.fps)
    end
end

@async spin()
Output:


M2000 Interpreter

We handle refresh rate to animate without flickering. Mouse move when we press mouse.

Module Animated_Spinners {
	Cls #222222, 0
	move scale.x div 2, scale.y div 2
	R=min.data(scale.x, scale.y) *1/10
	Circle Fill 0,  R*3
	Z=0
	z1=pi/180*random(1,9)
	p2=2*pi
	' hold statement save the screen - as our background (including circle)
	' release statement write back the background
	X=scale.x div 2
	Y=scale.y div 2
	hold
	' 60 hz = 1000ms/60 cycles
	every 1000/60 {
		release
		Pen 15 {
			move X, Y
			draw angle Z, R
		}
		Pen 11 {
			move X+R, Y-R
			draw angle Z, R
		}	
		Pen 12 {
			move X-R, Y+R
			draw angle Z, R
		}	
		Pen 14 {
			move X+R, Y+R
			draw angle Z, R
		}	
		move X-R, Y-R
		draw angle Z, R, 13  ' specific color
		Z-=z1*random(1, 10)
		Z|mod p2
		Refresh 200
		if inkey$<>"" then exit
		if mouse then x=mouse.x : y=mouse.y
	}
	refresh 25
}
Animated_Spinners

Phix

Library: Phix/xpGUI
-- demo\rosetta\Animated_Spinners.exw
with javascript_semantics
requires("1.0.6") -- (not yet shipped)
include xpGUI.e

constant title = "Animated Spinners"

atom angle = 0, 
     dpt = 1 -- degrees per tick

procedure redraw(gdx canvas, integer w,h)
    integer r = floor(min(w,h)*0.45),
            cx = floor(w/2),
            cy = floor(h/2)
    gCanvasCircle(canvas, cx, cy, r, true, colour:=XPG_BLACK)
    atom ar = angle*XPG_DEG2RAD,
         ds = r*sqrt(2)/4,
         dx = r/2.5*cos(ar),
         dy = r/2.5*sin(ar)
    for s in {{  0,  0,XPG_GREEN},
              {-ds,-ds,XPG_RED},
              {+ds,-ds,XPG_YELLOW},
              {+ds,+ds,XPG_ORANGE},
              {-ds,+ds,XPG_WHITE}} do
        atom {ax,ay,c} = s
        gCanvasLine(canvas,cx+ax,cy+ay,cx+ax+dx,cy+ay+dy,colour:=c)
    end for
end procedure

procedure timer_action(gdx timer)
    angle += dpt
    gdx canvas = gGetAttribute(timer,"USER_DATA")
    gRedraw(canvas)
end procedure

function key_handler(gdx dlg, integer c)
    switch c
        case '+','=': dpt = min(dpt+1,160)
        case '-','_': dpt = max(dpt-1,1)
    end switch
    gSetAttribute(dlg,"TITLE","%s [%g dpt]",{title,dpt})
    gRedraw(dlg)
    return XPG_DEFAULT
end function

gdx canvas = gCanvas(redraw,"BGCLR=DARK_GREY"),
    dialog = gDialog(canvas,title,"SIZE=510x540, MINSIZE=273x58"),
    timer = gTimer(timer_action, 10, true, canvas)
gSetHandler(dialog, `KEY`, key_handler)
gShow(dialog)
gMainLoop()

Python

This was inspired by the Java solution.

import pygame

WIDTH, HEIGHT = 350, 400
RADIUS = 50
FPS = 20


def calculate_angle_line_pos(
    start: tuple[int | float, int | float], radius: int | float, angle: int | float
):
    vec = pygame.math.Vector2(0, -radius).rotate((angle) % 360)
    return start[0] + vec.x, start[1] + vec.y


class Spinner:
    def __init__(
        self,
        pos: tuple[int, int],
        color: tuple[int, int, int],
        radius=10,
        speed=110,
        starting_angle=360,
        line_width=2,
    ):
        self.__pos = pos
        self.__radius = radius
        self.__angle = starting_angle
        self.__color = color
        self.__speed = speed
        self.__line_width = line_width

    def draw(self, surface: pygame.Surface):
        pygame.draw.line(
            surface,
            self.__color,
            self.__pos,
            calculate_angle_line_pos(self.__pos, self.__radius, self.__angle),
            self.__line_width,
        )

        self.__angle = (self.__angle - self.__speed) % 361


def main():
    pygame.init()
    pygame.display.set_caption("Spinners")

    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    clock = pygame.time.Clock()

    cx, cy = WIDTH // 2, HEIGHT // 2

    spinner = Spinner((cx - RADIUS, cy - RADIUS), (255, 0, 0), RADIUS)
    spinner2 = Spinner((cx, cy), (0, 0, 255), RADIUS)
    spinner3 = Spinner((cx + RADIUS, cy - RADIUS), (255, 255, 0), RADIUS)
    spinner4 = Spinner((cx - RADIUS, cy + RADIUS), (255, 255, 255), RADIUS)
    spinner5 = Spinner((cx + RADIUS, cy + RADIUS), (255, 175, 0), RADIUS)

    running = True

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        screen.fill((44, 45, 44))

        pygame.draw.circle(screen, (100, 100, 100), (cx, cy), 100 + RADIUS + 2)
        pygame.draw.circle(screen, (0, 0, 0), (cx, cy), 100 + RADIUS)

        spinner.draw(screen)
        spinner2.draw(screen)
        spinner3.draw(screen)
        spinner4.draw(screen)
        spinner5.draw(screen)

        pygame.display.flip()

        clock.tick(FPS)

    pygame.quit()


if __name__ == "__main__":
    main()
Output:

Wren

Library: DOME
import "dome" for Window, Process
import "graphics" for Canvas, Color
import "math" for Math
import "input" for Mouse

var Degrees3 = Num.pi / 60
var Rate = 1

class AnimatedSpinners {
    construct new(width, height) {
        Window.title = "Animated spinners"
        Window.resize(width, height)
        Canvas.resize(width, height)
        Canvas.cls(Color.darkgray)
        _w = width
        _h = height
        _r = 60
        _dx = [0, -180, 180, -180, 180]
        _dy = [0, -180, -180, 180, 180]
        _cols = [Color.green, Color.red, Color.white, Color.yellow, Color.brown]
    }

    drawHand(cx, cy, angle, color) {
        var x = cx + (_r * Math.cos(angle)).truncate
        var y = cy + (_r * Math.sin(angle)).truncate
        Canvas.line(cx, cy, x, y, color, 2)
    }

    init() {
        _frame = 0
        _cx = _w / 2
        _cy = _h / 2
        Canvas.circlefill(_cx, _cy, _cx - 20, Color.black)
        for (i in 0..4) drawHand(_cx + _dx[i], _cy + _dy[i], 0, _cols[i])
    }

    update() {
        _frame = _frame + 1
        if (_frame == 120) _frame = 0
    }

    draw(dt) {
        var angle = Degrees3 * _frame * Rate
        Canvas.cls(Color.darkgray)
        Canvas.circlefill(_w/2, _h/2, _w/2 - 20, Color.black)

        // Move the spinner left or right with the horizontal mouse  position
        if (Mouse.y > _cy - _r && Mouse.y < _cy + _r) {
            if (Mouse.x > _cx && Mouse.x < _cx + _r) _cx = _cx - 10
            if (Mouse.x < _cx && Mouse.x > _cx - _r) _cx = _cx + 10
        }

        // Move the spinner up or down with the vertical mouse position
        if (Mouse.x > _cx - _r && Mouse.x < _cx + _r) {
            if (Mouse.y > _cy && Mouse.y < _cy + _r) _cy = _cy - 10
            if (Mouse.y < _cy && Mouse.y > _cy - _r) _cy = _cy + 10
        }

        // Ensure the center spinner is always visible
        if (_cx < _r) _cx = _cx + _r
        if (_cx > _w - _r)_cx = _cx - _r
        if (_cy < _r) _cy = _cy + _r
        if (_cy > _h - _r) _cy = _cy - _r

        for (i in 0..4) drawHand(_cx + _dx[i], _cy + _dy[i], angle, _cols[i]) 
    }
}

// Pass Rate as a command line argument to speed up the spinner, otherwise 1 is used
if (Process.args.count == 3) Rate = Num.fromString(Process.args[2])
var Game = AnimatedSpinners.new(800, 800)

XPL0

def  R = 50.;
real X, Y, A;
int  X0, Y0, C, N;
[SetVid($13);           \320x200
X0:= [50, 150, 100,  50, 150];
Y0:= [50,  50, 100, 150, 150];
A:= 0.;
repeat  X:= R*Cos(A);
        Y:= R*Sin(A);
        for N:= 0 to 4 do
                [Move(X0(N), Y0(N));
                Line(fix(X)+X0(N), fix(Y)+Y0(N), 9+N);
                ];
        WaitForVSync;
        for N:= 0 to 4 do
                [Move(X0(N), Y0(N));
                Line(fix(X)+X0(N), fix(Y)+Y0(N), 0);
                ];
        A:= A + 0.05;
until   KeyHit;
]
Cookies help us deliver our services. By using our services, you agree to our use of cookies.