tl;dr I recently made a WatchMaker Android Wear watch face based on the ‘squircle’ (download here). This post covers the maths behind squircles and getting (x,y) co-ordinates for any given polar angle. Finally I reveal the secrets of functions in WatchMaker Lau script.
A couple of months ago I started to play with the WatchMaker Watch Face app for Android. This app makes it relatively simple to design your own watch faces for Android Wear. After my first foray in becoming a “watch [face] maker” I’ve gone on to customise other watch faces and also put together R∆D∆R, shown below.
As part of the WatchMaker app there is a Google+ Community which include a ‘request a watch face’ thread. A lot of the time people are just asking for copies of existing ‘real’ watches, which can result in beautiful replicas but don’t interest me much. However recently a request from Andrew Davis came in that intrigued me.
Squircles
Squircles are apparently the The Hottest Thing In Car Design Right Now and hitting Wikipedia you can find a basic equation to represent the path:
Given Watchmaker uses a Cartesian co-ordinate system with 0,0 at the centre (that is, with a = b = 0) you get Lamé’s special quartic. Delving into the recesses of my high schools maths I knew if I solved this equation with that of a line (using tan(ѳ) for a gradient I could get the intercept co-ordinates.
So basically:
and so
And cue much scribbling and crumpled paper you get…
and
Putting these equations into a spreadsheet and providing a range of angles gives:
Graphing in a spreadsheet it’s easy to adjust the variables and see the effect. In this case we only have a, the radius of a squircle. And it turns out a is not much to play with e.g. a = 20 and so on.
Wondering if I missed something I hit ‘google’ again and discovered on Wolfram Mathworld that there are ‘two incompatible definitions of the squircle’ (guess wikipedia needs updating), the second by Fernandez Guasti:
with squareness parameter s, where s=0 corresponds to a circle with radius k and s=1 to a square of side length 2k. This curve is actually semialgebraic, as it must be restricted to |x|,|y|≤k to exclude other branches.
Having scraped a solution for the first definition of a squircle I didn’t feel confident that I’d be able to do the same with the Guasti definition. Knowing of the existence of software for solving algebraic systems another ‘googling’ turned up the open source Maxima. So cue much keyboard bashing and head scratching to get:
and
The nice thing about Maxima is you can copy and paste solved equations directly into a spreadsheet and get them to work with little modification i.e. copying the x output can be pasted as =(k*sqrt(-sqrt(-4*tan(f)^2*s^2 + tan(f)^4 + 2*tan(f)^2 + 1) + tan(f)^2+1))/(sqrt(2)*tan(f)*s)
. To make your life even either you can use Named Ranges to define the constants. Putting some data in to this Google Sheet I was able to see how the definition of a Guasti Squircle renders, and importantly performs.
Something I wanted to do in Google Sheets, but couldn’t get the charting tools to do was to graph different Squircles with varying k and s values on the same x-y scatter graph, the idea being I could use this as the background image for my watch face, so exported to MS Excel I’ve dropped this file on to OneDrive if you want to have a look):
WatchMaker functions and Lau scripting
WatchMaker makes it easy to add primitive elements to your watch face, and a host of predefined variables you can drop in. In previous faces I’ve designed I’ve dropped these into an element property. For example, rotations based on second, minute, hour hand and other device readings can be used as part of the element properties. Below in an example where I’m using the watch battery charge level expressed as a rotation {br} as the degrees in a radar element.
So I could use the formula developed in the spreadsheet for the x, y position of an element using a rotation variable, but given I’ve got three rings and separate x and y values this didn’t seem clever particularly if I wanted to adjust the k and s values. Looking at the WatchMaker developer reference I could see you could define functions that return values, but despite my best efforts every time I tried to call these functions in a property nothing would happen. Trying to find a solution online I drew I blank, my breakthrough was to look at other people’s watches I’d imported into WatchMaker to see how they did it. Doing this I was able to finally work out what the documentation was trying to say … or at least what I think it should say. The solution was to define a set of global variables which are then triggered to update every millisecond or every second in a script file that runs on startup. The variables are then used in the element properties. I’ve included the script below after you can see the finished work, which you can download here.
-- setup some globals -- var_ms used to prefix on_millisecond variables var_s_s = 0.85 var_s_k = 230 var_ms_s_x = 0 var_ms_s_y = -var_s_k -- var_s used to prefix on_second variables var_m_s = 0.895 var_m_k = 180 var_s_m_x = 0 var_s_m_y = -var_m_k var_h_s = 0.94 var_h_k = 130 var_s_h_x = 0 var_s_h_y = -var_h_k -- adaptive layout for circular faces var_round_face = 0 if ({around}) then var_s_s = 0.7 var_s_k = 187 var_ms_s_x = 0 var_ms_s_y = -var_s_k var_m_s = 0.71 var_m_k = 147 var_s_m_x = 0 var_s_m_y = -var_m_k var_h_s = 0.72 var_h_k = 100 var_s_h_x = 0 var_s_h_y = -var_h_k var_round_face = 100 end -- things to do each millisecond function on_millisecond() var_ms_s_x = xpos({drss}, var_s_s, var_s_k) var_ms_s_y = ypos({drss}, var_s_s, var_s_k) end -- things to do every second function on_second() var_s_m_x = xpos({drm}, var_m_s, var_m_k) var_s_m_y = ypos({drm}, var_m_s, var_m_k) var_s_h_x = xpos({drh}, var_h_s, var_h_k) var_s_h_y = ypos({drh}, var_h_s, var_h_k) end -- function to calc x position function xpos(a, s, k) if (a == 0 or a == 180) then return 0 elseif (a == 90 or a == 270) then pos = k else pos = (k*math.sqrt(-math.sqrt(math.tan(math.rad(a-90))^4-4*s^2*math.tan(math.rad(a-90))^2+2*math.tan(math.rad(a-90))^2+1)+math.tan(math.rad(a-90))^2+1))/(math.sqrt(2)*s*math.tan(math.rad(a-90))) end if (a>0 and a <90) then return -pos elseif (a>=90 and a<270) then return pos else return -pos end end -- function to calc y position function ypos(a, s, k) if (a == 90 or a == 270) then return 0 elseif (a == 0 or a == 180) then pos = k else pos = (k*math.sqrt(-math.sqrt(math.tan(math.rad(a-90))^4-4*s^2*math.tan(math.rad(a-90))^2+2*math.tan(math.rad(a-90))^2+1)+math.tan(math.rad(a-90))^2+1))/(math.sqrt(2)*s) end if (a>=0 and a <90) then return -pos elseif (a>=90 and a<270) then return pos else return -pos end end
If you’ve made it this far, thank you 🙂