diff --git a/src/channels/laser-sunset/config.ts b/src/channels/laser-sunset/config.ts index 49c6e43..6be0d23 100644 --- a/src/channels/laser-sunset/config.ts +++ b/src/channels/laser-sunset/config.ts @@ -47,6 +47,14 @@ const CONFIG = { perspectiveGrowth: 2.4, colors: ['magenta', 'orchid', 'blueviolet', 'mediumpurple'], }, + Static: { + respawnPeriod: 32000, + respawnPeriodVariance: 4000, + ageMin: 5000, + ageMax: 8000, + heightMinPercent: 4, + heightMaxPercent: 10, + }, Timers: { fpsInterval: 1000 / 60, }, diff --git a/src/channels/laser-sunset/functions.ts b/src/channels/laser-sunset/functions.ts index 6088ef0..d9fa545 100644 --- a/src/channels/laser-sunset/functions.ts +++ b/src/channels/laser-sunset/functions.ts @@ -1,5 +1,5 @@ import type { FormattedDonation, TwitchSubscription } from '@gdq/types/tracker'; -import { StarVisual, Overcast, DonationPopup, SubscriptionVisual } from './types'; +import { StarVisual, Overcast, DonationPopup, SubscriptionVisual, Static } from './types'; import CONFIG from './config'; export const formatCurrency = (val: number) => { @@ -22,6 +22,10 @@ export const randomRange = (min: number, max: number) => { return min + Math.random() * (max - min); }; +export const staticOpacity = (yPosition: number) => { + return Math.max(0.1, Math.abs(yPosition - 50.0) / 100.0); +}; + export const randomStarOpacity = () => { return randomRange(CONFIG.Stars.opacityMin, CONFIG.Stars.opacityMax); }; @@ -34,16 +38,16 @@ export const spawnStar = (number: number, xMin: number, xMax: number, yMin: numb left: randomRange(xMin, xMax), top: randomRange(yMin, yMax), text: number % 2 == 0 ? '+' : '*', - color: 'RGB(' + clr + ',' + clr + ',' + clr + ')', + color: { r: clr, g: clr, b: clr }, opacity: opacity, }; }; -export const starStyle = (star: StarVisual) => { +export const starStyle = (star: StarVisual, highlight: boolean) => { return { left: star.left + '%', top: star.top + '%', - color: star.color, + color: 'RGB(' + (highlight ? '0' : star.color.r) + ',' + star.color.g + ',' + star.color.b + ')', opacity: star.opacity, }; }; @@ -79,6 +83,16 @@ export const spawnCloud = (donationAmount: number, count: number): Overcast => { }; }; +export const spawnStatic = (): Static => { + return { + spawnDate: new Date(), + maxAge: CONFIG.Static.ageMin + Math.random() * (CONFIG.Static.ageMax - CONFIG.Static.ageMin), + height: + CONFIG.Static.heightMinPercent + + Math.random() * (CONFIG.Static.heightMaxPercent - CONFIG.Static.heightMinPercent), + }; +}; + export const cloudScreenspaceProps = (cloud: Overcast) => { const now = new Date(); const timeVisible = Math.min(now.getTime() - cloud.received.getTime(), CONFIG.Cloud.despawnMs); @@ -220,3 +234,16 @@ export const subscriptionReflectionStyle = (sub: SubscriptionVisual) => { top: dsp.radius + pushDown + '%', }; }; + +export const staticStyle = (staticOverlay: Static, staticNoiseSrc: string) => { + const maxHeight = CONFIG.Static.heightMaxPercent; + const now = new Date(); + const age = now.getTime() - staticOverlay.spawnDate.getTime(); + const top = -maxHeight + (age / staticOverlay.maxAge) * (100.0 + maxHeight); + return { + top: top + '%', + height: staticOverlay.height + '%', + opacity: staticOpacity(top), + backgroundImage: 'url(' + staticNoiseSrc + ')', + }; +}; diff --git a/src/channels/laser-sunset/index.tsx b/src/channels/laser-sunset/index.tsx index 809b852..e2ac081 100644 --- a/src/channels/laser-sunset/index.tsx +++ b/src/channels/laser-sunset/index.tsx @@ -14,10 +14,11 @@ import { useActive } from '@gdq/lib/hooks/useActive'; import { useListenForFn } from '@gdq/lib/hooks/useListenForFn'; import TweenNumber from '@gdq/lib/components/TweenNumber'; -import { StarVisual, SunReflectionLine, Overcast, DonationPopup, SubscriptionVisual } from './types'; +import { StarVisual, SunReflectionLine, Overcast, DonationPopup, SubscriptionVisual, Static } from './types'; import * as fn from './functions'; import CONFIG from './config'; import './style.css'; +import staticNoise from '@gdq/assets/static.gif'; registerChannel('Laser Sunset', 15, LaserSunset, { position: 'topRight', @@ -34,9 +35,7 @@ export function LaserSunset(props: ChannelProps) { 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 - CONFIG.Lasers.scrollSpeed : CONFIG.Lasers.bgXstart; - }, CONFIG.Lasers.bgXstart); + const [_, scrollLasers] = useReducer((x) => x + 1, 0); const [clouds, setClouds] = useState([]); const [sunReflections, setSunReflections] = useState([ { xPosition: 50, marginTop: 2, width: 16 }, @@ -48,8 +47,9 @@ export function LaserSunset(props: ChannelProps) { { xPosition: 50, marginTop: 2, width: 6 }, { xPosition: 50, marginTop: 2, width: 4 }, ]); + const [staticOverlays, setStaticOverlays] = useState([]); - const lasersXstyle = `.Lasers:after { background-position-x: ${lasersX}px; }`; + const bgHighlightClass = donations.length || subscriptions.length ? ' highlight' : ''; const onDonationReceived = (donation: FormattedDonation) => { if (!active || donations.length >= CONFIG.Donations.countMax) return; @@ -131,6 +131,21 @@ export function LaserSunset(props: ChannelProps) { return () => clearInterval(twinkleTimer); }, [active]); + useEffect(() => { + if (!active) return; + + const staticOverlaysTimer = setInterval(() => { + setTimeout(() => { + const staticOverlay = fn.spawnStatic(); + + setStaticOverlays((overlays) => [...overlays, staticOverlay]); + setTimeout(() => removeStaticOverlay(staticOverlay.spawnDate), staticOverlay.maxAge); + }, Math.random() * CONFIG.Static.respawnPeriodVariance); + }, CONFIG.Static.respawnPeriod); + + return () => clearInterval(staticOverlaysTimer); + }, [active]); + const removeDonation = (date: Date) => { setDonations((donos) => donos.filter((d) => d.received !== date)); props.unlock(); @@ -144,6 +159,10 @@ export function LaserSunset(props: ChannelProps) { setSubscriptions((subscriptions) => subscriptions.filter((s) => s.received !== date)); }; + const removeStaticOverlay = (date: Date) => { + setStaticOverlays((staticOverlays) => staticOverlays.filter((so) => so.spawnDate !== date)); + }; + const animateSunReflections = () => { setSunReflections((lines) => { return lines.map((line, index) => { @@ -161,11 +180,10 @@ export function LaserSunset(props: ChannelProps) { return ( - - + {stars.map((s, index) => ( - + {s.text} ))} @@ -181,10 +199,13 @@ export function LaserSunset(props: ChannelProps) { ))} - + {stars.map((s, index) => ( - + 0 || subscriptions.length > 0)}> {s.text} ))} @@ -195,7 +216,7 @@ export function LaserSunset(props: ChannelProps) { {sunReflections.map((line, index) => ( ))} + + $ + {subscriptions.map((s, idx) => ( $ + + {staticOverlays.map((so, idx) => ( + + ))} + ); } @@ -267,3 +296,5 @@ export const Lasers = styled.div``; export const LasersHorizon = styled.div``; export const SunReflections = styled.div``; export const SunReflection = styled.div``; +export const StaticList = styled.div``; +export const StaticEl = styled.div``; diff --git a/src/channels/laser-sunset/style.css b/src/channels/laser-sunset/style.css index 780d112..1d52558 100644 --- a/src/channels/laser-sunset/style.css +++ b/src/channels/laser-sunset/style.css @@ -21,6 +21,12 @@ z-index: 101; } +.TotalEl.reflection { + opacity: 0.3; + transform: translate(0%, 111%) rotateX(-230deg); + z-index: initial; +} + .DonationsList { position: absolute; width: 100%; @@ -83,6 +89,11 @@ rgba(222, 33, 111, .5) 90%, rgba(222, 33, 111, 1) 100% ); + transition: background-color 1s ease-in-out; +} + +.Sky.highlight { + background-color: rgba(222, 33, 111, .1); } .Stars { @@ -100,7 +111,7 @@ .Star { font-family: gdqpixel; - font-size: 10px; + font-size: 13px; color: #eee; position: absolute; left: 10%; @@ -167,12 +178,12 @@ height: 200px; border-radius: 100px; box-shadow: 0 0 32px 0 #f09000; - background-color: #f09000; + background: linear-gradient(to bottom,rgba(240,144,0,1) 0%,rgba(240,144,0,1) 33%,rgba(240,0,240,1) 100%); transform: translate(-50%, -35%); } .Sun-top { - clip-path: inset(-50px -50px 166px -50px); + clip-path: inset(-50px -50px 167px -50px); } .Sun-middle-top { @@ -180,15 +191,17 @@ } .Sun-middle { - clip-path: inset(56px -50px 126px -50px); + clip-path: inset(57px -50px 126px -50px); } .Sun-middle-bottom { + box-shadow: 0 0 32px 0 #f0752d; clip-path: inset(82px -50px 97px -50px); } .Sun-bottom { - clip-path: inset(112px -50px -50px -50px); + box-shadow: 0 0 32px 0 #f05d55; + clip-path: inset(112px -50px 77px -50px); } .Ocean { @@ -198,6 +211,11 @@ height: 50%; overflow: hidden; background: #000; + transition: background-color 1s ease-in-out; +} + +.Ocean.highlight { + background-color: rgba(0, 215, 215, .1); } .Ocean-background { @@ -212,6 +230,15 @@ ); } +@keyframes laserScroll { + 0% { + transform: perspective(200px) rotateX(38deg) scale(2,1) translateZ(0) translateX(0px); + } + 100% { + transform: perspective(200px) rotateX(38deg) scale(2,1) translateZ(0) translateX(-120px); + } +} + .Lasers { position: absolute; overflow: hidden; @@ -223,7 +250,7 @@ /* 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); + transform: perspective(200px) rotateX(38deg) scale(2,1) translateZ(0) translateX(0%); content: ""; display: block; position: absolute; @@ -247,6 +274,8 @@ background-image: linear-gradient(to right, cyan 10px, transparent 0), linear-gradient(to bottom, cyan 16px, transparent 0); + background-repeat: repeat; + animation: laserScroll 5s linear infinite; } .Lasers-horizon { @@ -277,8 +306,48 @@ transition: width 1s cubic-bezier(0, .3, .5, 1), left 1s cubic-bezier(0, .3, .5, 1), margin-top 1s ease-in-out; } -.Lamps { +.SunReflection-0 { + box-shadow: 0 0 16px 1px #f0585e; + background-color: #f0585ead; +} + +.SunReflection-1 { + box-shadow: 0 0 16px 1px #f05d55; + background-color: #f05d55ad; +} + +.SunReflection-2 { + box-shadow: 0 0 16px 1px #f06c3c; + background-color: #f06c3cad; +} + +.SunReflection-3 { + box-shadow: 0 0 16px 1px #f07a25; + background-color: #f07a25ad; +} + +.SunReflection-4 { + box-shadow: 0 0 16px 1px #f08b05; + background-color: #f08b05ad; +} + +.StaticList { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + opacity: 0.8; +} + +.Static { position: absolute; - right: 2%; - bottom: 10%; + z-index: 101; + width: 100%; + height: 4%; + background-color: #999; + background-size: 75% 75%; + background-repeat: repeat; + border-top: 1px solid rgba(36, 36, 36, 0.3); + border-bottom: 1px solid rgba(150, 150, 150, 0.3); + opacity: 1; } diff --git a/src/channels/laser-sunset/types.ts b/src/channels/laser-sunset/types.ts index df463e5..2bed5a6 100644 --- a/src/channels/laser-sunset/types.ts +++ b/src/channels/laser-sunset/types.ts @@ -1,6 +1,8 @@ import type { FormattedDonation } from '@gdq/types/tracker'; -export type StarVisual = { left: number; top: number; text: string; color: string; opacity: number }; +export type RGB = { r: number; g: number; b: number }; + +export type StarVisual = { left: number; top: number; text: string; color: RGB; opacity: number }; export type SunReflectionLine = { xPosition: number; marginTop: number; width: number }; @@ -26,3 +28,9 @@ export type SubscriptionVisual = { color: string; received: Date; }; + +export type Static = { + spawnDate: Date; + maxAge: number; + height: number; +};