moracarta 2.1.3 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +34 -30
- package/package.json +15 -10
- package/src/commands/setup.js +27 -11
- package/src/content/letters.example.js +9 -0
- package/src/loaders/lettersLoader.js +6 -0
- package/src/scripts/generatePalette.js +51 -0
- package/src/scripts/lettersPage.js +7 -58
- package/src/scripts/main.js +19 -56
- package/src/scripts/templateBinding.js +2 -5
- package/src/styles/letters.css +11 -11
- package/src/styles/main.css +27 -18
- package/src/views/letters.html +6 -6
- package/src/scripts/letters.js +0 -256
package/index.html
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
6
|
-
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
7
|
-
<title data-variable="TITLE"></title>
|
|
8
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
-
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300..700;1,300..700&family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lora:ital,wght@0,400;1,400&family=Montserrat:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
|
|
11
|
-
<link rel="stylesheet" href="src/styles/main.css">
|
|
12
|
-
</head>
|
|
13
|
-
<body>
|
|
14
|
-
<div class="header">
|
|
15
|
-
<div class="header-ornament">❧ ❦ ☙</div>
|
|
16
|
-
<h1 data-variable="TITLE"></h1>
|
|
17
|
-
<div class="header-line"></div>
|
|
18
|
-
<p class="header-sub love-note" data-variable="TEXT_TOP"></p>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<div class="letter-list" id="letter-list"></div>
|
|
22
|
-
|
|
23
|
-
<div class="
|
|
24
|
-
<div class="
|
|
25
|
-
<p data-variable="TEXT_BOTTOM"></p>
|
|
26
|
-
<p data-variable="YOUR_NAME"></p>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
6
|
+
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
7
|
+
<title data-variable="TITLE"></title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300..700;1,300..700&family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lora:ital,wght@0,400;1,400&family=Montserrat:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
|
|
11
|
+
<link rel="stylesheet" href="src/styles/main.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div class="header">
|
|
15
|
+
<div class="header-ornament">❧ ❦ ☙</div>
|
|
16
|
+
<h1 data-variable="TITLE"></h1>
|
|
17
|
+
<div class="header-line"></div>
|
|
18
|
+
<p class="header-sub love-note" data-variable="TEXT_TOP"></p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="letter-list" id="letter-list"></div>
|
|
22
|
+
|
|
23
|
+
<div class="bottom-text">
|
|
24
|
+
<div class="bottom-text-line"></div>
|
|
25
|
+
<p data-variable="TEXT_BOTTOM"></p>
|
|
26
|
+
<p data-variable="YOUR_NAME"></p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<footer class="footer">
|
|
30
|
+
<a id="footer-link" class="footer-link" href="https://github.com/wesleyhanauer/moracarta"></a>
|
|
31
|
+
</footer>
|
|
32
|
+
|
|
33
|
+
<script type="module" src="src/scripts/main.js"></script>
|
|
34
|
+
<script type="module" src="src/scripts/templateBinding.js"></script>
|
|
31
35
|
</html>
|
package/package.json
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moracarta",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"keywords": [
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"wrangler",
|
|
14
|
-
"gift",
|
|
15
|
-
"envelope",
|
|
16
|
-
"letters",
|
|
9
|
+
"love letter",
|
|
10
|
+
"digital love letter",
|
|
11
|
+
"romantic website",
|
|
12
|
+
"love website",
|
|
17
13
|
"romantic",
|
|
18
|
-
"
|
|
14
|
+
"love",
|
|
15
|
+
"letter",
|
|
16
|
+
"valentines",
|
|
17
|
+
"valentines day",
|
|
18
|
+
"anniversary",
|
|
19
|
+
"romantic website template",
|
|
20
|
+
"website template",
|
|
21
|
+
"static website",
|
|
22
|
+
"npm",
|
|
23
|
+
"open source"
|
|
19
24
|
],
|
|
20
25
|
"files": [
|
|
21
26
|
"index.html",
|
package/src/commands/setup.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
select,
|
|
3
|
-
input,
|
|
4
|
-
confirm
|
|
5
|
-
} from "@inquirer/prompts";
|
|
1
|
+
import { select, input, confirm } from "@inquirer/prompts";
|
|
6
2
|
import { writeFile, mkdir, cp, readFile } from "node:fs/promises";
|
|
7
3
|
import { resolve, dirname } from "node:path";
|
|
8
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -17,12 +13,12 @@ const PROJECT_NAME = await input({
|
|
|
17
13
|
});
|
|
18
14
|
|
|
19
15
|
const TITLE = await input({
|
|
20
|
-
message: "Enter the title of the application:",
|
|
16
|
+
message: "Enter the title of the application: ",
|
|
21
17
|
default: "Letters to my love"
|
|
22
18
|
});
|
|
23
19
|
|
|
24
20
|
const FONT = await select({
|
|
25
|
-
message: "What font would you like to use?",
|
|
21
|
+
message: "What font style would you like to use?",
|
|
26
22
|
choices: [
|
|
27
23
|
{ name: "Handwritten", value: "handwritten" },
|
|
28
24
|
{ name: "Classic", value: "classic" },
|
|
@@ -102,9 +98,15 @@ const DEFAULT_CLOSED_LETTER_TEXT_TOP_LINE =
|
|
|
102
98
|
const DEFAULT_CLOSED_LETTER_TEXT_BOTTOM_LINE =
|
|
103
99
|
"❤️ I love you ❤️";
|
|
104
100
|
|
|
105
|
-
const EDIT_ENVELOPE_TEXT = await
|
|
106
|
-
message: "Would you like to customize the envelope text?",
|
|
107
|
-
|
|
101
|
+
const EDIT_ENVELOPE_TEXT = await select({
|
|
102
|
+
message: "Would you like to customize the envelope text? ",
|
|
103
|
+
choices: [{
|
|
104
|
+
name: "yes",
|
|
105
|
+
value: true
|
|
106
|
+
},{
|
|
107
|
+
name: "no",
|
|
108
|
+
value: false
|
|
109
|
+
}],
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
let CLOSED_LETTER_TEXT_TOP_LINE = DEFAULT_CLOSED_LETTER_TEXT_TOP_LINE;
|
|
@@ -122,6 +124,17 @@ if (EDIT_ENVELOPE_TEXT) {
|
|
|
122
124
|
});
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
const SHOW_BRANDING = await select({
|
|
128
|
+
message: "Would you like to display 'Made with 💌 Moracarta' branding in the footer? ",
|
|
129
|
+
choices: [{
|
|
130
|
+
name: "yes",
|
|
131
|
+
value: true
|
|
132
|
+
},{
|
|
133
|
+
name: "no",
|
|
134
|
+
value: false
|
|
135
|
+
}],
|
|
136
|
+
});
|
|
137
|
+
|
|
125
138
|
const config = `/**
|
|
126
139
|
* This file was generated automatically by Moracarta setup.
|
|
127
140
|
*/
|
|
@@ -141,6 +154,8 @@ const CLOSED_LETTER_TEXT_TOP_LINE =
|
|
|
141
154
|
const CLOSED_LETTER_TEXT_BOTTOM_LINE =
|
|
142
155
|
${JSON.stringify(CLOSED_LETTER_TEXT_BOTTOM_LINE)};
|
|
143
156
|
|
|
157
|
+
const SHOW_BRANDING = ${SHOW_BRANDING};
|
|
158
|
+
|
|
144
159
|
const globalVariables = {
|
|
145
160
|
PROJECT_NAME,
|
|
146
161
|
TITLE,
|
|
@@ -149,7 +164,8 @@ const globalVariables = {
|
|
|
149
164
|
TEXT_TOP,
|
|
150
165
|
TEXT_BOTTOM,
|
|
151
166
|
CLOSED_LETTER_TEXT_TOP_LINE,
|
|
152
|
-
CLOSED_LETTER_TEXT_BOTTOM_LINE
|
|
167
|
+
CLOSED_LETTER_TEXT_BOTTOM_LINE,
|
|
168
|
+
SHOW_BRANDING
|
|
153
169
|
};
|
|
154
170
|
|
|
155
171
|
export default globalVariables;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// This file is the placeholder for the letters, this one is used until the user
|
|
2
|
+
// runs 'npx moracarta add' for the first time, then letters.js is created.
|
|
3
|
+
// This file is also usefull in the live showcase since it's built together
|
|
4
|
+
// with the rest of the app.
|
|
5
|
+
|
|
6
|
+
// Each different letter is a different object inside const letters.
|
|
7
|
+
// Color palettes are generated from the id, date and title.
|
|
8
|
+
// Since they are generated from a hashed string letters with
|
|
9
|
+
// the same information will always generate the same colors.
|
|
1
10
|
export const letters = [
|
|
2
11
|
{
|
|
3
12
|
id: 1,
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// This file checks wheter or not src/content/letters.js exists, if not
|
|
2
|
+
// it loads letters.example.js as the letter provider file.
|
|
3
|
+
// It is done this way so that the user doesn't have to manually remove the
|
|
4
|
+
// placeholder example letters before adding his own letters.
|
|
5
|
+
|
|
6
|
+
// Uses meta.glob to check files with given name.
|
|
1
7
|
const modules = import.meta.glob("../content/letters*.js", {
|
|
2
8
|
eager: true,
|
|
3
9
|
import: "letters",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// A set of base color families used to generate a unique
|
|
2
|
+
// envelope palette for each letter based on its id, title, and date.
|
|
3
|
+
// Same code as lettersPage.js so that the same color is applied on both
|
|
4
|
+
// main page and the envelope opening animation.
|
|
5
|
+
const paletteFamilies = [
|
|
6
|
+
{ hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
|
|
7
|
+
{ hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
|
|
8
|
+
{ hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
|
|
9
|
+
{ hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
|
|
10
|
+
{ hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
|
|
11
|
+
{ hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
|
|
12
|
+
{ hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
|
|
13
|
+
{ hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
|
|
14
|
+
{ hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
|
|
15
|
+
{ hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Simple hash function to determine which color palette to use on the envelope.
|
|
19
|
+
function hashString(value) {
|
|
20
|
+
let hash = 0;
|
|
21
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
22
|
+
hash = ((hash << 5) - hash) + value.charCodeAt(index);
|
|
23
|
+
hash |= 0;
|
|
24
|
+
}
|
|
25
|
+
return Math.abs(hash);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Keeps color value within a set range.
|
|
29
|
+
function clamp(value, min, max) {
|
|
30
|
+
return Math.min(Math.max(value, min), max);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Build a deterministic palette based on the letter's identity.
|
|
34
|
+
// The base hash selects one of the palette families, while the variant
|
|
35
|
+
// arrays add subtle shifts to keep similar letters visually distinct.
|
|
36
|
+
export default function generatePalette(letter) {
|
|
37
|
+
const baseHash = hashString(`${letter.id}|${letter.title}|${letter.date}`);
|
|
38
|
+
const family = paletteFamilies[baseHash % paletteFamilies.length];
|
|
39
|
+
const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
|
|
40
|
+
const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
|
|
41
|
+
const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
|
|
42
|
+
const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
|
|
43
|
+
const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
|
|
44
|
+
const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
|
|
45
|
+
const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
|
|
46
|
+
const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
|
|
47
|
+
const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
|
|
48
|
+
const text = `hsl(${family.hue}, 22%, 18%)`;
|
|
49
|
+
|
|
50
|
+
return { envelope, flap, shadow, base, seal, text };
|
|
51
|
+
}
|
|
@@ -1,72 +1,22 @@
|
|
|
1
1
|
import { letters } from '../loaders/lettersLoader.js';
|
|
2
|
+
import generatePalette from './generatePalette.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
// Checks the URL to see what ID is select.
|
|
5
|
+
// Then loads letter with that same ID.
|
|
5
6
|
const params = new URLSearchParams(window.location.search);
|
|
6
7
|
const requestedId = parseInt(params.get('id'), 10);
|
|
7
8
|
const letter = letters.find((entry) => entry.id === requestedId);
|
|
8
9
|
|
|
9
|
-
// A set of base color families used to generate a unique yet consistent
|
|
10
|
-
// envelope palette for each letter based on its id, title, and date.
|
|
11
|
-
const paletteFamilies = [
|
|
12
|
-
{ hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
|
|
13
|
-
{ hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
|
|
14
|
-
{ hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
|
|
15
|
-
{ hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
|
|
16
|
-
{ hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
|
|
17
|
-
{ hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
|
|
18
|
-
{ hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
|
|
19
|
-
{ hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
|
|
20
|
-
{ hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
|
|
21
|
-
{ hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function hashString(value) {
|
|
25
|
-
// Simple deterministic string hash used to select a palette and variant.
|
|
26
|
-
// This keeps each letter appearance stable across page loads.
|
|
27
|
-
let hash = 0;
|
|
28
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
29
|
-
hash = ((hash << 5) - hash) + value.charCodeAt(index);
|
|
30
|
-
hash |= 0;
|
|
31
|
-
}
|
|
32
|
-
return Math.abs(hash);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function clamp(value, min, max) {
|
|
36
|
-
return Math.min(Math.max(value, min), max);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function generatePalette(letterItem) {
|
|
40
|
-
// Build a deterministic palette based on the letter's identity.
|
|
41
|
-
// The base hash selects one of the palette families, while the variant
|
|
42
|
-
// arrays add subtle shifts to keep similar letters visually distinct.
|
|
43
|
-
const baseHash = hashString(`${letterItem.id}|${letterItem.title}|${letterItem.date}`);
|
|
44
|
-
const family = paletteFamilies[baseHash % paletteFamilies.length];
|
|
45
|
-
const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
|
|
46
|
-
const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
|
|
47
|
-
const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
|
|
48
|
-
const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
|
|
49
|
-
|
|
50
|
-
const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
|
|
51
|
-
const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
|
|
52
|
-
const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
|
|
53
|
-
const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
|
|
54
|
-
const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
|
|
55
|
-
const text = `hsl(${family.hue}, 22%, 18%)`;
|
|
56
|
-
|
|
57
|
-
return { envelope, flap, shadow, base, seal, text };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
10
|
function applyEnvelopePalette(letterItem) {
|
|
61
11
|
const palette = generatePalette(letterItem);
|
|
62
|
-
document.querySelector('.env-
|
|
12
|
+
document.querySelector('.env-background').style.background = palette.envelope;
|
|
63
13
|
|
|
64
|
-
const flapPolygon = document.querySelector('.env-
|
|
14
|
+
const flapPolygon = document.querySelector('.env-flap svg polygon');
|
|
65
15
|
if (flapPolygon) {
|
|
66
16
|
flapPolygon.setAttribute('fill', palette.flap);
|
|
67
17
|
}
|
|
68
18
|
|
|
69
|
-
const sidePolygons = document.querySelectorAll('.env-
|
|
19
|
+
const sidePolygons = document.querySelectorAll('.env-body-svg svg polygon');
|
|
70
20
|
if (sidePolygons.length === 3) {
|
|
71
21
|
sidePolygons[0].setAttribute('fill', palette.shadow);
|
|
72
22
|
sidePolygons[1].setAttribute('fill', palette.shadow);
|
|
@@ -136,10 +86,10 @@ if (!letter) {
|
|
|
136
86
|
}, 2100);
|
|
137
87
|
}
|
|
138
88
|
|
|
139
|
-
function startHeartAnimation() {
|
|
140
89
|
// Floating heart particle animation for the letter view background.
|
|
141
90
|
// Particles are born after a delay, float upward, fade at the top, and die
|
|
142
91
|
// when they leave the canvas to keep the animation visually light.
|
|
92
|
+
function startHeartAnimation() {
|
|
143
93
|
const canvas = document.getElementById('canvas-hearts');
|
|
144
94
|
const context = canvas.getContext('2d');
|
|
145
95
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
@@ -251,6 +201,5 @@ function startHeartAnimation() {
|
|
|
251
201
|
context.clearRect(0, 0, width, height);
|
|
252
202
|
}
|
|
253
203
|
}
|
|
254
|
-
|
|
255
204
|
window.requestAnimationFrame(animate);
|
|
256
205
|
}
|
package/src/scripts/main.js
CHANGED
|
@@ -1,51 +1,9 @@
|
|
|
1
1
|
import { letters } from '../loaders/lettersLoader.js';
|
|
2
|
+
import globalVariables from '../config/globalVariables.js';
|
|
3
|
+
import generatePalette from './generatePalette.js';
|
|
2
4
|
|
|
3
5
|
const listContainer = document.getElementById('letter-list');
|
|
4
6
|
|
|
5
|
-
const paletteFamilies = [
|
|
6
|
-
{ hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
|
|
7
|
-
{ hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
|
|
8
|
-
{ hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
|
|
9
|
-
{ hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
|
|
10
|
-
{ hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
|
|
11
|
-
{ hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
|
|
12
|
-
{ hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
|
|
13
|
-
{ hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
|
|
14
|
-
{ hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
|
|
15
|
-
{ hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
function hashString(value) {
|
|
19
|
-
let hash = 0;
|
|
20
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
21
|
-
hash = ((hash << 5) - hash) + value.charCodeAt(index);
|
|
22
|
-
hash |= 0;
|
|
23
|
-
}
|
|
24
|
-
return Math.abs(hash);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function clamp(value, min, max) {
|
|
28
|
-
return Math.min(Math.max(value, min), max);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function generatePalette(letter) {
|
|
32
|
-
const baseHash = hashString(`${letter.id}|${letter.title}|${letter.date}`);
|
|
33
|
-
const family = paletteFamilies[baseHash % paletteFamilies.length];
|
|
34
|
-
const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
|
|
35
|
-
const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
|
|
36
|
-
const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
|
|
37
|
-
const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
|
|
38
|
-
|
|
39
|
-
const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
|
|
40
|
-
const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
|
|
41
|
-
const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
|
|
42
|
-
const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
|
|
43
|
-
const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
|
|
44
|
-
const text = `hsl(${family.hue}, 22%, 18%)`;
|
|
45
|
-
|
|
46
|
-
return { envelope, flap, shadow, base, seal, text };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
7
|
function renderLetters() {
|
|
50
8
|
if (!listContainer) {
|
|
51
9
|
return;
|
|
@@ -59,11 +17,12 @@ function renderLetters() {
|
|
|
59
17
|
link.href = `./src/views/letters.html?id=${letter.id}`;
|
|
60
18
|
link.className = 'envelope-link';
|
|
61
19
|
|
|
20
|
+
// Creates a new envelope object for each loaded letter.
|
|
62
21
|
link.innerHTML = `
|
|
63
22
|
<div class="envelope-wrap">
|
|
64
|
-
<div class="envelope-
|
|
65
|
-
<div class="envelope" style="background:${palette.envelope}; --envelope-
|
|
66
|
-
<div class="env-
|
|
23
|
+
<div class="envelope-shadow"></div>
|
|
24
|
+
<div class="envelope" style="background:${palette.envelope}; --envelope-shadow:${palette.shadow};">
|
|
25
|
+
<div class="env-body-svg">
|
|
67
26
|
<svg viewBox="0 0 100 63" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
68
27
|
<polygon points="0,0 0,63 50,31" fill="${palette.shadow}" />
|
|
69
28
|
<polygon points="100,0 100,63 50,31" fill="${palette.shadow}" />
|
|
@@ -71,22 +30,22 @@ function renderLetters() {
|
|
|
71
30
|
</svg>
|
|
72
31
|
</div>
|
|
73
32
|
|
|
74
|
-
<div class="env-
|
|
33
|
+
<div class="env-flap" style="border-top-color:${palette.flap};">
|
|
75
34
|
<svg viewBox="0 0 100 44" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
76
35
|
<polygon points="0,0 100,0 50,44" fill="${palette.flap}" />
|
|
77
36
|
</svg>
|
|
78
|
-
<div class="env-
|
|
37
|
+
<div class="env-seal" style="background:${palette.seal};">♡</div>
|
|
79
38
|
</div>
|
|
80
39
|
|
|
81
|
-
<div class="envelope-
|
|
82
|
-
<span class="envelope-
|
|
83
|
-
<span class="envelope-
|
|
40
|
+
<div class="envelope-center">
|
|
41
|
+
<span class="envelope-date" style="color:${palette.text};">${letter.date}</span>
|
|
42
|
+
<span class="envelope-title" style="color:${palette.text};">${letter.title}</span>
|
|
84
43
|
<span class="envelope-hint" style="color:${palette.text};">Tap to open</span>
|
|
85
44
|
</div>
|
|
86
|
-
<div class="envelope-
|
|
87
|
-
<div class="envelope-
|
|
88
|
-
<div class="envelope-
|
|
89
|
-
<div class="envelope-
|
|
45
|
+
<div class="envelope-corner tl"></div>
|
|
46
|
+
<div class="envelope-corner tr"></div>
|
|
47
|
+
<div class="envelope-corner bl"></div>
|
|
48
|
+
<div class="envelope-corner br"></div>
|
|
90
49
|
</div>
|
|
91
50
|
</div>
|
|
92
51
|
`;
|
|
@@ -97,3 +56,7 @@ function renderLetters() {
|
|
|
97
56
|
|
|
98
57
|
renderLetters();
|
|
99
58
|
|
|
59
|
+
if(globalVariables.SHOW_BRANDING){
|
|
60
|
+
let footer = document.getElementById('footer-link');
|
|
61
|
+
footer.textContent = "Made with 💌 Moracarta";
|
|
62
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import globalVariables from '../config/globalVariables.js';
|
|
2
2
|
import fonts from '../config/fonts.js';
|
|
3
3
|
|
|
4
|
+
// Sets the main font for the application
|
|
4
5
|
document.documentElement.style.setProperty(
|
|
5
6
|
'--main-font',
|
|
6
7
|
fonts[globalVariables.FONT] || fonts.handwritten
|
|
@@ -8,17 +9,13 @@ document.documentElement.style.setProperty(
|
|
|
8
9
|
|
|
9
10
|
// Template variables are injected from `src/config/globalVariables.js` into
|
|
10
11
|
// HTML elements using the `data-variable` attribute.
|
|
11
|
-
// HTML entities are preserved when the value contains markup characters.
|
|
12
12
|
document.querySelectorAll("[data-variable]").forEach(element => {
|
|
13
13
|
const value = globalVariables[element.dataset.variable];
|
|
14
14
|
|
|
15
|
+
//Checks if the value is to be interpreted as literal HTML or pure text
|
|
15
16
|
if (typeof value === 'string' && (value.includes('&') || value.includes('<') || value.includes('>'))) {
|
|
16
17
|
element.innerHTML = value;
|
|
17
18
|
} else {
|
|
18
19
|
element.textContent = value;
|
|
19
20
|
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
document.querySelectorAll("[data-variable-src]").forEach(element => {
|
|
23
|
-
element.src = globalVariables[element.dataset.variableSrc];
|
|
24
21
|
});
|
package/src/styles/letters.css
CHANGED
|
@@ -74,7 +74,7 @@ body {
|
|
|
74
74
|
opacity: 1;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
.env-
|
|
77
|
+
.env-background {
|
|
78
78
|
position: absolute;
|
|
79
79
|
inset: 0;
|
|
80
80
|
background: #e8d5b0;
|
|
@@ -82,7 +82,7 @@ body {
|
|
|
82
82
|
z-index: 1;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
.env-
|
|
85
|
+
.env-paper {
|
|
86
86
|
position: absolute;
|
|
87
87
|
left: 11%;
|
|
88
88
|
right: 11%;
|
|
@@ -97,7 +97,7 @@ body {
|
|
|
97
97
|
overflow: hidden;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
.env-
|
|
100
|
+
.env-paper.leaving {
|
|
101
101
|
transform: translateY(-120%);
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -111,19 +111,19 @@ body {
|
|
|
111
111
|
font-style: italic;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
.env-
|
|
114
|
+
.env-body-svg {
|
|
115
115
|
position: absolute;
|
|
116
116
|
inset: 0;
|
|
117
117
|
z-index: 3;
|
|
118
118
|
pointer-events: none;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
.env-
|
|
121
|
+
.env-body-svg svg {
|
|
122
122
|
width: 100%;
|
|
123
123
|
height: 100%;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
.env-
|
|
126
|
+
.env-flap {
|
|
127
127
|
position: absolute;
|
|
128
128
|
top: 0;
|
|
129
129
|
left: 0;
|
|
@@ -136,20 +136,20 @@ body {
|
|
|
136
136
|
overflow: visible;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
.env-
|
|
139
|
+
.env-flap.open {
|
|
140
140
|
transform: rotateX(180deg);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
.env-
|
|
143
|
+
.env-flap.behind {
|
|
144
144
|
z-index: 0;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
.env-
|
|
147
|
+
.env-flap svg {
|
|
148
148
|
width: 100%;
|
|
149
149
|
height: 100%;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
.env-
|
|
152
|
+
.env-seal {
|
|
153
153
|
position: absolute;
|
|
154
154
|
z-index: 5;
|
|
155
155
|
width: 50px;
|
|
@@ -176,7 +176,7 @@ body {
|
|
|
176
176
|
will-change: transform;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
.env-
|
|
179
|
+
.env-seal.fading {
|
|
180
180
|
opacity: 0;
|
|
181
181
|
transform: translate(-50%, -120%) scale(0.5);
|
|
182
182
|
}
|
package/src/styles/main.css
CHANGED
|
@@ -143,7 +143,7 @@ body {
|
|
|
143
143
|
transform: scale(0.97) rotate(0deg);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
.envelope-
|
|
146
|
+
.envelope-shadow {
|
|
147
147
|
position: absolute;
|
|
148
148
|
bottom: -6px;
|
|
149
149
|
left: 6px;
|
|
@@ -180,20 +180,20 @@ body {
|
|
|
180
180
|
background-repeat: no-repeat;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
.env-
|
|
183
|
+
.env-body-svg {
|
|
184
184
|
position: absolute;
|
|
185
185
|
inset: 0;
|
|
186
186
|
z-index: 1;
|
|
187
187
|
pointer-events: none;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
.env-
|
|
190
|
+
.env-body-svg svg {
|
|
191
191
|
width: 100%;
|
|
192
192
|
height: 100%;
|
|
193
193
|
display: block;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
.env-
|
|
196
|
+
.env-flap {
|
|
197
197
|
position: relative;
|
|
198
198
|
z-index: 4;
|
|
199
199
|
pointer-events: none;
|
|
@@ -202,13 +202,13 @@ body {
|
|
|
202
202
|
width: 100%;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
.env-
|
|
205
|
+
.env-flap svg {
|
|
206
206
|
width: 100%;
|
|
207
207
|
height: 100%;
|
|
208
208
|
display: block;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
.env-
|
|
211
|
+
.env-seal {
|
|
212
212
|
position: absolute;
|
|
213
213
|
z-index: 5;
|
|
214
214
|
width: 48px;
|
|
@@ -226,7 +226,7 @@ body {
|
|
|
226
226
|
bottom: -24px;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
.envelope-
|
|
229
|
+
.envelope-center {
|
|
230
230
|
position: relative;
|
|
231
231
|
z-index: 2;
|
|
232
232
|
padding: 12px 16px 16px;
|
|
@@ -235,7 +235,7 @@ body {
|
|
|
235
235
|
gap: 2px;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
.envelope-
|
|
238
|
+
.envelope-date {
|
|
239
239
|
font-size: 10px;
|
|
240
240
|
letter-spacing: 0.5px;
|
|
241
241
|
text-transform: uppercase;
|
|
@@ -244,7 +244,7 @@ body {
|
|
|
244
244
|
font-style: italic;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
.envelope-
|
|
247
|
+
.envelope-title {
|
|
248
248
|
font-family: 'Playfair Display', Georgia, serif;
|
|
249
249
|
font-size: 16px;
|
|
250
250
|
font-weight: 600;
|
|
@@ -267,7 +267,7 @@ body {
|
|
|
267
267
|
color: var(--red);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
.envelope-
|
|
270
|
+
.envelope-corner {
|
|
271
271
|
position: absolute;
|
|
272
272
|
width: 20px;
|
|
273
273
|
height: 20px;
|
|
@@ -276,25 +276,25 @@ body {
|
|
|
276
276
|
opacity: 0.5;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
.envelope-
|
|
279
|
+
.envelope-corner.tl {
|
|
280
280
|
top: 6px;
|
|
281
281
|
left: 6px;
|
|
282
282
|
border-width: 1px 0 0 1px;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
.envelope-
|
|
285
|
+
.envelope-corner.tr {
|
|
286
286
|
top: 6px;
|
|
287
287
|
right: 6px;
|
|
288
288
|
border-width: 1px 1px 0 0;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
.envelope-
|
|
291
|
+
.envelope-corner.bl {
|
|
292
292
|
bottom: 6px;
|
|
293
293
|
left: 6px;
|
|
294
294
|
border-width: 0 0 1px 1px;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
.envelope-
|
|
297
|
+
.envelope-corner.br {
|
|
298
298
|
bottom: 6px;
|
|
299
299
|
right: 6px;
|
|
300
300
|
border-width: 0 1px 1px 0;
|
|
@@ -310,7 +310,7 @@ body {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
.
|
|
313
|
+
.bottom-text {
|
|
314
314
|
text-align: center;
|
|
315
315
|
margin-top: 40px;
|
|
316
316
|
padding: 0 20px;
|
|
@@ -319,14 +319,14 @@ body {
|
|
|
319
319
|
margin-right: auto;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
.
|
|
322
|
+
.bottom-text-line {
|
|
323
323
|
width: 60px;
|
|
324
324
|
height: 1px;
|
|
325
325
|
background: linear-gradient(to right, transparent, #6b4c2a, transparent);
|
|
326
326
|
margin: 0 auto 16px;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
.
|
|
329
|
+
.bottom-text p {
|
|
330
330
|
font-size: 12px;
|
|
331
331
|
color: #6b4c2a;
|
|
332
332
|
font-style: italic;
|
|
@@ -335,4 +335,13 @@ body {
|
|
|
335
335
|
text-align: justify;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
.footer{
|
|
339
|
+
padding-top: 11vh;
|
|
340
|
+
display: flex;
|
|
341
|
+
justify-content: center;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.footer-link{
|
|
345
|
+
text-decoration: none;
|
|
346
|
+
color: rgba(176, 53, 16, 0.571);
|
|
347
|
+
}
|
package/src/views/letters.html
CHANGED
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
<div class="envelope-animation-wrap" id="envelope-wrap">
|
|
25
25
|
<div class="envelope-stage">
|
|
26
|
-
<div class="env-
|
|
26
|
+
<div class="env-background"></div>
|
|
27
27
|
|
|
28
|
-
<div class="env-
|
|
28
|
+
<div class="env-paper" id="paper">
|
|
29
29
|
<div class="paper-text">
|
|
30
30
|
<p data-variable="CLOSED_LETTER_TEXT_TOP_LINE"></p><br>
|
|
31
31
|
<p data-variable="CLOSED_LETTER_TEXT_BOTTOM_LINE"></p>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
|
-
<div class="env-
|
|
35
|
+
<div class="env-body-svg">
|
|
36
36
|
<svg viewBox="0 0 100 63" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
37
37
|
<polygon points="0,0 0,63 47,42" fill="#c9a87c" />
|
|
38
38
|
<polygon points="100,0 100,63 53,42" fill="#c4a07a" />
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
</svg>
|
|
41
41
|
</div>
|
|
42
42
|
|
|
43
|
-
<div class="env-
|
|
43
|
+
<div class="env-flap" id="flap">
|
|
44
44
|
<svg viewBox="0 0 100 44" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
45
45
|
<polygon points="0,0 100,0 50,44" fill="#d4b896" />
|
|
46
46
|
</svg>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
|
-
<div class="env-
|
|
49
|
+
<div class="env-seal" id="seal"><span class="seal-heart" aria-hidden="true">♡</span></div>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
52
52
|
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<span class="nav-counter" id="letter-counter"></span>
|
|
77
77
|
<a id="nav-next" class="nav-button invisible" href="#"><p>Next</p> →</a>
|
|
78
78
|
</nav>
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
<script type="module" src="../scripts/lettersPage.js"></script>
|
|
81
81
|
<script type="module" src="../scripts/templateBinding.js"></script>
|
|
82
82
|
</html>
|
package/src/scripts/letters.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { letters } from '../loaders/lettersLoader.js';
|
|
2
|
-
|
|
3
|
-
console.log('letters.script loaded');
|
|
4
|
-
|
|
5
|
-
const params = new URLSearchParams(window.location.search);
|
|
6
|
-
const requestedId = parseInt(params.get('id'), 10);
|
|
7
|
-
const letter = letters.find((entry) => entry.id === requestedId);
|
|
8
|
-
|
|
9
|
-
// A set of base color families used to generate a unique yet consistent
|
|
10
|
-
// envelope palette for each letter based on its id, title, and date.
|
|
11
|
-
const paletteFamilies = [
|
|
12
|
-
{ hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
|
|
13
|
-
{ hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
|
|
14
|
-
{ hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
|
|
15
|
-
{ hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
|
|
16
|
-
{ hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
|
|
17
|
-
{ hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
|
|
18
|
-
{ hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
|
|
19
|
-
{ hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
|
|
20
|
-
{ hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
|
|
21
|
-
{ hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function hashString(value) {
|
|
25
|
-
// Simple deterministic string hash used to select a palette and variant.
|
|
26
|
-
// This keeps each letter appearance stable across page loads.
|
|
27
|
-
let hash = 0;
|
|
28
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
29
|
-
hash = ((hash << 5) - hash) + value.charCodeAt(index);
|
|
30
|
-
hash |= 0;
|
|
31
|
-
}
|
|
32
|
-
return Math.abs(hash);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function clamp(value, min, max) {
|
|
36
|
-
return Math.min(Math.max(value, min), max);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function generatePalette(letterItem) {
|
|
40
|
-
// Build a deterministic palette based on the letter's identity.
|
|
41
|
-
// The base hash selects one of the palette families, while the variant
|
|
42
|
-
// arrays add subtle shifts to keep similar letters visually distinct.
|
|
43
|
-
const baseHash = hashString(`${letterItem.id}|${letterItem.title}|${letterItem.date}`);
|
|
44
|
-
const family = paletteFamilies[baseHash % paletteFamilies.length];
|
|
45
|
-
const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
|
|
46
|
-
const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
|
|
47
|
-
const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
|
|
48
|
-
const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
|
|
49
|
-
|
|
50
|
-
const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
|
|
51
|
-
const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
|
|
52
|
-
const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
|
|
53
|
-
const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
|
|
54
|
-
const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
|
|
55
|
-
const text = `hsl(${family.hue}, 22%, 18%)`;
|
|
56
|
-
|
|
57
|
-
return { envelope, flap, shadow, base, seal, text };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function applyEnvelopePalette(letterItem) {
|
|
61
|
-
const palette = generatePalette(letterItem);
|
|
62
|
-
document.querySelector('.env-fundo').style.background = palette.envelope;
|
|
63
|
-
|
|
64
|
-
const flapPolygon = document.querySelector('.env-aba svg polygon');
|
|
65
|
-
if (flapPolygon) {
|
|
66
|
-
flapPolygon.setAttribute('fill', palette.flap);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const sidePolygons = document.querySelectorAll('.env-corpo-svg svg polygon');
|
|
70
|
-
if (sidePolygons.length === 3) {
|
|
71
|
-
sidePolygons[0].setAttribute('fill', palette.shadow);
|
|
72
|
-
sidePolygons[1].setAttribute('fill', palette.shadow);
|
|
73
|
-
sidePolygons[2].setAttribute('fill', palette.base);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!letter) {
|
|
78
|
-
document.getElementById('error-message').style.display = 'block';
|
|
79
|
-
document.getElementById('envelope-wrap').style.display = 'none';
|
|
80
|
-
document.getElementById('letter-wrap').style.display = 'none';
|
|
81
|
-
document.getElementById('letter-nav').style.display = 'none';
|
|
82
|
-
} else {
|
|
83
|
-
document.title = letter.title;
|
|
84
|
-
|
|
85
|
-
document.getElementById('letter-date').textContent = letter.date;
|
|
86
|
-
document.getElementById('letter-title').textContent = letter.title;
|
|
87
|
-
document.getElementById('letter-body').innerHTML = letter.content
|
|
88
|
-
.split(/\n\s*\n/)
|
|
89
|
-
.filter(paragraph => paragraph.trim())
|
|
90
|
-
.map(paragraph => `<p>${paragraph.trimEnd()}</p>`)
|
|
91
|
-
.join('');
|
|
92
|
-
|
|
93
|
-
applyEnvelopePalette(letter);
|
|
94
|
-
document.querySelector('.envelope-stage').classList.add('ready');
|
|
95
|
-
|
|
96
|
-
const sortedLetters = letters.slice().sort((first, second) => first.id - second.id);
|
|
97
|
-
const currentIndex = sortedLetters.findIndex((entry) => entry.id === requestedId);
|
|
98
|
-
const previousLetter = currentIndex > 0 ? sortedLetters[currentIndex - 1] : null;
|
|
99
|
-
const nextLetter = currentIndex < sortedLetters.length - 1 ? sortedLetters[currentIndex + 1] : null;
|
|
100
|
-
|
|
101
|
-
document.getElementById('letter-counter').textContent = `${currentIndex + 1} Of ${sortedLetters.length}`;
|
|
102
|
-
|
|
103
|
-
if (previousLetter) {
|
|
104
|
-
const previousButton = document.getElementById('nav-previous');
|
|
105
|
-
previousButton.href = `./letters.html?id=${previousLetter.id}`;
|
|
106
|
-
previousButton.classList.remove('invisible');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (nextLetter) {
|
|
110
|
-
const nextButton = document.getElementById('nav-next');
|
|
111
|
-
nextButton.href = `./letters.html?id=${nextLetter.id}`;
|
|
112
|
-
nextButton.classList.remove('invisible');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
startHeartAnimation();
|
|
116
|
-
|
|
117
|
-
// Animate the envelope opening, sealing, and letter reveal in timed stages.
|
|
118
|
-
window.setTimeout(() => {
|
|
119
|
-
document.getElementById('flap').classList.add('open');
|
|
120
|
-
document.getElementById('seal').classList.add('fading');
|
|
121
|
-
}, 400);
|
|
122
|
-
|
|
123
|
-
window.setTimeout(() => {
|
|
124
|
-
document.getElementById('flap').classList.add('behind');
|
|
125
|
-
}, 1050);
|
|
126
|
-
|
|
127
|
-
window.setTimeout(() => {
|
|
128
|
-
document.getElementById('paper').classList.add('leaving');
|
|
129
|
-
}, 900);
|
|
130
|
-
|
|
131
|
-
window.setTimeout(() => {
|
|
132
|
-
document.getElementById('letter-wrap').classList.add('visible');
|
|
133
|
-
document.getElementById('letter-nav').classList.add('visible');
|
|
134
|
-
document.getElementById('envelope-wrap').style.display = 'none';
|
|
135
|
-
|
|
136
|
-
}, 2100);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function startHeartAnimation() {
|
|
140
|
-
// Floating heart particle animation for the letter view background.
|
|
141
|
-
// Particles are born after a delay, float upward, fade at the top, and die
|
|
142
|
-
// when they leave the canvas to keep the animation visually light.
|
|
143
|
-
const canvas = document.getElementById('canvas-hearts');
|
|
144
|
-
const context = canvas.getContext('2d');
|
|
145
|
-
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
146
|
-
|
|
147
|
-
canvas.width = window.innerWidth * devicePixelRatio;
|
|
148
|
-
canvas.height = window.innerHeight * devicePixelRatio;
|
|
149
|
-
canvas.style.width = `${window.innerWidth}px`;
|
|
150
|
-
canvas.style.height = `${window.innerHeight}px`;
|
|
151
|
-
context.scale(devicePixelRatio, devicePixelRatio);
|
|
152
|
-
|
|
153
|
-
const width = window.innerWidth;
|
|
154
|
-
const height = window.innerHeight;
|
|
155
|
-
const colors = ['#c0392b', '#e74c3c', '#e91e63', '#ff4081', '#ad1457', '#f06292', '#ff6b6b', '#d63031', '#ff8a80'];
|
|
156
|
-
|
|
157
|
-
function drawHeart(radius) {
|
|
158
|
-
context.beginPath();
|
|
159
|
-
context.moveTo(0, -radius * 0.3);
|
|
160
|
-
context.bezierCurveTo(radius, -radius * 1.1, radius * 1.5, radius * 0.4, 0, radius);
|
|
161
|
-
context.bezierCurveTo(-radius * 1.5, radius * 0.4, -radius, -radius * 1.1, 0, -radius * 0.3);
|
|
162
|
-
context.closePath();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const totalParticles = 55;
|
|
166
|
-
const particles = [];
|
|
167
|
-
|
|
168
|
-
for (let index = 0; index < totalParticles; index += 1) {
|
|
169
|
-
const angle = (Math.random() - 0.5) * (Math.PI / 1.5);
|
|
170
|
-
const velocity = 1.2 + Math.random() * 2.2;
|
|
171
|
-
const radius = 8 + Math.random() * 18;
|
|
172
|
-
|
|
173
|
-
particles.push({
|
|
174
|
-
x: width * 0.05 + Math.random() * width * 0.9,
|
|
175
|
-
y: height + radius * 2,
|
|
176
|
-
velocityX: Math.sin(angle) * velocity,
|
|
177
|
-
velocityY: -Math.cos(angle) * velocity,
|
|
178
|
-
radius,
|
|
179
|
-
rotation: (Math.random() - 0.5) * 0.06,
|
|
180
|
-
currentAngle: 0,
|
|
181
|
-
color: colors[Math.floor(Math.random() * colors.length)],
|
|
182
|
-
alpha: 0,
|
|
183
|
-
delay: Math.random() * 2500,
|
|
184
|
-
born: false,
|
|
185
|
-
dead: false
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
let startTime = null;
|
|
190
|
-
|
|
191
|
-
function animate(timestamp) {
|
|
192
|
-
if (!startTime) {
|
|
193
|
-
startTime = timestamp;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const elapsed = timestamp - startTime;
|
|
197
|
-
context.clearRect(0, 0, width, height);
|
|
198
|
-
|
|
199
|
-
let allDead = true;
|
|
200
|
-
|
|
201
|
-
particles.forEach((particle) => {
|
|
202
|
-
if (elapsed < particle.delay) {
|
|
203
|
-
allDead = false;
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (!particle.born) {
|
|
208
|
-
particle.born = true;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (particle.dead) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
allDead = false;
|
|
216
|
-
particle.x += particle.velocityX;
|
|
217
|
-
particle.y += particle.velocityY;
|
|
218
|
-
particle.velocityY -= 0.012;
|
|
219
|
-
particle.velocityX *= 0.998;
|
|
220
|
-
particle.currentAngle += particle.rotation;
|
|
221
|
-
|
|
222
|
-
const fraction = 1 - (particle.y / height);
|
|
223
|
-
if (fraction < 0.08) {
|
|
224
|
-
particle.alpha = fraction / 0.08;
|
|
225
|
-
} else if (fraction > 0.55) {
|
|
226
|
-
particle.alpha = Math.max(0, 1 - (fraction - 0.55) / 0.45);
|
|
227
|
-
} else {
|
|
228
|
-
particle.alpha = 1;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (particle.y < -particle.radius * 3) {
|
|
232
|
-
particle.dead = true;
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
context.save();
|
|
237
|
-
context.translate(particle.x, particle.y);
|
|
238
|
-
context.rotate(particle.currentAngle);
|
|
239
|
-
context.globalAlpha = particle.alpha;
|
|
240
|
-
context.shadowColor = particle.color;
|
|
241
|
-
context.shadowBlur = 6;
|
|
242
|
-
context.fillStyle = particle.color;
|
|
243
|
-
drawHeart(particle.radius);
|
|
244
|
-
context.fill();
|
|
245
|
-
context.restore();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
if (!allDead) {
|
|
249
|
-
window.requestAnimationFrame(animate);
|
|
250
|
-
} else {
|
|
251
|
-
context.clearRect(0, 0, width, height);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
window.requestAnimationFrame(animate);
|
|
256
|
-
}
|