@projektemacher/storymapjs 0.10.5 → 0.10.7

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.
Files changed (148) hide show
  1. package/CHANGELOG +11 -0
  2. package/dist/assets/{demo-CVQ7kFek.js → demo-D6cTmzn6.js} +1 -1
  3. package/dist/assets/{harness-C-VZV-ou.js → harness-BPFlzutp.js} +1 -1
  4. package/dist/assets/main-Y-MYUSJ4.js +17 -0
  5. package/dist/css/fonts/font.oldstandard.css +14 -32
  6. package/dist/demo.html +2 -2
  7. package/dist/docs/migration.html +1 -1
  8. package/dist/embed/index.html +129 -4
  9. package/dist/harness.html +2 -2
  10. package/dist/index.html +17 -0
  11. package/dist/js/storymap.d.ts +126 -7
  12. package/dist/js/storymap.js +4518 -3613
  13. package/dist/js/storymap.js.map +1 -1
  14. package/docs/migration-from-knightlab.md +139 -0
  15. package/docs/storymap-as-iiif-manifest.md +510 -0
  16. package/package.json +6 -3
  17. package/src/animation/easings.ts +18 -0
  18. package/src/animation/morpheus.d.ts +41 -0
  19. package/src/core/Browser.ts +19 -0
  20. package/src/core/Load.ts +180 -0
  21. package/src/core/Util.ts +354 -0
  22. package/src/core/mixins.ts +171 -0
  23. package/src/dom/Dom.ts +31 -0
  24. package/src/dom/DomEvent.ts +61 -0
  25. package/src/globals.d.ts +16 -0
  26. package/src/language/Language.ts +107 -0
  27. package/src/language/locale/be.json +18 -0
  28. package/src/language/locale/bg.json +18 -0
  29. package/src/language/locale/cs.json +18 -0
  30. package/src/language/locale/de.json +20 -0
  31. package/src/language/locale/el.json +18 -0
  32. package/src/language/locale/en.json +28 -0
  33. package/src/language/locale/es.json +18 -0
  34. package/src/language/locale/et.json +18 -0
  35. package/src/language/locale/fr.json +18 -0
  36. package/src/language/locale/he.json +18 -0
  37. package/src/language/locale/hu.json +18 -0
  38. package/src/language/locale/is.json +18 -0
  39. package/src/language/locale/it.json +18 -0
  40. package/src/language/locale/jp.json +18 -0
  41. package/src/language/locale/ko.json +18 -0
  42. package/src/language/locale/nl.json +18 -0
  43. package/src/language/locale/nn.json +18 -0
  44. package/src/language/locale/no.json +18 -0
  45. package/src/language/locale/pl.json +18 -0
  46. package/src/language/locale/pt.json +18 -0
  47. package/src/language/locale/ru.json +18 -0
  48. package/src/language/locale/sk.json +18 -0
  49. package/src/language/locale/sr.json +17 -0
  50. package/src/language/locale/sv.json +18 -0
  51. package/src/language/locale/tr.json +18 -0
  52. package/src/language/locale/uk.json +18 -0
  53. package/src/language/locale/ur.json +18 -0
  54. package/src/language/locale/zh-cn.json +18 -0
  55. package/src/language/locale/zh-tw.json +18 -0
  56. package/src/main.ts +51 -0
  57. package/src/map/Map.ts +717 -0
  58. package/src/map/MapMarker.ts +149 -0
  59. package/src/map/openlayers/Map.OpenLayers.ts +1879 -0
  60. package/src/map/openlayers/MapMarker.OpenLayers.ts +176 -0
  61. package/src/map/openlayers/zoomifyTiles.ts +28 -0
  62. package/src/map/types.ts +17 -0
  63. package/src/media/EmbedUtil.ts +168 -0
  64. package/src/media/Media.ts +409 -0
  65. package/src/media/MediaType.ts +163 -0
  66. package/src/media/types/Audio.ts +87 -0
  67. package/src/media/types/Blockquote.ts +40 -0
  68. package/src/media/types/DailyMotion.ts +58 -0
  69. package/src/media/types/DocumentCloud.ts +41 -0
  70. package/src/media/types/Facebook.ts +41 -0
  71. package/src/media/types/Flickr.ts +115 -0
  72. package/src/media/types/GoogleDoc.ts +59 -0
  73. package/src/media/types/IFrame.ts +51 -0
  74. package/src/media/types/Image.ts +49 -0
  75. package/src/media/types/Juxtapose.ts +32 -0
  76. package/src/media/types/SoundCloud.ts +77 -0
  77. package/src/media/types/Text.ts +172 -0
  78. package/src/media/types/Twitter.ts +96 -0
  79. package/src/media/types/Video.ts +84 -0
  80. package/src/media/types/Vimeo.ts +65 -0
  81. package/src/media/types/Website.ts +36 -0
  82. package/src/media/types/Wikipedia.ts +124 -0
  83. package/src/media/types/YouTube.ts +192 -0
  84. package/src/scss/Typography.scss +237 -0
  85. package/src/scss/VCO.StoryMap.Dark.scss +138 -0
  86. package/src/scss/VCO.StoryMap.scss +164 -0
  87. package/src/scss/Variables.Dark.scss +56 -0
  88. package/src/scss/Variables.scss +47 -0
  89. package/src/scss/core/Mixins.scss +112 -0
  90. package/src/scss/core/Reset.scss +206 -0
  91. package/src/scss/fonts/_font.base.scss +113 -0
  92. package/src/scss/fonts/font.abril-droidsans.scss +75 -0
  93. package/src/scss/fonts/font.amatic-andika.scss +73 -0
  94. package/src/scss/fonts/font.bitter-raleway.scss +86 -0
  95. package/src/scss/fonts/font.clicker-garamond.scss +92 -0
  96. package/src/scss/fonts/font.dancing-ledger.scss +74 -0
  97. package/src/scss/fonts/font.default.scss +9 -0
  98. package/src/scss/fonts/font.fjalla-average.scss +80 -0
  99. package/src/scss/fonts/font.georgia-helvetica.scss +58 -0
  100. package/src/scss/fonts/font.knightlab.scss +97 -0
  101. package/src/scss/fonts/font.lustria-lato.scss +80 -0
  102. package/src/scss/fonts/font.medula-lato.scss +73 -0
  103. package/src/scss/fonts/font.oldstandard.scss +88 -0
  104. package/src/scss/fonts/font.opensans-gentiumbook.scss +80 -0
  105. package/src/scss/fonts/font.playfair-faunaone.scss +86 -0
  106. package/src/scss/fonts/font.playfair.scss +85 -0
  107. package/src/scss/fonts/font.pt.scss +90 -0
  108. package/src/scss/fonts/font.roboto-megrim.scss +73 -0
  109. package/src/scss/fonts/font.rufina-sintony.scss +73 -0
  110. package/src/scss/fonts/font.ubuntu.scss +78 -0
  111. package/src/scss/fonts/font.unicaone-vollkorn.scss +85 -0
  112. package/src/scss/icons/Icons.scss +239 -0
  113. package/src/scss/map/VCO.ImageMarker.scss +23 -0
  114. package/src/scss/map/VCO.Map.scss +21 -0
  115. package/src/scss/map/VCO.MapMarker.scss +147 -0
  116. package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +126 -0
  117. package/src/scss/media/VCO.Media.scss +222 -0
  118. package/src/scss/media/types/VCO.Media.Blockquote.scss +76 -0
  119. package/src/scss/media/types/VCO.Media.IFrame.scss +11 -0
  120. package/src/scss/media/types/VCO.Media.Image.scss +13 -0
  121. package/src/scss/media/types/VCO.Media.Instagram.scss +7 -0
  122. package/src/scss/media/types/VCO.Media.Profile.scss +5 -0
  123. package/src/scss/media/types/VCO.Media.Text.scss +90 -0
  124. package/src/scss/media/types/VCO.Media.Twitter.scss +87 -0
  125. package/src/scss/media/types/VCO.Media.Wikipedia.scss +64 -0
  126. package/src/scss/site/site.scss +748 -0
  127. package/src/scss/slider/VCO.Slide.scss +447 -0
  128. package/src/scss/slider/VCO.SlideNav.scss +338 -0
  129. package/src/scss/slider/VCO.StorySlider.scss +64 -0
  130. package/src/scss/snapmap/VCO.SnapMap.scss +103 -0
  131. package/src/scss/ui/VCO.Consent.scss +77 -0
  132. package/src/scss/ui/VCO.MenuBar.Button.scss +99 -0
  133. package/src/scss/ui/VCO.MenuBar.scss +20 -0
  134. package/src/scss/ui/VCO.Message.scss +104 -0
  135. package/src/site/site.ts +87 -0
  136. package/src/slider/Slide.ts +460 -0
  137. package/src/slider/SlideNav.ts +212 -0
  138. package/src/slider/StorySlider.ts +722 -0
  139. package/src/storymap/Consent.ts +146 -0
  140. package/src/storymap/StoryMap.ts +1108 -0
  141. package/src/storymap/iiif.ts +386 -0
  142. package/src/storymap/validate.ts +184 -0
  143. package/src/types.ts +297 -0
  144. package/src/ui/Draggable.ts +374 -0
  145. package/src/ui/MenuBar.ts +323 -0
  146. package/src/ui/Message.ts +118 -0
  147. package/src/ui/Swipable.ts +388 -0
  148. package/dist/assets/main-DwbXrrYn.js +0 -17
@@ -0,0 +1,139 @@
1
+ # Migrating from the Knight Lab StoryMapJS
2
+
3
+ This guide helps you move from the original Knight Lab StoryMapJS viewer
4
+ (distributed as a webpack UMD bundle with a `KLStoryMap` global) to this
5
+ rewritten TypeScript/OpenLayers viewer (ESM only).
6
+
7
+ The markup (`vco-*` classes), storymap JSON format and the
8
+ `new StoryMap(elem, data, options, listeners)` constructor signature are
9
+ preserved, so most stories render the same. What changed is the delivery
10
+ (ESM instead of UMD), the map engine (Leaflet → OpenLayers, so some
11
+ rendering details differ) and a few removed legacy paths.
12
+
13
+ ## Bundle and loading
14
+
15
+ | Knight Lab version (master) | This version |
16
+ | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `<script src="https://cdn.knightlab.com/libs/storymapjs/latest/js/storymap.js">` + `new KLStoryMap.StoryMap(...)` (UMD global) | `npm install` + ESM import: `import { StoryMap } from "storymapjs"` |
18
+ | `<link rel="stylesheet" href=".../css/storymap.css">` loaded manually | Import the CSS once (`import "storymapjs/css/storymap.css"` or the stylesheet from `dist/css/`); font themes load automatically via the `font_css` option |
19
+ | `main.js`/`main-min.js`, `compiled/` | single ESM bundle `dist/js/storymap.js` |
20
+
21
+ There is **no UMD/IIFE bundle and no `KLStoryMap`/`VCO` global** anymore. For
22
+ script-tag embedding use the ES module:
23
+
24
+ ```html
25
+ <script type="module">
26
+ import { StoryMap } from "https://example.com/storymap/js/storymap.js";
27
+ const storymap = new StoryMap("storymap-embed", "https://example.com/my-storymap.json");
28
+ </script>
29
+ ```
30
+
31
+ ## Constructor
32
+
33
+ The signature is the same:
34
+
35
+ ```ts
36
+ new StoryMap(elem, data, options?, listeners?)
37
+ ```
38
+
39
+ - `elem` — container element or DOM id (unchanged).
40
+ - `data` — a storymap document (`{ storymap: ... }`), an **IIIF Presentation 3
41
+ manifest**, or a **URL of the source file**, which is fetched and validated
42
+ automatically (unchanged).
43
+ - `options` — see below for removed/changed options.
44
+ - `listeners` — event listeners keyed by name (unchanged); `storymap.on(...)`
45
+ still works.
46
+
47
+ New capabilities:
48
+
49
+ - **Resize handling**: the viewer re-layouts automatically when its container
50
+ resizes (disable with `trackResize: false`).
51
+ - **Fullscreen**: a menubar fullscreen toggle driven by the standard HTML5
52
+ fullscreen API (`fullscreen: false` hides the button).
53
+ - **Raw OpenLayers options**: pass `map_options: { controls, interactions, view,
54
+ element, ... }` to configure the underlying OpenLayers map; `element`
55
+ (HTMLElement or DOM id) replaces the auto-created map container.
56
+ - **Runtime options**: `storymap.setMapOption(name, value)` /
57
+ `storymap.setMapOptions({...})` — e.g. switch `map_type` (main + minimap tile
58
+ layers are rebuilt together) or line styling live. Relative tile templates
59
+ such as `./tiles/{z}/{x}/{y}.png` are accepted, not just absolute `https://`
60
+ URLs.
61
+ - **Left-area map layout**: `map_area: "left"` limits the map to the left,
62
+ visible half in landscape with an opaque slide panel (no gradient over the
63
+ map) — the view needs no offset, so fits, `map_bbox` constraints and the
64
+ minimap align with the visible area directly. Default: `"full"` (the map
65
+ spans the whole width with the slide panel fading in over it).
66
+ - **Map bounding box**: `map_bbox: [west, south, east, north]` (lon/lat; raw
67
+ image pixels for image-space maps) constrains the view center to the box.
68
+ - **Scroll hint**: when a slide's content overflows, a bouncing downward
69
+ arrow appears at the bottom center of the slide (all devices) — tapping it
70
+ scrolls one step down; it hides after the first scroll. Touch scrollbars
71
+ are rendered wider with a visible track for contrast.
72
+
73
+ ## Changed options and map types
74
+
75
+ | Old | New |
76
+ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77
+ | `map_type: "zoomify"` (+ `options.zoomify`) | preferred: `map_type: "iiif"` with `options.iiif = { url, attribution }` (IIIF Image API 2/3); zoomify still works as a legacy image-pyramid basemap (see below) |
78
+ | `map_type: "stamen:toner"` etc. | `"stamen:*"` types are deprecated and remapped: `stamen:watercolor` → `ch-watercolor`, others → `osm:standard` |
79
+ | Stadia Maps paid tiles via `map_access_token` | Recommended: free OpenStreetMap vector styles, e.g. `map_type: "osm:bright"` (OpenFreeMap), or any style JSON URL |
80
+ | Bundled `map_access_token` | removed — pass `map_access_token` in the options if you use Mapbox/Stadia tiles |
81
+ | Bundled Flickr API key | removed — pass `api_key_flickr` in the options if you use `flickr.com/photos` API URLs |
82
+ | `relative_date: true` (moment.js) | removed — format dates in the story text |
83
+ | `font_css: "stock:<name>"` (or a path) | paths now resolve against the page URL (not the library location), plus the font files ship via `@fontsource-utils/scss`; no separate font CSS link needed |
84
+
85
+ ## Removed globals and exports
86
+
87
+ | Removed | Replacement |
88
+ | --------------------------------- | ----------------------------------------------------- |
89
+ | `KLStoryMap` / `VCO` global (UMD) | ESM import of the named exports |
90
+ | `window.trace` | `console.log` |
91
+ | `getJSON(url, onload)` | `fetch` / the StoryMap constructor's URL loading |
92
+ | `StamenTileLayer` export | `map_type: "osm:bright"` or another OpenFreeMap style |
93
+ | `ZoomifyTileLayer` export | `map_type: "iiif"` |
94
+ | Knight Lab usage tracking (gtag) | none — nothing is sent |
95
+
96
+ ## Removed media types and services
97
+
98
+ | Removed | Replacement |
99
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | `vine` media type (service shut down) | vine URLs fall back to the `website` iframe — use another media source for those slides |
101
+ | Juxtapose `frame/?uid=` embed URLs | the `juxtapose.knightlab.com/frame/?uid=` host is dead — use the published format `https://cdn.knightlab.com/libs/juxtapose/latest/embed/index.html?uid=...` (note: this keeps a Knight Lab CDN dependency for those slides) |
102
+ | Twitter `@nickname` rendering | tweets from `x.com` URLs are now parsed too (fixes `@undefined` nicknames); no migration needed |
103
+
104
+ `map_type: "zoomify"` is supported again (legacy): the image pyramid renders
105
+ via the storymap data's `zoomify` options (`path`, `width`, `height`), using a
106
+ JS warning instead of the previous removal error. Cropped edge/remainder tiles
107
+ are padded onto a full tile canvas before rendering (the original renderer's
108
+ per-tile clamp), and the minimap fits the whole image on the pyramid ladder.
109
+ Using zoomify in a IIIF
110
+ Presentation manifest is ignored — legacy zoomify options only work with
111
+ storymap JSON sources.
112
+
113
+ ## Map engine: Leaflet → OpenLayers
114
+
115
+ - `storymap.map` now exposes the **OpenLayers `Map`** instance instead of a
116
+ Leaflet map. Port any direct Leaflet calls (e.g. `setView`, `flyTo`) to the
117
+ OpenLayers API (`getView().animate(...)`, ...).
118
+ - Markers are DOM overlays (`.vco-mapmarker` / `.vco-mapmarker-active`) like
119
+ before; the mini map is an OpenLayers `OverviewMap` control.
120
+ - New basemap options: vector styles via `map_type: "osm:<style>"` (OpenFreeMap)
121
+ or a Mapbox style JSON URL.
122
+
123
+ ## Data and tooling
124
+
125
+ - Storymap JSON is now validated against a JSON Schema
126
+ (`schema/storymap.schema.json`); invalid documents are reported to the
127
+ console at load time.
128
+ - IIIF Presentation 3 manifests are accepted directly as storymap sources.
129
+ - The StoryMap IIIF extension context changed to
130
+ `https://christianmahnke.de/iiif/storymap` — manifests produced with the old
131
+ `https://example.org/ns/storymap/v1` context need re-converting
132
+ (`scripts/convert-to-iiif.mjs`).
133
+ - The editor, staging/backend infrastructure, AWS/GitHub hosting scripts and the
134
+ Python authoring server are gone — this is a viewer-only library.
135
+
136
+ ## Events (unchanged)
137
+
138
+ `change` (with `current_slide`), `loaded`, `title`, `dataloaded`,
139
+ `fontLoaded`, plus the listener map in the constructor — all work as before.
@@ -0,0 +1,510 @@
1
+ # StoryMap data as IIIF Presentation 3.0 manifests
2
+
3
+ A proposal for exchanging StoryMapJS data as
4
+ [IIIF Presentation API 3.0](https://iiif.io/api/presentation/3.0/) manifests so
5
+ that stories can be preserved, harvested and re-rendered by IIIF-aware tools.
6
+
7
+ The legacy format is described by [`schema/storymap.schema.json`](../schema/storymap.schema.json);
8
+ the fixtures in [`public/examples/`](../public/examples/) are real-world samples.
9
+ `scripts/convert-to-iiif.mjs` converts every fixture to this proposal
10
+ ([`public/examples-iiif/`](../public/examples-iiif/)), and
11
+ `scripts/validate-iiif.mjs` checks every converted manifest against the
12
+ [official IIIF presentation validator](https://presentation-validator.iiif.io/)
13
+ (`npm run validate:iiif`). All converted fixtures pass the official validator.
14
+
15
+ ## Contexts
16
+
17
+ A manifest carries three context URLs: the Presentation 3.0 context, the
18
+ official [navPlace extension](https://iiif.io/api/extension/navplace/) context,
19
+ and the StoryMap extension context:
20
+
21
+ ```json
22
+ {
23
+ "@context": [
24
+ "http://iiif.io/api/presentation/3/context.json",
25
+ "http://iiif.io/api/extension/navplace/context.json",
26
+ "https://cmahnke.github.io/StoryMapJS/context.json"
27
+ ]
28
+ }
29
+ ```
30
+
31
+ ### navPlace context (official)
32
+
33
+ Served at `http://iiif.io/api/extension/navplace/context.json`. It binds the
34
+ `navPlace` property to a GeoJSON FeatureCollection:
35
+
36
+ ```json
37
+ {
38
+ "@context": {
39
+ "@version": 1.1,
40
+ "iiif_navPlace": "http://iiif.io/api/extension/navplace#",
41
+ "navPlace": {
42
+ "@context": "https://geojson.org/geojson-ld/geojson-context.jsonld",
43
+ "@id": "iiif_navPlace:navPlace"
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### StoryMap context
50
+
51
+ StoryMap-specific terms use the `storymap:` prefix (the term IRIs live under
52
+ `https://christianmahnke.de/iiif/storymap#`). The context document is
53
+ [`public/context.json`](../public/context.json) — it ships with the demo build
54
+ and is served at `https://cmahnke.github.io/StoryMapJS/context.json`, which is
55
+ what the fixtures reference. Content:
56
+
57
+ ```json
58
+ {
59
+ "@context": {
60
+ "@version": 1.1,
61
+ "storymap": "https://christianmahnke.de/iiif/storymap#",
62
+ "mapType": "storymap:mapType",
63
+ "mapAsImage": "storymap:mapAsImage",
64
+ "mapAccessToken": "storymap:mapAccessToken",
65
+ "mapBackgroundColor": "storymap:mapBackgroundColor",
66
+ "mapCenterOffset": "storymap:mapCenterOffset",
67
+ "mapSubdomains": "storymap:mapSubdomains",
68
+ "iiifUrl": { "@id": "storymap:iiifUrl", "@type": "@id" },
69
+ "fontCss": "storymap:fontCss",
70
+ "callToAction": "storymap:callToAction",
71
+ "callToActionText": "storymap:callToActionText",
72
+ "startAtSlide": "storymap:startAtSlide",
73
+ "language": "storymap:language",
74
+ "calculateZoom": "storymap:calculateZoom",
75
+ "lessBounce": "storymap:lessBounce",
76
+ "lineFollowsPath": "storymap:lineFollowsPath",
77
+ "showLines": "storymap:showLines",
78
+ "showHistoryLine": "storymap:showHistoryLine",
79
+ "lineColor": "storymap:lineColor",
80
+ "lineColorInactive": "storymap:lineColorInactive",
81
+ "lineWeight": "storymap:lineWeight",
82
+ "lineOpacity": "storymap:lineOpacity",
83
+ "lineDash": "storymap:lineDash",
84
+ "lineJoin": "storymap:lineJoin",
85
+ "useCustomMarkers": "storymap:useCustomMarkers",
86
+ "type": { "@id": "storymap:type", "@type": "@id" },
87
+ "background": "storymap:background",
88
+ "mediaCaption": "storymap:mediaCaption",
89
+ "mediaCredit": "storymap:mediaCredit",
90
+ "date": "storymap:date"
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Manifest ↔ storymap root
96
+
97
+ | IIIF | StoryMap | Notes |
98
+ | ----------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
99
+ | `id` | — | `https://example.org/storymap/<name>` (absolute URIs required) |
100
+ | `type` | — | `"Manifest"` |
101
+ | `label` | `storymap.title` (extra) | Language map, e.g. `{"none": ["StoryMapJS"]}`; the legacy format has no title field, so readers keep it as an extra `title` field |
102
+ | `items` | `storymap.slides` | One Canvas per slide, in slide order |
103
+ | `summary` | — | Optional language map |
104
+ | `provider` | — | Optional publishing institution |
105
+ | `behavior` | — | `["paged"]` — the story is consumed slide by slide |
106
+ | `requiredStatement` | `storymap.iiif.attribution` | Only when an attribution is present |
107
+ | `navPlace` _(optional)_ | — | Optional manifest-level aggregation of canvas locations |
108
+ | `service` (map config) | `storymap.*` map settings | See "StoryMap-specific terms" below |
109
+
110
+ ## Canvas ↔ slide
111
+
112
+ Each slide becomes one Canvas in `items` order. Canvas ids are
113
+ `<manifest-id>/canvas/<n>` (1-based); each Canvas carries a single AnnotationPage
114
+ (`<canvas-id>/annotationpage/1`) holding the slide's painted media
115
+ (`<canvas-id>/annotation/1`, `motivation: "painting"`, `target: <canvas-id>`).
116
+
117
+ | IIIF | StoryMap | Notes |
118
+ | ----------------------- | --------------------- | ---------------------------------------------------------------------------------------- |
119
+ | `id`, `type` | — | `"Canvas"` |
120
+ | `label` | `slide.text.headline` | Language map `{"none": [headline]}`; omitted when the headline is empty |
121
+ | `summary` | `slide.text.text` | Language map; the slide body text (may contain HTML) |
122
+ | `height`, `width` | — | Nominal `1080 × 1080` for non-image slides; actual image dimensions for image-map slides |
123
+ | `items` | `slide.media` | AnnotationPage with the painting annotation, see below |
124
+ | `storymap:type` | `slide.type` | `"overview"` marks the map overview slide |
125
+ | `storymap:background` | `slide.background` | `{url, color}` — only present keys |
126
+ | `storymap:mediaCaption` | `slide.media.caption` | |
127
+ | `storymap:mediaCredit` | `slide.media.credit` | |
128
+ | `storymap:date` | `slide.date` | String or object, verbatim |
129
+ | `navPlace` | `slide.location` | See below |
130
+
131
+ ## Locations via navPlace
132
+
133
+ Canvas locations use the official
134
+ [navPlace extension](https://iiif.io/api/extension/navplace/): each Canvas with
135
+ a numeric `location.lat`/`location.lon` carries a `navPlace` FeatureCollection
136
+ with one GeoJSON Feature:
137
+
138
+ - `geometry`: `{type: "Point", coordinates: [lon, lat]}` — the storymap values
139
+ verbatim. For image-map storymaps (`storymap:mapAsImage: true`) the values are
140
+ image coordinates rather than WGS84 degrees; consumers can detect this via
141
+ `storymap:mapAsImage`.
142
+ - `properties`: marker data — `zoom`, `line`, `icon`, `iconSize`, `image`,
143
+ `use_custom_marker`, `name` (only present keys).
144
+ - `location.use_custom_marker` / `use_custom_markers` (manifest) opt into
145
+ custom marker rendering; see the storymap terms below.
146
+
147
+ Alternatively a manifest MAY aggregate all slide locations in a single
148
+ manifest-level `navPlace` with one Feature per Canvas in `items` order.
149
+
150
+ ## Slide media
151
+
152
+ The slide's media becomes a painting annotation on the canvas with a typed body:
153
+
154
+ - Image URLs → `{id, type: "Image", format: "image/jpeg" | ...}`
155
+ - Video services (YouTube, Vimeo, Dailymotion, Vine) → `{id, type: "Video"}`
156
+ - Audio services (SoundCloud) → `{id, type: "Sound"}`
157
+ - Page-embedded media (tweets, Wikipedia, photo pages) → `{id, type: "Text", format: "text/html"}`
158
+ - Text-only slides (no media URL, or a media value that is an HTML snippet) →
159
+ `{type: "TextualBody", format: "text/html", value: "<html>"}`
160
+
161
+ Media URL inference (YouTube players, Twitter embeds, etc.) remains a viewer
162
+ concern — manifests carry plain typed bodies and never embed service-specific
163
+ logic.
164
+
165
+ ## IIIF image-map slides (replacing zoomify)
166
+
167
+ Storymaps that paint an image as the map (`map_as_image: true` with `map_type:
168
+ "iiif"`, formerly `"zoomify"`) have canvases that paint an Image annotation
169
+ whose body references an IIIF Image API service. The canvas dimensions are the
170
+ actual image dimensions. Two body variants are valid:
171
+
172
+ **(a) plain image body:**
173
+
174
+ ```json
175
+ {
176
+ "id": "https://iiif.io/api/image/3.0/example/reference/28473c77da3deebe4375c3a50572d9d3-laocoon/full/max/0/default.jpg",
177
+ "type": "Image",
178
+ "format": "image/jpeg",
179
+ "width": 2315,
180
+ "height": 3000
181
+ }
182
+ ```
183
+
184
+ **(b) body with an ImageService3 reference (preferred):**
185
+
186
+ ```json
187
+ {
188
+ "id": "https://iiif.io/api/image/3.0/example/reference/28473c77da3deebe4375c3a50572d9d3-laocoon/full/max/0/default.jpg",
189
+ "type": "Image",
190
+ "format": "image/jpeg",
191
+ "width": 2315,
192
+ "height": 3000,
193
+ "service": [
194
+ {
195
+ "id": "https://iiif.io/api/image/3.0/example/reference/28473c77da3deebe4375c3a50572d9d3-laocoon/info.json",
196
+ "type": "ImageService3",
197
+ "profile": "level2"
198
+ }
199
+ ]
200
+ }
201
+ ```
202
+
203
+ The converter emits variant (b). Legacy `zoomify` storymaps are converted to
204
+ `storymap:mapType: "iiif"`; since the original zoomify tile paths are dead, the
205
+ converter substitutes the IIIF reference image above (the legacy pyramid
206
+ definition is not carried — nothing reads it).
207
+
208
+ ## StoryMap-specific terms
209
+
210
+ ### Manifest level — via a map configuration service
211
+
212
+ The official validator validates manifests against a JSON Schema whose Manifest
213
+ class is closed (`additionalProperties: false`), so manifest-level StoryMap
214
+ settings cannot be placed directly on the Manifest object. They live on a
215
+ dedicated extension service instead — a JSON-LD-idiomatic place for extended
216
+ configuration, and one the validator accepts (`service` entries allow extra
217
+ properties):
218
+
219
+ ```json
220
+ {
221
+ "service": [
222
+ {
223
+ "id": "https://example.org/storymap/<name>/map-config",
224
+ "type": "Service",
225
+ "profile": "https://christianmahnke.de/iiif/storymap/mapconfig",
226
+ "storymap:mapType": "osm:standard"
227
+ }
228
+ ]
229
+ }
230
+ ```
231
+
232
+ | Service property (storymap:) | StoryMap field | Values |
233
+ | ---------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
234
+ | `mapType` | `map_type` | `osm:standard`, `mapbox:...`, `stadia:...`, `iiif`, or an `https://...` tile URL; `zoomify` is replaced by `iiif` |
235
+ | `mapAsImage` | `map_as_image` | `true` when the image itself is the map |
236
+ | `mapAccessToken` | `map_access_token` | Mapbox/Stadia token from the storymap data, never the repository |
237
+ | `mapBackgroundColor` | `map_background_color` | CSS color |
238
+ | `mapCenterOffset` | `map_center_offset` | `{left, top}` |
239
+ | `mapSubdomains` | `map_subdomains` | Tile URL subdomains |
240
+ | `iiifUrl` | `iiif.url` | IIIF Image API `info.json` URL for image-map storymaps |
241
+ | `fontCss` | `font_css` | e.g. `stock:dancing-ledger` |
242
+ | `callToAction` | `call_to_action` | boolean |
243
+ | `callToActionText` | `call_to_action_text` | string |
244
+ | `startAtSlide` | `start_at_slide` | 0-based slide index |
245
+ | `language` | `language` | IETF language tag |
246
+ | `calculateZoom` | `calculate_zoom` | boolean |
247
+ | `lessBounce` | `less_bounce` | boolean |
248
+ | `lineFollowsPath` | `line_follows_path` | boolean |
249
+ | `showLines` | `show_lines` | boolean |
250
+ | `showHistoryLine` | `show_history_line` | boolean |
251
+ | `lineColor` | `line_color` | CSS color |
252
+ | `lineColorInactive` | `line_color_inactive` | CSS color |
253
+ | `lineWeight` | `line_weight` | number (px) |
254
+ | `lineOpacity` | `line_opacity` | 0–1 |
255
+ | `lineDash` | `line_dash` | CSS dash pattern |
256
+ | `lineJoin` | `line_join` | CSS line join |
257
+ | `useCustomMarkers` | `use_custom_markers` | boolean |
258
+
259
+ ### Canvas level — direct properties
260
+
261
+ Canvas objects are open for extension terms, so slide-specific StoryMap data is
262
+ carried directly on the Canvas: `storymap:type`, `storymap:background`,
263
+ `storymap:mediaCaption`, `storymap:mediaCredit`, `storymap:date` (see the
264
+ Canvas table above).
265
+
266
+ Readers must use the prefixed term for the slide type (`storymap:type`): the
267
+ bare `type` key of a Canvas is the IIIF class type (`"Canvas"`) and can never
268
+ carry the StoryMap slide type.
269
+
270
+ ## Validator interop
271
+
272
+ The official IIIF presentation validator
273
+ (`https://presentation-validator.iiif.io/validate?version=3.0&format=json`)
274
+ validates manifests with a static JSON Schema:
275
+
276
+ - `navPlace` (manifest- and canvas-level) is part of the schema and passes;
277
+ GeoJSON Feature `properties` are open objects and tolerate marker data.
278
+ - Canvas objects are open and tolerate `storymap:` terms.
279
+ - The Manifest object is **closed** — `storymap:` terms directly on the Manifest
280
+ are rejected, hence the map configuration service described above.
281
+ - `@context` entries must be URI strings; a StoryMap namespace cannot be
282
+ declared inline but must be referenced by URL.
283
+
284
+ `npm run validate:iiif` posts every manifest in `public/examples-iiif/` to the
285
+ official validator and fails CI when any of them is rejected.
286
+
287
+ ## Worked example
288
+
289
+ A small hurricane storymap: an overview slide (text-only media), a slide with a
290
+ photo, and a slide with a YouTube video — full manifest:
291
+
292
+ ```json
293
+ {
294
+ "@context": [
295
+ "http://iiif.io/api/presentation/3/context.json",
296
+ "http://iiif.io/api/extension/navplace/context.json",
297
+ "https://cmahnke.github.io/StoryMapJS/context.json"
298
+ ],
299
+ "id": "https://example.org/storymap/storm",
300
+ "type": "Manifest",
301
+ "label": { "none": ["storm"] },
302
+ "behavior": ["paged"],
303
+ "provider": [
304
+ {
305
+ "id": "https://example.org/",
306
+ "type": "Agent",
307
+ "label": { "none": ["StoryMapJS"] }
308
+ }
309
+ ],
310
+ "service": [
311
+ {
312
+ "id": "https://example.org/storymap/storm/map-config",
313
+ "type": "Service",
314
+ "profile": "https://christianmahnke.de/iiif/storymap/mapconfig",
315
+ "storymap:mapType": "osm:standard",
316
+ "storymap:language": "en",
317
+ "storymap:showLines": true,
318
+ "storymap:showHistoryLine": true,
319
+ "storymap:lineColor": "#c0392b",
320
+ "storymap:lineWeight": 3,
321
+ "storymap:lineOpacity": 0.8
322
+ }
323
+ ],
324
+ "items": [
325
+ {
326
+ "id": "https://example.org/storymap/storm/canvas/1",
327
+ "type": "Canvas",
328
+ "height": 1080,
329
+ "width": 1080,
330
+ "label": { "none": ["The Path of the Storm"] },
331
+ "summary": { "none": ["A storm formed over the warm ocean."] },
332
+ "items": [
333
+ {
334
+ "id": "https://example.org/storymap/storm/canvas/1/annotationpage/1",
335
+ "type": "AnnotationPage",
336
+ "items": [
337
+ {
338
+ "id": "https://example.org/storymap/storm/canvas/1/annotation/1",
339
+ "type": "Annotation",
340
+ "motivation": "painting",
341
+ "body": {
342
+ "type": "TextualBody",
343
+ "format": "text/html",
344
+ "value": "A storm formed over the warm ocean."
345
+ },
346
+ "target": "https://example.org/storymap/storm/canvas/1"
347
+ }
348
+ ]
349
+ }
350
+ ],
351
+ "storymap:type": "overview",
352
+ "storymap:date": "Sep 1"
353
+ },
354
+ {
355
+ "id": "https://example.org/storymap/storm/canvas/2",
356
+ "type": "Canvas",
357
+ "height": 1080,
358
+ "width": 1080,
359
+ "label": { "none": ["Sep 2"] },
360
+ "summary": { "none": ["The storm made landfall."] },
361
+ "items": [
362
+ {
363
+ "id": "https://example.org/storymap/storm/canvas/2/annotationpage/1",
364
+ "type": "AnnotationPage",
365
+ "items": [
366
+ {
367
+ "id": "https://example.org/storymap/storm/canvas/2/annotation/1",
368
+ "type": "Annotation",
369
+ "motivation": "painting",
370
+ "body": {
371
+ "id": "https://example.org/images/landfall.jpg",
372
+ "type": "Image",
373
+ "format": "image/jpeg"
374
+ },
375
+ "target": "https://example.org/storymap/storm/canvas/2"
376
+ }
377
+ ]
378
+ }
379
+ ],
380
+ "storymap:mediaCaption": "Landfall",
381
+ "storymap:mediaCredit": "Weather Service",
382
+ "storymap:date": "Sep 2",
383
+ "navPlace": {
384
+ "id": "https://example.org/storymap/storm/canvas/2/navplace",
385
+ "type": "FeatureCollection",
386
+ "features": [
387
+ {
388
+ "id": "https://example.org/storymap/storm/canvas/2/navplace/feature/1",
389
+ "type": "Feature",
390
+ "geometry": { "type": "Point", "coordinates": [-89.6, 28.2] },
391
+ "properties": { "zoom": 10, "line": true }
392
+ }
393
+ ]
394
+ }
395
+ },
396
+ {
397
+ "id": "https://example.org/storymap/storm/canvas/3",
398
+ "type": "Canvas",
399
+ "height": 1080,
400
+ "width": 1080,
401
+ "label": { "none": ["Sep 3"] },
402
+ "items": [
403
+ {
404
+ "id": "https://example.org/storymap/storm/canvas/3/annotationpage/1",
405
+ "type": "AnnotationPage",
406
+ "items": [
407
+ {
408
+ "id": "https://example.org/storymap/storm/canvas/3/annotation/1",
409
+ "type": "Annotation",
410
+ "motivation": "painting",
411
+ "body": {
412
+ "id": "https://www.youtube.com/watch?v=example",
413
+ "type": "Video"
414
+ },
415
+ "target": "https://example.org/storymap/storm/canvas/3"
416
+ }
417
+ ]
418
+ }
419
+ ],
420
+ "storymap:mediaCredit": "News",
421
+ "storymap:date": "Sep 3",
422
+ "navPlace": {
423
+ "id": "https://example.org/storymap/storm/canvas/3/navplace",
424
+ "type": "FeatureCollection",
425
+ "features": [
426
+ {
427
+ "id": "https://example.org/storymap/storm/canvas/3/navplace/feature/1",
428
+ "type": "Feature",
429
+ "geometry": { "type": "Point", "coordinates": [-90.1, 29.9] },
430
+ "properties": { "zoom": 9, "line": true }
431
+ }
432
+ ]
433
+ }
434
+ }
435
+ ]
436
+ }
437
+ ```
438
+
439
+ ## Full mapping table
440
+
441
+ | Legacy field (storymap root) | IIIF path |
442
+ | ---------------------------- | ------------------------------------------------------------------- |
443
+ | `slides` | `items[]` (Canvas per slide) |
444
+ | `language` | `service[0].storymap:language` |
445
+ | `map_type` | `service[0].storymap:mapType` (`zoomify` → `iiif`) |
446
+ | `map_as_image` | `service[0].storymap:mapAsImage` |
447
+ | `map_mini` | _dropped_ (viewer setting, not part of the exchange format) |
448
+ | `map_subdomains` | `service[0].storymap:mapSubdomains` |
449
+ | `map_access_token` | `service[0].storymap:mapAccessToken` |
450
+ | `map_background_color` | `service[0].storymap:mapBackgroundColor` |
451
+ | `map_center_offset` | `service[0].storymap:mapCenterOffset` |
452
+ | `map_popup` | _dropped_ (viewer setting) |
453
+ | `use_custom_markers` | `service[0].storymap:useCustomMarkers` |
454
+ | `zoom_distance` | _dropped_ (viewer setting) |
455
+ | `calculate_zoom` | `service[0].storymap:calculateZoom` |
456
+ | `less_bounce` | `service[0].storymap:lessBounce` |
457
+ | `line_follows_path` | `service[0].storymap:lineFollowsPath` |
458
+ | `show_lines` | `service[0].storymap:showLines` |
459
+ | `show_history_line` | `service[0].storymap:showHistoryLine` |
460
+ | `line_color` | `service[0].storymap:lineColor` |
461
+ | `line_color_inactive` | `service[0].storymap:lineColorInactive` |
462
+ | `line_weight` | `service[0].storymap:lineWeight` |
463
+ | `line_opacity` | `service[0].storymap:lineOpacity` |
464
+ | `line_dash` | `service[0].storymap:lineDash` |
465
+ | `line_join` | `service[0].storymap:lineJoin` |
466
+ | `iiif.url` | `service[0].storymap:iiifUrl` + canvas Image annotation `service[]` |
467
+ | `iiif.attribution` | `requiredStatement` |
468
+ | `zoomify` | _dropped_ (zoomify is replaced by the IIIF reference image) |
469
+ | `font_css` | `service[0].storymap:fontCss` |
470
+ | `call_to_action` | `service[0].storymap:callToAction` |
471
+ | `call_to_action_text` | `service[0].storymap:callToActionText` |
472
+ | `relative_date` | _dropped_ (viewer setting) |
473
+ | `start_at_slide` | `service[0].storymap:startAtSlide` |
474
+ | _(root) `width`, `height`_ | _dropped_ (viewer embed size) |
475
+
476
+ | Legacy field (slide) | IIIF path |
477
+ | ------------------------------ | ---------------------------------------------------------- |
478
+ | `type: "overview"` | Canvas `storymap:type: "overview"` |
479
+ | `date` | Canvas `storymap:date` |
480
+ | `text.headline` | Canvas `label` (language map) |
481
+ | `text.text` | Canvas `summary` (language map) |
482
+ | `location.lat`, `location.lon` | Canvas `navPlace` Feature `geometry.coordinates` |
483
+ | `location.zoom` | Canvas `navPlace` Feature `properties.zoom` |
484
+ | `location.line` | Canvas `navPlace` Feature `properties.line` |
485
+ | `location.name` | Canvas `navPlace` Feature `properties.name` |
486
+ | `location.icon` | Canvas `navPlace` Feature `properties.icon` |
487
+ | `location.iconSize` | Canvas `navPlace` Feature `properties.iconSize` |
488
+ | `location.image` | Canvas `navPlace` Feature `properties.image` |
489
+ | `location.use_custom_marker` | Canvas `navPlace` Feature `properties.use_custom_marker` |
490
+ | `media.url` (image) | Annotation body `{type: "Image", format: ...}` |
491
+ | `media.url` (video service) | Annotation body `{type: "Video"}` |
492
+ | `media.url` (audio service) | Annotation body `{type: "Sound"}` |
493
+ | `media.url` (web page) | Annotation body `{type: "Text", format: "text/html"}` |
494
+ | `media.url` (absent / HTML) | Annotation body `{type: "TextualBody", value: ...}` |
495
+ | `media.caption` | Canvas `storymap:mediaCaption` |
496
+ | `media.credit` | Canvas `storymap:mediaCredit` |
497
+ | `media.thumb` | _dropped_ (thumbnails may be added via Canvas `thumbnail`) |
498
+ | `background` | Canvas `storymap:background` `{url, color}` |
499
+ | `uniqueid` | _dropped_ (canvas `id`s are the canonical identifiers) |
500
+
501
+ ## Conversion
502
+
503
+ `node scripts/convert-to-iiif.mjs` regenerates `public/examples-iiif/` from
504
+ `public/examples/`. Ids are deterministic:
505
+
506
+ - Manifest: `https://example.org/storymap/<name>`
507
+ - Canvas: `<manifest-id>/canvas/<n>` (1-based, slide order)
508
+ - AnnotationPage: `<canvas-id>/annotationpage/1`
509
+ - Annotation: `<canvas-id>/annotation/1`
510
+ - FeatureCollection / Feature: `<canvas-id>/navplace[.../feature/1]`