Public
Edited
Jan 29, 2023
Importers
Insert cell
Insert cell
Insert cell
wwshowPlan(plan)
Insert cell
plan = {
const p = new GalleryPlan(200, 200, 1);
const a = 200;
const s = a / (Math.sqrt(2) + 1) * Math.sqrt(2) / 2;
const eps = 0.1;
p.jumpTo(0, s);
p.wallTo(s, 0);
p.wallTo(a - s, 0);
p.wallTo(a, s);
p.wallTo(a, a - s);
p.wallTo(a - s, a);
p.wallTo(s, a);
p.wallTo(0, a - s);
p.wallTo(0, s);
const k = 0.33;
p.jumpTo(a * k, a * k);
p.wallTo(a - a * k, a * k);
p.jumpTo(a / 2, a * k - eps);
p.put(-90, '9 ', {});
p.jumpTo(a / 2, a * k + eps);
p.put(90, ' 10', {});
p.jumpTo(s / 2 + eps, s / 2 + eps);
p.put(45, '1', {});
p.jumpTo(a - s / 2 - eps, s / 2 + eps);
p.put(180 - 45, '3', {});
p.jumpTo(s / 2 + eps, a - s / 2 - eps);
p.put(-45, '7', {});
p.jumpTo(a - s / 2 - eps, a - s / 2 - eps);
p.put(180 + 45, '5', {});
p.jumpTo(a / 2, eps);
p.put(90, '2', {});
p.jumpTo(a / 2, a - eps);
p.put(-90, '6', {});
p.jumpTo(a - eps, a / 2);
p.put(180, '4', {});
p.jumpTo(eps, a / 2);
p.put(0, '8', {});
p.jumpTo(a / 2, a * 3 / 4);
p.put(-90, '3d', {}, {}, 10);
p.jumpTo(a / 2, a / 2);
return p;
}
Insert cell
function showPlan(plan) {
plan.mountControls();
invalidation.then(() => plan.unmountControls());
return plan.draw();
}
Insert cell
class GalleryPlan {
constructor(width, height, scale) {
this.width = width;
this.height = height;
this.scale = scale;
this.walls = [];
this.objects = [];
this.wallAttrs = {};
this.minimalDistanceToWall = 0.1;
this.curPos = [0, 0];
}
jumpTo(x, y) {
this.curPos = [x, y];
}
wallTo(x, y, wallAttrs=null) {
this.walls.push({
x1: this.curPos[0],
y1: this.curPos[1],
x2: x,
y2: y,
attrs: {...this.wallAttrs, ...wallAttrs},
});
this.curPos = [x, y];
}
tryMove(dx, dy) {
const N = 10;
let waltThroughFired = false;
for (let i=0; i<=N; i++) {
const [sx, sy] = this.curPos;
const [tx, ty] = [sx + dx / N, sy + dy / N];
for (const {x1, y1, x2, y2, attrs} of this.walls) {
const dist = ptToSegDist(tx, ty, x1, y1, x2, y2);
const int = intersect(sx, sy, tx, ty, x1, y1, x2, y2);
if (attrs.canWalkThrough) {
if (attrs.onWalkThrough && int && !waltThroughFired) {
attrs.onWalkThrough();
waltThroughFired = true;
}
} else if (dist < this.minimalDistanceToWall || int) {
return false;
}
}
for (const {x, y, r, attrs} of this.objects) {
const dist = ((x - tx) ** 2 + (y - ty) ** 2) ** 0.5;
if (dist < r) {
return false;
}
}
this.curPos = [tx, ty];
}
}
put(dir, name, constructor, attrs, r=0) {
this.objects.push({
x: this.curPos[0],
y: this.curPos[1],
r,
dir,
name,
constructor,
attrs,
});
}
onKeyDown = e => {
const delta = 10;
if (e.key === 'a') {
this.tryMove(-delta, 0);
} else if (e.key === 'd') {
this.tryMove(+delta, 0);
} else if (e.key === 'w') {
this.tryMove(0, -delta);
} else if (e.key === 's') {
this.tryMove(0, +delta);
}
this.draw();
}
mountControls() {
window.addEventListener('keydown', this.onKeyDown);
}
unmountControls() {
window.removeEventListener('keydown', this.onKeyDown);
}
draw() {
if (!this.ctx) {
this.ctx = DOM.context2d(this.width * 1.2 * this.scale, this.height * 1.2 * this.scale);
}
const {ctx} = this;
ctx.save();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.translate(this.width * 0.1 * this.scale, this.height * 0.1 * this.scale);
ctx.scale(this.scale, this.scale);
ctx.fillStyle = 'black';
ctx.strokeStyle = 'black';
ctx.lineWidth = 1 / this.scale;
this.walls.forEach(({x1, y1, x2, y2, attrs}) => {
ctx.strokeStyle = attrs.canWalkThrough ? 'red' : 'black';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
});
this.objects.forEach(({x, y, r, dir, name}) => {
ctx.fillStyle = 'green';
ctx.strokeStyle = 'green';
ctx.fillRect(x - 2 / this.scale, y - 2 / this.scale, 4 / this.scale, 4 / this.scale);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.arc(x, y, r / this.scale, 0, Math.PI * 2);
ctx.stroke();
const a = dir / 180 * Math.PI;
const rr = 10 / this.scale;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + rr * Math.cos(a), y + rr * Math.sin(a));
ctx.stroke();
ctx.font = `${10 / this.scale}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.strokeStyle = 'white';
ctx.lineWidth = 3 / this.scale;
ctx.strokeText(name, x, y - 2 / this.scale);
ctx.fillText(name, x, y - 2 / this.scale);
ctx.lineWidth = 1 / this.scale;
});
const [x, y] = this.curPos;
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(x, y, 2 / this.scale, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
return ctx.canvas;
}
}
Insert cell
function wallsTo3d(plan, {height, topAttrs, bottomAttrs}) {
return [
new DivPlane({
width: plan.width,
height: plan.height,
transform: `rotateX(90deg)`,
position: [plan.width / 2, height / 2, plan.height / 2],
...topAttrs,
}),
new DivPlane({
width: plan.width,
height: plan.height,
transform: `rotateX(90deg)`,
position: [plan.width / 2, -height / 2, plan.height / 2],
...bottomAttrs,
}),
...plan.walls.map(({x1, y1, x2, y2, attrs}) => {
const width = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5;
const a = -Math.atan2(y2 - y1, x2 - x1);
return new DivPlane({
width,
height,
transform: [
`rotateY(${a / Math.PI * 180}deg)`
].join(' '),
position: [(x1 + x2) / 2, 0, (y1 + y2) / 2],
...attrs,
});
}),
...plan.objects.map(({x, y, r, dir, name, constructor, attrs}) => {
return new constructor({
...attrs,
position: [x, -3, y],
transform: `rotateY(${90-dir}deg)`,
});
}),
];
}
Insert cell
// line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/
// Determine the intersection point of two line segments
// Return FALSE if the lines don't intersect
function intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return false;
}
const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
if (denominator === 0) {
return false;
}
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return false;
}
const x = x1 + ua * (x2 - x1);
const y = y1 + ua * (y2 - y1);
return {x, y}
}
Insert cell
// Distance from point to segment
// https://stackoverflow.com/a/6853926/6794086
function ptToSegDist(x, y, x1, y1, x2, y2) {
const A = x - x1;
const B = y - y1;
const C = x2 - x1;
const D = y2 - y1;
const dot = A * C + B * D;
const len_sq = C * C + D * D;
let param = -1;
if (len_sq != 0) param = dot / len_sq;
let xx, yy;
if (param < 0) {
xx = x1;
yy = y1;
} else if (param > 1) {
xx = x2;
yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
const dx = x - xx;
const dy = y - yy;
return Math.sqrt(dx * dx + dy * dy);
}
Insert cell
import {DivPlane} from 'a9a6ee48aa1de1bc';
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more