Produce and use webp pictures with different sizes

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-12-11 15:27:04 +01:00
parent c8987df7af
commit 6a52ca0d91
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
15 changed files with 267 additions and 17 deletions

View file

@ -4,7 +4,9 @@
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern", "build:assets": "vue-cli-service build --modern",
"build:pictures": "scripty",
"build": "yarn run build:assets && yarn run build:pictures",
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 vue-cli-service test:unit", "test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e", "test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
@ -87,6 +89,7 @@
"prettier-eslint": "^12.0.0", "prettier-eslint": "^12.0.0",
"sass": "^1.29.0", "sass": "^1.29.0",
"sass-loader": "^10.0.1", "sass-loader": "^10.0.1",
"scripty": "^2.0.0",
"typescript": "~4.1.2", "typescript": "~4.1.2",
"vue-cli-plugin-svg": "~0.1.3", "vue-cli-plugin-svg": "~0.1.3",
"vue-i18n-extract": "^1.0.2", "vue-i18n-extract": "^1.0.2",

View file

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 725 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

90
js/scripts/build/pictures.sh Executable file
View file

@ -0,0 +1,90 @@
#!/bin/bash
set -eu
output_dir="../priv/static/img/pics"
resolutions=(
480
1024
1920
)
ignore=(
homepage_background.png
)
file_extension () {
filename=$(basename -- "$file")
echo "${filename##*.}"
}
file_name () {
filename=$(basename -- "$file")
echo "${filename%.*}"
}
convert_image () {
name=$(file_name)
extension=$(file_extension)
res="$1w"
output="$output_dir/$name-$res.$extension"
convert -geometry "$resolution"x $file $output
}
produce_webp () {
name=$(file_name)
output="$output_dir/$name.webp"
cwebp $file -quiet -o $output
}
progress() {
local w=80 p=$1; shift
# create a string of spaces, then change them to dots
printf -v dots "%*s" "$(( $p*$w/100 ))" ""; dots=${dots// /.};
# print those dots on a fixed-width space plus the percentage etc.
printf "\r\e[K|%-*s| %3d %% %s" "$w" "$dots" "$p" "$*";
}
echo "Generating responsive versions of the pictures…"
if ! command -v convert &> /dev/null
then
echo "$(tput setaf 1)ERROR: The convert command could not be found. You need to install ImageMagick.$(tput sgr 0)"
exit 1
fi
nb_files=$( shopt -s nullglob ; set -- $output_dir/* ; echo $#)
tasks=$((${#resolutions[@]}*$nb_files))
i=1
for file in $output_dir/*
do
if [[ -f $file ]]; then
for resolution in "${resolutions[@]}"; do
convert_image $resolution
progress $(($i*100/$tasks)) still working...
i=$((i+1))
done
fi
done
echo -e "\nDone!"
echo "Generating optimized versions of the pictures…"
if ! command -v cwebp &> /dev/null
then
echo "$(tput setaf 1)ERROR: The cwebp command could not be found. You need to install webp.$(tput sgr 0)"
exit 1
fi
nb_files=$( shopt -s nullglob ; set -- $output_dir/* ; echo $#)
i=1
for file in $output_dir/*
do
if [[ -f $file ]]; then
produce_webp
progress $(($i*100/$nb_files)) still working...
i=$((i+1))
fi
done
echo -e "\nDone!"

View file

@ -1,6 +1,22 @@
<template> <template>
<footer class="footer" ref="footer"> <footer class="footer" ref="footer">
<img :src="`/img/pics/footer_${random}.jpg`" alt="" /> <picture>
<source
:srcset="`/img/pics/footer_${random}-1024w.webp 1x, /img/pics/footer_${random}-1920w.webp 2x`"
type="image/webp"
/>
<source
:srcset="`/img/pics/footer_${random}-1024w.jpg 1x, /img/pics/footer_${random}-1920w.jpg 2x`"
type="image/jpeg"
/>
<img
:src="`/img/pics/footer_${random}-1024w.jpg`"
alt=""
width="5234"
height="2189"
loading="lazy"
/>
</picture>
<ul> <ul>
<li> <li>
<b-select <b-select

10
js/src/utils/support.ts Normal file
View file

@ -0,0 +1,10 @@
export function supportsWebPFormat(): boolean {
const elem = document.createElement("canvas");
if (elem.getContext && elem.getContext("2d")) {
// was able or not to get WebP representation
return elem.toDataURL("image/webp").indexOf("data:image/webp") === 0;
}
// very old browser like IE 8, canvas not supported
return false;
}

View file

@ -101,7 +101,7 @@
> >
<div class="columns is-vertical is-centered"> <div class="columns is-vertical is-centered">
<div class="column is-three-quarters"> <div class="column is-three-quarters">
<div class="img-container" /> <div class="img-container" :class="{ webp: supportsWebPFormat }" />
<div class="content has-text-centered"> <div class="content has-text-centered">
<p> <p>
{{ $t("You didn't create or join any event yet.") }} {{ $t("You didn't create or join any event yet.") }}
@ -129,6 +129,7 @@
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { ParticipantRole } from "@/types/enums"; import { ParticipantRole } from "@/types/enums";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { supportsWebPFormat } from "@/utils/support";
import { IParticipant, Participant } from "../../types/participant.model"; import { IParticipant, Participant } from "../../types/participant.model";
import { import {
LOGGED_USER_PARTICIPATIONS, LOGGED_USER_PARTICIPATIONS,
@ -211,6 +212,8 @@ export default class MyEvents extends Vue {
RouteName = RouteName; RouteName = RouteName;
supportsWebPFormat = supportsWebPFormat;
static monthlyParticipations( static monthlyParticipations(
participations: IParticipant[], participations: IParticipant[],
revertSort = false revertSort = false
@ -355,7 +358,21 @@ section {
.not-found { .not-found {
.img-container { .img-container {
background-image: url("/img/pics/2020-10-06-mobilizon-illustration-B_creation-evenement.jpg"); background-image: url("/img/pics/event_creation-480w.jpg");
@media (min-resolution: 2dppx) {
& {
background-image: url("/img/pics/event_creation-1024w.jpg");
}
}
&.webp {
background-image: url("/img/pics/event_creation-480w.webp");
@media (min-resolution: 2dppx) {
& {
background-image: url("/img/pics/event_creation-1024w.webp");
}
}
}
max-width: 450px; max-width: 450px;
height: 300px; height: 300px;
box-shadow: 0 0 8px 8px white inset; box-shadow: 0 0 8px 8px white inset;

View file

@ -46,7 +46,7 @@
> >
<div class="columns is-vertical is-centered"> <div class="columns is-vertical is-centered">
<div class="column is-three-quarters"> <div class="column is-three-quarters">
<div class="img-container" /> <div class="img-container" :class="{ webp: supportsWebPFormat }" />
<div class="content has-text-centered"> <div class="content has-text-centered">
<p> <p>
{{ $t("You are not part of any group.") }} {{ $t("You are not part of any group.") }}
@ -81,6 +81,7 @@ import { IGroup, usernameWithDomain } from "@/types/actor";
import { Route } from "vue-router"; import { Route } from "vue-router";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums"; import { MemberRole } from "@/types/enums";
import { supportsWebPFormat } from "@/utils/support";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@Component({ @Component({
@ -119,6 +120,8 @@ export default class MyGroups extends Vue {
limit = 10; limit = 10;
supportsWebPFormat = supportsWebPFormat;
acceptInvitation(member: IMember): Promise<Route> { acceptInvitation(member: IMember): Promise<Route> {
return this.$router.push({ return this.$router.push({
name: RouteName.GROUP, name: RouteName.GROUP,
@ -199,7 +202,22 @@ section {
.not-found { .not-found {
.img-container { .img-container {
background-image: url("/img/pics/2020-10-06-mobilizon-illustration-C_groupe.jpg"); background-image: url("/img/pics/group-480w.jpg");
@media (min-resolution: 2dppx) {
& {
background-image: url("/img/pics/group-1024w.jpg");
}
}
&.webp {
background-image: url("/img/pics/group-480w.webp");
@media (min-resolution: 2dppx) {
& {
background-image: url("/img/pics/group-1024w.webp");
}
}
}
max-width: 450px; max-width: 450px;
height: 300px; height: 300px;
box-shadow: 0 0 8px 8px white inset; box-shadow: 0 0 8px 8px white inset;

View file

@ -2,6 +2,7 @@
<div id="homepage"> <div id="homepage">
<section <section
class="hero" class="hero"
:class="{ webp: supportsWebPFormat }"
v-if="config && (!currentUser.id || !currentActor.id)" v-if="config && (!currentUser.id || !currentActor.id)"
> >
<div class="hero-body"> <div class="hero-body">
@ -72,10 +73,59 @@
</div> </div>
<div id="picture" v-if="config && (!currentUser.id || !currentActor.id)"> <div id="picture" v-if="config && (!currentUser.id || !currentActor.id)">
<div class="picture-container"> <div class="picture-container">
<img <picture>
src="/img/pics/2020-10-06-mobilizon-illustration-A_homepage.jpg" <source
alt="" media="(max-width: 799px)"
srcset="/img/pics/homepage-480w.webp"
type="image/webp"
/> />
<source
media="(max-width: 799px)"
srcset="/img/pics/homepage-480w.jpg"
type="image/jpeg"
/>
<source
media="(max-width: 1024px)"
srcset="/img/pics/homepage-1024w.webp"
type="image/webp"
/>
<source
media="(max-width: 1024px)"
srcset="/img/pics/homepage-1024w.jpg"
type="image/jpeg"
/>
<source
media="(max-width: 1920px)"
srcset="/img/pics/homepage-1920w.webp"
type="image/webp"
/>
<source
media="(max-width: 1920px)"
srcset="/img/pics/homepage-1920w.jpg"
type="image/jpeg"
/>
<source
media="(min-width: 1921px)"
srcset="/img/pics/homepage.webp"
type="image/webp"
/>
<source
media="(min-width: 1921px)"
srcset="/img/pics/homepage.jpg"
type="image/jpeg"
/>
<img
src="/img/pics/homepage-1024w.jpg"
width="3840"
height="2719"
alt=""
loading="lazy"
/>
</picture>
</div> </div>
<div class="container section"> <div class="container section">
<div class="columns"> <div class="columns">
@ -221,6 +271,7 @@
import { Component, Vue, Watch } from "vue-property-decorator"; import { Component, Vue, Watch } from "vue-property-decorator";
import { ParticipantRole } from "@/types/enums"; import { ParticipantRole } from "@/types/enums";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import { supportsWebPFormat } from "@/utils/support";
import { IParticipant, Participant } from "../types/participant.model"; import { IParticipant, Participant } from "../types/participant.model";
import { FETCH_EVENTS } from "../graphql/event"; import { FETCH_EVENTS } from "../graphql/event";
import EventListCard from "../components/Event/EventListCard.vue"; import EventListCard from "../components/Event/EventListCard.vue";
@ -296,7 +347,10 @@ import Subtitle from "../components/Utils/Subtitle.vue";
}, },
}) })
export default class Home extends Vue { export default class Home extends Vue {
events!: Paginate<IEvent>; events: Paginate<IEvent> = {
elements: [],
total: 0,
};
locations = []; locations = [];
@ -316,6 +370,8 @@ export default class Home extends Vue {
currentUserParticipations: IParticipant[] = []; currentUserParticipations: IParticipant[] = [];
supportsWebPFormat = supportsWebPFormat;
// get displayed_name() { // get displayed_name() {
// return this.loggedPerson && this.loggedPerson.name === null // return this.loggedPerson && this.loggedPerson.name === null
// ? this.loggedPerson.preferredUsername // ? this.loggedPerson.preferredUsername
@ -531,7 +587,11 @@ section.hero {
height: 100%; height: 100%;
opacity: 0.3; opacity: 0.3;
z-index: -1; z-index: -1;
background: url("/img/pics/homepage_background.png"); background: url("/img/pics/homepage_background-1024w.png");
background-size: cover;
}
&.webp::before {
background-image: url("/img/pics/homepage_background-1024w.webp");
} }
& > .hero-body { & > .hero-body {

View file

@ -2,10 +2,24 @@
<section class="section container has-text-centered not-found"> <section class="section container has-text-centered not-found">
<div class="columns is-vertical is-centered"> <div class="columns is-vertical is-centered">
<div class="column is-half"> <div class="column is-half">
<img <picture>
src="/img/pics/2020-10-06-mobilizon-illustration-E_realisation.jpg" <source
alt="" srcset="/img/pics/error-480w.webp 1x, /img/pics/error-1024w.webp 2x"
type="image/webp"
/> />
<source
srcset="/img/pics/error-480w.jpg 1x, /img/pics/error-1024w.jpg 2x"
type="image/jpeg"
/>
<img
:src="`/img/pics/error-480w.jpg`"
alt=""
width="2616"
height="1698"
loading="lazy"
/>
</picture>
<h1 class="title"> <h1 class="title">
{{ $t("The page you're looking for doesn't exist.") }} {{ $t("The page you're looking for doesn't exist.") }}
</h1> </h1>

View file

@ -2701,7 +2701,7 @@ async@2.6.1:
dependencies: dependencies:
lodash "^4.17.10" lodash "^4.17.10"
async@^2.6.2: async@^2.6.1, async@^2.6.2:
version "2.6.3" version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
@ -10489,6 +10489,11 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1:
expand-tilde "^2.0.0" expand-tilde "^2.0.0"
global-modules "^1.0.0" global-modules "^1.0.0"
resolve-from@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=
resolve-from@^3.0.0: resolve-from@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
@ -10499,6 +10504,13 @@ resolve-from@^4.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve-pkg@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-1.0.0.tgz#e19a15e78aca2e124461dc92b2e3943ef93494d9"
integrity sha1-4ZoV54rKLhJEYdySsuOUPvk0lNk=
dependencies:
resolve-from "^2.0.0"
resolve-url@^0.2.1: resolve-url@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@ -10739,6 +10751,16 @@ schema-utils@^3.0.0:
ajv "^6.12.5" ajv "^6.12.5"
ajv-keywords "^3.5.2" ajv-keywords "^3.5.2"
scripty@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/scripty/-/scripty-2.0.0.tgz#25761bb2e237a7563f705d87357db07791d38459"
integrity sha512-vbd4FPeuNwYNGtRtYa1wDZLPCx5PpW6VrldCEiBGqPz7Je1xZOgNvVPD2axymvqNghBIRiXxAU+JwYrOzvuLJg==
dependencies:
async "^2.6.1"
glob "^7.0.3"
lodash "^4.17.11"
resolve-pkg "^1.0.0"
select-hose@^2.0.0: select-hose@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"