export function ApexForBaseAngles(a0: number, a1: number): Coords2d { let leftTangent = OffsetCoords2d.fromPolar(1, a0).rootedAtCoords(Coords2d.origin()); let rightTangent = OffsetCoords2d.fromPolar(1, Math.PI - a1) .rootedAtCoords(Coords2d.fromXY(1, 0)); return leftTangent.intersectionWith(rightTangent); }
elementsToRender (frameRect: AxisRect): Array<CanvasRender.Renderable> { console.log("BilliardsFlowModel.elementsToRender"); let image = new ImageRendering( AxisRect.fromXYWH(0, 0, 1, 1), this.gridSize, (imageData: ImageData) => { let width = this.gridSize.width; let height = this.gridSize.height; //let renderScale = this.renderTarget.canvas.renderScale; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let val = Math.floor(255.9 * this.grid[y * width + x]); let offset = y * width + x; //let modelCoords = pixelSpaceToModelSpace.transformCoords( // Coords2d.fromXY(x / renderScale, y / renderScale)); imageData.data[offset*4 + 0] = val; imageData.data[offset*4 + 1] = 0; imageData.data[offset*4 + 2] = val; imageData.data[offset*4 + 3] = 255; } } }); let line = new Line(Coords2d.fromXY(0, 0), Coords2d.fromXY(1, 0)); return [image, line]; }
constructor(container: HTMLElement, viewSize: [number, number]) { // Internal UI state this._viewSize = viewSize; this._container = container; this._canvasSize = [viewSize[0] / 2, viewSize[1]]; this._canvasBounds = new Rect( Coords2d.fromXY(0, this._canvasSize[1]), OffsetCoords2d.fromDxDy(this._canvasSize[0], -this._canvasSize[1]) ); this._needsRedraw = false; this.gluingBounds = Rect.fromDimensions(2, 2); this.editMode = 'triangle'; // Internal array to store the embedded 3-d triangles. this._distance = 3; this._triangle = Triangle2d.fromCoords( Coords2d.origin(), Coords2d.fromXY(1, 0), Coords2d.fromXY(0.5, Math.sqrt(3)/4) ); this._rotation = Quaternion.fromReal(1); this._gluing = TriangleTetraGluing.fromEdgeIndexAndGluePoint(0, 0); this._refreshEmbedding(); }
export function TriangleForApex(apex: Coords2d): Triangle2d { return Triangle2d.fromCoords( Coords2d.origin(), Coords2d.fromXY(1, 0), apex ); }
static fromSideLengths (a: number, b: number, c: number): Triangle2d { //throw "Triangle.fromSideLengths unimplemented"; // Numerically stable Heron's formula: var area = Math.sqrt( (a + (b + c)) * (c - (a - b)) * (c + (a - b)) * (a + (b - c))); // Now treat b as the length of the base, with the first vertex of the // triangle being the apex, which moves counterclockwise along a. var ascent = 2 * area / b; // The right-side root from the left side of the base. var rightLeft = b - Math.sqrt(c - ascent * ascent); // The left-side root from the right side of the base. var leftRight = Math.sqrt(a - ascent * ascent); var apexX; if (rightLeft < 0) { apexX = rightLeft; } else { apexX = leftRight; } return Triangle2d.fromCoords( Coords2d.fromXY(apexX, ascent), Coords2d.fromXY(0, 0), Coords2d.fromXY(b, 0) ); }
vertexCoords (): Array<Coords2d> { let {x, y} = this.origin; let {width, height} = this.size; return [ Coords2d.fromXY(x, y), Coords2d.fromXY(x + width, y), Coords2d.fromXY(x + width, y + height), Coords2d.fromXY(x, y + height)]; }
vertexCoords (): Array<Coords2d> { let o = this.origin; let d = this.diagonal; return [ Coords2d.fromXY(o.x, o.y), Coords2d.fromXY(o.x + d.dx, o.y), Coords2d.fromXY(o.x + d.dx, o.y + d.dy), Coords2d.fromXY(o.x, o.y + d.dy)]; }
export function addLinesForTriangle( elements: Array<Renderable>, origin: Coords2d, sign: number) { let v0 = origin; let v1 = Coords2d.fromXY(origin.x + sign*triWidth, origin.y); let v2 = Coords2d.fromXY( origin.x + sign*triWidth/2, origin.y + sign * triHeight); elements.push(new Line(v0, v1)); elements.push(new Line(v1, v2)); elements.push(new Line(v2, v0)); elements.push(new Line( origin.plus(triBase[0].asOffsetFromOrigin().timesReal(sign)), origin.plus(triRight.asOffsetFromOrigin().timesReal(sign)))); let inner = [ triInner[0].asOffsetFromOrigin(), triInner[1].asOffsetFromOrigin()]; elements.push(new Line( origin.plus(triLeft.asOffsetFromOrigin().timesReal(sign)), origin.plus(inner[0].timesReal(sign)))); elements.push(new Line( origin.plus(triBase[2].asOffsetFromOrigin().timesReal(sign)), origin.plus(inner[1].timesReal(sign)))); return elements; }
elementsToRender (frameRect: AxisRect): Array<CanvasRender.Renderable> { let elements: Array<CanvasRender.Renderable> = []; let boundaryRect = AxisRect.fromXYWH(0, 0, this.columnCount, this.rowCount); let borderElement = Polygon.fromRect(boundaryRect.addMargin(0.35)); elements.push(borderElement); let colors = ['#eeeeee', '#fae', '#000066']; for (let i = 0; i < this.rowCount; i++) { let y = (i + 0.5); for (let j = 0; j < this.columnCount; j++) { let x = (j + 0.5); let value = this.grid(this.rowCount - i - 1, j); let color = '#333333'; if (value >= 0 && value < colors.length) { color = colors[value]; } let cellRect = AxisRect.fromCenterAndDimensions( Coords2d.fromXY(x, y), 1, 1); let cellElement = Polygon.fromRect(cellRect.scaleBy(0.9)); cellElement.style.fillStyle = color; cellElement.style.strokeStyle = ""; elements.push(cellElement); } } return elements; }
return (newCanvasCoords: Coords2d) => { var newModelCoords = canvasToModel.transformCoords(newCanvasCoords); if (newModelCoords.y < 0.01) { newModelCoords.y = 0.01; } if (newModelCoords.x < 0) { newModelCoords.x = 0; } if (newModelCoords.x > 1) { newModelCoords.x = 1; } var modelDelta = newModelCoords.asOffsetFrom(modelCoords); let newApexCoords = originalApexCoords.plus(modelDelta); let newSourceY = newApexCoords.y * originalSourceVert; let newSourceX = newApexCoords.x + (originalSourceHorz - newApexCoords.x) * (1.0 - originalSourceVert); this.apex = newApexCoords; this.source.base = Coords2d.fromXY(newSourceX, newSourceY); this.source.offset.dy = originalSource.offset.dy * (newApexCoords.y / originalApexCoords.y); this.source.offset = this.source.offset.normalized(); this._needsRedraw = true; };
return (newCanvasCoords: Coords2d, final: boolean) => { let canvasOffset = newCanvasCoords.asOffsetFrom(canvasCoords); let modelOffset = canvasToModel.transformOffset(canvasOffset); let indexOffset = Math.round(modelOffset.dx / intervalWidth); let newIndex = selectedIndex + indexOffset; if (currentIndex != newIndex) { currentIndex = newIndex; let forwardMap: Array<number> = originalPermutation.forwardMap; let newForwardMap: Array<number> = forwardMap.map( (index: number) => { if (index < Math.min(selectedIndex, newIndex) || index > Math.max(selectedIndex, newIndex)) { return index; } if (index == selectedIndex) { return newIndex; } if (selectedIndex < newIndex) { return index - 1; } return index + 1; }); let newPermutation = Permutation.fromForwardMap(newForwardMap); if (selectedRow == 0) { this.inputOrder = newPermutation; } else { this.outputOrder = newPermutation; } this.setNeedsRedraw(); } };
vectorForAngleIndex (i: number): OffsetCoords2d { let v: OffsetCoords2d; if (i == 0) { v = this.apex.asOffsetFromOrigin(); } else { v = OffsetCoords2d.fromDxDy(1 - this.apex.x, -this.apex.y); } return v.times(v).timesReal(1.0 / v.squaredLength()); }
_thingie () { for (let i = 0; i < this.edgePaths.length; i++) { if (!EdgePathIsClosed(this.edgePaths[i])) { window.console.log( `Warning: edge path ${i} (${this.edgePaths[i]}) is not closed`); } } let edgeSequences = this.edgePaths.map(EdgeSequenceForEdgePath); let cutoff = edgeSequences.length - this.highlightCount; this.extra = []; let h = Math.sqrt(3) / 3; let w = 1; let tri = this.triangle; //let xres = 100; //let yres = 50; //let dotRadius = 0.007; let xres = 200; let yres = 100; let dotRadius = 0.0045; let highlighted = 0; let sequenceCounts = edgeSequences.map(() => 0); for (let y = 1; y <= yres; y++) { for (let x = 0; x <= xres; x++) { for (let i = 0; i < edgeSequences.length; i++) { //for (let i = edgeSequences.length - 1; i >= 0; i--) { let edgeSequence = edgeSequences[i]; let v = tri.vertexCoords; v[2] = Coords2d.fromXY(v[0].x + x * w / xres, v[0].y + y * h / yres); let margin = PathMargin(tri, edgeSequence); if (margin > 0) { let c = CanvasRender.Circle.fromCenterAndRadius(v[2], dotRadius); if (i >= cutoff) { c.style.fillStyle = '#bb8888'; highlighted++; } else { let intensity = Math.min(Math.sqrt(margin), 1); let rg = Math.floor(128 * (1 - intensity)); let b = Math.floor(rg + 255 * intensity); c.style.fillStyle = `rgb(${rg},${rg},${b}`; } this.extra.push(c); sequenceCounts[i]++; break; } } } } window.console.log(`${highlighted} points were highlighted`); this.sequenceResults = []; for (let i = 0; i < sequenceCounts.length; i++) { if (sequenceCounts[i] > 0) { this.sequenceResults.push([i, sequenceCounts[i]]); } } this._needsRedraw = true; }
_internalMouseDown(x: number, y: number) { var canvasCoords = Coords2d.fromXY(x, y); let listener = this.controlPointsCallback(canvasCoords); if (listener != null) { this._isMouseDown = true; this._activePoint = { listener: listener, canvasCoords: canvasCoords, }; } }
nearestVertexTo (coords: Coords2d): number { var bestVertex = -1; var bestDist = 0; for (var i = 0; i < 3; i++) { var distance = coords.distanceFrom(this.vertexCoords[i]); if (bestVertex < 0 || distance < bestDist) { bestVertex = i; bestDist = distance; } } return bestVertex; }
return (newCanvasCoords: Coords2d, final: boolean) => { let canvasOffset = newCanvasCoords.asOffsetFrom(canvasCoords); let modelOffset = canvasToModel.transformOffset(canvasOffset); let newPosition = modelCoords.x + modelOffset.dx; let delta = newPosition - modelCoords.x; let rationalDelta = Rational.approximationForNumber(delta, 5); let newLeftBoundary = intervals[oldIndex].leftBoundary.plus(rationalDelta); // Find the output interval index closest to newLeftBoundary let curPos = intervals[0].leftBoundary; let newIndex = 0; for (let i = 0; i < intervals.length; i++) { if (i == oldIndex) { continue; } let curLength = intervals[i].length; let centerPos = curPos.plus(curLength.dividedBy(Rational.fromInt(2))); if (centerPos.greaterThan(newLeftBoundary)) { break; } newIndex++; curPos = curPos.plus(curLength); } if (currentIndex != newIndex) { console.log(`Moving to index ${newIndex}`); currentIndex = newIndex; let forwardMap: Array<number> = intervalExchangeMap.permutation.forwardMap; let newForwardMap: Array<number> = forwardMap.map( (outputIndex: number) => { if (outputIndex < Math.min(oldIndex, newIndex) || outputIndex > Math.max(oldIndex, newIndex)) { return outputIndex; } if (outputIndex == oldIndex) { return newIndex; } if (oldIndex < newIndex) { return outputIndex - 1; } return outputIndex + 1; }); let newPermutation = Permutation.fromForwardMap(newForwardMap); this.intervalExchangeMap.set( IntervalExchangeMap.fromLengthsAndPermutation( intervalExchangeMap.intervalLengths, newPermutation)); } };
return (newCanvasCoords: Coords2d, final: boolean) => { let canvasOffset = newCanvasCoords.asOffsetFrom(canvasCoords); let modelOffset = canvasToModel.transformOffset(canvasOffset); let newPosition = modelCoords.x + modelOffset.dx; newPosition = Math.max(newPosition, minValue); newPosition = Math.min(newPosition, maxValue); let delta = newPosition - modelCoords.x; let rationalDelta = Rational.approximationForNumber(delta, 5); let newLengths = intervalExchangeMap.intervalLengths.map((x) => x); newLengths[targetIndex] = newLengths[targetIndex].plus(rationalDelta); newLengths[targetIndex + 1] = newLengths[targetIndex + 1].minus(rationalDelta); this.intervalExchangeMap.set( IntervalExchangeMap.fromLengthsAndPermutation( newLengths, intervalExchangeMap.permutation)); };
elementsToRender (modelFrame: AxisRect): Array<Renderable> { let horizontalShift = true; let trueCenter = false; let strokeStyle = '#ff33ee'; let lineWidth = '2'; let elements: Array<Renderable> = []; console.log("elementsToRender"); let xOffset = 0; if (trueCenter) { xOffset += centerOffset; } if (horizontalShift) { xOffset += triWidth / 2; } for (let i = -10; i <= 10; i++) { let origin = Coords2d.fromXY(i * triWidth, 1.5); addLinesForTriangle(elements, Coords2d.fromXY(origin.x, origin.y), 1); addLinesForTriangle(elements, Coords2d.fromXY(origin.x + triWidth/2, origin.y + triHeight), -1); addLinesForTriangle(elements, Coords2d.fromXY(origin.x + xOffset, origin.y - triHeight), 1); addLinesForTriangle(elements, Coords2d.fromXY(origin.x + triWidth/2 + xOffset, origin.y), -1); } let rowShift = 0.5721554288609622; // The angle of the fundamental interval we want to map to the // x axis let angle = Math.atan2(-1, rowShift); // The distance of each cell along the x axis let dx = Math.sqrt(1 + rowShift * rowShift); for (let i = -10; i <= 10; i++) { let origin = Coords2d.fromXY(i * dx, -3); addLinesForSquare(elements, origin, -angle); /*for (let j = 0; j < 3; j++) { addLinesForSquare(elements, Coords2d.fromXY(origin.x + rowShift * j, origin.y - j), 0);//-angle); }*/ } for (let i = 0; i < elements.length; i++) { let e = elements[i]; e.style.strokeStyle = strokeStyle; e.style.lineWidth = lineWidth; } return elements; }
export function PathMargin ( triangle: Triangle2d, edgeSequence: Array<number>): number { let tri = triangle.copy(); let leftBoundary = []; let rightBoundary = []; let prevEdge = 0; for (let i = 0; i < edgeSequence.length; i++) { let edgeDelta = (edgeSequence[i] - (prevEdge + 1) + 3) % 3; // always 0 or 1 if (edgeDelta == i % 2) { // Right leftBoundary.push(tri.vertex(edgeSequence[i] + 1 - (i % 2))); } else { // Left rightBoundary.push(tri.vertex(edgeSequence[i] + (i % 2))); } tri.reflectThroughEdge(edgeSequence[i]); prevEdge = edgeSequence[i]; } let offset = tri.vertexCoords[0] .asOffsetFrom(triangle.vertexCoords[0]); let lineCoeff = OffsetCoords2d.fromDxDy(-offset.dy, offset.dx).normalized(); let o = Coords2d.origin(); let minLeft = lineCoeff.dot(triangle.vertexCoords[0].asOffsetFrom(o)); for (let i = 0; i < leftBoundary.length; i++) { let coeff = lineCoeff.dot(leftBoundary[i].asOffsetFrom(o)); if (coeff < minLeft) { minLeft = coeff; } } let maxRight = lineCoeff.dot(triangle.vertexCoords[1].asOffsetFrom(o)); for (let i = 0; i < rightBoundary.length; i++) { let coeff = lineCoeff.dot(rightBoundary[i].asOffsetFrom(o)); if (coeff > maxRight) { maxRight = coeff; } } //window.console.log(`${minLeft}, ${maxRight}`); return minLeft - maxRight; }
constructor(renderTarget: RenderTarget) { this._renderTarget = renderTarget; this.renderParams = new BilliardsPhaseRendererParams(); this._renderer = new BilliardsPhaseRenderer(renderTarget.canvas, _paletteShader); this._redrawCounter = 1000; this._needsRedraw = false; this._controlPointsInterface = null; // Exposed properties this.numerator = ComplexPoly.fromReal(1); this.denominator = ComplexPoly.fromReal(1); this.phaseOffset = 0; this.modelBounds = AxisRect.fromXYWH(-1, -1, 2, 2); this._renderer.renderFrom(this.modelBounds); // The coordinates constantOffset was derived from. this._offsetCoords = Coords2d.fromXY(0, 0); this._createControlInterfaces(); requestAnimationFrame(this.generateClockTick()); }
constructor (target: RenderTarget) { this.target = target; this.modelBounds = AxisRect.fromCenterAndDimensions( Coords2d.origin(), 2, 2); this.backgroundColor = "white"; }
center (): Coords2d { return this.origin.plus(this.size.asOffsetCoords().timesReal(0.5)); }
static fromCenterAndDimensions ( center: Coords2d, width: number, height: number): AxisRect { let origin = Coords2d.fromXY(center.x - width/2, center.y - height/2); let size = Size.fromWH(width, height); return new AxisRect(origin, size); }
static fromXYWH (x: number, y: number, w: number, h: number): AxisRect { return new AxisRect(Coords2d.fromXY(x, y), Size.fromWH(w, h)); }
constructor (start: Coords2d, end: Coords2d) { super(); this.start = start.copy(); this.end = end.copy(); this.lineDash = null; }
// @flow import { ElementsModel } from 'faec/elements_model'; import { Coords2d, OffsetCoords2d } from 'faec/geometry'; import { AxisRect } from 'faec/polygon'; import { Line, Renderable } from 'faec/element_render'; let squareRoot = Math.sqrt(3); let fourthRoot = Math.sqrt(squareRoot); // (w, h) are the dimensions of the unit-area equilateral triangle // with its base aligned with the x axis. let triWidth = 2 / fourthRoot; let triHeight = fourthRoot; let triVerts = [ Coords2d.origin(), Coords2d.fromXY(triWidth, 0), Coords2d.fromXY(triWidth / 2, triHeight)]; let triLeft = Coords2d.midpoint(triVerts[0], triVerts[2]); let triRight = Coords2d.midpoint(triVerts[1], triVerts[2]); // The horizontal width of the long diagonal, which goes from // triRight to the base and has length 1. let diagonalWidth = Math.sqrt(1.0 - triRight.y * triRight.y); let diagonal = OffsetCoords2d.fromDxDy(diagonalWidth, triRight.y); let triBase = [ Coords2d.fromXY(triRight.x - diagonalWidth, 0), Coords2d.fromXY((triRight.x - diagonalWidth) * 2, 0), Coords2d.fromXY((triRight.x - diagonalWidth) + triWidth/2, 0)]; // The offset of the center base point from the geometric center
center (): Coords2d { return this.origin.plus(this.diagonal.timesReal(0.5)); }
static fromDimensions (width: number, height: number): Rect { return Rect.fromCenterAndDimensions(Coords2d.origin(), width, height); }
static fromXYWH (x: number, y: number, width: number, height: number): Rect { return new Rect( Coords2d.fromXY(x, y), OffsetCoords2d.fromDxDy(width, height)); }
export function addLinesForSquare( elements: Array<Renderable>, origin: Coords2d, angle: number) { // Extract the dimensions for the capstone triangle from its // simplest orientation in the base of the equilateral triangle. let bottom = capLeft; let top = 1 - capLeft; let sin = Math.sin(angle); let cos = Math.cos(angle); let basis = [ OffsetCoords2d.fromDxDy(cos, sin), OffsetCoords2d.fromDxDy(-sin, cos), ]; // All six corners in two adjacent grid squares, ordered // widdershins from the base. let corners = [ origin, origin.plus(basis[0]), origin.plus(basis[0].timesReal(2)), origin.plus(basis[0].timesReal(2)).plus(basis[1]), origin.plus(basis[0]).plus(basis[1]), origin.plus(basis[1])]; elements.push(new Line(corners[0], corners[2])); elements.push(new Line(corners[2], corners[3])); elements.push(new Line(corners[3], corners[5])); elements.push(new Line(corners[5], corners[0])); elements.push(new Line(corners[1], corners[4])); let vTop = [ corners[5].plus(basis[0].timesReal(top)), corners[3].plus(basis[0].timesReal(-bottom))]; let vBottom = [ corners[0].plus(basis[0].timesReal(bottom)), corners[2].plus(basis[0].timesReal(-top))]; let vLeft = corners[0].plus(basis[1].timesReal(0.5)); let vCenter = corners[1].plus(basis[1].timesReal(0.5)); let vRight = corners[2].plus(basis[1].timesReal(0.5)); let innerVecs = [ vLeft.asOffsetFrom(vBottom[0]), vRight.asOffsetFrom(vTop[1])]; let innerCoeff = triBase[0].x / (triWidth / 2); let vInner = [ vBottom[0].plus(innerVecs[0].timesReal(innerCoeff)), vTop[1].plus(innerVecs[1].timesReal(innerCoeff))]; elements.push(new Line(vLeft, vBottom[0])); elements.push(new Line(vInner[0], vCenter)); elements.push(new Line(vInner[0], vTop[0])); elements.push(new Line(vTop[1], vRight)); elements.push(new Line(vInner[1], vCenter)); elements.push(new Line(vInner[1], vBottom[1])); let vOutsideLeft = corners[5].plus(basis[0].timesReal(-capLeft)); elements.push(new Line(vOutsideLeft, vLeft)); let vOutsideRight = corners[2].plus(basis[0].timesReal(capLeft)); elements.push(new Line(vRight, vOutsideRight)); console.log(`Horizontal shift: ${1 - top}`); }