@field/cascade 0.10.0 → 0.12.0

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