+8
-1
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { registerRoot } from "remotion";
|
||||
import { RemotionRoot } from "./Root";
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user