6.031
6.031 — Software Construction
Spring 2022

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="sizeTextbox" value="10" min="1" max="50" maxlength="2"> 
              <input type="range" id="sizeSlider" value="10" min="1" max="50">
    </div>
    <div>
        <button id="drawRandomlyButton">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 drawRandomlyButton: HTMLButtonElement = document.getElementById('drawRandomlyButton') as HTMLButtonElement;
const clearButton: HTMLButtonElement = document.getElementById('clearButton') as HTMLButtonElement;

const canvasElement: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement;

const colorTextbox: HTMLInputElement = document.getElementById('colorTextbox') as HTMLInputElement;

const shapeDropdown: HTMLSelectElement = document.getElementById('shapeDropdown') as HTMLSelectElement;
const filledCheckbox: HTMLInputElement = document.getElementById('filledCheckbox') as HTMLInputElement;

const sizeTextbox: HTMLInputElement = document.getElementById('sizeTextbox') as HTMLInputElement;
const sizeSlider: HTMLInputElement = document.getElementById('sizeSlider') as HTMLInputElement;

canvasElement.addEventListener('click', (event:MouseEvent) => {
    drawShape(event.offsetX, event.offsetY);
});

drawRandomlyButton.addEventListener('click', () => {
    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
sizeTextbox.addEventListener('input', () => {
    sizeSlider.value = sizeTextbox.value;
});
sizeSlider.addEventListener('input', () => {
    sizeTextbox.value = sizeSlider.value;
});


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

/**
 * @returns size shown in the size textbox
 */
 function getSize():number {
    return parseInt(sizeTextbox.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);

    // describe the shape, either a circle or a rectangle, centered on the origin
    const size = getSize();
    canvas.beginPath();
    if (shapeDropdown.value === 'circle') {
        canvas.arc(0, 0, size/2, 0, 2 * Math.PI);
    } else {
        canvas.rect(-size/2, -size/2, size, size)
    }

    // stroke the outline of the shape
    canvas.strokeStyle = 'black';
    canvas.lineWidth = 2;
    canvas.stroke();

    if (filledCheckbox.checked) {
        // fill with a color
        canvas.fillStyle = getColor();
        canvas.fill();
    }

    // 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


/** 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<(numberReached:bigint) => void> = [];

    // 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: (numberReached:bigint) => void):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: (numberReached:bigint) => void):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();
}