diff --git a/client/src/components/Studio/TextEffectsView.tsx b/client/src/components/Studio/TextEffectsView.tsx index 016b403a3..d1b3a78b7 100644 --- a/client/src/components/Studio/TextEffectsView.tsx +++ b/client/src/components/Studio/TextEffectsView.tsx @@ -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 ( + + {parts.fill !== 'none' && } + + {pattern.layers.map((layer, i) => ( + + ))} + + + ); +} + 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(); } else if (pattern && letterPaint) { - // The pattern's ground and its marks, clipped to the letters. - layers.push( - - {letterPaint.fill !== 'none' && } - - {pattern.layers.map((layer, i) => ( - - ))} - - - ); + layers.push(); } else { layers.push(glyphs({ fill: letterPaint ? letterPaint.fill : (letterFill(el) ?? el.color) }, 'fill')); } diff --git a/client/src/components/Studio/paint/paintParts.tsx b/client/src/components/Studio/paint/paintParts.tsx new file mode 100644 index 000000000..8727ec7e7 --- /dev/null +++ b/client/src/components/Studio/paint/paintParts.tsx @@ -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) => ( + + ))} + + ); +} + +/** 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: ( + + + + ), + fill: `url(#${id})`, + pattern: null, + }; + } + case 'radial': + return { + defs: ( + + + + ), + 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 }, + }; + } + } +} diff --git a/client/src/components/Studio/paint/paintRender.tsx b/client/src/components/Studio/paint/paintRender.tsx index 95b28640c..dc9060436 100644 --- a/client/src/components/Studio/paint/paintRender.tsx +++ b/client/src/components/Studio/paint/paintRender.tsx @@ -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) => ( - - ))} - - ); -} - -/** 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: ( - - - - ), - fill: `url(#${id})`, - pattern: null, - }; - } - case 'radial': - return { - defs: ( - - - - ), - 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, 'fill' | 'd'>; /**