Skip to content

Instantly share code, notes, and snippets.

@saharan
Created February 26, 2022 11:09
Show Gist options
  • Save saharan/9ff36be4db19aeccfd6e39732cf32941 to your computer and use it in GitHub Desktop.
Save saharan/9ff36be4db19aeccfd6e39732cf32941 to your computer and use it in GitHub Desktop.
a tiny visualization library
#include <random>
// ------------------------------------ LIBRARY BEGIN ------------------------------------
#include <fstream>
#include <sstream>
#include <string>
namespace svg {
using namespace std;
string s(double x) {
return to_string(x);
}
string rgb(double r, double g, double b) {
return "rgb(" + s(lround(r * 255)) + "," + s(lround(g * 255)) + "," + s(lround(b * 255)) + ")";
}
string p(string name, double val) {
return name + "=\"" + s(val) + "\" ";
}
string p(string name, string val) {
return name + "=\"" + val + "\" ";
}
string tag(string name, string props) {
return "<" + name + " " + props + "/>";
}
string tag(string name, string props, string content) {
return "<" + name + " " + props + ">" + content + "</" + name + ">";
}
class Style {
public:
double sr, sg, sb, sa; // stroke
double fr, fg, fb, fa; // fill
double e00, e01, e02, e10, e11, e12; // affine matrix
int textAlign;
double fs;
double sw;
string tf;
Style() : sr(0), sg(0), sb(0), sa(1), fr(1), fg(1), fb(1), fa(1), e00(1), e01(0), e02(0), e10(0), e11(1), e12(0), textAlign(0), fs(16), sw(1), tf() {
}
string stroke() const {
return sa <= 0 ? "" : p("stroke", rgb(sr, sg, sb)) + p("stroke-opacity", sa) + p("stroke-width", sw);
}
string fill() const {
return fa <= 0 ? p("fill", "none") : p("fill", rgb(fr, fg, fb)) + p("fill-opacity", fa);
}
string textAnchor() const {
return p("text-anchor", textAlign < 0 ? "left" : textAlign > 0 ? "right" : "middle");
}
string transform() const {
return tf.empty() ? "" : p("transform", tf);
}
string font() const {
return p("font-size", fs);
}
};
class Graphics {
public:
double screenW;
double screenH;
string data;
Graphics() : screenW(100), screenH(100) {
}
void screen(double width, double height) {
screenW = width;
screenH = height;
}
void clear() {
data = "";
st = Style();
}
void save() {
stack.push_back(st);
}
void restore() {
st = stack.back();
stack.pop_back();
}
void stroke(double r, double g, double b, double a = 1) {
st.sr = r;
st.sg = g;
st.sb = b;
st.sa = a;
}
void noStroke() {
stroke(0, 0, 0, 0);
}
void strokeWidth(double w) {
st.sw = w;
}
void fontSize(double size) {
st.fs = size;
}
void fill(double r, double g, double b, double a = 1) {
st.fr = r;
st.fg = g;
st.fb = b;
st.fa = a;
}
void noFill() {
fill(0, 0, 0, 0);
}
void translate(double tx, double ty) {
st.tf = "translate(" + s(tx) + " " + s(ty) + ")" + st.tf;
}
void rotate(double rad, double x = 0, double y = 0) {
st.tf = "rotate(" + s(rad) + " " + s(x) + " " + s(y) + ")" + st.tf;
}
void scale(double sx, double sy) {
st.tf = "scale(" + s(sx) + " " + s(sy) + ")" + st.tf;
}
void line(double x1, double y1, double x2, double y2) {
data += tag("line", p("x1", x1) + p("y1", y1) + p("x2", x2) + p("y2", y2) + st.stroke() + st.transform()) + "\n";
}
void rect(double x, double y, double w, double h) {
data += tag("rect", p("x", x) + p("y", y) + p("width", w) + p("height", h) + st.stroke() + st.fill() + st.transform()) + "\n";
}
void circle(double x, double y, double r) {
data += tag("circle", p("cx", x) + p("cy", y) + p("r", r) + st.stroke() + st.fill() + st.transform()) + "\n";
}
void text(string str, double x, double y) {
data += tag("text", st.textAnchor() + p("x", x) + p("y", y) + st.font() + st.fill() + st.transform(), str) + "\n";
}
string dumpSvg(string id = "", string style = "") const {
string res;
res += "<svg ";
if (id != "") res += p("id", id);
if (style != "") res += p("style", style);
res += p("viewBox", "-1 -1 " + s(screenW + 2) + " " + s(screenH + 2)) + p("xmlns", "http://www.w3.org/2000/svg");
res += ">\n" + data + "</svg>";
return res;
}
private:
Style st;
vector<Style> stack;
};
class Movie {
public:
vector<string> svgs;
Movie() {
}
void clear() {
svgs.clear();
}
void addFrame(const Graphics& g) {
svgs.push_back(g.dumpSvg("f" + to_string(svgs.size()), "display:none;pointer-events:none;user-select:none;"));
}
string dumpHtml() {
ostringstream oss;
int num = (int) svgs.size();
oss << "<html><body><div id=\"text\">loading...</div>" << endl;
oss << "<div style=\"display:flex;\"><input type=\"button\" value=\"prev\" style=\"height:32px;\" id=\"prev\"><input type=\"button\" value=\"&#x25b6;\" style=\"width:60px;height:32px;\" id=\"play\"><input type=\"button\" value=\"next\" style=\"height:32px;margin-right:4px;\" id=\"next\"><label>slow<input type=\"range\" min=\"2\" max=\"60\" step=\"1\" value=\"10\" id=\"fps\">fast</label></div>" << endl;
oss << "<input type=\"range\" min=\"1\" max=\"" << num << "\" step=\"1\" id=\"bar\" style=\"width:500px\"></div>" << endl;
for (int i = 0; i < num; i++) {
string& svg = svgs[i];
oss << svg << endl;
}
oss << "<script>\nlet numFrames = " << num << ";";
oss << R"(
let text = document.getElementById("text");
let toggle = document.getElementById("play");
let bar = document.getElementById("bar");
let fps = document.getElementById("fps");
let frames = [];
for (let i = 0; i < numFrames; i++) {
let f = document.getElementById("f" + i);
frames.push(f);
f.style.display = "none";
}
let currentFrame = 0;
let playing = false;
let id = 0;
function play() {
if (playing) return;
if (currentFrame == numFrames - 1) setFrame(0);
playing = true;
toggle.value = "\u25a0";
id++;
setTimeout(() => frame(id), 1000 / fps.value);
}
function stop() {
if (!playing) return;
id++;
playing = false;
toggle.value = "\u25b6";
}
toggle.onclick = () => {
if (playing) stop();
else play();
}
bar.oninput = () => {
if (currentFrame != bar.value - 1) setFrame(bar.value - 1);
}
document.getElementById("prev").onclick = () => { stop(); currentFrame > 0 && setFrame(currentFrame - 1); }
document.getElementById("next").onclick = () => { stop(); nextFrame(); }
function setFrame(at) {
frames[currentFrame].style.display = "none";
frames[at].style.display = null;
text.innerText = (at + 1) + " / " + numFrames;
bar.value = at + 1;
currentFrame = at;
}
function nextFrame() {
if (currentFrame == numFrames - 1) return;
setFrame(currentFrame + 1);
if (currentFrame == numFrames - 1) stop();
}
function frame(i) {
if (i != id) return;
nextFrame();
setTimeout(() => frame(i), 1000 / fps.value);
}
play();
)";
oss << "</script></body></html>" << endl;
return oss.str();
}
};
}
// ------------------------------------ LIBRARY END ------------------------------------
using namespace std;
bool writeText(string fileName, string text, bool append = false) {
ofstream fout;
fout.open(fileName, append ? ios::out | ios::app : ios::out);
if (!fout) {
return false;
}
fout << text;
fout.close();
return true;
}
using Graphics = svg::Graphics;
using Movie = svg::Movie;
struct Ball {
double x = 0;
double y = 0;
double vx = 0;
double vy = 0;
double r = 0;
double color[3] = {};
};
int main(int argc, char* argv[]) {
Graphics g;
Movie mov;
// init screen size
g.screen(400, 400);
const int NUM = 20;
Ball balls[NUM];
random_device engine;
uniform_real_distribution<> rand(0, 1);
for (int i = 0; i < NUM; i++) {
auto& b = balls[i];
b.x = rand(engine) * 350 + 25;
b.y = rand(engine) * 350 + 25;
b.r = rand(engine) * 15 + 10;
b.vx = rand(engine) * 20 - 10;
b.vy = rand(engine) * 20 - 10;
b.color[0] = rand(engine);
b.color[1] = rand(engine);
b.color[2] = rand(engine);
}
for (int t = 0; t < 500; t++) {
// move
for (int i = 0; i < NUM; i++) {
auto& b = balls[i];
b.x += b.vx;
b.y += b.vy;
b.vy += 0.2;
if (b.x - b.r < 0 && b.vx < 0) {
b.vx *= -1;
}
if (b.x + b.r > 400 && b.vx > 0) {
b.vx *= -1;
}
if (b.y + b.r > 400 && b.vy > 0) {
b.vy *= -0.75;
}
}
// draw
g.clear();
g.stroke(0, 0, 0);
g.fontSize(10);
for (int i = 0; i < NUM; i++) {
auto& b = balls[i];
g.fill(b.color[0], b.color[1], b.color[2]);
g.circle(b.x, b.y, b.r);
g.line(b.x, b.y, b.x + b.vx * 10, b.y + b.vy * 10);
g.fill(0, 0, 0);
g.text("BALL " + to_string(i), b.x, b.y - b.r);
}
// add frame
mov.addFrame(g);
}
// write html
writeText("mov.html", mov.dumpHtml());
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment