1 /** 2 * Game objects and logic 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.demo.pewpew.objects; 15 16 import std.algorithm.comparison; 17 import std.random; 18 import std.math; 19 20 import ae.utils.container.listnode; 21 import ae.utils.math; 22 import ae.utils.geometry; 23 import ae.utils.graphics.color; 24 import ae.utils.graphics.draw; 25 import ae.utils.graphics.image; 26 27 __gshared: 28 29 enum Plane 30 { 31 Logic, 32 BG0, 33 BG1, 34 BG2, 35 Ship, 36 PlasmaOrbs, 37 Enemies, 38 Particles, 39 Torpedoes, 40 Explosions, 41 Max 42 } 43 44 enum STAR_LAYERS = 3; 45 46 DListContainer!GameEntity[Plane.Max] planes; 47 bool initializing = true; 48 49 alias L16 COLOR; 50 //alias G8 COLOR; // less precise, but a bit faster 51 52 Image!COLOR canvas; 53 float cf(float x) { assert(canvas.w == canvas.h); return x*canvas.w; } 54 int ci(float x) { assert(canvas.w == canvas.h); return cast(int)(x*canvas.w); } 55 T cbound(T)(T x) { return bound(x, 0, canvas.w); } 56 auto BLACK = COLOR(0); 57 auto WHITE = COLOR(COLOR.ChannelType.max); 58 59 bool up, down, left, right, fire; 60 61 bool useAnalog; float analogX, analogY; 62 63 float frand () { return uniform!`[)`( 0.0f, 1.0f); } 64 float frands() { return uniform!`()`(-1.0f, 1.0f); } 65 66 T ssqr(T)(T x) { return sqr(x) * sign(x); } 67 float frands2() { return ssqr(frands()); } 68 69 mixin FixMath; 70 71 // ********************************************************* 72 73 class GameEntity 74 { 75 mixin DListLink; 76 Plane plane; 77 78 abstract void step(uint deltaTicks); 79 abstract void render(); 80 81 void add(Plane plane) 82 { 83 this.plane = plane; 84 planes[plane].pushBack(this); 85 } 86 87 void remove() 88 { 89 planes[plane].remove(this); 90 } 91 } 92 93 class Game : GameEntity 94 { 95 this() 96 { 97 add(Plane.Logic); 98 spawnParticles = new SpawnParticles(Plane.Particles); 99 foreach (layer; 0..STAR_LAYERS) 100 starFields[layer] = new StarField(cast(Plane)(Plane.BG0 + layer)); 101 torpedoParticles = new TorpedoParticles(Plane.Particles); 102 } 103 104 uint spawnTimer; 105 106 override void step(uint deltaTicks) 107 { 108 foreach (i; 0..deltaTicks) 109 { 110 auto z = frand(); 111 auto star = Star( 112 frand(), 0, 113 0.0001f + (1-z) * 0.00005f, 114 COLOR(fixfpart(tofix((1-z)*0.5f)))); 115 auto layer = cast(int)((1-z)*3); 116 starFields[layer].add(star); 117 } 118 if (!initializing && ship && !ship.dead && spawnTimer--==0) 119 { 120 new Thingy(); 121 spawnTimer = uniform(1500, 2500); 122 } 123 if (!initializing && planes[Plane.Ship].empty && planes[Plane.Enemies].empty && planes[Plane.PlasmaOrbs].empty && planes[Plane.Explosions].empty) 124 new Ship(); 125 } 126 127 override void render() {} 128 } 129 130 // ********************************************************* 131 132 class ParticleManager(Particle) : GameEntity 133 { 134 Particle* particles; 135 int particleCount; 136 137 void add(Particle particle) 138 { 139 if (particleCount == Particle.MAX) 140 return; 141 particles[particleCount++] = particle; 142 } 143 144 this(Plane plane) 145 { 146 particles = (new Particle[Particle.MAX]).ptr; 147 super.add(plane); 148 } 149 150 override void step(uint deltaTicks) 151 { 152 int i = 0; 153 while (i < particleCount) 154 with (particles[i]) 155 { 156 enum REMOVE = q{ particles[i] = particles[--particleCount]; }; 157 enum NEXT = q{ i++; }; 158 mixin(Particle.STEP); 159 } 160 } 161 162 override void render() 163 { 164 import std.parallelism; 165 foreach (ref particle; taskPool.parallel(particles[0..particleCount])) 166 with (particle) 167 { 168 mixin(Particle.RENDER); 169 } 170 } 171 } 172 173 /+ 174 class ParticleManager(Particle) : GameEntity 175 { 176 Particle[] particles; 177 178 void add(Particle particle) 179 { 180 particles ~= particle; 181 } 182 183 this(Plane plane) 184 { 185 super.add(plane); 186 } 187 188 override void step(uint deltaTicks) 189 { 190 for (int i=0; i<particles.length; i++) 191 with (particles[i]) 192 { 193 enum REMOVE = q{ particles = particles[0..i] ~ particles[i+1..$]; }; 194 enum NEXT = q{ }; 195 mixin(Particle.STEP); 196 } 197 } 198 199 override void render() 200 { 201 foreach (ref particle; particles) 202 with (particle) 203 { 204 mixin(Particle.RENDER); 205 } 206 } 207 } 208 +/ 209 210 struct Star 211 { 212 float x, y, vy; 213 COLOR color; 214 215 enum MAX = 1024*32; 216 217 enum STEP = 218 q{ 219 y += deltaTicks * vy; 220 if (y > 1f) 221 { mixin(REMOVE); } 222 else 223 { mixin(NEXT); } 224 }; 225 226 enum RENDER = 227 q{ 228 canvas.aaPutPixel(cf(x), cf(y), color); 229 }; 230 } 231 232 alias ParticleManager!Star StarField; 233 StarField[STAR_LAYERS] starFields; 234 235 // ********************************************************* 236 237 class GameObject : GameEntity 238 { 239 float x, y, vx=0, vy=0; 240 Shape!float[] shapes; /// shape coordinates are relative to x,y 241 bool dead; 242 243 enum DEATHBRAKES = 0.998f; 244 245 override void step(uint deltaTicks) 246 { 247 x += vx*deltaTicks; 248 y += vy*deltaTicks; 249 if (dead) 250 vx *= DEATHBRAKES, 251 vy *= DEATHBRAKES; 252 } 253 254 final void collideWith(Plane[] planeIndices...) 255 { 256 assert(!dead); 257 258 foreach (plane; planeIndices) 259 foreach (obj; planes[plane]) 260 { 261 auto enemy = cast(GameObject) obj; 262 if (enemy && !enemy.dead) 263 foreach (shape1; shapes) 264 { 265 if (shape1.kind == ShapeKind.none) continue; 266 shape1.translate(x, y); 267 foreach (shape2; enemy.shapes) 268 { 269 if (shape2.kind == ShapeKind.none) continue; 270 shape2.translate(enemy.x, enemy.y); 271 if (intersects(shape1, shape2)) 272 { 273 die(); 274 enemy.die(); 275 return; 276 } 277 } 278 } 279 } 280 } 281 282 void die() 283 { 284 remove(); 285 } 286 } 287 288 // ********************************************************* 289 290 class Ship : GameObject 291 { 292 float death = 0f, spawn = 0f; 293 bool spawning; 294 uint t; 295 296 enum SPAWN_START = 0.3f; 297 enum SPAWN_END = 0.3f; 298 299 this() 300 { 301 x = 0.5f; 302 y = 0.85f; 303 vx = vy = 0; 304 shapes ~= shape(rect(-0.040f, -0.020f, -0.028f, +0.040f)); // left wing 305 shapes ~= shape(rect(+0.040f, -0.020f, +0.028f, +0.040f)); // right wing 306 shapes ~= shape(rect(-0.008f, -0.040f, +0.008f, +0.030f)); // center hull 307 shapes ~= shape(rect(-0.030f, +0.020f, +0.030f, +0.024f)); // bridge 308 shapes ~= shape(circle(0, +0.020f, 0.020f)); // round section 309 add(Plane.Ship); 310 ship = this; 311 dead = spawning = true; 312 } 313 314 override void step(uint deltaTicks) 315 { 316 if (!dead) 317 { 318 const a = 0.000_001f; 319 const maxv = 0.000_500f; 320 321 if (useAnalog) 322 vx = analogX * maxv, 323 vy = analogY * maxv; 324 else 325 { 326 if (left) 327 vx = bound(vx-a*deltaTicks, -maxv, 0); 328 else 329 if (right) 330 vx = bound(vx+a*deltaTicks, 0, maxv); 331 else 332 vx = 0; 333 334 if (up) 335 vy = bound(vy-a*deltaTicks, -maxv, 0); 336 else 337 if (down) 338 vy = bound(vy+a*deltaTicks, 0, maxv); 339 else 340 vy = 0; 341 } 342 343 t += deltaTicks; 344 } 345 super.step(deltaTicks); 346 347 if (!dead) 348 { 349 if (x<0.05f || x>0.95f) vx = 0; 350 if (y<0.05f || y>0.95f) vy = 0; 351 x = bound(x, 0.05f, 0.95f); 352 y = bound(y, 0.05f, 0.95f); 353 354 static bool wasFiring; 355 //fire = t % 250 == 0; 356 if (fire && !wasFiring) 357 { 358 new Torpedo(-0.034f, -0.020f); 359 new Torpedo(+0.034f, -0.020f); 360 } 361 wasFiring = !!fire; 362 363 collideWith(Plane.Enemies, Plane.PlasmaOrbs); 364 } 365 else 366 if (spawning) 367 { 368 spawn += 0.0005f; 369 370 if (spawn < SPAWN_START+1f) 371 foreach (n; 0..5) 372 { 373 float px = frands()*0.050f; 374 spawnParticles.add(SpawnParticle( 375 x + px*1.7f + frands ()*0.010f, 376 spawnY() + frands2()*0.050f, 377 x + px, 378 )); 379 } 380 381 if (spawn >= SPAWN_START+1f+SPAWN_END) 382 spawning = dead = false; 383 } 384 else 385 { 386 death += (1f/2475f); 387 if (death > 1f) 388 remove(); 389 } 390 } 391 392 override void die() 393 { 394 new Explosion(this, 0.150f); 395 dead = true; 396 } 397 398 override void render() 399 { 400 enum Gray25 = COLOR.ChannelType.max / 4; 401 enum Gray75 = COLOR.ChannelType.max / 4 * 3; 402 403 void drawRect(float x0, float y0, float x1, float y1, COLOR color) 404 { 405 canvas.aaFillRect(cf(x+x0), cf(y+y0), cf(x+x1), cf(y+y1), color); 406 } 407 408 void drawRect2(Rect!float r) 409 { 410 r.sort(); 411 drawRect(r.x0 , r.y0 , r.x1 , r.y1 , COLOR(Gray25)); 412 drawRect(r.x0+0.002f, r.y0+0.002f, r.x1-0.002f, r.y1-0.002f, COLOR(Gray75)); 413 } 414 415 void drawCircle(Circle!float c, COLOR color) 416 { 417 canvas.softCircle(cf(x+c.x), cf(y+c.y), cf(c.r*0.7f), cf(c.r), color); 418 } 419 420 421 void warp(float x, float y, float r) 422 { 423 auto bgx0 = cbound(ci(x-r)); 424 auto bgy0 = cbound(ci(y-r)); 425 auto bgx1 = cbound(ci(x+r)); 426 auto bgy1 = cbound(ci(y+r)); 427 auto window = canvas.crop(bgx0, bgy0, bgx1, bgy1); 428 static Image!COLOR bg; 429 window.copy(bg); 430 auto cx = ci(x)-bgx0; 431 auto cy = ci(y)-bgy0; 432 procedural!((x, y) 433 { 434 int dx = x-cx; 435 int dy = y-cy; 436 int sx = x; 437 int sy = y; 438 float f = dist(dx, dy) / cx; 439 if (f < 1f && f > 0f) 440 { 441 float f2 = (1-f)*sqrt(sqrt(f)) + f*f; 442 assert(f2 < 1f); 443 sx = cx + cast(int)(dx / f * f2); 444 sy = cy + cast(int)(dy / f * f2); 445 } 446 return bg.safeGet(sx, sy, COLOR(0)); 447 })(window.w, window.h).blitTo(window); 448 } 449 450 if (spawning) 451 { 452 enum R = 0.15f; 453 warp(x, spawnY(), R * sqrt(sin(spawn/(SPAWN_START+1f+SPAWN_END)*PI))); 454 } 455 456 static Image!COLOR bg; 457 int bgx0, bgyS; 458 if (spawning) 459 { 460 bgx0 = ci(x-0.050f); 461 bgyS = ci(spawnY()); 462 canvas.crop(bgx0, bgyS, ci(x+0.050f), ci(y+0.050f)).copy(bg); 463 } 464 465 drawRect2 (shapes[0].rect); 466 drawRect2 (shapes[1].rect); 467 drawRect2 (shapes[2].rect); 468 drawRect (shapes[3].rect.tupleof, COLOR(Gray25)); 469 drawCircle(shapes[4].circle , COLOR(Gray75)); 470 471 if (spawning) 472 bg.blitTo(canvas, bgx0, bgyS); 473 } 474 475 float spawnY() 476 { 477 return y-0.050f + (0.100f * bound(spawn-SPAWN_START, 0f, 1f)); 478 } 479 } 480 481 Ship ship; 482 483 struct SpawnParticle 484 { 485 float x0, y0, x1, t=0f; 486 487 enum MAX = 1024*32; 488 489 enum STEP = 490 q{ 491 t += 0.002f; 492 if (t > 1f) 493 { mixin(REMOVE); } 494 else 495 { mixin(NEXT); } 496 }; 497 498 enum RENDER = 499 q{ 500 float y1 = ship.spawnY(); 501 float tt0 = sqr(sqr(t)); 502 float tt1 = min(1, tt0+0.15f); 503 float lx0 = x0 + tt0*(x1-x0); 504 float ly0 = y0 + tt0*(y1-y0); 505 float lx1 = x0 + tt1*(x1-x0); 506 float ly1 = y0 + tt1*(y1-y0); 507 //canvas.aaPutPixel(cf(x), cf(y), WHITE, tofracBounded(tt)); 508 canvas.aaLine(cf(lx0), cf(ly0), cf(lx1), cf(ly1), WHITE, tofracBounded(sqr(tt0))); 509 }; 510 } 511 512 alias ParticleManager!SpawnParticle SpawnParticles; 513 SpawnParticles spawnParticles; 514 515 // ********************************************************* 516 517 class Torpedo : GameObject 518 { 519 this(float dx, float dy) 520 { 521 this.x = ship.x + dx; 522 this.y = ship.y + dy; 523 this.vx = ship.vx; 524 this.vy = ship.vy - 0.000_550f; 525 shapes ~= Shape!float(Point!float(0, 0)); 526 add(Plane.Torpedoes); 527 } 528 529 int t; 530 531 override void step(uint deltaTicks) 532 { 533 super.step(deltaTicks); 534 if (y < -0.25f || x < 0 || x > 1) 535 return remove(); 536 537 //if ((t+=deltaTicks) % 1 == 0) 538 torpedoParticles.add(TorpedoParticle( 539 x, y, 540 frands()*0.000_010f, 541 frand ()*0.000_100f + 0.000_200f)); 542 543 collideWith(Plane.Enemies, Plane.PlasmaOrbs); 544 } 545 546 override void die() 547 { 548 remove(); 549 foreach (n; 0..500) 550 { 551 auto a = uniform(0, TAU); 552 torpedoParticles.add(TorpedoParticle(x, y, 553 frand()*cos(a)*0.000_300f, 554 frand()*sin(a)*0.000_300f - 0.000_100f, 555 0.003f)); 556 } 557 } 558 559 override void render() 560 { 561 canvas.aaPutPixel(cf(x), cf(y), WHITE); 562 } 563 } 564 565 struct TorpedoParticle 566 { 567 float x, y, vx, vy, s = 0.001f, t = 0; 568 569 enum MAX = 1024*64; 570 571 enum STEP = 572 q{ 573 x += vx; 574 y += vy; 575 t += s; 576 if (t >= 1) 577 mixin(REMOVE); 578 else 579 mixin(NEXT); 580 }; 581 582 enum RENDER = 583 q{ 584 canvas.aaPutPixel(cf(x), cf(y), WHITE, tofracBounded(1-t)); 585 }; 586 } 587 588 alias ParticleManager!TorpedoParticle TorpedoParticles; 589 TorpedoParticles torpedoParticles; 590 591 // ********************************************************* 592 593 class Enemy : GameObject 594 { 595 } 596 597 class ThingyPart : Enemy 598 { 599 float death = 0f; 600 601 override void render() 602 { 603 float r1 = shapes[0].circle.r; 604 float r0 = r1*(2f/3f); 605 606 if (!dead) 607 canvas.softCircle(cf(x), cf(y), cf(r0), cf(r1), WHITE); 608 else 609 { 610 canvas.softCircle(cf(x), cf(y), cf(r0), cf(r1), COLOR(tofracBounded(1-death))); 611 canvas.softCircle(cf(x), cf(y), cf(r0*death), cf(r1*death), BLACK); 612 } 613 } 614 } 615 616 class Thingy : ThingyPart 617 { 618 float a, va; 619 ThingySatellite[] satellites; 620 int charge; // max is 2000 621 622 this() 623 { 624 this.x = uniform(0f, 1f); 625 this.y = -0.060f; 626 this.vx = frands()*0.0003f; 627 this.vy = 0.0002f+frand()*0.0003f; 628 shapes ~= shape(circle(0, 0, 0.030f)); 629 va = 0.002f * sign(frands()); 630 a = frand()*TAU; 631 charge = 0; 632 auto numSatellites = uniform!"[]"(2, 2+(ship.t / 10_000)); 633 634 //charge=int.min; x=0.25f;vx=0;vy=0.0001f; static int c=2; numSatellites=c++; 635 636 foreach (i; 0..numSatellites) 637 satellites ~= new ThingySatellite(); 638 639 add(Plane.Enemies); 640 } 641 642 override void step(uint deltaTicks) 643 { 644 for (int n=0; n<deltaTicks; n++) 645 { 646 if (x < 0.020f) 647 vx = max(vx, -vx); 648 if (x > 0.980f) 649 vx = min(vx, -vx); 650 super.step(1); 651 if (y > 1.060f) 652 { 653 foreach (s; satellites) 654 if (!s.dead) 655 s.remove(); 656 remove(); 657 return; 658 } 659 a += va; 660 661 if (!dead) 662 { 663 foreach (s; satellites) 664 if (!s.dead) 665 charge++; 666 667 while (charge >= 2000) 668 { 669 charge -= 2000; 670 if (ship && !ship.dead) 671 new PlasmaOrb(this); 672 } 673 } 674 else 675 { 676 death += 0.002f; 677 if (death > 1) 678 remove(); 679 } 680 } 681 682 uint level = 0; 683 uint lastLevelCount = 1; 684 void arrange(ThingySatellite[] satellites) 685 { 686 foreach (i, s; satellites) 687 { 688 auto sd = 0.040f + 0.025f*level + s.death*0.020f; // satellite distance 689 auto sa = a + TAU*i/satellites.length + level*(TAU/lastLevelCount/2); 690 s.x = x + sd*cos(sa); 691 s.y = y + sd*sin(sa); 692 } 693 level++; 694 lastLevelCount = cast(uint)satellites.length; 695 } 696 697 if (satellites.length <= 8) 698 arrange(satellites[ 0.. $]); 699 else 700 if (satellites.length <= 12) 701 { 702 arrange(satellites[ 0.. 6]); 703 arrange(satellites[ 6.. $]); 704 } 705 else 706 if (satellites.length <= 24) 707 { 708 arrange(satellites[ 0.. 8]); 709 arrange(satellites[ 8.. $]); 710 } 711 else 712 if (satellites.length <= 32) 713 { 714 arrange(satellites[ 0.. 8]); 715 arrange(satellites[ 8..16]); 716 arrange(satellites[16.. $]); 717 } 718 else 719 if (satellites.length <= 48) 720 { 721 arrange(satellites[ 0.. 8]); 722 arrange(satellites[ 8..24]); 723 arrange(satellites[24.. $]); 724 } 725 else 726 if (satellites.length <= 64) 727 { 728 arrange(satellites[ 0.. 8]); 729 arrange(satellites[ 8..24]); 730 arrange(satellites[24..40]); 731 arrange(satellites[40.. $]); 732 } 733 else 734 { 735 arrange(satellites[ 0.. 8]); 736 arrange(satellites[ 8..24]); 737 arrange(satellites[24..48]); 738 arrange(satellites[48.. $]); 739 } 740 } 741 742 override void die() 743 { 744 dead = true; 745 foreach (s; satellites) 746 s.dead = true; 747 new Explosion(this, 0.060f); 748 } 749 } 750 751 class ThingySatellite : ThingyPart 752 { 753 this() 754 { 755 shapes ~= shape(circle(0, 0, 0.014f)); 756 add(Plane.Enemies); 757 } 758 759 override void step(uint deltaTicks) 760 { 761 if (dead) 762 { 763 death += 0.002f; 764 if (death > 1) 765 remove(); 766 } 767 } 768 769 override void die() 770 { 771 dead = true; 772 } 773 } 774 775 class PlasmaOrb : Enemy 776 { 777 int t; 778 float death = 0f; 779 780 this(Enemy parent) 781 { 782 this.x = parent.x; 783 this.y = parent.y; 784 shapes ~= shape(circle(0, 0, 0.008f)); 785 t = 0; 786 vx = ship.x - parent.x; 787 vy = ship.y - parent.y; 788 float f = 0.0002f/dist(this.vx, this.vy); 789 vx *= f; 790 vy *= f; 791 add(Plane.PlasmaOrbs); 792 } 793 794 override void step(uint deltaTicks) 795 { 796 super.step(deltaTicks); 797 if (x<0 || x>1 || y<0 || y>1) 798 return remove(); 799 if (!dead) 800 { 801 t += deltaTicks; 802 shapes[0].circle.r = 0.008f+0.002f*sin(t/100f); 803 } 804 else 805 { 806 shapes[0].circle.r += 0.000_050f; 807 death += 0.005f; 808 if (death >= 1f) 809 remove(); 810 } 811 } 812 813 override void die() 814 { 815 dead = true; 816 } 817 818 override void render() 819 { 820 auto r = shapes[0].circle.r; 821 auto brightness = 0.75f+0.25f*(-sin(t/100f)); 822 if (!dead) 823 canvas.softCircle(cf(x), cf(y), cf(r-0.003f), cf(r), COLOR(tofracBounded(brightness))); 824 else 825 { 826 brightness *= 1-(death/2); 827 canvas.softRing(cf(x), cf(y), cf(death*r), cf(average(r, death*r)), cf(r), COLOR(tofracBounded(brightness))); 828 } 829 } 830 } 831 832 // ********************************************************* 833 834 class Explosion : GameObject 835 { 836 float size, maxt; 837 int t; 838 839 this(GameObject source, float size) 840 { 841 this.x = source.x; 842 this.y = source.y; 843 this.vx = source.vx; 844 this.vy = source.vy; 845 this.size = size; 846 this.t = 0; 847 this.maxt = size*16500; 848 this.dead = true; 849 add(Plane.Explosions); 850 } 851 852 override void step(uint deltaTicks) 853 { 854 t += deltaTicks; 855 super.step(deltaTicks); 856 857 if (t>maxt) 858 remove; 859 else 860 if (frand() < size) 861 { 862 float tf = t/maxt; // time factor 863 float ex = x+tf*size*frands(); 864 float ey = y+tf*size*frands(); 865 float ed = dist(x-ex, y-ey); 866 if (ed>size) return; 867 float r = 868 frand() * // random factor 869 (1-ed/size); // distance factor 870 new Splode(ex, ey, size/3*r); 871 } 872 } 873 874 override void die() { assert(0); } 875 876 override void render() 877 { 878 } 879 } 880 881 class Splode : GameEntity 882 { 883 float x, y, r, cr; 884 bool growing; 885 886 this(float x, float y, float r) 887 { 888 this.x = x; 889 this.y = y; 890 this.r = r; 891 this.cr = 0; 892 this.growing = true; 893 add(Plane.Explosions); 894 } 895 896 override void step(uint deltaTicks) 897 { 898 const ra = 0.000_020f; 899 if (growing) 900 { 901 cr += ra; 902 if (cr>=r) 903 growing = false; 904 } 905 else 906 { 907 cr -= ra; 908 if (cr<=0) 909 { 910 remove(); 911 return; 912 } 913 } 914 } 915 916 override void render() 917 { 918 //std.stdio.writeln([x, y, cr]); 919 canvas.softCircle(cf(x), cf(y), max(0f, cf(cr)-1.5f), cf(cr), COLOR(tofracBounded(cr/r))); 920 } 921 }