From 1553ed610c830851fdfb8d921a1f8168ed475151 Mon Sep 17 00:00:00 2001 From: SushiElemental Date: Sat, 6 Jan 2024 19:14:35 +0100 Subject: [PATCH] feat: Adding channel with a V A P O R W A V E scenery --- src/channels/index.ts | 1 + src/channels/laser-sunset/config.ts | 84 ++++++ src/channels/laser-sunset/functions.ts | 228 ++++++++++++++++ src/channels/laser-sunset/index.tsx | 308 +++++++++++++++++++++ src/channels/laser-sunset/style.css | 363 +++++++++++++++++++++++++ src/channels/laser-sunset/types.ts | 33 +++ 6 files changed, 1017 insertions(+) create mode 100644 src/channels/laser-sunset/config.ts create mode 100644 src/channels/laser-sunset/functions.ts create mode 100644 src/channels/laser-sunset/index.tsx create mode 100644 src/channels/laser-sunset/style.css create mode 100644 src/channels/laser-sunset/types.ts diff --git a/src/channels/index.ts b/src/channels/index.ts index f55d4bd..cb88211 100644 --- a/src/channels/index.ts +++ b/src/channels/index.ts @@ -15,5 +15,6 @@ import './dragon-warrior'; import './ff-shop'; import './template'; import './papers-please'; +import './laser-sunset'; export * from './channels'; diff --git a/src/channels/laser-sunset/config.ts b/src/channels/laser-sunset/config.ts new file mode 100644 index 0000000..a6bfcc8 --- /dev/null +++ b/src/channels/laser-sunset/config.ts @@ -0,0 +1,84 @@ +/** + * @file Configuration values - mainly for changing colors, fps, animations. + * Many of the settings are now balanced to look good enough and will break if put to extremes. + * Handle with care. + */ + +const CONFIG = { + Donations: { + despawnMs: 5000, + fontSizeMin: 20, + fontSizeMax: 50, + lockTimeMs: 100, + colors: [ + 'cyan', + 'magenta', + 'orchid', + 'blueviolet', + 'mediumpurple', + 'mediumvioletred', + 'mediumspringgreen', + 'deepskyblue', + ], + }, + Subscriptions: { + despawnMs: 6000, + spawnPercent: -10, + despawnPercent: 110, + slowdownPercent: 48, + speedupPercent: 42, + topMinPercent: 5, + topMaxPercent: 80, + lockTimeMs: 500, + minSize: 0.6, + maxSize: 1, + maxMonths: 12, + colors: ['ghostwhite', 'mintcream', 'pink', 'lavender', 'hotpink', 'salmon', 'mistyrose'], + }, + Stars: { + countX: 10, + countY: 7, + brightnessMin: 120, + brightnessMax: 240, + opacityMin: 0.3, + opacityMax: 1.0, + twinkleMs: 400, + }, + Lasers: { + bgXstart: 67, + bgXmin: -53, + scaleXdefault: 2, + }, + Cloud: { + despawnMs: 10000, + xMargin: 5, + sizeMinPercent: 3, + sizeMaxPercent: 20, + sizeDonationMin: 5, + sizeDonationMax: 1000, + perspectiveGrowth: 2.4, + colors: ['magenta', 'orchid', 'blueviolet', 'mediumpurple'], + }, + Thumping: { + scale: 1.05, + intervalMs: 800, + durationMs: 90, + }, + Timers: { + fpsInterval: 1000 / 60, + }, + Titles: { + // See https://en.wikipedia.org/wiki/Combining_Diacritical_Marks + diacriticalMarks: [ + '', // none + '\u0307', // ̇ Dot + '\u0320', // ̠ Minus sign below + '\u032A', // ̪ Bridge below + '\u0332', // ̲ Low line + '\u0333', // ̳ Double low line + '\u0359', // ͙ Asterisk below + ], + }, +}; + +export default CONFIG; diff --git a/src/channels/laser-sunset/functions.ts b/src/channels/laser-sunset/functions.ts new file mode 100644 index 0000000..f60174e --- /dev/null +++ b/src/channels/laser-sunset/functions.ts @@ -0,0 +1,228 @@ +import type { FormattedDonation, TwitchSubscription } from '@gdq/types/tracker'; +import { StarVisual, Overcast, DonationPopup, SubscriptionVisual } from './types'; +import CONFIG from './config'; + +export const formatCurrency = (val: number) => { + return val.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 2, + }); +}; + +export const vaporifyText = (text: string) => { + const marks = CONFIG.Titles.diacriticalMarks; + return text + .split('') + .map((character) => character.replace(/([^\s])/i, (c) => c + marks[c.charCodeAt(0) % marks.length])) + .join(''); +}; + +export const easeIn = (number: number, total: number, scaleMin = 1.0, scaleMax = 1.0) => { + return scaleMin + (number / total) * (scaleMax - scaleMin); +}; + +export const easeOut = (number: number, total: number, scaleMin = 1.0, scaleMax = 1.0) => { + return scaleMin + (scaleMax - scaleMin) * easeIn(number, total, 0.0, 1.0); +}; + +export const randomRange = (min: number, max: number) => { + return min + Math.random() * (max - min); +}; + +export const subscriptionFlyby = (number: number, total: number, x0: number, x1: number, x2: number, x3: number) => { + const progress = number / total; + return ( + Math.pow(1 - progress, 3) * x0 + + 3 * Math.pow(1 - progress, 2) * progress * x1 + + 3 * (1 - progress) * Math.pow(progress, 2) * x2 + + Math.pow(progress, 3) * x3 + ); +}; + +export const randomStarOpacity = () => { + return randomRange(CONFIG.Stars.opacityMin, CONFIG.Stars.opacityMax); +}; + +export const spawnStar = (number: number, xMin: number, xMax: number, yMin: number, yMax: number): StarVisual => { + const clr = randomRange(CONFIG.Stars.brightnessMin, CONFIG.Stars.brightnessMax); + const opacity = randomStarOpacity(); + + return { + left: randomRange(xMin, xMax), + top: randomRange(yMin, yMax), + text: number % 2 == 0 ? '+' : '*', + color: 'RGB(' + clr + ',' + clr + ',' + clr + ')', + opacity: opacity, + }; +}; + +export const starStyle = (star: StarVisual) => { + return { + left: star.left + '%', + top: star.top + '%', + color: star.color, + opacity: star.opacity, + }; +}; + +export const spawnDonation = (baseProps: FormattedDonation, count: number): DonationPopup => { + const angleDegs = count % 2 ? (count % 36) * 10 : ((count + 18) % 36) * 10; + const angleRads = (angleDegs / 360) * Math.PI * 2.0; + + return { + ...baseProps, + renderedAmount: formatCurrency(baseProps.rawAmount), + angle: angleRads, + radius: 2 + Math.random() * 5, + color: CONFIG.Donations.colors[count % CONFIG.Donations.colors.length], + received: new Date(), + }; +}; + +export const spawnCloud = (donationAmount: number, count: number): Overcast => { + const clamped = Math.min(Math.max(donationAmount, CONFIG.Cloud.sizeDonationMin), CONFIG.Cloud.sizeDonationMax); + const amountRatio = (clamped - CONFIG.Cloud.sizeDonationMin) / CONFIG.Cloud.sizeDonationMax; + const sizeMin = CONFIG.Cloud.sizeMinPercent; + const sizeMax = CONFIG.Cloud.sizeMaxPercent; + const sideLength = sizeMin + amountRatio * (sizeMax - sizeMin); + + return { + left: CONFIG.Cloud.xMargin + Math.random() * (100 - CONFIG.Cloud.xMargin * 2), + width: sideLength, + height: sideLength * 1.5, + backgroundColor: CONFIG.Cloud.colors[count % CONFIG.Cloud.colors.length], + received: new Date(), + }; +}; + +export const cloudScreenspaceProps = (cloud: Overcast) => { + const now = new Date(); + const timeVisible = Math.min(now.getTime() - cloud.received.getTime(), CONFIG.Cloud.despawnMs); + const ageRatio = timeVisible / CONFIG.Cloud.despawnMs; + const visibilityWindow = 100 + CONFIG.Cloud.sizeMaxPercent; + const age = ageRatio * visibilityWindow; + + return { + left: cloud.left, + bottom: age, + width: cloud.width + cloud.width * ageRatio * CONFIG.Cloud.perspectiveGrowth, + height: cloud.height, + backgroundColor: cloud.backgroundColor, + now: now, + age: age, + rotate: age * 28, + }; +}; + +export const cloudStyle = (cloud: Overcast) => { + const csp = cloudScreenspaceProps(cloud); + + return { + left: csp.left + '%', + bottom: csp.bottom + '%', + width: csp.width + '%', + height: csp.height + '%', + backgroundColor: csp.backgroundColor, + rotate: 'x -' + csp.rotate + 'deg', + }; +}; + +export const cloudReflectionStyle = (cloud: Overcast) => { + const csp = cloudScreenspaceProps(cloud); + + return { + left: csp.left + '%', + top: csp.bottom + '%', + width: csp.width + '%', + height: csp.height + '%', + backgroundColor: csp.backgroundColor, + rotate: 'x ' + csp.rotate + 'deg', + }; +}; + +export const donationScreenspaceProps = (donation: DonationPopup) => { + const now = new Date(); + const age = now.getTime() - donation.received.getTime(); + const radius = donation.radius + age / 50.0; + + return { + x: 50 + Math.cos(donation.angle) * radius, + y: 50 + Math.sin(donation.angle) * radius, + now: now, + age: age, + radius: radius, + color: donation.color, + fontSize: easeIn(age, CONFIG.Donations.despawnMs, CONFIG.Donations.fontSizeMin, CONFIG.Donations.fontSizeMax), + }; +}; + +export const donationStyle = (donation: DonationPopup) => { + const dsp = donationScreenspaceProps(donation); + return { left: dsp.x + '%', top: dsp.y + '%', fontSize: dsp.fontSize + 'px', color: dsp.color }; +}; + +export const donationReflectionStyle = (donation: DonationPopup) => { + const dsp = donationScreenspaceProps(donation); + const age = dsp.age * 2; + const pushDown = 10 + (age / CONFIG.Donations.despawnMs) * 120; + return { + ...donation, + left: dsp.x + '%', + top: dsp.radius + pushDown + '%', + }; +}; + +export const spawnSubscription = (sub: TwitchSubscription, count: number): SubscriptionVisual => { + // The y position is based on the months subscribed. When I tested subscriptions they always had "months" set to 1. + // To test layout use months value below based on number of subscriptions: + // const months = Math.floor(count) % CONFIG.Subscriptions.maxMonths; + const months = Math.min(sub.months, CONFIG.Subscriptions.maxMonths); + const ageRatio = months / CONFIG.Subscriptions.maxMonths; + const top = CONFIG.Subscriptions.topMinPercent + ageRatio * CONFIG.Subscriptions.topMaxPercent; + + return { + left: CONFIG.Subscriptions.spawnPercent, + top: top, + zoom: + CONFIG.Subscriptions.minSize + (top / 100) * (CONFIG.Subscriptions.maxSize - CONFIG.Subscriptions.minSize), + user_name: sub.user_name, + display_name: sub.display_name, + months: sub.months, + context: sub.context, + color: CONFIG.Subscriptions.colors[count % CONFIG.Subscriptions.colors.length], + received: new Date(), + }; +}; + +export const subscriptionScreenspaceProps = (sub: SubscriptionVisual) => { + const now = new Date(); + const age = now.getTime() - sub.received.getTime(); + + return { + now: now, + age: age, + left: subscriptionFlyby( + age, + CONFIG.Subscriptions.despawnMs, + CONFIG.Subscriptions.spawnPercent, + CONFIG.Subscriptions.slowdownPercent, + CONFIG.Subscriptions.speedupPercent, + CONFIG.Subscriptions.despawnPercent, + ), + top: sub.top, + zoom: sub.zoom, + color: sub.color, + }; +}; + +export const subscriptionStyle = (sub: SubscriptionVisual) => { + const ssp = subscriptionScreenspaceProps(sub); + return { + left: ssp.left + '%', + top: ssp.top + '%', + color: ssp.color, + zIndex: Math.floor(ssp.top), + zoom: ssp.zoom, + }; +}; diff --git a/src/channels/laser-sunset/index.tsx b/src/channels/laser-sunset/index.tsx new file mode 100644 index 0000000..1a1e6c3 --- /dev/null +++ b/src/channels/laser-sunset/index.tsx @@ -0,0 +1,308 @@ +/** + * @author SushiElemental + * + * @file Break channel showing an HTML/CSS 【 V A P O R W A V E 】 scenery + */ + +import type { Event, FormattedDonation, TwitchSubscription, Total } from '@gdq/types/tracker'; +import { ChannelProps, registerChannel } from '../channels'; +import styled from '@emotion/styled'; + +import { useEffect, useState, useReducer } from 'react'; +import { useReplicant } from 'use-nodecg'; +import { useActive } from '@gdq/lib/hooks/useActive'; +import { useListenForFn } from '@gdq/lib/hooks/useListenForFn'; +import { usePreloadedReplicant } from '@gdq/lib/hooks/usePreloadedReplicant'; +import TweenNumber from '@gdq/lib/components/TweenNumber'; + +import { StarVisual, SunReflectionLine, Overcast, DonationPopup, SubscriptionVisual } from './types'; +import * as fn from './functions'; +import CONFIG from './config'; +import './style.css'; + +registerChannel('Laser Sunset', 15, LaserSunset, { + position: 'topRight', + site: 'Twitch', + handle: 'SushiElemental', +}); + +export function LaserSunset(props: ChannelProps) { + const active = useActive(); + const [running, setRunning] = useState(false); + const [event] = usePreloadedReplicant('currentEvent'); + const eventBeneficiary = fn.vaporifyText(event.beneficiary); + const eventShortname = fn.vaporifyText(event.shortname); + const [total] = useReplicant('total', null); + const [thumpingMs, incrementThumpingMs] = useReducer((x) => { + x += CONFIG.Timers.fpsInterval; + if (x >= CONFIG.Thumping.intervalMs + CONFIG.Thumping.durationMs) { + x -= CONFIG.Thumping.intervalMs + CONFIG.Thumping.durationMs; + } + return x; + }, 0); + const [thumpingScale, animateThumping] = useReducer((x) => { + const thump = thumpingMs - CONFIG.Thumping.intervalMs; + return thump >= 0 && thump < CONFIG.Thumping.durationMs + ? fn.easeOut(thump, CONFIG.Thumping.durationMs, 1.0, CONFIG.Thumping.scale) + : 1.0; + }, 1.0); + const [stars, setStars] = useState([]); + const [countDonations, incrementDonationsCount] = useReducer((x) => x + 1, 0); + const [donations, setDonations] = useState([]); + const [countSubscriptions, incrementSubscriptionsCount] = useReducer((x) => x + 1, 0); + const [subscriptions, setSubscriptions] = useState([]); + const [lasersX, scrollLasers] = useReducer((x) => { + return x > CONFIG.Lasers.bgXmin ? x - 1 : CONFIG.Lasers.bgXstart; + }, CONFIG.Lasers.bgXstart); + const [clouds, setClouds] = useState([]); + const [sunReflections, setSunReflections] = useState([ + { xPosition: 50, marginTop: 2, width: 16 }, + { xPosition: 50, marginTop: 2, width: 14 }, + { xPosition: 50, marginTop: 2, width: 12 }, + { xPosition: 50, marginTop: 2, width: 10 }, + { xPosition: 50, marginTop: 2, width: 9 }, + { xPosition: 50, marginTop: 2, width: 8 }, + { xPosition: 50, marginTop: 2, width: 6 }, + { xPosition: 50, marginTop: 2, width: 4 }, + ]); + + const lasersXstyle = `.Lasers:after { background-position-x: ${lasersX}px; scale: ${thumpingScale} 1; }`; + + const onDonationReceived = (donation: FormattedDonation) => { + if (!active) return; + + props.lock(); + setTimeout(() => props.unlock(), CONFIG.Donations.lockTimeMs); + + const dono = fn.spawnDonation(donation, countDonations); + const cloud = fn.spawnCloud(donation.rawAmount, countDonations); + setDonations((donos) => [...donos, dono]); + setClouds((clouds) => [...clouds, cloud]); + setTimeout(() => removeDonation(dono.received), CONFIG.Donations.despawnMs); + setTimeout(() => removeCloud(cloud.received), CONFIG.Cloud.despawnMs); + + incrementDonationsCount(); + animateSunReflections(); + }; + + const onSubscriptionReceived = (subscription: TwitchSubscription) => { + if (!active) return; + + props.lock(); + setTimeout(() => props.unlock(), CONFIG.Subscriptions.lockTimeMs); + + const sub = fn.spawnSubscription(subscription, countSubscriptions); + setSubscriptions((subs) => [...subs, sub]); + setTimeout(() => removeSubscription(sub.received), CONFIG.Subscriptions.despawnMs); + + incrementSubscriptionsCount(); + animateSunReflections(); + }; + + useListenForFn('donation', onDonationReceived); + useListenForFn('subscription', onSubscriptionReceived); + + useEffect(() => { + if (!active) return; + + const fpsTimer = setInterval(() => { + scrollLasers(); + incrementThumpingMs(); + animateThumping(); + }, CONFIG.Timers.fpsInterval); + + setRunning(true); + return () => clearInterval(fpsTimer); + }, [active]); + + useEffect(() => { + const xStep = Math.min(100, 100 / CONFIG.Stars.countX); + const yStep = Math.min(100, 100 / CONFIG.Stars.countY); + const stars = new Array(); + + for (let y = 0; y < CONFIG.Stars.countY; y++) { + for (let x = 0; x < CONFIG.Stars.countX; x++) { + const star = fn.spawnStar( + y * CONFIG.Stars.countY + x, + x * xStep, + x * xStep + xStep, + y * yStep, + y * yStep + yStep, + ); + stars.push(star); + } + } + + setStars(stars); + }, [CONFIG.Stars.countX, CONFIG.Stars.countY]); + + useEffect(() => { + if (!active) return; + + const twinkleTimer = setInterval(() => { + setStars((stars) => + stars.map((s, index) => { + return { ...s, opacity: fn.randomStarOpacity() }; + }), + ); + }, CONFIG.Stars.twinkleMs); + + return () => clearInterval(twinkleTimer); + }, [active]); + + const removeDonation = (date: Date) => { + setDonations((donos) => donos.filter((d) => d.received !== date)); + }; + + const removeCloud = (date: Date) => { + setClouds((donos) => donos.filter((d) => d.received !== date)); + }; + + const removeSubscription = (date: Date) => { + setSubscriptions((subscriptions) => subscriptions.filter((s) => s.received !== date)); + }; + + const animateSunReflections = () => { + setSunReflections((lines) => { + return lines.map((line, index) => { + line.width = 16 - index * 2 + (Math.random() * 2 - 1); + line.marginTop = Math.floor(2.5 + (Math.random() * 2 - 1)); + line.xPosition = 50 + (Math.random() * 2 - 1); + return line; + }); + }); + }; + + return ( + + {active && running && ( + <> + + + + {stars.map((s, index) => ( + + {s.text} + + ))} + + + + + + + + {clouds.map((c, index) => ( + + ))} + + + + 『 {eventShortname} 』 + + + 【{eventBeneficiary}】 + + + + {stars.map((s, index) => ( + + {s.text} + + ))} + + + + + + 『 {eventShortname} 』 + + + {sunReflections.map((line, index) => ( + + ))} + + + {subscriptions.map((s, idx) => ( + + ♥ {s.display_name} ♥ + + ))} + + + {subscriptions.map((s, idx) => ( + + ♥ {s.display_name} ♥ + + ))} + + + {clouds.map((c, index) => ( + + ))} + + + {donations.map((d, idx) => ( + + ◄{d.renderedAmount}► + + ))} + + + + {donations.map((d, idx) => ( + + ◄{d.renderedAmount}► + + ))} + + + $ + + + $ + + + )} + + ); +} + +export const Container = styled.div``; +export const TotalEl = styled.div``; +export const DonationsList = styled.div``; +export const Donation = styled.div``; +export const SubscriptionsList = styled.div``; +export const Subscription = styled.div``; +export const Sky = styled.div``; +export const Stars = styled.div``; +export const Star = styled.div``; +export const Clouds = styled.div``; +export const Cloud = styled.div``; +export const CloudReflections = styled.div``; +export const Sun = styled.div``; +export const Beneficiary = styled.div``; +export const Title = styled.div``; +export const Ocean = styled.div``; +export const OceanBackground = styled.div``; +export const Lasers = styled.div``; +export const LasersHorizon = styled.div``; +export const SunReflections = styled.div``; +export const SunReflection = styled.div``; diff --git a/src/channels/laser-sunset/style.css b/src/channels/laser-sunset/style.css new file mode 100644 index 0000000..4d169eb --- /dev/null +++ b/src/channels/laser-sunset/style.css @@ -0,0 +1,363 @@ + +.Container { + position: absolute; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + background-color: #000; +} + +.TotalEl { + font-family: gdqpixel; + font-size: 46px; + color: white; + text-shadow: 1px 1px 4px indigo; + opacity: 0.8; + position: absolute; + left: 4%; + top: 50%; + margin-top: -23px; + z-index: 101; +} + +.TotalEl-top { + color: cyan; + clip-path: inset(23px 0 -23px 0); +} + +.TotalEl-bottom { + color: magenta; + clip-path: inset(-23px 0 23px 0); +} + +.DonationsList { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; +} + +.Donation { + position: absolute; + left: 0; + top: 0; + font-family: gdqpixel; + font-size: 28px; + color: magenta; + text-shadow: 4px 2px indigo; + transform: translate(-50%, 0%); + z-index: 69; +} + +.Donation.reflection { + opacity: 0.3; + transform: translate(-50%, 0%) rotateX(-230deg); + z-index: initial; +} + +.fade-full { + animation: fade-full 0.5s cubic-bezier(0, .2, .8, 1); +} + +@keyframes fade-full { + 0% { + opacity: 0; + display: none; + } + + 100% { + opacity: 1; + display: block; + } +} + +.fade-reflection { + animation: fade-reflection 0.5s cubic-bezier(0, .2, .8, 1); +} + +@keyframes fade-reflection { + 0% { + opacity: 0; + display: none; + } + + 100% { + opacity: 0.3; + display: block; + } +} + +.SubscriptionsList { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; +} + +.Subscription { + position: absolute; + width: auto; + left: 0; + top: 0; + font-family: 'Courier New', Courier, monospace; + font-weight: bold; + font-size: 28px; + text-shadow: 1px 1px 1px indigo; + color: white; + text-wrap: nowrap; +} + +.Subscription.reflection { + opacity: 0.3; + transform: translate(0, 8px) rotateX(-230deg); + z-index: initial; +} + +.Sky { + position: absolute; + width: 100%; + height: 50%; + background: linear-gradient( + to bottom, + rgba(222, 33, 111, .0) 0%, + rgba(222, 33, 111, .2) 80%, + rgba(222, 33, 111, .5) 90%, + rgba(222, 33, 111, 1) 100% + ); +} + +.Stars { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; + opacity: 0.7; +} + +.Stars.reflection { + transform: rotateX(-180deg); + opacity: 0.4; +} + +.Star { + font-family: gdqpixel; + font-size: 10px; + color: #eee; + position: absolute; + left: 10%; + top: 10%; + transition: opacity .4s ease-in-out; +} + +.Clouds { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.Clouds:after { + transform: perspective(200px) rotateX(-38deg) scale(2,1) translateZ(0); + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100vh; + -webkit-background-clip: content-box; + background-clip: content-box; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + outline: 1px solid transparent; + transform-origin: bottom center; + will-change: transform; +} + +.Clouds:after { + background-position: center bottom; + background-size: 110px 110px; + background-image: + linear-gradient(to right, magenta 2px, transparent 0); +} + +.Cloud { + transform: perspective(200px) rotateX(-38deg) scale(2,1) translateZ(0); + position: absolute; + z-index: 42; +} + +.CloudReflections { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; +} + +.Cloud.reflection { + transform: perspective(200px) rotateX(38deg) scale(2,1) translateZ(0); + opacity: 0.4; +} + +.Sun { + position: absolute; + left: 50%; + top: 69%; + width: 200px; + height: 200px; + border-radius: 100px; + box-shadow: 0 0 32px 0 #f09000; + background-color: #f09000; + transform: translate(-50%, -35%); +} + +.Sun-top { + clip-path: inset(-50px -50px 166px -50px); +} + +.Sun-middle-top { + clip-path: inset(38px -50px 150px -50px); +} + +.Sun-middle { + clip-path: inset(56px -50px 126px -50px); +} + +.Sun-middle-bottom { + clip-path: inset(82px -50px 97px -50px); +} + +.Sun-bottom { + clip-path: inset(112px -50px -50px -50px); +} + +.Beneficiary { + position: absolute; + width: 100%; + left: 50%; + bottom: 3%; + transform: translate(-50%, 0); + text-align: center; + font-family: 'Courier New', Courier, monospace; + font-weight: bolder; + letter-spacing: 16px; + font-size: 18px; + text-shadow: 0 2px indigo; + color: white; + z-index: 99; +} + +.Title { + position: absolute; + width: 100%; + left: 50%; + top: 3%; + transform: translate(-49%, 0); + text-align: center; + font-family: 'Courier New', Courier, monospace; + font-weight: bolder; + letter-spacing: 16px; + font-size: 32px; + text-shadow: 0 3px indigo; + color: white; + z-index: 99; +} + +.Title.reflection { + transform: rotateX(-140deg) translate(-49%, -100%); + opacity: 0.4; +} + +.Ocean { + position: absolute; + bottom: 0%; + width: 100%; + height: 50%; + overflow: hidden; + background: #000; +} + +.Ocean-background { + position: absolute; + width: 100%; + height: 100%; + background: linear-gradient( + to bottom, + rgba(0, 215, 215, .7) 0%, + rgba(0, 215, 215, .1) 20%, + rgba(0, 215, 215, 0) 100% + ); +} + +.Lasers { + position: absolute; + overflow: hidden; + width: 200%; + height: 100%; + left: 50%; + transform: translate(-50%, 0%); +} + +/* Laser grid background effect, taken from https://stackoverflow.com/questions/53416334/css-80s-tron-grid */ +.Lasers:after { + transform: perspective(200px) rotateX(38deg) scale(2,1) translateZ(0); + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100vh; + -webkit-background-clip: content-box; + background-clip: content-box; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + outline: 1px solid transparent; + transform-origin: bottom center; + will-change: transform; +} + +.Lasers:after { + background-position: center bottom; + background-size: 120px 120px; + background-image: + linear-gradient(to right, cyan 10px, transparent 0), + linear-gradient(to bottom, cyan 16px, transparent 0); +} + +.Lasers-horizon { + position: absolute; + width: 100%; + top: 0; + height: 100%; + background: linear-gradient(to bottom,rgba(0,0,0,0.4) 0%,rgba(0,0,0,0.2) 50%,rgba(0,0,0,0) 100%); +} + +.SunReflections { + position: absolute; + top: 3%; + width: 100%; + left: 50%; + transform: translate(-50%, 0%); +} + +.SunReflection { + position: relative; + left: 50%; + margin: 6px 0; + width: 20%; + height: 5px; + box-shadow: 0 0 16px 1px #f09000; + background-color: #f09000ad; + transform: translate(-50%, 0%); + transition: width 1s cubic-bezier(0, .3, .5, 1), left 1s cubic-bezier(0, .3, .5, 1), margin-top 1s ease-in-out; +} + +.Lamps { + position: absolute; + right: 2%; + bottom: 10%; +} diff --git a/src/channels/laser-sunset/types.ts b/src/channels/laser-sunset/types.ts new file mode 100644 index 0000000..3c1038d --- /dev/null +++ b/src/channels/laser-sunset/types.ts @@ -0,0 +1,33 @@ +import type { FormattedDonation } from '@gdq/types/tracker'; + +export type StarVisual = { left: number; top: number; text: string; color: string; opacity: number }; + +export type SunReflectionLine = { xPosition: number; marginTop: number; width: number }; + +export type Overcast = { + left: number; + width: number; + height: number; + backgroundColor: string; + received: Date; +}; + +export type DonationPopup = FormattedDonation & { + renderedAmount: string; + angle: number; + radius: number; + color: string; + received: Date; +}; + +export type SubscriptionVisual = { + left: number; + top: number; + zoom: number; + user_name: string; + display_name: string; + months: number; + context: string; + color: string; + received: Date; +};