// Basic implementation of the image library. // // This should mimic the implementation of 2htdp/image. ////////////////////////////////////////////////////////////////////// var colorNamespace = MACHINE.modules['whalesong/image/private/color.rkt'].namespace; var colorStruct = colorNamespace['struct:color']; var makeColor = colorStruct.constructor; var isColor = colorStruct.predicate; var colorRed = function(c) { return colorStruct.accessor(c, 0); }; var colorGreen = function(c) { return colorStruct.accessor(c, 1); }; var colorBlue = function(c) { return colorStruct.accessor(c, 2); }; var colorAlpha = function(c) { return colorStruct.accessor(c, 3); }; ////////////////////////////////////////////////////////////////////// var heir = plt.baselib.heir; var clone = plt.baselib.clone; var isAngle = function(x) { return plt.baselib.numbers.isReal(x) && jsnums.greaterThanOrEqual(x, 0) && jsnums.lessThan(x, 360); }; // Produces true if the value is a color or a color string. // On the Racket side of things, this is exposed as image-color?. var isColorOrColorString = function(thing) { return (isColor(thing) || ((plt.baselib.strings.isString(thing) || plt.baselib.symbols.isSymbol(thing)) && typeof(colorDb.get(thing)) != 'undefined')); } var colorString = function(aColor) { return ("rgb(" + colorRed(aColor) + "," + colorGreen(aColor) + ", " + colorBlue(aColor) + ")"); }; var isSideCount = function(x) { return plt.baselib.numbers.isInteger(x) && jsnums.greaterThanOrEqual(x, 3); }; var isStepCount = function(x) { return plt.baselib.numbers.isInteger(x) && jsnums.greaterThanOrEqual(x, 1); }; var isPointsCount = function(x) { return plt.baselib.numbers.isNatural(x) && jsnums.greaterThanOrEqual(x, 2); }; // Produces true if thing is an image-like object. var isImage = function(thing) { if (typeof(thing.getHeight) !== 'function') return false; if (typeof(thing.getWidth) !== 'function') return false; if (typeof(thing.getBaseline) !== 'function') return false; if (typeof(thing.updatePinhole) !== 'function') return false; if (typeof(thing.render) !== 'function') return false; return true; }; // Base class for all images. var BaseImage = function(pinholeX, pinholeY) { this.pinholeX = pinholeX; this.pinholeY = pinholeY; }; BaseImage.prototype.updatePinhole = function(x, y) { var aCopy = clone(this); aCopy.pinholeX = x; aCopy.pinholeY = y; return aCopy; }; BaseImage.prototype.getHeight = function(){ return this.height; }; BaseImage.prototype.getWidth = function(){ return this.width; }; BaseImage.prototype.getBaseline = function(){ return this.height; }; // render: context fixnum fixnum: -> void // Render the image, where the upper-left corner of the image is drawn at // (x, y). // NOTE: the rendering should be oblivous to the pinhole. BaseImage.prototype.render = function(ctx, x, y) { throw new Error('BaseImage.render unimplemented!'); }; // makeCanvas: number number -> canvas // Constructs a canvas object of a particular width and height. var makeCanvas = function(width, height) { var canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; $(canvas).css('width', canvas.width + "px"); $(canvas).css('height', canvas.height + "px"); $(canvas).css('padding', '0px'); // KLUDGE: IE compatibility uses /js/excanvas.js, and dynamic // elements must be marked this way. if (window.G_vmlCanvasManager) { canvas = window.G_vmlCanvasManager.initElement(canvas); } return canvas; }; var withIeHack = function(canvas, f) { // canvas.style.display = 'none'; // document.body.appendChild(canvas); // try { var result = f(canvas); // } catch(e) { // document.body.removeChild(canvas); // canvas.style.display = ''; // throw e; // } // document.body.removeChild(canvas); // canvas.style.display = ''; return result; }; // Images are expected to define a render() method, which is used // here to draw to the canvas. BaseImage.prototype.toDomNode = function(params) { var that = this; var width = that.getWidth(); var height = that.getHeight(); var canvas = makeCanvas(width, height); var ctx; // // Try best effort to render to screen at this point. // try { // ctx = canvas.getContext("2d"); // that.render(ctx, 0, 0); // } catch (e) { // } // KLUDGE: on IE, the canvas rendering functions depend on a // context where the canvas is attached to the DOM tree. // We initialize an afterAttach hook; the client's responsible // for calling this after the dom node is attached to the // document. var onAfterAttach = function(event) { // $(canvas).unbind('afterAttach', onAfterAttach); var ctx = this.getContext("2d"); that.render(ctx, 0, 0); }; $(canvas).bind('afterAttach', onAfterAttach); // Canvases lose their drawn content on cloning. data may help us to preserve it. $(canvas).data('toRender', onAfterAttach); return canvas; }; BaseImage.prototype.toWrittenString = function(cache) { return ""; } BaseImage.prototype.toDisplayedString = function(cache) { return ""; } BaseImage.prototype.equals = function(other, aUnionFind) { return (this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY); }; // isScene: any -> boolean // Produces true when x is a scene. var isScene = function(x) { return ((x != undefined) && (x != null) && (x instanceof SceneImage)); }; ////////////////////////////////////////////////////////////////////// // SceneImage: primitive-number primitive-number (listof image) -> Scene var SceneImage = function(width, height, children, withBorder) { BaseImage.call(this, 0, 0); this.width = width; this.height = height; this.children = children; // arrayof [image, number, number] this.withBorder = withBorder; } SceneImage.prototype = heir(BaseImage.prototype); // add: image primitive-number primitive-number -> Scene SceneImage.prototype.add = function(anImage, x, y) { return new SceneImage(this.width, this.height, this.children.concat([[anImage, x - anImage.pinholeX, y - anImage.pinholeY]]), this.withBorder); }; // render: 2d-context primitive-number primitive-number -> void SceneImage.prototype.render = function(ctx, x, y) { var i; var childImage, childX, childY; // Clear the scene. ctx.clearRect(x, y, this.width, this.height); // Then ask every object to render itself. for(i = 0; i < this.children.length; i++) { childImage = this.children[i][0]; childX = this.children[i][1]; childY = this.children[i][2]; ctx.save(); childImage.render(ctx, childX + x, childY + y); ctx.restore(); } // Finally, draw the black border if withBorder is true if (this.withBorder) { ctx.strokeStyle = 'black'; ctx.strokeRect(x, y, this.width, this.height); } }; SceneImage.prototype.equals = function(other, aUnionFind) { if (!(other instanceof SceneImage)) { return false; } if (this.pinholeX != other.pinholeX || this.pinholeY != other.pinholeY || this.width != other.width || this.height != other.height || this.children.length != other.children.length) { return false; } for (var i = 0; i < this.children.length; i++) { var rec1 = this.children[i]; var rec2 = other.children[i]; if (rec1[1] !== rec2[1] || rec1[2] !== rec2[2] || !plt.baselib.equality.equals(rec1[0], rec2[0], aUnionFind)) { return false; } } return true; }; ////////////////////////////////////////////////////////////////////// // FileImage: string node -> Image var FileImage = function(src, rawImage) { BaseImage.call(this, 0, 0); var self = this; this.src = src; this.isLoaded = false; // animationHack: see installHackToSupportAnimatedGifs() for details. this.animationHackImg = undefined; if (rawImage && rawImage.complete) { this.img = rawImage; this.isLoaded = true; this.pinholeX = self.img.width / 2; this.pinholeY = self.img.height / 2; } else { // fixme: we may want to do something blocking here for // onload, since we don't know at this time what the file size // should be, nor will drawImage do the right thing until the // file is loaded. this.img = new Image(); this.img.onload = function() { self.isLoaded = true; self.pinholeX = self.img.width / 2; self.pinholeY = self.img.height / 2; }; this.img.onerror = function(e) { self.img.onerror = ""; self.img.src = "http://www.wescheme.org/images/broken.png"; } this.img.src = src; } } FileImage.prototype = heir(BaseImage.prototype); var imageCache = {}; FileImage.makeInstance = function(path, rawImage) { if (! (path in imageCache)) { imageCache[path] = new FileImage(path, rawImage); } return imageCache[path]; }; FileImage.installInstance = function(path, rawImage) { imageCache[path] = new FileImage(path, rawImage); }; FileImage.installBrokenImage = function(path) { imageCache[path] = new TextImage("Unable to load " + path, 10, colorDb.get("red"), "normal", "Optimer","","",false); }; FileImage.prototype.render = function(ctx, x, y) { this.installHackToSupportAnimatedGifs(); ctx.drawImage(this.animationHackImg, x, y); }; // The following is a hack that we use to allow animated gifs to show // as animating on the canvas. FileImage.prototype.installHackToSupportAnimatedGifs = function() { if (this.animationHackImg) { return; } this.animationHackImg = this.img.cloneNode(true); document.body.appendChild(this.animationHackImg); this.animationHackImg.width = 0; this.animationHackImg.height = 0; }; FileImage.prototype.getWidth = function() { return this.img.width; }; FileImage.prototype.getHeight = function() { return this.img.height; }; // Override toDomNode: we don't need a full-fledged canvas here. FileImage.prototype.toDomNode = function(params) { return this.img.cloneNode(true); }; FileImage.prototype.equals = function(other, aUnionFind) { return (other instanceof FileImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.src == other.src); }; ////////////////////////////////////////////////////////////////////// // VideoImage: String Node -> Video var VideoImage = function(src, rawVideo) { BaseImage.call(this, 0, 0); var self = this; this.src = src; if (rawVideo) { this.video = rawVideo; this.width = self.video.videoWidth; this.height = self.video.videoHeight; this.pinholeX = self.width / 2; this.pinholeY = self.height / 2; this.video.volume = 1; this.video.poster = "http://www.wescheme.org/images/broken.png"; this.video.autoplay = true; this.video.autobuffer=true; this.video.loop = true; this.video.play(); } else { // fixme: we may want to do something blocking here for // onload, since we don't know at this time what the file size // should be, nor will drawImage do the right thing until the // file is loaded. this.video = document.createElement('video'); this.video.src = src; this.video.addEventListener('canplay', function() { this.width = self.video.videoWidth; this.height = self.video.videoHeight; this.pinholeX = self.width / 2; this.pinholeY = self.height / 2; this.video.poster = "http://www.wescheme.org/images/broken.png"; this.video.autoplay = true; this.video.autobuffer=true; this.video.loop = true; this.video.play(); }); this.video.addEventListener('error', function(e) { self.video.onerror = ""; self.video.poster = "http://www.wescheme.org/images/broken.png"; }); } } VideoImage.prototype = heir(BaseImage.prototype); var videos = {}; VideoImage.makeInstance = function(path, rawVideo) { if (! (path in VideoImage)) { videos[path] = new VideoImage(path, rawVideo); } return videos[path]; }; VideoImage.prototype.render = function(ctx, x, y) { ctx.drawImage(this.video, x, y); }; VideoImage.prototype.equals = function(other, aUnionFind) { return (other instanceof VideoImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.src == other.src); }; ////////////////////////////////////////////////////////////////////// // OverlayImage: image image placeX placeY -> image // Creates an image that overlays img1 on top of the // other image. var OverlayImage = function(img1, img2, placeX, placeY) { // calculate centers using width/height, so we are scene/image agnostic var c1x = img1.getWidth()/2; var c1y = img1.getHeight()/2; var c2x = img2.getWidth()/2; var c2y = img2.getHeight()/2; // calculate absolute offset of 2nd image's *CENTER* // convert relative X,Y to center offsets, // if placeX and placeY are UL corner offsets, convert to center offsets if (placeX == "left" ) var xOffset = img2.getWidth()-(c1x+c2x); else if (placeX == "right" ) var xOffset = img1.getWidth()-(c1x+c2x); else if (placeX == "beside") var xOffset = c1x+c2x; else if (placeX == "middle") var xOffset = 0; else if (placeX == "center") var xOffset = 0; else var xOffset = placeX - (c1x-c2x); if (placeY == "bottom") var yOffset = img1.getHeight()-(c1y+c2y); else if (placeY == "top") var yOffset = img2.getHeight()-(c1y+c2y); else if (placeY == "above" ) var yOffset = c1y+c2y; else if (placeY == "middle") var yOffset = 0; else if (placeY == "center") var yOffset = 0; else if (placeY == "baseline") var yOffset= img1.getBaseline()-img2.getBaseline(); else var yOffset = placeY - (c1y-c2y); // Correct offsets when dealing with Scenes instead of images, // by adding the center values if(isScene(img1)){xOffset =+c1x; yOffset =+c1y;} if(isScene(img2)){xOffset =+c2x; yOffset =+c2y;} // The *center* of the 2nd image, once overlaid, changes by the original difference in centers, // plus the size of the offsets. Calculate this delta for X and Y. var deltaX = c1x - c2x + xOffset; var deltaY = c1y - c2y + yOffset; // Each edge of the new, combined image may be grown or shrunk, depending on deltaX or deltaY var left = Math.min(0, deltaX); var top = Math.min(0, deltaY); var right = Math.max(deltaX + img2.getWidth(), img1.getWidth()); var bottom = Math.max(deltaY + img2.getHeight(), img1.getHeight()); // Calculate the new width, height and center based on edge lengths this.width = right - left; this.height = bottom - top; BaseImage.call(this, Math.floor((right-left) / 2), Math.floor((bottom-top) / 2)); // store the overlaid images, and the offsets for each this.img1 = img1; this.img2 = img2; this.img1Dx = -left; this.img1Dy = -top; this.img2Dx = deltaX - left; this.img2Dy = deltaY - top; }; OverlayImage.prototype = heir(BaseImage.prototype); OverlayImage.prototype.render = function(ctx, x, y) { ctx.save(); this.img2.render(ctx, x + this.img2Dx, y + this.img2Dy); this.img1.render(ctx, x + this.img1Dx, y + this.img1Dy); ctx.restore(); }; OverlayImage.prototype.equals = function(other, aUnionFind) { return ( other instanceof OverlayImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.width == other.width && this.height == other.height && this.img1Dx == other.img1Dx && this.img1Dy == other.img1Dy && this.img2Dx == other.img2Dx && this.img2Dy == other.img2Dy && plt.baselib.equality.equals(this.img1, other.img1, aUnionFind) && plt.baselib.equality.equals(this.img2, other.img2, aUnionFind) ); }; ////////////////////////////////////////////////////////////////////// // rotate: angle image -> image // Rotates image by angle degrees in a counter-clockwise direction. // based on http://stackoverflow.com/questions/3276467/adjusting-div-width-and-height-after-rotated var RotateImage = function(angle, img) { var sin = Math.sin(angle * Math.PI / 180), cos = Math.cos(angle * Math.PI / 180); // (w,0) rotation var x1 = Math.floor(cos * img.getWidth()), y1 = Math.floor(sin * img.getWidth()); // (0,h) rotation var x2 = Math.floor(-sin * img.getHeight()), y2 = Math.floor( cos * img.getHeight()); // (w,h) rotation var x3 = Math.floor(cos * img.getWidth() - sin * img.getHeight()), y3 = Math.floor(sin * img.getWidth() + cos * img.getHeight()); var minX = Math.min(0, x1, x2, x3), maxX = Math.max(0, x1, x2, x3), minY = Math.min(0, y1, y2, y3), maxY = Math.max(0, y1, y2, y3); var rotatedWidth = maxX - minX, rotatedHeight = maxY - minY; // resize the image BaseImage.call(this, Math.floor(rotatedWidth / 2), Math.floor(rotatedHeight / 2)); this.img = img; this.width = rotatedWidth; this.height = rotatedHeight; this.angle = angle; this.translateX = -minX; this.translateY = -minY; }; RotateImage.prototype = heir(BaseImage.prototype); // translate the canvas using the calculated values, then draw at the rotated (x,y) offset. RotateImage.prototype.render = function(ctx, x, y) { // calculate the new x and y offsets, by rotating the radius formed by the hypoteneuse var sin = Math.sin(this.angle * Math.PI / 180), cos = Math.cos(this.angle * Math.PI / 180), r = Math.sqrt(x*x + y*y); ctx.save(); ctx.translate(x + this.translateX, y + this.translateY); ctx.rotate(this.angle * Math.PI / 180); this.img.render(ctx, 0, 0); ctx.restore(); }; RotateImage.prototype.equals = function(other, aUnionFind) { return ( other instanceof RotateImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.width == other.width && this.height == other.height && this.angle == other.angle && this.translateX == other.translateX && this.translateY == other.translateY && plt.baselib.equality.equals(this.img, other.img, aUnionFind) ); }; ////////////////////////////////////////////////////////////////////// // ScaleImage: factor factor image -> image // Scale an image var ScaleImage = function(xFactor, yFactor, img) { // resize the image BaseImage.call(this, Math.floor((img.getWidth() * xFactor) / 2), Math.floor((img.getHeight() * yFactor) / 2)); this.img = img; this.width = img.getWidth() * xFactor; this.height = img.getHeight() * yFactor; this.xFactor = xFactor; this.yFactor = yFactor; }; ScaleImage.prototype = heir(BaseImage.prototype); // scale the context, and pass it to the image's render function ScaleImage.prototype.render = function(ctx, x, y) { ctx.save(); ctx.scale(this.xFactor, this.yFactor); this.img.render(ctx, x / this.xFactor, y / this.yFactor); ctx.restore(); }; ScaleImage.prototype.equals = function(other, aUnionFind) { return ( other instanceof ScaleImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.width == other.width && this.height == other.height && this.xFactor == other.xFactor && this.yFactor == other.yFactor && plt.baselib.equality.equals(this.img, other.img, aUnionFind) ); }; ////////////////////////////////////////////////////////////////////// // CropImage: startX startY width height image -> image // Crop an image var CropImage = function(x, y, width, height, img) { BaseImage.call(this, Math.floor(width / 2), Math.floor(height / 2)); this.x = x; this.y = y; this.width = width; this.height = height; this.img = img; }; CropImage.prototype = heir(BaseImage.prototype); CropImage.prototype.render = function(ctx, x, y) { ctx.save(); ctx.translate(-this.x, -this.y); this.img.render(ctx, x, y); ctx.restore(); }; CropImage.prototype.equals = function(other, aUnionFind) { return ( other instanceof CropImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.width == other.width && this.height == other.height && this.x == other.x && this.y == other.y && plt.baselib.equality.equals(this.img, other.img, aUnionFind) ); }; ////////////////////////////////////////////////////////////////////// // FrameImage: factor factor image -> image // Stick a frame around the image var FrameImage = function(img) { BaseImage.call(this, Math.floor(img.getWidth()/ 2), Math.floor(img.getHeight()/ 2)); this.img = img; this.width = img.getWidth(); this.height = img.getHeight(); }; FrameImage.prototype = heir(BaseImage.prototype); // scale the context, and pass it to the image's render function FrameImage.prototype.render = function(ctx, x, y) { ctx.save(); this.img.render(ctx, x, y); ctx.beginPath(); ctx.strokeStyle = "black"; ctx.strokeRect(x, y, this.width, this.height); ctx.closePath(); ctx.restore(); }; FrameImage.prototype.equals = function(other, aUnionFind) { return ( other instanceof FrameImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && plt.baselib.equality.equals(this.img, other.img, aUnionFind) ); }; ////////////////////////////////////////////////////////////////////// // FlipImage: image string -> image // Flip an image either horizontally or vertically var FlipImage = function(img, direction) { this.img = img; this.width = img.getWidth(); this.height = img.getHeight(); this.direction = direction; BaseImage.call(this, img.pinholeX, img.pinholeY); }; FlipImage.prototype = heir(BaseImage.prototype); FlipImage.prototype.render = function(ctx, x, y) { // when flipping an image of dimension M and offset by N across an axis, // we need to translate the canvas by M+2N in the opposite direction ctx.save(); if(this.direction == "horizontal"){ ctx.scale(-1, 1); ctx.translate(-(this.width+2*x), 0); this.img.render(ctx, x, y); } if (this.direction == "vertical"){ ctx.scale(1, -1); ctx.translate(0, -(this.height+2*y)); this.img.render(ctx, x, y); } ctx.restore(); }; FlipImage.prototype.getWidth = function() { return this.width; }; FlipImage.prototype.getHeight = function() { return this.height; }; FlipImage.prototype.equals = function(other, aUnionFind) { return ( other instanceof FlipImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.width == other.width && this.height == other.height && this.direction == other.direction && plt.baselib.equality.equals(this.img, other.img, aUnionFind) ); }; ////////////////////////////////////////////////////////////////////// // RectangleImage: Number Number Mode Color -> Image var RectangleImage = function(width, height, style, color) { BaseImage.call(this, width/2, height/2); this.width = width; this.height = height; this.style = style; this.color = color; }; RectangleImage.prototype = heir(BaseImage.prototype); RectangleImage.prototype.render = function(ctx, x, y) { if (this.style.toString().toLowerCase() == "outline") { ctx.save(); ctx.beginPath(); ctx.strokeStyle = colorString(this.color); ctx.strokeRect(x, y, this.width, this.height); ctx.closePath(); ctx.restore(); } else { ctx.save(); ctx.beginPath(); ctx.fillStyle = colorString(this.color); ctx.fillRect(x, y, this.width, this.height); ctx.closePath(); ctx.restore(); } }; RectangleImage.prototype.getWidth = function() { return this.width; }; RectangleImage.prototype.getHeight = function() { return this.height; }; RectangleImage.prototype.equals = function(other, aUnionFind) { return (other instanceof RectangleImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.width == other.width && this.height == other.height && this.style == other.style && plt.baselib.equality.equals(this.color, other.color, aUnionFind)); }; ////////////////////////////////////////////////////////////////////// // RhombusImage: Number Number Mode Color -> Image var RhombusImage = function(side, angle, style, color) { // sin(angle/2-in-radians) * side = half of base this.width = Math.sin(angle/2 * Math.PI / 180) * side * 2; // cos(angle/2-in-radians) * side = half of height this.height = Math.abs(Math.cos(angle/2 * Math.PI / 180)) * side * 2; BaseImage.call(this, this.width/2, this.height/2); this.side = side; this.angle = angle; this.style = style; this.color = color; }; RhombusImage.prototype = heir(BaseImage.prototype); RhombusImage.prototype.render = function(ctx, x, y) { ctx.save(); ctx.beginPath(); // if angle < 180 start at the top of the canvas, otherwise start at the bottom ctx.moveTo(x+this.getWidth()/2, y); ctx.lineTo(x+this.getWidth(), y+this.getHeight()/2); ctx.lineTo(x+this.getWidth()/2, y+this.getHeight()); ctx.lineTo(x, y+this.getHeight()/2); ctx.closePath(); if (this.style.toString().toLowerCase() == "outline") { ctx.strokeStyle = colorString(this.color); ctx.stroke(); } else { ctx.fillStyle = colorString(this.color); ctx.fill(); } ctx.restore(); }; RhombusImage.prototype.getWidth = function() { return this.width; }; RhombusImage.prototype.getHeight = function() { return this.height; }; RhombusImage.prototype.equals = function(other, aUnionFind) { return (other instanceof RhombusImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.side == other.side && this.angle == other.angle && this.style == other.style && plt.baselib.equality.equals(this.color, other.color, aUnionFind)); }; ////////////////////////////////////////////////////////////////////// var ImageDataImage = function(imageData) { BaseImage.call(this, 0, 0); this.imageData = imageData; this.width = imageData.width; this.height = imageData.height; }; ImageDataImage.prototype = heir(BaseImage.prototype); ImageDataImage.prototype.render = function(ctx, x, y) { ctx.putImageData(this.imageData, x, y); }; ImageDataImage.prototype.getWidth = function() { return this.width; }; ImageDataImage.prototype.getHeight = function() { return this.height; }; ImageDataImage.prototype.equals = function(other, aUnionFind) { return (other instanceof ImageDataImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY); }; ////////////////////////////////////////////////////////////////////// // PolygonImage: Number Count Step Mode Color -> Image // // See http://www.algebra.com/algebra/homework/Polygons/Inscribed-and-circumscribed-polygons.lesson // the polygon is inscribed in a circle, whose radius is length/2sin(pi/count) // another circle is inscribed in the polygon, whose radius is length/2tan(pi/count) // rotate a 3/4 quarter turn plus half the angle length to keep bottom base level var PolygonImage = function(length, count, step, style, color) { this.aVertices = []; var xMax = 0; var yMax = 0; var xMin = 0; var yMin = 0; this.outerRadius = Math.floor(length/(2*Math.sin(Math.PI/count))); this.innerRadius = Math.floor(length/(2*Math.tan(Math.PI/count))); var adjust = (3*Math.PI/2)+Math.PI/count; // rotate around outer circle, storing x,y pairs as vertices // keep track of mins and maxs var radians = 0; for(var i = 0; i < count; i++) { // rotate to the next vertex (skipping by this.step) radians = radians + (step*2*Math.PI/count); var v = { x: this.outerRadius*Math.cos(radians-adjust), y: this.outerRadius*Math.sin(radians-adjust) }; if(v.x < xMin) xMin = v.x; if(v.x > xMax) xMax = v.y; if(v.y < yMin) yMin = v.x; if(v.y > yMax) yMax = v.y; this.aVertices.push(v); } // HACK: try to work around handling of non-integer coordinates in CANVAS // by ensuring that the boundaries of the canvas are outside of the vertices for(var i=0; i xMax) xMax = this.aVertices[i].x+1; if(this.aVertices[i].y < yMin) yMin = this.aVertices[i].y-1; if(this.aVertices[i].y > yMax) yMax = this.aVertices[i].y+1; } this.width = Math.floor(xMax-xMin); this.height = Math.floor(yMax-yMin); this.length = length; this.count = count; this.step = step; this.style = style; this.color = color; BaseImage.call(this, Math.floor(this.width/2), Math.floor(this.height/2)); }; PolygonImage.prototype = heir(BaseImage.prototype); // shift all vertices by an offset to put the center of the polygon at the // center of the canvas. Even-sided polygons highest points are in line with // the innerRadius. Odd-sides polygons highest vertex is on the outerRadius PolygonImage.prototype.render = function(ctx, x, y) { var xOffset = x+Math.round(this.width/2); var yOffset = y+((this.count % 2)? this.outerRadius : this.innerRadius); ctx.save(); ctx.beginPath(); ctx.moveTo(xOffset+this.aVertices[0].x, yOffset+this.aVertices[0].y); for(var i=1; i Image ////////////////////////////////////////////////////////////////////// // TextImage: String Number Color String String String String any/c -> Image var TextImage = function(msg, size, color, face, family, style, weight, underline) { var metrics; this.msg = msg; this.size = size; this.color = color; this.face = face; this.family = family; this.style = (style == "slant")? "oblique" : style; // Racket's "slant" -> CSS's "oblique" this.weight = (weight== "light")? "lighter" : weight; // Racket's "light" -> CSS's "lighter" this.underline = underline; // example: "bold italic 20px 'Times', sans-serif". // Default weight is "normal", face is "Optimer" var canvas = makeCanvas(0, 0); var ctx = canvas.getContext("2d"); this.font = (this.weight + " " + this.style + " " + this.size + "px " + maybeQuote(this.face) + " " + maybeQuote(this.family)); try { ctx.font = this.font; } catch (e) { this.fallbackOnFont(); ctx.font = this.font; } // Defensive: on IE, this can break. try { metrics = ctx.measureText(msg); this.width = metrics.width; this.height = Number(this.size); } catch(e) { this.fallbackOnFont(); } BaseImage.call(this, Math.round(this.width/2), 0);// weird pinhole settings needed for "baseline" alignment } TextImage.prototype = heir(BaseImage.prototype); TextImage.prototype.fallbackOnFont = function() { // Defensive: if the browser doesn't support certain features, we // reduce to a smaller feature set and try again. this.font = this.size + "px " + maybeQuote(this.family); var canvas = makeCanvas(0, 0); var ctx = canvas.getContext("2d"); ctx.font = this.font; var metrics = ctx.measureText(this.msg); this.width = metrics.width; // KLUDGE: I don't know how to get at the height. this.height = Number(this.size);//ctx.measureText("m").width + 20; }; TextImage.prototype.render = function(ctx, x, y) { ctx.save(); ctx.textAlign = 'left'; ctx.textBaseline= 'top'; ctx.fillStyle = colorString(this.color); ctx.font = this.font; try { ctx.fillText(this.msg, x, y); } catch (e) { this.fallbackOnFont(); ctx.font = this.font; ctx.fillText(this.msg, x, y); } if(this.underline){ ctx.beginPath(); ctx.moveTo(x, y+this.size); // we use this.size, as it is more accurate for underlining than this.height ctx.lineTo(x+this.width, y+this.size); ctx.closePath(); ctx.strokeStyle = colorString(this.color); ctx.stroke(); } ctx.restore(); }; TextImage.prototype.getBaseline = function() { return this.size; }; TextImage.prototype.equals = function(other, aUnionFind) { return (other instanceof TextImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.msg == other.msg && this.size == other.size && this.face == other.face && this.family == other.family && this.style == other.style && this.weight == other.weight && this.underline == other.underline && plt.baselib.equality.equals(this.color, other.color, aUnionFind) && this.font == other.font); }; ////////////////////////////////////////////////////////////////////// // StarImage: fixnum fixnum fixnum color -> image var StarImage = function(points, outer, inner, style, color) { BaseImage.call(this, Math.max(outer, inner), Math.max(outer, inner)); this.points = points; this.outer = outer; this.inner = inner; this.style = style; this.color = color; this.radius = Math.max(this.inner, this.outer); this.width = this.radius*2; this.height = this.radius*2; }; StarImage.prototype = heir(BaseImage.prototype); var oneDegreeAsRadian = Math.PI / 180; // render: context fixnum fixnum -> void // Draws a star on the given context. // Most of this code here adapted from the Canvas tutorial at: // http://developer.apple.com/safari/articles/makinggraphicswithcanvas.html StarImage.prototype.render = function(ctx, x, y) { ctx.save(); ctx.beginPath(); for( var pt = 0; pt < (this.points * 2) + 1; pt++ ) { var rads = ( ( 360 / (2 * this.points) ) * pt ) * oneDegreeAsRadian - 0.5; var radius = ( pt % 2 == 1 ) ? this.outer : this.inner; ctx.lineTo(x + this.radius + ( Math.sin( rads ) * radius ), y + this.radius + ( Math.cos( rads ) * radius ) ); } ctx.closePath(); if (this.style.toString().toLowerCase() == "outline") { ctx.strokeStyle = colorString(this.color); ctx.stroke(); } else { ctx.fillStyle = colorString(this.color); ctx.fill(); } ctx.restore(); }; StarImage.prototype.equals = function(other, aUnionFind) { return (other instanceof StarImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.points == other.points && this.outer == other.outer && this.inner == other.inner && this.style == other.style && plt.baselib.equality.equals(this.color, other.color, aUnionFind)); }; ///////////////////////////////////////////////////////////////////// //TriangleImage: Number Number Mode Color -> Image var TriangleImage = function(side, angle, style, color) { // sin(angle/2-in-radians) * side = half of base this.width = Math.sin(angle/2 * Math.PI / 180) * side * 2; // cos(angle/2-in-radians) * side = height of altitude this.height = Math.floor(Math.abs(Math.cos(angle/2 * Math.PI / 180)) * side); BaseImage.call(this, Math.floor(this.width/2), Math.floor(this.height/2)); this.side = side; this.angle = angle; this.style = style; this.color = color; } TriangleImage.prototype = heir(BaseImage.prototype); TriangleImage.prototype.render = function(ctx, x, y) { var width = this.getWidth(); var height = this.getHeight(); ctx.save(); ctx.beginPath(); // if angle < 180 start at the top of the canvas, otherwise start at the bottom if(this.angle < 180){ ctx.moveTo(x+width/2, y); ctx.lineTo(x, y+height); ctx.lineTo(x+width, y+height); } else { ctx.moveTo(x+width/2, y+height); ctx.lineTo(x, y); ctx.lineTo(x+width, y); } ctx.closePath(); if (this.style.toString().toLowerCase() == "outline") { ctx.strokeStyle = colorString(this.color); ctx.stroke(); } else { ctx.fillStyle = colorString(this.color); ctx.fill(); } ctx.restore(); }; TriangleImage.prototype.equals = function(other, aUnionFind) { return (other instanceof TriangleImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.side == other.side && this.angle == other.angle && this.style == other.style && plt.baselib.equality.equals(this.color, other.color, aUnionFind)); }; ///////////////////////////////////////////////////////////////////// //RightTriangleImage: Number Number Mode Color -> Image var RightTriangleImage = function(side1, side2, style, color) { this.width = side1; this.height = side2; BaseImage.call(this, Math.floor(this.width/2), Math.floor(this.height/2)); this.side1 = side1; this.side2 = side2; this.style = style; this.color = color; } RightTriangleImage.prototype = heir(BaseImage.prototype); RightTriangleImage.prototype.render = function(ctx, x, y) { var width = this.getWidth(); var height = this.getHeight(); ctx.save(); ctx.beginPath(); ctx.moveTo(x, y+this.side2); ctx.lineTo(x+this.side1, y+this.side2); ctx.lineTo(x, y); ctx.closePath(); if (this.style.toString().toLowerCase() == "outline") { ctx.strokeStyle = colorString(this.color); ctx.stroke(); } else { ctx.fillStyle = colorString(this.color); ctx.fill(); } ctx.restore(); }; RightTriangleImage.prototype.equals = function(other, aUnionFind) { return (other instanceof RightTriangleImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.side1 == other.side1 && this.side2 == other.side2 && this.style == other.style && plt.baselib.equality.equals(this.color, other.color, aUnionFind)); }; ////////////////////////////////////////////////////////////////////// //Ellipse : Number Number Mode Color -> Image var EllipseImage = function(width, height, style, color) { BaseImage.call(this, Math.floor(width/2), Math.floor(height/2)); this.width = width; this.height = height; this.style = style; this.color = color; }; EllipseImage.prototype = heir(BaseImage.prototype); EllipseImage.prototype.render = function(ctx, aX, aY) { ctx.save(); ctx.beginPath(); // Most of this code is taken from: // http://webreflection.blogspot.com/2009/01/ellipse-and-circle-for-canvas-2d.html var hB = (this.width / 2) * .5522848, vB = (this.height / 2) * .5522848, eX = aX + this.width, eY = aY + this.height, mX = aX + this.width / 2, mY = aY + this.height / 2; ctx.moveTo(aX, mY); ctx.bezierCurveTo(aX, mY - vB, mX - hB, aY, mX, aY); ctx.bezierCurveTo(mX + hB, aY, eX, mY - vB, eX, mY); ctx.bezierCurveTo(eX, mY + vB, mX + hB, eY, mX, eY); ctx.bezierCurveTo(mX - hB, eY, aX, mY + vB, aX, mY); ctx.closePath(); if (this.style.toString().toLowerCase() == "outline") { ctx.strokeStyle = colorString(this.color); ctx.stroke(); } else { ctx.fillStyle = colorString(this.color); ctx.fill(); } ctx.restore(); }; EllipseImage.prototype.equals = function(other, aUnionFind) { return (other instanceof EllipseImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.width == other.width && this.height == other.height && this.style == other.style && plt.baselib.equality.equals(this.color, other.color, aUnionFind)); }; ////////////////////////////////////////////////////////////////////// //Line: Number Number Color Boolean -> Image var LineImage = function(x, y, color, normalPinhole) { if (x >= 0) { if (y >= 0) { BaseImage.call(this, 0, 0); } else { BaseImage.call(this, 0, -y); } } else { if (y >= 0) { BaseImage.call(this, -x, 0); } else { BaseImage.call(this, -x, -y); } } this.x = x; this.y = y; this.color = color; this.width = Math.abs(x) + 1; this.height = Math.abs(y) + 1; // put the pinhle in the center of the image if(normalPinhole){ this.pinholeX = this.width/2; this.pinholeY = this.height/2; } } LineImage.prototype = heir(BaseImage.prototype); LineImage.prototype.render = function(ctx, xstart, ystart) { ctx.save(); ctx.beginPath(); ctx.strokeStyle = colorString(this.color); if (this.x >= 0) { if (this.y >= 0) { ctx.moveTo(xstart, ystart); ctx.lineTo((xstart + this.x), (ystart + this.y)); } else { ctx.moveTo(xstart, ystart + (-this.y)); ctx.lineTo(xstart + this.x, ystart); } } else { if (this.y >= 0) { ctx.moveTo(xstart + (-this.x), ystart); ctx.lineTo(xstart, (ystart + this.y)); } else { ctx.moveTo(xstart + (-this.x), ystart + (-this.y)); ctx.lineTo(xstart, ystart); } } ctx.closePath(); ctx.stroke(); ctx.restore(); }; LineImage.prototype.equals = function(other, aUnionFind) { return (other instanceof LineImage && this.pinholeX == other.pinholeX && this.pinholeY == other.pinholeY && this.x == other.x && this.y == other.y && plt.baselib.equality.equals(this.color, other.color, aUnionFind)); }; var imageToColorList = function(img) { var width = img.getWidth(), height = img.getHeight(), canvas = makeCanvas(width, height), ctx = canvas.getContext("2d"), imageData, data, i, r, g, b, a; img.render(ctx, 0, 0); imageData = ctx.getImageData(0, 0, width, height); data = imageData.data; var colors = []; for (i = 0 ; i < data.length; i += 4) { r = data[i]; g = data[i+1]; b = data[i+2]; a = data[i+3]; colors.push(makeColor(r, g, b, a)); } return plt.baselib.lists.makeList.apply(null, colors); } var colorListToImage = function(listOfColors, width, height, pinholeX, pinholeY) { var canvas = makeCanvas(jsnums.toFixnum(width), jsnums.toFixnum(height)), ctx = canvas.getContext("2d"), imageData = ctx.createImageData(jsnums.toFixnum(width), jsnums.toFixnum(height)), data = imageData.data, aColor, i = 0; while (listOfColors !== plt.baselib.lists.EMPTY) { aColor = listOfColors.first; data[i] = jsnums.toFixnum(colorRed(aColor)); data[i+1] = jsnums.toFixnum(colorGreen(aColor)); data[i+2] = jsnums.toFixnum(colorBlue(aColor)); data[i+3] = jsnums.toFixnum(colorAlpha(aColor)); i += 4; listOfColors = listOfColors.rest; }; return makeImageDataImage(imageData); }; var makeSceneImage = function(width, height, children, withBorder) { return new SceneImage(width, height, children, withBorder); }; var makeCircleImage = function(radius, style, color) { return new EllipseImage(2*radius, 2*radius, style, color); }; var makeStarImage = function(points, outer, inner, style, color) { return new StarImage(points, outer, inner, style, color); }; var makeRectangleImage = function(width, height, style, color) { return new RectangleImage(width, height, style, color); }; var makeRhombusImage = function(side, angle, style, color) { return new RhombusImage(side, angle, style, color); }; var makePolygonImage = function(length, count, step, style, color) { return new PolygonImage(length, count, step, style, color); }; var makeSquareImage = function(length, style, color) { return new RectangleImage(length, length, style, color); }; var makeRightTriangleImage = function(side1, side2, style, color) { return new RightTriangleImage(side1, side2, style, color); }; var makeTriangleImage = function(side, angle, style, color) { return new TriangleImage(side, angle, style, color); }; var makeEllipseImage = function(width, height, style, color) { return new EllipseImage(width, height, style, color); }; var makeLineImage = function(x, y, color, normalPinhole) { return new LineImage(x, y, color, normalPinhole); }; var makeOverlayImage = function(img1, img2, X, Y) { return new OverlayImage(img1, img2, X, Y); }; var makeRotateImage = function(angle, img) { return new RotateImage(angle, img); }; var makeScaleImage = function(xFactor, yFactor, img) { return new ScaleImage(xFactor, yFactor, img); }; var makeCropImage = function(x, y, width, height, img) { return new CropImage(x, y, width, height, img); }; var makeFrameImage = function(img) { return new FrameImage(img); }; var makeFlipImage = function(img, direction) { return new FlipImage(img, direction); }; var makeTextImage = function(msg, size, color, face, family, style, weight, underline) { return new TextImage(msg, size, color, face, family, style, weight, underline); }; var makeImageDataImage = function(imageData) { return new ImageDataImage(imageData); }; var makeFileImage = function(path, rawImage) { return FileImage.makeInstance(path, rawImage); }; var makeVideoImage = function(path, rawVideo) { return VideoImage.makeInstance(path, rawVideo); }; var isSceneImage = function(x) { return x instanceof SceneImage; }; var isCircleImage = function(x) { return x instanceof EllipseImage && x.width === x.height; }; var isStarImage = function(x) { return x instanceof StarImage; }; var isRectangleImage=function(x) { return x instanceof RectangleImage; }; var isPolygonImage = function(x) { return x instanceof PolygonImage; }; var isRhombusImage = function(x) { return x instanceof RhombusImage; }; var isSquareImage = function(x) { return x instanceof SquareImage; }; var isTriangleImage= function(x) { return x instanceof TriangleImage; }; var isRightTriangleImage = function(x) { return x instanceof RightTriangleImage; }; var isEllipseImage = function(x) { return x instanceof EllipseImage; }; var isLineImage = function(x) { return x instanceof LineImage; }; var isOverlayImage = function(x) { return x instanceof OverlayImage; }; var isRotateImage = function(x) { return x instanceof RotateImage; }; var isScaleImage = function(x) { return x instanceof ScaleImage; }; var isCropImage = function(x) { return x instanceof CropImage; }; var isFrameImage = function(x) { return x instanceof FrameImage; }; var isFlipImage = function(x) { return x instanceof FlipImage; }; var isTextImage = function(x) { return x instanceof TextImage; }; var isFileImage = function(x) { return x instanceof FileImage; }; var isFileVideo = function(x) { return x instanceof FileVideo; }; /////////////////////////////////////////////////////////////// // Exports // These functions are available for direct access without the typechecks // of the Racket-exposed functions. EXPORTS.makeCanvas = makeCanvas; EXPORTS.BaseImage = BaseImage; EXPORTS.SceneImage = SceneImage; EXPORTS.FileImage = FileImage; EXPORTS.VideoImage = VideoImage; EXPORTS.OverlayImage = OverlayImage; EXPORTS.RotateImage = RotateImage; EXPORTS.ScaleImage = ScaleImage; EXPORTS.CropImage = CropImage; EXPORTS.FrameImage = FrameImage; EXPORTS.FlipImage = FlipImage; EXPORTS.RectangleImage = RectangleImage; EXPORTS.RhombusImage = RhombusImage; EXPORTS.ImageDataImage = ImageDataImage; EXPORTS.PolygonImage = PolygonImage; EXPORTS.TextImage = TextImage; EXPORTS.StarImage = StarImage; EXPORTS.TriangleImage = TriangleImage; EXPORTS.RightTriangleImage = RightTriangleImage; EXPORTS.EllipseImage = EllipseImage; EXPORTS.LineImage = LineImage; EXPORTS.StarImage = StarImage; EXPORTS.colorDb = colorDb; EXPORTS.makeSceneImage = makeSceneImage; EXPORTS.makeCircleImage = makeCircleImage; EXPORTS.makeStarImage = makeStarImage; EXPORTS.makeRectangleImage = makeRectangleImage; EXPORTS.makeRhombusImage = makeRhombusImage; EXPORTS.makePolygonImage = makePolygonImage; EXPORTS.makeSquareImage = makeSquareImage; EXPORTS.makeRightTriangleImage = makeRightTriangleImage; EXPORTS.makeTriangleImage = makeTriangleImage; EXPORTS.makeEllipseImage = makeEllipseImage; EXPORTS.makeLineImage = makeLineImage; EXPORTS.makeOverlayImage = makeOverlayImage; EXPORTS.makeRotateImage = makeRotateImage; EXPORTS.makeScaleImage = makeScaleImage; EXPORTS.makeCropImage = makeCropImage; EXPORTS.makeFrameImage = makeFrameImage; EXPORTS.makeFlipImage = makeFlipImage; EXPORTS.makeTextImage = makeTextImage; EXPORTS.makeImageDataImage = makeImageDataImage; EXPORTS.makeFileImage = makeFileImage; EXPORTS.makeVideoImage = makeVideoImage; EXPORTS.imageToColorList = imageToColorList; EXPORTS.colorListToImage = colorListToImage; EXPORTS.isImage = isImage; EXPORTS.isScene = isScene; EXPORTS.isColorOrColorString = isColorOrColorString; EXPORTS.isAngle = isAngle; EXPORTS.isSideCount = isSideCount; EXPORTS.isStepCount = isStepCount; EXPORTS.isPointsCount = isPointsCount; EXPORTS.isSceneImage = isSceneImage; EXPORTS.isCircleImage = isCircleImage; EXPORTS.isStarImage = isStarImage; EXPORTS.isRectangleImage = isRectangleImage; EXPORTS.isPolygonImage = isPolygonImage; EXPORTS.isRhombusImage = isRhombusImage; EXPORTS.isSquareImage = isSquareImage; EXPORTS.isTriangleImage = isTriangleImage; EXPORTS.isRightTriangleImage = isRightTriangleImage; EXPORTS.isEllipseImage = isEllipseImage; EXPORTS.isLineImage = isLineImage; EXPORTS.isOverlayImage = isOverlayImage; EXPORTS.isRotateImage = isRotateImage; EXPORTS.isScaleImage = isScaleImage; EXPORTS.isCropImage = isCropImage; EXPORTS.isFrameImage = isFrameImage; EXPORTS.isFlipImage = isFlipImage; EXPORTS.isTextImage = isTextImage; EXPORTS.isFileImage = isFileImage; EXPORTS.isFileVideo = isFileVideo; EXPORTS.makeColor = makeColor; EXPORTS.isColor = isColor; EXPORTS.colorRed = colorRed; EXPORTS.colorGreen = colorGreen; EXPORTS.colorBlue = colorBlue; EXPORTS.colorAlpha = colorAlpha;