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:
Implementation steps
To add custom annotations capability to a PDF with WebViewer:
Step 1: Choose your preferred web stack
Step 2: Download required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided in this guide
Demo Dependencies
This sample uses the following:
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