@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-
|
|
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.
|
|
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](
|
|
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.
|
|
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-
|
|
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
|
-
//
|
|
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
|
|
13
|
-
|
|
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
|
|
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({
|
|
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 {
|
|
265
|
-
line(
|
|
266
|
-
line(
|
|
267
|
-
polygon(
|
|
268
|
-
polygon(
|
|
269
|
-
line(
|
|
270
|
-
|
|
271
|
-
line(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
circle(earX, earY,
|
|
278
|
-
|
|
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;
|
|
Binary file
|