File: /var/www/html/simulink/circuit/pong/ball.html
<meta name="viewport" content="width=820">
<title>Circuit Simulator Applet</title>
<link rel="SHORTCUT ICON" href="favicon.ico">
<link rel="stylesheet" href="pong.css" type="text/css">
<body>
<hr>
<div class="split">
<div id="split-0">
<iframe id="circuitFrame" src="../circuitjs.html?startCircuit=ponghfullslow.txt&mouseMode=DragAll"></iframe>
</div>
<div id="split-1">
<div id="info"></div>
<canvas id="canvas" width=375 height=246 style="background-color:black;"></canvas>
<br>
Paddle 1: <input type="range" class="paddle" id="pad1" min="1" max="220" value="120"><br>
Paddle 2: <input type="range" class="paddle" id="pad2" min="1" max="220" value="120"><br>
<input id="recording" type="checkbox">Recording
<p>
The position of the ball is controlled by a series of counters. There are two 4-bit counters and a JK flip flop,
which act as a 9-bit counter that keeps track of the horizontal position. The phase of the counter determines
the horizontal position of the ball. The counter only counts when HBLANK is low, and counts at the same rate
as the main horizontal counter. When it reaches 508, that is the left side of the ball;
<span class="overline">HVID</span> is brought low. When it reaches
511, that is the right side. After that, it normally gets reloaded with the value 138. So it counts 512-138=374 times
and then resets. That is equal to the number of rising edges of the clock when HBLANK is low. At the end of every line,
during the horizontal blanking interval, the counter will have the same value.
<p>
To move the ball horizontally, we load a value other than 138 into the counter. The "Horizontal Ball Control"
portion of the circuit takes care of this at the end of every frame.
<p>
There are 2 more 4-bit counters, acting as a 8-bit counter, that keep track of the vertical position of the
ball. This counter counts horizontal lines (positive transitions of <span class="overline">HSYNC</span>) and
only counts when VBLANK is low. It resets after it hits 255. When the counter has a value 252 or greater,
that means the ball is located on the current horizontal scanline (VVID is high).
<p>
There are 246 visible lines on the screen, so if
we want the vertical ball counter to have the same value each frame, we should load it with 10 when it resets.
But when the simulation starts, the load inputs are equal to 7. So, at the end of every frame the counter's value
will be 3 less (which you can verify by reducing the simulation speed). This will make the ball move down by
3 pixels each frame. When the ball hits the bottom of the screen, then the counter gets loaded with 13,
which makes the ball move up by 3 pixels each frame. The "Vertical Ball Control" portion of the circuit
takes care of this.
<p>
Since the simulation is so slow, you can click on the screen to reposition the ball.
<p>
At the top of the screen, in the circuit that generates HBLANK, there is a buffer which introduces a small delay.
This is not there in the original circuit, but is
needed to emulate some delays in the real counters and gates. If we didn't do this,
then the horizontal ball counter would be out of sync with the main counter.
See <a href="http://www1.cs.columbia.edu/~sedwards/papers/edwards2012reconstructing.pdf">Edwards</a> 1.4.
<p>
In this example, the vertical ball counters have a <span class="overline">CLR V</span> input (instead of +5V like
the original circuit) in order to implement the ball repositioning.
<p>
Next: <a href="hbc.html">Horizontal Ball Control</a>.
<p>
<textarea id="textarea"></textarea>
<p>
<a href="index.html">Index</a>
</div>
<script type="module">
import Split from './split.js';
Split(['#split-0', '#split-1'], { sizes: [70, 30] });
// get iframe the simulator is running in. Must have same origin as this file!
var iframe = document.getElementById("circuitFrame");
var sim;
var freq, ampl;
var clk, video, score, ctx;
function round(x) {
return Math.round(x*1000)/1000;
}
var nettext = '';
var highlightY = -1;
var savedLine;
var lastTime;
function didAnalyze(sim) {
// get nodes we need
//clk = sim.getNode("CLK");
//video = sim.getNode("VIDEO");
//score = sim.getNode("SCORE");
sim.setExtVoltage("/CLR V", 5);
sim.setExtVoltage("SERVE", 0);
}
// called when simulator updates its display
function didUpdate(sim) {
var info = document.getElementById("info");
// erase highlight
if (savedLine) {
ctx.putImageData(savedLine, 0, highlightY);
savedLine = null;
}
ctx.fillStyle = "red";
if (sim.getTime()-lastTime > 1/(60*200)) {
// draw highlight on part of line where we updated
highlightY = y-1;
savedLine = ctx.getImageData(0, highlightY, 455, 1);
ctx.fillRect(0, highlightY, 455, 1);
} else {
// draw highlight just above line we are updating
highlightY = y;
if (x > 0) {
savedLine = ctx.getImageData(0, y, x, 1);
ctx.fillRect(0, y, x, 1);
}
}
lastTime = sim.getTime();
ctx.fillStyle = "white";
}
var lastclk = -1;
var x = -55, y = 0;
var clearedY = -1;
var moveBall = 1, moveBallX = 48, moveBallY = 90;
var stepCounter = 0;
// called every timestep, which is 6 nanoseconds of game time.
// we do as little work in here as possible.
function didStep(sim) {
var c = sim.getNodeVoltage("CLK");
stepCounter++;
if (stepCounter == 9)
sim.getCircuitAsSVG();
if (c == lastclk)
return;
stepCounter = 0;
if (c > 2.5) {
// positive clock transition, increase x
x++;
if (x == 375) { // end of line?
// horizontal blanking lasts 80 clocks
x = -80;
y++;
if (sim.getNodeVoltage("VBLANK") > 2.5) {
// if we're in vertical blank, set y to 0
y = 0;
clearedY = -1;
sim.setExtVoltage("PADTRIGGER1", 5);
sim.setExtVoltage("PADTRIGGER2", 5);
}
// trigger paddles
if (y == parseInt(pad1.value))
sim.setExtVoltage("PADTRIGGER1", 0);
if (y == parseInt(pad2.value))
sim.setExtVoltage("PADTRIGGER2", 0);
ctx.fillStyle = "black";
ctx.fillRect(0, y, 455, 1);
ctx.fillStyle = "white";
} else if (y == 0 && sim.getNodeVoltage("HBLANK") > 2.5)
x = 0;
// draw video
if (sim.getNodeVoltage("VIDEO") > 2.5) {
ctx.fillStyle = "#fff";
ctx.fillRect(x, y, 1, 1);
} else if (sim.getNodeVoltage("SCORE") > 2.5) {
// draw score slightly dimmer
ctx.fillStyle = "#d4d4d4";
ctx.fillRect(x, y, 1, 1);
}
}
if (moveBall > 0) {
// user is positioning ball
if (moveBall == 1 && x == moveBallX && y == moveBallY) {
//console.log("moveball1");
// zero h and v counters by setting SERVE high and /CLR V low
sim.setExtVoltage("SERVE", 5);
sim.setExtVoltage("/CLR V", 0);
moveBall = 2;
} else if (moveBall >= 2) {
// put SERVE low and /CLR V high again, we're done setting the ball position
sim.setExtVoltage("SERVE", 0);
sim.setExtVoltage("/CLR V", 5);
//console.log("moveball " + moveBall);
moveBall = 0;
}
}
lastclk = c;
}
function didRenderSVG(sim, data) {
if (document.getElementById("recording").checked)
console.dir(data);
}
// callback called when simulation is done initializing
function simLoaded() {
// get simulator object
sim = iframe.contentWindow.CircuitJS1;
// set up callbacks on update and timestep
sim.onupdate = didUpdate;
sim.ontimestep = didStep;
sim.onanalyze = didAnalyze;
sim.onsvgrendered = didRenderSVG;
ctx = canvas.getContext("2d");
//ctx.fillRect(0, 0, 455, 262);
ctx.strokeStyle = "white";
ctx.fillStyle = "white";
canvas.onmousedown = function (event) {
var rect = canvas.getBoundingClientRect();
// set ball position. we do this by zeroing the h and v counters. when that's done, the ball
// will be located 139 pixels to the right, 8 pixels down. so we correct for that here.
moveBallX = Math.floor((event.clientX - rect.left) * 375 / rect.width) - 139;
moveBallY = Math.floor((event.clientY - rect.top) * 246 / rect.height) - 8;
if (moveBallX < 0) {
// wrap around
moveBallX += 375;
moveBallY--;
}
//console.log("move ball " + moveBallX + " " + moveBallY);
moveBall = true;
}
}
// set up callback
iframe.contentWindow.oncircuitjsloaded = simLoaded;
</script>