Factor out a SvgCompositionContext. (#72)
This fixes #67 by making the background color selection/randomization code more DRY via the addition of a new `SvgCompositionContext` and a `CompositionContextWidget`. It also documents the `SvgSymbolContext` type, and moves the "randomize colors" button closer to the actual colors.pull/74/head
rodzic
7c05f78fd9
commit
e48b5f9bae
|
@ -71,6 +71,12 @@ ul.navbar li:last-child {
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mandala-container label.checkbox {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.mandala-container .color-widget {
|
.mandala-container .color-widget {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ type CheckboxProps = {
|
||||||
|
|
||||||
export const Checkbox: React.FC<CheckboxProps> = (props) => {
|
export const Checkbox: React.FC<CheckboxProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<label>
|
<label className="checkbox">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={props.value}
|
checked={props.value}
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
import React, { useContext, useRef, useState } from "react";
|
import React, { useContext, useRef, useState } from "react";
|
||||||
import { SvgVocabulary } from "../svg-vocabulary";
|
import { SvgVocabulary } from "../svg-vocabulary";
|
||||||
import {
|
import { noFillIfShowingSpecs, SvgSymbolData } from "../svg-symbol";
|
||||||
createSvgSymbolContext,
|
|
||||||
noFillIfShowingSpecs,
|
|
||||||
SvgSymbolData,
|
|
||||||
} from "../svg-symbol";
|
|
||||||
import {
|
import {
|
||||||
AttachmentPointType,
|
AttachmentPointType,
|
||||||
ATTACHMENT_POINT_TYPES,
|
ATTACHMENT_POINT_TYPES,
|
||||||
iterAttachmentPoints,
|
iterAttachmentPoints,
|
||||||
} from "../specs";
|
} from "../specs";
|
||||||
import { Random } from "../random";
|
import { Random } from "../random";
|
||||||
import { SymbolContextWidget } from "../symbol-context-widget";
|
|
||||||
import { range } from "../util";
|
import { range } from "../util";
|
||||||
|
|
||||||
import { AutoSizingSvg } from "../auto-sizing-svg";
|
import { AutoSizingSvg } from "../auto-sizing-svg";
|
||||||
|
@ -24,11 +19,12 @@ import {
|
||||||
} from "../creature-symbol";
|
} from "../creature-symbol";
|
||||||
import { HoverDebugHelper } from "../hover-debug-helper";
|
import { HoverDebugHelper } from "../hover-debug-helper";
|
||||||
import { svgScale, SvgTransform } from "../svg-transform";
|
import { svgScale, SvgTransform } from "../svg-transform";
|
||||||
import { ColorWidget } from "../color-widget";
|
|
||||||
import { NumericSlider } from "../numeric-slider";
|
import { NumericSlider } from "../numeric-slider";
|
||||||
import { DEFAULT_BG_COLOR } from "../colors";
|
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { createRandomColorPalette } from "../random-colors";
|
import {
|
||||||
|
CompositionContextWidget,
|
||||||
|
createSvgCompositionContext,
|
||||||
|
} from "../svg-composition-context";
|
||||||
|
|
||||||
/** 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.items.filter(
|
const ROOT_SYMBOLS = SvgVocabulary.items.filter(
|
||||||
|
@ -178,38 +174,26 @@ function getDownloadBasename(randomSeed: number) {
|
||||||
|
|
||||||
export const CreaturePage: React.FC<{}> = () => {
|
export const CreaturePage: React.FC<{}> = () => {
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
const [bgColor, setBgColor] = useState(DEFAULT_BG_COLOR);
|
|
||||||
const [randomSeed, setRandomSeed] = useState<number>(Date.now());
|
const [randomSeed, setRandomSeed] = useState<number>(Date.now());
|
||||||
const [randomlyInvert, setRandomlyInvert] = useState(true);
|
const [randomlyInvert, setRandomlyInvert] = useState(true);
|
||||||
const [symbolCtx, setSymbolCtx] = useState(createSvgSymbolContext());
|
const [compCtx, setCompCtx] = useState(createSvgCompositionContext());
|
||||||
const [complexity, setComplexity] = useState(INITIAL_COMPLEXITY_LEVEL);
|
const [complexity, setComplexity] = useState(INITIAL_COMPLEXITY_LEVEL);
|
||||||
const defaultCtx = useContext(CreatureContext);
|
const defaultCtx = useContext(CreatureContext);
|
||||||
const newRandomSeed = () => setRandomSeed(Date.now());
|
const newRandomSeed = () => setRandomSeed(Date.now());
|
||||||
const ctx: CreatureContextType = noFillIfShowingSpecs({
|
const ctx: CreatureContextType = noFillIfShowingSpecs({
|
||||||
...defaultCtx,
|
...defaultCtx,
|
||||||
...symbolCtx,
|
...compCtx,
|
||||||
});
|
});
|
||||||
const creature = COMPLEXITY_LEVEL_GENERATORS[complexity]({
|
const creature = COMPLEXITY_LEVEL_GENERATORS[complexity]({
|
||||||
rng: new Random(randomSeed),
|
rng: new Random(randomSeed),
|
||||||
randomlyInvert,
|
randomlyInvert,
|
||||||
});
|
});
|
||||||
const randomizeColors = () => {
|
|
||||||
const [bgColor, stroke, fill] = createRandomColorPalette(3);
|
|
||||||
setBgColor(bgColor);
|
|
||||||
setSymbolCtx({ ...symbolCtx, stroke, fill });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Creature!</h1>
|
<h1>Creature!</h1>
|
||||||
<SymbolContextWidget ctx={symbolCtx} onChange={setSymbolCtx}>
|
<CompositionContextWidget ctx={compCtx} onChange={setCompCtx} />
|
||||||
<ColorWidget label="Background" value={bgColor} onChange={setBgColor} />{" "}
|
<div className="thingy"></div>
|
||||||
</SymbolContextWidget>
|
|
||||||
<div className="thingy">
|
|
||||||
<button accessKey="c" onClick={randomizeColors}>
|
|
||||||
Randomize <u>c</u>olors!
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="thingy">
|
<div className="thingy">
|
||||||
<NumericSlider
|
<NumericSlider
|
||||||
label="Random creature complexity"
|
label="Random creature complexity"
|
||||||
|
@ -242,7 +226,7 @@ export const CreaturePage: React.FC<{}> = () => {
|
||||||
</div>
|
</div>
|
||||||
<CreatureContext.Provider value={ctx}>
|
<CreatureContext.Provider value={ctx}>
|
||||||
<HoverDebugHelper>
|
<HoverDebugHelper>
|
||||||
<AutoSizingSvg padding={20} ref={svgRef} bgColor={bgColor}>
|
<AutoSizingSvg padding={20} ref={svgRef} bgColor={compCtx.background}>
|
||||||
<SvgTransform transform={svgScale(0.5)}>
|
<SvgTransform transform={svgScale(0.5)}>
|
||||||
<CreatureSymbol {...creature} />
|
<CreatureSymbol {...creature} />
|
||||||
</SvgTransform>
|
</SvgTransform>
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { AutoSizingSvg } from "../auto-sizing-svg";
|
import { AutoSizingSvg } from "../auto-sizing-svg";
|
||||||
import { getBoundingBoxCenter } from "../bounding-box";
|
import { getBoundingBoxCenter } from "../bounding-box";
|
||||||
import { ColorWidget } from "../color-widget";
|
|
||||||
import { DEFAULT_BG_COLOR } from "../colors";
|
|
||||||
import { ExportWidget } from "../export-svg";
|
import { ExportWidget } from "../export-svg";
|
||||||
import { HoverDebugHelper } from "../hover-debug-helper";
|
import { HoverDebugHelper } from "../hover-debug-helper";
|
||||||
import { NumericSlider } from "../numeric-slider";
|
import { NumericSlider } from "../numeric-slider";
|
||||||
import {
|
import {
|
||||||
createSvgSymbolContext,
|
|
||||||
noFillIfShowingSpecs,
|
noFillIfShowingSpecs,
|
||||||
safeGetAttachmentPoint,
|
safeGetAttachmentPoint,
|
||||||
SvgSymbolContent,
|
SvgSymbolContent,
|
||||||
|
@ -23,13 +20,15 @@ import {
|
||||||
svgTranslate,
|
svgTranslate,
|
||||||
} from "../svg-transform";
|
} from "../svg-transform";
|
||||||
import { SvgVocabulary } from "../svg-vocabulary";
|
import { SvgVocabulary } from "../svg-vocabulary";
|
||||||
import { SymbolContextWidget } from "../symbol-context-widget";
|
|
||||||
import { NumericRange, range } from "../util";
|
import { NumericRange, range } from "../util";
|
||||||
import { Random } from "../random";
|
import { Random } from "../random";
|
||||||
import { PointWithNormal } from "../specs";
|
import { PointWithNormal } from "../specs";
|
||||||
import { getAttachmentTransforms } from "../attach";
|
import { getAttachmentTransforms } from "../attach";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { createRandomColorPalette } from "../random-colors";
|
import {
|
||||||
|
CompositionContextWidget,
|
||||||
|
createSvgCompositionContext,
|
||||||
|
} from "../svg-composition-context";
|
||||||
|
|
||||||
type ExtendedMandalaCircleParams = MandalaCircleParams & {
|
type ExtendedMandalaCircleParams = MandalaCircleParams & {
|
||||||
scaling: number;
|
scaling: number;
|
||||||
|
@ -252,10 +251,9 @@ function getRandomCircleParams(rng: Random): MandalaCircleParams {
|
||||||
|
|
||||||
export const MandalaPage: React.FC<{}> = () => {
|
export const MandalaPage: React.FC<{}> = () => {
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
const [bgColor, setBgColor] = useState(DEFAULT_BG_COLOR);
|
|
||||||
const [circle1, setCircle1] = useState(CIRCLE_1_DEFAULTS);
|
const [circle1, setCircle1] = useState(CIRCLE_1_DEFAULTS);
|
||||||
const [circle2, setCircle2] = useState(CIRCLE_2_DEFAULTS);
|
const [circle2, setCircle2] = useState(CIRCLE_2_DEFAULTS);
|
||||||
const [baseSymbolCtx, setBaseSymbolCtx] = useState(createSvgSymbolContext());
|
const [baseCompCtx, setBaseCompCtx] = useState(createSvgCompositionContext());
|
||||||
const [useTwoCircles, setUseTwoCircles] = useState(false);
|
const [useTwoCircles, setUseTwoCircles] = useState(false);
|
||||||
const [invertCircle2, setInvertCircle2] = useState(true);
|
const [invertCircle2, setInvertCircle2] = useState(true);
|
||||||
const [firstBehindSecond, setFirstBehindSecond] = useState(false);
|
const [firstBehindSecond, setFirstBehindSecond] = useState(false);
|
||||||
|
@ -265,16 +263,10 @@ export const MandalaPage: React.FC<{}> = () => {
|
||||||
setCircle2({ ...circle2, ...getRandomCircleParams(rng) });
|
setCircle2({ ...circle2, ...getRandomCircleParams(rng) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const symbolCtx = noFillIfShowingSpecs(baseSymbolCtx);
|
const symbolCtx = noFillIfShowingSpecs(baseCompCtx);
|
||||||
|
|
||||||
const circle2SymbolCtx = invertCircle2 ? swapColors(symbolCtx) : symbolCtx;
|
const circle2SymbolCtx = invertCircle2 ? swapColors(symbolCtx) : symbolCtx;
|
||||||
|
|
||||||
const randomizeColors = () => {
|
|
||||||
const [bgColor, stroke, fill] = createRandomColorPalette(3);
|
|
||||||
setBgColor(bgColor);
|
|
||||||
setBaseSymbolCtx({ ...baseSymbolCtx, stroke, fill });
|
|
||||||
};
|
|
||||||
|
|
||||||
const circles = [
|
const circles = [
|
||||||
<ExtendedMandalaCircle key="first" {...circle1} {...symbolCtx} />,
|
<ExtendedMandalaCircle key="first" {...circle1} {...symbolCtx} />,
|
||||||
];
|
];
|
||||||
|
@ -291,20 +283,15 @@ export const MandalaPage: React.FC<{}> = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Mandala!</h1>
|
<h1>Mandala!</h1>
|
||||||
<div className="mandala-container" style={{ backgroundColor: bgColor }}>
|
<div
|
||||||
|
className="mandala-container"
|
||||||
|
style={{ backgroundColor: baseCompCtx.background }}
|
||||||
|
>
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<SymbolContextWidget ctx={baseSymbolCtx} onChange={setBaseSymbolCtx}>
|
<CompositionContextWidget
|
||||||
<ColorWidget
|
ctx={baseCompCtx}
|
||||||
label="Background"
|
onChange={setBaseCompCtx}
|
||||||
value={bgColor}
|
/>
|
||||||
onChange={setBgColor}
|
|
||||||
/>{" "}
|
|
||||||
</SymbolContextWidget>
|
|
||||||
<div className="thingy">
|
|
||||||
<button accessKey="c" onClick={randomizeColors}>
|
|
||||||
Randomize <u>c</u>olors!
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>First circle</legend>
|
<legend>First circle</legend>
|
||||||
<ExtendedMandalaCircleParamsWidget
|
<ExtendedMandalaCircleParamsWidget
|
||||||
|
@ -349,7 +336,11 @@ export const MandalaPage: React.FC<{}> = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="canvas">
|
<div className="canvas">
|
||||||
<HoverDebugHelper>
|
<HoverDebugHelper>
|
||||||
<AutoSizingSvg padding={20} ref={svgRef} bgColor={bgColor}>
|
<AutoSizingSvg
|
||||||
|
padding={20}
|
||||||
|
ref={svgRef}
|
||||||
|
bgColor={baseCompCtx.background}
|
||||||
|
>
|
||||||
<SvgTransform transform={svgScale(0.5)}>{circles}</SvgTransform>
|
<SvgTransform transform={svgScale(0.5)}>{circles}</SvgTransform>
|
||||||
</AutoSizingSvg>
|
</AutoSizingSvg>
|
||||||
</HoverDebugHelper>
|
</HoverDebugHelper>
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ColorWidget } from "./color-widget";
|
||||||
|
import { DEFAULT_BG_COLOR } from "./colors";
|
||||||
|
import { createRandomColorPalette } from "./random-colors";
|
||||||
|
import { createSvgSymbolContext, SvgSymbolContext } from "./svg-symbol";
|
||||||
|
import {
|
||||||
|
SymbolContextWidget,
|
||||||
|
SymbolContextWidgetProps,
|
||||||
|
} from "./symbol-context-widget";
|
||||||
|
|
||||||
|
const DEFAULT_CONTEXT: SvgCompositionContext = {
|
||||||
|
background: DEFAULT_BG_COLOR,
|
||||||
|
...createSvgSymbolContext(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SvgCompositionContext = SvgSymbolContext & {
|
||||||
|
/** The background color of the composition, as a hex hash (e.g. '#ff0000'). */
|
||||||
|
background: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createSvgCompositionContext(
|
||||||
|
ctx: Partial<SvgCompositionContext> = {}
|
||||||
|
): SvgCompositionContext {
|
||||||
|
return {
|
||||||
|
...DEFAULT_CONTEXT,
|
||||||
|
...ctx,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompositionContextWidgetProps<
|
||||||
|
T extends SvgCompositionContext
|
||||||
|
> = SymbolContextWidgetProps<T>;
|
||||||
|
|
||||||
|
export function CompositionContextWidget<T extends SvgCompositionContext>({
|
||||||
|
ctx,
|
||||||
|
onChange,
|
||||||
|
children,
|
||||||
|
}: CompositionContextWidgetProps<T>): JSX.Element {
|
||||||
|
const randomizeColors = () => {
|
||||||
|
const [background, stroke, fill] = createRandomColorPalette(3);
|
||||||
|
onChange({ ...ctx, background, stroke, fill });
|
||||||
|
};
|
||||||
|
|
||||||
|
const extra = (
|
||||||
|
<button accessKey="c" onClick={randomizeColors}>
|
||||||
|
Randomize <u>c</u>olors!
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SymbolContextWidget ctx={ctx} onChange={onChange} extraButtons={extra}>
|
||||||
|
{children}
|
||||||
|
<ColorWidget
|
||||||
|
label="Background"
|
||||||
|
value={ctx.background}
|
||||||
|
onChange={(background) => onChange({ ...ctx, background })}
|
||||||
|
/>{" "}
|
||||||
|
</SymbolContextWidget>
|
||||||
|
);
|
||||||
|
}
|
|
@ -30,9 +30,23 @@ export type SvgSymbolElement = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SvgSymbolContext = {
|
export type SvgSymbolContext = {
|
||||||
|
/** The stroke color of the symbol, as a hex hash (e.g. '#ff0000'). */
|
||||||
stroke: string;
|
stroke: string;
|
||||||
|
|
||||||
|
/** The fill color of the symbol, as a hex hash (e.g. '#ff0000'). */
|
||||||
fill: string;
|
fill: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to visibly show the specifications for the symbol,
|
||||||
|
* e.g. its attachment points, nesting boxes, and so on.
|
||||||
|
*/
|
||||||
showSpecs: boolean;
|
showSpecs: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to forcibly apply a uniform stroke width to all
|
||||||
|
* the shapes in the symbol. If defined, the stroke width will
|
||||||
|
* *not* vary as the symbol is scaled.
|
||||||
|
*/
|
||||||
uniformStrokeWidth?: number;
|
uniformStrokeWidth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,6 +57,11 @@ const DEFAULT_CONTEXT: SvgSymbolContext = {
|
||||||
uniformStrokeWidth: DEFAULT_UNIFORM_STROKE_WIDTH,
|
uniformStrokeWidth: DEFAULT_UNIFORM_STROKE_WIDTH,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the given symbol context is visibly showing its specifications,
|
||||||
|
* return one with its fill color set to "none" so that the specs can
|
||||||
|
* be seen more easily.
|
||||||
|
*/
|
||||||
export function noFillIfShowingSpecs<T extends SvgSymbolContext>(ctx: T): T {
|
export function noFillIfShowingSpecs<T extends SvgSymbolContext>(ctx: T): T {
|
||||||
return {
|
return {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
@ -50,6 +69,9 @@ export function noFillIfShowingSpecs<T extends SvgSymbolContext>(ctx: T): T {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a symbol context with the stroke and fill colors swapped.
|
||||||
|
*/
|
||||||
export function swapColors<T extends SvgSymbolContext>(ctx: T): T {
|
export function swapColors<T extends SvgSymbolContext>(ctx: T): T {
|
||||||
return {
|
return {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|
|
@ -4,11 +4,19 @@ import { ColorWidget } from "./color-widget";
|
||||||
import { NumericSlider } from "./numeric-slider";
|
import { NumericSlider } from "./numeric-slider";
|
||||||
import { SvgSymbolContext, swapColors } from "./svg-symbol";
|
import { SvgSymbolContext, swapColors } from "./svg-symbol";
|
||||||
|
|
||||||
export const SymbolContextWidget: React.FC<{
|
export type SymbolContextWidgetProps<T extends SvgSymbolContext> = {
|
||||||
ctx: SvgSymbolContext;
|
ctx: T;
|
||||||
onChange: (value: SvgSymbolContext) => void;
|
onChange: (value: T) => void;
|
||||||
children?: any;
|
children?: any;
|
||||||
}> = ({ ctx, children, onChange }) => {
|
extraButtons?: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SymbolContextWidget<T extends SvgSymbolContext>({
|
||||||
|
ctx,
|
||||||
|
children,
|
||||||
|
onChange,
|
||||||
|
extraButtons,
|
||||||
|
}: SymbolContextWidgetProps<T>): JSX.Element {
|
||||||
const updateCtx = (updates: Partial<SvgSymbolContext>) => {
|
const updateCtx = (updates: Partial<SvgSymbolContext>) => {
|
||||||
onChange({ ...ctx, ...updates });
|
onChange({ ...ctx, ...updates });
|
||||||
};
|
};
|
||||||
|
@ -29,6 +37,7 @@ export const SymbolContextWidget: React.FC<{
|
||||||
<button onClick={() => updateCtx(swapColors(ctx))}>
|
<button onClick={() => updateCtx(swapColors(ctx))}>
|
||||||
Swap stroke/fill
|
Swap stroke/fill
|
||||||
</button>{" "}
|
</button>{" "}
|
||||||
|
{extraButtons}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Show specs"
|
label="Show specs"
|
||||||
value={ctx.showSpecs}
|
value={ctx.showSpecs}
|
||||||
|
@ -48,4 +57,4 @@ export const SymbolContextWidget: React.FC<{
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue