Spaces:
Running
Running
| <!-- StarWarsEffect.svelte --> | |
| <script lang="ts"> | |
| import { onMount, onDestroy } from 'svelte'; | |
| /** | |
| * Props for the StarWarsEffect component. | |
| * @typedef {Object} Props | |
| * @property {Array<{title?: string, name?: string, section_title?: string}>} credits - List of credits, can include sections. | |
| * @property {number} speed - Animation speed in seconds (default: 40). | |
| * @property {number} base_font_size - Base font size in rem (default: 1.5). | |
| * @property {string | null} background_color - Background color (default: black). | |
| * @property {string | null} title_color - Title text color (default: #feda4a). | |
| * @property {string | null} name_color - Name text color (default: #feda4a). | |
| * @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 = 40; | |
| export let base_font_size: number = 1.5; | |
| export let background_color: string | null = null; | |
| export let title_color: string | null = null; | |
| export let name_color: string | null = null; | |
| 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"; | |
| // Reactive styles | |
| $: title_style = (is_intro: boolean) => `color: ${title_color || '#feda4a'}; font-size: ${is_intro ? base_font_size * 1.5 : base_font_size}rem;`; | |
| $: name_style = (is_intro: boolean) => `color: ${name_color || '#feda4a'}; font-size: ${is_intro ? base_font_size * 0.9 : base_font_size * 0.7}rem;`; | |
| $: section_title_style = `color: ${title_color || '#feda4a'}; font-size: ${base_font_size * 1.2}rem;`; | |
| // Combine 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 }))]; | |
| })(); | |
| let crawlElement: HTMLElement | null; | |
| function resetAnimation() { | |
| if (crawlElement) { | |
| crawlElement.style.animation = 'none'; | |
| void crawlElement.offsetHeight; // Trigger reflow | |
| crawlElement.style.animation = ''; | |
| } | |
| } | |
| onMount(resetAnimation); | |
| onDestroy(() => { crawlElement = null; }); | |
| // Trigger reset on prop changes | |
| $: credits, speed, layout_style, resetAnimation(); | |
| // Generate star shadows for background | |
| const generate_star_shadows = (count: number, size: string) => { | |
| let shadows = Array.from({ length: count }, () => `${Math.random() * 2000}px ${Math.random() * 2000}px ${size} white`).join(', '); | |
| return shadows; | |
| }; | |
| const small_stars = generate_star_shadows(200, '1px'); | |
| const medium_stars = generate_star_shadows(100, '2px'); | |
| const large_stars = generate_star_shadows(50, '3px'); | |
| </script> | |
| <div class="viewport" style:background={background_color || 'black'}> | |
| <div class="stars small" style="box-shadow: {small_stars};"></div> | |
| <div class="stars medium" style="box-shadow: {medium_stars};"></div> | |
| <div class="stars large" style="box-shadow: {large_stars};"></div> | |
| <div class="crawl" bind:this={crawlElement} 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"> | |
| <span style={swap_font_sizes_on_two_column ? name_style(false) : title_style(false)} class:uppercase={title_uppercase}>{item.title}</span> | |
| <span class="spacer"></span> | |
| <span style={swap_font_sizes_on_two_column ? title_style(false) : name_style(false)} class:uppercase={name_uppercase}>{item.name}</span> | |
| </div> | |
| {:else} | |
| <!-- Stacked Layout --> | |
| <div class="credit" class:intro-block={item.is_intro}> | |
| <h2 style={title_style(item.is_intro)} class:uppercase={title_uppercase && !item.is_intro}>{item.title}</h2> | |
| {#if item.name}<p style={name_style(item.is_intro)} class:uppercase={name_uppercase && !item.is_intro}>{item.name}</p>{/if} | |
| </div> | |
| {/if} | |
| {/if} | |
| {/each} | |
| </div> | |
| </div> | |
| <style> | |
| .viewport { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| overflow: hidden; | |
| perspective: 400px; | |
| -webkit-mask-image: linear-gradient(to bottom, black 60%, transparent 100%); | |
| mask-image: linear-gradient(to bottom, black 60%, transparent 100%); | |
| font-family: "Droid Sans", sans-serif; | |
| font-weight: bold; | |
| } | |
| .stars { | |
| position: absolute; top: 0; left: 0; | |
| width: 1px; height: 1px; | |
| background: transparent; z-index: 0; | |
| animation: twinkle 10s linear infinite; | |
| } | |
| .stars.small { animation-duration: 10s; } | |
| .stars.medium { animation-duration: 15s; } | |
| .stars.large { animation-duration: 20s; } | |
| @keyframes twinkle { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } | |
| .crawl { | |
| position: absolute; width: 100%; bottom: 0; | |
| transform-origin: 50% 100%; | |
| animation: crawl-animation var(--animation-duration) linear infinite; | |
| z-index: 1; text-align: center; | |
| } | |
| @keyframes crawl-animation { | |
| 0% { transform: rotateX(60deg) translateY(100%) translateZ(100px); } | |
| 100% { transform: rotateX(60deg) translateY(-150%) translateZ(-1200px); } | |
| } | |
| /* STYLES */ | |
| .scroll-logo-container { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .scroll-logo-container img { | |
| display: block; | |
| margin-left: auto; | |
| margin-right: auto; | |
| max-width: 70%; | |
| object-fit: contain; | |
| filter: grayscale(1) sepia(100%) hue-rotate(15deg) saturate(8) brightness(1.2); | |
| } | |
| .uppercase { text-transform: uppercase; } | |
| .section-title { | |
| margin-top: 5rem; | |
| margin-bottom: 3rem; | |
| font-weight: bold; | |
| } | |
| .credit.intro-block { margin-bottom: 5rem; } | |
| .credit { margin-bottom: 2rem; } | |
| .credit h2, .credit p { margin: 0.5rem 0; padding: 0; white-space: nowrap; } | |
| .credit-two-column { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| margin: 0.8rem auto; | |
| width: 90%; | |
| white-space: nowrap; | |
| } | |
| .credit-two-column .spacer { | |
| flex-grow: 1; | |
| border-bottom: 1px dotted rgba(254, 218, 74, 0.3); | |
| margin: 0 1em; | |
| transform: translateY(-0.5em); | |
| } | |
| </style> |