import { Flywheel } from "./effects.js"; import { getPathShapeBetweenPoints } from "./svg.js"; export class Game { constructor(svgEl, crosshairXEl, crosshairYEl) { this.flywheel = new Flywheel(); this.svgEl = svgEl; // canvas position relative to mouse/screen // TODO: update on resize: svgEl.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); this.rect = svgEl.getBoundingClientRect(); // move crosshairs window.addEventListener('mousemove', function (mousemoveEv) { let x = mousemoveEv.clientX - this.rect.left; let y = mousemoveEv.clientY - this.rect.top; crosshairXEl.setAttribute('x1', x) crosshairXEl.setAttribute('x2', x) crosshairYEl.setAttribute('y1', y) crosshairYEl.setAttribute('y2', y) }.bind(this)); this.loadAnnotation(); } loadAnnotation() { let json_request = new Request('/annotation.json?min_area=500'); fetch(json_request).then(function (response) { return response.json(); }).then((annotation) => { let svg_request = new Request('/annotation.svg?id='+annotation.id); fetch(svg_request).then(function (response) { return response.text() }).then((text) => { this.addAnnotation(annotation, text); }) }).catch((e) => console.error(e)); } addAnnotation(annotation, svgText) { //determine position const maxX = this.rect.width - annotation.bbox[2]; const maxY = this.rect.height - annotation.bbox[3]; const posX = Math.random() * maxX; const posY = Math.random() * maxY; let imgEl = document.createElementNS('http://www.w3.org/2000/svg', 'g'); imgEl.innerHTML = svgText; imgEl.classList.add('img'); imgEl.setAttribute('transform', `translate(${posX} ${posY})`); this.svgEl.appendChild(imgEl); let startTrace = function (mousedownEv) { // create shape to draw let shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); shapeEl.classList.add('rect'); let p1 = { 'x': mousedownEv.clientX - this.rect.left, 'y': mousedownEv.clientY - this.rect.top } let corners = this.flywheel.effects.shape.getAngles(); let d = getPathShapeBetweenPoints(p1, p1, corners); this.svgEl.appendChild(shapeEl); shapeEl.setAttribute('d', d); let mouseMoveEv = function (mousemoveE) { let p2 = { 'x': mousemoveE.clientX - this.rect.left, 'y': mousemoveE.clientY - this.rect.top } let d = getPathShapeBetweenPoints(p1, p2, corners); shapeEl.setAttribute('d', d); }.bind(this); let mouseUpEv = function (mouseupE) { let p2 = { 'x': mouseupE.clientX - this.rect.left, 'y': mouseupE.clientY - this.rect.top } let d = getPathShapeBetweenPoints(p1, p2, corners); shapeEl.setAttribute('d', d); console.log('up'); window.removeEventListener('mousemove', mouseMoveEv); // remove itself. window.removeEventListener('mouseup', mouseUpEv); // remove itself. this.svgEl.removeEventListener('mousedown', startTrace); // remove itself. shapeEl.classList.add('locked'); //TODO: calculate points const points = getPathShapeBetweenPoints(p1, p2, corners, true); this.calculateOverlap(annotation, posX, posY, points); this.flywheel.add(); this.loadAnnotation(); // fade out box setTimeout(function () { shapeEl.classList.add('hide'); // remove shape setTimeout(function () { shapeEl.parentNode.removeChild(shapeEl); }, 1000); }, 1000); // fade out img setTimeout(function () { imgEl.classList.add('hide'); // remove shape setTimeout(function () { imgEl.parentNode.removeChild(imgEl); }, 500); }, 500); }.bind(this); window.addEventListener('mousemove', mouseMoveEv); window.addEventListener('mouseup', mouseUpEv); }.bind(this); this.svgEl.addEventListener('mousedown', startTrace) } calculateOverlap(annotation, annotationDx, annotationDy, points){ const subj_paths = annotation.segments.map((seg) => { return seg.map((point) => { return {X: point[0] + annotationDx - annotation.bbox[0], Y: point[1] + annotationDy - annotation.bbox[1]} }) }); // apparently, we need a deep copy if we pass it to findDifferencePaths/Clipper const subj_paths2 = JSON.parse(JSON.stringify(subj_paths)); const subj_paths3 = JSON.parse(JSON.stringify(subj_paths)); // const subj_paths4 = JSON.parse(JSON.stringify(subj_paths)); const drawn_paths = [points.map((point) => { return {X: point[0], Y: point[1]} })]; const drawn_paths2 = [points.map((point) => { return {X: point[0], Y: point[1]} })]; // const drawn_paths3 = [points.map((point) => { // return {X: point[0], Y: point[1]} // })]; const optimal_bbox_paths = [[ {X: annotationDx, Y: annotationDy}, {X: annotationDx + annotation.bbox[2], Y: annotationDy}, {X: annotationDx + annotation.bbox[2], Y: annotationDy + annotation.bbox[3]}, {X: annotationDx, Y: annotationDy + annotation.bbox[3]}, ]]; // console.log(subj_paths, drawn_paths, optimal_bbox_paths); const scale = 100; let minimalErrorPaths = this.findDifferencePaths(subj_paths, optimal_bbox_paths, scale); let drawnErrorPaths = this.findDifferencePaths(drawn_paths, subj_paths2, scale, ClipperLib.ClipType.ctDifference); // only error outside of shape let drawnSuccessPaths = this.findDifferencePaths(subj_paths3, drawn_paths2, scale, ClipperLib.ClipType.ctIntersection); // let drawnMissPaths = this.findDifferencePaths(subj_paths4, drawn_paths3, scale, ClipperLib.ClipType.ctDifference); // let drawnSolutionPaths = this.findIntersectionPaths(subj_paths2, drawn_paths, scale); // const drawnArea = ClipperLib.JS.AreaOfPolygons(clip_paths) / (100*100)); const shapeArea = Math.abs(ClipperLib.JS.AreaOfPolygons(subj_paths) / (100*100)); // const shapeArea = ClipperLib.JS.AreaOfPolygons(subj_paths) / (100*100)); // const minimalErrorArea = ClipperLib.JS.AreaOfPolygons(minimalErrorPaths) / (100*100); const successArea = Math.abs(ClipperLib.JS.AreaOfPolygons(drawnSuccessPaths) / (100*100)); // const missArea = Math.abs(ClipperLib.JS.AreaOfPolygons(drawnMissPaths) / (100*100)); const minimalErrorArea = Math.abs(ClipperLib.JS.AreaOfPolygons(minimalErrorPaths) / (100*100)); const drawnErrorArea = Math.abs(ClipperLib.JS.AreaOfPolygons(drawnErrorPaths) / (100*100)); // const error = ((drawnErrorArea+1)/(minimalErrorArea+1)); // TODO different? We expected 20% of selection to be error (use minimalErrorArea/drawnArea), it was 25% of error (drawnErrorArea/drawnArea) // is dat drawnErrorArea/minimalErrorArea const simpleScore = (successArea / shapeArea);// TODO: Math.pow() for harder punishment of missed areas? const errorScore = (drawnErrorArea - minimalErrorArea) / shapeArea; // it can get negative, which is what we want for the odd shapes. const score = simpleScore - Math.min(1, errorScore) / 2; // cap error at 1, and make things easier for the player, so errors count less (/2) console.log(successArea, shapeArea, simpleScore, errorScore, score); // Scale down coordinates and draw ... if(window.location.search == "?debug") { let shapeEl2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); shapeEl2.classList.add('test'); this.svgEl.appendChild(shapeEl2); shapeEl2.setAttribute('d', paths2string(minimalErrorPaths, scale)); shapeEl2.setAttribute('stroke', 'green'); shapeEl2.setAttribute('stroke-width', '2'); shapeEl2.setAttribute('fill', 'red'); let shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); shapeEl.classList.add('test'); this.svgEl.appendChild(shapeEl); shapeEl.setAttribute('d', paths2string(drawnErrorPaths, scale)); shapeEl.setAttribute('stroke', 'yellow'); shapeEl.setAttribute('stroke-width', '2'); shapeEl.setAttribute('fill', 'orange'); let shapeEl3 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); shapeEl3.classList.add('test'); this.svgEl.appendChild(shapeEl3); shapeEl3.setAttribute('d', paths2string(drawnSuccessPaths, scale)); shapeEl3.setAttribute('stroke-width', '2'); shapeEl3.setAttribute('fill', 'green'); } // Converts Paths to SVG path string // and scales down the coordinates function paths2string (paths, scale) { var svgpath = "", i, j; if (!scale) scale = 1; for(i = 0; i < paths.length; i++) { for(j = 0; j < paths[i].length; j++){ if (!j) svgpath += "M"; else svgpath += "L"; svgpath += (paths[i][j].X / scale) + ", " + (paths[i][j].Y / scale); } svgpath += "Z"; } if (svgpath=="") svgpath = "M0,0"; return svgpath; } // var paths = [[{"X":10,"Y":10},{"X":110,"Y":10},{"X":110,"Y":110},{"X":10,"Y":110}]]; // console.log(JSON.stringify(paths)); // ClipperLib.Clipper.ReversePaths(paths); // console.log(JSON.stringify(paths)); } findDifferencePaths(subj_paths, clip_paths, scale, overrideClipType) { let cpr = new ClipperLib.Clipper(); ClipperLib.JS.ScaleUpPaths(subj_paths, scale); ClipperLib.JS.ScaleUpPaths(clip_paths, scale); cpr.AddPaths(subj_paths, ClipperLib.PolyType.ptSubject, true); // true means closed path cpr.AddPaths(clip_paths, ClipperLib.PolyType.ptClip, true); let solution_paths = new ClipperLib.Paths(); // const clipType = ClipperLib.ClipType.ctIntersection; // const clipType = ClipperLib.ClipType.ctDifference; // we like to know everything that is wrong. const clipType = overrideClipType ?? ClipperLib.ClipType.ctXor; // we like to know everything that is wrong. const subject_fillType = ClipperLib.PolyFillType.pftNonZero; const clip_fillType = ClipperLib.PolyFillType.pftNonZero; const succeeded = cpr.Execute(clipType, solution_paths, subject_fillType, clip_fillType); return solution_paths; } }