The new watermark features are now live, along with bug fixes.
The watermark editor implemented by Fabric has been online for a while (see “Implement real-time watermark preview using Fabric.js”). The previous version of the code and research work were all completed by Benny. As a front-end developer for WebP Cloud Services, I’m only now starting to learn Fabric, which is quite embarrassing 🤪.
During this period, I also discovered some bugs and had some new ideas. Therefore, we have made some improvements to the watermark functionality to make it look more elegant and enhance its usability.
New Features
This update includes three new features:
- Added editable text and width adjustment functionality
- Preset watermark positions
- Watermark preview for images of different sizes
The page effect is roughly as shown in the picture below. Doesn’t it look almost the same? But it’s not! (Just kidding 😂)
Added Editable Text and Width Adjustment Functionality on Canvas
In the previous version, the text content could only be set through the config on the left side of the canvas, and it could not be edited directly on the canvas. This change adds the editable functionality, allowing you to double-click the text on the canvas to edit it directly. It’s important to note that since Fabric’s TextBox supports line breaks, after editing the text, you need to click on the blank area of the canvas again to save the edited content.
After adding the editable functionality, we discovered the first pitfall of Fabric: the width of the text box can become longer but not shorter! Moreover, setting the width attribute does not work. So I asked ChatGPT:
Alright, the width of the text should also support customization! Therefore, we added two control points on the left and right, allowing users to adjust the width themselves.
On the code level, we also made some refactoring and optimizations:
Changed the canvas instance bound events from
object:scaling
andobject:moving
tomouse:up
to reduce the frequency of event triggers. The event will be triggered after the user operation is completed (mouse released). If there are changes in width, height, or offset, further steps will be taken.this.canvas.on("mouse:up", this.handleObject.bind(this));
Changed the canvas instance from
Fabric.Text
toFabric.Textbox
to support text wrapping, and seteditable: true
to allow text editing on the canvas.const textObject = new Fabric.Textbox(text, { editable: true, // Initialize text box }); textObject.setControlsVisibility({ // Hide control points });
Bound the
editing:exited
event to the textbox instance, which is triggered after the content editing is completed.textObject.on("editing:exited", this.afterEditText.bind(this)); this.canvas.add(textObject);
Reduced the number of times the textbox instance is assigned values by merging multiple attribute assignments into one.
const updateProperties: Record<string, string | number> = {}; keys.forEach((key) => { updateProperties[key] = value; }); textObject.set(updateProperties); this.canvas.renderAll();
Added Preset Watermark Positions
Based on the habits of most people using watermarks (including ourselves), we added some preset watermark positions for users to quickly choose from. These preset positions include top left, top right, bottom left, bottom right, and center. Users can select according to their needs. Clicking the button will directly move the watermark to the corresponding position and request a preview image for easy viewing of the effect.
Added Watermark Preview for Images of Different Sizes
The aspect ratio of the watermark canvas is 1:1, but in actual use, there may be images of different sizes. Therefore, we added a watermark preview for images of different sizes. In the preview image, you can see the watermark effect on images of different sizes.
Bug Fixes
Watermark Position Exceeding Canvas Boundaries Not Restored to Canvas
Currently, two scenarios are considered for the watermark exceeding the boundary:
Dragging the text box outside the boundary: In this scenario, the text box needs to be restored to the canvas. Based on the actual dragging position and angle, adjust the text box to the position where the offset is 0 at the boundary.
const boundingRect = textObject.getBoundingRect(); const canvasWidth = this.canvas.width; const canvasHeight = this.canvas.height; const { left = 0, top = 0, width = 0, height = 0 } = boundingRect; // Check and adjust the left boundary if (left < 0) { textObject.left = 0; } // Check and adjust the top boundary if (top < 0) { textObject.top = 0; } // Check and adjust the right boundary if (left + width > canvasWidth) { textObject.left = canvasWidth - width; } // Check and adjust the bottom boundary if (top + height > canvasHeight) { textObject.top = canvasHeight - height; }
Scaling the text box width/height beyond the canvas: In this scenario, the text box’s width and height need to be adjusted based on the canvas’s width and height to ensure the maximum width and height are within the canvas.
Just when everything seemed to be going smoothly, we discovered the second pitfall of Fabric: Fabric’s Textbox has a characteristic where after scaling the text box, the width and height of the text box instance (i.e., textObject in the code above) do not update, only the scaleX/scaleY values change.
So how do we get the current width and height information? Fortunately, Fabric provides getBoundingRect for us to use. This method returns the current width, height, and offset information of the text box.
Therefore, when controlling the width and height range of the text box, we cannot simply set the width/height values but need to change scaleX/scaleY (and these two values must be the same, otherwise the font will become thinner/fatter). We also need to control the left/top values to prevent the text box from drifting out of the canvas.
Otherwise, a strange phenomenon will occur: after scaling beyond the canvas boundary, the width of the text box will be even larger than when the scaling ended! The reason is naturally that its width calculation is width * scaleX, not the width itself.
const maxWidth = this.canvas.width, maxHeight = this.canvas.height; const { width, height } = boundingRect; // Check and set the maximum width if (width >= maxWidth) { if (!textObject.width) return; const max_scale = maxWidth / textObject.width; textObject.scaleX = max_scale; textObject.scaleY = max_scale; textObject.left = 0; } // Check and set the maximum height if (height >= maxHeight) { if (!textObject.height) return; const max_scale = maxHeight / textObject.height; textObject.scaleX = max_scale; textObject.scaleY = max_scale; textObject.top = 0; }
Optimization
Added Debounce to Input Box to Reduce Request Frequency
The previous online version did not handle debounce in the input box, causing a new preview image request for each input, leading to excessive requests and affecting performance. Therefore, we added debounce processing in the input box (now a request is sent every 500ms) to reduce the number of requests.
import { Subject, debounceTime } from 'rxjs';
private searchSubject: Subject<string> = new Subject();
ngOnInit(): void {
this.searchSubject.pipe(debounceTime(500)).subscribe(() => {
// Request preview image
});
}
requestPreview() {
this.searchSubject.next();
}
Modified Background Image of Canvas and Preview Image
The previous solid color images had colors, which might blend with the text color, making the watermark effect less obvious. So we changed the background image to a common transparent background material.
Optimized Layout of List Page
The existing watermark list page shows all relevant content, making the display look cluttered. So we redesigned and rearranged the list page to make it clearer. Since the list page will display the preview image of each watermark, we chose to enlarge the images for a clear view of the different watermark effects. At the same time, to better display the effect, we hid the detailed information, which will be shown when the mouse hovers over it.
Optimized Watermark Font Display
In the previous version, we did not choose to download all font files in advance. Instead, we downloaded the corresponding font file each time a different font was selected. This was to reduce bandwidth consumption, as the total size of all font files is quite large. However, this approach had the problem that each time a different font was selected for the first time, there would be a waiting period (because there was no cache, and we set the maximum font loading time to 10 seconds). Besides the poor experience, it could also time out, resulting in the font not being updated. Therefore, we chose not to display the font on the canvas but to display it in the preview image generated by the server.
The WebP Cloud Services team is a small team of three individuals from Shanghai and Helsingborg. Since we are not funded and have no profit pressure, we remain committed to doing what we believe is right. We strive to do our best within the scope of our resources and capabilities. We also engage in various activities without affecting the services we provide to the public, and we continuously explore novel ideas in our products.
If you find this service interesting, feel free to log in to the WebP Cloud Dashboard to experience it. If you’re curious about other magical features it offers, take a look at our WebP Cloud Services Docs. We hope everyone enjoys using it!
Discuss on Hacker News