Sunday, July 29, 2012

Tutorial 10 - A Simple Dial Class


10.1 Your Personality and a Little Maths


This tutorial is based on some code that we ported from our iPhone App Personality Profile. In this App you use a dial to answer a number of questions and a profile is provided based on your selection. We used a dial because we wanted a touch friendly solution and this seemed like an intuitive approach. The App is FREE on iTunes so download it and compare the functionality of the Objective C version to our Lua implementation. The embedded video above gives you an idea of how it works but the effect is better understood when used on your iDevice.


The trick to working out how to move the dial is a mathematical one. In essence you need to convert the cartesian co-ordinates of your finger on the screen to equivalent polar co-ordinates.

It is about now that you realise that those maths lessons way back weren't completely useless and perhaps you should have paid more attention. Don't panic, all you need to know is that it is possible to convert from a point (x, y) to polar co-ordinates which consist of an angle and a distance. The angle tells you how much to rotate the dial and you can use the distance portion to work out whether a point is within a circle which is handy for detecting if a tap was on the dial.

To make life easy we created a function which does all the hard work. We have reproduced it below but note that you also  need the math.hypot() function which isn't available in the Codea version of Lua. Both of these functions (and more) are contained in the Math tab of RSLibrary.

We have made use of the new Codea dependency functionality so if you download the entire tutorial code in one file, then you also need to download RSLibrary and link it as a dependency to DialDemo. See Tutorial 9 if you want to understand dependencies better or see what is in RSLibrary. For this tute you only need the Math and Button tabs but you might as well grab the whole thing.

As an aside, a handy trick which we use in the math.polar function is to assign a default value to a parameter using "or". This works because nil evaluates to false in Lua. See the code below to understand how you can use this to overload functions.


function math.polar(x, y, originX, originY)
-- Usage: math.polar(x, y) - origin is assumed at (0, 0)
-- math.polar(x, y, originX, originY)
--
-- This function converts from cartesian co-ordinates (x, y) to
-- polar co-ordinates (distance, angle) using the two functions:
--
-- 1. distance = hypot(x, y) - the hypotenuse of a right-angle triangle; and
-- 2. angle = atan2(x, y) - is the arc tangent of y/x in radians.
--
-- Since nil evaluates to false in Lua, you can use "or" to assign a default value.
--
-- Reefwing Software (www.reefwing.com.au)
-- Version 1.0
local oX = originX or 0
local oY = originY or 0
local dx = x - oX
local dy = y - oY
local distance = math.hypot(dx, dy)
local angleInDegrees = math.deg(math.atan2(dy, dx))
-- Functions may return multiple results in Lua.
return distance, angleInDegrees
end
view raw Math_Polar.lua hosted with ❤ by GitHub


10.2 The Main Class

   
In this instance the Main class is just there to demonstrate the Dial Class. Both Landscape and Portrait orientations are supported using the orientationChanged() function.


--# Main
-- DialDemo
-- Define supported orientations
supportedOrientations(ANY)
-- Use this function to perform your initial setup
function setup()
-- Project Metadata
version = 1.0
saveProjectInfo("Description", "Codea Dial Demonstration")
saveProjectInfo("Author", "Reefwing Software")
saveProjectInfo("Date", "26th July 2012")
saveProjectInfo("Version", version)
print("DialDemo v"..version.."\n")
-- Keep an eye on Frame Rate (Frames Per Second, FPS) so we can
-- compare and contrast different approaches.
FPS = 0
watch("FPS")
-- Define a new dial = Dial(x, y, angle)
--
-- Where: (x, y) are the CENTER co-ordinates of the dial. (default alignment is CENTER).
-- angle = the current angle (in degrees) of the dial.
dial = Dial(WIDTH/2, HEIGHT/2, 0)
-- Create a button which will zero the dial
-- The Button Class is contained within RSLibrary
local rbX = WIDTH/2 - dial.width/2
local rbY = HEIGHT/2 - dial.height - 20
resetButton = Button("Reset", rbX, rbY, dial.width, 50)
resetButton.action = function() resetButtonTapped() end
end
-- Drawing Functions
function draw()
-- Update instantaneous FPS
FPS = math.round(1/DeltaTime)
-- This sets a black background color
background(blackColor)
-- Draw the dial and reset button
dial:draw()
resetButton:draw()
end
function orientationChanged(newOrientation)
-- If the iPad orientation changes we need to
-- adjust object co-ordinates.
local rbX = WIDTH/2 - dial.width/2
local rbY = HEIGHT/2 - dial.height - 20
dial.x, dial.y = WIDTH/2, HEIGHT/2
resetButton.x, resetButton.y = rbX, rbY
end
-- Handle Touches
function touched(touch)
dial:touched(touch)
resetButton:touched(touch)
end
function resetButtonTapped()
dial:reset()
end
view raw Main.lua hosted with ❤ by GitHub
      

10.3 The Dial Class


The Dial Class is where the magic happens. We only have a licence to use the knob and dial images in our App, so you are going to have to draw or obtain your own version. Strictly speaking you only need a knob image (the bit that moves). The class should handle any differences in size to our sprites.


--# Dial
Dial = class()
-- A Dial Class
-- Reefwing Software (www.reefwing.com.au)
--
-- 26 July 2012
-- Version 1.0
function Dial:init(x, y, angle)
-- These parameters are used to customise your Dial
self.x = x -- x screen co-ordinate for the dial
self.y = y -- y screen co-ordinate for the dial
self.alignment = CENTER -- Dial uses CENTER alignment
self.visible = true -- Boolean to indicate whether dial is drawn and touches handled
self.currentAngle = angle or 0 -- Current Angle of the dial in degrees (default 0)
dialImage = readImage("Dropbox:pDial200px")
knobImage = readImage("Dropbox:pKnob147px")
radius = knobImage.width/2 -- Radius of knob circle sprite
self.width = dialImage.width -- Defined by the dial sprite width
self.height = dialImage.height -- Defined by the dial sprite height
end
-- Draw function
function Dial:draw()
-- Codea does not automatically call this method
if self.visible then
sprite(dialImage, self.x, self.y)
pushMatrix()
rotateDialByDegrees(self.x, self.y, self.currentAngle)
sprite(knobImage, 0, 0)
popMatrix()
end
end
-- Touch Handler
function Dial:touched(touch)
-- Codea does not automatically call this method
--
-- The pointInCircle() function determines if the touch was
-- on the dial. This function is contained in RSLibrary.
--
-- math.polar(x, y) converts from cartesian co-ordinates (x, y) to polar
-- co-ordinates (distance, angle). We only use the returned angle not the
-- distance. This function is also contained in RSLibrary.
if self.visible and pointInCircle(touch.x, touch.y, self.x, self.y, radius) then
if touch.state == MOVING then
_, self.currentAngle = math.polar(touch.x, touch.y, self.x, self.y)
end
end
end
-- Utility Functions
function Dial:reset()
self.currentAngle = 0
end
function rotateDialByDegrees(centreX, centreY, angle)
translate(centreX, centreY)
rotate(angle)
end
view raw Dial.lua hosted with ❤ by GitHub
   

No comments:

Post a Comment