commit 9e4d936a96744590cf3a8df93ff57a6e64738a5e Author: Christian Lawson-Perfect Date: Sun Feb 9 20:33:33 2025 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70d7b4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +jme-runtime.js +locales.js +code-editor.mjs \ No newline at end of file diff --git a/animate.js b/animate.js new file mode 100644 index 0000000..faaa007 --- /dev/null +++ b/animate.js @@ -0,0 +1,11 @@ +const drawing = editor.drawing; +const duration = 10; +const fps = 5; +const frames = []; +for(let frame = 0; frame < duration*fps; frame++) { + const t = frame/fps; + drawing.ctx.get_time = () => t; + drawing.ctx.draw(); + frames.push(drawing.html); +} +console.log(frames.join('\n\n<>\n\n')) \ No newline at end of file diff --git a/drawing.txt b/drawing.txt new file mode 100644 index 0000000..8f9f5ed --- /dev/null +++ b/drawing.txt @@ -0,0 +1,35 @@ +let( +margin, 0.1, +eukleides("",-0.1-margin,-margin,sqrt(2)+0.1+margin,1+margin, +let( +ease, x -> if(x<0.5, 4x^3, 1 - ((2-2x)^3)/2), +frame, (from,to) -> ease(clamp((time-from)/(to-from), 0, 1)), +frame_lengths, [3,1,1,5], +frame_times, foldl((a,b) -> a+[a[-1]+b], [0], frame_lengths), +frame_number, (take(1, i -> frame_times[i]>=time, 0..len(frame_times)-1)+[len(frame_times)])[0]-1, +frames, frame(x,y) for: [x,y] of: zip(frame_times, frame_times[2..len(frame_times)]), +[a,b,c,d], rectangle(sqrt(2),1), +e, point(1,0), +f, point(1,1), +square_label, center(a..e..f..d) text("210×210") size(0.5), +strip_label, center(e..b..c..f) text("87×210") size(0.5), +[ + switch( + frame_number<3, + [a..b..c..d + , (e..f) opacity(frames[0]) + , [square_label, strip_label + ] * opacity(frames[1]) + ] + , [ [(a..e..f..d), square_label] - vector(frames[3]*0.1, 0) + , (x + vector(frames[3]*0.1, 0)) for: x of: [(e..b..c..f), strip_label] + ] + ) +//, point(0.7,0.7) text(jme_string(frame_times)) size(0.4) +//, point(0.7,0.5) text(jme_string(frames)) size(0.4) +//, point(0.5,0.3) text(jme_string(frame_number)) size(0.5) +//, point(0.5,0.1) text(jme_string(time)) size(0.5) +] +) +) +) \ No newline at end of file diff --git a/eukleides.mjs b/eukleides.mjs new file mode 100644 index 0000000..d9eb108 --- /dev/null +++ b/eukleides.mjs @@ -0,0 +1,2833 @@ +const {cos, sin, tan, acos, atan, atan2, PI, sqrt, abs, ceil, floor, max, min} = Math; + +const EPSILON = 1e-12; + +function dpformat(n,dp=2) { + const s = n.toFixed(dp); + return s.replace(/\.?0*$/,''); +} + +function ZERO(n) { + return abs(n) u.y*v.x ? 1 : -1) * acos( Vector.scalar_product(u,v)/(u.length()*v.length()) ); + } + + static scalar_product(u,v) { + return u.x*v.x + u.y*v.y; + } +} + +class Line extends Object { + constructor(x,y,a) { + super(); + this.x = x; + this.y = y; + this.a = a; + this.defined_by = {kind:'heading',through:new Point(x,y), heading: a}; + } + static create_with_points(A,B) { + if(EQL(A.x,B.x) && EQL(A.y,B.y)) { + throw(new Error("undefined line")); + } + const l = new Line(A.x,A.y,argument(A,B)); + l.defined_by = {kind: 'points', points: [A,B]}; + return l; + } + static create_with_vector(O,u) { + const l = new Line(O.x,O.y,atan2(u.y,u.x)); + l.defined_by = {kind: 'vector', through: O, vector: u}; + return l; + } + static create_with_segment(s) { + const [A,B] = s.points; + return Line.create_with_points(A,B); + } + parallel(O) { + return new Line(O.x,O.y,this.a); + } + static create_parallel_to_segment(seg,O) { + const [A,B] = seg; + if(EQL(A.x,B.x) && EQL(A.y,B.y)) { + throw(new Error("invalid argument")); + } + return new Line(O.x,O.y,argument(A,B)); + } + perpendicular(O) { + return new Line(O.x,O.y,this.a + (this.a<=PI/2 ? PI/2 : -PI*3/2)); + } + + static create_angle_bisector(A,B,C) { + if((EQL(A.x,B.x) && EQL(A.y,B.y)) || (EQL(B.x,C.x) && EQL(B.y,C.y))) { + throw(new Error("invalid angle")); + } + return new Line(B.x,B.y,(argument(B,A) + argument(B,C))/2); + } + + static create_lines_bisector(l1,l2) { + const c1 = cos(l1.a); + const s1 = sin(l1.a); + const c2 = cos(l2.a); + const s2 = sin(l2.a); + const d = det2(c1,c2,s1,s2); + if(ZERO(d)) { + if(ZERO(det2(l2.x-l1.x, l2.y-l1.y, c1,s1))) { + return l1; + } else { + throw(new Error("parallel lines")); + } + } else { + const b1 = det2(l1.x,l1.y,c1,s1); + const b2 = det2(l2.x,l2.y,c2,s2); + const x = det2(c1,c2,b1,b2)/d; + const y = det2(s1,s2,b1,b2)/d; + let a = (l1.a+l2.a)/2; + if(abs(l1.a-l2.a)>PI/2) { + a += (a<=PI/2 ? PI/2 : -PI*3/2); + } + return new Line(x,y,a); + } + } + + static create_altitude(A,B,C) { + const a = argument(B,C); + return new Line(A.x,A.y,a+(a<=PI/2 ? PI/2 : -PI*3/2)); + } + + static create_median(A,B,C) { + const x = (B.x+C.x)/2; + const y = (B.y+C.y)/2; + if(EQL(A.x,x) && EQL(A.y,y)) { + throw(new Error("invalid triangle")); + } + return new Line(A.x,A.y,atan2(y-A.y,x-A.x)); + } + + translate(u) { + return new Line(this.x+u.x, this.y+u.y, this.a); + } + + reflect(d) { + const c = cos(this.a); + const s = sin(this.a); + const x = this.x-d.x; + const y = this.y-d.y; + const p = 2*(c*x+s*y); + return new Line(d.x+p*c-x, d.y+p*s-y, principal(2*d.a-this.a)); + } + + symmetric(O) { + return new Line(2*O.x-this.x, 2*O.y-this.y, this.a+(this.a>0 ? -PI : PI)); + } + + rotate(O,a) { + const c = cos(a); + const s = sin(a); + const x = this.x - O.x; + const y = this.y - O.y; + return new Line(O.x+c*x-s*y, O.y+s*x+c*y, principal(this.a+a)); + } + + homothetic(O,k) { + if(k==0) { + throw(new Error("invalid ratio")); + } + return new Line(O.x+k*(this.x-O.x), O.y+k*(this.y-O.y), this.a+(k<0?(this.a>0?-PI:PI):0)); + } + + argument() { + return this.a; + } +} + +function point_line_distance(A,l) { + const c = cos(l.a); + const s = sin(l.a); + return abs(s*(A.x-l.x) - c*(A.y-l.y)); +} + +class Set extends Obj { + constructor(points) { + super(); + this.points = points.slice(); + } + + static create_polygon(n,O,r,a) { + const points = []; + for(let i=0;ip.translate(u)); + return new Set(points); + } + + reflect(l) { + const points = this.points.map(p=>p.reflect(l)); + return new Set(points); + } + + symmetric(O) { + const points = this.points.map(p=>p.symmetric(O)); + return new Set(points); + } + + rotate(O,a) { + const points = this.points.map(p=>p.rotate(O,a)); + return new Set(points); + } + + cardinal() { + return this.points.length; + } + + path_length() { + let t = 0; + for(let i=1;i2 ? 1 : 0);i++) { + const a = this.points[i%this.points.length]; + const b = this.points[i-1]; + t += a.distance(b); + } + return t; + } + + area() { + if(this.points.length==0) { + return 0; + } else { + return abs(compute_area(this.points,this.points))*0.5; + } + } + + perpendicular_to_segment(O) { + const [A,B] = this.points; + if(EQL(A.x,B.x) && EQL(A.y,B.y)) { + throw(new Error("invalid set")); + } + const a = argument(A,B); + return new Line(O.x,O.y,a+(a<=PI/2 ? PI/2 : -PI*3/2)); + } + + perpendicular_bisector() { + const [A,B] = this.points; + if(EQL(A.x,B.x) && EQL(A.y,B.y)) { + throw(new Error("invalid set")); + } + const a = argument(A,B); + return new Line((A.x+B.x)/2, (A.y+B.y)/2, a+(a<=PI/2 ? PI/2 : -PI*3/2)); + } + + isobarycenter() { + let x = 0; + let y = 0; + for(let p of this.points) { + x += p.x; + y += p.y; + } + const n = this.points.length; + return new Point(x/n, y/n); + } + + is_rectangle() { + if(this.points.length!=4) { + return false; + } + const [a,b,c,d] = this.points; + const [u,v] = [Vector.create_from_points(a,b), Vector.create_from_points(a,d)]; + const f = a.translate(u.add(v)); + const parallelogram = EQL(f.x,c.x) && EQL(f.y,c.y); + const dp = u.x*v.x + u.y*v.y; + return parallelogram && ZERO(dp); + } + + is_square() { + if(!this.is_rectangle()) { + return false; + } + const [a,b,c,d] = this.points; + const [u,v] = [Vector.create_from_points(a,b), Vector.create_from_points(a,d)]; + return EQL(u.length(),v.length()); + } + + is_equilateral_triangle() { + if(this.points.length!=3) { + return false; + } + const [a,b,c] = this.points; + const [u,v,w] = [Vector.create_from_points(a,b), Vector.create_from_points(a,c), Vector.create_from_points(c,b)]; + const [lu,lv,lw] = [u.length(), v.length(), w.length()]; + return EQL(lu,lv) && EQL(lv,lw); + } + + compute_shape_name() { + if(this.is_rectangle()) { + if(this.is_square()) { + return 'square'; + } else { + return 'rectangle'; + } + } else if(this.is_equilateral_triangle()) { + return 'equilateral triangle'; + } else { + const names = ['', '', 'segment', 'triangle', 'quadrilateral', 'pentagon', 'hexagon']; + if(this.points.length 0 ? 1 : -1; + const s1 = sqrt((a-b+c)*(a+b-c)); + const s2 = sqrt((b+c-a)*(b+c+a)); + if(ZERO(s1) || ZERO(s2)) { + throw(new Error("invalid points")); + } + const r = 0.5*s1*s2/(a+b+c); + const t = s1/s2; + const u = (B.x-A.x)/c; + const v = (B.y-A.y)/c; + const center = new Point(A.x+r*(u/t-s*v), A.y=r*(v/t+s*u)); + return new Circle(center,r); + } + + translate(u) { + const center = (new Point(this.x,this.y)).translate(u); + return new Circle(center, this.r); + } + + center() { + return new Point(this.x,this.y); + } + + tangent(a) { + return new Line(this.x+this.r*cos(a), this.y+this.r*sin(a), a+(a<=PI/2 ? PI/2 : -PI*3/2)); + } +} + +class Conic extends Obj { + constructor(v,a,b,d) { + super(); + this.d = d; + this.b = b; + this.a = a; + this.x = v.x; + this.y = v.y; + } + + static create_with_directrix(A,l,e) { + const c = cos(l.a); + const s = sin(l.a); + const d = s*(A.x-l.x) - c*(A.y-l.y); + if(ZERO(d)) { + throw(new Error("d is 0")); + } + const dd = principal(l.a + (d<0 ? PI/2 : -PI/2)); + if(e==1) { + return new Parabola(A, abs(d), dd); + } else { + const h = 1/e - e; + const f = abs(d)*e/h; + const {x,y} = {x: A.x+s*f, y: A.y-c*f}; + const v = new Point(x,y); + const a = abs(d/h); + if(e<1) { + const b = a*sqrt(1-e*e); + return new Ellipse(v,a,b,dd); + } else { + const b = a*sqrt(e*e-1); + return new Hyperbola(v,a,b,dd); + } + } + } + + static create_with_foci(A,B,a) { + if(a<=0) { + throw(new Error("invalid major or real axis")); + } + const f = A.distance(B); + if(ZERO(f) || EQL(f,a)) { + throw(new Error("invalid parameters")); + } + const x = (A.x+B.x)/2; + const y = (A.y+B.y)/2; + const v = new Point(x,y); + const d = argument(A,B); + if(f 0 ? -PI : PI) + ); + } + + rotate(O,a) { + const c = cos(a); + const s = sin(a); + const x = this.x - O.x; + const y = this.y - O.y; + return new (this.__proto__.constructor)( + new Point(O.x + c*x - s*y, O.y + s*x + c*y), + this.a, + this.b, + principal(this.d+a) + ); + } + + homothetic(O,k) { + return new (this.__proto__.constructor)( + new Point(O.x + k*(this.x - O.x), O.y + k*(this.y - O.y)), + abs(k)*this.a, + abs(k)*this.b, + this.d + ); + } + + major_axis() { + return this.a; + } + + minor_axis() { + return this.b; + } + + argument() { + return this.d; + } +} + +class Ellipse extends Conic { + constructor(v,a,b,d) { + if(a<=0 || b<=0 || a= PI || t==0) { + throw(new Error("invalid argument")); + } + const [x,y] = parametric_hyperbola(t,this.x,this.y,this.a,this.b,c,s); + return new Point(x,y); + } + + eccentricity() { + return sqrt(1+this.b*this.b/(this.a*this.a)); + } + + point_argument(A) { + const c = cos(this.d); + const s = sin(this.d); + const x = A.x-this.x; + const y = A.y-this.y; + const u = c*x + s*y; + const v = -s*x + c*y; + + return atan2(this.b,v) - (u<0 ? PI : 0); + } + + tangent(t) { + const c = cos(this.d); + const s = sin(this.d); + if(t<=-PI || t>=PI) { + throw(new Error("invalid argument")); + } + let x,y,a; + if(t==0 || t==PI) { + x = this.x; + y = this.y; + a = atan2(this.b,this.a*(t==0?1:-1)); + } else { + const [x,y] = parametric_hyperbola(t,this.x,this.y,this.a,this.b,c,s); + a = atan2(-this.b, -this.a*cos(t)); + } + return new Line(x,y,principal(a+this.d)); + } +} + +class Parabola extends Conic { + point_on(t) { + const c = cos(this.d); + const s = sin(this.d); + if(t<=-PI || t>= PI || t==0) { + throw(new Error("invalid argument")); + } + const [x,y] = parametric_parabola(t,this.x,this.y,this.a,this.b,c,s); + return new Point(x,y); + } + + constructor(v,a,d) { + if(a<0) { + throw(new Error("Invalid a")); + } + const [nx,ny] = [ + v.x+a*Math.cos(d)/2, + v.y+a*Math.sin(d)/2 + ]; + super(new Point(nx,ny),a,0,d); + } + + center() { + throw(new Error("undefined center")); + } + + eccentricity() { + return 1; + } + + point_argument(A) { + const c = cos(this.d); + const s = sin(this.d); + const x = A.x-this.x; + const y = A.y-this.y; + const u = c*x + s*y; + const v = -s*x + c*y; + + return atan2(-v, (this.a-v*v/this.a)/2); + } + + tangent(t) { + if(t<=-PI || t>=PI) { + throw(new Error("invalid argument")); + } + const c = cos(this.d); + const s = sin(this.d); + const [x,y] = parametric_parabola(t,this.x,this.y,this.a,c,s); + const a = principal(atan2(-1-cos(t),sin(t))+this.d); + return new Line(x,y,a); + } +} + +class TriangleMaker { + constructor(vertices) { + this.vertices = vertices.slice(); + } + + assign_A_B() { + let A,B; + switch(this.vertices.length) { + case 0: + A = new Point(0,0); + B = new Point(this.a*this.x,this.a*this.y); + this.vertices = [A,B]; + break; + case 1: + [A] = this.vertices; + B = new Point(A.x + this.a*this.x, A.y + this.a*this.y); + this.vertices = [A,B]; + break; + case 2: + [A,B] = this.vertices; + this.a = A.distance(B); + if(ZERO(this.a)) { + throw(new Error("invalid points")); + } + [this.x,this.y] = [(B.x-A.x)/this.a, (B.y-A.y)/this.a]; + break; + } + + } + + assign_C(u,v) { + const [A,B] = this.vertices; + const C = new Point( + A.x+u*this.x-v*this.y, + A.y+v*this.x+u*this.y + ); + this.vertices = [A,B,C]; + } + + static define_optimal_scalene(vertices,a,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + tm.assign_A_B(); + tm.assign_C(tm.a*.375, tm.a*.61237244); + return tm.vertices; + } + + static define_triangle_SSS(vertices,a,b,c,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + tm.assign_A_B(); + a = tm.a; + const s1 = (b-a+c)*(b+a-c); + const s2 = (a+c-b)*(a+b+c); + let u,v; + if(ZERO(s2)) { + u = -c; + v = 0; + } else { + const s = s1/s2; + if(s<0) { + throw(new Error("invalid lengths")); + } + t = 2*atan(sqrt(s)); + u = c*cos(t); + v = c*sin(t); + } + tm.assign_C(u,v); + return tm.vertices; + } + + static define_triangle_SAA(vertices,a,u,v,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + const cu = cos(u); + const su = sin(u); + const cv = cos(v); + const sv = sin(v); + const d = cu*sv + su*cv; + if(ZERO(d)) { + throw(new Error("invalid angles")); + } + tm.assign_A_B(); + const c = a*sv/d; + tm.assign_C(c*cu,c*su); + return tm.vertices; + } + + static define_triangle_SAS(vertices,a,u,c,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + tm.assign_A_B(); + tm.assign_C(c*cos(u),c*sin(u)); + return tm.vertices; + } + + static define_triangle_SSA(vertices,a,c,v,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + tm.assign_A_B(); + const cv = cos(v); + const sv = sin(v); + const s = c*c-a*a*sv*sv; + if(s<0) { + throw(new Error("invalid parameters")); + } + const b = a*cv + sqrt(s); + tm.assign_C(a-b*cv, b*sv); + return tm.vertices; + } + + static define_right_SS(vertices,a,b,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + tm.assign_A_B(); + a = tm.a; + tm.assign_C(a, b); + return tm.vertices; + } + + static define_right_SA(vertices,a,u,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + if(u>=PI/2 || u<=-PI/2) { + throw(new Error("invalid angle")); + } + tm.assign_A_B(); + a = tm.a; + tm.assign_C(a, a*tan(u)); + return tm.vertices; + } + + static define_isosceles_SS(vertices,a,b,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + tm.assign_A_B(); + a = tm.a/2; + const s = b*b-a*a; + if(s<0) { + throw(new Error("invalid lengths")); + } + tm.assign_C(a,sqrt(s)); + return tm.vertices; + } + + static define_isosceles_SA(vertices,a,u,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + if(u>=PI/2 || u<=-PI/2) { + throw(new Error("invalid angle")); + } + tm.assign_A_B(); + a = tm.a/2; + tm.assign_C(a,a*tan(u)); + return tm.vertices; + } + + static define_equilateral(vertices,a,t) { + const tm = new TriangleMaker(vertices); + if(vertices.length<2) { + tm.a = a; + tm.x = cos(t); + tm.y = sin(t); + } + tm.assign_A_B(); + a = tm.a/2; + tm.assign_C(a,a*sqrt(3)); + return tm.vertices; + } +} + +class QuadrilateralMaker { + constructor(vertices) { + this.vertices = vertices.slice(); + } + + /** Assign the positions of the vertices of a quadrilateral ABCD + * @param {Number} x - x-component of vector in direction of first side (A-B) + * @param {Number} y - y-component of vector in direction of first side (A-B) + * @param {Number} u - x-component of vector in direction of second side (B-C) + * @param {Number} v - y-component of vector in direction of second side (B-C) + * @param {Number} l - length of first side (A-B) + * @param {Number} m - length of second side (B-C) + * @param {Boolean} square - is the quadrilateral a square? (i.e., length of second side equals length of first side) + * @returns {Array.} - the vertices A,B,C,D + */ + assign(x,y,u,v,l,m,square) { + let [A,B,C] = this.vertices; + if(this.vertices.length<3) { + switch(this.vertices.length) { + case 0: + A = new Point(0,0); + B = new Point(l*x,l*y); + break; + case 1: + B = new Point(A.x+l*x, A.y+l*y); + break; + case 2: + l = A.distance(B); + if(ZERO(l)) { + throw(new Error("invalid points")); + } + x = (B.x-A.x)/l; + y = (B.y-A.y)/l; + break; + } + if(square) { + m = l; + } + C = new Point(B.x+m*(u*x-v*y), B.y+m*(v*x+u*y)); + } + const D = new Point( + A.x+C.x-B.x, + A.y+C.y-B.y + ); + this.vertices = [A,B,C,D]; + return this.vertices; + } + + static define_parallelogram_SSA(vertices,m,a,l,b) { + let x,y,u,v; + if(vertices.length < 2) { + x = cos(b); + y = sin(b); + u = cos(a); + v = sin(a); + } else { + u = cos(a); + v = sin(a); + } + const qm = new QuadrilateralMaker(vertices); + return qm.assign(x,y,u,v,l,m); + } + + static define_parallelogram_VV(vertices,u,v) { + let [A] = vertices; + if(vertices.length==0) { + A = new Point(0,0); + } + const B = new Point(A.x+u.x, A.y+u.y); + const C = new Point(A.x+u.x+v.x, A.y+u.y+v.y); + const D = new Point(A.x+v.x,A.y+v.y); + return [A,B,C,D]; + } + + static define_rectangle(vertices,l,m,b) { + let x,y; + if(vertices.length<2) { + x = cos(b); + y = sin(b); + } + const qm = new QuadrilateralMaker(vertices); + return qm.assign(x,y,0,1,l,m); + } + + static define_square(vertices,l,b) { + let x,y; + if(vertices.length<2) { + x = cos(b); + y = sin(b); + } + const qm = new QuadrilateralMaker(vertices); + return qm.assign(x,y,0,1,l,l,true); + } +} + +function orthogonal_projection(A,l) { + const c = cos(l.a); + const s = sin(l.a); + const p = c*(A.x - l.x) + s*(A.y-l.y); + + return new Point(l.x + p*c, l.y + p*s); +} + +function intersection_point(x1,y1,a1,x2,y2,a2) { + const c1 = cos(a1); + const s1 = sin(a1); + const c2 = cos(a2); + const s2 = sin(a2); + const d = det2(c1,c2,s1,s2); + if(ZERO(d)) { + throw(new Error("parallel lines")); + } + const b1 = det2(x1,y1,c1,s1); + const b2 = det2(x2,y2,c2,s2); + return new Point( + det2(c1,c2,b1,b2)/d, + det2(s1,s2,b1,b2)/d + ); +} + +function parallel_projection(A,l1,l2) { + return intersection_point(l1.x,l1.y,l1.a,A.x,A.y,l2.a); +} + +function lines_intersection(l1,l2) { + return intersection_point(l1.x,l1.y,l1.a,l2.x,l2.y,l2.a); +} + +function line_segment_intersection(set,a,b,c,d) { + const [p,t] = set; + if(t===undefined) { + return []; + } + function dist(p) { + return a*p.x+b*p.y+c; + } + + const e = dist(t,a,b,c); + if(d*e<=EPSILON && abs(e)>EPSILON) { + const f = abs(d)+abs(e); + const g = f==0 ? 0 : abs(d)/f; + const p1 = new Point(p.x+g*(t.x-p.x), p.y+g*(t.y-p.y)); + return [p1].concat(line_segment_intersection(set.slice(1),a,b,c,e)); + } + return line_segment_intersection(set.slice(1),a,b,c,e); +} + +function line_set_intersection(l,s) { + if(s.points.length==0) { + return new Set([]); + } + const a = sin(l.a); + const b = -cos(l.a); + const c = -a*l.x-b*l.y; + function dist(p) { + return a*p.x+b*p.y+c; + } + + return new Set(line_segment_intersection(s.points,a,b,c,dist(s.points[0],a,b,c))); +} + +/** Solve a quadratic equation a*x^2 + b*x + c = 0 + */ +function solve(a,b,c) { + if(ZERO(a)) { + if(ZERO(b)) { + return []; + } + return [-c/b,1]; + } + const d = b*b-4*a*c; + if(d<0) { + return []; + } + if(ZERO(d)) { + return [-b/(2*a)]; + } + const r = sqrt(d); + const x1 = (-b-r)/(2*a); + const x2 = (-b+r)/(2*a); + return [x1,x2]; +} + +function line_circle_intersection(l,C) { + const x = l.x-C.x; + const y = l.y-C.y; + const c = cos(l.a); + const s = sin(l.a); + const roots = solve(1,2*(x*c+y*s),x*x+y*y-C.r*C.r); + return new Set(roots.map(t=>new Point(l.x+c*t, l.y+s*t))); +} + +function line_conic_intersection(l,C) { + const c = cos(C.d); + const s = sin(C.d); + const a = C.a*C.a; + const b = C.b*C.b; + const x = c*(l.x-C.x)+s*(l.y-C.y); + const y = -s*(l.x-C.x)+c*(l.y-C.y); + const ca = cos(l.a); + const sa = sin(l.a); + const u = c*ca+s*sa; + const v = -s*ca+c*sa; + let roots; + if(C instanceof Ellipse) { + roots = solve(u*u/a + v*v/b, 2*(x*u/a + y*v/b), x*x/a + y*y/b - 1); + } else if(C instanceof Hyperbola) { + roots = solve(u*u/a - v*v/b, 2*(x*u/a - y*v/b), x*x/a - y*y/b - 1); + } else if(C instanceof Parabola) { + roots = solve(v*v/(2*C.a), y*v/C.a - u, y*y/(2*C.a) - x - C.a/2); + } + + return new Set(roots.map(t=>new Point(l.x+ca*t, l.y+sa*t))); +} + +function sets_intersection(s1,s2) { + if(s1.points.length==0 || s2.points.length==0) { + return new Set([]); + } + const out = []; + let s = s1.points[s1.points.length-1]; + for(let t of s1.points) { + let v = s2.points[s2.points.length-1]; + for(let w of s2.points) { + if(max(s.x,t.x) >= min(v.x,w.x) + && max(v.x,w.x) >= min(s.x,t.x) + && max(s.y,t.y) >= min(v.y,w.y) + && max(v.y,w.y) >= min(s.y,t.y) + ) { + const d1 = s.distance(t); + const c1 = (t.x-s.x)/d1; + const s1 = (t.y-s.y)/d1; + const d2 = v.distance(w); + const c2 = (w.x-v.x)/d2; + const s2 = (w.y-v.y)/d2; + const x = v.x-s.x; + const y = v.y-s.y; + const d = det2(c1,c2,s1,s2); + if(ZERO(d)) { + if(ZERO(det2(x,y,c1,s1))) { + out.push(s); + } + } else { + const t1 = det2(x,y,c2,s2)/d; + const t2 = det2(x,y,c1,s1)/d; + if(t1>=0 && t1<=d1 && t2>+0 && t2<=d2) { + out.push(new Point(s.x+c1*t1,s.y+s1*t1)); + } + } + } + v = w; + } + s = t; + } + return new Set(out); +} + +function circles_intersection(c1,c2) { + let x = c2.x-c1.x; + let y = c2.y-c1.y; + const a = hypot(x,y); + const b = c2.r; + const c = c1.r; + if(ZERO(a) || a>b+c || a=c.r || e>=c.r) { + const f = s.distance(t); + const x = s.x-c.x; + const y = s.y-c.y; + const u = (t.x-s.x)/f; + const v = (t.y-s.y)/f; + const roots = solve(1,2*(x*u+y*v), x*x+y*y-c.r*c.r); + if(roots.length>0) { + const [t1,t2] = roots; + if(roots.length>1 && t2>=0 && t2<=f) { + const v2 = new Point( + s.x+u*t2, + s.y+v*t2 + ); + out.push(v2); + } + if(t1>=0 && t1<=f) { + const v1 = new Point( + s.x+u*t1, + s.y+v*t1 + ); + out.push(v1); + } + } + } + s = t; + } + return new Set(out); +} + +function clean_label(text) { + text = text+''; + text = text.replace("'","′"); + var superscripts = '⁰¹²³⁴⁵⁶⁷⁸⁹'; + var subscripts = '₀₁₂₃₄₅₆₇₈₉'; + text = text.replace(/\^(\d)/g,function(m){return superscripts[parseInt(m[1])]}); + text = text.replace(/_(\d)/g,function(m){return subscripts[parseInt(m[1])]}); + + return text; +} + + +class Drawer { + constructor() { + } + + initialise() { + this.restore_default_settings(); + this.restore_local_settings(); + this.setup_frame(-2,-2,8,6,1); + this.settings_stack = []; + } + + restore_default_settings() { + this.global = { + label: false, + label_segment: NONE, + aria_mode: NOSPOILERS, + color: BLACK, + size: 1, + step: 3, + style: FULL, + shape: DOT, + part: ENTIRE, + dir: FORTH, + arrow: NONE, + font_desc: '', + segment: SIMPLE, + angle: SIMPLE, + dec: NONE, + opacity: 1, + font_size: 0.2, + bold: false, + italic: false, + font_family: 'sans-serif', + close: true, + label_dist: 0.2 + } + } + + restore_local_settings() { + this.local = Object.assign({},this.global); + } + + push_local_settings() { + this.settings_stack.push(this.local); + this.local = {...this.local}; + } + pop_local_settings() { + this.local = this.settings_stack.pop(); + } + + setup_frame(min_x,min_y,max_x,max_y,scale) { + this.min_x = min_x; + this.min_y = min_y; + this.max_x = max_x; + this.max_y = max_y; + this.scale = scale || 1; + this.font_scale = 100; + this.default_dist = 0.2; + } + + SIZE(x) { + return this.local.size*x/this.scale; + } + + check_basic_settings() { + this.check_color(); + this.check_size(); + this.check_style(); + } + + set_xy(A,B,C,d) { + const l1 = B.distance(A); + if(ZERO(l1)) { + throw(new Error("invalid angle")); + } + const x1 = d*(A.x-B.x)/l1; + const y1 = d*(A.y-B.y)/l1; + const l2 = B.distance(C); + if(ZERO(l2)) { + throw(new Error("invalid angle")); + } + const x2 = d*(C.x-B.x)/l2; + const y2 = d*(C.y-B.y)/l2; + return [x1,y1,x2,y2]; + } + + // distance to the furthest corner of the frame + get_max(x,y) { + const {min_x,min_y,max_x,max_y} = this; + let m = hypot(min_x-x,min_y-y); + let l = hypot(max_x-x,min_y-y); + if(l>m) { + m = l; + } + l = hypot(max_x-x,max_y-y); + if(l>m) { + m = l; + } + l = hypot(min_x-x,max_y-y); + if(l>m) { + m = l; + } + return m; + } + + draw_hyperbolic_arc(x0,y0,a,b,f,g,c,s) { + const e = atan(b/this.get_max(x0,y0)); + if(f<-e) { + this.draw_branch(-PI+e,-e,x0,y0,a,b,f,g,c,s); + } + if(g>e) { + this.draw_branch(e,PI-e,x0,y0,a,b,f,g,c,s); + } + } + + draw_conic(C) { + this.check_basic_settings(); + if(C instanceof Parabola) { + this.draw_parabolic_arc(C.x,C.y,C.a,-PI,PI,cos(C.d),sin(C.d)); + } else if(C instanceof Ellipse) { + this.draw_elliptic_arc(C.x,C.y,C.a,C.b,-PI,PI,cos(C.d),sin(C.d)); + } else if(C instanceof Hyperbola) { + this.draw_hyperbolic_arc(C.x, C.y, C.a, C.b, -PI, PI, cos(C.d), sin(C.d)); + } + } + + draw_conic_arc(C,f,g) { + this.check_basic_settings(); + if(C instanceof Ellipse && f>g) { + g += 360; + } + if(f>=g) { + throw(new Error("invalid boundaries")); + } + if(C instanceof Parabola) { + this.draw_parabolic_arc(C.x,C.y,C.a,f,g,cos(C.d),sin(C.d)); + } else if(C instanceof Ellipse) { + this.draw_elliptic_arc(C.x,C.y,C.a,C.b,f,g,cos(C.d),sin(C.d)); + } else if(C instanceof Hyperbola) { + this.draw_hyperbolic_arc(C.x,C.y,C.a,C.b,f,g,cos(C.d),sin(C.d)); + } + } +} + +const labels = ["simple","double","triple"]; +const [SIMPLE,DOUBLE,TRIPLE] = labels; +const styles = ["full","dotted","dashed"]; +const [FULL,DOTTED,DASHED] = styles; +const shapes = ["dot","disc","box","plus","cross"]; +const [DOT,DISC,BOX,PLUS,CROSS] = shapes; +const parts = ["entire","half"]; +const [ENTIRE,HALF] = parts; +const dirs = ["right","forth","back"]; +const [RIGHT,FORTH,BACK] = dirs; +const arrows = ["none","arrow","arrows"]; +const [NONE,ARROW,ARROWS] = arrows; +const colors = ['black','darkgray','gray','lightgray','white','red','green','blue','cyan','magenta','yellow']; +const [BLACK,DARKGRAY,GRAY,LIGHTGRAY,WHITE,RED,GREEN,BLUE,CYAN,MAGENTA,YELLOW] = colors; +const aria_modes = ['verbose','nospoilers']; +const [VERBOSE,NOSPOILERS] = aria_modes; + +function dp(n) { + return parseFloat(n).toFixed(7); +} + +class SVGDrawer extends Drawer { + constructor(svg,doc) { + super(); + this.svg = svg; + this.doc = doc || document; + this.shapes = {}; + this.elements = {}; + this.initialise(); + this.before_render(); + } + + initialise() { + super.initialise(); + let defs = this.svg.querySelector('defs'); + if(!defs) { + defs = this.doc.createElementNS('http://www.w3.org/2000/svg','defs'); + this.svg.appendChild(defs); + } + const def_items = { + 'eukleides-pattern-stripes': ` + + + + `, + 'eukleides-mask-stripes': ` + + + + `, + 'eukleides-pattern-dots': ` + + + + + `, + 'eukleides-mask-dots': ` + + + + ` + } + for(let [id,def] of Object.entries(def_items)) { + if(!defs.querySelector('#'+id)) { + defs.innerHTML += def; + } + } + } + + before_render() { + this.used_ids = {}; + this.idacc = 0; + this.point_labels = []; + } + after_render() { + Object.entries(this.elements).forEach(([id,element]) => { + if(!this.used_ids[id]) { + delete this.elements[id]; + for(let el of this.svg.querySelectorAll(`[data-eukleides-id="${id}"]`)) { + el.parentElement.removeChild(el); + } + } + }); + } + + add_point_label(p) { + this.point_labels.push({point: p, label: clean_label(this.local.label_text)}); + } + + has_label_for_point(p) { + if(this.aria_mode==VERBOSE) { + return true; + } + const d = this.point_labels.find(d=>d.point.x==p.x && d.point.y==p.y); + return d; + } + + label_for_point(p) { + const d = this.point_labels.find(d=>d.point.x==p.x && d.point.y==p.y); + return d ? d.label : `(${dpformat(p.x)},${dpformat(p.y)})`; + } + + setup_frame(min_x,min_y,max_x,max_y,scale) { + super.setup_frame(min_x,min_y,max_x,max_y,scale); + + this.svg.setAttribute('viewBox',`${dp(min_x)} ${dp(min_y)} ${dp(max_x-min_x)} ${dp(max_y-min_y)}`); + + this.container = this.doc.createElementNS('http://www.w3.org/2000/svg','g'); + this.container.setAttribute('transform', `translate(0 1) scale(${dp(scale)} ${dp(-scale)})`); + this.svg.appendChild(this.container); + } + + check_color() { } + check_font() { } + check_size() { } + check_style() { } + check_angle_style() { } + + + set_fill(e,apply_pattern=true) { + if(apply_pattern) { + switch(this.local.style) { + case DOTTED: + e.setAttribute('mask','url(#eukleides-mask-dots)'); + break; + case DASHED: + e.setAttribute('mask','url(#eukleides-mask-stripes)'); + break; + } + } + e.setAttribute('fill',this.local.color); + e.style.opacity = this.local.opacity; + } + set_stroke(e) { + e.setAttribute('fill','none'); + e.setAttribute('stroke',this.local.color); + e.setAttribute('stroke-width',this.local.size*0.02); + e.setAttribute('stroke-linejoin','round'); + e.setAttribute('stroke-linecap','round'); + e.style.opacity = this.local.opacity; + } + set_style(e) { + let s = ''; + switch(this.local.style) { + case FULL: + s = '1 0'; + break; + case DOTTED: + const lineWidth = e.getAttribute('stroke-width') || this.local.size*0.02; + s = `0 0.2`; + break; + case DASHED: + s = `${this.SIZE(0.15)} ${this.SIZE(0.1)}`; + break; + } + e.setAttribute('stroke-dasharray',s); + } + set_font(e) { + const size = this.font_scale*this.SIZE(this.local.font_size)/100; + e.style['font'] = `${this.local.italic ? 'italic ': ''}${this.local.bold ? 'bold ' : ''}${dp(size)}pt ${this.local.font_family}`; + } + + element(name,attr,content) { + const id = this.local.key || this.idacc++; + this.used_ids[id] = true; + const olde = this.elements[id]; + let e; + if(olde && olde.tagName==name) { + e = olde; + e.removeAttribute('transform'); + } else { + for(let e of this.svg.querySelectorAll(`[data-eukleides-id="${id}"]`)) { + e.parentElement.removeChild(e); + } + e = this.doc.createElementNS('http://www.w3.org/2000/svg',name); + e.setAttribute('data-eukleides-id',id); + } + if(name!='text') { + e.setAttribute('role','presentation'); + } + this.elements[id] = e; + if(attr) { + for(let [k,v] of Object.entries(attr)) { + e.setAttribute(k,v); + } + } + if(content!==undefined) { + e.innerHTML = content; + } + this.container.appendChild(e); + return e; + } + + transform(element,def) { + var odef = element.getAttribute('transform') || ''; + element.setAttribute('transform',[odef,def].join(' ')); + return element; + } + + describe_style(desc) { + switch(this.local.style) { + case DOTTED: + desc = `dotted ${desc}`; + break; + case DASHED: + desc = `dashed ${desc}`; + break; + } + return desc; + } + + describe_arrows(desc) { + if(this.local.arrow==NONE) { + return desc; + } else if(this.local.arrow==ARROWS) { + desc += ' with an arrow at each end'; + } else { + desc += this.local.dir==BACK ? ' with an arrow at the start' : ' with an arrow at the end'; + } + return desc; + } + + set_aria_label(element,label) { + if(this.local.color_description) { + label = `${this.local.color_description} ${label}`; + } else if(colors.contains(this.local.color) && this.local.color != this.global.color) { + label = `${this.local.color} ${label}`; + } + if(this.local.opacity<1) { + label = `transparent ${label}`; + } + if(this.local.description!==undefined) { + label = this.local.description; + } + element.setAttribute('aria-label',label); + if(element.getAttribute('role')=='presentation') { + element.setAttribute('role','img'); + } + } + + handle_dragging(element,callback) { + var onstart = (e) => { + const ondrag = callback(); + e.stopPropagation(); + e.preventDefault(); + e = e.touches ? e.touches[0] : e; + const [sx,sy] = [e.clientX, e.clientY]; + var onmove = (e) => { + e.preventDefault(); + e = e.touches ? e.touches[0] : e; + const m = element.getScreenCTM().inverse(); + const p = this.svg.createSVGPoint(); + p.x = e.clientX; + p.y = e.clientY; + const tp1 = p.matrixTransform(m); + p.x = sx; + p.y = sy; + const tp0 = p.matrixTransform(m); + const [dx,dy] = [tp1.x-tp0.x,tp1.y-tp0.y]; + ondrag(dx,dy); + } + var onend = (e) => { + e.preventDefault(); + document.removeEventListener('mousemove',onmove); + document.removeEventListener('mouseup',onend); + document.removeEventListener('touchmove',onmove); + document.removeEventListener('touchend',onend); + document.removeEventListener('touchcancel',onend); + } + document.addEventListener('mousemove',onmove); + document.addEventListener('touchmove',onmove); + document.addEventListener('mouseup',onend); + document.addEventListener('touchend',onend); + document.addEventListener('touchcancel',onend); + } + element.addEventListener('mousedown',onstart); + element.addEventListener('touchstart',onstart); + } + + show(element) { + this.svg.appendChild(element); + return element; + } + + draw_dot(x,y,r) { + const c = this.element('circle',{cx: x, cy: y, r: r}); + this.set_fill(c,false); + return c; + } + + arc(x,y,r,a,b,fill) { + let d = b-a; + let sweep = 0; + if(d>0) { + d -= 2*PI; + } + let large_circle = Math.abs(d)>=PI ? 1 : 0; + if(d>=2*PI) { + return this.element('circle',{cx:x,cy:y,r:r}); + } else { + let d = `M ${dp(x+Math.cos(a)*r)} ${dp(y+Math.sin(a)*r)} A ${dp(r)} ${dp(r)} 0 ${large_circle} ${sweep} ${dp(x+Math.cos(b)*r)} ${dp(y+Math.sin(b)*r)}`; + if(fill) { + d += `L ${dp(x)} ${dp(y)}`; + } + return this.element('path',{d}); + } + } + + draw_dash(x,y,angle,a,b) { + const e = this.transform( + this.element('path',{d: `M ${dp(a)} 0 L ${dp(b)} 0`}), + `translate(${dp(x)} ${dp(y)}) rotate(${dp(RTOD(angle))})` + ); + this.set_stroke(e); + return e; + } + + draw_double_dot(x,y,angle,t,d,r) { + const g = this.transform(this.element('g'),`translate(${dp(x)} ${dp(y)}) rotate(${dp(RTOD(angle))})`); + for(let i=0;i<2;i++) { + const dot = this.transform(this.draw_dot(d,0,r), `rotate(${dp(RTOD(i*t))})`); + g.appendChild(dot); + } + return g; + } + + draw_double_dash(x,y,angle,a,b,t) { + const g = this.transform(this.element('g'),`translate(${dp(x)} ${dp(y)}) rotate(${dp(RTOD(angle))})`); + for(let i=0;i<2;i++) { + const p = this.transform(this.element('path',{d:`M ${dp(a)} 0 L ${dp(b)} 0`}), `rotate(${dp(RTOD(i*t))})`); + g.appendChild(p); + } + this.set_stroke(g); + return g; + } + + draw_double_arc(x,y,r1,r2,a,b) { + const g = this.element('g'); + g.appendChild(this.arc(x,y,r1,a,b)); + g.appendChild(this.arc(x,y,r2,a,b)); + this.set_stroke(g); + return g; + } + + draw_triple_dot(x,y,angle,t,d,r) { + const g = this.transform(this.element('g'),`translate(${dp(x)} ${dp(y)}) rotate(${dp(RTOD(angle))})`); + for(let i=0;i<2;i++) { + const dot = this.transform(this.draw_dot(d,0,r), `rotate(${dp(RTOD(i*t))})`); + g.appendChild(dot); + } + g.appendChild(this.transform(this.draw_dot(d*.75,0,r),`rotate(${dp(RTOD(t*0.5))})`)); + return g; + } + + draw_triple_dash(x,y,angle,a,b,t) { + const g = this.transform(this.element('g'),`translate(${dp(x)} ${dp(y)}) rotate(${dp(RTOD(angle))})`); + for(let i=0;i<3;i++) { + const p = this.transform(this.element('path',{d:`M ${dp(a)} 0 L ${dp(b)} 0`}), `rotate(${dp(RTOD(i*t))})`); + g.appendChild(p); + } + this.set_stroke(g); + return g; + } + + draw_triple_arc(x,y,r1,r2,r3,a,b) { + const g = this.element('g'); + g.appendChild(this.arc(x,y,r1,a,b)); + g.appendChild(this.arc(x,y,r2,a,b)); + g.appendChild(this.arc(x,y,r3,a,b)); + this.set_stroke(g); + return g; + } + + draw_point(A) { + const size = this.SIZE(0.05); + this.check_color(); + + const element = (() => { + switch(this.local.shape) { + case DOT: + return this.draw_dot(A.x,A.y,size); + case DISC: + const disc = this.element('circle',{cx: A.x, cy: A.y, r: size}); + this.set_stroke(disc); + return disc; + case BOX: + const r = this.element('rect',{x:A.x-size, y:A.y-size, width: 2*size, height: 2*size}); + this.set_fill(r,false); + return r; + case PLUS: + const plus = this.element('path',{ + d: `M ${dp(A.x-size)} ${dp(A.y)} l ${dp(2*size)} 0 M ${dp(A.x)} ${dp(A.y-size)} l 0 ${dp(2*size)}` + }); + this.set_stroke(plus); + return plus; + case CROSS: + const cross = this.element('path',{ + d: `M ${dp(A.x-size)} ${dp(A.y-size)} l ${dp(2*size)} ${dp(2*size)} M ${dp(A.x-size)} ${dp(A.y+size)} l ${dp(2*size)} ${dp(-2*size)}` + }); + this.set_stroke(cross); + return cross; + default: + console.error("no style"); + } + })(); + let desc = this.local.shape; + if(this.local.draggable) { + desc = `draggable ${desc}`; + } + if(this.has_label_for_point(A)) { + desc += ` at ${this.label_for_point(A)}`; + } + this.set_aria_label(element,desc); + return element; + } + + draw_text(text,x,y) { + const e = this.element('text',{'dominant-baseline': 'central', transform: `scale(1,-1) translate(${x},${-y})`},clean_label(text)); + this.set_font(e); + this.set_fill(e,false); + return e; + } + + label_point(A, align_angle) { + const text = this.local.label_text; + if(!text) { + return; + } + let angle = this.local.label_direction || 0; + if(angle>PI) { + angle -= 2*PI; + } + if(angle<-PI) { + angle += 2*PI; + } + const dist = this.SIZE(this.local.label_dist); + const x = A.x+dist*cos(angle); + const y = A.y+dist*sin(angle); + const e = this.draw_text(text,x,y); + let textAlign = 'start'; + let dy = 0; + if(this.local.align_label) { + dy = -0.5 * dist; + textAlign = 'middle'; + } else if(angle>=3*PI/4 || angle<=-3*PI/4) { + textAlign = 'end'; + } else if(angle>PI/4 || angle<-PI/4) { + textAlign = 'middle'; + if(angle>PI/4) { + dy = -0.5; + } else { + dy = 0.5; + } + } + if(dist==0) { + textAlign = 'middle'; + } + e.setAttribute('dy',`${dy}em`); + e.setAttribute('text-anchor',textAlign); + let desc = `label "${clean_label(text)}"`; + if(this.aria_mode==VERBOSE) { + desc += ` at ${A}`; + } + this.set_aria_label(e,desc); + if(align_angle === undefined) { + align_angle = angle; + } + if(this.local.align_label) { + this.transform(e, `rotate(${dp(RTOD(-align_angle))})`); + } + return e; + } + + label_segment(A,B) { + const size = this.SIZE(0.1); + + const angle = argument(A,B); + const x = (A.x+B.x)/2; + const y = (A.y+B.y)/2; + + const g = this.element('g'); + const s = this.draw_polygon(new Set([A,B])); + let desc = s.getAttribute('aria-label') || ''; + g.appendChild(s); + + if(this.local.label_segment!=NONE) { + const mark = this.transform(this.element('path'), `translate(${dp(x)} ${dp(y)}) scale(${dp(size)}) rotate(${dp(RTOD(angle))})`); + this.set_stroke(mark); + mark.setAttribute('stroke-width',mark.getAttribute('stroke-width')/size); + this.set_style(mark); + let d; + switch(this.local.label_segment) { + case SIMPLE: + d = `M -0.5 -1 L 0.5 1`; + desc += ', marked with a dash'; + break; + case DOUBLE: + d = `M -1 -1 L 0 1 M 0 -1 L 1 1`; + desc += ', marked with two dashes'; + break; + case TRIPLE: + d = `M -1.5 -1 L -0.5 1 M -0.5 -1 L 0.5 1 M 0.5 -1 L 1.5 1`; + desc += ', marked with three dashes'; + break; + } + mark.setAttribute('d',d); + g.appendChild(mark); + } + if(this.local.label_text) { + this.push_local_settings(); + if(this.local.label_direction===undefined) { + this.local.label_direction = argument(A,B)+PI/2; + } + const m = Point.create_midpoint(new Set([A,B])); + const text = this.label_point(m, angle); + this.pop_local_settings(); + g.appendChild(text); + text.removeAttribute('aria-label'); + desc += `, labelled "${this.local.label_text}"`; + } + g.setAttribute('aria-label',desc); + return g; + } + + draw_mark(B,r,a,b) { + const e = this.arc(B.x,B.y,r,a,b); + this.set_stroke(e); + return e; + } + + draw_arrow(x,y,angle,size) { + const p = this.transform( + this.element('path',{d:`M -2 1 L 0.0362998 0.0803779 A 0.088194 0.088194 0 0 0 0.0362998 -0.0803779 L -2 -1 L -1 0 z`}), + `translate(${dp(x)} ${dp(y)}) rotate(${dp(RTOD(angle))}) scale(${dp(size)})` + ); + this.set_fill(p,false); + return p; + } + + label_angle(A,B,C) { + const a = argument(B,C); + const b = argument(B,A); + const wiggle = (a%(2*PI)+(a<0 ? 2*PI : 0))/(4*PI)*0.2; // so angles round the same point can be distinguished + const r = this.SIZE(0.5+wiggle); + const s = 0.08/this.scale; + let mida = (b-a)/2; + if(mida>0) { + mida += PI; + } + let x1,y1,x2,y2; + const t = (8*PI/180)/this.local.size; + const g = this.element('g'); + + let desc = 'angle'; + if(this.has_label_for_point(A) && this.has_label_for_point(B) && this.has_label_for_point(C)) { + desc += ` ${this.label_for_point(A)} ${this.label_for_point(B)} ${this.label_for_point(C)}`; + } + switch(this.local.angle) { + case SIMPLE: + g.appendChild(this.draw_mark(B,r,a,b)); + switch(this.local.dec) { + case DOTTED: + [x1,y1,x2,y2] = this.set_xy(A,B,C,this.SIZE(Math.sqrt(2)/8)); + g.appendChild(this.draw_dot(B.x+x1+x2,B.y+y1+y2,this.SIZE(0.05))); + desc += ' marked with a dot'; + break; + case DASHED: + g.appendChild(this.draw_dash(B.x,B.y,(a+b)/2,r-s,r+s)); + desc += ' marked with a dash'; + break; + } + break; + case DOUBLE: + switch(this.local.dec) { + case DOTTED: + g.appendChild(this.draw_mark(B,r,a,b)); + g.appendChild(this.draw_double_dot(B.x,B.y,(a+b)/2-t,t*2,r*0.75,0.03)); + desc += ' marked with two dots'; + break; + case DASHED: + g.appendChild(this.draw_mark(B,r,a,b)); + g.appendChild(this.draw_double_dash(B.x,B.y,(a+b)/2-t/2,r+s,r-s,t)); + desc += ' marked with two dashes'; + break; + default: + g.appendChild(this.draw_double_arc(B.x,B.y,r-s/2,r+s/2,a,b)); + desc += ' double marked'; + } + break; + case TRIPLE: + switch(this.local.dec) { + case DASHED: + g.appendChild(this.draw_mark(B,r,a,b)); + g.appendChild(this.draw_triple_dash(B.x,B.y,(a+b)/2-t,r+s,r-s,t)); + desc += ' marked with three dashes'; + break; + case DOTTED: + g.appendChild(this.draw_mark(B,r,a,b)); + g.appendChild(this.draw_triple_dot(B.x,B.y,(a+b)/2-t,t*2,r*0.75,0.03)); + desc += ' marked with three dots'; + break; + default: + g.appendChild(this.draw_triple_arc(B.x,B.y,r-s,r,r+s,a,b)); + desc += ' triple marked'; + break; + } + break; + case RIGHT: + [x1,y1,x2,y2] = this.set_xy(A,B,C,this.SIZE(0.35)); + const p = this.element('path',{d:`M ${dp(B.x+x1)} ${dp(B.y+y1)} l ${dp(x2)} ${dp(y2)} l ${dp(-x1)} ${dp(-y1)}`}); + this.set_stroke(p); + g.appendChild(p); + desc = 'right '+desc; + if(this.local.dec==DOTTED) { + g.appendChild(this.draw_dot(B.x+(x1+x2)/2,B.y+(y1+y2)/2,this.SIZE(0.05))); + desc += ' marked with a dot'; + } + break; + case FORTH: + this.draw_mark(B,r,a,b); + [x1,y1,x2,y2] = this.set_xy(A,B,C,r); + g.appendChild(this.draw_arrow(B.x+x2,B.y+y2, a+acos(0.12/this.scale), 0.1/this.scale)); + desc += ' clockwise'; + break; + case BACK: + this.draw_mark(B,r,a,b); + [x1,y1,x2,y2] = this.set_xy(A,B,C,r); + g.appendChild(this.draw_arrow(B.x+x1,B.y+y1, b-acos(0.12/this.scale), 0.1/this.scale)); + desc += ' anti-clockwise'; + break; + } + if(this.local.label_text) { + desc += ` labelled "${clean_label(this.local.label_text)}"`; + this.push_local_settings(); + this.local.label_direction = a + mida; + this.local.label_dist = (r+2*s)*this.scale; + const text = this.label_point(B); + g.appendChild(text); + this.pop_local_settings(); + } + this.set_aria_label(g,desc); + return g; + } + + polygon(set,closed) { + const p = this.element(closed ? 'polygon' : 'polyline'); + p.setAttribute('points',set.points.map(p=>`${p.x},${p.y}`).join(' ')); + return p; + } + + draw_polygon(set) { + const p = this.polygon(set, set.points.length>2 && this.local.arrow==NONE && this.local.close); + this.set_stroke(p); + this.set_style(p); + let desc; + if(set.points.length==2) { + desc = `line segment`; + if(this.has_label_for_point(set.points[0]) && this.has_label_for_point(set.points[1])) { + desc += ` from ${this.label_for_point(set.points[0])} to ${this.label_for_point(set.points[1])}`; + } + } else { + desc = this.local.close ? set.shape_name() : 'path'; + if(set.points.every(p=>this.has_label_for_point(p))) { + desc += ` through ${set.points.map(p=>this.label_for_point(p)).join(', ')}`; + } else if(desc=='polygon' || desc=='path') { + desc += ` through ${set.points.length} vertices`; + } + } + desc = this.describe_style(desc); + if(this.local.arrow != NONE && set.points.length>=2) { + const g = this.element('g'); + g.appendChild(p); + if(this.local.dir==BACK || this.local.arrow==ARROWS) { + const [p1,p2] = set.points; + g.appendChild(this.draw_arrow(p1.x,p1.y,argument(p2,p1),this.SIZE(0.1))); + } + if(this.local.dir==FORTH || this.local.arrow==ARROWS) { + const [p3,p4] = [set.points[set.points.length-2], set.points[set.points.length-1]]; + g.appendChild(this.draw_arrow(p4.x,p4.y,argument(p3,p4),this.SIZE(0.1))); + } + desc = this.describe_arrows(desc); + this.set_aria_label(g,desc); + return g; + } else { + this.set_aria_label(p,desc); + return p; + } + } + + fill_polygon(set) { + const p = this.polygon(set,true); + this.set_fill(p); + let desc = `filled ${set.shape_name()}`; + switch(this.local.style) { + case DOTTED: + desc = `dotted ${desc}`; + break; + case DASHED: + desc = `striped ${desc}`; + break; + } + if(set.points.every(p=>this.has_label_for_point(p))) { + desc += ` through vertices ${set.points.map(p=>this.label_for_point(p)).join(', ')}`; + } else if(set.shape_name()=='polygon') { + desc += ` through ${set.points.length} vertices`; + } + this.set_aria_label(p,desc); + return p; + } + + draw_line(l) { + let m_x=this.min_x, m_y = this.min_y, M_x = this.max_x, M_y = this.max_y; + let desc = 'line'; + if(this.local.part==HALF) { + desc = 'ray'; + if((this.local.dir==FORTH && (l.a> -PI/2 && l.a <= PI/2)) || (this.local.dir==BACK && (l.a<= -PI/2 || l.a > PI/2))) { + m_x = l.x; + } else { + M_x = l.x; + } + if((this.local.dir == FORTH && l.a > 0) || (this.local.dir == BACK && l.a<=0)) { + m_y = l.y; + } else { + M_y = l.y; + } + } + const x = [0,0]; + const y = [0,0]; + let i = 0; + if(l.a==PI/2 || l.a==-PI/2) { + if(l.x >= m_x && l.x <= M_x) { + x[0] = x[1] = l.x; + y[0] = m_y; + y[1] = M_y; + i = 2; + } + } else if(l.a==0 || l.a==PI) { + if(l.y >= m_y && l.y <= M_y) { + y[0] = y[1] = l.y; + x[0] = m_x; + x[1] = M_x; + i = 2; + } + } else { + const t = tan(l.a); + let z = t*(m_x-l.x)+l.y; + if(z >= m_y && z <= M_y) { + x[i] = m_x; + y[i] = z; + i += 1; + } + z = t*(M_x-l.x)+l.y; + if(z >= m_y && z<= M_y) { + x[i] = M_x; + y[i] = z; + i += 1; + } + z = (m_y-l.y)/t+l.x; + if(z > m_x && z< M_x && i<2) { + x[i] = z; + y[i] = m_y; + i += 1; + } + z = (M_y-l.y)/t+l.x; + if(z > m_x && z< M_x && i<2) { + x[i] = z; + y[i] = M_y; + i += 1; + } + } + if(i==2) { + const p = this.element('line',{x1:x[0],y1:y[0],x2:x[1],y2:y[1]}); + switch(l.defined_by.kind) { + case 'heading': + if(this.has_label_for_point(l)) { + desc += ` through ${this.label_for_point(l)}`; + } + if(this.aria_mode==VERBOSE) { + desc += ` with heading ${dpformat(RTOD(l.a))}°`; + } + break; + case 'points': + const lps = l.defined_by.points.filter(p=>this.has_label_for_point(p)); + if(lps.length>0) { + desc += ` through ${lps.join(' and ')}`; + } + break; + case 'vector': + if(this.has_label_for_point(l)) { + desc += ` through ${this.label_for_point(l)}`; + } + if(this.aria_mode==VERBOSE) { + desc += ` with vector ${l.defined_by.vector}`; + } + break; + } + desc = this.describe_style(desc); + this.set_aria_label(p,desc); + this.set_stroke(p); + this.set_style(p); + return p; + } + + } + + draw_circle(c) { + const e = this.element('circle',{cx:c.x,cy:c.y,r:c.r}); + this.set_stroke(e); + this.set_style(e); + let desc = 'circle'; + if(this.has_label_for_point(c)) { + desc += ` centred at ${this.label_for_point(c)}`; + } + if(this.aria_mode==VERBOSE) { + desc += ` with radius ${dpformat(c.r)}`; + } + desc = this.describe_style(desc); + this.set_aria_label(e,desc); + return e; + } + + draw_arc(c,a,b) { + const arc = this.arc(c.x,c.y,c.r,b,a); + this.set_stroke(arc); + this.set_style(arc); + let desc = 'arc'; + if(this.has_label_for_point(c)) { + desc += ` centred at ${this.label_for_point(c)}`; + } + if(this.aria_mode==VERBOSE) { + desc += ` with radius ${dpformat(c.r)} between ${dpformat(RTOD(a))}° and ${dpformat(RTOD(b))}°`; + } + desc = this.describe_style(desc); + if(this.local.arrow!=NONE) { + const g = this.element('g'); + g.appendChild(arc); + const d = acos(this.SIZE(.06)/c.r); + if(this.local.dir == BACK || this.local.arrow == ARROWS) { + g.appendChild(this.draw_arrow(c.x+c.r*cos(b), c.y+c.r*sin(b), b+d, this.SIZE(0.1))); + } + if(this.local.dir == FORTH || this.local.arrow == ARROWS) { + g.appendChild(this.draw_arrow(c.x+c.r*cos(a), c.y+c.r*sin(a), a-d, this.SIZE(0.1))); + } + if(this.local.arrow==ARROWS) { + desc += ' with an arrow at each end'; + } else { + desc += this.local.dir==BACK ? ' anti-clockwise' : ' clockwise'; + } + this.set_aria_label(g,desc); + return g; + } else { + this.set_aria_label(arc,desc); + return arc; + } + } + + fill_arc(c,a,b) { + const arc = this.arc(c.x, c.y, c.r, b, a, true); + this.set_fill(arc); + this.set_style(arc); + let desc = 'filled arc'; + if(this.has_label_for_point(c)) { + desc += ` centred at ${this.label_for_point(c)}`; + } + if(this.aria_mode==VERBOSE) { + desc += ` with radius ${dpformat(c.r)} between ${dpformat(RTOD(a))}° and ${dpformat(RTOD(b))}°`; + } + desc = this.describe_style(desc); + this.set_aria_label(arc,desc); + return arc; + } + + fill_circle(c) { + const e = this.element('circle',{cx:c.x,cy:c.y,r:c.r}); + this.set_fill(e); + let desc = 'filled circle'; + if(this.has_label_for_point(c)) { + desc += ` centred at ${this.label_for_point(c)}`; + } + if(this.aria_mode==VERBOSE) { + desc += ` with radius ${dpformat(c.r)}`; + } + this.set_aria_label(e,desc); + return e; + } + + draw_parabolic_arc(x0,y0,p,f,g,c,s) { + const e = acos(p/this.get_max(x0,y0)-1); + f = Math.max(f,-e); + g = Math.min(g,e); + const ds = []; + for(let t=f, n=1; t<=g; t+=this.local.step*PI/180, n++) { + const [x,y] = parametric_parabola(t,x0,y0,p,c,s); + ds.push(n==1 ? `M ${x} ${y}` : `L ${x} ${y}`); + } + const path = this.element('path',{d:ds.join(' ')}); + this.set_stroke(path); + this.set_style(path); + let desc = `parabola`; + desc = this.describe_style(desc); + this.set_aria_label(path,desc); + return path; + } + + draw_elliptic_arc(x0,y0,a,b,f,g,c,s) { + const ds = []; + for(let t=f,n=1; t<=g; t+= this.local.step*PI/180, n++) { + const [x,y] = parametric_ellipse(t,x0,y0,a,b,c,s); + ds.push(n==1 ? `M ${x} ${y}` : `L ${x} ${y}`); + } + const path = this.element('path',{d:ds.join(' ')}); + this.set_stroke(path); + this.set_style(path); + let desc = 'ellipse'; + desc = this.describe_style(desc); + this.set_aria_label(path,desc); + return path; + } + + draw_branch(i,j,x0,y0,a,b,f,g,c,s) { + i = Math.max(i,f); + j = Math.min(j,g); + const ds = []; + for(let t=i,n=1; t<=j; t+=this.local.step*PI/180, n++) { + const [x,y] = parametric_hyperbola(t,x0,y0,a,b,c,s); + ds.push(n==1 ? `M ${x} ${y}` : `L ${x} ${y}`); + } + const path = this.element('path',{d:ds.join(' ')}); + this.set_stroke(path); + this.set_style(path); + let desc = 'hyperbola'; + desc = this.describe_style(desc); + this.set_aria_label(path,desc); + return path; + } +} + +/** Minimization routines + * from https://github.com/bijection/g9/blob/master/src/minimize.js + * + * MIT License + * + * Copyright (c) 2016 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +function norm2(x){ + return Math.sqrt(x.reduce((a,b)=>a+b*b,0)) +} + +function identity(n){ + var ret = Array(n) + for (var i = 0; i < n; i++) { + ret[i] = Array(n) + for (var j = 0; j < n; j++) ret[i][j] = +(i == j) ; + } + return ret +} + +function neg(x){ + return x.map(a=>-a) +} + +function dot(a,b){ + if (typeof a[0] !== 'number'){ + return a.map(x=>dot(x,b)) + } + return a.reduce((x,y,i) => x+y*b[i],0) +} + +function sub(a,b){ + if(typeof a[0] !== 'number'){ + return a.map((c,i)=>sub(c,b[i])) + } + return a.map((c,i)=>c-b[i]) +} + +function add(a,b){ + if(typeof a[0] !== 'number'){ + return a.map((c,i)=>add(c,b[i])) + } + return a.map((c,i)=>c+b[i]) +} + +function div(a,b){ + return a.map(c=>c.map(d=>d/b)) +} + +function mul(a,b){ + if(typeof a[0] !== 'number'){ + return a.map(c=>mul(c,b)) + } + return a.map(c=>c*b) +} + +function ten(a,b){ + return a.map((c,i)=>mul(b,c)) +} + +function isZero(a){ + for (var i = 0; i < a.length; i++) { + if(a[i]!== 0) return false + } + return true +} + +// Adapted from the numeric.js gradient and uncmin functions +// Numeric Javascript +// Copyright (C) 2011 by Sébastien Loisel + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +function gradient(f,x) { + var dim = x.length, f1 = f(x); + if(isNaN(f1)) throw new Error('The gradient at ['+x.join(' ')+'] is NaN!'); + var {max, abs, min} = Math + var tempX = x.slice(0), grad = Array(dim); + for(var i=0; i gradient(f,a) + + x0 = x0.slice(0) + var g0 = grad(x0) + var f0 = f(x0) + if(isNaN(f0)) throw new Error('minimize: f(x0) is a NaN!'); + var n = x0.length; + var H1 = identity(n) + + for(var it = 0; it= tol; it++) { + s = mul(step,t); + x1 = add(x0,s); + var f1 = f(x1); + if(!(f1-f0 >= 0.1*t*df0 || isNaN(f1))) break; + t *= 0.5; + } + if(t*nstep < tol && end_on_line_search) {var msg = "Line search step size smaller than tol"; break; } + if(it === maxit) { var msg = "maxit reached during line search"; break; } + var g1 = grad(x1); + var y = sub(g1,g0); + var ys = dot(y,s); + var Hy = dot(H1,y); + H1 = sub(add(H1,mul(ten(s,s),(ys+dot(y,Hy))/(ys*ys))),div(add(ten(Hy,s),ten(s,Hy)),ys)); + x0 = x1; + f0 = f1; + g0 = g1; + } + + return {solution: x0, f: f0, gradient: g0, invHessian: H1, iterations:it, message: msg}; +} + +function findPhaseChange(f, known_true, known_false){ + while(Math.abs(known_true - known_false) > 1e-3){ + var mid = (known_true + known_false) / 2 + f(mid) ? known_true = mid : known_false = mid + } + return (known_true + known_false) / 2 +} + + +/* + * This product includes color specifications and designs developed by Cynthia + * Brewer (http://colorbrewer.org/). + + https://groups.google.com/forum/?fromgroups=#!topic/d3-js/iyXFgJR1JY0 + */ + +const colorbrewer = { + +/*** Diverging ***/ +Spectral: {3: ['rgb(252,141,89)', 'rgb(255,255,191)', 'rgb(153,213,148)'], 4: ['rgb(215,25,28)', 'rgb(253,174,97)', 'rgb(171,221,164)', 'rgb(43,131,186)'], 5: ['rgb(215,25,28)', 'rgb(253,174,97)', 'rgb(255,255,191)', 'rgb(171,221,164)', 'rgb(43,131,186)'], 6: ['rgb(213,62,79)', 'rgb(252,141,89)', 'rgb(254,224,139)', 'rgb(230,245,152)', 'rgb(153,213,148)', 'rgb(50,136,189)'], 7: ['rgb(213,62,79)', 'rgb(252,141,89)', 'rgb(254,224,139)', 'rgb(255,255,191)', 'rgb(230,245,152)', 'rgb(153,213,148)', 'rgb(50,136,189)'], 8: ['rgb(213,62,79)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(230,245,152)', 'rgb(171,221,164)', 'rgb(102,194,165)', 'rgb(50,136,189)'], 9: ['rgb(213,62,79)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(255,255,191)', 'rgb(230,245,152)', 'rgb(171,221,164)', 'rgb(102,194,165)', 'rgb(50,136,189)'], 10: ['rgb(158,1,66)', 'rgb(213,62,79)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(230,245,152)', 'rgb(171,221,164)', 'rgb(102,194,165)', 'rgb(50,136,189)', 'rgb(94,79,162)'], 11: ['rgb(158,1,66)', 'rgb(213,62,79)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(255,255,191)', 'rgb(230,245,152)', 'rgb(171,221,164)', 'rgb(102,194,165)', 'rgb(50,136,189)', 'rgb(94,79,162)'], 'properties':{'type': 'div', 'blind':[2,2,2,0,0,0,0,0,0],'print':[1,1,1,0,0,0,0,0,0],'copy':[1,1,1,0,0,0,0,0,0],'screen':[1,1,2,0,0,0,0,0,0] } } , +RdYlGn: {3: ['rgb(252,141,89)', 'rgb(255,255,191)', 'rgb(145,207,96)'], 4: ['rgb(215,25,28)', 'rgb(253,174,97)', 'rgb(166,217,106)', 'rgb(26,150,65)'], 5: ['rgb(215,25,28)', 'rgb(253,174,97)', 'rgb(255,255,191)', 'rgb(166,217,106)', 'rgb(26,150,65)'], 6: ['rgb(215,48,39)', 'rgb(252,141,89)', 'rgb(254,224,139)', 'rgb(217,239,139)', 'rgb(145,207,96)', 'rgb(26,152,80)'], 7: ['rgb(215,48,39)', 'rgb(252,141,89)', 'rgb(254,224,139)', 'rgb(255,255,191)', 'rgb(217,239,139)', 'rgb(145,207,96)', 'rgb(26,152,80)'], 8: ['rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(217,239,139)', 'rgb(166,217,106)', 'rgb(102,189,99)', 'rgb(26,152,80)'], 9: ['rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(255,255,191)', 'rgb(217,239,139)', 'rgb(166,217,106)', 'rgb(102,189,99)', 'rgb(26,152,80)'], 10: ['rgb(165,0,38)', 'rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(217,239,139)', 'rgb(166,217,106)', 'rgb(102,189,99)', 'rgb(26,152,80)', 'rgb(0,104,55)'], 11: ['rgb(165,0,38)', 'rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,139)', 'rgb(255,255,191)', 'rgb(217,239,139)', 'rgb(166,217,106)', 'rgb(102,189,99)', 'rgb(26,152,80)', 'rgb(0,104,55)'], 'properties':{'type': 'div', 'blind':[2,2,2,0,0,0,0,0,0],'print':[1,1,1,2,0,0,0,0,0],'copy':[0],'screen':[1,1,1,0,0,0,0,0,0] } } , +RdBu: {3: ['rgb(239,138,98)', 'rgb(247,247,247)', 'rgb(103,169,207)'], 4: ['rgb(202,0,32)', 'rgb(244,165,130)', 'rgb(146,197,222)', 'rgb(5,113,176)'], 5: ['rgb(202,0,32)', 'rgb(244,165,130)', 'rgb(247,247,247)', 'rgb(146,197,222)', 'rgb(5,113,176)'], 6: ['rgb(178,24,43)', 'rgb(239,138,98)', 'rgb(253,219,199)', 'rgb(209,229,240)', 'rgb(103,169,207)', 'rgb(33,102,172)'], 7: ['rgb(178,24,43)', 'rgb(239,138,98)', 'rgb(253,219,199)', 'rgb(247,247,247)', 'rgb(209,229,240)', 'rgb(103,169,207)', 'rgb(33,102,172)'], 8: ['rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(209,229,240)', 'rgb(146,197,222)', 'rgb(67,147,195)', 'rgb(33,102,172)'], 9: ['rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(247,247,247)', 'rgb(209,229,240)', 'rgb(146,197,222)', 'rgb(67,147,195)', 'rgb(33,102,172)'], 10: ['rgb(103,0,31)', 'rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(209,229,240)', 'rgb(146,197,222)', 'rgb(67,147,195)', 'rgb(33,102,172)', 'rgb(5,48,97)'], 11: ['rgb(103,0,31)', 'rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(247,247,247)', 'rgb(209,229,240)', 'rgb(146,197,222)', 'rgb(67,147,195)', 'rgb(33,102,172)', 'rgb(5,48,97)'], 'properties':{'type': 'div','blind':[1],'print':[1,1,1,1,0,0,0,0,0],'copy':[0],'screen':[1,1,1,0,0,0,0,0,0] } } , +PiYG: {3: ['rgb(233,163,201)', 'rgb(247,247,247)', 'rgb(161,215,106)'], 4: ['rgb(208,28,139)', 'rgb(241,182,218)', 'rgb(184,225,134)', 'rgb(77,172,38)'], 5: ['rgb(208,28,139)', 'rgb(241,182,218)', 'rgb(247,247,247)', 'rgb(184,225,134)', 'rgb(77,172,38)'], 6: ['rgb(197,27,125)', 'rgb(233,163,201)', 'rgb(253,224,239)', 'rgb(230,245,208)', 'rgb(161,215,106)', 'rgb(77,146,33)'], 7: ['rgb(197,27,125)', 'rgb(233,163,201)', 'rgb(253,224,239)', 'rgb(247,247,247)', 'rgb(230,245,208)', 'rgb(161,215,106)', 'rgb(77,146,33)'], 8: ['rgb(197,27,125)', 'rgb(222,119,174)', 'rgb(241,182,218)', 'rgb(253,224,239)', 'rgb(230,245,208)', 'rgb(184,225,134)', 'rgb(127,188,65)', 'rgb(77,146,33)'], 9: ['rgb(197,27,125)', 'rgb(222,119,174)', 'rgb(241,182,218)', 'rgb(253,224,239)', 'rgb(247,247,247)', 'rgb(230,245,208)', 'rgb(184,225,134)', 'rgb(127,188,65)', 'rgb(77,146,33)'], 10: ['rgb(142,1,82)', 'rgb(197,27,125)', 'rgb(222,119,174)', 'rgb(241,182,218)', 'rgb(253,224,239)', 'rgb(230,245,208)', 'rgb(184,225,134)', 'rgb(127,188,65)', 'rgb(77,146,33)', 'rgb(39,100,25)'], 11: ['rgb(142,1,82)', 'rgb(197,27,125)', 'rgb(222,119,174)', 'rgb(241,182,218)', 'rgb(253,224,239)', 'rgb(247,247,247)', 'rgb(230,245,208)', 'rgb(184,225,134)', 'rgb(127,188,65)', 'rgb(77,146,33)', 'rgb(39,100,25)'], 'properties':{'type': 'div','blind':[1],'print':[1,1,2,0,0,0,0,0,0],'copy':[0],'screen':[1,1,2,0,0,0,0,0,0] } } , +PRGn: {3: ['rgb(175,141,195)', 'rgb(247,247,247)', 'rgb(127,191,123)'], 4: ['rgb(123,50,148)', 'rgb(194,165,207)', 'rgb(166,219,160)', 'rgb(0,136,55)'], 5: ['rgb(123,50,148)', 'rgb(194,165,207)', 'rgb(247,247,247)', 'rgb(166,219,160)', 'rgb(0,136,55)'], 6: ['rgb(118,42,131)', 'rgb(175,141,195)', 'rgb(231,212,232)', 'rgb(217,240,211)', 'rgb(127,191,123)', 'rgb(27,120,55)'], 7: ['rgb(118,42,131)', 'rgb(175,141,195)', 'rgb(231,212,232)', 'rgb(247,247,247)', 'rgb(217,240,211)', 'rgb(127,191,123)', 'rgb(27,120,55)'], 8: ['rgb(118,42,131)', 'rgb(153,112,171)', 'rgb(194,165,207)', 'rgb(231,212,232)', 'rgb(217,240,211)', 'rgb(166,219,160)', 'rgb(90,174,97)', 'rgb(27,120,55)'], 9: ['rgb(118,42,131)', 'rgb(153,112,171)', 'rgb(194,165,207)', 'rgb(231,212,232)', 'rgb(247,247,247)', 'rgb(217,240,211)', 'rgb(166,219,160)', 'rgb(90,174,97)', 'rgb(27,120,55)'], 10: ['rgb(64,0,75)', 'rgb(118,42,131)', 'rgb(153,112,171)', 'rgb(194,165,207)', 'rgb(231,212,232)', 'rgb(217,240,211)', 'rgb(166,219,160)', 'rgb(90,174,97)', 'rgb(27,120,55)', 'rgb(0,68,27)'], 11: ['rgb(64,0,75)', 'rgb(118,42,131)', 'rgb(153,112,171)', 'rgb(194,165,207)', 'rgb(231,212,232)', 'rgb(247,247,247)', 'rgb(217,240,211)', 'rgb(166,219,160)', 'rgb(90,174,97)', 'rgb(27,120,55)', 'rgb(0,68,27)'], 'properties':{'type': 'div','blind':[1],'print':[1,1,1,1,0,0,0,0,0],'copy':[0],'screen':[1,1,2,2,0,0,0,0,0] } } , +RdYlBu: {3: ['rgb(252,141,89)', 'rgb(255,255,191)', 'rgb(145,191,219)'], 4: ['rgb(215,25,28)', 'rgb(253,174,97)', 'rgb(171,217,233)', 'rgb(44,123,182)'], 5: ['rgb(215,25,28)', 'rgb(253,174,97)', 'rgb(255,255,191)', 'rgb(171,217,233)', 'rgb(44,123,182)'], 6: ['rgb(215,48,39)', 'rgb(252,141,89)', 'rgb(254,224,144)', 'rgb(224,243,248)', 'rgb(145,191,219)', 'rgb(69,117,180)'], 7: ['rgb(215,48,39)', 'rgb(252,141,89)', 'rgb(254,224,144)', 'rgb(255,255,191)', 'rgb(224,243,248)', 'rgb(145,191,219)', 'rgb(69,117,180)'], 8: ['rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,144)', 'rgb(224,243,248)', 'rgb(171,217,233)', 'rgb(116,173,209)', 'rgb(69,117,180)'], 9: ['rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,144)', 'rgb(255,255,191)', 'rgb(224,243,248)', 'rgb(171,217,233)', 'rgb(116,173,209)', 'rgb(69,117,180)'], 10: ['rgb(165,0,38)', 'rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,144)', 'rgb(224,243,248)', 'rgb(171,217,233)', 'rgb(116,173,209)', 'rgb(69,117,180)', 'rgb(49,54,149)'], 11: ['rgb(165,0,38)', 'rgb(215,48,39)', 'rgb(244,109,67)', 'rgb(253,174,97)', 'rgb(254,224,144)', 'rgb(255,255,191)', 'rgb(224,243,248)', 'rgb(171,217,233)', 'rgb(116,173,209)', 'rgb(69,117,180)', 'rgb(49,54,149)'], 'properties':{'type': 'div','blind':[1],'print':[1,1,1,1,2,0,0,0,0],'copy':[0],'screen':[1,1,1,2,0,0,0,0,0] } } , +BrBG: {3: ['rgb(216,179,101)', 'rgb(245,245,245)', 'rgb(90,180,172)'], 4: ['rgb(166,97,26)', 'rgb(223,194,125)', 'rgb(128,205,193)', 'rgb(1,133,113)'], 5: ['rgb(166,97,26)', 'rgb(223,194,125)', 'rgb(245,245,245)', 'rgb(128,205,193)', 'rgb(1,133,113)'], 6: ['rgb(140,81,10)', 'rgb(216,179,101)', 'rgb(246,232,195)', 'rgb(199,234,229)', 'rgb(90,180,172)', 'rgb(1,102,94)'], 7: ['rgb(140,81,10)', 'rgb(216,179,101)', 'rgb(246,232,195)', 'rgb(245,245,245)', 'rgb(199,234,229)', 'rgb(90,180,172)', 'rgb(1,102,94)'], 8: ['rgb(140,81,10)', 'rgb(191,129,45)', 'rgb(223,194,125)', 'rgb(246,232,195)', 'rgb(199,234,229)', 'rgb(128,205,193)', 'rgb(53,151,143)', 'rgb(1,102,94)'], 9: ['rgb(140,81,10)', 'rgb(191,129,45)', 'rgb(223,194,125)', 'rgb(246,232,195)', 'rgb(245,245,245)', 'rgb(199,234,229)', 'rgb(128,205,193)', 'rgb(53,151,143)', 'rgb(1,102,94)'], 10: ['rgb(84,48,5)', 'rgb(140,81,10)', 'rgb(191,129,45)', 'rgb(223,194,125)', 'rgb(246,232,195)', 'rgb(199,234,229)', 'rgb(128,205,193)', 'rgb(53,151,143)', 'rgb(1,102,94)', 'rgb(0,60,48)'], 11: ['rgb(84,48,5)', 'rgb(140,81,10)', 'rgb(191,129,45)', 'rgb(223,194,125)', 'rgb(246,232,195)', 'rgb(245,245,245)', 'rgb(199,234,229)', 'rgb(128,205,193)', 'rgb(53,151,143)', 'rgb(1,102,94)', 'rgb(0,60,48)'], 'properties':{'type': 'div','blind':[1],'print':[1,1,1,1,0,0,0,0,0],'copy':[0],'screen':[1,1,1,1,0,0,0,0,0] } } , +RdGy: {3: ['rgb(239,138,98)', 'rgb(255,255,255)', 'rgb(153,153,153)'], 4: ['rgb(202,0,32)', 'rgb(244,165,130)', 'rgb(186,186,186)', 'rgb(64,64,64)'], 5: ['rgb(202,0,32)', 'rgb(244,165,130)', 'rgb(255,255,255)', 'rgb(186,186,186)', 'rgb(64,64,64)'], 6: ['rgb(178,24,43)', 'rgb(239,138,98)', 'rgb(253,219,199)', 'rgb(224,224,224)', 'rgb(153,153,153)', 'rgb(77,77,77)'], 7: ['rgb(178,24,43)', 'rgb(239,138,98)', 'rgb(253,219,199)', 'rgb(255,255,255)', 'rgb(224,224,224)', 'rgb(153,153,153)', 'rgb(77,77,77)'], 8: ['rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(224,224,224)', 'rgb(186,186,186)', 'rgb(135,135,135)', 'rgb(77,77,77)'], 9: ['rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(255,255,255)', 'rgb(224,224,224)', 'rgb(186,186,186)', 'rgb(135,135,135)', 'rgb(77,77,77)'], 10: ['rgb(103,0,31)', 'rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(224,224,224)', 'rgb(186,186,186)', 'rgb(135,135,135)', 'rgb(77,77,77)', 'rgb(26,26,26)'], 11: ['rgb(103,0,31)', 'rgb(178,24,43)', 'rgb(214,96,77)', 'rgb(244,165,130)', 'rgb(253,219,199)', 'rgb(255,255,255)', 'rgb(224,224,224)', 'rgb(186,186,186)', 'rgb(135,135,135)', 'rgb(77,77,77)', 'rgb(26,26,26)'], 'properties':{'type': 'div','blind':[2],'print':[1,1,1,2,0,0,0,0,0],'copy':[0],'screen':[1,1,2,0,0,0,0,0,0] } } , +PuOr: {3: ['rgb(241,163,64)', 'rgb(247,247,247)', 'rgb(153,142,195)'], 4: ['rgb(230,97,1)', 'rgb(253,184,99)', 'rgb(178,171,210)', 'rgb(94,60,153)'], 5: ['rgb(230,97,1)', 'rgb(253,184,99)', 'rgb(247,247,247)', 'rgb(178,171,210)', 'rgb(94,60,153)'], 6: ['rgb(179,88,6)', 'rgb(241,163,64)', 'rgb(254,224,182)', 'rgb(216,218,235)', 'rgb(153,142,195)', 'rgb(84,39,136)'], 7: ['rgb(179,88,6)', 'rgb(241,163,64)', 'rgb(254,224,182)', 'rgb(247,247,247)', 'rgb(216,218,235)', 'rgb(153,142,195)', 'rgb(84,39,136)'], 8: ['rgb(179,88,6)', 'rgb(224,130,20)', 'rgb(253,184,99)', 'rgb(254,224,182)', 'rgb(216,218,235)', 'rgb(178,171,210)', 'rgb(128,115,172)', 'rgb(84,39,136)'], 9: ['rgb(179,88,6)', 'rgb(224,130,20)', 'rgb(253,184,99)', 'rgb(254,224,182)', 'rgb(247,247,247)', 'rgb(216,218,235)', 'rgb(178,171,210)', 'rgb(128,115,172)', 'rgb(84,39,136)'], 10: ['rgb(127,59,8)', 'rgb(179,88,6)', 'rgb(224,130,20)', 'rgb(253,184,99)', 'rgb(254,224,182)', 'rgb(216,218,235)', 'rgb(178,171,210)', 'rgb(128,115,172)', 'rgb(84,39,136)', 'rgb(45,0,75)'], 11: ['rgb(127,59,8)', 'rgb(179,88,6)', 'rgb(224,130,20)', 'rgb(253,184,99)', 'rgb(254,224,182)', 'rgb(247,247,247)', 'rgb(216,218,235)', 'rgb(178,171,210)', 'rgb(128,115,172)', 'rgb(84,39,136)', 'rgb(45,0,75)'], 'properties':{'type': 'div','blind':[1],'print':[1,1,2,2,0,0,0,0,0],'copy':[1,1,0,0,0,0,0,0,0],'screen':[1,1,1,1,0,0,0,0,0] } } , + +/*** Qualitative ***/ +Set2: {3: ['rgb(102,194,165)', 'rgb(252,141,98)', 'rgb(141,160,203)'], 4: ['rgb(102,194,165)', 'rgb(252,141,98)', 'rgb(141,160,203)', 'rgb(231,138,195)'], 5: ['rgb(102,194,165)', 'rgb(252,141,98)', 'rgb(141,160,203)', 'rgb(231,138,195)', 'rgb(166,216,84)'], 6: ['rgb(102,194,165)', 'rgb(252,141,98)', 'rgb(141,160,203)', 'rgb(231,138,195)', 'rgb(166,216,84)', 'rgb(255,217,47)'], 7: ['rgb(102,194,165)', 'rgb(252,141,98)', 'rgb(141,160,203)', 'rgb(231,138,195)', 'rgb(166,216,84)', 'rgb(255,217,47)', 'rgb(229,196,148)'], 8: ['rgb(102,194,165)', 'rgb(252,141,98)', 'rgb(141,160,203)', 'rgb(231,138,195)', 'rgb(166,216,84)', 'rgb(255,217,47)', 'rgb(229,196,148)', 'rgb(179,179,179)'], 'properties':{'type': 'qual','blind':[1,2,2,2,0,0,0],'print':[1,1,1,2,2,2],'copy':[0],'screen':[1,1,2,2,2,2] } } , +Accent: {3: ['rgb(127,201,127)', 'rgb(190,174,212)', 'rgb(253,192,134)'], 4: ['rgb(127,201,127)', 'rgb(190,174,212)', 'rgb(253,192,134)', 'rgb(255,255,153)'], 5: ['rgb(127,201,127)', 'rgb(190,174,212)', 'rgb(253,192,134)', 'rgb(255,255,153)', 'rgb(56,108,176)'], 6: ['rgb(127,201,127)', 'rgb(190,174,212)', 'rgb(253,192,134)', 'rgb(255,255,153)', 'rgb(56,108,176)', 'rgb(240,2,127)'], 7: ['rgb(127,201,127)', 'rgb(190,174,212)', 'rgb(253,192,134)', 'rgb(255,255,153)', 'rgb(56,108,176)', 'rgb(240,2,127)', 'rgb(191,91,23)'], 8: ['rgb(127,201,127)', 'rgb(190,174,212)', 'rgb(253,192,134)', 'rgb(255,255,153)', 'rgb(56,108,176)', 'rgb(240,2,127)', 'rgb(191,91,23)', 'rgb(102,102,102)'], 'properties':{'type': 'qual','blind':[2,0,0,0,0,0,0],'print':[1,1,2,2,2,2],'copy':[0],'screen':[1,1,1,2,2,2] } } , +Set1: {3: ['rgb(228,26,28)', 'rgb(55,126,184)', 'rgb(77,175,74)'], 4: ['rgb(228,26,28)', 'rgb(55,126,184)', 'rgb(77,175,74)', 'rgb(152,78,163)'], 5: ['rgb(228,26,28)', 'rgb(55,126,184)', 'rgb(77,175,74)', 'rgb(152,78,163)', 'rgb(255,127,0)'], 6: ['rgb(228,26,28)', 'rgb(55,126,184)', 'rgb(77,175,74)', 'rgb(152,78,163)', 'rgb(255,127,0)', 'rgb(255,255,51)'], 7: ['rgb(228,26,28)', 'rgb(55,126,184)', 'rgb(77,175,74)', 'rgb(152,78,163)', 'rgb(255,127,0)', 'rgb(255,255,51)', 'rgb(166,86,40)'], 8: ['rgb(228,26,28)', 'rgb(55,126,184)', 'rgb(77,175,74)', 'rgb(152,78,163)', 'rgb(255,127,0)', 'rgb(255,255,51)', 'rgb(166,86,40)', 'rgb(247,129,191)'], 9: ['rgb(228,26,28)', 'rgb(55,126,184)', 'rgb(77,175,74)', 'rgb(152,78,163)', 'rgb(255,127,0)', 'rgb(255,255,51)', 'rgb(166,86,40)', 'rgb(247,129,191)', 'rgb(153,153,153)'], 'properties':{'type': 'qual','blind':[2],'print':[1],'copy':[0],'screen':[1] } } , +Set3: {3: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)'], 4: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)'], 5: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)'], 6: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)', 'rgb(253,180,98)'], 7: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)', 'rgb(253,180,98)', 'rgb(179,222,105)'], 8: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)', 'rgb(253,180,98)', 'rgb(179,222,105)', 'rgb(252,205,229)'], 9: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)', 'rgb(253,180,98)', 'rgb(179,222,105)', 'rgb(252,205,229)', 'rgb(217,217,217)'], 10: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)', 'rgb(253,180,98)', 'rgb(179,222,105)', 'rgb(252,205,229)', 'rgb(217,217,217)', 'rgb(188,128,189)'], 11: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)', 'rgb(253,180,98)', 'rgb(179,222,105)', 'rgb(252,205,229)', 'rgb(217,217,217)', 'rgb(188,128,189)', 'rgb(204,235,197)'], 12: ['rgb(141,211,199)', 'rgb(255,255,179)', 'rgb(190,186,218)', 'rgb(251,128,114)', 'rgb(128,177,211)', 'rgb(253,180,98)', 'rgb(179,222,105)', 'rgb(252,205,229)', 'rgb(217,217,217)', 'rgb(188,128,189)', 'rgb(204,235,197)', 'rgb(255,237,111)'], 'properties':{'type': 'qual','blind':[2,2,0,0,0,0,0,0,0,0],'print':[1,1,1,1,1,1,2,0,0,0],'copy':[1,2,2,2,2,2,2,0,0,0],'screen':[1,1,1,2,2,2,0,0,0,0] } } , +Dark2: {3: ['rgb(27,158,119)', 'rgb(217,95,2)', 'rgb(117,112,179)'], 4: ['rgb(27,158,119)', 'rgb(217,95,2)', 'rgb(117,112,179)', 'rgb(231,41,138)'], 5: ['rgb(27,158,119)', 'rgb(217,95,2)', 'rgb(117,112,179)', 'rgb(231,41,138)', 'rgb(102,166,30)'], 6: ['rgb(27,158,119)', 'rgb(217,95,2)', 'rgb(117,112,179)', 'rgb(231,41,138)', 'rgb(102,166,30)', 'rgb(230,171,2)'], 7: ['rgb(27,158,119)', 'rgb(217,95,2)', 'rgb(117,112,179)', 'rgb(231,41,138)', 'rgb(102,166,30)', 'rgb(230,171,2)', 'rgb(166,118,29)'], 8: ['rgb(27,158,119)', 'rgb(217,95,2)', 'rgb(117,112,179)', 'rgb(231,41,138)', 'rgb(102,166,30)', 'rgb(230,171,2)', 'rgb(166,118,29)', 'rgb(102,102,102)'], 'properties':{'type': 'qual','blind':[1,2,2,2,0,0],'print':[1],'copy':[0],'screen':[1] } } , +Paired: {3: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)'], 4: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)'], 5: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)'], 6: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)'], 7: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)', 'rgb(253,191,111)'], 8: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)', 'rgb(253,191,111)', 'rgb(255,127,0)'], 9: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)', 'rgb(253,191,111)', 'rgb(255,127,0)', 'rgb(202,178,214)'], 10: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)', 'rgb(253,191,111)', 'rgb(255,127,0)', 'rgb(202,178,214)', 'rgb(106,61,154)'], 11: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)', 'rgb(253,191,111)', 'rgb(255,127,0)', 'rgb(202,178,214)', 'rgb(106,61,154)', 'rgb(255,255,153)'], 12: ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)', 'rgb(253,191,111)', 'rgb(255,127,0)', 'rgb(202,178,214)', 'rgb(106,61,154)', 'rgb(255,255,153)', 'rgb(177,89,40)'], 'properties':{'type': 'qual','blind':[1,1,2,2,2,2,0,0,0],'print':[1,1,1,1,1,2,2,2,2],'copy':[0],'screen':[1,1,1,1,1,1,1,1,2] } } , +Pastel2: {3: ['rgb(179,226,205)', 'rgb(253,205,172)', 'rgb(203,213,232)'], 4: ['rgb(179,226,205)', 'rgb(253,205,172)', 'rgb(203,213,232)', 'rgb(244,202,228)'], 5: ['rgb(179,226,205)', 'rgb(253,205,172)', 'rgb(203,213,232)', 'rgb(244,202,228)', 'rgb(230,245,201)'], 6: ['rgb(179,226,205)', 'rgb(253,205,172)', 'rgb(203,213,232)', 'rgb(244,202,228)', 'rgb(230,245,201)', 'rgb(255,242,174)'], 7: ['rgb(179,226,205)', 'rgb(253,205,172)', 'rgb(203,213,232)', 'rgb(244,202,228)', 'rgb(230,245,201)', 'rgb(255,242,174)', 'rgb(241,226,204)'], 8: ['rgb(179,226,205)', 'rgb(253,205,172)', 'rgb(203,213,232)', 'rgb(244,202,228)', 'rgb(230,245,201)', 'rgb(255,242,174)', 'rgb(241,226,204)', 'rgb(204,204,204)'], 'properties':{'type': 'qual','blind':[2,0,0,0,0,0],'print':[2,0,0,0,0,0],'copy':[0],'screen':[2,2,0,0,0,0] } } , +Pastel1: {3: ['rgb(251,180,174)', 'rgb(179,205,227)', 'rgb(204,235,197)'], 4: ['rgb(251,180,174)', 'rgb(179,205,227)', 'rgb(204,235,197)', 'rgb(222,203,228)'], 5: ['rgb(251,180,174)', 'rgb(179,205,227)', 'rgb(204,235,197)', 'rgb(222,203,228)', 'rgb(254,217,166)'], 6: ['rgb(251,180,174)', 'rgb(179,205,227)', 'rgb(204,235,197)', 'rgb(222,203,228)', 'rgb(254,217,166)', 'rgb(255,255,204)'], 7: ['rgb(251,180,174)', 'rgb(179,205,227)', 'rgb(204,235,197)', 'rgb(222,203,228)', 'rgb(254,217,166)', 'rgb(255,255,204)', 'rgb(229,216,189)'], 8: ['rgb(251,180,174)', 'rgb(179,205,227)', 'rgb(204,235,197)', 'rgb(222,203,228)', 'rgb(254,217,166)', 'rgb(255,255,204)', 'rgb(229,216,189)', 'rgb(253,218,236)'], 9: ['rgb(251,180,174)', 'rgb(179,205,227)', 'rgb(204,235,197)', 'rgb(222,203,228)', 'rgb(254,217,166)', 'rgb(255,255,204)', 'rgb(229,216,189)', 'rgb(253,218,236)', 'rgb(242,242,242)'], 'properties':{'type': 'qual','blind':[2,0,0,0,0,0,0],'print':[2,2,2,0,0,0,0],'copy':[0],'screen':[2,2,2,2,0,0,0] } } , + +// from https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ +Trubetskoy: {3: ['#ffe119','#4363d8','#f58231'], 4: ['#ffe119','#4363d8','#f58231','#e6beff'], 5: ['#ffe119','#4363d8','#f58231','#e6beff','#800000'], 6: ['#ffe119','#4363d8','#f58231','#e6beff','#800000','#000075'], 'properties': {'type': 'qual','blind':[1],'print':[1],'copy':[1],'screen':[1] } }, + +/*** Sequential ***/ +OrRd: {3: ['rgb(254,232,200)', 'rgb(253,187,132)', 'rgb(227,74,51)'], 4: ['rgb(254,240,217)', 'rgb(253,204,138)', 'rgb(252,141,89)', 'rgb(215,48,31)'], 5: ['rgb(254,240,217)', 'rgb(253,204,138)', 'rgb(252,141,89)', 'rgb(227,74,51)', 'rgb(179,0,0)'], 6: ['rgb(254,240,217)', 'rgb(253,212,158)', 'rgb(253,187,132)', 'rgb(252,141,89)', 'rgb(227,74,51)', 'rgb(179,0,0)'], 7: ['rgb(254,240,217)', 'rgb(253,212,158)', 'rgb(253,187,132)', 'rgb(252,141,89)', 'rgb(239,101,72)', 'rgb(215,48,31)', 'rgb(153,0,0)'], 8: ['rgb(255,247,236)', 'rgb(254,232,200)', 'rgb(253,212,158)', 'rgb(253,187,132)', 'rgb(252,141,89)', 'rgb(239,101,72)', 'rgb(215,48,31)', 'rgb(153,0,0)'], 9: ['rgb(255,247,236)', 'rgb(254,232,200)', 'rgb(253,212,158)', 'rgb(253,187,132)', 'rgb(252,141,89)', 'rgb(239,101,72)', 'rgb(215,48,31)', 'rgb(179,0,0)', 'rgb(127,0,0)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,0,0,0,0,0],'copy':[1,1,2,0,0,0,0],'screen':[1,1,1,0,0,0,0] } } , +PuBu: {3: ['rgb(236,231,242)', 'rgb(166,189,219)', 'rgb(43,140,190)'], 4: ['rgb(241,238,246)', 'rgb(189,201,225)', 'rgb(116,169,207)', 'rgb(5,112,176)'], 5: ['rgb(241,238,246)', 'rgb(189,201,225)', 'rgb(116,169,207)', 'rgb(43,140,190)', 'rgb(4,90,141)'], 6: ['rgb(241,238,246)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(116,169,207)', 'rgb(43,140,190)', 'rgb(4,90,141)'], 7: ['rgb(241,238,246)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(116,169,207)', 'rgb(54,144,192)', 'rgb(5,112,176)', 'rgb(3,78,123)'], 8: ['rgb(255,247,251)', 'rgb(236,231,242)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(116,169,207)', 'rgb(54,144,192)', 'rgb(5,112,176)', 'rgb(3,78,123)'], 9: ['rgb(255,247,251)', 'rgb(236,231,242)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(116,169,207)', 'rgb(54,144,192)', 'rgb(5,112,176)', 'rgb(4,90,141)', 'rgb(2,56,88)'], 'properties':{'type': 'seq','blind':[1],'print':[1,2,2,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,2,0,0,0,0] } } , +BuPu: {3: ['rgb(224,236,244)', 'rgb(158,188,218)', 'rgb(136,86,167)'], 4: ['rgb(237,248,251)', 'rgb(179,205,227)', 'rgb(140,150,198)', 'rgb(136,65,157)'], 5: ['rgb(237,248,251)', 'rgb(179,205,227)', 'rgb(140,150,198)', 'rgb(136,86,167)', 'rgb(129,15,124)'], 6: ['rgb(237,248,251)', 'rgb(191,211,230)', 'rgb(158,188,218)', 'rgb(140,150,198)', 'rgb(136,86,167)', 'rgb(129,15,124)'], 7: ['rgb(237,248,251)', 'rgb(191,211,230)', 'rgb(158,188,218)', 'rgb(140,150,198)', 'rgb(140,107,177)', 'rgb(136,65,157)', 'rgb(110,1,107)'], 8: ['rgb(247,252,253)', 'rgb(224,236,244)', 'rgb(191,211,230)', 'rgb(158,188,218)', 'rgb(140,150,198)', 'rgb(140,107,177)', 'rgb(136,65,157)', 'rgb(110,1,107)'], 9: ['rgb(247,252,253)', 'rgb(224,236,244)', 'rgb(191,211,230)', 'rgb(158,188,218)', 'rgb(140,150,198)', 'rgb(140,107,177)', 'rgb(136,65,157)', 'rgb(129,15,124)', 'rgb(77,0,75)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,2,2,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,1,0,0,0,0] } } , +Oranges: {3: ['rgb(254,230,206)', 'rgb(253,174,107)', 'rgb(230,85,13)'], 4: ['rgb(254,237,222)', 'rgb(253,190,133)', 'rgb(253,141,60)', 'rgb(217,71,1)'], 5: ['rgb(254,237,222)', 'rgb(253,190,133)', 'rgb(253,141,60)', 'rgb(230,85,13)', 'rgb(166,54,3)'], 6: ['rgb(254,237,222)', 'rgb(253,208,162)', 'rgb(253,174,107)', 'rgb(253,141,60)', 'rgb(230,85,13)', 'rgb(166,54,3)'], 7: ['rgb(254,237,222)', 'rgb(253,208,162)', 'rgb(253,174,107)', 'rgb(253,141,60)', 'rgb(241,105,19)', 'rgb(217,72,1)', 'rgb(140,45,4)'], 8: ['rgb(255,245,235)', 'rgb(254,230,206)', 'rgb(253,208,162)', 'rgb(253,174,107)', 'rgb(253,141,60)', 'rgb(241,105,19)', 'rgb(217,72,1)', 'rgb(140,45,4)'], 9: ['rgb(255,245,235)', 'rgb(254,230,206)', 'rgb(253,208,162)', 'rgb(253,174,107)', 'rgb(253,141,60)', 'rgb(241,105,19)', 'rgb(217,72,1)', 'rgb(166,54,3)', 'rgb(127,39,4)'], 'properties':{'type': 'seq','blind':[1],'print':[1,2,0,0,0,0,0],'copy':[1,2,2,0,0,0,0],'screen':[1,1,1,0,0,0,0] } } , +BuGn: {3: ['rgb(229,245,249)', 'rgb(153,216,201)', 'rgb(44,162,95)'], 4: ['rgb(237,248,251)', 'rgb(178,226,226)', 'rgb(102,194,164)', 'rgb(35,139,69)'], 5: ['rgb(237,248,251)', 'rgb(178,226,226)', 'rgb(102,194,164)', 'rgb(44,162,95)', 'rgb(0,109,44)'], 6: ['rgb(237,248,251)', 'rgb(204,236,230)', 'rgb(153,216,201)', 'rgb(102,194,164)', 'rgb(44,162,95)', 'rgb(0,109,44)'], 7: ['rgb(237,248,251)', 'rgb(204,236,230)', 'rgb(153,216,201)', 'rgb(102,194,164)', 'rgb(65,174,118)', 'rgb(35,139,69)', 'rgb(0,88,36)'], 8: ['rgb(247,252,253)', 'rgb(229,245,249)', 'rgb(204,236,230)', 'rgb(153,216,201)', 'rgb(102,194,164)', 'rgb(65,174,118)', 'rgb(35,139,69)', 'rgb(0,88,36)'], 9: ['rgb(247,252,253)', 'rgb(229,245,249)', 'rgb(204,236,230)', 'rgb(153,216,201)', 'rgb(102,194,164)', 'rgb(65,174,118)', 'rgb(35,139,69)', 'rgb(0,109,44)', 'rgb(0,68,27)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,2,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,2,0,0,0,0,0] } } , +YlOrBr: {3: ['rgb(255,247,188)', 'rgb(254,196,79)', 'rgb(217,95,14)'], 4: ['rgb(255,255,212)', 'rgb(254,217,142)', 'rgb(254,153,41)', 'rgb(204,76,2)'], 5: ['rgb(255,255,212)', 'rgb(254,217,142)', 'rgb(254,153,41)', 'rgb(217,95,14)', 'rgb(153,52,4)'], 6: ['rgb(255,255,212)', 'rgb(254,227,145)', 'rgb(254,196,79)', 'rgb(254,153,41)', 'rgb(217,95,14)', 'rgb(153,52,4)'], 7: ['rgb(255,255,212)', 'rgb(254,227,145)', 'rgb(254,196,79)', 'rgb(254,153,41)', 'rgb(236,112,20)', 'rgb(204,76,2)', 'rgb(140,45,4)'], 8: ['rgb(255,255,229)', 'rgb(255,247,188)', 'rgb(254,227,145)', 'rgb(254,196,79)', 'rgb(254,153,41)', 'rgb(236,112,20)', 'rgb(204,76,2)', 'rgb(140,45,4)'], 9: ['rgb(255,255,229)', 'rgb(255,247,188)', 'rgb(254,227,145)', 'rgb(254,196,79)', 'rgb(254,153,41)', 'rgb(236,112,20)', 'rgb(204,76,2)', 'rgb(153,52,4)', 'rgb(102,37,6)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,2,0,0,0,0],'copy':[1,2,2,0,0,0,0],'screen':[1,2,0,0,0,0,0] } } , +YlGn: {3: ['rgb(247,252,185)', 'rgb(173,221,142)', 'rgb(49,163,84)'], 4: ['rgb(255,255,204)', 'rgb(194,230,153)', 'rgb(120,198,121)', 'rgb(35,132,67)'], 5: ['rgb(255,255,204)', 'rgb(194,230,153)', 'rgb(120,198,121)', 'rgb(49,163,84)', 'rgb(0,104,55)'], 6: ['rgb(255,255,204)', 'rgb(217,240,163)', 'rgb(173,221,142)', 'rgb(120,198,121)', 'rgb(49,163,84)', 'rgb(0,104,55)'], 7: ['rgb(255,255,204)', 'rgb(217,240,163)', 'rgb(173,221,142)', 'rgb(120,198,121)', 'rgb(65,171,93)', 'rgb(35,132,67)', 'rgb(0,90,50)'], 8: ['rgb(255,255,229)', 'rgb(247,252,185)', 'rgb(217,240,163)', 'rgb(173,221,142)', 'rgb(120,198,121)', 'rgb(65,171,93)', 'rgb(35,132,67)', 'rgb(0,90,50)'], 9: ['rgb(255,255,229)', 'rgb(247,252,185)', 'rgb(217,240,163)', 'rgb(173,221,142)', 'rgb(120,198,121)', 'rgb(65,171,93)', 'rgb(35,132,67)', 'rgb(0,104,55)', 'rgb(0,69,41)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,1,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,1,0,0,0,0] } } , +Reds: {3: ['rgb(254,224,210)', 'rgb(252,146,114)', 'rgb(222,45,38)'], 4: ['rgb(254,229,217)', 'rgb(252,174,145)', 'rgb(251,106,74)', 'rgb(203,24,29)'], 5: ['rgb(254,229,217)', 'rgb(252,174,145)', 'rgb(251,106,74)', 'rgb(222,45,38)', 'rgb(165,15,21)'], 6: ['rgb(254,229,217)', 'rgb(252,187,161)', 'rgb(252,146,114)', 'rgb(251,106,74)', 'rgb(222,45,38)', 'rgb(165,15,21)'], 7: ['rgb(254,229,217)', 'rgb(252,187,161)', 'rgb(252,146,114)', 'rgb(251,106,74)', 'rgb(239,59,44)', 'rgb(203,24,29)', 'rgb(153,0,13)'], 8: ['rgb(255,245,240)', 'rgb(254,224,210)', 'rgb(252,187,161)', 'rgb(252,146,114)', 'rgb(251,106,74)', 'rgb(239,59,44)', 'rgb(203,24,29)', 'rgb(153,0,13)'], 9: ['rgb(255,245,240)', 'rgb(254,224,210)', 'rgb(252,187,161)', 'rgb(252,146,114)', 'rgb(251,106,74)', 'rgb(239,59,44)', 'rgb(203,24,29)', 'rgb(165,15,21)', 'rgb(103,0,13)'], 'properties':{'type': 'seq','blind':[1],'print':[1,2,2,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,2,0,0,0,0,0] } } , +RdPu: {3: ['rgb(253,224,221)', 'rgb(250,159,181)', 'rgb(197,27,138)'], 4: ['rgb(254,235,226)', 'rgb(251,180,185)', 'rgb(247,104,161)', 'rgb(174,1,126)'], 5: ['rgb(254,235,226)', 'rgb(251,180,185)', 'rgb(247,104,161)', 'rgb(197,27,138)', 'rgb(122,1,119)'], 6: ['rgb(254,235,226)', 'rgb(252,197,192)', 'rgb(250,159,181)', 'rgb(247,104,161)', 'rgb(197,27,138)', 'rgb(122,1,119)'], 7: ['rgb(254,235,226)', 'rgb(252,197,192)', 'rgb(250,159,181)', 'rgb(247,104,161)', 'rgb(221,52,151)', 'rgb(174,1,126)', 'rgb(122,1,119)'], 8: ['rgb(255,247,243)', 'rgb(253,224,221)', 'rgb(252,197,192)', 'rgb(250,159,181)', 'rgb(247,104,161)', 'rgb(221,52,151)', 'rgb(174,1,126)', 'rgb(122,1,119)'], 9: ['rgb(255,247,243)', 'rgb(253,224,221)', 'rgb(252,197,192)', 'rgb(250,159,181)', 'rgb(247,104,161)', 'rgb(221,52,151)', 'rgb(174,1,126)', 'rgb(122,1,119)', 'rgb(73,0,106)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,1,2,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,1,0,0,0,0] } } , +Greens: {3: ['rgb(229,245,224)', 'rgb(161,217,155)', 'rgb(49,163,84)'], 4: ['rgb(237,248,233)', 'rgb(186,228,179)', 'rgb(116,196,118)', 'rgb(35,139,69)'], 5: ['rgb(237,248,233)', 'rgb(186,228,179)', 'rgb(116,196,118)', 'rgb(49,163,84)', 'rgb(0,109,44)'], 6: ['rgb(237,248,233)', 'rgb(199,233,192)', 'rgb(161,217,155)', 'rgb(116,196,118)', 'rgb(49,163,84)', 'rgb(0,109,44)'], 7: ['rgb(237,248,233)', 'rgb(199,233,192)', 'rgb(161,217,155)', 'rgb(116,196,118)', 'rgb(65,171,93)', 'rgb(35,139,69)', 'rgb(0,90,50)'], 8: ['rgb(247,252,245)', 'rgb(229,245,224)', 'rgb(199,233,192)', 'rgb(161,217,155)', 'rgb(116,196,118)', 'rgb(65,171,93)', 'rgb(35,139,69)', 'rgb(0,90,50)'], 9: ['rgb(247,252,245)', 'rgb(229,245,224)', 'rgb(199,233,192)', 'rgb(161,217,155)', 'rgb(116,196,118)', 'rgb(65,171,93)', 'rgb(35,139,69)', 'rgb(0,109,44)', 'rgb(0,68,27)'], 'properties':{'type': 'seq','blind':[1],'print':[1,0,0,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,2,0,0,0,0,0] } } , +YlGnBu: {3: ['rgb(237,248,177)', 'rgb(127,205,187)', 'rgb(44,127,184)'], 4: ['rgb(255,255,204)', 'rgb(161,218,180)', 'rgb(65,182,196)', 'rgb(34,94,168)'], 5: ['rgb(255,255,204)', 'rgb(161,218,180)', 'rgb(65,182,196)', 'rgb(44,127,184)', 'rgb(37,52,148)'], 6: ['rgb(255,255,204)', 'rgb(199,233,180)', 'rgb(127,205,187)', 'rgb(65,182,196)', 'rgb(44,127,184)', 'rgb(37,52,148)'], 7: ['rgb(255,255,204)', 'rgb(199,233,180)', 'rgb(127,205,187)', 'rgb(65,182,196)', 'rgb(29,145,192)', 'rgb(34,94,168)', 'rgb(12,44,132)'], 8: ['rgb(255,255,217)', 'rgb(237,248,177)', 'rgb(199,233,180)', 'rgb(127,205,187)', 'rgb(65,182,196)', 'rgb(29,145,192)', 'rgb(34,94,168)', 'rgb(12,44,132)'], 9: ['rgb(255,255,217)', 'rgb(237,248,177)', 'rgb(199,233,180)', 'rgb(127,205,187)', 'rgb(65,182,196)', 'rgb(29,145,192)', 'rgb(34,94,168)', 'rgb(37,52,148)', 'rgb(8,29,88)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,1,2,2,2,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,2,0,0,0,0] } } , +Purples: {3: ['rgb(239,237,245)', 'rgb(188,189,220)', 'rgb(117,107,177)'], 4: ['rgb(242,240,247)', 'rgb(203,201,226)', 'rgb(158,154,200)', 'rgb(106,81,163)'], 5: ['rgb(242,240,247)', 'rgb(203,201,226)', 'rgb(158,154,200)', 'rgb(117,107,177)', 'rgb(84,39,143)'], 6: ['rgb(242,240,247)', 'rgb(218,218,235)', 'rgb(188,189,220)', 'rgb(158,154,200)', 'rgb(117,107,177)', 'rgb(84,39,143)'], 7: ['rgb(242,240,247)', 'rgb(218,218,235)', 'rgb(188,189,220)', 'rgb(158,154,200)', 'rgb(128,125,186)', 'rgb(106,81,163)', 'rgb(74,20,134)'], 8: ['rgb(252,251,253)', 'rgb(239,237,245)', 'rgb(218,218,235)', 'rgb(188,189,220)', 'rgb(158,154,200)', 'rgb(128,125,186)', 'rgb(106,81,163)', 'rgb(74,20,134)'], 9: ['rgb(252,251,253)', 'rgb(239,237,245)', 'rgb(218,218,235)', 'rgb(188,189,220)', 'rgb(158,154,200)', 'rgb(128,125,186)', 'rgb(106,81,163)', 'rgb(84,39,143)', 'rgb(63,0,125)'], 'properties':{'type': 'seq','blind':[1],'print':[1,0,0,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,0,0,0,0,0,0] } } , +GnBu: {3: ['rgb(224,243,219)', 'rgb(168,221,181)', 'rgb(67,162,202)'], 4: ['rgb(240,249,232)', 'rgb(186,228,188)', 'rgb(123,204,196)', 'rgb(43,140,190)'], 5: ['rgb(240,249,232)', 'rgb(186,228,188)', 'rgb(123,204,196)', 'rgb(67,162,202)', 'rgb(8,104,172)'], 6: ['rgb(240,249,232)', 'rgb(204,235,197)', 'rgb(168,221,181)', 'rgb(123,204,196)', 'rgb(67,162,202)', 'rgb(8,104,172)'], 7: ['rgb(240,249,232)', 'rgb(204,235,197)', 'rgb(168,221,181)', 'rgb(123,204,196)', 'rgb(78,179,211)', 'rgb(43,140,190)', 'rgb(8,88,158)'], 8: ['rgb(247,252,240)', 'rgb(224,243,219)', 'rgb(204,235,197)', 'rgb(168,221,181)', 'rgb(123,204,196)', 'rgb(78,179,211)', 'rgb(43,140,190)', 'rgb(8,88,158)'], 9: ['rgb(247,252,240)', 'rgb(224,243,219)', 'rgb(204,235,197)', 'rgb(168,221,181)', 'rgb(123,204,196)', 'rgb(78,179,211)', 'rgb(43,140,190)', 'rgb(8,104,172)', 'rgb(8,64,129)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,1,2,2,2,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,2,0,0,0,0] } } , +Greys: {3: ['rgb(240,240,240)', 'rgb(189,189,189)', 'rgb(99,99,99)'], 4: ['rgb(247,247,247)', 'rgb(204,204,204)', 'rgb(150,150,150)', 'rgb(82,82,82)'], 5: ['rgb(247,247,247)', 'rgb(204,204,204)', 'rgb(150,150,150)', 'rgb(99,99,99)', 'rgb(37,37,37)'], 6: ['rgb(247,247,247)', 'rgb(217,217,217)', 'rgb(189,189,189)', 'rgb(150,150,150)', 'rgb(99,99,99)', 'rgb(37,37,37)'], 7: ['rgb(247,247,247)', 'rgb(217,217,217)', 'rgb(189,189,189)', 'rgb(150,150,150)', 'rgb(115,115,115)', 'rgb(82,82,82)', 'rgb(37,37,37)'], 8: ['rgb(255,255,255)', 'rgb(240,240,240)', 'rgb(217,217,217)', 'rgb(189,189,189)', 'rgb(150,150,150)', 'rgb(115,115,115)', 'rgb(82,82,82)', 'rgb(37,37,37)'], 9: ['rgb(255,255,255)', 'rgb(240,240,240)', 'rgb(217,217,217)', 'rgb(189,189,189)', 'rgb(150,150,150)', 'rgb(115,115,115)', 'rgb(82,82,82)', 'rgb(37,37,37)', 'rgb(0,0,0)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,2,0,0,0,0],'copy':[1,0,0,0,0,0,0],'screen':[1,2,0,0,0,0,0] } } , +YlOrRd: {3: ['rgb(255,237,160)', 'rgb(254,178,76)', 'rgb(240,59,32)'], 4: ['rgb(255,255,178)', 'rgb(254,204,92)', 'rgb(253,141,60)', 'rgb(227,26,28)'], 5: ['rgb(255,255,178)', 'rgb(254,204,92)', 'rgb(253,141,60)', 'rgb(240,59,32)', 'rgb(189,0,38)'], 6: ['rgb(255,255,178)', 'rgb(254,217,118)', 'rgb(254,178,76)', 'rgb(253,141,60)', 'rgb(240,59,32)', 'rgb(189,0,38)'], 7: ['rgb(255,255,178)', 'rgb(254,217,118)', 'rgb(254,178,76)', 'rgb(253,141,60)', 'rgb(252,78,42)', 'rgb(227,26,28)', 'rgb(177,0,38)'], 8: ['rgb(255,255,204)', 'rgb(255,237,160)', 'rgb(254,217,118)', 'rgb(254,178,76)', 'rgb(253,141,60)', 'rgb(252,78,42)', 'rgb(227,26,28)', 'rgb(177,0,38)'], 9:['rgb(255,255,204)','rgb(255,237,160)','rgb(254,217,118)','rgb(254,178,76)','rgb(253,141,60)','rgb(252,78,42)','rgb(227,26,28)','rgb(189,0,38)','rgb(128,0,38)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,2,2,0,0,0],'copy':[1,2,2,0,0,0,0],'screen':[1,2,2,0,0,0,0] } } , +PuRd: {3: ['rgb(231,225,239)', 'rgb(201,148,199)', 'rgb(221,28,119)'], 4: ['rgb(241,238,246)', 'rgb(215,181,216)', 'rgb(223,101,176)', 'rgb(206,18,86)'], 5: ['rgb(241,238,246)', 'rgb(215,181,216)', 'rgb(223,101,176)', 'rgb(221,28,119)', 'rgb(152,0,67)'], 6: ['rgb(241,238,246)', 'rgb(212,185,218)', 'rgb(201,148,199)', 'rgb(223,101,176)', 'rgb(221,28,119)', 'rgb(152,0,67)'], 7: ['rgb(241,238,246)', 'rgb(212,185,218)', 'rgb(201,148,199)', 'rgb(223,101,176)', 'rgb(231,41,138)', 'rgb(206,18,86)', 'rgb(145,0,63)'], 8: ['rgb(247,244,249)', 'rgb(231,225,239)', 'rgb(212,185,218)', 'rgb(201,148,199)', 'rgb(223,101,176)', 'rgb(231,41,138)', 'rgb(206,18,86)', 'rgb(145,0,63)'], 9: ['rgb(247,244,249)', 'rgb(231,225,239)', 'rgb(212,185,218)', 'rgb(201,148,199)', 'rgb(223,101,176)', 'rgb(231,41,138)', 'rgb(206,18,86)', 'rgb(152,0,67)', 'rgb(103,0,31)'], 'properties':{'type': 'seq','blind':[1],'print':[1,1,1,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,1,0,0,0,0] } } , +Blues: {3: ['rgb(222,235,247)', 'rgb(158,202,225)', 'rgb(49,130,189)'], 4: ['rgb(239,243,255)', 'rgb(189,215,231)', 'rgb(107,174,214)', 'rgb(33,113,181)'], 5: ['rgb(239,243,255)', 'rgb(189,215,231)', 'rgb(107,174,214)', 'rgb(49,130,189)', 'rgb(8,81,156)'], 6: ['rgb(239,243,255)', 'rgb(198,219,239)', 'rgb(158,202,225)', 'rgb(107,174,214)', 'rgb(49,130,189)', 'rgb(8,81,156)'], 7: ['rgb(239,243,255)', 'rgb(198,219,239)', 'rgb(158,202,225)', 'rgb(107,174,214)', 'rgb(66,146,198)', 'rgb(33,113,181)', 'rgb(8,69,148)'], 8: ['rgb(247,251,255)', 'rgb(222,235,247)', 'rgb(198,219,239)', 'rgb(158,202,225)', 'rgb(107,174,214)', 'rgb(66,146,198)', 'rgb(33,113,181)', 'rgb(8,69,148)'], 9: ['rgb(247,251,255)', 'rgb(222,235,247)', 'rgb(198,219,239)', 'rgb(158,202,225)', 'rgb(107,174,214)', 'rgb(66,146,198)', 'rgb(33,113,181)', 'rgb(8,81,156)', 'rgb(8,48,107)'], 'properties':{'type': 'seq','blind':[1],'print':[1,2,0,0,0,0,0],'copy':[1,0,0,0,0,0,0],'screen':[1,2,0,0,0,0,0] } } , +PuBuGn: {3: ['rgb(236,226,240)', 'rgb(166,189,219)', 'rgb(28,144,153)'], 4: ['rgb(246,239,247)', 'rgb(189,201,225)', 'rgb(103,169,207)', 'rgb(2,129,138)'], 5: ['rgb(246,239,247)', 'rgb(189,201,225)', 'rgb(103,169,207)', 'rgb(28,144,153)', 'rgb(1,108,89)'], 6: ['rgb(246,239,247)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(103,169,207)', 'rgb(28,144,153)', 'rgb(1,108,89)'], 7: ['rgb(246,239,247)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(103,169,207)', 'rgb(54,144,192)', 'rgb(2,129,138)', 'rgb(1,100,80)'], 8: ['rgb(255,247,251)', 'rgb(236,226,240)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(103,169,207)', 'rgb(54,144,192)', 'rgb(2,129,138)', 'rgb(1,100,80)'], 9: ['rgb(255,247,251)', 'rgb(236,226,240)', 'rgb(208,209,230)', 'rgb(166,189,219)', 'rgb(103,169,207)', 'rgb(54,144,192)', 'rgb(2,129,138)', 'rgb(1,108,89)', 'rgb(1,70,54)'], 'properties':{'type': 'seq','blind':[1],'print':[1,2,2,0,0,0,0],'copy':[1,2,0,0,0,0,0],'screen':[1,1,2,0,0,0,0] } } +} + +function color_schemes(n, kind) { + const scheme_names = Object.keys(colorbrewer).filter(name=>{ + const scheme = colorbrewer[name]; + function check(oks) { + return oks.length==1 ? oks[0] : oks[n-3]; + } + return scheme.properties.type==kind && check(scheme.properties.blind)==1 && check(scheme.properties.screen)==1; + }); + const schemes = scheme_names.map(name=>colorbrewer[name][n]); + return schemes; +} + +export { + Obj, Point, Vector, Line, Set, Circle, Conic, Ellipse, Hyperbola, Parabola, + point_line_distance, + orthogonal_projection, parallel_projection, lines_intersection, line_set_intersection, + line_circle_intersection, line_conic_intersection, sets_intersection, circles_intersection, + circle_set_intersection, + TriangleMaker, QuadrilateralMaker, + Drawer, SVGDrawer, + labels, styles, shapes, parts, dirs, arrows, colors, + minimize, gradient, findPhaseChange, + colorbrewer, color_schemes +}; \ No newline at end of file diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..9f89c54 --- /dev/null +++ b/extension.js @@ -0,0 +1,1681 @@ +Numbas.addExtension('eukleides',['math','jme','jme-display'], function(extension) { + + var euk = window.eukleides; + var math = Numbas.math; + var jme = Numbas.jme; + var types = extension.types = {}; + + /** Wrapper to convert Numbas vector (list of numbers) to Eukleides Vector object + */ + function vec(vector) { + return new euk.Vector(vector[0],vector[1]); + } + + /** Wrapper to convert Eukleides Vector object to Numbas vector (list of numbers) + */ + function unvec(vector) { + return [vector.x,vector.y]; + } + + function registerType(constructor,name,casts,display) { + jme.registerType(constructor,name,casts); + if(display) { + jme.display.registerType(constructor,display); + } else { + console.error("no display code for "+name); + } + types[name] = constructor; + } + + + var TAngle = function(angle) { + this.value = angle; + }; + registerType( + TAngle, + 'eukleides_angle', + { + string: function(v) { + return new TString(math.niceNumber(math.precround(math.mod(math.degrees(v.value),360),2))+'°'); + } + }, + { + tex: function(thing,tok,texArgs) { + return this.number(tok.value)+'°'; + }, + jme: function(tree,tok,bits) { + var deg = math.degrees(tok.value); + if(Numbas.util.isInt(deg)) { + return 'deg('+this.number(deg)+')'; + } else { + return 'rad('+this.number(tok.value)+')'; + } + }, + displayString: function(a) { + return math.niceNumber(math.precround(math.mod(math.degrees(a.value),360),2))+'°'.toString(); + } + } + ); + + var TPoint = function(point) { + this.value = point; + }; + registerType( + TPoint, + 'eukleides_point', + {}, + { + tex: function(thing,tok,texArgs) { + return '\\left( '+this.number(tok.value.x)+', '+this.number(tok.value.y, settings)+' \\right)'; + }, + jme: function(tree,tok,bits) { + return 'point('+this.number(tok.value.x)+', '+this.number(tok.value.y)+')'; + }, + displayString: function(p) { + return '('+math.niceNumber(p.value.x)+','+math.niceNumber(p.value.y)+')'; + } + } + ); + + var TLine = function(line) { + this.value = line; + }; + registerType( + TLine, + 'eukleides_line', + {}, + { + jme: function(tree,tok,bits) { + return 'line(point('+this.number(tok.value.x)+','+this.number(tok.value.y)+'),rad('+this.number(tok.value.a)+'))'; + } + } + ); + + var TPointSet = function(point_set) { + this.value = point_set; + }; + registerType( + TPointSet, + 'eukleides_point_set', + { + 'list': function(s) { + return new TList(s.value.points.map(function(p){return new TPoint(p)})); + } + }, + { + tex: function(thing,tok,texArgs) { + return tok.value.points.map(function(p) { return Numbas.jme.display.texify({tok:new TPoint(p)}) }).join(' \\ldots '); + }, + jme: function(tree,tok,bits) { + return tok.value.points.map(function(p) { return Numbas.jme.display.treeToJME({tok: new TPoint(p)}) }).join(' .. '); + }, + displayString: function(l) { + return tok.value.points.map(function(p) { return Numbas.jme.tokenToDisplayString(new TPoint(p)) }).join(' … '); + } + } + ); + + var TCircle = function(circle) { + this.value = circle; + }; + registerType( + TCircle, + 'eukleides_circle', + {}, + { + jme: function(tree,tok,bits) { + return 'circle(point('+this.number(tok.value.x)+','+this.number(tok.value.y)+'),'+this.number(tok.value.r)+')'; + } + } + ); + + var TConic = function(conic) { + this.value = conic; + }; + registerType( + TConic, + 'eukleides_conic', + {}, + { + jme: function(tree,tok,bits) { + var foci = tok.value.foci().map(function(p){ return Numbas.jme.display.treeToJME({tok:new TPoint(p)}) }); + return 'conic('+foci[0]+','+foci[1]+','+this.number(tok.value.a)+')'; + } + } + ); + + var TDrawing = function(objects,style) { + this.value = { + objects: objects || [], + style: style || {} + }; + }; + registerType( + TDrawing, + 'eukleides_drawing', + {}, + {} + ); + + var TAngleLabel = function(a,b,c) { + this.a = a; + this.b = b; + this.c = c; + }; + registerType( + TAngleLabel, + 'eukleides_angle_label', + {}, + { + jme: function(tree,tok,bits) { + var points = [tok.a,tok.b,tok.c].map(function(p){ return Numbas.jme.display.treeToJME({tok: new TPoint(p)}) }); + return 'angle('+points.join(',')+')'; + }, + } + ); + + function drawing_visitor(fn) { + function visit(drawer,drawing,ctx) { + drawer.push_local_settings(); + Object.entries(drawing.style).forEach(function(d) { + if(d[1]!==undefined) { + drawer.local[d[0]] = d[1]; + } + }); + drawing.objects.forEach(function(obj) { + fn(drawer,obj,ctx); + switch(obj.type) { + case 'eukleides_drawing': + visit(drawer,obj.value,ctx); + break; + case 'list': + visit(drawer, {objects:obj.value, style:{}},ctx); + break; + default: + } + }); + drawer.pop_local_settings(); + } + return visit; + } + + var get_point_labels = drawing_visitor(function(drawer,obj) { + switch(obj.type) { + case 'eukleides_point': + if(drawer.local.label) { + drawer.add_point_label(obj.value); + } + break; + } + }); + + var draw_drawing = drawing_visitor(function(drawer,obj,ctx) { + switch(obj.type) { + case 'eukleides_point': + if(drawer.local.label) { + drawer.label_point(obj.value); + } else { + var point = drawer.draw_point(obj.value); + if(ctx && drawer.local.draggable) { + ctx.make_draggable(point, drawer.local.interactive_vars); + } + } + break; + case 'eukleides_point_set': + if(drawer.local.label) { + drawer.label_segment(obj.value.points[0],obj.value.points[1]); + } else if(drawer.local.fill) { + drawer.fill_polygon(obj.value); + } else { + drawer.draw_polygon(obj.value); + } + break; + case 'eukleides_line': + drawer.draw_line(obj.value); + break; + case 'eukleides_circle': + if(drawer.local.fill) { + if(obj.from!==undefined) { + drawer.fill_arc(obj.value,obj.from,obj.to); + } else { + drawer.fill_circle(obj.value); + } + } else { + if(obj.from!==undefined) { + drawer.draw_arc(obj.value,obj.from,obj.to) + } else { + drawer.draw_circle(obj.value); + } + } + break; + case 'eukleides_conic': + if(obj.from!==undefined) { + drawer.draw_conic_arc(obj.value,obj.from,obj.to) + } else { + drawer.draw_conic(obj.value); + } + break; + case 'eukleides_angle_label': + drawer.label_angle(obj.a,obj.b,obj.c); + break; + case 'eukleides_drawing': + case 'list': + break; + default: + throw(new Numbas.Error('Eukleides trying to draw unknown object type: '+obj.type)); + } + }); + + var translate_types = { + 'eukleides_point': function(p,u) { + return new TPoint(p.value.translate(u)); + }, + 'eukleides_line': function(line,u) { + return new TLine(line.value.translate(u)); + }, + 'eukleides_point_set': function(set,u) { + return new TPointSet(set.value.translate(u)); + }, + 'eukleides_circle': function(circle,u) { + var c2 = new TCircle(circle.value.translate(u)); + c2.from = circle.from; + c2.to = circle.to; + return c2; + }, + 'eukleides_conic': function(conic,u) { + return new TConic(conic.value.translate(u)); + }, + 'eukleides_drawing': function(drawing,u) { + return new TDrawing(drawing.value.objects.map(function(x){return translate_object(x,u)}),drawing.value.style); + }, + 'eukleides_angle_label': function(l,u) { + return new TAngleLabel(l.a.translate(u), l.b.translate(u), l.c.translate(u)); + }, + 'list': function(list,u) { + return new TList(list.value.map(function(x){return translate_object(x,u)})); + } + }; + + function translate_object(x,v) { + return translate_types[x.type](x,v); + } + + var funcObj = Numbas.jme.funcObj; + var TString = Numbas.jme.types.TString; + var TNum = Numbas.jme.types.TNum; + var TInt = Numbas.jme.types.TInt; + var TList = Numbas.jme.types.TList; + var TDict = Numbas.jme.types.TDict; + var TBool = Numbas.jme.types.TBool; + var TVector = Numbas.jme.types.TVector; + var TRange = Numbas.jme.types.TRange; + var THTML = Numbas.jme.types.THTML; + + var sig = Numbas.jme.signature; + function named(s,name) { + s.param_name = name; + return s; + } + function spoint(name) { + var s = sig.type('eukleides_point'); + s.param_name = name; + return s; + } + function sangle(name) { + var s = sig.type('eukleides_angle'); + s.param_name = name; + return s; + } + function snum(name) { + var s = sig.type('number'); + s.param_name = name; + return s; + } + function snumorangle(name_num,name_angle) { + return sig.optional(sig.or(snum(name_num),sangle(name_angle))); + } + var sig_eukleides = sig.or.apply(sig,['eukleides_angle','eukleides_point','eukleides_point_set','eukleides_line','eukleides_circle','eukleides_conic','eukleides_angle_label','eukleides_angle','eukleides_drawing'].map(sig.type)); + + function sig_drawing_of(sig) { + + var f = function(args) { + if(args.length==0) { + return false; + } + var d = args[0]; + if(d.type!='eukleides_drawing') { + return false; + } + var items = sig(d.value.objects); + if(items===false || items.length != d.value.objects.length) { + return false; + } else { + return [{type:'eukleides_drawing', items: items}]; + } + } + f.kind = 'eukleides_drawing'; + f.signature = sig; + return f; + } + + extension.scope.setVariable('origin',new TPoint(new euk.Point(0,0))); + + extension.scope.addFunction(new funcObj('degrees',[TAngle],TNum,function(v){return math.degrees(v)}, + {description: 'Convert an angle to a number of degrees.'})); + + var sig_translatable = sig.or.apply(sig,Object.keys(translate_types).map(sig.type)); + extension.scope.addFunction(new funcObj('+',[sig_translatable,TVector],'?',null,{ + evaluate: function(args,scope) { + var x = args[0]; + var v = vec(args[1].value); + return translate_object(x,v); + }, + description: 'Translate an object or list of objects by the given vector.' + })); + extension.scope.addFunction(new funcObj('-',[sig_translatable,TVector],'?',null,{ + evaluate: function(args,scope) { + var x = args[0]; + var v = vec(Numbas.vectormath.negate(args[1].value)); + return translate_object(x,v); + }, + description: 'Translate an object or list of objects by the opposite of the given vector.' + })); + + extension.scope.addFunction(new funcObj('sin',[TAngle],TNum,math.sin), + {description: 'Sine'}); + extension.scope.addFunction(new funcObj('cos',[TAngle],TNum,math.cos), + {description: 'Cosine'}); + extension.scope.addFunction(new funcObj('tan',[TAngle],TNum,math.tan), + {description: 'Tangent'}); + extension.scope.addFunction(new funcObj('cosec',[TAngle],TNum,math.cosec), + {description: 'Cosecant'}); + extension.scope.addFunction(new funcObj('sec',[TAngle],TNum,math.sec), + {description: 'Secant'}); + extension.scope.addFunction(new funcObj('cot',[TAngle],TNum,math.cot), + {description: 'Tangent'}); + + extension.scope.addFunction(new funcObj('+',[TAngle,TAngle],TAngle,math.add, + {description: 'Add two angles.'})); + extension.scope.addFunction(new funcObj('-u',[TAngle],TAngle,math.negate, + {description: 'Flip the direction of the given angle.'})); + extension.scope.addFunction(new funcObj('-',[TAngle,TAngle],TAngle,math.sub, + {description: 'Subtract two angles.'})); + extension.scope.addFunction(new funcObj('*',[TNum,TAngle],TAngle,math.mul, + {description: 'Multiply an angle by the given scale factor.'})); + extension.scope.addFunction(new funcObj('*',[TAngle,TNum],TAngle,math.mul, + {description: 'Multiply an angle by the given scale factor.'})); + extension.scope.addFunction(new funcObj('/',[TAngle,TNum],TAngle,math.div, + {description: 'Divide an angle by the given scale factor.'})); + + extension.scope.addFunction(new funcObj('deg',[TNum],TAngle,function(degrees) { + var rad = math.radians(degrees); + return rad; + },{description: 'Construct an angle in degrees.'})); + + extension.scope.addFunction(new funcObj('rad',[TNum],TAngle,function(radians) { + return radians; + },{description: 'Construct an angle in radians.'})); + + extension.scope.addFunction(new funcObj('point',[TNum,TNum],TPoint,function(x,y) { + return new euk.Point(x,y); + },{description: 'A point at the given coordinates.'})); + + extension.scope.addFunction(new funcObj('point',[TNum,TAngle],TPoint,function(r,a) { + return euk.Point.create_polar(r,a); + },{description: 'A point at the given polar coordinates.'})); + + extension.scope.addFunction(new funcObj('point',[TPointSet,TNum],TPoint,function(set,t) { + return euk.Point.create_point_on_segment(set,t); + },{description: 'A point along the first edge of the given polygon.'})); + + extension.scope.addFunction(new funcObj('point',[TLine,TNum],TPoint,function(line,t) { + return euk.Point.create_point_on_line(line,t); + },{description:'A point on the given line, the given distance away from its origin.'})); + + extension.scope.addFunction(new funcObj('point_with_abscissa',[TLine,TNum],TPoint,function(line,x) { + return euk.Point.create_point_with_abscissa(line,x); + },{description:'A point on the given line with the given abscissa, with respect to the implicit coordinate system.'})); + + extension.scope.addFunction(new funcObj('point_with_ordinate',[TLine,TNum],TPoint,function(line,y) { + return euk.Point.create_point_with_ordinate(line,y); + },{description:'A point on the given line with the given ordinate, with respect to the implicit coordinate system.'})); + + extension.scope.addFunction(new funcObj('point',[TCircle,TAngle],TPoint,function(circle,a) { + return euk.Point.create_point_on_circle(circle,a); + },{description:'A point on the given circle at the given angle.'})); + + extension.scope.addFunction(new funcObj('list',[TPointSet],TList,function(ps){ + return ps.points.map(function(p){return new TPoint(p)}); + },{description: 'Convert a set of points to a list of points.'})); + + extension.scope.addFunction(new funcObj('midpoint',[TPointSet],TPoint,function(set) { + return euk.Point.create_midpoint(set); + },{description:'The midpoint of the given segment.'})); + + extension.scope.addFunction(new funcObj('barycenter',[TPointSet,TList],TPoint,function(points,weights) { + return euk.Point.create_barycenter(points,weights); + },{unwrapValues: true, description:'The barycenter of the given polygon.'})); + + extension.scope.addFunction(new funcObj('orthocenter',[TPoint,TPoint,TPoint],TPoint,function(A,B,C) { + return euk.Point.create_orthocenter(A,B,C); + },{description:'The orthocenter of the given triangle.'})); + + extension.scope.addFunction(new funcObj('reflect',[TPoint,TLine],TPoint,function(p,l) { + return p.reflect(l); + },{description:'Reflect a point in a line.'})); + + extension.scope.addFunction(new funcObj('symmetric',[TPoint,TPoint],TPoint,function(p,origin) { + return p.symmetric(origin); + },{description:'180° rotation of the first point around the second.'})); + + extension.scope.addFunction(new funcObj('rotate',[TPoint,TPoint,TAngle],TPoint,function(p,origin,angle) { + return p.rotate(origin,angle); + },{description:'Rotate the first point the given angle around the second.'})); + + extension.scope.addFunction(new funcObj('distance',[TPoint,TPoint],TNum,function(a,b) { + return a.distance(b); + },{description:'Distance between two points.'})); + + extension.scope.addFunction(new funcObj('homothetic',[TPoint,TPoint,TNum],TPoint,function(p,origin,k) { + return p.homothetic(origin,k); + },{description:'Homothecy (reduction or dilation) of the first point with respect to the second, and the given scale.'})); + + extension.scope.addFunction(new funcObj('x',[TPoint],TNum,function(p) { + return p.abscissa(); + },{description:'x coordinate of a point.'})); + + extension.scope.addFunction(new funcObj('y',[TPoint],TNum,function(p) { + return p.ordinate(); + },{description:'y coordinate of a point.'})); + + extension.scope.addFunction(new funcObj('-',[TPoint,TPoint],TVector,function(a,b) { + return unvec(euk.Vector.create_from_points(b,a)); + },{description:'Vector from the second point\'s position to the first\'s.'})); + + extension.scope.addFunction(new funcObj('vector',[TPointSet],TVector,function(set) { + return unvec(euk.Vector.create_from_segment(set)); + },{description:'Vector from the first point of the polygon to the second.'})); + + extension.scope.addFunction(new funcObj('vector',[TLine],TVector,function(line) { + return unvec(euk.Vector.create_from_line(line)); + },{description:'Unit vector in the direction of the given line.'})); + + extension.scope.addFunction(new funcObj('rotate',[TVector,TAngle],TVector,function(v,a) { + return unvec(vec(c).rotate(a)); + },{description:'Rotate a vector by the given angle.'})); + + extension.scope.addFunction(new funcObj('argument',[TVector],TAngle,function(v) { + return vec(v).argument(); + },{description:'Direction of the given vector.'})); + + extension.scope.addFunction(new funcObj('angle_between',[TVector,TVector],TAngle,function(u,v) { + return euk.Vector.angle_between(vec(u),vec(v)); + },{description:'Angle between two vectors.'})); + + extension.scope.addFunction(new funcObj('line',[TPoint,TAngle],TLine,function(origin,angle) { + return new euk.Line(origin.x,origin.y,angle); + },{description:'A line with the given origin and direction.'})); + + extension.scope.addFunction(new funcObj('line',[TPoint,TPoint],TLine,function(A,B) { + return euk.Line.create_with_points(A,B); + },{description:'A line containing the two given points.'})); + + extension.scope.addFunction(new funcObj('line',[TPoint,TVector],TLine,function(origin,u) { + return euk.Line.create_with_vector(origin,vec(u)); + },{description:'A line with the given origin and direction vector.'})); + + extension.scope.addFunction(new funcObj('line',[TPointSet],TLine,function(set) { + return euk.Line.create_with_segment(set); + },{description:'A line containing the given segment.'})); + + extension.scope.addFunction(new funcObj('parallel',[TLine,TPoint],TLine,function(line,p) { + return line.parallel(p); + },{description:'A line parallel to the given line and containing the given point.'})); + + extension.scope.addFunction(new funcObj('parallel',[TPointSet,TPoint],TLine,function(set,p) { + return euk.Line.create_parallel_to_segment(set,p); + },{description:'A line parallel to the given segment and containing the given point.'})); + + extension.scope.addFunction(new funcObj('perpendicular',[TLine,TPoint],TLine,function(line,p) { + return line.perpendicular(p); + },{description:'A line perpendicular to the given line and containing the given point.'})); + + extension.scope.addFunction(new funcObj('bisector',[TPoint,TPoint,TPoint],TLine,function(A,B,C) { + return euk.Line.create_angle_bisector(A,B,C); + },{description:'The bisector of the angle formed by the given points, and containing the second.'})); + + extension.scope.addFunction(new funcObj('bisector',[TLine,TLine],TLine,function(l1,l2) { + return euk.Line.create_lines_bisector(l1,l2); + },{description:'The bisector of the two given lines.'})); + + extension.scope.addFunction(new funcObj('altitude',[TPoint,TPoint,TPoint],TLine,function(A,B,C) { + return euk.Line.create_altitude(A,B,C); + },{description:'The line containing the first point and perpendicular to the segment between the second and third.'})); + + extension.scope.addFunction(new funcObj('median',[TPoint,TPoint,TPoint],TLine,function(A,B,C) { + return euk.Line.create_median(A,B,C); + },{description:'The line containing the first point and passing through the midpoint of the segment between the second and third.'})); + + extension.scope.addFunction(new funcObj('reflect',[TLine,TPoint],TLine,function(line,p) { + return line.reflect(p); + },{description:'Reflect a line in a point.'})); + + extension.scope.addFunction(new funcObj('symmetric',[TLine,TPoint],TLine,function(line,p) { + return line.symmetric(p); + },{description:'180° degree rotation of a line around the given point.'})); + + extension.scope.addFunction(new funcObj('rotate',[TLine,TPoint,TAngle],TLine,function(line,origin,angle) { + return line.rotate(origin,angle); + },{description:'Rotate a line by the given angle around the given point.'})); + + extension.scope.addFunction(new funcObj('homothetic',[TLine,TPoint,TNum],TLine,function(line,origin,k) { + return line.homothetic(origin,k); + },{description:'Homothecy (reduction or dilation) of a line with respect to the given point and scale factor.'})); + + extension.scope.addFunction(new funcObj('argument',[TLine],TAngle,function(line) { + return line.argument(); + },{description:'Direction angle of the given line.'})); + + extension.scope.addFunction(new funcObj('distance',[TLine,TPoint],TNum,function(l,p) { + return euk.point_line_distance(p,l); + },{description:'Distance between the given line and point.'})); + + extension.scope.addFunction(new funcObj('distance',[TPoint,TLine],TNum,function(p,l) { + return euk.point_line_distance(p,l); + },{description:'Distance between the given point and line.'})); + + extension.scope.addFunction(new funcObj('..',[TPoint,TPoint],TPointSet,function(a,b) { + return new euk.Set([a,b]); + },{description:'A segment between two points.'})); + + extension.scope.addFunction(new funcObj('..',[TPointSet,TPoint],TPointSet,function(set,p) { + return set.add_tail_point(p); + },{description:'Add a point to the end of a polygon.'})); + + extension.scope.addFunction(new funcObj('..',[TPoint,TPointSet],TPointSet,function(p,set) { + return set.add_head_point(p); + },{description:'Add a point to the start of a polygon.'})); + + extension.scope.addFunction(new funcObj('polygon',[sig.listof(sig.type('eukleides_point'))],TPointSet,function(points) { + return new TPointSet(new euk.Set(points)); + },{unwrapValues: true, description:'Construct a polygon from the given list of points.'})) + + extension.scope.addFunction(new funcObj('polygon',[TNum,TPoint,TNum,TAngle],TPointSet,function(n,origin,r,a) { + return euk.Set.create_polygon(n,origin,r,a); + },{description:'A regular polygon with the given number of sides and circumradius, with center at the given point and rotated by the given angle.'})); + + extension.scope.addFunction(new funcObj('segment',[TPointSet,TPoint],TPointSet,function(set,p) { + return set.segment(p); + },{description:'A segment from the first point of the given polygon to the given point.'})); + + extension.scope.addFunction(new funcObj('..',[TPointSet,TPointSet],TPointSet,function(a,b) { + return a.concatenate(b); + },{description:'Concatenate two polygons.'})); + + extension.scope.addFunction(new funcObj('..',[named(sig.type('eukleides_point'),'p1'),named(sig_drawing_of(sig.type('eukleides_point')),'p2')],TDrawing,null,{ + evaluate: function(args,scope) { + var p1 = args[0].value; + var d = args[1].value; + var p2 = d.objects[0].value; + return new TDrawing([new TPointSet(new euk.Set([p1,p2]))],d.style); + }, + description: 'A segment between two points.' + })); + + extension.scope.addFunction(new funcObj('..',[named(sig.type('eukleides_point_set'),'set'),named(sig_drawing_of(sig.type('eukleides_point')),'p')],TDrawing,null,{ + evaluate: function(args,scope) { + var s = args[0].value; + var d = args[1].value; + var p = d.objects[0].value; + return new TDrawing([new TPointSet(s.add_tail_point(p))],d.style); + }, + description: 'Add a point to the end of a polygon.' + })); + + extension.scope.addFunction(new funcObj('..',[named(sig.type('eukleides_point'),'p'),named(sig_drawing_of(sig.type('eukleides_point_set')),'set')],TDrawing,null,{ + evaluate: function(args,scope) { + var p = args[0].value; + var d = args[1].value; + var s = d.objects[0].value; + return new TDrawing([new TPointSet(s.add_head_point(p))],d.style); + }, + description: 'Add a point to the start of a polygon.' + })); + + extension.scope.addFunction(new funcObj('reflect',[TPointSet,TLine],TPointSet,function(set,line) { + return set.reflect(line); + },{description:'Reflect a polygon in the given point.'})); + + extension.scope.addFunction(new funcObj('symmetric',[TPointSet,TPoint],TPointSet,function(set,p) { + return set.symmetric(p); + },{description:'180° degree rotation of the given polygon around the given point.'})); + + extension.scope.addFunction(new funcObj('rotate',[TPointSet,TPoint,TAngle],TPointSet,function(set,origin,a) { + return set.rotate(origin,a); + },{description:'Rotation of a polygon by the given angle around the given point.'})); + + extension.scope.addFunction(new funcObj('cardinality',[TPointSet],TNum,function(set) { + return set.cardinal(); + },{description:'Number of vertices in the given polygon.'})); + + extension.scope.addFunction(new funcObj('perimeter',[TPointSet],TNum,function(set) { + return set.path_length(); + },{description:'Total length of the given polygon\'s edges.'})); + + extension.scope.addFunction(new funcObj('area',[TPointSet],TNum,function(set) { + return set.area(); + },{description:'Area of the given polygon.'})); + + extension.scope.addFunction(new funcObj('perpendicular',[TPointSet,TPoint],TLine,function(set,p) { + return set.perpendicular_to_segment(p); + },{description:'A line perpendicular to the given segment and containing the given point.'})); + + extension.scope.addFunction(new funcObj('perpendicular_bisector',[TPointSet],TLine,function(set) { + return set.perpendicular_bisector(); + },{description:'The perpendicular bisector of the given segment.'})); + + extension.scope.addFunction(new funcObj('center',[TPointSet],TPoint,function(set) { + return set.isobarycenter(); + },{description:'The isobarycenter (centre of gravity) of the given polygon.'})); + + extension.scope.addFunction(new funcObj('isobarycenter',[TPointSet],TPoint,function(set) { + return set.isobarycenter(); + },{description:'The isobarycenter (centre of gravity) of the given polygon.'})); + + extension.scope.addFunction(new funcObj('circle',[TPoint,TNum],TCircle,function(center,r) { + return new euk.Circle(center,r); + },{description:'A circle centered at the given point and with the given radius.'})); + + extension.scope.addFunction(new funcObj('circle',[TPointSet],TCircle,function(set) { + return euk.Circle.create_circle_with_diameter(set); + },{description:'The circle with the given segment as a diameter.'})); + + extension.scope.addFunction(new funcObj('circle',[TPoint,TPoint,TPoint],TCircle,function(A,B,C) { + return euk.Circle.create_circumcircle(A,B,C); + },{description:'The circle through the given points.'})); + + extension.scope.addFunction(new funcObj('incircle',[TPoint,TPoint,TPoint],TCircle,function(A,B,C) { + return euk.Circle.create_incircle(A,B,C); + },{description:'The circle inscribed in the triangle defined by the given points.'})); + + extension.scope.addFunction(new funcObj('center',[TCircle],TPoint,function(circle) { + return circle.center(); + },{description:'The center of the given circle.'})); + + extension.scope.addFunction(new funcObj('tangent',[TCircle,TAngle],TLine,function(circle,a) { + return circle.tangent(a); + },{description:'A line tangent to the given circle at the given heading.'})); + + extension.scope.addFunction(new funcObj('arc',[TCircle,TAngle,TAngle],TCircle,function(circle,from,to) { + var c = new TCircle(circle); + c.from = from; + c.to = to; + return c; + },{unwrapValues:true, description: 'An arc of the given circle between the given angles.'})); + + extension.scope.addFunction(new funcObj('ellipse',[TPoint,TNum,TNum,TAngle],TConic,function(v,a,b,d) { + return new euk.Ellipse(v,a,b,d); + },{description:'An ellipse with the given center, major and minor axis, and rotated by the given angle.'})); + + extension.scope.addFunction(new funcObj('hyperbola',[TPoint,TNum,TNum,TAngle],TConic,function(v,x,y,a) { + return new euk.Hyperbola(v,x,y,a); + },{description:'A hyperbola with the given center, real and imaginary axis, and rotated by the given angle.'})); + + extension.scope.addFunction(new funcObj('parabola',[TPoint,TNum,TAngle],TConic,function(v,a,d) { + return new euk.Parabola(v,a,d); + },{description:'A parabola with the given summit and parameter, rotated by the given angle.'})); + + extension.scope.addFunction(new funcObj('parabola',[TPoint,TLine],TConic,function(A,l) { + return euk.Conic.create_with_directrix(A,l,1); + },{description:'A parabola with the given focus and directrix.'})); + + extension.scope.addFunction(new funcObj('conic',[TPoint,TLine,TNum],TConic,function(A,l,x) { + return euk.Conic.create_with_directrix(A,l,x); + },{description:'A conic with the given focus, directrix and eccentricity.'})); + + extension.scope.addFunction(new funcObj('conic',[TPoint,TPoint,TNum],TConic,function(A,B,a) { + return euk.Conic.create_with_foci(A,B,a); + },{description:'A conic with the given foci and eccentricity.'})); + + extension.scope.addFunction(new funcObj('center',[TConic],TPoint,function(conic) { + return conic.center(); + },{description:'The center of the given conic.'})); + + extension.scope.addFunction(new funcObj('foci',[TConic],TList,function(conic) { + return conic.foci(); + },{unwrapValues:true, description: 'The foci of the given conic.'})); + + extension.scope.addFunction(new funcObj('reflect',[TConic,TLine],TConic,function(conic,line) { + return conic.reflect(line); + },{description:'Reflect a conic in a line.'})); + + extension.scope.addFunction(new funcObj('symmetric',[TConic,TPoint],TConic,function(conic,p) { + return conic.symmetric(p); + },{description:'180° rotation of the given conic around the given point.'})); + + extension.scope.addFunction(new funcObj('rotate',[TConic,TPoint,TAngle],TConic,function(conic,origin,a) { + return conic.rotate(origin,a); + },{description:'Rotate a conic by the given angle around the given point.'})); + + extension.scope.addFunction(new funcObj('homothetic',[TConic,TPoint,TNum],TConic,function(conic,origin,k) { + return conic.homothetic(origin,k); + },{description:'Homothecy (reduction or dilation) of a conic with respect to the given point and scaling factor.'})); + + extension.scope.addFunction(new funcObj('major',[TConic],TNum,function(conic) { + return conic.major_axis(); + },{description:'The major axis of the given conic.'})); + + extension.scope.addFunction(new funcObj('minor',[TConic],TNum,function(conic) { + return conic.minor_axis(); + },{description:'The minor axis of the given conic.'})); + + extension.scope.addFunction(new funcObj('argument',[TConic],TAngle,function(conic) { + return conic.argument(); + },{description:'The direction of the given conic.'})); + + extension.scope.addFunction(new funcObj('point',[TConic,TNum],TPoint,function(conic,t) { + return conic.point_on(t); + },{description:'A point with the given argument on the given conic.'})); + + extension.scope.addFunction(new funcObj('eccentricity',[TConic],TNum,function(conic) { + return conic.eccentricity(); + },{description:'The eccentricity of the given conic.'})); + + extension.scope.addFunction(new funcObj('argument',[TConic,TPoint],TAngle,function(conic,p) { + return conic.point_argument(p); + },{description:'Polar angle of the given point with respect to the center of the given conic.'})); + + extension.scope.addFunction(new funcObj('tangent',[TConic,TNum],TLine,function(conic,t) { + return conic.tangent(t); + },{description:'A line tangent to the given conic at the given argument.'})); + + extension.scope.addFunction(new funcObj('arc',[TConic,TAngle,TAngle],TConic,function(conic,from,to) { + var c = new TConic(conic); + c.from = from; + c.to = to; + return c; + },{unwrapValues:true, description: 'The portion of the given conic between the given arguments.'})); + + function wrap_vertices(vertices) { + return new TList(vertices.map(function(v){ return new TPoint(v); })); + } + + var sig_triangle = sig.or( + sig.sequence(spoint('p1'), spoint('p2'), snumorangle('l2','a2')), + sig.sequence(spoint('p1'), sig.optional(sig.sequence(snum('l1'), sig.optional(snumorangle('l2','a2')))), sig.optional(sangle('orientation'))), + sig.sequence(sig.optional(sig.sequence(snum('l1'), sig.optional(sig.sequence(snumorangle('l2','a2'), snumorangle('l3','a3'))))), sig.optional(sangle('orientation'))) + ); + + function remove_undefined(args) { + return args.filter(function(a) { return a.type!='nothing'; }); + } + + extension.scope.addFunction(new funcObj('triangle',[sig_triangle],TList,null,{ + evaluate: function(args,scope) { + args = remove_undefined(args); + var vertices = []; + // can give up to two vertices + for(var i=0;i<2 && i=args.length) { + x = 6; + } else { + x = args[i].value; + i += 1; + } + } + // can optionally give the two remaining lengths or angles + if(ieukleides must be a dictionary, not "+initial_values.type)); + } + initial_values = initial_values.value; + } + } else { + min_x = scope.evaluate(args[1]).value; + min_y = scope.evaluate(args[2]).value; + max_x = scope.evaluate(args[3]).value; + max_y = scope.evaluate(args[4]).value; + objects = args[5]; + if(args[6]) { + initial_values = scope.evaluate(args[6]); + if(initial_values.type!='dict') { + throw(new Numbas.Error("The final argument to eukleides must be a dictionary, not "+initial_values.type)); + } + initial_values = initial_values.value; + } + } + var svg = create_svg(); + var drawer = new euk.SVGDrawer(svg,document); + + if(min_x!==undefined) { + drawer.setup_frame(min_x,min_y,max_x,max_y,1); + } + + var ctx = new InteractiveContext(drawer,title_tree,objects,scope,initial_values,min_x===undefined); + + if(min_x===undefined) { + var res = find_bounding_box(svg); + drawer.setup_frame(res.min_x,res.min_y,res.max_x,res.max_y,1); + ctx.draw(); + } + + const tok = new THTML(svg); + tok.ctx = ctx; + return tok; + }, + description:'Draw a Eukleides diagram.' + })); + jme.lazyOps.push('eukleides'); + jme.findvarsOps.eukleides = function(tree,boundvars,scope) { + var vars = []; + var initial_values; + var args = tree.args; + if(args.length<=3) { + initial_values = args[2]; + } else { + for(var i=1;i<5;i++) { + vars = vars.concat(jme.findvars(args[i],boundvars,scope)); + } + initial_values = args[6]; + } + if(initial_values) { + vars = vars.concat(jme.findvars(initial_values,boundvars,scope)); + } + return vars; + } + + extension.scope.addFunction(new funcObj('grid',['integer','integer','number','number','lambda'], TList, null, { + evaluate: function(args,scope) { + const [cols,rows,colgap,rowgap] = args.slice(0,4).map(v=>jme.unwrapValue(v)); + const fn = args[4]; + var o = []; + for(let col=0;col { + const from = args[0].value; + const to = args[1].value; + const time = scope.getVariable('time').value; + return new TNum(clamp_frame(from,to,time)); + } + })); + + extension.scope.addFunction(new funcObj('frame_positions', ['number', 'list of number'], TList, null, { + evaluate: (args,scope) => { + const time = args[0].value; + const frame_lengths = jme.unwrapValue(args[1]); + let last = 0; + return jme.wrapValue(frame_lengths.map((l,i) => { + const o = clamp_frame(i>0 ? last : 0, last+l, time); + last += l; + return o; + })); + } + })); + + extension.scope.addFunction(new funcObj('frame_at', ['number', 'list of number'], TInt, null, { + evaluate: (args,scope) => { + let time = args[0].value; + const frame_lengths = jme.unwrapValue(args[1]); + for(let i=0; i + + + + + Eukleides drawing + + + + +
+

Eukleides drawing

+
+
+
+
+ Editing +
    +
+
+ + + +
+
+ +
+
+

Download still

+

+ + + + +

+

Download animation

+

+ + 0 +

+
+
+ +
+ + \ No newline at end of file diff --git a/make_animation.py b/make_animation.py new file mode 100644 index 0000000..93485f3 --- /dev/null +++ b/make_animation.py @@ -0,0 +1,30 @@ +import base64 +from pathlib import Path +import json +import subprocess +import sys + +fps = 5 + +infile = Path(sys.argv[1]) + +with open(infile) as f: + data = json.load(f) + +fps = data['fps'] +duration = data['duration'] +frames = data['frames'] + +print(len(frames)) +for i,frame in enumerate(frames): + print(i) + pngdir = Path('pngs') + pngdir.mkdir(exist_ok=True) + filename = Path(f'frame-{i:04d}.png') + pngname = pngdir / filename + with open(pngname, 'wb') as f: + png = base64.b64decode(frame[len('data:image/png;base64,'):]) + f.write(png) + +outfile = Path(infile.name).with_suffix('.mp4') +subprocess.run(['ffmpeg','-r',str(fps),'-f','image2','-i','pngs/frame-%04d.png','-vcodec','libx264','-crf','25','-pix_fmt','yuv420p','-vf','pad=ceil(iw/2)*2:ceil(ih/2)*2',outfile,'-y']); \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..469a411 --- /dev/null +++ b/script.js @@ -0,0 +1,195 @@ +import './jme-runtime.js'; +import './locales.js'; +import * as euk from './eukleides.mjs'; +import './extension.js'; +import './code-editor.mjs'; + +window.eukleides = euk; + +function toBinary(string) { + const codeUnits = new Uint16Array(string.length); + for (let i = 0; i < codeUnits.length; i++) { + codeUnits[i] = string.charCodeAt(i); + } + return btoa(String.fromCharCode(...new Uint8Array(codeUnits.buffer))); +} + +class Editor { + constructor() { + this.files = []; + this.scope = new Numbas.jme.Scope([Numbas.jme.builtinScope, Numbas.extensions.eukleides.scope]) + this.files = JSON.parse(localStorage.getItem('files') || '{"drawing":""}'); + this.code_editor = document.getElementById('code'); + this.display = document.getElementById('display'); + this.time_input = document.getElementById('time'); + this.time_display = document.querySelector('output[for="time"]'); + this.fps_input = document.getElementById('fps'); + this.width_input = document.getElementById('width'); + this.download_still_link = document.getElementById('download-still'); + this.make_animation_button = document.getElementById('make-animation'); + this.download_animation_link = document.getElementById('download-animation'); + + this.code_editor.addEventListener('change', e => { + const code = this.code_editor.value; + this.save_file(code); + this.render(); + }) + + this.time_input.addEventListener('input', e => { + this.redraw(); + }); + + this.save_as_form = document.getElementById('save-as-form'); + this.save_as_form.addEventListener('submit', e => { + e.preventDefault(); + + this.filename = this.save_as_form.elements['save-as'].value; + this.save_file(this.code_editor.value); + this.make_files_list(); + }) + + this.download_still_link.addEventListener('click', (e) => this.download_still()); + this.make_animation_button.addEventListener('click', (e) => this.make_animation()); + + this.load_file('drawing'); + this.render(); + + } + + make_files_list() { + const ul = document.querySelector('#files ul'); + ul.innerHTML = ''; + for(let name of Object.keys(this.files)) { + const li = document.createElement('li'); + const button = document.createElement('button'); + li.append(button); + if(name == this.filename) { + li.ariaCurrent = 'page'; + } + button.type = 'button'; + button.addEventListener('click', (e) => { + this.load_file(name); + }); + button.textContent = name; + ul.append(li); + } + } + + load_file(name) { + this.filename = name; + this.code_editor.value = this.files[this.filename] || ''; + this.make_files_list(); + document.getElementById('current-file').textContent = this.filename; + } + + save_file(text) { + this.files[this.filename] = text; + localStorage.setItem('files', JSON.stringify(this.files)); + } + + render() { + const code = this.code_editor.value; + try { + const v = this.scope.evaluate(code); + const h = Numbas.jme.castToType(v, 'html'); + this.drawing = h; + h.ctx.get_time = () => this.time_input.valueAsNumber; + h.ctx.draw(); + this.display.innerHTML = ''; + this.display.append(...h.value); + } catch(e) { + this.drawing = null; + this.display.innerHTML = e.message; + console.clear(); + console.error(e); + return; + } + } + + redraw() { + if(!this.drawing) { + return; + } + this.time_display.textContent = this.time_input.value; + this.drawing.ctx.draw(); + } + + async to_canvas() { + console.log('to canvas'); + const {min_x, min_y, max_x, max_y} = this.drawing.ctx.drawer; + + const vw = max_x - min_x; + const vh = max_y - min_y; + + + const width = this.width_input.valueAsNumber; + const height = vh/vw * width; + + const img = document.createElement('img'); + document.body.appendChild(img); + const canvas = new OffscreenCanvas(width, height); + + return new Promise((resolve) => { + // set it as the source of the img element + img.onload = async function() { + console.log('loaded'); + // draw the image onto the canvas + const ctx = canvas.getContext('2d'); + ctx.fillStyle = 'white'; + ctx.fillRect(0,0,width,height); + ctx.drawImage(img, 0, 0, width, height); + const blob = await canvas.convertToBlob(); + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function() { + const base64data = reader.result; + resolve(base64data); + img.parentElement.removeChild(img); + } + } + console.log('setting src'); + img.src = this.to_blob_url(); + }) + } + + to_blob_url() { + const content = '\n' + this.display.innerHTML; + const blob = new Blob([content], {type: 'image/svg+xml'}); + const url = URL.createObjectURL(blob); + return url + } + + async download_still() { + this.download_still_link.download = this.filename+'.svg'; + this.download_still_link.href = this.to_blob_url(); + } + + async make_animation() { + this.download_animation_link.href = ''; + const duration = 10; + const fps = this.fps_input.valueAsNumber; + const frames = []; + for(let i=0; i> frame',i); + this.drawing.ctx.get_time = () => i/fps; + this.drawing.ctx.draw(); + frames.push(await this.to_canvas()) + } + const blob = new Blob([JSON.stringify({fps, duration, frames})], {type: 'application/json'}); + const url = URL.createObjectURL(blob); + this.download_animation_link.download = this.filename+'.json'; + this.download_animation_link.href = url; + } +} + +Numbas.queueScript('base', [], function () {}); +Numbas.runImmediately(['jme'], function () {}); +Numbas.queueScript('base', [], function () {}); +Numbas.queueScript('demo', ['extensions/eukleides/eukleides.js'], function () { + Numbas.activateExtension('eukleides'); + try { + window.editor = new Editor(); + } catch(e) { + console.error(e); + } +}) \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..5c3ba3f --- /dev/null +++ b/style.css @@ -0,0 +1,30 @@ +body { + margin: 0; +} + +main { + display: grid; + grid-template: + "editor display" / 50svw 50svw; + ; + width: 100svw; + + & code-editor { + display: block; + overflow: auto; + } + + & #time { + width: 100%; + } + + & #files { + [aria-current] button { + font-weight: bold; + } + } + + & #display svg { + max-height: 50svh; + } +} \ No newline at end of file