diff --git a/src/fencer.html b/src/fencer.html index 75dff20..8142863 100644 --- a/src/fencer.html +++ b/src/fencer.html @@ -183,17 +183,26 @@ display: grid; grid-template-columns: 40px 10px 1fr 10px; grid-template-rows: 40px 10px 1fr 10px 80px; + background-color: #eee; + } + .svg-container { /* this contains the SVG element */ + position: relative; } #mappings-visual { /* this is the SVG element */ - background-color: #eee; - width: 100%; - height: 100%; + position: absolute; + width: calc(100% + 20px); + height: calc(100% + 20px); + left: -10px; + top: -10px; } .ruler { - background-color: #ccc; + position: relative; color: black; font-size: 0.8em; } + .extra { + background-color: white; + } } .window.mappings { @@ -227,6 +236,9 @@ .zoom { font-family: Material Symbols Outlined; } + details[open] { + background-color: #bad5fe; + } } .window.renders { @@ -335,10 +347,6 @@ display: none; } -.mappings details[open] { - background-color: #bad5fe; -} - input.numeric { font-weight: bold; width: calc(100% - 7px); @@ -349,9 +357,9 @@ color: grey; } - + @@ -393,9 +401,9 @@

Mappings

-
Ruler
+
-
R
+
@@ -416,7 +424,6 @@

Mappings


-
diff --git a/src/fencer.js b/src/fencer.js index f2341a2..bb9d3e3 100644 --- a/src/fencer.js +++ b/src/fencer.js @@ -231,16 +231,14 @@ function svgCoordFromAxisCoord (a, val) { const axis = GLOBAL.font.fvar.axes[a]; const rect = Q(".svg-container").getBoundingClientRect(); const length = parseFloat((visibleAxisIds[0] === a) ? rect.width : rect.height); - return (val - axis.minValue) / (axis.maxValue - axis.minValue) * length; + return Math.round((val - axis.minValue) / (axis.maxValue - axis.minValue) * length * 1000) / 1000; // round to nearest 0.001 (avoids tiny rounding errors that bloat SVG) } function svgCoordsFromAxisCoords (coords) { - const a0 = coords[GLOBAL.mappingsView[0]]; - const a1 = coords[GLOBAL.mappingsView[1]]; - - const s0 = svgCoordFromAxisCoord(GLOBAL.mappingsView[0], a0); - const s1 = svgCoordFromAxisCoord(GLOBAL.mappingsView[1], a1); + const [a0, a1] = GLOBAL.mappingsView; + const s0 = svgCoordFromAxisCoord(a0, coords[a0]); + const s1 = svgCoordFromAxisCoord(a1, coords[a1]); return [s0, s1]; } @@ -748,7 +746,10 @@ function svgMouseMove(e) { const visibleAxisIds = getVisibleAxisIds(); // which axes are we using? const el = GLOBAL.dragging; // not e.target const index = parseInt(el.dataset.index); - const rect = GLOBAL.svgEl.getBoundingClientRect(); + //const rect = GLOBAL.svgEl.getBoundingClientRect(); + const rect = Q(".svg-container").getBoundingClientRect(); + + const mousex = e.clientX; const mousey = rect.height - e.clientY; const x = mousex - rect.left; @@ -843,8 +844,9 @@ function mappingsChanged(mode) { const visibleAxisIds = getVisibleAxisIds(); const visibleAxes = visibleAxisIds.map(a => GLOBAL.font.fvar.axes[a]); const gridLocations = []; - const xGraticules = getGraticulesForAxis(visibleAxes[0]); - const yGraticules = getGraticulesForAxis(visibleAxes[1]); + const graticuleStyle = Q("#grid-style").value; + const xGraticules = getGraticulesForAxis(visibleAxes[0], graticuleStyle); + const yGraticules = getGraticulesForAxis(visibleAxes[1], graticuleStyle); // draw a grid xGraticules.forEach(x => { @@ -1009,16 +1011,19 @@ function mappingsChanged(mode) { } // ok start redrawing the SVG - GLOBAL.svgEl.innerHTML = ""; + Q("#mappings-visual g").innerHTML = ""; + + // get base rectangle + const rect = Q(".svg-container").getBoundingClientRect(); + + // draw a white rectangle to clear the SVG + Q("#mappings-visual g").append(SVG("rect", {x:0, y:0, width:rect.width, height:rect.height, fill: "white"})); // draw a white rectangle // draw x-axis and y-axis const svgOriginCoords = svgCoordsFromAxisCoords(getDefaultAxisCoords()); - const rect = Q(".svg-container").getBoundingClientRect(); - const xAxisEl = SVG("line", {x1:0, y1:svgOriginCoords[1], x2:rect.width, y2:svgOriginCoords[1], stroke: "black", strokeWidth: 2}); - const yAxisEl = SVG("line", {x1:svgOriginCoords[0], y1:0, x2:svgOriginCoords[0], y2:rect.height, stroke: "black", strokeWidth: 2}); - GLOBAL.svgEl.appendChild(xAxisEl); - GLOBAL.svgEl.appendChild(yAxisEl); + const axesEl = SVG("path", {d: `M0,${svgOriginCoords[1]}H${rect.width}M${svgOriginCoords[0]},0V${rect.height}Z`, fill: "none", stroke: "black", "stroke-width": 2}); // draw the axes with 2 lines + Q("#mappings-visual g").append(axesEl); // draw grid locations as a grid if (Q("#grid-style").value.startsWith("grid-")) { @@ -1028,17 +1033,17 @@ function mappingsChanged(mode) { // vertical lines for (let xn=0; xn < xGraticules.length; xn++) - for (let yn=0; yn < yGraticules.length; yn++) - pathStr += (yn === 0 ? "M" : "L") + svgCoordsFromAxisCoords(gridLocations[xn * yGraticules.length + yn][1]).join(); + for (let yn=0, cmd="M"; yn < yGraticules.length; yn++, cmd="L") + pathStr += cmd + svgCoordsFromAxisCoords(gridLocations[xn * yGraticules.length + yn][1]).join(); // horizontal lines for (let yn=0; yn < yGraticules.length; yn++) - for (let xn=0; xn < xGraticules.length; xn++) - pathStr += (xn === 0 ? "M" : "L") + svgCoordsFromAxisCoords(gridLocations[xn * yGraticules.length + yn][1]).join(); + for (let xn=0, cmd="M"; xn < xGraticules.length; xn++, cmd="L") + pathStr += cmd + svgCoordsFromAxisCoords(gridLocations[xn * yGraticules.length + yn][1]).join(); // add the path to the SVG - const path = SVG("path", {d: pathStr, stroke: "#bbb", fill: "none"}); - GLOBAL.svgEl.append(path); + const path = SVG("path", {d: pathStr, stroke: "#ccc", fill: "none"}); + Q("#mappings-visual g").append(path); } // draw grid locations as vectors @@ -1049,10 +1054,10 @@ function mappingsChanged(mode) { // are the input and output equal in this projection? (need to allow for normalization rounding) if (!locationsAreEqual(location[0], location[1], visibleAxisIds)) { - const arrow = svgArrow({x1: svgX0, y1: svgY0, x2: svgX1, y2: svgY1, tipLen: 7, tipWid: 7, strokeWidth: 1, color: "grey"}); // draw an arrow - GLOBAL.svgEl.append(arrow); + const arrow = svgArrow({x1: svgX0, y1: svgY0, x2: svgX1, y2: svgY1, tipLen: 7, tipWid: 7, strokeWidth: 1, color: "#bbb"}); // draw an arrow + Q("#mappings-visual g").append(arrow); } - GLOBAL.svgEl.append(SVG("circle", {cx: svgX0, cy: svgY0, r: 2.5, fill: "grey"})); // draw a dot + Q("#mappings-visual g").append(SVG("circle", {cx: svgX0, cy: svgY0, r: 2.5, fill: "#bbb"})); // draw a dot }); } @@ -1074,14 +1079,14 @@ function mappingsChanged(mode) { elInstance1.style.opacity = 0.4; elInstance1.style.color = instanceColor; - GLOBAL.svgEl.append(elInstance1, elInstance0); + Q("#mappings-visual g").append(elInstance1, elInstance0); // are the input and output equal in this projection? (need to allow for normalization rounding) if (locationsAreEqual(location[0], location[1], visibleAxisIds)) { - GLOBAL.svgEl.append(elInstance0); + Q("#mappings-visual g").append(elInstance0); } else { - GLOBAL.svgEl.append(elInstance1, elInstance0, svgArrow({x1: svgX0, y1: svgY0, x2: svgX1, y2: svgY1, tipLen: 7, tipWid: 7, strokeWidth: 1, color: instanceColor})); // add an arrow + Q("#mappings-visual g").append(elInstance1, elInstance0, svgArrow({x1: svgX0, y1: svgY0, x2: svgX1, y2: svgY1, tipLen: 7, tipWid: 7, strokeWidth: 1, color: instanceColor})); // add an arrow } }); @@ -1115,9 +1120,8 @@ function mappingsChanged(mode) { const arrowSvg = svgArrow({index: m, x1: svgCoordsFrom[0], y1: svgCoordsFrom[1], x2: svgCoordsTo[0], y2: svgCoordsTo[1], tipLen: 11, tipWid: 11, strokeWidth: 2}); arrowSvg.classList.add("mapping"); - // add them all to the SVG element - GLOBAL.svgEl.append(arrowSvg, elInput, elOutput); - + // add them all to the SVG element + Q("#mappings-visual g").append(arrowSvg, elOutput, elInput); }); // display the current location (untransformed #0 and transformed #1) @@ -1148,28 +1152,45 @@ function mappingsChanged(mode) { formatNumericControls(-1); } - // draw the arrow + // draw the current arrow const arrowSvg = svgArrow({index: -1, x1: svgCoordsFrom[0], y1: svgCoordsFrom[1], x2: svgCoordsTo[0], y2: svgCoordsTo[1], tipLen: 7, tipWid: 7, strokeWidth: 1, color: "var(--currentLocationColor)"}); - - GLOBAL.svgEl.append(elCurrent1, elCurrent0, arrowSvg); // order is important, since we must be able to click on the [0] version if they overlap - + Q("#mappings-visual g").append(elCurrent1, elCurrent0, arrowSvg); // order is important, since we must be able to click on the [0] version if they overlap + + // draw the rulers + const rulerX = Q(".ruler.horizontal"), rulerY = Q(".ruler.vertical"); + const rulerGraticulesX = getGraticulesForAxis(visibleAxes[0], "ruler"); + const rulerGraticulesY = getGraticulesForAxis(visibleAxes[1], "ruler"); + + if (!rulerX.textContent) { + rulerGraticulesX.forEach(x => { + const label = EL("div", {style: `position: absolute; transform: rotate(-90deg); transform-origin: left; bottom: 0; left: ${svgCoordFromAxisCoord(visibleAxes[0].axisId, x)}px`}); + label.textContent = x; + rulerX.append(label); + }); + } + if (!rulerY.textContent) { + rulerGraticulesY.forEach(y => { + const label = EL("div", {style: `position: absolute; right: 0; bottom: ${svgCoordFromAxisCoord(visibleAxes[1].axisId, y)-10}px`}); + label.textContent = y; + rulerY.append(label); + }); + } } function svgMouseUp(e) { e.stopPropagation(); - const rect = GLOBAL.svgEl.getBoundingClientRect(); + const rect = Q(".svg-container").getBoundingClientRect(); + const x = e.clientX; const y = e.clientY; - GLOBAL.svgEl.removeEventListener("mousemove", svgMouseMove); // = undefined; - GLOBAL.svgEl.removeEventListener("mouseup", svgMouseUp); // = undefined; GLOBAL.dragging = undefined; GLOBAL.dragOffset = undefined; // disable what we put in place when we started dragging - document.mousemove = undefined; - document.mouseup = undefined; + document.mousemove = null; + document.mouseup = null; } function mappingMouseDown (e) { @@ -1184,7 +1205,8 @@ function mappingMouseDown (e) { // we hit a location e.stopPropagation(); - const rect = GLOBAL.svgEl.getBoundingClientRect(); + const rect = Q(".svg-container").getBoundingClientRect(); + GLOBAL.draggingIndex = parseInt(el.dataset.index); @@ -1214,13 +1236,13 @@ function mappingMouseDown (e) { } // return a sorted array of values that span the axis from min to max, and are base 10 friendly -function getGraticulesForAxis(axis) { +function getGraticulesForAxis(axis, graticuleSpec) { if (axis.maxValue - axis.minValue == 0) return [axis.maxValue]; const graticules = new Set([axis.minValue, axis.defaultValue, axis.maxValue]); // init the set of graticules - if (Q("#grid-style").value === "powers-of-10") { + if (graticuleSpec === "powers-of-10") { let inc = Math.pow(10, Math.floor(Math.log10((axis.maxValue - axis.minValue) * 0.3))); // get a value for inc, which is a power of 10 (10 as the inc from 33 to 330, then it goes to 100) for (let v = axis.minValue; v < axis.maxValue; v+=inc) { const gridVal = Math.floor(v / inc) * inc; @@ -1228,10 +1250,10 @@ function getGraticulesForAxis(axis) { graticules.add(Math.floor(v / inc) * inc); } } - else if (Q("#grid-style").value.match(/^(fill-space-|grid-)/)) { + else if (graticuleSpec.match(/^(fill-space-|grid-)/)) { let inc = 20; // measured in svg px units let match; - if (match = Q("#grid-style").value.match(/^(fill-space-|grid-)(\d+)/)) // e.g. fill-space-20, fill-space-40 + if (match = graticuleSpec.match(/^(fill-space-|grid-)(\d+)/)) // e.g. fill-space-20, fill-space-40 inc = parseInt(match[2]); for (let val = svgCoordFromAxisCoord(axis.axisId, axis.defaultValue) + inc; axisCoordFromSvgCoord(axis.axisId, val) < axis.maxValue; val += inc) { // get the max side of the axis graticules.add(axisCoordFromSvgCoord(axis.axisId, val)); @@ -1472,6 +1494,34 @@ function updateMappingsSliders(m) { } } +function updateSVGTransform() { + + // fix the transform + Q("#mappings-visual g").attr({ + transform: `scale(1 -1) translate(10 -${Q(".svg-container").getBoundingClientRect().height + 10})`, + }); + + // draw the rulers + const visibleAxisIds = getVisibleAxisIds(); + const visibleAxes = visibleAxisIds.map(a => GLOBAL.font.fvar.axes[a]); + const rulerX = Q(".ruler.horizontal"), rulerY = Q(".ruler.vertical"); + const rulerGraticulesX = getGraticulesForAxis(visibleAxes[0], "ruler"); + const rulerGraticulesY = getGraticulesForAxis(visibleAxes[1], "ruler"); + + rulerX.textContent = ""; + rulerGraticulesX.forEach(x => { + const label = EL("div", {style: `position: absolute; transform: rotate(-90deg); transform-origin: left; bottom: 0; left: ${svgCoordFromAxisCoord(visibleAxisIds[0], x)}px`}); + label.textContent = x; + rulerX.append(label); + }); + rulerY.textContent = ""; + rulerGraticulesY.forEach(y => { + const label = EL("div", {style: `position: absolute; right: 0; bottom: ${svgCoordFromAxisCoord(visibleAxisIds[1], y)-8}px`}); + label.textContent = y; + rulerY.append(label); + }); +} + function selectAxisControls(e) { @@ -1486,8 +1536,8 @@ function initFencer() { // init the svg GLOBAL.svgEl = SVG("svg"); + GLOBAL.svgEl.append(SVG("g")); // this element has all the content and has a transform GLOBAL.svgEl.id = "mappings-visual"; - GLOBAL.svgEl.setAttribute("transform", "scale(1 -1)"); Q("#mapping-selector").onchange = selectAxisControls; @@ -1521,7 +1571,6 @@ function initFencer() { // save XML Q("button#save-xml").onclick = e => { - //const uint8 = new Uint8Array(GLOBAL.fontBuffer.buffer); const fauxLink = EL("a"); fauxLink.download = "fencer.xml"; fauxLink.href = "data:application/xml;charset=UTF-8," + encodeURIComponent(Q(".mappings .xml").value); @@ -1537,6 +1586,8 @@ function initFencer() { .then(response => response.arrayBuffer()) .then(arrayBuffer => { loadFontFromArrayBuffer(arrayBuffer, {filename: filename}); + + updateSVGTransform(); }); // set grid-style selector to value stored in localStorage @@ -1548,7 +1599,7 @@ function initFencer() { function saveWindowProperties() { // save window states in local storage (position has changed for this window, classes may have changed for other windows) - // - TODO: add z-index to the stored properties when we implement it in UI + // - TODO: add z-index to the stored properties for all windows when we implement it in UI Qall(".window").forEach(el => { const name = el.querySelector(":scope > h2").textContent; const propString = JSON.stringify({left: el.style.left, top: el.style.top, width: el.style.width, height: el.style.height, classes: [...el.classList]}); @@ -1631,8 +1682,12 @@ function initFencer() { if (!resizeHandle.classList.contains("no-horizontal")) windowEl.style.width = initialWindowWidth + dx + 'px'; if (!resizeHandle.classList.contains("no-vertical")) - windowEl.style.height = initialWindowHeight + dy + 'px'; - mappingsChanged(); // update the SVG, yay! + windowEl.style.height = initialWindowHeight + dy + 'px'; + + if (windowEl.classList.contains("mappings-ui")) { + mappingsChanged(); // update the SVG, yay! + updateSVGTransform(); + } }; // ending resize