152 lines
4.0 KiB
Markdown
152 lines
4.0 KiB
Markdown
---
|
||
name: motion/magnetic-button
|
||
title: Magnetic Button Effect
|
||
description: Buttons that follow cursor within radius. Creates premium tactile feel.
|
||
tags: [motion, magnetic, button, micro-interaction, premium]
|
||
source: raycast.com, linear.app, brad.co, amppz.com
|
||
---
|
||
|
||
# Magnetic Button Effect
|
||
|
||
## Что это
|
||
Кнопки и элементы, которые притягиваются к курсору мыши в пределах радиуса. Создаёт ощущение тактильности и премиальности.
|
||
|
||
## Параметры
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| **Radius** | 60–100px |
|
||
| **Strength** | 0.1–0.3 |
|
||
| **Easing** | `cubic-bezier(0.25, 0.1, 0.25, 1)` |
|
||
| **Duration** | 0.3s |
|
||
|
||
## Реализация
|
||
|
||
```html
|
||
<button class="magnetic-btn">Hover me</button>
|
||
```
|
||
|
||
```css
|
||
.magnetic-btn {
|
||
padding: 16px 32px;
|
||
background: var(--color-accent);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 12px;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
position: relative;
|
||
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||
}
|
||
```
|
||
|
||
```javascript
|
||
class MagneticButton {
|
||
constructor(element, strength = 0.2, radius = 80) {
|
||
this.el = element;
|
||
this.strength = strength;
|
||
this.radius = radius;
|
||
this.rect = element.getBoundingClientRect();
|
||
this.isHovering = false;
|
||
this.animationId = null;
|
||
this.pos = { x: 0, y: 0 };
|
||
this.target = { x: 0, y: 0 };
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.el.addEventListener('mouseenter', (e) => {
|
||
this.isHovering = true;
|
||
this.rect = this.el.getBoundingClientRect();
|
||
});
|
||
|
||
this.el.addEventListener('mouseleave', () => {
|
||
this.isHovering = false;
|
||
this.target = { x: 0, y: 0 };
|
||
this.animate();
|
||
});
|
||
|
||
this.el.addEventListener('mousemove', (e) => {
|
||
const x = e.clientX - this.rect.left - this.rect.width / 2;
|
||
const y = e.clientY - this.rect.top - this.rect.height / 2;
|
||
this.target = { x: x * this.strength, y: y * this.strength };
|
||
});
|
||
|
||
this.animate();
|
||
}
|
||
|
||
animate() {
|
||
if (this.isHovering) {
|
||
this.pos.x += (this.target.x - this.pos.x) * 0.1;
|
||
this.pos.y += (this.target.y - this.pos.y) * 0.1;
|
||
} else {
|
||
this.pos.x *= 0.9;
|
||
this.pos.y *= 0.9;
|
||
}
|
||
|
||
this.el.style.transform = `translate(${this.pos.x}px, ${this.pos.y}px)`;
|
||
this.animationId = requestAnimationFrame(() => this.animate());
|
||
}
|
||
|
||
destroy() {
|
||
cancelAnimationFrame(this.animationId);
|
||
}
|
||
}
|
||
|
||
// Usage
|
||
document.querySelectorAll('.magnetic-btn').forEach(el => {
|
||
new MagneticButton(el);
|
||
});
|
||
```
|
||
|
||
## Варианты силы притяжения
|
||
|
||
```javascript
|
||
// Слабое (для текста)
|
||
new MagneticButton(el, 0.1, 60);
|
||
|
||
// Среднее (для кнопок)
|
||
new MagneticButton(el, 0.2, 80);
|
||
|
||
// Сильное (для крупных элементов)
|
||
new MagneticButton(el, 0.3, 100);
|
||
```
|
||
|
||
## Anti-patterns
|
||
|
||
| ❌ Плохо | ✅ Хорошо |
|
||
|----------|-----------|
|
||
| `strength: 0.5` — слишком резко | `strength: 0.1–0.3` |
|
||
| Без `mouseleave` сброса | Всегда сбрасывать в 0,0 |
|
||
| Без `requestAnimationFrame` | Использовать RAF |
|
||
| На мобильных без mouse | Проверять `matchMedia('(pointer: fine)')` |
|
||
|
||
## Mobile Fallback
|
||
|
||
```javascript
|
||
if (window.matchMedia('(pointer: fine)').matches) {
|
||
document.querySelectorAll('.magnetic-btn').forEach(el => {
|
||
new MagneticButton(el);
|
||
});
|
||
}
|
||
// На мобильных — просто hover-эффект
|
||
```
|
||
|
||
## CV Service Validation
|
||
|
||
```bash
|
||
# Проверить micro-interactions
|
||
curl -s -X POST http://localhost:8001/design/analyze \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"image_base64": "...", "full_analysis": false}' | python3 -c "
|
||
import sys, json
|
||
d = json.load(sys.stdin)
|
||
motion = d['dimensions'].get('motion_quality', {})
|
||
print(f\"Motion Quality: {motion.get('score', 'N/A')}/10\")
|
||
"
|
||
```
|
||
|
||
Ожидается: `motion_quality` > 6.
|