The Pacman map as a quick demo of the Outliner utility .
The path is optimized for drawing with a plotter.
Log in to post a comment.
// LL 2021
const precision = 16; /// min=2 max=100 step=1
const thickness = 3; // min=1 max=3 step=1
const offset_x = 14, offset_y = 15.9;
const scale = 6;
Canvas.setpenopacity(1);
const turtle = new Turtle();
const map=[{r:0.5,p:[[-0.5,22.5],[-0.5,19.5],[2.5,19.5]]},{r:0.5,p:[[-0.5,28.5],[-0.5,25.5],[2.5,25.5]]},{r:0.5,p:[[-0.5,10.5],[-0.5,7.5],[2.5,7.5]]},{r:0.5,p:[[-0.5,4.5],[-0.5,1.5]]},{r:0.25,p:[[-.5,1],[13.2,1],[13.2,10],[8.2,10],[8.2,14],[13.2,14]]},{r:0.25,p:[[-.5,31],[13.2,31],[13.2,20],[8.2,20],[8.2,16],[13.2,16]]},{r:0.5,p:[[2.5,22.5],[5.5,22.5]]},{r:0.5,p:[[5.5,16.5],[5.5,19.5]]},{r:0.5,p:[[5.5,25.5],[5.5,28.5],[2.5,28.5],[10.5,28.5]]},{r:0.5,p:[[11.5,25.5],[12.8,25.5]]},{r:0.5,p:[[8.5,25.5],[8.5,22.5],[10.5,22.5]]},{r:0.5,p:[[5.5,7.5],[5.5,13.5],[5.5,10.5],[2.5,10.5]]},{r:0.5,p:[[8.5,7.5],[10.5,7.5]]},{r:0.2,p:[[1.,13.5],[3,13.5],[3,17],[-0.5,17]]},{r:0.501,p:[[2.5,3.5],[5.5,3.5],[5.5,4.5],[2.5,4.5],[2.5,3.5]]},{r:0.501,p:[[8.5,3.5],[10.5,3.5],[10.5,4.5],[8.5,4.5],[8.5,3.5]]}];
const food=[[[1,2],[12,2]],[[1,3],[1,5]],[[7,3],[7,5]],[[12,3]],[[12,5]],[[12,22]],[[1,22],[1,23]],[[5,15],[7,15]],[[0,6],[12,6]],[[4,7],[4,9]],[[1,9],[3,9]],[[1,10],[1,11]],[[0,12],[4,12]],[[7,7],[7,27]],[[12,7],[12,9]],[[8,9],[11,9]],[[4,13],[4,20]],[[1,21],[6,21]],[[8,21],[12,21]],[[10,24],[12,24]],[[8,27],[12,27]],[[12,28],[12,29]],[[0,30],[12,30]],[[0,24],[6,24]],[[4,25],[4,27]],[[1,27],[3,27]],[[1,28],[1,29]],[[10,25],[10,27]],[[0,18],[3,18]],];
const powerup=[[12,4],[12,23]];
var outliner;
console.clear();
function transform(point, dir) {
return [ (14 + dir * (point[0]+0.5) - offset_x) * scale, (point[1] - offset_y) * scale];
}
function walk(i, t) {
if (i==0) {
outliner = new Outliner(turtle, precision, thickness);
map.forEach(section => {
for (var dir=-1; dir<=1; dir+=2) {
for (var j=0; j<section.p.length-1; j++) {
const point1 = transform(section.p[j], dir);
const point2 = transform(section.p[j+1], dir);
const p1 = outliner.addPoint(point1, section.r*scale);
const p2 = outliner.addPoint(point2, section.r*scale);
outliner.addSegment(p1, p2);
}
}
});
food.forEach(group => {
for (var dir=-1; dir<=1; dir+=2) {
var x = group[0][0], y = group[0][1];
while (y <= group[group.length-1][1]) {
while (x <= group[group.length-1][0]) {
const point = transform([x, y], dir);
outliner.addPoint(point, 0.15*scale);
x++;
}
y++;
x = group[0][0];
}
}
});
powerup.forEach(p => {
for (var dir=-1; dir<=1; dir+=2) {
const point = transform(p, dir);
outliner.addPoint(point, 0.4*scale);
}
});
outliner.build();
}
// 5. Draw a little bit of the outlines in a loop until there is no more to draw
if (!outliner.drawNext()) return false;
return true;
}
/////////////////////////////////////////////////////////////////
// Outliner utility code - Created by Lionel Lemarie 2021
// https://turtletoy.net/turtle/a206296e80
/////////////////////////////////////////////////////////////////
function Outliner(tt_=turtle, precision_=20, thickness_=1) {
const timers = {}
const timer_list = [];
function startTimer(name) {
const now = performance.now();
if (!(name in timers)) {
timers[name] = [1, -now];
timer_list.push(name);
} else {
timers[name][0]++;
timers[name][1]-=now;
}
}
function stopTimer(name) {
const now = performance.now();
if (name in timers) {
timers[name][1]+=now;
}
}
function printTimers() {
function numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }
timer_list.forEach(name => {
var elapsed = timers[name][1], count = timers[name][0], unit = "ms";
if (elapsed > 1000) { unit = "sec"; elapsed /= 1000; if (elapsed > 60) { unit = "min"; elapsed /= 60; } }
console.log(`Outliner: ${name} - calls: ${numberWithCommas(count)} - total: ${elapsed.toFixed(1)} ${unit}`);
});
}
startTimer("Total time");
const lines_to_draw = [];
const figure_points = [];
const figure_segments = [];
const epsilon = 0.0001;
function createPointOutline(point) {
const step = 1 / precision_;
for (var a=0; a<1; a+=step) {
const x0 = point[0] + point[2] * Math.cos(a * Math.PI * 2);
const y0 = point[1] + point[2] * Math.sin(a * Math.PI * 2);
const x1 = point[0] + point[2] * Math.cos((a+step) * Math.PI * 2);
const y1 = point[1] + point[2] * Math.sin((a+step) * Math.PI * 2);
addLine([ [x0, y0], [x1, y1] ]);
}
}
function rotX(x, y, a) { return Math.cos(a) * x - Math.sin(a) * y; }
function rotY(x, y, a) { return Math.sin(a) * x + Math.cos(a) * y; }
function createSegmentOutline(segment) {
const l = Math.hypot(segment[1][0]-segment[0][0], segment[1][1]-segment[0][1]);
if (l < epsilon) return [];
var px11 = (segment[0][0] + segment[0][2] * rotX(segment[0][0] - segment[1][0], segment[0][1] - segment[1][1], -0.5 * Math.PI) / l);
var py11 = (segment[0][1] + segment[0][2] * rotY(segment[0][0] - segment[1][0], segment[0][1] - segment[1][1], -0.5 * Math.PI) / l);
var px12 = (segment[0][0] + segment[0][2] * rotX(segment[0][0] - segment[1][0], segment[0][1] - segment[1][1], 0.5 * Math.PI) / l);
var py12 = (segment[0][1] + segment[0][2] * rotY(segment[0][0] - segment[1][0], segment[0][1] - segment[1][1], 0.5 * Math.PI) / l);
var px21 = (segment[1][0] + segment[1][2] * rotX(segment[1][0] - segment[0][0], segment[1][1] - segment[0][1], -0.5 * Math.PI) / l);
var py21 = (segment[1][1] + segment[1][2] * rotY(segment[1][0] - segment[0][0], segment[1][1] - segment[0][1], -0.5 * Math.PI) / l);
var px22 = (segment[1][0] + segment[1][2] * rotX(segment[1][0] - segment[0][0], segment[1][1] - segment[0][1], 0.5 * Math.PI) / l);
var py22 = (segment[1][1] + segment[1][2] * rotY(segment[1][0] - segment[0][0], segment[1][1] - segment[0][1], 0.5 * Math.PI) / l);
addSubDivLine([[px11, py11], [px22, py22]]);
addSubDivLine([[px12, py12], [px21, py21]]);
}
function addSubDivLine(line) {
const step = 1 / precision_;
for (t=0; t<(1-epsilon); t+=step) {
const x1 = line[0][0] + (line[1][0]-line[0][0]) * t;
const y1 = line[0][1] + (line[1][1]-line[0][1]) * t;
const x2 = line[0][0] + (line[1][0]-line[0][0]) * (t+step);
const y2 = line[0][1] + (line[1][1]-line[0][1]) * (t+step);
addLine([[x1, y1], [x2, y2]]);
}
}
const binSize = 1;
const bins = {};
function point_key(point) {
return Math.floor(point[1]/binSize) * 100000 + Math.floor(point[0]/binSize);
}
function getBinListForPoint(point) {
var bin_list = [];
const key0 = point_key(point);
if (key0 in bins) bin_list.push(bins[key0]);
var found_good = false;
bin_list.forEach(bin => bin.forEach(index => found_good |= !inBlacklist(index)));
if (!found_good) {
bin_list = [];
const search_radius = 2;
for (var y=-search_radius; y<=search_radius+0.1; y+=1) {
for (var x=-search_radius; x<=search_radius+0.1; x+=1) {
if (x==0 && y==0) continue;
const key = point_key([point[0]+x*binSize, point[1]+y*binSize]);
if (key in bins) bin_list.push(bins[key]);
}
}
}
return bin_list;
}
function getBinListForLine(line) {
const bin_list = [];
const key0 = point_key(line[0]);
if (key0 in bins) bin_list.push(bins[key0]);
const key1 = point_key(line[1]);
if (key1 in bins) bin_list.push(bins[key1]);
return bin_list;
}
function addToBin(line, index) {
for (var i=0; i<2; i++) {
const key = point_key(line[i]);
if (!(key in bins)) bins[key] = [];
bins[key].push(index);
}
}
function checkOverlap(line1, line2) {
const distance00 = distance2(line1[0], line2[0]);
const distance01 = distance2(line1[0], line2[1]);
const distance10 = distance2(line1[1], line2[0]);
const distance11 = distance2(line1[1], line2[1]);
const value = ! ( ((distance00 > epsilon) || (distance11 > epsilon)) && ((distance01 > epsilon) || (distance10 > epsilon)) );
return value;
}
function addLine(line) {
startTimer("addLine");
var ok_to_add = true;
startTimer(`addLine - figure_points ${figure_points.length}`);
for (var i=0; i<figure_points.length && ok_to_add; i++) {
const dist0 = distance2(line[0], figure_points[i]) * (1+epsilon);
const dist1 = distance2(line[1], figure_points[i]) * (1+epsilon);
ok_to_add &= dist0 > (figure_points[i][2]**2);
ok_to_add &= dist1 > (figure_points[i][2]**2);
}
stopTimer(`addLine - figure_points ${figure_points.length}`);
startTimer(`addLine - figure_segments ${figure_segments.length}`);
for (var i=0; i<figure_segments.length && ok_to_add; i++) {
const segment = [0,1].map(si => figure_points[figure_segments[i][si]]);
ok_to_add &= !pointInSegment(line[0], segment) && !pointInSegment(line[1], segment);
}
stopTimer(`addLine - figure_segments ${figure_segments.length}`);
getBinListForLine(line).forEach(bin => {
for (var i=0; i<bin.length && ok_to_add; i++) {
ok_to_add &= !checkOverlap(line, lines_to_draw[bin[i]]);
}
});
if (ok_to_add) {
addToBin(line, lines_to_draw.length);
lines_to_draw.push(line);
}
stopTimer("addLine");
}
function distance2(point1, point2) {
const dist2 = (point1[0]-point2[0]) * (point1[0]-point2[0]) + (point1[1]-point2[1]) * (point1[1]-point2[1]);
return dist2;
}
function distanceToSegmentFactor(point, segment) {
var l2 = distance2(segment[0], segment[1]);
if (l2 < epsilon) return 0;
var t = ((point[0] - segment[0][0]) * (segment[1][0] - segment[0][0]) + (point[1] - segment[0][1]) * (segment[1][1] - segment[0][1])) / l2;
t = Math.max(0, Math.min(1, t));
return t;
}
function distance2ToSegmentWithFactor(point, segment, t) {
const dist2 = distance2(point, [segment[0][0] + t * (segment[1][0] - segment[0][0]), segment[0][1] + t * (segment[1][1] - segment[0][1])]);
return dist2;
}
function pointInSegment(point, segment) {
const t = distanceToSegmentFactor(point, segment);
const radius = segment[0][2] * (1-t) + segment[1][2] * t;
const inside = distance2ToSegmentWithFactor(point, segment, t) < radius*radius*(1-epsilon);
return inside;
}
function fillGaps() {
const dangling_points = [];
for (var i1=0; i1<lines_to_draw.length; i1++) {
for (var j1=0; j1<2; j1++) {
var found_close = false;
getBinListForPoint(lines_to_draw[i1][j1]).forEach(bin => {
var closest_point = [];
var closest_distance = NaN;
for (var i2=0; i2<bin.length; i2++) {
if (checkOverlap(lines_to_draw[i1], lines_to_draw[bin[i2]])) continue;
for (var j2=0; j2<2; j2++) {
const dist2 = distance2(lines_to_draw[i1][j1], lines_to_draw[bin[i2]][j2]);
if (isNaN(closest_distance) || dist2 < closest_distance) {
closest_distance = dist2;
closest_point = lines_to_draw[bin[i2]][j2];
}
}
}
if (!isNaN(closest_distance) && closest_distance < epsilon) {
found_close = true;
}
});
if (!found_close) dangling_points.push(lines_to_draw[i1][j1]);
}
}
const extra_lines = [];
while (dangling_points.length > 1) {
const point1 = dangling_points.shift();
var closest_index = -1;
var closest_distance = NaN;
for (var i=0; i<dangling_points.length; i++) {
const dist2 = distance2(dangling_points[i], point1);
if (closest_index < 0 || dist2 < closest_distance) {
closest_distance = dist2;
closest_index = i;
}
}
if (closest_index >= 0 && closest_distance < 30) {
const point2 = dangling_points[closest_index];
dangling_points.splice(closest_index, 1);
extra_lines.push([point1, point2]);
}
}
extra_lines.forEach(l => { addToBin(l, lines_to_draw.length); lines_to_draw.push(l); });
}
const blacklist = [];
var blacklist_count = 0;
function inBlacklist(index) {
while (blacklist.length <= index) blacklist.push(false);
const value = blacklist[index];
return value;
}
function addToBlacklist(index) {
while (blacklist.length <= index) blacklist.push(false);
blacklist[index] = true;
blacklist_count++;
}
function drawNextLine() {
startTimer("drawNextLine");
if (lines_to_draw.length == 0) return false;
const position = tt_.position();
var closest_distance = 1000000;
var closest_line_index = -1;
var closest_point_index = -1;
startTimer("drawNextLine - bins");
getBinListForPoint(position).forEach(bin => {
for (var i=0; i<bin.length; i++) {
if (inBlacklist(bin[i])) continue;
for (var j=0; j<2; j++) {
const dist2 = distance2(position, lines_to_draw[bin[i]][j]);
if (closest_line_index < 0 || dist2 < closest_distance) {
closest_distance = dist2;
closest_line_index = bin[i];
closest_point_index = j;
}
}
}
});
stopTimer("drawNextLine - bins");
if (closest_line_index < 0) {
startTimer("drawNextLine - all");
for (var i=0; i<lines_to_draw.length && closest_distance > 1; i++) {
if (inBlacklist(i)) continue;
for (var j=0; j<2; j++) {
const dist2 = distance2(position, lines_to_draw[i][j]);
if (closest_line_index < 0 || dist2 < closest_distance) {
closest_distance = dist2;
closest_line_index = i;
closest_point_index = j;
}
}
}
stopTimer("drawNextLine - all");
}
if (closest_line_index < 0) {
startTimer("drawNextLine - last restort");
closest_line_index = 0;
closest_point_index = 0;
closest_distance = 1;
while (inBlacklist(closest_line_index)) closest_line_index++;
stopTimer("drawNextLine - last restort");
}
if (closest_distance > 0.21) {
tt_.jump(lines_to_draw[closest_line_index][closest_point_index]);
}
tt_.down();
tt_.goto(lines_to_draw[closest_line_index][1-closest_point_index]);
addToBlacklist(closest_line_index);
stopTimer("drawNextLine");
const more_to_do = lines_to_draw.length - blacklist_count > 0;
if (!more_to_do) {
stopTimer("Total time");
printTimers();
}
return more_to_do;
}
function applyThickness() {
const original_count = lines_to_draw.length;
const offset = 0.2;
for (var ty=0; ty<thickness; ty++) {
for (var tx=0; tx<thickness; tx++) {
if (tx==0 && ty==0) continue;
for (var i=0; i<original_count; i++) {
const line = lines_to_draw[i].map(p => [p[0]-tx*offset, p[1]-ty*offset, p[2]]);
addToBin(line, lines_to_draw.length);
lines_to_draw.push(line);
}
}
}
}
function buildOutline() {
startTimer("buildOutline");
for (var i=0; i<figure_points.length; i++) {
createPointOutline(figure_points[i]);
}
for (var i=0; i<figure_segments.length; i++) {
const segment = [0,1].map(si => figure_points[figure_segments[i][si]]);
createSegmentOutline(segment);
}
fillGaps();
applyThickness();
stopTimer("buildOutline");
}
function newPoint(point, radius) {
figure_points.push([point[0], point[1], radius]);
return figure_points.length - 1;
}
function newSegment(index1, index2) {
figure_segments.push([index1, index2]);
return figure_segments.length - 1;
}
return {
addPoint: (point, radius) => newPoint(point, radius),
addSegment: (index1, index2) => newSegment(index1, index2),
addPoints: (point_list) => point_list.forEach(p => newPoint(p[0], p[1])),
addSegments: (segment_list) => segment_list.forEach(s => newSegment(s[0], s[1])),
build: () => buildOutline(),
drawNext: () => drawNextLine(),
unsortedLines: lines_to_draw
};
}