├── .gitignore ├── LICENSE ├── build.clj ├── deps.edn ├── examples ├── screen-recording.mp4 └── smooth-cube.svg ├── readme.md ├── resources ├── clojure.min.js ├── closebrackets.min.js ├── codemirror.min.css ├── codemirror.min.js ├── favicon.ico │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── htmx.min.js ├── marching-cubes.glsl ├── matchbrackets.min.js ├── missing.min.css ├── nord.min.css └── render.js └── src └── sdfx ├── examples.clj ├── geom.clj ├── main.clj └── util.clj /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *.jar 4 | *.class 5 | /lib/ 6 | /classes/ 7 | /target/ 8 | /checkouts/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .lein-failures 13 | .nrepl-port 14 | .cpcache/ 15 | .DS_Store 16 | /.clj-kondo/ 17 | /.lsp/ 18 | /output/ 19 | /scratch/ 20 | sdfx.org 21 | sdfx.org.md 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 adam-james 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [clojure.tools.build.api :as b])) 3 | 4 | ;; This build script based off of the tutorial here: 5 | ;; https://kozieiev.com/blog/packaging-clojure-into-jar-uberjar-with-tools-build/ 6 | 7 | (def build-folder "target") 8 | (def jar-content (str build-folder "/classes")) 9 | 10 | (def basis (b/create-basis {:project "deps.edn"})) 11 | (def version "0.0.1") 12 | (def app-name "SDFx") 13 | (def uber-file-name (format "%s/%s-%s.jar" build-folder app-name version)) ; path for result uber file 14 | 15 | (defn clean [_] 16 | (b/delete {:path "target"}) 17 | (println (format "Build folder \"%s\" removed" build-folder))) 18 | 19 | (defn uber [_] 20 | (clean nil) 21 | 22 | (b/copy-dir {:src-dirs ["resources"] ; copy resources 23 | :target-dir jar-content}) 24 | 25 | (b/compile-clj {:basis basis ; compile clojure code 26 | :src-dirs ["src"] 27 | :class-dir jar-content}) 28 | 29 | (b/uber {:class-dir jar-content ; create uber file 30 | :uber-file uber-file-name 31 | :basis basis 32 | :main 'sdfx.main}) ; here we specify the entry point for uberjar 33 | 34 | (println (format "Uber file created: \"%s\"" uber-file-name))) 35 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources" "test"] 2 | :deps 3 | {cheshire/cheshire {:mvn/version "5.11.0"} 4 | hiccup/hiccup {:mvn/version "2.0.0-RC1"} 5 | http-kit/http-kit {:mvn/version "2.7.0-alpha1"} 6 | org.clojure/clojure {:mvn/version "1.11.1"} 7 | svg-clj/svg-clj {:git/url "https://github.com/adam-james-v/svg-clj" 8 | :git/sha "dce9d756bb1052034d360ae7c0743dd9153fca18"} #_{:local/root "../svg-clj/"} 9 | org.clojars.askonomm/ruuter {:mvn/version "1.3.2"}} 10 | 11 | :aliases 12 | ;; clj -T:build uber 13 | {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}} 14 | :ns-default build}}} 15 | -------------------------------------------------------------------------------- /examples/screen-recording.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/examples/screen-recording.mp4 -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SDFx 2 | A Signed Distance Field art tool built with Clojure, WebGL, and HTMX. 3 | 4 | Signed Distance Fields and the operations you can perform on them allow you to create complex and cool shapes which make for really cool art pieces; particularly if you can plot them with a pen plotter (I'm partial to AxiDraw). To that end, I've made this tool to make it fun and easy to build SDFs. 5 | 6 | ![](./examples/screen-recording.mp4) 7 | ![](./examples/smooth-cube.svg) 8 | 9 | ## Getting Started 10 | Since this is a tool for me, I'm not too fussed about distribution. However, it's actually pretty easy to get started. 11 | 12 | ### Build from Source 13 | If you're comfortable with Clojure, you can do the following: 14 | - `git clone https://github.com/adam-james-v/sdfx.git` 15 | - `cd sdfx` 16 | - `clojure -T:build uber` which should build the uberjar `target/SDFx-0.0.1.jar` 17 | - run SDFx with `java -jar target/SDFx-0.0.1.jar`. This command will start the server and print the port it's using. 18 | - Head to `localhost:THE_PORT_SPECIFIED` and have fun! 19 | 20 | ### Run the Jar 21 | Otherwise, you can 22 | - [download the jar](https://github.com/adam-james-v/sdfx/releases/download/001/SDFx-0.0.1.jar). 23 | - `cd` into that directory and 24 | - run the build with `java -jar SDFx-0.0.1.jar`. This will start the server and print the port it's using. 25 | - Head to `localhost:THE_PORT_SPECIFIED` and get creative! 26 | -------------------------------------------------------------------------------- /resources/clojure.min.js: -------------------------------------------------------------------------------- 1 | !function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(b){"use strict";b.defineMode("clojure",function(o){var e=["false","nil","true"],t=[".","catch","def","do","if","monitor-enter","monitor-exit","new","quote","recur","set!","throw","try","var"],n=["*","*'","*1","*2","*3","*agent*","*allow-unresolved-vars*","*assert*","*clojure-version*","*command-line-args*","*compile-files*","*compile-path*","*compiler-options*","*data-readers*","*default-data-reader-fn*","*e","*err*","*file*","*flush-on-newline*","*fn-loader*","*in*","*math-context*","*ns*","*out*","*print-dup*","*print-length*","*print-level*","*print-meta*","*print-namespace-maps*","*print-readably*","*read-eval*","*reader-resolver*","*source-path*","*suppress-read*","*unchecked-math*","*use-context-classloader*","*verbose-defrecords*","*warn-on-reflection*","+","+'","-","-'","->","->>","->ArrayChunk","->Eduction","->Vec","->VecNode","->VecSeq","-cache-protocol-fn","-reset-methods","..","/","<","<=","=","==",">",">=","EMPTY-NODE","Inst","StackTraceElement->vec","Throwable->map","accessor","aclone","add-classpath","add-watch","agent","agent-error","agent-errors","aget","alength","alias","all-ns","alter","alter-meta!","alter-var-root","amap","ancestors","and","any?","apply","areduce","array-map","as->","aset","aset-boolean","aset-byte","aset-char","aset-double","aset-float","aset-int","aset-long","aset-short","assert","assoc","assoc!","assoc-in","associative?","atom","await","await-for","await1","bases","bean","bigdec","bigint","biginteger","binding","bit-and","bit-and-not","bit-clear","bit-flip","bit-not","bit-or","bit-set","bit-shift-left","bit-shift-right","bit-test","bit-xor","boolean","boolean-array","boolean?","booleans","bound-fn","bound-fn*","bound?","bounded-count","butlast","byte","byte-array","bytes","bytes?","case","cast","cat","char","char-array","char-escape-string","char-name-string","char?","chars","chunk","chunk-append","chunk-buffer","chunk-cons","chunk-first","chunk-next","chunk-rest","chunked-seq?","class","class?","clear-agent-errors","clojure-version","coll?","comment","commute","comp","comparator","compare","compare-and-set!","compile","complement","completing","concat","cond","cond->","cond->>","condp","conj","conj!","cons","constantly","construct-proxy","contains?","count","counted?","create-ns","create-struct","cycle","dec","dec'","decimal?","declare","dedupe","default-data-readers","definline","definterface","defmacro","defmethod","defmulti","defn","defn-","defonce","defprotocol","defrecord","defstruct","deftype","delay","delay?","deliver","denominator","deref","derive","descendants","destructure","disj","disj!","dissoc","dissoc!","distinct","distinct?","doall","dorun","doseq","dosync","dotimes","doto","double","double-array","double?","doubles","drop","drop-last","drop-while","eduction","empty","empty?","ensure","ensure-reduced","enumeration-seq","error-handler","error-mode","eval","even?","every-pred","every?","ex-data","ex-info","extend","extend-protocol","extend-type","extenders","extends?","false?","ffirst","file-seq","filter","filterv","find","find-keyword","find-ns","find-protocol-impl","find-protocol-method","find-var","first","flatten","float","float-array","float?","floats","flush","fn","fn?","fnext","fnil","for","force","format","frequencies","future","future-call","future-cancel","future-cancelled?","future-done?","future?","gen-class","gen-interface","gensym","get","get-in","get-method","get-proxy-class","get-thread-bindings","get-validator","group-by","halt-when","hash","hash-combine","hash-map","hash-ordered-coll","hash-set","hash-unordered-coll","ident?","identical?","identity","if-let","if-not","if-some","ifn?","import","in-ns","inc","inc'","indexed?","init-proxy","inst-ms","inst-ms*","inst?","instance?","int","int-array","int?","integer?","interleave","intern","interpose","into","into-array","ints","io!","isa?","iterate","iterator-seq","juxt","keep","keep-indexed","key","keys","keyword","keyword?","last","lazy-cat","lazy-seq","let","letfn","line-seq","list","list*","list?","load","load-file","load-reader","load-string","loaded-libs","locking","long","long-array","longs","loop","macroexpand","macroexpand-1","make-array","make-hierarchy","map","map-entry?","map-indexed","map?","mapcat","mapv","max","max-key","memfn","memoize","merge","merge-with","meta","method-sig","methods","min","min-key","mix-collection-hash","mod","munge","name","namespace","namespace-munge","nat-int?","neg-int?","neg?","newline","next","nfirst","nil?","nnext","not","not-any?","not-empty","not-every?","not=","ns","ns-aliases","ns-imports","ns-interns","ns-map","ns-name","ns-publics","ns-refers","ns-resolve","ns-unalias","ns-unmap","nth","nthnext","nthrest","num","number?","numerator","object-array","odd?","or","parents","partial","partition","partition-all","partition-by","pcalls","peek","persistent!","pmap","pop","pop!","pop-thread-bindings","pos-int?","pos?","pr","pr-str","prefer-method","prefers","primitives-classnames","print","print-ctor","print-dup","print-method","print-simple","print-str","printf","println","println-str","prn","prn-str","promise","proxy","proxy-call-with-super","proxy-mappings","proxy-name","proxy-super","push-thread-bindings","pvalues","qualified-ident?","qualified-keyword?","qualified-symbol?","quot","rand","rand-int","rand-nth","random-sample","range","ratio?","rational?","rationalize","re-find","re-groups","re-matcher","re-matches","re-pattern","re-seq","read","read-line","read-string","reader-conditional","reader-conditional?","realized?","record?","reduce","reduce-kv","reduced","reduced?","reductions","ref","ref-history-count","ref-max-history","ref-min-history","ref-set","refer","refer-clojure","reify","release-pending-sends","rem","remove","remove-all-methods","remove-method","remove-ns","remove-watch","repeat","repeatedly","replace","replicate","require","reset!","reset-meta!","reset-vals!","resolve","rest","restart-agent","resultset-seq","reverse","reversible?","rseq","rsubseq","run!","satisfies?","second","select-keys","send","send-off","send-via","seq","seq?","seqable?","seque","sequence","sequential?","set","set-agent-send-executor!","set-agent-send-off-executor!","set-error-handler!","set-error-mode!","set-validator!","set?","short","short-array","shorts","shuffle","shutdown-agents","simple-ident?","simple-keyword?","simple-symbol?","slurp","some","some->","some->>","some-fn","some?","sort","sort-by","sorted-map","sorted-map-by","sorted-set","sorted-set-by","sorted?","special-symbol?","spit","split-at","split-with","str","string?","struct","struct-map","subs","subseq","subvec","supers","swap!","swap-vals!","symbol","symbol?","sync","tagged-literal","tagged-literal?","take","take-last","take-nth","take-while","test","the-ns","thread-bound?","time","to-array","to-array-2d","trampoline","transduce","transient","tree-seq","true?","type","unchecked-add","unchecked-add-int","unchecked-byte","unchecked-char","unchecked-dec","unchecked-dec-int","unchecked-divide-int","unchecked-double","unchecked-float","unchecked-inc","unchecked-inc-int","unchecked-int","unchecked-long","unchecked-multiply","unchecked-multiply-int","unchecked-negate","unchecked-negate-int","unchecked-remainder-int","unchecked-short","unchecked-subtract","unchecked-subtract-int","underive","unquote","unquote-splicing","unreduced","unsigned-bit-shift-right","update","update-in","update-proxy","uri?","use","uuid?","val","vals","var-get","var-set","var?","vary-meta","vec","vector","vector-of","vector?","volatile!","volatile?","vreset!","vswap!","when","when-first","when-let","when-not","when-some","while","with-bindings","with-bindings*","with-in-str","with-loading-context","with-local-vars","with-meta","with-open","with-out-str","with-precision","with-redefs","with-redefs-fn","xml-seq","zero?","zipmap"],r=(b.registerHelper("hintWords","clojure",[].concat(e,t,n)),h(e)),a=h(t),s=h(n),i=h(["->","->>","as->","binding","bound-fn","case","catch","comment","cond","cond->","cond->>","condp","def","definterface","defmethod","defn","defmacro","defprotocol","defrecord","defstruct","deftype","do","doseq","dotimes","doto","extend","extend-protocol","extend-type","fn","for","future","if","if-let","if-not","if-some","let","letfn","locking","loop","ns","proxy","reify","struct-map","some->","some->>","try","when","when-first","when-let","when-not","when-some","while","with-bindings","with-bindings*","with-in-str","with-loading-context","with-local-vars","with-meta","with-open","with-out-str","with-precision","with-redefs","with-redefs-fn"]),c=/^(?:[\\\[\]\s"(),;@^`{}~]|$)/,d=/^(?:[+\-]?\d+(?:(?:N|(?:[eE][+\-]?\d+))|(?:\.?\d*(?:M|(?:[eE][+\-]?\d+))?)|\/\d+|[xX][0-9a-fA-F]+|r[0-9a-zA-Z]+)?(?=[\\\[\]\s"#'(),;@^`{}~]|$))/,l=/^(?:\\(?:backspace|formfeed|newline|return|space|tab|o[0-7]{3}|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{4}|.)?(?=[\\\[\]\s"(),;@^`{}~]|$))/,u=/^(?:(?:[^\\\/\[\]\d\s"#'(),;@^`{}~.][^\\\[\]\s"(),;@^`{}~.\/]*(?:\.[^\\\/\[\]\d\s"#'(),;@^`{}~.][^\\\[\]\s"(),;@^`{}~.\/]*)*\/)?(?:\/|[^\\\/\[\]\d\s"#'(),;@^`{}~][^\\\[\]\s"(),;@^`{}~]*)*(?=[\\\[\]\s"(),;@^`{}~]|$))/;function p(e,t){var n;return e.eatSpace()||e.eat(",")?["space",null]:e.match(d)?[null,"number"]:e.match(l)?[null,"string-2"]:e.eat(/^"/)?(t.tokenize=m)(e,t):e.eat(/^[(\[{]/)?["open","bracket"]:e.eat(/^[)\]}]/)?["close","bracket"]:e.eat(/^;/)?(e.skipToEnd(),["space","comment"]):e.eat(/^[#'@^`~]/)?[null,"meta"]:(n=(n=e.match(u))&&n[0])?"comment"===n&&"("===t.lastToken?(t.tokenize=f)(e,t):y(n,r)||":"===n.charAt(0)?["symbol","atom"]:y(n,a)||y(n,s)?["symbol","keyword"]:"("===t.lastToken?["symbol","builtin"]:["symbol","variable"]:(e.next(),e.eatWhile(function(e){return!y(e,c)}),[null,"error"])}function m(e,t){for(var n,r=!1;n=e.next();){if('"'===n&&!r){t.tokenize=p;break}r=!r&&"\\"===n}return[null,"string"]}function f(e,t){for(var n,r=1;n=e.next();)if(")"===n&&r--,"("===n&&r++,0===r){e.backUp(1),t.tokenize=p;break}return["space","comment"]}function h(e){for(var t={},n=0;n",triples:"",explode:"[]{}"},k=S.Pos;function y(e,t){return"pairs"==t&&"string"==typeof e?e:("object"==typeof e&&null!=e[t]?e:n)[t]}S.defineOption("autoCloseBrackets",!1,function(e,t,n){n&&n!=S.Init&&(e.removeKeyMap(i),e.state.closeBrackets=null),t&&(r(y(t,"pairs")),e.state.closeBrackets=t,e.addKeyMap(i))});var i={Backspace:function(e){var t=O(e);if(!t||e.getOption("disableInput"))return S.Pass;for(var n=y(t,"pairs"),r=e.listSelections(),i=0;ispan::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:0 0}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:0 0}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0} -------------------------------------------------------------------------------- /resources/favicon.ico/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/android-icon-144x144.png -------------------------------------------------------------------------------- /resources/favicon.ico/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/android-icon-192x192.png -------------------------------------------------------------------------------- /resources/favicon.ico/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/android-icon-36x36.png -------------------------------------------------------------------------------- /resources/favicon.ico/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/android-icon-48x48.png -------------------------------------------------------------------------------- /resources/favicon.ico/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/android-icon-72x72.png -------------------------------------------------------------------------------- /resources/favicon.ico/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/android-icon-96x96.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-114x114.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-120x120.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-144x144.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-152x152.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-180x180.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-57x57.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-60x60.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-72x72.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-76x76.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon-precomposed.png -------------------------------------------------------------------------------- /resources/favicon.ico/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/apple-icon.png -------------------------------------------------------------------------------- /resources/favicon.ico/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /resources/favicon.ico/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/favicon-16x16.png -------------------------------------------------------------------------------- /resources/favicon.ico/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/favicon-32x32.png -------------------------------------------------------------------------------- /resources/favicon.ico/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/favicon-96x96.png -------------------------------------------------------------------------------- /resources/favicon.ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/favicon.ico -------------------------------------------------------------------------------- /resources/favicon.ico/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /resources/favicon.ico/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/ms-icon-144x144.png -------------------------------------------------------------------------------- /resources/favicon.ico/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/ms-icon-150x150.png -------------------------------------------------------------------------------- /resources/favicon.ico/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/ms-icon-310x310.png -------------------------------------------------------------------------------- /resources/favicon.ico/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-james-v/sdfx/c23d7ebf889ff4a16a6b8d1b1159165e85c4e17d/resources/favicon.ico/ms-icon-70x70.png -------------------------------------------------------------------------------- /resources/marching-cubes.glsl: -------------------------------------------------------------------------------- 1 | const int GRID_SIZE = 256; 2 | const float CELL_SIZE = 1.0 / float(GRID_SIZE); 3 | vec2 segments[GRID_SIZE * GRID_SIZE * 2]; // space for 2 segments per cell 4 | int segmentCount = 0; 5 | 6 | // values will be a 1D array with access helpers to make it effectively 2D 7 | const int TOTAL_SIZE = GRID_SIZE * GRID_SIZE; 8 | float values[TOTAL_SIZE]; 9 | 10 | int getIndex(int x, int y) { 11 | return y * GRID_SIZE + x; 12 | } 13 | 14 | void setValue(int x, int y, float value) { 15 | values[getIndex(x, y)] = value; 16 | } 17 | 18 | float getValue(int x, int y) { 19 | return values[getIndex(x, y)]; 20 | } 21 | 22 | float sdf2D(vec2 p) { 23 | vec3 pp = vec3(p.x, p.y, 0.0); 24 | return mySdf(pp); 25 | } 26 | 27 | int getConfiguration(float tl, float tr, float br, float bl) { 28 | int config = 0; 29 | if(tl < 0.0) config |= 1; 30 | if(tr < 0.0) config |= 2; 31 | if(br < 0.0) config |= 4; 32 | if(bl < 0.0) config |= 8; 33 | return config; 34 | } 35 | 36 | vec2 interpolate(vec2 p1, vec2 p2, float v1, float v2) { 37 | float alpha = (0.0 - v1) / (v2 - v1); 38 | return mix(p1, p2, alpha); 39 | } 40 | 41 | void main() { 42 | // 1. Discretization and SDF Evaluation 43 | for(int i = 0; i <= GRID_SIZE; i++) { 44 | for(int j = 0; j <= GRID_SIZE; j++) { 45 | vec2 pos = vec2(float(i) * CELL_SIZE, float(j) * CELL_SIZE); 46 | setValue(i, j, sdf2D(pos)); 47 | } 48 | } 49 | 50 | // 2. Extract contour segments 51 | for(int i = 0; i < GRID_SIZE; i++) { 52 | for(int j = 0; j < GRID_SIZE; j++) { 53 | //int config = getConfiguration(values[i][j], values[i+1][j], values[i+1][j+1], values[i][j+1]); 54 | int config = getConfiguration(getValue(i,j), getValue(i+1,j), getValue(i+1,j+1), getValue(i,j+1)); 55 | 56 | vec2 topLeft = vec2(float(i) * CELL_SIZE, float(j) * CELL_SIZE); 57 | vec2 topRight = vec2(float(i + 1) * CELL_SIZE, float(j) * CELL_SIZE); 58 | vec2 bottomRight = vec2(float(i + 1) * CELL_SIZE, float(j + 1) * CELL_SIZE); 59 | vec2 bottomLeft = vec2(float(i) * CELL_SIZE, float(j + 1) * CELL_SIZE); 60 | 61 | // For simplicity, handling a subset of configurations here. Expand as needed. 62 | if (config == 1 || config == 14) { 63 | segments[segmentCount++] = interpolate(topLeft, bottomLeft, getValue(i,j), getValue(i,j+1)); 64 | segments[segmentCount++] = interpolate(topLeft, topRight, getValue(i,j), getValue(i+1,j)); 65 | } else if (config == 2 || config == 13) { 66 | segments[segmentCount++] = interpolate(topRight, bottomRight, getValue(i+1,j), getValue(i+1,j+1)); 67 | segments[segmentCount++] = interpolate(topLeft, topRight, getValue(i,j), getValue(i+1,j)); 68 | } else if(config == 3 || config == 12) { 69 | segments[segmentCount++] = interpolate(topLeft, bottomLeft, getValue(i,j), getValue(i,j+1)); 70 | segments[segmentCount++] = interpolate(topRight, bottomRight, getValue(i+1,j), getValue(i+1,j+1)); 71 | } else if (config == 4 || config == 11) { 72 | segments[segmentCount++] = interpolate(bottomRight, topRight, getValue(i+1,j+1), getValue(i+1,j)); 73 | segments[segmentCount++] = interpolate(bottomRight, bottomLeft, getValue(i+1,j+1), getValue(i,j+1)); 74 | } else if (config == 5) { 75 | segments[segmentCount++] = interpolate(topLeft, bottomLeft, getValue(i,j), getValue(i,j+1)); 76 | segments[segmentCount++] = interpolate(topLeft, topRight, getValue(i,j), getValue(i+1,j)); 77 | segments[segmentCount++] = interpolate(bottomRight, topRight, getValue(i+1,j+1), getValue(i+1,j)); 78 | segments[segmentCount++] = interpolate(bottomRight, bottomLeft, getValue(i+1,j+1), getValue(i,j+1)); 79 | } else if (config == 6 || config == 9) { 80 | segments[segmentCount++] = interpolate(topLeft, bottomLeft, getValue(i,j), getValue(i,j+1)); 81 | segments[segmentCount++] = interpolate(bottomRight, bottomLeft, getValue(i+1,j+1), getValue(i,j+1)); 82 | } else if (config == 7 || config == 8) { 83 | segments[segmentCount++] = interpolate(topLeft, bottomLeft, getValue(i,j), getValue(i,j+1)); 84 | segments[segmentCount++] = interpolate(bottomRight, bottomLeft, getValue(i+1,j+1), getValue(i,j+1)); 85 | segments[segmentCount++] = interpolate(bottomRight, topRight, getValue(i+1,j+1), getValue(i+1,j)); 86 | } else if (config == 10) { 87 | segments[segmentCount++] = interpolate(topLeft, topRight, getValue(i,j), getValue(i+1,j)); 88 | segments[segmentCount++] = interpolate(bottomRight, topRight, getValue(i+1,j+1), getValue(i+1,j)); 89 | segments[segmentCount++] = interpolate(bottomRight, bottomLeft, getValue(i+1,j+1), getValue(i,j+1)); 90 | } 91 | } 92 | } 93 | 94 | // You now have all segments. You'd typically sort or chain these segments to get a proper contour. 95 | 96 | // In a rendering scenario, you'd then decide how to visualize these segments. 97 | // For the purpose of sending data to JavaScript, you'd typically leverage a buffer or texture to 98 | // pass this data out, then read this buffer in JavaScript. 99 | } 100 | -------------------------------------------------------------------------------- /resources/matchbrackets.min.js: -------------------------------------------------------------------------------- 1 | !function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(r){var f=/MSIE \d/.test(navigator.userAgent)&&(null==document.documentMode||document.documentMode<8),k=r.Pos,p={"(":")>",")":"(<","[":"]>","]":"[<","{":"}>","}":"{<","<":">>",">":"<<"};function y(t){return t&&t.bracketRegex||/[(){}[\]]/}function u(t,e,n){var r=t.getLineHandle(e.line),i=e.ch-1,c=n&&n.afterCursor,a=(null==c&&(c=/(^| )cm-fat-cursor($| )/.test(t.getWrapperElement().className)),y(n)),c=!c&&0<=i&&a.test(r.text.charAt(i))&&p[r.text.charAt(i)]||a.test(r.text.charAt(i+1))&&p[r.text.charAt(++i)];return!c||(a=">"==c.charAt(1)?1:-1,n&&n.strict&&0c))for(l==e.line&&(u=e.ch-(n<0?1:0));u!=m;u+=n){var g=f.charAt(u);if(h.test(g)&&(void 0===r||(t.getTokenTypeAt(k(l,u+1))||"")==(r||""))){var d=p[g];if(d&&">"==d.charAt(1)==0summary:first-of-type{display:list-item}[aria-busy=true]{cursor:progress}[aria-disabled=true],[disabled]{cursor:not-allowed}:root{--gray-0: #f8fafb;--gray-1: #f2f4f6;--gray-2: #ebedef;--gray-3: #e0e4e5;--gray-4: #d1d6d8;--gray-5: #b1b6b9;--gray-6: #979b9d;--gray-7: #7e8282;--gray-8: #666968;--gray-9: #50514f;--gray-10: #3a3a37;--gray-11: #252521;--gray-12: #121210;--red-0: #fff5f5;--red-1: #ffe3e3;--red-2: #ffc9c9;--red-3: #ffa8a8;--red-4: #ff8787;--red-5: #ff6b6b;--red-6: #fa5252;--red-7: #f03e3e;--red-8: #e03131;--red-9: #c92a2a;--red-10: #b02525;--red-11: #962020;--red-12: #7d1a1a;--pink-0: #fff0f6;--pink-1: #ffdeeb;--pink-2: #fcc2d7;--pink-3: #faa2c1;--pink-4: #f783ac;--pink-5: #f06595;--pink-6: #e64980;--pink-7: #d6336c;--pink-8: #c2255c;--pink-9: #a61e4d;--pink-10: #8c1941;--pink-11: #731536;--pink-12: #59102a;--purple-0: #f8f0fc;--purple-1: #f3d9fa;--purple-2: #eebefa;--purple-3: #e599f7;--purple-4: #da77f2;--purple-5: #cc5de8;--purple-6: #be4bdb;--purple-7: #ae3ec9;--purple-8: #9c36b5;--purple-9: #862e9c;--purple-10: #702682;--purple-11: #5a1e69;--purple-12: #44174f;--violet-0: #f3f0ff;--violet-1: #e5dbff;--violet-2: #d0bfff;--violet-3: #b197fc;--violet-4: #9775fa;--violet-5: #845ef7;--violet-6: #7950f2;--violet-7: #7048e8;--violet-8: #6741d9;--violet-9: #5f3dc4;--violet-10: #5235ab;--violet-11: #462d91;--violet-12: #3a2578;--indigo-0: #edf2ff;--indigo-1: #dbe4ff;--indigo-2: #bac8ff;--indigo-3: #91a7ff;--indigo-4: #748ffc;--indigo-5: #5c7cfa;--indigo-6: #4c6ef5;--indigo-7: #4263eb;--indigo-8: #3b5bdb;--indigo-9: #364fc7;--indigo-10: #2f44ad;--indigo-11: #283a94;--indigo-12: #21307a;--blue-0: #e7f5ff;--blue-1: #d0ebff;--blue-2: #a5d8ff;--blue-3: #74c0fc;--blue-4: #4dabf7;--blue-5: #339af0;--blue-6: #228be6;--blue-7: #1c7ed6;--blue-8: #1971c2;--blue-9: #1864ab;--blue-10: #145591;--blue-11: #114678;--blue-12: #0d375e;--cyan-0: #e3fafc;--cyan-1: #c5f6fa;--cyan-2: #99e9f2;--cyan-3: #66d9e8;--cyan-4: #3bc9db;--cyan-5: #22b8cf;--cyan-6: #15aabf;--cyan-7: #1098ad;--cyan-8: #0c8599;--cyan-9: #0b7285;--cyan-10: #095c6b;--cyan-11: #074652;--cyan-12: #053038;--teal-0: #e6fcf5;--teal-1: #c3fae8;--teal-2: #96f2d7;--teal-3: #63e6be;--teal-4: #38d9a9;--teal-5: #20c997;--teal-6: #12b886;--teal-7: #0ca678;--teal-8: #099268;--teal-9: #087f5b;--teal-10: #066649;--teal-11: #054d37;--teal-12: #033325;--green-0: #ebfbee;--green-1: #d3f9d8;--green-2: #b2f2bb;--green-3: #8ce99a;--green-4: #69db7c;--green-5: #51cf66;--green-6: #40c057;--green-7: #37b24d;--green-8: #2f9e44;--green-9: #2b8a3e;--green-10: #237032;--green-11: #1b5727;--green-12: #133d1b;--lime-0: #f4fce3;--lime-1: #e9fac8;--lime-2: #d8f5a2;--lime-3: #c0eb75;--lime-4: #a9e34b;--lime-5: #94d82d;--lime-6: #82c91e;--lime-7: #74b816;--lime-8: #66a80f;--lime-9: #5c940d;--lime-10: #4c7a0b;--lime-11: #3c6109;--lime-12: #2c4706;--yellow-0: #fff9db;--yellow-1: #fff3bf;--yellow-2: #ffec99;--yellow-3: #ffe066;--yellow-4: #ffd43b;--yellow-5: #fcc419;--yellow-6: #fab005;--yellow-7: #f59f00;--yellow-8: #f08c00;--yellow-9: #e67700;--yellow-10: #b35c00;--yellow-11: #804200;--yellow-12: #663500;--orange-0: #fff4e6;--orange-1: #ffe8cc;--orange-2: #ffd8a8;--orange-3: #ffc078;--orange-4: #ffa94d;--orange-5: #ff922b;--orange-6: #fd7e14;--orange-7: #f76707;--orange-8: #e8590c;--orange-9: #d9480f;--orange-10: #bf400d;--orange-11: #99330b;--orange-12: #802b09;--choco-0: #fff8dc;--choco-1: #fce1bc;--choco-2: #f7ca9e;--choco-3: #f1b280;--choco-4: #e99b62;--choco-5: #df8545;--choco-6: #d46e25;--choco-7: #bd5f1b;--choco-8: #a45117;--choco-9: #8a4513;--choco-10: #703a13;--choco-11: #572f12;--choco-12: #3d210d;--brown-0: #faf4eb;--brown-1: #ede0d1;--brown-2: #e0cab7;--brown-3: #d3b79e;--brown-4: #c5a285;--brown-5: #b78f6d;--brown-6: #a87c56;--brown-7: #956b47;--brown-8: #825b3a;--brown-9: #6f4b2d;--brown-10:#5e3a21;--brown-11:#4e2b15;--brown-12: #422412;--sand-0: #f8fafb;--sand-1: #e6e4dc;--sand-2: #d5cfbd;--sand-3: #c2b9a0;--sand-4: #aea58c;--sand-5: #9a9178;--sand-6: #867c65;--sand-7: #736a53;--sand-8: #5f5746;--sand-9: #4b4639;--sand-10:#38352d;--sand-11:#252521;--sand-12: #121210;--camo-0: #f9fbe7;--camo-1: #e8ed9c;--camo-2: #d2df4e;--camo-3: #c2ce34;--camo-4: #b5bb2e;--camo-5: #a7a827;--camo-6: #999621;--camo-7: #8c851c;--camo-8: #7e7416;--camo-9: #6d6414;--camo-10: #5d5411;--camo-11: #4d460e;--camo-12: #36300a;--jungle-0: #ecfeb0;--jungle-1: #def39a;--jungle-2: #d0e884;--jungle-3: #c2dd6e;--jungle-4: #b5d15b;--jungle-5: #a8c648;--jungle-6: #9bbb36;--jungle-7: #8fb024;--jungle-8: #84a513;--jungle-9: #7a9908;--jungle-10: #658006;--jungle-11: #516605;--jungle-12: #3d4d04}html{font-family:var(--main-font);line-height:var(--rhythm);background:var(--bg);color:var(--fg);scroll-padding-block-start:calc(4*var(--gap))}footer,header,section+section{margin-block:calc(2*var(--gap))}aside.big,nav a{color:var(--accent)}nav a{text-decoration:none}aside h1,aside h2,aside h3,aside h4,aside h5,aside h6{font-size:1em;text-transform:none;letter-spacing:none}aside.big{background:0 0;border:0;-webkit-border-start:1px solid var(--muted-fg);border-inline-start:1px solid var(--muted-fg);border-radius:0;padding:0;-webkit-padding-start:var(--rhythm);padding-inline-start:var(--rhythm);font-style:italic}.\,.\,.\,.\,.\,.\,h1,h2,h3,h4,h5,h6{-webkit-margin-after:var(--gap);margin-block-end:var(--gap);font-family:var(--secondary-font);-webkit-margin-before:calc(2*var(--gap));margin-block-start:calc(2*var(--gap));position:relative}.\,.\,h1,h2{font-size:2em;text-transform:none;line-height:calc(2*var(--rhythm));letter-spacing:0}.\,h2{font-size:1.6em;line-height:calc(1.5*var(--rhythm))}.\,.\,.\,.\,h3,h4,h5,h6{font-size:1.17em;line-height:calc(1*var(--rhythm))}.\,.\,.\,h4,h5,h6{font-size:1em;text-transform:none;letter-spacing:0;-webkit-margin-before:var(--gap);margin-block-start:var(--gap)}h1+h2,h1:first-child,h2+h3,h2:first-child,h3+h4,h3:first-child,h4+h5,h4:first-child,h5+h6,h5:first-child,h6:first-child{-webkit-margin-before:var(--gap);margin-block-start:var(--gap)}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{outline:0}h1:target::before,h2:target::before,h3:target::before,h4:target::before,h5:target::before,h6:target::before{content:"";display:block;position:absolute;left:-.5em;width:4px;height:100%;background:var(--accent)}header{-webkit-border-after:1px solid var(--graphical-fg);border-block-end:1px solid var(--graphical-fg)}dt,footer,header{font-family:var(--secondary-font)}footer{-webkit-border-before:1px solid var(--graphical-fg);border-block-start:1px solid var(--graphical-fg)}body>footer,body>header,main+footer{padding:var(--rhythm) calc((100% - var(--eff-line-length))/2)}address{--density: 0}dl,hr,p{margin-block:var(--gap)}hr{color:inherit;margin-inline:0;flex:0 1 0px;-webkit-border-start:1px solid var(--accent);border-inline-start:1px solid var(--accent);block-size:auto;-webkit-border-before:1px solid var(--accent);border-block-start:1px solid var(--accent);-webkit-border-after:none;border-block-end:none;-webkit-border-end:none;border-inline-end:none}blockquote,pre{line-height:var(--rhythm)}pre{font-family:var(--mono-font);tab-size:2;margin:var(--gap) 0;overflow-x:auto;scrollbar-width:thin;scrollbar-color:var(--accent) transparent;font-size:.9em}blockquote{margin-inline:0 var(--gap);padding-inline:var(--gap) 0;margin-block:var(--gap);font-size:1.1em;font-style:italic;-webkit-border-start:1px solid var(--graphical-fg);border-inline-start:1px solid var(--graphical-fg);color:var(--muted-fg)}.italic address,.italic cite,.italic dfn,.italic em,.italic i,.italic var,blockquote address,blockquote cite,blockquote dfn,blockquote em,blockquote i,blockquote var,q address,q cite,q dfn,q em,q i,q var{font-style:normal}blockquote footer{text-align:right;text-align:end}ol,ul{-webkit-padding-start:var(--rhythm);padding-inline-start:var(--rhythm)}ol ol,ol ul,ul ol,ul ul{-webkit-padding-start:var(--gap);padding-inline-start:var(--gap)}ol[role=list],ol[role=listbox],ul[role=list],ul[role=listbox]{-webkit-padding-start:0;padding-inline-start:0;list-style:none}ol{list-style:decimal}dt{font-weight:700}dd{-webkit-margin-start:var(--rhythm);margin-inline-start:var(--rhythm)}li::marker{font-family:var(--secondary-font)}figure{max-width:100%;margin-inline:0}figcaption{margin-block:var(--gap);font-family:var(--secondary-font);color:var(--muted-fg)}main{max-inline-size:var(--eff-line-length);inline-size:100%;margin-inline:auto}main:first-child{padding-top:var(--gap)}.\,a{color:var(--link-fg, var(--accent));font-family:var(--secondary-font);border-radius:var(--border-radius);outline-offset:1px;background:0 0;border:0;font-size:1em;-webkit-text-decoration:1px dashed underline;text-decoration:1px dashed underline}.list-of-links :is(a,.\){text-decoration:none}:is(a,.\):focus,:is(a,.\):hover{-webkit-text-decoration:2px solid underline;text-decoration:2px solid underline;cursor:pointer;outline:0}small[role=note]{display:block;float:inline-end;clear:inline-end;--sidenote-width: 20ch;max-inline-size:var(--sidenote-width);padding-inline:1.5ch 1ch;-webkit-margin-end:calc(1em - var(--sidenote-width));margin-inline-end:calc(1em - var(--sidenote-width));-webkit-margin-after:var(--rhythm);margin-block-end:var(--rhythm);font-family:var(--secondary-font);background:var(--bg);border:1px solid transparent;transition:transform .1s ease-in-out}small[role=note]:focus-within,small[role=note]:hover{border:1px solid var(--graphical-fg);border-radius:var(--border-radius);transform:translateX(calc(0px - var(--sidenote-width) + min(var(--gutter-width),var(--sidenote-width))))}.\,kbd kbd,small{font-size:.8em}del,s{color:var(--bad-fg)}caption,q{font-style:italic}time{font-variant-numeric:tabular-nums}code,kbd,samp{font-family:var(--mono-font);font-style:normal}ins,samp{color:var(--ok-fg)}kbd kbd{display:inline-block;padding:0 .3em;line-height:1.1em;background:var(--interactive-bg);border:1px outset var(--graphical-fg);border-block-end-width:3px;border-radius:var(--border-radius)}sub{vertical-align:bottom}sub,sup{line-height:1}mark{background:var(--warn-bg);color:var(--warn-fg)}ins{background:var(--ok-bg)}del{background:var(--bad-bg)}audio,embed,iframe,img,object,video{max-inline-size:100%;inline-size:max-content;block-size:auto}caption{text-align:start;font-family:var(--secondary-font)}tbody{border-block:1px solid var(--faded-fg)}select[multiple],sup,td,th{vertical-align:top}td:not(:last-child),th:not(:last-child){-webkit-padding-end:var(--rhythm);padding-inline-end:var(--rhythm)}th{font-family:var(--secondary-font);text-align:start}input{display:block}label input:not([specificity-hack]){display:inline;padding-block:0}.\,button,input::file-selector-button,input[type=button],input[type=reset],input[type=submit]{display:inline-block;padding:0 calc(var(--rhythm)/4);vertical-align:middle;box-sizing:border-box;font-size:.8rem;line-height:1.125em;font-family:var(--secondary-font);min-height:var(--rhythm);background:var(--interactive-bg);border:1px solid var(--muted-fg);box-shadow:0 2px 4px -2px var(--fg);border-radius:var(--border-radius);color:var(--fg);text-decoration:none;display:inline-flex;place-items:center}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):focus-visible,:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):hover{filter:brightness(1.1);box-shadow:0 3px 6px -2px var(--fg);text-decoration:none}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):active{box-shadow:none}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):where([aria-pressed="true"], [aria-expanded="true"]){box-shadow:0 2px 4px -1px var(--fg) inset;background:var(--pressed-interactive-bg)}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):where([aria-pressed="true"], [aria-expanded="true"]):focus-visible,:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):where([aria-pressed="true"], [aria-expanded="true"]):hover{box-shadow:0 1px 3px -1px var(--fg) inset}[disabled]:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){color:var(--muted-fg);box-shadow:none}strong>:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){background:var(--accent);color:var(--bg);border:0;font-weight:700}strong>[disabled]:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){color:var(--muted-accent)}.big:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){min-block-size:calc(1.5*var(--rhythm));font-size:1rem;padding-inline:calc(.5*var(--rhythm));line-height:var(--rhythm)}input:not([type]),input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{padding:calc(var(--rhythm)/4);font-size:1rem;line-height:inherit;font-family:var(--main-font);background:var(--bg);color:var(--fg);border:1px solid var(--graphical-fg);border-radius:var(--border-radius);vertical-align:top}:is(input:not([type]),input[type="text"],input[type="search"],input[type="tel"],input[type="url"],input[type="email"],input[type="password"],input[type="date"],input[type="month"],input[type="week"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="number"],select,textarea):focus-visible{border:1px solid var(--accent)}:is(input:not([type]),input[type="text"],input[type="search"],input[type="tel"],input[type="url"],input[type="email"],input[type="password"],input[type="date"],input[type="month"],input[type="week"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="number"],select,textarea)::placeholder{color:var(--muted-fg);opacity:1;text-align:end}input[type=range]{width:100%;padding:calc(var(--gap)/4)}input[type=color]{padding:0;margin:0;height:calc(1.5*var(--rhythm));border:0;background:0 0}input[type=file]{padding:calc(var(--gap)/4) 0;font:inherit;line-height:calc(var(--rhythm)/2)}input[type=file]::file-selector-button{margin-block:.1em 0;-webkit-margin-end:1ch;margin-inline-end:1ch}optgroup::before{color:var(--muted-fg);font-style:normal}label[for]{display:block;padding-block:calc(var(--gap)/4)}fieldset>legend+*{-webkit-margin-before:0;margin-block-start:0}details:not(specificity-hack){-webkit-padding-before:0;padding-block-start:0}details:not(specificity-hack):not([open]){-webkit-padding-after:0;padding-block-end:0}summary{margin:calc(0px - var(--gap));margin-bottom:0;padding-inline:var(--gap);font-family:var(--secondary-font);font-weight:700;cursor:pointer}summary:active,summary:focus-visible{filter:brightness(.8);outline:0}dialog{inline-inset:0;block-size:-moz-fit-content;block-size:fit-content;inline-size:-moz-fit-content;inline-size:fit-content;margin:auto!important;background-color:var(--bg);color:var(--fg)}dialog[open]::backdrop{display:block;background:#000;opacity:.4;animation:bg 2s}dialog:not([open]){display:none}.box,.sidebar-layout>header,[role=menu],[role=tabpanel],aside,details,dialog,figure{margin:var(--gap) 0;padding:var(--gap);overflow:clip;border-radius:var(--border-radius);background:var(--box-bg);border:1px solid var(--graphical-fg)}.titlebar{margin-inline:calc(0px - var(--gap));-webkit-margin-after:calc(0px - var(--gap));margin-block-end:calc(0px - var(--gap));padding-inline:var(--gap);font:inherit;font-family:var(--secondary-font);font-weight:700;translate:0 calc(-1px - var(--gap));background:var(--graphical-fg);color:var(--bg)}.sub-title,sub-title{display:block;font-weight:400;color:var(--muted-fg)}.tool-bar,[role=toolbar]{display:flex;flex-flow:row wrap;gap:calc(var(--gap)/2)}.tool-bar>*,[role=toolbar]>*{margin:0}.sidebar-layout header li{margin-block:calc(.5*var(--gap))}.breadcrumbs[aria-label] [aria-current=page],.sidebar-layout header a{font-weight:700}@media (min-width:75ch){.sidebar-layout{display:grid;grid-template-columns:25ch auto;inset:0}.sidebar-layout>header{border-block:none;-webkit-border-start:none;border-inline-start:none;margin:0}.sidebar-layout>:nth-child(2){overflow:auto;--full-width: calc(100vw - 25ch);margin-top:var(--gap)}}.breadcrumbs[aria-label]{font-family:var(--secondary-font)}.breadcrumbs[aria-label] ol,.breadcrumbs[aria-label] ul{list-style:none;-webkit-padding-start:0;padding-inline-start:0}.breadcrumbs[aria-label] li{display:inline}:is(.breadcrumbs[aria-label] li)+li::before{content:' / ';display:inline}.chip,.navbar,chip{font-family:var(--secondary-font);background:var(--box-bg)}.chip,chip{border:1px solid var(--accent);border-radius:calc(var(--rhythm)/2);padding-inline:calc(var(--rhythm)/2)}.navbar{padding:var(--rhythm);-webkit-border-after:1px solid var(--accent);border-block-end:1px solid var(--accent);overflow-x:auto;scrollbar-width:thin;position:sticky;z-index:5;top:0;left:0;right:0;display:flex;flex-flow:row;align-items:center;gap:var(--gap)}.navbar.expanded{flex-flow:column;align-items:start;max-height:90vh;overflow-y:auto}.navbar.expanded ul[role=list]{flex-flow:column}.navbar *{flex-shrink:0;margin-block:0}.navbar:not(.expanded) nav>:first-child,.navbar:not(.expanded)>:first-child{-webkit-margin-start:auto;margin-inline-start:auto}.navbar:not(.expanded) nav>:last-child,.navbar:not(.expanded)>:last-child{-webkit-margin-end:auto;margin-inline-end:auto}.navbar hr{align-self:stretch}.navbar nav ul[role=list]{display:flex;flex-flow:row;gap:var(--rhythm);-webkit-padding-start:0;padding-inline-start:0}.navbar nav ul[role=list] *{flex-shrink:0}.navbar a{font-weight:700;text-decoration:none;padding-inline:.2em}.navbar a:focus,.navbar a:hover{text-decoration:underline}.navbar [aria-current=page]{position:relative}.navbar [aria-current=page]::after{width:100%;height:6px;content:"";display:block;position:absolute;bottom:calc(-1*var(--gap));background:currentcolor}.navbar.expanded [aria-current=page]::after{width:6px;height:100%;position:absolute;left:calc(-1*var(--gap));top:0}.permalink-anchor{display:none}:hover>.permalink-anchor{display:initial}button.iconbutton{border:0;background:0 0;color:currentcolor;padding:0;line-height:var(--rhythm);font-size:24px;width:24px;height:24px;display:inline-block;text-align:center;border-radius:50%;transition:font-weight .2s ease-in-out}button.iconbutton:focus-visible,button.iconbutton:hover{outline:1px solid var(--accent);outline-offset:6px}button.iconbutton:active{outline-offset:3px;background:0 0}button.iconbutton[aria-pressed=true]{box-shadow:none;transform:none}[role=tablist]{display:flex;gap:.5ch;scrollbar-width:thin}[role=tab][role=tab]{all:initial;font-family:var(--secondary-font);padding:0 calc(var(--rhythm)/4);margin:0;min-height:var(--rhythm);bottom:-1px;position:relative;color:var(--fg);border:solid var(--graphical-fg);border-width:1px;background:var(--interactive-bg);border-start-start-radius:.4em;border-start-end-radius:.4em}[role=tab][role=tab]:active,[role=tab][role=tab][aria-selected=true]{background:var(--box-bg);-webkit-border-after:1px solid transparent;border-block-end:1px solid transparent}[role=tab][role=tab]:hover{background-color:var(--box-bg);box-shadow:none}[role=tab][role=tab]:focus-visible{box-shadow:none;color:var(--accent);text-decoration:underline}[role=tabpanel]{-webkit-margin-before:0;margin-block-start:0;border-start-start-radius:0;border-start-end-radius:0;z-index:1}[role=menu]{position:absolute;z-index:10;padding:calc(var(--gap)/2) 0;margin:1px 0 0;display:flex;flex-flow:column nowrap}[role=menuitem]{padding:0 calc(var(--gap)/2);display:block;text-decoration:none;border-radius:0;color:var(--fg)}[role=menuitem]:active,[role=menuitem]:focus{background:var(--accent);color:var(--bg)}[role=listbox]{list-style:none}[role=listbox] [role=option]{margin-inline:calc(-1*var(--gap));padding-inline:var(--gap)}[role=listbox] [role=option][aria-selected=true]{background:var(--interactive-bg)}[role=listbox] .active[role=option]{--temporary-bg: var(--accent);--temporary-fg: var(--bg);--temporary-accent: parent-var(--muted-accent);--temporary-muted-accent: parent-var(--box-bg);background:var(--temporary-bg);color:var(--temporary-fg)}[role=listbox] .active[role=option]>*{--bg: var(--temporary-bg);--fg: var(--temporary-fg);--accent: var(--temporary-accent);--muted-accent: var(--temporary-muted-accent)}[aria-orientation=vertical]{flex-direction:column;width:-moz-fit-content;width:fit-content;text-align:center}.plain{--box-bg: var(--plain-bg);--accent: var(--plain-fg);--graphical-fg: var(--plain-graphical-fg)}.info{--box-bg: var(--info-bg);--accent: var(--info-fg);--graphical-fg: var(--info-graphical-fg)}.ok{--box-bg: var(--ok-bg);--accent: var(--ok-fg);--graphical-fg: var(--ok-graphical-fg)}.warn{--box-bg: var(--warn-bg);--accent: var(--warn-fg);--graphical-fg: var(--warn-graphical-fg)}.bad{--box-bg: var(--bad-bg);--accent: var(--bad-fg);--graphical-fg: var(--bad-graphical-fg)}.color{color:var(--accent)}.bg{background:var(--box-bg)}.border{border-style:solid;border-color:var(--graphical-fg)}:root{--fg: var(--gray-12);--muted-fg: var(--gray-10);--faded-fg: var(--gray-6);--graphical-fg: var(--plain-graphical-fg);--plain-fg: var(--blue-10);--info-fg: var(--blue-11);--ok-fg: var(--green-11);--bad-fg: var(--red-11);--warn-fg: var(--yellow-11);--plain-graphical-fg: var(--gray-6);--info-graphical-fg: var(--blue-6);--ok-graphical-fg: var(--green-6);--bad-graphical-fg: var(--red-6);--warn-graphical-fg: var(--yellow-6);--bg: var(--gray-0);--box-bg: var(--plain-bg);--interactive-bg: var(--gray-4);--plain-bg: var(--gray-1);--info-bg: var(--blue-1);--ok-bg: var(--green-1);--bad-bg: var(--red-1);--warn-bg: var(--yellow-1);--accent: var(--blue-10);--muted-accent: var(--blue-7);--rhythm: 1.4rem;--line-length: 40rem;--border-radius: .2rem;--main-font: 'Source Sans 3', 'Source Sans Pro', -apple-system, system-ui, sans-serif;--secondary-font: var(--main-font);--mono-font: 'M Plus Code Latin', monospace, monospace;--density: 1;--full-width: 100vw;--eff-line-length: /* Effective line length for prose. */ 2 | min( 3 | calc( var(--full-width) - (2 * var(--rhythm)) ), 4 | var(--line-length) 5 | );--gutter-width: /* Width of spaces at each side of page content. */ 6 | calc( 7 | ( 8 | var(--full-width) /* Viewport width */ 9 | - var(--eff-line-length) /* minus line width */ 10 | ) / 2)}@media (prefers-color-scheme:dark){:root:not(.-no-dark-theme){--fg: var(--gray-0);--muted-fg: var(--gray-2);--faded-fg: var(--gray-7);--plain-bg: var(--gray-11);--info-bg: var(--blue-12);--ok-bg: var(--green-12);--bad-bg: var(--red-12);--warn-bg: var(--yellow-12);--plain-faded-fg: var(--blue-6);--info-faded-fg: var(--blue-6);--ok-faded-fg: var(--green-6);--bad-faded-fg: var(--red-6);--warn-faded-fg: var(--yellow-6);--bg: var(--gray-12);--box-bg: var(--gray-10);--interactive-bg: var(--gray-8);--plain-fg: (--blue-3);--info-fg: var(--blue-3);--ok-fg: var(--green-3);--bad-fg: var(--red-3);--warn-fg: var(--yellow-3);--accent: var(--blue-3);--muted-accent: var(--blue-5)}}*{--gap: calc(var(--rhythm) * var(--density));accent-color:var(--accent)}.textcolumns{--col-width: 30ch;column-width:var(--col-width);column-gap:var(--gap);margin-block:var(--gap)}.textcolumns :first-child{-webkit-margin-before:0!important;margin-block-start:0!important}.text-align\:center{text-align:center}.center{display:grid;place-items:center}.container{max-inline-size:var(--eff-line-length);margin-inline:auto}.fullbleed,.fullscreen{position:relative;left:50%;border-radius:0;border-inline:none}.fullbleed{width:var(--full-width);transform:translateX(calc(-.5*var(--full-width)))}.fullscreen{height:100vh;width:100vw;transform:translateX(-50vw)}.width\:100\%{width:100%;max-width:100%}.height\:100\%{height:100%;max-height:100%}:is( 11 | body, 12 | .box, 13 | [role=menu], 14 | .sidebar-layout > header, 15 | [role=tabpanel], 16 | figure, 17 | details, 18 | dialog, 19 | aside, 20 | fieldset, 21 | dd, 22 | td, 23 | th 24 | )>:first-child:first-child:first-child:first-child,:is( 25 | body, 26 | .box, 27 | [role=menu], 28 | .sidebar-layout > header, 29 | [role=tabpanel], 30 | figure, 31 | details, 32 | dialog, 33 | aside, 34 | fieldset, 35 | dd, 36 | td, 37 | th 38 | )>:first-child>:first-child:first-child:first-child,:is( 39 | body, 40 | .box, 41 | [role=menu], 42 | .sidebar-layout > header, 43 | [role=tabpanel], 44 | figure, 45 | details, 46 | dialog, 47 | aside, 48 | fieldset, 49 | dd, 50 | td, 51 | th 52 | )>:first-child>:first-child>:first-child:first-child,:is( 53 | body, 54 | .box, 55 | [role=menu], 56 | .sidebar-layout > header, 57 | [role=tabpanel], 58 | figure, 59 | details, 60 | dialog, 61 | aside, 62 | fieldset, 63 | dd, 64 | td, 65 | th 66 | )>:first-child>:first-child>:first-child>:first-child{-webkit-margin-before:0;margin-block-start:0}:is( 67 | body, 68 | .box, 69 | [role=menu], 70 | .sidebar-layout > header, 71 | [role=tabpanel], 72 | figure, 73 | details, 74 | dialog, 75 | aside, 76 | fieldset, 77 | dd, 78 | td, 79 | th 80 | )>:last-child:last-child:last-child:last-child,:is( 81 | body, 82 | .box, 83 | [role=menu], 84 | .sidebar-layout > header, 85 | [role=tabpanel], 86 | figure, 87 | details, 88 | dialog, 89 | aside, 90 | fieldset, 91 | dd, 92 | td, 93 | th 94 | )>:last-child>:last-child:last-child:last-child,:is( 95 | body, 96 | .box, 97 | [role=menu], 98 | .sidebar-layout > header, 99 | [role=tabpanel], 100 | figure, 101 | details, 102 | dialog, 103 | aside, 104 | fieldset, 105 | dd, 106 | td, 107 | th 108 | )>:last-child>:last-child>:last-child:last-child,:is( 109 | body, 110 | .box, 111 | [role=menu], 112 | .sidebar-layout > header, 113 | [role=tabpanel], 114 | figure, 115 | details, 116 | dialog, 117 | aside, 118 | fieldset, 119 | dd, 120 | td, 121 | th 122 | )>:last-child>:last-child>:last-child>:last-child{-webkit-margin-after:0;margin-block-end:0}.padding{padding-inline:var(--gap)}.padding-block{padding-block:var(--gap)}.padding-block-start{-webkit-padding-before:var(--gap);padding-block-start:var(--gap)}.padding-block-end{-webkit-padding-after:var(--gap);padding-block-end:var(--gap)}.padding-inline{padding-inline:var(--gap)}.padding-inline-end,.padding-inline-start{-webkit-padding-start:var(--gap);padding-inline-start:var(--gap)}.margin{margin:var(--gap)}.margin-block{margin-block:var(--gap)}.margin-block-start{-webkit-margin-before:var(--gap);margin-block-start:var(--gap)}.margin-block-end{-webkit-margin-after:var(--gap);margin-block-end:var(--gap)}.margin-inline{margin-inline:var(--gap)}.margin-inline-start{-webkit-margin-start:var(--gap);margin-inline-start:var(--gap)}.margin-inline-end{-webkit-margin-end:var(--gap);margin-inline-end:var(--gap)}.flow-gap>:not(:last-child){margin-bottom:1em}.inline{display:inline}.block{display:block}.contents{display:contents}.table{display:table;width:100%;margin:0}.row,.rows>*{display:table-row}.row:not(:last-child):not([specificity-hack])>*,.rows>:not(:last-child):not([specificity-hack])>*{margin-bottom:var(--gap)}.row>:not([specificity-hack]),.rows>*>:not([specificity-hack]){display:table-cell;vertical-align:top}.row>*+:not([specificity-hack]),:is(.rows > *)>*+:not([specificity-hack]){-webkit-margin-start:var(--gap);margin-inline-start:var(--gap);display:inline-block}.big{font-size:1.4em;line-height:calc(1.5*var(--rhythm))}.fixed{position:fixed}.sticky{position:sticky}.top{top:0}.right{right:0}.bottom{bottom:0}.left{left:0}.float\:left{float:left}.float\:right{float:right}.overflow\:auto{overflow:auto}.overflow\:scroll{overflow:scroll}.airy{--density: 3}.spacious{--density: 2}.dense{--density: 1}.crowded{--density: .5}.packed{--density: 0}.autodensity{--density: 1 123 | }@media (min-width:768px){.autodensity{--density: 2 124 | }}@media (min-width:1024px){.autodensity{--density: 3 125 | }}.vh,v-h{clip:rect(0 0 0 0);-webkit-clip-path:inset(50%);clip-path:inset(50%);block-size:1px;inline-size:1px;overflow:hidden;white-space:nowrap}.all\:initial{all:initial}.bold{font-weight:700}.italic{font-style:italic}.allcaps{text-transform:uppercase;letter-spacing:.1rem}.primary-font{font-family:var(--primary-font)}.secondary-font{font-family:var(--secondary-font)}.display-font{font-family:var(--display-font)}.mono-font,.monospace{font-family:var(--mono-font)}.massivetext{font-size:calc(.13*var(--eff-line-length));line-height:1em;letter-spacing:0}.aestheticbreak{display:block;margin:0;padding:0;height:calc(.5*var(--gap))}.f-row{display:flex;flex-direction:row;gap:var(--gap)}.f-row>*{margin:0}.f-col{display:flex;flex-direction:column;gap:var(--gap)}.f-col>*{margin:0}.f-switch{display:flex;flex-wrap:wrap;gap:var(--gap);--f-switch-threshold: 55ch 126 | }.f-switch>*{margin:0;flex-grow:1;flex-basis:calc((var(--f-switch-threshold) - 100%)*999)}.justify-content\:start{justify-content:start}.justify-content\:end{justify-content:end}.justify-content\:baseline{justify-content:baseline}.justify-content\:center{justify-content:center}.justify-content\:stretch{justify-content:stretch}.justify-content\:space-between{justify-content:space-between}.justify-content\:space-around{justify-content:space-around}.justify-content\:space-evenly{justify-content:space-evenly}.align-items\:start{align-items:start}.align-items\:end{align-items:end}.align-items\:baseline{align-items:baseline}.align-items\:center{align-items:center}.align-items\:stretch{align-items:stretch}.align-self\:start{align-self:start}.align-self\:end{align-self:end}.align-self\:baseline{align-self:baseline}.align-self\:center{align-self:center}.align-self\:stretch{align-self:stretch}.flex-grow\:0{flex-grow:0}.flex-grow\:1{flex-grow:1}.flex-grow\:2{flex-grow:2}.flex-grow\:3{flex-grow:3}.flex-grow\:4{flex-grow:4}.flex-grow\:5{flex-grow:5}.flex-grow\:6{flex-grow:6}.flex-grow\:7{flex-grow:7}.flex-grow\:8{flex-grow:8}.flex-grow\:9{flex-grow:9}.flex-grow\:10{flex-grow:10}.flex-grow\:11{flex-grow:11}.flex-grow\:12{flex-grow:12}.flex-wrap\:wrap{flex-wrap:wrap}.flex-wrap\:nowrap{flex-wrap:nowrap}.grid{display:grid;grid-auto-columns:var(--grid-col-width, 1fr);grid-auto-rows:var(--grid-row-width, auto);gap:var(--gap)}.grid>*{margin:0}.grid-even-rows{--grid-row-width: 1fr}.grid-variable-cols{--grid-column-width: auto}[data-cols^="1 "]{grid-column-start:1}[data-cols$=" 1"]{grid-column-end:2}[data-cols="1"]{grid-column:1}[data-cols^="2 "]{grid-column-start:2}[data-cols$=" 2"]{grid-column-end:3}[data-cols="2"]{grid-column:2}[data-cols^="3 "]{grid-column-start:3}[data-cols$=" 3"]{grid-column-end:4}[data-cols="3"]{grid-column:3}[data-cols^="4 "]{grid-column-start:4}[data-cols$=" 4"]{grid-column-end:5}[data-cols="4"]{grid-column:4}[data-cols^="5 "]{grid-column-start:5}[data-cols$=" 5"]{grid-column-end:6}[data-cols="5"]{grid-column:5}[data-cols^="6 "]{grid-column-start:6}[data-cols$=" 6"]{grid-column-end:7}[data-cols="6"]{grid-column:6}[data-cols^="7 "]{grid-column-start:7}[data-cols$=" 7"]{grid-column-end:8}[data-cols="7"]{grid-column:7}[data-cols^="8 "]{grid-column-start:8}[data-cols$=" 8"]{grid-column-end:9}[data-cols="8"]{grid-column:8}[data-cols^="9 "]{grid-column-start:9}[data-cols$=" 9"]{grid-column-end:10}[data-cols="9"]{grid-column:9}[data-cols^="10 "]{grid-column-start:10}[data-cols$=" 10"]{grid-column-end:11}[data-cols="10"]{grid-column:10}[data-cols^="11 "]{grid-column-start:11}[data-cols$=" 11"]{grid-column-end:12}[data-cols="11"]{grid-column:11}[data-cols^="12 "]{grid-column-start:12}[data-cols$=" 12"]{grid-column-end:13}[data-cols="12"]{grid-column:12}[data-rows^="1 "]{grid-row-start:1}[data-rows$=" 1"]{grid-row-end:2}[data-rows="1"]{grid-row:1}[data-rows^="2 "]{grid-row-start:2}[data-rows$=" 2"]{grid-row-end:3}[data-rows="2"]{grid-row:2}[data-rows^="3 "]{grid-row-start:3}[data-rows$=" 3"]{grid-row-end:4}[data-rows="3"]{grid-row:3}[data-rows^="4 "]{grid-row-start:4}[data-rows$=" 4"]{grid-row-end:5}[data-rows="4"]{grid-row:4}[data-rows^="5 "]{grid-row-start:5}[data-rows$=" 5"]{grid-row-end:6}[data-rows="5"]{grid-row:5}[data-rows^="6 "]{grid-row-start:6}[data-rows$=" 6"]{grid-row-end:7}[data-rows="6"]{grid-row:6}[data-rows^="7 "]{grid-row-start:7}[data-rows$=" 7"]{grid-row-end:8}[data-rows="7"]{grid-row:7}[data-rows^="8 "]{grid-row-start:8}[data-rows$=" 8"]{grid-row-end:9}[data-rows="8"]{grid-row:8}[data-rows^="9 "]{grid-row-start:9}[data-rows$=" 9"]{grid-row-end:10}[data-rows="9"]{grid-row:9}[data-rows^="10 "]{grid-row-start:10}[data-rows$=" 10"]{grid-row-end:11}[data-rows="10"]{grid-row:10}[data-rows^="11 "]{grid-row-start:11}[data-rows$=" 11"]{grid-row-end:12}[data-rows="11"]{grid-row:11}[data-rows^="12 "]{grid-row-start:12}[data-rows$=" 12"]{grid-row-end:13}[data-rows="12"]{grid-row:12}@media (max-width:768px){[data-cols\@s^="1 "]{grid-column-start:1}[data-cols\@s$=" 1"]{grid-column-end:2}[data-cols\@s="1"]{grid-column:1}[data-cols\@s^="2 "]{grid-column-start:2}[data-cols\@s$=" 2"]{grid-column-end:3}[data-cols\@s="2"]{grid-column:2}[data-cols\@s^="3 "]{grid-column-start:3}[data-cols\@s$=" 3"]{grid-column-end:4}[data-cols\@s="3"]{grid-column:3}[data-cols\@s^="4 "]{grid-column-start:4}[data-cols\@s$=" 4"]{grid-column-end:5}[data-cols\@s="4"]{grid-column:4}[data-cols\@s^="5 "]{grid-column-start:5}[data-cols\@s$=" 5"]{grid-column-end:6}[data-cols\@s="5"]{grid-column:5}[data-cols\@s^="6 "]{grid-column-start:6}[data-cols\@s$=" 6"]{grid-column-end:7}[data-cols\@s="6"]{grid-column:6}[data-cols\@s^="7 "]{grid-column-start:7}[data-cols\@s$=" 7"]{grid-column-end:8}[data-cols\@s="7"]{grid-column:7}[data-cols\@s^="8 "]{grid-column-start:8}[data-cols\@s$=" 8"]{grid-column-end:9}[data-cols\@s="8"]{grid-column:8}[data-cols\@s^="9 "]{grid-column-start:9}[data-cols\@s$=" 9"]{grid-column-end:10}[data-cols\@s="9"]{grid-column:9}[data-cols\@s^="10 "]{grid-column-start:10}[data-cols\@s$=" 10"]{grid-column-end:11}[data-cols\@s="10"]{grid-column:10}[data-cols\@s^="11 "]{grid-column-start:11}[data-cols\@s$=" 11"]{grid-column-end:12}[data-cols\@s="11"]{grid-column:11}[data-cols\@s^="12 "]{grid-column-start:12}[data-cols\@s$=" 12"]{grid-column-end:13}[data-cols\@s="12"]{grid-column:12}[data-rows\@s^="1 "]{grid-row-start:1}[data-rows\@s$=" 1"]{grid-row-end:2}[data-rows\@s="1"]{grid-row:1}[data-rows\@s^="2 "]{grid-row-start:2}[data-rows\@s$=" 2"]{grid-row-end:3}[data-rows\@s="2"]{grid-row:2}[data-rows\@s^="3 "]{grid-row-start:3}[data-rows\@s$=" 3"]{grid-row-end:4}[data-rows\@s="3"]{grid-row:3}[data-rows\@s^="4 "]{grid-row-start:4}[data-rows\@s$=" 4"]{grid-row-end:5}[data-rows\@s="4"]{grid-row:4}[data-rows\@s^="5 "]{grid-row-start:5}[data-rows\@s$=" 5"]{grid-row-end:6}[data-rows\@s="5"]{grid-row:5}[data-rows\@s^="6 "]{grid-row-start:6}[data-rows\@s$=" 6"]{grid-row-end:7}[data-rows\@s="6"]{grid-row:6}[data-rows\@s^="7 "]{grid-row-start:7}[data-rows\@s$=" 7"]{grid-row-end:8}[data-rows\@s="7"]{grid-row:7}[data-rows\@s^="8 "]{grid-row-start:8}[data-rows\@s$=" 8"]{grid-row-end:9}[data-rows\@s="8"]{grid-row:8}[data-rows\@s^="9 "]{grid-row-start:9}[data-rows\@s$=" 9"]{grid-row-end:10}[data-rows\@s="9"]{grid-row:9}[data-rows\@s^="10 "]{grid-row-start:10}[data-rows\@s$=" 10"]{grid-row-end:11}[data-rows\@s="10"]{grid-row:10}[data-rows\@s^="11 "]{grid-row-start:11}[data-rows\@s$=" 11"]{grid-row-end:12}[data-rows\@s="11"]{grid-row:11}[data-rows\@s^="12 "]{grid-row-start:12}[data-rows\@s$=" 12"]{grid-row-end:13}[data-rows\@s="12"]{grid-row:12}}@media (min-width:1024px){[data-cols\@l^="1 "]{grid-column-start:1}[data-cols\@l$=" 1"]{grid-column-end:2}[data-cols\@l="1"]{grid-column:1}[data-cols\@l^="2 "]{grid-column-start:2}[data-cols\@l$=" 2"]{grid-column-end:3}[data-cols\@l="2"]{grid-column:2}[data-cols\@l^="3 "]{grid-column-start:3}[data-cols\@l$=" 3"]{grid-column-end:4}[data-cols\@l="3"]{grid-column:3}[data-cols\@l^="4 "]{grid-column-start:4}[data-cols\@l$=" 4"]{grid-column-end:5}[data-cols\@l="4"]{grid-column:4}[data-cols\@l^="5 "]{grid-column-start:5}[data-cols\@l$=" 5"]{grid-column-end:6}[data-cols\@l="5"]{grid-column:5}[data-cols\@l^="6 "]{grid-column-start:6}[data-cols\@l$=" 6"]{grid-column-end:7}[data-cols\@l="6"]{grid-column:6}[data-cols\@l^="7 "]{grid-column-start:7}[data-cols\@l$=" 7"]{grid-column-end:8}[data-cols\@l="7"]{grid-column:7}[data-cols\@l^="8 "]{grid-column-start:8}[data-cols\@l$=" 8"]{grid-column-end:9}[data-cols\@l="8"]{grid-column:8}[data-cols\@l^="9 "]{grid-column-start:9}[data-cols\@l$=" 9"]{grid-column-end:10}[data-cols\@l="9"]{grid-column:9}[data-cols\@l^="10 "]{grid-column-start:10}[data-cols\@l$=" 10"]{grid-column-end:11}[data-cols\@l="10"]{grid-column:10}[data-cols\@l^="11 "]{grid-column-start:11}[data-cols\@l$=" 11"]{grid-column-end:12}[data-cols\@l="11"]{grid-column:11}[data-cols\@l^="12 "]{grid-column-start:12}[data-cols\@l$=" 12"]{grid-column-end:13}[data-cols\@l="12"]{grid-column:12}[data-rows\@l^="1 "]{grid-row-start:1}[data-rows\@l$=" 1"]{grid-row-end:2}[data-rows\@l="1"]{grid-row:1}[data-rows\@l^="2 "]{grid-row-start:2}[data-rows\@l$=" 2"]{grid-row-end:3}[data-rows\@l="2"]{grid-row:2}[data-rows\@l^="3 "]{grid-row-start:3}[data-rows\@l$=" 3"]{grid-row-end:4}[data-rows\@l="3"]{grid-row:3}[data-rows\@l^="4 "]{grid-row-start:4}[data-rows\@l$=" 4"]{grid-row-end:5}[data-rows\@l="4"]{grid-row:4}[data-rows\@l^="5 "]{grid-row-start:5}[data-rows\@l$=" 5"]{grid-row-end:6}[data-rows\@l="5"]{grid-row:5}[data-rows\@l^="6 "]{grid-row-start:6}[data-rows\@l$=" 6"]{grid-row-end:7}[data-rows\@l="6"]{grid-row:6}[data-rows\@l^="7 "]{grid-row-start:7}[data-rows\@l$=" 7"]{grid-row-end:8}[data-rows\@l="7"]{grid-row:7}[data-rows\@l^="8 "]{grid-row-start:8}[data-rows\@l$=" 8"]{grid-row-end:9}[data-rows\@l="8"]{grid-row:8}[data-rows\@l^="9 "]{grid-row-start:9}[data-rows\@l$=" 9"]{grid-row-end:10}[data-rows\@l="9"]{grid-row:9}[data-rows\@l^="10 "]{grid-row-start:10}[data-rows\@l$=" 10"]{grid-row-end:11}[data-rows\@l="10"]{grid-row:10}[data-rows\@l^="11 "]{grid-row-start:11}[data-rows\@l$=" 11"]{grid-row-end:12}[data-rows\@l="11"]{grid-row:11}[data-rows\@l^="12 "]{grid-row-start:12}[data-rows\@l$=" 12"]{grid-row-end:13}[data-rows\@l="12"]{grid-row:12}} -------------------------------------------------------------------------------- /resources/nord.min.css: -------------------------------------------------------------------------------- 1 | .cm-s-nord.CodeMirror{background:#2e3440;color:#d8dee9}.cm-s-nord div.CodeMirror-selected{background:#434c5e}.cm-s-nord .CodeMirror-line::selection,.cm-s-nord .CodeMirror-line>span::selection,.cm-s-nord .CodeMirror-line>span>span::selection{background:#3b4252}.cm-s-nord .CodeMirror-line::-moz-selection,.cm-s-nord .CodeMirror-line>span::-moz-selection,.cm-s-nord .CodeMirror-line>span>span::-moz-selection{background:#3b4252}.cm-s-nord .CodeMirror-gutters{background:#2e3440;border-right:0}.cm-s-nord .CodeMirror-guttermarker{color:#4c566a}.cm-s-nord .CodeMirror-guttermarker-subtle{color:#4c566a}.cm-s-nord .CodeMirror-linenumber{color:#4c566a}.cm-s-nord .CodeMirror-cursor{border-left:1px solid #f8f8f0}.cm-s-nord span.cm-comment{color:#4c566a}.cm-s-nord span.cm-atom{color:#b48ead}.cm-s-nord span.cm-number{color:#b48ead}.cm-s-nord span.cm-comment.cm-attribute{color:#97b757}.cm-s-nord span.cm-comment.cm-def{color:#bc9262}.cm-s-nord span.cm-comment.cm-tag{color:#bc6283}.cm-s-nord span.cm-comment.cm-type{color:#5998a6}.cm-s-nord span.cm-attribute,.cm-s-nord span.cm-property{color:#8fbcbb}.cm-s-nord span.cm-keyword{color:#81a1c1}.cm-s-nord span.cm-builtin{color:#81a1c1}.cm-s-nord span.cm-string{color:#a3be8c}.cm-s-nord span.cm-variable{color:#d8dee9}.cm-s-nord span.cm-variable-2{color:#d8dee9}.cm-s-nord span.cm-type,.cm-s-nord span.cm-variable-3{color:#d8dee9}.cm-s-nord span.cm-def{color:#8fbcbb}.cm-s-nord span.cm-bracket{color:#81a1c1}.cm-s-nord span.cm-tag{color:#bf616a}.cm-s-nord span.cm-header{color:#b48ead}.cm-s-nord span.cm-link{color:#b48ead}.cm-s-nord span.cm-error{background:#bf616a;color:#f8f8f0}.cm-s-nord .CodeMirror-activeline-background{background:#3b4252}.cm-s-nord .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important} -------------------------------------------------------------------------------- /resources/render.js: -------------------------------------------------------------------------------- 1 | const sdfSrcContainer = document.querySelector("#sdf-src-container"); 2 | 3 | let canvas; 4 | let gl; 5 | let ext; 6 | 7 | // Mouse and Keyboard Events for controlling the SDF render 8 | let mouseDown = false; 9 | let ctrlDown = false; 10 | let lastMouseX = null; 11 | let lastMouseY = null; 12 | 13 | function handleKeyDown(event) { 14 | if (event.key == "Shift") { 15 | ctrlDown = true; 16 | } 17 | } 18 | 19 | function handleKeyUp(event) { 20 | if (event.key == "Shift") { 21 | ctrlDown = false; 22 | } 23 | } 24 | 25 | window.addEventListener('keydown', handleKeyDown); 26 | window.addEventListener('keyup', handleKeyUp); 27 | 28 | let rotationAngleZ = 0.0; // in radians 29 | let rotationAngleScreenX = 0.0 30 | let translationX = 0.0; 31 | let translationY = 0.0; 32 | let zoom = 1.0; 33 | 34 | // Named event listeners 35 | 36 | function handleWebGLContextLost(event) { 37 | event.preventDefault(); 38 | } 39 | 40 | function handleWheelEvent(event) { 41 | event.preventDefault(); 42 | } 43 | 44 | function handleMouseDownEvent(e) { 45 | mouseDown = true; 46 | lastMouseX = e.clientX; 47 | lastMouseY = e.clientY; 48 | } 49 | 50 | function handleMouseUpEvent() { 51 | mouseDown = false; 52 | } 53 | 54 | function handleWheelZoom(e) { 55 | const zoomAmount = e.deltaY * 0.01; 56 | zoom += zoomAmount; 57 | } 58 | 59 | function handleMouseMoveEvent(e) { 60 | if (!mouseDown) { 61 | return; 62 | } 63 | 64 | const deltaX = e.clientX - lastMouseX; 65 | const deltaY = e.clientY - lastMouseY; 66 | 67 | if (ctrlDown) { 68 | rotationAngleZ += deltaX * 0.5; 69 | rotationAngleScreenX += deltaY * 0.5; 70 | } 71 | 72 | if (!ctrlDown) { 73 | translationX += deltaX * 0.005; 74 | translationY += deltaY * 0.005; 75 | } 76 | 77 | lastMouseX = e.clientX; 78 | lastMouseY = e.clientY; 79 | } 80 | 81 | // Init context function 82 | 83 | function initContext() { 84 | canvas = document.querySelector("#canvas"); 85 | gl = canvas.getContext("webgl2", { 86 | preserveDrawingBuffer: true 87 | }); 88 | 89 | ext = gl.getExtension("WEBGL_lose_context"); 90 | 91 | if (!gl) { 92 | alert("WebGL is not available on your browser."); 93 | } 94 | 95 | // Add event listeners 96 | canvas.addEventListener("webglcontextlost", handleWebGLContextLost, false); 97 | canvas.addEventListener("webglcontextrestored", initContext, false); 98 | canvas.addEventListener('wheel', handleWheelEvent, false); 99 | canvas.addEventListener('mousedown', handleMouseDownEvent, false); 100 | canvas.addEventListener('mouseup', handleMouseUpEvent, false); 101 | canvas.addEventListener('wheel', handleWheelZoom, false); 102 | canvas.addEventListener('mousemove', handleMouseMoveEvent, false); 103 | } 104 | 105 | // Define vertices for a quad. This quad is what the fragment shader result is rendered to 106 | const quadVertices = new Float32Array([ 107 | -1.0, 1.0, // Vertex 1 108 | -1.0, -1.0, // Vertex 2 109 | 1.0, 1.0, // Vertex 3 110 | 1.0, 1.0, // Vertex 4 (repeat Vertex 3) 111 | -1.0, -1.0, // Vertex 5 (repeat Vertex 2) 112 | 1.0, -1.0 // Vertex 6 113 | ]); 114 | 115 | // The first chunk of the fragment shader code. 116 | // This contains the implementations for the SDF functions. That is, the primitives and transformations 117 | // it is not complete on its own and must be combined with: 118 | // 1. the SDF code (taken from a hidden div, populated by the backend) 119 | // 2. the second part of the fragment shader code which has the raymarcher, sets the scene, and renders it with the main() function 120 | 121 | // SDF function name conventions used: 122 | // - any primitive starts with the prefix 'sd' and is CamelCased eg. sdBox 123 | // - any transform starts with the prefix 'op' and is CamelCased eg. opUnion, opTranslate 124 | // this convention just comes directly from Inigo Quilez's code on his website: https://iquilezles.org/articles/distfunctions/ 125 | // note that not everything is a 1:1 copy from that site, for example the transforms are implemented differently in this code. 126 | 127 | const fragCodeA = `#version 300 es 128 | 129 | precision highp float; 130 | 131 | uniform vec2 iResolution; 132 | uniform float u_rotationAngleZ; 133 | uniform float u_rotationAngleScreenX; 134 | uniform float u_translationX; 135 | uniform float u_translationY; 136 | uniform float u_zoom; 137 | out vec4 outputColor; 138 | 139 | const int MAX_STEPS = 100; 140 | 141 | vec3 lightDir = normalize(vec3(-0.3, -1.0, -0.4)); 142 | 143 | float sdBox( vec3 p, vec3 b ) { 144 | vec3 q = abs(p) - b; 145 | return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0); 146 | } 147 | 148 | float sdSphere(vec3 p, float r) { 149 | return length(p) - r; 150 | } 151 | 152 | vec2 opRevolve( vec3 p ) { 153 | return vec2( length(p.xz), p.y ); 154 | } 155 | 156 | float sdPlane(vec3 p, vec3 n, float h) { 157 | // n must be normalized 158 | return dot(p, n) - h; 159 | } 160 | 161 | float sdWedge(vec3 p, float deg) { 162 | float theta = atan(p.z, p.x); // angle in radians 163 | if (theta < 0.0) theta += 2.0 * 3.14159265; // map theta to [0, 2PI] 164 | 165 | float rad = radians(deg); // convert deg to radians 166 | float r = 1000.0; 167 | vec3 p1 = vec3(r*cos(rad), 0.0, r*sin(rad)); 168 | vec3 n1 = vec3(r*-sin(rad), 0.0, r*cos(rad)); 169 | vec3 p2 = vec3(r, 0.0, 0.0); 170 | vec3 n2 = vec3(0.0, 0.0, -1.0); 171 | 172 | float d1 = sdPlane(p, n1, dot(n1, p1)); 173 | float d2 = sdPlane(p, n2, dot(n2, p2)); 174 | float dcap = max(-p.y, p.y - 1000.0); // assuming unit height cylinder 175 | 176 | if (theta > rad) { 177 | return length(max(vec2(d1, d2), 0.0)) - length(min(vec2(d1, d2), 0.0)); 178 | } 179 | return dcap; 180 | } 181 | 182 | //vec2 opExtrudeAlong( vec3 p ) { 183 | // return vec2( length(p.xz), p.y );/ 184 | //} 185 | 186 | float opExtrude( vec3 p, float sdf, float h ) { 187 | vec2 w = vec2( sdf, abs(p.z) - h); 188 | return min(max(w.x,w.y), 0.0) + length(max(w,0.0)); 189 | } 190 | 191 | // https://iquilezles.org/articles/distfunctions2d/ 192 | // create 2D shape fns that take a 3D pt so that they can be viewed as-is 193 | 194 | vec3 opSlice( vec2 p, float h ) { 195 | return vec3( p, h ); 196 | } 197 | 198 | //vec3 opSlice( vec3 p, float h ) { 199 | // return vec3( p.xy, h ); 200 | //} 201 | 202 | float sdCircle( vec2 p, float r ) { 203 | return length(p.xy) - r; 204 | } 205 | 206 | float sdCircle( vec3 p, float r ) { 207 | return opExtrude(p, sdCircle(p.xy, r), 0.001); 208 | } 209 | 210 | float sdPolygon( vec2 p, vec2 v[200], int num ) { 211 | float d = dot(p-v[0],p-v[0]); 212 | float s = 1.0; 213 | for( int i=0, j=num-1; i=v[i].y, 221 | p.y e.y*w.x ); 223 | if( all(cond) || all(not(cond)) ) s=-s; 224 | } 225 | return s*sqrt(d); 226 | } 227 | 228 | float sdPolygon( vec3 p, vec2 v[200], int num ) { return opExtrude(p, sdPolygon(p.xy, v, num), 1.0); } 229 | 230 | // WIP: can I 'extrude' by just subtracting the dist of a 2D sdf? 231 | float sdLine( in vec3 p, in vec3 a, in vec3 b, float sdf ) { 232 | vec3 pa = p-a, ba = b-a; 233 | float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); 234 | return length( pa - ba*h ) - sdf; 235 | } 236 | 237 | vec3 opTranslate( vec3 p, vec3 d) { return(p - d); } 238 | vec2 opTranslate( vec2 p, vec3 d) { return(p - d.xy); } 239 | vec2 opTranslate( vec2 p, vec2 d) { return(p - d); } 240 | 241 | vec3 opRotateX( vec3 p, float theta) { 242 | theta = radians(theta); 243 | // Rotation about X-axis 244 | mat3 Rx = mat3( 245 | 1.0, 0.0, 0.0, 246 | 0.0, cos(theta), -sin(theta), 247 | 0.0, sin(theta), cos(theta) 248 | ); 249 | return Rx * p; 250 | } 251 | 252 | vec3 opRotateY( vec3 p, float theta) { 253 | theta = radians(theta); 254 | // Rotation about Y-axis 255 | mat3 Ry = mat3( 256 | cos(theta), 0.0, sin(theta), 257 | 0.0, 1.0, 0.0, 258 | -sin(theta), 0.0, cos(theta) 259 | ); 260 | return Ry * p; 261 | } 262 | 263 | vec3 opRotateZ( vec3 p, float theta) { 264 | theta = radians(theta); 265 | // Rotation about Z-axis 266 | mat3 Rz = mat3( 267 | cos(theta), -sin(theta), 0.0, 268 | sin(theta), cos(theta), 0.0, 269 | 0.0, 0.0, 1.0 270 | ); 271 | return Rz * p; 272 | } 273 | 274 | vec3 opRotate( vec3 p, vec3 rs) { 275 | float thetaX = rs.x; 276 | float thetaY = rs.y; 277 | float thetaZ = rs.z; 278 | 279 | p = opRotateZ(p, thetaZ); 280 | p = opRotateY(p, thetaY); 281 | p = opRotateX(p, thetaX); 282 | return p; 283 | } 284 | vec2 opRotate( vec2 p, vec3 rs) { 285 | vec3 pp = opRotateZ( vec3(p, 0.0), rs.z); 286 | return pp.xy; 287 | } 288 | vec2 opRotate( vec2 p, float theta) { 289 | vec3 pp = opRotateZ( vec3(p, 0.0), theta); 290 | return pp.xy; 291 | } 292 | 293 | float opOnion( in float d, in float t ) { 294 | return abs(d)-t; 295 | } 296 | 297 | vec3 opRepetition( in vec3 p, in vec3 s ) { 298 | vec3 q = p - s*round(p/s); 299 | return q; 300 | } 301 | 302 | float opUnion( float d1, float d2 ) { return min(d1,d2); } 303 | 304 | float opDifference( float d1, float d2 ) { return max(-d1,d2); } 305 | 306 | float opIntersection( float d1, float d2 ) { return max(d1,d2); } 307 | 308 | float opSmoothUnion( float d1, float d2, float k ) { 309 | float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 ); 310 | return mix( d2, d1, h ) - k*h*(1.0-h); } 311 | 312 | float opSmoothDifference( float d1, float d2, float k ) { 313 | float h = clamp( 0.5 - 0.5*(d2+d1)/k, 0.0, 1.0 ); 314 | return mix( d2, -d1, h ) + k*h*(1.0-h); } 315 | 316 | float opSmoothIntersection( float d1, float d2, float k ) { 317 | float h = clamp( 0.5 - 0.5*(d2-d1)/k, 0.0, 1.0 ); 318 | return mix( d2, d1, h ) + k*h*(1.0-h); } 319 | 320 | // utils 321 | 322 | float dot2( in vec2 v ) { return dot(v,v); } 323 | float dot2( in vec3 v ) { return dot(v,v); } 324 | float ndot( in vec2 a, in vec2 b ) { return a.x*b.x - a.y*b.y; } 325 | 326 | mat3 rotationMatrix(vec3 axis, float angle) { 327 | float s = sin(angle); 328 | float c = cos(angle); 329 | float oc = 1.0 - c; 330 | 331 | return mat3(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 332 | oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 333 | oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c); 334 | } 335 | 336 | `; 337 | 338 | const fragCodeB = ` 339 | vec3 normal(vec3 p) { 340 | vec2 e = vec2(0.0001, 0.0); 341 | return normalize(vec3(mySdf(p + e.xyy) - mySdf(p - e.xyy), 342 | mySdf(p + e.yxy) - mySdf(p - e.yxy), 343 | mySdf(p + e.yyx) - mySdf(p - e.yyx))); 344 | } 345 | 346 | struct OrthoCamera { 347 | vec3 position; 348 | vec3 forward; 349 | vec3 up; 350 | vec3 rightv; 351 | float left; 352 | float right; 353 | float bottom; 354 | float top; 355 | }; 356 | 357 | OrthoCamera createOrthoCamera(vec3 pos, vec3 lookAt, float width, float height) { 358 | OrthoCamera cam; 359 | cam.position = pos; 360 | cam.forward = normalize(lookAt - pos); 361 | cam.up = vec3(0, 0, 1); // assuming Z is up 362 | cam.rightv = normalize(cross(cam.up, cam.forward)); 363 | 364 | // how to avoid gimbal lock and other flipping problems? 365 | cam.position = rotationMatrix(cam.rightv, clamp(u_rotationAngleScreenX, -87.0, 38.0) * -0.025) * pos; 366 | cam.forward = normalize(lookAt - cam.position); 367 | cam.up = vec3(0, 0, 1); // assuming Z is up 368 | cam.rightv = normalize(cross(cam.up, cam.forward)); 369 | 370 | cam.up = cross(cam.forward, cam.rightv); // Re-orthogonalize up vector to ensure it's perpendicular 371 | 372 | cam.left = -width / 2.0; 373 | cam.right = width / 2.0; 374 | cam.bottom = -height / 2.0; 375 | cam.top = height / 2.0; 376 | 377 | return cam; 378 | } 379 | 380 | struct Ray { 381 | vec3 origin; 382 | vec3 direction; 383 | }; 384 | 385 | const float panCoefficient = 0.4; 386 | Ray getRay(OrthoCamera cam, vec2 uv) { 387 | // Use the adjusted UV and the camera's parameters to calculate the ray's origin. 388 | vec2 adjustedUV = vec2((uv.x - u_translationX * panCoefficient), 389 | (uv.y + u_translationY * panCoefficient)); 390 | vec3 rayOrigin = cam.position + 391 | adjustedUV.x * (cam.right - cam.left) * cam.rightv + 392 | adjustedUV.y * (cam.top - cam.bottom) * cam.up; 393 | 394 | return Ray(rayOrigin, cam.forward); 395 | } 396 | 397 | 398 | const float max_t = 1000.0; 399 | const float min_d = 0.0001; 400 | 401 | vec4 raymarch_simple_normal_colors(Ray ray) { 402 | float t = 0.0; 403 | for (int i = 0; i < MAX_STEPS; i++) { 404 | vec3 p = ray.origin + ray.direction * t; 405 | float d = mySdf(p); 406 | if (d < min_d) { 407 | vec3 n = normal(p); 408 | vec3 col = n*0.5+0.5; 409 | float gradLen = length(vec3(d - mySdf(p - ray.direction * 0.001))); 410 | float alpha = 1.0 - smoothstep(0.0, 0.2, gradLen); 411 | return vec4(col * alpha, alpha); 412 | } 413 | t += d; 414 | if (t > max_t) break; 415 | } 416 | return vec4(0.0); 417 | } 418 | 419 | // basic raymarcher that renders the SDF's edges 420 | vec4 raymarch_edges(Ray ray) { 421 | vec4 col = vec4(0.0); // default color 422 | float t = 0.0; 423 | for(int i = 0; i < MAX_STEPS; i++) { 424 | vec3 p = ray.origin + ray.direction * t; 425 | float d = mySdf(p); 426 | 427 | if (d < min_d) { // we hit the surface 428 | vec3 n = normal(p); 429 | 430 | // Sample normals in neighboring points around p 431 | vec3 n1 = normal(p + 0.0075 * n); 432 | vec3 n2 = normal(p - 0.0075 * n); 433 | 434 | // Compare the normals using dot product 435 | float dotProd = dot(n1, n2); 436 | 437 | if(dotProd < 0.1) { // threshold should be adjusted based on the specifics of the SDF 438 | col = vec4(1.0, 1.0, 1.0, 1.0); // edge color 439 | } 440 | 441 | break; 442 | } 443 | t += d; 444 | if(t > max_t) break; 445 | } 446 | return vec4(col); 447 | } 448 | 449 | // Helper function to convert HSV to RGB 450 | vec3 hsv2rgb(vec3 c) { 451 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 452 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 453 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 454 | } 455 | 456 | 457 | vec4 raymarch_zColor(Ray ray) { 458 | vec4 col = vec4(0.0); // default transparent color 459 | 460 | float t = 0.0; 461 | for(int i = 0; i < MAX_STEPS; i++) { 462 | vec3 p = ray.origin + ray.direction * t; 463 | 464 | float d = mySdf(p); 465 | 466 | if (d < 0.00001) { 467 | vec3 n = normal(p); 468 | 469 | // Check if the normal is approximately Z-facing 470 | if(abs(n.z) > 0.95) { // 0.95 is a threshold, you can adjust it 471 | // Use the z value of the point to derive a color, for example: 472 | float hue = mod(p.z * 0.125, 360.0); // you can adjust scaling and modulation 473 | col = vec4(hsv2rgb(vec3(hue, 1.0, 1.0)), 1.0); // Assuming you have a function to convert HSV to RGB 474 | } else { 475 | col = vec4(0.0, 0.0, 0.0, 0.0); // Some other color for non Z-facing surfaces 476 | } 477 | 478 | return col; 479 | } 480 | 481 | t += d; 482 | if(t > max_t) break; 483 | } 484 | 485 | return col; 486 | } 487 | 488 | // Main function 489 | void main() { 490 | vec2 uv = (gl_FragCoord.xy - iResolution * 0.5) / min(iResolution.y, iResolution.x); 491 | 492 | // Define and construct your camera 493 | vec3 camPos = vec3(50.0, 50.0, 50.0); 494 | 495 | camPos = opRotate(camPos, vec3(0.0, 0.0, -u_rotationAngleZ)); 496 | vec3 camLookAt = vec3(0.0, 0.0, 0.0); 497 | float width = 8.0/u_zoom; 498 | OrthoCamera cam = createOrthoCamera(camPos, camLookAt, width, width); 499 | 500 | // Get ray for current pixel 501 | Ray ray = getRay(cam, uv); 502 | 503 | // Raymarch and fetch color 504 | vec4 col = vec4(0.0); 505 | if (normalCols == true) col += raymarch_simple_normal_colors(ray); 506 | if (contourCols == true) col += raymarch_zColor(ray); 507 | if (outlines == true) col += raymarch_edges(ray); 508 | 509 | outputColor = col; 510 | } 511 | 512 | 513 | `; 514 | 515 | let animation; 516 | let shaderProgram; 517 | let fragmentShader; 518 | let vertexShader; 519 | let vertexBuffer; 520 | 521 | function createShader(type, source) { 522 | const shader = gl.createShader(type); 523 | gl.shaderSource(shader, source); 524 | gl.compileShader(shader); 525 | 526 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 527 | console.error('An error occurred compiling the shaders:', gl.getShaderInfoLog(shader)); 528 | gl.deleteShader(shader); 529 | return null; 530 | } 531 | 532 | return shader; 533 | } 534 | 535 | function initialize() { 536 | // If shaderProgram already exists, delete the old program and shaders 537 | if (shaderProgram) { 538 | gl.deleteProgram(shaderProgram); 539 | shaderProgram = null; 540 | } 541 | 542 | // Create shader program 543 | shaderProgram = gl.createProgram(); 544 | 545 | // Create a new vertex buffer if it doesn't exist 546 | if (!vertexBuffer) { 547 | vertexBuffer = gl.createBuffer(); 548 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 549 | gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW); 550 | } 551 | 552 | // Define vertex shader with aspect ratio adjustment 553 | const vertexShaderSource = `#version 300 es 554 | in vec2 coordinates; 555 | uniform float aspectRatio; 556 | void main() { 557 | gl_Position = vec4(coordinates.x, coordinates.y * aspectRatio, 0.0, 1.0); 558 | }`; 559 | const vertexShader = createShader(gl.VERTEX_SHADER, vertexShaderSource); 560 | 561 | // Define fragment shader using source from div 562 | let sdfSrcDiv = document.querySelector("#sdf-src"); 563 | let sdfCode = sdfSrcDiv.innerHTML; 564 | let fragCode = `${fragCodeA}${sdfCode}${fragCodeB}`; 565 | const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragCode); 566 | 567 | // Attach and link shaders to the program 568 | gl.attachShader(shaderProgram, vertexShader); 569 | gl.attachShader(shaderProgram, fragmentShader); 570 | gl.linkProgram(shaderProgram); 571 | 572 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 573 | console.error('Unable to initialize the shader program:', gl.getProgramInfoLog(shaderProgram)); 574 | return; 575 | } 576 | 577 | // After linking the shaders to the program, they can be safely deleted. 578 | gl.deleteShader(vertexShader); 579 | gl.deleteShader(fragmentShader); 580 | 581 | gl.useProgram(shaderProgram); 582 | } 583 | 584 | initContext(); 585 | initialize(); 586 | 587 | function setUniforms() { 588 | const iResolutionUniformLocation = gl.getUniformLocation(shaderProgram, 'iResolution'); 589 | gl.uniform2f(iResolutionUniformLocation, canvas.width, canvas.height); 590 | 591 | // Get the position attribute location 592 | const position = gl.getAttribLocation(shaderProgram, 'coordinates'); 593 | gl.enableVertexAttribArray(position); 594 | gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); 595 | 596 | // Get the rotation, translation, and zoom uniforms location and set their values 597 | const rotationAngleZUniformLocation = gl.getUniformLocation(shaderProgram, "u_rotationAngleZ"); 598 | gl.uniform1f(rotationAngleZUniformLocation, rotationAngleZ); 599 | const rotationAngleScreenXUniformLocation = gl.getUniformLocation(shaderProgram, "u_rotationAngleScreenX"); 600 | gl.uniform1f(rotationAngleScreenXUniformLocation, rotationAngleScreenX); 601 | const translationXUniformLocation = gl.getUniformLocation(shaderProgram, "u_translationX"); 602 | gl.uniform1f(translationXUniformLocation, translationX); 603 | const translationYUniformLocation = gl.getUniformLocation(shaderProgram, "u_translationY"); 604 | gl.uniform1f(translationYUniformLocation, translationY); 605 | const zoomUniformLocation = gl.getUniformLocation(shaderProgram, "u_zoom"); 606 | gl.uniform1f(zoomUniformLocation, zoom); 607 | 608 | // Get the aspect ratio uniform location and set its value 609 | const aspectRatioUniform = gl.getUniformLocation(shaderProgram, 'aspectRatio'); 610 | gl.uniform1f(aspectRatioUniform, canvas.width / canvas.height); 611 | } 612 | 613 | function draw() { 614 | gl.viewport(0, 0, canvas.width, canvas.height); 615 | gl.clearColor(0.0, 0.0, 0.0, 0.0); 616 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 617 | 618 | gl.useProgram(shaderProgram); 619 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 620 | 621 | const position = gl.getAttribLocation(shaderProgram, 'coordinates'); 622 | gl.enableVertexAttribArray(position); 623 | gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); 624 | 625 | setUniforms(); 626 | 627 | gl.drawArrays(gl.TRIANGLES, 0, 6); 628 | 629 | animation = requestAnimationFrame(draw); 630 | } 631 | 632 | // call initContext to grab the canvas and set up the GL context 633 | initContext(); 634 | // call draw to render the SDF 635 | draw(); 636 | 637 | function redraw() { 638 | cancelAnimationFrame(animation); 639 | initialize(); 640 | draw(); 641 | } 642 | 643 | // Redraw the triangle every time the window size changes 644 | //window.addEventListener('resize', redraw); 645 | 646 | // Watch for changes to the shader source code and redraw when there's a change 647 | const observer = new MutationObserver(redraw); 648 | observer.observe(sdfSrcContainer, { characterData: true, childList: true, subtree: true }); 649 | 650 | const sdfSrc = document.getElementById("sdf-src"); 651 | let sdfSrcEventListener = htmx.on("#sdf-src", "contentChanged"); 652 | 653 | function dispatchContentChanged() { 654 | const contentChangedEvent = new Event('contentChanged', { 655 | 'bubbles': true, 656 | 'cancelable': true 657 | }); 658 | 659 | sdfSrc.dispatchEvent(contentChangedEvent); 660 | } 661 | 662 | // Editor 663 | let editor; 664 | function initEditor() { 665 | // Initialize CodeMirror 666 | const ed = document.getElementById('editor'); 667 | editor = CodeMirror.fromTextArea(ed, { 668 | mode: 'clojure', 669 | smartIndent: true, 670 | electricChars: true, 671 | lineNumbers: true, // show line numbers 672 | theme: 'nord', // optional theme; you can choose others 673 | matchBrackets: true, // parentheses matching 674 | autoCloseBrackets: true 675 | }); 676 | 677 | editor.setSize(ed.style.width, ed.style.height); 678 | 679 | // Editor Can inc/dec numbers at cursor 680 | editor.setOption("extraKeys", { 681 | "Ctrl-Up": function(cm) { 682 | incrementOrDecrement(cm, 1); // increment 683 | }, 684 | "Ctrl-Down": function(cm) { 685 | incrementOrDecrement(cm, -1); // decrement 686 | }, 687 | "Shift-Ctrl-Up": function(cm) { 688 | incrementOrDecrement(cm, 10); // increment 689 | }, 690 | "Shift-Ctrl-Down": function(cm) { 691 | incrementOrDecrement(cm, -10); // decrement 692 | } 693 | }); 694 | 695 | function incrementOrDecrement(cm, change) { 696 | // Get cursor position 697 | var cursor = cm.getCursor(); 698 | 699 | // Find the start and end of the number around the cursor 700 | var start = cursor.ch; 701 | var end = cursor.ch; 702 | var lineContent = cm.getLine(cursor.line); 703 | 704 | while (start > 0 && isNumericOrNegativeSign(lineContent.charAt(start - 1), lineContent.charAt(start - 2))) { 705 | start--; 706 | } 707 | 708 | while (end < lineContent.length && isNumeric(lineContent.charAt(end))) { 709 | end++; 710 | } 711 | 712 | // Get the number 713 | var number = lineContent.slice(start, end); 714 | 715 | if (number) { 716 | var changedNumber = (parseInt(number) + change).toString(); 717 | cm.replaceRange(changedNumber, CodeMirror.Pos(cursor.line, start), CodeMirror.Pos(cursor.line, end)); 718 | 719 | // Keep the cursor at its position 720 | var cursorShift = changedNumber.length - number.length; 721 | cm.setCursor(CodeMirror.Pos(cursor.line, cursor.ch + cursorShift)); 722 | } 723 | } 724 | 725 | // Helper function to check if a character is numeric 726 | function isNumeric(char) { 727 | return !isNaN(parseInt(char)); 728 | } 729 | 730 | // Helper function to check if a character is numeric or a negative sign 731 | function isNumericOrNegativeSign(char, prevChar) { 732 | // Check if it's a numeric character 733 | if (isNumeric(char)) { 734 | return true; 735 | } 736 | 737 | // Check if it's a negative sign that's not preceded by another number (to ensure it's not a subtraction operation) 738 | return char === '-' && !isNumeric(prevChar); 739 | } 740 | 741 | const form = document.getElementById('render-settings'); 742 | editor.on("change", dispatchContentChanged); 743 | form.addEventListener('change', dispatchContentChanged); 744 | 745 | 746 | } 747 | 748 | document.addEventListener("DOMContentLoaded", initEditor); 749 | 750 | window.addEventListener('unload', function() { 751 | if (shaderProgram) { 752 | gl.deleteProgram(shaderProgram); 753 | shaderProgram = null; 754 | } 755 | 756 | if (shaderProgram2) { 757 | gl.deleteProgram(shaderProgram2); 758 | shaderProgram2 = null; 759 | } 760 | 761 | if (vertexBuffer) { 762 | gl.deleteBuffer(vertexBuffer); 763 | vertexBuffer = null; 764 | } 765 | 766 | if (vertexBuffer2) { 767 | gl.deleteBuffer(vertexBuffer2); 768 | vertexBuffer2 = null; 769 | } 770 | 771 | 772 | document.removeEventListener("DOMContentLoaded", initEditor); 773 | window.removeEventListener('keydown', handleKeyDown); 774 | window.removeEventListener('keyup', handleKeyUp); 775 | canvas.removeEventListener("webglcontextlost", handleWebGLContextLost); 776 | canvas.removeEventListener("webglcontextrestored", initContext); 777 | canvas.removeEventListener('wheel', handleWheelEvent); 778 | canvas.removeEventListener('mousedown', handleMouseDownEvent); 779 | canvas.removeEventListener('mouseup', handleMouseUpEvent); 780 | canvas.removeEventListener('wheel', handleWheelZoom); 781 | canvas.removeEventListener('mousemove', handleMouseMoveEvent); 782 | 783 | canvas = null; 784 | gl = null; 785 | canvas2 = null; 786 | gl2 = null; 787 | }); 788 | 789 | 790 | function distance(a, b) { 791 | return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); 792 | } 793 | 794 | function chainPoints(points) { 795 | points = points.filter((_, index) => (index + 1) % 1 === 0); 796 | if (points.length === 0) return []; 797 | 798 | // Start with the first point 799 | let currentPt = points[0]; 800 | let chainedPts = [currentPt]; 801 | let remainingPts = [...points.slice(1)]; // Copy all the other points to the remainingPts array 802 | 803 | // This loop continues until there are no points left in remainingPts 804 | while (remainingPts.length > 0) { 805 | // Sort the remaining points based on their distance to the current point 806 | remainingPts.sort((a, b) => distance(a, currentPt) - distance(b, currentPt)); 807 | 808 | // The closest point becomes the new current point 809 | currentPt = remainingPts[0]; 810 | chainedPts.push(currentPt); 811 | 812 | // Remove the new current point from remainingPts 813 | remainingPts = remainingPts.slice(1); 814 | } 815 | 816 | //return simplify(chainedPts, 1.5, false); 817 | return chainedPts; 818 | } 819 | 820 | // Edge tracing algorithm for concave hull 821 | function edgeTracing(points) { 822 | if (points.length < 3) { 823 | // Cannot form a polygon with less than 3 points 824 | return null; 825 | } 826 | 827 | let hull = []; 828 | let usedPoints = new Set(); 829 | 830 | // Starting point (can be any point) 831 | let currentPoint = points[0]; 832 | let startPoint = currentPoint; 833 | 834 | // Add start point to the hull and mark as used 835 | hull.push(startPoint); 836 | usedPoints.add(JSON.stringify(startPoint)); 837 | 838 | while (true) { 839 | let closestPoint = null; 840 | let closestDistance = Infinity; 841 | 842 | // Find the closest point to the current point 843 | for (let point of points) { 844 | if (usedPoints.has(JSON.stringify(point))) { 845 | // Skip if the point has already been used 846 | continue; 847 | } 848 | 849 | let dist = distance(currentPoint, point); 850 | if (dist < closestDistance) { 851 | closestDistance = dist; 852 | closestPoint = point; 853 | } 854 | } 855 | 856 | if (closestPoint === null) { 857 | // No available points to connect, thus end 858 | break; 859 | } 860 | 861 | // Add the closest point to the hull and mark as used 862 | hull.push(closestPoint); 863 | usedPoints.add(JSON.stringify(closestPoint)); 864 | 865 | // If we have returned to the start point, break 866 | //if (closestPoint.x === startPoint.x && closestPoint.y === startPoint.y) { 867 | // break; 868 | //} 869 | 870 | // Update current point 871 | currentPoint = closestPoint; 872 | } 873 | 874 | return hull; 875 | } 876 | 877 | 878 | // Function to calculate the Euclidean distance between two points 879 | function distance(A, B) { 880 | let dx = A.x - B.x; 881 | let dy = A.y - B.y; 882 | return Math.sqrt(dx * dx + dy * dy); 883 | } 884 | 885 | // DBSCAN algorithm 886 | function dbscan(points, epsilon, minPts) { 887 | let clusters = []; 888 | let visited = new Set(); 889 | let noise = new Set(); 890 | 891 | function regionQuery(P) { 892 | return points.filter(Q => distance(P, Q) <= epsilon); 893 | } 894 | 895 | function expandCluster(P, neighbors, C) { 896 | C.push(P); 897 | visited.add(JSON.stringify(P)); 898 | 899 | let index = 0; 900 | while (index < neighbors.length) { 901 | let point = neighbors[index]; 902 | let pointStr = JSON.stringify(point); 903 | 904 | if (!visited.has(pointStr)) { 905 | visited.add(pointStr); 906 | let pointNeighbors = regionQuery(point); 907 | 908 | if (pointNeighbors.length >= minPts) { 909 | neighbors = neighbors.concat(pointNeighbors); 910 | } 911 | } 912 | 913 | if (!clusters.some(cluster => cluster.includes(point)) && !noise.has(pointStr)) { 914 | C.push(point); 915 | } 916 | 917 | index++; 918 | } 919 | } 920 | 921 | for (let point of points) { 922 | let pointStr = JSON.stringify(point); 923 | 924 | if (visited.has(pointStr)) continue; 925 | 926 | let neighbors = regionQuery(point); 927 | 928 | if (neighbors.length < minPts) { 929 | noise.add(pointStr); 930 | } else { 931 | let C = []; 932 | expandCluster(point, neighbors, C); 933 | clusters.push(C); 934 | } 935 | } 936 | 937 | return clusters; 938 | } 939 | 940 | function getPixelColor(x, y, width, data) { 941 | const index = (y * width + x) * 4; 942 | const r = data[index]; 943 | const g = data[index + 1]; 944 | const b = data[index + 2]; 945 | const a = data[index + 3]; 946 | 947 | if (a === 0 || (r === 0 && g === 0 && b === 0)) { 948 | return null; 949 | } 950 | 951 | return {r, g, b}; 952 | } 953 | 954 | function getAdjacentPixels(x, y) { 955 | return [ 956 | {x: x + 1, y}, 957 | {x: x - 1, y}, 958 | {x, y: y + 1}, 959 | {x, y: y - 1}, 960 | ]; 961 | } 962 | 963 | function colorDistance(color1, color2) { 964 | return Math.sqrt( 965 | Math.pow(color1.r - color2.r, 2) + 966 | Math.pow(color1.g - color2.g, 2) + 967 | Math.pow(color1.b - color2.b, 2) 968 | ); 969 | } 970 | 971 | 972 | function findContours(width, height, pixelData) { 973 | const contours = {}; 974 | const visited = new Set(); 975 | 976 | for (let y = 0; y < height; y++) { 977 | for (let x = 0; x < width; x++) { 978 | const key = `${x}_${y}`; 979 | 980 | if (visited.has(key)) continue; 981 | 982 | const color = getPixelColor(x, y, width, pixelData); 983 | 984 | // Skip if the color is not interesting 985 | if (color === null) continue; 986 | 987 | if (!contours[color]) { 988 | contours[color] = []; 989 | } 990 | 991 | let boundary = []; 992 | let stack = [{x, y}]; 993 | 994 | while (stack.length > 0) { 995 | const point = stack.pop(); 996 | const {x, y} = point; 997 | const pointKey = `${x}_${y}`; 998 | 999 | if (visited.has(pointKey)) continue; 1000 | 1001 | visited.add(pointKey); 1002 | 1003 | const adjacentPixels = getAdjacentPixels(x, y); 1004 | let isBoundary = false; 1005 | 1006 | const SIMILARITY_THRESHOLD = 12; 1007 | for (const adj of adjacentPixels) { 1008 | const adjKey = `${adj.x}_${adj.y}`; 1009 | if (adj.x < 0 || adj.x >= width || adj.y < 0 || adj.y >= height) { 1010 | isBoundary = true; 1011 | continue; 1012 | } 1013 | 1014 | const adjColor = getPixelColor(adj.x, adj.y, width, pixelData); 1015 | 1016 | if (adjColor === null) { 1017 | isBoundary = true; // Add this line to mark it as boundary if neighbor is null 1018 | continue; // and continue to the next adjacent pixel 1019 | } 1020 | 1021 | if (colorDistance(color, adjColor) < SIMILARITY_THRESHOLD) { 1022 | stack.push(adj); 1023 | } else { 1024 | isBoundary = true; 1025 | } 1026 | } 1027 | 1028 | if (isBoundary) { 1029 | boundary.push(point); 1030 | } 1031 | } 1032 | 1033 | if (boundary.length > 2) { 1034 | boundary = edgeTracing(boundary); 1035 | //boundary = simplify(boundary, 0.6, false); 1036 | contours[color].push(boundary); 1037 | } 1038 | } 1039 | } 1040 | 1041 | return contours; 1042 | } 1043 | 1044 | function grabPixels() { 1045 | ctx = canvas.getContext("webgl2"); 1046 | const width = canvas.width; 1047 | const height = canvas.height; 1048 | 1049 | const pixels = new Uint8Array(width * height * 4); 1050 | 1051 | gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); 1052 | const contours = findContours(width, height, pixels); 1053 | 1054 | const url = '/contours'; 1055 | htmx.ajax('POST', '/contours', { 1056 | source: '#canvas', 1057 | target: '#contour', 1058 | event: "contour", 1059 | swap: 'innerHTML', 1060 | values: { body: { contours: JSON.stringify(contours), 1061 | sdfSrc: editor.getValue() }} 1062 | }); 1063 | 1064 | return contours; 1065 | } 1066 | -------------------------------------------------------------------------------- /src/sdfx/examples.clj: -------------------------------------------------------------------------------- 1 | (ns sdfx.examples 2 | (:require [sdfx.geom :refer :all] 3 | [svg-clj.parametric :as p])) 4 | 5 | (comment 6 | ;; puffs 7 | (let [pts (p/regular-polygon-pts 2.4 6)] 8 | (-> 9 | (reduce 10 | (partial smooth-union 1.3) 11 | (for [[x y] (shuffle pts)] 12 | (-> (rbox 1.4 2.9) 13 | (translate (perturb [x y 0] 0.3))))) 14 | (translate [0 0 0.6]) 15 | (slices 0.09 0.001))) 16 | 17 | 18 | (let [ln (p/line [0 0 0] [0 0 30]) 19 | f (fn [t] 20 | (smooth-union t 21 | (box 0.7 0.7 0.7) 22 | (sphere 0.9)))] 23 | (-> (reduce 24 | union 25 | (for [t (range 0 1 0.125)] 26 | (-> (f (* t 5)) (translate (ln t))))) 27 | (slices 0.125 0.01))) 28 | 29 | (-> 30 | (smooth-difference 31 | 60 32 | (reduce 33 | (partial smooth-union 10) 34 | [(-> (sphere 190) (translate [0 0 0])) 35 | (-> (sphere 150) (translate [0 0 150]))]) 36 | (-> (box 100 100 100) (rotate [45 35 0]))) 37 | (onion 55) 38 | (translate [0 0 0]) 39 | (slices 15 5)) 40 | 41 | 42 | 43 | 44 | ) 45 | -------------------------------------------------------------------------------- /src/sdfx/geom.clj: -------------------------------------------------------------------------------- 1 | (ns sdfx.geom 2 | (:require [clojure.string :as str])) 3 | 4 | ;; Utils 5 | 6 | (defn- vec-str 7 | [v] 8 | (format "vec%s(%s)" (count v) (str/join "," (map double v)))) 9 | 10 | 11 | (defn normalize 12 | "find the unit vector of the given vector `v`." 13 | [v] 14 | (when v 15 | (let [m (Math/sqrt ^double (reduce + (mapv * v v)))] 16 | (mapv / v (repeat m))))) 17 | 18 | (def ^:dynamic *rounding-decimal-places* 19 | "The number of decimal places the `round` funciton will round to." 20 | 3) 21 | 22 | (defn round 23 | "Rounds a non-integer number `num` to `places` decimal places." 24 | ([num] 25 | (round num *rounding-decimal-places*)) 26 | ([num places] 27 | (if places 28 | (let [d (bigdec (Math/pow 10 places))] 29 | (double (/ (Math/round (* (double num) d)) d))) 30 | num))) 31 | 32 | (defn distance 33 | "Computes the distance between two points `a` and `b`." 34 | [a b] 35 | (let [v (mapv - b a) 36 | v2 (reduce + (mapv * v v))] 37 | (round (Math/sqrt ^double v2)))) 38 | 39 | (defn perturb-pts 40 | "Adds a random value between 0 and `r` to each component of every point." 41 | [pts r] 42 | (mapv (fn [pt] (mapv + pt (repeatedly #(rand r)))) pts)) 43 | 44 | ;; scale things down 100x so (box 100 100 100) => sdBox(vec3(1.0,1.0,1.0)); 45 | 46 | (def ^:dynamic *internal-scale* 47 | "The number of decimal places the `round` funciton will round to." 48 | 0.01) 49 | 50 | ;; Primitives 51 | 52 | (defn sphere 53 | "Defines a sphere of radius `r` centered at the origin." 54 | [r] 55 | (let [r (* r *internal-scale*)] 56 | (fn [p] 57 | (cond 58 | (string? p) 59 | (format "sdSphere(%s, %s)" (str p) (double r)))))) 60 | 61 | (defn box 62 | "Defines a box with dimensions `l`, `w`, and `h` centered at the origin." 63 | [l w h] 64 | (let [l (* l *internal-scale*) 65 | w (* w *internal-scale*) 66 | h (* h *internal-scale*)] 67 | (fn [p] 68 | (cond 69 | (string? p) 70 | (format "sdBox(%s, vec3(%s, %s, %s))" (str p) (double l) (double w) (double h)))))) 71 | 72 | (defn circle 73 | "Defines a circle of radius `r` on the XY plane centered at the origin." 74 | [r] 75 | (let [r (* r *internal-scale*)] 76 | (fn 77 | [p] 78 | (cond 79 | (string? p) ;; string input means we want the fragmentShader code output 80 | (format "sdCircle(%s, %s)" (str p) (double r)))))) 81 | 82 | (defn line 83 | "Defines a line segment starting at point `a` and ending at point `b`. 84 | Points can be 2D or 3D." 85 | [a b] 86 | (let [a (mapv * a (repeat *internal-scale*)) 87 | b (mapv * b (repeat *internal-scale*))] 88 | (fn [p] 89 | (format "sdLine(%s, %s, %s)" (str p) (vec-str a) (vec-str b))))) 90 | 91 | (defn polygon 92 | "Defines a polygon with vertices defined by the points list `pts`. 93 | Points must be 2D. Currently, there is a limit of 200 points maximum." 94 | [pts] 95 | (let [pts (mapv (fn [pt] (mapv * pt (repeat *internal-scale*))) pts) 96 | n (count pts) 97 | pts-str (str/join "," (map vec-str (take 200 (concat pts (repeat [0 0])))))] 98 | (fn [p] 99 | (format "sdPolygon(%s, vec2[200](%s), %s)" (str p) pts-str n)))) 100 | 101 | (defn plane 102 | "Defines an infinite plane with normal vector `nv` and offset from the origin along the vector `h`. 103 | This is best used to construct closed faceted shapes using `intersection` as the infinite planes will render strangely." 104 | [nv h] 105 | (let [h (* h *internal-scale*) 106 | nv (normalize nv) 107 | f (fn [xyz] 108 | (format "vec3(%s)" (str/join "," (map double xyz))))] 109 | (fn [p] 110 | (format "sdPlane(%s, %s, %s)" (str p) (f nv) (double h))))) 111 | 112 | (defn extrude 113 | "Defines a 3D shape by 'pulling' the 2D `shape` along the Z axis up to the given height `h`." 114 | [shape h] 115 | (let [h (* h *internal-scale*)] 116 | (fn [p] 117 | (cond 118 | (string? p) 119 | (format "opExtrude(%s, %s,%s)" (str p) (shape (format "%s.xy" p)) (double h)) 120 | 121 | (every? number? p) 122 | (let [d (shape (drop-last p)) 123 | w (- (Math/abs ^long (- (last p) (/ h 2))) (/ h 2))] 124 | (+ (min (max d w) 0) 125 | (distance [0 0] [(max d 0) (max w 0)]))))))) 126 | 127 | (defn revolve 128 | "Defines a 3D shape by 'pulling' the 2D `shape` around the Y axis." 129 | ([shape] 130 | (fn [p] 131 | (shape (format "opRevolve(%s)" (str p)))))) 132 | 133 | (defn slice 134 | "Defines a 2D shape by 'slicing' the 3D `shape` at the height `h` on the Z axis." 135 | ([shape h] 136 | (let [h (* h *internal-scale*)] 137 | (fn [p] 138 | (format "opExtrude(%s, %s, 0.001)" (str p) (shape (format "vec3( p.xy, %s)" (double h))))))) 139 | ([shape t h] 140 | (let [t (* t *internal-scale*) 141 | h (* h *internal-scale*)] 142 | (fn [p] 143 | (format "opExtrude(%s, %s, %s)" (str p) (shape (format "vec3( p.xy, %s)" (double h))) (double t)))))) 144 | 145 | ;; Transforms 146 | 147 | (defn translate 148 | "Translates the given shape function `f` by [`x` `y` `z`] or by [`x` `y`] if the shape function is a 2D shape." 149 | [f [x y z]] 150 | (let [x (* x *internal-scale*) 151 | y (* y *internal-scale*) 152 | z (when z (* z *internal-scale*))] 153 | (if z 154 | (fn [p] 155 | (f (format "opTranslate(%s, vec3(%s, %s, %s))" (str p) (double x) (double y) (double z)))) 156 | (fn [p] 157 | (f (format "opTranslate(%s, vec2(%s, %s))" (str p) (double x) (double y))))))) 158 | 159 | (defn rotate 160 | "Rotates the given shape function `f` by rotations `rs`, which is a vector of [`rx` `ry` `rz`] for 3D shapes and a number in degress for 2D shapes. 161 | The order of 3D rotations matters and will occurs as follows: 162 | - `rz` is the rotation around the Z axis towards the Y+ axis. 163 | - `ry` is the rotation around the Y axis towards the X+ axis. 164 | - `rx` is the rotation around the Y axis towards the Z+ axis." 165 | [f rs] 166 | (if (vector? rs) 167 | (let [[rx ry rz] rs] 168 | (fn [p] 169 | (f (format "opRotate(%s, vec3(%s, %s, %s))" (str p) (double rx) (double ry) (double rz))))) 170 | (fn [p] 171 | (f (format "opRotate(%s, %s)" (str p) (double rs)))))) 172 | 173 | (defn repeat-shape 174 | "Infinitely repeats the SDF `f` according to the spacing vector `s`, which indicates spacing along [x y z] axes." 175 | [f s] 176 | (fn [p] 177 | (f (format "opRepetition(%s, %s)" (str p) (vec-str s))))) 178 | 179 | (defn onion 180 | "Creates a shell of `f` with thickness `t`." 181 | [f t] 182 | (let [t (* t *internal-scale*)] 183 | (fn [p] 184 | (format "opOnion(%s, %s)" (f p) (double t))))) 185 | 186 | (defn union 187 | "Defines the joined shape of `fa` and `fb`." 188 | ([fa] fa) 189 | ([fa fb] 190 | (fn [p] 191 | (format "opUnion(%s, %s)" (fa p) (fb p))))) 192 | 193 | (defn difference 194 | "Defines the cut shape of `fa` and `fb` where `fa` is removed." 195 | ([fa] fa) 196 | ([fa fb] 197 | (fn [p] 198 | (format "opDifference(%s, %s)" (fa p) (fb p))))) 199 | 200 | (defn intersection 201 | "Defines the shared shape of `fa` and `fb`." 202 | ([fa] fa) 203 | ([fa fb] 204 | (fn [p] 205 | (format "opIntersection(%s, %s)" (fa p) (fb p))))) 206 | 207 | (defn smooth-union 208 | "Defines the joined shape of `fa` and `fb` with a smoothing factor `k`. 209 | A `k` factor of 0 causes the equivalent of a standard `union`." 210 | ([k fa] fa) 211 | ([k fa fb] 212 | (let [k (* k *internal-scale*)] 213 | (fn [p] 214 | (format "opSmoothUnion(%s, %s, %s)" (fa p) (fb p) (double k)))))) 215 | 216 | (defn smooth-difference 217 | "Defines the cut shape of `fa` and `fb` where `fa` is removed with a smoothing factor `k`. 218 | A `k` factor of 0 causes the equivalent of a standard `difference`." 219 | ([k fa] fa) 220 | ([k fa fb] 221 | (let [k (* k *internal-scale*)] 222 | (fn [p] 223 | (format "opSmoothDifference(%s, %s, %s)" (fa p) (fb p) (double k)))))) 224 | 225 | (defn smooth-intersection 226 | "Defines the shared shape of `fa` and `fb` with a smoothing factor `k`. 227 | A `k` factor of 0 causes the equivalent of a standard `intersection`." 228 | ([k fa] fa) 229 | ([k fa fb] 230 | (let [k (* k *internal-scale*)] 231 | (fn [p] 232 | (format "opSmoothIntersection(%s, %s, %s)" (fa p) (fb p) (double k)))))) 233 | 234 | (defn slices 235 | "Slices the shape `shape` leaving slices of `thickness` spaced according to `spacing`." 236 | [shape spacing thickness] 237 | (let [s (* spacing *internal-scale*) 238 | t (* thickness *internal-scale*) 239 | sl (-> (box 100000 100000 (* (- s t) (/ 1.0 *internal-scale*))) 240 | (repeat-shape [0 0 (* s 2)]))] 241 | (difference 242 | sl 243 | shape))) 244 | 245 | (defn rings 246 | "Slices the onioned shape `shape` leaving rings of `thickness` spaced according to `spacing`." 247 | [shape spacing thickness] 248 | (let [s (* spacing *internal-scale*) 249 | t (* thickness *internal-scale*) 250 | sl (fn [t] 251 | (-> (box 100000 100000 (* (- s t) (/ 1.0 *internal-scale*))) 252 | (repeat-shape [0 0 (* s 2)])))] 253 | (-> (smooth-union (* t 2) 254 | (-> 255 | (difference 256 | (sl (* t 0.1)) 257 | (onion shape (* t 0.25)))))))) 258 | -------------------------------------------------------------------------------- /src/sdfx/main.clj: -------------------------------------------------------------------------------- 1 | (ns sdfx.main 2 | (:require [cheshire.core :as json] 3 | [clojure.edn :as edn] 4 | [clojure.java.io :as io] 5 | [clojure.pprint :as pprint] 6 | [clojure.string :as str] 7 | [hiccup2.core :as h] 8 | [org.httpkit.server :as srv] 9 | [ruuter.core :as ruuter] 10 | [sdfx.geom :as g] 11 | [sdfx.util :as u] 12 | [svg-clj.composites :refer [svg]] 13 | [svg-clj.path :as path] 14 | [svg-clj.tools :as tools] 15 | [svg-clj.transforms :as tf] 16 | [svg-clj.utils :as svg.u]) 17 | (:import [java.net ServerSocket BindException]) 18 | (:gen-class)) 19 | 20 | (def starting-sdf-ns 21 | '(ns sdfx.scratch 22 | (:require [sdfx.geom :as g] 23 | [svg-clj.parametric :as p]))) 24 | 25 | (def starting-sdf-src 26 | '(-> (reduce 27 | g/union 28 | [(-> (g/sphere 50) (g/translate [-150 0 0])) 29 | (-> (g/box 50 50 50) (g/translate [0 0 0])) 30 | (-> (g/circle 50) (g/extrude 50) (g/translate [150 0 0]))]) 31 | (g/translate [0 0 0]) 32 | (g/rotate [45 35 0]) 33 | (g/slices 5 0.1))) 34 | 35 | (def starting-sdf-fn (eval starting-sdf-src)) 36 | 37 | (defonce ^:private last-src-str (atom nil)) 38 | (defonce ^:private contour-history (atom [])) 39 | (defonce ^:private show-docs (atom false)) 40 | (defonce ^:private show-gallery (atom false)) 41 | (defonce ^:private render-settings 42 | (atom {:colors "contours" 43 | :outlines false})) 44 | 45 | (defn- compile-sdf 46 | [shapefn render-settings] 47 | (let [{:keys [colors outlines]} render-settings 48 | contour-cols (= "contours" colors) 49 | normal-cols (not contour-cols) 50 | outlines (= "on" outlines)] 51 | (format "bool normalCols = %s; 52 | bool contourCols = %s; 53 | bool outlines = %s; 54 | float mySdf (in vec3 p) { 55 | return %s; 56 | }" 57 | normal-cols 58 | contour-cols 59 | outlines 60 | (shapefn "p")))) 61 | 62 | (defn- update-contour-history! 63 | [entry] 64 | (let [history @contour-history 65 | f (if (> (count history) 99) 66 | (comp #(vec (rest %1)) #(conj %1 %2)) 67 | #(conj %1 %2))] 68 | (swap! contour-history f entry))) 69 | 70 | (defn- strip-do 71 | [s] 72 | (if-let [[_ inner-content] (re-matches #"\(do\s+([\s\S]+)\)\s*$" s)] 73 | inner-content 74 | s)) 75 | 76 | (defn- simple-code-printer 77 | [quoted-expr-or-string] 78 | (with-out-str 79 | (pprint/with-pprint-dispatch 80 | pprint/code-dispatch 81 | (pprint/pprint 82 | (edn/read-string (str quoted-expr-or-string)))))) 83 | 84 | (defn- template 85 | [] 86 | (list 87 | "" 88 | (str 89 | (h/html 90 | [:head 91 | [:meta {:charset "UTF-8"}] 92 | [:meta {:name "viewport" 93 | :content "width=device-width, initial-scale=1"}] 94 | ;; HTMX stylesheet 95 | [:link {:rel "stylesheet" :href "resources/missing.min.css"}] 96 | ;; Codemirror stylesheets 97 | [:link {:rel "stylesheet" :href "resources/codemirror.min.css"}] 98 | [:link {:rel "stylesheet" :href "resources/nord.min.css"}] 99 | ;; SDFx stylesheet 100 | [:style "#code-container { height: 1100px } 101 | @media (max-width: 1200px) { 102 | #editor-container { flex-direction: column; } #code-container { height: 300px; } 103 | }"] 104 | [:title "SDFx"]] 105 | [:body 106 | [:main 107 | {:style {:padding 0}} 108 | ;; sdf src that gets compiled into the fragment Shader 109 | ;; Needs the container div because the mutation observer looks at the container 110 | ;; when the #sdf-src div is swapped, then the mutation is observed. If you observe 111 | ;; the #sdf-src directly, the mutation is not observed because HTMX is swapping a new node in, 112 | ;; and the mutation observer is attached to the old one, so nothing ever happens 113 | [:div#sdf-src-container 114 | [:div#sdf-src {:hx-get "/src" 115 | :hx-trigger "contentChanged" 116 | :hx-vals "js:{ 'sdf-src': editor.getValue() }" 117 | :style {:display "none"}} 118 | (if-let [src-str nil #_@last-src-str] 119 | (compile-sdf (eval (read-string src-str)) @render-settings) 120 | (compile-sdf starting-sdf-fn @render-settings))]] 121 | [:h2.center {:style {:margin "0.5rem"}} "SDFx"] 122 | [:div#editor-container.center.f-row.justify-content:center 123 | {:style {:gap "0px"}} 124 | ;; editor 125 | [:div#code-container.center 126 | {:style {:border "1px solid white"}} 127 | [:textarea#editor 128 | {:style {:width "700px" :height "100%" 129 | :border "1px solid white"} 130 | :type "text" 131 | :name "sdf-src"} 132 | (if-let [src-str @last-src-str] 133 | (strip-do src-str) 134 | (str 135 | (simple-code-printer starting-sdf-ns) "\n" 136 | (simple-code-printer starting-sdf-src)))]] 137 | ;; canvas 138 | [:div.center.f-col.align-items:start 139 | {:style {:gap "0px"}} 140 | [:form#render-settings.margin.tool-bar {:style {:height 0 :z-index 1000}} 141 | [:input#contour-cols {:hx-post "/settings" 142 | :hx-swap "none" 143 | :type "radio" 144 | :name "colors" 145 | :value "contours" 146 | :checked (= "contours" (:colors @render-settings))}] 147 | [:label {:for "contour-cols"} "Z-Contour Color"] 148 | [:input#normal-cols {:hx-post "/settings" 149 | :hx-swap "none" 150 | :type "radio" 151 | :name "colors" 152 | :value "normals" 153 | :checked (= "normals" (:colors @render-settings))}] 154 | [:label {:for "normal-cols"} "Normal Color"] 155 | [:input#outlines {:hx-post "/settings" 156 | :hx-swap "none" 157 | :type "checkbox" 158 | :name "outlines" 159 | :checked (:outlines @render-settings)}] 160 | [:label {:for "outlines"} "Outlines"]] 161 | [:canvas#canvas 162 | {:style {:width "700px" :height "700px" 163 | :border "1px solid white"} 164 | :width 1400 :height 1400}] 165 | [:div#contour.center 166 | {:style {:width "700px" :height "400px" 167 | :border "1px solid white"}}]]] 168 | [:div.center 169 | [:section.tool-bar.margin 170 | [:button {:hx-get "/docs" 171 | :hx-target "#docs"} "Docs"] 172 | [:hr] 173 | [:button {:type "button" 174 | :onclick "grabPixels()"} "Get Contours"] 175 | [:hr] 176 | [:button {:hx-get "/gallery" 177 | :hx-target "#gallery"} "Gallery"] 178 | [:hr] 179 | [:form 180 | [:label "filename: " [:input {:type "text" :name "filename" :style {:width "150px" :margin-right "16px"}}]] 181 | [:button {:hx-post "/save" 182 | :hx-target "#save-status"} "Save"]] 183 | [:div#save-status]]] 184 | [:section#docs] 185 | [:section#gallery] 186 | [:div#scripts {:style {:display "none"}} 187 | ;; Codemirror scripts 188 | [:script {:src "resources/codemirror.min.js"}] 189 | [:script {:src "resources/clojure.min.js"}] 190 | [:script {:src "resources/matchbrackets.min.js"}] 191 | [:script {:src "resources/closebrackets.min.js"}] 192 | ;; HTMX scripts 193 | [:script {:src "resources/htmx.min.js"}] 194 | ;; sdfx render script 195 | [:script {:src "resources/render.js"}]]]])))) 196 | 197 | (defn split-by [pred coll] 198 | (lazy-seq 199 | (when-let [s (seq coll)] 200 | (let [[xs ys] (split-with pred s)] 201 | (if (seq xs) 202 | (cons xs (split-by pred ys)) 203 | (let [!pred (complement pred) 204 | skip (take-while !pred s) 205 | others (drop-while !pred s) 206 | [xs ys] (split-with pred others)] 207 | (cons (concat skip xs) 208 | (split-by pred ys)))))))) 209 | 210 | (defn- split-path 211 | "split the list of points at any large jumps in the chain." 212 | [pts] 213 | (let [segments (partition 2 1 pts) 214 | lengths (mapv #(apply svg.u/distance %) segments) 215 | threshhold (* 6 (apply svg.u/average lengths))] 216 | (->> (map vector segments lengths) 217 | (mapv (fn [[segment l]] (when (< l threshhold) (first segment)))) 218 | (split-by nil?) 219 | (filter seq) 220 | (mapv (fn [pts] (vec (remove nil? pts))))))) 221 | 222 | (defn join-paths 223 | [& paths] 224 | (let [ms (map (comp #(select-keys % [:d]) second) paths) 225 | props (-> (last paths) second (dissoc :d))] 226 | [:path (merge 227 | (apply (partial merge-with str) ms) 228 | props)])) 229 | 230 | ;; Route Response Functions 231 | 232 | (defn initial-response 233 | [_] 234 | {:status 200 235 | :body (apply str (template))}) 236 | 237 | (defn- resource-response 238 | [{:keys [params]}] 239 | (let [file (:file params) 240 | ext (last (str/split file #"\."))] 241 | {:status 200 242 | :headers {"Content-Type" (format "text/%s" (if (= ext "js") "javascript" ext))} 243 | :body (slurp (io/resource file))})) 244 | 245 | (defn- docs-response 246 | [_] 247 | (swap! show-docs not) 248 | {:status 200 249 | :body 250 | (str 251 | (h/html 252 | (when @show-docs 253 | (into 254 | [:div 255 | [:h3 "Docs"]] 256 | (for [{:keys [name ns arglists doc]} (map #(meta (second %)) 257 | (concat 258 | (ns-publics 'sdfx.geom) 259 | (ns-publics 'svg-clj.parametric)))] 260 | [:dl 261 | (into 262 | [:dt.mono-font] 263 | (for [arglist arglists] 264 | [:div (format "(%s/%s %s)" (str ns) name (str/join " " arglist))])) 265 | [:dd (str doc)]])))))}) 266 | 267 | (defn- gallery-response 268 | [_] 269 | (swap! show-gallery not) 270 | {:status 200 271 | :body 272 | (str 273 | (h/html 274 | (when @show-gallery 275 | [:div 276 | [:h3 "Gallery"] 277 | (into 278 | [:div#gallery-container.f-row.flex-wrap:wrap] 279 | (for [{:keys [svg-data src-str]} (reverse @contour-history)] 280 | [:div.f-row 281 | svg-data 282 | [:pre [:code src-str]]]))])))}) 283 | 284 | (defn- settings-response 285 | [{:keys [body]}] 286 | (reset! render-settings (u/query-string->map (slurp body))) 287 | {:status 200 288 | :body nil}) 289 | 290 | (defn- contours-response 291 | [{body :body}] 292 | (let [{contours "contours" sdf-src "sdfSrc"} (u/parse-body body) 293 | svg-data 294 | (let [poly-pts (remove nil? (mapcat second (json/parse-string contours) #_(sort-by rgb-str->hue (u/parse-body body))))] 295 | (-> 296 | (svg 297 | (for [pts poly-pts] 298 | (let [pts (map (fn [pt] [(+ (get pt "x")) (- (get pt "y"))]) pts) 299 | paths (map path/polygon (split-path pts))] 300 | (-> (apply join-paths paths) 301 | (tf/style {:fill "black" 302 | :stroke "white"}))))) 303 | (tf/style {:style {:max-width "400px" :max-height "400px"}})))] 304 | (update-contour-history! {:svg-data svg-data 305 | :src-str sdf-src}) 306 | {:status 200 307 | :body (str (h/html svg-data))})) 308 | 309 | (defn- save-response 310 | [{:keys [body]}] 311 | (let [output-dir (io/file "output/")] 312 | (when-not (.exists output-dir) 313 | (.mkdir output-dir))) 314 | (let [f (fn [body] 315 | (try (slurp body) 316 | (catch Exception _e ""))) 317 | [_ fname] (str/split (f body) #"=") 318 | fname (-> (or fname "") 319 | str/trim 320 | (->> (format "output/%s")))] 321 | (if (not= fname "output/") 322 | (do 323 | (spit fname (str (h/html (last @contour-history)))) 324 | {:status 200 325 | :body (format "Successfully saved as %s" fname)}) 326 | {:status 200 327 | :body "Filename required to save."}))) 328 | 329 | (defn- src-response 330 | [{:keys [query-string]}] 331 | (let [{:keys [sdf-src]} (u/query-string->map-reading-values query-string) 332 | {original-src-str :sdf-src} (u/query-string->map query-string) 333 | src (str/trim (str sdf-src)) #_(format "(do %s)" (str/trim (str sdf-src))) 334 | maybe-f (u/maybe-load-string src) 335 | f (if (fn? maybe-f) maybe-f (fn [_] "p"))] 336 | (when (fn? maybe-f) 337 | (reset! last-src-str (format "(do %s)" original-src-str) #_(if (= (first sdf-src) 'do) 338 | (apply str (rest sdf-src)) 339 | (str sdf-src)))) 340 | {:status 200 341 | :body (str (h/html (compile-sdf f @render-settings)))})) 342 | 343 | (def ^:private routes 344 | [{:path "/resources/:file" 345 | :method :get 346 | :response resource-response} 347 | {:path "/" 348 | :method :get 349 | :response initial-response} 350 | {:path "/settings" 351 | :method :post 352 | :response settings-response} 353 | {:path "/docs" 354 | :method :get 355 | :response docs-response} 356 | {:path "/gallery" 357 | :method :get 358 | :response gallery-response} 359 | {:path "/contours" 360 | :method :post 361 | :response contours-response} 362 | {:path "/save" 363 | :method :post 364 | :response save-response} 365 | {:path "/src" 366 | :method :get 367 | :response src-response}]) 368 | 369 | (defn- app 370 | [] 371 | #(ruuter/route routes %)) 372 | 373 | (defonce ^:private server (atom nil)) 374 | 375 | ;; https://github.com/prestancedesign/get-port/blob/main/src/prestancedesign/get_port.clj 376 | (defn- get-available-port 377 | "Return a random available TCP port in allowed range (between 1024 and 65535) or a specified one" 378 | ([] (get-available-port 0)) 379 | ([port] 380 | (with-open [socket (ServerSocket. port)] 381 | (.getLocalPort socket)))) 382 | 383 | (defn get-port 384 | "Get an available TCP port according to the supplied options. 385 | - A preferred port: (get-port {:port 3000}) 386 | - A vector of preferred ports: (get-port {:port [3000 3004 3010]}) 387 | - Use the `make-range` helper in case you need a port in a certain (inclusive) range: (get-port {:port (make-range 3000 3005)}) 388 | No args return a random available port" 389 | ([] (get-available-port)) 390 | ([opts] 391 | (loop [port (:port opts)] 392 | (let [result 393 | (try 394 | (get-available-port (if (number? port) port (first port))) 395 | (catch Exception e (instance? BindException (.getCause e))))] 396 | (or result (recur (if (number? port) 0 (next port)))))))) 397 | 398 | (defn stop-server [] 399 | (when-not (nil? @server) 400 | (@server :timeout 100) 401 | (reset! server nil))) 402 | 403 | (defn serve! 404 | [& {:keys [port]}] 405 | (stop-server) 406 | (let [available-port (get-port {:port (concat [(or port 9876)] (range 8000 9000))})] 407 | (reset! server (srv/run-server (#'app) {:port available-port})) 408 | (println "Server started on Port: " available-port))) 409 | 410 | (defn -main 411 | [] 412 | (serve!)) 413 | 414 | 415 | 416 | 417 | 418 | (comment 419 | 420 | 421 | (defn- save! 422 | [svg-data] 423 | (let [fname (str "output/" (gensym "contours-") ".svg")] 424 | (spit fname (str (h/html svg-data))))) 425 | 426 | (defn- show-latest 427 | [] 428 | (tools/cider-show (last @contour-history))) 429 | 430 | (defn- show-nth 431 | [n] 432 | (tools/cider-show (nth @contour-history n))) 433 | 434 | (defn- save-latest! 435 | [] 436 | (save! (last @contour-history))) 437 | 438 | (defn- save-nth! 439 | [n] 440 | (save! (nth @contour-history n))) 441 | ) 442 | -------------------------------------------------------------------------------- /src/sdfx/util.clj: -------------------------------------------------------------------------------- 1 | (ns sdfx.util 2 | (:require [cheshire.core :as json] 3 | [clojure.string :as str]) 4 | (:import [java.net URLDecoder])) 5 | 6 | ;; parsing utils 7 | 8 | (defn parse-body [body] 9 | (let [body (if (string? body) body (slurp body))] 10 | (-> body 11 | URLDecoder/decode 12 | (str/split #"=") 13 | second 14 | json/parse-string))) 15 | 16 | (defn url-encoded-str->str 17 | [s] 18 | (URLDecoder/decode s)) 19 | 20 | (defn maybe-read-string 21 | [s] 22 | (try (read-string s) 23 | (catch Exception _e nil))) 24 | 25 | (defn maybe-load-string 26 | [s] 27 | (try (load-string s) 28 | (catch Exception _e nil))) 29 | 30 | (defn query-string->map-reading-values [query-string] 31 | (if query-string 32 | (let [m (->> (str/split query-string #"[=&]") 33 | (partition 2) 34 | (mapv vec) 35 | (group-by first))] 36 | (-> m 37 | (update-vals #(map second %)) 38 | (update-vals #(map url-encoded-str->str %)) 39 | (update-vals #(map (fn [str] (maybe-read-string (format "(do %s)" str))) %)) 40 | (update-vals #(vec (replace {'NaN nil} %))) 41 | (update-vals #(if (= (count %) 1) (first %) %)) 42 | (update-keys keyword))) 43 | {})) 44 | 45 | (defn query-string->map [query-string] 46 | (if query-string 47 | (let [m (->> (str/split query-string #"[=&]") 48 | (partition 2) 49 | (mapv vec) 50 | (group-by first))] 51 | (-> m 52 | (update-vals #(map second %)) 53 | (update-vals #(map url-encoded-str->str %)) 54 | #_(update-vals #(map (fn [str] (maybe-read-string (format "(do %s)" str))) %)) 55 | (update-vals #(vec (replace {'NaN nil} %))) 56 | (update-vals #(if (= (count %) 1) (first %) %)) 57 | (update-keys keyword))) 58 | {})) 59 | 60 | (defn rgb->hsv [r g b] 61 | (let [r (/ r 255.0) 62 | g (/ g 255.0) 63 | b (/ b 255.0) 64 | max (max r g b) 65 | min (min r g b) 66 | d (- max min) 67 | v max 68 | s (if (zero? max) 0 (/ d max)) 69 | h (cond 70 | (zero? s) 0 71 | (= r max) (mod (/ (- g b) d) 6) 72 | (= g max) (/ (+ 2 (- b r)) d) 73 | :else (/ (+ 4 (- r g)) d))] 74 | [(-> h (* 60) (mod 360)) (* s 100) (* v 100)])) 75 | 76 | (defn rgb-str->hue 77 | [[rgb-str _]] 78 | (let [f (fn [s] 79 | (mapv #(Integer. %) (str/split s #"_"))) 80 | [r g b] (f rgb-str) 81 | [h _ _] (rgb->hsv r g b)] 82 | h)) 83 | --------------------------------------------------------------------------------