BrowserLogic/js/logicengine.js
2021-02-19 13:47:37 -08:00

636 lines
18 KiB
JavaScript

/*
First a few needed global functions
*/
function getMousePos(canvas, evt) {
let rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
function averageArray(arr) {
return arr.reduce((a, b) => (a + b)) / arr.length;
}
/*
Now we can get into the classes
*/
class CanvasTools {
constructor() {
}
textSize(ctx,text,fontStyle) {
ctx.save();
ctx.font = fontStyle;
let tHeight = Math.round(ctx.measureText(text).actualBoundingBoxAscent + ctx.measureText(text).actualBoundingBoxDescent);
let tWidth = Math.round(ctx.measureText(text).width);
ctx.restore();
return {
width: tWidth,
height: tHeight
};
}
drawBorderBox(ctx,x,y,drawWidth,drawHeight,borderWidth=1,borderColor="#000",fillColor="#f7e979") {
let old_fillStyle = ctx.fillStyle;
ctx.fillStyle = borderColor;
ctx.fillRect(x,y,drawWidth,drawHeight);
ctx.fillStyle = fillColor;
ctx.fillRect(x+borderWidth,y+borderWidth,drawWidth-(borderWidth*2),drawHeight-(borderWidth*2));
ctx.fillStyle = old_fillStyle;
}
drawTextCentered(ctx,x,y,x2,y2,text,fontStyle="24px Console",fontColor = "#555") {
let old_fillStyle = ctx.fillStyle;
let old_font = ctx.font;
ctx.font = fontStyle;
ctx.fillStyle = fontColor;
let tHeight = ctx.measureText(text).actualBoundingBoxAscent + ctx.measureText(text).actualBoundingBoxDescent;
let tX = x+((x2/2)-(ctx.measureText(text).width/2));
let tY = y+tHeight+((y2/2)-(tHeight/2));
ctx.fillText(text,tX,tY);
ctx.fillStyle = old_fillStyle;
ctx.font = old_font;
}
drawText(ctx,x,y,text,fontStyle="24px Console",fontColor = "#555") {
let old_fillStyle = ctx.fillStyle;
let old_font = ctx.font;
ctx.font = fontStyle;
ctx.fillStyle = fontColor;
ctx.fillText(text,x,y);
ctx.fillStyle = old_fillStyle;
ctx.font = old_font;
}
}
class ElementConnection {
constructor(elementContainer,element,input) {
this.Container = elementContainer;
this.Element = element;
this.Input = input;
}
}
class Element extends CanvasTools {
constructor(Inputs) {
super();
this.Name = "Element";
this.Designator = "";
this.Inputs = new Array(Inputs);
this.totalInputs = Inputs;
this.Width = 150;
this.Height = 100;
this.inputCircleRadius = 10;
this.outputCircleRadius = 10;
this.X = 0;
this.Y = 0;
this.OutputConnections = new Array();
}
Delete() {
// Just to clean up connections
for (let a = 0; a < this.OutputConnections.length;a++) {
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input,false);
}
}
MouseClick() {
// Function for if mouse is clicked
}
mouseInside(mousePos) {
if (((mousePos.x >= this.X ) && (mousePos.x <= (this.X + this.Width))) & ((mousePos.y >= this.Y ) && (mousePos.y <= (this.Y + this.Height)))) return true;
return false;
}
addConnection(container, element, input) {
let newConnection = new ElementConnection(container,element,input);
this.OutputConnections.push(newConnection);
element.setInput(input,this.getOutput());
}
drawInputs(ctx,x,y,borderColor = "#000",circleColorFalse = "#ff0000",circleColorTrue="#00ff00") {
let old_strokeStyle = ctx.strokeStyle;
let old_fillStyle = ctx.fillStyle;
if ((this.totalInputs * ((this.inputCircleRadius*2)+4)) > (this.Height-2)) {
this.inputCircleRadius = ((this.Height-(2 + this.totalInputs * 4)) / this.totalInputs)/2;
}
for (let a = 0; a < this.totalInputs;a++) {
ctx.beginPath();
ctx.arc(x+20,y+(this.inputCircleRadius + 2)+(((a*(4+(this.inputCircleRadius*2))))-2)+(this.inputCircleRadius/2),this.inputCircleRadius,0,2*Math.PI);
ctx.strokeStyle = borderColor;
ctx.fillStyle = circleColorFalse;
if (this.Inputs[a]) ctx.fillStyle = circleColorTrue;
ctx.fill();
ctx.stroke();
}
ctx.strokeStyle = old_strokeStyle;
ctx.fillStyle = old_fillStyle;
}
drawOutputs(ctx,x,y,borderColor = "#000",circleColorFalse = "#ff0000",circleColorTrue="#00ff00") {
let old_strokeStyle = ctx.strokeStyle;
let old_fillStyle = ctx.fillStyle;
ctx.beginPath();
ctx.arc(x+(this.Width-20),y+(this.Height/2),this.outputCircleRadius,0,2*Math.PI);
ctx.strokeStyle = borderColor;
ctx.fillStyle = circleColorFalse;
if (this.getOutput()) ctx.fillStyle = circleColorTrue;
ctx.fill();
ctx.stroke();
ctx.strokeStyle = old_strokeStyle;
ctx.fillStyle = old_fillStyle;
}
drawConnections(ctx) {
ctx.save();
for (let a = 0; a < this.OutputConnections.length;a++) {
if (!this.OutputConnections[a].Container.HasElement(this.OutputConnections[a].Element)) {
// This is a ghosted connection, lets get rid of it
this.OutputConnections.splice(a,1);
a--;
} else {
ctx.beginPath();
ctx.moveTo((this.X + this.Width) - 20, this.Y + (this.Height / 2));
ctx.lineTo(this.OutputConnections[a].Element.X + 20, this.OutputConnections[a].Element.Y + 20);
ctx.strokeStyle = "#555";
ctx.stroke();
}
}
ctx.restore();
}
setInput(Input,Value) {
let oldOutput = this.getOutput();
if (Value) {
Value = true;
} else {
Value = false;
}
if (Input < this.totalInputs) {
this.Inputs[Input] = Value;
}
if (this.getOutput() != oldOutput) {
// The output changed, we need to notify connected elements
for (let a = 0; a < this.OutputConnections.length;a++) {
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input,this.getOutput());
}
}
}
getOutput() {
/*
Should return true or false
*/
return false;
}
drawElement(x,y,ctx) {
/*
Draw routine for the element
*/
this.drawBorderBox(ctx,x,y,drawWidth,drawHeight);
this.drawTextCentered(ctx,x,y,this.Width,this.Height,"LOGIC");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class inputElement extends Element {
constructor() {
super(0);
this.Output = false;
this.Width = 100;
this.Name = "InputElement";
}
getOutput() {
return this.Output;
}
drawElement(x, y, ctx) {
this.drawBorderBox(ctx, x,y,this.Width,this.Height);
this.drawTextCentered(ctx,x,y,this.Width,this.Height,"IN");
this.drawOutputs(ctx,x,y);
}
}
class inputButton extends inputElement {
constructor() {
super();
this.Name = "Button";
}
MouseClick() {
this.Output = ~this.Output;
for (let a = 0; a < this.OutputConnections.length;a++) {
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input,this.getOutput());
}
}
drawElement(x, y, ctx) {
this.drawBorderBox(ctx, x,y,this.Width,this.Height);
this.drawBorderBox(ctx,x+10,y+25,50,50,1,"#ccc","#777");
this.drawOutputs(ctx,x,y);
}
}
class LogicAND extends Element {
constructor(Inputs) {
super(Inputs);
this.Name = "AND";
}
getOutput() {
let ANDResult = true;
for (let a = 0; a < this.totalInputs;a++) {
if (!this.Inputs[a]) ANDResult = false;
}
return ANDResult;
}
drawElement(x,y,ctx) {
this.drawBorderBox(ctx, x,y,this.Width,this.Height);
this.drawTextCentered(ctx,x,y,this.Width,this.Height,"|⊃");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicNAND extends LogicAND {
constructor(Inputs) {
super(Inputs);
this.Name = "NAND";
}
getOutput() {
if (super.getOutput()) {
return false;
} else {
return true;
}
}
drawElement(x,y,ctx) {
this.drawBorderBox(ctx, x,y,this.Width,this.Height);
this.drawTextCentered(ctx,x,y,this.Width,this.Height,"|⊃🞄");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicOR extends Element {
constructor(Inputs) {
super(Inputs);
this.Name = "OR";
}
getOutput() {
let ORResult = false;
for (let a = 0; a < this.totalInputs;a++) {
if (this.Inputs[a]) ORResult = true;
}
return ORResult;
}
drawElement(x,y,ctx) {
let drawWidth = this.Width;
let drawHeight = this.Height;
this.drawBorderBox(ctx, x,y,drawWidth,drawHeight);
this.drawTextCentered(ctx,x,y,drawWidth,drawHeight,")⊃");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicNOR extends LogicOR {
constructor(Inputs) {
super(Inputs);
this.Name = "NOR";
}
getOutput() {
if (super.getOutput()) {
return false;
} else {
return true;
}
}
drawElement(x,y,ctx) {
this.drawBorderBox(ctx, x,y,this.Width,this.Height);
this.drawTextCentered(ctx,x,y,this.Width,this.Height,")⊃🞄");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicXOR extends Element {
constructor() {
super(2); // Only 2 inputs on XOR
this.Name = "XOR";
}
getOutput() {
let ORResult = false;
if ( (this.Inputs[0] && !this.Inputs[1]) || (!this.Inputs[0] && this.Inputs[1]) ) ORResult = true;
return ORResult;
}
drawElement(x,y,ctx) {
let drawWidth = this.Width;
let drawHeight = this.Height;
this.drawBorderBox(ctx, x,y,drawWidth,drawHeight);
this.drawTextCentered(ctx,x,y,drawWidth,drawHeight,"))⊃");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicXNOR extends Element {
constructor() {
super(2); // Only 2 inputs on XOR
this.Name = "XNOR";
}
getOutput() {
let ORResult = false;
if ( (this.Inputs[0] && !this.Inputs[1]) || (!this.Inputs[0] && this.Inputs[1]) ) ORResult = true;
return !ORResult;
}
drawElement(x,y,ctx) {
let drawWidth = this.Width;
let drawHeight = this.Height;
this.drawBorderBox(ctx, x,y,drawWidth,drawHeight);
this.drawTextCentered(ctx,x,y,drawWidth,drawHeight,"))⊃🞄");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class elementContainer {
constructor() {
this.Elements = new Array();
this.Selected = false;
}
AddElement(element) {
let designatorNumber = 1;
let designatorTest = element.Name + designatorNumber;
let unused = false;
while (!unused) {
let foundMatch = false;
for (let a=0;a < this.Elements.length;a++) {
if (this.Elements[a].Designator == designatorTest) foundMatch = true;
}
if (foundMatch) {
designatorNumber++;
designatorTest = element.Name + designatorNumber;
} else {
unused = true;
element.Designator = designatorTest;
this.Elements.push(element);
}
}
}
DeleteElement(element) {
// Can pass object or Designator
for (let a = 0; a < this.Elements.length; a++) {
if ((this.Elements[a] == element) || (this.Elements[a].Designator == element)) {
this.Elements[a].Delete();
this.Elements.splice(a,1);
return true;
}
}
return false;
}
HasElement(element) {
// Can pass object or Designator
for (let a = 0; a < this.Elements.length; a++) {
if ((this.Elements[a] == element) || (this.Elements[a].Designator == element)) {
return true;
}
}
return false;
}
DrawAll(ctx) {
for (let a = 0;a < this.Elements.length;a++) {
// Not ideal to loop twice but we need the connections drawn first
this.Elements[a].drawConnections(ctx);
}
for (let a = 0;a < this.Elements.length;a++) {
if (this.Elements[a] == this.Selected) this.Elements[a].drawBorderBox(ctx,this.Elements[a].X - 2,this.Elements[a].Y - 2,this.Elements[a].Width+4,this.Elements[a].Height+4,2,"#5555FF","#5555ff");
this.Elements[a].drawElement(this.Elements[a].X,this.Elements[a].Y,ctx);
let old_font = ctx.font;
let old_fillStyle = ctx.fillStyle;
ctx.font = "10px Console";
let x = this.Elements[a].X;
let y = this.Elements[a].Y + (this.Elements[a].Height-12);
let x2 = this.Elements[a].Width;
let y2 = 10;
this.Elements[a].drawTextCentered(ctx,x,y,x2,y2,this.Elements[a].Designator,ctx.font,"#000");
ctx.font = old_font;
ctx.fillStyle = old_fillStyle;
}
}
Select(element) {
this.Selected = element;
}
checkMouseBounds(mousePos) {
// We go backwards so that the newest (highest drawn) element is clicked before one lower.
for (let a = (this.Elements.length - 1); a >= 0; a--) {
if (this.Elements[a].mouseInside(mousePos)) return this.Elements[a];
}
return false;
}
}
class LogicEngine {
Resize(evt) {
this.Canvas.width = window.innerWidth;
this.Canvas.height = window.innerHeight - 50;
this.Mouse = false;
}
Mouse_Down(evt) {
let mousePos = getMousePos(this.Canvas, evt);
this.MouseDown = true;
this.MouseDownTime = performance.now();
let element = this.ActiveContainer.checkMouseBounds(mousePos);
if (element) {
console.log("Moused down on " + element.Designator);
this.MovingElement = element;
this.MovingElementStartX = element.X;
this.MovingElementStartY = element.Y;
this.MovingElementMouseStartX = mousePos.x;
this.MovingElementMouseStartY = mousePos.y;
}
}
Mouse_Up(evt) {
this.MouseDown = false;
if (this.MovingElement && (this.MovingElement.X == this.MovingElementStartX) && (this.MovingElement.Y == this.MovingElementStartY)) {
if ((performance.now() - this.MouseDownTime) < 3000) {
// Presume this was a click
this.ActiveContainer.Select(this.MovingElement);
if (this.ActiveLink) {
this.Link();
} else {
this.MovingElement.MouseClick();
}
}
//console.log("Mouse Up");
}
if (!this.MovingElement) this.ActiveContainer.Selected = false;
this.ActiveLink = false;
this.MovingElement = false;
}
Mouse_Move(evt) {
let mousePos = getMousePos(this.Canvas, evt);
this.Mouse = mousePos;
if(this.MouseDown) {
//console.log('Mouse at position: ' + mousePos.x + ',' + mousePos.y);
if (this.MovingElement) {
if ((performance.now() - this.MouseDownTime) > 100) {
this.ActiveContainer.Select(this.MovingElement);
let xOffset = mousePos.x - this.MovingElementMouseStartX;
let yOffset = mousePos.y - this.MovingElementMouseStartY;
let diffxOffset = this.MovingElementMouseStartX - this.MovingElementStartX;
let diffyOffset = this.MovingElementMouseStartY - this.MovingElementStartY;
this.MovingElement.X = (this.MovingElementMouseStartX + xOffset) - diffxOffset;
this.MovingElement.Y = (this.MovingElementMouseStartY + yOffset) - diffyOffset;
}
}
}
}
Key_Press(evt) {
if (evt.key == "Escape") {
if (this.MovingElement && this.MouseDown) {
this.MovingElement.X = this.MovingElementStartX;
this.MovingElement.Y = this.MovingElementStartY;
this.MovingElement = false;
}
}
if (evt.key == "Delete") {
if (this.ActiveContainer.Selected) {
this.ActiveContainer.DeleteElement(this.ActiveContainer.Selected);
this.ActiveContainer.Selected = false;
}
}
}
constructor(canvas) {
this.Canvas = canvas;
this.Ctx = canvas.getContext("2d");
this.FPSCounter = 0;
this.FPS = 0;
this.PotentialFPS = 0;
this.PotentialFPSAVGs = new Array(20);
this.PotentialFPSAVGLoc = 0;
this.LastFPSCheck = performance.now();
this.MouseDown = false;
this.MouseDownTime = 0;
this.MovingElementContainer = false;
this.MovingElement = false;
this.MovingElementStartX = 0;
this.MovingElementStartY = 0;
this.MovingElementMouseStartX = 0;
this.MovingElementMouseStartY = 0;
this.ActiveContainer = new elementContainer();
this.ActiveLink = false;
this.Canvas.setAttribute('tabindex','0');
}
Link() {
if (this.ActiveLink) {
if (this.ActiveContainer.Selected && (this.ActiveContainer.Selected != this.ActiveLink)) {
let input = parseInt(prompt("Please enter the input number (0 - x)"));
while (input == NaN) {
input = parseInt(prompt("Please enter the input number (0 - x)"));
}
this.ActiveLink.addConnection(this.ActiveContainer,this.ActiveContainer.Selected,input);
this.ActiveLink = false;
} else {
this.ActiveLink = false;
}
} else {
if (this.ActiveContainer.Selected) {
this.ActiveLink = this.ActiveContainer.Selected;
}
}
}
DrawLoop() {
let startLoop = performance.now();
this.Canvas.focus();
this.Ctx.clearRect(0,0,this.Canvas.width,this.Canvas.height);
if (this.ActiveLink) {
this.Ctx.save();
this.Ctx.strokeStyle = "#5555AA";
this.Ctx.lineWidth = 5;
this.Ctx.beginPath();
this.Ctx.moveTo(this.ActiveLink.X + (this.ActiveLink.Width/2), this.ActiveLink.Y + (this.ActiveLink.Height/2));
this.Ctx.lineTo(this.Mouse.x, this.Mouse.y);
this.Ctx.stroke();
this.Ctx.restore();
}
this.ActiveContainer.DrawAll(this.Ctx);
let ct = new CanvasTools();
let FPSOffset = this.Canvas.width - 150;
ct.drawText(this.Ctx,FPSOffset,650,"FPS: " + this.FPS,"12px console", "#005500");
ct.drawText(this.Ctx,FPSOffset,670,"Potential FPS: " + this.PotentialFPS,"12px console", "#005500");
let timeCheck = performance.now();
this.FPSCounter++;
if (!(Math.round(timeCheck - this.LastFPSCheck) % 50)) {
let frameTimeUS = (performance.now() - startLoop) * 1000;
let potentialFPS = 1000000 / frameTimeUS;
this.PotentialFPSAVGs[this.PotentialFPSAVGLoc] = Math.round(potentialFPS);
this.PotentialFPSAVGLoc++;
if (this.PotentialFPSAVGLoc == this.PotentialFPSAVGs.length) this.PotentialFPSAVGLoc = 0;
this.PotentialFPS = averageArray(this.PotentialFPSAVGs);
}
if ((timeCheck - this.LastFPSCheck) >= 1000) {
this.FPS = this.FPSCounter;
this.FPSCounter = 0;
this.LastFPSCheck = performance.now();
//console.log("Frame Time: " + frameTimeUS + "uS" + ", Potential FPS: " + potentialFPS);
//console.log("FPS: " + FPS);
}
requestAnimationFrame(this.DrawLoop.bind(this));
}
StartEngine() {
this.Resize("");
this.DrawLoop();
}
}