Spaces:
Running
Running
| <script lang="ts"> | |
| import { onMount, onDestroy } from "svelte"; | |
| /** | |
| * Props for the MatrixEffect component. | |
| * @typedef {Object} Props | |
| * @property {Array<{title: string, name: string}>} credits - List of credits with title and name. | |
| * @property {number} speed - Animation speed in seconds (default: 20). | |
| * @property {number} base_font_size - Base font size in em (default: 1.0). | |
| * @property {string | null} intro_title - Optional intro title. | |
| * @property {string | null} intro_subtitle - Optional intro subtitle. | |
| * @property {"stacked" | "two-column"} layout_style - Layout for credits. | |
| * @property {boolean} title_uppercase - Transform title to uppercase. | |
| * @property {boolean} name_uppercase - Transform name to uppercase. | |
| * @property {boolean} section_title_uppercase - Transform section title to uppercase. | |
| * @property {boolean} swap_font_sizes_on_two_column - Swap title/name font sizes. | |
| * @property {{path: string | null, url: string | null, ...} | null} scroll_logo_path - Logo to display inside the scroll. | |
| * @property {string} scroll_logo_height - Height of the scrolling logo. | |
| */ | |
| export let credits: Props["credits"]; | |
| export let speed: number = 20; | |
| export let base_font_size: number = 1.0; | |
| export let intro_title: string | null = null; | |
| export let intro_subtitle: string | null = null; | |
| export let layout_style: "stacked" | "two-column" = "stacked"; | |
| export let title_uppercase: boolean = false; | |
| export let name_uppercase: boolean = false; | |
| export let section_title_uppercase: boolean = true; | |
| export let swap_font_sizes_on_two_column: boolean = false; | |
| export let scroll_logo_path: { url: string | null } | null = null; | |
| export let scroll_logo_height: string = "120px"; | |
| // Combines intro and credits for display | |
| $: display_items = (() => { | |
| const items = []; | |
| if (intro_title || intro_subtitle) { | |
| items.push({ | |
| title: intro_title || "", | |
| name: intro_subtitle || "", | |
| is_intro: true, | |
| }); | |
| } | |
| return [...items, ...credits.map((c) => ({ ...c, is_intro: false }))]; | |
| })(); | |
| // Reactive font size styles | |
| $: title_style = (is_intro: boolean) => | |
| `font-size: ${is_intro ? base_font_size * 1.5 : base_font_size}em;`; | |
| $: name_style = (is_intro: boolean) => | |
| `font-size: ${is_intro ? base_font_size * 0.9 : base_font_size * 0.8}em;`; | |
| $: section_title_style = `font-size: ${base_font_size * 1.2}em;`; | |
| // Canvas setup for Matrix effect | |
| let canvas: HTMLCanvasElement; | |
| let ctx: CanvasRenderingContext2D; | |
| let contentElement: HTMLElement | null; | |
| const fontSize = 16; | |
| const characters = | |
| "アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッン01"; | |
| let columns: number; | |
| let drops: number[] = []; | |
| let animationFrameId: number; | |
| // Initialize canvas and drops | |
| function setup() { | |
| if (!canvas) return; | |
| const parent = canvas.parentElement; | |
| if (parent) { | |
| canvas.width = parent.clientWidth; | |
| canvas.height = parent.clientHeight; | |
| } | |
| ctx = canvas.getContext("2d")!; | |
| columns = Math.floor(canvas.width / fontSize); | |
| drops = Array(columns).fill(1); | |
| } | |
| // Draw Matrix falling characters | |
| function drawMatrix() { | |
| if (!ctx) return; | |
| ctx.fillStyle = "rgba(0, 0, 0, 0.05)"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = "#0F0"; | |
| ctx.font = `${fontSize}px monospace`; | |
| for (let i = 0; i < drops.length; i++) { | |
| const text = characters.charAt( | |
| Math.floor(Math.random() * characters.length) | |
| ); | |
| ctx.fillText(text, i * fontSize, drops[i] * fontSize); | |
| if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) { | |
| drops[i] = 0; | |
| } | |
| drops[i]++; | |
| } | |
| animationFrameId = requestAnimationFrame(drawMatrix); | |
| } | |
| // Reset credits animation | |
| function resetCreditsAnimation() { | |
| if (contentElement) { | |
| contentElement.style.animation = "none"; | |
| void contentElement.offsetHeight; // Trigger reflow | |
| contentElement.style.animation = ""; | |
| } | |
| } | |
| // Setup canvas and animation on mount | |
| onMount(() => { | |
| setup(); | |
| drawMatrix(); | |
| resetCreditsAnimation(); | |
| const resizeObserver = new ResizeObserver(() => { | |
| cancelAnimationFrame(animationFrameId); | |
| setup(); | |
| drawMatrix(); | |
| }); | |
| if (canvas.parentElement) { | |
| resizeObserver.observe(canvas.parentElement); | |
| } | |
| return () => { | |
| cancelAnimationFrame(animationFrameId); | |
| if (canvas.parentElement) { | |
| resizeObserver.unobserve(canvas.parentElement); | |
| } | |
| }; | |
| }); | |
| // Reset animation on prop changes | |
| $: credits, | |
| speed, | |
| intro_title, | |
| intro_subtitle, | |
| layout_style, | |
| resetCreditsAnimation(); | |
| // Cleanup on destroy | |
| onDestroy(() => { | |
| contentElement = null; | |
| }); | |
| </script> | |
| <div class="matrix-container"> | |
| <canvas bind:this={canvas}></canvas> | |
| <div class="credits-scroll-overlay"> | |
| <div | |
| class="credits-content" | |
| bind:this={contentElement} | |
| style="--animation-duration: {speed}s;" | |
| > | |
| {#if scroll_logo_path?.url} | |
| <div class="scroll-logo-container"> | |
| <img src={scroll_logo_path.url} alt="Scrolling Logo" style:height={scroll_logo_height} /> | |
| </div> | |
| {/if} | |
| {#each display_items as item} | |
| <!-- Render Section Title --> | |
| {#if item.section_title} | |
| <div | |
| class="section-title" | |
| style={section_title_style} | |
| class:uppercase={section_title_uppercase} | |
| > | |
| {item.section_title} | |
| </div> | |
| <!-- Render Credit or Intro --> | |
| {:else if layout_style === "two-column" && !item.is_intro} | |
| <!-- Two-Column Layout --> | |
| <div class="credit-two-column"> | |
| <div | |
| class="title" | |
| style={swap_font_sizes_on_two_column | |
| ? name_style(false) | |
| : title_style(false)} | |
| class:uppercase={title_uppercase} | |
| > | |
| {item.title} | |
| </div> | |
| <div | |
| class="name" | |
| style={swap_font_sizes_on_two_column | |
| ? title_style(false) | |
| : name_style(false)} | |
| class:uppercase={name_uppercase} | |
| > | |
| {item.name} | |
| </div> | |
| </div> | |
| {:else} | |
| <!-- Stacked Layout --> | |
| <div class="credit-block" class:intro-block={item.is_intro}> | |
| <div | |
| style={title_style(item.is_intro)} | |
| class="title" | |
| class:uppercase={title_uppercase && !item.is_intro} | |
| > | |
| {item.title} | |
| </div> | |
| {#if item.name} | |
| <div | |
| style={name_style(item.is_intro)} | |
| class="name" | |
| class:uppercase={name_uppercase && !item.is_intro} | |
| > | |
| {item.name} | |
| </div> | |
| {/if} | |
| </div> | |
| {/if} | |
| {/each} | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| /* Container for Matrix effect */ | |
| .matrix-container { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* Canvas for falling characters */ | |
| canvas { | |
| display: block; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| } | |
| /* Overlay for scrolling credits */ | |
| .credits-scroll-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 2; | |
| color: #fff; | |
| font-family: monospace; | |
| text-align: center; | |
| -webkit-mask-image: linear-gradient( | |
| transparent, | |
| black 20%, | |
| black 80%, | |
| transparent | |
| ); | |
| mask-image: linear-gradient(transparent, black 20%, black 80%, transparent); | |
| } | |
| /* Scrolling credits container */ | |
| .credits-content { | |
| position: absolute; | |
| width: 100%; | |
| bottom: 0; | |
| transform: translateY(100%); | |
| animation: scroll-from-bottom var(--animation-duration) linear infinite; | |
| padding: 0 2rem; | |
| box-sizing: border-box; | |
| } | |
| @keyframes scroll-from-bottom { | |
| from { | |
| transform: translateY(100%); | |
| } | |
| to { | |
| transform: translateY(-100%); | |
| } | |
| } | |
| .uppercase { | |
| text-transform: uppercase; | |
| } | |
| .section-title { | |
| margin-top: 4rem; | |
| margin-bottom: 2.5rem; | |
| font-weight: bold; | |
| color: #5f5; | |
| text-shadow: 0 0 8px #0f0; | |
| } | |
| /* Intro block spacing */ | |
| .credit-block.intro-block { | |
| margin-bottom: 5rem; | |
| } | |
| /* Credit block spacing */ | |
| .credit-block { | |
| margin-bottom: 2.5em; | |
| } | |
| /* Title styling */ | |
| .title { | |
| color: #0f0; | |
| opacity: 0.8; | |
| } | |
| /* Name styling */ | |
| .name { | |
| font-weight: bold; | |
| color: #5f5; | |
| text-shadow: 0 0 5px #0f0; | |
| } | |
| .credit-two-column { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| text-align: left; | |
| margin: 0.8em auto; | |
| max-width: 80%; | |
| gap: 1em; | |
| } | |
| .credit-two-column .title { | |
| flex: 1; | |
| text-align: right; | |
| padding-right: 1em; | |
| } | |
| .credit-two-column .name { | |
| flex: 1; | |
| text-align: left; | |
| padding-left: 1em; | |
| } | |
| .scroll-logo-container { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .scroll-logo-container img { | |
| display: block; | |
| margin-left: auto; | |
| margin-right: auto; | |
| max-width: 80%; | |
| object-fit: contain; | |
| filter: grayscale(1) brightness(0.5) sepia(100%) hue-rotate(50deg) saturate(500%); | |
| } | |
| </style> | |