Codrops 원본
Infinite Canvas: Building a Seamless, Pan-Anywhere Image Space
섹션 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다.
index.html
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Infinite Canvas | Codrops</title>
<meta
name="description"
content="An infinite 3D canvas built with React Three Fiber for exploring and displaying media in a seamless, immersive space." />
<meta name="keywords" content="" />
<meta name="author" content="Codrops" />
<link rel="icon" type="image/svg+xml" href="https://tympanus.net/favicon/favicon.svg" />
<link rel="shortcut icon" href="https://tympanus.net/favicon/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Asul:wght@400;700&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
<script>
document.documentElement.className = 'js';
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/index.tsx"></script>
</body>
</html>scripts/download-artworks.ts
import fs from "node:fs";
import path from "node:path";
const API_BASE = "https://api.artic.edu/api/v1";
const IIIF_BASE = "https://www.artic.edu/iiif/2";
const OUTPUT_DIR = "./public/artworks";
const MANIFEST_PATH = "./public/artworks/manifest.json";
const HEADERS = {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
Referer: "https://www.artic.edu/",
};
type ArticArtwork = {
id: number;
title: string;
artist_display: string;
date_display: string;
image_id: string;
thumbnail: { width: number; height: number } | null;
};
type ManifestItem = {
url: string;
title: string;
artist: string;
year: string;
link: string;
width: number;
height: number;
};
const SEARCH_QUERY = {
query: {
bool: {
must: [
{ term: { is_public_domain: true } },
{ term: { "classification_titles.keyword": "painting" } },
{ exists: { field: "image_id" } },
{ range: { date_end: { gte: 1600 } } },
{ range: { date_start: { lte: 1725 } } },
],
should: [
{ match: { style_title: "Baroque" } },
{ term: { "department_title.keyword": "Painting and Sculpture of Europe" } },
],
minimum_should_match: 1,
},
},
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function fetchAllArtworks(): Promise<ArticArtwork[]> {
const allArtworks: ArticArtwork[] = [];
const fields = "id,title,artist_display,date_display,image_id,thumbnail";
const params = encodeURIComponent(JSON.stringify(SEARCH_QUERY));
let page = 1;
while (allArtworks.length < 250) {
console.log(`Fetching page ${page}...`);
const url = `${API_BASE}/artworks/search?params=${params}&page=${page}&limit=50&fields=${fields}`;
const res = await fetch(url, { headers: HEADERS });
if (!res.ok) throw new Error(`API error: ${res.status}`);
const data = await res.json();
if (!data.data?.length) break;
const valid = data.data.filter((a: ArticArtwork) => a.image_id && a.thumbnail);
allArtworks.push(...valid);
console.log(` Got ${valid.length} (total: ${allArtworks.length})`);
page++;
await sleep(300);
}
return allArtworks.slice(0, 250);
}
async function downloadImage(imageId: string, filepath: string): Promise<boolean> {
const url = `${IIIF_BASE}/${imageId}/full/512,/0/default.jpg`;
try {
const res = await fetch(url, { headers: HEADERS });
if (!res.ok) {
console.error(` Failed: ${res.status}`);
return false;
}
const buffer = Buffer.from(await res.arrayBuffer());
fs.writeFileSync(filepath, buffer);
return true;
} catch (err) {
console.error(` Error:`, err);
return false;
}
}
async function main() {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
console.log("Fetching artworks from API...\n");
const artworks = await fetchAllArtworks();
console.log(`\nFound ${artworks.length} artworks\n`);
const manifest: ManifestItem[] = [];
for (let i = 0; i < artworks.length; i++) {
const artwork = artworks[i];
const filename = `${artwork.image_id}.jpg`;
const filepath = path.join(OUTPUT_DIR, filename);
const item: ManifestItem = {
url: `/artworks/${filename}`,
title: artwork.title,
artist: artwork.artist_display || "Unknown Artist",
year: artwork.date_display,
link: `https://www.artic.edu/artworks/${artwork.id}`,
width: artwork.thumbnail?.width ?? 0,
height: artwork.thumbnail?.height ?? 0,
};
if (fs.existsSync(filepath)) {
console.log(`[${i + 1}/${artworks.length}] Skipping (exists): ${artwork.title.slice(0, 40)}`);
manifest.push(item);
continue;
}
console.log(`[${i + 1}/${artworks.length}] Downloading: ${artwork.title.slice(0, 40)}`);
const success = await downloadImage(artwork.image_id, filepath);
if (success) {
manifest.push(item);
}
await sleep(500);
}
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
console.log(`\nDone! ${manifest.length} images → ${MANIFEST_PATH}`);
}
main().catch(console.error);
함께 쓰는 파일 22개 보기
src/app/index.tsx
import * as React from "react";
import manifest from "~/src/artworks/manifest.json";
import { Frame } from "~/src/frame";
import { InfiniteCanvas } from "~/src/infinite-canvas";
import type { MediaItem } from "~/src/infinite-canvas/types";
import { PageLoader } from "~/src/loader";
export function App() {
const [media] = React.useState<MediaItem[]>(manifest);
const [textureProgress, setTextureProgress] = React.useState(0);
if (!media.length) {
return <PageLoader progress={0} />;
}
return (
<>
<Frame />
<PageLoader progress={textureProgress} />
<InfiniteCanvas media={media} onTextureProgress={setTextureProgress} />
</>
);
}
src/app/style.module.css
.errorContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: sans-serif;
flex-direction: column;
gap: 1rem;
}
.errorMessage {
color: red;
}
.retryButton {
padding: 8px 16px;
cursor: pointer;
}
src/artworks/manifest.json
[
{
"url": "artworks/d9bde524-38b2-4262-3338-e4d06a50746d.jpg",
"type": "image",
"title": "Still Life with Dead Game, Fruits, and Vegetables in a Market",
"artist": "Frans Snyders (Flemish, 1579-1657)",
"year": "1614",
"link": "https://www.artic.eduartworks/62042",
"width": 512,
"height": 335
},
{
"url": "artworks/c8ee825f-bc8c-c76b-1f05-5d692d9a6b47.jpg",
"type": "image",
"title": "The Crucifixion",
"artist": "Francisco de Zurbarán (Spanish, 1598–1664)",
"year": "1627",
"link": "https://www.artic.eduartworks/80084",
"width": 512,
"height": 903
},
{
"url": "artworks/0f951635-312c-0829-451b-553f461c5518.jpg",
"type": "image",
"title": "Cupid Chastised",
"artist": "Bartolomeo Manfredi (Italian, 1582–1622)",
"year": "1613",
"link": "https://www.artic.eduartworks/59847",
"width": 512,
"height": 717
},
{
"url": "artworks/237c25a2-6051-a8e7-1610-a01938d4deab.jpg",
"type": "image",
"title": "The Watermill with the Great Red Roof",
"artist": "Meindert Hobbema (Dutch, 1638–1709)",
"year": "c. 1665",
"link": "https://www.artic.eduartworks/869",
"width": 512,
"height": 377
},
{
"url": "artworks/aa870b0d-5a1b-660a-6dc6-56c12109cf6e.jpg",
"type": "image",
"title": "Landscape with Saint John on Patmos",
"artist": "Nicolas Poussin (French, 1594–1665)",
"year": "1640",
"link": "https://www.artic.eduartworks/5848",
"width": 512,
"height": 379
},
{
"url": "artworks/3eaab3a3-2b47-9fdd-121c-050f6b8d9ccb.jpg",
"type": "image",
"title": "Old Man with a Gold Chain",
"artist": "Rembrandt van Rijn (Dutch, 1606–1669)",
"year": "1631",
"link": "https://www.artic.eduartworks/95998",
"width": 512,
"height": 558
},
{
"url": "artworks/f2021182-1302-f76f-97f1-4e7850030e3b.jpg",
"type": "image",
"title": "Still Life with Game Fowl",
"artist": "Juan Sánchez Cotán (Spanish, 1560–1627)",
"year": "c. 1600–3",
"link": "https://www.artic.eduartworks/84709",
"width": 512,
"height": 393
},
{
"url": "artworks/8111acce-c8ce-2ef3-5f32-61cd63905c7d.jpg",
"type": "image",
"title": "The Battle between the Gods and the Giants",
"artist": "Joachim Antonisz. Wtewael (Dutch, c. 1566–1638)",
"year": "c. 1608",
"link": "https://www.artic.eduartworks/105466",
"width": 512,
"height": 396
},
{
"url": "artworks/7c752046-744f-2f68-482b-a7fd42550f2b.jpg",
"type": "image",
"title": "The Holy Family with Saints Elizabeth and John the Baptist",
"artist": "Peter Paul Rubens (Flemish, 1577–1640)",
"year": "c. 1615",
"link": "https://www.artic.eduartworks/27310",
"width": 512,
"height": 645
},
{
"url": "artworks/8eccb189-92f3-353a-3337-c0778c2680d9.jpg",
"type": "image",
"title": "Salome with the Head of Saint John the Baptist",
"artist": "Guido Reni (Italian, 1575–1642)",
"year": "c. 1639–42",
"link": "https://www.artic.eduartworks/11434",
"width": 512,
"height": 739
},
{
"url": "artworks/50ac2e3e-1d1a-553f-6aeb-5c22cf323a8e.jpg",
"type": "image",
"title": "Landscape with the Ruins of the Castle of Egmond",
"artist": "Jacob van Ruisdael (Dutch, 1628/29-1682)",
"year": "1650–55",
"link": "https://www.artic.eduartworks/60755",
"width": 512,
"height": 372
},
{
"url": "artworks/ec272cba-5f5c-dcc1-00e3-00dfdb042a52.jpg",
"type": "image",
"title": "Annunciation to the Shepherds",
"artist": "After Jacobo Bassano (Italian, c. 1510-1592)",
"year": "c. 1710",
"link": "https://www.artic.eduartworks/109220",
"width": 512,
"height": 640
},
{
"url": "artworks/2e7e28aa-a77b-c7f8-852b-708c1171f928.jpg",
"type": "image",
"title": "View of Delphi with a Procession",
"artist": "Claude Lorrain (Claude Gellée; French, 1600-1682)",
"year": "1673",
"link": "https://www.artic.eduartworks/43145",
"width": 512,
"height": 400
},
{
"url": "artworks/5de78980-17d7-8fb5-83de-7b2ae4e997f2.jpg",
"type": "image",
"title": "Two Cows and a Young Bull beside a Fence in a Meadow",
"artist": "Paulus Potter (Dutch, 1625–1654)",
"year": "1647",
"link": "https://www.artic.eduartworks/146953",
"width": 512,
"height": 680
},
{
"url": "artworks/a34d9d72-c4ec-0750-389e-a01215c9aab0.jpg",
"type": "image",
"title": "Pastoral Landscape with Ruins",
"artist": "Adriaen van de Velde (Dutch, 1636–1672)",
"year": "1664",
"link": "https://www.artic.eduartworks/863",
"width": 512,
"height": 434
},
{
"url": "artworks/c95d58bf-fe9e-e5bb-2c71-ab8bad984759.jpg",
"type": "image",
"title": "Portrait of an Artist",
"artist": "Follower of Frans Hals (Dutch, 1582–1666)",
"year": "1644",
"link": "https://www.artic.eduartworks/862",
"width": 512,
"height": 646
},
{
"url": "artworks/91c51644-871f-cda9-82bb-94f4973ae339.jpg",
"type": "image",
"title": "Young Woman at an Open Half-Door",
"artist": "Rembrandt van Rijn (Dutch, 1606–1669)\nWorkshop of Rembrandt van Rijn (Dutch, 1606–1669)",
"year": "1645",
"link": "https://www.artic.eduartworks/94840",
"width": 512,
"height": 614
},
{
"url": "artworks/a9a3e2fa-f7a4-2713-00ed-909062cb48d7.jpg",
"type": "image",
"title": "A Mother Feeding her Child (The Happy Mother)",
"artist": "Willem van Mieris (Dutch, 1662–1747)",
"year": "1707",
"link": "https://www.artic.eduartworks/497",
"width": 512,
"height": 610
},
{
"url": "artworks/a49c5ada-f461-d7d1-0f1b-468ac577a872.jpg",
"type": "image",
"title": "The Resurrection",
"artist": "Cecco del Caravaggio (Francesco Boneri; Italian, 1588/90–after 1620)",
"year": "c. 1619-20",
"link": "https://www.artic.eduartworks/19336",
"width": 512,
"height": 902
},
{
"url": "artworks/7f1ea423-7538-3bc7-3d4a-0766522ab62f.jpg",
"type": "image",
"title": "The Family Concert",
"artist": "Jan Steen (Dutch, 1626–1679)",
"year": "1666",
"link": "https://www.artic.eduartworks/561",
"width": 512,
"height": 441
},
{
"url": "artworks/4a04138f-43d8-cd9f-5ac4-478cd8828210.jpg",
"type": "image",
"title": "Trompe-l'Oeil Still Life with a Flower Garland and a Curtain",
"artist": "Adriaen van der Spelt (Dutch, 1630–1673)\nFrans van Mieris (Dutch, 1635–1681)",
"year": "1658",
"link": "https://www.artic.eduartworks/66042",
"width": 512,
"height": 369
},
{
"url": "artworks/18adfd19-6c76-989e-36ae-1343aa15701b.jpg",
"type": "image",
"title": "The Guardhouse",
"artist": "David Teniers the Younger (Flemish, 1610-1690)",
"year": "c. 1645",
"link": "https://www.artic.eduartworks/867",
"width": 512,
"height": 670
},
{
"url": "artworks/0b0b5c15-0633-376b-278e-2660f09b582a.jpg",
"type": "image",
"title": "The Wedding at Cana",
"artist": "Giuseppe Maria Crespi (Italian, 1665–1747)",
"year": "c. 1686",
"link": "https://www.artic.eduartworks/2166",
"width": 512,
"height": 397
},
{
"url": "artworks/75886aaa-002a-2047-a05c-baf73f0ac093.jpg",
"type": "image",
"title": "The Feast in the House of Simon",
"artist": "El Greco (Doménikos Theotokópoulos; Greek, active in Spain, 1541–1614)\nWorkshop of El Greco (Doménikos Theotokópoulos; Greek, active in Spain, 1541–1614)",
"year": "c. 1608–14",
"link": "https://www.artic.eduartworks/65509",
"width": 512,
"height": 731
},
{
"url": "artworks/68452725-eba5-06e3-46d2-50c678a5d672.jpg",
"type": "image",
"title": "Kitchen Scene",
"artist": "Diego Velázquez (Spanish, 1599–1660)",
"year": "1618–20",
"link": "https://www.artic.eduartworks/21934",
"width": 512,
"height": 265
},
{
"url": "artworks/b703b636-976c-b35b-8a8e-f8df3f5ba003.jpg",
"type": "image",
"title": "Saint Francis Kneeling in Meditation",
"artist": "El Greco (Doménikos Theotokópoulos; Greek, active in Spain, 1541–1614)",
"year": "c. 1595–c. 1600",
"link": "https://www.artic.eduartworks/21907",
"width": 512,
"height": 641
},
{
"url": "artworks/c612af6e-f630-487e-5b85-3a15381065fc.jpg",
"type": "image",
"title": "A Witches' Sabbath",
"artist": "Cornelis Saftleven (Dutch, 1607–1681)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/53495",
"width": 512,
"height": 349
},
{
"url": "artworks/11f33728-685f-c85c-2326-cd0e42536044.jpg",
"type": "image",
"title": "Abraham's Sacrifice of Isaac",
"artist": "David Teniers the Younger (Flemish, 1610–1690)\nAfter Paolo Veronese (Italian, 1528–1588)",
"year": "1654–56",
"link": "https://www.artic.eduartworks/111649",
"width": 512,
"height": 350
},
{
"url": "artworks/52098776-6e2e-9683-e258-7b1cec8660dd.jpg",
"type": "image",
"title": "Saint Martin and the Beggar",
"artist": "El Greco (Doménikos Theotokópoulos; Greek, active in Spain, 1541–1614)",
"year": "c. 1597–c. 1600",
"link": "https://www.artic.eduartworks/67362",
"width": 512,
"height": 896
},
{
"url": "artworks/086f1a92-1a07-1805-3321-d238009fcda0.jpg",
"type": "image",
"title": "The Terrace",
"artist": "Dutch; Delft",
"year": "c. 1660",
"link": "https://www.artic.eduartworks/62460",
"width": 512,
"height": 631
},
{
"url": "artworks/574695d5-6bf3-fe58-a1d5-e6cbb5e10c77.jpg",
"type": "image",
"title": "Still Life",
"artist": "Pieter Claesz (Dutch, 1596/97–1660)",
"year": "c. 1625",
"link": "https://www.artic.eduartworks/21682",
"width": 512,
"height": 314
},
{
"url": "artworks/d51eb11c-da63-0888-0a59-6a5ef0631142.jpg",
"type": "image",
"title": "Penitent Saint Peter",
"artist": "Jusepe de Ribera (Spanish, 1588–1652)",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/120172",
"width": 512,
"height": 665
},
{
"url": "artworks/06f3c07e-c180-00ef-1f0e-5f1325461459.jpg",
"type": "image",
"title": "Jupiter Rebuked by Venus",
"artist": "Abraham Janssens (Flemish, c. 1575-1632)",
"year": "c. 1612",
"link": "https://www.artic.eduartworks/64996",
"width": 512,
"height": 419
},
{
"url": "artworks/d55ad474-aa3c-6881-b10d-06a9ac6e05b7.jpg",
"type": "image",
"title": "The Music Lesson",
"artist": "Gerard ter Borch (Dutch, 1617-1681)",
"year": "c. 1670",
"link": "https://www.artic.eduartworks/512",
"width": 512,
"height": 646
},
{
"url": "artworks/a8fd8a92-d5fd-bb98-5d70-f970ab1788b8.jpg",
"type": "image",
"title": "The Capture of Samson",
"artist": "Peter Paul Rubens (Flemish, 1577-1640)",
"year": "1609–10",
"link": "https://www.artic.eduartworks/8953",
"width": 512,
"height": 388
},
{
"url": "artworks/c6c6a21a-b985-35d9-b0ef-a2b798ad9fba.jpg",
"type": "image",
"title": "The Music Lesson",
"artist": "Jacob Ochtervelt (Dutch, 1634-1682)",
"year": "1671",
"link": "https://www.artic.eduartworks/16398",
"width": 512,
"height": 636
},
{
"url": "artworks/571dac0d-3f2e-3463-814a-e56951bbe722.jpg",
"type": "image",
"title": "A View of Vianen with a Herdsman and Cattle by a River",
"artist": "Aelbert Cuyp (Dutch, 1621–1691)",
"year": "c. 1643–c. 1645",
"link": "https://www.artic.eduartworks/181702",
"width": 512,
"height": 373
},
{
"url": "artworks/c01c7f64-0098-a2d5-56c2-3eb0b28f68ae.jpg",
"type": "image",
"title": "The Abduction of the Sabine Women",
"artist": "Luca Giordano (Italian, 1632–1705)",
"year": "c. 1675",
"link": "https://www.artic.eduartworks/111620",
"width": 512,
"height": 441
},
{
"url": "artworks/666d318a-dc2a-0e58-f734-961ab7c2930a.jpg",
"type": "image",
"title": "Ecce Agnus Dei",
"artist": "Bartolomé Estéban Murillo (Spanish, 1617–1682)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/11392",
"width": 512,
"height": 754
},
{
"url": "artworks/107dd824-9ae2-b091-9787-5901d5e300da.jpg",
"type": "image",
"title": "The Ecstasy of Saint Francis",
"artist": "Giovanni Baglione (Italian, 1566–1643)",
"year": "1601",
"link": "https://www.artic.eduartworks/160030",
"width": 512,
"height": 677
},
{
"url": "artworks/29163982-8310-36bb-3530-2b5000ce7290.jpg",
"type": "image",
"title": "Kitchen Still Life",
"artist": "Attributed to Paolo Antonio Barbieri (Italian, 1603–1649)",
"year": "c. 1640",
"link": "https://www.artic.eduartworks/19333",
"width": 512,
"height": 422
},
{
"url": "artworks/5ee19981-a8f5-1306-3ec5-eb79c14ac1a6.jpg",
"type": "image",
"title": "The Wedding of Peleus and Thetis",
"artist": "Peter Paul Rubens (Flemish, 1577–1640)",
"year": "1636",
"link": "https://www.artic.eduartworks/59956",
"width": 512,
"height": 315
},
{
"url": "artworks/ae144f13-c81e-804d-fe08-e5783dac27ef.jpg",
"type": "image",
"title": "An Elegant Company",
"artist": "Pieter Codde (Dutch, 1599-1678)",
"year": "1632",
"link": "https://www.artic.eduartworks/16347",
"width": 512,
"height": 321
},
{
"url": "artworks/293ab61e-5536-c2d3-936a-4116f027260c.jpg",
"type": "image",
"title": "Still Life with Monkey, Fruits, and Flowers",
"artist": "Jean Baptiste Oudry (French, 1686–1755)",
"year": "1724",
"link": "https://www.artic.eduartworks/94126",
"width": 512,
"height": 497
},
{
"url": "artworks/f6eddea6-5789-b6f9-315c-50a08a7c4adc.jpg",
"type": "image",
"title": "Fête champêtre (Pastoral Gathering)",
"artist": "Jean Antoine Watteau (French, 1684–1721)\nAssisted by Jean-Baptiste Pater (French, 1695-1736)",
"year": "1718–21",
"link": "https://www.artic.eduartworks/107938",
"width": 512,
"height": 368
},
{
"url": "artworks/d065e4c4-6aef-3d16-b3ac-63883282e8f0.jpg",
"type": "image",
"title": "Meekness",
"artist": "Eustache Le Sueur (French, 1616–1655)",
"year": "1650",
"link": "https://www.artic.eduartworks/47159",
"width": 512,
"height": 823
},
{
"url": "artworks/44fce132-4eb4-7541-06c5-1b6f61c32912.jpg",
"type": "image",
"title": "Christ Washing the Feet of His Disciples",
"artist": "Nicolas Bertin (French, 1668–1736)",
"year": "1720–30",
"link": "https://www.artic.eduartworks/58052",
"width": 512,
"height": 347
},
{
"url": "artworks/eba4ff21-3b90-801b-0f0a-1bd913b49049.jpg",
"type": "image",
"title": "Adam and Eve in Paradise",
"artist": "Francesco Solimena (Italian, 1657–1747)",
"year": "c. 1700",
"link": "https://www.artic.eduartworks/101077",
"width": 512,
"height": 659
},
{
"url": "artworks/da3e9da7-09ed-412c-16b0-8ceedd38571f.jpg",
"type": "image",
"title": "Saint John the Baptist in the Wilderness",
"artist": "Diego Velázquez (Spanish, 1599–1660)",
"year": "c. 1622",
"link": "https://www.artic.eduartworks/6831",
"width": 512,
"height": 573
},
{
"url": "artworks/8f899167-2faf-bd0d-cf9c-9ca2fcb07a4b.jpg",
"type": "image",
"title": "Helena Tromper Du Bois",
"artist": "Attributed to Anthony van Dyck (Flemish, 1599–1641)",
"year": "c. 1631",
"link": "https://www.artic.eduartworks/866",
"width": 512,
"height": 626
},
{
"url": "artworks/40bf35ab-2898-650e-9e27-d38c4cf39a30.jpg",
"type": "image",
"title": "The Dreamer (La Rêveuse)",
"artist": "Jean Antoine Watteau (French, 1684–1721)",
"year": "1712–14",
"link": "https://www.artic.eduartworks/12007",
"width": 512,
"height": 699
},
{
"url": "artworks/8eed2fbf-5090-4fb2-8f7a-74e97c8e6eaf.jpg",
"type": "image",
"title": "Homer Dictating",
"artist": "Pier Francesco Mola (Italian, 1612–1666)",
"year": "1660–65",
"link": "https://www.artic.eduartworks/5304",
"width": 512,
"height": 372
},
{
"url": "artworks/5a47a0b8-d21b-b5ae-c8f2-3b99476e6e75.jpg",
"type": "image",
"title": "Coast Scene",
"artist": "Attributed to Reinier Nooms (Reinier Zeeman; Dutch, c. 1623-c. 1668)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/495",
"width": 512,
"height": 569
},
{
"url": "artworks/ac23f590-c2b3-0560-5058-ae4cc59b890d.jpg",
"type": "image",
"title": "Virgin and Child Adored by Saint Francis",
"artist": "Francesco Albani (Italian, 1578-1660)",
"year": "c. 1606",
"link": "https://www.artic.eduartworks/15305",
"width": 512,
"height": 649
},
{
"url": "artworks/19d1878d-943a-d47f-c32e-2ce56f917bc2.jpg",
"type": "image",
"title": "Allegory of Venus and Cupid",
"artist": "Imitator of Titian (Italian, c. 1488–1576)",
"year": "c. 1600",
"link": "https://www.artic.eduartworks/46314",
"width": 512,
"height": 429
},
{
"url": "artworks/8df04a6e-cc79-491b-86d4-388c2b33d888.jpg",
"type": "image",
"title": "Saint Francis",
"artist": "Peter Paul Rubens (Flemish, 1577-1640)",
"year": "c. 1615",
"link": "https://www.artic.eduartworks/100342",
"width": 512,
"height": 712
},
{
"url": "artworks/7904b9d2-675f-7aeb-88c3-e29cd519b7be.jpg",
"type": "image",
"title": "Portrait of a Noblewoman Dressed in Mourning",
"artist": "Jacopo da Empoli (Jacopo Chimenti; Italian, 1551–1640)",
"year": "c. 1600",
"link": "https://www.artic.eduartworks/11390",
"width": 512,
"height": 931
},
{
"url": "artworks/b30b708a-be47-c3cc-f3af-2a6798b7b088.jpg",
"type": "image",
"title": "The Entombment",
"artist": "Guercino (Giovanni Francesco Barbieri; Italian, 1591-1666)",
"year": "1656",
"link": "https://www.artic.eduartworks/86323",
"width": 512,
"height": 336
},
{
"url": "artworks/beeba230-022f-449a-a2fd-c0cf6c47d232.jpg",
"type": "image",
"title": "Christ Receiving the Children",
"artist": "Sébastien Bourdon (French, 1616–1671)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/9672",
"width": 512,
"height": 385
},
{
"url": "artworks/9e4bfbcc-176f-3dac-4529-f67c9a98065c.jpg",
"type": "image",
"title": "Flowers and Fruit in a Chinese Bowl",
"artist": "Juan de Zurbarán (Spanish, 1620–1649)",
"year": "c. 1645",
"link": "https://www.artic.eduartworks/111059",
"width": 512,
"height": 386
},
{
"url": "artworks/5fa784d4-f5d4-cc88-2f8c-0d7bad4945c4.jpg",
"type": "image",
"title": "Saint Romanus of Antioch and Saint Barulas",
"artist": "Francisco de Zurbarán (Spanish, 1598–1664)",
"year": "1638",
"link": "https://www.artic.eduartworks/61665",
"width": 512,
"height": 684
},
{
"url": "artworks/da256af1-651b-72a4-2d0f-59cf461f29c7.jpg",
"type": "image",
"title": "Theodosius Repulsed from the Church by Saint Ambrose",
"artist": "Alessandro Magnasco (Italian, 1667–1749)",
"year": "c. 1705",
"link": "https://www.artic.eduartworks/12891",
"width": 512,
"height": 361
},
{
"url": "artworks/55cd82ad-be40-3ccc-11a5-cdbcc6c70d20.jpg",
"type": "image",
"title": "The Temptation of the Magdalene",
"artist": "Jacob Jordaens (Flemish, 1593-1678)",
"year": "c. 1616",
"link": "https://www.artic.eduartworks/111613",
"width": 512,
"height": 689
},
{
"url": "artworks/599e0546-ed58-0224-a623-39a8e3b51ef8.jpg",
"type": "image",
"title": "Bouquet of Flowers in an Earthenware Vase",
"artist": "Attributed to Jan Brueghel the Elder (Flemish, 1568–1625)",
"year": "c. 1610",
"link": "https://www.artic.eduartworks/64029",
"width": 512,
"height": 754
},
{
"url": "artworks/71d54071-9aa9-b702-99ba-914b1b23278e.jpg",
"type": "image",
"title": "The Continence of Scipio",
"artist": "Sebastiano Ricci (Italian, 1659–1734)",
"year": "c. 1706",
"link": "https://www.artic.eduartworks/33249",
"width": 512,
"height": 397
},
{
"url": "artworks/a3ec9571-7c26-2d6a-3c60-28872828ddcf.jpg",
"type": "image",
"title": "Portrait of a Young Lady",
"artist": "Paulus Moreelse (Dutch, 1571–1638)",
"year": "c. 1620",
"link": "https://www.artic.eduartworks/80539",
"width": 512,
"height": 635
},
{
"url": "artworks/2a6d3782-8d79-e526-e382-b482579aff3d.jpg",
"type": "image",
"title": "Aeneas Rescuing Anchises from Burning Troy",
"artist": "Hendrick van Steenwijck the Younger (Flemish, c. 1580-1640)",
"year": "c. 1610",
"link": "https://www.artic.eduartworks/88619",
"width": 512,
"height": 369
},
{
"url": "artworks/c5e50da8-6bff-ea02-ed3c-c154b356cceb.jpg",
"type": "image",
"title": "Apollo Granting Phaeton Permission to Drive the Chariot of the Sun",
"artist": "Johann Michael Rottmayr (Austrian, 1654–1730)",
"year": "c. 1695",
"link": "https://www.artic.eduartworks/20155",
"width": 512,
"height": 330
},
{
"url": "artworks/ab49fdd2-5acc-981e-f11e-c631bbd5487f.jpg",
"type": "image",
"title": "Diana and Endymion",
"artist": "Johann Michael Rottmayr (Austrian, 1654–1730)",
"year": "c. 1695",
"link": "https://www.artic.eduartworks/12876",
"width": 512,
"height": 329
},
{
"url": "artworks/96458291-2c6c-76ba-4be9-dca201687edf.jpg",
"type": "image",
"title": "Italian Landscape with Travelers",
"artist": "Jan Both (Dutch, c. 1618–1652)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/69041",
"width": 512,
"height": 412
},
{
"url": "artworks/87d426ca-4537-1386-50bf-6c2d5a5d7d17.jpg",
"type": "image",
"title": "Still Life with a Basket of Fruit and a Bunch of Asparagus",
"artist": "Louise Moillon (French, 1610–1696)",
"year": "1630",
"link": "https://www.artic.eduartworks/62450",
"width": 512,
"height": 369
},
{
"url": "artworks/8b73e4fa-c454-3744-3c11-81ada02ac978.jpg",
"type": "image",
"title": "Crèche",
"artist": "Neapolitan",
"year": "1725–75, with later additions",
"link": "https://www.artic.eduartworks/217536",
"width": 512,
"height": 345
},
{
"url": "artworks/747149cf-b1eb-4877-52cb-49ba2341a72b.jpg",
"type": "image",
"title": "Merrymakers in an Inn",
"artist": "Adriaen van Ostade (Dutch, 1610–1685)",
"year": "1674",
"link": "https://www.artic.eduartworks/117476",
"width": 512,
"height": 584
},
{
"url": "artworks/b246d3eb-8bfe-a031-4848-e00c053546ea.jpg",
"type": "image",
"title": "Fishing Boats in a Calm",
"artist": "Jan van de Cappelle (Dutch, 1626–1679)",
"year": "1651",
"link": "https://www.artic.eduartworks/16343",
"width": 512,
"height": 407
},
{
"url": "artworks/15a87438-df7b-8fe1-203f-e0b4d36236a0.jpg",
"type": "image",
"title": "The Adoration of the Eucharist",
"artist": "Peter Paul Rubens (Flemish, 1577–1640)",
"year": "c. 1626",
"link": "https://www.artic.eduartworks/25790",
"width": 512,
"height": 507
},
{
"url": "artworks/b7a594db-5c71-253b-aedc-4226d8661f11.jpg",
"type": "image",
"title": "An Old Man in a Fur Cap",
"artist": "Karel van der Pluym (Dutch, 1625-1672)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/72190",
"width": 512,
"height": 675
},
{
"url": "artworks/f8a1f3b3-4506-d336-aceb-5aec6c4a2e3d.jpg",
"type": "image",
"title": "The Denial of Saint Peter",
"artist": "Hendrick ter Brugghen (Dutch, 1588-1629)",
"year": "c. 1626",
"link": "https://www.artic.eduartworks/30901",
"width": 512,
"height": 375
},
{
"url": "artworks/0905c8a0-b2f1-b534-fa37-0f1aa016bec5.jpg",
"type": "image",
"title": "Interior of the Oude Kerk, Delft",
"artist": "Emanuel de Witte (Dutch, c. 1617–1691/92)",
"year": "c. 1680",
"link": "https://www.artic.eduartworks/43211",
"width": 512,
"height": 644
},
{
"url": "artworks/cec52f00-87a3-1e81-a663-520004ab7259.jpg",
"type": "image",
"title": "Panthea, Cyrus, and Araspas",
"artist": "Laurent de La Hyre (French, 1606-1656)",
"year": "1631-34",
"link": "https://www.artic.eduartworks/111418",
"width": 512,
"height": 733
},
{
"url": "artworks/118eacd7-3e92-a96d-7b0e-160eb0c0bf20.jpg",
"type": "image",
"title": "Polycrates' Crucifixion",
"artist": "Salvator Rosa (Italian, 1615–1673)",
"year": "1664",
"link": "https://www.artic.eduartworks/44829",
"width": 512,
"height": 380
},
{
"url": "artworks/fc76abe6-5252-a690-dd1c-6e2480c0ffbb.jpg",
"type": "image",
"title": "Fishing Boats off an Estuary",
"artist": "Jan van Goyen (Dutch, 1596–1656)",
"year": "1633",
"link": "https://www.artic.eduartworks/16370",
"width": 512,
"height": 312
},
{
"url": "artworks/d67f1cfe-c16f-0b19-1acb-4d915f158991.jpg",
"type": "image",
"title": "Christ Washing the Disciples' Feet",
"artist": "Attributed to Jan Lievens (Dutch, 1607–1674)",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/19322",
"width": 512,
"height": 398
},
{
"url": "artworks/43325575-c0a1-67c2-4eb5-5fcabafd098e.jpg",
"type": "image",
"title": "Heraclitus, the Weeping Philosopher",
"artist": "Spanish",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/111428",
"width": 512,
"height": 437
},
{
"url": "artworks/5a7c5a7a-39f2-a172-9d98-2544c49d39f6.jpg",
"type": "image",
"title": "Judith with the Head of Holofernes",
"artist": "Felice Ficherelli (Italian, 1605–1669)",
"year": "c. 1665",
"link": "https://www.artic.eduartworks/36525",
"width": 512,
"height": 670
},
{
"url": "artworks/5403c6fd-fb8b-c22c-abaa-9c30eb3710f4.jpg",
"type": "image",
"title": "Job",
"artist": "Spanish; possibly Seville",
"year": "c. 1618–c. 1630",
"link": "https://www.artic.eduartworks/87515",
"width": 512,
"height": 646
},
{
"url": "artworks/9420990e-602d-5b82-d82c-538c25bc7e53.jpg",
"type": "image",
"title": "Portrait of Isabella of Bourbon",
"artist": "Follower of Peter Paul Rubens (Flemish, 1577–1640)",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/15698",
"width": 512,
"height": 668
},
{
"url": "artworks/2f7bd26c-ffdf-0b4a-f1e2-8acd09554023.jpg",
"type": "image",
"title": "Assumption of the Virgin",
"artist": "Marcellus Coffermans (Netherlandish, 1524–1581)",
"year": "16th century",
"link": "https://www.artic.eduartworks/102088",
"width": 512,
"height": 772
},
{
"url": "artworks/51b5e908-743a-04a2-b080-9f9f2a33e502.jpg",
"type": "image",
"title": "Marie de’ Medici",
"artist": "Frans Pourbus the Younger (Flemish, 1569-1622)",
"year": "1616",
"link": "https://www.artic.eduartworks/78591",
"width": 512,
"height": 674
},
{
"url": "artworks/3b06a501-7b91-07b2-80f1-a683cbfeaad9.jpg",
"type": "image",
"title": "Landscape with Hunters",
"artist": "Paul Bril (Flemish, 1553/54–1626)",
"year": "1619",
"link": "https://www.artic.eduartworks/209969",
"width": 512,
"height": 346
},
{
"url": "artworks/8f22ca73-94f1-a426-6656-8ab825657c25.jpg",
"type": "image",
"title": "Christ on the Cross with Mary Magdalene",
"artist": "Follower of Simon Vouet (French, 1590–1649)",
"year": "c. 1645",
"link": "https://www.artic.eduartworks/53061",
"width": 512,
"height": 688
},
{
"url": "artworks/26ec4ba5-0571-aded-9bbb-601973682b07.jpg",
"type": "image",
"title": "Polycrates and the Fisherman",
"artist": "Salvator Rosa (Italian, 1615–1673)",
"year": "1664",
"link": "https://www.artic.eduartworks/44826",
"width": 512,
"height": 379
},
{
"url": "artworks/389e3222-0ff0-9e88-4b84-80e78fbb6b8d.jpg",
"type": "image",
"title": "Margaret of Austria, Queen of Spain",
"artist": "Andrés López Polanco (Spanish, active 1608, died 1641)",
"year": "c. 1610",
"link": "https://www.artic.eduartworks/111637",
"width": 512,
"height": 937
},
{
"url": "artworks/52894b83-8d40-5a19-5c98-e28befc87ad1.jpg",
"type": "image",
"title": "Portrait of a Young Woman",
"artist": "Aert de Gelder (Dutch, 1645–1727)",
"year": "c. 1690",
"link": "https://www.artic.eduartworks/105887",
"width": 512,
"height": 647
},
{
"url": "artworks/58f58363-f567-0e49-7956-d49400721b1c.jpg",
"type": "image",
"title": "St. Albert of Louvain",
"artist": "Peter Paul Rubens (Flemish, 1577-1640)",
"year": "1620",
"link": "https://www.artic.eduartworks/28874",
"width": 512,
"height": 390
},
{
"url": "artworks/f0447c50-f4c9-ecb2-2695-22669e1469cd.jpg",
"type": "image",
"title": "St. Gerardo Sagredo, Bishop of Csanád",
"artist": "Bernardo Strozzi (Italian, 1581–1664)",
"year": "1633",
"link": "https://www.artic.eduartworks/8104",
"width": 512,
"height": 710
},
{
"url": "artworks/5f1fda6c-f116-c251-1cee-c6bad53d8d18.jpg",
"type": "image",
"title": "Mountain Road with Travelers",
"artist": "Joos de Momper, II (Flemish, 1564-1635)",
"year": "c. 1615",
"link": "https://www.artic.eduartworks/16340",
"width": 512,
"height": 330
},
{
"url": "artworks/439d68fb-062c-8079-0ab9-2fc9d2690275.jpg",
"type": "image",
"title": "Resurrection of Christ",
"artist": "Samuel van Hoogstraten (Dutch, 1627-1678)",
"year": "c. 1665",
"link": "https://www.artic.eduartworks/31173",
"width": 512,
"height": 640
},
{
"url": "artworks/60a6eba1-7d17-796d-df04-7d1f8b0f1f94.jpg",
"type": "image",
"title": "Arcadian Landscape with Figures",
"artist": "Alessandro Magnasco (Italian, 1667–1749)",
"year": "c. 1700",
"link": "https://www.artic.eduartworks/4092",
"width": 512,
"height": 393
},
{
"url": "artworks/ec7c2bc7-7e9e-eb36-6edf-b5198533da10.jpg",
"type": "image",
"title": "Old Man Lighting a Pipe",
"artist": "Johann Carl Loth (German, 1623–1698)",
"year": "c. 1660",
"link": "https://www.artic.eduartworks/70118",
"width": 512,
"height": 571
},
{
"url": "artworks/1b605198-afe5-1052-3c7b-0c08734238e1.jpg",
"type": "image",
"title": "Landscape with Rock and Fortress",
"artist": "Attributed to Domenico Gargiulo (Micco Spadaro; Italian, 1609–1675)",
"year": "c. 1645",
"link": "https://www.artic.eduartworks/8097",
"width": 512,
"height": 324
},
{
"url": "artworks/a19aa3c2-56aa-cfd2-d92a-dabb5ba5b488.jpg",
"type": "image",
"title": "Virgin and Child with Saint Elizabeth and the Infant Saint John the Baptist",
"artist": "Jacques Blanchard (French, 1600–1638)",
"year": "c. 1628",
"link": "https://www.artic.eduartworks/16494",
"width": 512,
"height": 389
},
{
"url": "artworks/9c2c809d-7637-84b0-747c-0520fa16e901.jpg",
"type": "image",
"title": "The Abduction of Europa",
"artist": "David Teniers the Younger (Flemish, 1610–1690)\nAfter artist unknown (Venetian, active 16th century)",
"year": "1654–56",
"link": "https://www.artic.eduartworks/111645",
"width": 512,
"height": 341
},
{
"url": "artworks/50ac4789-b167-eb84-b8da-3b3ddc1c095d.jpg",
"type": "image",
"title": "The Resurrection",
"artist": "Bartholomeus Breenbergh (Dutch, 1598–1657)",
"year": "c. 1635",
"link": "https://www.artic.eduartworks/28173",
"width": 512,
"height": 863
},
{
"url": "artworks/730ab90f-45c3-ca32-77d0-a43858cdbbab.jpg",
"type": "image",
"title": "Portrait of a Lady",
"artist": "Frans Hals (Dutch, 1582–1666)",
"year": "1627",
"link": "https://www.artic.eduartworks/105945",
"width": 512,
"height": 633
},
{
"url": "artworks/474d2283-78fc-1dbe-1d45-f67908f69033.jpg",
"type": "image",
"title": "Christ on the Cross",
"artist": "Flemish",
"year": "c. 1580–c. 1600",
"link": "https://www.artic.eduartworks/56133",
"width": 512,
"height": 644
},
{
"url": "artworks/ecc57db2-c119-fa97-9c40-f865607d0e9e.jpg",
"type": "image",
"title": "The Abduction of Europa",
"artist": "David Teniers the Younger (Flemish, 1610–1690)\nAfter Titian (Italian, c. 1488–1576)",
"year": "1654–56",
"link": "https://www.artic.eduartworks/111647",
"width": 512,
"height": 342
},
{
"url": "artworks/a3bfd218-9b88-f59b-5ade-f39b45f3ec4f.jpg",
"type": "image",
"title": "Still Life with Ostrich Egg Cup and the Whitfield Heirlooms",
"artist": "Pieter Gerritsz. van Roestraeten (Dutch, 1630–1700)",
"year": "c. 1670",
"link": "https://www.artic.eduartworks/198905",
"width": 512,
"height": 436
},
{
"url": "artworks/3cef23a8-7fca-59ce-a95d-4c27cd3d8f21.jpg",
"type": "image",
"title": "Self-Portrait",
"artist": "Nicolas de Largillière (French, 1656–1746)",
"year": "c. 1725",
"link": "https://www.artic.eduartworks/69003",
"width": 512,
"height": 646
},
{
"url": "artworks/ad3f403a-62e5-3d91-3a8c-ecc5f4fc99e5.jpg",
"type": "image",
"title": "Democritus, the Laughing Philosopher",
"artist": "Spanish",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/111427",
"width": 512,
"height": 439
},
{
"url": "artworks/91f56173-2f20-70fa-c0ac-f00bed4268d0.jpg",
"type": "image",
"title": "Landscape with a Herdsman and Goats",
"artist": "Gaspard Dughet (French, 1615-1675)",
"year": "c. 1635",
"link": "https://www.artic.eduartworks/45829",
"width": 512,
"height": 285
},
{
"url": "artworks/c68a078e-b4c1-7ea8-d37a-a30c7c28f94e.jpg",
"type": "image",
"title": "The Oude Kerk, Delft",
"artist": "Cornelis de Man (Dutch, 1621–1706)",
"year": "c. 1665",
"link": "https://www.artic.eduartworks/24674",
"width": 512,
"height": 431
},
{
"url": "artworks/273ecd81-2caf-86ad-b454-c83622a96ef5.jpg",
"type": "image",
"title": "Virgin and Child with Angels",
"artist": "Giulio Cesare Procaccini (Italian, 1574–1625)",
"year": "c. 1610",
"link": "https://www.artic.eduartworks/32091",
"width": 512,
"height": 718
},
{
"url": "artworks/6f9efc13-46f6-32a6-ac9a-80447870fb5c.jpg",
"type": "image",
"title": "Portrait of Thomas Bulwer",
"artist": "Gerard van Soest (Dutch, c. 1605–1681)",
"year": "1654",
"link": "https://www.artic.eduartworks/15714",
"width": 512,
"height": 600
},
{
"url": "artworks/3dea7add-50e5-fe2c-8f05-2fb433a9e4ef.jpg",
"type": "image",
"title": "The Rommel-Pot Player",
"artist": "Workshop of Frans Hals (Dutch, 1582–1666)",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/59894",
"width": 512,
"height": 665
},
{
"url": "artworks/b0b312cf-6b99-fc0a-e561-c60a700cf0cc.jpg",
"type": "image",
"title": "The Vision of Saint Francis",
"artist": "Ludovico Carracci (Italian, 1555–1619)",
"year": "c. 1602",
"link": "https://www.artic.eduartworks/203330",
"width": 512,
"height": 667
},
{
"url": "artworks/98d8c11a-21be-6ece-ad42-c34aaa4f8005.jpg",
"type": "image",
"title": "Girl Standing before a Mirror",
"artist": "Caspar Netscher (Dutch, 1639–1684)",
"year": "1668",
"link": "https://www.artic.eduartworks/16539",
"width": 512,
"height": 661
},
{
"url": "artworks/e99fba95-e252-1cfc-4204-55d2f3143a5e.jpg",
"type": "image",
"title": "Christ in the Storm",
"artist": "Heinrich Jansen (Danish, 1625–1667)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/40507",
"width": 512,
"height": 353
},
{
"url": "artworks/a2450337-afed-2797-17e7-2e1f2158235c.jpg",
"type": "image",
"title": "Portrait of Philip IV",
"artist": "Workshop of Diego Velázquez (Spanish, 1599-1660)",
"year": "c. 1632",
"link": "https://www.artic.eduartworks/41211",
"width": 512,
"height": 861
},
{
"url": "artworks/14dd6a9c-11d0-0ed1-abe5-91444e7715f9.jpg",
"type": "image",
"title": "Sir Andrew Fountaine",
"artist": "Jonathan Richardson the Elder (English, 1667–1745)",
"year": "c. 1710",
"link": "https://www.artic.eduartworks/15486",
"width": 512,
"height": 624
},
{
"url": "artworks/ea80056a-5db8-b4b8-6110-3d12902b99cc.jpg",
"type": "image",
"title": "Jove Casts his Thunderbolts at the Rebellious Giants",
"artist": "Johann Michael Rottmayr (Austrian, 1654–1730)",
"year": "c. 1695",
"link": "https://www.artic.eduartworks/12883",
"width": 512,
"height": 331
},
{
"url": "artworks/5372920d-7536-64ec-bf48-264e084635f8.jpg",
"type": "image",
"title": "Portrait of a Young Girl",
"artist": "Attributed to Pieter Dubordieu (Dutch, c. 1609–after 1678)",
"year": "1633–35",
"link": "https://www.artic.eduartworks/20887",
"width": 512,
"height": 619
},
{
"url": "artworks/5c9652f0-33b4-7b2b-2be5-007d2e6ea214.jpg",
"type": "image",
"title": "Hercules and Hesione",
"artist": "Attributed to Bartolomeo Salvestrini (Italian, died 1630)",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/50309",
"width": 512,
"height": 635
},
{
"url": "artworks/28a43a52-4bf2-52a5-3977-65a385564955.jpg",
"type": "image",
"title": "Mercury Rescues the Disguised Io after Beheading Argus",
"artist": "Johann Michael Rottmayr (Austrian, 1654–1730)",
"year": "c. 1695",
"link": "https://www.artic.eduartworks/12875",
"width": 512,
"height": 329
},
{
"url": "artworks/7f41bfb7-ea0c-f173-bb1f-19def17f06aa.jpg",
"type": "image",
"title": "The Madonna with the Seven Founders of the Servite Order",
"artist": "Agostino Masucci (Italian, 1690–1768)",
"year": "c. 1728",
"link": "https://www.artic.eduartworks/94125",
"width": 512,
"height": 760
},
{
"url": "artworks/63a98f96-93d4-a6a7-4309-c24818135b9f.jpg",
"type": "image",
"title": "Saint John the Baptist in the Wilderness",
"artist": "Denys Calvaert (Flemish, c. 1540–1619)",
"year": "c. 1610",
"link": "https://www.artic.eduartworks/152019",
"width": 512,
"height": 666
},
{
"url": "artworks/f6103cb8-ee9b-2334-96f5-9ac8e07f8b78.jpg",
"type": "image",
"title": "Saint Catherine Delivered from the Wheel",
"artist": "Northern Spanish",
"year": "c. 1395",
"link": "https://www.artic.eduartworks/12683",
"width": 512,
"height": 555
},
{
"url": "artworks/2ee1ea7b-4ad8-e8fd-6821-394bab14c395.jpg",
"type": "image",
"title": "Woman Looking For Fleas",
"artist": "Attributed to Giuseppe Maria Crespi (Italian, 1665–1747)",
"year": "c. 1715",
"link": "https://www.artic.eduartworks/59858",
"width": 512,
"height": 652
},
{
"url": "artworks/e9855650-5773-cddc-34ac-7240cd634a2f.jpg",
"type": "image",
"title": "Holy Family with the Infant St. John",
"artist": "Francesco Trevisani (Italian, 1656–1746)",
"year": "c. 1700",
"link": "https://www.artic.eduartworks/52177",
"width": 512,
"height": 685
},
{
"url": "artworks/14f54f04-6bf4-f75f-7a85-2317578071fc.jpg",
"type": "image",
"title": "Virgin and Child",
"artist": "After Giovanni Bellini (Italian, 1428/30–1516)",
"year": "16th century or later",
"link": "https://www.artic.eduartworks/14896",
"width": 512,
"height": 656
},
{
"url": "artworks/bc8a4129-fae7-c0ed-aad8-73ac5e638bd6.jpg",
"type": "image",
"title": "Lady Playing with a Dog",
"artist": "Eglon van der Neer (Dutch, 1634–1703)",
"year": "c. 1670",
"link": "https://www.artic.eduartworks/49068",
"width": 512,
"height": 658
},
{
"url": "artworks/b68961a6-eed0-da9d-2196-fe5732503064.jpg",
"type": "image",
"title": "Wooded Landscape with Cottage and Horseman",
"artist": "Meindert Hobbema (Dutch, 1638–1709)",
"year": "1663",
"link": "https://www.artic.eduartworks/4770",
"width": 512,
"height": 398
},
{
"url": "artworks/ff93ff9f-3a49-581d-f6a3-01c078ff987e.jpg",
"type": "image",
"title": "Jacob's Farewell to Benjamin",
"artist": "Follower of Rembrandt van Rijn (Dutch, 1606–1669)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/87633",
"width": 512,
"height": 621
},
{
"url": "artworks/6b2e856f-01c4-1f23-4ded-79c2398c2c6b.jpg",
"type": "image",
"title": "Portrait of a Lady",
"artist": "Artist unknown (French or Swiss, active 18th century)",
"year": "1725–50",
"link": "https://www.artic.eduartworks/21679",
"width": 512,
"height": 649
},
{
"url": "artworks/add1be75-4581-568d-0751-ae2fbb61ebf3.jpg",
"type": "image",
"title": "Dead Birds and Shot Bags",
"artist": "Attributed to Pieter Boel (Flemish, 1622-1674)",
"year": "c. 1660",
"link": "https://www.artic.eduartworks/84096",
"width": 512,
"height": 378
},
{
"url": "artworks/5510e671-04eb-e498-51b1-09e177bb5b46.jpg",
"type": "image",
"title": "Portrait of a Grand Master of the Knights of Malta, Martin de Redin",
"artist": "Mattia Preti (Italian, 1613–1699)",
"year": "c. 1660",
"link": "https://www.artic.eduartworks/30365",
"width": 512,
"height": 646
},
{
"url": "artworks/59ce4cb2-beae-5492-0d83-345cbc0745c2.jpg",
"type": "image",
"title": "Venus and Cupid at the Forge of Vulcan",
"artist": "Johann Michael Rottmayr (Austrian, 1654–1730)",
"year": "c. 1695",
"link": "https://www.artic.eduartworks/12879",
"width": 512,
"height": 328
},
{
"url": "artworks/edf68284-ce38-4d0b-3c93-91b05d575e1b.jpg",
"type": "image",
"title": "Man with a Ruff",
"artist": "Follower of Anthony van Dyck (Flemish, 1599–1641)",
"year": "17th century",
"link": "https://www.artic.eduartworks/28857",
"width": 512,
"height": 667
},
{
"url": "artworks/0893821b-474d-c87c-b528-a059919089e7.jpg",
"type": "image",
"title": "Isabella of Bourbon, Wife of Philip IV of Spain",
"artist": "Workshop of Diego Velázquez (Spanish, 1599–1660)",
"year": "c. 1632",
"link": "https://www.artic.eduartworks/80560",
"width": 512,
"height": 630
},
{
"url": "artworks/02f89de9-0b05-fb1c-fc15-bc48787debfb.jpg",
"type": "image",
"title": "La Bonne Aventure (The Fortune Teller)",
"artist": "After Jean Baptiste Joseph Pater (French, 1695–1736)",
"year": "Date unknown",
"link": "https://www.artic.eduartworks/105698",
"width": 512,
"height": 393
},
{
"url": "artworks/46765075-304f-8dac-6b72-9a6262239a19.jpg",
"type": "image",
"title": "Portrait of a Man",
"artist": "Jan Mijtens (Dutch, c. 1614-1670)",
"year": "c. 1665",
"link": "https://www.artic.eduartworks/66022",
"width": 512,
"height": 637
},
{
"url": "artworks/2202b9d5-7eaf-d6ec-b8fd-3873c8c2933e.jpg",
"type": "image",
"title": "The Synagogue",
"artist": "Alessandro Magnasco (Italian, 1667-1749)",
"year": "c. 1730",
"link": "https://www.artic.eduartworks/66046",
"width": 512,
"height": 404
},
{
"url": "artworks/1b09b81c-d793-e6f9-7862-8770be502f7b.jpg",
"type": "image",
"title": "Sancho Panza Being Tossed in a Blanket",
"artist": "Pierre Charles Trémolières (French, 1703–1739)",
"year": "1723–24",
"link": "https://www.artic.eduartworks/94123",
"width": 512,
"height": 401
},
{
"url": "artworks/c60a4c07-9245-f864-a0fa-43d6eec5e85b.jpg",
"type": "image",
"title": "Liberation of Saint Peter from Prison",
"artist": "Netherlandish",
"year": "c. 1600",
"link": "https://www.artic.eduartworks/36495",
"width": 512,
"height": 560
},
{
"url": "artworks/544b3f30-4860-73a1-1a04-c84d0dafd005.jpg",
"type": "image",
"title": "Tobias and the Angel",
"artist": "Pieter Mulier, II (Dutch, 1637–1701)",
"year": "After 1684",
"link": "https://www.artic.eduartworks/109426",
"width": 512,
"height": 414
},
{
"url": "artworks/40bc6b72-8acf-8e9f-cf6d-c33050250768.jpg",
"type": "image",
"title": "The Flight into Egypt",
"artist": "Abraham van Diepenbeeck (Flemish, 1596-1675)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/16497",
"width": 512,
"height": 697
},
{
"url": "artworks/fb90c93c-3806-a2ea-3969-43376d090848.jpg",
"type": "image",
"title": "Portrait of a Gentleman",
"artist": "Caspar Netscher (Dutch, 1639-1684)",
"year": "1680",
"link": "https://www.artic.eduartworks/16394",
"width": 512,
"height": 625
},
{
"url": "artworks/5a1b1b10-f3e0-78e5-55db-d107bfcce82c.jpg",
"type": "image",
"title": "Portrait of a Man",
"artist": "Sébastien Bourdon (French, 1616–1671)",
"year": "1657–58",
"link": "https://www.artic.eduartworks/49422",
"width": 513,
"height": 603
},
{
"url": "artworks/0f1d3bfb-cbd3-80cb-dfb9-d157e77d1542.jpg",
"type": "image",
"title": "Two Monks in a Landscape",
"artist": "Italian or Spanish",
"year": "c. 1645",
"link": "https://www.artic.eduartworks/27309",
"width": 512,
"height": 424
},
{
"url": "artworks/f0fba705-6f01-14b0-87d2-b01be3390343.jpg",
"type": "image",
"title": "Portrait of a Sculptor",
"artist": "Jean Baptiste Santerre (French, 1651–1717)",
"year": "c. 1705",
"link": "https://www.artic.eduartworks/110873",
"width": 512,
"height": 635
},
{
"url": "artworks/50fdb185-fb4f-dad3-ed35-0add5467c780.jpg",
"type": "image",
"title": "Saint John the Baptist in the Wilderness",
"artist": "Adam Elsheimer (German, 1578–1610)",
"year": "c. 1605",
"link": "https://www.artic.eduartworks/213758",
"width": 512,
"height": 661
},
{
"url": "artworks/89300e26-55e8-ca20-9422-24c721192216.jpg",
"type": "image",
"title": "A Boy Blowing on a Firebrand",
"artist": "Gerrit van Honthorst (Dutch, 1592–1656)",
"year": "1621–22",
"link": "https://www.artic.eduartworks/243872",
"width": 512,
"height": 609
},
{
"url": "artworks/f621af47-1f60-d479-0be5-7e3a3700041a.jpg",
"type": "image",
"title": "The Adoration of the Magi",
"artist": "Pietro della Vecchia (Italian, 1605-1678)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/24677",
"width": 512,
"height": 156
},
{
"url": "artworks/2685ae09-bc4e-5e01-f2f6-cf41b1a75349.jpg",
"type": "image",
"title": "Venus and Cupid",
"artist": "Alessandro Turchi (Italian, 1578–1649)",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/20419",
"width": 512,
"height": 367
},
{
"url": "artworks/0f16b763-7dd4-8c2f-3fde-321729f6c552.jpg",
"type": "image",
"title": "Praying Virgin",
"artist": "Roman",
"year": "c. 1720",
"link": "https://www.artic.eduartworks/50306",
"width": 512,
"height": 632
},
{
"url": "artworks/d47db02e-de1b-0ba5-28f9-b4f7e8527016.jpg",
"type": "image",
"title": "The Judgement of Zaleucus",
"artist": "Otto van Veen (Flemish, 1556–1629)",
"year": "c. 1605",
"link": "https://www.artic.eduartworks/15260",
"width": 512,
"height": 415
},
{
"url": "artworks/25bad661-3b81-9925-0832-1d43a4327288.jpg",
"type": "image",
"title": "Landscape with Figures",
"artist": "Attributed to Joos de Momper, II (Flemish, 1564–1635)",
"year": "c. 1610",
"link": "https://www.artic.eduartworks/93737",
"width": 512,
"height": 296
},
{
"url": "artworks/4aae65a7-e5db-54be-82d5-43cc6b347dbd.jpg",
"type": "image",
"title": "The Raising of Lazarus",
"artist": "Follower of Rembrandt van Rijn (Dutch, 1606–1669)",
"year": "c. 1630",
"link": "https://www.artic.eduartworks/111467",
"width": 512,
"height": 610
},
{
"url": "artworks/4cf54b0c-c643-20c7-b0a4-68bc4b7626d9.jpg",
"type": "image",
"title": "Pastoral Scene with a Shepherdess Milking a Goat",
"artist": "Nicolaes Pietersz. Berchem (Dutch, 1621/22-1683)",
"year": "c. 1665–c. 1670",
"link": "https://www.artic.eduartworks/214269",
"width": 512,
"height": 399
},
{
"url": "artworks/e7d7ca13-6630-b5ba-69cc-48c126f14d5d.jpg",
"type": "image",
"title": "Mountain Landscape",
"artist": "Attributed to Joos de Momper, II (Flemish, 1564–1635)",
"year": "c. 1610",
"link": "https://www.artic.eduartworks/110767",
"width": 512,
"height": 273
},
{
"url": "artworks/0a991ed0-5816-1fbe-33d2-a12c384334ab.jpg",
"type": "image",
"title": "Nicolas Rubens, the Artist's Son",
"artist": "Attributed to Peter Paul Rubens (Flemish, 1577–1640)",
"year": "c. 1635",
"link": "https://www.artic.eduartworks/80553",
"width": 512,
"height": 628
},
{
"url": "artworks/97ca059c-6d86-4128-e216-6dbdcf4e8f3a.jpg",
"type": "image",
"title": "Young Man in a Turban",
"artist": "Follower of Rembrandt van Rijn (Dutch, 1606–1669)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/80548",
"width": 512,
"height": 623
},
{
"url": "artworks/a7fb57e1-d5d2-8569-7314-9dc35c573640.jpg",
"type": "image",
"title": "Nymphs Bathing",
"artist": "Attributed to Dirck van der Lisse (Dutch, active 1639–1669)\nAfter Cornelis Poelenburgh (Dutch, c. 1585–1667)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/35280",
"width": 512,
"height": 410
},
{
"url": "artworks/8b728f79-4937-4a98-5552-a3419cd6358f.jpg",
"type": "image",
"title": "Peasant Family at a Well",
"artist": "Master of the Children's Caps (Le Maître aux Béguins; French, 17th century)",
"year": "1650/60",
"link": "https://www.artic.eduartworks/8606",
"width": 512,
"height": 485
},
{
"url": "artworks/53957710-4a92-1028-4cbd-f38faaac5861.jpg",
"type": "image",
"title": "Classical Landscape with Two Women and a Man on a Path",
"artist": "Attributed to Francisque Millet (French, 1642–1679)",
"year": "c. 1660–c. 1670",
"link": "https://www.artic.eduartworks/35180",
"width": 512,
"height": 337
},
{
"url": "artworks/e6a3f16b-321a-2d47-bfea-fbc1c6f584c0.jpg",
"type": "image",
"title": "Portrait of a Girl",
"artist": "Attributed to Francesco Solimena (Italian, 1657–1747)",
"year": "c. 1700",
"link": "https://www.artic.eduartworks/28876",
"width": 512,
"height": 683
},
{
"url": "artworks/e8c48b99-4c1f-7ecf-0747-c5957b48f4f7.jpg",
"type": "image",
"title": "Man in Armour",
"artist": "Antonio Puga (Spanish, 1602–1648)",
"year": "17th century",
"link": "https://www.artic.eduartworks/48776",
"width": 512,
"height": 664
},
{
"url": "artworks/aabaac7c-d41a-f31d-7411-77f5f3fea322.jpg",
"type": "image",
"title": "The Flageolet Player",
"artist": "David Teniers the Younger (Flemish, 1610-1690)",
"year": "1635/40",
"link": "https://www.artic.eduartworks/16417",
"width": 512,
"height": 693
},
{
"url": "artworks/19110464-eec1-5e0a-7015-1972f63b9f07.jpg",
"type": "image",
"title": "Jacopo Butera",
"artist": "Francesco Solimena (Italian, 1657–1747)",
"year": "c. 1695",
"link": "https://www.artic.eduartworks/58050",
"width": 512,
"height": 658
},
{
"url": "artworks/084d1143-6073-96e5-433c-6a3bb83a1bbd.jpg",
"type": "image",
"title": "Landscape with Figures and Buildings",
"artist": "French",
"year": "17th century",
"link": "https://www.artic.eduartworks/36508",
"width": 512,
"height": 361
},
{
"url": "artworks/ee702210-de7a-8e2a-fdae-50464ba7d2be.jpg",
"type": "image",
"title": "Saint Hymer in Solitude",
"artist": "After Jean Restout (French, 1692–1768)",
"year": "c. 1735",
"link": "https://www.artic.eduartworks/99424",
"width": 512,
"height": 416
},
{
"url": "artworks/e540512e-7bb1-e713-4ef9-fdea43ebbcd3.jpg",
"type": "image",
"title": "The Apotheosis of the Hero",
"artist": "Follower of Peter Paul Rubens (Flemish, 1577–1640)",
"year": "c. 1635",
"link": "https://www.artic.eduartworks/100356",
"width": 512,
"height": 389
},
{
"url": "artworks/424365f4-5c5a-e993-f5a9-34b4f732c1d8.jpg",
"type": "image",
"title": "La Fête du Mai",
"artist": "After Jean Baptiste Joseph Pater (French, 1695-1736)",
"year": "Date unknown",
"link": "https://www.artic.eduartworks/93784",
"width": 512,
"height": 385
},
{
"url": "artworks/3874bdf8-6415-798e-1d5e-8f7a241b5309.jpg",
"type": "image",
"title": "Portrait of a Man",
"artist": "Jan Baptist Weenix (Dutch, 1621-1660/69)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/31176",
"width": 512,
"height": 617
},
{
"url": "artworks/1865cfaa-51b3-8916-56eb-6b57cf95c772.jpg",
"type": "image",
"title": "The Annunciation",
"artist": "Frans Francken II (Flemish, 1581–1642)",
"year": "c. 1620",
"link": "https://www.artic.eduartworks/37743",
"width": 512,
"height": 664
},
{
"url": "artworks/3b2866b5-2b4d-871f-3a61-33178831b0ec.jpg",
"type": "image",
"title": "Portrait of a Man",
"artist": "Attributed to Nicolaes Maes (Dutch, 1634–1693)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/111614",
"width": 512,
"height": 582
},
{
"url": "artworks/2432b014-7b1e-7d53-2c0c-62228344a14d.jpg",
"type": "image",
"title": "Monks at Supper",
"artist": "Alessandro Magnasco (Italian, 1667–1749)",
"year": "c. 1720",
"link": "https://www.artic.eduartworks/16389",
"width": 512,
"height": 431
},
{
"url": "artworks/c858614f-f411-e7d5-71fb-014a534f8294.jpg",
"type": "image",
"title": "The Hollow Road",
"artist": "Cornelis Huysmans (Flemish, 1648-1727)",
"year": "c. 1700",
"link": "https://www.artic.eduartworks/16385",
"width": 512,
"height": 396
},
{
"url": "artworks/10429355-e9c3-c48d-1d2a-6fef025d140e.jpg",
"type": "image",
"title": "River Landscape with a View of Naarden",
"artist": "Salomon van Ruysdael (Dutch, about 1602–1670)",
"year": "1642",
"link": "https://www.artic.eduartworks/238272",
"width": 512,
"height": 358
},
{
"url": "artworks/76017100-be7f-4c3c-2622-064f1472c889.jpg",
"type": "image",
"title": "The Lamentation",
"artist": "Andrea Vaccaro (Italian, 1604-1670)",
"year": "1652",
"link": "https://www.artic.eduartworks/35195",
"width": 512,
"height": 422
},
{
"url": "artworks/73a93ee5-b169-73f4-4db9-6e738eca7fce.jpg",
"type": "image",
"title": "Melancholia",
"artist": "Domenico Fetti (Italian, c. 1589–1623)",
"year": "c. 1615",
"link": "https://www.artic.eduartworks/250762",
"width": 512,
"height": 684
},
{
"url": "artworks/56909e17-e34d-9c18-d0b9-2a08ceff911c.jpg",
"type": "image",
"title": "Portrait of a Woman",
"artist": "Dutch",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/110559",
"width": 512,
"height": 606
},
{
"url": "artworks/6246e043-2b40-bf6b-ab7d-f2a4109e25d8.jpg",
"type": "image",
"title": "Portrait of a Woman",
"artist": "Attributed to Nicolaes Maes (Dutch, 1634–1693)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/86782",
"width": 512,
"height": 581
},
{
"url": "artworks/292c2b1e-90cb-3539-7527-819a7875218d.jpg",
"type": "image",
"title": "Portrait of an Officer",
"artist": "Italian",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/16413",
"width": 512,
"height": 607
},
{
"url": "artworks/451ba11b-1cfc-da58-d7f7-ec7f8734ae83.jpg",
"type": "image",
"title": "Travellers Halting at an Inn",
"artist": "Style of Isaac van Ostade (Dutch, 1621–1649)",
"year": "1643",
"link": "https://www.artic.eduartworks/35175",
"width": 512,
"height": 386
},
{
"url": "artworks/d0cbcbff-7858-c6a4-e12a-4b691f21ca6d.jpg",
"type": "image",
"title": "The Sacrifice of Polyxena",
"artist": "Giulio Carpioni (Italian, 1613–1678)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/11022",
"width": 512,
"height": 404
},
{
"url": "artworks/a061fcd4-88da-53f4-726f-72e3d71757a3.jpg",
"type": "image",
"title": "Portrait of a Man",
"artist": "Follower of Anthony van Dyck (Flemish, 1599–1641)",
"year": "1625–30",
"link": "https://www.artic.eduartworks/80535",
"width": 512,
"height": 702
},
{
"url": "artworks/e2329a9b-c990-1d9f-8332-fef0970d3a61.jpg",
"type": "image",
"title": "Portrait of a woman",
"artist": "Ferdinand Bol (Dutch, 1616–1680)",
"year": "c. 1655",
"link": "https://www.artic.eduartworks/80524",
"width": 512,
"height": 650
},
{
"url": "artworks/50931576-e0cd-3616-4668-7dbdcf6185de.jpg",
"type": "image",
"title": "Travellers Arriving at an Inn",
"artist": "Pieter de Neyn (Dutch, 1597–1639)",
"year": "1639–40",
"link": "https://www.artic.eduartworks/16349",
"width": 512,
"height": 338
},
{
"url": "artworks/c6b00d5b-c576-6129-10d5-fa8818887204.jpg",
"type": "image",
"title": "The Housekeeper",
"artist": "Hendrik Martensz. Sorgh (Dutch, c. 1610-1670)",
"year": "1657",
"link": "https://www.artic.eduartworks/16410",
"width": 512,
"height": 381
},
{
"url": "artworks/85d86039-4f89-aee7-feb7-b60a0ff3790a.jpg",
"type": "image",
"title": "Appenine Landscape",
"artist": "François Le Moyne (French, 1688-1737)",
"year": "c. 1730",
"link": "https://www.artic.eduartworks/250456",
"width": 512,
"height": 542
},
{
"url": "artworks/1dae4ac9-d33c-56e4-06d5-985b6be3898c.jpg",
"type": "image",
"title": "Venus, Cupid and Ceres",
"artist": "Cornelis Cornelisz van Haarlem (Dutch, 1562–1638)",
"year": "1604",
"link": "https://www.artic.eduartworks/256795",
"width": 512,
"height": 450
},
{
"url": "artworks/c3adfaeb-c058-0246-0e0c-07891837d995.jpg",
"type": "image",
"title": "Breakfast Still Life",
"artist": "Willem Claesz. Heda (Dutch, 1594-1680)",
"year": "1647",
"link": "https://www.artic.eduartworks/257286",
"width": 512,
"height": 330
},
{
"url": "artworks/c0a15a78-d295-f193-32b8-968b6d3c001d.jpg",
"type": "image",
"title": "Alpheus and Arethusa",
"artist": "Moyses van Uyttenbroeck (Dutch, c. 1590-1648)",
"year": "1626",
"link": "https://www.artic.eduartworks/257287",
"width": 512,
"height": 360
},
{
"url": "artworks/2b9bef7f-d495-24e9-e856-8db0be2827b4.jpg",
"type": "image",
"title": "A Vanitas Still Life with a Flag, Candlestick, Musical Instruments, Books, Writing Paraphernalia, Globes, and Hourglass",
"artist": "Edwaert Collier (Dutch, 1642–1708)",
"year": "1662",
"link": "https://www.artic.eduartworks/258490",
"width": 512,
"height": 386
},
{
"url": "artworks/5812d385-7383-b6d5-94d7-8759008b250b.jpg",
"type": "image",
"title": "The Witch",
"artist": "Circle of Alessandro Magnasco (Italian, 1667–1749)",
"year": "1700–1725",
"link": "https://www.artic.eduartworks/4096",
"width": 512,
"height": 710
},
{
"url": "artworks/da911441-2b80-1b9f-e471-2359ffa51301.jpg",
"type": "image",
"title": "Head of an Apostle",
"artist": "Follower of Jacob Jordaens (Flemish, 1593-1678)",
"year": "Date unknown",
"link": "https://www.artic.eduartworks/29403",
"width": 512,
"height": 626
},
{
"url": "artworks/09564c4e-7ffd-3e6f-b286-83c04c3e35db.jpg",
"type": "image",
"title": "Rebecca Welcomed by Abraham",
"artist": "Attributed to Barent Fabritius (Dutch, 1624–1673)",
"year": "c. 1650",
"link": "https://www.artic.eduartworks/28167",
"width": 512,
"height": 414
},
{
"url": "artworks/12d3265d-c9fc-ca4c-ca25-59032b3227de.jpg",
"type": "image",
"title": "Vase of Flowers",
"artist": "Jean Baptiste Belin I (Blin de Fontenay; French, 1653-1715)",
"year": "Date unknown",
"link": "https://www.artic.eduartworks/11142",
"width": 512,
"height": 356
},
{
"url": "artworks/762464dc-a339-0963-c76f-aef228a6c489.jpg",
"type": "image",
"title": "Saint Paul the Apostle",
"artist": "Attributed to Govaert Flinck (Dutch, 1615–1660)",
"year": "Date unknown",
"link": "https://www.artic.eduartworks/35161",
"width": 512,
"height": 621
},
{
"url": "artworks/568e033e-35ad-acf4-c529-b2e94a3346d9.jpg",
"type": "image",
"title": "Christ in Gethsemane",
"artist": "Italian",
"year": "18th century",
"link": "https://www.artic.eduartworks/35253",
"width": 512,
"height": 434
},
{
"url": "artworks/74f81dc6-4671-fdb8-0913-b93f1862bd83.jpg",
"type": "image",
"title": "Peasants Fighting over Cards",
"artist": "Dutch",
"year": "17th century",
"link": "https://www.artic.eduartworks/35256",
"width": 512,
"height": 449
},
{
"url": "artworks/88f1e8be-9ed4-b737-957c-abfae6b3c8a5.jpg",
"type": "image",
"title": "Cherubs Floating on a Cloud",
"artist": "Italian or Spanish",
"year": "17th century",
"link": "https://www.artic.eduartworks/60717",
"width": 512,
"height": 519
},
{
"url": "artworks/573f3d19-f5aa-df73-8515-d49d00e6a87a.jpg",
"type": "image",
"title": "Head of a Girl",
"artist": "Attributed to Anthony van Dyck (Flemish, 1599–1641)",
"year": "c. 1618–c. 1620",
"link": "https://www.artic.eduartworks/59872",
"width": 512,
"height": 662
},
{
"url": "artworks/e1e58d46-02e9-092f-b9ef-56ad14668056.jpg",
"type": "image",
"title": "Birds in Landscape",
"artist": "Marmaduke Cradock (English, c. 1660-1717)",
"year": "Date unknown",
"link": "https://www.artic.eduartworks/86994",
"width": 512,
"height": 544
},
{
"url": "artworks/90b26fa4-2b86-9cdf-4ebe-6d92abf6e3f3.jpg",
"type": "image",
"title": "Portrait of a Man",
"artist": "French",
"year": "17th century",
"link": "https://www.artic.eduartworks/46076",
"width": 512,
"height": 601
},
{
"url": "artworks/09820b6f-dc56-87e8-649d-874c0b0fa0db.jpg",
"type": "image",
"title": "Man with a Tankard at a Window",
"artist": "Adriaen van Ostade (Dutch, 1610–1685)",
"year": "1650–60",
"link": "https://www.artic.eduartworks/57643",
"width": 512,
"height": 655
},
{
"url": "artworks/ad4393f4-393c-cbe5-6a71-48f67d2ec96d.jpg",
"type": "image",
"title": "The Dutch Whaling Fleet",
"artist": "Abraham Storck (Dutch, 1644–1710)",
"year": "c. 1695",
"link": "https://www.artic.eduartworks/111728",
"width": 512,
"height": 392
},
{
"url": "artworks/91e51803-fc81-c96d-d6e6-938fba7ce2af.jpg",
"type": "image",
"title": "David Slaying Goliath",
"artist": "Dutch",
"year": "1600–50",
"link": "https://www.artic.eduartworks/100357",
"width": 512,
"height": 325
},
{
"url": "artworks/71d5639f-5858-df6c-29b7-cede99944ddd.jpg",
"type": "image",
"title": "Portrait of a Man",
"artist": "Nicolaes Maes (Dutch, 1632–1693)",
"year": "1655",
"link": "https://www.artic.eduartworks/96190",
"width": 512,
"height": 656
},
{
"url": "artworks/140d06a1-7fe1-3672-4d0b-15511b6913e6.jpg",
"type": "image",
"title": "Bouquet of Flowers and Fruit with Blue Ribbon",
"artist": "Maria van Oosterwijck (Dutch, 1630–1693)",
"year": "c. 1680",
"link": "https://www.artic.eduartworks/264716",
"width": 512,
"height": 655
},
{
"url": "artworks/ba078c9c-8532-73db-ffbd-bd2bd79eb032.jpg",
"type": "image",
"title": "Fête Champêtre",
"artist": "Artist unknown (French, active 18th century)",
"year": "1725–1750",
"link": "https://www.artic.eduartworks/79584",
"width": 512,
"height": 629
}
]
src/frame/index.tsx
import styles from "./style.module.css";
export function Frame() {
return (
<header className={`frame ${styles.frame}`}>
<h1 className={styles.frame__title}>Infinite Canvas</h1>
<a className={styles.frame__back} href="https://tympanus.net/codrops/?p=106679">
Article
</a>
<a className={styles.frame__archive} href="https://tympanus.net/codrops/hub/">
All demos
</a>
<a className={styles.frame__github} href="https://github.com/edoardolunardi/infinite-canvas">
GitHub
</a>
<div className={styles.frame__credits}>
<span>By </span>
<a href="https://www.edoardolunardi.dev/">Edoardo Lunardi</a>
</div>
<nav className={styles.frame__tags}>
<a href="https://tympanus.net/codrops/demos/?tag=scroll">#scroll</a>
<a href="https://tympanus.net/codrops/demos/?tag=infinite">#inifinite</a>
<a href="https://tympanus.net/codrops/demos/?tag=draggable">#draggable</a>
<a href="https://tympanus.net/codrops/demos/?tag=three-js">#three.js</a>
<a href="https://tympanus.net/codrops/demos/?tag=webgl">#webgl</a>
</nav>
</header>
);
}
src/frame/style.module.css
.frame {
padding: 1rem var(--page-padding, 1.5rem) 0;
display: grid;
z-index: 1000;
position: relative;
grid-row-gap: 0.5rem;
grid-column-gap: 1rem;
pointer-events: none;
justify-items: start;
align-items: center;
grid-template-columns: auto auto auto auto 1fr;
grid-template-areas:
"title title credits ..."
"back archive github ..."
"tags tags tags tags"
"sponsor sponsor sponsor sponsor";
color: #fff;
mix-blend-mode: difference;
font-family: "DM Sans", sans-serif;
font-weight: 300;
text-transform: uppercase;
}
.frame a,
.frame button {
pointer-events: auto;
color: #fff;
mix-blend-mode: difference;
}
.frame__title {
grid-area: title;
font-family: "Asul", serif;
font-size: 1rem;
font-weight: 400;
margin: 0;
color: #fff;
mix-blend-mode: difference;
}
.frame__back {
grid-area: back;
justify-self: start;
}
.frame__archive {
grid-area: archive;
justify-self: start;
}
.frame__github {
grid-area: github;
}
.frame__credits {
grid-area: credits;
}
.frame__tags {
grid-area: tags;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.frame__tags a {
color: #fff;
mix-blend-mode: difference;
}
@media screen and (min-width: 53em) {
.frame {
padding: var(--page-padding, 1.5rem);
height: 100%;
position: fixed;
top: 0;
left: 0;
width: 100%;
grid-column-gap: 2rem;
grid-template-columns: auto auto auto 1fr;
grid-template-rows: auto auto auto;
align-content: space-between;
grid-template-areas:
"back github archive ... credits"
"title title title title title"
"tags tags tags tags sponsor";
}
.frame__title {
justify-self: center;
font-size: clamp(2rem, 5vw, 3rem);
}
.frame__tags {
align-self: end;
gap: 2rem;
}
.frame__credits {
text-align: right;
}
}
src/index.css
:root {
font-size: 12px;
font-family: ui-monospace, monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--color-background: #000;
--color-text: #fff;
--color-link: #fff;
--color-link-hover: #fff;
--page-padding: 1.5rem;
--color-loader-bg: #ffffff;
--color-loader-bar-bg: rgba(101, 82, 60, 0.1);
--color-loader-bar-fill: rgb(101, 82, 60);
}
*,
*::after,
*::before {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
color: var(--color-text);
background-color: var(--color-background);
}
a {
text-decoration: none;
color: var(--color-link);
outline: none;
cursor: pointer;
}
a:hover {
text-decoration: underline;
color: var(--color-link-hover);
}
a:focus:not(:focus-visible) {
outline: none;
}
a:focus-visible {
outline: 2px solid red;
}
#root {
position: relative;
min-height: 100svh;
}
src/index.tsx
import * as React from "react";
import { createRoot } from "react-dom/client";
import "~/src/index.css";
import { App } from "~/src/app";
createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src/infinite-canvas/constants.ts
import { run } from "~/src/utils";
export const CHUNK_SIZE = 110;
export const RENDER_DISTANCE = 2;
export const CHUNK_FADE_MARGIN = 1;
export const MAX_VELOCITY = 3.2;
export const DEPTH_FADE_START = 140;
export const DEPTH_FADE_END = 260;
export const INVIS_THRESHOLD = 0.01;
export const KEYBOARD_SPEED = 0.18;
export const VELOCITY_LERP = 0.16;
export const VELOCITY_DECAY = 0.9;
export const INITIAL_CAMERA_Z = 50;
export type ChunkOffset = {
dx: number;
dy: number;
dz: number;
dist: number;
};
export const CHUNK_OFFSETS: ChunkOffset[] = run(() => {
const maxDist = RENDER_DISTANCE + CHUNK_FADE_MARGIN;
const offsets: ChunkOffset[] = [];
for (let dx = -maxDist; dx <= maxDist; dx++) {
for (let dy = -maxDist; dy <= maxDist; dy++) {
for (let dz = -maxDist; dz <= maxDist; dz++) {
const dist = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
if (dist > maxDist) continue;
offsets.push({ dx, dy, dz, dist });
}
}
}
return offsets;
});
src/infinite-canvas/index.tsx
import * as React from "react";
const LazyInfiniteCanvasScene = React.lazy(() => import("./scene").then((mod) => ({ default: mod.InfiniteCanvasScene })));
export function InfiniteCanvas(props: React.ComponentProps<typeof LazyInfiniteCanvasScene>) {
return (
<React.Suspense fallback={null}>
<LazyInfiniteCanvasScene {...props} />
</React.Suspense>
);
}
src/infinite-canvas/scene.tsx
import { KeyboardControls, Stats, useKeyboardControls, useProgress } from "@react-three/drei";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import * as React from "react";
import * as THREE from "three";
import { useIsTouchDevice } from "~/src/use-is-touch-device";
import { clamp, lerp } from "~/src/utils";
import {
CHUNK_FADE_MARGIN,
CHUNK_OFFSETS,
CHUNK_SIZE,
DEPTH_FADE_END,
DEPTH_FADE_START,
INITIAL_CAMERA_Z,
INVIS_THRESHOLD,
KEYBOARD_SPEED,
MAX_VELOCITY,
RENDER_DISTANCE,
VELOCITY_DECAY,
VELOCITY_LERP,
} from "./constants";
import styles from "./style.module.css";
import { getTexture } from "./texture-manager";
import type { ChunkData, InfiniteCanvasProps, MediaItem, PlaneData } from "./types";
import { generateChunkPlanesCached, getChunkUpdateThrottleMs, shouldThrottleUpdate } from "./utils";
const PLANE_GEOMETRY = new THREE.PlaneGeometry(1, 1);
const KEYBOARD_MAP = [
{ name: "forward", keys: ["w", "W", "ArrowUp"] },
{ name: "backward", keys: ["s", "S", "ArrowDown"] },
{ name: "left", keys: ["a", "A", "ArrowLeft"] },
{ name: "right", keys: ["d", "D", "ArrowRight"] },
{ name: "up", keys: ["e", "E"] },
{ name: "down", keys: ["q", "Q"] },
];
type KeyboardKeys = {
forward: boolean;
backward: boolean;
left: boolean;
right: boolean;
up: boolean;
down: boolean;
};
const getTouchDistance = (touches: Touch[]) => {
if (touches.length < 2) {
return 0;
}
const [t1, t2] = touches;
const dx = t1.clientX - t2.clientX;
const dy = t1.clientY - t2.clientY;
return Math.sqrt(dx * dx + dy * dy);
};
type CameraGridState = {
cx: number;
cy: number;
cz: number;
camZ: number;
};
function MediaPlane({
position,
scale,
media,
chunkCx,
chunkCy,
chunkCz,
cameraGridRef,
}: {
position: THREE.Vector3;
scale: THREE.Vector3;
media: MediaItem;
chunkCx: number;
chunkCy: number;
chunkCz: number;
cameraGridRef: React.RefObject<CameraGridState>;
}) {
const meshRef = React.useRef<THREE.Mesh>(null);
const materialRef = React.useRef<THREE.MeshBasicMaterial>(null);
const localState = React.useRef({ opacity: 0, frame: 0, ready: false });
const [texture, setTexture] = React.useState<THREE.Texture | null>(null);
const [isReady, setIsReady] = React.useState(false);
useFrame(() => {
const material = materialRef.current;
const mesh = meshRef.current;
const state = localState.current;
if (!material || !mesh) {
return;
}
state.frame = (state.frame + 1) & 1;
if (state.opacity < INVIS_THRESHOLD && !mesh.visible && state.frame === 0) {
return;
}
const cam = cameraGridRef.current;
const dist = Math.max(Math.abs(chunkCx - cam.cx), Math.abs(chunkCy - cam.cy), Math.abs(chunkCz - cam.cz));
const absDepth = Math.abs(position.z - cam.camZ);
if (absDepth > DEPTH_FADE_END + 50) {
state.opacity = 0;
material.opacity = 0;
material.depthWrite = false;
mesh.visible = false;
return;
}
const gridFade =
dist <= RENDER_DISTANCE ? 1 : Math.max(0, 1 - (dist - RENDER_DISTANCE) / Math.max(CHUNK_FADE_MARGIN, 0.0001));
const depthFade =
absDepth <= DEPTH_FADE_START
? 1
: Math.max(0, 1 - (absDepth - DEPTH_FADE_START) / Math.max(DEPTH_FADE_END - DEPTH_FADE_START, 0.0001));
const target = Math.min(gridFade, depthFade * depthFade);
state.opacity = target < INVIS_THRESHOLD && state.opacity < INVIS_THRESHOLD ? 0 : lerp(state.opacity, target, 0.18);
const isFullyOpaque = state.opacity > 0.99;
material.opacity = isFullyOpaque ? 1 : state.opacity;
material.depthWrite = isFullyOpaque;
mesh.visible = state.opacity > INVIS_THRESHOLD;
});
// Calculate display scale from media dimensions (from manifest)
const displayScale = React.useMemo(() => {
if (media.width && media.height) {
const aspect = media.width / media.height;
return new THREE.Vector3(scale.y * aspect, scale.y, 1);
}
return scale;
}, [media.width, media.height, scale]);
// Load texture with onLoad callback
React.useEffect(() => {
const state = localState.current;
state.ready = false;
state.opacity = 0;
setIsReady(false);
const material = materialRef.current;
if (material) {
material.opacity = 0;
material.depthWrite = false;
material.map = null;
}
const tex = getTexture(media, () => {
state.ready = true;
setIsReady(true);
});
setTexture(tex);
}, [media]);
// Apply texture when ready
React.useEffect(() => {
const material = materialRef.current;
const mesh = meshRef.current;
const state = localState.current;
if (!material || !mesh || !texture || !isReady || !state.ready) {
return;
}
material.map = texture;
material.opacity = state.opacity;
material.depthWrite = state.opacity >= 1;
mesh.scale.copy(displayScale);
}, [displayScale, texture, isReady]);
if (!texture || !isReady) {
return null;
}
return (
<mesh ref={meshRef} position={position} scale={displayScale} visible={false} geometry={PLANE_GEOMETRY}>
<meshBasicMaterial ref={materialRef} transparent opacity={0} side={THREE.DoubleSide} />
</mesh>
);
}
function Chunk({
cx,
cy,
cz,
media,
cameraGridRef,
}: {
cx: number;
cy: number;
cz: number;
media: MediaItem[];
cameraGridRef: React.RefObject<CameraGridState>;
}) {
const [planes, setPlanes] = React.useState<PlaneData[] | null>(null);
React.useEffect(() => {
let canceled = false;
const run = () => !canceled && setPlanes(generateChunkPlanesCached(cx, cy, cz));
if (typeof requestIdleCallback !== "undefined") {
const id = requestIdleCallback(run, { timeout: 100 });
return () => {
canceled = true;
cancelIdleCallback(id);
};
}
const id = setTimeout(run, 0);
return () => {
canceled = true;
clearTimeout(id);
};
}, [cx, cy, cz]);
if (!planes) {
return null;
}
return (
<group>
{planes.map((plane) => {
const mediaItem = media[plane.mediaIndex % media.length];
if (!mediaItem) {
return null;
}
return (
<MediaPlane
key={plane.id}
position={plane.position}
scale={plane.scale}
media={mediaItem}
chunkCx={cx}
chunkCy={cy}
chunkCz={cz}
cameraGridRef={cameraGridRef}
/>
);
})}
</group>
);
}
type ControllerState = {
velocity: { x: number; y: number; z: number };
targetVel: { x: number; y: number; z: number };
basePos: { x: number; y: number; z: number };
drift: { x: number; y: number };
mouse: { x: number; y: number };
lastMouse: { x: number; y: number };
scrollAccum: number;
isDragging: boolean;
lastTouches: Touch[];
lastTouchDist: number;
lastChunkKey: string;
lastChunkUpdate: number;
pendingChunk: { cx: number; cy: number; cz: number } | null;
};
const createInitialState = (camZ: number): ControllerState => ({
velocity: { x: 0, y: 0, z: 0 },
targetVel: { x: 0, y: 0, z: 0 },
basePos: { x: 0, y: 0, z: camZ },
drift: { x: 0, y: 0 },
mouse: { x: 0, y: 0 },
lastMouse: { x: 0, y: 0 },
scrollAccum: 0,
isDragging: false,
lastTouches: [],
lastTouchDist: 0,
lastChunkKey: "",
lastChunkUpdate: 0,
pendingChunk: null,
});
function SceneController({ media, onTextureProgress }: { media: MediaItem[]; onTextureProgress?: (progress: number) => void }) {
const { camera, gl } = useThree();
const isTouchDevice = useIsTouchDevice();
const [, getKeys] = useKeyboardControls<keyof KeyboardKeys>();
const state = React.useRef<ControllerState>(createInitialState(INITIAL_CAMERA_Z));
const cameraGridRef = React.useRef<CameraGridState>({ cx: 0, cy: 0, cz: 0, camZ: camera.position.z });
const [chunks, setChunks] = React.useState<ChunkData[]>([]);
const { progress } = useProgress();
const maxProgress = React.useRef(0);
React.useEffect(() => {
const rounded = Math.round(progress);
if (rounded > maxProgress.current) {
maxProgress.current = rounded;
onTextureProgress?.(rounded);
}
}, [progress, onTextureProgress]);
React.useEffect(() => {
const canvas = gl.domElement;
const s = state.current;
canvas.style.cursor = "grab";
const setCursor = (cursor: string) => {
canvas.style.cursor = cursor;
};
const onMouseDown = (e: MouseEvent) => {
// Just start dragging - keep drift frozen at current value
s.isDragging = true;
s.lastMouse = { x: e.clientX, y: e.clientY };
setCursor("grabbing");
};
const onMouseUp = () => {
s.isDragging = false;
setCursor("grab");
};
const onMouseLeave = () => {
s.mouse = { x: 0, y: 0 };
s.isDragging = false;
setCursor("grab");
};
const onMouseMove = (e: MouseEvent) => {
s.mouse = {
x: (e.clientX / window.innerWidth) * 2 - 1,
y: -(e.clientY / window.innerHeight) * 2 + 1,
};
if (s.isDragging) {
s.targetVel.x -= (e.clientX - s.lastMouse.x) * 0.025;
s.targetVel.y += (e.clientY - s.lastMouse.y) * 0.025;
s.lastMouse = { x: e.clientX, y: e.clientY };
}
};
const onWheel = (e: WheelEvent) => {
e.preventDefault();
s.scrollAccum += e.deltaY * 0.006;
};
const onTouchStart = (e: TouchEvent) => {
e.preventDefault();
s.lastTouches = Array.from(e.touches) as Touch[];
s.lastTouchDist = getTouchDistance(s.lastTouches);
setCursor("grabbing");
};
const onTouchMove = (e: TouchEvent) => {
e.preventDefault();
const touches = Array.from(e.touches) as Touch[];
if (touches.length === 1 && s.lastTouches.length >= 1) {
const [touch] = touches;
const [last] = s.lastTouches;
if (touch && last) {
s.targetVel.x -= (touch.clientX - last.clientX) * 0.02;
s.targetVel.y += (touch.clientY - last.clientY) * 0.02;
}
} else if (touches.length === 2 && s.lastTouchDist > 0) {
const dist = getTouchDistance(touches);
s.scrollAccum += (s.lastTouchDist - dist) * 0.006;
s.lastTouchDist = dist;
}
s.lastTouches = touches;
};
const onTouchEnd = (e: TouchEvent) => {
s.lastTouches = Array.from(e.touches) as Touch[];
s.lastTouchDist = getTouchDistance(s.lastTouches);
setCursor("grab");
};
canvas.addEventListener("mousedown", onMouseDown);
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("mousemove", onMouseMove);
canvas.addEventListener("mouseleave", onMouseLeave);
canvas.addEventListener("wheel", onWheel, { passive: false });
canvas.addEventListener("touchstart", onTouchStart, { passive: false });
canvas.addEventListener("touchmove", onTouchMove, { passive: false });
canvas.addEventListener("touchend", onTouchEnd, { passive: false });
return () => {
canvas.removeEventListener("mousedown", onMouseDown);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("mousemove", onMouseMove);
canvas.removeEventListener("mouseleave", onMouseLeave);
canvas.removeEventListener("wheel", onWheel);
canvas.removeEventListener("touchstart", onTouchStart);
canvas.removeEventListener("touchmove", onTouchMove);
canvas.removeEventListener("touchend", onTouchEnd);
};
}, [gl]);
useFrame(() => {
const s = state.current;
const now = performance.now();
const { forward, backward, left, right, up, down } = getKeys();
if (forward) s.targetVel.z -= KEYBOARD_SPEED;
if (backward) s.targetVel.z += KEYBOARD_SPEED;
if (left) s.targetVel.x -= KEYBOARD_SPEED;
if (right) s.targetVel.x += KEYBOARD_SPEED;
if (down) s.targetVel.y -= KEYBOARD_SPEED;
if (up) s.targetVel.y += KEYBOARD_SPEED;
const isZooming = Math.abs(s.velocity.z) > 0.05;
const zoomFactor = clamp(s.basePos.z / 50, 0.3, 2.0);
const driftAmount = 8.0 * zoomFactor;
const driftLerp = isZooming ? 0.2 : 0.12;
if (s.isDragging) {
// Freeze drift during drag - keep it at current value
} else if (isTouchDevice) {
s.drift.x = lerp(s.drift.x, 0, driftLerp);
s.drift.y = lerp(s.drift.y, 0, driftLerp);
} else {
s.drift.x = lerp(s.drift.x, s.mouse.x * driftAmount, driftLerp);
s.drift.y = lerp(s.drift.y, s.mouse.y * driftAmount, driftLerp);
}
s.targetVel.z += s.scrollAccum;
s.scrollAccum *= 0.8;
s.targetVel.x = clamp(s.targetVel.x, -MAX_VELOCITY, MAX_VELOCITY);
s.targetVel.y = clamp(s.targetVel.y, -MAX_VELOCITY, MAX_VELOCITY);
s.targetVel.z = clamp(s.targetVel.z, -MAX_VELOCITY, MAX_VELOCITY);
s.velocity.x = lerp(s.velocity.x, s.targetVel.x, VELOCITY_LERP);
s.velocity.y = lerp(s.velocity.y, s.targetVel.y, VELOCITY_LERP);
s.velocity.z = lerp(s.velocity.z, s.targetVel.z, VELOCITY_LERP);
s.basePos.x += s.velocity.x;
s.basePos.y += s.velocity.y;
s.basePos.z += s.velocity.z;
camera.position.set(s.basePos.x + s.drift.x, s.basePos.y + s.drift.y, s.basePos.z);
s.targetVel.x *= VELOCITY_DECAY;
s.targetVel.y *= VELOCITY_DECAY;
s.targetVel.z *= VELOCITY_DECAY;
const cx = Math.floor(s.basePos.x / CHUNK_SIZE);
const cy = Math.floor(s.basePos.y / CHUNK_SIZE);
const cz = Math.floor(s.basePos.z / CHUNK_SIZE);
cameraGridRef.current = { cx, cy, cz, camZ: s.basePos.z };
const key = `${cx},${cy},${cz}`;
if (key !== s.lastChunkKey) {
s.pendingChunk = { cx, cy, cz };
s.lastChunkKey = key;
}
const throttleMs = getChunkUpdateThrottleMs(isZooming, Math.abs(s.velocity.z));
if (s.pendingChunk && shouldThrottleUpdate(s.lastChunkUpdate, throttleMs, now)) {
const { cx: ucx, cy: ucy, cz: ucz } = s.pendingChunk;
s.pendingChunk = null;
s.lastChunkUpdate = now;
setChunks(
CHUNK_OFFSETS.map((o) => ({
key: `${ucx + o.dx},${ucy + o.dy},${ucz + o.dz}`,
cx: ucx + o.dx,
cy: ucy + o.dy,
cz: ucz + o.dz,
}))
);
}
});
React.useEffect(() => {
const s = state.current;
s.basePos = { x: camera.position.x, y: camera.position.y, z: camera.position.z };
setChunks(
CHUNK_OFFSETS.map((o) => ({
key: `${o.dx},${o.dy},${o.dz}`,
cx: o.dx,
cy: o.dy,
cz: o.dz,
}))
);
}, [camera]);
return (
<>
{chunks.map((chunk) => (
<Chunk key={chunk.key} cx={chunk.cx} cy={chunk.cy} cz={chunk.cz} media={media} cameraGridRef={cameraGridRef} />
))}
</>
);
}
export function InfiniteCanvasScene({
media,
onTextureProgress,
showFps = false,
showControls = false,
cameraFov = 60,
cameraNear = 1,
cameraFar = 500,
fogNear = 120,
fogFar = 320,
backgroundColor = "#ffffff",
fogColor = "#ffffff",
}: InfiniteCanvasProps) {
const isTouchDevice = useIsTouchDevice();
const dpr = Math.min(window.devicePixelRatio || 1, isTouchDevice ? 1.25 : 1.5);
if (!media.length) {
return null;
}
return (
<KeyboardControls map={KEYBOARD_MAP}>
<div className={styles.container}>
<Canvas
camera={{ position: [0, 0, INITIAL_CAMERA_Z], fov: cameraFov, near: cameraNear, far: cameraFar }}
dpr={dpr}
flat
gl={{ antialias: false, powerPreference: "high-performance" }}
className={styles.canvas}
>
<color attach="background" args={[backgroundColor]} />
<fog attach="fog" args={[fogColor, fogNear, fogFar]} />
<SceneController media={media} onTextureProgress={onTextureProgress} />
{showFps && <Stats className={styles.stats} />}
</Canvas>
{showControls && (
<div className={styles.controlsPanel}>
{isTouchDevice ? (
<>
<b>Drag</b> Pan · <b>Pinch</b> Zoom
</>
) : (
<>
<b>WASD</b> Move · <b>QE</b> Up/Down · <b>Scroll/Space</b> Zoom
</>
)}
</div>
)}
</div>
</KeyboardControls>
);
}
src/infinite-canvas/style.module.css
/** biome-ignore-all lint/complexity/noImportantStyles: who cares! */
.container {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
touch-action: none;
}
.canvas {
background-color: #ffffff;
width: 100%;
height: 100%;
position: absolute;
inset: 0;
touch-action: none;
}
.infoPanel {
position: absolute;
top: 12px;
right: 12px;
z-index: 10;
border-radius: 8px;
background-color: #ffffff;
padding: 12px;
font-size: 60%;
color: #000000;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
}
.controlsPanel {
position: absolute;
bottom: 12px;
right: 12px;
z-index: 10;
border-radius: 8px;
background-color: #ffffff;
padding: 12px;
color: #000000;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
}
.stats {
position: absolute !important;
top: 12px !important;
right: 12px !important;
left: auto !important;
}
src/infinite-canvas/texture-manager.ts
import * as THREE from "three";
import type { MediaItem } from "./types";
const textureCache = new Map<string, THREE.Texture>();
const loadCallbacks = new Map<string, Set<(tex: THREE.Texture) => void>>();
const loader = new THREE.TextureLoader();
const isTextureLoaded = (tex: THREE.Texture): boolean => {
const img = tex.image as HTMLImageElement | undefined;
return img instanceof HTMLImageElement && img.complete && img.naturalWidth > 0;
};
export const getTexture = (item: MediaItem, onLoad?: (texture: THREE.Texture) => void): THREE.Texture => {
const key = item.url;
const existing = textureCache.get(key);
if (existing) {
if (onLoad) {
if (isTextureLoaded(existing)) {
onLoad(existing);
} else {
loadCallbacks.get(key)?.add(onLoad);
}
}
return existing;
}
const callbacks = new Set<(tex: THREE.Texture) => void>();
if (onLoad) callbacks.add(onLoad);
loadCallbacks.set(key, callbacks);
const texture = loader.load(
key,
(tex) => {
tex.minFilter = THREE.LinearMipmapLinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.generateMipmaps = true;
tex.anisotropy = 4;
tex.colorSpace = THREE.SRGBColorSpace;
tex.needsUpdate = true;
loadCallbacks.get(key)?.forEach((cb) => {
try {
cb(tex);
} catch (err) {
console.error(`Callback failed: ${JSON.stringify(err)}`);
}
});
loadCallbacks.delete(key);
},
undefined,
(err) => console.error("Texture load failed:", key, err)
);
textureCache.set(key, texture);
return texture;
};
src/infinite-canvas/types.ts
import type * as THREE from "three";
export type MediaItem = {
url: string;
width: number;
height: number;
};
export type InfiniteCanvasProps = {
media: MediaItem[];
onTextureProgress?: (progress: number) => void;
showFps?: boolean;
showControls?: boolean;
cameraFov?: number;
cameraNear?: number;
cameraFar?: number;
fogNear?: number;
fogFar?: number;
backgroundColor?: string;
fogColor?: string;
};
export type ChunkData = {
key: string;
cx: number;
cy: number;
cz: number;
};
export type PlaneData = {
id: string;
position: THREE.Vector3;
scale: THREE.Vector3;
mediaIndex: number;
};
src/infinite-canvas/utils.ts
import * as THREE from "three";
import { hashString, seededRandom } from "~/src/utils";
import { CHUNK_SIZE } from "./constants";
import type { PlaneData } from "./types";
const MAX_PLANE_CACHE = 256;
const planeCache = new Map<string, PlaneData[]>();
const touchPlaneCache = (key: string) => {
const v = planeCache.get(key);
if (!v) {
return;
}
planeCache.delete(key);
planeCache.set(key, v);
};
const evictPlaneCache = () => {
while (planeCache.size > MAX_PLANE_CACHE) {
const firstKey = planeCache.keys().next().value as string | undefined;
if (!firstKey) break;
planeCache.delete(firstKey);
}
};
export const getChunkUpdateThrottleMs = (isZooming: boolean, zoomSpeed: number): number => {
if (zoomSpeed > 1.0) {
return 500;
}
if (isZooming) {
return 400;
}
return 100;
};
export const getMediaDimensions = (media: HTMLImageElement | undefined) => {
const width = media instanceof HTMLImageElement ? media.naturalWidth || media.width : undefined;
const height = media instanceof HTMLImageElement ? media.naturalHeight || media.height : undefined;
return { width, height };
};
export const generateChunkPlanes = (cx: number, cy: number, cz: number): PlaneData[] => {
const planes: PlaneData[] = [];
const seed = hashString(`${cx},${cy},${cz}`);
// ITEMS_PER_CHUNK = 5
for (let i = 0; i < 5; i++) {
const s = seed + i * 1000;
const r = (n: number) => seededRandom(s + n);
const size = 12 + r(4) * 8;
planes.push({
id: `${cx}-${cy}-${cz}-${i}`,
position: new THREE.Vector3(
cx * CHUNK_SIZE + r(0) * CHUNK_SIZE,
cy * CHUNK_SIZE + r(1) * CHUNK_SIZE,
cz * CHUNK_SIZE + r(2) * CHUNK_SIZE
),
scale: new THREE.Vector3(size, size, 1),
mediaIndex: Math.floor(r(5) * 1_000_000),
});
}
return planes;
};
export const generateChunkPlanesCached = (cx: number, cy: number, cz: number): PlaneData[] => {
const key = `${cx},${cy},${cz}`;
const cached = planeCache.get(key);
if (cached) {
touchPlaneCache(key);
return cached;
}
const planes = generateChunkPlanes(cx, cy, cz);
planeCache.set(key, planes);
evictPlaneCache();
return planes;
};
export const shouldThrottleUpdate = (lastUpdateTime: number, throttleMs: number, currentTime: number): boolean => {
return currentTime - lastUpdateTime >= throttleMs;
};
src/loader/index.tsx
import * as React from "react";
import styles from "./style.module.css";
export function PageLoader({ progress }: { progress: number }) {
const [show, setShow] = React.useState(true);
const [minTimeElapsed, setMinTimeElapsed] = React.useState(false);
const visualRef = React.useRef(0);
const [visualProgress, setVisualProgress] = React.useState(0);
React.useEffect(() => {
const timer = setTimeout(() => setMinTimeElapsed(true), 1500);
return () => clearTimeout(timer);
}, []);
React.useEffect(() => {
let raf: number;
const animate = () => {
const diff = progress - visualRef.current;
if (diff > 0.1) {
// Lerp toward target, faster when further behind
visualRef.current += diff * 0.08;
setVisualProgress(visualRef.current);
raf = requestAnimationFrame(animate);
} else {
// Snap when close enough
visualRef.current = progress;
setVisualProgress(progress);
}
};
raf = requestAnimationFrame(animate);
return () => cancelAnimationFrame(raf);
}, [progress]);
React.useEffect(() => {
if (minTimeElapsed && progress === 100 && visualProgress >= 99.5) {
const t = setTimeout(() => setShow(false), 200);
return () => clearTimeout(t);
}
}, [minTimeElapsed, progress, visualProgress]);
if (!show) {
return null;
}
const isHidden = minTimeElapsed && progress === 100 && visualProgress >= 99.5;
return (
<div className={`${styles.overlay} ${isHidden ? styles.hidden : styles.visible}`}>
<div className={styles.progressBarContainer}>
<div className={styles.progressBarFill} style={{ transform: `scaleX(${visualProgress / 100})` }} />
</div>
</div>
);
}
src/loader/style.module.css
.overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-loader-bg);
transition: opacity 0.5s ease-out;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.overlay.visible {
opacity: 1;
pointer-events: auto;
}
.progressBarContainer {
width: 200px;
background-color: var(--color-loader-bar-bg);
height: 4px;
border-radius: 2px;
overflow: hidden;
}
.progressBarFill {
height: 100%;
width: 100%;
transform-origin: left;
background-color: var(--color-loader-bar-fill);
}
src/use-is-touch-device.ts
import * as React from "react";
const getIsTouchDevice = (): boolean => {
const hasTouchEvent = "ontouchstart" in window;
const hasTouchPoints = navigator.maxTouchPoints > 0;
const hasCoarsePointer = window.matchMedia?.("(pointer: coarse)").matches ?? false;
return hasTouchEvent || hasTouchPoints || hasCoarsePointer;
};
export function useIsTouchDevice(): boolean {
const [isTouchDevice, setIsTouchDevice] = React.useState<boolean>(() => getIsTouchDevice());
React.useEffect(() => {
const mediaQuery = window.matchMedia("(pointer: coarse)");
const handleChange = () => {
setIsTouchDevice(getIsTouchDevice());
};
handleChange();
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return isTouchDevice;
}
src/utils.ts
export const run = <T>(fn: () => T): T => fn();
export const clamp = (v: number, min: number, max: number): number => Math.max(min, Math.min(max, v));
export const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;
export const seededRandom = (seed: number): number => {
const x = Math.sin(seed * 9999) * 10000;
return x - Math.floor(x);
};
export const hashString = (str: string): number => {
let h = 0;
for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
return Math.abs(h);
};
vite.config.ts
import path from "node:path";
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
base: "./",
plugins: [
react({
babel: {
plugins: [["babel-plugin-react-compiler"]],
},
}),
],
resolve: {
alias: {
"~": path.resolve(__dirname, "."),
},
},
});
Media credits and license evidence실행 안내·자료
README.md
## Credits
- Images courtesy of [The Art Institute of Chicago](https://www.artic.edu/open-access/public-api)
## License
[MIT](LICENSE)
LICENSE실행 안내·자료
MIT License
Copyright (c) 2009 - 2025 [Codrops](https://codrops.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Bundled dependency licenses실행 안내·자료
react@19.2.3 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
scheduler@0.27.0 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
react-dom@19.2.3 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@babel/runtime@7.29.7 — LICENSE
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
three@0.182.0 — LICENSE
The MIT License
Copyright © 2010-2025 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
react-reconciler@0.31.0 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
use-sync-external-store@1.7.0 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
zustand@5.0.15 — LICENSE
MIT License
Copyright (c) 2019 Paul Henschel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
suspend-react@0.1.3 — LICENSE
MIT License
Copyright (c) 2021 Paul Henschel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
scheduler@0.25.0 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
its-fine@2.0.0 — LICENSE
MIT License
Copyright (c) 2022-2025 Poimandres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
react-use-measure@2.1.7 — LICENSE
MIT License
Copyright (c) 2019-2025 Poimandres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@react-three/drei@10.7.7 — LICENSE
MIT License
Copyright (c) 2020 react-spring
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@use-gesture/core@10.3.1 — LICENSE
Copyright (c) 2018-present Paul Henschel <drcmda@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@use-gesture/react@10.3.1 — LICENSE
Copyright (c) 2018-present Paul Henschel <drcmda@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
three-stdlib@2.36.1 — LICENSE
MIT License
Copyright (c) 2021-2023 Poimandres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
potpack@1.0.2 — LICENSE
ISC License
Copyright (c) 2018, Mapbox
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
fflate@0.6.11 — LICENSE
MIT License
Copyright (c) 2020 Arjun Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
troika-worker-utils@0.52.0 — LICENSE
MIT License
Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
webgl-sdf-generator@1.1.1 — LICENSE.txt
Copyright (c) 2021 Jason Johnston
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
bidi-js@1.1.0 — LICENSE.txt
Copyright (c) 2021 Jason Johnston
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
troika-three-utils@0.52.5 — LICENSE
MIT License
Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
troika-three-text@0.52.5 — LICENSE
MIT License
Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
meshline@3.3.1 — LICENSE
MIT License
Copyright (c) 2016 Jaume Sanchez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
camera-controls@3.1.2 — LICENSE
MIT License
Copyright (c) 2017 @yomotsu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
hls.js@1.7.3 — LICENSE
Copyright (c) 2017 Dailymotion (http://www.dailymotion.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
src/remux/mp4-generator.js and src/demux/exp-golomb.ts implementation in this project
are derived from the HLS library for video.js (https://github.com/videojs/videojs-contrib-hls)
That work is also covered by the Apache 2 License, following copyright:
Copyright (c) 2013-2015 Brightcove
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
stats.js@0.17.0 — LICENSE
The MIT License
Copyright (c) 2009-2016 stats.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
detect-gpu@5.0.70 — LICENSE
MIT License
Copyright (c) 2020 Tim van Scherpenzeel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
three-mesh-bvh@0.8.3 — LICENSE
MIT License
Copyright (c) 2018 Garrett Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@monogrid/gainmap-js@3.4.0 — LICENSE
MIT License
Copyright (c) 2023 MONOGRID
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
zustand@4.5.7 — LICENSE
MIT License
Copyright (c) 2019 Paul Henschel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
tunnel-rat@0.1.2 — LICENSE
MIT License
Copyright (c) 2022 Poimandres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
BEHIND THE EXAMPLE
이 장면을 만드는 원리
반복 이미지가 공유하는 텍스처와 대기 콜백
반복 입력에서 현재 의도와 전환의 종료·취소 책임을 명시합니다.
- 이 예제에서는
- texture-manager는 URL별 Texture를 캐시하고 아직 로드되지 않은 동일 URL의 요청에는 콜백 Set을 공유합니다. 무한 배치의 여러 복제본이 같은 자원을 기다립니다.
코드와 함께 확인하기
코드에서 찾기
getTexturetexture-manager.ts기존 텍스처의 완료 상태에 따라 즉시 호출과 대기를 나눕니다.
직접 해보기
같은 URL의 여러 복제본을 만들고 로드 전에 화면을 제거하거나 이미지를 실패시킵니다.
살펴볼 변화실패·해제 시 대기 콜백과 공유 자원의 수명이 어떻게 끝나는지 확인해야 합니다.
