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

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,205 @@
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 previewSvg = doc.createElementNS('http://www.w3.org/2000/svg','svg');
317
+ previewSvg.setAttribute('data-papyrus-ink-preview-overlay','');
318
+ previewSvg.style.cssText = 'position:fixed;left:0;top:0;overflow:hidden;pointer-events:none;z-index:2147482999';
319
+ doc.documentElement.appendChild(previewSvg);
320
+ var inkCanvas = doc.createElement('canvas');
321
+ inkCanvas.dataset.papyrusInkCapture = '';
322
+ inkCanvas.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;z-index:2147483000;background:transparent;pointer-events:none;touch-action:none';
323
+ doc.documentElement.appendChild(inkCanvas);
324
+ var inkContext = null; try { if(typeof win.CanvasRenderingContext2D==='function')inkContext = inkCanvas.getContext('2d'); } catch (_) {}
325
+ function resizeCanvas() {
326
+ var ratio = Math.max(1,Math.min(3,win.devicePixelRatio || 1));
327
+ var width=Math.max(1,Math.min(4096,win.innerWidth||1));
328
+ var height=Math.max(1,Math.min(4096,win.innerHeight||1));
329
+ inkCanvas.style.width=width+'px';inkCanvas.style.height=height+'px';
330
+ previewSvg.setAttribute('width',String(width));previewSvg.setAttribute('height',String(height));
331
+ previewSvg.setAttribute('viewBox','0 0 '+width+' '+height);
332
+ inkCanvas.width = Math.round(width*ratio); inkCanvas.height = Math.round(height*ratio);
333
+ if (inkContext) { inkContext.setTransform(ratio,0,0,ratio,0,0); inkContext.clearRect(0,0,width,height); }
334
+ }
335
+ function collectTextRects() {
336
+ var walker = doc.createTreeWalker(doc.body,4), node, entries=[];
337
+ while ((node=walker.nextNode())) {
338
+ var parent=node.parentElement;
339
+ if (!parent || parent.closest('script,style,noscript,textarea,[data-papyrus-overlay],[data-papyrus-ink-capture]')) continue;
340
+ var text=node.nodeValue || '', style=win.getComputedStyle(parent), computed=parseFloat(style.lineHeight);
341
+ for (var start=0; start<text.length;) {
342
+ var end=Math.min(text.length,start+24), last=text.charCodeAt(end-1), next=text.charCodeAt(end);
343
+ if(end<text.length&&last>=0xD800&&last<=0xDBFF&&next>=0xDC00&&next<=0xDFFF)end++;
344
+ var range=doc.createRange(); range.setStart(node,start); range.setEnd(node,end);
345
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
346
+ rects.forEach(function(rect) {
347
+ if (!(rect.width>0 && rect.height>0)) return;
348
+ 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,
349
+ node:node,start:start,end:end,lineHeight:Number.isFinite(computed)&&computed>0?computed:rect.height});
350
+ });
351
+ start=end;
352
+ }
353
+ }
354
+ return entries;
355
+ }
356
+ function rebuildSpatialIndex() { spatialIndex=createSpatialIndex(collectTextRects(),64); rangeCache.clear(); }
357
+ function inkViewportExtent() {
358
+ var root=doc.documentElement,body=doc.body,bodyRect=body.getBoundingClientRect();
359
+ var width=root.clientWidth||body.clientWidth||bodyRect.width||win.innerWidth||1;
360
+ var height=root.clientHeight||body.clientHeight||bodyRect.height||win.innerHeight||1;
361
+ return Math.max(1,Math.min(4096,width),Math.min(4096,height));
362
+ }
363
+ function inkStrokeWidthPx(width,preset) {
364
+ var scale=preset==='highlight'?0.8:preset==='underline'?0.1:0.15;
365
+ return Math.max(1,Math.min(24,(Number.isFinite(width)?width:0.006)*inkViewportExtent()*scale));
366
+ }
367
+ function closestGlyph(entry,x,y,maxDistance) {
368
+ var source=(entry.node.nodeValue || '').slice(entry.start,entry.end), best=null, offset=0;
369
+ for (var glyph of source) {
370
+ var range=doc.createRange(); range.setStart(entry.node,entry.start+offset); range.setEnd(entry.node,entry.start+offset+glyph.length); offset+=glyph.length;
371
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
372
+ rects.forEach(function(rect) {
373
+ if (!(rect.width>0 && rect.height>0)) return;
374
+ 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));
375
+ var dx=x-nx,dy=y-ny,distance=Math.sqrt(dx*dx+dy*dy);
376
+ if (distance<=maxDistance && (!best || distance<best.distance)) best={range:range,rect:rect,distance:distance};
377
+ });
378
+ }
379
+ if (!best || typeof contents.cfiFromRange!=='function') return null;
380
+ var quote=best.range.toString(), cfiRange; try { cfiRange=contents.cfiFromRange(best.range); } catch (_) { return null; }
381
+ if (!quote || typeof cfiRange!=='string' || !/^epubcfi\([^\n<>]+\)$/.test(cfiRange)) return null;
382
+ var lineHeight=Math.max(1,entry.lineHeight || best.rect.height), px=x-(best.rect.left+win.scrollX), py=y-(best.rect.top+win.scrollY);
383
+ return {cfiRange:cfiRange,quote:quote,x:Math.max(-3,Math.min(3,px/lineHeight)),y:Math.max(-3,Math.min(3,py/lineHeight))};
384
+ }
385
+ function renderInkAnnotation(annotation,keep,bodyRect) {
386
+ var baseRect=bodyRect || doc.body.getBoundingClientRect() || {left:0,top:0};
387
+ var data=annotation.epubInk;
388
+ if (!data || data.version!==1 || data.kind!=='epub-ink' || !Array.isArray(data.segments)) return;
389
+ keep.add(annotation.id);
390
+ var group=inkRegistry.get(annotation.id);
391
+ 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); }
392
+ group.replaceChildren();
393
+ var color=/^#[0-9a-f]{6}$/i.test(annotation.color)?annotation.color:'#2563eb';
394
+ var opacity=Number.isFinite(annotation.opacity)?Math.max(0,Math.min(1,annotation.opacity)):1;
395
+ var preset=['ink','highlight','underline'].indexOf(data.preset)>=0?data.preset:'ink';
396
+ var stroke=inkStrokeWidthPx(annotation.strokeWidth,preset);
397
+ data.segments.forEach(function(segment) {
398
+ if (segment.href!==options.href || !Array.isArray(segment.points)) return;
399
+ var paths=[], current=[];
400
+ segment.points.forEach(function(point) {
401
+ var range=rangeCache.get(point.cfiRange);
402
+ if (!range) { try { range=contents.range(point.cfiRange); } catch (_) { range=null; } if (range) rangeCache.set(point.cfiRange,range); }
403
+ if (!range || range.toString()!==point.quote) { if(current.length>1)paths.push(current);current=[];return; }
404
+ var rect; try { rect=range.getBoundingClientRect(); } catch (_) { rect=null; }
405
+ if (!rect || !(rect.width>0 && rect.height>0)) { if(current.length>1)paths.push(current);current=[];return; }
406
+ var lineHeight=Math.max(1,rect.height), x=rect.left-baseRect.left+point.x*lineHeight, y=rect.top-baseRect.top+point.y*lineHeight;
407
+ var previous=current[current.length-1], jumpLimit=Math.max(24,lineHeight*2.5);
408
+ if (previous && Math.hypot(x-previous.x,y-previous.y)>jumpLimit) { if(current.length>1)paths.push(current);current=[]; }
409
+ current.push({x:x,y:y,lineHeight:lineHeight});
410
+ });
411
+ if(current.length>1)paths.push(current);
412
+ paths.forEach(function(points) {
413
+ var d='M '+points[0].x.toFixed(1)+' '+points[0].y.toFixed(1);
414
+ 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);}
415
+ var last=points[points.length-1];d+=' L '+last.x.toFixed(1)+' '+last.y.toFixed(1);
416
+ 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);
417
+ });
418
+ });
419
+ }
420
+ function clearPreview() {
421
+ if(inkContext)inkContext.clearRect(0,0,inkCanvas.width,inkCanvas.height);
422
+ previewSvg.replaceChildren();
423
+ }
424
+ function createPreviewPath(x,y) {
425
+ var path=doc.createElementNS('http://www.w3.org/2000/svg','path');
426
+ path.setAttribute('data-papyrus-ink-preview','');path.setAttribute('d','M '+x.toFixed(1)+' '+y.toFixed(1));path.setAttribute('fill','none');
427
+ path.setAttribute('stroke',inkConfig.color);path.setAttribute('stroke-width',String(inkStrokeWidthPx(inkConfig.strokeWidth,inkConfig.preset)));
428
+ path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('opacity',String(inkConfig.opacity));
429
+ path.style.pointerEvents='none';previewSvg.appendChild(path);return path;
430
+ }
431
+ function updatePreviewPath(clientX,clientY) {
432
+ var x=clientX,y=clientY;
433
+ var gesture=activeGesture;
434
+ var startsSegment=!gesture.previewPath||gesture.previewPathPoints>=32;
435
+ if(startsSegment){
436
+ gesture.previewPath=createPreviewPath(gesture.previewLast?gesture.previewLast.x:x,gesture.previewLast?gesture.previewLast.y:y);
437
+ gesture.previewPathPoints=1;
438
+ }
439
+ if(!startsSegment||gesture.previewLast){
440
+ var pathData=gesture.previewPath.getAttribute('d')||'';
441
+ gesture.previewPath.setAttribute('d',pathData+' L '+x.toFixed(1)+' '+y.toFixed(1));
442
+ gesture.previewPathPoints++;
443
+ }
444
+ gesture.previewLast={x:x,y:y};
445
+ }
446
+ function cancelGesture() {
447
+ var gesture=activeGesture; activeGesture=null; clearPreview();
448
+ if(gesture)try{inkCanvas.releasePointerCapture(gesture.pointerId);}catch(_){}
449
+ }
450
+ function commitGesture() {
451
+ var gesture=activeGesture; activeGesture=null; clearPreview();
452
+ if(!gesture || !alive || !options.isCurrent() || !inkConfig.enabled || !spatialIndex) return;
453
+ var simplified=simplifyPath(gesture.points,1.5,512), anchorSamples=densifyPath(simplified,12,512), segments=[], current=[];
454
+ anchorSamples.forEach(function(point) {
455
+ var coarse=queryNearestTextRect(spatialIndex,point.x,point.y,28);
456
+ var cutoff=coarse?Math.max(12,Math.min(28,0.8*coarse.entry.lineHeight)):12;
457
+ var anchored=coarse && closestGlyph(coarse.entry,point.x,point.y,cutoff);
458
+ if(!anchored){if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});current=[];return;}
459
+ current.push(anchored);
460
+ });
461
+ if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});
462
+ segments=segments.slice(0,64);
463
+ var pointCount=segments.reduce(function(total,segment){return total+segment.points.length;},0);
464
+ if(!pointCount)return;
465
+ var width=Math.max(1,doc.documentElement.scrollWidth,win.innerWidth),height=Math.max(1,doc.documentElement.scrollHeight,win.innerHeight);
466
+ 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))};});
467
+ 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;}));
468
+ 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;}));
469
+ var first=segments[0].points[0], href=segments[0].href, documentId=typeof options.documentId==='function'?options.documentId():options.documentId;
470
+ var annotation={id:'epub-ink-'+Date.now()+'-'+Math.random().toString(36).slice(2,9),type:'ink',pageIndex:(options.spineIndex||0)+1,
471
+ 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,
472
+ anchor:{version:1,kind:'epub-cfi',cfiRange:first.cfiRange,href:href,spineIndex:options.spineIndex,documentId:documentId,quote:first.quote},
473
+ epubInk:{version:1,kind:'epub-ink',documentId:documentId,preset:inkConfig.preset,segments:segments}};
474
+ if(typeof options.onInkCommit==='function')options.onInkCommit(annotation);
475
+ }
476
+ function pointerDown(event) {
477
+ if(!inkConfig.enabled || !alive || !options.isCurrent() || event.button!==undefined && event.button!==0)return;
478
+ if(activeGesture){cancelGesture();return;}
479
+ event.preventDefault();event.stopPropagation();
480
+ 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};
481
+ updatePreviewPath(event.clientX,event.clientY);
482
+ try{inkCanvas.setPointerCapture(event.pointerId);}catch(_){}
483
+ }
484
+ function pointerMove(event) {
485
+ if(!activeGesture || activeGesture.pointerId!==event.pointerId)return;
486
+ if(!options.isCurrent()||!inkConfig.enabled){cancelGesture();return;}
487
+ event.preventDefault();event.stopPropagation();
488
+ var point={x:event.clientX+win.scrollX,y:event.clientY+win.scrollY};activeGesture.points.push(point);
489
+ if(activeGesture.points.length>2048)activeGesture.points=compactGesturePoints(activeGesture.points,1024);
490
+ 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();}
491
+ updatePreviewPath(event.clientX,event.clientY);
492
+ activeGesture.last={x:event.clientX,y:event.clientY};
493
+ }
494
+ 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();}
495
+ function pointerCancel(event) {if(activeGesture && (event.pointerId===undefined || activeGesture.pointerId===event.pointerId))cancelGesture();}
496
+ resizeCanvas();rebuildSpatialIndex();
497
+ inkCanvas.addEventListener('pointerdown',pointerDown);inkCanvas.addEventListener('pointermove',pointerMove);inkCanvas.addEventListener('pointerup',pointerUp);inkCanvas.addEventListener('pointercancel',pointerCancel);inkCanvas.addEventListener('lostpointercapture',pointerCancel);
498
+ win.addEventListener('blur',pointerCancel);
233
499
  function render() {
234
500
  frame = 0; if (!alive || !options.isCurrent()) return;
235
- var keep = new Set(), noteGroups = new Map();
501
+ var keep = new Set(), keepInk = new Set(), noteGroups = new Map();
236
502
  hitRegions = [];
503
+ var bodyRect=doc.body.getBoundingClientRect() || {left:0,top:0};
504
+ inkSvg.setAttribute('width',String(Math.max(doc.documentElement.scrollWidth,win.innerWidth)));
505
+ inkSvg.setAttribute('height',String(Math.max(doc.documentElement.scrollHeight,win.innerHeight)));
237
506
  options.annotations().forEach(function(annotation) {
238
507
  var anchor = annotation.anchor;
239
508
  if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
509
+ if(annotation.type==='ink' && annotation.epubInk){renderInkAnnotation(annotation,keepInk,bodyRect);return;}
240
510
  var range = resolveRange(contents,anchor); if (!range) return;
241
511
  var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
242
512
  if (!rects.length) return;
@@ -275,22 +545,26 @@
275
545
  group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
276
546
  });
277
547
  registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
548
+ inkRegistry.forEach(function(group,id){if (!keepInk.has(id)) {group.remove();inkRegistry.delete(id);}});
278
549
  }
279
550
  function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
551
+ function rebuildAndSchedule(){if(!alive)return;rebuildSpatialIndex();schedule();}
552
+ function handleResize(){resizeCanvas();rebuildAndSchedule();}
553
+ function handleDocumentLoad(){rebuildAndSchedule();}
280
554
  function tapMarkup(event) {
281
555
  if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
282
556
  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
557
  if(hit) {event.stopPropagation();options.onTap(hit.id);}
284
558
  }
285
559
  doc.addEventListener('click',tapMarkup);
286
- var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
560
+ var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(rebuildAndSchedule) : null;
287
561
  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);
562
+ win.addEventListener('resize',handleResize);win.addEventListener('scroll',schedule); doc.addEventListener('load',handleDocumentLoad,true);
563
+ if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(function(){if(alive){rebuildSpatialIndex();schedule();}});
290
564
  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();}};
565
+ 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();previewSvg.remove();overlay.remove();doc.body.style.position=originalPosition;registry.clear();inkRegistry.clear();rangeCache.clear();}};
292
566
  }
293
- return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
567
+ return {validAnchor:validAnchor,simplifyPath:simplifyPath,compactGesturePoints:compactGesturePoints,densifyPath:densifyPath,createSpatialIndex:createSpatialIndex,queryNearestTextRect:queryNearestTextRect,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
294
568
  });
295
569
 
296
570
  /* @papyrus-epub-annotations:end */
@@ -306,6 +580,7 @@
306
580
  let annotationDocumentId;
307
581
  let annotationLabels = {};
308
582
  let epubAnnotations = [];
583
+ let epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
309
584
  const epubAnnotationRenderers = new Map();
310
585
  let spineItems = [];
311
586
  let textPages = [];
@@ -1420,11 +1695,14 @@
1420
1695
  if (href && window.PapyrusEpubAnnotations) {
1421
1696
  const renderer = window.PapyrusEpubAnnotations.install(contents, {
1422
1697
  href, get documentId() { return annotationDocumentId; },
1698
+ spineIndex:contents.sectionIndex,
1423
1699
  isCurrent: () => session === documentSessionId && activeRendition === rendition,
1424
1700
  pageWidth: () => activeRendition.layout?.width || viewer.clientWidth,
1425
1701
  annotations: () => epubAnnotations, labels: () => annotationLabels,
1702
+ onInkCommit: annotation => sendEvent('EPUB_INK_CREATED',{documentSessionId:session,annotation}),
1426
1703
  onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
1427
1704
  });
1705
+ renderer.setInkMode(epubInkMode);
1428
1706
  epubAnnotationRenderers.set(contents,renderer);
1429
1707
  }
1430
1708
  const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
@@ -1516,7 +1794,7 @@
1516
1794
  sendEvent('EPUB_TEXT_SELECTED', payload);
1517
1795
  });
1518
1796
  ['rendered','resized','layout','relocated'].forEach(name => rendition.on(name, () => {
1519
- if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1797
+ if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => name==='relocated'?renderer.schedule():renderer.invalidateLayout());
1520
1798
  }));
1521
1799
  rendition.on('relocated', (location) => {
1522
1800
  if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
@@ -1813,6 +2091,7 @@
1813
2091
  documentSessionId = payload.documentSessionId || null;
1814
2092
  epubAnnotations = [];
1815
2093
  annotationDocumentId = undefined;
2094
+ epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
1816
2095
  zoom = 1.0;
1817
2096
  epubViewMode = payload.epubMode === "chapter-scroll" ? "continuous" : "single";
1818
2097
  epubLocation = null;
@@ -1944,6 +2223,12 @@
1944
2223
  }
1945
2224
  sendResponse(id,true,null);return;
1946
2225
  }
2226
+ if (kind === 'set-epub-ink-mode') {
2227
+ if(currentType!=='epub'||!documentSessionId||payload.documentSessionId!==documentSessionId)throw new Error('Stale EPUB ink session');
2228
+ 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};
2229
+ epubAnnotationRenderers.forEach(renderer=>renderer.setInkMode(epubInkMode));
2230
+ sendResponse(id,true,null);return;
2231
+ }
1947
2232
  if(kind === 'epub-go-to-location'){
1948
2233
  if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
1949
2234
  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
- });