Add a very basic Mandala page. (#57)
This adds an extremely simple Mandala page (for #24) with a single circle Mandala comprised of several eyes. The symbol style is configurable, but parameters for the actual Mandala are not (yet). Doing this also involved factoring out a `<SvgTransforms>` component, which makes setting up SVG transforms a bit easier. Also moved `getSymbol` of `creature-page.tsx` and into `svg-vocabulary.tsx`, with the new name `getSvgSymbol`.pull/58/head
rodzic
7d62a5b7f6
commit
f790838b06
|
@ -3,11 +3,13 @@ import ReactDOM from "react-dom";
|
|||
import { WavesPage } from "./pages/waves-page";
|
||||
import { VocabularyPage } from "./pages/vocabulary-page";
|
||||
import { CreaturePage } from "./pages/creature-page";
|
||||
import { MandalaPage } from "./pages/mandala-page";
|
||||
|
||||
const Pages = {
|
||||
vocabulary: VocabularyPage,
|
||||
creature: CreaturePage,
|
||||
waves: WavesPage,
|
||||
mandala: MandalaPage,
|
||||
};
|
||||
|
||||
type PageName = keyof typeof Pages;
|
||||
|
|
|
@ -11,6 +11,13 @@ import {
|
|||
SvgSymbolData,
|
||||
swapColors,
|
||||
} from "./svg-symbol";
|
||||
import {
|
||||
svgRotate,
|
||||
svgScale,
|
||||
svgTransformOrigin,
|
||||
SvgTransforms,
|
||||
svgTranslate,
|
||||
} from "./svg-transform";
|
||||
|
||||
const DEFAULT_ATTACHMENT_SCALE = 0.5;
|
||||
|
||||
|
@ -111,27 +118,17 @@ type AttachmentTransformProps = {
|
|||
};
|
||||
|
||||
const AttachmentTransform: React.FC<AttachmentTransformProps> = (props) => (
|
||||
<g transform={`translate(${props.translate.x} ${props.translate.y})`}>
|
||||
{/**
|
||||
* We originally used "transform-origin" here but that's not currently
|
||||
* supported by Safari. Instead, we'll set the origin of our symbol to
|
||||
* the transform origin, do the transform, and then move our origin back to
|
||||
* the original origin, which is equivalent to setting "transform-origin".
|
||||
**/}
|
||||
<g
|
||||
transform={`translate(${props.transformOrigin.x} ${props.transformOrigin.y})`}
|
||||
>
|
||||
<g
|
||||
transform={`scale(${props.scale.x} ${props.scale.y}) rotate(${props.rotate})`}
|
||||
>
|
||||
<g
|
||||
transform={`translate(-${props.transformOrigin.x} -${props.transformOrigin.y})`}
|
||||
>
|
||||
{props.children}
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<SvgTransforms
|
||||
transforms={[
|
||||
svgTranslate(props.translate),
|
||||
svgTransformOrigin(props.transformOrigin, [
|
||||
svgScale(props.scale),
|
||||
svgRotate(props.rotate),
|
||||
]),
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</SvgTransforms>
|
||||
);
|
||||
|
||||
const AttachedCreatureSymbol: React.FC<AttachedCreatureSymbolProps> = ({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext, useRef, useState } from "react";
|
||||
import { SvgVocabulary } from "../svg-vocabulary";
|
||||
import { getSvgSymbol, SvgVocabulary } from "../svg-vocabulary";
|
||||
import { createSvgSymbolContext, SvgSymbolData } from "../svg-symbol";
|
||||
import {
|
||||
AttachmentPointType,
|
||||
|
@ -26,13 +26,6 @@ import { HoverDebugHelper } from "../hover-debug-helper";
|
|||
|
||||
const DEFAULT_BG_COLOR = "#858585";
|
||||
|
||||
/**
|
||||
* Mapping from symbol names to symbol data, for quick and easy access.
|
||||
*/
|
||||
const SYMBOL_MAP = new Map(
|
||||
SvgVocabulary.map((symbol) => [symbol.name, symbol])
|
||||
);
|
||||
|
||||
/** Symbols that can be the "root" (i.e., main body) of a creature. */
|
||||
const ROOT_SYMBOLS = SvgVocabulary.filter(
|
||||
(data) => data.meta?.always_be_nested !== true
|
||||
|
@ -82,18 +75,6 @@ const NESTED_SYMBOLS = SvgVocabulary.filter(
|
|||
data.meta?.always_nest !== true && data.meta?.never_be_nested !== true
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the data for the given symbol, throwing an error
|
||||
* if it doesn't exist.
|
||||
*/
|
||||
function getSymbol(name: string): SvgSymbolData {
|
||||
const symbol = SYMBOL_MAP.get(name);
|
||||
if (!symbol) {
|
||||
throw new Error(`Unable to find the symbol "${name}"!`);
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a parent symbol, return an array of random children to be nested within
|
||||
* it.
|
||||
|
@ -165,7 +146,7 @@ function getSymbolWithAttachments(
|
|||
return result;
|
||||
}
|
||||
|
||||
const symbol = createCreatureSymbolFactory(getSymbol);
|
||||
const symbol = createCreatureSymbolFactory(getSvgSymbol);
|
||||
|
||||
const Eye = symbol("eye");
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import React, { useState } from "react";
|
||||
import { AutoSizingSvg } from "../auto-sizing-svg";
|
||||
import { getBoundingBoxCenter } from "../bounding-box";
|
||||
import { HoverDebugHelper } from "../hover-debug-helper";
|
||||
import { reversePoint } from "../point";
|
||||
import {
|
||||
createSvgSymbolContext,
|
||||
SvgSymbolContent,
|
||||
SvgSymbolContext,
|
||||
SvgSymbolData,
|
||||
} from "../svg-symbol";
|
||||
import {
|
||||
svgRotate,
|
||||
svgScale,
|
||||
SvgTransforms,
|
||||
svgTranslate,
|
||||
} from "../svg-transform";
|
||||
import { getSvgSymbol } from "../svg-vocabulary";
|
||||
import { SymbolContextWidget } from "../symbol-context-widget";
|
||||
import { range } from "../util";
|
||||
|
||||
const EYE = getSvgSymbol("eye");
|
||||
|
||||
const MandalaCircle: React.FC<
|
||||
{
|
||||
data: SvgSymbolData;
|
||||
radius: number;
|
||||
numSymbols: number;
|
||||
} & SvgSymbolContext
|
||||
> = (props) => {
|
||||
const center = getBoundingBoxCenter(props.data.bbox);
|
||||
const degreesPerItem = 360 / props.numSymbols;
|
||||
const symbol = (
|
||||
<SvgTransforms
|
||||
transforms={[
|
||||
svgTranslate({ x: props.radius, y: 0 }),
|
||||
svgTranslate(reversePoint(center)),
|
||||
]}
|
||||
>
|
||||
<SvgSymbolContent {...props} />
|
||||
</SvgTransforms>
|
||||
);
|
||||
|
||||
const symbols = range(props.numSymbols).map((i) => (
|
||||
<SvgTransforms
|
||||
key={i}
|
||||
transforms={[svgRotate(degreesPerItem * i)]}
|
||||
children={symbol}
|
||||
/>
|
||||
));
|
||||
|
||||
return <>{symbols}</>;
|
||||
};
|
||||
|
||||
export const MandalaPage: React.FC<{}> = () => {
|
||||
const [symbolCtx, setSymbolCtx] = useState(createSvgSymbolContext());
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Mandala!</h1>
|
||||
<SymbolContextWidget ctx={symbolCtx} onChange={setSymbolCtx} />
|
||||
<HoverDebugHelper>
|
||||
<AutoSizingSvg padding={20}>
|
||||
<SvgTransforms transforms={[svgScale(0.5)]}>
|
||||
<MandalaCircle
|
||||
data={EYE}
|
||||
radius={400}
|
||||
numSymbols={6}
|
||||
{...symbolCtx}
|
||||
/>
|
||||
</SvgTransforms>
|
||||
</AutoSizingSvg>
|
||||
</HoverDebugHelper>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,10 @@
|
|||
import { Point } from "../vendor/bezier-js";
|
||||
|
||||
/** Return the "reverse" of the given point/vector, i.e. scale it by -1. */
|
||||
export function reversePoint(p: Point): Point {
|
||||
return { x: -p.x, y: -p.y };
|
||||
}
|
||||
|
||||
export function scalePointXY(p: Point, xScale: number, yScale: number): Point {
|
||||
return {
|
||||
x: p.x * xScale,
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import React from "react";
|
||||
import { Point } from "../vendor/bezier-js";
|
||||
import { reversePoint } from "./point";
|
||||
|
||||
type SvgTransform =
|
||||
| {
|
||||
kind: "translate";
|
||||
amount: Point;
|
||||
}
|
||||
| {
|
||||
kind: "rotate";
|
||||
degrees: number;
|
||||
}
|
||||
| {
|
||||
kind: "scale";
|
||||
amount: Point;
|
||||
}
|
||||
| {
|
||||
kind: "transformOrigin";
|
||||
amount: Point;
|
||||
transforms: SvgTransform[];
|
||||
};
|
||||
|
||||
function getSvgCodeForTransform(t: SvgTransform): string {
|
||||
switch (t.kind) {
|
||||
case "translate":
|
||||
return `translate(${t.amount.x} ${t.amount.y})`;
|
||||
|
||||
case "scale":
|
||||
return `scale(${t.amount.x} ${t.amount.y})`;
|
||||
|
||||
case "rotate":
|
||||
return `rotate(${t.degrees})`;
|
||||
|
||||
case "transformOrigin":
|
||||
/**
|
||||
* We originally used the SVG "transform-origin" attribute here but
|
||||
* that's not currently supported by Safari. Instead, we'll set the origin
|
||||
* of our SVG to the transform origin, do the transform, and then move our
|
||||
* origin back to the original origin, which does the same thing.
|
||||
**/
|
||||
return getSvgCodeForTransforms([
|
||||
svgTranslate(t.amount),
|
||||
...t.transforms,
|
||||
svgTranslate(reversePoint(t.amount)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function getSvgCodeForTransforms(transforms: SvgTransform[]): string {
|
||||
return transforms.map(getSvgCodeForTransform).join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given SVG transforms (e.g. rotate, scale)
|
||||
* centered at the given origin point.
|
||||
*/
|
||||
export function svgTransformOrigin(
|
||||
amount: Point,
|
||||
transforms: SvgTransform[]
|
||||
): SvgTransform {
|
||||
return { kind: "transformOrigin", amount, transforms };
|
||||
}
|
||||
|
||||
export function svgTranslate(amount: Point): SvgTransform {
|
||||
return { kind: "translate", amount };
|
||||
}
|
||||
|
||||
export function svgScale(amount: Point | number): SvgTransform {
|
||||
if (typeof amount === "number") {
|
||||
amount = { x: amount, y: amount };
|
||||
}
|
||||
return { kind: "scale", amount };
|
||||
}
|
||||
|
||||
export function svgRotate(degrees: number): SvgTransform {
|
||||
return { kind: "rotate", degrees };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SVG `<g>` element with the given children and transforms.
|
||||
*
|
||||
* Like the SVG `transform` attribute, the transforms are applied in
|
||||
* the *reverse* order that they are specified.
|
||||
*/
|
||||
export const SvgTransforms: React.FC<{
|
||||
transforms: SvgTransform[];
|
||||
children: any;
|
||||
}> = ({ transforms, children }) => {
|
||||
return <g transform={getSvgCodeForTransforms(transforms)}>{children}</g>;
|
||||
};
|
|
@ -2,3 +2,22 @@ import type { SvgSymbolData } from "./svg-symbol";
|
|||
import _SvgVocabulary from "./_svg-vocabulary.json";
|
||||
|
||||
export const SvgVocabulary: SvgSymbolData[] = _SvgVocabulary as any;
|
||||
|
||||
/**
|
||||
* Mapping from symbol names to symbol data, for quick and easy access.
|
||||
*/
|
||||
const SYMBOL_MAP = new Map(
|
||||
SvgVocabulary.map((symbol) => [symbol.name, symbol])
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the data for the given symbol, throwing an error
|
||||
* if it doesn't exist.
|
||||
*/
|
||||
export function getSvgSymbol(name: string): SvgSymbolData {
|
||||
const symbol = SYMBOL_MAP.get(name);
|
||||
if (!symbol) {
|
||||
throw new Error(`Unable to find the symbol "${name}"!`);
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue