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 @@ + - + Lechner Julian -