Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

notepat: pixel-art dancing harpist sprite in harp mode

Tiny 18×22 pixel-art sprite rendered at 2× block size (36×44 px on
screen) in the empty center space between the left + right grids,
shown only when wave === "harp". She has two animation frames that
swap every 6 frames when harp notes are actively playing and every
14 frames while idle, producing a faster "dance" during play and a
gentle idle sway otherwise.

Design:
- Centered brown hair, peach skin, pink lips
- Purple dress with darker skirt panel (split legs for the sitting
cross-legged posture)
- Gold harp frame + strings to her right
- Strings flash bright yellow within 10 frames of any keypress
("pluckFresh") for a little shimmer when notes trigger

The sprite is defined as two multi-line strings with a tiny char→color
palette (H/S/M/L/D/d/=/F). Rendering is a single nested loop of
block-sized box() calls — ~200 filled rects per frame, cheap enough
to draw every frame without hurting FPS.

Positioning: centered horizontally between leftX+gridW and rightX,
vertically just above the record strip so the existing per-lane wave
bars remain visible on either side of her. Bounded above by topBarH+2
so she won't overlap the status bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+115
+115
fedac/native/pieces/notepat.mjs
··· 4760 4760 } 4761 4761 } 4762 4762 4763 + // === HARPIST SPRITE (harp mode only) === 4764 + // Tiny pixel-art lady playing a harp, shown in the empty center 4765 + // space between the left + right grids when wave === "harp". Has 4766 + // a 2-frame dance animation keyed to harp-note activity: when a 4767 + // harp note has played within the last ~30 frames, she animates 4768 + // faster (keyed from frame%). Idle sway otherwise. 4769 + if (wave === "harp") { 4770 + // Color palette 4771 + const HAIR = dark ? [180, 90, 60] : [140, 60, 30]; 4772 + const SKIN = dark ? [240, 200, 170] : [240, 200, 170]; 4773 + const LIPS = dark ? [220, 90, 100] : [200, 60, 70]; 4774 + const DRESS = dark ? [180, 120, 220] : [150, 80, 200]; 4775 + const DRESS_D = dark ? [120, 70, 160] : [100, 50, 150]; 4776 + const STRING = dark ? [255, 230, 140] : [200, 160, 40]; 4777 + const STRING_A= dark ? [255, 255, 200] : [240, 200, 80]; 4778 + const FRAME_C = dark ? [220, 180, 80] : [180, 120, 40]; 4779 + // 18×22 sprite grid. '.' = transparent. Block size is 2 so the 4780 + // whole thing is 36×44 px at native resolution — roughly fills 4781 + // the center space above the record strip. 4782 + const SPRITE_A = [ 4783 + "....HHHHHH........", 4784 + "...HHHHHHHH.......", 4785 + "..HHSSHHSSHH......", 4786 + "..HHSSHHSSHH......", 4787 + "..HHHHMMHHHH......", 4788 + "..HHHLLLLHHH......", 4789 + "...HHHHHHHH.......", 4790 + "....SSSSSS........", 4791 + "...DDDDDDDD..FF...", 4792 + "..DDDDDDDDDD.FF...", 4793 + "..DDDDDDDDDDFF....", 4794 + "..DDDDdDDDDD=F....", 4795 + "..DDDDddDDDD==F...", 4796 + "..DDDDdDdDDD=F....", 4797 + "...DDDDDDDD.F=F...", 4798 + "....dddddd..F=F...", 4799 + "....dd..dd..F=F...", 4800 + "....dd..dd..FFF...", 4801 + "...ddd..ddd.......", 4802 + "..ddd....ddd......", 4803 + "..ddd....ddd......", 4804 + "..ddd....ddd......", 4805 + ]; 4806 + const SPRITE_B = [ 4807 + "....HHHHHH........", 4808 + "...HHHHHHHH.......", 4809 + "..HHSSHHSSHH......", 4810 + "..HHSSHHSSHH......", 4811 + "..HHHHMMHHHH......", 4812 + "..HHHLLLLHHH......", 4813 + "...HHHHHHHH.......", 4814 + "....SSSSSS........", 4815 + "...DDDDDDDD.FF....", 4816 + "..DDDDDDDDDDFF....", 4817 + "..DDDDDDDDDD=F....", 4818 + "..DDDDdDDDDD==F...", 4819 + "..DDDDddDDDD=F....", 4820 + "..DDDDdDdDDDF=F...", 4821 + "...DDDDDDDDFF=F...", 4822 + "....dddddd.F=F....", 4823 + "....dd..dd.FFF....", 4824 + "....dd..dd........", 4825 + "...ddd..ddd.......", 4826 + "..ddd....ddd......", 4827 + "..ddd....ddd......", 4828 + "..ddd....ddd......", 4829 + ]; 4830 + const colorFor = (ch) => { 4831 + switch (ch) { 4832 + case "H": return HAIR; 4833 + case "S": return SKIN; 4834 + case "M": return LIPS; 4835 + case "L": return LIPS; 4836 + case "D": return DRESS; 4837 + case "d": return DRESS_D; 4838 + case "=": return STRING; 4839 + case "F": return FRAME_C; 4840 + default: return null; 4841 + } 4842 + }; 4843 + // Track harp-note activity to switch the animation speed. 4844 + const harpActive = activeCount > 0 || 4845 + (frame - lastKeyFrame) < 30; 4846 + // Faster sway when playing: swap frames every 6 frames; slower 4847 + // idle sway every 14 frames. 4848 + const swapEvery = harpActive ? 6 : 14; 4849 + const sprite = Math.floor(frame / swapEvery) % 2 === 0 ? SPRITE_A : SPRITE_B; 4850 + // Center the sprite between the left-grid right edge and the 4851 + // right-grid left edge. Place just above the record strip. 4852 + const spW = sprite[0].length; 4853 + const spH = sprite.length; 4854 + const block = 2; 4855 + const spriteW = spW * block; 4856 + const spriteH = spH * block; 4857 + const betweenX0 = leftX + gridW; 4858 + const betweenX1 = rightX; 4859 + const sx = Math.floor((betweenX0 + betweenX1 - spriteW) / 2); 4860 + const sy = Math.max(topBarH + 2, recordStripTop - spriteH - 2); 4861 + // When the player hits a harp note, briefly flash the STRING 4862 + // color brighter. Indexed by plucked-age for a simple shimmer. 4863 + const pluckFresh = (frame - lastKeyFrame) < 10; 4864 + for (let row = 0; row < spH; row++) { 4865 + const line = sprite[row]; 4866 + for (let col = 0; col < spW; col++) { 4867 + const ch = line[col]; 4868 + if (ch === "." || ch === " ") continue; 4869 + let color = colorFor(ch); 4870 + if (!color) continue; 4871 + if (ch === "=" && pluckFresh) color = STRING_A; 4872 + ink(color[0], color[1], color[2]); 4873 + box(sx + col * block, sy + row * block, block, block, true); 4874 + } 4875 + } 4876 + } 4877 + 4763 4878 // Expose grid layout for touch hit-testing in act() 4764 4879 globalThis.__gridInfo = { leftX, rightX, gridTop, btnW, btnH, gap }; 4765 4880