before removing smooth shadows

This commit is contained in:
Matei Adriel 2019-07-15 14:45:52 +03:00
parent b28eec6342
commit 9eba227ec3
70 changed files with 4277 additions and 3673 deletions

1
.nvmrc
View file

@ -1 +0,0 @@
12.2.0

View file

@ -1,2 +0,0 @@
Pentru a rula aplicatia, deschideti dist/index.html.
Codul sursa se afla in folderul src.

17
babel.config.js Normal file
View file

@ -0,0 +1,17 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }]
],
env: {
test: {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
}
}
}

View file

@ -1,11 +1,9 @@
const { publish } = require('gh-pages')
const { exec } = require('child_process')
const { random } = require('random-emoji')
// const { publish } = require("gh-pages")
const args = process.argv.splice(2)
const randomEmoji = () => random({ count: 1 })[0].character
const mFlag = (args.indexOf('--message') + 1 || args.indexOf('-m') + 1) - 1
const message = `${mFlag >= 0 ? args[mFlag + 1] : 'automated update'}`
@ -23,7 +21,6 @@ const run = (command: string): Promise<string> => {
})
})
}
;(async () => {
try {
if (!args.includes('--skipBuild') && !args.includes('-sb'))

3818
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,66 +1,50 @@
{
"name": "html5-game-template",
"name": "logic-gate-simulator",
"version": "1.0.0",
"description": "A template for writing jam games in HTML5 + TypeScript",
"main": "./src/index.ts",
"scripts": {
"dev": "webpack-dev-server --open --mode development",
"build": "webpack --mode production",
"build": "cross-env NODE_ENV=production webpack",
"deploy": "ts-node deploy"
},
"sideEffects": [
"*.scss",
"./src/ts/main.ts"
],
"repository": {
"type": "git",
"url": "git+https://github.com/neverix/html5-game-template.git"
},
"author": "neverix",
"license": "MIT",
"bugs": {
"url": "https://github.com/neverix/html5-game-template/issues"
},
"homepage": "https://github.com/neverix/html5-game-template#readme",
"devDependencies": {
"@types/file-saver": "^2.0.1",
"@babel/core": "^7.5.0",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@types/deepmerge": "^2.2.0",
"@types/gh-pages": "^2.0.0",
"@types/micromodal": "^0.3.0",
"@types/toastr": "^2.1.37",
"css-loader": "^2.1.0",
"extract-loader": "^3.1.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"gh-pages": "^2.0.1",
"html-loader": "^0.5.5",
"@types/mainloop.js": "^1.0.5",
"@types/react-router-dom": "^4.3.4",
"babel-loader": "^8.0.6",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-regenerator-runtime": "^6.5.0",
"css-loader": "^3.0.0",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.11.0",
"random-emoji": "^1.0.2",
"mini-css-extract-plugin": "^0.7.0",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"sass-loader": "^7.1.0",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^1.3.0",
"ts-loader": "^5.3.3",
"ts-node": "^8.2.0",
"typescript": "^3.3.3333",
"webpack": "^4.29.5",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.0"
"typescript": "^3.5.2",
"webpack": "^4.35.2",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2",
"webpack-merge": "^4.2.1"
},
"dependencies": {
"@eix/input": "git+https://github.com/eix-js/input.git",
"@eix/utils": "git+https://github.com/eix-js/utils.git",
"@material/button": "^2.3.0",
"@material/dialog": "^2.3.0",
"@material/drawer": "^2.3.0",
"@material/list": "^2.3.0",
"@material/menu": "^2.3.0",
"@material/textfield": "^2.3.0",
"@material/theme": "^1.1.0",
"file-saver": "^2.0.2",
"lit-html": "^1.0.0",
"lit-rx": "0.0.2",
"@eix-js/utils": "0.0.6",
"deepmerge": "^4.0.0",
"mainloop.js": "^1.0.4",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1",
"rxjs": "^6.5.2",
"toastr": "^2.1.4"
"rxjs-hooks": "^0.5.1"
}
}

View file

@ -7,136 +7,6 @@
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0"
/>
<style>
html,
body {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
body {
background: #222;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.sk-folding-cube {
margin: 20px auto;
width: 40px;
height: 40px;
position: relative;
-webkit-transform: rotateZ(45deg);
transform: rotateZ(45deg);
}
.sk-folding-cube .sk-cube {
float: left;
width: 50%;
height: 50%;
position: relative;
-webkit-transform: scale(1.1);
-ms-transform: scale(1.1);
transform: scale(1.1);
}
.sk-folding-cube .sk-cube:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #bbb;
-webkit-animation: sk-foldCubeAngle 2.4s infinite linear both;
animation: sk-foldCubeAngle 2.4s infinite linear both;
-webkit-transform-origin: 100% 100%;
-ms-transform-origin: 100% 100%;
transform-origin: 100% 100%;
}
.sk-folding-cube .sk-cube2 {
-webkit-transform: scale(1.1) rotateZ(90deg);
transform: scale(1.1) rotateZ(90deg);
}
.sk-folding-cube .sk-cube3 {
-webkit-transform: scale(1.1) rotateZ(180deg);
transform: scale(1.1) rotateZ(180deg);
}
.sk-folding-cube .sk-cube4 {
-webkit-transform: scale(1.1) rotateZ(270deg);
transform: scale(1.1) rotateZ(270deg);
}
.sk-folding-cube .sk-cube2:before {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
.sk-folding-cube .sk-cube3:before {
-webkit-animation-delay: 0.6s;
animation-delay: 0.6s;
}
.sk-folding-cube .sk-cube4:before {
-webkit-animation-delay: 0.9s;
animation-delay: 0.9s;
}
@-webkit-keyframes sk-foldCubeAngle {
0%,
10% {
-webkit-transform: perspective(140px) rotateX(-180deg);
transform: perspective(140px) rotateX(-180deg);
opacity: 0;
}
25%,
75% {
-webkit-transform: perspective(140px) rotateX(0deg);
transform: perspective(140px) rotateX(0deg);
opacity: 1;
}
90%,
100% {
-webkit-transform: perspective(140px) rotateY(180deg);
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}
@keyframes sk-foldCubeAngle {
0%,
10% {
-webkit-transform: perspective(140px) rotateX(-180deg);
transform: perspective(140px) rotateX(-180deg);
opacity: 0;
}
25%,
75% {
-webkit-transform: perspective(140px) rotateX(0deg);
transform: perspective(140px) rotateX(0deg);
opacity: 1;
}
90%,
100% {
-webkit-transform: perspective(140px) rotateY(180deg);
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}
</style>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body
@ -144,11 +14,6 @@
ondrop="return false;"
oncontextmenu="return false"
>
<div class="sk-folding-cube">
<div class="sk-cube1 sk-cube"></div>
<div class="sk-cube2 sk-cube"></div>
<div class="sk-cube4 sk-cube"></div>
<div class="sk-cube3 sk-cube"></div>
</div>
<div id="app"></div>
</body>
</html>

View file

@ -1,2 +0,0 @@
import "./ts/main"
import "./scss/base.scss"

6
src/main.tsx Normal file
View file

@ -0,0 +1,6 @@
import React from 'react'
import App from './modules/core/components/App'
import { render } from 'react-dom'
render(<App />, document.getElementById('app'))

View file

@ -0,0 +1,28 @@
import { Singleton } from '@eix-js/utils'
import { Observable, fromEvent, BehaviorSubject } from 'rxjs'
import { map } from 'rxjs/operators'
@Singleton
export class Screen {
public width = new BehaviorSubject<number>(window.innerWidth)
public height = new BehaviorSubject<number>(window.innerHeight)
public constructor() {
const resize = fromEvent(window, 'resize')
resize
.pipe(map(() => window.innerWidth))
.subscribe(val => this.width.next(val))
resize
.pipe(map(() => window.innerHeight))
.subscribe(val => this.height.next(val))
}
public get x() {
return this.width.value
}
public get y() {
return this.height.value
}
}

View file

@ -0,0 +1,7 @@
html,
body {
height: 100%;
width: 100%;
display: block;
overflow: hidden;
}

View file

@ -0,0 +1,10 @@
import React from 'react'
import '../styles/reset'
import './App.scss'
import Canvas from './Canvas'
const App = () => {
return <Canvas />
}
export default App

View file

@ -0,0 +1,56 @@
import React, { Component, createRef, Ref, RefObject } from 'react'
import FluidCanvas, { MouseEventInfo } from './FluidCanvas'
import loop from 'mainloop.js'
import { Gate } from '../../simulation/classes/Gate'
import { SimulationRenderer } from '../../simulation/classes/SimulationRenderer'
import { Subject } from 'rxjs'
class Canvas extends Component {
private canvasRef: RefObject<HTMLCanvasElement> = createRef()
private renderingContext: CanvasRenderingContext2D | null
private renderer = new SimulationRenderer()
public constructor(props: {}) {
super(props)
const foo = new Gate('blue')
const bar = new Gate('green')
foo.transform.position = [100, 100]
foo.transform.scale = [70, 70]
bar.transform.position = [200, 200]
bar.transform.scale = [70, 70]
this.renderer.simulation.push(foo, bar)
loop.setDraw(() => {
if (this.renderingContext)
this.renderer.render(this.renderingContext)
}).setUpdate(delta => this.renderer.update(delta))
}
public componentDidMount() {
if (this.canvasRef.current)
this.renderingContext = this.canvasRef.current.getContext('2d')
loop.start()
}
public componentWillUnmount() {
loop.stop()
}
public render() {
return (
<FluidCanvas
ref={this.canvasRef}
mouseDownOuput={this.renderer.mouseDownOutput}
mouseUpOutput={this.renderer.mouseUpOutput}
mouseMoveOutput={this.renderer.mouseMoveOutput}
/>
)
}
}
export default Canvas

View file

@ -0,0 +1,53 @@
import React, { RefObject, forwardRef, MouseEvent } from 'react'
import { useObservable } from 'rxjs-hooks'
import { Screen } from '../classes/Screen'
import { Subject } from 'rxjs'
import { vector2 } from '../../simulation/classes/Transform'
const screen = new Screen()
export interface MouseEventInfo {
position: vector2
button: number
}
export interface FluidCanvasProps {
mouseDownOuput: Subject<MouseEventInfo>
mouseUpOutput: Subject<MouseEventInfo>
mouseMoveOutput: Subject<MouseEventInfo>
}
export const getEventInfo = (
e: MouseEvent<HTMLCanvasElement>
): MouseEventInfo => {
return {
button: e.button,
position: [e.clientX, e.clientY]
}
}
export const mouseEventHandler = (output: Subject<MouseEventInfo>) => (
e: MouseEvent<HTMLCanvasElement>
) => {
output.next(getEventInfo(e))
}
const FluidCanvas = forwardRef(
(props: FluidCanvasProps, ref: RefObject<HTMLCanvasElement>) => {
const width = useObservable(() => screen.width, 0)
const height = useObservable(() => screen.height, 0)
return (
<canvas
ref={ref}
width={width}
height={height}
onMouseDown={mouseEventHandler(props.mouseDownOuput)}
onMouseUp={mouseEventHandler(props.mouseUpOutput)}
onMouseMove={mouseEventHandler(props.mouseMoveOutput)}
/>
)
}
)
export default FluidCanvas

View file

@ -0,0 +1,446 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0-modified | 20110126
License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* make sure to set some focus styles for accessibility */
:focus {
outline: 0;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-decoration,
input[type='search']::-webkit-search-results-button,
input[type='search']::-webkit-search-results-decoration {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type='search'] {
-webkit-appearance: none;
-moz-appearance: none;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
/**
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
max-width: 100%;
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
* Known issue: no IE 6 support.
*/
[hidden] {
display: none;
}
/**
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
* `em` units.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-size: 100%; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
}
/**
* Address `outline` inconsistency between Chrome and other browsers.
*/
a:focus {
outline: thin dotted;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/**
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
* 2. Improve image quality when scaled in IE 7.
*/
img {
border: 0; /* 1 */
-ms-interpolation-mode: bicubic; /* 2 */
}
/**
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
*/
figure {
margin: 0;
}
/**
* Correct margin displayed oddly in IE 6/7.
*/
form {
margin: 0;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct color not being inherited in IE 6/7/8/9.
* 2. Correct text not wrapping in Firefox 3.
* 3. Correct alignment displayed oddly in IE 6/7.
*/
legend {
border: 0; /* 1 */
padding: 0;
white-space: normal; /* 2 */
*margin-left: -7px; /* 3 */
}
/**
* 1. Correct font size not being inherited in all browsers.
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
* and Chrome.
* 3. Improve appearance and consistency in all browsers.
*/
button,
input,
select,
textarea {
font-size: 100%; /* 1 */
margin: 0; /* 2 */
vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
}
/**
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
* Correct `select` style inheritance in Firefox 4+ and Opera.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
* Known issue: inner spacing remains in IE 6.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
*overflow: visible; /* 4 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* 1. Address box sizing set to content-box in IE 8/9.
* 2. Remove excess padding in IE 8/9.
* 3. Remove excess padding in IE 7.
* Known issue: excess padding remains in IE 6.
*/
input[type='checkbox'],
input[type='radio'] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
*height: 13px; /* 3 */
*width: 13px; /* 3 */
}
/**
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type='search'] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Remove inner padding and border in Firefox 3+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
* 2. Improve readability and alignment in all browsers.
*/
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
html,
button,
input,
select,
textarea {
color: #222;
}
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
img {
vertical-align: middle;
}
fieldset {
border: 0;
margin: 0;
padding: 0;
}
textarea {
resize: vertical;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}

View file

@ -0,0 +1,4 @@
import { Subject } from 'rxjs'
import { MouseEventInfo } from '../components/FluidCanvas'
export type MouseSubject = Subject<MouseEventInfo>

View file

@ -0,0 +1,13 @@
import { Transform, vector2 } from './Transform'
import { Screen } from '../../core/classes/Screen'
export class Camera {
private screen = new Screen()
public transform = new Transform([0, 0], [this.screen.x, this.screen.y])
public toWordPostition(position: vector2) {
return position.map(
(value, index) => value + this.transform.position[index]
) as vector2
}
}

View file

@ -0,0 +1,10 @@
import { Transform, vector2 } from './Transform'
export class Gate {
public static lastId = 0
public transform = new Transform()
public id = Gate.lastId++
public shadow: vector2 = [0, 0]
public constructor(public color = 'blue') {}
}

View file

@ -0,0 +1,70 @@
import { LruCacheNode } from '@eix-js/utils'
import { Gate } from './Gate'
export type GateNode = LruCacheNode<Gate>
export class GateStorage {
private hashMap = new Map<number, GateNode>()
private head = new LruCacheNode<Gate>(0, null)
private tail = new LruCacheNode<Gate>(0, null)
public constructor() {
this.head.next = this.tail
this.tail.previous = this.head
}
private delete(node: GateNode) {
node.previous.next = node.next
node.next.previous = node.previous
return this
}
public set(key: number, node: GateNode) {
this.hashMap.set(key, node)
this.addToHead(node)
}
public addToHead(node: GateNode) {
node.next = this.head.next
node.next.previous = node
node.previous = this.head
this.head.next = node
return this
}
public moveOnTop(node: GateNode) {
this.delete(node).addToHead(node)
return this
}
public get(key: number) {
const node = this.hashMap.get(key)
return node
}
public first() {
const first = this.head.next
return
}
public *[Symbol.iterator](): Iterator<Gate> {
let last = this.tail
while (true) {
if (last.previous === this.head) {
break
} else {
if (last.previous.data) {
yield last.previous.data
}
last = last.previous
}
}
}
}

View file

@ -0,0 +1,86 @@
import { Singleton } from '@eix-js/utils'
import { MouseSubject } from '../../core/types/MouseSubject'
import { clamp } from '../helpers/clamp'
@Singleton
export class MouseManager {
private history: number[] = []
private total = 0
private limit = 10
private lastPosition = 0
private lastDirection = 0
private minimumDifference = 10
private lastMove = performance.now()
private resetLimit = 500
// mouseMoveInput is optional because we want to be able to get
// the instance even if we don't have a subject
// (as long as we passed one the first time)
public constructor(public mouseMoveInput?: MouseSubject) {
if (this.mouseMoveInput) {
this.mouseMoveInput.subscribe(event => {
this.lastMove = performance.now()
const position = event.position[0]
const dx = position - this.lastPosition
if (Math.abs(dx) < this.minimumDifference) {
this.lastPosition = position
return
}
if (dx === 0) {
this.lastDirection = 0
} else {
this.lastDirection = Math.abs(dx) / dx
}
this.lastPosition = event.position[0]
})
} else {
throw new Error(
'You need to pass a MouseMoveInput the first time you instantiate this class!'
)
}
}
public getDirection() {
return clamp(-1, 1, this.total / this.history.length)
}
public update(maybeAgain = true) {
if (this.lastDirection === 0) {
return 0
}
if (
this.lastDirection !== 0 &&
performance.now() - this.lastMove > this.resetLimit
) {
this.lastDirection = 0
}
this.history.push(this.lastDirection)
this.total += this.lastDirection
if (this.history.length > this.limit) {
const removed = this.history.shift()
if (removed !== undefined) {
this.total -= removed
}
}
}
public clear(value?: number) {
if (value) {
this.lastPosition = value
}
this.history = []
this.total = 0
this.lastMove = performance.now()
this.lastDirection = 0
}
}

View file

@ -0,0 +1,15 @@
import { Gate } from './Gate'
import { GateStorage } from './GateStorage'
import { LruCacheNode } from '@eix-js/utils'
export class Simulation {
public gates = new GateStorage()
public push(...gates: Gate[]) {
for (const gate of gates) {
const node = new LruCacheNode<Gate>(gate.id, gate)
this.gates.set(gate.id, node)
}
}
}

View file

@ -0,0 +1,201 @@
import { Camera } from './Camera'
import { Simulation } from './Simulation'
import { Subject } from 'rxjs'
import { MouseEventInfo } from '../../core/components/FluidCanvas'
import { pointInSquare } from '../helpers/pointInSquare'
import { vector2 } from './Transform'
import merge from 'deepmerge'
import { smoothStep } from '../../vector2/helpers/smoothStep'
import { renderGate } from '../helpers/renderGate'
import { renderGateShadow } from '../helpers/renderGateShadow'
import { Gate } from './Gate'
import { MouseManager } from './MouseManager'
import { Screen } from '../../core/classes/Screen'
import {
add,
invert,
ofLength,
length,
multiply
} from '../../vector2/helpers/basic'
export interface SimulationRendererOptions {
shadows: {
enabled: boolean
color: string
offset: number
speed: number
}
dnd: {
rotation: number
}
}
export const defaultSimulationRendererOptions: SimulationRendererOptions = {
shadows: {
enabled: true,
color: 'rgba(0,0,0,0.3)',
offset: 15,
speed: 1
},
dnd: {
rotation: Math.PI / 12 // 7.5 degrees
}
}
export class SimulationRenderer {
public camera = new Camera()
public mouseDownOutput = new Subject<MouseEventInfo>()
public mouseUpOutput = new Subject<MouseEventInfo>()
public mouseMoveOutput = new Subject<MouseEventInfo>()
public selectedGate: number | null = null
public selectOffset: vector2 = [0, 0]
public movedSelection = false
private options: SimulationRendererOptions
private mouseManager = new MouseManager(this.mouseMoveOutput)
private screen = new Screen()
public constructor(
options: Partial<SimulationRendererOptions> = {},
public simulation = new Simulation()
) {
this.options = merge(options, defaultSimulationRendererOptions)
this.init()
}
public init() {
this.mouseDownOutput.subscribe(event => {
const worldPosition = this.camera.toWordPostition(event.position)
const gates = Array.from(this.simulation.gates)
// We need to iterate from the last to the first
// because if we have 2 overlapping gates,
// we want to select the one on top
for (let index = gates.length - 1; index >= 0; index--) {
const { transform, id } = gates[index]
if (pointInSquare(worldPosition, transform)) {
this.mouseManager.clear(worldPosition[0])
this.movedSelection = false
this.selectedGate = id
this.selectOffset = worldPosition.map(
(position, index) =>
position - transform.position[index]
) as vector2
const gateNode = this.simulation.gates.get(id)
if (gateNode) {
return this.simulation.gates.moveOnTop(gateNode)
}
}
}
})
this.mouseUpOutput.subscribe(event => {
if (this.selectedGate !== null) {
const selected = this.getSelected()
if (selected) {
selected.transform.rotation = 0
}
this.selectedGate = null
}
})
this.mouseMoveOutput.subscribe(event => {
if (this.selectedGate !== null) {
const gate = this.getGateById(this.selectedGate)
if (!gate || !gate.data) return
const transform = gate.data.transform
const worldPosition = this.camera.toWordPostition(
event.position
)
transform.x = worldPosition[0] - this.selectOffset[0]
transform.y = worldPosition[1] - this.selectOffset[1]
if (!this.movedSelection) {
this.movedSelection = true
}
}
})
}
public render(ctx: CanvasRenderingContext2D) {
this.clear(ctx)
// render gates
for (const gate of this.simulation.gates) {
if (this.options.shadows.enabled) {
renderGateShadow(ctx, this.options.shadows.color, gate)
}
renderGate(ctx, gate)
}
}
public clear(ctx: CanvasRenderingContext2D) {
const boundingBox = this.camera.transform.getBoundingBox()
ctx.clearRect(...boundingBox)
}
public getGateById(id: number) {
return this.simulation.gates.get(id)
}
public getOptimalShadow(gate: Gate) {
const center = multiply([this.screen.x, this.screen.y] as vector2, 0.5)
const difference = add(center, invert(gate.transform.position))
return add(
add(difference, center),
ofLength(difference, this.options.shadows.offset)
)
}
public getShadowPosition(gate: Gate) {
return gate.transform.position.map(
(value, index) => value - this.getOptimalShadow(gate)[index]
) as vector2
}
public update(delta: number) {
for (const gate of this.simulation.gates) {
gate.shadow = smoothStep(
this.options.shadows.speed,
gate.shadow,
this.getShadowPosition(gate)
)
}
const selected = this.getSelected()
if (selected && this.movedSelection) {
this.mouseManager.update()
selected.transform.rotation =
this.mouseManager.getDirection() * this.options.dnd.rotation
} else {
this.mouseManager.update()
}
}
public getSelected() {
if (this.selectedGate === null) return null
const gate = this.getGateById(this.selectedGate)
if (!gate || !gate.data) return null
return gate.data
}
}

View file

@ -0,0 +1,50 @@
import { BehaviorSubject } from 'rxjs'
export type vector2 = [number, number]
export type vector4 = [number, number, number, number]
export class Transform {
public constructor(
public position: vector2 = [0, 0],
public scale: vector2 = [1, 1],
public rotation = 0
) {}
public getBoundingBox() {
return [...this.position, ...this.scale] as vector4
}
/** Short forms for random stuff */
get x() {
return this.position[0]
}
get y() {
return this.position[1]
}
get width() {
return this.scale[0]
}
get height() {
return this.scale[1]
}
get maxX() {
return this.x + this.width
}
get maxY() {
return this.y + this.height
}
set x(value: number) {
this.position = [value, this.y]
}
set y(value: number) {
this.position = [this.x, value]
}
}

View file

@ -0,0 +1,6 @@
export const clamp = (low: number, high: number, current: number) => {
if (current < low) return low
if (current > high) return high
return current
}

View file

@ -0,0 +1,31 @@
import { Transform } from '../classes/Transform'
export const drawRotatedSquare = (
ctx: CanvasRenderingContext2D,
{ position, scale, rotation }: Transform,
rotationMode = 0
) => {
ctx.save()
ctx.translate(...position)
if (rotationMode === 0) {
ctx.translate(scale[0] / 2, scale[1] / 2)
} else if (rotationMode === 1) {
ctx.translate(scale[0], scale[1])
} else if (rotationMode === 1) {
ctx.translate(0, scale[1])
}
ctx.rotate(rotation)
if (rotationMode === 0) {
ctx.fillRect(scale[0] / -2, scale[1] / -2, ...scale)
} else if (rotationMode === 1) {
ctx.fillRect(-scale[0], -scale[1], ...scale)
} else if (rotationMode === -1) {
ctx.fillRect(0, 0, ...scale)
}
ctx.restore()
}

View file

@ -0,0 +1,11 @@
import { vector2, Transform } from '../classes/Transform'
import { LruCacheNode } from '@eix-js/utils'
export const pointInSquare = (point: vector2, square: Transform) => {
return (
point[0] >= square.x &&
point[0] <= square.maxX &&
point[1] >= square.y &&
point[1] <= square.maxY
)
}

View file

@ -0,0 +1,13 @@
import { Gate } from '../classes/Gate'
import { drawRotatedSquare } from './drawRotatedSquare'
import { MouseManager } from '../classes/MouseManager'
export const renderGate = (ctx: CanvasRenderingContext2D, gate: Gate) => {
let mode = 0
if (gate.transform.rotation > 0) mode = 1
else if (gate.transform.rotation < 0) mode = -1
ctx.fillStyle = gate.color
drawRotatedSquare(ctx, gate.transform, mode)
}

View file

@ -0,0 +1,19 @@
import { Gate } from '../classes/Gate'
import { vector2, Transform } from '../classes/Transform'
import { clamp } from './clamp'
import { drawRotatedSquare } from './drawRotatedSquare'
export const renderGateShadow = (
ctx: CanvasRenderingContext2D,
color: string,
gate: Gate
) => {
const scale = gate.transform.scale
ctx.fillStyle = color
drawRotatedSquare(
ctx,
new Transform(gate.shadow, scale, gate.transform.rotation)
)
}

View file

@ -0,0 +1 @@
export type DeepPartial<T> = { [key in keyof T]?: DeepPartial<T[key]> }

View file

@ -0,0 +1,24 @@
import { vector2 } from '../../simulation/classes/Transform'
// Basic stuff for arrays
export const add = (first: vector2, second: vector2) =>
first.map((value, index) => value + second[index]) as vector2
export const invert = (vector: vector2) => vector.map(val => -val) as vector2
export const length = (vector: vector2) =>
Math.sqrt(vector[0] ** 2 + vector[1] ** 2)
export const multiply = (vector: vector2, scalar: number) =>
vector.map(val => val * scalar) as vector2
export const normalise = (vector: vector2) => {
const size = length(vector)
return vector.map(val => val / size) as vector2
}
export const ofLength = (vector: vector2, l: number) => {
return multiply(vector, l / length(vector))
}

View file

@ -0,0 +1,8 @@
import { vector2 } from '../../simulation/classes/Transform'
// TODO: rename
export const smoothStep = (step: number, current: vector2, target: vector2) => {
return current.map(
(position, index) => position + (target[index] - position) / step
) as vector2
}

View file

@ -1,118 +0,0 @@
@import "./toastr.scss";
@import "./modal.scss";
$mdc-theme-primary: orange;
$mdc-theme-secondary: white;
$mdc-theme-on-primary: white;
$mdc-theme-surface: black;
$mdc-theme-on-secondary: white;
$mdc-theme-text-secondary-on-background: white;
@import "@material/drawer/mdc-drawer.scss";
@import "@material/list/mdc-list";
@import "@material/dialog/mdc-dialog";
@import "@material/button/mdc-button";
@import "@material/menu-surface/mdc-menu-surface";
@import "@material/menu/mdc-menu";
@import "@material/textfield/mdc-text-field";
html, body {
padding: 0;
margin: 0;
overflow: hidden;
height:100%;
display: flex;
}
svg {
background-color: #222222;
height: 100%;
width: 100%;
display: block;
}
// .mdc-list-item__secondary-text {
// color: rgba(128,128,128,0.5) !important;
// margin-bottom: 3px;
// }
.createBar {
z-index:10;
position: absolute;
top:0px;
left:0px;
width:100%;
height:100%;
background-color: rgba(0,0,0,0.5);
transition: all 0.6s ease-in-out 0s;
.topContainer {
height: 30%;
width: 100%;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
div{
height:25%;
width:75%;
input{
background-color: #444444;
color: white;
border: none;
font-size: 250%;
height:100%;
width:100%;
padding: 1%;
font-family: "roboto";
}
}
}
opacity: 0;
visibility: hidden;
}
.createBar#shown{
opacity: 1;
visibility: visible;
}
.toasts{
background-color: #000000;
box-shadow: 0 0 0px black !important;
}
div.component-container{
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
img.component {
// border: 10px solid black;
border-radius: 20px;
user-select: none;
}
}
.main-sidebar {
position: absolute;
display: flex;
background: #111111;
color: white;
right: 0;
}
.mdc-list-item {
color:white !important;
}
.material-icons {
color: white !important;
}
.mdc-list-item--activated {
background : orange;
*{
color: rgb(255, 255, 255);
}
}

View file

@ -1,153 +0,0 @@
/**************************\
Basic Modal Styles
\**************************/
.modal {
font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
}
.modal__overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
}
.modal__container {
background-color: #222;
color: rgb(256,256,256);
padding: 30px;
max-width: 500px;
max-height: 100vh;
border-radius: 4px;
overflow-y: auto;
box-sizing: border-box;
}
.modal__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.modal__title {
margin-top: 0;
margin-bottom: 0;
font-weight: 600;
font-size: 1.25rem;
line-height: 1.25;
color: #0099aa;
box-sizing: border-box;
}
.modal__close {
background: transparent;
border: 0;
color: white;
}
.modal__header .modal__close:before { content: "\2715"; }
.modal__content {
margin-top: 2rem;
margin-bottom: 2rem;
line-height: 1.5;
color: rgba(256,256,256,0.8);
}
.modal__btn {
font-size: .875rem;
padding-left: 1rem;
padding-right: 1rem;
padding-top: .5rem;
padding-bottom: .5rem;
background-color: #e6e6e6;
color: rgba(0,0,0,.8);
border-radius: .25rem;
border-style: none;
border-width: 0;
cursor: pointer;
-webkit-appearance: button;
text-transform: none;
overflow: visible;
line-height: 1.15;
margin: 0;
will-change: transform;
-moz-osx-font-smoothing: grayscale;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translateZ(0);
transform: translateZ(0);
transition: -webkit-transform .25s ease-out;
transition: transform .25s ease-out;
transition: transform .25s ease-out,-webkit-transform .25s ease-out;
}
.modal__btn:focus, .modal__btn:hover {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
.modal__btn-primary {
background-color: #00449e;
color: #fff;
}
/**************************\
Demo Animation Style
\**************************/
@keyframes mmfadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes mmfadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes mmslideIn {
from { transform: translateY(15%); }
to { transform: translateY(0); }
}
@keyframes mmslideOut {
from { transform: translateY(0); }
to { transform: translateY(-10%); }
}
.micromodal-slide {
display: none;
}
.micromodal-slide.is-open {
display: block;
}
.micromodal-slide[aria-hidden="false"] .modal__overlay {
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.micromodal-slide[aria-hidden="false"] .modal__container {
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
}
.micromodal-slide[aria-hidden="true"] .modal__overlay {
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.micromodal-slide[aria-hidden="true"] .modal__container {
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
}
.micromodal-slide .modal__container,
.micromodal-slide .modal__overlay {
will-change: transform;
}

File diff suppressed because one or more lines are too long

View file

@ -1,5 +0,0 @@
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)
}

View file

@ -1 +0,0 @@
export * from "./clamp"

View file

@ -1,231 +0,0 @@
import { BehaviorSubject, Subscription, timer } from "rxjs";
import { ComponentState, activationContext } from "./interfaces";
import { map, debounce } from "rxjs/operators";
import { Screen } from "../screen.ts";
import { ComponentTemplateStore } from "../componentManager/componentTemplateStore";
import { svg } from "lit-html";
import { subscribe } from "lit-rx";
import { Pin } from "../pin";
import { success, error } from "toastr"
import { alertOptions } from "../componentManager/alertOptions";
import { WireManager } from "../wires";
import { runCounter } from "./runCounter";
import { Material } from "./material";
import { manager } from "../../main";
export class Component {
private static store = new ComponentTemplateStore()
private static screen = new Screen()
private static wireManager = new WireManager()
public position = new BehaviorSubject<number[]>(null)
public scale = new BehaviorSubject<number[]>(null)
public clicked = false
public id: number
public material: Material
public clickedChanges = new BehaviorSubject(false)
public scaling = false
private mouserDelta: number[]
private strokeColor = "#888888"
private inputs: number
private outputs: number
private activation: ((ctx: activationContext) => any)[] = []
private subscriptions: Subscription[] = []
public inputPins: Pin[] = []
public outputPins: Pin[] = []
public x = this.position.pipe(map(val =>
val[0]
))
public y = this.position.pipe(map(val =>
val[1]
))
public width = this.scale.pipe(map(val =>
val[0]
))
public height = this.scale.pipe(map(val =>
val[1]
))
constructor(private template: string,
position: [number, number] = [0, 0],
scale: [number, number] = [0, 0],
id?: number) {
//set initial props
this.position.next(position)
this.scale.next(scale)
//set the correct id
this.id = (typeof id === "number") ? id : Component.getId()
//load template
const data = Component.store.store.get(template)
if (!data)
throw new Error(`Template ${template} doesnt exist`)
this.inputs = data.inputs
this.outputs = data.outputs
this.inputPins = [...Array(this.inputs)].fill(true).map(() => new Pin(false, this))
this.outputPins = [...Array(this.outputs)].fill(true).map(() => new Pin(true, this))
this.activation = [data.activation, data.onclick ? data.onclick : ""]
.map(val => {
return new Function(`return (ctx) => {
try{
${val}
}
catch(err){
ctx.error(err,"",ctx.alertOptions)
}
}`)()
})
this.inputPins.forEach(val => {
const subscription = val.valueChanges.pipe(debounce(() => timer(1000 / 60)))
.subscribe(() => this.activate())
this.subscriptions.push(subscription)
})
this.material = new Material(data.material.mode, data.material.data)
this.activate()
}
public dispose() {
this.subscriptions.forEach(val => val.unsubscribe())
}
public handleMouseUp() {
this.clicked = false
this.scaling = false
this.clickedChanges.next(this.clicked)
}
private activate(index: number = 0) {
this.activation[index]({
outputs: this.outputPins,
inputs: this.inputPins,
succes: (mes: string) => { success(mes, "", alertOptions) },
error: (mes: string) => { error(mes, "", alertOptions) },
color: (color: string) => {
this.material.color.next(color)
}
} as activationContext)
}
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) {
if (e.button === 0) {
const mousePosition = Component.screen.getWorldPosition(e.clientX, e.clientY)
this.mouserDelta = this.position.value.map((value, index) =>
mousePosition[index] - value
)
this.clicked = true
this.clickedChanges.next(this.clicked)
this.activate(1)
this.activate(0)
}
else if (e.button === 1) {
this.scaling = true
}
else if (e.button === 2) {
manager.components = manager.components.filter(({ id }) => id !== this.id)
manager.wireManager.wires
.filter(val => val.input.of.id == this.id || val.output.of.id == this.id)
.forEach(val => {
manager.wireManager.remove(val)
})
manager.silentRefresh()
}
}
handlePinClick(pin: Pin) {
Component.wireManager.add(pin)
}
get state(): ComponentState {
return {
position: this.position.value as [number, number],
scale: this.scale.value as [number, number],
template: this.template,
id: this.id
}
}
pinsSvg(pinScale: number, pinLength = 20, mode = "input") {
const stroke = 3
return ((mode === "input") ? this.inputPins : this.outputPins)
.map((val, index) => {
const y = subscribe(this.piny(mode === "input", index))
const x = subscribe(this.pinx(mode === "input", pinLength))
const linex = subscribe(this.x.pipe(map(val =>
val + ((mode === "input") ? -pinLength : pinLength + this.scale.value[0])
)))
const middleX = subscribe(this.x.pipe(map(val => {
const scale = this.scale.value[0]
return val + ((mode === "input") ? scale / 10 : 9 * scale / 10)
})))
return svg`
<line stroke=${this.strokeColor} y1=${y} y2=${y}
x1=${(mode === "input") ? linex : middleX}
x2=${(mode !== "input") ? linex : middleX}
stroke-width=${stroke}></line>
<circle fill=${subscribe(val.svgColor)}
stroke=${this.strokeColor}
r=${pinScale}
cx=${x}
cy=${y} stroke-width=${stroke}
@click=${() => this.handlePinClick(val)}
></circle>
`})
}
public pinx(mode = true, pinLength = 15) {
return this.position.pipe(
map(val => val[0] + (
(mode) ?
-pinLength :
this.scale.value[0] + pinLength
))
)
}
public piny(mode = true, index: number) {
const space = this.scale.value[1] / (mode ? this.inputs : this.outputs)
return this.y.pipe(
map(val => val + space * (2 * index + 1) / 2)
)
}
static fromState(state: ComponentState) {
return new Component(state.template, state.position, state.scale, state.id)
}
public static getId() {
const data = runCounter.get()
runCounter.increase()
return data
}
}

View file

@ -1 +0,0 @@
export * from "./component"

View file

@ -1,18 +0,0 @@
import { Pin } from '../pin'
export interface ComponentState {
position: [number, number]
scale: [number, number]
template: string
id: number
}
export interface activationContext {
inputs: Pin[]
outputs: Pin[]
succes: (mes: string) => any
error: (mes: string) => any
color: (color: string) => void
}
export type materialMode = 'standard_image' | 'color' | 'url'

View file

@ -1,37 +0,0 @@
import { svg, Part } from 'lit-html'
import { BehaviorSubject } from 'rxjs'
import { materialMode } from './interfaces'
declare function require<T>(path: string): T
type partFactory = (part: Part) => void
export class Material {
private static images: {
[key: string]: string
} = {
and: require('../../../assets/and_gate.jpg'),
or: require('../../../assets/or_gate.png'),
xor: require('../../../assets/xor_gate.png'),
nor: require('../../../assets/nor_gate.png')
}
public color = new BehaviorSubject<string>('rgba(0,0,0,0)')
constructor(public mode: materialMode, public data: string) {
if (this.mode === 'color') this.color.next(data)
}
innerHTML(x: partFactory, y: partFactory, w: partFactory, h: partFactory) {
const src =
this.mode === 'standard_image'
? Material.images[this.data]
: this.data
return svg`<foreignobject x=${x} y=${y} width=${w} height=${h}>
<div class="component-container">
<img src=${src} height="97%" width="97%" draggable=false class="component">
</div>
</foreignobject>`
}
}

View file

@ -1,14 +0,0 @@
import { Store } from "../store";
export const runCounter = {
store: new Store<number>("runCounter"),
get(){
return runCounter.store.get("main")
},
increase(){
runCounter.store.set("main", runCounter.store.get("main") + 1)
}
}
if (!runCounter.get())
runCounter.store.set("main",1)

View file

@ -1,23 +0,0 @@
import { fecthAsJson } from './fetchJson'
import { getFirstFileFromGist, getGist } from './getGist'
export const evalImport = async <T>(
command: string,
extension = 'json'
): Promise<T> => {
const words = command.split(' ')
let final: T
if (words.length === 1) {
if (extension === 'json') {
final = await fecthAsJson<T>(command)
} else {
final = ((await (await fetch(command)).text()) as unknown) as T
}
} else if (words[0] === 'gist') {
final = getFirstFileFromGist(await getGist(words[1]), extension)
}
return final
}

View file

@ -1,6 +0,0 @@
export const fecthAsJson = async <T>(url: string) => {
const res = await fetch(url)
const json = await res.json()
return json as T
}

View file

@ -1,27 +0,0 @@
import { fecthAsJson } from './fetchJson'
export interface Gist {
files: Record<string, { content: string }>
}
export const getGist = async (id: string) => {
const url = `https://api.github.com/gists/${id}`
const json = await fecthAsJson<Gist>(url)
return json
}
export const getFirstFileFromGist = (gist: Gist, extension = 'json') => {
const content =
gist.files[
Object.keys(gist.files).find(
name => name.indexOf(`.${extension}`) !== -1
)
].content
if (extension === 'json') {
return JSON.parse(content)
}
return content
}

View file

@ -1,62 +0,0 @@
import { ComponentTemplate } from '../componentManager/interfaces'
import { ComponentManager } from '../componentManager'
import { materialMode } from '../component/interfaces'
import { fecthAsJson } from './fetchJson'
import { getGist, getFirstFileFromGist } from './getGist'
import { evalImport } from './evalImport'
export interface Importable {
name: string
activation: string
onClick?: string
inputs: number
outputs: number
material: {
mode: materialMode
data: string
}
}
const defaults: Importable = {
activation: 'ctx.outputs[0].value = ctx.inputs[0].value',
inputs: 1,
outputs: 1,
material: {
mode: 'color',
data: 'red'
},
name: 'Imported component'
}
export async function parseActivation(activaton: string) {
const words = activaton.split(' ')
if (words[0] === 'url') {
return await evalImport<string>(words.slice(1).join(' '), 'js')
} else {
return activaton
}
}
export async function importComponent(
manager: ComponentManager,
command: string
): Promise<ComponentTemplate> {
const final: Importable = await evalImport(command)
const template: ComponentTemplate = {
...defaults,
...final,
editable: false,
version: '1.0.0',
imported: true,
importCommand: command
}
template.activation = await parseActivation(template.activation)
manager.templateStore.store.set(template.name, template)
manager.succes(`Succesfully imported component ${template.name}`)
return template
}

View file

@ -1,4 +0,0 @@
export const alertOptions = {
positionClass: "toast-bottom-right",
toastClass: "toasts"
}

View file

@ -1,735 +0,0 @@
import { Singleton } from '@eix/utils'
import { Component } from '../component'
import { Subject, BehaviorSubject, fromEvent } from 'rxjs'
import { svg, SVGTemplateResult, html } from 'lit-html'
import { subscribe } from 'lit-rx'
import { Screen } from '../screen.ts'
import { ManagerState, ComponentTemplate } from './interfaces'
import { Store } from '../store'
import { KeyboardInput } from '@eix/input'
import { success, error } from 'toastr'
import { ComponentTemplateStore } from './componentTemplateStore'
import { alertOptions } from './alertOptions'
import { WireManager } from '../wires'
import { runCounter } from '../component/runCounter'
import { Settings } from '../store/settings'
import { download } from './download'
import { modal } from '../modals'
import { map } from 'rxjs/operators'
import { persistent } from '../store/persistent'
import { MDCTextField } from '@material/textfield'
import { importComponent } from '../componentImporter/importComponent'
const defaultName = 'default'
@Singleton
export class ComponentManager {
public components: Component[] = []
public svgs = new Subject<SVGTemplateResult>()
public placeholder = new BehaviorSubject('Create simulation')
public barAlpha = new BehaviorSubject<string>('0')
public wireManager = new WireManager()
public onTop: Component
public templateStore = new ComponentTemplateStore()
private temporaryCommnad = ''
private clicked = false
private ignoreKeyDowns = false
private screen = new Screen()
private settings = new Settings()
private standard: {
offset: number
scale: [number, number]
} = {
offset: 50,
scale: [100, 100]
}
private commandHistoryStore = new Store<string>('commandHistory')
private store = new Store<ManagerState>('simulationStates')
private saveEvent = new KeyboardInput('s')
private createEvent = new KeyboardInput('m')
private closeInputEvent = new KeyboardInput('enter')
private ctrlEvent = new KeyboardInput('ctrl')
private palleteEvent = new KeyboardInput('p')
private undoEvent = new KeyboardInput('z')
private shiftEvent = new KeyboardInput('shift')
private refreshEvent = new KeyboardInput('r')
private gEvent = new KeyboardInput('g')
private clearEvent = new KeyboardInput('delete')
private upEvent = new KeyboardInput('up')
private downEvent = new KeyboardInput('down')
@persistent<ComponentManager, string>(defaultName, 'main')
public name: string
public alertOptions = alertOptions
private commandHistory: string[] = []
private commands: {
[key: string]: (
ctx: ComponentManager,
args: string[],
flags: string[]
) => any
} = {
clear(ctx: ComponentManager) {
ctx.clear()
},
save(ctx: ComponentManager) {
ctx.save()
},
ls(ctx: ComponentManager) {
const data = ctx.store.ls()
const message = data.join('\n')
success(message, '', ctx.alertOptions)
},
help(ctx: ComponentManager) {
success(
`Usage: &ltcommand> <br>
Where &ltcommand> is one of:
<ul>
${Object.keys(ctx.commands)
.map(
val => `
<li>${val}</li>
`
)
.join('')}
</ul>
`,
'',
ctx.alertOptions
)
},
refresh(ctx: ComponentManager) {
ctx.refresh()
},
rewind(ctx: ComponentManager) {
localStorage.clear()
success('Succesfully cleared localStorage!', '', ctx.alertOptions)
},
ctp: this.templateStore.commands.template,
settings: this.settings.commands,
download
}
private inputMode: string
public gates = this.templateStore.store.lsChanges
public saves = this.store.lsChanges
public file: {
[key: string]: () => void
} = {
clear: () => this.clear(),
clean: () => this.smartClear(),
save: () => this.save(),
undo: () => this.refresh(),
download: () => download(this, [], []),
delete: () => this.delete(this.name),
refresh: () => this.silentRefresh(true)
}
public shortcuts: {
[key: string]: string
} = {
clear: 'shift delete',
clean: 'delete',
save: 'ctrl s',
undo: 'ctrl z',
refresh: 'ctrl r'
}
constructor() {
runCounter.increase()
this.svgs.next(this.render())
this.refresh()
fromEvent(document.body, 'keydown').subscribe((e: KeyboardEvent) => {
if (this.barAlpha.value == '1') {
const elem = document.getElementById('nameInput')
elem.focus()
} else if (!this.ignoreKeyDowns) {
e.preventDefault()
}
})
fromEvent(document.body, 'keyup').subscribe((e: MouseEvent) => {
if (this.barAlpha.value === '1') {
if (this.closeInputEvent.value) this.create()
else if (this.inputMode === 'command') {
const elem = <HTMLInputElement>(
document.getElementById('nameInput')
)
if (this.upEvent.value) {
document.body.focus()
e.preventDefault()
const index = this.commandHistory.indexOf(elem.value)
if (index) {
//save drafts
if (index === -1) this.temporaryCommnad = elem.value
const newIndex =
index === -1
? this.commandHistory.length - 1
: index - 1
elem.value = this.commandHistory[newIndex]
}
}
if (this.downEvent.value) {
document.body.focus()
e.preventDefault()
const index = this.commandHistory.indexOf(elem.value)
if (index > -1) {
const maxIndex = this.commandHistory.length - 1
elem.value =
index === maxIndex
? this.temporaryCommnad
: this.commandHistory[index + 1]
}
}
}
} else {
if (this.ctrlEvent.value) {
if (this.createEvent.value) {
this.prepareNewSimulation()
} else if (
this.shiftEvent.value &&
this.palleteEvent.value
) {
this.preInput()
this.inputMode = 'command'
this.placeholder.next('Command palette')
} else if (this.gEvent.value) {
this.importGate()
} else if (this.saveEvent.value) {
this.save()
} else if (this.undoEvent.value) {
this.refresh()
} else if (this.refreshEvent.value) {
this.silentRefresh(true)
}
} else if (this.clearEvent.value) {
if (this.shiftEvent.value) this.clear()
else this.smartClear()
}
}
})
this.wireManager.update.subscribe(() => {
// this.save()
this.update()
// this.save()
})
if (this.saves.value.length === 0) this.save()
}
private initEmptyGate(name: string) {
const obj: ComponentTemplate = {
inputs: 1,
name,
version: '1.0.0',
outputs: 1,
activation: '',
editable: true,
material: {
mode: 'color',
data: 'blue'
}
}
this.templateStore.store.set(name, obj)
this.edit(name)
}
public newGate() {
this.preInput()
this.inputMode = 'gate'
this.placeholder.next('Gate name')
}
public importGate() {
this.preInput()
this.inputMode = 'importGate'
this.placeholder.next('Gate url')
}
public prepareNewSimulation() {
this.preInput()
this.inputMode = 'create'
this.placeholder.next('Create simulation')
}
private preInput() {
const elem = <HTMLInputElement>document.getElementById('nameInput')
elem.value = ''
this.barAlpha.next('1')
}
private async create() {
const elem = <HTMLInputElement>document.getElementById('nameInput')
this.barAlpha.next('0')
if (this.inputMode === 'create') {
await this.createEmptySimulation(elem.value)
success(
`Succesfully created simulation ${elem.value}`,
'',
this.alertOptions
)
} else if (this.inputMode === 'command') this.eval(elem.value)
else if (this.inputMode === 'gate') this.initEmptyGate(elem.value)
else if (this.inputMode === 'importGate') {
importComponent(this, elem.value)
}
}
public succes(message: string) {
success(message, '', this.alertOptions)
}
private async handleDuplicateModal(name: string) {
const result = await modal({
title: 'Warning',
content: html`
There was already a simulation called ${name}, are you sure you
want to override it? All your work will be lost!
`
})
return result
}
public async edit(name: string) {
this.ignoreKeyDowns = true
const gate = this.templateStore.store.get(name)
modal({
no: '',
yes: 'save',
title: `Edit ${name}`,
content: html`
${html`
<br />
<div class="mdc-text-field mdc-text-field--textarea">
<textarea
id="codeArea"
class="mdc-text-field__input js"
rows="8"
cols="40"
>
${gate.activation}</textarea
>
<div class="mdc-notched-outline">
<div class="mdc-notched-outline__leading"></div>
<div class="mdc-notched-outline__notch">
<label for="textarea" class="mdc-floating-label"
>Activation function</label
>
</div>
<div class="mdc-notched-outline__trailing"></div>
</div>
</div>
<br /><br />
<div class="mdc-text-field" id="inputCount">
<input
type="number"
id="my-text-field"
class="mdc-text-field__input inputCount-i"
value=${gate.inputs}
/>
<label class="mdc-floating-label" for="my-text-field"
>Inputs</label
>
<div class="mdc-line-ripple"></div>
</div>
<br /><br />
<div class="mdc-text-field" id="outputCount">
<input
type="number"
id="my-text-field"
class="mdc-text-field__input outputCount-i"
value=${gate.outputs}
/>
<label class="mdc-floating-label" for="my-text-field"
>Outputs</label
>
<div class="mdc-line-ripple"></div>
</div>
<br /><br />
<div class="mdc-text-field" id="color">
<input
type="string"
id="my-text-field"
class="mdc-text-field__input color-i"
value=${gate.material.data}
/>
<label class="mdc-floating-label" for="my-text-field"
>Color</label
>
<div class="mdc-line-ripple"></div>
</div>
<br />
`}
`
}).then(val => {
this.ignoreKeyDowns = false
const elems: (HTMLInputElement | HTMLTextAreaElement)[] = [
document.querySelector('#codeArea'),
document.querySelector('.inputCount-i'),
document.querySelector('.outputCount-i'),
document.querySelector('.color-i')
]
const data = elems.map(val => val.value)
this.templateStore.store.set(name, {
...gate,
activation: data[0],
inputs: Number(data[1]),
outputs: Number(data[2]),
material: {
mode: 'color',
data: data[3]
}
})
})
new MDCTextField(document.querySelector('.mdc-text-field'))
new MDCTextField(document.querySelector('#outputCount'))
new MDCTextField(document.querySelector('#inputCount'))
new MDCTextField(document.querySelector('#color'))
}
public add(template: string, position?: [number, number]) {
const pos = position
? position
: ([...Array(2)].fill(
this.standard.offset * this.components.length
) as [number, number])
this.components.push(new Component(template, pos, this.standard.scale))
this.update()
}
public async delete(name: string) {
const res = await modal({
title: 'Are you sure?',
content: html`
Deleting a simulations is ireversible, and all work will be
lost!
`
})
if (res) {
if (this.name === name) {
if (this.saves.value.length > 1) {
this.switchTo(this.saves.value.find(val => val !== name))
} else {
let newName =
name === defaultName ? `${defaultName}(1)` : defaultName
await this.createEmptySimulation(newName)
this.switchTo(newName)
}
}
this.store.delete(name)
}
}
public createEmptySimulation(name: string) {
const create = () => {
this.store.set(name, {
wires: [],
components: [],
position: [0, 0],
scale: [1, 1]
})
if (name !== this.name) this.save()
this.name = name
this.refresh()
}
return new Promise(async res => {
//get wheater theres already a simulation with that name
if (
(this.store.get(name) &&
(await this.handleDuplicateModal(name))) ||
!this.store.get(name)
) {
create()
res(true)
}
})
}
public switchTo(name: string) {
const data = this.store.get(name)
if (!data)
error(
`An error occured when trying to load ${name}`,
'',
this.alertOptions
)
this.name = name
this.refresh()
}
eval(command: string) {
if (!this.commandHistory.includes(command))
// no duplicates
this.commandHistory.push(command)
while (
this.commandHistory.length > 10 // max of 10 elements
)
this.commandHistory.shift()
const words = command.split(' ')
if (words[0] in this.commands) {
const remaining = words.slice(1)
const flags = remaining.filter(val => val[0] == '-')
const args = remaining.filter(val => val[0] != '-')
this.commands[words[0]](this, args, flags)
} else
error(
`Command ${words} doesn't exist. Run help to get a list of all commands.`,
'',
this.alertOptions
)
}
public smartClear() {
this.components = this.components.filter(({ id }) =>
this.wireManager.wires.find(
val => val.input.of.id == id || val.output.of.id == id
)
)
this.update()
success(
'Succesfully cleared all unconnected components',
'',
this.alertOptions
)
}
public clear() {
this.components = []
this.wireManager.dispose()
this.update()
success('Succesfully cleared all components', '', this.alertOptions)
}
public refresh() {
if (this.store.get(this.name)) {
this.loadState(this.store.get(this.name))
}
for (const i of this.commandHistoryStore.ls())
this.commandHistory[Number(i)] = this.commandHistoryStore.get(i)
this.update()
success(
'Succesfully refreshed to the latest save',
'',
this.alertOptions
)
}
update() {
this.svgs.next(this.render())
}
handleMouseDown() {
this.clicked = true
}
handleMouseUp() {
this.clicked = false
}
handleMouseMove(e: MouseEvent) {
if (e.button === 0) {
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 (false) { }
if (toAddOnTop >= 0) {
this.top(this.components[toAddOnTop])
} 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)
}
}
}
public silentRefresh(verboose = false) {
this.loadState(this.state)
if (verboose)
success(
'Succesfully reloaded all components',
'',
this.alertOptions
)
}
public top(component: Component) {
if (this.onTop !== component) {
this.onTop = component
this.components.push(component)
}
this.update()
}
private render() {
let toRemoveDuplicatesFor: Component
const result = this.components.map(component => {
const mouseupHandler = () => {
component.handleMouseUp()
toRemoveDuplicatesFor = component
}
const stroke = subscribe(
component.clickedChanges.pipe(
map(val => (val ? 'yellow' : 'black'))
)
)
return svg`
<g>
${component.pinsSvg(10, 20)}
${component.pinsSvg(10, 20, 'output')}
<g @mousedown=${(e: MouseEvent) => component.handleClick(e)}
@touchstart=${(e: MouseEvent) => component.handleClick(e)}
@mouseup=${mouseupHandler}
@touchend=${mouseupHandler}>
<rect width=${subscribe(component.width)}
height=${subscribe(component.height)}
x=${subscribe(component.x)}
y=${subscribe(component.y)}
stroke=${stroke}
fill=${
component.material.mode !== 'color'
? 'rgba(0,0,0,0)'
: subscribe(component.material.color)
}
rx=20
ry=20>
</rect>
${
component.material.mode !== 'color'
? component.material.innerHTML(
subscribe(component.x),
subscribe(component.y),
subscribe(component.width),
subscribe(component.height)
)
: ''
}
</g>
</g>
`
})
if (toRemoveDuplicatesFor) this.removeDuplicates(toRemoveDuplicatesFor)
return svg`${this.wireManager.svg} ${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(
(_, index) => instances.indexOf(index) != -1
)
}
get state(): ManagerState {
const components = Array.from(new Set(this.components).values())
return {
components: components.map(value => value.state),
position: this.screen.position as [number, number],
scale: this.screen.scale as [number, number],
wires: this.wireManager.state
}
}
get scaling(): Component {
return this.components.find(val => val.scaling)
}
public getComponentById(id: number) {
return this.components.find(val => val.id === id)
}
private loadState(state: ManagerState) {
if (!state.wires)
//old state
return
this.wireManager.dispose()
this.clicked = false
this.components = state.components.map(value =>
Component.fromState(value)
)
this.onTop = null
state.wires.forEach(val => {
this.wireManager.start = this.getComponentById(
val.from.owner
).outputPins[val.from.index]
this.wireManager.end = this.getComponentById(
val.to.owner
).inputPins[val.to.index]
this.wireManager.tryResolving()
})
this.screen.scale = state.scale
this.screen.position = state.position
this.screen.update()
this.update()
}
save() {
for (let i = 0; i < this.commandHistory.length; i++) {
const element = this.commandHistory[i]
this.commandHistoryStore.set(i.toString(), element)
}
this.store.set(this.name, this.state)
success(
`Saved the simulation ${this.name} succesfully!`,
'',
this.alertOptions
)
}
}

View file

@ -1,186 +0,0 @@
import { Singleton } from "@eix/utils";
import { Store } from "../store";
import { ComponentTemplate } from "./interfaces";
import { ComponentManager } from "./componentManager";
import { success, error } from "toastr"
@Singleton
export class ComponentTemplateStore {
public store = new Store<ComponentTemplate>("componentTemplate")
public commands = {
template: (ctx: ComponentManager, args: string[], flags: string[]) => {
const command = args[0]
switch (command) {
case (undefined):
for (let i of flags) {
if (i === "--version" || i === "-v")
return success("1.0.1", "", ctx.alertOptions)
}
error(`Welcome to the component template program!
To get started, try running this basic commands:
${["--version", "ls"].map(val => `${val}`).join(" ")}
`, "", {
...ctx.alertOptions,
timeOut: 7500
})
break
case ("ls"):
success(`Here is a list of all the current registered component templates (including ics):
<ul>
${this.store.ls().map(val => `
<li>
${val}
</li>
`).join(" ")}
</ul>
`, "", ctx.alertOptions)
break
case ("info"):
if (!args[1])
return error("You need to specify a template name", "", ctx.alertOptions)
const data = this.store.get(args[1])
if (!data)
return error(`Component ${args[1]} doesnt exist`, "", ctx.alertOptions)
const showFunction = flags.find(value =>
value === "-sf" || value === "--showFunctions"
)
success(`
Name: ${data.name} <br>
Inputs: ${data.inputs} <br>
Outputs: ${data.outputs}
${showFunction ? `<br> Activation: ${data.activation}` : ""}
`, "", ctx.alertOptions)
break
default:
error(`${command} is not a valid command for the template program`, "", ctx.alertOptions)
}
}
}
constructor() {
if (this.store.ls().length) return
this.store.set("buffer", {
inputs: 1,
outputs: 1,
name: "buffer",
version: "1.0.0",
activation: `
ctx.outputs[0].value = ctx.inputs[0].value
`.trim(),
material: {
mode: "color",
data: "blue"
}
})
this.store.set("not", {
inputs: 1,
outputs: 1,
name: "buffer",
version: "1.0.0",
activation: `
ctx.outputs[0].value = !ctx.inputs[0].value
`.trim(),
material: {
mode: "color",
data: "red"
}
})
this.store.set("and", {
inputs: 2,
outputs: 1,
name: "and",
version: "1.0.0",
activation: `
ctx.outputs[0].value = ctx.inputs[0].value && ctx.inputs[1].value
`.trim(),
material: {
mode: "standard_image",
data: "and"
}
})
this.store.set("or", {
inputs: 2,
outputs: 1,
name: "or",
version: "1.0.0",
activation: `
ctx.outputs[0].value = ctx.inputs[0].value || ctx.inputs[1].value
`.trim(),
material: {
mode: "standard_image",
data: "or"
}
})
this.store.set("nor", {
inputs: 2,
outputs: 1,
name: "nor",
version: "1.0.0",
activation: `
ctx.outputs[0].value = !(ctx.inputs[0].value || ctx.inputs[1].value)
`.trim(),
material: {
mode: "standard_image",
data: "nor"
}
})
this.store.set("xor", {
inputs: 2,
outputs: 1,
name: "xor",
version: "1.0.0",
activation: `
ctx.outputs[0].value = (ctx.inputs[0].value || ctx.inputs[1].value) && !(ctx.inputs[0].value && ctx.inputs[1].value)
`.trim(),
material: {
mode: "standard_image",
data: "xor"
},
editable: false
})
this.store.set("light", {
inputs: 1,
outputs: 0,
name: "light",
version: "1.0.0",
activation: `
if (ctx.inputs[0].value)
ctx.color("yellow")
else
ctx.color("white")
`.trim(),
material: {
mode: "color",
data: "white"
}
})
this.store.set("button", {
inputs: 0,
outputs: 1,
name: "button",
version: "1.0.0",
activation: `
ctx.outputs[0].value = ctx.outputs[0].memory.value
`.trim(),
material: {
mode: "color",
data: "red"
},
onclick: `
ctx.outputs[0].memory.value = !ctx.outputs[0].memory.value
if (ctx.outputs[0].memory.value)
ctx.color("#550000")
else
ctx.color("red")
`
})
}
}

View file

@ -1,31 +0,0 @@
import { ComponentManager } from "./componentManager";
import { success } from "toastr"
import { saveAs } from "file-saver"
const version = "1.0.0"
export const download = (ctx: ComponentManager, args: string[], flags:string[]) => {
//important flags
for (let i of flags) {
if (i === "--version" || i === "-v")
return success(`${version}`, "", ctx.alertOptions)
else if (i === "--help" || i === "-h")
return success(`Run "download" to download the save as a json file.
Flags:<ul>
<li>-v or --version to get the version.</li>
<li>-h or --help to get help (ou are reading that right now)</li>
<li>-s or --save to automatically save before downloading</li>
</ul>`,"",ctx.alertOptions)
}
const command = args[0]
if (command === undefined){
if (flags.includes("-s") || flags.includes("--save"))
ctx.save()
const data = JSON.stringify(ctx.state)
saveAs(new Blob([data]), `${ctx.name}.json`)
}
}

View file

@ -1 +0,0 @@
export * from "./componentManager"

View file

@ -1,25 +0,0 @@
import { ComponentState, materialMode } from '../component/interfaces'
import { WireState } from '../wires/interface'
export interface ManagerState {
components: ComponentState[]
scale: [number, number]
position: [number, number]
wires: WireState
}
export interface ComponentTemplate {
name: string
version: string
activation: string
onclick?: string
inputs: number
outputs: number
material: {
mode: materialMode
data: string
}
editable?: boolean
imported?: boolean
importCommand?: string
}

View file

@ -1 +0,0 @@
export * from "./modal"

View file

@ -1,8 +0,0 @@
import { TemplateResult } from "lit-html";
export interface confirmModalOptions {
title: string
content: TemplateResult
yes: string
no: string
}

View file

@ -1,92 +0,0 @@
import { render, html } from "lit-html"
import { confirmModalOptions } from "./interfaces";
import { fromEvent } from "rxjs";
import { MDCDialog } from '@material/dialog';
let lastId = 0
export const modal = (options: Partial<confirmModalOptions>) => new Promise((res, rej) => {
const defaultOptions = {
yes: "yes",
no: "no",
title: "modal",
content: html`Hello world!`
}
const parent = document.getElementsByClassName("ModalContainer")[0]
const { title, content, yes, no }: confirmModalOptions = { ...defaultOptions, ...options }
const id = lastId++
if (!parent)
rej(false)
const template = html`
<div class="mdc-dialog"
id="modal-${id}"
role="alertdialog"
aria-modal="true"
aria-labelledby="${title}"
aria-describedby="my-dialog-content">
<div class="mdc-dialog__container">
<div class="mdc-dialog__surface">
<h2 class="mdc-dialog__title" id="${title}">
${title}
</h2>
<div class="mdc-dialog__content" id="my-dialog-content">
${content}
</div>
<footer class="mdc-dialog__actions">
${(no !== "") ?
html`<button type="button" class="mdc-button mdc-dialog__button" id="no-${id}">
<span class="mdc-button__label">${no}</span>
</button>` : no
}
${(yes !== "") ?
html`<button type="button" class="mdc-button mdc-dialog__button" id="yes-${id}">
<span class="mdc-button__label">${yes}</span>
</button>` : yes
}
</footer>
</div>
</div>
<div class="mdc-dialog__scrim"></div>
</div>
`
render(template, parent)
const dialog = new MDCDialog(document.querySelector(`#modal-${id}`))
dialog.open()
const _yes = document.getElementById(`yes-${id}`)
const _no = document.getElementById(`no-${id}`)
const clear = () => {
// render(html``,parent)
dialog.close()
subscriptions.forEach(val => val.unsubscribe())
}
const subscriptions = [
fromEvent(_yes, "click").subscribe(val => {
clear()
res(true)
}),
fromEvent(_no, "click").subscribe(val => {
clear()
res(false)
})
]
})

View file

@ -1 +0,0 @@
export * from "./pin"

View file

@ -1,70 +0,0 @@
import { BehaviorSubject, Subject, Subscription } from "rxjs";
import { map } from "rxjs/operators"
import clamp from "../clamp/clamp";
import { Component } from "../component";
export class Pin {
private static lastId = 0
public pairs: Pin[] = []
private subscriptions: {
subscription: Subscription
key: Pin
}[] = []
public id: number
public _value = 0
public color = new BehaviorSubject<[number, number, number, number]>([0, 0, 0, 0])
public memory: any = {}
public valueChanges = new Subject<number>()
public svgColor = this.color.pipe(map(val =>
`rgba(${val.join(",")})`
))
constructor(public allowWrite = true, public of: Component) {
this.setValue(0)
this.id = Pin.lastId++
}
get value() {
return this._value
}
set value(value: number) {
if (!this.allowWrite) return
this.setValue(value)
}
public setValue(value: number) {
this._value = clamp(value, 0, 1)
this.valueChanges.next(this._value)
const color: [number, number, number, number] = (value > 0.5) ?
[255, 216, 20, 1] :
[90, 90, 90, 1]
this.color.next((this.pairs.length) ? color : [0, 0, 0, 0])
}
public bindTo(pin: Pin) {
this.pairs.push(pin)
const subscription = pin.valueChanges.subscribe(val => this.setValue(val))
this.subscriptions.push({
subscription,
key: pin
})
}
public unbind(pin: Pin) {
if (this.pairs.includes(pin)) {
this.pairs = this.pairs.filter(val => val !== pin)
this.subscriptions.filter(val => val.key === pin).forEach(({ subscription }) => subscription.unsubscribe())
}
}
public update() {
this.setValue(this._value)
}
}

View file

@ -1 +0,0 @@
export * from "./screen"

View file

@ -1,90 +0,0 @@
import { fromEvent, BehaviorSubject, combineLatest } from "rxjs"
import { Singleton } from "@eix/utils"
import { map, take } from "rxjs/operators";
import clamp from "../clamp/clamp";
import { manager } from "../../main";
@Singleton
export class Screen {
width = new BehaviorSubject<number>(0)
height = new BehaviorSubject<number>(0)
viewBox = combineLatest(this.width, this.height).pipe(map((values: [number, number]) =>
this.getViewBox(...values)
));
public position = [0, 0]
public scale = [2, 2]
private zoomLimits: [number, number] = [0.1, 10]
private scrollStep = 1.3
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 componentToScale = manager.scaling
const sign = e.deltaY / Math.abs(e.deltaY)
const zoom = this.scrollStep ** sign
if (componentToScale) {
const oldScale = componentToScale.scale.value
const newScale = oldScale.map(val => val / zoom)
componentToScale.scale.next(newScale)
componentToScale.position.pipe(take(1)).subscribe(data => {
componentToScale.position.next(data.map((val, index) =>
val - (newScale[index] - oldScale[index]) / 2
))
})
manager.top(componentToScale)
}
else {
const size = [this.width.value, this.height.value]
const mouseFraction = size.map((value, index) => this.mousePosition[index] / value)
const newScale = this.scale.map(value => clamp(value * zoom, ...this.zoomLimits))
const delta = this.scale.map((_, 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]]
}
}

View file

@ -1 +0,0 @@
export * from "./store"

View file

@ -1,21 +0,0 @@
import { Store } from "./store";
export const persistent = <T,K>(_default:K,storeKey = "main") => (target:T, key: keyof T & string) => {
let secret: K
const store = new Store<K>(key)
if (store.get(storeKey))
secret = store.get(storeKey)
else
secret = _default
Object.defineProperty(target,key,{
get() {
return secret
},
set(value:K) {
secret = value
store.set(storeKey, secret)
},
enumerable: true
})
}

View file

@ -1,33 +0,0 @@
import { Singleton } from "@eix/utils";
import { ComponentManager } from "../componentManager";
import { success, error } from "toastr"
@Singleton
export class Settings {
version = "1.0.0"
settings = {
jumpToNewSimulations: true
}
commands = (ctx: ComponentManager, args: string[], flags: string[]) => {
//important flags
for (let i of flags) {
if (i === "--version" || i === "-v")
return success(`${this.version}`, "", ctx.alertOptions)
}
const command = args[0]
if (command === undefined)
return success(
`Welcome to the settings cli. You can use this to tweak settings in any way imaginable!`,
"",
ctx.alertOptions)
//nothing here
error(`Commands ${args} couldnt be found`,"",ctx.alertOptions)
}
constructor() { }
}

View file

@ -1,45 +0,0 @@
import { BehaviorSubject } from "rxjs";
export class Store<T> {
public lsChanges = new BehaviorSubject<string[]>([])
constructor(private name: string){
this.update()
}
update(){
this.lsChanges.next(this.ls())
}
get(key:string):T{
const data = localStorage[`${this.name}/${key}`]
if(data)
return JSON.parse(data).value
return null
}
delete(key:string){
localStorage.removeItem(`${this.name}/${key}`)
this.update()
}
set(key:string,value:T){
localStorage[`${this.name}/${key}`] = JSON.stringify({ value })
this.update()
return this
}
ls() {
let keys = []
for (const i in localStorage){
if (i.indexOf(this.name) == 0)
keys.push(i.substr(this.name.length + 1))
}
return keys
}
}

View file

@ -1 +0,0 @@
export * from "./wireManager"

View file

@ -1,12 +0,0 @@
export interface WireStateVal {
from: {
owner: number
index: number
},
to: {
owner: number
index: number
}
}
export type WireState = WireStateVal[]

View file

@ -1,18 +0,0 @@
import { Pin } from "../pin";
export class Wire {
constructor (public input:Pin,public output:Pin){
this.output.bindTo(this.input)
this.input.pairs.push(this.output)
this.input.update()
this.output.update()
}
public dispose(){
this.output.setValue(0)
this.output.unbind(this.input)
this.input.unbind(this.output)
this.input.update()
this.output.update()
}
}

View file

@ -1,115 +0,0 @@
import { Singleton } from '@eix/utils'
import { Pin } from '../pin'
import { Wire } from './wire'
import { svg } from 'lit-html'
import { subscribe } from 'lit-rx'
import { Subject, combineLatest } from 'rxjs'
import { WireStateVal } from './interface'
import { merge, map } from 'rxjs/operators'
@Singleton
export class WireManager {
public start: Pin
public end: Pin
public wires: Wire[] = []
public update = new Subject<boolean>()
constructor() {}
public add(data: Pin) {
if (data.allowWrite)
//output
this.start = data
else this.end = data
this.tryResolving()
}
public dispose() {
for (let i of this.wires) i.dispose()
this.wires = []
}
public tryResolving() {
if (this.start && this.end && this.start != this.end) {
if (this.canBind(this.end)) {
this.wires.push(new Wire(this.start, this.end))
this.start = null
this.end = null
this.update.next(true)
}
}
}
private canBind(end: Pin) {
if (this.wires.find(val => val.output === end)) return false
return true
}
public remove(target: Wire) {
target.dispose()
this.wires = this.wires.filter(val => val !== target)
this.update.next(true)
}
get svg() {
return svg`${this.wires.map(val => {
const i = val.input.of
const o = val.output.of
const inputIndex = i.outputPins.indexOf(val.input)
const inputY = i.piny(false, inputIndex)
const outputY = o.piny(true, o.inputPins.indexOf(val.output))
const output = [o.pinx(true, 20), outputY]
const input = [i.pinx(false, 20), inputY]
const midX = combineLatest(output[0], input[0]).pipe(
map(values => {
return (values[0] + values[1]) / 2
})
)
const mid1 = [midX, outputY]
const mid2 = [midX, inputY]
const d = combineLatest<number[]>(
...output,
...mid1,
...mid2,
...input
).pipe(
map(
points =>
`M ${points.slice(0, 2).join(' ')} C ${points
.slice(2)
.join(' ')}`
)
)
return svg`
<path d=${subscribe(d)}
stroke=${subscribe(val.input.svgColor)}
stroke-width=10
fill="rgba(0,0,0,0)"
@click=${() => this.remove(val)} />
`
})}`
}
get state() {
return this.wires.map(
(val): WireStateVal => ({
from: {
owner: val.input.of.id,
index: val.input.of.outputPins.indexOf(val.input)
},
to: {
owner: val.output.of.id,
index: val.output.of.inputPins.indexOf(val.output)
}
})
)
}
}

View file

@ -1,308 +0,0 @@
import { render, html } from 'lit-html'
import { subscribe } from 'lit-rx'
import { Screen } from './common/screen.ts'
import { ComponentManager } from './common/componentManager'
import { map } from 'rxjs/operators'
import { MDCMenu } from '@material/menu'
import { error } from 'toastr'
import { modal } from './common/modals'
import { importComponent } from './common/componentImporter/importComponent'
const screen = new Screen()
export const manager = new ComponentManager()
manager.save()
manager.update()
window.onerror = (
message: string,
url: string,
lineNumber: number
): boolean => {
error(message, '', {
...manager.alertOptions,
onclick: () =>
modal({
no: '',
yes: 'close',
title: 'Error',
content: html`
<table>
<tr>
<td>Url:</td>
<td>${url}</td>
</tr>
<tr>
<td>Message:</td>
<td>${message}</td>
</tr>
<tr>
<td>Line:</td>
<td>${lineNumber}</td>
</tr>
</table>
`
})
})
return true
}
const handleEvent = <T>(e: T, func: (e: T) => any) => {
if (manager.barAlpha.value == '0') func(e)
else if (
manager.barAlpha.value == '1' &&
((e as unknown) as MouseEvent).type == 'mousedown' &&
((e as unknown) as MouseEvent).target !=
document.getElementById('nameInput')
)
manager.barAlpha.next('0')
}
const moveHandler = (e: MouseEvent) =>
handleEvent(e, (e: MouseEvent) => {
manager.handleMouseMove(e)
screen.updateMouse(e)
})
render(
html`
<div @mousemove=${moveHandler}
@touchmove=${moveHandler}
@mousedown=${(e: MouseEvent) =>
handleEvent(e, () => manager.handleMouseDown())}
@touchdown=${(e: MouseEvent) =>
handleEvent(e, () => manager.handleMouseDown())}
@mouseup=${(e: MouseEvent) =>
handleEvent(e, () => manager.handleMouseUp())}
@touchup=${(e: MouseEvent) =>
handleEvent(e, () => manager.handleMouseUp())}
@wheel=${(e: MouseEvent) =>
handleEvent(e, (e: WheelEvent) => screen.handleScroll(e))}>
<div id=${subscribe(
manager.barAlpha.pipe(map(val => (val == '1' ? 'shown' : '')))
)}
class=createBar>
<div class="topContainer">
<div>
<input name="ComponentName" id="nameInput"
placeholder=${subscribe(manager.placeholder)}
></input>
</div>
</div>
</div>
<svg height=${subscribe(screen.height)}
width=${subscribe(screen.width)}
viewBox=${subscribe(screen.viewBox)}>
${subscribe(manager.svgs)}
</svg>
</div>
<div class="ModalContainer"></div>
<aside class="mdc-drawer main-sidebar">
<div class="mdc-drawer__content">
<nav class="mdc-list">
<a class="mdc-list-item mdc-list-item--activated" href="#" aria-current="page" @click=${() =>
manager.prepareNewSimulation()}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">note_add</i>
<span class="mdc-list-item__text">Create new simulation</span>
</a>
<a class="mdc-list-item" href="#" id="openSimulation" @click=${() => {
menus[0].open = true
}}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">folder_open</i>
<span class="mdc-list-item__text">Open simulation</span>
</a>
<a class="mdc-list-item" href="#" id="openFile" @click=${() => {
menus[2].open = true
}}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">insert_drive_file</i>
<span class="mdc-list-item__text">Simulation</span>
</a>
<a class="mdc-list-item" href="#" id="openCustomGates" @click=${() => {
menus[3].open = true
}}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">edit</i>
<span class="mdc-list-item__text">Custom gates</span>
</a>
<a class="mdc-list-item" href="#" id="openGates" @click=${() => {
menus[1].open = true
}}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i>
<span class="mdc-list-item__text">Add component</span>
</a>
</nav>
</div>
</aside>
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="saveMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${subscribe(
manager.saves.pipe(
map(_ =>
_.map(
val => html`
<li
class="mdc-list-item"
role="menuitem"
@click=${() => manager.switchTo(val)}
>
<span class="mdc-list-item__text">
${val}
</span>
<span
class="material-icons mdc-list-item__meta"
@click=${() => manager.delete(val)}
>
delete
</span>
</li>
`
)
)
)
)}
</ul>
</div>
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="gateMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${subscribe(
manager.gates.pipe(
map(gates =>
[...gates].sort().map(name => {
const gate = manager.templateStore.store.get(name)
return html`
<li
class="mdc-list-item"
role="menuitem"
@click=${() => manager.add(name)}
>
<span class="mdc-list-item__text">
${name}
</span>
${gate.imported || gate.editable
? html`
&nbsp &nbsp &nbsp
<span
class="material-icons mdc-list-item__meta"
@click=${(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
manager.templateStore.store.delete(
name
)
}}
>
delete
</span>
`
: ''}
</li>
`
})
)
)
)}
</ul>
</div>
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="fileMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${[...Object.keys(manager.file)].sort().map(
key => html`
<li
class="mdc-list-item"
role="menuitem"
@click=${() => manager.file[key]()}
>
<span class="mdc-list-item__text">${key}</span>
${manager.shortcuts[key]
? html`
<span class="mdc-list-item__meta"
>&nbsp &nbsp &nbsp
${manager.shortcuts[key]}</span
>
`
: ''}
</li>
`
)}
</ul>
</div>
<div class="mdc-menu mdc-menu-surface mdc-theme--primary-bg mdc-theme--on-primary" id="customGateMenu">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
${subscribe(
manager.gates.pipe(
map(gates =>
gates
.map(name => manager.templateStore.store.get(name))
.filter(gate => gate.editable || gate.imported)
.map(
gate => html`
<li
class="mdc-list-item"
role="menuitem"
@click=${(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (gate.editable) {
manager.edit(gate.name)
} else {
importComponent(
manager,
gate.importCommand
)
}
}}
>
<i
class="material-icons mdc-list-item__graphic"
aria-hidden="true"
>${gate.imported
? 'refresh'
: 'edit'}</i
>
<span class="mdc-list-item__text">
${gate.name}
</span>
</li>
`
)
)
)
)}
<li class= "mdc-list-item" role = "menuitem" @click=${() =>
manager.newGate()}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i>
<span class="mdc-list-item__text"> New custom gate </span>
</li>
<li class= "mdc-list-item" role = "menuitem" @click=${() =>
manager.importGate()}>
<i class="material-icons mdc-list-item__graphic" aria-hidden="true">add</i>
<span class="mdc-list-item__text"> Import new gate </span>
<span class="mdc-list-item__meta">&nbsp &nbsp &nbsp ctrl + g</span>
</li>
</ul>
</div>
`,
document.body
)
const menus = [
new MDCMenu(document.querySelector('#saveMenu')),
new MDCMenu(document.querySelector('#gateMenu')),
new MDCMenu(document.querySelector('#fileMenu')),
new MDCMenu(document.querySelector('#customGateMenu'))
]
menus.forEach(menu => menu.hoistMenuToBody())
menus[0].setAnchorElement(document.querySelector(`#openSimulation`))
menus[1].setAnchorElement(document.querySelector('#openGates'))
menus[2].setAnchorElement(document.querySelector('#openFile'))
menus[3].setAnchorElement(document.querySelector('#openCustomGates'))
manager.update()

View file

@ -1,32 +1,13 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"types/*"
]
},
"module": "esnext",
"target": "esnext",
"removeComments": true,
"sourceMap": true,
"lib": [
"es2015",
"es2017",
"dom"
],
"moduleResolution": "node",
"esModuleInterop": true,
"jsx": "preserve",
"noImplicitAny": true,
"alwaysStrict": true,
"moduleResolution": "Node",
"experimentalDecorators": true,
"esModuleInterop": true
"target": "esnext",
"strictNullChecks": true
},
"include": [
"src/**/*.ts",
"deploy.ts"
],
"exclude": [
"node_modules",
"dist"
]
}
"exclude": ["node_modules"],
"include": ["src"]
}

View file

@ -1,9 +0,0 @@
export function haiku(options: any): any;
export function random(options: {
count: number
}): {
character: string,
name: string,
image: string,
imageSrc: string
}[];

View file

@ -1,82 +1,114 @@
const HtmlWebPackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const webpackMerge = require('webpack-merge')
module.exports = {
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.html$/,
use: [
{
loader: "html-loader"
}
]
},
{
test: /\.(png|jpg|mp3|wav)$/,
use: [
{
loader: 'file-loader',
options: {}
}
]
},
{
test: /\.scss$/,
use: [
{
loader: 'file-loader',
options: {
name: 'style.css',
},
},
{ loader: 'extract-loader' },
{ loader: 'css-loader' },
{
loader: 'sass-loader',
options: {
includePaths: ['./node_modules']
}
},
]
},
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
use: {
loader: 'file-loader?name=./res/fonts/[name].[ext]'
}
const isProduction = process.env.NODE_ENV === 'production'
const projectRoot = resolve(__dirname)
const sourceFolder = resolve(projectRoot, 'src')
const buildFolder = resolve(projectRoot, 'dist')
const htmlTemplateFile = resolve(sourceFolder, 'index.html')
const babelRule = {
test: /\.(js|tsx?)$/,
use: 'babel-loader'
}
const sassRule = {
test: /\.scss$/,
use: [
isProduction
? MiniCssExtractPlugin.loader
: {
loader: 'style-loader',
options: {
singleton: true
}
},
{ loader: 'css-loader' },
{
loader: 'sass-loader',
options: {
includePaths: [sourceFolder]
}
]
}
]
}
const baseConfig = {
mode: 'none',
entry: ['babel-regenerator-runtime', resolve(sourceFolder, 'main')],
output: {
filename: 'js/[name].js',
path: buildFolder,
publicPath: '/'
},
module: {
rules: [babelRule, sassRule]
},
resolve: {
extensions: ['.js', '.ts', '.tsx', '.scss']
},
plugins: []
}
const devConfig = {
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
template: htmlTemplateFile,
chunksSortMode: 'dependency'
})
],
devtool: 'inline-source-map',
devServer: {
historyApiFallback: true
}
}
const prodConfig = {
mode: 'production',
optimization: {
minimize: true,
nodeEnv: 'production'
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./index.html"
new MiniCssExtractPlugin({
filename: 'css/[name].min.css'
}),
new ExtractTextPlugin(
{
filename: 'style.css',
allChunks: true
}
)
new OptimizeCssAssetsWebpackPlugin(),
new HtmlWebpackPlugin({
template: htmlTemplateFile,
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
},
inject: true
}),
new HtmlWebpackInlineSourcePlugin()
],
resolve: {
extensions: [
".js",
".ts"
]
},
entry: [
"./src/index.ts"
],
optimization: {
minimizer: [new TerserPlugin()],
devtool: 'source-map'
}
function getFinalConfig() {
if (process.env.NODE_ENV === 'production') {
console.info('Running production config')
return webpackMerge(baseConfig, prodConfig)
}
};
console.info('Running development config')
return webpackMerge(baseConfig, devConfig)
}
module.exports = getFinalConfig()