awacke1 commited on
Commit
1e5d4f8
·
verified ·
1 Parent(s): cc2cb33

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +295 -60
index.html CHANGED
@@ -3,10 +3,11 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Moulin Rouge! Tarot</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,700&family=Crimson+Text:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
 
10
  <style>
11
  /* --- Basic Setup & Moulin Rouge Theming --- */
12
  :root {
@@ -193,7 +194,6 @@
193
  align-items: center;
194
  box-sizing: border-box;
195
  }
196
- .card-back .animated-svg-back { width: 100%; height: 100%; }
197
  .card-front {
198
  background-color: var(--card-bg);
199
  color: var(--secondary-text);
@@ -203,16 +203,20 @@
203
  box-sizing: border-box;
204
  position: relative; /* For background SVG positioning */
205
  }
206
- .background-suit-icon {
207
  position: absolute;
208
- top: 50%;
209
- left: 50%;
210
- transform: translate(-50%, -50%);
211
- width: 80%;
212
- height: 80%;
213
- opacity: 0.08;
214
  z-index: 0;
 
215
  }
 
 
 
 
 
216
  .card-content {
217
  position: relative;
218
  z-index: 1;
@@ -220,16 +224,7 @@
220
  display: flex;
221
  flex-direction: column;
222
  }
223
- .suit-border {
224
- position: absolute;
225
- left: 0;
226
- top: 0;
227
- width: 100%;
228
- height: 100%;
229
- z-index: 2;
230
- pointer-events: none; /* Make sure it doesn't block clicks */
231
- }
232
-
233
  /* --- Card Content Styling (NO SCROLL) --- */
234
  .card-header {
235
  text-align: center;
@@ -429,6 +424,7 @@
429
 
430
  let drawnCards = [];
431
  let score = 0;
 
432
 
433
  // --- SVG Definitions ---
434
  const bodyIcons = {
@@ -444,75 +440,274 @@
444
  'Major Arcana': '#D4AF37' // Gold
445
  };
446
 
447
- const backgroundSuitIcons = {
448
- 'Wands': `<svg class="background-suit-icon" viewBox="0 0 100 100"><defs><filter id="glow-wands"><feGaussianBlur stdDeviation="2.5" result="coloredBlur"/><feFlood flood-color="${suitColors.Wands}" flood-opacity="0.8" result="flood"/><feComposite in="flood" in2="coloredBlur" operator="in" result="glow"/><feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g stroke-width="3" stroke="currentColor" filter="url(#glow-wands)"><path d="M50,10 L50,90" /><path d="M40,20 L60,20" /><path d="M45,90 L55,90" /><circle cx="50" cy="10" r="8"><animate attributeName="r" values="8;10;8" dur="3s" repeatCount="indefinite" /></circle><path d="M50,10 L50,90" stroke-width="1.5" stroke-opacity="0.5"><animate attributeName="stroke-dasharray" values="0 100; 50 50; 100 0" dur="3s" repeatCount="indefinite"/></path></g></svg>`,
449
- 'Cups': `<svg class="background-suit-icon" viewBox="0 0 100 100"><defs><filter id="glow-cups"><feGaussianBlur stdDeviation="3" result="coloredBlur"/><feFlood flood-color="${suitColors.Cups}" flood-opacity="0.8" result="flood"/><feComposite in="flood" in2="coloredBlur" operator="in" result="glow"/><feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g stroke-width="2" stroke="currentColor" fill="none" filter="url(#glow-cups)"><path d="M30 10 H 70 V 30 C 70 45, 60 50, 50 60 C 40 50, 30 45, 30 30 Z" /><path d="M40 70 H 60" /><path d="M50 60 V 80" /><path d="M35 80 H 65" /><path d="M40,5 C40,0 45,0 45,5 S50,0 50,5 S55,0 55,5 S60,0 60,5"><animate attributeName="d" values="M40,5 C40,0 45,0 45,5 S50,0 50,5 S55,0 55,5 S60,0 60,5; M40,8 C40,3 45,3 45,8 S50,3 50,8 S55,3 55,8 S60,3 60,8; M40,5 C40,0 45,0 45,5 S50,0 50,5 S55,0 55,5 S60,0 60,5" dur="3s" repeatCount="indefinite" /></path></g></svg>`,
450
- 'Swords': `<svg class="background-suit-icon" viewBox="0 0 100 100"><defs><filter id="glow-swords"><feGaussianBlur stdDeviation="2.5" result="coloredBlur"/><feFlood flood-color="${suitColors.Swords}" flood-opacity="0.7" result="flood"/><feComposite in="flood" in2="coloredBlur" operator="in" result="glow"/><feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g stroke-width="3" stroke="currentColor" fill="none" filter="url(#glow-swords)"><path d="M50,10 L50,70" /><path d="M35,80 L65,80" /><path d="M50,70 C 40 80, 40 85, 50 90 C 60 85, 60 80, 50 70 Z" /><path d="M50,10 L50,70" stroke-width="1" stroke-dasharray="2 4"><animate attributeName="stroke-dashoffset" from="0" to="12" dur="1s" repeatCount="indefinite" /></path></g></svg>`,
451
- 'Pentacles': `<svg class="background-suit-icon" viewBox="0 0 100 100"><defs><filter id="glow-pentacles"><feGaussianBlur stdDeviation="3" result="coloredBlur"/><feFlood flood-color="${suitColors.Pentacles}" flood-opacity="0.8" result="flood"/><feComposite in="flood" in2="coloredBlur" operator="in" result="glow"/><feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g fill="none" stroke="currentColor" filter="url(#glow-pentacles)"><circle cx="50" cy="50" r="38" stroke-width="1.5" /><path stroke-width="2" d="M50 10 L21 89 L90 35 L10 35 L79 89 Z"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="20s" linear="true" repeatCount="indefinite" /></path></g></svg>`,
452
- 'Major Arcana': `<svg class="background-suit-icon" viewBox="0 0 100 100"><defs><filter id="glow-arcana"><feGaussianBlur stdDeviation="3" result="coloredBlur"/><feFlood flood-color="${suitColors['Major Arcana']}" flood-opacity="0.8" result="flood"/><feComposite in="flood" in2="coloredBlur" operator="in" result="glow"/><feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g stroke-width="1.5" stroke="currentColor" fill="none" filter="url(#glow-arcana)"><path d="M50,10 L50,90 M10,50 L90,50"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="25s" linear="true" repeatCount="indefinite" /></path><path d="M22,22 L78,78 M22,78 L78,22"><animateTransform attributeName="transform" type="rotate" from="360 50 50" to="0 50 50" dur="35s" linear="true" repeatCount="indefinite" /></path><circle cx="50" cy="50" r="20"><animate attributeName="r" values="20;28;20" dur="7s" repeatCount="indefinite" /></circle></g></svg>`
453
- };
454
-
455
- const suitBorders = {
456
- 'Wands': `<svg class="suit-border" viewBox="0 0 100 180" preserveAspectRatio="none"><path d="M 5,5 C 20 80, -10 100, 5 175 M 95,5 C 80 80, 110 100, 95 175" fill="none" stroke="${suitColors.Wands}" stroke-width="1.5" opacity="0.3" stroke-dasharray="5,5"><animate attributeName="stroke-dashoffset" from="0" to="20" dur="2s" repeatCount="indefinite" /></path></svg>`,
457
- 'Cups': `<svg class="suit-border" viewBox="0 0 100 180" preserveAspectRatio="none"><path d="M 5,5 Q 25 90, 5 175 M 95,5 Q 75 90, 95 175" fill="none" stroke="${suitColors.Cups}" stroke-width="1.5" opacity="0.3" stroke-dasharray="170" stroke-dashoffset="170"><animate attributeName="stroke-dashoffset" values="340;0;340" dur="6s" repeatCount="indefinite" /></path></svg>`,
458
- 'Swords': `<svg class="suit-border" viewBox="0 0 100 180" preserveAspectRatio="none"><path d="M5,5 L5,175 M95,5 L95,175" fill="none" stroke="${suitColors.Swords}" stroke-width="1.5" opacity="0.4" /><path d="M5,5 L95,5 M5,175 L95,175" fill="none" stroke="${suitColors.Swords}" stroke-width="3" opacity="0.4" /></svg>`,
459
- 'Pentacles': `<svg class="suit-border" viewBox="0 0 100 180" preserveAspectRatio="none"><rect x="2" y="2" width="96" height="176" rx="10" fill="none" stroke="${suitColors.Pentacles}" stroke-width="1.5" opacity="0.3"><animate attributeName="stroke-dasharray" values="10 40; 40 10; 10 40" dur="5s" repeatCount="indefinite" /></rect></svg>`,
460
- 'Major Arcana': `<svg class="suit-border" viewBox="0 0 100 180" preserveAspectRatio="none"><defs><path id="arcana-filigree" d="M0,10 C 20,0 30,20 50,10 S 80,0 100,10" stroke="${suitColors['Major Arcana']}" fill="none" stroke-width="1.5" opacity="0.4" stroke-dasharray="100"/></defs><use href="#arcana-filigree" transform="translate(0, 5)"><animate attributeName="stroke-dashoffset" values="200;0" dur="8s" linear="true" repeatCount="indefinite" /></use><use href="#arcana-filigree" transform="translate(100, 5) scale(-1, 1) translate(-100, 0)"><animate attributeName="stroke-dashoffset" values="0;200" dur="8s" linear="true" repeatCount="indefinite" /></use><use href="#arcana-filigree" transform="translate(0, 165) scale(1,-1)"><animate attributeName="stroke-dashoffset" values="0;200" dur="8s" linear="true" repeatCount="indefinite" /></use><use href="#arcana-filigree" transform="translate(100, 165) scale(-1, -1) translate(-100, 0)"><animate attributeName="stroke-dashoffset" values="200;0" dur="8s" linear="true" repeatCount="indefinite" /></use></svg>`
461
- };
462
-
463
  // --- L-System SVG Generator for Card Backs ---
464
  const lSystemGenerator = (suit) => {
465
  const rules = {
466
  'Wands': { 'F': 'F[+F]F[-F]F' },
467
  'Cups': { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' },
468
  'Swords': { 'F': 'F-F++F-F' },
469
- 'Pentacles': { 'F': 'FF-[-F+F+F]+[+F-F-F]' },
470
  'Major Arcana': { 'X': 'F[+X]F[-X]+X', 'F': 'FF' }
471
  };
472
- const axiom = (suit === 'Cups' || suit === 'Major Arcana') ? 'X' : 'F';
473
  const iterations = (suit === 'Wands' || suit === 'Swords') ? 3 : 4;
474
- const angle = (suit === 'Swords') ? 60 : 25;
475
  const length = (suit === 'Wands' || suit === 'Swords') ? 15 : 5;
476
  const color = suitColors[suit] || suitColors['Major Arcana'];
477
 
478
  let current = axiom;
479
  for (let i = 0; i < iterations; i++) {
480
  let next = '';
481
- for (const char of current) {
482
- next += rules[suit][char] || char;
483
- }
484
  current = next;
485
  }
486
 
487
  let path = '';
488
  const stack = [];
489
  let x = 50, y = 100, a = -90;
490
- for (const char of current) {
491
- if (char === 'F') {
492
- const newX = x + length * Math.cos(a * Math.PI / 180);
493
- const newY = y + length * Math.sin(a * Math.PI / 180);
494
- path += `M${x},${y} L${newX},${newY} `;
495
- x = newX; y = newY;
496
- } else if (char === '+') a += angle;
497
- else if (char === '-') a -= angle;
498
- else if (char === '[') stack.push({x, y, a});
499
- else if (char === ']') ({x, y, a} = stack.pop());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  }
501
 
502
  return `
503
- <svg class="animated-svg-back" width="100%" height="100%" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
504
  <defs>
505
- <filter id="glow-back-${suit}"><feGaussianBlur stdDeviation="1" result="coloredBlur"/><feFlood flood-color="${color}" flood-opacity="0.9" result="flood"/><feComposite in="flood" in2="coloredBlur" operator="in" result="glow"/><feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
506
  </defs>
507
- <g transform="translate(0, -10)">
508
- <path d="${path}" stroke="${color}" stroke-width="0.5" fill="none" opacity="0.7">
509
- <animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="60s" linear="true" repeatCount="indefinite" />
510
- </path>
511
  </g>
512
- <text x="50" y="50" font-family="Playfair Display" font-size="60" fill="${color}" text-anchor="middle" dominant-baseline="central" filter="url(#glow-back-${suit})">M</text>
513
  </svg>`;
514
  };
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  /**
517
  * Shuffles an array in place.
518
  */
@@ -540,13 +735,14 @@
540
  const suit = getSuit(cardData.classifier);
541
  const displayName = cardData.name.replace(/^The\s/, '');
542
 
 
 
 
543
  card.innerHTML = `
544
  <div class="card-face card-back">
545
  ${lSystemGenerator(suit)}
546
  </div>
547
  <div class="card-face card-front">
548
- ${backgroundSuitIcons[suit] || ''}
549
- ${suitBorders[suit] || ''}
550
  <div class="card-content">
551
  <div class="card-header">
552
  <h2>${displayName} ${cardData.emojis}</h2>
@@ -561,6 +757,32 @@
561
  </div>
562
  </div>
563
  `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
 
565
  card.addEventListener('click', () => {
566
  card.classList.toggle('is-flipped');
@@ -598,11 +820,23 @@
598
  bonusInfoDisplay.innerHTML = bonusMessages.join('<br>');
599
  }
600
 
 
 
 
 
 
 
 
 
 
 
 
601
  /**
602
  * Draws cards and places them in the slots.
603
  */
604
  function drawReading() {
605
  if (drawnCards.length > 0) return; // Prevent re-drawing
 
606
  shuffle(tarot_data);
607
  drawnCards = tarot_data.slice(0, 3);
608
  const positions = ['Past', 'Present', 'Future'];
@@ -627,6 +861,7 @@
627
  score = 0;
628
  scoreDisplay.textContent = '0';
629
  bonusInfoDisplay.innerHTML = '';
 
630
  cardSlotsContainer.innerHTML = '';
631
  setupPlaceholders();
632
  }
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Moulin Rouge! Tarot - 3D Edition</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,700&family=Crimson+Text:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
11
  <style>
12
  /* --- Basic Setup & Moulin Rouge Theming --- */
13
  :root {
 
194
  align-items: center;
195
  box-sizing: border-box;
196
  }
 
197
  .card-front {
198
  background-color: var(--card-bg);
199
  color: var(--secondary-text);
 
203
  box-sizing: border-box;
204
  position: relative; /* For background SVG positioning */
205
  }
206
+ .three-canvas {
207
  position: absolute;
208
+ top: 0;
209
+ left: 0;
210
+ width: 100%;
211
+ height: 100%;
 
 
212
  z-index: 0;
213
+ opacity: 0.3; /* Adjusted opacity for better text readability */
214
  }
215
+ .card-back .svg-back {
216
+ width: 100%;
217
+ height: 100%;
218
+ }
219
+
220
  .card-content {
221
  position: relative;
222
  z-index: 1;
 
224
  display: flex;
225
  flex-direction: column;
226
  }
227
+
 
 
 
 
 
 
 
 
 
228
  /* --- Card Content Styling (NO SCROLL) --- */
229
  .card-header {
230
  text-align: center;
 
424
 
425
  let drawnCards = [];
426
  let score = 0;
427
+ let activeAnimations = [];
428
 
429
  // --- SVG Definitions ---
430
  const bodyIcons = {
 
440
  'Major Arcana': '#D4AF37' // Gold
441
  };
442
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  // --- L-System SVG Generator for Card Backs ---
444
  const lSystemGenerator = (suit) => {
445
  const rules = {
446
  'Wands': { 'F': 'F[+F]F[-F]F' },
447
  'Cups': { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' },
448
  'Swords': { 'F': 'F-F++F-F' },
449
+ 'Pentacles': { 'X': 'F-[[X]+X]+F[+FX]-X', 'F': 'FF' },
450
  'Major Arcana': { 'X': 'F[+X]F[-X]+X', 'F': 'FF' }
451
  };
452
+ const axiom = (suit === 'Cups' || suit === 'Major Arcana' || suit === 'Pentacles') ? 'X' : 'F';
453
  const iterations = (suit === 'Wands' || suit === 'Swords') ? 3 : 4;
454
+ const angle = (suit === 'Swords') ? 60 : (suit === 'Pentacles' ? 22.5 : 25.7);
455
  const length = (suit === 'Wands' || suit === 'Swords') ? 15 : 5;
456
  const color = suitColors[suit] || suitColors['Major Arcana'];
457
 
458
  let current = axiom;
459
  for (let i = 0; i < iterations; i++) {
460
  let next = '';
461
+ for (const char of current) { next += rules[suit][char] || char; }
 
 
462
  current = next;
463
  }
464
 
465
  let path = '';
466
  const stack = [];
467
  let x = 50, y = 100, a = -90;
468
+
469
+ if (suit === 'Pentacles') {
470
+ x = 50; y = 50;
471
+ let pathQuadrant = '';
472
+ for (const char of current) {
473
+ if (char === 'F') {
474
+ const newX = x + length * Math.cos(a * Math.PI / 180);
475
+ const newY = y + length * Math.sin(a * Math.PI / 180);
476
+ pathQuadrant += `M${x.toFixed(2)},${y.toFixed(2)} L${newX.toFixed(2)},${newY.toFixed(2)} `;
477
+ x = newX; y = newY;
478
+ } else if (char === '+') a += angle;
479
+ else if (char === '-') a -= angle;
480
+ else if (char === '[') stack.push({x, y, a});
481
+ else if (char === ']') ({x, y, a} = stack.pop());
482
+ }
483
+ path = `<g id="penta-q"><path d="${pathQuadrant}" /></g>
484
+ <use href="#penta-q" transform="scale(-1, 1) translate(-100, 0)" />
485
+ <use href="#penta-q" transform="scale(1, -1) translate(0, -100)" />
486
+ <use href="#penta-q" transform="scale(-1, -1) translate(-100, -100)" />`;
487
+ } else {
488
+ for (const char of current) {
489
+ if (char === 'F') {
490
+ const newX = x + length * Math.cos(a * Math.PI / 180);
491
+ const newY = y + length * Math.sin(a * Math.PI / 180);
492
+ path += `M${x.toFixed(2)},${y.toFixed(2)} L${newX.toFixed(2)},${newY.toFixed(2)} `;
493
+ x = newX; y = newY;
494
+ } else if (char === '+') a += angle;
495
+ else if (char === '-') a -= angle;
496
+ else if (char === '[') stack.push({x, y, a});
497
+ else if (char === ']') ({x, y, a} = stack.pop());
498
+ }
499
  }
500
 
501
  return `
502
+ <svg class="svg-back" width="100%" height="100%" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
503
  <defs>
504
+ <filter id="glow-back-${suit}"><feGaussianBlur stdDeviation="1.5" result="coloredBlur"/><feFlood flood-color="${color}" flood-opacity="0.9" result="flood"/><feComposite in="flood" in2="coloredBlur" operator="in" result="glow"/><feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
505
  </defs>
506
+ <g transform="${suit === 'Pentacles' ? '' : 'translate(0, -10)'}" stroke="${color}" stroke-width="0.5" fill="none" opacity="0.7">
507
+ ${suit === 'Pentacles' ? path : `<path d="${path}">`}
508
+ <animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="90s" linear="true" repeatCount="indefinite" />
509
+ ${suit === 'Pentacles' ? '' : `</path>`}
510
  </g>
511
+ <text x="50" y="50" font-family="Playfair Display" font-size="60" fill="${color}" text-anchor="middle" dominant-baseline="central" filter="url(#glow-back-${suit})">M</text>
512
  </svg>`;
513
  };
514
 
515
+ // --- Three.js Scene Factory ---
516
+ const createCardScene = (canvas, cardData) => {
517
+ const suit = getSuit(cardData.classifier);
518
+ const scene = new THREE.Scene();
519
+ const camera = new THREE.PerspectiveCamera(75, 9 / 16, 0.1, 1000);
520
+ camera.position.z = 10;
521
+ const renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true });
522
+ const clock = new THREE.Clock();
523
+
524
+ const color = new THREE.Color(suitColors[suit]);
525
+ let objectsToUpdate = [];
526
+ const randomSeed = Math.random();
527
+
528
+ // --- Generic Particle System ---
529
+ const createParticles = (config) => {
530
+ const particlesGeo = new THREE.BufferGeometry();
531
+ const count = config.count || 200;
532
+ const posArray = new Float32Array(count * 3);
533
+ const velocityArray = new Float32Array(count * 3);
534
+ const colorArray = new Float32Array(count * 3);
535
+
536
+ for(let i = 0; i < count * 3; i++) {
537
+ posArray[i] = (Math.random() - 0.5) * 0.1;
538
+ velocityArray[i] = (Math.random() - 0.5) * (config.initialVelocity || 1);
539
+ const variedColor = config.color.clone().offsetHSL((Math.random() - 0.5) * 0.2, 0, 0);
540
+ variedColor.toArray(colorArray, i * 3);
541
+ }
542
+ particlesGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
543
+ particlesGeo.setAttribute('velocity', new THREE.BufferAttribute(velocityArray, 3));
544
+ particlesGeo.setAttribute('color', new THREE.BufferAttribute(colorArray, 3));
545
+
546
+ const particleMat = new THREE.PointsMaterial({
547
+ size: config.size || 0.08,
548
+ vertexColors: true,
549
+ transparent: true,
550
+ blending: THREE.AdditiveBlending,
551
+ depthWrite: false
552
+ });
553
+ const particles = new THREE.Points(particlesGeo, particleMat);
554
+ scene.add(particles);
555
+
556
+ objectsToUpdate.push({
557
+ mesh: particles,
558
+ update: (delta) => {
559
+ const positions = particles.geometry.attributes.position.array;
560
+ const velocities = particles.geometry.attributes.velocity.array;
561
+ const range = config.range || 10;
562
+
563
+ for (let i = 0; i < count * 3; i+=3) {
564
+ velocities[i] += (Math.random() - 0.5) * (config.acceleration || 0.1) * delta;
565
+ velocities[i+1] += (Math.random() - 0.5) * (config.acceleration || 0.1) * delta;
566
+ velocities[i+2] += (Math.random() - 0.5) * (config.acceleration || 0.1) * delta;
567
+
568
+ positions[i] += velocities[i] * delta;
569
+ positions[i+1] += velocities[i+1] * delta;
570
+ positions[i+2] += velocities[i+2] * delta;
571
+
572
+ // Decay and reset
573
+ if (positions[i]**2 + positions[i+1]**2 + positions[i+2]**2 > range**2) {
574
+ positions[i] = positions[i+1] = positions[i+2] = 0;
575
+ velocities[i] = (Math.random() - 0.5) * (config.initialVelocity || 1);
576
+ velocities[i+1] = (Math.random() - 0.5) * (config.initialVelocity || 1);
577
+ velocities[i+2] = (Math.random() - 0.5) * (config.initialVelocity || 1);
578
+ }
579
+ }
580
+ particles.geometry.attributes.position.needsUpdate = true;
581
+ }
582
+ });
583
+ };
584
+
585
+ // --- Scene construction based on card name ---
586
+ switch(cardData.name) {
587
+ case "Consumption's Toll": {
588
+ // ... (specific scene from previous version)
589
+ break;
590
+ }
591
+ case "The Green Fairy": {
592
+ // ... (specific scene from previous version)
593
+ break;
594
+ }
595
+ case "The Elephant": {
596
+ // ... (specific scene from previous version)
597
+ break;
598
+ }
599
+ case "The Choice": { // VI - The Lovers
600
+ const geo1 = new THREE.TorusGeometry(2, 0.3, 16, 100);
601
+ const mat1 = new THREE.MeshStandardMaterial({color: 0xff00ff, emissive: 0xff00ff, emissiveIntensity: 0.4, metalness: 0.5, roughness: 0.5});
602
+ const torus1 = new THREE.Mesh(geo1, mat1);
603
+ const geo2 = new THREE.TorusGeometry(2, 0.3, 16, 100);
604
+ const mat2 = new THREE.MeshStandardMaterial({color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.4, metalness: 0.5, roughness: 0.5});
605
+ const torus2 = new THREE.Mesh(geo2, mat2);
606
+ torus2.rotation.x = Math.PI / 2;
607
+ scene.add(torus1);
608
+ scene.add(torus2);
609
+ const light = new THREE.PointLight(0xffffff, 1, 30);
610
+ scene.add(light);
611
+ objectsToUpdate.push({ mesh: torus1, update: (d) => { torus1.rotation.y += d * 0.3; torus1.rotation.z -= d * 0.2; }});
612
+ objectsToUpdate.push({ mesh: torus2, update: (d) => { torus2.rotation.y -= d * 0.3; torus2.rotation.z += d * 0.2; }});
613
+ break;
614
+ }
615
+ case "The Final Confrontation": { // XVI - The Tower
616
+ const towerGeo = new THREE.BoxGeometry(2, 6, 2);
617
+ const towerMat = new THREE.MeshStandardMaterial({color: 0x555555, roughness: 0.8});
618
+ const tower = new THREE.Mesh(towerGeo, towerMat);
619
+ scene.add(tower);
620
+ const light = new THREE.PointLight(0xffaa00, 5, 30);
621
+ light.position.y = 3;
622
+ scene.add(light);
623
+ createParticles({count: 500, range: 15, size: 0.1, color: new THREE.Color(0xffaa00), initialVelocity: 5, acceleration: 1});
624
+ objectsToUpdate.push({ mesh: light, update: () => { light.intensity = Math.random() * 5 + 2; }});
625
+ break;
626
+ }
627
+ case "The Topsy-Turvy World": { // X - Wheel of Fortune
628
+ const wheelGeo = new THREE.TorusGeometry(3, 0.2, 16, 100);
629
+ const wheelMat = new THREE.MeshStandardMaterial({color: color, metalness: 0.8, roughness: 0.2});
630
+ const wheel = new THREE.Mesh(wheelGeo, wheelMat);
631
+ scene.add(wheel);
632
+ for (let i=0; i<5; i++) {
633
+ const gemGeo = new THREE.IcosahedronGeometry(0.3, 0);
634
+ const gemMat = new THREE.MeshStandardMaterial({color: new THREE.Color().setHSL(Math.random(), 1, 0.5), emissive: 0x111111});
635
+ const gem = new THREE.Mesh(gemGeo, gemMat);
636
+ const angle = (i/5) * Math.PI * 2;
637
+ gem.position.set(Math.cos(angle) * 3, Math.sin(angle) * 3, 0);
638
+ wheel.add(gem);
639
+ }
640
+ const light = new THREE.DirectionalLight(0xffffff, 1);
641
+ light.position.set(1,1,1);
642
+ scene.add(light);
643
+ objectsToUpdate.push({ mesh: wheel, update: (d) => { wheel.rotation.z += d * (0.5 + randomSeed * 0.5); }});
644
+ break;
645
+ }
646
+ // Default scenes for suits
647
+ default: {
648
+ let geo;
649
+ const mat = new THREE.MeshStandardMaterial({color: color, emissive: color, emissiveIntensity: 0.2, metalness: 0.5, roughness: 0.5});
650
+ switch(suit) {
651
+ case 'Wands':
652
+ geo = new THREE.ConeGeometry(1, 4, 8);
653
+ createParticles({count: 200, range: 10, size: 0.05, color: color, initialVelocity: 0.5, acceleration: 0.2});
654
+ break;
655
+ case 'Cups':
656
+ geo = new THREE.TorusGeometry(1.5, 0.5, 16, 100);
657
+ createParticles({count: 100, range: 8, size: 0.1, color: color, initialVelocity: 0.2, acceleration: -0.1});
658
+ break;
659
+ case 'Swords':
660
+ geo = new THREE.OctahedronGeometry(2, 0);
661
+ createParticles({count: 300, range: 12, size: 0.03, color: color, initialVelocity: 2, acceleration: 0});
662
+ break;
663
+ case 'Pentacles':
664
+ geo = new THREE.CylinderGeometry(2, 2, 0.2, 5);
665
+ createParticles({count: 150, range: 10, size: 0.06, color: color, initialVelocity: 0.1, acceleration: 0.05});
666
+ break;
667
+ default:
668
+ geo = new THREE.IcosahedronGeometry(2, 1);
669
+ createParticles({count: 400, range: 15, size: 0.04, color: color, initialVelocity: 0.3, acceleration: 0.1});
670
+ break;
671
+ }
672
+ object = new THREE.Mesh(geo, mat);
673
+ scene.add(object);
674
+ const light = new THREE.PointLight(color, 2, 20);
675
+ scene.add(light);
676
+ objectsToUpdate.push({
677
+ mesh: object, update: (delta) => { object.rotation.x += delta * 0.1 * randomSeed; object.rotation.y += delta * 0.2; }
678
+ });
679
+ }
680
+ }
681
+
682
+ let animationId;
683
+ const animate = () => {
684
+ animationId = requestAnimationFrame(animate);
685
+ const delta = clock.getDelta();
686
+ objectsToUpdate.forEach(obj => obj.update(delta));
687
+ renderer.render(scene, camera);
688
+ };
689
+
690
+ const resizeRendererToDisplaySize = () => {
691
+ const canvas = renderer.domElement;
692
+ const width = canvas.clientWidth;
693
+ const height = canvas.clientHeight;
694
+ if (canvas.width !== width || canvas.height !== height) {
695
+ renderer.setSize(width, height, false);
696
+ camera.aspect = width / height;
697
+ camera.updateProjectionMatrix();
698
+ }
699
+ };
700
+
701
+ resizeRendererToDisplaySize();
702
+ animate();
703
+
704
+ return {
705
+ stop: () => cancelAnimationFrame(animationId),
706
+ start: animate,
707
+ resize: resizeRendererToDisplaySize
708
+ };
709
+ };
710
+
711
  /**
712
  * Shuffles an array in place.
713
  */
 
735
  const suit = getSuit(cardData.classifier);
736
  const displayName = cardData.name.replace(/^The\s/, '');
737
 
738
+ const frontCanvas = document.createElement('canvas');
739
+ frontCanvas.className = 'three-canvas';
740
+
741
  card.innerHTML = `
742
  <div class="card-face card-back">
743
  ${lSystemGenerator(suit)}
744
  </div>
745
  <div class="card-face card-front">
 
 
746
  <div class="card-content">
747
  <div class="card-header">
748
  <h2>${displayName} ${cardData.emojis}</h2>
 
757
  </div>
758
  </div>
759
  `;
760
+
761
+ card.querySelector('.card-front').prepend(frontCanvas);
762
+
763
+ // Use a timeout to ensure canvas is in the DOM before initializing Three.js
764
+ setTimeout(() => {
765
+ const frontAnimation = createCardScene(frontCanvas, cardData);
766
+
767
+ const observer = new IntersectionObserver((entries) => {
768
+ entries.forEach(entry => {
769
+ if (entry.isIntersecting) {
770
+ frontAnimation.start();
771
+ } else {
772
+ frontAnimation.stop();
773
+ }
774
+ });
775
+ });
776
+ observer.observe(card);
777
+
778
+ // Handle resize
779
+ new ResizeObserver(() => {
780
+ frontAnimation.resize();
781
+ }).observe(card);
782
+
783
+ activeAnimations.push({ observer, frontAnimation });
784
+ }, 0);
785
+
786
 
787
  card.addEventListener('click', () => {
788
  card.classList.toggle('is-flipped');
 
820
  bonusInfoDisplay.innerHTML = bonusMessages.join('<br>');
821
  }
822
 
823
+ /**
824
+ * Cleans up old animations.
825
+ */
826
+ function cleanupAnimations() {
827
+ activeAnimations.forEach(({ observer, frontAnimation }) => {
828
+ observer.disconnect();
829
+ frontAnimation.stop();
830
+ });
831
+ activeAnimations = [];
832
+ }
833
+
834
  /**
835
  * Draws cards and places them in the slots.
836
  */
837
  function drawReading() {
838
  if (drawnCards.length > 0) return; // Prevent re-drawing
839
+ cleanupAnimations();
840
  shuffle(tarot_data);
841
  drawnCards = tarot_data.slice(0, 3);
842
  const positions = ['Past', 'Present', 'Future'];
 
861
  score = 0;
862
  scoreDisplay.textContent = '0';
863
  bonusInfoDisplay.innerHTML = '';
864
+ cleanupAnimations();
865
  cardSlotsContainer.innerHTML = '';
866
  setupPlaceholders();
867
  }