feat: base setup

Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
Varun Raj
2024-12-02 00:03:55 +05:30
parent 7639de9e45
commit 759d772423
14 changed files with 668 additions and 4288 deletions
+8 -1
View File
@@ -1,3 +1,10 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"plugins": ["@remotion"],
"overrides": [
{
"files": ["remotion/*.{ts,tsx}"],
"extends": ["plugin:@remotion/recommended"]
}
]
}
BIN
View File
Binary file not shown.
+3
View File
@@ -30,6 +30,8 @@
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@remotion/cli": "4.0.232",
"@remotion/eslint-plugin": "^4.0.232",
"@tanstack/react-table": "^8.20.1",
"@types/cookie": "^0.6.0",
"@types/qs": "^6.9.15",
@@ -54,6 +56,7 @@
"react-grid-gallery": "^1.0.1",
"react-query": "^3.39.3",
"recharts": "^2.12.7",
"remotion": "4.0.232",
"sass": "^1.77.8",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
import { AbsoluteFill, useCurrentFrame, interpolate, staticFile, Audio } from "remotion";
import WelcomeScene from "./rewind/WelcomeScene";
import CountryScene from "./rewind/CountryScene";
import MemoriesScene from "./rewind/MemoriesScene";
import AlbumsScene from "./rewind/AlbumScene";
import FriendsScene from "./rewind/FriendsScene";
export interface Scene {
message: string;
type: string;
emoji: string;
data: any;
}
export const IntroComposition = ({ scenes }: { scenes: Scene[] }) => {
const frame = useCurrentFrame();
const screenDuration = 120;
const sceneIndex = Math.floor(frame / screenDuration) % scenes.length;
const scene = scenes[sceneIndex];
const renderScene = () => {
switch (scene.type) {
case "WELCOME":
return <WelcomeScene message={scene.message} emoji={scene.emoji} />;
case "COUNTRY":
return <CountryScene message={scene.message} emoji={scene.emoji} data={scene.data} />;
case "MEMORY":
return <MemoriesScene message={scene.message} emoji={scene.emoji} data={scene.data} />;
case "ALBUM":
return <AlbumsScene message={scene.message} emoji={scene.emoji} data={scene.data} />;
case "FRIEND":
return <FriendsScene message={scene.message} emoji={scene.emoji} data={scene.data} />;
default:
return <WelcomeScene message={scene.message} emoji={scene.emoji} />;
}
}
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
backgroundColor: "black",
transition: "background-color 1s ease",
fontFamily: "sans-serif",
}}
>
{renderScene()}
<Audio src={staticFile("audio/rewind.mp3")} />
</AbsoluteFill>
);
};
+104
View File
@@ -0,0 +1,104 @@
import React from 'react';
import { Composition } from 'remotion';
import { IntroComposition, Scene } from './Composition';
const scenes: Scene[] = [
{ message: "Welcome to Immich Replay Varun!",
type: "WELCOME",
emoji: "👋",
data: {},
},
{
message: "2024! So much memories!",
type: "TITLE",
emoji: "📅",
data: {
photos: 100,
},
},
{
message: "You've been to 10 new countries!", type: "COUNTRY", emoji: "🌍", data: {
countries: ["India", "USA", "Canada", "UK", "Australia", "New Zealand", "Singapore", "Malaysia", "Thailand", "Japan", "South Korea"],
},
},
{
message: "And 25 new cities!", type: "CITY", emoji: "🏙️", data: {
cities: 25,
},
},
{
message: "Here are some of my favorite memories from this year!", type: "MEMORY", emoji: "🎉", data: {
photos: 100,
images: [
"https://picsum.photos/id/237/200/300",
"https://picsum.photos/id/238/200/300",
"https://picsum.photos/id/239/200/300",
"https://picsum.photos/id/240/200/300",
"https://picsum.photos/id/241/200/300",
],
},
},
{
message: "The most memory filled album of all time!", type: "ALBUM", emoji: "📸", data: {
photos: 100,
albums: [
{
name: "Ladakh 2024",
cover: "https://picsum.photos/id/400/200/300",
},
{
name: "Kerala 2024",
cover: "https://picsum.photos/id/401/200/300",
},
{
name: "Mumbai 2024",
cover: "https://picsum.photos/id/402/200/300",
},
{
name: "Kerala 2023",
cover: "https://picsum.photos/id/403/200/300",
},
],
},
},
{
message: "You made so much memory with Soundarapandian", type: "FRIEND", emoji: "👯", data: {
friends: [
{
name: "Soundarapandian",
cover: "https://i.pravatar.cc/150?img=64",
},
{
name: "Varun",
cover: "https://i.pravatar.cc/150?img=65",
},
],
},
},
{
message: "Lets see what 2025 has in store!", type: "END", emoji: "🚀", data: {
},
},
]
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="Intro"
defaultProps={{ scenes }}
component={IntroComposition}
durationInFrames={scenes.length * 120}
fps={30}
width={1280}
height={720}
/>
</>
);
};
+4
View File
@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);
+112
View File
@@ -0,0 +1,112 @@
/* eslint-disable @next/next/no-img-element */
import React, { useMemo, useRef } from 'react'
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
interface AlbumsSceneProps {
message: string;
emoji: string;
data: {
albums: {
name: string;
cover: string;
}[];
};
}
export default function AlbumsScene({ message, emoji, data }: AlbumsSceneProps) {
const albums = useMemo(() => data.albums.map((album, index) => ({
name: album.name,
cover: album.cover,
})), [data.albums]);
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const initialDelay = useRef(frame);
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
animation: "zoomIn 1s ease forwards",
}}>
<p style={{
fontSize: 70,
padding: "0 70px",
color: "lightblue",
marginTop: 40,
textAlign: "center",
transition: "opacity 1s ease",
}}>
{message}
</p>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: 20,
}}>
{albums.map((album, index) => {
const delay = (initialDelay.current) + (index * 0.1 * fps);
const scale = spring({
frame: frame - delay,
fps,
from: 0,
to: 1,
config: {
damping: 12,
},
});
return (
<div key={index} style={{
border: "1px solid lightblue",
borderRadius: 10,
padding: 10,
}}>
<img
key={index}
src={album.cover}
alt="memory"
style={{
width: 200,
height: 200,
objectFit: "cover",
borderRadius: 10,
margin: 10,
transform: `scale(${scale})`,
}}
/>
<p style={{
fontSize: 20,
textAlign: "center",
marginTop: 10,
color: "lightblue",
}}>
{album.name}
</p>
</div>
)
})}
</div>
</div>
)
}
// Add this CSS to your global styles or a CSS-in-JS solution
const styles = `
@keyframes zoomIn {
from {
transform: scale(0.5);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
`;
// Inject styles into the document
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
+89
View File
@@ -0,0 +1,89 @@
import React from 'react'
import { spring, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
interface CountrySceneProps {
message: string;
emoji: string;
data: any;
}
export default function CountryScene({ message, emoji, data }: CountrySceneProps) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const countries = data.countries as string[];
// Message animation (takes 1 second to appear)
const messageOpacity = interpolate(
frame,
[240, 260],
[0, 1],
{ extrapolateRight: 'clamp' }
);
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}>
<p style={{
fontSize: 40,
color: "lightblue",
marginTop: 40,
textAlign: "center",
padding: "0 20px",
opacity: messageOpacity,
}}>
{message}
</p>
<div style={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
padding: 20,
gap: 10,
width: "70%",
}}>
{countries.map((country: string, index: number) => {
// Start countries after message (1 second) with 0.3 second delay between each
const delay = (240) + (index * 0.1 * fps);
const scale = spring({
frame: frame - delay,
fps,
from: 0,
to: 1,
config: {
damping: 12,
},
});
return (
<div key={country} style={{
borderColor: "lightblue",
borderRadius: "25px",
height: 50,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "lightblue",
padding: "0px 20px",
fontSize: 20,
textAlign: "center",
borderWidth: 1,
borderStyle: "solid",
transform: `scale(${scale})`,
opacity: scale,
}}>
<div>
<p style={{ margin: 0 }}>{country}</p>
</div>
</div>
);
})}
</div>
</div>
)
}
+111
View File
@@ -0,0 +1,111 @@
/* eslint-disable @next/next/no-img-element */
import React, { useMemo, useRef } from 'react'
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
interface FriendsSceneProps {
message: string;
emoji: string;
data: {
friends: {
name: string;
cover: string;
}[];
};
}
export default function FriendsScene({ message, emoji, data }: FriendsSceneProps) {
const friends = useMemo(() => data.friends.map((friend, index) => ({
name: friend.name,
cover: friend.cover,
})), [data.friends]);
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const initialDelay = useRef(frame);
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
animation: "zoomIn 1s ease forwards",
}}>
<p style={{
fontSize: 70,
padding: "0 70px",
color: "lightblue",
marginTop: 40,
textAlign: "center",
transition: "opacity 1s ease",
}}>
{message}
</p>
<div style={{
display: "flex",
flexDirection: "row",
gap: 20,
}}>
{friends.map((friend, index) => {
const delay = (initialDelay.current) + (index * 0.1 * fps);
const scale = spring({
frame: frame - delay,
fps,
from: 0,
to: 1,
config: {
damping: 12,
},
});
return (
<div key={index} style={{
borderRadius: 10,
padding: 10,
}}>
<img
key={index}
src={friend.cover}
alt="memory"
style={{
width: 200,
height: 200,
objectFit: "cover",
borderRadius: "100%",
margin: 10,
transform: `scale(${scale})`,
}}
/>
<p style={{
fontSize: 20,
textAlign: "center",
marginTop: 10,
color: "lightblue",
}}>
{friend.name}
</p>
</div>
)
})}
</div>
</div>
)
}
// Add this CSS to your global styles or a CSS-in-JS solution
const styles = `
@keyframes zoomIn {
from {
transform: scale(0.5);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
`;
// Inject styles into the document
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
+95
View File
@@ -0,0 +1,95 @@
import React, { useMemo, useRef } from 'react'
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
interface MemoriesSceneProps {
message: string;
emoji: string;
data: {
images: string[];
};
}
export default function MemoriesScene({ message, emoji, data }: MemoriesSceneProps) {
const images = useMemo(() => data.images.map((image, index) => ({
src: image,
rotate: `${(index % 2 === 0 ? 1 : -1) * Math.random() * 10}deg`,
})), [data.images]);
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const initialDelay = useRef(frame);
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
animation: "zoomIn 1s ease forwards",
}}>
<p style={{
fontSize: 70,
padding: "0 70px",
color: "lightblue",
marginTop: 40,
textAlign: "center",
transition: "opacity 1s ease",
}}>
{message}
</p>
<div style={{
display: "flex",
flexDirection: "row",
gap: 10,
}}>
{images.map((image, index) => {
const delay = (initialDelay.current) + (index * 0.1 * fps);
const scale = spring({
frame: frame - delay,
fps,
from: 0,
to: 1,
config: {
damping: 12,
},
});
return (
<img
key={index}
src={image.src}
alt="memory"
style={{
width: 200,
height: 200,
objectFit: "cover",
borderRadius: 10,
margin: 10,
rotate: image.rotate,
transform: `scale(${scale})`,
}}
/>
)
})}
</div>
</div>
)
}
// Add this CSS to your global styles or a CSS-in-JS solution
const styles = `
@keyframes zoomIn {
from {
transform: scale(0.5);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
`;
// Inject styles into the document
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
+58
View File
@@ -0,0 +1,58 @@
import React from 'react'
interface WelcomeSceneProps {
message: string;
emoji: string;
}
export default function WelcomeScene({ message, emoji }: WelcomeSceneProps) {
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
animation: "zoomIn 1s ease forwards",
}}>
<p style={{
fontSize: 100,
color: "lightblue",
marginTop: 40,
marginBottom: 0,
textAlign: "center",
transition: "opacity 1s ease",
}}>
{emoji}
</p>
<p style={{
fontSize: 70,
padding: "0 70px",
color: "lightblue",
marginTop: 0,
textAlign: "center",
transition: "opacity 1s ease",
}}>
{message}
</p>
</div>
)
}
// Add this CSS to your global styles or a CSS-in-JS solution
const styles = `
@keyframes zoomIn {
from {
transform: scale(0.5);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
`;
// Inject styles into the document
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
+33
View File
@@ -0,0 +1,33 @@
import React from 'react'
import { Player } from '@remotion/player';
import { IntroComposition } from '../../remotion/Composition';
const scenes = [
{ message: "Welcome to Immich Replay Varun!" },
{ message: "2024! So much memories!" },
{ message: "You've been to 10 new countries!" },
{ message: "And 25 new cities!" },
{ message: "Here are some of my favorite memories from this year!" },
{ message: "The most memory filled album of all time!" },
{ message: "You made so much memory with Soundarapandian"},
]
export default function RewindPage() {
return (
<div>
<Player
component={IntroComposition}
inputProps={{ scenes: scenes }}
durationInFrames={scenes.length * 30}
compositionWidth={1280}
compositionHeight={720}
fps={30}
style={{
width: 1280,
height: 720,
}}
controls
/>
</div>
)
}
-4287
View File
File diff suppressed because it is too large Load Diff