Easily draw custom annotations in a PDF document. Set your annotation customization from multiple options in an interactive fashion that set the color, opacity, and stroke thickness.
This demo lets you:
To add custom annotations capability to a PDF with WebViewer:
Step 1: Choose your get started with your preferred web stack for WebViewer
Step 2: Add the ES6 JavaScript sample code provided in this guide
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, GPT-4.1, September 11, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// Custom Annotation Demo
8//
9// This code demonstrates how to draw custom annotations inside a document
10// and customize their appearance, including color, opacity and
11// stroke thickness.
12//
13
14// Custom Triangle Annotation Tool name
15const TRIANGLE_TOOL_NAME = 'AnnotationCreateTriangle';
16
17// Default Document
18const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/comp_sci_cheatsheet.pdf';
19
20// Default custom annotation properties
21let fillColor = '#F7D038';
22let strokeColor = '#EB7532';
23let width = 5;
24let isCustomToolActive = true;
25let opacity = 1;
26
27// Tool instance
28let toolInstance = null;
29
30// Default colors for color picker
31const DEFAULT_COLORS = [
32 '#EB7532', // Orange
33 '#F7D038', // Yellow
34 '#A3E048', // Green
35 '#519A64', // Dark Green
36 '#33BBE6', // Blue
37 '#4355DB', // Dark Blue
38 '#D23BE7', // Purple
39 '#E6261F', // Red
40]
41
42const customizeUI = async (instance) => {
43 // Load default document
44 await instance.Core.documentViewer.loadDocument(defaultDoc, {
45 extension: 'pdf',
46 });
47
48 // Initialize custom annotation tool
49 await initCustomAnnotTool(instance);
50 setToolInstanceStyle(instance);
51
52 // Add event listeners
53 const { documentViewer } = instance.Core;
54 documentViewer.addEventListener('toolModeUpdated', toolModeUpdated);
55 documentViewer.addEventListener('toolUpdated', setUIState);
56 documentViewer.addEventListener('documentLoaded', setTriangleTool);
57};
58
59const setTriangleTool = (instance) => {
60 try {
61 instance.UI.setToolMode(TRIANGLE_TOOL_NAME);
62 } catch (error) {
63 console.error('Calling setToolMode on instance error:', error);
64 }
65};
66
67const setUIState = (updatedTool) => {
68 if (isCustomToolActive) {
69 const { defaults } = updatedTool;
70 const { StrokeColor, FillColor, StrokeThickness, Opacity } = defaults;
71
72 const fill = !FillColor.toHexString() ? '#000000' : FillColor.toHexString();
73 const stroke = !StrokeColor.toHexString() ? '#000000' : StrokeColor.toHexString();
74 fillColor = fill;
75 strokeColor = stroke;
76 width = Math.floor(StrokeThickness);
77 opacity = Opacity;
78 }
79};
80
81const toolModeUpdated = (type) => {
82 const { name } = type;
83 isCustomToolActive = (name === TRIANGLE_TOOL_NAME);
84};
85
86const setToolInstanceStyle = (instance) => {
87 const { Annotations } = instance.Core;
88 const style = toolInstance.defaults;
89 toolInstance.setStyles({
90 ...style,
91 FillColor: new Annotations.Color(fillColor),
92 StrokeColor: new Annotations.Color(strokeColor),
93 StrokeThickness: width,
94 Opacity: opacity,
95 });
96};
97
98const initCustomAnnotTool = async (instance) => {
99 const { Annotations, documentViewer, Tools } = instance.Core;
100 Annotations.SelectionAlgorithm.canvasVisibilityPadding = 50;
101
102 const TriangleAnnotation = TriangleAnnotationFactory(Annotations, documentViewer);
103 const TriangleCreateTool = TriangleCreateToolFactory(Annotations, Tools, TriangleAnnotation);
104
105 // Register the annotation type so that it can be saved to XFDF
106 documentViewer
107 .getAnnotationManager()
108 .registerAnnotationType(TriangleAnnotation.prototype.elementName, TriangleAnnotation);
109 toolInstance = new TriangleCreateTool(documentViewer);
110 toolInstance.defaults.FillColor = new Annotations.Color(fillColor);
111 toolInstance.defaults.StrokeColor = new Annotations.Color(strokeColor);
112 toolInstance.defaults.StrokeThickness = width;
113 toolInstance.defaults.Opacity = opacity;
114 instance.UI.registerTool(
115 {
116 toolName: TRIANGLE_TOOL_NAME,
117 toolObject: toolInstance,
118 buttonImage:
119 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
120 '<path d="M12 7.77L18.39 18H5.61L12 7.77M12 4L2 20h20L12 4z"/>' +
121 '<path fill="none" d="M0 0h24v24H0V0z"/>' +
122 '</svg>',
123 buttonName: 'triangleToolButton',
124 tooltip: 'Triangle',
125 },
126 TriangleAnnotation
127 );
128
129 // documentLoaded has already happened by the time this file is loaded, so use a different event
130 await documentViewer.getAnnotationsLoadedPromise();
131
132 const topHeader = instance.UI.getModularHeader('tools-header');
133 // This is the ribbonGroup already
134 const shapesGroup = topHeader.getItems()[1];
135 const shapesGroupedItems = shapesGroup.items[0];
136
137 const customTriangleButton = new instance.UI.Components.ToolButton({
138 dataElement: 'custom-triangle-button',
139 toolName: TRIANGLE_TOOL_NAME,
140 });
141
142 shapesGroupedItems.setItems([...shapesGroupedItems.items, customTriangleButton]);
143
144 setTriangleTool(instance);
145};
146
147// Custom Triangle Annotation section
148//
149// This code defines a custom triangle annotation
150//
151
152// Control Handler
153const TriangleControlHandleFactory = (Annotations) => {
154 const TriangleControlHandle = function (annotation, index) {
155 this.annotation = annotation;
156 // set the index of this control handle so that we know which vertex it corresponds to
157 this.index = index;
158 };
159
160 TriangleControlHandle.prototype = new Annotations.ControlHandle();
161
162 // returns a rect that should represent the control handle's position and size
163 TriangleControlHandle.prototype.getDimensions = function (annotation, selectionBox, zoom) {
164 let x = annotation.vertices[this.index].x;
165 let y = annotation.vertices[this.index].y;
166 const width = Annotations.ControlHandle.handleWidth / zoom;
167 const height = Annotations.ControlHandle.handleHeight / zoom;
168
169 // adjust for the control handle's own width and height
170 x -= width * 0.5;
171 y -= height * 0.5;
172 return new Annotations.Rect(x, y, x + width, y + height);
173 };
174
175 TriangleControlHandle.prototype.draw = function (ctx, annotation, selectionBox, zoom) {
176 const dim = this.getDimensions(annotation, selectionBox, zoom);
177 ctx.fillStyle = '#FFFFFF';
178 ctx.beginPath();
179 ctx.moveTo(dim.x1 + dim.getWidth() / 2, dim.y1);
180 ctx.lineTo(dim.x1 + dim.getWidth(), dim.y1 + dim.getHeight());
181 ctx.lineTo(dim.x1, dim.y1 + dim.getHeight());
182 ctx.closePath();
183 ctx.stroke();
184 ctx.fill();
185 };
186
187 // this function is called when the control handle is dragged
188 TriangleControlHandle.prototype.move = function (annotation, deltaX, deltaY) {
189 annotation.vertices[this.index].x += deltaX;
190 annotation.vertices[this.index].y += deltaY;
191
192 // recalculate the X, Y, width and height of the annotation
193 let minX = Number.MAX_VALUE;
194 let maxX = -Number.MAX_VALUE;
195 let minY = Number.MAX_VALUE;
196 let maxY = -Number.MAX_VALUE;
197 for (let i = 0; i < annotation.vertices.length; ++i) {
198 const vertex = annotation.vertices[i];
199 minX = Math.min(minX, vertex.x);
200 maxX = Math.max(maxX, vertex.x);
201 minY = Math.min(minY, vertex.y);
202 maxY = Math.max(maxY, vertex.y);
203 }
204
205 const rect = new Annotations.Rect(minX, minY, maxX, maxY);
206 annotation.setRect(rect);
207 // return true if redraw is needed
208 return true;
209 };
210 return TriangleControlHandle;
211};
212
213// Selection Model
214const TriangleSelectionModelFactory = (Annotations, documentViewer) => {
215 const TriangleSelectionModel = function (annotation, canModify) {
216 Annotations.SelectionModel.call(this, annotation, canModify, false, documentViewer);
217 if (canModify) {
218 const controlHandles = this.getControlHandles();
219 const TriangleControlHandle = TriangleControlHandleFactory(Annotations);
220 controlHandles.push(new TriangleControlHandle(annotation, 0));
221 controlHandles.push(new TriangleControlHandle(annotation, 1));
222 controlHandles.push(new TriangleControlHandle(annotation, 2));
223 }
224 };
225
226 TriangleSelectionModel.prototype = new Annotations.SelectionModel();
227
228 TriangleSelectionModel.prototype.drawSelectionOutline = function (ctx, annotation, zoom) {
229 if (typeof zoom !== 'undefined') {
230 ctx.lineWidth = Annotations.SelectionModel.selectionOutlineThickness / zoom;
231 } else {
232 ctx.lineWidth = Annotations.SelectionModel.selectionOutlineThickness;
233 }
234
235 // changes the selection outline color if the user doesn't have permission to modify this annotation
236 if (this.canModify()) {
237 ctx.strokeStyle = Annotations.SelectionModel.defaultSelectionOutlineColor.toString();
238 } else {
239 ctx.strokeStyle =
240 Annotations.SelectionModel.defaultNoPermissionSelectionOutlineColor.toString();
241 }
242
243 ctx.beginPath();
244 ctx.moveTo(annotation.vertices[0].x, annotation.vertices[0].y);
245 ctx.lineTo(annotation.vertices[1].x, annotation.vertices[1].y);
246 ctx.lineTo(annotation.vertices[2].x, annotation.vertices[2].y);
247 ctx.closePath();
248 ctx.stroke();
249
250 const dashUnit = Annotations.SelectionModel.selectionOutlineDashSize / zoom;
251 const sequence = [dashUnit, dashUnit];
252 ctx.setLineDash(sequence);
253 ctx.strokeStyle = 'rgb(255, 255, 255)';
254 ctx.stroke();
255 };
256
257 TriangleSelectionModel.prototype.testSelection = function (annotation, x, y, pageMatrix) {
258 // the canvas visibility test will only select the annotation
259 // if a user clicks exactly on it as opposed to the rectangular bounding box
260 return Annotations.SelectionAlgorithm.canvasVisibilityTest(annotation, x, y, pageMatrix);
261 };
262
263 return TriangleSelectionModel;
264};
265
266// Annotation Factory
267const TriangleAnnotationFactory = (Annotations, documentViewer) => {
268 const TriangleAnnotation = function () {
269 Annotations.MarkupAnnotation.call(this);
270 this.Subject = 'Triangle';
271 this.vertices = [];
272 const numVertices = 3;
273 for (let i = 0; i < numVertices; ++i) {
274 this.vertices.push({
275 x: 0,
276 y: 0,
277 });
278 }
279 };
280
281 TriangleAnnotation.prototype = new Annotations.MarkupAnnotation();
282
283 TriangleAnnotation.prototype.elementName = 'triangle';
284
285 const triangleSelectionModel = TriangleSelectionModelFactory(Annotations, documentViewer);
286 TriangleAnnotation.prototype.selectionModel = triangleSelectionModel;
287
288 TriangleAnnotation.prototype.draw = function (ctx, pageMatrix) {
289 // the setStyles function is a function on markup annotations that sets up
290 // certain properties for us on the canvas for the annotation's stroke thickness.
291 this.setStyles(ctx, pageMatrix);
292
293 ctx.beginPath();
294 ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
295 ctx.lineTo(this.vertices[1].x, this.vertices[1].y);
296 ctx.lineTo(this.vertices[2].x, this.vertices[2].y);
297 ctx.closePath();
298 ctx.fill();
299 ctx.stroke();
300 };
301
302 TriangleAnnotation.prototype.resize = function (rect) {
303 // this function is only called when the annotation is dragged
304 // since we handle the case where the control handles move
305 const annotRect = this.getRect();
306 const deltaX = rect.x1 - annotRect.x1;
307 const deltaY = rect.y1 - annotRect.y1;
308
309 // shift the vertices by the amount the rect has shifted
310 this.vertices = this.vertices.map(function (vertex) {
311 vertex.x += deltaX;
312 vertex.y += deltaY;
313 return vertex;
314 });
315 this.setRect(rect);
316 };
317
318 TriangleAnnotation.prototype.serialize = function (element, pageMatrix) {
319 const el = Annotations.MarkupAnnotation.prototype.serialize.call(this, element, pageMatrix);
320 el.setAttribute(
321 'vertices',
322 Annotations.XFDFUtils.serializePointArray(this.vertices, pageMatrix)
323 );
324 return el;
325 };
326
327 TriangleAnnotation.prototype.deserialize = function (element, pageMatrix) {
328 Annotations.MarkupAnnotation.prototype.deserialize.call(this, element, pageMatrix);
329 this.vertices = Annotations.XFDFUtils.deserializePointArray(
330 element.getAttribute('vertices'),
331 pageMatrix
332 );
333 };
334
335 return TriangleAnnotation;
336};
337
338// Create Tool
339const TriangleCreateToolFactory = (Annotations, Tools, TriangleAnnotation) => {
340 const TriangleCreateTool = function (documentViewer) {
341 // TriangleAnnotation is the constructor function for our annotation we defined previously
342 Tools.GenericAnnotationCreateTool.call(this, documentViewer, TriangleAnnotation);
343 };
344
345 TriangleCreateTool.prototype = new Tools.GenericAnnotationCreateTool();
346
347 TriangleCreateTool.prototype.mouseMove = function (e) {
348 // call the parent mouseMove first
349 Tools.GenericAnnotationCreateTool.prototype.mouseMove.call(this, e);
350 if (this.annotation) {
351 this.annotation.vertices[0].x = this.annotation.X + this.annotation.Width / 2;
352 this.annotation.vertices[0].y = this.annotation.Y;
353 this.annotation.vertices[1].x = this.annotation.X + this.annotation.Width;
354 this.annotation.vertices[1].y = this.annotation.Y + this.annotation.Height;
355 this.annotation.vertices[2].x = this.annotation.X;
356 this.annotation.vertices[2].y = this.annotation.Y + this.annotation.Height;
357
358 // update the annotation appearance
359 this.documentViewer.getAnnotationManager().redrawAnnotation(this.annotation);
360 }
361 };
362
363 return TriangleCreateTool;
364};
365
366
367
368// WebViewer section
369//
370// This code initializes the WebViewer with the basic settings
371// that are found in the default showcase WebViewer
372//
373
374const searchParams = new URLSearchParams(window.location.search);
375const history = window.history || window.parent.history || window.top.history;
376const licenseKey = 'YOUR_LICENSE_KEY';
377const element = document.getElementById('viewer');
378
379// Initialize WebViewer with the specified settings
380WebViewer({
381 path: '/lib',
382 licenseKey: licenseKey,
383 enableFilePicker: true,
384}, element).then((instance) => {
385 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
386 const cloudyTools = [
387 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
388 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
389 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
390 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
391 ];
392 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
393 instance.UI.disableTools(cloudyTools);
394
395 // Set default toolbar group to Annotate
396 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
397
398 // Set default tool on mobile devices to Pan.
399 // https://apryse.atlassian.net/browse/WVR-3134
400 if (isMobileDevice()) {
401 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
402 }
403
404 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
405 if (searchParams.has('file')) {
406 searchParams.delete('file');
407 history.replaceState(null, '', '?' + searchParams.toString());
408 }
409 });
410
411 instance.Core.annotationManager.enableAnnotationNumbering();
412
413 instance.UI.NotesPanel.enableAttachmentPreview();
414
415 // Add the demo-specific functionality
416 customizeUI(instance).then(() => {
417 // Create UI controls after demo is initialized
418 createUIControls(instance);
419 });
420});
421
422// Function to check if the user is on a mobile device
423const isMobileDevice = () => {
424 return (
425 /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
426 window.navigator.userAgent
427 ) ||
428 /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
429 window.navigator.userAgent.substring(0, 4)
430 )
431 );
432}
433
434// Cleanup function for when the demo is closed or page is unloaded
435const cleanup = (instance) => {
436 const { documentViewer } = instance.Core;
437
438 if (typeof instance !== 'undefined' && instance.UI) {
439 instance.UI.unregisterTool(TRIANGLE_TOOL_NAME);
440 instance.UI.setToolMode('AnnotationEdit');
441 documentViewer.removeEventListener('toolModeUpdated', toolModeUpdated);
442 documentViewer.removeEventListener('toolUpdated', setUIState);
443 console.log('Cleaning up compare-files demo');
444 }
445};
446
447// Register cleanup for page unload
448window.addEventListener('beforeunload', () => cleanup(instance));
449window.addEventListener('unload', () => cleanup(instance));
450
451
452// UI section
453//
454// Helper code to add controls to the viewer holding the buttons
455// This code creates a container for the buttons, styles them, and adds them to the viewer
456//
457
458
459// Color Picker
460const colorPicker = (filled, instance) => {
461 const wrapper = document.createElement('div');
462
463 const title = document.createElement('h2');
464 title.textContent = filled ? 'Select custom annotation fill color' : 'Select custom annotation stroke color';
465 title.className = 'picker-title'; // <-- Use the CSS class here
466
467 wrapper.appendChild(title);
468
469 const colorDisplay = document.createElement('div');
470 colorDisplay.className = 'color-display';
471
472 const colorDisplayLabel = document.createElement('p');
473 colorDisplayLabel.className = 'color-display-label';
474 colorDisplayLabel.textContent = 'Color:';
475
476 const colorDisplayInput = document.createElement('input');
477 colorDisplayInput.className = 'color-display-input';
478 colorDisplayInput.type = 'text';
479 colorDisplayInput.readOnly = true;
480
481 colorDisplayInput.onchange = (e) => {
482 if (filled) {
483 fillColor = e.target.value;
484 } else {
485 strokeColor = e.target.value;
486 }
487 };
488
489 colorDisplay.appendChild(colorDisplayLabel);
490 colorDisplay.appendChild(colorDisplayInput);
491
492 wrapper.appendChild(colorDisplay);
493
494 const checkboxContainer = document.createElement('div');
495 checkboxContainer.className = 'checkbox-container';
496
497 DEFAULT_COLORS.forEach((color) => {
498 // Actual radio/checkbox input
499 const checkbox = document.createElement('input');
500 checkbox.className = 'checkbox-input';
501 checkbox.name = filled ? 'fill-color' : 'stroke-color';
502 checkbox.id = `checkbox-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`;
503 checkbox.type = 'radio';
504 checkbox.value = color;
505
506 if (filled) { // default fill color
507 if (color === fillColor) {
508 checkbox.checked = true;
509 colorDisplayInput.value = `${color}`;
510 }
511 } else { // default stroke color
512 if (color === strokeColor) {
513 checkbox.checked = true;
514 colorDisplayInput.value = `${color}`;
515 }
516 }
517
518 checkbox.onchange = (e) => {
519 if (e.target.checked) {
520 colorDisplayInput.value = `${e.target.value}`;
521 if (filled) {
522 fillColor = e.target.value;
523 } else {
524 strokeColor = e.target.value;
525 }
526 setToolInstanceStyle(instance);
527 }
528 };
529
530 checkbox.style.display = 'none'; // Hide to use custom checkbox instead
531
532 checkboxContainer.appendChild(checkbox);
533
534 // Custom checkbox
535 const span = document.createElement('span');
536 span.className = 'checkbox-checkmark';
537 span.id = `checkmark-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`;
538
539 if (checkbox.checked) {
540 span.textContent = '✓'; // U+2713
541 }
542
543 span.onclick = () => {
544 checkbox.checked = true;
545 checkbox.onchange({ target: checkbox });
546 // Update all checkmarks of the same group
547 document.querySelectorAll(`input[name=${filled ? 'fill-color' : 'stroke-color'}]`).forEach((cb) => {
548 const checkmark = document.getElementById(`checkmark-${filled ? 'fill' : 'stroke'}-${cb.value.replace('#', '')}`);
549 if (checkmark) checkmark.textContent = '';
550 });
551 document.getElementById(`checkmark-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`).textContent = '✓'; // U+2713
552 }
553
554 span.style.backgroundColor = (filled ? color : 'transparent');
555 span.style.color = (filled ? '#FFFFFF' : color);
556 span.style.border = `3px solid ${color}`;
557
558 checkboxContainer.appendChild(span);
559 });
560
561 wrapper.appendChild(checkboxContainer);
562
563 // Opacity/thickness Slider
564 const sliderWrapper = document.createElement('div');
565
566 const sliderLabel = document.createElement('label');
567 sliderLabel.className = 'slider-label';
568 sliderLabel.textContent = filled ? 'Select custom annotation opacity' : 'Select custom annotation stroke thickness';
569 sliderWrapper.appendChild(sliderLabel);
570
571 const sliderInput = document.createElement('input');
572 sliderInput.className = 'slider-input';
573 sliderInput.type = 'range';
574 sliderInput.min = filled ? '0' : '0';
575 sliderInput.max = filled ? '1' : '20';
576 sliderInput.step = filled ? '0.01' : '1';
577 sliderInput.value = filled ? opacity : width;
578
579 sliderInput.onchange = (e) => {
580 if (filled) {
581 opacity = parseFloat(e.target.value);
582 } else {
583 width = parseInt(e.target.value, 10);
584 }
585 setToolInstanceStyle(instance);
586 };
587
588 sliderWrapper.appendChild(sliderInput);
589
590 wrapper.appendChild(sliderWrapper);
591
592 return wrapper;
593};
594
595const createUIControls = (instance) => {
596 const controlsContainer = document.createElement('div');
597 controlsContainer.className = 'controls-container';
598
599 // Stroke Color Picker
600 controlsContainer.appendChild(colorPicker(false, instance));
601
602 // Fill Color Picker
603 controlsContainer.appendChild(colorPicker(true, instance));
604
605 element.insertBefore(controlsContainer, element.firstChild);
606};
1/* CSS Standard Compliant Syntax */
2/* GitHub Copilot v1.0, GPT-4.1, September 11, 2025 */
3/* File: index.css */
4
5.picker-title {
6 font-weight: 900;
7 font-size: 14px;
8 line-height: 125%;
9 color: #334250;
10 letter-spacing: 0;
11 padding-bottom: 10px;
12 display: flex;
13 justify-content: space-between;
14}
15
16.color-display {
17 display: flex;
18 flex-direction: row;
19 align-items: center;
20}
21
22.color-display-label {
23 font-size: 14px;
24 line-height: 20px;
25 color: #485056;
26 letter-spacing: -0.3px;
27 margin-right: 5px;
28 font-weight: 700;
29}
30
31.color-display-input {
32 width: 100%;
33 min-width: 0px;
34 outline: 2px solid transparent;
35 outline-offset: 2px;
36 transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
37 transition-duration: 200ms;
38 border-radius: 6px;
39 background-color: #FFFFFF;
40 color: #485056;
41 border: 1px solid;
42 box-sizing: border-box;
43 font-size: 14px;
44 padding-inline-start: 16px;
45 padding-inline-end: 16px;
46 height: 40px;
47}
48
49.checkbox-checkmark {
50 border-radius: 6px;
51 width: 24px;
52 height: 24px;
53 margin: 3px;
54 cursor: pointer;
55 text-align: center;
56 line-height: 18px;
57 display: inline-block;
58 /* The following will be set dynamically in JS:
59 background-color, color, border */
60}
61
62.slider-label {
63 font-weight: 900;
64 font-size: 14px;
65 line-height: 125%;
66 color: #334250;
67 letter-spacing: 0;
68 padding: 10px 0 10px 0;
69 display: flex;
70 justify-content: space-between;
71}
72
73.slider-input {
74 width: 100%;
75 margin: 0;
76}
77
78.controls-container {
79 display: flex;
80 flex-direction: row;
81 gap: 20px;
82}
83
84.checkbox-container {
85 display: flex;
86 flex-wrap: wrap;
87}
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales