@projektemacher/storymapjs 0.10.8 → 0.11.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/CHANGELOG +81 -0
- package/README.md +129 -12
- package/dist/assets/{demo-DBpeF31K.js → demo-bWnOyfUw.js} +1 -1
- package/dist/assets/harness-C3JUv5Ii.js +1 -0
- package/dist/assets/{harness-multi-CYHgI0a9.js → harness-multi-XZNex3tb.js} +1 -1
- package/dist/assets/{harness-support-Do7sIp2j.js → harness-support-CjfZIVeb.js} +1 -1
- package/dist/assets/main-DQ32hWBM.js +17 -0
- package/dist/context.json +13 -2
- package/dist/css/storymap.css +1 -1
- package/dist/demo.html +3 -2
- package/dist/docs/api/assets/navigation.js +1 -1
- package/dist/docs/api/assets/search.js +1 -1
- package/dist/docs/api/classes/StoryMap.html +777 -269
- package/dist/docs/api/functions/MediaType.html +1 -1
- package/dist/docs/api/functions/isPresentation3Collection.html +1 -1
- package/dist/docs/api/functions/isPresentation3Manifest.html +1 -1
- package/dist/docs/api/functions/isSlideshowCollection.html +51 -0
- package/dist/docs/api/functions/loadCSS.html +1 -1
- package/dist/docs/api/functions/manifestToStorymapData.html +1 -1
- package/dist/docs/api/functions/setLanguage.html +1 -1
- package/dist/docs/api/functions/slideshowToStorymapData.html +51 -0
- package/dist/docs/api/functions/slideshowVersion.html +48 -0
- package/dist/docs/api/functions/storymapToManifest.html +1 -1
- package/dist/docs/api/functions/validateStorymap.html +1 -1
- package/dist/docs/api/functions/validateStorymapAndReport.html +1 -1
- package/dist/docs/api/index.html +115 -12
- package/dist/docs/api/interfaces/AnimationHandle.html +2 -2
- package/dist/docs/api/interfaces/ImagereadyPayload.html +4 -4
- package/dist/docs/api/interfaces/LanguageEntry.html +6 -6
- package/dist/docs/api/interfaces/LatLngLiteral.html +4 -4
- package/dist/docs/api/interfaces/LoadOptions.html +2 -2
- package/dist/docs/api/interfaces/MapEvents.html +116 -0
- package/dist/docs/api/interfaces/MapMarkerEvents.html +66 -0
- package/dist/docs/api/interfaces/MarkerEventPayload.html +57 -0
- package/dist/docs/api/interfaces/MediaEvents.html +78 -0
- package/dist/docs/api/interfaces/MenuBarEvents.html +114 -0
- package/dist/docs/api/interfaces/MessageEvents.html +72 -0
- package/dist/docs/api/interfaces/OlLayer.html +1 -1
- package/dist/docs/api/interfaces/OlMap.html +1 -1
- package/dist/docs/api/interfaces/OlSource.html +1 -1
- package/dist/docs/api/interfaces/OlTileLayer.html +1 -1
- package/dist/docs/api/interfaces/OlVectorLayer.html +1 -1
- package/dist/docs/api/interfaces/OlView.html +1 -1
- package/dist/docs/api/interfaces/SlideBackgroundState.html +66 -0
- package/dist/docs/api/interfaces/SlideEvents.html +78 -0
- package/dist/docs/api/interfaces/SlideNavEvents.html +72 -0
- package/dist/docs/api/interfaces/SlideshowPlayerSettings.html +182 -0
- package/dist/docs/api/interfaces/SlideshowToStorymapResult.html +60 -0
- package/dist/docs/api/interfaces/SlideshowTranslatorOptions.html +65 -0
- package/dist/docs/api/interfaces/SlideshowWarning.html +72 -0
- package/dist/docs/api/interfaces/StoryMapEvents.html +146 -0
- package/dist/docs/api/interfaces/StoryMapMarker.html +763 -0
- package/dist/docs/api/interfaces/StorySliderEvents.html +102 -0
- package/dist/docs/api/interfaces/StorymapBasemap.html +69 -0
- package/dist/docs/api/interfaces/StorymapData.html +12 -6
- package/dist/docs/api/interfaces/StorymapDataWrapper.html +2 -2
- package/dist/docs/api/interfaces/StorymapDocument.html +2 -2
- package/dist/docs/api/interfaces/StorymapError.html +3 -3
- package/dist/docs/api/interfaces/StorymapGeoreference.html +5 -5
- package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +4 -4
- package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +4 -4
- package/dist/docs/api/interfaces/StorymapLanguageMap.html +2 -2
- package/dist/docs/api/interfaces/StorymapManifest.html +12 -12
- package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +43 -11
- package/dist/docs/api/interfaces/StorymapManifestBackground.html +6 -6
- package/dist/docs/api/interfaces/StorymapManifestCanvas.html +65 -17
- package/dist/docs/api/interfaces/StorymapManifestContentResource.html +8 -8
- package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +7 -7
- package/dist/docs/api/interfaces/StorymapManifestImageService.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestRange.html +5 -5
- package/dist/docs/api/interfaces/StorymapManifestService.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestStatement.html +3 -3
- package/dist/docs/api/interfaces/StorymapMapOptions.html +5 -5
- package/dist/docs/api/interfaces/StorymapOptions.html +277 -75
- package/dist/docs/api/interfaces/StorymapOverlayLayer.html +42 -11
- package/dist/docs/api/interfaces/StorymapSlide.html +70 -16
- package/dist/docs/api/interfaces/StorymapSlideBackground.html +4 -4
- package/dist/docs/api/interfaces/StorymapSlideLocation.html +58 -14
- package/dist/docs/api/interfaces/StorymapSlideMarker.html +8 -9
- package/dist/docs/api/interfaces/StorymapSlideMedia.html +49 -10
- package/dist/docs/api/interfaces/StorymapSlideNarration.html +61 -9
- package/dist/docs/api/interfaces/StorymapSlideText.html +4 -4
- package/dist/docs/api/interfaces/StorymapTilejson.html +8 -8
- package/dist/docs/api/interfaces/SwipableEvents.html +102 -0
- package/dist/docs/api/interfaces/TextEvents.html +66 -0
- package/dist/docs/api/media/events.md +66 -0
- package/dist/docs/api/media/migration-from-knightlab.md +89 -49
- package/dist/docs/api/media/slideshow.md +109 -0
- package/dist/docs/api/media/storymap-as-iiif-manifest.md +1007 -0
- package/dist/docs/api/modules.html +6 -6
- package/dist/docs/api/types/ContentState.html +3 -3
- package/dist/docs/api/types/FiredEvent.html +43 -0
- package/dist/docs/api/types/ImagereadyKind.html +1 -1
- package/dist/docs/api/types/MediaTypeMatch.html +5 -5
- package/dist/docs/api/types/SwipeDirection.html +36 -0
- package/dist/docs/api/types/TileSourceFactory.html +1 -1
- package/dist/docs/authoring.html +20 -7
- package/dist/docs/events.html +263 -0
- package/dist/docs/iiif.html +136 -22
- package/dist/docs/migration.html +66 -19
- package/dist/docs/readme.html +113 -12
- package/dist/embed/index.html +7 -0
- package/dist/examples/font-css-false.json +33 -0
- package/dist/examples/issue-image-region.json +1 -1
- package/dist/examples/layers-control.json +60 -0
- package/dist/examples/map-template-provider.json +36 -0
- package/dist/examples/multi-media.json +71 -0
- package/dist/examples-iiif/annotated-image.json +16 -0
- package/dist/examples-iiif/font-css-false.json +114 -0
- package/dist/examples-iiif/issue-image-region.json +1 -1
- package/dist/examples-iiif/layers-control.json +226 -0
- package/dist/examples-iiif/map-template-provider.json +133 -0
- package/dist/examples-iiif/multi-media.json +237 -0
- package/dist/examples-slideshow/single.json +79 -0
- package/dist/examples-slideshow/static/pixel.png +0 -0
- package/dist/examples-slideshow/static.json +44 -0
- package/dist/harness-multi.html +3 -3
- package/dist/harness.html +4 -3
- package/dist/index.html +3 -2
- package/dist/js/storymap.d.ts +1326 -289
- package/dist/js/storymap.js +6690 -4784
- package/dist/js/storymap.js.map +1 -1
- package/dist/navplace-properties.json +14 -0
- package/dist/site.css +54 -36
- package/docs/migration-from-knightlab.md +89 -49
- package/docs/storymap-as-iiif-manifest.md +199 -126
- package/package.json +4 -2
- package/schema/storymap.schema.json +334 -34
- package/src/core/Util.ts +3 -1
- package/src/core/mixins.ts +42 -28
- package/src/language/Language.ts +7 -0
- package/src/language/locale/.expected-gaps.json +287 -27
- package/src/language/locale/de.json +11 -1
- package/src/language/locale/en.json +11 -1
- package/src/main.ts +51 -0
- package/src/map/Map.ts +44 -6
- package/src/map/MapMarker.ts +16 -3
- package/src/map/openlayers/Map.OpenLayers.ts +787 -62
- package/src/map/openlayers/MapMarker.OpenLayers.ts +113 -13
- package/src/map/slideView.ts +149 -0
- package/src/map/types.ts +2 -0
- package/src/media/EmbedUtil.ts +27 -2
- package/src/media/Media.ts +27 -4
- package/src/media/embedId.ts +6 -0
- package/src/media/types/Flickr.ts +14 -3
- package/src/media/types/HtmlMedia.ts +66 -0
- package/src/media/types/Image.ts +15 -3
- package/src/media/types/SoundCloud.ts +1 -1
- package/src/media/types/Text.ts +27 -8
- package/src/media/types/Twitter.ts +9 -7
- package/src/media/types/Vimeo.ts +1 -1
- package/src/media/types/YouTube.ts +2 -2
- package/src/scss/Typography.scss +15 -15
- package/src/scss/VCO.StoryMap.scss +173 -8
- package/src/scss/core/Reset.scss +5 -18
- package/src/scss/map/VCO.Map.scss +1 -1
- package/src/scss/map/VCO.MapMarker.scss +20 -9
- package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +7 -4
- package/src/scss/media/VCO.Media.scss +7 -7
- package/src/scss/media/types/VCO.Media.Blockquote.scss +1 -1
- package/src/scss/media/types/VCO.Media.Text.scss +5 -5
- package/src/scss/media/types/VCO.Media.Twitter.scss +4 -4
- package/src/scss/media/types/VCO.Media.Wikipedia.scss +2 -19
- package/src/scss/site/site.scss +55 -36
- package/src/scss/slider/VCO.Slide.scss +49 -11
- package/src/scss/slider/VCO.SlideNav.scss +23 -7
- package/src/scss/slider/VCO.StorySlider.scss +50 -0
- package/src/scss/snapmap/VCO.SnapMap.scss +3 -3
- package/src/scss/ui/VCO.Consent.scss +20 -14
- package/src/scss/ui/VCO.Layers.scss +119 -0
- package/src/scss/ui/VCO.MenuBar.Button.scss +140 -19
- package/src/scss/ui/VCO.MenuBar.scss +9 -2
- package/src/scss/ui/VCO.Message.scss +14 -0
- package/src/slider/Slide.ts +232 -74
- package/src/slider/SlideNav.ts +15 -4
- package/src/slider/StorySlider.ts +203 -22
- package/src/storymap/Consent.ts +22 -0
- package/src/storymap/StoryMap.ts +944 -131
- package/src/storymap/content-state.ts +3 -8
- package/src/storymap/font-css.ts +72 -0
- package/src/storymap/from-slideshow.ts +785 -0
- package/src/storymap/iiif-shared.ts +228 -0
- package/src/storymap/iiif.ts +289 -100
- package/src/storymap/to-iiif.ts +131 -44
- package/src/types.ts +382 -17
- package/src/ui/LayersControl.ts +302 -0
- package/src/ui/MenuBar.ts +232 -21
- package/src/ui/Message.ts +36 -3
- package/src/ui/Swipable.ts +25 -7
- package/dist/assets/harness-Cr0rYpvt.js +0 -1
- package/dist/assets/main-DXalctuR.js +0 -17
- package/src/scss/VCO.StoryMap.Dark.scss +0 -150
- package/src/scss/Variables.Dark.scss +0 -57
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the IIIF round-trip pair (`iiif.ts` reads manifests,
|
|
3
|
+
* `to-iiif.ts` writes them) and the Content State codec (`content-state.ts`).
|
|
4
|
+
*
|
|
5
|
+
* These were three verbatim copies (`LOCATION_PROPERTIES`, `asRecord`, the
|
|
6
|
+
* `xywh` handling, the URL checks): adding a key to one side silently broke
|
|
7
|
+
* the round trip. There is exactly one intentional non-unification here —
|
|
8
|
+
* the `xywh` fragment parsers keep their own validation (the manifest reader
|
|
9
|
+
* requires non-negative origins and positive sizes for an image region, the
|
|
10
|
+
* Content State codec parses Media Fragment URIs permissively), so only the
|
|
11
|
+
* guards below are shared, not the fragment regexes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Keys copied verbatim between navPlace Feature properties and the location. */
|
|
15
|
+
export const LOCATION_PROPERTIES = [
|
|
16
|
+
"name",
|
|
17
|
+
"zoom",
|
|
18
|
+
"rotation",
|
|
19
|
+
"basemap",
|
|
20
|
+
"filter",
|
|
21
|
+
"mask",
|
|
22
|
+
"line",
|
|
23
|
+
"icon",
|
|
24
|
+
"iconSize",
|
|
25
|
+
"image",
|
|
26
|
+
"use_custom_marker",
|
|
27
|
+
// marker presentation with no IIIF vocabulary of its own: a GeoJSON
|
|
28
|
+
// foreign member needs no registration, which is why these live here
|
|
29
|
+
// rather than in a storymap: term (a GeoJSON foreign member needs no registration)
|
|
30
|
+
"popup",
|
|
31
|
+
"audioBadge",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** A plain object, or null for anything else (arrays included). */
|
|
35
|
+
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
36
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return value as Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A non-empty string, or null. */
|
|
43
|
+
export function asString(value: unknown): string | null {
|
|
44
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A real number (NaN rejected), or null. */
|
|
48
|
+
export function asNumber(value: unknown): number | null {
|
|
49
|
+
return typeof value === "number" && !isNaN(value) ? value : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A boolean, or null. */
|
|
53
|
+
export function asBoolean(value: unknown): boolean | null {
|
|
54
|
+
return typeof value === "boolean" ? value : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A string or list of strings as a string list (anything else: empty). */
|
|
58
|
+
export function asStringArray(value: unknown): string[] {
|
|
59
|
+
if (typeof value === "string") return [value];
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
return value.filter((entry): entry is string => typeof entry === "string");
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A `[west, south, east, north]` box of four finite numbers. */
|
|
67
|
+
export function isLonLatBox(value: unknown): value is [number, number, number, number] {
|
|
68
|
+
return (
|
|
69
|
+
Array.isArray(value) &&
|
|
70
|
+
value.length === 4 &&
|
|
71
|
+
value.every((n) => typeof n === "number" && Number.isFinite(n))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True for absolute `http(s)` URLs. DOM-free, so the CLI converter can use it. */
|
|
76
|
+
export function isHttpUrl(value: unknown): value is string {
|
|
77
|
+
return typeof value === "string" && /^https?:\/\//i.test(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A plain string as a language-neutral (`none`) language map. */
|
|
81
|
+
export function languageMap(value: string): { none: string[] } {
|
|
82
|
+
return { none: [value] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Escape the five characters that would otherwise be markup. Shared by the
|
|
87
|
+
* manifest reader (multi-paragraph bodies) and the slideshow translator
|
|
88
|
+
* (single-paragraph bodies) — the wrappers differ, the escaping must not.
|
|
89
|
+
*/
|
|
90
|
+
export function escapeText(value: string): string {
|
|
91
|
+
return value
|
|
92
|
+
.replace(/&/g, "&")
|
|
93
|
+
.replace(/</g, "<")
|
|
94
|
+
.replace(/>/g, ">")
|
|
95
|
+
.replace(/"/g, """)
|
|
96
|
+
.replace(/'/g, "'");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Playback flags shared by narration and audio/video media (slideshow
|
|
101
|
+
* audio terms when they ride `storymap:` foreign members). All optional;
|
|
102
|
+
* absent means the long-standing behavior (play on arrival, stop on leave).
|
|
103
|
+
*/
|
|
104
|
+
export interface PlaybackBag {
|
|
105
|
+
loop?: boolean;
|
|
106
|
+
offset?: number;
|
|
107
|
+
play?: "auto" | "click";
|
|
108
|
+
stopOnExit?: boolean;
|
|
109
|
+
stopAllPrevious?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read playback flags off a record's `storymap:` foreign members. Only
|
|
114
|
+
* well-typed values are kept; `onUnknownPlay` reports an unexpected `play`
|
|
115
|
+
* (the slideshow translator warns, the manifest reader stays silent).
|
|
116
|
+
*/
|
|
117
|
+
export function readPlaybackBag(
|
|
118
|
+
record: Record<string, unknown> | null,
|
|
119
|
+
onUnknownPlay?: (value: unknown) => void,
|
|
120
|
+
): PlaybackBag {
|
|
121
|
+
const bag: PlaybackBag = {};
|
|
122
|
+
if (!record) return bag;
|
|
123
|
+
if (typeof record["storymap:loop"] === "boolean") bag.loop = record["storymap:loop"];
|
|
124
|
+
const offset = record["storymap:offset"];
|
|
125
|
+
if (typeof offset === "number" && Number.isFinite(offset) && offset >= 0) {
|
|
126
|
+
bag.offset = offset;
|
|
127
|
+
}
|
|
128
|
+
const play = record["storymap:play"];
|
|
129
|
+
if (play === "auto" || play === "click") {
|
|
130
|
+
bag.play = play;
|
|
131
|
+
} else if (play !== undefined) {
|
|
132
|
+
onUnknownPlay?.(play);
|
|
133
|
+
}
|
|
134
|
+
if (typeof record["storymap:stopOnExit"] === "boolean") {
|
|
135
|
+
bag.stopOnExit = record["storymap:stopOnExit"];
|
|
136
|
+
}
|
|
137
|
+
if (typeof record["storymap:stopAllPrevious"] === "boolean") {
|
|
138
|
+
bag.stopAllPrevious = record["storymap:stopAllPrevious"];
|
|
139
|
+
}
|
|
140
|
+
return bag;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write playback flags as `storymap:` foreign members. Only present flags
|
|
145
|
+
* are written, so readers that never heard of them see an unchanged
|
|
146
|
+
* annotation.
|
|
147
|
+
*/
|
|
148
|
+
export function writePlaybackTerms(bag: {
|
|
149
|
+
loop?: boolean;
|
|
150
|
+
offset?: number;
|
|
151
|
+
play?: "auto" | "click";
|
|
152
|
+
stopOnExit?: boolean;
|
|
153
|
+
stopAllPrevious?: boolean;
|
|
154
|
+
}): {
|
|
155
|
+
"storymap:loop"?: boolean;
|
|
156
|
+
"storymap:offset"?: number;
|
|
157
|
+
"storymap:play"?: string;
|
|
158
|
+
"storymap:stopOnExit"?: boolean;
|
|
159
|
+
"storymap:stopAllPrevious"?: boolean;
|
|
160
|
+
} {
|
|
161
|
+
const terms: {
|
|
162
|
+
"storymap:loop"?: boolean;
|
|
163
|
+
"storymap:offset"?: number;
|
|
164
|
+
"storymap:play"?: string;
|
|
165
|
+
"storymap:stopOnExit"?: boolean;
|
|
166
|
+
"storymap:stopAllPrevious"?: boolean;
|
|
167
|
+
} = {};
|
|
168
|
+
if (typeof bag.loop === "boolean") terms["storymap:loop"] = bag.loop;
|
|
169
|
+
if (typeof bag.offset === "number") terms["storymap:offset"] = bag.offset;
|
|
170
|
+
if (bag.play === "auto" || bag.play === "click") terms["storymap:play"] = bag.play;
|
|
171
|
+
if (typeof bag.stopOnExit === "boolean") terms["storymap:stopOnExit"] = bag.stopOnExit;
|
|
172
|
+
if (typeof bag.stopAllPrevious === "boolean") {
|
|
173
|
+
terms["storymap:stopAllPrevious"] = bag.stopAllPrevious;
|
|
174
|
+
}
|
|
175
|
+
return terms;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* True when a narration URL duplicates slide media (the track is kept
|
|
180
|
+
* once, as media, never twice).
|
|
181
|
+
*/
|
|
182
|
+
export function isDuplicateNarration(
|
|
183
|
+
mediaUrls: (string | null | undefined)[],
|
|
184
|
+
url: string,
|
|
185
|
+
): boolean {
|
|
186
|
+
return mediaUrls.some((mediaUrl) => typeof mediaUrl === "string" && mediaUrl === url);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clamp an image region `[x, y, w, h]` to the canvas `[0, 0, W, H]`
|
|
191
|
+
* (slideshow targets routinely overshoot, e.g. `#-922,1005,2129,949`).
|
|
192
|
+
* Returns the intersected region, or null when nothing visible remains.
|
|
193
|
+
* Without known dims only the negative origin is pulled to 0 (preserving
|
|
194
|
+
* the far edge); zero/negative sizes are always null.
|
|
195
|
+
*/
|
|
196
|
+
export function clampRegion(
|
|
197
|
+
region: [number, number, number, number],
|
|
198
|
+
width: number | null,
|
|
199
|
+
height: number | null,
|
|
200
|
+
): [number, number, number, number] | null {
|
|
201
|
+
let [x, y, w, h] = region;
|
|
202
|
+
if (![x, y, w, h].every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
206
|
+
if (x < 0) {
|
|
207
|
+
w += x;
|
|
208
|
+
x = 0;
|
|
209
|
+
}
|
|
210
|
+
if (y < 0) {
|
|
211
|
+
h += y;
|
|
212
|
+
y = 0;
|
|
213
|
+
}
|
|
214
|
+
if (width !== null && width > 0) {
|
|
215
|
+
w = Math.min(w, width - x);
|
|
216
|
+
}
|
|
217
|
+
if (height !== null && height > 0) {
|
|
218
|
+
h = Math.min(h, height - y);
|
|
219
|
+
}
|
|
220
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
221
|
+
return [x, y, w, h];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** A finite number within `[min, max]`, or null. */
|
|
225
|
+
export function asClampedNumber(value: unknown, min: number, max: number): number | null {
|
|
226
|
+
if (typeof value !== "number" || isNaN(value) || !Number.isFinite(value)) return null;
|
|
227
|
+
return Math.min(max, Math.max(min, value));
|
|
228
|
+
}
|