@jjlmoya/utils-creative 1.19.0 → 1.21.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.
Files changed (146) hide show
  1. package/package.json +1 -1
  2. package/src/data.ts +2 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +3 -0
  5. package/src/pages/[locale]/[slug].astro +28 -12
  6. package/src/tests/locale_completeness.test.ts +2 -14
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/bead-pattern-generator/bibliography.astro +2 -12
  11. package/src/tool/bead-pattern-generator/bibliography.ts +5 -0
  12. package/src/tool/bead-pattern-generator/entry.ts +0 -1
  13. package/src/tool/bead-pattern-generator/i18n/de.ts +2 -6
  14. package/src/tool/bead-pattern-generator/i18n/en.ts +2 -6
  15. package/src/tool/bead-pattern-generator/i18n/es.ts +2 -6
  16. package/src/tool/bead-pattern-generator/i18n/fr.ts +2 -6
  17. package/src/tool/bead-pattern-generator/i18n/id.ts +2 -6
  18. package/src/tool/bead-pattern-generator/i18n/it.ts +2 -6
  19. package/src/tool/bead-pattern-generator/i18n/ja.ts +2 -6
  20. package/src/tool/bead-pattern-generator/i18n/ko.ts +2 -6
  21. package/src/tool/bead-pattern-generator/i18n/nl.ts +2 -6
  22. package/src/tool/bead-pattern-generator/i18n/pl.ts +2 -6
  23. package/src/tool/bead-pattern-generator/i18n/pt.ts +2 -6
  24. package/src/tool/bead-pattern-generator/i18n/ru.ts +2 -6
  25. package/src/tool/bead-pattern-generator/i18n/sv.ts +2 -6
  26. package/src/tool/bead-pattern-generator/i18n/tr.ts +2 -6
  27. package/src/tool/bead-pattern-generator/i18n/zh.ts +2 -6
  28. package/src/tool/bead-pattern-generator/seo.astro +10 -5
  29. package/src/tool/dice-roller/bibliography.astro +3 -12
  30. package/src/tool/dice-roller/bibliography.ts +5 -0
  31. package/src/tool/dice-roller/entry.ts +0 -1
  32. package/src/tool/dice-roller/i18n/de.ts +2 -6
  33. package/src/tool/dice-roller/i18n/en.ts +2 -6
  34. package/src/tool/dice-roller/i18n/es.ts +2 -6
  35. package/src/tool/dice-roller/i18n/fr.ts +2 -6
  36. package/src/tool/dice-roller/i18n/id.ts +2 -6
  37. package/src/tool/dice-roller/i18n/it.ts +2 -6
  38. package/src/tool/dice-roller/i18n/ja.ts +2 -6
  39. package/src/tool/dice-roller/i18n/ko.ts +2 -6
  40. package/src/tool/dice-roller/i18n/nl.ts +2 -6
  41. package/src/tool/dice-roller/i18n/pl.ts +2 -6
  42. package/src/tool/dice-roller/i18n/pt.ts +2 -6
  43. package/src/tool/dice-roller/i18n/ru.ts +2 -6
  44. package/src/tool/dice-roller/i18n/sv.ts +2 -6
  45. package/src/tool/dice-roller/i18n/tr.ts +2 -6
  46. package/src/tool/dice-roller/i18n/zh.ts +2 -6
  47. package/src/tool/dice-roller/seo.astro +2 -1
  48. package/src/tool/excuse-generator/bibliography.astro +2 -12
  49. package/src/tool/excuse-generator/bibliography.ts +3 -0
  50. package/src/tool/excuse-generator/entry.ts +0 -1
  51. package/src/tool/excuse-generator/i18n/de.ts +2 -4
  52. package/src/tool/excuse-generator/i18n/en.ts +2 -4
  53. package/src/tool/excuse-generator/i18n/es.ts +2 -4
  54. package/src/tool/excuse-generator/i18n/fr.ts +2 -4
  55. package/src/tool/excuse-generator/i18n/id.ts +2 -4
  56. package/src/tool/excuse-generator/i18n/it.ts +2 -4
  57. package/src/tool/excuse-generator/i18n/ja.ts +2 -4
  58. package/src/tool/excuse-generator/i18n/ko.ts +2 -4
  59. package/src/tool/excuse-generator/i18n/nl.ts +2 -4
  60. package/src/tool/excuse-generator/i18n/pl.ts +2 -4
  61. package/src/tool/excuse-generator/i18n/pt.ts +2 -4
  62. package/src/tool/excuse-generator/i18n/ru.ts +2 -4
  63. package/src/tool/excuse-generator/i18n/sv.ts +2 -4
  64. package/src/tool/excuse-generator/i18n/tr.ts +2 -4
  65. package/src/tool/excuse-generator/i18n/zh.ts +2 -4
  66. package/src/tool/excuse-generator/seo.astro +2 -1
  67. package/src/tool/fortune-cookie/bibliography.astro +2 -12
  68. package/src/tool/fortune-cookie/bibliography.ts +3 -0
  69. package/src/tool/fortune-cookie/entry.ts +0 -1
  70. package/src/tool/fortune-cookie/i18n/de.ts +2 -5
  71. package/src/tool/fortune-cookie/i18n/en.ts +2 -5
  72. package/src/tool/fortune-cookie/i18n/es.ts +2 -4
  73. package/src/tool/fortune-cookie/i18n/fr.ts +2 -5
  74. package/src/tool/fortune-cookie/i18n/id.ts +2 -5
  75. package/src/tool/fortune-cookie/i18n/it.ts +2 -5
  76. package/src/tool/fortune-cookie/i18n/ja.ts +2 -5
  77. package/src/tool/fortune-cookie/i18n/ko.ts +2 -5
  78. package/src/tool/fortune-cookie/i18n/nl.ts +2 -5
  79. package/src/tool/fortune-cookie/i18n/pl.ts +2 -5
  80. package/src/tool/fortune-cookie/i18n/pt.ts +2 -5
  81. package/src/tool/fortune-cookie/i18n/ru.ts +2 -5
  82. package/src/tool/fortune-cookie/i18n/sv.ts +2 -5
  83. package/src/tool/fortune-cookie/i18n/tr.ts +2 -5
  84. package/src/tool/fortune-cookie/i18n/zh.ts +2 -5
  85. package/src/tool/fortune-cookie/seo.astro +2 -1
  86. package/src/tool/glitch-image-distorter/bibliography.astro +8 -0
  87. package/src/tool/glitch-image-distorter/bibliography.ts +4 -0
  88. package/src/tool/glitch-image-distorter/component.astro +393 -0
  89. package/src/tool/glitch-image-distorter/entry.ts +48 -0
  90. package/src/tool/glitch-image-distorter/glitch-image-distorter.css +233 -0
  91. package/src/tool/glitch-image-distorter/i18n/de.ts +131 -0
  92. package/src/tool/glitch-image-distorter/i18n/en.ts +131 -0
  93. package/src/tool/glitch-image-distorter/i18n/es.ts +131 -0
  94. package/src/tool/glitch-image-distorter/i18n/fr.ts +131 -0
  95. package/src/tool/glitch-image-distorter/i18n/id.ts +131 -0
  96. package/src/tool/glitch-image-distorter/i18n/it.ts +131 -0
  97. package/src/tool/glitch-image-distorter/i18n/ja.ts +131 -0
  98. package/src/tool/glitch-image-distorter/i18n/ko.ts +131 -0
  99. package/src/tool/glitch-image-distorter/i18n/nl.ts +131 -0
  100. package/src/tool/glitch-image-distorter/i18n/pl.ts +131 -0
  101. package/src/tool/glitch-image-distorter/i18n/pt.ts +131 -0
  102. package/src/tool/glitch-image-distorter/i18n/ru.ts +131 -0
  103. package/src/tool/glitch-image-distorter/i18n/sv.ts +131 -0
  104. package/src/tool/glitch-image-distorter/i18n/tr.ts +131 -0
  105. package/src/tool/glitch-image-distorter/i18n/zh.ts +131 -0
  106. package/src/tool/glitch-image-distorter/index.ts +9 -0
  107. package/src/tool/glitch-image-distorter/seo.astro +15 -0
  108. package/src/tool/synesthesia-painter/bibliography.astro +3 -12
  109. package/src/tool/synesthesia-painter/bibliography.ts +5 -0
  110. package/src/tool/synesthesia-painter/entry.ts +0 -1
  111. package/src/tool/synesthesia-painter/i18n/de.ts +2 -6
  112. package/src/tool/synesthesia-painter/i18n/en.ts +2 -6
  113. package/src/tool/synesthesia-painter/i18n/es.ts +2 -6
  114. package/src/tool/synesthesia-painter/i18n/fr.ts +2 -6
  115. package/src/tool/synesthesia-painter/i18n/id.ts +2 -6
  116. package/src/tool/synesthesia-painter/i18n/it.ts +2 -6
  117. package/src/tool/synesthesia-painter/i18n/ja.ts +2 -6
  118. package/src/tool/synesthesia-painter/i18n/ko.ts +2 -6
  119. package/src/tool/synesthesia-painter/i18n/nl.ts +2 -6
  120. package/src/tool/synesthesia-painter/i18n/pl.ts +2 -6
  121. package/src/tool/synesthesia-painter/i18n/pt.ts +2 -6
  122. package/src/tool/synesthesia-painter/i18n/ru.ts +2 -6
  123. package/src/tool/synesthesia-painter/i18n/sv.ts +2 -6
  124. package/src/tool/synesthesia-painter/i18n/tr.ts +2 -6
  125. package/src/tool/synesthesia-painter/i18n/zh.ts +2 -6
  126. package/src/tool/synesthesia-painter/seo.astro +2 -1
  127. package/src/tool/zalgo-generator/bibliography.astro +2 -12
  128. package/src/tool/zalgo-generator/bibliography.ts +5 -0
  129. package/src/tool/zalgo-generator/entry.ts +0 -1
  130. package/src/tool/zalgo-generator/i18n/de.ts +2 -6
  131. package/src/tool/zalgo-generator/i18n/en.ts +2 -6
  132. package/src/tool/zalgo-generator/i18n/es.ts +2 -6
  133. package/src/tool/zalgo-generator/i18n/fr.ts +2 -6
  134. package/src/tool/zalgo-generator/i18n/id.ts +2 -6
  135. package/src/tool/zalgo-generator/i18n/it.ts +2 -6
  136. package/src/tool/zalgo-generator/i18n/ja.ts +2 -6
  137. package/src/tool/zalgo-generator/i18n/ko.ts +2 -6
  138. package/src/tool/zalgo-generator/i18n/nl.ts +2 -6
  139. package/src/tool/zalgo-generator/i18n/pl.ts +2 -6
  140. package/src/tool/zalgo-generator/i18n/pt.ts +2 -6
  141. package/src/tool/zalgo-generator/i18n/ru.ts +2 -6
  142. package/src/tool/zalgo-generator/i18n/sv.ts +2 -6
  143. package/src/tool/zalgo-generator/i18n/tr.ts +2 -6
  144. package/src/tool/zalgo-generator/i18n/zh.ts +2 -6
  145. package/src/tool/zalgo-generator/seo.astro +2 -1
  146. package/src/tools.ts +2 -0
@@ -1,5 +1,6 @@
1
1
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import type { FortuneCookieLocaleContent } from '../index';
3
+ import { bibliography } from '../bibliography';
3
4
 
4
5
  const slug = 'ciasteczko-z-wrozba-online';
5
6
  const title = 'Ciasteczko z Wróżbą';
@@ -59,7 +60,6 @@ export const content: FortuneCookieLocaleContent = {
59
60
  title,
60
61
  description,
61
62
  faqTitle: 'Najczęściej Zadawane Pytania',
62
- bibliographyTitle: 'Bibliografia Przeznaczenia',
63
63
  ui: {
64
64
  title: 'Ciasteczko z Wróżbą Online',
65
65
  description: 'Twoja codzienna cyfrowa wyrocznia.',
@@ -97,7 +97,6 @@ export const content: FortuneCookieLocaleContent = {
97
97
  "Nieoczekiwana podróż przyniesie Ci wielkie radości."
98
98
  ]),
99
99
  faqTitle: 'FAQ',
100
- bibliographyTitle: 'Źródła'
101
100
  },
102
101
  seo: [
103
102
  { type: 'title', text: 'Tajemnica ciasteczka z wróżbą', level: 2 },
@@ -119,9 +118,7 @@ export const content: FortuneCookieLocaleContent = {
119
118
  ], columns: 4 },
120
119
  ],
121
120
  faq,
122
- bibliography: [
123
- { name: 'Historia ciasteczka z wróżbą', url: 'https://pl.wikipedia.org/wiki/Ciasteczko_z_wróżbą' }
124
- ],
121
+ bibliography,
125
122
  howTo,
126
123
  schemas: [faqSchema as any, howToSchema as any, appSchema],
127
124
  };
@@ -1,5 +1,6 @@
1
1
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import type { FortuneCookieLocaleContent } from '../index';
3
+ import { bibliography } from '../bibliography';
3
4
 
4
5
  const slug = 'biscoito-da-sorte-online';
5
6
  const title = 'Biscoito da Sorte';
@@ -59,7 +60,6 @@ export const content: FortuneCookieLocaleContent = {
59
60
  title,
60
61
  description,
61
62
  faqTitle: 'Perguntas Frequentes',
62
- bibliographyTitle: 'Bibliografia do Destino',
63
63
  ui: {
64
64
  title: 'Biscoito da Sorte Online',
65
65
  description: 'O seu oráculo digital diário.',
@@ -97,7 +97,6 @@ export const content: FortuneCookieLocaleContent = {
97
97
  "Uma viagem inesperada trar-lhe-á grandes alegrias."
98
98
  ]),
99
99
  faqTitle: 'FAQ',
100
- bibliographyTitle: 'Referências'
101
100
  },
102
101
  seo: [
103
102
  { type: 'title', text: 'O Mistério do Biscoito da Sorte', level: 2 },
@@ -119,9 +118,7 @@ export const content: FortuneCookieLocaleContent = {
119
118
  ], columns: 4 },
120
119
  ],
121
120
  faq,
122
- bibliography: [
123
- { name: 'História do Biscoito da Sorte', url: 'https://pt.wikipedia.org/wiki/Biscoito_da_sorte' }
124
- ],
121
+ bibliography,
125
122
  howTo,
126
123
  schemas: [faqSchema as any, howToSchema as any, appSchema],
127
124
  };
@@ -1,5 +1,6 @@
1
1
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import type { FortuneCookieLocaleContent } from '../index';
3
+ import { bibliography } from '../bibliography';
3
4
 
4
5
  const slug = 'pechenie-s-predskazaniem-online';
5
6
  const title = 'Печенье с предсказанием';
@@ -59,7 +60,6 @@ export const content: FortuneCookieLocaleContent = {
59
60
  title,
60
61
  description,
61
62
  faqTitle: 'Часто задаваемые вопросы',
62
- bibliographyTitle: 'Библиография судьбы',
63
63
  ui: {
64
64
  title: 'Печенье с предсказанием онлайн',
65
65
  description: 'Ваш ежедневный цифровой оракул.',
@@ -97,7 +97,6 @@ export const content: FortuneCookieLocaleContent = {
97
97
  "Неожиданное путешествие принесет вам большую радость."
98
98
  ]),
99
99
  faqTitle: 'FAQ',
100
- bibliographyTitle: 'Ссылки'
101
100
  },
102
101
  seo: [
103
102
  { type: 'title', text: 'Тайна печенья с предсказанием', level: 2 },
@@ -119,9 +118,7 @@ export const content: FortuneCookieLocaleContent = {
119
118
  ], columns: 4 },
120
119
  ],
121
120
  faq,
122
- bibliography: [
123
- { name: 'История печенья с предсказанием (англ.)', url: 'https://en.wikipedia.org/wiki/Fortune_cookie' }
124
- ],
121
+ bibliography,
125
122
  howTo,
126
123
  schemas: [faqSchema as any, howToSchema as any, appSchema],
127
124
  };
@@ -1,5 +1,6 @@
1
1
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import type { FortuneCookieLocaleContent } from '../index';
3
+ import { bibliography } from '../bibliography';
3
4
 
4
5
  const slug = 'lyckokaka-online';
5
6
  const title = 'Lyckokaka';
@@ -59,7 +60,6 @@ export const content: FortuneCookieLocaleContent = {
59
60
  title,
60
61
  description,
61
62
  faqTitle: 'Vanliga frågor',
62
- bibliographyTitle: 'Ödesbibliografi',
63
63
  ui: {
64
64
  title: 'Online Lyckokaka',
65
65
  description: 'Ditt dagliga digitala orakel.',
@@ -97,7 +97,6 @@ export const content: FortuneCookieLocaleContent = {
97
97
  "En oväntad resa kommer att ge dig stor glädje."
98
98
  ]),
99
99
  faqTitle: 'FAQ',
100
- bibliographyTitle: 'Referenser'
101
100
  },
102
101
  seo: [
103
102
  { type: 'title', text: 'Lyckokakans mysterium', level: 2 },
@@ -119,9 +118,7 @@ export const content: FortuneCookieLocaleContent = {
119
118
  ], columns: 4 },
120
119
  ],
121
120
  faq,
122
- bibliography: [
123
- { name: 'Lyckokakans historia (engelska)', url: 'https://en.wikipedia.org/wiki/Fortune_cookie' }
124
- ],
121
+ bibliography,
125
122
  howTo,
126
123
  schemas: [faqSchema as any, howToSchema as any, appSchema],
127
124
  };
@@ -1,5 +1,6 @@
1
1
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import type { FortuneCookieLocaleContent } from '../index';
3
+ import { bibliography } from '../bibliography';
3
4
 
4
5
  const slug = 'sans-kurabiyesi-online';
5
6
  const title = 'Şans Kurabiyesi';
@@ -59,7 +60,6 @@ export const content: FortuneCookieLocaleContent = {
59
60
  title,
60
61
  description,
61
62
  faqTitle: 'Sıkça Sorulan Sorular',
62
- bibliographyTitle: 'Kader Kaynakçası',
63
63
  ui: {
64
64
  title: 'Online Şans Kurabiyesi',
65
65
  description: 'Günlük dijital falınız.',
@@ -97,7 +97,6 @@ export const content: FortuneCookieLocaleContent = {
97
97
  "Beklenmedik bir yolculuk size büyük neşeler getirecek."
98
98
  ]),
99
99
  faqTitle: 'FAQ',
100
- bibliographyTitle: 'Referanslar'
101
100
  },
102
101
  seo: [
103
102
  { type: 'title', text: 'Şans Kurabiyesinin Gizemi', level: 2 },
@@ -119,9 +118,7 @@ export const content: FortuneCookieLocaleContent = {
119
118
  ], columns: 4 },
120
119
  ],
121
120
  faq,
122
- bibliography: [
123
- { name: 'Şans Kurabiyesi Tarihi (İngilizce)', url: 'https://en.wikipedia.org/wiki/Fortune_cookie' }
124
- ],
121
+ bibliography,
125
122
  howTo,
126
123
  schemas: [faqSchema as any, howToSchema as any, appSchema],
127
124
  };
@@ -1,5 +1,6 @@
1
1
  import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
2
  import type { FortuneCookieLocaleContent } from '../index';
3
+ import { bibliography } from '../bibliography';
3
4
 
4
5
  const slug = 'fortune-cookie';
5
6
  const title = '幸运饼干';
@@ -59,7 +60,6 @@ export const content: FortuneCookieLocaleContent = {
59
60
  title,
60
61
  description,
61
62
  faqTitle: '常见问题解答',
62
- bibliographyTitle: '命运文献',
63
63
  ui: {
64
64
  title: '在线幸运饼干',
65
65
  description: '您的每日数字神谕。',
@@ -97,7 +97,6 @@ export const content: FortuneCookieLocaleContent = {
97
97
  "一次意外的旅行会给您带来巨大的喜悦。"
98
98
  ]),
99
99
  faqTitle: 'FAQ',
100
- bibliographyTitle: '参考资料'
101
100
  },
102
101
  seo: [
103
102
  { type: 'title', text: '幸运饼干之谜', level: 2 },
@@ -119,9 +118,7 @@ export const content: FortuneCookieLocaleContent = {
119
118
  ], columns: 4 },
120
119
  ],
121
120
  faq,
122
- bibliography: [
123
- { name: '幸运饼干的历史(英文)', url: 'https://en.wikipedia.org/wiki/Fortune_cookie' }
124
- ],
121
+ bibliography,
125
122
  howTo,
126
123
  schemas: [faqSchema as any, howToSchema as any, appSchema],
127
124
  };
@@ -9,6 +9,7 @@ interface Props {
9
9
 
10
10
  const { locale = 'es' } = Astro.props;
11
11
  const content = await fortuneCookie.i18n[locale]?.();
12
+ if (!content) return null;
12
13
  ---
13
14
 
14
- {content && <SEORenderer content={{ locale, sections: content.seo }} />}
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,8 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+
5
+ if (!bibliography || bibliography.length === 0) return null;
6
+ ---
7
+
8
+ <SharedBibliography links={bibliography} />
@@ -0,0 +1,4 @@
1
+ export const bibliography = [
2
+ { name: 'Pixel Sorting by Jeff Thompson', url: 'https://github.com/jeffThompson/PixelSorting' },
3
+ { name: 'Digital Glitch Art - Creative Computing', url: 'https://www.adobe.com/uk/express/learn/blog/what-is-glitch-art' },
4
+ ];
@@ -0,0 +1,393 @@
1
+ ---
2
+ import type { GlitchImageDistorterUI } from './index';
3
+
4
+ interface Props {
5
+ ui: GlitchImageDistorterUI;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div id="gid-root" class="glitch-image-distorter" data-ui={JSON.stringify(ui)}>
12
+ <div class="glitch-image-distorter-card">
13
+ <div class="glitch-image-distorter-canvas-wrapper" id="gid-canvas-wrapper">
14
+ <canvas id="gid-canvas" class="glitch-image-distorter-canvas"></canvas>
15
+ <div class="glitch-image-distorter-drop-zone" id="gid-drop-zone">
16
+ <svg class="glitch-image-distorter-drop-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
17
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
18
+ </svg>
19
+ <span class="glitch-image-distorter-drop-text">{ui.dragDropText}</span>
20
+ <span class="glitch-image-distorter-paste-hint">{ui.pasteHint}</span>
21
+ </div>
22
+ <button class="glitch-image-distorter-example-btn" id="gid-example-btn">{ui.exampleImageBtn}</button>
23
+ <input type="file" id="gid-file-input" accept="image/*" hidden />
24
+ </div>
25
+
26
+ <div class="glitch-image-distorter-controls" id="gid-controls">
27
+ <div class="glitch-image-distorter-control">
28
+ <label class="glitch-image-distorter-label">{ui.corruptionLabel}</label>
29
+ <input type="range" id="gid-corruption" class="glitch-image-distorter-slider" min="0" max="100" value="0" />
30
+ <span style="font-size: 0.75rem; color: var(--gid-text-dim);" id="gid-corruption-val">0</span>
31
+ </div>
32
+
33
+ <div class="glitch-image-distorter-control">
34
+ <label class="glitch-image-distorter-label">{ui.interferenceLabel}</label>
35
+ <input type="range" id="gid-interference" class="glitch-image-distorter-slider" min="0" max="100" value="0" />
36
+ <span style="font-size: 0.75rem; color: var(--gid-text-dim);" id="gid-interference-val">0</span>
37
+ </div>
38
+
39
+ <div class="glitch-image-distorter-control">
40
+ <label class="glitch-image-distorter-label">{ui.tremorLabel}</label>
41
+ <input type="range" id="gid-tremor" class="glitch-image-distorter-slider" min="0" max="100" value="0" />
42
+ <span style="font-size: 0.75rem; color: var(--gid-text-dim);" id="gid-tremor-val">0</span>
43
+ </div>
44
+
45
+ <div class="glitch-image-distorter-control">
46
+ <label class="glitch-image-distorter-label">{ui.pixelSortLabel}</label>
47
+ <input type="range" id="gid-pixel-sort" class="glitch-image-distorter-slider" min="0" max="100" value="0" />
48
+ <span style="font-size: 0.75rem; color: var(--gid-text-dim);" id="gid-pixel-sort-val">0</span>
49
+ </div>
50
+
51
+ <div class="glitch-image-distorter-control">
52
+ <label class="glitch-image-distorter-label">{ui.pixelSortDirectionLabel}</label>
53
+ <select id="gid-sort-direction" class="glitch-image-distorter-select">
54
+ <option value="none">None</option>
55
+ <option value="up">Up</option>
56
+ <option value="down">Down</option>
57
+ <option value="left">Left</option>
58
+ <option value="right">Right</option>
59
+ </select>
60
+ </div>
61
+
62
+ <div class="glitch-image-distorter-control">
63
+ <label class="glitch-image-distorter-label">{ui.noiseLabel}</label>
64
+ <input type="range" id="gid-noise" class="glitch-image-distorter-slider" min="0" max="100" value="0" />
65
+ <span style="font-size: 0.75rem; color: var(--gid-text-dim);" id="gid-noise-val">0</span>
66
+ </div>
67
+
68
+ <div class="glitch-image-distorter-control">
69
+ <label class="glitch-image-distorter-checkbox">
70
+ <input type="checkbox" id="gid-animate" />
71
+ <span>{ui.animateBtn}</span>
72
+ </label>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="glitch-image-distorter-buttons">
77
+ <button class="glitch-image-distorter-btn" id="gid-chaos-btn">{ui.chaosBtn}</button>
78
+ <button class="glitch-image-distorter-btn" id="gid-undo-btn">{ui.undoBtn}</button>
79
+ <button class="glitch-image-distorter-btn" id="gid-redo-btn">{ui.redoBtn}</button>
80
+ <button class="glitch-image-distorter-btn primary" id="gid-download-png-btn">{ui.downloadPngBtn}</button>
81
+ <button class="glitch-image-distorter-btn primary" id="gid-download-gif-btn">{ui.downloadGifBtn}</button>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <script>
87
+ interface State {
88
+ corruption: number;
89
+ interference: number;
90
+ tremor: number;
91
+ pixelSort: number;
92
+ sortDirection: string;
93
+ noise: number;
94
+ }
95
+
96
+ class GlitchDistorter {
97
+ canvas: HTMLCanvasElement;
98
+ ctx: CanvasRenderingContext2D | null;
99
+ sourceImage: HTMLImageElement | null = null;
100
+ state: State;
101
+ history: State[] = [];
102
+ historyIdx = -1;
103
+
104
+ constructor() {
105
+ this.canvas = document.getElementById('gid-canvas') as HTMLCanvasElement;
106
+ this.ctx = this.canvas.getContext('2d');
107
+ this.state = { corruption: 0, interference: 0, tremor: 0, pixelSort: 0, sortDirection: 'none', noise: 0 };
108
+ }
109
+
110
+ loadExampleImage() {
111
+ const svg = `<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:rgb(255,0,0)"/><stop offset="50%" style="stop-color:rgb(0,255,0)"/><stop offset="100%" style="stop-color:rgb(0,0,255)"/></linearGradient></defs><rect width="400" height="300" fill="url(#g)"/><circle cx="100" cy="100" r="50" fill="rgba(255,255,255,0.5)"/><text x="200" y="150" font-size="40" fill="white" text-anchor="middle" font-weight="bold">GLITCH</text></svg>`;
112
+ const blob = new Blob([svg], { type: 'image/svg+xml' });
113
+ const img = new Image();
114
+ img.onload = () => {
115
+ this.sourceImage = img;
116
+ this.canvas.width = img.width;
117
+ this.canvas.height = img.height;
118
+ this.applyDefaultGlitch();
119
+ this.updateDropZone();
120
+ URL.revokeObjectURL(img.src);
121
+ };
122
+ img.src = URL.createObjectURL(blob);
123
+ }
124
+
125
+ loadImage(file: File) {
126
+ const reader = new FileReader();
127
+ reader.onload = (e) => {
128
+ const img = new Image();
129
+ img.onload = () => {
130
+ this.sourceImage = img;
131
+ this.canvas.width = img.width;
132
+ this.canvas.height = img.height;
133
+ this.applyDefaultGlitch();
134
+ this.updateDropZone();
135
+ };
136
+ img.src = e.target?.result as string;
137
+ };
138
+ reader.readAsDataURL(file);
139
+ }
140
+
141
+ applyDefaultGlitch() {
142
+ this.state = { corruption: 40, interference: 50, tremor: 30, pixelSort: 35, sortDirection: 'down', noise: 25 };
143
+ this.updateSliders();
144
+ this.saveState();
145
+ this.render();
146
+ }
147
+
148
+ updateDropZone() {
149
+ const zone = document.getElementById('gid-drop-zone') as HTMLElement;
150
+ zone.classList.toggle('hidden', !!this.sourceImage);
151
+ }
152
+
153
+ render() {
154
+ if (!this.ctx || !this.sourceImage) return;
155
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
156
+ this.ctx.drawImage(this.sourceImage, 0, 0);
157
+ const img = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
158
+ if (this.state.corruption > 0) this.applyRGBSplit(img.data, this.state.corruption);
159
+ if (this.state.interference > 0) this.applyScanlines(img.data, this.state.interference, this.state.tremor);
160
+ if (this.state.noise > 0) this.applyNoise(img.data, this.state.noise);
161
+ this.ctx.putImageData(img, 0, 0);
162
+ if (this.state.pixelSort > 0 && this.state.sortDirection !== 'none') {
163
+ this.applyPixelSort(this.state.pixelSort, this.state.sortDirection);
164
+ }
165
+ }
166
+
167
+ applyRGBSplit(data: Uint8ClampedArray, intensity: number) {
168
+ const offset = Math.floor((intensity / 100) * 20);
169
+ for (let i = 0; i < data.length; i += 4) {
170
+ if (Math.random() < intensity / 100) {
171
+ const tmp = data[i] ?? 0;
172
+ data[i] = data[i + 2] ?? 0;
173
+ data[i + 2] = tmp;
174
+ if (offset > 0 && i + offset * 4 + 1 < data.length) {
175
+ data[i + 1] = data[i + offset * 4 + 1] ?? 0;
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ applyScanlines(data: Uint8ClampedArray, freq: number, tremor: number) {
182
+ const spacing = Math.max(1, Math.floor((100 - freq) / 10));
183
+ const w = this.canvas.width;
184
+ for (let y = 0; y < this.canvas.height; y += spacing) {
185
+ const off = Math.floor(y * w * 4);
186
+ for (let x = 0; x < w; x++) {
187
+ const idx = off + x * 4;
188
+ if (idx + 2 < data.length) {
189
+ const tint = 0.8 + Math.random() * 0.2 * (tremor / 100);
190
+ data[idx] = Math.floor((data[idx] ?? 0) * tint);
191
+ data[idx + 1] = Math.floor((data[idx + 1] ?? 0) * tint);
192
+ data[idx + 2] = Math.floor((data[idx + 2] ?? 0) * tint);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ applyNoise(data: Uint8ClampedArray, intensity: number) {
199
+ for (let i = 0; i < data.length; i += 4) {
200
+ if (Math.random() < intensity / 100 && i + 3 < data.length) {
201
+ const n = Math.random() * 255;
202
+ data[i] = Math.floor((data[i] ?? 0) * 0.7 + n * 0.3);
203
+ data[i + 1] = Math.floor((data[i + 1] ?? 0) * 0.7 + n * 0.3);
204
+ data[i + 2] = Math.floor((data[i + 2] ?? 0) * 0.7 + n * 0.3);
205
+ data[i + 3] = Math.floor((intensity / 100) * 255);
206
+ }
207
+ }
208
+ }
209
+
210
+ applyPixelSort(intensity: number, dir: string) {
211
+ if (!this.ctx) return;
212
+ const cd = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
213
+ const cfg = { th: (intensity / 100) * 255, sh: Math.floor((intensity / 100) * 30), w: this.canvas.width, h: this.canvas.height, dir };
214
+ const v = dir === 'up' || dir === 'down';
215
+ const max = v ? cfg.w : cfg.h;
216
+ for (let i = 0; i < max; i++) {
217
+ this.sortLine(cd.data, i, v, cfg);
218
+ }
219
+ this.ctx.putImageData(cd, 0, 0);
220
+ }
221
+
222
+ sortLine(d: Uint8ClampedArray, ln: number, v: boolean, cfg: { th: number; sh: number; w: number; h: number; dir: string }) {
223
+ const idx: number[] = [];
224
+ const max = v ? cfg.h : cfg.w;
225
+ for (let j = 0; j < max; j++) {
226
+ const p = (v ? j * cfg.w + ln : ln * cfg.w + j) * 4;
227
+ if (p + 2 < d.length && this.isBright(d, p, cfg.th)) idx.push(p);
228
+ }
229
+ if (cfg.dir === 'down' || cfg.dir === 'left') idx.reverse();
230
+ const o = this.getOffset(v, cfg.sh, cfg.w, cfg.dir);
231
+ this.movePixels(d, idx, o);
232
+ }
233
+
234
+ getOffset(v: boolean, sh: number, w: number, dir: string): number {
235
+ if (v) return sh * w * 4;
236
+ return dir === 'right' ? sh * 4 : -sh * 4;
237
+ }
238
+
239
+ isBright(d: Uint8ClampedArray, p: number, th: number): boolean {
240
+ return ((d[p] ?? 0) + (d[p + 1] ?? 0) + (d[p + 2] ?? 0)) / 3 > th;
241
+ }
242
+
243
+ movePixels(d: Uint8ClampedArray, idx: number[], off: number) {
244
+ for (const i of idx) {
245
+ const t = i + off;
246
+ if (t >= 0 && t + 3 < d.length && i + 3 < d.length) {
247
+ for (let j = 0; j < 4; j++) d[t + j] = d[i + j] ?? 0;
248
+ }
249
+ }
250
+ }
251
+
252
+ saveState() {
253
+ this.history = this.history.slice(0, this.historyIdx + 1);
254
+ this.history.push({ ...this.state });
255
+ this.historyIdx++;
256
+ if (this.history.length > 20) {
257
+ this.history.shift();
258
+ this.historyIdx--;
259
+ }
260
+ }
261
+
262
+ undo() {
263
+ if (this.historyIdx > 0) {
264
+ this.historyIdx--;
265
+ this.state = { ...this.history[this.historyIdx] };
266
+ this.updateSliders();
267
+ this.render();
268
+ }
269
+ }
270
+
271
+ redo() {
272
+ if (this.historyIdx < this.history.length - 1) {
273
+ this.historyIdx++;
274
+ this.state = { ...this.history[this.historyIdx] };
275
+ this.updateSliders();
276
+ this.render();
277
+ }
278
+ }
279
+
280
+ updateSliders() {
281
+ (document.getElementById('gid-corruption') as HTMLInputElement).value = String(this.state.corruption);
282
+ (document.getElementById('gid-interference') as HTMLInputElement).value = String(this.state.interference);
283
+ (document.getElementById('gid-tremor') as HTMLInputElement).value = String(this.state.tremor);
284
+ (document.getElementById('gid-pixel-sort') as HTMLInputElement).value = String(this.state.pixelSort);
285
+ (document.getElementById('gid-sort-direction') as HTMLSelectElement).value = this.state.sortDirection;
286
+ (document.getElementById('gid-noise') as HTMLInputElement).value = String(this.state.noise);
287
+ this.updateValueDisplays();
288
+ }
289
+
290
+ updateValueDisplays() {
291
+ (document.getElementById('gid-corruption-val') as HTMLElement).textContent = String(this.state.corruption);
292
+ (document.getElementById('gid-interference-val') as HTMLElement).textContent = String(this.state.interference);
293
+ (document.getElementById('gid-tremor-val') as HTMLElement).textContent = String(this.state.tremor);
294
+ (document.getElementById('gid-pixel-sort-val') as HTMLElement).textContent = String(this.state.pixelSort);
295
+ (document.getElementById('gid-noise-val') as HTMLElement).textContent = String(this.state.noise);
296
+ }
297
+
298
+ randomizeSettings() {
299
+ const dirs = ['none', 'up', 'down', 'left', 'right'];
300
+ this.state = {
301
+ corruption: Math.floor(Math.random() * 100),
302
+ interference: Math.floor(Math.random() * 100),
303
+ tremor: Math.floor(Math.random() * 100),
304
+ pixelSort: Math.floor(Math.random() * 100),
305
+ sortDirection: dirs[Math.floor(Math.random() * dirs.length)] ?? 'none',
306
+ noise: Math.floor(Math.random() * 100),
307
+ };
308
+ this.updateSliders();
309
+ this.saveState();
310
+ this.render();
311
+ }
312
+
313
+ downloadPNG() {
314
+ const link = document.createElement('a');
315
+ link.href = this.canvas.toDataURL('image/png');
316
+ link.download = 'glitch-art.png';
317
+ link.click();
318
+ }
319
+
320
+ downloadGIF() {
321
+ alert('GIF export coming soon. Use PNG export or screen recording.');
322
+ }
323
+
324
+ setupInputs() {
325
+ const w = document.getElementById('gid-canvas-wrapper');
326
+ const fi = document.getElementById('gid-file-input') as HTMLInputElement;
327
+ const eb = document.getElementById('gid-example-btn') as HTMLButtonElement;
328
+ w?.addEventListener('click', () => fi.click());
329
+ w?.addEventListener('dragover', (e) => e.preventDefault());
330
+ w?.addEventListener('drop', (e) => {
331
+ e.preventDefault();
332
+ const f = (e as DragEvent).dataTransfer?.files?.[0];
333
+ if (f) this.loadImage(f);
334
+ });
335
+ fi.addEventListener('change', (e) => {
336
+ const f = (e.target as HTMLInputElement).files?.[0];
337
+ if (f) this.loadImage(f);
338
+ });
339
+ eb.addEventListener('click', (e) => {
340
+ e.stopPropagation();
341
+ this.loadExampleImage();
342
+ });
343
+ document.addEventListener('paste', (e) => {
344
+ for (let i = 0; i < (e.clipboardData?.items.length ?? 0); i++) {
345
+ const item = e.clipboardData?.items[i];
346
+ if (item?.type.includes('image')) {
347
+ const f = item.getAsFile();
348
+ if (f) this.loadImage(f);
349
+ }
350
+ }
351
+ });
352
+ }
353
+
354
+ setupSliders() {
355
+ const sliders: { id: string; key: keyof State }[] = [
356
+ { id: 'gid-corruption', key: 'corruption' },
357
+ { id: 'gid-interference', key: 'interference' },
358
+ { id: 'gid-tremor', key: 'tremor' },
359
+ { id: 'gid-pixel-sort', key: 'pixelSort' },
360
+ { id: 'gid-noise', key: 'noise' },
361
+ ];
362
+ sliders.forEach(({ id, key }) => {
363
+ document.getElementById(id)?.addEventListener('input', (e) => {
364
+ (this.state[key] as number) = Number((e.target as HTMLInputElement).value);
365
+ this.updateValueDisplays();
366
+ this.render();
367
+ });
368
+ });
369
+ document.getElementById('gid-sort-direction')?.addEventListener('change', (e) => {
370
+ this.state.sortDirection = (e.target as HTMLSelectElement).value;
371
+ this.render();
372
+ });
373
+ }
374
+
375
+ setupButtons() {
376
+ document.getElementById('gid-chaos-btn')?.addEventListener('click', () => this.randomizeSettings());
377
+ document.getElementById('gid-undo-btn')?.addEventListener('click', () => this.undo());
378
+ document.getElementById('gid-redo-btn')?.addEventListener('click', () => this.redo());
379
+ document.getElementById('gid-download-png-btn')?.addEventListener('click', () => this.downloadPNG());
380
+ document.getElementById('gid-download-gif-btn')?.addEventListener('click', () => this.downloadGIF());
381
+ }
382
+
383
+ init() {
384
+ this.setupInputs();
385
+ this.setupSliders();
386
+ this.setupButtons();
387
+ }
388
+ }
389
+
390
+ const d = new GlitchDistorter();
391
+ d.init();
392
+ document.addEventListener('astro:page-load', () => new GlitchDistorter().init());
393
+ </script>
@@ -0,0 +1,48 @@
1
+ import type { CreativeToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export interface GlitchImageDistorterUI {
4
+ [key: string]: string;
5
+ title: string;
6
+ description: string;
7
+ dragDropText: string;
8
+ pasteHint: string;
9
+ exampleImageBtn: string;
10
+ corruptionLabel: string;
11
+ interferenceLabel: string;
12
+ tremorLabel: string;
13
+ pixelSortLabel: string;
14
+ pixelSortDirectionLabel: string;
15
+ noiseLabel: string;
16
+ animateBtn: string;
17
+ chaosBtn: string;
18
+ downloadPngBtn: string;
19
+ downloadGifBtn: string;
20
+ undoBtn: string;
21
+ redoBtn: string;
22
+ footerText: string;
23
+ faqTitle: string;
24
+ }
25
+
26
+ export type GlitchImageDistorterLocaleContent = ToolLocaleContent<GlitchImageDistorterUI>;
27
+
28
+ export const glitchImageDistorter: CreativeToolEntry<GlitchImageDistorterUI> = {
29
+ id: 'glitch-image-distorter',
30
+ icons: { bg: 'mdi:image-broken-variant', fg: 'mdi:flash-outline' },
31
+ i18n: {
32
+ de: () => import('./i18n/de').then((m) => m.content),
33
+ en: () => import('./i18n/en').then((m) => m.content),
34
+ es: () => import('./i18n/es').then((m) => m.content),
35
+ fr: () => import('./i18n/fr').then((m) => m.content),
36
+ id: () => import('./i18n/id').then((m) => m.content),
37
+ it: () => import('./i18n/it').then((m) => m.content),
38
+ ja: () => import('./i18n/ja').then((m) => m.content),
39
+ ko: () => import('./i18n/ko').then((m) => m.content),
40
+ nl: () => import('./i18n/nl').then((m) => m.content),
41
+ pl: () => import('./i18n/pl').then((m) => m.content),
42
+ pt: () => import('./i18n/pt').then((m) => m.content),
43
+ ru: () => import('./i18n/ru').then((m) => m.content),
44
+ sv: () => import('./i18n/sv').then((m) => m.content),
45
+ tr: () => import('./i18n/tr').then((m) => m.content),
46
+ zh: () => import('./i18n/zh').then((m) => m.content),
47
+ },
48
+ };