mirror of
https://github.com/zen-browser/desktop.git
synced 2025-09-05 19:08:18 +00:00
Added new animation when download button is not visible
This commit is contained in:
@@ -198,8 +198,8 @@ pref('zen.splitView.min-resize-width', 7);
|
||||
pref('zen.splitView.rearrange-hover-size', 24);
|
||||
|
||||
// Zen Download Animations
|
||||
pref('zen.animations.download-animation', true);
|
||||
pref('zen.animations.download-animation-duration', 1200); // ms
|
||||
pref('zen.downloads.download-animation', true);
|
||||
pref('zen.downloads.download-animation-duration', 1210); // ms
|
||||
|
||||
// Startup flags
|
||||
pref('zen.startup.smooth-scroll-in-tabs', true);
|
||||
|
@@ -25,6 +25,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-branding.css" />
|
||||
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-welcome.css" />
|
||||
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-media-controls.css" />
|
||||
<link rel="stylesheet" type="text/css" href="chrome://browser/content/zen-styles/zen-download-box-animation.css" />
|
||||
</linkset>
|
||||
|
||||
# Scripts used all over the browser
|
||||
|
@@ -69,8 +69,9 @@
|
||||
content/browser/zen-components/ZenMediaController.mjs (../../zen/media/ZenMediaController.mjs)
|
||||
content/browser/zen-styles/zen-media-controls.css (../../zen/media/zen-media-controls.css)
|
||||
|
||||
content/browser/zen-components/ZenDownloadAnimation.mjs (../../zen/animations/ZenDownloadAnimation.mjs)
|
||||
content/browser/zen-styles/zen-download-animation.css (../../zen/animations/zen-download-animation.css)
|
||||
content/browser/zen-components/ZenDownloadAnimation.mjs (../../zen/downloads/ZenDownloadAnimation.mjs)
|
||||
content/browser/zen-styles/zen-download-arc-animation.css (../../zen/downloads/zen-download-arc-animation.css)
|
||||
content/browser/zen-styles/zen-download-box-animation.css (../../zen/downloads/zen-download-box-animation.css)
|
||||
|
||||
|
||||
# Images
|
||||
@@ -82,7 +83,8 @@
|
||||
content/browser/zen-images/grain-bg.png (../../zen/images/grain-bg.png)
|
||||
content/browser/zen-images/note-indicator.svg (../../zen/images/note-indicator.svg)
|
||||
|
||||
content/browser/zen-images/animations/download.svg (../../zen/images/animations/download.svg)
|
||||
content/browser/zen-images/downloads/download.svg (../../zen/images/downloads/download.svg)
|
||||
content/browser/zen-images/downloads/archive.svg (../../zen/images/downloads/archive.svg)
|
||||
|
||||
# Fonts
|
||||
content/browser/zen-fonts/JunicodeVF-Italic.woff2 (../../zen/fonts/JunicodeVF-Italic.woff2)
|
||||
|
@@ -1,272 +0,0 @@
|
||||
var { Downloads } = ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs');
|
||||
|
||||
{
|
||||
const CONFIG = Object.freeze({
|
||||
ANIMATION: {
|
||||
ARC_STEPS: 60,
|
||||
MAX_ARC_HEIGHT: 800,
|
||||
ARC_HEIGHT_RATIO: 0.8, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
|
||||
SCALE_END: 0.45, // Final scale at destination
|
||||
},
|
||||
});
|
||||
|
||||
class ZenDownloadAnimation extends ZenDOMOperatedFeature {
|
||||
async init() {
|
||||
this._lastClickPosition = null;
|
||||
this._setupClickListener();
|
||||
await this._setupDownloadListeners();
|
||||
}
|
||||
|
||||
_setupClickListener() {
|
||||
document.addEventListener('mousedown', this._handleClick.bind(this), true);
|
||||
}
|
||||
|
||||
_handleClick(event) {
|
||||
this._lastClickPosition = {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
async _setupDownloadListeners() {
|
||||
try {
|
||||
const list = await Downloads.getList(Downloads.ALL);
|
||||
list.addView({
|
||||
onDownloadAdded: this._handleNewDownload.bind(this),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to set up download animation listeners:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
_handleNewDownload() {
|
||||
if (!Services.prefs.getBoolPref('zen.animations.download-animation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._lastClickPosition) {
|
||||
console.warn('No recent click position available for animation');
|
||||
return;
|
||||
}
|
||||
|
||||
this._animateDownload(this._lastClickPosition);
|
||||
}
|
||||
|
||||
_animateDownload(startPosition) {
|
||||
let animationElement = document.querySelector('zen-download-animation');
|
||||
if (!animationElement) {
|
||||
animationElement = document.createElement('zen-download-animation');
|
||||
document.body.appendChild(animationElement);
|
||||
} else {
|
||||
animationElement.initializeAnimation(startPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZenDownloadAnimationElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this._loadStyles();
|
||||
}
|
||||
|
||||
_loadStyles() {
|
||||
const linkElem = document.createElement('link');
|
||||
linkElem.setAttribute('rel', 'stylesheet');
|
||||
linkElem.setAttribute('href', 'chrome://browser/content/zen-styles/zen-download-animation.css');
|
||||
this.shadowRoot.appendChild(linkElem);
|
||||
}
|
||||
|
||||
_createAnimationElement(startPosition) {
|
||||
const animationHTML = `
|
||||
<div class="zen-download-animation">
|
||||
<div class="zen-download-animation-inner-circle">
|
||||
<div class="zen-download-animation-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const fragment = window.MozXULElement.parseXULToFragment(animationHTML);
|
||||
const animationElement = fragment.querySelector('.zen-download-animation');
|
||||
|
||||
animationElement.style.left = `${startPosition.clientX}px`;
|
||||
animationElement.style.top = `${startPosition.clientY}px`;
|
||||
animationElement.style.transform = 'translate(-50%, -50%)';
|
||||
|
||||
this.shadowRoot.appendChild(animationElement);
|
||||
|
||||
return animationElement;
|
||||
}
|
||||
|
||||
initializeAnimation(startPosition) {
|
||||
if (!startPosition) {
|
||||
console.warn('No start position provided, skipping animation');
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadsButton = document.getElementById('downloads-button');
|
||||
|
||||
if (!downloadsButton) {
|
||||
console.warn('Downloads button not found, skipping animation');
|
||||
return;
|
||||
}
|
||||
|
||||
const isVisible = this._isElementVisible(downloadsButton);
|
||||
if (!isVisible) {
|
||||
console.warn('Downloads button is not visible, skipping animation');
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonRect = downloadsButton.getBoundingClientRect();
|
||||
const endPosition = {
|
||||
clientX: buttonRect.left + buttonRect.width / 2,
|
||||
clientY: buttonRect.top + buttonRect.height / 2,
|
||||
};
|
||||
|
||||
const animationElement = this._createAnimationElement(startPosition);
|
||||
const distance = this._calculateDistance(startPosition, endPosition);
|
||||
|
||||
// Determine optimal arc direction based on available space
|
||||
const { arcHeight, shouldArcDownward } = this._calculateOptimalArc(startPosition, endPosition, distance);
|
||||
|
||||
this._runAnimationSequence(animationElement, startPosition, endPosition, arcHeight, shouldArcDownward);
|
||||
}
|
||||
|
||||
_calculateOptimalArc(startPosition, endPosition, distance) {
|
||||
// Calculate available space for the arc
|
||||
const availableTopSpace = Math.min(startPosition.clientY, endPosition.clientY);
|
||||
const viewportHeight = window.innerHeight;
|
||||
const availableBottomSpace = viewportHeight - Math.max(startPosition.clientY, endPosition.clientY);
|
||||
|
||||
// Determine if we should arc downward or upward based on available space
|
||||
const shouldArcDownward = availableBottomSpace > availableTopSpace;
|
||||
|
||||
// Use the space in the direction we're arcing
|
||||
const availableSpace = shouldArcDownward ? availableBottomSpace : availableTopSpace;
|
||||
|
||||
// Limit arc height to a percentage of the available space
|
||||
const arcHeight = Math.min(
|
||||
distance * CONFIG.ANIMATION.ARC_HEIGHT_RATIO,
|
||||
CONFIG.ANIMATION.MAX_ARC_HEIGHT,
|
||||
availableSpace * 0.8
|
||||
);
|
||||
|
||||
return { arcHeight, shouldArcDownward };
|
||||
}
|
||||
|
||||
_calculateDistance(start, end) {
|
||||
const distanceX = end.clientX - start.clientX;
|
||||
const distanceY = end.clientY - start.clientY;
|
||||
return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
|
||||
}
|
||||
|
||||
_runAnimationSequence(element, start, end, arcHeight, shouldArcDownward) {
|
||||
try {
|
||||
const distanceX = end.clientX - start.clientX;
|
||||
const distanceY = end.clientY - start.clientY;
|
||||
|
||||
const arcAnimation = this._createArcAnimation(element, distanceX, distanceY, arcHeight, shouldArcDownward);
|
||||
arcAnimation.onfinish = () => this._cleanupAnimation(element);
|
||||
} catch (error) {
|
||||
console.error('Error in animation sequence:', error);
|
||||
this._cleanupAnimation(element);
|
||||
}
|
||||
}
|
||||
|
||||
_createArcAnimation(element, distanceX, distanceY, arcHeight, shouldArcDownward) {
|
||||
const keyframes = [];
|
||||
const arcDirection = shouldArcDownward ? 1 : -1;
|
||||
const steps = CONFIG.ANIMATION.ARC_STEPS;
|
||||
const endScale = CONFIG.ANIMATION.SCALE_END;
|
||||
|
||||
function easeInOutQuad(t) {
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
}
|
||||
|
||||
let previousRotation = 0;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const progress = i / steps;
|
||||
const eased = easeInOutQuad(progress);
|
||||
|
||||
// Calculate opacity changes
|
||||
let opacity;
|
||||
if (progress < 0.3) {
|
||||
// Fade in during first 30%
|
||||
opacity = 0.3 + (progress / 0.3) * 0.6;
|
||||
} else if (progress < 0.98) {
|
||||
// Slight increase to full opacity
|
||||
opacity = 0.9 + ((progress - 0.3) / 0.6) * 0.1;
|
||||
} else {
|
||||
// Decrease opacity in the final steps
|
||||
opacity = 1 - ((progress - 0.9) / 0.1) * 1;
|
||||
}
|
||||
|
||||
// Calculate scaling changes
|
||||
let scale;
|
||||
if (progress < 0.5) {
|
||||
scale = 0.5 + (progress / 0.5) * 1.3;
|
||||
} else {
|
||||
scale = 1.8 - ((progress - 0.5) / 0.5) * (1.8 - endScale);
|
||||
}
|
||||
|
||||
// Position on arc
|
||||
const x = distanceX * eased;
|
||||
const y = distanceY * eased + arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);
|
||||
|
||||
// Calculate rotation to point in the direction of movement
|
||||
let rotation = previousRotation;
|
||||
if (i > 0) {
|
||||
const prevEased = easeInOutQuad((i - 1) / steps);
|
||||
|
||||
const prevX = distanceX * prevEased;
|
||||
const prevAdjustedProgress = prevEased * 2 - 1;
|
||||
const prevVerticalOffset = arcDirection * arcHeight * (1 - prevAdjustedProgress * 2);
|
||||
const prevY = distanceY * prevEased + prevVerticalOffset;
|
||||
|
||||
const targetRotation = Math.atan2(y - prevY, x - prevX) * (180 / Math.PI);
|
||||
|
||||
rotation += (targetRotation - previousRotation) * 0.01;
|
||||
previousRotation = rotation;
|
||||
}
|
||||
|
||||
keyframes.push({
|
||||
offset: progress,
|
||||
opacity: opacity,
|
||||
transform: `translate(calc(${x}px - 50%), calc(${y}px - 50%)) rotate(${rotation}deg) scale(${scale})`,
|
||||
});
|
||||
}
|
||||
|
||||
return element.animate(keyframes, {
|
||||
duration: Services.prefs.getIntPref('zen.animations.download-animation-duration'),
|
||||
easing: 'cubic-bezier(0.37, 0, 0.63, 1)',
|
||||
fill: 'forwards',
|
||||
});
|
||||
}
|
||||
|
||||
_cleanupAnimation(element) {
|
||||
if (element && element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
} else {
|
||||
console.warn('Error cleaning download animation');
|
||||
}
|
||||
}
|
||||
|
||||
_isElementVisible(element) {
|
||||
if (!element) return false;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// Element must be in the viewport
|
||||
if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight || rect.left > window.innerWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('zen-download-animation', ZenDownloadAnimationElement);
|
||||
|
||||
new ZenDownloadAnimation().init().catch(console.error);
|
||||
}
|
460
src/zen/downloads/ZenDownloadAnimation.mjs
Normal file
460
src/zen/downloads/ZenDownloadAnimation.mjs
Normal file
@@ -0,0 +1,460 @@
|
||||
{
|
||||
const { Downloads } = ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs');
|
||||
|
||||
const CONFIG = Object.freeze({
|
||||
ANIMATION: {
|
||||
ARC_STEPS: 60,
|
||||
MAX_ARC_HEIGHT: 1200,
|
||||
ARC_HEIGHT_RATIO: 0.8, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
|
||||
SCALE_END: 0.45, // Final scale at destination
|
||||
},
|
||||
});
|
||||
|
||||
class ZenDownloadAnimation extends ZenDOMOperatedFeature {
|
||||
#lastClickPosition = null;
|
||||
|
||||
async init() {
|
||||
this.#setupClickListener();
|
||||
await this.#setupDownloadListeners();
|
||||
}
|
||||
|
||||
#setupClickListener() {
|
||||
document.addEventListener('mousedown', this.#handleClick.bind(this), true);
|
||||
}
|
||||
|
||||
#handleClick(event) {
|
||||
this.#lastClickPosition = {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
async #setupDownloadListeners() {
|
||||
try {
|
||||
const list = await Downloads.getList(Downloads.ALL);
|
||||
list.addView({
|
||||
onDownloadAdded: this.#handleNewDownload.bind(this),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[${ZenDownloadAnimation.name}] Failed to set up download animation listeners: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
#handleNewDownload() {
|
||||
if (!Services.prefs.getBoolPref('zen.downloads.download-animation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#lastClickPosition) {
|
||||
console.warn(`[${ZenDownloadAnimation.name}] No recent click position available for animation`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#animateDownload(this.#lastClickPosition);
|
||||
}
|
||||
|
||||
#animateDownload(startPosition) {
|
||||
let animationElement = document.querySelector('zen-download-animation');
|
||||
|
||||
if (!animationElement) {
|
||||
animationElement = document.createElement('zen-download-animation');
|
||||
document.body.appendChild(animationElement);
|
||||
}
|
||||
|
||||
animationElement.initializeAnimation(startPosition);
|
||||
}
|
||||
}
|
||||
|
||||
class ZenDownloadAnimationElement extends HTMLElement {
|
||||
#boxAnimationElement = null;
|
||||
#boxAnimationTimeoutId = null;
|
||||
#isBoxAnimationRunning = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.#loadArcStyles();
|
||||
}
|
||||
|
||||
#loadArcStyles() {
|
||||
try {
|
||||
const link = document.createElement('link');
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.setAttribute('href', 'chrome://browser/content/zen-styles/zen-download-arc-animation.css');
|
||||
this.shadowRoot.appendChild(link);
|
||||
} catch (error) {
|
||||
console.error(`[${ZenDownloadAnimationElement.name}] Error loading arc styles: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async initializeAnimation(startPosition) {
|
||||
if (!startPosition) {
|
||||
console.warn(`[${ZenDownloadAnimationElement.name}] No start position provided, skipping animation`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine animation target position
|
||||
const { endPosition, isDownloadButtonVisible } = this.#determineEndPosition();
|
||||
const areTabsPositionedRight = this.#areTabsOnRightSide();
|
||||
|
||||
// Create and prepare the arc animation element
|
||||
const arcAnimationElement = this.#createArcAnimationElement(startPosition);
|
||||
|
||||
// Calculate optimal arc parameters based on available space
|
||||
const distance = this.#calculateDistance(startPosition, endPosition);
|
||||
const { arcHeight, shouldArcDownward } = this.#calculateOptimalArc(startPosition, endPosition, distance);
|
||||
const distanceX = endPosition.clientX - startPosition.clientX;
|
||||
const distanceY = endPosition.clientY - startPosition.clientY;
|
||||
const arcSequence = this.#createArcAnimationSequence(distanceX, distanceY, arcHeight, shouldArcDownward);
|
||||
|
||||
// Start the download animation
|
||||
await this.#startDownloadAnimation(areTabsPositionedRight, isDownloadButtonVisible, arcAnimationElement, arcSequence);
|
||||
}
|
||||
|
||||
#areTabsOnRightSide() {
|
||||
return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
|
||||
}
|
||||
|
||||
#determineEndPosition() {
|
||||
const downloadsButton = document.getElementById('downloads-button');
|
||||
const isDownloadButtonVisible = downloadsButton && this.#isElementVisible(downloadsButton);
|
||||
|
||||
let endPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
if (isDownloadButtonVisible) {
|
||||
// Use download button as target
|
||||
const buttonRect = downloadsButton.getBoundingClientRect();
|
||||
endPosition = {
|
||||
clientX: buttonRect.left + buttonRect.width / 2,
|
||||
clientY: buttonRect.top + buttonRect.height / 2,
|
||||
};
|
||||
} else {
|
||||
// Use alternative position at bottom of wrapper
|
||||
const areTabsPositionedRight = this.#areTabsOnRightSide();
|
||||
const wrapper = document.getElementById('zen-main-app-wrapper');
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
|
||||
endPosition = {
|
||||
clientX: areTabsPositionedRight ? wrapperRect.right - 42 : wrapperRect.left + 42,
|
||||
clientY: wrapperRect.bottom - 40,
|
||||
};
|
||||
}
|
||||
|
||||
return { endPosition, isDownloadButtonVisible };
|
||||
}
|
||||
|
||||
#createArcAnimationElement(startPosition) {
|
||||
const arcAnimationHTML = `
|
||||
<div class="zen-download-arc-animation">
|
||||
<div class="zen-download-arc-animation-inner-circle">
|
||||
<div class="zen-download-arc-animation-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const fragment = window.MozXULElement.parseXULToFragment(arcAnimationHTML);
|
||||
const animationElement = fragment.querySelector('.zen-download-arc-animation');
|
||||
|
||||
Object.assign(animationElement.style, {
|
||||
left: `${startPosition.clientX}px`,
|
||||
top: `${startPosition.clientY}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
});
|
||||
|
||||
this.shadowRoot.appendChild(animationElement);
|
||||
|
||||
return animationElement;
|
||||
}
|
||||
|
||||
#calculateOptimalArc(startPosition, endPosition, distance) {
|
||||
// Calculate available space for the arc
|
||||
const availableTopSpace = Math.min(startPosition.clientY, endPosition.clientY);
|
||||
const viewportHeight = window.innerHeight;
|
||||
const availableBottomSpace = viewportHeight - Math.max(startPosition.clientY, endPosition.clientY);
|
||||
|
||||
// Determine if we should arc downward or upward based on available space
|
||||
const shouldArcDownward = availableBottomSpace > availableTopSpace;
|
||||
|
||||
// Use the space in the direction we're arcing
|
||||
const availableSpace = shouldArcDownward ? availableBottomSpace : availableTopSpace;
|
||||
|
||||
// Limit arc height to a percentage of the available space
|
||||
const arcHeight = Math.min(
|
||||
distance * CONFIG.ANIMATION.ARC_HEIGHT_RATIO,
|
||||
CONFIG.ANIMATION.MAX_ARC_HEIGHT,
|
||||
availableSpace * 0.8
|
||||
);
|
||||
|
||||
return { arcHeight, shouldArcDownward };
|
||||
}
|
||||
|
||||
#calculateDistance(start, end) {
|
||||
const distanceX = end.clientX - start.clientX;
|
||||
const distanceY = end.clientY - start.clientY;
|
||||
return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
|
||||
}
|
||||
|
||||
async #startDownloadAnimation(areTabsPositionedRight, isDownloadButtonVisible, arcAnimationElement, sequence) {
|
||||
try {
|
||||
if (!isDownloadButtonVisible) {
|
||||
this.#startBoxAnimation(areTabsPositionedRight);
|
||||
}
|
||||
|
||||
const arcAnimation = await gZenUIManager.motion.animate(arcAnimationElement, sequence, {
|
||||
duration: Services.prefs.getIntPref('zen.downloads.download-animation-duration') / 1000,
|
||||
easing: 'cubic-bezier(0.37, 0, 0.63, 1)',
|
||||
fill: 'forwards',
|
||||
});
|
||||
|
||||
arcAnimation.onfinish = () => this.#cleanArcAnimation(arcAnimationElement);
|
||||
} catch (error) {
|
||||
console.error('[ZenDownloadAnimationElement] Error in animation sequence:', error);
|
||||
this.#cleanArcAnimation(arcAnimationElement);
|
||||
}
|
||||
}
|
||||
|
||||
#createArcAnimationSequence(distanceX, distanceY, arcHeight, shouldArcDownward) {
|
||||
const sequence = { offset: [], opacity: [], transform: [] };
|
||||
|
||||
const arcDirection = shouldArcDownward ? 1 : -1;
|
||||
const steps = CONFIG.ANIMATION.ARC_STEPS;
|
||||
const endScale = CONFIG.ANIMATION.SCALE_END;
|
||||
|
||||
function easeInOutQuad(t) {
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
}
|
||||
|
||||
let previousRotation = 0;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const progress = i / steps;
|
||||
const eased = easeInOutQuad(progress);
|
||||
|
||||
// Calculate opacity changes
|
||||
let opacity;
|
||||
if (progress < 0.3) {
|
||||
// Fade in during first 30%
|
||||
opacity = 0.3 + (progress / 0.3) * 0.6;
|
||||
} else if (progress < 0.98) {
|
||||
// Slight increase to full opacity
|
||||
opacity = 0.9 + ((progress - 0.3) / 0.6) * 0.1;
|
||||
} else {
|
||||
// Decrease opacity in the final steps
|
||||
opacity = 1 - ((progress - 0.9) / 0.1) * 1;
|
||||
}
|
||||
|
||||
// Calculate scaling changes
|
||||
let scale;
|
||||
if (progress < 0.5) {
|
||||
scale = 0.5 + (progress / 0.5) * 1.3;
|
||||
} else {
|
||||
scale = 1.8 - ((progress - 0.5) / 0.5) * (1.8 - endScale);
|
||||
}
|
||||
|
||||
// Position on arc
|
||||
const x = distanceX * eased;
|
||||
const y = distanceY * eased + arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);
|
||||
|
||||
// Calculate rotation to point in the direction of movement
|
||||
let rotation = previousRotation;
|
||||
if (i > 0) {
|
||||
const prevEased = easeInOutQuad((i - 1) / steps);
|
||||
|
||||
const prevX = distanceX * prevEased;
|
||||
const prevAdjustedProgress = prevEased * 2 - 1;
|
||||
const prevVerticalOffset = arcDirection * arcHeight * (1 - prevAdjustedProgress * 2);
|
||||
const prevY = distanceY * prevEased + prevVerticalOffset;
|
||||
|
||||
const targetRotation = Math.atan2(y - prevY, x - prevX) * (180 / Math.PI);
|
||||
|
||||
rotation += (targetRotation - previousRotation) * 0.01;
|
||||
previousRotation = rotation;
|
||||
}
|
||||
|
||||
sequence.offset.push(progress);
|
||||
sequence.opacity.push(opacity);
|
||||
sequence.transform.push(`translate(calc(${x}px - 50%), calc(${y}px - 50%)) rotate(${rotation}deg) scale(${scale})`);
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
#cleanArcAnimation(element) {
|
||||
if (element && element.parentNode) {
|
||||
element.remove();
|
||||
} else {
|
||||
console.warn(`[${ZenDownloadAnimationElement.name}] Error cleaning download animation`);
|
||||
}
|
||||
}
|
||||
|
||||
async #startBoxAnimation(areTabsPositionedRight) {
|
||||
// If animation is already in progress, don't start a new one
|
||||
if (this.#isBoxAnimationRunning) {
|
||||
console.warn(`[${ZenDownloadAnimationElement.name}] Box animation already running, skipping new request.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#boxAnimationElement) {
|
||||
clearTimeout(this.#boxAnimationTimeoutId);
|
||||
this.#boxAnimationTimeoutId = setTimeout(
|
||||
() => this.#finishBoxAnimation(areTabsPositionedRight),
|
||||
this.#getBoxAnimationDurationMs()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = document.getElementById('zen-main-app-wrapper');
|
||||
if (!wrapper) {
|
||||
console.warn(`[${ZenDownloadAnimationElement.name}] Cannot start box animation, Wrapper element not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#isBoxAnimationRunning = true;
|
||||
|
||||
try {
|
||||
const boxAnimationHTML = `
|
||||
<div class="zen-download-box-animation">
|
||||
<div class="zen-download-box-animation-icon"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const sideProp = areTabsPositionedRight ? 'right' : 'left';
|
||||
const startSideValue = areTabsPositionedRight ? '50px' : '-50px';
|
||||
|
||||
const fragment = window.MozXULElement.parseXULToFragment(boxAnimationHTML);
|
||||
this.#boxAnimationElement = fragment.querySelector('.zen-download-box-animation');
|
||||
|
||||
Object.assign(this.#boxAnimationElement.style, {
|
||||
bottom: '24px',
|
||||
transform: 'scale(0.8)',
|
||||
[sideProp]: startSideValue,
|
||||
});
|
||||
|
||||
wrapper.appendChild(this.#boxAnimationElement);
|
||||
|
||||
await gZenUIManager.motion.animate(
|
||||
this.#boxAnimationElement,
|
||||
{
|
||||
[sideProp]: '34px',
|
||||
opacity: 1,
|
||||
transform: 'scale(1.1)',
|
||||
},
|
||||
{
|
||||
duration: 0.35,
|
||||
easing: 'ease-out',
|
||||
}
|
||||
).finished;
|
||||
|
||||
await gZenUIManager.motion.animate(
|
||||
this.#boxAnimationElement,
|
||||
{
|
||||
[sideProp]: '24px',
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
{
|
||||
duration: 0.2,
|
||||
easing: 'ease-in-out',
|
||||
}
|
||||
).finished;
|
||||
|
||||
clearTimeout(this.#boxAnimationTimeoutId);
|
||||
this.#boxAnimationTimeoutId = setTimeout(
|
||||
() => this.#finishBoxAnimation(areTabsPositionedRight),
|
||||
this.#getBoxAnimationDurationMs()
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[${ZenDownloadAnimationElement.name}] Error during box entry animation: ${error}`);
|
||||
this.#cleanBoxAnimation();
|
||||
} finally {
|
||||
this.#isBoxAnimationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
#getBoxAnimationDurationMs() {
|
||||
return Services.prefs.getIntPref('zen.downloads.download-animation-duration') + 200;
|
||||
}
|
||||
|
||||
async #finishBoxAnimation(areTabsPositionedRight) {
|
||||
clearTimeout(this.#boxAnimationTimeoutId);
|
||||
this.#boxAnimationTimeoutId = null;
|
||||
|
||||
if (!this.#boxAnimationElement || this.#isBoxAnimationRunning) {
|
||||
if (!this.#boxAnimationElement) this.#cleanBoxAnimationState();
|
||||
return;
|
||||
}
|
||||
|
||||
this.#isBoxAnimationRunning = true;
|
||||
|
||||
try {
|
||||
const sideProp = areTabsPositionedRight ? 'right' : 'left';
|
||||
|
||||
await gZenUIManager.motion.animate(
|
||||
this.#boxAnimationElement,
|
||||
{
|
||||
transform: 'scale(0.9)',
|
||||
},
|
||||
{
|
||||
duration: 0.15,
|
||||
easing: 'ease-in',
|
||||
}
|
||||
).finished;
|
||||
|
||||
await gZenUIManager.motion.animate(
|
||||
this.#boxAnimationElement,
|
||||
{
|
||||
[sideProp]: '-50px',
|
||||
opacity: 0,
|
||||
transform: 'scale(0.8)',
|
||||
},
|
||||
{
|
||||
duration: 0.3,
|
||||
easing: 'cubic-bezier(0.5, 0, 0.75, 0)',
|
||||
}
|
||||
).finished;
|
||||
} catch (error) {
|
||||
console.warn(`[${ZenDownloadAnimationElement.name}] Error during box exit animation: ${error}`);
|
||||
} finally {
|
||||
this.#cleanBoxAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
#cleanBoxAnimationState() {
|
||||
this.#boxAnimationElement = null;
|
||||
if (this.#boxAnimationTimeoutId) {
|
||||
clearTimeout(this.#boxAnimationTimeoutId);
|
||||
this.#boxAnimationTimeoutId = null;
|
||||
}
|
||||
this.#isBoxAnimationRunning = false;
|
||||
}
|
||||
|
||||
#cleanBoxAnimation() {
|
||||
if (this.#boxAnimationElement && this.#boxAnimationElement.isConnected) {
|
||||
try {
|
||||
this.#boxAnimationElement.remove();
|
||||
} catch (error) {
|
||||
console.error(`[${ZenDownloadAnimationElement.name}] Error removing box animation element: ${error}`, error);
|
||||
}
|
||||
}
|
||||
this.#cleanBoxAnimationState();
|
||||
}
|
||||
|
||||
#isElementVisible(element) {
|
||||
if (!element) return false;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// Element must be in the viewport
|
||||
// Is 1 and no 0 because if you pin the download button in the overflow menu
|
||||
// the download button is in the viewport but in the position 0,0 so this
|
||||
// avoid this case
|
||||
if (rect.bottom < 1 || rect.right < 1 || rect.top > window.innerHeight || rect.left > window.innerWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('zen-download-animation', ZenDownloadAnimationElement);
|
||||
|
||||
new ZenDownloadAnimation();
|
||||
}
|
@@ -5,10 +5,10 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.zen-download-animation {
|
||||
.zen-download-arc-animation {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -18,25 +18,26 @@
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
background-color: var(--zen-primary-color);
|
||||
box-shadow: var(--zen-big-shadow);
|
||||
}
|
||||
|
||||
.zen-download-animation-inner-circle {
|
||||
.zen-download-arc-animation-inner-circle {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: black;
|
||||
background-color: var(--zen-colors-secondary);
|
||||
}
|
||||
|
||||
.zen-download-animation-icon {
|
||||
.zen-download-arc-animation-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--zen-primary-color);
|
||||
-webkit-mask: url('chrome://browser/content/zen-images/animations/download.svg') no-repeat center;
|
||||
-webkit-mask: url('chrome://browser/content/zen-images/downloads/download.svg') no-repeat center;
|
||||
-webkit-mask-size: 70%;
|
||||
mask: url('chrome://browser/content/zen-images/animations/download.svg') no-repeat center;
|
||||
mask: url('chrome://browser/content/zen-images/downloads/download.svg') no-repeat center;
|
||||
mask-size: 70%;
|
||||
}
|
23
src/zen/downloads/zen-download-box-animation.css
Normal file
23
src/zen/downloads/zen-download-box-animation.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.zen-download-box-animation {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--zen-colors-secondary);
|
||||
border-radius: var(--zen-border-radius);
|
||||
box-shadow: var(--zen-big-shadow);
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.zen-download-box-animation-icon {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background-color: var(--zen-primary-color);
|
||||
-webkit-mask: url('chrome://browser/content/zen-images/downloads/archive.svg') no-repeat center;
|
||||
-webkit-mask-size: contain;
|
||||
mask: url('chrome://browser/content/zen-images/downloads/archive.svg') no-repeat center;
|
||||
mask-size: contain;
|
||||
display: block;
|
||||
}
|
7
src/zen/images/downloads/archive.svg
Normal file
7
src/zen/images/downloads/archive.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-archive-icon lucide-archive">
|
||||
<rect width="20" height="5" x="2" y="3" rx="1" />
|
||||
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
|
||||
<path d="M10 12h4" />
|
||||
</svg>
|
After Width: | Height: | Size: 384 B |
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 305 B |
Reference in New Issue
Block a user