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 |
27 | );
28 | }
29 |
30 | export function RotateIcon() {
31 | return (
32 |
47 | );
48 | }
49 |
50 | export function ScaleIcon() {
51 | return (
52 |
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 |
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 |
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 |
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 |
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 |
35 |
36 |
37 |
Slye!
38 |
3D presentation desktop-application
39 |
40 | ⌄
41 |
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 |
--------------------------------------------------------------------------------