birthdays

This commit is contained in:
Elizabeth Hunt 2024-02-11 20:44:17 -07:00
parent 9db4283a20
commit c55d9d2832
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
13 changed files with 597 additions and 12 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
`animations don't need to be written cleanly :))`

15
public/img/profile.svg Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 72 72" id="emoji" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#D0CFCE" stroke="none" d="M58,61c0,0,0-3-1-7c-1.2109-4.8457-4-8-10-8c-5,0-15,0-22,0c-6,0-8.7891,3.1543-10,8c-1,4-1,7-1,7H58z"/>
<path fill="#D0CFCE" stroke="none" d="M26,26c0,3.7246,0.5391,7.8086,2,10c1.8613,2.793,5.0176,4,8,4c3.0957,0,6.1367-1.207,8-4 c1.46-2.1914,2-6.2754,2-10c0-2.7935-1-12-10-12S26,21.3442,26,26z"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M58,60c0,0,0-2-1-6 c-1.2109-4.8457-4-8-10-8c-5,0-15,0-22,0c-6,0-8.7891,3.1543-10,8c-1,4-1,6-1,6"/>
<path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M26,26c0,3.7246,0.5391,7.8086,2,10 c1.8613,2.793,5.0176,4,8,4c3.0957,0,6.1367-1.207,8-4c1.46-2.1914,2-6.2754,2-10c0-2.7935-1-12-10-12S26,21.3442,26,26z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -84,7 +84,7 @@ def make_valentines_letters(people):
Good! Now they will all share our love. Good! Now they will all share our love.
But a few months goes by, and soon all their birthdays are coming up! So we make another black box to generate a letter, and in how many days we should give it to them: [SPACE] But a few months goes by, and all our friends' birthdays are soon coming up (why are so many cs people born in June)! We don't want to make them sad, as we see here, so we make another black box to generate a letter, and in how many days we should give it to them:
```python ```python
def make_birthday_cards(people): def make_birthday_cards(people):
@ -101,9 +101,11 @@ def make_birthday_cards(people):
return cards return cards
``` ```
But this is getting annoying; what about Christmas, Thanksgiving, Easter, or another life event such as a wedding? Making this function to make a list of new cards, going through each person every single time is getting really tedious. There, now, we can make sure our friends are happy on their birthdays!
To help, we create a bunch of black boxes that take a person and create them a card, specifically. Then we create a black box that takes a list of people and another black box and creates a new card for each person? But, this is getting annoying; what about Christmas, Thanksgiving, or Easter? Making a new black box to make a list of new cards, going through each person, every single time to construct a list of cards, is getting really tedious.
What if we generalized this? We create a bunch of black boxes that take a person, and generate them a card, specifically; like a template for a card you could print off and fill in manually.
```python ```python
def valentine_letter(person): def valentine_letter(person):
@ -116,7 +118,11 @@ def birthday_card(person):
card = f"Happy Birthday {name}\nI can't believe you're already {newAge} years old!" card = f"Happy Birthday {name}\nI can't believe you're already {newAge} years old!"
cards.append({ "message": card, "deliverInDays": daysUntilBirthday }) cards.append({ "message": card, "deliverInDays": daysUntilBirthday })
```
Then, we can use a black box that takes a list of people, and applies this template to each person.
```
def buildCards(people, cardMaker): def buildCards(people, cardMaker):
cards = [] cards = []
for person in people: for person in people:
@ -128,7 +134,7 @@ people = [{"name": "Joseph", birthday: new Date()}, {"name": "DeeDee", birthday:
buildCards(people, birthdayCard) buildCards(people, birthdayCard)
``` ```
The ability to pass a function around like this - like a variable - is what makes functions "first class". And the `buildCards` function takes a function as input, making it a "higher order function". (TODO: slides) The ability in a language to pass a function around like this - like a variable - is what makes functions "first class". And the `buildCards` function takes a function as input, making it a "higher order function". (TODO: slides)
Functional Reproduction Functional Reproduction
=== ===
@ -250,7 +256,7 @@ But there are also downsides:
+ Keeping immutability requires more computation (data structures) + Keeping immutability requires more computation (data structures)
+ More difficult to write (huge selling point for Object Oriented Programming; encapsulation) + More difficult to write (huge selling point for Object Oriented Programming; encapsulation)
But like all relationships, we need to compromise with our black boxes. Side effects are _unavoidable_ as programmers; when people press the button, they don't want the computation to just exist out there in the computer void. No! They want to see the pop up box, and the value updated on their account. We need to be able to mutate state where the tradeoff of extra compute time is unavoidable like in I/O or a relatively large database. But like all relationships, we need to compromise with our black boxes. Side effects are effectively _unavoidable_ as programmers; when people press the button, they don't want the computation to just exist out there in the computer void. No! They want to see the pop up box, and the value updated on their account. We need to be able to mutate state where the tradeoff of extra compute time is unavoidable like in I/O or a relatively large database.
Writing Code Functionally Writing Code Functionally
=== ===

View File

@ -20,7 +20,7 @@ export interface FunctionBoxProps extends NodeProps {
arity?: number; arity?: number;
isChild?: boolean; isChild?: boolean; // todo: attach DNA
} }
type FunctionArgs = { node?: Node; val: any }[]; type FunctionArgs = { node?: Node; val: any }[];
@ -59,7 +59,7 @@ export class FunctionBox extends Node {
this.arity = props?.arity ?? 1; this.arity = props?.arity ?? 1;
if (props.fn) { if (props.fn) {
this.source = props.fn.toString(); this.source = props.source ?? props.fn.toString();
this.function = props.fn; this.function = props.fn;
} else { } else {
this.source = props?.source ?? `(x: number): number => x + 2`; this.source = props?.source ?? `(x: number): number => x + 2`;
@ -273,7 +273,7 @@ export class FunctionBox extends Node {
<CodeBlock <CodeBlock
fontFamily={theme.font} fontFamily={theme.font}
language="typescript" language="typescript"
fontSize={15} fontSize={this.outputFontSize}
code={JSON.stringify(output, null, 2)} code={JSON.stringify(output, null, 2)}
></CodeBlock>, ></CodeBlock>,
); );

105
src/components/person.tsx Normal file
View File

@ -0,0 +1,105 @@
import { Layout, Node, NodeProps, SVG, Txt } from "@motion-canvas/2d";
import { theme } from "../theme";
import { all, createRef, waitFor } from "@motion-canvas/core";
export const PEOPLE: PersonI[] = [
{
name: "Alan Turing",
birthday: new Date("06/23/1912"),
color: theme.green.hex,
},
{
name: "Grace Hopper",
birthday: new Date("12/09/1906"),
color: theme.flamingo.hex,
},
{
name: "Edsger Dijkstra",
birthday: new Date("07/11/1930"),
color: theme.red.hex,
},
{
name: "Alonzo Church",
birthday: new Date("06/14/1912"),
color: theme.sapphire.hex,
},
{
name: "Margaret Hamilton",
birthday: new Date("12/09/1902"),
color: theme.yellow.hex,
},
];
const profileSrc = (color: string) => `
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 72 72" id="emoji" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="${color}" stroke="none" d="M58,61c0,0,0-3-1-7c-1.2109-4.8457-4-8-10-8c-5,0-15,0-22,0c-6,0-8.7891,3.1543-10,8c-1,4-1,7-1,7H58z"/>
<path fill="${color}" stroke="none" d="M26,26c0,3.7246,0.5391,7.8086,2,10c1.8613,2.793,5.0176,4,8,4c3.0957,0,6.1367-1.207,8-4 c1.46-2.1914,2-6.2754,2-10c0-2.7935-1-12-10-12S26,21.3442,26,26z"/>
</g>
<g id="line">
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M58,60c0,0,0-2-1-6 c-1.2109-4.8457-4-8-10-8c-5,0-15,0-22,0c-6,0-8.7891,3.1543-10,8c-1,4-1,6-1,6"/>
<path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M26,26c0,3.7246,0.5391,7.8086,2,10 c1.8613,2.793,5.0176,4,8,4c3.0957,0,6.1367-1.207,8-4c1.46-2.1914,2-6.2754,2-10c0-2.7935-1-12-10-12S26,21.3442,26,26z"/>
</g>
</svg>`;
export interface PersonI {
name: string;
birthday: Date;
color: string;
}
export interface PersonProps extends NodeProps {
person: PersonI;
width?: number;
height?: number;
}
export class Person extends Node {
private readonly svg = createRef<SVG>();
public constructor(props?: PersonProps) {
super({ ...props });
this.add(
<Layout direction="column" alignItems="center" layout>
<SVG
ref={this.svg}
svg={profileSrc(props.person.color)}
fill="green"
width={props.width ?? 150}
height={props.height ?? 150}
></SVG>
<Txt fontSize={20} fontFamily={theme.font} fill={theme.text.hex}>
{props.person.name}
</Txt>
</Layout>,
);
}
public *emit(text: string, duration: number, cleanUp = true) {
const ref = createRef<Txt>();
this.insert(
<Txt
position={this.svg().bottomRight()}
ref={ref}
fontSize={0}
opacity={0}
fontFamily={theme.font}
fill={theme.text.hex}
>
{text}
</Txt>,
0,
);
yield* all(ref().fontSize(40, duration), ref().opacity(1, duration));
if (cleanUp) {
yield* waitFor(duration);
yield* all(ref().fontSize(0, duration), ref().opacity(0, duration));
yield ref().remove();
}
}
}

View File

@ -0,0 +1,5 @@
{
"version": 0,
"timeEvents": [],
"seed": 1594477027
}

View File

@ -0,0 +1,213 @@
import { Layout, Txt, makeScene2D } from "@motion-canvas/2d";
import {
Direction,
all,
beginSlide,
createRef,
makeRef,
slideTransition,
waitFor,
} from "@motion-canvas/core";
import { FunctionBox } from "../components/function_box";
import { theme } from "../theme";
import { PEOPLE, Person, PersonI } from "../components/person";
const daysUntilNextBirthday = (birthDate: Date): number => {
const today = new Date();
const nextBirthday = new Date(
today.getFullYear(),
birthDate.getMonth(),
birthDate.getDate(),
);
if (today > nextBirthday) nextBirthday.setFullYear(today.getFullYear() + 1);
const timeDiff = nextBirthday.getTime() - today.getTime();
return Math.ceil(timeDiff / (1000 * 3600 * 24));
};
export interface CardI {
message: string;
deliverInDays: number;
}
export const birthdayCardsFor = (people: PersonI[]): CardI[] => {
const cards: CardI[] = [];
for (const person of people) {
const age = new Date().getFullYear() - person.birthday.getFullYear();
const ending =
({ 1: "st", 2: "nd", 3: "rd" } as Record<number, string>)[
parseInt(Array.from(age.toString()).at(-1))
] ?? "th";
const message =
`Happy ${age}${ending} birthday, ${person.name}!\n` +
"I can't believe it's already been a year!";
const deliverInDays = daysUntilNextBirthday(person.birthday);
cards.push({ deliverInDays, message });
}
return cards;
};
const birthdayCardsSrc = `export interface CardI {
message: string;
deliverInDays: number;
}
const birthdayCardsFor = (people: PersonI[]): CardI[] => {
const cards: CardI[] = [];
for (const person of people) {
const age = new Date().getFullYear() - person.birthday.getFullYear();
const ageEnding = (
{ 1: "st", 2: "nd", 3: "rd" } as Record<number, string>
)[parseInt(Array.from(age.toString()).at(-1))] ?? "th";
const message =
\`Happy \${age}\${ageEnding} birthday, \${person.name}!\\n\` +
"I can't believe it's already been a year!";
const deliverInDays = daysUntilNextBirthday(person.birthday);
cards.push({ deliverInDays, message });
}
return cards;
};`;
export default makeScene2D(function* (view) {
const layout = createRef<Layout>();
const date = createRef<Txt>();
const functionBox = createRef<FunctionBox>();
const people: Person[] = [];
const peopleLayout: Layout[] = [];
const peopleText: Txt[] = [];
view.add(
<FunctionBox
ref={functionBox}
source={birthdayCardsSrc}
fn={birthdayCardsFor}
workingText="📝⚙"
outputFontSize={25}
></FunctionBox>,
);
yield* all(slideTransition(Direction.Left), functionBox().showCode(0.75));
yield* functionBox().reset(0.1);
yield* beginSlide("Show code");
yield* functionBox().reset(0.1);
yield* all(
functionBox().hideCode(0.8),
functionBox().setInputs(
[
{
val: PEOPLE,
node: (
<Layout direction="column" gap={5} layout>
{PEOPLE.map((person) => (
<Person person={person} />
))}
</Layout>
),
},
],
0.8,
),
);
yield* beginSlide("Show people");
yield* functionBox().propogateInput(0.6);
yield* functionBox().propogateOutput(0.6);
yield* beginSlide("Generate birthday cards");
yield* functionBox().opacity(0, 0.5);
functionBox().remove();
view.add(
<Layout opacity={0} ref={layout} direction="column" gap={15} layout>
<Txt
fontSize={30}
fontFamily={theme.font}
fill={theme.text.hex}
ref={date}
/>
<Layout direction="row" gap={15}>
{PEOPLE.map((person, i) => (
<Layout
ref={makeRef(peopleLayout, i)}
alignItems="center"
direction="column"
gap={100}
>
<Person ref={makeRef(people, i)} person={person} />
<Txt
textAlign="center"
ref={makeRef(peopleText, i)}
fontSize={0}
fontFamily={theme.font}
fill={theme.text.hex}
/>
</Layout>
))}
</Layout>
</Layout>,
);
yield* layout().opacity(1, 0.5);
const cards = birthdayCardsFor(PEOPLE);
for (let i = 1; i <= 365 + 1; i++) {
const day = new Date(Date.now() + i * 24 * 60 * 60 * 1000);
yield* waitFor(0.02);
yield date().text(day.toDateString());
const peoplesBirthday = cards
.map((x, i) => [x, i] as [CardI, number])
.filter(([{ deliverInDays }]) => deliverInDays === i)
.map(([_, i]) => i);
yield* all(
...peoplesBirthday.map((p) => {
const text = peopleText[p];
return all(
text.text("🗓️🎂\n" + cards[p].message, 0),
text.fontSize(20, 0.5),
);
}),
);
yield* all(
...peoplesBirthday.map((p) => {
const layout = peopleLayout[p];
return layout.gap(0, 0.5);
}),
);
yield* all(
...peoplesBirthday.map((p) => {
const person = people[p];
const text = peopleText[p];
return all(
text.opacity(0, 0.5),
text.fontSize(0, 0.5),
person.emit("🥳", 0.8, false),
);
}),
);
}
yield* beginSlide("See their reactions");
yield* all(
date().fontSize(0, 0.5),
date().opacity(0, 0.65),
...people.map((person) => person.opacity(0, 0.5)),
);
});

View File

@ -6,6 +6,9 @@ import doctor from "./doctor?scene";
import first_box from "./first_box?scene"; import first_box from "./first_box?scene";
import hungry_partner from "./hungry_partner?scene"; import hungry_partner from "./hungry_partner?scene";
import pure_functions from "./pure_functions?scene"; import pure_functions from "./pure_functions?scene";
import valentines_letters from "./valentines_letters?scene";
import birthday_letters from "./birthday_letters?scene";
import sad_people from "./sad_people?scene";
export const scenes = [ export const scenes = [
//title, //title,
@ -14,6 +17,9 @@ export const scenes = [
//flirtingwithfunctions, //flirtingwithfunctions,
//doctor, //doctor,
//first_box, //first_box,
hungry_partner, //hungry_partner,
pure_functions, //pure_functions,
//valentines_letters,
sad_people,
birthday_letters,
]; ];

View File

@ -0,0 +1,5 @@
{
"version": 0,
"timeEvents": [],
"seed": 867976792
}

99
src/scenes/sad_people.tsx Normal file
View File

@ -0,0 +1,99 @@
import { Layout, Txt, makeScene2D } from "@motion-canvas/2d";
import {
Direction,
all,
beginSlide,
createRef,
makeRef,
slideTransition,
waitFor,
} from "@motion-canvas/core";
import { theme } from "../theme";
import { PEOPLE, Person } from "../components/person";
import { CardI, birthdayCardsFor } from "./birthday_letters";
export default makeScene2D(function* (view) {
const date = createRef<Txt>();
const people: Person[] = [];
const peopleLayout: Layout[] = [];
const peopleText: Txt[] = [];
view.add(
<Layout direction="column" gap={15} layout>
<Txt
fontSize={30}
fontFamily={theme.font}
fill={theme.text.hex}
ref={date}
/>
<Layout direction="row" gap={15}>
{PEOPLE.map((person, i) => (
<Layout
ref={makeRef(peopleLayout, i)}
alignItems="center"
direction="column"
gap={100}
>
<Person ref={makeRef(people, i)} person={person} />
<Txt
textAlign="center"
ref={makeRef(peopleText, i)}
fontSize={0}
fontFamily={theme.font}
fill={theme.text.hex}
/>
</Layout>
))}
</Layout>
</Layout>,
);
yield* all(slideTransition(Direction.Right));
const cards = birthdayCardsFor(PEOPLE);
for (let i = 1; i <= 365 + 1; i++) {
const day = new Date(Date.now() + i * 24 * 60 * 60 * 1000);
yield* waitFor(0.01);
yield date().text(day.toDateString());
const peoplesBirthday = cards
.map((x, i) => [x, i] as [CardI, number])
.filter(([{ deliverInDays }]) => deliverInDays === i)
.map(([_, i]) => i);
yield* all(
...peoplesBirthday.map((p) => {
const text = peopleText[p];
return all(text.text("🗓️🎂", 0), text.fontSize(50, 0.5));
}),
);
yield* all(
...peoplesBirthday.map((p) => {
const layout = peopleLayout[p];
return layout.gap(0, 0.5);
}),
);
yield* all(
...peoplesBirthday.map((p) => {
const person = people[p];
const text = peopleText[p];
return all(
text.opacity(0, 0.5),
text.fontSize(0, 0.5),
person.emit(":(", 0.8, false),
);
}),
);
}
yield* beginSlide("See their reactions");
yield* all(
date().fontSize(0, 0.5),
date().opacity(0, 0.65),
...people.map((person) => person.opacity(0, 0.5)),
);
});

View File

@ -0,0 +1,5 @@
{
"version": 0,
"timeEvents": [],
"seed": 3850509512
}

View File

@ -0,0 +1,122 @@
import { Layout, Txt, makeScene2D } from "@motion-canvas/2d";
import {
Direction,
all,
beginSlide,
createRef,
makeRef,
slideTransition,
} from "@motion-canvas/core";
import { FunctionBox } from "../components/function_box";
import { theme } from "../theme";
import { PEOPLE, Person } from "../components/person";
const valentineLetterGenerator = `
interface PersonI {
name: string;
birthday: Date;
color: string;
}
(function valentinesLettersFor(people: PersonI[]) {
const letters: string[] = [];
for (const person of people) {
const letter = "Dear, " + person.name + "\\n"
+ "Your smile lights up my world.\\n"
+ "Happy Valentine's Day!";
letters.push(letter);
}
return letters;
})`;
export default makeScene2D(function* (view) {
const layout = createRef<Layout>();
const functionBox = createRef<FunctionBox>();
const people: Person[] = [];
const peopleLayout: Layout[] = [];
const peopleText: Txt[] = [];
view.add(
<FunctionBox
ref={functionBox}
source={valentineLetterGenerator}
workingText="📝⚙"
outputFontSize={25}
></FunctionBox>,
);
yield* all(slideTransition(Direction.Left), functionBox().showCode(0.75));
yield* functionBox().reset(0.1);
yield* beginSlide("Show code");
yield* functionBox().reset(0.1);
yield* all(
functionBox().hideCode(0.8),
functionBox().setInputs(
[
{
val: PEOPLE,
node: (
<Layout direction="column" gap={5} layout>
{PEOPLE.map((person) => (
<Person person={person} />
))}
</Layout>
),
},
],
0.8,
),
);
yield* beginSlide("Show people");
yield* functionBox().propogateInput(0.6);
yield* functionBox().propogateOutput(0.6);
yield* beginSlide("Generate valentines letters");
yield* functionBox().opacity(0, 0.5);
functionBox().remove();
view.add(
<Layout opacity={0} ref={layout} direction="row" gap={15} layout>
{PEOPLE.map((person, i) => (
<Layout
ref={makeRef(peopleLayout, i)}
alignItems="center"
direction="column"
gap={100}
>
<Person ref={makeRef(people, i)} person={person} />
<Txt
ref={makeRef(peopleText, i)}
fontSize={0}
fontFamily={theme.font}
fill={theme.text.hex}
/>
</Layout>
))}
</Layout>,
);
yield* layout().opacity(1, 0.5);
yield* all(...peopleText.map((text) => text.text("💌", 0)));
yield* all(...peopleText.map((text) => text.fontSize(50, 0.5)));
yield* all(...peopleLayout.map((layout) => layout.gap(0, 0.5)));
yield* all(
...peopleText.map((text) =>
all(text.fontSize(0, 0.5), text.opacity(0, 0.5)),
),
);
yield* beginSlide("Give people valentines letters");
yield* all(...people.map((person) => person.emit("😊", 0.5)));
yield* beginSlide("See their reactions");
yield* all(...people.map((person) => person.opacity(0, 0.5)));
});

View File

@ -1,4 +1,7 @@
{ {
"extends": "@motion-canvas/2d/tsconfig.project.json", "include": ["src"],
"include": ["src"] "compilerOptions": {
"target": "es2022"
},
"extends": "@motion-canvas/2d/tsconfig.project.json"
} }