diff --git a/package.json b/package.json index 748c9fc..0efdbb4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A template for writing jam games in HTML5 + TypeScript", "main": "./src/index.ts", "scripts": { - "start": "webpack-dev-server --open --mode development", + "dev": "webpack-dev-server --open --mode development", "build": "webpack --mode production" }, "repository": { @@ -32,5 +32,13 @@ "webpack": "^4.29.5", "webpack-cli": "^3.2.3", "webpack-dev-server": "^3.2.0" + }, + "dependencies": { + "@eix/utils": "git+https://github.com/eix-js/utils.git", + "haunted": "^4.3.0", + "lit-html": "^1.0.0", + "lit-rx": "0.0.2", + "prelude-ts": "^0.8.2", + "rxjs": "^6.5.2" } } diff --git a/src/index.html b/src/index.html index 83ae23d..3ca22f1 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@ + hello world \ No newline at end of file diff --git a/src/scss/base.scss b/src/scss/base.scss index 375a34a..7e88921 100644 --- a/src/scss/base.scss +++ b/src/scss/base.scss @@ -1,3 +1,14 @@ -body { - padding: 0 +html, body { + padding: 0; + margin: 0; + overflow: hidden; + height:100%; + display: block; +} + +svg { + background-color: #444444; + height: 100%; + width: 100%; + display: block; } \ No newline at end of file diff --git a/src/ts/common/activation/activationStore.ts b/src/ts/common/activation/activationStore.ts new file mode 100644 index 0000000..909b2c7 --- /dev/null +++ b/src/ts/common/activation/activationStore.ts @@ -0,0 +1,23 @@ +import { Singleton } from "@eix/utils"; +import { activationFunction, activationFunctionParam } from "./interfaces" +import { toActivationFunction } from "./toActivation"; + +@Singleton +export class FunctionStore { + functions = new Map() + + private storageKeyword: string + + constructor(name="activation") { + this.storageKeyword =`/${name}` + for (let i in localStorage) { + if (i.indexOf(this.storageKeyword) == 0) + this.register(i.substr(this.storageKeyword.length), localStorage[i]) + } + } + + register(name: string, activation: activationFunctionParam) { + this.functions.set(name, toActivationFunction(activation)) + localStorage[`${this.storageKeyword.substr(1)}/${name}`] = activation + } +} \ No newline at end of file diff --git a/src/ts/common/activation/index.ts b/src/ts/common/activation/index.ts new file mode 100644 index 0000000..ed346eb --- /dev/null +++ b/src/ts/common/activation/index.ts @@ -0,0 +1 @@ +export * from "./toActivation" \ No newline at end of file diff --git a/src/ts/common/activation/interfaces.ts b/src/ts/common/activation/interfaces.ts new file mode 100644 index 0000000..a2ca08e --- /dev/null +++ b/src/ts/common/activation/interfaces.ts @@ -0,0 +1,4 @@ +export type activationInput = any[] +export type activationOutput = boolean +export type activationFunctionParam = ( (data:activationInput) => activationOutput ) | string +export type activationFunction = (data:activationInput) => activationOutput \ No newline at end of file diff --git a/src/ts/common/activation/toActivation.ts b/src/ts/common/activation/toActivation.ts new file mode 100644 index 0000000..ef9a7e5 --- /dev/null +++ b/src/ts/common/activation/toActivation.ts @@ -0,0 +1,8 @@ +import { activationFunctionParam, activationFunction } from "./interfaces"; + +export const toActivationFunction = (original: activationFunctionParam) => { + const stringified = (typeof original == "string") ? original : original.toString() + const final = new Function(`return ${stringified}`) as () => activationFunction + + return final() +} \ No newline at end of file diff --git a/src/ts/common/clamp/clamp.ts b/src/ts/common/clamp/clamp.ts new file mode 100644 index 0000000..3ba70be --- /dev/null +++ b/src/ts/common/clamp/clamp.ts @@ -0,0 +1,5 @@ +export default function clamp(value:number, min:number, max:number) { + return min < max + ? (value < min ? min : value > max ? max : value) + : (value < max ? max : value > min ? min : value) + } \ No newline at end of file diff --git a/src/ts/common/clamp/index.ts b/src/ts/common/clamp/index.ts new file mode 100644 index 0000000..bddee4f --- /dev/null +++ b/src/ts/common/clamp/index.ts @@ -0,0 +1 @@ +export * from "./clamp" \ No newline at end of file diff --git a/src/ts/common/component/component.ts b/src/ts/common/component/component.ts new file mode 100644 index 0000000..1294ec4 --- /dev/null +++ b/src/ts/common/component/component.ts @@ -0,0 +1,76 @@ +import { Vector } from "prelude-ts" +import { Subject, BehaviorSubject } from "rxjs"; +import { ComponentState } from "./interfaces"; +import { map } from "rxjs/operators"; +import { Screen } from "../screen.ts"; + +export class Component { + private static screen = new Screen() + + public position = new BehaviorSubject(null) + public scale = new BehaviorSubject(null) + public clicked = false + + private mouserDelta: number[] + + constructor(public activationType: string, + position: [number, number] = [0, 0], + scale: [number, number] = [0, 0]) { + this.position.next(position) + this.scale.next(scale) + } + + handleMouseUp(e: MouseEvent) { + this.clicked = false + } + + move(e: MouseEvent) { + const mousePosition = Component.screen.getWorldPosition(e.clientX, e.clientY) + this.position.next(mousePosition.map((value, index) => + value - this.mouserDelta[index] + )) + } + + handleClick(e: MouseEvent) { + const mousePosition = Component.screen.getWorldPosition(e.clientX, e.clientY) + + this.mouserDelta = this.position.value.map((value, index) => + mousePosition[index] - value + ) + this.clicked = true + } + + get state(): ComponentState { + return { + position: this.position.value as [number, number], + scale: this.position.value as [number, number], + activationType: this.activationType + } + } + + get x() { + return this.position.pipe(map(val => + val[0] + )) + } + get y() { + return this.position.pipe(map(val => + val[1] + )) + } + + get width() { + return this.scale.pipe(map(val => + val[0] + )) + } + get height() { + return this.scale.pipe(map(val => + val[1] + )) + } + + static fromState(state:ComponentState){ + return new Component(state.activationType, state.position, state.scale) + } +} \ No newline at end of file diff --git a/src/ts/common/component/index.ts b/src/ts/common/component/index.ts new file mode 100644 index 0000000..7c2d3e4 --- /dev/null +++ b/src/ts/common/component/index.ts @@ -0,0 +1 @@ +export * from "./component" \ No newline at end of file diff --git a/src/ts/common/component/interfaces.ts b/src/ts/common/component/interfaces.ts new file mode 100644 index 0000000..6b4ca7a --- /dev/null +++ b/src/ts/common/component/interfaces.ts @@ -0,0 +1,5 @@ +export interface ComponentState { + position: [number,number] + scale: [number,number] + activationType: string +} \ No newline at end of file diff --git a/src/ts/common/componentManager/componentManager.ts b/src/ts/common/componentManager/componentManager.ts new file mode 100644 index 0000000..21eb38e --- /dev/null +++ b/src/ts/common/componentManager/componentManager.ts @@ -0,0 +1,115 @@ +import { Singleton } from "@eix/utils"; +import { Component } from "../component/component"; +import { Subject } from "rxjs"; +import { svg, SVGTemplateResult } from "lit-html"; +import { subscribe } from "lit-rx"; +import { Screen } from "../screen.ts"; +import { MnanagerState } from "./interfaces"; + +@Singleton +export class ComponentManager { + public components: Component[] = [] + public svgs = new Subject() + + private onTop: Component + private clicked = false + private screen = new Screen() + + constructor() { + this.svgs.next(this.render()) + } + + update() { + this.svgs.next(this.render()) + } + + handleMouseDown(e: MouseEvent) { + this.clicked = true + } + + handleMouseUp(e: MouseEvent) { + this.clicked = false + } + + handleMouseMove(e: MouseEvent) { + let toAddOnTop: number + let outsideComponents = true + + for (let i = 0; i < this.components.length; i++) { + const component = this.components[i] + if (component.clicked) { + outsideComponents = false + component.move(e) + if (this.onTop != component) { + toAddOnTop = i + } + } + } + + if (toAddOnTop >= 0) { + this.onTop = this.components[toAddOnTop] + this.components.push(this.onTop) + this.update() + } + + else if (outsideComponents && this.clicked) { + const mousePosition = [e.clientX, e.clientY] + const delta = mousePosition.map((value, index) => + this.screen.mousePosition[index] - value + ) as [number, number] + this.screen.move(...delta) + } + } + + render() { + let toRemoveDuplicatesFor: Component + + const result = this.components.map(component => svg` + component.handleClick(e)} + @mouseup=${ (e: MouseEvent) => { + component.handleMouseUp(e) + toRemoveDuplicatesFor = component + }} + > + `); + + if (toRemoveDuplicatesFor) + this.removeDuplicates(toRemoveDuplicatesFor) + + return result + } + + private removeDuplicates(component: Component) { + let instances = this.components + .map((value, index) => (value == component) ? index : null) + .filter(value => value) + instances.pop() + + this.components = this.components + .filter((val, index) => instances.indexOf(index) != -1) + } + + get state(): MnanagerState { + const components = Array.from((new Set(this.components)).values()) + return { + components: components.map(value => value.state) + } + } + + loadState(state:MnanagerState) { + this.clicked = false + this.components = state.components.map(value => Component.fromState(value)) + this.onTop = null + this.update() + } + + save(){ + //TODO: implement + } +} \ No newline at end of file diff --git a/src/ts/common/componentManager/index.ts b/src/ts/common/componentManager/index.ts new file mode 100644 index 0000000..65d4de9 --- /dev/null +++ b/src/ts/common/componentManager/index.ts @@ -0,0 +1 @@ +export * from "./componentManager" \ No newline at end of file diff --git a/src/ts/common/componentManager/interfaces.ts b/src/ts/common/componentManager/interfaces.ts new file mode 100644 index 0000000..54b1bc5 --- /dev/null +++ b/src/ts/common/componentManager/interfaces.ts @@ -0,0 +1,5 @@ +import { ComponentState } from "../component/interfaces"; + +export interface MnanagerState { + components: ComponentState[] +} \ No newline at end of file diff --git a/src/ts/common/screen.ts/index.ts b/src/ts/common/screen.ts/index.ts new file mode 100644 index 0000000..4cc2a70 --- /dev/null +++ b/src/ts/common/screen.ts/index.ts @@ -0,0 +1 @@ +export * from "./screen" \ No newline at end of file diff --git a/src/ts/common/screen.ts/screen.ts b/src/ts/common/screen.ts/screen.ts new file mode 100644 index 0000000..a2ca841 --- /dev/null +++ b/src/ts/common/screen.ts/screen.ts @@ -0,0 +1,72 @@ +import { Subject, fromEvent, BehaviorSubject, combineLatest } from "rxjs" +import { Singleton } from "@eix/utils" +import { map } from "rxjs/operators"; +import clamp from "../clamp/clamp"; + +@Singleton +export class Screen { + width = new BehaviorSubject(0) + height = new BehaviorSubject(0) + viewBox = combineLatest(this.width, this.height).pipe(map((values: [number,number]) => + this.getViewBox(...values) + )); + + private scrollStep = 1.3 + private position = [0, 0] + public scale = [2, 2] + + private zoomLimits: [number,number] = [0.1,10] + + public mousePosition = [this.width.value / 2, this.height.value / 2] + + constructor() { + this.update() + + fromEvent(window, "resize").subscribe(() => this.update()) + } + + updateMouse(e:MouseEvent){ + this.mousePosition = [e.clientX,e.clientY] + } + + handleScroll(e: WheelEvent){ + e.preventDefault() + + const size = [this.width.value,this.height.value] + const mouseFraction = size.map((value,index) => this.mousePosition[index] / value) + const sign = e.deltaY / Math.abs(e.deltaY) + const zoom = this.scrollStep ** sign + const newScale = this.scale.map(value => clamp(value * zoom, ...this.zoomLimits )) + const delta = this.scale.map((value,index) => + size[index] * (newScale[index] - this.scale[index]) * mouseFraction[index] + ) + + this.scale = newScale + this.position = this.position.map((value,index) => value - delta[index]) + this.update() + } + + move(x:number, y:number) { + this.position[0] += x * this.scale[0] + this.position[1] += y * this.scale[1] + this.update() + } + + getViewBox(width: number, height:number) { + return [ + this.position[0], + this.position[1], + this.scale[0] * width, + this.scale[1] * height + ].join(" ") + } + + update() { + this.height.next(window.innerHeight) + this.width.next(window.innerWidth) + } + + getWorldPosition(x:number, y:number) { + return [x * this.scale[0], y * this.scale[1]] + } +} \ No newline at end of file diff --git a/src/ts/main.ts b/src/ts/main.ts index 8c1ba46..9e7449f 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -1 +1,40 @@ -console.log("hello world!") \ No newline at end of file +import { render, html, svg } from "lit-html" +import { subscribe } from "lit-rx" +import { Screen } from "./common/screen.ts"; +import { Component } from "./common/component"; +import { FunctionStore } from "./common/activation/activationStore"; +import { ComponentManager } from "./common/componentManager"; + +const screen = new Screen() + +const test = new FunctionStore() +test.register("buffer",(data) => { + return true; +}) + +const manager = new ComponentManager() +manager.components.push(new Component("none",[200,100],[200,30])) +manager.components.push(new Component("none",[300,100],[200,30])) +manager.components.push(new Component("none",[400,100],[200,30])) +manager.components.push(new Component("none",[500,100],[200,30])) +manager.components.push(new Component("none",[600,100],[200,30])) +manager.update() + +console.log(manager.state) + +render(html` + { + manager.handleMouseMove(e) + screen.updateMouse(e) + }} + viewBox=${subscribe(screen.viewBox)} + @mousedown=${(e:MouseEvent) => manager.handleMouseDown(e)} + @mouseup=${(e:MouseEvent) => manager.handleMouseUp(e)} + @wheel=${(e:WheelEvent) => screen.handleScroll(e)} + > + ${ subscribe(manager.svgs) } + +`, document.body) + +manager.update() \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2730819..cac08af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "types/*" ] }, - "module": "es6", + "module": "commonjs", "target": "es5", "removeComments": true, "sourceMap": true, @@ -17,7 +17,9 @@ ], "noImplicitAny": true, "alwaysStrict": true, - "moduleResolution": "Node" + "moduleResolution": "Node", + "experimentalDecorators": true, + "esModuleInterop": true }, "include": [ "src/**/*.ts"