diff --git a/lib/loading-indicator.css b/lib/loading-indicator.css new file mode 100644 index 0000000..62b1602 --- /dev/null +++ b/lib/loading-indicator.css @@ -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; +} diff --git a/lib/loading-indicator.tsx b/lib/loading-indicator.tsx new file mode 100644 index 0000000..c2ddf10 --- /dev/null +++ b/lib/loading-indicator.tsx @@ -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 = () => ( +
+ + + + + +
+); diff --git a/lib/pages/gallery-page.css b/lib/pages/gallery-page.css new file mode 100644 index 0000000..420b5a5 --- /dev/null +++ b/lib/pages/gallery-page.css @@ -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; +} diff --git a/lib/pages/gallery-page.tsx b/lib/pages/gallery-page.tsx index df8a853..75f5454 100644 --- a/lib/pages/gallery-page.tsx +++ b/lib/pages/gallery-page.tsx @@ -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 = (props) => { +const THUMBNAIL_CLASS = "gallery-thumbnail canvas"; + +const THUMBNAIL_SCALE = 0.2; + +const ErrorThumbnail: React.FC<{ title?: string }> = ({ title }) => ( +
+ +
+); + +const CreatureThumbnail: React.FC<{ design: CreatureDesign }> = (props) => { + const svgRef = useRef(null); + const ctx: CreatureContextType = { + ...useContext(CreatureContext), + ...props.design.compCtx, + }; + const { background } = props.design.compCtx; + return ( -

- - {props.title} - {" "} - {props.kind} by {props.ownerName} -

+
+ + + + + + + +
+ ); +}; + +const MandalaThumbnail: React.FC<{ design: MandalaDesign }> = (props) => { + const render = createMandalaAnimationRenderer(props.design, THUMBNAIL_SCALE); + const { background } = props.design.baseCompCtx; + const svgRef = useRef(null); + const canvasRef = useRef(null); + + return ( +
+ + {render(0)} + +
+ ); +}; + +const THUMBNAILERS: { + [key in GalleryCompositionKind]: (gc: GalleryComposition) => JSX.Element; +} = { + creature: (gc) => ( + + ), + mandala: (gc) => ( + + ), +}; + +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 ; +} + +const GalleryCompositionView: React.FC = (props) => { + const thumbnail = getThumbnail(props); + const url = compositionRemixUrl(props); + + return ( +
+ + {thumbnail} + +

+ + {props.title} + {" "} + {props.kind} by {props.ownerName} +

+
); }; @@ -33,10 +146,9 @@ export const GalleryPage: React.FC<{}> = () => { return (
-

- 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. +

+ You can publish to this gallery via the sidebar on other pages of this + site.

- {ctx.compositions.map((comp) => ( - - ))} + {ctx.isLoading ? ( + + ) : ( + ctx.compositions.map((comp) => ( + + )) + )}
); diff --git a/lib/pages/mandala-page/core.tsx b/lib/pages/mandala-page/core.tsx index 2970a71..97f9467 100644 --- a/lib/pages/mandala-page/core.tsx +++ b/lib/pages/mandala-page/core.tsx @@ -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 {circles}; + return {circles}; }; } @@ -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,