惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
GbyAI
GbyAI
P
Proofpoint News Feed
B
Blog
MyScale Blog
MyScale Blog
V
V2EX
B
Blog RSS Feed
Microsoft Security Blog
Microsoft Security Blog
量子位
Jina AI
Jina AI
博客园 - 叶小钗
Recent Announcements
Recent Announcements
有赞技术团队
有赞技术团队
罗磊的独立博客
L
LangChain Blog
I
InfoQ
云风的 BLOG
云风的 BLOG
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
人人都是产品经理
人人都是产品经理
小众软件
小众软件
V
Visual Studio Blog
月光博客
月光博客
The Cloudflare Blog
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Stop Exporting The Viewport: How Zoomed Image Editors Map...
byeval · 2026-04-24 · via DEV Community

One of the easiest ways to break an image editor is to confuse the viewport with the image.

The screen needs zooming, centering, and a comfortable interaction scale. The exported file needs exact source-pixel geometry. If those two layers get mixed together, the UI might look fine while the saved result is wrong.

That boundary shows up in tools such as crop editors, screenshot redactors, and browser-side markup tools.

The full companion guide is here:

https://happyimg.com/guides/how-zoomed-image-editors-map-back-to-original-pixels

The viewport is for comfort, not truth

In the editor code, the Fabric canvas is sized to the visible container, then the viewport is transformed to fit the uploaded image:

const scale = Math.min(clientWidth / width, clientHeight / height) * 0.5;
const offsetX = (clientWidth - width * scale) / 2;
const offsetY = (clientHeight - height * scale) / 2;

this.canvas.setZoom(scale);
this.canvas.setViewportTransform([scale, 0, 0, scale, offsetX, offsetY]);

Enter fullscreen mode Exit fullscreen mode

That is exactly what the UI should do. Users should not be forced to edit an 1800-pixel image at a 1:1 screen scale.

The important part is what this transform does not mean. It does not mean the viewport has become the source of truth for export.

Keep objects in image space

The source image and editor objects are still inserted using the original image dimensions:

this.image = new FabricImage(image, {
  left: image.width / 2,
  top: image.height / 2,
  selectable: false,
  evented: false,
});

Enter fullscreen mode Exit fullscreen mode

That means a crop box or redaction region is defined in source-image coordinates even while the visible canvas is zoomed and centered for editing.

Once that boundary is in place, zooming becomes a viewing concern instead of a geometry problem.

Resizing is where geometry bugs usually appear

The subtle failure mode shows up during scaling handles.

If you keep committing transformed width and height on every live scaling event, the numbers start compounding and the box drifts away from the correct geometry.

The safer approach is to read the current temporary size from the base dimensions plus scale:

const width = (this.moveableRect.width ?? 0) * (this.moveableRect.scaleX ?? 1);
const height = (this.moveableRect.height ?? 0) * (this.moveableRect.scaleY ?? 1);

Enter fullscreen mode Exit fullscreen mode

Then normalize the box back into real width, height, left, and top values once the interaction should be committed.

That sounds small, but it is the difference between a stable editor and a resize tool that gets weirder every time the user drags a corner.

Export should rebuild from source pixels

The other common mistake is exporting the visible viewport as if it were the artifact.

For crop export, the editor creates a fresh canvas sized to the normalized crop region and draws directly from the original image:

context.drawImage(
  this.sourceImage,
  Math.round(nextRect.left),
  Math.round(nextRect.top),
  width,
  height,
  0,
  0,
  width,
  height
);

Enter fullscreen mode Exit fullscreen mode

For full markup export, the same idea appears as a clean StaticCanvas at the original image dimensions, with the base image re-added and the current overlay objects replayed onto it.

That is what makes the saved result accurate even if the editor viewport was zoomed out, zoomed in, or centered differently when the user clicked export.

The useful mental model

I think the cleanest rule is:

  • viewport transform is for viewing
  • object geometry is for editing
  • export canvas is for truth

If those three concerns stay separate, the math stays understandable and the output stays aligned.

If they get mixed together, you usually end up debugging "why is the export offset?" bugs that are really just architecture bugs.

More implementation details:

https://happyimg.com/guides/how-zoomed-image-editors-map-back-to-original-pixels