bsign-customization-full 0.0.1

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.
Files changed (126) hide show
  1. package/.env +2 -0
  2. package/components.json +21 -0
  3. package/dist/colors/anthracite-gray.webp +0 -0
  4. package/dist/colors/anthracite-gray_50x50.png +0 -0
  5. package/dist/colors/dark-wenge.webp +0 -0
  6. package/dist/colors/dark-wenge_50x50.png +0 -0
  7. package/dist/colors/indian-rosewood.webp +0 -0
  8. package/dist/colors/indian-rosewood_50x50.png +0 -0
  9. package/dist/colors/natural-wood.webp +0 -0
  10. package/dist/colors/natural-wood_50x50.png +0 -0
  11. package/dist/colors/redwood.webp +0 -0
  12. package/dist/colors/redwood_50x50.png +0 -0
  13. package/dist/colors/walnut.webp +0 -0
  14. package/dist/colors/walnut_50x50.png +0 -0
  15. package/dist/html2canvas.esm-BJ_egzt0.js +4802 -0
  16. package/dist/index-Dw5Zc1iD.js +33162 -0
  17. package/dist/index.es-Co1KNpGS.js +6681 -0
  18. package/dist/logo.png +0 -0
  19. package/dist/purify.es-CKpD2xIC.js +552 -0
  20. package/dist/sign-constructor.es.js +4 -0
  21. package/dist/sign-constructor.iife.js +171 -0
  22. package/dist/size-guide.webp +0 -0
  23. package/dist/size.webp +0 -0
  24. package/dist/templates/assets/modern/rectangle-black.webp +0 -0
  25. package/dist/templates/assets/modern/rectangle-white.webp +0 -0
  26. package/dist/templates/assets/modern/square-black.webp +0 -0
  27. package/dist/templates/assets/modern/square-white.webp +0 -0
  28. package/dist/templates/assets/wave.webp +0 -0
  29. package/dist/templates/jure.webp +0 -0
  30. package/dist/templates/modern.webp +0 -0
  31. package/dist/templates/sherwood.webp +0 -0
  32. package/dist/templates/wave.webp +0 -0
  33. package/eslint.config.js +23 -0
  34. package/index.html +13 -0
  35. package/modern-debug.svg +39 -0
  36. package/package.json +62 -0
  37. package/public/colors/anthracite-gray.webp +0 -0
  38. package/public/colors/anthracite-gray_50x50.png +0 -0
  39. package/public/colors/dark-wenge.webp +0 -0
  40. package/public/colors/dark-wenge_50x50.png +0 -0
  41. package/public/colors/indian-rosewood.webp +0 -0
  42. package/public/colors/indian-rosewood_50x50.png +0 -0
  43. package/public/colors/natural-wood.webp +0 -0
  44. package/public/colors/natural-wood_50x50.png +0 -0
  45. package/public/colors/redwood.webp +0 -0
  46. package/public/colors/redwood_50x50.png +0 -0
  47. package/public/colors/walnut.webp +0 -0
  48. package/public/colors/walnut_50x50.png +0 -0
  49. package/public/logo.png +0 -0
  50. package/public/size-guide.webp +0 -0
  51. package/public/size.webp +0 -0
  52. package/public/templates/assets/modern/rectangle-black.webp +0 -0
  53. package/public/templates/assets/modern/rectangle-white.webp +0 -0
  54. package/public/templates/assets/modern/square-black.webp +0 -0
  55. package/public/templates/assets/modern/square-white.webp +0 -0
  56. package/public/templates/assets/wave.webp +0 -0
  57. package/public/templates/jure.webp +0 -0
  58. package/public/templates/modern.webp +0 -0
  59. package/public/templates/sherwood.webp +0 -0
  60. package/public/templates/wave.webp +0 -0
  61. package/src/App.css +43 -0
  62. package/src/AppDemo2.tsx +257 -0
  63. package/src/components/cart-panel.tsx +170 -0
  64. package/src/components/cart-preview.tsx +356 -0
  65. package/src/components/cart-view.tsx +113 -0
  66. package/src/components/constructure-menu.tsx +37 -0
  67. package/src/components/header.tsx +214 -0
  68. package/src/components/heading.tsx +28 -0
  69. package/src/components/icons.tsx +54 -0
  70. package/src/components/import-file-modal.tsx +252 -0
  71. package/src/components/layers/grid-view.tsx +29 -0
  72. package/src/components/layers/image-layer.tsx +128 -0
  73. package/src/components/layers/layer-forms/material-form.tsx +53 -0
  74. package/src/components/layers/layer-forms/size-form.tsx +69 -0
  75. package/src/components/layers/layer-forms/template-form.tsx +39 -0
  76. package/src/components/layers/layer-forms/text-form.tsx +477 -0
  77. package/src/components/layers/layers-container.tsx +259 -0
  78. package/src/components/layers/text-layer.tsx +128 -0
  79. package/src/components/movable-item.tsx +228 -0
  80. package/src/components/preview.tsx +258 -0
  81. package/src/components/resize-button.tsx +83 -0
  82. package/src/components/size-guide-modal.tsx +47 -0
  83. package/src/components/size-guide.tsx +98 -0
  84. package/src/components/ui/button-group.tsx +83 -0
  85. package/src/components/ui/button.tsx +60 -0
  86. package/src/components/ui/checkbox.tsx +30 -0
  87. package/src/components/ui/dialog.tsx +151 -0
  88. package/src/components/ui/input-group.tsx +168 -0
  89. package/src/components/ui/input.tsx +21 -0
  90. package/src/components/ui/label.tsx +22 -0
  91. package/src/components/ui/popover.tsx +54 -0
  92. package/src/components/ui/progress.tsx +28 -0
  93. package/src/components/ui/radio-group.tsx +43 -0
  94. package/src/components/ui/scroll-area.tsx +56 -0
  95. package/src/components/ui/select.tsx +191 -0
  96. package/src/components/ui/separator.tsx +25 -0
  97. package/src/components/ui/sheet.tsx +141 -0
  98. package/src/components/ui/slider.tsx +61 -0
  99. package/src/components/ui/spinner.tsx +15 -0
  100. package/src/components/ui/textarea.tsx +18 -0
  101. package/src/components/ui/toggle-group.tsx +73 -0
  102. package/src/components/ui/toggle.tsx +45 -0
  103. package/src/components/ui/tooltip.tsx +67 -0
  104. package/src/fonts/BEBASNEUE-REGULAR.TTF +0 -0
  105. package/src/fonts/Braille-Regular.ttf +0 -0
  106. package/src/fonts/GOTHICB.TTF +0 -0
  107. package/src/hooks/use-mobile.ts +23 -0
  108. package/src/hooks/use-resize-constraints.ts +62 -0
  109. package/src/index.css +238 -0
  110. package/src/index.tsx +141 -0
  111. package/src/lib/cart-proposal-pdf.ts +350 -0
  112. package/src/lib/config-font.tsx +109 -0
  113. package/src/lib/config.ts +730 -0
  114. package/src/lib/pricing.ts +61 -0
  115. package/src/lib/type-checks.ts +47 -0
  116. package/src/lib/utils.ts +146 -0
  117. package/src/lib/widget-context.tsx +9 -0
  118. package/src/main.tsx +11 -0
  119. package/src/store/cart-store.ts +78 -0
  120. package/src/store/layers-store.ts +337 -0
  121. package/src/vite-env.d.ts +1 -0
  122. package/test/preview.html +37 -0
  123. package/tsconfig.app.json +33 -0
  124. package/tsconfig.json +13 -0
  125. package/tsconfig.node.json +25 -0
  126. package/vite.config.ts +38 -0
@@ -0,0 +1,350 @@
1
+ import { jsPDF } from "jspdf"
2
+
3
+ import { getTemplateById, getTemplateMaterial } from "./config"
4
+ import { CURRENCY_SYMBOL, formatPrice, getCartSummary, getUnitPrice } from "./pricing"
5
+ import type { TCartItem } from "../store/cart-store"
6
+ import type { LayerTextProps } from "../components/layers/text-layer"
7
+ import type { TLayer } from "../components/preview"
8
+
9
+ const PROPOSAL_LOGO_SRC = "https://bsign-store.com/cdn/shop/files/Logo_black_140x.png"
10
+ const PAGE_MARGIN_MM = 14
11
+ const HEADER_HEIGHT_MM = 22
12
+ const IMAGE_BOX_SIZE_MM = 52
13
+ const CARD_PADDING_MM = 4
14
+ const CARD_GAP_MM = 6
15
+ const CARD_TEXT_LINE_HEIGHT_MM = 4.8
16
+ const PAGE_FOOTER_SAFE_SPACE_MM = 12
17
+
18
+ const normalizeQty = (qty: number): number => {
19
+ return Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1
20
+ }
21
+
22
+ const formatSizeNumber = (value: number): string => {
23
+ return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, "")
24
+ }
25
+
26
+ const formatLinePrice = (value: number): string => {
27
+ return `${CURRENCY_SYMBOL}${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(2)}`
28
+ }
29
+
30
+ const getPersonalizationLines = (item: TCartItem): string[] => {
31
+ const textLayers = item.sign.filter((layer) => layer.type === "text" && layer.subtype !== "braille") as TLayer<LayerTextProps>[]
32
+ const lines = textLayers
33
+ .map((layer) => layer.props.text.trim())
34
+ .filter(Boolean)
35
+ return lines.length > 0 ? lines : ["-"]
36
+ }
37
+
38
+ const hasAdaFont = (item: TCartItem): boolean => {
39
+ const textLayers = item.sign.filter((layer) => layer.type === "text" && layer.subtype !== "braille") as TLayer<LayerTextProps>[]
40
+ return textLayers.some((layer) => Boolean(layer.props.braille))
41
+ }
42
+
43
+ const getColorLabel = (item: TCartItem): string => {
44
+ const template = getTemplateById(item.options.selectedTemplateId)
45
+ const material = getTemplateMaterial({
46
+ template,
47
+ materialId: item.options.selectedMaterialId,
48
+ })
49
+
50
+ return material.label
51
+ }
52
+
53
+ const blobToDataUrl = (blob: Blob): Promise<string> => {
54
+ return new Promise((resolve, reject) => {
55
+ const reader = new FileReader()
56
+ reader.onerror = () => reject(reader.error ?? new Error("Unable to read image blob"))
57
+ reader.onload = () => {
58
+ if (typeof reader.result === "string") {
59
+ resolve(reader.result)
60
+ return
61
+ }
62
+ reject(new Error("Unable to convert image blob to data URL"))
63
+ }
64
+ reader.readAsDataURL(blob)
65
+ })
66
+ }
67
+
68
+ const resolveImageDataUrl = async (src: string): Promise<string | null> => {
69
+ if (!src) return null
70
+ if (src.startsWith("data:image/")) return src
71
+
72
+ try {
73
+ const response = await fetch(src)
74
+ if (!response.ok) return null
75
+ const blob = await response.blob()
76
+ return blobToDataUrl(blob)
77
+ } catch (error) {
78
+ console.error(error)
79
+ return null
80
+ }
81
+ }
82
+
83
+ const getImageFormat = (dataUrl: string): "PNG" | "JPEG" => {
84
+ const mimeType = dataUrl.match(/^data:image\/([a-zA-Z0-9.+-]+);/)?.[1]?.toLowerCase()
85
+ if (mimeType === "jpg" || mimeType === "jpeg") {
86
+ return "JPEG"
87
+ }
88
+ return "PNG"
89
+ }
90
+
91
+ const drawImageContained = ({
92
+ pdf,
93
+ dataUrl,
94
+ x,
95
+ y,
96
+ width,
97
+ height,
98
+ }: {
99
+ pdf: jsPDF
100
+ dataUrl: string
101
+ x: number
102
+ y: number
103
+ width: number
104
+ height: number
105
+ }): boolean => {
106
+ try {
107
+ const imageProps = pdf.getImageProperties(dataUrl)
108
+ if (!Number.isFinite(imageProps.width) || !Number.isFinite(imageProps.height) || imageProps.width <= 0 || imageProps.height <= 0) {
109
+ return false
110
+ }
111
+
112
+ const scale = Math.min(width / imageProps.width, height / imageProps.height)
113
+ const renderWidth = imageProps.width * scale
114
+ const renderHeight = imageProps.height * scale
115
+ const renderX = x + (width - renderWidth) / 2
116
+ const renderY = y + (height - renderHeight) / 2
117
+
118
+ pdf.addImage(dataUrl, getImageFormat(dataUrl), renderX, renderY, renderWidth, renderHeight, undefined, "FAST")
119
+ return true
120
+ } catch (error) {
121
+ console.error(error)
122
+ return false
123
+ }
124
+ }
125
+
126
+ const drawPageHeader = ({
127
+ pdf,
128
+ logoDataUrl,
129
+ proposalDate,
130
+ proposalId,
131
+ }: {
132
+ pdf: jsPDF
133
+ logoDataUrl: string | null
134
+ proposalDate: string
135
+ proposalId: string
136
+ }): number => {
137
+ const pageWidth = pdf.internal.pageSize.getWidth()
138
+ const topY = PAGE_MARGIN_MM
139
+
140
+ if (logoDataUrl) {
141
+ drawImageContained({
142
+ pdf,
143
+ dataUrl: logoDataUrl,
144
+ x: PAGE_MARGIN_MM,
145
+ y: topY - 1,
146
+ width: 30,
147
+ height: 12,
148
+ })
149
+ }
150
+
151
+ const titleX = logoDataUrl ? PAGE_MARGIN_MM + 35 : PAGE_MARGIN_MM
152
+
153
+ pdf.setFont("helvetica", "bold")
154
+ pdf.setFontSize(18)
155
+ pdf.setTextColor(28, 29, 29)
156
+ pdf.text("Bsign", titleX, topY + 7)
157
+
158
+ pdf.setFont("helvetica", "normal")
159
+ pdf.setFontSize(9.5)
160
+ pdf.setTextColor(102, 102, 102)
161
+ pdf.text(`Date: ${proposalDate}`, pageWidth - PAGE_MARGIN_MM, topY + 4, { align: "right" })
162
+ pdf.text(`#${proposalId}`, pageWidth - PAGE_MARGIN_MM, topY + 9, { align: "right" })
163
+
164
+ const separatorY = topY + HEADER_HEIGHT_MM - 2
165
+ pdf.setDrawColor(218, 218, 218)
166
+ pdf.line(PAGE_MARGIN_MM, separatorY, pageWidth - PAGE_MARGIN_MM, separatorY)
167
+
168
+ return topY + HEADER_HEIGHT_MM + 2
169
+ }
170
+
171
+ const drawSummaryBlock = ({
172
+ pdf,
173
+ y,
174
+ signsCount,
175
+ totalQty,
176
+ totalAmount,
177
+ }: {
178
+ pdf: jsPDF
179
+ y: number
180
+ signsCount: number
181
+ totalQty: number
182
+ totalAmount: number
183
+ }): number => {
184
+ const pageWidth = pdf.internal.pageSize.getWidth()
185
+ const contentWidth = pageWidth - PAGE_MARGIN_MM * 2
186
+
187
+ pdf.setFillColor(247, 247, 247)
188
+ pdf.setDrawColor(222, 222, 222)
189
+ pdf.roundedRect(PAGE_MARGIN_MM, y, contentWidth, 22, 3, 3, "FD")
190
+
191
+ pdf.setFont("helvetica", "bold")
192
+ pdf.setFontSize(10)
193
+ pdf.setTextColor(28, 29, 29)
194
+ pdf.text("Summary", PAGE_MARGIN_MM + 4, y + 6)
195
+
196
+ pdf.setFont("helvetica", "normal")
197
+ pdf.setFontSize(10)
198
+ pdf.setTextColor(56, 56, 56)
199
+ pdf.text(`Signs in cart: ${signsCount}`, PAGE_MARGIN_MM + 4, y + 12)
200
+ pdf.text(`Total quantity: ${totalQty}`, PAGE_MARGIN_MM + 4, y + 17)
201
+
202
+ pdf.setFont("helvetica", "bold")
203
+ pdf.setFontSize(11)
204
+ pdf.setTextColor(28, 29, 29)
205
+ pdf.text(`Total amount: ${formatPrice(totalAmount)}`, pageWidth - PAGE_MARGIN_MM - 4, y + 17, { align: "right" })
206
+
207
+ return y + 28
208
+ }
209
+
210
+ export const downloadCartProposalPdf = async (cart: TCartItem[]): Promise<void> => {
211
+ if (!cart.length) return
212
+
213
+ const [logoDataUrl, ...cartImageDataUrls] = await Promise.all([
214
+ resolveImageDataUrl(PROPOSAL_LOGO_SRC),
215
+ ...cart.map((item) => resolveImageDataUrl(item.img)),
216
+ ])
217
+
218
+ const pdf = new jsPDF({
219
+ orientation: "portrait",
220
+ unit: "mm",
221
+ format: "a4",
222
+ })
223
+
224
+ const pageHeight = pdf.internal.pageSize.getHeight()
225
+ const contentWidth = pdf.internal.pageSize.getWidth() - PAGE_MARGIN_MM * 2
226
+
227
+ const now = new Date()
228
+ const proposalDate = new Intl.DateTimeFormat("en-US", {
229
+ dateStyle: "long",
230
+ }).format(now)
231
+ const proposalId = now.toISOString().replace(/[-:TZ.]/g, "").slice(0, 14)
232
+
233
+ let y = drawPageHeader({
234
+ pdf,
235
+ logoDataUrl,
236
+ proposalDate,
237
+ proposalId,
238
+ })
239
+
240
+ const { totalQty, totalAmount } = getCartSummary(cart)
241
+ y = drawSummaryBlock({
242
+ pdf,
243
+ y,
244
+ signsCount: cart.length,
245
+ totalQty,
246
+ totalAmount,
247
+ })
248
+
249
+ for (const [index, item] of cart.entries()) {
250
+ const qty = normalizeQty(item.qty)
251
+ const lineTotal = getUnitPrice(item.options.selectedSize, qty) * qty
252
+ const colorLabel = getColorLabel(item)
253
+ const templateLabel = getTemplateById(item.options.selectedTemplateId).name
254
+ const personalizationText = getPersonalizationLines(item).join(" | ")
255
+ const adaText = hasAdaFont(item) ? "Yes" : "No"
256
+ const width = formatSizeNumber(item.options.selectedSize.inchs.w)
257
+ const height = formatSizeNumber(item.options.selectedSize.inchs.h)
258
+
259
+ const textX = PAGE_MARGIN_MM + CARD_PADDING_MM + IMAGE_BOX_SIZE_MM + CARD_GAP_MM
260
+ const textWidth = contentWidth - IMAGE_BOX_SIZE_MM - CARD_PADDING_MM * 2 - CARD_GAP_MM
261
+ const detailLines = [
262
+ `Size & Color: ${width}" x ${height}" - ${colorLabel} - ${templateLabel}`,
263
+ `Personalization: ${personalizationText}`,
264
+ `Braille Font: ${adaText}`,
265
+ `Quantity: ${qty}`,
266
+ `Line total: ${formatLinePrice(lineTotal)}`,
267
+ ].flatMap((line) => pdf.splitTextToSize(line, textWidth) as string[])
268
+
269
+ const textBlockHeight = 8 + detailLines.length * CARD_TEXT_LINE_HEIGHT_MM
270
+ const cardHeight = Math.max(IMAGE_BOX_SIZE_MM + CARD_PADDING_MM * 2, textBlockHeight + CARD_PADDING_MM * 2)
271
+
272
+ if (y + cardHeight > pageHeight - PAGE_FOOTER_SAFE_SPACE_MM) {
273
+ pdf.addPage()
274
+ y = drawPageHeader({
275
+ pdf,
276
+ logoDataUrl,
277
+ proposalDate,
278
+ proposalId,
279
+ })
280
+ }
281
+
282
+ pdf.setDrawColor(214, 214, 214)
283
+ pdf.setFillColor(255, 255, 255)
284
+ pdf.roundedRect(PAGE_MARGIN_MM, y, contentWidth, cardHeight, 2.5, 2.5, "FD")
285
+
286
+ const imageX = PAGE_MARGIN_MM + CARD_PADDING_MM
287
+ const imageY = y + (cardHeight - IMAGE_BOX_SIZE_MM) / 2
288
+ pdf.setFillColor(244, 244, 244)
289
+ pdf.rect(imageX, imageY, IMAGE_BOX_SIZE_MM, IMAGE_BOX_SIZE_MM, "F")
290
+
291
+ const imageDataUrl = cartImageDataUrls[index]
292
+ const imageRendered = imageDataUrl
293
+ ? drawImageContained({
294
+ pdf,
295
+ dataUrl: imageDataUrl,
296
+ x: imageX,
297
+ y: imageY,
298
+ width: IMAGE_BOX_SIZE_MM,
299
+ height: IMAGE_BOX_SIZE_MM,
300
+ })
301
+ : false
302
+
303
+ if (!imageRendered) {
304
+ pdf.setFont("helvetica", "normal")
305
+ pdf.setFontSize(8.5)
306
+ pdf.setTextColor(140, 140, 140)
307
+ pdf.text("Preview unavailable", imageX + IMAGE_BOX_SIZE_MM / 2, imageY + IMAGE_BOX_SIZE_MM / 2, { align: "center" })
308
+ }
309
+
310
+ let lineY = y + CARD_PADDING_MM + 5
311
+ pdf.setFont("helvetica", "bold")
312
+ pdf.setFontSize(12)
313
+ pdf.setTextColor(28, 29, 29)
314
+ pdf.text(`Sign ${index + 1}`, textX, lineY)
315
+
316
+ lineY += 5.5
317
+ pdf.setFont("helvetica", "normal")
318
+ pdf.setFontSize(9.8)
319
+ pdf.setTextColor(52, 52, 52)
320
+ for (const line of detailLines) {
321
+ pdf.text(line, textX, lineY)
322
+ lineY += CARD_TEXT_LINE_HEIGHT_MM
323
+ }
324
+
325
+ y += cardHeight + 6
326
+ }
327
+
328
+ if (y + 16 < pageHeight - PAGE_FOOTER_SAFE_SPACE_MM) {
329
+ pdf.setDrawColor(224, 224, 224)
330
+ pdf.line(PAGE_MARGIN_MM, y + 2, PAGE_MARGIN_MM + contentWidth, y + 2)
331
+ pdf.setFont("helvetica", "italic")
332
+ pdf.setFontSize(9)
333
+ pdf.setTextColor(120, 120, 120)
334
+ pdf.text("Thank you for your request. We are ready to proceed after your confirmation.", PAGE_MARGIN_MM, y + 8)
335
+ }
336
+
337
+ const totalPages = pdf.getNumberOfPages()
338
+ for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
339
+ pdf.setPage(pageNumber)
340
+ pdf.setFont("helvetica", "normal")
341
+ pdf.setFontSize(8.5)
342
+ pdf.setTextColor(130, 130, 130)
343
+ pdf.text(`Page ${pageNumber} of ${totalPages}`, pdf.internal.pageSize.getWidth() - PAGE_MARGIN_MM, pageHeight - 5, {
344
+ align: "right",
345
+ })
346
+ }
347
+
348
+ const filenameDate = now.getTime();
349
+ pdf.save(`bsign-cart-${filenameDate}.pdf`);
350
+ }
@@ -0,0 +1,109 @@
1
+ export const BASE_FONT_SIZE = 100 as const;
2
+
3
+ export const BASE_TEXT_SCALE_CHANGE_VALUE = 0.05 as const;
4
+ export const MAX_SCALE = 1.8 as const;
5
+ export const MIN_SCALE = 0.5 as const;
6
+ export const MODERN_MIN_FONT_PT = 64 as const;
7
+
8
+ export const AUTO_ADD_TEXT_SCALE_STEP = 0.30 as const;
9
+ export const AUTO_ADD_TEXT_LINE_DENSITY_STEP = 0.15 as const;
10
+ export const AUTO_ADD_TEXT_MIN_SCALE_RATIO = 0.55 as const;
11
+
12
+ export const MAX_LINE_LENGTH = 12 as const;
13
+ export const MIN_LINE_LENGTH = 1 as const;
14
+ export const MAX_LINE_LENGTH_HARD_CAP = 32 as const;
15
+ export const BASE_FONT_SCALE = 1 as const;
16
+ const MAX_LINE_LENGTH_CURVE_POWER = 1 as const;
17
+ const BASE_SIGN_AREA_INCH = 3.5 * 3.5;
18
+ const BASE_RECOMMENDED_PT = 90 as const;
19
+ const BASE_RECOMMENDED_LINE_LENGTH = 8 as const;
20
+ const TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH = {
21
+ modern: 4,
22
+ wave: BASE_RECOMMENDED_LINE_LENGTH,
23
+ } as const;
24
+
25
+ type TSignInchs = {
26
+ w: number;
27
+ h: number;
28
+ };
29
+
30
+ const getSignSizeFactor = (signInchs?: TSignInchs) => {
31
+ if (!signInchs) return 1;
32
+
33
+ const safeWidth = Math.max(0.1, signInchs.w);
34
+ const safeHeight = Math.max(0.1, signInchs.h);
35
+ const areaRatio = (safeWidth * safeHeight) / BASE_SIGN_AREA_INCH;
36
+
37
+ // Square root keeps growth smoother for bigger signs.
38
+ return Math.sqrt(areaRatio);
39
+ };
40
+
41
+ const getTemplateBaseRecommendedLineLength = (templateId?: string) => {
42
+ if (!templateId) return BASE_RECOMMENDED_LINE_LENGTH;
43
+
44
+ if (templateId in TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH) {
45
+ return TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH[
46
+ templateId as keyof typeof TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH
47
+ ];
48
+ }
49
+
50
+ return BASE_RECOMMENDED_LINE_LENGTH;
51
+ };
52
+
53
+ export const getMaxLineLengthByPt = (
54
+ pt: number,
55
+ signInchs?: TSignInchs,
56
+ templateId?: string
57
+ ) => {
58
+ const safePt = Math.max(1, pt);
59
+ const sizeFactor = getSignSizeFactor(signInchs);
60
+ const baseRecommendedLineLength = getTemplateBaseRecommendedLineLength(templateId);
61
+ const estimatedLength = baseRecommendedLineLength
62
+ * sizeFactor
63
+ * Math.pow(BASE_RECOMMENDED_PT / safePt, MAX_LINE_LENGTH_CURVE_POWER);
64
+
65
+ return Math.max(
66
+ MIN_LINE_LENGTH,
67
+ Math.min(MAX_LINE_LENGTH_HARD_CAP, Math.round(estimatedLength))
68
+ );
69
+ };
70
+
71
+ export const getMinScaleByTemplate = ({
72
+ templateId,
73
+ sizeLimitScale = 1,
74
+ }: {
75
+ templateId?: string;
76
+ sizeLimitScale?: number;
77
+ }) => {
78
+ if (templateId !== "modern") return MIN_SCALE;
79
+
80
+ const normalizedSizeLimitScale =
81
+ Number.isFinite(sizeLimitScale) && sizeLimitScale > 0
82
+ ? sizeLimitScale
83
+ : 1;
84
+
85
+ const modernMinScale = MODERN_MIN_FONT_PT / (BASE_FONT_SIZE * normalizedSizeLimitScale);
86
+
87
+ return Math.min(MAX_SCALE, Math.max(MIN_SCALE, modernMinScale));
88
+ };
89
+
90
+ export const FONT_RANGE = (() => {
91
+ let range: Record<number, number> = {};
92
+
93
+ let newFontScale = BASE_FONT_SCALE;
94
+
95
+ for (let length = 5; length <= MAX_LINE_LENGTH; length += 1) {
96
+ range[length] = newFontScale;
97
+ newFontScale -= 0.1;
98
+ }
99
+
100
+ return range;
101
+ })();
102
+
103
+ // export const LINES_SCALE = [
104
+ // { lines: 2, length: 5, scale: 0.75 },
105
+ // { lines: 2, length: 6, scale: 0.8 },
106
+ // { lines: 2, length: 8, scale: 0.8 },
107
+ // { lines: 3, length: 8, scale: 0.6 },
108
+ // { lines: 4, length: 8, scale: 0.7 },
109
+ // ] as const;