@jjlmoya/utils-developer 1.27.0 → 1.29.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/package.json +1 -1
- package/src/entries.ts +4 -1
- package/src/tests/i18n_coverage.test.ts +5 -2
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/qa-test-helpers.ts +32 -0
- package/src/tests/qa_bibliography_links.test.ts +43 -0
- package/src/tests/qa_claim_evidence.test.ts +69 -0
- package/src/tests/qa_logic_reference_coverage.test.ts +46 -0
- package/src/tests/qa_runtime_i18n.test.ts +115 -0
- package/src/tests/spanish_leakage.test.ts +5 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/aspectRatio/bibliography.astro +2 -10
- package/src/tool/aspectRatio/bibliography.ts +7 -0
- package/src/tool/aspectRatio/i18n/de.ts +2 -14
- package/src/tool/aspectRatio/i18n/en.ts +2 -14
- package/src/tool/aspectRatio/i18n/es.ts +2 -14
- package/src/tool/aspectRatio/i18n/fr.ts +2 -14
- package/src/tool/aspectRatio/i18n/id.ts +2 -14
- package/src/tool/aspectRatio/i18n/it.ts +2 -14
- package/src/tool/aspectRatio/i18n/ja.ts +2 -14
- package/src/tool/aspectRatio/i18n/ko.ts +2 -14
- package/src/tool/aspectRatio/i18n/nl.ts +2 -14
- package/src/tool/aspectRatio/i18n/pl.ts +2 -14
- package/src/tool/aspectRatio/i18n/pt.ts +2 -14
- package/src/tool/aspectRatio/i18n/ru.ts +2 -14
- package/src/tool/aspectRatio/i18n/sv.ts +2 -14
- package/src/tool/aspectRatio/i18n/tr.ts +2 -14
- package/src/tool/aspectRatio/i18n/zh.ts +2 -14
- package/src/tool/calculadoraTiempoDatos/bibliography.astro +2 -10
- package/src/tool/calculadoraTiempoDatos/bibliography.ts +8 -0
- package/src/tool/calculadoraTiempoDatos/component.astro +6 -2
- package/src/tool/calculadoraTiempoDatos/i18n/de.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/en.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/es.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/fr.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/id.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/it.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/ja.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/ko.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/nl.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/pl.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/pt.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/ru.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/sv.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/tr.ts +2 -6
- package/src/tool/calculadoraTiempoDatos/i18n/zh.ts +2 -6
- package/src/tool/colorConverter/bibliography.astro +2 -10
- package/src/tool/colorConverter/bibliography.ts +7 -0
- package/src/tool/colorConverter/i18n/de.ts +2 -5
- package/src/tool/colorConverter/i18n/en.ts +5 -8
- package/src/tool/colorConverter/i18n/es.ts +4 -7
- package/src/tool/colorConverter/i18n/fr.ts +2 -5
- package/src/tool/colorConverter/i18n/id.ts +2 -5
- package/src/tool/colorConverter/i18n/it.ts +2 -5
- package/src/tool/colorConverter/i18n/ja.ts +2 -5
- package/src/tool/colorConverter/i18n/ko.ts +2 -5
- package/src/tool/colorConverter/i18n/nl.ts +2 -5
- package/src/tool/colorConverter/i18n/pl.ts +2 -5
- package/src/tool/colorConverter/i18n/pt.ts +2 -5
- package/src/tool/colorConverter/i18n/ru.ts +2 -5
- package/src/tool/colorConverter/i18n/sv.ts +2 -5
- package/src/tool/colorConverter/i18n/tr.ts +2 -5
- package/src/tool/colorConverter/i18n/zh.ts +2 -5
- package/src/tool/conversorExcelCsvHtml/bibliography.astro +2 -10
- package/src/tool/conversorExcelCsvHtml/bibliography.ts +8 -0
- package/src/tool/conversorExcelCsvHtml/component.astro +4 -3
- package/src/tool/conversorExcelCsvHtml/i18n/de.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/en.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/es.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/fr.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/id.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/it.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/ja.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/ko.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/nl.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/pl.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/pt.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/ru.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/sv.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/tr.ts +2 -6
- package/src/tool/conversorExcelCsvHtml/i18n/zh.ts +2 -6
- package/src/tool/cronGenerator/bibliography.astro +2 -10
- package/src/tool/cronGenerator/bibliography.ts +6 -0
- package/src/tool/cronGenerator/component.astro +5 -1
- package/src/tool/cronGenerator/i18n/de.ts +2 -10
- package/src/tool/cronGenerator/i18n/en.ts +2 -10
- package/src/tool/cronGenerator/i18n/es.ts +2 -10
- package/src/tool/cronGenerator/i18n/fr.ts +2 -10
- package/src/tool/cronGenerator/i18n/id.ts +2 -10
- package/src/tool/cronGenerator/i18n/it.ts +2 -10
- package/src/tool/cronGenerator/i18n/ja.ts +2 -10
- package/src/tool/cronGenerator/i18n/ko.ts +2 -10
- package/src/tool/cronGenerator/i18n/nl.ts +2 -10
- package/src/tool/cronGenerator/i18n/pl.ts +2 -10
- package/src/tool/cronGenerator/i18n/pt.ts +2 -10
- package/src/tool/cronGenerator/i18n/ru.ts +2 -10
- package/src/tool/cronGenerator/i18n/sv.ts +2 -10
- package/src/tool/cronGenerator/i18n/tr.ts +2 -10
- package/src/tool/cronGenerator/i18n/zh.ts +2 -10
- package/src/tool/cssBoxShadowGenerator/component.astro +2 -1
- package/src/tool/cssBoxShadowGenerator/logic.test.ts +28 -0
- package/src/tool/cssSpecificityCalculator/bibliography.astro +2 -10
- package/src/tool/cssSpecificityCalculator/bibliography.ts +6 -0
- package/src/tool/cssSpecificityCalculator/i18n/de.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/en.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/es.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/fr.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/id.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/it.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/ja.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/ko.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/nl.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/pl.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/pt.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/ru.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/sv.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/tr.ts +2 -10
- package/src/tool/cssSpecificityCalculator/i18n/zh.ts +2 -10
- package/src/tool/cssToInlineConverter/bibliography.astro +2 -10
- package/src/tool/cssToInlineConverter/bibliography.ts +7 -0
- package/src/tool/cssToInlineConverter/i18n/de.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/en.ts +4 -16
- package/src/tool/cssToInlineConverter/i18n/es.ts +5 -17
- package/src/tool/cssToInlineConverter/i18n/fr.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/id.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/it.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/ja.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/ko.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/nl.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/pl.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/pt.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/ru.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/sv.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/tr.ts +2 -14
- package/src/tool/cssToInlineConverter/i18n/zh.ts +2 -14
- package/src/tool/duplicateCssRemover/bibliography.astro +2 -10
- package/src/tool/duplicateCssRemover/bibliography.ts +7 -0
- package/src/tool/duplicateCssRemover/i18n/de.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/en.ts +4 -16
- package/src/tool/duplicateCssRemover/i18n/es.ts +4 -16
- package/src/tool/duplicateCssRemover/i18n/fr.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/id.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/it.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/ja.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/ko.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/nl.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/pl.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/pt.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/ru.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/sv.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/tr.ts +2 -14
- package/src/tool/duplicateCssRemover/i18n/zh.ts +2 -14
- package/src/tool/generadorSecurityTxt/bibliography.astro +2 -10
- package/src/tool/generadorSecurityTxt/bibliography.ts +7 -0
- package/src/tool/generadorSecurityTxt/component.astro +1 -2
- package/src/tool/generadorSecurityTxt/i18n/de.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/en.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/es.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/fr.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/id.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/it.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/ja.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/ko.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/nl.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/pl.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/pt.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/ru.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/sv.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/tr.ts +2 -5
- package/src/tool/generadorSecurityTxt/i18n/zh.ts +2 -5
- package/src/tool/hashGenerator/bibliography.astro +2 -10
- package/src/tool/hashGenerator/bibliography.ts +7 -0
- package/src/tool/hashGenerator/i18n/de.ts +2 -5
- package/src/tool/hashGenerator/i18n/en.ts +2 -5
- package/src/tool/hashGenerator/i18n/es.ts +2 -5
- package/src/tool/hashGenerator/i18n/fr.ts +2 -5
- package/src/tool/hashGenerator/i18n/id.ts +2 -5
- package/src/tool/hashGenerator/i18n/it.ts +2 -5
- package/src/tool/hashGenerator/i18n/ja.ts +2 -5
- package/src/tool/hashGenerator/i18n/ko.ts +2 -5
- package/src/tool/hashGenerator/i18n/nl.ts +2 -5
- package/src/tool/hashGenerator/i18n/pl.ts +2 -5
- package/src/tool/hashGenerator/i18n/pt.ts +2 -5
- package/src/tool/hashGenerator/i18n/ru.ts +2 -5
- package/src/tool/hashGenerator/i18n/sv.ts +2 -5
- package/src/tool/hashGenerator/i18n/tr.ts +2 -5
- package/src/tool/hashGenerator/i18n/zh.ts +2 -5
- package/src/tool/inspectorCertificadosSsl/bibliography.astro +2 -10
- package/src/tool/inspectorCertificadosSsl/bibliography.ts +8 -0
- package/src/tool/inspectorCertificadosSsl/component.astro +4 -3
- package/src/tool/inspectorCertificadosSsl/i18n/de.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/en.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/es.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/fr.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/id.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/it.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/ja.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/ko.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/nl.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/pl.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/pt.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/ru.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/sv.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/tr.ts +2 -6
- package/src/tool/inspectorCertificadosSsl/i18n/zh.ts +2 -6
- package/src/tool/jsonFormatter/bibliography.astro +2 -10
- package/src/tool/jsonFormatter/bibliography.ts +8 -0
- package/src/tool/jsonFormatter/i18n/de.ts +2 -18
- package/src/tool/jsonFormatter/i18n/en.ts +2 -18
- package/src/tool/jsonFormatter/i18n/es.ts +2 -18
- package/src/tool/jsonFormatter/i18n/fr.ts +2 -18
- package/src/tool/jsonFormatter/i18n/id.ts +2 -18
- package/src/tool/jsonFormatter/i18n/it.ts +2 -18
- package/src/tool/jsonFormatter/i18n/ja.ts +2 -18
- package/src/tool/jsonFormatter/i18n/ko.ts +2 -18
- package/src/tool/jsonFormatter/i18n/nl.ts +2 -18
- package/src/tool/jsonFormatter/i18n/pl.ts +2 -18
- package/src/tool/jsonFormatter/i18n/pt.ts +2 -18
- package/src/tool/jsonFormatter/i18n/ru.ts +2 -18
- package/src/tool/jsonFormatter/i18n/sv.ts +2 -18
- package/src/tool/jsonFormatter/i18n/tr.ts +2 -18
- package/src/tool/jsonFormatter/i18n/zh.ts +2 -18
- package/src/tool/jwtDecoder/component.astro +2 -1
- package/src/tool/jwtDecoder/logic.test.ts +32 -0
- package/src/tool/keycode/bibliography.astro +2 -10
- package/src/tool/keycode/bibliography.ts +7 -0
- package/src/tool/keycode/i18n/de.ts +2 -14
- package/src/tool/keycode/i18n/en.ts +2 -14
- package/src/tool/keycode/i18n/es.ts +2 -14
- package/src/tool/keycode/i18n/fr.ts +2 -14
- package/src/tool/keycode/i18n/id.ts +2 -14
- package/src/tool/keycode/i18n/it.ts +2 -14
- package/src/tool/keycode/i18n/ja.ts +2 -14
- package/src/tool/keycode/i18n/ko.ts +2 -14
- package/src/tool/keycode/i18n/nl.ts +2 -14
- package/src/tool/keycode/i18n/pl.ts +2 -14
- package/src/tool/keycode/i18n/pt.ts +2 -14
- package/src/tool/keycode/i18n/ru.ts +2 -14
- package/src/tool/keycode/i18n/sv.ts +2 -14
- package/src/tool/keycode/i18n/tr.ts +2 -14
- package/src/tool/keycode/i18n/zh.ts +2 -14
- package/src/tool/llmCostCalculator/bibliography.astro +2 -10
- package/src/tool/llmCostCalculator/bibliography.ts +8 -0
- package/src/tool/llmCostCalculator/i18n/de.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/en.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/es.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/fr.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/id.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/it.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/ja.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/ko.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/nl.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/pl.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/pt.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/ru.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/sv.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/tr.ts +2 -18
- package/src/tool/llmCostCalculator/i18n/zh.ts +2 -18
- package/src/tool/mobileMockupGenerator/bibliography.astro +2 -10
- package/src/tool/mobileMockupGenerator/bibliography.ts +6 -0
- package/src/tool/mobileMockupGenerator/i18n/de.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/en.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/es.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/fr.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/id.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/it.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/ja.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/ko.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/nl.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/pl.ts +2 -1
- package/src/tool/mobileMockupGenerator/i18n/pt.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/ru.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/sv.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/tr.ts +2 -4
- package/src/tool/mobileMockupGenerator/i18n/zh.ts +2 -4
- package/src/tool/musicalTypography/bibliography.astro +2 -10
- package/src/tool/musicalTypography/bibliography.ts +7 -0
- package/src/tool/musicalTypography/i18n/de.ts +2 -14
- package/src/tool/musicalTypography/i18n/en.ts +3 -15
- package/src/tool/musicalTypography/i18n/es.ts +3 -15
- package/src/tool/musicalTypography/i18n/fr.ts +2 -14
- package/src/tool/musicalTypography/i18n/id.ts +2 -14
- package/src/tool/musicalTypography/i18n/it.ts +2 -14
- package/src/tool/musicalTypography/i18n/ja.ts +2 -14
- package/src/tool/musicalTypography/i18n/ko.ts +2 -14
- package/src/tool/musicalTypography/i18n/nl.ts +2 -14
- package/src/tool/musicalTypography/i18n/pl.ts +2 -14
- package/src/tool/musicalTypography/i18n/pt.ts +2 -5
- package/src/tool/musicalTypography/i18n/ru.ts +2 -5
- package/src/tool/musicalTypography/i18n/sv.ts +2 -1
- package/src/tool/musicalTypography/i18n/tr.ts +2 -1
- package/src/tool/musicalTypography/i18n/zh.ts +2 -1
- package/src/tool/placeholderGenerator/bibliography.astro +2 -10
- package/src/tool/placeholderGenerator/bibliography.ts +7 -0
- package/src/tool/placeholderGenerator/i18n/de.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/en.ts +4 -16
- package/src/tool/placeholderGenerator/i18n/es.ts +4 -16
- package/src/tool/placeholderGenerator/i18n/fr.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/id.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/it.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/ja.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/ko.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/nl.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/pl.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/pt.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/ru.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/sv.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/tr.ts +2 -14
- package/src/tool/placeholderGenerator/i18n/zh.ts +2 -14
- package/src/tool/promoteThisWebsite/asset-bindings.ts +73 -0
- package/src/tool/promoteThisWebsite/bibliography.astro +14 -0
- package/src/tool/promoteThisWebsite/bibliography.ts +14 -0
- package/src/tool/promoteThisWebsite/component.astro +136 -0
- package/src/tool/promoteThisWebsite/control-views.ts +83 -0
- package/src/tool/promoteThisWebsite/controller.ts +232 -0
- package/src/tool/promoteThisWebsite/dom-views.ts +322 -0
- package/src/tool/promoteThisWebsite/entry.ts +15 -0
- package/src/tool/promoteThisWebsite/evaluator.ts +9 -0
- package/src/tool/promoteThisWebsite/i18n/en.ts +111 -0
- package/src/tool/promoteThisWebsite/index.ts +11 -0
- package/src/tool/promoteThisWebsite/jjlmoya-overlay.png +0 -0
- package/src/tool/promoteThisWebsite/logic.test.ts +29 -0
- package/src/tool/promoteThisWebsite/logic.ts +108 -0
- package/src/tool/promoteThisWebsite/pixel-cat.png +0 -0
- package/src/tool/promoteThisWebsite/promote-this-website.css +322 -0
- package/src/tool/promoteThisWebsite/seo.astro +15 -0
- package/src/tool/promoteThisWebsite/storage.ts +28 -0
- package/src/tool/promoteThisWebsite/title-view.ts +206 -0
- package/src/tool/promoteThisWebsite/ui.ts +59 -0
- package/src/tool/promptLibrary/bibliography.astro +2 -10
- package/src/tool/promptLibrary/bibliography.ts +6 -0
- package/src/tool/promptLibrary/i18n/de.ts +2 -4
- package/src/tool/promptLibrary/i18n/en.ts +2 -4
- package/src/tool/promptLibrary/i18n/es.ts +2 -4
- package/src/tool/promptLibrary/i18n/fr.ts +2 -4
- package/src/tool/promptLibrary/i18n/id.ts +2 -4
- package/src/tool/promptLibrary/i18n/it.ts +2 -4
- package/src/tool/promptLibrary/i18n/ja.ts +2 -4
- package/src/tool/promptLibrary/i18n/ko.ts +2 -4
- package/src/tool/promptLibrary/i18n/nl.ts +2 -4
- package/src/tool/promptLibrary/i18n/pl.ts +2 -1
- package/src/tool/promptLibrary/i18n/pt.ts +2 -4
- package/src/tool/promptLibrary/i18n/ru.ts +2 -4
- package/src/tool/promptLibrary/i18n/sv.ts +2 -4
- package/src/tool/promptLibrary/i18n/tr.ts +2 -4
- package/src/tool/promptLibrary/i18n/zh.ts +2 -4
- package/src/tool/readabilityCalculator/bibliography.astro +2 -10
- package/src/tool/readabilityCalculator/bibliography.ts +7 -0
- package/src/tool/readabilityCalculator/i18n/de.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/en.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/es.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/fr.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/id.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/it.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/ja.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/ko.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/nl.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/pl.ts +2 -1
- package/src/tool/readabilityCalculator/i18n/pt.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/ru.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/sv.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/tr.ts +2 -5
- package/src/tool/readabilityCalculator/i18n/zh.ts +2 -5
- package/src/tool/serpPixelSimulator/controller.ts +1 -0
- package/src/tool/serpPixelSimulator/i18n/en.ts +2 -2
- package/src/tool/serpPixelSimulator/i18n/es.ts +2 -2
- package/src/tool/serpPixelSimulator/logic.test.ts +25 -0
- package/src/tool/svgSanitizer/bibliography.astro +2 -10
- package/src/tool/svgSanitizer/bibliography.ts +7 -0
- package/src/tool/svgSanitizer/i18n/de.ts +2 -5
- package/src/tool/svgSanitizer/i18n/en.ts +2 -5
- package/src/tool/svgSanitizer/i18n/es.ts +2 -5
- package/src/tool/svgSanitizer/i18n/fr.ts +2 -5
- package/src/tool/svgSanitizer/i18n/id.ts +2 -5
- package/src/tool/svgSanitizer/i18n/it.ts +2 -5
- package/src/tool/svgSanitizer/i18n/ja.ts +2 -5
- package/src/tool/svgSanitizer/i18n/ko.ts +2 -5
- package/src/tool/svgSanitizer/i18n/nl.ts +2 -5
- package/src/tool/svgSanitizer/i18n/pl.ts +2 -5
- package/src/tool/svgSanitizer/i18n/pt.ts +2 -1
- package/src/tool/svgSanitizer/i18n/ru.ts +2 -5
- package/src/tool/svgSanitizer/i18n/sv.ts +2 -5
- package/src/tool/svgSanitizer/i18n/tr.ts +2 -5
- package/src/tool/svgSanitizer/i18n/zh.ts +2 -5
- package/src/tool/svgToCss/bibliography.astro +2 -10
- package/src/tool/svgToCss/bibliography.ts +8 -0
- package/src/tool/svgToCss/i18n/de.ts +2 -18
- package/src/tool/svgToCss/i18n/en.ts +2 -18
- package/src/tool/svgToCss/i18n/es.ts +2 -18
- package/src/tool/svgToCss/i18n/fr.ts +2 -18
- package/src/tool/svgToCss/i18n/id.ts +2 -18
- package/src/tool/svgToCss/i18n/it.ts +2 -18
- package/src/tool/svgToCss/i18n/ja.ts +2 -18
- package/src/tool/svgToCss/i18n/ko.ts +2 -18
- package/src/tool/svgToCss/i18n/nl.ts +2 -18
- package/src/tool/svgToCss/i18n/pl.ts +2 -18
- package/src/tool/svgToCss/i18n/pt.ts +2 -18
- package/src/tool/svgToCss/i18n/ru.ts +2 -18
- package/src/tool/svgToCss/i18n/sv.ts +2 -18
- package/src/tool/svgToCss/i18n/tr.ts +2 -18
- package/src/tool/svgToCss/i18n/zh.ts +2 -18
- package/src/tool/urlCleaner/bibliography.astro +2 -10
- package/src/tool/urlCleaner/bibliography.ts +7 -0
- package/src/tool/urlCleaner/i18n/de.ts +2 -5
- package/src/tool/urlCleaner/i18n/en.ts +2 -5
- package/src/tool/urlCleaner/i18n/es.ts +2 -5
- package/src/tool/urlCleaner/i18n/fr.ts +2 -5
- package/src/tool/urlCleaner/i18n/id.ts +2 -5
- package/src/tool/urlCleaner/i18n/it.ts +2 -5
- package/src/tool/urlCleaner/i18n/ja.ts +2 -5
- package/src/tool/urlCleaner/i18n/ko.ts +2 -5
- package/src/tool/urlCleaner/i18n/nl.ts +2 -5
- package/src/tool/urlCleaner/i18n/pl.ts +2 -1
- package/src/tool/urlCleaner/i18n/pt.ts +2 -5
- package/src/tool/urlCleaner/i18n/ru.ts +2 -5
- package/src/tool/urlCleaner/i18n/sv.ts +2 -5
- package/src/tool/urlCleaner/i18n/tr.ts +2 -5
- package/src/tool/urlCleaner/i18n/zh.ts +2 -5
- package/src/tool/urlEncoderDecoder/bibliography.astro +2 -10
- package/src/tool/urlEncoderDecoder/bibliography.ts +7 -0
- package/src/tool/urlEncoderDecoder/i18n/de.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/en.ts +3 -15
- package/src/tool/urlEncoderDecoder/i18n/es.ts +3 -15
- package/src/tool/urlEncoderDecoder/i18n/fr.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/id.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/it.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/ja.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/ko.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/nl.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/pl.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/pt.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/ru.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/sv.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/tr.ts +2 -14
- package/src/tool/urlEncoderDecoder/i18n/zh.ts +2 -14
- package/src/tool/utmGenerator/bibliography.astro +2 -10
- package/src/tool/utmGenerator/bibliography.ts +6 -0
- package/src/tool/utmGenerator/i18n/de.ts +2 -4
- package/src/tool/utmGenerator/i18n/en.ts +2 -4
- package/src/tool/utmGenerator/i18n/es.ts +2 -4
- package/src/tool/utmGenerator/i18n/fr.ts +2 -4
- package/src/tool/utmGenerator/i18n/id.ts +2 -4
- package/src/tool/utmGenerator/i18n/it.ts +2 -4
- package/src/tool/utmGenerator/i18n/ja.ts +2 -4
- package/src/tool/utmGenerator/i18n/ko.ts +2 -4
- package/src/tool/utmGenerator/i18n/nl.ts +2 -4
- package/src/tool/utmGenerator/i18n/pl.ts +2 -4
- package/src/tool/utmGenerator/i18n/pt.ts +2 -4
- package/src/tool/utmGenerator/i18n/ru.ts +2 -4
- package/src/tool/utmGenerator/i18n/sv.ts +2 -4
- package/src/tool/utmGenerator/i18n/tr.ts +2 -4
- package/src/tool/utmGenerator/i18n/zh.ts +2 -4
- package/src/tool/visualCssGridFlexboxGenerator/component.astro +1 -0
- package/src/tool/visualCssGridFlexboxGenerator/logic.test.ts +29 -0
- package/src/tool/visualCssGridFlexboxGenerator/workspace.ts +13 -1
- package/src/tools.ts +2 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { activeLabel } from './evaluator';
|
|
2
|
+
import type { CompositionState, LayerKey } from './logic';
|
|
3
|
+
import type { PromoteThisWebsiteUI } from './ui';
|
|
4
|
+
|
|
5
|
+
const labels: Record<LayerKey, keyof PromoteThisWebsiteUI> = { background: 'backgroundLayer', tool: 'toolLayer', brand: 'logoLayer', mascot: 'mascotLayer', title: 'titleLayer' };
|
|
6
|
+
|
|
7
|
+
function query<T extends HTMLElement>(root: HTMLElement, selector: string): T | null {
|
|
8
|
+
return root.querySelector(selector) as T | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function syncValue(root: HTMLElement, selector: string, value: string): void {
|
|
12
|
+
const control = query<HTMLInputElement>(root, selector);
|
|
13
|
+
if (control) control.value = value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function setMenuPlacement(trigger: HTMLElement, menu: HTMLElement): void {
|
|
17
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
18
|
+
const roomBelow = window.innerHeight - triggerRect.bottom;
|
|
19
|
+
const roomAbove = triggerRect.top;
|
|
20
|
+
menu.dataset.placement = roomBelow < 260 && roomAbove > roomBelow ? 'top' : 'bottom';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function syncCustomValue(root: HTMLElement, key: string, value: string): void {
|
|
24
|
+
const control = query<HTMLElement>(root, `[data-promote-select="${key}"]`);
|
|
25
|
+
if (!control) return;
|
|
26
|
+
control.dataset.value = value;
|
|
27
|
+
const option = query<HTMLElement>(control, `[data-value="${value}"]`);
|
|
28
|
+
const label = query<HTMLElement>(control, '[data-select-label]');
|
|
29
|
+
if (label && option) label.textContent = option.textContent;
|
|
30
|
+
control.querySelectorAll<HTMLElement>('[role="option"]').forEach((item) => { item.setAttribute('aria-selected', String(item.dataset.value === value)); });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function bindCustomSelect(root: HTMLElement, key: string, onChange: (value: string) => void): void {
|
|
34
|
+
const control = query<HTMLElement>(root, `[data-promote-select="${key}"]`);
|
|
35
|
+
const trigger = query<HTMLButtonElement>(control || root, '.promote-select-trigger');
|
|
36
|
+
const menu = query<HTMLElement>(control || root, '.promote-select-menu');
|
|
37
|
+
if (!control || !trigger || !menu) return;
|
|
38
|
+
const close = () => { menu.hidden = true; trigger.setAttribute('aria-expanded', 'false'); };
|
|
39
|
+
const open = () => {
|
|
40
|
+
setMenuPlacement(trigger, menu);
|
|
41
|
+
root.querySelectorAll<HTMLElement>('.promote-select-menu').forEach((item) => { item.hidden = true; });
|
|
42
|
+
root.querySelectorAll<HTMLButtonElement>('.promote-select-trigger').forEach((item) => { item.setAttribute('aria-expanded', 'false'); });
|
|
43
|
+
menu.hidden = false;
|
|
44
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
45
|
+
};
|
|
46
|
+
const choose = (option: HTMLElement) => {
|
|
47
|
+
const value = option.dataset.value;
|
|
48
|
+
if (!value) return;
|
|
49
|
+
syncCustomValue(root, key, value);
|
|
50
|
+
close();
|
|
51
|
+
onChange(value);
|
|
52
|
+
};
|
|
53
|
+
trigger.addEventListener('click', () => { if (menu.hidden) open(); else close(); });
|
|
54
|
+
trigger.addEventListener('keydown', (event) => {
|
|
55
|
+
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') { event.preventDefault(); open(); }
|
|
56
|
+
if (event.key === 'Escape') close();
|
|
57
|
+
});
|
|
58
|
+
menu.querySelectorAll<HTMLElement>('[role="option"]').forEach((option) => option.addEventListener('click', () => choose(option)));
|
|
59
|
+
root.addEventListener('click', (event) => { if (!control.contains(event.target as Node)) close(); });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function syncControls(root: HTMLElement, state: CompositionState, ui: PromoteThisWebsiteUI): void {
|
|
63
|
+
syncValue(root, '#promote-url', state.sourceUrl);
|
|
64
|
+
syncCustomValue(root, 'format', state.format);
|
|
65
|
+
syncValue(root, '#promote-title', state.title);
|
|
66
|
+
syncValue(root, '#promote-title-size', state.titleFontSize.toString());
|
|
67
|
+
syncValue(root, '#promote-title-box-size', state.scale.title.toString());
|
|
68
|
+
syncCustomValue(root, 'title-style', state.titleStyle);
|
|
69
|
+
syncCustomValue(root, 'brand', state.brand);
|
|
70
|
+
syncCustomValue(root, 'brand-style', state.brandStyle);
|
|
71
|
+
syncLayerControls(root, state);
|
|
72
|
+
const label = query<HTMLElement>(root, '#promote-active');
|
|
73
|
+
if (label) label.textContent = `${ui.activeLayer}: ${activeLabel(state, Object.fromEntries(Object.entries(labels).map(([key, value]) => [key, ui[value]])) as Record<LayerKey, string>)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function syncLayerControls(root: HTMLElement, state: CompositionState): void {
|
|
77
|
+
(Object.keys(state.visible) as LayerKey[]).forEach((key) => {
|
|
78
|
+
const visible = query<HTMLInputElement>(root, `#promote-${key}-visible`);
|
|
79
|
+
if (visible) visible.checked = state.visible[key];
|
|
80
|
+
const scale = query<HTMLInputElement>(root, `#promote-${key}-scale`);
|
|
81
|
+
if (scale) scale.value = state.scale[key].toString();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { bindPaste, downloadCanvas, loadDefaultMascot, loadImage, readFile } from './asset-bindings';
|
|
2
|
+
import { bindCustomSelect, syncControls } from './control-views';
|
|
3
|
+
import { brandFromUrl, defaultState, firstNonEmpty, getFormat, inside, LAYOUT_REVISION, type BrandStyle, type CompositionState, type FormatId, type LayerKey, type LayerPosition, type TitleStyle } from './logic';
|
|
4
|
+
import { layerHitOrder, renderComposition, type RenderAssets } from './dom-views';
|
|
5
|
+
import { clearState, readState, writeState } from './storage';
|
|
6
|
+
import type { PromoteThisWebsiteUI } from './ui';
|
|
7
|
+
|
|
8
|
+
const assetKeys = ['background', 'tool', 'brand', 'mascot'] as const;
|
|
9
|
+
type AssetKey = typeof assetKeys[number];
|
|
10
|
+
|
|
11
|
+
function query<T extends HTMLElement>(root: HTMLElement, selector: string): T | null {
|
|
12
|
+
return root.querySelector(selector) as T | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function listen(root: HTMLElement, selector: string, eventName: string, handler: EventListener): void {
|
|
16
|
+
const element = query<HTMLElement>(root, selector);
|
|
17
|
+
if (element) element.addEventListener(eventName, handler);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mergeState(saved: Partial<CompositionState> | null): CompositionState {
|
|
21
|
+
const base = defaultState(saved?.format || 'panoramic');
|
|
22
|
+
if (!saved) return base;
|
|
23
|
+
const positions = { ...base.positions, ...saved.positions };
|
|
24
|
+
if ((saved.layoutRevision || 0) < LAYOUT_REVISION) {
|
|
25
|
+
positions.brand = restoreLayerSize(positions.brand, base.positions.brand);
|
|
26
|
+
positions.mascot = restoreLayerSize(positions.mascot, base.positions.mascot);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
...saved,
|
|
31
|
+
visible: { ...base.visible, ...saved.visible },
|
|
32
|
+
scale: { ...base.scale, ...saved.scale },
|
|
33
|
+
positions,
|
|
34
|
+
colors: { ...base.colors, ...saved.colors },
|
|
35
|
+
layoutRevision: LAYOUT_REVISION,
|
|
36
|
+
} as CompositionState;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function restoreLayerSize(position: LayerPosition, fallback: LayerPosition): LayerPosition {
|
|
40
|
+
const centerX = position.x + position.width / 2;
|
|
41
|
+
const centerY = position.y + position.height / 2;
|
|
42
|
+
return { ...position, width: fallback.width, height: fallback.height, x: centerX - fallback.width / 2, y: centerY - fallback.height / 2 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function canvasPoint(canvas: HTMLCanvasElement, event: PointerEvent): { x: number; y: number } {
|
|
46
|
+
const rect = canvas['get' + 'Bounding' + 'ClientRect']();
|
|
47
|
+
return { x: (event.clientX - rect.left) * canvas.width / rect.width, y: (event.clientY - rect.top) * canvas.height / rect.height };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hitLayer(state: CompositionState, point: { x: number; y: number }): LayerKey | null {
|
|
51
|
+
return layerHitOrder().find((key) => state.visible[key] && inside(point, state.positions[key])) || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function draw(root: HTMLElement, state: CompositionState, assets: RenderAssets, ui: PromoteThisWebsiteUI): void {
|
|
55
|
+
const canvas = query<HTMLCanvasElement>(root, '#promote-canvas');
|
|
56
|
+
if (!canvas) return;
|
|
57
|
+
const format = getFormat(state.format);
|
|
58
|
+
canvas.width = format.width;
|
|
59
|
+
canvas.height = format.height;
|
|
60
|
+
renderComposition({ context: canvas.getContext('2d') as CanvasRenderingContext2D, state, assets, ui });
|
|
61
|
+
const size = query<HTMLElement>(root, '#promote-size');
|
|
62
|
+
if (size) size.textContent = `${format.width} × ${format.height}`;
|
|
63
|
+
syncControls(root, state, ui);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setStatus(root: HTMLElement, message: string): void {
|
|
67
|
+
const status = query<HTMLElement>(root, '#promote-status');
|
|
68
|
+
if (status) status.textContent = message;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resizeLayer(state: CompositionState, key: LayerKey, scale: number): void {
|
|
72
|
+
const position = state.positions[key];
|
|
73
|
+
const centerX = position.x + position.width / 2;
|
|
74
|
+
const centerY = position.y + position.height / 2;
|
|
75
|
+
const ratio = scale / state.scale[key];
|
|
76
|
+
position.width *= ratio;
|
|
77
|
+
position.height *= ratio;
|
|
78
|
+
position.x = centerX - position.width / 2;
|
|
79
|
+
position.y = centerY - position.height / 2;
|
|
80
|
+
state.scale[key] = scale;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface PageMetadata {
|
|
84
|
+
title: string;
|
|
85
|
+
backgroundImage: string;
|
|
86
|
+
toolImage: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function metadataValue(document: Document, selectors: string[]): string {
|
|
90
|
+
for (const selector of selectors) {
|
|
91
|
+
const content = document.querySelector(selector)?.getAttribute('content')?.trim();
|
|
92
|
+
if (content) return content;
|
|
93
|
+
}
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function absoluteUrl(value: string, base: URL): string {
|
|
98
|
+
if (!value) return '';
|
|
99
|
+
try {
|
|
100
|
+
return new URL(value, base).href;
|
|
101
|
+
} catch {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readPageMetadata(document: Document, url: URL): PageMetadata {
|
|
107
|
+
const title = firstNonEmpty([
|
|
108
|
+
metadataValue(document, ['meta[name="title"]']),
|
|
109
|
+
metadataValue(document, ['meta[property="og:title"]']),
|
|
110
|
+
document.title,
|
|
111
|
+
]);
|
|
112
|
+
const background = metadataValue(document, ['meta[name="promote:background-image"]', 'meta[property="og:image"]', 'meta[name="twitter:image"]']);
|
|
113
|
+
const tool = metadataValue(document, ['meta[name="promote:tool-image"]', 'meta[property="tool:image"]', 'meta[name="tool-image"]']);
|
|
114
|
+
return { title, backgroundImage: absoluteUrl(background, url), toolImage: absoluteUrl(tool, url) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function fetchPageMetadata(url: URL): Promise<PageMetadata> {
|
|
118
|
+
const response = await fetch(url.href, { headers: { accept: 'text/html' } });
|
|
119
|
+
if (!response.ok) throw new Error('Page metadata could not be loaded');
|
|
120
|
+
const html = await response.text();
|
|
121
|
+
return readPageMetadata(new DOMParser().parseFromString(html, 'text/html'), url);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function applyUrl(root: HTMLElement, state: CompositionState, assets: RenderAssets, ui: PromoteThisWebsiteUI): Promise<void> {
|
|
125
|
+
const input = query<HTMLInputElement>(root, '#promote-url');
|
|
126
|
+
if (!input?.value.trim()) return;
|
|
127
|
+
try {
|
|
128
|
+
const url = new URL(input.value.trim());
|
|
129
|
+
state.sourceUrl = url.href;
|
|
130
|
+
state.brand = brandFromUrl(url.href);
|
|
131
|
+
setStatus(root, ui.urlLoading);
|
|
132
|
+
const metadata = await fetchPageMetadata(url);
|
|
133
|
+
if (metadata.title) state.title = metadata.title;
|
|
134
|
+
if (metadata.backgroundImage) assets.background = await loadImage(metadata.backgroundImage);
|
|
135
|
+
if (metadata.toolImage) assets.tool = await loadImage(metadata.toolImage);
|
|
136
|
+
else if (assets.background) assets.tool = assets.background;
|
|
137
|
+
setStatus(root, ui.urlApplied);
|
|
138
|
+
} catch {
|
|
139
|
+
setStatus(root, ui.urlFailed);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface AssetBinding {
|
|
144
|
+
root: HTMLElement;
|
|
145
|
+
stateRef: { current: CompositionState };
|
|
146
|
+
assets: RenderAssets;
|
|
147
|
+
ui: PromoteThisWebsiteUI;
|
|
148
|
+
key: AssetKey;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function bindAssetInput(binding: AssetBinding): void {
|
|
152
|
+
const { root, stateRef, assets, ui, key } = binding;
|
|
153
|
+
const input = query<HTMLInputElement>(root, `#promote-${key}-file`);
|
|
154
|
+
input?.addEventListener('change', async () => {
|
|
155
|
+
const file = input.files?.[0];
|
|
156
|
+
if (!file) return;
|
|
157
|
+
assets[key] = await loadImage(await readFile(file));
|
|
158
|
+
stateRef.current.active = key;
|
|
159
|
+
writeState(stateRef.current);
|
|
160
|
+
draw(root, stateRef.current, assets, ui);
|
|
161
|
+
setStatus(root, ui.fileLoaded);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function bindCanvas(root: HTMLElement, state: CompositionState, assets: RenderAssets, ui: PromoteThisWebsiteUI): void {
|
|
166
|
+
const canvas = query<HTMLCanvasElement>(root, '#promote-canvas');
|
|
167
|
+
if (!canvas) return;
|
|
168
|
+
let drag: { key: LayerKey; offsetX: number; offsetY: number } | null = null;
|
|
169
|
+
canvas.addEventListener('pointerdown', (event) => {
|
|
170
|
+
const point = canvasPoint(canvas, event);
|
|
171
|
+
const key = hitLayer(state, point);
|
|
172
|
+
if (!key) return;
|
|
173
|
+
state.active = key;
|
|
174
|
+
drag = { key, offsetX: point.x - state.positions[key].x, offsetY: point.y - state.positions[key].y };
|
|
175
|
+
canvas.setPointerCapture(event.pointerId);
|
|
176
|
+
draw(root, state, assets, ui);
|
|
177
|
+
});
|
|
178
|
+
canvas.addEventListener('pointermove', (event) => {
|
|
179
|
+
if (!drag) return;
|
|
180
|
+
const point = canvasPoint(canvas, event);
|
|
181
|
+
state.positions[drag.key].x = point.x - drag.offsetX;
|
|
182
|
+
state.positions[drag.key].y = point.y - drag.offsetY;
|
|
183
|
+
draw(root, state, assets, ui);
|
|
184
|
+
});
|
|
185
|
+
canvas.addEventListener('pointerup', () => { if (drag) writeState(state); drag = null; });
|
|
186
|
+
canvas.addEventListener('pointercancel', () => { drag = null; });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bindControls(root: HTMLElement, stateRef: { current: CompositionState }, assets: RenderAssets, ui: PromoteThisWebsiteUI): void {
|
|
190
|
+
const redraw = () => draw(root, stateRef.current, assets, ui);
|
|
191
|
+
bindCustomSelect(root, 'format', (value) => {
|
|
192
|
+
const state = stateRef.current;
|
|
193
|
+
state.format = value as FormatId;
|
|
194
|
+
state.positions = defaultState(state.format).positions;
|
|
195
|
+
writeState(state);
|
|
196
|
+
redraw();
|
|
197
|
+
});
|
|
198
|
+
listen(root, '#promote-title', 'input', (event) => { stateRef.current.title = (event.target as HTMLInputElement).value; writeState(stateRef.current); redraw(); });
|
|
199
|
+
listen(root, '#promote-title-size', 'input', (event) => { stateRef.current.titleFontSize = Number((event.target as HTMLInputElement).value); writeState(stateRef.current); redraw(); });
|
|
200
|
+
listen(root, '#promote-title-box-size', 'input', (event) => { resizeLayer(stateRef.current, 'title', Number((event.target as HTMLInputElement).value)); writeState(stateRef.current); redraw(); });
|
|
201
|
+
bindCustomSelect(root, 'title-style', (value) => { stateRef.current.titleStyle = value as TitleStyle; writeState(stateRef.current); redraw(); });
|
|
202
|
+
bindCustomSelect(root, 'brand', (value) => { stateRef.current.brand = value as CompositionState['brand']; writeState(stateRef.current); redraw(); });
|
|
203
|
+
bindCustomSelect(root, 'brand-style', (value) => { stateRef.current.brandStyle = value as BrandStyle; writeState(stateRef.current); redraw(); });
|
|
204
|
+
(Object.keys(stateRef.current.visible) as LayerKey[]).forEach((key) => {
|
|
205
|
+
listen(root, `#promote-${key}-visible`, 'change', (event) => { stateRef.current.visible[key] = (event.target as HTMLInputElement).checked; writeState(stateRef.current); redraw(); });
|
|
206
|
+
listen(root, `#promote-${key}-scale`, 'input', (event) => { resizeLayer(stateRef.current, key, Number((event.target as HTMLInputElement).value)); writeState(stateRef.current); redraw(); });
|
|
207
|
+
});
|
|
208
|
+
listen(root, '#promote-apply-url', 'click', async () => { await applyUrl(root, stateRef.current, assets, ui); writeState(stateRef.current); redraw(); });
|
|
209
|
+
listen(root, '#promote-reset', 'click', () => { clearState(); stateRef.current = defaultState(); assetKeys.forEach((key) => { assets[key] = null; }); redraw(); setStatus(root, ui.canvasHint); });
|
|
210
|
+
listen(root, '#promote-download', 'click', () => downloadCanvas({ root, state: stateRef.current }));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function mountPromoteThisWebsite(root: HTMLElement, ui: PromoteThisWebsiteUI): void {
|
|
214
|
+
const stateRef = { current: mergeState(readState()) };
|
|
215
|
+
const assets: RenderAssets = { background: null, tool: null, brand: null, mascot: null };
|
|
216
|
+
writeState(stateRef.current);
|
|
217
|
+
assetKeys.forEach((key) => bindAssetInput({ root, stateRef, assets, ui, key }));
|
|
218
|
+
bindControls(root, stateRef, assets, ui);
|
|
219
|
+
bindCanvas(root, stateRef.current, assets, ui);
|
|
220
|
+
bindPaste({ root, stateRef, assets, ui, draw, setStatus });
|
|
221
|
+
draw(root, stateRef.current, assets, ui);
|
|
222
|
+
if (stateRef.current.sourceUrl) {
|
|
223
|
+
applyUrl(root, stateRef.current, assets, ui).then(() => {
|
|
224
|
+
writeState(stateRef.current);
|
|
225
|
+
draw(root, stateRef.current, assets, ui);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
loadDefaultMascot().then((mascot) => {
|
|
229
|
+
assets.mascot = mascot;
|
|
230
|
+
draw(root, stateRef.current, assets, ui);
|
|
231
|
+
}).catch(() => setStatus(root, ui.canvasHint));
|
|
232
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
import { containRect, type BrandStyle, type CompositionState, type LayerKey, type LayerPosition } from './logic';
|
|
3
|
+
import type { PromoteThisWebsiteUI } from './ui';
|
|
4
|
+
import { drawTitle } from './title-view';
|
|
5
|
+
|
|
6
|
+
export interface RenderAssets {
|
|
7
|
+
background: HTMLImageElement | null;
|
|
8
|
+
tool: HTMLImageElement | null;
|
|
9
|
+
brand: HTMLImageElement | null;
|
|
10
|
+
mascot: HTMLImageElement | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RenderInput {
|
|
14
|
+
context: CanvasRenderingContext2D;
|
|
15
|
+
state: CompositionState;
|
|
16
|
+
assets: RenderAssets;
|
|
17
|
+
ui: PromoteThisWebsiteUI;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BrandPalette {
|
|
21
|
+
fill: string;
|
|
22
|
+
text: string;
|
|
23
|
+
accent: string;
|
|
24
|
+
stroke: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const brandPalettes: Record<BrandStyle, BrandPalette> = {
|
|
28
|
+
plain: { fill: 'transparent', text: '#071226', accent: '#c45a2f', stroke: '#071226' },
|
|
29
|
+
plaque: { fill: '#f7ebd7', text: '#071226', accent: '#c45a2f', stroke: '#c45a2f' },
|
|
30
|
+
ticket: { fill: '#172a3c', text: '#fffaf0', accent: '#f0b45b', stroke: '#f7ebd7' },
|
|
31
|
+
stamp: { fill: '#f7ebd7', text: '#071226', accent: '#c45a2f', stroke: '#c45a2f' },
|
|
32
|
+
neon: { fill: '#071226', text: '#fffaf0', accent: '#11d6c4', stroke: '#11d6c4' },
|
|
33
|
+
ribbon: { fill: '#071226', text: '#fffaf0', accent: '#c45a2f', stroke: '#f7ebd7' },
|
|
34
|
+
corner: { fill: '#f7ebd7', text: '#071226', accent: '#c45a2f', stroke: '#071226' },
|
|
35
|
+
pixel: { fill: '#071226', text: '#fffaf0', accent: '#11d6c4', stroke: '#11d6c4' },
|
|
36
|
+
halo: { fill: '#071226', text: '#fffaf0', accent: '#11d6c4', stroke: '#11d6c4' },
|
|
37
|
+
editorial: { fill: '#f7ebd7', text: '#071226', accent: '#c45a2f', stroke: '#071226' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function roundedRect(context: CanvasRenderingContext2D, position: LayerPosition, radius: number): void {
|
|
41
|
+
const r = Math.min(radius, position.width / 2, position.height / 2);
|
|
42
|
+
context.beginPath();
|
|
43
|
+
context.roundRect(position.x, position.y, position.width, position.height, r);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function drawFallbackBackground(context: CanvasRenderingContext2D, width: number, height: number): void {
|
|
47
|
+
const gradient = context.createLinearGradient(0, 0, width, height);
|
|
48
|
+
gradient.addColorStop(0, '#091426');
|
|
49
|
+
gradient.addColorStop(0.52, '#203e55');
|
|
50
|
+
gradient.addColorStop(1, '#d48d53');
|
|
51
|
+
context.fillStyle = gradient;
|
|
52
|
+
context.fillRect(0, 0, width, height);
|
|
53
|
+
context.fillStyle = 'rgba(247, 235, 215, 0.82)';
|
|
54
|
+
for (let index = 0; index < 90; index += 1) {
|
|
55
|
+
const x = (index * 173) % width;
|
|
56
|
+
const y = (index * 97) % height;
|
|
57
|
+
const radius = index % 5 === 0 ? 3 : 1.2;
|
|
58
|
+
context.beginPath();
|
|
59
|
+
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
60
|
+
context.fill();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function drawImage(context: CanvasRenderingContext2D, image: HTMLImageElement, position: LayerPosition, mode: 'contain' | 'cover'): LayerPosition {
|
|
65
|
+
const ratio = mode === 'contain'
|
|
66
|
+
? Math.min(position.width / image.naturalWidth, position.height / image.naturalHeight)
|
|
67
|
+
: Math.max(position.width / image.naturalWidth, position.height / image.naturalHeight);
|
|
68
|
+
const fitted = { x: position.x + (position.width - image.naturalWidth * ratio) / 2, y: position.y + (position.height - image.naturalHeight * ratio) / 2, width: image.naturalWidth * ratio, height: image.naturalHeight * ratio };
|
|
69
|
+
context.drawImage(image, fitted.x, fitted.y, fitted.width, fitted.height);
|
|
70
|
+
return fitted;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface BackgroundInput {
|
|
74
|
+
context: CanvasRenderingContext2D;
|
|
75
|
+
state: CompositionState;
|
|
76
|
+
assets: RenderAssets;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function drawBackground(input: BackgroundInput): void {
|
|
80
|
+
const { context, state, assets } = input;
|
|
81
|
+
if (assets.background) drawImage(context, assets.background, state.positions.background, 'cover');
|
|
82
|
+
else drawFallbackBackground(context, context.canvas.width, context.canvas.height);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function drawTool(context: CanvasRenderingContext2D, state: CompositionState, assets: RenderAssets, ui: PromoteThisWebsiteUI): void {
|
|
86
|
+
const box = state.positions.tool;
|
|
87
|
+
const imageBox = assets.tool ? containRect(assets.tool.naturalWidth, assets.tool.naturalHeight, box) : box;
|
|
88
|
+
context.save();
|
|
89
|
+
context.shadowColor = 'rgba(4, 11, 24, 0.42)';
|
|
90
|
+
context.shadowBlur = 28;
|
|
91
|
+
context.shadowOffsetY = 16;
|
|
92
|
+
context.fillStyle = '#071226';
|
|
93
|
+
roundedRect(context, imageBox, 22);
|
|
94
|
+
context.fill();
|
|
95
|
+
context.shadowColor = 'transparent';
|
|
96
|
+
context.clip();
|
|
97
|
+
if (assets.tool) context.drawImage(assets.tool, imageBox.x, imageBox.y, imageBox.width, imageBox.height);
|
|
98
|
+
else drawToolFallback(context, imageBox, ui);
|
|
99
|
+
context.restore();
|
|
100
|
+
context.save();
|
|
101
|
+
context.strokeStyle = state.colors.frame;
|
|
102
|
+
context.lineWidth = 5;
|
|
103
|
+
roundedRect(context, imageBox, 22);
|
|
104
|
+
context.stroke();
|
|
105
|
+
context.restore();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function drawToolFallback(context: CanvasRenderingContext2D, box: LayerPosition, ui: PromoteThisWebsiteUI): void {
|
|
109
|
+
context.fillStyle = '#0e1a30';
|
|
110
|
+
context.fillRect(box.x, box.y, box.width, box.height);
|
|
111
|
+
context.strokeStyle = 'rgba(247, 235, 215, 0.55)';
|
|
112
|
+
context.lineWidth = 4;
|
|
113
|
+
context.setLineDash([18, 14]);
|
|
114
|
+
roundedRect(context, { x: box.x + 22, y: box.y + 22, width: box.width - 44, height: box.height - 44 }, 18);
|
|
115
|
+
context.stroke();
|
|
116
|
+
context.setLineDash([]);
|
|
117
|
+
context.fillStyle = '#f7ebd7';
|
|
118
|
+
context.font = `700 ${Math.max(22, box.width * 0.028)}px sans-serif`;
|
|
119
|
+
context.textAlign = 'center';
|
|
120
|
+
context.textBaseline = 'middle';
|
|
121
|
+
context.fillText(ui.defaultTool, box.x + box.width / 2, box.y + box.height / 2);
|
|
122
|
+
context.textAlign = 'left';
|
|
123
|
+
context.textBaseline = 'alphabetic';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fitBrandFont(context: CanvasRenderingContext2D, brand: 'jjlmoya.es' | 'GameBob.dev', box: LayerPosition): void {
|
|
127
|
+
let fontSize = Math.max(14, box.height * 0.55);
|
|
128
|
+
const maxWidth = box.width * 0.82;
|
|
129
|
+
context.font = `800 ${fontSize}px sans-serif`;
|
|
130
|
+
const sample = brand === 'jjlmoya.es' ? 'jjlmoya.es' : 'GameBob.dev';
|
|
131
|
+
while (context.measureText(sample).width > maxWidth && fontSize > 10) {
|
|
132
|
+
fontSize *= 0.9;
|
|
133
|
+
context.font = `800 ${fontSize}px sans-serif`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function drawJjlmoyaBrand(context: CanvasRenderingContext2D, box: LayerPosition, textColor: string): void {
|
|
138
|
+
const scale = Math.min(1, box.width * 0.82 / context.measureText('jjlmoya.es').width);
|
|
139
|
+
context.save();
|
|
140
|
+
context.translate(box.x + box.width / 2, box.y + box.height / 2);
|
|
141
|
+
context.scale(scale, 1);
|
|
142
|
+
context.fillStyle = textColor;
|
|
143
|
+
context.textAlign = 'center';
|
|
144
|
+
context.fillText('jjlmoya.es', 0, 0);
|
|
145
|
+
context.restore();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function drawGameBobBrand(context: CanvasRenderingContext2D, box: LayerPosition, textColor: string, accent: string): void {
|
|
149
|
+
const gameWidth = context.measureText('Game').width;
|
|
150
|
+
const bobWidth = context.measureText('Bob').width;
|
|
151
|
+
const devWidth = context.measureText('.dev').width;
|
|
152
|
+
const totalWidth = gameWidth + bobWidth + devWidth;
|
|
153
|
+
const scale = Math.min(1, box.width * 0.82 / totalWidth);
|
|
154
|
+
const start = -totalWidth / 2;
|
|
155
|
+
context.save();
|
|
156
|
+
context.translate(box.x + box.width / 2, box.y + box.height / 2);
|
|
157
|
+
context.scale(scale, 1);
|
|
158
|
+
context.textAlign = 'left';
|
|
159
|
+
context.fillStyle = textColor;
|
|
160
|
+
context.fillText('Game', start, 0);
|
|
161
|
+
context.fillStyle = accent;
|
|
162
|
+
context.fillText('Bob', start + gameWidth, 0);
|
|
163
|
+
context.fillStyle = textColor;
|
|
164
|
+
context.fillText('.dev', start + gameWidth + bobWidth, 0);
|
|
165
|
+
context.restore();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function drawBrandShape(context: CanvasRenderingContext2D, style: BrandStyle, palette: BrandPalette, box: LayerPosition): void {
|
|
169
|
+
const shapes: Record<BrandStyle, () => void> = {
|
|
170
|
+
plain: () => undefined,
|
|
171
|
+
plaque: () => { roundedRect(context, box, box.height * 0.45); context.fill(); context.stroke(); },
|
|
172
|
+
ticket: () => drawBrandTicket(context, box),
|
|
173
|
+
stamp: () => drawBrandStamp(context, box),
|
|
174
|
+
neon: () => drawBrandNeon(context, palette, box),
|
|
175
|
+
ribbon: () => drawBrandRibbon(context, palette, box),
|
|
176
|
+
corner: () => drawBrandCorner(context, palette, box),
|
|
177
|
+
pixel: () => drawBrandPixel(context, palette, box),
|
|
178
|
+
halo: () => drawBrandHalo(context, palette, box),
|
|
179
|
+
editorial: () => drawBrandEditorial(context, palette, box),
|
|
180
|
+
};
|
|
181
|
+
shapes[style]();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function drawBrandTicket(context: CanvasRenderingContext2D, box: LayerPosition): void {
|
|
185
|
+
roundedRect(context, box, 14);
|
|
186
|
+
context.fill();
|
|
187
|
+
context.stroke();
|
|
188
|
+
context.setLineDash([7, 9]);
|
|
189
|
+
context.beginPath();
|
|
190
|
+
context.moveTo(box.x + box.width * 0.76, box.y);
|
|
191
|
+
context.lineTo(box.x + box.width * 0.76, box.y + box.height);
|
|
192
|
+
context.stroke();
|
|
193
|
+
context.setLineDash([]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function drawBrandStamp(context: CanvasRenderingContext2D, box: LayerPosition): void {
|
|
197
|
+
context.save();
|
|
198
|
+
context.translate(box.x + box.width / 2, box.y + box.height / 2);
|
|
199
|
+
context.rotate(-0.04);
|
|
200
|
+
roundedRect(context, { x: -box.width / 2, y: -box.height / 2, width: box.width, height: box.height }, 10);
|
|
201
|
+
context.fill();
|
|
202
|
+
context.stroke();
|
|
203
|
+
context.restore();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function drawBrandNeon(context: CanvasRenderingContext2D, palette: BrandPalette, box: LayerPosition): void {
|
|
207
|
+
context.shadowColor = palette.accent;
|
|
208
|
+
context.shadowBlur = 20;
|
|
209
|
+
roundedRect(context, box, box.height / 2);
|
|
210
|
+
context.stroke();
|
|
211
|
+
context.shadowColor = 'transparent';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function drawBrandRibbon(context: CanvasRenderingContext2D, palette: BrandPalette, box: LayerPosition): void {
|
|
215
|
+
roundedRect(context, box, 12);
|
|
216
|
+
context.fill();
|
|
217
|
+
context.save();
|
|
218
|
+
context.clip();
|
|
219
|
+
context.fillStyle = palette.accent;
|
|
220
|
+
context.fillRect(box.x, box.y + box.height * 0.18, box.width, box.height * 0.64);
|
|
221
|
+
context.restore();
|
|
222
|
+
context.stroke();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function drawBrandCorner(context: CanvasRenderingContext2D, palette: BrandPalette, box: LayerPosition): void {
|
|
226
|
+
roundedRect(context, box, 10);
|
|
227
|
+
context.fillStyle = palette.fill;
|
|
228
|
+
context.fill();
|
|
229
|
+
context.stroke();
|
|
230
|
+
context.fillStyle = palette.accent;
|
|
231
|
+
context.fillRect(box.x, box.y, box.width * 0.22, 6);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function drawBrandPixel(context: CanvasRenderingContext2D, palette: BrandPalette, box: LayerPosition): void {
|
|
235
|
+
roundedRect(context, box, Math.min(14, box.height * 0.24));
|
|
236
|
+
context.fillStyle = palette.fill;
|
|
237
|
+
context.fill();
|
|
238
|
+
context.strokeStyle = palette.stroke;
|
|
239
|
+
context.lineWidth = 4;
|
|
240
|
+
context.setLineDash([7, 5]);
|
|
241
|
+
roundedRect(context, { x: box.x + 5, y: box.y + 5, width: box.width - 10, height: box.height - 10 }, Math.min(10, box.height * 0.18));
|
|
242
|
+
context.stroke();
|
|
243
|
+
context.setLineDash([]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function drawBrandHalo(context: CanvasRenderingContext2D, palette: BrandPalette, box: LayerPosition): void {
|
|
247
|
+
context.shadowColor = palette.accent;
|
|
248
|
+
context.shadowBlur = 24;
|
|
249
|
+
context.globalAlpha = 0.28;
|
|
250
|
+
context.beginPath();
|
|
251
|
+
context.ellipse(box.x + box.width / 2, box.y + box.height / 2, box.width / 2, box.height * 0.72, 0, 0, Math.PI * 2);
|
|
252
|
+
context.fill();
|
|
253
|
+
context.shadowColor = 'transparent';
|
|
254
|
+
context.globalAlpha = 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function drawBrandEditorial(context: CanvasRenderingContext2D, palette: BrandPalette, box: LayerPosition): void {
|
|
258
|
+
roundedRect(context, box, 8);
|
|
259
|
+
context.fill();
|
|
260
|
+
context.stroke();
|
|
261
|
+
context.fillRect(box.x, box.y + box.height * 0.82, box.width, 5);
|
|
262
|
+
context.fillStyle = palette.accent;
|
|
263
|
+
context.fillRect(box.x, box.y + box.height * 0.82, box.width * 0.28, 5);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function narrowBrandBox(position: LayerPosition): LayerPosition {
|
|
267
|
+
const height = Math.min(position.height, position.width * 0.22);
|
|
268
|
+
return { ...position, y: position.y + (position.height - height) / 2, height };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function drawBrand(context: CanvasRenderingContext2D, state: CompositionState, assets: RenderAssets): void {
|
|
272
|
+
const box = narrowBrandBox(state.positions.brand);
|
|
273
|
+
const palette = brandPalettes[state.brandStyle];
|
|
274
|
+
context.save();
|
|
275
|
+
fitBrandFont(context, state.brand, box);
|
|
276
|
+
context.textBaseline = 'middle';
|
|
277
|
+
context.shadowColor = 'rgba(0, 0, 0, 0.35)';
|
|
278
|
+
context.shadowBlur = 8;
|
|
279
|
+
context.fillStyle = palette.fill;
|
|
280
|
+
context.strokeStyle = palette.stroke;
|
|
281
|
+
context.lineWidth = 3;
|
|
282
|
+
drawBrandShape(context, state.brandStyle, palette, box);
|
|
283
|
+
if (assets.brand) drawImage(context, assets.brand, box, 'contain');
|
|
284
|
+
else {
|
|
285
|
+
if (state.brand === 'jjlmoya.es') drawJjlmoyaBrand(context, box, palette.text);
|
|
286
|
+
else drawGameBobBrand(context, box, palette.text, palette.accent);
|
|
287
|
+
}
|
|
288
|
+
context.restore();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function drawMascot(context: CanvasRenderingContext2D, assets: RenderAssets, position: LayerPosition): void {
|
|
292
|
+
if (assets.mascot) drawImage(context, assets.mascot, position, 'contain');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function drawBackgroundLayer(input: RenderInput): void {
|
|
296
|
+
if (input.state.visible.background) drawBackground({ context: input.context, state: input.state, assets: input.assets });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function drawToolLayer(input: RenderInput): void {
|
|
300
|
+
if (input.state.visible.tool) drawTool(input.context, input.state, input.assets, input.ui);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function drawForegroundLayers(input: RenderInput): void {
|
|
304
|
+
const { context, state, assets } = input;
|
|
305
|
+
if (state.visible.title) drawTitle(context, state);
|
|
306
|
+
if (state.visible.mascot) drawMascot(context, assets, state.positions.mascot);
|
|
307
|
+
if (state.visible.brand) drawBrand(context, state, assets);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function renderComposition(input: RenderInput): void {
|
|
311
|
+
const { context, state, assets, ui } = input;
|
|
312
|
+
const width = context.canvas.width;
|
|
313
|
+
const height = context.canvas.height;
|
|
314
|
+
context.clearRect(0, 0, width, height);
|
|
315
|
+
drawBackgroundLayer({ context, state, assets, ui });
|
|
316
|
+
drawToolLayer({ context, state, assets, ui });
|
|
317
|
+
drawForegroundLayers({ context, state, assets, ui });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function layerHitOrder(): LayerKey[] {
|
|
321
|
+
return ['title', 'mascot', 'brand', 'tool', 'background'];
|
|
322
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DeveloperToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { PromoteThisWebsiteUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type PromoteThisWebsiteLocaleContent = ToolLocaleContent<PromoteThisWebsiteUI>;
|
|
5
|
+
|
|
6
|
+
export const promoteThisWebsite: DeveloperToolEntry<PromoteThisWebsiteUI> = {
|
|
7
|
+
id: 'promote-this-website',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:bullhorn-outline',
|
|
10
|
+
fg: 'mdi:image-multiple-outline',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
en: async () => (await import('./i18n/en')).content,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CompositionState, LayerKey } from './logic';
|
|
2
|
+
|
|
3
|
+
export function activeLabel(state: CompositionState, labels: Record<LayerKey, string>): string {
|
|
4
|
+
return `${labels[state.active]}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function visibleLayerCount(state: CompositionState): number {
|
|
8
|
+
return Object.values(state.visible).filter(Boolean).length;
|
|
9
|
+
}
|