開発者向け
Kasane のビューアは、パッケージ iiif-kasane として、ほかのサイトにも置けます(MIT ライセンス)。React の部品として使う方法、HTML のタグ <iiif-kasane> として使う方法、Astro で使う方法を、動く例とコードで示します。
各例の上のタブで「プレビュー」(動くビューア)とコードを切り替えます。
インストール
npm で iiif-kasane として公開しています。3D 用の three.js などは、画像だけを見せる場合も入れてください(ブラウザが読むのは 3D を表示したときだけです)。タグだけを使う場合は、インストールは要りません。
npm install iiif-kasane
npm install react react-dom jotai three three-stdlib @react-three/fiber @react-three/drei gsapReact: Viewer
いちばん簡単な使い方です。Viewer は親の要素いっぱいに広がるので、親に高さを付けて className="h-full" を渡します。iiifContent にはマニフェストの URL か IIIF Content State を渡します。
import { Viewer } from 'iiif-kasane';
import 'iiif-kasane/style.css';
export default function Page() {
return (
<div style={{ height: '32rem' }}>
<Viewer
iiifContent="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
locale="ja"
className="h-full"
/>
</div>
);
}ほかに locale(ja / en)、theme(light / dark / auto)、表示する頁を外から指定する target、表示中の頁を知らせる onSelect などがあります。
React: 部品を自分のレイアウトに置く
ViewerProvider の中に、表示(CanvasView)・注釈の一覧(AnnotationList)・資料情報(Metadata)などの部品を好きな配置で置けます。部品どうしは同じ状態を共有します。一覧の注釈を選ぶと表示が動き、頁をめくると資料情報が追いかけます。useViewer() を使えば、自分の部品からも同じ状態を読めます(例の PageLabel)。
// The viewer's parts placed in a layout of your own. ViewerProvider loads the manifest and holds the
// shown Canvas; the parts inside it read the same state, so a click on an annotation in the list
// moves the 3D view, and the metadata follows the page turns.
import {
AnnotationList,
CanvasView,
Metadata,
ViewerProvider,
useViewer,
type ViewerSelection,
} from 'iiif-kasane';
import { labelIn } from 'iiif-kasane/core';
import 'iiif-kasane/style.css';
import { useState } from 'react';
// A part of your own: reads the provider's state with useViewer(). IIIF labels are language maps
// ({ en: [...], none: [...] }); labelIn picks one language as a string.
function PageLabel() {
const { container, selectedIndex, containers } = useViewer();
if (!container) return null;
return (
<p className="page-label">
{selectedIndex + 1} / {containers.length}: {labelIn(container.label, 'en') || container.id}
</p>
);
}
export default function FreeLayout({ manifest }: { manifest: string }) {
const [shown, setShown] = useState<ViewerSelection | null>(null);
return (
<>
{/* The provider draws one <div class="iiif-viewer">; className makes it your layout's container. */}
<ViewerProvider iiifContent={manifest} locale="en" theme="light" className="free-layout" onSelect={setShown}>
<section className="area-view">
<PageLabel />
<CanvasView className="view" />
</section>
{/* AnnotationList has its own heading, search and "show on model" switch. */}
<section className="area-annotations">
<AnnotationList />
</section>
<section className="area-metadata">
<h2>About</h2>
<Metadata />
</section>
</ViewerProvider>
<p>onSelect: {shown ? `${shown.index}, ${shown.canvasId}` : '(not yet)'}</p>
</>
);
}/*
* The layout is this page's own CSS. The package's style.css has only the classes its own parts
* use, and they apply only inside the provider, so write your layout with your own classes.
*/
.free-layout {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 22rem 14rem;
grid-template-areas: 'view annotations' 'view metadata';
gap: 0.75rem;
}
.free-layout section { display: flex; flex-direction: column; min-height: 0; border: 1px solid #ccc; }
.free-layout h2 { margin: 0; padding: 0.25rem 0.75rem; font-size: 1rem; background: #f3f4f6; }
.area-view { grid-area: view; }
.area-view .view { flex: 1; }
.area-annotations { grid-area: annotations; overflow-y: auto; }
.area-metadata { grid-area: metadata; overflow-y: auto; }
.page-label { margin: 0; padding: 0.25rem 0.75rem; font-size: 0.875rem; }
@media (max-width: 48rem) {
.free-layout {
grid-template-columns: 1fr;
grid-template-rows: 24rem auto auto;
grid-template-areas: 'view' 'annotations' 'metadata';
}
}レイアウトはページ側の CSS で書きます。パッケージの style.css には部品が使うクラスしか無く、それもビューアの中だけに効きます。
タグ: <iiif-kasane>
フレームワークの無いページでも、スクリプトを 1 行読み込めばタグとして使えます。React はスクリプトの中に入っています。高さは CSS か --iiif-viewer-height で、色は --iiif-viewer-* の変数で変えられます。版ごとの URL は残し続けるので、版を固定して読み込めます。
<script type="module" src="https://kasane.ldas.jp/element/v0.3.0/iiif-kasane.js"></script>
<iiif-kasane
iiif-content="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
locale="ja"
style="height: 32rem"
></iiif-kasane>属性は iiif-content・locale・theme と、options(JSON。例: {"panelOpen":false,"pager":false})です。属性を書き換えると、表示もそのとおりに変わります。
資料ごとの埋め込みのコードは、ビューアの「共有」から作れます。 構造・共有・埋め込みの説明
Astro
Astro では両方の方法が使えます。tag.astro はタグを素の HTML として送り、スクリプトがブラウザでビューアにします(プレビューはこの形)。parts.astro は上の「部品を自分のレイアウトに置く」の FreeLayout.tsx を React の island として置きます。ビューアはブラウザの機能(WebGL・画面の大きさ)を使うので、client:only="react" で描きます。
---
import Layout from '../components/Layout.astro';
const manifests = [
{ label: 'Globe (3D, with annotations)', url: 'https://kasane.ldas.jp/manifests/sample-manifest-with-annotations.json' },
{ label: 'Image (IIIF Cookbook 0001)', url: 'https://iiif.io/api/cookbook/recipe/0001-mvm-image/v4/manifest.json' },
];
---
<Layout title="The <iiif-kasane> tag">
<h1>The <iiif-viewer> tag</h1>
<p>
The tag needs no framework: Astro sends it as plain HTML, and the script below turns it into a
viewer in the browser. The controls change its attributes.
</p>
<form class="controls">
<label>Manifest
<select id="manifest">
{manifests.map((m) => <option value={m.url}>{m.label}</option>)}
</select>
</label>
<label>Theme
<select id="theme">
<option>auto</option>
<option>light</option>
<option>dark</option>
</select>
</label>
</form>
<iiif-kasane id="viewer" iiif-content={manifests[0].url} locale="en" theme="auto"></iiif-kasane>
<!-- On a real site: src="https://kasane.ldas.jp/element/v<version>/iiif-kasane.js".
The script loads style.css from its own directory. -->
<script is:inline type="module" src="/iiif-viewer/iiif-kasane.js"></script>
<script>
const viewer = document.getElementById('viewer')!;
const bind = (id: string, attribute: string) =>
document.getElementById(id)!.addEventListener('change', (e) =>
viewer.setAttribute(attribute, (e.target as HTMLSelectElement).value),
);
bind('manifest', 'iiif-content');
bind('theme', 'theme');
</script>
<style>
.controls { display: flex; gap: 1rem; margin-bottom: 0.75rem; }
/* The tag fills the height you give it (36rem by default). */
iiif-kasane { height: 32rem; border: 1px solid #ccc; }
</style>
</Layout>---
import Layout from '../components/Layout.astro';
import FreeLayout from '../components/FreeLayout';
---
<Layout title="React parts in your own layout">
<h1>React parts in your own layout</h1>
<p>
<code>ViewerProvider</code> with <code>CanvasView</code>, <code>AnnotationList</code> and
<code>Metadata</code> in a grid of this page. The viewer reads the browser (WebGL, the window
size), so the island is rendered on the client only.
</p>
<FreeLayout client:only="react" manifest="https://kasane.ldas.jp/manifests/sample-manifest-with-annotations.json" />
<style is:global>
/*
* The layout is this page's own CSS. The package's style.css has only the classes its own parts
* use, and they apply only inside the provider, so write your layout with your own classes.
*/
.free-layout {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 22rem 14rem;
grid-template-areas: 'view annotations' 'view metadata';
gap: 0.75rem;
}
.free-layout section { display: flex; flex-direction: column; min-height: 0; border: 1px solid #ccc; }
.free-layout h2 { margin: 0; padding: 0.25rem 0.75rem; font-size: 1rem; background: #f3f4f6; }
.area-view { grid-area: view; }
.area-view .view { flex: 1; }
.area-annotations { grid-area: annotations; overflow-y: auto; }
.area-metadata { grid-area: metadata; overflow-y: auto; }
.page-label { margin: 0; padding: 0.25rem 0.75rem; font-size: 0.875rem; }
@media (max-width: 48rem) {
.free-layout {
grid-template-columns: 1fr;
grid-template-rows: 24rem auto auto;
grid-template-areas: 'view' 'annotations' 'metadata';
}
}
</style>
</Layout>// The viewer's parts placed in a layout of your own. ViewerProvider loads the manifest and holds the
// shown Canvas; the parts inside it read the same state, so a click on an annotation in the list
// moves the 3D view, and the metadata follows the page turns.
import {
AnnotationList,
CanvasView,
Metadata,
ViewerProvider,
useViewer,
type ViewerSelection,
} from 'iiif-kasane';
import { labelIn } from 'iiif-kasane/core';
import 'iiif-kasane/style.css';
import { useState } from 'react';
// A part of your own: reads the provider's state with useViewer(). IIIF labels are language maps
// ({ en: [...], none: [...] }); labelIn picks one language as a string.
function PageLabel() {
const { container, selectedIndex, containers } = useViewer();
if (!container) return null;
return (
<p className="page-label">
{selectedIndex + 1} / {containers.length}: {labelIn(container.label, 'en') || container.id}
</p>
);
}
export default function FreeLayout({ manifest }: { manifest: string }) {
const [shown, setShown] = useState<ViewerSelection | null>(null);
return (
<>
{/* The provider draws one <div class="iiif-viewer">; className makes it your layout's container. */}
<ViewerProvider iiifContent={manifest} locale="en" theme="light" className="free-layout" onSelect={setShown}>
<section className="area-view">
<PageLabel />
<CanvasView className="view" />
</section>
{/* AnnotationList has its own heading, search and "show on model" switch. */}
<section className="area-annotations">
<AnnotationList />
</section>
<section className="area-metadata">
<h2>About</h2>
<Metadata />
</section>
</ViewerProvider>
<p>onSelect: {shown ? `${shown.index}, ${shown.canvasId}` : '(not yet)'}</p>
</>
);
}例の一式(動かし方は README): packages/iiif-viewer/examples/astro