mirror of
https://github.com/zen-browser/desktop.git
synced 2025-10-15 22:36:23 +00:00
Improved download animation with better animation and better code, added custom prefs
This commit is contained in:
@@ -197,6 +197,10 @@ pref('zen.splitView.enable-tab-drop', true);
|
|||||||
pref('zen.splitView.min-resize-width', 7);
|
pref('zen.splitView.min-resize-width', 7);
|
||||||
pref('zen.splitView.rearrange-hover-size', 24);
|
pref('zen.splitView.rearrange-hover-size', 24);
|
||||||
|
|
||||||
|
// Zen Download Animations
|
||||||
|
pref('zen.animations.download-animation', true);
|
||||||
|
pref('zen.animations.download-animation-duration', 1200); // ms
|
||||||
|
|
||||||
// Startup flags
|
// Startup flags
|
||||||
pref('zen.startup.smooth-scroll-in-tabs', true);
|
pref('zen.startup.smooth-scroll-in-tabs', true);
|
||||||
|
|
||||||
|
@@ -69,7 +69,9 @@
|
|||||||
content/browser/zen-components/ZenMediaController.mjs (../../zen/media/ZenMediaController.mjs)
|
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-styles/zen-media-controls.css (../../zen/media/zen-media-controls.css)
|
||||||
|
|
||||||
content/browser/zen-components/ZenDownloadAnimation.mjs (../../zen/downloads/ZenDownloadAnimation.mjs)
|
content/browser/zen-components/ZenDownloadAnimation.mjs (../../zen/animations/ZenDownloadAnimation.mjs)
|
||||||
|
content/browser/zen-styles/zen-download-animation.css (../../zen/animations/zen-download-animation.css)
|
||||||
|
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
content/browser/zen-images/gradient.png (../../zen/images/gradient.png)
|
content/browser/zen-images/gradient.png (../../zen/images/gradient.png)
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
content/browser/zen-images/grain-bg.png (../../zen/images/grain-bg.png)
|
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/note-indicator.svg (../../zen/images/note-indicator.svg)
|
||||||
|
|
||||||
content/browser/zen-images/downloads/download.svg (../../zen/images/downloads/download.svg)
|
content/browser/zen-images/animations/download.svg (../../zen/images/animations/download.svg)
|
||||||
|
|
||||||
# Fonts
|
# Fonts
|
||||||
content/browser/zen-fonts/JunicodeVF-Italic.woff2 (../../zen/fonts/JunicodeVF-Italic.woff2)
|
content/browser/zen-fonts/JunicodeVF-Italic.woff2 (../../zen/fonts/JunicodeVF-Italic.woff2)
|
||||||
|
272
src/zen/animations/ZenDownloadAnimation.mjs
Normal file
272
src/zen/animations/ZenDownloadAnimation.mjs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
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);
|
||||||
|
}
|
42
src/zen/animations/zen-download-animation.css
Normal file
42
src/zen/animations/zen-download-animation.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-download-animation {
|
||||||
|
position: absolute;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: var(--zen-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-download-animation-inner-circle {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-download-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-size: 70%;
|
||||||
|
mask: url('chrome://browser/content/zen-images/animations/download.svg') no-repeat center;
|
||||||
|
mask-size: 70%;
|
||||||
|
}
|
@@ -1,290 +0,0 @@
|
|||||||
var { Downloads } = ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs');
|
|
||||||
|
|
||||||
{
|
|
||||||
const CONFIG = Object.freeze({
|
|
||||||
ANIMATION: {
|
|
||||||
FADE_DURATION: 300,
|
|
||||||
ARC_STEPS: 60,
|
|
||||||
DURATION: 1500,
|
|
||||||
MAX_ARC_HEIGHT: 500,
|
|
||||||
ARC_HEIGHT_RATIO: 0.8, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
|
|
||||||
SCALE_END: 0.5, // Final scale at destination
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class ZenDownloadAnimation extends ZenDOMOperatedFeature {
|
|
||||||
async init() {
|
|
||||||
this._lastClickPosition = null;
|
|
||||||
this._lastClickTime = 0;
|
|
||||||
this._setupClickListener();
|
|
||||||
await this._setupDownloadListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupClickListener() {
|
|
||||||
document.addEventListener(
|
|
||||||
'mousedown',
|
|
||||||
(event) => {
|
|
||||||
this._lastClickPosition = {
|
|
||||||
clientX: event.clientX,
|
|
||||||
clientY: event.clientY,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getLastClickPosition() {
|
|
||||||
if (this._lastClickPosition) {
|
|
||||||
return this._lastClickPosition;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _setupDownloadListeners() {
|
|
||||||
try {
|
|
||||||
const list = await Downloads.getList(Downloads.ALL);
|
|
||||||
list.addView({
|
|
||||||
onDownloadAdded: () => {
|
|
||||||
this._handleNewDownload();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to set up download listeners:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_animateDownload(startPosition) {
|
|
||||||
const animationElement = document.querySelector('zen-download-animation');
|
|
||||||
if (animationElement) {
|
|
||||||
animationElement.initializeAnimation(startPosition);
|
|
||||||
} else {
|
|
||||||
if (!document.querySelector('zen-download-animation')) {
|
|
||||||
const downloadAnimation = document.createElement('zen-download-animation');
|
|
||||||
document.body.appendChild(downloadAnimation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleNewDownload() {
|
|
||||||
const clickPosition = this._getLastClickPosition();
|
|
||||||
|
|
||||||
if (!clickPosition) {
|
|
||||||
console.warn('No recent click position available for animation');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._animateDownload(clickPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ZenDownloadAnimationElement extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
this._createStyles();
|
|
||||||
}
|
|
||||||
_createStyles() {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
:host {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
.download-animation {
|
|
||||||
position: absolute;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 4px;
|
|
||||||
background-color: var(--zen-primary-color);
|
|
||||||
}
|
|
||||||
.download-animation-inner-circle {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
.download-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/downloads/download.svg") no-repeat center;
|
|
||||||
-webkit-mask-size: 70%;
|
|
||||||
mask: url("chrome://browser/content/zen-images/downloads/download.svg") no-repeat center;
|
|
||||||
mask-size: 70%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
this.shadowRoot.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
_createAnimationElement(startPosition) {
|
|
||||||
const animationElement = document.createElement('div');
|
|
||||||
animationElement.className = 'download-animation';
|
|
||||||
animationElement.style.position = 'absolute';
|
|
||||||
animationElement.style.left = `${startPosition.clientX}px`;
|
|
||||||
animationElement.style.top = `${startPosition.clientY}px`;
|
|
||||||
animationElement.style.transform = 'translate(-50%, -50%)';
|
|
||||||
|
|
||||||
const innerCircle = document.createElement('div');
|
|
||||||
innerCircle.className = 'download-animation-inner-circle';
|
|
||||||
|
|
||||||
const icon = document.createElement('div');
|
|
||||||
icon.className = 'download-animation-icon';
|
|
||||||
|
|
||||||
innerCircle.appendChild(icon);
|
|
||||||
animationElement.appendChild(innerCircle);
|
|
||||||
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 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);
|
|
||||||
const arcHeight = Math.min(distance * CONFIG.ANIMATION.ARC_HEIGHT_RATIO, CONFIG.ANIMATION.MAX_ARC_HEIGHT);
|
|
||||||
|
|
||||||
this._runAnimationSequence(animationElement, startPosition, endPosition, arcHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
_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) {
|
|
||||||
try {
|
|
||||||
const distanceX = end.clientX - start.clientX;
|
|
||||||
const distanceY = end.clientY - start.clientY;
|
|
||||||
|
|
||||||
this._runAnimation(element, distanceX, distanceY, arcHeight);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in animation sequence:', error);
|
|
||||||
this._cleanupAnimation(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_runAnimation(element, distanceX, distanceY, arcHeight) {
|
|
||||||
this._createArcAnimation(element, distanceX, distanceY, arcHeight).onfinish = () => {
|
|
||||||
this._fadeOutAnimation(element);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_createArcAnimation(element, distanceX, distanceY, arcHeight) {
|
|
||||||
const keyframes = [];
|
|
||||||
const steps = CONFIG.ANIMATION.ARC_STEPS;
|
|
||||||
const endScale = CONFIG.ANIMATION.SCALE_END;
|
|
||||||
|
|
||||||
const opacityValues = [];
|
|
||||||
const scaleValues = [];
|
|
||||||
|
|
||||||
for (let i = 0; i <= steps; i++) {
|
|
||||||
const progress = i / steps;
|
|
||||||
|
|
||||||
let opacity;
|
|
||||||
if (progress < 0.3) {
|
|
||||||
opacity = 0.3 + (progress / 0.3) * 0.6;
|
|
||||||
} else if (progress < 0.5) {
|
|
||||||
opacity = 0.9 + ((progress - 0.3) / 0.2) * 0.1;
|
|
||||||
} else {
|
|
||||||
opacity = 1;
|
|
||||||
}
|
|
||||||
opacityValues.push(opacity);
|
|
||||||
|
|
||||||
let scale;
|
|
||||||
if (progress < 0.3) {
|
|
||||||
scale = 0.5 + (progress / 0.3) * 0.5;
|
|
||||||
} else if (progress < 0.5) {
|
|
||||||
scale = 1 + ((progress - 0.3) / 0.2) * 0.05;
|
|
||||||
} else {
|
|
||||||
scale = 1.05 - ((progress - 0.5) / 0.5) * (1.05 - endScale);
|
|
||||||
}
|
|
||||||
scaleValues.push(scale);
|
|
||||||
|
|
||||||
const x = distanceX * progress;
|
|
||||||
|
|
||||||
const adjustedProgress = progress * 2 - 1; // -1 to 1
|
|
||||||
const verticalOffset = -arcHeight * (1 - adjustedProgress * adjustedProgress);
|
|
||||||
const y = distanceY * progress + verticalOffset;
|
|
||||||
|
|
||||||
let rotation = 0;
|
|
||||||
let previousRotation = 0;
|
|
||||||
|
|
||||||
if (i > 0 && i < steps) {
|
|
||||||
const prevProgress = (i - 1) / steps;
|
|
||||||
const prevX = distanceX * prevProgress;
|
|
||||||
const prevAdjustedProgress = prevProgress * 2 - 1;
|
|
||||||
const prevVerticalOffset = -arcHeight * (1 - prevAdjustedProgress * prevAdjustedProgress);
|
|
||||||
const prevY = distanceY * prevProgress + prevVerticalOffset;
|
|
||||||
|
|
||||||
const targetRotation = Math.atan2(y - prevY, x - prevX) * (180 / Math.PI);
|
|
||||||
|
|
||||||
rotation = previousRotation + (targetRotation - previousRotation) * 0.1;
|
|
||||||
|
|
||||||
previousRotation = rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
keyframes.push({
|
|
||||||
offset: progress,
|
|
||||||
opacity: opacityValues[i],
|
|
||||||
transform: `translate(calc(${x}px - 50%), calc(${y}px - 50%)) rotate(${rotation}deg) scale(${scaleValues[i]})`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return element.animate(keyframes, {
|
|
||||||
duration: CONFIG.ANIMATION.DURATION,
|
|
||||||
easing: 'cubic-bezier(0.37, 0, 0.63, 1)',
|
|
||||||
fill: 'forwards',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_fadeOutAnimation(element) {
|
|
||||||
element.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
||||||
duration: CONFIG.ANIMATION.FADE_DURATION,
|
|
||||||
fill: 'forwards',
|
|
||||||
}).onfinish = () => this._cleanupAnimation(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
_cleanupAnimation(element) {
|
|
||||||
if (element && element.parentNode) {
|
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('zen-download-animation', ZenDownloadAnimationElement);
|
|
||||||
|
|
||||||
const zenDownloadAnimation = new ZenDownloadAnimation();
|
|
||||||
zenDownloadAnimation.init().catch(console.error);
|
|
||||||
}
|
|
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
stroke="#000000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"
|
||||||
class="lucide lucide-arrow-down-icon lucide-arrow-down">
|
class="lucide lucide-arrow-down-icon lucide-arrow-down">
|
||||||
<path d="M12 5v14" />
|
<path d="M12 5v14" />
|
||||||
<path d="m19 12-7 7-7-7" />
|
<path d="m19 12-7 7-7-7" />
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 305 B |
Reference in New Issue
Block a user