├── .gitignore ├── LICENSE ├── README.md ├── colorFade.lua ├── conf.lua ├── main.lua ├── run.lua └── scenes ├── 1.lua └── 2.lua /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019-2021 Nicola Orlando 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VRRTest 2 | A very small utility I wrote to test variable refresh rate on Linux. Should work on all major OSes. 3 | 4 | ## Usage 5 | Just run the executable. Builds are provided for Windows and Linux (64-bit). You can download the LÖVE [https://love2d.org] runtime to run the .love file on any supported OS on any supported architecture. 6 | Assuming the runtime is installed and in PATH, you can run it with `love `, where `` is the directory where this repo is cloned/extracted. 7 | * Up and down arrows will change the target FPS of the tool. 8 | * `Ctrl+f` toggles fullscreen. 9 | * `b` toggles busy waiting. Having it on makes the framerate more precise, at the cost of a ton of battery and CPU utilization. Off by default. 10 | * `s` toggles VSync. 11 | * `f` toggles fluctuating framerate; `Ctrl+↑/↓` changes the maximum framerate, `Ctrl+←/→` changes the fluctuation speed. 12 | * `r` toggles random stutter; `Alt+↑/↓` changes the amount of stuttering. Hold Shift as well to change faster. 13 | * `Alt+←/→` changes the monitor the tool will be displayed on. 14 | * `l` increases the amount of information shown on the screen, from nothing, to GPU-related information, to a list of frametimes. Wraps around. 15 | * Number keys will select a scene to be displayed. Each scene has additional controls, shown on the right. 16 | 17 | ## Scenes 18 | As of version 2.0.0, VRRTest supports different scenes, of which there are currently two. (A 100% increase over previous versions!) 19 | 20 | ### Bars 21 | The first and default scene, Bars, is easy on the eyes (I'm not claiming it looks good; you'll see what I mean in the screenshots section) and easily allows the user to detect screen tearing, by displaying vertical bars moving towards the right. The number and speed of said bars are tunable by the user. 22 | Additional controls are as follows: 23 | * Left and right arrows will change the speed of the columns moving across the screen. 24 | * `+` and `-` will change the amount of columns. 25 | 26 | ### Squares 27 | The second scene, Squares, adopts a higher-contrast color scheme (pure white on pure black) due to one of its functions. It displays a grid of squares, lighting up one (or more; see further) square per frame, each frame switching to the next one. The size of the squares can be changed by the user. 28 | Optionally, a trail can be set to light up more than one square per frame (or to have squares stay lit up for more than one frame; the end result is the same) to achieve two different functions: 29 | * Having no trail and a very high contrast allows the user to easily take a video, to later check frame-by-frame, or a long-exposure picture (assuming your phone or camera can do it) to check for duplicate or dropped frames. The maximum exposure length is the tool's period, which is displayed on the top-right. Examples of long-exposure pictures will be provided in the Screenshots section, even though they aren't screenshots. Guess I might just call it "Screenshots and pictures". 30 | * Having a trail might be useful if you need or want to check the latency difference of two mirrored monitors (or any other way to mirror a monitor, such as [Looking Glass](https://looking-glass.io) if, like me, you're one of those VFIO people), taking a picture of this tool running on the mirrored monitors with a trail makes counting the difference in frames easier, by just counting how many squares ahead (or behind) each output is. Not sure why that is, though; it might just be me. 31 | 32 | Additional controls are as follows: 33 | * Left and right arrows will decrease or increase the trail length. 34 | * `+` and `-` will increase or decrease, respectively, the size of the squares. 35 | 36 | ## Screenshots and pictures 37 | Some of these can't be screenshots. I apologize in advance for the quality of the pictures I took, as I don't own a camera or a tripod, both of which would prove useful in taking long-exposure pictures. 38 | ### Scene 1 - Bars 39 | ![Scene 1 screenshot without tearing](https://nixo.la/img/vrrtest_scene1.png) 40 | *How the Bars scene is supposed to look like, without any screen tearing. Ignore the visible portion of the cursor, please.* 41 | 42 | 43 | ![Scene 1 screenshot with tearing](https://nixo.la/img/vrrtest_scene1_tearing.png) 44 | *A screenshot of how the scene looks like with screen tearing.* 45 | 46 | 47 | ### Scene 2 - Squares 48 | ![Scene 2 screenshot](https://nixo.la/img/vrrtest_scene2.png) 49 | *How a still frame of the Squares scene looks like. Its usefulness can't easily be conveyed by still screenshots.* 50 | 51 | 52 | ![Scene 2 low framerate without VRR](https://nixo.la/img/vrrtest_scene2_novrr_low.png) 53 | *Long-exposure picture of a monitor with Freesync disabled with lower framerate than its refresh frequency. Notice how some squares are brighter than others, caused by duplicated frames.* 54 | 55 | 56 | ![Scene 2 high framerate without VRR](https://nixo.la/img/vrrtest_scene2_novrr_high.png) 57 | *Long-exposure picture of a monitor with Freesync disabled with higher framerate than its refresh frequency. The empty squared are caused by dropped frames. Note that this might happen on a Freesync monitor too, when above its frequency range.* 58 | 59 | 60 | ![Scene 2 low framerate with VRR](https://nixo.la/img/vrrtest_scene2_vrr_low.png) 61 | *Finally, a long-exposure picture of a Freesync monitor with refresh rate within its range. Everything looks like it should, with every lit square being approximately the same as the others.* 62 | 63 | 64 | ![Scene 2 high framerate with VRR](https://nixo.la/img/vrrtest_scene2_vrr_high.png) 65 | *Long-exposure picture of a Freesync monitor with higher framerate than its maximum refresh frequency. VRR can't do much in this case; either limit your framerate to below your monitor maximum refresh frequency or enable V-Sync in your software and/or driver.* 66 | -------------------------------------------------------------------------------- /colorFade.lua: -------------------------------------------------------------------------------- 1 | local color = {} 2 | 3 | local current = {} 4 | current.fg = {} 5 | current.bg = {} 6 | 7 | local target = {} 8 | target.fg = {} 9 | target.bg = {} 10 | 11 | local speed = {} 12 | speed.fg = {} 13 | speed.bg = {} 14 | 15 | color.setColor = function(fg, bg) 16 | current.fg = {unpack(fg)} 17 | current.bg = {unpack(bg)} 18 | target.fg = {unpack(fg)} 19 | target.bg = {unpack(bg)} 20 | speed.fg = {0, 0, 0} 21 | speed.bg = {0, 0, 0} 22 | end 23 | 24 | color.setTarget = function(fg, bg) 25 | target.fg = {unpack(fg)} 26 | target.bg = {unpack(bg)} 27 | speed.fg = {target.fg[1] - current.fg[1], target.fg[2] - current.fg[2], target.fg[3] - current.fg[3]} 28 | speed.bg = {target.bg[1] - current.bg[1], target.bg[2] - current.bg[2], target.bg[3] - current.bg[3]} 29 | end 30 | 31 | 32 | color.update = function(dt) 33 | for i = 1, 3 do 34 | local new_fg = current.fg[i] + speed.fg[i] * dt 35 | if new_fg <= target.fg[i] and current.fg[i] >= target.fg[i] or 36 | new_fg >= target.fg[i] and current.fg[i] <= target.fg[i] then 37 | new_fg = target.fg[i] 38 | end 39 | current.fg[i] = new_fg 40 | 41 | local new_bg = current.bg[i] + speed.bg[i] * dt 42 | if new_bg <= target.bg[i] and current.bg[i] >= target.bg[i] or 43 | new_bg >= target.bg[i] and current.bg[i] <= target.bg[i] then 44 | new_bg = target.bg[i] 45 | end 46 | current.bg[i] = new_bg 47 | end 48 | love.graphics.setColor(current.fg) 49 | love.graphics.setBackgroundColor(current.bg) 50 | end 51 | 52 | color.bg = function() 53 | return current.bg 54 | end 55 | 56 | color.fg = function() 57 | return current.fg 58 | end 59 | 60 | return color -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.identity = "freesynctest" -- The name of the save directory (string) 3 | t.appendidentity = false -- Search files in source directory before save directory (boolean) 4 | t.version = "11.0" -- The LÖVE version this game was made for (string) 5 | t.console = false -- Attach a console (boolean, Windows only) 6 | t.accelerometerjoystick = false -- Enable the accelerometer on iOS and Android by exposing it as a Joystick (boolean) 7 | t.externalstorage = false -- True to save files (and read from the save directory) in external storage on Android (boolean) 8 | t.gammacorrect = false -- Enable gamma-correct rendering, when supported by the system (boolean) 9 | 10 | t.audio.mixwithsystem = true -- Keep background music playing when opening LOVE (boolean, iOS and Android only) 11 | 12 | t.window.title = "Freesync test" -- The window title (string) 13 | t.window.icon = nil -- Filepath to an image to use as the window's icon (string) 14 | t.window.width = 0 -- The window width (number) 15 | t.window.height = 0 -- The window height (number) 16 | t.window.borderless = true -- Remove all border visuals from the window (boolean) 17 | t.window.resizable = false -- Let the window be user-resizable (boolean) 18 | t.window.minwidth = 1 -- Minimum window width if the window is resizable (number) 19 | t.window.minheight = 1 -- Minimum window height if the window is resizable (number) 20 | t.window.fullscreen = true -- Enable fullscreen (boolean) 21 | t.window.fullscreentype = "desktop" -- Choose between "desktop" fullscreen or "exclusive" fullscreen mode (string) 22 | t.window.vsync = 0 -- Vertical sync mode (number) 23 | t.window.msaa = 0 -- The number of samples to use with multi-sampled antialiasing (number) 24 | t.window.depth = nil -- The number of bits per sample in the depth buffer 25 | t.window.stencil = nil -- The number of bits per sample in the stencil buffer 26 | t.window.display = 1 -- Index of the monitor to show the window in (number) 27 | t.window.highdpi = false -- Enable high-dpi mode for the window on a Retina display (boolean) 28 | t.window.x = nil -- The x-coordinate of the window's position in the specified display (number) 29 | t.window.y = nil -- The y-coordinate of the window's position in the specified display (number) 30 | 31 | t.modules.audio = false -- Enable the audio module (boolean) 32 | t.modules.data = false -- Enable the data module (boolean) 33 | t.modules.event = true -- Enable the event module (boolean) 34 | t.modules.font = true -- Enable the font module (boolean) 35 | t.modules.graphics = true -- Enable the graphics module (boolean) 36 | t.modules.image = true -- Enable the image module (boolean) 37 | t.modules.joystick = false -- Enable the joystick module (boolean) 38 | t.modules.keyboard = true -- Enable the keyboard module (boolean) 39 | t.modules.math = true -- Enable the math module (boolean) 40 | t.modules.mouse = true -- Enable the mouse module (boolean) 41 | t.modules.physics = false -- Enable the physics module (boolean) 42 | t.modules.sound = false -- Enable the sound module (boolean) 43 | t.modules.system = false -- Enable the system module (boolean) 44 | t.modules.thread = false -- Enable the thread module (boolean) 45 | t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update 46 | t.modules.touch = false -- Enable the touch module (boolean) 47 | t.modules.video = false -- Enable the video module (boolean) 48 | t.modules.window = true -- Enable the window module (boolean) 49 | end -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | require "run" 2 | 3 | local color = require "colorFade" 4 | 5 | local fps, frameTime, lastUpdate 6 | local fpsMax, fpsTimer, fluctuating, fpsSpeed 7 | 8 | local random, randomAmount, randomTime 9 | 10 | local displays, display 11 | 12 | local fullscreen 13 | 14 | local vsync 15 | 16 | local logLevel, logLevels 17 | local deltaTimes, logLines, logWidth 18 | 19 | local WIDTH, HEIGHT 20 | 21 | local scenes = {} 22 | scenes.x = 0 23 | scenes.y = 0 24 | local scene = 1 25 | local loadScenes = function(width, height) 26 | for i, file in ipairs(love.filesystem.getDirectoryItems("scenes")) do 27 | local f, err = love.filesystem.load("scenes/" .. file) 28 | if not f then 29 | print("Could not load", file..":", err) 30 | else 31 | f, err = pcall(f) 32 | if not f then 33 | print("Could not run", file..":", err) 34 | else 35 | scenes[i] = err 36 | err.load(width, height) 37 | print(i, err) 38 | end 39 | end 40 | end 41 | end 42 | 43 | local setDisplay = function(n) 44 | local new_displays = love.window.getDisplayCount() 45 | n = (n-1) % new_displays + 1 46 | if n == display and new_displays == displays then return end 47 | WIDTH, HEIGHT = love.window.getDesktopDimensions(n) 48 | love.window.setMode(WIDTH, HEIGHT, {display = n, fullscreen = fullscreen, vsync = vsync and 1 or 0}) 49 | display = n 50 | displays = new_displays 51 | for i, scene in ipairs(scenes) do 52 | scene.resize(WIDTH - scenes.x, HEIGHT - scenes.y) 53 | end 54 | end 55 | 56 | 57 | 58 | local str = [[ 59 | actual FPS: %d 60 | target FPS: %d (change with up/down arrow) 61 | fullscreen: %s (toggle with ctrl+f) 62 | busy wait: %s (toggle with b) 63 | vsync: %s (toggle with s) 64 | fluctuating: %s (toggle with f, change max with ctrl + up/down arrow, change speed with ctrl + left/right arrow) 65 | random stutter: %s [%dms] (toggle with r, change max amount with alt + up/down arrow, shift to change faster) 66 | display: %d/%d (switch with alt + left/right arrow) 67 | log level: %d/%d (increase with l, wraps around) 68 | selected scene: %d (%s) 69 | 70 | Freesync will only work when the application is fullscreen on Linux. 71 | Busy waiting is more precise, but much heavier on processor and battery. 72 | Vsync should eliminate tearing, but increases input lag and adds no smoothness. 73 | You can quit this program with the Escape or Q keys.]] 74 | local sceneStr 75 | 76 | love.load = function() 77 | 78 | love.busy = false 79 | 80 | WIDTH, HEIGHT = love.graphics.getDimensions() 81 | 82 | local flags = select(3, love.window.getMode()) 83 | 84 | scenes.y = (#select(2, love.graphics.getFont():getWrap(str, WIDTH)) + 1) * love.graphics.getFont():getHeight() + 8 85 | 86 | fps = flags.refreshrate - 5 87 | fps = (fps > 0) and fps or 56 88 | 89 | fpsMax = fps 90 | fpsTimer = 0 91 | fluctuating = false 92 | fpsSpeed = 10 93 | 94 | random = false 95 | randomTime = 0 96 | randomAmount = 0 97 | 98 | displays = love.window.getDisplayCount() 99 | display = 1 100 | 101 | frameTime = 1 / fps 102 | lastUpdate = 0 103 | 104 | logLevel = 0 105 | logLevels = 3 -- 0-2 106 | deltaTimes = {} 107 | logLines = math.floor((HEIGHT - scenes.y) / love.graphics.getFont():getHeight()) 108 | logWidth = love.graphics.getFont():getWidth("00000 µs") + 16 109 | 110 | fullscreen = flags.fullscreen 111 | vsync = flags.vsync > 0 112 | love.keyboard.setKeyRepeat(true) 113 | 114 | loadScenes(WIDTH - scenes.x, HEIGHT - scenes.y) 115 | 116 | color.setColor(scenes[scene].color.fg, scenes[scene].color.bg) 117 | end 118 | 119 | 120 | love.update = function(dt) 121 | 122 | if love.busy then 123 | while lastUpdate + frameTime + randomTime > love.timer.getTime() do end 124 | else 125 | while lastUpdate + frameTime + randomTime > love.timer.getTime() do 126 | love.timer.sleep(0) 127 | end 128 | end 129 | lastUpdate = love.timer.getTime() 130 | 131 | fpsTimer = fpsTimer + dt * fpsSpeed / 10 132 | if fluctuating then 133 | fpsCur = fps + (math.sin(fpsTimer)/2 + 0.5) * (fpsMax - fps) 134 | frameTime = 1/fpsCur 135 | end 136 | 137 | if random then 138 | randomTime = (love.math.random() - 0.5 ) * randomAmount/1000 139 | end 140 | 141 | scenes[scene].update(dt, fps) 142 | color.update(dt) 143 | if logLevel > 1 then 144 | table.insert(deltaTimes, 1, string.format("%d µs", dt * 1000000)) 145 | deltaTimes[logLines + 1] = nil 146 | end 147 | end 148 | 149 | 150 | love.draw = function() 151 | local fstr = fluctuating and ("true [max: %d, speed: %d, current: %d]"):format(fpsMax, fpsSpeed, fpsCur) or "false" 152 | 153 | local str = string.format(str, 154 | love.timer.getFPS(), 155 | fps, 156 | tostring(fullscreen), 157 | tostring(love.busy), 158 | tostring(vsync), 159 | fstr, 160 | tostring(random), randomAmount, 161 | display, displays, 162 | logLevel, logLevels - 1, 163 | scene, 164 | scenes[scene].name) 165 | 166 | love.graphics.print(str, 8, 8) 167 | love.graphics.print(scenes[scene].str, WIDTH - scenes[scene].strWidth - 8, 8) 168 | if logLevel > 0 then 169 | love.graphics.printf(table.concat({love.graphics.getRendererInfo()}, "\n"), 0, scenes.y - love.graphics.getFont():getHeight() * 5, WIDTH - 8, "right") 170 | end 171 | 172 | scenes[scene].draw(scenes.x, scenes.y) 173 | 174 | if logLevel > 1 then 175 | --love.graphics.setColor(.75, 0, 0) 176 | love.graphics.setColor(color.bg()) 177 | love.graphics.rectangle("fill", WIDTH - logWidth, scenes.y, logWidth + 1, HEIGHT) 178 | love.graphics.setColor(color.fg()) 179 | for i, ms in ipairs(deltaTimes) do 180 | love.graphics.printf(ms, 0, scenes.y + (i - 1) * love.graphics.getFont():getHeight(), WIDTH - 8, "right") 181 | end 182 | end 183 | 184 | end 185 | 186 | 187 | sanitize = function() 188 | fps = math.max(1, fps) 189 | frameTime = 1/fps 190 | fpsMax = math.max(fpsMax, fps) 191 | fpsSpeed = math.max(1, fpsSpeed) 192 | 193 | randomAmount = math.max(math.min(randomAmount, 1000), 0) 194 | end 195 | 196 | love.keypressed = function(key, keycode) 197 | 198 | local ctrl = love.keyboard.isDown("lctrl", "rctrl") 199 | local shift = love.keyboard.isDown("lshift", "rshift") 200 | local alt = love.keyboard.isDown("ralt", "lalt") 201 | 202 | if ctrl then 203 | if key == "up" then 204 | fpsMax = fpsMax + 1 205 | elseif key == "down" then 206 | fpsMax = fpsMax - 1 207 | elseif key == "left" then 208 | fpsSpeed = fpsSpeed - 1 209 | elseif key == "right" then 210 | fpsSpeed = fpsSpeed + 1 211 | elseif key == "f" then 212 | if fullscreen then 213 | love.window.setFullscreen(false) 214 | love.window.setPosition(1, 1) 215 | else 216 | love.window.setFullscreen(true) 217 | love.window.setPosition(0, 0) 218 | end 219 | fullscreen = not fullscreen 220 | end 221 | elseif alt then 222 | if key == "up" then 223 | randomAmount = randomAmount + (shift and 5 or 1) 224 | elseif key == "down" then 225 | randomAmount = randomAmount - (shift and 5 or 1) 226 | elseif key == "left" then 227 | setDisplay(display - 1) 228 | return 229 | elseif key == "right" then 230 | setDisplay(display + 1) 231 | return 232 | end 233 | else 234 | if key == "up" then 235 | fps = fps + 1 236 | elseif key == "down" then 237 | fps = fps - 1 238 | elseif key == "f" then 239 | fluctuating = not fluctuating 240 | fpsTimer = 0 241 | elseif key == "b" then 242 | love.busy = not love.busy 243 | elseif key == "s" then 244 | local w, h, flags = love.window.getMode() 245 | flags.vsync = (flags.vsync == 0) and 1 or 0 246 | love.window.setMode(w, h, flags) 247 | flags = select(3, love.window.getMode()) 248 | vsync = flags.vsync > 0 249 | elseif key == "r" then 250 | random = not random 251 | randomTime = 0 252 | elseif key == "escape" or key == "q" then 253 | love.event.quit() 254 | elseif key == "l" then 255 | logLevel = (logLevel + 1) % logLevels 256 | end 257 | end 258 | if tonumber(key) and scenes[tonumber(key)] then 259 | scene = tonumber(key) 260 | color.setTarget(scenes[scene].color.fg, scenes[scene].color.bg) 261 | return 262 | end 263 | scenes[scene].keypressed(key, keycode) 264 | sanitize() 265 | end 266 | 267 | love.textinput = function(str) 268 | scenes[scene].textinput(str) 269 | sanitize() 270 | end 271 | -------------------------------------------------------------------------------- /run.lua: -------------------------------------------------------------------------------- 1 | function love.run() 2 | if love.load then love.load(love.arg.parseGameArguments(arg), arg) end 3 | 4 | -- We don't want the first frame's dt to include time taken by love.load. 5 | if love.timer then love.timer.step() end 6 | 7 | local dt = 0 8 | 9 | -- Main loop time. 10 | return function() 11 | -- Process events. 12 | if love.event then 13 | love.event.pump() 14 | for name, a,b,c,d,e,f in love.event.poll() do 15 | if name == "quit" then 16 | if not love.quit or not love.quit() then 17 | return a or 0 18 | end 19 | end 20 | love.handlers[name](a,b,c,d,e,f) 21 | end 22 | end 23 | 24 | -- Update dt, as we'll be passing it to update 25 | if love.timer then dt = love.timer.step() end 26 | 27 | -- Call update and draw 28 | if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled 29 | 30 | if love.graphics and love.graphics.isActive() then 31 | love.graphics.origin() 32 | love.graphics.clear(love.graphics.getBackgroundColor()) 33 | 34 | if love.draw then love.draw() end 35 | 36 | love.graphics.present() 37 | end 38 | 39 | if not love.busy then 40 | love.timer.sleep(0.001) 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /scenes/1.lua: -------------------------------------------------------------------------------- 1 | local scene = {} 2 | scene.name = "Bars" 3 | scene.color = {} 4 | scene.color.fg = {5/8, 5/8, 5/8} 5 | scene.color.bg = {3/8, 3/8, 3/8} 6 | 7 | local speed, num, bars, barWidth 8 | 9 | local WIDTH, HEIGHT 10 | 11 | local str = [[ 12 | speed: %d (change with left/right arrow) 13 | number of bars: %d (change with -/+)]] 14 | 15 | newBars = function() 16 | barWidth = WIDTH / (num * 3) 17 | for i = 1, num do 18 | bars[i] = WIDTH / num * (i - 1) 19 | end 20 | end 21 | 22 | scene.load = function(w, h) 23 | WIDTH, HEIGHT = w, h 24 | speed = 10 25 | num = 3 26 | bars = {} 27 | newBars() 28 | scene.strWidth = love.graphics.getFont():getWidth(str:format(1000, 1000)) 29 | end 30 | 31 | scene.resize = function(w, h) 32 | WIDTH, HEIGHT = w, h 33 | end 34 | 35 | scene.update = function(dt, fps) 36 | for i = 1, num do 37 | bars[i] = (bars[i] + speed * dt * WIDTH / 20) % (WIDTH) 38 | end 39 | scene.str = str:format(speed, num) 40 | end 41 | 42 | scene.draw = function(x, y) 43 | for i = 1, num do 44 | love.graphics.rectangle("fill", bars[i] + x , y, barWidth, HEIGHT) 45 | if bars[i] > WIDTH - barWidth then 46 | love.graphics.rectangle("fill", bars[i] - WIDTH + x, y, barWidth, HEIGHT) 47 | end 48 | end 49 | end 50 | 51 | scene.keypressed = function(key, keycode, isRepeat) 52 | local ctrl = love.keyboard.isDown("lctrl", "rctrl") 53 | local shift = love.keyboard.isDown("lshift", "rshift") 54 | local alt = love.keyboard.isDown("ralt", "lalt") 55 | 56 | if ctrl or shift or alt then return end 57 | 58 | if key == "left" then 59 | speed = speed - 1 60 | elseif key == "right" then 61 | speed = speed + 1 62 | end 63 | speed = math.max(1, speed) 64 | end 65 | 66 | scene.textinput = function(str) 67 | local ctrl = love.keyboard.isDown("lctrl", "rctrl") 68 | local shift = love.keyboard.isDown("lshift", "rshift") 69 | local alt = love.keyboard.isDown("ralt", "lalt") 70 | 71 | if str == "-" then 72 | num = num - 1 73 | num = math.max(1, num) 74 | newBars() 75 | elseif str == "+" then 76 | num = num + 1 77 | num = math.max(1, num) 78 | newBars() 79 | end 80 | end 81 | 82 | return scene 83 | -------------------------------------------------------------------------------- /scenes/2.lua: -------------------------------------------------------------------------------- 1 | local scene = {} 2 | scene.name = "Squares" 3 | scene.color = {} 4 | scene.color.fg = {1, 1, 1} 5 | scene.color.bg = {0, 0, 0} 6 | 7 | local frame, size, width, height, frames, trail, gcd 8 | local WIDTH, HEIGHT 9 | 10 | local str = [[ 11 | trail (frames): %d (change with left/right arrow) 12 | square size (px): %d (change with +/-) 13 | period (seconds): ~%.2f (results from size) 14 | 15 | trail=0 makes it easier to use a video or a 16 | long-exposure picture (lasting up to the shown period) 17 | to see repeated or dropped frames. Higher values can 18 | help show latency between monitors or other ways of 19 | mirroring a screen, when the same istance of this 20 | program is displayed on all of them.]] 21 | 22 | gcd = function(n1, n2) 23 | if n1 % n2 == 0 then 24 | return n2 25 | elseif n1 < n2 then 26 | n1, n2 = n2, n1 27 | end 28 | return gcd(n1 % n2, n2) 29 | end 30 | 31 | local wrap = function(n, limit) 32 | return n % limit 33 | end 34 | 35 | local sanitize = function() 36 | size = math.min(math.max(3, size), WIDTH, HEIGHT) 37 | width = math.ceil(WIDTH / size) 38 | height = math.ceil(HEIGHT / size) 39 | frames = width * height 40 | trail = math.min(math.max(trail, 0), frames - 1) 41 | end 42 | 43 | scene.load = function(w, h) 44 | WIDTH, HEIGHT = w, h 45 | frame = 0 46 | size = math.max(math.min(gcd(WIDTH, HEIGHT), WIDTH/4, HEIGHT/3), WIDTH/16, HEIGHT/9) 47 | width = math.ceil(WIDTH / size) 48 | height = math.ceil(HEIGHT / size) 49 | frames = width * height 50 | trail = 0 51 | scene.strWidth = love.graphics.getFont():getWidth(str:format(1000, 1000, 10.99)) 52 | end 53 | 54 | scene.resize = function(w, h) 55 | WIDTH, HEIGHT = w, h 56 | sanitize() 57 | end 58 | 59 | scene.update = function(dt, fps) 60 | frame = wrap(frame + 1, frames) 61 | scene.str = str:format(trail, size, frames / fps) 62 | end 63 | 64 | scene.draw = function(x, y) 65 | for lx = 0, width do 66 | love.graphics.line(lx * size + x, y, lx * size + x, HEIGHT + y) 67 | end 68 | for ly = 0, height do 69 | love.graphics.line(x, ly * size + y, WIDTH + x, ly * size + y) 70 | end 71 | 72 | for f = frame - trail, frame do 73 | local f = wrap(f, frames) 74 | local rx = f % width 75 | local ry = math.floor(f / width) 76 | 77 | love.graphics.rectangle("fill", x + rx * size, y + ry * size, size, size) 78 | end 79 | 80 | end 81 | 82 | scene.keypressed = function(key, keycode, isRepeat) 83 | local ctrl = love.keyboard.isDown("lctrl", "rctrl") 84 | local shift = love.keyboard.isDown("lshift", "rshift") 85 | local alt = love.keyboard.isDown("ralt", "lalt") 86 | 87 | if ctrl or shift or alt then return end 88 | 89 | if key == "left" then 90 | trail = trail - 1 91 | elseif key == "right" then 92 | trail = trail + 1 93 | end 94 | sanitize() 95 | end 96 | 97 | scene.textinput = function(str) 98 | local ctrl = love.keyboard.isDown("lctrl", "rctrl") 99 | local shift = love.keyboard.isDown("lshift", "rshift") 100 | local alt = love.keyboard.isDown("ralt", "lalt") 101 | 102 | if str == "+" then 103 | size = size + 1 104 | elseif str == "-" then 105 | size = size - 1 106 | end 107 | sanitize() 108 | end 109 | 110 | return scene 111 | --------------------------------------------------------------------------------