screengraft 0.54.1 → 0.54.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/ui/index.html +60 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screengraft",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.3",
|
|
4
4
|
"description": "Put a UI screenshot or screen recording onto a photographed device screen with the perspective exactly right \u2014 a homography you confirm by hand, not a generative guess.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mockup",
|
package/ui/index.html
CHANGED
|
@@ -1472,14 +1472,29 @@ const recall = (k,d) => { try{ const v = localStorage.getItem('sg.'+k); return v
|
|
|
1472
1472
|
The dark casing underneath is NOT optional and stays: flat accent measures
|
|
1473
1473
|
~1.06:1 against mid-tone photography, which is the failure the casing was
|
|
1474
1474
|
introduced to fix. CORE_IDLE stays white for the strip's idle rule. */
|
|
1475
|
-
const CASE_A = 'rgba(0,0,0,.
|
|
1475
|
+
const CASE_A = 'rgba(0,0,0,.45)', CORE_IDLE = 'rgba(255,255,255,.92)';
|
|
1476
1476
|
const CORE_ACTIVE = '#f5623d';
|
|
1477
1477
|
const CORE_QUAD = '#f23b0d';
|
|
1478
|
+
// Line weights for the fit overlay, in one place. Thinned 13 Sep 2026 on
|
|
1479
|
+
// request: quad 1 -> 0.8, active edge 2 -> 1.5, casing +2.5 -> +1.6 at 45%
|
|
1480
|
+
// black instead of 62%. The casing is still there -- it is the part that
|
|
1481
|
+
// survives an arbitrary photograph -- just lighter.
|
|
1482
|
+
// There is no native rotate cursor; this is a 22px circular arrow, white
|
|
1483
|
+
// with a dark outline so it reads on any photograph, hotspot at its centre.
|
|
1484
|
+
const ROTATE_CURSOR = 'url("data:image/svg+xml;utf8,' + encodeURIComponent(
|
|
1485
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">' +
|
|
1486
|
+
'<path d="M17.5 11a6.5 6.5 0 1 1-2.2-4.9" fill="none" stroke="rgba(0,0,0,.85)" stroke-width="4.2" stroke-linecap="round"/>' +
|
|
1487
|
+
'<path d="M17.5 11a6.5 6.5 0 1 1-2.2-4.9" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/>' +
|
|
1488
|
+
'<path d="M15.6 2.6l0.4 4.2-4.2 0.3" fill="none" stroke="rgba(0,0,0,.85)" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
1489
|
+
'<path d="M15.6 2.6l0.4 4.2-4.2 0.3" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
1490
|
+
'</svg>') + '") 11 11, crosshair';
|
|
1491
|
+
const QUAD_W = 0.8, QUAD_ACTIVE_W = 1.5, GUIDE_W = 0.8, CASE_PAD = 1.6;
|
|
1492
|
+
const GUIDE_DASH = [10, 7];
|
|
1478
1493
|
function cased(c, path, coreW, coreColor, dash){
|
|
1479
1494
|
c.save();
|
|
1480
1495
|
if (dash) c.setLineDash(dash);
|
|
1481
1496
|
c.lineCap = 'round'; c.lineJoin = 'round';
|
|
1482
|
-
c.strokeStyle = CASE_A; c.lineWidth = coreW +
|
|
1497
|
+
c.strokeStyle = CASE_A; c.lineWidth = coreW + CASE_PAD; path(); c.stroke();
|
|
1483
1498
|
c.strokeStyle = coreColor; c.lineWidth = coreW; path(); c.stroke();
|
|
1484
1499
|
c.restore();
|
|
1485
1500
|
}
|
|
@@ -1488,8 +1503,8 @@ function cased(c, path, coreW, coreColor, dash){
|
|
|
1488
1503
|
readability guarantee that makes markers survive an arbitrary photograph. */
|
|
1489
1504
|
function casedDisc(c, x, y, r, coreColor){
|
|
1490
1505
|
c.save();
|
|
1491
|
-
c.strokeStyle = CASE_A; c.lineWidth =
|
|
1492
|
-
c.beginPath(); c.arc(x, y, r + 0.
|
|
1506
|
+
c.strokeStyle = CASE_A; c.lineWidth = 1.8;
|
|
1507
|
+
c.beginPath(); c.arc(x, y, r + 0.6, 0, Math.PI*2); c.stroke();
|
|
1493
1508
|
c.fillStyle = coreColor;
|
|
1494
1509
|
c.beginPath(); c.arc(x, y, r, 0, Math.PI*2); c.fill();
|
|
1495
1510
|
c.restore();
|
|
@@ -2285,7 +2300,7 @@ function draw(){
|
|
|
2285
2300
|
const a=P[active.i], b=P[(active.i+1)%4], d=unit(sub(b,a)), big=4000;
|
|
2286
2301
|
cased(ctx, () => { ctx.beginPath();
|
|
2287
2302
|
ctx.moveTo(a[0]-d[0]*big, a[1]-d[1]*big); ctx.lineTo(b[0]+d[0]*big, b[1]+d[1]*big); },
|
|
2288
|
-
|
|
2303
|
+
GUIDE_W, CORE_ACTIVE, GUIDE_DASH);
|
|
2289
2304
|
}
|
|
2290
2305
|
|
|
2291
2306
|
for (let i=0;i<4;i++){
|
|
@@ -2300,7 +2315,7 @@ function draw(){
|
|
|
2300
2315
|
const s1 = add(a, mul(d, Math.max(0, L - gap)));
|
|
2301
2316
|
if (L > 1){
|
|
2302
2317
|
cased(ctx, () => { ctx.beginPath(); ctx.moveTo(s0[0],s0[1]); ctx.lineTo(s1[0],s1[1]); },
|
|
2303
|
-
on ?
|
|
2318
|
+
on ? QUAD_ACTIVE_W : QUAD_W, CORE_QUAD);
|
|
2304
2319
|
}
|
|
2305
2320
|
// Pivot pips on the active edge keep their meaning: grab near an end to
|
|
2306
2321
|
// rotate. Filled, like the corners, but deliberately smaller — size is what
|
|
@@ -2382,7 +2397,7 @@ cv.onpointermove = e => {
|
|
|
2382
2397
|
const h = hitTest(p);
|
|
2383
2398
|
const changed = JSON.stringify(h) !== JSON.stringify(hover);
|
|
2384
2399
|
hover = h;
|
|
2385
|
-
cv.style.cursor = h ? (h.kind==='corner' ? '
|
|
2400
|
+
cv.style.cursor = h ? (h.kind==='corner' ? 'move' : (h.mode==='translate' ? 'move' : ROTATE_CURSOR)) : 'default';
|
|
2386
2401
|
if (changed){ draw(); drawStrip(); }
|
|
2387
2402
|
return;
|
|
2388
2403
|
}
|
|
@@ -2655,6 +2670,10 @@ $('#dofMeasure').onclick = measureDof;
|
|
|
2655
2670
|
const gz = $('#dofGizmo');
|
|
2656
2671
|
const SIGMA_FULL_FRAC = 0.02; // mirrors dof.DOF_MAX_FRAC -- keep in step
|
|
2657
2672
|
let dofEnd = parseFloat(recall('dofEnd','1')); if (!(dofEnd > 0)) dofEnd = 1;
|
|
2673
|
+
// Where along the lines the handles sit (0..1 across the screen's extent).
|
|
2674
|
+
// Page-only: it changes nothing in the render. Turning re-derives it so the
|
|
2675
|
+
// handle stays on the point you turned about instead of sliding along the line.
|
|
2676
|
+
let dofK = 0.5;
|
|
2658
2677
|
// The near limit. null = one-sided (the plane of focus at the screen's edge).
|
|
2659
2678
|
let dofEnd2 = recall('dofEnd2','') === '' ? null : parseFloat(recall('dofEnd2','')); if (dofEnd2 !== null && !(dofEnd2 < 1)) dofEnd2 = null;
|
|
2660
2679
|
function dofEndV(){ return dofOn ? dofEnd : 1; }
|
|
@@ -2695,10 +2714,10 @@ function paintGizmo(){
|
|
|
2695
2714
|
const k = W / Math.max(im.clientWidth, 1); // photo px per screen px
|
|
2696
2715
|
const g = gizmoGeom(); if (!g.H || !g.Hi) return;
|
|
2697
2716
|
const P = (f, q) => applyH(g.H, gzPt(g, f, q));
|
|
2698
|
-
const a0 = P(dofStart, -0.06), a1 = P(dofStart, 1.06), ac = P(dofStart,
|
|
2699
|
-
const b0 = P(dofEnd, -0.06), b1 = P(dofEnd, 1.06), bc = P(dofEnd,
|
|
2717
|
+
const a0 = P(dofStart, -0.06), a1 = P(dofStart, 1.06), ac = P(dofStart, dofK);
|
|
2718
|
+
const b0 = P(dofEnd, -0.06), b1 = P(dofEnd, 1.06), bc = P(dofEnd, dofK);
|
|
2700
2719
|
const two = dofEnd2 !== null;
|
|
2701
|
-
const c0 = two ? P(dofEnd2, -0.06) : null, c1 = two ? P(dofEnd2, 1.06) : null, cc = two ? P(dofEnd2,
|
|
2720
|
+
const c0 = two ? P(dofEnd2, -0.06) : null, c1 = two ? P(dofEnd2, 1.06) : null, cc = two ? P(dofEnd2, dofK) : null;
|
|
2702
2721
|
const th = P(dofEnd, 0.08 + 0.84 * dofStr); // the strength thumb slides along the dashed line
|
|
2703
2722
|
const sigma = dofStr * SIGMA_FULL_FRAC * g.side;
|
|
2704
2723
|
const r = 6 * k, rp = 4 * k, fs11 = 11 * k, off = 14 * k;
|
|
@@ -2733,7 +2752,19 @@ let gzDrag = null;
|
|
|
2733
2752
|
gz.addEventListener('pointerdown', e => {
|
|
2734
2753
|
const h = e.target.dataset && e.target.dataset.h; if (!h) return;
|
|
2735
2754
|
e.preventDefault(); gz.setPointerCapture(e.pointerId);
|
|
2736
|
-
gzDrag = {h};
|
|
2755
|
+
gzDrag = {h};
|
|
2756
|
+
if (h === 'rotA' || h === 'rotB'){
|
|
2757
|
+
// Turning keeps the SHARP line's centre where it is on the screen and the
|
|
2758
|
+
// limits at their distances from it, in screenshot pixels. The stored
|
|
2759
|
+
// numbers are fractions of the screen's extent along the direction, and
|
|
2760
|
+
// that extent changes with the angle -- so turning on the fractions alone
|
|
2761
|
+
// slid every line as it turned (reported 13 Sep 2026).
|
|
2762
|
+
const g = gizmoGeom();
|
|
2763
|
+
gzDrag.pivot = gzPt(g, dofStart, dofK);
|
|
2764
|
+
gzDrag.dFar = (dofEnd - dofStart) * (g.hi - g.lo);
|
|
2765
|
+
gzDrag.dNear = dofEnd2 === null ? null : (dofStart - dofEnd2) * (g.hi - g.lo);
|
|
2766
|
+
}
|
|
2767
|
+
gz.classList.add('drag');
|
|
2737
2768
|
});
|
|
2738
2769
|
gz.addEventListener('pointermove', e => {
|
|
2739
2770
|
if (!gzDrag) return;
|
|
@@ -2752,11 +2783,26 @@ gz.addEventListener('pointermove', e => {
|
|
|
2752
2783
|
} else {
|
|
2753
2784
|
// Turn about the SHARP line's centre -- the plane of focus is what turns;
|
|
2754
2785
|
// the pip under the pointer follows.
|
|
2755
|
-
const c =
|
|
2786
|
+
const c = gzDrag.pivot;
|
|
2756
2787
|
const v = [p[0]-c[0], p[1]-c[1]];
|
|
2757
2788
|
const sign = gzDrag.h === 'rotB' ? 1 : -1; // rotB sits at +u, rotA at -u
|
|
2758
|
-
|
|
2759
|
-
if (e.shiftKey)
|
|
2789
|
+
let ang = ((Math.atan2(v[1]*sign, v[0]*sign) * 180 / Math.PI - 90) % 360 + 360) % 360;
|
|
2790
|
+
if (e.shiftKey) ang = Math.round(ang / 15) * 15;
|
|
2791
|
+
else {
|
|
2792
|
+
// Magnetic to the screen's own axes: within 4° of 0/90/180/270 the lines
|
|
2793
|
+
// run exactly along the screen's edges, which is what "along the phone"
|
|
2794
|
+
// means in the photograph once the perspective is applied.
|
|
2795
|
+
const near = Math.round(ang / 90) * 90;
|
|
2796
|
+
if (Math.abs(((ang - near) + 180) % 360 - 180) < 4) ang = near % 360;
|
|
2797
|
+
}
|
|
2798
|
+
dofAng = ang;
|
|
2799
|
+
// Re-express the lines against the new extent so nothing moves but the angle.
|
|
2800
|
+
const g2 = gizmoGeom();
|
|
2801
|
+
const f0 = (c[0]*g2.d[0] + c[1]*g2.d[1] - g2.lo) / (g2.hi - g2.lo);
|
|
2802
|
+
dofStart = Math.max(0, Math.min(0.95, f0));
|
|
2803
|
+
dofK = Math.max(0.05, Math.min(0.95, (c[0]*g2.u[0] + c[1]*g2.u[1] - g2.ulo) / (g2.uhi - g2.ulo)));
|
|
2804
|
+
dofEnd = Math.max(dofStart + 0.05, Math.min(1.5, dofStart + gzDrag.dFar / (g2.hi - g2.lo)));
|
|
2805
|
+
if (dofEnd2 !== null) dofEnd2 = Math.max(-0.5, Math.min(dofStart - 0.05, dofStart - gzDrag.dNear / (g2.hi - g2.lo)));
|
|
2760
2806
|
}
|
|
2761
2807
|
paintDof(); paintGizmo(); autoPreview();
|
|
2762
2808
|
});
|