@popcraft/kit 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{agent-45ZGYKTC.js → agent-NTEAEZBN.js} +1 -1
- package/dist/chunks/{build-K6QBTLT7.js → build-W5O2ZVN7.js} +2 -2
- package/dist/chunks/{builtin-ZS3HJHZL.js → builtin-KX2SGDND.js} +3 -3
- package/dist/chunks/{check-template-RZ7IO333.js → check-template-B2KKE6BW.js} +2 -2
- package/dist/chunks/{chunk-ZKHYC3QO.js → chunk-37WES5PM.js} +2 -2
- package/dist/chunks/chunk-45JABNLR.js +161 -0
- package/dist/chunks/chunk-4OGILN7Y.js +141 -0
- package/dist/chunks/{chunk-63X37RCC.js → chunk-5NWVUAWR.js} +4 -4
- package/dist/chunks/{chunk-QREG6HEK.js → chunk-67SLLJJB.js} +40 -40
- package/dist/chunks/chunk-6DV5JZ5M.js +121 -0
- package/dist/chunks/chunk-7Q67MXXL.js +140 -0
- package/dist/chunks/{chunk-R5CW4CNP.js → chunk-A6GPFR5O.js} +11 -11
- package/dist/chunks/chunk-AISWLDDO.js +895 -0
- package/dist/chunks/{chunk-ADZXK6SM.js → chunk-AQGECTRJ.js} +1 -1
- package/dist/chunks/chunk-ASWWZNTY.js +130 -0
- package/dist/chunks/chunk-BNEFANT2.js +134 -0
- package/dist/chunks/{chunk-REJO7GXO.js → chunk-CFEKYNGQ.js} +2 -2
- package/dist/chunks/{chunk-5HG4LWF5.js → chunk-D4HEDPVN.js} +161 -0
- package/dist/chunks/chunk-DRESZDOP.js +130 -0
- package/dist/chunks/{chunk-3W2MSVJA.js → chunk-EBLHS2EG.js} +5 -5
- package/dist/chunks/chunk-EHTLT6XJ.js +131 -0
- package/dist/chunks/{chunk-2AQK4IIV.js → chunk-EVFG2K4L.js} +1 -1
- package/dist/chunks/{chunk-6TJIEERG.js → chunk-F4NTIOTT.js} +26 -26
- package/dist/chunks/chunk-FC7B22QS.js +150 -0
- package/dist/chunks/{chunk-V6COW3DB.js → chunk-G6MIC23V.js} +1 -1
- package/dist/chunks/{chunk-4QBQ4RAA.js → chunk-GODXMHO3.js} +4 -4
- package/dist/chunks/chunk-GT22XGDH.js +152 -0
- package/dist/chunks/chunk-HL4M53ZR.js +143 -0
- package/dist/chunks/{chunk-MPI6SCA7.js → chunk-IDL5KKIC.js} +5 -5
- package/dist/chunks/{chunk-HR7HCFPX.js → chunk-IM6HORNC.js} +5 -5
- package/dist/chunks/{chunk-UVMU6CXR.js → chunk-IWCZDFXL.js} +17 -17
- package/dist/chunks/{chunk-OCSLBM6W.js → chunk-JXAJ6OST.js} +1 -1
- package/dist/chunks/{chunk-5WGMPGKY.js → chunk-KS7DVWDB.js} +1 -1
- package/dist/chunks/chunk-NHDBQ4G7.js +137 -0
- package/dist/chunks/chunk-NKPU5TF2.js +103 -0
- package/dist/chunks/chunk-NO734F3F.js +146 -0
- package/dist/chunks/{chunk-ASWAIYNC.js → chunk-NR2SFSJU.js} +3 -3
- package/dist/chunks/{chunk-CJY4TNOS.js → chunk-O3BEBIDN.js} +3 -1
- package/dist/chunks/{chunk-UDO3ZU5T.js → chunk-OR6HMJRM.js} +1 -1
- package/dist/chunks/chunk-OTICJQ4T.js +267 -0
- package/dist/chunks/chunk-PWZZRGWI.js +141 -0
- package/dist/chunks/{chunk-AE6S2M2B.js → chunk-QX4U3VTU.js} +25 -25
- package/dist/chunks/{chunk-TRUFIU5W.js → chunk-R3YD6VL6.js} +3 -3
- package/dist/chunks/{chunk-YSYZW4VZ.js → chunk-SFPM3UJA.js} +7 -7
- package/dist/chunks/chunk-SNXQM4UO.js +115 -0
- package/dist/chunks/chunk-T3LV66CQ.js +139 -0
- package/dist/chunks/chunk-TDGIO2S7.js +145 -0
- package/dist/chunks/chunk-TFNV2UP4.js +87 -0
- package/dist/chunks/chunk-UJZXKOTP.js +136 -0
- package/dist/chunks/{chunk-QAVWMRRM.js → chunk-V5HI55XO.js} +2 -2
- package/dist/chunks/chunk-VVNPBBPL.js +174 -0
- package/dist/chunks/{chunk-22RNC42G.js → chunk-YHA45NK5.js} +1 -1
- package/dist/chunks/{chunk-27EVM272.js → chunk-YPMT4Y4N.js} +2 -2
- package/dist/chunks/chunk-YRSDFRCX.js +122 -0
- package/dist/chunks/chunk-ZNARVSMD.js +140 -0
- package/dist/chunks/{chunk-3DWOQ4LW.js → chunk-ZTBKAGBN.js} +2 -2
- package/dist/chunks/{client-6DUSRTZG.js → client-25NJXQWL.js} +1 -1
- package/dist/chunks/{client-XYPBNSQ7.js → client-7AQTKZHV.js} +2 -2
- package/dist/chunks/{client-TRFNJJHQ.js → client-LWBTUXQK.js} +2 -2
- package/dist/chunks/{client-actions-SSUZUEG7.js → client-actions-3HIRRSOL.js} +4 -4
- package/dist/chunks/{dangerous-UX4DHJ7U.js → dangerous-XJOHEWL6.js} +3 -3
- package/dist/chunks/{editor-seams-V7ZP7XH3.js → editor-seams-4DREZRTT.js} +9 -9
- package/dist/chunks/{editor-store-OIHZT244.js → editor-store-C4VWC23J.js} +1 -1
- package/dist/chunks/{export-watermark-4NAP5RVM.js → export-watermark-OKGRZNOU.js} +5 -5
- package/dist/chunks/{eyedropper-QXEY7LVL.js → eyedropper-WZYSTUTM.js} +1 -1
- package/dist/chunks/{generate-media-JAEHWNF6.js → generate-media-DFSHOPOI.js} +2 -2
- package/dist/chunks/{host-IKFYVZS7.js → host-4ZE7SIWR.js} +6 -6
- package/dist/chunks/{integrations-U5X2BHVK.js → integrations-LPC35FKX.js} +1 -1
- package/dist/chunks/{kit-modules-BAUMECYQ.js → kit-modules-BAKTDLOZ.js} +1 -1
- package/dist/chunks/{libraries-Q5TCHWGI.js → libraries-ACERXG2O.js} +5 -5
- package/dist/chunks/{live-4XQEGNLA.js → live-ZIVBFX3G.js} +3 -3
- package/dist/chunks/{media-tool-OSQR45HY.js → media-tool-2ZPIQKJD.js} +1 -1
- package/dist/chunks/{new-file-VVLWHEJ4.js → new-file-XS3KCMGM.js} +3 -3
- package/dist/chunks/{plugin-44ZW5UEP.js → plugin-6RNMUP5I.js} +2 -2
- package/dist/chunks/{plugin-import-3ISLDX7T.js → plugin-import-UC5BHZFE.js} +13 -13
- package/dist/chunks/{presence-WVU4OIUA.js → presence-TLDOMBPD.js} +2 -2
- package/dist/chunks/{project-EG7XFEJU.js → project-NMPK4R5M.js} +1 -1
- package/dist/chunks/{publish-site-WFIX42D3.js → publish-site-R46ZILSS.js} +6 -6
- package/dist/chunks/{publish-store-6BS7OIP4.js → publish-store-MIPHEDLV.js} +4 -4
- package/dist/chunks/{raster-actions-37XUQSVZ.js → raster-actions-7EI5THLB.js} +1 -1
- package/dist/chunks/{runner-HZ7MU6ER.js → runner-I66TTKKZ.js} +16 -16
- package/dist/chunks/{store-PHWBXKNY.js → store-7WMR3JKP.js} +5 -5
- package/dist/chunks/{sync-IJNKHQRF.js → sync-OFINS45Q.js} +5 -5
- package/dist/chunks/{tabs-G6OTEGA5.js → tabs-PEH6VX3X.js} +8 -8
- package/dist/chunks/{team-M2EN4JT5.js → team-GGW3O32G.js} +1 -1
- package/dist/chunks/{tools-WDFCY4BH.js → tools-TKCA46ZS.js} +2 -2
- package/dist/chunks/{ui-commands-QHFGR33P.js → ui-commands-6K4PYKU5.js} +11 -11
- package/dist/chunks/{versions-S7COCZSQ.js → versions-KHRNQFGF.js} +5 -5
- package/dist/index.js +4 -4
- package/dist/seed/files.json +1 -1
- package/dist/templates/count.js +1 -1
- package/dist/templates/index.js +125 -101
- package/dist/templates/index.json +756 -0
- package/dist/templates/motion/landing-hero/index.js +4 -4
- package/dist/templates/motion/launch-trailer/document.js +125 -101
- package/dist/templates/motion/launch-trailer/index.js +4 -4
- package/dist/templates/motion/launch-trailer/scenes.js +3 -3
- package/dist/templates/motion/template-wall.js +2 -2
- package/dist/templates/print/holidays-christmas-church-newsletter.js +94 -0
- package/dist/templates/print/holidays-christmas-eve-invitation.js +94 -0
- package/dist/templates/print/holidays-christmas-gift-tags.js +94 -0
- package/dist/templates/print/holidays-diwali-party-invitation.js +95 -0
- package/dist/templates/print/holidays-easter-activity-sheet.js +94 -0
- package/dist/templates/print/holidays-easter-egg-hunt-flyer.js +94 -0
- package/dist/templates/print/holidays-eid-dinner-invitation.js +95 -0
- package/dist/templates/print/holidays-eid-gift-tags.js +94 -0
- package/dist/templates/print/holidays-halloween-party-invitation.js +95 -0
- package/dist/templates/print/holidays-halloween-treat-labels.js +94 -0
- package/dist/templates/print/holidays-halloween-trunk-or-treat-poster.js +94 -0
- package/dist/templates/print/holidays-hanukkah-activity-sheet.js +94 -0
- package/dist/templates/print/holidays-hanukkah-party-invitation.js +94 -0
- package/dist/templates/print/holidays-lunar-new-year-banquet-menu.js +95 -0
- package/dist/templates/print/holidays-new-years-eve-poster.js +94 -0
- package/dist/templates/print/holidays-thanksgiving-dinner-menu.js +94 -0
- package/dist/templates/print/holidays-thanksgiving-place-cards.js +94 -0
- package/dist/templates/print/holidays-valentines-dinner-menu.js +94 -0
- package/dist/templates/saas/email-decks.js +2 -2
- package/dist/templates/saas/index.js +37 -37
- package/dist/templates/saas/launch-kit.js +2 -2
- package/dist/templates/saas/launch-video.js +2 -2
- package/dist/templates/saas/stores.js +2 -2
- package/dist/templates/saas/style-bento.js +2 -2
- package/dist/templates/saas/style-dark.js +2 -2
- package/dist/templates/saas/style-editorial.js +2 -2
- package/dist/templates/saas/style-gradient.js +2 -2
- package/dist/templates/saas/style-pastel.js +2 -2
- package/dist/templates/saas/style-swiss.js +2 -2
- package/dist/templates/slideshow/index.js +3 -3
- package/dist/templates/social/holidays-diwali-sweet-shop-post.js +96 -0
- package/dist/templates/social/holidays-lunar-new-year-bakery-post.js +96 -0
- package/dist/templates/social/holidays-new-years-eve-shop-hours.js +96 -0
- package/dist/templates/social/holidays-thanksgiving-pie-orders-post.js +96 -0
- package/dist/templates/social/holidays-valentines-florist-post.js +96 -0
- package/dist/templates/themes/black-friday-coupons.js +2 -2
- package/dist/templates/themes/black-friday-neon.js +2 -2
- package/dist/templates/themes/blueprint.js +2 -2
- package/dist/templates/themes/broadsheet.js +2 -2
- package/dist/templates/themes/cyberpunk.js +2 -2
- package/dist/templates/themes/departure-board.js +2 -2
- package/dist/templates/themes/festival-presale-family-a.js +2 -2
- package/dist/templates/themes/festival-presale-family-b.js +2 -2
- package/dist/templates/themes/holidays-shared.js +172 -0
- package/dist/templates/themes/holidays.js +107 -1
- package/dist/templates/themes/index.js +72 -48
- package/dist/templates/themes/lunar-new-year.js +2 -2
- package/dist/templates/themes/noir.js +2 -2
- package/dist/templates/themes/psychedelic.js +2 -2
- package/dist/templates/themes/space-opera.js +2 -2
- package/dist/templates/themes/studio.js +3 -3
- package/dist/templates/themes/tour-tickets.js +2 -2
- package/dist/templates/themes/vaporwave.js +2 -2
- package/package.json +1 -1
- package/src/templates/print/holidays-christmas-church-newsletter.ts +246 -0
- package/src/templates/print/holidays-christmas-eve-invitation.ts +121 -0
- package/src/templates/print/holidays-christmas-gift-tags.ts +117 -0
- package/src/templates/print/holidays-diwali-party-invitation.ts +98 -0
- package/src/templates/print/holidays-easter-activity-sheet.ts +108 -0
- package/src/templates/print/holidays-easter-egg-hunt-flyer.ts +118 -0
- package/src/templates/print/holidays-eid-dinner-invitation.ts +104 -0
- package/src/templates/print/holidays-eid-gift-tags.ts +94 -0
- package/src/templates/print/holidays-halloween-party-invitation.ts +101 -0
- package/src/templates/print/holidays-halloween-treat-labels.ts +86 -0
- package/src/templates/print/holidays-halloween-trunk-or-treat-poster.ts +148 -0
- package/src/templates/print/holidays-hanukkah-activity-sheet.ts +112 -0
- package/src/templates/print/holidays-hanukkah-party-invitation.ts +112 -0
- package/src/templates/print/holidays-lunar-new-year-banquet-menu.ts +110 -0
- package/src/templates/print/holidays-new-years-eve-poster.ts +117 -0
- package/src/templates/print/holidays-thanksgiving-dinner-menu.ts +110 -0
- package/src/templates/print/holidays-thanksgiving-place-cards.ts +80 -0
- package/src/templates/print/holidays-valentines-dinner-menu.ts +95 -0
- package/src/templates/social/holidays-diwali-sweet-shop-post.ts +121 -0
- package/src/templates/social/holidays-lunar-new-year-bakery-post.ts +110 -0
- package/src/templates/social/holidays-new-years-eve-shop-hours.ts +118 -0
- package/src/templates/social/holidays-thanksgiving-pie-orders-post.ts +115 -0
- package/src/templates/social/holidays-valentines-florist-post.ts +102 -0
- package/src/templates/themes/holidays-shared.ts +903 -0
- package/src/templates/themes/holidays.ts +24 -1
- package/types/templates/src/print/holidays-christmas-church-newsletter.d.ts +2 -0
- package/types/templates/src/print/holidays-christmas-eve-invitation.d.ts +2 -0
- package/types/templates/src/print/holidays-christmas-gift-tags.d.ts +2 -0
- package/types/templates/src/print/holidays-diwali-party-invitation.d.ts +2 -0
- package/types/templates/src/print/holidays-easter-activity-sheet.d.ts +2 -0
- package/types/templates/src/print/holidays-easter-egg-hunt-flyer.d.ts +2 -0
- package/types/templates/src/print/holidays-eid-dinner-invitation.d.ts +2 -0
- package/types/templates/src/print/holidays-eid-gift-tags.d.ts +2 -0
- package/types/templates/src/print/holidays-halloween-party-invitation.d.ts +2 -0
- package/types/templates/src/print/holidays-halloween-treat-labels.d.ts +2 -0
- package/types/templates/src/print/holidays-halloween-trunk-or-treat-poster.d.ts +2 -0
- package/types/templates/src/print/holidays-hanukkah-activity-sheet.d.ts +2 -0
- package/types/templates/src/print/holidays-hanukkah-party-invitation.d.ts +2 -0
- package/types/templates/src/print/holidays-lunar-new-year-banquet-menu.d.ts +2 -0
- package/types/templates/src/print/holidays-new-years-eve-poster.d.ts +2 -0
- package/types/templates/src/print/holidays-thanksgiving-dinner-menu.d.ts +2 -0
- package/types/templates/src/print/holidays-thanksgiving-place-cards.d.ts +2 -0
- package/types/templates/src/print/holidays-valentines-dinner-menu.d.ts +2 -0
- package/types/templates/src/social/holidays-diwali-sweet-shop-post.d.ts +2 -0
- package/types/templates/src/social/holidays-lunar-new-year-bakery-post.d.ts +2 -0
- package/types/templates/src/social/holidays-new-years-eve-shop-hours.d.ts +2 -0
- package/types/templates/src/social/holidays-thanksgiving-pie-orders-post.d.ts +2 -0
- package/types/templates/src/social/holidays-valentines-florist-post.d.ts +2 -0
- package/types/templates/src/themes/holidays-shared.d.ts +374 -0
- package/dist/chunks/chunk-OWZLREWX.js +0 -18
- package/dist/chunks/{chunk-IFJTWD4O.js → chunk-2R3MZKA2.js} +3 -3
- package/dist/chunks/{chunk-P67L32AF.js → chunk-3DBNMCGN.js} +3 -3
- package/dist/chunks/{chunk-DZDTVMUO.js → chunk-3VHOXVMF.js} +3 -3
- package/dist/chunks/{chunk-2PQI2R5L.js → chunk-64RMGOTG.js} +0 -0
- package/dist/chunks/{chunk-7BDCMMSD.js → chunk-6Y46SDFE.js} +3 -3
- package/dist/chunks/{chunk-6VCQ4ZEG.js → chunk-AFARXTNQ.js} +3 -3
- package/dist/chunks/{chunk-ITSYENCT.js → chunk-BI3M4RUH.js} +3 -3
- package/dist/chunks/{chunk-25FM5JIR.js → chunk-BP7CHO7K.js} +3 -3
- package/dist/chunks/{chunk-D22534DX.js → chunk-DPTFZDOS.js} +3 -3
- package/dist/chunks/{chunk-JWLR6K62.js → chunk-FNUM5W3W.js} +3 -3
- package/dist/chunks/{chunk-JPH6HOYU.js → chunk-HJQU5POV.js} +3 -3
- package/dist/chunks/{chunk-HQYZ4URA.js → chunk-IJ65QZLL.js} +3 -3
- package/dist/chunks/{chunk-47VVF5TM.js → chunk-INT2OPPY.js} +8 -8
- package/dist/chunks/{chunk-AYVAHMWQ.js → chunk-JPMLUHZX.js} +3 -3
- package/dist/chunks/{chunk-LFYNAAAQ.js → chunk-LMRTRVPC.js} +3 -3
- package/dist/chunks/{chunk-HJIMF4NA.js → chunk-NGMCQZA6.js} +3 -3
- package/dist/chunks/{chunk-B3ASV6KS.js → chunk-PQBYL7AI.js} +3 -3
- package/dist/chunks/{chunk-NHXNUDC5.js → chunk-QD4QORNR.js} +3 -3
- package/dist/chunks/{chunk-P6TPDLAF.js → chunk-S3C6L5KF.js} +3 -3
- package/dist/chunks/{chunk-TIRPXUK2.js → chunk-S7L2D7NH.js} +3 -3
- package/dist/chunks/{chunk-QFGFR5FQ.js → chunk-TYOOL27R.js} +3 -3
- package/dist/chunks/{chunk-32V4D45K.js → chunk-TYW7EXV6.js} +3 -3
- package/dist/chunks/{chunk-3QIDCP4M.js → chunk-VOEAQIQT.js} +3 -3
- package/dist/chunks/{chunk-ASVDOX62.js → chunk-VVUQG4ME.js} +3 -3
- package/dist/chunks/{chunk-ZXZ22FKF.js → chunk-XTWKSMJC.js} +3 -3
- package/dist/chunks/{chunk-6NG5K7WN.js → chunk-YHEMML5Q.js} +3 -3
- package/dist/chunks/{chunk-MVFPEBMY.js → chunk-ZKRSVNY4.js} +3 -3
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
// What the holidays pack draws with: vectors boxed tight round what they draw (so a resized copy is worked out again,
|
|
2
|
+
// not stretched), and the small pictures each holiday is made of — baubles and pine, a menorah and dreidels,
|
|
3
|
+
// fireworks, hearts and roses, eggs and tulips, pumpkins and bats, maple leaves and wheat, diyas, marigolds and
|
|
4
|
+
// rangoli, crescents and lanterns, plum blossom and red lanterns. Every picture is plain path data in the parent's
|
|
5
|
+
// px, coloured by the template through a palette role. Only this pack's own things live here; anything general is
|
|
6
|
+
// in templates/src/shared.
|
|
7
|
+
|
|
8
|
+
import type { VectorPath, VectorPoint } from '@/lib/document/types'
|
|
9
|
+
import type { Kit, VariantSpec } from '../kit'
|
|
10
|
+
import { pt3, r3, rng, type P } from '../shared/draw'
|
|
11
|
+
|
|
12
|
+
type VectorOpts = Parameters<Kit['vector']>[5]
|
|
13
|
+
|
|
14
|
+
// ── Placing drawings ──
|
|
15
|
+
|
|
16
|
+
/** `paths` (in the parent's px) boxed tight: the box, and the paths moved into it, handles and all. */
|
|
17
|
+
export function boxTight(paths: VectorPath[]): { x: number; y: number; width: number; height: number; paths: VectorPath[] } {
|
|
18
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity
|
|
19
|
+
for (const p of paths) for (const q of p.points) {
|
|
20
|
+
for (const c of [q, q.handleIn, q.handleOut]) if (c) { x0 = Math.min(x0, c.x); y0 = Math.min(y0, c.y); x1 = Math.max(x1, c.x); y1 = Math.max(y1, c.y) }
|
|
21
|
+
}
|
|
22
|
+
x0 = Math.floor(x0); y0 = Math.floor(y0)
|
|
23
|
+
const mv = (q: { x: number; y: number }) => pt3(q.x - x0, q.y - y0)
|
|
24
|
+
const moved = paths.map(p => ({ ...p, points: p.points.map(q => ({ ...q, ...mv(q), ...(q.handleIn ? { handleIn: mv(q.handleIn) } : {}), ...(q.handleOut ? { handleOut: mv(q.handleOut) } : {}) })) }))
|
|
25
|
+
return { x: x0, y: y0, width: Math.max(1, Math.ceil(x1 - x0)), height: Math.max(1, Math.ceil(y1 - y0)), paths: moved }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A drawing as named layers: what `draw` makes once, and `redraw` patches in a resized copy. */
|
|
29
|
+
export interface Drawn { name: string; paths: VectorPath[]; color: string; o?: VectorOpts }
|
|
30
|
+
|
|
31
|
+
/** Draw every part of `parts` on `parent`, each a vector boxed tight round its paths. Empty parts are skipped. */
|
|
32
|
+
export function draw(k: Kit, parent: string, parts: readonly Drawn[]): string[] {
|
|
33
|
+
return parts.filter(p => p.paths.length).map(p => {
|
|
34
|
+
const b = boxTight(p.paths)
|
|
35
|
+
return k.vector(parent, b.width, b.height, b.paths, p.color, { name: p.name, ...p.o, absolute: { x: b.x, y: b.y } })
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The same drawing worked out for a copy of another size: each layer named as a part, in order, takes the part's box
|
|
41
|
+
* and paths (magic resize stretches a vector's box but not its paths).
|
|
42
|
+
*/
|
|
43
|
+
export function redraw(k: Kit, root: string, parts: readonly Drawn[]) {
|
|
44
|
+
const seen = new Map<string, number>()
|
|
45
|
+
for (const p of parts.filter(q => q.paths.length)) {
|
|
46
|
+
const i = seen.get(p.name) ?? 0
|
|
47
|
+
seen.set(p.name, i + 1)
|
|
48
|
+
const id = k.named(root, p.name)[i]
|
|
49
|
+
if (!id) continue
|
|
50
|
+
const b = boxTight(p.paths)
|
|
51
|
+
const w = p.o?.strokeWidth
|
|
52
|
+
const strokes = (k.node(id) as { strokes?: { width: unknown }[] }).strokes ?? []
|
|
53
|
+
k.patch(id, { x: b.x, y: b.y, width: b.width, height: b.height, paths: b.paths, ...(w !== undefined ? { strokes: strokes.map(st => ({ ...st, width: w })) } : {}) } as never)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Restyle every layer named `layer` under `root`. */
|
|
58
|
+
export function restyleAll(k: Kit, root: string, pairs: readonly (readonly [string, string])[]) {
|
|
59
|
+
for (const [layer, style] of pairs) for (const n of k.named(root, layer)) k.restyle(n, style)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Scale the height of every spacer named `Gap` under `root` by `s`. */
|
|
63
|
+
export function scaleGaps(k: Kit, root: string, s: number) {
|
|
64
|
+
for (const n of k.named(root, 'Gap')) k.patch(n, { height: Math.max(2, Math.round(k.node(n).height * s)) })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A print size made from the master: the art worked out again at the new size, the padding scaled from the master's
|
|
69
|
+
* by the ratio of widths, the gaps scaled, and the type restyled.
|
|
70
|
+
*/
|
|
71
|
+
export function printVariant(preset: string, o: { art: (w: number, h: number) => Drawn[]; pad: { top: number; right: number; bottom: number; left: number }; masterWidth: number; styles?: readonly (readonly [string, string])[]; more?: (k: Kit, id: string, s: number) => void }): VariantSpec {
|
|
72
|
+
return {
|
|
73
|
+
preset,
|
|
74
|
+
adjust: (k, id) => {
|
|
75
|
+
const { width: w, height: h } = k.node(id)
|
|
76
|
+
const s = w / o.masterWidth
|
|
77
|
+
redraw(k, id, o.art(w, h))
|
|
78
|
+
k.patch(id, { padding: { top: Math.round(o.pad.top * s), right: Math.round(o.pad.right * s), bottom: Math.round(o.pad.bottom * s), left: Math.round(o.pad.left * s) } })
|
|
79
|
+
restyleAll(k, id, o.styles ?? [])
|
|
80
|
+
scaleGaps(k, id, s)
|
|
81
|
+
o.more?.(k, id, s)
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Basic shapes ──
|
|
87
|
+
|
|
88
|
+
const C = 0.5523
|
|
89
|
+
|
|
90
|
+
/** A closed ellipse through four points with handles. */
|
|
91
|
+
export function disc(cx: number, cy: number, r: number, ry = r): VectorPath {
|
|
92
|
+
return { closed: true, points: [
|
|
93
|
+
{ ...pt3(cx, cy - ry), handleIn: pt3(cx - r * C, cy - ry), handleOut: pt3(cx + r * C, cy - ry) },
|
|
94
|
+
{ ...pt3(cx + r, cy), handleIn: pt3(cx + r, cy - ry * C), handleOut: pt3(cx + r, cy + ry * C) },
|
|
95
|
+
{ ...pt3(cx, cy + ry), handleIn: pt3(cx + r * C, cy + ry), handleOut: pt3(cx - r * C, cy + ry) },
|
|
96
|
+
{ ...pt3(cx - r, cy), handleIn: pt3(cx - r, cy + ry * C), handleOut: pt3(cx - r, cy - ry * C) },
|
|
97
|
+
] }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A rectangle with round corners of radius `r`. */
|
|
101
|
+
export function roundRect(x: number, y: number, w: number, h: number, r: number): VectorPath {
|
|
102
|
+
r = Math.min(r, w / 2, h / 2)
|
|
103
|
+
const k = r * (1 - C)
|
|
104
|
+
return { closed: true, points: [
|
|
105
|
+
{ ...pt3(x + r, y), handleIn: pt3(x + k, y) }, { ...pt3(x + w - r, y), handleOut: pt3(x + w - k, y) },
|
|
106
|
+
{ ...pt3(x + w, y + r), handleIn: pt3(x + w, y + k) }, { ...pt3(x + w, y + h - r), handleOut: pt3(x + w, y + h - k) },
|
|
107
|
+
{ ...pt3(x + w - r, y + h), handleIn: pt3(x + w - k, y + h) }, { ...pt3(x + r, y + h), handleOut: pt3(x + k, y + h) },
|
|
108
|
+
{ ...pt3(x, y + h - r), handleIn: pt3(x, y + h - k) }, { ...pt3(x, y + r), handleOut: pt3(x, y + k) },
|
|
109
|
+
] }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A plain rectangle. */
|
|
113
|
+
export const rectPath = (x: number, y: number, w: number, h: number): VectorPath => ({ closed: true, points: [pt3(x, y), pt3(x + w, y), pt3(x + w, y + h), pt3(x, y + h)] })
|
|
114
|
+
|
|
115
|
+
/** A straight stroke. */
|
|
116
|
+
export const seg = (a: P, b: P): VectorPath => ({ closed: false, points: [pt3(a.x, a.y), pt3(b.x, b.y)] })
|
|
117
|
+
|
|
118
|
+
/** A polygon through `pts`. */
|
|
119
|
+
export const polyP = (pts: P[], closed = true): VectorPath => ({ closed, points: pts.map(p => pt3(p.x, p.y)) })
|
|
120
|
+
|
|
121
|
+
/** A point at `a` radians and `r` from (cx, cy). */
|
|
122
|
+
const at = (cx: number, cy: number, r: number, a: number): P => ({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r })
|
|
123
|
+
|
|
124
|
+
/** An open arc of a circle from angle `a0` to `a1`, as a polyline of `n` steps. */
|
|
125
|
+
export function arc(cx: number, cy: number, r: number, a0: number, a1: number, n = 16, ry = r): VectorPath {
|
|
126
|
+
return { closed: false, points: Array.from({ length: n + 1 }, (_, i) => { const a = a0 + ((a1 - a0) * i) / n; return pt3(cx + Math.cos(a) * r, cy + Math.sin(a) * ry) }) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** An n-pointed star (outer radius `R`, inner `r`), its first point up. */
|
|
130
|
+
export function starPath(cx: number, cy: number, R: number, r = R * 0.42, n = 5, turn = 0): VectorPath {
|
|
131
|
+
return polyP(Array.from({ length: n * 2 }, (_, i) => at(cx, cy, i % 2 ? r : R, -Math.PI / 2 + turn + (i * Math.PI) / n)))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A four-pointed sparkle with curved sides. */
|
|
135
|
+
export function sparkle(cx: number, cy: number, s: number, pinch = 0.18): VectorPath {
|
|
136
|
+
const q = s * pinch
|
|
137
|
+
const pts: VectorPoint[] = []
|
|
138
|
+
for (let i = 0; i < 4; i++) {
|
|
139
|
+
const a = -Math.PI / 2 + (i * Math.PI) / 2
|
|
140
|
+
const tip = at(cx, cy, s, a)
|
|
141
|
+
pts.push({ ...pt3(tip.x, tip.y) })
|
|
142
|
+
const mid = at(cx, cy, q, a + Math.PI / 4)
|
|
143
|
+
pts.push({ ...pt3(mid.x, mid.y) })
|
|
144
|
+
}
|
|
145
|
+
return { closed: true, points: pts }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A heart, `s` wide, its point at the bottom, tilted by `turn` radians. */
|
|
149
|
+
export function heart(cx: number, cy: number, s: number, turn = 0): VectorPath {
|
|
150
|
+
const u = s / 2
|
|
151
|
+
const raw: [number, number, [number, number]?, [number, number]?][] = [
|
|
152
|
+
[0, -0.45, [-0.08, -0.85], [0.08, -0.85]],
|
|
153
|
+
[0.62, -0.78, [0.32, -1.0], [0.95, -0.55]],
|
|
154
|
+
[0.98, -0.18, [1.02, -0.45], [0.92, 0.18]],
|
|
155
|
+
[0, 0.95, [0.45, 0.55], [-0.45, 0.55]],
|
|
156
|
+
[-0.98, -0.18, [-0.92, 0.18], [-1.02, -0.45]],
|
|
157
|
+
[-0.62, -0.78, [-0.95, -0.55], [-0.32, -1.0]],
|
|
158
|
+
]
|
|
159
|
+
const c = Math.cos(turn), sn = Math.sin(turn)
|
|
160
|
+
const tr = (x: number, y: number) => pt3(cx + (x * c - y * sn) * u, cy + (x * sn + y * c) * u + u * 0.08)
|
|
161
|
+
// handleIn is the control before the point, handleOut after (clockwise).
|
|
162
|
+
return { closed: true, points: raw.map(([x, y, hi, ho]) => ({ ...tr(x, y), ...(hi ? { handleIn: tr(hi[0], hi[1]) } : {}), ...(ho ? { handleOut: tr(ho[0], ho[1]) } : {}) })) }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A pointed leaf from `base`, `len` long and `w` wide, pointing at `angle` (radians, 0 = right). */
|
|
166
|
+
export function leaf(base: P, len: number, w: number, angle: number): VectorPath {
|
|
167
|
+
const c = Math.cos(angle), s = Math.sin(angle)
|
|
168
|
+
const p = (u: number, v: number) => pt3(base.x + u * c - v * s, base.y + u * s + v * c)
|
|
169
|
+
return { closed: true, points: [
|
|
170
|
+
{ ...p(0, 0), handleIn: p(len * 0.3, w * 0.62), handleOut: p(len * 0.3, -w * 0.62) },
|
|
171
|
+
{ ...p(len, 0), handleIn: p(len * 0.72, -w * 0.5), handleOut: p(len * 0.72, w * 0.5) },
|
|
172
|
+
] }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A rounded petal (a teardrop from the centre outwards). */
|
|
176
|
+
export function petal(base: P, len: number, w: number, angle: number): VectorPath {
|
|
177
|
+
const c = Math.cos(angle), s = Math.sin(angle)
|
|
178
|
+
const p = (u: number, v: number) => pt3(base.x + u * c - v * s, base.y + u * s + v * c)
|
|
179
|
+
return { closed: true, points: [
|
|
180
|
+
{ ...p(0, 0), handleIn: p(len * 0.15, w * 0.5), handleOut: p(len * 0.15, -w * 0.5) },
|
|
181
|
+
{ ...p(len, 0), handleIn: p(len * 1.02, -w * 0.62), handleOut: p(len * 1.02, w * 0.62) },
|
|
182
|
+
] }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A flame (a teardrop pointing up), `w` wide and `h` tall, standing on (cx, cy). */
|
|
186
|
+
export function flame(cx: number, cy: number, w: number, h: number): VectorPath {
|
|
187
|
+
return { closed: true, points: [
|
|
188
|
+
{ ...pt3(cx, cy - h), handleIn: pt3(cx - w * 0.1, cy - h * 0.7), handleOut: pt3(cx + w * 0.1, cy - h * 0.7) },
|
|
189
|
+
{ ...pt3(cx + w / 2, cy - h * 0.28), handleIn: pt3(cx + w * 0.5, cy - h * 0.5), handleOut: pt3(cx + w * 0.5, cy - h * 0.05) },
|
|
190
|
+
{ ...pt3(cx, cy), handleIn: pt3(cx + w * 0.3, cy), handleOut: pt3(cx - w * 0.3, cy) },
|
|
191
|
+
{ ...pt3(cx - w / 2, cy - h * 0.28), handleIn: pt3(cx - w * 0.5, cy - h * 0.05), handleOut: pt3(cx - w * 0.5, cy - h * 0.5) },
|
|
192
|
+
] }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A crescent moon, `r` its radius, the bite `bite` (0…1) of it taken from the `side` (1 = bite on the right). */
|
|
196
|
+
export function crescent(cx: number, cy: number, r: number, bite = 0.62, side = 1, turn = 0): VectorPath {
|
|
197
|
+
// The outer arc, then the inner (a larger circle offset to the side) back along the bite.
|
|
198
|
+
const d = r * bite, R2 = r * 0.92
|
|
199
|
+
const pts: P[] = []
|
|
200
|
+
// Where the two circles cross.
|
|
201
|
+
const x = (r * r - R2 * R2 + d * d) / (2 * d), y = Math.sqrt(Math.max(0, r * r - x * x))
|
|
202
|
+
const a0 = Math.atan2(y, x), a1 = Math.atan2(-y, x)
|
|
203
|
+
for (let i = 0; i <= 28; i++) { const a = a0 + ((a1 + 2 * Math.PI - a0) * i) / 28; pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r }) }
|
|
204
|
+
const b0 = Math.atan2(-y, x - d), b1 = Math.atan2(y, x - d)
|
|
205
|
+
// Back along the inner circle's near side (through its leftmost point, angle π).
|
|
206
|
+
const span = (b1 > b0 ? b1 - 2 * Math.PI : b1) - b0
|
|
207
|
+
for (let i = 1; i < 20; i++) { const a = b0 + (span * i) / 20; pts.push({ x: d + Math.cos(a) * R2, y: Math.sin(a) * R2 }) }
|
|
208
|
+
const c = Math.cos(turn), s = Math.sin(turn)
|
|
209
|
+
return polyP(pts.map(p => ({ x: cx + side * (p.x * c - p.y * s), y: cy + p.x * s + p.y * c })))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Scattered confetti over a `w` × `h` box: little bars and dots, kept off the `keepOut` boxes. */
|
|
213
|
+
export function confetti(w: number, h: number, n: number, seed: number, size = 10, keepOut: readonly { x: number; y: number; w: number; h: number }[] = []): { bars: VectorPath[]; dots: VectorPath[] } {
|
|
214
|
+
const r = rng(seed), bars: VectorPath[] = [], dots: VectorPath[] = []
|
|
215
|
+
for (let i = 0, made = 0; made < n && i < n * 8; i++) {
|
|
216
|
+
const x = r() * w, y = r() * h
|
|
217
|
+
if (keepOut.some(b => x > b.x && x < b.x + b.w && y > b.y && y < b.y + b.h)) continue
|
|
218
|
+
made++
|
|
219
|
+
if (r() < 0.6) {
|
|
220
|
+
const a = r() * Math.PI, l = size * (0.7 + r() * 0.8), t = size * 0.32
|
|
221
|
+
const c = Math.cos(a), s = Math.sin(a)
|
|
222
|
+
const p = (u: number, v: number) => ({ x: x + u * c - v * s, y: y + u * s + v * c })
|
|
223
|
+
bars.push(polyP([p(-l / 2, -t / 2), p(l / 2, -t / 2), p(l / 2, t / 2), p(-l / 2, t / 2)]))
|
|
224
|
+
} else dots.push(disc(x, y, size * (0.22 + r() * 0.2)))
|
|
225
|
+
}
|
|
226
|
+
return { bars, dots }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Specks of grain over a `w` × `h` box (paper fibre, chalk, sparkle dust). */
|
|
230
|
+
export function specks(w: number, h: number, n: number, seed: number, size = 2): VectorPath[] {
|
|
231
|
+
const r = rng(seed)
|
|
232
|
+
return Array.from({ length: n }, () => { const x = r() * w, y = r() * h, s = size * (0.5 + r()); return polyP([{ x, y }, { x: x + s, y: y + r() * s * 0.4 }, { x: x + s * 0.5, y: y + s }]) })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A box. */
|
|
236
|
+
export interface Rect { x: number; y: number; w: number; h: number }
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The bands of a `w` × `h` sheet round a box kept clear for type: above it, below it, and the two margins beside it.
|
|
240
|
+
* Scatter (snow, confetti, stars) is drawn as one layer per band, so no layer's box lies under the words: the
|
|
241
|
+
* contrast check reads a layer's whole box as the ground of any text over it.
|
|
242
|
+
*/
|
|
243
|
+
export function bandsAround(w: number, h: number, keep: Rect, bleed = 0): Rect[] {
|
|
244
|
+
return [
|
|
245
|
+
{ x: -bleed, y: -bleed, w: w + 2 * bleed, h: keep.y + bleed },
|
|
246
|
+
{ x: -bleed, y: keep.y + keep.h, w: w + 2 * bleed, h: h - keep.y - keep.h + bleed },
|
|
247
|
+
{ x: -bleed, y: keep.y, w: keep.x + bleed, h: keep.h },
|
|
248
|
+
{ x: keep.x + keep.w, y: keep.y, w: w - keep.x - keep.w + bleed, h: keep.h },
|
|
249
|
+
].filter(b => b.w > 2 && b.h > 2)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One part per band: `paths(band, i)` drawn in each, under the same name. */
|
|
253
|
+
export function perBand(name: string, color: string, bands: readonly Rect[], paths: (b: Rect, i: number) => VectorPath[], o?: VectorOpts): Drawn[] {
|
|
254
|
+
return bands.map((b, i) => ({ name, color, paths: paths(b, i), ...(o ? { o } : {}) }))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Specks scattered over one band. */
|
|
258
|
+
export const specksIn = (b: Rect, density: number, seed: number, size = 2) => specks(b.w, b.h, Math.max(1, Math.round((b.w * b.h) / density)), seed, size).map(p => ({ ...p, points: p.points.map(q => pt3(q.x + b.x, q.y + b.y)) }))
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A small ornament between two rules (a lozenge, a star, a heart…), as a component: the pack's dividers and
|
|
262
|
+
* kicker flourishes, so a person restyles every one at once.
|
|
263
|
+
*/
|
|
264
|
+
export function flourishComponent(k: Kit, name: string, role: string, mark: VectorPath, o: { rule?: number; size?: number; gap?: number } = {}) {
|
|
265
|
+
if (k.hasComponent(name)) return
|
|
266
|
+
const size = o.size ?? 9
|
|
267
|
+
k.component(name, { direction: 'HORIZONTAL', width: 'HUG', gap: o.gap ?? 6, align: 'CENTER' }, id => {
|
|
268
|
+
k.rect(id, o.rule ?? 28, 1, role, { name: 'Rule' })
|
|
269
|
+
const b = boxTight([mark])
|
|
270
|
+
k.vector(id, size, size * (b.height / Math.max(1, b.width)), b.paths.map(p => ({ ...p, points: p.points.map(q => scalePt(q, size / Math.max(1, b.width))) })), role, { name: 'Mark' })
|
|
271
|
+
k.rect(id, o.rule ?? 28, 1, role, { name: 'Rule' })
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const scalePt = (q: VectorPoint, f: number): VectorPoint => ({ ...q, ...pt3(q.x * f, q.y * f), ...(q.handleIn ? { handleIn: pt3(q.handleIn.x * f, q.handleIn.y * f) } : {}), ...(q.handleOut ? { handleOut: pt3(q.handleOut.x * f, q.handleOut.y * f) } : {}) })
|
|
276
|
+
|
|
277
|
+
// ── Christmas ──
|
|
278
|
+
|
|
279
|
+
/** A glass bauble hanging from `top`: its body, its cap, the hook and a band across it. */
|
|
280
|
+
export function bauble(cx: number, top: number, r: number, o: { string?: number } = {}): { body: VectorPath[]; cap: VectorPath[]; band: VectorPath[]; shine: VectorPath[]; string: VectorPath[]; shade: VectorPath[] } {
|
|
281
|
+
const s = o.string ?? 0
|
|
282
|
+
const cy = top + s + r * 0.28 + r
|
|
283
|
+
return {
|
|
284
|
+
string: s > 0 ? [seg({ x: cx, y: top }, { x: cx, y: top + s })] : [],
|
|
285
|
+
cap: [rectPath(cx - r * 0.2, top + s, r * 0.4, r * 0.3), disc(cx, top + s - r * 0.04, r * 0.09)],
|
|
286
|
+
body: [disc(cx, cy, r)],
|
|
287
|
+
band: [arc(cx, cy, r * 0.98, Math.PI * 0.05, Math.PI * 0.95, 14, r * 0.3), arc(cx, cy + r * 0.1, r * 0.99, Math.PI * 0.08, Math.PI * 0.92, 14, r * 0.42)].map(p => ({ ...p })),
|
|
288
|
+
shine: [leaf({ x: cx - r * 0.62, y: cy - r * 0.05 }, r * 0.55, r * 0.16, -1.25), disc(cx - r * 0.42, cy - r * 0.62, r * 0.07)],
|
|
289
|
+
shade: [crescent(cx, cy, r, 0.36, -1, -0.75)],
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** A sprig of pine along a → b → c: a stem and fine needles in two tones. */
|
|
294
|
+
export function pine(a: P, b: P, c: P, o: { needle: number; seed: number; density?: number }): { stem: VectorPath; dark: VectorPath[]; light: VectorPath[] } {
|
|
295
|
+
const r = rng(o.seed)
|
|
296
|
+
const n = Math.round(26 * (o.density ?? 1))
|
|
297
|
+
const pts = Array.from({ length: 13 }, (_, i) => along(a, b, c, i / 12).p)
|
|
298
|
+
const dark: VectorPath[] = [], light: VectorPath[] = []
|
|
299
|
+
for (let i = 0; i < n; i++) {
|
|
300
|
+
const t = 0.04 + (0.94 * i) / n
|
|
301
|
+
const { p, angle } = along(a, b, c, t)
|
|
302
|
+
const len = o.needle * (1 - t * 0.45) * (0.85 + r() * 0.3)
|
|
303
|
+
for (const side of [-1, 1]) {
|
|
304
|
+
const tilt = angle + side * (0.55 + r() * 0.2)
|
|
305
|
+
const end = at(p.x, p.y, len, tilt)
|
|
306
|
+
;((i + (side > 0 ? 1 : 0)) % 2 ? dark : light).push(seg(p, end))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { stem: polyP(pts, false), dark, light }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** A holly leaf with spines, `len` long, pointing at `angle`. */
|
|
313
|
+
export function holly(base: P, len: number, angle: number): VectorPath {
|
|
314
|
+
const c = Math.cos(angle), s = Math.sin(angle)
|
|
315
|
+
const p = (u: number, v: number) => ({ x: base.x + u * c - v * s, y: base.y + u * s + v * c })
|
|
316
|
+
const w = len * 0.24, pts: P[] = [p(0, 0)]
|
|
317
|
+
for (let i = 1; i <= 4; i++) { const u = (len * i) / 5; const bulge = Math.sin((Math.PI * i) / 5) * w; pts.push(p(u - len * 0.08, bulge * 0.55), p(u, bulge * 1.05)) }
|
|
318
|
+
pts.push(p(len, 0))
|
|
319
|
+
for (let i = 4; i >= 1; i--) { const u = (len * i) / 5; const bulge = Math.sin((Math.PI * i) / 5) * w; pts.push(p(u, -bulge * 1.05), p(u - len * 0.08, -bulge * 0.55)) }
|
|
320
|
+
return polyP(pts)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** A six-armed snowflake of strokes, radius `r`. */
|
|
324
|
+
export function snowflake(cx: number, cy: number, r: number, turn = 0): VectorPath[] {
|
|
325
|
+
const out: VectorPath[] = []
|
|
326
|
+
for (let i = 0; i < 6; i++) {
|
|
327
|
+
const a = turn + (i * Math.PI) / 3
|
|
328
|
+
out.push(seg({ x: cx, y: cy }, at(cx, cy, r, a)))
|
|
329
|
+
for (const f of [0.45, 0.72]) {
|
|
330
|
+
const p = at(cx, cy, r * f, a), l = r * (f < 0.6 ? 0.3 : 0.2)
|
|
331
|
+
out.push(seg(p, at(p.x, p.y, l, a + 0.75)), seg(p, at(p.x, p.y, l, a - 0.75)))
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return out
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** A point and its direction along a quadratic curve a → b → c at `t`. */
|
|
338
|
+
export function along(a: P, b: P, c: P, t: number): { p: P; angle: number } {
|
|
339
|
+
const u = 1 - t
|
|
340
|
+
const p = { x: u * u * a.x + 2 * u * t * b.x + t * t * c.x, y: u * u * a.y + 2 * u * t * b.y + t * t * c.y }
|
|
341
|
+
const d = { x: 2 * u * (b.x - a.x) + 2 * t * (c.x - b.x), y: 2 * u * (b.y - a.y) + 2 * t * (c.y - b.y) }
|
|
342
|
+
return { p, angle: Math.atan2(d.y, d.x) }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** A quadratic curve a → b → c as an open polyline of `n` steps. */
|
|
346
|
+
export const quad = (a: P, b: P, c: P, n = 20): VectorPath => polyP(Array.from({ length: n + 1 }, (_, i) => along(a, b, c, i / n).p), false)
|
|
347
|
+
|
|
348
|
+
/** A sprig of leaves along a → b → c, in two tones, with berries. */
|
|
349
|
+
export function sprig(a: P, b: P, c: P, o: { leaves: number; size: number; seed: number; berries?: number; narrow?: number }): { stem: VectorPath; dark: VectorPath[]; light: VectorPath[]; berries: VectorPath[] } {
|
|
350
|
+
const r = rng(o.seed)
|
|
351
|
+
const dark: VectorPath[] = [], light: VectorPath[] = [], berries: VectorPath[] = []
|
|
352
|
+
for (let i = 0; i < o.leaves; i++) {
|
|
353
|
+
const t = 0.08 + (0.9 * i) / o.leaves
|
|
354
|
+
const { p, angle } = along(a, b, c, t)
|
|
355
|
+
const len = o.size * (1 - t * 0.5) * (0.85 + r() * 0.3), w = len * (o.narrow ?? 0.42)
|
|
356
|
+
for (const side of [-1, 1]) (side < 0 === (i % 2 === 0) ? dark : light).push(leaf(p, len, w, angle + side * (0.75 + r() * 0.25)))
|
|
357
|
+
}
|
|
358
|
+
const tip = along(a, b, c, 1)
|
|
359
|
+
dark.push(leaf(tip.p, o.size * 0.55, o.size * 0.22, tip.angle))
|
|
360
|
+
for (let i = 0; i < (o.berries ?? 0); i++) {
|
|
361
|
+
const { p, angle } = along(a, b, c, 0.2 + r() * 0.65)
|
|
362
|
+
const off = (r() > 0.5 ? 1 : -1) * o.size * 0.42
|
|
363
|
+
berries.push(disc(p.x - Math.sin(angle) * off, p.y + Math.cos(angle) * off, o.size * 0.1))
|
|
364
|
+
}
|
|
365
|
+
return { stem: quad(a, b, c), dark, light, berries }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Hanukkah ──
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* A hanukkiah (the Hanukkah menorah) of nine branches standing on `base` (the middle of its foot), `w` wide and `h`
|
|
372
|
+
* tall to the cups, the shamash in the middle standing higher. Candles stand only for the nights so far, placed
|
|
373
|
+
* from the right as they are, and every one placed is lit, the shamash too. Returns the stand (strokes), its foot,
|
|
374
|
+
* the candles, the empty cups and the flames.
|
|
375
|
+
*/
|
|
376
|
+
export function menorah(cx: number, base: number, w: number, h: number, nights = 8): { stand: VectorPath[]; candles: VectorPath[]; flames: VectorPath[]; foot: VectorPath[]; cups: VectorPath[] } {
|
|
377
|
+
const stand: VectorPath[] = [], candles: VectorPath[] = [], flames: VectorPath[] = [], cups: VectorPath[] = []
|
|
378
|
+
const cupY = base - h, armBase = base - h * 0.32
|
|
379
|
+
const step = w / 8
|
|
380
|
+
stand.push(seg({ x: cx, y: base - h * 0.06 }, { x: cx, y: cupY - h * 0.1 }))
|
|
381
|
+
for (let i = 1; i <= 4; i++) stand.push(arc(cx, cupY, step * i, 0, Math.PI, 18, armBase - cupY))
|
|
382
|
+
const foot = [polyP([{ x: cx - w * 0.2, y: base }, { x: cx + w * 0.2, y: base }, { x: cx + w * 0.05, y: base - h * 0.08 }, { x: cx - w * 0.05, y: base - h * 0.08 }])]
|
|
383
|
+
const cw = step * 0.34, ch = h * 0.3
|
|
384
|
+
for (let i = -4; i <= 4; i++) {
|
|
385
|
+
const x = cx + i * step, top = i === 0 ? cupY - h * 0.1 : cupY
|
|
386
|
+
cups.push(rectPath(x - cw * 0.85, top - h * 0.03, cw * 1.7, h * 0.04))
|
|
387
|
+
// Night one is the far right (as you face it), then leftwards.
|
|
388
|
+
const rank = i > 0 ? 4 - i : 3 - i
|
|
389
|
+
if (i !== 0 && rank >= nights) continue
|
|
390
|
+
candles.push(rectPath(x - cw / 2, top - h * 0.03 - ch, cw, ch))
|
|
391
|
+
flames.push(flame(x, top - h * 0.03 - ch - h * 0.015, cw * 1.25, ch * 0.62))
|
|
392
|
+
}
|
|
393
|
+
return { stand, candles, flames, foot, cups }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** A dreidel, `s` tall, tilted by `turn`: its body, its handle and the panel the letter would sit on. */
|
|
397
|
+
export function dreidel(cx: number, cy: number, s: number, turn = 0): { body: VectorPath[]; side: VectorPath[]; handle: VectorPath[] } {
|
|
398
|
+
const c = Math.cos(turn), sn = Math.sin(turn)
|
|
399
|
+
const p = (x: number, y: number) => ({ x: cx + (x * c - y * sn) * s, y: cy + (x * sn + y * c) * s })
|
|
400
|
+
return {
|
|
401
|
+
handle: [polyP([p(-0.06, -0.62), p(0.06, -0.62), p(0.06, -0.3), p(-0.06, -0.3)])],
|
|
402
|
+
body: [polyP([p(-0.3, -0.3), p(0.22, -0.3), p(0.22, 0.18), p(0, 0.5), p(-0.3, 0.18)])],
|
|
403
|
+
side: [polyP([p(0.22, -0.3), p(0.36, -0.36), p(0.36, 0.1), p(0, 0.5), p(0.22, 0.18)])],
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** A six-pointed star of two interlaced triangles, as outlines. */
|
|
408
|
+
export function starOfDavid(cx: number, cy: number, r: number): VectorPath[] {
|
|
409
|
+
const tri = (off: number) => polyP([0, 1, 2].map(i => at(cx, cy, r, -Math.PI / 2 + off + (i * 2 * Math.PI) / 3)))
|
|
410
|
+
return [tri(0), tri(Math.PI / 3)]
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** A gold coin of gelt: a disc and an inner rim. */
|
|
414
|
+
export const gelt = (cx: number, cy: number, r: number) => ({ coin: [disc(cx, cy, r)], rim: [disc(cx, cy, r * 0.78)] })
|
|
415
|
+
|
|
416
|
+
// ── New Year's Eve ──
|
|
417
|
+
|
|
418
|
+
/** A firework: `n` rays from `r0` to `r1`, each ending in a spark, and a ring of dots. */
|
|
419
|
+
export function burst(cx: number, cy: number, r1: number, n = 18, seed = 1, r0 = r1 * 0.25): { rays: VectorPath[]; sparks: VectorPath[] } {
|
|
420
|
+
const r = rng(seed), rays: VectorPath[] = [], sparks: VectorPath[] = []
|
|
421
|
+
for (let i = 0; i < n; i++) {
|
|
422
|
+
const a = (i * 2 * Math.PI) / n + (r() - 0.5) * 0.12
|
|
423
|
+
const len = r1 * (0.82 + r() * 0.18)
|
|
424
|
+
const p0 = at(cx, cy, r0, a), p1 = at(cx, cy, len, a)
|
|
425
|
+
// A slight droop, as a spark falls.
|
|
426
|
+
rays.push({ closed: false, points: [pt3(p0.x, p0.y), { ...pt3(p1.x, p1.y + len * 0.04), handleIn: pt3((p0.x + p1.x) / 2, (p0.y + p1.y) / 2 - len * 0.03) }] })
|
|
427
|
+
const e = at(cx, cy, len * 1.08, a)
|
|
428
|
+
sparks.push(disc(e.x, e.y + len * 0.05, r1 * 0.022))
|
|
429
|
+
if (i % 2 === 0) { const m = at(cx, cy, len * 0.62, a + Math.PI / n); sparks.push(disc(m.x, m.y, r1 * 0.014)) }
|
|
430
|
+
}
|
|
431
|
+
return { rays, sparks }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** A champagne coupe, `s` tall, its foot on (cx, base): the bowl, the stem and foot, and the wine in it. */
|
|
435
|
+
export function coupe(cx: number, base: number, s: number, tilt = 0): { glass: VectorPath[]; wine: VectorPath[] } {
|
|
436
|
+
const c = Math.cos(tilt), sn = Math.sin(tilt)
|
|
437
|
+
const p = (x: number, y: number) => pt3(cx + (x * c - y * sn) * s, base + (x * sn + y * c) * s)
|
|
438
|
+
const bowl: VectorPath = { closed: true, points: [
|
|
439
|
+
{ ...p(-0.42, -0.92) }, { ...p(0.42, -0.92), handleOut: p(0.4, -0.66) },
|
|
440
|
+
{ ...p(0.04, -0.6), handleIn: p(0.22, -0.6) }, { ...p(-0.04, -0.6), handleOut: p(-0.22, -0.6) },
|
|
441
|
+
{ ...p(-0.42, -0.92), handleIn: p(-0.4, -0.66) },
|
|
442
|
+
] }
|
|
443
|
+
return {
|
|
444
|
+
glass: [bowl, polyP([p(-0.025, -0.6), p(0.025, -0.6), p(0.025, -0.06), p(-0.025, -0.06)].map(q => ({ x: q.x, y: q.y }))), disc(p(0, -0.03).x, p(0, -0.03).y, 0.26 * s, 0.045 * s)],
|
|
445
|
+
wine: [{ closed: true, points: [{ ...p(-0.36, -0.86) }, { ...p(0.36, -0.86), handleOut: p(0.34, -0.66) }, { ...p(0, -0.63), handleIn: p(0.2, -0.63), handleOut: p(-0.2, -0.63) }, { ...p(-0.36, -0.86), handleIn: p(-0.34, -0.66) }] }],
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** A curling streamer from `a` across `len`, waving `amp` up and down `waves` times. */
|
|
450
|
+
export function streamer(a: P, len: number, angle: number, amp: number, waves: number): VectorPath {
|
|
451
|
+
const c = Math.cos(angle), s = Math.sin(angle)
|
|
452
|
+
const n = Math.round(waves * 10)
|
|
453
|
+
return polyP(Array.from({ length: n + 1 }, (_, i) => { const u = (len * i) / n, v = Math.sin((i / n) * waves * 2 * Math.PI) * amp; return { x: a.x + u * c - v * s, y: a.y + u * s + v * c } }), false)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** A clock face: the ring, twelve ticks and two hands at five to midnight. */
|
|
457
|
+
export function clock(cx: number, cy: number, r: number): { ring: VectorPath[]; ticks: VectorPath[]; hands: VectorPath[] } {
|
|
458
|
+
const ticks = Array.from({ length: 12 }, (_, i) => { const a = (i * Math.PI) / 6; return seg(at(cx, cy, r * (i % 3 ? 0.84 : 0.76), a), at(cx, cy, r * 0.92, a)) })
|
|
459
|
+
return { ring: [disc(cx, cy, r)], ticks, hands: [seg({ x: cx, y: cy }, at(cx, cy, r * 0.5, -Math.PI / 2 - 0.08)), seg({ x: cx, y: cy }, at(cx, cy, r * 0.74, -Math.PI / 2 - (Math.PI / 6) * 0.5))] }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Valentine's Day ──
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* A rose from above, `r` across: an outer ring of broad overlapping petals, a middle ring, a tight heart and a
|
|
466
|
+
* swirl of strokes into the centre. `inner` is the middle ring and the heart together, for a two-tone rose.
|
|
467
|
+
*/
|
|
468
|
+
export function rose(cx: number, cy: number, r: number, seed = 1): { outer: VectorPath[]; inner: VectorPath[]; mid: VectorPath[]; core: VectorPath[]; swirl: VectorPath[] } {
|
|
469
|
+
const rr = rng(seed)
|
|
470
|
+
const ring = (n: number, R: number, off: number) => Array.from({ length: n }, (_, i) => {
|
|
471
|
+
const a = off + (i * 2 * Math.PI) / n + (rr() - 0.5) * 0.25
|
|
472
|
+
const p = at(cx, cy, R * 0.5, a)
|
|
473
|
+
return disc(p.x, p.y, R * 0.56, R * 0.5)
|
|
474
|
+
})
|
|
475
|
+
const outer = ring(7, r, rr() * 6)
|
|
476
|
+
const mid = ring(5, r * 0.66, rr() * 6)
|
|
477
|
+
const core = [disc(cx, cy, r * 0.34)]
|
|
478
|
+
const swirl = Array.from({ length: 3 }, (_, i) => arc(cx, cy, r * (0.12 + i * 0.09), i * 2.1, i * 2.1 + 3.6, 12))
|
|
479
|
+
return { outer, inner: [...mid, ...core], mid, core, swirl }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ── Easter and spring ──
|
|
483
|
+
|
|
484
|
+
/** An egg, `w` wide and `h` tall, its middle at (cx, cy), tilted by `turn`. */
|
|
485
|
+
export function egg(cx: number, cy: number, w: number, h: number, turn = 0): VectorPath {
|
|
486
|
+
const c = Math.cos(turn), s = Math.sin(turn)
|
|
487
|
+
const p = (x: number, y: number) => pt3(cx + (x * c - y * s), cy + (x * s + y * c))
|
|
488
|
+
const a = w / 2, top = -h * 0.55, bot = h * 0.45
|
|
489
|
+
return { closed: true, points: [
|
|
490
|
+
{ ...p(0, top), handleIn: p(-a * 0.58, top), handleOut: p(a * 0.58, top) },
|
|
491
|
+
{ ...p(a, h * 0.08), handleIn: p(a, top * 0.55), handleOut: p(a, bot * 0.62) },
|
|
492
|
+
{ ...p(0, bot), handleIn: p(a * 0.56, bot), handleOut: p(-a * 0.56, bot) },
|
|
493
|
+
{ ...p(-a, h * 0.08), handleIn: p(-a, bot * 0.62), handleOut: p(-a, top * 0.55) },
|
|
494
|
+
] }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The bands painted on an egg: a zigzag and two lines across it, and dots, clipped to the egg's width by eye. */
|
|
498
|
+
export function eggBands(cx: number, cy: number, w: number, h: number, turn = 0, kind: 'zigzag' | 'dots' | 'waves' = 'zigzag'): { lines: VectorPath[]; dots: VectorPath[] } {
|
|
499
|
+
const c = Math.cos(turn), s = Math.sin(turn)
|
|
500
|
+
const p = (x: number, y: number) => ({ x: cx + (x * c - y * s), y: cy + (x * s + y * c) })
|
|
501
|
+
// Half the egg's width at height y (an ellipse-ish profile, wider below the middle).
|
|
502
|
+
const half = (y: number) => { const t = (y + h * 0.05) / (h * 0.52); return (w / 2) * Math.sqrt(Math.max(0, 1 - t * t)) * 0.96 }
|
|
503
|
+
const lines: VectorPath[] = [], dots: VectorPath[] = []
|
|
504
|
+
const band = (y: number) => { const hw = half(y); lines.push(polyP(Array.from({ length: 9 }, (_, i) => p(-hw + (2 * hw * i) / 8, y + Math.sin((i / 8) * Math.PI) * h * 0.035)), false)) }
|
|
505
|
+
band(-h * 0.12); band(h * 0.14)
|
|
506
|
+
if (kind === 'zigzag') { const hw = half(0); lines.push(polyP(Array.from({ length: 11 }, (_, i) => p(-hw * 0.92 + (1.84 * hw * i) / 10, (i % 2 ? -1 : 1) * h * 0.06 + h * 0.01)), false)) }
|
|
507
|
+
if (kind === 'waves') { const hw = half(0); lines.push(polyP(Array.from({ length: 25 }, (_, i) => p(-hw * 0.94 + (1.88 * hw * i) / 24, Math.sin(i * 0.8) * h * 0.045 + h * 0.01)), false)) }
|
|
508
|
+
if (kind === 'dots') { const hw = half(0); for (let i = 0; i < 5; i++) { const q = p(-hw * 0.7 + (1.4 * hw * i) / 4, h * 0.01); dots.push(disc(q.x, q.y, w * 0.055)) } }
|
|
509
|
+
for (const y of [-h * 0.33, h * 0.32]) { const hw = half(y) * 0.6; for (let i = 0; i < 3; i++) { const q = p(-hw + hw * i, y); dots.push(disc(q.x, q.y, w * 0.035)) } }
|
|
510
|
+
return { lines, dots }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** A tulip on a stem from (x, ground), `h` tall: the cup, the stem and two leaves. */
|
|
514
|
+
export function tulip(x: number, ground: number, h: number, lean = 0): { cup: VectorPath[]; stem: VectorPath[]; leaves: VectorPath[] } {
|
|
515
|
+
const topX = x + lean * h, topY = ground - h
|
|
516
|
+
const w = h * 0.2
|
|
517
|
+
const cup: VectorPath = { closed: true, points: [
|
|
518
|
+
{ ...pt3(topX - w / 2, topY - w * 0.9) }, { ...pt3(topX - w * 0.18, topY - w * 0.45) }, { ...pt3(topX, topY - w * 1.0) },
|
|
519
|
+
{ ...pt3(topX + w * 0.18, topY - w * 0.45) }, { ...pt3(topX + w / 2, topY - w * 0.9), handleOut: pt3(topX + w * 0.62, topY - w * 0.1) },
|
|
520
|
+
{ ...pt3(topX, topY + w * 0.12), handleIn: pt3(topX + w * 0.42, topY + w * 0.12), handleOut: pt3(topX - w * 0.42, topY + w * 0.12) },
|
|
521
|
+
{ ...pt3(topX - w / 2, topY - w * 0.9), handleIn: pt3(topX - w * 0.62, topY - w * 0.1) },
|
|
522
|
+
] }
|
|
523
|
+
return {
|
|
524
|
+
cup: [cup],
|
|
525
|
+
stem: [{ closed: false, points: [pt3(x, ground), { ...pt3(topX, topY), handleIn: pt3(x + lean * h * 0.2, ground - h * 0.6) }] }],
|
|
526
|
+
leaves: [leaf({ x, y: ground }, h * 0.55, h * 0.14, -Math.PI / 2 - 0.35), leaf({ x, y: ground }, h * 0.42, h * 0.12, -Math.PI / 2 + 0.42)],
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** A tuft of grass blades along the ground from x0 to x1. */
|
|
531
|
+
export function grass(x0: number, x1: number, ground: number, h: number, seed: number, n = 30): VectorPath[] {
|
|
532
|
+
const r = rng(seed)
|
|
533
|
+
return Array.from({ length: n }, (_, i) => {
|
|
534
|
+
const x = x0 + ((x1 - x0) * (i + r() * 0.8)) / n, hh = h * (0.5 + r() * 0.6), lean = (r() - 0.5) * hh * 0.5, w = h * 0.06
|
|
535
|
+
return { closed: true, points: [pt3(x - w, ground), { ...pt3(x + lean, ground - hh), handleIn: pt3(x - w * 0.5, ground - hh * 0.5) }, { ...pt3(x + w, ground), handleIn: pt3(x + w * 0.4, ground - hh * 0.5) }] }
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** A sitting rabbit seen from behind and the side, as one silhouette made of discs and ears, `s` tall. */
|
|
540
|
+
export function rabbit(cx: number, ground: number, s: number, facing = 1): { body: VectorPath[]; tail: VectorPath[] } {
|
|
541
|
+
const f = facing
|
|
542
|
+
return {
|
|
543
|
+
body: [
|
|
544
|
+
disc(cx, ground - s * 0.27, s * 0.3, s * 0.27),
|
|
545
|
+
disc(cx + f * s * 0.2, ground - s * 0.56, s * 0.15, s * 0.14),
|
|
546
|
+
leaf({ x: cx + f * s * 0.17, y: ground - s * 0.64 }, s * 0.38, s * 0.12, -Math.PI / 2 - f * 0.25),
|
|
547
|
+
leaf({ x: cx + f * s * 0.25, y: ground - s * 0.64 }, s * 0.36, s * 0.11, -Math.PI / 2 + f * 0.05),
|
|
548
|
+
disc(cx + f * s * 0.2, ground - s * 0.04, s * 0.12, s * 0.05),
|
|
549
|
+
],
|
|
550
|
+
tail: [disc(cx - f * s * 0.29, ground - s * 0.16, s * 0.07)],
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ── Halloween ──
|
|
555
|
+
|
|
556
|
+
/** A pumpkin, `w` wide and `h` tall, sitting on (cx, ground): its lobes, the creases between, its stem and a curl. */
|
|
557
|
+
export function pumpkin(cx: number, ground: number, w: number, h: number): { lobes: VectorPath[]; creases: VectorPath[]; stem: VectorPath[] } {
|
|
558
|
+
const cy = ground - h / 2
|
|
559
|
+
const lobes = [disc(cx - w * 0.27, cy, w * 0.25, h / 2), disc(cx + w * 0.27, cy, w * 0.25, h / 2), disc(cx - w * 0.12, cy, w * 0.24, h / 2 * 1.01), disc(cx + w * 0.12, cy, w * 0.24, h / 2 * 1.01), disc(cx, cy, w * 0.2, h / 2 * 1.02)]
|
|
560
|
+
const creases = [-0.2, -0.07, 0.07, 0.2].map(f => ({ closed: false, points: [pt3(cx + w * f * 0.7, cy - h * 0.42), { ...pt3(cx + w * f * 0.7, cy + h * 0.42), handleIn: pt3(cx + w * f * 1.35, cy) }] }))
|
|
561
|
+
const stem: VectorPath = { closed: true, points: [pt3(cx - w * 0.05, cy - h * 0.44), { ...pt3(cx - w * 0.03, cy - h * 0.66), handleIn: pt3(cx - w * 0.06, cy - h * 0.56) }, pt3(cx + w * 0.07, cy - h * 0.7), { ...pt3(cx + w * 0.05, cy - h * 0.44), handleIn: pt3(cx + w * 0.02, cy - h * 0.58) }] }
|
|
562
|
+
return { lobes, creases, stem: [stem] }
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** A jack-o'-lantern's face cut into a pumpkin `w` wide at (cx, cy): two triangle eyes, a nose and a toothy grin. */
|
|
566
|
+
export function jackFace(cx: number, cy: number, w: number): VectorPath[] {
|
|
567
|
+
const u = w / 100
|
|
568
|
+
const tri = (x: number, y: number, s: number, up = true) => polyP(up ? [{ x: cx + (x - s) * u, y: cy + (y + s * 0.7) * u }, { x: cx + (x + s) * u, y: cy + (y + s * 0.7) * u }, { x: cx + x * u, y: cy + (y - s) * u }] : [{ x: cx + (x - s) * u, y: cy + (y - s * 0.6) * u }, { x: cx + (x + s) * u, y: cy + (y - s * 0.6) * u }, { x: cx + x * u, y: cy + (y + s * 0.6) * u }])
|
|
569
|
+
const mouth = polyP([[-32, 8], [-20, 14], [-14, 9], [-6, 16], [6, 16], [14, 9], [20, 14], [32, 8], [24, 24], [10, 28], [6, 22], [-6, 22], [-10, 28], [-24, 24]].map(([x, y]) => ({ x: cx + x * u, y: cy + y * u })))
|
|
570
|
+
return [tri(-17, -12, 10), tri(17, -12, 10), tri(0, 0, 5, true), mouth]
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** A bat with scalloped wings, `s` across. */
|
|
574
|
+
export function bat(cx: number, cy: number, s: number, flap = 0): VectorPath {
|
|
575
|
+
const u = s / 100, f = flap * 10
|
|
576
|
+
const pts: [number, number][] = [[0, -8], [4, -14], [6, -6], [14, -10 - f], [30, -18 - f], [50, -12 - f], [44, -2], [36, 2], [30, 8], [20, 4], [12, 12], [4, 6], [0, 12]]
|
|
577
|
+
const full = [...pts, ...pts.slice(1, -1).reverse().map(([x, y]) => [-x, y] as [number, number])]
|
|
578
|
+
return polyP(full.map(([x, y]) => ({ x: cx + x * u, y: cy + y * u })))
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** A spider's web in a corner: spokes from (cx, cy) between angles a0 and a1, and sagging threads across them. */
|
|
582
|
+
export function web(cx: number, cy: number, r: number, a0: number, a1: number, spokes = 7, rings = 6): VectorPath[] {
|
|
583
|
+
const out: VectorPath[] = []
|
|
584
|
+
const angles = Array.from({ length: spokes }, (_, i) => a0 + ((a1 - a0) * i) / (spokes - 1))
|
|
585
|
+
for (const a of angles) out.push(seg({ x: cx, y: cy }, at(cx, cy, r, a)))
|
|
586
|
+
for (let j = 1; j <= rings; j++) {
|
|
587
|
+
const rr = (r * j) / (rings + 0.4)
|
|
588
|
+
for (let i = 0; i < spokes - 1; i++) {
|
|
589
|
+
const p0 = at(cx, cy, rr, angles[i]), p1 = at(cx, cy, rr, angles[i + 1]), m = at(cx, cy, rr * 0.86, (angles[i] + angles[i + 1]) / 2)
|
|
590
|
+
out.push({ closed: false, points: [pt3(p0.x, p0.y), { ...pt3(p1.x, p1.y), handleIn: pt3(m.x, m.y) }] })
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return out
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** A ghost, `s` tall, with a wavy hem: the sheet and its two eyes and mouth. */
|
|
597
|
+
export function ghost(cx: number, top: number, s: number): { sheet: VectorPath[]; face: VectorPath[] } {
|
|
598
|
+
const w = s * 0.62, bot = top + s
|
|
599
|
+
const pts: VectorPoint[] = [{ ...pt3(cx - w / 2, bot - s * 0.05), handleOut: pt3(cx - w / 2, top + s * 0.2) }, { ...pt3(cx, top), handleIn: pt3(cx - w / 2, top), handleOut: pt3(cx + w / 2, top) }, { ...pt3(cx + w / 2, bot - s * 0.05), handleIn: pt3(cx + w / 2, top + s * 0.2) }]
|
|
600
|
+
for (let i = 1; i <= 4; i++) { const x = cx + w / 2 - (w * i) / 4; pts.push({ ...pt3(x, bot - (i % 2 ? 0 : s * 0.08)), handleIn: pt3(x + w / 8, bot + (i % 2 ? s * 0.04 : -s * 0.12)) }) }
|
|
601
|
+
return { sheet: [{ closed: true, points: pts }], face: [disc(cx - w * 0.16, top + s * 0.32, s * 0.05, s * 0.07), disc(cx + w * 0.16, top + s * 0.32, s * 0.05, s * 0.07), disc(cx, top + s * 0.5, s * 0.06, s * 0.08)] }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** A piece of candy corn, `s` tall: its three bands. */
|
|
605
|
+
export function candyCorn(cx: number, cy: number, s: number, turn = 0): { base: VectorPath[]; mid: VectorPath[]; tip: VectorPath[] } {
|
|
606
|
+
const c = Math.cos(turn), sn = Math.sin(turn)
|
|
607
|
+
const p = (x: number, y: number) => ({ x: cx + (x * c - y * sn) * s, y: cy + (x * sn + y * c) * s })
|
|
608
|
+
const band = (y0: number, y1: number) => { const w0 = 0.36 * (y0 + 0.5), w1 = 0.36 * (y1 + 0.5); return polyP([p(-w0, y0), p(w0, y0), p(w1, y1), p(-w1, y1)]) }
|
|
609
|
+
return { tip: [polyP([p(0, -0.5), p(0.12, -0.17), p(-0.12, -0.17)])], mid: [band(-0.17, 0.15)], base: [{ ...band(0.15, 0.48) }] }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── Thanksgiving and autumn ──
|
|
613
|
+
|
|
614
|
+
/** A maple leaf, `s` across, its stem down, turned by `turn`. */
|
|
615
|
+
export function maple(cx: number, cy: number, s: number, turn = 0): VectorPath[] {
|
|
616
|
+
const u = s / 100, c = Math.cos(turn), sn = Math.sin(turn)
|
|
617
|
+
const raw: [number, number][] = [[0, -50], [8, -30], [18, -36], [16, -14], [38, -26], [34, -14], [48, -10], [30, 4], [36, 14], [14, 12], [10, 22], [4, 18], [2, 40], [-2, 40], [-4, 18], [-10, 22], [-14, 12], [-36, 14], [-30, 4], [-48, -10], [-34, -14], [-38, -26], [-16, -14], [-18, -36], [-8, -30]]
|
|
618
|
+
const p = ([x, y]: [number, number]) => ({ x: cx + (x * c - y * sn) * u, y: cy + (x * sn + y * c) * u })
|
|
619
|
+
return [polyP(raw.map(p))]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** A maple leaf's veins, to lay over it in a darker tone. */
|
|
623
|
+
export function mapleVeins(cx: number, cy: number, s: number, turn = 0): VectorPath[] {
|
|
624
|
+
const u = s / 100, c = Math.cos(turn), sn = Math.sin(turn)
|
|
625
|
+
const p = (x: number, y: number) => ({ x: cx + (x * c - y * sn) * u, y: cy + (x * sn + y * c) * u })
|
|
626
|
+
return [seg(p(0, 36), p(0, -42)), seg(p(0, 14), p(36, -20)), seg(p(0, 14), p(-36, -20)), seg(p(0, 20), p(28, 10)), seg(p(0, 20), p(-28, 10))]
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** An oak leaf with rounded lobes, `len` long, pointing at `angle`. */
|
|
630
|
+
export function oak(base: P, len: number, angle: number): VectorPath {
|
|
631
|
+
const c = Math.cos(angle), s = Math.sin(angle)
|
|
632
|
+
const p = (u: number, v: number) => ({ x: base.x + u * c - v * s, y: base.y + u * s + v * c })
|
|
633
|
+
const pts: P[] = [p(0, 0)]
|
|
634
|
+
const lobes = 4
|
|
635
|
+
for (let i = 0; i < lobes; i++) { const u0 = len * (0.12 + (i * 0.8) / lobes), u1 = u0 + (len * 0.8) / lobes / 2, w = len * 0.26 * (1 - i * 0.12); pts.push(p(u0, w * 0.45), p(u1, w), p(u0 + (len * 0.8) / lobes, w * 0.42)) }
|
|
636
|
+
pts.push(p(len, 0))
|
|
637
|
+
for (let i = lobes - 1; i >= 0; i--) { const u0 = len * (0.12 + (i * 0.8) / lobes), u1 = u0 + (len * 0.8) / lobes / 2, w = len * 0.26 * (1 - i * 0.12); pts.push(p(u0 + (len * 0.8) / lobes, -w * 0.42), p(u1, -w), p(u0, -w * 0.45)) }
|
|
638
|
+
return smoothP(pts, 0.18)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** A closed curve through `pts`. */
|
|
642
|
+
export function smoothP(pts: P[], tension = 0.2): VectorPath {
|
|
643
|
+
const n = pts.length
|
|
644
|
+
return { closed: true, points: pts.map((p, i) => { const a = pts[(i - 1 + n) % n], b = pts[(i + 1) % n], tx = (b.x - a.x) * tension, ty = (b.y - a.y) * tension; return { ...pt3(p.x, p.y), handleIn: pt3(p.x - tx, p.y - ty), handleOut: pt3(p.x + tx, p.y + ty) } }) }
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** A stalk of wheat from (x, ground) up to its ear, `h` tall: the stalk and the grains. */
|
|
648
|
+
export function wheat(x: number, ground: number, h: number, lean = 0): { stalk: VectorPath[]; grains: VectorPath[] } {
|
|
649
|
+
const top = { x: x + lean * h, y: ground - h }
|
|
650
|
+
const grains: VectorPath[] = []
|
|
651
|
+
const angle = Math.atan2(top.y - ground, top.x - x)
|
|
652
|
+
for (let i = 0; i < 7; i++) {
|
|
653
|
+
const t = 0.66 + i * 0.05
|
|
654
|
+
const p = { x: x + (top.x - x) * t, y: ground + (top.y - ground) * t }
|
|
655
|
+
const len = h * 0.1 * (1 - i * 0.06)
|
|
656
|
+
grains.push(petal(p, len, len * 0.5, angle - 0.5), petal(p, len, len * 0.5, angle + 0.5))
|
|
657
|
+
}
|
|
658
|
+
grains.push(petal(top, h * 0.08, h * 0.04, angle))
|
|
659
|
+
return { stalk: [{ closed: false, points: [pt3(x, ground), { ...pt3(top.x, top.y), handleIn: pt3(x + lean * h * 0.3, ground - h * 0.5) }] }], grains }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** An acorn, `s` tall: its nut, its cap and the stalk. */
|
|
663
|
+
export function acorn(cx: number, cy: number, s: number, turn = 0): { nut: VectorPath[]; cap: VectorPath[] } {
|
|
664
|
+
const c = Math.cos(turn), sn = Math.sin(turn)
|
|
665
|
+
const tr = (x: number, y: number) => ({ x: cx + (x * c - y * sn) * s, y: cy + (x * sn + y * c) * s })
|
|
666
|
+
const nut = egg(cx, cy + s * 0.12, s * 0.5, s * 0.66, turn + Math.PI)
|
|
667
|
+
const cap = smoothP([tr(-0.3, -0.1), tr(-0.28, -0.26), tr(0, -0.34), tr(0.28, -0.26), tr(0.3, -0.1), tr(0, -0.06)], 0.2)
|
|
668
|
+
return { nut: [nut], cap: [cap, polyP([tr(-0.03, -0.32), tr(0.03, -0.32), tr(0.06, -0.46), tr(0.01, -0.46)])] }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ── Diwali ──
|
|
672
|
+
|
|
673
|
+
/** A clay diya, `w` wide, sitting on (cx, ground): its bowl, the painted rim, and the flame with its glow. */
|
|
674
|
+
export function diya(cx: number, ground: number, w: number): { bowl: VectorPath[]; rim: VectorPath[]; flame: VectorPath[]; core: VectorPath[] } {
|
|
675
|
+
const h = w * 0.36, top = ground - h
|
|
676
|
+
const bowl: VectorPath = { closed: true, points: [
|
|
677
|
+
{ ...pt3(cx - w / 2, top) }, { ...pt3(cx + w * 0.38, top) }, { ...pt3(cx + w * 0.58, top - h * 0.28) },
|
|
678
|
+
{ ...pt3(cx + w * 0.42, top + h * 0.3), handleOut: pt3(cx + w * 0.3, ground + h * 0.05) },
|
|
679
|
+
{ ...pt3(cx - w * 0.1, ground), handleIn: pt3(cx + w * 0.15, ground + h * 0.02), handleOut: pt3(cx - w * 0.38, ground) },
|
|
680
|
+
{ ...pt3(cx - w / 2, top), handleIn: pt3(cx - w * 0.52, top + h * 0.6) },
|
|
681
|
+
] }
|
|
682
|
+
const rim = [polyP([{ x: cx - w * 0.47, y: top + h * 0.12 }, { x: cx + w * 0.4, y: top + h * 0.12 }, { x: cx + w * 0.37, y: top + h * 0.26 }, { x: cx - w * 0.45, y: top + h * 0.26 }])]
|
|
683
|
+
const fx = cx + w * 0.5, fy = top - h * 0.22
|
|
684
|
+
return { bowl: [bowl], rim, flame: [flame(fx, fy, w * 0.2, w * 0.5)], core: [flame(fx, fy - w * 0.02, w * 0.09, w * 0.24)] }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** A marigold bloom: a ruff of petals in two rings and a dark heart. */
|
|
688
|
+
export function marigold(cx: number, cy: number, r: number, seed = 1): { outer: VectorPath[]; inner: VectorPath[] } {
|
|
689
|
+
const rr = rng(seed)
|
|
690
|
+
const ring = (R: number, n: number, off: number) => Array.from({ length: n }, (_, i) => { const a = off + (i * 2 * Math.PI) / n; const p = at(cx, cy, R * (0.92 + rr() * 0.12), a); return disc(p.x, p.y, r * 0.3) })
|
|
691
|
+
return { outer: ring(r * 0.7, 12, 0), inner: [...ring(r * 0.36, 7, 0.3), disc(cx, cy, r * 0.36)] }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* A rangoli: rings of petals, lozenges and dots about (cx, cy), `R` across. Returns its layers from the back, each
|
|
696
|
+
* for its own colour.
|
|
697
|
+
*/
|
|
698
|
+
export function rangoli(cx: number, cy: number, R: number, petals = 8): { outer: VectorPath[]; middle: VectorPath[]; inner: VectorPath[]; dots: VectorPath[]; heart: VectorPath[] } {
|
|
699
|
+
const outer: VectorPath[] = [], middle: VectorPath[] = [], inner: VectorPath[] = [], dots: VectorPath[] = []
|
|
700
|
+
for (let i = 0; i < petals * 2; i++) { const a = (i * Math.PI) / petals; outer.push(petal(at(cx, cy, R * 0.42, a), R * 0.56, R * 0.3, a)) }
|
|
701
|
+
for (let i = 0; i < petals; i++) { const a = (i * 2 * Math.PI) / petals + Math.PI / petals; middle.push(petal(at(cx, cy, R * 0.18, a), R * 0.5, R * 0.36, a)) }
|
|
702
|
+
for (let i = 0; i < petals; i++) { const a = (i * 2 * Math.PI) / petals; inner.push(petal({ x: cx, y: cy }, R * 0.36, R * 0.2, a)) }
|
|
703
|
+
for (let i = 0; i < petals * 4; i++) { const a = (i * Math.PI) / (petals * 2); const p = at(cx, cy, R * 1.04, a); dots.push(disc(p.x, p.y, R * 0.022)) }
|
|
704
|
+
for (let i = 0; i < petals * 2; i++) { const a = (i * Math.PI) / petals + Math.PI / (petals * 2); const p = at(cx, cy, R * 0.78, a); dots.push(disc(p.x, p.y, R * 0.03)) }
|
|
705
|
+
return { outer, middle, inner, dots, heart: [disc(cx, cy, R * 0.12)] }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* A toran across the top of a `w` wide frame: a string that dips between hooks, marigolds hung from it and pairs of
|
|
710
|
+
* mango leaves between them.
|
|
711
|
+
*/
|
|
712
|
+
export function toran(x0: number, x1: number, y: number, sag: number, n: number, r: number, seed = 3): { string: VectorPath[]; blooms: { outer: VectorPath[]; inner: VectorPath[] }; leaves: VectorPath[] } {
|
|
713
|
+
const string: VectorPath[] = [], outer: VectorPath[] = [], inner: VectorPath[] = [], leaves: VectorPath[] = []
|
|
714
|
+
const span = (x1 - x0) / n
|
|
715
|
+
for (let i = 0; i < n; i++) {
|
|
716
|
+
const a = { x: x0 + span * i, y }, b = { x: x0 + span * (i + 1), y }, m = { x: (a.x + b.x) / 2, y: y + sag * 2 }
|
|
717
|
+
string.push(quad(a, m, b, 12))
|
|
718
|
+
const lowest = along(a, m, b, 0.5).p
|
|
719
|
+
const g = marigold(lowest.x, lowest.y + r * 0.6, r, seed + i)
|
|
720
|
+
outer.push(...g.outer); inner.push(...g.inner)
|
|
721
|
+
for (const t of [0.25, 0.75]) { const p = along(a, m, b, t).p; leaves.push(leaf(p, r * 2.3, r * 0.75, Math.PI / 2 + (t < 0.5 ? 0.15 : -0.15))) }
|
|
722
|
+
}
|
|
723
|
+
return { string, blooms: { outer, inner }, leaves }
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ── Eid ──
|
|
727
|
+
|
|
728
|
+
/** An eight-pointed star of two squares (the khatam), radius `r`. */
|
|
729
|
+
export function eightStar(cx: number, cy: number, r: number): VectorPath {
|
|
730
|
+
return starPath(cx, cy, r, r * 0.76, 8, Math.PI / 8)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* A hanging lantern (fanous), `s` tall from its ring at (cx, top): the ring and chain, the domed cap, the glass body
|
|
735
|
+
* with its frame, the glow inside and the foot.
|
|
736
|
+
*/
|
|
737
|
+
export function fanous(cx: number, top: number, s: number): { metal: VectorPath[]; glass: VectorPath[]; frame: VectorPath[]; glow: VectorPath[] } {
|
|
738
|
+
const u = s / 100
|
|
739
|
+
const y = (v: number) => top + v * u, x = (v: number) => cx + v * u
|
|
740
|
+
const metal = [
|
|
741
|
+
disc(cx, y(3), 3 * u),
|
|
742
|
+
polyP([{ x: x(-3), y: y(6) }, { x: x(3), y: y(6) }, { x: x(2), y: y(14) }, { x: x(-2), y: y(14) }]),
|
|
743
|
+
{ closed: true, points: [pt3(x(-16), y(26)), { ...pt3(x(0), y(13)), handleIn: pt3(x(-14), y(15)), handleOut: pt3(x(14), y(15)) }, pt3(x(16), y(26))] } as VectorPath,
|
|
744
|
+
polyP([{ x: x(-20), y: y(26) }, { x: x(20), y: y(26) }, { x: x(18), y: y(30) }, { x: x(-18), y: y(30) }]),
|
|
745
|
+
polyP([{ x: x(-18), y: y(74) }, { x: x(18), y: y(74) }, { x: x(10), y: y(84) }, { x: x(-10), y: y(84) }]),
|
|
746
|
+
polyP([{ x: x(-4), y: y(84) }, { x: x(4), y: y(84) }, { x: x(0), y: y(97) }]),
|
|
747
|
+
]
|
|
748
|
+
const glass = [polyP([{ x: x(-17), y: y(30) }, { x: x(17), y: y(30) }, { x: x(22), y: y(52) }, { x: x(17), y: y(74) }, { x: x(-17), y: y(74) }, { x: x(-22), y: y(52) }])]
|
|
749
|
+
const frame = [seg({ x: x(-6), y: y(30) }, { x: x(-6), y: y(74) }), seg({ x: x(6), y: y(30) }, { x: x(6), y: y(74) }), seg({ x: x(-22), y: y(52) }, { x: x(22), y: y(52) }),
|
|
750
|
+
{ closed: false, points: [pt3(x(-6), y(44)), { ...pt3(x(6), y(44)), handleIn: pt3(x(0), y(36)) }] }, { closed: false, points: [pt3(x(-6), y(66)), { ...pt3(x(6), y(66)), handleIn: pt3(x(0), y(58)) }] }]
|
|
751
|
+
return { metal, glass, frame, glow: [flame(cx, y(70), 12 * u, 26 * u)] }
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** A pointed (ogee-topped) arch, `w` wide, standing on y = `base` with its springing at `spring` and its tip at `tip`. */
|
|
755
|
+
export function archPath(cx: number, w: number, base: number, spring: number, tip: number): VectorPath {
|
|
756
|
+
const l = cx - w / 2, r = cx + w / 2
|
|
757
|
+
return { closed: true, points: [
|
|
758
|
+
pt3(l, base), { ...pt3(l, spring), handleOut: pt3(l, spring - (spring - tip) * 0.55) },
|
|
759
|
+
{ ...pt3(cx, tip), handleIn: pt3(cx - w * 0.12, tip + (spring - tip) * 0.35), handleOut: pt3(cx + w * 0.12, tip + (spring - tip) * 0.35) },
|
|
760
|
+
{ ...pt3(r, spring), handleIn: pt3(r, spring - (spring - tip) * 0.55) }, pt3(r, base),
|
|
761
|
+
] }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** A field of eight-pointed stars on a grid, with small lozenges between them (a tile pattern), over a `w` × `h` box. */
|
|
765
|
+
export function tilePattern(w: number, h: number, step: number, x0 = 0, y0 = 0): { stars: VectorPath[]; joins: VectorPath[] } {
|
|
766
|
+
const stars: VectorPath[] = [], joins: VectorPath[] = []
|
|
767
|
+
for (let y = y0; y < h + step; y += step) for (let x = x0; x < w + step; x += step) {
|
|
768
|
+
stars.push(eightStar(x, y, step * 0.3))
|
|
769
|
+
const m = { x: x + step / 2, y: y + step / 2 }
|
|
770
|
+
joins.push(polyP([{ x: m.x, y: m.y - step * 0.12 }, { x: m.x + step * 0.12, y: m.y }, { x: m.x, y: m.y + step * 0.12 }, { x: m.x - step * 0.12, y: m.y }]))
|
|
771
|
+
}
|
|
772
|
+
return { stars, joins }
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ── Lunar New Year ──
|
|
776
|
+
|
|
777
|
+
/** A round red lantern, `w` wide, hanging from (cx, top): the cord, the caps, the body, its ribs and the tassel. */
|
|
778
|
+
export function redLantern(cx: number, top: number, w: number, cord = 0): { cord: VectorPath[]; caps: VectorPath[]; body: VectorPath[]; ribs: VectorPath[]; tassel: VectorPath[] } {
|
|
779
|
+
const h = w * 0.82, y0 = top + cord + w * 0.08, cy = y0 + h / 2
|
|
780
|
+
const ribs: VectorPath[] = []
|
|
781
|
+
for (const f of [-0.62, -0.3, 0, 0.3, 0.62]) ribs.push({ closed: false, points: [pt3(cx + f * w * 0.32, y0 + h * 0.03), { ...pt3(cx + f * w * 0.32, y0 + h * 0.97), handleIn: pt3(cx + f * w * 0.62, cy) }] })
|
|
782
|
+
const tTop = y0 + h + w * 0.07
|
|
783
|
+
return {
|
|
784
|
+
cord: cord > 0 ? [seg({ x: cx, y: top }, { x: cx, y: top + cord })] : [],
|
|
785
|
+
caps: [rectPath(cx - w * 0.22, y0 - w * 0.07, w * 0.44, w * 0.1), rectPath(cx - w * 0.22, y0 + h - w * 0.03, w * 0.44, w * 0.1)],
|
|
786
|
+
body: [disc(cx, cy, w / 2, h / 2)],
|
|
787
|
+
ribs,
|
|
788
|
+
tassel: [rectPath(cx - w * 0.04, tTop, w * 0.08, w * 0.06), polyP([{ x: cx - w * 0.06, y: tTop + w * 0.06 }, { x: cx + w * 0.06, y: tTop + w * 0.06 }, { x: cx + w * 0.1, y: tTop + w * 0.42 }, { x: cx - w * 0.1, y: tTop + w * 0.42 }])],
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** A plum blossom: five round petals and a heart of stamens. */
|
|
793
|
+
export function plum(cx: number, cy: number, r: number, turn = 0): { petals: VectorPath[]; heart: VectorPath[] } {
|
|
794
|
+
const petals = Array.from({ length: 5 }, (_, i) => { const a = turn + (i * 2 * Math.PI) / 5; const p = at(cx, cy, r * 0.55, a); return disc(p.x, p.y, r * 0.48) })
|
|
795
|
+
return { petals, heart: [disc(cx, cy, r * 0.22)] }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** A gnarled branch: a main limb through `pts` with twigs off it. Returns the strokes, thick and thin. */
|
|
799
|
+
export function branch(pts: P[], seed: number, twigs = 5, len = 60): { limb: VectorPath[]; twigs: VectorPath[]; ends: P[] } {
|
|
800
|
+
const r = rng(seed)
|
|
801
|
+
const limb = [polyP(pts, false)]
|
|
802
|
+
const tw: VectorPath[] = [], ends: P[] = [pts[pts.length - 1]]
|
|
803
|
+
for (let i = 0; i < twigs; i++) {
|
|
804
|
+
const j = 1 + Math.floor(r() * (pts.length - 2))
|
|
805
|
+
const a = pts[j - 1], b = pts[j], t = r()
|
|
806
|
+
const p = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }
|
|
807
|
+
const dir = Math.atan2(b.y - a.y, b.x - a.x) + (r() > 0.5 ? 1 : -1) * (0.5 + r() * 0.5)
|
|
808
|
+
const l = len * (0.5 + r() * 0.6)
|
|
809
|
+
const e = at(p.x, p.y, l, dir), m = at(p.x, p.y, l * 0.5, dir + 0.2)
|
|
810
|
+
tw.push(polyP([p, m, e], false))
|
|
811
|
+
ends.push(e, m)
|
|
812
|
+
}
|
|
813
|
+
return { limb, twigs: tw, ends }
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** An auspicious cloud (xiangyun): three curls on a flat tail, as one stroke, `s` wide. */
|
|
817
|
+
export function cloudCurl(x: number, y: number, s: number, flip = 1): VectorPath[] {
|
|
818
|
+
const u = s / 100
|
|
819
|
+
const curl = (cx: number, cy: number, r: number, dir: number) => polyP(Array.from({ length: 22 }, (_, i) => { const t = i / 21; const a = dir * (Math.PI * 0.2 + t * Math.PI * 1.9); const rr = r * (1 - t * 0.6); return { x: x + flip * (cx + Math.cos(a + Math.PI) * rr) * u, y: y + (cy + Math.sin(a + Math.PI) * rr) * u } }), false)
|
|
820
|
+
return [curl(30, 0, 22, 1), curl(62, -4, 18, -1), curl(48, -20, 14, 1), polyP([{ x: x + flip * 8 * u, y: y + 22 * u }, { x: x + flip * 96 * u, y: y + 22 * u }], false)]
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** A cash coin: a disc with a square hole (drawn as a square in the ground's colour on top). */
|
|
824
|
+
export function coin(cx: number, cy: number, r: number): { coin: VectorPath[]; hole: VectorPath[]; rim: VectorPath[] } {
|
|
825
|
+
return { coin: [disc(cx, cy, r)], rim: [disc(cx, cy, r * 0.82)], hole: [rectPath(cx - r * 0.24, cy - r * 0.24, r * 0.48, r * 0.48)] }
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** A string of firecrackers hanging from (cx, top): the cord and `n` crackers in pairs. */
|
|
829
|
+
export function firecrackers(cx: number, top: number, n: number, s: number): { cord: VectorPath[]; crackers: VectorPath[]; tips: VectorPath[] } {
|
|
830
|
+
const crackers: VectorPath[] = [], tips: VectorPath[] = []
|
|
831
|
+
for (let i = 0; i < n; i++) {
|
|
832
|
+
const y = top + s * 0.5 + i * s * 0.55, side = i % 2 ? 1 : -1
|
|
833
|
+
const c = Math.cos(side * 0.5), sn = Math.sin(side * 0.5)
|
|
834
|
+
const p = (u: number, v: number) => ({ x: cx + u * c - v * sn, y: y + u * sn + v * c })
|
|
835
|
+
crackers.push(polyP([p(0, -s * 0.1), p(side * s * 0.6, -s * 0.1), p(side * s * 0.6, s * 0.1), p(0, s * 0.1)]))
|
|
836
|
+
tips.push(polyP([p(side * s * 0.6, -s * 0.1), p(side * s * 0.68, -s * 0.1), p(side * s * 0.68, s * 0.1), p(side * s * 0.6, s * 0.1)]))
|
|
837
|
+
}
|
|
838
|
+
return { cord: [seg({ x: cx, y: top }, { x: cx, y: top + s * 0.5 + n * s * 0.55 })], crackers, tips }
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ── Print helpers ──
|
|
842
|
+
|
|
843
|
+
/** A border of short dashes round a `w` × `h` box at inset `m` (a stitch, a cut line). */
|
|
844
|
+
export function dashedFrame(w: number, h: number, m: number, r = 0): VectorPath[] {
|
|
845
|
+
return [r > 0 ? roundRect(m, m, w - 2 * m, h - 2 * m, r) : rectPath(m, m, w - 2 * m, h - 2 * m)]
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Two fine rules, one inside the other, round a `w` × `h` box at insets `m` and `m + gap`. */
|
|
849
|
+
export function doubleRule(w: number, h: number, m: number, gap: number, r = 0): VectorPath[] {
|
|
850
|
+
return [r ? roundRect(m, m, w - 2 * m, h - 2 * m, r) : rectPath(m, m, w - 2 * m, h - 2 * m), r ? roundRect(m + gap, m + gap, w - 2 * (m + gap), h - 2 * (m + gap), Math.max(0, r - gap)) : rectPath(m + gap, m + gap, w - 2 * (m + gap), h - 2 * (m + gap))]
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** A seeded maze of `cols` × `rows` cells at (x, y), `cell` px each, with an entrance top left and an exit bottom right. */
|
|
854
|
+
export function maze(x: number, y: number, cols: number, rows: number, cell: number, seed: number): VectorPath[] {
|
|
855
|
+
const r = rng(seed)
|
|
856
|
+
// Walls: right[c][r] and bottom[c][r] true while standing.
|
|
857
|
+
const right = Array.from({ length: cols }, () => Array<boolean>(rows).fill(true))
|
|
858
|
+
const bottom = Array.from({ length: cols }, () => Array<boolean>(rows).fill(true))
|
|
859
|
+
const seen = Array.from({ length: cols }, () => Array<boolean>(rows).fill(false))
|
|
860
|
+
const stack: [number, number][] = [[0, 0]]
|
|
861
|
+
seen[0][0] = true
|
|
862
|
+
while (stack.length) {
|
|
863
|
+
const [c, w] = stack[stack.length - 1]
|
|
864
|
+
const next = ([[1, 0], [-1, 0], [0, 1], [0, -1]] as const).map(([dc, dr]) => [c + dc, w + dr] as [number, number]).filter(([a, b]) => a >= 0 && b >= 0 && a < cols && b < rows && !seen[a][b])
|
|
865
|
+
if (!next.length) { stack.pop(); continue }
|
|
866
|
+
const [a, b] = next[Math.floor(r() * next.length)]
|
|
867
|
+
if (a > c) right[c][w] = false; else if (a < c) right[a][b] = false; else if (b > w) bottom[c][w] = false; else bottom[a][b] = false
|
|
868
|
+
seen[a][b] = true
|
|
869
|
+
stack.push([a, b])
|
|
870
|
+
}
|
|
871
|
+
const out: VectorPath[] = []
|
|
872
|
+
// The outer walls, with the gaps.
|
|
873
|
+
out.push(seg({ x: x + cell, y }, { x: x + cols * cell, y }), seg({ x, y }, { x, y: y + rows * cell }))
|
|
874
|
+
out.push(seg({ x, y: y + rows * cell }, { x: x + (cols - 1) * cell, y: y + rows * cell }), seg({ x: x + cols * cell, y }, { x: x + cols * cell, y: y + rows * cell }))
|
|
875
|
+
for (let c = 0; c < cols; c++) for (let w = 0; w < rows; w++) {
|
|
876
|
+
if (right[c][w] && c < cols - 1) out.push(seg({ x: x + (c + 1) * cell, y: y + w * cell }, { x: x + (c + 1) * cell, y: y + (w + 1) * cell }))
|
|
877
|
+
if (bottom[c][w] && w < rows - 1) out.push(seg({ x: x + c * cell, y: y + (w + 1) * cell }, { x: x + (c + 1) * cell, y: y + (w + 1) * cell }))
|
|
878
|
+
}
|
|
879
|
+
return out
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** A word-search grid: the `words` hidden across, down and diagonally in a `n` × `n` grid, the rest filled at random. */
|
|
883
|
+
export function wordSearch(words: readonly string[], n: number, seed: number): string[] {
|
|
884
|
+
const r = rng(seed)
|
|
885
|
+
const g: string[][] = Array.from({ length: n }, () => Array<string>(n).fill(''))
|
|
886
|
+
const dirs = [[1, 0], [0, 1], [1, 1], [1, -1]] as const
|
|
887
|
+
for (const word of words) {
|
|
888
|
+
const w = word.toUpperCase().replace(/[^A-Z]/g, '')
|
|
889
|
+
for (let tries = 0; tries < 400; tries++) {
|
|
890
|
+
const [dx, dy] = dirs[Math.floor(r() * dirs.length)]
|
|
891
|
+
const x = Math.floor(r() * n), y = Math.floor(r() * n)
|
|
892
|
+
const ex = x + dx * (w.length - 1), ey = y + dy * (w.length - 1)
|
|
893
|
+
if (ex < 0 || ex >= n || ey < 0 || ey >= n) continue
|
|
894
|
+
if ([...w].some((ch, i) => { const c = g[y + dy * i][x + dx * i]; return c && c !== ch })) continue
|
|
895
|
+
;[...w].forEach((ch, i) => { g[y + dy * i][x + dx * i] = ch })
|
|
896
|
+
break
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const A = 'ABCDEFGHIJKLMNOPRSTUWY'
|
|
900
|
+
return g.map(row => row.map(c => c || A[Math.floor(r() * A.length)]).join(' '))
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export { r3, rng, pt3, type P }
|