-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathL5.lua
More file actions
4094 lines (3531 loc) · 121 KB
/
L5.lua
File metadata and controls
4094 lines (3531 loc) · 121 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- L5 0.1.5 (c) Lee Tusman and Contributors GNU LGPL2.1
VERSION = '0.1.5'
-- Override love.run() - adds double buffering and custom events
function love.run()
defaults()
define_env_globals()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local dt = 0
local setupComplete = false
-- Main loop
return function()
-- Process events
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a or 0
end
end
-- Handle mouse events - store them for drawing phase
if name == "mousepressed" then
-- a = x, b = y, c = button, d = istouch, e = presses
L5_env.pendingMouseClicked = {x = a, y = b, button = c}
elseif name == "mousereleased" then
-- a = x, b = y, c = button, d = istouch, e = presses
L5_env.pendingMouseReleased = {x = a, y = b, button = c}
end
-- Handle other events through the default handlers
if love.handlers[name] then
love.handlers[name](a,b,c,d,e,f)
end
end
end
-- Update dt
if love.timer then dt = love.timer.step() end
-- Update
if love.update then love.update(dt) end
-- Draw with double buffering
if love.graphics and love.graphics.isActive() then
love.graphics.origin()
-- Set render target to back buffer
if L5_env.backBuffer then
love.graphics.setCanvas(L5_env.backBuffer)
end
-- Only clear if background() was called this frame
if L5_env.clearscreen then
-- background() already cleared with the right color
L5_env.clearscreen = false
end
-- Draw current frame
-- Run setup() once in the drawing context
if not setupComplete and setup then
setup()
setupComplete = true
else
if love.draw then love.draw() end
end
-- Reset to screen and draw the back buffer
love.graphics.setCanvas()
if L5_env.backBuffer then
-- Save current color
local r, g, b, a = love.graphics.getColor()
-- Set to white (no tint) when drawing the canvas to screen
love.graphics.setColor(1, 1, 1, 1)
if L5_env.filterOn then
if L5_env.filter == "blur_twopass" then
-- Two-pass blur requires intermediate canvas
if not L5_env.blurTempCanvas or
L5_env.blurTempCanvas:getWidth() ~= love.graphics.getWidth() or
L5_env.blurTempCanvas:getHeight() ~= love.graphics.getHeight() then
L5_env.blurTempCanvas = love.graphics.newCanvas()
end
-- Pass 1: Horizontal blur to temp canvas
love.graphics.setCanvas(L5_env.blurTempCanvas)
love.graphics.clear()
love.graphics.setShader(L5_filter.blur_horizontal)
love.graphics.draw(L5_env.backBuffer, 0, 0)
-- Pass 2: Vertical blur to screen
love.graphics.setCanvas()
love.graphics.setShader(L5_filter.blur_vertical)
love.graphics.draw(L5_env.blurTempCanvas, 0, 0)
love.graphics.setShader()
else
-- Single-pass filter
love.graphics.setShader(L5_env.filter)
love.graphics.draw(L5_env.backBuffer, 0, 0)
love.graphics.setShader()
end
L5_env.filterOn = false
else
-- No filter, just draw normally
love.graphics.draw(L5_env.backBuffer, 0, 0)
end
-- Restore color (after drawing the canvas)
love.graphics.setColor(r, g, b, a)
drawPrintBuffer()
love.graphics.present()
end
if love.timer then
if L5_env.framerate then --user-specified framerate
love.timer.sleep(1/L5_env.framerate)
else --default framerate
love.timer.sleep(0.001)
end
end
end
end
end
function love.load()
love.window.setVSync(1)
love.math.setRandomSeed(os.time())
displayWidth, displayHeight = love.window.getDesktopDimensions()
-- create default-size buffers. will be recreated again if size() or fullscreen(true) called
local w, h = love.graphics.getDimensions()
-- Create double buffers
L5_env.backBuffer = love.graphics.newCanvas(w, h)
L5_env.frontBuffer = love.graphics.newCanvas(w, h)
-- Clear both buffers initially
love.graphics.setCanvas(L5_env.backBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1) -- gray background
love.graphics.setCanvas(L5_env.frontBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1) -- gray background
love.graphics.setCanvas()
initShaderDefaults()
stroke(0)
fill(255)
end
function love.update(dt)
mouseX, mouseY = love.mouse.getPosition()
movedX=mouseX-pmouseX
movedY=mouseY-pmouseY
deltaTime = dt * 1000
key = updateLastKeyPressed()
-- Update looping videos
-- Note: Videos with audio tracks may experience sync issues when looping
-- This is a LÖVE limitation with video/audio stream synchronization
if L5_env.videos then
for _, v in ipairs(L5_env.videos) do
if v._shouldLoop and not v._manuallyPaused and not v._video:isPlaying() then
v._video:rewind()
v._video:play()
end
end
end
-- Optional update (not typically Processing-like but available)
if update ~= nil then update() end
end
function love.draw()
-- checking user events happens regardless of whether the user draw() function is currently looping
local isPressed = love.mouse.isDown(1) or love.mouse.isDown(2) or love.mouse.isDown(3)
if isPressed and not L5_env.wasPressed then
-- Mouse was just pressed this frame
if mousePressed ~= nil then mousePressed() end
mouseIsPressed = true
elseif not isPressed and L5_env.wasPressed then
-- Mouse was just released this frame
if mouseReleased ~= nil then mouseReleased() end
if mouseClicked ~= nil then mouseClicked() end -- Run immediately after mouseReleased
mouseIsPressed = false
elseif isPressed then
-- Still pressed - only call mouseDragged if mouse actually moved
if L5_env.mouseWasMoved then
if mouseDragged ~= nil then mouseDragged() end
L5_env.mouseWasMoved = false -- Clear the flag
end
mouseIsPressed = true
else
mouseIsPressed = false
end
L5_env.wasPressed = isPressed
-- Check for keyboard events in the draw cycle
if L5_env.keyWasPressed then
if keyPressed ~= nil then keyPressed() end
L5_env.keyWasPressed = false
end
if L5_env.keyWasReleased then
if keyReleased ~= nil then keyReleased() end
L5_env.keyWasReleased = false
end
if L5_env.keyWasTyped then
local savedKey = key
key = L5_env.typedKey -- Temporarily use the typed character
if keyTyped ~= nil then keyTyped() end
key = savedKey -- Restore
L5_env.keyWasTyped = false
L5_env.typedKey = nil
end
-- Check for mouse events in draw cycle
-- Only call mouseMoved if mouse button is NOT pressed
if L5_env.mouseWasMoved and not isPressed then
if mouseMoved ~= nil then mouseMoved() end
L5_env.mouseWasMoved = false
elseif L5_env.mouseWasMoved and isPressed then
-- Clear the flag even if we don't call mouseMoved
-- (mouseDragged already handled above)
L5_env.mouseWasMoved = false
end
if L5_env.wheelWasMoved then
if mouseWheel ~= nil then
mouseWheel(L5_env.wheelY or 0)
end
L5_env.wheelWasMoved = false
L5_env.wheelX = nil
L5_env.wheelY = nil
end
-- only run if user draw() function is looping
if L5_env.drawing then
frameCount = frameCount + 1
-- Reset transformation matrix to identity at start of each frame
love.graphics.origin()
love.graphics.push()
-- Call user draw function
if draw ~= nil then draw() end
pmouseX, pmouseY = mouseX,mouseY
love.graphics.pop()
end
end
function love.mousepressed(_x, _y, button, istouch, presses)
--turned off so as not to duplicate event handling running twice
--if mousePressed ~= nil then mousePressed() end
if button==1 then
mouseButton=LEFT
elseif button==2 then
mouseButton=RIGHT
elseif button==3 then
mouseButton=CENTER
end
end
function love.mousereleased( x, y, button, istouch, presses )
--if mouseClicked ~= nil then mouseClicked() end
--if focused and mouseReleased ~= nil then mouseReleased() end
end
function love.wheelmoved(_x,_y)
L5_env.wheelWasMoved = true
L5_env.wheelX = _x
L5_env.wheelY = _y
return _x, _y
end
function love.mousemoved(x,y,dx,dy,istouch)
L5_env.mouseWasMoved = true
end
function love.keypressed(k, scancode, isrepeat)
-- Add key to pressed keys table
L5_env.pressedKeys[k] = true
key = k
keyCode = love.keyboard.getScancodeFromKey(k)
L5_env.keyWasPressed = true
keyIsPressed = true
end
function love.keyreleased(k)
-- Remove key from pressed keys table
L5_env.pressedKeys[k] = nil
key = k
keyCode = love.keyboard.getScancodeFromKey(k)
L5_env.keyWasReleased = true
-- Only set keyIsPressed to false if no keys are pressed
local anyKeyPressed = false
for _ in pairs(L5_env.pressedKeys) do
anyKeyPressed = true
break
end
keyIsPressed = anyKeyPressed
end
function love.textinput(_text)
key = _text
L5_env.typedKey = _text
L5_env.keyWasTyped = true
end
function love.resize(w, h)
-- Recreate buffers when window is resized at density-scaled resolution
if L5_env.backBuffer then L5_env.backBuffer:release() end
if L5_env.frontBuffer then L5_env.frontBuffer:release() end
L5_env.backBuffer = love.graphics.newCanvas(w, h)
L5_env.frontBuffer = love.graphics.newCanvas(w, h )
-- Clear new buffers and apply scaling
love.graphics.setCanvas(L5_env.backBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1)
love.graphics.setCanvas(L5_env.frontBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1)
love.graphics.setCanvas(L5_env.backBuffer)
-- Update global width/height to logical size
width, height = w, h
-- Call user's windowResized function if it exists
if windowResized then
windowResized()
end
end
function love.focus(_focused)
focused = _focused
end
------------------- CUSTOM FUNCTIONS -----------------
function drawPrintBuffer()
if not L5_env.showPrintBuffer or #L5_env.printBuffer == 0 then
return
end
love.graphics.push()
love.graphics.origin()
-- Save user's current state
local userFont = love.graphics.getFont()
local pr, pg, pb, pa = love.graphics.getColor()
love.graphics.setFont(L5_env.printFont or L5_env.defaultFont)
-- Calculate max lines that fit on screen
local maxLines = math.floor((height - 10) / L5_env.printLineHeight)
-- Trim buffer to only show lines that fit
local displayBuffer = {}
local startIdx = math.max(1, #L5_env.printBuffer - maxLines + 1)
for i = startIdx, #L5_env.printBuffer do
table.insert(displayBuffer, L5_env.printBuffer[i])
end
-- Get the font to measure text width
local font = love.graphics.getFont()
-- Draw each line with its own background
local y = 5
for _, line in ipairs(displayBuffer) do
local textWidth = font:getWidth(line)
love.graphics.setColor(0, 0, 0, 0.7)
love.graphics.rectangle('fill', 5, y, textWidth + 4, L5_env.printLineHeight)
love.graphics.setColor(1, 1, 1)
love.graphics.print(line, 5, y)
y = y + L5_env.printLineHeight
end
-- Restore user's state
love.graphics.setFont(userFont)
love.graphics.setColor(pr, pg, pb, pa)
love.graphics.pop()
end
function printToScreen(textSize)
L5_env.showPrintBuffer = true
textSize = textSize or 16
L5_env.printFont = love.graphics.newFont(textSize)
L5_env.printLineHeight = L5_env.printFont:getHeight()
end
function size(_w, _h)
-- do nothing if the window size hasn't changed
if _w == width and _h == height then return end
-- must clear canvas before setMode
love.graphics.setCanvas()
love.window.setMode(_w, _h)
-- Recreate buffers for new size
if L5_env.backBuffer then L5_env.backBuffer:release() end
if L5_env.frontBuffer then L5_env.frontBuffer:release() end
L5_env.backBuffer = love.graphics.newCanvas(_w, _h)
L5_env.frontBuffer = love.graphics.newCanvas(_w, _h)
-- Clear new buffers
love.graphics.setCanvas(L5_env.backBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1)
love.graphics.setCanvas(L5_env.frontBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1)
-- Set back to back buffer for continued drawing
love.graphics.setCanvas(L5_env.backBuffer)
width, height = love.graphics.getDimensions()
end
function fullscreen(display)
display = display or 1
love.graphics.setCanvas()
local displays = love.window.getDisplayCount()
if display > displays then
display = 1
end
-- Get dimensions for the specified display
local w, h = love.window.getDesktopDimensions(display)
-- First, create a windowed mode on that display
love.window.setMode(w, h, {fullscreen = false})
-- Position the window on the target display
local xPos = 0
for i = 1, display - 1 do
local dw, _ = love.window.getDesktopDimensions(i)
xPos = xPos + dw
end
love.window.setPosition(xPos, 0)
-- Small delay for Windows to process window positioning
if love.timer then love.timer.sleep(0.1) end
-- Now go fullscreen
local success, err = pcall(function()
love.window.setFullscreen(true, "desktop")
end)
if not success then
print("Fullscreen error:", err)
return
end
-- Release old canvases
if L5_env.backBuffer then
pcall(function() L5_env.backBuffer:release() end)
end
if L5_env.frontBuffer then
pcall(function() L5_env.frontBuffer:release() end)
end
-- Create new canvases
L5_env.backBuffer = love.graphics.newCanvas(w, h)
L5_env.frontBuffer = love.graphics.newCanvas(w, h)
love.graphics.setCanvas(L5_env.backBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1)
love.graphics.setCanvas(L5_env.frontBuffer)
love.graphics.clear(0.5, 0.5, 0.5, 1)
love.graphics.setCanvas(L5_env.backBuffer)
width, height = love.graphics.getDimensions()
if windowResized then
windowResized()
end
end
function toColor(_a, _b, _c, _d)
-- If _a is a table, return it (assuming it's already in RGBA format)
if type(_a) == "table" and _b == nil and #_a == 4 then
return _a
end
local r, g, b, a
-- Handle different argument patterns
if _b == nil then
-- One argument = grayscale or color name
if type(_a) == "number" then
if L5_env.color_mode == RGB then
r, g, b, a = _a, _a, _a, L5_env.color_max[4]
elseif L5_env.color_mode == HSB then
-- Grayscale in HSB: hue=0, saturation=0, brightness=value
r, g, b = HSVtoRGB(0, 0, _a / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = L5_env.color_max[4]
elseif L5_env.color_mode == HSL then
-- Grayscale in HSL: hue=0, saturation=0, lightness=value
r, g, b = HSLtoRGB(0, 0, _a / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = L5_env.color_max[4]
end
elseif type(_a) == "string" then
if _a:sub(1, 1) == "#" then -- Hex color
r, g, b = hexToRGB(_a)
a = L5_env.color_max[4]
else -- HTML color name
if htmlColors[_a] then
r, g, b = unpack(htmlColors[_a])
a = L5_env.color_max[4]
else
error("Color '" .. _a .. "' not found in htmlColors table")
end
end
else
error("Invalid color argument")
end
elseif _c == nil then
-- Two arguments = grayscale with alpha
if L5_env.color_mode == RGB then
r, g, b, a = _a, _a, _a, _b
elseif L5_env.color_mode == HSB then
r, g, b = HSVtoRGB(0, 0, _a / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = _b
elseif L5_env.color_mode == HSL then
r, g, b = HSLtoRGB(0, 0, _a / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = _b
end
elseif _d == nil then
-- Three arguments = color components without alpha
if L5_env.color_mode == RGB then
r, g, b, a = _a, _b, _c, L5_env.color_max[4]
elseif L5_env.color_mode == HSB then
r, g, b = HSVtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = L5_env.color_max[4]
elseif L5_env.color_mode == HSL then
r, g, b = HSLtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = L5_env.color_max[4]
end
else
-- Four arguments = color components with alpha
if L5_env.color_mode == RGB then
r, g, b, a = _a, _b, _c, _d
elseif L5_env.color_mode == HSB then
r, g, b = HSVtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = _d
elseif L5_env.color_mode == HSL then
r, g, b = HSLtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
a = _d
end
end
-- Return normalized RGBA values (0-1 range)
return {r/L5_env.color_max[1], g/L5_env.color_max[2], b/L5_env.color_max[3], a/L5_env.color_max[4]}
end
function hexToRGB(hex)
hex = hex:gsub("#", "") -- Remove # if present
-- Check valid length
if #hex == 3 then
hex = hex:gsub("(.)", "%1%1") -- Convert 3 to 6-digit
elseif #hex ~= 6 then
return nil, "Invalid hex color format. Expected 3 or 6 characters."
end
-- Extract RGB components
local r = tonumber(hex:sub(1, 2), 16)
local g = tonumber(hex:sub(3, 4), 16)
local b = tonumber(hex:sub(5, 6), 16)
-- Check if conversion was successful
if not r or not g or not b then
return nil, "Invalid hex color format. Contains non-hex characters."
end
return r, g, b
end
function HSVtoRGB(h, s, v)
if s <= 0 then
return v, v, v
end
h = h * 6
local c = v * s
local x = c * (1 - math.abs((h % 2) - 1))
local m = v - c
local r, g, b = 0, 0, 0
if h < 1 then
r, g, b = c, x, 0
elseif h < 2 then
r, g, b = x, c, 0
elseif h < 3 then
r, g, b = 0, c, x
elseif h < 4 then
r, g, b = 0, x, c
elseif h < 5 then
r, g, b = x, 0, c
else
r, g, b = c, 0, x
end
return r + m, g + m, b + m
end
function HSLtoRGB(h, s, l)
if s <= 0 then
return l, l, l
end
h = h * 6
local c = (1 - math.abs(2 * l - 1)) * s
local x = c * (1 - math.abs((h % 2) - 1))
local m = l - c / 2
local r, g, b = 0, 0, 0
if h < 1 then
r, g, b = c, x, 0
elseif h < 2 then
r, g, b = x, c, 0
elseif h < 3 then
r, g, b = 0, c, x
elseif h < 4 then
r, g, b = 0, x, c
elseif h < 5 then
r, g, b = x, 0, c
else
r, g, b = c, 0, x
end
return r + m, g + m, b + m
end
function RGBtoHSL(r, g, b)
-- Normalize RGB values to 0-1 range
r = r / 255
g = g / 255
b = b / 255
local max = math.max(r, g, b)
local min = math.min(r, g, b)
local h, s, l
-- Calculate lightness
l = (max + min) / 2
if max == min then
-- Achromatic (no color)
h = 0
s = 0
else
local d = max - min
-- Calculate saturation
if l > 0.5 then
s = d / (2 - max - min)
else
s = d / (max + min)
end
-- Calculate hue
if max == r then
h = (g - b) / d + (g < b and 6 or 0)
elseif max == g then
h = (b - r) / d + 2
elseif max == b then
h = (r - g) / d + 4
end
h = h / 6
end
-- Convert to 0-360 for hue, 0-100 for saturation and lightness
return h * L5_env.color_max[1], s * L5_env.color_max[2], l * L5_env.color_max[3]
end
function save(filename)
love.graphics.captureScreenshot(function(imageData)
-- Generate filename
local finalFilename
if filename then
-- Check if filename ends with .png
if filename:match("%.png$") then
finalFilename = filename
else
-- Add .png extension
finalFilename = filename .. ".png"
end
else
-- Use default timestamp-based name
local timestamp = os.date("%Y%m%d_%H%M%S")
finalFilename = "screenshot_" .. timestamp .. ".png"
end
-- Encode to PNG
local pngData = imageData:encode("png")
-- Try to write to current directory first
local programDir = love.filesystem.getSource()
local targetPath = programDir .. "/" .. finalFilename
local file = io.open(targetPath, "wb")
if file then
file:write(pngData:getString())
file:close()
print("Screenshot saved to: " .. targetPath)
else
-- Fallback: use Love2d's save directory
print("Warning: Could not write to current directory, using save directory instead")
local success = love.filesystem.write(finalFilename, pngData)
if success then
local saveDir = love.filesystem.getSaveDirectory()
print("Screenshot saved to: " .. saveDir .. "/" .. finalFilename)
else
print("Error: Could not save screenshot")
end
end
end)
end
function describe(sceneDescription)
if not L5_env.described then
L5_env.originalPrint("CANVAS_DESCRIPTION: " .. sceneDescription)
io.flush() -- Ensure immediate output for screen readers
L5_env.described = true
end
end
function defaults()
-- constants
-- shapes
CORNER = "CORNER"
RADIUS = "RADIUS"
CORNERS = "CORNERS"
CENTER = "CENTER"
RADIANS = "RADIANS"
DEGREES = "DEGREES"
ROUND = "smooth"
SQUARE = "rough"
PROJECT = "project"
MITER = "miter"
BEVEL = "bevel"
NONE = "none"
CLOSE = "close"
-- typography
LEFT = "left"
RIGHT = "right"
CENTER = "center"
TOP = "top"
BOTTOM = "bottom"
BASELINE = "baseline"
WORD = "word"
CHAR = "char"
-- color
RGB = "rgb"
HSB = "hsb"
HSL = "hsl"
-- math
PI = math.pi
HALF_PI = math.pi/2
QUARTER_PI=math.pi/4
TWO_PI = 2 * math.pi
TAU = TWO_PI
PIE = "pie"
OPEN = "open"
CHORD = "closed"
-- filters (shaders)
GRAY = "gray"
THRESHOLD = "threshold"
INVERT = "invert"
POSTERIZE = "posterize"
BLUR = "blur"
ERODE = "erode"
DILATE = "dilate"
-- for applying texture wrapping
NORMAL = "NORMAL"
IMAGE = "IMAGE"
CLAMP = "clamp"
REPEAT = "repeat"
-- blend modes
BLEND = "blend"
ADD = "add"
MULTIPLY = "multiply"
SCREEN = "screen"
LIGHTEST = "lightest"
DARKEST = "darkest"
REPLACE = "replace"
-- system cursors
ARROW = "arrow"
IBEAM = "ibeam"
WAIT = "wait"
WAITARROW = "waitarrow"
CROSSHAIR = "crosshair"
SIZENWSE = "sizenwse"
SIZENESW = "sizenesw"
SIZEWE = "sizewe"
SIZENS = "sizens"
SIZEALL = "sizeall"
NO = "no"
HAND = "hand"
-- beginShape kinds
POINTS = "points"
LINES = "lines"
TRIANGLES = "triangles"
TRIANGLE_FAN = "fan"
TRIANGLE_STRIP = "strip"
-- global user vars - can be read by user but shouldn't be altered by user
key = "" --default, overriden with key presses detected in love.update(dt)
width = 800 --default, overridden with size() or fullscreen()
height = 600 --ditto
frameCount = 0
mouseIsPressed = false
mouseX=0
mouseY=0
keyIsPressed = false
pmouseX,pmouseY,movedX,movedY=0,0
mouseButton = nil
focused = true
pixels = {}
end
-- environment global variables not user-facing
function define_env_globals()
L5_env = L5_env or {} -- Initialize L5_env if it doesn't exist
L5_env.drawing = true
-- drawing mode state
L5_env.degree_mode = RADIANS --also: DEGREES
L5_env.rect_mode = CORNER --also: CORNERS, CENTER, RADIUS
L5_env.ellipse_mode = CENTER --also: CORNER, CORNERS, RADIUS
L5_env.image_mode = CORNER --also: CENTER, CORNERS
-- global color state
L5_env.fill_mode="fill" --also: "line"
L5_env.stroke_color = {0,0,0}
L5_env.currentTint = {1, 1, 1, 1} -- Default: no tint white
L5_env.color_max = {255,255,255,255}
L5_env.color_mode = RGB --also: HSB, HSL
-- global key state
L5_env.pressedKeys = {}
L5_env.keyWasPressed = false
L5_env.keyWasReleased = false
L5_env.keyWasTyped = false
L5_env.typedKey = nil
-- mouse state
L5_env.mouseWasMoved = false
L5_env.wasPressed = false
L5_env.wheelWasMoved = false
L5_env.wheelX = nil
L5_env.wheelY = nil
L5_env.pendingMouseClicked = nil
L5_env.pendingMouseReleased = nil
-- screen buffer state
L5_env.framerate = nil
L5_env.backBuffer = nil
L5_env.frontBuffer = nil
L5_env.clearscreen = false
L5_env.described = false
-- global video tracking for looping
L5_env.videos = {}
-- global font state
L5_env.fontPaths = {}
L5_env.currentFontPath = nil
L5_env.currentFontSize = 12
L5_env.textAlignX = LEFT
L5_env.textAlignY = BASELINE
L5_env.textWrap = WORD
-- filters (shaders)
L5_env.filterOn = false
L5_env.filter = nil
-- pixel array
L5_env.pixels = {}
L5_env.imageData = nil
L5_env.pixelsLoaded = false
-- custom shape drawing
L5_env.vertices = {}
L5_env.kind = nil
L5_env.shapeKinds = {[POINTS] = true, [LINES] = true, [TRIANGLES]=true, [TRIANGLE_FAN]=true, [TRIANGLE_STRIP]=true}
L5_env.mesh = love.graphics.newMesh(
{{"VertexPosition", "float", 2}},
4096, "triangles", "dynamic"
) -- reusable mesh for non-texture shapes
-- custom texture mesh
L5_env.currentTexture = nil
L5_env.useTexture = false
L5_env.textureMode=IMAGE -- NORMAL or IMAGE
L5_env.textureWrap=CLAMP -- wrap mode CLAMP or REPEAT
-- custom print output on screen
L5_env.printBuffer = {}
L5_env.defaultFont = love.graphics.getFont()
L5_env.printFont = L5_env.defaultFont
L5_env.showPrintBuffer = false
L5_env.printY = 5
L5_env.printLineHeight = L5_env.defaultFont:getHeight() + 2
-- Override print to also draw to screen
local originalPrint = print
L5_env.originalPrint = originalPrint
function print(...)
originalPrint(...) -- Still print to console
local text = ""
local args = {...}
for i = 1, #args do
if i > 1 then text = text .. "\t" end
text = text .. tostring(args[i])
end
table.insert(L5_env.printBuffer, text)
end
end
------------------ INIT SHADERS ---------------------
-- initialize shader default values
function initShaderDefaults()
-- Set default values for threshold shader
L5_filter.threshold:send("soft", 0.5)
L5_filter.threshold:send("threshold", 0.5)
-- Set default value for posterize
L5_filter.posterize:send("levels", 4.0)
-- Set default values for blur
if L5_filter.blurSupportsParameter then
L5_filter.blur_horizontal:send("blurRadius", 4.0)
L5_filter.blur_horizontal:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
L5_filter.blur_vertical:send("blurRadius", 4.0)
L5_filter.blur_vertical:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
elseif L5_filter.blur then
L5_filter.blur:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
end
-- Set default values for erode
L5_filter.erode:send("strength", 0.5)
L5_filter.erode:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
-- Set default values for dilate
L5_filter.dilate:send("strength", 1.0)
L5_filter.dilate:send("threshold", 0.1)
L5_filter.dilate:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
end
----------------------- INPUT -----------------------
function loadStrings(_file)
local lines = {}
for line in love.filesystem.lines(_file) do
table.insert(lines, line)
end
return lines
end
function loadTable(_file, _header)
local extension = _file:match("%.([^%.]+)$")
if extension == "csv" or extension == "tsv" then
local separator = (extension == "csv") and "," or "\t"
local pattern = (extension == "csv") and "[^,]+" or "[^\t]+"
local function splitLine(line)
local values = {}
for value in line:gmatch(pattern) do
if tonumber(value) then table.insert(values, tonumber(value))
elseif value == "true" then table.insert(values, true)
elseif value == "false" then table.insert(values, false)
else table.insert(values, value)
end
end
return values
end
local function loadDelimitedFile(filename)
local data = {}
local headers = {}
local first_line = true