initial: support lqip and responsive image rendering/resizing

This commit is contained in:
kdevo
2021-09-18 11:20:39 +02:00
commit 3cfc4e6945
21 changed files with 380 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
_vendor/
resources/
public/
+81
View File
@@ -0,0 +1,81 @@
<!-- <a href=""><img alt="Logo" align="right" width="200" src="https://raw.githubusercontent.com/hugo-mods/lazy/main/.github/logo.png"></a> -->
# lazyimg
## Lazy image loading made easy. With automatic image resizing and LQIP support.
- Lazy loading for images via `lazySizes`
- No-Script-safe: Fallback to browser's native method
- ...
> Used by the [Osprey Delight](https://github.com/kdevo/osprey-delight) theme, which directly benefits from this module!
## Go get it
Initialize [Hugo's mod system](https://gohugo.io/hugo-modules/) on your site:
`hugo mod init github.com/{username}/{repo}`
Add module to site's config (e.g. `config.yaml`):
```yaml
module:
imports:
- path: github.com/hugo-mods/lazyimg
```
Get the module (also upgrades existing one):
`hugo mod get -u`
## Quickstart
1. Put the images which you want to use in the `assets` directory of your project. They should be in a high resolution and will be resized automatically.
2. Add the following boilerplate setup code to your site's `<head>`:
```
{{ partial "lazyimg-setup" }}
```
3. Load the image by calling the `lazyimg` partial where you would usually use an `img` tag:
```
{{ partial "lazyimg" "my-awesome-image.jpeg" }}
```
For more advanced usage, please refer to the [`exampleSite`](./exampleSite) for a practical approach or continue reading the theory as described in the next section.
## Configuration
### Resizers
| Name | Description
|:----------------------------|-------------------------------------------------------------
| `simple` | Produces default image with `maxSize`, LQIP with `lqipSize`.
| `responsive` | Produces default image with `maxSize`, LQIP with `lqipSize` and responsive images based on `responsiveSizes`.
| `auto` | Produces default image with `maxSize`, LQIP with `lqipSize` and guesses responsive sizes based on an algorithm. Highly experimental.
### Renderers
| Name | Description
|:----------------------------|-------------------------------------------------------------
| `lqip` | All-around responsive lazy-loading with LQIP blur-up preview. Recommended: [`lazyimg.css`](#CSS).
| `lqip-webp` | `lqip` with additional WebP support. TODO(kdevo): not implemented yet.
### CSS
The `lazyimg.css` is the recommended boilderplate CSS.
It contains rules for blur-up animation, hiding "broken image icon" on lazy-loading and [no-js](#disabled-js) selector.
### Disabled JS
Support for disabled JS can be accomplished by adding a "no-js" class to your `html` root tag, e.g.:
```html
<html lang="en" class="no-js"> <!-- ... --> </html>
```
Then, to remove the class when the client indeed supports JS, add the following script:
```html
<script>document.documentElement.className = "js"</script>
```
Alternatively, use `lazyimg-setup-nojs` instead of `lazyimg-setup` which will do the replacement for you.
In this case, you only need to add the `no-js` class to your `html` root tag.
+20
View File
@@ -0,0 +1,20 @@
/* Hides images to be loaded with JS when it is disabled */
.no-js img.lazyload {
display: none;
}
/* Hides broken image icon which may shortly be disaplayed on load */
img.lazyload:not([src]) {
visibility: hidden;
}
/* Simple blur-up animation */
.blur-up {
-webkit-filter: blur(3px);
filter: blur(3px);
transition: filter 500ms, -webkit-filter 500ms;
}
.blur-up.lazyloaded {
-webkit-filter: blur(0);
filter: blur(0);
}
+15
View File
@@ -0,0 +1,15 @@
Params:
lazyimg:
# TODO(kdevo): provide sensible defaults
module:
hugoVersion:
extended: true
# needed for webp support and mutli-line dicts:
min: "0.82.0" # TODO(kdevo): raise to 0.83.0
max: "1.00.0"
imports:
- path: "github.com/aFarkas/lazysizes"
mounts:
- source: "./"
target: "assets/lazyimg/lazysizes"
+3
View File
@@ -0,0 +1,3 @@
_vendor/
resources/
public/
+25
View File
@@ -0,0 +1,25 @@
module:
imports:
- path: github.com/hugo-mods/lazyimg
# replacements forces using local version for dev instead of using the remote:
replacements:
- github.com/hugo-mods/lazyimg -> ../../.
# disableKinds is only needed for this exampleSite to surpress some warnings:
disableKinds: ["taxonomy", "term"]
Params:
lazyimg:
resizer: auto
renderer: lqip
# Resizer options:
lqipSize: "120x"
maxSize: "1920x"
responsiveSizes: ["320x", "640x", "768x", "1024x", "1366x", "1600x", "1920x"]
resizeOptions: "Lanczos q95"
# Renderer options:
class: "my-img-class"
alt: "My logo"
noscript: true
+3
View File
@@ -0,0 +1,3 @@
module github.com/hugo-mods/lazyimg/exampleSite
go 1.13
+55
View File
@@ -0,0 +1,55 @@
<!doctype html>
<!-- The 'no-js' class will be removed on lazyimg-setup: -->
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hugo-mods: lazyimg</title>
<style>
/* TODO(kdevo): create a nice flexbox image gallery example
/* .grid {
display: flex;
flex-flow: column;
justify-content: space-between;
}
.item {
width: 50%;
flex-grow: 1;
height: auto;
max-width: 1024px;
} */
html {
box-sizing: border-box;
}
img {
height: auto;
width: 100%;
max-width: 1920px;
border-radius: 5px;
border: 1px solid #aaa;
}
</style>
<!-- lazyimg.css ensures that the image is not displayed twice when the user has no-js: -->
<style>{{ (resources.Get "lazyimg.css" | minify).Content | safeCSS }}</style>
<!-- This script basically loads laszysizes asynchronously: -->
{{ partial "lazyimg-setup-nojs" . }}
</head>
<body>
<!-- Most basic usage with single parameter that can either be a filename or resource: -->
{{ partial "lazyimg" "alps.jpg" }}
<!-- Here we call the partial by passing a param dict that overrides the site's params: -->
{{ partial "lazyimg" (dict "img" "alps.jpg" "class" "my-fancy-class" "alt" "This image shows the alps" "resizer" "simple") }}
{{ $images := (partial "images" .) }}
{{ range $images -}}
<!-- Here we call the partial with a resource: -->
{{ partial "lazyimg" . }}
{{- end }}
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<!-- Create different versions of example image to demonstrate lazy-loading: -->
{{ $images := slice }}
{{ range seq 10 -}}
{{ $img := resources.Get "alps.jpg" | images.Filter (images.Hue (add -180 (mul . 36))) }}
{{ $images = append $img $images }}
{{- end }}
{{ return $images }}
+5
View File
@@ -0,0 +1,5 @@
module github.com/kdevo/hugo-mod-lazy
go 1.13
require github.com/aFarkas/lazysizes v0.0.0-20210408151554-27c7ceb7d737 // indirect
+2
View File
@@ -0,0 +1,2 @@
github.com/aFarkas/lazysizes v0.0.0-20210408151554-27c7ceb7d737 h1:0rEJGkemFAVemGpxArHHkICg/XhP6parOV4vnnjT174=
github.com/aFarkas/lazysizes v0.0.0-20210408151554-27c7ceb7d737/go.mod h1:xl3heUINAn0oPowV1biDQ2Pyw1w3x66I7KEbWkaxox0=
+2
View File
@@ -0,0 +1,2 @@
{{ partial "partials/lazyimg-setup" }}
<script>document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/g, "js");</script>
+6
View File
@@ -0,0 +1,6 @@
{{- $lsPath := "lazyimg/lazysizes/lazysizes.min.js" -}}
{{- with (resources.Get $lsPath) -}}
<script src="{{ .Permalink }}" async></script>
{{- else -}}
{{ errorf "could not locate %s" $lsPath }}
{{- end }}
+32
View File
@@ -0,0 +1,32 @@
{{/* Parameter Parsing */}}
{{ $usage := `lazyimg:
Usage like:
- {{ partial "lazyimg" "image.jpg" }}
- {{ partial "lazyimg" (resources.Get "image.jpg") }}
...or more verbose by specifying a dict overriding the site.Params.lazyimg, e.g.:
- {{ partial "lazyimg" (dict "resizer" "auto" "renderer" "lqip") }}
- {{ partial "lazyimg" (dict "img" "image.jpg" "responsiveSizes" (slice "320px" "640px") "resizer" "responsive") }}
Please ensure that image exists in ./assets directory.
` }}
{{ $params := merge site.Params.lazyimg (dict
"img" .
"resizer" (default "responsive" site.Params.lazyimg.resizer)
"renderer" (default "lqip" site.Params.lazyimg.renderer)
)}}
{{ if reflect.IsMap . }}
{{ $params = merge $params . }}
{{ end }}
{{ $resType := (printf "%T" $params.img) }}
{{ if not (in $resType "resource") }}
{{ $params = merge $params (dict "img" (resources.Get $params.img)) }}
{{ end }}
{{ if not $params.img }}
{{ errorf "image path ./assets/%s does not exist" $params.img }}
{{ warnf $usage }}
{{ end }}
{{ $sizes := partial (printf "resizer/%s" $params.resizer) $params }}
{{ return (partial (printf "renderer/%s" $params.renderer) (merge (dict "imgDict" $sizes) $params)) }}
@@ -0,0 +1,7 @@
<picture>
<!-- TODO(kdevo): load webp images -->
<source type="image/webp"
srcset="
URL 100w,
URL 200w">
</picture>
+8
View File
@@ -0,0 +1,8 @@
{{/* Input: Input: dict {"responsive": [RESOURCE, ...]}. Output: srcset string */}}
{{ $images := .responsive }}
{{ $srcset := slice }}
{{ range $images }}
{{ $srcset = append (printf "%s %dw" .Permalink .Width) $srcset }}
{{ end }}
{{ return (delimit $srcset ",\n") }}
+28
View File
@@ -0,0 +1,28 @@
{{ $class := .class }}
{{ $alt := .alt }}
{{ $noscriptSupport := .noscript }}
{{ $imgDict := .imgDict }}
{{ if $noscriptSupport }}
<noscript>
<!-- TODO: add responsiveness -->
<img src="{{ $imgDict.default.Permalink }}" {{ with $alt }} alt="{{ . }}" {{ end -}}
class="{{ $class }}" loading="lazy"
{{- with $imgDict.default }} height="{{ .Height }}" width="{{ .Width }}" {{ end -}}>
</noscript>
{{ end }}
<img
{{- with $imgDict.lqip -}}
{{- $placeholderB64 := .Content | base64Encode -}}
{{- if gt (len $placeholderB64) 2048 }}
src="{{ .Permalink }}"
{{- else }}
src="data:{{ .MediaType }};base64,{{ $placeholderB64 }}"
{{- end }}
data-src="{{ $imgDict.default.Permalink }}"
class="{{ with $class }}{{ . }} {{ end }}blur-up lazyload"
{{ else }}
src="{{ $imgDict.default.Permalink }}" class="{{ $class }} lazyload"
{{- end -}}
{{- with (default $imgDict.default $imgDict.lqip) }} height="{{ .Height }}" width="{{ .Width }}" {{- end -}}
{{- if $imgDict.responsive -}}{{ with partial "renderer/_srcset" $imgDict }} data-sizes="auto" data-srcset="{{ . }}" {{- end }}{{- end -}}
{{- with $alt }} alt="{{ . }}" {{- end }}>
+35
View File
@@ -0,0 +1,35 @@
{{/* Input: Params. Output: dict {"default": RESOURCE, "lqip": RESOURCE, "responsive": [RESOURCE, ...]} */}}
{{ $res := .img }}
{{ $resizeOptions := .resizeoptions }}
{{ $sizes := .responsivesizes }}
{{ $imageDict := partial "resizer/simple" . }}
{{ $default := $imageDict.default }}
{{ $name := replace (path.Base ($res.Permalink)) (path.Ext $res.Name) "" }}
{{/* Responsive img sizing from LQIP width to default width */}}
{{ $startWidth := (default 100 $imageDict.lqip.Width) }}
{{ $sizes := seq $startWidth 50 $imageDict.default.Width }}
{{ $n := 5 }}
{{ if gt $n (len $sizes) -}}
{{ $n = len $sizes }}
{{- end }}
{{/* Partition the sizes */}}
{{ $part := int (div (len $sizes) $n) }}
{{ $wset := slice }}
{{ range (seq 1 (sub $n 1)) -}}
{{ $wset = append (index $sizes (mul $part .)) $wset }}
{{- end }}
{{ $wset = (uniq (append (int $imageDict.default.Width) $wset)) }}
{{/* Fill responsive slice: */}}
{{ $responsive := slice }}
{{ $break := false }}
{{- range $wset -}}
{{- $resized := $res.Resize (printf "%dx %s" . $resizeOptions) -}}
{{- $resized := slice $resized | resources.Concat (printf "%s-%dx%s" $name . (path.Ext $default.Permalink)) -}}
{{- $responsive = append $resized $responsive -}}
{{- end -}}
{{- return merge (dict "responsive" $responsive) $imageDict -}}
+26
View File
@@ -0,0 +1,26 @@
{{/* Input: Params. Output: dict {"default": RESOURCE, "lqip": RESOURCE, "responsive": [RESOURCE, ...]} */}}
{{ $res := .img }}
{{ $resizeOptions := .resizeoptions }}
{{ $sizes := .responsivesizes }}
{{ $imageDict := partial "resizer/simple" . }}
{{ $default := $imageDict.default }}
{{ $name := replace (path.Base ($res.Permalink)) (path.Ext $res.Name) "" }}
{{/* Fill responsive slice. Abort loop with $break when size exceeded (needed due to templating restrictions). */}}
{{ $responsive := slice }}
{{ $break := false }}
{{- range $sizes -}}
{{ if not $break }}
{{- $resized := $res.Resize (printf "%s %s" . $resizeOptions) -}}
{{- $resized := slice $resized | resources.Concat (printf "%s-%s%s" $name . (path.Ext $default.Permalink)) -}}
{{- $responsive = append $resized $responsive -}}
{{- if or (gt $resized.Width $default.Width) (gt $resized.Height $default.Height) -}}
{{- $break = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- return merge (dict "responsive" $responsive) $imageDict -}}
+16
View File
@@ -0,0 +1,16 @@
{{/* Input: Params. Output: dict {"default": RESOURCE, "lqip": RESOURCE} */}}
{{ $res := .img }}
{{ $maxSize := .maxsize }}
{{ $lqipSize := .lqipsize }}
{{ $resizeOptions := .resizeoptions }}
{{ $name := replace (path.Base ($res.Permalink)) (path.Ext $res.Name) "" }}
{{/* Resize base images */}}
{{ $lqip := $res.Resize (printf "%s %s" $lqipSize $resizeOptions ) }}
{{ $lqip := slice $lqip | resources.Concat (printf "%s-%s%s" $name $lqipSize (path.Ext $lqip.Permalink)) }}
{{ $default := $res.Resize (printf "%s %s" $maxSize $resizeOptions) }}
{{ $default := slice $default | resources.Concat (printf "%s-%s%s" $name $maxSize (path.Ext $default.Permalink)) }}
{{ return dict "lqip" $lqip "default" $default }}
+1
View File
@@ -0,0 +1 @@
{{/* TODO(kdevo) */}}