├── .bowerrc ├── .busted ├── .gitignore ├── .luacov ├── LICENSE.md ├── README.md ├── bower.json ├── build.settings ├── config.lua ├── docs ├── composite.jpg ├── data.jpg ├── diffuse.jpg ├── light.jpg └── normal.jpg ├── dynacam.lua ├── images ├── arrow_128x88_horizontal_normal.png ├── arrow_128x88_horizontal_texture.png ├── bg1.png ├── brick.png ├── brick_n.png ├── powerup_health.png ├── powerup_health_n.png ├── spaceship_carrier_01.png ├── spaceship_carrier_01_n.png ├── spaceship_carrier_02.png ├── spaceship_carrier_02_n.png ├── spinning_coin_gold.png ├── spinning_coin_gold_n.png ├── wall.png └── wall_n.png ├── main.lua ├── quantum.lua └── shaders ├── apply.lua ├── light.lua ├── normal.lua └── rotate.lua /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "dependencies", 3 | "registry": "https://localhost:8000", 4 | "scripts": { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | coverage = true, 4 | verbose = true, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dependencies/ 3 | build/ 4 | mobdebug.lua 5 | 6 | *.out 7 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return { 2 | configfile = ".luacov", 3 | statsfile = "luacov.stats.out", 4 | reportfile = "luacov.report.out", 5 | runreport = true, 6 | deletestats = true, 7 | 8 | include = { 9 | }, 10 | 11 | exclude = { 12 | "busted", 13 | "luarocks", 14 | "say", 15 | "luacov", 16 | "term", 17 | "luassert", 18 | "mediator", 19 | "pl", 20 | 21 | "main$", 22 | "spec/", 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Zetosoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *# dynacam.** 2 | ### Overview 3 | --- 4 | 5 | Dynamic Lights + Camera for [Solar2D](https://solar2d.com/) 6 | 7 | Feel that your game needs to expand beyond the default screen limits? Want to add dynamic lighting effects to your game? Want to add split screen to your game? This is the plugin for you. Add dynamic lighting and full camera tracking to your game using normal maps and light objects: 8 | 9 | # ![Normal](docs/normal.jpg) **+** ![Data](docs/data.jpg) **=** ![Light](docs/light.jpg) 10 | 11 | # ![Diffuse](docs/diffuse.jpg) **+** ![Light](docs/light.jpg) **=** ![Composite](docs/composite.jpg) 12 | 13 | You can [try out the web demo for desktop here](https://www.zetosoft.com/dynacam/demo) (Move with arrow keys) 14 | 15 | ### Notes 16 | 17 | - The **quantum.*** engine is responsible for *lightObject* creation 18 | - All *lightObject* creator functions in **quantum.*** are available and reflected on **dynacam.*** as well (You can call *dynacam.newGroup()*, for example) 19 | - All *lightObject* inherit all properties and functions from the conventional *displayObject*, Additional functions and properties are listed below 20 | - All **quantum.*** constructor functions take in the same parameters as the **display.*** library, except for specified parameters listed below (Usually a normal map filename) 21 | - Physics bodies must be added with **dynacam.addBody()**, so lights can be calculated correctly for them 22 | - groups can be inserted into lightGroups, but will not work correctly the other way around. 23 | 24 | ### Gotchas 25 | - Because objects are drawn to a canvas, and the graphics engine "owns" these objects, touch, tap, and mouse listeners are forwarded using a mirror object hierarchy that sit on front of the canvas. Complex, large objects will make the engine stutter if the hierarchy is too dynamic (Objects deleted, created, moved or scaled constantly). 26 | - All default functions have been replaced with a pointer function, for your own safety do not reference it as it loses its pointed function after referencing it again, even with a different index (translate, scale, rotate, etc.) The engine will warn about unusual behavior. 27 | - Fill and effect userdata have been also replaced by a pointer table, same precautions apply. 28 | - Performance wise, light objects count as 2 display objects, event forwarded objects count as 3, so these can stack up easily, test well for performance! 29 | - Multiple cameras will share the global FPS, so 2 cameras will both work at 30 FPS if Solar2D is set to 60 FPS, 3 cameras will work at 20 FPS each, and so on. 30 | - Nil can be used in functions that specify a *baseDir* parameter, *system.ResourceDirectory* will be used. 31 | - The *baseDir* parameter is not optional anymore! use *nil* for default value 32 | - Cameras created with *vertices* will still have their touch area and default view area as a square container, so in some cases these objects will bleed out of a camera edge. 33 | 34 | ### Functions 35 | --- 36 | 37 | - dynacam.* 38 | - dynacam.*newCamera(**options**)* : Returns new *cameraObject*, all keys are optional 39 | - **options.damping** (number) Number specifying damping. Higher value means slower camera tracking. Default is 10 40 | - **options.ambientLightColor** (table) 4 Indexed table specifying RGB and intensity respective float values. Default is black *{0, 0, 0, 1}* 41 | - **options.width** (number) If specified, camera will be of specific width, or width ratio if less than 1. Full screen resize width is the default. 42 | - **options.height** (number) If specified, camera will be of specific height, or height ratio if less than 1. Full screen resize height is the default. 43 | - **options.vertices** (table) Table containing normalized (0 - 1 range) vertices for canvas shape. If specified, *.width* and *.height* options will be ignored. 44 | - **options.cullLimit** (number) Cull limit in pixel value. Lights with a position outside any edge plus/minus this number will not be rendered. default is 800. Think of it as a safe border where lights will still be rendered. 45 | - dynacam.*refresh()* 46 | - Refresh internal display values in case of a viewport dimensions change, updates fullscreen dimensions on all cameras 47 | - dynacam.*newLight(**options**)* : Creates and returns a new light object 48 | - **options.color** (table) Table containing normalized RGB and intensity values 49 | - **options.attenuationFactors** (table) Table containing *constant*, *linear* and *quadratic* light attenuation factors 50 | - **options.z** (number) Light height (0 - 1 range) 51 | - dynacam.*addBody(**object**, **...**)* 52 | - Create a light physics body. Uses same parameters as *physics.addBody()*. Used to update physics body normal rotation correctly. Lights will not work on a physics body correctly unless created by dynacam. Returns *true* if body was created and *false* if not. 53 | - dynacam.*start()* 54 | - Starts updating all cameras 55 | - dynacam.*stop()* 56 | - Stops updating all cameras 57 | - *cameraObject* 58 | - cameraObject:*add(**object**, **normal**, **back**)* 59 | - Add specified *object* to camera hierarchy. Think of it like an *:insert()* replacement: 60 | - **normal** (bool) Adds object to normal map view, use this when you only want to change the normal part of the canvas, changing the surface reflection but not the diffuse. 61 | - **back** (bool) Adds default object to default back view. This background layer is unaffected by lighting, usefull for backgrounds, for example. 62 | - cameraObject:*getZoom()* 63 | - Returns zoom value. Default is 1. 64 | - cameraObject:*setZoom(**zoom**, **zoomDelay**, **zoomTime**, **onComplete**)* 65 | - Sets camera **zoom** (number), as a scale number, higher values zoom in and smaller values zoom out 66 | - **zoomDelay** (number) delay in milliseconds before zoom begins or sets, set to 0 for instant 67 | - **zoomTime** (number) time in milliseconds for zoom to get to specified value, set to 0 for instant 68 | - **onComplete** Optional function called when zoom animation completes. 69 | - cameraObject:*setBounds(**minX**, **maxX**, **minY**, **maxY**)* 70 | - Sets camera boundaries 71 | - cameraObject:*setFocus(**object**, **options**)* 72 | - Will track and follow **object** in camera center. dynacam.*start()* has to be called for any tracking to occur. 73 | - **options.soft** (bool) If *false*, focus will be immediate 74 | - **options.trackRotation** (bool) If *true*, will track object rotation 75 | - cameraObject:*removeFocus()* 76 | - Removes any object from focus 77 | - cameraObject:*toPoint(**x**, **y**, **options**)* 78 | - Sets focus on the specified **x** and **y** coordinates. **options** are the same as *cameraObject:setFocus()* 79 | - cameraObject:*setDrawMode(**value**)* 80 | - **value** (string/bool) Can be set to one of the following: 81 | - *true* to view lights as small dots 82 | - *"diffuse"* to view diffuse frameBuffer 83 | - *"normal"* to view normal frameBuffer 84 | - *"listeners"* to view touch forward areas. 85 | - *"light"* to view lightBuffer 86 | - cameraObject:*addListenerObject(**object**)* 87 | - Internal function used to forward touch, tap and mouse events to objects owned by the camera canvas. This is done automatically and internally by all *lightObject* 88 | - quantum.* 89 | - quantum.*newGroup()* 90 | - quantum.*newLine(**...**)* 91 | - quantum.*newCircle(**x**, **y**, **radius**)* 92 | - quantum.*newRect(**x**, **y**, **width**, **height**)* 93 | - quantum.*newRoundedRect(**x**, **y**, **width**, **height**, **cornerRadius**)* 94 | - quantum.*newImage(**filename**, **normalFilename**, **baseDir**)* 95 | - **normalFilename** (string) Normal map filename 96 | - quantum.*newContainer(**width**, **height**)* 97 | - quantum.*newImageRect(**filename**, **normalFilename**, **baseDir**, **width**, **height**)* 98 | - **normalFilename** (string) Normal map filename 99 | - quantum.*newPolygon(**x**, **y**, **vertices**)* 100 | - quantum.*newMesh(**options**)* 101 | - quantum.*newText(**options**)* 102 | - **options.normal** (table) Table containing normal vector values, RGB + Intensity 103 | - quantum.*newSprite(**diffuseSheet**, **normalSheet**, **sequenceData**)* 104 | - **normalSheet** (table) same as diffuseSheet, but using normal map filename instead. 105 | - quantum.*newSnapshot(**width**, **height**)* 106 | 107 | ### Properties 108 | --- 109 | 110 | - *lightObject* 111 | - lightObject.*normal* (paint) : Supports any paint like *lightObject.fill*, but is intended for normal maps. A normal map rotation fix effect is placed by default, if removed, normal maps will stop illuminating correctly if rotated! 112 | - lightObject.*super* (table) : Table to call default display object functions that only affect the diffuse part of the object. for example: `lightObject.super:setFillColor(1)` 113 | - *cameraObject* 114 | - cameraObject.values.*targetRotation* (number) : Use this value to manually rotate the internal camera view. 115 | 116 | ### Examples 117 | --- 118 | Set up a new camera, add objects, add lights, & track an object 119 | ````lua 120 | local dynacam = require("dynacam") -- Require plugin 121 | local camera = dynacam.newCamera() -- Create a new camera and position it 122 | camera.x = display.contentCenterX 123 | camera.y = display.contentCenterY 124 | 125 | local background = dynacam.newGroup() -- Add a background layer 126 | camera:add(background) -- Add BG 127 | 128 | local bgCircle = dynacam.newCircle(0, 50) -- BG circle 129 | background:insert(bgCircle) 130 | 131 | local foreground = dynacam.newGroup() -- Add a foreground layer 132 | camera:add(foreground) -- Add FG 133 | 134 | local light = dynacam.newLight() -- Create a new light 135 | foreground:insert(light) 136 | 137 | local fgRect = dynacam.newRect(0, 0, 100, 100) -- FG rect 138 | background:insert(fgRect) 139 | 140 | dynacam.start() -- Start plugin 141 | 142 | camera:setFocus(fgRect) -- Set focus on rect, placing it on center of camera. 143 | transition.to(fgRect, {delay = 1000, time = 5000, x = 5000}) -- Camera should follow rect, leaving circle behind 144 | ```` 145 | Texturing a rect 146 | ````lua 147 | local rect = dynacam.newRect(0, 0, 500, 500) 148 | rect.fill = {type = "image", filename = "diffuse.png"} 149 | rect.normal = {type = "image", filename = "normal.png"} 150 | ```` 151 | 152 | 153 | 154 | --- 155 | Copyright (c) 2019-2021, Zetosoft 156 | All rights reserved. -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynacam", 3 | "version": "1.0", 4 | "main": "dynacam.lua", 5 | "dependencies": { 6 | 7 | }, 8 | "ignore": [ 9 | "main.lua", 10 | "config.lua", 11 | "build.settings", 12 | "README.md", 13 | "docs/" 14 | ], 15 | "private": true 16 | } 17 | 18 | -------------------------------------------------------------------------------- /build.settings: -------------------------------------------------------------------------------- 1 | settings = { 2 | orientation = { 3 | default = "landscapeRight", 4 | supported = {"landscapeLeft", "landscapeRight"}, 5 | }, 6 | 7 | win32 = { 8 | 9 | }, 10 | 11 | window = { 12 | titleText = { 13 | default = "Dynacam", 14 | }, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | application = { 2 | content = { 3 | fps = 60, 4 | width = 1000, 5 | height = 1000, 6 | scale = "zoomEven", 7 | } 8 | } -------------------------------------------------------------------------------- /docs/composite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/docs/composite.jpg -------------------------------------------------------------------------------- /docs/data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/docs/data.jpg -------------------------------------------------------------------------------- /docs/diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/docs/diffuse.jpg -------------------------------------------------------------------------------- /docs/light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/docs/light.jpg -------------------------------------------------------------------------------- /docs/normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/docs/normal.jpg -------------------------------------------------------------------------------- /dynacam.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------- Dynacam - Dynamic Lighting Camera System - Basilio Germán 2 | local moduleParams = ... 3 | local moduleName = moduleParams.name or moduleParams 4 | local requirePath = moduleParams.path or "" 5 | local projectPath = string.gsub(requirePath, "%.", "/") 6 | 7 | require(requirePath.."shaders.rotate") 8 | require(requirePath.."shaders.apply") 9 | require(requirePath.."shaders.light") 10 | 11 | local quantum = require(requirePath.."quantum") 12 | local CoronaLibrary = require("CoronaLibrary") 13 | local physics = require("physics") 14 | 15 | local dynacam = setmetatable(CoronaLibrary:new({ 16 | name = "dynacam", 17 | publisherId="com.zetosoft", 18 | version = 1, 19 | revision = 1, 20 | }), { -- Quantum provides object creation 21 | __index = function(self, index) 22 | return quantum[index] 23 | end, 24 | }) 25 | ---------------------------------------------- Variables 26 | local targetRotation 27 | local finalX, finalY 28 | local radAngle 29 | local focusRotationX, focusRotationY 30 | local rotationX, rotationY 31 | 32 | local initialized 33 | local isTracking 34 | local cameras, lights, bodies 35 | ---------------------------------------------- Constants 36 | local Q_KEY = string.format("%x", tonumber(string.match(tostring(quantum), "(0x.*)")) + 16) 37 | 38 | local CULL_LIMIT_PX = 800 39 | local RADIANS_MAGIC = math.pi / 180 -- Used to convert degrees to radians 40 | local DEFAULT_ATTENUATION = {0.4, 3, 20} 41 | local DEFAULT_AMBIENT_LIGHT = {0, 0, 0, 1} 42 | 43 | local TRANSFORM_PROPERTIES_MATCHER = { 44 | ["x"] = true, 45 | ["y"] = true, 46 | ["xScale"] = true, 47 | ["yScale"] = true, 48 | ["rotation"] = true, 49 | } 50 | local FLAG_REMOVE = "_removeFlag" 51 | local SCALE_LIGHTS = 1000 / display.viewableContentHeight 52 | ---------------------------------------------- Cache 53 | local mathAbs = math.abs 54 | local mathHuge = math.huge 55 | local mathCos = math.cos 56 | local mathSin = math.sin 57 | 58 | local tableRemove = table.remove 59 | 60 | local ccx = display.contentCenterX 61 | local ccy = display.contentCenterY 62 | local vcw = display.viewableContentWidth 63 | local vch = display.viewableContentHeight 64 | 65 | local vcwr = 1 / vcw 66 | local vchr = 1 / vch 67 | 68 | local display = display 69 | local easing = easing 70 | local transition = transition 71 | 72 | local rawset = rawset 73 | local rawget = rawget 74 | local pcall = pcall 75 | ---------------------------------------------- Metatable 76 | local touchMonitorMetatable = { -- Monitor transform changes 77 | __index = function(self, index) 78 | return self._superMetaTouch.__index(self, index) 79 | end, 80 | __newindex = function(self, index, value) 81 | if TRANSFORM_PROPERTIES_MATCHER[index] then -- Replicate transform to maskObject 82 | rawget(self, "maskObject")[index] = value 83 | end 84 | self._superMetaTouch.__newindex(self, index, value) 85 | end 86 | } 87 | ---------------------------------------------- Local functions 88 | local function finalizeAdded(event) 89 | local added = event.target 90 | rawset(added, FLAG_REMOVE, true) 91 | end 92 | 93 | local function cameraAdd(self, object, normal, back) 94 | if object.normalObject then -- Only lightObjects have a normalObject property 95 | if object.camera then -- Object belongs to another camera, track as borrowed for both cameras 96 | if object.camera ~= self then 97 | self.borrowed[#self.borrowed + 1] = object 98 | object.camera.borrowed[#object.camera.borrowed + 1] = object 99 | end 100 | else 101 | object.camera = self 102 | 103 | self.diffuseView:insert(object) 104 | self.normalView:insert(object.normalObject) 105 | end 106 | elseif normal then -- Normal object 107 | self.normalView:insert(object) 108 | elseif back then -- Display object (back) 109 | self.defaultViewBack:insert(object) 110 | else -- Display object (front) 111 | self.defaultView:insert(object) 112 | end 113 | 114 | object:addEventListener("finalize", finalizeAdded) 115 | end 116 | 117 | local function cameraSetZoom(self, zoom, zoomDelay, zoomTime, onComplete) 118 | zoom = zoom or 1 119 | zoomDelay = zoomDelay or 0 120 | zoomTime = zoomTime or 500 121 | 122 | transition.cancel(self.diffuseView) 123 | transition.cancel(self.normalView) 124 | transition.cancel(self.defaultView) 125 | transition.cancel(self.defaultViewBack) 126 | transition.cancel(self.values) 127 | 128 | if zoomDelay <= 0 and zoomTime <= 0 then 129 | self.values.zoom = zoom 130 | 131 | self.diffuseView.xScale = zoom 132 | self.diffuseView.yScale = zoom 133 | 134 | self.normalView.xScale = zoom 135 | self.normalView.yScale = zoom 136 | 137 | self.defaultView.xScale = zoom 138 | self.defaultView.yScale = zoom 139 | self.defaultViewBack.xScale = zoom 140 | self.defaultViewBack.yScale = zoom 141 | 142 | if onComplete then 143 | onComplete() 144 | end 145 | else 146 | transition.to(self.values, {zoom = zoom, time = zoomTime, delay = zoomDelay, transition = easing.inOutQuad, onComplete = onComplete}) 147 | 148 | transition.to(self.diffuseView, {xScale = zoom, yScale = zoom, time = zoomTime, delay = zoomDelay, transition = easing.inOutQuad}) 149 | transition.to(self.normalView, {xScale = zoom, yScale = zoom, time = zoomTime, delay = zoomDelay, transition = easing.inOutQuad}) 150 | 151 | transition.to(self.defaultView, {xScale = zoom, yScale = zoom, time = zoomTime, delay = zoomDelay, transition = easing.inOutQuad}) 152 | transition.to(self.defaultViewBack, {xScale = zoom, yScale = zoom, time = zoomTime, delay = zoomDelay, transition = easing.inOutQuad}) 153 | end 154 | end 155 | 156 | local function cameraGetZoom(self) 157 | return self.values.zoom 158 | end 159 | 160 | local function removeTouchArea(object) 161 | local touchArea = object.touchArea 162 | display.remove(touchArea) 163 | object.touchArea = nil 164 | end 165 | 166 | local function forwardAreaEvent(event) 167 | local touchArea = event.target 168 | if touchArea then 169 | local object = touchArea.object 170 | if object then 171 | if not rawget(object, FLAG_REMOVE) then -- Avoid sending event to destroyed one 172 | event.target = object 173 | return object:dispatchEvent(event) 174 | end 175 | end 176 | end 177 | end 178 | 179 | local function finalizeMaskedObject(event) 180 | local object = event.target 181 | 182 | display.remove(object.maskObject) 183 | object.maskObject = nil 184 | end 185 | 186 | local function protectedMaskInsert(self, newObject) 187 | local maskObject = self.maskObject 188 | local newMaskObject = self.buildMaskGroup(newObject, true, self.touchArea.color) 189 | self:entangledInsert(newObject) 190 | maskObject:insert(newMaskObject) 191 | end 192 | 193 | local function createMaskInsert(self, newObject) 194 | local status, value = pcall(protectedMaskInsert, self, newObject) 195 | if not status then 196 | error("Touch object insert failed", 2) 197 | end 198 | end 199 | 200 | local function buildMaskGroup(object, internalFlag, color) 201 | local maskObject = nil 202 | 203 | if object.numChildren then -- Is Group 204 | maskObject = display.newGroup() 205 | for index = 1, object.numChildren do 206 | local childMaskObject = buildMaskGroup(object[index], true, color) 207 | 208 | maskObject:insert(childMaskObject) 209 | end 210 | 211 | object.entangledInsert = object.entangleFunctions.insert -- Store super function 212 | object.entangleFunctions.insert = createMaskInsert -- Override inheritance 213 | object.buildMaskGroup = buildMaskGroup 214 | elseif object.path then -- ShapeObject 215 | local path = object.path 216 | 217 | local x = internalFlag and object.x or 0 218 | local y = internalFlag and object.y or 0 219 | 220 | if path.type == "rect" then 221 | maskObject = display.newRect(x, y, path.width, path.height) 222 | elseif path.type == "circle" then 223 | maskObject = display.newCircle(x, y, path.radius) 224 | elseif path.type == "roundedRect" then 225 | maskObject = display.newRoundedRect(x, y, path.width, path.height, path.radius) 226 | elseif path.type == "polygon" then 227 | maskObject = display.newPolygon(x, y, object.vertices) 228 | else -- Fallback: Mesh? TODO: implement mesh, maybe? 229 | maskObject = display.newRect(x, y, path.width or object.width, path.height or object.height) 230 | end 231 | 232 | maskObject.fill = color 233 | maskObject.x = x 234 | maskObject.y = y 235 | maskObject.anchorX = object.anchorX 236 | maskObject.anchorY = object.anchorY 237 | maskObject:scale(object.xScale, object.yScale) 238 | end 239 | 240 | object.maskObject = maskObject -- Object itself will update maskObject transform, save reference 241 | if internalFlag then -- Only child object need to be monitored 242 | local superMetaTouch = getmetatable(object) 243 | rawset(object, "_superMetaTouch", superMetaTouch) 244 | setmetatable(object, touchMonitorMetatable) 245 | object:addEventListener("finalize", finalizeMaskedObject) 246 | end 247 | 248 | return maskObject 249 | end 250 | 251 | local function buildTouchArea(camera, object) 252 | local color = (object.touchArea and object.touchArea.color) or { 253 | math.random(1, 4) / 4, 254 | math.random(1, 4) / 4, 255 | math.random(1, 4) / 4, 256 | } 257 | display.remove(object.touchArea) 258 | 259 | local touchArea = buildMaskGroup(object, nil, color) -- Works as intended, but can be replaced with rect + mask (Tried it but needs to save individual temp files, too much) 260 | touchArea.isHitTestable = true 261 | touchArea.alpha = 0.25 262 | touchArea:toFront() 263 | touchArea.color = color 264 | touchArea.object = object 265 | touchArea.camera = camera 266 | touchArea:addEventListener("tap", forwardAreaEvent) 267 | touchArea:addEventListener("touch", forwardAreaEvent) 268 | touchArea:addEventListener("mouse", forwardAreaEvent) 269 | camera.touchView:insert(touchArea) 270 | object.touchArea = touchArea 271 | end 272 | 273 | local function enterFrame(event) -- Do not refactor! performance is better 274 | local cameraIndex = (event.frame % #cameras) + 1 275 | local camera = cameras[cameraIndex] 276 | local values = camera.values 277 | 278 | -- Handle damping 279 | if values.prevDamping ~= values.damping then -- Damping changed 280 | values.prevDamping = values.damping 281 | values.dampingRatio = 1 / values.damping 282 | end 283 | 284 | -- Handle focus 285 | if values.focus then 286 | targetRotation = values.trackRotation and -values.focus.rotation or values.targetRotation 287 | 288 | -- Damp and apply rotation 289 | camera.diffuseView.rotation = (camera.diffuseView.rotation - (camera.diffuseView.rotation - targetRotation) * values.dampingRatio) 290 | camera.normalView.rotation = camera.diffuseView.rotation 291 | camera.defaultView.rotation = camera.diffuseView.rotation 292 | camera.defaultViewBack.rotation = camera.diffuseView.rotation 293 | 294 | -- Damp x and y 295 | values.currentX = (values.currentX - (values.currentX - (values.focus.x or 0)) * values.dampingRatio) 296 | values.currentY = (values.currentY - (values.currentY - (values.focus.y or 0)) * values.dampingRatio) 297 | 298 | -- Boundary checker TODO: support edges? 299 | values.currentX = values.minX < values.currentX and values.currentX or values.minX 300 | values.currentX = values.maxX > values.currentX and values.currentX or values.maxX 301 | values.currentY = values.minY < values.currentY and values.currentY or values.minY 302 | values.currentY = values.maxY > values.currentY and values.currentY or values.maxY 303 | 304 | -- Transform and calculate final position 305 | radAngle = camera.diffuseView.rotation * RADIANS_MAGIC -- Faster convert to radians 306 | focusRotationX = mathSin(radAngle) * values.currentY 307 | rotationX = mathCos(radAngle) * values.currentX 308 | finalX = (-rotationX + focusRotationX) * values.zoom 309 | 310 | focusRotationY = mathCos(radAngle) * values.currentY 311 | rotationY = mathSin(radAngle) * values.currentX 312 | finalY = (-rotationY - focusRotationY) * values.zoom 313 | 314 | -- Apply x and y 315 | camera.diffuseView.x = finalX 316 | camera.diffuseView.y = finalY 317 | 318 | camera.normalView.x = finalX 319 | camera.normalView.y = finalY 320 | 321 | camera.defaultView.x = finalX 322 | camera.defaultView.y = finalY 323 | camera.defaultViewBack.x = finalX 324 | camera.defaultViewBack.y = finalY 325 | 326 | -- Update rotation normal on all children 327 | if values.trackRotation then -- Only if global rotation has significantly changed 328 | if (camera.diffuseView.rotation - (camera.diffuseView.rotation % 1)) ~= (targetRotation - (targetRotation % 1)) then 329 | for cIndex = 1, camera.diffuseView.numChildren do 330 | camera.diffuseView[cIndex].parentRotation = camera.diffuseView.rotation 331 | end 332 | end 333 | end 334 | end 335 | 336 | -- Add borrowed objects, if any 337 | for bIndex = #camera.borrowed, 1, -1 do 338 | local borrowed = camera.borrowed[bIndex] 339 | 340 | if rawget(borrowed, FLAG_REMOVE) then 341 | tableRemove(camera.borrowed, bIndex) 342 | else 343 | camera.diffuseView:insert(borrowed) 344 | camera.normalView:insert(borrowed.normalObject) 345 | end 346 | end 347 | 348 | -- Prepare buffers 349 | camera.diffuseBuffer:draw(camera.diffuseView) 350 | camera.diffuseBuffer:invalidate({accumulate = values.accumulateBuffer}) 351 | 352 | camera.normalBuffer:draw(camera.normalView) 353 | camera.normalBuffer:invalidate({accumulate = values.accumulateBuffer}) 354 | 355 | -- Handle light drawer pooling 356 | if camera.lightDrawers.numChildren ~= #lights then 357 | local diff = #lights - camera.lightDrawers.numChildren 358 | 359 | if diff > 0 then -- Create 360 | local pixelWidth = (values.vcw or vcw) * camera.values.lightBufferScale 361 | local pixelHeight = (values.vch or vch) * camera.values.lightBufferScale 362 | 363 | local inverseBufferScale = 1 / camera.values.lightBufferScale 364 | 365 | for aIndex = 1, diff do 366 | local lightDrawer = display.newRect(0, 0, pixelWidth, pixelHeight) 367 | lightDrawer:scale(inverseBufferScale, inverseBufferScale) 368 | lightDrawer.fill = {type = "image", filename = camera.normalBuffer.filename, baseDir = camera.normalBuffer.baseDir} 369 | lightDrawer.fill.blendMode = "add" 370 | lightDrawer.fill.effect = "filter.dynacam.light" 371 | camera.lightDrawers:insert(lightDrawer) 372 | end 373 | elseif diff < 0 then -- Remove 374 | local target = camera.lightDrawers.numChildren + diff + 1 375 | for rIndex = camera.lightDrawers.numChildren, target, -1 do 376 | display.remove(camera.lightDrawers[rIndex]) 377 | end 378 | end 379 | end 380 | 381 | local vcwr = values.vcwr or vcwr 382 | local vchr = values.vchr or vchr 383 | 384 | -- Handle lights 385 | for lIndex = #lights, 1, -1 do 386 | local light = lights[lIndex] 387 | 388 | if rawget(light, FLAG_REMOVE) then 389 | tableRemove(lights, lIndex) 390 | else 391 | local x, y = light:localToContent(0, 0) 392 | 393 | light.position[1] = (x) * vcwr + 0.5 394 | light.position[2] = (y) * vchr + 0.5 395 | light.position[3] = light.z 396 | 397 | local lightDrawer = camera.lightDrawers[lIndex] 398 | 399 | -- Light Culling 400 | if (light.position[1] >= values.cullMinX) -- X 401 | and (light.position[1] <= values.cullMaxX) 402 | and (light.position[2] >= values.cullMinY) -- Y 403 | and (light.position[2] <= values.cullMaxY) then 404 | lightDrawer.alpha = 1 405 | 406 | lightDrawer.fill.effect.pointLightPos = light.position 407 | lightDrawer.fill.effect.pointLightColor = light.color 408 | lightDrawer.fill.effect.attenuationFactors = light.attenuationFactors or DEFAULT_ATTENUATION 409 | lightDrawer.fill.effect.pointLightScale = 1 / (values.zoom * light.scale * SCALE_LIGHTS) -- TODO: implement light.inverseScale -- (1 / scale) 410 | else 411 | lightDrawer.alpha = 0 412 | end 413 | end 414 | end 415 | camera.lightBuffer:draw(camera.lightDrawers) 416 | camera.lightBuffer:invalidate({accumulate = false}) 417 | 418 | -- Handle physics bodies 419 | for bIndex = #bodies, 1, -1 do 420 | local body = bodies[bIndex] 421 | 422 | if rawget(body, FLAG_REMOVE) then 423 | tableRemove(bodies, bIndex) 424 | else 425 | body.normalObject.x = body.x 426 | body.normalObject.y = body.y 427 | body.rotation = body.rotation -- This will propagate changes to normal object 428 | end 429 | end 430 | 431 | -- Handle listener objects 432 | for lIndex = #camera.listenerObjects, 1, -1 do 433 | local object = camera.listenerObjects[lIndex] 434 | 435 | if rawget(object, FLAG_REMOVE) then 436 | tableRemove(camera.listenerObjects, lIndex) 437 | 438 | removeTouchArea(object) 439 | else 440 | local x, y = object:localToContent(0, 0) 441 | 442 | -- Override our values 443 | object.touchArea.xScale = values.zoom 444 | object.touchArea.yScale = values.zoom 445 | object.touchArea.x = x 446 | object.touchArea.y = y 447 | object.touchArea.rotation = object.viewRotation 448 | end 449 | end 450 | end 451 | 452 | local function cameraSetBounds(self, minX, maxX, minY, maxY) 453 | minX = minX or -mathHuge 454 | maxX = maxX or mathHuge 455 | minY = minY or -mathHuge 456 | maxY = maxY or mathHuge 457 | 458 | if "boolean" == type(minX) or minX == nil then -- Reset camera bounds 459 | self.values.minX, self.values.maxX, self.values.minY, self.values.maxY = -mathHuge, mathHuge, -mathHuge, mathHuge 460 | else 461 | self.values.minX, self.values.maxX, self.values.minY, self.values.maxY = minX, maxX, minY, maxY 462 | end 463 | end 464 | 465 | local function cameraToPoint(self, x, y, options) 466 | local tempFocus = { 467 | x = x or ccx, 468 | y = y or ccy 469 | } 470 | 471 | self:setFocus(tempFocus, options) 472 | 473 | return tempFocus 474 | end 475 | 476 | local function cameraRemoveFocus(self) 477 | self.values.focus = nil 478 | end 479 | 480 | local function cameraSetFocus(self, object, options) 481 | options = options or {} 482 | 483 | local trackRotation = options.trackRotation 484 | local soft = options.soft 485 | 486 | if object and object.x and object.y and self.values.focus ~= object then -- Valid object and is not in focus 487 | self.values.focus = object 488 | 489 | if not soft then 490 | self.values.currentX = object.x 491 | self.values.currentY = object.y 492 | end 493 | else 494 | self.values.focus = nil 495 | end 496 | 497 | if not soft then 498 | self.diffuseView.rotation = 0 499 | self.normalView.rotation = 0 500 | self.defaultView.rotation = 0 501 | self.defaultViewBack.rotation = 0 502 | end 503 | self.values.trackRotation = trackRotation 504 | end 505 | 506 | local function finalizeCamera(event) 507 | local camera = event.target 508 | 509 | for cIndex = 1, #cameras do -- Find self and remove from camera lists 510 | if cameras[cIndex] == camera then 511 | tableRemove(cameras, cIndex) 512 | 513 | break 514 | end 515 | end 516 | 517 | -- Garbage collector will be happy 518 | camera.values = nil 519 | camera.touchView = nil 520 | camera.normalView = nil 521 | camera.diffuseView = nil 522 | camera.defaultView = nil 523 | camera.defaultViewBack = nil 524 | camera.container = nil 525 | camera.canvas = nil 526 | 527 | camera.borrowed = nil 528 | camera.listenerObjects = nil 529 | camera.lightDrawers = nil 530 | 531 | camera.ambientLightColor = nil 532 | 533 | camera.add = nil 534 | camera.addListenerObject = nil 535 | camera.getZoom = nil 536 | camera.removeFocus = nil 537 | camera.removeSelf = nil 538 | camera.setBounds = nil 539 | camera.setDrawMode = nil 540 | camera.setFocus = nil 541 | camera.setZoom = nil 542 | camera.toPoint = nil 543 | 544 | -- Release buffers 545 | camera.diffuseBuffer:releaseSelf() 546 | camera.lightBuffer:releaseSelf() 547 | camera.normalBuffer:releaseSelf() 548 | 549 | camera.diffuseBuffer = nil 550 | camera.lightBuffer = nil 551 | camera.normalBuffer = nil 552 | end 553 | 554 | local function finalizeCameraBody(event) -- Physics 555 | local body = event.target 556 | rawset(body, FLAG_REMOVE, true) 557 | end 558 | 559 | local function finalizeCameraLight(event) 560 | local light = event.target 561 | rawset(light, FLAG_REMOVE, true) 562 | end 563 | 564 | local function finalizeTouchObject(event) 565 | local object = event.target 566 | rawset(object, FLAG_REMOVE, true) 567 | end 568 | 569 | local function trackBody(body) 570 | if body and body.bodyType then 571 | bodies[#bodies + 1] = body 572 | body:addEventListener("finalize", finalizeCameraBody) 573 | end 574 | end 575 | 576 | local function trackLight(light) 577 | lights[#lights + 1] = light 578 | 579 | light:addEventListener("finalize", finalizeCameraLight) 580 | end 581 | 582 | local function cameraAddListenerObject(self, object) -- Add tap and touch forwarder rects 583 | if (object.camera == self) and (not object.touchArea) then 584 | self.listenerObjects[#self.listenerObjects + 1] = object 585 | 586 | buildTouchArea(self, object) 587 | 588 | object:addEventListener("finalize", finalizeTouchObject) -- Remove touchArea and remove from list 589 | else 590 | return false 591 | end 592 | end 593 | 594 | local function cameraSetDrawMode(self, value) 595 | self.values.debug = value 596 | 597 | self.touchView.isVisible = false 598 | if value == "light" then 599 | self.canvas.fill = {type = "image", filename = self.lightBuffer.filename, baseDir = self.lightBuffer.baseDir} 600 | elseif value == "normal" then 601 | self.canvas.fill = {type = "image", filename = self.normalBuffer.filename, baseDir = self.normalBuffer.baseDir} 602 | elseif value == "diffuse" then 603 | self.canvas.fill = {type = "image", filename = self.diffuseBuffer.filename, baseDir = self.diffuseBuffer.baseDir} 604 | elseif value == "listeners" then 605 | self.touchView.isVisible = true 606 | self.canvas.fill = self.canvas.defaultFill -- Restore saved default fill 607 | self.canvas.fill.effect = "composite.dynacam.apply" 608 | self.canvas.fill.effect.ambientLightColor = self.ambientLightColor 609 | elseif not value then -- Default 610 | self.canvas.fill = self.canvas.defaultFill -- Restore saved default fill 611 | self.canvas.fill.effect = "composite.dynacam.apply" 612 | self.canvas.fill.effect.ambientLightColor = self.ambientLightColor 613 | end 614 | 615 | for lIndex = 1, #lights do 616 | lights[lIndex].debug.isVisible = value 617 | end 618 | end 619 | 620 | local function buildPolygonCanvas(camera) 621 | local cVertices = {} 622 | for vIndex = 1, #camera.values.vertices do 623 | local side = (vIndex % 2 == 0) and vch or vcw 624 | cVertices[vIndex] = camera.values.vertices[vIndex] * side 625 | end 626 | camera.canvas = display.newPolygon(0, 0, cVertices) 627 | end 628 | 629 | local function rebuildCameraEngine(camera) 630 | if camera.canvas then -- Camera already has a canvas 631 | -- Prevent content deletion 632 | camera:insert(camera.diffuseView) 633 | camera:insert(camera.normalView) 634 | camera:insert(camera.lightDrawers) 635 | 636 | -- Release old textures 637 | camera.diffuseBuffer:releaseSelf() 638 | camera.normalBuffer:releaseSelf() 639 | camera.lightBuffer:releaseSelf() 640 | end 641 | 642 | local vcw = camera.values.vcw or vcw 643 | local vch = camera.values.vch or vch 644 | 645 | -- Update container dimensions 646 | camera.container.width = vcw 647 | camera.container.height = vch 648 | 649 | -- Recreate frame buffers 650 | local pW = vcw 651 | local pH = vch 652 | camera.diffuseBuffer = graphics.newTexture({type = "canvas", width = vcw, height = vch, pixelWidth = pW, pixelHeight = pH}) 653 | camera.normalBuffer = graphics.newTexture({type = "canvas", width = vcw, height = vch, pixelWidth = pW * camera.values.lightBufferScale, pixelHeight = pH * camera.values.lightBufferScale}) 654 | camera.lightBuffer = graphics.newTexture({type = "canvas", width = vcw, height = vch, pixelWidth = pW * camera.values.lightBufferScale, pixelHeight = pH * camera.values.lightBufferScale}) 655 | 656 | camera.normalBuffer:setBackground(0.5, 0.5, 1) 657 | 658 | -- Create or refresh canvas 659 | if not camera.canvas then -- Canvas - this is what is actually shown 660 | if camera.values.vertices then 661 | buildPolygonCanvas(camera) 662 | else 663 | camera.canvas = display.newRect(0, 0, vcw, vch) 664 | end 665 | else 666 | camera.canvas.width = vcw 667 | camera.canvas.height = vch 668 | end 669 | 670 | -- Refresh existing light drawers 671 | for lIndex = 1, camera.lightDrawers.numChildren do 672 | local lightDrawer = camera.lightDrawers[lIndex] 673 | lightDrawer.width = vcw 674 | lightDrawer.height = vch 675 | lightDrawer.fill = {type = "image", filename = camera.normalBuffer.filename, baseDir = camera.normalBuffer.baseDir} 676 | lightDrawer.fill.blendMode = "add" 677 | lightDrawer.fill.effect = "filter.dynacam.light" 678 | end 679 | 680 | -- Refresh default fill 681 | camera.canvas.defaultFill = { -- Save default fill 682 | type = "composite", 683 | paint1 = {type = "image", filename = camera.diffuseBuffer.filename, baseDir = camera.diffuseBuffer.baseDir}, 684 | paint2 = {type = "image", filename = camera.lightBuffer.filename, baseDir = camera.lightBuffer.baseDir} 685 | } 686 | camera.canvas.fill = camera.canvas.defaultFill 687 | camera.canvas.fill.effect = "composite.dynacam.apply" 688 | camera.canvas.fill.effect.ambientLightColor = camera.ambientLightColor 689 | end 690 | 691 | local function initialize() 692 | if not initialized then 693 | initialized = true 694 | 695 | cameras = {} 696 | lights = {} 697 | bodies = {} 698 | end 699 | end 700 | 701 | local function setDimensions(camera, options) 702 | local values = camera.values 703 | 704 | local oWidth = options.width 705 | local oHeight = options.height 706 | 707 | if options.vertices then 708 | values.vertices = options.vertices 709 | 710 | local tPolygon = display.newPolygon(0, 0, values.vertices) -- Use temp polygon to determine polygon width and height 711 | oWidth = vcw * tPolygon.width 712 | oHeight = vch * tPolygon.height 713 | display.remove(tPolygon) 714 | end 715 | 716 | values.vcw = oWidth 717 | values.vch = oHeight 718 | values.vcwr = oWidth and (1 / oWidth) or nil 719 | values.vchr = oHeight and (1 / oHeight) or nil 720 | 721 | -- Cull values 722 | local cullX = (options.cullLimit or CULL_LIMIT_PX) / (oWidth or vcw) 723 | local cullY = (options.cullLimit or CULL_LIMIT_PX) / (oHeight or vch) 724 | 725 | values.cullMinX = 0 - cullX 726 | values.cullMaxX = 1 + cullX 727 | 728 | values.cullMinY = 0 - cullY 729 | values.cullMaxY = 1 + cullY 730 | end 731 | ---------------------------------------------- Functions 732 | function dynacam.start() 733 | if not isTracking then 734 | isTracking = true 735 | Runtime:addEventListener("enterFrame", enterFrame) 736 | end 737 | end 738 | 739 | function dynacam.stop() 740 | if isTracking then 741 | isTracking = false 742 | Runtime:removeEventListener("enterFrame", enterFrame) 743 | end 744 | end 745 | 746 | function dynacam.refresh() 747 | ccx = display.contentCenterX 748 | ccy = display.contentCenterY 749 | vcw = display.viewableContentWidth 750 | vch = display.viewableContentHeight 751 | 752 | vcwr = 1 / vcw 753 | vchr = 1 / vch 754 | 755 | for cIndex = 1, #cameras do 756 | rebuildCameraEngine(cameras[cIndex]) 757 | end 758 | end 759 | 760 | function dynacam.newLight(options, debugLight) 761 | local light = quantum.newLight(options, debugLight, Q_KEY) 762 | trackLight(light) 763 | 764 | return light 765 | end 766 | 767 | function dynacam.addBody(object, ...) 768 | if physics.addBody(object, ...) then 769 | trackBody(object) 770 | 771 | return true 772 | end 773 | return false 774 | end 775 | 776 | function dynacam.newCamera(options) 777 | options = options or {} 778 | 779 | local damping = options.damping or 10 780 | local ambientLightColor = options.ambientLightColor or DEFAULT_AMBIENT_LIGHT 781 | 782 | local camera = display.newGroup() 783 | 784 | camera.values = { 785 | -- Size stuff set in `setDimensions()` 786 | vertices = nil, 787 | vcw = nil, 788 | vch = nil, 789 | vcwr = nil, 790 | vchr = nil, 791 | cullMinX = nil, 792 | cullMinY = nil, 793 | 794 | -- Camera Limits 795 | minX = -mathHuge, 796 | maxX = mathHuge, 797 | minY = -mathHuge, 798 | maxY = mathHuge, 799 | 800 | -- Camera rotation 801 | targetRotation = 0, 802 | 803 | -- Damping & internal stuff 804 | damping = damping, -- Can be used to transition 805 | prevDamping = damping, -- Used to check damping changes 806 | dampingRatio = 1 / damping, -- Actual value used, pre divide 807 | currentX = 0, -- Internal 808 | currentY = 0, -- Internal 809 | 810 | -- Zoom 811 | zoom = options.zoom or 1, 812 | 813 | -- Flags 814 | accumulateBuffer = false, 815 | trackRotation = false, 816 | debug = false, 817 | 818 | -- Buffer scaling 819 | lightBufferScale = options.lightBufferScale or 1 820 | } 821 | 822 | setDimensions(camera, options) 823 | 824 | camera.diffuseView = display.newGroup() 825 | camera.normalView = display.newGroup() 826 | 827 | camera.defaultView = display.newGroup() 828 | camera.defaultViewBack = display.newGroup() 829 | 830 | camera.container = display.newContainer(camera.values.vcw or vcw, camera.values.vch or vch) 831 | 832 | 833 | camera.touchView = display.newGroup() 834 | camera.touchView.isVisible = false 835 | camera.touchView.isHitTestable = true 836 | 837 | 838 | camera.borrowed = {} 839 | camera.listenerObjects = {} -- Touch & tap proxies 840 | camera.lightDrawers = display.newGroup() 841 | camera.ambientLightColor = ambientLightColor 842 | 843 | rebuildCameraEngine(camera) 844 | 845 | camera.container:insert(camera.defaultViewBack) 846 | camera.container:insert(camera.canvas) 847 | camera.container:insert(camera.defaultView) 848 | camera.container:insert(camera.touchView) 849 | 850 | camera:insert(camera.container) 851 | 852 | camera.add = cameraAdd 853 | camera.setZoom = cameraSetZoom 854 | camera.getZoom = cameraGetZoom 855 | camera.setBounds = cameraSetBounds 856 | 857 | camera.setFocus = cameraSetFocus 858 | camera.removeFocus = cameraRemoveFocus 859 | camera.toPoint = cameraToPoint 860 | 861 | camera.addListenerObject = cameraAddListenerObject 862 | 863 | camera.setDrawMode = cameraSetDrawMode 864 | 865 | camera:addEventListener("finalize", finalizeCamera) 866 | 867 | cameras[#cameras + 1] = camera 868 | 869 | return camera 870 | end 871 | ---------------------------------------------- 872 | initialize() 873 | 874 | return dynacam 875 | -------------------------------------------------------------------------------- /images/arrow_128x88_horizontal_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/arrow_128x88_horizontal_normal.png -------------------------------------------------------------------------------- /images/arrow_128x88_horizontal_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/arrow_128x88_horizontal_texture.png -------------------------------------------------------------------------------- /images/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/bg1.png -------------------------------------------------------------------------------- /images/brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/brick.png -------------------------------------------------------------------------------- /images/brick_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/brick_n.png -------------------------------------------------------------------------------- /images/powerup_health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/powerup_health.png -------------------------------------------------------------------------------- /images/powerup_health_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/powerup_health_n.png -------------------------------------------------------------------------------- /images/spaceship_carrier_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/spaceship_carrier_01.png -------------------------------------------------------------------------------- /images/spaceship_carrier_01_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/spaceship_carrier_01_n.png -------------------------------------------------------------------------------- /images/spaceship_carrier_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/spaceship_carrier_02.png -------------------------------------------------------------------------------- /images/spaceship_carrier_02_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/spaceship_carrier_02_n.png -------------------------------------------------------------------------------- /images/spinning_coin_gold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/spinning_coin_gold.png -------------------------------------------------------------------------------- /images/spinning_coin_gold_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/spinning_coin_gold_n.png -------------------------------------------------------------------------------- /images/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/wall.png -------------------------------------------------------------------------------- /images/wall_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zetosoft/dynacam/b9cdd43d9dac1b5c6f6e49454962c0e1bedfa3c8/images/wall_n.png -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------- Dynacam small demo 2 | local dynacam = require("dynacam") 3 | ---------------------------------------------- Setup 4 | local ccx = display.contentCenterX 5 | local ccy = display.contentCenterY 6 | 7 | local camera = dynacam.newCamera() -- Default camera 8 | camera.x = ccx 9 | camera.y = ccy 10 | ---------------------------------------------- Object creation 11 | local iTextOptions = { 12 | x = 0, 13 | y = 0, 14 | font = native.systemFontBold, 15 | fontSize = 60, 16 | text = "See README.md for more info", 17 | normal = {0.5, 0.5, 1}, -- Facing straight at camera 18 | } 19 | local infoText = dynacam.newText(iTextOptions) 20 | camera:add(infoText) 21 | camera:setFocus(infoText) -- Centers camera on infoText 22 | 23 | local lightOptions = {x = 0, y = 0, z = 0.1, color = {1, 1, 1, 1}, attenuationFactors = {0.1, 2, 5}} 24 | local rotatingLight = dynacam.newLight(lightOptions) 25 | camera:add(rotatingLight) 26 | ---------------------------------------------- Start demo 27 | dynacam.start() 28 | 29 | -- Light rotate and color change 30 | Runtime:addEventListener("enterFrame", function(event) 31 | local counter = (event.frame + 1) % (360 * 4) 32 | local angle = math.rad(counter * 0.25) 33 | rotatingLight.x = math.cos(angle) * infoText.width * 0.15 34 | rotatingLight.y = math.sin(angle) * infoText.width * 0.15 35 | 36 | -- Cycle colors 37 | rotatingLight.color[1] = math.cos(angle * 2) -- Vary R component 38 | rotatingLight.color[2] = math.sin(angle * 2) -- Vary G component 39 | end) 40 | 41 | camera:setDrawMode(true) 42 | -------------------------------------------------------------------------------- /quantum.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------- Quantum - Light object creation - Basilio Germán 2 | local moduleName = ... 3 | local upRequire = string.match(moduleName or "", "(.*[%.])") or "" 4 | 5 | local normalShaders = require(upRequire.."shaders.normal") 6 | 7 | local quantum = { 8 | utils = { 9 | copy = function(fTable) -- Simple copy 10 | local copy = {} 11 | for key, value in pairs(fTable) do 12 | copy[key] = value 13 | end 14 | return copy 15 | end, 16 | merge = function(table1, table2) -- Simple merge 17 | local result = {} 18 | for key, value in pairs(table1) do 19 | result[key] = value 20 | end 21 | for key, value in pairs(table2) do 22 | result[key] = value 23 | end 24 | return result 25 | end, 26 | } 27 | } 28 | ---------------------------------------------- Caches 29 | local tableRemove = table.remove 30 | local pcall = pcall 31 | ---------------------------------------------- Variables 32 | ---------------------------------------------- Local functions 1 33 | local function protectedInsert(self, lightObject) 34 | if self.super then 35 | if lightObject.normalObject then 36 | self.super:insert(lightObject) 37 | self.normalObject:insert(lightObject.normalObject) 38 | 39 | lightObject.camera = self.camera 40 | lightObject.parentRotation = self.viewRotation -- Let metatable update efefct 41 | else -- Not light object! 42 | self.super:insert(lightObject) 43 | 44 | return true 45 | end 46 | end 47 | end 48 | 49 | local function lightInsert(self, lightObject) 50 | local status, value = pcall(protectedInsert, self, lightObject) 51 | if not status then 52 | error("insert failed!", 2) 53 | end 54 | 55 | return value 56 | end 57 | 58 | local function monitorAddedEvents(self, eventName, eventFunction) -- Metatable called function 59 | if eventName == "tap" or eventName == "touch" or eventName == "mouse" then 60 | if self.camera then 61 | self.camera:addListenerObject(self) 62 | else 63 | self.forwardEvents = true 64 | end 65 | end 66 | 67 | return self.super:addEventListener(eventName, eventFunction) 68 | end 69 | ---------------------------------------------- Constants 70 | local Q_KEY = string.format("%x", tonumber(string.match(tostring(quantum), "(0x.*)")) + 16) 71 | 72 | local DEFAULT_NORMAL = {0.5, 0.5, 1.0} 73 | local DEFAULT_Z = 0.2 74 | local DEFAULT_ATTENUATION = {0.4, 3, 20} 75 | local DEFAULT_LIGHT_COLOR = {1, 1, 1, 1} 76 | 77 | local QUICK_DEFAULT_INDEX = { -- Skip all compare and return expected value 78 | ["numChildren"] = true, 79 | ["rotation"] = true, 80 | ["parent"] = true, 81 | ["localToContent"] = true, 82 | ["x"] = true, 83 | ["y"] = true, 84 | ["xScale"] = true, 85 | ["yScale"] = true, 86 | } 87 | 88 | local FUNCTIONS_DISPLAY = { 89 | ["rotate"] = true, 90 | ["scale"] = true, 91 | ["setMask"] = true, 92 | ["toBack"] = true, 93 | ["toFront"] = true, 94 | ["translate"] = true, 95 | ["removeSelf"] = true, 96 | ["addEventListener"] = monitorAddedEvents, 97 | } 98 | local FUNCTIONS = { 99 | DISPLAY = FUNCTIONS_DISPLAY, 100 | SPRITE = quantum.utils.merge(FUNCTIONS_DISPLAY, { 101 | ["play"] = true, 102 | ["pause"] = true, 103 | ["setFrame"] = true, 104 | ["setSequence"] = true 105 | }), 106 | SNAPSHOT = quantum.utils.merge(FUNCTIONS_DISPLAY, { 107 | ["invalidate"] = true, 108 | ["insert"] = lightInsert, 109 | }), 110 | LINE = quantum.utils.merge(FUNCTIONS_DISPLAY, { 111 | ["append"] = true 112 | }), 113 | GROUP = quantum.utils.merge(FUNCTIONS_DISPLAY, { -- Container too 114 | ["insert"] = lightInsert, 115 | }) 116 | } 117 | 118 | local HIT_REFRESH = { -- Refresh touch area on newindex 119 | ["alpha"] = true, 120 | ["isVisible"] = true, 121 | ["isHitTestable"] = true, 122 | } 123 | ---------------------------------------------- Metatables 124 | local meshPathEntangleMetatable = { -- used to intercept mesh path functions and replicate to normal 125 | __index = function(self, index) 126 | if index == "path" then 127 | return self.pathFunctions 128 | end 129 | return self._superMetaMesh.__index(self, index) 130 | end, 131 | __newindex = function(self, index, value) 132 | self._superMetaMesh.__newindex(self, index, value) 133 | end 134 | } 135 | 136 | local effectProxyMetatable = { 137 | __index = function(self, index) 138 | return self.effect[index] 139 | end, 140 | __newindex = function(self, index, value) 141 | if self.normalObject.fill.effect.effect then -- Update normal version of effect (Indexed at .effect) 142 | self.normalObject.fill.effect.effect[index] = value 143 | end 144 | 145 | self.effect[index] = value 146 | end, 147 | } 148 | 149 | local fillProxyMetatable = { -- Used to intercept .fill transform changes and replicate to normal 150 | __index = function(self, index) 151 | if index == "effect" then 152 | if self.fill.effect then 153 | rawset(self.effectProxy, "effect", self.fill.effect) 154 | return self.effectProxy -- Effect proxy can now be modified 155 | end 156 | end 157 | return self.fill[index] 158 | end, 159 | __newindex = function(self, index, value) 160 | if index == "effect" then -- Get same effect in normal variant 161 | self.normalObject.fill.effect = normalShaders.getEffect(value) 162 | else -- x, y, scaleX, scaleY, colors, etc 163 | self.normalObject.fill[index] = value 164 | end 165 | 166 | self.fill[index] = value 167 | end, 168 | } 169 | 170 | local dualityTable -- Save self reference for paradox 171 | dualityTable = setmetatable({ 172 | object = nil, 173 | index = nil, 174 | warn = nil, 175 | called = true, 176 | proxyFunction = function(...) -- Used instead as it fixes `type()` behavior 177 | return dualityTable(...) -- Paradox occurs here 178 | end, 179 | }, { 180 | __call = function(self, object, ...) 181 | if object == self.object then 182 | 183 | self.called = true 184 | if self.warn then 185 | print("WARNING: Expected "..self.warn.." to be called before "..self.index.."") 186 | self.warn = nil 187 | end 188 | 189 | if object.normalObject then 190 | object.normalObject[self.index](object.normalObject, ...) 191 | end 192 | 193 | local superFunction = object._superMeta.__index(object, self.index) 194 | 195 | return superFunction(object, ...) 196 | elseif object then 197 | error("ERROR: Invalid object", 2) 198 | else 199 | error("ERROR: Missing object", 2) 200 | end 201 | end, 202 | }) 203 | 204 | local inheritFunction = setmetatable({ 205 | object = nil, 206 | index = nil, 207 | }, { 208 | __call = function(self, inheritTable, ...) 209 | if inheritTable.object == self.object then 210 | local superFunction = self.object._superMeta.__index(self.object, self.index) 211 | return superFunction(self.object, ...) 212 | elseif object then 213 | error("ERROR: Invalid object", 2) 214 | else 215 | error("ERROR: Missing object", 2) 216 | end 217 | end, 218 | }) 219 | 220 | local inheritTable = setmetatable({ -- Returned when `.super` is used, to forward default functions 221 | object = nil, 222 | }, { 223 | __index = function(self, index) 224 | local objectValue = self.object._superMeta.__index(self.object, index) 225 | 226 | if type(objectValue) == "function" then 227 | rawset(inheritFunction, "object", self.object) 228 | rawset(inheritFunction, "index", index) 229 | return inheritFunction 230 | end 231 | 232 | return objectValue 233 | end, 234 | }) 235 | 236 | local entangleMetatable = { 237 | __index = function(self, index) 238 | if QUICK_DEFAULT_INDEX[index] then -- Shortcut for default values 239 | return self._superMeta.__index(self, index) 240 | elseif index == "parentRotation" then -- .parent can be nil apparently when deleting object 241 | return self.parent and self.parent.viewRotation -- Will be nil once we hit normal objects in hierarchy 242 | elseif index == "fill" then 243 | rawset(self.fillProxy, "fill", self._superMeta.__index(self, index)) -- Update original fill & normal reference in proxy, skipping metamethods 244 | rawset(self.fillProxy, "normal", self.normalObject.fill) 245 | return self.fillProxy -- Fill proxy can now be modified 246 | elseif index == "normal" then 247 | return self.normalObject.fill 248 | elseif index == "camera" then 249 | return self._camera 250 | elseif index == "super" then 251 | rawset(inheritTable, "object", self) -- Skip metatable 252 | return inheritTable 253 | end 254 | 255 | if self.entangleFunctions[index] then 256 | if type(self.entangleFunctions[index]) == "function" then 257 | return self.entangleFunctions[index] 258 | end 259 | 260 | local message 261 | if not dualityTable.called then 262 | if index ~= dualityTable.index then -- Function was previously stored but not called, warn user on execution 263 | dualityTable.warn = dualityTable.index 264 | end 265 | end 266 | 267 | rawset(dualityTable, "object", self) 268 | rawset(dualityTable, "index", index) 269 | dualityTable.called = false 270 | 271 | return dualityTable.proxyFunction, message 272 | end 273 | 274 | return self._superMeta.__index(self, index) 275 | end, 276 | __newindex = function(self, index, value) 277 | local normalObject = self.normalObject 278 | 279 | if index == "parentRotation" then -- Parent is telling us to update our view rotation 280 | self.viewRotation = value + self.rotation 281 | 282 | if normalObject.fill and normalObject.fill.effect then 283 | normalObject.fill.effect.rotate.rotation = math.rad(self.viewRotation + (self.fill.rotation or 0)) -- Fill might be rotated 284 | end 285 | 286 | if self.numChildren then 287 | for cIndex = 1, self.numChildren do 288 | local lightObject = self[cIndex] 289 | 290 | lightObject.parentRotation = self.viewRotation 291 | end 292 | end 293 | elseif index == "normal" then -- Forward normal fill 294 | if normalObject.fill then 295 | normalObject.fill = value 296 | if value then 297 | normalObject.fill.effect = normalShaders.getEffect() 298 | normalObject.fill.effect.rotate.rotation = math.rad(self.viewRotation + self.fill.rotation) -- Fill might be rotated 299 | end 300 | end 301 | elseif index == "camera" and value then 302 | rawset(self, "_camera", value) 303 | 304 | if self.forwardEvents then 305 | value:addListenerObject(self) -- value is `_camera` 306 | end 307 | 308 | if self.numChildren then 309 | for cIndex = 1, self.numChildren do 310 | local lightObject = self[cIndex] 311 | 312 | lightObject.camera = value 313 | end 314 | end 315 | else 316 | normalObject[index] = value -- Send values to entangled pair 317 | self._superMeta.__newindex(self, index, value) 318 | 319 | if HIT_REFRESH[index] and rawget(self, "touchArea") then 320 | local touchArea = rawget(self, "touchArea") 321 | touchArea.isHitTestable = (self.isVisible and (self.alpha > 0)) or self.isHitTestable 322 | end 323 | 324 | if (index == "rotation") and value then -- Propagate rotation change 325 | -- Rotation was already set in _superMeta 326 | self.viewRotation = (self.parentRotation or 0) + value -- parentRotation can be nil 327 | 328 | if self.numChildren then 329 | for cIndex = 1, self.numChildren do 330 | local lightObject = self[cIndex] 331 | 332 | lightObject.parentRotation = self.viewRotation 333 | end 334 | else 335 | if normalObject.fill and normalObject.fill.effect then 336 | normalObject.fill.effect.rotate.rotation = math.rad(self.viewRotation + (self.fill.rotation or 0)) -- Fill might be rotated 337 | end 338 | end 339 | end 340 | end 341 | end, 342 | } 343 | ---------------------------------------------- Constants 344 | ---------------------------------------------- Local functions 345 | local function finalizeEntangledObject(event) 346 | local lightObject = event.target 347 | 348 | display.remove(lightObject.normalObject) 349 | lightObject.normalObject = nil 350 | end 351 | 352 | local function entangleObject(lightObject, entangleFunctions) -- Basic light object principle, where we make object pairs in different worlds (diffuse & normal) 353 | entangleFunctions = entangleFunctions or FUNCTIONS.DISPLAY 354 | 355 | lightObject.viewRotation = 0 356 | 357 | -- Fill & Effect are replaced by proxies that forward set values to diffuse and normal objects at the same time. 358 | local effectProxy = { -- All individual objects have unique fill and effect proxies 359 | normalObject = lightObject.normalObject, -- Needed to update effect 360 | effect = nil, -- Set during meta query (fill) 361 | } 362 | 363 | local fillProxy = { 364 | normalObject = lightObject.normalObject, -- Needed to update fill 365 | effectProxy = setmetatable(effectProxy, effectProxyMetatable), 366 | fill = nil, -- Set during meta query (fill) 367 | } 368 | lightObject.fillProxy = setmetatable(fillProxy, fillProxyMetatable) 369 | 370 | lightObject.entangleFunctions = quantum.utils.copy(entangleFunctions) -- Own reference to entangle Functions 371 | 372 | local superMeta = getmetatable(lightObject) 373 | rawset(lightObject, "_superMeta", superMeta) 374 | setmetatable(lightObject, entangleMetatable) 375 | 376 | lightObject:addEventListener("finalize", finalizeEntangledObject) 377 | end 378 | ---------------------------------------------- Module functions 379 | function quantum.newLight(options, debugLight, key) -- Only meant to be used internally by dynacam 380 | options = options or {} 381 | 382 | if key ~= Q_KEY then -- Prevent devs from calling this function, as names are the same 383 | error("Invalid key, light creation has been disabled", 2) 384 | end 385 | 386 | local z = options.z or DEFAULT_Z 387 | local color = options.color or DEFAULT_LIGHT_COLOR -- New instance of white 388 | local scale = options.scale or 1 389 | local attenuationFactors = options.attenuationFactors or DEFAULT_ATTENUATION -- Default attenuation here as we don't have table copy 390 | 391 | local light = display.newGroup() 392 | light.normalObject = display.newGroup() 393 | 394 | light.debug = display.newCircle(light, 0, 0, 5) 395 | light.debug.isVisible = debugLight 396 | 397 | entangleObject(light) 398 | 399 | light.position = {options.x or 0, options.y or 0, z} -- Internal table, auto updates for fast shader data pass 400 | light.scale = scale 401 | light.z = z 402 | light.attenuationFactors = quantum.utils.copy(attenuationFactors) 403 | light.color = quantum.utils.copy(color) 404 | 405 | return light 406 | end 407 | 408 | function quantum.newGroup() 409 | local lightGroup = display.newGroup() 410 | lightGroup.normalObject = display.newGroup() 411 | 412 | entangleObject(lightGroup, FUNCTIONS.GROUP) 413 | 414 | return lightGroup 415 | end 416 | 417 | function quantum.newCircle(x, y, radius) 418 | local lightCircle = display.newCircle(x, y, radius) 419 | local normalCircle = display.newCircle(x, y, radius) 420 | 421 | return quantum.newLightObject(lightCircle, normalCircle) 422 | end 423 | 424 | function quantum.newContainer(width, height) 425 | local lightContainer = display.newContainer(width, height) 426 | lightContainer.normalObject = display.newContainer(width, height) 427 | 428 | entangleObject(lightContainer, FUNCTIONS.GROUP) 429 | 430 | return lightContainer 431 | end 432 | 433 | function quantum.newImage(filename, normalFilename, baseDir) 434 | baseDir = baseDir or system.ResourceDirectory 435 | 436 | local lightImage = display.newImage(filename, baseDir) 437 | local normalImage = display.newImage(normalFilename, baseDir) 438 | 439 | return quantum.newLightObject(lightImage, normalImage) 440 | end 441 | 442 | function quantum.newImageRect(filename, normalFilename, baseDir, width, height) 443 | baseDir = baseDir or system.ResourceDirectory 444 | 445 | local lightImageRect = display.newImageRect(filename, baseDir, width, height) 446 | local normalImageRect = display.newImageRect(normalFilename, baseDir, width, height) 447 | 448 | return quantum.newLightObject(lightImageRect, normalImageRect) 449 | end 450 | 451 | function quantum.newLine(...) 452 | local lightLine = display.newLine(...) 453 | local normalLine = display.newLine(...) 454 | normalLine:setStrokeColor(unpack(DEFAULT_NORMAL)) -- Normal vector facing up 455 | 456 | return quantum.newLightObject(lightLine, normalLine, FUNCTIONS.LINE) 457 | end 458 | 459 | function quantum.newMesh(options) 460 | local lightMesh = display.newMesh(options) 461 | local normalMesh = display.newMesh(options) 462 | 463 | lightMesh.pathFunctions = { 464 | type = "mesh", 465 | path = lightMesh.path, 466 | normalPath = normalMesh.path, 467 | 468 | setVertex = function(self, index, x, y) 469 | self.path:setVertex(index, x, y) 470 | self.normalPath:setVertex(index, x, y) 471 | end, 472 | getVertex = function(self, index) 473 | return self.path:getVertex(index) 474 | end, 475 | setUV= function(self, index, u, v) 476 | self.path:setUV(index, u, v) 477 | self.normalPath:setUV(index, u, v) 478 | end, 479 | getUV = function(self, index) 480 | return self.path:getUV(index) 481 | end, 482 | getVertexOffset = function(self) 483 | return self.path:getVertexOffset() 484 | end 485 | } 486 | 487 | normalMesh.fill.effect = normalShaders.getEffect() 488 | 489 | lightMesh.normalObject = normalMesh 490 | entangleObject(lightMesh) 491 | 492 | local superMetaMesh = getmetatable(lightMesh) 493 | rawset(lightMesh, "_superMetaMesh", superMetaMesh) 494 | setmetatable(lightMesh, meshPathEntangleMetatable) 495 | 496 | return lightMesh 497 | end 498 | 499 | function quantum.newPolygon(x, y, vertices) 500 | local lightPolygon = display.newPolygon(x, y, vertices) 501 | local normalPolygon = display.newPolygon(x, y, vertices) 502 | 503 | lightPolygon.vertices = quantum.utils.copy(vertices) -- Save vertices in case of touch listener mask rebuild 504 | 505 | return quantum.newLightObject(lightPolygon, normalPolygon) 506 | end 507 | 508 | function quantum.newRoundedRect(x, y, width, height, cornerRadius) 509 | local lightRoundedRect = display.newRoundedRect(x, y, width, height, cornerRadius) 510 | local normalRoundedRect = display.newRoundedRect(x, y, width, height, cornerRadius) 511 | 512 | return quantum.newLightObject(lightRoundedRect, normalRoundedRect) 513 | end 514 | 515 | function quantum.newSnapshot(width, height) 516 | local lightSnapshot = display.newSnapshot(width, height) 517 | local normalSnapshot = display.newSnapshot(width, height) 518 | 519 | return quantum.newLightObject(lightSnapshot, normalSnapshot, FUNCTIONS.SNAPSHOT) 520 | end 521 | 522 | function quantum.newText(options) 523 | options = options or {} 524 | local normal = options.normal or quantum.utils.copy(DEFAULT_NORMAL) 525 | 526 | local lightText = display.newText(options) 527 | local normalText = display.newText(options) 528 | normalText.fill = normal 529 | 530 | lightText.normalObject = normalText 531 | entangleObject(lightText) 532 | 533 | return lightText 534 | end 535 | 536 | function quantum.newRect(x, y, width, height) 537 | local lightRect = display.newRect(x, y, width, height) 538 | local normalRect = display.newRect(x, y, width, height) 539 | 540 | return quantum.newLightObject(lightRect, normalRect) 541 | end 542 | 543 | function quantum.newSprite(diffuseSheet, normalSheet, sequenceData) 544 | local lightSprite = display.newSprite(diffuseSheet, sequenceData) 545 | local normalSprite = display.newSprite(normalSheet, sequenceData) 546 | 547 | return quantum.newLightObject(lightSprite, normalSprite, FUNCTIONS.SPRITE) 548 | end 549 | 550 | -- Used internally to create lightObjects 551 | function quantum.newLightObject(diffuseObject, normalObject, entangleFunctions) 552 | diffuseObject.normalObject = normalObject 553 | 554 | if normalObject.fill then 555 | normalObject.fill.effect = normalShaders.getEffect() -- Default normal shader 556 | end 557 | 558 | entangleObject(diffuseObject, entangleFunctions) 559 | 560 | return diffuseObject 561 | end 562 | 563 | return quantum -------------------------------------------------------------------------------- /shaders/apply.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------- Apply shader - Mixes composites for final shaded result - Basilio Germán 2 | display.setDefault("isShaderCompilerVerbose", true) 3 | 4 | local kernel = {} 5 | 6 | kernel.language = "glsl" 7 | kernel.category = "composite" 8 | kernel.group = "dynacam" 9 | kernel.name = "apply" 10 | kernel.uniformData = 11 | { 12 | { 13 | name = "ambientLightColor", 14 | default = {0, 0, 0, 1}, -- Color RGB and intensity (alpha) 15 | min = {0, 0, 0, 0}, 16 | max = {1, 1, 1, 1}, 17 | type = "vec4", 18 | index = 0, -- u_UserData0 19 | }, 20 | } 21 | 22 | kernel.vertex = [[ 23 | uniform P_COLOR vec4 u_UserData0; // ambientLightColor 24 | 25 | varying P_COLOR vec3 ambientLightColor; 26 | 27 | P_POSITION vec2 VertexKernel(P_POSITION vec2 position) { 28 | // Pre-multiply the light color with intensity 29 | ambientLightColor = (u_UserData0.rgb * u_UserData0.a); 30 | 31 | return position; 32 | } 33 | ]] 34 | 35 | kernel.fragment = [[ 36 | uniform P_COLOR vec4 u_UserData0; // ambientLightColor 37 | 38 | varying P_COLOR vec3 ambientLightColor; 39 | 40 | P_COLOR vec4 FragmentKernel(P_UV vec2 texCoord) { 41 | P_NORMAL float proportion = CoronaTexelSize.y / CoronaTexelSize.x; 42 | 43 | // Diffuse color 44 | P_COLOR vec4 diffuseColor = texture2D(u_FillSampler0, texCoord); 45 | 46 | // Lightmap color 47 | P_NORMAL vec4 lightBuffer = texture2D(u_FillSampler1, texCoord); 48 | 49 | // Intensity map 50 | P_COLOR vec3 bufferColor = lightBuffer.xyz; 51 | P_NORMAL float nothing = lightBuffer.w; 52 | 53 | diffuseColor.rgb *= ambientLightColor + bufferColor; 54 | 55 | return (diffuseColor * v_ColorScale); 56 | } 57 | ]] 58 | 59 | graphics.defineEffect(kernel) 60 | 61 | return kernel 62 | -------------------------------------------------------------------------------- /shaders/light.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------- Light shader - Lightmap is processed for later adding on a framebuffer - Basilio Germán 2 | display.setDefault("isShaderCompilerVerbose", true) 3 | 4 | local kernel = {} 5 | 6 | kernel.language = "glsl" 7 | kernel.category = "filter" 8 | kernel.group = "dynacam" 9 | kernel.name = "light" 10 | kernel.uniformData = 11 | { 12 | { 13 | name = "pointLightColor", 14 | default = {1, 1, 1, 1}, -- Color RGB and intensity (alpha) 15 | min = {0, 0, 0, 0}, 16 | max = {1, 1, 1, 1}, 17 | type = "vec4", 18 | index = 0, -- u_UserData0 19 | }, 20 | { 21 | name = "pointLightPos", 22 | default = {0.5, 0.5, 0.5}, -- x, y, z 23 | min = {0, 0, 0}, 24 | max = {1, 1, 1}, 25 | type = "vec3", 26 | index = 1, -- u_UserData1 27 | }, 28 | { 29 | name = "attenuationFactors", 30 | default = {0.4, 3, 20}, -- Constant, Linear, Quadratic 31 | type = "vec3", 32 | index = 2, -- u_UserData2 33 | }, 34 | { 35 | name = "pointLightScale", 36 | default = 1, 37 | type = "scalar", 38 | index = 3, -- u_UserData3 39 | }, 40 | } 41 | 42 | kernel.vertex = [[ 43 | uniform P_COLOR vec4 u_UserData0; // pointLightColor 44 | uniform P_UV vec3 u_UserData1; // pointLightPos 45 | uniform P_COLOR vec3 u_UserData2; // attenuationFactors 46 | uniform P_DEFAULT float u_UserData3; // pointLightScale 47 | 48 | varying P_COLOR vec3 pointLightColor; 49 | 50 | P_POSITION vec2 VertexKernel(P_POSITION vec2 position) { 51 | // Pre-multiply the light color with intensity 52 | pointLightColor = (u_UserData0.rgb * u_UserData0.a); 53 | 54 | return position; 55 | } 56 | ]] 57 | 58 | kernel.fragment = [[ 59 | uniform P_COLOR vec4 u_UserData0; // pointLightColor 60 | uniform P_UV vec3 u_UserData1; // pointLightPos 61 | uniform P_COLOR vec3 u_UserData2; // attenuationFactors 62 | uniform P_DEFAULT float u_UserData3; // pointLightScale 63 | 64 | varying P_COLOR vec3 pointLightColor; 65 | 66 | P_UV float GetDistanceAttenuation(in P_UV vec3 attenuationFactors, in P_UV float lightDistance) { 67 | P_UV float constantFactor = attenuationFactors.x; 68 | P_UV float linearFactor = attenuationFactors.y; 69 | P_UV float quadraticFactor = attenuationFactors.z; 70 | 71 | // Calculate attenuation 72 | P_UV float constantAtt = constantFactor; 73 | P_UV float linearAtt = (linearFactor * lightDistance); 74 | P_UV float quadraticAtt = (quadraticFactor * lightDistance * lightDistance); 75 | 76 | return (1.0 / (constantAtt + linearAtt + quadraticAtt)); 77 | } 78 | 79 | P_COLOR vec4 FragmentKernel(P_UV vec2 texCoord) { 80 | P_UV vec3 pointLightPos = u_UserData1; 81 | 82 | // Get normal map pixel values 83 | P_NORMAL vec3 normalPixel = texture2D( u_FillSampler0, texCoord).xyz; 84 | 85 | // Transform from 0.0 <> 1.0 to -1.0 <> 1.0 range. 86 | normalPixel.xyz = normalize((normalPixel.xyz * 2.0) - 1.0); 87 | 88 | // Invert Y component as Corona is inverted 89 | normalPixel.y = -normalPixel.y; 90 | 91 | // Fix scale proportion 92 | P_NORMAL float proportion = CoronaTexelSize.y / CoronaTexelSize.x; 93 | P_UV vec3 fragmentToLight = (pointLightPos - vec3(texCoord, 0.0)); 94 | fragmentToLight.x *= proportion; 95 | fragmentToLight.xy *= u_UserData3; 96 | 97 | P_UV vec3 lightDirection = normalize(fragmentToLight); 98 | 99 | // Distance attenuation. 100 | P_UV float attenuation = GetDistanceAttenuation( u_UserData2, length(fragmentToLight)); 101 | 102 | // Apply light intensity, avoid negative intensities 103 | P_UV float diffuseIntensity = max(dot(lightDirection, normalPixel), 0.0); 104 | 105 | // Apply light distance attenuation. 106 | diffuseIntensity *= attenuation; 107 | 108 | // Add point light color. 109 | P_COLOR vec4 lightColor = vec4(pointLightColor * diffuseIntensity, 1.0); 110 | 111 | //////////////////////////////////////////////////////////////////////////////////////////////// 112 | #if 0 // Debug and testing 113 | // Adjust for resolution 114 | P_UV vec2 adjustedTexCoord = vec2(texCoord); 115 | adjustedTexCoord.x *= proportion; 116 | 117 | P_UV vec2 adjustedPos = vec2(pointLightPos.xy); 118 | adjustedPos.x *= proportion; 119 | 120 | P_UV float lightDistance = distance(adjustedTexCoord, adjustedPos); 121 | 122 | // Inner and outer thresholds 123 | const P_UV float inner_threshold = (1.0 / 150.0); 124 | const P_UV float outer_threshold = (1.0 / 130.0); 125 | 126 | if (lightDistance < inner_threshold) { 127 | if( pointLightPos.z >= 0.0 ) { 128 | // Gray when in top 129 | return vec4(0.5, 0.5, 0.5, 1.0); 130 | } else { 131 | // Red when behind 132 | return vec4(1.0, 0.0, 0.0, 1.0); 133 | } 134 | } 135 | else if (lightDistance < outer_threshold) { 136 | // White outline 137 | return vec4( 1.0, 1.0, 1.0, 1.0); 138 | } 139 | #endif 140 | 141 | return lightColor; 142 | } 143 | ]] 144 | 145 | graphics.defineEffect(kernel) 146 | 147 | return kernel 148 | -------------------------------------------------------------------------------- /shaders/normal.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------- Normal shader factory - Fuse rotate shader with any other effect 2 | local normal = {} 3 | ---------------------------------------------- Variables 4 | local normalEffects = {} 5 | local existTested = {} 6 | ---------------------------------------------- Caches 7 | local stringGsub = string.gsub 8 | ---------------------------------------------- Local functions 9 | local function effectExists(effectName) 10 | local exists = false 11 | 12 | if existTested[effectName] == nil then 13 | local tempRect = display.newRect(0, 0, 1, 1) -- Create temp rect 14 | tempRect.fill.effect = effectName 15 | existTested[effectName] = tempRect.fill.effect ~= nil -- see if effect was set 16 | display.remove(tempRect) 17 | end 18 | 19 | return existTested[effectName] 20 | end 21 | ---------------------------------------------- Module functions 22 | function normal.getEffect(effectName) 23 | if effectName and effectExists(effectName) then 24 | local internalName = stringGsub(effectName, "%.", "") 25 | 26 | if not normalEffects[effectName] then 27 | local kernel = {} 28 | 29 | kernel.language = "glsl" 30 | kernel.category = "filter" 31 | kernel.group = "normal" 32 | kernel.name = internalName 33 | 34 | kernel.graph = { 35 | nodes = { 36 | rotate = {effect = "filter.dynacam.rotate", input1 = "paint1"}, 37 | effect = {effect = effectName, input1 = "rotate"}, 38 | }, 39 | output = "effect", 40 | } 41 | graphics.defineEffect(kernel) 42 | 43 | normalEffects[effectName] = kernel 44 | end 45 | 46 | return "filter.normal."..internalName 47 | else 48 | if not normalEffects["default"] then 49 | local kernel = {} 50 | 51 | kernel.language = "glsl" 52 | kernel.category = "filter" 53 | kernel.group = "normal" 54 | kernel.name = "default" 55 | 56 | kernel.graph = { 57 | nodes = { 58 | rotate = {effect = "filter.dynacam.rotate", input1 = "paint1"}, 59 | }, 60 | output = "rotate", 61 | } 62 | graphics.defineEffect(kernel) 63 | 64 | normalEffects["default"] = kernel 65 | end 66 | 67 | return "filter.normal.default" 68 | end 69 | end 70 | 71 | return normal -------------------------------------------------------------------------------- /shaders/rotate.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------- Rotate shader - Fixes normal vectors when object is rotated - Basilio Germán 2 | display.setDefault("isShaderCompilerVerbose", true) 3 | 4 | local kernel = {} 5 | 6 | kernel.language = "glsl" 7 | kernel.category = "filter" 8 | kernel.group = "dynacam" 9 | kernel.name = "rotate" 10 | kernel.vertexData = 11 | { 12 | { 13 | name = "rotation", 14 | default = 0, 15 | type = "scalar", 16 | index = 0, -- CoronaVertexUserData.x 17 | }, 18 | { 19 | name = "xMult", 20 | default = 1, 21 | type = "scalar", 22 | index = 1, -- CoronaVertexUserData.y 23 | }, 24 | { 25 | name = "yMult", 26 | default = 1, 27 | type = "scalar", 28 | index = 2, -- CoronaVertexUserData.z 29 | }, 30 | { 31 | name = "zMult", 32 | default = 1, 33 | type = "scalar", 34 | index = 3, -- CoronaVertexUserData.w 35 | }, 36 | } 37 | kernel.fragment = [[ 38 | P_POSITION vec2 rotateNormalVector(P_NORMAL vec2 vector, P_DEFAULT float angle) { 39 | P_DEFAULT float s = sin(angle); 40 | P_DEFAULT float c = cos(angle); 41 | P_DEFAULT mat2 m = mat2(c, -s, s, c); 42 | 43 | return m * vector; 44 | } 45 | 46 | P_COLOR vec4 FragmentKernel(P_UV vec2 texCoord){ 47 | P_NORMAL vec4 normalPixel = texture2D(CoronaSampler0, texCoord); 48 | 49 | normalPixel.xy -= 0.5; // Normal vectors are aligned from the center 50 | normalPixel.x *= CoronaVertexUserData.y; 51 | normalPixel.y *= CoronaVertexUserData.z; 52 | normalPixel.xy = rotateNormalVector(normalPixel.xy, CoronaVertexUserData.x); 53 | normalPixel.xy += 0.5; 54 | 55 | normalPixel.z *= CoronaVertexUserData.w; 56 | 57 | normalPixel.xy *= normalPixel.w; 58 | 59 | return CoronaColorScale(normalPixel); 60 | } 61 | ]] 62 | 63 | graphics.defineEffect(kernel) 64 | 65 | return kernel 66 | 67 | --------------------------------------------------------------------------------