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 { WavesPage } from "./pages/waves-page";
|
||||||
import { VocabularyPage } from "./pages/vocabulary-page";
|
import { VocabularyPage } from "./pages/vocabulary-page";
|
||||||
import { CreaturePage } from "./pages/creature-page";
|
import { CreaturePage } from "./pages/creature-page";
|
||||||
|
import { MandalaPage } from "./pages/mandala-page";
|
||||||
|
|
||||||
const Pages = {
|
const Pages = {
|
||||||
vocabulary: VocabularyPage,
|
vocabulary: VocabularyPage,
|
||||||
creature: CreaturePage,
|
creature: CreaturePage,
|
||||||
waves: WavesPage,
|
waves: WavesPage,
|
||||||
|
mandala: MandalaPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
type PageName = keyof typeof Pages;
|
type PageName = keyof typeof Pages;
|
||||||
|
|
|
@ -11,6 +11,13 @@ import {
|
||||||
SvgSymbolData,
|
SvgSymbolData,
|
||||||
swapColors,
|
swapColors,
|
||||||
} from "./svg-symbol";
|
} from "./svg-symbol";
|
||||||
|
import {
|
||||||
|
svgRotate,
|
||||||
|
svgScale,
|
||||||
|
svgTransformOrigin,
|
||||||
|
SvgTransforms,
|
||||||
|
svgTranslate,
|
||||||
|
} from "./svg-transform";
|
||||||
|
|
||||||
const DEFAULT_ATTACHMENT_SCALE = 0.5;
|
const DEFAULT_ATTACHMENT_SCALE = 0.5;
|
||||||
|
|
||||||
|
@ -111,27 +118,17 @@ type AttachmentTransformProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AttachmentTransform: React.FC<AttachmentTransformProps> = (props) => (
|
const AttachmentTransform: React.FC<AttachmentTransformProps> = (props) => (
|
||||||
<g transform={`translate(${props.translate.x} ${props.translate.y})`}>
|
<SvgTransforms
|
||||||
{/**
|
transforms={[
|
||||||
* We originally used "transform-origin" here but that's not currently
|
svgTranslate(props.translate),
|
||||||
* supported by Safari. Instead, we'll set the origin of our symbol to
|
svgTransformOrigin(props.transformOrigin, [
|
||||||
* the transform origin, do the transform, and then move our origin back to
|
svgScale(props.scale),
|
||||||
* the original origin, which is equivalent to setting "transform-origin".
|
svgRotate(props.rotate),
|
||||||
**/}
|
]),
|
||||||
<g
|
]}
|
||||||
transform={`translate(${props.transformOrigin.x} ${props.transformOrigin.y})`}
|
>
|
||||||
>
|
{props.children}
|
||||||
<g
|
</SvgTransforms>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const AttachedCreatureSymbol: React.FC<AttachedCreatureSymbolProps> = ({
|
const AttachedCreatureSymbol: React.FC<AttachedCreatureSymbolProps> = ({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useContext, useRef, useState } from "react";
|
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 { createSvgSymbolContext, SvgSymbolData } from "../svg-symbol";
|
||||||
import {
|
import {
|
||||||
AttachmentPointType,
|
AttachmentPointType,
|
||||||
|
@ -26,13 +26,6 @@ import { HoverDebugHelper } from "../hover-debug-helper";
|
||||||
|
|
||||||
const DEFAULT_BG_COLOR = "#858585";
|
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. */
|
/** Symbols that can be the "root" (i.e., main body) of a creature. */
|
||||||
const ROOT_SYMBOLS = SvgVocabulary.filter(
|
const ROOT_SYMBOLS = SvgVocabulary.filter(
|
||||||
(data) => data.meta?.always_be_nested !== true
|
(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
|
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
|
* Given a parent symbol, return an array of random children to be nested within
|
||||||
* it.
|
* it.
|
||||||
|
@ -165,7 +146,7 @@ function getSymbolWithAttachments(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const symbol = createCreatureSymbolFactory(getSymbol);
|
const symbol = createCreatureSymbolFactory(getSvgSymbol);
|
||||||
|
|
||||||
const Eye = symbol("eye");
|
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";
|
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 {
|
export function scalePointXY(p: Point, xScale: number, yScale: number): Point {
|
||||||
return {
|
return {
|
||||||
x: p.x * xScale,
|
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";
|
import _SvgVocabulary from "./_svg-vocabulary.json";
|
||||||
|
|
||||||
export const SvgVocabulary: SvgSymbolData[] = _SvgVocabulary as any;
|
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