What I learned building Crop Wizard
Hey, do you crop and resize images on your desktop or mobile device?
Maybe it's me, but I feel like cropping and resizing is not as easy as it should be. You go to upload a profile photo or banner image, and it doesn't size correctly. Or maybe you're adding an image to your website and it needs to be an exact size. It's such a pain doing it in an image editor.
That's why I created Crop Wizard! Crop Wizard can crop and resize web images like JPG, PNG, and webp right in your browser. Simply open your image in Crop Wizard, resize the crop area, and save! No 3rd-party servers or internet required. No need to switch between apps.
And it also lets you crop to specific aspect ratios or dimensions and works on both desktop and mobile devices.
Now on to what I learned!
Web Components
I've used web components before, but never using the vanilla web APIs. Implementing a web component from scratch was pretty easy with customElements. Web Components were a good way to encapsulate all the functionality for the Crop Wizard image editor. For Crop Wizard, there is a single entrypoint for the creation of the component (the connectedCallback
function). If I was going to use web components in other apps, I'd need to think about the callbacks for "destroy" and "move" events and possibly others. But for this project I didn't have to think about that.
Overall, I'm pretty excited for how well it turned out and I plan on building other web components that can be used in vanilla HTML or dropped into any of the other web frameworks Vue/React/Svelte/Astro, etc.
Shadow Dom
When I'm working with vanilla JavaScript/HTML I like to create an index.html file and just start building and styling. I did this to build the image editor. Once that was done, I was able to move all of the JavaScript, HTML, and CSS into the web component. Thanks to attachShadow and ShadowRoot I didn't have to change my css styles or worry about conflicting css names. All the CSS is isolated in the shadow dom.
Touch Events
This was probably the learning highlight of the project. I've build mouse handlers for a long time, but working with touch events is slightly different. Adding mobile support to the application was an excellent opportunity to see how touch events work, and also how you need to add extra invisible padding to areas you want the user to touch.
CSS touch-action:none was important to use to prevent scrolling when the user touched and moved the viewport.
touchstart, touchmove, and touchend are important. Getting the event target element with a touch event requires using elementFromPoint with the clientX and clientY properties of the TouchEvent.
Custom Events
Inside the Web Component class, I used CustomEvent to fire/handle events like when an image was "uploaded" (a custom "update-image" event) or when the viewport was changed.
Movement Handlers
I setup my event handlers to register the starting position of the click/touch of the element the user wanted to interact with. Then the move handlers were registered on the top level element (not the element being interacted with). The start position was compared with the current position to get the delta. And because the move handler wasn't added to the "interaction element" I didn't have to worry about the user moving too quickly or resizing the elements and losing focus.
Aspect Ratios, Algebra, and Resolving Size Discrepancies
One of the interesting problems I solved was converted the coordinates of the UI viewport into the coordinates of the natural image. Since the UI displays a scaled image we have to resolve the problem of fractional pixels - basically, when we scale the image, we loose information about where exactly the edges of the viewport are. Thankfully, this is a situation where "close enough" is OK and you can multiplying the viewport coordinates by the vertical and horizontal scale and then round up or down. However, if you use this method when there is a viewport constraint like 1:1 or 9:16, you're going to end up with a width and height that doesn't exactly match the desired aspect ratio.
To solve this, I used a brute force approach to find the image size that was the exact aspect ratio but also close to the original. I looped over a set of width modifications, added the modification to the desired width, and then checked that the width and height were both whole integers.
Other Random Things
I've used JavaScript private elements before and they're pretty nice. Basically, it lets you add private variables to a class (like the WebComponent) by prefixing a pound sign #. It feels odd at first using the pound sign in your variable name, but you get used to it.
Firefox doesn't yet support attributeStyleMap and computedStyleMap, so I had to revert to use ing element.style.setProperty and window.getComputedStyle.
Testing with Playwright
I did some manual testing in different browsers and devices, but I also setup some automated tests in Playwright. That made it easy to test a bunch of browsers when I had to migrate away from attributeStyleMap and computedStyleMap.
Logo Design
It was fun sketching the Crop Wizard logo on pen and paper and exploring some possible compositions. I used one sketch for the larger logo and another for the favicon and other bookmark icons. It was a nice change of pace and fun to refresh my Inkscape and SVG skills.