/* 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]; } } 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") { 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 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); this.drawTextCentered(ctx,x,y+5,this.Width,12,this.Period + "ms " + (this.Duty * 100) + "%","10px Console"); 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-10,this.Height); this.drawBorderBox(ctx,x+5,y+5,50,50,1,"#ccc","#777"); 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); this.drawTextCentered(ctx,x,y,this.Width,this.Height,"|⊃"); this.drawInputs(ctx,x,y); this.drawOutputs(ctx,x,y); } } class LogicNAND extends LogicAND { constructor(logicengine,Inputs) { super(logicengine,Inputs); this.Name = "NAND"; } getOutput() { if (super.getOutput()) { return false; } else { return true; } } drawElement(x,y,ctx) { this.drawBorderBox(ctx, x+10,y,this.Width-20,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(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; this.drawBorderBox(ctx, x+10,y,drawWidth-20,drawHeight); this.drawTextCentered(ctx,x,y,drawWidth,drawHeight,")⊃"); this.drawInputs(ctx,x,y); this.drawOutputs(ctx,x,y); } } class LogicNOR extends LogicOR { constructor(logicengine,Inputs) { super(logicengine,Inputs); this.Name = "NOR"; } getOutput() { if (super.getOutput()) { return false; } else { return true; } } drawElement(x,y,ctx) { this.drawBorderBox(ctx, x+10,y,this.Width-20,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(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; this.drawBorderBox(ctx, x+10,y,drawWidth-20,drawHeight); this.drawTextCentered(ctx,x,y,drawWidth,drawHeight,"))⊃"); 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"); } 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+10,y,drawWidth-20,drawHeight); this.drawTextCentered(ctx,x,y,drawWidth,drawHeight,"))⊃🞄"); 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"); } getOutput() { if (this.Inputs[0]) return false; return true; } drawElement(x,y,ctx) { let drawWidth = this.Width; let drawHeight = this.Height; this.drawBorderBox(ctx, x+10,y,drawWidth-20,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,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 = "
" + this.Selected.Properties[a].Name + " | "; switch (this.Selected.Properties[a].Type) { case "int": contentString += ""; break; } contentString += " |