Skip to content

Commit

Permalink
Merge pull request #726 from simonihmig/wc-blurhash
Browse files Browse the repository at this point in the history
Support BlurHash in wc/lit package
  • Loading branch information
simonihmig authored Nov 6, 2024
2 parents 18255f0 + 4dabb86 commit b8ebf96
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 133 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-knives-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@responsive-image/wc': minor
---

Add BlurHash support to web component
6 changes: 6 additions & 0 deletions .changeset/moody-fireants-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@responsive-image/ember': patch
'@responsive-image/core': patch
---

Extract BlurHash utils onto core
13 changes: 13 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
"test:watch": "vitest watch",
"prepack": "pnpm turbo build"
},
"peerDependencies": {
"blurhash": "^2.0.0"
},
"devDependencies": {
"@tsconfig/strictest": "2.0.5",
"@typescript-eslint/eslint-plugin": "8.13.0",
"@typescript-eslint/parser": "8.13.0",
"blurhash": "^2.0.0",
"concurrently": "9.1.0",
"eslint": "8.57.1",
"eslint-config-prettier": "9.1.0",
Expand All @@ -37,13 +41,22 @@
"typescript": "5.6.3",
"vitest": "1.6.0"
},
"peerDependenciesMeta": {
"blurhash": {
"optional": true
}
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./blurhash": {
"types": "./dist/blurhash.d.ts",
"default": "./dist/blurhash.js"
}
},
"typesVersions": {
Expand Down
48 changes: 48 additions & 0 deletions packages/core/src/blurhash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { decode } from 'blurhash';

const BLURHASH_ATTRIBUTE = 'data-ri-bh';
const WIDTH_ATTRIBUTE = 'data-ri-bh-w';
const HEIGHT_ATTRIBUTE = 'data-ri-bh-h';

const BLURHASH_SCALE_FACTOR = 4;

export function bh2url(
hash: string,
width: number,
height: number,
): string | undefined {
const blurWidth = width * BLURHASH_SCALE_FACTOR;
const blurHeight = height * BLURHASH_SCALE_FACTOR;
const pixels = decode(hash, blurWidth, blurHeight);
const canvas = document.createElement('canvas');
canvas.width = blurWidth;
canvas.height = blurHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
return undefined;
}

const imageData = ctx.createImageData(blurWidth, blurHeight);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL('image/png');
}

export function applySSR(): void {
const images = document.querySelectorAll<HTMLImageElement>(
`img[${BLURHASH_ATTRIBUTE}]`,
);
images.forEach((image) => {
const hash = image.getAttribute(BLURHASH_ATTRIBUTE);
const width = image.getAttribute(WIDTH_ATTRIBUTE);
const height = image.getAttribute(HEIGHT_ATTRIBUTE);

if (hash && width && height) {
const url = bh2url(hash, parseInt(width, 10), parseInt(height, 10));
if (url) {
image.style.backgroundImage = `url("${url}")`;
image.style.backgroundSize = 'cover';
}
}
});
}
49 changes: 1 addition & 48 deletions packages/ember/src/blurhash.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1 @@
import { decode } from 'blurhash';

const BLURHASH_ATTRIBUTE = 'data-ri-bh';
const WIDTH_ATTRIBUTE = 'data-ri-bh-w';
const HEIGHT_ATTRIBUTE = 'data-ri-bh-h';

const BLURHASH_SCALE_FACTOR = 4;

export function bh2url(
hash: string,
width: number,
height: number,
): string | undefined {
const blurWidth = width * BLURHASH_SCALE_FACTOR;
const blurHeight = height * BLURHASH_SCALE_FACTOR;
const pixels = decode(hash, blurWidth, blurHeight);
const canvas = document.createElement('canvas');
canvas.width = blurWidth;
canvas.height = blurHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
return undefined;
}

const imageData = ctx.createImageData(blurWidth, blurHeight);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL('image/png');
}

export function applySSR(): void {
const images = document.querySelectorAll<HTMLImageElement>(
`img[${BLURHASH_ATTRIBUTE}]`,
);
images.forEach((image) => {
const hash = image.getAttribute(BLURHASH_ATTRIBUTE);
const width = image.getAttribute(WIDTH_ATTRIBUTE);
const height = image.getAttribute(HEIGHT_ATTRIBUTE);

if (hash && width && height) {
const url = bh2url(hash, parseInt(width, 10), parseInt(height, 10));
if (url) {
image.style.backgroundImage = `url("${url}")`;
image.style.backgroundSize = 'cover';
}
}
});
}
export * from '@responsive-image/core/blurhash';
1 change: 0 additions & 1 deletion packages/ember/src/components/responsive-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ export default class ResponsiveImageComponent extends Component<ResponsiveImageC
}

const url = this.args.src.imageUrlFor(this.width ?? 640);
console.log(url);
return url;
}

Expand Down
1 change: 1 addition & 0 deletions packages/wc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"@responsive-image/core": "workspace:^",
"blurhash": "^2.0.0",
"lit": "^3.1.4"
},
"devDependencies": {
Expand Down
31 changes: 30 additions & 1 deletion packages/wc/src/responsive-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { html, css, LitElement, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import {
type ImageType,
// type LqipBlurhash,
type LqipBlurhash,
type ImageData,
env,
getDestinationWidthBySize,
} from '@responsive-image/core';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { bh2url } from '@responsive-image/core/blurhash';
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';

interface ImageSource {
srcset: string;
Expand Down Expand Up @@ -153,6 +155,25 @@ export class ResponsiveImage extends LitElement {
return this.src.imageUrlFor(this.imgWidth ?? 640);
}

get hasLqipBlurhash(): boolean {
return this.src.lqip?.type === 'blurhash';
}

get showLqipBlurhash(): boolean {
return !this.isLoaded && this.hasLqipBlurhash;
}

get lqipBlurhash(): string | undefined {
if (!this.hasLqipBlurhash) {
return undefined;
}

const { hash, width, height } = this.src.lqip as LqipBlurhash;
const uri = bh2url(hash, width, height);

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

render() {
const { lqip } = this.src;

Expand All @@ -166,6 +187,13 @@ export class ResponsiveImage extends LitElement {
(lqip?.type === 'color' || lqip?.type === 'inline') && !this.isLoaded,
};

const styles: StyleInfo = this.showLqipBlurhash
? {
backgroundImage: this.lqipBlurhash,
backgroundSize: 'cover',
}
: {};

return html`
<picture>
${this.sourcesSorted.map(
Expand All @@ -180,6 +208,7 @@ export class ResponsiveImage extends LitElement {
width=${ifDefined(this.imgWidth)}
height=${ifDefined(this.imgHeight)}
class=${classMap(classes)}
style=${styleMap(styles)}
src=${ifDefined(this.imgSrc)}
alt=${this.alt}
loading=${this.loading}
Expand Down
4 changes: 2 additions & 2 deletions packages/wc/test/responsive-image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ describe('ResponsiveImage', () => {
});
});

describe.skip('blurhash', () => {
describe('blurhash', () => {
it('it sets LQIP from blurhash as background', async () => {
const { onload, loaded } = imageLoaded();
const imageData: ImageData = {
Expand Down Expand Up @@ -487,7 +487,7 @@ describe('ResponsiveImage', () => {
expect(
window.getComputedStyle(imgEl!).backgroundImage,
'after image is loaded the background PNG is removed',
).to.be('none');
).to.equal('none');
});
});
});
Expand Down
Loading

0 comments on commit b8ebf96

Please sign in to comment.