├── colorData.lua ├── conf.lua ├── locations.txt ├── main.lua ├── readme.md ├── screenshots ├── 1.png ├── 2.png ├── 3.png └── 4.png └── shader.frag /colorData.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- color maps 3 | -------------------------------------------------------------------------------- 4 | -- these have a max length of 300, which can be extended in shader externs 5 | -- these were generated using an HCL picker here: 6 | -- http://tristen.ca/hcl-picker/#/hlc/6/1/21313E/EFEE69 7 | -- I generated some (what I thought were) nice gradients, used the copy 8 | -- utility on the website, and then wrote a small program to convert the copied 9 | -- format into something that lua could use 10 | colors= { 11 | { 12 | backgroundColor = {6,41,61}, 13 | colorMap = { 14 | {207, 240, 158}, 15 | {168, 219, 168}, 16 | {121, 189, 154}, 17 | { 59, 134, 134}, 18 | { 11, 72, 107}, 19 | { 59, 134, 134}, 20 | {121, 189, 154}, 21 | {121, 189, 154}, 22 | {168, 219, 168}, 23 | {207, 240, 158}, 24 | } 25 | }, 26 | { 27 | backgroundColor = {6,41,61}, 28 | colorMap = { 29 | {20,38,61}, 30 | {22,39,62}, 31 | {25,40,64}, 32 | {28,41,66}, 33 | {30,42,68}, 34 | {33,43,70}, 35 | {36,44,72}, 36 | {39,45,74}, 37 | {41,46,75}, 38 | {44,47,77}, 39 | {47,48,79}, 40 | {50,48,81}, 41 | {53,49,82}, 42 | {56,50,84}, 43 | {59,51,85}, 44 | {62,52,87}, 45 | {66,52,89}, 46 | {69,53,90}, 47 | {72,54,91}, 48 | {75,55,93}, 49 | {78,55,94}, 50 | {82,56,96}, 51 | {85,57,97}, 52 | {88,57,98}, 53 | {92,58,99}, 54 | {95,58,100}, 55 | {99,59,101}, 56 | {102,60,102}, 57 | {105,60,103}, 58 | {109,61,104}, 59 | {112,61,105}, 60 | {116,62,106}, 61 | {119,62,107}, 62 | {123,63,107}, 63 | {126,63,108}, 64 | {130,63,109}, 65 | {133,64,109}, 66 | {137,64,110}, 67 | {140,65,110}, 68 | {144,65,110}, 69 | {148,66,111}, 70 | {151,66,111}, 71 | {154,66,111}, 72 | {158,67,111}, 73 | {161,67,111}, 74 | {165,68,111}, 75 | {168,68,111}, 76 | {172,69,111}, 77 | {175,69,111}, 78 | {178,70,111}, 79 | {182,71,110}, 80 | {185,71,110}, 81 | {188,72,110}, 82 | {192,73,109}, 83 | {195,73,109}, 84 | {198,74,108}, 85 | {201,75,108}, 86 | {204,76,107}, 87 | {207,77,106}, 88 | {210,78,106}, 89 | {213,79,105}, 90 | {216,80,104}, 91 | {219,81,103}, 92 | {221,82,102}, 93 | {224,83,101}, 94 | {227,85,100}, 95 | {229,86,99}, 96 | {232,88,98}, 97 | {235,89,97}, 98 | {237,91,96}, 99 | {239,93,95}, 100 | {242,94,93}, 101 | {244,96,92}, 102 | {246,98,91}, 103 | {244,96,92}, 104 | {242,94,93}, 105 | {239,93,95}, 106 | {237,91,96}, 107 | {235,89,97}, 108 | {232,88,98}, 109 | {229,86,99}, 110 | {227,85,100}, 111 | {224,83,101}, 112 | {221,82,102}, 113 | {219,81,103}, 114 | {216,80,104}, 115 | {213,79,105}, 116 | {210,78,106}, 117 | {207,77,106}, 118 | {204,76,107}, 119 | {201,75,108}, 120 | {198,74,108}, 121 | {195,73,109}, 122 | {192,73,109}, 123 | {188,72,110}, 124 | {185,71,110}, 125 | {182,71,110}, 126 | {178,70,111}, 127 | {175,69,111}, 128 | {172,69,111}, 129 | {168,68,111}, 130 | {165,68,111}, 131 | {161,67,111}, 132 | {158,67,111}, 133 | {154,66,111}, 134 | {151,66,111}, 135 | {148,66,111}, 136 | {144,65,110}, 137 | {140,65,110}, 138 | {137,64,110}, 139 | {133,64,109}, 140 | {130,63,109}, 141 | {126,63,108}, 142 | {123,63,107}, 143 | {119,62,107}, 144 | {116,62,106}, 145 | {112,61,105}, 146 | {109,61,104}, 147 | {105,60,103}, 148 | {102,60,102}, 149 | {99,59,101}, 150 | {95,58,100}, 151 | {92,58,99}, 152 | {88,57,98}, 153 | {85,57,97}, 154 | {82,56,96}, 155 | {78,55,94}, 156 | {75,55,93}, 157 | {72,54,91}, 158 | {69,53,90}, 159 | {66,52,89}, 160 | {62,52,87}, 161 | {59,51,85}, 162 | {56,50,84}, 163 | {53,49,82}, 164 | {50,48,81}, 165 | {47,48,79}, 166 | {44,47,77}, 167 | {41,46,75}, 168 | {39,45,74}, 169 | {36,44,72}, 170 | {33,43,70}, 171 | {30,42,68}, 172 | {28,41,66}, 173 | {25,40,64}, 174 | {22,39,62}, 175 | {20,38,61}, 176 | } 177 | }, 178 | { 179 | backgroundColor = {6,41,61}, 180 | colorMap = { 181 | {222,119,111}, 182 | {224,119,115}, 183 | {225,120,118}, 184 | {226,120,122}, 185 | {227,121,126}, 186 | {228,122,129}, 187 | {229,122,133}, 188 | {229,123,137}, 189 | {230,124,141}, 190 | {230,125,144}, 191 | {230,126,148}, 192 | {230,127,152}, 193 | {230,129,156}, 194 | {229,130,160}, 195 | {229,131,163}, 196 | {228,133,167}, 197 | {227,135,171}, 198 | {226,136,175}, 199 | {225,138,178}, 200 | {223,140,182}, 201 | {222,142,185}, 202 | {220,144,189}, 203 | {218,146,192}, 204 | {216,148,196}, 205 | {214,150,199}, 206 | {211,152,202}, 207 | {209,154,205}, 208 | {206,156,208}, 209 | {203,158,211}, 210 | {200,161,214}, 211 | {197,163,217}, 212 | {194,165,219}, 213 | {190,167,222}, 214 | {187,170,224}, 215 | {183,172,226}, 216 | {179,174,228}, 217 | {175,176,230}, 218 | {171,179,232}, 219 | {167,181,233}, 220 | {162,183,234}, 221 | {158,185,236}, 222 | {154,187,237}, 223 | {149,189,238}, 224 | {144,192,238}, 225 | {140,194,239}, 226 | {135,196,239}, 227 | {130,198,240}, 228 | {126,200,240}, 229 | {121,202,239}, 230 | {116,204,239}, 231 | {111,205,239}, 232 | {107,207,238}, 233 | {102,209,237}, 234 | {98,211,236}, 235 | {94,213,235}, 236 | {90,214,234}, 237 | {86,216,232}, 238 | {82,218,231}, 239 | {79,219,229}, 240 | {77,221,227}, 241 | {75,222,225}, 242 | {73,224,223}, 243 | {72,225,221}, 244 | {72,227,218}, 245 | {72,228,216}, 246 | {73,229,213}, 247 | {74,231,210}, 248 | {76,232,207}, 249 | {79,233,204}, 250 | {82,234,201}, 251 | {85,235,198}, 252 | {89,236,195}, 253 | {93,237,192}, 254 | {97,238,188}, 255 | {102,239,185}, 256 | {107,240,182}, 257 | {111,241,178}, 258 | {116,242,175}, 259 | {121,243,171}, 260 | {127,243,168}, 261 | {132,244,164}, 262 | {137,245,161}, 263 | {143,245,157}, 264 | {148,246,154}, 265 | {153,246,151}, 266 | {159,247,147}, 267 | {164,247,144}, 268 | {170,247,141}, 269 | {176,248,138}, 270 | {181,248,134}, 271 | {187,248,131}, 272 | {193,248,129}, 273 | {198,248,126}, 274 | {204,248,123}, 275 | {210,248,121}, 276 | {216,248,118}, 277 | {221,248,116}, 278 | {227,248,114}, 279 | {233,248,112}, 280 | {239,247,111}, 281 | {233,248,112}, 282 | {227,248,114}, 283 | {221,248,116}, 284 | {216,248,118}, 285 | {210,248,121}, 286 | {204,248,123}, 287 | {198,248,126}, 288 | {193,248,129}, 289 | {187,248,131}, 290 | {181,248,134}, 291 | {176,248,138}, 292 | {170,247,141}, 293 | {164,247,144}, 294 | {159,247,147}, 295 | {153,246,151}, 296 | {148,246,154}, 297 | {143,245,157}, 298 | {137,245,161}, 299 | {132,244,164}, 300 | {127,243,168}, 301 | {121,243,171}, 302 | {116,242,175}, 303 | {111,241,178}, 304 | {107,240,182}, 305 | {102,239,185}, 306 | {97,238,188}, 307 | {93,237,192}, 308 | {89,236,195}, 309 | {85,235,198}, 310 | {82,234,201}, 311 | {79,233,204}, 312 | {76,232,207}, 313 | {74,231,210}, 314 | {73,229,213}, 315 | {72,228,216}, 316 | {72,227,218}, 317 | {72,225,221}, 318 | {73,224,223}, 319 | {75,222,225}, 320 | {77,221,227}, 321 | {79,219,229}, 322 | {82,218,231}, 323 | {86,216,232}, 324 | {90,214,234}, 325 | {94,213,235}, 326 | {98,211,236}, 327 | {102,209,237}, 328 | {107,207,238}, 329 | {111,205,239}, 330 | {116,204,239}, 331 | {121,202,239}, 332 | {126,200,240}, 333 | {130,198,240}, 334 | {135,196,239}, 335 | {140,194,239}, 336 | {144,192,238}, 337 | {149,189,238}, 338 | {154,187,237}, 339 | {158,185,236}, 340 | {162,183,234}, 341 | {167,181,233}, 342 | {171,179,232}, 343 | {175,176,230}, 344 | {179,174,228}, 345 | {183,172,226}, 346 | {187,170,224}, 347 | {190,167,222}, 348 | {194,165,219}, 349 | {197,163,217}, 350 | {200,161,214}, 351 | {203,158,211}, 352 | {206,156,208}, 353 | {209,154,205}, 354 | {211,152,202}, 355 | {214,150,199}, 356 | {216,148,196}, 357 | {218,146,192}, 358 | {220,144,189}, 359 | {222,142,185}, 360 | {223,140,182}, 361 | {225,138,178}, 362 | {226,136,175}, 363 | {227,135,171}, 364 | {228,133,167}, 365 | {229,131,163}, 366 | {229,130,160}, 367 | {230,129,156}, 368 | {230,127,152}, 369 | {230,126,148}, 370 | {230,125,144}, 371 | {230,124,141}, 372 | {229,123,137}, 373 | {229,122,133}, 374 | {228,122,129}, 375 | {227,121,126}, 376 | {226,120,122}, 377 | {225,120,118}, 378 | {224,119,115}, 379 | {222,119,111}, 380 | } 381 | }, 382 | { 383 | backgroundColor = {6,41,61}, 384 | colorMap = { 385 | {53,239,242}, 386 | {53,237,240}, 387 | {53,234,238}, 388 | {53,232,237}, 389 | {53,230,235}, 390 | {54,227,233}, 391 | {54,225,232}, 392 | {54,222,230}, 393 | {54,220,228}, 394 | {54,218,227}, 395 | {54,215,225}, 396 | {55,213,223}, 397 | {55,211,222}, 398 | {55,208,220}, 399 | {55,206,218}, 400 | {56,204,216}, 401 | {56,201,214}, 402 | {56,199,213}, 403 | {56,197,211}, 404 | {57,194,209}, 405 | {57,192,207}, 406 | {57,190,205}, 407 | {57,187,203}, 408 | {58,185,202}, 409 | {58,183,200}, 410 | {58,180,198}, 411 | {58,178,196}, 412 | {58,176,194}, 413 | {58,173,192}, 414 | {59,171,190}, 415 | {59,169,188}, 416 | {59,167,186}, 417 | {59,164,184}, 418 | {59,162,182}, 419 | {59,160,180}, 420 | {59,158,178}, 421 | {59,155,176}, 422 | {59,153,174}, 423 | {59,151,172}, 424 | {59,149,170}, 425 | {59,147,168}, 426 | {59,144,166}, 427 | {59,142,164}, 428 | {59,140,162}, 429 | {59,138,160}, 430 | {59,136,158}, 431 | {59,134,156}, 432 | {58,131,154}, 433 | {58,129,152}, 434 | {58,127,150}, 435 | {58,125,148}, 436 | {58,123,146}, 437 | {57,121,143}, 438 | {57,119,141}, 439 | {57,117,139}, 440 | {56,115,137}, 441 | {56,112,135}, 442 | {56,110,133}, 443 | {55,108,131}, 444 | {55,106,129}, 445 | {55,104,127}, 446 | {54,102,124}, 447 | {54,100,122}, 448 | {53,98,120}, 449 | {53,96,118}, 450 | {52,94,116}, 451 | {52,92,114}, 452 | {51,90,112}, 453 | {51,88,110}, 454 | {50,86,107}, 455 | {50,84,105}, 456 | {49,82,103}, 457 | {48,80,101}, 458 | {48,79,99}, 459 | {47,77,97}, 460 | {46,75,95}, 461 | {46,73,93}, 462 | {45,71,90}, 463 | {44,69,88}, 464 | {44,67,86}, 465 | {43,65,84}, 466 | {42,64,82}, 467 | {41,62,80}, 468 | {40,60,78}, 469 | {40,58,76}, 470 | {39,56,74}, 471 | {38,55,72}, 472 | {37,53,70}, 473 | {36,51,67}, 474 | {35,49,65}, 475 | {34,48,63}, 476 | {33,46,61}, 477 | {33,44,59}, 478 | {32,43,57}, 479 | {31,41,55}, 480 | {30,39,53}, 481 | {29,38,51}, 482 | {28,36,49}, 483 | {27,34,47}, 484 | {26,33,45}, 485 | {27,34,47}, 486 | {28,36,49}, 487 | {29,38,51}, 488 | {30,39,53}, 489 | {31,41,55}, 490 | {32,43,57}, 491 | {33,44,59}, 492 | {33,46,61}, 493 | {34,48,63}, 494 | {35,49,65}, 495 | {36,51,67}, 496 | {37,53,70}, 497 | {38,55,72}, 498 | {39,56,74}, 499 | {40,58,76}, 500 | {40,60,78}, 501 | {41,62,80}, 502 | {42,64,82}, 503 | {43,65,84}, 504 | {44,67,86}, 505 | {44,69,88}, 506 | {45,71,90}, 507 | {46,73,93}, 508 | {46,75,95}, 509 | {47,77,97}, 510 | {48,79,99}, 511 | {48,80,101}, 512 | {49,82,103}, 513 | {50,84,105}, 514 | {50,86,107}, 515 | {51,88,110}, 516 | {51,90,112}, 517 | {52,92,114}, 518 | {52,94,116}, 519 | {53,96,118}, 520 | {53,98,120}, 521 | {54,100,122}, 522 | {54,102,124}, 523 | {55,104,127}, 524 | {55,106,129}, 525 | {55,108,131}, 526 | {56,110,133}, 527 | {56,112,135}, 528 | {56,115,137}, 529 | {57,117,139}, 530 | {57,119,141}, 531 | {57,121,143}, 532 | {58,123,146}, 533 | {58,125,148}, 534 | {58,127,150}, 535 | {58,129,152}, 536 | {58,131,154}, 537 | {59,134,156}, 538 | {59,136,158}, 539 | {59,138,160}, 540 | {59,140,162}, 541 | {59,142,164}, 542 | {59,144,166}, 543 | {59,147,168}, 544 | {59,149,170}, 545 | {59,151,172}, 546 | {59,153,174}, 547 | {59,155,176}, 548 | {59,158,178}, 549 | {59,160,180}, 550 | {59,162,182}, 551 | {59,164,184}, 552 | {59,167,186}, 553 | {59,169,188}, 554 | {59,171,190}, 555 | {58,173,192}, 556 | {58,176,194}, 557 | {58,178,196}, 558 | {58,180,198}, 559 | {58,183,200}, 560 | {58,185,202}, 561 | {57,187,203}, 562 | {57,190,205}, 563 | {57,192,207}, 564 | {57,194,209}, 565 | {56,197,211}, 566 | {56,199,213}, 567 | {56,201,214}, 568 | {56,204,216}, 569 | {55,206,218}, 570 | {55,208,220}, 571 | {55,211,222}, 572 | {55,213,223}, 573 | {54,215,225}, 574 | {54,218,227}, 575 | {54,220,228}, 576 | {54,222,230}, 577 | {54,225,232}, 578 | {54,227,233}, 579 | {53,230,235}, 580 | {53,232,237}, 581 | {53,234,238}, 582 | {53,237,240}, 583 | {53,239,242}, 584 | } 585 | } 586 | } 587 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- Love2d configuration 3 | -------------------------------------------------------------------------------- 4 | function love.conf(conf) 5 | 6 | conf.window.title = "mandelbrot viwer" 7 | 8 | -- the domain and range of the mandelbrot is (3.5, 2), for simplicity sake, 9 | -- the viewer window is created with this aspect ratio, this can be 10 | -- adjusted, without 'stretching' the fractal since the fractal calculates 11 | -- the range of the viewport using the current aspect ratio of the window 12 | conf.window.height = 500 13 | conf.window.width = conf.window.height * (3.5 / 2) 14 | 15 | conf.window.resizable = true 16 | 17 | end 18 | -------------------------------------------------------------------------------- /locations.txt: -------------------------------------------------------------------------------- 1 | interesting locations to explore 2 | 3 | xCenter = -1.2558081562179 4 | yCenter = -0.38087748157101 5 | domain = 0.011303445482483 6 | 7 | xCenter = -0.86340997875238 8 | yCenter = -0.27488519462645 9 | domain = 0.00065364460966453 10 | 11 | xCenter = -1.2487877699720 12 | yCenter = 0.07179990006388 13 | domain = 0.0011047709493061 14 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- globals 3 | -------------------------------------------------------------------------------- 4 | -- windowWidth, windowHeight and the window aspect ratio are used multiple times 5 | -- in the program, so they're stored here in love.load() for handy use 6 | windowWidth = 0 7 | windowHeight = 0 8 | aspectRatio = 0 9 | 10 | -- the fragment shader used to render the fractal 11 | shader = {} 12 | 13 | -- the fractal is rendered to this canvas off-screen only whenever its 14 | -- parameters are changed, this keeps the graphics card from being pegged 15 | -- constantly, also keep track of this canvas' dimensions 16 | fractalCanvas = {} 17 | fractalCanvasWidth = 0 18 | fractalCanvasHeight = 0 19 | 20 | -- the index of the 'colorpack' to use on startup 21 | colorPack = 2 22 | 23 | -- this controls how large the rendering canvas and rendering size is, as a 24 | -- multiple of the window size 25 | oversample = 2 26 | 27 | -- initial starting coordinates and domain (range is calculated in the shader 28 | -- using the window's aspect ratio to avoid distortion of the fractal) 29 | xCenter = -0.75; 30 | yCenter = 0; 31 | domain = 3.5; 32 | 33 | -- the domain is multiplied times this when zooming, smaller numbers cause 34 | -- zooming to occur faster 35 | zoomSpeed = 0.98 36 | 37 | -- controls if the current zoomspeed is displayed 38 | displayZoomSpeed = true 39 | 40 | -------------------------------------------------------------------------------- 41 | -- Love2d initialization 42 | -------------------------------------------------------------------------------- 43 | function love.load(a) 44 | 45 | -- populate some of the configuration globals for use later 46 | windowWidth = love.graphics.getWidth() 47 | windowHeight = love.graphics.getHeight() 48 | aspectRatio = windowWidth / windowHeight 49 | 50 | -- load and execute the color data, which creates a "colors" global 51 | local colorData = love.filesystem.load("colorData.lua") 52 | colorData() 53 | 54 | -- create the fractal shader from the shader program 55 | shader = love.graphics.newShader("shader.frag") 56 | 57 | -- create the offscreen drawing canvas 58 | createCanvas() 59 | 60 | -- send the current colorPack to the shader 61 | sendColorPack(colorPack) 62 | 63 | -- draw the fractal to the canvas 64 | drawFractal(fractalCanvasWidth, fractalCanvasHeight, fractalCanvas) 65 | end 66 | 67 | -------------------------------------------------------------------------------- 68 | -- Love2d pre-frame update 69 | -------------------------------------------------------------------------------- 70 | function love.update(dt) 71 | -- the mouse zooms the fractal in our out (left = in, right = out) 72 | if love.mouse.isDown(1, 2) then 73 | 74 | -- find the x and y point in the current fractal domain 75 | local x = love.mouse.getX() 76 | local y = love.mouse.getY() 77 | local range = domain / aspectRatio 78 | local xClick = xCenter + ((x / windowWidth) * domain) - (domain / 2) 79 | local yClick = yCenter + ((y / windowHeight) * range) - (range / 2) 80 | 81 | -- are we zooming in or out? 82 | if love.mouse.isDown(1) then 83 | domain = domain * zoomSpeed 84 | elseif love.mouse.isDown(2) then 85 | domain = domain / zoomSpeed 86 | end 87 | 88 | -- need to set a new center so that this point stays at the 89 | -- location of the click, instead of zooming in at the center, this is 90 | -- essentially the inverse of the previous calculation, just using 91 | -- the new domain after zooming in or out 92 | local range = domain / aspectRatio 93 | xCenter = xClick - ((x / windowWidth) * domain) + (domain / 2) 94 | yCenter = yClick - ((y / windowHeight) * range) + (range / 2) 95 | 96 | -- update the fractal in the offscreen buffer 97 | drawFractal(fractalCanvasWidth, fractalCanvasHeight, fractalCanvas) 98 | end 99 | 100 | local panRatio = 1000 101 | -- the arrow keys can be used to pan around the fractal 102 | -- TODO: implement accurate panning math, right now the panning appears to 103 | -- occur at different speeds at different zoom levels 104 | if love.keyboard.isDown("up", "down", "left", "right") then 105 | if love.keyboard.isDown("up") then 106 | yCenter = yCenter - ((zoomSpeed * domain) / panRatio) 107 | elseif love.keyboard.isDown("down") then 108 | yCenter = yCenter + ((zoomSpeed * domain) / panRatio) 109 | end 110 | 111 | if love.keyboard.isDown("left") then 112 | xCenter = xCenter - ((zoomSpeed * domain) / panRatio) 113 | elseif love.keyboard.isDown("right") then 114 | xCenter = xCenter + ((zoomSpeed * domain) / panRatio) 115 | end 116 | print(xCenter, yCenter, domain) 117 | drawFractal(fractalCanvasWidth, fractalCanvasHeight, fractalCanvas) 118 | end 119 | 120 | end 121 | 122 | -------------------------------------------------------------------------------- 123 | -- Love2d drawing 124 | -------------------------------------------------------------------------------- 125 | function love.draw(dt) 126 | 127 | -- draw the fractal canvas, rescaling it to the window's size 128 | love.graphics.draw(fractalCanvas, 0, 0, 0, 1 / oversample) 129 | 130 | -- print the current zoom speed to the screen 131 | if displayZoomSpeed then 132 | local x = 5 133 | local y = windowHeight - 18 134 | love.graphics.print("zoom speed : " .. zoomSpeed, x, y, 0, 0.8) 135 | end 136 | 137 | end 138 | 139 | -------------------------------------------------------------------------------- 140 | -- Love2d event callbacks 141 | -------------------------------------------------------------------------------- 142 | function love.resize() 143 | -- recalculate some of the configuration globals 144 | windowWidth = love.graphics.getWidth() 145 | windowHeight = love.graphics.getHeight() 146 | aspectRatio = windowWidth / windowHeight 147 | 148 | -- recreate the canvas and the canvas size globals 149 | createCanvas() 150 | 151 | -- redraw the fractal 152 | drawFractal(fractalCanvasWidth, fractalCanvasHeight, fractalCanvas) 153 | end 154 | function love.wheelmoved(x, y) 155 | 156 | -- adjust the zoom speed with a up or down scrolling 157 | zoomSpeed = math.min(zoomSpeed + (-0.0005 * y), 1) 158 | 159 | end 160 | 161 | function love.keypressed(key, scancode, isrepeat) 162 | 163 | -- the 'i' and 'k' keys can be used to index through the available 164 | -- color packs, after changing the index, the new colorPack is sent to 165 | -- the shader and then the fractal is redrawn 166 | if key == "i" or key == "k" then 167 | if key == "i" then 168 | colorPack = ((colorPack + 1) % table.maxn(colors)) 169 | elseif key == "k" then 170 | colorPack = ((colorPack - 1) % table.maxn(colors)) 171 | end 172 | print("Changed colorPack index to " .. colorPack) 173 | sendColorPack(colorPack) 174 | drawFractal(fractalCanvasWidth, fractalCanvasHeight, fractalCanvas) 175 | end 176 | 177 | -- 's' initiates a save of the current window to the game directory 178 | -- which is platform dependent (check the Love2d docs for the location) 179 | if key == "s" then 180 | saveFractal() 181 | end 182 | 183 | -- the 'o' and 'l' keys can be used to increase or decrease the sample 184 | -- rate, this requires recreating the canvas and redrawing the fractal 185 | if key == "o" or key == "l" then 186 | if key == "o" then 187 | oversample = oversample + 1 188 | elseif key == "l" and not (oversample == 1) then 189 | oversample = oversample - 1 190 | end 191 | print("Changed oversample rate to " .. oversample) 192 | createCanvas() 193 | drawFractal(fractalCanvasWidth, fractalCanvasHeight, fractalCanvas) 194 | end 195 | end 196 | 197 | -------------------------------------------------------------------------------- 198 | -- helper functions 199 | -------------------------------------------------------------------------------- 200 | function createCanvas() 201 | -- the canvas needs to be oversample times the windowsize large 202 | fractalCanvasWidth = windowWidth * oversample 203 | fractalCanvasHeight = windowHeight * oversample 204 | 205 | -- create a canvas to the render fractal to offscreen 206 | fractalCanvas = love.graphics.newCanvas(fractalCanvasWidth, fractalCanvasHeight) 207 | end 208 | 209 | function drawFractal(width, height, drawCanvas, sendSize) 210 | 211 | -- send all of the information to the shader 212 | print(width, height, xCenter, yCenter, domain) 213 | print(drawCanvas) 214 | print(fractalCanvas) 215 | shader:send("window_width", width) 216 | shader:send("window_height", height) 217 | shader:send("x_center", xCenter) 218 | shader:send("y_center", yCenter) 219 | shader:send("domain", domain) 220 | 221 | -- draw the fractal to the drawCanvas using the shader by filling the 222 | -- canvas with a filled rectangle 223 | love.graphics.setCanvas(drawCanvas) 224 | love.graphics.setShader(shader) 225 | love.graphics.rectangle("fill", 0, 0, width, height) 226 | love.graphics.setShader() 227 | love.graphics.setCanvas() 228 | 229 | end 230 | 231 | function saveFractal() 232 | -- set the output height an the width using the current window aspect ratio 233 | local outputHeight = 1500 234 | local outputWidth = outputHeight * aspectRatio 235 | 236 | -- set the rate at which the final output will be supersampled 237 | local outputOversample = 4 238 | 239 | -- create the output canvas and draw the fractal to it 240 | local outputCanvasWidth = outputWidth * outputOversample 241 | local outputCanvasHeight = outputHeight * outputOversample 242 | local outputCanvas = love.graphics.newCanvas(outputCanvasWidth, outputCanvasHeight) 243 | 244 | print("Rendering...") 245 | drawFractal(outputCanvasWidth, outputCanvasHeight, outputCanvas) 246 | 247 | -- get the image data and save it to the data directory 248 | local canvasData = outputCanvas:newImageData() 249 | local canvasImage = canvasData:encode("png") 250 | local filename = os.time() .. ".png" 251 | local result = love.filesystem.write(filename, canvasImage:getString()) 252 | 253 | -- see if it worked 254 | if result then 255 | print("Fractal output was successful, saved to file: " .. filename) 256 | else 257 | print("Error while saving fractal output") 258 | end 259 | 260 | end 261 | 262 | function sendColorPack(n) 263 | -- send the color_count (the length of the colorMap) to the shader, the 264 | -- colors themselves, and the backgroundColor of the colorMap 265 | shader:send("color_count", table.maxn(colors[n + 1]["colorMap"])) 266 | shader:sendColor("colors", unpack(colors[n + 1]["colorMap"])) 267 | shader:sendColor("background_color", colors[n + 1]["backgroundColor"]) 268 | end 269 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Love2d Mandelbrot Browser 2 | 3 | This is a small Love2d experiment with it's fragment (pixel) shader support. The fractals are rendered offscreen to an oversized buffer canvas, and then scaled down to the window size to achieve smoother results. Click to zoom in and out, pan and change some configuration with the keyboard. Targets Love 0.10.0. 4 | 5 | #### Controls 6 | * `click` - zoom in on the area under the mouse pointer 7 | * `scroll up/scroll down` - increase/decrease the zoom speed 8 | * `up, down, left, right` - pan the viewport 9 | * `i/k` - step through the available color maps 10 | * `s` - save the fractal in a high resolution version to the Love2d [data directory](https://love2d.org/wiki/love.filesystem) 11 | * `o/l` - increase/decrease sample rate (the multiple of the window size that the drawing canvas is sized to) 12 | 13 | Most debug and utility output is directed towards a console window, so launching using the terminal is useful. Read [here](https://love2d.org/wiki/Debug) on how to do this on Windows. 14 | 15 | 16 | #### Bugs + Needed Features 17 | * panning with the arrow keys doesn't work at the same speed at multiple zoom levels 18 | * ability to set output width or height, output currently fails if the viewing window is not tall enough (the output height is hardcoded in, and for the output width to match the aspect ratio of a window that isn't very tall makes the rendering size very large) 19 | 20 | #### Adding and Changing Coloring 21 | 22 | The `colorData.lua` file can be edited to change existing colorings or add more. They look better if they are cyclic, but they don't have to be. There is a note inside this file on how the existing colorings were generated. 23 | 24 | 25 | #### Nofix 26 | * low precision at high zoom levels, a limitation of the OpenGL floating point precision, and this whole thing was created with that expectation 27 | 28 | #### Screenshots 29 | 30 | ![image](https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/master/screenshots/1.png) 31 | 32 | ![image](https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/master/screenshots/2.png) 33 | 34 | ![image](https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/master/screenshots/3.png) 35 | 36 | ![image](https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/master/screenshots/4.png) -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/a76bd49f5cccbe35f2c460528652a1254aeb6aca/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/a76bd49f5cccbe35f2c460528652a1254aeb6aca/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/a76bd49f5cccbe35f2c460528652a1254aeb6aca/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattegan/Love-Mandelbrot/a76bd49f5cccbe35f2c460528652a1254aeb6aca/screenshots/4.png -------------------------------------------------------------------------------- /shader.frag: -------------------------------------------------------------------------------- 1 | // these external variables are described in the drawFractal(), 2 | // and sendColorPack() functions in the lua code 3 | extern float window_width; 4 | extern float window_height; 5 | extern float x_center; 6 | extern float y_center; 7 | extern float domain; 8 | extern float color_count; 9 | extern vec3 colors[300]; 10 | extern vec3 background_color; 11 | 12 | vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) { 13 | 14 | // only the domain is defined, and then the aspect ratio of the window is 15 | // used to calculate the range, this way the fractal is never stretched 16 | // TODO: calculate this stuff on the CPU side and use an extern to do these 17 | // two calculations for aspect_ratio and range, it should save some 18 | // time (most likely marginal) 19 | float aspect_ratio = window_width / window_height; 20 | float range = domain / aspect_ratio; 21 | 22 | // most of this is stolen from the Wikipedia page at: 23 | // https://en.wikipedia.org/wiki/Mandelbrot_set#Computer_drawings 24 | 25 | // normalize the pixel coordinate (supplied in integer pixels) such that the 26 | // center of the screen is at x_center and y_center, and the edges of the 27 | // screen are domain / 2 and range / 2 away from the center, respectively 28 | float x0 = x_center + ((float(screen_coords.x) / window_width) * domain) - (domain / 2.0); 29 | float y0 = y_center + ((float(screen_coords.y) / window_height) * range) - (range / 2.0); 30 | 31 | // check to see if the point is within the main cardioid (a large portion of 32 | // the fractal which never converges and will surely hit the iteration limit) 33 | // using some limacon math, if the point is within the cardiod just return 34 | // the background color 35 | // this is described at: 36 | // https://en.wikipedia.org/wiki/Mandelbrot_set#Cardioid_.2F_bulb_checking 37 | float p = sqrt(pow(x0 - 0.25, 2.0) + pow(y0, 2.0)); 38 | if (x0 < (p - (2.0 * pow(p, 2.0)) + 0.25)) { 39 | return vec4(background_color, 1.0); 40 | } 41 | 42 | // iteration variables 43 | float x = 0.0; 44 | float y = 0.0; 45 | int iteration = 0; 46 | int max_iterations = 1000; 47 | float bailout_radius = 256.0; 48 | 49 | // precalculate the powers of two to prevent them from being calculated twice 50 | // every iteration, probably not incredibly significant 51 | float x_pow_two = pow(x, 2.0); 52 | float y_pow_two = pow(y, 2.0); 53 | 54 | // iterate until the bailout_radius or iteration limit is reached 55 | while((x_pow_two + y_pow_two < bailout_radius) && iteration < max_iterations) { 56 | float xtemp = x_pow_two - y_pow_two + x0; 57 | y = 2.0 * x * y + y0; 58 | x = xtemp; 59 | iteration += 1; 60 | x_pow_two = pow(x, 2.0); 61 | y_pow_two = pow(y, 2.0); 62 | } 63 | 64 | // this is an implementation of the continuous coloring escape-time algorithm 65 | // described at Wikipedia here: 66 | // https://en.wikipedia.org/wiki/Mandelbrot_set#Continuous_.28smooth.29_coloring 67 | // and a little better here: 68 | // http://yozh.org/2010/12/01/mset005/ 69 | 70 | // store log(2) since it's used a few times 71 | float log_two = log(2.0); 72 | if (iteration < max_iterations) { 73 | float log_zn = log(x_pow_two + y_pow_two) / 2.0; 74 | float nu = log(log_zn / log_two) / log_two; 75 | float normalized_iteration = float(iteration) + 1.0 - nu; 76 | float interp = fract(normalized_iteration); 77 | vec3 first_color = colors[int(mod(float(iteration), color_count))]; 78 | vec3 second_color = colors[int(mod(float(iteration) + 1.0, color_count))]; 79 | return vec4(mix(first_color, second_color, interp), 1.0); 80 | } else { 81 | return vec4(background_color, 1.0); 82 | } 83 | } 84 | --------------------------------------------------------------------------------