@field/cascade 0.2.22

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 (319) hide show
  1. package/AGENTS.md +168 -0
  2. package/ARCHITECTURE.md +137 -0
  3. package/DESIGN.md +161 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/dist/assets/CodeEditor-DR7u0q7p.css +1 -0
  7. package/dist/assets/CodeEditor-DoaNEUaM.js +641 -0
  8. package/dist/assets/CodePanel-B0_2XJ-t.js +2 -0
  9. package/dist/assets/CodePanel-IT1secyI.css +1 -0
  10. package/dist/assets/CustomNodeDialog-026lzecD.js +1 -0
  11. package/dist/assets/CustomNodeDialog-JGYnWY6m.css +1 -0
  12. package/dist/assets/ExportDialog-DOq0flMV.css +1 -0
  13. package/dist/assets/ExportDialog-DWN4nhXI.js +358 -0
  14. package/dist/assets/ProjectSettings-BzT164Yx.js +2 -0
  15. package/dist/assets/ProjectSettings-DdO9xoQz.css +1 -0
  16. package/dist/assets/SettingsDialog-CeK7ytpY.css +1 -0
  17. package/dist/assets/SettingsDialog-DmHb1V0r.js +1 -0
  18. package/dist/assets/VersionHistoryDialog-CWNmHQ-P.css +1 -0
  19. package/dist/assets/VersionHistoryDialog-hH6VsXLX.js +3 -0
  20. package/dist/assets/abap-08VXUWAP.js +1 -0
  21. package/dist/assets/apex-BWPQTe0t.js +1 -0
  22. package/dist/assets/azcli-Bc_sGQ0U.js +1 -0
  23. package/dist/assets/bat-i0X4ZdIN.js +1 -0
  24. package/dist/assets/bicep-B5-_aFwp.js +2 -0
  25. package/dist/assets/cameligo-DMUM7wLl.js +1 -0
  26. package/dist/assets/cascade.jpg +0 -0
  27. package/dist/assets/clojure-Cm7r79vr.js +1 -0
  28. package/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
  29. package/dist/assets/coffee-Ba7i2nA0.js +1 -0
  30. package/dist/assets/core-Bji4aZUU.js +965 -0
  31. package/dist/assets/cpp-C7h46wYY.js +1 -0
  32. package/dist/assets/csharp-BKxtCVv1.js +1 -0
  33. package/dist/assets/csp-bTuwJoIa.js +1 -0
  34. package/dist/assets/css-DIMkf-bt.js +3 -0
  35. package/dist/assets/css.worker-B3ciXF_0.js +93 -0
  36. package/dist/assets/cssMode-B6Z_Q1Kh.js +1 -0
  37. package/dist/assets/cypher-CVaqCwHa.js +1 -0
  38. package/dist/assets/dart-onAF5SnQ.js +1 -0
  39. package/dist/assets/dockerfile-DZFCIeNp.js +1 -0
  40. package/dist/assets/ecl-D05T4iGw.js +1 -0
  41. package/dist/assets/editor-1FQrmyyI.css +1 -0
  42. package/dist/assets/editor.api-jMoW_43X.js +847 -0
  43. package/dist/assets/editor.worker-q-txB4vs.js +30 -0
  44. package/dist/assets/elixir-6RTg0lbw.js +1 -0
  45. package/dist/assets/flow9-C5_-GSwl.js +1 -0
  46. package/dist/assets/freemarker2-DvvPV6m5.js +3 -0
  47. package/dist/assets/fsharp-C8Ef5oNN.js +1 -0
  48. package/dist/assets/geo-CXmCtzMM.js +2 -0
  49. package/dist/assets/go-C-y9NEjX.js +1 -0
  50. package/dist/assets/graphql-fmXr3nnJ.js +1 -0
  51. package/dist/assets/handlebars-B8rfuSIa.js +1 -0
  52. package/dist/assets/hcl-CpzslTdj.js +1 -0
  53. package/dist/assets/html-DyHm6JCp.js +1 -0
  54. package/dist/assets/html.worker-C93Ht9o9.js +506 -0
  55. package/dist/assets/htmlMode-E9XLWE5u.js +1 -0
  56. package/dist/assets/image-BUcQC7PG.js +2076 -0
  57. package/dist/assets/index-BXYXSdgh.js +224 -0
  58. package/dist/assets/index-CrNke7SW.css +1 -0
  59. package/dist/assets/index-client-DN_FvjN8.js +1 -0
  60. package/dist/assets/ini-sBoK_t0W.js +1 -0
  61. package/dist/assets/java-BEtHBSE6.js +1 -0
  62. package/dist/assets/javascript-C_bM5PCN.js +1 -0
  63. package/dist/assets/json.worker-B2V3pomh.js +62 -0
  64. package/dist/assets/jsonMode-BdyVS7G4.js +7 -0
  65. package/dist/assets/julia-Bri6UV-V.js +1 -0
  66. package/dist/assets/kotlin-BOotOW0E.js +1 -0
  67. package/dist/assets/legacy-DIu3XRwl.js +2 -0
  68. package/dist/assets/less-B9JPFI3C.js +2 -0
  69. package/dist/assets/lexon-CfSJPG6W.js +1 -0
  70. package/dist/assets/liquid-CDkn3spA.js +1 -0
  71. package/dist/assets/lspLanguageFeatures-AB5DMAsr.js +4 -0
  72. package/dist/assets/lua-CsQS60Ue.js +1 -0
  73. package/dist/assets/m3-D-oSqn_W.js +1 -0
  74. package/dist/assets/markdown-Cimd5fb3.js +1 -0
  75. package/dist/assets/mdx-BvH_NjDN.js +1 -0
  76. package/dist/assets/mips-CIPQ_RoX.js +1 -0
  77. package/dist/assets/msdax-DauUninz.js +1 -0
  78. package/dist/assets/mysql-SOo6toE5.js +1 -0
  79. package/dist/assets/nodeTypeUtils-CweA3NHd.js +1 -0
  80. package/dist/assets/objective-c-FvmIjYaQ.js +1 -0
  81. package/dist/assets/pascal-DrH0SRf2.js +1 -0
  82. package/dist/assets/pascaligo-D-ptJ9y-.js +1 -0
  83. package/dist/assets/perl-oz_6vUea.js +1 -0
  84. package/dist/assets/pgsql-DTj74zXo.js +1 -0
  85. package/dist/assets/php-nr791fC2.js +1 -0
  86. package/dist/assets/pla-CopQ2nXW.js +1 -0
  87. package/dist/assets/postiats-43DmfD33.js +1 -0
  88. package/dist/assets/powerquery-D3hlyOfw.js +1 -0
  89. package/dist/assets/powershell-DmHpPYUd.js +1 -0
  90. package/dist/assets/projectSettingsApi-C1WvIwfP.js +1 -0
  91. package/dist/assets/protobuf-C531GsRP.js +2 -0
  92. package/dist/assets/pug-Z5eAx3Zn.js +1 -0
  93. package/dist/assets/python-B7uii4fi.js +1 -0
  94. package/dist/assets/qsharp-DkqhCAOL.js +1 -0
  95. package/dist/assets/quill-DjZ69wL_.js +121 -0
  96. package/dist/assets/r-BwWrilGY.js +1 -0
  97. package/dist/assets/razor-Z48fWdJc.js +1 -0
  98. package/dist/assets/redis-ClamHrr6.js +1 -0
  99. package/dist/assets/redshift-DT7zqm-g.js +1 -0
  100. package/dist/assets/restructuredtext-BYgofb2h.js +1 -0
  101. package/dist/assets/ruby-DezsRK8O.js +1 -0
  102. package/dist/assets/rust-DdL9SqIa.js +1 -0
  103. package/dist/assets/sb-CcwsVR0C.js +1 -0
  104. package/dist/assets/scala-DHpiXF5c.js +1 -0
  105. package/dist/assets/scheme-BeGwcela.js +1 -0
  106. package/dist/assets/scss-gp-XZpBa.js +3 -0
  107. package/dist/assets/shell-CC2rA5mh.js +1 -0
  108. package/dist/assets/solidity-BEEn4gHE.js +1 -0
  109. package/dist/assets/sophia-CRfGWb83.js +1 -0
  110. package/dist/assets/sparql-D_Lu-MrJ.js +1 -0
  111. package/dist/assets/sql-NEE52Syq.js +1 -0
  112. package/dist/assets/st-DbInun42.js +1 -0
  113. package/dist/assets/swift-Bxkupp3x.js +1 -0
  114. package/dist/assets/systemverilog-Bz4Y3fRF.js +1 -0
  115. package/dist/assets/tcl-DISqw1ZD.js +1 -0
  116. package/dist/assets/ts.worker-D7T1-Ig5.js +67738 -0
  117. package/dist/assets/tsMode-BDgacVG4.js +11 -0
  118. package/dist/assets/twig-De2hgUGE.js +1 -0
  119. package/dist/assets/typescript-Derb6Oqa.js +1 -0
  120. package/dist/assets/typespec-B8J7ngcE.js +1 -0
  121. package/dist/assets/vb-DV3o63ZY.js +1 -0
  122. package/dist/assets/wgsl-DpFanUEy.js +298 -0
  123. package/dist/assets/workers-BtoMX_pm.js +1 -0
  124. package/dist/assets/xml-B1CaFSrG.js +1 -0
  125. package/dist/assets/yaml-HmIeA0KH.js +1 -0
  126. package/dist/cli/index.js +11377 -0
  127. package/dist/cli/index.js.map +7 -0
  128. package/dist/cli/runtime/io.ts +445 -0
  129. package/dist/cli/runtime/net.ts +19 -0
  130. package/dist/cli/runtime/shell.ts +46 -0
  131. package/dist/cli/runtime/stage.ts +132 -0
  132. package/dist/contracts/capabilities.d.ts +109 -0
  133. package/dist/contracts/capabilities.d.ts.map +1 -0
  134. package/dist/contracts/capabilities.js +11 -0
  135. package/dist/contracts/capabilities.js.map +1 -0
  136. package/dist/contracts/color.d.ts +67 -0
  137. package/dist/contracts/color.d.ts.map +1 -0
  138. package/dist/contracts/color.js +83 -0
  139. package/dist/contracts/color.js.map +1 -0
  140. package/dist/contracts/definition.d.ts +118 -0
  141. package/dist/contracts/definition.d.ts.map +1 -0
  142. package/dist/contracts/definition.js +2 -0
  143. package/dist/contracts/definition.js.map +1 -0
  144. package/dist/contracts/document.d.ts +50 -0
  145. package/dist/contracts/document.d.ts.map +1 -0
  146. package/dist/contracts/document.js +2 -0
  147. package/dist/contracts/document.js.map +1 -0
  148. package/dist/contracts/geometry.d.ts +301 -0
  149. package/dist/contracts/geometry.d.ts.map +1 -0
  150. package/dist/contracts/geometry.js +604 -0
  151. package/dist/contracts/geometry.js.map +1 -0
  152. package/dist/contracts/index.d.ts +8 -0
  153. package/dist/contracts/index.d.ts.map +1 -0
  154. package/dist/contracts/index.js +8 -0
  155. package/dist/contracts/index.js.map +1 -0
  156. package/dist/contracts/schema.d.ts +6 -0
  157. package/dist/contracts/schema.d.ts.map +1 -0
  158. package/dist/contracts/schema.js +85 -0
  159. package/dist/contracts/schema.js.map +1 -0
  160. package/dist/contracts/validate.d.ts +13 -0
  161. package/dist/contracts/validate.d.ts.map +1 -0
  162. package/dist/contracts/validate.js +202 -0
  163. package/dist/contracts/validate.js.map +1 -0
  164. package/dist/contracts/values.d.ts +100 -0
  165. package/dist/contracts/values.d.ts.map +1 -0
  166. package/dist/contracts/values.js +59 -0
  167. package/dist/contracts/values.js.map +1 -0
  168. package/dist/graphs/default-orig.cascade +128 -0
  169. package/dist/graphs/default.cascade +21 -0
  170. package/dist/index.html +19 -0
  171. package/dist/runtime/browser.d.ts +3 -0
  172. package/dist/runtime/browser.d.ts.map +1 -0
  173. package/dist/runtime/browser.js +12 -0
  174. package/dist/runtime/browser.js.map +1 -0
  175. package/dist/runtime/builtins/core/index.d.ts +9 -0
  176. package/dist/runtime/builtins/core/index.d.ts.map +1 -0
  177. package/dist/runtime/builtins/core/index.js +21 -0
  178. package/dist/runtime/builtins/core/index.js.map +1 -0
  179. package/dist/runtime/builtins/core/random.d.ts +66 -0
  180. package/dist/runtime/builtins/core/random.d.ts.map +1 -0
  181. package/dist/runtime/builtins/core/random.js +46 -0
  182. package/dist/runtime/builtins/core/random.js.map +1 -0
  183. package/dist/runtime/builtins/core/remap.d.ts +105 -0
  184. package/dist/runtime/builtins/core/remap.d.ts.map +1 -0
  185. package/dist/runtime/builtins/core/remap.js +43 -0
  186. package/dist/runtime/builtins/core/remap.js.map +1 -0
  187. package/dist/runtime/builtins/core/routing.d.ts +117 -0
  188. package/dist/runtime/builtins/core/routing.d.ts.map +1 -0
  189. package/dist/runtime/builtins/core/routing.js +114 -0
  190. package/dist/runtime/builtins/core/routing.js.map +1 -0
  191. package/dist/runtime/builtins/core/structural.d.ts +86 -0
  192. package/dist/runtime/builtins/core/structural.d.ts.map +1 -0
  193. package/dist/runtime/builtins/core/structural.js +63 -0
  194. package/dist/runtime/builtins/core/structural.js.map +1 -0
  195. package/dist/runtime/builtins/geo/copyToPoints.d.ts +58 -0
  196. package/dist/runtime/builtins/geo/copyToPoints.d.ts.map +1 -0
  197. package/dist/runtime/builtins/geo/copyToPoints.js +35 -0
  198. package/dist/runtime/builtins/geo/copyToPoints.js.map +1 -0
  199. package/dist/runtime/builtins/geo/definitions.d.ts +526 -0
  200. package/dist/runtime/builtins/geo/definitions.d.ts.map +1 -0
  201. package/dist/runtime/builtins/geo/definitions.js +182 -0
  202. package/dist/runtime/builtins/geo/definitions.js.map +1 -0
  203. package/dist/runtime/builtins/geo/generators.d.ts +94 -0
  204. package/dist/runtime/builtins/geo/generators.d.ts.map +1 -0
  205. package/dist/runtime/builtins/geo/generators.js +41 -0
  206. package/dist/runtime/builtins/geo/generators.js.map +1 -0
  207. package/dist/runtime/builtins/geo/index.d.ts +23 -0
  208. package/dist/runtime/builtins/geo/index.d.ts.map +1 -0
  209. package/dist/runtime/builtins/geo/index.js +35 -0
  210. package/dist/runtime/builtins/geo/index.js.map +1 -0
  211. package/dist/runtime/builtins/geo/merge.d.ts +40 -0
  212. package/dist/runtime/builtins/geo/merge.d.ts.map +1 -0
  213. package/dist/runtime/builtins/geo/merge.js +24 -0
  214. package/dist/runtime/builtins/geo/merge.js.map +1 -0
  215. package/dist/runtime/builtins/geo/namespace.d.ts +15 -0
  216. package/dist/runtime/builtins/geo/namespace.d.ts.map +1 -0
  217. package/dist/runtime/builtins/geo/namespace.js +17 -0
  218. package/dist/runtime/builtins/geo/namespace.js.map +1 -0
  219. package/dist/runtime/builtins/geo/svgExport.d.ts +130 -0
  220. package/dist/runtime/builtins/geo/svgExport.d.ts.map +1 -0
  221. package/dist/runtime/builtins/geo/svgExport.js +50 -0
  222. package/dist/runtime/builtins/geo/svgExport.js.map +1 -0
  223. package/dist/runtime/builtins/geo/transform.d.ts +60 -0
  224. package/dist/runtime/builtins/geo/transform.d.ts.map +1 -0
  225. package/dist/runtime/builtins/geo/transform.js +32 -0
  226. package/dist/runtime/builtins/geo/transform.js.map +1 -0
  227. package/dist/runtime/builtins/index.d.ts +9 -0
  228. package/dist/runtime/builtins/index.d.ts.map +1 -0
  229. package/dist/runtime/builtins/index.js +16 -0
  230. package/dist/runtime/builtins/index.js.map +1 -0
  231. package/dist/runtime/definition/architecture.d.ts +8 -0
  232. package/dist/runtime/definition/architecture.d.ts.map +1 -0
  233. package/dist/runtime/definition/architecture.js +112 -0
  234. package/dist/runtime/definition/architecture.js.map +1 -0
  235. package/dist/runtime/definition/extract.d.ts +12 -0
  236. package/dist/runtime/definition/extract.d.ts.map +1 -0
  237. package/dist/runtime/definition/extract.js +151 -0
  238. package/dist/runtime/definition/extract.js.map +1 -0
  239. package/dist/runtime/error.d.ts +8 -0
  240. package/dist/runtime/error.d.ts.map +1 -0
  241. package/dist/runtime/error.js +11 -0
  242. package/dist/runtime/error.js.map +1 -0
  243. package/dist/runtime/geometry/attributes.d.ts +65 -0
  244. package/dist/runtime/geometry/attributes.d.ts.map +1 -0
  245. package/dist/runtime/geometry/attributes.js +201 -0
  246. package/dist/runtime/geometry/attributes.js.map +1 -0
  247. package/dist/runtime/geometry/builder.d.ts +57 -0
  248. package/dist/runtime/geometry/builder.d.ts.map +1 -0
  249. package/dist/runtime/geometry/builder.js +153 -0
  250. package/dist/runtime/geometry/builder.js.map +1 -0
  251. package/dist/runtime/geometry/copy.d.ts +36 -0
  252. package/dist/runtime/geometry/copy.d.ts.map +1 -0
  253. package/dist/runtime/geometry/copy.js +68 -0
  254. package/dist/runtime/geometry/copy.js.map +1 -0
  255. package/dist/runtime/geometry/generate.d.ts +43 -0
  256. package/dist/runtime/geometry/generate.d.ts.map +1 -0
  257. package/dist/runtime/geometry/generate.js +76 -0
  258. package/dist/runtime/geometry/generate.js.map +1 -0
  259. package/dist/runtime/geometry/groups.d.ts +18 -0
  260. package/dist/runtime/geometry/groups.d.ts.map +1 -0
  261. package/dist/runtime/geometry/groups.js +69 -0
  262. package/dist/runtime/geometry/groups.js.map +1 -0
  263. package/dist/runtime/geometry/index.d.ts +23 -0
  264. package/dist/runtime/geometry/index.d.ts.map +1 -0
  265. package/dist/runtime/geometry/index.js +23 -0
  266. package/dist/runtime/geometry/index.js.map +1 -0
  267. package/dist/runtime/geometry/matrix.d.ts +26 -0
  268. package/dist/runtime/geometry/matrix.d.ts.map +1 -0
  269. package/dist/runtime/geometry/matrix.js +27 -0
  270. package/dist/runtime/geometry/matrix.js.map +1 -0
  271. package/dist/runtime/geometry/merge.d.ts +33 -0
  272. package/dist/runtime/geometry/merge.d.ts.map +1 -0
  273. package/dist/runtime/geometry/merge.js +185 -0
  274. package/dist/runtime/geometry/merge.js.map +1 -0
  275. package/dist/runtime/geometry/primitives.d.ts +41 -0
  276. package/dist/runtime/geometry/primitives.d.ts.map +1 -0
  277. package/dist/runtime/geometry/primitives.js +131 -0
  278. package/dist/runtime/geometry/primitives.js.map +1 -0
  279. package/dist/runtime/geometry/promote.d.ts +44 -0
  280. package/dist/runtime/geometry/promote.d.ts.map +1 -0
  281. package/dist/runtime/geometry/promote.js +203 -0
  282. package/dist/runtime/geometry/promote.js.map +1 -0
  283. package/dist/runtime/geometry/resample.d.ts +29 -0
  284. package/dist/runtime/geometry/resample.d.ts.map +1 -0
  285. package/dist/runtime/geometry/resample.js +165 -0
  286. package/dist/runtime/geometry/resample.js.map +1 -0
  287. package/dist/runtime/geometry/svg.d.ts +91 -0
  288. package/dist/runtime/geometry/svg.d.ts.map +1 -0
  289. package/dist/runtime/geometry/svg.js +216 -0
  290. package/dist/runtime/geometry/svg.js.map +1 -0
  291. package/dist/runtime/geometry/transform.d.ts +18 -0
  292. package/dist/runtime/geometry/transform.d.ts.map +1 -0
  293. package/dist/runtime/geometry/transform.js +56 -0
  294. package/dist/runtime/geometry/transform.js.map +1 -0
  295. package/dist/runtime/index.d.ts +5 -0
  296. package/dist/runtime/index.d.ts.map +1 -0
  297. package/dist/runtime/index.js +4 -0
  298. package/dist/runtime/index.js.map +1 -0
  299. package/dist/runtime/node.d.ts +4 -0
  300. package/dist/runtime/node.d.ts.map +1 -0
  301. package/dist/runtime/node.js +12 -0
  302. package/dist/runtime/node.js.map +1 -0
  303. package/dist/runtime/runtime.d.ts +3 -0
  304. package/dist/runtime/runtime.d.ts.map +1 -0
  305. package/dist/runtime/runtime.js +1198 -0
  306. package/dist/runtime/runtime.js.map +1 -0
  307. package/dist/runtime/types.d.ts +241 -0
  308. package/dist/runtime/types.d.ts.map +1 -0
  309. package/dist/runtime/types.js +2 -0
  310. package/dist/runtime/types.js.map +1 -0
  311. package/dist/shell.d.ts +8 -0
  312. package/dist/shell.js +54 -0
  313. package/dist/shell.js.map +7 -0
  314. package/dist/stage.d.ts +7 -0
  315. package/dist/stage.js +84 -0
  316. package/dist/stage.js.map +7 -0
  317. package/dist/studio/panel.d.ts +48 -0
  318. package/doc/PROJECT_AUTHORING.md +360 -0
  319. package/package.json +126 -0
@@ -0,0 +1,2076 @@
1
+ import{c as e,o as t}from"./nodeTypeUtils-CweA3NHd.js";import{E as n,T as r,_ as i,g as a,o}from"./index-BXYXSdgh.js";var s=class extends o{constructor(e,t){super(e,`Color`,t)}setup(){this.addParm(`color`,{value:{r:1,g:1,b:1},type:`color`,displayName:`Color`}),this.addResolutionParm(),this.output=this.out(`image`),this.watchProp(`color`,()=>this.requestCook()),this.onReady=()=>this.requestCook()}render(){let[e,t]=this.getResolution(),r=n(this.props.color.value),a=i.solid(e,t,r);this.setOutput(this.output,a)}},c=class extends o{constructor(e,t){super(e,`File`,t)}setup(){this.imageInput=this.in(`image`,null),this.addParm(`file`,{value:``,type:`image`,params:{accept:`image/*`},displayName:`File`,onChange:()=>{this.props.file.value&&!this.imageInput.value&&this.requestCook()}}),this.addParm(`resolutionMode`,{value:`original`,params:{options:[{value:`original`,label:`Original`},{value:`max`,label:`Max Resolution`},{value:`fixed`,label:`Fixed Resolution`}]},displayName:`Resolution Mode`,onChange:()=>{(this.props.file.value||this.imageInput.value)&&this.requestCook()}}),this.addParm(`maxResolution`,{value:[2048,2048],params:{min:[1,1],max:[4096,4096]},displayName:`Max Resolution`,hidden:()=>this.props.resolutionMode.value!==`max`,onChange:()=>{(this.props.file.value||this.imageInput.value)&&this.props.resolutionMode.value===`max`&&this.requestCook()}}),this.addParm(`fixedResolution`,{value:[512,512],params:{min:[1,1],max:[4096,4096]},displayName:`Fixed Resolution`,hidden:()=>this.props.resolutionMode.value!==`fixed`,onChange:()=>{(this.props.file.value||this.imageInput.value)&&this.props.resolutionMode.value===`fixed`&&this.requestCook()}}),this.output=this.out(`image`),this.imageInput.onChange=()=>this.requestCook(),this.onReady=()=>{(this.props.file.value||this.imageInput.value)&&this.requestCook()}}async render(){let e=null;if(this.imageInput.value){if(this.imageInput.value instanceof i)e=this.imageInput.value;else if(this.imageInput.value instanceof HTMLImageElement||this.imageInput.value instanceof HTMLCanvasElement)e=i.fromCanvas(this.imageInput.value);else if(typeof this.imageInput.value==`string`){let t=new Image;t.crossOrigin=`anonymous`,await new Promise((e,n)=>{t.onload=()=>e(),t.onerror=n,t.src=this.imageInput.value}),e=i.fromCanvas(t)}}else if(this.props.file.value){let t=new Image;t.crossOrigin=`anonymous`,await new Promise((e,n)=>{t.onload=()=>e(),t.onerror=n,t.src=this.props.file.value}),e=i.fromCanvas(t)}if(e)try{let t=this.props.resolutionMode.value,n;if(t===`original`)n=e;else if(t===`max`){let[t,r]=this.props.maxResolution.value,i=e.width/e.height,a=e.width,o=e.height;a>t&&(a=t,o=t/i),o>r&&(o=r,a=r*i),a=Math.round(a),o=Math.round(o),n=a!==e.width||o!==e.height?this.resizeBuffer(e,a,o):e}else{let[t,r]=this.props.fixedResolution.value;n=this.resizeBuffer(e,t,r)}this.setOutput(this.output,n)}catch(e){this.error=e,console.error(`Failed to process image:`,e)}}resizeBuffer(e,t,n){let r=i.rgba(t,n),a=e.width/t,o=e.height/n;for(let i=0;i<Math.min(e.channelCount,4);i++){let s=r.channels[i];for(let r=0;r<n;r++)for(let n=0;n<t;n++){let c=n*a,l=r*o;s[r*t+n]=e.sample(c,l,i)}}return e.channelCount<4&&r.fill(3,1),r}},l=class extends o{constructor(e,t){super(e,`Composite`,t)}setup(){this.image1=this.in(`image1`,null),this.image2=this.in(`image2`,null),this.addParm(`blendMode`,{value:`multiply`,params:{options:[{value:`normal`,label:`Normal`},{value:`multiply`,label:`Multiply`},{value:`screen`,label:`Screen`},{value:`overlay`,label:`Overlay`},{value:`darken`,label:`Darken`},{value:`lighten`,label:`Lighten`},{value:`color-dodge`,label:`Color Dodge`},{value:`color-burn`,label:`Color Burn`},{value:`hard-light`,label:`Hard Light`},{value:`soft-light`,label:`Soft Light`},{value:`difference`,label:`Difference`},{value:`exclusion`,label:`Exclusion`},{value:`add`,label:`Add`},{value:`subtract`,label:`Subtract`},{value:`divide`,label:`Divide`},{value:`pin-light`,label:`Pin Light`},{value:`vivid-light`,label:`Vivid Light`},{value:`linear-dodge`,label:`Linear Dodge`},{value:`linear-burn`,label:`Linear Burn`}]},displayName:`Blend Mode`,onChange:()=>this.requestCook()}),this.addParm(`opacity`,{value:1,params:{min:0,max:1,step:.01},displayName:`Opacity`,onChange:()=>this.requestCook()}),this.addParm(`outputResolution`,{value:`input1`,params:{options:[{value:`input1`,label:`Use Input 1`},{value:`input2`,label:`Use Input 2`},{value:`largest`,label:`Largest Input`},{value:`smallest`,label:`Smallest Input`},{value:`custom`,label:`Custom`}]},displayName:`Output Resolution`,onChange:()=>this.requestCook()}),this.addParm(`customResolution`,{value:[512,512],params:{min:[1,1],max:[4096,4096],integer:!0},displayName:`Resolution`,hidden:()=>this.props.outputResolution.value!==`custom`,onChange:()=>this.requestCook()}),this.addParm(`fitMode`,{value:`fill`,params:{options:[{value:`fill`,label:`Fill (Crop)`},{value:`fit`,label:`Fit (Letterbox)`},{value:`stretch`,label:`Stretch`},{value:`native`,label:`Native (No Scale)`}]},displayName:`Fit Mode`,onChange:()=>this.requestCook()}),this.output=this.out(`image`),this.image1.onChange=()=>this.requestCook(),this.image2.onChange=()=>this.requestCook(),this.onReady=()=>this.requestCook()}blendBuffers(e,t,n,r){let a=e.width,o=e.height,s=i.rgba(a,o),c=e.channelCount>=4?e:e.toRGBA(),l=t.channelCount>=4?t:t.toRGBA(),u=c.channels[0],d=c.channels[1],f=c.channels[2],p=c.channels[3],m=l.channels[0],h=l.channels[1],g=l.channels[2],_=l.channels[3],v=s.channels[0],y=s.channels[1],b=s.channels[2],x=s.channels[3];for(let e=0;e<a*o;e++){let t=u[e],i=d[e],a=f[e],o=p[e],s=m[e],c=h[e],l=g[e],S=_[e],C,w,T;switch(n){case`normal`:C=s,w=c,T=l;break;case`multiply`:C=t*s,w=i*c,T=a*l;break;case`screen`:C=1-(1-t)*(1-s),w=1-(1-i)*(1-c),T=1-(1-a)*(1-l);break;case`overlay`:C=t<.5?2*t*s:1-2*(1-t)*(1-s),w=i<.5?2*i*c:1-2*(1-i)*(1-c),T=a<.5?2*a*l:1-2*(1-a)*(1-l);break;case`darken`:C=Math.min(t,s),w=Math.min(i,c),T=Math.min(a,l);break;case`lighten`:C=Math.max(t,s),w=Math.max(i,c),T=Math.max(a,l);break;case`color-dodge`:C=s===1?1:Math.min(1,t/(1-s)),w=c===1?1:Math.min(1,i/(1-c)),T=l===1?1:Math.min(1,a/(1-l));break;case`color-burn`:C=s===0?0:Math.max(0,1-(1-t)/s),w=c===0?0:Math.max(0,1-(1-i)/c),T=l===0?0:Math.max(0,1-(1-a)/l);break;case`hard-light`:C=s<.5?2*t*s:1-2*(1-t)*(1-s),w=c<.5?2*i*c:1-2*(1-i)*(1-c),T=l<.5?2*a*l:1-2*(1-a)*(1-l);break;case`soft-light`:C=s<.5?t-(1-2*s)*t*(1-t):t+(2*s-1)*(Math.sqrt(t)-t),w=c<.5?i-(1-2*c)*i*(1-i):i+(2*c-1)*(Math.sqrt(i)-i),T=l<.5?a-(1-2*l)*a*(1-a):a+(2*l-1)*(Math.sqrt(a)-a);break;case`difference`:C=Math.abs(t-s),w=Math.abs(i-c),T=Math.abs(a-l);break;case`exclusion`:C=t+s-2*t*s,w=i+c-2*i*c,T=a+l-2*a*l;break;case`add`:C=Math.min(1,t+s),w=Math.min(1,i+c),T=Math.min(1,a+l);break;case`subtract`:C=Math.max(0,t-s),w=Math.max(0,i-c),T=Math.max(0,a-l);break;case`divide`:C=s===0?1:Math.min(1,t/s),w=c===0?1:Math.min(1,i/c),T=l===0?1:Math.min(1,a/l);break;case`pin-light`:C=s<.5?Math.min(t,2*s):Math.max(t,2*(s-.5)),w=c<.5?Math.min(i,2*c):Math.max(i,2*(c-.5)),T=l<.5?Math.min(a,2*l):Math.max(a,2*(l-.5));break;case`vivid-light`:C=s<.5?s===0?0:1-(1-t)/(2*s):s===1?1:t/(2*(1-s)),w=c<.5?c===0?0:1-(1-i)/(2*c):c===1?1:i/(2*(1-c)),T=l<.5?l===0?0:1-(1-a)/(2*l):l===1?1:a/(2*(1-l));break;case`linear-dodge`:C=Math.min(1,t+s),w=Math.min(1,i+c),T=Math.min(1,a+l);break;case`linear-burn`:C=Math.max(0,t+s-1),w=Math.max(0,i+c-1),T=Math.max(0,a+l-1);break;default:C=s,w=c,T=l}v[e]=t+(C-t)*r*S,y[e]=i+(w-i)*r*S,b[e]=a+(T-a)*r*S,x[e]=o+(S-o)*r}return s}render(){let e=this.props.outputResolution.value,t=this.props.fitMode.value,n=this.props.customResolution.value,{buffers:r}=this.prepareInputs([this.image1.value,this.image2.value],e,t,n),[i,a]=r;if(!i||!a)return;let o=this.props.blendMode.value,s=this.props.opacity.value;try{let e=this.blendBuffers(i,a,o,s);this.setOutput(this.output,e)}catch(e){console.error(`Composite render error:`,e),this.error=e}}},u=class extends o{constructor(e,t){super(e,`Checkers`,t)}setup(){this.addParm(`color1`,{value:{r:1,g:1,b:1},type:`color`,displayName:`Color 1`,onChange:()=>this.requestCook()}),this.addParm(`color2`,{value:{r:0,g:0,b:0},type:`color`,displayName:`Color 2`,onChange:()=>this.requestCook()}),this.addParm(`mode`,{value:`size`,params:{options:[{value:`size`,label:`Size`},{value:`divisions`,label:`Divisions`}]},displayName:`Mode`,onChange:()=>this.requestCook()}),this.addParm(`size`,{value:32,params:{min:1,max:512,step:.1},displayName:`Size`,hidden:()=>this.props.mode.value!==`size`,onChange:()=>this.requestCook()}),this.addParm(`divisions`,{value:16,params:{min:1,max:512,step:1,integer:!0},displayName:`Divisions`,hidden:()=>this.props.mode.value!==`divisions`,onChange:()=>this.requestCook()}),this.addParm(`centered`,{value:!1,type:`boolean`,displayName:`Centered`,onChange:()=>this.requestCook()}),this.addResolutionParm(),this.output=this.out(`image`),this.onReady=()=>this.requestCook()}render(){let[e,t]=this.getResolution(),r=this.props.mode.value,a=n(this.props.color1.value),o=n(this.props.color2.value),s=this.props.centered.value,c;c=r===`size`?this.props.size.value:e/this.props.divisions.value;let l=0,u=0;s&&(l=e/2%c-c/2,u=t/2%c-c/2);let d=i.rgba(e,t),f=d.r(),p=d.g(),m=d.b(),h=d.a();for(let n=0;n<t;n++)for(let t=0;t<e;t++){let r=n*e+t,i=(Math.floor((t-l)/c)+Math.floor((n-u)/c))%2==0?a:o;f[r]=i.r,p[r]=i.g,m[r]=i.b,h[r]=i.a}d.markDirty(),this.setOutput(this.output,d)}},d=class extends o{constructor(e,t){super(e,`Resize`,t)}setup(){this.image=this.in(`image`,null),this.addParm(`mode`,{value:`scale`,params:{options:[{value:`scale`,label:`Scale`},{value:`fixed`,label:`Fixed`}]},displayName:`Mode`,onChange:()=>this.requestCook()}),this.addParm(`scale`,{value:1,params:{min:.1,max:10,step:.1},displayName:`Scale`,hidden:()=>this.props.mode.value!==`scale`,onChange:()=>this.requestCook()}),this.addParm(`width`,{value:512,params:{min:1,max:4096,step:1},displayName:`Width`,hidden:()=>this.props.mode.value!==`fixed`,onChange:()=>this.requestCook()}),this.addParm(`height`,{value:512,params:{min:1,max:4096,step:1},displayName:`Height`,hidden:()=>this.props.mode.value!==`fixed`,onChange:()=>this.requestCook()}),this.output=this.out(`image`),this.image.onChange=()=>this.requestCook(),this.onReady=()=>this.requestCook()}resizeBuffer(e,t,n){let r=i.rgba(t,n),a=e.width/t,o=e.height/n;for(let i=0;i<Math.min(e.channelCount,4);i++){let s=r.channels[i];for(let r=0;r<n;r++)for(let n=0;n<t;n++){let c=n*a,l=r*o;s[r*t+n]=e.sample(c,l,i)}}return e.channelCount<4&&r.fill(3,1),r}render(){let e=this.toImageBuffer(this.image.value);if(!e)return;let t,n;if(this.props.mode.value===`scale`){let r=this.props.scale.value;t=Math.round(e.width*r),n=Math.round(e.height*r)}else t=this.props.width.value,n=this.props.height.value;let r=this.resizeBuffer(e,t,n);this.setOutput(this.output,r)}},f=class extends o{constructor(e,t){super(e,`NormalMap`,t)}setup(){this.image=this.in(`image`,null),this.addParm(`scale`,{value:1,params:{min:0,max:10,step:.1},displayName:`Scale`,onChange:()=>this.requestCook()}),this.addParm(`flipX`,{value:!1,type:`boolean`,displayName:`Flip X`,onChange:()=>this.requestCook()}),this.addParm(`flipY`,{value:!1,type:`boolean`,displayName:`Flip Y`,onChange:()=>this.requestCook()}),this.output=this.out(`image`),this.image.onChange=()=>this.requestCook(),this.onReady=()=>this.requestCook()}computeNormalMap(e,t,n,r){let{width:a,height:o}=e,s=i.rgba(a,o),c=e.channels[0],l=s.channels[0],u=s.channels[1],d=s.channels[2],f=s.channels[3];for(let e=0;e<o;e++)for(let i=0;i<a;i++){let s=e*a+i,p=c[s],m=c[Math.min(i+1,a-1)+e*a],h=c[i+Math.min(e+1,o-1)*a],g=(m-p)*t,_=(h-p)*t,v=n?-g:g,y=r?-_:_,b=-v,x=-y,S=Math.sqrt(b*b+x*x+1),C=b/S,w=x/S,T=1/S;l[s]=C*.5+.5,u[s]=w*.5+.5,d[s]=T*.5+.5,f[s]=1}return s}render(){let e=this.toImageBuffer(this.image.value);if(!e)return;let t=this.props.scale.value,n=this.props.flipX.value,r=this.props.flipY.value,i=this.computeNormalMap(e,t,n,r);this.setOutput(this.output,i)}},p=class extends o{constructor(e,t){super(e,`Ramp`,t)}setup(){this.addParm(`type`,{value:`horizontal`,params:{options:[{value:`horizontal`,label:`Horizontal`},{value:`vertical`,label:`Vertical`},{value:`radial`,label:`Radial`},{value:`concentric`,label:`Concentric`}]},displayName:`Type`,onChange:()=>this.requestCook()}),this.addParm(`points`,{value:[{position:0,color:{r:0,g:0,b:0},interpolation:`linear`},{position:1,color:{r:1,g:1,b:1},interpolation:`linear`}],type:`colorramp`,displayName:`Ramp`,onChange:()=>this.requestCook()}),this.addResolutionParm(),this.output=this.out(`image`),this.onReady=()=>this.requestCook()}normalizeRampColor(e){if(typeof e==`object`&&e&&`r`in e&&`g`in e&&`b`in e){let t=e.r<=1&&e.g<=1&&e.b<=1;return{r:t?e.r:e.r/255,g:t?e.g:e.g/255,b:t?e.b:e.b/255,a:e.a===void 0?1:t?e.a:e.a/255}}if(Array.isArray(e)){let t=e.every(e=>e<=1);return{r:t?e[0]:e[0]/255,g:t?e[1]:e[1]/255,b:t?e[2]:e[2]/255,a:e[3]===void 0?1:t?e[3]:e[3]/255}}if(typeof e==`string`){let t=e.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);if(t)return{r:parseInt(t[1],10)/255,g:parseInt(t[2],10)/255,b:parseInt(t[3],10)/255,a:1};let n=e.replace(`#`,``);if(n.length===3||n.length===6){let e=n.length===3?n.split(``).map(e=>e+e).join(``):n,t=/^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);if(t)return{r:parseInt(t[1],16)/255,g:parseInt(t[2],16)/255,b:parseInt(t[3],16)/255,a:1}}}return{r:0,g:0,b:0,a:1}}interpolateColor(e,t,n,r){return r===`constant`?n<.5?e:t:(r===`smooth`&&(n=n*n*(3-2*n)),{r:e.r+(t.r-e.r)*n,g:e.g+(t.g-e.g)*n,b:e.b+(t.b-e.b)*n,a:(e.a??1)+((t.a??1)-(e.a??1))*n})}getColorAtPosition(e,t){let n=[...e].sort((e,t)=>e.position-t.position);t=Math.max(0,Math.min(1,t));let r=null,i=null;for(let e=0;e<n.length;e++)if(n[e].position<=t&&(r=n[e]),n[e].position>=t&&!i){i=n[e];break}if(!r&&i)return this.normalizeRampColor(i.color);if(r&&!i)return this.normalizeRampColor(r.color);if(!r&&!i)return{r:0,g:0,b:0,a:1};if(r.position===t)return this.normalizeRampColor(r.color);if(i.position===t)return this.normalizeRampColor(i.color);let a=(t-r.position)/(i.position-r.position),o=this.normalizeRampColor(r.color),s=this.normalizeRampColor(i.color),c=i.interpolation||`linear`;return this.interpolateColor(o,s,a,c)}render(){let[e,t]=this.getResolution(),n=this.props.type.value,r=this.props.points.value||[];if(r.length===0)return;let a=i.rgba(e,t),o=a.r(),s=a.g(),c=a.b(),l=a.a(),u=e/2,d=t/2,f=Math.sqrt(u*u+d*d);for(let i=0;i<t;i++)for(let a=0;a<e;a++){let p=0;if(n===`horizontal`)p=a/e;else if(n===`vertical`)p=i/t;else if(n===`radial`){let e=a-u,t=i-d;p=(Math.atan2(t,e)+Math.PI)/(2*Math.PI)}else if(n===`concentric`){let e=a-u,t=i-d;p=Math.sqrt(e*e+t*t)/f}let m=this.getColorAtPosition(r,p),h=i*e+a;o[h]=m.r,s[h]=m.g,c[h]=m.b,l[h]=m.a??1}a.markDirty(),this.setOutput(this.output,a)}},m=class extends o{constructor(e,t){super(e,`Noise`,t),this.noise2D=null,this.currentSeed=null}setup(){this.addParm(`seed`,{value:0,params:{min:0,max:1e4,step:1,integer:!0},displayName:`Seed`,onChange:()=>{this.initNoise().then(()=>{this.noise2D&&this.requestCook()})}}),this.addParm(`scale`,{value:.01,params:{min:.001,max:1,step:.001},displayName:`Scale`,onChange:()=>this.requestCook()}),this.addParm(`iterations`,{value:4,params:{min:1,max:8,step:1,integer:!0},displayName:`Iterations`,onChange:()=>this.requestCook()}),this.addResolutionParm(),this.output=this.out(`image`),this.onReady=async()=>{await this.initNoise(),this.noise2D&&this.requestCook()}}async initNoise(){try{let{createNoise2D:e}=await this.require(`simplex-noise`),t=this.props.seed.value,n=t;function r(){return n=(n*9301+49297)%233280,n/233280}this.noise2D=e(r),this.currentSeed=t}catch(e){console.error(`Failed to load simplex-noise:`,e),this.error=e}}render(){if(!this.noise2D){this.initNoise().then(()=>{this.noise2D&&this.requestCook()});return}let[e,t]=this.getResolution(),n=this.props.scale.value,r=this.props.iterations.value;if(this.props.seed.value!==this.currentSeed){this.initNoise().then(()=>{this.noise2D&&this.requestCook()});return}let a=i.grayscale(e,t),o=a.r();for(let i=0;i<t;i++)for(let t=0;t<e;t++){let a=0,s=1,c=n,l=0;for(let e=0;e<r;e++){let e=t*c,n=i*c,r=this.noise2D(e,n);a+=r*s,l+=s,s*=.5,c*=2}a=(a/l+1)*.5,o[i*e+t]=a}a.markDirty(),this.setOutput(this.output,a)}},h=class extends o{constructor(e,t){super(e,`Blur`,t)}setup(){this.image=this.in(`image`,null),this.addParm(`method`,{value:`box`,params:{options:[{value:`box`,label:`Box (Fast)`},{value:`gaussian`,label:`Gaussian (Accurate)`}]},displayName:`Method`,onChange:()=>this.requestCook()}),this.addParm(`radius`,{value:5,params:{min:0,max:100,step:.1},displayName:`Radius`,onChange:()=>this.requestCook()}),this.addParm(`wrapEdges`,{value:!1,type:`boolean`,displayName:`Wrap Edges`,onChange:()=>this.requestCook()}),this.output=this.out(`image`),this.image.onChange=()=>this.requestCook(),this.onReady=()=>this.requestCook()}boxBlur(e,t,n){if(t<=0)return e.clone();let{width:r,height:i}=e,a=t,o=this.boxesForGauss(a,3),s=e.emptyClone();for(let t=0;t<e.channelCount;t++){let a=new Float32Array(e.channels[t]);for(let e of o)this.boxBlurPass(a,r,i,e,n);s.channels[t].set(a)}return s}boxesForGauss(e,t){let n=Math.sqrt(12*e*e/t+1),r=Math.floor(n);r%2==0&&r--;let i=r+2,a=(12*e*e-t*r*r-4*t*r-3*t)/(-4*r-4),o=Math.round(a),s=[];for(let e=0;e<t;e++)s.push(e<o?r:i);return s.map(e=>(e-1)/2)}boxBlurPass(e,t,n,r,i){if(r<1)return;let a=new Float32Array(t*n),o=Math.floor(r),s=1/(o*2+1);for(let r=0;r<n;r++){let n=r*t,c=0;for(let r=-o;r<=o;r++){let a=i?(r%t+t)%t:Math.max(0,Math.min(t-1,r));c+=e[n+a]}a[n]=c*s;for(let r=1;r<t;r++){let l=i?((r-o-1)%t+t)%t:Math.max(0,r-o-1),u=i?((r+o)%t+t)%t:Math.min(t-1,r+o);c-=e[n+l],c+=e[n+u],a[n+r]=c*s}}for(let r=0;r<t;r++){let c=0;for(let e=-o;e<=o;e++){let o=i?(e%n+n)%n:Math.max(0,Math.min(n-1,e));c+=a[o*t+r]}e[r]=c*s;for(let l=1;l<n;l++){let u=i?((l-o-1)%n+n)%n:Math.max(0,l-o-1),d=i?((l+o)%n+n)%n:Math.min(n-1,l+o);c-=a[u*t+r],c+=a[d*t+r],e[l*t+r]=c*s}}}trueGaussianBlur(e,t,n){if(t<=0)return e.clone();let{width:r,height:i}=e,a=e.emptyClone(),o=Math.ceil(t*3)*2+1,s=new Float32Array(o),c=t/3,l=2*c*c,u=0,d=o-1>>1;for(let e=0;e<o;e++){let t=e-d,n=Math.exp(-(t*t)/l);s[e]=n,u+=n}let f=1/u;for(let e=0;e<o;e++)s[e]*=f;let p=n?e=>(e%r+r)%r:e=>e<0?0:e>=r?r-1:e,m=n?e=>(e%i+i)%i:e=>e<0?0:e>=i?i-1:e;for(let t=0;t<e.channelCount;t++){let n=e.channels[t],c=a.channels[t],l=new Float32Array(r*i);for(let e=0;e<i;e++){let t=e*r;for(let e=0;e<r;e++){let r=0;for(let i=0;i<o;i++){let a=p(e+i-d);r+=n[t+a]*s[i]}l[t+e]=r}}for(let e=0;e<r;e++)for(let t=0;t<i;t++){let n=0;for(let i=0;i<o;i++){let a=m(t+i-d);n+=l[a*r+e]*s[i]}c[t*r+e]=n}}return a}render(){let e=this.toImageBuffer(this.image.value);if(!e)return;let t=this.props.method.value,n=this.props.radius.value,r=this.props.wrapEdges.value,i=t===`gaussian`?this.trueGaussianBlur(e,n,r):this.boxBlur(e,n,r);this.setOutput(this.output,i)}},g=class extends o{constructor(e,t){super(e,`Text`,t)}setup(){this.addParm(`text`,{value:`Hello, World!`,type:`textarea`,displayName:`Text`}),this.addParm(`font`,{value:`Inter, system-ui, sans-serif`,type:`text`,displayName:`Font`}),this.addParm(`fontSize`,{value:48,type:`slider`,params:{min:1,max:500,step:1},displayName:`Font Size`}),this.addParm(`fontWeight`,{value:`normal`,type:`select`,params:{options:[{value:`normal`,label:`Normal`},{value:`bold`,label:`Bold`},{value:`100`,label:`Thin (100)`},{value:`200`,label:`Extra Light (200)`},{value:`300`,label:`Light (300)`},{value:`500`,label:`Medium (500)`},{value:`600`,label:`Semi Bold (600)`},{value:`700`,label:`Bold (700)`},{value:`800`,label:`Extra Bold (800)`},{value:`900`,label:`Black (900)`}]},displayName:`Font Weight`}),this.addParm(`fontStyle`,{value:`normal`,type:`select`,params:{options:[{value:`normal`,label:`Normal`},{value:`italic`,label:`Italic`},{value:`oblique`,label:`Oblique`}]},displayName:`Font Style`}),this.addParm(`alignH`,{value:`center`,type:`select`,params:{options:[{value:`left`,label:`Left`},{value:`center`,label:`Center`},{value:`right`,label:`Right`}]},displayName:`Horizontal Align`}),this.addParm(`alignV`,{value:`middle`,type:`select`,params:{options:[{value:`top`,label:`Top`},{value:`middle`,label:`Middle`},{value:`bottom`,label:`Bottom`}]},displayName:`Vertical Align`}),this.addParm(`color`,{value:{r:1,g:1,b:1},type:`color`,displayName:`Color`}),this.addParm(`backgroundColor`,{value:{r:0,g:0,b:0,a:0},type:`color`,displayName:`Background`}),this.addResolutionParm(),this.addParm(`offset`,{value:[0,0],params:{min:[-2048,-2048],max:[2048,2048],step:1},displayName:`Offset`}),this.addParm(`rotation`,{value:0,type:`slider`,params:{min:-180,max:180,step:.1},displayName:`Rotation`}),this.addParm(`letterSpacing`,{value:0,type:`slider`,params:{min:-50,max:100,step:.5},displayName:`Letter Spacing`}),this.addParm(`lineHeight`,{value:1.2,type:`slider`,params:{min:.5,max:3,step:.05},displayName:`Line Height`}),this.addParm(`opacity`,{value:1,type:`slider`,params:{min:0,max:1,step:.01},displayName:`Opacity`}),this.output=this.out(`image`),[`text`,`font`,`fontSize`,`fontWeight`,`fontStyle`,`alignH`,`alignV`,`color`,`backgroundColor`,`offset`,`rotation`,`letterSpacing`,`lineHeight`,`opacity`].forEach(e=>this.watchProp(e,()=>this.requestCook())),this.onReady=()=>this.requestCook()}render(){let[e,t]=this.getResolution(),a=this.props.text.value,o=this.props.font.value,s=this.props.fontSize.value,c=this.props.fontWeight.value,l=this.props.fontStyle.value,u=this.props.alignH.value,d=this.props.alignV.value,f=n(this.props.color.value),p=n(this.props.backgroundColor.value),[m,h]=this.props.offset.value,g=this.props.rotation.value,_=this.props.letterSpacing.value,v=this.props.lineHeight.value,y=this.props.opacity.value,b=document.createElement(`canvas`);b.width=e,b.height=t;let x=b.getContext(`2d`);if(!x){this.setOutput(this.output,i.rgba(e,t));return}p.a>0?(x.fillStyle=r(p),x.fillRect(0,0,e,t)):x.clearRect(0,0,e,t),x.save(),x.globalAlpha=y,x.font=`${l} ${c} ${s}px ${o}`,x.fillStyle=r(f);let S=a.split(`
2
+ `),C=s*v,w=S.length*C,T,E;switch(u){case`left`:x.textAlign=`left`,T=0;break;case`right`:x.textAlign=`right`,T=e;break;default:x.textAlign=`center`,T=e/2}switch(d){case`top`:E=s;break;case`bottom`:E=t-w+s;break;default:E=(t-w)/2+s}if(T+=m,E+=h,g!==0){let e=g*Math.PI/180;x.translate(T,E-s/2+w/2),x.rotate(e),x.translate(-T,-(E-s/2+w/2))}if(_!==0){x.textAlign=`left`;for(let e=0;e<S.length;e++){let t=S[e],n=E+e*C,r=0;for(let e of t)r+=x.measureText(e).width+_;r-=_;let i;switch(u){case`left`:i=T;break;case`right`:i=T-r;break;default:i=T-r/2}for(let e of t)x.fillText(e,i,n),i+=x.measureText(e).width+_}}else for(let e=0;e<S.length;e++){let t=E+e*C;x.fillText(S[e],T,t)}x.restore();let D=i.fromCanvas(b);this.setOutput(this.output,D)}},_=class extends o{constructor(e,t){super(e,`Transform`,t)}setup(){this.imageInput=this.in(`image`,null),this.addParm(`translate`,{value:[0,0],params:{min:[-2048,-2048],max:[2048,2048],step:1},displayName:`Translate`,onChange:()=>this.requestCook()}),this.addParm(`rotation`,{value:0,params:{min:-180,max:180,step:.1},displayName:`Rotation`,onChange:()=>this.requestCook()}),this.addParm(`scale`,{value:[1,1],params:{min:[.01,.01],max:[10,10],step:.01},displayName:`Scale`,onChange:()=>this.requestCook()}),this.addParm(`uniformScale`,{value:!0,type:`boolean`,displayName:`Uniform Scale`,onChange:()=>this.requestCook()}),this.addParm(`pivot`,{value:`center`,params:{options:[{value:`center`,label:`Center`},{value:`top-left`,label:`Top Left`},{value:`top-right`,label:`Top Right`},{value:`bottom-left`,label:`Bottom Left`},{value:`bottom-right`,label:`Bottom Right`},{value:`custom`,label:`Custom`}]},displayName:`Pivot`,onChange:()=>this.requestCook()}),this.addParm(`customPivot`,{value:[.5,.5],params:{min:[0,0],max:[1,1],step:.01},displayName:`Custom Pivot`,hidden:()=>this.props.pivot.value!==`custom`,onChange:()=>this.requestCook()}),this.addParm(`filter`,{value:`bilinear`,params:{options:[{value:`nearest`,label:`Nearest`},{value:`bilinear`,label:`Bilinear`}]},displayName:`Filter`,onChange:()=>this.requestCook()}),this.addParm(`wrap`,{value:`clamp`,params:{options:[{value:`clamp`,label:`Clamp`},{value:`repeat`,label:`Repeat`},{value:`mirror`,label:`Mirror`},{value:`transparent`,label:`Transparent`}]},displayName:`Wrap Mode`,onChange:()=>this.requestCook()}),this.output=this.out(`image`),this.imageInput.onChange=()=>this.requestCook(),this.onReady=()=>this.requestCook()}getPivotPoint(e,t){switch(this.props.pivot.value){case`center`:return[e/2,t/2];case`top-left`:return[0,0];case`top-right`:return[e,0];case`bottom-left`:return[0,t];case`bottom-right`:return[e,t];case`custom`:let[n,r]=this.props.customPivot.value;return[n*e,r*t];default:return[e/2,t/2]}}wrapCoord(e,t,n){if(n===`clamp`)return Math.max(0,Math.min(t-1,e));if(n===`repeat`)return e%=t,e<0&&(e+=t),e;if(n===`mirror`){let n=t*2;return e=Math.abs(e%n),e>=t&&(e=n-e-1),Math.max(0,Math.min(t-1,e))}return e}render(){let e=this.toImageBuffer(this.imageInput.value);if(!e)return;let t=e.width,n=e.height,[r,a]=this.props.translate.value,o=this.props.rotation.value*Math.PI/180,[s,c]=this.props.scale.value,l=this.props.uniformScale.value,u=this.props.filter.value,d=this.props.wrap.value;l&&(c=s);let[f,p]=this.getPivotPoint(t,n),m=Math.cos(-o),h=Math.sin(-o),g=1/s,_=1/c,v=i.rgba(t,n),y=Math.min(e.channelCount,4);for(let i=0;i<n;i++)for(let o=0;o<t;o++){let s=o-f-r,c=i-p-a,l=s*m-c*h,b=s*h+c*m,x=l*g+f,S=b*_+p;if(d===`transparent`&&(x<0||x>=t||S<0||S>=n)){let e=i*t+o;for(let t=0;t<4;t++)v.channels[t][e]=0;continue}let C=i*t+o;if(u===`nearest`){let r=Math.round(x),i=Math.round(S),a=this.wrapCoord(r,t,d),o=this.wrapCoord(i,n,d)*t+a;for(let t=0;t<y;t++)v.channels[t][C]=e.channels[t][o];y<4&&(v.channels[3][C]=1)}else{let r=Math.floor(x),i=Math.floor(S),a=r+1,o=i+1,s=x-r,c=S-i,l=this.wrapCoord(r,t,d),u=this.wrapCoord(a,t,d),f=this.wrapCoord(i,n,d),p=this.wrapCoord(o,n,d),m=f*t+l,h=f*t+u,g=p*t+l,_=p*t+u;for(let t=0;t<y;t++){let n=e.channels[t][m],r=e.channels[t][h],i=e.channels[t][g],a=e.channels[t][_],o=n+(r-n)*s,l=i+(a-i)*s;v.channels[t][C]=o+(l-o)*c}y<4&&(v.channels[3][C]=1)}}v.markDirty(),this.setOutput(this.output,v)}},v=`/**
3
+ * ColorNode - creates a solid color ImageBuffer
4
+ */
5
+
6
+ import { ImageNodeBase, ImageBuffer } from '../ImageNodeBase';
7
+ import type { Graph } from '@/nodes/Graph';
8
+ import type { OutputPort } from '@/types/node.types';
9
+ import { normalizeColor } from '@/utils/colorUtils';
10
+
11
+ export class ColorNode extends ImageNodeBase {
12
+ private output!: OutputPort<ImageBuffer>;
13
+
14
+ constructor(id: string, graph: Graph) {
15
+ super(id, 'Color', graph);
16
+ }
17
+
18
+ protected setup(): void {
19
+ this.addParm('color', {
20
+ value: { r: 1.0, g: 1.0, b: 1.0 },
21
+ type: 'color',
22
+ displayName: 'Color'
23
+ });
24
+
25
+ this.addResolutionParm();
26
+
27
+ this.output = this.out('image');
28
+
29
+ this.watchProp('color', () => this.requestCook());
30
+
31
+ this.onReady = () => this.requestCook();
32
+ }
33
+
34
+ protected render(): void {
35
+ const [width, height] = this.getResolution();
36
+ const color = normalizeColor(this.props.color.value);
37
+
38
+ const buffer = ImageBuffer.solid(width, height, color);
39
+ this.setOutput(this.output, buffer);
40
+ }
41
+ }
42
+ `,y=`/**
43
+ * ImageNode - loads an image file or accepts image input
44
+ * Converts to ImageBuffer for the processing pipeline
45
+ */
46
+
47
+ import { ImageNodeBase, ImageBuffer, type ImageInput } from '../ImageNodeBase';
48
+ import type { Graph } from '@/nodes/Graph';
49
+ import type { InputPort, OutputPort } from '@/types/node.types';
50
+
51
+ type ImageInputValue = HTMLCanvasElement | HTMLImageElement | ImageBuffer | string | null;
52
+
53
+ export class ImageNode extends ImageNodeBase {
54
+ private imageInput!: InputPort<ImageInputValue>;
55
+ private output!: OutputPort<ImageBuffer>;
56
+
57
+ constructor(id: string, graph: Graph) {
58
+ super(id, 'File', graph);
59
+ }
60
+
61
+ protected setup(): void {
62
+ this.imageInput = this.in<ImageInputValue>('image', null);
63
+
64
+ this.addParm('file', {
65
+ value: '',
66
+ type: 'image',
67
+ params: {
68
+ accept: 'image/*'
69
+ },
70
+ displayName: 'File',
71
+ onChange: () => {
72
+ if (this.props.file.value && !this.imageInput.value) {
73
+ this.requestCook();
74
+ }
75
+ }
76
+ });
77
+
78
+ this.addParm('resolutionMode', {
79
+ value: 'original',
80
+ params: {
81
+ options: [
82
+ { value: 'original', label: 'Original' },
83
+ { value: 'max', label: 'Max Resolution' },
84
+ { value: 'fixed', label: 'Fixed Resolution' }
85
+ ]
86
+ },
87
+ displayName: 'Resolution Mode',
88
+ onChange: () => {
89
+ if (this.props.file.value || this.imageInput.value) {
90
+ this.requestCook();
91
+ }
92
+ }
93
+ });
94
+
95
+ this.addParm('maxResolution', {
96
+ value: [2048, 2048],
97
+ params: {
98
+ min: [1, 1],
99
+ max: [4096, 4096]
100
+ },
101
+ displayName: 'Max Resolution',
102
+ hidden: () => this.props.resolutionMode.value !== 'max',
103
+ onChange: () => {
104
+ if ((this.props.file.value || this.imageInput.value) && this.props.resolutionMode.value === 'max') {
105
+ this.requestCook();
106
+ }
107
+ }
108
+ });
109
+
110
+ this.addParm('fixedResolution', {
111
+ value: [512, 512],
112
+ params: {
113
+ min: [1, 1],
114
+ max: [4096, 4096]
115
+ },
116
+ displayName: 'Fixed Resolution',
117
+ hidden: () => this.props.resolutionMode.value !== 'fixed',
118
+ onChange: () => {
119
+ if ((this.props.file.value || this.imageInput.value) && this.props.resolutionMode.value === 'fixed') {
120
+ this.requestCook();
121
+ }
122
+ }
123
+ });
124
+
125
+ this.output = this.out('image');
126
+
127
+ this.imageInput.onChange = () => this.requestCook();
128
+
129
+ this.onReady = () => {
130
+ if (this.props.file.value || this.imageInput.value) {
131
+ this.requestCook();
132
+ }
133
+ };
134
+ }
135
+
136
+ protected async render(): Promise<void> {
137
+ let sourceBuffer: ImageBuffer | null = null;
138
+
139
+ // First check input port
140
+ if (this.imageInput.value) {
141
+ if (this.imageInput.value instanceof ImageBuffer) {
142
+ sourceBuffer = this.imageInput.value;
143
+ } else if (this.imageInput.value instanceof HTMLImageElement ||
144
+ this.imageInput.value instanceof HTMLCanvasElement) {
145
+ sourceBuffer = ImageBuffer.fromCanvas(this.imageInput.value);
146
+ } else if (typeof this.imageInput.value === 'string') {
147
+ const img = new Image();
148
+ img.crossOrigin = 'anonymous';
149
+ await new Promise<void>((resolve, reject) => {
150
+ img.onload = () => resolve();
151
+ img.onerror = reject;
152
+ img.src = this.imageInput.value as string;
153
+ });
154
+ sourceBuffer = ImageBuffer.fromCanvas(img);
155
+ }
156
+ } else if (this.props.file.value) {
157
+ const img = new Image();
158
+ img.crossOrigin = 'anonymous';
159
+ await new Promise<void>((resolve, reject) => {
160
+ img.onload = () => resolve();
161
+ img.onerror = reject;
162
+ img.src = this.props.file.value;
163
+ });
164
+ sourceBuffer = ImageBuffer.fromCanvas(img);
165
+ }
166
+
167
+ if (!sourceBuffer) {
168
+ return;
169
+ }
170
+
171
+ try {
172
+ const mode = this.props.resolutionMode.value;
173
+ let outputBuffer: ImageBuffer;
174
+
175
+ if (mode === 'original') {
176
+ outputBuffer = sourceBuffer;
177
+ } else if (mode === 'max') {
178
+ const [maxWidth, maxHeight] = this.props.maxResolution.value;
179
+ const aspectRatio = sourceBuffer.width / sourceBuffer.height;
180
+ let newWidth = sourceBuffer.width;
181
+ let newHeight = sourceBuffer.height;
182
+
183
+ if (newWidth > maxWidth) {
184
+ newWidth = maxWidth;
185
+ newHeight = maxWidth / aspectRatio;
186
+ }
187
+ if (newHeight > maxHeight) {
188
+ newHeight = maxHeight;
189
+ newWidth = maxHeight * aspectRatio;
190
+ }
191
+
192
+ newWidth = Math.round(newWidth);
193
+ newHeight = Math.round(newHeight);
194
+
195
+ if (newWidth !== sourceBuffer.width || newHeight !== sourceBuffer.height) {
196
+ outputBuffer = this.resizeBuffer(sourceBuffer, newWidth, newHeight);
197
+ } else {
198
+ outputBuffer = sourceBuffer;
199
+ }
200
+ } else {
201
+ const [width, height] = this.props.fixedResolution.value;
202
+ outputBuffer = this.resizeBuffer(sourceBuffer, width, height);
203
+ }
204
+
205
+ this.setOutput(this.output, outputBuffer);
206
+ } catch (error) {
207
+ this.error = error as Error;
208
+ console.error('Failed to process image:', error);
209
+ }
210
+ }
211
+
212
+ private resizeBuffer(source: ImageBuffer, newWidth: number, newHeight: number): ImageBuffer {
213
+ const result = ImageBuffer.rgba(newWidth, newHeight);
214
+ const scaleX = source.width / newWidth;
215
+ const scaleY = source.height / newHeight;
216
+
217
+ for (let c = 0; c < Math.min(source.channelCount, 4); c++) {
218
+ const dstChannel = result.channels[c];
219
+
220
+ for (let y = 0; y < newHeight; y++) {
221
+ for (let x = 0; x < newWidth; x++) {
222
+ const srcX = x * scaleX;
223
+ const srcY = y * scaleY;
224
+ dstChannel[y * newWidth + x] = source.sample(srcX, srcY, c);
225
+ }
226
+ }
227
+ }
228
+
229
+ if (source.channelCount < 4) {
230
+ result.fill(3, 1);
231
+ }
232
+
233
+ return result;
234
+ }
235
+ }
236
+ `,b=`/**
237
+ * CompositeNode - blends two ImageBuffers together
238
+ * Supports resolution control similar to TouchDesigner TOPs
239
+ */
240
+
241
+ import { ImageNodeBase, ImageBuffer, type ImageInput, type ResolutionMode, type FitMode } from '../ImageNodeBase';
242
+ import type { Graph } from '@/nodes/Graph';
243
+ import type { InputPort, OutputPort } from '@/types/node.types';
244
+
245
+ export class CompositeNode extends ImageNodeBase {
246
+ private image1!: InputPort<ImageInput>;
247
+ private image2!: InputPort<ImageInput>;
248
+ private output!: OutputPort<ImageBuffer>;
249
+
250
+ constructor(id: string, graph: Graph) {
251
+ super(id, 'Composite', graph);
252
+ }
253
+
254
+ protected setup(): void {
255
+ this.image1 = this.in<ImageInput>('image1', null);
256
+ this.image2 = this.in<ImageInput>('image2', null);
257
+
258
+ this.addParm('blendMode', {
259
+ value: 'multiply',
260
+ params: {
261
+ options: [
262
+ { value: 'normal', label: 'Normal' },
263
+ { value: 'multiply', label: 'Multiply' },
264
+ { value: 'screen', label: 'Screen' },
265
+ { value: 'overlay', label: 'Overlay' },
266
+ { value: 'darken', label: 'Darken' },
267
+ { value: 'lighten', label: 'Lighten' },
268
+ { value: 'color-dodge', label: 'Color Dodge' },
269
+ { value: 'color-burn', label: 'Color Burn' },
270
+ { value: 'hard-light', label: 'Hard Light' },
271
+ { value: 'soft-light', label: 'Soft Light' },
272
+ { value: 'difference', label: 'Difference' },
273
+ { value: 'exclusion', label: 'Exclusion' },
274
+ { value: 'add', label: 'Add' },
275
+ { value: 'subtract', label: 'Subtract' },
276
+ { value: 'divide', label: 'Divide' },
277
+ { value: 'pin-light', label: 'Pin Light' },
278
+ { value: 'vivid-light', label: 'Vivid Light' },
279
+ { value: 'linear-dodge', label: 'Linear Dodge' },
280
+ { value: 'linear-burn', label: 'Linear Burn' }
281
+ ]
282
+ },
283
+ displayName: 'Blend Mode',
284
+ onChange: () => this.requestCook()
285
+ });
286
+
287
+ this.addParm('opacity', {
288
+ value: 1.0,
289
+ params: {
290
+ min: 0.0,
291
+ max: 1.0,
292
+ step: 0.01
293
+ },
294
+ displayName: 'Opacity',
295
+ onChange: () => this.requestCook()
296
+ });
297
+
298
+ // Resolution control - which input determines output size
299
+ this.addParm('outputResolution', {
300
+ value: 'input1',
301
+ params: {
302
+ options: [
303
+ { value: 'input1', label: 'Use Input 1' },
304
+ { value: 'input2', label: 'Use Input 2' },
305
+ { value: 'largest', label: 'Largest Input' },
306
+ { value: 'smallest', label: 'Smallest Input' },
307
+ { value: 'custom', label: 'Custom' }
308
+ ]
309
+ },
310
+ displayName: 'Output Resolution',
311
+ onChange: () => this.requestCook()
312
+ });
313
+
314
+ this.addParm('customResolution', {
315
+ value: [512, 512],
316
+ params: {
317
+ min: [1, 1],
318
+ max: [4096, 4096],
319
+ integer: true
320
+ },
321
+ displayName: 'Resolution',
322
+ hidden: () => this.props.outputResolution.value !== 'custom',
323
+ onChange: () => this.requestCook()
324
+ });
325
+
326
+ // How to fit inputs that don't match output resolution
327
+ this.addParm('fitMode', {
328
+ value: 'fill',
329
+ params: {
330
+ options: [
331
+ { value: 'fill', label: 'Fill (Crop)' },
332
+ { value: 'fit', label: 'Fit (Letterbox)' },
333
+ { value: 'stretch', label: 'Stretch' },
334
+ { value: 'native', label: 'Native (No Scale)' }
335
+ ]
336
+ },
337
+ displayName: 'Fit Mode',
338
+ onChange: () => this.requestCook()
339
+ });
340
+
341
+ this.output = this.out('image');
342
+
343
+ this.image1.onChange = () => this.requestCook();
344
+ this.image2.onChange = () => this.requestCook();
345
+
346
+ this.onReady = () => this.requestCook();
347
+ }
348
+
349
+ private blendBuffers(base: ImageBuffer, blend: ImageBuffer, mode: string, opacity: number): ImageBuffer {
350
+ const width = base.width;
351
+ const height = base.height;
352
+ const result = ImageBuffer.rgba(width, height);
353
+
354
+ // Ensure both have RGBA
355
+ const baseRGBA = base.channelCount >= 4 ? base : base.toRGBA();
356
+ const blendRGBA = blend.channelCount >= 4 ? blend : blend.toRGBA();
357
+
358
+ const r1 = baseRGBA.channels[0];
359
+ const g1 = baseRGBA.channels[1];
360
+ const b1 = baseRGBA.channels[2];
361
+ const a1 = baseRGBA.channels[3];
362
+
363
+ const r2 = blendRGBA.channels[0];
364
+ const g2 = blendRGBA.channels[1];
365
+ const b2 = blendRGBA.channels[2];
366
+ const a2 = blendRGBA.channels[3];
367
+
368
+ const rOut = result.channels[0];
369
+ const gOut = result.channels[1];
370
+ const bOut = result.channels[2];
371
+ const aOut = result.channels[3];
372
+
373
+ for (let i = 0; i < width * height; i++) {
374
+ const baseR = r1[i], baseG = g1[i], baseB = b1[i], baseA = a1[i];
375
+ const blendR = r2[i], blendG = g2[i], blendB = b2[i], blendA = a2[i];
376
+
377
+ let r: number, g: number, b: number;
378
+
379
+ switch (mode) {
380
+ case 'normal':
381
+ r = blendR; g = blendG; b = blendB;
382
+ break;
383
+ case 'multiply':
384
+ r = baseR * blendR; g = baseG * blendG; b = baseB * blendB;
385
+ break;
386
+ case 'screen':
387
+ r = 1 - (1 - baseR) * (1 - blendR);
388
+ g = 1 - (1 - baseG) * (1 - blendG);
389
+ b = 1 - (1 - baseB) * (1 - blendB);
390
+ break;
391
+ case 'overlay':
392
+ r = baseR < 0.5 ? 2 * baseR * blendR : 1 - 2 * (1 - baseR) * (1 - blendR);
393
+ g = baseG < 0.5 ? 2 * baseG * blendG : 1 - 2 * (1 - baseG) * (1 - blendG);
394
+ b = baseB < 0.5 ? 2 * baseB * blendB : 1 - 2 * (1 - baseB) * (1 - blendB);
395
+ break;
396
+ case 'darken':
397
+ r = Math.min(baseR, blendR); g = Math.min(baseG, blendG); b = Math.min(baseB, blendB);
398
+ break;
399
+ case 'lighten':
400
+ r = Math.max(baseR, blendR); g = Math.max(baseG, blendG); b = Math.max(baseB, blendB);
401
+ break;
402
+ case 'color-dodge':
403
+ r = blendR === 1 ? 1 : Math.min(1, baseR / (1 - blendR));
404
+ g = blendG === 1 ? 1 : Math.min(1, baseG / (1 - blendG));
405
+ b = blendB === 1 ? 1 : Math.min(1, baseB / (1 - blendB));
406
+ break;
407
+ case 'color-burn':
408
+ r = blendR === 0 ? 0 : Math.max(0, 1 - (1 - baseR) / blendR);
409
+ g = blendG === 0 ? 0 : Math.max(0, 1 - (1 - baseG) / blendG);
410
+ b = blendB === 0 ? 0 : Math.max(0, 1 - (1 - baseB) / blendB);
411
+ break;
412
+ case 'hard-light':
413
+ r = blendR < 0.5 ? 2 * baseR * blendR : 1 - 2 * (1 - baseR) * (1 - blendR);
414
+ g = blendG < 0.5 ? 2 * baseG * blendG : 1 - 2 * (1 - baseG) * (1 - blendG);
415
+ b = blendB < 0.5 ? 2 * baseB * blendB : 1 - 2 * (1 - baseB) * (1 - blendB);
416
+ break;
417
+ case 'soft-light':
418
+ r = blendR < 0.5 ? baseR - (1 - 2 * blendR) * baseR * (1 - baseR) : baseR + (2 * blendR - 1) * (Math.sqrt(baseR) - baseR);
419
+ g = blendG < 0.5 ? baseG - (1 - 2 * blendG) * baseG * (1 - baseG) : baseG + (2 * blendG - 1) * (Math.sqrt(baseG) - baseG);
420
+ b = blendB < 0.5 ? baseB - (1 - 2 * blendB) * baseB * (1 - baseB) : baseB + (2 * blendB - 1) * (Math.sqrt(baseB) - baseB);
421
+ break;
422
+ case 'difference':
423
+ r = Math.abs(baseR - blendR); g = Math.abs(baseG - blendG); b = Math.abs(baseB - blendB);
424
+ break;
425
+ case 'exclusion':
426
+ r = baseR + blendR - 2 * baseR * blendR;
427
+ g = baseG + blendG - 2 * baseG * blendG;
428
+ b = baseB + blendB - 2 * baseB * blendB;
429
+ break;
430
+ case 'add':
431
+ r = Math.min(1, baseR + blendR); g = Math.min(1, baseG + blendG); b = Math.min(1, baseB + blendB);
432
+ break;
433
+ case 'subtract':
434
+ r = Math.max(0, baseR - blendR); g = Math.max(0, baseG - blendG); b = Math.max(0, baseB - blendB);
435
+ break;
436
+ case 'divide':
437
+ r = blendR === 0 ? 1 : Math.min(1, baseR / blendR);
438
+ g = blendG === 0 ? 1 : Math.min(1, baseG / blendG);
439
+ b = blendB === 0 ? 1 : Math.min(1, baseB / blendB);
440
+ break;
441
+ case 'pin-light':
442
+ r = blendR < 0.5 ? Math.min(baseR, 2 * blendR) : Math.max(baseR, 2 * (blendR - 0.5));
443
+ g = blendG < 0.5 ? Math.min(baseG, 2 * blendG) : Math.max(baseG, 2 * (blendG - 0.5));
444
+ b = blendB < 0.5 ? Math.min(baseB, 2 * blendB) : Math.max(baseB, 2 * (blendB - 0.5));
445
+ break;
446
+ case 'vivid-light':
447
+ r = blendR < 0.5 ? (blendR === 0 ? 0 : 1 - (1 - baseR) / (2 * blendR)) : (blendR === 1 ? 1 : baseR / (2 * (1 - blendR)));
448
+ g = blendG < 0.5 ? (blendG === 0 ? 0 : 1 - (1 - baseG) / (2 * blendG)) : (blendG === 1 ? 1 : baseG / (2 * (1 - blendG)));
449
+ b = blendB < 0.5 ? (blendB === 0 ? 0 : 1 - (1 - baseB) / (2 * blendB)) : (blendB === 1 ? 1 : baseB / (2 * (1 - blendB)));
450
+ break;
451
+ case 'linear-dodge':
452
+ r = Math.min(1, baseR + blendR); g = Math.min(1, baseG + blendG); b = Math.min(1, baseB + blendB);
453
+ break;
454
+ case 'linear-burn':
455
+ r = Math.max(0, baseR + blendR - 1); g = Math.max(0, baseG + blendG - 1); b = Math.max(0, baseB + blendB - 1);
456
+ break;
457
+ default:
458
+ r = blendR; g = blendG; b = blendB;
459
+ }
460
+
461
+ // Apply opacity
462
+ rOut[i] = baseR + (r - baseR) * opacity * blendA;
463
+ gOut[i] = baseG + (g - baseG) * opacity * blendA;
464
+ bOut[i] = baseB + (b - baseB) * opacity * blendA;
465
+ aOut[i] = baseA + (blendA - baseA) * opacity;
466
+ }
467
+
468
+ return result;
469
+ }
470
+
471
+ protected render(): void {
472
+ // Get resolution settings
473
+ const resolutionMode = this.props.outputResolution.value as ResolutionMode;
474
+ const fitMode = this.props.fitMode.value as FitMode;
475
+ const customSize = this.props.customResolution.value as [number, number];
476
+
477
+ // Prepare inputs - handles resolution matching
478
+ const { buffers } = this.prepareInputs(
479
+ [this.image1.value, this.image2.value],
480
+ resolutionMode,
481
+ fitMode,
482
+ customSize
483
+ );
484
+
485
+ const [buf1, buf2] = buffers;
486
+
487
+ if (!buf1 || !buf2) {
488
+ return;
489
+ }
490
+
491
+ const blendMode = this.props.blendMode.value;
492
+ const opacity = this.props.opacity.value;
493
+
494
+ try {
495
+ const result = this.blendBuffers(buf1, buf2, blendMode, opacity);
496
+ this.setOutput(this.output, result);
497
+ } catch (error) {
498
+ console.error('Composite render error:', error);
499
+ this.error = error as Error;
500
+ }
501
+ }
502
+ }
503
+ `,x=`/**
504
+ * CheckersNode - generates a checkerboard pattern as ImageBuffer
505
+ */
506
+
507
+ import { ImageNodeBase, ImageBuffer } from '../ImageNodeBase';
508
+ import type { Graph } from '@/nodes/Graph';
509
+ import type { OutputPort } from '@/types/node.types';
510
+ import { normalizeColor } from '@/utils/colorUtils';
511
+
512
+ export class CheckersNode extends ImageNodeBase {
513
+ private output!: OutputPort<ImageBuffer>;
514
+
515
+ constructor(id: string, graph: Graph) {
516
+ super(id, 'Checkers', graph);
517
+ }
518
+
519
+ protected setup(): void {
520
+ this.addParm('color1', {
521
+ value: { r: 1.0, g: 1.0, b: 1.0 },
522
+ type: 'color',
523
+ displayName: 'Color 1',
524
+ onChange: () => this.requestCook()
525
+ });
526
+
527
+ this.addParm('color2', {
528
+ value: { r: 0.0, g: 0.0, b: 0.0 },
529
+ type: 'color',
530
+ displayName: 'Color 2',
531
+ onChange: () => this.requestCook()
532
+ });
533
+
534
+ this.addParm('mode', {
535
+ value: 'size',
536
+ params: {
537
+ options: [
538
+ { value: 'size', label: 'Size' },
539
+ { value: 'divisions', label: 'Divisions' }
540
+ ]
541
+ },
542
+ displayName: 'Mode',
543
+ onChange: () => this.requestCook()
544
+ });
545
+
546
+ this.addParm('size', {
547
+ value: 32,
548
+ params: {
549
+ min: 1,
550
+ max: 512,
551
+ step: 0.1
552
+ },
553
+ displayName: 'Size',
554
+ hidden: () => this.props.mode.value !== 'size',
555
+ onChange: () => this.requestCook()
556
+ });
557
+
558
+ this.addParm('divisions', {
559
+ value: 16,
560
+ params: {
561
+ min: 1,
562
+ max: 512,
563
+ step: 1,
564
+ integer: true
565
+ },
566
+ displayName: 'Divisions',
567
+ hidden: () => this.props.mode.value !== 'divisions',
568
+ onChange: () => this.requestCook()
569
+ });
570
+
571
+ this.addParm('centered', {
572
+ value: false,
573
+ type: 'boolean',
574
+ displayName: 'Centered',
575
+ onChange: () => this.requestCook()
576
+ });
577
+
578
+ this.addResolutionParm();
579
+
580
+ this.output = this.out('image');
581
+
582
+ this.onReady = () => this.requestCook();
583
+ }
584
+
585
+ protected render(): void {
586
+ const [width, height] = this.getResolution();
587
+ const mode = this.props.mode.value;
588
+ const color1 = normalizeColor(this.props.color1.value);
589
+ const color2 = normalizeColor(this.props.color2.value);
590
+ const centered = this.props.centered.value;
591
+
592
+ let checkerSize: number;
593
+ if (mode === 'size') {
594
+ checkerSize = this.props.size.value;
595
+ } else {
596
+ checkerSize = width / this.props.divisions.value;
597
+ }
598
+
599
+ // Calculate offset to center the pattern
600
+ let offsetX = 0;
601
+ let offsetY = 0;
602
+ if (centered) {
603
+ // Offset so that the center of the image is at the center of a checker cell
604
+ offsetX = (width / 2) % checkerSize - checkerSize / 2;
605
+ offsetY = (height / 2) % checkerSize - checkerSize / 2;
606
+ }
607
+
608
+ const buffer = ImageBuffer.rgba(width, height);
609
+ const r = buffer.r();
610
+ const g = buffer.g();
611
+ const b = buffer.b();
612
+ const a = buffer.a();
613
+
614
+ for (let y = 0; y < height; y++) {
615
+ for (let x = 0; x < width; x++) {
616
+ const idx = y * width + x;
617
+ const isEven = (Math.floor((x - offsetX) / checkerSize) + Math.floor((y - offsetY) / checkerSize)) % 2 === 0;
618
+ const color = isEven ? color1 : color2;
619
+
620
+ r[idx] = color.r;
621
+ g[idx] = color.g;
622
+ b[idx] = color.b;
623
+ a[idx] = color.a;
624
+ }
625
+ }
626
+
627
+ buffer.markDirty();
628
+ this.setOutput(this.output, buffer);
629
+ }
630
+ }
631
+ `,S=`/**
632
+ * ResizeNode - scales or resizes an ImageBuffer with bilinear interpolation
633
+ */
634
+
635
+ import { ImageNodeBase, ImageBuffer, type ImageInput } from '../ImageNodeBase';
636
+ import type { Graph } from '@/nodes/Graph';
637
+ import type { InputPort, OutputPort } from '@/types/node.types';
638
+
639
+ export class ResizeNode extends ImageNodeBase {
640
+ private image!: InputPort<ImageInput>;
641
+ private output!: OutputPort<ImageBuffer>;
642
+
643
+ constructor(id: string, graph: Graph) {
644
+ super(id, 'Resize', graph);
645
+ }
646
+
647
+ protected setup(): void {
648
+ this.image = this.in<ImageInput>('image', null);
649
+
650
+ this.addParm('mode', {
651
+ value: 'scale',
652
+ params: {
653
+ options: [
654
+ { value: 'scale', label: 'Scale' },
655
+ { value: 'fixed', label: 'Fixed' }
656
+ ]
657
+ },
658
+ displayName: 'Mode',
659
+ onChange: () => this.requestCook()
660
+ });
661
+
662
+ this.addParm('scale', {
663
+ value: 1.0,
664
+ params: {
665
+ min: 0.1,
666
+ max: 10.0,
667
+ step: 0.1
668
+ },
669
+ displayName: 'Scale',
670
+ hidden: () => this.props.mode.value !== 'scale',
671
+ onChange: () => this.requestCook()
672
+ });
673
+
674
+ this.addParm('width', {
675
+ value: 512,
676
+ params: {
677
+ min: 1,
678
+ max: 4096,
679
+ step: 1
680
+ },
681
+ displayName: 'Width',
682
+ hidden: () => this.props.mode.value !== 'fixed',
683
+ onChange: () => this.requestCook()
684
+ });
685
+
686
+ this.addParm('height', {
687
+ value: 512,
688
+ params: {
689
+ min: 1,
690
+ max: 4096,
691
+ step: 1
692
+ },
693
+ displayName: 'Height',
694
+ hidden: () => this.props.mode.value !== 'fixed',
695
+ onChange: () => this.requestCook()
696
+ });
697
+
698
+ this.output = this.out('image');
699
+
700
+ this.image.onChange = () => this.requestCook();
701
+
702
+ this.onReady = () => this.requestCook();
703
+ }
704
+
705
+ private resizeBuffer(source: ImageBuffer, newWidth: number, newHeight: number): ImageBuffer {
706
+ const result = ImageBuffer.rgba(newWidth, newHeight);
707
+ const scaleX = source.width / newWidth;
708
+ const scaleY = source.height / newHeight;
709
+
710
+ // Process each channel using bilinear interpolation
711
+ for (let c = 0; c < Math.min(source.channelCount, 4); c++) {
712
+ const dstChannel = result.channels[c];
713
+
714
+ for (let y = 0; y < newHeight; y++) {
715
+ for (let x = 0; x < newWidth; x++) {
716
+ const srcX = x * scaleX;
717
+ const srcY = y * scaleY;
718
+ dstChannel[y * newWidth + x] = source.sample(srcX, srcY, c);
719
+ }
720
+ }
721
+ }
722
+
723
+ // Fill alpha if source doesn't have it
724
+ if (source.channelCount < 4) {
725
+ result.fill(3, 1);
726
+ }
727
+
728
+ return result;
729
+ }
730
+
731
+ protected render(): void {
732
+ const buffer = this.toImageBuffer(this.image.value);
733
+ if (!buffer) {
734
+ return;
735
+ }
736
+
737
+ let newWidth: number, newHeight: number;
738
+
739
+ if (this.props.mode.value === 'scale') {
740
+ const scale = this.props.scale.value;
741
+ newWidth = Math.round(buffer.width * scale);
742
+ newHeight = Math.round(buffer.height * scale);
743
+ } else {
744
+ newWidth = this.props.width.value;
745
+ newHeight = this.props.height.value;
746
+ }
747
+
748
+ const resized = this.resizeBuffer(buffer, newWidth, newHeight);
749
+ this.setOutput(this.output, resized);
750
+ }
751
+ }
752
+ `,C=`/**
753
+ * NormalMapNode - computes normal map from height map (red/grayscale channel)
754
+ */
755
+
756
+ import { ImageNodeBase, ImageBuffer, type ImageInput } from '../ImageNodeBase';
757
+ import type { Graph } from '@/nodes/Graph';
758
+ import type { InputPort, OutputPort } from '@/types/node.types';
759
+
760
+ export class NormalMapNode extends ImageNodeBase {
761
+ private image!: InputPort<ImageInput>;
762
+ private output!: OutputPort<ImageBuffer>;
763
+
764
+ constructor(id: string, graph: Graph) {
765
+ super(id, 'NormalMap', graph);
766
+ }
767
+
768
+ protected setup(): void {
769
+ this.image = this.in<ImageInput>('image', null);
770
+
771
+ this.addParm('scale', {
772
+ value: 1.0,
773
+ params: {
774
+ min: 0.0,
775
+ max: 10.0,
776
+ step: 0.1
777
+ },
778
+ displayName: 'Scale',
779
+ onChange: () => this.requestCook()
780
+ });
781
+
782
+ this.addParm('flipX', {
783
+ value: false,
784
+ type: 'boolean',
785
+ displayName: 'Flip X',
786
+ onChange: () => this.requestCook()
787
+ });
788
+
789
+ this.addParm('flipY', {
790
+ value: false,
791
+ type: 'boolean',
792
+ displayName: 'Flip Y',
793
+ onChange: () => this.requestCook()
794
+ });
795
+
796
+ this.output = this.out('image');
797
+
798
+ this.image.onChange = () => this.requestCook();
799
+
800
+ this.onReady = () => this.requestCook();
801
+ }
802
+
803
+ private computeNormalMap(source: ImageBuffer, scale: number, flipX: boolean, flipY: boolean): ImageBuffer {
804
+ const { width, height } = source;
805
+ const result = ImageBuffer.rgba(width, height);
806
+
807
+ // Use first channel as height (works for grayscale or red channel)
808
+ const heightMap = source.channels[0];
809
+
810
+ const rOut = result.channels[0];
811
+ const gOut = result.channels[1];
812
+ const bOut = result.channels[2];
813
+ const aOut = result.channels[3];
814
+
815
+ for (let y = 0; y < height; y++) {
816
+ for (let x = 0; x < width; x++) {
817
+ const idx = y * width + x;
818
+
819
+ // Get neighboring heights with clamping
820
+ const h00 = heightMap[idx];
821
+ const h10 = heightMap[Math.min(x + 1, width - 1) + y * width];
822
+ const h01 = heightMap[x + Math.min(y + 1, height - 1) * width];
823
+
824
+ // Compute gradients
825
+ const dx = (h10 - h00) * scale;
826
+ const dy = (h01 - h00) * scale;
827
+
828
+ // Apply flips
829
+ const finalDx = flipX ? -dx : dx;
830
+ const finalDy = flipY ? -dy : dy;
831
+
832
+ // Compute normal vector: Normal = normalize(-dx, -dy, 1)
833
+ const nx = -finalDx;
834
+ const ny = -finalDy;
835
+ const nz = 1.0;
836
+
837
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
838
+ const normalizedX = nx / length;
839
+ const normalizedY = ny / length;
840
+ const normalizedZ = nz / length;
841
+
842
+ // Map from [-1, 1] to [0, 1] for normal map format
843
+ rOut[idx] = normalizedX * 0.5 + 0.5;
844
+ gOut[idx] = normalizedY * 0.5 + 0.5;
845
+ bOut[idx] = normalizedZ * 0.5 + 0.5;
846
+ aOut[idx] = 1.0;
847
+ }
848
+ }
849
+
850
+ return result;
851
+ }
852
+
853
+ protected render(): void {
854
+ const buffer = this.toImageBuffer(this.image.value);
855
+ if (!buffer) {
856
+ return;
857
+ }
858
+
859
+ const scale = this.props.scale.value;
860
+ const flipX = this.props.flipX.value;
861
+ const flipY = this.props.flipY.value;
862
+
863
+ const normalMap = this.computeNormalMap(buffer, scale, flipX, flipY);
864
+ this.setOutput(this.output, normalMap);
865
+ }
866
+ }
867
+ `,w=`/**
868
+ * RampNode - generates color ramps as ImageBuffer
869
+ */
870
+
871
+ import { ImageNodeBase, ImageBuffer } from '../ImageNodeBase';
872
+ import type { Graph } from '@/nodes/Graph';
873
+ import type { OutputPort } from '@/types/node.types';
874
+
875
+ interface ColorValue {
876
+ r: number;
877
+ g: number;
878
+ b: number;
879
+ a?: number;
880
+ }
881
+
882
+ interface RampPoint {
883
+ position: number;
884
+ color: ColorValue | number[] | string;
885
+ interpolation?: 'linear' | 'smooth' | 'constant';
886
+ }
887
+
888
+ export class RampNode extends ImageNodeBase {
889
+ private output!: OutputPort<ImageBuffer>;
890
+
891
+ constructor(id: string, graph: Graph) {
892
+ super(id, 'Ramp', graph);
893
+ }
894
+
895
+ protected setup(): void {
896
+ this.addParm('type', {
897
+ value: 'horizontal',
898
+ params: {
899
+ options: [
900
+ { value: 'horizontal', label: 'Horizontal' },
901
+ { value: 'vertical', label: 'Vertical' },
902
+ { value: 'radial', label: 'Radial' },
903
+ { value: 'concentric', label: 'Concentric' }
904
+ ]
905
+ },
906
+ displayName: 'Type',
907
+ onChange: () => this.requestCook()
908
+ });
909
+
910
+ this.addParm('points', {
911
+ value: [
912
+ { position: 0.0, color: { r: 0.0, g: 0.0, b: 0.0 }, interpolation: 'linear' },
913
+ { position: 1.0, color: { r: 1.0, g: 1.0, b: 1.0 }, interpolation: 'linear' }
914
+ ],
915
+ type: 'colorramp',
916
+ displayName: 'Ramp',
917
+ onChange: () => this.requestCook()
918
+ });
919
+
920
+ this.addResolutionParm();
921
+
922
+ this.output = this.out('image');
923
+
924
+ this.onReady = () => this.requestCook();
925
+ }
926
+
927
+ private normalizeRampColor(color: ColorValue | number[] | string): ColorValue {
928
+ if (typeof color === 'object' && color !== null && 'r' in color && 'g' in color && 'b' in color) {
929
+ const isNormalized = color.r <= 1.0 && color.g <= 1.0 && color.b <= 1.0;
930
+ return {
931
+ r: isNormalized ? color.r : color.r / 255,
932
+ g: isNormalized ? color.g : color.g / 255,
933
+ b: isNormalized ? color.b : color.b / 255,
934
+ a: color.a !== undefined ? (isNormalized ? color.a : color.a / 255) : 1.0
935
+ };
936
+ }
937
+
938
+ if (Array.isArray(color)) {
939
+ const isNormalized = color.every(v => v <= 1.0);
940
+ return {
941
+ r: isNormalized ? color[0] : color[0] / 255,
942
+ g: isNormalized ? color[1] : color[1] / 255,
943
+ b: isNormalized ? color[2] : color[2] / 255,
944
+ a: color[3] !== undefined ? (isNormalized ? color[3] : color[3] / 255) : 1.0
945
+ };
946
+ }
947
+
948
+ if (typeof color === 'string') {
949
+ const rgbMatch = color.match(/rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)/);
950
+ if (rgbMatch) {
951
+ return {
952
+ r: parseInt(rgbMatch[1], 10) / 255,
953
+ g: parseInt(rgbMatch[2], 10) / 255,
954
+ b: parseInt(rgbMatch[3], 10) / 255,
955
+ a: 1.0
956
+ };
957
+ }
958
+
959
+ const hex = color.replace('#', '');
960
+ if (hex.length === 3 || hex.length === 6) {
961
+ const expanded = hex.length === 3
962
+ ? hex.split('').map(c => c + c).join('')
963
+ : hex;
964
+ const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(expanded);
965
+ if (result) {
966
+ return {
967
+ r: parseInt(result[1], 16) / 255,
968
+ g: parseInt(result[2], 16) / 255,
969
+ b: parseInt(result[3], 16) / 255,
970
+ a: 1.0
971
+ };
972
+ }
973
+ }
974
+ }
975
+
976
+ return { r: 0, g: 0, b: 0, a: 1.0 };
977
+ }
978
+
979
+ private interpolateColor(color1: ColorValue, color2: ColorValue, t: number, interpolation: string): ColorValue {
980
+ if (interpolation === 'constant') {
981
+ return t < 0.5 ? color1 : color2;
982
+ } else if (interpolation === 'smooth') {
983
+ t = t * t * (3 - 2 * t);
984
+ }
985
+
986
+ return {
987
+ r: color1.r + (color2.r - color1.r) * t,
988
+ g: color1.g + (color2.g - color1.g) * t,
989
+ b: color1.b + (color2.b - color1.b) * t,
990
+ a: (color1.a ?? 1) + ((color2.a ?? 1) - (color1.a ?? 1)) * t
991
+ };
992
+ }
993
+
994
+ private getColorAtPosition(points: RampPoint[], position: number): ColorValue {
995
+ const sortedPoints = [...points].sort((a, b) => a.position - b.position);
996
+ position = Math.max(0, Math.min(1, position));
997
+
998
+ let before: RampPoint | null = null;
999
+ let after: RampPoint | null = null;
1000
+
1001
+ for (let i = 0; i < sortedPoints.length; i++) {
1002
+ if (sortedPoints[i].position <= position) {
1003
+ before = sortedPoints[i];
1004
+ }
1005
+ if (sortedPoints[i].position >= position && !after) {
1006
+ after = sortedPoints[i];
1007
+ break;
1008
+ }
1009
+ }
1010
+
1011
+ if (!before && after) return this.normalizeRampColor(after.color);
1012
+ if (before && !after) return this.normalizeRampColor(before.color);
1013
+ if (!before && !after) return { r: 0, g: 0, b: 0, a: 1 };
1014
+
1015
+ if (before!.position === position) return this.normalizeRampColor(before!.color);
1016
+ if (after!.position === position) return this.normalizeRampColor(after!.color);
1017
+
1018
+ const t = (position - before!.position) / (after!.position - before!.position);
1019
+ const color1 = this.normalizeRampColor(before!.color);
1020
+ const color2 = this.normalizeRampColor(after!.color);
1021
+ const interpolation = after!.interpolation || 'linear';
1022
+
1023
+ return this.interpolateColor(color1, color2, t, interpolation);
1024
+ }
1025
+
1026
+ protected render(): void {
1027
+ const [width, height] = this.getResolution();
1028
+ const rampType = this.props.type.value;
1029
+ const points: RampPoint[] = this.props.points.value || [];
1030
+
1031
+ if (points.length === 0) {
1032
+ return;
1033
+ }
1034
+
1035
+ const buffer = ImageBuffer.rgba(width, height);
1036
+ const r = buffer.r();
1037
+ const g = buffer.g();
1038
+ const b = buffer.b();
1039
+ const a = buffer.a();
1040
+
1041
+ const centerX = width / 2;
1042
+ const centerY = height / 2;
1043
+ const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
1044
+
1045
+ for (let y = 0; y < height; y++) {
1046
+ for (let x = 0; x < width; x++) {
1047
+ let position = 0;
1048
+
1049
+ if (rampType === 'horizontal') {
1050
+ position = x / width;
1051
+ } else if (rampType === 'vertical') {
1052
+ position = y / height;
1053
+ } else if (rampType === 'radial') {
1054
+ const dx = x - centerX;
1055
+ const dy = y - centerY;
1056
+ const angle = Math.atan2(dy, dx);
1057
+ position = (angle + Math.PI) / (2 * Math.PI);
1058
+ } else if (rampType === 'concentric') {
1059
+ const dx = x - centerX;
1060
+ const dy = y - centerY;
1061
+ const dist = Math.sqrt(dx * dx + dy * dy);
1062
+ position = dist / maxDist;
1063
+ }
1064
+
1065
+ const color = this.getColorAtPosition(points, position);
1066
+ const idx = y * width + x;
1067
+ r[idx] = color.r;
1068
+ g[idx] = color.g;
1069
+ b[idx] = color.b;
1070
+ a[idx] = color.a ?? 1;
1071
+ }
1072
+ }
1073
+
1074
+ buffer.markDirty();
1075
+ this.setOutput(this.output, buffer);
1076
+ }
1077
+ }
1078
+ `,T=`/**
1079
+ * NoiseNode - generates procedural noise patterns as ImageBuffer
1080
+ */
1081
+
1082
+ import { ImageNodeBase, ImageBuffer } from '../ImageNodeBase';
1083
+ import type { Graph } from '@/nodes/Graph';
1084
+ import type { OutputPort } from '@/types/node.types';
1085
+
1086
+ type Noise2DFunction = (x: number, y: number) => number;
1087
+
1088
+ export class NoiseNode extends ImageNodeBase {
1089
+ private output!: OutputPort<ImageBuffer>;
1090
+ private noise2D: Noise2DFunction | null = null;
1091
+ private currentSeed: number | null = null;
1092
+
1093
+ constructor(id: string, graph: Graph) {
1094
+ super(id, 'Noise', graph);
1095
+ }
1096
+
1097
+ protected setup(): void {
1098
+ this.addParm('seed', {
1099
+ value: 0,
1100
+ params: {
1101
+ min: 0,
1102
+ max: 10000,
1103
+ step: 1,
1104
+ integer: true
1105
+ },
1106
+ displayName: 'Seed',
1107
+ onChange: () => {
1108
+ void this.initNoise().then(() => {
1109
+ if (this.noise2D) this.requestCook();
1110
+ });
1111
+ }
1112
+ });
1113
+
1114
+ this.addParm('scale', {
1115
+ value: 0.01,
1116
+ params: {
1117
+ min: 0.001,
1118
+ max: 1.0,
1119
+ step: 0.001
1120
+ },
1121
+ displayName: 'Scale',
1122
+ onChange: () => this.requestCook()
1123
+ });
1124
+
1125
+ this.addParm('iterations', {
1126
+ value: 4,
1127
+ params: {
1128
+ min: 1,
1129
+ max: 8,
1130
+ step: 1,
1131
+ integer: true
1132
+ },
1133
+ displayName: 'Iterations',
1134
+ onChange: () => this.requestCook()
1135
+ });
1136
+
1137
+ this.addResolutionParm();
1138
+
1139
+ this.output = this.out('image');
1140
+
1141
+ this.onReady = async () => {
1142
+ await this.initNoise();
1143
+ if (this.noise2D) {
1144
+ this.requestCook();
1145
+ }
1146
+ };
1147
+ }
1148
+
1149
+ private async initNoise(): Promise<void> {
1150
+ try {
1151
+ const simplexNoise = await this.require('simplex-noise');
1152
+ const { createNoise2D } = simplexNoise;
1153
+ const seed = this.props.seed.value;
1154
+
1155
+ let rng = seed;
1156
+ function seededRandom() {
1157
+ rng = (rng * 9301 + 49297) % 233280;
1158
+ return rng / 233280;
1159
+ }
1160
+
1161
+ this.noise2D = createNoise2D(seededRandom);
1162
+ this.currentSeed = seed;
1163
+ } catch (error) {
1164
+ console.error('Failed to load simplex-noise:', error);
1165
+ this.error = error as Error;
1166
+ }
1167
+ }
1168
+
1169
+ protected render(): void {
1170
+ if (!this.noise2D) {
1171
+ this.initNoise().then(() => {
1172
+ if (this.noise2D) {
1173
+ this.requestCook();
1174
+ }
1175
+ });
1176
+ return;
1177
+ }
1178
+
1179
+ const [width, height] = this.getResolution();
1180
+ const scale = this.props.scale.value;
1181
+ const iterations = this.props.iterations.value;
1182
+ const seed = this.props.seed.value;
1183
+
1184
+ if (seed !== this.currentSeed) {
1185
+ this.initNoise().then(() => {
1186
+ if (this.noise2D) {
1187
+ this.requestCook();
1188
+ }
1189
+ });
1190
+ return;
1191
+ }
1192
+
1193
+ // Create grayscale buffer (single channel for efficiency)
1194
+ const buffer = ImageBuffer.grayscale(width, height);
1195
+ const gray = buffer.r();
1196
+
1197
+ // Generate fractal noise
1198
+ for (let y = 0; y < height; y++) {
1199
+ for (let x = 0; x < width; x++) {
1200
+ let value = 0;
1201
+ let amplitude = 1;
1202
+ let frequency = scale;
1203
+ let maxValue = 0;
1204
+
1205
+ for (let i = 0; i < iterations; i++) {
1206
+ const nx = x * frequency;
1207
+ const ny = y * frequency;
1208
+ const noiseValue = this.noise2D!(nx, ny);
1209
+ value += noiseValue * amplitude;
1210
+ maxValue += amplitude;
1211
+ amplitude *= 0.5;
1212
+ frequency *= 2;
1213
+ }
1214
+
1215
+ // Normalize to [0, 1]
1216
+ value = (value / maxValue + 1) * 0.5;
1217
+ gray[y * width + x] = value;
1218
+ }
1219
+ }
1220
+
1221
+ buffer.markDirty();
1222
+ this.setOutput(this.output, buffer);
1223
+ }
1224
+ }
1225
+ `,E=`/**
1226
+ * BlurNode - applies Gaussian blur to an ImageBuffer
1227
+ * Uses separable convolution for efficiency
1228
+ */
1229
+
1230
+ import { ImageNodeBase, ImageBuffer, type ImageInput } from '../ImageNodeBase';
1231
+ import type { Graph } from '@/nodes/Graph';
1232
+ import type { InputPort, OutputPort } from '@/types/node.types';
1233
+
1234
+ export class BlurNode extends ImageNodeBase {
1235
+ private image!: InputPort<ImageInput>;
1236
+ private output!: OutputPort<ImageBuffer>;
1237
+
1238
+ constructor(id: string, graph: Graph) {
1239
+ super(id, 'Blur', graph);
1240
+ }
1241
+
1242
+ protected setup(): void {
1243
+ this.image = this.in<ImageInput>('image', null);
1244
+
1245
+ this.addParm('method', {
1246
+ value: 'box',
1247
+ params: {
1248
+ options: [
1249
+ { value: 'box', label: 'Box (Fast)' },
1250
+ { value: 'gaussian', label: 'Gaussian (Accurate)' }
1251
+ ]
1252
+ },
1253
+ displayName: 'Method',
1254
+ onChange: () => this.requestCook()
1255
+ });
1256
+
1257
+ this.addParm('radius', {
1258
+ value: 5.0,
1259
+ params: {
1260
+ min: 0.0,
1261
+ max: 100.0,
1262
+ step: 0.1
1263
+ },
1264
+ displayName: 'Radius',
1265
+ onChange: () => this.requestCook()
1266
+ });
1267
+
1268
+ this.addParm('wrapEdges', {
1269
+ value: false,
1270
+ type: 'boolean',
1271
+ displayName: 'Wrap Edges',
1272
+ onChange: () => this.requestCook()
1273
+ });
1274
+
1275
+ this.output = this.out('image');
1276
+
1277
+ this.image.onChange = () => this.requestCook();
1278
+
1279
+ this.onReady = () => this.requestCook();
1280
+ }
1281
+
1282
+ /**
1283
+ * Fast blur using stacked box blur approximation.
1284
+ * 3 passes of box blur closely approximates Gaussian and is O(1) per pixel.
1285
+ */
1286
+ private boxBlur(source: ImageBuffer, radius: number, wrapEdges: boolean): ImageBuffer {
1287
+ if (radius <= 0) {
1288
+ return source.clone();
1289
+ }
1290
+
1291
+ const { width, height } = source;
1292
+
1293
+ // Calculate box sizes for Gaussian approximation
1294
+ // Using 3 passes gives a good approximation
1295
+ const sigma = radius;
1296
+ const boxes = this.boxesForGauss(sigma, 3);
1297
+
1298
+ // Process each channel
1299
+ const result = source.emptyClone();
1300
+
1301
+ for (let c = 0; c < source.channelCount; c++) {
1302
+ // Copy source to result for in-place processing
1303
+ const channelData = new Float32Array(source.channels[c]);
1304
+
1305
+ // Apply 3 box blur passes
1306
+ for (const boxSize of boxes) {
1307
+ this.boxBlurPass(channelData, width, height, boxSize, wrapEdges);
1308
+ }
1309
+
1310
+ result.channels[c].set(channelData);
1311
+ }
1312
+
1313
+ return result;
1314
+ }
1315
+
1316
+ /**
1317
+ * Calculate box sizes for Gaussian approximation.
1318
+ * Returns array of box radii for n passes.
1319
+ */
1320
+ private boxesForGauss(sigma: number, n: number): number[] {
1321
+ // Ideal box width for Gaussian approximation
1322
+ const wIdeal = Math.sqrt((12 * sigma * sigma / n) + 1);
1323
+ let wl = Math.floor(wIdeal);
1324
+ if (wl % 2 === 0) wl--;
1325
+ const wu = wl + 2;
1326
+
1327
+ const mIdeal = (12 * sigma * sigma - n * wl * wl - 4 * n * wl - 3 * n) / (-4 * wl - 4);
1328
+ const m = Math.round(mIdeal);
1329
+
1330
+ const sizes: number[] = [];
1331
+ for (let i = 0; i < n; i++) {
1332
+ sizes.push(i < m ? wl : wu);
1333
+ }
1334
+ return sizes.map(s => (s - 1) / 2); // Convert to radius
1335
+ }
1336
+
1337
+ /**
1338
+ * Single box blur pass (horizontal then vertical) - O(1) per pixel
1339
+ */
1340
+ private boxBlurPass(data: Float32Array, w: number, h: number, r: number, wrap: boolean): void {
1341
+ if (r < 1) return;
1342
+
1343
+ const temp = new Float32Array(w * h);
1344
+ const ri = Math.floor(r);
1345
+ const diameter = ri * 2 + 1;
1346
+ const invDiameter = 1 / diameter;
1347
+
1348
+ // Horizontal pass - O(1) per pixel using sliding window
1349
+ for (let y = 0; y < h; y++) {
1350
+ const rowOffset = y * w;
1351
+ let sum = 0;
1352
+
1353
+ // Initialize sum for first pixel
1354
+ for (let i = -ri; i <= ri; i++) {
1355
+ const px = wrap ? ((i % w) + w) % w : Math.max(0, Math.min(w - 1, i));
1356
+ sum += data[rowOffset + px];
1357
+ }
1358
+ temp[rowOffset] = sum * invDiameter;
1359
+
1360
+ // Slide window across row
1361
+ for (let x = 1; x < w; x++) {
1362
+ // Remove left pixel, add right pixel
1363
+ const removeX = wrap ? (((x - ri - 1) % w) + w) % w : Math.max(0, x - ri - 1);
1364
+ const addX = wrap ? (((x + ri) % w) + w) % w : Math.min(w - 1, x + ri);
1365
+ sum -= data[rowOffset + removeX];
1366
+ sum += data[rowOffset + addX];
1367
+ temp[rowOffset + x] = sum * invDiameter;
1368
+ }
1369
+ }
1370
+
1371
+ // Vertical pass - O(1) per pixel using sliding window
1372
+ for (let x = 0; x < w; x++) {
1373
+ let sum = 0;
1374
+
1375
+ // Initialize sum for first pixel
1376
+ for (let i = -ri; i <= ri; i++) {
1377
+ const py = wrap ? ((i % h) + h) % h : Math.max(0, Math.min(h - 1, i));
1378
+ sum += temp[py * w + x];
1379
+ }
1380
+ data[x] = sum * invDiameter;
1381
+
1382
+ // Slide window down column
1383
+ for (let y = 1; y < h; y++) {
1384
+ const removeY = wrap ? (((y - ri - 1) % h) + h) % h : Math.max(0, y - ri - 1);
1385
+ const addY = wrap ? (((y + ri) % h) + h) % h : Math.min(h - 1, y + ri);
1386
+ sum -= temp[removeY * w + x];
1387
+ sum += temp[addY * w + x];
1388
+ data[y * w + x] = sum * invDiameter;
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ /**
1394
+ * True Gaussian blur using separable convolution.
1395
+ * More accurate but O(radius) per pixel - slower for large radii.
1396
+ */
1397
+ private trueGaussianBlur(source: ImageBuffer, radius: number, wrapEdges: boolean): ImageBuffer {
1398
+ if (radius <= 0) {
1399
+ return source.clone();
1400
+ }
1401
+
1402
+ const { width, height } = source;
1403
+ const result = source.emptyClone();
1404
+
1405
+ // Create Gaussian kernel
1406
+ const kernelSize = Math.ceil(radius * 3) * 2 + 1;
1407
+ const kernel = new Float32Array(kernelSize);
1408
+ const sigma = radius / 3;
1409
+ const twoSigmaSq = 2 * sigma * sigma;
1410
+ let sum = 0;
1411
+
1412
+ const halfKernel = (kernelSize - 1) >> 1;
1413
+
1414
+ for (let i = 0; i < kernelSize; i++) {
1415
+ const x = i - halfKernel;
1416
+ const value = Math.exp(-(x * x) / twoSigmaSq);
1417
+ kernel[i] = value;
1418
+ sum += value;
1419
+ }
1420
+
1421
+ // Normalize kernel
1422
+ const invSum = 1 / sum;
1423
+ for (let i = 0; i < kernelSize; i++) {
1424
+ kernel[i] *= invSum;
1425
+ }
1426
+
1427
+ // Inline wrapCoord functions
1428
+ const wrapX = wrapEdges
1429
+ ? (coord: number) => ((coord % width) + width) % width
1430
+ : (coord: number) => coord < 0 ? 0 : coord >= width ? width - 1 : coord;
1431
+ const wrapY = wrapEdges
1432
+ ? (coord: number) => ((coord % height) + height) % height
1433
+ : (coord: number) => coord < 0 ? 0 : coord >= height ? height - 1 : coord;
1434
+
1435
+ // Process each channel
1436
+ for (let c = 0; c < source.channelCount; c++) {
1437
+ const srcChannel = source.channels[c];
1438
+ const dstChannel = result.channels[c];
1439
+ const temp = new Float32Array(width * height);
1440
+
1441
+ // Horizontal pass
1442
+ for (let y = 0; y < height; y++) {
1443
+ const rowOffset = y * width;
1444
+ for (let x = 0; x < width; x++) {
1445
+ let acc = 0;
1446
+ for (let k = 0; k < kernelSize; k++) {
1447
+ const px = wrapX(x + k - halfKernel);
1448
+ acc += srcChannel[rowOffset + px] * kernel[k];
1449
+ }
1450
+ temp[rowOffset + x] = acc;
1451
+ }
1452
+ }
1453
+
1454
+ // Vertical pass
1455
+ for (let x = 0; x < width; x++) {
1456
+ for (let y = 0; y < height; y++) {
1457
+ let acc = 0;
1458
+ for (let k = 0; k < kernelSize; k++) {
1459
+ const py = wrapY(y + k - halfKernel);
1460
+ acc += temp[py * width + x] * kernel[k];
1461
+ }
1462
+ dstChannel[y * width + x] = acc;
1463
+ }
1464
+ }
1465
+ }
1466
+
1467
+ return result;
1468
+ }
1469
+
1470
+ protected render(): void {
1471
+ const buffer = this.toImageBuffer(this.image.value);
1472
+ if (!buffer) {
1473
+ return;
1474
+ }
1475
+
1476
+ const method = this.props.method.value;
1477
+ const radius = this.props.radius.value;
1478
+ const wrapEdges = this.props.wrapEdges.value;
1479
+
1480
+ const blurred = method === 'gaussian'
1481
+ ? this.trueGaussianBlur(buffer, radius, wrapEdges)
1482
+ : this.boxBlur(buffer, radius, wrapEdges);
1483
+
1484
+ this.setOutput(this.output, blurred);
1485
+ }
1486
+ }
1487
+ `,D=`/**
1488
+ * TextNode - Renders text onto an ImageBuffer
1489
+ *
1490
+ * Similar to Houdini's Font node, provides extensive control over
1491
+ * text rendering including font, size, alignment, color, and transforms.
1492
+ */
1493
+
1494
+ import { ImageNodeBase, ImageBuffer } from '../ImageNodeBase';
1495
+ import type { Graph } from '@/nodes/Graph';
1496
+ import type { OutputPort } from '@/types/node.types';
1497
+ import { colorToCss, normalizeColor } from '@/utils/colorUtils';
1498
+
1499
+ export class TextNode extends ImageNodeBase {
1500
+ private output!: OutputPort<ImageBuffer>;
1501
+
1502
+ constructor(id: string, graph: Graph) {
1503
+ super(id, 'Text', graph);
1504
+ }
1505
+
1506
+ protected setup(): void {
1507
+ // Text content (multiline)
1508
+ this.addParm('text', {
1509
+ value: 'Hello, World!',
1510
+ type: 'textarea',
1511
+ displayName: 'Text'
1512
+ });
1513
+
1514
+ // Font family
1515
+ this.addParm('font', {
1516
+ value: 'Inter, system-ui, sans-serif',
1517
+ type: 'text',
1518
+ displayName: 'Font'
1519
+ });
1520
+
1521
+ // Font size in pixels
1522
+ this.addParm('fontSize', {
1523
+ value: 48,
1524
+ type: 'slider',
1525
+ params: { min: 1, max: 500, step: 1 },
1526
+ displayName: 'Font Size'
1527
+ });
1528
+
1529
+ // Font weight
1530
+ this.addParm('fontWeight', {
1531
+ value: 'normal',
1532
+ type: 'select',
1533
+ params: {
1534
+ options: [
1535
+ { value: 'normal', label: 'Normal' },
1536
+ { value: 'bold', label: 'Bold' },
1537
+ { value: '100', label: 'Thin (100)' },
1538
+ { value: '200', label: 'Extra Light (200)' },
1539
+ { value: '300', label: 'Light (300)' },
1540
+ { value: '500', label: 'Medium (500)' },
1541
+ { value: '600', label: 'Semi Bold (600)' },
1542
+ { value: '700', label: 'Bold (700)' },
1543
+ { value: '800', label: 'Extra Bold (800)' },
1544
+ { value: '900', label: 'Black (900)' }
1545
+ ]
1546
+ },
1547
+ displayName: 'Font Weight'
1548
+ });
1549
+
1550
+ // Font style
1551
+ this.addParm('fontStyle', {
1552
+ value: 'normal',
1553
+ type: 'select',
1554
+ params: {
1555
+ options: [
1556
+ { value: 'normal', label: 'Normal' },
1557
+ { value: 'italic', label: 'Italic' },
1558
+ { value: 'oblique', label: 'Oblique' }
1559
+ ]
1560
+ },
1561
+ displayName: 'Font Style'
1562
+ });
1563
+
1564
+ // Horizontal alignment
1565
+ this.addParm('alignH', {
1566
+ value: 'center',
1567
+ type: 'select',
1568
+ params: {
1569
+ options: [
1570
+ { value: 'left', label: 'Left' },
1571
+ { value: 'center', label: 'Center' },
1572
+ { value: 'right', label: 'Right' }
1573
+ ]
1574
+ },
1575
+ displayName: 'Horizontal Align'
1576
+ });
1577
+
1578
+ // Vertical alignment
1579
+ this.addParm('alignV', {
1580
+ value: 'middle',
1581
+ type: 'select',
1582
+ params: {
1583
+ options: [
1584
+ { value: 'top', label: 'Top' },
1585
+ { value: 'middle', label: 'Middle' },
1586
+ { value: 'bottom', label: 'Bottom' }
1587
+ ]
1588
+ },
1589
+ displayName: 'Vertical Align'
1590
+ });
1591
+
1592
+ // Text color
1593
+ this.addParm('color', {
1594
+ value: { r: 1.0, g: 1.0, b: 1.0 },
1595
+ type: 'color',
1596
+ displayName: 'Color'
1597
+ });
1598
+
1599
+ // Background color (with alpha for transparency)
1600
+ this.addParm('backgroundColor', {
1601
+ value: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
1602
+ type: 'color',
1603
+ displayName: 'Background'
1604
+ });
1605
+
1606
+ this.addResolutionParm();
1607
+
1608
+ // Position offset (relative to alignment anchor)
1609
+ this.addParm('offset', {
1610
+ value: [0, 0],
1611
+ params: {
1612
+ min: [-2048, -2048],
1613
+ max: [2048, 2048],
1614
+ step: 1
1615
+ },
1616
+ displayName: 'Offset'
1617
+ });
1618
+
1619
+ // Rotation in degrees
1620
+ this.addParm('rotation', {
1621
+ value: 0,
1622
+ type: 'slider',
1623
+ params: { min: -180, max: 180, step: 0.1 },
1624
+ displayName: 'Rotation'
1625
+ });
1626
+
1627
+ // Letter spacing (tracking)
1628
+ this.addParm('letterSpacing', {
1629
+ value: 0,
1630
+ type: 'slider',
1631
+ params: { min: -50, max: 100, step: 0.5 },
1632
+ displayName: 'Letter Spacing'
1633
+ });
1634
+
1635
+ // Line height multiplier
1636
+ this.addParm('lineHeight', {
1637
+ value: 1.2,
1638
+ type: 'slider',
1639
+ params: { min: 0.5, max: 3.0, step: 0.05 },
1640
+ displayName: 'Line Height'
1641
+ });
1642
+
1643
+ // Opacity
1644
+ this.addParm('opacity', {
1645
+ value: 1.0,
1646
+ type: 'slider',
1647
+ params: { min: 0, max: 1, step: 0.01 },
1648
+ displayName: 'Opacity'
1649
+ });
1650
+
1651
+ this.output = this.out('image');
1652
+
1653
+ // Watch all properties for changes (resolution is handled by addResolutionParm)
1654
+ const propNames = [
1655
+ 'text', 'font', 'fontSize', 'fontWeight', 'fontStyle',
1656
+ 'alignH', 'alignV', 'color', 'backgroundColor',
1657
+ 'offset', 'rotation', 'letterSpacing', 'lineHeight', 'opacity'
1658
+ ];
1659
+ propNames.forEach(prop => this.watchProp(prop, () => this.requestCook()));
1660
+
1661
+ this.onReady = () => this.requestCook();
1662
+ }
1663
+
1664
+ protected render(): void {
1665
+ const [width, height] = this.getResolution();
1666
+ const text = this.props.text.value as string;
1667
+ const font = this.props.font.value as string;
1668
+ const fontSize = this.props.fontSize.value as number;
1669
+ const fontWeight = this.props.fontWeight.value as string;
1670
+ const fontStyle = this.props.fontStyle.value as string;
1671
+ const alignH = this.props.alignH.value as string;
1672
+ const alignV = this.props.alignV.value as string;
1673
+ const color = normalizeColor(this.props.color.value);
1674
+ const bgColor = normalizeColor(this.props.backgroundColor.value);
1675
+ const [offsetX, offsetY] = this.props.offset.value;
1676
+ const rotation = this.props.rotation.value as number;
1677
+ const letterSpacing = this.props.letterSpacing.value as number;
1678
+ const lineHeight = this.props.lineHeight.value as number;
1679
+ const opacity = this.props.opacity.value as number;
1680
+
1681
+ // Create canvas for rendering
1682
+ const canvas = document.createElement('canvas');
1683
+ canvas.width = width;
1684
+ canvas.height = height;
1685
+ const ctx = canvas.getContext('2d');
1686
+ if (!ctx) {
1687
+ this.setOutput(this.output, ImageBuffer.rgba(width, height));
1688
+ return;
1689
+ }
1690
+
1691
+ // Fill background
1692
+ if (bgColor.a > 0) {
1693
+ ctx.fillStyle = colorToCss(bgColor);
1694
+ ctx.fillRect(0, 0, width, height);
1695
+ } else {
1696
+ // Transparent background
1697
+ ctx.clearRect(0, 0, width, height);
1698
+ }
1699
+
1700
+ // Set up text rendering
1701
+ ctx.save();
1702
+
1703
+ // Apply global alpha for opacity
1704
+ ctx.globalAlpha = opacity;
1705
+
1706
+ // Build font string
1707
+ const fontString = \`\${fontStyle} \${fontWeight} \${fontSize}px \${font}\`;
1708
+ ctx.font = fontString;
1709
+ ctx.fillStyle = colorToCss(color);
1710
+
1711
+ // Calculate text metrics for multiline
1712
+ const lines = text.split('\\n');
1713
+ const lineHeightPx = fontSize * lineHeight;
1714
+ const totalTextHeight = lines.length * lineHeightPx;
1715
+
1716
+ // Calculate anchor point based on alignment
1717
+ let anchorX: number;
1718
+ let anchorY: number;
1719
+
1720
+ switch (alignH) {
1721
+ case 'left':
1722
+ ctx.textAlign = 'left';
1723
+ anchorX = 0;
1724
+ break;
1725
+ case 'right':
1726
+ ctx.textAlign = 'right';
1727
+ anchorX = width;
1728
+ break;
1729
+ case 'center':
1730
+ default:
1731
+ ctx.textAlign = 'center';
1732
+ anchorX = width / 2;
1733
+ break;
1734
+ }
1735
+
1736
+ switch (alignV) {
1737
+ case 'top':
1738
+ anchorY = fontSize; // Baseline offset
1739
+ break;
1740
+ case 'bottom':
1741
+ anchorY = height - totalTextHeight + fontSize;
1742
+ break;
1743
+ case 'middle':
1744
+ default:
1745
+ anchorY = (height - totalTextHeight) / 2 + fontSize;
1746
+ break;
1747
+ }
1748
+
1749
+ // Apply offset
1750
+ anchorX += offsetX;
1751
+ anchorY += offsetY;
1752
+
1753
+ // Apply rotation around anchor point
1754
+ if (rotation !== 0) {
1755
+ const rotationRad = (rotation * Math.PI) / 180;
1756
+ ctx.translate(anchorX, anchorY - fontSize / 2 + totalTextHeight / 2);
1757
+ ctx.rotate(rotationRad);
1758
+ ctx.translate(-anchorX, -(anchorY - fontSize / 2 + totalTextHeight / 2));
1759
+ }
1760
+
1761
+ // Render each line
1762
+ if (letterSpacing !== 0) {
1763
+ // Manual letter spacing - render character by character
1764
+ ctx.textAlign = 'left';
1765
+ for (let i = 0; i < lines.length; i++) {
1766
+ const line = lines[i];
1767
+ const y = anchorY + i * lineHeightPx;
1768
+
1769
+ // Calculate line width for alignment
1770
+ let lineWidth = 0;
1771
+ for (const char of line) {
1772
+ lineWidth += ctx.measureText(char).width + letterSpacing;
1773
+ }
1774
+ lineWidth -= letterSpacing; // Remove trailing spacing
1775
+
1776
+ // Calculate starting X based on alignment
1777
+ let x: number;
1778
+ switch (alignH) {
1779
+ case 'left':
1780
+ x = anchorX;
1781
+ break;
1782
+ case 'right':
1783
+ x = anchorX - lineWidth;
1784
+ break;
1785
+ case 'center':
1786
+ default:
1787
+ x = anchorX - lineWidth / 2;
1788
+ break;
1789
+ }
1790
+
1791
+ // Render each character
1792
+ for (const char of line) {
1793
+ ctx.fillText(char, x, y);
1794
+ x += ctx.measureText(char).width + letterSpacing;
1795
+ }
1796
+ }
1797
+ } else {
1798
+ // Standard text rendering (faster)
1799
+ for (let i = 0; i < lines.length; i++) {
1800
+ const y = anchorY + i * lineHeightPx;
1801
+ ctx.fillText(lines[i], anchorX, y);
1802
+ }
1803
+ }
1804
+
1805
+ ctx.restore();
1806
+
1807
+ // Convert canvas to ImageBuffer
1808
+ const buffer = ImageBuffer.fromCanvas(canvas);
1809
+ this.setOutput(this.output, buffer);
1810
+ }
1811
+ }
1812
+ `,O=`/**
1813
+ * TransformNode - 2D transformation (rotation, translation, scaling) for images
1814
+ */
1815
+
1816
+ import { ImageNodeBase, ImageBuffer } from '../ImageNodeBase';
1817
+ import type { Graph } from '@/nodes/Graph';
1818
+ import type { InputPort, OutputPort } from '@/types/node.types';
1819
+
1820
+ export class TransformNode extends ImageNodeBase {
1821
+ private imageInput!: InputPort<ImageBuffer | null>;
1822
+ private output!: OutputPort<ImageBuffer>;
1823
+
1824
+ constructor(id: string, graph: Graph) {
1825
+ super(id, 'Transform', graph);
1826
+ }
1827
+
1828
+ protected setup(): void {
1829
+ this.imageInput = this.in<ImageBuffer | null>('image', null);
1830
+
1831
+ this.addParm('translate', {
1832
+ value: [0, 0],
1833
+ params: {
1834
+ min: [-2048, -2048],
1835
+ max: [2048, 2048],
1836
+ step: 1
1837
+ },
1838
+ displayName: 'Translate',
1839
+ onChange: () => this.requestCook()
1840
+ });
1841
+
1842
+ this.addParm('rotation', {
1843
+ value: 0,
1844
+ params: {
1845
+ min: -180,
1846
+ max: 180,
1847
+ step: 0.1
1848
+ },
1849
+ displayName: 'Rotation',
1850
+ onChange: () => this.requestCook()
1851
+ });
1852
+
1853
+ this.addParm('scale', {
1854
+ value: [1, 1],
1855
+ params: {
1856
+ min: [0.01, 0.01],
1857
+ max: [10, 10],
1858
+ step: 0.01
1859
+ },
1860
+ displayName: 'Scale',
1861
+ onChange: () => this.requestCook()
1862
+ });
1863
+
1864
+ this.addParm('uniformScale', {
1865
+ value: true,
1866
+ type: 'boolean',
1867
+ displayName: 'Uniform Scale',
1868
+ onChange: () => this.requestCook()
1869
+ });
1870
+
1871
+ this.addParm('pivot', {
1872
+ value: 'center',
1873
+ params: {
1874
+ options: [
1875
+ { value: 'center', label: 'Center' },
1876
+ { value: 'top-left', label: 'Top Left' },
1877
+ { value: 'top-right', label: 'Top Right' },
1878
+ { value: 'bottom-left', label: 'Bottom Left' },
1879
+ { value: 'bottom-right', label: 'Bottom Right' },
1880
+ { value: 'custom', label: 'Custom' }
1881
+ ]
1882
+ },
1883
+ displayName: 'Pivot',
1884
+ onChange: () => this.requestCook()
1885
+ });
1886
+
1887
+ this.addParm('customPivot', {
1888
+ value: [0.5, 0.5],
1889
+ params: {
1890
+ min: [0, 0],
1891
+ max: [1, 1],
1892
+ step: 0.01
1893
+ },
1894
+ displayName: 'Custom Pivot',
1895
+ hidden: () => this.props.pivot.value !== 'custom',
1896
+ onChange: () => this.requestCook()
1897
+ });
1898
+
1899
+ this.addParm('filter', {
1900
+ value: 'bilinear',
1901
+ params: {
1902
+ options: [
1903
+ { value: 'nearest', label: 'Nearest' },
1904
+ { value: 'bilinear', label: 'Bilinear' }
1905
+ ]
1906
+ },
1907
+ displayName: 'Filter',
1908
+ onChange: () => this.requestCook()
1909
+ });
1910
+
1911
+ this.addParm('wrap', {
1912
+ value: 'clamp',
1913
+ params: {
1914
+ options: [
1915
+ { value: 'clamp', label: 'Clamp' },
1916
+ { value: 'repeat', label: 'Repeat' },
1917
+ { value: 'mirror', label: 'Mirror' },
1918
+ { value: 'transparent', label: 'Transparent' }
1919
+ ]
1920
+ },
1921
+ displayName: 'Wrap Mode',
1922
+ onChange: () => this.requestCook()
1923
+ });
1924
+
1925
+ this.output = this.out('image');
1926
+
1927
+ this.imageInput.onChange = () => this.requestCook();
1928
+
1929
+ this.onReady = () => this.requestCook();
1930
+ }
1931
+
1932
+ private getPivotPoint(width: number, height: number): [number, number] {
1933
+ const pivot = this.props.pivot.value;
1934
+ switch (pivot) {
1935
+ case 'center': return [width / 2, height / 2];
1936
+ case 'top-left': return [0, 0];
1937
+ case 'top-right': return [width, 0];
1938
+ case 'bottom-left': return [0, height];
1939
+ case 'bottom-right': return [width, height];
1940
+ case 'custom':
1941
+ const [px, py] = this.props.customPivot.value;
1942
+ return [px * width, py * height];
1943
+ default: return [width / 2, height / 2];
1944
+ }
1945
+ }
1946
+
1947
+ private wrapCoord(coord: number, size: number, mode: string): number {
1948
+ if (mode === 'clamp') {
1949
+ return Math.max(0, Math.min(size - 1, coord));
1950
+ } else if (mode === 'repeat') {
1951
+ coord = coord % size;
1952
+ if (coord < 0) coord += size;
1953
+ return coord;
1954
+ } else if (mode === 'mirror') {
1955
+ const period = size * 2;
1956
+ coord = Math.abs(coord % period);
1957
+ if (coord >= size) coord = period - coord - 1;
1958
+ return Math.max(0, Math.min(size - 1, coord));
1959
+ }
1960
+ return coord; // transparent - will be checked separately
1961
+ }
1962
+
1963
+ protected render(): void {
1964
+ const input = this.toImageBuffer(this.imageInput.value);
1965
+ if (!input) {
1966
+ return;
1967
+ }
1968
+
1969
+ const width = input.width;
1970
+ const height = input.height;
1971
+ const [tx, ty] = this.props.translate.value;
1972
+ const rotation = this.props.rotation.value * Math.PI / 180;
1973
+ let [sx, sy] = this.props.scale.value;
1974
+ const uniformScale = this.props.uniformScale.value;
1975
+ const filterMode = this.props.filter.value;
1976
+ const wrapMode = this.props.wrap.value;
1977
+
1978
+ if (uniformScale) {
1979
+ sy = sx;
1980
+ }
1981
+
1982
+ const [pivotX, pivotY] = this.getPivotPoint(width, height);
1983
+
1984
+ // Pre-compute inverse transform matrix
1985
+ const cosR = Math.cos(-rotation);
1986
+ const sinR = Math.sin(-rotation);
1987
+ const invSx = 1 / sx;
1988
+ const invSy = 1 / sy;
1989
+
1990
+ const buffer = ImageBuffer.rgba(width, height);
1991
+ const channelCount = Math.min(input.channelCount, 4);
1992
+
1993
+ for (let y = 0; y < height; y++) {
1994
+ for (let x = 0; x < width; x++) {
1995
+ // Transform from destination to source coordinates
1996
+ // 1. Translate to pivot
1997
+ let dx = x - pivotX - tx;
1998
+ let dy = y - pivotY - ty;
1999
+
2000
+ // 2. Rotate (inverse)
2001
+ const rx = dx * cosR - dy * sinR;
2002
+ const ry = dx * sinR + dy * cosR;
2003
+
2004
+ // 3. Scale (inverse)
2005
+ const srcX = rx * invSx + pivotX;
2006
+ const srcY = ry * invSy + pivotY;
2007
+
2008
+ // Check bounds for transparent mode
2009
+ if (wrapMode === 'transparent') {
2010
+ if (srcX < 0 || srcX >= width || srcY < 0 || srcY >= height) {
2011
+ const idx = y * width + x;
2012
+ for (let c = 0; c < 4; c++) {
2013
+ buffer.channels[c][idx] = c === 3 ? 0 : 0; // transparent black
2014
+ }
2015
+ continue;
2016
+ }
2017
+ }
2018
+
2019
+ const idx = y * width + x;
2020
+
2021
+ if (filterMode === 'nearest') {
2022
+ const sx0 = Math.round(srcX);
2023
+ const sy0 = Math.round(srcY);
2024
+ const wx = this.wrapCoord(sx0, width, wrapMode);
2025
+ const wy = this.wrapCoord(sy0, height, wrapMode);
2026
+ const srcIdx = wy * width + wx;
2027
+
2028
+ for (let c = 0; c < channelCount; c++) {
2029
+ buffer.channels[c][idx] = input.channels[c][srcIdx];
2030
+ }
2031
+ // Fill alpha if input doesn't have it
2032
+ if (channelCount < 4) {
2033
+ buffer.channels[3][idx] = 1;
2034
+ }
2035
+ } else {
2036
+ // Bilinear interpolation
2037
+ const x0 = Math.floor(srcX);
2038
+ const y0 = Math.floor(srcY);
2039
+ const x1 = x0 + 1;
2040
+ const y1 = y0 + 1;
2041
+ const fx = srcX - x0;
2042
+ const fy = srcY - y0;
2043
+
2044
+ const wx0 = this.wrapCoord(x0, width, wrapMode);
2045
+ const wx1 = this.wrapCoord(x1, width, wrapMode);
2046
+ const wy0 = this.wrapCoord(y0, height, wrapMode);
2047
+ const wy1 = this.wrapCoord(y1, height, wrapMode);
2048
+
2049
+ const idx00 = wy0 * width + wx0;
2050
+ const idx10 = wy0 * width + wx1;
2051
+ const idx01 = wy1 * width + wx0;
2052
+ const idx11 = wy1 * width + wx1;
2053
+
2054
+ for (let c = 0; c < channelCount; c++) {
2055
+ const v00 = input.channels[c][idx00];
2056
+ const v10 = input.channels[c][idx10];
2057
+ const v01 = input.channels[c][idx01];
2058
+ const v11 = input.channels[c][idx11];
2059
+
2060
+ const v0 = v00 + (v10 - v00) * fx;
2061
+ const v1 = v01 + (v11 - v01) * fx;
2062
+ buffer.channels[c][idx] = v0 + (v1 - v0) * fy;
2063
+ }
2064
+ // Fill alpha if input doesn't have it
2065
+ if (channelCount < 4) {
2066
+ buffer.channels[3][idx] = 1;
2067
+ }
2068
+ }
2069
+ }
2070
+ }
2071
+
2072
+ buffer.markDirty();
2073
+ this.setOutput(this.output, buffer);
2074
+ }
2075
+ }
2076
+ `;function k(e,t){let n=document.createElement(`canvas`);return n.width=e,n.height=t,n}function A(e){return e instanceof HTMLCanvasElement?{width:e.width,height:e.height}:e instanceof HTMLImageElement?{width:e.naturalWidth||e.width,height:e.naturalHeight||e.height}:{width:0,height:0}}function j(e){if(e instanceof HTMLCanvasElement)return e;let t=k(e.naturalWidth||e.width,e.naturalHeight||e.height),n=t.getContext(`2d`);return n&&n.drawImage(e,0,0),t}function M(e){return new Promise((t,n)=>{let r=new Image;r.crossOrigin=`anonymous`,r.onload=()=>t(r),r.onerror=()=>n(Error(`Failed to load image: ${e}`)),r.src=e})}function N(e,t,n){let{width:r,height:i}=A(e),a=r/i,o=r,s=i;r>t&&(o=t,s=t/a),s>n&&(s=n,o=n*a);let c=k(Math.round(o),Math.round(s)),l=c.getContext(`2d`);return l&&l.drawImage(e,0,0,c.width,c.height),c}function P(e,t,n){let r=k(t,n),i=r.getContext(`2d`);return i&&i.drawImage(e,0,0,t,n),r}function F(e,t){let{width:n,height:r}=A(e);return P(e,Math.round(n*t),Math.round(r*t))}function I(e){return{normal:`source-over`,multiply:`multiply`,screen:`screen`,overlay:`overlay`,darken:`darken`,lighten:`lighten`,"color-dodge":`color-dodge`,"color-burn":`color-burn`,"hard-light":`hard-light`,"soft-light":`soft-light`,difference:`difference`,exclusion:`exclusion`,add:`lighter`,subtract:`difference`}[e]||`source-over`}var L={Color:s,Image:c,Composite:l,Checkers:u,Resize:d,NormalMap:f,Ramp:p,Noise:m,Blur:h,Text:g,Transform:_};t(`image`,L),e(`Color`,v),e(`Image`,y),e(`Composite`,b),e(`Checkers`,x),e(`Resize`,S),e(`NormalMap`,C),e(`Ramp`,w),e(`Noise`,T),e(`Blur`,E),e(`Text`,D),e(`Transform`,O);export{h as BlurNode,u as CheckersNode,s as ColorNode,l as CompositeNode,c as ImageNode,o as ImageNodeBase,m as NoiseNode,f as NormalMapNode,p as RampNode,d as ResizeNode,g as TextNode,_ as TransformNode,k as createCanvas,I as getBlendMode,A as getImageResolution,a as imageLibrary,L as imageNodeClasses,j as imageToCanvas,M as loadImage,P as resizeImageToFixed,N as resizeImageWithAspectRatio,F as scaleImage};