diff --git a/l10n b/l10n index 3df11464b..ead5aeaac 160000 --- a/l10n +++ b/l10n @@ -1 +1 @@ -Subproject commit 3df11464bed7fef8c51d091e80a1ef18cfa1c65e +Subproject commit ead5aeaac4db7950b3411008a26c575082244bcc diff --git a/src/browser/app/profile/zen-browser.js b/src/browser/app/profile/zen-browser.js index 8784e766d..ba457eef5 100644 --- a/src/browser/app/profile/zen-browser.js +++ b/src/browser/app/profile/zen-browser.js @@ -83,6 +83,7 @@ pref('zen.tabs.rename-tabs', true); pref('zen.theme.accent-color', "#ffb787"); pref('zen.theme.content-element-separation', 6); // In pixels pref('zen.theme.gradient', true); +pref('zen.theme.gradient.show-custom-colors', false); pref('zen.theme.essentials-favicon-bg', true); pref('zen.tabs.show-newtab-vertical', true); diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index 7d01806eb..3b7dbedc6 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -57,7 +57,6 @@ # Images content/browser/zen-images/gradient.png (content/zen-images/gradient.png) - content/browser/zen-images/gradient-display.png (content/zen-images/gradient-display.png) content/browser/zen-images/brand-header.svg (content/zen-images/brand-header.svg) content/browser/zen-images/layouts/collapsed.png (content/zen-images/layouts/collapsed.png) content/browser/zen-images/layouts/multiple-toolbar.png (content/zen-images/layouts/multiple-toolbar.png) diff --git a/src/browser/base/content/zen-images/gradient-display.png b/src/browser/base/content/zen-images/gradient-display.png deleted file mode 100644 index c22d8f9fd..000000000 Binary files a/src/browser/base/content/zen-images/gradient-display.png and /dev/null differ diff --git a/src/browser/base/content/zen-popupset.inc.xhtml b/src/browser/base/content/zen-popupset.inc.xhtml index f2bbdf531..babf2a244 100644 --- a/src/browser/base/content/zen-popupset.inc.xhtml +++ b/src/browser/base/content/zen-popupset.inc.xhtml @@ -82,28 +82,45 @@ - + + + + + + + + + + + + + + + +
+ + + + +
+
- - - - - - - + - - + diff --git a/src/browser/base/content/zen-styles/zen-gradient-generator.css b/src/browser/base/content/zen-styles/zen-gradient-generator.css index c9a0844ae..2fe3087bd 100644 --- a/src/browser/base/content/zen-styles/zen-gradient-generator.css +++ b/src/browser/base/content/zen-styles/zen-gradient-generator.css @@ -17,60 +17,10 @@ width: 100%; } -#PanelUI-zen-gradient-degrees { - position: relative; - border-radius: 999px; - min-height: 50px; - min-width: 50px; - max-height: 50px; - max-width: 50px; - border: 3px solid var(--zen-primary-color); - display: flex; - justify-items: center; - align-items: center; - z-index: 1; - - & .dot { - position: absolute; - z-index: 2; - /* +3 because of the border */ - width: 53px; - height: 53px; - top: -3px; - left: -3px; - - &::after { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--zen-colors-tertiary); - box-shadow: 0 0 0 2px var(--zen-themed-toolbar-bg); - cursor: grab; - content: ''; - position: absolute; - bottom: 2px; - right: 0; - border: 2px solid var(--zen-colors-border); - transition: transform 0.2s; - } - - &[dragging='true']::after { - transform: scale(1.2); - } - } - - & .text { - margin: 0 auto; - } -} - #PanelUI-zen-gradient-generator-controls { padding-right: var(--panel-padding); align-items: center; gap: var(--panel-padding); - border-bottom: 1px solid color-mix(in srgb, var(--zen-colors-border) 50%, transparent 50%); - padding-bottom: 15px; - margin-bottom: 15px; } #zen-theme-picker-color { @@ -97,8 +47,8 @@ #PanelUI-zen-gradient-generator-color-custom-add { position: absolute; - right: 5px; - top: 5px; + right: 0px; + top: 0px; cursor: pointer; } @@ -112,11 +62,30 @@ gap: 10px; } -#PanelUI-zen-gradient-generator-options { +#PanelUI-zen-gradient-generator-predefined { + display: flex; + gap: 5px; align-items: center; - margin: auto; - height: 100%; - justify-content: center; + margin: 5px 5px 10px 0; + + & > box { + width: 18px; + height: 18px; + border-radius: 50%; + cursor: pointer; + position: relative; + &::after { + content: ''; + position: absolute; + width: 18px; + height: 18px; + top: 0; + left: 0; + outline: 2px solid var(--zen-toolbar-element-bg); + border-radius: 50%; + pointer-events: none; + } + } } #PanelUI-zen-gradient-generator-custom-list { @@ -178,80 +147,118 @@ &::-moz-range-track { background: var(--zen-colors-border); border-radius: 999px; - height: 4px; + height: 6px; } &::-moz-range-progress { background: var(--zen-primary-color); border-radius: 999px; - height: 4px; + height: 6px; } } .zen-theme-picker-gradient { position: relative; - border-radius: calc(var(--zen-border-radius) + 5px); + border-radius: calc(var(--zen-border-radius) - 2px); overflow: hidden; - border: 1px solid var(--zen-colors-border); + border-radius: var(--zen-border-radius); min-height: calc(var(--panel-width) - var(--panel-padding) * 2); - min-width: calc(var(--panel-width) - var(--panel-padding) * 2); + margin-bottom: 20px; - margin-bottom: 15px; - - background: var(--zen-theme-picker-gradient-image) center center / cover no-repeat; - - &::before { - background: light-dark( - color-mix(in srgb, var(--zen-themed-toolbar-bg) 60%, transparent 40%), - color-mix(in srgb, var(--zen-themed-toolbar-bg) 80%, transparent 20%) - ); - backdrop-filter: blur(5px); - content: ''; - position: absolute; - top: 0; - left: 0; - pointer-events: none; - margin: var(--panel-padding); - box-shadow: 0 0 0 1px 1px var(--zen-colors-border); - width: calc(100% - var(--panel-padding) * 2); - height: calc(100% - var(--panel-padding) * 2); - border-radius: var(--zen-border-radius); - } - - &::after { - z-index: 0; - position: absolute; - inset: 0; - background-image: linear-gradient(to right, var(--zen-primary-color) 1px, transparent 1px), - linear-gradient(to bottom, var(--zen-primary-color) 1px, transparent 1px); - background-size: 24px 24px; - content: ''; - position: absolute; - top: 0; - left: 0; - margin: var(--panel-padding); - width: calc(100% - var(--panel-padding) * 2); - height: calc(100% - var(--panel-padding) * 2); - opacity: 0.1; - } + background: var(--zen-toolbar-element-bg); + background-image: radial-gradient(light-dark(rgba(0, 0, 0, 0.05), rgba(0,0,0,.225)) 1px, transparent 0); + background-position: -19px -19px; + background-size: 10px 10px; & .zen-theme-picker-dot { position: absolute; z-index: 2; - width: 20px; + width: 22px; height: 20px; border-radius: 50%; background: var(--zen-theme-picker-dot-color); - box-shadow: 0 0 0 2px var(--zen-themed-toolbar-bg); + @media (-prefers-color-scheme: dark) { + box-shadow: 0 0 0 2px rgba(0,0,0,.1); + } cursor: pointer; - border: 2px solid #fff; + border: 2px solid #ffffff; animation: zen-theme-picker-dot-animation 0.5s; transition: transform 0.2s; transform: translate(-50%, -50%); + &.primary { + width: 28px; + height: 28px; + border-width: 3px; + } + &[dragging='true'] { transform: scale(1.2) translate(-50%, -50%) !important; } } } + +#PanelUI-zen-gradient-generator-color-click-to-add { + position: absolute; + font-weight: 600; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + white-space: nowrap; + pointer-events: none; + + &[hidden] { + display: none; + } +} + +#PanelUI-zen-gradient-generator-color-actions { + display: flex; + position: absolute; + bottom: 8px; + left: 50%; + z-index: 1; + transform: translateX(-50%); + & .separator, + & button { + background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)); + } + + & button { + border: none !important; + padding: 0.4rem !important; + min-width: fit-content !important; + transition: background 0.2s; + appearance: none; + + & .button-box { + gap: 0.1rem; + } + + &:not(#PanelUI-zen-gradient-generator-color-toggle-algo) .button-text { + display: none; + } + + &:hover { + background: light-dark(#cfcfcf, #313131); + } + + &[disabled] { + opacity: 0.5; + cursor: not-allowed; + } + } + + & .separator { + width: 1px; + margin: 0 0.5rem; + height: 30px; + } +} + +@media not (-moz-bool-pref: 'zen.theme.gradient.show-custom-colors') { + #PanelUI-zen-gradient-generator-custom-colors { + display: none !important; + } +} diff --git a/src/browser/base/content/zen-styles/zen-welcome.css b/src/browser/base/content/zen-styles/zen-welcome.css index 441a82ba7..344afb8b0 100644 --- a/src/browser/base/content/zen-styles/zen-welcome.css +++ b/src/browser/base/content/zen-styles/zen-welcome.css @@ -166,7 +166,7 @@ #zen-welcome-workspace-colors-anchor { width: 1px; height: 1px; - margin: 10% auto auto 30%; + margin: 20% auto auto 28%; } #zen-welcome-initial-essentials-browser { diff --git a/src/browser/base/zen-components/ZenGradientGenerator.mjs b/src/browser/base/zen-components/ZenGradientGenerator.mjs index 884c0f020..ecf160e34 100644 --- a/src/browser/base/zen-components/ZenGradientGenerator.mjs +++ b/src/browser/base/zen-components/ZenGradientGenerator.mjs @@ -1,14 +1,12 @@ { class ZenThemePicker extends ZenMultiWindowFeature { static GRADIENT_IMAGE_URL = 'chrome://browser/content/zen-images/gradient.png'; - static GRADIENT_DISPLAY_URL = 'chrome://browser/content/zen-images/gradient-display.png'; - static MAX_DOTS = 5; + static MAX_DOTS = 3; currentOpacity = 0.5; currentRotation = 45; - - numberOfDots = 0; - + dots = []; + useAlgo = ''; constructor() { super(); if (!Services.prefs.getBoolPref('zen.theme.gradient', true) || !ZenWorkspaces.shouldHaveWorkspaces) { @@ -33,7 +31,6 @@ this.onDarkModeChange.bind(this) ); - this.initRotation(); this.initCanvas(); this.initCustomColorInput(); @@ -79,7 +76,7 @@ onImageLoad() { // resize the image to fit the panel - const imageSize = 300 - 20; // 20 is the padding (10px) + const imageSize = 350 - 20; // 20 is the padding (10px) const scale = imageSize / Math.max(this.image.width, this.image.height); this.image.width *= scale; this.image.height *= scale; @@ -92,78 +89,52 @@ // Call the rest of the initialization this.initContextMenu(); - this.initThemePicker(); + this.initPredefinedColors(); this._hasInitialized = true; this.onDarkModeChange(null); } - initRotation() { - this.rotationInput = document.getElementById('PanelUI-zen-gradient-degrees'); - this.rotationInputDot = this.rotationInput.querySelector('.dot'); - this.rotationInputText = this.rotationInput.querySelector('.text'); - this.rotationInputDot.addEventListener('mousedown', this.onRotationMouseDown.bind(this)); - this.rotationInput.addEventListener('wheel', this.onRotationWheel.bind(this)); - } - - onRotationWheel(event) { - event.preventDefault(); - const delta = event.deltaY; - const degrees = this.currentRotation + (delta > 0 ? 10 : -10); - this.setRotationInput(degrees); - this.updateCurrentWorkspace(); - } - - onRotationMouseDown(event) { - event.preventDefault(); - this.rotationDragging = true; - this.rotationInputDot.style.zIndex = 2; - this.rotationInputDot.classList.add('dragging'); - document.addEventListener('mousemove', this.onRotationMouseMove.bind(this)); - document.addEventListener('mouseup', this.onRotationMouseUp.bind(this)); - } - - onRotationMouseUp(event) { - this.rotationDragging = false; - this.rotationInputDot.style.zIndex = 1; - this.rotationInputDot.classList.remove('dragging'); - document.removeEventListener('mousemove', this.onRotationMouseMove.bind(this)); - document.removeEventListener('mouseup', this.onRotationMouseUp.bind(this)); - } - - onRotationMouseMove(event) { - if (this.rotationDragging) { - event.preventDefault(); - const rect = this.rotationInput.getBoundingClientRect(); - // Make the dot follow the mouse in a circle, it can't go outside or inside the circle - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - const angle = Math.atan2(event.clientY - centerY, event.clientX - centerX); - const distance = Math.sqrt((event.clientX - centerX) ** 2 + (event.clientY - centerY) ** 2); - const radius = rect.width / 2; - let x = centerX + Math.cos(angle) * radius; - let y = centerY + Math.sin(angle) * radius; - if (distance > radius) { - x = event.clientX; - y = event.clientY; + initPredefinedColors() { + document.getElementById('PanelUI-zen-gradient-generator-predefined').addEventListener('click', async (event) => { + const target = event.target; + const rawPosition = target.getAttribute('data-position'); + if (!rawPosition) { + return; } - const degrees = Math.round((Math.atan2(y - centerY, x - centerX) * 180) / Math.PI); - this.setRotationInput(degrees); + const algo = target.getAttribute('data-algo'); + const numDots = parseInt(target.getAttribute('data-num-dots')); + if (algo == 'float') { + for (const dot of this.dots) { + dot.element.remove(); + } + this.dots = []; + } + // Generate new gradient from the single color given + const [x, y] = rawPosition.split(',').map((pos) => parseInt(pos)); + let dots = [ + { + ID: 0, + position: { x, y }, + }, + ]; + for (let i = 1; i < numDots; i++) { + dots.push({ + ID: i, + position: { x: 0, y: 0 }, + }); + } + this.useAlgo = algo; + dots = this.calculateCompliments(dots, 'update', this.useAlgo); + if (algo == 'float') { + for (const dot of dots) { + this.spawnDot(dot.position); + } + this.dots[0].element.classList.add('primary'); + } + this.handleColorPositions(dots); this.updateCurrentWorkspace(); - } - } - - setRotationInput(degrees) { - let fixedRotation = degrees; - while (fixedRotation < 0) { - fixedRotation += 360; - } - while (fixedRotation >= 360) { - fixedRotation -= 360; - } - this.currentRotation = degrees; - this.rotationInputDot.style.transform = `rotate(${degrees - 20}deg)`; - this.rotationInputText.textContent = `${fixedRotation}°`; + }); } initCustomColorInput() { @@ -171,7 +142,7 @@ } onCustomColorKeydown(event) { - //checks for enter key for custom colors + // Check for Enter key to add custom colors if (event.key === 'Enter') { event.preventDefault(); this.addCustomColor(); @@ -180,10 +151,26 @@ initThemePicker() { const themePicker = this.panel.querySelector('.zen-theme-picker-gradient'); - themePicker.style.setProperty('--zen-theme-picker-gradient-image', `url(${ZenThemePicker.GRADIENT_DISPLAY_URL})`); - themePicker.addEventListener('mousemove', this.onDotMouseMove.bind(this)); - themePicker.addEventListener('mouseup', this.onDotMouseUp.bind(this)); - themePicker.addEventListener('click', this.onThemePickerClick.bind(this)); + this._onDotMouseMove = this.onDotMouseMove.bind(this); + this._onDotMouseUp = this.onDotMouseUp.bind(this); + this._onDotMouseDown = this.onDotMouseDown.bind(this); + this._onThemePickerClick = this.onThemePickerClick.bind(this); + document.addEventListener('mousemove', this._onDotMouseMove); + document.addEventListener('mouseup', this._onDotMouseUp); + themePicker.addEventListener('mousedown', this._onDotMouseDown); + themePicker.addEventListener('click', this._onThemePickerClick); + } + + uninitThemePicker() { + const themePicker = this.panel.querySelector('.zen-theme-picker-gradient'); + document.removeEventListener('mousemove', this._onDotMouseMove); + document.removeEventListener('mouseup', this._onDotMouseUp); + themePicker.removeEventListener('mousedown', this._onDotMouseDown); + themePicker.removeEventListener('click', this._onThemePickerClick); + this._onDotMouseMove = null; + this._onDotMouseUp = null; + this._onDotMouseDown = null; + this._onThemePickerClick = null; } calculateInitialPosition(color) { @@ -224,7 +211,9 @@ createDot(color, fromWorkspace = false) { const [r, g, b] = color.c; const dot = document.createElement('div'); - dot.classList.add('zen-theme-picker-dot'); + if (color.isPrimary) { + dot.classList.add('primary'); + } if (color.isCustom) { if (!color.c) { return; @@ -233,64 +222,31 @@ dot.style.opacity = 0; dot.style.setProperty('--zen-theme-picker-dot-color', color.c); } else { - dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${r}, ${g}, ${b})`); const { x, y } = this.calculateInitialPosition(color); + const dotPad = this.panel.querySelector('.zen-theme-picker-gradient'); + + dot.classList.add('zen-theme-picker-dot'); + dot.style.left = `${x * 100}%`; dot.style.top = `${y * 100}%`; - dot.addEventListener('mousedown', this.onDotMouseDown.bind(this)); - } - this.panel.querySelector('.zen-theme-picker-gradient').appendChild(dot); - if (!fromWorkspace) { - this.updateCurrentWorkspace(true); - } - } - onDotMouseDown(event) { - event.preventDefault(); - if (event.button === 2) { - return; - } - this.dragging = true; - this.draggedDot = event.target; - this.draggedDot.style.zIndex = 1; - this.draggedDot.classList.add('dragging'); - - // Store the starting position of the drag - this.dragStartPosition = { - x: event.clientX, - y: event.clientY, - }; - } - - onDotMouseMove(event) { - if (this.dragging) { - event.preventDefault(); - const rect = this.panel.querySelector('.zen-theme-picker-gradient').getBoundingClientRect(); - const padding = 90; // each side - // do NOT let the ball be draged outside of an imaginary circle. You can drag it anywhere inside the circle - // if the distance between the center of the circle and the dragged ball is bigger than the radius, then the ball - // should be placed on the edge of the circle. If it's inside the circle, then the ball just follows the mouse - - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - const radius = (rect.width - padding) / 2; - let pixelX = event.clientX; - let pixelY = event.clientY; - const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2); - if (distance > radius) { - const angle = Math.atan2(pixelY - centerY, pixelX - centerX); - pixelX = centerX + Math.cos(angle) * radius; - pixelY = centerY + Math.sin(angle) * radius; + if (this.dots.length < 1) { + dot.classList.add('primary'); } - // set the location of the dot in pixels - const relativeX = pixelX - rect.left; - const relativeY = pixelY - rect.top; - this.draggedDot.style.left = `${relativeX}px`; - this.draggedDot.style.top = `${relativeY}px`; - const color = this.getColorFromPosition(relativeX, relativeY); - this.draggedDot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${color[0]}, ${color[1]}, ${color[2]})`); - this.updateCurrentWorkspace(); + dotPad.appendChild(dot); + let id = this.dots.length; + + dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${r}, ${g}, ${b})`); + + this.dots.push({ + ID: id, + element: dot, + position: { x: null, y: null }, // at some point possition should instead be stored as percentege just so that the size of the color picker does not matter. + }); + } + if (!fromWorkspace) { + this.updateCurrentWorkspace(true); } } @@ -331,14 +287,275 @@ await this.updateCurrentWorkspace(); } + spawnDot(relativePosition, primary = false) { + const dotPad = this.panel.querySelector('.zen-theme-picker-gradient'); + + const dot = document.createElement('div'); + dot.classList.add('zen-theme-picker-dot'); + + dot.style.left = `${relativePosition.x}px`; + dot.style.top = `${relativePosition.y}px`; + + dotPad.appendChild(dot); + + let id = this.dots.length; + + if (primary) { + id = 0; + dot.classList.add('primary'); + + const existingPrimaryDot = this.dots.find((d) => d.ID === 0); + if (existingPrimaryDot) { + existingPrimaryDot.ID = this.dots.length; + existingPrimaryDot.element.classList.remove('primary'); + } + dot.classList.add('primary'); + } + + const colorFromPos = this.getColorFromPosition(relativePosition.x, relativePosition.y); + dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})`); + + this.dots.push({ + ID: id, + element: dot, + position: { x: relativePosition.x, y: relativePosition.y }, + }); + } + + calculateCompliments(dots, action = 'update', useHarmony = '') { + const colorHarmonies = [ + { type: 'complementary', angles: [180] }, + { type: 'splitComplementary', angles: [150, 210] }, + { type: 'analogous', angles: [30, 330] }, + { type: 'triadic', angles: [120, 240] }, + { type: 'floating', angles: [] }, + ]; + + if (dots.length === 0) { + return []; + } + + function getColorHarmonyType(numDots, dots) { + if (useHarmony !== '') { + const selectedHarmony = colorHarmonies.find((harmony) => harmony.type === useHarmony); + + if (selectedHarmony) { + if (action === 'remove') { + if (dots.length !== 0) { + return colorHarmonies.find((harmony) => harmony.angles.length === selectedHarmony.angles.length - 1); + } else { + return { type: 'floating', angles: [] }; + } + } + if (action === 'add') { + return colorHarmonies.find((harmony) => harmony.angles.length === selectedHarmony.angles.length + 1); + } + if (action === 'update') { + return selectedHarmony; + } + } + } + + if (action === 'remove') { + return colorHarmonies.find((harmony) => harmony.angles.length === numDots); + } + if (action === 'add') { + return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots); + } + if (action === 'update') { + return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots); + } + } + + function getAngleFromPosition(position, centerPosition) { + let deltaX = position.x - centerPosition.x; + let deltaY = position.y - centerPosition.y; + let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI); + return (angle + 360) % 360; + } + + function getDistanceFromCenter(position, centerPosition) { + const deltaX = position.x - centerPosition.x; + const deltaY = position.y - centerPosition.y; + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + } + + const dotPad = this.panel.querySelector('.zen-theme-picker-gradient'); + const rect = dotPad.getBoundingClientRect(); + const padding = 90; + + let updatedDots = [...dots]; + const centerPosition = { x: rect.width / 2, y: rect.height / 2 }; + + const harmonyAngles = getColorHarmonyType(dots.length + (action === 'add' ? 1 : action === 'remove' ? -1 : 0), this.dots); + + this.useAlgo = harmonyAngles.type; + + if (!harmonyAngles || harmonyAngles.angles.length === 0) return dots; + + let primaryDot = dots.find((dot) => dot.ID === 0); + if (!primaryDot) return []; + + if (action === 'add' && this.dots.length) { + updatedDots.push({ ID: this.dots.length, position: centerPosition }); + } + const baseAngle = getAngleFromPosition(primaryDot.position, centerPosition); + let distance = getDistanceFromCenter(primaryDot.position, centerPosition); + const radius = (rect.width - padding) / 2; + if (distance > radius) distance = radius; + + if (this.dots.length > 0) { + updatedDots = [{ ID: 0, position: primaryDot.position }]; + } + + harmonyAngles.angles.forEach((angleOffset, index) => { + let newAngle = (baseAngle + angleOffset) % 360; + let radian = (newAngle * Math.PI) / 180; + + let newPosition = { + x: centerPosition.x + distance * Math.cos(radian), + y: centerPosition.y + distance * Math.sin(radian), + }; + + updatedDots.push({ ID: index + 1, position: newPosition }); + }); + + return updatedDots; + } + + handleColorPositions(colorPositions) { + colorPositions.sort((a, b) => a.ID - b.ID); + if (this.useAlgo === 'floating') { + const dotPad = this.panel.querySelector('.zen-theme-picker-gradient'); + const rect = dotPad.getBoundingClientRect(); + this.dots.forEach((dot) => { + dot.element.style.zIndex = 999; + + let pixelX, pixelY; + if (dot.position.x === null) { + const leftPercentage = parseFloat(dot.element.style.left) / 100; + const topPercentage = parseFloat(dot.element.style.top) / 100; + + pixelX = leftPercentage * rect.width; + pixelY = topPercentage * rect.height; + } else { + pixelX = dot.position.x; + pixelY = dot.position.y; + } + + const colorFromPos = this.getColorFromPosition(pixelX, pixelY); + + dot.element.style.setProperty( + '--zen-theme-picker-dot-color', + `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})` + ); + }); + + return; + } + const existingPrimaryDot = this.dots.find((d) => d.ID === 0); + + if (existingPrimaryDot) { + existingPrimaryDot.element.style.zIndex = 999; + const colorFromPos = this.getColorFromPosition(existingPrimaryDot.position.x, existingPrimaryDot.position.y); + existingPrimaryDot.element.style.setProperty( + '--zen-theme-picker-dot-color', + `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})` + ); + } + + colorPositions.forEach((dotPosition) => { + const existingDot = this.dots.find((dot) => dot.ID === dotPosition.ID); + + if (existingDot) { + existingDot.position = dotPosition.position; + existingDot.element.style.left = `${dotPosition.position.x}px`; + existingDot.element.style.top = `${dotPosition.position.y}px`; + const colorFromPos = this.getColorFromPosition(dotPosition.position.x, dotPosition.position.y); + existingDot.element.style.setProperty( + '--zen-theme-picker-dot-color', + `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})` + ); + + if (!this.dragging) { + gZenUIManager.motion.animate( + existingDot.element, + { + left: `${dotPosition.position.x}px`, + top: `${dotPosition.position.y}px`, + }, + { + duration: 0.4, + type: 'spring', + bounce: 0.3, + } + ); + } + } else { + this.spawnDot(dotPosition.position); + } + }); + } + onThemePickerClick(event) { event.preventDefault(); + const target = event.target; + if (target.id === 'PanelUI-zen-gradient-generator-color-add') { + if (this.dots.length >= ZenThemePicker.MAX_DOTS) return; + let colorPositions = this.calculateCompliments(this.dots, 'add', this.useAlgo); - if (event.button !== 0 || this.dragging) return; + this.handleColorPositions(colorPositions); + this.updateCurrentWorkspace(); + return; + } else if (target.id === 'PanelUI-zen-gradient-generator-color-remove') { + this.dots.sort((a, b) => a.ID - b.ID); + if (this.dots.length === 0) return; + + const lastDot = this.dots.pop(); + lastDot.element.remove(); + + this.dots.forEach((dot, index) => { + dot.ID = index; + if (index === 0) { + dot.element.classList.add('primary'); + } else { + dot.element.classList.remove('primary'); + } + }); + + let colorPositions = this.calculateCompliments(this.dots, 'remove', this.useAlgo); + this.handleColorPositions(colorPositions); + this.updateCurrentWorkspace(); + return; + } else if (target.id === 'PanelUI-zen-gradient-generator-color-toggle-algo') { + const colorHarmonies = [ + { type: 'complementary', angles: [180] }, + { type: 'splitComplementary', angles: [150, 210] }, + { type: 'analogous', angles: [30, 330] }, + { type: 'triadic', angles: [120, 240] }, + { type: 'floating', angles: [] }, + ]; + + const applicableHarmonies = colorHarmonies.filter( + (harmony) => harmony.angles.length + 1 === this.dots.length || harmony.type === 'floating' + ); + + let currentIndex = applicableHarmonies.findIndex((harmony) => harmony.type === this.useAlgo); + + let nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % applicableHarmonies.length; + this.useAlgo = applicableHarmonies[nextIndex].type; + + let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo); + this.handleColorPositions(colorPositions); + this.updateCurrentWorkspace(); + return; + } + + if (event.button !== 0 || this.dragging || this.recentlyDragged) return; const gradient = this.panel.querySelector('.zen-theme-picker-gradient'); const rect = gradient.getBoundingClientRect(); - const padding = 90; // each side + const padding = 90; const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; @@ -346,44 +563,79 @@ let pixelX = event.clientX; let pixelY = event.clientY; - // Check if the click is within the circle - const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2); - if (distance > radius) { + const clickedElement = event.target; + let clickedDot = null; + const existingPrimaryDot = this.dots.find((d) => d.ID === 0); + + clickedDot = this.dots.find((dot) => dot.element === clickedElement); + + if (clickedDot) { + // TODO: this doesnt work and needs to be fixed + existingPrimaryDot.ID = clickedDot.ID; + clickedDot.ID = 0; + clickedDot.element.style.zIndex = 999; + + let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo); + this.handleColorPositions(colorPositions); return; } - const clickedElement = event.target; - const isExistingDot = clickedElement.classList.contains('zen-theme-picker-dot'); + const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2); + if (distance > radius) { + const angle = Math.atan2(pixelY - centerY, pixelX - centerX); + pixelX = centerX + Math.cos(angle) * radius; + pixelY = centerY + Math.sin(angle) * radius; + } - if (!isExistingDot && this.numberOfDots < ZenThemePicker.MAX_DOTS) { - const relativeX = event.clientX - rect.left; - const relativeY = event.clientY - rect.top; + const relativeX = pixelX - rect.left; + const relativeY = pixelY - rect.top; - const color = this.getColorFromPosition(relativeX, relativeY); - - const dot = document.createElement('div'); - dot.classList.add('zen-theme-picker-dot'); - dot.addEventListener('mousedown', this.onDotMouseDown.bind(this)); - - dot.style.left = `${relativeX}px`; - dot.style.top = `${relativeY}px`; - dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${color[0]}, ${color[1]}, ${color[2]})`); - - gradient.appendChild(dot); + if (!clickedDot && this.dots.length < 1) { + if (this.dots.length === 0) { + this.spawnDot({ x: relativeX, y: relativeY }, true); + } else { + this.spawnDot({ x: relativeX, y: relativeY }); + } this.updateCurrentWorkspace(true); + } else if (!clickedDot && existingPrimaryDot) { + existingPrimaryDot.element.style.left = `${relativeX}px`; + existingPrimaryDot.element.style.top = `${relativeY}px`; + existingPrimaryDot.position = { + x: relativeX, + y: relativeY, + }; + + let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo); + this.handleColorPositions(colorPositions); + this.updateCurrentWorkspace(true); + + gZenUIManager.motion.animate( + existingPrimaryDot.element, + { + left: `${existingPrimaryDot.position.x}px`, + top: `${existingPrimaryDot.position.y}px`, + }, + { + duration: 0.4, + type: 'spring', + bounce: 0.3, + } + ); } } onDotMouseDown(event) { - event.preventDefault(); if (event.button === 2) { return; } - this.dragging = true; - this.draggedDot = event.target; - this.draggedDot.style.zIndex = 1; - this.draggedDot.classList.add('dragging'); + const draggedDot = this.dots.find((dot) => dot.element === event.target); + if (draggedDot) { + event.preventDefault(); + this.dragging = true; + this.draggedDot = event.target; + this.draggedDot.classList.add('dragging'); + } // Store the starting position of the drag this.dragStartPosition = { @@ -397,9 +649,25 @@ if (!event.target.classList.contains('zen-theme-picker-dot')) { return; } + this.dots = this.dots.filter((dot) => dot.element !== event.target); event.target.remove(); + + this.dots.sort((a, b) => a.ID - b.ID); + + // Reassign the IDs after sorting + this.dots.forEach((dot, index) => { + dot.ID = index; + if (index === 0) { + dot.element.classList.add('primary'); + } else { + dot.element.classList.remove('primary'); + } + }); + + let colorPositions = this.calculateCompliments(this.dots, 'remove', this.useAlgo); + this.handleColorPositions(colorPositions); + this.updateCurrentWorkspace(); - this.numberOfDots--; return; } @@ -407,27 +675,68 @@ event.preventDefault(); event.stopPropagation(); this.dragging = false; - this.draggedDot.style.zIndex = 1; this.draggedDot.classList.remove('dragging'); this.draggedDot = null; this.dragStartPosition = null; // Reset the drag start position + + this.recentlyDragged = true; + setTimeout(() => { + this.recentlyDragged = false; + }, 100); return; } + } - this.numberOfDots = this.panel.querySelectorAll('.zen-theme-picker-dot').length; + onDotMouseMove(event) { + if (this.dragging) { + event.preventDefault(); + const rect = this.panel.querySelector('.zen-theme-picker-gradient').getBoundingClientRect(); + const padding = 90; // each side + // do NOT let the ball be draged outside of an imaginary circle. You can drag it anywhere inside the circle + // if the distance between the center of the circle and the dragged ball is bigger than the radius, then the ball + // should be placed on the edge of the circle. If it's inside the circle, then the ball just follows the mouse + + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const radius = (rect.width - padding) / 2; + let pixelX = event.clientX; + let pixelY = event.clientY; + const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2); + if (distance > radius) { + const angle = Math.atan2(pixelY - centerY, pixelX - centerX); + pixelX = centerX + Math.cos(angle) * radius; + pixelY = centerY + Math.sin(angle) * radius; + } + + // set the location of the dot in pixels + const relativeX = pixelX - rect.left; + const relativeY = pixelY - rect.top; + + const draggedDot = this.dots.find((dot) => dot.element === this.draggedDot); + draggedDot.element.style.left = `${relativeX}px`; + draggedDot.element.style.top = `${relativeY}px`; + draggedDot.position = { + x: relativeX, + y: relativeY, + }; + let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo); + this.handleColorPositions(colorPositions); + + this.updateCurrentWorkspace(); + } } themedColors(colors) { const isDarkMode = this.isDarkMode; const factor = isDarkMode ? 0.5 : 1.1; - return colors.map((color) => { - return { - c: color.isCustom - ? color.c - : [Math.min(255, color.c[0] * factor), Math.min(255, color.c[1] * factor), Math.min(255, color.c[2] * factor)], - isCustom: color.isCustom, - }; - }); + + return colors.map((color) => ({ + c: color.isCustom + ? color.c + : [Math.min(255, color.c[0] * factor), Math.min(255, color.c[1] * factor), Math.min(255, color.c[2] * factor)], + isCustom: color.isCustom, + algorithm: color.algorithm, + })); } onOpacityChange(event) { @@ -456,6 +765,8 @@ getGradient(colors, forToolbar = false) { const themedColors = this.themedColors(colors); + this.useAlgo = themedColors[0]?.algorithm ?? ''; + if (themedColors.length === 0) { return forToolbar ? 'var(--zen-themed-toolbar-bg)' : 'var(--zen-themed-toolbar-bg-transparent)'; } else if (themedColors.length === 1) { @@ -546,23 +857,7 @@ }; getMostDominantColor(allColors) { - const colors = this.themedColors(allColors); - const themedColors = colors.filter((color) => !color.isCustom); - if (themedColors.length === 0 || !this.allowWorkspaceColors) { - return null; - } - // get the most dominant color in the gradient - let dominantColor = themedColors[0].c; - let dominantColorCount = 0; - for (const color of themedColors) { - const count = themedColors.filter( - (c) => c.c[0] === color.c[0] && c.c[1] === color.c[1] && c.c[2] === color.c[2] - ).length; - if (count > dominantColorCount) { - dominantColorCount = count; - dominantColor = color.c; - } - } + const dominantColor = this.getPrimaryColor(allColors); const result = this.pSBC( this.isDarkMode ? 0.2 : -0.5, `rgb(${dominantColor[0]}, ${dominantColor[1]}, ${dominantColor[2]})` @@ -586,7 +881,7 @@ } // get the theme from the window - workspaceTheme = theme || windowWorkspace.theme; + workspaceTheme = this.fixTheme(theme || windowWorkspace.theme); if (!skipUpdate) { for (const dot of browser.gZenThemePicker.panel.querySelectorAll('.zen-theme-picker-dot')) { @@ -617,6 +912,9 @@ }); } + const button = browser.document.getElementById('PanelUI-zen-gradient-generator-color-toggle-algo'); + document.l10n.setAttributes(button, `zen-panel-ui-gradient-generator-algo-${browser.gZenThemePicker.useAlgo}`); + browser.gZenThemePicker.resetCustomColorList(); if (!workspaceTheme || workspaceTheme.type !== 'gradient') { const gradient = browser.gZenThemePicker.getGradient([]); @@ -632,13 +930,22 @@ browser.gZenThemePicker.currentRotation = workspaceTheme.rotation ?? 45; browser.gZenThemePicker.currentTexture = workspaceTheme.texture ?? 0; - browser.gZenThemePicker.numberOfDots = workspaceTheme.gradientColors.length; + for (const button of browser.document.querySelectorAll('#PanelUI-zen-gradient-generator-color-actions button')) { + // disable if there are no buttons + button.disabled = + workspaceTheme.gradientColors.length === 0 || + (button.id === 'PanelUI-zen-gradient-generator-color-add' + ? workspaceTheme.gradientColors.length >= ZenThemePicker.MAX_DOTS + : false); + } + document + .getElementById('PanelUI-zen-gradient-generator-color-click-to-add') + .toggleAttribute('hidden', workspaceTheme.gradientColors.length > 0); browser.document.getElementById('PanelUI-zen-gradient-generator-opacity').value = browser.gZenThemePicker.currentOpacity; browser.document.getElementById('PanelUI-zen-gradient-generator-texture').value = browser.gZenThemePicker.currentTexture; - browser.gZenThemePicker.setRotationInput(browser.gZenThemePicker.currentRotation); const gradient = browser.gZenThemePicker.getGradient(workspaceTheme.gradientColors); const gradientToolbar = browser.gZenThemePicker.getGradient(workspaceTheme.gradientColors, true); @@ -662,11 +969,20 @@ } if (!skipUpdate) { + this.dots = []; browser.gZenThemePicker.recalculateDots(workspaceTheme.gradientColors); } }); } + fixTheme(theme) { + // add a primary color if there isn't one + if (!theme.gradientColors.find((color) => color.isPrimary) && theme.gradientColors.length > 0) { + theme.gradientColors[(theme.gradientColors.length / 2) | 0].isPrimary = true; + } + return theme; + } + get riceManager() { if (!this._riceManager) { this._riceManager = new window.ZenRiceManager(); @@ -701,8 +1017,19 @@ this.updateCurrentWorkspace(); } + getPrimaryColor(colors) { + const primaryColor = colors.find((color) => color.isPrimary); + if (primaryColor) { + return primaryColor.c; + } + if (colors.length === 0) { + return this.hexToRgb(this.getNativeAccentColor()); + } + // Get the middle color + return colors[Math.floor(colors.length / 2)].c; + } + recalculateDots(colors) { - //THIS IS PART OF THE ISSUE for (const color of colors) { this.createDot(color, true); } @@ -711,14 +1038,19 @@ async updateCurrentWorkspace(skipSave = true) { this.updated = skipSave; const dots = this.panel.querySelectorAll('.zen-theme-picker-dot'); - const colors = Array.from(dots).map((dot) => { - const color = dot.style.getPropertyValue('--zen-theme-picker-dot-color'); - if (color === 'undefined') { - return; - } - const isCustom = dot.classList.contains('custom'); - return { c: isCustom ? color : color.match(/\d+/g).map(Number), isCustom }; - }); + const colors = Array.from(dots) + .sort((a, b) => a.getAttribute('data-index') - b.getAttribute('data-index')) + .map((dot) => { + const color = dot.style.getPropertyValue('--zen-theme-picker-dot-color'); + const isPrimary = dot.classList.contains('primary'); + + if (color === 'undefined') { + return; + } + const isCustom = dot.classList.contains('custom'); + const algorithm = this.useAlgo; + return { c: isCustom ? color : color.match(/\d+/g).map(Number), isCustom, algorithm, isPrimary }; + }); const gradient = ZenThemePicker.getTheme(colors, this.currentOpacity, this.currentRotation, this.currentTexture); let currentWorkspace = await ZenWorkspaces.getActiveWorkspace(); @@ -736,6 +1068,11 @@ if (this.updated) { await this.updateCurrentWorkspace(false); } + this.uninitThemePicker(); + } + + handlePanelOpen() { + this.initThemePicker(); } } diff --git a/src/browser/base/zen-components/ZenWelcome.mjs b/src/browser/base/zen-components/ZenWelcome.mjs index 3a6d16a97..8652f50c7 100644 --- a/src/browser/base/zen-components/ZenWelcome.mjs +++ b/src/browser/base/zen-components/ZenWelcome.mjs @@ -71,7 +71,7 @@ document.getElementById('zen-welcome-pages').style.display = 'flex'; document.getElementById('zen-welcome-start').remove(); window.maximize(); - animate('#zen-welcome-pages', { opacity: [0, 1] }, { delay: 0.1 }); + animate('#zen-welcome-pages', { opacity: [0, 1] }, { delay: 0.2, duration: 0.2 }); } async fadeInTitles(page) { diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index a44d27339..aa00115d3 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -45,7 +45,12 @@ list-style-image: url('reload.svg') !important; } -.tab-reset-button { +#PanelUI-zen-gradient-generator-color-toggle-algo { + list-style-image: url('palette.svg'); +} + +.tab-reset-button, +#PanelUI-zen-gradient-generator-color-remove { list-style-image: url('unpin.svg') !important; } @@ -87,7 +92,8 @@ list-style-image: url('translations.svg') !important; } -#appMenu-zoom-controls { +#appMenu-zoom-controls, +#PanelUI-zen-gradient-generator-color-add { list-style-image: url('plus.svg') !important; } diff --git a/src/browser/themes/shared/zen-icons/lin/palette.svg b/src/browser/themes/shared/zen-icons/lin/palette.svg index c47d19138..44c6edbf2 100644 --- a/src/browser/themes/shared/zen-icons/lin/palette.svg +++ b/src/browser/themes/shared/zen-icons/lin/palette.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file