-- A simple animation of a bouncing ball.

local glu = glu()
local cv = canvas()
local gp = require "gplus"

local backclip = "back"     -- clip with background (fills canvas)
local ballclip = "ball"     -- clip with image of ball
local shadowclip = "shadow" -- clip with image of ball shadow
local minsize = 10          -- minimum diameter of ball
local maxsize = 200         -- maximum diameter of ball
local ballsize = 100        -- initial diameter of ball
local ballx, bally = 30, 30 -- initial ball position (top left corner of ballclip)
local direction = 315       -- initial direction (in degrees: 1 to 360)
local speed = 120           -- initial speed (pixels per sec)
local deltax, deltay        -- delta values for moving ball
local fps = 60              -- number of frames per second

--------------------------------------------------------------------------------

function CalculateDeltas()
    -- calculate the delta values for MoveBall
    deltax =  math.cos(direction * math.pi / 180) * (speed / fps)
    deltay = -math.sin(direction * math.pi / 180) * (speed / fps)
end

--------------------------------------------------------------------------------

function CreateBackground()
    cv.create(0, 0, backclip)
    cv.target(backclip)

    cv.blend(0)
    cv.rgba(180, 200, 255, 255)
    cv.fill()

    local textclip = "text"
    cv.rgba(0, 0, 255, 255)
    cv.font("default-bold", 10)
    cv.text(textclip, [[
Hit the up/down arrows to change the speed.
Hit the left/right arrows to change the ball size.
Hit the D key to change the direction.
Hit the F key to toggle full screen mode.
Click anywhere to exit.
]])
    -- draw text in backclip
    cv.blend(1)
    cv.paste(textclip, 10, 10)
    cv.delete(textclip)

    cv.target()
end

--------------------------------------------------------------------------------

function CreateBall(diameter)
    cv.create(diameter, diameter, ballclip)
    cv.target(ballclip)
    cv.blend(1)

    local d = diameter
    local x = 0
    local y = 0
    local gray = 128    -- initial gray level
    local r = (diameter+1)//2
    local grayinc = gray/(r-2)
    while true do
        cv.rgba(gray//1, gray//1, gray//1, 255)
        -- draw a solid circle by setting the line width to the radius
        cv.linewidth(r)
        cv.ellipse(x//1, y//1, d, d)
        d = d - 2
        r = r - 1
        if r < 2 then break end
        x = x + 0.5
        y = y + 0.5
        gray = gray + grayinc
        if gray > 250 then gray = 250 end
    end

    cv.optimize(ballclip)

    -- create ball shadow from ball clip
    cv.copy(0, 0, diameter, diameter, shadowclip)
    cv.target(shadowclip)
    -- replace non-transparent pixels with highly transparent black
    cv.rgba(0, 0, 0, 30)
    cv.replace("* * * !0")

    cv.target()
end

--------------------------------------------------------------------------------

function Refresh()
    -- draw opaque background (includes text)
    cv.blend(0)
    cv.paste(backclip, 0, 0)
    
    -- background is opaque so we can use faster blend to draw shadow and ball
    cv.blend(2)
    local x, y = (ballx+0.5)//1, (bally+0.5)//1
    cv.paste(shadowclip, x+ballsize//5, y+ballsize//5)
    cv.paste(ballclip, x, y)
    
    -- because the canvas is opaque and covers the entire viewport we can
    -- call cv.update rather than glu.update
    cv.update()
end

--------------------------------------------------------------------------------

function Initialize()
    local viewwd, viewht = glu.getview()
    -- ensure viewport has plenty of room for biggest ball
    if viewwd < maxsize+10 then viewwd = maxsize+10 end
    if viewht < maxsize+10 then viewht = maxsize+10 end
    glu.setview(viewwd, viewht)
    cv.create()
    CreateBackground()
    CreateBall(ballsize)
    CalculateDeltas()
    Refresh()
end

--------------------------------------------------------------------------------

function CheckViewSize()
    -- might need to resize canvas and/or viewport
    local cvwd, cvht = cv.getsize()
    local viewwd, viewht = glu.getview()
    if cvwd ~= viewwd or cvht ~= viewht then
        -- ensure viewport has plenty of room for biggest ball
        if viewwd < maxsize+10 then viewwd = maxsize+10 end
        if viewht < maxsize+10 then viewht = maxsize+10 end
        glu.setview(viewwd, viewht)
        cv.resize()
        CreateBackground()
        Refresh()
    end
end

--------------------------------------------------------------------------------

function MoveBall(framestart)
    ballx = ballx + deltax
    bally = bally + deltay
    
    -- check for ball hitting edge of canvas
    local cvwd, cvht = cv.getsize()
    if ballx + ballsize > cvwd then
        ballx = cvwd - ballsize
        deltax = -deltax
        -- bounce off right edge, so direction is 1..89 or 271..360
        direction = (540 - direction) % 360
    elseif ballx < 0 then
        ballx = 0
        deltax = -deltax
        -- bounce off left edge, so direction is 91..269
        direction = (540 - direction) % 360
    end
    if bally + ballsize > cvht then
        bally = cvht - ballsize
        deltay = -deltay
        -- bounce off bottom edge, so direction is 181..359
        direction = 360 - direction
    elseif bally < 0 then
        bally = 0
        deltay = -deltay
        -- bounce off top edge, so direction is 1..179
        direction = 360 - direction
    end
    
    Refresh()    
    -- pause to get the desired number of frames per second
    while glu.millisecs() - framestart < 1000/fps do end
end

--------------------------------------------------------------------------------

function EventLoop()
    while true do
        local framestart = glu.millisecs()
        local event = glu.getevent()
        if event == "key d none" then
            -- change direction of ball
            direction = math.random(1, 360)
            CalculateDeltas()
        elseif event == "key up none" then
            -- speed up
            speed = speed + speed/10
            if speed > 10000 then speed = 10000 end
            CalculateDeltas()
        elseif event == "key down none" then
            -- slow down
            speed = speed - speed/10
            if speed < 1 then speed = 1 end
            CalculateDeltas()
        elseif event == "key right none" then
            -- increase ball size
            if ballsize < maxsize then
                ballsize = ballsize + 1
                CreateBall(ballsize)
            end
        elseif event == "key left none" then
            -- decrease ball size
            if ballsize > minsize then
                ballsize = ballsize - 1
                CreateBall(ballsize)
            end
        elseif event == "key f none" then
            -- toggle full screen mode
            glu.setoption("fullscreen", 1 - glu.getoption("fullscreen"))
        elseif event:find("^cclick") then
            glu.exit()
        end
        CheckViewSize()
        MoveBall(framestart)
    end
end

--------------------------------------------------------------------------------

function Main()
    glu.settitle("Bouncing Ball")
    Initialize()
    EventLoop()
end

--------------------------------------------------------------------------------

status, err = xpcall(Main, gp.trace)
if err then glu.continue(err) end
-- the following code is always executed
cv.delete()
