/* 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(); } }