Gallery thumbnails (#220)

This adds thumbnails to the gallery (#26).  Mandalas are not currently animated (I'm worried that doing this for lots of mandalas would severely impact performance).  It also adds a loading throbber for the gallery page.

If a gallery item can't be deserialized, a sad face is shown in place of a thumbnail, with a more detailed explanation on mouseover.

Currently the implementation is not as clean as I'd like.  I'll clean it up in a separate PR, as it will involve factoring out logic from the creature and mandala pages (similar in concept to #189 / #190), which would add lots of noise to this diff.
pull/223/head
Atul Varma 2021-09-01 09:47:40 -04:00 zatwierdzone przez GitHub
rodzic 6bfd84add6
commit 6277fc9157
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
5 zmienionych plików z 228 dodań i 26 usunięć

Wyświetl plik

@ -0,0 +1,13 @@
.loading-indicator {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.loading-indicator svg {
width: 64px;
height: 64px;
fill: lightgray;
}

Wyświetl plik

@ -0,0 +1,39 @@
import React from "react";
import "./loading-indicator.css";
/**
* Loading indicator taken from:
*
* https://commons.wikimedia.org/wiki/File:Chromiumthrobber.svg
*/
export const LoadingIndicator: React.FC = () => (
<div className="loading-indicator">
<svg
width="16"
height="16"
viewBox="0 0 300 300"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
>
<path
d="M 150,0
a 150,150 0 0,1 106.066,256.066
l -35.355,-35.355
a -100,-100 0 0,0 -70.711,-170.711 z"
>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 150 150"
to="360 150 150"
begin="0s"
dur="1s"
fill="freeze"
repeatCount="indefinite"
/>
</path>
</svg>
</div>
);

Wyświetl plik

@ -0,0 +1,28 @@
.gallery-thumbnail {
width: 320px;
height: 240px;
}
.gallery-item > a {
text-decoration: none;
}
.gallery-thumbnail.is-empty {
background-color: lightgray;
}
.gallery-thumbnail.is-empty > span {
color: darkgray;
font-size: 48px;
}
.gallery-item {
display: inline-block;
border: 1px solid black;
margin-right: 1em;
margin-bottom: 1em;
}
.gallery-item p {
margin: 1em;
}

Wyświetl plik

@ -1,7 +1,28 @@
import React, { useContext, useEffect } from "react";
import { GalleryComposition, GalleryContext } from "../gallery-context";
import React, { useContext, useEffect, useRef } from "react";
import { AutoSizingSvg } from "../auto-sizing-svg";
import {
CreatureContext,
CreatureContextType,
CreatureSymbol,
} from "../creature-symbol";
import {
GalleryComposition,
GalleryCompositionKind,
GalleryContext,
} from "../gallery-context";
import { LoadingIndicator } from "../loading-indicator";
import { Page } from "../page";
import { createPageWithStateSearchParams } from "../page-with-shareable-state";
import { svgScale, SvgTransform } from "../svg-transform";
import { CreatureDesign } from "./creature-page/core";
import { deserializeCreatureDesign } from "./creature-page/serialization";
import "./gallery-page.css";
import {
createMandalaAnimationRenderer,
MandalaDesign,
} from "./mandala-page/core";
import { deserializeMandalaDesign } from "./mandala-page/serialization";
function compositionRemixUrl(comp: GalleryComposition): string {
return (
@ -10,14 +31,106 @@ function compositionRemixUrl(comp: GalleryComposition): string {
);
}
const GalleryCompositionView: React.FC<GalleryComposition> = (props) => {
const THUMBNAIL_CLASS = "gallery-thumbnail canvas";
const THUMBNAIL_SCALE = 0.2;
const ErrorThumbnail: React.FC<{ title?: string }> = ({ title }) => (
<div className={THUMBNAIL_CLASS + " is-empty"} title={title}>
<span></span>
</div>
);
const CreatureThumbnail: React.FC<{ design: CreatureDesign }> = (props) => {
const svgRef = useRef<SVGSVGElement>(null);
const ctx: CreatureContextType = {
...useContext(CreatureContext),
...props.design.compCtx,
};
const { background } = props.design.compCtx;
return (
<p>
<a href={compositionRemixUrl(props)} target="_blank">
{props.title}
</a>{" "}
{props.kind} by {props.ownerName}
</p>
<div className={THUMBNAIL_CLASS} style={{ backgroundColor: background }}>
<CreatureContext.Provider value={ctx}>
<AutoSizingSvg padding={10} ref={svgRef} bgColor={background}>
<SvgTransform transform={svgScale(THUMBNAIL_SCALE)}>
<CreatureSymbol {...props.design.creature} />
</SvgTransform>
</AutoSizingSvg>
</CreatureContext.Provider>
</div>
);
};
const MandalaThumbnail: React.FC<{ design: MandalaDesign }> = (props) => {
const render = createMandalaAnimationRenderer(props.design, THUMBNAIL_SCALE);
const { background } = props.design.baseCompCtx;
const svgRef = useRef<SVGSVGElement>(null);
const canvasRef = useRef<HTMLDivElement>(null);
return (
<div
className={THUMBNAIL_CLASS}
style={{ backgroundColor: background }}
ref={canvasRef}
>
<AutoSizingSvg
ref={svgRef}
bgColor={background}
sizeToElement={canvasRef}
>
{render(0)}
</AutoSizingSvg>
</div>
);
};
const THUMBNAILERS: {
[key in GalleryCompositionKind]: (gc: GalleryComposition) => JSX.Element;
} = {
creature: (gc) => (
<CreatureThumbnail design={deserializeCreatureDesign(gc.serializedValue)} />
),
mandala: (gc) => (
<MandalaThumbnail design={deserializeMandalaDesign(gc.serializedValue)} />
),
};
function getThumbnail(gc: GalleryComposition): JSX.Element {
let errorTitle: string;
if (gc.kind in THUMBNAILERS) {
try {
return THUMBNAILERS[gc.kind](gc);
} catch (e) {
errorTitle = `Could not deserialize ${gc.kind} "${gc.title}": ${e.message}`;
console.error(e);
}
} else {
errorTitle = `Found unknown gallery composition kind "${gc.kind}".`;
}
console.log(errorTitle);
return <ErrorThumbnail title={errorTitle} />;
}
const GalleryCompositionView: React.FC<GalleryComposition> = (props) => {
const thumbnail = getThumbnail(props);
const url = compositionRemixUrl(props);
return (
<div className="gallery-item">
<a href={url} target="_blank">
{thumbnail}
</a>
<p>
<a href={url} target="_blank">
{props.title}
</a>{" "}
{props.kind} by {props.ownerName}
</p>
</div>
);
};
@ -33,10 +146,9 @@ export const GalleryPage: React.FC<{}> = () => {
return (
<Page title="Gallery!">
<div className="sidebar">
<p>
This gallery is a work in progress! You can publish to it via the
sidebar on the creature and mandala pages. We don't have thumbnails
yet, though. It will improve over time.
<p style={{ marginTop: "0" }}>
You can publish to this gallery via the sidebar on other pages of this
site.
</p>
<button onClick={ctx.refresh} disabled={ctx.isLoading}>
{ctx.isLoading ? "Loading\u2026" : "Refresh"}
@ -44,9 +156,13 @@ export const GalleryPage: React.FC<{}> = () => {
{ctx.error && <p className="error">{ctx.error}</p>}
</div>
<div className="canvas scrollable">
{ctx.compositions.map((comp) => (
<GalleryCompositionView key={comp.id} {...comp} />
))}
{ctx.isLoading ? (
<LoadingIndicator />
) : (
ctx.compositions.map((comp) => (
<GalleryCompositionView key={comp.id} {...comp} />
))
)}
</div>
</Page>
);

Wyświetl plik

@ -226,14 +226,17 @@ function isDesignAnimated(design: MandalaDesign): boolean {
return getCirclesFromDesign(design).some((c) => c.animateSymbolRotation);
}
function createAnimationRenderer({
baseCompCtx,
invertCircle2,
circle1,
circle2,
useTwoCircles,
firstBehind,
}: MandalaDesign): AnimationRenderer {
export function createMandalaAnimationRenderer(
{
baseCompCtx,
invertCircle2,
circle1,
circle2,
useTwoCircles,
firstBehind,
}: MandalaDesign,
scale = 0.5
): AnimationRenderer {
const symbolCtx = noFillIfShowingSpecs(baseCompCtx);
const circle2SymbolCtx = invertCircle2 ? swapColors(symbolCtx) : symbolCtx;
@ -259,7 +262,7 @@ function createAnimationRenderer({
}
}
return <SvgTransform transform={svgScale(0.5)}>{circles}</SvgTransform>;
return <SvgTransform transform={svgScale(scale)}>{circles}</SvgTransform>;
};
}
@ -336,7 +339,10 @@ export const MandalaPageWithDefaults: React.FC<{
]
);
const isAnimated = isDesignAnimated(design);
const render = useMemo(() => createAnimationRenderer(design), [design]);
const render = useMemo(
() => createMandalaAnimationRenderer(design),
[design]
);
useDebouncedEffect(
250,