From 25a3009811a24cf40d8493221a8fbfa6cecaac67 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Sun, 27 Dec 2020 12:43:20 -0500 Subject: [PATCH 01/12] WIP commit to eliminate the .vertices arrays from images. This commit changes the image implementation in a few ways: - Because .vertices are going away whenever possible, we no longer have to render images solely in Quadrant I - Introduces a BoundingBox object to track bounding boxes of images - Computing the bounding box of an image can be done more precisely (especially for ellipses), by passing the current transformation *down* the image tree, and computing the bounding box for the particular transformation as needed. These bounding boxes are memoized for the identity transformation, so that we know what the bounding box is for a standalone image - Rendering images to canvases shifts slightly, to translate the origin of the canvas to the center, and then again to make that match the actual middle of the image. This transformation eliminates the dependency on Quadrant I Not all tests pass yet, some pinholes are still wrong, and possibly not all image primitives work yet. I *think* this should improve performance, but it's possible the additional bounding-box computations slow things down. --- src/web/js/trove/image-lib.js | 709 +++++++++++------- src/web/js/trove/make-image.js | 2 +- .../pyret-programs/images/image-tests.arr | 2 +- 3 files changed, 438 insertions(+), 275 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index 79abf0f5f..79f5cf406 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -119,7 +119,11 @@ var c = new C(); for (property in obj) { if (hasOwnProperty.call(obj, property)) { - c[property] = obj[property]; + if (obj[property] instanceof DOMPoint) { + c[property] = DOMPoint.fromPoint(obj[property]); + } else { + c[property] = obj[property]; + } } } return c; @@ -247,6 +251,8 @@ return false; if (typeof(thing.getBaseline) !== 'function') return false; + if (typeof(thing.centerPinhole) !== 'function') + return false; if (typeof(thing.updatePinhole) !== 'function') return false; if (typeof(thing.offsetPinhole) !== 'function') @@ -261,6 +267,19 @@ }; var annImage = RUNTIME.makePrimitiveAnn("Image", checkImagePred); + + const imageBBs = new WeakMap(); + // Computes the tight bounding box of the given image. + // Stores a weak-map cache of the bounding boxes, so that if an image goes away, + // the bounding box does too. + function getBB(img) { + var bb = imageBBs.get(img); + if (bb !== undefined) { return bb; } + bb = img.computeBB(new DOMMatrix()); + imageBBs.set(img, bb); + return bb; + }; + // given two arrays of {x,y} structs, determine their equivalence var verticesEqual = function(v1, v2){ if(v1.length !== v2.length){ return false; } @@ -299,53 +318,129 @@ }) } + function BoundingBox(topLeft, botRight) { + this.topLeft = DOMPoint.fromPoint(topLeft); + this.botRight = DOMPoint.fromPoint(botRight === undefined ? topLeft : botRight); + }; + BoundingBox.prototype.getWidth = function() { + return Math.round(Math.abs(this.botRight.x - this.topLeft.x)); + }; + BoundingBox.prototype.getHeight = function() { + return Math.round(Math.abs(this.botRight.y - this.topLeft.y)); + }; + BoundingBox.prototype.getCenterX = function() { + return (this.topLeft.x + this.botRight.x) / 2; + }; + BoundingBox.prototype.getCenterY = function() { + return (this.topLeft.y + this.botRight.y) / 2; + }; + // Produces a new bounding box combining this and that + BoundingBox.prototype.union = function(that) { + return new BoundingBox(this.topLeft, this.botRight).merge(that); + }; + // Expands this bounding box to include the given box + BoundingBox.prototype.merge = function(that) { + this.addPoint(that.topLeft); + this.addPoint(that.botRight); + return this; + }; + // Expands this bounding box to include the given point + BoundingBox.prototype.addPoint = function(pt) { + this.topLeft.x = Math.min(this.topLeft.x, pt.x), + this.topLeft.y = Math.min(this.topLeft.y, pt.y), + this.botRight.x = Math.max(this.botRight.x, pt.x), + this.botRight.y = Math.max(this.botRight.y, pt.y); + return this; + }; + // Translates its bounding box around its middle to produce a new bounding box + BoundingBox.prototype.center = function() { + var halfW = this.getWidth() / 2; + var halfH = this.getHeight() / 2; + return new BoundingBox(new DOMPoint(this.topLeft.x - halfW, this.topLeft.y - halfH), + new DOMPoint(this.botRight.x - halfW, this.botRight.y - halfH)); + }; + // Transforms this bounding box by the given transformation to produce a new bounding box + BoundingBox.prototype.transform = function(tx) { + return new BoundingBox(this.topLeft.matrixTransform(tx)) + .addPoint(this.botRight.matrixTransform(tx)) + .addPoint(new DOMPoint(this.topLeft.x, this.botRight.y).matrixTransform(tx)) + .addPoint(new DOMPoint(this.botRight.x, this.topLeft.y).matrixTransform(tx)); + }; + // Base class for all images. var BaseImage = function() {}; + BaseImage.prototype.computeBB = function(tx) { + var topLeft = new DOMPoint(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + var botRight = new DOMPoint(Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER); + if (this.vertices === undefined) { + topLeft.x = 0; + topLeft.y = 0; + botRight.x = 0; + botRight.y = 0; + return new BoundingBox(topLeft, botRight); + } else { + var ans = new BoundingBox(topLeft, botRight); + // console.log("bb", JSON.stringify(ans)); + for (let v of this.vertices) { + // console.log("v", v.toJSON(), "tx(v)", v.matrixTransform(tx).toJSON()); + ans.addPoint(v.matrixTransform(tx)); + // console.log("bb", JSON.stringify(ans)); + } + return ans; + } + }; + BaseImage.prototype.updatePinhole = function(x, y) { var aCopy = clone(this); - aCopy.pinholeX = x; - aCopy.pinholeY = y; + aCopy.pinhole = new DOMPoint(x, y); return aCopy; }; BaseImage.prototype.offsetPinhole = function(dx, dy) { var aCopy = clone(this); - aCopy.pinholeX += dx; - aCopy.pinholeY += dy; + aCopy.pinhole.x += dx; + aCopy.pinhole.y += dy; + return aCopy; + }; + + BaseImage.prototype.centerPinhole = function() { + var aCopy = clone(this); + var bb = this.computeBB(new DOMMatrix()) + aCopy.pinhole = new DOMPoint(bb.getCenterX(), bb.getCenterY()); return aCopy; }; BaseImage.prototype.getHeight = function(){ - return Math.round(this.height); + return getBB(this).getHeight(); }; BaseImage.prototype.getWidth = function(){ - return Math.round(this.width); + return getBB(this).getWidth(); }; BaseImage.prototype.getBaseline = function(){ - return this.alphaBaseline !== undefined ? this.alphaBaseline : Math.round(this.height); + return this.alphaBaseline !== undefined ? this.alphaBaseline : Math.round(this.getHeight()); }; BaseImage.prototype.getPinholeX = function() { - return this.pinholeX !== undefined ? this.pinholeX : 0; - } + return this.pinhole !== undefined ? this.pinhole.x : 0; + }; BaseImage.prototype.getPinholeY = function() { - return this.pinholeY !== undefined ? this.pinholeY : 0; - } + return this.pinhole !== undefined ? this.pinhole.y : 0; + }; - // return the vertex array if it exists, otherwise make one using height and width - BaseImage.prototype.getVertices = function(){ - if(this.vertices){ return this.vertices; } - else{ return [{x:0 , y: 0}, - {x: this.width, y: 0}, - {x: 0, y: this.height}, - {x: this.width, y: this.height}]; } + BaseImage.prototype.getCenterX = function() { + return getBB(this).getCenterX(); }; + BaseImage.prototype.getCenterY = function() { + return getBB(this).getCenterY(); + }; + + // render: context: -> void // Render the image in its local coordinate system // (i.e., (0,0) maps to the origin of the context) @@ -372,8 +467,13 @@ vertices = this.vertices; } else { // find the midpoint of the xs and ys from vertices - var midX = findWidth(this.vertices) / 2; - var midY = findHeight(this.vertices) / 2; + var midX = 0, midY = 0; + for (let v of this.vertices) { + midX += v.x; + midY += v.y; + } + midX /= 2; + midY /= 2; // compute 0.5px offsets to ensure that we draw on the pixel // and not the pixel boundary @@ -425,6 +525,16 @@ var onAfterAttach = function(event) { // jQuery(canvas).unbind('afterAttach', onAfterAttach); ctx = this.getContext("2d"); + if (false) { // DEBUG: Swap this to true to see the boundaries of the canvas + ctx.save() + ctx.fillStyle = "rgb(220,200,192)"; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + } + ctx.translate(width / 2, height / 2); // move origin to center of canvas + var bb = getBB(that); + // move canvas origin such that bb's center is at canvas center + ctx.translate(- bb.getCenterX(), - bb.getCenterY()); that.render(ctx); }; jQuery(canvas).bind('afterAttach', onAfterAttach); @@ -479,7 +589,12 @@ c2.width + ", " + c2.height + "])"); } var ctx1 = c1.getContext('2d'), ctx2 = c2.getContext('2d'); + var bb1 = getBB(this), bb2 = getBB(other); + ctx1.translate(w1 / 2, h1 / 2); + ctx1.translate(- bb1.getCenterX(), - bb1.getCenterY()); this.render(ctx1); + ctx2.translate(w2 / 2, h2 / 2); + ctx2.translate(- bb2.getCenterX(), - bb2.getCenterY()); other.render(ctx2); try{ var data1 = ctx1.getImageData(0, 0, w1, h1), @@ -513,8 +628,13 @@ if(c1.width !== c2.width || c1.height !== c2.height){ return false;} try{ var ctx1 = c1.getContext('2d'), ctx2 = c2.getContext('2d'); + var bb1 = getBB(this), bb2 = getBB(other); ctx1.isEqualityTest = true; + ctx1.translate(c1.width / 2, c1.height / 2); + ctx1.translate(- bb1.getCenterX(), - bb1.getCenterY()); ctx2.isEqualityTest = true; + ctx2.translate(c2.width / 2, c2.height / 2); + ctx2.translate(- bb2.getCenterX(), - bb2.getCenterY()); this.render(ctx1); other.render(ctx2); // create temporary canvases var slice1 = document.createElement('canvas').getContext('2d'), @@ -562,22 +682,24 @@ this.children = children; // arrayof [image, number, number] this.withBorder = withBorder; this.color = color; - this.pinholeX = width / 2; - this.pinholeY = height / 2; + this.pinhole = new DOMPoint(width / 2, height / 2); this.ariaText = " scene that is "+width+" by "+height+". children are: "; this.ariaText += children.map(function(c,i){ return "child "+(i+1)+": "+c[0].ariaText+", positioned at "+c[1]+","+c[2]+" "; }).join(". "); }; SceneImage.prototype = heir(BaseImage.prototype); + SceneImage.prototype.computeBB = function(tx) { + return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).transform(tx); + }; // 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.getWidth()/2, - y - anImage.getHeight()/2]]), + x - anImage.getCenterX(), + y - anImage.getCenterY()]]), this.withBorder, this.color); }; @@ -592,6 +714,7 @@ ctx.restore(); // save the context, reset the path, and clip to the path around the scene edge ctx.save(); + //ctx.translate(-this.width / 2, -this.height / 2); // SceneImage coordinates are at the corner ctx.beginPath(); ctx.rect(0, 0, this.width, this.height); ctx.clip(); @@ -647,8 +770,7 @@ this.isLoaded = true; self.width = self.img.width; self.height = self.img.height; - self.pinholeX = self.width / 2; - self.pinholeY = self.height / 2; + self.pinhole = new DOMPoint(self.width / 2, self.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 @@ -659,8 +781,7 @@ self.isLoaded = true; self.width = self.img.width; self.height = self.img.height; - self.pinholeX = self.width / 2; - self.pinholeY = self.height / 2; + self.pinhole = new DOMPoint(self.width / 2, self.height / 2); }; this.img.onerror = function(e) { self.img.onerror = ""; @@ -670,6 +791,9 @@ } } FileImage.prototype = heir(BaseImage.prototype); + FileImage.prototype.computeBB = function(tx) { + return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).transform(tx); + }; var imageCache = {}; FileImage.makeInstance = function(path, rawImage) { @@ -780,9 +904,13 @@ this.imageData= imageData; this.width = imageData.width; this.height = imageData.height; + this.pinhole = new DOMPoint(this.width / 2, this.height / 2); }; ImageDataImage.prototype = heir(BaseImage.prototype); + ImageDataImage.prototype.computeBB = function(tx) { + return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).transform(tx); + }; ImageDataImage.prototype.render = function(ctx) { // Simply using putImageData on ctx would ignore the current transformation matrix, @@ -800,6 +928,44 @@ var OverlayImage = function(img1, placeX1, placeY1, offsetX, offsetY, img2, placeX2, placeY2) { BaseImage.call(this); + // To find where to place the two images relative to one another + // start in a coordinate system with origin at centers. + var x1 = 0, y1 = 0, x2 = 0, y2 = 0; + var anchor1, anchor2; + + // compute the x1/y1 and x2/y2 offsets, to translate the target points to a common origin + switch(placeX1.toLowerCase()) { + case "left": x1 += img1.getWidth() / 2; anchor1 = "-left"; break; + case "middle": x1 -= 0; anchor1 = "-middle"; break; + case "pinhole": x1 -= img1.getPinholeX(); anchor1 = "-pinhole"; break; + case "right": x1 -= img1.getWidth() / 2; anchor1 = "-right"; break; + default: throw new Error("Unknown XPlace option for image 1: " + placeX1); + } + switch(placeY1.toLowerCase()) { + case "top": y1 += img1.getHeight() / 2; anchor1 = "top" + anchor1; break; + case "center": y1 -= 0; anchor1 = "center" + anchor1; break; + case "pinhole": y1 -= img1.getPinholeY(); anchor1 = "pinhole" + anchor1; break; + case "baseline": y1 -= img1.getBaseline() - img1.getHeight() / 2; anchor1 = "baseline" + anchor1; break; + case "bottom": y1 -= img1.getHeight() / 2; anchor1 = "bottom" + anchor1; break; + default: throw new Error("Unknown YPlace option for image 1: " + placeY1); + } + switch(placeX2.toLowerCase()) { + case "left": x2 += img2.getWidth() / 2; anchor2 = "-left"; break; + case "middle": x2 -= 0; anchor2 = "-middle"; break; + case "pinhole": x2 -= img2.getPinholeX(); anchor2 = "-pinhole"; break; + case "right": x2 -= img2.getWidth() / 2; anchor2 = "-right"; break; + default: throw new Error("Unknown XPlace option for image 2: " + placeX2); + } + switch(placeY2.toLowerCase()) { + case "top": y2 += img2.getHeight() / 2; anchor2 = "top" + anchor2; break; + case "center": y2 -= 0; anchor2 = "center" + anchor2; break; + case "pinhole": y2 -= img2.getPinholeY(); anchor2 = "pinhole" + anchor2; break; + case "baseline": y2 -= img2.getBaseline() - img2.getHeight() / 2; anchor2 = "baseline" + anchor2; break; + case "bottom": y2 -= img2.getHeight() / 2; anchor2 = "bottom" + anchor2; break; + default: throw new Error("Unknown YPlace option for image 2: " + placeY2); + } + + /* // To find where to place the two images relative to one another // start in a coordinate system with origin at top/left corners var x1 = 0, y1 = 0, x2 = 0, y2 = 0; @@ -808,58 +974,62 @@ // compute the x1/y1 and x2/y2 offsets, relative to the top/left of img1/img2: switch(placeX1.toLowerCase()) { case "left": x1 -= 0; anchor1 = "-left"; break; - case "middle": x1 -= img1.width / 2; anchor1 = "-middle"; break; - case "pinhole": x1 -= img1.pinholeX; anchor1 = "-pinhole"; break; - case "right": x1 -= img1.width; anchor1 = "-right"; break; + case "middle": x1 -= img1.getWidth() / 2; anchor1 = "-middle"; break; + case "pinhole": x1 -= img1.getPinholeX(); anchor1 = "-pinhole"; break; + case "right": x1 -= img1.getWidth(); anchor1 = "-right"; break; default: throw new Error("Unknown XPlace option for image 1: " + placeX1); } switch(placeY1.toLowerCase()) { case "top": y1 -= 0; anchor1 = "top" + anchor1; break; - case "center": y1 -= img1.height / 2; anchor1 = "center" + anchor1; break; - case "pinhole": y1 -= img1.pinholeY; anchor1 = "pinhole" + anchor1; break; + case "center": y1 -= img1.getHeight() / 2; anchor1 = "center" + anchor1; break; + case "pinhole": y1 -= img1.getPinholeY(); anchor1 = "pinhole" + anchor1; break; case "baseline": y1 -= img1.getBaseline(); anchor1 = "baseline" + anchor1; break; - case "bottom": y1 -= img1.height; anchor1 = "bottom" + anchor1; break; + case "bottom": y1 -= img1.getHeight(); anchor1 = "bottom" + anchor1; break; default: throw new Error("Unknown YPlace option for image 1: " + placeY1); } switch(placeX2.toLowerCase()) { case "left": x2 -= 0; anchor2 = "-left"; break; - case "middle": x2 -= img2.width / 2; anchor2 = "-middle"; break; - case "pinhole": x2 -= img2.pinholeX; anchor2 = "-pinhole"; break; - case "right": x2 -= img2.width; anchor2 = "-right"; break; + case "middle": x2 -= img2.getWidth() / 2; anchor2 = "-middle"; break; + case "pinhole": x2 -= img2.getPinholeX(); anchor2 = "-pinhole"; break; + case "right": x2 -= img2.getWidth(); anchor2 = "-right"; break; default: throw new Error("Unknown XPlace option for image 2: " + placeX2); } switch(placeY2.toLowerCase()) { case "top": y2 -= 0; anchor2 = "top" + anchor2; break; - case "center": y2 -= img2.height / 2; anchor2 = "center" + anchor2; break; - case "pinhole": y2 -= img2.pinholeY; anchor2 = "pinhole" + anchor2; break; + case "center": y2 -= img2.getHeight() / 2; anchor2 = "center" + anchor2; break; + case "pinhole": y2 -= img2.getPinholeY(); anchor2 = "pinhole" + anchor2; break; case "baseline": y2 -= img2.getBaseline(); anchor2 = "baseline" + anchor2; break; - case "bottom": y2 -= img2.height; anchor2 = "bottom" + anchor2; break; + case "bottom": y2 -= img2.getHeight(); anchor2 = "bottom" + anchor2; break; default: throw new Error("Unknown YPlace option for image 2: " + placeY2); } + */ // Next, offset x2/y2 by the given offsetX/Y x2 += offsetX; y2 += offsetY; - - // Translate both offset pairs by the smaller of the half-dimensions - var xMax = Math.max(img1.width, img2.width); - var yMax = Math.max(img1.height, img2.height); - x1 += xMax; x2 += xMax; - y1 += yMax; y2 += yMax; - - // Last, translate both offset pairs so that none are negative - var xMin = Math.min(x1, x2) - var yMin = Math.min(y1, y2) - x1 -= xMin; x2 -= xMin; - y1 -= yMin; y2 -= yMin; - - // calculate the vertices of this image by translating the vertices of the sub-images - var i, v1 = img1.getVertices(), v2 = img2.getVertices(), xs = [], ys = []; - v1 = v1.map(function(v){ return {x: v.x + x1, y: v.y + y1}; }); - v2 = v2.map(function(v){ return {x: v.x + x2, y: v.y + y2}; }); - - // store the vertices as something private, so this.getVertices() will still return undefined - this._vertices = v1.concat(v2); + var leftX = Math.min((-img1.getWidth() / 2) + x1, (-img2.getWidth() / 2) + x2); + var rightX = Math.max(( img1.getWidth() / 2) + x1, ( img2.getWidth() / 2) + x2); + var topY = Math.min((-img1.getHeight() / 2) + y1, (-img2.getHeight() / 2) + y2); + var bottomY = Math.max(( img1.getHeight() / 2) + y1, ( img2.getHeight() / 2) + y2); + + var centerX = (rightX + leftX) / 2; + var centerY = (bottomY + topY) / 2; + + + x1 -= centerX; x2 -= centerX; + y1 -= centerY; y2 -= centerY; + + // // Translate both offset pairs by the smaller of the half-dimensions + // var xMax = Math.max(img1.getWidth(), img2.getWidth()); + // var yMax = Math.max(img1.getHeight(), img2.getHeight()); + // x1 += xMax; x2 += xMax; + // y1 += yMax; y2 += yMax; + + // // Last, translate both offset pairs so that none are negative + // var xMin = Math.min(x1, x2) + // var yMin = Math.min(y1, y2) + // x1 -= xMin; x2 -= xMin; + // y1 -= yMin; y2 -= yMin; // store the offsets for rendering this.x1 = x1; @@ -868,8 +1038,7 @@ this.y2 = y2; this.img1 = img1; this.img2 = img2; - this.pinholeX = img1.pinholeX + x1; - this.pinholeY = img1.pinholeY + y1; + this.pinhole = new DOMPoint(img1.getPinholeX() + x1, img1.getPinholeY() + y1); // TODO(Ben): +x1/y1? this.alphaBaseline = img1.alphaBaseline ? img1.getBaseline() + y1 : img2.getBaseline() + y2; // console.log("Baseline1: " + img1.alphaBaseline + ", Baseline2: " + img2.alphaBaseline + " ==> " + this.alphaBaseline); var shiftText = ""; @@ -879,14 +1048,18 @@ if (offsetY > 0) { shiftText += "shifted up by " + offsetX; } else if (offsetX < 0) { shiftText += "shifted down by " + (-offsetX); } if (shiftText !== "") { shiftText = ", and " + shiftText; } - this.width = findWidth(this._vertices); - this.height = findHeight(this._vertices); this.ariaText = " an overlay: first image is " + img1.ariaText + ", second image is " + img2.ariaText + ", aligning " + anchor1 + " of first image with " + anchor2 + " of second image" + shiftText; }; OverlayImage.prototype = heir(BaseImage.prototype); - - OverlayImage.prototype.getVertices = function() { return this._vertices; }; + OverlayImage.prototype.computeBB = function(tx) { + var bb1 = this.img1.computeBB(tx.translate(this.x1, this.y1)); + var bb2 = this.img2.computeBB(tx.translate(this.x2, this.y2)); + // console.log("Img1: ", this.x1, this.y1, JSON.stringify(bb1)); + // console.log("Img2: ", this.x2, this.y2, JSON.stringify(bb2)); + // console.log("Merged: ", JSON.stringify(bb1.union(bb2))); + return bb1.merge(bb2); + }; OverlayImage.prototype.render = function(ctx) { ctx.save(); @@ -901,8 +1074,6 @@ OverlayImage.prototype.equals = function(other) { return (other instanceof OverlayImage && - this.width === other.width && - this.height === other.height && this.x1 === other.x1 && this.y1 === other.y1 && this.x2 === other.x2 && @@ -922,38 +1093,20 @@ if((img instanceof EllipseImage) && (img.width == img.height)){ angle = 0; } - var sin = Math.sin(angle * Math.PI / 180); - var cos = Math.cos(angle * Math.PI / 180); - // rotate each point as if it were rotated about the pinhole - var vertices = img.getVertices().map(function(v) { - return {x: v.x*cos - v.y*sin, y: v.x*sin + v.y*cos}; - }); - - // extract the xs and ys separately - var vs = unzipVertices(vertices); - - // store the vertices as something private, so this.getVertices() will still return undefined - var translate = {} - this._vertices = translateVertices(vertices, translate); this.img = img; - this.width = findWidth(vertices); - this.height = findHeight(vertices); this.angle = Math.round(angle); - this.translateX = translate.x; - this.translateY = translate.y; - this.pinholeX = img.pinholeX*cos - img.pinholeY*sin + translate.x; - this.pinholeY = img.pinholeX*sin + img.pinholeY*cos + translate.y; + this.pinhole = DOMPoint.fromPoint(img.pinhole).matrixTransform(new DOMMatrix().rotate(this.angle)); this.ariaText = "Rotated image, "+(-1 * angle)+" degrees: "+img.ariaText; }; RotateImage.prototype = heir(BaseImage.prototype); - - RotateImage.prototype.getVertices = function() { return this._vertices; }; + RotateImage.prototype.computeBB = function(tx) { + return this.img.computeBB(tx.rotate(this.angle)); + }; // translate the canvas using the calculated values, then rotate as needed. RotateImage.prototype.render = function(ctx) { ctx.save(); - ctx.translate(this.translateX, this.translateY); ctx.rotate(this.angle * Math.PI / 180); this.img.render(ctx); ctx.restore(); @@ -961,11 +1114,9 @@ RotateImage.prototype.equals = function(other) { return (other instanceof RotateImage && - this.width === other.width && - this.height === other.height && this.angle === other.angle && - this.pinholeX === other.pinholeX && - this.pinholeY === other.pinholeX && + this.getPinholeX() === other.getPinholeX() && + this.getPinholeY() === other.getPinholeY() && imageEquals(this.img, other.img) ) || BaseImage.prototype.equals.call(this, other); }; @@ -975,54 +1126,33 @@ // Scale an image var ScaleImage = function(xFactor, yFactor, img) { BaseImage.call(this); - // grab the img vertices, scale them, and save the result to this_vertices - this._vertices = img.getVertices().map(function(v) { - return {x: v.x * xFactor, y: v.y * yFactor }; - }); - this.img = img; - this.width = img.width * Math.abs(xFactor); - this.height = img.height * Math.abs(yFactor); - this.xFactor = xFactor; - this.yFactor = yFactor; - this.pinholeX = img.pinholeX * xFactor; - if (xFactor < 0) { // translate pinhole into image region - this.pinholeX += this.width; - this._vertices.forEach((v) => v.x += this.width); - } - this.pinholeY = img.pinholeY * yFactor; - if (yFactor < 0) { // translate pinhole into image region - this.pinholeY += this.height; - this._vertices.forEach((v) => v.y += this.height); - } + this.scaleX = xFactor; + this.scaleY = yFactor; + this.pinhole = DOMPoint.fromPoint(img.pinhole); this.ariaText = "Scaled Image, "+ (xFactor===yFactor? "by "+xFactor : "horizontally by "+xFactor+" and vertically by "+yFactor)+". "+img.ariaText; }; ScaleImage.prototype = heir(BaseImage.prototype); - - ScaleImage.prototype.getVertices = function() { return this._vertices; }; + ScaleImage.prototype.computeBB = function(tx) { + return this.img.computeBB(tx.scale(this.scaleX, this.scaleY)); + }; // scale the context, and pass it to the image's render function ScaleImage.prototype.render = function(ctx) { ctx.save(); - ctx.scale(this.xFactor, this.yFactor); - if (this.xFactor < 0) { - ctx.translate((this.width)/(this.xFactor), 0); - } - if (this.yFactor < 0) { - ctx.translate(0, (this.height)/(this.yFactor)); - } + ctx.scale(this.scaleX, this.scaleY); this.img.render(ctx); ctx.restore(); }; ScaleImage.prototype.equals = function(other) { return (other instanceof ScaleImage && - this.width === other.width && - this.height === other.height && - this.xFactor === other.xFactor && - this.yFactor === other.yFactor && + this.scaleX === other.scaleX && + this.scaleY === other.scaleY && + this.getPinholeX() === other.getPinholeX() && + this.getPinholeY() === other.getPinholeY() && imageEquals(this.img, other.img) ) || BaseImage.prototype.equals.call(this, other); }; @@ -1037,18 +1167,19 @@ this.width = width; this.height = height; this.img = img; - if (img.pinholeX >= x && img.pinholeX <= x + width && - img.pinholeY >= y && img.pinholeY <= y + height) { - this.pinholeX = img.pinholeX - x; - this.pinholeY = img.pinholeY - y; + if (img.getPinholeX() >= x && img.getPinholeX() <= x + width && + img.getPinholeY() >= y && img.getPinholeY() <= y + height) { + this.pinhole = new DOMPoint(img.getPinholeX() - x, img.getPinholeY() - y); } else { - this.pinholeX = width / 2; - this.pinholeY = height / 2; + this.pinhole = new DOMPoint(width / 2, height / 2); } this.ariaText = "Cropped image, from "+x+", "+y+" to "+(x+width)+", "+(y+height)+": "+img.ariaText; }; CropImage.prototype = heir(BaseImage.prototype); + CropImage.prototype.computeBB = function(tx) { + return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); + }; CropImage.prototype.render = function(ctx) { ctx.save(); @@ -1076,23 +1207,25 @@ var FrameImage = function(img) { BaseImage.call(this); this.img = img; - this.width = img.width; - this.height = img.height; - this.pinholeX = img.pinholeX; - this.pinholeY = img.pinholeY; + this.pinhole = DOMPoint.fromPoint(img.pinhole); this.ariaText = " Framed image: "+img.ariaText; this.alphaBaseline = img.alphaBaseline; }; FrameImage.prototype = heir(BaseImage.prototype); + FrameImage.prototype.computeBB = function(tx) { return this.img.computeBB(tx); }; // scale the context, and pass it to the image's render function FrameImage.prototype.render = function(ctx) { + ctx.save(); ctx.save(); this.img.render(ctx); + ctx.restore(); ctx.beginPath(); ctx.strokeStyle = "black"; - ctx.strokeRect(0, 0, this.width, this.height); + var bb = getBB(this.img); + // console.log(JSON.stringify(bb)); + ctx.strokeRect(bb.topLeft.x, bb.topLeft.y, bb.botRight.x - bb.topLeft.x, bb.botRight.y - bb.topLeft.y); ctx.closePath(); ctx.restore(); }; @@ -1111,12 +1244,12 @@ this.img = img; this.width = img.width; this.height = img.height; - this.pinholeX = img.pinholeX; - this.pinholeY = img.pinholeY; + this.pinhole = DOMPoint.fromPoint(img.pinhole); this.ariaText = " Pinhole image: "+img.ariaText; }; PinholeImage.prototype = heir(BaseImage.prototype); + PinholeImage.prototype.computeBB = function(tx) { return this.img.computeBB(tx); } // scale the context, and pass it to the image's render function PinholeImage.prototype.render = function(ctx) { @@ -1126,18 +1259,18 @@ ctx.restore(); ctx.beginPath(); ctx.strokeStyle = "black"; ctx.lineWidth = 1.5; - ctx.moveTo(this.pinholeX - 5, this.pinholeY); - ctx.lineTo(this.pinholeX + 5, this.pinholeY); - ctx.moveTo(this.pinholeX, this.pinholeY - 5); - ctx.lineTo(this.pinholeX, this.pinholeY + 5); + ctx.moveTo(this.getPinholeX() - 5, this.getPinholeY()); + ctx.lineTo(this.getPinholeX() + 5, this.getPinholeY()); + ctx.moveTo(this.getPinholeX(), this.getPinholeY() - 5); + ctx.lineTo(this.getPinholeX(), this.getPinholeY() + 5); ctx.closePath(); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = "white"; ctx.lineWidth = 0.75; - ctx.moveTo(this.pinholeX - 5, this.pinholeY); - ctx.lineTo(this.pinholeX + 5, this.pinholeY); - ctx.moveTo(this.pinholeX, this.pinholeY - 5); - ctx.lineTo(this.pinholeX, this.pinholeY + 5); + ctx.moveTo(this.getPinholeX() - 5, this.getPinholeY()); + ctx.lineTo(this.getPinholeX() + 5, this.getPinholeY()); + ctx.moveTo(this.getPinholeX(), this.getPinholeY() - 5); + ctx.lineTo(this.getPinholeX(), this.getPinholeY() + 5); ctx.closePath(); ctx.stroke(); ctx.restore(); @@ -1149,28 +1282,29 @@ || imageEquals(this.img, other.img); }; - PinholeImage.prototype.getVertices = function() { return this.img.getVertices(); } - ////////////////////////////////////////////////////////////////////// // FlipImage: image string -> image // Flip an image either horizontally or vertically var FlipImage = function(img, direction) { BaseImage.call(this); this.img = img; - this.width = img.width; - this.height = img.height; this.direction = direction; if (direction === "horizontal") { - this.pinholeX = this.width - img.pinholeX; - this.pinholeY = img.pinholeY; + this.pinhole = new DOMPoint(-img.getPinholeX(), img.getPinholeY()); } else { - this.pinholeX = img.pinholeX; - this.pinholeY = this.height - img.pinholeY; + this.pinhole = new DOMPoint(img.getPinholeX(), -img.getPinholeY()); } this.ariaText = direction+"ly flipped image: " + img.ariaText; }; FlipImage.prototype = heir(BaseImage.prototype); + FlipImage.prototype.computeBB = function(tx) { + if (this.direction === "horizontal") { + return this.img.computeBB(tx.flipX()); + } else { + return this.img.computeBB(tx.flipY()); + } + }; FlipImage.prototype.render = function(ctx) { // when flipping an image of dimension M across an axis, @@ -1178,12 +1312,12 @@ ctx.save(); if(this.direction === "horizontal"){ ctx.scale(-1, 1); - ctx.translate(-(this.width), 0); + //ctx.translate(-(this.width), 0); this.img.render(ctx); } if (this.direction === "vertical"){ ctx.scale(1, -1); - ctx.translate(0, -(this.height)); + //ctx.translate(0, -(this.height)); this.img.render(ctx); } ctx.restore(); @@ -1191,8 +1325,6 @@ FlipImage.prototype.equals = function(other) { return (other instanceof FlipImage && - this.width === other.width && - this.height === other.height && this.direction === other.direction && imageEquals(this.img, other.img) ) || BaseImage.prototype.equals.call(this, other); @@ -1206,13 +1338,18 @@ this.height = height; this.style = style; this.color = color; - this.vertices = [{x:0,y:height},{x:0,y:0},{x:width,y:0},{x:width,y:height}]; - this.pinholeX = width / 2; - this.pinholeY = height / 2; + this.vertices = [new DOMPoint(-width/2,height/2), + new DOMPoint(-width/2,-height/2), + new DOMPoint(width/2,-height/2), + new DOMPoint(width/2,height/2)]; + this.pinhole = new DOMPoint(0, 0); this.ariaText = " a" + colorToSpokenString(color,style) + ((width===height)? " square of size "+width : " rectangle of width "+width+" and height "+height); }; RectangleImage.prototype = heir(BaseImage.prototype); + RectangleImage.prototype.computeBB = function(tx) { + return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); + }; ////////////////////////////////////////////////////////////////////// // RhombusImage: Number Number Mode Color -> Image @@ -1224,12 +1361,11 @@ this.height = Math.abs(Math.cos(angle/2 * Math.PI / 180)) * side * 2; this.style = style; this.color = color; - this.vertices = [{x:this.width/2, y:0}, - {x:this.width, y:this.height/2}, - {x:this.width/2, y:this.height}, - {x:0, y:this.height/2}]; - this.pinholeX = this.width / 2; - this.pinholeY = this.height / 2; + this.vertices = [new DOMPoint(this.width/2, 0), + new DOMPoint(this.width, this.height/2), + new DOMPoint(this.width/2, this.height), + new DOMPoint(0, this.height/2)]; + this.pinhole = new DOMPoint(this.width / 2, this.height / 2); this.ariaText = " a"+colorToSpokenString(color,style) + " rhombus of size "+side+" and angle "+angle; }; RhombusImage.prototype = heir(BaseImage.prototype); @@ -1258,16 +1394,13 @@ radians = curComp * angle; for(var i = 0; i < pointsPerComponent; i++) { radians = radians + (step * angle); - vertices.push({ x: Math.round(this.outerRadius*Math.cos(radians-adjust)), - y: Math.round(this.outerRadius*Math.sin(radians-adjust))}); + vertices.push(new DOMPoint(Math.round(this.outerRadius*Math.cos(radians-adjust)), + Math.round(this.outerRadius*Math.sin(radians-adjust)))); } } - this.width = findWidth(vertices); - this.height = findHeight(vertices); this.style = style; this.color = color; - var translate = {} - this.vertices = translateVertices(vertices, translate); + this.vertices = vertices; this._vertices = []; for (var curComp = 0; curComp < numComponents; curComp++) { var component = []; @@ -1277,14 +1410,13 @@ } this._vertices.push(component); } - this.pinholeX = 0; - this.pinholeY = 0; + this.pinhole = new DOMPoint(0, 0); for (var v = 0; v < this.vertices.length; v++) { - this.pinholeX += this.vertices[v].x; - this.pinholeY += this.vertices[v].y; + this.pinhole.x += this.vertices[v].x; + this.pinhole.y += this.vertices[v].y; } - this.pinholeX /= this.vertices.length; - this.pinholeY /= this.vertices.length; + this.pinhole.x /= this.vertices.length; + this.pinhole.y /= this.vertices.length; this.ariaText = " a"+colorToSpokenString(color,style) + ", "+count +" sided polygon with each side of length "+length; }; @@ -1302,26 +1434,25 @@ // For point-polygon images, positive-y points *upward*, by request var PointPolygonImage = function(vertices, style, color) { BaseImage.call(this); + var totX = 0, totY = 0; + this.vertices = []; + for (var v = 0; v < vertices.length; v++) { + var x = jsnums.toFixnum(vertices[v].x); + totX += x; + var y = -1 * jsnums.toFixnum(vertices[v].y); + totY += y; + this.vertices[v] = new DOMPoint(x, y); + } + totX /= vertices.length; + totY /= vertices.length; for (var v = 0; v < vertices.length; v++) { - vertices[v].x = jsnums.toFixnum(vertices[v].x); - vertices[v].y = jsnums.toFixnum(vertices[v].y); - vertices[v].y *= -1; + this.vertices[v].x -= totX; + this.vertices[v].y -= totY; } - this.width = findWidth(vertices); - this.height = findHeight(vertices); this.style = style; this.color = color; - var translate = {} - this.vertices = translateVertices(vertices, translate); - this.pinholeX = 0; - this.pinholeY = 0; - for (var v = 0; v < this.vertices.length; v++) { - this.pinholeX += this.vertices[v].x; - this.pinholeY += this.vertices[v].y; - } - this.pinholeX /= this.vertices.length; - this.pinholeY /= this.vertices.length; + this.pinhole = new DOMPoint(0, 0); this.ariaText = " a"+colorToSpokenString(color,style) + ", polygon with "+vertices.length+" points"; }; PointPolygonImage.prototype = heir(BaseImage.prototype); @@ -1397,12 +1528,14 @@ this.height = metrics.height; // we measure coordinates from the *top* of the image, and getTextDimensions seems to be off by a bit this.alphaBaseline = metrics.ascent + baselineFudge; - this.pinholeX = this.width / 2; - this.pinholeY = this.alphaBaseline; + this.pinhole = new DOMPoint(0, this.alphaBaseline - this.height / 2); this.ariaText = " the string "+str+", colored "+colorToSpokenString(color,'solid')+" of size "+ size; }; TextImage.prototype = heir(BaseImage.prototype); + TextImage.prototype.computeBB = function(tx) { + return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); + }; TextImage.prototype.render = function(ctx) { ctx.save(); @@ -1412,6 +1545,7 @@ // if 'outline' is enabled, use strokeText. Otherwise use fillText ctx.fillStyle = this.outline? 'white' : colorString(this.color); + ctx.translate(-this.width / 2, -this.height / 2); ctx.fillText(this.str, 0, this.alphaBaseline - 1); // handle the baseline offset here if(this.outline){ ctx.strokeStyle = colorString(this.color); @@ -1460,24 +1594,14 @@ for(var pt = 0; pt < (points * 2) + 1; pt++ ) { var rads = ( ( 360 / (2 * points) ) * pt ) * oneDegreeAsRadian - 0.5; var whichRadius = ( pt % 2 === 1 ) ? outer : inner; - vertices.push({x:maxRadius + ( Math.sin( rads ) * whichRadius ), - y:maxRadius + ( Math.cos( rads ) * whichRadius )} ); + vertices.push(new DOMPoint(maxRadius + ( Math.sin( rads ) * whichRadius ), + maxRadius + ( Math.cos( rads ) * whichRadius )) ); } // calculate width and height of the bounding box - this.width = findWidth(vertices); - this.height = findHeight(vertices); this.style = style; this.color = color; - var translate = {}; - this.vertices = translateVertices(vertices, translate); - this.pinholeX = 0; - this.pinholeY = 0; - for (var v = 0; v < this.vertices.length; v++) { - this.pinholeX += this.vertices[v].x; - this.pinholeY += this.vertices[v].y; - } - this.pinholeX /= this.vertices.length; - this.pinholeY /= this.vertices.length; + this.vertices = vertices; + this.pinhole = new DOMPoint(0, 0); this.ariaText = " a" + colorToSpokenString(color,style) + ", " + points + "pointed star with inner radius "+inner+" and outer radius "+outer; }; @@ -1498,23 +1622,31 @@ var vertices = []; // if angle < 180 start at the top of the canvas, otherwise start at the bottom if(thirdY > 0){ - vertices.push({x: offsetX + 0, y: 0}); - vertices.push({x: offsetX + sideC, y: 0}); - vertices.push({x: offsetX + thirdX, y: thirdY}); + vertices.push(new DOMPoint(offsetX + 0, 0)); + vertices.push(new DOMPoint(offsetX + sideC, 0)); + vertices.push(new DOMPoint(offsetX + thirdX, thirdY)); } else { - vertices.push({x: offsetX + 0, y: -thirdY}); - vertices.push({x: offsetX + sideC, y: -thirdY}); - vertices.push({x: offsetX + thirdX, y: 0}); + vertices.push(new DOMPoint(offsetX + 0, -thirdY)); + vertices.push(new DOMPoint(offsetX + sideC, -thirdY)); + vertices.push(new DOMPoint(offsetX + thirdX, 0)); } - this.width = Math.max(sideC, thirdX) + offsetX; - this.height = Math.abs(thirdY); this.style = style; this.color = color; this.vertices = vertices; + var left = Math.min(vertices[0].x, vertices[1].x, vertices[2].x); + var right = Math.max(vertices[0].x, vertices[1].x, vertices[2].x); + var top = Math.min(vertices[0].y, vertices[1].y, vertices[2].y); + var bot = Math.max(vertices[0].y, vertices[1].y, vertices[2].y); + var centerX = (left + right) / 2; + var centerY = (top + bot) / 2; // pinhole is set to centroid (or barycenter): average of three corners - this.pinholeX = (vertices[0].x + vertices[1].x + vertices[2].x) / 3; - this.pinholeY = (vertices[0].y + vertices[1].y + vertices[2].y) / 3; + this.pinhole = new DOMPoint((vertices[0].x + vertices[1].x + vertices[2].x) / 3 - centerX, + (vertices[0].y + vertices[1].y + vertices[2].y) / 3 - centerY); + for (var i = 0; i < vertices.length; i++) { + vertices[i].x -= centerX; + vertices[i].y -= centerY; + } this.ariaText = " a"+colorToSpokenString(color,style) + " triangle whose base is of length "+sideC +", with an angle of " + (angleA%180) + " degrees between it and a side of length "+sideB; }; @@ -1528,40 +1660,71 @@ this.height = height; this.style = style; this.color = color; - this.pinholeX = width / 2; - this.pinholeY = height / 2; + this.pinhole = new DOMPoint(0, 0); this.ariaText = " a"+colorToSpokenString(color,style) + ((width===height)? " circle of radius "+(width/2) : " ellipse of width "+width+" and height "+height); }; EllipseImage.prototype = heir(BaseImage.prototype); + EllipseImage.prototype.computeBB = function(tx) { + // From + // https://stackoverflow.com/questions/24746834/calculating-an-aabb-for-a-transformed-ellipse + var rx = this.width / 2.0; + var ry = this.height / 2.0; + + // /M11 M21 0 M41\ + // |M12 M22 0 M42| Transform matrix format + // |0 0 1 0 | + // \0 0 0 1/ + var xOffset = Math.sqrt((Math.pow(tx.m11, 2) * Math.pow(rx, 2)) + + (Math.pow(tx.m21, 2) * Math.pow(ry, 2))); + var yOffset = Math.sqrt((Math.pow(tx.m12, 2) * Math.pow(rx, 2)) + + (Math.pow(tx.m22, 2) * Math.pow(ry, 2))); + + var centerX = tx.m41; // Transform center of + var centerY = tx.m42; // ellipse using M + var xMin = centerX - xOffset; + var xMax = centerX + xOffset; + + var yMin = centerY - yOffset; + var yMax = centerY + yOffset; + + return new BoundingBox(new DOMPoint(xMin, yMin), new DOMPoint(xMax, yMax)); + }; EllipseImage.prototype.render = function(ctx) { ctx.save(); ctx.beginPath(); - // if it's a solid ellipse... + // // if it's a solid ellipse... var isSolid = this.style.toString().toLowerCase() !== "outline"; var adjust = isSolid? 0 : 0.5; - // ...account for the 1px border width - var width = this.width - 2*adjust, height = this.height - 2*adjust; - var aX = adjust, aY = adjust; - // Most of this code is taken from: - // http://webreflection.blogspot.com/2009/01/ellipse-and-circle-for-canvas-2d.html - var hB = (width / 2) * 0.5522848, - vB = (height / 2) * 0.5522848, - eX = aX + width, - eY = aY + height, - mX = aX + width / 2, - mY = aY + 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); + // var width = this.width - 2*adjust, height = this.height - 2*adjust; + // var aX = adjust, aY = adjust; + ctx.ellipse(0, 0, this.width / 2 - adjust, this.height / 2 - adjust, 0, 0, Math.PI * 2.0); + + // // Most of this code is taken from: + // // http://webreflection.blogspot.com/2009/01/ellipse-and-circle-for-canvas-2d.html + // var hB = (width / 2) * 0.5522848, + // vB = (height / 2) * 0.5522848, + // eX = aX + width, + // eY = aY + height, + // mX = aX + width / 2, + // mY = aY + 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.save(); + // // // ...account for the 1px border width + // ctx.scale(this.width / 2 - adjust, this.height / 2 - adjust); + // ctx.arc(0, 0, 1, 0, 2 * Math.PI); ctx.closePath(); - if (this.style.toString().toLowerCase() === "outline") { + // ctx.restore(); + if (!isSolid) { ctx.strokeStyle = colorString(this.color); ctx.stroke(); } else { @@ -1586,32 +1749,32 @@ var WedgeImage = function(radius, angle, style, color) { BaseImage.call(this); this.radius = radius; - this.angle = (angle % 360.0) * Math.PI / 180; + this.angle = (angle % 360.0); this.style = style; this.color = color; - var endPointX = radius * Math.cos(this.angle); - var endPointY = radius * Math.sin(this.angle); - var vertices = [{x: 0, y: 0}, {x: radius, y: 0}]; - // Going in 5-degree increments ensures we hit the extremal points if they are part of the wedge - // Negate the y-components, because we want y-up behavior - for (var i = 5; i < angle; i += 5) { - vertices.push({x: radius * Math.cos(i * Math.PI / 180), y: -1 * radius * Math.sin(i * Math.PI / 180)}); - } - vertices.push({x: endPointX, y: -endPointY}); - this.width = findWidth(vertices); - this.height = findHeight(vertices); - this.vertices = translateVertices(vertices); - this.pinholeX = this.vertices[0].x; - this.pinholeY = this.vertices[0].y; + this.pinhole = new DOMPoint(0, 0); + this.offsetX = 0; + this.offsetY = 0; + this.pinhole.x = 0; + this.pinhole.y = 0; this.ariaText = " a"+colorToSpokenString(color,style) + " wedge of angle "+angle; }; - WedgeImage.prototype.rotate = function(angle) { - return new WedgeImage(this.radius, this.startAngle + angle, this.angle, this.style, this.color); - } - WedgeImage.prototype = heir(BaseImage.prototype); - + WedgeImage.prototype.computeBB = function(tx) { + var pt = new DOMPoint(this.radius, 0); + var bb = new BoundingBox(new DOMPoint(0, 0).matrixTransform(tx)); + // when the tx is rotated, we can't just assume that the "obvious" extremeties at 90n degrees + // are actually extremal: suppose the wedge is 30deg and the rotation is 75deg. Then the + // rotated start and end points of the wedge straddle 90 degrees, which is the actual y maximum. + for (var i = 0; i < this.angle; i += 5) { + bb.addPoint(pt.matrixTransform(tx.rotate(-i))); + } + bb.addPoint(pt.matrixTransform(tx.rotate(-this.angle))); + // console.log("Final wedge bb", JSON.stringify(bb)); + return bb; + }; + WedgeImage.prototype.render = function(ctx) { ctx.save(); ctx.beginPath(); @@ -1620,11 +1783,14 @@ var isSolid = this.style.toString().toLowerCase() !== "outline"; var adjust = isSolid? 0 : 0.5; // ...account for the 1px border width - var width = this.width - 2*adjust, height = this.height - 2*adjust; var aX = adjust, aY = adjust; - ctx.moveTo(aX + this.pinholeX - adjust, aY + this.pinholeY - adjust); - ctx.arc(aX + this.pinholeX - adjust, aY + this.pinholeY - adjust, this.radius - 2*adjust, 0, -this.angle, true); + //ctx.moveTo(aX + this.getPinholeX() - adjust, aY + this.getPinholeY() - adjust); + ctx.save(); + ctx.scale(this.radius - adjust, this.radius - adjust) + ctx.moveTo(0, 0); + ctx.arc(0, 0, 1, 0, -this.angle * (Math.PI / 180), true); + ctx.restore(); ctx.closePath(); if (this.style.toString().toLowerCase() === "outline") { ctx.strokeStyle = colorString(this.color); @@ -1652,20 +1818,17 @@ BaseImage.call(this); var vertices; if (x >= 0) { - if (y >= 0) { vertices = [{x: 0, y: 0}, {x: x, y: y}]; } - else { vertices = [{x: 0, y: -y}, {x: x, y: 0}]; } + if (y >= 0) { vertices = [new DOMPoint(0, 0), new DOMPoint(x, y)]; } + else { vertices = [new DOMPoint(0, -y), new DOMPoint(x, 0)]; } } else { - if (y >= 0) { vertices = [{x: -x, y: 0}, {x: 0, y: y}]; } - else { vertices = [{x: -x, y: -y}, {x: 0, y: 0}]; } + if (y >= 0) { vertices = [new DOMPoint(-x, 0), new DOMPoint(0, y)]; } + else { vertices = [new DOMPoint(-x, -y), new DOMPoint(0, 0)]; } } - this.width = Math.abs(x); - this.height = Math.abs(y); this.style = "outline"; // all vertex-based images must have a style this.color = color; this.vertices = vertices; - this.pinholeX = Math.abs(x) / 2; - this.pinholeY = Math.abs(y) / 2; + this.pinhole = new DOMPoint(Math.abs(x) / 2, Math.abs(y) / 2); this.ariaText = " a" + colorToSpokenString(color,'solid') + " line of width "+x+" and height "+y; }; diff --git a/src/web/js/trove/make-image.js b/src/web/js/trove/make-image.js index 8e827edd3..4177de7f2 100644 --- a/src/web/js/trove/make-image.js +++ b/src/web/js/trove/make-image.js @@ -552,7 +552,7 @@ checkArity(1, arguments, "center-pinhole", false); c1("center-pinhole", maybeImg, annImage); var img = unwrapImage(maybeImg); - return makeImage(img.updatePinhole(img.getWidth() / 2, img.getHeight() / 2)); + return makeImage(img.centerPinhole()); }); f("place-image-align", function(maybeImg, maybeX, maybeY, maybePlaceX, maybePlaceY, maybeBackground) { diff --git a/test-util/pyret-programs/images/image-tests.arr b/test-util/pyret-programs/images/image-tests.arr index d6184624a..76d054247 100644 --- a/test-util/pyret-programs/images/image-tests.arr +++ b/test-util/pyret-programs/images/image-tests.arr @@ -43,7 +43,7 @@ check "Composing lists of images": below-list([list: red-circ, yellow-circ, green-circ]) is%(within-n-badness(1)) flip-vertical(above-list([list: red-circ, yellow-circ, green-circ])) below-align-list(x-left, [list: red-circ, yellow-circ, green-circ]) - is flip-vertical(above-align(x-left, red-circ, above-align(x-left, yellow-circ, green-circ))) + is%(within-n-badness(1)) flip-vertical(above-align(x-left, red-circ, above-align(x-left, yellow-circ, green-circ))) beside-list([list: red-circ, yellow-circ, green-circ]) is beside(red-circ, beside(yellow-circ, green-circ)) beside-align-list(y-top, [list: red-circ, yellow-circ, green-circ]) From a84a0b8ce79cae718fceefac1cf1e3b5aea8e666 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Sun, 27 Dec 2020 17:47:50 -0500 Subject: [PATCH 02/12] cleanup how chart and world libraries call render, to account for new coordinate system --- src/web/js/trove/chart-lib.js | 5 ++++- src/web/js/trove/world.js | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/web/js/trove/chart-lib.js b/src/web/js/trove/chart-lib.js index 44e14f7fb..88c0a1d98 100644 --- a/src/web/js/trove/chart-lib.js +++ b/src/web/js/trove/chart-lib.js @@ -721,7 +721,10 @@ ${labelRow}`; const xPos = layout.getXLocation(data.getValue(i, 0)); const yPos = layout.getYLocation(data.getValue(i, 1)); const imgDOM = p[3].val.toDomNode(); - p[3].val.render(imgDOM.getContext('2d'), 0, 0); + const ctx = imgDOM.getContext('2d'); + ctx.translate(p[3].val.getWidth() / 2, p[3].val.getHeight() / 2); + ctx.translate(-p[3].val.getCenterX(), -p[3].val.getCenterY()); + p[3].val.render(ctx); // make an image element from the SVG namespace let imageElt = document.createElementNS("http://www.w3.org/2000/svg", 'image'); imageElt.classList.add('__img_labels'); // tag for later garbage collection diff --git a/src/web/js/trove/world.js b/src/web/js/trove/world.js index 6b9362293..361e7ce32 100644 --- a/src/web/js/trove/world.js +++ b/src/web/js/trove/world.js @@ -499,7 +499,11 @@ ctx.fillStyle = "rgba(255,255,255,1)"; ctx.fillRect(0, 0, width, height); ctx.restore(); - theImage.render(ctx, 0, 0); + ctx.save(); + ctx.translate(theImage.getWidth() / 2, theImage.getHeight() / 2); + ctx.translate(- theImage.getCenterX(), - theImage.getCenterY()); + theImage.render(ctx); + ctx.restore(); success([toplevelNode, reusableCanvasNode]); }); }; From ff9a6efdc109dee1069d5bc981f3d872ba2d7289 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Sun, 27 Dec 2020 18:10:14 -0500 Subject: [PATCH 03/12] handle runtime.wrap more carefully, to produce proper Pyret numbers where possible --- src/web/js/trove/image-lib.js | 7 ++++++- src/web/js/trove/make-image.js | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index 79f5cf406..b0c2dec68 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -601,7 +601,12 @@ data2 = ctx2.getImageData(0, 0, w2, h2); var pixels1 = data1.data, pixels2 = data2.data; - return RUNTIME.ffi.makeRight(rmsDiff(pixels1, pixels2)); + var diff = rmsDiff(pixels1, pixels2); + if (diff === Math.floor(diff)) { + return RUNTIME.ffi.makeRight(jsnums.fromFixnum(diff, RUNTIME.NumberErrbacks)); + } else { + return RUNTIME.ffi.makeRight(jsnums.makeRoughnum(diff, RUNTIME.NumberErrbacks)); + } } catch(e){ // if we violate CORS, just bail return RUNTIME.ffi.makeLeft("exception: " + String(e)); diff --git a/src/web/js/trove/make-image.js b/src/web/js/trove/make-image.js index 4177de7f2..b7c998bed 100644 --- a/src/web/js/trove/make-image.js +++ b/src/web/js/trove/make-image.js @@ -1119,21 +1119,21 @@ checkArity(1, arguments, "image-width", false); c1("image-width", maybeImg, annImage); var img = unwrapImage(maybeImg); - return runtime.wrap(img.getWidth()); + return runtime.wrap(jsnums.fromFixnum(img.getWidth(), runtime.NumberErrbacks)); }); f("image-height", function(maybeImg) { checkArity(1, arguments, "image-height", false); c1("image-height", maybeImg, annImage); var img = unwrapImage(maybeImg); - return runtime.wrap(img.getHeight()); + return runtime.wrap(jsnums.fromFixnum(img.getHeight(), runtime.NumberErrbacks)); }); f("image-baseline", function(maybeImg) { checkArity(1, arguments, "image-baseline", false); c1("image-baseline", maybeImg, annImage); var img = unwrapImage(maybeImg); - return runtime.wrap(img.getBaseline()); + return runtime.wrap(jsnums.fromFixnum(img.getBaseline(), runtime.NumberErrbacks)); }); f("image-pinhole-x", function(maybeImg) { @@ -1141,7 +1141,7 @@ c1("image-pinhole-x", maybeImg, annImage); var img = unwrapImage(maybeImg); debugger - return runtime.wrap(img.getPinholeX()); + return runtime.wrap(jsnums.fromFixnum(img.getPinholeX(), runtime.NumberErrbacks)); }); f("image-pinhole-y", function(maybeImg) { @@ -1149,7 +1149,7 @@ c1("image-pinhole-y", maybeImg, annImage); var img = unwrapImage(maybeImg); debugger - return runtime.wrap(img.getPinholeY()); + return runtime.wrap(jsnums.fromFixnum(img.getPinholeY(), runtime.NumberErrbacks)); }); f("color-at-position", function(maybeImage, maybeX, maybeY) { From 4882fdf3e7bf2a93cfd4b3a74a6f76cafc3b89ab Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Mon, 28 Dec 2020 14:27:35 -0500 Subject: [PATCH 04/12] fix frame images not including the entire frame --- src/web/js/trove/image-lib.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index b0c2dec68..c0732984b 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -1218,7 +1218,10 @@ }; FrameImage.prototype = heir(BaseImage.prototype); - FrameImage.prototype.computeBB = function(tx) { return this.img.computeBB(tx); }; + FrameImage.prototype.computeBB = function(tx) { + // The frame needs to be entirely contained in the resulting image + return getBB(this.img).transform(tx); + }; // scale the context, and pass it to the image's render function FrameImage.prototype.render = function(ctx) { From b3b9273959a9acf52a566d4a160739573f156763 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Mon, 28 Dec 2020 14:28:07 -0500 Subject: [PATCH 05/12] Per https://github.com/brownplt/pyret-lang/issues/1562#issuecomment-751796982, wrap and unwrap are mostly vestigial and should be removed --- src/web/js/trove/image-lib.js | 21 +++++++++-------- src/web/js/trove/internal-image-typed.js | 6 ++--- src/web/js/trove/internal-image-untyped.js | 10 ++++----- src/web/js/trove/make-image.js | 26 +++++++++++----------- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index c0732984b..351244cc7 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -20,8 +20,7 @@ var xyPoint = gf(image, "point-xy"); var annPoint = imageTypes["Point"]; var rawIsPoint = gf(image, "is-Point"); - var isPoint = function(p) { return unwrap(rawIsPoint.app(p)); }; - var unwrap = RUNTIME.unwrap; + var isPoint = function(p) { return rawIsPoint.app(p); }; var hasOwnProperty = {}.hasOwnProperty; @@ -33,11 +32,11 @@ throw new Error("Internal error: non-number in makeColor argList ", [r, g, b, a]); } return color.app( - RUNTIME.wrap(r), - RUNTIME.wrap(g), - RUNTIME.wrap(b), + jsnums.fromFixnum(r, RUNTIME.NumberErrbacks), + jsnums.fromFixnum(g, RUNTIME.NumberErrbacks), + jsnums.fromFixnum(b, RUNTIME.NumberErrbacks), // alpha may be passed in as a fixnum, so we coerce to rational - RUNTIME.wrap(jsnums.fromFixnum(a)) + jsnums.fromFixnum(a, RUNTIME.NumberErrbacks) ); }; @@ -46,11 +45,11 @@ else if (num > max) { return max; } else { return num; } } - var isColor = function(c) { return unwrap(rawIsColor.app(c)); }; - var colorRed = function(c) { return clamp(jsnums.toFixnum(unwrap(gf(c, "red"))), 0, 255); } - var colorGreen = function(c) { return clamp(jsnums.toFixnum(unwrap(gf(c, "green"))), 0, 255); } - var colorBlue = function(c) { return clamp(jsnums.toFixnum(unwrap(gf(c, "blue"))), 0, 255); } - var colorAlpha = function(c) { return clamp(jsnums.toFixnum(unwrap(gf(c, "alpha"))), 0, 1); } + var isColor = function(c) { return rawIsColor.app(c); }; + var colorRed = function(c) { return clamp(jsnums.toFixnum(gf(c, "red")), 0, 255); } + var colorGreen = function(c) { return clamp(jsnums.toFixnum(gf(c, "green")), 0, 255); } + var colorBlue = function(c) { return clamp(jsnums.toFixnum(gf(c, "blue")), 0, 255); } + var colorAlpha = function(c) { return clamp(jsnums.toFixnum(gf(c, "alpha")), 0, 1); } var annFillMode = imageTypes["FillMode"]; var annXPlace = imageTypes["XPlace"]; diff --git a/src/web/js/trove/internal-image-typed.js b/src/web/js/trove/internal-image-typed.js index fbc1512b9..106c93dd7 100644 --- a/src/web/js/trove/internal-image-typed.js +++ b/src/web/js/trove/internal-image-typed.js @@ -203,7 +203,7 @@ if (!runtime.ffi.isList(val)) return false; var cur = val; var gf = runtime.getField; - while (runtime.unwrap(ffi.isLink(cur))) { + while (ffi.isLink(cur)) { var f = gf(cur, "first"); if (!checkImagePred(f)) return false; cur = gf(cur, "rest"); @@ -314,7 +314,7 @@ if (!runtime.ffi.isList(val)) return false; var cur = val; var gf = runtime.getField; - while (runtime.unwrap(ffi.isLink(cur))) { + while (ffi.isLink(cur)) { var f = gf(cur, "first"); if (!image.isColor(f)) return false; cur = gf(cur, "rest"); @@ -327,7 +327,7 @@ var cur = val; var gf = runtime.getField; var count = 0; - while (runtime.unwrap(ffi.isLink(cur))) { + while (ffi.isLink(cur)) { var f = gf(cur, "first"); if (!image.isPoint(f)) return false; cur = gf(cur, "rest"); diff --git a/src/web/js/trove/internal-image-untyped.js b/src/web/js/trove/internal-image-untyped.js index 2591a32e1..896bc7807 100644 --- a/src/web/js/trove/internal-image-untyped.js +++ b/src/web/js/trove/internal-image-untyped.js @@ -223,7 +223,7 @@ if (!runtime.ffi.isList(val)) return false; var cur = val; var gf = runtime.getField; - while (runtime.unwrap(ffi.isLink(cur))) { + while (ffi.isLink(cur)) { var f = gf(cur, "first"); if (!checkImagePred(f)) return false; cur = gf(cur, "rest"); @@ -350,19 +350,19 @@ f("is-image-color", function(maybeColor) { checkArity(1, arguments, "image", false); - return runtime.wrap(image.isColorOrColorString(maybeColor)); + return image.isColorOrColorString(maybeColor); }); f("is-mode", function(maybeMode) { checkArity(1, arguments, "is-mode", false); - return runtime.wrap(isMode(maybeMode)); + return isMode(maybeMode); }); f("is-x-place", function(maybeXPlace) { checkArity(1, arguments, "is-x-place", false); - return runtime.wrap(isPlaceX(maybeXPlace)); + return isPlaceX(maybeXPlace); }); f("is-y-place", function(maybeYPlace) { checkArity(1, arguments, "is-y-place", false); - return runtime.wrap(isPlaceY(maybeYPlace)); + return isPlaceY(maybeYPlace); }); diff --git a/src/web/js/trove/make-image.js b/src/web/js/trove/make-image.js index b7c998bed..8351c0867 100644 --- a/src/web/js/trove/make-image.js +++ b/src/web/js/trove/make-image.js @@ -59,7 +59,7 @@ var ans = base; var gf = runtime.getField; var index = 0; - while (runtime.unwrap(ffi.isLink(cur))) { + while (ffi.isLink(cur)) { var f = gf(cur, "first"); ans = func(ans, index++, unwrapImage(f)); cur = gf(cur, "rest"); @@ -141,19 +141,19 @@ }); f("is-angle", function(maybeAngle) { checkArity(1, arguments, "is-angle", false); - return runtime.wrap(image.isAngle(maybeAngle)); + return image.isAngle(maybeAngle); }); f("is-side-count", function(maybeSideCount) { checkArity(1, arguments, "is-side-count", false); - return runtime.wrap(image.isSideCount(maybeSideCount)); + return image.isSideCount(maybeSideCount); }); f("is-step-count", function(maybeStepCount) { checkArity(1, arguments, "is-step-count", false); - return runtime.wrap(image.isStepCount(maybeStepCount)); + return image.isStepCount(maybeStepCount); }); f("is-image", function(maybeImage) { checkArity(1, arguments, "is-image", false); - return runtime.wrap(runtime.isOpaque(maybeImage) && image.isImage(maybeImage.val)); + return runtime.isOpaque(maybeImage) && image.isImage(maybeImage.val); }); f("bitmap-url", function(maybeURL) { checkArity(1, arguments, "bitmap-url", false); @@ -168,14 +168,14 @@ c2("images-difference", maybeImage1, annImage, maybeImage2, annImage); var img1 = unwrapImage(maybeImage1); var img2 = unwrapImage(maybeImage2); - return runtime.wrap(image.imageDifference(img1, img2)); + return image.imageDifference(img1, img2); }); f("images-equal", function(maybeImage1, maybeImage2) { checkArity(2, arguments, "image", false); c2("images-equal", maybeImage1, annImage, maybeImage2, annImage); var img1 = unwrapImage(maybeImage1); var img2 = unwrapImage(maybeImage2); - return runtime.wrap(image.imageEquals(img1, img2)); + return image.imageEquals(img1, img2); }); f("text", function(maybeString, maybeSize, maybeColor) { checkArity(3, arguments, "image", false); @@ -1119,21 +1119,21 @@ checkArity(1, arguments, "image-width", false); c1("image-width", maybeImg, annImage); var img = unwrapImage(maybeImg); - return runtime.wrap(jsnums.fromFixnum(img.getWidth(), runtime.NumberErrbacks)); + return jsnums.fromFixnum(img.getWidth(), runtime.NumberErrbacks); }); f("image-height", function(maybeImg) { checkArity(1, arguments, "image-height", false); c1("image-height", maybeImg, annImage); var img = unwrapImage(maybeImg); - return runtime.wrap(jsnums.fromFixnum(img.getHeight(), runtime.NumberErrbacks)); + return jsnums.fromFixnum(img.getHeight(), runtime.NumberErrbacks); }); f("image-baseline", function(maybeImg) { checkArity(1, arguments, "image-baseline", false); c1("image-baseline", maybeImg, annImage); var img = unwrapImage(maybeImg); - return runtime.wrap(jsnums.fromFixnum(img.getBaseline(), runtime.NumberErrbacks)); + return jsnums.fromFixnum(img.getBaseline(), runtime.NumberErrbacks); }); f("image-pinhole-x", function(maybeImg) { @@ -1141,7 +1141,7 @@ c1("image-pinhole-x", maybeImg, annImage); var img = unwrapImage(maybeImg); debugger - return runtime.wrap(jsnums.fromFixnum(img.getPinholeX(), runtime.NumberErrbacks)); + return jsnums.fromFixnum(img.getPinholeX(), runtime.NumberErrbacks); }); f("image-pinhole-y", function(maybeImg) { @@ -1149,7 +1149,7 @@ c1("image-pinhole-y", maybeImg, annImage); var img = unwrapImage(maybeImg); debugger - return runtime.wrap(jsnums.fromFixnum(img.getPinholeY(), runtime.NumberErrbacks)); + return jsnums.fromFixnum(img.getPinholeY(), runtime.NumberErrbacks); }); f("color-at-position", function(maybeImage, maybeX, maybeY) { @@ -1220,7 +1220,7 @@ var name = maybeName; var val = colorDb.get(String(name)); if (val) { - return runtime.wrap(val); + return val; } throwMessage("Unknown color name '" + String(name) + "'"); }); From 0cd2d5c25efef03033c2420d8457cfb2e4d5e9aa Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Wed, 30 Dec 2020 15:31:33 -0500 Subject: [PATCH 06/12] lazily compute ariaText descriptions --- src/web/js/trove/image-lib.js | 132 +++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 35 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index 351244cc7..c404db130 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -541,12 +541,14 @@ // Canvases lose their drawn content on cloning. data may help us to preserve it. jQuery(canvas).data('toRender', onAfterAttach); // ARIA: use "image" as default text. - canvas.ariaText = this.ariaText || "image"; + canvas.ariaText = this.getAriaText(BaseImage.ariaNestingDepth); return canvas; }; + BaseImage.ariaNestingDepth = 6; BaseImage.prototype.toWrittenString = function(cache) { return ""; } BaseImage.prototype.toDisplayedString = function(cache) { return ""; } + BaseImage.prototype.getAriaText = function(depth) { return "image"; } /* Calculates the difference between two images, and returns it as a Pyret Either @@ -687,10 +689,6 @@ this.withBorder = withBorder; this.color = color; this.pinhole = new DOMPoint(width / 2, height / 2); - this.ariaText = " scene that is "+width+" by "+height+". children are: "; - this.ariaText += children.map(function(c,i){ - return "child "+(i+1)+": "+c[0].ariaText+", positioned at "+c[1]+","+c[2]+" "; - }).join(". "); }; SceneImage.prototype = heir(BaseImage.prototype); SceneImage.prototype.computeBB = function(tx) { @@ -707,6 +705,14 @@ this.withBorder, this.color); }; + SceneImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "scene image"; + var ariaText = " scene that is "+this.width+" by "+this.height+". children are: "; + ariaText += this.children.map(function(c,i){ + return "child "+(i+1)+": "+c[0].getAriaText(depth - 1)+", positioned at "+c[1]+","+c[2]+" "; + }).join(". "); + return ariaText; + }; // render: 2d-context -> void SceneImage.prototype.render = function(ctx) { @@ -764,8 +770,6 @@ var self = this; this.src = src; this.isLoaded = false; - this.ariaText = " image file from "+decodeURIComponent(src).slice(16); - // animationHack: see installHackToSupportAnimatedGifs() for details. this.animationHackImg = undefined; @@ -798,7 +802,10 @@ FileImage.prototype.computeBB = function(tx) { return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).transform(tx); }; - + FileImage.prototype.getAriaText = function(depth) { + return " image file from "+decodeURIComponent(this.src).slice(16); + }; + var imageCache = {}; FileImage.makeInstance = function(path, rawImage) { if (! (path in imageCache)) { @@ -850,7 +857,6 @@ BaseImage.call(this); var self = this; this.src = src; - this.ariaText = " video file from "+decodeURIComponent(src).slice(16); if (rawVideo) { this.video = rawVideo; this.width = self.video.videoWidth; @@ -884,7 +890,10 @@ } } FileVideo.prototype = heir(BaseImage.prototype); - + FileVideo.prototype.getAriaText = function(depth) { + return " video file from "+decodeURIComponent(this.src).slice(16); + }; + var videoCache = {}; FileVideo.makeInstance = function(path, rawVideo) { if (! (path in FileVideo)) { @@ -1052,7 +1061,9 @@ if (offsetY > 0) { shiftText += "shifted up by " + offsetX; } else if (offsetX < 0) { shiftText += "shifted down by " + (-offsetX); } if (shiftText !== "") { shiftText = ", and " + shiftText; } - this.ariaText = " an overlay: first image is " + img1.ariaText + ", second image is " + img2.ariaText + ", aligning " + anchor1 + " of first image with " + anchor2 + " of second image" + shiftText; + this.anchor1 = anchor1; + this.anchor2 = anchor2; + this.shiftText = shiftText; }; OverlayImage.prototype = heir(BaseImage.prototype); @@ -1064,7 +1075,11 @@ // console.log("Merged: ", JSON.stringify(bb1.union(bb2))); return bb1.merge(bb2); }; - + OverlayImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "overlay image"; + return " an overlay: first image is " + this.img1.getAriaText(depth - 1) + ", second image is " + this.img2.getAriaText(depth - 1) + ", aligning " + this.anchor1 + " of first image with " + this.anchor2 + " of second image" + this.shiftText; + }; + OverlayImage.prototype.render = function(ctx) { ctx.save(); ctx.translate(this.x2, this.y2); @@ -1100,13 +1115,16 @@ this.img = img; this.angle = Math.round(angle); this.pinhole = DOMPoint.fromPoint(img.pinhole).matrixTransform(new DOMMatrix().rotate(this.angle)); - this.ariaText = "Rotated image, "+(-1 * angle)+" degrees: "+img.ariaText; }; RotateImage.prototype = heir(BaseImage.prototype); RotateImage.prototype.computeBB = function(tx) { return this.img.computeBB(tx.rotate(this.angle)); }; + RotateImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "rotated image"; + return "Rotated image, "+(-1 * this.angle)+" degrees: "+this.img.getAriaText(depth - 1); + }; // translate the canvas using the calculated values, then rotate as needed. RotateImage.prototype.render = function(ctx) { @@ -1134,14 +1152,20 @@ this.scaleX = xFactor; this.scaleY = yFactor; this.pinhole = DOMPoint.fromPoint(img.pinhole); - this.ariaText = "Scaled Image, "+ (xFactor===yFactor? "by "+xFactor - : "horizontally by "+xFactor+" and vertically by "+yFactor)+". "+img.ariaText; }; ScaleImage.prototype = heir(BaseImage.prototype); ScaleImage.prototype.computeBB = function(tx) { return this.img.computeBB(tx.scale(this.scaleX, this.scaleY)); }; + ScaleImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "scaled image"; + return "Scaled Image, "+ + (this.scaleX===this.scaleY + ? "by "+this.scaleX + : "horizontally by "+this.scaleX+" and vertically by "+this.scaleY)+". " + + this.img.getAriaText(depth - 1); + }; // scale the context, and pass it to the image's render function ScaleImage.prototype.render = function(ctx) { @@ -1177,13 +1201,16 @@ } else { this.pinhole = new DOMPoint(width / 2, height / 2); } - this.ariaText = "Cropped image, from "+x+", "+y+" to "+(x+width)+", "+(y+height)+": "+img.ariaText; }; CropImage.prototype = heir(BaseImage.prototype); CropImage.prototype.computeBB = function(tx) { return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); }; + CropImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "cropped image"; + return "Cropped image, from "+this.x+", "+this.y+" to "+(this.x+this.width)+", "+(this.y+this.height)+": "+this.img.getAriaText(depth - 1); + }; CropImage.prototype.render = function(ctx) { ctx.save(); @@ -1212,7 +1239,6 @@ BaseImage.call(this); this.img = img; this.pinhole = DOMPoint.fromPoint(img.pinhole); - this.ariaText = " Framed image: "+img.ariaText; this.alphaBaseline = img.alphaBaseline; }; @@ -1221,6 +1247,10 @@ // The frame needs to be entirely contained in the resulting image return getBB(this.img).transform(tx); }; + FrameImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "framed image"; + return " Framed image: "+this.img.getAriaText(depth - 1); + }; // scale the context, and pass it to the image's render function FrameImage.prototype.render = function(ctx) { @@ -1252,11 +1282,14 @@ this.width = img.width; this.height = img.height; this.pinhole = DOMPoint.fromPoint(img.pinhole); - this.ariaText = " Pinhole image: "+img.ariaText; }; PinholeImage.prototype = heir(BaseImage.prototype); PinholeImage.prototype.computeBB = function(tx) { return this.img.computeBB(tx); } + PinholeImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "pinhole image"; + return " Pinhole image: "+this.img.getAriaText(depth - 1); + }; // scale the context, and pass it to the image's render function PinholeImage.prototype.render = function(ctx) { @@ -1301,7 +1334,6 @@ } else { this.pinhole = new DOMPoint(img.getPinholeX(), -img.getPinholeY()); } - this.ariaText = direction+"ly flipped image: " + img.ariaText; }; FlipImage.prototype = heir(BaseImage.prototype); @@ -1312,6 +1344,10 @@ return this.img.computeBB(tx.flipY()); } }; + FlipImage.prototype.getAriaText = function(depth) { + if (depth <= 0) return "flipped image"; + return this.direction+"ly flipped image: " + this.img.getAriaText(depth - 1); + }; FlipImage.prototype.render = function(ctx) { // when flipping an image of dimension M across an axis, @@ -1350,13 +1386,17 @@ new DOMPoint(width/2,-height/2), new DOMPoint(width/2,height/2)]; this.pinhole = new DOMPoint(0, 0); - this.ariaText = " a" + colorToSpokenString(color,style) + ((width===height)? " square of size "+width - : " rectangle of width "+width+" and height "+height); }; RectangleImage.prototype = heir(BaseImage.prototype); RectangleImage.prototype.computeBB = function(tx) { return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); }; + RectangleImage.prototype.getAriaText = function(depth) { + return " a" + colorToSpokenString(this.color,this.style) + + ((this.width===this.height) + ? " square of size "+this.width + : " rectangle of width "+this.width+" and height "+this.height); + }; ////////////////////////////////////////////////////////////////////// // RhombusImage: Number Number Mode Color -> Image @@ -1373,9 +1413,11 @@ new DOMPoint(this.width/2, this.height), new DOMPoint(0, this.height/2)]; this.pinhole = new DOMPoint(this.width / 2, this.height / 2); - this.ariaText = " a"+colorToSpokenString(color,style) + " rhombus of size "+side+" and angle "+angle; }; RhombusImage.prototype = heir(BaseImage.prototype); + RhombusImage.prototype.getAriaText = function(depth) { + return " a"+colorToSpokenString(this.color,this.style) + " rhombus of size "+this.side+" and angle "+this.angle; + }; ////////////////////////////////////////////////////////////////////// // RegularPolygonImage: Number Count Step Mode Color -> Image @@ -1424,8 +1466,6 @@ } this.pinhole.x /= this.vertices.length; this.pinhole.y /= this.vertices.length; - this.ariaText = " a"+colorToSpokenString(color,style) + ", "+count - +" sided polygon with each side of length "+length; }; RegularPolygonImage.prototype = heir(BaseImage.prototype); @@ -1437,6 +1477,10 @@ } this.vertices = actualVertices; } + RegularPolygonImage.prototype.getAriaText = function(depth) { + return " a"+colorToSpokenString(this.color,this.style) + ", "+this.count + +" sided polygon with each side of length "+this.length; + } // For point-polygon images, positive-y points *upward*, by request var PointPolygonImage = function(vertices, style, color) { @@ -1460,10 +1504,11 @@ this.style = style; this.color = color; this.pinhole = new DOMPoint(0, 0); - this.ariaText = " a"+colorToSpokenString(color,style) + ", polygon with "+vertices.length+" points"; }; PointPolygonImage.prototype = heir(BaseImage.prototype); - + PointPolygonImage.prototype.getAriaText = function(depth) { + return " a"+colorToSpokenString(this.color,this.style) + ", polygon with "+this.vertices.length+" points"; + } // We don't trust ctx.measureText, since (a) it's buggy and (b) it doesn't measure height // based off of https://stackoverflow.com/a/9847841/783424, @@ -1537,12 +1582,14 @@ this.alphaBaseline = metrics.ascent + baselineFudge; this.pinhole = new DOMPoint(0, this.alphaBaseline - this.height / 2); - this.ariaText = " the string "+str+", colored "+colorToSpokenString(color,'solid')+" of size "+ size; }; TextImage.prototype = heir(BaseImage.prototype); TextImage.prototype.computeBB = function(tx) { return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); }; + TextImage.prototype.getAriaText = function(depth) { + return " the string "+this.str+", colored "+colorToSpokenString(this.color,'solid')+" of size "+ this.size; + }; TextImage.prototype.render = function(ctx) { ctx.save(); @@ -1609,11 +1656,13 @@ this.color = color; this.vertices = vertices; this.pinhole = new DOMPoint(0, 0); - this.ariaText = " a" + colorToSpokenString(color,style) + ", " + points + - "pointed star with inner radius "+inner+" and outer radius "+outer; }; StarImage.prototype = heir(BaseImage.prototype); + StarImage.prototype.getAriaText = function(depth) { + return " a" + colorToSpokenString(this.color,this.style) + ", " + this.points + + "pointed star with inner radius "+this.inner+" and outer radius "+this.outer; + }; ///////////////////////////////////////////////////////////////////// //TriangleImage: Number Number Number Mode Color -> Image @@ -1622,6 +1671,9 @@ // See http://docs.racket-lang.org/teachpack/2htdpimage.html#(def._((lib._2htdp/image..rkt)._triangle)) var TriangleImage = function(sideC, angleA, sideB, style, color) { BaseImage.call(this); + this.sideC = sideC; + this.sideB = sideB; + this.angleA = angleA; var thirdX = sideB * Math.cos(angleA * Math.PI/180); var thirdY = sideB * Math.sin(angleA * Math.PI/180); var offsetX = 0 - Math.min(0, thirdX); // angleA could be obtuse @@ -1654,10 +1706,12 @@ vertices[i].x -= centerX; vertices[i].y -= centerY; } - this.ariaText = " a"+colorToSpokenString(color,style) + " triangle whose base is of length "+sideC - +", with an angle of " + (angleA%180) + " degrees between it and a side of length "+sideB; }; TriangleImage.prototype = heir(BaseImage.prototype); + TriangleImage.prototype.getAriaText = function(depth) { + return " a"+colorToSpokenString(this.color,this.style) + " triangle whose base is of length "+this.sideC + +", with an angle of " + (this.angleA % 180) + " degrees between it and a side of length "+this.sideB; + } ////////////////////////////////////////////////////////////////////// //Ellipse : Number Number Mode Color -> Image @@ -1668,8 +1722,6 @@ this.style = style; this.color = color; this.pinhole = new DOMPoint(0, 0); - this.ariaText = " a"+colorToSpokenString(color,style) + ((width===height)? " circle of radius "+(width/2) - : " ellipse of width "+width+" and height "+height); }; EllipseImage.prototype = heir(BaseImage.prototype); @@ -1698,6 +1750,12 @@ return new BoundingBox(new DOMPoint(xMin, yMin), new DOMPoint(xMax, yMax)); }; + EllipseImage.prototype.getAriaText = function(depth) { + return " a"+colorToSpokenString(this.color,this.style) + + ((this.width===this.height) + ? " circle of radius "+(this.width/2) + : " ellipse of width "+this.width+" and height "+this.height); + }; EllipseImage.prototype.render = function(ctx) { ctx.save(); @@ -1764,7 +1822,6 @@ this.offsetY = 0; this.pinhole.x = 0; this.pinhole.y = 0; - this.ariaText = " a"+colorToSpokenString(color,style) + " wedge of angle "+angle; }; WedgeImage.prototype = heir(BaseImage.prototype); @@ -1781,6 +1838,9 @@ // console.log("Final wedge bb", JSON.stringify(bb)); return bb; }; + WedgeImage.prototype.getAriaText = function(depth) { + return " a"+colorToSpokenString(this.color,this.style) + " wedge of angle "+this.angle; + }; WedgeImage.prototype.render = function(ctx) { ctx.save(); @@ -1836,11 +1896,13 @@ this.color = color; this.vertices = vertices; this.pinhole = new DOMPoint(Math.abs(x) / 2, Math.abs(y) / 2); - this.ariaText = " a" + colorToSpokenString(color,'solid') + " line of width "+x+" and height "+y; }; LineImage.prototype = heir(BaseImage.prototype); + LineImage.prototype.getAriaText = function(depth) { + return " a" + colorToSpokenString(this.color,'solid') + " line of width "+this.x+" and height "+this.y; + }; var colorAtPosition = function(img, x, y) { var width = img.getWidth(), From 742e93643fd3e8bc52cb9185e78ee5a5412cefe7 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Wed, 30 Dec 2020 15:36:20 -0500 Subject: [PATCH 07/12] replacing DOMPoint/DOMMatrix with custom Point2D/Matrix implementations gives _massive_ speedups --- src/web/js/trove/image-lib.js | 208 +++++++++++++++++++++------------- 1 file changed, 130 insertions(+), 78 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index c404db130..67d3f2917 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -118,8 +118,8 @@ var c = new C(); for (property in obj) { if (hasOwnProperty.call(obj, property)) { - if (obj[property] instanceof DOMPoint) { - c[property] = DOMPoint.fromPoint(obj[property]); + if (obj[property] instanceof Point2D) { + c[property] = Point2D.fromPoint(obj[property]); } else { c[property] = obj[property]; } @@ -274,7 +274,7 @@ function getBB(img) { var bb = imageBBs.get(img); if (bb !== undefined) { return bb; } - bb = img.computeBB(new DOMMatrix()); + bb = img.computeBB(Matrix.identity()); imageBBs.set(img, bb); return bb; }; @@ -317,9 +317,62 @@ }) } + function Point2D(x, y) { + this.x = x; + this.y = y; + }; + Point2D.fromPoint = function(obj) { + return new Point2D(obj.x, obj.y); + }; + Point2D.prototype.matrixTransform = function(mtx) { + return new Point2D(mtx.a * this.x + mtx.b * this.y + mtx.c, + mtx.d * this.x + mtx.e * this.y + mtx.f); + }; + function Matrix(a, b, c, d, e, f) { + this.a = a; this.b = b; this.c = c; + this.d = d; this.e = e; this.f = f; + }; + Matrix.identity = function() { return new Matrix(1, 0, 0, 0, 1, 0); } + Matrix.rotation = function(angleDeg) { + var angleRad = angleDeg * (Math.PI / 180); + var c = Math.cos(angleRad); + var s = Math.sin(angleRad); + return new Matrix(c, -s, 0, + s, c, 0); + }; + Matrix.translation = function(tx, ty) { + return new Matrix(1, 0, tx, + 0, 1, ty); + }; + Matrix.scale = function(s) { + return new Matrix(s, 0, 0, + 0, s, 0); + }; + Matrix.scaleXY = function(sx, sy) { + return new Matrix(sx, 0, 0, + 0, sy, 0); + }; + Matrix.prototype.times = function(that) { + /* returns this * that: + /a1 b1 c1\ /a2 b2 c2\ + |d1 e1 f1| |d2 e3 f2| + \0 0 1 / \0 0 1 / + */ + var a = this.a * that.a + this.b * that.d; + var b = this.a * that.b + this.b * that.e; + var c = this.a * that.c + this.b * that.f + this.c; + var d = this.d * that.a + this.e * that.d; + var e = this.d * that.b + this.e * that.e; + var f = this.d * that.c + this.e * that.f + this.f; + return new Matrix(a, b, c, d, e, f); + }; + Matrix.prototype.rotate = function(angleDeg) { return this.times(Matrix.rotation(angleDeg)); } + Matrix.prototype.scale = function(sx, sy) { return this.times(Matrix.scaleXY(sx, sy)); } + Matrix.prototype.translate = function(tx, ty) { return this.times(Matrix.translation(tx, ty)); } + function BoundingBox(topLeft, botRight) { - this.topLeft = DOMPoint.fromPoint(topLeft); - this.botRight = DOMPoint.fromPoint(botRight === undefined ? topLeft : botRight); + this.topLeft = Point2D.fromPoint(topLeft); + this.botRight = Point2D.fromPoint(botRight === undefined ? topLeft : botRight); }; BoundingBox.prototype.getWidth = function() { return Math.round(Math.abs(this.botRight.x - this.topLeft.x)); @@ -355,15 +408,15 @@ BoundingBox.prototype.center = function() { var halfW = this.getWidth() / 2; var halfH = this.getHeight() / 2; - return new BoundingBox(new DOMPoint(this.topLeft.x - halfW, this.topLeft.y - halfH), - new DOMPoint(this.botRight.x - halfW, this.botRight.y - halfH)); + return new BoundingBox(new Point2D(this.topLeft.x - halfW, this.topLeft.y - halfH), + new Point2D(this.botRight.x - halfW, this.botRight.y - halfH)); }; // Transforms this bounding box by the given transformation to produce a new bounding box BoundingBox.prototype.transform = function(tx) { return new BoundingBox(this.topLeft.matrixTransform(tx)) .addPoint(this.botRight.matrixTransform(tx)) - .addPoint(new DOMPoint(this.topLeft.x, this.botRight.y).matrixTransform(tx)) - .addPoint(new DOMPoint(this.botRight.x, this.topLeft.y).matrixTransform(tx)); + .addPoint(new Point2D(this.topLeft.x, this.botRight.y).matrixTransform(tx)) + .addPoint(new Point2D(this.botRight.x, this.topLeft.y).matrixTransform(tx)); }; @@ -371,8 +424,8 @@ var BaseImage = function() {}; BaseImage.prototype.computeBB = function(tx) { - var topLeft = new DOMPoint(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); - var botRight = new DOMPoint(Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER); + var topLeft = new Point2D(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + var botRight = new Point2D(Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER); if (this.vertices === undefined) { topLeft.x = 0; topLeft.y = 0; @@ -393,7 +446,7 @@ BaseImage.prototype.updatePinhole = function(x, y) { var aCopy = clone(this); - aCopy.pinhole = new DOMPoint(x, y); + aCopy.pinhole = new Point2D(x, y); return aCopy; }; @@ -406,8 +459,8 @@ BaseImage.prototype.centerPinhole = function() { var aCopy = clone(this); - var bb = this.computeBB(new DOMMatrix()) - aCopy.pinhole = new DOMPoint(bb.getCenterX(), bb.getCenterY()); + var bb = this.computeBB(Matrix.identity()) + aCopy.pinhole = new Point2D(bb.getCenterX(), bb.getCenterY()); return aCopy; }; @@ -688,11 +741,11 @@ this.children = children; // arrayof [image, number, number] this.withBorder = withBorder; this.color = color; - this.pinhole = new DOMPoint(width / 2, height / 2); + this.pinhole = new Point2D(width / 2, height / 2); }; SceneImage.prototype = heir(BaseImage.prototype); SceneImage.prototype.computeBB = function(tx) { - return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).transform(tx); + return new BoundingBox(new Point2D(0, 0), new Point2D(this.width, this.height)).transform(tx); }; // add: image primitive-number primitive-number -> Scene @@ -778,7 +831,7 @@ this.isLoaded = true; self.width = self.img.width; self.height = self.img.height; - self.pinhole = new DOMPoint(self.width / 2, self.height / 2); + self.pinhole = new Point2D(self.width / 2, self.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 @@ -789,7 +842,7 @@ self.isLoaded = true; self.width = self.img.width; self.height = self.img.height; - self.pinhole = new DOMPoint(self.width / 2, self.height / 2); + self.pinhole = new Point2D(self.width / 2, self.height / 2); }; this.img.onerror = function(e) { self.img.onerror = ""; @@ -800,7 +853,7 @@ } FileImage.prototype = heir(BaseImage.prototype); FileImage.prototype.computeBB = function(tx) { - return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).transform(tx); + return new BoundingBox(new Point2D(0, 0), new Point2D(this.width, this.height)).transform(tx); }; FileImage.prototype.getAriaText = function(depth) { return " image file from "+decodeURIComponent(this.src).slice(16); @@ -917,12 +970,12 @@ this.imageData= imageData; this.width = imageData.width; this.height = imageData.height; - this.pinhole = new DOMPoint(this.width / 2, this.height / 2); + this.pinhole = new Point2D(this.width / 2, this.height / 2); }; ImageDataImage.prototype = heir(BaseImage.prototype); ImageDataImage.prototype.computeBB = function(tx) { - return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).transform(tx); + return new BoundingBox(new Point2D(0, 0), new Point2D(this.width, this.height)).transform(tx); }; ImageDataImage.prototype.render = function(ctx) { @@ -1051,7 +1104,7 @@ this.y2 = y2; this.img1 = img1; this.img2 = img2; - this.pinhole = new DOMPoint(img1.getPinholeX() + x1, img1.getPinholeY() + y1); // TODO(Ben): +x1/y1? + this.pinhole = new Point2D(img1.getPinholeX() + x1, img1.getPinholeY() + y1); // TODO(Ben): +x1/y1? this.alphaBaseline = img1.alphaBaseline ? img1.getBaseline() + y1 : img2.getBaseline() + y2; // console.log("Baseline1: " + img1.alphaBaseline + ", Baseline2: " + img2.alphaBaseline + " ==> " + this.alphaBaseline); var shiftText = ""; @@ -1114,7 +1167,7 @@ } this.img = img; this.angle = Math.round(angle); - this.pinhole = DOMPoint.fromPoint(img.pinhole).matrixTransform(new DOMMatrix().rotate(this.angle)); + this.pinhole = Point2D.fromPoint(img.pinhole).matrixTransform(Matrix.rotation(this.angle)); }; RotateImage.prototype = heir(BaseImage.prototype); @@ -1151,7 +1204,7 @@ this.img = img; this.scaleX = xFactor; this.scaleY = yFactor; - this.pinhole = DOMPoint.fromPoint(img.pinhole); + this.pinhole = Point2D.fromPoint(img.pinhole); }; ScaleImage.prototype = heir(BaseImage.prototype); @@ -1197,15 +1250,15 @@ this.img = img; if (img.getPinholeX() >= x && img.getPinholeX() <= x + width && img.getPinholeY() >= y && img.getPinholeY() <= y + height) { - this.pinhole = new DOMPoint(img.getPinholeX() - x, img.getPinholeY() - y); + this.pinhole = new Point2D(img.getPinholeX() - x, img.getPinholeY() - y); } else { - this.pinhole = new DOMPoint(width / 2, height / 2); + this.pinhole = new Point2D(width / 2, height / 2); } }; CropImage.prototype = heir(BaseImage.prototype); CropImage.prototype.computeBB = function(tx) { - return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); + return new BoundingBox(new Point2D(0, 0), new Point2D(this.width, this.height)).center().transform(tx); }; CropImage.prototype.getAriaText = function(depth) { if (depth <= 0) return "cropped image"; @@ -1238,7 +1291,7 @@ var FrameImage = function(img) { BaseImage.call(this); this.img = img; - this.pinhole = DOMPoint.fromPoint(img.pinhole); + this.pinhole = Point2D.fromPoint(img.pinhole); this.alphaBaseline = img.alphaBaseline; }; @@ -1281,7 +1334,7 @@ this.img = img; this.width = img.width; this.height = img.height; - this.pinhole = DOMPoint.fromPoint(img.pinhole); + this.pinhole = Point2D.fromPoint(img.pinhole); }; PinholeImage.prototype = heir(BaseImage.prototype); @@ -1330,9 +1383,9 @@ this.img = img; this.direction = direction; if (direction === "horizontal") { - this.pinhole = new DOMPoint(-img.getPinholeX(), img.getPinholeY()); + this.pinhole = new Point2D(-img.getPinholeX(), img.getPinholeY()); } else { - this.pinhole = new DOMPoint(img.getPinholeX(), -img.getPinholeY()); + this.pinhole = new Point2D(img.getPinholeX(), -img.getPinholeY()); } }; @@ -1381,15 +1434,15 @@ this.height = height; this.style = style; this.color = color; - this.vertices = [new DOMPoint(-width/2,height/2), - new DOMPoint(-width/2,-height/2), - new DOMPoint(width/2,-height/2), - new DOMPoint(width/2,height/2)]; - this.pinhole = new DOMPoint(0, 0); + this.vertices = [new Point2D(-width/2,height/2), + new Point2D(-width/2,-height/2), + new Point2D(width/2,-height/2), + new Point2D(width/2,height/2)]; + this.pinhole = new Point2D(0, 0); }; RectangleImage.prototype = heir(BaseImage.prototype); RectangleImage.prototype.computeBB = function(tx) { - return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); + return new BoundingBox(new Point2D(0, 0), new Point2D(this.width, this.height)).center().transform(tx); }; RectangleImage.prototype.getAriaText = function(depth) { return " a" + colorToSpokenString(this.color,this.style) + @@ -1408,11 +1461,11 @@ this.height = Math.abs(Math.cos(angle/2 * Math.PI / 180)) * side * 2; this.style = style; this.color = color; - this.vertices = [new DOMPoint(this.width/2, 0), - new DOMPoint(this.width, this.height/2), - new DOMPoint(this.width/2, this.height), - new DOMPoint(0, this.height/2)]; - this.pinhole = new DOMPoint(this.width / 2, this.height / 2); + this.vertices = [new Point2D(this.width/2, 0), + new Point2D(this.width, this.height/2), + new Point2D(this.width/2, this.height), + new Point2D(0, this.height/2)]; + this.pinhole = new Point2D(this.width / 2, this.height / 2); }; RhombusImage.prototype = heir(BaseImage.prototype); RhombusImage.prototype.getAriaText = function(depth) { @@ -1443,7 +1496,7 @@ radians = curComp * angle; for(var i = 0; i < pointsPerComponent; i++) { radians = radians + (step * angle); - vertices.push(new DOMPoint(Math.round(this.outerRadius*Math.cos(radians-adjust)), + vertices.push(new Point2D(Math.round(this.outerRadius*Math.cos(radians-adjust)), Math.round(this.outerRadius*Math.sin(radians-adjust)))); } } @@ -1459,7 +1512,7 @@ } this._vertices.push(component); } - this.pinhole = new DOMPoint(0, 0); + this.pinhole = new Point2D(0, 0); for (var v = 0; v < this.vertices.length; v++) { this.pinhole.x += this.vertices[v].x; this.pinhole.y += this.vertices[v].y; @@ -1492,7 +1545,7 @@ totX += x; var y = -1 * jsnums.toFixnum(vertices[v].y); totY += y; - this.vertices[v] = new DOMPoint(x, y); + this.vertices[v] = new Point2D(x, y); } totX /= vertices.length; totY /= vertices.length; @@ -1503,7 +1556,7 @@ this.style = style; this.color = color; - this.pinhole = new DOMPoint(0, 0); + this.pinhole = new Point2D(0, 0); }; PointPolygonImage.prototype = heir(BaseImage.prototype); PointPolygonImage.prototype.getAriaText = function(depth) { @@ -1580,12 +1633,12 @@ this.height = metrics.height; // we measure coordinates from the *top* of the image, and getTextDimensions seems to be off by a bit this.alphaBaseline = metrics.ascent + baselineFudge; - this.pinhole = new DOMPoint(0, this.alphaBaseline - this.height / 2); + this.pinhole = new Point2D(0, this.alphaBaseline - this.height / 2); }; TextImage.prototype = heir(BaseImage.prototype); TextImage.prototype.computeBB = function(tx) { - return new BoundingBox(new DOMPoint(0, 0), new DOMPoint(this.width, this.height)).center().transform(tx); + return new BoundingBox(new Point2D(0, 0), new Point2D(this.width, this.height)).center().transform(tx); }; TextImage.prototype.getAriaText = function(depth) { return " the string "+this.str+", colored "+colorToSpokenString(this.color,'solid')+" of size "+ this.size; @@ -1648,14 +1701,14 @@ for(var pt = 0; pt < (points * 2) + 1; pt++ ) { var rads = ( ( 360 / (2 * points) ) * pt ) * oneDegreeAsRadian - 0.5; var whichRadius = ( pt % 2 === 1 ) ? outer : inner; - vertices.push(new DOMPoint(maxRadius + ( Math.sin( rads ) * whichRadius ), + vertices.push(new Point2D(maxRadius + ( Math.sin( rads ) * whichRadius ), maxRadius + ( Math.cos( rads ) * whichRadius )) ); } // calculate width and height of the bounding box this.style = style; this.color = color; this.vertices = vertices; - this.pinhole = new DOMPoint(0, 0); + this.pinhole = new Point2D(0, 0); }; StarImage.prototype = heir(BaseImage.prototype); @@ -1681,13 +1734,13 @@ var vertices = []; // if angle < 180 start at the top of the canvas, otherwise start at the bottom if(thirdY > 0){ - vertices.push(new DOMPoint(offsetX + 0, 0)); - vertices.push(new DOMPoint(offsetX + sideC, 0)); - vertices.push(new DOMPoint(offsetX + thirdX, thirdY)); + vertices.push(new Point2D(offsetX + 0, 0)); + vertices.push(new Point2D(offsetX + sideC, 0)); + vertices.push(new Point2D(offsetX + thirdX, thirdY)); } else { - vertices.push(new DOMPoint(offsetX + 0, -thirdY)); - vertices.push(new DOMPoint(offsetX + sideC, -thirdY)); - vertices.push(new DOMPoint(offsetX + thirdX, 0)); + vertices.push(new Point2D(offsetX + 0, -thirdY)); + vertices.push(new Point2D(offsetX + sideC, -thirdY)); + vertices.push(new Point2D(offsetX + thirdX, 0)); } this.style = style; @@ -1700,7 +1753,7 @@ var centerX = (left + right) / 2; var centerY = (top + bot) / 2; // pinhole is set to centroid (or barycenter): average of three corners - this.pinhole = new DOMPoint((vertices[0].x + vertices[1].x + vertices[2].x) / 3 - centerX, + this.pinhole = new Point2D((vertices[0].x + vertices[1].x + vertices[2].x) / 3 - centerX, (vertices[0].y + vertices[1].y + vertices[2].y) / 3 - centerY); for (var i = 0; i < vertices.length; i++) { vertices[i].x -= centerX; @@ -1721,7 +1774,7 @@ this.height = height; this.style = style; this.color = color; - this.pinhole = new DOMPoint(0, 0); + this.pinhole = new Point2D(0, 0); }; EllipseImage.prototype = heir(BaseImage.prototype); @@ -1731,24 +1784,23 @@ var rx = this.width / 2.0; var ry = this.height / 2.0; - // /M11 M21 0 M41\ - // |M12 M22 0 M42| Transform matrix format - // |0 0 1 0 | - // \0 0 0 1/ - var xOffset = Math.sqrt((Math.pow(tx.m11, 2) * Math.pow(rx, 2)) - + (Math.pow(tx.m21, 2) * Math.pow(ry, 2))); - var yOffset = Math.sqrt((Math.pow(tx.m12, 2) * Math.pow(rx, 2)) - + (Math.pow(tx.m22, 2) * Math.pow(ry, 2))); - - var centerX = tx.m41; // Transform center of - var centerY = tx.m42; // ellipse using M + // /a b c\ + // |d e f| Transform matrix format + // \0 0 1/ + var xOffset = Math.sqrt((Math.pow(tx.a, 2) * Math.pow(rx, 2)) + + (Math.pow(tx.b, 2) * Math.pow(ry, 2))); + var yOffset = Math.sqrt((Math.pow(tx.d, 2) * Math.pow(rx, 2)) + + (Math.pow(tx.e, 2) * Math.pow(ry, 2))); + + var centerX = tx.c; // Transform center of + var centerY = tx.f; // ellipse using M var xMin = centerX - xOffset; var xMax = centerX + xOffset; var yMin = centerY - yOffset; var yMax = centerY + yOffset; - return new BoundingBox(new DOMPoint(xMin, yMin), new DOMPoint(xMax, yMax)); + return new BoundingBox(new Point2D(xMin, yMin), new Point2D(xMax, yMax)); }; EllipseImage.prototype.getAriaText = function(depth) { return " a"+colorToSpokenString(this.color,this.style) + @@ -1817,7 +1869,7 @@ this.angle = (angle % 360.0); this.style = style; this.color = color; - this.pinhole = new DOMPoint(0, 0); + this.pinhole = new Point2D(0, 0); this.offsetX = 0; this.offsetY = 0; this.pinhole.x = 0; @@ -1826,8 +1878,8 @@ WedgeImage.prototype = heir(BaseImage.prototype); WedgeImage.prototype.computeBB = function(tx) { - var pt = new DOMPoint(this.radius, 0); - var bb = new BoundingBox(new DOMPoint(0, 0).matrixTransform(tx)); + var pt = new Point2D(this.radius, 0); + var bb = new BoundingBox(new Point2D(0, 0).matrixTransform(tx)); // when the tx is rotated, we can't just assume that the "obvious" extremeties at 90n degrees // are actually extremal: suppose the wedge is 30deg and the rotation is 75deg. Then the // rotated start and end points of the wedge straddle 90 degrees, which is the actual y maximum. @@ -1885,17 +1937,17 @@ BaseImage.call(this); var vertices; if (x >= 0) { - if (y >= 0) { vertices = [new DOMPoint(0, 0), new DOMPoint(x, y)]; } - else { vertices = [new DOMPoint(0, -y), new DOMPoint(x, 0)]; } + if (y >= 0) { vertices = [new Point2D(0, 0), new Point2D(x, y)]; } + else { vertices = [new Point2D(0, -y), new Point2D(x, 0)]; } } else { - if (y >= 0) { vertices = [new DOMPoint(-x, 0), new DOMPoint(0, y)]; } - else { vertices = [new DOMPoint(-x, -y), new DOMPoint(0, 0)]; } + if (y >= 0) { vertices = [new Point2D(-x, 0), new Point2D(0, y)]; } + else { vertices = [new Point2D(-x, -y), new Point2D(0, 0)]; } } this.style = "outline"; // all vertex-based images must have a style this.color = color; this.vertices = vertices; - this.pinhole = new DOMPoint(Math.abs(x) / 2, Math.abs(y) / 2); + this.pinhole = new Point2D(Math.abs(x) / 2, Math.abs(y) / 2); }; From ba4116a260689e952eef798de710acad9cf5f8ff Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Wed, 30 Dec 2020 15:52:10 -0500 Subject: [PATCH 08/12] missing flipX/Y methods --- src/web/js/trove/image-lib.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index 67d3f2917..61ef3918e 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -369,6 +369,8 @@ Matrix.prototype.rotate = function(angleDeg) { return this.times(Matrix.rotation(angleDeg)); } Matrix.prototype.scale = function(sx, sy) { return this.times(Matrix.scaleXY(sx, sy)); } Matrix.prototype.translate = function(tx, ty) { return this.times(Matrix.translation(tx, ty)); } + Matrix.prototype.flipX = function() { return this.times(Matrix.scaleXY(-1, 1)); } + Matrix.prototype.flipY = function() { return this.times(Matrix.scaleXY(1, -1)); } function BoundingBox(topLeft, botRight) { this.topLeft = Point2D.fromPoint(topLeft); From d472813d767ae62f71808f3b307f3c0fd36c147c Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Sun, 3 Jan 2021 16:41:11 -0500 Subject: [PATCH 09/12] Progress: - resetting pinhole coordinate system to top-left, per docs - fixing overlay somewhat more, so that e.g. overlaying a triangle on a circle never overflows the circle - adding color-frame and color-pinhole, for additional debugging. Three tests currently fail, and arguably the reference images need fixing --- src/web/js/trove/image-lib.js | 100 +++++++++++++-------- src/web/js/trove/internal-image-typed.js | 2 + src/web/js/trove/internal-image-untyped.js | 2 + src/web/js/trove/make-image.js | 40 ++++++--- 4 files changed, 97 insertions(+), 47 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index 61ef3918e..e963f310b 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -377,10 +377,10 @@ this.botRight = Point2D.fromPoint(botRight === undefined ? topLeft : botRight); }; BoundingBox.prototype.getWidth = function() { - return Math.round(Math.abs(this.botRight.x - this.topLeft.x)); + return Math.abs(this.botRight.x - this.topLeft.x); }; BoundingBox.prototype.getHeight = function() { - return Math.round(Math.abs(this.botRight.y - this.topLeft.y)); + return Math.abs(this.botRight.y - this.topLeft.y); }; BoundingBox.prototype.getCenterX = function() { return (this.topLeft.x + this.botRight.x) / 2; @@ -548,7 +548,17 @@ ctx.strokeStyle = colorString(this.color); ctx.stroke(); } + ctx.beginPath(); + var tx = ctx.getTransform(); + ctx.setTransform(1, 0, 0, 0, 1, 0); + var pt = this.pinhole.matrixTransform(tx); + ctx.ellipse(pt.x, pt.y, 3, 3, 0, 0, Math.PI * 2.0); + ctx.closePath(); + ctx.fillStyle = "red"; + ctx.fill(); ctx.restore(); + + }; // makeCanvas: number number -> canvas @@ -997,21 +1007,23 @@ BaseImage.call(this); // To find where to place the two images relative to one another - // start in a coordinate system with origin at centers. + // start in a coordinate system with origin defined by each image. var x1 = 0, y1 = 0, x2 = 0, y2 = 0; + console.log("Image 1", JSON.stringify(getBB(img1)), x1, y1); + console.log("Image 2", JSON.stringify(getBB(img2)), x2, y2); var anchor1, anchor2; // compute the x1/y1 and x2/y2 offsets, to translate the target points to a common origin switch(placeX1.toLowerCase()) { case "left": x1 += img1.getWidth() / 2; anchor1 = "-left"; break; - case "middle": x1 -= 0; anchor1 = "-middle"; break; + case "middle": x1 -= img1.getCenterX(); anchor1 = "-middle"; break; case "pinhole": x1 -= img1.getPinholeX(); anchor1 = "-pinhole"; break; case "right": x1 -= img1.getWidth() / 2; anchor1 = "-right"; break; default: throw new Error("Unknown XPlace option for image 1: " + placeX1); } switch(placeY1.toLowerCase()) { case "top": y1 += img1.getHeight() / 2; anchor1 = "top" + anchor1; break; - case "center": y1 -= 0; anchor1 = "center" + anchor1; break; + case "center": y1 -= img1.getCenterY(); anchor1 = "center" + anchor1; break; case "pinhole": y1 -= img1.getPinholeY(); anchor1 = "pinhole" + anchor1; break; case "baseline": y1 -= img1.getBaseline() - img1.getHeight() / 2; anchor1 = "baseline" + anchor1; break; case "bottom": y1 -= img1.getHeight() / 2; anchor1 = "bottom" + anchor1; break; @@ -1019,14 +1031,14 @@ } switch(placeX2.toLowerCase()) { case "left": x2 += img2.getWidth() / 2; anchor2 = "-left"; break; - case "middle": x2 -= 0; anchor2 = "-middle"; break; + case "middle": x2 -= img2.getCenterX(); anchor2 = "-middle"; break; case "pinhole": x2 -= img2.getPinholeX(); anchor2 = "-pinhole"; break; case "right": x2 -= img2.getWidth() / 2; anchor2 = "-right"; break; default: throw new Error("Unknown XPlace option for image 2: " + placeX2); } switch(placeY2.toLowerCase()) { case "top": y2 += img2.getHeight() / 2; anchor2 = "top" + anchor2; break; - case "center": y2 -= 0; anchor2 = "center" + anchor2; break; + case "center": y2 -= img2.getCenterY(); anchor2 = "center" + anchor2; break; case "pinhole": y2 -= img2.getPinholeY(); anchor2 = "pinhole" + anchor2; break; case "baseline": y2 -= img2.getBaseline() - img2.getHeight() / 2; anchor2 = "baseline" + anchor2; break; case "bottom": y2 -= img2.getHeight() / 2; anchor2 = "bottom" + anchor2; break; @@ -1074,14 +1086,12 @@ // Next, offset x2/y2 by the given offsetX/Y x2 += offsetX; y2 += offsetY; + var bb1 = getBB(img1); + var bb2 = getBB(img2); + var newBB = bb1.transform(Matrix.translation(x1, y1)).merge(bb2.transform(Matrix.translation(x2, y2))) - var leftX = Math.min((-img1.getWidth() / 2) + x1, (-img2.getWidth() / 2) + x2); - var rightX = Math.max(( img1.getWidth() / 2) + x1, ( img2.getWidth() / 2) + x2); - var topY = Math.min((-img1.getHeight() / 2) + y1, (-img2.getHeight() / 2) + y2); - var bottomY = Math.max(( img1.getHeight() / 2) + y1, ( img2.getHeight() / 2) + y2); - - var centerX = (rightX + leftX) / 2; - var centerY = (bottomY + topY) / 2; + var centerX = newBB.getCenterX(); + var centerY = newBB.getCenterY(); x1 -= centerX; x2 -= centerX; @@ -1168,7 +1178,7 @@ angle = 0; } this.img = img; - this.angle = Math.round(angle); + this.angle = -1 * Math.round(angle); // since +y points downward, need to negate rotation angle this.pinhole = Point2D.fromPoint(img.pinhole).matrixTransform(Matrix.rotation(this.angle)); }; @@ -1206,7 +1216,7 @@ this.img = img; this.scaleX = xFactor; this.scaleY = yFactor; - this.pinhole = Point2D.fromPoint(img.pinhole); + this.pinhole = Point2D.fromPoint(img.pinhole).matrixTransform(Matrix.scale(xFactor, yFactor)); }; ScaleImage.prototype = heir(BaseImage.prototype); @@ -1290,9 +1300,10 @@ ////////////////////////////////////////////////////////////////////// // FrameImage: image -> image // Stick a frame around the image - var FrameImage = function(img) { + var FrameImage = function(img, color) { BaseImage.call(this); this.img = img; + this.color = color; this.pinhole = Point2D.fromPoint(img.pinhole); this.alphaBaseline = img.alphaBaseline; }; @@ -1314,7 +1325,7 @@ this.img.render(ctx); ctx.restore(); ctx.beginPath(); - ctx.strokeStyle = "black"; + ctx.strokeStyle = colorString(this.color); var bb = getBB(this.img); // console.log(JSON.stringify(bb)); ctx.strokeRect(bb.topLeft.x, bb.topLeft.y, bb.botRight.x - bb.topLeft.x, bb.botRight.y - bb.topLeft.y); @@ -1331,11 +1342,11 @@ ////////////////////////////////////////////////////////////////////// // PinholeImage: image -> image // Draw a small mark at the pinhole of the image - var PinholeImage = function(img) { + var PinholeImage = function(img, color1, color2) { BaseImage.call(this); this.img = img; - this.width = img.width; - this.height = img.height; + this.color1 = color1; + this.color2 = color2; this.pinhole = Point2D.fromPoint(img.pinhole); }; @@ -1352,20 +1363,33 @@ ctx.save(); this.img.render(ctx); ctx.restore(); + var px = this.getPinholeX(); + var py = this.getPinholeY(); + console.log("In pinhole, px = ", px, " py = ", py); + var tx = ctx.getTransform(); + var pt = new DOMPoint(px, py); + var txpt = pt.matrixTransform(tx); + console.log("Transformed pinhole, px = ", txpt.x, " py = ", txpt.y); ctx.beginPath(); - ctx.strokeStyle = "black"; ctx.lineWidth = 1.5; - ctx.moveTo(this.getPinholeX() - 5, this.getPinholeY()); - ctx.lineTo(this.getPinholeX() + 5, this.getPinholeY()); - ctx.moveTo(this.getPinholeX(), this.getPinholeY() - 5); - ctx.lineTo(this.getPinholeX(), this.getPinholeY() + 5); + ctx.strokeStyle = colorString(this.color1); ctx.lineWidth = 1.5; + ctx.moveTo(px - 5, py); + ctx.lineTo(px + 5, py); + ctx.moveTo(px, py - 5); + ctx.lineTo(px, py + 5); ctx.closePath(); ctx.stroke(); ctx.beginPath(); - ctx.strokeStyle = "white"; ctx.lineWidth = 0.75; - ctx.moveTo(this.getPinholeX() - 5, this.getPinholeY()); - ctx.lineTo(this.getPinholeX() + 5, this.getPinholeY()); - ctx.moveTo(this.getPinholeX(), this.getPinholeY() - 5); - ctx.lineTo(this.getPinholeX(), this.getPinholeY() + 5); + ctx.strokeStyle = colorString(this.color2); + if (colorAlpha(this.color1) === 0) { + // full-width stroke if only one color + ctx.lineWidth = 1; + } else { + ctx.lineWidth = 0.75; + } + ctx.moveTo(px - 5, py); + ctx.lineTo(px + 5, py); + ctx.moveTo(px, py - 5); + ctx.lineTo(px, py + 5); ctx.closePath(); ctx.stroke(); ctx.restore(); @@ -1756,7 +1780,7 @@ var centerY = (top + bot) / 2; // pinhole is set to centroid (or barycenter): average of three corners this.pinhole = new Point2D((vertices[0].x + vertices[1].x + vertices[2].x) / 3 - centerX, - (vertices[0].y + vertices[1].y + vertices[2].y) / 3 - centerY); + (vertices[0].y + vertices[1].y + vertices[2].y) / 3 - centerY); for (var i = 0; i < vertices.length; i++) { vertices[i].x -= centerX; vertices[i].y -= centerY; @@ -1802,7 +1826,8 @@ var yMin = centerY - yOffset; var yMax = centerY + yOffset; - return new BoundingBox(new Point2D(xMin, yMin), new Point2D(xMax, yMax)); + return new BoundingBox(new Point2D(Math.floor(xMin), Math.floor(yMin)), + new Point2D(Math.ceil(xMax), Math.ceil(yMax))); }; EllipseImage.prototype.getAriaText = function(depth) { return " a"+colorToSpokenString(this.color,this.style) + @@ -2076,11 +2101,11 @@ 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 makeFrameImage = function(img, color) { + return new FrameImage(img, color); }; - var makePinholeImage = function(img) { - return new PinholeImage(img); + var makePinholeImage = function(img, color1, color2) { + return new PinholeImage(img, color1, color2); } var makeFlipImage = function(img, direction) { return new FlipImage(img, direction); @@ -2190,6 +2215,7 @@ imageToColorList: imageToColorList, colorListToImage: colorListToImage, + getBB: getBB, isImage: isImage, isScene: isScene, diff --git a/src/web/js/trove/internal-image-typed.js b/src/web/js/trove/internal-image-typed.js index 106c93dd7..5d6da6661 100644 --- a/src/web/js/trove/internal-image-typed.js +++ b/src/web/js/trove/internal-image-typed.js @@ -118,7 +118,9 @@ "reflect-x": ["arrow", ["Image"], "Image"], "reflect-y": ["arrow", ["Image"], "Image"], "frame": ["arrow", ["Image"], "Image"], + "color-frame": ["arrow", ["Color", "Image"], "Image"], "draw-pinhole": ["arrow", ["Image"], "Image"], + "color-pinhole": ["arrow", ["Color", "Image"], "Image"], "crop": ["arrow", ["Number", "Number", "Number", "Number", "Image"], "Image"], "line": ["arrow", ["Number", "Number", "Color"], "Image"], "add-line": ["arrow", ["Image", "Number", "Number", "Number", "Number", "Color"], "Image"], diff --git a/src/web/js/trove/internal-image-untyped.js b/src/web/js/trove/internal-image-untyped.js index 896bc7807..8830d2b31 100644 --- a/src/web/js/trove/internal-image-untyped.js +++ b/src/web/js/trove/internal-image-untyped.js @@ -108,7 +108,9 @@ "reflect-x": ["arrow", ["Image"], "Image"], "reflect-y": ["arrow", ["Image"], "Image"], "frame": ["arrow", ["Image"], "Image"], + "color-frame": ["arrow", ["ColorString", "Image"], "Image"], "draw-pinhole": ["arrow", ["Image"], "Image"], + "color-pinhole": ["arrow", ["ColorString", "Image"], "Image"], "crop": ["arrow", ["Number", "Number", "Number", "Number", "Image"], "Image"], "line": ["arrow", ["Number", "Number", "ColorString"], "Image"], "add-line": ["arrow", ["Image", "Number", "Number", "Number", "Number", "ColorString"], "Image"], diff --git a/src/web/js/trove/make-image.js b/src/web/js/trove/make-image.js index 8351c0867..1aaa54e8d 100644 --- a/src/web/js/trove/make-image.js +++ b/src/web/js/trove/make-image.js @@ -546,7 +546,9 @@ var img = unwrapImage(maybeImg); var x = jsnums.toFixnum(maybeX); var y = jsnums.toFixnum(maybeY); - return makeImage(img.updatePinhole(x, y)); + var bb = image.getBB(img); + // Per documentation, a pinhole of (0,0) is in the upper-left corner + return makeImage(img.updatePinhole(x + bb.topLeft.x, y + bb.topLeft.y)); }); f("center-pinhole", function(maybeImg) { checkArity(1, arguments, "center-pinhole", false); @@ -594,7 +596,7 @@ c2("rotate", maybeAngle, annReal, maybeImg, annImage); var angle = jsnums.toFixnum(canonicalizeAngle(maybeAngle)); var img = unwrapImage(maybeImg); - return makeImage(image.makeRotateImage(-angle, img)); + return makeImage(image.makeRotateImage(angle, img)); }); f("scale", function(maybeFactor, maybeImg) { @@ -635,14 +637,30 @@ checkArity(1, arguments, "frame", false); c1("frame", maybeImg, annImage); var img = unwrapImage(maybeImg); - return makeImage(image.makeFrameImage(img)); + return makeImage(image.makeFrameImage(img, colorDb.get("black"))); + }); + + f("color-frame", function(maybeColor, maybeImg) { + checkArity(2, arguments, "frame", false); + c1("color-frame", maybeColor, annColor, maybeImg, annImage); + var color = unwrapColor(maybeColor); + var img = unwrapImage(maybeImg); + return makeImage(image.makeFrameImage(img, color)); }); f("draw-pinhole", function(maybeImg) { checkArity(1, arguments, "draw-pinhole", false); c1("draw-pinhole", maybeImg, annImage); var img = unwrapImage(maybeImg); - return makeImage(image.makePinholeImage(img)); + return makeImage(image.makePinholeImage(img, colorDb.get("black"), colorDb.get("white"))); + }); + + f("color-pinhole", function(maybeColor, maybeImg) { + checkArity(2, arguments, "color-pinhole", false); + c2("color-pinhole", maybeColor, annColor, maybeImg, annImage); + var color = unwrapColor(maybeColor) + var img = unwrapImage(maybeImg); + return makeImage(image.makePinholeImage(img, colorDb.get("transparent"), color)); }); f("crop", function(maybeX, maybeY, maybeWidth, maybeHeight, maybeImg) { @@ -1119,14 +1137,14 @@ checkArity(1, arguments, "image-width", false); c1("image-width", maybeImg, annImage); var img = unwrapImage(maybeImg); - return jsnums.fromFixnum(img.getWidth(), runtime.NumberErrbacks); + return jsnums.fromFixnum(Math.ceil(img.getWidth()), runtime.NumberErrbacks); }); f("image-height", function(maybeImg) { checkArity(1, arguments, "image-height", false); c1("image-height", maybeImg, annImage); var img = unwrapImage(maybeImg); - return jsnums.fromFixnum(img.getHeight(), runtime.NumberErrbacks); + return jsnums.fromFixnum(Math.ceil(img.getHeight()), runtime.NumberErrbacks); }); f("image-baseline", function(maybeImg) { @@ -1140,16 +1158,18 @@ checkArity(1, arguments, "image-pinhole-x", false); c1("image-pinhole-x", maybeImg, annImage); var img = unwrapImage(maybeImg); - debugger - return jsnums.fromFixnum(img.getPinholeX(), runtime.NumberErrbacks); + var bb = image.getBB(img); + // per documentation, image-pinhole-x measures from the left of the image + return jsnums.fromFixnum(img.getPinholeX() - bb.topLeft.x, runtime.NumberErrbacks); }); f("image-pinhole-y", function(maybeImg) { checkArity(1, arguments, "image-pinhole-y", false); c1("image-pinhole-y", maybeImg, annImage); var img = unwrapImage(maybeImg); - debugger - return jsnums.fromFixnum(img.getPinholeY(), runtime.NumberErrbacks); + var bb = image.getBB(img); + // per documentation, image-pinhole-x measures from the left of the image + return jsnums.fromFixnum(img.getPinholeY() - bb.topLeft.y, runtime.NumberErrbacks); }); f("color-at-position", function(maybeImage, maybeX, maybeY) { From 943f9d1153741b45008ec2743100d43d7f5b23a4 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Sun, 3 Jan 2021 17:05:17 -0500 Subject: [PATCH 10/12] cleanup stray console.logs and commented-out legacy code --- src/web/js/trove/image-lib.js | 107 +++++---------------- src/web/js/trove/internal-image-typed.js | 2 - src/web/js/trove/internal-image-untyped.js | 2 - 3 files changed, 23 insertions(+), 88 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index e963f310b..c7d00e39d 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -436,11 +436,8 @@ return new BoundingBox(topLeft, botRight); } else { var ans = new BoundingBox(topLeft, botRight); - // console.log("bb", JSON.stringify(ans)); for (let v of this.vertices) { - // console.log("v", v.toJSON(), "tx(v)", v.matrixTransform(tx).toJSON()); ans.addPoint(v.matrixTransform(tx)); - // console.log("bb", JSON.stringify(ans)); } return ans; } @@ -1009,105 +1006,57 @@ // To find where to place the two images relative to one another // start in a coordinate system with origin defined by each image. var x1 = 0, y1 = 0, x2 = 0, y2 = 0; - console.log("Image 1", JSON.stringify(getBB(img1)), x1, y1); - console.log("Image 2", JSON.stringify(getBB(img2)), x2, y2); var anchor1, anchor2; // compute the x1/y1 and x2/y2 offsets, to translate the target points to a common origin + var bb1 = getBB(img1); + var bb2 = getBB(img2); switch(placeX1.toLowerCase()) { - case "left": x1 += img1.getWidth() / 2; anchor1 = "-left"; break; - case "middle": x1 -= img1.getCenterX(); anchor1 = "-middle"; break; + case "left": x1 -= bb1.topLeft.x; anchor1 = "-left"; break; + case "middle": x1 -= bb1.getCenterX(); anchor1 = "-middle"; break; case "pinhole": x1 -= img1.getPinholeX(); anchor1 = "-pinhole"; break; - case "right": x1 -= img1.getWidth() / 2; anchor1 = "-right"; break; + case "right": x1 -= bb1.botRight.x; anchor1 = "-right"; break; default: throw new Error("Unknown XPlace option for image 1: " + placeX1); } switch(placeY1.toLowerCase()) { - case "top": y1 += img1.getHeight() / 2; anchor1 = "top" + anchor1; break; - case "center": y1 -= img1.getCenterY(); anchor1 = "center" + anchor1; break; + case "top": y1 -= bb1.topLeft.y; anchor1 = "top" + anchor1; break; + case "center": y1 -= bb1.getCenterY(); anchor1 = "center" + anchor1; break; case "pinhole": y1 -= img1.getPinholeY(); anchor1 = "pinhole" + anchor1; break; case "baseline": y1 -= img1.getBaseline() - img1.getHeight() / 2; anchor1 = "baseline" + anchor1; break; - case "bottom": y1 -= img1.getHeight() / 2; anchor1 = "bottom" + anchor1; break; + case "bottom": y1 -= bb1.botRight.y; anchor1 = "bottom" + anchor1; break; default: throw new Error("Unknown YPlace option for image 1: " + placeY1); } switch(placeX2.toLowerCase()) { - case "left": x2 += img2.getWidth() / 2; anchor2 = "-left"; break; - case "middle": x2 -= img2.getCenterX(); anchor2 = "-middle"; break; + case "left": x2 -= bb2.topLeft.x; anchor2 = "-left"; break; + case "middle": x2 -= bb2.getCenterX(); anchor2 = "-middle"; break; case "pinhole": x2 -= img2.getPinholeX(); anchor2 = "-pinhole"; break; - case "right": x2 -= img2.getWidth() / 2; anchor2 = "-right"; break; + case "right": x2 -= bb2.botRight.x; anchor2 = "-right"; break; default: throw new Error("Unknown XPlace option for image 2: " + placeX2); } switch(placeY2.toLowerCase()) { - case "top": y2 += img2.getHeight() / 2; anchor2 = "top" + anchor2; break; - case "center": y2 -= img2.getCenterY(); anchor2 = "center" + anchor2; break; + case "top": y2 -= bb2.topLeft.y; anchor2 = "top" + anchor2; break; + case "center": y2 -= bb2.getCenterY(); anchor2 = "center" + anchor2; break; case "pinhole": y2 -= img2.getPinholeY(); anchor2 = "pinhole" + anchor2; break; case "baseline": y2 -= img2.getBaseline() - img2.getHeight() / 2; anchor2 = "baseline" + anchor2; break; - case "bottom": y2 -= img2.getHeight() / 2; anchor2 = "bottom" + anchor2; break; - default: throw new Error("Unknown YPlace option for image 2: " + placeY2); - } - - /* - // To find where to place the two images relative to one another - // start in a coordinate system with origin at top/left corners - var x1 = 0, y1 = 0, x2 = 0, y2 = 0; - var anchor1, anchor2; - - // compute the x1/y1 and x2/y2 offsets, relative to the top/left of img1/img2: - switch(placeX1.toLowerCase()) { - case "left": x1 -= 0; anchor1 = "-left"; break; - case "middle": x1 -= img1.getWidth() / 2; anchor1 = "-middle"; break; - case "pinhole": x1 -= img1.getPinholeX(); anchor1 = "-pinhole"; break; - case "right": x1 -= img1.getWidth(); anchor1 = "-right"; break; - default: throw new Error("Unknown XPlace option for image 1: " + placeX1); - } - switch(placeY1.toLowerCase()) { - case "top": y1 -= 0; anchor1 = "top" + anchor1; break; - case "center": y1 -= img1.getHeight() / 2; anchor1 = "center" + anchor1; break; - case "pinhole": y1 -= img1.getPinholeY(); anchor1 = "pinhole" + anchor1; break; - case "baseline": y1 -= img1.getBaseline(); anchor1 = "baseline" + anchor1; break; - case "bottom": y1 -= img1.getHeight(); anchor1 = "bottom" + anchor1; break; - default: throw new Error("Unknown YPlace option for image 1: " + placeY1); - } - switch(placeX2.toLowerCase()) { - case "left": x2 -= 0; anchor2 = "-left"; break; - case "middle": x2 -= img2.getWidth() / 2; anchor2 = "-middle"; break; - case "pinhole": x2 -= img2.getPinholeX(); anchor2 = "-pinhole"; break; - case "right": x2 -= img2.getWidth(); anchor2 = "-right"; break; - default: throw new Error("Unknown XPlace option for image 2: " + placeX2); - } - switch(placeY2.toLowerCase()) { - case "top": y2 -= 0; anchor2 = "top" + anchor2; break; - case "center": y2 -= img2.getHeight() / 2; anchor2 = "center" + anchor2; break; - case "pinhole": y2 -= img2.getPinholeY(); anchor2 = "pinhole" + anchor2; break; - case "baseline": y2 -= img2.getBaseline(); anchor2 = "baseline" + anchor2; break; - case "bottom": y2 -= img2.getHeight(); anchor2 = "bottom" + anchor2; break; + case "bottom": y2 -= bb2.botRight.y; anchor2 = "bottom" + anchor2; break; default: throw new Error("Unknown YPlace option for image 2: " + placeY2); } - */ // Next, offset x2/y2 by the given offsetX/Y x2 += offsetX; y2 += offsetY; - var bb1 = getBB(img1); - var bb2 = getBB(img2); - var newBB = bb1.transform(Matrix.translation(x1, y1)).merge(bb2.transform(Matrix.translation(x2, y2))) - var centerX = newBB.getCenterX(); - var centerY = newBB.getCenterY(); + // Recenter the overlay, such that its midpoint is at (0,0) + // (This is not strictly necessary, though it does help "normalize" the images a bit.) + var leftX = Math.min(bb1.topLeft.x + x1, bb2.topLeft.x + x2) + var rightX = Math.min(bb1.botRight.x + x1, bb2.botRight.x + x2) + var topY = Math.min(bb1.topLeft.y + y1, bb2.topLeft.y + y2) + var bottomY = Math.min(bb1.botRight.y + y1, bb2.botRight.y + y2) + var centerX = (leftX + rightX) / 2; + var centerY = (topY + bottomY) / 2; x1 -= centerX; x2 -= centerX; y1 -= centerY; y2 -= centerY; - - // // Translate both offset pairs by the smaller of the half-dimensions - // var xMax = Math.max(img1.getWidth(), img2.getWidth()); - // var yMax = Math.max(img1.getHeight(), img2.getHeight()); - // x1 += xMax; x2 += xMax; - // y1 += yMax; y2 += yMax; - - // // Last, translate both offset pairs so that none are negative - // var xMin = Math.min(x1, x2) - // var yMin = Math.min(y1, y2) - // x1 -= xMin; x2 -= xMin; - // y1 -= yMin; y2 -= yMin; // store the offsets for rendering this.x1 = x1; @@ -1116,7 +1065,7 @@ this.y2 = y2; this.img1 = img1; this.img2 = img2; - this.pinhole = new Point2D(img1.getPinholeX() + x1, img1.getPinholeY() + y1); // TODO(Ben): +x1/y1? + this.pinhole = new Point2D(img1.getPinholeX() + x1, img1.getPinholeY() + y1); this.alphaBaseline = img1.alphaBaseline ? img1.getBaseline() + y1 : img2.getBaseline() + y2; // console.log("Baseline1: " + img1.alphaBaseline + ", Baseline2: " + img2.alphaBaseline + " ==> " + this.alphaBaseline); var shiftText = ""; @@ -1135,9 +1084,6 @@ OverlayImage.prototype.computeBB = function(tx) { var bb1 = this.img1.computeBB(tx.translate(this.x1, this.y1)); var bb2 = this.img2.computeBB(tx.translate(this.x2, this.y2)); - // console.log("Img1: ", this.x1, this.y1, JSON.stringify(bb1)); - // console.log("Img2: ", this.x2, this.y2, JSON.stringify(bb2)); - // console.log("Merged: ", JSON.stringify(bb1.union(bb2))); return bb1.merge(bb2); }; OverlayImage.prototype.getAriaText = function(depth) { @@ -1327,7 +1273,6 @@ ctx.beginPath(); ctx.strokeStyle = colorString(this.color); var bb = getBB(this.img); - // console.log(JSON.stringify(bb)); ctx.strokeRect(bb.topLeft.x, bb.topLeft.y, bb.botRight.x - bb.topLeft.x, bb.botRight.y - bb.topLeft.y); ctx.closePath(); ctx.restore(); @@ -1365,11 +1310,6 @@ ctx.restore(); var px = this.getPinholeX(); var py = this.getPinholeY(); - console.log("In pinhole, px = ", px, " py = ", py); - var tx = ctx.getTransform(); - var pt = new DOMPoint(px, py); - var txpt = pt.matrixTransform(tx); - console.log("Transformed pinhole, px = ", txpt.x, " py = ", txpt.y); ctx.beginPath(); ctx.strokeStyle = colorString(this.color1); ctx.lineWidth = 1.5; ctx.moveTo(px - 5, py); @@ -1914,7 +1854,6 @@ bb.addPoint(pt.matrixTransform(tx.rotate(-i))); } bb.addPoint(pt.matrixTransform(tx.rotate(-this.angle))); - // console.log("Final wedge bb", JSON.stringify(bb)); return bb; }; WedgeImage.prototype.getAriaText = function(depth) { diff --git a/src/web/js/trove/internal-image-typed.js b/src/web/js/trove/internal-image-typed.js index 5d6da6661..176a526a3 100644 --- a/src/web/js/trove/internal-image-typed.js +++ b/src/web/js/trove/internal-image-typed.js @@ -168,8 +168,6 @@ theModule: function(runtime, namespace, uri, imageLib, makeImage, jsnums) { var ffi = runtime.ffi; - console.log("From typed: ", imageLib); - var image = runtime.getField(imageLib, "internal"); var colorDb = image.colorDb; diff --git a/src/web/js/trove/internal-image-untyped.js b/src/web/js/trove/internal-image-untyped.js index 8830d2b31..346baaba5 100644 --- a/src/web/js/trove/internal-image-untyped.js +++ b/src/web/js/trove/internal-image-untyped.js @@ -160,8 +160,6 @@ var isString = runtime.isString; - console.log("From untyped: ", imageLib); - var image = runtime.getField(imageLib, "internal"); var colorDb = image.colorDb; From 73e203bde0a48b878bf7bc0c72c596a49ee9c234 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Sun, 3 Jan 2021 21:18:39 -0500 Subject: [PATCH 11/12] fix a test by putting the pinhole back at the geometric center rather than the barycenter --- test/image-equality.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/image-equality.js b/test/image-equality.js index 4fb554776..92cd3e7e4 100644 --- a/test/image-equality.js +++ b/test/image-equality.js @@ -226,7 +226,8 @@ replaceArgs` beside(acc, overlay-align(${"xPinhole"}, ${"yPinhole"}, draw-pinhole(circle(50 / num-sqrt(2), ${"outline"}, ${"black"})), - draw-pinhole(rotate(((row * 10) + col) * 12, triangle-sas(50, 90, 50, ${"outline"}, ${"green"}))))) + draw-pinhole(rotate(((row * 10) + col) * 12, + center-pinhole(triangle-sas(50, 90, 50, ${"outline"}, ${"green"})))))) end, acc) end`], From 065653214d6d3100581feb03f27c1c5db8b4b0a4 Mon Sep 17 00:00:00 2001 From: Ben Lerner Date: Tue, 7 Sep 2021 09:11:25 -0400 Subject: [PATCH 12/12] don't recall what this debugging code was for... --- src/web/js/trove/image-lib.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/web/js/trove/image-lib.js b/src/web/js/trove/image-lib.js index c7d00e39d..1804e1816 100644 --- a/src/web/js/trove/image-lib.js +++ b/src/web/js/trove/image-lib.js @@ -545,14 +545,14 @@ ctx.strokeStyle = colorString(this.color); ctx.stroke(); } - ctx.beginPath(); - var tx = ctx.getTransform(); - ctx.setTransform(1, 0, 0, 0, 1, 0); - var pt = this.pinhole.matrixTransform(tx); - ctx.ellipse(pt.x, pt.y, 3, 3, 0, 0, Math.PI * 2.0); - ctx.closePath(); - ctx.fillStyle = "red"; - ctx.fill(); + // ctx.beginPath(); + // var tx = ctx.getTransform(); + // ctx.setTransform(1, 0, 0, 0, 1, 0); + // var pt = this.pinhole.matrixTransform(tx); + // ctx.ellipse(pt.x, pt.y, 3, 3, 0, 0, Math.PI * 2.0); + // ctx.closePath(); + // ctx.fillStyle = "red"; + // ctx.fill(); ctx.restore();