--- 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 ``` ```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.