Created
February 26, 2022 11:09
-
-
Save saharan/9ff36be4db19aeccfd6e39732cf32941 to your computer and use it in GitHub Desktop.
a tiny visualization library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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=\"▶\" 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