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 any 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 serverUrl: null,
383 forceClientSideInit: true,
384 fullAPI: true,
385 css: '../styles/stylesheet.css',
386 ui: 'beta',
387 licenseKey: licenseKey,
388 enableFilePicker: true,
389}, element).then((instance) => {
390 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
391 const cloudyTools = [
392 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
393 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
394 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
395 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
396 ];
397 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
398 instance.UI.disableTools(cloudyTools);
399
400 // Set default toolbar group to Annotate
401 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
402
403 // Set default tool on mobile devices to Pan.
404 // https://apryse.atlassian.net/browse/WVR-3134
405 if (isMobileDevice()) {
406 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
407 }
408
409 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
410 if (searchParams.has('file')) {
411 searchParams.delete('file');
412 history.replaceState(null, '', '?' + searchParams.toString());
413 }
414 });
415
416 instance.Core.annotationManager.enableAnnotationNumbering();
417
418 instance.UI.NotesPanel.enableAttachmentPreview();
419
420 // Add the demo-specific functionality
421 customizeUI(instance).then(() => {
422 // Create UI controls after demo is initialized
423 createUIControls(instance);
424 });
425});
426
427// Function to check if the user is on a mobile device
428const isMobileDevice = () => {
429 return (
430 /(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(
431 window.navigator.userAgent
432 ) ||
433 /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(
434 window.navigator.userAgent.substring(0, 4)
435 )
436 );
437}
438
439// Cleanup function for when the demo is closed or page is unloaded
440const cleanup = (instance) => {
441 const { documentViewer } = instance.Core;
442
443 if (typeof instance !== 'undefined' && instance.UI) {
444 instance.UI.unregisterTool(TRIANGLE_TOOL_NAME);
445 instance.UI.setToolMode('AnnotationEdit');
446 documentViewer.removeEventListener('toolModeUpdated', toolModeUpdated);
447 documentViewer.removeEventListener('toolUpdated', setUIState);
448 console.log('Cleaning up compare-files demo');
449 }
450};
451
452// Register cleanup for page unload
453window.addEventListener('beforeunload', () => cleanup(instance));
454window.addEventListener('unload', () => cleanup(instance));
455
456
457// UI section
458//
459// Helper code to add controls to the viewer holding the buttons
460// This code creates a container for the buttons, styles them, and adds them to the viewer
461//
462
463
464// Color Picker
465const colorPicker = (filled, instance) => {
466 const wrapper = document.createElement('div');
467
468 const title = document.createElement('h2');
469 title.textContent = filled ? 'Select custom annotation fill color' : 'Select custom annotation stroke color';
470 title.className = 'picker-title'; // <-- Use the CSS class here
471
472 wrapper.appendChild(title);
473
474 const colorDisplay = document.createElement('div');
475 colorDisplay.className = 'color-display';
476
477 const colorDisplayLabel = document.createElement('p');
478 colorDisplayLabel.className = 'color-display-label';
479 colorDisplayLabel.textContent = 'Color:';
480
481 const colorDisplayInput = document.createElement('input');
482 colorDisplayInput.className = 'color-display-input';
483 colorDisplayInput.type = 'text';
484 colorDisplayInput.readOnly = true;
485
486 colorDisplayInput.onchange = (e) => {
487 if (filled) {
488 fillColor = e.target.value;
489 } else {
490 strokeColor = e.target.value;
491 }
492 };
493
494 colorDisplay.appendChild(colorDisplayLabel);
495 colorDisplay.appendChild(colorDisplayInput);
496
497 wrapper.appendChild(colorDisplay);
498
499 const checkboxContainer = document.createElement('div');
500 checkboxContainer.className = 'checkbox-container';
501
502 DEFAULT_COLORS.forEach((color) => {
503 // Actual radio/checkbox input
504 const checkbox = document.createElement('input');
505 checkbox.className = 'checkbox-input';
506 checkbox.name = filled ? 'fill-color' : 'stroke-color';
507 checkbox.id = `checkbox-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`;
508 checkbox.type = 'radio';
509 checkbox.value = color;
510
511 if (filled) { // default fill color
512 if (color === fillColor) {
513 checkbox.checked = true;
514 colorDisplayInput.value = `${color}`;
515 }
516 } else { // default stroke color
517 if (color === strokeColor) {
518 checkbox.checked = true;
519 colorDisplayInput.value = `${color}`;
520 }
521 }
522
523 checkbox.onchange = (e) => {
524 if (e.target.checked) {
525 colorDisplayInput.value = `${e.target.value}`;
526 if (filled) {
527 fillColor = e.target.value;
528 } else {
529 strokeColor = e.target.value;
530 }
531 setToolInstanceStyle(instance);
532 }
533 };
534
535 checkbox.style.display = 'none'; // Hide to use custom checkbox instead
536
537 checkboxContainer.appendChild(checkbox);
538
539 // Custom checkbox
540 const span = document.createElement('span');
541 span.className = 'checkbox-checkmark';
542 span.id = `checkmark-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`;
543
544 if (checkbox.checked) {
545 span.textContent = '✓'; // U+2713
546 }
547
548 span.onclick = () => {
549 checkbox.checked = true;
550 checkbox.onchange({ target: checkbox });
551 // Update all checkmarks of the same group
552 document.querySelectorAll(`input[name=${filled ? 'fill-color' : 'stroke-color'}]`).forEach((cb) => {
553 const checkmark = document.getElementById(`checkmark-${filled ? 'fill' : 'stroke'}-${cb.value.replace('#', '')}`);
554 if (checkmark) checkmark.textContent = '';
555 });
556 document.getElementById(`checkmark-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`).textContent = '✓'; // U+2713
557 }
558
559 span.style.backgroundColor = (filled ? color : 'transparent');
560 span.style.color = (filled ? '#FFFFFF' : color);
561 span.style.border = `3px solid ${color}`;
562
563 checkboxContainer.appendChild(span);
564 });
565
566 wrapper.appendChild(checkboxContainer);
567
568 // Opacity/thickness Slider
569 const sliderWrapper = document.createElement('div');
570
571 const sliderLabel = document.createElement('label');
572 sliderLabel.className = 'slider-label';
573 sliderLabel.textContent = filled ? 'Select custom annotation opacity' : 'Select custom annotation stroke thickness';
574 sliderWrapper.appendChild(sliderLabel);
575
576 const sliderInput = document.createElement('input');
577 sliderInput.className = 'slider-input';
578 sliderInput.type = 'range';
579 sliderInput.min = filled ? '0' : '0';
580 sliderInput.max = filled ? '1' : '20';
581 sliderInput.step = filled ? '0.01' : '1';
582 sliderInput.value = filled ? opacity : width;
583
584 sliderInput.onchange = (e) => {
585 if (filled) {
586 opacity = parseFloat(e.target.value);
587 } else {
588 width = parseInt(e.target.value, 10);
589 }
590 setToolInstanceStyle(instance);
591 };
592
593 sliderWrapper.appendChild(sliderInput);
594
595 wrapper.appendChild(sliderWrapper);
596
597 return wrapper;
598};
599
600const createUIControls = (instance) => {
601 const controlsContainer = document.createElement('div');
602 controlsContainer.className = 'controls-container';
603
604 // Stroke Color Picker
605 controlsContainer.appendChild(colorPicker(false, instance));
606
607 // Fill Color Picker
608 controlsContainer.appendChild(colorPicker(true, instance));
609
610 element.insertBefore(controlsContainer, element.firstChild);
611};
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