Last active 3 weeks ago

Revision f814cd3a4b1197c39bc15fa958c39bb0aa5b1921

book_anim.html Raw
1<!DOCTYPE html>
2<html lang="ru">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Книга - SVG Анимация</title>
7 <style>
8 body {
9 margin: 0;
10 display: flex;
11 justify-content: center;
12 align-items: center;
13 min-height: 100vh;
14 background: #fff;
15 }
16 </style>
17</head>
18<body>
19<svg id="bookSvg" width="400" height="400" viewBox="0 0 400 400"></svg>
20
21<script>
22 const svg = document.getElementById('bookSvg');
23 const NS = 'http://www.w3.org/2000/svg';
24
25 const svgWidth = 400;
26 const svgHeight = 400;
27
28 // === НАСТРОЙКИ ===
29 const lineCount = 5;
30 const lineDelay = 16;
31 // =================
32
33 // Книга по центру
34 const bookWidth = 240;
35 const bookHeight = 360;
36 const bookX = (svgWidth - bookWidth) / 2;
37 const bookY = (svgHeight - bookHeight) / 2;
38
39 // Параметры полос (страниц)
40 const desiredLineSpacing = 12;
41 const baseLineWidth = 2;
42 const maxLineWidth = 14;
43 const maxLineHeight = bookHeight - 36;
44 const innerPaddingX = 14;
45
46 // Задержка между появлением/исчезновением полос
47 const appearStagger = 8;
48
49 // Вычисляем реальное расстояние между полосами
50 let lineSpacing;
51 if (lineCount > 1) {
52 const maxSpan = Math.max(0, bookWidth - maxLineWidth - 2 * innerPaddingX);
53 const wishSpan = desiredLineSpacing * (lineCount - 1);
54 const realSpan = Math.min(wishSpan, maxSpan);
55 lineSpacing = realSpan / (lineCount - 1);
56 } else {
57 lineSpacing = 0;
58 }
59 const linesSpan = lineSpacing * (lineCount - 1);
60
61 // Правая группа (стартовая позиция)
62 const rightBase = bookX + bookWidth - innerPaddingX - maxLineWidth;
63 const lineStartX = rightBase - linesSpan;
64
65 // Левая группа (финишная позиция)
66 const leftLimit = bookX + innerPaddingX;
67
68 // Состояние анимации
69 let phase = 0;
70 let time = 0;
71
72 // Тайминги (в кадрах)
73 const baseAppearDuration = 40;
74 const appearDuration = baseAppearDuration + (lineCount - 1) * appearStagger;
75
76 const baseFlipDuration = 120;
77 const flipDuration = baseFlipDuration + (lineCount - 1) * lineDelay;
78
79 const baseDisappearDuration = 40;
80 const disappearDuration = baseDisappearDuration + (lineCount - 1) * appearStagger;
81
82 const pauseDuration = 30;
83
84 // Книга
85 const book = document.createElementNS(NS, 'rect');
86 book.setAttribute('x', bookX);
87 book.setAttribute('y', bookY);
88 book.setAttribute('width', bookWidth);
89 book.setAttribute('height', bookHeight);
90 book.setAttribute('fill', '#000');
91 book.setAttribute('rx', '4');
92 svg.appendChild(book);
93
94 // Полосы (страницы)
95 const lines = [];
96 for (let i = 0; i < lineCount; i++) {
97 const line = document.createElementNS(NS, 'rect');
98 line.setAttribute('fill', '#fff');
99 line.setAttribute('rx', '1');
100 svg.appendChild(line);
101
102 const baseX = lineStartX + i * lineSpacing;
103 const targetX = leftLimit + i * lineSpacing;
104 const moveDistance = baseX - targetX;
105
106 lines.push({
107 el: line,
108 baseX,
109 targetX,
110 moveDistance,
111 currentX: baseX,
112 width: baseLineWidth,
113 height: 0
114 });
115 }
116
117 // Функции плавности
118 function easeInOutQuad(t) {
119 return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
120 }
121 function easeOutQuad(t) {
122 return 1 - (1 - t) * (1 - t);
123 }
124 function easeInQuad(t) {
125 return t * t;
126 }
127
128 // Обновление одной полосы
129 function updateLine(line) {
130 const el = line.el;
131 const centerY = bookY + bookHeight / 2;
132
133 el.setAttribute('x', line.currentX);
134 el.setAttribute('y', centerY - line.height / 2);
135 el.setAttribute('width', line.width);
136 el.setAttribute('height', Math.max(0, line.height));
137 }
138
139 // Главный цикл анимации
140 function animate() {
141 time++;
142
143 if (phase === 0) {
144 // Появление: слева направо (индекс 0 первый, потом 1, 2...)
145 for (let i = 0; i < lineCount; i++) {
146 // Задержка увеличивается с индексом → левые появляются раньше
147 const delay = (lineCount - 1 - i) * appearStagger;
148 const localTime = Math.max(0, time - delay);
149 const progress = Math.min(1, localTime / baseAppearDuration);
150 const easedProgress = easeOutQuad(progress);
151
152 lines[i].height = maxLineHeight * easedProgress;
153 lines[i].currentX = lines[i].baseX;
154 lines[i].width = baseLineWidth;
155 updateLine(lines[i]);
156 }
157
158 if (time >= appearDuration) {
159 phase = 1;
160 time = 0;
161 }
162 } else if (phase === 1) {
163 // Перелистывание
164 for (let i = 0; i < lineCount; i++) {
165 const delay = i * lineDelay;
166 const localTime = Math.max(0, time - delay);
167 const progress = Math.min(1, localTime / baseFlipDuration);
168
169 const moveProgress = easeInOutQuad(progress);
170 lines[i].currentX = lines[i].baseX - lines[i].moveDistance * moveProgress;
171
172 const widthProgress = progress < 0.5
173 ? easeOutQuad(progress * 2)
174 : 1 - easeInQuad((progress - 0.5) * 2);
175
176 lines[i].width = baseLineWidth + (maxLineWidth - baseLineWidth) * widthProgress;
177
178 updateLine(lines[i]);
179 }
180
181 if (time >= flipDuration) {
182 phase = 2;
183 time = 0;
184 }
185 } else if (phase === 2) {
186 // Исчезновение: справа налево (последний индекс первый, потом предпоследний...)
187 for (let i = 0; i < lineCount; i++) {
188 // Задержка уменьшается с индексом → правые (большие индексы) исчезают раньше
189 const delay = (lineCount - 1 - i) * appearStagger;
190 const localTime = Math.max(0, time - delay);
191 const progress = Math.min(1, localTime / baseDisappearDuration);
192 const easedProgress = easeInQuad(progress);
193
194 lines[i].height = maxLineHeight * (1 - easedProgress);
195 updateLine(lines[i]);
196 }
197
198 if (time >= disappearDuration + pauseDuration) {
199 // Сброс
200 phase = 0;
201 time = 0;
202 for (let i = 0; i < lineCount; i++) {
203 lines[i].currentX = lines[i].baseX;
204 lines[i].width = baseLineWidth;
205 lines[i].height = 0;
206 }
207 }
208 }
209
210 requestAnimationFrame(animate);
211 }
212
213 animate();
214</script>
215</body>
216</html>