Mattias Bodlund 6 months ago
parent
commit
7d29638552
80 changed files with 1133 additions and 114 deletions
  1. +2
    -2
      Gemfile.lock
  2. +84
    -34
      app/assets/stylesheets/application.css
  3. +3
    -0
      app/controllers/answers_controller.rb
  4. +30
    -1
      app/controllers/questions_controller.rb
  5. +14
    -2
      app/helpers/questions_helper.rb
  6. +444
    -0
      app/javascript/application.js
  7. +23
    -0
      app/javascript/image_controller.js
  8. +13
    -7
      app/javascript/locale_controller.js
  9. +15
    -0
      app/models/quiz_result.rb
  10. +17
    -4
      app/views/languages/_intro.html.erb
  11. +1
    -1
      app/views/languages/index.html.erb
  12. +5
    -3
      app/views/layouts/application.html.erb
  13. +16
    -11
      app/views/players/new.html.erb
  14. +24
    -12
      app/views/questions/answer.html.erb
  15. +24
    -11
      app/views/questions/result.html.erb
  16. +41
    -0
      app/views/questions/shared_result.html.erb
  17. +20
    -13
      app/views/questions/show.html.erb
  18. +1
    -0
      config/importmap.rb
  19. +4
    -0
      config/locales/en.yml
  20. +1
    -1
      config/routes.rb
  21. +15
    -0
      db/migrate/20250527114853_create_quiz_results.rb
  22. +12
    -1
      db/schema.rb
  23. +2
    -3
      public/400.html
  24. +1
    -2
      public/404.html
  25. +1
    -2
      public/406-unsupported-browser.html
  26. +1
    -2
      public/422.html
  27. +1
    -2
      public/500.html
  28. +11
    -0
      test/fixtures/quiz_results.yml
  29. +7
    -0
      test/models/quiz_result_test.rb
  30. BIN
      vendor/javascript/gsap/.DS_Store
  31. +11
    -0
      vendor/javascript/gsap/CSSRulePlugin.min.js
  32. +1
    -0
      vendor/javascript/gsap/CSSRulePlugin.min.js.map
  33. +11
    -0
      vendor/javascript/gsap/CustomBounce.min.js
  34. +1
    -0
      vendor/javascript/gsap/CustomBounce.min.js.map
  35. +11
    -0
      vendor/javascript/gsap/CustomEase.min.js
  36. +1
    -0
      vendor/javascript/gsap/CustomEase.min.js.map
  37. +11
    -0
      vendor/javascript/gsap/CustomWiggle.min.js
  38. +1
    -0
      vendor/javascript/gsap/CustomWiggle.min.js.map
  39. +11
    -0
      vendor/javascript/gsap/Draggable.min.js
  40. +1
    -0
      vendor/javascript/gsap/Draggable.min.js.map
  41. +11
    -0
      vendor/javascript/gsap/DrawSVGPlugin.min.js
  42. +1
    -0
      vendor/javascript/gsap/DrawSVGPlugin.min.js.map
  43. +11
    -0
      vendor/javascript/gsap/EasePack.min.js
  44. +1
    -0
      vendor/javascript/gsap/EasePack.min.js.map
  45. +11
    -0
      vendor/javascript/gsap/EaselPlugin.min.js
  46. +1
    -0
      vendor/javascript/gsap/EaselPlugin.min.js.map
  47. +11
    -0
      vendor/javascript/gsap/Flip.min.js
  48. +1
    -0
      vendor/javascript/gsap/Flip.min.js.map
  49. +11
    -0
      vendor/javascript/gsap/GSDevTools.min.js
  50. +1
    -0
      vendor/javascript/gsap/GSDevTools.min.js.map
  51. +11
    -0
      vendor/javascript/gsap/InertiaPlugin.min.js
  52. +1
    -0
      vendor/javascript/gsap/InertiaPlugin.min.js.map
  53. +11
    -0
      vendor/javascript/gsap/MorphSVGPlugin.min.js
  54. +1
    -0
      vendor/javascript/gsap/MorphSVGPlugin.min.js.map
  55. +11
    -0
      vendor/javascript/gsap/MotionPathHelper.min.js
  56. +1
    -0
      vendor/javascript/gsap/MotionPathHelper.min.js.map
  57. +11
    -0
      vendor/javascript/gsap/MotionPathPlugin.min.js
  58. +1
    -0
      vendor/javascript/gsap/MotionPathPlugin.min.js.map
  59. +11
    -0
      vendor/javascript/gsap/Observer.min.js
  60. +1
    -0
      vendor/javascript/gsap/Observer.min.js.map
  61. +11
    -0
      vendor/javascript/gsap/Physics2DPlugin.min.js
  62. +1
    -0
      vendor/javascript/gsap/Physics2DPlugin.min.js.map
  63. +11
    -0
      vendor/javascript/gsap/PhysicsPropsPlugin.min.js
  64. +1
    -0
      vendor/javascript/gsap/PhysicsPropsPlugin.min.js.map
  65. +11
    -0
      vendor/javascript/gsap/PixiPlugin.min.js
  66. +1
    -0
      vendor/javascript/gsap/PixiPlugin.min.js.map
  67. +11
    -0
      vendor/javascript/gsap/ScrambleTextPlugin.min.js
  68. +1
    -0
      vendor/javascript/gsap/ScrambleTextPlugin.min.js.map
  69. +11
    -0
      vendor/javascript/gsap/ScrollSmoother.min.js
  70. +1
    -0
      vendor/javascript/gsap/ScrollSmoother.min.js.map
  71. +11
    -0
      vendor/javascript/gsap/ScrollToPlugin.min.js
  72. +1
    -0
      vendor/javascript/gsap/ScrollToPlugin.min.js.map
  73. +11
    -0
      vendor/javascript/gsap/ScrollTrigger.min.js
  74. +1
    -0
      vendor/javascript/gsap/ScrollTrigger.min.js.map
  75. +11
    -0
      vendor/javascript/gsap/SplitText.min.js
  76. +1
    -0
      vendor/javascript/gsap/SplitText.min.js.map
  77. +11
    -0
      vendor/javascript/gsap/TextPlugin.min.js
  78. +1
    -0
      vendor/javascript/gsap/TextPlugin.min.js.map
  79. +11
    -0
      vendor/javascript/gsap/gsap.min.js
  80. +1
    -0
      vendor/javascript/gsap/gsap.min.js.map

+ 2
- 2
Gemfile.lock View File

@ -130,7 +130,7 @@ GEM
jbuilder (2.13.0) jbuilder (2.13.0)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
json (2.12.0)
json (2.12.2)
kaminari (1.2.2) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2) kaminari-actionview (= 1.2.2)
@ -289,7 +289,7 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)


+ 84
- 34
app/assets/stylesheets/application.css View File

@ -92,10 +92,6 @@
--fs-3xl: 4.0rem; --fs-3xl: 4.0rem;
--fs-4xl: 9.0rem; --fs-4xl: 9.0rem;
--flip-deg: 0deg;
--flip-scale: 1;
--flip-rotate: 0deg;
font: 10px/1.3 var(--ff-ikea); font: 10px/1.3 var(--ff-ikea);
} }
@ -107,7 +103,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
min-height: 100svh;
min-height: 100dvh;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
touch-action: manipulation; touch-action: manipulation;
@ -116,10 +112,6 @@ body {
} }
} }
ul[class] { ul[class] {
margin: 0 auto; margin: 0 auto;
padding: 0; padding: 0;
@ -210,7 +202,6 @@ footer {
} }
} }
.button__base { .button__base {
appearance: none; appearance: none;
background-color: var(--clr-black); background-color: var(--clr-black);
@ -219,15 +210,16 @@ footer {
font-size: var(--fs-base); font-size: var(--fs-base);
font-family: var(--ff-ikea); font-family: var(--ff-ikea);
font-weight: 700; font-weight: 700;
line-height: 1.2;
border-radius: 400px;
height: 4.4rem;
border-radius: 4.4rem;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
display: inline-block;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1em;
box-sizing: border-box; box-sizing: border-box;
padding: 0.62em 1em;
} }
.button__base-light { .button__base-light {
@ -258,6 +250,12 @@ main {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
opacity: 0;
transition: opacity 0.4s ease-in-out;
&.loaded {
opacity: 1;
}
} }
} }
@ -271,7 +269,7 @@ main {
display: grid; display: grid;
grid-template-columns: 1; grid-template-columns: 1;
grid-template-rows: auto; grid-template-rows: auto;
gap: 8px;
/*gap: 8px;*/
} }
.intro-content-container, .intro-content-container,
@ -279,8 +277,8 @@ main {
display: grid; display: grid;
grid-template-columns: 1; grid-template-columns: 1;
grid-template-rows: auto; grid-template-rows: auto;
gap: 8px;
/*gap: 8px;*/
width: 100%;
} }
.intro-container, .intro-container,
@ -292,9 +290,10 @@ main {
.intro-content-header, .intro-content-header,
.intro-content-body { .intro-content-body {
padding: 1.6rem 1.2rem; padding: 1.6rem 1.2rem;
border-radius: 1.2rem;
border-radius: 1.2rem;
} }
.intro-content-header { .intro-content-header {
background-color: var(--clr-white); background-color: var(--clr-white);
font-size: var(--fs-2xl); font-size: var(--fs-2xl);
@ -308,12 +307,6 @@ main {
line-height: 1.4; line-height: 1.4;
} }
.intro-content-container,
.question-container {
& > :last-child {
margin-top: 4px;
}
}
.question-step { .question-step {
font-size: var(--fs-s); font-size: var(--fs-s);
@ -374,6 +367,10 @@ main {
} }
} }
& + .question-answer {
margin-top: 8px;
}
} }
.question-container { .question-container {
@ -415,16 +412,30 @@ main {
.answers-distribution { .answers-distribution {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 0.8rem;
gap: 8px;
height: 45dvh;
max-height: 300px;
& > div { & > div {
box-sizing: border-box;
padding: 1.6rem 1.2rem; padding: 1.6rem 1.2rem;
background-color: var(--clr-medium-yellow); background-color: var(--clr-medium-yellow);
border-radius: 1.2rem; border-radius: 1.2rem;
color: var(--clr-dark-yellow); color: var(--clr-dark-yellow);
font-size: var(--fs-s); font-size: var(--fs-s);
font-weight: 700; font-weight: 700;
line-height: 1.3;
line-height: 1.3;
align-self: end;
min-height: 160px;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: background-color 200ms ease-in-out, color 200ms ease-in-out;
&.answered { &.answered {
background-color: var(--clr-white); background-color: var(--clr-white);
@ -432,9 +443,14 @@ main {
} }
} }
& .explanation {
display: none;
}
& .percentage { & .percentage {
font-size: var(--fs-4xl); font-size: var(--fs-4xl);
line-height: 1;
line-height: 1;
opacity: 0;
&::after { &::after {
content: '%'; content: '%';
@ -455,21 +471,21 @@ main {
line-height: 1.1; line-height: 1.1;
margin: 0 0 0.4em 0; margin: 0 0 0.4em 0;
} }
}
& div:first-child {
font-weight: 700;
}
}
label.question-answer { label.question-answer {
display: flex; display: flex;
align-items: start; align-items: start;
min-height: 4.2em;
height: 10.8rem;
cursor: pointer; cursor: pointer;
padding: 1.6rem 1.2rem; padding: 1.6rem 1.2rem;
border-radius: 1.6rem; border-radius: 1.6rem;
background-color: var(--clr-white);
background-color: var(--clr-white);
& input { & input {
flex-grow: 1; flex-grow: 1;
@ -490,7 +506,41 @@ label {
cursor: pointer; cursor: pointer;
} }
.animation-element {
box-sizing: border-box;
opacity: 0;
padding-top: 8px;
&:last-child {
padding-top: 12px;
}
& > * {
box-sizing: border-box;
}
}
.typewriter-cursor::after {
content: '|';
animation: typewriter-blink 1s infinite;
color: currentColor;
position: absolute;
}
@keyframes typewriter-blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/*.cursor {
animation: blink 1000ms infinite;
color: var(--clr-black);
font-weight: 400;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}*/
@media (min-width: 815px) { @media (min-width: 815px) {


+ 3
- 0
app/controllers/answers_controller.rb View File

@ -32,6 +32,9 @@ private
params.require(:answer).permit( params.require(:answer).permit(
:value :value
) )
rescue ActionController::ParameterMissing
{}
end end


+ 30
- 1
app/controllers/questions_controller.rb View File

@ -33,7 +33,14 @@ class QuestionsController < ApplicationController
current_player.update(score: planet_score) current_player.update(score: planet_score)
redirect_to url_for(action: 'result')
quiz_result = QuizResult.create!(
player_name: current_player.name,
score: planet_score,
stats: current_player.stats,
locale: I18n.locale
)
redirect_to url_for(action: 'result', sid: quiz_result.share_id)
end end
@ -60,6 +67,28 @@ class QuestionsController < ApplicationController
end end
def shared_result
@quiz_result = QuizResult.find_by!(share_id: params[:id])
people_score = questions.count - @quiz_result.score
score_diff = people_score - @quiz_result.score
attachment_index = case
when score_diff >= 2
0 # People
when score_diff <= -2
1 # Planet
else
2 # Balanced
end
@result_attachment = result_node.attachments.offset(attachment_index).first
@stats_attachment = result_node.attachments.offset(3).first
end
end end

+ 14
- 2
app/helpers/questions_helper.rb View File

@ -1,5 +1,15 @@
module QuestionsHelper module QuestionsHelper
def decorate_divs_for_typewriting_effect(html)
doc = Nokogiri::HTML.fragment(html, 'utf-8')
doc.xpath('./*').each do |div|
div['class'] = 'typewriter-text'
end
doc.to_html.html_safe
end
def image_orientation(file_attachment) def image_orientation(file_attachment)
return nil unless file_attachment&.attached? return nil unless file_attachment&.attached?
@ -66,7 +76,8 @@ private
srcset: rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [800, nil])) + " 800w, " + srcset: rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [800, nil])) + " 800w, " +
rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [1600, nil])) + " 1600w, " + rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [1600, nil])) + " 1600w, " +
rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [2400, nil])) + " 2400w", rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [2400, nil])) + " 2400w",
sizes: "100vw"
sizes: "100vw",
'data-controller': "image"
) )
) )
end end
@ -79,7 +90,8 @@ private
srcset: rails_storage_proxy_path(asset.file.variant(resize_to_limit: [800, nil])) + " 800w, " + srcset: rails_storage_proxy_path(asset.file.variant(resize_to_limit: [800, nil])) + " 800w, " +
rails_storage_proxy_path(asset.file.variant(resize_to_limit: [1600, nil])) + " 1600w, " + rails_storage_proxy_path(asset.file.variant(resize_to_limit: [1600, nil])) + " 1600w, " +
rails_storage_proxy_path(asset.file.variant(resize_to_limit: [2400, nil])) + " 2400w", rails_storage_proxy_path(asset.file.variant(resize_to_limit: [2400, nil])) + " 2400w",
sizes: "100vw"
sizes: "100vw",
'data-controller': "image"
) )
end end
end end


+ 444
- 0
app/javascript/application.js View File

@ -1,10 +1,13 @@
import "@hotwired/turbo-rails" import "@hotwired/turbo-rails"
import { Application } from "@hotwired/stimulus" import { Application } from "@hotwired/stimulus"
import LocaleController from "locale_controller" import LocaleController from "locale_controller"
import ImageController from "image_controller"
const application = Application.start() const application = Application.start()
application.register("locale", LocaleController) application.register("locale", LocaleController)
application.register("image", ImageController)
// Configure Stimulus development experience // Configure Stimulus development experience
application.debug = false application.debug = false
@ -12,3 +15,444 @@ window.Stimulus = application
export { application } export { application }
var animationElements
var originalHeights
document.addEventListener('turbo:render', (event) => {
console.info('turbo:render')
let errorElements = document.querySelectorAll('.field_with_errors')
if (errorElements.length > 0) {
animationElements = document.querySelectorAll('.animation-element');
gsap.set(animationElements, {
opacity: 1
})
}
});
// document.addEventListener("turbo:before-stream-render", animateElements)
document.addEventListener("turbo:load", animateElements)
function animateElements(event) {
console.info(event.type)
// Copy link to clipboard
document.querySelectorAll('[data-share-title]').forEach((share_btn) => {
share_btn.addEventListener('click', (e) => {
if (navigator.share) {
navigator.share({
title: e.currentTarget.getAttribute('data-share-title'),
text: e.currentTarget.getAttribute('data-share-text'),
url: e.currentTarget.getAttribute('data-share-url'),
}).then(() => {
console.log('Thanks for sharing!');
})
.catch(console.error);
} else {
navigator.clipboard.writeText(window.location.href)
alert('URL copied to clipboard!')
}
})
})
originalHeights = []
animationElements = document.querySelectorAll('.animation-element');
animationElements.forEach((animationElement, index) => {
const height = animationElement.offsetHeight
originalHeights.push(height)
// console.log(`Element ${index + 1} height: ${height}px`)
})
gsap.set(animationElements, {
height: 0,
opacity: 0,
overflow: 'hidden'
})
let typewriterElements = document.querySelectorAll('.typewriter-text');
if (typewriterElements.length > 0) {
gsap.set(typewriterElements, {
opacity: 0
})
}
// Start the sequential animation
animateElementsSequentially();
}
function animateElementsSequentially(index = 0) {
if (index >= animationElements.length) {
console.log('All animations complete');
return;
}
// 'margin-top': (index == (animationElements.length-1) ? "12px" : "8px"),
gsap.to(animationElements[index], {
height: originalHeights[index],
opacity: 1,
duration: (0.004 * originalHeights[index]),
delay: (index > 0 ? 0.4 : 0),
ease: "power2.out",
onComplete: () => {
// Typewriter
const typewriterTexts = animationElements[index].querySelectorAll('.typewriter-text')
if (typewriterTexts.length > 0) {
const typewriter = typeWriterConsoleSequential(typewriterTexts[0].parentElement, {
baseSpeed: 20,
onAllComplete: () => {
// Start next element after typewriter finishes
animateElementsSequentially(index + 1);
}
})
typewriter.start()
return
}
// % Answer Distributions
const answerDistributions = animationElements[index].querySelectorAll('.answer-distribution')
if (answerDistributions.length > 0 ) {
const containerHeight = animationElements[index].offsetHeight - 8; // - padding
const percentages = Array.from(answerDistributions).map(bar => parseInt(bar.dataset.value));
const maxPercentage = Math.max(...percentages);
console.info(maxPercentage)
answerDistributions.forEach((bar, i) => {
const percentage = parseInt(bar.dataset.value);
const relativePercentage = (percentage / maxPercentage) * 100;
const calculatedHeight = (relativePercentage / 100) * containerHeight;
const computedStyle = window.getComputedStyle(bar);
const minHeight = parseInt(computedStyle.minHeight) || 0;
const targetHeight = Math.max(calculatedHeight, minHeight);
gsap.to(bar, {
height: targetHeight,
duration: 0.4,
ease: "power2.out",
delay: index * 0.1,
onAllComplete: () => {
if (bar.dataset.answered != undefined) {
bar.classList.add('answered')
}
gsap.to(bar.querySelectorAll('.percentage'), {
opacity: 1,
duration: 0.4,
onAllComplete: function() {
const explanationElement = this.targets()[0].nextElementSibling
if (explanationElement) {
explanationElement.style.display = 'block'
}
}
})
if (i == (answerDistributions.length - 1)) {
setTimeout(() => animateElementsSequentially(index + 1), 1000);
}
}
});
})
return
}
animateElementsSequentially(index + 1);
}
});
}
function typeWriterConsoleSequential(container, options = {}) {
const {
selector = '.typewriter-text',
baseSpeed = 60,
divDelay = 600,
showCursor = true,
onDivComplete = null,
onAllComplete = null
} = options;
const textDivs = container.querySelectorAll(selector);
if (textDivs.length === 0) return null;
let currentDivIndex = 0;
let isActive = false;
let animationId = null;
let timeoutIds = new Set(); // Track all timeouts for cleanup
// Cleanup function for Turbo navigation
function cleanup() {
isActive = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
// Clear all timeouts
timeoutIds.forEach(id => clearTimeout(id));
timeoutIds.clear();
}
// Set up Turbo event listeners for cleanup
function setupTurboListeners() {
const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload'];
turboEvents.forEach(eventName => {
document.addEventListener(eventName, cleanup, { once: false });
});
// Store cleanup function on container for manual cleanup
container._typewriterCleanup = cleanup;
}
// Enhanced setTimeout that tracks IDs
function safeSetTimeout(callback, delay) {
const id = setTimeout(() => {
timeoutIds.delete(id);
if (isActive) callback();
}, delay);
timeoutIds.add(id);
return id;
}
// Store original text and prepare divs
function initializeDivs() {
// Set container height to prevent layout shifts
const containerHeight = container.offsetHeight;
container.style.minHeight = `${containerHeight}px`;
textDivs.forEach(div => {
if (!div.dataset.originalText) {
div.dataset.originalText = div.textContent.trim();
}
// Use visibility instead of blanking content to maintain layout
div.style.visibility = 'hidden';
div.innerHTML = '';
div.style.opacity = '1';
});
// Force reflow to ensure styles are applied
container.offsetHeight;
}
function createCursorSpan() {
// No longer needed - cursor is now handled via CSS
return null;
}
function updateDivContent(div, text, withCursor = false) {
// Use textContent for better performance
div.textContent = text;
// Toggle cursor class instead of adding DOM element
if (withCursor && showCursor) {
div.classList.add('typewriter-cursor');
} else {
div.classList.remove('typewriter-cursor');
}
}
function animateNextDiv() {
if (currentDivIndex >= textDivs.length || !isActive) {
isActive = false;
if (onAllComplete) {
// Use setTimeout to avoid potential timing issues
requestAnimationFrame(() => {
safeSetTimeout(() => onAllComplete(container), 200);
});
}
return;
}
const currentDiv = textDivs[currentDivIndex];
const originalText = currentDiv.dataset.originalText;
const isLastDiv = currentDivIndex === textDivs.length - 1;
// Make div visible and prepare for typing
currentDiv.style.visibility = 'visible';
updateDivContent(currentDiv, '', showCursor);
currentDiv.classList.add('typing-active');
let charIndex = 0;
let nextCharTime = performance.now() + 200; // Initial delay
function typeFrame(currentTime) {
if (!isActive) {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
return;
}
if (currentTime >= nextCharTime && charIndex < originalText.length) {
charIndex++;
const currentText = originalText.substring(0, charIndex);
updateDivContent(currentDiv, currentText, true);
// Calculate delay for next character
let delay = baseSpeed + Math.random() * 30;
const char = originalText.charAt(charIndex - 1);
if (char === '.' || char === '!' || char === '?') {
delay += 250;
} else if (char === ',' || char === ';') {
delay += 120;
} else if (char === ' ') {
delay += 60;
}
nextCharTime = currentTime + delay;
}
// Check if current div is complete
if (charIndex >= originalText.length) {
currentDiv.classList.remove('typing-active');
// Final content update
updateDivContent(currentDiv, originalText, showCursor);
if (onDivComplete) {
onDivComplete(currentDiv, currentDivIndex, container);
}
currentDivIndex++;
if (isActive && currentDivIndex < textDivs.length) {
// Move to next div after delay
safeSetTimeout(() => {
if (isActive) {
// Remove cursor from previous div (except last one)
if (!isLastDiv) {
updateDivContent(currentDiv, originalText, false);
}
animateNextDiv();
}
}, divDelay);
} else if (isActive) {
// This was the last div
safeSetTimeout(() => {
if (onAllComplete) {
onAllComplete(container);
}
}, 200);
}
return;
}
// Continue animation
if (isActive) {
animationId = requestAnimationFrame(typeFrame);
}
}
// Start the animation
animationId = requestAnimationFrame(typeFrame);
}
// Initialize Turbo listeners
setupTurboListeners();
// Add required CSS for cursor pseudo-element
function addCursorStyles() {
const styleId = 'typewriter-cursor-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.typewriter-cursor::after {
content: '|';
animation: typewriter-blink 1s infinite;
color: currentColor;
}
@keyframes typewriter-blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
`;
document.head.appendChild(style);
}
}
// Initialize styles
addCursorStyles();
return {
start: () => {
if (isActive) return; // Prevent multiple starts
initializeDivs();
isActive = true;
currentDivIndex = 0;
// Use requestAnimationFrame for smoother start
requestAnimationFrame(() => {
if (isActive) {
animateNextDiv();
}
});
},
stop: () => {
cleanup();
},
reset: () => {
cleanup();
textDivs.forEach(div => {
if (div.dataset.originalText) {
div.style.visibility = 'visible';
updateDivContent(div, div.dataset.originalText, false);
div.classList.remove('typing-active');
div.classList.remove('typewriter-cursor');
}
});
currentDivIndex = 0;
},
// Manual cleanup method for explicit cleanup
destroy: () => {
cleanup();
// Remove Turbo event listeners
const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload'];
turboEvents.forEach(eventName => {
document.removeEventListener(eventName, cleanup);
});
// Remove cleanup reference
delete container._typewriterCleanup;
},
get isActive() { return isActive; }
};
}

+ 23
- 0
app/javascript/image_controller.js View File

@ -0,0 +1,23 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
//static targets = ["img"]
connect() {
if (this.element.complete && this.element.naturalHeight !== 0) {
this.element.classList.add('loaded')
} else {
this.element.addEventListener('load', function() {
this.classList.add('loaded')
})
}
}
disconnect() {
}
}

+ 13
- 7
app/javascript/locale_controller.js View File

@ -1,12 +1,10 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
export default class extends Controller { export default class extends Controller {
static targets = ["select", "current"] static targets = ["select", "current"]
static values = { url: String } static values = { url: String }
connect() {
connect() {
} }
disconnect() { disconnect() {
@ -32,19 +30,27 @@ export default class extends Controller {
}) })
.then (response => response.text()) .then (response => response.text())
.then(html => { .then(html => {
Turbo.renderStreamMessage(html) Turbo.renderStreamMessage(html)
document.documentElement.setAttribute('lang', this.selectTarget.value)
})
document.documentElement.setAttribute('lang', this.selectTarget.value)
requestAnimationFrame(() => {
document.querySelectorAll('.animation-element').forEach((animationElement) => {
animationElement.style.opacity = 1
})
})
})
.catch((err) => { .catch((err) => {
console.info('rejected', err) console.info('rejected', err)
}) })
} }
// Helper method to get CSRF token // Helper method to get CSRF token
getMetaValue(name) { getMetaValue(name) {
const element = document.head.querySelector(`meta[name="${name}"]`) const element = document.head.querySelector(`meta[name="${name}"]`)
return element.getAttribute("content") return element.getAttribute("content")
} }
}
}

+ 15
- 0
app/models/quiz_result.rb View File

@ -0,0 +1,15 @@
class QuizResult < ApplicationRecord
validates :share_id, presence: true, uniqueness: true
validates :player_name, :stats, :score, presence: true
before_validation :generate_share_id, on: :create
private
def generate_share_id
self.share_id = SecureRandom.urlsafe_base64(8) if share_id.blank?
# Ensure uniqueness (very unlikely collision but good practice)
generate_share_id if QuizResult.exists?(share_id: share_id)
end
end

+ 17
- 4
app/views/languages/_intro.html.erb View File

@ -1,7 +1,20 @@
<% @node.attachments.limit(2).each_with_index do |attachment, i| %> <% @node.attachments.limit(2).each_with_index do |attachment, i| %>
<%= tag.div attachment.body.html_safe, class: i == 0 ? "intro-content-header" : "intro-content-body" %>
<div class="animation-element">
<% if i == 0 %>
<div class="intro-content-header">
<%= decorate_divs_for_typewriting_effect(attachment.body) %>
</div>
<% else %>
<div class="intro-content-body">
<%= attachment.body.html_safe %>
</div>
<% end %>
</div>
<% end %> <% end %>
<%= link_to t('get_started'), url_for(controller: 'players', action: 'new', locale: I18n.locale), class: 'button__base' %>
<div class="animation-element">
<%= link_to tag.span(t('get_started')),
url_for(controller: 'players', action: 'new', locale: I18n.locale),
class: 'button__base' %>
</div>

+ 1
- 1
app/views/languages/index.html.erb View File

@ -10,7 +10,7 @@
<div class="intro-container"> <div class="intro-container">
<div data-controller="locale" data-locale-url-value="<%= url_for(controller: 'languages', action: 'update') %>">
<div class="animation-element" data-controller="locale" data-locale-url-value="<%= url_for(controller: 'languages', action: 'update') %>">
<div class="language__selector-select"> <div class="language__selector-select">
<%= select_tag :language, <%= select_tag :language,


+ 5
- 3
app/views/layouts/application.html.erb View File

@ -18,13 +18,15 @@
<link rel="apple-touch-icon" href="/ikea-favicon-300x300.png"> <link rel="apple-touch-icon" href="/ikea-favicon-300x300.png">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" sizes="16x16 32x32"> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" sizes="16x16 32x32">
<link rel="icon" sizes="192x192" href="/ikea-favicon-300x300.png"> <link rel="icon" sizes="192x192" href="/ikea-favicon-300x300.png">
<%= stylesheet_link_tag "application" %> <%= stylesheet_link_tag "application" %>
<%= frontend_javascript_importmap_tags %w'application @hotwired/turbo-rails @hotwired/stimulus locale_controller' %>
<%= javascript_include_tag 'gsap/gsap.min.js' %>
<%= frontend_javascript_importmap_tags %w'application @hotwired/turbo-rails @hotwired/stimulus locale_controller image_controller' %>
</head> </head>
<body> <body>
<header> <header>
<%= link_to svg('ikea-foundation-203x22'), root_url %>
<%= link_to svg('ikea-foundation-203x22'), url_for(controller: 'languages', action: 'show') %>
</header> </header>
<main> <main>


+ 16
- 11
app/views/players/new.html.erb View File

@ -7,27 +7,32 @@
<div class="question-container"> <div class="question-container">
<!--<div class="question-step">
<div>1/<%= questions_size %></div>
</div>-->
<div class="question-header">
<%= t 'what_is_your_name' %>
<div class="animation-element">
<div class="question-header">
<div class="typewriter-text"><%= t 'what_is_your_name' %></div>
</div>
</div> </div>
<% if form.object.errors.any? %> <% if form.object.errors.any? %>
<div class="animation-element">
<div class="error"> <div class="error">
<% form.object.errors.messages.each do |k, v| %> <% form.object.errors.messages.each do |k, v| %>
<%= v.join(', ') %> <%= v.join(', ') %>
<% end %> <% end %>
</div> </div>
<% end %>
</div>
<% end %>
<%= form.label :name, class: "question-answer" do %>
<%= form.text_field :name, placeholder: t('what_is_your_name') %>
<% end %>
<div class="animation-element">
<%= form.label :name, class: "question-answer" do %>
<%= form.text_field :name, placeholder: t('what_is_your_name') %>
<% end %>
</div>
<%= form.submit t('submit'), class: 'button__base' %>
<div class="animation-element">
<%= form.button tag.span(t('submit')),
class: 'button__base' %>
</div>
</div> </div>

+ 24
- 12
app/views/questions/answer.html.erb View File

@ -6,28 +6,40 @@
<div class="question-container"> <div class="question-container">
<div class="question-step">
<div><%= question_index %>/<%= questions_size %></div>
<div class="animation-element">
<div class="question-step">
<div><%= question_index %>/<%= questions_size %></div>
</div>
</div> </div>
<div class="answers-distribution">
<% question.answer_percentages.each do |k,v| %>
<%= tag.div class: (k == player_question_answer ? 'answered' : nil) do %>
<%= tag.div v, class: 'percentage' %>
<%= t 'of_people_worldwide_think_just_lik_you' if k == player_question_answer %>
<div class="animation-element">
<div class="answers-distribution">
<% question.answer_percentages.each do |k,v| %>
<%= tag.div data: { value: v, answered: (k == player_question_answer ? 1 : nil) }, class: 'answer-distribution' do %>
<%= tag.div v, class: 'percentage' %>
<%= tag.div t('of_people_worldwide_think_just_lik_you'), class: 'explanation' if k == player_question_answer %>
<% end %>
<% end %> <% end %>
<% end %>
</div>
</div> </div>
<div class="question-result">
<%= @question_answer.last.body.html_safe %>
<div class="animation-element">
<div class="question-result">
<%= @question_answer.last.body.html_safe %>
</div>
</div> </div>
<div class="animation-element">
<% if question == questions.last %> <% if question == questions.last %>
<%= button_to t('see_results'), url_for(controller: 'questions', action: 'score'), class: 'button__base' %>
<%= button_to tag.span(t('see_results')),
url_for(controller: 'questions', action: 'score'),
class: 'button__base' %>
<%# link_to t('see_results'), url_for(controller: 'questions', action: 'result') %> <%# link_to t('see_results'), url_for(controller: 'questions', action: 'result') %>
<% else %> <% else %>
<%= link_to t('next_question'), url_for(controller: 'questions', action: 'show', id: question_index.next), class: 'button__base' %>
<%= link_to tag.span(t('next_question')),
url_for(controller: 'questions', action: 'show', id: question_index.next),
class: 'button__base' %>
<% end %> <% end %>
</div>
</div> </div>

+ 24
- 11
app/views/questions/result.html.erb View File

@ -6,23 +6,36 @@
<div class="question-container"> <div class="question-container">
<div class="question-step">
<div><%= t 'results' %></div>
<div class="animation-element">
<div class="question-step">
<div><%= t 'results' %></div>
</div>
</div> </div>
<div class="result-msg">
<%= @result_attachment&.body&.sub("&lt;name&gt;", html_escape(current_player.name)).html_safe %>
<div class="animation-element">
<div class="result-msg">
<%= decorate_divs_for_typewriting_effect @result_attachment&.body&.sub("&lt;name&gt;", html_escape(current_player.name)).html_safe %>
</div>
</div> </div>
<div class="result-stats">
<%= @stats_attachment&.body&.sub("&lt;score&gt;", current_player.stats).html_safe %>
<div class="animation-element">
<div class="result-stats">
<%= @stats_attachment&.body&.sub("&lt;score&gt;", current_player.stats).html_safe %>
</div>
</div> </div>
<div class="result-actions">
<%= link_to t('read_more'), "#", class: 'button__base' %>
<%= button_tag t('share_on_story'), type: 'button', class: 'button__base' %>
<div class="animation-element">
<div class="result-actions">
<%= link_to tag.span(t('read_more')), "#", class: 'button__base' %>
<%= button_tag tag.span(t('share_on_story')),
data: {
share_title: t('share.title'),
share_text: t('share.text'),
share_url: url_for(controller: 'questions', action: 'shared_result', id: params[:sid], only_path: false)
},
type: 'button',
class: 'button__base' %>
</div>
</div> </div>
</div> </div>

+ 41
- 0
app/views/questions/shared_result.html.erb View File

@ -0,0 +1,41 @@
<%-
content_for :title, result_node.page_title.blank? ? result_node.title : result_node.page_title
content_for :meta_description, result_node.page_description
%>
<div class="question-container">
<div class="animation-element">
<div class="question-step">
<div><%= t 'results' %></div>
</div>
</div>
<div class="animation-element">
<div class="result-msg">
<%= decorate_divs_for_typewriting_effect @result_attachment&.body&.sub("&lt;name&gt;", html_escape(@quiz_result.player_name)).html_safe %>
</div>
</div>
<div class="animation-element">
<div class="result-stats">
<%= @stats_attachment&.body&.sub("&lt;score&gt;", @quiz_result.stats).html_safe %>
</div>
</div>
<div class="animation-element">
<div class="result-actions">
<%= link_to tag.span(t('read_more')), "#", class: 'button__base' %>
<%# button_tag tag.span(t('share_on_story')),
data: {
share_title: '',
share_text: '',
share_url: url_for(controller: 'questions', action: 'shared_result', id: params[:sid])
},
type: 'button',
class: 'button__base' %>
</div>
</div>
</div>

+ 20
- 13
app/views/questions/show.html.erb View File

@ -19,25 +19,32 @@
<div class="question-container"> <div class="question-container">
<div class="question-step">
<div><%= question_index %>/<%= questions_size %></div>
<div class="animation-element">
<div class="question-step">
<div><%= question_index %>/<%= questions_size %></div>
</div>
</div> </div>
<div class="question-header">
<%= question.title %>
<div class="animation-element">
<div class="question-header">
<div class="typewriter-text"><%= question.title %></div>
</div>
</div> </div>
<% question.attachments.with_text.each_slice(2).each_with_index do |answer_option, i| %>
<div class="question-answer">
<%= form.label :value, for: nil do %>
<%= answer_option.first.body.html_safe %>
<%= form.radio_button :value, i %>
<%- end -%>
<div class="animation-element">
<% question.attachments.with_text.each_slice(2).each_with_index do |answer_option, i| %>
<div class="question-answer">
<%= form.label :value, for: nil do %>
<%= answer_option.first.body.html_safe %>
<%= form.radio_button :value, i %>
<%- end -%>
</div>
<% end %>
</div> </div>
<% end %>
<%= form.submit t('submit'), class: 'button__base' %>
<div class="animation-element">
<%= form.button tag.span(t('submit')), class: 'button__base' %>
</div>
</div> </div>
<% end %> <% end %>

+ 1
- 0
config/importmap.rb View File

@ -15,3 +15,4 @@ pin "trix" # @2.1.15
# site_helper # site_helper
pin "application", preload: false pin "application", preload: false
pin "locale_controller", preload: false pin "locale_controller", preload: false
pin "image_controller", preload: false

+ 4
- 0
config/locales/en.yml View File

@ -15,6 +15,10 @@ en:
share_on_story: Share share_on_story: Share
please_type_your_name_to_continue: Please type your name to continue please_type_your_name_to_continue: Please type your name to continue
share:
title: IKEA Foundation Week quiz result
text: Quiz result description
languages: languages:
en: English en: English


+ 1
- 1
config/routes.rb View File

@ -61,9 +61,9 @@ Rails.application.routes.draw do
get 'q/:id', to: 'questions#show' get 'q/:id', to: 'questions#show'
post 'score', to: 'questions#score' post 'score', to: 'questions#score'
get 'result/:id', to: 'questions#shared_result'
get 'result', to: 'questions#result' get 'result', to: 'questions#result'
get '', to: 'languages#show' get '', to: 'languages#show'
# get '*url', to: 'site#page', constraints: lambda { |req| req.path.exclude?('storage') } # get '*url', to: 'site#page', constraints: lambda { |req| req.path.exclude?('storage') }
end end


+ 15
- 0
db/migrate/20250527114853_create_quiz_results.rb View File

@ -0,0 +1,15 @@
class CreateQuizResults < ActiveRecord::Migration[8.0]
def change
create_table :quiz_results do |t|
t.string :share_id, null: false
t.string :player_name, null: false
t.string :stats, null: false
t.string :locale, null: false
t.integer :score, null: false
t.timestamps
end
add_index :quiz_results, :share_id, unique: true
end
end

+ 12
- 1
db/schema.rb View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_05_22_111116) do
ActiveRecord::Schema[8.0].define(version: 2025_05_27_114853) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@ -125,6 +125,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_22_111116) do
t.index ["score"], name: "index_players_on_score" t.index ["score"], name: "index_players_on_score"
end end
create_table "quiz_results", force: :cascade do |t|
t.string "share_id", null: false
t.string "player_name", null: false
t.string "stats", null: false
t.string "locale", null: false
t.integer "score", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["share_id"], name: "index_quiz_results_on_share_id", unique: true
end
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.text "email" t.text "email"
t.text "password_digest" t.text "password_digest"


+ 2
- 3
public/400.html View File

@ -19,9 +19,8 @@
align-items: center; align-items: center;
} }
body {
min-height: 100vh;
min-height: 100svh;
body {
min-height: 100dvh;
} }
h2 { h2 {


+ 1
- 2
public/404.html View File

@ -21,8 +21,7 @@
} }
body { body {
min-height: 100vh;
min-height: 100svh;
min-height: 100dvh;
} }
h2 { h2 {


+ 1
- 2
public/406-unsupported-browser.html View File

@ -17,8 +17,7 @@
} }
body { body {
min-height: 100vh;
min-height: 100svh;
min-height: 100dvh;
} }
h2 { h2 {


+ 1
- 2
public/422.html View File

@ -20,8 +20,7 @@
} }
body { body {
min-height: 100vh;
min-height: 100svh;
min-height: 100dvh;
} }
h2 { h2 {


+ 1
- 2
public/500.html View File

@ -20,8 +20,7 @@
} }
body { body {
min-height: 100vh;
min-height: 100svh;
min-height: 100dvh;
} }
h2 { h2 {


+ 11
- 0
test/fixtures/quiz_results.yml View File

@ -0,0 +1,11 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
# This model initially had no columns defined. If you add columns to the
# model remove the "{}" from the fixture names and add the columns immediately
# below each fixture, per the syntax in the comments below
#
one: {}
# column: value
#
two: {}
# column: value

+ 7
- 0
test/models/quiz_result_test.rb View File

@ -0,0 +1,7 @@
require "test_helper"
class QuizResultTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

BIN
vendor/javascript/gsap/.DS_Store View File


+ 11
- 0
vendor/javascript/gsap/CSSRulePlugin.min.js View File

@ -0,0 +1,11 @@
/*!
* CSSRulePlugin 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function h(){return"undefined"!=typeof window}function i(){return t||h()&&(t=window.gsap)&&t.registerPlugin&&t}function j(){return n||(s(),o||console.warn("Please gsap.registerPlugin(CSSPlugin, CSSRulePlugin)")),n}var t,n,c,o,s=function _initCore(e){t=e||i(),h()&&(c=document),t&&(o=t.plugins.css)&&(n=1)},r={version:"3.13.0",name:"cssRule",init:function init(e,t,n,i,s){if(!j()||void 0===e.cssText)return!1;var r=e._gsProxy=e._gsProxy||c.createElement("div");this.ss=e,this.style=r.style,r.style.cssText=e.cssText,o.prototype.init.call(this,r,t,n,i,s)},render:function render(e,t){for(var n,i=t._pt,s=t.style,r=t.ss;i;)i.r(e,i.d),i=i._next;for(n=s.length;-1<--n;)r[s[n]]=s[s[n]]},getRule:function getRule(e){j();var t,n,i,s,r=c.all?"rules":"cssRules",o=c.styleSheets,l=o.length,u=":"===e.charAt(0);for(e=(u?"":",")+e.split("::").join(":").toLowerCase()+",",u&&(s=[]);l--;){try{if(!(n=o[l][r]))continue;t=n.length}catch(e){console.warn(e);continue}for(;-1<--t;)if((i=n[t]).selectorText&&-1!==(","+i.selectorText.split("::").join(":").toLowerCase()+",").indexOf(e)){if(!u)return i.style;s.push(i.style)}}return s},register:s};i()&&t.registerPlugin(r),e.CSSRulePlugin=r,e.default=r;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/CSSRulePlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/CustomBounce.min.js View File

@ -0,0 +1,11 @@
/*!
* CustomBounce 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e=e||self).window=e.window||{})}(this,function(e){"use strict";function g(){return n||"undefined"!=typeof window&&(n=window.gsap)&&n.registerPlugin&&n}function h(e){n=g(),(j=n&&n.parseEase("_CE"))?(b=1,n.parseEase("bounce").config=function(e){return"object"==typeof e?t("",e):t("bounce("+e+")",{strength:+e})}):e&&console.warn("Please gsap.registerPlugin(CustomEase, CustomBounce)")}function i(e){var n,t=e.length,o=1/e[t-2];for(n=2;n<t;n+=2)e[n]=~~(e[n]*o*1e3)/1e3;e[t-2]=1}var n,b,j,t=function _create(e,n){b||h(1),n=n||{};var t,o,u,s,r,f,c,a=Math.min(.999,n.strength||.7),g=a,d=(n.squash||0)/100,p=d,l=1/.03,m=.2,C=1,w=.1,y=[0,0,.07,0,.1,1,.1,1],B=[0,0,0,0,.1,0,.1,0];for(r=0;r<200&&(f=w+(m*=g*((g+1)/2)),s=1-(C*=a*a),o=(u=w+.49*m)+.8*(u-(t=w+C/l)),d&&(w+=d,t+=d,u+=d,o+=d,f+=d,c=d/p,B.push(w-d,0,w-d,c,w-d/2,c,w,c,w,0,w,0,w,-.6*c,w+(f-w)/6,0,f,0),y.push(w-d,1,w,1,w,1),d*=a*a),y.push(w,1,t,s,u,s,o,s,f,1,f,1),a*=.95,l=C/(f-o),w=f,!(.999<s));r++);if(n.endAtStart&&"false"!==n.endAtStart){if(u=-.1,y.unshift(u,1,u,1,-.07,0),p)for(u-=d=2.5*p,y.unshift(u,1,u,1,u,1),B.splice(0,6),B.unshift(u,0,u,0,u,1,u+d/2,1,u+d,1,u+d,0,u+d,0,u+d,-.6,u+d+.033,0),r=0;r<B.length;r+=2)B[r]-=u;for(r=0;r<y.length;r+=2)y[r]-=u,y[r+1]=1-y[r+1]}return d&&(i(B),B[2]="C"+B[2],j(n.squashID||e+"-squash","M"+B.join(","))),i(y),y[2]="C"+y[2],j(e,"M"+y.join(","))},o=(CustomBounce.create=function create(e,n){return t(e,n)},CustomBounce.register=function register(e){n=e,h()},CustomBounce);function CustomBounce(e,n){this.ease=t(e,n)}g()&&n.registerPlugin(o),o.version="3.13.0",e.CustomBounce=o,e.default=o;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/CustomBounce.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/CustomEase.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/CustomEase.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/CustomWiggle.min.js View File

@ -0,0 +1,11 @@
/*!
* CustomWiggle 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function g(){return n||"undefined"!=typeof window&&(n=window.gsap)&&n.registerPlugin&&n}function i(e){return e}function j(e){if(!C)if(n=g(),M=n&&n.parseEase("_CE")){for(var t in y)y[t]=M("",y[t]);C=1,o("wiggle").config=function(e){return"object"==typeof e?o("",e):o("wiggle("+e+")",{wiggles:+e})}}else e&&console.warn("Please gsap.registerPlugin(CustomEase, CustomWiggle)")}function k(t,e){return"function"!=typeof t&&(t=n.parseEase(t)||M("",t)),t.custom||!e?t:function(e){return 1-t(e)}}var n,C,M,y={easeOut:"M0,1,C0.7,1,0.6,0,1,0",easeInOut:"M0,0,C0.1,0,0.24,1,0.444,1,0.644,1,0.6,0,1,0",anticipate:"M0,0,C0,0.222,0.024,0.386,0,0.4,0.18,0.455,0.65,0.646,0.7,0.67,0.9,0.76,1,0.846,1,1",uniform:"M0,0,C0,0.95,0,1,0,1,0,1,1,1,1,1,1,1,1,0,1,0"},o=function _create(e,t){C||j(1);var n,o,s,u,r,a,g,f,l,c=0|((t=t||{}).wiggles||10),p=1/c,d=p/2,m="anticipate"===t.type,h=y[t.type]||y.easeOut,w=i;if(m&&(w=h,h=y.easeOut),t.timingEase&&(w=k(t.timingEase)),t.amplitudeEase&&(h=k(t.amplitudeEase,!0)),f=[0,0,(a=w(d))/4,0,a/2,g=m?-h(d):h(d),a,g],"random"===t.type){for(f.length=4,n=w(p),o=2*Math.random()-1,l=2;l<c;l++)d=n,g=o,n=w(p*l),o=2*Math.random()-1,s=Math.atan2(o-f[f.length-3],n-f[f.length-4]),u=Math.cos(s)*p,r=Math.sin(s)*p,f.push(d-u,g-r,d,g,d+u,g+r);f.push(n,0,1,0)}else{for(l=1;l<c;l++)f.push(w(d+p/2),g),d+=p,g=(0<g?-1:1)*h(l*p),a=w(d),f.push(w(d-p/2),g,a,g);f.push(w(d+p/4),g,w(d+p/4),0,1,0)}for(l=f.length;-1<--l;)f[l]=~~(1e3*f[l])/1e3;return f[2]="C"+f[2],M(e,"M"+f.join(","))},t=(CustomWiggle.create=function create(e,t){return o(e,t)},CustomWiggle.register=function register(e){n=e,j()},CustomWiggle);function CustomWiggle(e,t){this.ease=o(e,t)}g()&&n.registerPlugin(t),t.version="3.13.0",e.CustomWiggle=t,e.default=t;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/CustomWiggle.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/Draggable.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/Draggable.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/DrawSVGPlugin.min.js View File

@ -0,0 +1,11 @@
/*!
* DrawSVGPlugin 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function l(){return"undefined"!=typeof window}function m(){return t||l()&&(t=window.gsap)&&t.registerPlugin&&t}function p(e){return Math.round(1e4*e)/1e4}function q(e){return parseFloat(e)||0}function r(e,t){var r=q(e);return~e.indexOf("%")?r/100*t:r}function s(e,t){return q(e.getAttribute(t))}function u(e,t,r,n,s,i){return D(Math.pow((q(r)-q(e))*s,2)+Math.pow((q(n)-q(t))*i,2))}function v(e){return console.warn(e)}function w(e){return"non-scaling-stroke"===e.getAttribute("vector-effect")}function z(e){if(!(e=k(e)[0]))return 0;var t,r,n,i,o,a,f,h=e.tagName.toLowerCase(),l=e.style,d=1,c=1;w(e)&&(c=e.getScreenCTM(),d=D(c.a*c.a+c.b*c.b),c=D(c.d*c.d+c.c*c.c));try{r=e.getBBox()}catch(e){v("Some browsers won't measure invisible elements (like display:none or masks inside defs).")}var g=r||{x:0,y:0,width:0,height:0},_=g.x,y=g.y,x=g.width,m=g.height;if(r&&(x||m)||!M[h]||(x=s(e,M[h][0]),m=s(e,M[h][1]),"rect"!==h&&"line"!==h&&(x*=2,m*=2),"line"===h&&(_=s(e,"x1"),y=s(e,"y1"),x=Math.abs(x-_),m=Math.abs(m-y))),"path"===h)i=l.strokeDasharray,l.strokeDasharray="none",t=e.getTotalLength()||0,p(d)!==p(c)&&!b&&(b=1)&&v("Warning: <path> length cannot be measured when vector-effect is non-scaling-stroke and the element isn't proportionally scaled."),t*=(d+c)/2,l.strokeDasharray=i;else if("rect"===h)t=2*x*d+2*m*c;else if("line"===h)t=u(_,y,_+x,y+m,d,c);else if("polyline"===h||"polygon"===h)for(n=e.getAttribute("points").match(P)||[],"polygon"===h&&n.push(n[0],n[1]),t=0,o=2;o<n.length;o+=2)t+=u(n[o-2],n[o-1],n[o],n[o+1],d,c)||0;else"circle"!==h&&"ellipse"!==h||(a=x/2*d,f=m/2*c,t=Math.PI*(3*(a+f)-D((3*a+f)*(a+3*f))));return t||0}function A(e,t){if(!(e=k(e)[0]))return[0,0];t=t||z(e)+1;var r=f.getComputedStyle(e),n=r.strokeDasharray||"",s=q(r.strokeDashoffset),i=n.indexOf(",");return i<0&&(i=n.indexOf(" ")),t<(n=i<0?t:q(n.substr(0,i)))&&(n=t),[-s||0,n-s||0]}function B(){l()&&(f=window,d=t=m(),k=t.utils.toArray,c=t.core.getStyleSaver,g=t.core.reverting||function(){},h=-1!==((f.navigator||{}).userAgent||"").indexOf("Edge"))}var t,k,f,h,d,b,c,g,P=/[-+=\.]*\d+[\.e\-\+]*\d*[e\-\+]*\d*/gi,M={rect:["width","height"],circle:["r","r"],ellipse:["rx","ry"],line:["x2","y2"]},D=Math.sqrt,n={version:"3.13.0",name:"drawSVG",register:function register(e){t=e,B()},init:function init(e,t,n){if(!e.getBBox)return!1;d||B();var s,i,o,a=z(e);return this.styles=c&&c(e,"strokeDashoffset,strokeDasharray,strokeMiterlimit"),this.tween=n,this._style=e.style,this._target=e,t+""=="true"?t="0 100%":t?-1===(t+"").indexOf(" ")&&(t="0 "+t):t="0 0",i=function _parse(e,t,n){var s,i,o=e.indexOf(" ");return i=o<0?(s=void 0!==n?n+"":e,e):(s=e.substr(0,o),e.substr(o+1)),s=r(s,t),(i=r(i,t))<s?[i,s]:[s,i]}(t,a,(s=A(e,a))[0]),this._length=p(a),this._dash=p(s[1]-s[0]),this._offset=p(-s[0]),this._dashPT=this.add(this,"_dash",this._dash,p(i[1]-i[0]),0,0,0,0,0,1),this._offsetPT=this.add(this,"_offset",this._offset,p(-i[0]),0,0,0,0,0,1),h&&(o=f.getComputedStyle(e)).strokeLinecap!==o.strokeLinejoin&&(i=q(o.strokeMiterlimit),this.add(e.style,"strokeMiterlimit",i,i+.01)),this._live=w(e)||~(t+"").indexOf("live"),this._nowrap=~(t+"").indexOf("nowrap"),this._props.push("drawSVG"),1},render:function render(e,t){if(t.tween._time||!g()){var r,n,s,i,o=t._pt,a=t._style;if(o){for(t._live&&(r=z(t._target))!==t._length&&(n=r/t._length,t._length=r,t._offsetPT&&(t._offsetPT.s*=n,t._offsetPT.c*=n),t._dashPT?(t._dashPT.s*=n,t._dashPT.c*=n):t._dash*=n);o;)o.r(e,o.d),o=o._next;s=t._dash||e&&1!==e&&1e-4||0,r=t._length-s+.1,i=t._offset,s&&i&&s+Math.abs(i%t._length)>t._length-.05&&(i+=i<0?.005:-.005)&&(r+=.005),a.strokeDashoffset=s?i:i+.001,a.strokeDasharray=r<.1?"none":s?s+"px,"+(t._nowrap?999999:r)+"px":"0px, 999999px"}}else t.styles.revert()},getLength:z,getPosition:A};m()&&t.registerPlugin(n),e.DrawSVGPlugin=n,e.default=n;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/DrawSVGPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/EasePack.min.js View File

@ -0,0 +1,11 @@
/*!
* EasePack 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e=e||self).window=e.window||{})}(this,function(e){"use strict";function f(){return w||"undefined"!=typeof window&&(w=window.gsap)&&w.registerPlugin&&w}function g(e,n){return!!(void 0===e?n:e&&!~(e+"").indexOf("false"))}function h(e){if(w=e||f()){r=w.registerEase;var n,t=w.parseEase(),o=function createConfig(t){return function(e){var n=.5+e/2;t.config=function(e){return t(2*(1-e)*e*n+e*e)}}};for(n in t)t[n].config||o(t[n]);for(n in r("slow",a),r("expoScale",s),r("rough",u),c)"version"!==n&&w.core.globals(n,c[n])}}function i(e,n,t){var o=(e=Math.min(1,e||.7))<1?n||0===n?n:.7:0,r=(1-e)/2,i=r+e,a=g(t);return function(e){var n=e+(.5-e)*o;return e<r?a?1-(e=1-e/r)*e:n-(e=1-e/r)*e*e*e*n:i<e?a?1===e?0:1-(e=(e-i)/r)*e:n+(e-n)*(e=(e-i)/r)*e*e*e:a?1:n}}function j(n,e,t){var o=Math.log(e/n),r=e-n;return t=t&&w.parseEase(t),function(e){return(n*Math.exp(o*(t?t(e):e))-n)/r}}function k(e,n,t){this.t=e,this.v=n,t&&(((this.next=t).prev=this).c=t.v-n,this.gap=t.t-e)}function l(e){"object"!=typeof e&&(e={points:+e||20});for(var n,t,o,r,i,a,f,s=e.taper||"none",u=[],c=0,p=0|(+e.points||20),l=p,v=g(e.randomize,!0),d=g(e.clamp),h=w?w.parseEase(e.template):0,x=.4*(+e.strength||1);-1<--l;)n=v?Math.random():1/p*l,t=h?h(n):n,o="none"===s?x:"out"===s?(r=1-n)*r*x:"in"===s?n*n*x:n<.5?(r=2*n)*r*.5*x:(r=2*(1-n))*r*.5*x,v?t+=Math.random()*o-.5*o:l%2?t+=.5*o:t-=.5*o,d&&(1<t?t=1:t<0&&(t=0)),u[c++]={x:n,y:t};for(u.sort(function(e,n){return e.x-n.x}),a=new k(1,1,null),l=p;l--;)i=u[l],a=new k(i.x,i.y,a);return f=new k(0,0,a.t?a:a.next),function(e){var n=f;if(e>n.t){for(;n.next&&e>=n.t;)n=n.next;n=n.prev}else for(;n.prev&&e<=n.t;)n=n.prev;return(f=n).v+(e-n.t)/n.gap*n.c}}var w,r,a=i(.7);(a.ease=a).config=i;var s=j(1,2);s.config=j;var u=l();(u.ease=u).config=l;var c={SlowMo:a,RoughEase:u,ExpoScaleEase:s};for(var n in c)c[n].register=h,c[n].version="3.13.0";f()&&w.registerPlugin(a),e.EasePack=c,e.ExpoScaleEase=s,e.RoughEase=u,e.SlowMo=a,e.default=c;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/EasePack.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/EaselPlugin.min.js View File

@ -0,0 +1,11 @@
/*!
* EaselPlugin 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function k(){return"undefined"!=typeof window}function l(){return h||k()&&(h=window.gsap)&&h.registerPlugin&&h}function m(){return r||t&&t.createjs||t||{}}function n(e){return console.warn(e)}function o(e){var t=e.getBounds&&e.getBounds();t||(t=e.nominalBounds||{x:0,y:0,width:100,height:100},e.setBounds&&e.setBounds(t.x,t.y,t.width,t.height)),e.cache&&e.cache(t.x,t.y,t.width,t.height),n("EaselPlugin: for filters to display in EaselJS, you must call the object's cache() method first. GSAP attempted to use the target's getBounds() for the cache but that may not be completely accurate. "+e)}function p(e,t,r){(b=b||m().ColorFilter)||n("EaselPlugin error: The EaselJS ColorFilter JavaScript file wasn't loaded.");for(var i,l,s,u,a,f,c=e.filters||[],d=c.length;d--;)if(c[d]instanceof b){l=c[d];break}if(l||(l=new b,c.push(l),e.filters=c),s=l.clone(),null!=t.tint)i=h.utils.splitColor(t.tint),u=null!=t.tintAmount?+t.tintAmount:1,s.redOffset=i[0]*u,s.greenOffset=i[1]*u,s.blueOffset=i[2]*u,s.redMultiplier=s.greenMultiplier=s.blueMultiplier=1-u;else for(a in t)"exposure"!==a&&"brightness"!==a&&(s[a]=+t[a]);for(null!=t.exposure?(s.redOffset=s.greenOffset=s.blueOffset=255*(t.exposure-1),s.redMultiplier=s.greenMultiplier=s.blueMultiplier=1):null!=t.brightness&&(u=t.brightness-1,s.redOffset=s.greenOffset=s.blueOffset=0<u?255*u:0,s.redMultiplier=s.greenMultiplier=s.blueMultiplier=1-Math.abs(u)),d=8;d--;)l[a=M[d]]!==s[a]&&(f=r.add(l,a,l[a],s[a],0,0,0,0,0,1))&&(f.op="easel_colorFilter");r._props.push("easel_colorFilter"),e.cacheID||o(e)}function u(e,t){if(!(e instanceof Array&&t instanceof Array))return t;var r,i,n=[],l=0,o=0;for(r=0;r<4;r++){for(i=0;i<5;i++)o=4===i?e[l+4]:0,n[l+i]=e[l]*t[i]+e[l+1]*t[i+5]+e[l+2]*t[i+10]+e[l+3]*t[i+15]+o;l+=5}return n}function z(e,t,r){(d=d||m().ColorMatrixFilter)||n("EaselPlugin: The EaselJS ColorMatrixFilter JavaScript file wasn't loaded.");for(var i,l,s,a,f=e.filters||[],c=f.length;-1<--c;)if(f[c]instanceof d){s=f[c];break}for(s||(s=new d(w.slice()),f.push(s),e.filters=f),l=s.matrix,i=w.slice(),null!=t.colorize&&(i=function _colorize(e,t,r){isNaN(r)&&(r=1);var i=h.utils.splitColor(t),n=i[0]/255,l=i[1]/255,o=i[2]/255,s=1-r;return u([s+r*n*x,r*n*y,r*n*_,0,0,r*l*x,s+r*l*y,r*l*_,0,0,r*o*x,r*o*y,s+r*o*_,0,0,0,0,0,1,0],e)}(i,t.colorize,Number(t.colorizeAmount))),null!=t.contrast&&(i=function _setContrast(e,t){return isNaN(t)?e:u([t+=.01,0,0,0,128*(1-t),0,t,0,0,128*(1-t),0,0,t,0,128*(1-t),0,0,0,1,0],e)}(i,Number(t.contrast))),null!=t.hue&&(i=function _setHue(e,t){if(isNaN(t))return e;t*=Math.PI/180;var r=Math.cos(t),i=Math.sin(t);return u([x+r*(1-x)+i*-x,y+r*-y+i*-y,_+r*-_+i*(1-_),0,0,x+r*-x+.143*i,y+r*(1-y)+.14*i,_+r*-_+-.283*i,0,0,x+r*-x+i*-(1-x),y+r*-y+i*y,_+r*(1-_)+i*_,0,0,0,0,0,1,0,0,0,0,0,1],e)}(i,Number(t.hue))),null!=t.saturation&&(i=function _setSaturation(e,t){if(isNaN(t))return e;var r=1-t,i=r*x,n=r*y,l=r*_;return u([i+t,n,l,0,0,i,n+t,l,0,0,i,n,l+t,0,0,0,0,0,1,0],e)}(i,Number(t.saturation))),c=i.length;-1<--c;)i[c]!==l[c]&&(a=r.add(l,c,l[c],i[c],0,0,0,0,0,1))&&(a.op="easel_colorMatrixFilter");r._props.push("easel_colorMatrixFilter"),e.cacheID||o(),r._matrix=l}function A(e){h=e||l(),k()&&(t=window),h&&(g=1)}var h,g,t,r,b,d,M="redMultiplier,greenMultiplier,blueMultiplier,alphaMultiplier,redOffset,greenOffset,blueOffset,alphaOffset".split(","),w=[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0],x=.212671,y=.71516,_=.072169,i={version:"3.13.0",name:"easel",init:function init(e,t,r,i,l){var o,s,u,a,f,c,d;for(o in g||(A(),h||n("Please gsap.registerPlugin(EaselPlugin)")),this.target=e,t)if(f=t[o],"colorFilter"===o||"tint"===o||"tintAmount"===o||"exposure"===o||"brightness"===o)u||(p(e,t.colorFilter||t,this),u=!0);else if("saturation"===o||"contrast"===o||"hue"===o||"colorize"===o||"colorizeAmount"===o)a||(z(e,t.colorMatrixFilter||t,this),a=!0);else if("frame"===o){if("string"==typeof f&&"="!==f.charAt(1)&&(c=e.labels))for(d=0;d<c.length;d++)c[d].label===f&&(f=c[d].position);(s=this.add(e,"gotoAndStop",e.currentFrame,f,i,l,Math.round,0,0,1))&&(s.op=o)}else null!=e[o]&&this.add(e,o,"get",f)},render:function render(e,t){for(var r=t._pt;r;)r.r(e,r.d),r=r._next;t.target.cacheID&&t.target.updateCache()},register:A,registerCreateJS:function(e){r=e}};l()&&h.registerPlugin(i),e.EaselPlugin=i,e.default=i;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/EaselPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/Flip.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/Flip.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/GSDevTools.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/GSDevTools.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/InertiaPlugin.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/InertiaPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/MorphSVGPlugin.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/MorphSVGPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/MotionPathHelper.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/MotionPathHelper.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/MotionPathPlugin.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/MotionPathPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/Observer.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/Observer.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/Physics2DPlugin.min.js View File

@ -0,0 +1,11 @@
/*!
* Physics2DPlugin 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function j(){return t||"undefined"!=typeof window&&(t=window.gsap)&&t.registerPlugin&&t}function k(e){return Math.round(1e4*e)/1e4}function l(e){t=e||j(),v||(o=t.utils.getUnit,f=t.core.getStyleSaver,w=t.core.reverting||function(){},v=1)}function m(e,t,i,s,n){var r=e._gsap,a=r.get(e,t);this.p=t,this.set=r.set(e,t),this.s=this.val=parseFloat(a),this.u=o(a)||0,this.vel=i||0,this.v=this.vel/n,s||0===s?(this.acc=s,this.a=this.acc/(n*n)):this.acc=this.a=0}var t,v,o,f,w,u=Math.PI/180,i={version:"3.13.0",name:"physics2D",register:l,init:function init(e,t,i){v||l();var s=this,n=+t.angle||0,r=+t.velocity||0,a=+t.acceleration||0,o=t.xProp||"x",p=t.yProp||"y",c=t.accelerationAngle||0===t.accelerationAngle?+t.accelerationAngle:n;s.styles=f&&f(e,t.xProp&&"x"!==t.xProp?t.xProp+","+t.yProp:"transform"),s.target=e,s.tween=i,s.step=0,s.sps=30,t.gravity&&(a=+t.gravity,c=90),n*=u,c*=u,s.fr=1-(+t.friction||0),s._props.push(o,p),s.xp=new m(e,o,Math.cos(n)*r,Math.cos(c)*a,s.sps),s.yp=new m(e,p,Math.sin(n)*r,Math.sin(c)*a,s.sps),s.skipX=s.skipY=0},render:function render(e,t){var i,s,n,r,a,o,p=t.xp,l=t.yp,c=t.tween,v=t.target,f=t.step,u=t.sps,h=t.fr,d=t.skipX,g=t.skipY,y=c._from?c._dur-c._time:c._time;if(c._time||!w()){if(1===h)n=y*y*.5,i=p.s+p.vel*y+p.acc*n,s=l.s+l.vel*y+l.acc*n;else{for(r=o=(0|(y*=u))-f,o<0&&(p.v=p.vel/u,l.v=l.vel/u,p.val=p.s,l.val=l.s,r=o=(t.step=0)|y),a=y%1*h;o--;)p.v+=p.a,l.v+=l.a,p.v*=h,l.v*=h,p.val+=p.v,l.val+=l.v;i=p.val+p.v*a,s=l.val+l.v*a,t.step+=r}d||p.set(v,p.p,k(i)+p.u),g||l.set(v,l.p,k(s)+l.u)}else t.styles.revert()},kill:function kill(e){this.xp.p===e&&(this.skipX=1),this.yp.p===e&&(this.skipY=1)}};j()&&t.registerPlugin(i),e.Physics2DPlugin=i,e.default=i;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/Physics2DPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/PhysicsPropsPlugin.min.js View File

@ -0,0 +1,11 @@
/*!
* PhysicsPropsPlugin 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function i(){return t||"undefined"!=typeof window&&(t=window.gsap)&&t.registerPlugin&&t}function j(e){return Math.round(1e4*e)/1e4}function k(e){t=e||i(),p||(a=t.utils.getUnit,c=t.core.getStyleSaver,d=t.core.reverting||function(){},p=1)}function l(e,t,s,i,r,n){var o=e._gsap,f=o.get(e,t);this.p=t,this.set=o.set(e,t),this.s=this.val=parseFloat(f),this.u=a(f)||0,this.vel=s||0,this.v=this.vel/n,i||0===i?(this.acc=i,this.a=this.acc/(n*n)):this.acc=this.a=0,this.fr=1-(r||0)}var t,p,a,c,d,s={version:"3.13.0",name:"physicsProps",register:k,init:function init(e,t,s){p||k();var i,r=this;for(i in r.styles=c&&c(e),r.target=e,r.tween=s,r.step=0,r.sps=30,r.vProps=[],t){var n=t[i],o=n.velocity,f=n.acceleration,a=n.friction;(o||f)&&(r.vProps.push(new l(e,i,o,f,a,r.sps)),r._props.push(i),c&&r.styles.save(i),a&&(r.hasFr=1))}},render:function render(e,t){var s,i,r,n,o,f=t.vProps,l=t.tween,a=t.target,p=t.step,c=t.hasFr,u=t.sps,v=f.length,h=l._from?l._dur-l._time:l._time;if(l._time||!d())if(c){if((i=(0|(h*=u))-p)<0){for(;v--;)(s=f[v]).v=s.vel/u,s.val=s.s;v=f.length,t.step=p=0,i=0|h}for(r=h%1;v--;){for(s=f[v],n=i;n--;)s.v+=s.a,s.v*=s.fr,s.val+=s.v;s.set(a,s.p,j(s.val+s.v*r*s.fr)+s.u)}t.step+=i}else for(o=h*h*.5;v--;)(s=f[v]).set(a,s.p,j(s.s+s.vel*h+s.acc*o)+s.u);else t.styles.revert()},kill:function kill(e){for(var t=this.vProps,s=t.length;s--;)t[s].p===e&&t.splice(s,1)}};i()&&t.registerPlugin(s),e.PhysicsPropsPlugin=s,e.default=s;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/PhysicsPropsPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/PixiPlugin.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/PixiPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/ScrambleTextPlugin.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/ScrambleTextPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/ScrollSmoother.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/ScrollSmoother.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/ScrollToPlugin.min.js View File

@ -0,0 +1,11 @@
/*!
* ScrollToPlugin 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle, jack@greensock.com
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function l(){return"undefined"!=typeof window}function m(){return f||l()&&(f=window.gsap)&&f.registerPlugin&&f}function n(e){return"string"==typeof e}function o(e){return"function"==typeof e}function p(e,t){var o="x"===t?"Width":"Height",n="scroll"+o,r="client"+o;return e===T||e===i||e===c?Math.max(i[n],c[n])-(T["inner"+o]||i[r]||c[r]):e[n]-e["offset"+o]}function q(e,t){var o="scroll"+("x"===t?"Left":"Top");return e===T&&(null!=e.pageXOffset?o="page"+t.toUpperCase()+"Offset":e=null!=i[o]?i:c),function(){return e[o]}}function s(e,t){if(!(e=y(e)[0])||!e.getBoundingClientRect)return console.warn("scrollTo target doesn't exist. Using 0")||{x:0,y:0};var o=e.getBoundingClientRect(),n=!t||t===T||t===c,r=n?{top:i.clientTop-(T.pageYOffset||i.scrollTop||c.scrollTop||0),left:i.clientLeft-(T.pageXOffset||i.scrollLeft||c.scrollLeft||0)}:t.getBoundingClientRect(),l={x:o.left-r.left,y:o.top-r.top};return!n&&t&&(l.x+=q(t,"x")(),l.y+=q(t,"y")()),l}function t(e,t,o,r,l){return isNaN(e)||"object"==typeof e?n(e)&&"="===e.charAt(1)?parseFloat(e.substr(2))*("-"===e.charAt(0)?-1:1)+r-l:"max"===e?p(t,o)-l:Math.min(p(t,o),s(e,t)[o]-l):parseFloat(e)-l}function u(){f=m(),l()&&f&&"undefined"!=typeof document&&document.body&&(T=window,c=document.body,i=document.documentElement,y=f.utils.toArray,f.config({autoKillThreshold:7}),h=f.config(),a=1)}var f,a,T,i,c,y,h,v,r={version:"3.13.0",name:"scrollTo",rawVars:1,register:function register(e){f=e,u()},init:function init(e,r,l,i,s){a||u();var p=this,c=f.getProperty(e,"scrollSnapType");p.isWin=e===T,p.target=e,p.tween=l,r=function _clean(e,t,r,l){if(o(e)&&(e=e(t,r,l)),"object"!=typeof e)return n(e)&&"max"!==e&&"="!==e.charAt(1)?{x:e,y:e}:{y:e};if(e.nodeType)return{y:e,x:e};var i,s={};for(i in e)s[i]="onAutoKill"!==i&&o(e[i])?e[i](t,r,l):e[i];return s}(r,i,e,s),p.vars=r,p.autoKill=!!("autoKill"in r?r:h).autoKill,p.getX=q(e,"x"),p.getY=q(e,"y"),p.x=p.xPrev=p.getX(),p.y=p.yPrev=p.getY(),v=v||f.core.globals().ScrollTrigger,"smooth"===f.getProperty(e,"scrollBehavior")&&f.set(e,{scrollBehavior:"auto"}),c&&"none"!==c&&(p.snap=1,p.snapInline=e.style.scrollSnapType,e.style.scrollSnapType="none"),null!=r.x?(p.add(p,"x",p.x,t(r.x,e,"x",p.x,r.offsetX||0),i,s),p._props.push("scrollTo_x")):p.skipX=1,null!=r.y?(p.add(p,"y",p.y,t(r.y,e,"y",p.y,r.offsetY||0),i,s),p._props.push("scrollTo_y")):p.skipY=1},render:function render(e,t){for(var o,n,r,l,i,s=t._pt,c=t.target,u=t.tween,f=t.autoKill,a=t.xPrev,y=t.yPrev,d=t.isWin,g=t.snap,x=t.snapInline;s;)s.r(e,s.d),s=s._next;o=d||!t.skipX?t.getX():a,r=(n=d||!t.skipY?t.getY():y)-y,l=o-a,i=h.autoKillThreshold,t.x<0&&(t.x=0),t.y<0&&(t.y=0),f&&(!t.skipX&&(i<l||l<-i)&&o<p(c,"x")&&(t.skipX=1),!t.skipY&&(i<r||r<-i)&&n<p(c,"y")&&(t.skipY=1),t.skipX&&t.skipY&&(u.kill(),t.vars.onAutoKill&&t.vars.onAutoKill.apply(u,t.vars.onAutoKillParams||[]))),d?T.scrollTo(t.skipX?o:t.x,t.skipY?n:t.y):(t.skipY||(c.scrollTop=t.y),t.skipX||(c.scrollLeft=t.x)),!g||1!==e&&0!==e||(n=c.scrollTop,o=c.scrollLeft,x?c.style.scrollSnapType=x:c.style.removeProperty("scroll-snap-type"),c.scrollTop=n+1,c.scrollLeft=o+1,c.scrollTop=n,c.scrollLeft=o),t.xPrev=t.x,t.yPrev=t.y,v&&v.update()},kill:function kill(e){var t="scrollTo"===e,o=this._props.indexOf(e);return!t&&"scrollTo_x"!==e||(this.skipX=1),!t&&"scrollTo_y"!==e||(this.skipY=1),-1<o&&this._props.splice(o,1),!this._props.length}};r.max=p,r.getOffset=s,r.buildGetter=q,r.config=function(e){for(var t in h||u()||(h=f.config()),e)h[t]=e[t]},m()&&f.registerPlugin(r),e.ScrollToPlugin=r,e.default=r;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});

+ 1
- 0
vendor/javascript/gsap/ScrollToPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/ScrollTrigger.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/ScrollTrigger.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/SplitText.min.js View File

@ -0,0 +1,11 @@
/*!
* SplitText 3.13.0
* https://gsap.com
*
* @license Copyright 2025, GreenSock. All rights reserved. Subject to the terms at https://gsap.com/standard-license.
* @author: Jack Doyle
*/
(function(B,A){typeof exports=="object"&&typeof module!="undefined"?A(exports):typeof define=="function"&&define.amd?define(["exports"],A):(B=typeof globalThis!="undefined"?globalThis:B||self,A(B.window=B.window||{}))})(this,function(B){"use strict";let A,_,G,re=()=>G||U.register(window.gsap),V=typeof Intl!="undefined"?new Intl.Segmenter:0,P=e=>typeof e=="string"?P(document.querySelectorAll(e)):"length"in e?Array.from(e):[e],X=e=>P(e).filter(t=>t instanceof HTMLElement),J=[],K=function(){},oe=/\s+/g,Y=new RegExp("\\p{RI}\\p{RI}|\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?(\\u{200D}\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?)*|.","gu"),Z={left:0,top:0,width:0,height:0},ee=(e,t)=>{if(t){let s=new Set(e.join("").match(t)||J),i=e.length,o,c,n,a;if(s.size)for(;--i>-1;){c=e[i];for(n of s)if(n.startsWith(c)&&n.length>c.length){for(o=0,a=c;n.startsWith(a+=e[i+ ++o])&&a.length<n.length;);if(o&&a.length===n.length){e[i]=n,e.splice(i+1,o);break}}}}return e},te=e=>window.getComputedStyle(e).display==="inline"&&(e.style.display="inline-block"),z=(e,t,s)=>t.insertBefore(typeof e=="string"?document.createTextNode(e):e,s),Q=(e,t,s)=>{let i=t[e+"sClass"]||"",{tag:o="div",aria:c="auto",propIndex:n=!1}=t,a=e==="line"?"block":"inline-block",h=i.indexOf("++")>-1,b=x=>{let g=document.createElement(o),C=s.length+1;return i&&(g.className=i+(h?" "+i+C:"")),n&&g.style.setProperty("--"+e,C+""),c!=="none"&&g.setAttribute("aria-hidden","true"),o!=="span"&&(g.style.position="relative",g.style.display=a),g.textContent=x,s.push(g),g};return h&&(i=i.replace("++","")),b.collection=s,b},ae=(e,t,s,i)=>{let o=Q("line",s,i),c=window.getComputedStyle(e).textAlign||"left";return(n,a)=>{let h=o("");for(h.style.textAlign=c,e.insertBefore(h,t[n]);n<a;n++)h.appendChild(t[n]);h.normalize()}},ie=(e,t,s,i,o,c,n,a,h,b)=>{var x;let g=Array.from(e.childNodes),C=0,{wordDelimiter:R,reduceWhiteSpace:L=!0,prepareText:$}=t,q=e.getBoundingClientRect(),j=q,D=!L&&window.getComputedStyle(e).whiteSpace.substring(0,3)==="pre",E=0,v=s.collection,r,f,H,l,m,y,I,d,u,W,S,O,T,F,w,p,k,N;for(typeof R=="object"?(H=R.delimiter||R,f=R.replaceWith||""):f=R===""?"":R||" ",r=f!==" ";C<g.length;C++)if(l=g[C],l.nodeType===3){for(w=l.textContent||"",L?w=w.replace(oe," "):D&&(w=w.replace(/\n/g,f+`
`)),$&&(w=$(w,e)),l.textContent=w,m=f||H?w.split(H||f):w.match(a)||J,k=m[m.length-1],d=r?k.slice(-1)===" ":!k,k||m.pop(),j=q,I=r?m[0].charAt(0)===" ":!m[0],I&&z(" ",e,l),m[0]||m.shift(),ee(m,h),c&&b||(l.textContent=""),u=1;u<=m.length;u++)if(p=m[u-1],!L&&D&&p.charAt(0)===`
`&&((x=l.previousSibling)==null||x.remove(),z(document.createElement("br"),e,l),p=p.slice(1)),!L&&p==="")z(f,e,l);else if(p===" ")e.insertBefore(document.createTextNode(" "),l);else{if(r&&p.charAt(0)===" "&&z(" ",e,l),E&&u===1&&!I&&v.indexOf(E.parentNode)>-1?(y=v[v.length-1],y.appendChild(document.createTextNode(i?"":p))):(y=s(i?"":p),z(y,e,l),E&&u===1&&!I&&y.insertBefore(E,y.firstChild)),i)for(S=V?ee([...V.segment(p)].map(M=>M.segment),h):p.match(a)||J,N=0;N<S.length;N++)y.appendChild(S[N]===" "?document.createTextNode(" "):i(S[N]));if(c&&b){if(w=l.textContent=w.substring(p.length+1,w.length),W=y.getBoundingClientRect(),W.top>j.top&&W.left<=j.left){for(O=e.cloneNode(),T=e.childNodes[0];T&&T!==y;)F=T,T=T.nextSibling,O.appendChild(F);e.parentNode.insertBefore(O,e),o&&te(O)}j=W}(u<m.length||d)&&z(u>=m.length?" ":r&&p.slice(-1)===" "?" "+f:f,e,l)}e.removeChild(l),E=0}else l.nodeType===1&&(n&&n.indexOf(l)>-1?(v.indexOf(l.previousSibling)>-1&&v[v.length-1].appendChild(l),E=l):(ie(l,t,s,i,o,c,n,a,h,!0),E=0),o&&te(l))};const ne=class se{constructor(t,s){this.isSplit=!1,re(),this.elements=X(t),this.chars=[],this.words=[],this.lines=[],this.masks=[],this.vars=s,this._split=()=>this.isSplit&&this.split(this.vars);let i=[],o,c=()=>{let n=i.length,a;for(;n--;){a=i[n];let h=a.element.offsetWidth;if(h!==a.width){a.width=h,this._split();return}}};this._data={orig:i,obs:typeof ResizeObserver!="undefined"&&new ResizeObserver(()=>{clearTimeout(o),o=setTimeout(c,200)})},K(this),this.split(s)}split(t){this.isSplit&&this.revert(),this.vars=t=t||this.vars||{};let{type:s="chars,words,lines",aria:i="auto",deepSlice:o=!0,smartWrap:c,onSplit:n,autoSplit:a=!1,specialChars:h,mask:b}=this.vars,x=s.indexOf("lines")>-1,g=s.indexOf("chars")>-1,C=s.indexOf("words")>-1,R=g&&!C&&!x,L=h&&("push"in h?new RegExp("(?:"+h.join("|")+")","gu"):h),$=L?new RegExp(L.source+"|"+Y.source,"gu"):Y,q=!!t.ignore&&X(t.ignore),{orig:j,animTime:D,obs:E}=this._data,v;return(g||C||x)&&(this.elements.forEach((r,f)=>{j[f]={element:r,html:r.innerHTML,ariaL:r.getAttribute("aria-label"),ariaH:r.getAttribute("aria-hidden")},i==="auto"?r.setAttribute("aria-label",(r.textContent||"").trim()):i==="hidden"&&r.setAttribute("aria-hidden","true");let H=[],l=[],m=[],y=g?Q("char",t,H):null,I=Q("word",t,l),d,u,W,S;if(ie(r,t,I,y,R,o&&(x||R),q,$,L,!1),x){let O=P(r.childNodes),T=ae(r,O,t,m),F,w=[],p=0,k=O.map(M=>M.nodeType===1?M.getBoundingClientRect():Z),N=Z;for(d=0;d<O.length;d++)F=O[d],F.nodeType===1&&(F.nodeName==="BR"?(w.push(F),T(p,d+1),p=d+1,N=k[p]):(d&&k[d].top>N.top&&k[d].left<=N.left&&(T(p,d),p=d),N=k[d]));p<d&&T(p,d),w.forEach(M=>{var le;return(le=M.parentNode)==null?void 0:le.removeChild(M)})}if(!C){for(d=0;d<l.length;d++)if(u=l[d],g||!u.nextSibling||u.nextSibling.nodeType!==3)if(c&&!x){for(W=document.createElement("span"),W.style.whiteSpace="nowrap";u.firstChild;)W.appendChild(u.firstChild);u.replaceWith(W)}else u.replaceWith(...u.childNodes);else S=u.nextSibling,S&&S.nodeType===3&&(S.textContent=(u.textContent||"")+(S.textContent||""),u.remove());l.length=0,r.normalize()}this.lines.push(...m),this.words.push(...l),this.chars.push(...H)}),b&&this[b]&&this.masks.push(...this[b].map(r=>{let f=r.cloneNode();return r.replaceWith(f),f.appendChild(r),r.className&&(f.className=r.className.replace(/(\b\w+\b)/g,"$1-mask")),f.style.overflow="clip",f}))),this.isSplit=!0,_&&(a?_.addEventListener("loadingdone",this._split):_.status==="loading"&&console.warn("SplitText called before fonts loaded")),(v=n&&n(this))&&v.totalTime&&(this._data.anim=D?v.totalTime(D):v),x&&a&&this.elements.forEach((r,f)=>{j[f].width=r.offsetWidth,E&&E.observe(r)}),this}revert(){var t,s;let{orig:i,anim:o,obs:c}=this._data;return c&&c.disconnect(),i.forEach(({element:n,html:a,ariaL:h,ariaH:b})=>{n.innerHTML=a,h?n.setAttribute("aria-label",h):n.removeAttribute("aria-label"),b?n.setAttribute("aria-hidden",b):n.removeAttribute("aria-hidden")}),this.chars.length=this.words.length=this.lines.length=i.length=this.masks.length=0,this.isSplit=!1,_==null||_.removeEventListener("loadingdone",this._split),o&&(this._data.animTime=o.totalTime(),o.revert()),(s=(t=this.vars).onRevert)==null||s.call(t,this),this}static create(t,s){return new se(t,s)}static register(t){A=A||t||window.gsap,A&&(P=A.utils.toArray,K=A.core.context||K),!G&&window.innerWidth>0&&(_=document.fonts,G=!0)}};ne.version="3.13.0";let U=ne;B.SplitText=U,B.default=U,Object.defineProperty(B,"__esModule",{value:!0})});

+ 1
- 0
vendor/javascript/gsap/SplitText.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/TextPlugin.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/TextPlugin.min.js.map
File diff suppressed because it is too large
View File


+ 11
- 0
vendor/javascript/gsap/gsap.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
vendor/javascript/gsap/gsap.min.js.map
File diff suppressed because it is too large
View File


Loading…
Cancel
Save