Skip to content

Instantly share code, notes, and snippets.

@loopspace
Created May 20, 2013 10:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loopspace/5611538 to your computer and use it in GitHub Desktop.
Save loopspace/5611538 to your computer and use it in GitHub Desktop.
Library Graphics Release v2.2a -A library of classes and functions relating to graphical things.
Library Graphics Tab Order
------------------------------
This file should not be included in the Codea project.
#ChangeLog
#Main
#Bezier
#Explosion
#Path
#TextNode
#View
#Fireworks
#DataSeries
#Frame
#TrendGraph
#Zoom
--[==[
-- Bezier path drawing
Bezier = class()
local __makeBezier = function(nsteps)
-- nsteps doesn't make a huge difference in the range 50,300
nsteps = nsteps or 150
local m = mesh()
m.shader = shader([[
//
// A basic vertex shader
//
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
//This is an output variable that will be passed to the fragment shader
varying highp vec2 vTexCoord;
varying highp float vWidth;
uniform float len;
uniform float width;
uniform float taper;
uniform float blur;
float swidth = width + blur;
float ewidth = taper*width - width;
uniform vec2 pts[4];
void main()
{
highp float t = position.y/len;
highp float w = smoothstep(0.,1.,t);
vWidth = w*ewidth + swidth;
highp float tt = 1.0 - t;
highp vec2 bpos = tt*tt*tt*pts[0] + 3.0*tt*tt*t*pts[1]
+ 3.0*tt*t*t*pts[2] + t*t*t*pts[3];
highp vec2 bdir = tt*tt*(pts[1]-pts[0])
+ 2.0*tt*t*(pts[2]-pts[1]) + t*t*(pts[3]-pts[2]);
bdir = vec2(bdir.y,-bdir.x);
bdir = vWidth*position.x*normalize(bdir);
bpos = bpos + bdir;
highp vec4 bzpos = vec4(bpos.x,bpos.y,0,1);
//Pass the mesh color to the fragment shader
vTexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
//Multiply the vertex position by our combined transform
gl_Position = modelViewProjection * bzpos;
}
]],[[
//
// A basic fragment shader
//
//This represents the current texture on the mesh
uniform lowp sampler2D texture;
//uniform highp float width;
uniform highp float blur;
uniform lowp vec4 colour;
//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;
varying highp float vWidth;
void main()
{
//Sample the texture at the interpolated coordinate
lowp vec4 col = colour;
highp float edge = blur/(vWidth+blur);
col.a = mix( 0., col.a,
smoothstep( 0., edge, min(vTexCoord.x,1. - vTexCoord.x) ) );
gl_FragColor = col;
}
]])
for n=1,nsteps do
m:addRect(0,(n-.5),1,1)
end
m.shader.len = nsteps
return m
end
local m = __makeBezier()
m.shader.blur = 2
function bezier(a,b,c,d,e)
if type(a) == "table" then
e = b
a,b,c,d = unpack(a)
end
if type(c) ~= "userdata" then
e = e or c
d = b
b = 2*a/3 + d/3
c = a/3 + 2*d/3
elseif type(d) ~= "userdata" then
e = e or d
d = c
b = 2*b/3
c = b + d/3
b = b + a/3
end
m.shader.taper = e or 1
m.shader.width = strokeWidth()
m.shader.colour = color(stroke())
m.shader.pts = {a,b,c,d}
m:draw()
end
function Bezier:init(...)
self:setPoints(...)
end
function Bezier:clone()
return Bezier(self.points)
end
function Bezier:makeDrawable(t)
t = t or {}
local m = __makeBezier(t.steps)
m.shader.taper = t.taper or 1
m.shader.blur = t.blur or 2
m.shader.width = t.width or strokeWidth()
m.shader.colour = t.colour or color(stroke())
m.shader.pts = self.points
self.curve = m
self.draw = function(self) self.curve:draw() end
end
function Bezier:draw(t)
self:makeDrawable(t)
self.curve:draw()
end
function Bezier:setPoints(a,b,c,d)
if type(a) == "table" then
a,b,c,d = unpack(a)
end
if not c then
d = b
b = 2*a/3 + d/3
c = a/3 + 2*d/3
elseif not d then
d = c
b = 2*b/3
c = b + d/3
b = b + a/3
elseif type(b) == "number" then
a,b,c,d = __hobby(a,b,c,d)
end
self.points = {a,b,c,d}
if self.curve then
self.curve.shader.pts = self.points
end
end
function Bezier:setStyle(t)
if not self.curve then
self:makeDrawable(t)
return
end
t = t or {}
if t.colour then
self.curve.shader.colour = t.colour
end
if t.width then
self.curve.shader.width = t.width
end
if t.taper then
self.curve.shader.taper = t.taper
end
if t.blur then
self.curve.shader.blur = t.blur
end
end
function Bezier:point(t)
local s = 1 - t
return s^3 * self.points[1]
+ 3*s*s*t * self.points[2]
+ 3*s*t*t * self.points[3]
+ t^3 * self.points[4]
end
function Bezier:tangent(t)
local s = 1 - t
return 3*s^2 * (self.points[2] - self.points[1])
+ 6*s*t * (self.points[3] - self.points[2])
+ 3*t^2 * (self.points[4] - self.points[3])
end
function Bezier:normal(t)
return self:tangent(t):rotate90()
end
function Bezier:unitNormal(t)
local pt = self:normal(t)
local l = pt:len()
if l == 0 then
return vec2(0,0)
else
return pt/l
end
end
function Bezier:unitTangent(t)
local pt = self:tangent(t)
local l = pt:len()
if l == 0 then
return vec2(0,0)
else
return pt/l
end
end
local ha = math.sqrt(2)
local hb = 1/16
local hc = (3 - math.sqrt(5))/2
local hd = 1 - hc
function __hobby(a,tha,phb,b)
local c = b - a
local sth = math.sin(tha)
local cth = math.cos(tha)
local sph = math.sin(phb)
local cph = math.cos(phb)
local alpha = ha * (sth - hb * sph) * (sph - hb * sth) * (cth - cph)
local rho = (2 + alpha)/(1 + hd * cth + hc * cph)
local sigma = (2 - alpha)/(1 + hd * cph + hc * cth)
return a,a + rho*c:rotate(tha)/3, b - sigma*c:rotate(-phb)/3,b
end
function QuickHobby(a,b,c,tha)
if type(a) == "table" then
tha = b
a,b,c = unpack(a)
end
local da = a:dist(b)
local db = b:dist(c)
local wa = vec2(1,0):angleBetween(b-a)
local wb = vec2(1,0):angleBetween(c-b)
local psi = wb - wa
if psi > math.pi then
psi = psi - 2*math.pi
end
if psi <= -math.pi then
psi = psi + 2*math.pi
end
local thb,phb,phc
if tha then
thb = -(2*psi + tha) * db / (2*db + da)
phb = - psi - thb
phc = thb
else
thb = - psi * db / (da + db)
tha = - psi - thb
phb = tha
phc = thb
end
return Bezier(__hobby(a,tha,phb,b)),Bezier(__hobby(b,thb,phc,c)),thb
end
function QHobbyGenerator(a,b)
local th
return function(c)
local p,q
p,q,th = QuickHobby(a,b,c,th)
a = b
b = c
return p,q
end
end
function QHobby(pts,th)
local n = #pts
if n == 1 then
return {}
end
if n == 2 then
th = th or 0
return {Bezier(pts[1], th, -th, pts[2] )},th
end
local a,b = pts[1],pts[2]
local p,q
local cvs = {}
for k=3,n do
p,q,th = QuickHobby(pts[k-2],pts[k-1],pts[k],th)
table.insert(cvs,p)
end
table.insert(cvs,q)
return cvs,th
end
function Hobby(pts,extra)
local z = {}
local d = {}
local omega = {}
local psi = {}
local it = {}
local ot = {}
local n = -1
local A = {}
local B = {}
local C = {}
local D = {}
local icurl = 1
local ocurl = 1
local theta = {}
local phi = {}
local rho ={}
local sigma = {}
local a = math.sqrt(2)
local b = 1/16
local c = (3 - math.sqrt(5))/2
local cpta = {}
local cptb = {}
local dten = 1
local curves = {}
if extra then
dten = extra.tension or 1
icurl = extra.inCurl or 1
ocurl = extra.outCurl or 1
end
for _,t in ipairs(pts) do
n = n + 1
if type(t) == "table" then
z[n] = t[1]
it[n] = t[2] or dten
ot[n] = t[3] or dten
else
z[n] = t
it[n] = dten
ot[n] = dten
end
end
if n < 1 then
return {}
end
if n == 1 then
local th,ph
if extra then
th = extra.inAngle
ph = extra.outAngle
end
if not th and ph then
th = -ph
elseif not ph and th then
ph = - th
elseif not th and not ph then
ph,th = 0,0
end
return {Bezier(__hobby(z[0],th,ph,z[1]))}
end
local ang
for k=0,n-1 do
d[k] = z[k]:dist(z[k+1])
omega[k] = vec2(1,0):angleBetween(z[k+1]-z[k])
if k > 0 then
ang = omega[k] - omega[k-1]
if ang > math.pi then
ang = ang - 2*math.pi
end
if ang < -math.pi then
ang = ang + 2*math.pi
end
psi[k] = ang
end
end
if extra then
theta[0] = extra.inAngle
phi[n] = extra.outAngle
end
for k=1,n-1 do
A[k] = d[k] * it[k+1] * it[k]^2
end
if theta[0] then
B[0] = 1
else
B[0] = ot[0]^3 * (3 * it[1] - 1) + icurl * it[1]^3
end
for k = 1,n-2 do
B[k] = d[k] * it[k+1] * it[k]^2 * (3 * ot[k-1] - 1) + d[k-1] * ot[k-1] * ot[k]^2 * (3 * it[k+1] - 1)
end
B[n-1] = d[n-1] * it[n] * it[n-1]^2 * (3 * ot[n-2] - 1) + d[n-2] * ot[n-2] * ot[n-1]^2 * (3 * it[n] - 1)
if not phi[n] then
B[n-1] = B[n-1] - d[n-2] * ot[n-2] * ot[n-1]^2 * (it[n]^3 + ocurl * ot[n-1]^3 * (3 * it[n] - 1)) / (it[n]^3 * (3 * ot[n-1] - 1) + ocurl * ot[n-1]^3)
end
if theta[0] then
C[0] = 0
else
C[0] = ot[0]^3 + icurl * it[1]^3 * (3 * ot[0] - 1)
end
for i=1,n do
C[i] = d[i-1] * ot[i-1] * ot[i]^2
end
if theta[0] then
D[0] = theta[0]
else
D[0] = - (ot[0]^3 + icurl * it[1]^3 * (3 * ot[0] - 1)) * psi[1]
end
for i=1,n-2 do
D[i] = - d[i] * it[i+1] * it[i]^2 * (3 * ot[i-1] - 1) * psi[i] - d[i-1] * ot[i-1] * ot[i]^2 * psi[i+1]
end
D[n-1] = - d[n-1] * it[n] * it[n-1]^2 * (3 * ot[n-2] - 1) * psi[n-1]
if phi[n] then
D[n-1] = D[n-1] - d[n-2] * ot[n-2] * ot[n-1]^2 * phi[n]
end
for i=1,n-1 do
B[i] = B[i-1] * B[i] - A[i] * C[i-1]
C[i] = B[i-1] * C[i]
D[i] = B[i-1] * D[i] - A[i] * D[i-1]
end
theta[n-1] = D[n-1]/B[n-1]
for i=n-2,1,-1 do
theta[i] = (D[i] - C[i] * theta[i+1])/B[i]
end
for i=1,n-1 do
phi[i] = -psi[i] - theta[i]
end
if not theta[0] then
theta[0] = (ot[0]^3 + icurl * it[1]^3 * (3 * ot[0] - 1)) / (ot[0]^3 * (3 * it[1] - 1) + icurl * it[1]^3) * phi[1]
end
if not phi[n] then
phi[n] = (it[n]^3 + ocurl * it[n-1]^3 * (3 * it[n] - 1)) / (it[n]^3 * (3 * ot[n-1] - 1) + ocurl * ot[n-1]^3) * theta[n-1]
end
local alpha
for i = 0,n-1 do
alpha = a * (math.sin(theta[i]) - b * math.sin(phi[i+1])) * (math.sin(phi[i+1]) - b * math.sin(theta[i])) * (math.cos(theta[i]) - math.cos(phi[i+1]))
rho[i] = (2 + alpha) / (1 + (1 - c) * math.cos(theta[i]) + c * math.cos(phi[i+1]))
sigma[i+1] = (2 - alpha) / (1 + (1 - c) * math.cos(phi[i+1]) + c * math.cos(theta[i]))
end
for i = 0,n-1 do
table.insert(curves,Bezier(
z[i],
z[i] + d[i]*rho[i] * vec2(math.cos(theta[i] + omega[i]), math.sin(theta[i] + omega[i]))/3,
z[i+1] - d[i] * sigma[i+1] * vec2(math.cos(omega[i] - phi[i+1]), math.sin(omega[i] - phi[i+1]))/3,
z[i+1]
))
end
return curves
end
cmodule.gexport {
QuickHobby = QuickHobby,
HobbyPoints = HobbyPoints,
QHobbyGenerator = QHobbyGenerator,
Hobby = Hobby,
QHobby = QHobby,
bezier = bezier
}
return Bezier
--]==]
--[[
ChangeLog
=========
v2.2a Added and developed Bezier class and bezier shader.
v2.1 Updated to latest version of cmodule. Cleaned up Main
v2.0 Split into separate projects: "Library Graphics" is a suite of
mainly graphical functions.
v1.0 Converted to use toadkick's cmodule for importing
and Briarfox's AutoGist for versionning.
It needs toadkick's code from
https://gist.github.com/apendley/5411561
tested with version 0.0.8
To use without AutoGist, comment out the lines in setup.
--]]
--[==[
-- Data Series
DataSeries = class()
local Colour = unpack(cimport "Colour",nil)
function DataSeries:init(name, length, min, max, symbol, symbolsize, thick, clr, value)
self.name = name
self.symbol = symbol
self.symbolsize = symbolsize
self.nextpt = 0
self.length = length
self.lineclr = clr
self.linethick = thick
self.points = {}
self.min = min
self.max = max
if self.min < 0 and self.max > 0 then
self.axis = true
self.acolour = Colour.shade(self.lineclr,50)
end
self.value = value
end
function DataSeries:addValue(y)
self.nextpt = self.nextpt + 1
if self.nextpt > self.length then
self.nextpt = 1
end
self.points[self.nextpt] = vec2(i, y)
end
function DataSeries:update()
if self.value then
self:addValue(self.value())
end
end
function DataSeries:addBreak()
self.nextpt = self.nextpt + 1
if self.nextpt > self.length then
self.nextpt = 1
end
self.points[self.nextpt] = nil
end
function DataSeries:draw(frame)
pushStyle()
local ox, oy, w, h, x, y, p, dx, dy
strokeWidth(self.linethick)
w = frame.x2 - frame.x1
h = frame.y2 - frame.y1
dx = w / (self.length + 1)
dy = h / (self.max - self.min)
pushMatrix()
translate(frame.x1, frame.y1)
clip(frame.x1, frame.y1, w, h)
if self.axis then
stroke(self.acolour)
line(0,-self.min*dy,w,-self.min*dy)
end
ox = 0
x = 0
stroke(self.lineclr)
for i = self.nextpt + 1, self.length do
x = x + dx
p = self.points[i]
if p ~= nil then
y = (p.y - self.min) * dy
if ox > 0 then
line(ox,oy,x,y)
end
ox = x
oy = y
if self.symbol == 1 then
noFill()
ellipse(x, y, self.symbolsize)
end
else
ox = 0
end
end
for i = 1, self.nextpt do
x = x + dx
p = self.points[i]
if p ~= nil then
y = (p.y - self.min) * dy
if ox > 0 then
line(ox,oy,x,y)
end
ox = x
oy = y
if self.symbol == 1 then
noFill()
ellipse(x, y, self.symbolsize)
end
else
ox = 0
end
end
popMatrix()
noClip()
popStyle()
end
return DataSeries
--]==]
--[==[
-- Explosion
local Explosion = class()
function Explosion:init(t)
t = t or {}
self.mesh = mesh()
local s = shader()
s.vertexProgram, s.fragmentProgram = expshader()
self.mesh.shader = s
self.mesh.texture = t.image
self.mesh.shader.friction = t.friction or .1
self.mesh.shader.separation = 0
local ft = t.factor or 10
self.mesh.shader.factor = ft
local vels = self.mesh:buffer("velocity")
local origin = self.mesh:buffer("origin")
local angv = self.mesh:buffer("angvel")
local lvl = self.mesh:buffer("level")
local m = t.rows or 20
local n = t.cols or 20
vels:resize(m*n*6)
origin:resize(m*n*6)
angv:resize(m*n*6)
lvl:resize(m*n*6)
local c = t.centre
local w,h
if type(t.image) == "string" then
local img = readImage(t.image)
w,h = img.width,img.height
else
w,h = t.image.width,t.image.height
end
local w = t.width or w
local h = t.height or h
local om = t.angularSpeed or 1/ft
local xx,y = c.x - w/2,c.y - h/2
local cl = vec2(w,h):len()/2
w,h = w/m,h/n
xx,y = xx+w/2,y+h/2
local r,th,sf,x,df,tth
sf = .3
df = math.random()
for i=1,m do
x = xx
for j = 1,n do
r = self.mesh:addRect(x,y,w,h)
self.mesh:setRectTex(r,(j-1)/n,(i-1)/m,1/n,1/m)
th = 2*noise(i*sf+df,j*sf+df)*math.pi
tth = 2*om*noise(j*sf+df,i*sf+df)*math.pi
for k=1,6 do
vels[6*r-k+1] = 20*(2-(c:dist(vec2(x,y))/cl))^2
*vec4(math.cos(th),math.sin(th),0,0)
origin[6*r-k+1] = vec2(x,y)
angv[6*r-k+1] = vec2(tth,0)
lvl[6*r-k+1] = 0
end
x = x + w
end
y = y + h
end
if t.trails then
local ntr = t.trailLength or 16
self.trails = mesh()
self.trails:setColors(255,255,255,255)
s = shader()
s.vertexProgram, s.fragmentProgram = expshader()
self.trails.shader = s
self.trails.texture = t.image
self.trails.shader.friction = t.friction or .1
self.trails.shader.factor = ft
self.trails.shader.separation = t.trailSeparation or .5
vels = self.trails:buffer("velocity")
origin = self.trails:buffer("origin")
angv = self.trails:buffer("angvel")
lvl = self.trails:buffer("level")
vels:resize(ntr*m*n*6)
origin:resize(ntr*m*n*6)
angv:resize(ntr*m*n*6)
lvl:resize(ntr*m*n*6)
local yy
xx,yy = c.x - (m-1)*w/2,c.y - (n-1)*h/2
for l=1,ntr do
y = yy
for i=1,m do
x = xx
for j = 1,n do
r = self.trails:addRect(x,y,w,h)
self.trails:setRectTex(r,(j-1)/n,(i-1)/m,1/n,1/m)
self.trails:setRectColor(r,255,255,255,l*127/ntr)
th = 2*noise(i*sf+df,j*sf+df)*math.pi
tth = 2*om*noise(j*sf+df,i*sf+df)*math.pi
for k=1,6 do
vels[6*r-k+1] = 20*(2-(c:dist(vec2(x,y))/cl))^2
*vec4(math.cos(th),math.sin(th),0,0)
origin[6*r-k+1] = vec2(x,y)
angv[6*r-k+1] = vec2(tth,0)
lvl[6*r-k+1] = l - ntr - 1
end
x = x + w
end
y = y + h
end
end
end
self.start = ElapsedTime
end
function Explosion:draw()
if not self.active then
return
end
pushStyle()
local time = ElapsedTime - self.start
if not self.paused then
if self.trails then
blendMode(SRC_ALPHA,ONE)
self.trails.shader.time = time
self.trails:draw()
end
self.mesh.shader.time = time
end
blendMode(NORMAL)
self.mesh:draw()
if not self.paused then
if self.stop and ElapsedTime > self.start + self.stop then
self:deactivate()
end
end
popStyle()
end
function Explosion:activate(t,s)
t = t or 0
self.start = ElapsedTime + t
self.stop = s
self.active = true
self.paused = false
end
function Explosion:deactivate()
self.active = false
end
function Explosion:pause()
if self.paused then
self.start = ElapsedTime - self.pausetime
self.paused = false
else
self.paused = true
self.pausetime = ElapsedTime - self.start
end
end
expshader = function()
return [[
//
// The explosion vertex shader
//
precision highp float;
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
uniform float time;
uniform float friction;
uniform float factor;
uniform float separation;
lowp vec4 gravity = vec4(0.,-1.,0.,0.);
mediump float mtime = max(0.,time)*factor;
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
// These are vertex buffers: initial velocity of the square,
// angular velocity,
// centre of square
attribute vec4 velocity;
attribute vec2 angvel;
attribute vec2 origin;
attribute float level;
//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
//varying mediump float vLevel;
// ODE: x'' = -friction x' + gravity
// Solution: A exp(- friction * time) + B + time*gravity/friction
// Initial conditions:
// A = gravity/(friction*friction) - x'(0)/friction
// B = x(0) -A
void main()
{
//Pass the mesh color to the fragment shader
vColor = color;
vTexCoord = texCoord;
//vLevel = level;
lowp vec4 pos;
mediump float t = mtime + level * separation;
lowp float angle = t*angvel.x;
highp vec4 A = gravity/(friction*friction) - velocity/friction;
highp vec4 B = vec4(origin,0.,0.) - A;
lowp mat2 rot = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
pos = (position - vec4(origin,0.,0.));
pos.xy = rot * pos.xy;
pos += exp(-t*friction)*A + B + t * gravity/friction;
if (level != 0. && t < 1.) pos = vec4(0.,0.,0.,1.);
//Multiply the vertex position by our combined transform
gl_Position = modelViewProjection * pos;
}
]],[[
//
// A basic fragment shader
//
//This represents the current texture on the mesh
uniform lowp sampler2D texture;
//The interpolated vertex color for this fragment
varying lowp vec4 vColor;
//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;
void main()
{
//Sample the texture at the interpolated coordinate
lowp vec4 col = texture2D( texture, vTexCoord );
col *= vColor;
//Set the output color to the texture color
gl_FragColor = col;
}
]]
end
return Explosion
--]==]
--[==[
-- Firework display
-- Authors: Stavrogin (original standalone program)
-- Andrew Stacey (conversion to "celebration" class)
-- Websites: http://pagantongue.posterous.com/fireworks
-- http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: unknown
local NUMSTARS = 50
local COLOURS = {
color(255, 0, 0, 125),
color(255, 227, 0, 125),
color(99, 255, 0, 125),
color(0, 140, 255, 125),
color(238, 0, 255, 125),
color(255, 156, 0, 125),
color(0, 255, 189, 125),
color(255, 0, 146, 125)
}
local Stars = 1
local See_Smoke = 1
local Firework_Parts = 100
local Life = 30
local Life_Variation = 100
local Air_Resistance = 0.1
local PartSize = 35
local PartSize_Variation = 50
local Velocity = 10
local Velocity_Variation = 100
local Color_Variation = 50
local Fireworks = class()
function Fireworks:init()
self.stars = {}
self.fireworks = {}
self.smokes = {}
for i=0,NUMSTARS do
self.stars[i] = {x=math.random(WIDTH),y=math.random(HEIGHT)}
end
self.active = false
end
function Fireworks:draw()
pushStyle()
noSmooth()
fill(0, 0, 0, 125)
rect(0,0,WIDTH,HEIGHT)
-- draw stars
if Stars == 1 then
for i=0, NUMSTARS do
fill(255, 255, 255, math.random(255))
rect(self.stars[i].x,
self.stars[i].y,
math.random(3),
math.random(3))
end
end
local dead = true
if self.ftimes[self.nfw] then
if ElapsedTime - self.fstart > self.ftimes[self.nfw] then
self.nfw = self.nfw + 1
self.fstart = ElapsedTime
sound(SOUND_EXPLODE)
end
dead = false
end
local sm,fw
for k = 1,self.nfw do
sm = self.smokes[k]
fw = self.fireworks[k]
if not sm:isDead() then
dead = false
sm:draw()
end
if not fw:isDead() then
dead = false
fw:draw()
end
end
if dead then
self.active = false
end
popStyle()
return self.nfw
end
function Fireworks:newshow(p)
self.fireworks = {}
self.smokes = {}
self.ftimes = {}
if not p then
local n = math.random(4,8)
p = {}
for i = 1,n do
table.insert(p,{
math.random(50,WIDTH - 50),
math.random(HEIGHT/2,HEIGHT - 50)
})
end
end
local m,t
for k,v in ipairs(p) do
m = math.random(#COLOURS)
table.insert(self.fireworks,Firework(v[1],v[2],COLOURS[m]))
if See_Smoke == 1 then
table.insert(self.smokes,Smoke(v[1],v[2],3))
end
t = math.random() + .5
table.insert(self.ftimes,t)
self.fstart = ElapsedTime
self.nfw = 1
end
table.remove(self.ftimes)
self.active = true
sound(SOUND_EXPLODE)
end
local Firework = class()
function Firework:init(x,y,colour)
-- you can accept and set parameters here
self.p = {}
self.numParticles = Firework_Parts
for i=1,self.numParticles do
local psize = genNumber(PartSize,PartSize_Variation)
local v = vec2(math.random(-100,100),math.random(-100,100))
v = v:normalize()
v = v * genNumber(Velocity,Velocity_Variation)
local c = color(genNumber(colour.r,Color_Variation),
genNumber(colour.g,Color_Variation),
genNumber(colour.b,Color_Variation),
colour.a
)
self.p[i] = Particle(x,
y,
psize,
genNumber(Life,Life_Variation),
c,
v)
end
end
function Firework:draw()
local resistance = 1/(Air_Resistance + 1)
local g = vec2(Gravity.x,Gravity.y)
for i=1,self.numParticles do
local p = self.p[i]
p.x = p.x + p.v.x
p.y = p.y + p.v.y
p.v = p.v + g
p.v = p.v * resistance
local size = math.random(PartSize) * (p.lifeLeft/p.life)
p.width = size
p.height = size
p:draw()
end
end
function Firework:isDead()
for i=1,self.numParticles do
p = self.p[i]
if p.lifeLeft ~= 0 and
(p.x>0 and p.x<WIDTH and p.y>0 and p.y<HEIGHT)then
return false
end
end
return true
end
local Particle = class()
local Particle.DEFAULT_OPACITY = 125
local Particle.DEFAULT_ANGLE = 0
local Particle.DEFAULT_MASS = 1
function Particle:init(posx,posy,size,life,colour,
velocity,mass,angle,sprite)
-- position
self.x = posx
self.y = posy
self.ox = 0
self.oy = 0
-- size
self.width = size
self.height = size
-- color
if colour == nil then
self.color = color(255, 255, 255, 255)
else
self.color = colour
end
-- velocity
self.v = velocity
-- life
self.life = life
self.lifeLeft= life
-- sprite
self.sprite = sprite
-- mass
if mass == nil then
self.mass = DEFAULT_MASS
else
self.mass = mass
end
-- angle
if angle == nil then
self.angle = self.DEFAULT_ANGLE
else
self.angle = angle
end
end
function Particle:draw()
if self.lifeLeft > 0 then
self.lifeLeft = self.lifeLeft - 1
end
if self.lifeLeft ~= 0 then
if self.sprite == nil then
fill(self.color)
ellipse(self.x,self.y,self.width,self.height)
else
pushMatrix()
translate(self.x,self.y)
rotate(self.angle)
tint(self.color)
sprite(self.sprite,0,0,self.width,self.height)
popMatrix()
end
end
end
local function genNumber(number,variation)
ret = variation*0.01*number
ret = number + math.random(-ret,ret)
return ret
end
local Smoke = class()
function Smoke:init(x,y,n)
self.numparts = n
-- color used to tint the particle
local c = color(73, 73, 73, 69)
-- name of the sprite of each smoke particle
local s = "Tyrian Remastered:Dark Orb"
self.parts = {}
for i=0,n do
-- initial size of the smoke particle
local sz = genNumber(60,100)
self.parts[i] = Particle(genNumber(x,20),
genNumber(y,20),sz,-7,c,nil,-1,genNumber(180,100),s)
end
-- rotation speed
self.rSpeed = 0.5
self.windX = 1
self.windY = 1
end
function Smoke:draw()
for i=1,self.numparts do
local p = self.parts[i]
if p.color.a > 0 then
p.angle = p.angle + self.rSpeed
p.width = p.width + 3
p.height = p.height + 3
p.color.a = p.color.a - 0.2
p.x = p.x + self.windX
p.y = p.y + self.windY
p:draw()
end
end
end
function Smoke:isDead()
for i=1,self.numparts do
local p = self.parts[i]
if p.color.a > 0 then
return false
end
end
return true
end
return Fireworks
--]==]
--[==[
-- Frame
-- ====================
-- Frame
-- ver. 0.1
-- a simple rectangle to act as a base for controls
-- ====================
local Frame = class()
function Frame:init(x1, y1, x2, y2)
self.x1 = x1
self.x2 = x2
self.y1 = y1
self.y2 = y2
end
function Frame:draw()
pushStyle()
rectMode(CORNERS)
rect(self.x1, self.y1, self.x2, self.y2)
popStyle()
end
function Frame:gloss()
local i, t, r, y
pushStyle()
fill(255, 255, 255, 255)
rectMode(CORNERS)
rect(self.x1, self.y1, self.x2, self.y2)
r = (self.y2 - self.y1) / 2
for i = 1 , r do
t = 255 - i
stroke(t, t, t, 255)
y = (self.y1 + self.y2) / 2
line(self.x1, y + i, self.x2, y + i)
line(self.x1, y - i, self.x2, y - i)
end
popStyle()
end
function Frame:touched(touch)
if touch.x >= self.x1 and touch.x <= self.x2 then
if touch.y >= self.y1 and touch.y <= self.y2 then
return true
end
end
return false
end
function Frame:midx()
return (self.x1 + self.x2) / 2
end
function Frame:midy()
return (self.y1 + self.y2) / 2
end
return Frame
--]==]
VERSION = "2.2a"
clearProjectData()
-- DEBUG = true
-- Use this function to perform your initial setup
function setup()
autogist = AutoGist("Library Graphics","A library of classes and functions relating to graphical things.",VERSION)
autogist:backup(true)
--displayMode(FULLSCREEN_NO_BUTTONS)
cmodule "Library Graphics"
cmodule.path("Library Base", "Library UI", "Library Utilities", "Library Maths")
cimport "TestSuite"
local Touches = cimport "Touch"
local UI = cimport "UI"
local Debug = cimport "Debug"
local Colour = unpack(cimport "Colour")
local Explosion = cimport "Explosion"
local TextNode = cimport "TextNode"
local View = cimport "View"
Bezier = cimport "Bezier"
touches = Touches()
ui = UI(touches)
debug = Debug({ui = ui})
ui:systemmenu()
testsuite.initialise({ui = ui})
debug:log({
name = "Screen north west",
message = function() local x,y = RectAnchorOf(Screen,"north west") return x .. ", " .. y end
})
--debug:activate()
tn = TextNode({
pos = function() return WIDTH/2,800 end,
anchor = "centre",
--angle = 30,
ui = ui,
fit = true,
maxHeight = HEIGHT,
})
touches:pushHandler(tn)
view = View(ui,touches)
parameter.watch("view.baseRotation")
shape = mesh()
shape.texture = "Documents:Daniel at barnehage"
local x,y,z = 1,1,1
shape.vertices = {
vec3(x,0,0),
vec3(0,y,0),
vec3(0,0,z),
vec3(0,0,0),
vec3(0,y,0),
vec3(0,0,z),
vec3(x,0,0),
vec3(0,0,0),
vec3(0,0,z),
vec3(x,0,0),
vec3(0,y,0),
vec3(0,0,0)
}
shape.colors = {
Colour.svg.Red,
Colour.svg.Green,
Colour.svg.Blue,
Colour.svg.White,
Colour.svg.Green,
Colour.svg.Blue,
Colour.svg.Red,
Colour.svg.White,
Colour.svg.Blue,
Colour.svg.Red,
Colour.svg.Green,
Colour.svg.White
}
view.eye = vec3(5,0,0)
view.range = .25
tw = {t = 0,s=0}
ui:setTimer(5,function() tween(5,tw,{t = 1,s = 0}) return true end)
ui:setTimer(12,function() tween(5,tw,{t = 1,s = 1}) return true end)
ui:setTimer(18,function() tw = {t = 0,s = 1} return true end)
ui:setTimer(19,function() tween(5,tw,{t = 1,s = 0}) return true end)
explosion = Explosion({
image = "Cargo Bot:Codea Icon",
trails = true,
centre = vec2(WIDTH/2,HEIGHT/2)
})
explosion:activate(1,5)
--ui:addNotice({text = "Watch carefully"})
strokeWidth(5)
stroke(255, 0, 0, 255)
ncurves = {}
--[[
local b
for k=1,300 do
b = Bezier(vec2(100,100),
vec2(100,200),
vec2(200,100),
vec2(200,200)
)
b:makeDrawable()
table.insert(ncurves,b)
end
--]]
fps = {}
for k=1,20 do
table.insert(fps,1/60)
end
afps = 60
parameter.watch("math.floor(20/afps)")
--displayMode(FULLSCREEN)
local width = 10
pts = {vec2(width/2,0),
vec2(width/2,3*HEIGHT-width/2),
vec2(WIDTH - width/2,-2*HEIGHT+width/2),
vec2(WIDTH - width/2,HEIGHT)}
--[[
vcurves = {}
local n = 200
for k=1,n do
table.insert(vcurves,{
function(t) return vec2(0,HEIGHT*(1+math.sin(t + 2*math.pi*k/n))/2) end,
function(t) return vec2(WIDTH/3,HEIGHT*(1+math.sin(t + 2*math.pi*(k+1)/n))/2) end,
function(t) return vec2(2*WIDTH/3,HEIGHT*(1+math.sin(t + 2*math.pi*(k+2)/n))/2) end,
function(t) return vec2(WIDTH,HEIGHT*(1+math.sin(t + 2*math.pi*(k+3)/n))/2) end
})
end
--]]
parameter.watch("math.floor(20/afps)")
curves = {}
hcurves = {}
parameter.watch("#curves")
tpts = {}
cpts = {}
end
function draw()
table.remove(fps,1)
table.insert(fps,DeltaTime)
afps = 0
for k,v in ipairs(fps) do
afps = afps + v
end
background(75, 104, 90, 255)
strokeWidth(5)
stroke(151, 115, 115, 255)
for i=1,3 do
line(pts[i].x,pts[i].y,pts[i+1].x,pts[i+1].y)
end
fill(160, 172, 22, 255)
noStroke()
for i = 1,4 do
ellipse(pts[i].x,pts[i].y,20)
end
strokeWidth(5)
stroke(255, 255, 255, 255)
for k,v in ipairs(ncurves) do
v:draw()
end
bezier(pts)
stroke(81, 255, 0, 255)
bezier(vec2(10,10),vec2(WIDTH/2,2*HEIGHT-20),vec2(WIDTH-10,10))
for k,v in ipairs(curves) do
v:draw()
end
stroke(0, 0, 0, 255)
strokeWidth(3)
for k,v in ipairs(hcurves) do
v:draw()
end
noStroke()
for k,v in ipairs(cpts) do
ellipse(v.x,v.y,15)
end
--[[
for _,v in ipairs(vcurves) do
bezier({v[1](ElapsedTime),v[2](ElapsedTime),v[3](ElapsedTime),v[4](ElapsedTime)})
end
--]]
end
function touched(touch)
local tv = vec2(touch.x,touch.y)
if touch.state == BEGAN then
mpoint = nil
for k,v in ipairs(cpts) do
if v:distSqr(tv) < 900 then
mpoint = k
end
end
if not mpoint then
table.insert(tpts,vec2(touch.x,touch.y))
table.insert(cpts,vec2(touch.x,touch.y))
if #tpts == 3 then
local a,b,aa,bb
a,b,th = QuickHobby(tpts,th)
aa = a:clone()
bb = b:clone()
aa:setStyle({width = 8, colour = color(75, 104, 90, 255)})
bb:setStyle({width = 8, colour = color(75, 104, 90, 255)})
table.remove(curves)
table.remove(curves)
table.insert(curves,aa)
table.insert(curves,a)
table.insert(curves,bb)
table.insert(curves,b)
table.remove(tpts,1)
elseif #tpts == 2 then
cvs = QHobby(tpts)
curves = {}
for _,v in ipairs(cvs) do
vv = v:clone()
vv:setStyle({width = 8, colour = color(75, 104, 90, 255)})
table.insert(curves,vv)
table.insert(curves,v)
end
end
end
elseif mpoint then
cpts[mpoint] = tv
if mpoint == #cpts then
tpts[#tpts] = tv
elseif mpoint == #cpts - 1 then
tpts[1] = tv
end
end
if touch.state == ENDED then
--if #cpts > 2 then
hcurves = Hobby(cpts)
--end
if mpoint then
local cvs,vv
cvs, th = QHobby(cpts)
curves = {}
for _,v in ipairs(cvs) do
vv = v:clone()
vv:setStyle({width = 8, colour = color(75, 104, 90, 255)})
table.insert(curves,vv)
table.insert(curves,v)
end
end
end
end
--[[
-- This function gets called once every frame
function draw()
-- process touches and taps
table.remove(fps,1)
table.insert(fps,DeltaTime)
afps = 0
for k,v in ipairs(fps) do
afps = afps + v
end
touches:draw()
background(34, 47, 53, 255)
--[=[
tn:draw()
pushMatrix()
view:draw()
vm = viewMatrix()
qt = qslp(tw.t)
qt = qt*qlp(tw.s)
modelMatrix(qt:tomatrix())
perspective(40,WIDTH/HEIGHT)
camera(10,10,10,0,0,0,0,1,0)
shape:draw()
popMatrix()
resetMatrix()
viewMatrix(matrix())
ortho()
--]=]
strokeWidth(5)
stroke(255, 0, 0, 255)
for k,v in ipairs(curves) do
v:draw()
end
explosion:draw()
ui:draw()
debug:draw()
touches:show()
end
function touched(touch)
touches:addTouch(touch)
end
--]]
function orientationChanged(o)
if ui then
ui:orientationChanged(o)
end
end
function fullscreen()
end
function reset()
end
--[==[
-- Path object
local Path = class()
local PATH_STEP = 1
local PATH_RES = .05*.05
local PATH_RENDER = 0
local PATH_CREATE = 1
local PATH_POINTS = 2
function Path:init(t)
t = t or {}
self.style = t.style or {}
self.lastPoint = vec2(0,0)
self.elements = t.elements or {}
if t.touchHandler then
t.touchHandler:pushHandler(self)
end
self.touchpt = {}
self.m = mesh()
end
function Path:deletePoints()
self.elements = {}
self.lastPoint = vec2(0,0)
end
function Path:draw()
pushStyle()
resetStyle()
self:applyStyle(PATH_RENDER)
self.m:draw()
if self.edit then
self:applyStyle(PATH_POINTS)
local r = self.ptRadius
local lpt
pushStyle()
noStroke()
for k,v in ipairs(self.elements) do
ellipse(v[2].x,v[2].y,r)
if v[1] == "curve" then
popStyle()
line(lpt.x,lpt.y,v[2].x,v[2].y)
line(v[3].x,v[3].y,v[4].x,v[4].y)
pushStyle()
noStroke()
ellipse(v[3].x,v[3].y,r)
ellipse(v[4].x,v[4].y,r)
lpt = v[4]
else
lpt = v[2]
end
end
popStyle()
end
popStyle()
end
function Path:generate()
self:applyStyle(PATH_CREATE)
local ver = {}
local s,h
for k,v in ipairs(self.elements) do
ver,s,h = self:getPoints(v,ver,s,h)
end
if h then
ver,s = HobbyPoints(ver,s,h)
end
self.lastPoint = s
--debug:log({name = "path", message = "got called"})
print("got called")
self:makeMesh(ver)
end
function Path:applyStyle(t)
local s = self.style or {}
if t == PATH_CREATE then
self.linewidth = s.linewidth or 5
self.blur = s.blur or 1
self.smooth = s.smooth or false
self.drawColour = s.drawColour or Colour.svg.Black
elseif t == PATH_RENDER then
elseif t == PATH_POINTS then
self.ptRadius = s.pointRadius or 15
local l = s.pointLineWidth or 3
strokeWidth(l)
local l = s.pointLineCap or SQUARE
lineCapMode(l)
end
end
function Path:addElement(e)
table.insert(self.elements,e)
end
function Path:moveto(v)
self:addElement({"move",v})
self.lastPoint = v
end
function Path:lineto(v)
self:addElement({"line",v})
self.lastPoint = v
end
function Path:curveto(b,c,d)
self:addElement({"curve",b,c,d})
self.lastPoint = d
end
function Path:curvethrough(v)
self:addElement({"hobby",v})
self.lastPoint = v
end
function Path:getPoints(e,ver,s,h)
local t = e[1]
if h and t ~= "hobby" then
ver,s = HobbyPoints(ver,s,h)
end
if t == "move" then
table.insert(ver,{e[2]})
return ver, e[2]
elseif t == "line" then
local n = e[2] - s
n = n:rotate90()
n = n/n:len()
table.insert(ver,{s,n})
table.insert(ver,{e[2],n})
return ver,e[2]
elseif e[1] == "curve" then
return BezierPoints(ver,s,e[2],e[3],e[4])
elseif e[1] == "hobby" then
h = h or {}
table.insert(h,e[2])
return ver,s,h
else
return ver,s
end
end
function Path:makeMesh(pts)
local ver = {}
local col = {}
local l = self.linewidth/2
local b = self.blur + l
local s = false -- self.smooth
local c = self.drawColour
local ct = Colour.opacity(c,0)
local p,n
local m = 0
for k,v in ipairs(pts) do
if v[2] and n then
table.insert(ver,p + l*n)
table.insert(ver,p - l*n)
table.insert(ver,v[1] + l*v[2])
table.insert(ver,v[1] + l*v[2])
table.insert(ver,v[1] - l*v[2])
table.insert(ver,p - l*n)
for i=1,6 do
table.insert(col,c)
end
m = m + 6
if s then
table.insert(ver,p + l*n)
table.insert(ver,p + b*n)
table.insert(ver,v[1] + l*v[2])
table.insert(ver,v[1] + l*v[2])
table.insert(ver,v[1] + b*v[2])
table.insert(ver,p + b*n)
table.insert(ver,p - l*n)
table.insert(ver,p - b*n)
table.insert(ver,v[1] - l*v[2])
table.insert(ver,v[1] - l*v[2])
table.insert(ver,v[1] - b*v[2])
table.insert(ver,p - b*n)
for i=1,12 do
table.insert(col,ct)
end
end
end
p,n = unpack(v)
end
self.m.vertices = ver
self.m.colors = col
end
function Path:isTouchedBy(touch)
local p = vec2(touch.x,touch.y)
for k,v in ipairs(self.elements) do
if p:distSqr(v[2]) < 625 then
self.touchpt[touch.id] = v[2]
self.edit = true
return true
end
if v[1] == "curve" then
if p:distSqr(v[3]) < 625 then
self.touchpt[touch.id] = v[3]
self.edit = true
return true
end
if p:distSqr(v[4]) < 625 then
self.touchpt[touch.id] = v[4]
self.edit = true
return true
end
end
end
self.edit = false
return false
end
function Path:processTouches(g)
local regenerate = false
if g.updated then
for k,t in ipairs(g.touchesArr) do
if t.updated then
regenerate = true
self.touchpt[t.touch.id].x =
self.touchpt[t.touch.id].x
+ t.touch.deltaX
self.touchpt[t.touch.id].y =
self.touchpt[t.touch.id].y
+ t.touch.deltaY
end
if t.touch.state == ENDED then
self.touchpt[t.touch.id] = nil
end
end
g:noted()
if regenerate then
self:generate()
end
end
if g.type.ended then
g:reset()
end
end
function Path:setLineWidth(l)
self.style.linewidth = l
end
function Path:setBlur(l)
self.style.blur = l
end
function Path:setSmooth(l)
self.style.smooth = l
end
function Path:setColour(c)
self.style.drawColour = c
end
function BezierPoints(pts,a,b,c,d)
pts = pts or {}
if not(type(a) == "table" and a.is_a and a:is_a(Bezier)) then
a = Bezier(a,b,c,d)
end
local t = 0
local r = PATH_RES
local s = PATH_STEP
local tpt = a
table.insert(pts,{tpt,a:unitNormal(0)})
local dis
local p
while t < 1 do
dis = 0
while dis < r do
t = t + s
p = a:point(t)
dis = p:distSqr(tpt)
end
if t > 1 then
t = 1
p = d
end
table.insert(pts,{p,a:unitNormal(t)})
tpt = p
end
return pts
end
function HobbyPoints(ver,s,pts,extra)
ver = ver or {}
if #pts == 1 then
local v = pts[1]
if type(v) == "table" then
v = v[1]
end
local n = v - s
n = n:rotate90()
n = n/n:len()
table.insert(ver,{s,n})
table.insert(ver,{v,n})
return ver
end
local apts = {}
table.insert(apts,s)
for _,v in ipairs(pts) do
table.insert(apts,v)
end
local bcurves = Hobby(apts,extra)
for _,v in ipairs(bcurves) do
ver = BezierPoints(v,ver)
end
return ver
end
return Path
--]==]
--[==[
-- Text Node
local Font,_,Textarea = unpack(cimport "Font",nil)
local Colour = unpack(cimport "Colour",nil)
local UTF8 = cimport "utf8"
cimport "Keyboard"
--cimport "RoundedRect"
cimport "ColourNames"
local TextNode = class(Textarea)
function TextNode:init(t)
t = t or {}
t.font = t.font or Font({name = "AmericanTypewriter", size = 12})
t.width = t.width or "32em"
t.height = t.height or "1lh"
if t.fit == nil then
t.fit = true
end
t.angle = t.angle or 0
Textarea.init(self,t)
self.keyboard = t.keyboard or "fullqwerty"
self.ui = t.ui
self:activate()
self.edit = t.edit or true
self.savecolour = self.colour
self:setColour(Colour.opacity(Colour.svg.Black,50))
self.ui:useKeyboard(self.keyboard,
function(k) return self:processKey(k) end)
end
function TextNode:processTouches(g)
if g.type.ended and g.type.tap and g.num == 2 then
self:setEdit()
g:reset()
return
end
if g.updated then
if g.num == 1 then
local t = g.touchesArr[1]
if self.edit then
local td =
vec2(t.touch.deltaX,
t.touch.deltaY):rotate(-math.rad(self.angle))
self:setSize({
width = self.width + td.x,
height = self.height + td.y,
maxWidth = self.mwidth + td.x,
maxHeight = self.mheight + td.y,
})
else
self.anchor = nil
self.opos = function() return self.x, self.y end
self.x = self.x + t.touch.deltaX
self.y = self.y + t.touch.deltaY
end
elseif g.num == 2 then
local ta,tb = g.touchesArr[1],g.touchesArr[2]
local sa,sb,ea,eb
ea = --OrientationInverse(PORTRAIT,
vec2(ta.touch.x,ta.touch.y)
eb = --OrientationInverse(PORTRAIT,
vec2(tb.touch.x,tb.touch.y)
if ta.updated and ta.state ~= BEGAN then
sa = --OrientationInverse(PORTRAIT,
vec2(ta.touch.prevX,ta.touch.prevY)
else
sa = ea
end
if tb.updated and tb.state ~= BEGAN then
sb = --OrientationInverse(PORTRAIT,
vec2(tb.touch.prevX,tb.touch.prevY)
else
sb = eb
end
local o = vec2(self.x,self.y)
local ang = (sb - sa):angleBetween(eb - ea)
local sc = ((sb + sa)/2 - o):rotate(ang)
local ec = (ea + eb)/2 - o
self.angle = self.angle + math.deg(ang)
self.anchor = nil
self.opos = function() return self.x, self.y end
self.x = self.x + ec.x - sc.x
self.y = self.y + ec.y - sc.y
end
end
g:noted()
if g.type.ended and not g.type.tap then
g:reset()
end
end
function TextNode:processKey(k)
if k == BACKSPACE then
self:delChar()
elseif k == RETURN then
self:addChar(UTF8("\n"))
else
self:addChar(k)
end
return false
end
function TextNode:setEdit(e)
if e == nil then
e = not self.edit
end
if e then
self.edit = true
self.savecolour = self.colour
self:setColour(Colour.opacity(Colour.svg.Black,50))
self.ui:useKeyboard(self.keyboard,
function(k) return self:processKey(k) end)
else
self.edit = false
self:setColour(self.savecolour)
self.ui:unuseKeyboard()
end
end
return TextNode
--]==]
--[==[
-- Trend Graph
local TrendGraph = class()
function TrendGraph:init(t)
self.outer = Frame(t.x, t.y, t.x + t.width, t.y + t.height)
self.inner = Frame(t.x + 20, t.y + 10, t.x + t.width - 10, t.y + t.height - 20)
self.title = t.title or ""
self.font = t.font or "Copperplate"
self.fontSize = t.fontSize or 16
self.colour = t.colour or Colour.svg.SlateGray
self.icolour = Colour.tint(self.colour,50)
self.rate = t.rate or 1
self.series = {}
self.xlabel = ""
self.ylabel = ""
font(self.font)
fontSize(self.fontSize)
local w,h = textSize(self.title)
self.titlex = self.outer:midx() - w/2
self.titley = self.outer.y2 - h - 5
self.frame = 0
end
function TrendGraph:addSeries(name, len, min, max, sym, size, thick, clr, value)
local i
i = table.getn(self.series)
self.series[i + 1] = DataSeries(name, len, min, max, sym, size, thick, clr, value)
end
function TrendGraph:draw(pts)
pushMatrix()
pushStyle()
self.frame = self.frame + 1
fill(self.colour)
self.outer:draw()
stroke(253, 3, 3, 255)
fill(self.icolour)
self.inner:draw()
fill(0, 0, 0, 255)
noSmooth()
font(self.font)
fontSize(self.fontSize)
textMode(CORNER)
text(self.title,self.titlex,self.titley)
for s, series in ipairs(self.series) do
if self.frame%self.rate == 0 and not self.paused then
series:update()
end
series:draw(self.inner, self.min, self.max)
end
popMatrix()
popStyle()
end
function TrendGraph:pause(p)
self.paused = p
for s, series in ipairs(self.series) do
series:addBreak()
end
end
return TrendGraph
--]==]
--[==[
-- View
--[[
The "View" class defines an object which handles positioning of
things in 3-dimensions and how they project to the 2-dimensional iPad
screen. It handles transformations and certain other aspects that are
more to do with the surrounding View than a particular object in it.
In this class, the term "internal" refers to the 3-dimensional
representation of View. The term "external" refers to things after
the projection to the plane of the iPad screen.
--]]
local View = class()
local Quaternion = cimport "Quaternion"
cimport "Coordinates"
--[[
We need to know the user interface object as we want to have our own
menu for the user to choose various options.
--]]
function View:init(ui,t,p)
p = p or {}
self.baseRotation = Quaternion.Rotation(0,vec3(1,0,0))
self.orientRotation = Quaternion.unit()
self.intScale = 1
self.extScale = 1
self.fishEye = 1
self.speed = 1/600
self.origin = vec3(0,0,0)
self.velocity = vec3(0,0,0)
self.acceleration = 1
self.friction = .2
self.bgColour = color(0,0,0,255)
self.eye = vec3(0,0,15)
self.looking = -self.eye
self.up = vec3(0,1,0)
self.light = vec3(0,1,0)
self.currentGravity = self.orientRotation:Gravity()
self.useGravity = true
self.rotation = self.baseRotation
self.matrix = matrix()
self.initials = {}
self.ui = ui
for k,v in pairs(p) do
self[k] = v
end
if Menu then
local m = ui:addMenu({title = "View", attach = true})
m:addItem({
title = "Use Gravity",
action = function()
self:gravityOnOff()
return true
end,
highlight = function()
return self.useGravity
end
})
ui:addHelp({title = "View", text = {"Instructions:",
"Single tap: toggle the reaction to tilt",
"Double tap: restore initial spatial settings",
"Single swipe: rotate the object about an axis in the plane of the screen",
"Double swipe: translate the object in 3-View",
"Triple swipe: translate the projected image of the object",
"Vertical pinch: scale the object in 3-View",
"Horizontal pinch: scale the projected image"
}}
)
end
t:pushHandler(self)
self:saveInitials()
end
--[[
Make sure we can get back to where we started.
--]]
function View:saveInitials()
self.initials.baseRotation = self.baseRotation
self.initials.intScale = self.intScale
self.initials.extScale = self.extScale
self.initials.origin = self.origin
self.initials.eye = self.eye
self.initials.currentGravity = self.currentGravity
self.initials.fishEye = self.fishEye
self.initials.light = self.light
end
--[[
Get us back to where we started.
--]]
function View:restoreInitials()
self.baseRotation = self.initials.baseRotation
self.intScale = self.initials.intScale
self.extScale = self.initials.extScale
self.origin = self.initials.origin
self.eye = self.initials.eye
self.currentGravity = self.initials.currentGravity
self.fishEye = self.initials.fishEye
self.light = self.initials.light
end
function View:reset()
self:restoreInitials()
end
--[[
This is the main draw function. It does not actually do much drawing
but rather sets up various things for use by other objects.
--]]
function View:draw()
self.orientRotation:updateReferenceFrame()
if self.angVelocity then
if ElapsedTime - self.angvTime > 1 then
self.angVelocity = nil
else
qa = self.angVelocity(ElapsedTime - self.angvTime)
self.baseRotation = self.baseRotation * qa
end
end
if self.moving then
self.origin = self.origin + DeltaTime*self.velocity
end
local q = self.baseRotation * self:getGravity()
local s = self.fishEye * self.intScale
local o = self.origin
local e = self.eye
local up = self.up
e = s*e^q + o
up = up^q
self.looking = o - e
if self.moving then
self.velocity = self.velocity
+ DeltaTime * self.acceleration * (o-e)
- DeltaTime * self.friction * self.velocity:len() * self.velocity
end
camera(e.x,e.y,e.z,o.x,o.y,o.z,up.x,up.y,up.z)
local fe = math.atan(self.fishEye)*180/math.pi
if self.near then
perspective(fe,WIDTH/HEIGHT,self.near,self.far)
else
perspective(fe,WIDTH/HEIGHT)
end
self.matrix = viewMatrix() * projectionMatrix()
end
--[[
If we are noticing gravity then this figures out the rotation defined
by our current gravity vector as a quatertion.
--]]
function View:getGravity()
if self.useGravity then
return self.currentGravity * self.orientRotation:Gravity()^""
else
return self.currentGravity
end
end
--[[
This applies the current internal transformation to the given vector;
this is before stereographic projection has occured.
--]]
function View:applyIntTransformation(v)
local q = self.baseRotation * self:getGravity()
local s = self.fishEye * self.intScale
local o = self.origin
return s * (v^q + o)
end
function View:applyIntDirTransformation(v)
local q = self.baseRotation * self:getGravity()
local s = self.fishEye * self.intScale
return s * v^q
end
function View:invertIntTransformation(v)
local q = self.baseRotation * self:getGravity()
local s = self.fishEye * self.intScale
local o = self.origin
q = q^""
return (v / s - o)^q
end
function View:invertIntDirTransformation(v)
local q = self.baseRotation * self:getGravity()
local s = self.fishEye * self.intScale
q = q^""
return v^q / s
end
--[[
This applies the current external transformation to the given vector;
this is after stereographic projection has occured.
--]]
function View:applyExtTransformation(v)
return (self.extScale/self.fishEye) * (v + self.extTranslate)
end
function View:applyExtDirTransformation(v)
return (self.extScale/self.fishEye) * v
end
--[[
This projects the vector onto the screen, taking into account the
current internal and external transformations.
--]]
function View:Project(v)
local u,w,r,l
-- u = self.intScale * ((v + self.intTranslate)^self.rotation)
u = self:applyIntTransformation(v)
w = u + Vec3.e1
w = w:stereoProject(self.eye) - u:stereoProject(self.eye)
r = w:len()
l = u:stereoLevel(self.eye)
u = self:applyExtTransformation(u:stereoProject(self.eye))
return {u,r,l,v:isInFront(self.eye)}
end
function View:ProjectDirection(v)
local u
u = self:applyIntDirTransformation(v)
return u
end
--[[
This inverts the projection to the z-level of the second vector.
--]]
function View:invProject(v,w)
local u,q,h
u = v / self.extScale - self.extTranslate
q = self.rotation
w = self.intScale * (w^q + self.intTranslate)
u = Vec3.stereoInvProject(u,self.eye,w.z)
u = u / self.intScale - self.intTranslate
q = q^""
u = u^q
return u
end
--[[
We should be pretty far down the "touch" queue so we take anything we
can.
--]]
function View:isTouchedBy(touch)
return true
end
--[[
Our possible touches and their actions are:
Single tap: freeze or unfreeze the gravitational effect.
Double tap: restore stuff to initial conditions.
Single move: rotate View, if it is short then we carry on spinning
for a bit.
Pinch: scale View, either internally or externally
Double swipe: translate internally
Triple swipe: translate externally
--]]
function View:processTouches(g)
if g.type.long and g.num == 1 then
self:singleLongTap(g.touchesArr[1])
if g.type.ended then
g:reset()
end
elseif g.type.tap then
if g.type.finished then
if g.num == 1 then
self:singleShortTap()
elseif g.num == 2 then
self:doubleTap()
end
end
else
if g.numactives == 1 then
self:singleTouch(g.actives[1])
if g.type.ended and g.type.short then
self:saveVelocity(g.actives[1])
end
elseif g.numactives == 2 then
self:doubleTouch(g.actives[1],g.actives[2])
elseif g.numactives == 3 then
if not g.type.ViewTriple then
g.type.ViewTripleType = self:isTriangle(
g.actives[1],
g.actives[2],
g.actives[3])
g.type.ViewTriple = true
end
if g.type.ViewTripleType then
self:triplePinch(
g.actives[1],
g.actives[2],
g.actives[3])
else
self:tripleSwipe(
g.actives[1],
g.actives[2],
g.actives[3])
end
end
if g.type.ended then
g:reset()
end
end
g:noted()
if g.type.finished then
g:reset()
end
end
--[[
Rotate View according to the movement, saving the velocity so that we
can carry on spinning for a bit if the total movement was short.
--]]
function View:singleTouch(thisTouch)
local r,p,v,lp,lv,ox,oy,u,qa,qg,touch
if not thisTouch.updated then
return
end
touch = thisTouch.touch
if touch.state == MOVING or touch.state == ENDED then
r =RectAnchorOf(Screen,"width")/2
ox,oy = RectAnchorOf(Screen,"centre")
p = vec2(touch.prevX - ox,touch.prevY - oy)/r
v = vec2(touch.x - ox,touch.y - oy)/r
lv = v:lenSqr()
lp = p:lenSqr()
if lp > .9 or lv > .9 then
return
end
p = vec3(p.x,p.y,math.sqrt(1 - lp))
v = vec3(v.x,v.y,math.sqrt(1 - lv))
qa = v:rotateTo(p)
local b = SO3(self.eye,self.up)
qa = Quaternion(qa.q.x,-qa.q.y*b[3] + qa.q.z*b[2] + qa.q.w*b[1])
qg = self:getGravity()
self.baseRotation = self.baseRotation * qg * qa * qg^""
end
end
--[[
Saves our velocity.
--]]
function View:saveVelocity(touch)
local ft,t,dt,r,p,ox,oy,v,lp,lv,qa,qg
ft = touch.firsttouch
t = touch.touch
dt = ElapsedTime - touch.createdat
r = RectAnchorOf(Screen,"width")/2
ox,oy = RectAnchorOf(Screen,"centre")
p = vec2(ft.x - ox,ft.y - oy)/r
v = vec2(t.x - ox,t.y - oy)/r
v = p + (v-p)*2*DeltaTime/dt
lp = p:lenSqr()
lv = v:lenSqr()
if lp < .9 and lv < .9 then
-- raise both to sphere
p = vec3(p.x,p.y,math.sqrt(1 - lp))
v = vec3(v.x,v.y,math.sqrt(1 - lv))
qa = v:rotateTo(p)
local b = SO3(self.eye,self.up)
qa = Quaternion(qa.q.x,-qa.q.y*b[3] + qa.q.z*b[2] + qa.q.w*b[1])
qg = self:getGravity()
qa = qg * qa * qg^""
self.angVelocity = qa:make_slerp(Quaternion.unit())
self.angvTime = ElapsedTime
touch.container.interrupt = self
end
end
--[[
This uses the "interrupt" feature of the touch controller so that if
we are rotating then the next touch stops us.
--]]
function View:interruption(t)
if self.angVelocity then
self.angVelocity = nil
t.container.interrupt = nil
return true
else
return false
end
end
--[[
General handling of double touches
--]]
function View:doubleTouch(ta,tb)
if not ta.updated and not tb.updated then
return
end
local sa,sb,ea,eb,o,n,u,v,A
ea = vec2(ta.touch.x,ta.touch.y)
eb = vec2(tb.touch.x,tb.touch.y)
if ta.updated then
sa = vec2(ta.touch.prevX,ta.touch.prevY)
else
sa = ea
end
if tb.updated then
sb = vec2(tb.touch.prevX,tb.touch.prevY)
else
sb = eb
end
local ed = eb - ea
local sd = sb - sa
local el = ed:len()
local sl = sd:len()
local s = 1
local theta = 0
if sl > 0.001 and el > 0.001 then
s = el/sl
theta = ed:angleBetween(sd)
end
o = (eb + ea)/2
A = self.matrix
n = screennormal(o,A)
u = n:cross(self.up)
v = u:cross(n)
o = self.origin
sa = screentoplane(sa,o,u,v,A)
ea = screentoplane(ea,o,u,v,A)
sb = screentoplane(sb,o,u,v,A)
eb = screentoplane(eb,o,u,v,A)
ed = (eb + ea)/2
sd = (sb + sa)/2
local a = self.eye - ed
a = a:normalize()
local q = Quaternion.Rotation(theta,a)
self.intScale = self.intScale / s
local tr = ed - sd^q*s
local qg = self:getGravity()
local qa = qg * q * qg^""
self.baseRotation = self.baseRotation * qa
self.origin = self.origin^qa - tr
end
--[[
Triple touch handling - are the coordinates "triangular" or "linear"
--]]
function View:isTriangle(ta,tb,tc)
local a = vec2(ta.touch.x,ta.touch.y)
local b = vec2(tb.touch.x,tb.touch.y)
local c = vec2(tc.touch.x,tc.touch.y)
local phi = (b - a):angleBetween(c - a)
local psi = (a - b):angleBetween(c - b)
phi = phi - math.floor(phi/(2*math.pi))*2*math.pi
if phi > math.pi then
phi = 2*math.pi - phi
end
psi = psi - math.floor(psi/(2*math.pi))*2*math.pi
if psi > math.pi then
psi = 2*math.pi - psi
end
if phi > math.pi/2
or psi > math.pi/2
or phi + psi < math.pi/2
then
return false
else
return true
end
end
--[[
A triple swipe to an external translation.
--]]
function View:tripleSwipe(ta,tb,tc)
if not ta.updated and not tb.updated and not tc.updated then
return
end
local ea,eb,ec,sa,sb,sc,ed,sd
ea = vec2(ta.touch.x,ta.touch.y)
eb = vec2(tb.touch.x,tb.touch.y)
ec = vec2(tc.touch.x,tc.touch.y)
if ta.updated then
sa = vec2(ta.touch.prevX,ta.touch.prevY)
else
sa = ea
end
if tb.updated then
sb = vec2(tb.touch.prevX,tb.touch.prevY)
else
sb = eb
end
if tc.updated then
sc = vec2(tc.touch.prevX,tc.touch.prevY)
else
sc = ec
end
o = (ec + eb + ea)/2
A = self.matrix
n = screennormal(o,A)
u = n:cross(self.up)
v = u:cross(n)
o = self.origin
sa = screentoplane(sa,o,u,v,A)
ea = screentoplane(ea,o,u,v,A)
sb = screentoplane(sb,o,u,v,A)
eb = screentoplane(eb,o,u,v,A)
sc = screentoplane(sc,o,u,v,A)
ec = screentoplane(ec,o,u,v,A)
ed = (ec + eb + ea)/2
sd = (sc + sb + sa)/2
self.origin = self.origin - ed + sd
end
--[[
A triple pinch to alter the "fish eye" effect
--]]
function View:triplePinch(ta,tb,tc)
local a = vec2(ta.touch.x,ta.touch.y)
local b = vec2(tb.touch.x,tb.touch.y)
local c = vec2(tc.touch.x,tc.touch.y)
local pa,pb,pc
if ta.updated then
pa = vec2(ta.touch.prevX,ta.touch.prevY)
else
pa = a
end
if tb.updated then
pb = vec2(tb.touch.prevX,tb.touch.prevY)
else
pb = b
end
if tc.updated then
pc = vec2(tc.touch.prevX,tc.touch.prevY)
else
pc = c
end
local d = TriangleArea(a,b,c)
local pd = TriangleArea(pa,pb,pc)
self.fishEye = self.fishEye*(1+pd)/(1+d)
end
--[[
A single tap toggles the use of gravity.
--]]
function View:singleShortTap()
self:gravityOnOff()
end
function View:singleLongTap(t)
if t.touch.state == ENDED then
self.moving = false
else
self.moving = true
end
end
--[[
This is the actual toggle function. As well as toggling the use, it
saves the current rotation so that the effect is to freeze the object.
--]]
function View:gravityOnOff()
if self.useGravity then
self.currentGravity = self:getGravity()
else
self.currentGravity = self.currentGravity * self.orientRotation:Gravity()
end
self.useGravity = not self.useGravity
end
--[[
This is the double tap function that calls the reset function.
--]]
function View:doubleTap()
self:restoreInitials()
end
return View
--]==]
--[==[
-- 2D Zoom
local Zoom = class()
function Zoom:init(t)
t:pushHandler(self)
self.ll = vec2(0,0)
self.size = vec2(1,1)
-- self.aspect = true
end
function Zoom:draw()
scale(self.size.x,self.size.y)
translate(self.ll.x,self.ll.y)
end
function Zoom:isTouchedBy(touch)
return true
end
function Zoom:processTouches(g)
if g.updated then
if g.numactives == 1 then
local dx = g.actives[1].touch.deltaX/self.size.x
local dy = g.actives[1].touch.deltaY/self.size.y
self.ll = self.ll + vec2(dx,dy)
elseif g.numactives == 2 then
local ta = g.actives[1]
local tb = g.actives[2]
local ea = vec2(ta.touch.x,ta.touch.y)
local eb = vec2(tb.touch.x,tb.touch.y)
local sa,sb
if ta.updated then
sa = vec2(ta.touch.prevX,ta.touch.prevY)
else
sa = ea
end
if tb.updated then
sb = vec2(tb.touch.prevX,tb.touch.prevY)
else
sb = eb
end
local sc = (sa + sb)/2
sc.x = sc.x / self.size.x
sc.y = sc.y / self.size.y
local sl = (sb - sa):len()
local el = (eb - ea):len()
self.size = self.size * el / sl
local ec = (ea + eb)/2
ec.x = ec.x / self.size.x
ec.y = ec.y / self.size.y
self.ll = self.ll + ec - sc
end
end
if g.type.finished then
g:reset()
else
g:noted()
end
end
return Zoom
--]==]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment