diff --git a/scripts/generate-optimized-assets.cjs b/scripts/generate-optimized-assets.cjs
index 8c03dcc..987ee66 100644
--- a/scripts/generate-optimized-assets.cjs
+++ b/scripts/generate-optimized-assets.cjs
@@ -53,22 +53,54 @@ function getProfile(relativePath) {
}
if (normalizedRelativePath.startsWith('logos/')) {
- return { maxWidth: 640, quality: 90, alphaQuality: 92, nearLossless: true };
+ return { maxWidth: 216, quality: 78, alphaQuality: 88, variants: [64, 128, 216] };
}
if (normalizedRelativePath.startsWith('portrait/')) {
- return { maxWidth: 1600, quality: 84, alphaQuality: 92 };
+ return { maxWidth: 900, quality: 82, alphaQuality: 90, variants: [128, 320, 480, 640, 900] };
}
return { maxWidth: 1600, quality: 82, alphaQuality: 92 };
}
+function getTargetRelativePath(relativePath, width) {
+ const parsedPath = path.parse(relativePath);
+ const fileName =
+ width === undefined ? `${parsedPath.name}.webp` : `${parsedPath.name}-${width}.webp`;
+ return path.join(parsedPath.dir, fileName);
+}
+
+async function createWebpBuffer(inputBuffer, profile, width) {
+ const image = sharp(inputBuffer, { animated: false }).rotate();
+ const metadata = await image.metadata();
+ const targetWidth = width ?? profile.maxWidth;
+
+ let pipeline = image;
+ if ((metadata.width ?? 0) > targetWidth) {
+ pipeline = pipeline.resize({
+ width: targetWidth,
+ fit: 'inside',
+ withoutEnlargement: true,
+ });
+ }
+
+ return pipeline
+ .webp({
+ quality: profile.quality,
+ alphaQuality: profile.alphaQuality,
+ effort: 6,
+ smartSubsample: true,
+ nearLossless: profile.nearLossless ?? false,
+ })
+ .toBuffer();
+}
+
async function writeOptimizedAsset(sourceFilePath) {
const relativePath = path.relative(sourceRoot, sourceFilePath);
const extension = path.extname(sourceFilePath).toLowerCase();
const targetRelativePath = passthroughExtensions.has(extension)
? relativePath
- : relativePath.replace(/\.[^.]+$/, '.webp');
+ : getTargetRelativePath(relativePath);
const targetFilePath = path.join(targetRoot, targetRelativePath);
fs.mkdirSync(path.dirname(targetFilePath), { recursive: true });
@@ -87,30 +119,17 @@ async function writeOptimizedAsset(sourceFilePath) {
const profile = getProfile(relativePath);
const inputBuffer = fs.readFileSync(sourceFilePath);
const inputBytes = inputBuffer.byteLength;
- const image = sharp(inputBuffer, { animated: false }).rotate();
- const metadata = await image.metadata();
-
- let pipeline = image;
- if ((metadata.width ?? 0) > profile.maxWidth) {
- pipeline = pipeline.resize({
- width: profile.maxWidth,
- fit: 'inside',
- withoutEnlargement: true,
- });
- }
-
- const outputBuffer = await pipeline
- .webp({
- quality: profile.quality,
- alphaQuality: profile.alphaQuality,
- effort: 6,
- smartSubsample: true,
- nearLossless: profile.nearLossless ?? false,
- })
- .toBuffer();
+ const outputBuffer = await createWebpBuffer(inputBuffer, profile);
fs.writeFileSync(targetFilePath, outputBuffer);
+ for (const variantWidth of profile.variants ?? []) {
+ const variantRelativePath = getTargetRelativePath(relativePath, variantWidth);
+ const variantFilePath = path.join(targetRoot, variantRelativePath);
+ const variantBuffer = await createWebpBuffer(inputBuffer, profile, variantWidth);
+ fs.writeFileSync(variantFilePath, variantBuffer);
+ }
+
return {
sourceRelativePath: relativePath,
targetRelativePath,
diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html
index 0024ab8..bf3a6aa 100644
--- a/src/app/home/home.component.html
+++ b/src/app/home/home.component.html
@@ -67,6 +67,8 @@
}
diff --git a/src/assets/unoptimized/logos/synhost.png b/src/assets/unoptimized/logos/synhost.png
new file mode 100644
index 0000000..b4e4a86
Binary files /dev/null and b/src/assets/unoptimized/logos/synhost.png differ
diff --git a/src/global.ts b/src/global.ts
index 1669590..a007a2b 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -4,11 +4,15 @@ export interface PortfolioProject {
description: string;
languages: string[];
icon?: string;
+ iconSrcset?: string;
+ iconSizes?: string;
CircleIcon?: boolean;
}
export interface PortraitHighlight {
image: string;
+ imageSrcset: string;
+ imageSizes: string;
text: string;
}
@@ -75,26 +79,50 @@ export const global: Global = {
portraitHighlights: [
{
image: 'assets/optimized/portrait/me.webp',
+ imageSrcset:
+ 'assets/optimized/portrait/me-320.webp 320w, assets/optimized/portrait/me-480.webp 480w, assets/optimized/portrait/me-640.webp 640w, assets/optimized/portrait/me.webp 900w',
+ imageSizes:
+ '(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'ME',
},
{
image: 'assets/optimized/portrait/love.webp',
+ imageSrcset:
+ 'assets/optimized/portrait/love-320.webp 320w, assets/optimized/portrait/love-480.webp 480w, assets/optimized/portrait/love-640.webp 640w, assets/optimized/portrait/love.webp 900w',
+ imageSizes:
+ '(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'My Love',
},
{
image: 'assets/optimized/portrait/scuba.webp',
+ imageSrcset:
+ 'assets/optimized/portrait/scuba-320.webp 320w, assets/optimized/portrait/scuba-480.webp 480w, assets/optimized/portrait/scuba-640.webp 640w, assets/optimized/portrait/scuba.webp 900w',
+ imageSizes:
+ '(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Scuba Diving',
},
{
image: 'assets/optimized/portrait/trains.webp',
+ imageSrcset:
+ 'assets/optimized/portrait/trains-320.webp 320w, assets/optimized/portrait/trains-480.webp 480w, assets/optimized/portrait/trains-640.webp 640w, assets/optimized/portrait/trains.webp 900w',
+ imageSizes:
+ '(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Trains',
},
{
image: 'assets/optimized/portrait/traveling.webp',
+ imageSrcset:
+ 'assets/optimized/portrait/traveling-320.webp 320w, assets/optimized/portrait/traveling-480.webp 480w, assets/optimized/portrait/traveling-640.webp 640w, assets/optimized/portrait/traveling.webp 900w',
+ imageSizes:
+ '(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Traveling',
},
{
image: 'assets/optimized/portrait/culture.webp',
+ imageSrcset:
+ 'assets/optimized/portrait/culture-320.webp 320w, assets/optimized/portrait/culture-480.webp 480w, assets/optimized/portrait/culture-640.webp 640w, assets/optimized/portrait/culture.webp 900w',
+ imageSizes:
+ '(max-width: 700px) calc(100vw - 2rem), (max-width: 900px) min(100vw - 6rem, 560px), 560px',
text: 'Culture',
},
],
@@ -106,6 +134,9 @@ export const global: Global = {
'I currently work primarily for the viennese company SobIT GmbH. This company develops software for the healthcare industry. - Development of modern desktop, mobile, and web applications to support healthcare delivery.',
languages: ['TypeScript', 'C#', 'WPF', 'YML', 'XAML'],
icon: 'assets/optimized/logos/sobit.webp',
+ iconSrcset:
+ 'assets/optimized/logos/sobit-64.webp 64w, assets/optimized/logos/sobit-128.webp 128w, assets/optimized/logos/sobit.webp 216w',
+ iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false,
},
{
@@ -114,7 +145,10 @@ export const global: Global = {
description:
'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'],
- icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
+ icon: 'assets/optimized/logos/synhost.webp',
+ iconSrcset:
+ 'assets/optimized/logos/synhost-64.webp 64w, assets/optimized/logos/synhost-128.webp 128w, assets/optimized/logos/synhost.webp 216w',
+ iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false,
},
{
@@ -123,7 +157,10 @@ export const global: Global = {
description:
'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'],
- icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
+ icon: 'assets/optimized/logos/synhost.webp',
+ iconSrcset:
+ 'assets/optimized/logos/synhost-64.webp 64w, assets/optimized/logos/synhost-128.webp 128w, assets/optimized/logos/synhost.webp 216w',
+ iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false,
},
{
@@ -132,7 +169,10 @@ export const global: Global = {
description:
'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'],
- icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
+ icon: 'assets/optimized/logos/synhost.webp',
+ iconSrcset:
+ 'assets/optimized/logos/synhost-64.webp 64w, assets/optimized/logos/synhost-128.webp 128w, assets/optimized/logos/synhost.webp 216w',
+ iconSizes: '(max-width: 700px) 64px, 108px',
CircleIcon: false,
},
],
diff --git a/src/index.html b/src/index.html
index 21332f3..5ec09b5 100644
--- a/src/index.html
+++ b/src/index.html
@@ -3,22 +3,66 @@