screengraft 0.64.0 → 0.64.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/ui/index.html +22 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screengraft",
3
- "version": "0.64.0",
3
+ "version": "0.64.1",
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
@@ -1747,7 +1747,10 @@ const GUIDE_DASH = [6, 5];
1747
1747
  function cased(c, path, coreW, coreColor, dash){
1748
1748
  c.save();
1749
1749
  if (dash) c.setLineDash(dash);
1750
- c.lineCap = 'round'; c.lineJoin = 'round';
1750
+ // Butt caps on a dashed stroke: a round cap grows every dash by the line
1751
+ // width at both ends, so the 6/5 guide read as ~9/2 under its 2.9px casing
1752
+ // while the gizmo (SVG, butt caps) showed a true 6/5 (14 Sep 2026).
1753
+ c.lineCap = dash ? 'butt' : 'round'; c.lineJoin = 'round';
1751
1754
  c.strokeStyle = CASE_A; c.lineWidth = coreW + CASE_PAD; path(); c.stroke();
1752
1755
  c.strokeStyle = coreColor; c.lineWidth = coreW; path(); c.stroke();
1753
1756
  c.restore();
@@ -2148,6 +2151,7 @@ $('#figmaBtn').onclick = async () => {
2148
2151
  with the two UNCHANGED adjacent lines. Corner handles stay for edges that
2149
2152
  run off-frame or sit behind an occluder. */
2150
2153
  const cv = $('#c'), ctx = cv.getContext('2d');
2154
+ let cvW = 0, cvH = 0, cvDpr = 1; // the canvas's CSS size and the device-pixel factor of its store
2151
2155
  const strip = $('#strip'), sctx = strip.getContext('2d');
2152
2156
  const stripF = $('#stripF'), fctx = stripF.getContext('2d');
2153
2157
  const img = new Image();
@@ -2223,8 +2227,17 @@ function computeFit(){
2223
2227
  function setCanvasZoom(z, focus){
2224
2228
  const prev = scale;
2225
2229
  scale = Math.max(0.05, Math.min(4, z));
2226
- cv.width = Math.round(img.naturalWidth * scale);
2227
- cv.height = Math.round(img.naturalHeight * scale);
2230
+ // The backing store is sized in DEVICE pixels and the element in CSS pixels,
2231
+ // so on a Retina display the photo and the overlay are drawn at 2x and the
2232
+ // overlay's 1px lines are as sharp as the SVG gizmo's in the result pane.
2233
+ // Until 14 Sep 2026 the store was CSS-sized and the browser upsampled it --
2234
+ // every line in the fit pane was softer than the same line one pane over.
2235
+ // draw() undoes the factor with setTransform, so everything below still
2236
+ // thinks in CSS px: cvW/cvH are the CSS size, cv.width/height the store.
2237
+ cvW = Math.round(img.naturalWidth * scale); cvH = Math.round(img.naturalHeight * scale);
2238
+ cvDpr = Math.max(1, window.devicePixelRatio || 1);
2239
+ cv.width = Math.round(cvW * cvDpr); cv.height = Math.round(cvH * cvDpr);
2240
+ cv.style.width = cvW + 'px'; cv.style.height = cvH + 'px';
2228
2241
  $('#czSt').textContent = Math.round(scale*100) + '%';
2229
2242
  draw();
2230
2243
  const sc = $('#scroller');
@@ -2264,8 +2277,8 @@ function zoomAt(z, clientX, clientY){
2264
2277
  const before = pos({clientX, clientY});
2265
2278
  setCanvasZoom(z);
2266
2279
  const sc = $('#scroller'), r = cv.getBoundingClientRect();
2267
- sc.scrollLeft += r.left + before[0] * scale * (r.width / cv.width) - clientX;
2268
- sc.scrollTop += r.top + before[1] * scale * (r.height / cv.height) - clientY;
2280
+ sc.scrollLeft += r.left + before[0] * scale * (r.width / cvW) - clientX;
2281
+ sc.scrollTop += r.top + before[1] * scale * (r.height / cvH) - clientY;
2269
2282
  }
2270
2283
 
2271
2284
  /* A trackpad pinch arrives as a wheel event with ctrlKey synthesised true, and
@@ -2661,9 +2674,10 @@ function draw(){
2661
2674
  // fit, so leaving it on the old corners would make the two panes disagree
2662
2675
  // mid-drag — which is the one thing the side-by-side exists to prevent.
2663
2676
  if (!$('#liveWrap').hidden) placeLiveVideo();
2664
- ctx.clearRect(0,0,cv.width,cv.height);
2677
+ ctx.setTransform(cvDpr, 0, 0, cvDpr, 0, 0); // CSS px from here on; the store is cvDpr times larger
2678
+ ctx.clearRect(0,0,cvW,cvH);
2665
2679
  if (!img.naturalWidth) return;
2666
- ctx.drawImage(img, 0,0, cv.width, cv.height);
2680
+ ctx.drawImage(img, 0,0, cvW, cvH);
2667
2681
  if (!st.corners) return;
2668
2682
  const P = st.corners.map(([x,y]) => [x*scale, y*scale]);
2669
2683
  const active = drag || hover || pick;
@@ -2742,7 +2756,7 @@ function draw(){
2742
2756
 
2743
2757
  function pos(e){
2744
2758
  const r = cv.getBoundingClientRect();
2745
- return [(e.clientX-r.left)*cv.width/r.width/scale, (e.clientY-r.top)*cv.height/r.height/scale];
2759
+ return [(e.clientX-r.left)*cvW/r.width/scale, (e.clientY-r.top)*cvH/r.height/scale];
2746
2760
  }
2747
2761
  function hitTest(p){
2748
2762
  if (!st.corners) return null;