mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
refactor(studio): a paint's SVG parts in a module of their own; the pattern letters a component of their own
This commit is contained in:
@@ -6,7 +6,7 @@ import { bookDrawnLines, bookTextCurvePath, bookTextIsDrawn, bookTextPaintFill }
|
||||
|
||||
import { fontStack } from './bookFonts';
|
||||
import { photoSrc } from './bookRender';
|
||||
import { paintParts } from './paint/paintRender';
|
||||
import { paintParts, type PaintParts } from './paint/paintParts';
|
||||
import {
|
||||
caseStyle,
|
||||
curveAnchor,
|
||||
@@ -146,6 +146,28 @@ interface GlyphPaint {
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
/** A gradient or pattern in the letters, laid out over the frame as a shape's paint is over its box. */
|
||||
function lettersPaint(el: BookTextElement, id: string): PaintParts | null {
|
||||
const paint = bookTextPaintFill(el);
|
||||
return paint ? paintParts(paint, { x: 0, y: 0, w: el.frame.w, h: el.frame.h }, id) : null;
|
||||
}
|
||||
|
||||
/** A pattern's ground and its marks, clipped to the letters. */
|
||||
function PatternLetters({ parts, w, h, clipId }: { parts: PaintParts; w: number; h: number; clipId: string }) {
|
||||
const pattern = parts.pattern;
|
||||
if (!pattern) return null;
|
||||
return (
|
||||
<g clipPath={`url(#${clipId})`}>
|
||||
{parts.fill !== 'none' && <rect width={w} height={h} fill={parts.fill} />}
|
||||
<g transform={pattern.transform}>
|
||||
{pattern.layers.map((layer, i) => (
|
||||
<path key={i} d={layer.d} fill={pattern.ink} fillOpacity={layer.opacity < 1 ? layer.opacity : undefined} />
|
||||
))}
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawnText({ el, frameStyle, big }: { el: BookTextElement; frameStyle: CSSProperties; big: boolean }) {
|
||||
const reactId = useId();
|
||||
const pathId = svgId('tp-', reactId);
|
||||
@@ -156,10 +178,8 @@ function DrawnText({ el, frameStyle, big }: { el: BookTextElement; frameStyle: C
|
||||
const lines = bookDrawnLines(el);
|
||||
const curve = el.curve ? bookTextCurvePath(el.curve, w, h, fontMm) : null;
|
||||
const photo = el.fillPaint?.type === 'photo' ? el.fillPaint : null;
|
||||
// A gradient or pattern in the letters, laid out over the frame as a shape's paint is over its box.
|
||||
const fillPaint = bookTextPaintFill(el);
|
||||
const letterPaint = fillPaint ? paintParts(fillPaint, { x: 0, y: 0, w, h }, paintId) : null;
|
||||
const pattern = letterPaint?.pattern ?? null;
|
||||
const letterPaint = lettersPaint(el, paintId);
|
||||
const pattern = letterPaint ? letterPaint.pattern : null;
|
||||
const hollow = isHollow(el);
|
||||
const { outline, shadow } = el;
|
||||
|
||||
@@ -218,17 +238,7 @@ function DrawnText({ el, frameStyle, big }: { el: BookTextElement; frameStyle: C
|
||||
const cover = photoCover(w, h, photo);
|
||||
layers.push(<image key="photo" href={photoSrc(photo.photoId, big)} {...cover} clipPath={`url(#${clipId})`} />);
|
||||
} else if (pattern && letterPaint) {
|
||||
// The pattern's ground and its marks, clipped to the letters.
|
||||
layers.push(
|
||||
<g key="pattern" clipPath={`url(#${clipId})`}>
|
||||
{letterPaint.fill !== 'none' && <rect width={w} height={h} fill={letterPaint.fill} />}
|
||||
<g transform={pattern.transform}>
|
||||
{pattern.layers.map((layer, i) => (
|
||||
<path key={i} d={layer.d} fill={pattern.ink} fillOpacity={layer.opacity < 1 ? layer.opacity : undefined} />
|
||||
))}
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
layers.push(<PatternLetters key="pattern" parts={letterPaint} w={w} h={h} clipId={clipId} />);
|
||||
} else {
|
||||
layers.push(glyphs({ fill: letterPaint ? letterPaint.fill : (letterFill(el) ?? el.color) }, 'fill'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { sortedStops, stopAlpha, type BookPaint, type BookPaintStop } from '@trek/shared';
|
||||
import type { ReactNode } from 'react';
|
||||
import { linearLine, r2 } from './paintGeometry';
|
||||
import { patternGeometry, type PatternBox } from './patterns';
|
||||
|
||||
/**
|
||||
* A paint as SVG parts over a box (paintRender.tsx explains why the ids come
|
||||
* from useId): kept apart from the components that draw them, so a shape and
|
||||
* the letters of a text share it without the component file exporting a helper.
|
||||
*/
|
||||
|
||||
function Stops({ stops }: { stops: readonly BookPaintStop[] }) {
|
||||
return (
|
||||
<>
|
||||
{sortedStops(stops).map((s, i) => (
|
||||
<stop
|
||||
// Two stops may share an offset (a hard edge), so the index keys them.
|
||||
key={i}
|
||||
offset={r2(s.at)}
|
||||
stopColor={s.color}
|
||||
stopOpacity={stopAlpha(s) < 1 ? stopAlpha(s) : undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** What a painted outline needs: the defs to put in the SVG, and how to fill the outline. */
|
||||
export interface PaintParts {
|
||||
defs: ReactNode;
|
||||
fill: string;
|
||||
fillOpacity?: number;
|
||||
/** A pattern's geometry, to draw clipped to the outline over its fill. */
|
||||
pattern: { clipId: string; layers: { d: string; opacity: number }[]; transform?: string; ink: string } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What drawing a paint over a box takes: the gradient definition and the fill
|
||||
* to point at it, or a pattern's geometry to draw clipped to the outline over
|
||||
* its ground. A shape's outline and the letters of a text both draw from it.
|
||||
*/
|
||||
export function paintParts(paint: BookPaint, box: PatternBox, id: string): PaintParts {
|
||||
switch (paint.type) {
|
||||
case 'solid':
|
||||
return {
|
||||
defs: null,
|
||||
fill: paint.color,
|
||||
fillOpacity: stopAlpha(paint) < 1 ? stopAlpha(paint) : undefined,
|
||||
pattern: null,
|
||||
};
|
||||
case 'linear': {
|
||||
const line = linearLine(paint.angle, box);
|
||||
return {
|
||||
defs: (
|
||||
<linearGradient id={id} gradientUnits="userSpaceOnUse" {...line}>
|
||||
<Stops stops={paint.stops} />
|
||||
</linearGradient>
|
||||
),
|
||||
fill: `url(#${id})`,
|
||||
pattern: null,
|
||||
};
|
||||
}
|
||||
case 'radial':
|
||||
return {
|
||||
defs: (
|
||||
<radialGradient
|
||||
id={id}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx={r2(box.x + paint.cx * box.w)}
|
||||
cy={r2(box.y + paint.cy * box.h)}
|
||||
r={r2((paint.r * Math.hypot(box.w, box.h)) / 2)}
|
||||
>
|
||||
<Stops stops={paint.stops} />
|
||||
</radialGradient>
|
||||
),
|
||||
fill: `url(#${id})`,
|
||||
pattern: null,
|
||||
};
|
||||
case 'pattern': {
|
||||
const geo = patternGeometry(paint, box);
|
||||
return {
|
||||
defs: null,
|
||||
fill: paint.bg ?? 'none',
|
||||
pattern: { clipId: `${id}-clip`, layers: geo.layers, transform: geo.transform, ink: paint.fg },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { sortedStops, stopAlpha, type BookPaint, type BookPaintStop } from '@trek/shared';
|
||||
import { useMemo, type ReactNode, type SVGProps } from 'react';
|
||||
import { linearLine, r2 } from './paintGeometry';
|
||||
import { patternGeometry, type PatternBox } from './patterns';
|
||||
import type { BookPaint } from '@trek/shared';
|
||||
import { useMemo, type SVGProps } from 'react';
|
||||
import { r2 } from './paintGeometry';
|
||||
import { paintParts } from './paintParts';
|
||||
import type { PatternBox } from './patterns';
|
||||
import { usePaintId } from './usePaintId';
|
||||
|
||||
/**
|
||||
@@ -25,84 +26,6 @@ import { usePaintId } from './usePaintId';
|
||||
* element's own id is kept in front only to make the markup readable.
|
||||
*/
|
||||
|
||||
function Stops({ stops }: { stops: readonly BookPaintStop[] }) {
|
||||
return (
|
||||
<>
|
||||
{sortedStops(stops).map((s, i) => (
|
||||
<stop
|
||||
// Two stops may share an offset (a hard edge), so the index keys them.
|
||||
key={i}
|
||||
offset={r2(s.at)}
|
||||
stopColor={s.color}
|
||||
stopOpacity={stopAlpha(s) < 1 ? stopAlpha(s) : undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** What a painted outline needs: the defs to put in the SVG, and how to fill the outline. */
|
||||
export interface PaintParts {
|
||||
defs: ReactNode;
|
||||
fill: string;
|
||||
fillOpacity?: number;
|
||||
/** A pattern's geometry, to draw clipped to the outline over its fill. */
|
||||
pattern: { clipId: string; layers: { d: string; opacity: number }[]; transform?: string; ink: string } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What drawing a paint over a box takes: the gradient definition and the fill
|
||||
* to point at it, or a pattern's geometry to draw clipped to the outline over
|
||||
* its ground. A shape's outline and the letters of a text both draw from it.
|
||||
*/
|
||||
export function paintParts(paint: BookPaint, box: PatternBox, id: string): PaintParts {
|
||||
switch (paint.type) {
|
||||
case 'solid':
|
||||
return {
|
||||
defs: null,
|
||||
fill: paint.color,
|
||||
fillOpacity: stopAlpha(paint) < 1 ? stopAlpha(paint) : undefined,
|
||||
pattern: null,
|
||||
};
|
||||
case 'linear': {
|
||||
const line = linearLine(paint.angle, box);
|
||||
return {
|
||||
defs: (
|
||||
<linearGradient id={id} gradientUnits="userSpaceOnUse" {...line}>
|
||||
<Stops stops={paint.stops} />
|
||||
</linearGradient>
|
||||
),
|
||||
fill: `url(#${id})`,
|
||||
pattern: null,
|
||||
};
|
||||
}
|
||||
case 'radial':
|
||||
return {
|
||||
defs: (
|
||||
<radialGradient
|
||||
id={id}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx={r2(box.x + paint.cx * box.w)}
|
||||
cy={r2(box.y + paint.cy * box.h)}
|
||||
r={r2((paint.r * Math.hypot(box.w, box.h)) / 2)}
|
||||
>
|
||||
<Stops stops={paint.stops} />
|
||||
</radialGradient>
|
||||
),
|
||||
fill: `url(#${id})`,
|
||||
pattern: null,
|
||||
};
|
||||
case 'pattern': {
|
||||
const geo = patternGeometry(paint, box);
|
||||
return {
|
||||
defs: null,
|
||||
fill: paint.bg ?? 'none',
|
||||
pattern: { clipId: `${id}-clip`, layers: geo.layers, transform: geo.transform, ink: paint.fg },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OutlineProps = Omit<SVGProps<SVGPathElement>, 'fill' | 'd'>;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user