Skip to content

Instantly share code, notes, and snippets.

@DolenzSong
Created October 18, 2017 06:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DolenzSong/d9c26576872ea5df8a11f1b336e8448a to your computer and use it in GitHub Desktop.
Save DolenzSong/d9c26576872ea5df8a11f1b336e8448a to your computer and use it in GitHub Desktop.
An adaptation of Codea craft's Cameras project in which the camera crashes the project
--# Main
-- Cameras
-----------------------------------------
-- Learn Craft
-- Written by John Millard
-- Special thanks to Ignatz for the MultiStep project template
-----------------------------------------
-- Description:
-----------------------------------------
supportedOrientations(ANY)
displayMode(STANDARD)
-- Use this function to perform your initial setup
function setup()
print("Hello Cameras!")
startTab("FirstPersonViewer 👁")
end
function drawStepName(name)
fill(255, 255, 255, 255)
font("Inconsolata")
fontSize(40)
textAlign(LEFT)
textMode(CORNER)
text(name, 30, HEIGHT - 60)
end
function setGlobalButtons(tabNames)
for i, tabName in ipairs(tabNames) do
parameter.action(tabName, function () startTab(tabName) end)
parameter.number("cameraX", -100,100,0)
parameter.number("cameraY", 0,5,4)
parameter.number("cameraZ", -100,100,0)
parameter.number("fov", 0,100,82)
parameter.number("np", 0,10,2)
parameter.number("rx", 0,100,0)
parameter.number("ry", 0,100,180)
parameter.number("gx", -80,80,-6.5)
parameter.number("gy", -10,10,-0.3)
parameter.number("gz", -80,80,5)
end
end
function startTab(tabName)
currentTabGlobal = tabName
if cleanup then cleanup() end
parameter.clear()
loadableName = string.gsub(tabName, "([A-Z])", " %1")
_, _, nameWithoutEmoji, _ = string.find(tabName, "(%a+)%s*")
loadstring(readProjectTab(nameWithoutEmoji))()
setUpViewer(nameWithoutEmoji)
--PrintExplanation(tabName)
setGlobalButtons({"FirstPersonViewer 👁", "OrbitViewer 💫"})
end
function setUpViewer(name)
scene = craft.scene()
local m = craft.model("CastleKit:knightBlue")
model = scene:entity()
model:add(craft.renderer, m)
ground = scene:entity()
ground.scale = vec3(25,2,25)
local k = craft.model("Nature:naturePack_011")
ground:add(craft.renderer, k)
currentCameraGlobal = nil
touches.handlers = {}
if name == "FirstPersonViewer" then
currentCameraGlobal = FirstPersonViewer
camerasettings = scene.camera:get(craft.camera)
camerasettings.fieldOfView = 60
print(camerasettings)
elseif name == "OrbitViewer" then
currentCameraGlobal = OrbitViewer
end
scene.camera:add(currentCameraGlobal, vec3(0,5,0), 10, 5, 40)
end
function PrintExplanation(tabName)
if tabName == "FirstPersonViewer" then
elseif tabName == "OrbitViewer" then
end
end
function update(dt)
scene.camera.x = cameraX
scene.camera.y = cameraY
scene.camera.z = cameraZ
scene.camera.eulerAngles = vec3(rx, ry, 0)
currentCameraGlobal.rx = rx
camerasettings.fieldOfView = fov
camerasettings.nearPlane = np
ground.position = vec3(gx,gy,gz)
scene:update(dt)
end
-- This function gets called once every frame
function draw()
update(DeltaTime)
scene:draw()
drawStepName(currentTabGlobal)
end
--# FirstPersonViewer
-----------------------------------------
-- FirstPersonViewer
-- Written by John Millard
-----------------------------------------
-- Description:
-- A basic viewer for first person cameras.
-- Attach to a camera's entity for basic first person controls:
-- i.e. scene.camera:add(FirstPersonViewer)
-----------------------------------------
FirstPersonViewer = class()
local IDLE = 1
local ROTATE = 2
function FirstPersonViewer:init(camera, tapHandler)
self.camera = camera
self.rx = 0
self.ry = 0
self.state = IDLE
self.enabled = true
self.tapHandler = tapHandler
self.sensitivity = 0.25
touches.addHandler(self, 0, false)
end
function FirstPersonViewer:isActive()
return self.state ~= IDLE
end
function FirstPersonViewer:update()
if self.enabled then
-- clamp vertical rotation between -90 and 90 degrees (no upside down view)
self.rx = math.min(math.max(self.rx, -90), 90)
local rotation = quat.eulerAngles(self.rx, self.ry, 0)
self.camera.rotation = rotation
end
end
function FirstPersonViewer:touched(touch)
if self.state == IDLE then
if touch.state == BEGAN then
self.start = vec2(touch.x, touch.y)
elseif touch.state == MOVING then
local length = (vec2(touch.x, touch.y) - self.start):len()
if length >= 5 then
self.state = ROTATE
end
end
elseif self.state == ROTATE then
if touch.state == MOVING then
self.rx = self.rx - touch.deltaY * self.sensitivity
self.ry = self.ry - touch.deltaX * self.sensitivity
elseif touch.state == ENDED then
self.state = IDLE
end
end
return true
end
--# OrbitViewer
-----------------------------------------
-- OrbitViewer
-- Written by John Millard
-----------------------------------------
-- Description:
-- A basic viewer that orbits a target via rotating, panning and zooming.
-- A particular point in space is used as the target.
-- Single touch rotates while pinching is used for zooming in and out.
-- Two finger drag is used for panning.
-- Attach to a camera's entity for basic first person controls:
-- i.e. scene.camera:add(OrbitViewer)
-----------------------------------------
OrbitViewer = class()
function OrbitViewer:init(entity, target, zoom, minZoom, maxZoom)
self.entity = entity
self.camera = entity:get(craft.camera)
-- The camera's current target
self.target = target or vec3(0,0,0)
self.origin = self.target
self.zoom = zoom or 5
self.minZoom = minZoom or 1
self.maxZoom = maxZoom or 20
self.touches = {}
self.prev = {}
-- Camera rotation
self.rx = 0
self.ry = 0
-- Angular momentum
self.mx = 0
self.my = 0
self.sensitivity = 0.25
touches.addHandler(self, 0, true)
end
-- Project a 2D point z units from the camera
function OrbitViewer:project(p,z)
local origin, dir = self.camera:screenToRay(p)
return origin + dir * z
end
-- Calculate overscroll curve for zooming
function scroll(x,s)
return s * math.log(x + s) - s * math.log(s)
end
function OrbitViewer:update()
if #self.touches == 0 then
-- Apply momentum from previous swipe
self.rx = self.rx + self.mx * DeltaTime
self.ry = self.ry + self.my * DeltaTime
self.mx = self.mx * 0.9
self.my = self.my * 0.9
-- If zooming past min or max interpolate back to limits
if self.zoom > self.maxZoom then
local overshoot = self.zoom - self.maxZoom
overshoot = overshoot * 0.9
self.zoom = self.maxZoom + overshoot
elseif self.zoom < self.minZoom then
local overshoot = self.minZoom - self.zoom
overshoot = overshoot * 0.9
self.zoom = self.minZoom - overshoot
end
elseif #self.touches == 2 then
self.entity.position = self.prev.target - self.entity.forward * self.zoom
local mid = self:pinchMid()
local dist = self:pinchDist()
local p1 = self:project(self.prev.mid, self.zoom)
local p2 = self:project(mid,self.zoom)
self.target = self.prev.target + (p1-p2)
self.zoom = self.prev.zoom * (self.prev.dist / dist)
if self.zoom > self.maxZoom then
local overshoot = self.zoom - self.maxZoom
overshoot = scroll(overshoot, 10.0)
self.zoom = self.maxZoom + overshoot
elseif self.zoom < self.minZoom then
local overshoot = self.minZoom - self.zoom
overshoot = scroll(overshoot, 10.0)
self.zoom = self.minZoom - overshoot
end
end
-- Clamp vertical rotation between -90 and 90 degrees (no upside down view)
self.rx = math.min(math.max(self.rx, -90), 90)
-- Calculate the camera's position and rotation
local rotation = quat.eulerAngles(self.rx, self.ry, 0)
self.entity.rotation = rotation
local t = vec3(self.target.x, self.target.y, self.target.z)
self.entity.position = t + self.entity.forward * -self.zoom
end
-- Calculate the distance between the current two touches
function OrbitViewer:pinchDist()
local p1 = vec2(self.touches[1].x, self.touches[1].y)
local p2 = vec2(self.touches[2].x, self.touches[2].y)
return p1:dist(p2)
end
-- Calculate the mid point between the current two touches
function OrbitViewer:pinchMid()
local p1 = vec2(self.touches[1].x, self.touches[1].y)
local p2 = vec2(self.touches[2].x, self.touches[2].y)
return (p1 + p2) * 0.5
end
function OrbitViewer:touched(touch)
if touch.tapCount == 2 then
self.target = self.origin
end
-- Allow a maximum of 2 touches
if touch.state == BEGAN and #self.touches < 2 then
table.insert(self.touches, touch)
if #self.touches == 2 then
self.prev.target = vec3(self.target:unpack())
self.prev.mid = self:pinchMid()
self.prev.dist = self:pinchDist()
self.prev.zoom = self.zoom
self.mx = 0
self.my = 0
end
return true
-- Cache updated touches
elseif touch.state == MOVING then
for i = 1,#self.touches do
if self.touches[i].id == touch.id then
self.touches[i] = touch
end
end
-- Remove old touches
elseif touch.state == ENDED or touch.state == CANCELLED then
for i = #self.touches,1,-1 do
if self.touches[i].id == touch.id then
table.remove(self.touches, i)
break
end
end
if #self.touches == 1 then
self.mx = 0
self.my = 0
end
end
-- When all touches are finished apply momentum if moving fast enough
if #self.touches == 0 then
self.mx = -touch.deltaY / DeltaTime * self.sensitivity
self.my = -touch.deltaX / DeltaTime * self.sensitivity
if math.abs(self.mx) < 70 then
self.mx = 0
end
if math.abs(self.my) < 70 then
self.my = 0
end
-- When only one touch is active simply rotate the camera
elseif #self.touches == 1 then
self.rx = self.rx - touch.deltaY * self.sensitivity
self.ry = self.ry - touch.deltaX * self.sensitivity
end
return false
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment