@webstudio-is/image 0.91.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/image-dev.stories.js +71 -0
- package/lib/image-loaders.js +2 -4
- package/lib/image-optimize.js +3 -6
- package/{src/image-optimize.test.ts → lib/image-optimize.test.js} +30 -48
- package/lib/image.js +2 -4
- package/lib/index.js +2 -4
- package/package.json +10 -13
- package/lib/cjs/image-loaders.js +0 -49
- package/lib/cjs/image-optimize.js +0 -118
- package/lib/cjs/image.js +0 -81
- package/lib/cjs/index.js +0 -27
- package/lib/cjs/package.json +0 -1
- package/src/image-dev.stories.tsx +0 -130
- package/src/image-loaders.ts +0 -29
- package/src/image-optimize.ts +0 -260
- package/src/image.tsx +0 -70
- package/src/index.ts +0 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Image as ImagePrimitive, createImageLoader } from "./";
|
|
4
|
+
import localLogoImage from "../storybook-assets/logo.webp";
|
|
5
|
+
export default {
|
|
6
|
+
title: "Components/ImageDev"
|
|
7
|
+
};
|
|
8
|
+
const USE_CLOUDFLARE_IMAGE_TRANSFORM = false;
|
|
9
|
+
const REMOTE_SELF_DOMAIN_IMAGE = "https://webstudio.is/logo.webp";
|
|
10
|
+
const imageSrc = USE_CLOUDFLARE_IMAGE_TRANSFORM ? REMOTE_SELF_DOMAIN_IMAGE : localLogoImage;
|
|
11
|
+
const imageLoader = createImageLoader({
|
|
12
|
+
imageBaseUrl: USE_CLOUDFLARE_IMAGE_TRANSFORM ? "https://webstudio.is/cdn-cgi/image/" : ""
|
|
13
|
+
});
|
|
14
|
+
const ImageBase = (args) => {
|
|
15
|
+
const style = {
|
|
16
|
+
maxWidth: "100%",
|
|
17
|
+
display: "block",
|
|
18
|
+
...args.style
|
|
19
|
+
};
|
|
20
|
+
return /* @__PURE__ */ jsx(
|
|
21
|
+
ImagePrimitive,
|
|
22
|
+
{
|
|
23
|
+
...args,
|
|
24
|
+
optimize: true,
|
|
25
|
+
loader: imageLoader,
|
|
26
|
+
style
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
export const FixedWidthImage = () => /* @__PURE__ */ jsx(ImageBase, { src: imageSrc, width: "300", height: "400" });
|
|
31
|
+
export const FixedWidthImageCover = () => /* @__PURE__ */ jsx(
|
|
32
|
+
ImageBase,
|
|
33
|
+
{
|
|
34
|
+
src: imageSrc,
|
|
35
|
+
width: "300",
|
|
36
|
+
height: "400",
|
|
37
|
+
style: { objectFit: "cover" }
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
export const UnknownWidthImage = () => /* @__PURE__ */ jsx(ImageBase, { src: imageSrc });
|
|
41
|
+
export const AspectRatioImage = () => /* @__PURE__ */ jsx("div", { style: { width: "50%" }, children: /* @__PURE__ */ jsx(
|
|
42
|
+
ImageBase,
|
|
43
|
+
{
|
|
44
|
+
src: imageSrc,
|
|
45
|
+
style: { aspectRatio: "2/1", objectFit: "cover", width: "100%" }
|
|
46
|
+
}
|
|
47
|
+
) });
|
|
48
|
+
export const FillParentImage = () => /* @__PURE__ */ jsx("div", { style: { width: "50%", aspectRatio: "2/1", position: "relative" }, children: /* @__PURE__ */ jsx(
|
|
49
|
+
ImageBase,
|
|
50
|
+
{
|
|
51
|
+
src: imageSrc,
|
|
52
|
+
style: {
|
|
53
|
+
objectFit: "cover",
|
|
54
|
+
position: "absolute",
|
|
55
|
+
width: "100%",
|
|
56
|
+
height: "100%"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
) });
|
|
60
|
+
export const HeroImage = () => /* @__PURE__ */ jsx(
|
|
61
|
+
ImageBase,
|
|
62
|
+
{
|
|
63
|
+
src: imageSrc,
|
|
64
|
+
sizes: "100vw",
|
|
65
|
+
style: {
|
|
66
|
+
aspectRatio: "3/1",
|
|
67
|
+
objectFit: "cover",
|
|
68
|
+
width: "100%"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
);
|
package/lib/image-loaders.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
import warnOnce from "warn-once";
|
|
2
3
|
import { allSizes } from "./image-optimize";
|
|
3
|
-
const createImageLoader = (loaderOptions) => ({ width, src, quality }) => {
|
|
4
|
+
export const createImageLoader = (loaderOptions) => ({ width, src, quality }) => {
|
|
4
5
|
if (true) {
|
|
5
6
|
warnOnce(
|
|
6
7
|
allSizes.includes(width) === false,
|
|
@@ -14,6 +15,3 @@ const createImageLoader = (loaderOptions) => ({ width, src, quality }) => {
|
|
|
14
15
|
searchParams.set("format", "auto");
|
|
15
16
|
return `${imageBaseUrl}${src}?${searchParams.toString()}`;
|
|
16
17
|
};
|
|
17
|
-
export {
|
|
18
|
-
createImageLoader
|
|
19
|
-
};
|
package/lib/image-optimize.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
2
3
|
const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
3
|
-
const allSizes = [...imageSizes, ...deviceSizes];
|
|
4
|
+
export const allSizes = [...imageSizes, ...deviceSizes];
|
|
4
5
|
const getWidths = (width, sizes) => {
|
|
5
6
|
if (sizes) {
|
|
6
7
|
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g;
|
|
@@ -64,7 +65,7 @@ const getInt = (value) => {
|
|
|
64
65
|
};
|
|
65
66
|
const DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
|
|
66
67
|
const DEFAULT_QUALITY = 80;
|
|
67
|
-
const getImageAttributes = (props) => {
|
|
68
|
+
export const getImageAttributes = (props) => {
|
|
68
69
|
const width = getInt(props.width);
|
|
69
70
|
const quality = Math.max(
|
|
70
71
|
Math.min(getInt(props.quality) ?? DEFAULT_QUALITY, 100),
|
|
@@ -92,7 +93,3 @@ const getImageAttributes = (props) => {
|
|
|
92
93
|
}
|
|
93
94
|
return null;
|
|
94
95
|
};
|
|
95
|
-
export {
|
|
96
|
-
allSizes,
|
|
97
|
-
getImageAttributes
|
|
98
|
-
};
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
import { describe, test, expect } from "@jest/globals";
|
|
2
3
|
import { getImageAttributes } from "./image-optimize";
|
|
3
4
|
import { createImageLoader } from "./image-loaders";
|
|
4
|
-
|
|
5
5
|
describe("Image optimizations applied", () => {
|
|
6
6
|
test("width is number, create pixel density descriptor 'x'", () => {
|
|
7
7
|
const imgAttr = getImageAttributes({
|
|
8
8
|
optimize: true,
|
|
9
9
|
width: 100,
|
|
10
10
|
src: "logo.webp",
|
|
11
|
-
srcSet:
|
|
12
|
-
sizes:
|
|
11
|
+
srcSet: void 0,
|
|
12
|
+
sizes: void 0,
|
|
13
13
|
quality: 100,
|
|
14
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
14
|
+
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
15
15
|
});
|
|
16
|
-
|
|
17
16
|
expect(imgAttr).toMatchInlineSnapshot(`
|
|
18
17
|
{
|
|
19
18
|
"sizes": undefined,
|
|
@@ -22,18 +21,16 @@ describe("Image optimizations applied", () => {
|
|
|
22
21
|
}
|
|
23
22
|
`);
|
|
24
23
|
});
|
|
25
|
-
|
|
26
24
|
test("width is undefined, create 'w' descriptor and sizes prop", () => {
|
|
27
25
|
const imgAttr = getImageAttributes({
|
|
28
26
|
optimize: true,
|
|
29
|
-
width:
|
|
27
|
+
width: void 0,
|
|
30
28
|
src: "logo.webp",
|
|
31
|
-
srcSet:
|
|
32
|
-
sizes:
|
|
29
|
+
srcSet: void 0,
|
|
30
|
+
sizes: void 0,
|
|
33
31
|
quality: 90,
|
|
34
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
32
|
+
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
35
33
|
});
|
|
36
|
-
|
|
37
34
|
expect(imgAttr).toMatchInlineSnapshot(`
|
|
38
35
|
{
|
|
39
36
|
"sizes": "(min-width: 1280px) 50vw, 100vw",
|
|
@@ -42,18 +39,16 @@ describe("Image optimizations applied", () => {
|
|
|
42
39
|
}
|
|
43
40
|
`);
|
|
44
41
|
});
|
|
45
|
-
|
|
46
42
|
test("width is undefined and size defined, creates 'w' descriptor and use input sizes props", () => {
|
|
47
43
|
const imgAttr = getImageAttributes({
|
|
48
44
|
optimize: true,
|
|
49
|
-
width:
|
|
45
|
+
width: void 0,
|
|
50
46
|
src: "logo.webp",
|
|
51
|
-
srcSet:
|
|
47
|
+
srcSet: void 0,
|
|
52
48
|
sizes: "100vw",
|
|
53
49
|
quality: 70,
|
|
54
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
50
|
+
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
55
51
|
});
|
|
56
|
-
|
|
57
52
|
expect(imgAttr).toMatchInlineSnapshot(`
|
|
58
53
|
{
|
|
59
54
|
"sizes": "100vw",
|
|
@@ -62,20 +57,18 @@ describe("Image optimizations applied", () => {
|
|
|
62
57
|
}
|
|
63
58
|
`);
|
|
64
59
|
});
|
|
65
|
-
|
|
66
60
|
test("width is undefined and size defined, creates 'w' descriptor and use input sizes props, resizeOrigin defined", () => {
|
|
67
61
|
const imgAttr = getImageAttributes({
|
|
68
62
|
optimize: true,
|
|
69
|
-
width:
|
|
63
|
+
width: void 0,
|
|
70
64
|
src: "logo.webp",
|
|
71
|
-
srcSet:
|
|
65
|
+
srcSet: void 0,
|
|
72
66
|
sizes: "100vw",
|
|
73
67
|
quality: 70,
|
|
74
68
|
loader: createImageLoader({
|
|
75
|
-
imageBaseUrl: "https://resize-origin.is/asset/image/"
|
|
76
|
-
})
|
|
69
|
+
imageBaseUrl: "https://resize-origin.is/asset/image/"
|
|
70
|
+
})
|
|
77
71
|
});
|
|
78
|
-
|
|
79
72
|
expect(imgAttr).toMatchInlineSnapshot(`
|
|
80
73
|
{
|
|
81
74
|
"sizes": "100vw",
|
|
@@ -84,19 +77,16 @@ describe("Image optimizations applied", () => {
|
|
|
84
77
|
}
|
|
85
78
|
`);
|
|
86
79
|
});
|
|
87
|
-
|
|
88
80
|
test("custom loader", () => {
|
|
89
81
|
const imgAttr = getImageAttributes({
|
|
90
82
|
optimize: true,
|
|
91
|
-
width:
|
|
83
|
+
width: void 0,
|
|
92
84
|
src: "https://webstudio.is/logo.webp",
|
|
93
|
-
srcSet:
|
|
85
|
+
srcSet: void 0,
|
|
94
86
|
sizes: "100vw",
|
|
95
87
|
quality: 70,
|
|
96
|
-
loader: ({ width, src, quality }) =>
|
|
97
|
-
`${new URL(src).pathname}?w=${width}&q=${quality}`,
|
|
88
|
+
loader: ({ width, src, quality }) => `${new URL(src).pathname}?w=${width}&q=${quality}`
|
|
98
89
|
});
|
|
99
|
-
|
|
100
90
|
expect(imgAttr).toMatchInlineSnapshot(`
|
|
101
91
|
{
|
|
102
92
|
"sizes": "100vw",
|
|
@@ -106,37 +96,33 @@ describe("Image optimizations applied", () => {
|
|
|
106
96
|
`);
|
|
107
97
|
});
|
|
108
98
|
});
|
|
109
|
-
|
|
110
99
|
describe("Image optimizations not applied", () => {
|
|
111
100
|
test("optimize is false", () => {
|
|
112
101
|
const imgAttr = getImageAttributes({
|
|
113
102
|
optimize: false,
|
|
114
103
|
width: 100,
|
|
115
104
|
src: "https://webstudio.is/logo.webp",
|
|
116
|
-
srcSet:
|
|
117
|
-
sizes:
|
|
105
|
+
srcSet: void 0,
|
|
106
|
+
sizes: void 0,
|
|
118
107
|
quality: 100,
|
|
119
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
108
|
+
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
120
109
|
});
|
|
121
|
-
|
|
122
110
|
expect(imgAttr).toMatchInlineSnapshot(`
|
|
123
111
|
{
|
|
124
112
|
"src": "https://webstudio.is/logo.webp",
|
|
125
113
|
}
|
|
126
114
|
`);
|
|
127
115
|
});
|
|
128
|
-
|
|
129
116
|
test("srcSet is defined", () => {
|
|
130
117
|
const imgAttr = getImageAttributes({
|
|
131
118
|
optimize: true,
|
|
132
119
|
width: 100,
|
|
133
120
|
src: "https://webstudio.is/logo.webp",
|
|
134
121
|
srcSet: "user-defined-srcset",
|
|
135
|
-
sizes:
|
|
122
|
+
sizes: void 0,
|
|
136
123
|
quality: 100,
|
|
137
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
124
|
+
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
138
125
|
});
|
|
139
|
-
|
|
140
126
|
expect(imgAttr).toMatchInlineSnapshot(`
|
|
141
127
|
{
|
|
142
128
|
"src": "https://webstudio.is/logo.webp",
|
|
@@ -144,32 +130,28 @@ describe("Image optimizations not applied", () => {
|
|
|
144
130
|
}
|
|
145
131
|
`);
|
|
146
132
|
});
|
|
147
|
-
|
|
148
133
|
test("src is empty", () => {
|
|
149
134
|
const imgAttr = getImageAttributes({
|
|
150
135
|
optimize: true,
|
|
151
136
|
width: 100,
|
|
152
137
|
src: "",
|
|
153
|
-
srcSet:
|
|
154
|
-
sizes:
|
|
138
|
+
srcSet: void 0,
|
|
139
|
+
sizes: void 0,
|
|
155
140
|
quality: 100,
|
|
156
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
141
|
+
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
157
142
|
});
|
|
158
|
-
|
|
159
143
|
expect(imgAttr).toMatchInlineSnapshot(`null`);
|
|
160
144
|
});
|
|
161
|
-
|
|
162
145
|
test("src is undefined", () => {
|
|
163
146
|
const imgAttr = getImageAttributes({
|
|
164
147
|
optimize: true,
|
|
165
148
|
width: 100,
|
|
166
|
-
src:
|
|
167
|
-
srcSet:
|
|
168
|
-
sizes:
|
|
149
|
+
src: void 0,
|
|
150
|
+
srcSet: void 0,
|
|
151
|
+
sizes: void 0,
|
|
169
152
|
quality: 100,
|
|
170
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
153
|
+
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
171
154
|
});
|
|
172
|
-
|
|
173
155
|
expect(imgAttr).toMatchInlineSnapshot(`null`);
|
|
174
156
|
});
|
|
175
157
|
});
|
package/lib/image.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
import { jsx } from "react/jsx-runtime";
|
|
2
3
|
import { forwardRef } from "react";
|
|
3
4
|
import { getImageAttributes } from "./image-optimize";
|
|
4
5
|
const defaultTag = "img";
|
|
5
|
-
const Image = forwardRef(
|
|
6
|
+
export const Image = forwardRef(
|
|
6
7
|
({
|
|
7
8
|
quality,
|
|
8
9
|
loader,
|
|
@@ -56,6 +57,3 @@ const imagePlaceholderSvg = `data:image/svg+xml;base64,${btoa(`<svg
|
|
|
56
57
|
fill="#A2A2A2"
|
|
57
58
|
/>
|
|
58
59
|
</svg>`)}`;
|
|
59
|
-
export {
|
|
60
|
-
Image
|
|
61
|
-
};
|
package/lib/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webstudio-is/image",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.93.0",
|
|
4
4
|
"description": "Image optimization",
|
|
5
5
|
"author": "Webstudio <github@webstudio.is>",
|
|
6
6
|
"homepage": "https://webstudio.is",
|
|
@@ -10,15 +10,14 @@
|
|
|
10
10
|
"warn-once": "^0.1.1"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@jest/globals": "^29.6.
|
|
14
|
-
"@storybook/react": "^7.
|
|
15
|
-
"@types/react": "^18.2.
|
|
16
|
-
"jest": "^29.6.
|
|
13
|
+
"@jest/globals": "^29.6.4",
|
|
14
|
+
"@storybook/react": "^7.4.0",
|
|
15
|
+
"@types/react": "^18.2.21",
|
|
16
|
+
"jest": "^29.6.4",
|
|
17
17
|
"react": "^18.2.0",
|
|
18
18
|
"react-dom": "^18.2.0",
|
|
19
|
-
"typescript": "5.
|
|
19
|
+
"typescript": "5.2.2",
|
|
20
20
|
"@webstudio-is/jest-config": "^1.0.7",
|
|
21
|
-
"@webstudio-is/scripts": "^0.0.0",
|
|
22
21
|
"@webstudio-is/storybook-config": "^0.0.0",
|
|
23
22
|
"@webstudio-is/tsconfig": "^1.0.7"
|
|
24
23
|
},
|
|
@@ -29,13 +28,11 @@
|
|
|
29
28
|
"exports": {
|
|
30
29
|
"source": "./src/index.ts",
|
|
31
30
|
"types": "./lib/types/index.d.ts",
|
|
32
|
-
"import": "./lib/index.js"
|
|
33
|
-
"require": "./lib/cjs/index.js"
|
|
31
|
+
"import": "./lib/index.js"
|
|
34
32
|
},
|
|
35
33
|
"files": [
|
|
36
34
|
"lib/*",
|
|
37
|
-
"
|
|
38
|
-
"!*.test.*"
|
|
35
|
+
"!*.{test,stories}.*"
|
|
39
36
|
],
|
|
40
37
|
"license": "AGPL-3.0-or-later",
|
|
41
38
|
"private": false,
|
|
@@ -44,8 +41,8 @@
|
|
|
44
41
|
"typecheck": "tsc",
|
|
45
42
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
46
43
|
"checks": "pnpm typecheck && pnpm test",
|
|
47
|
-
"dev": "build
|
|
48
|
-
"build": "
|
|
44
|
+
"dev": "pnpm build --watch",
|
|
45
|
+
"build": "rm -rf lib && esbuild 'src/**/*.ts' 'src/**/*.tsx' --outdir=lib",
|
|
49
46
|
"dts": "tsc --project tsconfig.dts.json",
|
|
50
47
|
"storybook:dev": "storybook dev -p 6006",
|
|
51
48
|
"storybook:build": "storybook build"
|
package/lib/cjs/image-loaders.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
var image_loaders_exports = {};
|
|
30
|
-
__export(image_loaders_exports, {
|
|
31
|
-
createImageLoader: () => createImageLoader
|
|
32
|
-
});
|
|
33
|
-
module.exports = __toCommonJS(image_loaders_exports);
|
|
34
|
-
var import_warn_once = __toESM(require("warn-once"), 1);
|
|
35
|
-
var import_image_optimize = require("./image-optimize");
|
|
36
|
-
const createImageLoader = (loaderOptions) => ({ width, src, quality }) => {
|
|
37
|
-
if (true) {
|
|
38
|
-
(0, import_warn_once.default)(
|
|
39
|
-
import_image_optimize.allSizes.includes(width) === false,
|
|
40
|
-
"Width must be only from allowed values"
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
const { imageBaseUrl } = loaderOptions;
|
|
44
|
-
const searchParams = new URLSearchParams();
|
|
45
|
-
searchParams.set("width", width.toString());
|
|
46
|
-
searchParams.set("quality", quality.toString());
|
|
47
|
-
searchParams.set("format", "auto");
|
|
48
|
-
return `${imageBaseUrl}${src}?${searchParams.toString()}`;
|
|
49
|
-
};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
var image_optimize_exports = {};
|
|
20
|
-
__export(image_optimize_exports, {
|
|
21
|
-
allSizes: () => allSizes,
|
|
22
|
-
getImageAttributes: () => getImageAttributes
|
|
23
|
-
});
|
|
24
|
-
module.exports = __toCommonJS(image_optimize_exports);
|
|
25
|
-
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
26
|
-
const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
27
|
-
const allSizes = [...imageSizes, ...deviceSizes];
|
|
28
|
-
const getWidths = (width, sizes) => {
|
|
29
|
-
if (sizes) {
|
|
30
|
-
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g;
|
|
31
|
-
const percentSizes = [];
|
|
32
|
-
for (let match; match = viewportWidthRe.exec(sizes); match) {
|
|
33
|
-
percentSizes.push(Number.parseInt(match[2], 10));
|
|
34
|
-
}
|
|
35
|
-
if (percentSizes.length) {
|
|
36
|
-
const smallestRatio = Math.min(...percentSizes) * 0.01;
|
|
37
|
-
return {
|
|
38
|
-
widths: allSizes.filter((s) => s >= deviceSizes[0] * smallestRatio),
|
|
39
|
-
kind: "w"
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
return { widths: allSizes, kind: "w" };
|
|
43
|
-
}
|
|
44
|
-
if (width == null) {
|
|
45
|
-
return { widths: deviceSizes, kind: "w" };
|
|
46
|
-
}
|
|
47
|
-
const widths = [
|
|
48
|
-
...new Set(
|
|
49
|
-
[width, width * 2].map(
|
|
50
|
-
(w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1]
|
|
51
|
-
)
|
|
52
|
-
)
|
|
53
|
-
];
|
|
54
|
-
return { widths, kind: "x" };
|
|
55
|
-
};
|
|
56
|
-
const generateImgAttrs = ({
|
|
57
|
-
src,
|
|
58
|
-
width,
|
|
59
|
-
quality,
|
|
60
|
-
sizes,
|
|
61
|
-
loader
|
|
62
|
-
}) => {
|
|
63
|
-
const { widths, kind } = getWidths(width, sizes);
|
|
64
|
-
return {
|
|
65
|
-
sizes: !sizes && kind === "w" ? "100vw" : sizes,
|
|
66
|
-
srcSet: widths.map(
|
|
67
|
-
(w, i) => `${loader({ src, quality, width: w })} ${kind === "w" ? w : i + 1}${kind}`
|
|
68
|
-
).join(", "),
|
|
69
|
-
// Must be last, to prevent Safari to load images twice
|
|
70
|
-
src: loader({
|
|
71
|
-
src,
|
|
72
|
-
quality,
|
|
73
|
-
width: widths[widths.length - 1]
|
|
74
|
-
})
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
const getInt = (value) => {
|
|
78
|
-
if (typeof value === "number") {
|
|
79
|
-
return Math.round(value);
|
|
80
|
-
}
|
|
81
|
-
if (typeof value === "string") {
|
|
82
|
-
const vNum = Number.parseFloat(value);
|
|
83
|
-
if (!Number.isNaN(vNum)) {
|
|
84
|
-
return Math.round(vNum);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return void 0;
|
|
88
|
-
};
|
|
89
|
-
const DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
|
|
90
|
-
const DEFAULT_QUALITY = 80;
|
|
91
|
-
const getImageAttributes = (props) => {
|
|
92
|
-
const width = getInt(props.width);
|
|
93
|
-
const quality = Math.max(
|
|
94
|
-
Math.min(getInt(props.quality) ?? DEFAULT_QUALITY, 100),
|
|
95
|
-
0
|
|
96
|
-
);
|
|
97
|
-
if (props.src != null && props.src !== "") {
|
|
98
|
-
if (props.srcSet == null && props.optimize) {
|
|
99
|
-
const sizes = props.sizes ?? (props.width == null ? DEFAULT_SIZES : void 0);
|
|
100
|
-
return generateImgAttrs({
|
|
101
|
-
src: props.src,
|
|
102
|
-
width,
|
|
103
|
-
quality,
|
|
104
|
-
sizes,
|
|
105
|
-
loader: props.loader
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
const resAttrs = { src: props.src };
|
|
109
|
-
if (props.srcSet != null) {
|
|
110
|
-
resAttrs.srcSet = props.srcSet;
|
|
111
|
-
}
|
|
112
|
-
if (props.sizes != null) {
|
|
113
|
-
resAttrs.sizes = props.sizes;
|
|
114
|
-
}
|
|
115
|
-
return resAttrs;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
};
|
package/lib/cjs/image.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
var image_exports = {};
|
|
20
|
-
__export(image_exports, {
|
|
21
|
-
Image: () => Image
|
|
22
|
-
});
|
|
23
|
-
module.exports = __toCommonJS(image_exports);
|
|
24
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
25
|
-
var import_react = require("react");
|
|
26
|
-
var import_image_optimize = require("./image-optimize");
|
|
27
|
-
const defaultTag = "img";
|
|
28
|
-
const Image = (0, import_react.forwardRef)(
|
|
29
|
-
({
|
|
30
|
-
quality,
|
|
31
|
-
loader,
|
|
32
|
-
optimize = true,
|
|
33
|
-
loading = "lazy",
|
|
34
|
-
decoding = "async",
|
|
35
|
-
...imageProps
|
|
36
|
-
}, ref) => {
|
|
37
|
-
const imageAttributes = (0, import_image_optimize.getImageAttributes)({
|
|
38
|
-
src: imageProps.src,
|
|
39
|
-
srcSet: imageProps.srcSet,
|
|
40
|
-
sizes: imageProps.sizes,
|
|
41
|
-
width: imageProps.width,
|
|
42
|
-
quality,
|
|
43
|
-
loader,
|
|
44
|
-
optimize
|
|
45
|
-
}) ?? { src: imagePlaceholderSvg };
|
|
46
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
47
|
-
"img",
|
|
48
|
-
{
|
|
49
|
-
...imageProps,
|
|
50
|
-
...imageAttributes,
|
|
51
|
-
decoding,
|
|
52
|
-
loading,
|
|
53
|
-
ref
|
|
54
|
-
}
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
Image.displayName = "Image";
|
|
59
|
-
const imagePlaceholderSvg = `data:image/svg+xml;base64,${btoa(`<svg
|
|
60
|
-
width="140"
|
|
61
|
-
height="140"
|
|
62
|
-
viewBox="0 0 600 600"
|
|
63
|
-
fill="none"
|
|
64
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
65
|
-
>
|
|
66
|
-
<rect width="600" height="600" fill="#CCCCCC" />
|
|
67
|
-
<path
|
|
68
|
-
fill-rule="evenodd"
|
|
69
|
-
clip-rule="evenodd"
|
|
70
|
-
d="M450 170H150C141.716 170 135 176.716 135 185V415C135 423.284 141.716 430 150 430H450C458.284 430 465 423.284 465 415V185C465 176.716 458.284 170 450 170ZM150 145C127.909 145 110 162.909 110 185V415C110 437.091 127.909 455 150 455H450C472.091 455 490 437.091 490 415V185C490 162.909 472.091 145 450 145H150Z"
|
|
71
|
-
fill="#A2A2A2"
|
|
72
|
-
/>
|
|
73
|
-
<path
|
|
74
|
-
d="M237.135 235.012C237.135 255.723 220.345 272.512 199.635 272.512C178.924 272.512 162.135 255.723 162.135 235.012C162.135 214.301 178.924 197.512 199.635 197.512C220.345 197.512 237.135 214.301 237.135 235.012Z"
|
|
75
|
-
fill="#A2A2A2"
|
|
76
|
-
/>
|
|
77
|
-
<path
|
|
78
|
-
d="M160 405V367.205L221.609 306.364L256.552 338.628L358.161 234L440 316.043V405H160Z"
|
|
79
|
-
fill="#A2A2A2"
|
|
80
|
-
/>
|
|
81
|
-
</svg>`)}`;
|
package/lib/cjs/index.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
19
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
-
var src_exports = {};
|
|
21
|
-
__export(src_exports, {
|
|
22
|
-
Image: () => import_image.Image
|
|
23
|
-
});
|
|
24
|
-
module.exports = __toCommonJS(src_exports);
|
|
25
|
-
var import_image = require("./image");
|
|
26
|
-
__reExport(src_exports, require("./image-loaders"), module.exports);
|
|
27
|
-
__reExport(src_exports, require("./image-optimize"), module.exports);
|
package/lib/cjs/package.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"type":"commonjs"}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// Story for image development, see https://github.com/webstudio-is/webstudio-builder/issues/387
|
|
2
|
-
|
|
3
|
-
import type * as React from "react";
|
|
4
|
-
import type { Meta, StoryFn } from "@storybook/react";
|
|
5
|
-
import { Image as ImagePrimitive, createImageLoader } from "./";
|
|
6
|
-
|
|
7
|
-
// to not allow include local assets everywhere, just enable it for this file
|
|
8
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
9
|
-
// @ts-ignore
|
|
10
|
-
import localLogoImage from "../storybook-assets/logo.webp"; // eslint-disable-line
|
|
11
|
-
|
|
12
|
-
export default {
|
|
13
|
-
title: "Components/ImageDev",
|
|
14
|
-
} satisfies Meta<typeof ImagePrimitive>;
|
|
15
|
-
|
|
16
|
-
type ImageProps = React.ComponentProps<typeof ImagePrimitive>;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* In case you need to test img with real cloudflare trasforms
|
|
20
|
-
* set USE_CLOUDFLARE_IMAGE_TRANSFORM = true
|
|
21
|
-
**/
|
|
22
|
-
const USE_CLOUDFLARE_IMAGE_TRANSFORM = false;
|
|
23
|
-
|
|
24
|
-
// For cloudflare image transform testing, logo should be the most consistent image on the site
|
|
25
|
-
const REMOTE_SELF_DOMAIN_IMAGE = "https://webstudio.is/logo.webp";
|
|
26
|
-
|
|
27
|
-
const imageSrc = USE_CLOUDFLARE_IMAGE_TRANSFORM
|
|
28
|
-
? REMOTE_SELF_DOMAIN_IMAGE
|
|
29
|
-
: localLogoImage;
|
|
30
|
-
|
|
31
|
-
const imageLoader = createImageLoader({
|
|
32
|
-
imageBaseUrl: USE_CLOUDFLARE_IMAGE_TRANSFORM
|
|
33
|
-
? "https://webstudio.is/cdn-cgi/image/"
|
|
34
|
-
: "",
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const ImageBase: StoryFn<
|
|
38
|
-
React.ForwardRefExoticComponent<
|
|
39
|
-
Omit<ImageProps, "loader"> & {
|
|
40
|
-
style?: React.HTMLAttributes<"img">["style"];
|
|
41
|
-
}
|
|
42
|
-
>
|
|
43
|
-
> = (args) => {
|
|
44
|
-
const style = {
|
|
45
|
-
maxWidth: "100%",
|
|
46
|
-
display: "block",
|
|
47
|
-
...args.style,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<ImagePrimitive
|
|
52
|
-
{...args}
|
|
53
|
-
optimize={true}
|
|
54
|
-
loader={imageLoader}
|
|
55
|
-
style={style}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Load images depending on image width and device per pixel ratio.
|
|
62
|
-
**/
|
|
63
|
-
export const FixedWidthImage: StoryFn<React.FunctionComponent> = () => (
|
|
64
|
-
<ImageBase src={imageSrc} width="300" height="400" />
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Preserve ratio using object-fit: cover. Load images depending on image width and device per pixel ratio.
|
|
69
|
-
**/
|
|
70
|
-
export const FixedWidthImageCover: StoryFn<React.FunctionComponent> = () => (
|
|
71
|
-
<ImageBase
|
|
72
|
-
src={imageSrc}
|
|
73
|
-
width="300"
|
|
74
|
-
height="400"
|
|
75
|
-
style={{ objectFit: "cover" }}
|
|
76
|
-
/>
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Load images depending on the viewport width.
|
|
81
|
-
**/
|
|
82
|
-
export const UnknownWidthImage: StoryFn<React.FunctionComponent> = () => (
|
|
83
|
-
<ImageBase src={imageSrc} />
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Fit width of the parent container, has own aspect-ratio and object-fit=cover.
|
|
88
|
-
* Load images depending on the viewport width.
|
|
89
|
-
**/
|
|
90
|
-
export const AspectRatioImage: StoryFn<React.FunctionComponent> = () => (
|
|
91
|
-
<div style={{ width: "50%" }}>
|
|
92
|
-
<ImageBase
|
|
93
|
-
src={imageSrc}
|
|
94
|
-
style={{ aspectRatio: "2/1", objectFit: "cover", width: "100%" }}
|
|
95
|
-
/>
|
|
96
|
-
</div>
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Fill width and height of the relative parent container, object-fit=cover. Load images depending on the viewport width.
|
|
101
|
-
**/
|
|
102
|
-
export const FillParentImage: StoryFn<React.FunctionComponent> = () => (
|
|
103
|
-
<div style={{ width: "50%", aspectRatio: "2/1", position: "relative" }}>
|
|
104
|
-
<ImageBase
|
|
105
|
-
src={imageSrc}
|
|
106
|
-
style={{
|
|
107
|
-
objectFit: "cover",
|
|
108
|
-
position: "absolute",
|
|
109
|
-
width: "100%",
|
|
110
|
-
height: "100%",
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* "sizes" attribute explicitly equal to 100vw allowing to skip the default behavior.
|
|
118
|
-
* See DEFAULT_SIZES in the Image component. Load images depending on the viewport width.
|
|
119
|
-
**/
|
|
120
|
-
export const HeroImage: StoryFn<React.FunctionComponent> = () => (
|
|
121
|
-
<ImageBase
|
|
122
|
-
src={imageSrc}
|
|
123
|
-
sizes="100vw"
|
|
124
|
-
style={{
|
|
125
|
-
aspectRatio: "3/1",
|
|
126
|
-
objectFit: "cover",
|
|
127
|
-
width: "100%",
|
|
128
|
-
}}
|
|
129
|
-
/>
|
|
130
|
-
);
|
package/src/image-loaders.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import warnOnce from "warn-once";
|
|
2
|
-
import { allSizes, type ImageLoader } from "./image-optimize";
|
|
3
|
-
|
|
4
|
-
export type ImageLoaderOptions = {
|
|
5
|
-
imageBaseUrl: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Default image loader in case of no loader provided
|
|
10
|
-
* https://developers.cloudflare.com/images/image-resizing/url-format/
|
|
11
|
-
**/
|
|
12
|
-
export const createImageLoader =
|
|
13
|
-
(loaderOptions: ImageLoaderOptions): ImageLoader =>
|
|
14
|
-
({ width, src, quality }) => {
|
|
15
|
-
if (process.env.NODE_ENV !== "production") {
|
|
16
|
-
warnOnce(
|
|
17
|
-
allSizes.includes(width) === false,
|
|
18
|
-
"Width must be only from allowed values"
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
const { imageBaseUrl } = loaderOptions;
|
|
22
|
-
const searchParams = new URLSearchParams();
|
|
23
|
-
searchParams.set("width", width.toString());
|
|
24
|
-
searchParams.set("quality", quality.toString());
|
|
25
|
-
searchParams.set("format", "auto");
|
|
26
|
-
|
|
27
|
-
// Cloudflare docs say that we don't need to urlencode the path params
|
|
28
|
-
return `${imageBaseUrl}${src}?${searchParams.toString()}`;
|
|
29
|
-
};
|
package/src/image-optimize.ts
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* # Responsive Image component helpers.
|
|
3
|
-
*
|
|
4
|
-
* ## Quick summary about img srcset and sizes attributes:
|
|
5
|
-
*
|
|
6
|
-
* There are 2 ways to define what image will be loaded in the img property srcset.
|
|
7
|
-
*
|
|
8
|
-
* 1. via pixel density descriptor 'x', like `srcset="photo-small.jpg 1x, photo-medium.jpg 1.5x, photo-huge.jpg 2x"`
|
|
9
|
-
* src will be selected depending on `device-pixel-ratio`.
|
|
10
|
-
*
|
|
11
|
-
* 2. via viewport width descriptor 'w' and sizes property containing source size descriptors, like
|
|
12
|
-
* `srcset="photo-small.jpg 320w, photo-medium.jpg 640w, photo-huge.jpg 1280w"`
|
|
13
|
-
* `sizes="(max-width: 600px) 400px, (max-width: 1200px) 70vw, 50vw"`
|
|
14
|
-
*
|
|
15
|
-
* The browser finds the first matching media query from source size descriptors,
|
|
16
|
-
* then use source size value to generate internally srcset
|
|
17
|
-
* with pixel density descriptors dividing width descriptor value by source size value.
|
|
18
|
-
*
|
|
19
|
-
* Using the example above for viewport width 800px.
|
|
20
|
-
* The first matching media query is (max-width: 1200px)
|
|
21
|
-
* source size value is 70vw equal to 800px * 0,7 = 560px
|
|
22
|
-
*
|
|
23
|
-
* browser internal srcset will be (we divide `w` descriptor by source size value):
|
|
24
|
-
* photo-small.jpg 320/560x, photo-medium.jpg 640/560x, photo-huge.jpg 1280/560x =>
|
|
25
|
-
* photo-small.jpg 0.57x, photo-medium.jpg 1.14x, photo-huge.jpg 2.28x
|
|
26
|
-
*
|
|
27
|
-
* Finally same rules as for pixel density descriptor 'x' are applied.
|
|
28
|
-
*
|
|
29
|
-
* ## Algorithm (without optimizations):
|
|
30
|
-
*
|
|
31
|
-
* We have a predefined array of all supported image sizes allSizes, this is the real width of an image in pixels.
|
|
32
|
-
* This is good for caching, as we can cache image with specific width and then use it for different devices.
|
|
33
|
-
*
|
|
34
|
-
* > allSizes array is a tradeoff between cache and the best possible image size you deliver to the user.
|
|
35
|
-
* > If allSizes.length is too small, you will deliver too big images to the user,
|
|
36
|
-
* > if allSizes.length is too big, you will have many caches misses.
|
|
37
|
-
*
|
|
38
|
-
* If img has a defined width property.
|
|
39
|
-
* 1. find the first value from allSizes which is greater or equal to the width property
|
|
40
|
-
* 2. use found value to generate srcset with pixel density descriptor 'x'
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* If img has no defined width property.
|
|
44
|
-
* 1. Generate srcset = allSizes.map((w) => `${getImageSrcAtWidth(w)} ${w}w`)
|
|
45
|
-
* 2. Use sizes property, or if it is not defined use opinionated DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
|
|
46
|
-
*
|
|
47
|
-
* Optimizations applied now:
|
|
48
|
-
*
|
|
49
|
-
* - If the sizes property is defined, we can exclude from `srcsets` all images
|
|
50
|
-
* which are smaller than the `smallestRatio * smallesDeviceSize`
|
|
51
|
-
*
|
|
52
|
-
* Future (not implemented) optimizations and improvements:
|
|
53
|
-
*
|
|
54
|
-
* - Knowing image size on different viewport widths we can provide nondefault sizes property
|
|
55
|
-
* - Knowledge of Image aspect-ratio would allow cropping images serverside.
|
|
56
|
-
* - Early hints for high priority images https://blog.cloudflare.com/early-hints/
|
|
57
|
-
* - Slow networks optimizations
|
|
58
|
-
* - 404 etc processing with CSS - https://bitsofco.de/styling-broken-images/ (has some opinionated issues) or js solution with custom user fallback.
|
|
59
|
-
*
|
|
60
|
-
* # Attributions
|
|
61
|
-
*
|
|
62
|
-
* The MIT License (MIT)
|
|
63
|
-
*
|
|
64
|
-
* applies to:
|
|
65
|
-
*
|
|
66
|
-
* - https://github.com/vercel/next.js, Copyright (c) 2022 Vercel, Inc.
|
|
67
|
-
*
|
|
68
|
-
* The MIT License (MIT)
|
|
69
|
-
*
|
|
70
|
-
* Copyright (c) 2022 Vercel, Inc.
|
|
71
|
-
*
|
|
72
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
73
|
-
* and associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
74
|
-
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
75
|
-
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
|
|
76
|
-
* is furnished to do so, subject to the following conditions:
|
|
77
|
-
*
|
|
78
|
-
* The above copyright notice and this permission notice shall be included in all copies
|
|
79
|
-
* or substantial portions of the Software.
|
|
80
|
-
*
|
|
81
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
82
|
-
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
83
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
84
|
-
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
85
|
-
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
86
|
-
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
87
|
-
**/
|
|
88
|
-
|
|
89
|
-
export type ImageLoader = (props: {
|
|
90
|
-
width: number;
|
|
91
|
-
quality: number;
|
|
92
|
-
src: string;
|
|
93
|
-
}) => string;
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* max(...imageSizes) must be less then min(...deviceSizes)
|
|
97
|
-
**/
|
|
98
|
-
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
99
|
-
const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
100
|
-
|
|
101
|
-
export const allSizes = [...imageSizes, ...deviceSizes];
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* https://github.com/vercel/next.js/blob/canary/packages/next/client/image.tsx
|
|
105
|
-
**/
|
|
106
|
-
const getWidths = (
|
|
107
|
-
width: number | undefined,
|
|
108
|
-
sizes: string | undefined
|
|
109
|
-
): { widths: number[]; kind: "w" | "x" } => {
|
|
110
|
-
if (sizes) {
|
|
111
|
-
// Find all the "vw" percent sizes used in the sizes prop
|
|
112
|
-
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g;
|
|
113
|
-
const percentSizes = [];
|
|
114
|
-
for (let match; (match = viewportWidthRe.exec(sizes)); match) {
|
|
115
|
-
percentSizes.push(Number.parseInt(match[2], 10));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (percentSizes.length) {
|
|
119
|
-
// we can exclude from srcSets all images which are smaller than the smallestRatio * smallesDeviceSize
|
|
120
|
-
const smallestRatio = Math.min(...percentSizes) * 0.01;
|
|
121
|
-
return {
|
|
122
|
-
widths: allSizes.filter((s) => s >= deviceSizes[0] * smallestRatio),
|
|
123
|
-
kind: "w",
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
return { widths: allSizes, kind: "w" };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (width == null) {
|
|
130
|
-
return { widths: deviceSizes, kind: "w" };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const widths = [
|
|
134
|
-
...new Set(
|
|
135
|
-
[width, width * 2].map(
|
|
136
|
-
(w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1]
|
|
137
|
-
)
|
|
138
|
-
),
|
|
139
|
-
];
|
|
140
|
-
return { widths, kind: "x" };
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const generateImgAttrs = ({
|
|
144
|
-
src,
|
|
145
|
-
width,
|
|
146
|
-
quality,
|
|
147
|
-
sizes,
|
|
148
|
-
loader,
|
|
149
|
-
}: {
|
|
150
|
-
src: string;
|
|
151
|
-
quality: number;
|
|
152
|
-
width: number | undefined;
|
|
153
|
-
sizes: string | undefined;
|
|
154
|
-
loader: ImageLoader;
|
|
155
|
-
}): {
|
|
156
|
-
src: string;
|
|
157
|
-
srcSet: string | undefined;
|
|
158
|
-
sizes: string | undefined;
|
|
159
|
-
} => {
|
|
160
|
-
const { widths, kind } = getWidths(width, sizes);
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
sizes: !sizes && kind === "w" ? "100vw" : sizes,
|
|
164
|
-
srcSet: widths
|
|
165
|
-
.map(
|
|
166
|
-
(w, i) =>
|
|
167
|
-
`${loader({ src, quality, width: w })} ${
|
|
168
|
-
kind === "w" ? w : i + 1
|
|
169
|
-
}${kind}`
|
|
170
|
-
)
|
|
171
|
-
.join(", "),
|
|
172
|
-
|
|
173
|
-
// Must be last, to prevent Safari to load images twice
|
|
174
|
-
src: loader({
|
|
175
|
-
src,
|
|
176
|
-
quality,
|
|
177
|
-
width: widths[widths.length - 1],
|
|
178
|
-
}),
|
|
179
|
-
};
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const getInt = (value: unknown): number | undefined => {
|
|
183
|
-
if (typeof value === "number") {
|
|
184
|
-
return Math.round(value);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (typeof value === "string") {
|
|
188
|
-
const vNum = Number.parseFloat(value);
|
|
189
|
-
|
|
190
|
-
if (!Number.isNaN(vNum)) {
|
|
191
|
-
return Math.round(vNum);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return undefined;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* DEFAULT_SIZES Just an assumption that most images (except hero and icons) are 100% wide on mobile and 50% on desktop.
|
|
200
|
-
* For icons width are usually set explicitly so DEFAULT_SIZES is not applied.
|
|
201
|
-
* For hero images, we can allow in UI to select sizes=100vw explicitly.
|
|
202
|
-
* Anyway, the best would be to calculate this based on canvas data from different breakpoints.
|
|
203
|
-
* See ../component-utils/image for detailed description
|
|
204
|
-
**/
|
|
205
|
-
const DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
|
|
206
|
-
|
|
207
|
-
const DEFAULT_QUALITY = 80;
|
|
208
|
-
|
|
209
|
-
export const getImageAttributes = (props: {
|
|
210
|
-
src: string | undefined;
|
|
211
|
-
srcSet: string | undefined;
|
|
212
|
-
sizes: string | undefined;
|
|
213
|
-
width: string | number | undefined;
|
|
214
|
-
quality: string | number | undefined;
|
|
215
|
-
loader: ImageLoader;
|
|
216
|
-
optimize: boolean;
|
|
217
|
-
}): {
|
|
218
|
-
src: string;
|
|
219
|
-
srcSet?: string;
|
|
220
|
-
sizes?: string;
|
|
221
|
-
} | null => {
|
|
222
|
-
const width = getInt(props.width);
|
|
223
|
-
const quality = Math.max(
|
|
224
|
-
Math.min(getInt(props.quality) ?? DEFAULT_QUALITY, 100),
|
|
225
|
-
0
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
if (props.src != null && props.src !== "") {
|
|
229
|
-
if (props.srcSet == null && props.optimize) {
|
|
230
|
-
const sizes =
|
|
231
|
-
props.sizes ?? (props.width == null ? DEFAULT_SIZES : undefined);
|
|
232
|
-
|
|
233
|
-
return generateImgAttrs({
|
|
234
|
-
src: props.src,
|
|
235
|
-
width,
|
|
236
|
-
quality,
|
|
237
|
-
sizes,
|
|
238
|
-
loader: props.loader,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const resAttrs: {
|
|
243
|
-
src: string;
|
|
244
|
-
srcSet?: string;
|
|
245
|
-
sizes?: string;
|
|
246
|
-
} = { src: props.src };
|
|
247
|
-
|
|
248
|
-
if (props.srcSet != null) {
|
|
249
|
-
resAttrs.srcSet = props.srcSet;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (props.sizes != null) {
|
|
253
|
-
resAttrs.sizes = props.sizes;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return resAttrs;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return null;
|
|
260
|
-
};
|
package/src/image.tsx
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type ElementRef, type ComponentProps } from "react";
|
|
2
|
-
import { getImageAttributes, type ImageLoader } from "./image-optimize";
|
|
3
|
-
|
|
4
|
-
const defaultTag = "img";
|
|
5
|
-
|
|
6
|
-
type ImageProps = ComponentProps<typeof defaultTag> & {
|
|
7
|
-
quality?: number;
|
|
8
|
-
optimize?: boolean;
|
|
9
|
-
loader: ImageLoader;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const Image = forwardRef<ElementRef<typeof defaultTag>, ImageProps>(
|
|
13
|
-
(
|
|
14
|
-
{
|
|
15
|
-
quality,
|
|
16
|
-
loader,
|
|
17
|
-
optimize = true,
|
|
18
|
-
loading = "lazy",
|
|
19
|
-
decoding = "async",
|
|
20
|
-
...imageProps
|
|
21
|
-
},
|
|
22
|
-
ref
|
|
23
|
-
) => {
|
|
24
|
-
const imageAttributes = getImageAttributes({
|
|
25
|
-
src: imageProps.src,
|
|
26
|
-
srcSet: imageProps.srcSet,
|
|
27
|
-
sizes: imageProps.sizes,
|
|
28
|
-
width: imageProps.width,
|
|
29
|
-
quality,
|
|
30
|
-
loader,
|
|
31
|
-
optimize,
|
|
32
|
-
}) ?? { src: imagePlaceholderSvg };
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<img
|
|
36
|
-
{...imageProps}
|
|
37
|
-
{...imageAttributes}
|
|
38
|
-
decoding={decoding}
|
|
39
|
-
loading={loading}
|
|
40
|
-
ref={ref}
|
|
41
|
-
/>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
Image.displayName = "Image";
|
|
47
|
-
|
|
48
|
-
const imagePlaceholderSvg = `data:image/svg+xml;base64,${btoa(`<svg
|
|
49
|
-
width="140"
|
|
50
|
-
height="140"
|
|
51
|
-
viewBox="0 0 600 600"
|
|
52
|
-
fill="none"
|
|
53
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
54
|
-
>
|
|
55
|
-
<rect width="600" height="600" fill="#CCCCCC" />
|
|
56
|
-
<path
|
|
57
|
-
fill-rule="evenodd"
|
|
58
|
-
clip-rule="evenodd"
|
|
59
|
-
d="M450 170H150C141.716 170 135 176.716 135 185V415C135 423.284 141.716 430 150 430H450C458.284 430 465 423.284 465 415V185C465 176.716 458.284 170 450 170ZM150 145C127.909 145 110 162.909 110 185V415C110 437.091 127.909 455 150 455H450C472.091 455 490 437.091 490 415V185C490 162.909 472.091 145 450 145H150Z"
|
|
60
|
-
fill="#A2A2A2"
|
|
61
|
-
/>
|
|
62
|
-
<path
|
|
63
|
-
d="M237.135 235.012C237.135 255.723 220.345 272.512 199.635 272.512C178.924 272.512 162.135 255.723 162.135 235.012C162.135 214.301 178.924 197.512 199.635 197.512C220.345 197.512 237.135 214.301 237.135 235.012Z"
|
|
64
|
-
fill="#A2A2A2"
|
|
65
|
-
/>
|
|
66
|
-
<path
|
|
67
|
-
d="M160 405V367.205L221.609 306.364L256.552 338.628L358.161 234L440 316.043V405H160Z"
|
|
68
|
-
fill="#A2A2A2"
|
|
69
|
-
/>
|
|
70
|
-
</svg>`)}`;
|
package/src/index.ts
DELETED