Color cycling - palette shifting | v0.05 update 29.12.2020

colour cycling script (current version here, for older versions see posts below):
test-4-clr-cycle test-4-clr-cycle_2 Copy

few words how this works:

  • when we think about colour cycling, we usually think about palette shifting. it is reasonable, however it seemed to me as easier option to shift pixel index values instead of shifting around palette entries. so that’s what i did. this script checks each pixel in cel for matches in entered list of indexes and switches them for the next entry in the list. it is as simple as it gets, but it works ;]
  • i didn’t come across any issues while using this script, but you know, just to be sure: always save before using (well, any) scripts.

enjoy and feel free to improve

v0.05 update: script now has “left to right” option to reverse the order of input list.
this is selected by default, which reverses the behaviour of previous versions. you probably want to leave this on, however if you prefer previous behaviour, change the line selected = true to selected = false under id = “reverse”.

to explain little bit more: in the beginning i was confused to find out that script gives results in opposite direction from what i expected. in my westerner’s left-to-right mind, when i enter 1 2 3, the colour cycling should go like this: 1 2 3, 3 1 2, 2 3 1 etc. right? well, wrong. because table (array) is also ordered from left to right, the script will look for first value (1) and then replace it with the next value (2) and so on, so the result is: 1 2 3, 2 3 1, 3 1 2 etc.
with left to right option the results are - hopefully - more in line of one would expect.

left to right OFF (normal order):
X 1 2 3 —> 1 2 3 X —> 2 3 X 1 —> 3 X 1 2
left to right ON (reversed order):
X 1 2 3 —> 3 X 1 2 —> 2 3 X 1 —> 1 2 3 X

colour-cycle-v005

-- COLOUR CYCLE 0.05 
-- b236 
-- 
-- *** scanline colour cycling effect *** 
-- 
-- 1) IMPORTANT: set sprite to indexed 
-- 2) select cel 
-- 3) run script 
-- 4) enter colour indexes you want to use 
--    for example: 42 23 11 
-- 5) select or deselect option "add frames" 
-- 6) select or deselect left to right option ("L->R") 
-- 6) click on button 
-- 
-- ADD FRAMES OPTION: 
-- if the option is selected then: 
--    the script will duplicate current frame n-1 times, 
--    with n being the length of your list (in our example n=3) 
-- if not: 
--    the script will duplicate only current cel n-1 times 
-- in both cases it will cycle through the colours 
-- changing selected pixel indexes on each step 
-- 
-- LEFT TO RIGHT OPTION: 
-- if selected: 
--    the script will reverse order of indexes, which 
--    will result in animation going 'from left to right' 
-- if deselected: 
--    the script will act as in previous versions, which 
--    will result in animation going 'from right to left' 
-- 
-- IMPORTANT: don't repeat numbers.
-- unless it's exactly what you want to do. 
-- if you want to have a "wave" effect, 
-- instead of entering a sequence 11 23 42 23 11 
-- duplicate colours 23 and 11 in your palette 
-- as new entries and use their indexes 
-- like this: 
-- 44 (duplicate of 11) 43 (duplicate of 23) 42 23 11 
-- 
-- IMPORTANT: for adding cels you need enough empty frames 
-- in timeline. 
-- 
-- IMPORTANT: don't use on layer which already contains animation! 
-- it will be overwritten! 
-- 


local dlgWin = Dialog{ title = "*** CYCLE COLOURS ***  " } 
local temp = {} 
local clrs = {} 
local steps = 0 


-- set dialog window 
dlgWin
	:entry{ 
		id = "clrs_input" 
	}
	
	:check{ 
		id = "add_frames", 
		text = "add frames", 
		selected = false 
	}
	
	:check{ 
		id = "reverse", 
		text = "L->R", 
		selected = true 
	}
	
	:button{ 
		text = "start the riot!", 
		onclick = function()
			fInput(dlgWin) 
		end 
	} 
	
-- show dialog window 
dlgWin:show{ wait = false } 


function fInput(dlgWin) 
	
	img = app.activeCel 
	img_h = img.image.height
	img_w = img.image.width 

	aS = app.activeSprite
	aL = app.activeLayer 
	aF = app.activeFrame.frameNumber
	aI = img.image 
	aP = img.position 
	sF = aS.frames 
	frames = #sF - aF 
		
	-- wrap function in transaction, 
	-- so only one history step is generated 
	app.transaction( function() 
	
		-- convert input entry to string 
		clr_list = tostring(dlgWin.data.clrs_input) 
		
		-- search string for digits, convert them to numbers and insert them into temporary table 
		for i in string.gmatch( clr_list, "%d+") do
			table.insert(temp, tonumber(i)) 
		end
		
		if dlgWin.data.reverse == true then 
			--- reverse temporary table and feed clrs table 
			for j = #temp, 1, -1 do 
				table.insert(clrs, temp[j]) 
			end 
		else 
			--- just feed clrs table 
			for k = 1, #temp, 1 do 
				table.insert(clrs, temp[k]) 
			end 
		end
		
		-- get table length 
		steps = #clrs 
		
		-- duplicate cels
		for v = 1, steps-1, 1 do 

			-- if add frames is selected: 
			if dlgWin.data.add_frames == true then 
				
				app.command.NewFrame() 
				img = app.activeCel 
				
				-- start the riot 	
				fMain()
				
			-- if add frames is NOT selected: 
			elseif dlgWin.data.add_frames == false then 
			
				-- check if there are enough empty cels: 
				if frames > 0 and frames >= steps-1 then 
					
					aI = img.image 
					aS:newCel(aL, aF+v, aI, aP)
					img = aL:cel(aF+v) 
					
					-- start the riot 
					fMain() 
	
				else 
					-- if not, do nothing 
					app.alert("not enough empty cels!") 
					break 
				end 
				
			-- this nasty block is here 
			-- to help aseprite to refresh :P 
			app.command.GotoNextFrame() 
			app.command.GotoPreviousFrame() 
			-- app.refresh() doesn't update timeline 
			
			end
		end 
	
	end) 
	
	-- clear colour table for next use 
	fClear() 
	
	-- close dialog	window 
	--dlgWin:close() 
end 


function fMain() 
	
	--img = app.activeCel 
	
	-- scan through all pixels in selected cel 
	for yy = 0, img_h-1, 1 do 
		for xx = 0, img_w-1, 1 do 
			-- get pixel value 
			clr_i = img.image:getPixel(xx,yy); 
			-- do the magic trick 
			fCycle(xx,yy,clr_i) 
		end 
	end 
	
end 


function fCycle(xx,yy,clr_i) 

	-- check if colour index of pixel is in the list 
	for i, value in pairs(clrs) do
		if value == clr_i then
			if i < steps then 
				-- replace the colour with new value 
				img.image:putPixel(xx,yy, clrs[i+1]) 
			elseif i == steps then 
				-- if the colour is last in the list 
				-- replace it with the first one 
				img.image:putPixel(xx,yy, clrs[1]) 
			end
		end 
	end 

end 


function fClear() 
	for i in pairs(clrs) do 
		temp[i] = nil 
        clrs[i] = nil 
    end 
	clr_list = nil 
end 

below is little bonus, if you like clicking the mouse.
what this does is that anytime you click on “get the colour” it will read foreground colour index and add it to the list. you can then copy this list and use it to feed colour cycling script. useful? err… maybe? anyway, here you go:

get-list

local dlgWin = Dialog{ title = "more clicks = more fun  " } 

list = {} 
counter = 1 

function fGet( dlgWin ) 
	
	table.insert(list, math.floor(app.fgColor.index) ) 
	
	local dlgDat = dlgWin.data
	dlgDat.output = table.concat(list, " ") 
	
	dlgWin.data = dlgDat 
	
end 

function fDel( dlgWin ) 
	
	local dlgDat = dlgWin.data 
	
	for i in pairs(list) do 
		list[i] = nil 
    end 
	
	dlgDat.output = ""
	
	dlgWin.data = dlgDat 
	
end 

-- set dialog window 
dlgWin
	:entry{ 
		id = "output", 
		--value = list 
	}
	
	:button{ 
		text = "get the colour", 
		onclick = function()
			fGet(dlgWin) 
		end 
	} 
	
	:newrow() 
	
	:button{ 
		text = "delete list", 
		onclick = function()
			fDel(dlgWin) 
		end 
	} 
	
-- show dialog window 
dlgWin:show{ wait = false }
5 Likes

v0.02 update: script now generates only one undo step in history, thanks to eishiya for help!

colour-cycle-v002

-- COLOUR CYCLE 0.02 
-- b236 
-- 
-- *** scanline colour cycling effect *** 
-- 
-- 1) IMPORTANT: set sprite to indexed 
-- 2) select cel 
-- 3) run script 
-- 4) enter colour indexes you want to use 
--    for example: 42 23 11 
-- 5) click on button 
--    the script will duplicate current frame n times, 
--    with n being the length of your list (in our example n=3) 
--    and it will cycle through the colours 
--    changing all pixel colours on each step 
--
-- IMPORTANT: don't repeat numbers 
-- if you want to have a "wave" effect, 
-- instead of entering a sequence 11 23 42 23 11 
-- duplicate colours 23 and 11 in your palette 
-- as new entries and use their indexes 
-- like this: 
-- 44 (duplicate of 11) 43 (duplicate of 23) 42 23 11 
-- 
-- if you need an animation in reversed order, select all frames 
-- and hit alt+i or go to menu Frame -> Reverse Frames 


local dlgWin = Dialog{ title = "*** CYCLE COLOURS ***  " } 
local clrs = {} 
local steps = 0 
local img = app.activeCel 
local img_h = img.image.height
local img_w = img.image.width 

-- set dialog window 
dlgWin
	:entry{ 
		id = "clrs_input"
	}
	
	:button{ 
		text = "start the riot!", 
		onclick = function()
			fInput(dlgWin) 
		end 
	} 
	
-- show dialog window 
dlgWin:show{ wait = false } 



function fInput(dlgWin) 
	
	-- wrap function in transaction, 
	-- so only one history step is generated 
	app.transaction( function() 
	
		-- convert input entry to string 
		clr_list = tostring(dlgWin.data.clrs_input) 
		
		-- search string for digits, convert them to numbers and insert them into table 
		for i in string.gmatch( clr_list, "%d+") do
			table.insert(clrs, tonumber(i)) 
		end
		
		-- get table length 
		steps = #clrs 
		
		-- duplicate cels
		for v = 1, steps-1, 1 do 
			app.command.NewFrame() 
			-- start the riot 
			fMain() 
		end 
	
	end) 
	
	-- close dialog	window 
	dlgWin:close() 
end 



function fMain() 
	
	img = app.activeCel 
	
	-- scan through all pixels in selected cel 
	for yy = 0, img_h-1, 1 do 
		for xx = 0, img_w-1, 1 do 
			-- get pixel value 
			clr_i = img.image:getPixel(xx,yy); 
			-- do the magic trick 
			fCycle(xx,yy,clr_i) 
		end 
	end 
	
end 



function fCycle(xx,yy,clr_i) 

	-- check if colour index of pixel is in the list 
	for i, value in pairs(clrs) do
		if value == clr_i then
			if i < steps then 
				-- replace the colour with new value 
				img.image:putPixel(xx,yy, clrs[i+1]) 
			elseif i == steps then 
				-- if the colour is last in the list 
				-- replace it with the first one 
				img.image:putPixel(xx,yy, clrs[1]) 
			end
		end 
	end 

end
1 Like

TODO (kind of) list:

  1. i’d love to have a widget for selecting colours instead of inserting a list of values, but i don’t feel competent enough to do that.
    edit: i’ve made few tests and widget doesn’t seem to be an option now. probably the main problem is that dialog:shades widget doesn’t wrap itself in the dialog window, so it can’t be used effectively for more than few colours.
  2. option for reversing the animation
    edit: v0.05
  3. option to choose whether to add cels or frames.
    edit: v0.03
  4. reusable version - script won’t close itself and doesn’t have to be run again to be used. very minor, but might be useful.
    edit: v0.04
1 Like

v0.03 update:
now you can choose whether to create new frames or just add new cels. no more fiddling with timeline to create more colour cycling effects.

colour-cycle-v003

-- COLOUR CYCLE 0.03 
-- b236 
-- 
-- *** scanline colour cycling effect *** 
-- 
-- 1) IMPORTANT: set sprite to indexed 
-- 2) select cel 
-- 3) run script 
-- 4) enter colour indexes you want to use 
--    for example: 42 23 11 
-- 5) select or deselect option "add frames"  
-- 6) click on button 
-- 
-- ADD FRAMES OPTION: 
-- if the option is selected then: 
--    the script will duplicate current frame n times, 
--    with n being the length of your list (in our example n=3) 
-- if not: 
--    the script will duplicate only current cel n times 
-- in both cases it will cycle through the colours 
-- changing selected pixel indexes on each step 
-- 
-- IMPORTANT: don't repeat numbers 
-- if you want to have a "wave" effect, 
-- instead of entering a sequence 11 23 42 23 11 
-- duplicate colours 23 and 11 in your palette 
-- as new entries and use their indexes 
-- like this: 
-- 44 (duplicate of 11) 43 (duplicate of 23) 42 23 11 
-- 
-- IMPORTANT: for adding cels you need enough empty frames 
-- in timeline. 
-- 
-- if you need an animation in reversed order, select all frames 
-- and hit alt+i or go to menu Frame -> Reverse Frames 


local dlgWin = Dialog{ title = "*** CYCLE COLOURS ***  " } 
local clrs = {} 
local steps = 0 
local img = app.activeCel 
local img_h = img.image.height
local img_w = img.image.width 

local aS = app.activeSprite
local aL = app.activeLayer 
local aF = app.activeFrame.frameNumber
local aI = img.image 
local aP = img.position 
local sF = aS.frames 
local frames = #sF - aF 


-- set dialog window 
dlgWin
	:entry{ 
		id = "clrs_input" 
	}
	
	:check{ 
		id = "add_frames", 
		text = "add frames", 
		selected = false 
	}
	
	:button{ 
		text = "start the riot!", 
		onclick = function()
			fInput(dlgWin) 
		end 
	} 
	
-- show dialog window 
dlgWin:show{ wait = false } 



function fInput(dlgWin) 
	
	-- wrap function in transaction, 
	-- so only one history step is generated 
	app.transaction( function() 
	
		-- convert input entry to string 
		clr_list = tostring(dlgWin.data.clrs_input) 
		
		-- search string for digits, convert them to numbers and insert them into table 
		for i in string.gmatch( clr_list, "%d+") do
			table.insert(clrs, tonumber(i)) 
		end
		
		-- get table length 
		steps = #clrs 
		
		-- duplicate cels
		for v = 1, steps-1, 1 do 

			-- if add frames is selected: 
			if dlgWin.data.add_frames == true then 
				
				app.command.NewFrame() 
				img = app.activeCel 
				
				-- start the riot 	
				fMain()
				
			-- if add frames is NOT selected: 
			elseif dlgWin.data.add_frames == false then 
			
				-- check if there are enough empty cels: 
				if frames > 0 and frames >= steps-1 then 
					
					aI = img.image 
					aS:newCel(aL, aF+v, aI, aP)
					img = aL:cel(aF+v) 
					
					-- start the riot 
					fMain() 
	
				else 
					-- if not, do nothing 
					app.alert("not enough empty cels!") 
					break 
				end 
				
			-- this nasty block 
			-- is here to help aseprite to refresh :P 
			app.command.GotoNextFrame() 
			app.command.GotoPreviousFrame() 
			-- 
			
			end
		end 
	
	end) 
	
	-- close dialog	window 
	dlgWin:close() 
end 



function fMain() 
	
	--img = app.activeCel 
	
	-- scan through all pixels in selected cel 
	for yy = 0, img_h-1, 1 do 
		for xx = 0, img_w-1, 1 do 
			-- get pixel value 
			clr_i = img.image:getPixel(xx,yy); 
			-- do the magic trick 
			fCycle(xx,yy,clr_i) 
		end 
	end 
	
end 



function fCycle(xx,yy,clr_i) 

	-- check if colour index of pixel is in the list 
	for i, value in pairs(clrs) do
		if value == clr_i then
			if i < steps then 
				-- replace the colour with new value 
				img.image:putPixel(xx,yy, clrs[i+1]) 
			elseif i == steps then 
				-- if the colour is last in the list 
				-- replace it with the first one 
				img.image:putPixel(xx,yy, clrs[1]) 
			end
		end 
	end 

end

v0.04 update:
script is now reusable - it won’t close itself after finishing the task.

colour-cycle-v003

-- COLOUR CYCLE 0.04 
-- b236 
-- 
-- *** scanline colour cycling effect *** 
-- 
-- 1) IMPORTANT: set sprite to indexed 
-- 2) select cel 
-- 3) run script 
-- 4) enter colour indexes you want to use 
--    for example: 42 23 11 
-- 5) select or deselect option "add frames"  
-- 6) click on button 
-- 
-- ADD FRAMES OPTION: 
-- if the option is selected then: 
--    the script will duplicate current frame n-1 times, 
--    with n being the length of your list (in our example n=3) 
-- if not: 
--    the script will duplicate only current cel n-1 times 
-- in both cases it will cycle through the colours 
-- changing selected pixel indexes on each step 
-- 
-- IMPORTANT: don't repeat numbers.
-- unless it's exactly what you want to do. 
-- if you want to have a "wave" effect, 
-- instead of entering a sequence 11 23 42 23 11 
-- duplicate colours 23 and 11 in your palette 
-- as new entries and use their indexes 
-- like this: 
-- 44 (duplicate of 11) 43 (duplicate of 23) 42 23 11 
-- 
-- IMPORTANT: for adding cels you need enough empty frames 
-- in timeline. 
-- 
-- IMPORTANT: don't use on layer which already contains animation! 
-- it will be overwritten! 
-- 
-- if you need an animation in reversed order, select all frames 
-- and hit alt+i or go to menu Frame -> Reverse Frames 


local dlgWin = Dialog{ title = "*** CYCLE COLOURS ***  " } 
local clrs = {} 
local steps = 0 


-- set dialog window 
dlgWin
	:entry{ 
		id = "clrs_input" 
	}
	
	:check{ 
		id = "add_frames", 
		text = "add frames", 
		selected = false 
	}
	
	:button{ 
		text = "start the riot!", 
		onclick = function()
			fInput(dlgWin) 
		end 
	} 
	
-- show dialog window 
dlgWin:show{ wait = false } 


function fInput(dlgWin) 
	
	img = app.activeCel 
	img_h = img.image.height
	img_w = img.image.width 

	aS = app.activeSprite
	aL = app.activeLayer 
	aF = app.activeFrame.frameNumber
	aI = img.image 
	aP = img.position 
	sF = aS.frames 
	frames = #sF - aF 
		
	-- wrap function in transaction, 
	-- so only one history step is generated 
	app.transaction( function() 
	
		-- convert input entry to string 
		clr_list = tostring(dlgWin.data.clrs_input) 
		
		-- search string for digits, convert them to numbers and insert them into table 
		for i in string.gmatch( clr_list, "%d+") do
			table.insert(clrs, tonumber(i)) 
		end
		
		-- get table length 
		steps = #clrs 
		
		-- duplicate cels
		for v = 1, steps-1, 1 do 

			-- if add frames is selected: 
			if dlgWin.data.add_frames == true then 
				
				app.command.NewFrame() 
				img = app.activeCel 
				
				-- start the riot 	
				fMain()
				
			-- if add frames is NOT selected: 
			elseif dlgWin.data.add_frames == false then 
			
				-- check if there are enough empty cels: 
				if frames > 0 and frames >= steps-1 then 
					
					aI = img.image 
					aS:newCel(aL, aF+v, aI, aP)
					img = aL:cel(aF+v) 
					
					-- start the riot 
					fMain() 
	
				else 
					-- if not, do nothing 
					app.alert("not enough empty cels!") 
					break 
				end 
				
			-- this nasty block is here 
			-- to help aseprite to refresh :P 
			app.command.GotoNextFrame() 
			app.command.GotoPreviousFrame() 
			-- app.refresh() doesn't update timeline 
			
			end
		end 
	
	end) 
	
	-- clear colour table for next use 
	fClear() 
	
	-- close dialog	window 
	--dlgWin:close() 
end 


function fMain() 
	
	--img = app.activeCel 
	
	-- scan through all pixels in selected cel 
	for yy = 0, img_h-1, 1 do 
		for xx = 0, img_w-1, 1 do 
			-- get pixel value 
			clr_i = img.image:getPixel(xx,yy); 
			-- do the magic trick 
			fCycle(xx,yy,clr_i) 
		end 
	end 
	
end 


function fCycle(xx,yy,clr_i) 

	-- check if colour index of pixel is in the list 
	for i, value in pairs(clrs) do
		if value == clr_i then
			if i < steps then 
				-- replace the colour with new value 
				img.image:putPixel(xx,yy, clrs[i+1]) 
			elseif i == steps then 
				-- if the colour is last in the list 
				-- replace it with the first one 
				img.image:putPixel(xx,yy, clrs[1]) 
			end
		end 
	end 

end 


function fClear() 
	for i in pairs(clrs) do
        clrs[i] = nil 
    end 
	clr_list = nil 
end