-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackgroundPattern.lua
More file actions
869 lines (815 loc) · 38.3 KB
/
Copy pathbackgroundPattern.lua
File metadata and controls
869 lines (815 loc) · 38.3 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
local BackgroundPattern=GameObject:extend()
-- BackgroundPattern is an abstract class that needs implementation of:
-- 1. new(args): initialize the background pattern.
-- 2. update(dt): update the background pattern, called in love.update(dt)
-- 3. draw(): draw the background pattern, called in love.draw()
-- It will be bound to G and called in G.update and G.draw.
function BackgroundPattern:new(args)
self.notRespondToDrawAll=true -- Since background should be drawn before everything else, it should not be drawn by Object:drawAll but directly called in G.draw before Object:drawAll.
end
local Empty=BackgroundPattern:extend()
function Empty:new(args)
Empty.super.new(self,args)
end
BackgroundPattern.Empty=Empty
local sideLengthCache={}
-- calculate {the side length} and {radius of circumcircle} of a polygon with [sideNum] sides and each angle 2pi/[angleNum] in hyperbolic geometry. The result is cached.
local calculateSideLength=function(sideNum,angleNum)
if sideLengthCache[sideNum] and sideLengthCache[sideNum][angleNum] then
return sideLengthCache[sideNum][angleNum][1],sideLengthCache[sideNum][angleNum][2]
end
local centerToVertex=(math.sqrt((math.tan(math.pi/2-math.pi/angleNum)-math.tan(math.pi/sideNum))/(math.tan(math.pi/2-math.pi/angleNum)+math.tan(math.pi/sideNum)))) -- reference: https://www.malinc.se/noneuclidean/en/poincaretiling.php. sideNum->p, angleNum->q. actually this radius is on a poincare disk
local x1,y1=centerToVertex,0
local x2,y2=centerToVertex*math.cos(math.pi*2/sideNum),centerToVertex*math.sin(math.pi*2/sideNum) -- two points on a side, on a poincare disk
local d=2*math.distance(x1,y1,x2,y2)^2/(1-centerToVertex^2)^2
local sideLength= math.acosh(1+d)*Shape.curvature -- distance formula of poincare disk. reference: https://en.wikipedia.org/wiki/Poincar%C3%A9_disk_model
local circumcircleRadius=2*math.atanh(centerToVertex)*Shape.curvature -- distance formula when 1 point is at center.
sideLengthCache[sideNum]=sideLengthCache[sideNum] or {}
sideLengthCache[sideNum][angleNum]={sideLength,circumcircleRadius}
return sideLength,circumcircleRadius
end
BackgroundPattern.calculateSideLength=calculateSideLength
local function getCenterOfPolygonWithSide(x1,y1,x2,y2,sideNum,angleNum)
local direction=Shape.to(x2,y2,x1,y1)
local toCenterDirection=direction+math.pi*2/angleNum/2
local _,centerRadius=calculateSideLength(sideNum,angleNum)
local x,y=Shape.rThetaPos(x2,y2,centerRadius,toCenterDirection)
return x,y
end
BackgroundPattern.getCenterOfPolygonWithSide=getCenterOfPolygonWithSide
local function drawSideLine(x1,y1,x2,y2,color)
local colorref={love.graphics.getColor()}
love.graphics.setColor(color[1],color[2],color[3])
local num=math.ceil(math.min(25,Shape.distance(x1,y1,x2,y2)/10))
Shape.drawSegment(x1,y1,x2,y2,num)
-- love.graphics.setColor(0.35,0.15,0.8)
-- Shape.drawSegment(x1+1,y1+1,x2+1,y2+1,num)
love.graphics.setColor(colorref[1],colorref[2],colorref[3])
end
local function getSideFacePolygonVertices(x1,y1,x2,y2,sideNum,angleNum)
local vertices={}
-- since love.graphics.polygon draws straight sides, need to insert vertices on each hyperbolic side to smooth the curve
local function addVerticesOnSide(x1,y1,x2,y2,num)
local xCenter,radius=Shape.lineCenter(x1,y1,x2,y2)
local theta1,theta2=math.atan2(y1-Shape.axisY,x1-xCenter),math.atan2(y2-Shape.axisY,x2-xCenter)
for i=1,num do
local alpha=theta1+(theta2-theta1)*(i-1)/(num)
local x,y=xCenter+radius*math.cos(alpha),Shape.axisY+radius*math.sin(alpha)
vertices[#vertices+1]=x
vertices[#vertices+1]=y
end
end
local sideLength=calculateSideLength(sideNum,angleNum)
for sideIndex=1,sideNum do
local alpha1=math.pi*2/angleNum+Shape.to(x2,y2,x1,y1)
local num=math.clamp(Shape.distance(x1,y1,x2,y2)/10,3,15)
addVerticesOnSide(x1,y1,x2,y2,num)
x1,y1=x2,y2
x2,y2=Shape.rThetaPos(x2,y2,sideLength,alpha1)
end
return vertices
end
-- draw a face of a polygon. x1,y1,x2,y2: two points of the side. color: {r,g,b}. sideNum: how many sides do each polygon have. angleNum: how many sides are connected to each point.
-- bad: since a polygon is drawn once for each side, bigger sideNum causes a polygon to be drawn redundant sideNum/2 times. should be fixed with redundant removal in FixedTesselation (but not in MainMenuTesselation yet)
local function drawSideFace(vertices,color)
local colorref={love.graphics.getColor()}
love.graphics.setColor(color[1],color[2],color[3])
-- without triangulate love.graphics.polygon("fill",vertices) is buggy at some concave part
local ok, triangles = pcall(love.math.triangulate,vertices)
if not ok then
local stringVertices = ""
for i = 1, #vertices, 2 do
stringVertices = stringVertices .. "(" .. string.format("%.2f", vertices[i]) .. ", " .. string.format("%.2f", vertices[i + 1]) .. ") "
end
error("Error triangulating vertices: "..stringVertices..'\n'..triangles)
return
end
for i, triangle in ipairs(triangles) do
love.graphics.polygon("fill", triangle)
end
love.graphics.setColor(colorref[1],colorref[2],colorref[3],colorref[4])
end
--[[
params:
[point]: where pattern begins. [angle]: direction of first line. [sideNum]: how many sides do each polygon have. [angleNum]: how many sides are connected to each point. [iteCount]: input 0, currently only to check if it's first point. [centerPoint]: input nil. [toDrawNum]: how many lines to draw. If only draw sides, a few hundred to merely above 1000 is a reasonable number. If draw faces <400 is recommended.
returns:
adjacentPoints,angles,sidesTable. [adjacentPoints]: adjacent points to centerPoint (inputted point). [angles]: angles from each adjacent point to center point. I knew it's only used to update center point while keeping the pattern same, so angle should be to center point. [sidesTable]: all sides that are drawn. Each side is a table {point1,point2,index}. index is the index of the side in the sidesTable.
the way to find tesselation points is rather simple: from a point, extend angleNum lines, and only keep points that are farther away from the center point. This is because the closer points are already drawn by the previous lines. However when sideNum is odd (especially 3) some lines' two ends have same distance to the center point, so another check (polar angle) is added to prevent the side drawn 0 or 2 times.
pointsQueue is a queue that stores points that are not drawn yet, drawedPointsNum being the pointer. If drawedPointsNum is more than toDrawNum/angleNum, clear the queue to stop the tesselation. So that you shouldn't try getting points information from pointsQueue since it's always cleared when function ends.]]
local drawedPointsNum=0
local pointsQueue={}
-- key format: key:int = ceil(distance to centerPoint)*1000+floor(angle*1000)
local visitedPoints={}
local function tesselation(point,angle,sideNum,angleNum,iteCount, centerPoint, toDrawNum, sidesTable, skipInRangeLimit)
centerPoint=centerPoint or point
if iteCount==0 then
drawedPointsNum=0
pointsQueue={}
visitedPoints={}
end
local iteCount=(iteCount or 0)+1
local adjacentPoints={}
local r=calculateSideLength(sideNum,angleNum)
local begin=1
local en=angleNum--iteCount>1 and angleNum-2 or angleNum
sidesTable=sidesTable or {}
drawedPointsNum=drawedPointsNum+1
local distance0=Shape.distance(point.x,point.y,centerPoint.x,centerPoint.y)
for i=begin,en do
if not skipInRangeLimit and not math.inRange(point.x,point.y,-400,1200,-5,4000) then
break
end
local alpha=angle+math.pi*2/angleNum*(i)
local ret={Shape.rThetaPos(point.x,point.y,r,alpha)}
local newpoint={x=ret[1],y=ret[2]}
local distance=Shape.distance(newpoint.x,newpoint.y,centerPoint.x,centerPoint.y)
-- these two ifs fully exclude duplicate sides, but points still can duplicate (two points connect to same further point)
local centerAngle=Shape.toObj(centerPoint,newpoint)
if distance<distance0-Shape.EPS*10 then
goto continue
elseif distance<distance0+Shape.EPS*10 then -- same distance on both ends: check angle
if Shape.toObj(centerPoint,point)>centerAngle then
goto continue
end
end
local centerDistance=Shape.distanceObj(centerPoint,newpoint)
local key=math.ceil(centerDistance)*1000+math.floor(centerAngle*1000)
if not visitedPoints[key] then -- skip redundant new point
adjacentPoints[#adjacentPoints+1]=newpoint
visitedPoints[key]=true
end
local len=#sidesTable
sidesTable[len+1]={point,newpoint,index=len+1}
if len+1>=toDrawNum then
break
end
::continue::
end
local angles={}
for i=1,#adjacentPoints do
local newpoint=adjacentPoints[i]
local newangle=Shape.to(newpoint.x,newpoint.y,point.x,point.y)
table.insert(angles,newangle)
pointsQueue[#pointsQueue+1]={newpoint,newangle,iteCount}
-- tesselation(newpoint,newangle,sideNum,angleNum,iteCount,color,i==1,centerPoint)
end
if #sidesTable<toDrawNum and pointsQueue[drawedPointsNum]then
tesselation(pointsQueue[drawedPointsNum][1],pointsQueue[drawedPointsNum][2],sideNum,angleNum,pointsQueue[drawedPointsNum][3],centerPoint,toDrawNum,sidesTable, skipInRangeLimit)
else
pointsQueue={}
end
return adjacentPoints,angles,sidesTable
end
BackgroundPattern.tesselation=tesselation
-- this function isn't used in main menu cuz sometimes random parameters gets laggy
local function randomSideNumAndAngleNum()
math.randomseed(os.time())
local sideNum=math.random(3,8)
local angleNum=math.random(3,8)
local factor=(sideNum-2)*(angleNum-2)
if factor<=4 or factor>20 then -- factor must > 4 to become a hyperbolic tesselation. the 20 limit is to prevent too scattered tesselation
return randomSideNumAndAngleNum()
end
return sideNum,angleNum
end
-- local shader=love.graphics.newShader('shaders/triangleTextureDrawerVertex.glsl','shaders/triangleTextureDrawer.glsl')
local shader=ShaderScan:load_shader('shaders/flipTessellation.glsl')
-- a tesselation that moves and rotates. It's used in main menu.
local MainMenuTesselation=BackgroundPattern:extend()
function MainMenuTesselation:new(args)
MainMenuTesselation.super.new(self,args)
-- self.name='Tesselation'
args=args or {}
self.p=args.p or 2
self.q=args.q or 5
self.r=args.r or 5
self.shader=shader
self.uvPoses={{265/800,376/600},{534/800,376/600},{399/800,140/600}}
-- self.uvPoses={{0.5-3^0.5/4,1},{0.5+3^0.5/4,1},{0.5,0}}
self.frame=0
end
function MainMenuTesselation:update(dt)
self.frame=self.frame+1
end
function MainMenuTesselation:randomize()
local rand=math.random(1,3)
self.uvPoses[rand],self.uvPoses[rand%3+1]=self.uvPoses[rand%3+1],self.uvPoses[rand] -- swap 2 random uvPoses
local tried=0
while tried<20 do
local p=math.random(3,14)/2
local q=math.random(3,14)/2
local r=math.random(3,14)/2
if 1/p+1/q+1/r<1 then
self.p=p
self.q=q
self.r=r
return
end
tried=tried+1
end
end
-- local testImage = love.graphics.newImage( "assets/test.png" )
-- testImage:setWrap("repeat", "repeat") -- set texture to repeat so that it can be used in shader
function MainMenuTesselation:draw()
local ay=Shape.axisY
Shape.axisY=-2
local width=love.graphics.getLineWidth()
love.graphics.setLineWidth(10)
love.graphics.setShader(self.shader)
local uvPoses=self.uvPoses
local t=self.frame/151
local x,y=400+50*math.sin(t),300+220*math.cos(t)
local V0,V1,V2=Shape.schwarzTriangleVertices(self.p,self.q,self.r,{x,y},self.frame/131)
-- local V0 = {400, 300}
-- local V1 = {500, 300}
-- local V2 = {400, 400}
shader:send("V0", V0)
shader:send("V1", V1)
shader:send("V2", V2)
shader:send("tex_uv_V0", uvPoses[1])
shader:send("tex_uv_V1", uvPoses[2])
shader:send("tex_uv_V2", uvPoses[3])
shader:send("shape_axis_y", Shape.axisY)
-- love.graphics.draw(testImage, 0,0)
love.graphics.draw(Asset.backgroundImage, 0,0)
love.graphics.setShader()
-- love.graphics.circle("fill",V0[1],V0[2],25)
-- love.graphics.circle("fill",V1[1],V1[2],25)
-- love.graphics.circle("fill",V2[1],V2[2],25)
love.graphics.setLineWidth(width)
Shape.axisY=ay
end
BackgroundPattern.MainMenuTesselation=MainMenuTesselation
local Square=BackgroundPattern:extend()
function Square:new(args)
Square.super.new(self,args)
args=args or {}
self.radius=args.radius or 10
self.radiusMax=args.radiusMax or 70
self.angle=args.angle or math.pi/3
self.speed=args.speed or 0.3
end
function Square:update(dt)
self.radius=(self.radius+self.speed)%self.radiusMax
self.angle=self.angle+self.speed/20
end
function Square:drawOne(r,angle)
local xc,yc=400,300
local r2=math.cosh(r/10)
local points={}
for i=1,4 do
local alpha=angle+math.pi*2/4*(i-1)
local ret={xc+r2*math.cos(alpha),yc+r2*math.sin(alpha)}
local newpoint={x=ret[1],y=ret[2]}
points[#points+1]=newpoint
end
local ratio=r/self.radiusMax
love.graphics.setColor(0.5,0.7,0.9,ratio*0.3)
for i=1,#points do
local newpoint=points[i]
love.graphics.line(newpoint.x,newpoint.y,points[i%#points+1].x,points[i%#points+1].y)
end
end
function Square:draw()
local colorref={love.graphics.getColor()}
love.graphics.setColor(0,0,0)
-- love.graphics.rectangle('fill',0,0,800,600)
local width=love.graphics.getLineWidth()
love.graphics.setLineWidth(10)
self:drawOne(self.radius,self.angle)
self:drawOne((self.radius+35)%self.radiusMax,self.angle)
love.graphics.setLineWidth(width)
love.graphics.setColor(colorref[1],colorref[2],colorref[3])
end
BackgroundPattern.Square=Square
local FixedTesselation=BackgroundPattern:extend()
-- FixedTesselation is a tesselation that calculate all sides upon new() and doesn't update. Used in normal levels.
function FixedTesselation:new(args)
FixedTesselation.super.new(self,args)
args=args or {}
self.sideNum=args.sideNum or 4
self.angleNum=args.angleNum or 5
self.centerPoint=args.centerPoint or {x=400,y=300}
self.faceColor=args.faceColor or {0.1,0.1,0.1}
self.sideColor=args.sideColor or {0.15,0.1,0.2}
self.overallColorScale=args.overallColorScale or 1
self.toDrawNum=args.toDrawNum or 40
self.angle=args.angle or 0
self.adjacentPoints,self.angles,self.sidesTable=tesselation(self.centerPoint,self.angle,self.sideNum,self.angleNum,0,nil,self.toDrawNum,nil,true) -- self.adjacentPoints and self.angles are not used in FixedTesselation but in FollowingTesselation so don't remove them
self.hashSet={}
self.redundantCount=0
for i=1,#self.sidesTable do
local centerPos={getCenterOfPolygonWithSide(self.sidesTable[i][1].x,self.sidesTable[i][1].y,self.sidesTable[i][2].x,self.sidesTable[i][2].y,self.sideNum,self.angleNum)}
local hashValue=Hash64(''..(math.floor(centerPos[1])*8)..(math.floor(centerPos[2])*8).."loool")
local color={hashValue[3]/256,hashValue[1]/256,hashValue[2]/256}
local hashValueInt=hashValue[1]+hashValue[2]*256+hashValue[3]*256*256+hashValue[4]*256*256*256
-- print(i,centerPos[1],centerPos[2],hashValueInt)
if self.hashSet[hashValueInt] then
self.sidesTable[i].redundantFace=true
self.redundantCount=self.redundantCount+1
end
self.hashSet[hashValueInt]=true
self.sidesTable[i].color=color
self.sidesTable[i][1]=copy_table(self.sidesTable[i][1]) -- without copying player's hyperbolic rotate can't retrieve the original position (i forgor why tho) and will crash after few frames
self.sidesTable[i][2]=copy_table(self.sidesTable[i][2])
end
-- print(self.redundantCount..' sides: '..#self.sidesTable)
self.sideLength=calculateSideLength(self.sideNum,self.angleNum)
end
function FixedTesselation:update(dt)
-- fixed tesselation doesn't need to update
end
function FixedTesselation:draw()
love.graphics.setColor(0,0,0,1)
love.graphics.rectangle('fill',0,0,800,600)
love.graphics.setColor(1,1,1,1)
local shader=love.graphics.getShader()
Asset.setHyperbolicRotateShader() -- contains G.UseHypRotShader check
local ay=Shape.axisY
-- Shape.axisY=-10
local width=love.graphics.getLineWidth()
love.graphics.setLineWidth(10)
local overallColorScale=self.overallColorScale
local faceColorCoeff=self.faceColor
if not self.dontDrawFaces then
for i=1,#self.sidesTable do
if self.sidesTable[i].redundantFace then
goto continue
end
local color=self.sidesTable[i].color
color={color[1]*faceColorCoeff[1],color[2]*faceColorCoeff[2],color[3]*faceColorCoeff[3]}
color={color[1]*overallColorScale,color[2]*overallColorScale,color[3]*overallColorScale}
local vertices=getSideFacePolygonVertices(self.sidesTable[i][1].x,self.sidesTable[i][1].y,self.sidesTable[i][2].x,self.sidesTable[i][2].y,self.sideNum,self.angleNum)
drawSideFace(vertices,color)
::continue::
end
end
local color=self.sideColor
color={color[1]*overallColorScale,color[2]*overallColorScale,color[3]*overallColorScale}
if not self.dontDrawSides then
for i=1,#self.sidesTable do
drawSideLine(self.sidesTable[i][1].x,self.sidesTable[i][1].y,self.sidesTable[i][2].x,self.sidesTable[i][2].y,color)
end
end
love.graphics.setLineWidth(width)
Shape.axisY=ay
love.graphics.setShader(shader)
end
BackgroundPattern.FixedTesselation=FixedTesselation
local FollowingTesselation=BackgroundPattern:extend()
-- FollowingTesselation is a seemingly fixed tesselation that changes its center point to follow a target object (usually player), so that not need to draw many sides, but still filling the screen in player's view.
-- use FixedTesselation to generate the initial tesselation, and whenever the center point leaves the range, calculate the new center point and use a new FixedTesselation to update the sides.
function FollowingTesselation:new(args)
FollowingTesselation.super.new(self,args)
args=args or {}
args.sideNum=args.sideNum or 4
args.angleNum=args.angleNum or 5
args.faceColor=args.faceColor or {0.1,0.1,0.1}
args.sideColor=args.sideColor or {0.15,0.1,0.2}
args.overallColorScale=args.overallColorScale or 1
args.toDrawNum=args.toDrawNum or 40
args.centerPoint=args.centerPoint or {x=400,y=300}
args.target=args.target or Player.objects[1]
args.updateRange=args.updateRange or 50
for key, value in pairs(args) do
self[key]=value
end
self.sidesTable=nil
self:updateSides()
end
function FollowingTesselation:updateSides()
local initialTesselation=FixedTesselation(self)
self.adjacentPoints,self.angles,self.sidesTable=initialTesselation.adjacentPoints,initialTesselation.angles,initialTesselation.sidesTable
initialTesselation:remove()
end
-- update the tesselation when the target is out of range
function FollowingTesselation:update(dt)
local target=self.target
if not target or target.removed or target:is(Player) and target~=Player.objects[1] then
-- look i really dont understand why after pressing r to restart or restart in game end screen, there is only one object in Player.objects, but the target is still the old one, and target.removed is nil. so i have to check if target is Player.objects[1]
if Player.objects[1] then
self.target=Player.objects[1]
target=Player.objects[1]
else
return
end
end
-- print(self.target.x..', '..self.target.y..', '..(#Player.objects)..', '..(self.target==Player.objects[1]and 't' or 'f'),400,300)
local centerPoint=self.centerPoint
local updateRange=self.updateRange
local currentDistance=Shape.distance(target.x,target.y,centerPoint.x,centerPoint.y)
if currentDistance>updateRange then -- should update the tesselation
local closestPointIndex=1
local closestDistance=Shape.distance(target.x,target.y,self.adjacentPoints[1].x,self.adjacentPoints[1].y)
for i=2,#self.adjacentPoints do -- first check all adjacentPoints and see if any is closer to the target
local distance=Shape.distance(target.x,target.y,self.adjacentPoints[i].x,self.adjacentPoints[i].y)
if distance<closestDistance then
closestDistance=distance
closestPointIndex=i
end
end
if closestDistance>currentDistance then -- if the closest point is still farther than the center point, don't update
return
end
local newCenterPoint=self.adjacentPoints[closestPointIndex]
local newAngle=self.angles[closestPointIndex]
self.centerPoint=newCenterPoint
self.angle=newAngle
self:updateSides()
end
end
function FollowingTesselation:draw()
FixedTesselation.draw(self)
end
BackgroundPattern.FollowingTesselation=FollowingTesselation
local Pendulum=BackgroundPattern:extend()
-- euclid pendulum clock (for scene 6-3)
function Pendulum:new(args)
Pendulum.super.new(self,args)
args=args or {}
self.radius=args.radius or 500
self.amplitude=args.amplitude or 0.1
self.period=args.period or 240
self.centerPoint=args.centerPoint or {x=400,y=-200}
self.point={x=self.centerPoint.x,y=self.centerPoint.y}
self.colorRatio=args.colorRatio or 0 -- gradually increase this parameter to make pendulum looks like gradually appear
self.frame=0
self.noZoom=true
end
function Pendulum:update(dt)
self.angle=math.sin(self.frame/self.period*math.pi*2)*self.amplitude+math.pi/2
local x,y=math.rThetaPos(self.centerPoint.x,self.centerPoint.y,self.radius,self.angle)
self.point={x=x,y=y}
self.frame=self.frame+1
end
local clockImage = love.graphics.newImage( "assets/pics/clock.png" )
local clockWidth, clockHeight = clockImage:getDimensions()
local clockQuad = love.graphics.newQuad(0, 0, clockWidth, clockHeight, clockWidth, clockHeight)
function Pendulum:draw() -- ugh direct drawing looks kinda cringe. maybe find an image for this later. (done but this is still kept)
local colorref={love.graphics.getColor()}
local width=love.graphics.getLineWidth()
love.graphics.setLineWidth(10)
local x1,y1=self.centerPoint.x,self.centerPoint.y
local x2,y2=self.point.x,self.point.y
local function colorMult(colorTable)
return {colorTable[1]*self.colorRatio,colorTable[2]*self.colorRatio,colorTable[3]*self.colorRatio}
end
-- the background
love.graphics.setColor(0,0,0,1)
love.graphics.rectangle('fill',0,0,800,600) -- black background, without it part of border won't display due to background has alpha=0, and "add" blend mode doesn't change alpha
local outerBGColor=colorMult{0.25,0.10,0.04}
love.graphics.setColor(outerBGColor)
love.graphics.rectangle('fill',250,0,300,400) -- outer rectangle
love.graphics.rectangle('fill',230,400,340,30) -- base of clock
love.graphics.setColor(colorMult{0.15,0.07,0.02})
love.graphics.rectangle('fill',300,100,200,250) -- inner rectangle
love.graphics.setColor(colorMult{0.10,0.05,0.02})
love.graphics.rectangle('line',300,100,200,250) -- darker sides of inner rectangle
love.graphics.rectangle('fill',250,390,300,10) -- shadow of base
love.graphics.rectangle('fill',230,430,340,10) -- shadow of base
-- the pendulum
love.graphics.setColor(colorMult{0.45,0.45,0.38})
love.graphics.line(x1,y1,x2,y2)
love.graphics.setColor(colorMult{0.25,0.25,0.08})
love.graphics.circle("fill",x2,y2,40)
-- the upper part of background should block the view of the pendulum
love.graphics.setColor(outerBGColor)
love.graphics.rectangle('fill',250,0,300,95)
love.graphics.setColor(colorMult{0.5,.5,.5})
love.graphics.draw(clockImage, clockQuad, 150,0,0,1,1)
love.graphics.setLineWidth(width)
love.graphics.setColor(colorref[1],colorref[2],colorref[3],colorref[4])
end
BackgroundPattern.Pendulum=Pendulum
-- love2d draws a white rectangle then shader draws pattern.
local Shader=BackgroundPattern:extend()
---@class ShaderBackground
---@class love.Shader
---@class ShaderBackgroundArgs
---@field shader love.Shader the shader to use for drawing the background
---@field paramSendFunction fun(self:ShaderBackground,shader:love.Shader):nil a function to send parameters to the shader, called in Shader:draw()
---@param args ShaderBackgroundArgs
function Shader:new(args)
Shader.super.new(self,args)
args=args or {}
self.shader=args.shader
self.frame=0
self.paramSendFunction=args.paramSendFunction or function(self,shader) end
self.color={1,1,1}
self.lightColor={1,1,1}
self.darkColor={0.5,0.5,0.5}
self.autoDark=false -- if true, color will be lerped to darkColor when not G.preWin (enemy exists, during spellcard) (for very bright shaders)
end
function Shader:update(dt)
self.frame=self.frame+1
if self.autoDark then
local ratio=0.02
if G.preWin then
self.color={self.color[1]*(1-ratio)+self.lightColor[1]*ratio,self.color[2]*(1-ratio)+self.lightColor[2]*ratio,self.color[3]*(1-ratio)+self.lightColor[3]*ratio}
else
self.color={self.color[1]*(1-ratio)+self.darkColor[1]*ratio,self.color[2]*(1-ratio)+self.darkColor[2]*ratio,self.color[3]*(1-ratio)+self.darkColor[3]*ratio}
end
end
end
function Shader:draw()
local colorref={love.graphics.getColor()}
love.graphics.setColor(self.color[1],self.color[2],self.color[3])
-- love.graphics.rectangle('fill',0,0,800,600)
love.graphics.setShader(self.shader)
self:paramSendFunction(self.shader) -- send parameters to shader
local translateX,translateY,scale=G:followModeTransform(true)
love.graphics.rectangle('fill',-translateX/scale,-translateY/scale,800/scale,600/scale)
love.graphics.setShader()
love.graphics.setColor(colorref[1],colorref[2],colorref[3],colorref[4])
end
BackgroundPattern.Shader=Shader
local plasmaGlobe=Shader:extend()
function plasmaGlobe:new(args)
plasmaGlobe.super.new(self,args)
args=args or {}
self.shader=love.graphics.newShader('shaders/backgrounds/plasmaGlobe.glsl')
-- Create iChannel0 (noise texture)
local noiseSize = 256
local imageData = love.image.newImageData(noiseSize, noiseSize)
-- The shader samples .x and .yx from iChannel0, so we need at least two noise channels (R and G)
imageData:mapPixel(function(x, y, r, g, b, a)
local val1 = math.random() -- For .x component
local val2 = math.random() -- For .y component (used in .yx)
return val1, val2, 0, 1 -- Store in R and G channels
end)
local iChannel0 = love.graphics.newImage(imageData)
iChannel0:setFilter("linear", "linear") -- ShaderToy textures usually have linear filtering
iChannel0:setWrap("repeat", "repeat") -- Important for noise sampling
self.cameraAngleX = math.pi/4 -- Corresponds to yaw (left/right)
self.cameraAngleY = 0.0 -- Corresponds to pitch (up/down)
self.rotationSpeed = math.pi / 1.5 -- Radians per second, adjust for sensitivity
self.paramSendFunction=function(self,shader)
shader:send("iTime", love.timer.getTime())
shader:send("iResolution", {WINDOW_WIDTH, WINDOW_HEIGHT})
shader:send("u_camAngleX", self.cameraAngleX)
shader:send("u_camAngleY", self.cameraAngleY)
shader:send("iChannel0", iChannel0)
end
end
plasmaGlobe.update=function(self,dt)
-- dt=0.005
self.frame=self.frame+1
-- if isDown("left") then
-- self.cameraAngleX = self.cameraAngleX - self.rotationSpeed * dt
-- end
-- if isDown("right") then
-- self.cameraAngleX = self.cameraAngleX + self.rotationSpeed * dt
-- end
-- if isDown("up") then
-- self.cameraAngleY = self.cameraAngleY + self.rotationSpeed * dt
-- end
-- if isDown("down") then
-- self.cameraAngleY = self.cameraAngleY - self.rotationSpeed * dt
-- end
end
-- testShader.draw=function(self)
-- Shader.draw(self)
-- love.graphics.print(''..self.cameraAngleX..', '..self.cameraAngleY, 310, 100)
-- end
BackgroundPattern.PlasmaGlobe=plasmaGlobe
local fractalShader=love.graphics.newShader('shaders/backgrounds/fractal.glsl')
local Fractal=Shader:extend()
function Fractal:new(args)
Fractal.super.new(self,args)
args=args or {}
self.shader=fractalShader
local randomOffset=math.random(0,1000)
self.paramSendFunction=function(self,shader)
shader:send("iTime", love.timer.getTime()/3+randomOffset)
shader:send("iResolution", {WINDOW_WIDTH, WINDOW_HEIGHT})
end
end
BackgroundPattern.Fractal=Fractal
local build_lorentz_mat4=require('import.H3math').build_lorentz_mat4
-- tessellation on H^2 is calculated similar to main menu tessellation: calculate schwarz triangle vertices and send this fundamental triangle to shader. after flip, flip count and barycenter coordinates are used to calculate color and height.
-- due to high computation cost, this could only fit ending / credits
local H3TerrainShader=ShaderScan:load_shader('shaders/backgrounds/H3Terrain2.glsl')
local H3Terrain=Shader:extend()
function H3Terrain:new()
H3Terrain.super.new(self)
self.autoDark=true
self.shader=H3TerrainShader
self.cam_translation={0,1.5,0.5}
self.cam_pitch=-0.9
self.cam_yaw=0
self.cam_roll=0
self.camMoveRange={0.3,0.0}
self.camMoveSpeed=0.2
self.p,self.q,self.r=3,6,6
local V0,V1,V2=Shape.schwarzTriangleVertices(self.p,self.q,self.r,{0,Shape.axisY+100-1},0)
local length01=Shape.distance(V0[1],V0[2],V1[1],V1[2])
local length02=Shape.distance(V0[1],V0[2],V2[1],V2[2])
local length12=Shape.distance(V1[1],V1[2],V2[1],V2[2])
self.tesseLoopLength=length01*2 -- based on pqr, the loop length has many possibilities. other possible values include (L01+L02+L12)*2, (L01+L02)*2
self.tesseDistance=0.01 -- distance of tessellation moved along the path. not camera
self.tesseMoveSpeed=0.3
local autoMove=false
self.paramSendFunction=function(self,shader)
local l=length01-self.tesseDistance
local x,y,dir=Shape.rThetaPosT(0,Shape.axisY+1,l,0)
-- dir=dir+(l>0 and math.pi or 0)
local V0,V1,V2=Shape.schwarzTriangleVertices(self.p,self.q,self.r,{x,y},dir)
local axisY=Shape.axisY
V0[2]=V0[2]-axisY
V1[2]=V1[2]-axisY
V2[2]=V2[2]-axisY
shader:send("V0", V0)
shader:send("V1", V1)
shader:send("V2", V2)
shader:send("time", self.frame/60*1.8)
local trans=self.cam_translation or {0,0,0}
if autoMove then
trans[3]=math.cos(self.frame/200)*-0.5+1.5
end
shader:send("cam_translation", trans)
local pitch=self.cam_pitch or 0
if autoMove then
pitch=math.cos(self.frame/200)*-0.3-0.3
end
shader:send("cam_pitch", pitch)
shader:send("cam_yaw", self.cam_yaw or 0)
local roll=self.cam_roll or 0
shader:send("cam_roll", roll)
end
end
H3Terrain.update=function(self,dt)
H3Terrain.super.update(self,dt)
local xRange,yRange=self.camMoveRange[1],self.camMoveRange[2]
if not self.camMoveCenter then
self.camMoveCenter={self.cam_translation[1],self.cam_translation[2]}
end
local xCenter,yCenter=self.camMoveCenter[1],self.camMoveCenter[2]
local xyStep=self.camMoveSpeed*dt
self.tesseDistance=(self.tesseDistance+self.tesseMoveSpeed/(1+self.cam_translation[1]^2))%(self.tesseLoopLength)
self.frame=self.frame+1
local keyIsDown=love.keyboard.isDown
if keyIsDown("n") then
self.cam_pitch = self.cam_pitch - dt
end
if keyIsDown("m") then
self.cam_pitch = self.cam_pitch + dt
end
if keyIsDown("h") then
self.cam_yaw = self.cam_yaw - dt
end
if keyIsDown("j") then
self.cam_yaw = self.cam_yaw + dt
end
if keyIsDown("y") then
self.cam_roll = self.cam_roll - dt
end
if keyIsDown("u") then
self.cam_roll = self.cam_roll + dt
end
if keyIsDown("i") then
self.cam_translation[3] = self.cam_translation[3] + dt
end
if keyIsDown("k") then
self.cam_translation[3] = self.cam_translation[3] - dt
end
if Player.objects[1] then
keyIsDown=Player.objects[1].keyIsDown -- nmhjyuik aren't recorded in player, so these keys use love.keyboard.isDown. arrow keys use player to restore in replay
end
if keyIsDown("right") then
self.cam_translation[1] = math.clamp(self.cam_translation[1] + xyStep,-xRange+xCenter,xRange+xCenter)
end
if keyIsDown("left") then
self.cam_translation[1] = math.clamp(self.cam_translation[1] - xyStep,-xRange+xCenter,xRange+xCenter)
end
if keyIsDown("up") then
self.cam_translation[2] = math.clamp(self.cam_translation[2] - xyStep,-yRange+yCenter,yRange+yCenter)
end
if keyIsDown("down") then
self.cam_translation[2] = math.clamp(self.cam_translation[2] + xyStep,-yRange+yCenter,yRange+yCenter)
end
end
BackgroundPattern.H3Terrain=H3Terrain
local dreamWorldShader=ShaderScan:load_shader('shaders/backgrounds/dreamWorld.glsl')
local DreamWorld=H3Terrain:extend()
function DreamWorld:new(args)
DreamWorld.super.new(self,args)
self.shader=dreamWorldShader
self.cam_translation={0.1,0,0.35}
self.cam_pitch=math.pi*-0.2
self.p,self.q,self.r=5,2,4
local V0,V1,V2=Shape.schwarzTriangleVertices(self.p,self.q,self.r,{0,-1},0)
local length01=Shape.distance(V0[1],V0[2],V1[1],V1[2])
local length02=Shape.distance(V0[1],V0[2],V2[1],V2[2])
local length12=Shape.distance(V1[1],V1[2],V2[1],V2[2])
self.tesseLoopLength=(length01+length02)*2
self.debug=false
if self.debug then -- a debug mode where pattern doesnt move and camera move range is increased
self.camMoveRange={-1,1}
self.camMoveSpeed=0.5
self.tesseMoveSpeed=0
end
end
BackgroundPattern.DreamWorld=DreamWorld
local honeycombShader=ShaderScan:load_shader('shaders/backgrounds/honeycomb.glsl')
local Honeycomb=H3Terrain:extend()
function Honeycomb:new(args)
Honeycomb.super.new(self,args)
self.shader=honeycombShader
self.inverse=args and args.inverse or false
if not self.inverse then
self.darkColor={0.3,0.3,0.3}
end
self.cam_translation={0.0001,0.4,0.3} -- when inverse, y=0.4 to avoid moving into ball at origin
self.cam_pitch=self.inverse and -math.pi/2 or 0
self.cam_yaw=-math.pi/2
self.camMoveRange={0.45,0.0}
self.autoMove=true
self.autoForwardSpeed=0.15
self.autoForwardWrap=1.06 -- currently should be distance from center to center of a side.
self.autoForwardValue=self.cam_translation[3]
self.manualForwardOffset=0.0
self.manualForwardLimit=0.3
self.reflectCount=0
self.paramSendFunction=function(self,shader)
shader:send("time", self.frame/60)
local trans=self.cam_translation
local pitch,yaw,roll=self.cam_pitch,self.cam_yaw,self.cam_roll
local changed=self.inverse and {trans[3], trans[1], trans[2]} or {trans[3], trans[2], trans[1]} -- auto move component must be first. rest two order is to let fixed component moving away from ball at origin or edge
local mat4=build_lorentz_mat4(pitch, yaw, roll, changed)
shader:send("cam_mat4", mat4)
shader:send("inverse",self.inverse)
shader:send("SHELL_RATIO",self.inverse and 2 or 0.5)
shader:send("reflect_count",self.reflectCount)
end
end
function Honeycomb:update(dt)
Honeycomb.super.update(self,dt)
if not self.autoMove then
return
end
local wrap = self.autoForwardWrap
local manualOffset = math.clamp(self.cam_translation[3] - self.autoForwardValue, -self.manualForwardLimit, self.manualForwardLimit)
self.manualForwardOffset = manualOffset
self.autoForwardValue = self.autoForwardValue + (self.autoForwardSpeed or 0.0) * dt
local span = wrap * 2.0
if self.autoForwardValue > wrap then
self.autoForwardValue = self.autoForwardValue - span
self.reflectCount = self.reflectCount + 1
elseif self.autoForwardValue < -wrap then
self.autoForwardValue = self.autoForwardValue + span
self.reflectCount = self.reflectCount + 1
end
self.cam_translation[3] = self.autoForwardValue + self.manualForwardOffset
end
BackgroundPattern.Honeycomb=Honeycomb
local stageShader=ShaderScan:load_shader('shaders/backgrounds/stage.glsl')
local Stage=H3Terrain:extend()
function Stage:new(args)
Stage.super.new(self,args)
self.cam_translation={0,1.2,0.8}
self.shader=stageShader
self.p,self.q,self.r=4,4,4
self.cam_pitch=-0.9
self.camMoveRange={0.5,0.2}
self.camMoveSpeed=0.5
self.holeSize = args and args.holeSize or 0.0
self.holeIsHorizon = args and args.holeIsHorizon or false
local axisY=Shape.axisY
local V0,V1,V2=Shape.schwarzTriangleVertices(self.p,self.q,self.r,{1,axisY+1},0)
V0[2]=V0[2]-axisY
V1[2]=V1[2]-axisY
V2[2]=V2[2]-axisY
-- print('triangle: '..V0[1]..','..V0[2]..' ; '..V1[1]..','..V1[2]..' ; '..V2[1]..','..V2[2])
self.paramSendFunction=function(self,shader)
shader:send("time", self.frame/60)
local trans=self.cam_translation
local pitch,yaw,roll=self.cam_pitch,self.cam_yaw,self.cam_roll
local mat4=build_lorentz_mat4(pitch, yaw, roll, trans)
shader:send("cam_mat4", mat4)
shader:send("V0", V0)
shader:send("V1", V1)
shader:send("V2", V2)
-- levelData create this pattern should set holeSize as integer from 1 to 4. ending screen uses 4. coefficient is set here.
shader:send("holeSize", self.holeSize*0.3)
shader:send("holeIsHorizon", self.holeIsHorizon)
end
end
BackgroundPattern.Stage=Stage
local youkaiMountainShader=ShaderScan:load_shader('shaders/backgrounds/youkaiMountain.glsl')
local YoukaiMountain=H3Terrain:extend()
function YoukaiMountain:new(args)
YoukaiMountain.super.new(self,args)
self.cam_translation={0,0,0.4}
self.camMoveRange={0.6,0.8}
self.cam_pitch=-0.9
self.shader=youkaiMountainShader
self.paramSendFunction=function(self,shader)
shader:send("time", self.frame/60)
local trans=self.cam_translation
local pitch,yaw,roll=self.cam_pitch,self.cam_yaw,self.cam_roll
local mat4=build_lorentz_mat4(pitch, yaw, roll, trans)
shader:send("cam_mat4", mat4)
end
end
BackgroundPattern.YoukaiMountain=YoukaiMountain
return BackgroundPattern