@penn-libraries/web 1.1.1-dev.1 → 1.2.0-dev.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/dist/cjs/{index-C0qvW4Ra.js → index-DJo51Q03.js} +58 -4
- package/dist/cjs/index-DJo51Q03.js.map +1 -0
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.cjs.js.map +1 -0
- package/dist/cjs/{pennlibs-autocomplete_3.cjs.entry.js → pennlibs-autocomplete_5.cjs.entry.js} +261 -2
- package/dist/cjs/pennlibs-banner.cjs.entry.js +1 -1
- package/dist/cjs/pennlibs-chat.cjs.entry.js +1 -1
- package/dist/cjs/pennlibs-feedback.cjs.entry.js +1 -1
- package/dist/cjs/pennlibs-hero.cjs.entry.js +2 -2
- package/dist/cjs/pennlibs-hero.entry.cjs.js.map +1 -1
- package/dist/cjs/web.cjs.js +2 -2
- package/dist/collection/assets/fonts/perpetua.woff +0 -0
- package/dist/collection/assets/fonts/perpetua.woff2 +0 -0
- package/dist/collection/components/pennlibs-hero/pennlibs-hero.css +1 -1
- package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.css +9 -2
- package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.js +147 -25
- package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.js.map +1 -1
- package/dist/components/pennlibs-hero.js +1 -1
- package/dist/components/pennlibs-hero.js.map +1 -1
- package/dist/components/pennlibs-iiif-img.js +144 -23
- package/dist/components/pennlibs-iiif-img.js.map +1 -1
- package/dist/docs.json +4 -4
- package/dist/{web/p-D9dYrmUF.js → esm/index-RqnbThKP.js} +58 -4
- package/dist/esm/index-RqnbThKP.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/loader.js +3 -3
- package/dist/esm/pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.js.map +1 -0
- package/dist/esm/{pennlibs-autocomplete_3.entry.js → pennlibs-autocomplete_5.entry.js} +260 -3
- package/dist/esm/pennlibs-banner.entry.js +1 -1
- package/dist/esm/pennlibs-chat.entry.js +1 -1
- package/dist/esm/pennlibs-feedback.entry.js +1 -1
- package/dist/esm/pennlibs-hero.entry.js +2 -2
- package/dist/esm/pennlibs-hero.entry.js.map +1 -1
- package/dist/esm/web.js +3 -3
- package/dist/types/components/pennlibs-iiif-img/pennlibs-iiif-img.d.ts +45 -4
- package/dist/types/components.d.ts +10 -10
- package/dist/web/assets/fonts/perpetua.woff +0 -0
- package/dist/web/assets/fonts/perpetua.woff2 +0 -0
- package/dist/web/index.esm.js +1 -1
- package/dist/web/{p-b4b58af0.entry.js → p-780e656e.entry.js} +1 -1
- package/dist/web/{p-43d9c2d4.entry.js → p-8ac5ef70.entry.js} +1 -1
- package/dist/{esm/index-D9dYrmUF.js → web/p-RqnbThKP.js} +58 -4
- package/dist/web/p-RqnbThKP.js.map +1 -0
- package/dist/web/{p-cb2584da.entry.js → p-b7b01d67.entry.js} +2 -2
- package/dist/web/{p-ad92090a.entry.js → p-ce09ae2e.entry.js} +1 -1
- package/dist/web/{p-e6188c30.entry.js → p-f37f3865.entry.js} +260 -3
- package/dist/web/pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.esm.js.map +1 -0
- package/dist/web/pennlibs-hero.entry.esm.js.map +1 -1
- package/dist/web/web.css +36 -15
- package/dist/web/web.esm.js +3 -3
- package/hydrate/index.js +190 -31
- package/hydrate/index.mjs +190 -31
- package/package.json +1 -1
- package/dist/cjs/index-C0qvW4Ra.js.map +0 -1
- package/dist/cjs/pennlibs-autocomplete.pennlibs-footer.pennlibs-header.entry.cjs.js.map +0 -1
- package/dist/cjs/pennlibs-fallback-img.cjs.entry.js +0 -20
- package/dist/cjs/pennlibs-fallback-img.entry.cjs.js.map +0 -1
- package/dist/cjs/pennlibs-iiif-img.cjs.entry.js +0 -132
- package/dist/cjs/pennlibs-iiif-img.entry.cjs.js.map +0 -1
- package/dist/esm/index-D9dYrmUF.js.map +0 -1
- package/dist/esm/pennlibs-autocomplete.pennlibs-footer.pennlibs-header.entry.js.map +0 -1
- package/dist/esm/pennlibs-fallback-img.entry.js +0 -18
- package/dist/esm/pennlibs-fallback-img.entry.js.map +0 -1
- package/dist/esm/pennlibs-iiif-img.entry.js +0 -130
- package/dist/esm/pennlibs-iiif-img.entry.js.map +0 -1
- package/dist/web/p-D9dYrmUF.js.map +0 -1
- package/dist/web/p-c4074cf1.entry.js +0 -130
- package/dist/web/p-ce97059c.entry.js +0 -18
- package/dist/web/pennlibs-autocomplete.pennlibs-footer.pennlibs-header.entry.esm.js.map +0 -1
- package/dist/web/pennlibs-fallback-img.entry.esm.js.map +0 -1
- package/dist/web/pennlibs-iiif-img.entry.esm.js.map +0 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { h } from "@stencil/core";
|
|
2
|
-
const
|
|
3
|
-
const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
|
|
2
|
+
const PIXEL_DENSITIES = [1, 1.5, 2];
|
|
4
3
|
/**
|
|
5
4
|
* Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.
|
|
6
|
-
*
|
|
7
|
-
* with modern WebP format and JPG fallback support.
|
|
5
|
+
* Measures its own rendered width and automatically generates optimal image sources at
|
|
6
|
+
* multiple pixel densities (1x, 1.5x, 2x) with modern WebP format and JPG fallback support.
|
|
8
7
|
*
|
|
9
8
|
* @component
|
|
10
9
|
* @example
|
|
@@ -23,6 +22,10 @@ export class IIIFImg {
|
|
|
23
22
|
*
|
|
24
23
|
* `square`: A square area where width and height equal the shorter dimension.
|
|
25
24
|
*
|
|
25
|
+
* `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies
|
|
26
|
+
* a centered crop based on the source image dimensions and sets the CSS aspect-ratio
|
|
27
|
+
* property for layout reservation.
|
|
28
|
+
*
|
|
26
29
|
* `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
|
|
27
30
|
*
|
|
28
31
|
* `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
|
|
@@ -77,26 +80,137 @@ export class IIIFImg {
|
|
|
77
80
|
this.hasError = true;
|
|
78
81
|
};
|
|
79
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetch IIIF image info to get actual dimensions.
|
|
85
|
+
*/
|
|
86
|
+
async fetchImageInfo() {
|
|
87
|
+
try {
|
|
88
|
+
const currentUuid = this.uuid; // Capture current UUID
|
|
89
|
+
const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;
|
|
90
|
+
const response = await fetch(infoUrl);
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const info = await response.json();
|
|
96
|
+
// Only update dimensions if UUID hasn't changed during fetch
|
|
97
|
+
// (protects against race conditions if component updates)
|
|
98
|
+
if (this.uuid === currentUuid) {
|
|
99
|
+
this.imageDimensions = {
|
|
100
|
+
width: info.width,
|
|
101
|
+
height: info.height,
|
|
102
|
+
};
|
|
103
|
+
this.fetchedUuid = currentUuid;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if a region value matches aspect ratio format (e.g., "16:9", "4:3", "3:2").
|
|
112
|
+
*/
|
|
113
|
+
isAspectRatio(value) {
|
|
114
|
+
return /^\d+:\d+$/.test(value);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Set CSS custom property for aspect ratio on the host element.
|
|
118
|
+
*/
|
|
119
|
+
setAspectRatioCss(aspectRatio) {
|
|
120
|
+
const [widthRatio, heightRatio] = aspectRatio.split(':');
|
|
121
|
+
this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Clear CSS custom property for aspect ratio from the host element.
|
|
125
|
+
*/
|
|
126
|
+
clearAspectRatioCss() {
|
|
127
|
+
this.hostElement.style.removeProperty('--aspect-ratio');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Format a number for IIIF percentage values (remove trailing zeros).
|
|
131
|
+
* IIIF spec requires no trailing zeros per section 4.7.
|
|
132
|
+
*/
|
|
133
|
+
formatPercent(value) {
|
|
134
|
+
return value.toFixed(2).replace(/\.?0+$/, '');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Calculate a centered crop region for a given aspect ratio.
|
|
138
|
+
* Requires actual image dimensions to calculate correctly.
|
|
139
|
+
* @param aspectRatio - The target aspect ratio (e.g., "3:2" or "2:3")
|
|
140
|
+
* @returns A IIIF percentage-based region string (e.g., "pct:0,16.67,100,66.67")
|
|
141
|
+
*/
|
|
142
|
+
calculateCenteredCrop(aspectRatio) {
|
|
143
|
+
if (!this.imageDimensions) {
|
|
144
|
+
throw new Error('Image dimensions required for aspect ratio calculation');
|
|
145
|
+
}
|
|
146
|
+
const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
|
|
147
|
+
const targetRatio = widthRatio / heightRatio;
|
|
148
|
+
const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;
|
|
149
|
+
// Determine if we need to crop width or height
|
|
150
|
+
if (sourceRatio > targetRatio) {
|
|
151
|
+
// Source is wider than target - crop left and right
|
|
152
|
+
const widthPercent = (targetRatio / sourceRatio) * 100;
|
|
153
|
+
const xOffset = (100 - widthPercent) / 2;
|
|
154
|
+
return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Source is taller than target - crop top and bottom
|
|
158
|
+
const heightPercent = (sourceRatio / targetRatio) * 100;
|
|
159
|
+
const yOffset = (100 - heightPercent) / 2;
|
|
160
|
+
return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the region parameter for the IIIF URL.
|
|
165
|
+
*/
|
|
166
|
+
getRegionParam() {
|
|
167
|
+
// Handle custom aspect ratios
|
|
168
|
+
let regionParam = this.region || 'full';
|
|
169
|
+
if (this.isAspectRatio(regionParam)) {
|
|
170
|
+
// Only apply crop if we have dimensions for the current UUID
|
|
171
|
+
if (this.imageDimensions && this.fetchedUuid === this.uuid) {
|
|
172
|
+
regionParam = this.calculateCenteredCrop(regionParam);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Use full image until dimensions are loaded
|
|
176
|
+
regionParam = 'full';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return regionParam;
|
|
180
|
+
}
|
|
80
181
|
buildIIIFUrl(width, format = 'jpg') {
|
|
81
182
|
const sizeParam = width ? `${width},` : 'max';
|
|
82
|
-
|
|
183
|
+
const regionParam = this.getRegionParam();
|
|
184
|
+
return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
|
|
83
185
|
}
|
|
84
|
-
generateSrcset(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const url = this.buildIIIFUrl(
|
|
88
|
-
return `${url} ${
|
|
89
|
-
});
|
|
90
|
-
return srcsetEntries.join(', ');
|
|
186
|
+
generateSrcset(baseWidth, format) {
|
|
187
|
+
return PIXEL_DENSITIES.map(density => {
|
|
188
|
+
const width = Math.round(baseWidth * density);
|
|
189
|
+
const url = this.buildIIIFUrl(width, format);
|
|
190
|
+
return `${url} ${density}x`;
|
|
191
|
+
}).join(', ');
|
|
91
192
|
}
|
|
92
|
-
|
|
93
|
-
if
|
|
94
|
-
|
|
95
|
-
return
|
|
193
|
+
handleRegionChange(newValue, oldValue) {
|
|
194
|
+
// Only process if region actually changed
|
|
195
|
+
if (newValue === oldValue) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// If new region is an aspect ratio, set CSS and fetch info
|
|
199
|
+
if (newValue && this.isAspectRatio(newValue)) {
|
|
200
|
+
this.setAspectRatioCss(newValue);
|
|
201
|
+
this.fetchImageInfo();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Clear aspect ratio CSS if no longer using aspect ratio format
|
|
205
|
+
this.clearAspectRatioCss();
|
|
96
206
|
}
|
|
97
|
-
return [...DEFAULT_IMAGE_SIZES];
|
|
98
207
|
}
|
|
99
208
|
componentDidLoad() {
|
|
209
|
+
// Set CSS aspect ratio and fetch image info if using aspect ratio format
|
|
210
|
+
if (this.region && this.isAspectRatio(this.region)) {
|
|
211
|
+
this.setAspectRatioCss(this.region);
|
|
212
|
+
this.fetchImageInfo();
|
|
213
|
+
}
|
|
100
214
|
if ('ResizeObserver' in window) {
|
|
101
215
|
this.resizeObserver = new ResizeObserver(entries => {
|
|
102
216
|
for (const entry of entries) {
|
|
@@ -105,8 +219,6 @@ export class IIIFImg {
|
|
|
105
219
|
this.resizeObserver.disconnect();
|
|
106
220
|
this.resizeObserver = undefined;
|
|
107
221
|
}
|
|
108
|
-
// Trigger re-render with optimized sizes
|
|
109
|
-
this.isLoaded = this.isLoaded;
|
|
110
222
|
}
|
|
111
223
|
});
|
|
112
224
|
this.resizeObserver.observe(this.hostElement);
|
|
@@ -119,7 +231,10 @@ export class IIIFImg {
|
|
|
119
231
|
}
|
|
120
232
|
}
|
|
121
233
|
render() {
|
|
122
|
-
|
|
234
|
+
// Show placeholder until we've measured the component width
|
|
235
|
+
if (!this.observedWidth) {
|
|
236
|
+
return h("pennlibs-fallback-img", null);
|
|
237
|
+
}
|
|
123
238
|
if (this.hasError && this.showFallback) {
|
|
124
239
|
return (h("div", { class: "fallback-container" }, h("pennlibs-fallback-img", null)));
|
|
125
240
|
}
|
|
@@ -127,8 +242,7 @@ export class IIIFImg {
|
|
|
127
242
|
'iiif-img': true,
|
|
128
243
|
'loaded': this.isLoaded
|
|
129
244
|
};
|
|
130
|
-
|
|
131
|
-
return (h("picture", null, h("source", { type: "image/webp", srcSet: this.generateSrcset(sizes, 'webp'), sizes: "100vw" }), h("img", { class: imgClasses, src: this.buildIIIFUrl(defaultSize, 'jpg'), srcSet: this.generateSrcset(sizes, 'jpg'), sizes: "100vw", alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
|
|
245
|
+
return (h("picture", null, h("source", { type: "image/webp", srcSet: this.generateSrcset(this.observedWidth, 'webp') }), h("img", { class: imgClasses, src: this.buildIIIFUrl(this.observedWidth, 'jpg'), srcSet: this.generateSrcset(this.observedWidth, 'jpg'), alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
|
|
132
246
|
}
|
|
133
247
|
static get is() { return "pennlibs-iiif-img"; }
|
|
134
248
|
static get encapsulation() { return "shadow"; }
|
|
@@ -197,11 +311,11 @@ export class IIIFImg {
|
|
|
197
311
|
"name": "default",
|
|
198
312
|
"text": "'full'"
|
|
199
313
|
}],
|
|
200
|
-
"text": "The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.\nDefines the rectangular portion of the underlying image to return.\n\n`full`: The full image is returned, without any cropping.\n\n`square`: A square area where width and height equal the shorter dimension.\n\n`x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).\n\n`pct:x,y,w,h`: Percentage-based coordinates of full image dimensions."
|
|
314
|
+
"text": "The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.\nDefines the rectangular portion of the underlying image to return.\n\n`full`: The full image is returned, without any cropping.\n\n`square`: A square area where width and height equal the shorter dimension.\n\n`width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies\na centered crop based on the source image dimensions and sets the CSS aspect-ratio\nproperty for layout reservation.\n\n`x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).\n\n`pct:x,y,w,h`: Percentage-based coordinates of full image dimensions."
|
|
201
315
|
},
|
|
202
316
|
"getter": false,
|
|
203
317
|
"setter": false,
|
|
204
|
-
"reflect":
|
|
318
|
+
"reflect": true,
|
|
205
319
|
"attribute": "region",
|
|
206
320
|
"defaultValue": "'full'"
|
|
207
321
|
},
|
|
@@ -302,9 +416,17 @@ export class IIIFImg {
|
|
|
302
416
|
static get states() {
|
|
303
417
|
return {
|
|
304
418
|
"isLoaded": {},
|
|
305
|
-
"hasError": {}
|
|
419
|
+
"hasError": {},
|
|
420
|
+
"imageDimensions": {},
|
|
421
|
+
"observedWidth": {}
|
|
306
422
|
};
|
|
307
423
|
}
|
|
308
424
|
static get elementRef() { return "hostElement"; }
|
|
425
|
+
static get watchers() {
|
|
426
|
+
return [{
|
|
427
|
+
"propName": "region",
|
|
428
|
+
"methodName": "handleRegionChange"
|
|
429
|
+
}];
|
|
430
|
+
}
|
|
309
431
|
}
|
|
310
432
|
//# sourceMappingURL=pennlibs-iiif-img.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pennlibs-iiif-img.js","sourceRoot":"","sources":["../../../src/components/pennlibs-iiif-img/pennlibs-iiif-img.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, h, Prop, State, Element } from '@stencil/core';\n\ntype ImageFormat = 'jpg' | 'webp';\n\nconst PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2] as const;\nconst DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400] as const;\n\n/**\n * Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.\n * Automatically generates optimal image sources for different viewport sizes and pixel densities,\n * with modern WebP format and JPG fallback support.\n *\n * @component\n * @example\n * <pennlibs-iiif-img\n * uuid=\"063ff35c-bbdd-4b63-bbdd-6206590e20d5\"\n * alt=\"\">\n * </pennlibs-iiif-img>\n */\n@Component({\n tag: 'pennlibs-iiif-img',\n styleUrl: 'pennlibs-iiif-img.css',\n shadow: true,\n})\nexport class IIIFImg {\n private resizeObserver?: ResizeObserver;\n\n @Element() hostElement!: HTMLElement;\n\n /**\n * The IIIF [UUID identifier](https://iiif.io/api/image/3.0/#3-identifier) of the image resource on the Penn Libraries IIIF server.\n */\n @Prop() uuid!: string;\n\n /**\n * Alternative text that describes the image content for screen readers and when images fail to load.\n * Use an empty string for purely decorative images.\n */\n @Prop() alt!: string;\n\n /**\n * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.\n * Defines the rectangular portion of the underlying image to return.\n * \n * `full`: The full image is returned, without any cropping.\n * \n * `square`: A square area where width and height equal the shorter dimension.\n * \n * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).\n * \n * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.\n * \n * @default 'full'\n */\n @Prop() region?: string = 'full';\n\n /**\n * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.\n * Specifies mirroring and clockwise rotation in degrees (0-360).\n * \n * `n`: Rotation in degrees only.\n * \n * `!n`: Mirror the image vertically, then rotate by n degrees.\n * \n * @default '0'\n */\n @Prop() rotation?: string = '0';\n\n /**\n * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.\n * Controls the color delivery mode.\n * \n * `default`: The server's default quality.\n * \n * `color`: Full color information.\n * \n * `gray`: Grayscale rendering.\n * \n * `bitonal`: Black and white only.\n * \n * @default 'default'\n */\n @Prop() quality?: string = 'default';\n\n /**\n * Native browser lazy loading behavior. Use \"lazy\" to defer loading until the image is near the viewport,\n * or \"eager\" to load immediately.\n * @default 'lazy'\n */\n @Prop() loading?: 'lazy' | 'eager' = 'lazy';\n\n /**\n * Whether to display a fallback placeholder image when the IIIF image fails to load.\n * @default true\n */\n @Prop() showFallback?: boolean = true;\n\n @State() isLoaded: boolean = false;\n @State() hasError: boolean = false;\n\n private observedWidth?: number;\n\n private readonly baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';\n\n private buildIIIFUrl(width?: number, format: ImageFormat = 'jpg'): string {\n const sizeParam = width ? `${width},` : 'max';\n return `${this.baseUrl}/${this.uuid}/${this.region}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;\n }\n\n private generateSrcset(sizes: number[], format: ImageFormat): string {\n const uniqueSizes = [...new Set(sizes)].sort((a, b) => a - b);\n\n const srcsetEntries = uniqueSizes.map(size => {\n const url = this.buildIIIFUrl(size, format);\n return `${url} ${size}w`;\n });\n\n return srcsetEntries.join(', ');\n }\n\n private getOptimalSizes(): number[] {\n if (this.observedWidth) {\n const baseSizes = PIXEL_DENSITY_MULTIPLIERS.map(multiplier =>\n Math.round(this.observedWidth! * multiplier)\n );\n\n return baseSizes;\n }\n\n return [...DEFAULT_IMAGE_SIZES];\n }\n\n private handleLoad = () => {\n this.isLoaded = true;\n };\n\n private handleError = () => {\n this.hasError = true;\n };\n\n componentDidLoad() {\n if ('ResizeObserver' in window) {\n this.resizeObserver = new ResizeObserver(entries => {\n for (const entry of entries) {\n this.observedWidth = Math.round(entry.contentRect.width);\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n\n // Trigger re-render with optimized sizes\n this.isLoaded = this.isLoaded;\n }\n });\n\n this.resizeObserver.observe(this.hostElement);\n }\n }\n\n disconnectedCallback() {\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n }\n\n render() {\n const sizes = this.getOptimalSizes();\n\n if (this.hasError && this.showFallback) {\n return (\n <div class=\"fallback-container\">\n <pennlibs-fallback-img />\n </div>\n );\n }\n\n const imgClasses = {\n 'iiif-img': true,\n 'loaded': this.isLoaded\n };\n\n const defaultSize = sizes[0];\n\n return (\n <picture>\n <source\n type=\"image/webp\"\n srcSet={this.generateSrcset(sizes, 'webp')}\n sizes=\"100vw\"\n />\n\n <img\n class={imgClasses}\n src={this.buildIIIFUrl(defaultSize, 'jpg')}\n srcSet={this.generateSrcset(sizes, 'jpg')}\n sizes=\"100vw\"\n alt={this.alt}\n loading={this.loading}\n onLoad={this.handleLoad}\n onError={this.handleError}\n />\n </picture>\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pennlibs-iiif-img.js","sourceRoot":"","sources":["../../../src/components/pennlibs-iiif-img/pennlibs-iiif-img.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, h, Prop, State, Element, Watch } from '@stencil/core';\n\ntype ImageFormat = 'jpg' | 'webp';\n\nconst PIXEL_DENSITIES = [1, 1.5, 2] as const;\n\n/**\n * Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.\n * Measures its own rendered width and automatically generates optimal image sources at\n * multiple pixel densities (1x, 1.5x, 2x) with modern WebP format and JPG fallback support.\n *\n * @component\n * @example\n * <pennlibs-iiif-img\n * uuid=\"063ff35c-bbdd-4b63-bbdd-6206590e20d5\"\n * alt=\"\">\n * </pennlibs-iiif-img>\n */\n@Component({\n tag: 'pennlibs-iiif-img',\n styleUrl: 'pennlibs-iiif-img.css',\n shadow: true,\n})\nexport class IIIFImg {\n private resizeObserver?: ResizeObserver;\n\n @Element() hostElement!: HTMLElement;\n\n /**\n * The IIIF [UUID identifier](https://iiif.io/api/image/3.0/#3-identifier) of the image resource on the Penn Libraries IIIF server.\n */\n @Prop() uuid!: string;\n\n /**\n * Alternative text that describes the image content for screen readers and when images fail to load.\n * Use an empty string for purely decorative images.\n */\n @Prop() alt!: string;\n\n /**\n * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.\n * Defines the rectangular portion of the underlying image to return.\n *\n * `full`: The full image is returned, without any cropping.\n *\n * `square`: A square area where width and height equal the shorter dimension.\n *\n * `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies\n * a centered crop based on the source image dimensions and sets the CSS aspect-ratio\n * property for layout reservation.\n *\n * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).\n *\n * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.\n *\n * @default 'full'\n */\n @Prop({ reflect: true }) region?: string = 'full';\n\n /**\n * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.\n * Specifies mirroring and clockwise rotation in degrees (0-360).\n * \n * `n`: Rotation in degrees only.\n * \n * `!n`: Mirror the image vertically, then rotate by n degrees.\n * \n * @default '0'\n */\n @Prop() rotation?: string = '0';\n\n /**\n * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.\n * Controls the color delivery mode.\n * \n * `default`: The server's default quality.\n * \n * `color`: Full color information.\n * \n * `gray`: Grayscale rendering.\n * \n * `bitonal`: Black and white only.\n * \n * @default 'default'\n */\n @Prop() quality?: string = 'default';\n\n /**\n * Native browser lazy loading behavior. Use \"lazy\" to defer loading until the image is near the viewport,\n * or \"eager\" to load immediately.\n * @default 'lazy'\n */\n @Prop() loading?: 'lazy' | 'eager' = 'lazy';\n\n /**\n * Whether to display a fallback placeholder image when the IIIF image fails to load.\n * @default true\n */\n @Prop() showFallback?: boolean = true;\n\n @State() isLoaded: boolean = false;\n @State() hasError: boolean = false;\n @State() imageDimensions?: { width: number; height: number };\n @State() observedWidth?: number;\n\n private fetchedUuid?: string; // Track which UUID these dimensions belong to\n\n private readonly baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';\n\n /**\n * Fetch IIIF image info to get actual dimensions.\n */\n private async fetchImageInfo(): Promise<void> {\n try {\n const currentUuid = this.uuid; // Capture current UUID\n const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;\n const response = await fetch(infoUrl);\n if (!response.ok) {\n console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);\n return;\n }\n const info = await response.json();\n\n // Only update dimensions if UUID hasn't changed during fetch\n // (protects against race conditions if component updates)\n if (this.uuid === currentUuid) {\n this.imageDimensions = {\n width: info.width,\n height: info.height,\n };\n this.fetchedUuid = currentUuid;\n }\n } catch (error) {\n console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);\n }\n }\n\n /**\n * Check if a region value matches aspect ratio format (e.g., \"16:9\", \"4:3\", \"3:2\").\n */\n private isAspectRatio(value: string): boolean {\n return /^\\d+:\\d+$/.test(value);\n }\n\n /**\n * Set CSS custom property for aspect ratio on the host element.\n */\n private setAspectRatioCss(aspectRatio: string): void {\n const [widthRatio, heightRatio] = aspectRatio.split(':');\n this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);\n }\n\n /**\n * Clear CSS custom property for aspect ratio from the host element.\n */\n private clearAspectRatioCss(): void {\n this.hostElement.style.removeProperty('--aspect-ratio');\n }\n\n /**\n * Format a number for IIIF percentage values (remove trailing zeros).\n * IIIF spec requires no trailing zeros per section 4.7.\n */\n private formatPercent(value: number): string {\n return value.toFixed(2).replace(/\\.?0+$/, '');\n }\n\n /**\n * Calculate a centered crop region for a given aspect ratio.\n * Requires actual image dimensions to calculate correctly.\n * @param aspectRatio - The target aspect ratio (e.g., \"3:2\" or \"2:3\")\n * @returns A IIIF percentage-based region string (e.g., \"pct:0,16.67,100,66.67\")\n */\n private calculateCenteredCrop(aspectRatio: string): string {\n if (!this.imageDimensions) {\n throw new Error('Image dimensions required for aspect ratio calculation');\n }\n\n const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);\n const targetRatio = widthRatio / heightRatio;\n const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;\n\n // Determine if we need to crop width or height\n if (sourceRatio > targetRatio) {\n // Source is wider than target - crop left and right\n const widthPercent = (targetRatio / sourceRatio) * 100;\n const xOffset = (100 - widthPercent) / 2;\n return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;\n } else {\n // Source is taller than target - crop top and bottom\n const heightPercent = (sourceRatio / targetRatio) * 100;\n const yOffset = (100 - heightPercent) / 2;\n return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;\n }\n }\n\n /**\n * Get the region parameter for the IIIF URL.\n */\n private getRegionParam(): string {\n // Handle custom aspect ratios\n let regionParam = this.region || 'full';\n if (this.isAspectRatio(regionParam)) {\n // Only apply crop if we have dimensions for the current UUID\n if (this.imageDimensions && this.fetchedUuid === this.uuid) {\n regionParam = this.calculateCenteredCrop(regionParam);\n } else {\n // Use full image until dimensions are loaded\n regionParam = 'full';\n }\n }\n\n return regionParam;\n }\n\n private buildIIIFUrl(width?: number, format: ImageFormat = 'jpg'): string {\n const sizeParam = width ? `${width},` : 'max';\n const regionParam = this.getRegionParam();\n return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;\n }\n\n private generateSrcset(baseWidth: number, format: ImageFormat): string {\n return PIXEL_DENSITIES.map(density => {\n const width = Math.round(baseWidth * density);\n const url = this.buildIIIFUrl(width, format);\n return `${url} ${density}x`;\n }).join(', ');\n }\n\n\n private handleLoad = () => {\n this.isLoaded = true;\n };\n\n private handleError = () => {\n this.hasError = true;\n };\n\n @Watch('region')\n handleRegionChange(newValue: string, oldValue: string) {\n // Only process if region actually changed\n if (newValue === oldValue) {\n return;\n }\n\n // If new region is an aspect ratio, set CSS and fetch info\n if (newValue && this.isAspectRatio(newValue)) {\n this.setAspectRatioCss(newValue);\n this.fetchImageInfo();\n } else {\n // Clear aspect ratio CSS if no longer using aspect ratio format\n this.clearAspectRatioCss();\n }\n }\n\n componentDidLoad() {\n // Set CSS aspect ratio and fetch image info if using aspect ratio format\n if (this.region && this.isAspectRatio(this.region)) {\n this.setAspectRatioCss(this.region);\n this.fetchImageInfo();\n }\n\n if ('ResizeObserver' in window) {\n this.resizeObserver = new ResizeObserver(entries => {\n for (const entry of entries) {\n this.observedWidth = Math.round(entry.contentRect.width);\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n }\n });\n\n this.resizeObserver.observe(this.hostElement);\n }\n }\n\n disconnectedCallback() {\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n }\n\n render() {\n // Show placeholder until we've measured the component width\n if (!this.observedWidth) {\n return <pennlibs-fallback-img />;\n }\n\n if (this.hasError && this.showFallback) {\n return (\n <div class=\"fallback-container\">\n <pennlibs-fallback-img />\n </div>\n );\n }\n\n const imgClasses = {\n 'iiif-img': true,\n 'loaded': this.isLoaded\n };\n\n return (\n <picture>\n <source\n type=\"image/webp\"\n srcSet={this.generateSrcset(this.observedWidth, 'webp')}\n />\n\n <img\n class={imgClasses}\n src={this.buildIIIFUrl(this.observedWidth, 'jpg')}\n srcSet={this.generateSrcset(this.observedWidth, 'jpg')}\n alt={this.alt}\n loading={this.loading}\n onLoad={this.handleLoad}\n onError={this.handleError}\n />\n </picture>\n );\n }\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { proxyCustomElement, HTMLElement, h } from '@stencil/core/internal/client';
|
|
2
2
|
|
|
3
|
-
const pennlibsHeroCss = ":host{--pl-hero-height:clamp(42vh, 32rem, 26rem);--pl-hero-heading-font:var(--pl-font-serif);--pl-hero-color:var(--pl-color-fg-on-emphasis)}*,*:before,*:after{box-sizing:inherit}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.viewport-margins{width:100%;max-width:var(--pl-viewport-margins-max-width);margin:0 auto;padding:0 var(--pl-viewport-margins-gutter, 1em)}.hero{position:relative;min-height:var(--pl-hero-height);height:100%;background-size:cover;background-repeat:no-repeat;background-position:50% 33%;display:flex}.hero::before{content:\"\";display:flex;width:100%;height:100%;top:0;position:absolute;background:linear-gradient(360deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 20%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.05) 100%);z-index:0}.hero::after{content:\"\";display:flex;width:100%;height:100%;top:0;position:absolute;background:linear-gradient(180deg, rgba(1, 31, 91, 1) 0%, rgba(1, 31, 91, 0.9) 10%, rgba(1, 31, 91, 0.8) 20%, rgba(1, 31, 91, 0.1) 50%, rgba(1, 31, 91, 0.0) 100%);z-index:0}.hero__content{position:relative;display:flex;flex-direction:column;width:100%;z-index:1}.hero__heading-container{margin-top:auto;padding-top:var(--pl-space-3xl);padding-bottom:var(--pl-space-3xl)}.hero__heading{text-shadow:1px 1px 2px var(--pl-color-fg-default);line-height:1.1;font-size:3em;font-weight:
|
|
3
|
+
const pennlibsHeroCss = ":host{--pl-hero-height:clamp(42vh, 32rem, 26rem);--pl-hero-heading-font:var(--pl-font-serif);--pl-hero-color:var(--pl-color-fg-on-emphasis)}*,*:before,*:after{box-sizing:inherit}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.viewport-margins{width:100%;max-width:var(--pl-viewport-margins-max-width);margin:0 auto;padding:0 var(--pl-viewport-margins-gutter, 1em)}.hero{position:relative;min-height:var(--pl-hero-height);height:100%;background-size:cover;background-repeat:no-repeat;background-position:50% 33%;display:flex}.hero::before{content:\"\";display:flex;width:100%;height:100%;top:0;position:absolute;background:linear-gradient(360deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 20%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.05) 100%);z-index:0}.hero::after{content:\"\";display:flex;width:100%;height:100%;top:0;position:absolute;background:linear-gradient(180deg, rgba(1, 31, 91, 1) 0%, rgba(1, 31, 91, 0.9) 10%, rgba(1, 31, 91, 0.8) 20%, rgba(1, 31, 91, 0.1) 50%, rgba(1, 31, 91, 0.0) 100%);z-index:0}.hero__content{position:relative;display:flex;flex-direction:column;width:100%;z-index:1}.hero__heading-container{margin-top:auto;padding-top:var(--pl-space-3xl);padding-bottom:var(--pl-space-3xl)}.hero__heading{text-shadow:1px 1px 2px var(--pl-color-fg-default);line-height:1.1;font-size:3em;font-weight:400;font-family:var(--pl-hero-heading-font);text-wrap:pretty;max-width:30ch;margin:0;color:var(--pl-hero-color)}@media (max-width: 920px){.hero__heading{font-size:2.5em}}.hero__sub-heading{font-size:1.25em;font-family:var(--pl-font-family);font-weight:500;color:var(--pl-hero-color);max-width:52ch;text-wrap:pretty;margin-top:1em;margin-bottom:0}.hero__sub-heading a{text-decoration:underline;text-underline-offset:var(--pl-link-text-underline-offset);text-decoration-thickness:var(--pl-link-text-decoration-thickness);color:var(--pl-hero-color)}.hero__sub-heading a:hover{text-decoration-thickness:var(--pl-link-hover-text-decoration-thickness)}.hero__sub-heading strong{font-weight:bold}@media (max-width: 620px){.hero__heading{font-size:2.75em}.hero__sub-heading{font-size:1em}}";
|
|
4
4
|
|
|
5
5
|
const getCurrentImageSource = (pictureElement) => {
|
|
6
6
|
const imgElement = pictureElement.querySelector('img');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"file":"pennlibs-hero.js","mappings":";;AAAA,MAAM,eAAe,GAAG,
|
|
1
|
+
{"file":"pennlibs-hero.js","mappings":"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","names":["__stencil_proxyCustomElement"],"sources":["src/components/pennlibs-hero/pennlibs-hero.css?tag=pennlibs-hero&encapsulation=shadow","src/components/pennlibs-hero/pennlibs-hero.tsx"],"sourcesContent":[":host {\n --pl-hero-height: clamp(42vh, 32rem, 26rem);\n --pl-hero-heading-font: var(--pl-font-serif);\n --pl-hero-color: var(--pl-color-fg-on-emphasis);\n}\n\n*, *:before, *:after {\n box-sizing: inherit;\n}\n\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.viewport-margins {\n width: 100%;\n max-width: var(--pl-viewport-margins-max-width);\n margin: 0 auto;\n padding: 0 var(--pl-viewport-margins-gutter, 1em);\n}\n\n/* Start of Selection */\n/* Hero section */\n.hero {\n position: relative;\n min-height: var(--pl-hero-height);\n height: 100%;\n background-size: cover;\n background-repeat: no-repeat;\n background-position: 50% 33%;\n display: flex;\n}\n/* End of Selection */\n\n.hero::before {\n content: \"\";\n display: flex;\n width: 100%;\n height: 100%;\n top: 0;\n position: absolute;\n background: linear-gradient(360deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 20%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.05) 100%);\n z-index: 0;\n}\n\n \n.hero::after {\n content: \"\";\n display: flex;\n width: 100%;\n height: 100%;\n top: 0;\n position: absolute;\n background: linear-gradient(180deg, rgba(1, 31, 91, 1) 0%, rgba(1, 31, 91, 0.9) 10%, rgba(1, 31, 91, 0.8) 20%, rgba(1, 31, 91, 0.1) 50%, rgba(1, 31, 91, 0.0) 100%);\n z-index: 0;\n}\n\n.hero__content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n z-index: 1;\n}\n\n.hero__heading-container {\n margin-top: auto;\n padding-top: var(--pl-space-3xl);\n padding-bottom: var(--pl-space-3xl);\n}\n\n.hero__heading {\n text-shadow: 1px 1px 2px var(--pl-color-fg-default);\n line-height: 1.1;\n font-size: 3em;\n font-weight: 400;\n font-family: var(--pl-hero-heading-font);\n text-wrap: pretty;\n max-width: 30ch;\n margin: 0;\n color: var(--pl-hero-color);\n}\n\n@media (max-width: 920px) {\n .hero__heading {\n font-size: 2.5em;\n }\n}\n\n.hero__sub-heading {\n font-size: 1.25em;\n font-family: var(--pl-font-family);\n font-weight: 500;\n color: var(--pl-hero-color);\n max-width: 52ch;\n text-wrap: pretty;\n margin-top: 1em;\n margin-bottom: 0;\n}\n\n.hero__sub-heading a {\n text-decoration: underline;\n text-underline-offset: var(--pl-link-text-underline-offset);\n text-decoration-thickness: var(--pl-link-text-decoration-thickness);\n color: var(--pl-hero-color);\n}\n\n.hero__sub-heading a:hover {\n text-decoration-thickness: var(--pl-link-hover-text-decoration-thickness);\n}\n\n.hero__sub-heading strong {\n font-weight: bold;\n}\n\n@media (max-width: 620px) {\n .hero__heading {\n font-size: 2.75em;\n }\n\n .hero__sub-heading {\n font-size: 1em;\n }\n}","import { h, Component, State, Element } from \"@stencil/core\";\n\nconst getCurrentImageSource = (pictureElement: HTMLPictureElement): string => {\n const imgElement = pictureElement.querySelector('img');\n return imgElement?.currentSrc || '';\n};\n\n/**\n * Place your most important content in a prominent space, often at the top of your website.\n *\n * @slot start - Content to display at the start (top) of the hero.\n * \n * @prop --pl-viewport-margins-max-width: The maximum width of the hero inner content.\n * @prop --pl-viewport-margins-gutter: The gutter width of the hero inner content.\n */\n@Component({\n tag: 'pennlibs-hero',\n styleUrl: 'pennlibs-hero.css',\n shadow: true,\n assetsDirs: ['assets']\n})\nexport class Hero {\n @Element() hostElement: HTMLElement;\n @State() heroPictureElement: null | HTMLPictureElement = null;\n @State() heroHeadingElement: null | HTMLHeadingElement = null;\n @State() heroParagraphElement: null | HTMLParagraphElement = null;\n @State() heroSrc: string = \"\";\n private lastSrc: string = \"\";\n private animationFrameId: number;\n\n componentWillLoad() {\n const heroPictureElement = this.hostElement.querySelector('picture[hero=art-direction]') as HTMLPictureElement;\n if (heroPictureElement) {\n this.heroPictureElement = heroPictureElement;\n this.startWatchingCurrentSrc(heroPictureElement);\n }\n\n const heroHeadingElement = this.hostElement.querySelector('h1[hero=heading]') as HTMLHeadingElement;\n if (heroHeadingElement) {\n this.heroHeadingElement = heroHeadingElement;\n }\n\n const heroParagraphElement = this.hostElement.querySelector('p[hero=sub-heading]') as HTMLParagraphElement;\n if (heroParagraphElement) {\n this.heroParagraphElement = heroParagraphElement;\n }\n }\n\n private startWatchingCurrentSrc(pictureElement: HTMLPictureElement) {\n const checkCurrentSrc = () => {\n const currentSrc = getCurrentImageSource(pictureElement);\n if (currentSrc !== this.lastSrc) {\n this.lastSrc = currentSrc;\n this.heroSrc = currentSrc;\n }\n this.animationFrameId = requestAnimationFrame(checkCurrentSrc);\n };\n \n this.animationFrameId = requestAnimationFrame(checkCurrentSrc);\n }\n\n disconnectedCallback() {\n if (this.animationFrameId) {\n cancelAnimationFrame(this.animationFrameId);\n }\n }\n\n render() {\n return (\n <div class=\"hero\" style={{ backgroundImage: `url(${this.heroSrc})` }}>\n <div class=\"hero__content\">\n <slot name=\"start\" />\n <div class=\"hero__heading-container\">\n {this.heroHeadingElement && (\n <div class=\"viewport-margins\">\n <h1 class=\"hero__heading\" innerHTML={this.heroHeadingElement.innerText} />\n {this.heroParagraphElement && <p class=\"hero__sub-heading\" innerHTML={this.heroParagraphElement.innerHTML} />}\n </div>\n )}\n </div>\n </div>\n </div>\n )\n }\n}"],"version":3}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { proxyCustomElement, HTMLElement, h } from '@stencil/core/internal/client';
|
|
2
2
|
import { d as defineCustomElement$2 } from './pennlibs-fallback-img2.js';
|
|
3
3
|
|
|
4
|
-
const pennlibsIiifImgCss = ":host{display:block;width:100%;max-width:100
|
|
4
|
+
const pennlibsIiifImgCss = ":host{display:block;width:100%;max-width:100%;align-self:flex-start;aspect-ratio:var(--aspect-ratio, auto)}picture{display:block;line-height:0}.iiif-img{display:block;width:100%;max-width:100%;height:auto;opacity:0;filter:blur(5px);transition:opacity 0.15s ease-in, filter 0.15s ease-in}.iiif-img.loaded{opacity:1;filter:blur(0)}.fallback-container{display:block;width:100%;height:100%}.fallback-container pennlibs-fallback-img{width:100%;height:100%}pennlibs-fallback-img{display:block;width:100%;height:100%}";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
|
|
6
|
+
const PIXEL_DENSITIES = [1, 1.5, 2];
|
|
8
7
|
const IIIFImg = /*@__PURE__*/ proxyCustomElement(class IIIFImg extends HTMLElement {
|
|
9
8
|
constructor(registerHost) {
|
|
10
9
|
super();
|
|
@@ -20,6 +19,10 @@ const IIIFImg = /*@__PURE__*/ proxyCustomElement(class IIIFImg extends HTMLEleme
|
|
|
20
19
|
*
|
|
21
20
|
* `square`: A square area where width and height equal the shorter dimension.
|
|
22
21
|
*
|
|
22
|
+
* `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies
|
|
23
|
+
* a centered crop based on the source image dimensions and sets the CSS aspect-ratio
|
|
24
|
+
* property for layout reservation.
|
|
25
|
+
*
|
|
23
26
|
* `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
|
|
24
27
|
*
|
|
25
28
|
* `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
|
|
@@ -74,26 +77,137 @@ const IIIFImg = /*@__PURE__*/ proxyCustomElement(class IIIFImg extends HTMLEleme
|
|
|
74
77
|
this.hasError = true;
|
|
75
78
|
};
|
|
76
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Fetch IIIF image info to get actual dimensions.
|
|
82
|
+
*/
|
|
83
|
+
async fetchImageInfo() {
|
|
84
|
+
try {
|
|
85
|
+
const currentUuid = this.uuid; // Capture current UUID
|
|
86
|
+
const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;
|
|
87
|
+
const response = await fetch(infoUrl);
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const info = await response.json();
|
|
93
|
+
// Only update dimensions if UUID hasn't changed during fetch
|
|
94
|
+
// (protects against race conditions if component updates)
|
|
95
|
+
if (this.uuid === currentUuid) {
|
|
96
|
+
this.imageDimensions = {
|
|
97
|
+
width: info.width,
|
|
98
|
+
height: info.height,
|
|
99
|
+
};
|
|
100
|
+
this.fetchedUuid = currentUuid;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if a region value matches aspect ratio format (e.g., "16:9", "4:3", "3:2").
|
|
109
|
+
*/
|
|
110
|
+
isAspectRatio(value) {
|
|
111
|
+
return /^\d+:\d+$/.test(value);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Set CSS custom property for aspect ratio on the host element.
|
|
115
|
+
*/
|
|
116
|
+
setAspectRatioCss(aspectRatio) {
|
|
117
|
+
const [widthRatio, heightRatio] = aspectRatio.split(':');
|
|
118
|
+
this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clear CSS custom property for aspect ratio from the host element.
|
|
122
|
+
*/
|
|
123
|
+
clearAspectRatioCss() {
|
|
124
|
+
this.hostElement.style.removeProperty('--aspect-ratio');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Format a number for IIIF percentage values (remove trailing zeros).
|
|
128
|
+
* IIIF spec requires no trailing zeros per section 4.7.
|
|
129
|
+
*/
|
|
130
|
+
formatPercent(value) {
|
|
131
|
+
return value.toFixed(2).replace(/\.?0+$/, '');
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Calculate a centered crop region for a given aspect ratio.
|
|
135
|
+
* Requires actual image dimensions to calculate correctly.
|
|
136
|
+
* @param aspectRatio - The target aspect ratio (e.g., "3:2" or "2:3")
|
|
137
|
+
* @returns A IIIF percentage-based region string (e.g., "pct:0,16.67,100,66.67")
|
|
138
|
+
*/
|
|
139
|
+
calculateCenteredCrop(aspectRatio) {
|
|
140
|
+
if (!this.imageDimensions) {
|
|
141
|
+
throw new Error('Image dimensions required for aspect ratio calculation');
|
|
142
|
+
}
|
|
143
|
+
const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
|
|
144
|
+
const targetRatio = widthRatio / heightRatio;
|
|
145
|
+
const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;
|
|
146
|
+
// Determine if we need to crop width or height
|
|
147
|
+
if (sourceRatio > targetRatio) {
|
|
148
|
+
// Source is wider than target - crop left and right
|
|
149
|
+
const widthPercent = (targetRatio / sourceRatio) * 100;
|
|
150
|
+
const xOffset = (100 - widthPercent) / 2;
|
|
151
|
+
return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Source is taller than target - crop top and bottom
|
|
155
|
+
const heightPercent = (sourceRatio / targetRatio) * 100;
|
|
156
|
+
const yOffset = (100 - heightPercent) / 2;
|
|
157
|
+
return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get the region parameter for the IIIF URL.
|
|
162
|
+
*/
|
|
163
|
+
getRegionParam() {
|
|
164
|
+
// Handle custom aspect ratios
|
|
165
|
+
let regionParam = this.region || 'full';
|
|
166
|
+
if (this.isAspectRatio(regionParam)) {
|
|
167
|
+
// Only apply crop if we have dimensions for the current UUID
|
|
168
|
+
if (this.imageDimensions && this.fetchedUuid === this.uuid) {
|
|
169
|
+
regionParam = this.calculateCenteredCrop(regionParam);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Use full image until dimensions are loaded
|
|
173
|
+
regionParam = 'full';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return regionParam;
|
|
177
|
+
}
|
|
77
178
|
buildIIIFUrl(width, format = 'jpg') {
|
|
78
179
|
const sizeParam = width ? `${width},` : 'max';
|
|
79
|
-
|
|
180
|
+
const regionParam = this.getRegionParam();
|
|
181
|
+
return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
|
|
80
182
|
}
|
|
81
|
-
generateSrcset(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const url = this.buildIIIFUrl(
|
|
85
|
-
return `${url} ${
|
|
86
|
-
});
|
|
87
|
-
return srcsetEntries.join(', ');
|
|
183
|
+
generateSrcset(baseWidth, format) {
|
|
184
|
+
return PIXEL_DENSITIES.map(density => {
|
|
185
|
+
const width = Math.round(baseWidth * density);
|
|
186
|
+
const url = this.buildIIIFUrl(width, format);
|
|
187
|
+
return `${url} ${density}x`;
|
|
188
|
+
}).join(', ');
|
|
88
189
|
}
|
|
89
|
-
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
return
|
|
190
|
+
handleRegionChange(newValue, oldValue) {
|
|
191
|
+
// Only process if region actually changed
|
|
192
|
+
if (newValue === oldValue) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// If new region is an aspect ratio, set CSS and fetch info
|
|
196
|
+
if (newValue && this.isAspectRatio(newValue)) {
|
|
197
|
+
this.setAspectRatioCss(newValue);
|
|
198
|
+
this.fetchImageInfo();
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Clear aspect ratio CSS if no longer using aspect ratio format
|
|
202
|
+
this.clearAspectRatioCss();
|
|
93
203
|
}
|
|
94
|
-
return [...DEFAULT_IMAGE_SIZES];
|
|
95
204
|
}
|
|
96
205
|
componentDidLoad() {
|
|
206
|
+
// Set CSS aspect ratio and fetch image info if using aspect ratio format
|
|
207
|
+
if (this.region && this.isAspectRatio(this.region)) {
|
|
208
|
+
this.setAspectRatioCss(this.region);
|
|
209
|
+
this.fetchImageInfo();
|
|
210
|
+
}
|
|
97
211
|
if ('ResizeObserver' in window) {
|
|
98
212
|
this.resizeObserver = new ResizeObserver(entries => {
|
|
99
213
|
for (const entry of entries) {
|
|
@@ -102,8 +216,6 @@ const IIIFImg = /*@__PURE__*/ proxyCustomElement(class IIIFImg extends HTMLEleme
|
|
|
102
216
|
this.resizeObserver.disconnect();
|
|
103
217
|
this.resizeObserver = undefined;
|
|
104
218
|
}
|
|
105
|
-
// Trigger re-render with optimized sizes
|
|
106
|
-
this.isLoaded = this.isLoaded;
|
|
107
219
|
}
|
|
108
220
|
});
|
|
109
221
|
this.resizeObserver.observe(this.hostElement);
|
|
@@ -116,7 +228,10 @@ const IIIFImg = /*@__PURE__*/ proxyCustomElement(class IIIFImg extends HTMLEleme
|
|
|
116
228
|
}
|
|
117
229
|
}
|
|
118
230
|
render() {
|
|
119
|
-
|
|
231
|
+
// Show placeholder until we've measured the component width
|
|
232
|
+
if (!this.observedWidth) {
|
|
233
|
+
return h("pennlibs-fallback-img", null);
|
|
234
|
+
}
|
|
120
235
|
if (this.hasError && this.showFallback) {
|
|
121
236
|
return (h("div", { class: "fallback-container" }, h("pennlibs-fallback-img", null)));
|
|
122
237
|
}
|
|
@@ -124,21 +239,27 @@ const IIIFImg = /*@__PURE__*/ proxyCustomElement(class IIIFImg extends HTMLEleme
|
|
|
124
239
|
'iiif-img': true,
|
|
125
240
|
'loaded': this.isLoaded
|
|
126
241
|
};
|
|
127
|
-
|
|
128
|
-
return (h("picture", null, h("source", { type: "image/webp", srcSet: this.generateSrcset(sizes, 'webp'), sizes: "100vw" }), h("img", { class: imgClasses, src: this.buildIIIFUrl(defaultSize, 'jpg'), srcSet: this.generateSrcset(sizes, 'jpg'), sizes: "100vw", alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
|
|
242
|
+
return (h("picture", null, h("source", { type: "image/webp", srcSet: this.generateSrcset(this.observedWidth, 'webp') }), h("img", { class: imgClasses, src: this.buildIIIFUrl(this.observedWidth, 'jpg'), srcSet: this.generateSrcset(this.observedWidth, 'jpg'), alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
|
|
129
243
|
}
|
|
130
244
|
get hostElement() { return this; }
|
|
245
|
+
static get watchers() { return {
|
|
246
|
+
"region": ["handleRegionChange"]
|
|
247
|
+
}; }
|
|
131
248
|
static get style() { return pennlibsIiifImgCss; }
|
|
132
249
|
}, [257, "pennlibs-iiif-img", {
|
|
133
250
|
"uuid": [1],
|
|
134
251
|
"alt": [1],
|
|
135
|
-
"region": [
|
|
252
|
+
"region": [513],
|
|
136
253
|
"rotation": [1],
|
|
137
254
|
"quality": [1],
|
|
138
255
|
"loading": [1],
|
|
139
256
|
"showFallback": [4, "show-fallback"],
|
|
140
257
|
"isLoaded": [32],
|
|
141
|
-
"hasError": [32]
|
|
258
|
+
"hasError": [32],
|
|
259
|
+
"imageDimensions": [32],
|
|
260
|
+
"observedWidth": [32]
|
|
261
|
+
}, undefined, {
|
|
262
|
+
"region": ["handleRegionChange"]
|
|
142
263
|
}]);
|
|
143
264
|
function defineCustomElement$1() {
|
|
144
265
|
if (typeof customElements === "undefined") {
|