@papyrus-sdk/ui-react-native 0.2.35 → 0.2.36

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.
@@ -156,7 +156,7 @@
156
156
  var doc=contents.document,win=contents.window,start=null,blocked=false;
157
157
  function point(e){var t=e.changedTouches&&e.changedTouches[0]||e.touches&&e.touches[0];return t?{x:t.clientX,y:t.clientY,time:Date.now()}:null;}
158
158
  function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
159
- function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
159
+ function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay],[data-papyrus-ink-capture]'));}
160
160
  function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
161
161
  function invalidateScroll(finish){if(config.current() && start && finish && config.mode()==='chapter-scroll' && !blocked && !selected() && Math.abs(finish.y-start.y)>12 && Math.abs(finish.y-start.y)>Math.abs(finish.x-start.x) && config.interact)config.interact();}
162
162
  function move(e){if(e.touches.length!==1)blocked=true;invalidateScroll(point(e));}
@@ -181,6 +181,84 @@
181
181
  typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
182
182
  typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
183
183
  }
184
+ function simplifyPath(points, tolerance, maxPoints) {
185
+ if (!Array.isArray(points) || points.length <= 2) return Array.isArray(points) ? points.slice() : [];
186
+ tolerance = Number.isFinite(tolerance) ? Math.max(0, tolerance) : 1.5;
187
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 512;
188
+ var keep = new Uint8Array(points.length); keep[0] = 1; keep[points.length - 1] = 1;
189
+ var stack = [[0, points.length - 1]], toleranceSquared = tolerance * tolerance;
190
+ while (stack.length) {
191
+ var interval = stack.pop(), first = interval[0], last = interval[1], a = points[first], b = points[last];
192
+ var dx = b.x - a.x, dy = b.y - a.y, lengthSquared = dx * dx + dy * dy, maxDistance = toleranceSquared, split = -1;
193
+ for (var i = first + 1; i < last; i++) {
194
+ var p = points[i], ratio = lengthSquared ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSquared)) : 0;
195
+ var px = a.x + ratio * dx, py = a.y + ratio * dy, ox = p.x - px, oy = p.y - py, distance = ox * ox + oy * oy;
196
+ if (distance > maxDistance) { maxDistance = distance; split = i; }
197
+ }
198
+ if (split >= 0) { keep[split] = 1; stack.push([first, split], [split, last]); }
199
+ }
200
+ var result = []; for (var j = 0; j < points.length; j++) if (keep[j]) result.push(points[j]);
201
+ if (result.length <= maxPoints) return result;
202
+ var capped = []; for (var k = 0; k < maxPoints; k++) capped.push(result[Math.round(k * (result.length - 1) / (maxPoints - 1))]);
203
+ return capped;
204
+ }
205
+ function compactGesturePoints(points, maxPoints) {
206
+ if (!Array.isArray(points)) return [];
207
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 1024;
208
+ if (points.length <= maxPoints) return points.slice();
209
+ var compacted = [];
210
+ for (var i = 0; i < maxPoints; i++) compacted.push(points[Math.round(i * (points.length - 1) / (maxPoints - 1))]);
211
+ return compacted;
212
+ }
213
+ function densifyPath(points, maxStep, maxPoints) {
214
+ if (!Array.isArray(points) || !points.length) return [];
215
+ if (points.length === 1) return points.slice();
216
+ maxStep = Number.isFinite(maxStep) ? Math.max(1, maxStep) : 12;
217
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 512;
218
+ var cumulative = [0], total = 0;
219
+ for (var i = 1; i < points.length; i++) {
220
+ if (![points[i-1].x,points[i-1].y,points[i].x,points[i].y].every(Number.isFinite)) return [];
221
+ total += Math.hypot(points[i].x-points[i-1].x, points[i].y-points[i-1].y);
222
+ cumulative.push(total);
223
+ }
224
+ if (!total) return [points[0], points[points.length-1]];
225
+ var intervals = Math.min(maxPoints-1, Math.max(1, Math.ceil(total/maxStep))), result = [], segment = 1;
226
+ for (var sample = 0; sample <= intervals; sample++) {
227
+ var distance = total*sample/intervals;
228
+ while (segment < cumulative.length-1 && cumulative[segment] < distance) segment++;
229
+ var start = points[segment-1], end = points[segment], span = cumulative[segment]-cumulative[segment-1];
230
+ var ratio = span ? (distance-cumulative[segment-1])/span : 0;
231
+ result.push({x:start.x+(end.x-start.x)*ratio,y:start.y+(end.y-start.y)*ratio});
232
+ }
233
+ return result;
234
+ }
235
+ function createSpatialIndex(entries, cellSize) {
236
+ cellSize = Number.isFinite(cellSize) ? Math.max(8, cellSize) : 64;
237
+ var cells = new Map();
238
+ entries.forEach(function(entry) {
239
+ var left = Math.floor(entry.left / cellSize), right = Math.floor(entry.right / cellSize);
240
+ var top = Math.floor(entry.top / cellSize), bottom = Math.floor(entry.bottom / cellSize);
241
+ for (var x = left; x <= right; x++) for (var y = top; y <= bottom; y++) {
242
+ var key = x + ':' + y, bucket = cells.get(key); if (!bucket) { bucket = []; cells.set(key, bucket); } bucket.push(entry);
243
+ }
244
+ });
245
+ return {cells:cells,cellSize:cellSize};
246
+ }
247
+ function queryNearestTextRect(index, x, y, maxDistance) {
248
+ if (!index || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(maxDistance) || maxDistance < 0) return null;
249
+ var size = index.cellSize, minX = Math.floor((x-maxDistance)/size), maxX = Math.floor((x+maxDistance)/size);
250
+ var minY = Math.floor((y-maxDistance)/size), maxY = Math.floor((y+maxDistance)/size), seen = new Set(), best = null;
251
+ for (var cx=minX; cx<=maxX; cx++) for (var cy=minY; cy<=maxY; cy++) {
252
+ var bucket = index.cells.get(cx+':'+cy); if (!bucket) continue;
253
+ bucket.forEach(function(entry) {
254
+ if (seen.has(entry)) return; seen.add(entry);
255
+ var nearestX = Math.max(entry.left,Math.min(entry.right,x)), nearestY = Math.max(entry.top,Math.min(entry.bottom,y));
256
+ var dx=x-nearestX,dy=y-nearestY,distance=Math.sqrt(dx*dx+dy*dy);
257
+ if (distance<=maxDistance && (!best || distance<best.distance)) best={entry:entry,distance:distance,x:nearestX,y:nearestY};
258
+ });
259
+ }
260
+ return best;
261
+ }
184
262
  function textNodes(document) {
185
263
  var walker = document.createTreeWalker(document.body, 4);
186
264
  var nodes = [], text = '', node;
@@ -230,13 +308,196 @@
230
308
  if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
231
309
  doc.body.appendChild(overlay);
232
310
  var registry = new Map(), hitRegions = [];
311
+ var inkRegistry = new Map(), rangeCache = new Map(), spatialIndex = null, activeGesture = null, inkConfig = {enabled:false,preset:'ink'};
312
+ var inkSvg = doc.createElementNS('http://www.w3.org/2000/svg','svg');
313
+ inkSvg.setAttribute('data-papyrus-ink-overlay','');
314
+ inkSvg.style.cssText = 'position:absolute;left:0;top:0;overflow:visible;pointer-events:none;z-index:1';
315
+ overlay.appendChild(inkSvg);
316
+ var inkCanvas = doc.createElement('canvas');
317
+ inkCanvas.dataset.papyrusInkCapture = '';
318
+ inkCanvas.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;z-index:2147483000;background:transparent;pointer-events:none;touch-action:none';
319
+ doc.body.appendChild(inkCanvas);
320
+ var inkContext = null; try { if(typeof win.CanvasRenderingContext2D==='function')inkContext = inkCanvas.getContext('2d'); } catch (_) {}
321
+ function resizeCanvas() {
322
+ var bodyRect=doc.body.getBoundingClientRect(), ratio = Math.max(1,Math.min(3,win.devicePixelRatio || 1));
323
+ var width=Math.max(1,Math.min(4096,doc.body.clientWidth||bodyRect.width||Math.min(4096,win.innerWidth||1)));
324
+ var height=Math.max(1,Math.min(4096,doc.body.clientHeight||bodyRect.height||Math.min(4096,win.innerHeight||1)));
325
+ inkCanvas.style.width=width+'px';inkCanvas.style.height=height+'px';
326
+ inkCanvas.width = Math.round(width*ratio); inkCanvas.height = Math.round(height*ratio);
327
+ if (inkContext) { inkContext.setTransform(ratio,0,0,ratio,0,0); inkContext.clearRect(0,0,width,height); }
328
+ }
329
+ function collectTextRects() {
330
+ var walker = doc.createTreeWalker(doc.body,4), node, entries=[];
331
+ while ((node=walker.nextNode())) {
332
+ var parent=node.parentElement;
333
+ if (!parent || parent.closest('script,style,noscript,textarea,[data-papyrus-overlay],[data-papyrus-ink-capture]')) continue;
334
+ var text=node.nodeValue || '', style=win.getComputedStyle(parent), computed=parseFloat(style.lineHeight);
335
+ for (var start=0; start<text.length;) {
336
+ var end=Math.min(text.length,start+24), last=text.charCodeAt(end-1), next=text.charCodeAt(end);
337
+ if(end<text.length&&last>=0xD800&&last<=0xDBFF&&next>=0xDC00&&next<=0xDFFF)end++;
338
+ var range=doc.createRange(); range.setStart(node,start); range.setEnd(node,end);
339
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
340
+ rects.forEach(function(rect) {
341
+ if (!(rect.width>0 && rect.height>0)) return;
342
+ entries.push({left:rect.left+win.scrollX,top:rect.top+win.scrollY,right:rect.right+win.scrollX,bottom:rect.bottom+win.scrollY,width:rect.width,height:rect.height,
343
+ node:node,start:start,end:end,lineHeight:Number.isFinite(computed)&&computed>0?computed:rect.height});
344
+ });
345
+ start=end;
346
+ }
347
+ }
348
+ return entries;
349
+ }
350
+ function rebuildSpatialIndex() { spatialIndex=createSpatialIndex(collectTextRects(),64); rangeCache.clear(); }
351
+ function inkViewportExtent() {
352
+ var root=doc.documentElement,body=doc.body,bodyRect=body.getBoundingClientRect();
353
+ var width=root.clientWidth||body.clientWidth||bodyRect.width||win.innerWidth||1;
354
+ var height=root.clientHeight||body.clientHeight||bodyRect.height||win.innerHeight||1;
355
+ return Math.max(1,Math.min(4096,width),Math.min(4096,height));
356
+ }
357
+ function inkStrokeWidthPx(width,preset) {
358
+ var scale=preset==='highlight'?0.8:preset==='underline'?0.1:0.15;
359
+ return Math.max(1,Math.min(24,(Number.isFinite(width)?width:0.006)*inkViewportExtent()*scale));
360
+ }
361
+ function closestGlyph(entry,x,y,maxDistance) {
362
+ var source=(entry.node.nodeValue || '').slice(entry.start,entry.end), best=null, offset=0;
363
+ for (var glyph of source) {
364
+ var range=doc.createRange(); range.setStart(entry.node,entry.start+offset); range.setEnd(entry.node,entry.start+offset+glyph.length); offset+=glyph.length;
365
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
366
+ rects.forEach(function(rect) {
367
+ if (!(rect.width>0 && rect.height>0)) return;
368
+ var nx=Math.max(rect.left+win.scrollX,Math.min(rect.right+win.scrollX,x)), ny=Math.max(rect.top+win.scrollY,Math.min(rect.bottom+win.scrollY,y));
369
+ var dx=x-nx,dy=y-ny,distance=Math.sqrt(dx*dx+dy*dy);
370
+ if (distance<=maxDistance && (!best || distance<best.distance)) best={range:range,rect:rect,distance:distance};
371
+ });
372
+ }
373
+ if (!best || typeof contents.cfiFromRange!=='function') return null;
374
+ var quote=best.range.toString(), cfiRange; try { cfiRange=contents.cfiFromRange(best.range); } catch (_) { return null; }
375
+ if (!quote || typeof cfiRange!=='string' || !/^epubcfi\([^\n<>]+\)$/.test(cfiRange)) return null;
376
+ var lineHeight=Math.max(1,entry.lineHeight || best.rect.height), px=x-(best.rect.left+win.scrollX), py=y-(best.rect.top+win.scrollY);
377
+ return {cfiRange:cfiRange,quote:quote,x:Math.max(-3,Math.min(3,px/lineHeight)),y:Math.max(-3,Math.min(3,py/lineHeight))};
378
+ }
379
+ function renderInkAnnotation(annotation,keep,bodyRect) {
380
+ var baseRect=bodyRect || doc.body.getBoundingClientRect() || {left:0,top:0};
381
+ var data=annotation.epubInk;
382
+ if (!data || data.version!==1 || data.kind!=='epub-ink' || !Array.isArray(data.segments)) return;
383
+ keep.add(annotation.id);
384
+ var group=inkRegistry.get(annotation.id);
385
+ if (!group) { group=doc.createElementNS('http://www.w3.org/2000/svg','g'); group.setAttribute('data-epub-ink-id',annotation.id); inkSvg.appendChild(group); inkRegistry.set(annotation.id,group); }
386
+ group.replaceChildren();
387
+ var color=/^#[0-9a-f]{6}$/i.test(annotation.color)?annotation.color:'#2563eb';
388
+ var opacity=Number.isFinite(annotation.opacity)?Math.max(0,Math.min(1,annotation.opacity)):1;
389
+ var preset=['ink','highlight','underline'].indexOf(data.preset)>=0?data.preset:'ink';
390
+ var stroke=inkStrokeWidthPx(annotation.strokeWidth,preset);
391
+ data.segments.forEach(function(segment) {
392
+ if (segment.href!==options.href || !Array.isArray(segment.points)) return;
393
+ var paths=[], current=[];
394
+ segment.points.forEach(function(point) {
395
+ var range=rangeCache.get(point.cfiRange);
396
+ if (!range) { try { range=contents.range(point.cfiRange); } catch (_) { range=null; } if (range) rangeCache.set(point.cfiRange,range); }
397
+ if (!range || range.toString()!==point.quote) { if(current.length>1)paths.push(current);current=[];return; }
398
+ var rect; try { rect=range.getBoundingClientRect(); } catch (_) { rect=null; }
399
+ if (!rect || !(rect.width>0 && rect.height>0)) { if(current.length>1)paths.push(current);current=[];return; }
400
+ var lineHeight=Math.max(1,rect.height), x=rect.left-baseRect.left+point.x*lineHeight, y=rect.top-baseRect.top+point.y*lineHeight;
401
+ var previous=current[current.length-1], jumpLimit=Math.max(24,lineHeight*2.5);
402
+ if (previous && Math.hypot(x-previous.x,y-previous.y)>jumpLimit) { if(current.length>1)paths.push(current);current=[]; }
403
+ current.push({x:x,y:y,lineHeight:lineHeight});
404
+ });
405
+ if(current.length>1)paths.push(current);
406
+ paths.forEach(function(points) {
407
+ var d='M '+points[0].x.toFixed(1)+' '+points[0].y.toFixed(1);
408
+ for(var i=1;i<points.length;i++){var prev=points[i-1],point=points[i],mx=(prev.x+point.x)/2,my=(prev.y+point.y)/2;d+=' Q '+prev.x.toFixed(1)+' '+prev.y.toFixed(1)+' '+mx.toFixed(1)+' '+my.toFixed(1);}
409
+ var last=points[points.length-1];d+=' L '+last.x.toFixed(1)+' '+last.y.toFixed(1);
410
+ var path=doc.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('d',d);path.setAttribute('fill','none');path.setAttribute('stroke',color);path.setAttribute('stroke-width',String(stroke));path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('opacity',String(opacity));group.appendChild(path);
411
+ });
412
+ });
413
+ }
414
+ function clearPreview() {
415
+ if(inkContext)inkContext.clearRect(0,0,inkCanvas.width,inkCanvas.height);
416
+ inkSvg.querySelectorAll('[data-papyrus-ink-preview]').forEach(function(path){path.remove();});
417
+ }
418
+ function createPreviewPath(x,y) {
419
+ var path=doc.createElementNS('http://www.w3.org/2000/svg','path');
420
+ path.setAttribute('data-papyrus-ink-preview','');path.setAttribute('d','M '+x.toFixed(1)+' '+y.toFixed(1));path.setAttribute('fill','none');
421
+ path.setAttribute('stroke',inkConfig.color);path.setAttribute('stroke-width',String(inkStrokeWidthPx(inkConfig.strokeWidth,inkConfig.preset)));
422
+ path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('opacity',String(inkConfig.opacity));
423
+ path.style.pointerEvents='none';inkSvg.appendChild(path);return path;
424
+ }
425
+ function updatePreviewPath(clientX,clientY) {
426
+ var bodyRect=doc.body.getBoundingClientRect(),x=clientX-bodyRect.left,y=clientY-bodyRect.top;
427
+ var gesture=activeGesture;
428
+ if(!gesture.previewPath||gesture.previewPathPoints>=32){
429
+ gesture.previewPath=createPreviewPath(gesture.previewLast?gesture.previewLast.x:x,gesture.previewLast?gesture.previewLast.y:y);
430
+ gesture.previewPathPoints=1;
431
+ }
432
+ var pathData=gesture.previewPath.getAttribute('d')||'';
433
+ gesture.previewPath.setAttribute('d',pathData+' L '+x.toFixed(1)+' '+y.toFixed(1));
434
+ gesture.previewPathPoints++;
435
+ gesture.previewLast={x:x,y:y};
436
+ }
437
+ function cancelGesture() {
438
+ var gesture=activeGesture; activeGesture=null; clearPreview();
439
+ if(gesture)try{inkCanvas.releasePointerCapture(gesture.pointerId);}catch(_){}
440
+ }
441
+ function commitGesture() {
442
+ var gesture=activeGesture; activeGesture=null; clearPreview();
443
+ if(!gesture || !alive || !options.isCurrent() || !inkConfig.enabled || !spatialIndex) return;
444
+ var simplified=simplifyPath(gesture.points,1.5,512), anchorSamples=densifyPath(simplified,12,512), segments=[], current=[];
445
+ anchorSamples.forEach(function(point) {
446
+ var coarse=queryNearestTextRect(spatialIndex,point.x,point.y,28);
447
+ var cutoff=coarse?Math.max(12,Math.min(28,0.8*coarse.entry.lineHeight)):12;
448
+ var anchored=coarse && closestGlyph(coarse.entry,point.x,point.y,cutoff);
449
+ if(!anchored){if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});current=[];return;}
450
+ current.push(anchored);
451
+ });
452
+ if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});
453
+ segments=segments.slice(0,64);
454
+ var pointCount=segments.reduce(function(total,segment){return total+segment.points.length;},0);
455
+ if(!pointCount)return;
456
+ var width=Math.max(1,doc.documentElement.scrollWidth,win.innerWidth),height=Math.max(1,doc.documentElement.scrollHeight,win.innerHeight);
457
+ var normalized=simplified.map(function(p){return {x:Math.max(0,Math.min(1,p.x/width)),y:Math.max(0,Math.min(1,p.y/height))};});
458
+ var minX=Math.min.apply(null,normalized.map(function(p){return p.x;})),maxX=Math.max.apply(null,normalized.map(function(p){return p.x;}));
459
+ var minY=Math.min.apply(null,normalized.map(function(p){return p.y;})),maxY=Math.max.apply(null,normalized.map(function(p){return p.y;}));
460
+ var first=segments[0].points[0], href=segments[0].href, documentId=typeof options.documentId==='function'?options.documentId():options.documentId;
461
+ var annotation={id:'epub-ink-'+Date.now()+'-'+Math.random().toString(36).slice(2,9),type:'ink',pageIndex:(options.spineIndex||0)+1,
462
+ rect:{x:minX,y:minY,width:Math.min(1-minX,Math.max(0.001,maxX-minX)),height:Math.min(1-minY,Math.max(0.001,maxY-minY))},path:normalized,color:inkConfig.color,opacity:inkConfig.opacity,strokeWidth:inkConfig.strokeWidth,createdAt:Date.now(),content:first.quote,
463
+ anchor:{version:1,kind:'epub-cfi',cfiRange:first.cfiRange,href:href,spineIndex:options.spineIndex,documentId:documentId,quote:first.quote},
464
+ epubInk:{version:1,kind:'epub-ink',documentId:documentId,preset:inkConfig.preset,segments:segments}};
465
+ if(typeof options.onInkCommit==='function')options.onInkCommit(annotation);
466
+ }
467
+ function pointerDown(event) {
468
+ if(!inkConfig.enabled || !alive || !options.isCurrent() || event.button!==undefined && event.button!==0)return;
469
+ if(activeGesture){cancelGesture();return;}
470
+ event.preventDefault();event.stopPropagation();
471
+ activeGesture={pointerId:event.pointerId,points:[{x:event.clientX+win.scrollX,y:event.clientY+win.scrollY}],last:{x:event.clientX,y:event.clientY},previewPath:null,previewPathPoints:0,previewLast:null};
472
+ updatePreviewPath(event.clientX,event.clientY);
473
+ try{inkCanvas.setPointerCapture(event.pointerId);}catch(_){}
474
+ }
475
+ function pointerMove(event) {
476
+ if(!activeGesture || activeGesture.pointerId!==event.pointerId)return;
477
+ if(!options.isCurrent()||!inkConfig.enabled){cancelGesture();return;}
478
+ event.preventDefault();event.stopPropagation();
479
+ var point={x:event.clientX+win.scrollX,y:event.clientY+win.scrollY};activeGesture.points.push(point);
480
+ if(activeGesture.points.length>2048)activeGesture.points=compactGesturePoints(activeGesture.points,1024);
481
+ if(inkContext){inkContext.beginPath();inkContext.moveTo(activeGesture.last.x,activeGesture.last.y);inkContext.lineTo(event.clientX,event.clientY);inkContext.strokeStyle=inkConfig.color;inkContext.globalAlpha=inkConfig.opacity;inkContext.lineWidth=inkStrokeWidthPx(inkConfig.strokeWidth,inkConfig.preset);inkContext.lineCap='round';inkContext.lineJoin='round';inkContext.stroke();}
482
+ updatePreviewPath(event.clientX,event.clientY);
483
+ activeGesture.last={x:event.clientX,y:event.clientY};
484
+ }
485
+ function pointerUp(event) {if(!activeGesture || activeGesture.pointerId!==event.pointerId)return;event.preventDefault();event.stopPropagation();var end={x:event.clientX+win.scrollX,y:event.clientY+win.scrollY},last=activeGesture.points[activeGesture.points.length-1];if(Math.hypot(end.x-last.x,end.y-last.y)>0.5){activeGesture.points.push(end);updatePreviewPath(event.clientX,event.clientY);}commitGesture();}
486
+ function pointerCancel(event) {if(activeGesture && (event.pointerId===undefined || activeGesture.pointerId===event.pointerId))cancelGesture();}
487
+ resizeCanvas();rebuildSpatialIndex();
488
+ inkCanvas.addEventListener('pointerdown',pointerDown);inkCanvas.addEventListener('pointermove',pointerMove);inkCanvas.addEventListener('pointerup',pointerUp);inkCanvas.addEventListener('pointercancel',pointerCancel);inkCanvas.addEventListener('lostpointercapture',pointerCancel);
489
+ win.addEventListener('blur',pointerCancel);win.addEventListener('resize',resizeCanvas);
233
490
  function render() {
234
491
  frame = 0; if (!alive || !options.isCurrent()) return;
235
- var keep = new Set(), noteGroups = new Map();
492
+ var keep = new Set(), keepInk = new Set(), noteGroups = new Map();
236
493
  hitRegions = [];
494
+ var bodyRect=doc.body.getBoundingClientRect() || {left:0,top:0};
495
+ inkSvg.setAttribute('width',String(Math.max(doc.documentElement.scrollWidth,win.innerWidth)));
496
+ inkSvg.setAttribute('height',String(Math.max(doc.documentElement.scrollHeight,win.innerHeight)));
237
497
  options.annotations().forEach(function(annotation) {
238
498
  var anchor = annotation.anchor;
239
499
  if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
500
+ if(annotation.type==='ink' && annotation.epubInk){renderInkAnnotation(annotation,keepInk,bodyRect);return;}
240
501
  var range = resolveRange(contents,anchor); if (!range) return;
241
502
  var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
242
503
  if (!rects.length) return;
@@ -275,22 +536,26 @@
275
536
  group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
276
537
  });
277
538
  registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
539
+ inkRegistry.forEach(function(group,id){if (!keepInk.has(id)) {group.remove();inkRegistry.delete(id);}});
278
540
  }
279
541
  function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
542
+ function rebuildAndSchedule(){if(!alive)return;rebuildSpatialIndex();schedule();}
543
+ function handleResize(){resizeCanvas();rebuildAndSchedule();}
544
+ function handleDocumentLoad(){rebuildAndSchedule();}
280
545
  function tapMarkup(event) {
281
546
  if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
282
547
  var hit=hitRegions.find(function(item){return item.rects.some(function(r){return event.clientX>=r.left && event.clientX<=r.right && event.clientY>=r.top && event.clientY<=r.bottom;});});
283
548
  if(hit) {event.stopPropagation();options.onTap(hit.id);}
284
549
  }
285
550
  doc.addEventListener('click',tapMarkup);
286
- var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
551
+ var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(rebuildAndSchedule) : null;
287
552
  if (observer) observer.observe(doc.body);
288
- win.addEventListener('resize',schedule);win.addEventListener('scroll',schedule); doc.addEventListener('load',schedule,true);
289
- if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
553
+ win.addEventListener('resize',handleResize);win.addEventListener('scroll',schedule); doc.addEventListener('load',handleDocumentLoad,true);
554
+ if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(function(){if(alive){rebuildSpatialIndex();schedule();}});
290
555
  schedule();
291
- return {schedule:schedule, hit:function(x,y){return hitRegions.some(function(item){return item.rects.some(function(r){return x>=r.left && x<=r.right && y>=r.top && y<=r.bottom;});});}, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);win.removeEventListener('scroll',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
556
+ return {schedule:schedule, setInkMode:function(config){var preset=['ink','highlight','underline'].indexOf(config?.preset)>=0?config.preset:'ink';inkConfig={enabled:!!config?.enabled,preset:preset,color:/^#[0-9a-f]{6}$/i.test(config?.color)?config.color:'#2563eb',opacity:Number.isFinite(config?.opacity)?Math.max(0,Math.min(1,config.opacity)):1,strokeWidth:Number.isFinite(config?.strokeWidth)?Math.max(0.0025,Math.min(0.02,config.strokeWidth)):0.006};inkCanvas.style.pointerEvents=inkConfig.enabled?'auto':'none';if(!inkConfig.enabled)cancelGesture();}, invalidateLayout:rebuildAndSchedule, hit:function(x,y){return hitRegions.some(function(item){return item.rects.some(function(r){return x>=r.left && x<=r.right && y>=r.top && y<=r.bottom;});});}, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;cancelGesture();if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',handleResize);win.removeEventListener('scroll',schedule);win.removeEventListener('blur',pointerCancel);doc.removeEventListener('load',handleDocumentLoad,true);doc.removeEventListener('click',tapMarkup);inkCanvas.removeEventListener('pointerdown',pointerDown);inkCanvas.removeEventListener('pointermove',pointerMove);inkCanvas.removeEventListener('pointerup',pointerUp);inkCanvas.removeEventListener('pointercancel',pointerCancel);inkCanvas.removeEventListener('lostpointercapture',pointerCancel);inkCanvas.remove();overlay.remove();doc.body.style.position=originalPosition;registry.clear();inkRegistry.clear();rangeCache.clear();}};
292
557
  }
293
- return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
558
+ return {validAnchor:validAnchor,simplifyPath:simplifyPath,compactGesturePoints:compactGesturePoints,densifyPath:densifyPath,createSpatialIndex:createSpatialIndex,queryNearestTextRect:queryNearestTextRect,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
294
559
  });
295
560
 
296
561
  /* @papyrus-epub-annotations:end */
@@ -306,6 +571,7 @@
306
571
  let annotationDocumentId;
307
572
  let annotationLabels = {};
308
573
  let epubAnnotations = [];
574
+ let epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
309
575
  const epubAnnotationRenderers = new Map();
310
576
  let spineItems = [];
311
577
  let textPages = [];
@@ -1420,11 +1686,14 @@
1420
1686
  if (href && window.PapyrusEpubAnnotations) {
1421
1687
  const renderer = window.PapyrusEpubAnnotations.install(contents, {
1422
1688
  href, get documentId() { return annotationDocumentId; },
1689
+ spineIndex:contents.sectionIndex,
1423
1690
  isCurrent: () => session === documentSessionId && activeRendition === rendition,
1424
1691
  pageWidth: () => activeRendition.layout?.width || viewer.clientWidth,
1425
1692
  annotations: () => epubAnnotations, labels: () => annotationLabels,
1693
+ onInkCommit: annotation => sendEvent('EPUB_INK_CREATED',{documentSessionId:session,annotation}),
1426
1694
  onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
1427
1695
  });
1696
+ renderer.setInkMode(epubInkMode);
1428
1697
  epubAnnotationRenderers.set(contents,renderer);
1429
1698
  }
1430
1699
  const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
@@ -1516,7 +1785,7 @@
1516
1785
  sendEvent('EPUB_TEXT_SELECTED', payload);
1517
1786
  });
1518
1787
  ['rendered','resized','layout','relocated'].forEach(name => rendition.on(name, () => {
1519
- if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1788
+ if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => name==='relocated'?renderer.schedule():renderer.invalidateLayout());
1520
1789
  }));
1521
1790
  rendition.on('relocated', (location) => {
1522
1791
  if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
@@ -1813,6 +2082,7 @@
1813
2082
  documentSessionId = payload.documentSessionId || null;
1814
2083
  epubAnnotations = [];
1815
2084
  annotationDocumentId = undefined;
2085
+ epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
1816
2086
  zoom = 1.0;
1817
2087
  epubViewMode = payload.epubMode === "chapter-scroll" ? "continuous" : "single";
1818
2088
  epubLocation = null;
@@ -1944,6 +2214,12 @@
1944
2214
  }
1945
2215
  sendResponse(id,true,null);return;
1946
2216
  }
2217
+ if (kind === 'set-epub-ink-mode') {
2218
+ if(currentType!=='epub'||!documentSessionId||payload.documentSessionId!==documentSessionId)throw new Error('Stale EPUB ink session');
2219
+ epubInkMode={enabled:payload.enabled===true,preset:['ink','highlight','underline'].indexOf(payload.preset)>=0?payload.preset:'ink',color:/^#[0-9a-f]{6}$/i.test(payload.color||'')?payload.color:'#2563eb',opacity:Number.isFinite(payload.opacity)?Math.max(0,Math.min(1,payload.opacity)):1,strokeWidth:Number.isFinite(payload.strokeWidth)?Math.max(0.0025,Math.min(0.02,payload.strokeWidth)):0.006};
2220
+ epubAnnotationRenderers.forEach(renderer=>renderer.setInkMode(epubInkMode));
2221
+ sendResponse(id,true,null);return;
2222
+ }
1947
2223
  if(kind === 'epub-go-to-location'){
1948
2224
  if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
1949
2225
  epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>{if(payload.documentSessionId!==documentSessionId)return;epubResizeAnchor?.invalidate();return rendition.display(payload.cfi);});
@@ -1,241 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { readFileSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import {
5
- getComicPreviewSize,
6
- getComicPageAspectRatio,
7
- getProtectedComicPageIndexes,
8
- isCurrentComicPageLoad,
9
- isComicImageName,
10
- patchCbrWorkerSource,
11
- sortComicPageNames,
12
- } from "./comicRuntime";
13
-
14
- describe("mobile comic runtime helpers", () => {
15
- it("keeps only supported comic image entries", () => {
16
- expect(isComicImageName("pages/001.jpg")).toBe(true);
17
- expect(isComicImageName("pages/002.webp")).toBe(true);
18
- expect(isComicImageName("pages/readme.txt")).toBe(false);
19
- expect(isComicImageName("pages/")).toBe(false);
20
- });
21
-
22
- it("sorts page names naturally", () => {
23
- expect(
24
- sortComicPageNames(["page-10.jpg", "page-2.jpg", "page-1.jpg"])
25
- ).toEqual(["page-1.jpg", "page-2.jpg", "page-10.jpg"]);
26
- });
27
-
28
- it("patches the shipped CBR worker to use the packaged WASM asset", () => {
29
- const worker = readFileSync(
30
- resolve(
31
- process.cwd(),
32
- "packages/engine-cbr-mobile/runtime/worker-bundle.js.txt"
33
- ),
34
- "utf8"
35
- );
36
-
37
- const patched = patchCbrWorkerSource(worker, "/assets/libarchive.wasm");
38
-
39
- expect(patched).not.toContain(
40
- 'new URL("libarchive.wasm",import.meta.url).href'
41
- );
42
- expect(patched).toContain(JSON.stringify("/assets/libarchive.wasm"));
43
- });
44
-
45
- it("caps comic previews without upscaling them", () => {
46
- expect(getComicPreviewSize(1600, 2400)).toEqual({ width: 240, height: 360 });
47
- expect(getComicPreviewSize(120, 180)).toEqual({ width: 120, height: 180 });
48
- });
49
-
50
- it("keeps the page geometry when a cached image URL is evicted", () => {
51
- expect(getComicPageAspectRatio(1200, 1800)).toBe("1200 / 1800");
52
- expect(getComicPageAspectRatio(0, 0)).toBe("1 / 1");
53
- });
54
-
55
- it("protects visible and pending pages from URL eviction", () => {
56
- expect(
57
- Array.from(getProtectedComicPageIndexes(1, [3, 4], [5])).sort(
58
- (left, right) => left - right
59
- )
60
- ).toEqual([-1, 0, 1, 3, 4, 5]);
61
- });
62
-
63
- it("rejects page extraction completions from an older document", () => {
64
- const entry = { name: "old.jpg" };
65
- const currentEntry = { name: "new.jpg" };
66
-
67
- expect(isCurrentComicPageLoad(2, 3, entry, currentEntry)).toBe(false);
68
- expect(isCurrentComicPageLoad(3, 3, entry, currentEntry)).toBe(false);
69
- expect(isCurrentComicPageLoad(3, 3, entry, entry)).toBe(true);
70
- });
71
-
72
- it("embeds the tested helpers in the shipped runtimes", () => {
73
- const runtime = readFileSync(
74
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
75
- "utf8"
76
- );
77
- const html = readFileSync(
78
- resolve(process.cwd(), "packages/ui-react-native/runtime/index.html"),
79
- "utf8"
80
- );
81
-
82
- expect(runtime.match(/function getComicPageAspectRatio/g)).toHaveLength(1);
83
- expect(html.match(/function getComicPageAspectRatio/g)).toHaveLength(1);
84
- expect(html).toContain("root.PapyrusComicRuntime");
85
- expect(runtime).toContain("getProtectedComicPageIndexes");
86
- expect(html).toContain("getProtectedComicPageIndexes");
87
- expect(runtime).toContain("file-chunk-request");
88
- expect(html).toContain("file-chunk-request");
89
- });
90
-
91
- it("loads local EPUB files through the native chunk bridge", () => {
92
- const runtime = readFileSync(
93
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
94
- "utf8"
95
- );
96
- const html = readFileSync(
97
- resolve(process.cwd(), "packages/ui-react-native/runtime/index.html"),
98
- "utf8"
99
- );
100
-
101
- expect(runtime).toContain("sourceToArrayBuffer(source),");
102
- expect(html).toContain("sourceToArrayBuffer(source),");
103
- expect(runtime).not.toContain("data = source.uri;");
104
- expect(html).not.toContain("data = source.uri;");
105
- });
106
-
107
- it("passes an exact ArrayBuffer to epub.js for base64 sources", () => {
108
- const runtime = readFileSync(
109
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
110
- "utf8"
111
- );
112
- const html = readFileSync(
113
- resolve(process.cwd(), "packages/ui-react-native/runtime/index.html"),
114
- "utf8"
115
- );
116
-
117
- expect(runtime).toContain("const toExactArrayBuffer = (bytes) =>");
118
- expect(html).toContain("const toExactArrayBuffer = (bytes) =>");
119
- expect(runtime).toContain("data = toExactArrayBuffer(decodeBase64(source.data));");
120
- expect(html).toContain("data = toExactArrayBuffer(decodeBase64(source.data));");
121
- });
122
-
123
- it("correlates EPUB diagnostics and load terminals by request id", () => {
124
- const runtime = readFileSync(
125
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
126
- "utf8"
127
- );
128
- const html = readFileSync(
129
- resolve(process.cwd(), "packages/ui-react-native/runtime/index.html"),
130
- "utf8"
131
- );
132
- for (const artifact of [runtime, html]) {
133
- expect(artifact).toContain("epub.load.start");
134
- expect(artifact).toContain("epub.book.ready");
135
- expect(artifact).toContain("epub.display.start");
136
- expect(artifact).toContain("epub.load.ready");
137
- expect(artifact).toContain("epub.load.error");
138
- expect(artifact).toContain("epub.load.timeout");
139
- expect(artifact).toContain("loadId");
140
- expect(artifact).toContain("document.ready");
141
- expect(artifact).toContain("document.error");
142
- expect(artifact).toContain("withEpubStageTimeout");
143
- }
144
- });
145
-
146
- it("invalidates a pending EPUB load when another document starts", () => {
147
- const runtime = readFileSync(
148
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
149
- "utf8"
150
- );
151
- const loadStart = runtime.indexOf("if (kind === 'load')");
152
- const generationInvalidation = runtime.indexOf(
153
- "epubLoadGeneration += 1",
154
- loadStart
155
- );
156
- const currentTypeAssignment = runtime.indexOf(
157
- "currentType = payload.type",
158
- loadStart
159
- );
160
-
161
- expect(generationInvalidation).toBeGreaterThan(loadStart);
162
- expect(generationInvalidation).toBeLessThan(currentTypeAssignment);
163
- expect(runtime).toContain(
164
- "const loadEpub = async (source, loadId, generation) =>"
165
- );
166
- expect(runtime).toContain(
167
- "const result = await loadEpub(payload.source, id, generation);"
168
- );
169
- });
170
-
171
- it("rejects a stale EPUB load after outline resolution", () => {
172
- const runtime = readFileSync(
173
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
174
- "utf8"
175
- );
176
- const outlineStart = runtime.indexOf("const outline = await withEpubStageTimeout(");
177
- const outlineGuard = runtime.indexOf(
178
- "if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');",
179
- outlineStart
180
- );
181
- const readyEvent = runtime.indexOf(
182
- "sendEpubDiagnostic('epub.load.ready'",
183
- outlineStart
184
- );
185
-
186
- expect(outlineStart).toBeGreaterThan(-1);
187
- expect(outlineGuard).toBeGreaterThan(outlineStart);
188
- expect(outlineGuard).toBeLessThan(readyEvent);
189
- });
190
-
191
- it("ships the paginated and chapter-scroll EPUB reading policy", () => {
192
- const runtime = readFileSync(
193
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
194
- "utf8"
195
- );
196
- const html = readFileSync(
197
- resolve(process.cwd(), "packages/ui-react-native/runtime/index.html"),
198
- "utf8"
199
- );
200
-
201
- expect(runtime).toContain("manager:'default'");
202
- expect(html).toContain("manager:'default'");
203
- expect(runtime).toContain("'scrolled-doc'");
204
- expect(html).toContain("'scrolled-doc'");
205
- expect(runtime).toContain("rendition.on('relocated'");
206
- expect(html).toContain("rendition.on('relocated'");
207
- expect(runtime).toContain("sendEvent('VIEWER_SCROLL'");
208
- expect(html).toContain("sendEvent('VIEWER_SCROLL'");
209
- expect(runtime).toContain("sendEvent('EPUB_CONTENT_TAP'");
210
- expect(html).toContain("sendEvent('EPUB_CONTENT_TAP'");
211
- expect(runtime).toContain("'paginated'");
212
- expect(html).toContain("'paginated'");
213
- });
214
-
215
- it("observes the real EPUB manager queue without changing queue semantics", () => {
216
- const runtime = readFileSync(
217
- resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
218
- "utf8"
219
- );
220
- const html = readFileSync(
221
- resolve(process.cwd(), "packages/ui-react-native/runtime/index.html"),
222
- "utf8"
223
- );
224
-
225
- for (const artifact of [runtime, html]) {
226
- expect(artifact).toContain("const originalCheck = typeof manager.check");
227
- expect(artifact).toContain("manager.check = function (...args)");
228
- expect(artifact).toContain("const originalEnqueue = queue && typeof queue.enqueue");
229
- expect(artifact).toContain("queue.enqueue = function (...args)");
230
- expect(artifact).toContain("manager.queue.enqueue");
231
- expect(artifact).toContain("queue.enqueue = originalEnqueue");
232
- expect(artifact).toContain("manager.check.request");
233
- expect(artifact).toContain("manager.check.start");
234
- expect(artifact).toContain("manager.check.end");
235
- expect(artifact).toContain("epubSelectionCleanups");
236
- expect(artifact).toContain("__papyrusSelectionCleanup");
237
- expect(artifact).toContain("setSelectionActive(false)");
238
- expect(artifact).not.toContain("String(task)");
239
- }
240
- });
241
- });