1"""Final alive version: each streak undulates like a tentacle with its own rhythm.
2
3Key technique: each streak gets its own SVG turbulence filter with animated
4baseFrequency/seed. The displacement scale also animates. This makes each
5streak warp and writhe organically, on its own rhythm.
6
7Plus rotations on a few blue streaks (their own axis), and the previous
8shape animations (pink starburst pulse, snail rotation, blue starburst rotation,
9flowers blooming).
10"""
11import base64
12
13def b64(path):
14 with open(path, 'rb') as f:
15 return base64.b64encode(f.read()).decode('ascii')
16
17
18img_full = b64('/home/claude/painting_FINAL_extended.png')
19img_base = b64('/home/claude/painting_base_full.png')
20img_blue_burst = b64('/home/claude/shape_blue_starburst.png')
21img_snail = b64('/home/claude/shape_snail.png')
22img_flower_top = b64('/home/claude/shape_flower_top.png')
23img_flower_lower = b64('/home/claude/shape_flower_lower.png')
24img_flower_left = b64('/home/claude/shape_flower_left.png')
25img_pink = b64('/home/claude/shape_pink_starburst.png')
26
27
28img_brown_scribble = b64('/home/claude/extra_brown_scribble.png')
29img_loaf = b64('/home/claude/extra_loaf.png')
30img_purple_drips = b64('/home/claude/extra_purple_drips.png')
31img_red_specks = b64('/home/claude/extra_red_specks.png')
32
33
34streak_imgs = {
35 'pink_leg_left': b64('/home/claude/streak_pink_leg_left.png'),
36 'pink_leg_centerleft': b64('/home/claude/streak_pink_leg_centerleft.png'),
37 'pink_leg_center': b64('/home/claude/streak_pink_leg_center.png'),
38 'pink_leg_right': b64('/home/claude/streak_pink_leg_right.png'),
39 'pink_leg_up': b64('/home/claude/streak_pink_leg_up.png'),
40 'pink_arc_topright': b64('/home/claude/streak_pink_arc_topright.png'),
41 'blue_vertical': b64('/home/claude/streak_blue_vertical.png'),
42 'blue_diagonal': b64('/home/claude/streak_blue_diagonal.png'),
43 'yellow_ray_right': b64('/home/claude/streak_yellow_ray_right.png'),
44 'yellow_ray_center': b64('/home/claude/streak_yellow_ray_center.png'),
45 'yellow_ray_left': b64('/home/claude/streak_yellow_ray_left.png'),
46 'brown_drip_left': b64('/home/claude/streak_brown_drip_left.png'),
47
48 'brown_scribble': b64('/home/claude/extra_brown_scribble.png'),
49 'loaf': b64('/home/claude/extra_loaf.png'),
50 'purple_drips': b64('/home/claude/extra_purple_drips.png'),
51 'red_specks': b64('/home/claude/extra_red_specks.png'),
52}
53
54
55streak_info = {
56 'pink_leg_left': (120, 770, 200, 330, 0.85, 0.05),
57 'pink_leg_centerleft': (180, 800, 110, 350, 1.0, 0.0),
58 'pink_leg_center': (220, 810, 110, 340, 0.5, 0.0),
59 'pink_leg_right': (280, 800, 130, 300, 0.1, 0.0),
60 'pink_leg_up': (220, 580, 110, 220, 0.5, 1.0),
61 'pink_arc_topright': (320, 130, 220, 570, 0.0, 1.0),
62 'blue_vertical': (630, 380, 90, 360, 0.5, 0.5),
63 'blue_diagonal': (530, 420, 160, 180, 0.0, 1.0),
64 'yellow_ray_right': (610, 380, 70, 620, 0.5, 0.05),
65 'yellow_ray_center': (490, 540, 80, 540, 0.5, 0.0),
66 'yellow_ray_left': (370, 540, 90, 510, 0.5, 0.0),
67 'brown_drip_left': (140, 380, 80, 320, 0.5, 0.0),
68
69 'brown_scribble': (170, 100, 410, 310, 0.5, 0.5),
70 'loaf': (440, 1180, 290, 250, 0.5, 0.5),
71 'purple_drips': (280, 1100, 180, 200, 0.5, 0.0),
72 'red_specks': (140, 870, 90, 140, 0.5, 0.5),
73
74 'snail_undulate': (0, 0, 0, 0, 0.5, 0.5),
75}
76
77
78streak_anim = {
79
80 'pink_leg_left': {'freq': 0.018, 'amp': 4.5, 'dur': 7, 'delay': 0.0, 'rotate': None},
81 'pink_leg_centerleft': {'freq': 0.022, 'amp': 4.0, 'dur': 6, 'delay': 1.2, 'rotate': None},
82 'pink_leg_center': {'freq': 0.020, 'amp': 5.0, 'dur': 8, 'delay': 0.5, 'rotate': None},
83 'pink_leg_right': {'freq': 0.025, 'amp': 4.5, 'dur': 6.5, 'delay': 2.0, 'rotate': None},
84 'pink_leg_up': {'freq': 0.020, 'amp': 3.5, 'dur': 9, 'delay': 1.5, 'rotate': None},
85 'pink_arc_topright': {'freq': 0.015, 'amp': 5.5, 'dur': 11, 'delay': 0.8, 'rotate': None},
86
87
88 'blue_vertical': {'freq': 0.022, 'amp': 3.5, 'dur': 9, 'delay': 0.0, 'rotate': None},
89 'blue_diagonal': {'freq': 0.025, 'amp': 3.0, 'dur': 7, 'delay': 1.0, 'rotate': None},
90
91
92 'yellow_ray_right': {'freq': 0.018, 'amp': 4.0, 'dur': 7.5, 'delay': 0.3, 'rotate': None},
93 'yellow_ray_center': {'freq': 0.020, 'amp': 3.5, 'dur': 8, 'delay': 1.7, 'rotate': None},
94 'yellow_ray_left': {'freq': 0.022, 'amp': 4.2, 'dur': 6.5, 'delay': 0.9, 'rotate': None},
95
96
97 'brown_drip_left': {'freq': 0.025, 'amp': 2.5, 'dur': 10, 'delay': 2.5, 'rotate': None},
98
99
100
101 'brown_scribble': {'freq': 0.020, 'amp': 3.5, 'dur': 9, 'delay': 0.0, 'rotate': None},
102
103 'loaf': {'freq': 0.012, 'amp': 2.5, 'dur': 12, 'delay': 1.5, 'rotate': None},
104
105 'purple_drips': {'freq': 0.025, 'amp': 2.0, 'dur': 7, 'delay': 0.5, 'rotate': None},
106
107 'red_specks': {'freq': 0.040, 'amp': 1.5, 'dur': 4, 'delay': 1.0, 'rotate': None},
108
109 'snail_undulate': {'freq': 0.025, 'amp': 1.8, 'dur': 6, 'delay': 0.4, 'rotate': None},
110}
111
112
113PAPER_X = 25
114PAPER_Y = 8
115PW = 100
116PH = 196
117PAPER_BOTTOM = PAPER_Y + PH
118CLIP_Y = PAPER_BOTTOM - 4
119TOTAL_W = 150
120TOTAL_H = PAPER_BOTTOM + 18
121
122CHALK_LEFT_X = 18
123CHALK_LEFT_W = 7
124CHALK_RIGHT_X = PAPER_X + PW
125CHALK_RIGHT_W = 7
126POST_LEFT_X = 8
127POST_LEFT_W = 10
128POST_RIGHT_X = PAPER_X + PW + 7
129POST_RIGHT_W = 10
130
131def px_to_svg_x(px):
132 return PAPER_X + (px / 800) * PW
133
134def px_to_svg_y(px):
135 return PAPER_Y + (px / 1572) * PH
136
137def px_to_svg_w(px):
138 return (px / 800) * PW
139
140def px_to_svg_h(px):
141 return (px / 1572) * PH
142
143
144
145shapes_info = {
146 'blue_burst': {'px': (470, 420), 'pw': 330, 'ph': 330, 'img': img_blue_burst,
147 'cx_pct': 0.79, 'cy_pct': 0.32},
148 'snail': {'px': (80, 1170), 'pw': 300, 'ph': 310, 'img': img_snail,
149 'cx_pct': 0.7, 'cy_pct': 0.45},
150 'flower_top': {'px': (400, 490), 'pw': 80, 'ph': 90, 'img': img_flower_top,
151 'cx_pct': 0.5, 'cy_pct': 0.5},
152 'flower_lower': {'px': (520, 940), 'pw': 80, 'ph': 90, 'img': img_flower_lower,
153 'cx_pct': 0.5, 'cy_pct': 0.5},
154 'flower_left': {'px': (130, 690), 'pw': 70, 'ph': 80, 'img': img_flower_left,
155 'cx_pct': 0.5, 'cy_pct': 0.5},
156 'pink_burst': {'px': (90, 580), 'pw': 420, 'ph': 520, 'img': img_pink,
157 'cx_pct': 0.55, 'cy_pct': 0.4},
158}
159
160
161filters_html = ''
162for name, params in streak_anim.items():
163 freq = params['freq']
164 amp = params['amp']
165 dur = params['dur']
166 delay = params['delay']
167
168
169
170
171
172 freq_low = freq * 0.75
173 freq_high = freq * 1.25
174
175 scale_low = amp * 0.4
176 scale_high = amp
177
178
179
180
181
182
183 filters_html += f'''
184 <filter id="undulate_{name}" x="-15%" y="-15%" width="130%" height="130%">
185 <feTurbulence type="fractalNoise" baseFrequency="{freq}" numOctaves="2" seed="{hash(name) % 100}">
186 <animate attributeName="baseFrequency"
187 values="{freq_low};{freq_high};{freq_low}"
188 keyTimes="0;0.5;1"
189 keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
190 calcMode="spline"
191 dur="{dur}s" begin="{delay}s" repeatCount="indefinite"/>
192 </feTurbulence>
193 <feDisplacementMap in="SourceGraphic" scale="{amp}">
194 <animate attributeName="scale"
195 values="{scale_low};{scale_high};{scale_low}"
196 keyTimes="0;0.5;1"
197 keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
198 calcMode="spline"
199 dur="{dur}s" begin="{delay}s" repeatCount="indefinite"/>
200 </feDisplacementMap>
201 </filter>'''
202
203
204streak_elements_html = ''
205for name, (px_x, px_y, px_w, px_h, ax, ay) in streak_info.items():
206 if name == 'snail_undulate':
207 continue # this one is just used as a filter on the snail
208 sx = px_to_svg_x(px_x)
209 sy = px_to_svg_y(px_y)
210 sw = px_to_svg_w(px_w)
211 sh = px_to_svg_h(px_h)
212 cx = sx + sw * ax
213 cy = sy + sh * ay
214
215 rotate_info = streak_anim[name].get('rotate')
216 rotate_class = f'streak-rotate streak-rotate-{name}' if rotate_info else ''
217
218 classes = f'shape-layer streak {rotate_class}'.strip()
219
220 streak_elements_html += f''' <g style="transform-origin: {cx:.3f}px {cy:.3f}px; transform-box: view-box;">
221 <image class="{classes}"
222 x="{sx:.3f}" y="{sy:.3f}" width="{sw:.3f}" height="{sh:.3f}"
223 href="data:image/png;base64,{streak_imgs[name]}"
224 filter="url(#undulate_{name})"
225 preserveAspectRatio="xMidYMid slice"/>
226 </g>
227'''
228
229
230rotation_css = ''
231for name, params in streak_anim.items():
232 rot = params.get('rotate')
233 if rot:
234 cx_px = streak_info[name][0] + streak_info[name][2] * streak_info[name][4]
235 cy_px = streak_info[name][1] + streak_info[name][3] * streak_info[name][5]
236 cx_svg = px_to_svg_x(cx_px)
237 cy_svg = px_to_svg_y(cy_px)
238 end_deg = 360 if rot['dir'] > 0 else -360
239 rotation_css += f'''
240 .streak-rotate-{name} {{
241 transform-origin: {cx_svg:.3f}px {cy_svg:.3f}px;
242 transform-box: view-box;
243 animation: fadeInShape 1.5s ease-in-out 6s forwards,
244 rotate_{name} {rot['dur']}s linear 7s infinite;
245 }}
246 @keyframes rotate_{name} {{
247 from {{ transform: rotate(0deg); }}
248 to {{ transform: rotate({end_deg}deg); }}
249 }}'''
250
251
252
253prev_shape_html = ''
254for name, info in shapes_info.items():
255 px_x, px_y = info['px']
256 sx = px_to_svg_x(px_x)
257 sy = px_to_svg_y(px_y)
258 sw = px_to_svg_w(info['pw'])
259 sh = px_to_svg_h(info['ph'])
260
261
262 filter_attr = ''
263 if name == 'snail':
264 filter_attr = ' filter="url(#undulate_snail_undulate)"'
265
266 prev_shape_html += f''' <image class="shape-layer anim-{name}"
267 x="{sx:.3f}" y="{sy:.3f}" width="{sw:.3f}" height="{sh:.3f}"
268 href="data:image/png;base64,{info['img']}"
269 preserveAspectRatio="xMidYMid slice"{filter_attr}/>
270'''
271
272html = f'''<!DOCTYPE html>
273<html lang="en">
274<head>
275<meta charset="UTF-8" />
276<title>The Painting</title>
277<style>
278 html, body {{
279 margin: 0; padding: 0;
280 background: #ebe5d8;
281 height: 100%;
282 overflow: hidden;
283 }}
284
285 .stage {{
286 position: fixed; inset: 0;
287 overflow: hidden;
288 display: grid;
289 place-items: center;
290 }}
291
292 .frame {{
293 width: min(85vmin, 700px);
294 height: min(98vh, 1000px);
295 position: relative;
296 }}
297
298 .room-svg {{ width: 100%; height: 100%; display: block; }}
299
300 /* Initial reveal of full painting */
301 .painting-static {{
302 -webkit-mask: linear-gradient(to bottom, black 0%, black 0%, transparent 0%);
303 mask: linear-gradient(to bottom, black 0%, black 0%, transparent 0%);
304 animation: revealStatic 5s ease-out 1.0s forwards,
305 fadeStatic 1.5s ease-in-out 6s forwards;
306 }}
307 @keyframes revealStatic {{
308 0% {{ -webkit-mask: linear-gradient(to bottom, black 0%, black 0%, transparent 0%);
309 mask: linear-gradient(to bottom, black 0%, black 0%, transparent 0%); }}
310 100% {{ -webkit-mask: linear-gradient(to bottom, black 100%, black 100%, transparent 100%);
311 mask: linear-gradient(to bottom, black 100%, black 100%, transparent 100%); }}
312 }}
313 @keyframes fadeStatic {{
314 to {{ opacity: 0; }}
315 }}
316
317 .painting-base, .shape-layer {{
318 opacity: 0;
319 animation: fadeInShape 1.5s ease-in-out 6s forwards;
320 }}
321 @keyframes fadeInShape {{
322 to {{ opacity: 1; }}
323 }}
324
325 /* === SHAPE-SPECIFIC ANIMATIONS === */
326
327 .anim-blue_burst {{
328 transform-origin: center;
329 transform-box: fill-box;
330 animation: fadeInShape 1.5s ease-in-out 6s forwards,
331 bluePulse 6s ease-in-out 7s infinite;
332 }}
333 @keyframes bluePulse {{
334 0%, 100% {{ transform: scale(1); }}
335 50% {{ transform: scale(1.04); }}
336 }}
337
338 .anim-snail {{
339 transform-origin: center;
340 transform-box: fill-box;
341 animation: fadeInShape 1.5s ease-in-out 6s forwards,
342 snailBreathe 5.5s ease-in-out 7s infinite;
343 }}
344 @keyframes snailBreathe {{
345 0%, 100% {{ transform: scale(1) translate(0, 0); }}
346 25% {{ transform: scale(1.025) translate(0.05px, -0.05px); }}
347 50% {{ transform: scale(1.04) translate(-0.05px, 0.05px); }}
348 75% {{ transform: scale(1.025) translate(0.05px, 0.05px); }}
349 }}
350
351 .anim-pink_burst {{
352 transform-origin: center;
353 transform-box: fill-box;
354 animation: fadeInShape 1.5s ease-in-out 6s forwards,
355 pinkPulse 4.5s ease-in-out 7s infinite;
356 }}
357 @keyframes pinkPulse {{
358 0%, 100% {{ transform: scale(1); }}
359 50% {{ transform: scale(1.05); }}
360 }}
361
362 .anim-flower_top {{
363 transform-origin: center;
364 transform-box: fill-box;
365 animation: fadeInShape 1.5s ease-in-out 6s forwards,
366 flowerBloom 4s ease-in-out 7.2s infinite;
367 }}
368 .anim-flower_lower {{
369 transform-origin: center;
370 transform-box: fill-box;
371 animation: fadeInShape 1.5s ease-in-out 6s forwards,
372 flowerBloom 5.3s ease-in-out 7.5s infinite;
373 }}
374 .anim-flower_left {{
375 transform-origin: center;
376 transform-box: fill-box;
377 animation: fadeInShape 1.5s ease-in-out 6s forwards,
378 flowerBloom 3.5s ease-in-out 8s infinite;
379 }}
380 @keyframes flowerBloom {{
381 0%, 100% {{ transform: scale(0.85); }}
382 50% {{ transform: scale(1.18); }}
383 }}
384
385 /* Streak rotation animations (blue ones rotate on own axis) */
386 {rotation_css}
387
388 /* Paper tilts gently */
389 .paper-tilt {{
390 transform-origin: center;
391 animation: paperTilt 18s ease-in-out infinite;
392 }}
393 @keyframes paperTilt {{
394 0%, 100% {{ transform: rotate(-1.2deg); }}
395 50% {{ transform: rotate(-0.6deg); }}
396 }}
397
398 .sketch-line {{
399 stroke-dasharray: var(--len, 200);
400 stroke-dashoffset: var(--len, 200);
401 animation: drawSketch var(--sdur, 1.6s) ease-out var(--sdelay, 0s) forwards;
402 }}
403 @keyframes drawSketch {{ to {{ stroke-dashoffset: 0; }} }}
404
405 .sketch-fill {{
406 opacity: 0;
407 animation: fadeInSketch 1.8s ease-out 0.4s forwards;
408 }}
409 @keyframes fadeInSketch {{ to {{ opacity: 1; }} }}
410
411 .clip-wiggle {{
412 transform-origin: center top;
413 transform-box: fill-box;
414 animation: clipwiggle 7s ease-in-out 6s infinite;
415 }}
416 @keyframes clipwiggle {{
417 0%, 100% {{ transform: rotate(0deg); }}
418 50% {{ transform: rotate(0.5deg); }}
419 }}
420</style>
421</head>
422<body>
423
424<div class="stage">
425 <div class="frame">
426
427<svg class="room-svg" viewBox="0 0 {TOTAL_W} {TOTAL_H}" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
428 <defs>
429 <linearGradient id="paperGrad" x1="0" y1="0" x2="1" y2="1">
430 <stop offset="0%" stop-color="#fefdf6"/>
431 <stop offset="50%" stop-color="#fbfaef"/>
432 <stop offset="100%" stop-color="#f5f0e0"/>
433 </linearGradient>
434 <linearGradient id="woodWash" x1="0" y1="0" x2="0" y2="1">
435 <stop offset="0%" stop-color="#d4c5a8" stop-opacity="0.45"/>
436 <stop offset="100%" stop-color="#b8a888" stop-opacity="0.55"/>
437 </linearGradient>
438 <linearGradient id="chalkWash" x1="0" y1="0" x2="0" y2="1">
439 <stop offset="0%" stop-color="#5a554d" stop-opacity="0.4"/>
440 <stop offset="100%" stop-color="#3a3530" stop-opacity="0.5"/>
441 </linearGradient>
442 <filter id="pencilWobble" x="-2%" y="-2%" width="104%" height="104%">
443 <feTurbulence type="fractalNoise" baseFrequency="1.5" numOctaves="2" seed="3"/>
444 <feDisplacementMap in="SourceGraphic" scale="0.2"/>
445 </filter>
446 <filter id="paperShadow" x="-15%" y="-15%" width="130%" height="130%">
447 <feGaussianBlur in="SourceAlpha" stdDeviation="1.2"/>
448 <feOffset dx="0.5" dy="1.5" result="offsetblur"/>
449 <feComponentTransfer><feFuncA type="linear" slope="0.3"/></feComponentTransfer>
450 <feMerge>
451 <feMergeNode/>
452 <feMergeNode in="SourceGraphic"/>
453 </feMerge>
454 </filter>
455
456 <!-- TRAVELING WAVE — soft warm-light band sweeping upward -->
457 <!-- Wider band with feathered edges, more stops for smoother gradient -->
458 <linearGradient id="waveGradient" x1="0" y1="0" x2="0" y2="1"
459 gradientUnits="objectBoundingBox">
460 <stop offset="0%" stop-color="rgba(255,248,225,0)">
461 <animate attributeName="offset"
462 values="1.3;-0.5"
463 keyTimes="0;1"
464 calcMode="spline"
465 keySplines="0.4 0 0.6 1"
466 dur="14s" repeatCount="indefinite"/>
467 </stop>
468 <stop offset="8%" stop-color="rgba(255,248,225,0.18)">
469 <animate attributeName="offset"
470 values="1.38;-0.42"
471 keyTimes="0;1"
472 calcMode="spline"
473 keySplines="0.4 0 0.6 1"
474 dur="14s" repeatCount="indefinite"/>
475 </stop>
476 <stop offset="16%" stop-color="rgba(255,248,225,0.42)">
477 <animate attributeName="offset"
478 values="1.46;-0.34"
479 keyTimes="0;1"
480 calcMode="spline"
481 keySplines="0.4 0 0.6 1"
482 dur="14s" repeatCount="indefinite"/>
483 </stop>
484 <stop offset="24%" stop-color="rgba(255,248,225,0.18)">
485 <animate attributeName="offset"
486 values="1.54;-0.26"
487 keyTimes="0;1"
488 calcMode="spline"
489 keySplines="0.4 0 0.6 1"
490 dur="14s" repeatCount="indefinite"/>
491 </stop>
492 <stop offset="32%" stop-color="rgba(255,248,225,0)">
493 <animate attributeName="offset"
494 values="1.62;-0.18"
495 keyTimes="0;1"
496 calcMode="spline"
497 keySplines="0.4 0 0.6 1"
498 dur="14s" repeatCount="indefinite"/>
499 </stop>
500 </linearGradient>
501
502 <!-- SHADOW BAND — a subtle dark line ahead of the light, like the curve of unrolling paper -->
503 <linearGradient id="shadowGradient" x1="0" y1="0" x2="0" y2="1"
504 gradientUnits="objectBoundingBox">
505 <stop offset="0%" stop-color="rgba(60,40,20,0)">
506 <animate attributeName="offset"
507 values="1.42;-0.38"
508 keyTimes="0;1"
509 calcMode="spline"
510 keySplines="0.4 0 0.6 1"
511 dur="14s" repeatCount="indefinite"/>
512 </stop>
513 <stop offset="6%" stop-color="rgba(60,40,20,0.18)">
514 <animate attributeName="offset"
515 values="1.48;-0.32"
516 keyTimes="0;1"
517 calcMode="spline"
518 keySplines="0.4 0 0.6 1"
519 dur="14s" repeatCount="indefinite"/>
520 </stop>
521 <stop offset="12%" stop-color="rgba(60,40,20,0)">
522 <animate attributeName="offset"
523 values="1.54;-0.26"
524 keyTimes="0;1"
525 calcMode="spline"
526 keySplines="0.4 0 0.6 1"
527 dur="14s" repeatCount="indefinite"/>
528 </stop>
529 </linearGradient>
530
531 <!-- Per-streak undulation filters -->
532{filters_html}
533 </defs>
534
535 <!-- Background slivers -->
536 <rect class="sketch-fill" x="{POST_LEFT_X}" y="0" width="{POST_LEFT_W}" height="{TOTAL_H}" fill="url(#woodWash)" stroke="none"/>
537 <rect class="sketch-fill" x="{POST_RIGHT_X}" y="0" width="{POST_RIGHT_W}" height="{TOTAL_H}" fill="url(#woodWash)" stroke="none"/>
538 <rect class="sketch-fill" x="{CHALK_LEFT_X}" y="0" width="{CHALK_LEFT_W}" height="{TOTAL_H}" fill="url(#chalkWash)" stroke="none"/>
539 <rect class="sketch-fill" x="{CHALK_RIGHT_X}" y="0" width="{CHALK_RIGHT_W}" height="{TOTAL_H}" fill="url(#chalkWash)" stroke="none"/>
540
541 <g filter="url(#pencilWobble)" stroke="#7a6a55" fill="none" stroke-linecap="round" stroke-width="0.25">
542 <line class="sketch-line" style="--len: {TOTAL_H}; --sdur: 1.6s; --sdelay: 0s;"
543 x1="{POST_LEFT_X}" y1="0" x2="{POST_LEFT_X}" y2="{TOTAL_H}" opacity="0.5"/>
544 <line class="sketch-line" style="--len: {TOTAL_H}; --sdur: 1.6s; --sdelay: 0.05s;"
545 x1="{POST_LEFT_X + POST_LEFT_W}" y1="0" x2="{POST_LEFT_X + POST_LEFT_W}" y2="{TOTAL_H}" opacity="0.5"/>
546 <line class="sketch-line" style="--len: {TOTAL_H}; --sdur: 1.6s; --sdelay: 0.1s;"
547 x1="{POST_RIGHT_X}" y1="0" x2="{POST_RIGHT_X}" y2="{TOTAL_H}" opacity="0.5"/>
548 <line class="sketch-line" style="--len: {TOTAL_H}; --sdur: 1.6s; --sdelay: 0.15s;"
549 x1="{POST_RIGHT_X + POST_RIGHT_W}" y1="0" x2="{POST_RIGHT_X + POST_RIGHT_W}" y2="{TOTAL_H}" opacity="0.5"/>
550 </g>
551
552 <!-- THE PAPER -->
553 <g class="paper-tilt" style="transform-origin: {TOTAL_W/2}px {TOTAL_H/2}px;">
554 <rect x="{PAPER_X}" y="{PAPER_Y}" width="{PW}" height="{PH}"
555 fill="url(#paperGrad)" filter="url(#paperShadow)"/>
556
557 <!-- Static painting (revealed first, then fades) -->
558 <image class="painting-static"
559 x="{PAPER_X}" y="{PAPER_Y}" width="{PW}" height="{PH}"
560 href="data:image/png;base64,{img_full}"
561 preserveAspectRatio="xMidYMid slice"/>
562
563 <!-- Base painting (everything except animated parts) -->
564 <image class="painting-base"
565 x="{PAPER_X}" y="{PAPER_Y}" width="{PW}" height="{PH}"
566 href="data:image/png;base64,{img_base}"
567 preserveAspectRatio="xMidYMid slice"/>
568
569 <!-- Animated SHAPES (rotation, pulse, bloom) -->
570{prev_shape_html}
571
572 <!-- Animated STREAKS (undulation) -->
573{streak_elements_html}
574
575 <!-- TRAVELING WAVE — a soft shadow band followed by a highlight band that sweeps upward -->
576 <!-- The shadow + light combo simulates the curve of unrolling paper -->
577 <rect class="wave-shadow"
578 x="{PAPER_X}" y="{PAPER_Y}" width="{PW}" height="{PH}"
579 fill="url(#shadowGradient)"
580 style="mix-blend-mode: multiply; pointer-events: none;"/>
581 <rect class="wave-band"
582 x="{PAPER_X}" y="{PAPER_Y}" width="{PW}" height="{PH}"
583 fill="url(#waveGradient)"
584 style="mix-blend-mode: overlay; pointer-events: none;"/>
585
586 <!-- Subtle paper top curl -->
587 <path d="M {PAPER_X + 2} {PAPER_Y + 1} Q {PAPER_X + PW/2} {PAPER_Y + 3} {PAPER_X + PW - 2} {PAPER_Y + 1}"
588 fill="none" stroke="rgba(140,120,90,0.18)" stroke-width="0.3"/>
589
590 <!-- BOTTOM YELLOW CLIP -->
591 <g filter="url(#pencilWobble)" stroke="#5a4f55" fill="none" stroke-linecap="round" stroke-linejoin="round">
592 <g class="clip-wiggle">
593 <rect class="sketch-fill"
594 x="{PAPER_X + PW/2 - 8}" y="{CLIP_Y}"
595 width="16" height="11" rx="1.5"
596 fill="rgba(220, 180, 50, 0.75)" stroke="none"/>
597 <rect class="sketch-line" style="--len: 60; --sdur: 1.2s; --sdelay: 1.6s;"
598 x="{PAPER_X + PW/2 - 8}" y="{CLIP_Y}"
599 width="16" height="11" rx="1.5" stroke-width="0.3"/>
600 <ellipse class="sketch-fill"
601 cx="{PAPER_X + PW/2}" cy="{CLIP_Y + 12.5}" rx="5" ry="2"
602 fill="rgba(180, 140, 30, 0.7)" stroke="none"/>
603 <ellipse class="sketch-line" style="--len: 32; --sdur: 0.8s; --sdelay: 1.8s;"
604 cx="{PAPER_X + PW/2}" cy="{CLIP_Y + 12.5}" rx="5" ry="2" stroke-width="0.3"/>
605 <circle class="sketch-line" style="--len: 12; --sdur: 0.5s; --sdelay: 2.0s;"
606 cx="{PAPER_X + PW/2}" cy="{CLIP_Y + 6}" r="2" stroke-width="0.3"
607 fill="rgba(140, 110, 20, 0.6)"/>
608 </g>
609 </g>
610 </g>
611
612</svg>
613
614 </div>
615</div>
616
617</body>
618</html>
619'''
620
621with open('/home/claude/painting.html', 'w') as f:
622 f.write(html)
623
624import os
625print(f"Wrote painting.html ({os.path.getsize('/home/claude/painting.html')} bytes)")
626