// Emacs settings: -*- mode: Fundamental; tab-width: 4; -*- //////////////////////////////////////////////////////////////////////////// // // // A web page for designing Celtic knots, implemented in Javascript // // // // Copyright 1998-2009, Andrew D. Birrell // // // // Javascript // // // //////////////////////////////////////////////////////////////////////////// // Assumes the following globals: // sqSize (pixels wide and high per square) // cols (number of squares) // rows (number of squares) // skin (the choice of image: "wide", "narrow", or "tiny") // // Expects each square to be an IMG, with ID of the form "img-1-2" // // The globals are set, and the image array created, by a server-side PHP // script "knotwork.php". See "knotwork-php.txt" for the source code. // In turn, "knotwork.php" was invoked by inclusion from "index.php" or // "large.php". See "index-php.txt" for source. /* The knot is built out of square images, in a rectangular array "cols" squares by "rows" squares (both even). The intersections occur at the top-left corner of the squares [x,y] where x+y is odd. The state of the intersections is held in the "cuts" array, indexed as if there were cols/2+1 intersection in each row (even although there are actually only cols/2 intersections in even rows). Note that there's an asymmetry in our data structures between x and y directions. This is an arbitrary choice - we could have chosen to pack the "cuts" array with either axis first. */ // The underlying image has the following shapes in its first row: var sh_TLBR = 0; // shape: straight diagonal TL var sh_BLTR = 1; // shape: straight diagonal BL var sh_TB = 2; // shape: straight top -> bottom var sh_LR = 3; // shape: straight left -> right var sh_TLB = 4; // shape: curve TL -> bottom var sh_TLR = 5; // shape: curve TL -> right var sh_BRT = 6; // shape: curve BR -> top var sh_BRL = 7; // shape: curve BR -> left var sh_BLT = 8; // shape: curve BL -> top var sh_BLR = 9; // shape: curve BL -> right var sh_TRB = 10; // shape: curve TR -> bottom var sh_TRL = 11; // shape: curve TR -> left var sh_TL = 12; // shape: arc top -> left var sh_BR = 13; // shape: arc bottom -> right var sh_BL = 14; // shape: arc bottom -> left var sh_TR = 15; // shape: arc top -> right // The second row is variants that go underneath at intersections var sh_uTLBR = 16; // shape: straight diagonal TL var sh_uBLTR = 17; // shape: straight diagonal BL var sh_uTB = 18; // shape: straight top -> bottom var sh_uLR = 19; // shape: straight left -> right var sh_uTLB = 20; // shape: curve TL -> bottom var sh_uTLR = 21; // shape: curve TL -> right var sh_uBRT = 22; // shape: curve BR -> top var sh_uBRL = 23; // shape: curve BR -> left var sh_uBLT = 24; // shape: curve BL -> top var sh_uBLR = 25; // shape: curve BL -> right var sh_uTRB = 26; // shape: curve TR -> bottom var sh_uTRL = 27; // shape: curve TR -> left var sh_uTL = 28; // shape: arc top -> left var sh_uBR = 29; // shape: arc bottom -> right var sh_uBL = 30; // shape: arc bottom -> left var sh_uTR = 31; // shape: arc top -> right // State of cuts var cut_none = 0; // no cut, normal intersection var cut_hor = 1; // horizontal cut var cut_ver = 2; // vertical cut /* The cut points are at top-left of the squares [x,y] where x+y is odd. The state of cut/intersection points is held in the "cuts" array, indexed as if there were cols/2+1 entries per row (even although there are actually 1 less in even rows). Note that there's an asymmetry in our data structures between x and y directions. This is an arbitrary choice - we could have chosen to pack the "cuts" array with either axis first. */ var xCuts; // cuts per row var cuts = new Array(); // the state of play /* The over-under rule is that lines going diagonally down-right pass underneath in even rows, and over in odd rows. This rule always produces properly woven knots. We treat the cells in groups of four, named by their position in the following diagram, where the square at the top-left is [x,y] with x and y both even, and where "o" is an intersection or cut point. + -- o -- + | TL | TR | | | | o -- + -- o | BL | BR | | | | + -- o -- + We use the following arrays to determine which image to display in each square. If the square is a TL square (x and y both even), we use an entry from the "tlShapes" array; a TR square uses the "trShapes" array, etc. The arrays are indexed by the value of the cuts adjacent to the given square. The index is the adjacent even-row cut plus 3 * the adjacent odd-row cut. This deals completely with shape of thread fragment, and with the over-under choice. */ /* none hor ver */ var tlShapes = [sh_BLTR, sh_uBLR, sh_uBLT, /* none */ sh_TRL, sh_LR, sh_TL, /* hor */ sh_TRB, sh_BR, sh_TB]; /* ver */ var trShapes = [sh_uTLBR, sh_BRL, sh_BRT, /* none */ sh_uTLR, sh_LR, sh_TR, /* hor */ sh_uTLB, sh_BL, sh_TB]; /* ver */ var blShapes = [sh_TLBR, sh_TLR, sh_TLB, /* none */ sh_uBRL, sh_LR, sh_BL, /* hor */ sh_uBRT, sh_TR, sh_TB]; /* ver */ var brShapes = [sh_uBLTR, sh_uTRL, sh_uTRB, /* none */ sh_BLR, sh_LR, sh_BR, /* hor */ sh_BLT, sh_TL, sh_TB]; /* ver */ function computeShape(x, y) { // Compute appropriate shape for square [x,y], looking at adjacent cuts var evenRow = cuts[Math.floor((y+1)/2) * 2 * xCuts + Math.floor(x/2)]; var oddRow = cuts[(Math.floor(y/2)*2+1) * xCuts + Math.floor((x+1)/2)]; var elt = document.getElementById("img-" + x + "-" + y); var shapes = y%2==0 ? ( x%2==0 ? tlShapes : trShapes ) : ( x%2==0 ? blShapes : brShapes ); elt.src = "image.php?img=" + skin + "&n=" + shapes[evenRow + oddRow*3] + "&w=" + sqSize; } function getElementPos(element) { // Return (x,y) for top-left of the given element, relative to document var res = Object(); res.x = 0; res.y = 0; for (var obj = element; obj.offsetParent; obj = obj.offsetParent) { res.x += obj.offsetLeft; res.y += obj.offsetTop; } return res; } function getMousePos(event) { // Return (x,y) for mouse coordinates, relative to the document // Thanks to quirksmode.org for browser-specific details var mouse = Object(); if (event.pageX) { mouse.x = event.pageX; mouse.y = event.pageY; } else { mouse.x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; mouse.y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; } return mouse; } function clickImg(event) { // Respond to click by letting the user adjust the cut-points // Round to index of square whose TL the click was closest to. var tl = getElementPos(document.getElementById("img-0-0")); if (!event) event = window.event; // IE versus the rest var mouse = getMousePos(event); var xCoord = mouse.x - tl.x; var yCoord = mouse.y - tl.y; var x = Math.floor((xCoord + Math.floor(sqSize/2)) / sqSize); var y = Math.floor((yCoord + Math.floor(sqSize/2)) / sqSize); if ( (x+y)%2 == 1 && x > 0 && x < cols && y > 0 && y < rows) { /* (x+y) is odd, so the closest square is a mutable cut point */ var cutIndex = y * xCuts + Math.floor(x/2); // index of cut point cuts[cutIndex] = (cuts[cutIndex] + 1) % 3; computeShape(x-1, y-1); computeShape(x, y-1); computeShape(x-1, y); computeShape(x, y); } return false; } function init() { // Initialize the knot to a simple weave - no cuts except at perimeter xCuts = Math.floor(cols/2) + 1; var n; for (n = 0; n < (rows+1)*xCuts; n++) { cuts[n] = cut_none; } /* Place fixed cuts around the perimeter */ var y; for (y = 0; y <= rows; y++) { if (y%2 == 1) { // cut at left and right edges in odd rows var x0 = y * xCuts; cuts[x0] = cut_ver; cuts[x0+xCuts-1] = cut_ver; } } var xLast = rows * xCuts; var x; for (x = 0; x < xCuts; x++) { cuts[x] = cut_hor; cuts[xLast+x] = cut_hor; } /* Set the initial shapes */ for (y = 0; y < rows; y++) { for (x = 0; x < cols; x++) { computeShape(x,y); } } }