@audiofool/moondog 0.2.0-beta.6 → 0.2.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  <p align="center">
2
- <img src="assets/brand/moondog-lunar-record/moondog-logo-1x1.png" width="156" alt="Moondog lunar record">
2
+ <img src="assets/brand/moondog-logo.svg" width="156" alt="Moondog: a dog in headphones inside a moon that is also a record">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Moondog</h1>
6
6
 
7
- <p align="center">Source candidate 0.2.0-beta.6</p>
7
+ <p align="center">Source candidate 0.2.0-beta.7</p>
8
8
 
9
9
  <p align="center"><strong>A listening room that shows its work.</strong></p>
10
10
 
@@ -46,7 +46,7 @@ See [updating and release channels](docs/UPDATES.md), including the one-time upg
46
46
  It shares your profile, settings, sign-ins, and conversations with `moondog`, and draws its own conversation, now playing, and up next.
47
47
  Run it from this checkout with `cd app && npm ci && npm start`.
48
48
 
49
- The [project page](site/README.md) runs the real room in a browser on fictional listening data.
49
+ The [project page](https://moondog.audiofool.ai) plays a short scene in this window, in a browser, with a made-up listener ([how](site/README.md)).
50
50
 
51
51
  ## The sleeve
52
52
 
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><style>svg{color:#141414}@media (prefers-color-scheme:dark){svg{color:#f2f2f2}}</style><defs><mask id="moondog-logo-clear" maskUnits="userSpaceOnUse" x="0" y="0" width="1024" height="1024"><rect width="1024" height="1024" fill="#fff"/><g fill="#000" stroke="#000" stroke-width="12" stroke-linejoin="round"><path d="M447.6 281C447.6 281 469.5 347.5 469.5 347.5C469.5 347.5 477.18 355.23 481 359.8C485.34 364.99 488.83 372.3 494 377.2C499.16 382.09 505.02 385.53 512 389.2C520.53 393.69 531.66 396.8 542 401.2C553.27 405.99 566.45 411 577 417C586.59 422.46 597.08 428.92 603 435.2C607.23 439.69 608.52 444.63 611.5 449C614.37 453.21 617.59 456.92 620.5 461C623.42 465.09 625.62 469.76 629 473.5C632.44 477.31 636.66 480.78 641 483.6C645.33 486.41 649.57 487.98 655 490.4C662.29 493.65 672.51 497.89 681 501C688.95 503.91 697.34 506.69 704.4 508.6C710.08 510.14 715.57 510.59 720 512C723.47 513.1 726.8 513.88 729 515.8C730.97 517.51 732.26 520.06 733 522.5C733.75 524.99 733.69 527.86 733.4 530.6C733.09 533.52 732.21 536.28 731 539.5C729.44 543.64 726.27 548.68 724.3 553.2C722.44 557.47 721.37 561.84 719.4 565.9C717.44 569.94 715.59 573.52 712.5 577.5C708.49 582.67 702.17 588.95 696.5 593.6C691.11 598.02 685.65 601.75 679.4 604.9C672.73 608.26 664.66 610.85 657.5 612.9C650.88 614.79 645.91 616.08 638 617C626.05 618.39 603.34 616.7 592.5 618.1C586.34 618.9 582.32 619.54 578.2 621.5C574.46 623.28 571.15 626.26 568.6 628.9C566.43 631.14 563.5 636 563.5 636C563.5 636 562.99 652.36 563.3 661C563.63 670.32 565.1 679.65 565.6 690C566.18 701.87 566.3 728.3 566.3 728.3C566.3 728.3 552.89 737.15 546.2 740.3C540.12 743.17 528 746.8 528 746.8C528 746.8 525.33 775.16 525.2 790C525.06 805.77 527.5 838.8 527.5 838.8C527.5 838.8 495.67 835.82 480 833.5C464.5 831.21 448.92 828.4 434 825C419.61 821.72 405.95 817.75 392 813.6C377.95 809.42 363.25 806.36 350 800C336.56 793.55 324.56 783.37 312 775C299.56 766.7 286.57 758.33 275 750C264.35 742.33 254.16 735.47 245 727C235.94 718.62 220.3 699.5 220.3 699.5C220.3 699.5 230.67 675.67 236 664C241.24 652.51 247.02 641.02 252 630C256.64 619.72 261.07 610.41 265 600C269.11 589.12 272.18 577.16 276 566C279.75 555.06 284.75 541.79 287.7 533.7C289.48 528.83 292 522 292 522C292 522 330 505 330 505C330 505 358 455 358 455C358 455 359.1 453.67 359.5 452C360.98 445.73 358.92 417.16 359.6 404.95C360.04 397.12 360.65 392.03 361.72 385.75C362.78 379.57 364.21 373.42 365.96 367.57C367.66 361.92 369.78 356.39 372.02 351.21C374.15 346.31 376.4 341.81 379 337.26C381.65 332.6 384.41 327.99 387.82 323.58C391.44 318.88 400.28 310 400.28 310C400.28 310 412.44 299.33 418.61 294.8C424.35 290.56 430.58 285.73 435.99 283.48C440.08 281.78 447.6 281 447.6 281Z"/><path d="M334 452C334 452 334.37 428.09 334.6 418C334.78 409.9 334.47 403.56 335.2 396.1C335.97 388.18 337.4 379.3 339.3 371.8C341 365.08 343.29 359.13 345.7 353.1C348.04 347.24 350.39 341.82 353.5 336.1C356.92 329.83 361.17 323.05 365.6 317.1C369.95 311.26 375.4 305.38 379.8 300.7C383.31 296.96 386.2 293.98 389.7 291C393.17 288.05 396.82 285.42 400.7 282.9C404.73 280.28 409.19 278.07 413.5 275.6C417.92 273.07 422.88 270.29 426.9 267.9C430.24 265.91 433.32 263.03 436 262.3C437.83 261.8 439.62 261.66 441 262.4C442.56 263.24 443.57 265.56 444.6 267.6C445.85 270.08 447.5 273.85 447.6 276.4C447.68 278.32 447.52 280.18 446.3 281.5C444.61 283.33 440.24 283.07 436.7 284.8C431.66 287.26 425.19 291.81 419.5 296C413.39 300.5 406.63 306.03 401.3 311.1C396.63 315.54 392.58 319.87 389 324.5C385.64 328.85 382.92 333.4 380.3 338C377.73 342.51 375.5 346.96 373.4 351.8C371.18 356.93 369.08 362.4 367.4 368C365.66 373.79 364.25 379.89 363.2 386C362.14 392.22 361.54 397.24 361.1 405C360.42 417.16 361 452 361 452C361 452 334 452 334 452Z"/><circle cx="362.5" cy="504" r="67"/><path d="M1017.45 38.94L1028.89 47.84L929.37 175.67L917.92 166.76Z"/><path d="M914.04 165.64L931.4 179.16L926.18 185.86L908.82 172.35Z"/><path d="M902.77 169.54L930.39 191.04L898.75 231.68L869.16 208.64Z"/><path d="M863.2 206.53L900.68 235.71L872.68 268.41L835.2 239.23Z"/><path d="M900.35 228.81L929.55 251.54L924.33 258.24L895.13 235.51Z"/><path d="M845.72 249.32L863.08 262.84L845.33 281.97L831.53 271.22Z"/><path d="M835.27 274.14L841.59 279.05L836.41 283.26L832.46 280.19Z"/><circle cx="926.94" cy="254.89" r="4.25"/></g><circle cx="321.5" cy="236.7" r="49" fill="#000"/><circle cx="712.8" cy="277.2" r="46" fill="#000"/><circle cx="212" cy="490.9" r="37.5" fill="#000"/><circle cx="816.5" cy="618.4" r="26.5" fill="#000"/><circle cx="256.5" cy="786.9" r="40.5" fill="#000"/><circle cx="640.8" cy="848.7" r="50" fill="#000"/></mask><mask id="moondog-logo-cut" maskUnits="userSpaceOnUse" x="0" y="0" width="1024" height="1024"><rect width="1024" height="1024" fill="#fff"/><circle cx="362.5" cy="504" r="72.5" fill="none" stroke="#000" stroke-width="11"/><path d="M459.61 282.74C459.61 282.74 451.67 285.49 448.09 286.7C444.97 287.76 442.59 288.06 439.32 289.64C434.54 291.95 428.27 296.37 422.78 300.41C416.85 304.79 410.21 310.22 405.06 315.12C400.58 319.37 396.74 323.47 393.34 327.88C390.14 332.03 387.57 336.35 385.08 340.73C382.62 345.04 380.47 349.32 378.45 353.97C376.31 358.9 374.29 364.18 372.67 369.57C370.99 375.15 369.63 381.03 368.62 386.92C367.59 392.92 366.96 398.59 366.59 405.25C366.16 413.21 366.6 431.5 366.6 431.5" fill="none" stroke="#000" stroke-width="11"/><circle cx="362.5" cy="504" r="5" fill="none" stroke="#000" stroke-width="3.2"/><circle cx="362.5" cy="504" r="14" fill="none" stroke="#000" stroke-width="3.2"/><circle cx="362.5" cy="504" r="23" fill="none" stroke="#000" stroke-width="3.2"/><circle cx="362.5" cy="504" r="32" fill="none" stroke="#000" stroke-width="3.2"/><circle cx="362.5" cy="504" r="41" fill="none" stroke="#000" stroke-width="3.2"/><circle cx="362.5" cy="504" r="50" fill="none" stroke="#000" stroke-width="3.2"/><circle cx="362.5" cy="504" r="59" fill="none" stroke="#000" stroke-width="3.2"/></mask><mask id="moondog-logo-arm" maskUnits="userSpaceOnUse" x="0" y="0" width="1024" height="1024"><rect width="1024" height="1024" fill="#fff"/><circle cx="907.21" cy="192.32" r="7.5" fill="none" stroke="#000" stroke-width="1.8"/><circle cx="859.72" cy="232.97" r="5.5" fill="#000"/><circle cx="875.11" cy="244.95" r="5.5" fill="#000"/><path d="M859.72 232.97L875.11 244.95" stroke="#000" stroke-width="1.6"/></mask></defs><g mask="url(#moondog-logo-clear)"><circle cx="512" cy="512" r="426.7" fill="none" stroke="currentColor" stroke-width="2.24"/><circle cx="512" cy="512" r="416.95" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="407.2" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="397.45" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="387.7" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="377.95" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="368.2" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="358.45" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="348.7" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="338.95" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="329.2" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="319.45" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="309.7" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="299.95" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="290.2" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="280.45" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="270.7" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="260.95" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="251.2" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="241.45" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="512" cy="512" r="231.7" fill="none" stroke="currentColor" stroke-width="1.4"/></g><path d="M311.61 233.1A10.53 10.53 0 1 1 317.9 246.59" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="321.5" cy="236.7" r="17.94" fill="none" stroke="currentColor" stroke-width="1.61"/><circle cx="321.5" cy="236.7" r="28.08" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="19.5 4.68" stroke-dashoffset="7.8"/><circle cx="321.5" cy="236.7" r="39" fill="none" stroke="currentColor" stroke-width="1.19" stroke-dasharray="10.92 3.9 2.34 3.9"/><path d="M703.67 273.88A9.72 9.72 0 1 1 709.48 286.33" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="712.8" cy="277.2" r="16.56" fill="none" stroke="currentColor" stroke-width="1.61"/><circle cx="712.8" cy="277.2" r="25.92" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="18 4.32" stroke-dashoffset="7.2"/><circle cx="712.8" cy="277.2" r="36" fill="none" stroke="currentColor" stroke-width="1.19" stroke-dasharray="10.08 3.6 2.16 3.6"/><path d="M205.02 488.36A7.43 7.43 0 1 1 209.46 497.88" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="212" cy="490.9" r="12.65" fill="none" stroke="currentColor" stroke-width="1.61"/><circle cx="212" cy="490.9" r="19.8" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="13.75 3.3" stroke-dashoffset="5.5"/><circle cx="212" cy="490.9" r="27.5" fill="none" stroke="currentColor" stroke-width="1.19" stroke-dasharray="7.7 2.75 1.65 2.75"/><path d="M812.31 616.88A4.46 4.46 0 1 1 814.98 622.59" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="816.5" cy="618.4" r="7.59" fill="none" stroke="currentColor" stroke-width="1.61"/><circle cx="816.5" cy="618.4" r="16.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="8.25 1.98" stroke-dashoffset="3.3"/><path d="M248.76 784.08A8.24 8.24 0 1 1 253.68 794.64" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="256.5" cy="786.9" r="14.03" fill="none" stroke="currentColor" stroke-width="1.61"/><circle cx="256.5" cy="786.9" r="21.96" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="15.25 3.66" stroke-dashoffset="6.1"/><circle cx="256.5" cy="786.9" r="30.5" fill="none" stroke="currentColor" stroke-width="1.19" stroke-dasharray="8.54 3.05 1.83 3.05"/><path d="M630.65 845.01A10.8 10.8 0 1 1 637.11 858.85" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="640.8" cy="848.7" r="18.4" fill="none" stroke="currentColor" stroke-width="1.61"/><circle cx="640.8" cy="848.7" r="28.8" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="20 4.8" stroke-dashoffset="8"/><circle cx="640.8" cy="848.7" r="40" fill="none" stroke="currentColor" stroke-width="1.19" stroke-dasharray="11.2 4 2.4 4"/><g mask="url(#moondog-logo-cut)" fill="currentColor"><path d="M447.6 281C447.6 281 469.5 347.5 469.5 347.5C469.5 347.5 477.18 355.23 481 359.8C485.34 364.99 488.83 372.3 494 377.2C499.16 382.09 505.02 385.53 512 389.2C520.53 393.69 531.66 396.8 542 401.2C553.27 405.99 566.45 411 577 417C586.59 422.46 597.08 428.92 603 435.2C607.23 439.69 608.52 444.63 611.5 449C614.37 453.21 617.59 456.92 620.5 461C623.42 465.09 625.62 469.76 629 473.5C632.44 477.31 636.66 480.78 641 483.6C645.33 486.41 649.57 487.98 655 490.4C662.29 493.65 672.51 497.89 681 501C688.95 503.91 697.34 506.69 704.4 508.6C710.08 510.14 715.57 510.59 720 512C723.47 513.1 726.8 513.88 729 515.8C730.97 517.51 732.26 520.06 733 522.5C733.75 524.99 733.69 527.86 733.4 530.6C733.09 533.52 732.21 536.28 731 539.5C729.44 543.64 726.27 548.68 724.3 553.2C722.44 557.47 721.37 561.84 719.4 565.9C717.44 569.94 715.59 573.52 712.5 577.5C708.49 582.67 702.17 588.95 696.5 593.6C691.11 598.02 685.65 601.75 679.4 604.9C672.73 608.26 664.66 610.85 657.5 612.9C650.88 614.79 645.91 616.08 638 617C626.05 618.39 603.34 616.7 592.5 618.1C586.34 618.9 582.32 619.54 578.2 621.5C574.46 623.28 571.15 626.26 568.6 628.9C566.43 631.14 563.5 636 563.5 636C563.5 636 562.99 652.36 563.3 661C563.63 670.32 565.1 679.65 565.6 690C566.18 701.87 566.3 728.3 566.3 728.3C566.3 728.3 552.89 737.15 546.2 740.3C540.12 743.17 528 746.8 528 746.8C528 746.8 525.33 775.16 525.2 790C525.06 805.77 527.5 838.8 527.5 838.8C527.5 838.8 495.67 835.82 480 833.5C464.5 831.21 448.92 828.4 434 825C419.61 821.72 405.95 817.75 392 813.6C377.95 809.42 363.25 806.36 350 800C336.56 793.55 324.56 783.37 312 775C299.56 766.7 286.57 758.33 275 750C264.35 742.33 254.16 735.47 245 727C235.94 718.62 220.3 699.5 220.3 699.5C220.3 699.5 230.67 675.67 236 664C241.24 652.51 247.02 641.02 252 630C256.64 619.72 261.07 610.41 265 600C269.11 589.12 272.18 577.16 276 566C279.75 555.06 284.75 541.79 287.7 533.7C289.48 528.83 292 522 292 522C292 522 330 505 330 505C330 505 358 455 358 455C358 455 359.1 453.67 359.5 452C360.98 445.73 358.92 417.16 359.6 404.95C360.04 397.12 360.65 392.03 361.72 385.75C362.78 379.57 364.21 373.42 365.96 367.57C367.66 361.92 369.78 356.39 372.02 351.21C374.15 346.31 376.4 341.81 379 337.26C381.65 332.6 384.41 327.99 387.82 323.58C391.44 318.88 400.28 310 400.28 310C400.28 310 412.44 299.33 418.61 294.8C424.35 290.56 430.58 285.73 435.99 283.48C440.08 281.78 447.6 281 447.6 281Z"/><path d="M334 452C334 452 334.37 428.09 334.6 418C334.78 409.9 334.47 403.56 335.2 396.1C335.97 388.18 337.4 379.3 339.3 371.8C341 365.08 343.29 359.13 345.7 353.1C348.04 347.24 350.39 341.82 353.5 336.1C356.92 329.83 361.17 323.05 365.6 317.1C369.95 311.26 375.4 305.38 379.8 300.7C383.31 296.96 386.2 293.98 389.7 291C393.17 288.05 396.82 285.42 400.7 282.9C404.73 280.28 409.19 278.07 413.5 275.6C417.92 273.07 422.88 270.29 426.9 267.9C430.24 265.91 433.32 263.03 436 262.3C437.83 261.8 439.62 261.66 441 262.4C442.56 263.24 443.57 265.56 444.6 267.6C445.85 270.08 447.5 273.85 447.6 276.4C447.68 278.32 447.52 280.18 446.3 281.5C444.61 283.33 440.24 283.07 436.7 284.8C431.66 287.26 425.19 291.81 419.5 296C413.39 300.5 406.63 306.03 401.3 311.1C396.63 315.54 392.58 319.87 389 324.5C385.64 328.85 382.92 333.4 380.3 338C377.73 342.51 375.5 346.96 373.4 351.8C371.18 356.93 369.08 362.4 367.4 368C365.66 373.79 364.25 379.89 363.2 386C362.14 392.22 361.54 397.24 361.1 405C360.42 417.16 361 452 361 452C361 452 334 452 334 452Z"/><circle cx="362.5" cy="504" r="67"/></g><g fill="currentColor" mask="url(#moondog-logo-arm)"><path d="M1017.45 38.94L1028.89 47.84L929.37 175.67L917.92 166.76Z"/><path d="M914.04 165.64L931.4 179.16L926.18 185.86L908.82 172.35Z"/><path d="M902.77 169.54L930.39 191.04L898.75 231.68L869.16 208.64Z"/><path d="M863.2 206.53L900.68 235.71L872.68 268.41L835.2 239.23Z"/><path d="M900.35 228.81L929.55 251.54L924.33 258.24L895.13 235.51Z"/><path d="M845.72 249.32L863.08 262.84L845.33 281.97L831.53 271.22Z"/><path d="M835.27 274.14L841.59 279.05L836.41 283.26L832.46 280.19Z"/><circle cx="926.94" cy="254.89" r="4.25"/></g></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audiofool/moondog",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.2.0-beta.7",
4
4
  "type": "module",
5
5
  "description": "Local-first personal AI music agent.",
6
6
  "license": "Apache-2.0",
@@ -18,7 +18,7 @@
18
18
  "moondog": "scripts/moondog"
19
19
  },
20
20
  "files": [
21
- "assets/brand/moondog-lunar-record/moondog-logo-1x1.png",
21
+ "assets/brand/moondog-logo.svg",
22
22
  "contracts/v1",
23
23
  "contracts/v2",
24
24
  "scripts/contract-lib.mjs",
@@ -0,0 +1,237 @@
1
+ // Moondog's logo, drawn from numbers. Every shape is measured from the selected 2026-09-02 picture
2
+ // (assets/brand/moondog-lunar-record): the dog's outline, the headphones, the record's grooves and
3
+ // craters, and the tonearm. The picture's paper and its bar mosaic stay with that key visual; the
4
+ // logo is one ink on nothing, so it sits on any background.
5
+ //
6
+ // Coordinates are the picture's 1024 px square with the record's centre moved to (512, 512).
7
+ // scripts/generate-logo.mjs writes the files in assets/brand, the app icons and the site's images
8
+ // from this module; the window's title-bar mark and the terminal's record draw the same dog.
9
+
10
+ const f = (n) => Number(n.toFixed(2));
11
+ const pt = ([x, y]) => `${f(x)} ${f(y)}`;
12
+
13
+ export const RECORD = Object.freeze({ cx: 512, cy: 512, rim: 426.7, inner: 222, pitch: 9.75 });
14
+
15
+ // The earcup is a small record of its own, its rings on the record's own pitch.
16
+ const CUP = Object.freeze({ cx: 362.5, cy: 504, r: 67, rings: [5, 14, 23, 32, 41, 50, 59] });
17
+
18
+ // The band, its outer edge first, from inside the earcup up over the crown to the ear's tip, then
19
+ // back down its inner edge. Its foot reaches into the earcup, so the gap round the cup cuts it.
20
+ const BAND_OUTER = [[334, 452], [334.6, 418], [335.2, 396.1], [339.3, 371.8], [345.7, 353.1], [353.5, 336.1], [365.6, 317.1],
21
+ [379.8, 300.7], [389.7, 291], [400.7, 282.9], [413.5, 275.6], [426.9, 267.9], [436, 262.3]];
22
+ const BAND_TIP = [[441, 262.4], [444.6, 267.6], [447.6, 276.4]];
23
+ const BAND_INNER = [[446.3, 281.5], [436.7, 284.8], [419.5, 296], [401.3, 311.1], [389, 324.5], [380.3, 338], [373.4, 351.8],
24
+ [367.4, 368], [363.2, 386], [361.1, 405], [361, 452]];
25
+
26
+ // The head, clockwise from the ear's tip: down the ear, the brow and the muzzle, round the nose, back
27
+ // under the jaw, down the throat and the chest, along the bottom and up the back of the neck into
28
+ // the earcup's gap. The corners are the ear, the throat, the chest's step and the bottom's two ends.
29
+ const HEAD = [
30
+ [447.6, 281], [469.5, 347.5],
31
+ [481, 359.8], [494, 377.2], [512, 389.2], [542, 401.2], [577, 417], [603, 435.2],
32
+ [611.5, 449], [620.5, 461], [629, 473.5], [641, 483.6], [655, 490.4], [681, 501], [704.4, 508.6], [720, 512],
33
+ [729, 515.8], [733, 522.5], [733.4, 530.6], [731, 539.5],
34
+ [724.3, 553.2], [719.4, 565.9], [712.5, 577.5], [696.5, 593.6], [679.4, 604.9], [657.5, 612.9], [638, 617],
35
+ [592.5, 618.1], [578.2, 621.5], [568.6, 628.9], [563.5, 636],
36
+ [563.3, 661], [565.6, 690], [566.3, 728.3],
37
+ [546.2, 740.3], [528, 746.8],
38
+ [525.2, 790], [527.5, 838.8],
39
+ [480, 833.5], [434, 825], [392, 813.6], [350, 800], [312, 775], [275, 750], [245, 727], [220.3, 699.5],
40
+ [236, 664], [252, 630], [265, 600], [276, 566], [287.7, 533.7], [292, 522],
41
+ ];
42
+ const HEAD_CORNERS = [0, 1, 30, 33, 35, 37, 45];
43
+
44
+ // Craters, each an inner arc, a ring and one or two dashed rings.
45
+ const CRATERS = [
46
+ { cx: 321.5, cy: 236.7, r: 39 }, { cx: 712.8, cy: 277.2, r: 36 }, { cx: 212, cy: 490.9, r: 27.5 },
47
+ { cx: 816.5, cy: 618.4, r: 16.5 }, { cx: 256.5, cy: 786.9, r: 30.5 }, { cx: 640.8, cy: 848.7, r: 40 },
48
+ ];
49
+
50
+ // The tonearm in its own coordinates: t runs down the arm from the headshell's middle, s across it.
51
+ const ARM = Object.freeze({ origin: [900.3, 201.2], down: [-0.61433, 0.78905] });
52
+
53
+ // A centripetal Catmull-Rom curve through the nodes, as cubic Béziers: it passes through every node
54
+ // and never loops at a sharp turn. A node in `corners` keeps its point; the curve meets it straight.
55
+ function segments(nodes, { closed = true, corners = [] } = {}) {
56
+ const sharp = new Set(corners);
57
+ const count = nodes.length;
58
+ const at = (index) => (closed ? nodes[(index + count) % count] : nodes[Math.max(0, Math.min(count - 1, index))]);
59
+ const tangent = (a, b, c, index) => {
60
+ if (sharp.has((index + count) % count)) return [0, 0];
61
+ const before = Math.hypot(b[0] - a[0], b[1] - a[1]) ** 0.5 || 1e-6;
62
+ const after = Math.hypot(c[0] - b[0], c[1] - b[1]) ** 0.5 || 1e-6;
63
+ return [0, 1].map((axis) => (b[axis] - a[axis]) / before - (c[axis] - a[axis]) / (before + after) + (c[axis] - b[axis]) / after);
64
+ };
65
+ const list = [];
66
+ for (let index = 0; index < (closed ? count : count - 1); index += 1) {
67
+ const [p0, p1, p2, p3] = [at(index - 1), at(index), at(index + 1), at(index + 2)];
68
+ const span = Math.hypot(p2[0] - p1[0], p2[1] - p1[1]) ** 0.5;
69
+ const [m1, m2] = [tangent(p0, p1, p2, index), tangent(p1, p2, p3, index + 1)];
70
+ list.push([p1, [p1[0] + m1[0] * span / 3, p1[1] + m1[1] * span / 3], [p2[0] - m2[0] * span / 3, p2[1] - m2[1] * span / 3], p2]);
71
+ }
72
+ return { list, closed };
73
+ }
74
+ const toPath = ({ list, closed }) => `M${pt(list[0][0])}${list.map(([, c1, c2, end]) => `C${pt(c1)} ${pt(c2)} ${pt(end)}`).join("")}${closed ? "Z" : ""}`;
75
+ function toPoints({ list }, steps = 8) {
76
+ const points = [];
77
+ for (const [a, b, c, d] of list) {
78
+ for (let step = 0; step < steps; step += 1) {
79
+ const t = step / steps, s = 1 - t;
80
+ points.push([0, 1].map((axis) => s ** 3 * a[axis] + 3 * s * s * t * b[axis] + 3 * s * t * t * c[axis] + t ** 3 * d[axis]));
81
+ }
82
+ }
83
+ points.push(list.at(-1)[3]);
84
+ return points;
85
+ }
86
+
87
+ // Moves each node of an open line sideways by `by`: positive is to the right of the way it runs.
88
+ function offset(nodes, by) {
89
+ return nodes.map((point, index) => {
90
+ const a = nodes[Math.max(0, index - 1)], b = nodes[Math.min(nodes.length - 1, index + 1)];
91
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
92
+ return [point[0] - ((b[1] - a[1]) / length) * by, point[1] + ((b[0] - a[0]) / length) * by];
93
+ });
94
+ }
95
+
96
+ // The dog's three shapes and the gap between band and head. The head closes through the earcup and
97
+ // just inside the band, so head and band overlap without a seam; the gaps are cut from all three.
98
+ function figure(gap) {
99
+ const band = segments([...BAND_OUTER, ...BAND_TIP, ...BAND_INNER], { corners: [0, BAND_OUTER.length + BAND_TIP.length + BAND_INNER.length - 1] });
100
+ const along = offset([...BAND_INNER].reverse(), -1.5).slice(0, -1);
101
+ const head = segments([...HEAD, [330, 505], [358, 455], ...along], {
102
+ corners: [...HEAD_CORNERS, HEAD.length - 1, HEAD.length, HEAD.length + 1, HEAD.length + along.length - 1],
103
+ });
104
+ // The band's gap runs beside its inner edge, from the middle of the earcup's gap out past the ear.
105
+ const edge = [...BAND_INNER.slice(0, -1), [361.1, CUP.cy - CUP.r - gap / 2]];
106
+ const [first, second] = edge;
107
+ const bandGap = segments(offset([[first[0] + (first[0] - second[0]) * 1.2, first[1] + (first[1] - second[1]) * 1.2], ...edge], -gap / 2), { closed: false });
108
+ return { head, band, bandGap };
109
+ }
110
+
111
+ const toArm = (t, s) => [ARM.origin[0] + t * ARM.down[0] + s * ARM.down[1], ARM.origin[1] + t * ARM.down[1] - s * ARM.down[0]];
112
+ const quad = (corners) => `M${corners.map((corner) => pt(toArm(...corner))).join("L")}Z`;
113
+ // The full tonearm: tube, collar, headshell, the block under it, finger lift, cartridge, stylus.
114
+ function tonearm(reach) {
115
+ return {
116
+ parts: [
117
+ quad([[-reach, -7.25], [-reach, 7.25], [-38, 7.25], [-38, -7.25]]), quad([[-36.5, -11], [-36.5, 11], [-28, 11], [-28, -11]]),
118
+ quad([[-26.5, -17.5], [-26.5, 17.5], [25, 17.5], [25, -20]]), quad([[27, -26], [27, 21.5], [70, 19.5], [70, -28]]),
119
+ quad([[21.75, 17], [21.75, 54], [30.25, 54], [30.25, 17]]),
120
+ quad([[71.5, -13.5], [71.5, 8.5], [97.5, 6.25], [97.5, -11.25]]), quad([[97.5, -6.5], [97.5, 1.5], [104, 0], [104, -5]]),
121
+ ],
122
+ liftEnd: toArm(26, 54),
123
+ screw: toArm(-11.25, 0),
124
+ holes: [toArm(50, -12.5), toArm(50, 7)],
125
+ };
126
+ }
127
+ // The small version, for 64 px and below: the rim and two heavy grooves, the clear margin they keep
128
+ // from the dog, gaps wide enough to last, and the earcup's one ring and its middle.
129
+ const SMALL = Object.freeze({ gap: 30, halo: 22, grooves: [[RECORD.rim - 15, 30], [362, 22], [312, 22]], cupRing: [34, 14], cupDot: 14 });
130
+ // Its arm is one tube, one headshell and a cartridge, heavy enough to show at 32 px.
131
+ const heavyArm = (reach) => [[[-reach, -15], [-reach, 15], [-30, 15], [-30, -15]], [[-26, -30], [-26, 26], [74, 24], [74, -32]],
132
+ [[78, -17], [78, 11], [104, 8], [104, -14]]];
133
+
134
+ function arc(cx, cy, r, from, to) {
135
+ const point = (angle) => [cx + r * Math.cos((angle * Math.PI) / 180), cy + r * Math.sin((angle * Math.PI) / 180)];
136
+ return `M${pt(point(from))}A${f(r)} ${f(r)} 0 ${to - from > 180 ? 1 : 0} 1 ${pt(point(to))}`;
137
+ }
138
+ const ring = (cx, cy, r, width, paint, more = "") =>
139
+ `<circle cx="${f(cx)}" cy="${f(cy)}" r="${f(r)}" fill="none" stroke="${paint}" stroke-width="${f(width)}"${more}/>`;
140
+ const crater = ({ cx, cy, r }, ink, line) => {
141
+ const large = r > 25;
142
+ return [
143
+ `<path d="${arc(cx, cy, r * 0.27, 200, 470)}" fill="none" stroke="${ink}" stroke-width="${line}" stroke-linecap="round"/>`,
144
+ ring(cx, cy, r * 0.46, line * 1.15, ink),
145
+ ring(cx, cy, r * (large ? 0.72 : 1), line, ink, ` stroke-dasharray="${f(r * 0.5)} ${f(r * 0.12)}" stroke-dashoffset="${f(r * 0.2)}"`),
146
+ large ? ring(cx, cy, r, line * 0.85, ink, ` stroke-dasharray="${f(r * 0.28)} ${f(r * 0.1)} ${f(r * 0.06)} ${f(r * 0.1)}"`) : "",
147
+ ].join("");
148
+ };
149
+ // A standalone file takes the reader's light or dark; drawn inside a page, the logo takes the page's colour.
150
+ const SCHEME = "<style>svg{color:#141414}@media (prefers-color-scheme:dark){svg{color:#f2f2f2}}</style>";
151
+
152
+ /**
153
+ * The logo as SVG. The full logo has every groove, the craters and the whole tonearm. `small` is
154
+ * the one for 64 px and below: the rim, two heavy grooves, a heavy tonearm, and gaps wide enough
155
+ * to last. `ink` paints it; `standalone` adds the light and dark colours a file on its own needs.
156
+ * `id` keeps the masks of several logos on one page apart.
157
+ */
158
+ export function logoSvg({ small = false, ink = "currentColor", standalone = false, id = "moondog-logo", reach = 200, line = 1.4 } = {}) {
159
+ const gap = small ? SMALL.gap : 11;
160
+ const halo = small ? SMALL.halo : 6;
161
+ const { head, band, bandGap } = figure(gap);
162
+ const arm = tonearm(reach);
163
+ const armParts = small ? heavyArm(reach + 60).map(quad) : arm.parts;
164
+ const liftEnd = small ? "" : `<circle cx="${f(arm.liftEnd[0])}" cy="${f(arm.liftEnd[1])}" r="4.25"/>`;
165
+ const dog = `<path d="${toPath(head)}"/><path d="${toPath(band)}"/><circle cx="${CUP.cx}" cy="${CUP.cy}" r="${CUP.r}"/>`;
166
+ const grooves = [];
167
+ if (small) grooves.push(...SMALL.grooves);
168
+ else for (let r = RECORD.rim, index = 0; r >= RECORD.inner - 0.01; r -= RECORD.pitch, index += 1) grooves.push([r, index ? line : line * 1.6]);
169
+ const cupRings = small
170
+ ? `${ring(CUP.cx, CUP.cy, ...SMALL.cupRing, "#000")}<circle cx="${CUP.cx}" cy="${CUP.cy}" r="${SMALL.cupDot}" fill="#000"/>`
171
+ : CUP.rings.map((r) => ring(CUP.cx, CUP.cy, r, 3.2, "#000")).join("");
172
+ const mask = (name, body) => `<mask id="${id}-${name}" maskUnits="userSpaceOnUse" x="0" y="0" width="1024" height="1024"><rect width="1024" height="1024" fill="#fff"/>${body}</mask>`;
173
+ // Grooves stop short of the dog, the arm and the craters; the gaps cut the dog into head, band and cup.
174
+ const clear = `<g fill="#000" stroke="#000" stroke-width="${halo * 2}" stroke-linejoin="round">${dog}${armParts.map((d) => `<path d="${d}"/>`).join("")}${liftEnd}</g>`
175
+ + (small ? "" : CRATERS.map((c) => `<circle cx="${c.cx}" cy="${c.cy}" r="${c.r + 10}" fill="#000"/>`).join(""));
176
+ const cuts = `${ring(CUP.cx, CUP.cy, CUP.r + gap / 2, gap, "#000")}<path d="${toPath(bandGap)}" fill="none" stroke="#000" stroke-width="${gap}"/>${cupRings}`;
177
+ const armDetail = small ? "" : ring(...arm.screw, 7.5, 1.8, "#000") + arm.holes.map(([x, y]) => `<circle cx="${f(x)}" cy="${f(y)}" r="5.5" fill="#000"/>`).join("")
178
+ + `<path d="M${pt(arm.holes[0])}L${pt(arm.holes[1])}" stroke="#000" stroke-width="1.6"/>`;
179
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${small ? "40 40 944 944" : "0 0 1024 1024"}">${standalone ? SCHEME : ""}`
180
+ + `<defs>${mask("clear", clear)}${mask("cut", cuts)}${small ? "" : mask("arm", armDetail)}</defs>`
181
+ + `<g mask="url(#${id}-clear)">${grooves.map(([r, width]) => ring(RECORD.cx, RECORD.cy, r, width, ink)).join("")}</g>`
182
+ + (small ? "" : CRATERS.map((c) => crater(c, ink, line)).join(""))
183
+ + `<g mask="url(#${id}-cut)" fill="${ink}">${dog}</g>`
184
+ + `<g fill="${ink}"${small ? "" : ` mask="url(#${id}-arm)"`}>${armParts.map((d) => `<path d="${d}"/>`).join("")}${liftEnd}</g></svg>`;
185
+ }
186
+
187
+ /**
188
+ * The app icon's tile on its 1024 canvas: a superellipse, the shape of a macOS icon, `half` from the
189
+ * center to each side. Larger halves draw the same shape round the tile, as a ring that follows it.
190
+ */
191
+ export function iconShape(half = 412) {
192
+ const corner = Array.from({ length: 160 }, (_, index) => {
193
+ const angle = (index / 160) * 2 * Math.PI, cos = Math.cos(angle), sin = Math.sin(angle);
194
+ return [512 + half * Math.sign(cos) * Math.abs(cos) ** 0.4, 512 + half * Math.sign(sin) * Math.abs(sin) ** 0.4];
195
+ });
196
+ return `M${corner.map(pt).join("L")}Z`;
197
+ }
198
+
199
+ /**
200
+ * The Mac app icon: the full logo on a rounded square of Apple's 824 px grid, its tonearm reaching in
201
+ * from beyond the tile's corner. Light is the app's own icon; dark is the one the listener can choose.
202
+ */
203
+ export function appIconSvg({ dark = false, id = "moondog-icon" } = {}) {
204
+ const [tile, ink] = dark ? ["#151515", "#f2f2f2"] : ["#ffffff", "#141414"];
205
+ const shape = iconShape();
206
+ const art = logoSvg({ ink, id, reach: 340, line: 1.8 }).replace(/^<svg[^>]*>/u, "").replace(/<\/svg>$/u, "");
207
+ // The soft shadow under the tile that Mac icons draw in their own margin.
208
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><defs><clipPath id="${id}-tile"><path d="${shape}"/></clipPath>`
209
+ + `<filter id="${id}-shadow" x="-10%" y="-10%" width="120%" height="125%"><feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#000" flood-opacity="0.28"/></filter></defs>`
210
+ + `<path d="${shape}" fill="${tile}" filter="url(#${id}-shadow)"/><g clip-path="url(#${id}-tile)"><g transform="translate(512 512) scale(0.74) translate(-512 -512)">${art}</g></g></svg>`;
211
+ }
212
+
213
+ /**
214
+ * The small logo for surfaces that draw it themselves, such as the window's title-bar mark and the
215
+ * terminal's record, in the record's unit coordinates (the disc has radius 1 around 0, 0): the dog's
216
+ * outlines as points, its gaps, the rim and grooves as [radius, width], the margin they keep from the
217
+ * dog, and the heavy arm's parts, its tube `reach` long.
218
+ */
219
+ export function logoFigure({ reach = 190 } = {}) {
220
+ const { head, band, bandGap } = figure(SMALL.gap);
221
+ const scale = (length) => length / RECORD.rim;
222
+ const unit = ([x, y]) => [scale(x - RECORD.cx), scale(y - RECORD.cy)];
223
+ return {
224
+ // The head to fill, and its outer edge alone, from the ear's tip round to the back of the neck.
225
+ head: toPoints(head).map(unit),
226
+ headEdge: toPoints(segments(HEAD, { closed: false, corners: HEAD_CORNERS })).map(unit),
227
+ // The band to fill, and the line down its middle, from the ear's tip to the earcup.
228
+ band: toPoints(band).map(unit),
229
+ bandCenter: toPoints(segments(offset(BAND_INNER, 12.5), { closed: false })).map(unit),
230
+ bandGap: toPoints(bandGap, 4).map(unit),
231
+ gap: scale(SMALL.gap),
232
+ cup: { center: unit([CUP.cx, CUP.cy]), radius: scale(CUP.r), ring: scale(SMALL.cupRing[0]), ringWidth: scale(SMALL.cupRing[1]), dot: scale(SMALL.cupDot) },
233
+ grooves: SMALL.grooves.map(([radius, width]) => [scale(radius), scale(width)]),
234
+ halo: scale(SMALL.halo),
235
+ tonearm: heavyArm(reach).map((corners) => corners.map((corner) => unit(toArm(...corner)))),
236
+ };
237
+ }
@@ -1,3 +1,5 @@
1
+ import { logoFigure } from "../brand/logo.mjs";
2
+
1
3
  export const LOGO_MOTION_FRAMES = 64;
2
4
  export const LOGO_MOTION_INTERVAL_MS = 125;
3
5
 
@@ -6,40 +8,12 @@ const turn = Math.PI * 2;
6
8
  /** Record layers from faintest to strongest, matching the logo's fine grooves and solid dog. */
7
9
  export const RECORD_LAYERS = Object.freeze({ groove: 1, surface: 2, figure: 3 });
8
10
 
9
- // The dog's eye, in the record's unit coordinates; layouts line the title up with it.
11
+ // Where the dog's eye would be, in the record's unit coordinates; layouts line the title up with it.
12
+ // The logo draws no eye, so neither does the record.
10
13
  const eye = [0.22, -0.13];
11
14
 
12
- // The dog's outline runs through these points as one smooth curve (smoothOutline, below), so it
13
- // keeps its shape without the facets of straight segments between them.
14
- const dogPoints = [
15
- [-0.62, 0.48], [-0.55, 0.29], [-0.47, 0.08], [-0.42, -0.16],
16
- [-0.4, -0.34], [-0.32, -0.47], [-0.18, -0.61], [-0.17, -0.44],
17
- [-0.12, -0.43], [-0.1, -0.32], [0.02, -0.27], [0.16, -0.24],
18
- [0.24, -0.18], [0.28, -0.08], [0.35, -0.03], [0.53, 0.015],
19
- [0.55, 0.055], [0.53, 0.16], [0.46, 0.22], [0.31, 0.26],
20
- [0.16, 0.26], [0.13, 0.33], [0.13, 0.56], [0.045, 0.58],
21
- [0.07, 0.77], [-0.13, 0.75], [-0.3, 0.7], [-0.48, 0.62],
22
- ];
23
-
24
- // A closed centripetal Catmull-Rom curve through `points`: it passes through every point, and unlike
25
- // the uniform kind it never loops or overshoots at the sharp turns of the ear and the nose.
26
- function smoothOutline(points, steps = 10) {
27
- const outline = [];
28
- const count = points.length;
29
- for (let index = 0; index < count; index += 1) {
30
- const p = [-1, 0, 1, 2].map((offset) => points[(index + offset + count) % count]);
31
- const t = [0];
32
- for (let at = 1; at < 4; at += 1) t.push(t[at - 1] + Math.hypot(p[at][0] - p[at - 1][0], p[at][1] - p[at - 1][1]) ** 0.5);
33
- for (let step = 0; step < steps; step += 1) {
34
- const u = t[1] + (t[2] - t[1]) * step / steps;
35
- const mix = (a, b, from, to) => a.map((value, axis) => ((to - u) * value + (u - from) * b[axis]) / (to - from));
36
- const [a1, a2, a3] = [mix(p[0], p[1], t[0], t[1]), mix(p[1], p[2], t[1], t[2]), mix(p[2], p[3], t[2], t[3])];
37
- outline.push(mix(mix(a1, a2, t[0], t[2]), mix(a2, a3, t[1], t[3]), t[1], t[2]));
38
- }
39
- }
40
- return outline;
41
- }
42
- const dog = smoothOutline(dogPoints);
15
+ // The dog and its headphones, measured from the logo (src/surfaces/brand/logo.mjs).
16
+ const figure = logoFigure();
43
17
 
44
18
  const smallAscii = [
45
19
  " /",
@@ -111,17 +85,6 @@ function insidePolygon(x, y, points) {
111
85
  return inside;
112
86
  }
113
87
 
114
- function bezier(start, first, second, end) {
115
- return Array.from({ length: 65 }, (_, index) => {
116
- const t = index / 64;
117
- const s = 1 - t;
118
- return [0, 1].map((axis) =>
119
- s ** 3 * start[axis] + 3 * s ** 2 * t * first[axis]
120
- + 3 * s * t ** 2 * second[axis] + t ** 3 * end[axis],
121
- );
122
- });
123
- }
124
-
125
88
  function drawing(columns, rows, cellAspect) {
126
89
  const width = columns * 2;
127
90
  const height = rows * 4;
@@ -182,11 +145,8 @@ function drawing(columns, rows, cellAspect) {
182
145
  return { dots, width, height, radius, centerX, centerY, pixel, circle, disc, line, polygon, paintLayer };
183
146
  }
184
147
 
185
- // The portrait and the tonearm in the record's unit coordinates: the disc has radius 1 around 0,0.
148
+ // The tonearm in the record's unit coordinates: the disc has radius 1 around 0,0.
186
149
  const shapes = {
187
- headband: bezier([-0.42, -0.03], [-0.47, -0.31], [-0.27, -0.58], [-0.18, -0.59]),
188
- ear: [-0.365, -0.005],
189
- nose: [0.53, 0.045],
190
150
  arm: [[1.17, -1.095], [0.91, -0.8]],
191
151
  cartridge: [[0.92, -0.845], [0.998, -0.777], [0.808, -0.567], [0.73, -0.635]],
192
152
  // Two short highlights ride fixed grooves as the record turns: the groove's place from the rim, its angle, its length.
@@ -195,7 +155,7 @@ const shapes = {
195
155
 
196
156
  /** The shapes the record is drawn from, for surfaces that draw it as vectors, such as the window's small mark. */
197
157
  export function lunarRecordShapes() {
198
- return structuredClone({ dog, eye, ...shapes, earRadius: 0.17, grooves: [1, 0.91, 0.8, 0.66] });
158
+ return structuredClone({ eye, ...shapes, grooves: [1, 0.91, 0.8, 0.66] });
199
159
  }
200
160
 
201
161
  // The tonearm swings about its pivot, the arm's first point; a negative swing turns it off the record.
@@ -261,25 +221,23 @@ function paint(columns, rows, phase, cellAspect, tonearm = true) {
261
221
 
262
222
  paintLayer(RECORD_LAYERS.figure);
263
223
  // Like the logo, a clear halo separates the dog from the grooves around it.
264
- const { headband, ear, nose } = shapes;
265
- line([...dog, dog[0]], pixel * 4.5, false);
266
- line(headband, Math.max(pixel * 7, 0.11), false);
267
- polygon(dog, false);
268
- polygon(dog, true, true);
269
- line([...dog, dog[0]], pixel * 0.8);
270
-
271
- line(headband, Math.max(pixel * 3.5, 0.065));
272
- line(headband, Math.max(pixel * 1.2, 0.025), false);
273
- const [earX, earY] = ear;
274
- const earRadius = detail > 0 ? 0.157 : 0.17;
275
- disc(earX, earY, earRadius + pixel * 1.6, false);
276
- circle(earX, earY, earRadius, pixel * 1.1);
277
- circle(earX, earY, earRadius * 0.69, pixel);
278
- if (detail > 0) circle(earX, earY, earRadius * 0.35, pixel);
224
+ const { head, headEdge, bandCenter, bandGap, gap, cup } = figure;
225
+ line(headEdge, pixel * 4.5, false);
226
+ line(bandCenter, Math.max(pixel * 7, 0.11), false);
227
+ polygon(head, false);
228
+ polygon(head, true, true);
229
+ line(headEdge, pixel * 0.8);
230
+ // As in the logo, a gap parts the band from the head.
231
+ line(bandGap, Math.max(gap, pixel * 2), false);
232
+
233
+ line(bandCenter, Math.max(pixel * 3.5, 0.065));
234
+ line(bandCenter, Math.max(pixel * 1.2, 0.025), false);
235
+ const [earX, earY] = cup.center;
236
+ disc(earX, earY, cup.radius + pixel * 1.6, false);
237
+ circle(earX, earY, cup.radius, pixel * 1.1);
238
+ circle(earX, earY, cup.radius * 0.69, pixel);
239
+ if (detail > 0) circle(earX, earY, cup.radius * 0.35, pixel);
279
240
  else disc(earX, earY, pixel * 0.9);
280
- // A small eye and nose keep the dog readable before the hatch pattern resolves.
281
- disc(eye[0], eye[1], pixel * 1.05, false);
282
- disc(nose[0], nose[1], pixel * 0.85);
283
241
 
284
242
  if (tonearm) paintTonearm(art, rotation);
285
243
  return art;