Readme

ASCII NYC Traffic Cameras

All of NYC's traffic cameras available as streaming ASCII images: https://maxm-asciinyccameras.web.val.run/

NYC has a bunch of traffic cameras and makes them available through static images like this one. If you refresh the page you'll see the image update every 2 seconds or so. I thought it might be fun to make these cameras viewable as an ASCII art video feed. I made a small library that takes most of its logic from this repo. You can see a basic example of how to convert any image to ASCII here.

I pull in NYC GeoJSON from here and then hook up a Server-Sent Events endpoint to stream the ASCII updates to the browser. (Polling would work just as well, I've just been on a bit of a SSE kick lately.)

Hilariously (and expectedly) The ASCII representation is about 4x the size of the the source jpeg and harder to see, but it has a retro-nostalgia look to it that is cool to me :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "npm:react-dom/server";
import valTownBadge from "https://esm.town/v/jxnblk/valTownBadge?v=16";
import { imageToAscii } from "https://esm.town/v/maxm/imageToAscii";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=29";
import { Hono } from "npm:hono@3";
const info = extractValInfo(import.meta.url);
const app = new Hono();
app.get("/", async (c) => {
const frontendJS = () => {
const width = 600;
const height = 600;
// Create SVG container
const svg = d3.select("#map")
.attr("width", width)
.attr("height", height);
// Define projection and path generator
const projection = d3.geoMercator()
.scale(50000)
.center([-74.036242, 40.700610]) // Center on NYC
.translate([width / 2, height / 2]);
const path = d3.geoPath().projection(projection);
let interval: number | undefined;
const showCamera = async (id: string) => {
document.location.hash = id;
document.getElementById("info-frame").src = "/camera/" + id;
document.getElementById("message").style.display = "none";
document.getElementById("close-btn").style.display = "flex";
await fetchImage(id);
};
const hideCamera = () => {
document.getElementById("message").style.display = "flex";
document.getElementById("close-btn").style.display = "none";
document.getElementById("info-frame").src = "";
document.location.hash = "";
};
if (document.location.hash) {
showCamera(document.location.hash.slice(1));
}
document.getElementById("close-btn").addEventListener("click", hideCamera);
// Load and render GeoJSON data
d3.json("/nyc.geojson").then(function(data) {
svg.selectAll("path")
.data(data.features)
.enter().append("path")
.attr("d", path)
.attr("fill", "white")
.attr("stroke", "black");
// Add points
svg.selectAll("circle")
.data(points)
.enter().append("circle")
.attr("cx", d => projection([d.lng, d.lat])[0])
.attr("cy", d => projection([d.lng, d.lat])[1])
.attr("r", 5)
.attr("class", "link")
.on("click", function(event, d) {
showCamera(d.id);
})
.on("mouseover", function(event, d) {
d3.select(this).transition()
.duration(200)
.attr("r", 10)
.attr("opacity", 1);
d3.select(this).raise();
})
.on("mouseout", function(event, d) {
d3.select(this).transition()
.duration(200)
.attr("r", 5)
.attr("opacity", 0.6);
});
});
};
let resp = await fetch("https://webcams.nyctmc.org/cameras/graphql", {
"headers": {
"content-type": "application/json",
},
"body": JSON.stringify({
query: `query { cameras {
id
lat: latitude
lng: longitude
isOnline
}}`,
}),
"method": "POST",
});
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
maxm-asciinyccameras.web.val.run
v249
June 10, 2024