before removing smooth shadows
This commit is contained in:
parent
b28eec6342
commit
9eba227ec3
|
@ -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
17
babel.config.js
Normal 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' } }]]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
3818
package-lock.json
generated
File diff suppressed because it is too large
Load diff
80
package.json
80
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
137
src/index.html
137
src/index.html
|
@ -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>
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
import "./ts/main"
|
||||
import "./scss/base.scss"
|
6
src/main.tsx
Normal file
6
src/main.tsx
Normal 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'))
|
28
src/modules/core/classes/Screen.ts
Normal file
28
src/modules/core/classes/Screen.ts
Normal 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
|
||||
}
|
||||
}
|
7
src/modules/core/components/App.scss
Normal file
7
src/modules/core/components/App.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
10
src/modules/core/components/App.tsx
Normal file
10
src/modules/core/components/App.tsx
Normal 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
|
56
src/modules/core/components/Canvas.tsx
Normal file
56
src/modules/core/components/Canvas.tsx
Normal 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
|
53
src/modules/core/components/FluidCanvas.tsx
Normal file
53
src/modules/core/components/FluidCanvas.tsx
Normal 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
|
446
src/modules/core/styles/reset.scss
Normal file
446
src/modules/core/styles/reset.scss
Normal 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;
|
||||
}
|
4
src/modules/core/types/MouseSubject.ts
Normal file
4
src/modules/core/types/MouseSubject.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { Subject } from 'rxjs'
|
||||
import { MouseEventInfo } from '../components/FluidCanvas'
|
||||
|
||||
export type MouseSubject = Subject<MouseEventInfo>
|
13
src/modules/simulation/classes/Camera.ts
Normal file
13
src/modules/simulation/classes/Camera.ts
Normal 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
|
||||
}
|
||||
}
|
10
src/modules/simulation/classes/Gate.ts
Normal file
10
src/modules/simulation/classes/Gate.ts
Normal 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') {}
|
||||
}
|
70
src/modules/simulation/classes/GateStorage.ts
Normal file
70
src/modules/simulation/classes/GateStorage.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
86
src/modules/simulation/classes/MouseManager.ts
Normal file
86
src/modules/simulation/classes/MouseManager.ts
Normal 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
|
||||
}
|
||||
}
|
15
src/modules/simulation/classes/Simulation.ts
Normal file
15
src/modules/simulation/classes/Simulation.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
201
src/modules/simulation/classes/SimulationRenderer.ts
Normal file
201
src/modules/simulation/classes/SimulationRenderer.ts
Normal 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
|
||||
}
|
||||
}
|
50
src/modules/simulation/classes/Transform.ts
Normal file
50
src/modules/simulation/classes/Transform.ts
Normal 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]
|
||||
}
|
||||
}
|
6
src/modules/simulation/helpers/clamp.ts
Normal file
6
src/modules/simulation/helpers/clamp.ts
Normal 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
|
||||
}
|
31
src/modules/simulation/helpers/drawRotatedSquare.ts
Normal file
31
src/modules/simulation/helpers/drawRotatedSquare.ts
Normal 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()
|
||||
}
|
11
src/modules/simulation/helpers/pointInSquare.ts
Normal file
11
src/modules/simulation/helpers/pointInSquare.ts
Normal 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
|
||||
)
|
||||
}
|
13
src/modules/simulation/helpers/renderGate.ts
Normal file
13
src/modules/simulation/helpers/renderGate.ts
Normal 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)
|
||||
}
|
19
src/modules/simulation/helpers/renderGateShadow.ts
Normal file
19
src/modules/simulation/helpers/renderGateShadow.ts
Normal 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)
|
||||
)
|
||||
}
|
1
src/modules/simulation/types/DeepPartial.ts
Normal file
1
src/modules/simulation/types/DeepPartial.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type DeepPartial<T> = { [key in keyof T]?: DeepPartial<T[key]> }
|
24
src/modules/vector2/helpers/basic.ts
Normal file
24
src/modules/vector2/helpers/basic.ts
Normal 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))
|
||||
}
|
8
src/modules/vector2/helpers/smoothStep.ts
Normal file
8
src/modules/vector2/helpers/smoothStep.ts
Normal 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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./clamp"
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./component"
|
|
@ -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'
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export const alertOptions = {
|
||||
positionClass: "toast-bottom-right",
|
||||
toastClass: "toasts"
|
||||
}
|
|
@ -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: <command> <br>
|
||||
Where <command> 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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
`
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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`)
|
||||
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./componentManager"
|
|
@ -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
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./modal"
|
|
@ -1,8 +0,0 @@
|
|||
import { TemplateResult } from "lit-html";
|
||||
|
||||
export interface confirmModalOptions {
|
||||
title: string
|
||||
content: TemplateResult
|
||||
yes: string
|
||||
no: string
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./pin"
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./screen"
|
|
@ -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]]
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./store"
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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() { }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./wireManager"
|
|
@ -1,12 +0,0 @@
|
|||
export interface WireStateVal {
|
||||
from: {
|
||||
owner: number
|
||||
index: number
|
||||
},
|
||||
to: {
|
||||
owner: number
|
||||
index: number
|
||||
}
|
||||
}
|
||||
|
||||
export type WireState = WireStateVal[]
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
308
src/ts/main.ts
308
src/ts/main.ts
|
@ -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`
|
||||
     
|
||||
<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"
|
||||
>     
|
||||
${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">      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()
|
|
@ -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"]
|
||||
}
|
||||
|
|
9
types/random-emoji.d.ts
vendored
9
types/random-emoji.d.ts
vendored
|
@ -1,9 +0,0 @@
|
|||
export function haiku(options: any): any;
|
||||
export function random(options: {
|
||||
count: number
|
||||
}): {
|
||||
character: string,
|
||||
name: string,
|
||||
image: string,
|
||||
imageSrc: string
|
||||
}[];
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue