Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply LQIP styles with dynamically generated CSS, to support FastBoot #162

Merged
merged 2 commits into from
Feb 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions addon/components/responsive-image.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@
src={{this.src}}
width={{this.width}}
height={{this.height}}
class="eri-{{this.layout}}"
class={{this.classNames}}
loading="lazy"
decoding="async"
...attributes
{{style
(if this.showLqipImage (hash background-image=this.lqipImage background-size="cover"))
(if this.showLqipBlurhash (hash background-image=this.lqipBlurhash background-size="cover"))
(if this.showLqipColor (hash background-color=this.lqipColor))
}}
{{on "load" this.onLoad}}
/>
Expand Down
75 changes: 25 additions & 50 deletions addon/components/responsive-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,12 @@ import ResponsiveImageService, {
ImageMeta,
ImageType,
LqipBlurhash,
LqipColor,
LqipInline,
Meta,
} from 'ember-responsive-image/services/responsive-image';
import { assert } from '@ember/debug';
import dataUri from 'ember-responsive-image/utils/data-uri';
import blurrySvg from 'ember-responsive-image/utils/blurry-svg';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import {
macroCondition,
getOwnConfig /*, importSync*/,
} from '@embroider/macros';
import { getOwnConfig, macroCondition } from '@embroider/macros';
import { decode } from 'blurhash';

declare module '@embroider/macros' {
Expand All @@ -29,6 +22,7 @@ interface ResponsiveImageComponentArgs {
sizes?: string;
width?: number;
height?: number;
cacheBreaker?: string;
}

interface PictureSource {
Expand Down Expand Up @@ -83,7 +77,12 @@ export default class ResponsiveImageComponent extends Component<ResponsiveImageC
.map((type) => {
const sources: string[] = this.responsiveImage
.getImages(this.args.src, type)
.map((imageMeta) => `${imageMeta.image} ${imageMeta.width}w`);
.map(
(imageMeta) =>
`${imageMeta.image}${
this.args.cacheBreaker ? '?' + this.args.cacheBreaker : ''
} ${imageMeta.width}w`
);

return {
srcset: sources.join(', '),
Expand All @@ -110,7 +109,9 @@ export default class ResponsiveImageComponent extends Component<ResponsiveImageC
type
)!;

return `${imageMeta.image} ${density}x`;
return `${imageMeta.image}${
this.args.cacheBreaker ? '?' + this.args.cacheBreaker : ''
} ${density}x`;
}).filter((source) => source !== undefined);

return {
Expand Down Expand Up @@ -150,7 +151,11 @@ export default class ResponsiveImageComponent extends Component<ResponsiveImageC
* the image source which fits at best for the size and screen
*/
get src(): string | undefined {
return this.imageMeta?.image;
return this.imageMeta
? `${this.imageMeta.image}${
this.args.cacheBreaker ? '?' + this.args.cacheBreaker : ''
}`
: undefined;
}

get width(): number | undefined {
Expand Down Expand Up @@ -187,47 +192,17 @@ export default class ResponsiveImageComponent extends Component<ResponsiveImageC
}
}

get hasLqipImage(): boolean {
return this.meta.lqip?.type === 'inline';
}

get showLqipImage(): boolean {
return !this.isLoaded && this.hasLqipImage;
}

get lqipImage(): string | undefined {
if (!this.hasLqipImage) {
return undefined;
}
const lqip = (this.meta as Required<Meta>).lqip as LqipInline;

const uri = dataUri(
blurrySvg(
dataUri(lqip.image, 'image/png', true),
lqip.width,
lqip.height
),
'image/svg+xml'
);

return `url("${uri}")`;
}

get hasLqipColor(): boolean {
return this.meta.lqip?.type === 'color';
}

get showLqipColor(): boolean {
return !this.isLoaded && this.hasLqipColor;
}

get lqipColor(): string | undefined {
if (!this.hasLqipColor) {
return undefined;
get classNames(): string {
const classNames = [`eri-${this.layout}`];
const lqip = this.meta.lqip;
if (lqip && !this.isLoaded) {
classNames.push(`eri-lqip-${lqip.type}`);
if (lqip.type === 'color' || lqip.type === 'inline') {
classNames.push(lqip.class);
}
}
const lqip = (this.meta as Required<Meta>).lqip as LqipColor;

return lqip.color;
return classNames.join(' ');
}

get hasLqipBlurhash(): boolean {
Expand Down
6 changes: 2 additions & 4 deletions addon/services/responsive-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ export interface LqipBase {

export interface LqipInline extends LqipBase {
type: 'inline';
image: string;
width: number;
height: number;
class: string;
}

export interface LqipColor extends LqipBase {
type: 'color';
color: string;
class: string;
}

export interface LqipBlurhash extends LqipBase {
Expand Down
4 changes: 4 additions & 0 deletions addon/styles/ember-responsive-image.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
.eri-responsive {
content-visibility: auto;
}

.eri-lqip-inline {
background-size: cover;
}
7 changes: 0 additions & 7 deletions addon/utils/data-uri.ts

This file was deleted.

16 changes: 16 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const path = require('path');
const Funnel = require('broccoli-funnel');
const Writer = require('./lib/image-writer');
const CssWriter = require('./lib/css-writer');
const fs = require('fs-extra');
const map = require('broccoli-stew').map;
const mergeTrees = require('broccoli-merge-trees');
Expand All @@ -26,6 +27,7 @@ module.exports = {
metaData: {},
configData: {},
app: null,
cssExtensions: [],
metadataExtensions: [],
extendedMetaData: null,
imagePreProcessors: [],
Expand Down Expand Up @@ -62,6 +64,10 @@ module.exports = {
this.metadataExtensions.push({ callback, target });
},

addCssExtension(callback, target) {
this.cssExtensions.push({ callback, target });
},

/**
* Add a callback function to hook into image processing before the addon's image processes are executed.
* The callback method you provide must have the following signature:
Expand Down Expand Up @@ -223,6 +229,7 @@ module.exports = {
// we write our image meta data as a script tag into the app's index.html, which the service will read from
// (that happens only in the browser, where we have easy access to the DOM. For FastBoot this is different, see below)
if (type === 'head-footer') {
console.log('content');
return [
'<script id="ember_responsive_image_meta" type="application/json">',
JSON.stringify(this.extendMetadata()),
Expand All @@ -231,6 +238,15 @@ module.exports = {
}
},

treeForAddonStyles(tree) {
const dynTree = new CssWriter(
[this.processingTree],
() => this.metaData,
this.cssExtensions
);
return mergeTrees([tree, dynTree]);
},

treeForFastBoot() {
// we have to rename our own fastboot tree so that our dummy app works correctly, due to this bug in ember-cli-fastboot:
// https://github.com/ember-fastboot/ember-cli-fastboot/issues/807
Expand Down
51 changes: 51 additions & 0 deletions lib/css-writer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

const CachingWriter = require('broccoli-caching-writer');
const path = require('path');
const fs = require('fs-extra');
const baseN = require('base-n');

const b64 = baseN.create();

class CssWriter extends CachingWriter {
constructor(inputNodes, getMeta, cssExtensions, options) {
options = options || {};
options.cacheInclude = [/.*/];
super(inputNodes, options);

this.getMeta = getMeta;
this.cssExtensions = cssExtensions;
}

async build() {
const destinationPath = path.join(
this.outputPath,
'ember-responsive-image-dynamic.css'
);

await fs.writeFile(destinationPath, this.generateCss());
}

generateCss() {
const meta = this.getMeta();
let cssEntries = [];

let classCounter = 0;
const generateClassName = () => `eri-dyn-${b64.encode(classCounter++)}`;

for (const [image, imageMeta] of Object.entries(meta)) {
for (const { callback, target } of this.cssExtensions) {
const css = callback.call(target, image, imageMeta, {
generateClassName,
});
if (css) {
cssEntries.push(css);
}
}
}

return cssEntries.join(`\n`);
}
}

module.exports = CssWriter;
16 changes: 14 additions & 2 deletions lib/plugins/lqip-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class LqipColorPlugin {

addon.addMetadataExtension(this.addMetaData, this);
addon.addImagePreProcessor(this.imagePreProcessor, this);
addon.addCssExtension(this.addCss, this);
}

canProcessImage(config) {
Expand All @@ -24,19 +25,30 @@ class LqipColorPlugin {
dominant.g.toString(16) +
dominant.b.toString(16);

this.metaData.set(image, { type: 'color', color });
this.metaData.set(image, { color });

return sharped;
}

addMetaData(image, metadata /*, config*/) {
const ourMeta = this.metaData.get(image);
if (ourMeta) {
metadata.lqip = ourMeta;
metadata.lqip = { type: 'color', class: ourMeta.class };
}

return metadata;
}

addCss(image, metaData, { generateClassName }) {
const ourMeta = this.metaData.get(image);
if (ourMeta) {
const className = generateClassName();
ourMeta.class = className;
return `.${className} { background-color: ${ourMeta.color}; }`;
}

return undefined;
}
}

module.exports = LqipColorPlugin;
24 changes: 23 additions & 1 deletion lib/plugins/lqip-inline.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const sharp = require('sharp');
const dataUri = require('../utils/data-uri');
const blurrySvg = require('../utils/blurry-svg');

class LqipInlinePlugin {
constructor(addon) {
Expand All @@ -7,6 +9,7 @@ class LqipInlinePlugin {

addon.addMetadataExtension(this.addMetaData, this);
addon.addImagePreProcessor(this.imagePreProcessor, this);
addon.addCssExtension(this.addCss, this);
}

canProcessImage(config) {
Expand Down Expand Up @@ -57,11 +60,30 @@ class LqipInlinePlugin {
addMetaData(image, metadata /*, config*/) {
const ourMeta = this.metaData.get(image);
if (ourMeta) {
metadata.lqip = ourMeta;
metadata.lqip = { type: 'inline', class: ourMeta.class };
}

return metadata;
}

addCss(image, metaData, { generateClassName }) {
const ourMeta = this.metaData.get(image);
if (ourMeta) {
const className = generateClassName();
ourMeta.class = className;
const uri = dataUri(
blurrySvg(
dataUri(ourMeta.image, 'image/png', true),
ourMeta.width,
ourMeta.height
),
'image/svg+xml'
);
return `.${className} { background-image: url(${uri}); }`;
}

return undefined;
}
}

module.exports = LqipInlinePlugin;
8 changes: 2 additions & 6 deletions addon/utils/blurry-svg.ts → lib/utils/blurry-svg.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
// taken from https://github.com/google/eleventy-high-performance-blog/blob/5ed39db7fd3f21ae82ac1a8e833bf283355bd3d0/_11ty/blurry-placeholder.js#L51

export default function blurrySvg(
src: string,
width: number,
height: number
): string {
module.exports = function blurrySvg(src, width, height) {
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${width} ${height}">
<filter id="b" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation=".5"></feGaussianBlur><feComponentTransfer><feFuncA type="discrete" tableValues="1 1"></feFuncA></feComponentTransfer></filter>
<image filter="url(#b)" preserveAspectRatio="none" height="100%" width="100%" xlink:href="${src}"></image>
</svg>`;
}
};
5 changes: 5 additions & 0 deletions lib/utils/data-uri.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = function dataUri(data, type, base64 = false) {
return `data:${type};base64,${
base64 ? data : Buffer.from(data).toString('base64')
}`;
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@glimmer/component": "^1.0.3",
"@glimmer/tracking": "^1.0.3",
"async-q": "^0.3.1",
"base-n": "^3.0.0",
"blurhash": "^1.1.3",
"broccoli-caching-writer": "^3.0.3",
"broccoli-funnel": "^3.0.3",
Expand Down
Loading