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