6.031
6.031 — Software Construction
Fall 2021

Code Examples for Reading 20

Download

You can also download a ZIP file containing this code.

Seems to be necessary to have a triple-backtick block at the start of this page,
otherwise all the pre/code tags below aren't syntax highlighted.
So create a hidden one.

draw.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Draw!</title>

    <style>
        #canvas {
            border: 1px solid black;
        }
        div {
            margin: 5px;
        }
    </style>
</head>

<body>

    <div>
        Color: <input type="text" id="colorTextbox" value="green">
    </div>
    <div>
        Shape: <select id="shapeDropdown">
                    <option value="circle" selected>circle</option>
                    <option value="square">square</option>
                </select>
        <input type="checkbox" id="filledCheckbox" checked="true"> filled
    </div>
    <div>
        Size: <input type="number" id="sizeBox" value="10" min="1" max="50" maxlength="2"> 
              <input type="range" id="sizeSlider" value="10" min="1" max="50">
    </div>
    <div>
        <button id="drawButton">Draw Randomly</button>
        <button id="clearButton">Clear</button>
    </div>

    <div>
        <canvas id="canvas" width="256" height="256">
        </canvas>
    </div>

    <script src="dist/draw-bundle.js"></script>
</body>
</html>

draw.ts

/* Copyright (c) 2021 MIT 6.031 course staff, all rights reserved.
 * Redistribution of original or derived work requires permission of course staff.
 */

// this code is loaded into draw.html

const drawButton: HTMLButtonElement = document.getElementById('drawButton') as HTMLButtonElement;
const clearButton: HTMLButtonElement = document.getElementById('clearButton') as HTMLButtonElement;
const canvasElement: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement;
const sizeBox: HTMLInputElement = document.getElementById('sizeBox') as HTMLInputElement;
const sizeSlider: HTMLInputElement = document.getElementById('sizeSlider') as HTMLInputElement;

drawButton.addEventListener('click', (event:MouseEvent) => {
    drawShape(canvasElement.width * Math.random(), canvasElement.height * Math.random());
    // for (let i = 0; i < 1000*1000; ++i) {
    //     drawShape(canvasElement.width * Math.random(), canvasElement.height * Math.random());
    // }
});

clearButton.addEventListener('click', clear);

// synchronize the size textbox and slider
sizeBox.addEventListener('input', () => {
    sizeSlider.value = sizeBox.value;
});
sizeSlider.addEventListener('input', () => {
    sizeBox.value = sizeSlider.value;
});

/**
 * @returns color shown in the color textbox
 */
function getColor():string {
    return (document.getElementById('colorTextbox') as HTMLInputElement).value; 
}

/**
 * @returns size shown in the size textbox
 */
 function getSize():number {
    return parseInt((document.getElementById('sizeBox') as HTMLInputElement).value); 
}

/**
 * Draw a square filled with a random color.
 * @param x x position of center of box centers
 * @param y y position of center of box centers
 */
function drawShape(x: number, y: number):void {
    const canvas = canvasElement.getContext('2d');
    if ( ! canvas ) throw new Error('unable to get canvas drawing context');

    // save original context settings before we translate and change colors
    canvas.save();

    // move origin to (x,y)
    canvas.translate(x, y);

    // draw the outer outline box centered on the origin (which is now x,y)
    canvas.strokeStyle = 'black';
    const size = getSize();
    canvas.lineWidth = 2;
    canvas.strokeRect( -size/2, -size/2, size, size);

    // fill with a random color
    canvas.fillStyle = getColor();
    canvas.fillRect( -size/2, -size/2, size, size);

    // reset the origin and styles back to defaults
    canvas.restore();
}

/**
 * Clear the drawing area, filling it with white again.
 */
function clear() {
    const context = canvasElement.getContext('2d');
    if ( ! context ) throw new Error('unable to get canvas drawing context');

    context.save();
    context.fillStyle = 'white';
    context.fillRect(0, 0, canvasElement.width, canvasElement.height);
    context.restore();
}

Counter.ts


// Type of Counter's listener callback
export type NumberListener = (numberReached:bigint) => void;

/** A mutable incrementing counter that calls its listeners each time it 
  * reaches a new number. */
export class Counter {

    private _value: bigint = 0n;
    private listeners: Array<NumberListener> = [];

    // Abstraction function
    //    AF(_value, listeners) = a counter currently at `_value`
    //       that sends events to the `listeners` whenever it changes
    // Rep invariant
    //    true


    /** Make a counter initially set to zero. */
    public constructor() {
    }

    /** @returns the value of this counter. */
    public get value():bigint {
        return this._value;
    }
    
    /** Increment this counter. */
    public increment():void {
        ++this._value;
        this.callListeners();
    }

    /** Modifies this counter by adding a listener.
      * @param listener called by this counter when it changes. */
    public addEventListener(listener: NumberListener):void {
        this.listeners.push(listener);
    }

    /** Modifies this counter by removing a listener.
      * @param listener will no longer be called by this counter. */
    public removeEventListener(listener: NumberListener):void {
        // search backwards through the array,
        // so we can remove all matches to `listeners`
        // without having to adjust the index
        for (let i = this.listeners.length-1; i >= 0; --i) {
            if (this.listeners[i] === listener) {
                this.listeners.splice(i, 1);
            }
        }
    }

    // Call all listeners to notify them about a new number
    private callListeners():void {
        // iterate over a copy of listeners so that we are re-entrant
        // (i.e. a listener may call add/removeNumberListener,
        // which would mutate the array in the midst of iteration)
        for (const listener of [...this.listeners]) {
            listener(this._value);
        }
    }
}



/**
 * Example client code, trying out a Counter with a couple listeners.
 */
function main() {
    const counter = new Counter();

    // listen for multiples of 113
    counter.addEventListener((value) => {
        if (value % 113n === 0n) {
            console.log(value, "is a multiple of 113");
        }
    });

    // listen for 10000, then exit the process
    counter.addEventListener((value) => {
        if (value === 10000n) {
            process.exit(0);
        }
    });

    // crank the counter
    while (true) {
        counter.increment();
    }
}    

if (require.main === module) {
    main();
}