@bitefoundry/retrieviewer 0.0.5 → 0.0.6

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.
@@ -35,6 +35,13 @@ must be checked again for the final release bytes.
35
35
 
36
36
  ## Remaining limits
37
37
 
38
+ 0.0.6 re-registers only the36 watch samples of the existing behavior archive.
39
+ The existing seated body/paws stay fixed while the authored head turns; no new
40
+ image/photo/video reference is introduced. The offline preparation and source
41
+ digest are in tools/prepare-watch-art.* and pet-q5-manifest.json. Butterfly wings
42
+ are small geometric marks, using the same existing output buffer; dog poses
43
+ reuse licensed gallop art. Existing attribution and CC BY-SA4.0 terms remain.
44
+
38
45
  0.0.5 extends that ninth licensed archive with four sleeping studies generated
39
46
  from the same existing puppy identity/pose references, approved by the owner in
40
47
  this conversation. COMFORT-ART.md and the comfort metadata record preparation,
package/BUTTERFLY.md ADDED
@@ -0,0 +1,30 @@
1
+ # Rare butterfly encounter (0.0.6)
2
+
3
+ No additional dog artwork, sprite archive, dependency, background process or
4
+ timer. Existing short grounded gallops supply the bounding legs and ear motion;
5
+ the visitor is drawn directly into the existing grayscale buffer before Braille
6
+ conversion. Camera, stride-distance integration, contact correction and palette
7
+ are unchanged. This is not a new up-looking/pouncing source illustration.
8
+
9
+ The initial opportunity is scheduled after240–480 active simulation seconds.
10
+ Only an awake, energetic puppy near the viewer may participate; sleep pressure
11
+ takes priority. Subsequent opportunities are at least720 seconds apart, with
12
+ up to480 additional random seconds. Hidden/paused time does not accumulate.
13
+ An opportunity can be delayed by sleep or another coherent episode, rather than
14
+ forcing a timer callback that wakes or interrupts the puppy.
15
+
16
+ The puppy notices, makes three short alternating chases, watches the visitor fly
17
+ away, returns and rests. Each target keeps the butterfly ahead of the muzzle.
18
+ The visitor has an18-second visibility guard. Accepted ball/treat/tug clears the
19
+ encounter and its obsolete route immediately, using normal actor recovery.
20
+ A separate random stream leaves the existing ordinary episode/gait choices
21
+ unchanged before the encounter.
22
+
23
+ Checks: `node --test puppy-butterfly.test.mjs`, `npm test`.
24
+ Generate a lightweight real-engine replay: `node tools/check-butterfly.mjs`.
25
+ Its local `site-dist/butterfly-preview.html` loads prerecorded cropped Braille
26
+ frames, not another full pet asset set. The replay is a QA artifact, not an npm
27
+ asset or an alternate implementation. Serve only `site-dist` on a local address.
28
+
29
+ Version0.0.6 includes this behavior. Existing0.0.5 registry artifacts remain
30
+ unchanged. An already-running companion must be restarted after updating.
package/PUPPY-MEMORY.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Runtime weight and performance
2
2
 
3
+ ## Node compressed crops (0.0.6)
4
+
5
+ Node now stores each validated Q5 crop as a small DEFLATE block, using built-in
6
+ `node:zlib`; only the existing two-frame LRU is expanded. The original archive
7
+ buffer is released. Browser hosts keep the portable bit-packed implementation.
8
+ No added runtime dependency, process, timer, lower resolution or lost pose.
9
+
10
+ Same Windows host, fresh processes, same450-frame ball sequence:
11
+
12
+ | Metric | 0.0.5 | 0.0.6 |
13
+ | --- | ---: | ---: |
14
+ | Loaded ArrayBuffers, after native close callbacks + diagnostic GC | 59.7 MiB | 21.4 MiB |
15
+ | Rendered ArrayBuffers | 67.3 MiB | 29.0 MiB |
16
+ | Mean frame time | 3.39 ms | 3.39 ms |
17
+ | P95 frame time | 6.11 ms | 6.27 ms |
18
+ | Initial load | 653 ms | 910 ms |
19
+ | Actual Windows overlay worker RSS, short owned-window checks | 130 MiB | 96–106 MiB |
20
+
21
+ The450-frame output hash is unchanged. `puppy-node-cache.test.mjs` compares every
22
+ packaged source frame in both stores, including the separately repaired watch
23
+ art. These samples are not a RAM ceiling: V8, allocator retention, viewport and
24
+ activity change RSS. The helper is separate. The large off-screen benchmark
25
+ peaked at267/238 MiB; do not substitute the short96–106 MiB samples for that peak.
26
+ Evidence: tools/benchmark-pet.mjs, local dist/006-paired-{before,after}.json and
27
+ dist/006-native-{before,source}.log. Diagnostic GC is never enabled in the pet.
28
+ Shrinking V8's young generation was tested but increased rendering cost, so no
29
+ global or embedded-host V8 flags are imposed.
30
+
31
+ Watch repair adds no frame count; its archive is15,422 bytes smaller. Butterfly
32
+ chasing adds no dog images or persistent image buffer; see BUTTERFLY.md. Existing
33
+ sleep, colors, wake-on-interaction and5fps sleeping repaint remain unchanged.
34
+
3
35
  ## Comfort behaviors (0.0.4)
4
36
 
5
37
  Belly-up flopping and eyes-closed sleeping reuse the existing action/rest art.
package/README.ko.md CHANGED
@@ -6,6 +6,7 @@
6
6
  앱 창 위에서 혼자 걷고, 뛰고, 앉아서 쉬는 보송한 점자 도트 리트리버예요.
7
7
  옆잠, 발라당, 몸 말기, 앞발에 턱 괴기. 조용하면 양쪽으로 누워 잠들어요.
8
8
  가끔 공·간식·터그로 놀아주세요.
9
+ 가끔 찾아오는 나비를 쫓기도 해요.
9
10
  나머지 시간에는 각자의 일을 하면 돼요.
10
11
  [BiteFoundry](https://github.com/BiteFoundry)가 만듭니다.
11
12
  런타임에는 계정, 모델 API, 텔레메트리가 필요하지 않아요.
package/README.md CHANGED
@@ -12,7 +12,8 @@
12
12
  </p>
13
13
 
14
14
  A fluffy, autonomous Braille-art retriever that hangs out above an app window.
15
- It wanders, runs, flops belly-up and naps. Toss a ball, offer a treat or play tug.
15
+ It wanders, runs, flops belly-up and naps. Occasionally it chases a butterfly.
16
+ Toss a ball, offer a treat or play tug.
16
17
  Get back to your thing.
17
18
 
18
19
  Made by [BiteFoundry](https://github.com/BiteFoundry). No model API, account or
package/RELEASING.md CHANGED
@@ -1,13 +1,13 @@
1
- # Retrieviewer 0.0.5 release process
2
-
3
- Current target: `@bitefoundry/retrieviewer@0.0.5`, public `latest`. The owner
4
- explicitly requested npm publication and homepage update of the approved sleep
5
- studies and mirrored side on2026-10-23. This also includes manual ink presets,
6
- sleep pressure independent of energy, shared gesture scratch and5fps sleep paint.
7
- Preserve0.0.4 registry bytes; never republish them. Run all checks below and publish
8
- only the exact tested0.0.5 tarball. Four new approved keys stay under the existing
9
- CC BY-SA4.0 artwork terms; see COMFORT-ART.md. The historical scope below describes
10
- 0.0.4, not current verification results.
1
+ # Retrieviewer 0.0.6 release process
2
+
3
+ Current target: `@bitefoundry/retrieviewer@0.0.6`, public `latest`. The owner
4
+ explicitly requested Node memory optimization, repaired watch motion, completed
5
+ butterfly encounters and npm publication on2026-10-23. Built-in Node zlib keeps
6
+ each Q5 crop compressed; portable browser storage stays unchanged. Watch art
7
+ reuses existing drawings with canonical seated paws/body. Butterfly encounters
8
+ reuse gallop poses with no extra dog archive. MIT/CC BY-SA4.0 unchanged.
9
+ Preserve0.0.5 and all earlier registry bytes; publish only the exact tested0.0.6
10
+ tarball. The historical scope below is not current verification evidence.
11
11
 
12
12
  Target: `@bitefoundry/retrieviewer@0.0.4`, public npm registry, `latest` tag.
13
13
  The npm organization `bitefoundry` uses the free public-packages plan; account
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitefoundry/retrieviewer",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "A fluffy Braille desktop pet for Windows and Linux X11, with experimental macOS support and an embeddable TUI engine.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://bitefoundry.github.io/retrieviewer/",
@@ -89,6 +89,7 @@
89
89
  "PUPPY-MEMORY.md",
90
90
  "PUPPY-Q5-COMPARISON.md",
91
91
  "COMFORT-ART.md",
92
+ "BUTTERFLY.md",
92
93
  "examples/embed.mjs",
93
94
  "examples/live.mjs",
94
95
  "examples/plugin.mjs",
@@ -80,11 +80,20 @@
80
80
  "uniqueFrames": 142,
81
81
  "sourceSha256": "7918261fb64c57a91739729841fd84c6dd53891196c7fb76ee9c27712ab6ef5d",
82
82
  "sourcePackedSha256": "7785ef65b8a2a5bf6f7ce600fe9957a60d9af82066ff49275bba061eac5392d1",
83
- "encodedBytes": 7031343,
84
- "gzipBytes": 1450156,
85
- "residentPixelBytes": 4373034,
86
- "encodedSha256": "38cfdfcee9c857c56a14f0a9734cb3301b4a04cb8e1ae3fb2e2b2ffe3666240f",
87
- "quantizedSha256": "3c8f40416d2fff1c1031d3ad8917ea885998a786126d5973dfee4102893a0a53"
83
+ "encodedBytes": 6185813,
84
+ "gzipBytes": 1434734,
85
+ "residentPixelBytes": 3845581,
86
+ "encodedSha256": "8e4811c4c72337d094770596b2b73e2f468f92884e6de2e14880ce9b9ad6f351",
87
+ "quantizedSha256": "500dcc9b5edbadf97159fc9256e9d942578e13e36f14c6fecda649a97214318b",
88
+ "watchRegistration": {
89
+ "sourceVersion": "0.0.5",
90
+ "sourceEncodedSha256": "38cfdfcee9c857c56a14f0a9734cb3301b4a04cb8e1ae3fb2e2b2ffe3666240f",
91
+ "canonicalSeat": 50,
92
+ "bodyFromY": 210,
93
+ "neckBlendFromY": 180,
94
+ "headTranslateY": 8,
95
+ "unchangedOtherFrames": true
96
+ }
88
97
  },
89
98
  "puppy-paper-actions-v9": {
90
99
  "file": "puppy-paper-actions-v9-q5.bin.gz",
@@ -13,12 +13,15 @@ const OFFER={x:0,z:.3};
13
13
  const TREAT_OFFSET={z:.08,height:.25};
14
14
  const COOLDOWN={ball:6,treat:15,tug:20};
15
15
  const FLIGHT=.65,BALL_PEAK=.75,CARRY_HEIGHT=.19,SETTLE_FALLBACK=.6,MAX_DT=.25,ARRIVE=.05;
16
+ // Active simulation seconds, never a wall-clock timer that wakes a hidden pet.
17
+ export const BUTTERFLY_RULES=Object.freeze({firstMin:240,firstSpread:240,cooldownMin:720,cooldownSpread:480,maxVisible:18});
16
18
  const LABEL={
17
19
  start:'기다리는 중',pause:'잠시 느긋하게 둘러보는 중',lie:'엎드리는 중',rest:'엎드려 쉬는 중',recover:'앉아서 기운 차리는 중',
18
20
  sit:'앉는 중',watch:'앉아서 지켜보는 중',look:'고개를 돌려 살피는 중',observe:'가만히 주변 관찰 중',
19
21
  stroll:'옆으로 살짝 걸어가는 중',sniff:'잠깐 멈춰 주변을 살피는 중',dash:'저 멀리 달려가는 중',far:'멀리서 둘러보는 중',home:'제자리로 돌아오는 중',
20
22
  patrol:'주변을 왕복 산책하는 중',scan:'양쪽을 번갈아 살피는 중',roll:'느긋하게 뒹구는 중',play:'신나서 한 바퀴 도는 중',checkin:'가까이 와서 함께 쉬는 중',
21
23
  belly:'배를 보이고 발라당 쉬는 중',sleep:'새근새근 잠자는 중',tug:'장난감을 물고 당기는 중',
24
+ butterflyNotice:'나비를 발견하고 귀를 쫑긋하는 중',butterfly:'나비를 폴짝폴짝 쫓는 중',butterflyBye:'날아가는 나비를 바라보는 중',
22
25
  fetch:'공을 쫓아가는 중',pickup:'공을 무는 중',carry:'공을 물고 돌아오는 중',drop:'공을 내려놓는 중',offer:'공을 내려놓고 기다리는 중',
23
26
  treatSit:'간식 앞에 앉는 중',chew:'간식 먹는 중'
24
27
  };
@@ -39,6 +42,9 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
39
42
  if(!Number.isFinite(energy)||energy<0||energy>1)throw RangeError('energy must be within 0..1');
40
43
 
41
44
  const rand=mulberry32(seed),range=(a,b)=>a+(b-a)*rand(),side=()=>rand()<.5?'left':'right';
45
+ // Separate stream: the rare encounter does not perturb ordinary episodes/gaits.
46
+ const encounterRand=mulberry32(seed^0x4bf17a93),encounterRange=(a,b)=>a+(b-a)*encounterRand();
47
+ let nextButterfly=BUTTERFLY_RULES.firstMin+encounterRange(0,BUTTERFLY_RULES.firstSpread),butterfly=null;
42
48
  const needs={energy,social:.5,curiosity:.5,drowsiness:0};
43
49
  const cooldowns={ball:0,treat:0,tug:0};
44
50
  const episodeCooldowns={roll:0,play:0,belly:0,sleep:0};
@@ -58,6 +64,32 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
58
64
  const sit=step=>cmd('sit',[],step,()=>!seatedAnyVariant());
59
65
  const lie=()=>cmd('lie',[],'lie',()=>actor.state!=='down');
60
66
  const walkTo=x=>cmd('walk',()=>{const dx=x-actor.sample(clock).x;return [dx<0?'left':'right',Math.abs(dx)];},'patrol',()=>Math.abs(x-actor.sample(clock).x)>.002);
67
+ const butterflyPoint=()=>{
68
+ if(!butterfly)return null;
69
+ const elapsed=clock-butterfly.since,p=clamp(elapsed/.65,0,1),ease=p*p*(3-2*p);
70
+ const leaving=butterfly.leaving?clamp((clock-butterfly.leaving)/1.1,0,1):0;
71
+ return {x:butterfly.from.x+(butterfly.to.x-butterfly.from.x)*ease+.025*Math.sin(elapsed*5)+leaving*.2*butterfly.side,
72
+ z:butterfly.from.z+(butterfly.to.z-butterfly.from.z)*ease,
73
+ height:.45+.045*Math.sin((clock-butterfly.start)*6)+leaving*.35,
74
+ phase:((clock-butterfly.start)*5)%1,opacity:1-leaving};
75
+ };
76
+ const butterflyEpisode=()=>{
77
+ const s=actor.sample(clock),direction=encounterRand()<.5?-1:1;
78
+ const targets=[clamp(s.x+direction*.36,-.5,.5),clamp(s.x-direction*.38,-.5,.5),clamp(s.x+direction*.24,-.5,.5)];
79
+ const z=s.z;
80
+ const moveFly=x=>fx(()=>{
81
+ const from=butterflyPoint()??{x:s.x-direction*.28,z};
82
+ const heading=x<actor.sample(clock).x?-1:1;
83
+ // The dog root is its body center, NOT its nose. Keep the visitor ahead of
84
+ // the muzzle on each leg, rather than hovering behind the running face.
85
+ butterfly={start:butterfly?.start??clock,since:clock,from:{x:from.x,z:from.z},to:{x:x+heading*.23,z},side:heading,leaving:0};
86
+ });
87
+ // Look names are dog-relative: its left is the viewer's right.
88
+ return [sit('butterflyNotice'),moveFly(targets[0]),cmd('look',[direction<0?'right':'left'],'butterflyNotice'),wait(.8,'butterflyNotice'),
89
+ ...targets.flatMap((x,i)=>[...(i?[moveFly(x)]:[]),cmd(typeof actor.chase==='function'?'chase':'go',[x,z],'butterfly'),wait(.3,'butterfly')]),
90
+ fx(()=>{if(butterfly)butterfly.leaving=clock;}),wait(1.1,'butterflyBye'),fx(()=>{butterfly=null;needs.energy-=.12;needs.curiosity-=.25;}),
91
+ home(),sit('recover'),wait(range(3,6),'recover')];
92
+ };
61
93
 
62
94
  // Each episode is a short coherent plan; a pause always leads in so autonomy never stalls.
63
95
  const EPISODES={
@@ -73,6 +105,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
73
105
  play:()=>[cmd('spin',[],'play'),wait(range(1,2),'pause'),sit('sit'),wait(range(2,4),'watch'),fx(()=>{needs.energy-=.12;needs.curiosity-=.2;})],
74
106
  belly:()=>[cmd('belly',[range(5,9)],'belly'),wait(range(2,4),'watch'),fx(()=>{needs.energy+=.08;needs.curiosity-=.15;})],
75
107
  sleep:()=>[cmd('sleep',[range(65,110),['side','curl','chin','belly'][Math.floor(rand()*4)],rand()<.5],'sleep'),wait(range(2,4),'recover'),fx(()=>{needs.energy+=.3;})],
108
+ butterfly:butterflyEpisode,
76
109
  };
77
110
  const weights=()=>{
78
111
  const {energy:e,social:s,curiosity:c}=needs;
@@ -90,6 +123,10 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
90
123
  // Quiet-time sleep pressure is independent of energy restored by sitting.
91
124
  // Wait for the current coherent episode to end; never yank a running puppy.
92
125
  if(quiet>=120&&needs.drowsiness>=.35&&episodeCooldowns.sleep===0&&lastEpisode!=='sleep'&&typeof actor.sleep==='function')kind='sleep';
126
+ const position=actor.sample(clock);
127
+ if(kind!=='sleep'&&clock>=nextButterfly&&quiet>=15&&needs.energy>.6&&needs.drowsiness<.35&&position.z> -1.2){
128
+ kind='butterfly';nextButterfly=clock+BUTTERFLY_RULES.cooldownMin+encounterRange(0,BUTTERFLY_RULES.cooldownSpread);
129
+ }
93
130
  lastEpisode=kind;decisions++;
94
131
  if(kind==='roll')episodeCooldowns.roll=90;
95
132
  if(kind==='play')episodeCooldowns.play=120;
@@ -139,6 +176,10 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
139
176
  for(const k in cooldowns)cooldowns[k]=Math.max(0,cooldowns[k]-dt);
140
177
  for(const k in episodeCooldowns)episodeCooldowns[k]=Math.max(0,episodeCooldowns[k]-dt);
141
178
  drift(dt);
179
+ // A stalled or externally substituted actor must not leave a permanent insect.
180
+ if(butterfly&&clock-butterfly.start>BUTTERFLY_RULES.maxVisible){
181
+ butterfly=null;queue=[sit('recover'),wait(3,'recover')];hold=0;holdIntent=null;gesture=null;waiting=null;actor.rest();
182
+ }
142
183
  const safe=SAFE.has(actor.state);
143
184
  if(waiting){
144
185
  waiting.age+=dt;if(!safe)waiting.left=true;
@@ -175,6 +216,10 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
175
216
  if(kind==='tug'&&typeof actor.tug!=='function')return false;
176
217
  if(pending||active||cooldowns[kind]>0)return false;
177
218
  quiet=0;
219
+ if(butterfly||lastEpisode==='butterfly'){
220
+ butterfly=null;queue=[];hold=0;holdIntent=null;gesture=null;waiting=null;
221
+ if(kind!=='ball')actor.sit();
222
+ }
178
223
  if(kind==='treat'||kind==='tug'){
179
224
  pending=kind;
180
225
  // The pet wakes through its authored recovery, not after a long sleep timer.
@@ -212,7 +257,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
212
257
  const interaction=active??pending;
213
258
  return {
214
259
  intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:label,episode:lastEpisode,interaction,busy:interaction!==null,
215
- ball:ballReport(),treat:treatReport(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
260
+ ball:ballReport(),treat:treatReport(),butterfly:butterflyPoint(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
216
261
  snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,drowsiness:needs.drowsiness,decisions,cooldowns:{...cooldowns},
217
262
  // Local hold clock only: progress freezes between updates; null outside the stationary hold.
218
263
  gesture:gesture&&SAFE.has(actor.state)?{clip:gesture.clip,progress:clamp(gesture.elapsed/gesture.duration,0,1)}:null
@@ -7,6 +7,22 @@ export function paintCompanionObjects(pixels,width,height,projection,report,stat
7
7
  // Interaction drawings already include the toy/food. Never paint a second
8
8
  // floating ball over the baked mouth, or a biscuit through the puppy's chest.
9
9
  if(report.gesture)return pixels;
10
+ // Tiny procedural butterfly: two paired wings and a body. World-space size,
11
+ // same fixed camera and palette as the pet; no image/cache or extra render pass.
12
+ if(report.butterfly){
13
+ const b=report.butterfly,p=projection.project(b.x,b.height,b.z);
14
+ const edge=projection.project(b.x+.04,b.height,b.z),up=projection.project(b.x,b.height+.035,b.z);
15
+ const cx=p.x*width,cy=p.y*height,rx=Math.abs(edge.x-p.x)*width,ry=Math.abs(up.y-p.y)*height;
16
+ const opening=.22+.78*Math.abs(Math.sin(b.phase*Math.PI)),tone=Math.round(235*b.opacity);
17
+ if([cx,cy,rx,ry,opening,tone].every(Number.isFinite)&&rx>0&&ry>0&&tone>0)
18
+ for(let y=Math.max(0,Math.floor(cy-ry));y<Math.min(height,Math.ceil(cy+ry));y++)
19
+ for(let x=Math.max(0,Math.floor(cx-rx));x<Math.min(width,Math.ceil(cx+rx));x++){
20
+ const u=(x+.5-cx)/rx,v=(y+.5-cy)/ry,a=Math.abs(u)/opening;
21
+ const wing=((a-.5)/.44)**2+((v+.28)/.56)**2<1||((a-.36)/.29)**2+((v-.43)/.37)**2<1;
22
+ const body=Math.abs(u)<.1&&Math.abs(v)<.7;
23
+ if(wing||body){const j=(y*width+x)*4;pixels[j]=pixels[j+1]=pixels[j+2]=Math.max(pixels[j],body?tone:Math.round(tone*(.65+.35*a)));}
24
+ }
25
+ }
10
26
  // A quiet, tiny rising "z" suggests snoring. The sleeping dog itself remains
11
27
  // the original eyes-closed source pose: no chest/body zoom or squashing.
12
28
  if(Number.isFinite(report.snore)&&state.state==='sleep'){
@@ -10,8 +10,9 @@
10
10
  // 20 u32[logicalFrameCount] logical -> unique index
11
11
  // then uniqueCount records of u32 x,y,w,h,offset,length (length = h*(w+1))
12
12
  // then the filtered rows of every unique crop, contiguous in order.
13
- // At load every crop is unfiltered once and kept as 5-bit packed indices; the
14
- // filtered input is not retained. Full frames are rebuilt on demand (LRU of 2).
13
+ // At load every crop is validated. The portable store keeps 5-bit packed indices;
14
+ // an optional host codec compresses each filtered crop instead (Node zlib).
15
+ // Neither retains the input archive. Full frames rebuild on demand (LRU of 2).
15
16
  const MAGIC=[0x52,0x56,0x51,0x35],RVF1=[0x52,0x56,0x46,0x31],HEADER=20,RECORD=24,MAX_SIDE=16384,BITS=5;
16
17
 
17
18
  export const quantize5=v=>Math.round(v*31/255);
@@ -80,7 +81,8 @@ function unfilterRow(type,src,at,cur,prev,w){
80
81
 
81
82
  // Validate and unfilter. Only the returned typed arrays survive; `bytes` is
82
83
  // not referenced by the store built from them.
83
- function decode(bytes,{frameIndices}={}){
84
+ function decode(bytes,{frameIndices,codec}={}){
85
+ if(codec&&(typeof codec.compress!=='function'||typeof codec.expand!=='function'))fail('codec');
84
86
  if(!(bytes instanceof Uint8Array))fail('bytes');
85
87
  if(!isPredictiveFrames(bytes)||bytes.length<HEADER)fail('magic');
86
88
  const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),u32=at=>view.getUint32(at,true);
@@ -107,27 +109,30 @@ function decode(bytes,{frameIndices}={}){
107
109
  }
108
110
  if(bytes.length!==data+total)fail('pixel length');
109
111
  if(pixels*BITS>0xffffffff)fail('pixel count');
110
- const packed=new Uint8Array(Math.ceil(pixels*BITS/8));
112
+ const packed=codec?null:new Uint8Array(Math.ceil(pixels*BITS/8)),chunks=codec?new Array(unique):null;
111
113
  let cur=new Uint8Array(width),prev=new Uint8Array(width),at=data,pos=0;
112
114
  for(let u=0;u<unique;u++){
113
- const w=rects[u*5+2],h=rects[u*5+3];prev.fill(0);
115
+ const w=rects[u*5+2],h=rects[u*5+3],start=at;prev.fill(0);
114
116
  for(let y=0;y<h;y++){
115
117
  unfilterRow(bytes[at],bytes,at+1,cur,prev,w);at+=w+1;
116
118
  for(let x=0;x<w;x++){
117
- const q=cur[x];if(q>31)fail('quantized value');if(needed&&!needed[u])continue;
119
+ const q=cur[x];if(q>31)fail('quantized value');if(codec||(needed&&!needed[u]))continue;
118
120
  const i=pos>>>3,s=pos&7;packed[i]|=(q<<s)&255;if(s>3)packed[i+1]|=q>>(8-s);pos+=BITS;
119
121
  }
120
122
  const t=prev;prev=cur;cur=t;
121
123
  }
124
+ if(codec&&h&&(!needed||needed[u]))chunks[u]=Uint8Array.from(codec.compress(bytes.subarray(start,at)));
122
125
  }
123
- return{width,height,frameCount,unique,map,rects,packed,retained};
126
+ return{width,height,frameCount,unique,map,rects,packed,retained,chunks,codec};
124
127
  }
125
128
 
126
- function createStore({width,height,frameCount,unique,map,rects,packed,retained}){
129
+ function createStore({width,height,frameCount,unique,map,rects,packed,retained,chunks,codec}){
127
130
  const n=width*height,slots=[{unique:-1,frame:null},{unique:-1,frame:null}];let decodes=0,hits=0;
131
+ const pixelBytes=packed?.byteLength??chunks.reduce((sum,c)=>sum+(c?.byteLength??0),0);
132
+ const cur=codec?new Uint8Array(width):null,prev=codec?new Uint8Array(width):null;
128
133
  return Object.freeze({
129
- width,height,frameCount,uniqueCount:unique,byteLength:n*frameCount,residentPixelBytes:packed.byteLength,
130
- get residentBytes(){return packed.byteLength+map.byteLength+rects.byteLength+(retained?.byteLength??0)+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
134
+ width,height,frameCount,uniqueCount:unique,byteLength:n*frameCount,residentPixelBytes:pixelBytes,
135
+ get residentBytes(){return pixelBytes+map.byteLength+rects.byteLength+(cur?.byteLength??0)+(prev?.byteLength??0)+(retained?.byteLength??0)+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
131
136
  stats(){return{decodes,hits,cachedFrames:slots.filter(e=>e.frame).length};},
132
137
  getFrame(index){
133
138
  if(!Number.isInteger(index)||index<0||index>=frameCount)fail('frame index');
@@ -138,6 +143,18 @@ function createStore({width,height,frameCount,unique,map,rects,packed,retained})
138
143
  const slot=slots.pop();slots.unshift(slot);decodes++;
139
144
  const frame=slot.frame??=new Uint8Array(n),k=u*5,x=rects[k],y=rects[k+1],w=rects[k+2],h=rects[k+3];
140
145
  frame.fill(0);slot.unique=u;
146
+ if(codec){
147
+ if(!h)return frame;
148
+ const rows=codec.expand(chunks[u],h*(w+1));
149
+ if(rows.length!==h*(w+1))fail('expanded length');
150
+ prev.fill(0);
151
+ for(let r=0,at=0;r<h;r++,at+=w+1){
152
+ unfilterRow(rows[at],rows,at+1,cur,prev,w);
153
+ for(let c=0;c<w;c++)frame[(y+r)*width+x+c]=DISPLAY[cur[c]];
154
+ prev.set(cur.subarray(0,w));
155
+ }
156
+ return frame;
157
+ }
141
158
  let pos=rects[k+4]*BITS;
142
159
  for(let r=0;r<h;r++){
143
160
  const row=(y+r)*width+x;
Binary file
package/puppy-paper.mjs CHANGED
@@ -28,6 +28,13 @@ export function createPaperComfortTimings(actionInfo,{key=PAPER_COMFORT_TIMINGS.
28
28
  }
29
29
  const comfortDuration=(name,seconds)=>{if(typeof seconds!=='number'||!Number.isFinite(seconds)||seconds<=0||seconds>PAPER_COMFORT_LIMITS[name])throw Error(`Invalid ${name} duration`);};
30
30
  const comfortEase=u=>u<=0?0:u>=1?1:u*u*(3-2*u);
31
+ // Turn, hold the gaze, then return. Progress addresses the two authored edges;
32
+ // the paws and torso are registered to the same seated source in both edges.
33
+ export const PAPER_WATCH_TIMING=Object.freeze({turn:.6,hold:.65,back:.65,total:1.9});
34
+ export function paperWatchProgress(age){
35
+ const {turn,hold,back}=PAPER_WATCH_TIMING;
36
+ return age<turn?.5*comfortEase(age/turn):age<turn+hold?.5:.5+.5*comfortEase((age-turn-hold)/back);
37
+ }
31
38
 
32
39
  // Original artwork only. Physics supplies ground travel; no procedural dog is drawn.
33
40
  export function createPaperActor(animal,info,{manualTricks=false,trickTimings=null,walkTransitions=false,comfortTimings=PAPER_COMFORT_TIMINGS}={}){
@@ -196,7 +203,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
196
203
  else if(state==='sit-down'){seatProgress=Math.min(1,seatProgress+dt/(snappyPreparation()?.35:.95));if(seatProgress===1){seatVariant='default';enter('idle');if(pendingAction)prepareDirection();}}
197
204
  else if(state==='rise'){seatProgress=Math.max(0,seatProgress-prepareDt/(snappyPreparation()?.28:.65));if(seatProgress===0){if(seatVariant==='arrival')enter('arrival-exit');else prepareDirection();}}
198
205
  else if(state==='arrival-exit'&&age>=.22){seatVariant='default';prepareDirection();}
199
- else if(state==='watch'&&age>=1.4){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}
206
+ else if(state==='watch'&&age>=PAPER_WATCH_TIMING.total){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}
200
207
  else if(state==='recline'){const end=stayDown?.25:1;restProgress=Math.min(end,restProgress+dt/1.8);if(restProgress===end)enter(stayDown?'down':comfort?.kind==='sleep'?'sleep':'nap');}
201
208
  else if(state==='sleep'&&age>=comfort.seconds){comfort=null;enter('wake');}
202
209
  else if(state==='tug'&&tugProgress()>=1){comfort=null;enter('idle');drain();}
@@ -222,7 +229,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
222
229
  if(walkTransitions&&state==='turn'){actionName='walk-turn';actionProgress=Math.min(1,age/turnSeconds());}
223
230
  if(view==='approach'&&approachPlan&&(state==='run'||(state==='finish'&&completed))){const p=approachPlan.at(travelDistance);if(state==='finish'){behavior='brake';behaviorProgress=1;}else if(p.clip!=='run'){behavior=p.clip;behaviorProgress=p.progress;}}
224
231
  if(state==='arrival-exit'){behavior='finish';behaviorProgress=Math.min(1,age/.22);}
225
- if(state==='watch'){behavior=`watch-${watchSide}`;behaviorProgress=Math.min(1,age/1.4);}
232
+ if(state==='watch'){behavior=`watch-${watchSide}`;behaviorProgress=paperWatchProgress(age);}
226
233
  if(restProgress>0||state==='recline'||state==='wake'||state==='nap'){behavior='rest';behaviorProgress=restProgress;}
227
234
  if(state==='run'||state==='finish'){
228
235
  const p=state==='finish'?phase+(Math.ceil(phase)-phase)*Math.min(1,age/.22):phase;
@@ -48,6 +48,8 @@ export interface LifeReport {
48
48
  cooldowns: {ball: number; treat: number; tug: number};
49
49
  gesture: {clip: string; progress: number} | null;
50
50
  ball: {x: number; z: number; height: number; carried: boolean} | null;
51
+ /** Rare autonomous visitor; absent during direct user interactions. */
52
+ butterfly: {x: number; z: number; height: number; phase: number; opacity: number} | null;
51
53
  treat: {x: number; z: number; height: number} | null;
52
54
  /** Tug-of-war state while a tug interaction is active. */
53
55
  tug: {progress: number; grip: number} | null;
package/retrieviewer.mjs CHANGED
@@ -60,9 +60,16 @@ async function gunzip(bytes){
60
60
  // life, renderer and output. Last dispose drops the shared lease, not other pets.
61
61
  const sharedAssets=new Map();
62
62
  async function loadPetAssets(loadAsset,artProfile){
63
+ // Node keeps filtered crops compressed; only the existing two-frame LRU is
64
+ // expanded. Browser bundles retain the portable bit-packed implementation.
65
+ let codec;
66
+ if(typeof process!=='undefined'&&process.versions?.node){
67
+ const {deflateRawSync,inflateRawSync}=await import('node:zlib');
68
+ codec={compress:bytes=>deflateRawSync(bytes,{level:1}),expand:(bytes,size)=>inflateRawSync(bytes,{maxOutputLength:size})};
69
+ }
63
70
  const load=async name=>bytesOf(name,await loadAsset(name));
64
71
  const json=async name=>JSON.parse(new TextDecoder().decode(await load(name)));
65
- const frames=async (name,options)=>readPackedFrames(await gunzip(await load(name)),options);
72
+ const frames=async (name,options)=>readPackedFrames(await gunzip(await load(name)),{...options,codec});
66
73
  // One family at a time: never inflate all eight legacy-sized buffers together.
67
74
  const data={assets:{}};
68
75
  data.info=await json(PET_ASSETS.strideInfo);data.meta=await json(PET_ASSETS.stableInfo);