├── docs ├── .nojekyll ├── _media │ ├── handles.gif │ ├── mograph.png │ ├── evaluation.gif │ ├── screenshot.png │ ├── bandwidth_hsl.png │ ├── bandwidth_vdmx.png │ ├── houdini-ladder.gif │ ├── transform_uis.png │ ├── circle-inspector.png │ ├── smikey-primitive.gif │ ├── bandwidth_psbrush.gif │ ├── bandwidth_xy-drag.gif │ ├── transform_houdini.png │ ├── transform_constraints.png │ └── bandwidth_xy-separated.gif ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── _navbar.md ├── defining-handle.md ├── extensibility.md ├── distance-space.md ├── _coverpage.md ├── en │ ├── _sidebar.md │ ├── README.md │ ├── draw-tree.md │ ├── get-started.md │ ├── cheatsheet.md │ └── styles.md ├── schema.md ├── orthogonality.md ├── _sidebar.md ├── README.md ├── principles.md ├── draw-tree.md ├── classification-of-animating.md ├── get-started.md ├── about.md ├── styles.md ├── cheatsheet.md ├── syntax.md ├── transform.md └── blend-modes.md ├── .browserslistrc ├── assets └── logo.png ├── public ├── ogp.png ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── .htaccess ├── examples │ ├── 10-print-chr.glisp │ ├── hello-world.glisp │ ├── replicator.glisp │ ├── transformation.glisp │ ├── path-modification.glisp │ └── primitive-definition.glisp ├── embed.html ├── lib │ ├── schema-study.glisp │ ├── ui.glisp │ ├── color.glisp │ └── tools-example.glisp └── index.html ├── src ├── default-canvas.glisp ├── scopes │ ├── repl.ts │ ├── view.ts │ └── app.ts ├── components │ ├── PageIndex │ │ ├── index.ts │ │ ├── utils.ts │ │ └── use │ │ │ ├── index.ts │ │ │ ├── use-compact-scrollbar.ts │ │ │ ├── use-dialog-settings.ts │ │ │ ├── use-hit-detector │ │ │ ├── index.ts │ │ │ └── hit-detector.ts │ │ │ ├── use-dialog-command.ts │ │ │ ├── use-exp-history.ts │ │ │ ├── use-url-parser.ts │ │ │ └── use-modes.ts │ ├── GlobalMenu │ │ ├── index.ts │ │ ├── GlobalSubmenu.vue │ │ ├── WindowTitleButtons.vue │ │ └── GlobalMenu.vue │ ├── GlispEditor │ │ ├── index.ts │ │ ├── utils.ts │ │ └── setup.ts │ ├── ViewHandles │ │ └── index.ts │ ├── use │ │ ├── use-rem.ts │ │ ├── index.ts │ │ ├── use-resize-sensor.ts │ │ ├── use-keyboard-state.ts │ │ ├── use-numeric-vector-updator.ts │ │ ├── use-mouse-events.ts │ │ ├── use-draggable.ts │ │ └── use-gesture.ts │ ├── inspectors │ │ └── index.ts │ ├── style │ │ ├── global.styl │ │ ├── vmodal.styl │ │ └── common.styl │ ├── inputs │ │ ├── index.ts │ │ ├── InputSeed.vue │ │ ├── use-number-input.styl │ │ ├── InputDropdown.vue │ │ ├── use-number-input.ts │ │ ├── InputRotery.vue │ │ ├── InputBoolean.vue │ │ ├── InputNumber.vue │ │ ├── InputString.vue │ │ ├── InputColor.vue │ │ ├── InputTranslate.vue │ │ └── InputSlider.vue │ ├── mal-inputs │ │ ├── index.ts │ │ ├── MalInputDropdown.vue │ │ ├── MalInputSymbol.vue │ │ ├── MalInputKeyword.vue │ │ ├── MalInputSeed.vue │ │ ├── MalInputBoolean.vue │ │ ├── MalInputString.vue │ │ ├── MalInputAngle.vue │ │ ├── MalInputParam.vue │ │ ├── MalInputVec2.vue │ │ └── MalExpButton.vue │ ├── ViewCanvas.vue │ └── dialogs │ │ ├── DialogSettings.vue │ │ └── DialogCommand.vue ├── @types │ ├── shims-vue.d.ts │ ├── shims-tsx.d.ts │ └── modules.d.ts ├── mal │ ├── index.ts │ └── interop.ts ├── renderer │ ├── canvas-renderer │ │ ├── worker.ts │ │ ├── index.ts │ │ └── canvas-renderer.ts │ ├── render-to-svg.ts │ └── get-rendererd-image.ts ├── generator.ts ├── pages │ ├── embed.ts │ └── index.ts ├── repl.ts ├── mal-lib │ ├── math.ts │ └── color.ts ├── utils.ts ├── path-utils.ts └── background.ts ├── ftp.info.sample.js ├── .prettierrc ├── .gitignore ├── FUNDING.yml ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── webpack.repl.config.js ├── vue.config.js ├── task.deploy.js ├── package.json └── export-refs.glisp /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/assets/logo.png -------------------------------------------------------------------------------- /public/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/public/ogp.png -------------------------------------------------------------------------------- /src/default-canvas.glisp: -------------------------------------------------------------------------------- 1 | (style 2 | (fill "crimson") 3 | (circle [0 0] 100)) -------------------------------------------------------------------------------- /src/scopes/repl.ts: -------------------------------------------------------------------------------- 1 | import Scope from '@/mal/scope' 2 | 3 | export default new Scope() 4 | -------------------------------------------------------------------------------- /docs/_media/handles.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/handles.gif -------------------------------------------------------------------------------- /docs/_media/mograph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/mograph.png -------------------------------------------------------------------------------- /docs/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/_media/evaluation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/evaluation.gif -------------------------------------------------------------------------------- /docs/_media/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/screenshot.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/_media/bandwidth_hsl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/bandwidth_hsl.png -------------------------------------------------------------------------------- /docs/_media/bandwidth_vdmx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/bandwidth_vdmx.png -------------------------------------------------------------------------------- /docs/_media/houdini-ladder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/houdini-ladder.gif -------------------------------------------------------------------------------- /docs/_media/transform_uis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/transform_uis.png -------------------------------------------------------------------------------- /docs/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /docs/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/components/PageIndex/index.ts: -------------------------------------------------------------------------------- 1 | import PageIndex from './PageIndex.vue' 2 | export default PageIndex 3 | -------------------------------------------------------------------------------- /docs/_media/circle-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/circle-inspector.png -------------------------------------------------------------------------------- /docs/_media/smikey-primitive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/smikey-primitive.gif -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/@types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /docs/_media/bandwidth_psbrush.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/bandwidth_psbrush.gif -------------------------------------------------------------------------------- /docs/_media/bandwidth_xy-drag.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/bandwidth_xy-drag.gif -------------------------------------------------------------------------------- /docs/_media/transform_houdini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/transform_houdini.png -------------------------------------------------------------------------------- /docs/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/components/GlobalMenu/index.ts: -------------------------------------------------------------------------------- 1 | import GlobalMenu from './GlobalMenu.vue' 2 | 3 | export default GlobalMenu 4 | -------------------------------------------------------------------------------- /src/mal/index.ts: -------------------------------------------------------------------------------- 1 | export {default as readStr} from './reader' 2 | export {default as printExp} from './printer' 3 | -------------------------------------------------------------------------------- /docs/_media/transform_constraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/transform_constraints.png -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - [Demo](https://glisp.app) 2 | 3 | - Translations 4 | - [🇺🇸 English](/en/) 5 | - [🇯🇵 日本語](/) 6 | -------------------------------------------------------------------------------- /src/components/GlispEditor/index.ts: -------------------------------------------------------------------------------- 1 | import GlispEditor from './GlispEditor.vue' 2 | 3 | export default GlispEditor 4 | -------------------------------------------------------------------------------- /src/components/ViewHandles/index.ts: -------------------------------------------------------------------------------- 1 | import ViewHandles from './ViewHandles.vue' 2 | 3 | export default ViewHandles 4 | -------------------------------------------------------------------------------- /docs/_media/bandwidth_xy-separated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/_media/bandwidth_xy-separated.gif -------------------------------------------------------------------------------- /docs/defining-handle.md: -------------------------------------------------------------------------------- 1 | # ハンドルを定義する 2 | 3 | Glisp の基本原則として、「全ての数値は、アナログ入力によって連続的に調整できる」 4 | 5 | もちろんインスペクタの数値をドラッグすることも 6 | -------------------------------------------------------------------------------- /docs/extensibility.md: -------------------------------------------------------------------------------- 1 | # 拡張性の違い 2 | 3 | 「拡張性の高いツールを作る」とは標榜するものの、ほとんどのデザインツールはなにかかしらの拡張性を備えています。ただ僕が言、単に拡張が可能かどうかではなく、いかに拡張 4 | -------------------------------------------------------------------------------- /docs/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/docs/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/components/PageIndex/utils.ts: -------------------------------------------------------------------------------- 1 | export function toSketchCode(code: string) { 2 | return `(sketch;__\n${code};__\n)` 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baku89/glisp/HEAD/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /ftp.info.sample.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | user: 'username', 3 | password: 'password', 4 | host: 'host.com', 5 | port: 21, 6 | remoteRoot: '/public_html', 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/canvas-renderer/worker.ts: -------------------------------------------------------------------------------- 1 | import * as Comlink from 'comlink' 2 | import CanvasRenderer from './canvas-renderer' 3 | 4 | Comlink.expose(CanvasRenderer) 5 | -------------------------------------------------------------------------------- /src/generator.ts: -------------------------------------------------------------------------------- 1 | // import ReplScope from '@/scopes/repl' 2 | // import ViewScope from '@/scopes/view' 3 | 4 | console.log('Unco') 5 | 6 | // window.generatorLoaded = true 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "bracketSpacing": false, 5 | "semi": false, 6 | "printWidth": 80, 7 | "trailingComma": "es5", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /src/components/use/use-rem.ts: -------------------------------------------------------------------------------- 1 | import {ref} from '@vue/composition-api' 2 | 3 | export default function useRem() { 4 | const rem = ref( 5 | parseFloat(getComputedStyle(document.documentElement).fontSize) 6 | ) 7 | return rem 8 | } 9 | -------------------------------------------------------------------------------- /docs/distance-space.md: -------------------------------------------------------------------------------- 1 | # 操作の距離空間 2 | 3 | デザインツールに限らず、良い道具は出来るだけ少ない手間で欲しい操作を行うことができます。 4 | 5 | 操作性 6 | 7 | ルールが単純なことです。 8 | 9 | デザインツールの「操作のしやすさ」を「その操作に何手かかるか」で表すとします。「一手」はクリックでもキー入力でも何でも良いです。すると、「操作性 10 | 11 | 「操作性の良さ」 12 | 13 | 例えばビューポート上に点があって、 14 | -------------------------------------------------------------------------------- /docs/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | #remove html file extension-e.g. https://example.com/file.html will become https://example.com/file 2 | RewriteEngine on 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME}\.html -f 5 | RewriteRule ^(.*)$ $1.html [NC,L] 6 | 7 | AddHandler fcgid-script .html -------------------------------------------------------------------------------- /src/components/inspectors/index.ts: -------------------------------------------------------------------------------- 1 | import CubicBezier from './cubic-bezier.vue' 2 | import Deftime from './deftime.vue' 3 | import Style from './style.vue' 4 | 5 | export default { 6 | // 'Inspector-cubic-bezier': CubicBezier, 7 | // 'Inspector-deftime': Deftime, 8 | // 'Inspector-style': Style, 9 | } 10 | -------------------------------------------------------------------------------- /src/@types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, {VNode} from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Glisp 6 | 7 | A Lisp-based Design Tool Bridging Graphic Design and Computational Arts. 8 | 9 | [Demo](https://glisp.app) 10 | [GitHub](https://github.com/baku89/glisp/) 11 | 12 |  13 | -------------------------------------------------------------------------------- /src/components/use/index.ts: -------------------------------------------------------------------------------- 1 | export {default as useDraggable} from './use-draggable' 2 | export {default as useKeyboardState} from './use-keyboard-state' 3 | export {default as useResizeSensor} from './use-resize-sensor' 4 | export {default as useRem} from './use-rem' 5 | export {default as useGesture} from './use-gesture' 6 | export {default as useNumericVectorUpdator} from './use-numeric-vector-updator' 7 | -------------------------------------------------------------------------------- /src/renderer/render-to-svg.ts: -------------------------------------------------------------------------------- 1 | import C2S from 'canvas2svg' 2 | import {MalVal} from '@/mal/types' 3 | import renderToContext from './render-to-context' 4 | 5 | export default function renderToSvg( 6 | view: MalVal, 7 | width: number, 8 | height: number 9 | ) { 10 | const ctx = new C2S(width, height) 11 | 12 | renderToContext(ctx, view) 13 | 14 | return ctx.getSerializedSvg(true) 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /mal 6 | 7 | /repl/lib/* 8 | /ftp.info.js 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | #Electron-builder output 29 | /dist_electron 30 | -------------------------------------------------------------------------------- /docs/en/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Getting Started 2 | 3 | - [What's Glisp?](about) 4 | - [Syntax](syntax) 5 | - [First Sketch](get-started) 6 | 7 | - Functionality 8 | 9 | - [Applying Styles](styles) 10 | - [Transformation](transform) 11 | - [Draw Tree](draw-tree) 12 | - Primitives 13 | - Handles 14 | - Docstring 15 | - Inverse Evaluation 16 | - Edit Mode 17 | - Units 18 | 19 | - Reference 20 | 21 | - [Cheat Sheet](cheatsheet) 22 | - [Function List](ref) 23 | -------------------------------------------------------------------------------- /src/components/style/global.styl: -------------------------------------------------------------------------------- 1 | @import 'common.styl' 2 | 3 | *, ::after, ::before 4 | box-sizing border-box 5 | outline none 6 | -webkit-tap-highlight-color transparent 7 | 8 | html 9 | letter-spacing 0.05em 10 | font-size 12px 11 | font-family 'Fira Sans', sans-serif // monospace 12 | -webkit-font-smoothing antialiased 13 | -moz-osx-font-smoothing grayscale 14 | 15 | button 16 | outline none 17 | border none 18 | background none 19 | cursor pointer 20 | user-select none -------------------------------------------------------------------------------- /src/components/PageIndex/use/index.ts: -------------------------------------------------------------------------------- 1 | export {default as useAppCommands} from './use-app-commands' 2 | export {default as useCompactScrollbar} from './use-compact-scrollbar' 3 | export {default as useDialogCommand} from './use-dialog-command' 4 | export {default as useDialogSettings} from './use-dialog-settings' 5 | export {default as useExpHistory} from './use-exp-history' 6 | export {default as useHitDetector} from './use-hit-detector' 7 | export {default as useURLParser} from './use-url-parser' 8 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # スキーマ 2 | 3 | Lisp は文字列としても編集できますが、インスペクタから編集することができます。例えば、円のパスを返すコードは `(circle [0 0] 100)` のようになりますが、Glisp ではこの引数部分を GUI から編集することができます。 4 | 5 |  6 | 7 | この時、Lisp がどのように GUI コンポーネントへと変換されるかを、**スキーマ** によって設定することができます。例の circle の引数部分は、このようなスキーマ構造となっています。 8 | 9 | ```clojure 10 | [{:label "Center" :type "vec2"} 11 | {:label "Radius" :type "number"}] 12 | ``` 13 | 14 | ### 値 15 | 16 | | 型 | UI | 17 | | --- | --- | 18 | 19 | -------------------------------------------------------------------------------- /public/examples/10-print-chr.glisp: -------------------------------------------------------------------------------- 1 | ;; Example: 10 PRINT CHR$(205.5+RND(1)); : GOTO 10 2 | ;; https://10print.org 3 | 4 | (def w 36) 5 | (def s (/ w 2)) 6 | 7 | (defn slash [i p] 8 | (path/transform 9 | (mat2d/* (translate p) 10 | (scale-x (compare (rnd i) .5))) 11 | (line [(- s) (- s)] [s s]))) 12 | 13 | (background "snow") 14 | 15 | (style (stroke "salmon" 10) 16 | (for [y (column -5 5 w) 17 | x (column -5 5 w) 18 | :index i] 19 | (slash i [x y]))) -------------------------------------------------------------------------------- /src/pages/embed.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ResizeSensor from 'resize-sensor' 3 | import VueCompositionApi from '@vue/composition-api' 4 | import App from '@/components/PageEmbed.vue' 5 | 6 | Vue.config.productionTip = false 7 | Vue.use(VueCompositionApi) 8 | 9 | new Vue({ 10 | render: h => h(App), 11 | }).$mount('#app') 12 | 13 | const el = document.documentElement 14 | new ResizeSensor(el, () => { 15 | const data = [document.body.scrollWidth, document.body.scrollHeight] 16 | window.parent.postMessage(data, '*') 17 | }) 18 | -------------------------------------------------------------------------------- /public/examples/hello-world.glisp: -------------------------------------------------------------------------------- 1 | ;; Example: Hello World 2 | 3 | (background "darkblue") 4 | 5 | ;; The basic syntax is almost same as Hiccup and Dali 6 | ;; https://github.com/stathissideris/dali/blob/master/doc/syntax.md 7 | 8 | (transform 9 | (translate [50 0]) 10 | ;; Imagine former line as: 11 | ;; 12 | 13 | ;; Circle 14 | (style (fill "#f2ff53") 15 | (circle [0 0] 100)) 16 | 17 | ;; Text 18 | (style (fill "#ff4684") 19 | (text "Hello World" [0 0] 20 | :size 25))) -------------------------------------------------------------------------------- /public/examples/replicator.glisp: -------------------------------------------------------------------------------- 1 | ;; Example: Replicator 2 | 3 | (defn path/replicator 4 | [xform n path] 5 | (->> (reduce #(conj % (mat2d/* (last %) xform)) 6 | [(mat2d/ident)] 7 | (range (dec n))) 8 | (map #(path/transform % path)))) 9 | 10 | :start-sketch 11 | 12 | (background "#08101D") 13 | 14 | (style (stroke "#CECCFF" 1) 15 | 16 | (path/replicator 17 | (mat2d/* (translate [30 -24]) 18 | (rotate (deg 10)) 19 | (scale [0.95 0.95])) 20 | 60 21 | (circle [0 0] 150))) -------------------------------------------------------------------------------- /public/examples/transformation.glisp: -------------------------------------------------------------------------------- 1 | ;; Example: Transformation 2 | 3 | (background "#F5F5F5") 4 | 5 | (transform 6 | ;; Applies transformation to the draw context. 7 | ;; ;; You can see the stroke scales 8 | (mat2d/* (translate [0 -100]) 9 | (scale [2 1]) 10 | (pivot [50 50] 11 | (rotate (deg 45)))) 12 | 13 | (style 14 | (stroke "#FFE04E" 10) 15 | 16 | ;; Applies transformation to the path data. 17 | (path/transform 18 | (mat2d/* (rotate 0) 19 | (scale [1 2])) 20 | 21 | (rect [0 0 100 100])))) 22 | 23 | -------------------------------------------------------------------------------- /src/components/inputs/index.ts: -------------------------------------------------------------------------------- 1 | export {default as InputBoolean} from './InputBoolean.vue' 2 | export {default as InputColor} from './InputColor.vue' 3 | export {default as InputDropdown} from './InputDropdown.vue' 4 | export {default as InputNumber} from './InputNumber.vue' 5 | export {default as InputString} from './InputString.vue' 6 | export {default as InputRotery} from './InputRotery.vue' 7 | export {default as InputSeed} from './InputSeed.vue' 8 | export {default as InputSlider} from './InputSlider.vue' 9 | export {default as InputTranslate} from './InputTranslate.vue' 10 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueCompositionApi from '@vue/composition-api' 3 | import App from '@/components/PageIndex' 4 | import VModal from 'vue-js-modal' 5 | import PortalVue from 'portal-vue' 6 | 7 | Vue.config.productionTip = false 8 | Vue.use(VueCompositionApi) 9 | Vue.use(VModal, { 10 | dynamicDefaults: { 11 | height: 'auto', 12 | width: 400, 13 | transition: 'vmodal__transition', 14 | overlayTransition: 'vmodal__overlay-transition', 15 | }, 16 | }) 17 | Vue.use(PortalVue) 18 | 19 | new Vue({ 20 | render: h => h(App), 21 | }).$mount('#app') 22 | -------------------------------------------------------------------------------- /docs/orthogonality.md: -------------------------------------------------------------------------------- 1 | # 直交性 2 | 3 | ポール・グレアム氏のテキスト [On Lisp](http://www.asahi-net.or.jp/~kc7k-nd/onlispjhtml/returningFunctions.html) にこのような一節がありました。 4 | 5 | > 直交的(orthogonal)なプログラミング言語とは, 少数のオペレータを多数の様々な方法で結合させることで, 多様な意味が表現できるもののことだ. おもちゃのブロックは極めて直交的だが,プラモデルはほとんど直交的でない. 6 | 7 | 便利な言葉を見つけたものだと思いました。これは UNIX 哲学にも通じるものがあります。 8 | 9 | > 一つのことを行い、またそれをうまくやるプログラムを書け。 10 | > 協調して動くプログラムを書け。 11 | 12 | 出来るだけコンパクトで抽象性の高い、少数のモジュールを好きなように組み合わせることで、自分の欲しい機能を無駄なく柔軟に表現することができるツールは、結果として拡張性の高いものになると同時に学習コストも低くなります。 13 | 14 | ノードベースのツールは直交性が高いものが多いです。Houdini は直交性が高いツールとえいますが、それでも個々のモジュールの粒度が高く、「名は体を表す」とは言えない複雑奇怪なモジュールを工夫して組み合わせる局面が多かったりします。もちろん、3DCG 自体がまだまだ負荷の高い処理であり、細かい部分で最適化を挟んでいかないといけないからでもあるのですが。 15 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: baku89 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | commonjs: true, 6 | }, 7 | extends: [ 8 | 'plugin:vue/essential', 9 | 'eslint:recommended', 10 | '@vue/typescript/recommended', 11 | '@vue/prettier', 12 | '@vue/prettier/@typescript-eslint', 13 | ], 14 | parserOptions: { 15 | ecmaVersion: 2020, 16 | }, 17 | rules: { 18 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | '@typescript-eslint/no-use-before-define': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /src/components/use/use-resize-sensor.ts: -------------------------------------------------------------------------------- 1 | import {Ref, onMounted, onBeforeMount, unref} from '@vue/composition-api' 2 | import ResizeSensor from 'resize-sensor' 3 | 4 | export default function useResizeSensor( 5 | element: Ref | HTMLElement, 6 | onResized: (el: HTMLElement) => any, 7 | immediate = false 8 | ) { 9 | let sensor: any 10 | 11 | function setup(el: HTMLElement) { 12 | sensor = new ResizeSensor(el, () => onResized(el)) 13 | if (immediate) { 14 | onResized(el) 15 | } 16 | } 17 | 18 | onMounted(() => { 19 | const el = unref(element) 20 | if (!el) return 21 | setup(el) 22 | }) 23 | 24 | onBeforeMount(() => { 25 | if (sensor) { 26 | sensor.detach() 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/style/vmodal.styl: -------------------------------------------------------------------------------- 1 | // Styles to overwrite vue-modal-js 2 | @import './common.styl' 3 | 4 | div#modals-container 5 | z-index 1000 6 | 7 | div.vm 8 | &--overlay 9 | background transparent 10 | backdrop-filter blur(3px) 11 | 12 | &--modal 13 | border 1px solid var(--border) 14 | background var(--opaque) 15 | box-shadow 0 0 20px 20px var(--background) 16 | 17 | div.vmodal 18 | &__transition 19 | &-enter-active, &-leave-active 20 | transition opacity 0.15s ease, transform 0.15s ease 21 | 22 | &-enter, &-leave-to 23 | opacity 0 24 | transform scale(0.98) 25 | 26 | &__overlay-transition 27 | &-enter-active, &-leave-active 28 | transition opacity 0.2s ease 29 | 30 | &-enter, &-leave-to 31 | opacity 0 -------------------------------------------------------------------------------- /src/repl.ts: -------------------------------------------------------------------------------- 1 | import readlineSync from 'readline-sync' 2 | import Scope from './mal/scope' 3 | 4 | const replScope = new Scope() 5 | 6 | if (typeof process !== 'undefined' && 2 < process.argv.length) { 7 | const filename = process.argv[2] 8 | 9 | replScope.def('*ARGV*', process.argv.slice(3)) 10 | replScope.def('*filename*', filename) 11 | replScope.REP(`(import "${filename}")`) 12 | process.exit(0) 13 | } 14 | 15 | replScope.REP(`(str "Glisp [" *host-language* "]")`) 16 | 17 | while (true) { 18 | const line = readlineSync.question('glisp> ') 19 | if (line == null) { 20 | break 21 | } 22 | if (line === '') { 23 | continue 24 | } 25 | try { 26 | replScope.REP(line) 27 | } catch (e) { 28 | console.error('Error:', e) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scopes/view.ts: -------------------------------------------------------------------------------- 1 | import {symbolFor as S} from '@/mal/types' 2 | import Scope from '@/mal/scope' 3 | 4 | import AppScope from './app' 5 | import Env from '@/mal/env' 6 | 7 | interface ViewScopeOption { 8 | guideColor: string | null 9 | } 10 | 11 | function onSetup(scope: Scope, option: ViewScopeOption) { 12 | const {guideColor} = option 13 | 14 | const env = new Env() 15 | 16 | if (guideColor) { 17 | env.set(S('*guide-color*'), guideColor) 18 | } else { 19 | env.set(S('guide/stroke'), () => null) 20 | } 21 | 22 | scope.popBinding() 23 | scope.pushBinding(env) 24 | } 25 | 26 | export function createViewScope() { 27 | return new Scope(AppScope, 'view', onSetup, true) 28 | } 29 | 30 | export default createViewScope() 31 | -------------------------------------------------------------------------------- /src/components/inputs/InputSeed.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env", "jquery", "node"], 15 | "typeRoots": ["node_modules/@types", "src"], 16 | "paths": { 17 | "*": ["src/@types/*"], 18 | "@/*": ["src/*"] 19 | }, 20 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 21 | }, 22 | "include": [ 23 | "src/**/*.ts", 24 | "src/**/*.tsx", 25 | "src/**/*.vue", 26 | "tests/**/*.ts", 27 | "tests/**/*.tsx" 28 | ], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - ことはじめ 2 | 3 | - [Glisp とは?](about) 4 | - [構文](syntax) 5 | - [はじめてのスケッチ](get-started) 6 | 7 | - 機能について 8 | 9 | - [スタイルの応用](styles) 10 | - [トランスフォーム](transform) 11 | - [描画ツリー](draw-tree) 12 | - プリミティブ 13 | - [ハンドル](defining-handle) 14 | - [スキーマ](schema) 15 | - Docstring 16 | - 逆評価 17 | - 編集モード 18 | - 単位系 19 | 20 | - リファレンス 21 | 22 | - [チートシート](cheatsheet) 23 | - [関数](ref) 24 | 25 | - デザインツールの考察 26 | 27 | - [なぜ Lisp なのか](why-lisp) 28 | - [マシな作り方の作り方を作る](https://baku89.com/2020/06/26/c-activity) 29 | - [ブートストラップ](bootstrapping ':disabled') 30 | - [直交性](orthogonality) 31 | - [動かし方の分類](classification-of-animating) 32 | - [UI の基本原則](principles) 33 | - [インターフェースの帯域幅](bandwidth) 34 | - [操作の距離空間](distance-space ':disabled') 35 | - [ブレンドモードの分類](blend-modes) 36 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-compact-scrollbar.ts: -------------------------------------------------------------------------------- 1 | export default function useCompactScrollBar() { 2 | const isMacOS = /mac/i.test(navigator.platform) 3 | 4 | // Set the style for platforms other than macOS 5 | if (!isMacOS) { 6 | const head = document.head || document.getElementsByTagName('head') 7 | const style = document.createElement('style') 8 | 9 | head.appendChild(style) 10 | 11 | style.type = 'text/css' 12 | style.appendChild( 13 | document.createTextNode(` 14 | 15 | ::-webkit-scrollbar { 16 | width: 0.25rem; 17 | height: 0.5rem; 18 | } 19 | 20 | /* Track */ 21 | ::-webkit-scrollbar-track { 22 | display: none; 23 | } 24 | 25 | /* Handle */ 26 | ::-webkit-scrollbar-thumb { 27 | border-radius: 0.125rem; 28 | background: rgba(128, 128, 128, 0.3); 29 | }`) 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-dialog-settings.ts: -------------------------------------------------------------------------------- 1 | import {SetupContext} from '@vue/composition-api' 2 | import ConsoleScope from '@/scopes/console' 3 | import DialogSettings from '@/components/dialogs/DialogSettings.vue' 4 | import AppScope from '@/scopes/app' 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const DEFAULT_SETTINGS = require('raw-loader!@/default-settings.glisp') 8 | .default as string 9 | 10 | export default function useDialogCommand(context: SetupContext) { 11 | const {$modal} = context.root 12 | 13 | const settings = localStorage.getItem('settings') || DEFAULT_SETTINGS 14 | 15 | AppScope.readEval(`(do ${settings}\n)`) 16 | 17 | ConsoleScope.def('show-settings', () => { 18 | $modal.show( 19 | DialogSettings, 20 | {}, 21 | { 22 | width: 800, 23 | } 24 | ) 25 | 26 | return null 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-hit-detector/index.ts: -------------------------------------------------------------------------------- 1 | import {Ref} from '@vue/composition-api' 2 | import {NonReactive} from '@/utils' 3 | import {MalVal, MalNode, isSeq, isNode} from '@/mal/types' 4 | import {HitDetector} from './hit-detector' 5 | import {vec2} from 'gl-matrix' 6 | import AppScope from '@/scopes/app' 7 | import {generateExpAbsPath} from '@/mal/utils' 8 | 9 | export default function useHitDetector(exp: Ref>) { 10 | const detector = new HitDetector() 11 | 12 | AppScope.def('detect-hit', (pos: MalVal) => { 13 | if ( 14 | isSeq(pos) && 15 | typeof pos[0] === 'number' && 16 | typeof pos[1] === 'number' 17 | ) { 18 | const p = vec2.clone(pos as any) 19 | const ret = detector.analyze(p, exp.value.value) 20 | 21 | if (isNode(ret)) { 22 | return generateExpAbsPath(ret) 23 | } 24 | } 25 | 26 | return false 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/get-rendererd-image.ts: -------------------------------------------------------------------------------- 1 | import {MalVal} from '@/mal/types' 2 | import createCanvasRender, {CanvasRendererType} from './canvas-renderer' 3 | import {mat2d} from 'gl-matrix' 4 | 5 | const getRendereredImage = (() => { 6 | let canvasRenderer: CanvasRendererType 7 | 8 | return async ( 9 | viewExp: MalVal, 10 | {format = 'png', scaling = 1, bounds = [0, 0, 100, 100]} = {} 11 | ) => { 12 | if (!canvasRenderer) { 13 | canvasRenderer = await createCanvasRender() 14 | } 15 | 16 | const [x, y, width, height] = bounds as number[] 17 | 18 | canvasRenderer.resize(width, height, scaling) 19 | const viewTransform = mat2d.fromTranslation(mat2d.create(), [-x, -y]) 20 | await canvasRenderer.render(viewExp, {viewTransform}) 21 | const image = await canvasRenderer.getImage({format}) 22 | 23 | return image 24 | } 25 | })() 26 | 27 | export default getRendereredImage 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Glisp 2 | 3 | Glisp = **G**raphical **L**isp 4 | 5 | Lisp ベースのグラフィックデザインツール / クリエイティブコーディング環境を目指して開発しています。 6 | 7 | ことはじめ 8 | 9 | - [Glisp とは?](about) 10 | - [構文](syntax) 11 | - [はじめてのスケッチ](get-started) 12 | 13 | 機能について 14 | 15 | - [スタイルの応用](styles) 16 | - [トランスフォーム](transform) 17 | - [描画ツリー](draw-tree) 18 | - パス関数 19 | - メタデータ 20 | - [ハンドル](defining-handle) 21 | - スキーマ 22 | - 逆評価 23 | - 編集モード 24 | - 単位系 25 | 26 | リファレンス 27 | 28 | - [チートシート](cheatsheet) 29 | - [関数](ref) 30 | 31 | デザインツールの考察 32 | 33 | - [なぜ Lisp なのか](why-lisp) 34 | - [マシな作り方の作り方を作る](https://baku89.com/2020/06/26/c-activity) 35 | - [ブートストラップ](bootstrapping ':disabled') 36 | - [直交性](orthogonality) 37 | - [動かし方の分類](classification-of-animating) 38 | - [UI の基本原則](principles) 39 | - [インターフェースの帯域幅](bandwidth) 40 | - [操作の距離空間](distance-space ':disabled') 41 | - [ブレンドモードの分類](blend-modes) 42 | -------------------------------------------------------------------------------- /src/components/inputs/use-number-input.styl: -------------------------------------------------------------------------------- 1 | @import '../style/common.styl' 2 | 3 | use-number() 4 | position relative 5 | font-monospace() 6 | 7 | &__drag 8 | position absolute 9 | top 0 10 | left 0 11 | z-index 100 12 | width 100% 13 | height 100% 14 | border 1px solid transparent 15 | border-radius 2px 16 | input-transition(border-color) 17 | input-border() 18 | 19 | &.editing &__drag 20 | display none 21 | 22 | &__input 23 | input() 24 | width 100% 25 | color var(--syntax-constant) 26 | text-align right 27 | 28 | /* Chrome, Safari, Edge, Opera */ 29 | &::-webkit-outer-spin-button, &::-webkit-inner-spin-button 30 | margin 0 31 | -webkit-appearance none 32 | 33 | /* Firefox */ 34 | &[type=number] 35 | -moz-appearance textfield 36 | 37 | &.exp &__input 38 | color var(--red) 39 | 40 | &.grayed-out &__input 41 | color var(--comment) 42 | 43 | &.tweaking &__input 44 | border-color var(--hover) -------------------------------------------------------------------------------- /public/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | '( G L I S P ) 8 | 12 | 16 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/GlispEditor/utils.ts: -------------------------------------------------------------------------------- 1 | import ace from 'brace' 2 | 3 | export function replaceStrByRange( 4 | s: string, 5 | start: number, 6 | end: number, 7 | substitute: string 8 | ) { 9 | return s.substring(0, start) + substitute + s.substring(end) 10 | } 11 | 12 | export function getEditorSelection(editor: ace.Editor) { 13 | const sel = editor.getSelection() 14 | const doc = editor.getSession().doc 15 | 16 | const range = sel.getRange() 17 | const start = doc.positionToIndex(range.start, 0) 18 | const end = doc.positionToIndex(range.end, 0) 19 | 20 | return [start, end] 21 | } 22 | 23 | export function convertToAceRange( 24 | editor: ace.Editor, 25 | start: number, 26 | end: number 27 | ) { 28 | const doc = editor.getSession().doc 29 | 30 | const s = doc.indexToPosition(start, 0) 31 | const e = doc.indexToPosition(end, 0) 32 | 33 | const range = editor.getSelectionRange() 34 | range.setStart(s.row, s.column) 35 | range.setEnd(e.row, e.column) 36 | 37 | return range 38 | } 39 | -------------------------------------------------------------------------------- /docs/principles.md: -------------------------------------------------------------------------------- 1 | # UI の基本原則 2 | 3 | [Apple Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/)や、Google の[Material Design](https://material.io/design)、あるいは「驚き最小の原則」のように、UI を設計する指針はいくつかありますが、プロが使うデザインソフトウェアは、市井の人たちが生活の中でカジュアルに使う道具とはまた違った考え方が必要になるはずです。体系立てられるほど整理はついていませんが、デザインソフトウェアを設計する上での基本原則になりそうな考え方・ルールを雑多に書き残しておきます。 4 | 5 | ## 不可知論的デザイン 6 | 7 | - 使われ方を想定して機能を具体化しすぎない 8 | - ✗「これはコマ撮りのためのソフトで…」 9 | - 禁止ではなく、非推奨にする 10 | - ✗「『角を丸める』半径に負の値を設定させない」(面白いバグが生まれるかも) 11 | 12 | ## 直交性 13 | 14 | - 少数の要素を多様な方法で結合させることで多様な意味を実現する 15 | 16 | ## カスタマイズ性 17 | 18 | - できる限りカスタマイズできるようにする 19 | - 仕様を覚えてもらうのではなく、覚えやすいように設定してもらう 20 | 21 | ## インターフェースの帯域 22 | 23 | - 入力方法を多様化させる 24 | - 入力される情報量を無駄にしない 25 | - 入力と操作の対応付けを任意に設定できるようにする 26 | 27 | ## 操作の距離空間 28 | 29 | - ある操作に必要なステップ数を最小化させる 30 | - 操作の種類ごとに必要なステップ数に偏りをつくらない 31 | - アーティスト自身が必要に応じて操作の距離空間をデザインできるようにする 32 | 33 | ## 構造の隠蔽 34 | 35 | - 構造ではなく目の前に見える結果を直接編集できるようにする 36 | - 構造上のキリの良さを意識させない 37 | - 構造をカプセル化し、より高次の構造のチューニングに集中できるようにする 38 | -------------------------------------------------------------------------------- /src/components/mal-inputs/index.ts: -------------------------------------------------------------------------------- 1 | export {default as MalInputAngle} from './MalInputAngle.vue' 2 | export {default as MalInputBoolean} from './MalInputBoolean.vue' 3 | export {default as MalInputColor} from './MalInputColor.vue' 4 | export {default as MalInputDropdown} from './MalInputDropdown.vue' 5 | export {default as MalInputKeyword} from './MalInputKeyword.vue' 6 | export {default as MalInputNumber} from './MalInputNumber.vue' 7 | export {default as MalInputVec2} from './MalInputVec2.vue' 8 | export {default as MalInputRect2d} from './MalInputRect2d.vue' 9 | export {default as MalInputMat2d} from './MalInputMat2d.vue' 10 | export {default as MalInputSeed} from './MalInputSeed.vue' 11 | export {default as MalInputSize2d} from './MalInputSize2d.vue' 12 | export {default as MalInputSlider} from './MalInputSlider.vue' 13 | export {default as MalInputString} from './MalInputString.vue' 14 | export {default as MalInputSymbol} from './MalInputSymbol.vue' 15 | export {default as MalExpButton} from './MalExpButton.vue' 16 | -------------------------------------------------------------------------------- /src/renderer/canvas-renderer/index.ts: -------------------------------------------------------------------------------- 1 | import CanvasRenderer from './canvas-renderer' 2 | import * as Comlink from 'comlink' 3 | import {mat2d} from 'gl-matrix' 4 | 5 | export interface ViewerSettings { 6 | viewTransform?: mat2d 7 | guideColor?: string 8 | } 9 | 10 | export type CanvasRendererType = CanvasRenderer | Comlink.Remote 11 | 12 | export default async function createCanvasRender(canvas?: HTMLCanvasElement) { 13 | let renderer: CanvasRendererType 14 | 15 | if (!canvas) { 16 | canvas = document.createElement('canvas') 17 | } 18 | 19 | if (typeof OffscreenCanvas !== 'undefined') { 20 | const CanvasRendererWorker = Comlink.wrap( 21 | new Worker('./worker.ts', {type: 'module'}) 22 | ) as any 23 | 24 | const offscreenCanvas = canvas.transferControlToOffscreen() 25 | 26 | renderer = (await new CanvasRendererWorker( 27 | Comlink.transfer(offscreenCanvas, [offscreenCanvas]) 28 | )) as Comlink.Remote 29 | } else { 30 | renderer = new CanvasRenderer(canvas) 31 | } 32 | 33 | return renderer 34 | } 35 | -------------------------------------------------------------------------------- /src/components/use/use-keyboard-state.ts: -------------------------------------------------------------------------------- 1 | import {toRefs, reactive, Ref} from '@vue/composition-api' 2 | import hotkeys from 'hotkeys-js' 3 | import keycode from 'keycode' 4 | import AppScope from '@/scopes/app' 5 | import {MalVal, getName} from '@/mal/types' 6 | 7 | let state: {[keycode: string]: Ref} 8 | 9 | hotkeys('*', {keyup: true, keydown: true}, (e: KeyboardEvent) => { 10 | if (!state) { 11 | return 12 | } 13 | 14 | let code = keycode(e) 15 | 16 | if (code.includes('command')) { 17 | code = 'ctrl' 18 | } 19 | 20 | if (code in state) { 21 | state[code].value = e.type === 'keydown' 22 | } 23 | }) 24 | 25 | export default function useKeyboardState() { 26 | if (!state) { 27 | state = toRefs( 28 | reactive({ 29 | shift: false, 30 | alt: false, 31 | ctrl: false, 32 | }) 33 | ) 34 | } 35 | 36 | AppScope.def('modifier-pressed?', (...keys: MalVal[]) => { 37 | for (const key of keys) { 38 | const code = getName(key) 39 | if (!state[code] || !state[code].value) { 40 | return false 41 | } 42 | } 43 | 44 | return true 45 | }) 46 | 47 | return state 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Baku Hashimoto 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. -------------------------------------------------------------------------------- /src/mal-lib/math.ts: -------------------------------------------------------------------------------- 1 | import {MalVal, symbolFor as S, createList as L} from '@/mal/types' 2 | import hull from 'hull.js' 3 | import BezierEasing from 'bezier-easing' 4 | import Delaunator from 'delaunator' 5 | import {partition} from '@/utils' 6 | 7 | const Exports = [ 8 | [ 9 | 'convex-hull', 10 | (pts: [number, number][], concavity: number | null = null) => { 11 | return hull(pts, concavity === null ? Infinity : concavity) 12 | }, 13 | ], 14 | [ 15 | 'delaunay', 16 | (pts: [number, number][]) => { 17 | const delaunay = Delaunator.from(pts) 18 | return partition(3, delaunay.triangles).map(([a, b, c]) => [ 19 | [...pts[a]], 20 | [...pts[b]], 21 | [...pts[c]], 22 | ]) 23 | }, 24 | ], 25 | [ 26 | 'cubic-bezier', 27 | (x1: number, y1: number, x2: number, y2: number, t: number) => { 28 | const easing = BezierEasing(x1, y1, x2, y2) 29 | return easing(Math.min(Math.max(0, t), 1)) 30 | }, 31 | ], 32 | ] as [string, MalVal][] 33 | 34 | const Exp = L( 35 | S('do'), 36 | ...Exports.map(([sym, body]) => L(S('def'), S(sym), body)) 37 | ) 38 | ;(globalThis as any)['glisp_library'] = Exp 39 | 40 | export default Exp 41 | -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | # Glisp 2 | 3 | Glisp = **G**raphical **L**isp 4 | 5 | A Lisp-based Design Tool Bridging Graphic Design and Computational Arts. 6 | 7 | Getting Started 8 | 9 | - [What's Glisp?](about) 10 | - [Syntax](syntax) 11 | - [First Sketch](get-started) 12 | 13 | Functionality 14 | 15 | - [Applying Styles](styles) 16 | - [Transformation](transform) 17 | - [Draw Tree](draw-tree) 18 | - Primitives 19 | - Handles 20 | - Docstring 21 | - Inverse Evaluation 22 | - Edit Mode 23 | - Units 24 | 25 | Reference 26 | 27 | - [Cheat Sheet](cheatsheet) 28 | - [Function List](ref) 29 | 30 | Studying Design Tools (in Japanese) 31 | 32 | - [Why Lisp?](/why-lisp) 33 | - [Making making of better making](https://baku89.com/2020/06/26/c-activity) 34 | - [Bootstrapping strategy](/bootstrapping ':disabled') 35 | - [Orthogonality of a tool](/orthogonality) 36 | - [Classification of animations](/classification-of-animating) 37 | - [Principles of UI](/principles) 38 | - [Bandwidth of interfaces](/bandwidth) 39 | - [Distance space of manipulation](/distance-space ':disabled') 40 | - [Categories of blend modes](/blend-modes) 41 | -------------------------------------------------------------------------------- /src/mal/interop.ts: -------------------------------------------------------------------------------- 1 | import {MalError} from './types' 2 | import {convertJSObjectToMalMap} from './reader' 3 | 4 | export default { 5 | resolveJS(str: string): [any, any] { 6 | if (str.match(/\./)) { 7 | // eslint-disable-next-line no-useless-escape 8 | const match = /^(.*)\.[^\.]*$/.exec(str) 9 | 10 | if (match === null) { 11 | throw new MalError('[interop.resolveJS] Cannot resolve') 12 | } else { 13 | return [eval(match[1]), eval(str)] 14 | } 15 | } else { 16 | return [globalThis, eval(str)] 17 | } 18 | }, 19 | 20 | jsToMal(obj: any) { 21 | if (obj === null || obj === undefined) { 22 | return null 23 | } 24 | 25 | // const cache: any[] = [] 26 | 27 | // const str = JSON.stringify(obj, (key, value) => { 28 | // if (typeof value === 'object' && value !== null) { 29 | // if (cache.indexOf(value) !== -1) { 30 | // // Circular reference found, discard key 31 | // return 32 | // } 33 | // // Store value in our collection 34 | // cache.push(value) 35 | // } 36 | // return value 37 | // }) 38 | // return JSON.parse(str) 39 | 40 | return convertJSObjectToMalMap(obj) 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /public/examples/path-modification.glisp: -------------------------------------------------------------------------------- 1 | (defn cloud 2 | {:handles 3 | {:draw (fn {:params [x0 x1 y width]} 4 | [{:type "path" :path (line [x0 y] [x1 y]) 5 | :class "dashed"} 6 | {:id :start 7 | :type "point" :pos [x0 y]} 8 | {:id :end 9 | :type "point" :pos [x1 y]} 10 | {:id :width 11 | :type "arrow" :pos [x0 (+ y width)] 12 | :angle HALF_PI}]) 13 | :drag (fn {:id id :pos p :params [x0 x1 y width]} 14 | (case id 15 | :start [(.x p) x1 (.y p) width] 16 | :end [x0 (.x p) (.y p) width] 17 | :width [x0 x1 y (abs (- (.y p) y))]))}} 18 | [x0 x1 y width] 19 | (path/offset-stroke width 20 | (line [x0 y] [x1 y]))) 21 | 22 | :start-sketch 23 | 24 | (background "#BCDEDE") 25 | 26 | (g {:style (stroke "#F34386" 4) 27 | :transform (translate [62 198])} 28 | 29 | (def p 30 | (path/unite 31 | (cloud -428 136 -261 41) 32 | (cloud -205 294 -189 45) 33 | (cloud -142 126 -122 31) 34 | (cloud -73 300 -61 45))) 35 | 36 | (for [off [10 20]] 37 | (path/offset off p))) 38 | -------------------------------------------------------------------------------- /webpack.repl.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | 5 | module.exports = { 6 | node: { 7 | __filename: false, 8 | __dirname: false, 9 | }, 10 | entry: { 11 | index: './src/repl.ts', 12 | 'lib/core': './src/mal-lib/core.ts', 13 | 'lib/color': './src/mal-lib/color.ts', 14 | 'lib/path': './src/mal-lib/path.ts', 15 | 'lib/math': './src/mal-lib/math.ts', 16 | }, 17 | mode: 'production', 18 | resolve: { 19 | alias: { 20 | '@': path.resolve(__dirname, 'src'), 21 | }, 22 | extensions: ['.tsx', '.ts', '.js'], 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | use: 'ts-loader', 29 | exclude: /node_modules/, 30 | }, 31 | ], 32 | }, 33 | target: 'node', 34 | output: { 35 | filename: '[name].js', 36 | path: path.resolve(__dirname, 'repl'), 37 | globalObject: 'this', 38 | libraryTarget: 'umd', 39 | libraryExport: '', 40 | }, 41 | plugins: [ 42 | new webpack.IgnorePlugin(/jsdom$/), 43 | new CopyPlugin({ 44 | patterns: [ 45 | { 46 | from: 'public/lib', 47 | to: 'lib', 48 | }, 49 | ], 50 | }), 51 | ], 52 | } 53 | -------------------------------------------------------------------------------- /src/components/GlobalMenu/GlobalSubmenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | {{ label }} 10 | 11 | 12 | 13 | 14 | 28 | 29 | 54 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputDropdown.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 52 | -------------------------------------------------------------------------------- /src/components/style/common.styl: -------------------------------------------------------------------------------- 1 | font-monospace() 2 | letter-spacing 0 3 | font-family 'Fira Code', monospace 4 | font-variant-ligatures normal !important 5 | 6 | $input-height = 1.8rem 7 | $button-height = 1.4rem 8 | $input-horiz-margin = 0.6rem 9 | 10 | input-transition($prop = all) 11 | transition $prop 0.1s ease 12 | 13 | input() 14 | width 6em 15 | height $input-height 16 | border none 17 | border 1px solid transparent 18 | border-radius 2px 19 | background var(--input) 20 | color var(--foreground) 21 | font-size inherit 22 | input-transition(border-color) 23 | 24 | &:hover 25 | border-color var(--hover) 26 | 27 | &:focus 28 | border-color var(--hover) 29 | 30 | input-border() 31 | border 1px solid transparent 32 | border-radius 2px 33 | input-transition(border-color) 34 | 35 | &:hover 36 | border-color var(--hover) 37 | 38 | &:focus 39 | border-color var(--hover) 40 | 41 | translucent-bg() 42 | background var(--translucent) 43 | backdrop-filter blur(16px) 44 | 45 | labeled-button() 46 | padding-top 0.2rem 47 | height $button-height 48 | border-radius 2px 49 | background var(--button) 50 | color var(--background) 51 | font-size 0.9em 52 | input-transition(all) 53 | 54 | &:hover, &:focus 55 | background var(--hover) -------------------------------------------------------------------------------- /src/mal-lib/color.ts: -------------------------------------------------------------------------------- 1 | import chroma from 'chroma-js' 2 | import { symbolFor as S, createList as L, MalVal } from '@/mal/types' 3 | 4 | const Exports = [ 5 | [ 6 | 'color/mix', 7 | function (color1: string, color2: string, ratio: number, mode = 'lrgb') { 8 | return chroma.mix(color1, color2, ratio, mode as any).css() 9 | }, 10 | ], 11 | [ 12 | 'color/brighten', 13 | function (color: string, value = 1) { 14 | return chroma(color) 15 | .brighten(value as number) 16 | .hex() 17 | }, 18 | ], 19 | [ 20 | 'color/darken', 21 | function (color: string, value = 1) { 22 | return chroma(color) 23 | .darken(value as number) 24 | .hex() 25 | }, 26 | ], 27 | [ 28 | 'color/invert', 29 | function (color: string, mode: 'rgb' | 'hsl' = 'rgb') { 30 | const c = chroma(color) 31 | const a = c.alpha() 32 | if (mode === 'rgb') { 33 | const [r, g, b] = c.rgb() 34 | return chroma([255 - r, 255 - g, 255 - b, a]).hex() 35 | } else if (mode === 'hsl') { 36 | const [h, s, l] = c.hsl() 37 | return chroma((h + 180) % 360, 1 - s, 1 - l, 'hsl').hex() 38 | } 39 | } 40 | ] 41 | ] as [string, MalVal][] 42 | 43 | const Exp = L( 44 | S('do'), 45 | ...Exports.map(([sym, body]) => L(S('def'), S(sym), body)) 46 | ) 47 | ; (globalThis as any)['glisp_library'] = Exp 48 | -------------------------------------------------------------------------------- /src/scopes/app.ts: -------------------------------------------------------------------------------- 1 | import Scope from '@/mal/scope' 2 | import Mousetrap from 'mousetrap' 3 | import ReplScope from './repl' 4 | import {MalVal, isList, MalError} from '@/mal/types' 5 | import ConsoleScope from './console' 6 | 7 | function onSetup() { 8 | AppScope.readEval('(unset-all-keybinds)') 9 | } 10 | 11 | const AppScope = new Scope(ReplScope, 'app', onSetup) 12 | 13 | // Keybinds 14 | 15 | AppScope.def('set-keybind', (keybind: MalVal, exp: MalVal) => { 16 | if (typeof keybind !== 'string' || !isList(exp)) { 17 | throw new MalError('Invalid argument for set-keybind') 18 | } 19 | 20 | const callback = (e: KeyboardEvent) => { 21 | e.stopPropagation() 22 | e.preventDefault() 23 | ConsoleScope.eval(exp) 24 | } 25 | 26 | Mousetrap.bind(keybind, callback) 27 | 28 | return true 29 | }) 30 | 31 | AppScope.def('trigger-keybind', (keybind: MalVal) => { 32 | if (typeof keybind !== 'string') { 33 | throw new MalError('Keybind should be string') 34 | } 35 | 36 | Mousetrap.trigger(keybind) 37 | 38 | return true 39 | }) 40 | 41 | AppScope.def('unset-all-keybinds', () => { 42 | Mousetrap.reset() 43 | 44 | return true 45 | }) 46 | 47 | AppScope.def('*global-menu*', []) 48 | AppScope.def('set-global-menu', (menu: MalVal) => { 49 | AppScope.def('*global-menu*', menu) 50 | return true 51 | }) 52 | 53 | export default AppScope 54 | -------------------------------------------------------------------------------- /src/components/inputs/InputDropdown.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ labels ? labels[index] : value }} 8 | 9 | 10 | 11 | 44 | 45 | 59 | -------------------------------------------------------------------------------- /src/components/use/use-numeric-vector-updator.ts: -------------------------------------------------------------------------------- 1 | import {Ref, computed, SetupContext} from '@vue/composition-api' 2 | import { 3 | MalVal, 4 | getEvaluated, 5 | MalSeq, 6 | MalSymbol, 7 | isVector, 8 | cloneExp, 9 | } from '@/mal/types' 10 | import {reverseEval} from '@/mal/utils' 11 | import {NonReactive, nonReactive} from '@/utils' 12 | 13 | /** 14 | * Refs and event handles for MalInputVec2, Rect2d, Mat2d 15 | */ 16 | export default function useNumericVectorUpdator( 17 | exp: Ref>, 18 | context: SetupContext 19 | ) { 20 | const isValueSeparated = computed(() => isVector(exp.value.value)) 21 | 22 | const nonReactiveValues = computed(() => { 23 | if (!isValueSeparated.value) { 24 | return [] 25 | } else { 26 | return Array.from(exp.value.value as MalSeq).map(nonReactive) 27 | } 28 | }) 29 | 30 | const evaluated = computed(() => getEvaluated(exp.value.value) as number[]) 31 | 32 | function onInputElement(i: number, v: NonReactive) { 33 | if (!isValueSeparated.value) { 34 | return 35 | } 36 | 37 | const newExp = cloneExp(exp.value.value as MalSeq) 38 | newExp[i] = v.value 39 | context.emit('input', nonReactive(newExp)) 40 | } 41 | 42 | function onInputEvaluatedElement(i: number, v: number) { 43 | const value = cloneExp(exp.value.value as MalSeq) 44 | value[i] = v 45 | const newExp = reverseEval(value, exp.value.value) 46 | context.emit('input', nonReactive(newExp)) 47 | } 48 | 49 | return { 50 | nonReactiveValues, 51 | isValueSeparated, 52 | evaluated, 53 | onInputElement, 54 | onInputEvaluatedElement, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/GlispEditor/setup.ts: -------------------------------------------------------------------------------- 1 | import ace from 'brace' 2 | import './define-glisp-mode' 3 | import {useResizeSensor} from '@/components/use' 4 | import ConsoleScope from '@/scopes/console' 5 | 6 | // require('brace/theme/tomorrow') 7 | // require('brace/theme/tomorrow_night') 8 | require('brace/mode/clojure') 9 | 10 | function setupSettings(editor: ace.Editor) { 11 | editor.$blockScrolling = Infinity 12 | editor.setShowPrintMargin(false) 13 | editor.setOption('displayIndentGuides', false) 14 | // editor.setTheme('tomorrow') 15 | 16 | const session = editor.getSession() 17 | // session.setMode('ace/mode/clojure') 18 | session.setMode('ace/mode/glisp') 19 | 20 | session.setUseWrapMode(true) 21 | 22 | editor.setOptions({ 23 | highlightActiveLine: false, 24 | showGutter: false, 25 | tabSize: 2, 26 | useSoftTabs: false, 27 | maxLines: Infinity, 28 | }) 29 | } 30 | 31 | function setupResizeHandler(editor: ace.Editor) { 32 | useResizeSensor(editor.container, () => { 33 | editor.resize(true) 34 | }) 35 | } 36 | 37 | function setupKeybinds(editor: ace.Editor) { 38 | editor.commands.addCommand({ 39 | name: 'select-outer', 40 | bindKey: {win: 'Ctrl-p', mac: 'Command-p'}, 41 | exec: () => { 42 | ConsoleScope.readEval('(select-outer)') 43 | }, 44 | }) 45 | 46 | editor.commands.addCommand({ 47 | name: 'expand-selected', 48 | bindKey: {win: 'Ctrl-e', mac: 'Command-e'}, 49 | exec: () => { 50 | ConsoleScope.readEval('(expand-selected)') 51 | }, 52 | }) 53 | } 54 | 55 | export function setupEditor(editor: ace.Editor) { 56 | setupSettings(editor) 57 | setupResizeHandler(editor) 58 | setupKeybinds(editor) 59 | } 60 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputSymbol.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /src/components/inputs/use-number-input.ts: -------------------------------------------------------------------------------- 1 | import {Ref, computed, SetupContext} from '@vue/composition-api' 2 | import keycode from 'keycode' 3 | 4 | const VERTICAL_ARROW_KEYS = new Set(['up', 'down']) 5 | 6 | export default function useNumber( 7 | value: Ref, 8 | tweaking: Ref, 9 | context: SetupContext 10 | ) { 11 | const displayValue = computed(() => { 12 | const v = value.value 13 | return tweaking.value ? v.toFixed(1) : v.toFixed(2).replace(/\.?[0]+$/, '') 14 | }) 15 | 16 | const step = computed(() => { 17 | const float = value.value.toString().split('.')[1] 18 | return float !== undefined ? Math.max(Math.pow(10, -float.length), 0.1) : 1 19 | }) 20 | 21 | function onConfirm(e: Event) { 22 | const str = (e.target as HTMLInputElement).value 23 | const num = parseFloat(str) 24 | const val = isNaN(num) ? str : num 25 | update(val) 26 | } 27 | 28 | function onBlur(e: Event) { 29 | onConfirm(e) 30 | } 31 | 32 | function onKeydown(e: KeyboardEvent) { 33 | const key = keycode(e) 34 | 35 | if (key === 'enter') { 36 | onConfirm(e) 37 | } else if (VERTICAL_ARROW_KEYS.has(key)) { 38 | e.preventDefault() 39 | 40 | let inc = 1 41 | if (e.altKey) { 42 | inc = 0.1 43 | } else if (e.shiftKey) { 44 | inc = 10 45 | } 46 | 47 | switch (key) { 48 | case 'up': 49 | update(value.value + inc) 50 | break 51 | case 'down': 52 | update(value.value - inc) 53 | break 54 | } 55 | } 56 | } 57 | 58 | function update(val: number | string) { 59 | context.emit('input', val) 60 | } 61 | return { 62 | step, 63 | displayValue, 64 | onBlur, 65 | onKeydown, 66 | update, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputKeyword.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | : 4 | 11 | 12 | 13 | 14 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-dialog-command.ts: -------------------------------------------------------------------------------- 1 | import {SetupContext} from '@vue/composition-api' 2 | import ConsoleScope from '@/scopes/console' 3 | import { 4 | MalVal, 5 | MalError, 6 | isFunc, 7 | isSymbol, 8 | getMeta, 9 | MalMap, 10 | MalFunc, 11 | MalType, 12 | createList as L, 13 | } from '@/mal/types' 14 | import {printExp} from '@/mal' 15 | import DialogCommand from '@/components/dialogs/DialogCommand.vue' 16 | import {getMapValue} from '@/mal/utils' 17 | import {printer} from '@/mal/printer' 18 | import {NonReactive, nonReactive} from '@/utils' 19 | 20 | export default function useDialogCommand(context: SetupContext) { 21 | const {$modal} = context.root 22 | 23 | ConsoleScope.def('show-command-dialog', (f: MalVal) => { 24 | if (f === undefined || !isSymbol(f)) { 25 | throw new MalError(`${printExp(f)} is not a symbol`) 26 | } 27 | 28 | // Retrieve default parameters 29 | const fn = ConsoleScope.var(f.value) 30 | const meta = getMeta(fn) 31 | const paramsDesc = getMapValue(meta, 'params', MalType.Vector) as 32 | | MalMap[] 33 | | null 34 | let initialParams = getMapValue(meta, 'initial-params') as 35 | | MalFunc 36 | | MalVal[] 37 | | null 38 | 39 | if (!paramsDesc || !initialParams) { 40 | printer.error('NO initial parmas') 41 | return null 42 | } 43 | 44 | if (isFunc(initialParams)) { 45 | initialParams = initialParams() as MalVal[] 46 | } 47 | 48 | // Create the expression with default parameters 49 | const exp: NonReactive = nonReactive(L(f, ...initialParams)) 50 | 51 | // Show Modal 52 | $modal.show( 53 | DialogCommand, 54 | { 55 | exp, 56 | fn, 57 | }, 58 | {} 59 | ) 60 | 61 | return null 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/components/inputs/InputRotery.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 54 | 55 | 81 | -------------------------------------------------------------------------------- /public/examples/primitive-definition.glisp: -------------------------------------------------------------------------------- 1 | ;; Example: Primitive Definition 2 | ;; Defining new type of primitives 3 | 4 | (defn star 5 | {:doc "Generates a star path" 6 | 7 | ;; Parameter annotation 8 | :params 9 | [{:label "Center" :type "vec2"} 10 | {:label "# Vertices" :type "number" 11 | :validator #(round (max 2 %))} 12 | {:label "Radius 0" :type "number" 13 | :validator #(max 0 %)} 14 | {:label "Radius 1" :type "number" 15 | :validator #(max 0 %)}] 16 | 17 | ;; Handles definition 18 | :handles 19 | {:draw 20 | ;; Returns a list of handles with ID 21 | ;; from the function's parameters 22 | (fn {:params [c n rmin rmax]} 23 | (let [rmin-angle (/ PI n)] 24 | [{:id :center :type "point" :pos c} 25 | {:id :rmin 26 | :type "arrow" 27 | :pos (vec2/+ 28 | c 29 | (vec2/dir rmin-angle rmin)) 30 | :angle rmin-angle} 31 | {:id :rmax 32 | :type "arrow" 33 | :pos (vec2/+ c [rmax 0])}])) 34 | :drag 35 | ;; In turn, returns new parameters 36 | ;; from the handle's ID and position 37 | (fn {:id id :pos pos 38 | :params [c n rmin rmax]} 39 | (case id 40 | :center [pos n rmin rmax] 41 | :rmin [c n (vec2/dist c pos) rmax] 42 | :rmax [c n rmin (vec2/dist c pos)]))}} 43 | 44 | ;; Function body 45 | [c n rmin rmax] 46 | (apply polygon 47 | (for [i (range (* n 2))] 48 | (let [a (* (/ i n) PI) 49 | r (if (mod i 2) rmin rmax)] 50 | (vec2/+ c (vec2/dir a r)))))) 51 | 52 | :start-sketch 53 | 54 | (background "#C5B3AE") 55 | 56 | (style (stroke "#FFC679" 12) 57 | ;; Try click 'star' on below 58 | ;; then you can see the inspector 59 | ;; and handles on the view 60 | (star [0 0] 6 70 190)) -------------------------------------------------------------------------------- /docs/draw-tree.md: -------------------------------------------------------------------------------- 1 | # 描画ツリー 2 | 3 | Glisp のスケッチは、評価されることで「描画ツリー」とここで呼ぶデータを返します。例えば: 4 | 5 | ```cljs 6 | (style (fill "darkblue") 7 | (rect [10 10 80 80])) 8 | ``` 9 | 10 | というスケッチは、このように展開されます。 11 | 12 | ```clojure 13 | [:g :_ 14 | [:style {:fill true :fill-color "darkblue"} 15 | [:path :M [10 10] :L [90 10] :L [90 90] :L [10 90] :Z]]] 16 | ``` 17 | 18 | この描画ツリーはスケッチと違い、関数呼び出しなど含まない、最終的に出力されるグラフィックを記述した静的なデータです。SVG の構造に似ています。ビューポート上に表示したり、PNG や SVG などの様々なフォーマットで書き出すするときには、実際にはこの描画ツリーがレンダラーに渡されることになります。 19 | 20 | `rect` や `style` をはじめ、グラフィックに関わる多くの関数はこの描画ツリーを返します。もちろん、描画ツリーをスケッチ上に直接描くすることも出来ますが、そこからは「ここにこの大きさの丸がある」などといった意味論的な編集情報は抜け落ちてしまいます。これはスケッチと描画ツリーとの関係は、Illustrator の編集用ファイルと、アウトライン化したファイルの関係性にも似ています。 21 | 22 | ## メリット 23 | 24 | Glisp の描画ツリーは動的なデータを含まず、その構造もとてもシンプルなので、レンダラーに評価器を実装する必要はなく、コンパクトに実装することができます。([Canvas API での実装例](https://github.com/baku89/glisp/blob/master/src/renderer/render-to-context.ts))また、プロジェクトファイルにスケッチに加えて評価後の描画ツリーのデータも同時に保存することできるはずです。そうすることでファイルを分けずともプロジェクトファイル自体をアウトライン後の入稿データとしても扱えたり、アプリ本体はなくともビュアーアプリでプレビュー出来る仕組みを作ることも容易です。 25 | 26 | ## 描画ツリーを確認する 27 | 28 | 現在のスケッチの描画ツリーは、コンソールから `*view*` と打ち込むことで確認できます。また、エディタでカーソルによって黄色くハイライトされている括弧部分を、`Ctrl+E` から評価後の値に展開することも可能です。次々と関数が描画ツリーに置き換わっていくのが確認できます。 29 | 30 | ## シンタックス 31 | 32 | 描画ツリーはベクタから構成され、子要素を持つエレメントとそうでない空要素に大別できます。 33 | 34 | - **子要素を持つ**: `:g` `:clip` `:transform` `:style` 35 | - **空要素**: `:path` `:text` など 36 | 37 | 子要素を持つエレメントはかならずこのような構造をとっています。 38 | 39 | ``` 40 | [:<タグ名><#ID名(オプション)> <属性値> <子要素1> <子要素2> ...] 41 | ``` 42 | 43 | また、`:path` は SVG の[パスコマンド](https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/d#Path_commands)と同じ構造です。しかし、使えるコマンドは絶対位置指定の `M` `L` `C` `Z` のみです。これもレンダラーの実装をシンプルにするためです。 44 | 45 | ```clojure 46 | [:path :M [0 0] ;; moveTo 47 | :L [100 100] ;; lineTo 48 | :C [50 100] [50 50] [20 20] ;; cubicCurveTo 49 | :Z] 50 | ``` 51 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputSeed.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 58 | 59 | 70 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputBoolean.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 14 | 15 | 16 | 17 | 64 | 65 | 72 | -------------------------------------------------------------------------------- /docs/classification-of-animating.md: -------------------------------------------------------------------------------- 1 | # 動かし方の分類 2 | 3 | 4 | 5 | なにかを動かす時、操作する物理量が何回微分されたものか — 具体的には位置/速度/加速度のどれをアニメーションさせるのかによって「動かし方」を分類できるのではないかとこの数年考えています。 6 | 7 | ゲーム内での自機の移動を例にとると分かりやすくて。Pong でツマミが操作しているのは位置。スーパーマリオ 64 で 3D スティックが操作しているのは速度。アーケードレースゲームでアクセルとブレーキが操作しているのは加速度。 8 | 9 | 映像制作の中で「動き」について考えるときも、この分類はなかなか有効だと思う。手描きアニメや、キーフレームアニメーションは位置の推移をプロットしていくことで動かすし、ストップモーションやクレイアニメは、フレーム間の変化量(速度)だけ対象物をずらしたり変形させることで動きを与えていく。アニメだって頭の中で「速さ」とか「タメ/ツメ」をイメージしながら描くじゃないか、と一瞬思うけれども、結局紙の上に残していくのはあくまでもそのコマにおけるキャラクターの位置。 10 | 11 | 実写撮影だと、慣性が強く働くものを身体で動かす際には加速度を操作することが多い。ドリーを手押しする時は、台車に力(≡ 加速度)を与えることで加速/減速させる。ジブもそう。手持ちやステディでカメラを動かす時も、基本的には加速度を操作していることになるのだけど、重力との合力になるので少しわかりづらい… 12 | 13 | そういう風に考えれば、3D ソフト上で実写らしいカメラワークをつけようにも、どこかぎこちなさが残るのは、アニメーションさせている物理量の次元が違うからだと説明がつく。 14 | 15 | そもそも多くの映像ソフトが、速度や加速度に対してキーフレームを打つことを許さないのは(パーティクル系のような例外はあるけれど)、現在の状態をフレーム 0 から漸化的に計算しないといけなくなるからで、時間を前後させながら編集したり、並列処理させるには不都合だからだったりする。またユーザーにとっては、変化量をアニメーションさせては動きの終点を厳密に設定できないという問題もあって。そのために、AfterEffects の速度カーブなんかは 2 つの固定されたキーフレーム間の速度の配分のみ編集できる仕組みになっている。 16 | 17 | アニメーターやモーションデザイナーのような、「動き」について考える人達の多くが、パラメーターの変化量ではなく、実は絶対量の推移を描いて(打ち込んで)動きを設計していることになる。それは先に挙げたようなソフトウェア側の都合でもあるし、一度紙に描いた線を指でこすってずらせないように、そのメディアが持つ物性によるものだったりもするので、良し悪しの話とはまた別なのだけど。考えてみれば「動き」という概念自体、文字通り変化量のことなので、そこにちょっとした矛盾も感じる。 18 | 19 | 「ここでシュン!っとなる」の「シュン!」は速度のピークを表しているのだけども、それを実際に紙やアニメーションカーブ上に再現するときには、一旦頭の中で積分して位置に置き換えてやらなくてはいけない。現実でものを動かす時、摩擦で慣性が働きづらいものには速度を、滑ったり転がったりするものには加速度を力として与えることになるので、そのワンクッションは、人の身体感覚からすると不自然なことなのかもしれなくて。パラパラマンガで跳ねるゴムボールを描くことすらとても難しく、アニメーターには鍛錬が必要なのも、本質的にはその部分の違いに由来する反直感性にある気がする。 20 | 21 | この 1、2 年、クレイアニメやフィードバック系を使ったグラフィック作りを試していたのは、(比喩としてではなく、物理量としての)違う次元から「動き」について考えてみたかったからなのかなぁ、と今になって思いました。 22 | 23 | とか衒学趣味じみたことをダラダラ考えつつ、こういう UI があれば面白いぞと oF でスケッチしてたのが、上の動画でした。 24 | -------------------------------------------------------------------------------- /docs/get-started.md: -------------------------------------------------------------------------------- 1 | # はじめてのスケッチ 2 | 3 | ## 丸を描く 4 | 5 | 一番簡単なスケッチの例です。 6 | 7 | ```cljs 8 | (circle [50 50] 40) 9 | ``` 10 | 11 | 関数 `circle` は、丸のパスを生成する関数です。`[100 100]` は丸の中心、`40`は半径となります。Glisp のキャンバスは右上が原点です。 12 | 13 | しかし、まだ塗りや線が設定されていないので、点線で表示されています。これをトマト色に塗りつぶしてみます。 14 | 15 | ```cljs 16 | (style (fill "tomato") 17 | (circle [50 50] 40)) 18 | ``` 19 | 20 | `style` はスタイルを適用する関数です。 1 つめの引数 `(fill "red")` というスタイルを 2 つめ以降の引数、この場合は `(circle [50 50] 40)` というパスに適用しています。ちなみに、`fill` もまた、塗りのスタイル情報を返す関数です。 21 | 22 | ついでに線色もつけたい場合は、 `style` を入れ子にします。 23 | 24 | ```cljs 25 | (style (fill "RoyalBlue") 26 | (style (stroke "tomato" 10) 27 | (circle [50 50] 40))) 28 | ``` 29 | 30 | スタイルは外側から順に描画されます。この例では、丸がまずトマト色で塗られてから太さ 10 の青い線で縁取られます。この表記は、`style` 関数の第 2 引数をベクタにすることで、このように縮めて書くこともできます。 31 | 32 | ```cljs 33 | (style [(fill "tomato") (stroke "RoyalBlue" 10)] 34 | (circle [50 50] 40)) 35 | ``` 36 | 37 | 複数のパスにまとめてスタイルを適用することもできます。 38 | 39 | ```cljs 40 | (style (stroke "turquoise" 6) 41 | (circle [50 50] 40) 42 | (rect [20 40 60 20])) 43 | ``` 44 | 45 | `rect` は右上の位置と幅・高さから四角形のパスを返す関数です。 46 | 47 | ## トランスフォーム 48 | 49 | `transform` 関数を使って、グループ全体の位置や角度といったトランスフォーム値を設定することができます。 50 | 51 | ```cljs 52 | (transform (translate [30 30]) 53 | (style (fill "gold") 54 | (rect [0 0 60 40]))) 55 | ``` 56 | 57 | `translate` 関数は、平行移動のトランスフォーム値(実体は行列)を返します。こうしたトランスフォーム値を返す関数は他に `scale` や `rotate` などがありますが、これらは `mat2d/*` 関数で組み合わせることが出来ます。 58 | 59 | ```cljs 60 | (transform (mat2d/* (translate [20 10]) 61 | (rotate (deg 20)) 62 | (scale [1.5 1])) 63 | (style (fill "gold") 64 | (rect [0 0 50 40]))) 65 | ``` 66 | 67 | `rotate` はラジアン角を引数に取るので、`deg` 関数で「度」からラジアンへの変換をしています。 68 | 69 | 細かい話は[トランスフォーム](transform)を参照してください。 70 | 71 | ## インスペクタとハンドル 72 | 73 | これまでのスケッチ例の**Open in Editor**からエディタを開くことができます。Glisp ではテキストとしてのコードを直接編集することもできますが、ビューワーや画面左下のインスペクタからも値を調整することができます。対応している関数であれば、その括弧の内側を選択すると自動的にハンドルやインスペクタが表示されます。 74 | 75 | >) 76 | -------------------------------------------------------------------------------- /src/components/use/use-mouse-events.ts: -------------------------------------------------------------------------------- 1 | import {Ref, onUnmounted, ref, onMounted, unref} from '@vue/composition-api' 2 | import {getHTMLElement} from '@/utils' 3 | 4 | export default function useMouseEvents( 5 | target: Ref | HTMLElement, 6 | { 7 | ignorePredicate, 8 | onMove, 9 | onDown, 10 | onDrag, 11 | onUp, 12 | }: { 13 | ignorePredicate?: (e: MouseEvent) => boolean 14 | onMove?: (e: MouseEvent) => any 15 | onDown?: (e: MouseEvent) => any 16 | onDrag?: (e: MouseEvent) => any 17 | onUp?: (e: MouseEvent) => any 18 | } 19 | ) { 20 | const mouseX = ref(0) 21 | const mouseY = ref(0) 22 | const mousePressed = ref(false) 23 | 24 | let targetEl: HTMLElement | undefined 25 | 26 | function onMouseMove(e: MouseEvent) { 27 | mouseX.value = e.pageX 28 | mouseY.value = e.pageY 29 | if (!mousePressed.value && onMove) { 30 | onMove(e) 31 | } else if (mousePressed.value && onDrag) { 32 | onDrag(e) 33 | } 34 | } 35 | 36 | function onMouseToggle(e: MouseEvent) { 37 | const pressed = e.type === 'mousedown' 38 | if ( 39 | (pressed && ignorePredicate && ignorePredicate(e)) || 40 | (!pressed && !mousePressed.value) 41 | ) { 42 | return 43 | } 44 | if (e.button === 0) { 45 | mousePressed.value = pressed 46 | if (pressed && onDown) { 47 | onDown(e) 48 | } else if (!pressed && onUp) { 49 | onUp(e) 50 | } 51 | } 52 | } 53 | 54 | onMounted(() => { 55 | targetEl = getHTMLElement(target) 56 | 57 | if (!targetEl) return 58 | targetEl.addEventListener('mousemove', onMouseMove) 59 | targetEl.addEventListener('mousedown', onMouseToggle) 60 | window.addEventListener('mouseup', onMouseToggle) 61 | }) 62 | 63 | onUnmounted(() => { 64 | if (!targetEl) return 65 | targetEl.removeEventListener('mousemove', onMouseMove) 66 | targetEl.removeEventListener('mousedown', onMouseToggle) 67 | window.removeEventListener('mouseup', onMouseToggle) 68 | }) 69 | 70 | return {mouseX, mouseY, mousePressed} 71 | } 72 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-exp-history.ts: -------------------------------------------------------------------------------- 1 | import {MalNode, getName, MalVal, MalError, isKeyword} from '@/mal/types' 2 | 3 | import {Ref, ref} from '@vue/composition-api' 4 | import {NonReactive} from '@/utils' 5 | import {reconstructTree} from '@/mal/reader' 6 | import AppScope from '@/scopes/app' 7 | 8 | type Commit = [NonReactive, Set] 9 | 10 | export default function useExpHistory( 11 | exp: Ref>, 12 | updateExp: (exp: NonReactive, pushHistory?: boolean) => any 13 | ) { 14 | const history: Ref = ref([]) 15 | 16 | function pushExpHistory(newExp: NonReactive, tag?: string) { 17 | history.value.push([newExp, new Set(tag ? [tag] : undefined)]) 18 | } 19 | 20 | function undoExp(tag?: string) { 21 | let index = -1 22 | if (tag) { 23 | for (let i = history.value.length - 2; i >= 0; i--) { 24 | if (history.value[i][1].has(tag)) { 25 | index = i 26 | break 27 | } 28 | } 29 | } else { 30 | if (history.value.length > 2) { 31 | index = history.value.length - 2 32 | } 33 | } 34 | 35 | if (index === -1) { 36 | return false 37 | } 38 | 39 | const [prev] = history.value[index] 40 | history.value.length = index + 1 41 | reconstructTree(prev.value) 42 | updateExp(prev, false) 43 | 44 | return true 45 | } 46 | 47 | AppScope.def('revert-history', (arg: MalVal) => { 48 | if (typeof arg !== 'string') { 49 | return undoExp() 50 | } else { 51 | const tag = getName(arg) 52 | return undoExp(tag) 53 | } 54 | }) 55 | 56 | function tagExpHistory(tag: string) { 57 | if (history.value.length > 0) { 58 | history.value[history.value.length - 1][1].add(tag) 59 | } 60 | } 61 | 62 | AppScope.def('tag-history', (tag: MalVal) => { 63 | if (!(typeof tag === 'string' || isKeyword(tag))) { 64 | throw new MalError('tag is not a string/keyword') 65 | } 66 | tagExpHistory(getName(tag)) 67 | return true 68 | }) 69 | 70 | return {pushExpHistory, tagExpHistory} 71 | } 72 | -------------------------------------------------------------------------------- /src/components/inputs/InputBoolean.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 36 | 37 | 89 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const WorkerPlugin = require('worker-plugin') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const CopyPlugin = require('copy-webpack-plugin') 5 | 6 | module.exports = { 7 | publicPath: './', 8 | productionSourceMap: false, 9 | devServer: { 10 | writeToDisk: true, 11 | }, 12 | filenameHashing: false, 13 | configureWebpack: { 14 | plugins: [ 15 | new WorkerPlugin(), 16 | new webpack.ProvidePlugin({ 17 | $: 'jquery', 18 | jQuery: 'jquery', 19 | 'window.jQuery': 'jquery', 20 | }), 21 | ], 22 | output: { 23 | globalObject: 'globalThis', 24 | filename: '[name].js', 25 | }, 26 | entry: { 27 | 'lib/core': path.join(__dirname, 'src/mal-lib/core.ts'), 28 | 'lib/color': path.join(__dirname, 'src/mal-lib/color.ts'), 29 | 'lib/path': path.join(__dirname, 'src/mal-lib/path.ts'), 30 | 'lib/math': path.join(__dirname, 'src/mal-lib/math.ts'), 31 | 'js/generator': path.join(__dirname, 'src/generator.ts'), 32 | }, 33 | node: { 34 | __dirname: false, 35 | }, 36 | }, 37 | css: { 38 | loaderOptions: { 39 | css: { 40 | url: false, 41 | }, 42 | }, 43 | }, 44 | chainWebpack: config => { 45 | config.plugin('html-js/index').tap(args => { 46 | args[0].hash = true 47 | args[0].minify = { 48 | removeComments: false, 49 | collapseWhitespace: false, 50 | removeAttributeQuotes: false, 51 | collapseBooleanAttributes: false, 52 | removeScriptTypeAttributes: false, 53 | } 54 | return args 55 | }) 56 | 57 | // Copy logo.png to dist 58 | config.plugin('copy-assets').use(CopyPlugin, [ 59 | { 60 | patterns: [ 61 | { 62 | from: 'assets/logo.png', 63 | to: '.', 64 | }, 65 | ], 66 | }, 67 | ]) 68 | }, 69 | pages: { 70 | 'js/index': { 71 | entry: 'src/pages/index.ts', 72 | template: 'public/index.html', 73 | filename: 'index.html', 74 | }, 75 | 'js/embed': { 76 | entry: 'src/pages/embed.ts', 77 | template: 'public/embed.html', 78 | filename: 'embed.html', 79 | }, 80 | }, 81 | } 82 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputString.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 17 | 18 | 19 | 20 | 73 | 74 | 78 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputAngle.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 18 | 19 | 20 | 21 | 67 | 68 | 79 | -------------------------------------------------------------------------------- /src/components/inputs/InputNumber.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 89 | 90 | 97 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # Glisp とは? 2 | 3 |  4 | 5 | 一言でいうと、**めちゃめちゃ柔軟で色んな使い方ができる次世代のデザインツール**を探求するプロジェクトです。[橋本麦](https://baku89.com)を中心に、[コントリビューター](https://github.com/baku89/glisp/graphs/contributors)、[スポンサー](https://github.com/sponsors/baku89?o=sd&sc=t)の皆さんの協力の元、オープンソースとして開発を進めています。 6 | 7 | 現状はまだプロトタイプに留まっていますが、ゆくゆくは誰でも使えるベクターグラフィックスツールに発展させていくつもりです。作者が映像作家なのでモーショングラフィックス機能も加えられたら嬉しいです。 8 | 9 | ## Graphical Lisp 10 | 11 | Glisp は、その名の通り多くの機能が **Lisp** というプログラミング言語を基盤に構築されています。詳細は[他の章](why-lisp)に譲りますが、 Lisp は**それ自身がプログラムでありながらデータでもある**という性質を持っています。ですから、Glisp ではこの Lisp を、スクリプトに限らず、プロジェクトファイル、プラグイン、アプリケーション設定まで様々なデータ表現に用いることができます。そしてそれらは同時にプログラムそのものでもありますから、それぞれの機能をプログラミングと同等の柔軟さで改造できることを意味します。 12 | 13 | こうした設計によって、Illustrator のような GUI ツールが持ち合わせる「直感的にちまちま手数を重ねる」やり方と、プログラミングが可能にする柔軟さ・自己改造性、そしてジェネレーティブ表現とのいいとこ取りができます。 14 | 15 | ## 機能のための機能 16 | 17 | Glisp のもう一つ大切な設計思想が、機能そのものではなく**機能をつくるための機能**を提供するという原則です。ベクターグラフィックひとつとっても、どのアプリも具体的なジャンルや使途を想定して設計されがちです。Adobe 製品を例に挙げると、 18 | 19 | - **InDesign**: 製本 20 | - **Illustrator**: ポスターや図、イラスト 21 | - **XD**: UI/UX デザイン 22 | 23 | といったように。こうした特殊化は、想定された使い方の範疇では最大の効率を発揮しますが、ひとたび妙な表現を試そうとしたとき、やけに面倒くさい操作が必要になることがあります。その上、用途に合わせて使い分けする手間もかかります。 24 | 25 | 一方、Glisp のアプローチは、とにかく「**誰にどういう使い方をされるかを想定しない**」ということに尽きます。アーティストが描きたいのは抽象画かもしれないし、スマホアプリのモックアップ、はたまた間取り図かもしれない。Glisp はそれぞれの使途に合わせて具体的な機能を提供する代わりに、アーティスト自身がその目的に見合った機能を実装するための抽象化された機能を提供します。その一例が、丸や四角、多角形といったプリミティブを自由に定義できる機能です。下の例ではニコちゃんマークを定義しています。 26 | 27 |  28 | 29 | このように、好きなように改造を重ねることで、やがてその使い手のクセや目的や合わせて機能が「**分化**」していくのが Glisp の大きな特徴です。 30 | 31 | ## 展望 32 | 33 | このプロジェクトの究極的な目標は、世の中のデザインツールを(作者の感覚で)マシにすることです。もちろん Glisp それ自体も便利なツールとして、世界中の制作者に使ってもらえるものになってほしいと願っています。ただ作者はツール開発が専門ではないので、こうしたツール設計の良さがみんなに理解してもらえて、 Adobe でも Figma でも Maxon でも、普段お世話になっているソフトウェアベンダーに自由にパクってもらえればそれで良いとも考えています。そして既製ツールの方が良くなってくれて、それを使ってストレスなく自分自身の制作に集中できれば最高です。 34 | 35 | ※ 本音を言ってしまうと、作者とコントリビューターの方々の分だけでもマネタイズする方法は探りたいです。この開発にもそれなりに時間が掛かってしまっているのと、クライアントワークはあまり向いてないので…。試験的に[GitHub Sponsors](https://github.com/sponsors/baku89?o=sd&sc=t)に登録していますので、よろしければご支援お願いします。 36 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import Case from 'case' 2 | import {MalVal, isKeyword} from '@/mal/types' 3 | import printExp from '@/mal/printer' 4 | import {Ref, unref} from '@vue/composition-api' 5 | 6 | export function replaceRange( 7 | s: string, 8 | start: number, 9 | end: number, 10 | substitute: string 11 | ): [string, number, number] { 12 | return [ 13 | s.substring(0, start) + substitute + s.substring(end), 14 | start, 15 | end + (substitute.length - (end - start)), 16 | ] 17 | } 18 | 19 | export function clamp(value: number, min: number, max: number) { 20 | return Math.max(min, Math.min(value, max)) 21 | } 22 | 23 | /** 24 | * Converts the bind expression to parameter's label 25 | * @param exp A bind expression 26 | */ 27 | export function getParamLabel(exp: MalVal) { 28 | const str = isKeyword(exp) ? exp.slice(1) : printExp(exp) 29 | return str.length === 1 ? str : Case.capital(str) 30 | } 31 | 32 | /** 33 | * The utility class holds a value which does not need to be watched by Vue 34 | */ 35 | const ValueSymbol = Symbol('NonReactive.value') 36 | // let counter = 0 37 | export class NonReactive { 38 | // private id: number 39 | 40 | constructor(value: T) { 41 | // this.id = counter++ 42 | ;(this as any)[ValueSymbol] = value 43 | } 44 | 45 | public get value(): T { 46 | return (this as any)[ValueSymbol] 47 | } 48 | } 49 | 50 | /** 51 | * Creates NonReactive object 52 | */ 53 | export function nonReactive(value: T): NonReactive { 54 | return new NonReactive(value) 55 | } 56 | 57 | export function partition(n: number, coll: any[]) { 58 | const ret = [] 59 | 60 | for (let i = 0; i < coll.length; i += n) { 61 | ret.push(coll.slice(i, i + n)) 62 | } 63 | return ret 64 | } 65 | 66 | export function delay(ms: number) { 67 | return new Promise(resolve => setTimeout(resolve, ms)) 68 | } 69 | 70 | export function getHTMLElement( 71 | el: Ref | HTMLElement 72 | ): HTMLElement | undefined { 73 | const _el = unref(el) 74 | return _el instanceof HTMLElement 75 | ? _el 76 | : _el instanceof Object && _el.$el instanceof HTMLElement 77 | ? _el.$el 78 | : undefined 79 | } 80 | -------------------------------------------------------------------------------- /src/components/inputs/InputString.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 19 | 20 | 80 | 81 | 98 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-url-parser.ts: -------------------------------------------------------------------------------- 1 | import {MalError, MalNode, isNode} from '@/mal/types' 2 | import {nonReactive, NonReactive} from '@/utils' 3 | import {readStr} from '@/mal' 4 | import {toSketchCode} from '../utils' 5 | 6 | export default function useURLParser( 7 | onLoadExp: (exp: NonReactive) => void 8 | ) { 9 | // URL 10 | const url = new URL(location.href) 11 | 12 | if (url.searchParams.has('clear')) { 13 | localStorage.removeItem('saved_code') 14 | localStorage.removeItem('settings') 15 | url.searchParams.delete('clear') 16 | history.pushState({}, document.title, url.pathname + url.search) 17 | } 18 | 19 | // Load initial codes 20 | const loadCodePromise = (async () => { 21 | let code = '' 22 | 23 | const queryCodeURL = url.searchParams.get('code_url') 24 | const queryCode = url.searchParams.get('code') 25 | 26 | if (queryCodeURL) { 27 | const codeURL = decodeURI(queryCodeURL) 28 | url.searchParams.delete('code_url') 29 | url.searchParams.delete('code') 30 | 31 | const res = await fetch(codeURL) 32 | if (res.ok) { 33 | code = await res.text() 34 | 35 | if (codeURL.startsWith('http')) { 36 | code = `;; Loaded from "${codeURL}"\n\n${code}` 37 | } 38 | } else { 39 | new MalError(`Failed to load from "${codeURL}"`) 40 | } 41 | 42 | history.pushState({}, document.title, url.pathname + url.search) 43 | } else if (queryCode) { 44 | code = decodeURI(queryCode) 45 | url.searchParams.delete('code') 46 | history.pushState({}, document.title, url.pathname + url.search) 47 | } else { 48 | code = 49 | localStorage.getItem('saved_code') || 50 | require('raw-loader!@/default-canvas.glisp').default // eslint-disable-line @typescript-eslint/no-var-requires 51 | } 52 | 53 | return code 54 | })() 55 | 56 | let onSetupConsole 57 | const setupConsolePromise = new Promise(resolve => { 58 | onSetupConsole = () => { 59 | resolve() 60 | } 61 | }) 62 | 63 | Promise.all([loadCodePromise, setupConsolePromise]).then(([code]) => { 64 | const exp = readStr(toSketchCode(code as string)) 65 | if (isNode(exp)) { 66 | onLoadExp(nonReactive(exp)) 67 | } 68 | }) 69 | 70 | return {onSetupConsole} 71 | } 72 | -------------------------------------------------------------------------------- /task.deploy.js: -------------------------------------------------------------------------------- 1 | const FtpDeploy = require('ftp-deploy') 2 | const {execSync} = require('child_process') 3 | const fetch = require('node-fetch') 4 | const argv = require('yargs').argv 5 | const fs = require('fs') 6 | const FtpInfo = require('./ftp.info.js') 7 | 8 | const siteURL = 'https://glisp.app' 9 | 10 | const gitHash = execSync('git rev-parse HEAD').toString().trim().slice(0, 7) 11 | 12 | async function upload() { 13 | // Upload to the subdirectory with git hash 14 | if (argv.commit) { 15 | await deploy('commit') 16 | } 17 | 18 | if (argv.docs) { 19 | await deploy('doc') 20 | } 21 | } 22 | upload() 23 | 24 | async function deploy(mode) { 25 | const ftpDeploy = new FtpDeploy() 26 | 27 | const urlSuffix = mode === 'commit' ? `commit:${gitHash}` : 'docs' 28 | const remoteRoot = `${FtpInfo.remoteRoot}/${urlSuffix}` 29 | const localRoot = `${__dirname}/${mode === 'commit' ? 'dist' : 'docs'}/` 30 | const publishedURL = `${siteURL}/${urlSuffix}` 31 | 32 | console.log(`Start Uploading: ${publishedURL}`) 33 | 34 | const config = { 35 | ...FtpInfo, 36 | localRoot, 37 | remoteRoot, 38 | include: ['*', '**/*', '.htaccess'], 39 | exclude: ['**/*.map', 'node_modules/**', 'node_modules/**/.*', '.git/**'], 40 | // delete ALL existing files at destination before uploading, if true 41 | deleteRemote: true, 42 | // Passive mode is forced (EPSV command is not sent) 43 | forcePasv: true, 44 | } 45 | 46 | ftpDeploy.on('uploading', function (data) { 47 | console.log(`[${urlSuffix}]`, 'Uploading...', data.filename) 48 | }) 49 | 50 | // Upload 51 | try { 52 | await ftpDeploy.deploy(config) 53 | console.log(`Uploaded: ${publishedURL}`) 54 | } catch (err) { 55 | console.error(err) 56 | } 57 | 58 | // Update Commits.json 59 | if (mode === 'commit') { 60 | const res = await fetch(`${siteURL}/commits.json`) 61 | const commits = await res.json() 62 | if (commits.length === 0 || commits[commits.length - 1][0] !== gitHash) { 63 | commits.push([gitHash, Date.now()]) 64 | } 65 | fs.writeFileSync(`${localRoot}/commits.json`, JSON.stringify(commits)) 66 | 67 | try { 68 | await ftpDeploy.deploy({ 69 | ...FtpInfo, 70 | localRoot, 71 | remoteRoot: FtpInfo.remoteRoot, 72 | include: ['commits.json'], 73 | forcePasv: true, 74 | }) 75 | console.log('Updated commits.json') 76 | } catch (err) { 77 | console.error(err) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /public/lib/schema-study.glisp: -------------------------------------------------------------------------------- 1 | ;; 静的なパラメーター 2 | 3 | ;; 基本構造 4 | {;; 共通 5 | :type "number|string|keyword|symbol|boolean|map|vector|vec2|rect2d|mat2d|path|color" 6 | :ui "angle|seed|dropdown" 7 | :label "Label" 8 | :validator validator-fn ;; value -> modified-value or nil (invalid) 9 | 10 | ;; UI Specific options 11 | 12 | ;;slider 13 | :range [0 1] 14 | 15 | ;; dropdown 16 | :values [0 1 2] 17 | :labels ["Zero" "One" "Two"]} 18 | 19 | 20 | ;; Function Application (List) 21 | {:type "list" 22 | :fn 'circle 23 | :items [;; :type === :ui の時省略可能 24 | {:label "Center" :type "vec2" :ui "vec2"} 25 | {:label "Radius" :type "number" :ui "number"}]} 26 | 27 | (circle [0 0] 100) 28 | 29 | ;; Nested parameter 30 | {:type "list" 31 | :fn 'interpolate-linear 32 | :item [{:type "number" :label "t"} 33 | {:type "vector" 34 | :items [{:type "number" :label "Time"} 35 | {:type "number" :label "Value"}]}]} 36 | 37 | (interpolate-linear .1 [0.0 10 38 | 0.5 100 39 | 1.0 -20]) 40 | 41 | ;; Rest parameter 42 | {:type "list" 43 | :fn '+ 44 | :item [{:type "vector" 45 | :variadic true 46 | :items {:type "number"}}]} 47 | 48 | (+ 1 2 3 4 5) 49 | 50 | ;; Map 51 | {:type "map" 52 | :items [{:key "width" :label "Width" ...} 53 | {:key "height" :label "Height" ...}]} 54 | 55 | {:width 10 :height 20} 56 | 57 | ;; Vector 58 | {:type "vector" 59 | :items {:type "number"}} 60 | 61 | [0 1 2 3 4] 62 | 63 | {:type "vector" 64 | :items [{:type "number"} 65 | {:type "string"}]} 66 | 67 | [10 "A" 20 "B" 30 "C"] 68 | 69 | 70 | ;; Vector (default) 71 | {:type "vector" 72 | :items {:type "any"}} 73 | 74 | ;; Dynamic 75 | 76 | ;; Example: range 77 | {:type "dynamic" 78 | :items {:type "any"} 79 | :to-schema (fn {:params xs} 80 | (let [[start end step] (case (count xs) 81 | 1 [0 (first xs) 1] 82 | 2 [(start xs) (second xs) 1] 83 | 3 xs)] 84 | [{:label "Start" :type "number" :value start :default 0} 85 | {:label "End" :type "number" :value end} 86 | {:label "Step" :type "number" :value step :default 1}])) 87 | :to-value (fn {:value [start end step]} 88 | (cond 89 | (and (= 0 start) (= 1 step)) [end] 90 | (= 1 step) [start end] 91 | [start end step]))} 92 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputParam.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ scheme.label }} 6 | 7 | 15 | 16 | 17 | 18 | 19 | 77 | 78 | 96 | -------------------------------------------------------------------------------- /docs/styles.md: -------------------------------------------------------------------------------- 1 | # スタイルの応用 2 | 3 | [はじめてのスケッチ](get-started)でも解説したとおり、エレメントはスタイルを適用することに初めて描画されます。スタイルは「色」や「線幅」など様々なプロパティからなりますが、それらはあるルールに基づいて継承、描画され、また参照することができます。 4 | 5 | ## プロパティの参照 6 | 7 | `style` 関数の内側では、適用されているスタイルのプロパティにアクセスすることができます。 8 | 9 | ```cljs 10 | (style (fill "pink") 11 | (text *fill-color* [50 50] :size 24)) 12 | ``` 13 | 14 | ここでは、外側の `fill` 関数で設定された `"pink"` を、シンボル `*fill-color*` によって参照しています。(「\*」で挟まれた変数名は、グローバル変数のようなシンボルを表しています。)同様に`*stroke-color*` や、 `*stroke-width*`、`*stroke-cap*` で線のプロパティにアクセスすることもできます。これはフローチャートなど、縁取りと同じ色でテキストを塗るときにも便利です。 15 | 16 | ```cljs 17 | (style (stroke "royalblue" 2) 18 | (ngon [50 50] 40 4) 19 | 20 | (style [(no-stroke) (fill *stroke-color*)] 21 | (text "Hello" [50 50]))) 22 | ``` 23 | 24 | ここで `no-stroke` 関数が登場していますが、これによって、それ以前に適用された塗りを無効にすることができます。以下のようなイメージです。 25 | 26 | ```clojure 27 | (stroke "royalblue" 2) -> (no-stroke) -> (fill *stroke-color*) 28 | ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ 29 | no-stroke によって打ち消される 打ち消されてもなお *stroke-color* は "royalblue" に設定されている 30 | ``` 31 | 32 | こうすることで、Hello というテキストに線が適用され、太くなってしまうのを防ぎます。これは矢印を描くときにも使えます。 33 | 34 | ```cljs 35 | (style (stroke "pink" 2) 36 | (line [10 50] [80 50]) 37 | 38 | (style [(no-stroke) (fill *stroke-color*)] 39 | (ngon [80 50] 10 3))) 40 | ``` 41 | 42 | ## スタイルの実体 43 | 44 | `fill` 関数も `stroke` 関数も、実際には CSS のようなプロパティからなる連想配列を返します。 45 | 46 | ```clojure 47 | (fill "pink") -> {:fill true :fill-color "pink"} 48 | 49 | (stroke "skyblue" 10) -> {:stroke true :stroke-color "skyblue" :stroke-width 10} 50 | ``` 51 | 52 | ですから、もちろん`style` 関数に直接これらのマップを渡すことも可能です。 53 | 54 | CSS と違うのは、「スタイルを有効にするかどうか」と、そのスタイルへの色指定が別のプロパティになっている点です。例えば CSS で`fill: "pink";`を設定することで塗りを有効にすると同時に塗りの色も指定していますが、Glisp ではそれぞれ `:fill` 、 `:fill-color` という独立したプロパティとなっています。この仕組みによって、`style` 関数の内側のパス全てに描画を与えることなく、スタイルの色のデフォルト値を指定することが出来ます。 55 | 56 | ```cljs 57 | (style {:fill-color "royalblue"} 58 | 59 | (style (fill) 60 | (text "Hello" [50 50])) 61 | 62 | (style (stroke "pink" 2) 63 | (circle [50 50] 40))) 64 | ``` 65 | 66 | 色に限らず、プロパティはそれ自身が描画されずとも内側のエレメントへと継承されていきます。同じプロパティが設定された場合は、CSS 同様に内側のプロパティが優先されます。 67 | 68 | ```cljs 69 | (style {:stroke-dash [15 4]} 70 | (style (stroke "crimson" 2) 71 | (circle [50 50] 40)) 72 | 73 | (style (stroke "plum" 2 :dash [4 4]) 74 | (ngon [50 50] 40 5))) 75 | ``` 76 | 77 | ## スタイルのプリセット化 78 | 79 | スタイルは単なるマップなので、変数として定義することでプリセットのように随所で使い回すことができます。もちろん、他のスタイルと組み合わせて適用することも可能です。 80 | 81 | ```cljs 82 | (def fill-skyblue (fill "skyblue")) 83 | 84 | (style fill-skyblue 85 | (rect [10 10 40 40])) 86 | 87 | (style [fill-skyblue (stroke "crimson" 5)] 88 | (circle [70 70] 20)) 89 | 90 | ``` 91 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 38 | 42 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 59 | 63 | 64 | 68 | 72 | 73 | 77 | (glisp) 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /public/lib/ui.glisp: -------------------------------------------------------------------------------- 1 | ;; Graphics and UI 2 | (def *sketch* "") 3 | (def *width* 0) 4 | (def *height* 0) 5 | (def *size* [0 0]) 6 | (def *background* nil) 7 | (def *guide-color* nil) 8 | (def *transform* [1 0 0 1 0 0]) 9 | 10 | (def *inside-artboard* false) 11 | (def *fill-color* nil) 12 | (def *stroke-color* nil) 13 | (def *stroke-width* 1) 14 | (def *stroke-cap* "round") 15 | (def *stroke-join* "round") 16 | 17 | (def *modes* (atom [])) 18 | 19 | (defn defmode [mode-name handlers] 20 | (swap! *modes* conj {:name (name mode-name) :handlers handlers})) 21 | 22 | ;; Atoms 23 | (def *app-background* (atom nil)) 24 | (def *time-variables* (atom [])) 25 | 26 | ;; Sketch 27 | ;; (defn normalize-element 28 | ;; {:private true} 29 | ;; [el] 30 | ;; (let [tag (first el) content (rest el)] 31 | ;; (if (keyword? tag) 32 | ;; (cond (= tag :g) 33 | ;; (let [has-attrs (map? (first content)) 34 | ;; attrs (if has-attrs (first content) {}) 35 | ;; body (->> (if has-attrs (rest content) content) 36 | ;; (map #(if (list? %) % [%])) 37 | ;; (apply concat) 38 | ;; (map normalize-element) 39 | ;; (remove nil?))] 40 | ;; (vec `(~tag ~attrs ~@body))) 41 | 42 | ;; :else 43 | ;; el)))) 44 | 45 | (defmacro sketch [& body] 46 | (reset! *time-variables* []) 47 | (let [evaluated-body (eval* body) 48 | start-line-num (inc (last-index-of evaluated-body :start-sketch)) 49 | sketch-body (filter element? (drop start-line-num evaluated-body))] 50 | `[:g :_ ~@sketch-body])) 51 | 52 | 53 | (defmacro sketch-at-time [sym time & body] 54 | `(let [deftime (fn [] nil) 55 | ~(symbol sym) ~time] 56 | (sketch ~@body))) 57 | 58 | ;; `(eval (read-string (format "(do (defn deftime [] nil) (def %s %f) (sketch %s\nnil))" ~sym ~time sketch)))) 59 | 60 | ;; Pens and Hands 61 | ;; (defmacro begin-draw 62 | ;; {:private true} 63 | ;; [state] 64 | ;; `(def ~state nil)) 65 | 66 | ;; (defmacro draw 67 | ;; {:private true} 68 | ;; [f state input] 69 | ;; `(do 70 | ;; (def __ret__ (~f ~state ~input)) 71 | ;; (def ~state (if (first __ret__) __ret__ (concat (list (first ~state)) (rest __ret__)))) 72 | ;; (first __ret__))) 73 | 74 | ;; (def $pens []) 75 | ;; (def $hands []) 76 | 77 | ;; (defmacro defpen 78 | ;; {:private true} 79 | ;; [name params body] 80 | ;; `(do 81 | ;; (def ~name (fn ~params ~body)) 82 | ;; (def $pens (conj $pens '~name)))) 83 | 84 | ;; (defmacro defhand 85 | ;; {:private true} 86 | ;; [name params body] 87 | ;; `(do 88 | ;; (def ~name (fn ~params ~body)) 89 | ;; (def $hands (conj $hands '~name)))) 90 | -------------------------------------------------------------------------------- /src/components/inputs/InputColor.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 45 | 46 | 110 | -------------------------------------------------------------------------------- /src/components/inputs/InputTranslate.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 86 | 87 | 123 | -------------------------------------------------------------------------------- /src/components/use/use-draggable.ts: -------------------------------------------------------------------------------- 1 | import {onBeforeUnmount, Ref, reactive, onMounted} from '@vue/composition-api' 2 | 3 | interface DragData { 4 | x: number 5 | y: number 6 | deltaX: number 7 | deltaY: number 8 | isMousedown: boolean 9 | isDragging: boolean 10 | prevX: number 11 | prevY: number 12 | } 13 | 14 | interface DraggableOptions { 15 | coordinate?: 'center' 16 | onClick?: () => void 17 | onDrag?: (drag: DragData) => void 18 | onDragStart?: (drag: DragData) => void 19 | onDragEnd?: (drag?: DragData) => void 20 | } 21 | 22 | export default function useDraggable( 23 | el: Ref, 24 | options: DraggableOptions = {} 25 | ) { 26 | const drag = reactive({ 27 | x: 0, 28 | y: 0, 29 | deltaX: 0, 30 | deltaY: 0, 31 | isMousedown: false, 32 | isDragging: false, 33 | prevX: 0, 34 | prevY: 0, 35 | }) 36 | 37 | let originX = 0, 38 | originY = 0, 39 | prevX = 0, 40 | prevY = 0 41 | 42 | function onMousedrag(e: MouseEvent) { 43 | const {clientX, clientY} = e 44 | 45 | drag.x = clientX - originX 46 | drag.y = clientY - originY 47 | drag.deltaX = drag.x - prevX 48 | drag.deltaY = drag.y - prevY 49 | 50 | if (options.onDragStart && !drag.isDragging) { 51 | options.onDragStart(drag) 52 | } 53 | 54 | if (Math.abs(drag.x) > 2 || Math.abs(drag.y) > 2) { 55 | drag.isDragging = true 56 | } 57 | 58 | if (options.onDrag) { 59 | options.onDrag(drag) 60 | } 61 | 62 | drag.prevX = prevX = drag.x 63 | drag.prevY = prevY = drag.y 64 | } 65 | 66 | function onMouseup() { 67 | if (!drag.isDragging && options.onClick) { 68 | options.onClick() 69 | } 70 | 71 | if (drag.isDragging && options.onDragEnd) { 72 | options.onDragEnd(drag) 73 | } 74 | 75 | drag.isMousedown = false 76 | drag.isDragging = false 77 | drag.x = 0 78 | drag.y = 0 79 | drag.deltaX = 0 80 | drag.deltaY = 0 81 | originX = 0 82 | originY = 0 83 | window.removeEventListener('mousemove', onMousedrag) 84 | window.removeEventListener('mouseup', onMouseup) 85 | } 86 | 87 | function onMousedown(e: MouseEvent) { 88 | const {clientX, clientY} = e 89 | 90 | drag.isMousedown = true 91 | if (options.coordinate === 'center' && el.value) { 92 | const {left, top, width, height} = el.value.getBoundingClientRect() 93 | originX = left + width / 2 94 | originY = top + height / 2 95 | } else { 96 | originX = clientX 97 | originY = clientY 98 | } 99 | drag.prevX = prevX = 0 100 | drag.prevY = prevY = 0 101 | 102 | window.addEventListener('mousemove', onMousedrag) 103 | window.addEventListener('mouseup', onMouseup) 104 | } 105 | 106 | onMounted(() => { 107 | if (!el.value) return 108 | el.value.addEventListener('mousedown', onMousedown) 109 | }) 110 | 111 | onBeforeUnmount(onMouseup) 112 | 113 | return drag 114 | } 115 | -------------------------------------------------------------------------------- /public/lib/color.glisp: -------------------------------------------------------------------------------- 1 | (import "math.glisp") 2 | (import-js-force "color.js") 3 | 4 | ;; Color 5 | (defn color? [x] 6 | (string? x)) 7 | 8 | (defn color/gray 9 | {:return {:type "string" :ui "color"}} 10 | [v] 11 | (def b (* v 255)) 12 | (format "rgb(%d,%d,%d)" b b b)) 13 | 14 | (defn color/rgb 15 | {:params [{:label "Red" :type "number" :validator clamp01} 16 | {:label "Green" :type "number" :validator clamp01} 17 | {:label "Blue" :type "number" :validator clamp01} 18 | {:label "Alpha" :type "number" :validator clamp01 :default 1}] 19 | :return {:type "string" :ui "color"}} 20 | [r g b & a] 21 | (if (zero? (count a)) 22 | (format "rgb(%d,%d,%d)" (* r 255) (* g 255) (* b 255)) 23 | (format "rgba(%d,%d,%d,%d)" (* r 255) (* g 255) (* b 255) (first a)))) 24 | (defalias rgb color/rgb) 25 | 26 | (defn color/hsl 27 | {:params [{:label "Hue" :type "number" :validator #(clamp 0 TWO_PI %)} 28 | {:label "Saturation" :type "number" :validator clamp01} 29 | {:label "Lightness" :type "number" :validator clamp01} 30 | {:label "Alpha" :type "number" :validator clamp01 :default 1}] 31 | :return {:type "string" :ui "color"}} 32 | [h s l & a] 33 | (if (zero? (count a)) 34 | (format "hsl(%d,%s,%s)" 35 | (mod (to-deg h) 360) 36 | (str (* s 100) "%") 37 | (str (* l 100) "%")) 38 | (format "hsla(%d,%s,%s,%f)" 39 | (mod (to-deg h) 360) 40 | (str (* s 100) "%") 41 | (str (* l 100) "%") 42 | (first a)))) 43 | (defalias hsl color/hsl) 44 | 45 | (annotate-fn! 46 | color/mix 47 | {:doc "Mixes two colors" 48 | :params [{:label "Color1" :type "string" :ui "color"} 49 | {:label "Color2" :type "string" :ui "color"} 50 | {:label "Ratio" :type "number" :ui "slider" 51 | :min 0 :max 1 :default 0.5 :validator clamp01} 52 | {:label "Mode" :type "string" :ui "dropdown" :default "lrgb" 53 | :values ["lrgb" "rgb" "hsl"]}] 54 | :return {:type "string" :ui "color"}}) 55 | 56 | (annotate-fn! 57 | color/brighten 58 | {:doc "Brightens the color" 59 | :params [{:label "Color" :type "string" :ui "color"} 60 | {:label "Value" :type "number" :default 1}] 61 | :return {:type "string" :ui "color"}}) 62 | 63 | (annotate-fn! 64 | color/darken 65 | {:doc "Darkens the color" 66 | :params [{:label "Color" :type "string" :ui "color"} 67 | {:label "Value" :type "number" :default 1}] 68 | :return {:type "string" :ui "color"}}) 69 | 70 | (annotate-fn! 71 | color/invert 72 | {:doc "Inverts the color" 73 | :params [{:label "Color" :type "string" :ui "color"} 74 | {:label "mode" :type "string" :ui "dropdown" :default "rgb" 75 | :values ["rgb" "hsl"]}] 76 | :inverse (fn {:return ret :params params} 77 | [(apply color/invert (replace-nth params 0 ret))])}) -------------------------------------------------------------------------------- /docs/en/draw-tree.md: -------------------------------------------------------------------------------- 1 | # Draw Tree 2 | 3 | When evaluated, a Glisp sketch returns what we call a "draw tree". For example, 4 | 5 | ```cljs 6 | (style (fill "darkblue") 7 | (rect [10 10 80 80])) 8 | ``` 9 | 10 | is evaluated as follows. 11 | 12 | ```clojure 13 | [:g :_ 14 | [:style {:fill true :fill-color "darkblue"} 15 | [:path :M [10 10] :L [90 10] :L [90 90] :L [10 90] :Z]]] 16 | ``` 17 | 18 | Unlike a sketch, a draw tree is a static data that does not include function calls and only describes the final graphic output. It is similar to a structure of SVG format. To render on the viewport or to output to various formats, such as PNG and SVG, the the draw tree is passed to the renderer under the hood. 19 | 20 | Most of the graphics-related functions such as `rect` and `style` return a draw tree. Of course, a draw tree can be directly written on the sketch; however, the context of the editing information (for example, a circle is at a certain point with a certain diameter) is omitted. The relationship between an edit file and an outlined file in Illustrator is an analogy of a Glisp sketch and a draw tree (although this is an issue with the implementation, the viewport may not function properly when the draw tree is handwritten). 21 | 22 | ## Advantages 23 | 24 | Since a draw tree in Glisp does not contain dynamic data and the structure is simple, the render does not need an evaluator and thus can be simplified ([implementation in Canvas API](https://github.com/baku89/glisp/blob/master/src/renderer/render-to-context.ts)). Also, the project file can contain its evaluated draw tree in addition to the sketch. With this idea, the project file can be used an outlined data for printing, or a viewer app can be easily made for previewing the file. 25 | 26 | ## Revealing the draw tree 27 | 28 | The draw tree of the current sketch can be revealed by typing `*view*` in the console. Or, pressing `Ctrl+E` in the editor unfolds parentheses highlighted yellow by the cursor into an evaluated values. By pressing the command a few times, functions will be gradually replaced by draw trees. 29 | 30 | ## Syntax 31 | 32 | A draw tree consists of a vector whose elements are categorized to ones with and without child elements. 33 | 34 | - **with child elements**: `:g` `:clip` `:transform` `:style` 35 | - **empty elements (without children)**: `:path` `:text` etc. 36 | 37 | All elements with children have the following structure. 38 | 39 | ``` 40 | [:<#ID (optional)> ...] 41 | ``` 42 | 43 | `:path` has the same structure as an SVG [path command](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#Path_commands). Nevertheless, the only available commands are `M` `L` `C` `Z`, which set the absolute position. This is also to keep the renderer implementation simple. 44 | 45 | ```clojure 46 | [:path :M [0 0] ;; moveTo 47 | :L [100 100] ;; lineTo 48 | :C [50 100] [50 50] [20 20] ;; cubicCurveTo 49 | :Z] 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/en/get-started.md: -------------------------------------------------------------------------------- 1 | # First Sketch 2 | 3 | ## Draw a circle 4 | 5 | Here is the simplest sketch: 6 | 7 | ```cljs 8 | (circle [50 50] 40) 9 | ``` 10 | 11 | Function `circle` generates a circle path. `[100 100]` is the center of the circle and `40` for the radius. The origin of Glisp canvas is at the center with the positive direction to the right horizontally and down vertically. 12 | 13 | However, since there is no fill or stroke setting, the path is shown as a dashed line. Let' fill this with a tomato color. 14 | 15 | ```cljs 16 | (style (fill "tomato") 17 | (circle [50 50] 40)) 18 | ``` 19 | 20 | Function `style` applies a style. This function applies the first argument, style `(fill "red")`, to the other arguments, `(circle [50 50] 40)` in this case. Also `fill` is a function that returns style information. 21 | 22 | To add stroke color, next `style`'s. 23 | 24 | ```cljs 25 | (style (fill "RoyalBlue") 26 | (style (stroke "tomato" 10) 27 | (circle [50 50] 40))) 28 | ``` 29 | 30 | Styles are rendered from the outermost ones. In this example, first, the circle is filled with a tomato color and the border is drawn in a blue line with a thickness of 10 pixels. This can be concisely expressed by using a vector as the second argument of `style`. 31 | 32 | ```cljs 33 | (style [(fill "tomato") (stroke "RoyalBlue" 10)] 34 | (circle [50 50] 40)) 35 | ``` 36 | 37 | Style can be applied to a bundle of paths. 38 | 39 | ```cljs 40 | (style (stroke "turquoise" 6) 41 | (circle [50 50] 40) 42 | (rect [20 40 60 20])) 43 | ``` 44 | 45 | Function `rect` returns a path from the top left corner, width and height. 46 | 47 | ## Transform 48 | 49 | Function `transform` sets the transformation parameters of a group's position and angle. 50 | 51 | ```cljs 52 | (transform (translate [30 30]) 53 | (style (fill "gold") 54 | (rect [0 0 60 40]))) 55 | ``` 56 | 57 | Function `translate` returns a transformation value of parallel translation (which actually is a matrix). Other functions return a transformation value include `scale` and `rotate`, and they can be chained by function `mat2d/*`. 58 | 59 | ```cljs 60 | (transform (mat2d/* (translate [20 10]) 61 | (rotate (deg 20)) 62 | (scale [1.5 1])) 63 | (style (fill "gold") 64 | (rect [0 0 50 40]))) 65 | ``` 66 | 67 | As `rotate` takes an argument of radians, here function `deg` converts the unit from degree to radian. 68 | 69 | You can find detailed explanation in [transformation](transform). 70 | 71 | ## Inspector and handles 72 | 73 | In the above examples, you can open the editor by pressing **Open in Editor**. In Glisp, in addition to edit the code as a text, parameters can be adjusted and edited in the viewer and in the inspector at the bottom left of the window. With functions supporting these features, the inspector and handles automatically show up when the inside of the parentheses of the function is selected by the cursor. 74 | 75 |  76 | -------------------------------------------------------------------------------- /src/renderer/canvas-renderer/canvas-renderer.ts: -------------------------------------------------------------------------------- 1 | import {MalVal, keywordFor as K, MalMap} from '@/mal/types' 2 | import {ViewerSettings} from './index' 3 | import renderToContext from '../render-to-context' 4 | 5 | type Canvas = HTMLCanvasElement | OffscreenCanvas 6 | 7 | type CanvasContext = 8 | | CanvasRenderingContext2D 9 | | OffscreenCanvasRenderingContext2D 10 | 11 | export default class CanvasRenderer { 12 | private ctx!: CanvasContext 13 | private dpi!: number 14 | private cachedExp!: MalVal 15 | 16 | constructor(private canvas: Canvas) { 17 | const ctx = this.canvas.getContext('2d') 18 | 19 | if (ctx) { 20 | this.ctx = ctx 21 | } else { 22 | throw new Error('Cannot initialize rendering context') 23 | } 24 | } 25 | 26 | public async resize(width: number, height: number, dpi: number) { 27 | this.dpi = dpi 28 | this.canvas.width = width * dpi 29 | this.canvas.height = height * dpi 30 | } 31 | 32 | public async render(exp: MalVal | undefined, settings: ViewerSettings) { 33 | if (!this.dpi) { 34 | throw new Error('trying to render before settings resolution') 35 | } 36 | 37 | // Use cached expression 38 | if (exp === undefined) { 39 | if (!this.cachedExp) { 40 | throw new Error('Cannot render because there iss no cached exp') 41 | } 42 | exp = this.cachedExp 43 | } else { 44 | this.cachedExp = exp 45 | } 46 | 47 | const ctx = this.ctx 48 | 49 | ctx.resetTransform() 50 | 51 | const w = ctx.canvas.width 52 | const h = ctx.canvas.height 53 | ctx.clearRect(0, 0, w, h) 54 | 55 | ctx.scale(this.dpi, this.dpi) 56 | 57 | // Apply view transform 58 | if (settings.viewTransform) { 59 | const m = settings.viewTransform 60 | ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]) 61 | } 62 | 63 | // default style 64 | const defaultStyle: MalMap | null = settings.guideColor 65 | ? { 66 | [K('stroke')]: true, 67 | [K('stroke-color')]: settings.guideColor, 68 | [K('stroke-width')]: 1, 69 | [K('stroke-dash')]: [2, 4], 70 | } 71 | : null 72 | 73 | // Start drawing 74 | return renderToContext(this.ctx, exp, defaultStyle) 75 | } 76 | 77 | public async getImage({format = 'png'} = {}) { 78 | let blob: Blob 79 | 80 | const imageType = `image/${format}` 81 | 82 | if (this.canvas instanceof OffscreenCanvas) { 83 | blob = await this.canvas.convertToBlob({type: imageType}) 84 | } else { 85 | blob = await new Promise((resolve, reject) => { 86 | ;(this.canvas as HTMLCanvasElement).toBlob(blob => { 87 | blob ? resolve(blob) : reject() 88 | }, imageType) 89 | }) 90 | } 91 | 92 | return await new Promise((resolve, reject) => { 93 | const reader = new FileReader() 94 | 95 | reader.readAsDataURL(blob) 96 | reader.onload = () => { 97 | if (reader.result) { 98 | resolve(reader.result) 99 | } else { 100 | reject('Failed to get data URL from canvas') 101 | } 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /docs/cheatsheet.md: -------------------------------------------------------------------------------- 1 | # チートシート 2 | 3 | 基礎的な文法は[Lisp の文法](../syntax)を参照。 4 | 雰囲気で理解出来る方向け。 5 | 6 | ### スケッチ 7 | 8 | ```clojure 9 | ;; パスの定義 10 | (def p (rect [10 10 50 50])) 11 | 12 | ;; トランスフォーム 13 | (translate [10 10]) 14 | (scale [(pct 50) 1]) 15 | (rotate (deg 45)) 16 | (mat2d/* (translate-x 10) ; 複数のトランスフォームを組み合わる 17 | (rotate PI) 18 | (scale [2 2])) 19 | (pivot [40 40] (rotate (deg 120))) ; アンカーポイント 20 | (view-center) 21 | (path/align-at .5 p) 22 | 23 | (transform (translate [50 50]) p) ; トランスフォームの適用 24 | 25 | ;; スタイル 26 | (style (fill "red") p) 27 | (style (stroke "blue" 20) p) ; -> width = 20 28 | (style (stroke "blue" 20 :cap "round") p) 29 | 30 | ;; パス 31 | (rect [0 0 20 20]) 32 | (circle [0 0] 100) 33 | (line [0 0] [100 100]) 34 | (ellipse [0 0] [40 60]) 35 | (ngon [0 0] 100 5) 36 | (polyline [20 -20] [0 0] [20 20]) 37 | (polygon [20 -20] [0 0] [20 20]) 38 | 39 | ;; パスの変形 40 | (def A (circle [0 0] 40)) 41 | (def B (rect [ 20 20 40 40])) 42 | 43 | (path/merge A B) 44 | (path/unite A B) 45 | (path/subtract A B) 46 | (path/intersect A B) 47 | 48 | (path/offset 10 B) 49 | (path/offset-stroke 2 A) 50 | 51 | ;; ベクタ操作 52 | [0 1] 53 | (vec2 [1 2]) 54 | (.x [10 20]) 55 | (.y [30 40]) 56 | (vec2/+ [10 20] [30 40]) 57 | (vec2/scale [2 3] 0.5) 58 | (vec2/normalize [20 20]) 59 | 60 | ;; 画面全体の背景色 61 | (background "blue") 62 | 63 | ;; アートボード 64 | (artboard {:bounds [10 10 50 50] 65 | :background "red"} 66 | (circle [0 0] 40)) 67 | 68 | ;; カラー 69 | "#ff0000" 70 | "red" 71 | (color 1 0 0) 72 | (color 0.5) ; 50%グレー 73 | 74 | 75 | ``` 76 | 77 | ### シンタックス 78 | 79 | ```clojure 80 | 81 | ;; リテラル 82 | 3.14159 83 | "String" 84 | :keyword 85 | symbol 86 | (+ 1 2) ; List 87 | [0 1 2 3 4 5] ; Vector 88 | {:key "value"} ; Map 89 | 90 | ;; 変数の宣言 91 | (def a 10) 92 | (let [b 10] 93 | (* b 2)) ; -> 10 (レキシカルスコープ) 94 | 95 | 96 | ;; 計算 97 | (+ 1 2) ; -> 3 98 | (/ 20 5) ; -> 4 99 | (- 10) ; -> 10 100 | (mod 12 5) ; -> 2 101 | (sqrt 9) ; -> 3 102 | PI 103 | TWO_PI 104 | HALF_PI 105 | E 106 | 107 | ;; ベクトル 108 | (vec2/+ [1 2] [50 50]) ; -> [51 52] 109 | (vec2/normalize [2 2]) ; -> [0.7071 0.7071] 110 | (vec2/angle [0 1]) ; -> 1.5707 (HALF_PI) 111 | 112 | ;; 論理演算子 113 | (= 1 1) ; -> true 114 | (> 2 3) ; -> false 115 | (not true) ; -> false 116 | (and true false) ; -> false 117 | (or false false true) ; -> true 118 | 119 | 120 | ;; 関数の定義 121 | (defn square [x] 122 | (* x x)) 123 | 124 | (square 5) ;-> 25 125 | 126 | 127 | ;; 条件分岐など 128 | (if true "x is true" "x is false") ; -> "x is true" 129 | 130 | (do (def x 10) 131 | (def y 50) 132 | (* x y)) ; -> 500 (最後の文が返される) 133 | 134 | (cond (zero? a) "a is zero" 135 | (even? a) "a is even" 136 | :else "a is odd") ; -> "a is even" 137 | 138 | (case a 139 | 0 "a is zero" 140 | 1 "a is one" 141 | "a is neither zero or one" ; -> "a is neither.. 142 | 143 | 144 | ;; その他 145 | (println "Hello World") 146 | (prn PI) 147 | 148 | ``` 149 | -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # 構文 2 | 3 | ## リストと前置記法 4 | 5 | Glisp で使われる言語は Clojure という Lisp 方言をベースにしています。Lisp は **LIS**t **P**rocessor の略で、その名の通り、すべての構文はリストからなります。リストは以下のように、丸括弧 `()` の中にスペースを挟んで表現します。カンマ `,` は要りません。 6 | 7 | ```clojure 8 | (max 1 2 3 4) ;; -> 4 9 | ``` 10 | 11 | 上記の `max` は、最大値を返す関数です。Lisp のリストは普通、関数呼び出しを表します。 `(<関数名> <引数1> <引数2>)` のようにリストの最初の要素が関数名、その後に続く要素がその関数への引数として解釈されます。これを前置記法といいます。例えば 12 | 13 | ```js 14 | 1 + 2 15 | ``` 16 | 17 | のように、2 つの値の間に挟む形で使われる加算記号 `+` も、Lisp では `+` という名前の一介の関数でしかありません。そして、関数名は常にリストの最初に来るので、 18 | 19 | ```clojure 20 | (+ 1 2) 21 | ``` 22 | 23 | となります。普通の関数呼び出しも同様です。JavaScript でいう `Math.sqrt(4)` は `(sqrt 4)` となります。Lisp の気味悪さはだいたいこの前置記法のせいですが、このシンプルさがもたらす良いこともたくさんあります。これは Lisp に慣れるうちにだんだんと分かってきます。 24 | 25 | ## すべてのリストが値を返す 26 | 27 | このように、基本的な四則演算子をはじめ、`>` のような比較演算子、変数の宣言から `if` のような条件分岐文まで、すべてが関数呼び出しの形で表現され、それらは**評価**されることで何らかの値を返します。以下は JavaScript との比較と、そのリストが返す値です。 28 | 29 | ```clojure 30 | 31 | ; 変数の宣言(右辺を返す) 32 | ; let x = 10 33 | (def x 10) ; -> 10 34 | 35 | ; コンソールにログを表示(文全体としては nil を返す) 36 | ; console.log("Hello World") 37 | (println "Hello World") ; -> nil 38 | 39 | ; 比較演算子(真偽値を返す) 40 | ; x === 10 41 | (= x 10) ; -> false 42 | 43 | ; if文(三項演算子のように、if文全体としても1つの値を返す) 44 | ; (x > 5) ? "A" : "B" 45 | (if (> x 5) "A" "B") ; -> "A" 46 | 47 | ; λ式(関数オブジェクトを返す) 48 | ; x => x * 2 49 | (fn [x] (* x 2)) ; -> (fn [x] (x * 2)) 50 | ``` 51 | 52 | ## リテラル 53 | 54 | `nil` は JavaScript でいう `null` です。Glisp では以下のようなリテラルを扱うことができます。 55 | 56 | ```clojure 57 | -3.14159 ; 数字 58 | "Hello" ; 文字列 59 | x max ; シンボル(変数名・関数名) 60 | :keyword ; キーワード(マップのキーなどで使う) 61 | true false ; 真偽値 62 | nil ; null 63 | 64 | ; ベクタ(関数呼び出しとは解釈されない、ただの配列データ) 65 | [0 1 2] 66 | 67 | ; マップ 68 | {:name "Taro" :age 20} 69 | ``` 70 | 71 | ちなみに `;` でコメントアウトできます。 72 | 73 | ## 評価 74 | 75 | リストは入れ子にすることができます。その場合、内側かつ最初のリストから順に評価されます。そのリストが関数呼び出しをした結果の値に次々と置き換わっていくイメージです。以下は、式全体が評価されていく様子です。(`Ctrl+E`でカーソルがある部分の式が評価されます) 76 | 77 |  78 | 79 | ## ベクタ 80 | 81 | カッコ `()` で囲まれたリストは常に関数呼び出しとして評価されるので、 1 つめの要素が関数ではなかった場合はエラーとなります。 82 | 83 | ```clojure 84 | (0 1 2) ; 0 は関数ではないのでエラー 85 | ``` 86 | 87 | そこで、データとしてのリストを表すにはベクタを使います。ベクタは角括弧 `[]` で囲われた Glisp では座標やパスデータを表すのに使われます。 88 | 89 | ```clojure 90 | [50 100] ; x=50, y=100 の位置 91 | [:path :M [0 0] :L [100 100]] ; 原点から[100 100]までの直線 92 | ``` 93 | 94 | ## Clojure との違い 95 | 96 | 以上が基本的な文法になります。 97 | 98 | より細かい構文は、[Clojure 入門](https://japan-clojurians.github.io/clojure-site-ja/guides/learn/syntax)を読んでください。 99 | 100 | Glisp で使われる Lisp は Clojure をベースにした簡易的なものです。実装されていないコアライブラリもあります。文法としては、例えば以下のような違いがあります。 101 | 102 | - マップは キーワードか文字列、シンボルしかキーに持つことができない 103 | ```clojure 104 | {10 "ten"} ;; だめ 105 | {[0 1] 100} ;; だめ 106 | ``` 107 | - マップの分割代入の表記順が逆 108 | ```clojure 109 | ;; Clojureでは 110 | (let [{x :A y :B} {:A 0 :B 1}]) 111 | ;; Glispでは 112 | (let [{:A x :B y} {:A 0 :B 1}]) ;; x = 0, y = 1 113 | ``` 114 | - その他 `:as` `:keys` などを使った分割代入はできない 115 | - 名前空間の非サポート(`/` 区切りの関数名として簡易的に表記しています) 116 | -------------------------------------------------------------------------------- /src/path-utils.ts: -------------------------------------------------------------------------------- 1 | import {vec2, mat2d} from 'gl-matrix' 2 | import { 3 | MalError, 4 | isKeyword, 5 | MalVal, 6 | keywordFor as K, 7 | isVector, 8 | } from '@/mal/types' 9 | 10 | const K_PATH = K('path') 11 | 12 | export type Vec2 = number[] | vec2 13 | 14 | export type PathType = (string | Vec2)[] 15 | export type SegmentType = [string, ...Vec2[]] 16 | 17 | export function isPath(exp: any): exp is PathType { 18 | return isVector(exp) && exp[0] === K_PATH 19 | } 20 | 21 | export function* iterateSegment(path: PathType): Generator { 22 | if (!Array.isArray(path)) { 23 | throw new MalError('Invalid path') 24 | } 25 | 26 | let start = path[0].toString().startsWith(K_PATH) ? 1 : 0 27 | 28 | for (let i = start + 1, l = path.length; i <= l; i++) { 29 | if (i === l || isKeyword(path[i] as MalVal)) { 30 | yield path.slice(start, i) as SegmentType 31 | start = i 32 | } 33 | } 34 | } 35 | 36 | export function getSVGPathDataRecursive(exp: MalVal): string { 37 | return convertPath(exp, mat2d.create()) 38 | 39 | function convertPath(exp: MalVal, transform?: mat2d): string { 40 | if (!isVector(exp)) { 41 | return '' 42 | } 43 | 44 | switch (exp[0]) { 45 | case K('path'): 46 | return getSVGPathData(transformPath(exp as PathType, transform)) 47 | case K('style'): { 48 | return exp 49 | .slice(2) 50 | .map(e => convertPath(e, transform)) 51 | .join(' ') 52 | } 53 | case K('transform'): { 54 | const newTransform = mat2d.mul( 55 | mat2d.create(), 56 | transform || mat2d.create(), 57 | exp[1] as mat2d 58 | ) 59 | return exp 60 | .slice(2) 61 | .map(e => convertPath(e, newTransform)) 62 | .join(' ') 63 | } 64 | } 65 | 66 | return '' 67 | } 68 | 69 | function transformPath(path: PathType, transform?: mat2d) { 70 | return !transform 71 | ? path 72 | : (path.map(p => 73 | isVector(p as MalVal) 74 | ? vec2.transformMat2d(vec2.create(), p as vec2, transform) 75 | : p 76 | ) as PathType) 77 | } 78 | } 79 | 80 | export function getSVGPathData(path: PathType) { 81 | if (path[0].toString().startsWith(K_PATH)) { 82 | path = path.slice(1) 83 | } 84 | 85 | return path.map(x => (isKeyword(x as MalVal) ? x.slice(1) : x)).join(' ') 86 | } 87 | 88 | const K_M = K('M'), 89 | K_L = K('L'), 90 | K_C = K('C'), 91 | K_Z = K('Z') 92 | 93 | export function convertToPath2D(exp: PathType) { 94 | const path = new Path2D() 95 | 96 | for (const [cmd, ...pts] of iterateSegment(exp)) { 97 | switch (cmd) { 98 | case K_M: 99 | path.moveTo(...(pts[0] as [number, number])) 100 | break 101 | case K_L: 102 | path.lineTo(...(pts[0] as [number, number])) 103 | break 104 | case K_C: 105 | path.bezierCurveTo( 106 | pts[0][0], 107 | pts[0][1], 108 | pts[1][0], 109 | pts[1][1], 110 | pts[2][0], 111 | pts[2][1] 112 | ) 113 | break 114 | case K_Z: 115 | path.closePath() 116 | } 117 | } 118 | 119 | return path 120 | } 121 | -------------------------------------------------------------------------------- /docs/en/cheatsheet.md: -------------------------------------------------------------------------------- 1 | # Cheat Sheet 2 | 3 | Please find [Grammar of Lisp](syntax) for the basic grammar. 4 | This document is merely a cheat sheet. 5 | 6 | ### Sketch 7 | 8 | ```clojure 9 | ;; define a path 10 | (def p (rect [10 10 50 50])) 11 | 12 | ;; transformation 13 | (translate [10 10]) 14 | (scale [(pct 50) 1]) 15 | (rotate (deg 45)) 16 | (mat2d/* (translate-x 10) ; combine several transforms 17 | (rotate PI) 18 | (scale [2 2])) 19 | (pivot [40 40] (rotate (deg 120))) ; anchor point (pivoting) 20 | (view-center) 21 | (path/align-at .5 p) 22 | 23 | (transform (translate [50 50]) p) ; apply transform 24 | 25 | ;; styling 26 | (style (fill "red") p) 27 | (style (stroke "blue" 20) p) ; -> width = 20 28 | (style (stroke "blue" 20 :cap "round") p) 29 | 30 | ;; path 31 | (rect [0 0 20 20]) 32 | (circle [0 0] 100) 33 | (line [0 0] [100 100]) 34 | (ellipse [0 0] [40 60]) 35 | (ngon [0 0] 100 5) 36 | (polyline [20 -20] [0 0] [20 20]) 37 | (polygon [20 -20] [0 0] [20 20]) 38 | 39 | ;; path operations 40 | (def A (circle [0 0] 40)) 41 | (def B (rect [ 20 20 40 40])) 42 | 43 | (path/merge A B) 44 | (path/unite A B) 45 | (path/subtract A B) 46 | (path/intersect A B) 47 | 48 | (path/offset 10 B) 49 | (path/offset-stroke 2 A) 50 | 51 | ;; vector manipulation 52 | [0 1] 53 | (vec2 [1 2]) 54 | (.x [10 20]) 55 | (.y [30 40]) 56 | (vec2/+ [10 20] [30 40]) 57 | (vec2/scale [2 3] 0.5) 58 | (vec2/normalize [20 20]) 59 | 60 | ;; canvas background 61 | (background "blue") 62 | 63 | ;; artboard 64 | (artboard {:bounds [10 10 50 50] 65 | :background "red"} 66 | (circle [0 0] 40)) 67 | 68 | ;; colors 69 | "#ff0000" 70 | "red" 71 | (color 1 0 0) 72 | (color 0.5) ; 50% grey 73 | 74 | 75 | ``` 76 | 77 | ### Syntax 78 | 79 | ```clojure 80 | 81 | ;; literal 82 | 3.14159 83 | "String" 84 | :keyword 85 | symbol 86 | (+ 1 2) ; List 87 | [0 1 2 3 4 5] ; Vector 88 | {:key "value"} ; Map 89 | 90 | ;; define a value 91 | (def a 10) 92 | (let [b 10] 93 | (* b 2)) ; -> 10 (lexical scoping) 94 | 95 | 96 | ;; arithmetic operations 97 | (+ 1 2) ; -> 3 98 | (/ 20 5) ; -> 4 99 | (- 10) ; -> 10 100 | (mod 12 5) ; -> 2 101 | (sqrt 9) ; -> 3 102 | PI 103 | TWO_PI 104 | HALF_PI 105 | E 106 | 107 | ;; vector operations in 2D space 108 | (vec2/+ [1 2] [50 50]) ; -> [51 52] 109 | (vec2/normalize [2 2]) ; -> [0.7071 0.7071] 110 | (vec2/angle [0 1]) ; -> 1.5707 (HALF_PI) 111 | 112 | ;; logical operations 113 | (= 1 1) ; -> true 114 | (> 2 3) ; -> false 115 | (not true) ; -> false 116 | (and true false) ; -> false 117 | (or false false true) ; -> true 118 | 119 | 120 | ;; define a function 121 | (defn square [x] 122 | (* x x)) 123 | 124 | (square 5) ;-> 25 125 | 126 | 127 | ;; conditional operators 128 | (if true "x is true" "x is false") ; -> "x is true" 129 | 130 | (do (def x 10) 131 | (def y 50) 132 | (* x y)) ; -> 500 (the last line is returned) 133 | 134 | (cond (zero? a) "a is zero" 135 | (even? a) "a is even" 136 | :else "a is odd") ; -> "a is even" 137 | 138 | (case a 139 | 0 "a is zero" 140 | 1 "a is one" 141 | "a is neither zero or one" ; -> "a is neither.. 142 | 143 | 144 | ;; miscellaneous 145 | (println "Hello World") 146 | (prn PI) 147 | 148 | ``` 149 | -------------------------------------------------------------------------------- /src/components/ViewCanvas.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 126 | 127 | 136 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalInputVec2.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 19 | 27 | 28 | 29 | 35 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 101 | 102 | 113 | -------------------------------------------------------------------------------- /src/components/use/use-gesture.ts: -------------------------------------------------------------------------------- 1 | import {Ref, onMounted} from '@vue/composition-api' 2 | import isElectron from 'is-electron' 3 | import hotkeys from 'hotkeys-js' 4 | 5 | interface UseGestureOptions { 6 | onScroll?: (e: MouseWheelEvent) => any 7 | onGrab?: (e: MouseWheelEvent) => any 8 | onZoom?: (e: MouseWheelEvent) => any 9 | onRotate?: (e: {rotation: number; pageX: number; pageY: number}) => any 10 | } 11 | 12 | export default function useGesture( 13 | el: Ref, 14 | options: UseGestureOptions 15 | ) { 16 | const isWindows = /win/i.test(navigator.platform) 17 | 18 | onMounted(() => { 19 | if (!el.value) return 20 | 21 | if (options.onScroll || options.onZoom) { 22 | // Wheel scrolling 23 | el.value.addEventListener('wheel', (e: MouseWheelEvent) => { 24 | if (e.altKey || e.ctrlKey) { 25 | if (options.onZoom) { 26 | e.preventDefault() 27 | e.stopPropagation() 28 | if (isWindows) { 29 | e = { 30 | pageX: e.pageX, 31 | pageY: e.pageY, 32 | deltaY: e.deltaY / 10, 33 | } as MouseWheelEvent 34 | } 35 | options.onZoom(e) 36 | } 37 | } else { 38 | if (options.onScroll) { 39 | e.preventDefault() 40 | e.stopPropagation() 41 | options.onScroll(e) 42 | } 43 | } 44 | }) 45 | } 46 | 47 | if (options.onGrab) { 48 | const onGrab = options.onGrab 49 | let prevX: number, prevY: number 50 | 51 | const onGrabMove = (_e: MouseEvent) => { 52 | const e = { 53 | deltaX: _e.pageX - prevX, 54 | deltaY: _e.pageY - prevY, 55 | } as MouseWheelEvent 56 | 57 | prevX = _e.pageX 58 | prevY = _e.pageY 59 | onGrab(e) 60 | } 61 | 62 | const onGrabEnd = () => { 63 | el.value?.removeEventListener('mousemove', onGrabMove) 64 | document.documentElement.style.cursor = 'default' 65 | } 66 | 67 | // Middle-button/space translation 68 | el.value.addEventListener('mousedown', (e: MouseEvent) => { 69 | if (e.button === 1 || hotkeys.isPressed('space')) { 70 | prevX = e.pageX 71 | prevY = e.pageY 72 | 73 | el.value?.addEventListener('mousemove', onGrabMove) 74 | el.value?.addEventListener('mouseup', onGrabEnd) 75 | document.documentElement.style.cursor = 'grab' 76 | } 77 | }) 78 | 79 | // Toggle cursor on pressing space 80 | hotkeys('space', {keydown: true, keyup: true}, e => { 81 | e.preventDefault() 82 | e.stopPropagation() 83 | 84 | if (e.type === 'keydown') { 85 | document.documentElement.style.cursor = 'grab' 86 | } else if (e.type === 'keyup') { 87 | document.documentElement.style.cursor = 'default' 88 | } 89 | }) 90 | 91 | // Rotation (only enabled in macos electron) 92 | if (options.onRotate && isElectron()) { 93 | const onRotate = options.onRotate 94 | const ipc = eval("require('electron').ipcRenderer") 95 | let pageX = 0, 96 | pageY = 0 97 | 98 | window.addEventListener('mousemove', e => { 99 | pageX = e.pageX 100 | pageY = e.pageY 101 | }) 102 | 103 | ipc.on('rotate-gesture', (e: any, rotation: number) => { 104 | // const {x, y} = GetCursorPosition() 105 | onRotate({rotation, pageX, pageY}) 106 | }) 107 | } 108 | } 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /src/components/mal-inputs/MalExpButton.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | {{ sign }} 8 | 9 | {{ str }} 10 | 11 | 12 | 13 | 78 | 79 | 139 | -------------------------------------------------------------------------------- /src/components/GlobalMenu/WindowTitleButtons.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 86 | 87 | 126 | -------------------------------------------------------------------------------- /docs/en/styles.md: -------------------------------------------------------------------------------- 1 | # Applying Styles 2 | 3 | As explained in the [First Sketch](get-started), an element is rendered only when a style is applied. A style consists of several properties such as color and thickness, and they can be inherited, drawn and referenced based on some rules. 4 | 5 | ## Referring to a property 6 | 7 | Inside `style` function, applied style properties can be accessed. 8 | 9 | ```cljs 10 | (style (fill "pink") 11 | (text *fill-color* [50 50] :size 24)) 12 | ``` 13 | 14 | Here, the symbol `*fill-color*` refers to the string `"pink"` set by `fill` function in the outer scope (variable names surrounded by `*` denote a symbol similar to global variables). Similarly, line properties can accessed by `*stroke-color*`, `*stroke-width*` and `*stroke-cap*`. This is useful, for example, for a flowchart whose outline and text colors are filled with the same color. 15 | 16 | ```cljs 17 | (style (stroke "royalblue" 2) 18 | (ngon [50 50] 40 4) 19 | 20 | (style [(no-stroke) (fill *stroke-color*)] 21 | (text "Hello" [50 50]))) 22 | ``` 23 | 24 | Here, `no-stroke` function is used, which cancels the previous stroke styles. The following is an explanation. 25 | 26 | ```clojure 27 | (stroke "royalblue" 2) -> (no-stroke) -> (fill *stroke-color*) 28 | ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ 29 | no-stroke is cancelled even though stroke is cancelled, *stroke-color* remains as "royalblue" 30 | ``` 31 | 32 | This method prevents the text Hello to become thicker with the outline applied. This is useful for drawing an arrow. 33 | 34 | ```cljs 35 | (style (stroke "pink" 2) 36 | (line [10 50] [80 50]) 37 | 38 | (style [(no-stroke) (fill *stroke-color*)] 39 | (ngon [80 50] 10 3))) 40 | ``` 41 | 42 | ## The content of a style 43 | 44 | `fill` and `stroke` function actually returns a map similar to CSS properties. 45 | 46 | ```clojure 47 | (fill "pink") -> {:fill true :fill-color "pink"} 48 | 49 | (stroke "skyblue" 10) -> {:stroke true :stroke-color "skyblue" :stroke-width 10} 50 | ``` 51 | 52 | Therefore, such a map can be directly passed to `style` function. 53 | 54 | Unlike CSS, a property whether the style is enabled and a style property for the color selection are separated. For example, `fill: "pink";` in CSS enables fill and sets the fill color, but Glisp holds two independent properties `:fill` and `:fill-color`, respectively. Thus, a default style color can be set without affecting the rendering of all the paths inside `style` function. 55 | 56 | ```cljs 57 | (style {:fill-color "royalblue"} 58 | 59 | (style (fill) 60 | (text "Hello" [50 50])) 61 | 62 | (style (stroke "pink" 2) 63 | (circle [50 50] 40))) 64 | ``` 65 | 66 | Not only colors, properties are inherited to the inner elements even if it is not rendered. When a property is overwritten, the innermost property is chosen as CSS does. 67 | 68 | ```cljs 69 | (style {:stroke-dash [15 4]} 70 | (style (stroke "crimson" 2) 71 | (circle [50 50] 40)) 72 | 73 | (style (stroke "plum" 2 :dash [4 4]) 74 | (ngon [50 50] 40 5))) 75 | ``` 76 | 77 | ## Presetting a style 78 | 79 | Since a style is merely a map, it can be reused as a preset by declaring as a variable. Of course, it can be applied in conjunction with other style. 80 | 81 | ```cljs 82 | (def fill-skyblue (fill "skyblue")) 83 | 84 | (style fill-skyblue 85 | (rect [10 10 40 40])) 86 | 87 | (style [fill-skyblue (stroke "crimson" 5)] 88 | (circle [70 70] 20)) 89 | 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/transform.md: -------------------------------------------------------------------------------- 1 | # トランスフォームについて 2 | 3 | ## 従来の固定的な UI 4 | 5 |  6 | 7 | どのソフトにも、ある要素のトランスフォーム(配置)を設定する UI があります。多少の違いはあれ、どのソフトもおおよそ **translate(平行移動)**、**rotate(回転)**、**scale(スケール)** の順に変形が適用されるようになっています。それぞれの変形を表す行列を $T$、$R$、$S$ とし、最終的な変形行列を $M$ とすると、 8 | 9 | $$ 10 | M = T R S 11 | $$ 12 | 13 | のようになるということです。 14 | 15 | AfterEffects の場合、アンカーポイントという設定項目が加わります。ただやっていること自体は簡単で、まずアンカーポイントが原点に来るよう逆方向に移動し、3 つの変形を順に適用した後、アンカーポイントの位置分だけ戻してあげるだけです。こうすることで、好きな点を基準に回転・スケールをさせることができます。 16 | 17 | $$ 18 | M = P^{-1} (T R S) P 19 | $$ 20 | 21 | Houdini の場合は $T$・$R$・$S$ の適用順を自由に並べ替えることが出来ます。 22 | 23 |  24 | 25 | ## スタック式トランスフォーム 26 | 27 | このように、トランスフォーム一つとっても様々な操作のしかたがあり、それを固定的な UI で表現しようとするとどうしても煩雑になります。3D の場合、さらに回転軸の順序も加わるので、もうシッチャカメッチャカです。 28 | 29 | だから、僕が思うベストなトランスフォーム UI は、それぞれの変形操作を好きな順に、いくつでも並べていける構造です。これをスタック式と呼ぶことにします。 30 | 31 | ```cljs 32 | (transform 33 | (mat2d/* (translate [50 10]) 34 | (rotate (deg 45)) 35 | (scale [1 1.5])) 36 | (guide/axis)) 37 | ``` 38 | 39 | `translate`、`scale`、`rotate` は行列を返すただの関数です。それを `mat2d/*` で文字通り乗算してあげることでトランスフォーム値を得ています。(Lisp では関数名をスラッシュ`/`で区切ることで名前空間を表現します。`mat2d/*` は、2 次元のアフィン行列の操作をまとめた `mat2d` という名前空間の中の、行列の乗算をする `*` という一文字の関数名です)これはちょうど [CSS Transform プロパティ](https://developer.mozilla.org/ja/docs/Web/CSS/transform)と同じような方法です。ですから、せん断や X 軸方向だけの平行移動を表したければ、CSS に倣って`skew` や `translate-x` 関数を定義してあげるまでのことです。 40 | 41 | AfterEffects のアンカーポイントを用いたトランスフォームは以下のように表現できます。 42 | 43 | ```cljs 44 | ;; 四角形の中心(20, 20)を軸に 45 | ;; 回転、スケールが適用される 46 | (transform 47 | (mat2d/* (pivot [20 20] 48 | (translate [0 0]) 49 | (rotate (deg 60)) 50 | (scale [1 1.5]))) 51 | (style (fill "crimson") 52 | (rect [0 0 40 40]))) 53 | ``` 54 | 55 | `pivot`という関数が登場していますが、上記のコードの`pivot`部分は以下と同じことです。 56 | 57 | ```clojure 58 | (mat2d/* (translate [-20 -20]) ;; アンカーポイントを原点へ 59 | (mat2d/* (translate [0 0]) 60 | (rotate (deg 60)) 61 | (scale [1 1.5])) 62 | (translate [20 20])) ;; 元に戻す 63 | ``` 64 | 65 | モーショングラフィックスの制作では、一つの図形を動かして回転させてさらにそこから動かして…といった、そのレイヤーのトランスフォームだけでは表現しきれない動かし方をするためにいくつものヌルを入れ子にすることがあります。しかしこの方法だと余計な親子関係を作らずとも、一つのトランスフォーム UI だけでそうした操作を表現することができます。 66 | 67 | ここまでに登場した `translate` も `mat2d/*` も、行列を返すただの関数でしかないので、オブジェクトのトランスフォームに限らず、例えばパスの頂点に対する変形操作も全く同じ方法と柔軟さで表現することが出来ます。 68 | 69 | ```cljs 70 | (style (stroke "orange" 4) 71 | 72 | ;; path/transformは描画コンテクストではなく 73 | ;; パスデータ自体にトランスフォームを適用するので 74 | ;; スケールさせてもストロークが潰れない 75 | (path/transform 76 | (mat2d/* (translate [30 30]) 77 | (rotate (deg 20)) 78 | (scale [1 2])) 79 | 80 | (rect [0 0 20 20]))) 81 | ``` 82 | 83 | ## コンストレイント機能 84 | 85 | これまであげた基本的な変形操作の他に、「パスに沿わせる」「ある点を向く」といったものもあります。こうした操作は、コンストレイント(拘束)機能として実装されていることが多いですが、どのソフトもトランスフォーム UI と統合するのに苦労しているようです。 86 | 87 |  88 | 89 | スタック式だと、これまでと同様、新しい行列 `path/align-at` や `mat2d/look-at` を定義するだけ事足ります。 90 | 91 | ```cljs 92 | ;; (def a b) で、 var a = b; のように変数を宣言 93 | (def circle-path (circle [50 50] 36)) 94 | 95 | ;; def文自体もまた代入された値を返すため、 96 | ;; :start-sketchで、 97 | ;; それ以降の文のみスケッチに反映させるようにする 98 | :start-sketch 99 | 100 | (style (stroke "skyblue" 2) 101 | circle-path) 102 | 103 | (transform 104 | ;; circle-pathの始点から40%の位置に沿わせるトランスフォーム値を設定 105 | (path/align-at 0.4 circle-path) 106 | 107 | (style (fill "crimson") 108 | (ngon [0 0] 10 3))) 109 | ``` 110 | -------------------------------------------------------------------------------- /src/components/inputs/InputSlider.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 123 | 124 | 158 | -------------------------------------------------------------------------------- /public/lib/tools-example.glisp: -------------------------------------------------------------------------------- 1 | (import "math.glisp") 2 | 3 | (defpen pencil (state input) 4 | (do 5 | 6 | ; Initialize state 7 | (if (nil? (first state)) 8 | (def state '((quote :path) false false false))) 9 | 10 | (let 11 | (; State 12 | item (last (first state)) 13 | px (nth state 1) 14 | py (nth state 2) 15 | pp (nth state 3) 16 | 17 | ; Input 18 | x (nth input 0) 19 | y (nth input 1) 20 | p (nth input 2) 21 | 22 | ; just mouse down? 23 | just-down (and (not pp) p) 24 | needs-update (or just-down (and p (or (!= x px) (!= y py))))) 25 | 26 | (list 27 | ; Updated Item or nil if no needs to update 28 | (if needs-update 29 | `(quote 30 | ~(concat 31 | item 32 | `(~(if just-down :M :L) ~x ~y)))) 33 | ; Updated State 34 | x y p)))) 35 | 36 | (defpen draw-circle (state input) 37 | (do 38 | 39 | ; Initialize state 40 | (if (nil? (first state)) 41 | (def state '((g) 0 0 false))) 42 | 43 | (let 44 | (; State 45 | item (first state) 46 | pp (nth state 3) 47 | 48 | ; Input 49 | x (nth input 0) 50 | y (nth input 1) 51 | p (nth input 2) 52 | 53 | ; just mouse down? 54 | just-down (and (not pp) p) 55 | 56 | cx (if just-down x (nth state 1)) 57 | cy (if just-down y (nth state 2))) 58 | 59 | (list 60 | ; Updated Item or nil if no needs to update 61 | (if p 62 | (let (C `(circle ~cx ~cy ~(round (distance cx cy x y)))) 63 | (if just-down 64 | (concat item (list C)) 65 | (concat (butlast item) (list C))))) 66 | 67 | ; Updated State 68 | cx cy p)))) 69 | 70 | 71 | (defpen draw-rect (state input) 72 | (do 73 | 74 | ; Initialize state 75 | (if (nil? (first state)) 76 | (def state '((g) 0 0 false))) 77 | 78 | (let 79 | (; State 80 | item (first state) 81 | pp (nth state 3) 82 | 83 | ; Input 84 | x (nth input 0) 85 | y (nth input 1) 86 | p (nth input 2) 87 | 88 | ; just mouse down? 89 | just-down (and (not pp) p) 90 | 91 | ox (if just-down x (nth state 1)) 92 | oy (if just-down y (nth state 2))) 93 | 94 | (list 95 | ; Updated Item or nil if no needs to update 96 | (if p 97 | (let (R `(rect ~ox ~oy ~(- x ox) ~(- y oy))) 98 | (if just-down 99 | (concat item (list R)) 100 | (concat (butlast item) (list R))))) 101 | 102 | ; Updated State 103 | ox oy p)))) 104 | 105 | (defpen draw-poly (state input) 106 | do 107 | 108 | (if (nil? (first state)) 109 | (def state '((poly) false false false))) 110 | 111 | (let 112 | (; State 113 | item (nth state 0) 114 | px (nth state 1) 115 | py (nth state 2) 116 | pp (nth state 3) 117 | 118 | ; Input 119 | x (nth input 0) 120 | y (nth input 1) 121 | p (nth input 2) 122 | 123 | just-down (and (not pp) p) 124 | needs-update (or just-down (and p (or (!= x px) (!= y py))))) 125 | 126 | (list 127 | ; Updated Item or nil if no needs to update 128 | (if needs-update 129 | (if just-down 130 | (push item x y) 131 | (push (take (- (count item) 2) item) x y)) 132 | nil) 133 | 134 | ; Updated state 135 | x y p)))) -------------------------------------------------------------------------------- /docs/blend-modes.md: -------------------------------------------------------------------------------- 1 | # ブレンドモードの分類 2 | 3 | ブレンドモードは、名前からどういう働きをするかが分かりづらいものが多いので、自分なりに整理しておきます。 4 | 5 | $a$が背景レイヤー、 $b$が上に重ねるレイヤー。反転値を、それぞれ$\overline{a} = 1 - a, \overline{b} = 1 - b$とします。 6 | 7 | ### 二者択一 8 | 9 | | 名前 | 計算式 | 10 | | ---------------- | ------ | 11 | | Normal | $b$ | 12 | | Destination Over | $a$ | 13 | 14 | ### 加算 15 | 16 | | 名前 | 計算式 | 17 | | ------------------ | ----------------------------- | 18 | | Linear Dodge (Add) | $a + b$ | 19 | | Linear Burn | $\overline{a} + \overline{b}$ | 20 | | AddSub | | 21 | 22 | Linear Burn は分かりづらいですが、「[暗さを足す](http://fe0km.blog.fc2.com/blog-entry-77.html)」イメージです。 23 | AddSub は Substance Designer にあるブレンドモード。 24 | 25 | ### 乗算 26 | 27 | | 名前 | 計算式 | 28 | | ---------- | ----------------------------------------------------------------------------------------------------------------- | 29 | | Multiply | $ab$ | 30 | | Screen | $\overline{\overline{a}\ \overline{b}}$ | 31 | | Overlay | $\begin{cases} 2ab, & \text{if}\ a>0.5 \\ \overline{2\overline{a}\ \overline{b}}, & \text{otherwise} \end{cases}$ | 32 | | Hard Light | $\begin{cases} 2ab, & \text{if}\ b>0.5 \\ \overline{2\overline{a}\ \overline{b}}, & \text{otherwise} \end{cases}$ | 33 | 34 | 下 2 つは、$a$、$b$いずれかが 50%グレーより明るければ Screen、暗ければ Multiply と場合分けします。明るい色はより明るく、暗い色はより暗くなるということです。Overlay の場合は$a$を基準に、Hard Light は$b$を場合分けの基準にするだけ。つまり、$Overlay(b, a) = HardLight(a, b)$ の関係です。 35 | 36 | ### 最大・最小 37 | 38 | | 名前 | 計算式 | 39 | | --------- | ----------- | 40 | | Lighten | $max(a, b)$ | 41 | | Darken | $min(a, b)$ | 42 | | Pin Light | | 43 | 44 | ### 除算 45 | 46 | | Mode | Calculation | 47 | | ----------- | ------------------------------------------------------------------------------------------------- | 48 | | Color Dodge | $\begin{cases} 1, & \text{if}\ b=1\\ a / \overline{b}, & \text{otherwise} \end{cases}$ | 49 | | Color Burn | $\begin{cases} 0, & \text{if}\ b=0\\ \overline{\overline{a} / b}, & \text{otherwise} \end{cases}$ | 50 | | Vivid Light | | 51 | 52 | Dodge と Burn の場合分けは単にゼロ除算対策のため。やっていることは至ってシンプルです。 53 | 54 | Vivid Light は b を基準とした 55 | 56 | ### 差分 57 | 58 | | 名前 | 計算式 | 59 | | ---------- | ----------------------------------- | 60 | | Subtract | $a - b$ | 61 | | Difference | $\|a - b\|$ | 62 | | Exclusion | $\overline{a}\ b + a\ \overline{b}$ | 63 | 64 | Subtract だと負の値を取ることもありますが、絶対値をとって必ず 0-1 になるようにしたのが Difference。 65 | 66 | Exclusion は、ニュアンスとしては 50%グレーを基準にした Difference。a, b が同じとき、Difference の場合は 0(黒)になるが、Exclusion は 0.5(50%グレー)になる。 67 | 68 | ### Soft Light 69 | 70 | 上のレイヤーの値に基づき僅かに明るくなったり暗くなったりします。 71 | 72 | | 名前 | 計算式 | 73 | | ---------- | --------------------------------------------------------------------------------------------------------------------------- | 74 | | Soft Light | $ \begin{cases} 2ab + a^2(1 - 2b), & \text{if}\ b < 0.5\\ 2a\overline{b} + a^{0.5}(2b - 1), & \text{otherwise} \end{cases}$ | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glisp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "deploy": "node task.deploy.js", 10 | "build-deploy": "yarn build; yarn deploy --commit", 11 | "doc:deploy": "yarn deploy --docs", 12 | "doc:serve": "docsify serve docs", 13 | "doc:export-refs": "yarn repl export-refs.glisp", 14 | "electron:build": "vue-cli-service electron:build", 15 | "electron:serve": "vue-cli-service electron:serve", 16 | "postinstall": "electron-builder install-app-deps", 17 | "postuninstall": "electron-builder install-app-deps", 18 | "repl": "node ./repl/index.js", 19 | "repl:build": "webpack --config webpack.repl.config.js", 20 | "repl:serve": "webpack --config webpack.repl.config.js --watch" 21 | }, 22 | "main": "background.js", 23 | "dependencies": { 24 | "docsify": "^4.11.5", 25 | "vue": "^2.6.12" 26 | }, 27 | "devDependencies": { 28 | "@types/chroma-js": "^2.0.0", 29 | "@types/color": "^3.0.1", 30 | "@types/file-saver": "^2.0.1", 31 | "@types/jquery": "^3.5.0", 32 | "@types/mousetrap": "^1.6.3", 33 | "@types/readline-sync": "^1.4.3", 34 | "@typescript-eslint/eslint-plugin": "^3.6.1", 35 | "@typescript-eslint/parser": "^3.6.1", 36 | "@vue/cli-plugin-eslint": "~4.4.6", 37 | "@vue/cli-plugin-typescript": "~4.4.6", 38 | "@vue/cli-service": "^4.4.6", 39 | "@vue/composition-api": "^1.0.0-beta.4", 40 | "@vue/eslint-config-prettier": "^6.0.0", 41 | "@vue/eslint-config-typescript": "^5.0.2", 42 | "@vue/runtime-core": "^3.0.0-rc.5", 43 | "babel-runtime": "^6.26.0", 44 | "bezier-easing": "^2.1.0", 45 | "bezier-js": "^2.6.1", 46 | "brace": "^0.11.1", 47 | "canvas2svg": "^1.0.16", 48 | "case": "^1.6.3", 49 | "chroma-js": "^2.1.0", 50 | "comlink": "^4.3.0", 51 | "comlink-loader": "^2.0.0", 52 | "copy-webpack-plugin": "^6.0.3", 53 | "css-color-names": "^1.0.1", 54 | "dateformat": "^3.0.3", 55 | "delaunator": "^4.0.1", 56 | "docsify-cli": "^4.4.3", 57 | "earcut": "^2.2.2", 58 | "electron": "^11.5.0", 59 | "eslint": "^6.7.2", 60 | "eslint-plugin-prettier": "^3.1.1", 61 | "eslint-plugin-vue": "^6.2.2", 62 | "eventemitter3": "^4.0.4", 63 | "file-saver": "^2.0.2", 64 | "ftp-deploy": "^2.3.7", 65 | "gif.js": "^0.2.0", 66 | "gl-matrix": "^3.3.0", 67 | "hotkeys-js": "^3.8.1", 68 | "hull.js": "^1.0.0", 69 | "is-electron": "^2.2.0", 70 | "is-node": "^1.0.2", 71 | "jq-console": "^2.13.2", 72 | "jquery": "^3.5.1", 73 | "keycode": "^2.2.0", 74 | "mousetrap": "^1.6.5", 75 | "node-fetch": "^2.6.1", 76 | "normalize.css": "^8.0.1", 77 | "paper": "^0.12.11", 78 | "paperjs-offset": "^1.0.8", 79 | "portal-vue": "^2.1.7", 80 | "prettier": "^2.0.5", 81 | "raw-loader": "^4.0.1", 82 | "readline-sync": "^1.4.10", 83 | "resize-sensor": "^0.0.6", 84 | "seedrandom": "^3.0.5", 85 | "sobol": "^1.1.4", 86 | "splitpanes": "^2.2.1", 87 | "stylus": "^0.54.8", 88 | "stylus-loader": "^3.0.2", 89 | "svgpath": "^2.3.0", 90 | "typescript": "^3.9.7", 91 | "voronoi": "^1.0.0", 92 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.4", 93 | "vue-click-outside": "^1.1.0", 94 | "vue-color": "^2.7.1", 95 | "vue-composable": "^1.0.0-dev.37", 96 | "vue-js-modal": "^2.0.0-rc.6", 97 | "vue-markdown": "^2.2.4", 98 | "vue-popperjs": "^2.3.0", 99 | "vue-resize": "^0.5.0", 100 | "vue-template-compiler": "^2.6.11", 101 | "vuedraggable": "^2.24.0", 102 | "webpack-cli": "^3.3.12", 103 | "worker-plugin": "^4.0.3", 104 | "yargs": "^15.4.1" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/@types/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-click-outside' { 2 | import {DirectiveFunction} from 'vue' 3 | const VueClickOutside: DirectiveFunction 4 | 5 | export default VueClickOutside 6 | } 7 | 8 | declare module 'seedrandom' { 9 | export default function (initialSeed: any): () => number 10 | } 11 | 12 | declare module 'dateformat' { 13 | export default function (format: string): string 14 | } 15 | 16 | declare module 'bezier-js' { 17 | interface Point { 18 | x: number 19 | y: number 20 | } 21 | 22 | interface BBoxDimension { 23 | min: number 24 | max: number 25 | mid: number 26 | size: number 27 | } 28 | 29 | interface BBox { 30 | x: BBoxDimension 31 | y: BBoxDimension 32 | } 33 | 34 | export default class Bezier { 35 | constructor(points: Point[]) 36 | 37 | points: Point[] 38 | 39 | offset(d: number): Bezier[] 40 | split(t1: number, t2?: number): Bezier 41 | length(): number 42 | get(t: number): Point 43 | normal(t: number): Point 44 | bbox(): BBox 45 | } 46 | } 47 | 48 | declare module 'sprintf-js' { 49 | export function vsprintf(format: string, args: any[]): string 50 | } 51 | 52 | declare module 'resize-sensor' { 53 | export default class ResizeSensor { 54 | constructor(el: HTMLElement, callback: () => any) 55 | public detach(): any 56 | } 57 | } 58 | 59 | declare module 'vue-markdown' { 60 | export const VueMarkdown: any 61 | export default VueMarkdown 62 | } 63 | 64 | declare module 'vue-color' { 65 | export const Chrome: any 66 | } 67 | 68 | declare module 'vue-popperjs' { 69 | const Popper: any 70 | export default Popper 71 | } 72 | 73 | declare module 'is-node' { 74 | const isNode: boolean 75 | export default isNode 76 | } 77 | 78 | declare module 'canvas2svg' { 79 | export default class Canvas2Svg extends CanvasRenderingContext2D { 80 | constructor(width: number, height: number) 81 | public getSerializedSvg(flag: boolean): string 82 | } 83 | } 84 | 85 | declare module 'hull.js' { 86 | export default function hull( 87 | points: [number, number][], 88 | concavity?: number 89 | ): [number, number][] 90 | } 91 | 92 | declare module 'voronoi' { 93 | export interface BBox { 94 | xl: number 95 | xr: number 96 | yt: number 97 | yb: number 98 | } 99 | 100 | export interface Vertex { 101 | x: number 102 | y: number 103 | } 104 | 105 | export interface Site extends Vertex { 106 | voronoiId: number 107 | } 108 | 109 | export interface Edge { 110 | va: Vertex 111 | vb: Vertex 112 | } 113 | 114 | export interface Cell { 115 | closedMe: boolean 116 | halfedges: { 117 | site: Vertex 118 | edge: Edge 119 | }[] 120 | site: Site 121 | } 122 | 123 | export interface Diagram { 124 | cells: Cell[] 125 | edges: Edge[] 126 | } 127 | 128 | export default class Voronoi { 129 | public compute(sites: Vertex[], bbox: BBox): Diagram 130 | } 131 | } 132 | 133 | declare module 'delaunator' { 134 | export default class Delaunator { 135 | public triangles: number[] 136 | static from(points: [number, number][]): Delaunator 137 | } 138 | } 139 | 140 | declare module 'css-color-names' { 141 | const csscolor: {[name: string]: string} 142 | export default csscolor 143 | } 144 | 145 | declare module 'splitpanes' { 146 | export const Splitpanes: any 147 | export const Pane: any 148 | } 149 | 150 | declare module 'gif.js' { 151 | export default class GIF { 152 | constructor(options: any) 153 | 154 | public addFrame(image: HTMLImageElement, options: any): void 155 | public on(type: string, callback: (blob: Blob) => any): void 156 | public render(): void 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-modes.ts: -------------------------------------------------------------------------------- 1 | import ConsoleScope from '@/scopes/console' 2 | import {convertMalNodeToJSObject} from '@/mal/reader' 3 | import {ref, Ref, computed, watch, markRaw} from '@vue/composition-api' 4 | import {MalAtom, MalMap, assocBang, keywordFor, isMap} from '@/mal/types' 5 | import {mat2d, vec2} from 'gl-matrix' 6 | import useMouseEvents from '@/components/use/use-mouse-events' 7 | import AppScope from '@/scopes/app' 8 | import {useKeyboardState} from '@/components/use' 9 | import {getHTMLElement} from '@/utils' 10 | 11 | const K_EVENT_TYPE = keywordFor('event-type') 12 | const K_POS = keywordFor('pos') 13 | const K_MOUSE_PRESSED = keywordFor('mouse-pressed') 14 | 15 | interface Mode { 16 | name: string 17 | handlers: { 18 | label: string 19 | icon: {type: 'character' | 'fontawesome'; value: string} 20 | setup?: () => MalMap 21 | move?: (state: MalMap) => MalMap 22 | press?: (state: MalMap) => MalMap 23 | drag?: (state: MalMap) => MalMap 24 | release?: (state: MalMap) => MalMap 25 | } 26 | } 27 | 28 | export function useModes( 29 | handleEl: Ref, 30 | viewTransform: Ref 31 | ) { 32 | // Force enable keyboard state to retrieve modifiers 33 | useKeyboardState() 34 | 35 | const modes = ref( 36 | markRaw( 37 | convertMalNodeToJSObject( 38 | (ConsoleScope.var('*modes*') as MalAtom).value 39 | ) as Mode[] 40 | ) 41 | ) 42 | 43 | let state: MalMap 44 | 45 | const {mouseX, mouseY, mousePressed} = useMouseEvents(handleEl, { 46 | onMove: () => executeMouseHandler('move'), 47 | onDown: () => executeMouseHandler('press'), 48 | onDrag: () => executeMouseHandler('drag'), 49 | onUp: () => executeMouseHandler('release'), 50 | ignorePredicate(e: MouseEvent) { 51 | const root = getHTMLElement(handleEl) 52 | if (!root) return true 53 | const target = e.target as HTMLElement 54 | // NOTE: Makeshift 55 | const svg = root.children[1] 56 | return target !== svg && svg.contains(target) 57 | }, 58 | }) 59 | 60 | const activeModeIndex: Ref = ref(0) 61 | 62 | const activeMode = computed(() => 63 | activeModeIndex.value !== undefined 64 | ? markRaw(modes.value[activeModeIndex.value]) 65 | : undefined 66 | ) 67 | 68 | const pos = computed(() => { 69 | const pos = vec2.fromValues(mouseX.value, mouseY.value) 70 | vec2.transformMat2d( 71 | pos, 72 | pos, 73 | mat2d.invert(mat2d.create(), viewTransform.value) 74 | ) 75 | return pos 76 | }) 77 | 78 | function executeMouseHandler(type: 'move' | 'press' | 'drag' | 'release') { 79 | if (!activeMode.value) return 80 | 81 | const handler = activeMode.value.handlers[type] 82 | if (handler) { 83 | const params = assocBang( 84 | state, 85 | K_EVENT_TYPE, 86 | type, 87 | K_POS, 88 | pos.value, 89 | K_MOUSE_PRESSED, 90 | mousePressed.value 91 | ) 92 | const updatedState = handler(params) 93 | if (isMap(updatedState)) { 94 | state = updatedState 95 | } 96 | } 97 | } 98 | 99 | // Execute setup 100 | AppScope.def('reset-mode', () => { 101 | if (activeMode.value) { 102 | state = activeMode.value.handlers.setup 103 | ? activeMode.value.handlers.setup() 104 | : ({} as MalMap) 105 | return true 106 | } else { 107 | return false 108 | } 109 | }) 110 | 111 | watch( 112 | () => activeMode.value, 113 | mode => { 114 | if (mode) { 115 | state = mode.handlers.setup ? mode.handlers.setup() : ({} as MalMap) 116 | } 117 | }, 118 | {immediate: true} 119 | ) 120 | 121 | return { 122 | modes, 123 | activeModeIndex, 124 | activeMode, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import {app, protocol, BrowserWindow} from 'electron' 2 | import {platform} from 'os' 3 | import { 4 | createProtocol, 5 | installVueDevtools, 6 | } from 'vue-cli-plugin-electron-builder/lib' 7 | const isDevelopment = process.env.NODE_ENV !== 'production' 8 | 9 | // Keep a global reference of the window object, if you don't, the window will 10 | // be closed automatically when the JavaScript object is garbage collected. 11 | let win: BrowserWindow 12 | 13 | // Scheme must be registered before the app is ready 14 | protocol.registerSchemesAsPrivileged([ 15 | {scheme: 'app', privileges: {secure: true, standard: true}}, 16 | ]) 17 | 18 | function createWindow() { 19 | // Create the browser window. 20 | let options = { 21 | width: 1280, 22 | height: 720, 23 | frame: false, 24 | backgroundColor: '#FFF', 25 | webPreferences: { 26 | // Use pluginOptions.nodeIntegration, leave this alone 27 | // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info 28 | // nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION 29 | nodeIntegration: true, 30 | nodeIntegrationInWorker: true, 31 | enableRemoteModule: true, 32 | spellcheck: false, 33 | }, 34 | } as Electron.BrowserWindowConstructorOptions 35 | 36 | options = 37 | platform() === 'darwin' 38 | ? (options = {...options, titleBarStyle: 'hiddenInset'}) 39 | : (options = {...options, frame: false}) 40 | win = new BrowserWindow(options) 41 | 42 | if (process.env.WEBPACK_DEV_SERVER_URL) { 43 | // Load the url of the dev server if in development mode 44 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string) 45 | if (!process.env.IS_TEST) { 46 | win.webContents.openDevTools() 47 | } 48 | } else { 49 | createProtocol('app') 50 | // Load the index.html when not in development 51 | win.loadURL('app://./index.html') 52 | } 53 | 54 | win.on('rotate-gesture', (e, rotation) => { 55 | win.webContents.send('rotate-gesture', rotation) 56 | }) 57 | } 58 | 59 | // Quit when all windows are closed. 60 | app.on('window-all-closed', () => { 61 | app.quit() 62 | }) 63 | 64 | app.on('activate', () => { 65 | // On macOS it's common to re-create a window in the app when the 66 | // dock icon is clicked and there are no other windows open. 67 | if (!win) { 68 | createWindow() 69 | } 70 | }) 71 | 72 | // This method will be called when Electron has finished 73 | // initialization and is ready to create browser windows. 74 | // Some APIs can only be used after this event occurs. 75 | app.on('ready', async () => { 76 | if (isDevelopment && !process.env.IS_TEST) { 77 | // Install Vue Devtools 78 | // Devtools extensions are broken in Electron 6.0.0 and greater 79 | // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info 80 | // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode 81 | // If you are not using Windows 10 dark mode, you may uncomment these lines 82 | // In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines 83 | try { 84 | await installVueDevtools() 85 | } catch (e) { 86 | console.error('Vue Devtools failed to install:', e.toString()) 87 | } 88 | } 89 | createWindow() 90 | }) 91 | 92 | // Exit cleanly on request from parent process in development mode. 93 | if (isDevelopment) { 94 | if (process.platform === 'win32') { 95 | process.on('message', data => { 96 | if (data === 'graceful-exit') { 97 | app.quit() 98 | } 99 | }) 100 | } else { 101 | process.on('SIGTERM', () => { 102 | app.quit() 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/components/GlobalMenu/GlobalMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | '(GLISP) 7 | 8 | 17 | {{ label }} 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 94 | 95 | 151 | -------------------------------------------------------------------------------- /export-refs.glisp: -------------------------------------------------------------------------------- 1 | ;; Template would be like below: 2 | 3 | ;; ###path/rect 4 | 5 | ;; Generates a rect path 6 | 7 | ;; - **Alias:** `rect `- **Parameters:** 8 | 9 | ;; | Name | Type | Description | 10 | ;; | ---- | ------ | :--------------------------------------------- | 11 | ;; | Pos | `vec2 `| coordinate of top-left corner of the rectangle | 12 | ;; | Size | `vec2 `| size of the rectangle | 13 | 14 | (defn gen-param-column [idx param variadic] 15 | (if (= param &) 16 | nil 17 | (format "| %-8s | %-9s | %-12s |\n" 18 | (format "%s%s" 19 | (if variadic "& " "") 20 | (get param :label (format "%%%d" idx))) 21 | (str "`" (get param :type "") "`") 22 | (get param :desc "")))) 23 | 24 | (defn gen-param-table [params] 25 | (if (not (sequential? params)) 26 | nil 27 | (if (sequential? (first params)) 28 | ;; Multi arity function 29 | (apply str 30 | (map #(->> % 31 | (gen-param-table) 32 | (format "%s\n")) 33 | params)) 34 | 35 | ;; Generates Table 36 | (str 37 | "| Name | Type | Description |\n" 38 | "| -------- | --------- | :----------- |\n" 39 | (apply 40 | str 41 | (remove nil? 42 | (map-indexed 43 | (fn [i p] 44 | (gen-param-column 45 | i p (and (> i 0) 46 | (= & (nth params (dec i)))))) 47 | params))) 48 | "\n")))) 49 | 50 | (defn upper-case [text] 51 | (js-eval (format "'%s'.toUpperCase()" text))) 52 | 53 | (defn capital-case [text] 54 | (if (= 1 (count text)) 55 | text 56 | (str (upper-case (subs text 0 1)) 57 | (subs text 1)))) 58 | 59 | (defn gen-doc [sym m f] 60 | (apply 61 | str 62 | 63 | (remove 64 | nil? 65 | 66 | [(format "### %s\n\n" (name sym)) 67 | 68 | (if (contains? m :doc) 69 | (format "%s\n\n" (get m :doc))) 70 | 71 | ;; Generate parameter table 72 | (let [fparams (fn-params f) 73 | params (or (get m :params) 74 | (map #(hash-map) fparams)) 75 | params (if (and fparams (>= (count fparams) (count params))) 76 | (for [p params :index i] 77 | (if (map? p) 78 | (apply hash-map 79 | (apply concat 80 | [:label (capital-case 81 | (print-str (nth fparams i)))] 82 | (entries p))) 83 | p)) 84 | params)] 85 | (if (pos? (count params)) 86 | (str 87 | "**Parameter**\n\n" 88 | (gen-param-table params)))) 89 | 90 | (if (contains? m :return) 91 | (format "**Returns**: `%s`\n\n" (get (get m :return) :type))) 92 | 93 | (if (contains? m :alias) 94 | (format "**Alias:** `%s`\n\n" (get m :alias)))]))) 95 | 96 | 97 | 98 | (def md (->> (get-all-symbols) 99 | (sort) 100 | (map #(vector % (eval %))) 101 | (map #(vector (first %) ; symbol 102 | (meta (second %)) ; meta 103 | (second %))) ; function reference 104 | (remove #(nil? (second %))) ; Delete functions with no metadata 105 | (remove #(get (second %) :alias-for)) ; Delete aliased functions 106 | (map #(apply gen-doc %)))) 107 | 108 | (def txt (join "\n" md)) 109 | 110 | (spit "docs/ref.md" txt) -------------------------------------------------------------------------------- /src/components/dialogs/DialogSettings.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Settings 5 | Reset 6 | 7 | 8 | 9 | {{ hasParseError ? '!' : '✓' }} 13 | 14 | 15 | Cancel 16 | Update 17 | 18 | 19 | 20 | 21 | 75 | 76 | 146 | -------------------------------------------------------------------------------- /src/components/dialogs/DialogCommand.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ fnName }} 6 | 11 | 12 | 13 | 14 | 15 | Cancel 16 | Execute 17 | 18 | 19 | 20 | 21 | 95 | 96 | 157 | -------------------------------------------------------------------------------- /src/components/PageIndex/use/use-hit-detector/hit-detector.ts: -------------------------------------------------------------------------------- 1 | import {vec2} from 'gl-matrix' 2 | import { 3 | MalVal, 4 | getEvaluated, 5 | isVector, 6 | keywordFor as K, 7 | isList, 8 | MalSeq, 9 | isKeyword, 10 | MalMap, 11 | isSymbol, 12 | } from '@/mal/types' 13 | import {PathType, convertToPath2D} from '@/path-utils' 14 | import {getUIBodyExp} from '@/mal/utils' 15 | 16 | const K_PATH = K('path'), 17 | K_TRANSFORM = K('transform'), 18 | K_STYLE = K('style'), 19 | K_FILL = K('fill'), 20 | K_STROKE = K('stroke'), 21 | K_STROKE_WIDTH = K('stroke-width') 22 | 23 | interface HitStyle { 24 | fill: boolean 25 | stroke: false | number 26 | } 27 | 28 | export class HitDetector { 29 | private ctx: CanvasRenderingContext2D 30 | private cachedExp: MalVal = null 31 | private cachedPath2D = new WeakMap() 32 | 33 | constructor() { 34 | const canvas = document.createElement('canvas') 35 | const ctx = canvas.getContext('2d') 36 | if (!ctx) { 37 | throw new Error('Cannot initialize OfscreenCanvasRenderingContext2D') 38 | } 39 | this.ctx = ctx 40 | } 41 | 42 | private getPath2D(exp: MalSeq) { 43 | if (this.cachedPath2D.has(exp)) { 44 | return this.cachedPath2D.get(exp) as Path2D 45 | } else { 46 | const path = convertToPath2D(exp as PathType) 47 | this.cachedPath2D.set(exp, path) 48 | return path 49 | } 50 | } 51 | 52 | private analyzeVector(pos: vec2, exp: MalVal[], hitStyle: MalMap) { 53 | for (const child of exp.reverse()) { 54 | const ret = this.analyzeNode(pos, child, hitStyle) 55 | if (ret) { 56 | return ret 57 | } 58 | } 59 | return null 60 | } 61 | 62 | private analyzeNode(pos: vec2, exp: MalVal, hitStyle: MalMap): null | MalVal { 63 | exp = getUIBodyExp(exp) 64 | 65 | const evaluated = getEvaluated(exp) 66 | if (isVector(evaluated)) { 67 | const command = evaluated[0] 68 | 69 | switch (command) { 70 | case K_PATH: { 71 | const path = this.getPath2D(evaluated) 72 | const hasFill = !!hitStyle[K_FILL] 73 | const hasStroke = !!hitStyle[K_STROKE] 74 | if (hasFill) { 75 | if (this.ctx.isPointInPath(path, pos[0], pos[1])) { 76 | return exp 77 | } 78 | } 79 | if (hasStroke || (!hasFill && !hasStroke)) { 80 | const width = Math.max((hitStyle[K_STROKE_WIDTH] as number) || 0, 4) 81 | this.ctx.lineWidth = width 82 | if (this.ctx.isPointInStroke(path, pos[0], pos[1])) { 83 | return exp 84 | } 85 | } 86 | break 87 | } 88 | case K_TRANSFORM: { 89 | const [, xform] = evaluated 90 | const [, , ...body] = exp as MalSeq 91 | this.ctx.save() 92 | this.ctx.transform( 93 | ...(xform as [number, number, number, number, number, number]) 94 | ) 95 | const ret = this.analyzeVector(pos, body, hitStyle) 96 | this.ctx.restore() 97 | return ret 98 | } 99 | case K_STYLE: { 100 | const [, styles] = evaluated 101 | const [, , ...body] = exp as MalSeq 102 | let mergedStyles = {...hitStyle} 103 | for (const s of (isVector(styles) ? styles : [styles]) as MalMap[]) { 104 | mergedStyles = {...mergedStyles, ...s} 105 | } 106 | const ret = this.analyzeVector(pos, body, mergedStyles) 107 | // if (ret && body.length === 1) { 108 | // return exp 109 | // } else { 110 | // return ret 111 | // } 112 | return ret 113 | } 114 | default: 115 | if (isKeyword(command)) { 116 | const body = (exp as MalSeq).slice(1) 117 | return this.analyzeVector(pos, body, hitStyle) 118 | } 119 | } 120 | } else if (isList(exp)) { 121 | return this.analyzeVector(pos, exp.slice(1), hitStyle) 122 | } 123 | 124 | return null 125 | } 126 | 127 | public analyze(pos: vec2, exp: MalVal = this.cachedExp) { 128 | this.cachedExp = exp 129 | this.ctx.resetTransform() 130 | return this.analyzeNode(pos, exp, {}) 131 | } 132 | } 133 | --------------------------------------------------------------------------------