From 36f8c99dd31a39c6f9513e5179a28a1bf8490e91 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 15 Nov 2024 00:15:58 -0500 Subject: [PATCH] basic drawing of closed shapes --- src/assets/draw.svg | 142 +++++ src/assets/select.svg | 139 +++++ src/assets/transform.svg | 200 ++++++ src/coloris.css | 577 +++++++++++++++++ src/coloris.js | 1263 ++++++++++++++++++++++++++++++++++++++ src/index.html | 2 + src/main.js | 154 ++++- src/styles.css | 7 + 8 files changed, 2470 insertions(+), 14 deletions(-) create mode 100644 src/assets/draw.svg create mode 100644 src/assets/select.svg create mode 100644 src/assets/transform.svg create mode 100644 src/coloris.css create mode 100644 src/coloris.js diff --git a/src/assets/draw.svg b/src/assets/draw.svg new file mode 100644 index 0000000..e1a096a --- /dev/null +++ b/src/assets/draw.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + + diff --git a/src/assets/select.svg b/src/assets/select.svg new file mode 100644 index 0000000..de109e2 --- /dev/null +++ b/src/assets/select.svg @@ -0,0 +1,139 @@ + + + + + + + + + image/svg+xml + + + + + Barbara Muraus, Jakub Steiner, Klaus Staedtler + + + Images originally created as the "Art Libre" icon set. Extended and adopted for GIMP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/transform.svg b/src/assets/transform.svg new file mode 100644 index 0000000..afc157b --- /dev/null +++ b/src/assets/transform.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + + + + + diff --git a/src/coloris.css b/src/coloris.css new file mode 100644 index 0000000..41ddad2 --- /dev/null +++ b/src/coloris.css @@ -0,0 +1,577 @@ +.clr-picker { + display: none; + flex-wrap: wrap; + position: absolute; + width: 200px; + z-index: 1000; + border-radius: 10px; + background-color: #fff; + justify-content: flex-end; + direction: ltr; + box-shadow: 0 0 5px rgba(0,0,0,.05), 0 5px 20px rgba(0,0,0,.1); + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.clr-picker.clr-open, +.clr-picker[data-inline="true"] { + display: flex; +} + +.clr-picker[data-inline="true"] { + position: relative; +} + +.clr-gradient { + position: relative; + width: 100%; + height: 100px; + margin-bottom: 15px; + border-radius: 3px 3px 0 0; + background-image: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentColor); + cursor: pointer; +} + +.clr-marker { + position: absolute; + width: 12px; + height: 12px; + margin: -6px 0 0 -6px; + border: 1px solid #fff; + border-radius: 50%; + background-color: currentColor; + cursor: pointer; +} + +.clr-picker input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 16px; +} + +.clr-picker input[type="range"]::-webkit-slider-thumb { + width: 16px; + height: 16px; + -webkit-appearance: none; +} + +.clr-picker input[type="range"]::-moz-range-track { + width: 100%; + height: 16px; + border: 0; +} + +.clr-picker input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + border: 0; +} + +.clr-hue { + background-image: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); +} + +.clr-hue, +.clr-alpha { + position: relative; + width: calc(100% - 40px); + height: 8px; + margin: 5px 20px; + border-radius: 4px; +} + +.clr-alpha span { + display: block; + height: 100%; + width: 100%; + border-radius: inherit; + background-image: linear-gradient(90deg, rgba(0,0,0,0), currentColor); +} + +.clr-hue input[type="range"], +.clr-alpha input[type="range"] { + position: absolute; + width: calc(100% + 32px); + height: 16px; + left: -16px; + top: -4px; + margin: 0; + background-color: transparent; + opacity: 0; + cursor: pointer; + appearance: none; + -webkit-appearance: none; +} + +.clr-hue div, +.clr-alpha div { + position: absolute; + width: 16px; + height: 16px; + left: 0; + top: 50%; + margin-left: -8px; + transform: translateY(-50%); + border: 2px solid #fff; + border-radius: 50%; + background-color: currentColor; + box-shadow: 0 0 1px #888; + pointer-events: none; +} + +.clr-alpha div:before { + content: ''; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + border-radius: 50%; + background-color: currentColor; +} + +.clr-format { + display: none; + order: 1; + width: calc(100% - 40px); + margin: 0 20px 20px; +} + +.clr-segmented { + display: flex; + position: relative; + width: 100%; + margin: 0; + padding: 0; + border: 1px solid #ddd; + border-radius: 15px; + box-sizing: border-box; + color: #999; + font-size: 12px; +} + +.clr-segmented input, +.clr-segmented legend { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border: 0; + left: 0; + top: 0; + opacity: 0; + pointer-events: none; +} + +.clr-segmented label { + flex-grow: 1; + margin: 0; + padding: 4px 0; + font-size: inherit; + font-weight: normal; + line-height: initial; + text-align: center; + cursor: pointer; +} + +.clr-segmented label:first-of-type { + border-radius: 10px 0 0 10px; +} + +.clr-segmented label:last-of-type { + border-radius: 0 10px 10px 0; +} + +.clr-segmented input:checked + label { + color: #fff; + background-color: #666; +} + +.clr-swatches { + order: 2; + width: calc(100% - 32px); + margin: 0 16px; +} + +.clr-swatches div { + display: flex; + flex-wrap: wrap; + padding-bottom: 12px; + justify-content: center; +} + +.clr-swatches button { + position: relative; + width: 20px; + height: 20px; + margin: 0 4px 6px 4px; + padding: 0; + border: 0; + border-radius: 50%; + color: inherit; + text-indent: -1000px; + white-space: nowrap; + overflow: hidden; + cursor: pointer; +} + +.clr-swatches button:after { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border-radius: inherit; + background-color: currentColor; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); +} + +input.clr-color { + order: 1; + width: calc(100% - 80px); + height: 32px; + margin: 15px 20px 20px auto; + padding: 0 10px; + border: 1px solid #ddd; + border-radius: 16px; + color: #444; + background-color: #fff; + font-family: sans-serif; + font-size: 14px; + text-align: center; + box-shadow: none; +} + +input.clr-color:focus { + outline: none; + border: 1px solid #1e90ff; +} + +.clr-close, +.clr-clear { + display: none; + order: 2; + height: 24px; + margin: 0 20px 20px; + padding: 0 20px; + border: 0; + border-radius: 12px; + color: #fff; + background-color: #666; + font-family: inherit; + font-size: 12px; + font-weight: 400; + cursor: pointer; +} + +.clr-close { + display: block; + margin: 0 20px 20px auto; +} + +.clr-preview { + position: relative; + width: 32px; + height: 32px; + margin: 15px 0 20px 20px; + border-radius: 50%; + overflow: hidden; +} + +.clr-preview:before, +.clr-preview:after { + content: ''; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + border: 1px solid #fff; + border-radius: 50%; +} + +.clr-preview:after { + border: 0; + background-color: currentColor; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); +} + +.clr-preview button { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + margin: 0; + padding: 0; + border: 0; + border-radius: 50%; + outline-offset: -2px; + background-color: transparent; + text-indent: -9999px; + cursor: pointer; + overflow: hidden; +} + +.clr-marker, +.clr-hue div, +.clr-alpha div, +.clr-color { + box-sizing: border-box; +} + +.clr-field { + display: inline-block; + position: relative; + color: transparent; +} + +.clr-field input { + margin: 0; + direction: ltr; +} + +.clr-field.clr-rtl input { + text-align: right; +} + +.clr-field button { + position: absolute; + width: 30px; + height: 100%; + right: 0; + top: 50%; + transform: translateY(-50%); + margin: 0; + padding: 0; + border: 0; + color: inherit; + text-indent: -1000px; + white-space: nowrap; + overflow: hidden; + pointer-events: none; +} + +.clr-field.clr-rtl button { + right: auto; + left: 0; +} + +.clr-field button:after { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border-radius: inherit; + background-color: currentColor; + box-shadow: inset 0 0 1px rgba(0,0,0,.5); +} + +.clr-alpha, +.clr-alpha div, +.clr-swatches button, +.clr-preview:before, +.clr-field button { + background-image: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); + background-position: 0 0, 4px 4px; + background-size: 8px 8px; +} + +.clr-marker:focus { + outline: none; +} + +.clr-keyboard-nav .clr-marker:focus, +.clr-keyboard-nav .clr-hue input:focus + div, +.clr-keyboard-nav .clr-alpha input:focus + div, +.clr-keyboard-nav .clr-segmented input:focus + label { + outline: none; + box-shadow: 0 0 0 2px #1e90ff, 0 0 2px 2px #fff; +} + +.clr-picker[data-alpha="false"] .clr-alpha { + display: none; +} + +.clr-picker[data-minimal="true"] { + padding-top: 16px; +} + +.clr-picker[data-minimal="true"] .clr-gradient, +.clr-picker[data-minimal="true"] .clr-hue, +.clr-picker[data-minimal="true"] .clr-alpha, +.clr-picker[data-minimal="true"] .clr-color, +.clr-picker[data-minimal="true"] .clr-preview { + display: none; +} + +/** Dark theme **/ + +.clr-dark { + background-color: #444; +} + +.clr-dark .clr-segmented { + border-color: #777; +} + +.clr-dark .clr-swatches button:after { + box-shadow: inset 0 0 0 1px rgba(255,255,255,.3); +} + +.clr-dark input.clr-color { + color: #fff; + border-color: #777; + background-color: #555; +} + +.clr-dark input.clr-color:focus { + border-color: #1e90ff; +} + +.clr-dark .clr-preview:after { + box-shadow: inset 0 0 0 1px rgba(255,255,255,.5); +} + +.clr-dark .clr-alpha, +.clr-dark .clr-alpha div, +.clr-dark .clr-swatches button, +.clr-dark .clr-preview:before { + background-image: repeating-linear-gradient(45deg, #666 25%, transparent 25%, transparent 75%, #888 75%, #888), repeating-linear-gradient(45deg, #888 25%, #444 25%, #444 75%, #888 75%, #888); +} + +/** Polaroid theme **/ + +.clr-picker.clr-polaroid { + border-radius: 6px; + box-shadow: 0 0 5px rgba(0,0,0,.1), 0 5px 30px rgba(0,0,0,.2); +} + +.clr-picker.clr-polaroid:before { + content: ''; + display: block; + position: absolute; + width: 16px; + height: 10px; + left: 20px; + top: -10px; + border: solid transparent; + border-width: 0 8px 10px 8px; + border-bottom-color: currentColor; + box-sizing: border-box; + color: #fff; + filter: drop-shadow(0 -4px 3px rgba(0,0,0,.1)); + pointer-events: none; +} + +.clr-picker.clr-polaroid.clr-dark:before { + color: #444; +} + +.clr-picker.clr-polaroid.clr-left:before { + left: auto; + right: 20px; +} + +.clr-picker.clr-polaroid.clr-top:before { + top: auto; + bottom: -10px; + transform: rotateZ(180deg); +} + +.clr-polaroid .clr-gradient { + width: calc(100% - 20px); + height: 120px; + margin: 10px; + border-radius: 3px; +} + +.clr-polaroid .clr-hue, +.clr-polaroid .clr-alpha { + width: calc(100% - 30px); + height: 10px; + margin: 6px 15px; + border-radius: 5px; +} + +.clr-polaroid .clr-hue div, +.clr-polaroid .clr-alpha div { + box-shadow: 0 0 5px rgba(0,0,0,.2); +} + +.clr-polaroid .clr-format { + width: calc(100% - 20px); + margin: 0 10px 15px; +} + +.clr-polaroid .clr-swatches { + width: calc(100% - 12px); + margin: 0 6px; +} +.clr-polaroid .clr-swatches div { + padding-bottom: 10px; +} + +.clr-polaroid .clr-swatches button { + width: 22px; + height: 22px; +} + +.clr-polaroid input.clr-color { + width: calc(100% - 60px); + margin: 10px 10px 15px auto; +} + +.clr-polaroid .clr-clear { + margin: 0 10px 15px 10px; +} + +.clr-polaroid .clr-close { + margin: 0 10px 15px auto; +} + +.clr-polaroid .clr-preview { + margin: 10px 0 15px 10px; +} + +/** Large theme **/ + +.clr-picker.clr-large { + width: 275px; +} + +.clr-large .clr-gradient { + height: 150px; +} + +.clr-large .clr-swatches button { + width: 22px; + height: 22px; +} + +/** Pill (horizontal) theme **/ + +.clr-picker.clr-pill { + width: 380px; + padding-left: 180px; + box-sizing: border-box; +} + +.clr-pill .clr-gradient { + position: absolute; + width: 180px; + height: 100%; + left: 0; + top: 0; + margin-bottom: 0; + border-radius: 3px 0 0 3px; +} + +.clr-pill .clr-hue { + margin-top: 20px; +} \ No newline at end of file diff --git a/src/coloris.js b/src/coloris.js new file mode 100644 index 0000000..1aa88b0 --- /dev/null +++ b/src/coloris.js @@ -0,0 +1,1263 @@ + /*! + * Copyright (c) 2021 Momo Bassit. + * Licensed under the MIT License (MIT) + * https://github.com/mdbassit/Coloris + */ + +(function (window, document, Math, undefined) { + var ctx = document.createElement('canvas').getContext('2d'); + var currentColor = { r: 0, g: 0, b: 0, h: 0, s: 0, v: 0, a: 1 }; + var container,picker,colorArea,colorMarker,colorPreview,colorValue,clearButton,closeButton, + hueSlider,hueMarker,alphaSlider,alphaMarker,currentEl,currentFormat,oldColor,keyboardNav, + colorAreaDims = {}; + + // Default settings + var settings = { + el: '[data-coloris]', + parent: 'body', + theme: 'default', + themeMode: 'light', + rtl: false, + wrap: true, + margin: 2, + format: 'hex', + formatToggle: false, + swatches: [], + swatchesOnly: false, + alpha: true, + forceAlpha: false, + focusInput: true, + selectInput: false, + inline: false, + defaultColor: '#000000', + clearButton: false, + clearLabel: 'Clear', + closeButton: false, + closeLabel: 'Close', + onChange: function onChange() {return undefined;}, + a11y: { + open: 'Open color picker', + close: 'Close color picker', + clear: 'Clear the selected color', + marker: 'Saturation: {s}. Brightness: {v}.', + hueSlider: 'Hue slider', + alphaSlider: 'Opacity slider', + input: 'Color value field', + format: 'Color format', + swatch: 'Color swatch', + instruction: 'Saturation and brightness selector. Use up, down, left and right arrow keys to select.' } }; + + + + // Virtual instances cache + var instances = {}; + var currentInstanceId = ''; + var defaultInstance = {}; + var hasInstance = false; + + /** + * Configure the color picker. + * @param {object} options Configuration options. + */ + function configure(options) { + if (typeof options !== 'object') { + return; + } + + for (var key in options) { + switch (key) { + case 'el': + bindFields(options.el); + if (options.wrap !== false) { + wrapFields(options.el); + } + break; + case 'parent': + container = options.parent instanceof HTMLElement ? options.parent : document.querySelector(options.parent); + if (container) { + container.appendChild(picker); + settings.parent = options.parent; + + // document.body is special + if (container === document.body) { + container = undefined; + } + } + break; + case 'themeMode': + settings.themeMode = options.themeMode; + if (options.themeMode === 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + settings.themeMode = 'dark'; + } + // The lack of a break statement is intentional + case 'theme': + if (options.theme) { + settings.theme = options.theme; + } + + // Set the theme and color scheme + picker.className = "clr-picker clr-" + settings.theme + " clr-" + settings.themeMode; + + // Update the color picker's position if inline mode is in use + if (settings.inline) { + updatePickerPosition(); + } + break; + case 'rtl': + settings.rtl = !!options.rtl; + Array.from(document.getElementsByClassName('clr-field')).forEach(function (field) {return field.classList.toggle('clr-rtl', settings.rtl);}); + break; + case 'margin': + options.margin *= 1; + settings.margin = !isNaN(options.margin) ? options.margin : settings.margin; + break; + case 'wrap': + if (options.el && options.wrap) { + wrapFields(options.el); + } + break; + case 'formatToggle': + settings.formatToggle = !!options.formatToggle; + getEl('clr-format').style.display = settings.formatToggle ? 'block' : 'none'; + if (settings.formatToggle) { + settings.format = 'auto'; + } + break; + case 'swatches': + if (Array.isArray(options.swatches)) {(function () { + var swatchesContainer = getEl('clr-swatches'); + var swatches = document.createElement('div'); + + // Clear current swatches + swatchesContainer.textContent = ''; + + // Build new swatches + options.swatches.forEach(function (swatch, i) { + var button = document.createElement('button'); + + button.setAttribute('type', "button"); + button.setAttribute('id', "clr-swatch-" + i); + button.setAttribute('aria-labelledby', "clr-swatch-label clr-swatch-" + i); + button.style.color = swatch; + button.textContent = swatch; + + swatches.appendChild(button); + }); + + // Append new swatches if any + if (options.swatches.length) { + swatchesContainer.appendChild(swatches); + } + + settings.swatches = options.swatches.slice();})(); + } + break; + case 'swatchesOnly': + settings.swatchesOnly = !!options.swatchesOnly; + picker.setAttribute('data-minimal', settings.swatchesOnly); + break; + case 'alpha': + settings.alpha = !!options.alpha; + picker.setAttribute('data-alpha', settings.alpha); + break; + case 'inline': + settings.inline = !!options.inline; + picker.setAttribute('data-inline', settings.inline); + + if (settings.inline) { + var defaultColor = options.defaultColor || settings.defaultColor; + + currentFormat = getColorFormatFromStr(defaultColor); + updatePickerPosition(); + setColorFromStr(defaultColor); + } + break; + case 'clearButton': + // Backward compatibility + if (typeof options.clearButton === 'object') { + if (options.clearButton.label) { + settings.clearLabel = options.clearButton.label; + clearButton.innerHTML = settings.clearLabel; + } + + options.clearButton = options.clearButton.show; + } + + settings.clearButton = !!options.clearButton; + clearButton.style.display = settings.clearButton ? 'block' : 'none'; + break; + case 'clearLabel': + settings.clearLabel = options.clearLabel; + clearButton.innerHTML = settings.clearLabel; + break; + case 'closeButton': + settings.closeButton = !!options.closeButton; + + if (settings.closeButton) { + picker.insertBefore(closeButton, colorPreview); + } else { + colorPreview.appendChild(closeButton); + } + + break; + case 'closeLabel': + settings.closeLabel = options.closeLabel; + closeButton.innerHTML = settings.closeLabel; + break; + case 'a11y': + var labels = options.a11y; + var update = false; + + if (typeof labels === 'object') { + for (var label in labels) { + if (labels[label] && settings.a11y[label]) { + settings.a11y[label] = labels[label]; + update = true; + } + } + } + + if (update) { + var openLabel = getEl('clr-open-label'); + var swatchLabel = getEl('clr-swatch-label'); + + openLabel.innerHTML = settings.a11y.open; + swatchLabel.innerHTML = settings.a11y.swatch; + closeButton.setAttribute('aria-label', settings.a11y.close); + clearButton.setAttribute('aria-label', settings.a11y.clear); + hueSlider.setAttribute('aria-label', settings.a11y.hueSlider); + alphaSlider.setAttribute('aria-label', settings.a11y.alphaSlider); + colorValue.setAttribute('aria-label', settings.a11y.input); + colorArea.setAttribute('aria-label', settings.a11y.instruction); + } + break; + default: + settings[key] = options[key];} + + } + } + + /** + * Add or update a virtual instance. + * @param {String} selector The CSS selector of the elements to which the instance is attached. + * @param {Object} options Per-instance options to apply. + */ + function setVirtualInstance(selector, options) { + if (typeof selector === 'string' && typeof options === 'object') { + instances[selector] = options; + hasInstance = true; + } + } + + /** + * Remove a virtual instance. + * @param {String} selector The CSS selector of the elements to which the instance is attached. + */ + function removeVirtualInstance(selector) { + delete instances[selector]; + + if (Object.keys(instances).length === 0) { + hasInstance = false; + + if (selector === currentInstanceId) { + resetVirtualInstance(); + } + } + } + + /** + * Attach a virtual instance to an element if it matches a selector. + * @param {Object} element Target element that will receive a virtual instance if applicable. + */ + function attachVirtualInstance(element) { + if (hasInstance) { + // These options can only be set globally, not per instance + var unsupportedOptions = ['el', 'wrap', 'rtl', 'inline', 'defaultColor', 'a11y'];var _loop = function _loop( + + selector) { + var options = instances[selector]; + + // If the element matches an instance's CSS selector + if (element.matches(selector)) { + currentInstanceId = selector; + defaultInstance = {}; + + // Delete unsupported options + unsupportedOptions.forEach(function (option) {return delete options[option];}); + + // Back up the default options so we can restore them later + for (var option in options) { + defaultInstance[option] = Array.isArray(settings[option]) ? settings[option].slice() : settings[option]; + } + + // Set the instance's options + configure(options); + return "break"; + }};for (var selector in instances) {var _ret = _loop(selector);if (_ret === "break") break; + } + } + } + + /** + * Revert any per-instance options that were previously applied. + */ + function resetVirtualInstance() { + if (Object.keys(defaultInstance).length > 0) { + configure(defaultInstance); + currentInstanceId = ''; + defaultInstance = {}; + } + } + + /** + * Bind the color picker to input fields that match the selector. + * @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements. + */ + function bindFields(selector) { + if (selector instanceof HTMLElement) { + selector = [selector]; + } + + if (Array.isArray(selector)) { + selector.forEach(function (field) { + addListener(field, 'click', openPicker); + addListener(field, 'input', updateColorPreview); + }); + } else { + addListener(document, 'click', selector, openPicker); + addListener(document, 'input', selector, updateColorPreview); + } + } + + /** + * Open the color picker. + * @param {object} event The event that opens the color picker. + */ + function openPicker(event) { + // Skip if inline mode is in use + if (settings.inline) { + return; + } + + // Apply any per-instance options first + attachVirtualInstance(event.target); + + currentEl = event.target; + oldColor = currentEl.value; + currentFormat = getColorFormatFromStr(oldColor); + picker.classList.add('clr-open'); + + updatePickerPosition(); + setColorFromStr(oldColor); + + if (settings.focusInput || settings.selectInput) { + colorValue.focus({ preventScroll: true }); + colorValue.setSelectionRange(currentEl.selectionStart, currentEl.selectionEnd); + } + + if (settings.selectInput) { + colorValue.select(); + } + + // Always focus the first element when using keyboard navigation + if (keyboardNav || settings.swatchesOnly) { + getFocusableElements().shift().focus(); + } + + // Trigger an "open" event + currentEl.dispatchEvent(new Event('open', { bubbles: true })); + } + + /** + * Update the color picker's position and the color gradient's offset + */ + function updatePickerPosition() { + var parent = container; + var scrollY = window.scrollY; + var pickerWidth = picker.offsetWidth; + var pickerHeight = picker.offsetHeight; + var reposition = { left: false, top: false }; + var parentStyle, parentMarginTop, parentBorderTop; + var offset = { x: 0, y: 0 }; + + if (parent) { + parentStyle = window.getComputedStyle(parent); + parentMarginTop = parseFloat(parentStyle.marginTop); + parentBorderTop = parseFloat(parentStyle.borderTopWidth); + + offset = parent.getBoundingClientRect(); + offset.y += parentBorderTop + scrollY; + } + + if (!settings.inline) { + var coords = currentEl.getBoundingClientRect(); + var left = coords.x; + var top = scrollY + coords.y + coords.height + settings.margin; + + // If the color picker is inside a custom container + // set the position relative to it + if (parent) { + left -= offset.x; + top -= offset.y; + + if (left + pickerWidth > parent.clientWidth) { + left += coords.width - pickerWidth; + reposition.left = true; + } + + if (top + pickerHeight > parent.clientHeight - parentMarginTop) { + if (pickerHeight + settings.margin <= coords.top - (offset.y - scrollY)) { + top -= coords.height + pickerHeight + settings.margin * 2; + reposition.top = true; + } + } + + top += parent.scrollTop; + + // Otherwise set the position relative to the whole document + } else { + if (left + pickerWidth > document.documentElement.clientWidth) { + left += coords.width - pickerWidth; + reposition.left = true; + } + + if (top + pickerHeight - scrollY > document.documentElement.clientHeight) { + if (pickerHeight + settings.margin <= coords.top) { + top = scrollY + coords.y - pickerHeight - settings.margin; + reposition.top = true; + } + } + } + + picker.classList.toggle('clr-left', reposition.left); + picker.classList.toggle('clr-top', reposition.top); + picker.style.left = left + "px"; + picker.style.top = top + "px"; + offset.x += picker.offsetLeft; + offset.y += picker.offsetTop; + } + + colorAreaDims = { + width: colorArea.offsetWidth, + height: colorArea.offsetHeight, + x: colorArea.offsetLeft + offset.x, + y: colorArea.offsetTop + offset.y }; + + } + + /** + * Wrap the linked input fields in a div that adds a color preview. + * @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements. + */ + function wrapFields(selector) { + if (selector instanceof HTMLElement) { + wrapColorField(selector); + } else if (Array.isArray(selector)) { + selector.forEach(wrapColorField); + } else { + document.querySelectorAll(selector).forEach(wrapColorField); + } + } + + /** + * Wrap an input field in a div that adds a color preview. + * @param {object} field The input field. + */ + function wrapColorField(field) { + var parentNode = field.parentNode; + + if (!parentNode.classList.contains('clr-field')) { + var wrapper = document.createElement('div'); + var classes = 'clr-field'; + + if (settings.rtl || field.classList.contains('clr-rtl')) { + classes += ' clr-rtl'; + } + + wrapper.innerHTML = ''; + parentNode.insertBefore(wrapper, field); + wrapper.className = classes; + wrapper.style.color = field.value; + wrapper.appendChild(field); + } + } + + /** + * Update the color preview of an input field + * @param {object} event The "input" event that triggers the color change. + */ + function updateColorPreview(event) { + var parent = event.target.parentNode; + + // Only update the preview if the field has been previously wrapped + if (parent.classList.contains('clr-field')) { + parent.style.color = event.target.value; + } + } + + /** + * Close the color picker. + * @param {boolean} [revert] If true, revert the color to the original value. + */ + function closePicker(revert) { + if (currentEl && !settings.inline) { + var prevEl = currentEl; + + // Revert the color to the original value if needed + if (revert) { + // This will prevent the "change" event on the colorValue input to execute its handler + currentEl = undefined; + + if (oldColor !== prevEl.value) { + prevEl.value = oldColor; + + // Trigger an "input" event to force update the thumbnail next to the input field + prevEl.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + // Trigger a "change" event if needed + setTimeout(function () {// Add this to the end of the event loop + if (oldColor !== prevEl.value) { + prevEl.dispatchEvent(new Event('change', { bubbles: true })); + } + }); + + // Hide the picker dialog + picker.classList.remove('clr-open'); + + // Reset any previously set per-instance options + if (hasInstance) { + resetVirtualInstance(); + } + + // Trigger a "close" event + prevEl.dispatchEvent(new Event('close', { bubbles: true })); + + if (settings.focusInput) { + prevEl.focus({ preventScroll: true }); + } + + // This essentially marks the picker as closed + currentEl = undefined; + } + } + + /** + * Set the active color from a string. + * @param {string} str String representing a color. + */ + function setColorFromStr(str) { + var rgba = strToRGBA(str); + var hsva = RGBAtoHSVA(rgba); + + updateMarkerA11yLabel(hsva.s, hsva.v); + updateColor(rgba, hsva); + + // Update the UI + hueSlider.value = hsva.h; + picker.style.color = "hsl(" + hsva.h + ", 100%, 50%)"; + hueMarker.style.left = hsva.h / 360 * 100 + "%"; + + colorMarker.style.left = colorAreaDims.width * hsva.s / 100 + "px"; + colorMarker.style.top = colorAreaDims.height - colorAreaDims.height * hsva.v / 100 + "px"; + + alphaSlider.value = hsva.a * 100; + alphaMarker.style.left = hsva.a * 100 + "%"; + } + + /** + * Guess the color format from a string. + * @param {string} str String representing a color. + * @return {string} The color format. + */ + function getColorFormatFromStr(str) { + var format = str.substring(0, 3).toLowerCase(); + + if (format === 'rgb' || format === 'hsl') { + return format; + } + + return 'hex'; + } + + /** + * Copy the active color to the linked input field. + * @param {number} [color] Color value to override the active color. + */ + function pickColor(color) { + color = color !== undefined ? color : colorValue.value; + + if (currentEl) { + currentEl.value = color; + currentEl.dispatchEvent(new Event('input', { bubbles: true })); + } + + if (settings.onChange) { + settings.onChange.call(window, color, currentEl); + } + + document.dispatchEvent(new CustomEvent('coloris:pick', { detail: { color: color, currentEl: currentEl } })); + } + + /** + * Set the active color based on a specific point in the color gradient. + * @param {number} x Left position. + * @param {number} y Top position. + */ + function setColorAtPosition(x, y) { + var hsva = { + h: hueSlider.value * 1, + s: x / colorAreaDims.width * 100, + v: 100 - y / colorAreaDims.height * 100, + a: alphaSlider.value / 100 }; + + var rgba = HSVAtoRGBA(hsva); + + updateMarkerA11yLabel(hsva.s, hsva.v); + updateColor(rgba, hsva); + pickColor(); + } + + /** + * Update the color marker's accessibility label. + * @param {number} saturation + * @param {number} value + */ + function updateMarkerA11yLabel(saturation, value) { + var label = settings.a11y.marker; + + saturation = saturation.toFixed(1) * 1; + value = value.toFixed(1) * 1; + label = label.replace('{s}', saturation); + label = label.replace('{v}', value); + colorMarker.setAttribute('aria-label', label); + } + + // + /** + * Get the pageX and pageY positions of the pointer. + * @param {object} event The MouseEvent or TouchEvent object. + * @return {object} The pageX and pageY positions. + */ + function getPointerPosition(event) { + return { + pageX: event.changedTouches ? event.changedTouches[0].pageX : event.pageX, + pageY: event.changedTouches ? event.changedTouches[0].pageY : event.pageY }; + + } + + /** + * Move the color marker when dragged. + * @param {object} event The MouseEvent object. + */ + function moveMarker(event) { + var pointer = getPointerPosition(event); + var x = pointer.pageX - colorAreaDims.x; + var y = pointer.pageY - colorAreaDims.y; + + if (container) { + y += container.scrollTop; + } + + setMarkerPosition(x, y); + + // Prevent scrolling while dragging the marker + event.preventDefault(); + event.stopPropagation(); + } + + /** + * Move the color marker when the arrow keys are pressed. + * @param {number} offsetX The horizontal amount to move. + * @param {number} offsetY The vertical amount to move. + */ + function moveMarkerOnKeydown(offsetX, offsetY) { + var x = colorMarker.style.left.replace('px', '') * 1 + offsetX; + var y = colorMarker.style.top.replace('px', '') * 1 + offsetY; + + setMarkerPosition(x, y); + } + + /** + * Set the color marker's position. + * @param {number} x Left position. + * @param {number} y Top position. + */ + function setMarkerPosition(x, y) { + // Make sure the marker doesn't go out of bounds + x = x < 0 ? 0 : x > colorAreaDims.width ? colorAreaDims.width : x; + y = y < 0 ? 0 : y > colorAreaDims.height ? colorAreaDims.height : y; + + // Set the position + colorMarker.style.left = x + "px"; + colorMarker.style.top = y + "px"; + + // Update the color + setColorAtPosition(x, y); + + // Make sure the marker is focused + colorMarker.focus(); + } + + /** + * Update the color picker's input field and preview thumb. + * @param {Object} rgba Red, green, blue and alpha values. + * @param {Object} [hsva] Hue, saturation, value and alpha values. + */ + function updateColor(rgba, hsva) {if (rgba === void 0) {rgba = {};}if (hsva === void 0) {hsva = {};} + var format = settings.format; + + for (var key in rgba) { + currentColor[key] = rgba[key]; + } + + for (var _key in hsva) { + currentColor[_key] = hsva[_key]; + } + + var hex = RGBAToHex(currentColor); + var opaqueHex = hex.substring(0, 7); + + colorMarker.style.color = opaqueHex; + alphaMarker.parentNode.style.color = opaqueHex; + alphaMarker.style.color = hex; + colorPreview.style.color = hex; + + // Force repaint the color and alpha gradients as a workaround for a Google Chrome bug + colorArea.style.display = 'none'; + colorArea.offsetHeight; + colorArea.style.display = ''; + alphaMarker.nextElementSibling.style.display = 'none'; + alphaMarker.nextElementSibling.offsetHeight; + alphaMarker.nextElementSibling.style.display = ''; + + if (format === 'mixed') { + format = currentColor.a === 1 ? 'hex' : 'rgb'; + } else if (format === 'auto') { + format = currentFormat; + } + + switch (format) { + case 'hex': + colorValue.value = hex; + break; + case 'rgb': + colorValue.value = RGBAToStr(currentColor); + break; + case 'hsl': + colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor)); + break;} + + + // Select the current format in the format switcher + document.querySelector(".clr-format [value=\"" + format + "\"]").checked = true; + } + + /** + * Set the hue when its slider is moved. + */ + function setHue() { + var hue = hueSlider.value * 1; + var x = colorMarker.style.left.replace('px', '') * 1; + var y = colorMarker.style.top.replace('px', '') * 1; + + picker.style.color = "hsl(" + hue + ", 100%, 50%)"; + hueMarker.style.left = hue / 360 * 100 + "%"; + + setColorAtPosition(x, y); + } + + /** + * Set the alpha when its slider is moved. + */ + function setAlpha() { + var alpha = alphaSlider.value / 100; + + alphaMarker.style.left = alpha * 100 + "%"; + updateColor({ a: alpha }); + pickColor(); + } + + /** + * Convert HSVA to RGBA. + * @param {object} hsva Hue, saturation, value and alpha values. + * @return {object} Red, green, blue and alpha values. + */ + function HSVAtoRGBA(hsva) { + var saturation = hsva.s / 100; + var value = hsva.v / 100; + var chroma = saturation * value; + var hueBy60 = hsva.h / 60; + var x = chroma * (1 - Math.abs(hueBy60 % 2 - 1)); + var m = value - chroma; + + chroma = chroma + m; + x = x + m; + + var index = Math.floor(hueBy60) % 6; + var red = [chroma, x, m, m, x, chroma][index]; + var green = [x, chroma, chroma, x, m, m][index]; + var blue = [m, m, x, chroma, chroma, x][index]; + + return { + r: Math.round(red * 255), + g: Math.round(green * 255), + b: Math.round(blue * 255), + a: hsva.a }; + + } + + /** + * Convert HSVA to HSLA. + * @param {object} hsva Hue, saturation, value and alpha values. + * @return {object} Hue, saturation, lightness and alpha values. + */ + function HSVAtoHSLA(hsva) { + var value = hsva.v / 100; + var lightness = value * (1 - hsva.s / 100 / 2); + var saturation; + + if (lightness > 0 && lightness < 1) { + saturation = Math.round((value - lightness) / Math.min(lightness, 1 - lightness) * 100); + } + + return { + h: hsva.h, + s: saturation || 0, + l: Math.round(lightness * 100), + a: hsva.a }; + + } + + /** + * Convert RGBA to HSVA. + * @param {object} rgba Red, green, blue and alpha values. + * @return {object} Hue, saturation, value and alpha values. + */ + function RGBAtoHSVA(rgba) { + var red = rgba.r / 255; + var green = rgba.g / 255; + var blue = rgba.b / 255; + var xmax = Math.max(red, green, blue); + var xmin = Math.min(red, green, blue); + var chroma = xmax - xmin; + var value = xmax; + var hue = 0; + var saturation = 0; + + if (chroma) { + if (xmax === red) {hue = (green - blue) / chroma;} + if (xmax === green) {hue = 2 + (blue - red) / chroma;} + if (xmax === blue) {hue = 4 + (red - green) / chroma;} + if (xmax) {saturation = chroma / xmax;} + } + + hue = Math.floor(hue * 60); + + return { + h: hue < 0 ? hue + 360 : hue, + s: Math.round(saturation * 100), + v: Math.round(value * 100), + a: rgba.a }; + + } + + /** + * Parse a string to RGBA. + * @param {string} str String representing a color. + * @return {object} Red, green, blue and alpha values. + */ + function strToRGBA(str) { + var regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i; + var match, rgba; + + // Default to black for invalid color strings + ctx.fillStyle = '#000'; + + // Use canvas to convert the string to a valid color string + ctx.fillStyle = str; + match = regex.exec(ctx.fillStyle); + + if (match) { + rgba = { + r: match[3] * 1, + g: match[4] * 1, + b: match[5] * 1, + a: match[6] * 1 }; + + + } else { + match = ctx.fillStyle.replace('#', '').match(/.{2}/g).map(function (h) {return parseInt(h, 16);}); + rgba = { + r: match[0], + g: match[1], + b: match[2], + a: 1 }; + + } + + return rgba; + } + + /** + * Convert RGBA to Hex. + * @param {object} rgba Red, green, blue and alpha values. + * @return {string} Hex color string. + */ + function RGBAToHex(rgba) { + var R = rgba.r.toString(16); + var G = rgba.g.toString(16); + var B = rgba.b.toString(16); + var A = ''; + + if (rgba.r < 16) { + R = '0' + R; + } + + if (rgba.g < 16) { + G = '0' + G; + } + + if (rgba.b < 16) { + B = '0' + B; + } + + if (settings.alpha && (rgba.a < 1 || settings.forceAlpha)) { + var alpha = rgba.a * 255 | 0; + A = alpha.toString(16); + + if (alpha < 16) { + A = '0' + A; + } + } + + return '#' + R + G + B + A; + } + + /** + * Convert RGBA values to a CSS rgb/rgba string. + * @param {object} rgba Red, green, blue and alpha values. + * @return {string} CSS color string. + */ + function RGBAToStr(rgba) { + if (!settings.alpha || rgba.a === 1 && !settings.forceAlpha) { + return "rgb(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ")"; + } else { + return "rgba(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ", " + rgba.a + ")"; + } + } + + /** + * Convert HSLA values to a CSS hsl/hsla string. + * @param {object} hsla Hue, saturation, lightness and alpha values. + * @return {string} CSS color string. + */ + function HSLAToStr(hsla) { + if (!settings.alpha || hsla.a === 1 && !settings.forceAlpha) { + return "hsl(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%)"; + } else { + return "hsla(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%, " + hsla.a + ")"; + } + } + + /** + * Init the color picker. + */ + function init() { + // Render the UI + container = undefined; + picker = document.createElement('div'); + picker.setAttribute('id', 'clr-picker'); + picker.className = 'clr-picker'; + picker.innerHTML = + "" + ("
") + + '
' + + '
' + + '
' + ("") + + '
' + + '
' + + '
' + ("") + + '
' + + '' + + '
' + + '
' + + '
' + ("" + + settings.a11y.format + "") + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + ("") + + '
' + ("") + + '
' + ("") + (""); + + // Append the color picker to the DOM + document.body.appendChild(picker); + + // Reference the UI elements + colorArea = getEl('clr-color-area'); + colorMarker = getEl('clr-color-marker'); + clearButton = getEl('clr-clear'); + closeButton = getEl('clr-close'); + colorPreview = getEl('clr-color-preview'); + colorValue = getEl('clr-color-value'); + hueSlider = getEl('clr-hue-slider'); + hueMarker = getEl('clr-hue-marker'); + alphaSlider = getEl('clr-alpha-slider'); + alphaMarker = getEl('clr-alpha-marker'); + + // Bind the picker to the default selector + bindFields(settings.el); + wrapFields(settings.el); + + addListener(picker, 'mousedown', function (event) { + picker.classList.remove('clr-keyboard-nav'); + event.stopPropagation(); + }); + + addListener(colorArea, 'mousedown', function (event) { + addListener(document, 'mousemove', moveMarker); + }); + + addListener(colorArea, 'contextmenu', function (event) { + event.preventDefault(); + }); + + addListener(colorArea, 'touchstart', function (event) { + document.addEventListener('touchmove', moveMarker, { passive: false }); + }); + + addListener(colorMarker, 'mousedown', function (event) { + addListener(document, 'mousemove', moveMarker); + }); + + addListener(colorMarker, 'touchstart', function (event) { + document.addEventListener('touchmove', moveMarker, { passive: false }); + }); + + addListener(colorValue, 'change', function (event) { + var value = colorValue.value; + + if (currentEl || settings.inline) { + var color = value === '' ? value : setColorFromStr(value); + pickColor(color); + } + }); + + addListener(clearButton, 'click', function (event) { + pickColor(''); + closePicker(); + }); + + addListener(closeButton, 'click', function (event) { + pickColor(); + closePicker(); + }); + + addListener(getEl('clr-format'), 'click', '.clr-format input', function (event) { + currentFormat = event.target.value; + updateColor(); + pickColor(); + }); + + addListener(picker, 'click', '.clr-swatches button', function (event) { + setColorFromStr(event.target.textContent); + pickColor(); + + if (settings.swatchesOnly) { + closePicker(); + } + }); + + addListener(document, 'mouseup', function (event) { + document.removeEventListener('mousemove', moveMarker); + }); + + addListener(document, 'touchend', function (event) { + document.removeEventListener('touchmove', moveMarker); + }); + + addListener(document, 'mousedown', function (event) { + keyboardNav = false; + picker.classList.remove('clr-keyboard-nav'); + closePicker(); + }); + + addListener(document, 'keydown', function (event) { + var key = event.key; + var target = event.target; + var shiftKey = event.shiftKey; + var navKeys = ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; + + if (key === 'Escape') { + closePicker(true); + + // Display focus rings when using the keyboard + } else if (navKeys.includes(key)) { + keyboardNav = true; + picker.classList.add('clr-keyboard-nav'); + } + + // Trap the focus within the color picker while it's open + if (key === 'Tab' && target.matches('.clr-picker *')) { + var focusables = getFocusableElements(); + var firstFocusable = focusables.shift(); + var lastFocusable = focusables.pop(); + + if (shiftKey && target === firstFocusable) { + lastFocusable.focus(); + event.preventDefault(); + } else if (!shiftKey && target === lastFocusable) { + firstFocusable.focus(); + event.preventDefault(); + } + } + }); + + addListener(document, 'click', '.clr-field button', function (event) { + // Reset any previously set per-instance options + if (hasInstance) { + resetVirtualInstance(); + } + + // Open the color picker + event.target.nextElementSibling.dispatchEvent(new Event('click', { bubbles: true })); + }); + + addListener(colorMarker, 'keydown', function (event) { + var movements = { + ArrowUp: [0, -1], + ArrowDown: [0, 1], + ArrowLeft: [-1, 0], + ArrowRight: [1, 0] }; + + + if (Object.keys(movements).includes(event.key)) { + moveMarkerOnKeydown.apply(void 0, movements[event.key]); + event.preventDefault(); + } + }); + + addListener(colorArea, 'click', moveMarker); + addListener(hueSlider, 'input', setHue); + addListener(alphaSlider, 'input', setAlpha); + } + + /** + * Return a list of focusable elements within the color picker. + * @return {array} The list of focusable DOM elemnts. + */ + function getFocusableElements() { + var controls = Array.from(picker.querySelectorAll('input, button')); + var focusables = controls.filter(function (node) {return !!node.offsetWidth;}); + + return focusables; + } + + /** + * Shortcut for getElementById to optimize the minified JS. + * @param {string} id The element id. + * @return {object} The DOM element with the provided id. + */ + function getEl(id) { + return document.getElementById(id); + } + + /** + * Shortcut for addEventListener to optimize the minified JS. + * @param {object} context The context to which the listener is attached. + * @param {string} type Event type. + * @param {(string|function)} selector Event target if delegation is used, event handler if not. + * @param {function} [fn] Event handler if delegation is used. + */ + function addListener(context, type, selector, fn) { + var matches = Element.prototype.matches || Element.prototype.msMatchesSelector; + + // Delegate event to the target of the selector + if (typeof selector === 'string') { + context.addEventListener(type, function (event) { + if (matches.call(event.target, selector)) { + fn.call(event.target, event); + } + }); + + // If the selector is not a string then it's a function + // in which case we need a regular event listener + } else { + fn = selector; + context.addEventListener(type, fn); + } + } + + /** + * Call a function only when the DOM is ready. + * @param {function} fn The function to call. + * @param {array} [args] Arguments to pass to the function. + */ + function DOMReady(fn, args) { + args = args !== undefined ? args : []; + + if (document.readyState !== 'loading') { + fn.apply(void 0, args); + } else { + document.addEventListener('DOMContentLoaded', function () { + fn.apply(void 0, args); + }); + } + } + + // Polyfill for Nodelist.forEach + if (NodeList !== undefined && NodeList.prototype && !NodeList.prototype.forEach) { + NodeList.prototype.forEach = Array.prototype.forEach; + } + + // Expose the color picker to the global scope + window.Coloris = function () { + var methods = { + set: configure, + wrap: wrapFields, + close: closePicker, + setInstance: setVirtualInstance, + removeInstance: removeVirtualInstance, + updatePosition: updatePickerPosition, + ready: DOMReady }; + + + function Coloris(options) { + DOMReady(function () { + if (options) { + if (typeof options === 'string') { + bindFields(options); + } else { + configure(options); + } + } + }); + }var _loop2 = function _loop2( + + key) { + Coloris[key] = function () {for (var _len = arguments.length, args = new Array(_len), _key2 = 0; _key2 < _len; _key2++) {args[_key2] = arguments[_key2];} + DOMReady(methods[key], args); + };};for (var key in methods) {_loop2(key); + } + + return Coloris; + }(); + + // Init the color picker when the DOM is ready + DOMReady(init); + +})(window, document, Math); \ No newline at end of file diff --git a/src/index.html b/src/index.html index 10c6e7f..30f3b57 100644 --- a/src/index.html +++ b/src/index.html @@ -3,6 +3,8 @@ + + Tauri App diff --git a/src/main.js b/src/main.js index 5c26fa9..5b4a295 100644 --- a/src/main.js +++ b/src/main.js @@ -8,25 +8,43 @@ let canvases = []; let mode = "draw" +let minSegmentSize = 5; + let tools = { select: { - icon: "/assets/select.png", + icon: "/assets/select.svg", + + }, + transform: { + icon: "/assets/transform.svg", }, draw: { - icon: "/assets/pen.png" + icon: "/assets/draw.svg" }, rectangle: { - icon: "/assets/rectangle.png" + icon: "/assets/rectangle.svg" }, polygon: { - icon: "assets/polygon.png" + icon: "assets/polygon.svg" } } let mouseEvent; -let context = {} +let context = { + mouseDown: false, + swatches: [ + "#000000", + "#FFFFFF", + "#FF0000", + "#FFFF00", + "#00FF00", + "#00FFFF", + "#0000FF", + "#FF00FF", + ] +} function uuidv4() { return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => @@ -34,6 +52,10 @@ function uuidv4() { ); } +function vectorDist(a, b) { + return Math.sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)) +} + function getMousePos(canvas, evt) { var rect = canvas.getBoundingClientRect(); return { @@ -69,6 +91,9 @@ class Shape { this.filled = filled; this.stroked = stroked; } + addCurve(curve) { + this.curves.push(curve) + } } class GraphicsObject { @@ -121,8 +146,9 @@ class GraphicsObject { let root = new GraphicsObject(); let shp = new Shape(100,100,'blue', 'black') -shp.curves.push(new Curve(150,150,150,150,200,100)) +shp.addCurve(new Curve(150,150,150,150,200,100)) root.addShape(shp) +context.activeObject = root async function greet() { // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ @@ -155,6 +181,60 @@ function stage() { scroller.className = "scroll" canvases.push(stage) scroller.appendChild(stage) + stage.addEventListener("mousedown", (e) => { + let mouse = getMousePos(stage, e) + switch (mode) { + case "draw": + context.mouseDown = true + context.activeShape = new Shape(mouse.x, mouse.y, context.fillStyle, context.strokeStyle, true, true) + context.activeObject.addShape(context.activeShape) + context.lastMouse = mouse + console.log(stage) + break; + + default: + break; + } + context.lastMouse = mouse + updateUI() + }) + stage.addEventListener("mouseup", (e) => { + context.mouseDown = false + let mouse = getMousePos(stage, e) + switch (mode) { + case "draw": + if (context.activeShape) { + if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { + let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} + context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + } + context.activeShape = undefined + } + break; + + default: + break; + } + context.lastMouse = mouse + updateUI() + }) + stage.addEventListener("mousemove", (e) => { + let mouse = getMousePos(stage, e) + switch (mode) { + case "draw": + if (context.activeShape) { + if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { + let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} + context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + } + } + break; + default: + break; + } + context.lastMouse = mouse + updateUI() + }) return scroller } @@ -170,6 +250,52 @@ function toolbar() { toolbtn.appendChild(icon) tools_scroller.appendChild(toolbtn) } + let tools_break = document.createElement("div") + tools_break.className = "horiz_break" + tools_scroller.appendChild(tools_break) + let fillColor = document.createElement("input") + let strokeColor = document.createElement("input") + fillColor.className = "color-field" + strokeColor.className = "color-field" + fillColor.value = "#ffffff" + strokeColor.value = "#000000" + fillColor.addEventListener('click', e => { + Coloris({ + el: ".color-field", + selectInput: true, + focusInput: true, + theme: 'default', + swatches: context.swatches, + defaultColor: '#ffffff', + onChange: (color) => { + context.fillStyle = color; + } + }) + }) + strokeColor.addEventListener('click', e => { + Coloris({ + el: ".color-field", + selectInput: true, + focusInput: true, + theme: 'default', + swatches: context.swatches, + defaultColor: '#000000', + onChange: (color) => { + context.strokeStyle = color; + } + }) + }) + // Fill and stroke colors use the same set of swatches + fillColor.addEventListener("change", e => { + context.swatches.unshift(fillColor.value) + if (context.swatches.length>12) context.swatches.pop(); + }) + strokeColor.addEventListener("change", e => { + context.swatches.unshift(strokeColor.value) + if (context.swatches.length>12) context.swatches.pop(); + }) + tools_scroller.appendChild(fillColor) + tools_scroller.appendChild(strokeColor) return tools_scroller } @@ -249,13 +375,13 @@ function updateUI() { context.ctx = ctx; root.draw(context) - let mouse; - if (mouseEvent) { - mouse = getMousePos(canvas, mouseEvent); - } else { - mouse = {x: 0, y: 0} - } - ctx.fillRect(mouse.x, mouse.y, 50,50) + // let mouse; + // if (mouseEvent) { + // mouse = getMousePos(canvas, mouseEvent); + // } else { + // mouse = {x: 0, y: 0} + // } + // ctx.fillRect(mouse.x, mouse.y, 50,50) } - requestAnimationFrame(updateUI) + // requestAnimationFrame(updateUI) } \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 7c180f3..b46af30 100644 --- a/src/styles.css +++ b/src/styles.css @@ -190,5 +190,12 @@ button { .toolbtn { /* width: var(--lineheight); height: var(--lineheight); */ + background-color: #2f2f2f; +} + +.horiz_break { + width: 100%; + height: 5px; + background-color: #2f2f2f; } \ No newline at end of file