BrowserLogic/js/logicengine.js

1233 lines
39 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;
}
function length2D(x1,y1,x2,y2) {
let xDist = x1 - x2;
let yDist = y1 - y2;
return Math.sqrt(xDist*xDist + yDist * yDist);
}
/*
Now we can get into the classes
*/
class LogicEngineSettings {
constructor() {
this.ActiveConnectionColor = "#aabbaa";
this.InactiveConnectionColor = "#bbaaaa";
this.LinkWidth = "2";
this.LinkDash = [];
this.LinkingConnectionColor = "#aabbbb";
this.LinkingWidth = "3";
this.LinkingDash = [2,2];
this.ShadowColor = "#222";
}
}
class Task {
constructor(taskname,taskdescription,tasktype,tasktime,callback,deleteonrun = false) {
// tasktype: 0: interval, 1: fixed time
this.Name = taskname;
this.Description = taskdescription;
this.Type = tasktype;
this.Enabled = true;
this.Time = tasktime;
this.LastCall = Date.now();
this.CallCount = 0;
this.DeleteOnRun = false;
if (deleteonrun) this.DeleteOnRun = true;
this.Callback = callback;
if (!(tasktype >= 0 && tasktype <= 1)) this.Type = 0;
}
CheckTime() {
let time = this.Time;
if (this.Type == 0) time = this.LastCall + this.Time;
if (this.Enabled && (Date.now() >= time)) {
this.LastCall = Date.now();
this.CallCount++;
if (this.Type == 1 || this.DeleteOnRun == true) this.Enabled = false;
this.Callback();
return true;
}
return false;
}
}
class ScheduleEngine {
constructor() {
this.Tasks = new Array();
}
addTask(task) {
this.Tasks.push(task);
}
deleteTask(task) {
for (let a = 0; a < this.Tasks.length; a++) {
if (this.Tasks[a] == task) {
this.Tasks.splice(a,1);
return true;
}
}
return false;
}
Tick() {
for (let a = 0; a < this.Tasks.length; a++) {
this.Tasks[a].CheckTime();
if (!this.Tasks[a].Enabled && this.Tasks[a].DeleteOnRun) {
this.Tasks.splice(a,1);
a--;
}
}
}
}
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",shadowColor = "transparent") {
ctx.save();
ctx.beginPath();
ctx.fillStyle = borderColor;
if (shadowColor != "transparent") {
ctx.shadowBlur = "6";
ctx.shadowColor = shadowColor;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
}
ctx.fillRect(x,y,drawWidth,drawHeight);
ctx.fillStyle = fillColor;
ctx.fillRect(x+borderWidth,y+borderWidth,drawWidth-(borderWidth*2),drawHeight-(borderWidth*2));
ctx.restore();
}
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 ElementProperty {
constructor(name,type,callback,defaultValue,currentValue = false,values=false,min=0,max=12) {
/*
Types
---------------------------------------
bool Boolean Values
int Integer Value
string String Value
list Dropdown box of values
Callback is an object of:
---------------------------------------
CBObject Object to call function on
CBFunction The function
*/
this.Name = name;
this.Type = type;
this.Callback = callback;
this.DefaultValue = defaultValue;
if (!currentValue) currentValue = defaultValue;
this.CurrentValue = currentValue;
this.Values = values;
if (!values) this.Values = new Array();
this.Minimium = min;
this.Maximium = max;
}
Call(value) {
this.Callback.CBObject[this.Callback.CBFunction](value);
}
}
class ElementConnection {
constructor(elementContainer,element,input) {
this.Container = elementContainer;
this.Element = element;
this.Input = input;
}
}
class Element extends CanvasTools {
constructor(logicengine,Inputs) {
super();
this.Name = "Element";
this.Designator = "";
this.Inputs = new Array(Inputs);
this.Width = 100;
this.Height = 60;
this.inputCircleRadius = 10;
this.outputCircleRadius = 10;
this.X = 0;
this.Y = 0;
this.OutputConnections = new Array();
this.MouseOver = false;
this.MousePosition = {x: 0, y: 0};
this.Properties = new Array();
this.LogicEngine = logicengine;
let inputProperty = new ElementProperty("Inputs","int",{CBObject: this,CBFunction: "ChangeInputs"},2,Inputs,false,2);
this.Properties.push(inputProperty);
}
getProperty(property) {
for (let a = 0; a < this.Properties.length;a++) {
if (this.Properties[a].Name == property) return this.Properties[a];
}
return false;
}
removeProperty(property) {
for (let a = 0; a < this.Properties.length;a++) {
if (this.Properties[a].Name == property) {
this.Properties.splice(a,1);
return true;
}
}
return false;
}
totalInputs() {
return this.Inputs.length;
}
ChangeInputs(inputs) {
inputs = parseInt(inputs,10);
this.Inputs = new Array(inputs);
this.getProperty("Inputs").CurrentValue = inputs;
this.Height = inputs*25;
if (this.Height < 60) this.Height = 60;
}
Delete() {
// Just to clean up connections
for (let a = 0; a < this.OutputConnections.length;a++) {
this.LogicEngine.RecursionCount = 0;
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input,false);
}
}
MouseClick(mousePos) {
let mouseDistOutput = length2D(this.X+(this.Width-10),
this.Y+(this.Height/2),
this.MousePosition.x,
this.MousePosition.y);
if (this.LogicEngine.ActiveLink) {
// We need to see if an input is being clicked on to be linked to
let foundInput = false;
for (let a = 0; a < this.Inputs.length;a++) {
let centerY = this.Y + Math.round(this.Height / 2);
let totalHeight = this.totalInputs() * ((this.inputCircleRadius*2)+4);
let firstY = (centerY - (totalHeight/2)) + 12;
let mouseDist = length2D(this.X+10,
firstY+ (a*24),
this.MousePosition.x,
this.MousePosition.y);
if (mouseDist <= (this.inputCircleRadius)) {
this.LogicEngine.Link(a);
foundInput = true;
break;
}
}
} else {
if (mouseDistOutput <= (this.outputCircleRadius)) {
// Clicked on output, let us start a link
this.LogicEngine.Link();
}
}
}
mouseInside(mousePos) {
this.MouseOver = false;
if (((mousePos.x >= this.X ) && (mousePos.x <= (this.X + this.Width))) & ((mousePos.y >= this.Y ) && (mousePos.y <= (this.Y + this.Height)))) this.MouseOver = true;
this.MousePosition = mousePos;
return this.MouseOver;
}
addConnection(container, element, input) {
let newConnection = new ElementConnection(container,element,input);
this.OutputConnections.push(newConnection);
this.LogicEngine.RecursionCount = 0;
element.setInput(input,this.getOutput());
}
drawInputs(ctx,x,y,borderColor = "#000",circleColorFalse = "#ff0000",circleColorTrue="#00ff00",circleColorHover = "#00ffff") {
ctx.save();
//this.inputCircleRadius = 10;
let centerY = y + Math.round(this.Height / 2);
let totalHeight = this.totalInputs() * ((this.inputCircleRadius*2)+4);
let firstY = (centerY - (totalHeight/2)) + 12;
for (let a = 0; a < this.totalInputs();a++) {
let mouseDist = length2D(x+10, firstY + (a*24),this.MousePosition.x,this.MousePosition.y);
ctx.beginPath();
ctx.arc(x+10,firstY + (a*24),this.inputCircleRadius,0,2*Math.PI);
ctx.strokeStyle = borderColor;
ctx.fillStyle = circleColorFalse;
if (this.Inputs[a]) ctx.fillStyle = circleColorTrue;
if ((mouseDist <= (this.inputCircleRadius)) && this.LogicEngine.ActiveLink) ctx.fillStyle = circleColorHover;
ctx.fill();
ctx.stroke();
}
ctx.restore();
}
drawOutputs(ctx,x,y,borderColor = "#000",circleColorFalse = "#ff0000",circleColorTrue="#00ff00",circleColorHover="#00ffff") {
let old_strokeStyle = ctx.strokeStyle;
let old_fillStyle = ctx.fillStyle;
let mouseDist = length2D(x+(this.Width-10),y+(this.Height/2),this.MousePosition.x,this.MousePosition.y);
ctx.beginPath();
ctx.arc(x+(this.Width-10),y+(this.Height/2),this.outputCircleRadius,0,2*Math.PI);
ctx.strokeStyle = borderColor;
ctx.fillStyle = circleColorFalse;
if (this.getOutput()) ctx.fillStyle = circleColorTrue;
if ((mouseDist <= (this.outputCircleRadius)) && !this.LogicEngine.ActiveLink) ctx.fillStyle = circleColorHover;
ctx.fill();
ctx.stroke();
ctx.strokeStyle = old_strokeStyle;
ctx.fillStyle = old_fillStyle;
}
drawConnections(ctx,settings) {
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 {
let endCenterY = this.OutputConnections[a].Element.Y + Math.round(this.OutputConnections[a].Element.Height / 2);
let endTotalHeight = this.OutputConnections[a].Element.totalInputs() * ((this.OutputConnections[a].Element.inputCircleRadius*2)+4);
let endFirstY = (endCenterY - (endTotalHeight/2)) + 12;
let startX = this.X + this.Width;
let startY = this.Y+(this.Height/2);
let endX = this.OutputConnections[a].Element.X;
//let endY = this.OutputConnections[a].Element.Y+(this.OutputConnections[a].Element.inputCircleRadius + 2)+(((this.OutputConnections[a].Input*(4+(this.OutputConnections[a].Element.inputCircleRadius*2))))-2)+(this.OutputConnections[a].Element.inputCircleRadius/2);
let endY = endFirstY + (this.OutputConnections[a].Input*24);
let startMidX = startX + ((endX - startX)/2);
let startMidY = startY;
let midX = startMidX;
let midY = startY + ((endY - startY)/2);
let endMidX = startMidX;
let endMidY = endY;
ctx.beginPath();
ctx.lineWidth = settings.LinkWidth;
ctx.setLineDash(settings.LinkDash);
ctx.moveTo(startX, startY);
//ctx.lineTo(endX, endY);
ctx.quadraticCurveTo(startMidX,startMidY,midX,midY);
ctx.quadraticCurveTo(endMidX,endMidY,endX,endY);
ctx.strokeStyle = settings.ActiveConnectionColor;
if (!this.getOutput()) ctx.strokeStyle = settings.InactiveConnectionColor;
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;
} else {
return;
}
if (this.getOutput() != oldOutput) {
// The output changed, we need to notify connected elements
for (let a = 0; a < this.OutputConnections.length;a++) {
//console.log(this.Designator + " sending " + this.getOutput() + " to " + this.OutputConnections[a].Element.Designator + " I" + this.OutputConnections[a].Input);
this.LogicEngine.RecursionCount++;
//console.log("Recursion: " + this.LogicEngine.RecursionCount);
if (this.LogicEngine.RecursionCount > 1000) {
if (!this.LogicEngine.RecursionError) {
console.log("RECURSION ERROR");
this.LogicEngine.RecursionError = true;
}
return;
}
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input,this.getOutput());
this.LogicEngine.RecursionCount--;
}
}
}
getOutput() {
/*
Should return true or false
*/
return false;
}
drawElement(x,y,ctx) {
/*
Draw routine for the element
*/
this.drawBorderBox(ctx,x+10,y,drawWidth-20,drawHeight);
this.drawTextCentered(ctx,x,y,this.Width,this.Height,"LOGIC");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class ClockElement extends Element {
ClockTick() {
if (this.Inputs[0]) {
this.Output = ~this.Output;
if (this.Output) {
this.Task.Time = Math.round(this.Period * this.Duty);
} else {
this.Task.Time = this.Period - Math.round(this.Period * this.Duty);
}
for (let a = 0; a < this.OutputConnections.length; a++) {
this.LogicEngine.RecursionCount = 0;
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input, this.getOutput());
}
}
}
Delete() {
super.Delete();
this.LogicEngine.Scheduler.deleteTask(this.Task);
}
getOutput() {
return this.Output;
}
setInput(Input, Value) {
super.setInput(Input, Value);
if (!this.Inputs[0]) {
this.Output = false;
this.Task.LastCall = 0;
this.Task.Enabled = false;
for (let a = 0; a < this.OutputConnections.length;a++) {
this.LogicEngine.RecursionCount = 0;
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input,this.getOutput());
}
} else {
this.Task.Enabled = true;
}
}
constructor(logicengine) {
super(logicengine,1);
this.removeProperty("Inputs");
this.Name = "Clock";
this.Period = 1000;
this.Duty = 0.5;
this.Output = false;
this.Width = 100;
this.Task = new Task("ClockTask","CLOCK",0,Math.round(this.Period * this.Duty),this.ClockTick.bind(this));
this.setInput(0,true);
this.removeProperty("Inputs");
let periodProperty = new ElementProperty("Period","int",{CBObject: this,CBFunction: "setPeriod"},1000,false,false,4,999999);
let dutyProperty = new ElementProperty("Duty","int",{CBObject: this,CBFunction: "setDuty"},50,false,false,0,100);
this.Properties.push(periodProperty);
this.Properties.push(dutyProperty);
}
setPeriod(period) {
this.Period = period;
this.Task.LastCall = 0;
this.getProperty("Period").CurrentValue = period;
}
setDuty(duty) {
this.Duty = duty/100;
this.Task.LastCall = 0;
this.getProperty("Duty").CurrentValue = duty;
}
drawElement(x, y, ctx) {
this.drawBorderBox(ctx, x+10,y,this.Width-20,this.Height,1,"#000","#f7e979",this.LogicEngine.Settings.ShadowColor);
this.drawTextCentered(ctx,x,y+5,this.Width,12,this.Period + "ms " + (this.Duty * 100) + "%","10px Console");
this.drawTextCentered(ctx,x,y,this.Width,this.Height,this.Designator,"12px Console","#000");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class inputElement extends Element {
constructor(logicengine) {
super(logicengine,0);
this.Name = "InputElement";
this.Output = false;
this.Width = 100;
this.removeProperty("Inputs");
}
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 InputSwitch extends inputElement {
constructor(logicengine) {
super(logicengine);
this.Name = "Switch";
this.Height = 70;
}
MouseClick(mousePos) {
super.MouseClick(mousePos);
if ((mousePos.x >= (this.X + 5)) && (mousePos.x <= (this.X + 55)) && (mousePos.y >= (this.Y + 5)) && (mousePos.y <= (this.Y + 55))) {
this.Output = ~this.Output;
for (let a = 0; a < this.OutputConnections.length; a++) {
this.LogicEngine.RecursionCount = 0;
this.OutputConnections[a].Element.setInput(this.OutputConnections[a].Input, this.getOutput());
}
}
}
drawElement(x, y, ctx) {
this.drawBorderBox(ctx, x,y,this.Width-20,this.Height,1,"#000","#f7e979",this.LogicEngine.Settings.ShadowColor);
this.drawBorderBox(ctx,x+5,y+5,50,50,1,"#ccc","#777");
this.drawTextCentered(ctx,x,y+(this.Height-14),this.Width-(this.outputCircleRadius*2),12,this.Designator,"12px Console","#000");
this.drawOutputs(ctx,x,y);
}
}
class LogicAND extends Element {
constructor(logicengine,Inputs) {
super(logicengine,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+10,y,this.Width-20,this.Height);
let xOffset = 20;
let shadowColor = this.LogicEngine.Settings.ShadowColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(x+xOffset,y);
ctx.lineTo((x+xOffset)+ this.Width/4,y);
ctx.quadraticCurveTo(x+(this.Width - xOffset),y,x+(this.Width-xOffset),y+(this.Height/2));
ctx.quadraticCurveTo(x+(this.Width - xOffset),y+this.Height,(x+xOffset)+ this.Width/4,y+this.Height);
ctx.lineTo(x+xOffset,y+this.Height);
ctx.lineTo(x+xOffset,y);
ctx.lineWidth = "3";
ctx.fillStyle = "#f7e979";
ctx.shadowColor = shadowColor;
ctx.shadowBlur = "6";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
ctx.fill();
ctx.restore();
this.drawTextCentered(ctx,x,y,this.Width,this.Height,this.Designator,"12px Console","#000");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicNAND extends LogicAND {
constructor(logicengine,Inputs) {
super(logicengine,Inputs);
this.Name = "NAND";
this.Width = this.Width + 10;
}
getOutput() {
if (super.getOutput()) {
return false;
} else {
return true;
}
}
drawElement(x,y,ctx) {
let xOffset = 20;
let shadowColor = this.LogicEngine.Settings.ShadowColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(x+xOffset,y);
ctx.lineTo((x+xOffset)+ this.Width/4,y);
ctx.quadraticCurveTo(x+(this.Width - (xOffset*1.5)),y,x+(this.Width-(xOffset*1.5)),y+(this.Height/2));
ctx.quadraticCurveTo(x+(this.Width - (xOffset*1.5)),y+this.Height,(x+xOffset)+ this.Width/4,y+this.Height);
ctx.lineTo(x+xOffset,y+this.Height);
ctx.lineTo(x+xOffset,y);
ctx.lineWidth = "3";
ctx.fillStyle = "#f7e979";
ctx.strokeStyle = "#000000";
ctx.shadowColor = shadowColor;
ctx.shadowBlur = "6";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.strokeStyle = "#000000";
ctx.lineWidth = "1";
ctx.arc(x + (this.Width - 25),y + (this.Height/2),5,0,2*Math.PI);
ctx.stroke();
ctx.fill();
ctx.restore();
this.drawTextCentered(ctx,x,y,this.Width-xOffset,this.Height,this.Designator,"12px Console","#000");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicOR extends Element {
constructor(logicengine,Inputs) {
super(logicengine,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;
let xOffset = 20;
let shadowColor = this.LogicEngine.Settings.ShadowColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(x+(xOffset/4),y);
ctx.lineTo((x+xOffset)+ this.Width/4,y);
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2)),y,x+(this.Width-xOffset),y+(this.Height/2));
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2)),y+this.Height,(x+xOffset)+ this.Width/4,y+this.Height);
ctx.lineTo(x+(xOffset/4),y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height - (this.Height/32)),x+xOffset+5,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height/32),x+(xOffset/4),y);
ctx.lineWidth = "3";
ctx.fillStyle = "#f7e979";
ctx.shadowColor = shadowColor;
ctx.shadowBlur = "6";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
ctx.fill();
ctx.restore();
this.drawTextCentered(ctx,x,y,this.Width,this.Height,this.Designator,"12px Console","#000");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicNOR extends LogicOR {
constructor(logicengine,Inputs) {
super(logicengine,Inputs);
this.Name = "NOR";
this.Width = this.Width + 10;
}
getOutput() {
if (super.getOutput()) {
return false;
} else {
return true;
}
}
drawElement(x,y,ctx) {
let xOffset = 20;
let shadowColor = this.LogicEngine.Settings.ShadowColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(x+(xOffset/4),y);
ctx.lineTo((x+xOffset)+ this.Width/4,y);
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2.5)),y,x+(this.Width-(xOffset*1.5)),y+(this.Height/2));
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2.5)),y+this.Height,(x+xOffset)+ this.Width/4,y+this.Height);
ctx.lineTo(x+(xOffset/4),y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height - (this.Height/32)),x+xOffset+5,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height/32),x+(xOffset/4),y);
ctx.lineWidth = "3";
ctx.fillStyle = "#f7e979";
ctx.shadowColor = shadowColor;
ctx.shadowBlur = "6";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.strokeStyle = "#000000";
ctx.lineWidth = "1";
ctx.arc(x + (this.Width - 25),y + (this.Height/2),5,0,2*Math.PI);
ctx.stroke();
ctx.fill();
ctx.restore();
this.drawTextCentered(ctx,x,y,this.Width-xOffset,this.Height,this.Designator,"12px Console","#000");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicXOR extends Element {
constructor(logicengine) {
super(logicengine,2); // Only 2 inputs on XOR
this.Name = "XOR";
this.removeProperty("Inputs");
}
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;
let xOffset = 20;
let shadowColor = this.LogicEngine.Settings.ShadowColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(x+(xOffset/4),y);
ctx.lineTo((x+xOffset)+ this.Width/4,y);
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2)),y,x+(this.Width-xOffset),y+(this.Height/2));
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2)),y+this.Height,(x+xOffset)+ this.Width/4,y+this.Height);
ctx.lineTo(x+(xOffset/4),y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height - (this.Height/32)),x+xOffset+5,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height/32),x+(xOffset/4),y);
ctx.lineWidth = "3";
ctx.fillStyle = "#f7e979";
ctx.shadowColor = shadowColor;
ctx.shadowBlur = "6";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
ctx.fill();
ctx.lineWidth = "2";
ctx.beginPath();
ctx.shadowColor = "transparent";
ctx.moveTo(x+(xOffset/4)+10,y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+10),y+(this.Height - (this.Height/32)),x+xOffset+10,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+10),y+(this.Height/32),x+(xOffset/4)+10,y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x+(xOffset/4),y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height - (this.Height/32)),x+xOffset+5,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height/32),x+(xOffset/4),y);
ctx.stroke();
ctx.restore();
this.drawTextCentered(ctx,x,y,this.Width,this.Height,this.Designator,"12px Console","#000");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicXNOR extends Element {
constructor(logicengine) {
super(logicengine,2); // Only 2 inputs on XOR
this.Name = "XNOR";
this.removeProperty("Inputs");
this.Width += 10;
}
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;
let xOffset = 20;
let shadowColor = this.LogicEngine.Settings.ShadowColor;
ctx.save();
ctx.beginPath();
ctx.save();
ctx.beginPath();
ctx.moveTo(x+(xOffset/4),y);
ctx.lineTo((x+xOffset)+ this.Width/4,y);
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2.5)),y,x+(this.Width-(xOffset*1.5)),y+(this.Height/2));
ctx.quadraticCurveTo(x+(this.Width - (xOffset*2.5)),y+this.Height,(x+xOffset)+ this.Width/4,y+this.Height);
ctx.lineTo(x+(xOffset/4),y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height - (this.Height/32)),x+xOffset+5,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height/32),x+(xOffset/4),y);
ctx.lineWidth = "3";
ctx.fillStyle = "#f7e979";
ctx.shadowColor = shadowColor;
ctx.shadowBlur = "6";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.strokeStyle = "#000000";
ctx.lineWidth = "1";
ctx.arc(x + (this.Width - 25),y + (this.Height/2),5,0,2*Math.PI);
ctx.stroke();
ctx.fill();
ctx.lineWidth = "2";
ctx.beginPath();
ctx.shadowColor = "transparent";
ctx.moveTo(x+(xOffset/4)+10,y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+10),y+(this.Height - (this.Height/32)),x+xOffset+10,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+10),y+(this.Height/32),x+(xOffset/4)+10,y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x+(xOffset/4),y+this.Height);
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height - (this.Height/32)),x+xOffset+5,y+(this.Height/2));
ctx.quadraticCurveTo(x+(xOffset+5),y+(this.Height/32),x+(xOffset/4),y);
ctx.stroke();
ctx.restore();
this.drawTextCentered(ctx,x,y,this.Width-(xOffset/2),this.Height,this.Designator,"10px Console","#000");
this.drawInputs(ctx,x,y);
this.drawOutputs(ctx,x,y);
}
}
class LogicNOT extends Element {
constructor(logicengine) {
super(logicengine,1); // Only 1 inputs on NOT
this.Name = "NOT";
this.removeProperty("Inputs");
this.Width += 10;
}
getOutput() {
if (this.Inputs[0]) return false;
return true;
}
drawElement(x,y,ctx) {
let drawWidth = this.Width;
let drawHeight = this.Height;
let xOffset = 20;
let shadowColor = this.LogicEngine.Settings.ShadowColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(x+xOffset,y);
ctx.lineTo(x+(this.Width-(xOffset+10)),y+(this.Height/2));
ctx.lineTo(x+xOffset,y+this.Height);
ctx.lineTo(x+xOffset,y);
ctx.lineWidth = "3";
ctx.fillStyle = "#f7e979";
ctx.shadowColor = shadowColor;
ctx.shadowBlur = "6";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.strokeStyle = "#000000";
ctx.lineWidth = "1";
ctx.arc(x + (this.Width - 25),y + (this.Height/2),5,0,2*Math.PI);
ctx.stroke();
ctx.fill();
ctx.restore();
this.drawTextCentered(ctx,x,y,this.Width-(xOffset),this.Height,this.Designator,"12px Console","#000");
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,settings) {
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, 1, "rgba(100,200,255,0.25)", "rgba(100,200,255,0.25)");
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;
}
if (!this.Selected) {
let PropertiesBox = document.getElementById("PropertiesBox");
if (PropertiesBox.style.display != "none") PropertiesBox.style.display = "none";
}
for (let a = 0; a < this.Elements.length; a++) {
// Not ideal to loop twice but we need the connections drawn all at once to prevent layer issues
this.Elements[a].drawConnections(ctx, settings);
}
}
Select(element) {
this.Selected = element;
let PropertiesBox = document.getElementById("PropertiesBox");
let PropertiesBoxTitle = document.getElementById("PropertiesBoxTitle");
let PropertiesBoxContent = document.getElementById("PropertiesBoxContent");
PropertiesBoxTitle.innerText = this.Selected.Designator + " Properties";
let contentString = "<table id='propertiesTable'>";
for (let a = 0; a < this.Selected.Properties.length;a++) {
contentString += "<tr><td>" + this.Selected.Properties[a].Name + "</td><td>";
switch (this.Selected.Properties[a].Type) {
case "int":
contentString += "<input type='number' id='prop_" + this.Selected.Properties[a].Name + "' min='" + this.Selected.Properties[a].Minimium + "' max='" + this.Selected.Properties[a].Maximium + "' value='" + this.Selected.Properties[a].CurrentValue + "' onchange='logicEngine.PropertyChange(" + String.fromCharCode(34) + this.Selected.Properties[a].Name + String.fromCharCode(34) +");'>";
break;
}
contentString += "</td></tr>";
}
PropertiesBoxContent.innerHTML = contentString;
PropertiesBox.style.display = "block";
}
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) {
let leftmenu = document.getElementById("left-menu");
leftmenu.style.height = (window.innerHeight - 52) + "px";
this.Canvas.width = window.innerWidth - 205;
this.Canvas.height = window.innerHeight - 50;
this.Mouse = false;
let gridPlane = document.getElementById("GridPlane");
gridPlane.width = this.Canvas.width;
gridPlane.height = this.Canvas.height;
let Ctx = gridPlane.getContext("2d");
Ctx.save();
let gridWidth = 20;
for (let x = gridWidth;x < (this.Canvas.width); x+= gridWidth) {
Ctx.beginPath();
Ctx.moveTo(x,0);
Ctx.lineTo(x,this.Canvas.height);
Ctx.strokeStyle = "#777";
Ctx.lineWidth = "1";
Ctx.stroke();
}
for (let y = gridWidth;y < (this.Canvas.width); y+= gridWidth) {
Ctx.beginPath();
Ctx.moveTo(0,y);
Ctx.lineTo(this.Canvas.width,y);
Ctx.lineWidth = "1";
Ctx.strokeStyle = "#777";
Ctx.stroke();
}
Ctx.restore();
}
PropertyChange(property) {
if (!this.ActiveContainer.Selected.getProperty(property)) return false;
let propElement = document.getElementById("prop_" + property);
this.ActiveContainer.Selected.getProperty(property).Call(propElement.value);
}
Mouse_Down(evt) {
let mousePos = getMousePos(this.Canvas, evt);
this.MouseDown = true;
this.MouseDownTime = performance.now();
let element = this.ActiveContainer.checkMouseBounds(mousePos);
if (element) {
this.MovingElement = element;
this.MovingElementStartX = element.X;
this.MovingElementStartY = element.Y;
this.MovingElementMouseStartX = mousePos.x;
this.MovingElementMouseStartY = mousePos.y;
} else {
this.ActiveLink = false;
}
}
Mouse_Up(evt) {
let mousePos = getMousePos(this.Canvas, 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);
this.MovingElement.MouseClick(mousePos);
}
//console.log("Mouse Up");
}
if (!this.MovingElement) this.ActiveContainer.Selected = false;
this.MovingElement = false;
}
Mouse_Move(evt) {
this.Canvas.focus();
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;
}
}
} else {
this.ActiveContainer.checkMouseBounds(mousePos);
}
}
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.Settings = new LogicEngineSettings;
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.Scheduler = new ScheduleEngine();
this.RecursionCount = 0;
this.RecursionError = false;
this.Canvas.setAttribute('tabindex','0');
}
Link(input = 0) {
if (this.ActiveLink) {
if (this.ActiveContainer.Selected && (this.ActiveContainer.Selected != this.ActiveLink)) {
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() {
if (this.RecursionError) {
this.RecursionError = false;
alert("Recursion Error! Whatever you last did is causing an oscillating loop, please check your connections and try again!");
}
let startLoop = performance.now();
this.Ctx.clearRect(0,0,this.Canvas.width,this.Canvas.height);
if (this.ActiveLink) {
this.Ctx.save();
this.Ctx.strokeStyle = this.Settings.LinkingConnectionColor;
this.Ctx.lineWidth = this.Settings.LinkingWidth;
this.Ctx.setLineDash(this.Settings.LinkingDash);
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,this.Settings);
let ct = new CanvasTools();
let FPSOffset = this.Canvas.width - 150;
ct.drawText(this.Ctx,FPSOffset,650,"FPS: " + this.FPS,"12px console", "#00ff00");
ct.drawText(this.Ctx,FPSOffset,670,"Potential FPS: " + this.PotentialFPS,"12px console", "#00ff00");
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();
}
}