├── README.md ├── Selected Pixel Count.lua ├── LICENSE ├── Save Palette To CSV.lua ├── Grid.lua ├── Load Palette From CSV.lua ├── Ghost Images.lua ├── Layer Transition.lua ├── Particle Simulator.lua └── Particle Simulator Looped.lua /README.md: -------------------------------------------------------------------------------- 1 | # Aseprite_LUA_Scripts 2 | LUA scripts for use within the pixel art application 'Aseprite'. 3 | 4 | * __Ghost Images :__ Animation effect similar to onion skinning. The script will generate a new layer containing a number of 'Ghosts' of previous frames. 5 | * __Layer Transition :__ Generates a new layer with a simple transition effect between two existing layers. 6 | * __Load Palette From CSV :__ Load a palette from a CSV file with red, green, blue, alpha columns. 7 | * __Save Palette To CSV :__ Save current palette To CSV file with red, green, blue, alpha columns. 8 | * __Selected Pixel Count :__ Counts the number of pixels in an arbitrary shaped selection. 9 | * __Particle Simulator :__ A simple 2D particle simulation. 10 | * __Particle Simulator Looped:__ An alternative version of the particle simulation. This generates looped particle animations. 11 | -------------------------------------------------------------------------------- /Selected Pixel Count.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------- 2 | -- Count the number of pixels inside an arbitrary selection 3 | ---------------------------------------------------------------------- 4 | 5 | local spr = app.activeSprite 6 | if not spr then return end 7 | 8 | local sel = spr.selection 9 | local im = Image(spr) 10 | local count = 0 11 | 12 | if not sel.isEmpty then 13 | local rect = sel.bounds 14 | 15 | for x = rect.x, rect.x + rect.width do 16 | for y = rect.y, rect.y + rect.height do 17 | if sel:contains(x, y) then 18 | count = count + 1 19 | end 20 | end 21 | end 22 | end 23 | 24 | local total = 0 25 | 26 | for it in im:pixels() do 27 | local pixelValue = it() 28 | if app.pixelColor.rgbaA(pixelValue) > 128 then 29 | total = total + 1 30 | end 31 | end 32 | 33 | print(string.format('%s%d', "Selected pixels: ", count)) 34 | print(string.format('%s%d', "Opaque pixels: ", total)) 35 | if total > 0 then 36 | print(string.format('%s%0.2f%%',"As percentage: ", (count/total)*100)) 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dave Barker 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. 22 | -------------------------------------------------------------------------------- /Save Palette To CSV.lua: -------------------------------------------------------------------------------- 1 | local spr = app.activeSprite 2 | if not spr then return end 3 | 4 | local function save_palette_csv(filename) 5 | local ncolors = #spr.palettes[1] 6 | local f = io.open(filename, "w") 7 | f:write(string.format('red,green,blue,alpha\n')) 8 | for i = 0, ncolors-1 do 9 | local c = spr.palettes[1]:getColor(i) 10 | local r = c.red 11 | local g = c.green 12 | local b = c.blue 13 | local a = c.alpha 14 | f:write(string.format('%d,%d,%d,%d\n',r,g,b,a)) 15 | end 16 | f:close() 17 | end 18 | 19 | local function enable_button(dlg, button_id) 20 | dlg:modify{ id=button_id, enabled=true } 21 | end 22 | 23 | local save_dlg = Dialog("Save Palette as CSV File") 24 | save_dlg:file{ id="csv_out", 25 | label="Filename: ", 26 | filename="palette.csv", 27 | open=false, 28 | save=true, 29 | filetypes={ "csv", "txt" }, 30 | onchange=function() enable_button(save_dlg, "save") end} 31 | save_dlg:button{ id="save", text="Save", enabled=false } 32 | save_dlg:button{ id="cancel", text="Cancel" } 33 | save_dlg:show() 34 | 35 | local data = save_dlg.data 36 | if data.save then 37 | save_palette_csv(data.csv_out) 38 | end 39 | 40 | -------------------------------------------------------------------------------- /Grid.lua: -------------------------------------------------------------------------------- 1 | do 2 | local spr = app.sprite 3 | 4 | if not spr then return app.alert "There is no active sprite" end 5 | 6 | if spr.spec.colorMode ~= ColorMode.RGB then return app.alert "Sprite needs to be in RGB color mode" end 7 | 8 | local function renderGrid(userSettings) 9 | app.transaction( 10 | string.format('Grid %sx%d', userSettings.gridWidth, userSettings.gridHeight), 11 | function() 12 | layer = spr:newLayer() 13 | layer.name = string.format('Grid %sx%d', userSettings.gridWidth, userSettings.gridHeight) 14 | cel = spr:newCel(layer, app.frame) 15 | im = cel.image:clone() 16 | for it in im:pixels() do 17 | if (it.x % userSettings.gridWidth) == 0 or (it.y % userSettings.gridHeight) == 0 then 18 | it(app.pixelColor.rgba(userSettings.gridColor.red, 19 | userSettings.gridColor.green, 20 | userSettings.gridColor.blue, 21 | userSettings.gridColor.alpha)) 22 | end 23 | end 24 | cel.image = im 25 | app.refresh() 26 | end 27 | ) 28 | end 29 | 30 | local dlg = Dialog("Draw Grid") 31 | 32 | dlg:color{ id="gridColor", label="Grid Color", color=Color{ r=0, g=0, b=0, a=255 }} 33 | dlg:number{ id="gridWidth", label="Grid Width", text="5", min=1, decimals=0} 34 | dlg:number{ id="gridHeight", label="Grid Height", text="5", min=1, decimals=0} 35 | dlg:button{ id="start", text="Start", onclick=function() renderGrid(dlg.data) end } 36 | dlg:button{ id="exit", text="Exit" } 37 | dlg:show{wait=false} 38 | end 39 | -------------------------------------------------------------------------------- /Load Palette From CSV.lua: -------------------------------------------------------------------------------- 1 | local spr = app.activeSprite 2 | if not spr then return end 3 | 4 | local function load_palette_csv(filename) 5 | local f = io.open(filename, "r") 6 | local colors = {} 7 | 8 | local line = f:read() 9 | 10 | if not line then 11 | app.alert("Empty File.") 12 | f:close() 13 | return nil 14 | end 15 | 16 | if line ~= "red,green,blue,alpha" then 17 | app.alert("File does not have valid first line.") 18 | f:close() 19 | return nil 20 | else 21 | line = f:read() 22 | while line do 23 | r,g,b,a = string.match(line, "(%d+),(%d+),(%d+),(%d+)") 24 | if r and g and b and a then 25 | table.insert(colors, Color{ r=r, g=g, b=b, a=a }) 26 | else 27 | app.alert("Invalid RGBA data in file.") 28 | f:close() 29 | return nil 30 | end 31 | line = f:read() 32 | end 33 | f:close() 34 | end 35 | 36 | if #colors == 0 then 37 | app.alert("Empty color palette.") 38 | return nil 39 | end 40 | 41 | local pal = Palette(#colors) 42 | for i=0, #colors-1 do 43 | pal:setColor(i, colors[i+1]) 44 | end 45 | 46 | return pal 47 | end 48 | 49 | local function enable_button(dlg, button_id) 50 | dlg:modify{ id=button_id, enabled=true } 51 | end 52 | 53 | local load_dlg = Dialog("Load Palette from CSV File") 54 | load_dlg:file{ id="csv_in", 55 | label="Filename: ", 56 | filename="palette.csv", 57 | open=true, 58 | save=false, 59 | filetypes={ "csv", "txt" }, 60 | onchange=function() enable_button(load_dlg, "load") end } 61 | load_dlg:button{ id="load", text="Load", enabled=false } 62 | load_dlg:button{ id="cancel", text="Cancel" } 63 | load_dlg:show() 64 | 65 | local data = load_dlg.data 66 | if data.load then 67 | pal = load_palette_csv(data.csv_in) 68 | if pal then 69 | app.transaction( 70 | function() 71 | spr:setPalette(pal) 72 | end) 73 | app.refresh() 74 | end 75 | end 76 | 77 | -------------------------------------------------------------------------------- /Ghost Images.lua: -------------------------------------------------------------------------------- 1 | do 2 | local spr = app.activeSprite 3 | if not spr then return app.alert "There is no active sprite" end 4 | 5 | if spr.spec.colorMode ~= ColorMode.RGB then return app.alert "Sprite needs to be in RGB color mode" end 6 | 7 | local renderCount = 0 8 | 9 | local function tableConcat(t1,t2) 10 | for i=1,#t2 do 11 | t1[#t1+1] = t2[i] 12 | end 13 | return t1 14 | end 15 | 16 | local function buildCustomLayerTable(layers, groupString, layerCount) 17 | layers = layers or spr.layers 18 | groupString = groupString or '' 19 | layerCount = layerCount or 1 20 | 21 | local clt = {} 22 | 23 | for i=#layers,1,-1 do 24 | layer = layers[i] 25 | if layer.isGroup then 26 | layerCount, groupLayers = buildCustomLayerTable(layer.layers, groupString..layer.name..'/', layerCount) 27 | tableConcat(clt, groupLayers) 28 | elseif layer.isImage then 29 | table.insert(clt, {name=layerCount..': '..groupString..layer.name, layer=layer}) 30 | layerCount = layerCount + 1 31 | end 32 | end 33 | 34 | return layerCount, clt 35 | end 36 | 37 | local function findInCustomLayerTable(clt, name) 38 | for _, l in ipairs(clt) do 39 | if l.name == name then 40 | return l.layer 41 | end 42 | end 43 | return nil 44 | end 45 | 46 | local function listNamesInCustomLayerTable(clt) 47 | names = {} 48 | for _, l in ipairs(clt) do 49 | table.insert(names, l.name) 50 | end 51 | return names 52 | end 53 | 54 | local function scaleAlpha(img, bounds, scale) 55 | maxX = bounds.x + bounds.width - 1 56 | maxY = bounds.y + bounds.height - 1 57 | 58 | for y=bounds.y, maxY do 59 | for x=bounds.x, maxX do 60 | local col = img:getPixel(x, y) 61 | 62 | local r = app.pixelColor.rgbaR(col) 63 | local g = app.pixelColor.rgbaG(col) 64 | local b = app.pixelColor.rgbaB(col) 65 | local a = app.pixelColor.rgbaA(col) * scale 66 | 67 | img:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a)) 68 | end 69 | end 70 | end 71 | 72 | local function tintAndScaleAlpha(img, bounds, scale, tintColor) 73 | local r = tintColor.red 74 | local g = tintColor.green 75 | local b = tintColor.blue 76 | local tintScale = scale * (tintColor.alpha / 255) 77 | 78 | maxX = bounds.x + bounds.width - 1 79 | maxY = bounds.y + bounds.height - 1 80 | 81 | for y=bounds.y, maxY do 82 | for x=bounds.x, maxX do 83 | local col = img:getPixel(x, y) 84 | 85 | local a = app.pixelColor.rgbaA(col) * tintScale 86 | 87 | img:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a)) 88 | end 89 | end 90 | end 91 | 92 | local function renderGhosts(userSettings,clt) 93 | app.transaction( 94 | function() 95 | renderCount = renderCount + 1 96 | local sourceLayer = findInCustomLayerTable(clt, userSettings.layerSelect) 97 | local sourceOpacity = sourceLayer.opacity 98 | local ghosts = {} 99 | 100 | local lowerLayer = spr:newLayer() 101 | lowerLayer.name = "Ghosts "..renderCount 102 | lowerLayer.opacity = 255 103 | 104 | for i= userSettings.nGhosts,0,-1 do 105 | 106 | local upperLayer = spr:newLayer() 107 | upperLayer.name = "temp" .. i 108 | upperLayer.opacity = 255 109 | 110 | local offsetX = i * userSettings.shiftX 111 | local offsetY = i * userSettings.shiftY 112 | 113 | for frmNumber,frame in ipairs(spr.frames) do 114 | local offsetFrameNumber = frmNumber - (i * userSettings.delay) % #spr.frames 115 | 116 | if offsetFrameNumber > 0 or userSettings.loop then 117 | if offsetFrameNumber < 1 then 118 | offsetFrameNumber = #spr.frames + offsetFrameNumber 119 | end 120 | 121 | if i == 0 then 122 | local img = Image(spr.spec) 123 | local ghostFrame = spr.frames[offsetFrameNumber] 124 | local layerCel = sourceLayer:cel(ghostFrame) 125 | if layerCel then 126 | img:drawImage(layerCel.image, layerCel.position) 127 | end 128 | spr:newCel(upperLayer, frame, img, Point(0, 0)) 129 | else 130 | local img = Image(spr.spec) 131 | local ghostFrame = spr.frames[offsetFrameNumber] 132 | local layerCel = sourceLayer:cel(ghostFrame) 133 | if layerCel then 134 | local pos = Point(layerCel.position.x + offsetX, layerCel.position.y - offsetY) 135 | local bounds = Rectangle(pos.x, pos.y, layerCel.bounds.width, layerCel.bounds.height) 136 | img:drawImage(layerCel.image, pos) 137 | if userSettings.doTint then 138 | tintAndScaleAlpha(img, bounds, 1 - i / (userSettings.nGhosts+1), userSettings.tintColor) 139 | else 140 | scaleAlpha(img, bounds, 1 - i / (userSettings.nGhosts+1)) 141 | end 142 | end 143 | spr:newCel(upperLayer, frame, img, Point(0, 0)) 144 | end 145 | end 146 | end 147 | app.activeLayer = upperLayer 148 | app.command.MergeDownLayer() 149 | end 150 | app.activeLayer.opacity = sourceOpacity 151 | end 152 | ) 153 | app.refresh() 154 | end 155 | 156 | local _,clt = buildCustomLayerTable() 157 | 158 | local dlg = Dialog("Create a Layer of Ghost Images") 159 | dlg:combobox{ id="layerSelect", label="Input Layer", option=clt[1].name, options=listNamesInCustomLayerTable(clt)} 160 | dlg:slider{ id="nGhosts", label="Number of Ghosts", min=1, max=20, value=5 } 161 | dlg:slider{ id="delay", label="Delay between Ghosts", min=1, max=math.max(1, #spr.frames-1), value=1 } 162 | dlg:check{ id="loop", label="Looping Animation", selected=true, onclick=function() return end} 163 | dlg:check{ id="doTint", label="Use Solid Color for Ghosts", selected=false, onclick=function() return end} 164 | dlg:color{ id="tintColor", label="Solid Color", color=Color{ r=0, g=0, b=0, a=255 }} 165 | dlg:number{ id="shiftX", label="Shift Ghost in X axis", text="0", decimals=0} 166 | dlg:number{ id="shiftY", label="Shift Ghost in Y axis", text="0", decimals=0} 167 | dlg:button{ id="start", text="Start", onclick=function() renderGhosts(dlg.data, clt) end } 168 | dlg:button{ id="exit", text="Exit" } 169 | dlg:show{wait=false} 170 | end -------------------------------------------------------------------------------- /Layer Transition.lua: -------------------------------------------------------------------------------- 1 | do 2 | local spr = app.activeSprite 3 | if not spr then return app.alert "There is no active sprite" end 4 | 5 | if spr.spec.colorMode ~= ColorMode.RGB then return app.alert "Sprite needs to be in RGB color mode" end 6 | 7 | local renderCount = 0 8 | 9 | local function tableConcat(t1,t2) 10 | for i=1,#t2 do 11 | t1[#t1+1] = t2[i] 12 | end 13 | return t1 14 | end 15 | 16 | local function buildCustomLayerTable(layers, groupString, layerCount) 17 | layers = layers or spr.layers 18 | groupString = groupString or '' 19 | layerCount = layerCount or 1 20 | 21 | local clt = {} 22 | 23 | for i=#layers,1,-1 do 24 | layer = layers[i] 25 | if layer.isGroup then 26 | layerCount, groupLayers = buildCustomLayerTable(layer.layers, groupString..layer.name..'/', layerCount) 27 | tableConcat(clt, groupLayers) 28 | elseif layer.isImage then 29 | table.insert(clt, {name=layerCount..': '..groupString..layer.name, layer=layer}) 30 | layerCount = layerCount + 1 31 | end 32 | end 33 | 34 | return layerCount, clt 35 | end 36 | 37 | local function findInCustomLayerTable(clt, name) 38 | for _, l in ipairs(clt) do 39 | if l.name == name then 40 | return l.layer 41 | end 42 | end 43 | return nil 44 | end 45 | 46 | local function listNamesInCustomLayerTable(clt) 47 | names = {} 48 | for _, l in ipairs(clt) do 49 | table.insert(names, l.name) 50 | end 51 | return names 52 | end 53 | 54 | local function straightWipe(imgA, imgB, bounds, testFunction) 55 | maxX = bounds.x + bounds.width - 1 56 | maxY = bounds.y + bounds.height - 1 57 | 58 | for y=bounds.y, maxY do 59 | for x=bounds.x, maxX do 60 | if testFunction(x, y) then 61 | local col = imgB:getPixel(x, y) 62 | local r = app.pixelColor.rgbaR(col) 63 | local g = app.pixelColor.rgbaG(col) 64 | local b = app.pixelColor.rgbaB(col) 65 | local a = app.pixelColor.rgbaA(col) 66 | 67 | imgA:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a)) 68 | end 69 | end 70 | end 71 | end 72 | 73 | local function randomWipe(imgA, imgB, bounds, randomValues, threshold) 74 | maxX = bounds.x + bounds.width - 1 75 | maxY = bounds.y + bounds.height - 1 76 | 77 | for y=bounds.y, maxY do 78 | for x=bounds.x, maxX do 79 | local i = 1 + x + y * imgA.width 80 | if randomValues[i] <= threshold then 81 | local col = imgB:getPixel(x, y) 82 | local r = app.pixelColor.rgbaR(col) 83 | local g = app.pixelColor.rgbaG(col) 84 | local b = app.pixelColor.rgbaB(col) 85 | local a = app.pixelColor.rgbaA(col) 86 | 87 | imgA:drawPixel(x, y, app.pixelColor.rgba(r, g, b, a)) 88 | end 89 | end 90 | end 91 | end 92 | 93 | local function renderTransition(userSettings,clt) 94 | app.transaction( 95 | function() 96 | renderCount = renderCount + 1 97 | local fromLayer = findInCustomLayerTable(clt, userSettings.fromLayerSelect) 98 | local toLayer = findInCustomLayerTable(clt, userSettings.toLayerSelect) 99 | 100 | local transitionLayer = spr:newLayer() 101 | transitionLayer.name = "Transition "..renderCount 102 | transitionLayer.opacity = 255 103 | 104 | local progressStep = 1 / (#spr.frames - 2) 105 | local progress = 0 106 | 107 | math.randomseed(userSettings.seed) 108 | local randomValues = {} 109 | if userSettings.transitionType == "Random Pixels" then 110 | local nPixels = spr.spec.width * spr.spec.height 111 | for i = 1, nPixels do 112 | randomValues[i] = math.random(2,#spr.frames) 113 | end 114 | end 115 | 116 | for frmNumber,frame in ipairs(spr.frames) do 117 | local fromImg = Image(spr.spec) 118 | local toImg = Image(spr.spec) 119 | local toLayerCel = toLayer:cel(frame) 120 | local fromLayerCel = fromLayer:cel(frame) 121 | 122 | if fromLayerCel and toLayerCel then 123 | fromImg:drawImage(fromLayerCel.image, fromLayerCel.position) 124 | toImg:drawImage(toLayerCel.image, toLayerCel.position) 125 | 126 | bounds = fromLayerCel.bounds 127 | bounds = bounds:union(toLayerCel.bounds) 128 | if bounds:intersects(spr.bounds) then 129 | bounds = bounds:intersect(spr.bounds) 130 | 131 | if userSettings.transitionType == "Left to Right" then 132 | local edge = math.floor(0.1 + (fromImg.width - 2) * progress) 133 | straightWipe(fromImg, toImg, bounds, function(x,y) return x < edge end) 134 | elseif userSettings.transitionType == "Right to Left" then 135 | local edge = fromImg.width - math.floor(0.1 + (fromImg.width - 2) * progress) 136 | straightWipe(fromImg, toImg, bounds, function(x,y) return x >= edge end) 137 | elseif userSettings.transitionType == "Top to Bottom" then 138 | local edge = math.floor(0.1 + (fromImg.height - 2) * progress) 139 | straightWipe(fromImg, toImg, bounds, function(x,y) return y < edge end) 140 | elseif userSettings.transitionType == "Bottom to Top" then 141 | local edge = fromImg.height - math.floor(0.1 + (fromImg.height - 2) * progress) 142 | straightWipe(fromImg, toImg, bounds, function(x,y) return y >= edge end) 143 | elseif userSettings.transitionType == "Random Pixels" then 144 | randomWipe(fromImg, toImg, bounds, randomValues, frmNumber) 145 | end 146 | end 147 | 148 | spr:newCel(transitionLayer, frame, fromImg, Point(0, 0)) 149 | end 150 | progress = progress + progressStep 151 | end 152 | end 153 | ) 154 | app.refresh() 155 | end 156 | 157 | local _,clt = buildCustomLayerTable() 158 | 159 | local dlg = Dialog("Create a Layer of Ghost Images") 160 | dlg:combobox{ id="fromLayerSelect", label="From Layer", option=clt[1].name, options=listNamesInCustomLayerTable(clt)} 161 | dlg:combobox{ id="toLayerSelect", label="To Layer", option=clt[1].name, options=listNamesInCustomLayerTable(clt)} 162 | dlg:combobox{ id="transitionType", label="Transition", "Left to Right", options={"Left to Right", "Right to Left", "Top to Bottom", "Bottom to Top", "Random Pixels"}} 163 | dlg:number{ id="seed", label="Random Seed", text="1234", decimals=0} 164 | dlg:button{ id="start", text="Start", onclick=function() renderTransition(dlg.data, clt) end } 165 | dlg:button{ id="exit", text="Exit" } 166 | dlg:show{wait=false} 167 | end -------------------------------------------------------------------------------- /Particle Simulator.lua: -------------------------------------------------------------------------------- 1 | local spr = app.activeSprite 2 | if not spr then return app.alert "There is no active sprite" end 3 | 4 | if spr.spec.colorMode ~= ColorMode.RGB then return app.alert "Sprite needs to be in RGB color mode" end 5 | 6 | app.activeTool = 'pencil' 7 | 8 | local renderCount = 0 9 | local warningDialogViewed = false 10 | local maxSpriteDimension = math.max(spr.spec.width, spr.spec.height) 11 | local oldSelection = Selection() 12 | oldSelection:add(spr.selection) 13 | 14 | local blob3 = { 15 | {-1,-1,0.3627}, { 0,-1,0.7137}, { 1,-1,0.3627}, 16 | {-1, 0,0.7137}, { 0, 0,1.0000}, { 1, 0,0.7137}, 17 | {-1, 1,0.3627}, { 0, 1,0.7137}, { 1, 1,0.3627} 18 | } 19 | 20 | local blob5 = { 21 | { 0,-2,0.2157}, 22 | {-1,-1,0.8392}, { 0,-1,1.0000}, { 1,-1,0.8392}, 23 | {-2, 0,0.2157}, {-1, 0,1.0000}, { 0, 0,1.0000}, { 1, 0,1.0000}, { 2, 0,0.2157}, 24 | {-1, 1,0.8392}, { 0, 1,1.0000}, { 1, 1,0.8392}, 25 | { 0, 2,0.2157}, 26 | } 27 | 28 | local blob7 = { 29 | {-1,-3,0.0510}, { 0,-3,0.2040}, { 1,-3,0.0510}, 30 | {-2,-2,0.3137}, {-1,-2,0.9451}, { 0,-2,1.0000}, { 1,-2,0.9451}, { 2,-2,0.3137}, 31 | {-3,-1,0.0510}, {-2,-1,0.9451}, {-1,-1,1.0000}, { 0,-1,1.0000}, { 1,-1,1.0000}, { 2,-1,0.9451}, { 3,-1,0.0510}, 32 | {-3, 0,0.2040}, {-2, 0,1.0000}, {-1, 0,1.0000}, { 0, 0,1.0000}, { 1, 0,1.0000}, { 2, 0,1.0000}, { 3, 0,0.2040}, 33 | {-3, 1,0.0510}, {-2, 1,0.9451}, {-1, 1,1.0000}, { 0, 1,1.0000}, { 1, 1,1.0000}, { 2, 1,0.9451}, { 3, 1,0.0510}, 34 | {-2, 2,0.3137}, {-1, 2,0.9451}, { 0, 2,1.0000}, { 1, 2,0.9451}, { 2, 2,0.3137}, 35 | {-1, 3,0.0510}, { 0, 3,0.2040}, { 1, 3,0.0510} 36 | } 37 | 38 | local function sqr(x) 39 | return x * x 40 | end 41 | 42 | local function dist2(x1, y1, x2, y2) 43 | return sqr(x1-x2) + sqr(y1-y2) 44 | end 45 | 46 | local function distanceToLineSquared(xA,yA,xB,yB,xP,yP) 47 | local l2 = dist2(xA,yA,xB,yB) 48 | if (l2 == 0) then 49 | return dist2(xA,yA, xP,yP) 50 | end 51 | local t = ((xP - xA) * (xB - xA) + (yP - yA) * (yB - yA)) / l2 52 | t = math.max(0, math.min(1, t)) 53 | return dist2(xP, yP, xA + t * (xB - xA), yA + t * (yB - yA)) 54 | end 55 | 56 | local function overlayRectangleCorners(centerX, centerY, width, height, angle) 57 | local sel = Selection() 58 | 59 | local radians = math.rad(angle) 60 | local cosR = math.cos(radians) 61 | local sinR = math.sin(radians) 62 | local halfWidth = width * 0.5 63 | local halfHeight = height * 0.5 64 | 65 | local corner1X = centerX + (halfWidth * cosR - halfHeight * sinR) 66 | local corner1Y = centerY + (halfWidth * sinR + halfHeight * cosR) 67 | 68 | local corner2X = centerX + (halfWidth * cosR + halfHeight * sinR) 69 | local corner2Y = centerY + (halfWidth * sinR - halfHeight * cosR) 70 | 71 | local corner3X = centerX - (halfWidth * cosR - halfHeight * sinR) 72 | local corner3Y = centerY - (halfWidth * sinR + halfHeight * cosR) 73 | 74 | local corner4X = centerX - (halfWidth * cosR + halfHeight * sinR) 75 | local corner4Y = centerY - (halfWidth * sinR - halfHeight * cosR) 76 | 77 | local boxSize = 1 + math.floor(maxSpriteDimension/100) 78 | 79 | sel:add(Rectangle(corner1X-boxSize, corner1Y-boxSize, boxSize*2, boxSize*2)) 80 | sel:add(Rectangle(corner2X-boxSize, corner2Y-boxSize, boxSize*2, boxSize*2)) 81 | sel:add(Rectangle(corner3X-boxSize, corner3Y-boxSize, boxSize*2, boxSize*2)) 82 | sel:add(Rectangle(corner4X-boxSize, corner4Y-boxSize, boxSize*2, boxSize*2)) 83 | sel:add(Rectangle(centerX-boxSize, centerY-boxSize, boxSize*2, boxSize*2)) 84 | 85 | spr.selection = sel 86 | end 87 | 88 | local function overlayAngle(angle, spread) 89 | local sel = Selection() 90 | 91 | local radians = math.rad(angle) 92 | local cosR = math.cos(radians) 93 | local sinR = math.sin(radians) 94 | 95 | local centerX = math.floor(spr.spec.width / 2) 96 | local centerY = math.floor(spr.spec.height / 2) 97 | 98 | local smallBoxSize = 1 + math.floor(maxSpriteDimension/50) 99 | local smallBoxOffset = math.floor(smallBoxSize/2) 100 | local largeBoxSize = smallBoxSize * 2 101 | local largeBoxOffset = math.floor(largeBoxSize/2) 102 | 103 | local segmentSize = maxSpriteDimension * 0.25 104 | 105 | sel:add(Rectangle(centerX-smallBoxOffset, centerY-smallBoxOffset, smallBoxSize, smallBoxSize)) 106 | 107 | local x = centerX + segmentSize * sinR 108 | local y = centerY - segmentSize * cosR 109 | sel:add(Rectangle(x-smallBoxOffset, y-smallBoxOffset, smallBoxSize, smallBoxSize)) 110 | 111 | x = centerX + 2 * segmentSize * sinR 112 | y = centerY - 2 * segmentSize * cosR 113 | sel:add(Rectangle(x-largeBoxOffset, y-largeBoxOffset, largeBoxSize, largeBoxSize)) 114 | 115 | if spread > 0 then 116 | radians = math.rad(angle + spread * 0.5) 117 | cosR = math.cos(radians) 118 | sinR = math.sin(radians) 119 | x = centerX + 2 * segmentSize * sinR 120 | y = centerY - 2 * segmentSize * cosR 121 | sel:add(Rectangle(x-smallBoxOffset, y-smallBoxOffset, smallBoxSize, smallBoxSize)) 122 | 123 | radians = math.rad(angle - spread * 0.5) 124 | cosR = math.cos(radians) 125 | sinR = math.sin(radians) 126 | x = centerX + 2 * segmentSize * sinR 127 | y = centerY - 2 * segmentSize * cosR 128 | sel:add(Rectangle(x-smallBoxOffset, y-smallBoxOffset, smallBoxSize, smallBoxSize)) 129 | end 130 | 131 | spr.selection = sel 132 | end 133 | 134 | local function overlayMagnitude(magnitude, variance) 135 | local selMagnitude = Selection() 136 | local stepX = math.max(1, spr.width*0.2) 137 | 138 | selMagnitude:add(stepX*2-2, 0, stepX+4, magnitude) 139 | 140 | if variance > 0 then 141 | local varianceFloat = variance / 100 142 | local varianceOffset = math.floor(magnitude * varianceFloat) 143 | 144 | local selVarianceA = Selection(Rectangle(stepX*2, magnitude, stepX, varianceOffset)) 145 | local selVarianceB = Selection(Rectangle(stepX*2, magnitude-varianceOffset, stepX, varianceOffset)) 146 | 147 | selMagnitude:add(selVarianceA) 148 | selMagnitude:subtract(selVarianceB) 149 | end 150 | 151 | spr.selection = selMagnitude 152 | end 153 | 154 | local function overlay(userSettings, id) 155 | if not userSettings.useOverlays then return end 156 | 157 | if id == 'emitter' then 158 | overlayRectangleCorners(userSettings.emitterX, 159 | userSettings.emitterY, 160 | userSettings.emitterLength + userSettings.emitterRadius * 2, 161 | userSettings.emitterRadius * 2, 162 | userSettings.emitterAngle) 163 | elseif id == 'gravityAngle' then 164 | overlayAngle(userSettings.gravityAngle, 165 | 0) 166 | elseif id == 'gravityMagnitude' then 167 | overlayMagnitude(userSettings.gravityMagnitude, 168 | 0) 169 | elseif id == 'startVectorAngle' then 170 | overlayAngle(userSettings.startVectorAngle, 171 | userSettings.startVectorAngleVariance) 172 | elseif id == 'startVectorMagnitude' then 173 | overlayMagnitude(userSettings.startVectorMagnitude, 174 | userSettings.startVectorMagnitudeVariance) 175 | end 176 | 177 | app.refresh() 178 | end 179 | 180 | local function calcEmitterPixel(centerX, centerY, length, angle, radius) 181 | local radians = math.rad(angle) 182 | local cosR = math.cos(radians) 183 | local sinR = math.sin(radians) 184 | local halfLength = length * 0.5 185 | local xA = centerX - halfLength * cosR 186 | local yA = centerY - halfLength * sinR 187 | local xB = centerX + halfLength * cosR 188 | local yB = centerY + halfLength * sinR 189 | local minX = math.min(xA,xB) - radius 190 | local maxX = math.max(xA,xB) + radius 191 | local minY = math.min(yA,yB) - radius 192 | local maxY = math.max(yA,yB) + radius 193 | local radiusSquared = sqr(radius) 194 | 195 | local emissionPixels = {{x=centerX, y=centerY}} 196 | for iy = minY, maxY do 197 | for ix = minX, maxX do 198 | if distanceToLineSquared(xA,yA,xB,yB,ix,iy) < radiusSquared then 199 | table.insert(emissionPixels, {x=ix,y=iy}) 200 | end 201 | end 202 | end 203 | 204 | return emissionPixels 205 | end 206 | 207 | local function renderEmitterPixels(emissionPixels) 208 | local layer = spr:newLayer() 209 | layer.name = "Emitter" 210 | layer.opacity = 255 211 | 212 | local img = Image(spr.spec) 213 | 214 | for _,pixel in ipairs(emissionPixels) do 215 | img:drawPixel(pixel.x, pixel.y, Color{ r=0, g=0, b=0, a=255 }) 216 | end 217 | 218 | spr:newCel(layer, 1, img, Point(0, 0)) 219 | app.refresh() 220 | end 221 | 222 | local function pixelOver(x, y, col, sAlpha, img) 223 | local bgndCol = img:getPixel(x, y) 224 | 225 | local bgndA = app.pixelColor.rgbaA(bgndCol) 226 | if bgndA == 0 then 227 | local outA = math.min(math.max(col.alpha * sAlpha, 0), 255) 228 | img:drawPixel(x, y, app.pixelColor.rgba(col.red, col.green, col.blue, outA)) 229 | return 230 | end 231 | 232 | local fgndR = col.red 233 | local fgndG = col.green 234 | local fgndB = col.blue 235 | local fgndA = math.min(math.max(col.alpha * sAlpha, 0), 255) 236 | 237 | local bgndR = app.pixelColor.rgbaR(bgndCol) 238 | local bgndG = app.pixelColor.rgbaG(bgndCol) 239 | local bgndB = app.pixelColor.rgbaB(bgndCol) 240 | 241 | local floatFgndA = fgndA / 255 242 | local floatBgndA = bgndA / 255 243 | 244 | local a0 = (floatFgndA + (floatBgndA * (1 - floatFgndA))) 245 | local fgndScale = floatFgndA / a0 246 | local bgndScale = (floatBgndA * (1 - floatFgndA)) / a0 247 | 248 | local outR = (fgndR * fgndScale) + (bgndR * bgndScale) 249 | local outG = (fgndG * fgndScale) + (bgndG * bgndScale) 250 | local outB = (fgndB * fgndScale) + (bgndB * bgndScale) 251 | local outA = 255 * a0 252 | 253 | img:drawPixel(x, y, app.pixelColor.rgba( outR, outG, outB, outA )) 254 | end 255 | 256 | local function drawBlob3(x, y, col, img) 257 | if (x < -1) then return end 258 | if (y < -1) then return end 259 | if (x > img.width+1) then return end 260 | if (y > img.height+1) then return end 261 | 262 | for _,p in ipairs(blob3) do 263 | pixelOver(x+p[1], y+p[2], col, p[3], img) 264 | end 265 | end 266 | 267 | local function drawBlob5(x, y, col, img) 268 | if (x < -2) then return end 269 | if (y < -2) then return end 270 | if (x > img.width+2) then return end 271 | if (y > img.height+2) then return end 272 | 273 | for _,p in ipairs(blob5) do 274 | pixelOver(x+p[1], y+p[2], col, p[3], img) 275 | end 276 | end 277 | 278 | local function drawBlob7(x, y, col, img) 279 | if (x < -3) then return end 280 | if (y < -3) then return end 281 | if (x > img.width+3) then return end 282 | if (y > img.height+3) then return end 283 | 284 | for _,p in ipairs(blob7) do 285 | pixelOver(x+p[1], y+p[2], col, p[3], img) 286 | end 287 | end 288 | 289 | local function plotLineLow(x0, y0, x1, y1, img, col, reversed) 290 | local dx = x1 - x0 291 | local dy = y1 - y0 292 | local yi = 1 293 | if dy < 0 then 294 | yi = -1 295 | dy = -dy 296 | end 297 | local D = (2 * dy) - dx 298 | local y = y0 299 | 300 | local dAlpha = math.abs(1/dx) 301 | local sAlpha = 0 302 | if not reversed then 303 | sAlpha = 1 304 | dAlpha = - dAlpha 305 | end 306 | 307 | for x = x0, x1 do 308 | pixelOver(x, y, col, sAlpha, img) 309 | sAlpha = sAlpha + dAlpha 310 | if D > 0 then 311 | y = y + yi 312 | D = D + (2 * (dy - dx)) 313 | else 314 | D = D + 2*dy 315 | end 316 | end 317 | end 318 | 319 | local function plotLineHigh(x0, y0, x1, y1, img, col, reversed) 320 | local dx = x1 - x0 321 | local dy = y1 - y0 322 | local xi = 1 323 | if dx < 0 then 324 | xi = -1 325 | dx = -dx 326 | end 327 | local D = (2 * dx) - dy 328 | local x = x0 329 | 330 | local dAlpha = math.abs(1/dy) 331 | local sAlpha = 0 332 | if not reversed then 333 | sAlpha = 1 334 | dAlpha = - dAlpha 335 | end 336 | 337 | for y = y0, y1 do 338 | pixelOver(x, y, col, sAlpha, img) 339 | sAlpha = sAlpha + dAlpha 340 | if D > 0 then 341 | x = x + xi 342 | D = D + (2 * (dx - dy)) 343 | else 344 | D = D + 2*dx 345 | end 346 | end 347 | end 348 | 349 | local function drawLine(x0, y0, x1, y1, col, img) 350 | if (x0 < 0) and (x1 < 0) then return end 351 | if (y0 < 0) and (y1 < 0) then return end 352 | if (x0 > img.width) and (x1 > img.width) then return end 353 | if (y0 > img.height) and (y1 > img.height) then return end 354 | 355 | if math.abs(y1 - y0) < math.abs(x1 - x0) then 356 | if x0 > x1 then 357 | plotLineLow(x1, y1, x0, y0, img, col, true) 358 | else 359 | plotLineLow(x0, y0, x1, y1, img, col, false) 360 | end 361 | else 362 | if y0 > y1 then 363 | plotLineHigh(x1, y1, x0, y0, img, col, true) 364 | else 365 | plotLineHigh(x0, y0, x1, y1, img, col, false) 366 | end 367 | end 368 | end 369 | 370 | local function drawPoint(x, y, col, img) 371 | if (x < 0) then return end 372 | if (y < 0) then return end 373 | if (x > img.width) then return end 374 | if (y > img.height) then return end 375 | 376 | pixelOver(x, y, col, 1, img) 377 | end 378 | 379 | local function renderParticles(userSettings) 380 | app.transaction( 381 | function() 382 | renderCount = renderCount + 1 383 | local layer = spr:newLayer() 384 | layer.name = "Particles "..renderCount 385 | layer.opacity = 255 386 | 387 | math.randomseed(userSettings.seed) 388 | 389 | emissionPixels = calcEmitterPixel(userSettings.emitterX, 390 | userSettings.emitterY, 391 | userSettings.emitterLength, 392 | userSettings.emitterAngle, 393 | userSettings.emitterRadius) 394 | 395 | local particles = {} 396 | local lifespan = {min = userSettings.lifespan * (1.0 - userSettings.lifespanVariance / 100), 397 | max = userSettings.lifespan * (1.0 + userSettings.lifespanVariance / 100)} 398 | local startAngle = {min = math.rad(userSettings.startVectorAngle - userSettings.startVectorAngleVariance / 2), 399 | range = math.rad(userSettings.startVectorAngleVariance)} 400 | 401 | local startVectorMagnitude = (maxSpriteDimension / 10) * (userSettings.startVectorMagnitude / 100) 402 | local startMagnitude = {min = startVectorMagnitude * (1.0 - userSettings.startVectorMagnitudeVariance / 100), 403 | range = startVectorMagnitude * (userSettings.startVectorMagnitudeVariance / 50)} 404 | 405 | local gravityRadians = math.rad(userSettings.gravityAngle) 406 | local gravityMagnitude = (maxSpriteDimension / 10) * (userSettings.gravityMagnitude / 100) 407 | local gravityVector = {x = gravityMagnitude * math.sin(gravityRadians), 408 | y = - (gravityMagnitude * math.cos(gravityRadians))} 409 | 410 | local floatDrag = 1.0 - userSettings.drag / 100 411 | 412 | local startH = userSettings.startColor.hue 413 | local diffH = userSettings.endColor.hue - userSettings.startColor.hue 414 | if math.abs(diffH) > 180 then 415 | if diffH > 0 then 416 | diffH = diffH - 360 417 | else 418 | diffH = diffH + 360 419 | end 420 | end 421 | 422 | local startS = userSettings.startColor.saturation 423 | local diffS = userSettings.endColor.saturation - userSettings.startColor.saturation 424 | 425 | local startV = userSettings.startColor.value 426 | local diffV = userSettings.endColor.value - userSettings.startColor.value 427 | 428 | local startA = userSettings.startColor.alpha 429 | local diffA = userSettings.endColor.alpha - userSettings.startColor.alpha 430 | 431 | for frmNumber = userSettings.emitStart, userSettings.simDuration do 432 | collectgarbage() 433 | local img 434 | if frmNumber > 0 then 435 | img = Image(spr.spec) 436 | end 437 | 438 | if frmNumber <= userSettings.emitEnd then 439 | for i = 1, userSettings.particlesPerFrame do 440 | local startPosition = math.random(#emissionPixels) 441 | local angle = startAngle.min + startAngle.range * math.random() 442 | local magnitude = startMagnitude.min + startMagnitude.range * math.random() 443 | local cosR = math.cos(angle) 444 | local sinR = math.sin(angle) 445 | local startPositionCopy = {} 446 | startPositionCopy['x'] = emissionPixels[startPosition].x 447 | startPositionCopy['y'] = emissionPixels[startPosition].y 448 | local subFrame 449 | table.insert(particles, {age = 0, 450 | subFrameTiming = math.random(), 451 | currentPosition = startPositionCopy, 452 | lifespan = math.random(lifespan.min, lifespan.max), 453 | vector = {x = magnitude * sinR, y = - (magnitude * cosR) }} ) 454 | end 455 | end 456 | 457 | for _,p in ipairs(particles) do 458 | p.age = p.age + 1 459 | if p.age > p.lifespan then 460 | p = nil 461 | else 462 | local previousPosition = {} 463 | previousPosition['x'] = p.currentPosition.x 464 | previousPosition['y'] = p.currentPosition.y 465 | p.vector.x = (p.vector.x + gravityVector.x) * floatDrag 466 | p.vector.y = (p.vector.y + gravityVector.y) * floatDrag 467 | p.currentPosition.x = p.currentPosition.x + p.vector.x 468 | p.currentPosition.y = p.currentPosition.y + p.vector.y 469 | 470 | local subFrameX = previousPosition.x + (p.currentPosition.x - previousPosition.x) * p.subFrameTiming 471 | local subFrameY = previousPosition.y + (p.currentPosition.y - previousPosition.y) * p.subFrameTiming 472 | 473 | local y0 = math.floor(subFrameY) 474 | local y1 = math.floor(subFrameY - p.vector.y) 475 | 476 | local x0 = math.floor(subFrameX) 477 | local x1 = math.floor(subFrameX - p.vector.x) 478 | 479 | local ageFloat = math.min(p.age+p.subFrameTiming, p.lifespan) / p.lifespan 480 | 481 | local h = startH + math.floor(diffH * ageFloat) 482 | if h > 360 then 483 | h = h - 361 484 | elseif h < 0 then 485 | h = 361 + h 486 | end 487 | local s = startS + diffS * ageFloat 488 | local v = startV + diffV * ageFloat 489 | local a = startA + diffA * ageFloat 490 | 491 | if frmNumber > 0 then 492 | if userSettings.particleShape == "Point" then 493 | drawPoint(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 494 | elseif userSettings.particleShape == "Streak" then 495 | drawLine(x0, y0, x1, y1, Color{hue=h, saturation=s, value=v, alpha=a}, img) 496 | elseif userSettings.particleShape == "Blob 3" then 497 | drawBlob3(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 498 | elseif userSettings.particleShape == "Blob 5" then 499 | drawBlob5(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 500 | elseif userSettings.particleShape == "Blob 7" then 501 | drawBlob7(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 502 | end 503 | end 504 | end 505 | end 506 | if frmNumber > 0 then 507 | if frmNumber > #spr.frames then 508 | spr:newEmptyFrame(frmNumber) 509 | end 510 | spr:newCel(layer, frmNumber, img, Point(0, 0)) 511 | end 512 | end 513 | 514 | app.refresh() 515 | end 516 | ) 517 | end 518 | 519 | local function showWarningDialog() 520 | if warningDialogViewed then return end 521 | 522 | app.alert{ title = "Warning Experimental Feature", 523 | text = {"Warning this is and experimental feature.", 524 | "While active multiple events will be added to Undo History."}, 525 | buttons="OK" } 526 | 527 | warningDialogViewed = true 528 | end 529 | 530 | local function onExit(dlg) 531 | spr.selection = oldSelection 532 | app.refresh() 533 | 534 | dlg:close() 535 | end 536 | 537 | local function updateDialog(dlg, id) 538 | if id == "seed" then 539 | if dlg.data.seed < 1 then 540 | dlg:modify{ id="seed", text="1" } 541 | elseif dlg.data.seed > 999999 then 542 | dlg:modify{ id="seed", text="999999" } 543 | end 544 | elseif id == "simDuration" then 545 | if dlg.data.simDuration < 25 then 546 | dlg:modify{ id="lifespan", max=50} 547 | dlg:modify{ id="emitStart", min=-25, max=25} 548 | dlg:modify{ id="emitEnd", max=25} 549 | else 550 | dlg:modify{ id="lifespan", max=2*dlg.data.simDuration} 551 | dlg:modify{ id="emitStart", min=-dlg.data.simDuration, max=dlg.data.simDuration} 552 | dlg:modify{ id="emitEnd", max=dlg.data.simDuration} 553 | end 554 | elseif id == "emitStart" then 555 | if dlg.data.emitStart < 1 then 556 | dlg:modify{ id="emitEnd", min=1} 557 | else 558 | dlg:modify{ id="emitEnd", min=dlg.data.emitStart} 559 | end 560 | end 561 | end 562 | 563 | local dlg = Dialog("Particle Simulator") 564 | 565 | dlg:slider{ 566 | id="simDuration", 567 | label="Simulation Duration", 568 | min=1, 569 | max=100, 570 | value=25, 571 | onchange=function() updateDialog(dlg, "simDuration") end 572 | } 573 | 574 | dlg:slider{ 575 | id="emitStart", 576 | label="Emission Starts Frame", 577 | min=-25, 578 | max=25, 579 | value=1, 580 | onchange=function() updateDialog(dlg, "emitStart") end 581 | } 582 | 583 | dlg:slider{ 584 | id="emitEnd", 585 | label="Emission Ends Frame", 586 | min=1, 587 | max=25, 588 | value=25 589 | } 590 | 591 | dlg:slider{ 592 | id="particlesPerFrame", 593 | label="Particles Per Frame", 594 | min=1, 595 | max=400, 596 | value=20 597 | } 598 | 599 | dlg:slider{ 600 | id="lifespan", 601 | label="Particle Lifespan", 602 | min=1, 603 | max=50, 604 | value=25 605 | } 606 | 607 | dlg:slider{ 608 | id="lifespanVariance", 609 | label="Particle Lifespan Variance", 610 | min=0, 611 | max=100, 612 | value=10 613 | } 614 | 615 | dlg:color{ 616 | id="startColor", 617 | label="Particle Color", 618 | color=Color{ r=255, g=255, b=0, a=255 } 619 | } 620 | 621 | dlg:color{ 622 | id="endColor", 623 | color=Color{ r=255, g=0, b=0, a=255 } 624 | } 625 | 626 | dlg:combobox{ 627 | id="particleShape", 628 | label="Particle Shape", 629 | option="Streak", 630 | options={ "Point", "Streak", "Blob 3", "Blob 5", "Blob 7" }, 631 | onchange=function() 632 | if dlg.data.particleShape == "Blob 3" then 633 | dlg:modify{id="particlesPerFrame", max=300} 634 | elseif dlg.data.particleShape == "Blob 5" then 635 | dlg:modify{id="particlesPerFrame", max=200} 636 | elseif dlg.data.particleShape == "Blob 7" then 637 | dlg:modify{id="particlesPerFrame", max=100} 638 | else 639 | dlg:modify{id="particlesPerFrame", max=400} 640 | end 641 | end 642 | } 643 | 644 | dlg:slider{ 645 | id="emitterX", 646 | label="Emitter Center X", 647 | min=-spr.width*0.5, 648 | max=spr.width*1.5, 649 | value=math.ceil(spr.width*0.5), 650 | onchange=function() overlay(dlg.data, 'emitter') end 651 | } 652 | 653 | dlg:slider{ 654 | id="emitterY", 655 | label="Emitter Center Y", 656 | min=-spr.height*0.5, 657 | max=spr.height*1.5, 658 | value=math.ceil(spr.height*0.1), 659 | onchange=function() overlay(dlg.data, 'emitter') end 660 | } 661 | 662 | dlg:slider{ 663 | id="emitterLength", 664 | label="Emitter Length", 665 | min=1, 666 | max=maxSpriteDimension*2, 667 | value=maxSpriteDimension, 668 | onchange=function() overlay(dlg.data, 'emitter') end 669 | } 670 | 671 | dlg:slider{ 672 | id="emitterRadius", 673 | label="Emitter Radius", 674 | min=1, 675 | max=math.ceil(maxSpriteDimension*0.5), 676 | value=5, 677 | onchange=function() overlay(dlg.data, 'emitter') end 678 | } 679 | 680 | dlg:slider{ 681 | id="emitterAngle", 682 | label="Emitter Angle", 683 | min=-90, 684 | max=90, 685 | value=0, 686 | onchange=function() overlay(dlg.data, 'emitter') end 687 | } 688 | 689 | dlg:slider{ 690 | id="gravityAngle", 691 | label="Gravity Angle", 692 | min=0, 693 | max=360, 694 | value=180, 695 | onchange=function() overlay(dlg.data, 'gravityAngle') end 696 | } 697 | 698 | dlg:slider{ 699 | id="gravityMagnitude", 700 | label="Gravity Magnitude", 701 | min=0, 702 | max=100, 703 | value=10, 704 | onchange=function() overlay(dlg.data, 'gravityMagnitude') end 705 | } 706 | 707 | dlg:slider{ 708 | id="startVectorAngle", 709 | label="Start Vector Angle", 710 | min=0, 711 | max=360, 712 | value=180, 713 | onchange=function() overlay(dlg.data, 'startVectorAngle') end 714 | } 715 | 716 | dlg:slider{ 717 | id="startVectorAngleVariance", 718 | label="Start Vector Angle Variance", 719 | min=0, 720 | max=360, 721 | value=5, 722 | onchange=function() overlay(dlg.data, 'startVectorAngle') end 723 | } 724 | 725 | dlg:slider{ 726 | id="startVectorMagnitude", 727 | label="Start Vector Magnitude", 728 | min=0, 729 | max=100, 730 | value=10, 731 | onchange=function() overlay(dlg.data, 'startVectorMagnitude') end 732 | } 733 | 734 | dlg:slider{ 735 | id="startVectorMagnitudeVariance", 736 | label="Start Vector Magnitude Variance", 737 | min=0, 738 | max=100, 739 | value=10, 740 | onchange=function() overlay(dlg.data, 'startVectorMagnitude') end 741 | } 742 | 743 | dlg:slider{ 744 | id="drag", 745 | label="Drag", 746 | min=0, 747 | max=100, 748 | value=10 749 | } 750 | 751 | dlg:number{ 752 | id="seed", 753 | label="Seed", 754 | text="1234", 755 | decimals=0, 756 | onchange=function() updateDialog(dlg, "seed") end 757 | } 758 | 759 | dlg:check{ 760 | id="useOverlays", 761 | label="Use Overlays", 762 | selected=false, 763 | onclick=function() showWarningDialog() end 764 | } 765 | 766 | dlg:button{ 767 | id="start", 768 | text="Start", 769 | onclick=function() renderParticles(dlg.data) end 770 | } 771 | 772 | dlg:button{ 773 | id="exit", 774 | text="Exit", 775 | onclick=function() onExit(dlg) end 776 | } 777 | 778 | local bounds = dlg.bounds 779 | dlg.bounds = Rectangle(85, 55, bounds.width*1.25, bounds.height) 780 | 781 | dlg:show{wait=false} -------------------------------------------------------------------------------- /Particle Simulator Looped.lua: -------------------------------------------------------------------------------- 1 | local spr = app.activeSprite 2 | if not spr then return app.alert "There is no active sprite" end 3 | 4 | if spr.spec.colorMode ~= ColorMode.RGB then return app.alert "Sprite needs to be in RGB color mode" end 5 | 6 | app.activeTool = 'pencil' 7 | 8 | local renderCount = 0 9 | local warningDialogViewed = false 10 | local maxSpriteDimension = math.max(spr.spec.width, spr.spec.height) 11 | local oldSelection = Selection() 12 | oldSelection:add(spr.selection) 13 | 14 | local blob3 = { 15 | {-1,-1,0.3627}, { 0,-1,0.7137}, { 1,-1,0.3627}, 16 | {-1, 0,0.7137}, { 0, 0,1.0000}, { 1, 0,0.7137}, 17 | {-1, 1,0.3627}, { 0, 1,0.7137}, { 1, 1,0.3627} 18 | } 19 | 20 | local blob5 = { 21 | { 0,-2,0.2157}, 22 | {-1,-1,0.8392}, { 0,-1,1.0000}, { 1,-1,0.8392}, 23 | {-2, 0,0.2157}, {-1, 0,1.0000}, { 0, 0,1.0000}, { 1, 0,1.0000}, { 2, 0,0.2157}, 24 | {-1, 1,0.8392}, { 0, 1,1.0000}, { 1, 1,0.8392}, 25 | { 0, 2,0.2157}, 26 | } 27 | 28 | local blob7 = { 29 | {-1,-3,0.0510}, { 0,-3,0.2040}, { 1,-3,0.0510}, 30 | {-2,-2,0.3137}, {-1,-2,0.9451}, { 0,-2,1.0000}, { 1,-2,0.9451}, { 2,-2,0.3137}, 31 | {-3,-1,0.0510}, {-2,-1,0.9451}, {-1,-1,1.0000}, { 0,-1,1.0000}, { 1,-1,1.0000}, { 2,-1,0.9451}, { 3,-1,0.0510}, 32 | {-3, 0,0.2040}, {-2, 0,1.0000}, {-1, 0,1.0000}, { 0, 0,1.0000}, { 1, 0,1.0000}, { 2, 0,1.0000}, { 3, 0,0.2040}, 33 | {-3, 1,0.0510}, {-2, 1,0.9451}, {-1, 1,1.0000}, { 0, 1,1.0000}, { 1, 1,1.0000}, { 2, 1,0.9451}, { 3, 1,0.0510}, 34 | {-2, 2,0.3137}, {-1, 2,0.9451}, { 0, 2,1.0000}, { 1, 2,0.9451}, { 2, 2,0.3137}, 35 | {-1, 3,0.0510}, { 0, 3,0.2040}, { 1, 3,0.0510} 36 | } 37 | 38 | local function sqr(x) 39 | return x * x 40 | end 41 | 42 | local function dist2(x1, y1, x2, y2) 43 | return sqr(x1-x2) + sqr(y1-y2) 44 | end 45 | 46 | local function distanceToLineSquared(xA,yA,xB,yB,xP,yP) 47 | local l2 = dist2(xA,yA,xB,yB) 48 | if (l2 == 0) then 49 | return dist2(xA,yA, xP,yP) 50 | end 51 | local t = ((xP - xA) * (xB - xA) + (yP - yA) * (yB - yA)) / l2 52 | t = math.max(0, math.min(1, t)) 53 | return dist2(xP, yP, xA + t * (xB - xA), yA + t * (yB - yA)) 54 | end 55 | 56 | local function overlayRectangleCorners(centerX, centerY, width, height, angle) 57 | local sel = Selection() 58 | 59 | local radians = math.rad(angle) 60 | local cosR = math.cos(radians) 61 | local sinR = math.sin(radians) 62 | local halfWidth = width * 0.5 63 | local halfHeight = height * 0.5 64 | 65 | local corner1X = centerX + (halfWidth * cosR - halfHeight * sinR) 66 | local corner1Y = centerY + (halfWidth * sinR + halfHeight * cosR) 67 | 68 | local corner2X = centerX + (halfWidth * cosR + halfHeight * sinR) 69 | local corner2Y = centerY + (halfWidth * sinR - halfHeight * cosR) 70 | 71 | local corner3X = centerX - (halfWidth * cosR - halfHeight * sinR) 72 | local corner3Y = centerY - (halfWidth * sinR + halfHeight * cosR) 73 | 74 | local corner4X = centerX - (halfWidth * cosR + halfHeight * sinR) 75 | local corner4Y = centerY - (halfWidth * sinR - halfHeight * cosR) 76 | 77 | local boxSize = 1 + math.floor(maxSpriteDimension/100) 78 | 79 | sel:add(Rectangle(corner1X-boxSize, corner1Y-boxSize, boxSize*2, boxSize*2)) 80 | sel:add(Rectangle(corner2X-boxSize, corner2Y-boxSize, boxSize*2, boxSize*2)) 81 | sel:add(Rectangle(corner3X-boxSize, corner3Y-boxSize, boxSize*2, boxSize*2)) 82 | sel:add(Rectangle(corner4X-boxSize, corner4Y-boxSize, boxSize*2, boxSize*2)) 83 | sel:add(Rectangle(centerX-boxSize, centerY-boxSize, boxSize*2, boxSize*2)) 84 | 85 | spr.selection = sel 86 | end 87 | 88 | local function overlayAngle(angle, spread) 89 | local sel = Selection() 90 | 91 | local radians = math.rad(angle) 92 | local cosR = math.cos(radians) 93 | local sinR = math.sin(radians) 94 | 95 | local centerX = math.floor(spr.spec.width / 2) 96 | local centerY = math.floor(spr.spec.height / 2) 97 | 98 | local smallBoxSize = 1 + math.floor(maxSpriteDimension/50) 99 | local smallBoxOffset = math.floor(smallBoxSize/2) 100 | local largeBoxSize = smallBoxSize * 2 101 | local largeBoxOffset = math.floor(largeBoxSize/2) 102 | 103 | local segmentSize = maxSpriteDimension * 0.25 104 | 105 | sel:add(Rectangle(centerX-smallBoxOffset, centerY-smallBoxOffset, smallBoxSize, smallBoxSize)) 106 | 107 | local x = centerX + segmentSize * sinR 108 | local y = centerY - segmentSize * cosR 109 | sel:add(Rectangle(x-smallBoxOffset, y-smallBoxOffset, smallBoxSize, smallBoxSize)) 110 | 111 | x = centerX + 2 * segmentSize * sinR 112 | y = centerY - 2 * segmentSize * cosR 113 | sel:add(Rectangle(x-largeBoxOffset, y-largeBoxOffset, largeBoxSize, largeBoxSize)) 114 | 115 | if spread > 0 then 116 | radians = math.rad(angle + spread * 0.5) 117 | cosR = math.cos(radians) 118 | sinR = math.sin(radians) 119 | x = centerX + 2 * segmentSize * sinR 120 | y = centerY - 2 * segmentSize * cosR 121 | sel:add(Rectangle(x-smallBoxOffset, y-smallBoxOffset, smallBoxSize, smallBoxSize)) 122 | 123 | radians = math.rad(angle - spread * 0.5) 124 | cosR = math.cos(radians) 125 | sinR = math.sin(radians) 126 | x = centerX + 2 * segmentSize * sinR 127 | y = centerY - 2 * segmentSize * cosR 128 | sel:add(Rectangle(x-smallBoxOffset, y-smallBoxOffset, smallBoxSize, smallBoxSize)) 129 | end 130 | 131 | spr.selection = sel 132 | end 133 | 134 | local function overlayMagnitude(magnitude, variance) 135 | local selMagnitude = Selection() 136 | local stepX = math.max(1, spr.width*0.2) 137 | 138 | selMagnitude:add(stepX*2-2, 0, stepX+4, magnitude) 139 | 140 | if variance > 0 then 141 | local varianceFloat = variance / 100 142 | local varianceOffset = math.floor(magnitude * varianceFloat) 143 | 144 | local selVarianceA = Selection(Rectangle(stepX*2, magnitude, stepX, varianceOffset)) 145 | local selVarianceB = Selection(Rectangle(stepX*2, magnitude-varianceOffset, stepX, varianceOffset)) 146 | 147 | selMagnitude:add(selVarianceA) 148 | selMagnitude:subtract(selVarianceB) 149 | end 150 | 151 | spr.selection = selMagnitude 152 | end 153 | 154 | local function overlay(userSettings, id) 155 | if not userSettings.useOverlays then return end 156 | 157 | if id == 'emitter' then 158 | overlayRectangleCorners(userSettings.emitterX, 159 | userSettings.emitterY, 160 | userSettings.emitterLength + userSettings.emitterRadius * 2, 161 | userSettings.emitterRadius * 2, 162 | userSettings.emitterAngle) 163 | elseif id == 'gravityAngle' then 164 | overlayAngle(userSettings.gravityAngle, 165 | 0) 166 | elseif id == 'gravityMagnitude' then 167 | overlayMagnitude(userSettings.gravityMagnitude, 168 | 0) 169 | elseif id == 'startVectorAngle' then 170 | overlayAngle(userSettings.startVectorAngle, 171 | userSettings.startVectorAngleVariance) 172 | elseif id == 'startVectorMagnitude' then 173 | overlayMagnitude(userSettings.startVectorMagnitude, 174 | userSettings.startVectorMagnitudeVariance) 175 | end 176 | 177 | app.refresh() 178 | end 179 | 180 | local function calcEmitterPixel(centerX, centerY, length, angle, radius) 181 | local radians = math.rad(angle) 182 | local cosR = math.cos(radians) 183 | local sinR = math.sin(radians) 184 | local halfLength = length * 0.5 185 | local xA = centerX - halfLength * cosR 186 | local yA = centerY - halfLength * sinR 187 | local xB = centerX + halfLength * cosR 188 | local yB = centerY + halfLength * sinR 189 | local minX = math.min(xA,xB) - radius 190 | local maxX = math.max(xA,xB) + radius 191 | local minY = math.min(yA,yB) - radius 192 | local maxY = math.max(yA,yB) + radius 193 | local radiusSquared = sqr(radius) 194 | 195 | local emissionPixels = {{x=centerX, y=centerY}} 196 | for iy = minY, maxY do 197 | for ix = minX, maxX do 198 | if distanceToLineSquared(xA,yA,xB,yB,ix,iy) < radiusSquared then 199 | table.insert(emissionPixels, {x=ix,y=iy}) 200 | end 201 | end 202 | end 203 | 204 | return emissionPixels 205 | end 206 | 207 | local function renderEmitterPixels(emissionPixels) 208 | local layer = spr:newLayer() 209 | layer.name = "Emitter" 210 | layer.opacity = 255 211 | 212 | local img = Image(spr.spec) 213 | 214 | for _,pixel in ipairs(emissionPixels) do 215 | img:drawPixel(pixel.x, pixel.y, Color{ r=0, g=0, b=0, a=255 }) 216 | end 217 | 218 | spr:newCel(layer, 1, img, Point(0, 0)) 219 | app.refresh() 220 | end 221 | 222 | local function pixelOver(x, y, col, sAlpha, img) 223 | local bgndCol = img:getPixel(x, y) 224 | 225 | local bgndA = app.pixelColor.rgbaA(bgndCol) 226 | if bgndA == 0 then 227 | local outA = math.min(math.max(col.alpha * sAlpha, 0), 255) 228 | img:drawPixel(x, y, app.pixelColor.rgba(col.red, col.green, col.blue, outA)) 229 | return 230 | end 231 | 232 | local fgndR = col.red 233 | local fgndG = col.green 234 | local fgndB = col.blue 235 | local fgndA = math.min(math.max(col.alpha * sAlpha, 0), 255) 236 | 237 | local bgndR = app.pixelColor.rgbaR(bgndCol) 238 | local bgndG = app.pixelColor.rgbaG(bgndCol) 239 | local bgndB = app.pixelColor.rgbaB(bgndCol) 240 | 241 | local floatFgndA = fgndA / 255 242 | local floatBgndA = bgndA / 255 243 | 244 | local a0 = (floatFgndA + (floatBgndA * (1 - floatFgndA))) 245 | local fgndScale = floatFgndA / a0 246 | local bgndScale = (floatBgndA * (1 - floatFgndA)) / a0 247 | 248 | local outR = (fgndR * fgndScale) + (bgndR * bgndScale) 249 | local outG = (fgndG * fgndScale) + (bgndG * bgndScale) 250 | local outB = (fgndB * fgndScale) + (bgndB * bgndScale) 251 | local outA = 255 * a0 252 | 253 | img:drawPixel(x, y, app.pixelColor.rgba( outR, outG, outB, outA )) 254 | end 255 | 256 | local function drawBlob3(x, y, col, img) 257 | if (x < -1) then return end 258 | if (y < -1) then return end 259 | if (x > img.width+1) then return end 260 | if (y > img.height+1) then return end 261 | 262 | for _,p in ipairs(blob3) do 263 | pixelOver(x+p[1], y+p[2], col, p[3], img) 264 | end 265 | end 266 | 267 | local function drawBlob5(x, y, col, img) 268 | if (x < -2) then return end 269 | if (y < -2) then return end 270 | if (x > img.width+2) then return end 271 | if (y > img.height+2) then return end 272 | 273 | for _,p in ipairs(blob5) do 274 | pixelOver(x+p[1], y+p[2], col, p[3], img) 275 | end 276 | end 277 | 278 | local function drawBlob7(x, y, col, img) 279 | if (x < -3) then return end 280 | if (y < -3) then return end 281 | if (x > img.width+3) then return end 282 | if (y > img.height+3) then return end 283 | 284 | for _,p in ipairs(blob7) do 285 | pixelOver(x+p[1], y+p[2], col, p[3], img) 286 | end 287 | end 288 | 289 | local function plotLineLow(x0, y0, x1, y1, img, col, reversed) 290 | local dx = x1 - x0 291 | local dy = y1 - y0 292 | local yi = 1 293 | if dy < 0 then 294 | yi = -1 295 | dy = -dy 296 | end 297 | local D = (2 * dy) - dx 298 | local y = y0 299 | 300 | local dAlpha = math.abs(1/dx) 301 | local sAlpha = 0 302 | if not reversed then 303 | sAlpha = 1 304 | dAlpha = - dAlpha 305 | end 306 | 307 | for x = x0, x1 do 308 | pixelOver(x, y, col, sAlpha, img) 309 | sAlpha = sAlpha + dAlpha 310 | if D > 0 then 311 | y = y + yi 312 | D = D + (2 * (dy - dx)) 313 | else 314 | D = D + 2*dy 315 | end 316 | end 317 | end 318 | 319 | local function plotLineHigh(x0, y0, x1, y1, img, col, reversed) 320 | local dx = x1 - x0 321 | local dy = y1 - y0 322 | local xi = 1 323 | if dx < 0 then 324 | xi = -1 325 | dx = -dx 326 | end 327 | local D = (2 * dx) - dy 328 | local x = x0 329 | 330 | local dAlpha = math.abs(1/dy) 331 | local sAlpha = 0 332 | if not reversed then 333 | sAlpha = 1 334 | dAlpha = - dAlpha 335 | end 336 | 337 | for y = y0, y1 do 338 | pixelOver(x, y, col, sAlpha, img) 339 | sAlpha = sAlpha + dAlpha 340 | if D > 0 then 341 | x = x + xi 342 | D = D + (2 * (dx - dy)) 343 | else 344 | D = D + 2*dx 345 | end 346 | end 347 | end 348 | 349 | local function drawLine(x0, y0, x1, y1, col, img) 350 | if (x0 < 0) and (x1 < 0) then return end 351 | if (y0 < 0) and (y1 < 0) then return end 352 | if (x0 > img.width) and (x1 > img.width) then return end 353 | if (y0 > img.height) and (y1 > img.height) then return end 354 | 355 | if math.abs(y1 - y0) < math.abs(x1 - x0) then 356 | if x0 > x1 then 357 | plotLineLow(x1, y1, x0, y0, img, col, true) 358 | else 359 | plotLineLow(x0, y0, x1, y1, img, col, false) 360 | end 361 | else 362 | if y0 > y1 then 363 | plotLineHigh(x1, y1, x0, y0, img, col, true) 364 | else 365 | plotLineHigh(x0, y0, x1, y1, img, col, false) 366 | end 367 | end 368 | end 369 | 370 | local function drawPoint(x, y, col, img) 371 | if (x < 0) then return end 372 | if (y < 0) then return end 373 | if (x > img.width) then return end 374 | if (y > img.height) then return end 375 | 376 | pixelOver(x, y, col, 1, img) 377 | end 378 | 379 | local function renderParticles(userSettings) 380 | app.transaction( 381 | function() 382 | renderCount = renderCount + 1 383 | local layer = spr:newLayer() 384 | layer.name = "Particles "..renderCount 385 | layer.opacity = 255 386 | 387 | math.randomseed(userSettings.seed) 388 | 389 | emissionPixels = calcEmitterPixel(userSettings.emitterX, 390 | userSettings.emitterY, 391 | userSettings.emitterLength, 392 | userSettings.emitterAngle, 393 | userSettings.emitterRadius) 394 | 395 | local particles = {} 396 | 397 | local startAngle = {min = math.rad(userSettings.startVectorAngle - userSettings.startVectorAngleVariance / 2), 398 | range = math.rad(userSettings.startVectorAngleVariance)} 399 | 400 | local startVectorMagnitude = (maxSpriteDimension / 10) * (userSettings.startVectorMagnitude / 100) 401 | local startMagnitude = {min = startVectorMagnitude * (1.0 - userSettings.startVectorMagnitudeVariance / 100), 402 | range = startVectorMagnitude * (userSettings.startVectorMagnitudeVariance / 50)} 403 | 404 | local gravityRadians = math.rad(userSettings.gravityAngle) 405 | local gravityMagnitude = (maxSpriteDimension / 10) * (userSettings.gravityMagnitude / 100) 406 | local gravityVector = {x = gravityMagnitude * math.sin(gravityRadians), 407 | y = - (gravityMagnitude * math.cos(gravityRadians))} 408 | 409 | local floatDrag = 1.0 - userSettings.drag / 100 410 | 411 | local startH = userSettings.startColor.hue 412 | local diffH = userSettings.endColor.hue - userSettings.startColor.hue 413 | if math.abs(diffH) > 180 then 414 | if diffH > 0 then 415 | diffH = diffH - 360 416 | else 417 | diffH = diffH + 360 418 | end 419 | end 420 | 421 | local startS = userSettings.startColor.saturation 422 | local diffS = userSettings.endColor.saturation - userSettings.startColor.saturation 423 | 424 | local startV = userSettings.startColor.value 425 | local diffV = userSettings.endColor.value - userSettings.startColor.value 426 | 427 | local startA = userSettings.startColor.alpha 428 | local diffA = userSettings.endColor.alpha - userSettings.startColor.alpha 429 | 430 | for frmNumber = 1, userSettings.simDuration do 431 | collectgarbage() 432 | local img = Image(spr.spec) 433 | 434 | for i = 1, userSettings.particlesPerFrame do 435 | local startPosition = math.random(#emissionPixels) 436 | local angle = startAngle.min + startAngle.range * math.random() 437 | local magnitude = startMagnitude.min + startMagnitude.range * math.random() 438 | local cosR = math.cos(angle) 439 | local sinR = math.sin(angle) 440 | local startPositionCopy = {} 441 | startPositionCopy['x'] = emissionPixels[startPosition].x 442 | startPositionCopy['y'] = emissionPixels[startPosition].y 443 | local subFrame 444 | table.insert(particles, {age = 0, 445 | subFrameTiming = math.random(), 446 | currentPosition = startPositionCopy, 447 | vector = {x = magnitude * sinR, y = - (magnitude * cosR) }} ) 448 | end 449 | 450 | 451 | for _,p in ipairs(particles) do 452 | p.age = p.age + 1 453 | if p.age > userSettings.simDuration then 454 | p = nil 455 | else 456 | local previousPosition = {} 457 | previousPosition['x'] = p.currentPosition.x 458 | previousPosition['y'] = p.currentPosition.y 459 | p.vector.x = (p.vector.x + gravityVector.x) * floatDrag 460 | p.vector.y = (p.vector.y + gravityVector.y) * floatDrag 461 | p.currentPosition.x = p.currentPosition.x + p.vector.x 462 | p.currentPosition.y = p.currentPosition.y + p.vector.y 463 | 464 | local subFrameX = previousPosition.x + (p.currentPosition.x - previousPosition.x) * p.subFrameTiming 465 | local subFrameY = previousPosition.y + (p.currentPosition.y - previousPosition.y) * p.subFrameTiming 466 | 467 | local y0 = math.floor(subFrameY) 468 | local y1 = math.floor(subFrameY - p.vector.y) 469 | 470 | local x0 = math.floor(subFrameX) 471 | local x1 = math.floor(subFrameX - p.vector.x) 472 | 473 | local ageFloat = math.min(p.age+p.subFrameTiming, userSettings.simDuration) / userSettings.simDuration 474 | 475 | local h = startH + math.floor(diffH * ageFloat) 476 | if h > 360 then 477 | h = h - 361 478 | elseif h < 0 then 479 | h = 361 + h 480 | end 481 | local s = startS + diffS * ageFloat 482 | local v = startV + diffV * ageFloat 483 | local a = startA + diffA * ageFloat 484 | 485 | if userSettings.particleShape == "Point" then 486 | drawPoint(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 487 | elseif userSettings.particleShape == "Streak" then 488 | drawLine(x0, y0, x1, y1, Color{hue=h, saturation=s, value=v, alpha=a}, img) 489 | elseif userSettings.particleShape == "Blob 3" then 490 | drawBlob3(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 491 | elseif userSettings.particleShape == "Blob 5" then 492 | drawBlob5(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 493 | elseif userSettings.particleShape == "Blob 7" then 494 | drawBlob7(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 495 | end 496 | end 497 | end 498 | 499 | if frmNumber > #spr.frames then 500 | spr:newEmptyFrame(frmNumber) 501 | end 502 | spr:newCel(layer, frmNumber, img, Point(0, 0)) 503 | end 504 | 505 | for frmNumber = 1, userSettings.simDuration do 506 | collectgarbage() 507 | local img = Image(spr.spec) 508 | local layerCel = layer:cel(spr.frames[frmNumber]) 509 | 510 | if layerCel then 511 | img:drawImage(layerCel.image, layerCel.position) 512 | 513 | for _,p in ipairs(particles) do 514 | p.age = p.age + 1 515 | if p.age > userSettings.simDuration then 516 | p = nil 517 | else 518 | local previousPosition = {} 519 | previousPosition['x'] = p.currentPosition.x 520 | previousPosition['y'] = p.currentPosition.y 521 | p.vector.x = (p.vector.x + gravityVector.x) * floatDrag 522 | p.vector.y = (p.vector.y + gravityVector.y) * floatDrag 523 | p.currentPosition.x = p.currentPosition.x + p.vector.x 524 | p.currentPosition.y = p.currentPosition.y + p.vector.y 525 | 526 | local subFrameX = previousPosition.x + (p.currentPosition.x - previousPosition.x) * p.subFrameTiming 527 | local subFrameY = previousPosition.y + (p.currentPosition.y - previousPosition.y) * p.subFrameTiming 528 | 529 | local y0 = math.floor(subFrameY) 530 | local y1 = math.floor(subFrameY - p.vector.y) 531 | 532 | local x0 = math.floor(subFrameX) 533 | local x1 = math.floor(subFrameX - p.vector.x) 534 | 535 | local ageFloat = math.min(p.age+p.subFrameTiming, userSettings.simDuration) / userSettings.simDuration 536 | 537 | local h = startH + math.floor(diffH * ageFloat) 538 | if h > 360 then 539 | h = h - 361 540 | elseif h < 0 then 541 | h = 361 + h 542 | end 543 | local s = startS + diffS * ageFloat 544 | local v = startV + diffV * ageFloat 545 | local a = startA + diffA * ageFloat 546 | 547 | if userSettings.particleShape == "Point" then 548 | drawPoint(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 549 | elseif userSettings.particleShape == "Streak" then 550 | drawLine(x0, y0, x1, y1, Color{hue=h, saturation=s, value=v, alpha=a}, img) 551 | elseif userSettings.particleShape == "Blob 3" then 552 | drawBlob3(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 553 | elseif userSettings.particleShape == "Blob 5" then 554 | drawBlob5(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 555 | elseif userSettings.particleShape == "Blob 7" then 556 | drawBlob7(x0, y0, Color{hue=h, saturation=s, value=v, alpha=a}, img) 557 | end 558 | end 559 | end 560 | end 561 | spr:newCel(layer, frmNumber, img, Point(0, 0)) 562 | end 563 | 564 | app.refresh() 565 | end 566 | ) 567 | end 568 | 569 | local function showWarningDialog() 570 | if warningDialogViewed then return end 571 | 572 | app.alert{ title = "Warning Experimental Feature", 573 | text = {"Warning this is and experimental feature.", 574 | "While active multiple events will be added to Undo History."}, 575 | buttons="OK" } 576 | 577 | warningDialogViewed = true 578 | end 579 | 580 | local function onExit(dlg) 581 | spr.selection = oldSelection 582 | app.refresh() 583 | 584 | dlg:close() 585 | end 586 | 587 | local function updateDialog(dlg, id) 588 | if id == "seed" then 589 | if dlg.data.seed < 1 then 590 | dlg:modify{ id="seed", text="1" } 591 | elseif dlg.data.seed > 999999 then 592 | dlg:modify{ id="seed", text="999999" } 593 | end 594 | end 595 | end 596 | 597 | local dlg = Dialog("Particle Simulator") 598 | 599 | dlg:slider{ 600 | id="simDuration", 601 | label="Simulation Duration", 602 | min=1, 603 | max=50, 604 | value=25, 605 | onchange=function() updateDialog(dlg, "simDuration") end 606 | } 607 | 608 | dlg:slider{ 609 | id="particlesPerFrame", 610 | label="Particles Per Frame", 611 | min=1, 612 | max=400, 613 | value=20 614 | } 615 | 616 | dlg:color{ 617 | id="startColor", 618 | label="Particle Color", 619 | color=Color{ r=255, g=255, b=0, a=255 } 620 | } 621 | 622 | dlg:color{ 623 | id="endColor", 624 | color=Color{ r=255, g=0, b=0, a=255 } 625 | } 626 | 627 | dlg:combobox{ 628 | id="particleShape", 629 | label="Particle Shape", 630 | option="Streak", 631 | options={ "Point", "Streak", "Blob 3", "Blob 5", "Blob 7" }, 632 | onchange=function() 633 | if dlg.data.particleShape == "Blob 3" then 634 | dlg:modify{id="particlesPerFrame", max=300} 635 | elseif dlg.data.particleShape == "Blob 5" then 636 | dlg:modify{id="particlesPerFrame", max=200} 637 | elseif dlg.data.particleShape == "Blob 7" then 638 | dlg:modify{id="particlesPerFrame", max=100} 639 | else 640 | dlg:modify{id="particlesPerFrame", max=400} 641 | end 642 | end 643 | } 644 | 645 | dlg:slider{ 646 | id="emitterX", 647 | label="Emitter Center X", 648 | min=-spr.width*0.5, 649 | max=spr.width*1.5, 650 | value=math.ceil(spr.width*0.5), 651 | onchange=function() overlay(dlg.data, 'emitter') end 652 | } 653 | 654 | dlg:slider{ 655 | id="emitterY", 656 | label="Emitter Center Y", 657 | min=-spr.height*0.5, 658 | max=spr.height*1.5, 659 | value=math.ceil(spr.height*0.1), 660 | onchange=function() overlay(dlg.data, 'emitter') end 661 | } 662 | 663 | dlg:slider{ 664 | id="emitterLength", 665 | label="Emitter Length", 666 | min=1, 667 | max=maxSpriteDimension*2, 668 | value=maxSpriteDimension, 669 | onchange=function() overlay(dlg.data, 'emitter') end 670 | } 671 | 672 | dlg:slider{ 673 | id="emitterAngle", 674 | label="Emitter Angle", 675 | min=-90, 676 | max=90, 677 | value=0, 678 | onchange=function() overlay(dlg.data, 'emitter') end 679 | } 680 | 681 | dlg:slider{ 682 | id="emitterRadius", 683 | label="Emitter Radius", 684 | min=1, 685 | max=math.ceil(maxSpriteDimension*0.5), 686 | value=5, 687 | onchange=function() overlay(dlg.data, 'emitter') end 688 | } 689 | 690 | dlg:slider{ 691 | id="gravityAngle", 692 | label="Gravity Angle", 693 | min=0, 694 | max=360, 695 | value=180, 696 | onchange=function() overlay(dlg.data, 'gravityAngle') end 697 | } 698 | 699 | dlg:slider{ 700 | id="gravityMagnitude", 701 | label="Gravity Magnitude", 702 | min=0, 703 | max=100, 704 | value=10, 705 | onchange=function() overlay(dlg.data, 'gravityMagnitude') end 706 | } 707 | 708 | dlg:slider{ 709 | id="startVectorAngle", 710 | label="Start Vector Angle", 711 | min=0, 712 | max=360, 713 | value=180, 714 | onchange=function() overlay(dlg.data, 'startVectorAngle') end 715 | } 716 | 717 | dlg:slider{ 718 | id="startVectorAngleVariance", 719 | label="Start Vector Angle Variance", 720 | min=0, 721 | max=360, 722 | value=5, 723 | onchange=function() overlay(dlg.data, 'startVectorAngle') end 724 | } 725 | 726 | dlg:slider{ 727 | id="startVectorMagnitude", 728 | label="Start Vector Magnitude", 729 | min=0, 730 | max=100, 731 | value=10, 732 | onchange=function() overlay(dlg.data, 'startVectorMagnitude') end 733 | } 734 | 735 | dlg:slider{ 736 | id="startVectorMagnitudeVariance", 737 | label="Start Vector Magnitude Variance", 738 | min=0, 739 | max=100, 740 | value=10, 741 | onchange=function() overlay(dlg.data, 'startVectorMagnitude') end 742 | } 743 | 744 | dlg:slider{ 745 | id="drag", 746 | label="Drag", 747 | min=0, 748 | max=100, 749 | value=10 750 | } 751 | 752 | dlg:number{ 753 | id="seed", 754 | label="Seed", 755 | text="1234", 756 | decimals=0, 757 | onchange=function() updateDialog(dlg, "seed") end 758 | } 759 | 760 | dlg:check{ 761 | id="useOverlays", 762 | label="Use Overlays", 763 | selected=false, 764 | onclick=function() showWarningDialog() end 765 | } 766 | 767 | dlg:button{ 768 | id="start", 769 | text="Start", 770 | onclick=function() renderParticles(dlg.data) end 771 | } 772 | 773 | dlg:button{ 774 | id="exit", 775 | text="Exit", 776 | onclick=function() onExit(dlg) end 777 | } 778 | 779 | local bounds = dlg.bounds 780 | dlg.bounds = Rectangle(85, 55, bounds.width*1.25, bounds.height) 781 | 782 | dlg:show{wait=false} --------------------------------------------------------------------------------