├── .gitignore ├── LICENSE ├── README.md ├── core ├── actionStack.ts ├── actions.ts ├── assetLoader.ts ├── draw.ts ├── ease.ts ├── file.ts ├── fonts.ts ├── headless │ ├── component.ts │ ├── font.ts │ ├── index.ts │ ├── presentation.ts │ ├── step.ts │ └── transformable.ts ├── index.ts ├── interfaces │ ├── common.ts │ ├── component.ts │ ├── file.ts │ ├── font.ts │ ├── index.ts │ ├── path.ts │ ├── presentation.ts │ ├── step.ts │ └── transformable.ts ├── math.ts ├── module.ts ├── raycaster.ts ├── renderer.ts ├── server.ts ├── sly │ ├── decoder.ts │ ├── encoder.ts │ ├── headlessDecoder.ts │ ├── index.ts │ └── types.ts ├── stepbar.ts ├── sync │ ├── common.ts │ ├── headlessSerializer.ts │ ├── index.ts │ ├── serializer │ │ ├── headless.ts │ │ ├── index.ts │ │ ├── serializers.ts │ │ ├── three.ts │ │ └── types.ts │ ├── sync.ts │ ├── threeSerializer.ts │ └── types.ts ├── three │ ├── component.ts │ ├── font.ts │ ├── group.ts │ ├── index.ts │ ├── presentation.ts │ └── step.ts └── ui.ts ├── electron ├── client.ts ├── main.ts ├── preload.ts ├── presentation.ts ├── protocol.ts ├── server.ts ├── static │ ├── index.html │ └── window.css └── window.ts ├── fontkit.d.ts ├── frontend ├── controls │ ├── icons.tsx │ ├── mapControl.tsx │ ├── orbitControl.tsx │ └── transformControl.tsx ├── dashboard │ └── index.tsx ├── editor │ ├── componentEditor.tsx │ ├── editor.tsx │ ├── index.tsx │ ├── player.tsx │ ├── stepEditor.tsx │ ├── thumbnail.scss │ ├── thumbnail.tsx │ ├── tour │ │ ├── arrow-orange.gif │ │ ├── arrow.tsx │ │ ├── index.tsx │ │ ├── mouse.tsx │ │ ├── steps.tsx │ │ └── tour.scss │ ├── widgets │ │ ├── color.tsx │ │ ├── file.tsx │ │ ├── font.tsx │ │ ├── index.tsx │ │ ├── size.tsx │ │ └── text.tsx │ └── worldEditor.tsx ├── gloabl.d.ts ├── index.tsx ├── ipc.ts ├── main.tsx ├── three.d.ts └── util.ts ├── gulpfile.js ├── gulpfile ├── deploy.js ├── module.js ├── packager.js ├── parcel.js ├── rm.js ├── rollup.js ├── server.js └── tester.js ├── icons ├── favicon.ico ├── favicon.png └── slye.png ├── mktemp.d.ts ├── modules └── slye │ ├── assets │ ├── emoji.ttf │ ├── homa.ttf │ ├── sahel.ttf │ └── shellia.ttf │ ├── components │ ├── picture.ts │ ├── text.ts │ └── video.ts │ └── main.ts ├── package.json ├── roadmap.txt ├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png └── 6.png ├── stats.d.ts ├── tests ├── headless.ts ├── sly.ts ├── test.html └── tests.ts ├── tsconfig.json ├── web ├── app.html ├── client.ts └── presentation.ts ├── website ├── assets │ ├── 3d.png │ ├── apple.png │ ├── bg.png │ ├── bg2.png │ ├── chrome.png │ ├── github.png │ ├── linux.png │ ├── modules.png │ ├── open-source.png │ └── windows.png ├── index.html ├── main.ts └── styles.scss └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Parcel cache 11 | .cache 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | .eslintcache 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 31 | node_modules 32 | app/node_modules 33 | 34 | # OSX 35 | .DS_Store 36 | 37 | # flow-typed 38 | flow-typed/npm/* 39 | !flow-typed/npm/module_vx.x.x.js 40 | 41 | # App packaged 42 | release 43 | app/main.prod.js 44 | app/main.prod.js.map 45 | app/renderer.prod.js 46 | app/renderer.prod.js.map 47 | app/style.css 48 | app/style.css.map 49 | dist 50 | dll 51 | main.js 52 | main.js.map 53 | 54 | .idea 55 | npm-debug.log.* 56 | tmp 57 | 58 | *.sm 59 | package-lock.json 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Parsa Ghadimi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slye 2 | 3 | Slye is a desktop application which helps users to create 3D presentations. 4 | 5 | | | | | 6 | | ----------------------------- | ----------------------------- | ----------------------------- | 7 | | | | | 8 | | | | | 9 | 10 | # Demo 11 | 12 | There is a [web demo](https://qti3e.github.io/slye/app.html) available if you 13 | want to try it first. 14 | (Not all of the functionalities are implemented in the demo.) 15 | 16 | ## Directory structure 17 | 18 | There are 3 main units in this project, `core`, `frontend` and `electron`. 19 | 20 | Although we are using electron, we are not using node integration in our 21 | frontend for security purposes and also we hope that one day we might be 22 | able to use the exact code to target for web browser. 23 | 24 | ### `core` 25 | 26 | This directory contains all the abstractions and the internal API to deal with a 27 | Slye presentation and modules. 28 | 29 | ### `frontend` 30 | 31 | User interface built using `React`, we are using Electron's IPC to communicate 32 | between frontend and the electron land (A.K.A server) at the moment. 33 | 34 | ## `electron` 35 | 36 | Codes for the main process. 37 | 38 | ## Producing a release version 39 | 40 | First build the app: 41 | 42 | ``` 43 | npx gulp 44 | # To build for linux 45 | npx gulp release:linux64 46 | # To build for windows 47 | npx gulp release:win32 48 | ``` 49 | 50 | ## Gulp tasks 51 | 52 | | Task | Description | 53 | | ----------------- | ------------------------------------------------------------- | 54 | | clean | Remove the dist directory. | 55 | | modules:slye | Build the default module. | 56 | | modules | Runs `modules:slye`. | 57 | | electron:main | Bundle electron's main process bundle. | 58 | | electron:preload | Bundle electron's preload script. | 59 | | electron:renderer | Bundle UI. | 60 | | electron:icons | Copy icons to the `dist` directory. | 61 | | electron | Runs `electron:*` | 62 | | package:win32 | Creates a binary release for Win32 using Electron packager. | 63 | | package:win32 | Creates a binary release for Linux64 using Electron packager. | 64 | | web | Build the website and the web demo. | 65 | | build:electron | Runs `clean`, `modules`, `electron` | 66 | | build:web | Runs `clean`, `modules`, `web` | 67 | | binary:all | Runs `build:electron` followed by `package:*` | 68 | | deploy | Build and deploy the website | 69 | | serve | Start a HTTP server on port 8080. | 70 | | test:bundle | Bundle the test files. | 71 | | test:run | Starts the tests. (server should be running) | 72 | | test | Start server, build tests and run tests. | 73 | 74 | ## TODO 75 | 76 | - [ ] Documents 77 | - [ ] End-user manuals 78 | - [ ] Tests 79 | - [ ] Templates 80 | 81 | ## Contributions 82 | 83 | Once you've cloned the repository locally you can start hacking. 84 | 85 | ``` 86 | git clone https://github.com/qti3e/Slye.git 87 | cd Slye 88 | # Install dependncies. 89 | yarn 90 | # Build preload and main process. 91 | npx gulp 92 | # Start the dev server. (Make sure port 1234 is free.) 93 | yarn dev 94 | ``` 95 | 96 | Sometimes `parcel` does not terminates and you have to kill it yourself: 97 | 98 | ``` 99 | kill $(ps aux | grep '[p]arcel' | awk '{print $2}') 100 | ``` 101 | -------------------------------------------------------------------------------- /core/actionStack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { 12 | ComponentBase, 13 | PresentationBase, 14 | StepBase, 15 | Transformable, 16 | PropValue, 17 | Vec3 18 | } from "./interfaces"; 19 | import { actions, Action, ActionTypes } from "./actions"; 20 | 21 | const LIMIT = 17; 22 | 23 | type Listener = (forward: boolean, action: string, data: any) => void; 24 | 25 | export type ForwardData< 26 | T extends keyof ActionTypes 27 | > = ActionTypes[T] extends Action ? R : never; 28 | 29 | export interface TakenAction { 30 | name: T; 31 | forwardData: any; 32 | backwardData: any; 33 | } 34 | 35 | export class ActionStack { 36 | private readonly actions: TakenAction[] = []; 37 | private cursor = -1; 38 | listener: Listener; 39 | 40 | constructor(readonly presentation: PresentationBase) {} 41 | 42 | private action( 43 | name: T, 44 | forwardData: ForwardData 45 | ): void { 46 | const backwardData = actions[name].forward( 47 | this.presentation, 48 | // TS is stupid. 49 | forwardData as any 50 | ); 51 | if (this.listener) this.listener(true, name, forwardData); 52 | const action: TakenAction = { 53 | name, 54 | forwardData, 55 | backwardData: backwardData 56 | }; 57 | // Now push the action to the stack. 58 | this.cursor += 1; 59 | this.actions.splice(this.cursor, Infinity, action); 60 | if (this.actions.length > LIMIT) { 61 | const count = this.actions.length - LIMIT; 62 | this.actions.splice(0, count); 63 | this.cursor -= count; 64 | } 65 | } 66 | 67 | undo(): void { 68 | if (this.cursor < 0) return; 69 | const takenAction = this.actions[this.cursor]; 70 | const action = actions[takenAction.name]; 71 | action.backward(this.presentation, takenAction.backwardData); 72 | this.cursor -= 1; 73 | if (this.listener) { 74 | this.listener(false, takenAction.name, takenAction.backwardData); 75 | } 76 | } 77 | 78 | redo(): void { 79 | if (this.cursor + 1 === this.actions.length) return; 80 | this.cursor += 1; 81 | const takenAction = this.actions[this.cursor]; 82 | const action = actions[takenAction.name]; 83 | action.forward(this.presentation, takenAction.forwardData); 84 | if (this.listener) { 85 | this.listener(true, takenAction.name, takenAction.forwardData); 86 | } 87 | } 88 | 89 | deleteStep(step: StepBase): void { 90 | this.action("DELETE_STEP", { 91 | step 92 | }); 93 | } 94 | 95 | deleteComponent(component: ComponentBase): void { 96 | this.action("DELETE_COMPONENT", { 97 | component 98 | }); 99 | } 100 | 101 | transform( 102 | mode: "translate" | "rotate" | "scale", 103 | object: Transformable, 104 | { x: prevX, y: prevY, z: prevZ }: Vec3, 105 | { x, y, z }: Vec3 106 | ): void { 107 | const action = 108 | mode === "translate" 109 | ? "UPDATE_POSITION" 110 | : mode === "rotate" 111 | ? "UPDATE_ROTATION" 112 | : "UPDATE_SCALE"; 113 | 114 | this.action(action, { 115 | object, 116 | x, 117 | y, 118 | z, 119 | prevX, 120 | prevY, 121 | prevZ 122 | }); 123 | } 124 | 125 | updateProps>( 126 | component: ComponentBase, 127 | patch: Partial 128 | ): void { 129 | this.action("UPDATE_PROPS", { 130 | component, 131 | patch 132 | }); 133 | } 134 | 135 | insertComponent(step: StepBase, component: ComponentBase): void { 136 | this.action("INSERT_COMPONENT", { 137 | step, 138 | component 139 | }); 140 | } 141 | 142 | insertStep(step: StepBase): void { 143 | this.action("INSERT_STEP", { 144 | step 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /core/actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { 12 | ComponentBase, 13 | PresentationBase, 14 | StepBase, 15 | Transformable, 16 | PropValue 17 | } from "./interfaces"; 18 | 19 | export interface Action { 20 | readonly forward: (presentation: PresentationBase, data: P) => T; 21 | readonly backward: (presentation: PresentationBase, data: T) => void; 22 | } 23 | 24 | type TransformForwardData = { 25 | object: Transformable; 26 | prevX: number; 27 | prevY: number; 28 | prevZ: number; 29 | x: number; 30 | y: number; 31 | z: number; 32 | }; 33 | 34 | type TransformBackwardData = { 35 | object: Transformable; 36 | prevX: number; 37 | prevY: number; 38 | prevZ: number; 39 | }; 40 | 41 | export interface ActionTypes { 42 | DELETE_STEP: Action<{ step: StepBase }, { step: StepBase; index: number }>; 43 | DELETE_COMPONENT: Action< 44 | { component: ComponentBase }, 45 | { component: ComponentBase; step: StepBase } 46 | >; 47 | UPDATE_POSITION: Action; 48 | UPDATE_ROTATION: Action; 49 | UPDATE_SCALE: Action; 50 | UPDATE_PROPS: Action< 51 | { component: ComponentBase; patch: Record }, 52 | { component: ComponentBase; patch: Record } 53 | >; 54 | INSERT_COMPONENT: Action< 55 | { step: StepBase; component: ComponentBase }, 56 | { component: ComponentBase } 57 | >; 58 | INSERT_STEP: Action<{ step: StepBase }, { step: StepBase }>; 59 | } 60 | 61 | export const actions: ActionTypes = { 62 | DELETE_STEP: { 63 | forward(presentation, { step }) { 64 | const index = presentation.steps.indexOf(step); 65 | if (index >= 0) presentation.del(step); 66 | return { step, index }; 67 | }, 68 | backward(presentation, { index, step }) { 69 | if (index < 0) return; 70 | presentation.add(step, index); 71 | } 72 | }, 73 | DELETE_COMPONENT: { 74 | forward(presentation, { component }) { 75 | const step = component.owner; 76 | step.del(component); 77 | return { step, component }; 78 | }, 79 | backward(presentation, { component, step }) { 80 | step.add(component); 81 | } 82 | }, 83 | UPDATE_POSITION: { 84 | forward(presentation, { object, prevX, prevY, prevZ, x, y, z }) { 85 | object.setPosition(x, y, z); 86 | return { object, prevX, prevY, prevZ }; 87 | }, 88 | backward(presentation, { object, prevX, prevY, prevZ }) { 89 | object.setPosition(prevX, prevY, prevZ); 90 | } 91 | }, 92 | UPDATE_ROTATION: { 93 | forward(presentation, { object, prevX, prevY, prevZ, x, y, z }) { 94 | object.setRotation(x, y, z); 95 | return { object, prevX, prevY, prevZ }; 96 | }, 97 | backward(presentation, { object, prevX, prevY, prevZ }) { 98 | object.setRotation(prevX, prevY, prevZ); 99 | } 100 | }, 101 | UPDATE_SCALE: { 102 | forward(presentation, { object, prevX, prevY, prevZ, x, y, z }) { 103 | object.setScale(x, y, z); 104 | return { object, prevX, prevY, prevZ }; 105 | }, 106 | backward(presentation, { object, prevX, prevY, prevZ }) { 107 | object.setScale(prevX, prevY, prevZ); 108 | } 109 | }, 110 | UPDATE_PROPS: { 111 | forward(presentation, { component, patch }) { 112 | const undoPatch: Record = {}; 113 | for (const key in patch) { 114 | const value = component.getProp(key); 115 | if (value !== patch[key]) { 116 | undoPatch[key] = value; 117 | } 118 | } 119 | component.patchProps(patch); 120 | return { component, patch: undoPatch }; 121 | }, 122 | backward(presentation, { component, patch }) { 123 | component.patchProps(patch); 124 | } 125 | }, 126 | INSERT_COMPONENT: { 127 | forward(presentation, { step, component }) { 128 | step.add(component); 129 | return { component }; 130 | }, 131 | backward(presentation, { component }) { 132 | const step = component.owner; 133 | step.del(component); 134 | } 135 | }, 136 | INSERT_STEP: { 137 | forward(presentation, { step }) { 138 | presentation.add(step); 139 | return { step }; 140 | }, 141 | backward(presentation, { step }) { 142 | presentation.del(step); 143 | } 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /core/assetLoader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export type AssetFetchFunction = (key: T) => Promise; 12 | 13 | /** 14 | * To store assets for either presentation or a module. 15 | * 16 | * For every asset we make a numeric ID so that we can 17 | * easily use it with WAsm. 18 | */ 19 | export class AssetLoader { 20 | /** 21 | * Data which is already fetched. 22 | */ 23 | private readonly data: Map = new Map(); 24 | 25 | /** 26 | * Pending promises. 27 | */ 28 | private readonly promises: Map> = new Map(); 29 | 30 | /** 31 | * Map asset keys to their id. 32 | */ 33 | private readonly key2id: Map = new Map(); 34 | 35 | /** 36 | * Asset Id -> Key 37 | */ 38 | private readonly id2key: Map = new Map(); 39 | 40 | /** 41 | * Last Used Id. 42 | */ 43 | private last_id = 0; 44 | 45 | /** 46 | * @param fetch Callback function that should be called when 47 | * an asset has to be loaded. 48 | */ 49 | constructor(private readonly fetch: AssetFetchFunction) {} 50 | 51 | /** 52 | * Load an asset - it does not actually fetch the asset. 53 | */ 54 | load(key: Key): number { 55 | if (this.key2id.has(key)) return this.key2id.get(key); 56 | const id = this.last_id++; 57 | this.key2id.set(key, id); 58 | this.id2key.set(id, key); 59 | return id; 60 | } 61 | 62 | alloc(ab: ArrayBuffer): number { 63 | const id = this.last_id++; 64 | this.data.set(id, ab); 65 | return id; 66 | } 67 | 68 | /** 69 | * Return the arraybuffer. 70 | */ 71 | async getData(index: number): Promise { 72 | if (this.data.has(index)) return this.data.get(index); 73 | if (this.promises.has(index)) return await this.promises.get(index); 74 | const key = this.id2key.get(index); 75 | const promise = this.fetch(key); 76 | this.promises.set(index, promise); 77 | const data = await promise; 78 | this.data.set(index, data); 79 | this.promises.delete(index); 80 | return data; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /core/draw.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Shape, ShapePath } from "three"; 12 | import { Glyph, PathCommandKind } from "./interfaces"; 13 | 14 | /** 15 | * Create a Three.js Shape from the given text layout. 16 | * 17 | * @param glyphs Set of glyphs. (Obtained by calling font.layout) 18 | * @param size Font size. 19 | * @return {Shape[]} 20 | */ 21 | export function generateShapes(glyphs: Glyph[], size = 25): Shape[] { 22 | const shapes: Shape[] = []; 23 | const paths = createPaths(glyphs, size); 24 | 25 | for (const p of paths) { 26 | Array.prototype.push.apply(shapes, p.toShapes(false, false)); 27 | } 28 | 29 | return shapes; 30 | } 31 | 32 | function createPaths(glyphs: Glyph[], size: number): ShapePath[] { 33 | const paths: ShapePath[] = []; 34 | let offsetX = 0; 35 | let offsetY = 0; 36 | 37 | for (const g of glyphs) { 38 | const ret = createPath(g, size, offsetX, offsetY); 39 | offsetX += ret.offsetX; 40 | paths.push(ret.path); 41 | // TODO(qti3e) Line break. 42 | // Maybe we should move this `offsetX += ` logic to font.layout. 43 | } 44 | 45 | return paths; 46 | } 47 | 48 | interface CreatePathResult { 49 | path: ShapePath; 50 | offsetX: number; 51 | } 52 | 53 | function createPath( 54 | g: Glyph, 55 | size: number, 56 | offsetX: number, 57 | offsetY: number 58 | ): CreatePathResult { 59 | const path = new ShapePath(); 60 | 61 | for (const u of g.path) { 62 | switch (u.command) { 63 | case PathCommandKind.MOVE_TO: 64 | u.x = u.x * size + offsetX; 65 | u.y = u.y * size + offsetY; 66 | path.moveTo(u.x, u.y); 67 | break; 68 | case PathCommandKind.LINE_TO: 69 | u.x = u.x * size + offsetX; 70 | u.y = u.y * size + offsetY; 71 | path.lineTo(u.x, u.y); 72 | break; 73 | case PathCommandKind.QUADRATIC_CURVE_TO: 74 | u.cpx = u.cpx * size + offsetX; 75 | u.cpy = u.cpy * size + offsetY; 76 | u.x = u.x * size + offsetX; 77 | u.y = u.y * size + offsetY; 78 | path.quadraticCurveTo(u.cpx, u.cpy, u.x, u.y); 79 | break; 80 | case PathCommandKind.BEZIER_CURVE_TO: 81 | u.cpx1 = u.cpx1 * size + offsetX; 82 | u.cpy1 = u.cpx1 * size + offsetY; 83 | u.cpx2 = u.cpx2 * size + offsetX; 84 | u.cpy2 = u.cpy2 * size + offsetY; 85 | u.x = u.x * size + offsetX; 86 | u.y = u.y * size + offsetY; 87 | path.bezierCurveTo(u.cpx1, u.cpy1, u.cpx2, u.cpy2, u.x, u.y); 88 | break; 89 | } 90 | } 91 | 92 | return { offsetX: g.advanceWidth * size, path }; 93 | } 94 | -------------------------------------------------------------------------------- /core/file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { FileBase } from "./interfaces"; 12 | import { fetchAsset, fetchModuleAsset } from "./server"; 13 | 14 | export class File implements FileBase { 15 | /** 16 | * Used for optimizations. 17 | */ 18 | readonly isSlyeFile = true; 19 | 20 | /** 21 | * Fetched data. 22 | */ 23 | private cache: ArrayBuffer; 24 | 25 | private urlCache: string; 26 | 27 | private blobURL: string; 28 | 29 | constructor( 30 | readonly owner: string, 31 | readonly uuid: string, 32 | readonly isModuleAsset: boolean 33 | ) {} 34 | 35 | /** 36 | * Fetch the file from the server. 37 | * 38 | * @returns {Promise} 39 | */ 40 | async load(): Promise { 41 | if (this.cache) return this.cache; 42 | const fetch = this.isModuleAsset ? fetchModuleAsset : fetchAsset; 43 | const ab = await fetch(this.owner, this.uuid); 44 | this.cache = ab; 45 | return ab; 46 | } 47 | 48 | async url(): Promise { 49 | if (this.blobURL) return this.blobURL; 50 | const ab = await this.load(); 51 | const blob = new Blob([ab]); 52 | const url = URL.createObjectURL(blob); 53 | this.blobURL = url; 54 | return url; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/fonts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { FontBase } from "./interfaces"; 12 | 13 | /** 14 | * List of all of the registered fonts. 15 | */ 16 | const fonts: FontBase[] = []; 17 | 18 | /** 19 | * Add a new font to the font registry. 20 | * 21 | * @param {FontBase} font Font object 22 | * @returns {void} 23 | */ 24 | export function registerFont(font: FontBase): void { 25 | fonts.push(font); 26 | } 27 | 28 | /** 29 | * Returns a list of all the registered fonts. 30 | * 31 | * @returns {FontBase[]} 32 | */ 33 | export function getFonts(): FontBase[] { 34 | return [...fonts]; 35 | } 36 | 37 | /** 38 | * Find and return a font by its name. 39 | * 40 | * @param {string} name Font name. 41 | * @returns {FontBase} 42 | */ 43 | export function getFont(name: string): FontBase { 44 | for (const font of fonts) if (font.name === name) return font; 45 | } 46 | -------------------------------------------------------------------------------- /core/headless/component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { ComponentBase, PropValue, ComponentProps } from "../interfaces"; 12 | import { HeadlessStep } from "./step"; 13 | import { TransformableImpl } from "./transformable"; 14 | 15 | export class HeadlessComponent extends TransformableImpl 16 | implements ComponentBase { 17 | readonly isSlyeComponent = true; 18 | owner: HeadlessStep; 19 | props: ComponentProps; 20 | 21 | constructor( 22 | readonly uuid: string, 23 | readonly moduleName: string, 24 | readonly componentName: string, 25 | props: Record 26 | ) { 27 | super(); 28 | this.props = props; 29 | } 30 | 31 | getProp(key: any): PropValue { 32 | return this.props[key]; 33 | } 34 | 35 | patchProps(patch: ComponentProps): void { 36 | this.props = { 37 | ...this.props, 38 | ...patch 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/headless/font.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { FontBase, Glyph } from "../interfaces"; 12 | import { File } from "../file"; 13 | 14 | export class HeadlessFont implements FontBase { 15 | readonly isSlyeFont = true; 16 | 17 | constructor(readonly name: string, readonly file: File) {} 18 | 19 | async layout(text: string): Promise { 20 | throw new Error("`layout` is not implemented for headless fonts"); 21 | return []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/headless/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export * from "./component"; 12 | export * from "./font"; 13 | export * from "./presentation"; 14 | export * from "./step"; 15 | -------------------------------------------------------------------------------- /core/headless/presentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { PresentationBase } from "../interfaces"; 12 | import { HeadlessStep } from "./step"; 13 | 14 | export class HeadlessPresentation implements PresentationBase { 15 | readonly isSlyePresentation = true; 16 | 17 | steps: HeadlessStep[] = []; 18 | 19 | constructor(readonly uuid: string) {} 20 | 21 | del(step: HeadlessStep): void { 22 | const index = this.steps.indexOf(step); 23 | if (index < 0) return; 24 | 25 | step.owner = undefined; 26 | this.steps.splice(index, 1); 27 | } 28 | 29 | add(step: HeadlessStep): void { 30 | if (step.owner && step.owner !== this) { 31 | step.owner.del(step); 32 | } 33 | 34 | step.owner = this; 35 | this.steps.push(step); 36 | } 37 | 38 | getStepId(step: HeadlessStep): number { 39 | return this.steps.indexOf(step); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/headless/step.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { StepBase } from "../interfaces"; 12 | import { HeadlessPresentation } from "./presentation"; 13 | import { HeadlessComponent } from "./component"; 14 | import { TransformableImpl } from "./transformable"; 15 | 16 | export class HeadlessStep extends TransformableImpl implements StepBase { 17 | readonly isSlyeStep = true; 18 | owner: HeadlessPresentation; 19 | components: HeadlessComponent[] = []; 20 | 21 | constructor(readonly uuid: string) { 22 | super(); 23 | } 24 | 25 | del(component: HeadlessComponent): void { 26 | const index = this.components.indexOf(component); 27 | if (index < 0) return; 28 | 29 | component.owner = undefined; 30 | this.components.splice(index, 1); 31 | } 32 | 33 | add(component: HeadlessComponent): void { 34 | if (component.owner && component.owner !== this) { 35 | component.owner.del(component); 36 | } 37 | 38 | component.owner = this; 39 | this.components.push(component); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/headless/transformable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Transformable, Vec3 } from "../interfaces"; 12 | 13 | export class TransformableImpl implements Transformable { 14 | private position: Vec3 = { x: 0, y: 0, z: 0 }; 15 | private rotation: Vec3 = { x: 0, y: 0, z: 0 }; 16 | private scale: Vec3 = { x: 1, y: 1, z: 1 }; 17 | 18 | setPosition(x: number, y: number, z: number): void { 19 | this.position.x = x; 20 | this.position.y = y; 21 | this.position.z = z; 22 | } 23 | 24 | setRotation(x: number, y: number, z: number): void { 25 | this.rotation.x = x; 26 | this.rotation.y = y; 27 | this.rotation.z = z; 28 | } 29 | 30 | setScale(x: number, y: number, z: number): void { 31 | this.scale.x = x; 32 | this.scale.y = y; 33 | this.scale.z = z; 34 | } 35 | 36 | getPosition(): Vec3 { 37 | const { x, y, z } = this.position; 38 | return { x, y, z }; 39 | } 40 | 41 | getRotation(): Vec3 { 42 | const { x, y, z } = this.rotation; 43 | return { x, y, z }; 44 | } 45 | 46 | getScale(): Vec3 { 47 | const { x, y, z } = this.scale; 48 | return { x, y, z }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export * from "./draw"; 12 | export * from "./ease"; 13 | export * from "./math"; 14 | export * from "./raycaster"; 15 | export * from "./renderer"; 16 | export * from "./ui"; 17 | export * from "./actions"; 18 | export * from "./actionStack"; 19 | export * from "./server"; 20 | export * from "./module"; 21 | export * from "./assetLoader"; 22 | export * from "./stepbar"; 23 | export * from "./file"; 24 | export * from "./fonts"; 25 | 26 | export * from "./headless"; 27 | export * from "./interfaces"; 28 | export * from "./three"; 29 | export * from "./sly"; 30 | export * from "./sync"; 31 | -------------------------------------------------------------------------------- /core/interfaces/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | /** 12 | * A 3D vector. 13 | */ 14 | export type Vec3 = { 15 | x: number; 16 | y: number; 17 | z: number; 18 | }; 19 | -------------------------------------------------------------------------------- /core/interfaces/component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Transformable } from "./transformable"; 12 | import { FontBase } from "./font"; 13 | import { StepBase } from "./step"; 14 | import { FileBase } from "./file"; 15 | 16 | /** 17 | * Any type that can be used as a prop value in a component. 18 | */ 19 | export type PropValue = string | number | boolean | FontBase | FileBase; 20 | 21 | /** 22 | * Properties of a component. 23 | */ 24 | export type ComponentProps = Record; 25 | 26 | /** 27 | * Basic informations and methods to represent a Slye Component. 28 | */ 29 | export interface ComponentBase extends Transformable { 30 | /** 31 | * Unique id for this step. 32 | */ 33 | readonly uuid: string; 34 | 35 | /** 36 | * Name of the module that provided this component. 37 | */ 38 | readonly moduleName: string; 39 | 40 | /** 41 | * Name of the component kind, it must be registered by the moduleName. 42 | */ 43 | readonly componentName: string; 44 | 45 | /** 46 | * Used for optimizations, you should never change this. 47 | */ 48 | readonly isSlyeComponent: true; 49 | 50 | /** 51 | * Current owner of this component. 52 | */ 53 | owner: StepBase; 54 | 55 | /** 56 | * Current props of this component. 57 | */ 58 | props: ComponentProps; 59 | 60 | // TODO(qti3e) We don't need this. 61 | getProp(key: any): PropValue; 62 | 63 | /** 64 | * Patch the given props to the current props. 65 | * 66 | * @param {ComponentProps} props Patch to be applied. 67 | * @returns {void} 68 | */ 69 | patchProps(props: ComponentProps): void; 70 | } 71 | -------------------------------------------------------------------------------- /core/interfaces/file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | type PresentationUUID = string; 12 | type ModuleName = string; 13 | 14 | /** 15 | * File represents a presentation file, like a picture or a video. 16 | */ 17 | export interface FileBase { 18 | /** 19 | * Used for optimizations, you should never change this. 20 | */ 21 | readonly isSlyeFile: true; 22 | 23 | /** 24 | * Whatever this file is a presentation file or a module asset. 25 | */ 26 | readonly isModuleAsset: boolean; 27 | 28 | /** 29 | * Unique id for this file. 30 | */ 31 | readonly uuid: string; 32 | 33 | /** 34 | * File provider. 35 | * If the file is a module asset it points to a module otherwise it's the 36 | * presentation id. 37 | */ 38 | readonly owner: PresentationUUID | ModuleName; 39 | 40 | /** 41 | * Fetch the file from the server. 42 | * 43 | * @returns {Promise} 44 | */ 45 | load(): Promise; 46 | 47 | /** 48 | * Returns a file URL. (Object URL) 49 | * 50 | * @returns {Promise} 51 | */ 52 | url(): Promise; 53 | } 54 | -------------------------------------------------------------------------------- /core/interfaces/font.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { FileBase } from "./file"; 12 | import { Path } from "./path"; 13 | 14 | /** 15 | * Slye fonts are objects that we can use as font files in 16 | * the rendering phase. 17 | */ 18 | export interface FontBase { 19 | /** 20 | * Used for optimizations, you should never change this. 21 | */ 22 | readonly isSlyeFont: true; 23 | 24 | /** 25 | * Font file. 26 | */ 27 | readonly file: FileBase; 28 | 29 | /** 30 | * Name of this font. 31 | */ 32 | readonly name: string; 33 | 34 | /** 35 | * Render a text and return an array of Glyphs. 36 | * This function is asynchronous as it might need to fetch the actual font 37 | * file from the server. 38 | * 39 | * @param {string} text The text which you want to render. 40 | * @returns {Promise} 41 | */ 42 | layout(text: string): Promise; 43 | } 44 | 45 | /** 46 | * Each Glyph is a Path and amount of width it'll consume. 47 | */ 48 | export interface Glyph { 49 | path: Path; 50 | advanceWidth: number; 51 | } 52 | -------------------------------------------------------------------------------- /core/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export * from "./presentation"; 12 | export * from "./step"; 13 | export * from "./component"; 14 | export * from "./transformable"; 15 | export * from "./common"; 16 | export * from "./font"; 17 | export * from "./path"; 18 | export * from "./file"; 19 | -------------------------------------------------------------------------------- /core/interfaces/path.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export type Path = PathCommand[]; 12 | 13 | export enum PathCommandKind { 14 | MOVE_TO, 15 | LINE_TO, 16 | QUADRATIC_CURVE_TO, 17 | BEZIER_CURVE_TO 18 | } 19 | 20 | export type PathCommand = 21 | | MoveToCommand 22 | | LineToCommand 23 | | QuadraticCurveToCommand 24 | | BezierCurveToCommand; 25 | 26 | export interface MoveToCommand { 27 | command: PathCommandKind.MOVE_TO; 28 | x: number; 29 | y: number; 30 | } 31 | 32 | export interface LineToCommand { 33 | command: PathCommandKind.LINE_TO; 34 | x: number; 35 | y: number; 36 | } 37 | 38 | export interface QuadraticCurveToCommand { 39 | command: PathCommandKind.QUADRATIC_CURVE_TO; 40 | x: number; 41 | y: number; 42 | cpx: number; 43 | cpy: number; 44 | } 45 | 46 | export interface BezierCurveToCommand { 47 | command: PathCommandKind.BEZIER_CURVE_TO; 48 | x: number; 49 | y: number; 50 | cpx1: number; 51 | cpy1: number; 52 | cpx2: number; 53 | cpy2: number; 54 | } 55 | -------------------------------------------------------------------------------- /core/interfaces/presentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { StepBase } from "./step"; 12 | 13 | /** 14 | * Basic informations and methods to represent a Slye Presentation. 15 | */ 16 | export interface PresentationBase { 17 | /** 18 | * Unique id for this presentation. 19 | */ 20 | readonly uuid: string; 21 | 22 | /** 23 | * Used for optimizations, you should never change this. 24 | */ 25 | readonly isSlyePresentation: true; 26 | 27 | /** 28 | * List of steps in this presentation. 29 | */ 30 | readonly steps: StepBase[]; 31 | 32 | /** 33 | * Remove the given step from this presentation. 34 | * 35 | * @param {StepBase} step Step you want to remove. 36 | * @returns {void} 37 | */ 38 | del(step: StepBase): void; 39 | 40 | /** 41 | * Insert the given step in the given offset, if `index` is not provided it 42 | * appends the step at the end of the list. 43 | * 44 | * @param {StepBase} step Step which you want to add into the presentation. 45 | * @param {number} index Index in the steps list. 46 | * @returns {void} 47 | */ 48 | add(step: StepBase, index?: number): void; 49 | } 50 | -------------------------------------------------------------------------------- /core/interfaces/step.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Transformable } from "./transformable"; 12 | import { PresentationBase } from "./presentation"; 13 | import { ComponentBase } from "./component"; 14 | 15 | /** 16 | * Basic informations to represent a Slye Step. (A.K.A Slide) 17 | */ 18 | export interface StepBase extends Transformable { 19 | /** 20 | * Unique id for this step. 21 | */ 22 | readonly uuid: string; 23 | 24 | /** 25 | * Used for optimizations, you should never change this. 26 | */ 27 | readonly isSlyeStep: true; 28 | 29 | /** 30 | * List of components that this step owns. 31 | */ 32 | readonly components: ComponentBase[]; 33 | 34 | /** 35 | * Presentation that owns this step. 36 | */ 37 | owner: PresentationBase; 38 | 39 | /** 40 | * Remove the given component from this step. 41 | * 42 | * @param {ComponentBase} component Component which you want to remove. 43 | * @returns {void} 44 | */ 45 | del(component: ComponentBase): void; 46 | 47 | /** 48 | * Add the given component to the step, removes the component from it's 49 | * current parent if there is one. 50 | * 51 | * @param {ComponentBase} component Component which you want to add into this 52 | * step. 53 | * @returns {void} 54 | */ 55 | add(component: ComponentBase): void; 56 | } 57 | -------------------------------------------------------------------------------- /core/interfaces/transformable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Vec3 } from "./common"; 12 | 13 | /** 14 | * A transformable object is any object that can be part of the 15 | * rendering space. (currently only Components and Steps) 16 | */ 17 | export interface Transformable { 18 | /** 19 | * Set the position. 20 | * 21 | * @param {number} x Value for the `x` axis. 22 | * @param {number} y Value for the `r` axis. 23 | * @param {number} z Value for the `z` axis. 24 | * @return {void} 25 | */ 26 | setPosition(x: number, y: number, z: number): void; 27 | 28 | /** 29 | * Set the orientation, values must be in radian. 30 | * 31 | * @param {number} x Value for the `x` axis. 32 | * @param {number} y Value for the `r` axis. 33 | * @param {number} z Value for the `z` axis. 34 | * @return {void} 35 | */ 36 | setRotation(x: number, y: number, z: number): void; 37 | 38 | /** 39 | * Set the scale factor. 40 | * 41 | * @param {number} x Value for the `x` axis. 42 | * @param {number} y Value for the `r` axis. 43 | * @param {number} z Value for the `z` axis. 44 | * @return {void} 45 | */ 46 | setScale(x: number, y: number, z: number): void; 47 | 48 | /** 49 | * Returns the current position as a Slye Vec3. 50 | * 51 | * @returns {Vec3} 52 | */ 53 | getPosition(): Vec3; 54 | 55 | /** 56 | * Returns the current orientation as a Slye Vec3. 57 | * 58 | * @returns {Vec3} 59 | */ 60 | getRotation(): Vec3; 61 | 62 | /** 63 | * Returns the current scale factors as a Slye Vec3. 64 | * 65 | * @returns {Vec3} 66 | */ 67 | getScale(): Vec3; 68 | } 69 | -------------------------------------------------------------------------------- /core/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as THREE from "three"; 12 | import { Vec3 } from "./interfaces"; 13 | import { ThreeStep } from "./three/step"; 14 | 15 | export interface CameraState { 16 | position: Vec3; 17 | rotation: Vec3; 18 | } 19 | 20 | /** 21 | * Computes the camera state to look at the given step. 22 | * 23 | * @param {ThreeStep} step 24 | * @param {THREE.PerspectiveCamera} camrea 25 | * @returns {CameraState} 26 | */ 27 | export function getCameraPosRotForStep( 28 | step: ThreeStep, 29 | camrea: THREE.PerspectiveCamera 30 | ): CameraState { 31 | const center: THREE.Vector3 = new THREE.Vector3(); 32 | const box3: THREE.Box3 = new THREE.Box3(); 33 | const targetVec: THREE.Vector3 = new THREE.Vector3(); 34 | const euler: THREE.Euler = new THREE.Euler(0, 0, 0, "XYZ"); 35 | 36 | const { fov, far, aspect } = camrea; 37 | const { x: rx, y: ry, z: rz } = step.getRotation(); 38 | const { x: sx, y: sy } = step.getScale(); 39 | const stepWidth = ThreeStep.width * sx; 40 | const stepHeight = ThreeStep.height * sy; 41 | 42 | const vFov = THREE.Math.degToRad(fov); 43 | const farHeight = 2 * Math.tan(vFov / 2) * far; 44 | const farWidth = farHeight * aspect; 45 | let distance = (far * stepWidth) / farWidth; 46 | const presentiveHeight = (stepHeight * far) / distance; 47 | if (presentiveHeight > farHeight) { 48 | distance = (far * stepHeight) / farHeight; 49 | } 50 | 51 | // Find camera's position. 52 | box3.setFromObject(step.group).getCenter(center); 53 | center.z = step.group.position.z; 54 | euler.set(rx, ry, rz); 55 | targetVec.set(0, 0, distance); 56 | targetVec.applyEuler(euler); 57 | targetVec.add(center); 58 | 59 | // Update the camera. 60 | const { x, y, z } = targetVec; 61 | 62 | return { 63 | position: { x, y, z }, 64 | rotation: { x: rx, y: ry, z: rz } 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /core/module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { generateShapes } from "./draw"; 12 | import { FontBase, PropValue, ComponentProps } from "./interfaces"; 13 | import { fetchModuleAsset, requestModule } from "./server"; 14 | import { ThreeComponent, Font } from "./three"; 15 | import { File } from "./file"; 16 | import uuidv1 from "uuid/v1"; 17 | 18 | const modulesTable: Map = (window.slyeModulesTable = 19 | window.slyeModulesTable || new Map()); 20 | 21 | /** 22 | * A module is a Slye extension that might provide a set of components, fonts, 23 | * template or other functionalities. 24 | */ 25 | export interface ModuleInterface { 26 | /** 27 | * Name of the module. 28 | */ 29 | readonly name: string; 30 | 31 | /** 32 | * Returns a new instance of the component. 33 | * 34 | * @param {string} name Name of the component. 35 | * @param {Record} props Properties. 36 | * @returns {Component} 37 | */ 38 | component( 39 | name: string, 40 | props: Record, 41 | id?: string 42 | ): ThreeComponent; 43 | 44 | /** 45 | * Returns a file object for the asset. 46 | * @param {string} name Name of the file. 47 | * @returns {File} 48 | */ 49 | file(name: string): File; 50 | 51 | /** 52 | * Initialize the module. 53 | * @returns {void} 54 | */ 55 | init(): void; 56 | } 57 | 58 | type ComponentClass = { 59 | new ( 60 | uuid: string, 61 | moduleName: string, 62 | componentName: string, 63 | props: ComponentProps 64 | ): ThreeComponent; 65 | }; 66 | 67 | type ModuleClass = { 68 | new (name: string): ModuleInterface; 69 | }; 70 | 71 | export abstract class Module implements ModuleInterface { 72 | private readonly components: Map = new Map(); 73 | private readonly files: Map = new Map(); 74 | readonly name: string; 75 | 76 | constructor(name: string) { 77 | this.name = name; 78 | } 79 | 80 | protected registerComponent(name: string, c: ComponentClass): void { 81 | this.components.set(name, c); 82 | } 83 | 84 | file(fileName: string): File { 85 | if (this.files.has(fileName)) return this.files.get(fileName); 86 | const file = new File(this.name, fileName, true); 87 | this.files.set(fileName, file); 88 | return file; 89 | } 90 | 91 | component( 92 | name: string, 93 | props: Record, 94 | id = uuidv1() 95 | ): ThreeComponent { 96 | const c = this.components.get(name); 97 | if (!c) 98 | throw new Error(`Component ${name} is not registered by ${this.name}.`); 99 | return new c(id, this.name, name, props); 100 | } 101 | 102 | abstract init(): Promise | void; 103 | } 104 | 105 | export function registerModule(name: string, m: ModuleClass): void { 106 | const instance = new m(name); 107 | instance.init(); 108 | modulesTable.set(name, instance); 109 | } 110 | 111 | /** 112 | * Load the given module, it uses server API to load module by its name. 113 | * 114 | * @param {string} name Name of the module. 115 | * @returns {Promise} 116 | */ 117 | export async function loadModule(name: string): Promise { 118 | if (modulesTable.has(name)) { 119 | return modulesTable.get(name); 120 | } 121 | await requestModule(name); 122 | return modulesTable.get(name); 123 | } 124 | 125 | /** 126 | * Returns a new instance of the component. 127 | * 128 | * @param {string} moduleName Name of the module which provides this component. 129 | * @param {string} componentName Name of the component. 130 | * @param {Record} props Properties for this instance. 131 | * @returns {Promise} 132 | */ 133 | export async function component( 134 | moduleName: string, 135 | componentName: string, 136 | props: Record = {}, 137 | id = uuidv1() 138 | ): Promise> { 139 | const m = await loadModule(moduleName); 140 | return m.component(componentName, props, id); 141 | } 142 | 143 | /** 144 | * Returns a module asset file. 145 | * 146 | * @param {string} moduleName Module which owns the font. 147 | * @param {string} fileName Name of the file. 148 | * @returns {Promise} 149 | */ 150 | export async function file( 151 | moduleName: string, 152 | fileName: string 153 | ): Promise { 154 | const m = await loadModule(moduleName); 155 | return m.file(fileName); 156 | } 157 | -------------------------------------------------------------------------------- /core/raycaster.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as THREE from "three"; 12 | import { ThreePresentation } from "./three/presentation"; 13 | import { ThreeStep } from "./three/step"; 14 | import { ThreeComponent } from "./three/component"; 15 | 16 | /** 17 | * Raycaster Implementation. 18 | */ 19 | export class Raycaster { 20 | /** 21 | * Mouse position in world coordinate. 22 | */ 23 | readonly mouse: THREE.Vector2 = new THREE.Vector2(); 24 | 25 | /** 26 | * Three.js backend. 27 | */ 28 | private readonly raycaster: THREE.Raycaster = new THREE.Raycaster(); 29 | 30 | /** 31 | * Raycaster Constructor. 32 | * 33 | * @param {ThreePresentation} presentation 34 | * @param {THREE.PerspectiveCamera} camera 35 | */ 36 | constructor( 37 | private readonly presentation: ThreePresentation, 38 | private readonly camera: THREE.PerspectiveCamera 39 | ) {} 40 | 41 | /** 42 | * Returns all of the intersections. 43 | * 44 | * @returns {THREE.Intersection[]} 45 | */ 46 | intersectInSteps(steps: ThreeStep[]): THREE.Intersection[] { 47 | this.raycaster.setFromCamera(this.mouse, this.camera); 48 | 49 | const intersects = this.raycaster.intersectObjects( 50 | steps.map(x => x.group), 51 | true 52 | ); 53 | 54 | return intersects; 55 | } 56 | 57 | /** 58 | * Search between intersections providing userData to the given filter. 59 | * 60 | * @param {THREE.Intersection[]} intersections 61 | * @param {Function} cb Filter. 62 | * @returns {T} 63 | */ 64 | findIntersectByUserData( 65 | intersections: THREE.Intersection[], 66 | cb: (userData: Record) => T 67 | ): T { 68 | if (intersections.length === 0) return undefined; 69 | 70 | let result: T; 71 | let tmp: T; 72 | let minDistance: number = Infinity; 73 | 74 | main_loop: for (const intersection of intersections) { 75 | if (intersection.distance > minDistance) continue; 76 | let current = intersection.object; 77 | tmp = undefined; 78 | // Search backward. (parents) 79 | for (; current; current = current.parent) { 80 | tmp = cb(current.userData); 81 | if (tmp) { 82 | result = tmp; 83 | minDistance = intersection.distance; 84 | continue main_loop; 85 | } 86 | } 87 | // Search frontward. (children) 88 | const notVisited = [...intersection.object.children]; 89 | while (notVisited.length) { 90 | const obj = notVisited.pop(); 91 | tmp = cb(obj.userData); 92 | if (tmp) { 93 | result = tmp; 94 | minDistance = intersection.distance; 95 | notVisited.length = 0; // Just for fun ;) 96 | continue main_loop; 97 | } 98 | // Cheek the children. 99 | notVisited.push(...obj.children); 100 | } 101 | } 102 | 103 | return result; 104 | } 105 | 106 | /** 107 | * Returns the intersected ThreeStep. 108 | * 109 | * @returns {ThreeStep} 110 | */ 111 | raycastStep(): ThreeStep { 112 | const tmp = this.intersectInSteps(this.presentation.steps); 113 | return this.findIntersectByUserData(tmp, ({ step }) => 114 | step instanceof ThreeStep ? step : undefined 115 | ); 116 | } 117 | 118 | /** 119 | * Returns the intersected ThreeComponent. 120 | * 121 | * @returns {ThreeComponent} 122 | */ 123 | raycastComponent(): ThreeComponent { 124 | const tmp = this.intersectInSteps(this.presentation.steps); 125 | return this.findIntersectByUserData(tmp, ({ component }) => 126 | component instanceof ThreeComponent ? component : undefined 127 | ); 128 | } 129 | 130 | /** 131 | * Returns the intersected ThreeComponent which is clickable. 132 | * 133 | * @param {ThreeStep} Current step to look into. 134 | * @returns {ThreeComponent} 135 | */ 136 | raycastClickableComponent(step: ThreeStep): ThreeComponent { 137 | const tmp = this.intersectInSteps([step]); 138 | return this.findIntersectByUserData(tmp, ({ component }) => 139 | component instanceof ThreeComponent && component.handleClick 140 | ? component 141 | : undefined 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /core/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { FileBase } from "./interfaces/file"; 12 | 13 | // TODO(qti3e) Move this file to binding.ts 14 | 15 | export interface Server { 16 | requestModule(moduleName: string): Promise; 17 | fetchModuleAsset(moduleName: string, assetKey: string): Promise; 18 | fetchAsset(presentationId: string, assetKey: string): Promise; 19 | getAssetURL(presentationId: string, assetKey: string): Promise; 20 | showFileDialog(presentationId: string): Promise; 21 | } 22 | 23 | let current_server: Server; 24 | 25 | export function setServer(s: Server): void { 26 | current_server = s; 27 | } 28 | 29 | export function requestModule(moduleName: string): Promise { 30 | return current_server.requestModule(moduleName); 31 | } 32 | 33 | export function fetchModuleAsset( 34 | moduleName: string, 35 | assetKey: string 36 | ): Promise { 37 | return current_server.fetchModuleAsset(moduleName, assetKey); 38 | } 39 | export function fetchAsset( 40 | presentationId: string, 41 | assetKey: string 42 | ): Promise { 43 | return current_server.fetchAsset(presentationId, assetKey); 44 | } 45 | 46 | export function showFileDialog(presentationId: string): Promise { 47 | return current_server.showFileDialog(presentationId); 48 | } 49 | 50 | export function getAssetURL( 51 | presentationId: string, 52 | assetKey: string 53 | ): Promise { 54 | return current_server.getAssetURL(presentationId, assetKey); 55 | } 56 | -------------------------------------------------------------------------------- /core/sly/decoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { PropValue } from "../interfaces"; 12 | import { ThreePresentation, ThreeStep, ThreeComponent, Font } from "../three"; 13 | import { getFont as getRegFont } from "../fonts"; 14 | import { File } from "../file"; 15 | import { file, component } from "../module"; 16 | import { RefKind, JSONPresentation, DecoderOptions } from "./types"; 17 | 18 | /** 19 | * Read a Slye presentation from a raw-object. 20 | * 21 | * @param {ThreePresentation} presentation An empty presentation instance. 22 | * @param {JSONPresentation} o 23 | * @returns {Promise} 24 | */ 25 | export async function sly( 26 | presentation: ThreePresentation, 27 | o: JSONPresentation, 28 | options: DecoderOptions = {} 29 | ): Promise { 30 | const filesMap: Map = new Map(); 31 | const fontsMap: Map = new Map(); 32 | 33 | function getFile(uuid: string, moduleName: string): Promise { 34 | const isModuleAsset = !!moduleName; 35 | if (isModuleAsset) return file(moduleName, uuid) as Promise; 36 | if (filesMap.has(uuid)) return Promise.resolve(filesMap.get(uuid)); 37 | const newFile = new File(presentation.uuid, uuid, isModuleAsset); 38 | filesMap.set(uuid, newFile); 39 | return Promise.resolve(newFile); 40 | } 41 | 42 | function getFont(name: string, file: File): Font { 43 | const regFont = getRegFont(name); 44 | if (regFont && regFont.file === file) { 45 | return regFont as Font; 46 | } 47 | if (!fontsMap.has(file)) fontsMap.set(file, []); 48 | const fonts = fontsMap.get(file); 49 | for (const font of fonts) { 50 | if (font.name === name) { 51 | return font; 52 | } 53 | } 54 | const font = new Font(name, file); 55 | fontsMap.set(file, [...fonts, font]); 56 | return font; 57 | } 58 | 59 | if (o.template) { 60 | //const tem = await component(o.template.moduleName, o.template.component); 61 | //presentation.setTemplate(tem); 62 | } 63 | 64 | for (let uuid in o.steps) { 65 | const jstep = o.steps[uuid]; 66 | 67 | const step = new ThreeStep(uuid); 68 | step.setPosition(jstep.position[0], jstep.position[1], jstep.position[2]); 69 | step.setRotation(jstep.rotation[0], jstep.rotation[1], jstep.rotation[2]); 70 | step.setScale(jstep.scale[0], jstep.scale[1], jstep.scale[2]); 71 | 72 | for (let j = 0; j < jstep.components.length; ++j) { 73 | const jcom = jstep.components[j]; 74 | const props: Record = {}; 75 | 76 | for (const key in jcom.props) { 77 | const jvalue = jcom.props[key]; 78 | if (typeof jvalue === "number" || typeof jvalue === "string") { 79 | props[key] = jvalue; 80 | } else if (jvalue.kind === RefKind.FILE) { 81 | props[key] = await getFile(jvalue.uuid, jvalue.moduleId); 82 | } else if (jvalue.kind === RefKind.FONT) { 83 | const file = await getFile(jvalue.fileUUID, jvalue.moduleId); 84 | props[key] = getFont(jvalue.font, file); 85 | } 86 | } 87 | 88 | const com = await component( 89 | jcom.moduleName, 90 | jcom.component, 91 | props, 92 | jcom.uuid 93 | ); 94 | com.setPosition(jcom.position[0], jcom.position[1], jcom.position[2]); 95 | com.setRotation(jcom.rotation[0], jcom.rotation[1], jcom.rotation[2]); 96 | com.setScale(jcom.scale[0], jcom.scale[1], jcom.scale[2]); 97 | 98 | step.add(com); 99 | if (options.onComponent) options.onComponent(com); 100 | } 101 | 102 | presentation.add(step); 103 | if (options.onStep) options.onStep(step); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /core/sly/encoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { PresentationBase, FontBase, FileBase } from "../interfaces"; 12 | import { 13 | JSONPresentation, 14 | JSONPresentationStep, 15 | JSONPresentationComponent, 16 | RefKind 17 | } from "./types"; 18 | 19 | export function encode(presentation: PresentationBase): JSONPresentation { 20 | const ret: JSONPresentation = { 21 | template: undefined, // TODO(qti3e) 22 | steps: {} 23 | }; 24 | 25 | for (const step of presentation.steps) { 26 | const { x: px, y: py, z: pz } = step.getPosition(); 27 | const { x: rx, y: ry, z: rz } = step.getRotation(); 28 | const { x: sx, y: sy, z: sz } = step.getScale(); 29 | 30 | const jstep: JSONPresentationStep = { 31 | position: [px, py, pz], 32 | rotation: [rx, ry, rz], 33 | scale: [sx, sy, sz], 34 | components: [] 35 | }; 36 | 37 | for (const component of step.components) { 38 | const { x: px, y: py, z: pz } = component.getPosition(); 39 | const { x: rx, y: ry, z: rz } = component.getRotation(); 40 | const { x: sx, y: sy, z: sz } = component.getScale(); 41 | 42 | const jcomp: JSONPresentationComponent = { 43 | uuid: component.uuid, 44 | moduleName: component.moduleName, 45 | component: component.componentName, 46 | position: [px, py, pz], 47 | rotation: [rx, ry, rz], 48 | scale: [sx, sy, sz], 49 | props: {} 50 | }; 51 | 52 | for (const key in component.props) { 53 | const value = component.props[key]; 54 | if (typeof value === "string" || typeof value === "number") { 55 | jcomp.props[key] = value; 56 | } else if (isFont(value)) { 57 | jcomp.props[key] = { 58 | kind: RefKind.FONT, 59 | font: value.name, 60 | fileUUID: value.file.uuid, 61 | moduleId: value.file.isModuleAsset ? value.file.owner : undefined 62 | }; 63 | } else if (isFile(value)) { 64 | jcomp.props[key] = { 65 | kind: RefKind.FILE, 66 | uuid: value.uuid, 67 | moduleId: value.isModuleAsset ? value.owner : undefined 68 | }; 69 | } else { 70 | throw new Error(`Encoder for ${value} is not implemented yet.`); 71 | } 72 | } 73 | 74 | jstep.components.push(jcomp); 75 | } 76 | 77 | ret.steps[step.uuid] = jstep; 78 | } 79 | 80 | return ret; 81 | } 82 | 83 | function isFont(value: any): value is FontBase { 84 | if (typeof value !== "object") return false; 85 | return !!value.isSlyeFont; 86 | } 87 | 88 | function isFile(value: any): value is FileBase { 89 | if (typeof value !== "object") return false; 90 | return !!value.isSlyeFile; 91 | } 92 | -------------------------------------------------------------------------------- /core/sly/headlessDecoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { 12 | HeadlessPresentation, 13 | HeadlessFont, 14 | HeadlessComponent, 15 | HeadlessStep 16 | } from "../headless"; 17 | import { RefKind, JSONPresentation, DecoderOptions } from "./types"; 18 | import { File } from "../file"; 19 | import { PropValue } from "../interfaces"; 20 | 21 | export function headlessDecode( 22 | presentation: HeadlessPresentation, 23 | o: JSONPresentation, 24 | options: DecoderOptions = {} 25 | ): void { 26 | const filesMap: Map = new Map(); 27 | const fontsMap: Map = new Map(); 28 | 29 | function getFile(uuid: string, moduleName: string): File { 30 | const key = `${uuid}-${moduleName}`; 31 | if (filesMap.has(key)) return filesMap.get(key); 32 | const isModuleAsset = !!moduleName; 33 | const owner = isModuleAsset ? moduleName : presentation.uuid; 34 | const file = new File(owner, uuid, isModuleAsset); 35 | filesMap.set(key, file); 36 | return file; 37 | } 38 | 39 | function getFont(name: string, file: File): HeadlessFont { 40 | if (!fontsMap.has(file)) fontsMap.set(file, []); 41 | const fonts = fontsMap.get(file); 42 | for (const font of fonts) { 43 | if (font.name === name) { 44 | return font; 45 | } 46 | } 47 | const font = new HeadlessFont(name, file); 48 | fontsMap.set(file, [...fonts, font]); 49 | return font; 50 | } 51 | 52 | for (let uuid in o.steps) { 53 | const jstep = o.steps[uuid]; 54 | 55 | const step = new HeadlessStep(uuid); 56 | step.setPosition(jstep.position[0], jstep.position[1], jstep.position[2]); 57 | step.setRotation(jstep.rotation[0], jstep.rotation[1], jstep.rotation[2]); 58 | step.setScale(jstep.scale[0], jstep.scale[1], jstep.scale[2]); 59 | 60 | for (let j = 0; j < jstep.components.length; ++j) { 61 | const jcom = jstep.components[j]; 62 | const props: Record = {}; 63 | 64 | for (const key in jcom.props) { 65 | const jvalue = jcom.props[key]; 66 | if (typeof jvalue === "number" || typeof jvalue === "string") { 67 | props[key] = jvalue; 68 | } else if (jvalue.kind === RefKind.FILE) { 69 | props[key] = getFile(jvalue.uuid, jvalue.moduleId); 70 | } else if (jvalue.kind === RefKind.FONT) { 71 | const file = getFile(jvalue.fileUUID, jvalue.moduleId); 72 | props[key] = getFont(jvalue.font, file); 73 | } 74 | } 75 | 76 | const com = new HeadlessComponent( 77 | jcom.uuid, 78 | jcom.moduleName, 79 | jcom.component, 80 | props 81 | ); 82 | com.setPosition(jcom.position[0], jcom.position[1], jcom.position[2]); 83 | com.setRotation(jcom.rotation[0], jcom.rotation[1], jcom.rotation[2]); 84 | com.setScale(jcom.scale[0], jcom.scale[1], jcom.scale[2]); 85 | 86 | step.add(com); 87 | if (options.onComponent) options.onComponent(com); 88 | } 89 | 90 | presentation.add(step); 91 | if (options.onStep) options.onStep(step); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /core/sly/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export * from "./types"; 12 | export * from "./decoder"; 13 | export * from "./headlessDecoder"; 14 | export * from "./encoder"; 15 | -------------------------------------------------------------------------------- /core/sly/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { PresentationBase, StepBase, ComponentBase } from "../interfaces"; 12 | 13 | export enum RefKind { 14 | FILE, 15 | FONT 16 | } 17 | 18 | export interface FileRef { 19 | kind: RefKind.FILE; 20 | uuid: string; 21 | moduleId?: string; 22 | } 23 | 24 | export interface FontRef { 25 | kind: RefKind.FONT; 26 | font: string; 27 | fileUUID: string; 28 | moduleId?: string; 29 | } 30 | 31 | export type ComponentPropValue = FileRef | FontRef | string | number; 32 | 33 | export interface JSONPresentationComponent { 34 | moduleName: string; 35 | component: string; 36 | position: [number, number, number]; 37 | rotation: [number, number, number]; 38 | scale: [number, number, number]; 39 | uuid: string; 40 | props: Record; 41 | } 42 | 43 | export interface JSONPresentationStep { 44 | position: [number, number, number]; 45 | rotation: [number, number, number]; 46 | scale: [number, number, number]; 47 | components: JSONPresentationComponent[]; 48 | } 49 | 50 | export interface JSONPresentation { 51 | template?: { 52 | moduleName: string; 53 | component: string; 54 | }; 55 | steps: Record; 56 | } 57 | 58 | export interface DecoderOptions { 59 | onComponent?(component: C): void; 60 | onStep?(step: S): void; 61 | } 62 | 63 | export type SlyDecoder = ( 64 | presentation: PresentationBase, 65 | o: JSONPresentation, 66 | options?: DecoderOptions 67 | ) => void; 68 | -------------------------------------------------------------------------------- /core/stepbar.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Renderer } from "./renderer"; 12 | 13 | export interface StepBarButton { 14 | label: string; 15 | icon: string; // For now just material icons. 16 | clickHandler: (renderer: Renderer) => any; 17 | } 18 | 19 | const e = eval; 20 | const g = e("this"); 21 | const stepBarButtons: StepBarButton[] = g.slyeSBtns || (g.slyeSBtns = []); 22 | 23 | export function addStepbarButton( 24 | label: string, 25 | icon: string, 26 | clickHandler: (renderer: Renderer) => any 27 | ): void { 28 | stepBarButtons.push( 29 | Object.freeze({ 30 | label, 31 | icon, 32 | clickHandler 33 | }) 34 | ); 35 | } 36 | 37 | export function getStepbarButtons(): StepBarButton[] { 38 | return [...stepBarButtons]; 39 | } 40 | -------------------------------------------------------------------------------- /core/sync/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Unserializers, Context } from "./serializer/types"; 12 | import { serializers } from "./serializer/serializers"; 13 | import { serialize, unserialize } from "./serializer"; 14 | import { Serializer } from "./types"; 15 | import { ActionTypes } from "../actions"; 16 | 17 | export function createSerializer(unserializers: Unserializers) { 18 | return class XSerializer implements Serializer { 19 | readonly ctx: Context; 20 | 21 | constructor() { 22 | this.ctx = { 23 | serializers: serializers(unserializers), 24 | fonts: new Map(), 25 | components: new Map(), 26 | steps: new Map(), 27 | files: new Map(), 28 | presentationUUID: undefined 29 | }; 30 | } 31 | 32 | serialize(forward: boolean, action: keyof ActionTypes, data: any): string { 33 | return JSON.stringify({ 34 | forward, 35 | action, 36 | data: serialize(this.ctx, data) 37 | }); 38 | } 39 | 40 | async unserialize(text: string): Promise { 41 | const raw = JSON.parse(text); 42 | console.log(JSON.stringify(raw, null, 4)); 43 | const data = await unserialize(this.ctx, raw.data); 44 | return { 45 | forward: !!raw.forward, 46 | action: raw.action, 47 | data: data 48 | }; 49 | } 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /core/sync/headlessSerializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { unserializers } from "./serializer/headless"; 12 | import { createSerializer } from "./common"; 13 | 14 | export const HeadlessSerializer = createSerializer(unserializers); 15 | -------------------------------------------------------------------------------- /core/sync/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export * from "./sync"; 12 | export * from "./serializer"; 13 | -------------------------------------------------------------------------------- /core/sync/serializer/headless.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { HeadlessStep, HeadlessComponent, HeadlessFont } from "../../headless"; 12 | import { File } from "../../file"; 13 | import { Unserializers } from "./types"; 14 | import { unserialize } from "./index"; 15 | 16 | export const unserializers: Unserializers = { 17 | font: { 18 | async unserialize(data) { 19 | const file = await unserialize(this, data.file); 20 | const key = `${file.owner}-${data.name}`; 21 | if (this.fonts.has(key)) { 22 | return this.fonts.get(key); 23 | } 24 | const font = new HeadlessFont(data.name, file as any); 25 | this.fonts.set(key, font); 26 | return font; 27 | } 28 | }, 29 | step: { 30 | async unserialize(serialized) { 31 | const { uuid, data } = serialized; 32 | if (this.steps.has(uuid)) { 33 | return this.steps.get(uuid); 34 | } 35 | const { position, rotation, scale } = data; 36 | const step = new HeadlessStep(uuid); 37 | step.setPosition(position[0], position[1], position[2]); 38 | step.setRotation(rotation[0], rotation[1], rotation[2]); 39 | step.setScale(scale[0], scale[1], scale[2]); 40 | const components = data.components.map(async c => { 41 | const component = await unserialize(this, c); 42 | step.add(component as HeadlessComponent); 43 | }); 44 | await Promise.all(components); 45 | this.steps.set(uuid, step); 46 | return step; 47 | } 48 | }, 49 | component: { 50 | async unserialize(serialized) { 51 | const { uuid, data } = serialized; 52 | if (this.components.has(uuid)) { 53 | return this.components.get(uuid); 54 | } 55 | const { position, rotation, scale, moduleName, componentName } = data; 56 | const props = await unserialize(this, data.props); 57 | const com = new HeadlessComponent(uuid, moduleName, componentName, props); 58 | com.setPosition(position[0], position[1], position[2]); 59 | com.setRotation(rotation[0], rotation[1], rotation[2]); 60 | com.setScale(scale[0], scale[1], scale[2]); 61 | this.components.set(uuid, com); 62 | return com; 63 | } 64 | }, 65 | file: { 66 | async unserialize(serialized) { 67 | const { uuid, moduleName } = serialized; 68 | const isModuleAsset = !!moduleName; 69 | const owner = isModuleAsset ? moduleName : this.presentationUUID; 70 | const key = `${isModuleAsset ? owner : ""}-${uuid}`; 71 | if (this.files.has(key)) { 72 | return this.files.get(key); 73 | } 74 | const file = new File(owner, uuid, isModuleAsset); 75 | this.files.set(key, file); 76 | return file; 77 | } 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /core/sync/serializer/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Context, SerializedData, Serializers, Serialized, P } from "./types"; 12 | 13 | export function serialize(ctx: Context, data: any): SerializedData { 14 | const keys: (keyof Serializers)[] = Object.keys(ctx.serializers) as any; 15 | 16 | for (const key of keys) { 17 | const serializer = ctx.serializers[key]; 18 | if (serializer.test(data)) { 19 | const serializedData = serializer.serialize.call(ctx, data); 20 | return { 21 | name: key, 22 | data: serializedData 23 | } as any; 24 | } 25 | } 26 | 27 | throw new Error("Can not serialize data."); 28 | } 29 | 30 | export function unserialize( 31 | ctx: Context, 32 | data: Serialized 33 | ): Promise>; 34 | 35 | export function unserialize(ctx: Context, data: SerializedData): Promise { 36 | const serializer = ctx.serializers[data.name]; 37 | return serializer.unserialize.call(ctx, data.data); 38 | } 39 | -------------------------------------------------------------------------------- /core/sync/serializer/serializers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { ComponentBase, FontBase, StepBase, FileBase } from "../../interfaces"; 12 | import { File } from "../../file"; 13 | import { 14 | Unserializers, 15 | Serializers, 16 | Serialized, 17 | SerializedData, 18 | Primary 19 | } from "./types"; 20 | import { serialize, unserialize } from "./index"; 21 | 22 | export const serializers: (u: Unserializers) => Serializers = u => ({ 23 | primary: { 24 | test(data): data is Primary { 25 | return ( 26 | typeof data === "string" || 27 | typeof data === "number" || 28 | typeof data === "boolean" || 29 | typeof data === "undefined" || 30 | data === null 31 | ); 32 | }, 33 | serialize(data) { 34 | return data; 35 | }, 36 | async unserialize(data) { 37 | return data; 38 | } 39 | }, 40 | font: { 41 | test(data): data is FontBase { 42 | return !!(data && typeof data === "object" && data.isSlyeFont); 43 | }, 44 | serialize(data) { 45 | const key = `${data.file.owner}-${data.name}`; 46 | this.fonts.set(key, data); 47 | return { 48 | name: data.name, 49 | file: serialize(this, data.file) as Serialized<"file"> 50 | }; 51 | }, 52 | ...u.font 53 | }, 54 | step: { 55 | test(data): data is StepBase { 56 | return !!(data && typeof data === "object" && data.isSlyeStep); 57 | }, 58 | serialize(step) { 59 | const { uuid } = step; 60 | 61 | let sendData = !this.steps.has(uuid); 62 | this.steps.set(uuid, step); 63 | 64 | const position = step.getPosition(); 65 | const rotation = step.getRotation(); 66 | const scale = step.getScale(); 67 | const components = step.components; 68 | 69 | return { 70 | uuid, 71 | data: sendData 72 | ? { 73 | position: [position.x, position.y, position.z], 74 | rotation: [rotation.x, rotation.y, rotation.z], 75 | scale: [scale.x, scale.y, scale.z], 76 | components: components.map>( 77 | c => serialize(this, c) as any 78 | ) 79 | } 80 | : undefined 81 | }; 82 | }, 83 | ...u.step 84 | }, 85 | component: { 86 | test(data): data is ComponentBase { 87 | return !!(data && typeof data === "object" && data.isSlyeComponent); 88 | }, 89 | serialize(component) { 90 | const { uuid } = component; 91 | 92 | let sendData = !this.components.has(uuid); 93 | this.components.set(uuid, component); 94 | 95 | const position = component.getPosition(); 96 | const rotation = component.getRotation(); 97 | const scale = component.getScale(); 98 | 99 | return { 100 | uuid, 101 | data: sendData 102 | ? { 103 | position: [position.x, position.y, position.z], 104 | rotation: [rotation.x, rotation.y, rotation.z], 105 | scale: [scale.x, scale.y, scale.z], 106 | moduleName: component.moduleName, 107 | componentName: component.componentName, 108 | props: serialize(this, component.props) as any 109 | } 110 | : undefined 111 | }; 112 | }, 113 | ...u.component 114 | }, 115 | file: { 116 | test(data): data is FileBase { 117 | return !!(data && typeof data == "object" && data.isSlyeFile); 118 | }, 119 | serialize(file) { 120 | const { uuid, isModuleAsset, owner } = file; 121 | const key = `${isModuleAsset ? owner : ""}-${uuid}`; 122 | const moduleName = isModuleAsset ? owner : undefined; 123 | this.files.set(key, file); 124 | return { uuid, moduleName }; 125 | }, 126 | ...u.file 127 | }, 128 | object: { 129 | test(data): data is Record { 130 | return data && typeof data === "object"; 131 | }, 132 | serialize(obj) { 133 | const ret: Record = {}; 134 | for (const key in obj) { 135 | ret[key] = serialize(this, obj[key]); 136 | } 137 | return ret; 138 | }, 139 | async unserialize(obj) { 140 | const ret: Record = {}; 141 | for (const key in obj) { 142 | ret[key] = await unserialize(this, obj[key]); 143 | } 144 | return ret; 145 | } 146 | } 147 | }); 148 | -------------------------------------------------------------------------------- /core/sync/serializer/three.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { ThreeStep, ThreeComponent, Font } from "../../three"; 12 | import { File } from "../../file"; 13 | import { file, component } from "../../module"; 14 | import { getFont } from "../../fonts"; 15 | import { Unserializers } from "./types"; 16 | import { unserialize } from "./index"; 17 | 18 | export const unserializers: Unserializers = { 19 | font: { 20 | async unserialize(data) { 21 | const file = await unserialize(this, data.file); 22 | const regFont = getFont(data.name); 23 | if (regFont && regFont.file === file) { 24 | return regFont; 25 | } 26 | const key = `${file.owner}-${data.name}`; 27 | if (this.fonts.has(key)) { 28 | return this.fonts.get(key); 29 | } 30 | const font = new Font(data.name, file as any); 31 | this.fonts.set(key, font); 32 | return font; 33 | } 34 | }, 35 | step: { 36 | async unserialize(serialized) { 37 | const { uuid, data } = serialized; 38 | if (this.steps.has(uuid)) { 39 | return this.steps.get(uuid); 40 | } 41 | const { position, rotation, scale } = data; 42 | const step = new ThreeStep(uuid); 43 | step.setPosition(position[0], position[1], position[2]); 44 | step.setRotation(rotation[0], rotation[1], rotation[2]); 45 | step.setScale(scale[0], scale[1], scale[2]); 46 | const components = data.components.map(async c => { 47 | const component = await unserialize(this, c); 48 | step.add(component as ThreeComponent); 49 | }); 50 | await Promise.all(components); 51 | this.steps.set(uuid, step); 52 | return step; 53 | } 54 | }, 55 | component: { 56 | async unserialize(serialized) { 57 | const { uuid, data } = serialized; 58 | if (this.components.has(uuid)) { 59 | return this.components.get(uuid); 60 | } 61 | const { position, rotation, scale } = data; 62 | const props = await unserialize(this, data.props); 63 | const com = await component( 64 | data.moduleName, 65 | data.componentName, 66 | props, 67 | uuid 68 | ); 69 | com.setPosition(position[0], position[1], position[2]); 70 | com.setRotation(rotation[0], rotation[1], rotation[2]); 71 | com.setScale(scale[0], scale[1], scale[2]); 72 | this.components.set(uuid, com); 73 | return com; 74 | } 75 | }, 76 | file: { 77 | async unserialize(serialized) { 78 | const { uuid, moduleName } = serialized; 79 | const isModuleAsset = !!moduleName; 80 | if (isModuleAsset) { 81 | return await file(moduleName, uuid); 82 | } 83 | const owner = isModuleAsset ? moduleName : this.presentationUUID; 84 | const key = `${isModuleAsset ? owner : ""}-${uuid}`; 85 | if (this.files.has(key)) { 86 | return this.files.get(key); 87 | } 88 | const newFile = new File(owner, uuid, isModuleAsset); 89 | this.files.set(key, newFile); 90 | return newFile; 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /core/sync/serializer/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { StepBase, ComponentBase, FontBase, FileBase } from "../../interfaces"; 12 | 13 | export type Primary = number | string | boolean | null | undefined; 14 | 15 | export interface Context { 16 | serializers: Serializers; 17 | fonts: Map; 18 | components: Map; 19 | steps: Map; 20 | files: Map; 21 | presentationUUID: string; 22 | } 23 | 24 | export interface Serializer { 25 | test(data: any): data is P; 26 | serialize(this: Context, data: P): T; 27 | unserialize(this: Context, data: T): Promise

; 28 | } 29 | 30 | export interface Unserializer { 31 | unserialize(this: Context, data: T): Promise

; 32 | } 33 | 34 | type V3 = [number, number, number]; 35 | 36 | export type Serializers = { 37 | primary: Serializer; 38 | object: Serializer, Record>; 39 | font: Serializer }>; 40 | step: Serializer< 41 | StepBase, 42 | { 43 | uuid: string; 44 | data?: { 45 | position: V3; 46 | rotation: V3; 47 | scale: V3; 48 | components: Serialized<"component">[]; 49 | }; 50 | } 51 | >; 52 | component: Serializer< 53 | ComponentBase, 54 | { 55 | uuid: string; 56 | data?: { 57 | position: V3; 58 | scale: V3; 59 | rotation: V3; 60 | moduleName: string; 61 | componentName: string; 62 | props: Serialized<"object">; 63 | }; 64 | } 65 | >; 66 | file: Serializer< 67 | FileBase, 68 | { 69 | uuid: string; 70 | moduleName?: string; 71 | } 72 | >; 73 | }; 74 | 75 | export type Unserializers = { 76 | font: U<"font">; 77 | step: U<"step">; 78 | component: U<"component">; 79 | file: U<"file">; 80 | }; 81 | 82 | export type SerializedData = { 83 | [K in keyof Serializers]: { 84 | name: K; 85 | data: T; 86 | } 87 | }[keyof Serializers]; 88 | 89 | export type Serialized = { 90 | name: K; 91 | data: T; 92 | }; 93 | 94 | // Utils. 95 | 96 | export type P = Serializers[K] extends Serializer< 97 | infer P, 98 | any 99 | > 100 | ? P 101 | : never; 102 | 103 | export type T = Serializers[K] extends Serializer< 104 | any, 105 | infer T 106 | > 107 | ? T 108 | : never; 109 | 110 | type U = Unserializer, T>; 111 | -------------------------------------------------------------------------------- /core/sync/sync.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { PresentationBase, StepBase, ComponentBase } from "../interfaces"; 12 | import { ActionStack } from "../actionStack"; 13 | import { SyncCommand, SyncChannel, Serializer } from "./types"; 14 | import { SlyDecoder, JSONPresentation } from "../sly/types"; 15 | import { encode } from "../sly/encoder"; 16 | import { actions, ActionTypes } from "../actions"; 17 | 18 | /** 19 | * An API to keep a presentations sync over a channel. 20 | */ 21 | export class Sync { 22 | private resolves: (() => void)[] = []; 23 | 24 | constructor( 25 | readonly presentation: PresentationBase, 26 | readonly serializer: Serializer, 27 | private readonly ch: SyncChannel, 28 | private readonly slyDecoder: SlyDecoder, 29 | readonly isServer = false 30 | ) { 31 | if (!this.isServer) this.load(); 32 | 33 | this.onMessage = this.onMessage.bind(this); 34 | this.onChange = this.onChange.bind(this); 35 | 36 | this.ch.onMessage(this.onMessage); 37 | 38 | if (serializer.ctx.presentationUUID) 39 | throw new Error("Serializer is already in use"); 40 | 41 | serializer.ctx.presentationUUID = presentation.uuid; 42 | } 43 | 44 | bind(actionStack: ActionStack): void { 45 | if (actionStack.listener === this.onChange) return; 46 | if (actionStack.listener) 47 | throw new Error("ActionStack is already binded to another Sync."); 48 | actionStack.listener = this.onChange; 49 | } 50 | 51 | async open(sly: JSONPresentation): Promise { 52 | // TODO(qti3e) We need to ensure it is only called once. 53 | await this.slyDecoder(this.presentation, sly, { 54 | onComponent: (component: ComponentBase): void => { 55 | this.serializer.ctx.components.set(component.uuid, component); 56 | }, 57 | onStep: (step: StepBase): void => { 58 | this.serializer.ctx.steps.set(step.uuid, step); 59 | } 60 | }); 61 | this.resolves.map(r => r()); 62 | this.resolves = []; 63 | } 64 | 65 | waitForOpen(): Promise { 66 | const promise = new Promise(r => this.resolves.push(r)); 67 | return promise; 68 | } 69 | 70 | private send(command: SyncCommand): void { 71 | this.ch.send(JSON.stringify(command)); 72 | } 73 | 74 | private onMessage(msg: string): void { 75 | // For now it just works but we need an handshake process. 76 | const cmd: SyncCommand = JSON.parse(msg); 77 | switch (cmd.command) { 78 | case "sly": 79 | this.handleSly(); 80 | break; 81 | case "sly_response": 82 | this.open(cmd.sly); 83 | break; 84 | case "action": 85 | this.handleAction(cmd.action); 86 | break; 87 | } 88 | } 89 | 90 | private handleSly(): void { 91 | this.send({ 92 | command: "sly_response", 93 | sly: encode(this.presentation) 94 | }); 95 | } 96 | 97 | private async handleAction(text: string): Promise { 98 | const raw = await this.serializer.unserialize(text); 99 | const action = (actions as any)[raw.action][ 100 | raw.forward ? "forward" : "backward" 101 | ]; 102 | action(this.presentation, raw.data); 103 | } 104 | 105 | private load(): void { 106 | const presentationDescriptor = this.presentation.uuid; 107 | this.send({ 108 | command: "sly", 109 | pd: presentationDescriptor 110 | }); 111 | } 112 | 113 | private onChange( 114 | forward: boolean, 115 | actionName: keyof ActionTypes, 116 | data: any 117 | ): void { 118 | const action = this.serializer.serialize(forward, actionName, data); 119 | this.send({ 120 | command: "action", 121 | action 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /core/sync/threeSerializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { unserializers } from "./serializer/three"; 12 | import { createSerializer } from "./common"; 13 | 14 | export const ThreeSerializer = createSerializer(unserializers); 15 | -------------------------------------------------------------------------------- /core/sync/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { JSONPresentation } from "../sly/types"; 12 | import { Context } from "./serializer/types"; 13 | import { ActionTypes } from "../actions"; 14 | 15 | export interface SyncChannel { 16 | send(msg: string): void; 17 | onMessage(handler: (msg: string) => void): void; 18 | } 19 | 20 | export interface Serializer { 21 | readonly ctx: Context; 22 | serialize(forward: boolean, action: keyof ActionTypes, data: any): string; 23 | unserialize(text: string): Promise; 24 | } 25 | 26 | export type SyncCommand = SyncSlyCommand | SyncActionCommand | SyncSlyResponse; 27 | 28 | export interface SyncSlyCommand { 29 | command: "sly"; 30 | pd: string; 31 | } 32 | 33 | export interface SyncSlyResponse { 34 | command: "sly_response"; 35 | sly: JSONPresentation; 36 | } 37 | 38 | export interface SyncActionCommand { 39 | command: "action"; 40 | action: any; 41 | } 42 | -------------------------------------------------------------------------------- /core/three/component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { ComponentBase, ComponentProps, PropValue } from "../interfaces"; 12 | import { ThreeStep } from "./step"; 13 | import { Group } from "./group"; 14 | import { UILayout } from "../ui"; 15 | 16 | /** 17 | * Abstract Three.js based implementation for Slye Components. 18 | * It is to be used in modules. 19 | */ 20 | export abstract class ThreeComponent< 21 | Props extends Record = Record 22 | > extends Group implements ComponentBase { 23 | /** 24 | * Used for optimizations, you should never change this. 25 | */ 26 | readonly isSlyeComponent = true; 27 | 28 | /** 29 | * Current props for this component. 30 | */ 31 | props: Props; 32 | 33 | /** 34 | * Current owner of this component. 35 | */ 36 | owner: ThreeStep; 37 | 38 | /** 39 | * Whatever component is currently in a render call or not. 40 | */ 41 | private isUpdating = false; 42 | 43 | /** 44 | * When `isUpdating` is true and we try to update props, that call will return 45 | * immediately and it sets this value, at the end of a render call we check 46 | * this value and if it's not undefined we re update the props. 47 | */ 48 | private nextProps: Props; 49 | 50 | /** 51 | * UI widgets to be shown in editor. 52 | */ 53 | abstract readonly ui: UILayout; 54 | 55 | /** 56 | * ThreeComponent constructor. 57 | * 58 | * @param {string} uuid Component's Unique ID. 59 | * @param {string} moduleName Name of the module that provides this component. 60 | * @param {string} componentName Component kind. 61 | * @param {Props} Initial props. 62 | */ 63 | constructor( 64 | readonly uuid: string, 65 | readonly moduleName: string, 66 | readonly componentName: string, 67 | props: Props 68 | ) { 69 | super(); 70 | this.group.userData.component = this; 71 | this.init(); 72 | this.updateProps(props); 73 | } 74 | 75 | /** 76 | * Update the props and re-renders the component. 77 | * 78 | * @param {Props} props New props. 79 | * @returns {void} 80 | */ 81 | private updateProps(props: Props): void { 82 | if (this.isUpdating) { 83 | this.nextProps = props; 84 | return; 85 | } 86 | 87 | // TODO(qti3e) Dispose every child gracefully. 88 | this.group.children.length = 0; 89 | this.props = props; 90 | 91 | this.isUpdating = true; 92 | 93 | (async () => { 94 | try { 95 | await this.render(); 96 | } catch (e) { 97 | console.error(e); 98 | this.group.children.length = 0; 99 | } 100 | 101 | this.isUpdating = false; 102 | if (this.nextProps) { 103 | props = this.nextProps; 104 | this.nextProps = undefined; 105 | this.updateProps(props); 106 | } 107 | })(); 108 | } 109 | 110 | /** 111 | * Returns the prop value by its key. 112 | * 113 | * @param {keyof Props} key Property name. 114 | * @returns {PropValue} 115 | */ 116 | getProp(key: T): Props[T] { 117 | return this.props[key]; 118 | } 119 | 120 | /** 121 | * Patch a set of properties to the component and update it. 122 | * 123 | * @param {Partial} New props. 124 | * @returns {void} 125 | */ 126 | patchProps(props: Partial): void { 127 | this.updateProps({ 128 | ...this.props, 129 | ...this.nextProps, 130 | ...props 131 | }); 132 | } 133 | 134 | protected abstract render(): Promise; 135 | protected abstract init(): void; 136 | 137 | handleClick?(): void; 138 | } 139 | -------------------------------------------------------------------------------- /core/three/font.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as fontkit from "fontkit"; 12 | import { FontBase, Glyph, PathCommandKind, PathCommand } from "../interfaces"; 13 | import { File } from "../file"; 14 | 15 | export class Font implements FontBase { 16 | readonly isSlyeFont = true; 17 | private font: fontkit.Font; 18 | 19 | constructor(readonly name: string, readonly file: File) {} 20 | 21 | private async ensure(): Promise { 22 | if (this.font) return; 23 | const data = await this.file.load(); 24 | const buffer = new Buffer(data); // WTF! I don't like Buffer. 25 | this.font = fontkit.create(buffer); 26 | } 27 | 28 | /** 29 | * Return a simplified & reusable font layout. 30 | * 31 | * @param text Unicode text we want to render. 32 | */ 33 | async layout(text: string): Promise { 34 | await this.ensure(); 35 | 36 | const ret: Glyph[] = []; 37 | const { glyphs } = this.font.layout(text); 38 | const scale = 1 / this.font.head.unitsPerEm; 39 | 40 | for (let i = 0; i < glyphs.length; ++i) { 41 | const glyph = glyphs[i]; 42 | const path: PathCommand[] = []; 43 | 44 | let sx, sy; 45 | let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2; 46 | 47 | for (let j = 0; j < glyph.path.commands.length; ++j) { 48 | const { command, args } = glyph.path.commands[j]; 49 | switch (command) { 50 | case "moveTo": 51 | x = args[0] * scale; 52 | y = args[1] * scale; 53 | path.push({ 54 | command: PathCommandKind.MOVE_TO, 55 | x, 56 | y 57 | }); 58 | if (sx === undefined) { 59 | sx = x; 60 | sy = y; 61 | } 62 | break; 63 | case "lineTo": 64 | x = args[0] * scale; 65 | y = args[1] * scale; 66 | path.push({ 67 | command: PathCommandKind.LINE_TO, 68 | x, 69 | y 70 | }); 71 | break; 72 | case "quadraticCurveTo": 73 | cpx = args[0] * scale; 74 | cpy = args[1] * scale; 75 | x = args[2] * scale; 76 | y = args[3] * scale; 77 | path.push({ 78 | command: PathCommandKind.QUADRATIC_CURVE_TO, 79 | cpx, 80 | cpy, 81 | x, 82 | y 83 | }); 84 | break; 85 | case "bezierCurveTo": 86 | cpx1 = args[0] * scale; 87 | cpy1 = args[1] * scale; 88 | cpx2 = args[2] * scale; 89 | cpy2 = args[3] * scale; 90 | x = args[4] * scale; 91 | y = args[5] * scale; 92 | path.push({ 93 | command: PathCommandKind.BEZIER_CURVE_TO, 94 | cpx1, 95 | cpy1, 96 | cpx2, 97 | cpy2, 98 | x, 99 | y 100 | }); 101 | break; 102 | case "closePath": 103 | path.push({ 104 | command: PathCommandKind.LINE_TO, 105 | x: sx, 106 | y: sy 107 | }); 108 | sx = sy = undefined; 109 | break; 110 | } 111 | } 112 | 113 | ret.push({ 114 | path, 115 | advanceWidth: glyph.advanceWidth * scale 116 | }); 117 | } 118 | 119 | return ret; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /core/three/group.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Group as ThreeGroup } from "three"; 12 | import { Transformable, Vec3 } from "../interfaces"; 13 | 14 | /** 15 | * Common methods from Step and Component. 16 | */ 17 | export class Group implements Transformable { 18 | /** 19 | * Three.js group that contains current instance's children. 20 | */ 21 | readonly group: ThreeGroup = new ThreeGroup(); 22 | 23 | /** 24 | * Set the position. 25 | * 26 | * @param {number} x Value for the `x` axis. 27 | * @param {number} y Value for the `r` axis. 28 | * @param {number} z Value for the `z` axis. 29 | * @return {void} 30 | */ 31 | setPosition(x: number, y: number, z: number): void { 32 | this.group.position.set(x, y, z); 33 | } 34 | 35 | /** 36 | * Set the orientation, values must be in radian. 37 | * 38 | * @param {number} x Value for the `x` axis. 39 | * @param {number} y Value for the `r` axis. 40 | * @param {number} z Value for the `z` axis. 41 | * @return {void} 42 | */ 43 | setRotation(x: number, y: number, z: number): void { 44 | this.group.rotation.set(x, y, z); 45 | } 46 | 47 | /** 48 | * Set the scale factor. 49 | * 50 | * @param {number} x Value for the `x` axis. 51 | * @param {number} y Value for the `r` axis. 52 | * @param {number} z Value for the `z` axis. 53 | * @return {void} 54 | */ 55 | setScale(x: number, y: number, z: number): void { 56 | this.group.scale.set(x, y, z); 57 | } 58 | 59 | /** 60 | * Returns the current position as a Slye Vec3. 61 | * 62 | * @returns {Vec3} 63 | */ 64 | getPosition(): Vec3 { 65 | const { x, y, z } = this.group.position; 66 | return { x, y, z }; 67 | } 68 | 69 | /** 70 | * Returns the current orientation as a Slye Vec3. 71 | * 72 | * @returns {Vec3} 73 | */ 74 | getRotation(): Vec3 { 75 | const { x, y, z } = this.group.rotation; 76 | return { x, y, z }; 77 | } 78 | 79 | /** 80 | * Returns the current scale factors as a Slye Vec3. 81 | * 82 | * @returns {Vec3} 83 | */ 84 | getScale(): Vec3 { 85 | const { x, y, z } = this.group.scale; 86 | return { x, y, z }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /core/three/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export * from "./presentation"; 12 | export * from "./step"; 13 | export * from "./component"; 14 | export * from "./group"; 15 | export * from "./font"; 16 | -------------------------------------------------------------------------------- /core/three/presentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Group } from "three"; 12 | import { PresentationBase } from "../interfaces"; 13 | import { ThreeStep } from "./step"; 14 | 15 | /** 16 | * ThreePresentation is a Three.js based implementation of Slye Presentation. 17 | */ 18 | export class ThreePresentation implements PresentationBase { 19 | /** 20 | * Used for optimizations, you should never change this. 21 | */ 22 | readonly isSlyePresentation = true; 23 | 24 | /** 25 | * This group will contain ThreeStep's groups. 26 | */ 27 | readonly group: Group = new Group(); 28 | 29 | /** 30 | * List of steps in this presentation. 31 | */ 32 | readonly steps: ThreeStep[] = []; 33 | 34 | /** 35 | * Creates a new ThreePresentation instance. 36 | * 37 | * @param {string} uuid Presentation's Unique ID. 38 | */ 39 | constructor(readonly uuid: string) {} 40 | 41 | /** 42 | * Remove the given step from this presentation. 43 | * 44 | * @param {ThreeStep} step Step you want to remove. 45 | * @returns {void} 46 | */ 47 | del(step: ThreeStep): void { 48 | if (step.owner !== this) return; 49 | step.owner = undefined; 50 | const index = this.steps.indexOf(step); 51 | this.steps.splice(index, 1); 52 | this.group.remove(step.group); 53 | } 54 | 55 | /** 56 | * Insert the given step in the given offset, if `index` is not provided it 57 | * appends the step at the end of the list. 58 | * 59 | * @param {StepBase} step Step which you want to add into the presentation. 60 | * @param {number} index Index in the steps list. 61 | * @returns {void} 62 | */ 63 | add(step: ThreeStep, index?: number): void { 64 | if (step.owner) step.owner.del(step); 65 | step.owner = this; 66 | index = index || this.steps.length; 67 | this.steps.splice(index, 0, step); 68 | this.group.add(step.group); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /core/three/step.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { DoubleSide, PlaneGeometry, MeshBasicMaterial, Mesh } from "three"; 12 | import { StepBase } from "../interfaces"; 13 | import { ThreePresentation } from "./presentation"; 14 | import { ThreeComponent } from "./component"; 15 | import { Group } from "./group"; 16 | 17 | /** 18 | * A Three.js based implementation for Slye Step. 19 | */ 20 | export class ThreeStep extends Group implements StepBase { 21 | static readonly width = 5 * 19.2; 22 | static readonly height = 5 * 10.8; 23 | static readonly placeholderGeo = new PlaneGeometry( 24 | ThreeStep.width, 25 | ThreeStep.height, 26 | 2 27 | ); 28 | static readonly placeholderMatt = new MeshBasicMaterial({ 29 | color: 0xe0e0e0, 30 | opacity: 0.5, 31 | transparent: true, 32 | side: DoubleSide 33 | }); 34 | 35 | /** 36 | * Used for optimizations, you should never change this. 37 | */ 38 | readonly isSlyeStep = true; 39 | 40 | /** 41 | * List of components that this step owns. 42 | */ 43 | readonly components: ThreeComponent[] = []; 44 | 45 | /** 46 | * Current owner of this step. 47 | */ 48 | owner: ThreePresentation; 49 | 50 | /** 51 | * Creates a new ThreeStep instance. 52 | * 53 | * @param {string} uuid Steps' Unique ID. 54 | */ 55 | constructor(readonly uuid: string) { 56 | super(); 57 | this.group.userData.step = this; 58 | const plane = new Mesh(ThreeStep.placeholderGeo, ThreeStep.placeholderMatt); 59 | this.group.add(plane); 60 | } 61 | 62 | /** 63 | * Removes `component` from this step. 64 | * 65 | * @param {ThreeComponent} component Component which you want to remove. 66 | * @returns {void} 67 | */ 68 | del(component: ThreeComponent): void { 69 | if (component.owner !== this) return; 70 | component.owner = undefined; 71 | const index = this.components.indexOf(component); 72 | this.components.splice(index, 1); 73 | this.group.remove(component.group); 74 | } 75 | 76 | /** 77 | * Adds `component` to this step. 78 | * 79 | * @param {ThreeComponent} component Component which you want to add into this 80 | * step. 81 | * @returns {void} 82 | */ 83 | add(component: ThreeComponent): void { 84 | if (component.owner === this) return; 85 | if (component.owner) component.owner.del(component); 86 | component.owner = this; 87 | this.components.push(component); 88 | this.group.add(component.group); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /core/ui.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { FileBase, FontBase, PropValue, ComponentProps } from "./interfaces"; 12 | 13 | // UI widgets 14 | const e = eval; 15 | const g: any = e("this"); 16 | export const TEXT: unique symbol = g.swT || (g.swT = Symbol("TEXT")); 17 | export const SIZE: unique symbol = g.swS || (g.swS = Symbol("SIZE")); 18 | export const FONT: unique symbol = g.swF || (g.swF = Symbol("FONT")); 19 | export const COLOR: unique symbol = g.swC || (g.swC = Symbol("COLOR")); 20 | export const FILE: unique symbol = g.swI || (g.swI = Symbol("FILE")); 21 | 22 | type WidgetTypeMap = T extends string 23 | ? (typeof TEXT) 24 | : T extends number 25 | ? (typeof SIZE | typeof COLOR) 26 | : T extends FontBase 27 | ? (typeof FONT) 28 | : T extends FileBase 29 | ? (typeof FILE) 30 | : never; 31 | 32 | export type Widget = { 33 | [K in keyof Props]: { 34 | name: K; 35 | widget: WidgetTypeMap; 36 | size: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | "auto"; 37 | } 38 | }[keyof Props]; 39 | 40 | export type UILayout = Widget[]; 41 | -------------------------------------------------------------------------------- /electron/client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { ipcRenderer, IpcMessageEvent } from "electron"; 12 | import { JSONPresentationStep } from "@slye/core/sly"; 13 | import * as types from "../frontend/ipc"; 14 | 15 | export class Client implements types.Client { 16 | private readonly resolves: Map = new Map(); 17 | private lastReqId: number = 0; 18 | 19 | constructor() { 20 | ipcRenderer.on( 21 | "asynchronous-reply", 22 | (event: IpcMessageEvent, res: types.Response) => { 23 | if (res && Object.prototype.hasOwnProperty.call(res, "kind")) { 24 | this.handleResponse(res); 25 | } 26 | } 27 | ); 28 | } 29 | 30 | private handleResponse(res: types.Response): void { 31 | const id = res.id; 32 | const resolve = this.resolves.get(id); 33 | this.resolves.delete(id); 34 | if (!resolve) throw new Error("Multiple response for a single request."); 35 | resolve(res.data); 36 | } 37 | 38 | // TODO(qti3e) Typing on this function can be improved or simpilfied. 39 | private sendRequest< 40 | R extends types.Request, 41 | D extends types.ResponseData, 42 | T = Pick> 43 | >(req: T): Promise { 44 | const id = this.lastReqId++; 45 | const promise = new Promise(resolve => { 46 | this.resolves.set(id, resolve); 47 | }); 48 | // Set the id & send the message. 49 | (req as any).id = id; 50 | ipcRenderer.send("asynchronous-message", req); 51 | return promise; 52 | } 53 | 54 | /** 55 | * Create a new presentation. 56 | */ 57 | create(): Promise { 58 | return this.sendRequest({ 59 | kind: types.MsgKind.CREATE 60 | }); 61 | } 62 | 63 | close(presentationDescriptor: string): Promise { 64 | return this.sendRequest({ 65 | kind: types.MsgKind.CLOSE, 66 | presentationDescriptor 67 | }); 68 | } 69 | 70 | patchMeta( 71 | presentationDescriptor: string, 72 | meta: types.Meta 73 | ): Promise { 74 | return this.sendRequest< 75 | types.PatchMetaRequest, 76 | types.PatchMetaResponseData 77 | >({ 78 | kind: types.MsgKind.PATCH_META, 79 | presentationDescriptor, 80 | meta 81 | }); 82 | } 83 | 84 | getMeta(presentationDescriptor: string): Promise { 85 | return this.sendRequest({ 86 | kind: types.MsgKind.GET_META, 87 | presentationDescriptor 88 | }); 89 | } 90 | 91 | fetchSly( 92 | presentationDescriptor: string 93 | ): Promise { 94 | return this.sendRequest({ 95 | kind: types.MsgKind.FETCH_SLY, 96 | presentationDescriptor 97 | }); 98 | } 99 | 100 | save(presentationDescriptor: string): Promise { 101 | return this.sendRequest({ 102 | kind: types.MsgKind.SAVE, 103 | presentationDescriptor 104 | }); 105 | } 106 | 107 | open(): Promise { 108 | return this.sendRequest({ 109 | kind: types.MsgKind.OPEN 110 | }); 111 | } 112 | 113 | showFileDialog( 114 | presentationDescriptor: string 115 | ): Promise { 116 | return this.sendRequest< 117 | types.ShowFileDialogRequest, 118 | types.ShowFileDialogResponseData 119 | >({ 120 | kind: types.MsgKind.SHOW_FILE_DIALOG, 121 | presentationDescriptor 122 | }); 123 | } 124 | 125 | async getModuleMainURL(moduleName: string): Promise { 126 | return `slye://modules/${moduleName}/main.js`; 127 | } 128 | 129 | async getModuleAssetURL(moduleName: string, asset: string): Promise { 130 | return `slye://modules/${moduleName}/assets/${asset}`; 131 | } 132 | 133 | async getAssetURL(pd: string, asset: string): Promise { 134 | return `slye://presentation-assets/${pd}/${asset}`; 135 | } 136 | 137 | syncChannelOnMessage(pd: string, handler: (msg: string) => void): void { 138 | ipcRenderer.on(`p${pd}`, (event: IpcMessageEvent, res: string) => { 139 | handler(res); 140 | }); 141 | } 142 | 143 | syncChannelSend(pd: string, msg: string): void { 144 | ipcRenderer.send(`p${pd}`, msg); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /electron/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { app, BrowserWindow } from "electron"; 12 | import { registerSlyeProtocol } from "./protocol"; 13 | import { Window as SlyeWindow } from "./window"; 14 | import * as path from "path"; 15 | 16 | function createWindow(file?: string): void { 17 | const dev = !!process.env.SLYE_DEV; 18 | const baseUrl = dev 19 | ? "http://localhost:1234" 20 | : `file://${__dirname}/index.html`; 21 | const win = new SlyeWindow(baseUrl); 22 | 23 | if (dev) { 24 | win.openDevTools(); 25 | } 26 | 27 | if (file) { 28 | win.openFile(file); 29 | } else { 30 | win.open(); 31 | } 32 | } 33 | 34 | function main() { 35 | const gotTheLock = app.requestSingleInstanceLock(); 36 | 37 | if (!gotTheLock) { 38 | app.quit(); 39 | return; 40 | } 41 | 42 | app.on("second-instance", (event, commandLine, workingDirectory) => { 43 | let file = app.isPackaged ? commandLine[1] : commandLine[2]; 44 | if (file) file = path.join(workingDirectory, file); 45 | createWindow(file); 46 | }); 47 | 48 | app.on("ready", () => { 49 | registerSlyeProtocol(); 50 | 51 | const file = app.isPackaged ? process.argv[1] : process.argv[2]; 52 | createWindow(file); 53 | }); 54 | 55 | // Quit when all windows are closed. 56 | app.on("window-all-closed", () => { 57 | // On OS X it is common for applications and their menu bar 58 | // to stay active until the user quits explicitly with Cmd + Q 59 | if (process.platform !== "darwin") { 60 | app.quit(); 61 | } 62 | }); 63 | 64 | app.on("activate", () => { 65 | createWindow(); 66 | }); 67 | } 68 | 69 | main(); 70 | -------------------------------------------------------------------------------- /electron/preload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { webFrame, remote } from "electron"; 12 | import { Client } from "./client"; 13 | 14 | webFrame.registerURLSchemeAsPrivileged("slye"); 15 | webFrame.registerURLSchemeAsBypassingCSP("slye"); 16 | window.client = new Client(); 17 | 18 | // When a preload script is loaded, the DOM is not present yet, so `document` 19 | // is undefined. 20 | setTimeout(() => { 21 | document.addEventListener("readystatechange", () => { 22 | if (document.readyState !== "complete") return; 23 | 24 | document.getElementById("min-btn").addEventListener("click", () => { 25 | const window = remote.getCurrentWindow(); 26 | window.minimize(); 27 | }); 28 | 29 | document.getElementById("max-btn").addEventListener("click", () => { 30 | const window = remote.getCurrentWindow(); 31 | if (!window.isMaximized()) { 32 | window.maximize(); 33 | } else { 34 | window.unmaximize(); 35 | } 36 | }); 37 | 38 | document.getElementById("close-btn").addEventListener("click", () => { 39 | const window = remote.getCurrentWindow(); 40 | window.close(); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /electron/protocol.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { protocol } from "electron"; 12 | import { presentations } from "./presentation"; 13 | import { join, normalize } from "path"; 14 | 15 | export function registerSlyeProtocol(): void { 16 | protocol.registerFileProtocol( 17 | "slye", 18 | (request, callback) => { 19 | const url = request.url.substr(7); 20 | const [cmd, ...internalUrlParts] = url.split("/"); 21 | const internalUrl = internalUrlParts.join("/"); 22 | 23 | switch (cmd) { 24 | case "modules": 25 | callback(normalize(join(__dirname, "modules", internalUrl))); 26 | break; 27 | case "presentation-assets": 28 | const [pd, asset] = internalUrlParts; 29 | callback(getAssetURL(pd, asset)); 30 | break; 31 | default: 32 | // TODO(qti3e) Handle 404. 33 | } 34 | }, 35 | error => { 36 | if (error) console.error("Failed to register protocol"); 37 | } 38 | ); 39 | } 40 | 41 | function getAssetURL(pd: string, asset: string): string { 42 | const p = presentations.get(pd); 43 | return normalize(join(p.dir, "assets", asset)); 44 | } 45 | -------------------------------------------------------------------------------- /electron/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slye 5 | 6 | 7 | 8 | 12 | Slye 13 | 52 | 53 | 54 |

55 | 56 |
57 |
Slye
58 |
59 | 60 | 61 | 62 |
63 |
64 | 65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /electron/static/window.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | /*background-color: #f9c416;*/ 10 | border: 1px solid #272727; 11 | border-bottom: 24px solid #272727; 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | bottom: 0; 17 | height: initial; 18 | overflow: hidden; 19 | /*color: #272727;*/ 20 | } 21 | 22 | * { 23 | outline: none; 24 | } 25 | 26 | #status-bar { 27 | height: 24px; 28 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22), 0 1px 2px rgba(0, 0, 0, 0.3); 29 | position: fixed; 30 | bottom: 0; 31 | left: 0; 32 | right: 0; 33 | background: #fff; 34 | } 35 | 36 | #title-bar { 37 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(0, 0, 0, 0.1); 38 | -webkit-app-region: drag; 39 | height: 32px; 40 | padding: 0; 41 | margin: 0; 42 | color: #272727; 43 | font-family: sans-serif; 44 | background-color: #fff; 45 | position: fixed; 46 | top: 0; 47 | left: 0; 48 | right: 0; 49 | } 50 | 51 | #title { 52 | position: fixed; 53 | top: 0; 54 | left: 12px; 55 | line-height: 32px; 56 | font-size: 14px; 57 | } 58 | 59 | #title img { 60 | padding: 5px; 61 | } 62 | 63 | #title-bar-btns { 64 | -webkit-app-region: no-drag; 65 | position: fixed; 66 | top: 0; 67 | right: 0; 68 | } 69 | 70 | #title-bar-btns button { 71 | height: 32px; 72 | width: 32px; 73 | background-color: transparent; 74 | border: none; 75 | color: #272727; 76 | font-size: 16px; 77 | } 78 | 79 | #title-bar-btns button:hover { 80 | background-color: #272727; 81 | color: #fff; 82 | } 83 | 84 | #page { 85 | position: fixed; 86 | left: 0; 87 | right: 0; 88 | top: 32px; 89 | bottom: 24px; 90 | /*color: #272727;*/ 91 | } 92 | 93 | a { 94 | color: #272727; 95 | } 96 | 97 | #status { 98 | color: #fff0ff; 99 | position: fixed; 100 | left: 5px; 101 | bottom: 0; 102 | } 103 | 104 | * { 105 | -webkit-user-select: none; 106 | user-select: none; 107 | } 108 | -------------------------------------------------------------------------------- /electron/window.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { BrowserWindow } from "electron"; 12 | import { Server } from "./server"; 13 | import * as path from "path"; 14 | 15 | export class Window { 16 | private readonly window: BrowserWindow; 17 | private readonly server: Server; 18 | closed: () => void; 19 | 20 | constructor(private readonly baseUrl: string) { 21 | // Create the browser window. 22 | this.window = new BrowserWindow({ 23 | show: false, 24 | width: 1024, 25 | height: 728, 26 | frame: false, 27 | icon: __dirname + "/icons/favicon.png", 28 | title: "Slye", 29 | webPreferences: { 30 | // We don't want Node in the renderer thread. 31 | // Every file access should be done in the main thread. 32 | nodeIntegration: false, 33 | preload: path.join(__dirname, "preload.js") 34 | } 35 | }); 36 | 37 | // Disable default menu bar. 38 | this.window.setMenu(null); 39 | 40 | // Set contexts. 41 | this.didFinishLoadHandler = this.didFinishLoadHandler.bind(this); 42 | this.closedHandler = this.closedHandler.bind(this); 43 | 44 | // Browser window events. 45 | this.window.webContents.on("did-finish-load", this.didFinishLoadHandler); 46 | this.window.on("closed", this.closedHandler); 47 | 48 | this.server = new Server(this.window); 49 | } 50 | 51 | private didFinishLoadHandler() { 52 | this.window.setTitle("Slye"); 53 | this.window.show(); 54 | this.window.focus(); 55 | } 56 | 57 | private closedHandler() { 58 | if (this.closed) this.closed(); 59 | } 60 | 61 | open() { 62 | this.window.loadURL(this.baseUrl); 63 | } 64 | 65 | async openFile(file: string) { 66 | try { 67 | const pd = await this.server.openFile(file); 68 | this.window.loadURL(`${this.baseUrl}?pd=${pd}`); 69 | } catch (e) { 70 | console.log(e); 71 | this.window.close(); 72 | } 73 | } 74 | 75 | openDevTools() { 76 | this.window.webContents.openDevTools(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /fontkit.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | declare module "fontkit" { 12 | interface PathCommand { 13 | command: 14 | | "moveTo" 15 | | "lineTo" 16 | | "quadraticCurveTo" 17 | | "bezierCurveTo" 18 | | "closePath"; 19 | args: number[]; 20 | } 21 | 22 | interface Path { 23 | commands: PathCommand[]; 24 | } 25 | 26 | interface Glyph { 27 | id: number; 28 | codePoints: number[]; 29 | path: Path; 30 | advanceWidth: number; 31 | } 32 | 33 | interface GlyphRun { 34 | glyphs: Glyph[]; 35 | } 36 | 37 | interface Font { 38 | postscriptName: string; 39 | fullName: string; 40 | familyName: string; 41 | subfamilyName: string; 42 | copyright: string; 43 | version: string; 44 | head: { 45 | unitsPerEm: number; 46 | }; 47 | layout(text: string): GlyphRun; 48 | } 49 | 50 | function create(buffer: ArrayBuffer): Font; 51 | } 52 | -------------------------------------------------------------------------------- /frontend/controls/icons.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React from "react"; 12 | 13 | export function MoveIcon() { 14 | return ( 15 | 20 | 26 | 27 | ); 28 | } 29 | 30 | export function RotateIcon() { 31 | return ( 32 | 37 | 46 | 47 | ); 48 | } 49 | 50 | export function ScaleIcon() { 51 | return ( 52 | 57 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /frontend/controls/mapControl.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import * as THREE from "three"; 13 | import * as slye from "@slye/core"; 14 | 15 | const mapControls: WeakMap = new WeakMap(); 16 | 17 | export interface MapControlProps { 18 | renderer: slye.Renderer; 19 | disabled?: boolean; 20 | } 21 | 22 | export class MapControl extends Component { 23 | private mapControl: THREE.MapControls; 24 | 25 | constructor(props: MapControlProps) { 26 | super(props); 27 | 28 | if (!props.renderer) 29 | throw new Error("MapControl: `renderer` prop is required."); 30 | 31 | if (!mapControls.has(props.renderer)) mapControls.set(props.renderer, []); 32 | } 33 | 34 | componentWillReceiveProps(nextProps: MapControlProps) { 35 | if (nextProps.renderer !== this.props.renderer) 36 | throw new Error("MapControl: `renderer` can not be changed."); 37 | } 38 | 39 | componentWillMount() { 40 | const { renderer } = this.props; 41 | const stack = mapControls.get(renderer); 42 | 43 | if (stack.length) { 44 | this.mapControl = stack.pop(); 45 | } else { 46 | console.info("MapControl: New Instance."); 47 | const controls = new THREE.MapControls( 48 | this.props.renderer.camera, 49 | this.props.renderer.domElement 50 | ); 51 | 52 | controls.enableDamping = false; 53 | controls.dampingFactor = 0.25; 54 | controls.screenSpacePanning = false; 55 | controls.minDistance = 100; 56 | controls.maxDistance = 500; 57 | controls.maxPolarAngle = Math.PI / 2; 58 | this.mapControl = controls; 59 | 60 | // Maybe we can use less magic here? 61 | this.props.renderer.camera.rotation.set( 62 | -1.665337208354138e-16, 63 | -1.1942754044593986, 64 | -1.5486794606577854e-16 65 | ); 66 | controls.zoom0 = 1; 67 | controls.position0.set( 68 | -306.6180783491735, 69 | 1.9681294204195255e-14, 70 | 188.24827612525496 71 | ); 72 | controls.target0.set( 73 | -33.55458497635768, 74 | 1.701482260977322e-15, 75 | 80.28328349179617 76 | ); 77 | controls.reset(); 78 | } 79 | } 80 | 81 | componentWillUnmount() { 82 | this.mapControl.enabled = false; 83 | const stack = mapControls.get(this.props.renderer); 84 | stack.push(this.mapControl); 85 | } 86 | 87 | render(): null { 88 | this.mapControl.enabled = !this.props.disabled; 89 | return null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /frontend/controls/orbitControl.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import * as THREE from "three"; 13 | import * as slye from "@slye/core"; 14 | 15 | const orbitControls: WeakMap< 16 | slye.Renderer, 17 | THREE.OrbitControls[] 18 | > = new WeakMap(); 19 | 20 | export interface OrbitControlProps { 21 | renderer: slye.Renderer; 22 | center: THREE.Object3D; 23 | disabled?: boolean; 24 | } 25 | 26 | export class OrbitControl extends Component { 27 | private orbitControl: THREE.OrbitControls; 28 | 29 | constructor(props: OrbitControlProps) { 30 | super(props); 31 | 32 | if (!props.renderer) 33 | throw new Error("OrbitControl: `renderer` prop is required."); 34 | 35 | if (!orbitControls.has(props.renderer)) 36 | orbitControls.set(props.renderer, []); 37 | } 38 | 39 | componentWillReceiveProps(nextProps: OrbitControlProps) { 40 | if (nextProps.renderer !== this.props.renderer) 41 | throw new Error("OrbitControl: `renderer` can not be changed."); 42 | } 43 | 44 | componentWillMount() { 45 | const { renderer } = this.props; 46 | const stack = orbitControls.get(renderer); 47 | 48 | if (stack.length) { 49 | this.orbitControl = stack.pop(); 50 | } else { 51 | console.info("OrbitControl: New Instance."); 52 | this.orbitControl = new THREE.OrbitControls( 53 | this.props.renderer.camera, 54 | this.props.renderer.domElement 55 | ); 56 | } 57 | } 58 | 59 | componentWillUnmount() { 60 | this.orbitControl.enabled = false; 61 | const stack = orbitControls.get(this.props.renderer); 62 | stack.push(this.orbitControl); 63 | } 64 | 65 | render(): null { 66 | this.orbitControl.enabled = !this.props.disabled && !!this.props.center; 67 | this.orbitControl.target.copy(this.props.center.position); 68 | return null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import Typography from "@material-ui/core/Typography"; 13 | import Button from "@material-ui/core/Button"; 14 | import TextField from "@material-ui/core/TextField"; 15 | import Fab from "@material-ui/core/Fab"; 16 | import OpenIcon from "@material-ui/icons/FolderOpenSharp"; 17 | 18 | export interface DashboardProps { 19 | onCreate(title: string, description: string): void; 20 | onOpen(): void; 21 | } 22 | 23 | export class Dashboard extends Component { 24 | title: string = ""; 25 | description: string = ""; 26 | 27 | create = () => { 28 | this.props.onCreate(this.title, this.description); 29 | }; 30 | 31 | render() { 32 | return ( 33 |
34 | 35 | Create your next stunning presentation... 36 | 37 |
38 |
39 | (this.title = e.currentTarget.value)} 45 | /> 46 |
47 | 50 | (this.description = e.currentTarget.value)} 58 | /> 59 | 60 | 61 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | const styles: Record = { 68 | titleWrapper: { 69 | width: "calc(100% - 115px)", 70 | marginRight: 15, 71 | display: "inline-flex" 72 | }, 73 | fab: { 74 | position: "absolute", 75 | right: 25, 76 | bottom: 25 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /frontend/editor/componentEditor.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component, Fragment } from "react"; 12 | import { Widgets } from "./widgets"; 13 | import * as slye from "@slye/core"; 14 | import * as UI from "../../core/ui"; 15 | 16 | import Paper from "@material-ui/core/Paper"; 17 | import Button from "@material-ui/core/Button"; 18 | import Grid from "@material-ui/core/Grid"; 19 | 20 | export interface ComponentEditorProps { 21 | renderer: slye.Renderer; 22 | component: slye.ThreeComponent; 23 | x: number; 24 | y: number; 25 | } 26 | 27 | export interface ComponentUIState { 28 | values: Record; 29 | } 30 | 31 | export class ComponentEditor extends Component< 32 | ComponentEditorProps, 33 | ComponentUIState 34 | > { 35 | state: ComponentUIState = { 36 | values: {} 37 | }; 38 | 39 | constructor(props: ComponentEditorProps) { 40 | super(props); 41 | this.setup(props.component, false); 42 | } 43 | 44 | setup(component: slye.ThreeComponent, m: boolean): void { 45 | const { ui } = component; 46 | const values: Record = {}; 47 | 48 | for (let i = 0; i < ui.length; ++i) { 49 | const { name } = ui[i]; 50 | values[name] = component.props[name]; 51 | } 52 | 53 | if (m) { 54 | this.setState({ values }); 55 | } else { 56 | this.state = { 57 | ...this.state, 58 | values 59 | }; 60 | } 61 | } 62 | 63 | onChange = (name: string, value: any) => { 64 | this.setState(state => ({ 65 | values: { 66 | ...state.values, 67 | [name]: value 68 | } 69 | })); 70 | }; 71 | 72 | componentWillReceiveProps(nextProps: ComponentEditorProps) { 73 | if (this.props.component === nextProps.component) return; 74 | this.setup(nextProps.component, true); 75 | } 76 | 77 | done = () => { 78 | const { renderer, component } = this.props; 79 | renderer.actions.updateProps(component, this.state.values); 80 | }; 81 | 82 | render() { 83 | const { x, y, component } = this.props; 84 | const { values } = this.state; 85 | const { ui } = component; 86 | const left = Math.min(x, innerWidth - 600); 87 | 88 | return ( 89 | 90 | 91 | {ui.map(({ name, size, widget }) => { 92 | const Widget = Widgets[widget] as any; 93 | return ( 94 | 95 | this.onChange(name, value)} 98 | /> 99 | 100 | ); 101 | })} 102 | 103 | 104 | 113 | 114 | ); 115 | } 116 | } 117 | 118 | const styles: Record = { 119 | container: { 120 | position: "fixed", 121 | alignItems: "center", 122 | maxWidth: 500, 123 | width: 500 124 | }, 125 | button: { 126 | margin: 10, 127 | width: "calc(100% - 20px)" 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /frontend/editor/player.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import Screenfull from "screenfull"; 13 | import * as slye from "@slye/core"; 14 | 15 | export interface PlayerProps { 16 | renderer: slye.Renderer; 17 | onExit: () => void; 18 | } 19 | 20 | export class Player extends Component { 21 | private fullScreen: boolean = true; 22 | 23 | componentWillReceiveProps(nextProps: PlayerProps) { 24 | if (nextProps.renderer !== this.props.renderer) 25 | throw new Error("Player: `renderer` can not be changed."); 26 | } 27 | 28 | componentWillMount() { 29 | this.props.renderer.setState("player"); 30 | document.addEventListener("keydown", this.onKeydown); 31 | document.addEventListener("keyup", this.onKeyUp); 32 | document.addEventListener("touchstart", this.onTouchStart); 33 | if (Screenfull) { 34 | Screenfull.request(this.props.renderer.domElement); 35 | Screenfull.on("change", this.handleScreenfull); 36 | this.props.renderer.resize(innerWidth, innerHeight); 37 | } 38 | } 39 | 40 | componentWillUnmount() { 41 | document.removeEventListener("keydown", this.onKeydown); 42 | document.removeEventListener("keyup", this.onKeyUp); 43 | document.removeEventListener("touchstart", this.onTouchStart); 44 | if (Screenfull) Screenfull.exit(); 45 | } 46 | 47 | // Events. 48 | onKeydown = (event: KeyboardEvent): void => { 49 | if ( 50 | event.keyCode === 9 || 51 | (event.keyCode >= 32 && event.keyCode <= 34) || 52 | (event.keyCode >= 37 && event.keyCode <= 40) 53 | ) { 54 | event.preventDefault(); 55 | } 56 | }; 57 | 58 | onKeyUp = (event: KeyboardEvent): void => { 59 | switch (event.keyCode) { 60 | case 33: // Pg up 61 | case 37: // Left 62 | case 38: // Up 63 | this.props.renderer.prev(); 64 | event.preventDefault(); 65 | break; 66 | case 9: // Tab 67 | case 32: // Space 68 | case 34: // pg down 69 | case 39: // Right 70 | case 40: // Down 71 | this.props.renderer.next(); 72 | event.preventDefault(); 73 | break; 74 | case 27: // Escape 75 | this.props.onExit(); 76 | event.preventDefault(); 77 | break; 78 | case 122: // F11 79 | this.toggleFullScreen(); 80 | event.preventDefault(); 81 | break; 82 | } 83 | }; 84 | 85 | onTouchStart = (event: TouchEvent): void => { 86 | if (event.touches.length === 1) { 87 | const x = event.touches[0].clientX; 88 | const width = innerWidth * 0.3; 89 | if (x < width) { 90 | this.props.renderer.prev(); 91 | } else if (x > innerWidth - width) { 92 | this.props.renderer.next(); 93 | } 94 | } 95 | }; 96 | 97 | handleScreenfull = () => { 98 | if (this.fullScreen && Screenfull && !Screenfull.isFullscreen) { 99 | this.props.onExit(); 100 | Screenfull.off("change", this.handleScreenfull); 101 | } 102 | }; 103 | 104 | toggleFullScreen = () => { 105 | if (!Screenfull) return; 106 | const isFullscreen = Screenfull && Screenfull.isFullscreen; 107 | this.fullScreen = !this.fullScreen; 108 | if (this.fullScreen) { 109 | Screenfull.request(this.props.renderer.domElement); 110 | } else { 111 | Screenfull.exit(); 112 | } 113 | }; 114 | 115 | render(): null { 116 | return null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /frontend/editor/thumbnail.scss: -------------------------------------------------------------------------------- 1 | .thumbnails-container { 2 | position: fixed; 3 | height: 100px; 4 | bottom: -66px; 5 | transition: bottom 0.5s ease-in 0s; 6 | border-radius: 25px 25px 0 0 !important; 7 | width: 600px; 8 | left: calc(50vw - 300px); 9 | &:hover { 10 | bottom: 24px; 11 | } 12 | 13 | .thumbnails-list { 14 | margin: 5px; 15 | margin-top: 25px; 16 | overflow-x: auto; 17 | white-space: nowrap; 18 | height: 80px; 19 | width: auto !important; 20 | } 21 | 22 | .thumbnail { 23 | display: inline-block; 24 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 25 | width: 105px; 26 | height: 60px; 27 | margin-right: 7px; 28 | margin-left: 7px; 29 | cursor: pointer; 30 | 31 | span { 32 | position: relative; 33 | display: inline-block; 34 | text-align: center; 35 | width: 40px; 36 | height: 20px; 37 | border-radius: 6px; 38 | left: calc(105px / 2 - 20px); 39 | top: 35px; 40 | background-color: rgba(0, 0, 0, 0.54); 41 | color: #fff; 42 | } 43 | } 44 | 45 | .add-btn { 46 | margin: -15px; 47 | top: 21px; 48 | left: 15px; 49 | margin-right: 25px; 50 | } 51 | 52 | canvas { 53 | position: absolute; 54 | top: 30px; 55 | } 56 | 57 | .toggle { 58 | width: calc(100% - 60px); 59 | height: 20px; 60 | position: absolute; 61 | top: 0; 62 | left: 30px; 63 | cursor: pointer; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/editor/tour/arrow-orange.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/frontend/editor/tour/arrow-orange.gif -------------------------------------------------------------------------------- /frontend/editor/tour/arrow.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React from "react"; 12 | 13 | export interface ArrowProps { 14 | variant: "down" | "left" | "up" | "right"; 15 | x: string; 16 | y: string; 17 | } 18 | 19 | export function Arrow(props: ArrowProps) { 20 | const { variant, x, y } = props; 21 | 22 | const bottom = `calc(${y})`; 23 | const left = `calc(${x})`; 24 | const transform = `rotate(${ 25 | { 26 | down: 90, 27 | up: -90, 28 | left: 180, 29 | right: 0 30 | }[variant] 31 | }deg)`; 32 | 33 | return ( 34 | 48 | 49 | 53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /frontend/editor/tour/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component, Fragment } from "react"; 12 | 13 | import { Mouse } from "./mouse"; 14 | import { Arrow } from "./arrow"; 15 | import { steps } from "./steps"; 16 | 17 | import Button from "@material-ui/core/Button"; 18 | import Dialog from "@material-ui/core/Dialog"; 19 | import DialogActions from "@material-ui/core/DialogActions"; 20 | import DialogContent from "@material-ui/core/DialogContent"; 21 | import DialogContentText from "@material-ui/core/DialogContentText"; 22 | import DialogTitle from "@material-ui/core/DialogTitle"; 23 | import Paper from "@material-ui/core/Paper"; 24 | import Typography from "@material-ui/core/Typography"; 25 | import MobileStepper from "@material-ui/core/MobileStepper"; 26 | import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft"; 27 | import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"; 28 | 29 | import "./tour.scss"; 30 | 31 | interface TourState { 32 | open: boolean; 33 | activeStep: number; 34 | } 35 | 36 | export class Tour extends Component<{}, TourState> { 37 | state = { 38 | open: true, 39 | activeStep: -1 40 | }; 41 | 42 | handleClose = () => { 43 | this.setState({ open: false }); 44 | }; 45 | 46 | handleStart = () => { 47 | this.setState({ open: false, activeStep: 0 }); 48 | }; 49 | 50 | handleNext = () => { 51 | this.setState(state => ({ activeStep: state.activeStep + 1 })); 52 | }; 53 | 54 | handleBack = () => { 55 | this.setState(state => ({ activeStep: state.activeStep - 1 })); 56 | }; 57 | 58 | handleNeverAskAgain = () => { 59 | this.setState({ open: false, activeStep: steps.length }); 60 | }; 61 | 62 | render() { 63 | const { open, activeStep } = this.state; 64 | 65 | if (activeStep === steps.length) { 66 | localStorage.setItem("slye-tour-finished", "1"); 67 | return null; 68 | } 69 | 70 | if (activeStep > -1) { 71 | const maxStep = steps.length - 1; 72 | const step = steps[activeStep]; 73 | return ( 74 | 75 |
76 | 84 | {activeStep === maxStep ? "Finish" : "Next"} 85 | 86 | 87 | } 88 | backButton={ 89 | 97 | } 98 | /> 99 | 100 | {step.text} 101 | 102 |
103 | {step.mouse ? : null} 104 | {step.arrows ? step.arrows.map(props => ) : null} 105 |
106 | ); 107 | } 108 | 109 | return ( 110 | 115 | Hey! 116 | 117 | 118 | A tour can walk you through the app so that you can learn more about 119 | it. Do you want to start the tour? 120 | 121 | 122 | 123 | 126 | 129 | 132 | 133 | 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /frontend/editor/tour/mouse.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | 13 | export interface MouseProps { 14 | leftActive?: boolean; 15 | rightActive?: boolean; 16 | caption?: string; 17 | keys?: string[] | string; 18 | arrowLeft?: boolean; 19 | arrowRight?: boolean; 20 | } 21 | 22 | export class Mouse extends Component { 23 | render() { 24 | const { 25 | arrowRight, 26 | arrowLeft, 27 | leftActive, 28 | rightActive, 29 | caption, 30 | keys: keysProp 31 | } = this.props; 32 | const rightColor = rightActive ? "#f6982e" : "#dfdfdf"; 33 | const leftColor = leftActive ? "#f6982e" : "#dfdfdf"; 34 | const keys: string[] = keysProp 35 | ? typeof keysProp === "string" 36 | ? [keysProp] 37 | : keysProp 38 | : []; 39 | 40 | return ( 41 |
42 |
43 | {keys.map(name => ( 44 | {name.toUpperCase()} 45 | ))} 46 |
47 | {arrowLeft &&
} 48 | {arrowRight &&
} 49 | {caption ?
{caption}
: null} 50 | 57 | 61 | 65 | 69 | 73 | 77 | 81 | 82 |
83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /frontend/editor/tour/tour.scss: -------------------------------------------------------------------------------- 1 | .tour-step { 2 | width: 400px; 3 | position: fixed; 4 | top: 40px; 5 | left: 10px; 6 | 7 | .stepper { 8 | width: 100%; 9 | } 10 | 11 | .body { 12 | width: 100%; 13 | padding: 8px; 14 | 15 | br { 16 | line-height: 35px; 17 | } 18 | } 19 | } 20 | 21 | .tour-mouse-container { 22 | position: fixed; 23 | width: 240px; 24 | height: 150px; 25 | right: 10px; 26 | bottom: 30px; 27 | border: 1px solid #8a8a8a; 28 | box-shadow: -5px 5px 6px 0px #8a8a8a; 29 | background: #548c6b; 30 | 31 | div.arrow { 32 | background: url("./arrow-orange.gif"); 33 | height: 150px; 34 | width: 150px; 35 | background-size: cover; 36 | position: absolute; 37 | top: 0; 38 | &.left { 39 | left: 0; 40 | transform: rotate(90deg) scale(0.5) translateY(45px); 41 | } 42 | &.right { 43 | right: 0; 44 | transform: rotate(-90deg) scale(0.5) translateY(60px); 45 | } 46 | } 47 | 48 | .keys { 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | } 53 | 54 | .caption { 55 | position: absolute; 56 | width: 100%; 57 | left: 0; 58 | bottom: 0; 59 | height: 20px; 60 | padding: 2px; 61 | background: #303030; 62 | color: #eee; 63 | } 64 | 65 | .tour-mouse { 66 | position: absolute; 67 | left: calc(50% - 48px); 68 | top: calc(50% - 48px); 69 | transition: left 0.4s ease, top 0.4s ease; 70 | } 71 | 72 | & path { 73 | transition: fill 0.4s ease; 74 | } 75 | } 76 | 77 | kbd { 78 | display: inline-block; 79 | margin: 5px; 80 | margin-right: 0; 81 | background: #303030; 82 | color: #efefef; 83 | padding: 5px; 84 | border-radius: 5px; 85 | min-width: 20px; 86 | text-align: center; 87 | } 88 | -------------------------------------------------------------------------------- /frontend/editor/widgets/color.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import * as THREE from "three"; 13 | import { SketchPicker, ColorResult } from "react-color"; 14 | 15 | import Popper from "@material-ui/core/Popper"; 16 | import Button from "@material-ui/core/Button"; 17 | import Fade from "@material-ui/core/Fade"; 18 | 19 | export interface ColorWidgetProps { 20 | value: number; 21 | onChange(color: number): void; 22 | } 23 | 24 | export interface ColorWidgetState { 25 | open: boolean; 26 | anchorEl: any; 27 | } 28 | 29 | export class ColorWidget extends Component { 30 | state: ColorWidgetState = { 31 | open: false, 32 | anchorEl: null 33 | }; 34 | 35 | onClick = (event: React.MouseEvent): void => { 36 | const { currentTarget } = event; 37 | this.setState(state => ({ 38 | anchorEl: currentTarget, 39 | open: !state.open 40 | })); 41 | }; 42 | 43 | onChange = (color: ColorResult): void => { 44 | const hex = new THREE.Color(color.hex).getHex(); 45 | this.props.onChange(hex); 46 | }; 47 | 48 | render() { 49 | const { open, anchorEl } = this.state; 50 | const { value } = this.props; 51 | const color = new THREE.Color(value).getStyle(); 52 | 53 | return ( 54 |
55 |
59 | 60 | {({ TransitionProps }) => ( 61 | 62 | 67 | 68 | )} 69 | 70 |
71 | ); 72 | } 73 | } 74 | 75 | const styles: Record = { 76 | button: { 77 | width: 25, 78 | height: 25, 79 | border: "1px solid #373737", 80 | cursor: "pointer" 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /frontend/editor/widgets/file.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import * as slye from "@slye/core"; 13 | 14 | import CloudUploadIcon from "@material-ui/icons/CloudUpload"; 15 | import Button from "@material-ui/core/Button"; 16 | 17 | export interface FileWidgetProps { 18 | value: slye.FileBase; 19 | onChange(file: slye.FileBase): void; 20 | } 21 | 22 | export class FileWidget extends Component { 23 | handleClick = async () => { 24 | if (this.props.value.isModuleAsset) 25 | throw new Error("FileWidget only works for presentation assets."); 26 | const { owner } = this.props.value; 27 | const { files } = await client.showFileDialog(owner); 28 | if (!files || !files.length) return; 29 | const file = new slye.File(owner, files[0], false); 30 | this.props.onChange(file); 31 | }; 32 | 33 | render() { 34 | return ( 35 | 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/editor/widgets/font.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import * as slye from "@slye/core"; 13 | 14 | import Select from "@material-ui/core/Select"; 15 | import MenuItem from "@material-ui/core/MenuItem"; 16 | 17 | export interface FontWidgetProps { 18 | value: slye.FontBase; 19 | onChange(font: slye.FontBase): void; 20 | } 21 | 22 | export class FontWidget extends Component { 23 | render() { 24 | const fonts = slye.getFonts(); 25 | const value = fonts.indexOf(this.props.value); 26 | 27 | const handleChange = (event: any): void => { 28 | this.props.onChange(fonts[event.target.value]); 29 | }; 30 | 31 | // TODO(qti3e) Virtual scrolling and font preview. 32 | return ( 33 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/editor/widgets/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as UI from "../../../core/ui"; 12 | 13 | import { TextWidget } from "./text"; 14 | import { SizeWidget } from "./size"; 15 | import { FontWidget } from "./font"; 16 | import { ColorWidget } from "./color"; 17 | import { FileWidget } from "./file"; 18 | 19 | export const Widgets = { 20 | [UI.TEXT]: TextWidget, 21 | [UI.SIZE]: SizeWidget, 22 | [UI.FONT]: FontWidget, 23 | [UI.COLOR]: ColorWidget, 24 | [UI.FILE]: FileWidget 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/editor/widgets/size.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | 13 | import InputBase from "@material-ui/core/InputBase"; 14 | 15 | export interface SizeWidgetProps { 16 | value: string; 17 | onChange(num: number): void; 18 | } 19 | 20 | export class SizeWidget extends Component { 21 | handleChange = (event: React.ChangeEvent): void => { 22 | this.props.onChange(Number(event.target.value)); 23 | }; 24 | 25 | render() { 26 | return ( 27 | 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/editor/widgets/text.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import { RTLify } from "persian-utils"; 13 | 14 | import InputBase from "@material-ui/core/InputBase"; 15 | 16 | export interface TextWidgetProps { 17 | value: string; 18 | onChange(text: string): void; 19 | } 20 | 21 | export class TextWidget extends Component { 22 | handleChange = (event: React.ChangeEvent): void => { 23 | this.props.onChange(event.target.value); 24 | }; 25 | 26 | render() { 27 | return ( 28 | 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/editor/worldEditor.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component, Fragment } from "react"; 12 | import * as slye from "@slye/core"; 13 | 14 | import { TransformControl } from "../controls/transformControl"; 15 | import { MapControl } from "../controls/mapControl"; 16 | 17 | export interface WorldEditorProps { 18 | renderer: slye.Renderer; 19 | onSelect: (step: slye.ThreeStep) => void; 20 | editStep: (step: slye.ThreeStep) => void; 21 | } 22 | 23 | interface WorldEditorState { 24 | focusedStep: slye.ThreeStep; 25 | transform: boolean; 26 | } 27 | 28 | export class WorldEditor extends Component { 29 | state: WorldEditorState = { 30 | focusedStep: undefined, 31 | transform: false 32 | }; 33 | 34 | private hoverdStep: slye.ThreeStep; 35 | 36 | componentWillReceiveProps(nextProps: WorldEditorProps) { 37 | if (nextProps.renderer !== this.props.renderer) 38 | throw new Error("WorldEditor: `renderer` can not be changed."); 39 | } 40 | 41 | componentWillMount() { 42 | this.props.renderer.setState("map"); 43 | const { domElement } = this.props.renderer; 44 | domElement.addEventListener("mousemove", this.onMouseMove); 45 | domElement.addEventListener("click", this.onClick); 46 | domElement.addEventListener("dblclick", this.onDblClick); 47 | document.addEventListener("keyup", this.onKeyup); 48 | } 49 | 50 | componentWillUnmount() { 51 | const { domElement } = this.props.renderer; 52 | domElement.style.cursor = "auto"; 53 | domElement.removeEventListener("mousemove", this.onMouseMove); 54 | domElement.removeEventListener("click", this.onClick); 55 | domElement.removeEventListener("dblclick", this.onDblClick); 56 | document.removeEventListener("keyup", this.onKeyup); 57 | } 58 | 59 | onMouseMove = (event: MouseEvent): void => { 60 | const { renderer } = this.props; 61 | this.hoverdStep = renderer.raycaster.raycastStep(); 62 | renderer.domElement.style.cursor = this.hoverdStep ? "pointer" : "auto"; 63 | }; 64 | 65 | onClick = (event: MouseEvent): void => { 66 | if (this.state.focusedStep && !this.hoverdStep) return; 67 | if (this.state.focusedStep === this.hoverdStep) return; 68 | 69 | this.setState({ focusedStep: this.hoverdStep, transform: true }); 70 | this.props.renderer.domElement.style.cursor = "auto"; 71 | if (this.hoverdStep) this.props.onSelect(this.hoverdStep); 72 | }; 73 | 74 | onDblClick = (event: MouseEvent): void => { 75 | if (this.hoverdStep) this.props.editStep(this.hoverdStep); 76 | }; 77 | 78 | onKeyup = (event: KeyboardEvent): void => { 79 | const { keyCode } = event; 80 | 81 | // Escape. 82 | if (this.state.transform && keyCode === 27) { 83 | this.setState({ focusedStep: undefined, transform: false }); 84 | } 85 | 86 | // Delete 87 | if (keyCode === 46) { 88 | const step = this.state.focusedStep; 89 | if (step) { 90 | this.props.renderer.actions.deleteStep(step); 91 | this.setState({ focusedStep: undefined }); 92 | this.props.onSelect(undefined); 93 | } 94 | } 95 | }; 96 | 97 | render() { 98 | const { renderer } = this.props; 99 | const { focusedStep, transform } = this.state; 100 | 101 | return ( 102 | 103 | {transform && focusedStep ? ( 104 | 105 | ) : null} 106 | {!transform ? : null} 107 | 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /frontend/gloabl.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { Remote } from "electron"; 12 | import { Client } from "./ipc"; 13 | import * as Slye from "../core"; 14 | import * as Three from "three"; 15 | 16 | declare global { 17 | interface Window { 18 | client: Client; 19 | slye: typeof Slye; 20 | THREE: typeof Three; 21 | slyeModulesTable: Map; 22 | } 23 | const client: Client; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React from "react"; 12 | import ReactDOM from "react-dom"; 13 | import { Main } from "./main"; 14 | 15 | import * as Slye from "@slye/core"; 16 | import * as Three from "three"; 17 | 18 | // For module loader & THREE modules. 19 | window.slye = Slye; 20 | window.THREE = Three; 21 | 22 | import "three/examples/js/controls/TransformControls"; 23 | import "three/examples/js/controls/OrbitControls"; 24 | import "three/examples/js/controls/MapControls"; 25 | 26 | Slye.setServer({ 27 | requestModule(moduleName: string): Promise { 28 | console.log("req module", moduleName); 29 | return new Promise(async resolve => { 30 | const url = await client.getModuleMainURL(moduleName); 31 | const script = document.createElement("script"); 32 | script.type = "text/javascript"; 33 | script.async = true; 34 | script.src = url; 35 | script.onload = () => resolve(true); 36 | document.head.appendChild(script); 37 | }); 38 | }, 39 | async fetchModuleAsset( 40 | moduleName: string, 41 | assetKey: string 42 | ): Promise { 43 | const url = await client.getModuleAssetURL(moduleName, assetKey); 44 | const res = await fetch(url); 45 | return res.arrayBuffer(); 46 | }, 47 | async fetchAsset( 48 | presentationId: string, 49 | asset: string 50 | ): Promise { 51 | const url = await client.getAssetURL(presentationId, asset); 52 | const res = await fetch(url); 53 | return res.arrayBuffer(); 54 | }, 55 | async showFileDialog(presentationId: string): Promise { 56 | const res = await client.showFileDialog(presentationId); 57 | return res.files.map(id => new Slye.File(presentationId, id, false)); 58 | }, 59 | getAssetURL(presentationId: string, asset: string): Promise { 60 | return client.getAssetURL(presentationId, asset); 61 | } 62 | }); 63 | 64 | document.addEventListener("readystatechange", () => { 65 | if (document.readyState !== "complete") return; 66 | const root = document.getElementById("page"); 67 | ReactDOM.render(
, root); 68 | }); 69 | -------------------------------------------------------------------------------- /frontend/main.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import React, { Component } from "react"; 12 | import { Dashboard } from "./dashboard"; 13 | import { App } from "./editor"; 14 | 15 | interface MainState { 16 | presentationDescriptor?: string; 17 | } 18 | 19 | function getQueryVariable(variable: string): string { 20 | var query = window.location.search.substring(1); 21 | var vars = query.split("&"); 22 | for (var i = 0; i < vars.length; i++) { 23 | var pair = vars[i].split("="); 24 | if (decodeURIComponent(pair[0]) == variable) { 25 | return decodeURIComponent(pair[1]); 26 | } 27 | } 28 | } 29 | 30 | export class Main extends Component<{}, MainState> { 31 | constructor(props: {}) { 32 | super(props); 33 | const pd = getQueryVariable("pd"); 34 | this.state = { presentationDescriptor: pd }; 35 | } 36 | 37 | create = async (title: string, description: string) => { 38 | const { presentationDescriptor } = await client.create(); 39 | 40 | client.patchMeta(presentationDescriptor, { 41 | title, 42 | description, 43 | created: Date.now() 44 | }); 45 | 46 | this.setState({ 47 | presentationDescriptor 48 | }); 49 | }; 50 | 51 | open = async () => { 52 | const { ok, presentationDescriptor } = await client.open(); 53 | if (ok) this.setState({ presentationDescriptor }); 54 | }; 55 | 56 | render() { 57 | const { presentationDescriptor } = this.state; 58 | if (presentationDescriptor) 59 | return ; 60 | return ; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /frontend/three.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace THREE { 2 | export class TransformControls extends THREE.Object3D { 3 | public enabled: boolean; 4 | public showX: boolean; 5 | public showY: boolean; 6 | public showZ: boolean; 7 | public mode: "translate" | "rotate" | "scale"; 8 | public space: "world" | "local"; 9 | public translationSnap: number; 10 | public rotationSnap: number; 11 | public size: number; 12 | 13 | constructor(camera: THREE.Camera, dom?: HTMLElement); 14 | 15 | attach(o: THREE.Object3D): void; 16 | detach(): void; 17 | 18 | setMode(m: "translate" | "rotate" | "scale"): void; 19 | setSpace(space: "world" | "local"): void; 20 | setTranslationSnap(n: number): void; 21 | setRotationSnap(n: number): void; 22 | setSize(n: number): void; 23 | } 24 | 25 | export class OrbitControls { 26 | public enabled: boolean; 27 | public readonly target: THREE.Vector3; 28 | 29 | constructor(camera: THREE.Camera, dom?: HTMLElement); 30 | } 31 | 32 | export class MapControls { 33 | public enabled: boolean; 34 | public enableDamping: boolean; 35 | public dampingFactor: number; 36 | public screenSpacePanning: boolean; 37 | public minDistance: number; 38 | public maxDistance: number; 39 | public maxPolarAngle: number; 40 | public zoom0: number; 41 | public position0: THREE.Vector3; 42 | public target0: THREE.Vector3; 43 | 44 | constructor(camera: THREE.Camera, dom?: HTMLElement); 45 | 46 | saveState(): void; 47 | reset(): void; 48 | update(): void; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | export function sleep(t: number): Promise { 12 | return new Promise(resolve => setTimeout(resolve, t)); 13 | } 14 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const path = require("path"); 3 | const rollup = require("./gulpfile/rollup"); 4 | const parcel = require("./gulpfile/parcel"); 5 | const slyeModule = require("./gulpfile/module"); 6 | const packager = require("./gulpfile/packager"); 7 | const rm = require("./gulpfile/rm"); 8 | const serve = require("./gulpfile/server"); 9 | const tester = require("./gulpfile/tester"); 10 | const deploy = require("./gulpfile/deploy"); 11 | 12 | // Cleanup script 13 | gulp.task("clean", rm("dist")); 14 | 15 | // Modules 16 | gulp.task("modules:slye", slyeModule("slye")); 17 | gulp.task("modules", gulp.parallel("modules:slye")); 18 | 19 | // Electron 20 | gulp.task("electron:main", rollup("electron/main.ts", "main.js")); 21 | 22 | gulp.task( 23 | "electron:preload", 24 | rollup("electron/preload.ts", "preload.js", { 25 | external: ["electron"] 26 | }) 27 | ); 28 | 29 | gulp.task( 30 | "electron:renderer", 31 | parcel("electron/static/index.html", { 32 | external: ["electron"] 33 | }) 34 | ); 35 | 36 | gulp.task("electron:icons", function() { 37 | return gulp.src("icons/*").pipe(gulp.dest("./dist/icons")); 38 | }); 39 | 40 | gulp.task( 41 | "electron", 42 | gulp.parallel( 43 | "electron:main", 44 | "electron:preload", 45 | "electron:icons", 46 | "electron:renderer" 47 | ) 48 | ); 49 | 50 | // Electron Binary Packages 51 | gulp.task("package:win32", packager("win32", "ia32")); 52 | gulp.task("package:linux64", packager("linux", "x64")); 53 | 54 | // Web 55 | gulp.task("website", parcel("website/index.html")); 56 | gulp.task("webapp", parcel("web/app.html")); 57 | gulp.task("web", gulp.series("website", "webapp")); 58 | 59 | // Build Targets 60 | gulp.task("build:electron", gulp.series("clean", "modules", "electron")); 61 | gulp.task("build:web", gulp.series("clean", "modules", "web")); 62 | 63 | // Binary Releases 64 | gulp.task( 65 | "binary:all", 66 | gulp.series( 67 | "build:electron", 68 | gulp.parallel("package:win32", "package:linux64") 69 | ) 70 | ); 71 | 72 | // GitHub deploy 73 | gulp.task("deploy", gulp.series("build:web", deploy("dist"))); 74 | 75 | // Internal Static Server 76 | gulp.task("serve", serve("./dist", 8080)); 77 | 78 | // Unit Testing 79 | gulp.task("test:bundle", parcel("tests/test.html", { minify: false })); 80 | gulp.task("test:run", tester("http://localhost:8080/test.html")); 81 | gulp.task("test", gulp.series("serve", "test:bundle", "test:run")); 82 | 83 | exports.default = gulp.parallel("build:electron"); 84 | -------------------------------------------------------------------------------- /gulpfile/deploy.js: -------------------------------------------------------------------------------- 1 | const ghpages = require("gh-pages"); 2 | 3 | function deploy(dir) { 4 | return function(cb) { 5 | ghpages.publish(dir, err => { 6 | if (err) { 7 | console.error(err); 8 | process.exit(-1); 9 | return; 10 | } 11 | cb(); 12 | }); 13 | }; 14 | } 15 | 16 | module.exports = deploy; 17 | -------------------------------------------------------------------------------- /gulpfile/module.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const rollup = require("gulp-better-rollup"); 3 | const rename = require("gulp-rename"); 4 | const typescript = require("rollup-plugin-typescript"); 5 | const mergeStream = require("merge-stream"); 6 | 7 | const uglifyes = require("uglify-es"); 8 | const composer = require("gulp-uglify/composer"); 9 | const minify = composer(uglifyes, console); 10 | 11 | function slyeModule(name) { 12 | return function() { 13 | const rollupOptions = { 14 | external: ["@slye/core", "three"], 15 | plugins: [ 16 | typescript({ 17 | target: "esnext", 18 | module: "ESNext" 19 | }) 20 | ] 21 | }; 22 | 23 | const outputOptions = { 24 | format: "iife", 25 | name: "SlyeModule", 26 | globals: { 27 | "@slye/core": "slye", 28 | three: "THREE" 29 | } 30 | }; 31 | 32 | const jsStream = gulp 33 | .src("modules/" + name + "/main.ts") 34 | .pipe(rollup(rollupOptions, outputOptions)) 35 | .pipe(minify()) 36 | .pipe(rename("main.js")) 37 | .pipe(gulp.dest("./dist/modules/" + name)); 38 | 39 | const assets = gulp 40 | .src("modules/" + name + "/assets/**") 41 | .pipe(gulp.dest("./dist/modules/" + name + "/assets")); 42 | 43 | return mergeStream(jsStream, assets); 44 | }; 45 | } 46 | 47 | module.exports = slyeModule; 48 | -------------------------------------------------------------------------------- /gulpfile/packager.js: -------------------------------------------------------------------------------- 1 | const electronPackager = require("electron-packager"); 2 | const path = require("path"); 3 | 4 | const electronPackagerOptions = { 5 | name: "Slye", 6 | dir: path.join(__dirname, ".."), 7 | "app-copyright": "Parsa Ghadimi", 8 | out: "release", 9 | version: "0.0.1", 10 | overwrite: true, 11 | win32metadata: { 12 | CompanyName: "Slye", 13 | FileDescription: "Slye", 14 | OriginalFilename: "Slye", 15 | ProductName: "Slye", 16 | InternalName: "Slye" 17 | }, 18 | ignore: file => { 19 | if (!file) return false; 20 | if (file.startsWith("/dist")) return false; 21 | if (file.startsWith("/node_modules/@slye")) return true; 22 | if (file.startsWith("/node_modules")) return false; 23 | if (file === "/package.json") return false; 24 | return true; 25 | } 26 | }; 27 | 28 | function packager(platform, arch) { 29 | return function(cb) { 30 | electronPackager({ 31 | ...electronPackagerOptions, 32 | platform, 33 | arch 34 | }).then(data => { 35 | console.log("Wrote " + platform + "-" + arch + " package to " + data[0]); 36 | cb(); 37 | }); 38 | }; 39 | } 40 | 41 | module.exports = packager; 42 | -------------------------------------------------------------------------------- /gulpfile/parcel.js: -------------------------------------------------------------------------------- 1 | const Bundler = require("parcel-bundler"); 2 | const path = require("path"); 3 | 4 | function parcel(entryPoint, opts) { 5 | return function(cb) { 6 | const options = { 7 | autoinstall: false, 8 | cache: true, 9 | hmr: false, 10 | logLevel: 3, 11 | minify: true, 12 | outDir: path.join(__dirname, "..", "./dist"), 13 | publicUrl: "./", 14 | sourceMaps: true, 15 | watch: false, 16 | ...opts 17 | }; 18 | 19 | const entryPoints = [entryPoint]; 20 | 21 | const bundler = new Bundler(entryPoints, options); 22 | bundler.on("bundled", () => cb()); 23 | bundler.bundle(); 24 | }; 25 | } 26 | 27 | module.exports = parcel; 28 | -------------------------------------------------------------------------------- /gulpfile/rm.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const del = require("del"); 3 | 4 | function rm(dir) { 5 | return function(cb) { 6 | del.sync([dir]); 7 | cb(); 8 | }; 9 | } 10 | 11 | module.exports = rm; 12 | -------------------------------------------------------------------------------- /gulpfile/rollup.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const path = require("path"); 3 | const rollup = require("gulp-better-rollup"); 4 | const rename = require("gulp-rename"); 5 | const typescript = require("rollup-plugin-typescript"); 6 | const commonjs = require("rollup-plugin-commonjs"); 7 | const resolve = require("rollup-plugin-node-resolve"); 8 | 9 | const uglifyes = require("uglify-es"); 10 | const composer = require("gulp-uglify/composer"); 11 | const minify = composer(uglifyes, console); 12 | 13 | function slyeRollup(input, out, options = {}) { 14 | return function(cb) { 15 | const rollupOptions = { 16 | external: [ 17 | "electron", 18 | "fs", 19 | "path", 20 | "assert", 21 | "util", 22 | "events", 23 | "crypto", 24 | "os", 25 | // Archive extraction fails silently otherwise. 26 | "tar", 27 | // Never include three. 28 | "three" 29 | ], 30 | plugins: [ 31 | resolve({ 32 | preferBuiltins: true 33 | }), 34 | commonjs(), 35 | typescript({ 36 | target: "esnext", 37 | module: "ESNext" 38 | }) 39 | ], 40 | ...options 41 | }; 42 | 43 | return gulp 44 | .src(input) 45 | .pipe(rollup(rollupOptions, "cjs")) 46 | .pipe(minify()) 47 | .pipe(rename(out)) 48 | .pipe(gulp.dest("./dist")); 49 | }; 50 | } 51 | 52 | module.exports = slyeRollup; 53 | -------------------------------------------------------------------------------- /gulpfile/server.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const http = require("http"); 4 | const url = require("url"); 5 | const mime = require("mime"); 6 | 7 | function httpServer(dir = "./dist", port = 8080) { 8 | return function(cb) { 9 | const basePath = path.join(__dirname, "..", dir); 10 | const index = path.join(basePath, "index.html"); 11 | 12 | const server = http.createServer((req, res) => { 13 | const reqUrl = url.parse(req.url, true); 14 | const filePath = path.join(basePath, reqUrl.pathname); 15 | 16 | if (!filePath.startsWith(basePath)) { 17 | res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" }); 18 | res.end("Access denied."); 19 | return; 20 | } 21 | 22 | fs.stat(filePath, (err, stat) => { 23 | let finalPath = filePath; 24 | if ((err && err.code === "ENOENT") || stat.isDirectory()) { 25 | finalPath = index; 26 | } else if (err) { 27 | res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" }); 28 | res.end(`Unexpected server error occurred. [#${err.code}]`); 29 | return; 30 | } 31 | if (!fs.existsSync(finalPath)) { 32 | res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); 33 | res.end("Not Found"); 34 | return; 35 | } 36 | res.writeHead(200, { "Content-Type": mime.getType(finalPath) }); 37 | const stream = fs.createReadStream(finalPath); 38 | stream.pipe(res); 39 | }); 40 | }); 41 | 42 | server.listen(port, () => { 43 | cb(); 44 | console.log(`Server started listening on port ${port}...`); 45 | }); 46 | }; 47 | } 48 | 49 | module.exports = httpServer; 50 | -------------------------------------------------------------------------------- /gulpfile/tester.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | 3 | function tester(url) { 4 | return async function(cb) { 5 | const browser = await puppeteer.launch(); 6 | const page = await browser.newPage(); 7 | await page.goto(url); 8 | 9 | page.on("console", async msg => { 10 | const text = msg.text(); 11 | const args = []; 12 | for (let i = 0; i < msg.args().length; ++i) { 13 | args.push(await msg.args()[i].jsonValue()); 14 | } 15 | console.log(...args); 16 | if (text.indexOf("DONE. Test passed") > -1) { 17 | await browser.close(); 18 | const index = text.lastIndexOf(" "); 19 | const n = Number(text.substr(index + 1)); 20 | process.exit(n > 0 ? 1 : 0); 21 | } 22 | }); 23 | }; 24 | } 25 | 26 | module.exports = tester; 27 | -------------------------------------------------------------------------------- /icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/icons/favicon.ico -------------------------------------------------------------------------------- /icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/icons/favicon.png -------------------------------------------------------------------------------- /icons/slye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/icons/slye.png -------------------------------------------------------------------------------- /mktemp.d.ts: -------------------------------------------------------------------------------- 1 | declare module "mktemp" { 2 | function createDir(template: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /modules/slye/assets/emoji.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/modules/slye/assets/emoji.ttf -------------------------------------------------------------------------------- /modules/slye/assets/homa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/modules/slye/assets/homa.ttf -------------------------------------------------------------------------------- /modules/slye/assets/sahel.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/modules/slye/assets/sahel.ttf -------------------------------------------------------------------------------- /modules/slye/assets/shellia.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/modules/slye/assets/shellia.ttf -------------------------------------------------------------------------------- /modules/slye/components/picture.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as slye from "@slye/core"; 12 | import * as UI from "@slye/core/ui"; 13 | import * as THREE from "three"; 14 | 15 | export type PictureProps = { 16 | scale: number; 17 | file: slye.FileBase; 18 | }; 19 | 20 | export class Picture extends slye.ThreeComponent { 21 | ui: UI.UILayout = [ 22 | //{ name: "scale", widget: UI.SIZE, size: 4 }, 23 | { name: "file", widget: UI.FILE, size: 12 } 24 | ]; 25 | 26 | init() {} 27 | 28 | render() { 29 | const { scale, file } = this.props; 30 | 31 | const texture = new THREE.Texture(); 32 | texture.generateMipmaps = false; 33 | texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping; 34 | texture.minFilter = THREE.LinearFilter; 35 | 36 | const material = new THREE.MeshBasicMaterial({ 37 | side: THREE.DoubleSide, 38 | map: texture, 39 | transparent: true 40 | }); 41 | 42 | return new Promise(resolve => { 43 | const image = new Image(); 44 | file.url().then(url => (image.src = url)); 45 | image.onload = () => { 46 | texture.image = image; 47 | texture.needsUpdate = true; 48 | 49 | const width = image.width * scale; 50 | const height = image.height * scale; 51 | const geometry = new THREE.PlaneBufferGeometry(width, height, 32); 52 | const plane = new THREE.Mesh(geometry, material); 53 | 54 | this.group.add(plane); 55 | resolve(); 56 | }; 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/slye/components/text.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as slye from "@slye/core"; 12 | import * as UI from "@slye/core/ui"; 13 | import * as THREE from "three"; 14 | 15 | export type TextProps = { 16 | font: slye.FontBase; 17 | size: number; 18 | text: string; 19 | color: number; 20 | }; 21 | 22 | export class Text extends slye.ThreeComponent { 23 | ui: UI.UILayout = [ 24 | { name: "text", widget: UI.TEXT, size: 12 }, 25 | { name: "font", widget: UI.FONT, size: 9 }, 26 | { name: "size", widget: UI.SIZE, size: 2 }, 27 | { name: "color", widget: UI.COLOR, size: 1 } 28 | ]; 29 | 30 | init() {} 31 | 32 | async render() { 33 | const { font, size, text, color } = this.props; 34 | const layout = await font.layout(text); 35 | const shapes = slye.generateShapes(layout, size); 36 | 37 | const geometry = new THREE.ExtrudeGeometry(shapes, { 38 | steps: 1, 39 | depth: 2, 40 | bevelEnabled: false, 41 | bevelThickness: 0, 42 | bevelSize: 0, 43 | bevelSegments: 0 44 | }); 45 | 46 | const material = new THREE.MeshPhongMaterial({ 47 | color, 48 | emissive: 0x4e2e11, 49 | flatShading: true, 50 | side: THREE.DoubleSide 51 | }); 52 | 53 | const mesh = new THREE.Mesh(geometry, material); 54 | 55 | this.group.add(mesh); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/slye/components/video.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as slye from "@slye/core"; 12 | import * as UI from "@slye/core/ui"; 13 | import * as THREE from "three"; 14 | 15 | export type VideoProps = { 16 | file: slye.FileBase; 17 | }; 18 | 19 | export class Video extends slye.ThreeComponent { 20 | ui: UI.UILayout = [{ name: "file", widget: UI.FILE, size: 12 }]; 21 | 22 | private video: HTMLVideoElement; 23 | private texture: THREE.VideoTexture; 24 | private material: THREE.MeshBasicMaterial; 25 | private mesh: THREE.Mesh; 26 | 27 | init() { 28 | this.video = document.createElement("video"); 29 | 30 | this.texture = new THREE.VideoTexture(this.video); 31 | this.texture.generateMipmaps = false; 32 | this.texture.wrapS = this.texture.wrapT = THREE.ClampToEdgeWrapping; 33 | this.texture.minFilter = THREE.LinearFilter; 34 | 35 | this.material = new THREE.MeshBasicMaterial({ 36 | side: THREE.DoubleSide, 37 | map: this.texture, 38 | transparent: true 39 | }); 40 | 41 | this.mesh = new THREE.Mesh(undefined, this.material); 42 | 43 | this.video.onloadeddata = () => { 44 | const width = this.video.videoWidth * 0.05; 45 | const height = this.video.videoHeight * 0.05; 46 | 47 | const geometry = new THREE.PlaneBufferGeometry(width, height, 32); 48 | this.mesh.geometry = geometry; 49 | 50 | this.group.add(this.mesh); 51 | }; 52 | } 53 | 54 | async render() { 55 | const { file } = this.props; 56 | const url = await file.url(); 57 | this.video.src = url; 58 | } 59 | 60 | handleClick(): void { 61 | if (this.video.paused) { 62 | this.video.play(); 63 | } else { 64 | this.video.pause(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /modules/slye/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import * as slye from "@slye/core"; 12 | 13 | import { Text } from "./components/text"; 14 | import { Picture } from "./components/picture"; 15 | import { Video } from "./components/video"; 16 | 17 | class SlyeModule extends slye.Module { 18 | textButtonClickHandler = async (renderer: slye.Renderer): Promise => { 19 | const component = await slye.component("slye", "text", { 20 | size: 10, 21 | font: await slye.getFont("Homa"), 22 | text: "Write...", 23 | color: 0x896215 24 | }); 25 | 26 | const step = renderer.getCurrentStep(); 27 | renderer.actions.insertComponent(step, component); 28 | }; 29 | 30 | picBtnClickHandler = async (renderer: slye.Renderer): Promise => { 31 | const files = await slye.showFileDialog(renderer.presentation.uuid); 32 | if (!files || !files.length) return; 33 | 34 | const component = await slye.component("slye", "picture", { 35 | scale: 0.05, 36 | file: files[0] 37 | }); 38 | 39 | component.setPosition(0, 0, 0.1); 40 | 41 | const step = renderer.getCurrentStep(); 42 | renderer.actions.insertComponent(step, component); 43 | }; 44 | 45 | videoBtnClickHandler = async (renderer: slye.Renderer): Promise => { 46 | const files = await slye.showFileDialog(renderer.presentation.uuid); 47 | if (!files || !files.length) return; 48 | 49 | const component = await slye.component("slye", "video", { 50 | file: files[0] 51 | }); 52 | 53 | component.setPosition(0, 0, 0.1); 54 | 55 | const step = renderer.getCurrentStep(); 56 | renderer.actions.insertComponent(step, component); 57 | }; 58 | 59 | init() { 60 | slye.registerFont(new slye.Font("Homa", this.file("homa.ttf"))); 61 | slye.registerFont(new slye.Font("Sahel", this.file("sahel.ttf"))); 62 | slye.registerFont(new slye.Font("Shellia", this.file("shellia.ttf"))); 63 | slye.registerFont(new slye.Font("Emoji", this.file("emoji.ttf"))); 64 | 65 | this.registerComponent("text", Text); 66 | slye.addStepbarButton("Text", "text_fields", this.textButtonClickHandler); 67 | 68 | this.registerComponent("picture", Picture); 69 | slye.addStepbarButton("Picture", "photo", this.picBtnClickHandler); 70 | 71 | this.registerComponent("video", Video); 72 | slye.addStepbarButton("Video", "video_library", this.videoBtnClickHandler); 73 | } 74 | } 75 | 76 | slye.registerModule("slye", SlyeModule); 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "main": "dist/main.js", 4 | "version": "0.0.3", 5 | "dependencies": { 6 | "tar": "^4.4.8" 7 | }, 8 | "scripts": { 9 | "preinstall": "rm -r node_modules/@slye; echo 'Otherwise yarn removes the files.' > /dev/null", 10 | "postinstall": "mkdir -p node_modules/@slye && ln -nsf ../../core node_modules/@slye/core", 11 | "build": "gulp", 12 | "start": "electron ./dist/main.js", 13 | "dev": "parcel electron/static/index.html & SLYE_DEV=1 yarn start" 14 | }, 15 | "devDependencies": { 16 | "@material-ui/core": "^3.9.3", 17 | "@material-ui/icons": "^3.0.2", 18 | "@types/node": "^11.13.6", 19 | "@types/react": "^16.8.14", 20 | "@types/react-color": "^3.0.0", 21 | "@types/react-custom-scrollbars": "^4.0.5", 22 | "@types/react-dom": "^16.8.4", 23 | "@types/tar": "^4.0.0", 24 | "@types/tmp": "^0.1.0", 25 | "@types/uuid": "^3.4.4", 26 | "@types/yauzl": "^2.9.1", 27 | "asar": "^2.0.1", 28 | "cssnano": "^4.1.10", 29 | "del": "^4.1.1", 30 | "electron": "9.4.0", 31 | "electron-packager": "^13.1.1", 32 | "eventemitter3": "^4.0.0", 33 | "fontkit": "^1.8.0", 34 | "gh-pages": "^2.0.1", 35 | "gulp": "^4.0.1", 36 | "gulp-better-rollup": "^4.0.1", 37 | "gulp-rename": "^1.4.0", 38 | "gulp-uglify": "^3.0.2", 39 | "liltest": "^0.0.5", 40 | "material-components-web": "^1.1.1", 41 | "material-design-icons": "^3.0.1", 42 | "merge-stream": "^1.0.1", 43 | "mime": "^2.4.4", 44 | "mktemp": "^0.4.0", 45 | "parcel": "^1.12.3", 46 | "parcel-bundler": "^1.12.3", 47 | "persian-utils": "^0.3.2", 48 | "react": "^16.8.6", 49 | "react-color": "^2.17.3", 50 | "react-custom-scrollbars": "^4.2.1", 51 | "react-dom": "^16.8.6", 52 | "rollup": "^1.10.1", 53 | "rollup-plugin-commonjs": "^9.3.4", 54 | "rollup-plugin-node-resolve": "^4.2.3", 55 | "rollup-plugin-typescript": "^1.0.1", 56 | "sass": "^1.20.1", 57 | "screenfull": "^4.2.0", 58 | "stats.js": "^0.17.0", 59 | "three": "^0.103.0", 60 | "tmp": "^0.1.0", 61 | "tslib": "^1.9.3", 62 | "typescript": "^3.4.4", 63 | "uglify-es": "^3.3.9", 64 | "uuid": "^3.3.2", 65 | "yauzl": "^2.10.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /roadmap.txt: -------------------------------------------------------------------------------- 1 | Things to accomplish: 2 | - Templates 3 | - I18N 4 | - Color Picker 5 | - Assets 6 | - Pictures 7 | - Videos (requires ffmpeg) 8 | - Charts 9 | - More fonts 10 | - Presentation Thumbnails (Recent section) 11 | - Mobile version 12 | - Font previews 13 | - 3D Models 14 | - Website 15 | - Decentralized Cloud Storage (IPFS?) 16 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/screenshots/6.png -------------------------------------------------------------------------------- /stats.d.ts: -------------------------------------------------------------------------------- 1 | declare module "stats.js" { 2 | class Stats { 3 | showPanel(mode: number): void; 4 | dom: HTMLElement; 5 | end(): void; 6 | begin(): void; 7 | } 8 | 9 | export default Stats; 10 | } 11 | -------------------------------------------------------------------------------- /tests/sly.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { assert, assertEqual, test } from "liltest"; 12 | 13 | import * as three from "../core/three"; 14 | import * as headless from "../core/headless"; 15 | import * as sly from "../core/sly"; 16 | 17 | test(async function slyEncoder() { 18 | const f1 = new headless.HeadlessFont("slye", "homa"); 19 | 20 | const p1 = new headless.HeadlessPresentation("p1"); 21 | const s1 = new headless.HeadlessStep("s1"); 22 | const s2 = new headless.HeadlessStep("s2"); 23 | const c1 = new headless.HeadlessComponent("c1", "slye", "text", { 24 | p1: "value", 25 | font: f1 26 | }); 27 | const c2 = new headless.HeadlessComponent("c2", "slye", "pic", { 28 | src: "D" 29 | }); 30 | const c3 = new headless.HeadlessComponent("c3", "slye", "pic", { 31 | src: "D" 32 | }); 33 | 34 | p1.add(s1); 35 | p1.add(s2); 36 | s1.add(c1); 37 | s1.add(c2); 38 | s1.add(c3); 39 | 40 | s1.setPosition(1, 2, 3); 41 | s1.setRotation(4, 5, 6); 42 | s2.setPosition(7, 8, 9); 43 | s2.setRotation(10, 11, 12); 44 | c1.setPosition(13, 14, 15); 45 | c1.setRotation(16, 17, 18); 46 | c2.setPosition(19, 20, 21); 47 | c2.setRotation(22, 23, 24); 48 | c3.setPosition(25, 26, 27); 49 | c3.setRotation(28, 29, 30); 50 | 51 | const actual = sly.encode(p1); 52 | const expected = { 53 | template: undefined, 54 | steps: { 55 | s1: { 56 | position: [1, 2, 3], 57 | rotation: [4, 5, 6], 58 | scale: [1, 1, 1], 59 | components: [ 60 | { 61 | uuid: "c1", 62 | moduleName: "slye", 63 | component: "text", 64 | position: [13, 14, 15], 65 | rotation: [16, 17, 18], 66 | scale: [1, 1, 1], 67 | props: { 68 | p1: "value", 69 | font: { kind: 1, font: "homa", moduleName: "slye" } 70 | } 71 | }, 72 | { 73 | uuid: "c2", 74 | moduleName: "slye", 75 | component: "pic", 76 | position: [19, 20, 21], 77 | rotation: [22, 23, 24], 78 | scale: [1, 1, 1], 79 | props: { src: "D" } 80 | }, 81 | { 82 | uuid: "c3", 83 | moduleName: "slye", 84 | component: "pic", 85 | position: [25, 26, 27], 86 | rotation: [28, 29, 30], 87 | scale: [1, 1, 1], 88 | props: { src: "D" } 89 | } 90 | ] 91 | }, 92 | s2: { 93 | position: [7, 8, 9], 94 | rotation: [10, 11, 12], 95 | scale: [1, 1, 1], 96 | components: [] 97 | } 98 | } 99 | }; 100 | 101 | assertEqual(actual, expected); 102 | }); 103 | 104 | test(async function slyDecoder() { 105 | const data = { 106 | template: undefined, 107 | steps: { 108 | s1: { 109 | position: [1, 2, 3], 110 | rotation: [4, 5, 6], 111 | scale: [1, 1, 1], 112 | components: [ 113 | { 114 | uuid: "c1", 115 | moduleName: "slye", 116 | component: "text", 117 | position: [13, 14, 15], 118 | rotation: [16, 17, 18], 119 | scale: [1, 1, 1], 120 | props: { 121 | p1: "value", 122 | font: { kind: 1, font: "homa", moduleName: "slye" } 123 | } 124 | }, 125 | { 126 | uuid: "c2", 127 | moduleName: "slye", 128 | component: "pic", 129 | position: [19, 20, 21], 130 | rotation: [22, 23, 24], 131 | scale: [1, 1, 1], 132 | props: { src: "D" } 133 | }, 134 | { 135 | uuid: "c3", 136 | moduleName: "slye", 137 | component: "pic", 138 | position: [25, 26, 27], 139 | rotation: [28, 29, 30], 140 | scale: [1, 1, 1], 141 | props: { src: "D" } 142 | } 143 | ] 144 | }, 145 | s2: { 146 | position: [7, 8, 9], 147 | rotation: [10, 11, 12], 148 | scale: [1, 1, 1], 149 | components: [] 150 | } 151 | } 152 | }; 153 | 154 | const p = new three.ThreePresentation("p1"); 155 | await sly.sly(p, data as any); 156 | 157 | const data2 = sly.encode(p); 158 | assertEqual(data, data2); 159 | }); 160 | -------------------------------------------------------------------------------- /tests/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slye | Tests 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/tests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import "./headless"; 12 | import "./sly"; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "sourceMap": true, 8 | "downlevelIteration": true, 9 | "noEmit": true, 10 | "lib": ["ES2015", "DOM"], 11 | "allowSyntheticDefaultImports": true, 12 | "jsx": "react" 13 | }, 14 | "include": [ 15 | "core/**/*", 16 | "demo/**/*", 17 | "*.d.ts", 18 | "electron/**/*", 19 | "frontend/**/*", 20 | "modules/**/*", 21 | "web/**/*" 22 | ], 23 | "exclude": ["node_modules", "**/*.spec.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /web/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slye 5 | 6 | 11 | 12 | 13 | 14 | 53 | 54 | 55 | 59 | 68 | 69 | 70 |
71 | 72 |
73 |
Slye
74 |
75 | 76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /web/presentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * _____ __ 3 | * / ___// /_ _____ 4 | * \__ \/ / / / / _ \ 5 | * ___/ / / /_/ / __/ 6 | * /____/_/\__, /\___/ 7 | * /____/ 8 | * Copyright 2019 Parsa Ghadimi. All Rights Reserved. 9 | */ 10 | 11 | import { JSONPresentation, JSONPresentationStep } from "../core/sly"; 12 | import { HeadlessSerializer } from "../core/sync/headlessSerializer"; 13 | import { Sync } from "../core/sync/sync"; 14 | import { headlessDecode } from "../core/sly/headlessDecoder"; 15 | import * as headless from "../core/headless"; 16 | import uuidv1 from "uuid/v1"; 17 | import EventEmitter from "eventemitter3"; 18 | 19 | export const presentations: Map = new Map(); 20 | export const ee = new EventEmitter(); 21 | 22 | type Meta = Record; 23 | 24 | export class Presentation { 25 | readonly uuid: string; 26 | readonly meta: Meta; 27 | readonly assets: Map = new Map(); 28 | private presentation: headless.HeadlessPresentation; 29 | 30 | constructor() { 31 | this.uuid = uuidv1(); 32 | this.meta = {}; 33 | 34 | presentations.set(this.uuid, this); 35 | } 36 | 37 | open(sly: JSONPresentation): void { 38 | const pd = this.uuid; 39 | this.presentation = new headless.HeadlessPresentation(pd); 40 | 41 | const sync = new Sync( 42 | this.presentation, 43 | new HeadlessSerializer(), 44 | { 45 | onMessage(handler: (msg: string) => void): void { 46 | ee.on(`p${pd}`, (msg: string) => { 47 | handler(msg); 48 | }); 49 | }, 50 | send(msg: string): void { 51 | setTimeout(() => ee.emit(`p${pd}-x`, msg)); 52 | } 53 | }, 54 | headlessDecode, 55 | true 56 | ); 57 | 58 | sync.open(sly); 59 | } 60 | 61 | patchMeta(m2: Meta): void { 62 | Object.assign(this.meta, m2); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /website/assets/3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/3d.png -------------------------------------------------------------------------------- /website/assets/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/apple.png -------------------------------------------------------------------------------- /website/assets/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/bg.png -------------------------------------------------------------------------------- /website/assets/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/bg2.png -------------------------------------------------------------------------------- /website/assets/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/chrome.png -------------------------------------------------------------------------------- /website/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/github.png -------------------------------------------------------------------------------- /website/assets/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/linux.png -------------------------------------------------------------------------------- /website/assets/modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/modules.png -------------------------------------------------------------------------------- /website/assets/open-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/open-source.png -------------------------------------------------------------------------------- /website/assets/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qti3e/slye/80da7719532877333160bd5f0aaad12b1e4fb99d/website/assets/windows.png -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slye 5 | 6 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 23 | 32 | 33 | 34 | 42 | 43 |
44 |
45 |

# Features...

46 | 47 |
48 |
49 |
50 |

Modular

51 |

You can use different modules, or even write your own!

52 |
53 |
54 |
55 |

3D

56 |

Slye is 3D, and it makes you look awesome.

57 |
58 |
59 |
60 |

Open Source

61 |

We have no secret everything is published on GitHub repo.

62 |
63 |
64 | 65 | Download 66 |
67 | 68 |
69 | 70 |
71 |
72 |

# Download...

73 |

Note that Slye is now in Beta state.

74 | 75 | 109 | 110 |

Current Version:

111 |
112 | 113 |
114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /website/main.ts: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const links = { linux: undefined, win32: undefined }; 3 | 4 | const req = await fetch("https://api.github.com/repos/qti3e/slye/releases"); 5 | const data = await req.json(); 6 | 7 | const { assets, published_at, name } = data[0]; 8 | for (const asset of assets) { 9 | if (/linux/i.test(asset.name)) { 10 | links.linux = asset.browser_download_url; 11 | } 12 | if (/win/i.test(asset.name)) { 13 | links.win32 = asset.browser_download_url; 14 | } 15 | } 16 | 17 | const version = (name as string) 18 | .toLocaleLowerCase() 19 | .replace("version", "") 20 | .replace("ver", "") 21 | .trim(); 22 | 23 | const versionEl = document.getElementById("version"); 24 | const linuxLink = document.getElementById("link-linux"); 25 | const win32Link = document.getElementById("link-win32"); 26 | 27 | linuxLink.setAttribute("href", links.linux); 28 | win32Link.setAttribute("href", links.win32); 29 | versionEl.innerText = version; 30 | } 31 | 32 | main(); 33 | -------------------------------------------------------------------------------- /website/styles.scss: -------------------------------------------------------------------------------- 1 | html { 2 | scroll-behavior: smooth; 3 | } 4 | 5 | body { 6 | background: #454545; 7 | } 8 | 9 | html, 10 | body { 11 | margin: 0; 12 | overflow-x: hidden; 13 | } 14 | 15 | * { 16 | color: #efefef; 17 | font-family: "Noto Serif", serif; 18 | } 19 | 20 | a { 21 | text-decoration: none; 22 | } 23 | 24 | h1 { 25 | font-family: "Pacifico", cursive; 26 | } 27 | 28 | .bg { 29 | position: absolute; 30 | top: 0; 31 | bottom: 0; 32 | right: 0; 33 | left: 0; 34 | width: 100%; 35 | height: 100%; 36 | z-index: -1; 37 | } 38 | 39 | .round-btn { 40 | width: 60px; 41 | height: 60px; 42 | border-radius: 100%; 43 | background: #fff; 44 | display: block; 45 | color: #303030; 46 | font-size: 50px; 47 | line-height: 45px; 48 | text-align: center; 49 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); 50 | } 51 | 52 | #head { 53 | width: 100vw; 54 | height: 100vh; 55 | 56 | .bg { 57 | background-image: linear-gradient(#12a7e0, #69f3ec96); 58 | transform: skewY(-10deg); 59 | transform-origin: top left; 60 | } 61 | 62 | .container { 63 | width: 100%; 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: center; 67 | align-items: center; 68 | margin-top: 50px; 69 | 70 | h1 { 71 | font-size: 100px; 72 | margin: 0; 73 | text-align: center; 74 | } 75 | 76 | p { 77 | font-size: 25px; 78 | text-align: center; 79 | } 80 | } 81 | 82 | a { 83 | position: absolute; 84 | left: calc(50% - 30px); 85 | bottom: 60px; 86 | } 87 | } 88 | 89 | #features { 90 | width: 100vw; 91 | height: 100vh; 92 | 93 | .bg { 94 | top: 100vh; 95 | background: #303030; 96 | transform: skewY(-10deg); 97 | transform-origin: top left; 98 | } 99 | 100 | .features-container { 101 | display: flex; 102 | justify-content: center; 103 | } 104 | 105 | .features-box { 106 | margin: 40px; 107 | 108 | h2 { 109 | text-align: center; 110 | } 111 | } 112 | 113 | a { 114 | width: 100px; 115 | position: relative; 116 | left: calc(50% - 50px); 117 | padding: 10px; 118 | border-radius: 10px; 119 | color: #ffffff; 120 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); 121 | background: #545454; 122 | text-transform: uppercase; 123 | } 124 | } 125 | 126 | #download { 127 | width: 100vw; 128 | height: 100vh; 129 | 130 | .bg { 131 | top: 200vh; 132 | background: #303030; 133 | transform: skewY(10deg); 134 | transform-origin: top left; 135 | } 136 | 137 | p { 138 | font-size: 20px; 139 | margin-left: 45px; 140 | } 141 | 142 | .container { 143 | display: flex; 144 | justify-content: center; 145 | margin-top: 150px; 146 | } 147 | 148 | .box { 149 | margin: 40px; 150 | 151 | h2 { 152 | text-align: center; 153 | } 154 | 155 | &.fade { 156 | opacity: 0.4; 157 | } 158 | } 159 | } 160 | 161 | #features, 162 | #download { 163 | h1 { 164 | font-size: 35px; 165 | display: inline-block; 166 | margin: 30px; 167 | border-bottom: 6px dotted #efdf29; 168 | } 169 | } 170 | 171 | .pattern { 172 | width: 100vw; 173 | height: 72vh; 174 | position: absolute; 175 | z-index: -2; 176 | 177 | &.i1 { 178 | top: 164vh; 179 | background-image: url("assets/bg.png"); 180 | } 181 | 182 | &.i2 { 183 | top: 264vh; 184 | background-image: url("assets/bg2.png"); 185 | } 186 | } 187 | 188 | .icon { 189 | width: 200px; 190 | height: 200px; 191 | background-size: contain; 192 | justify-content: center; 193 | margin: auto; 194 | 195 | &.three-d { 196 | background-image: url("assets/3d.png"); 197 | } 198 | 199 | &.open-source { 200 | background-image: url("assets/open-source.png"); 201 | } 202 | 203 | &.module { 204 | background-image: url("assets/modules.png"); 205 | } 206 | 207 | &.linux { 208 | background-image: url("assets/linux.png"); 209 | } 210 | 211 | &.windows { 212 | background-image: url("assets/windows.png"); 213 | } 214 | 215 | &.chrome { 216 | background-image: url("assets/chrome.png"); 217 | } 218 | 219 | &.apple { 220 | background-image: url("assets/apple.png"); 221 | } 222 | 223 | &.github { 224 | background-image: url("assets/github.png"); 225 | } 226 | } 227 | --------------------------------------------------------------------------------