Skip to content

Commit

Permalink
feat: add blur up images
Browse files Browse the repository at this point in the history
  • Loading branch information
kpfromer committed Feb 1, 2021
1 parent 0732529 commit 95b1627
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 33 deletions.
14 changes: 12 additions & 2 deletions components/Blog/Post/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import NextImage from 'next/image';
import { DateTime } from 'luxon';
import { MdxImage } from '@lib/common';
import { HTMLAttributes } from 'react';
import { ImgPlaceholder } from '@lib/placeholder';
import Img from '@components/Img';

export interface PostProps extends HTMLAttributes<HTMLDivElement> {
title: string;
coverImage: MdxImage;
coverImagePlaceholder: ImgPlaceholder;
coverImageAlt?: string;
created: string;
}

const Post: React.FC<PostProps> = ({
title,
coverImage,
coverImagePlaceholder,
coverImageAlt,
created,
children,
Expand All @@ -26,7 +29,14 @@ const Post: React.FC<PostProps> = ({
return (
<div {...props}>
<div className="overflow-hidden rounded-lg bg-white">
<NextImage layout="responsive" {...coverImage} alt={coverImageAlt} />
<Img
layout="responsive"
{...coverImage}
className="bg-white"
placeholderProps={{ className: 'bg-white' }}
placeholder={coverImagePlaceholder}
alt={coverImageAlt}
/>
</div>

<div className="mt-4">
Expand Down
19 changes: 15 additions & 4 deletions components/Blog/Preview/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import NextImage from 'next/image';
import { DateTime } from 'luxon';
import { HTMLMotionProps, motion } from 'framer-motion';
import classnames from 'classnames';
import { BlogPostFrontmatter } from '@lib/blog';
import { ImgPlaceholder } from '@lib/placeholder';
import Img from '@components/Img';

export interface PreviewProps
extends Omit<HTMLMotionProps<'div'>, keyof BlogPostFrontmatter>,
BlogPostFrontmatter {}
BlogPostFrontmatter {
coverImagePlaceholder: ImgPlaceholder;
}

const Preview: React.FC<PreviewProps> = ({
title,
coverImage,
coverImagePlaceholder,
coverImageAlt,
created,
...props
Expand All @@ -29,8 +33,15 @@ const Preview: React.FC<PreviewProps> = ({
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
}}
>
<div className="overflow-hidden rounded-lg bg-white">
<NextImage layout="responsive" {...coverImage} alt={coverImageAlt} />
<div className="overflow-hidden rounded-lg">
<Img
className="bg-white"
placeholderProps={{ className: 'bg-white' }}
layout="responsive"
placeholder={coverImagePlaceholder}
{...coverImage}
alt={coverImageAlt}
/>
</div>

<div className="flex-grow" />
Expand Down
147 changes: 147 additions & 0 deletions components/Img.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import Image from 'next/image';
import clsx from 'classnames';
import { HTMLAttributes, useCallback, useState } from 'react';
import React from 'react';

export type ImgPlaceholder = string;

const VALID_LAYOUT_VALUES = ['fill', 'fixed', 'intrinsic', 'responsive', undefined] as const;

type LayoutValue = typeof VALID_LAYOUT_VALUES[number];

const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const;
type LoadingValue = typeof VALID_LOADING_VALUES[number];
type ImgElementStyle = NonNullable<JSX.IntrinsicElements['img']['style']>;

export type ImgProps = (Omit<
JSX.IntrinsicElements['img'],
'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style'
> & {
src: string;

fadeIn?: boolean;
durationFadeIn?: number;

placeholder: ImgPlaceholder;

placeholderProps?: HTMLAttributes<HTMLImageElement>;
containerProps?: HTMLAttributes<HTMLDivElement>;

quality?: number | string;
priority?: boolean;
loading?: LoadingValue;
unoptimized?: boolean;
objectFit?: ImgElementStyle['objectFit'];
objectPosition?: ImgElementStyle['objectPosition'];
}) &
(
| {
width?: never;
height?: never;
layout: 'fill';
}
| {
width: number | string;
height: number | string;
layout?: Exclude<LayoutValue, 'fill'>;
}
);

const Img: React.FC<ImgProps> = ({
src,
width,
height,
layout = 'intrinsic',
fadeIn = true,
durationFadeIn = 500,
placeholder,
containerProps,
placeholderProps,
...rest
}) => {
const [imgLoaded, setImgLoaded] = useState(false);

const shouldReveal = !fadeIn || imgLoaded;
const shouldFadeIn = fadeIn; // && imgLoaded;

const imageStyle = {
opacity: shouldReveal ? 1 : 0,
transition: shouldFadeIn ? `opacity ${durationFadeIn}ms ease 0s` : `none`,
// transition: shouldFadeIn ? `opacity ${durationFadeIn}ms` : `none`,
// ...imgStyle
};

const delayHideStyle = { transitionDelay: `${durationFadeIn}ms` };

const imagePlaceholderStyle = {
// opacity: imgLoaded ? 0 : 1,
// transition: `opacity ${durationFadeIn}ms`,
// transitionDelay: `${durationFadeIn}ms`,
// ...(shouldFadeIn && delayHideStyle),

filter: 'blur(12px)',
transform: 'scale(1.2)',
...placeholderProps?.style,
};

const imageContainer = layout === 'intrinsic' ? { className: 'flex' } : {};

const onLoad = useCallback((event) => {
// https://github.com/vercel/next.js/discussions/18386

if (event.target.srcset) {
// Image is ready
setImgLoaded(true);
if (rest.onLoad) rest.onLoad(event);
}
}, []);

return (
<div
{...containerProps}
className={clsx(
containerProps?.className,
'relative',
layout === 'intrinsic' ? 'inline-block' : 'block',
'overflow-hidden',
)}
>
{placeholder && (
<img
{...placeholderProps}
aria-hidden="true"
alt=""
src={placeholder}
className={clsx(
placeholderProps?.className,
'absolute',
'inset-0',
'w-full',
'h-full',
'object-cover',
'object-center',
)}
style={imagePlaceholderStyle}
/>
)}

<div style={imageStyle} {...imageContainer}>
{layout !== 'fill' ? (
<Image
{...rest}
className={clsx(rest.className, 'block')}
src={src}
width={width}
height={height}
layout={layout}
onLoad={onLoad}
/>
) : (
<Image {...rest} src={src} layout={layout} onLoad={onLoad} />
)}
</div>
</div>
);
};

export default Img;
12 changes: 9 additions & 3 deletions components/Project/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import classnames from 'classnames';
import { HTMLMotionProps, motion } from 'framer-motion';
import NextImage from 'next/image';
import NextLink from 'next/link';
import { FiGithub } from 'react-icons/fi';
import IconButton from '@components/IconButton';
import { ProjectData } from '@lib/projects';
import Img from '@components/Img';

export interface ProjectProps
extends Omit<HTMLMotionProps<'div'>, keyof ProjectData>,
Expand Down Expand Up @@ -33,8 +33,14 @@ const Project: React.FC<ProjectProps> = ({
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
}}
>
<div className="overflow-hidden rounded-lg bg-white">
<NextImage layout="responsive" {...image} alt={imageAlt} />
<div className="overflow-hidden rounded-lg">
<Img
layout="responsive"
{...image}
alt={imageAlt}
className="bg-white"
placeholderProps={{ className: 'bg-white' }}
/>
</div>

<div className="mt-3">
Expand Down
2 changes: 1 addition & 1 deletion content/blog/terminal-setup-arch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ curl -fsSL https://starship.rs/install.sh | bash
```

<div className="rounded-md my-2 overflow-hidden">
<video muted="muted" autoPlay="autoplay" loop="loop" playsInline>
<video muted="muted" autoPlay="autoplay" loop="loop" playsInline width={914} height={573}>
<source src="/blog/terminal-setup-arch/demo.webm" type="video/webm" />
<source src="/blog/terminal-setup-arch/demo.mp4" type="video/mp4" />
</video>
Expand Down
11 changes: 10 additions & 1 deletion lib/common.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import imageSize from 'image-size';
import { promisify } from 'util';
import path from 'path';
import { generatePlaceholder } from './placeholder';

const sizeOf = promisify(imageSize);

export interface MdxImage {
width: number;
height: number;
src: string;
placeholder: string;
}

/**
Expand All @@ -18,7 +20,14 @@ export async function getMdxImage(src: string): Promise<MdxImage> {
const res = await sizeOf(path.join(process.cwd(), 'public', src));
if (!res) throw new Error(`Invalid image "${src}"`);

return { src, width: res.width, height: res.height };
const placeholder = await generatePlaceholder(src);

return {
src,
placeholder,
width: res.width,
height: res.height,
};
}

/**
Expand Down
15 changes: 15 additions & 0 deletions lib/placeholder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import fs from 'fs';
import { promisify } from 'util';
import path from 'path';
import { getBase64 } from '@plaiceholder/base64';

export type ImgPlaceholder = string;

export async function generatePlaceholder(imgPath: string): Promise<ImgPlaceholder> {
const image = await promisify(fs.readFile)(path.join(process.cwd(), 'public', imgPath));

// optimize using data uri thing?
const base64 = await getBase64(image);

return base64;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"build-storybook": "build-storybook"
},
"dependencies": {
"@plaiceholder/base64": "^1.0.0",
"autoprefixer": "^10.2.1",
"classnames": "^2.2.6",
"framer-motion": "^3.1.3",
Expand Down
10 changes: 9 additions & 1 deletion pages/blog/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import NextLink from 'next/link';
import info from '@configs/info';
import Icon from '@components/Icon';
import 'katex/dist/katex.min.css';
import { generatePlaceholder, ImgPlaceholder } from '@lib/placeholder';

export const getStaticPaths: GetStaticPaths = async () => {
const slugs = await getBlogPostSlugs();
Expand All @@ -36,7 +37,10 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
}

return {
props: post,
props: {
...post,
coverImagePlaceholder: await generatePlaceholder(post.frontmatter.coverImage.src),
},
};
};

Expand All @@ -46,6 +50,8 @@ interface BlogContext {
}

export interface BlogPostProps extends BlogPostData {
coverImagePlaceholder: ImgPlaceholder;

previous?: BlogContext;
next?: BlogContext;
}
Expand All @@ -54,6 +60,7 @@ const BlogPost: React.FC<BlogPostProps> = ({
body,
slug,
frontmatter: { title, coverImage, coverImageAlt, created },
coverImagePlaceholder,
previous,
next,
}) => {
Expand Down Expand Up @@ -82,6 +89,7 @@ const BlogPost: React.FC<BlogPostProps> = ({
className="pt-4 katex-custom"
title={title}
coverImage={coverImage}
coverImagePlaceholder={coverImagePlaceholder}
coverImageAlt={coverImageAlt}
created={created}
>
Expand Down
Loading

0 comments on commit 95b1627

Please sign in to comment.