diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ce191e3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.pyd
+__pycache__
+.DS_Store
+output/
\ No newline at end of file
diff --git a/makesite.py b/makesite.py
new file mode 100644
index 0000000..68f91b6
--- /dev/null
+++ b/makesite.py
@@ -0,0 +1,388 @@
+"""
+Script to build a website from a bunch of markdown files.
+Inspired by https://github.com/sunainapai/makesite
+Tweaked for almarklein.org
+Then for pygfx.org
+"""
+
+import os
+import shutil
+import webbrowser
+
+import markdown
+import pygments
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name
+
+
+TITLE = "pygfx.org"
+
+NAV = {
+ "Main": "index",
+ "Sponsor": "sponsor",
+ # "Blog": "blog",
+ # "Archive": "archive",
+ # "Social": {
+ # 'Twitter': 'https://twitter.com/pygfx',
+ # },
+}
+
+NEWS = {
+ "Released pygfx v0.5.0": "https://github.com/pygfx/pygfx/releases/tag/v0.5.0",
+ "Released wgpu-py v0.18.1": "https://github.com/pygfx/wgpu-py/releases/tag/v0.18.1",
+}
+
+THIS_DIR = os.path.dirname(os.path.abspath(__file__))
+OUT_DIR = os.path.join(THIS_DIR, "output")
+STATIC_DIR = os.path.join(THIS_DIR, "static")
+PAGES_DIR = os.path.join(THIS_DIR, "pages")
+POSTS_DIR = os.path.join(THIS_DIR, "posts")
+
+
+REDIRECT = '
'
+
+
+def create_menu(page):
+ """ Create the menu for the given page.
+ """
+ menu = [""]
+
+ menu.append('')
+ for title, target in NAV.items():
+ if isinstance(target, str):
+ if target.startswith(("https://", "http://", "/")):
+ menu.append(f"{title}")
+ else:
+ menu.append(f"{title}")
+ if target == page.name:
+ menu[-1] = menu[-1].replace("{title}")
+ if target.get("", None) == page.name:
+ menu[-1] = menu[-1].replace("{subtitle}")
+ else:
+ menu.append(
+ f"{subtitle}"
+ )
+ if subtarget == page.name:
+ menu[-1] = menu[-1].replace("class='", "class='current ")
+ else:
+ raise RuntimeError(f"Unexpected NAV entry {type(target)}")
+
+ subtitles = [title for level, title in page.headers if level == 2]
+ if subtitles:
+ menu.append("
")
+ menu += [
+ f"{title}" for title in subtitles
+ ]
+
+ if NEWS:
+ menu.append('
')
+ for title, url in NEWS.items():
+ # menu.append(f"{title}")
+ menu.append(f"{title}")
+
+ return "
".join(menu)
+
+
+def create_blog_relatated_pages(posts):
+ """ Create blog overview page.
+ """
+
+ # Filter and sort
+ posts = [
+ post for post in posts.values() if post.date and not post.name.startswith("_")
+ ]
+ posts.sort(key=lambda p: p.date)
+
+ blogpages = {}
+
+ # Generate overview page
+ html = ["Blog
"]
+ for page in reversed(posts):
+ text = page.md
+ if "" in text:
+ summary = text.split("")[-1].split(
+ ""
+ )[0]
+ else:
+ summary = text.split("## ")[0]
+ summary = summary.split("-->")[-1]
+
+ # html.append("
" + page.date_and_tags_html)
+ html.append("" + page.date_and_tags_html + "
")
+ html.append(f'')
+ if page.thumbnail:
+ html.append(f"
")
+ # html.append(f'{page.title}
')
+ html.append("" + summary + "
")
+ html.append(f"read more ...
")
+ html.append("")
+ blogpages["overview"] = "\n".join(html)
+
+ # Generate archive page
+ year = ""
+ html = ["Archive
\n"]
+ for page in reversed(posts):
+ if page.date[:4] != year:
+ year = page.date[:4]
+ html.append(f"{year}
")
+ html.append(f'{page.date}: {page.title}
')
+ blogpages["archive"] = "\n".join(html)
+
+ # todo: Generate page for each tag
+
+ return blogpages
+
+
+def create_assets():
+ """ Returns a dict of all the assets representing the website.
+ """
+ assets = {}
+
+ # Load all static files
+ for root, dirs, files in os.walk(STATIC_DIR):
+ for fname in files:
+ filename = os.path.join(root, fname)
+ with open(filename, "rb") as f:
+ assets[os.path.relpath(filename, STATIC_DIR)] = f.read()
+
+ # Collect pages
+ pages = {}
+ for fname in os.listdir(PAGES_DIR):
+ if fname.lower().endswith(".md"):
+ name = fname.split(".")[0].lower()
+ with open(os.path.join(PAGES_DIR, fname), "rb") as f:
+ md = f.read().decode()
+ pages[name] = Page(name, md)
+
+ # Collect blog posts
+ posts = {}
+ for fname in os.listdir(POSTS_DIR):
+ if fname.lower().endswith(".md"):
+ name = fname.split(".")[0].lower()
+ assert name not in pages, f"blog post slug not allowed: {name}"
+ with open(os.path.join(POSTS_DIR, fname), "rb") as f:
+ md = f.read().decode()
+ posts[name] = Page(name, md)
+
+ # Get template
+ with open(os.path.join(THIS_DIR, "template.html"), "rb") as f:
+ html_template = f.read().decode()
+
+ with open(os.path.join(THIS_DIR, "style.css"), "rb") as f:
+ css = f.read().decode()
+ css += "/* Pygments CSS */\n" + HtmlFormatter(style="vs").get_style_defs(
+ ".highlight"
+ )
+
+ # Generate posts
+ for page in posts.values():
+ page.prepare(pages.keys())
+ title = page.title
+ menu = create_menu(page)
+ html = html_template.format(
+ title=title, style=css, body=page.to_html(), menu=menu
+ )
+ print("generating post", page.name + ".html")
+ assets[page.name + ".html"] = html.encode()
+
+ # Generate pages
+ for page in pages.values():
+ page.prepare(pages.keys())
+ title = TITLE if page.name == "index" else TITLE + " - " + page.title
+ menu = create_menu(page)
+ html = html_template.format(
+ title=title, style=css, body=page.to_html(), menu=menu
+ )
+ print("generating page", page.name + ".html")
+ assets[page.name + ".html"] = html.encode()
+
+ # Generate special pages
+ fake_md = "" # "##index\n## archive\n## tags"
+ for name, html in create_blog_relatated_pages(posts).items():
+ name = "blog" if name == "overview" else name
+ print("generating page", name + ".html")
+ assets[f"{name}.html"] = html_template.format(
+ title=TITLE, style=css, body=html, menu=create_menu(Page("", fake_md))
+ ).encode()
+
+ # Backwards compat with previous site
+ for page in pages.values():
+ assets["pages/" + page.name + ".html"] = REDIRECT.replace(
+ "URL", f"/{page.name}.html"
+ ).encode()
+
+ # Fix backslashes on Windows
+ for key in list(assets.keys()):
+ if "\\" in key:
+ assets[key.replace("\\", "/")] = assets.pop(key)
+
+ return assets
+
+
+def main():
+ """ Main function that exports the page to the file system.
+ """
+ # Create / clean output dir
+ if os.path.isdir(OUT_DIR):
+ shutil.rmtree(OUT_DIR)
+ os.mkdir(OUT_DIR)
+
+ # Write all assets to the directory
+ for fname, bb in create_assets().items():
+ filename = os.path.join(OUT_DIR, fname)
+ dirname = os.path.dirname(filename)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+ with open(filename, "wb") as f:
+ f.write(bb)
+
+
+class Page:
+ """ Representation of a page. It takes in markdown and produces HTML.
+ """
+
+ def __init__(self, name, markdown):
+ self.name = name
+ self.md = markdown
+ self.parts = []
+ self.headers = []
+
+ self.title = name
+ if markdown.startswith("# "):
+ self.title = markdown.split("\n")[0][1:].strip()
+
+ self.date = None
+ if "")[0].strip() or None
+ if self.date is not None:
+ assert (
+ len(self.date) == 10 and self.date.count("-") == 2
+ ), f"Weird date in {name}.md"
+
+ self.author = None
+ if "")[0].strip()
+
+ self.tags = []
+ if "")[0].split(",")
+ ]
+
+ self.date_and_tags_html = ""
+ if self.date:
+ self.date_and_tags_html = f"{', '.join(self.tags)} - {self.date}"
+
+ self.thumbnail = None
+ for fname in ["thumbs/" + self.name + ".jpg"]:
+ if os.path.isfile(os.path.join(THIS_DIR, "static", fname)):
+ self.thumbnail = fname
+
+ def prepare(self, page_names):
+ # Convert markdown to HTML
+ self.md = self._fix_links(self.md, page_names)
+ self.md = self._highlight(self.md)
+ self._split() # populates self.parts and self.headers
+
+ def _fix_links(self, text, page_names):
+ """ Fix the markdown links based on the pages that we know.
+ """
+ for n in page_names:
+ text = text.replace(f"]({n})", f"]({n}.html)")
+ text = text.replace(f"]({n}.md)", f"]({n}.html)")
+ return text
+
+ def _highlight(self, text):
+ """ Apply syntax highlighting.
+ """
+ lines = []
+ code = []
+ for i, line in enumerate(text.splitlines()):
+ if line.startswith("```"):
+ if code:
+ formatter = HtmlFormatter()
+ try:
+ lexer = get_lexer_by_name(code[0])
+ except Exception:
+ lexer = get_lexer_by_name("text")
+ lines.append(
+ pygments.highlight("\n".join(code[1:]), lexer, formatter)
+ )
+ code = []
+ else:
+ code.append(line[3:].strip()) # language
+ elif code:
+ code.append(line)
+ else:
+ lines.append(line)
+ return "\n".join(lines).strip()
+
+ def _split(self):
+ """ Split the markdown into parts based on sections.
+ Each part is either text or a tuple representing a section.
+ """
+ text = self.md
+ self.parts = parts = []
+ self.headers = headers = []
+ lines = []
+
+ # Split in parts
+ for line in text.splitlines():
+ if line.startswith(("# ", "## ", "### ", "#### ", "##### ")):
+ # Finish pending lines
+ parts.append("\n".join(lines))
+ lines = []
+ # Process header
+ level = len(line.split(" ")[0])
+ title = line.split(" ", 1)[1]
+ title_short = title.split("(")[0].split("<")[0].strip().replace("`", "")
+ headers.append((level, title_short))
+ parts.append((level, title_short, title))
+ else:
+ lines.append(line)
+ parts.append("\n".join(lines))
+
+ # Now convert all text to html
+ for i in range(len(parts)):
+ if not isinstance(parts[i], tuple):
+ parts[i] = markdown.markdown(parts[i], extensions=[]) + "\n\n"
+
+ def to_html(self):
+ htmlparts = []
+ for part in self.parts:
+ if isinstance(part, tuple):
+ level, title_short, title = part
+ title_html = (
+ title.replace("``", "`")
+ .replace("`", "", 1)
+ .replace("`", "
", 1)
+ )
+ ts = title_short.lower()
+ if part[0] == 1:
+ htmlparts.append(self.date_and_tags_html)
+ htmlparts.append("%s
" % title_html)
+ elif part[0] == 2 and title_short:
+ htmlparts.append(
+ "".format(ts, ts)
+ )
+ htmlparts.append("%s" % (level, title_html, level))
+ htmlparts.append("")
+ else:
+ htmlparts.append("%s" % (level, title_html, level))
+ else:
+ htmlparts.append(part)
+ return "\n".join(htmlparts)
+
+
+if __name__ == "__main__":
+ main()
+ # webbrowser.open(os.path.join(OUT_DIR, "index.html"))
diff --git a/pages/index.md b/pages/index.md
new file mode 100644
index 0000000..4f09dd1
--- /dev/null
+++ b/pages/index.md
@@ -0,0 +1,72 @@
+
+pygfx.org
+
+
+## ๐ซ Projects
+
+
+
+
+
+
+
+
+## ๐ Mission
+
+We are dedicated to bring powerful and reliable visualization to the Python world.
+
+We believe that wgpu is the future for graphics and bring it to Python with the wgpu-py library. On top of that, we build pygfx: a modern, versatile, and Pythonic rendering engine.
+
+Pygfx provides a basis on top of which a multitude of visualizations become possible. From applications to libraries, from games to plotting.
+
+
+
+
+## โค๏ธ Current sponsors
+
+Pygfx and wgpu are free. To develop these projects we rely on [funding from our sponsors](sponsor.html). The more groups "chip in", the more time we can spend on moving the projects forwards. Recurring funding is especially welcome.
+
+
+
+
+
+
+
+## ๐ฅ Team
+
+
+
+
+
diff --git a/pages/sponsor.md b/pages/sponsor.md
new file mode 100644
index 0000000..c4a9b2d
--- /dev/null
+++ b/pages/sponsor.md
@@ -0,0 +1,47 @@
+
+
+# Sponsoring Pygfx
+
+
+## ๐งโ๐คโ๐ง Keep pygfx independent and active
+
+Maintaining and growing wgpu and pygfx costs time and dedication. We rely on sponsors to maintain (and grow) the project further.
+If you represent a company / group that relies on pygfx or wgpu-pu, we kindly ask for a sponsorship. That way we can keep replying to issues, review pull request, and move pygfx further.
+
+
+## ๐ What you get
+
+* Most importantly, sponsors help ensure that pygfx is actively maintained!
+* Sponsors also get priority on bug reports and feature requests.
+* An honorable mention on the front page of pygfx.org!
+* In the top tiers, one-on-one support to help you use Pygfx to the max.
+
+We employ a few different [sponsorship tiers](https://github.com/sponsors/pygfx).
+
+
+## ๐งพ Ways to sponsor Pygfx
+
+We provide a few ways to get funds to us. If you have questions, do not hesitate to reach out to [support@pygfx.com](mailto:support@pygfx.com)!
+
+### Directly
+
+You can sponsor us directly via *Almar Klein scientific computing*, based in The Netherlands. We can provide an invoice and you pay by bank transfer.
+Incoming funds for Pygfx are received at a dedicated bank account, and insights into how the funds are spent will be published on a yearly basis.
+
+
+### Via OpenCollective
+
+You can sponsor us via [https://opencollective.com/pygfx](https://opencollective.com/pygfx). These funds and how they are spent are publicly visible.
+
+
+### Via Github
+
+You can also sponsor via Github's sponsor system: https://github.com/sponsors/pygfx
+
+
+## ๐ฐ How funds are spent
+
+Sponsorship funds for Pygfx are primarily used to fund our developer time.
+If we receive more funds than we can spend, the surplus acts as a buffer to create runway. If that buffer becomes large enough we plan to onboard additional developers.
+
+
diff --git a/static/pygfx.png b/static/pygfx.png
new file mode 100644
index 0000000..409abc0
Binary files /dev/null and b/static/pygfx.png differ
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..e8d525c
--- /dev/null
+++ b/style.css
@@ -0,0 +1,259 @@
+/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
+html{line-height:1.15;-webkit-text-size-adjust:100%}
+body{margin:0}
+h1{font-size:2em;margin:.67em 0}
+hr{box-sizing:content-box;height:0;overflow:visible}
+pre{font-family:monospace,monospace;font-size:1em}
+a{background-color:transparent}
+abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
+b,strong{font-weight:bolder}
+code,kbd,samp{font-family:monospace,monospace;font-size:1em}
+small{font-size:80%}
+sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sub{bottom:-.25em}
+sup{top:-.5em}
+img{border-style:none}
+button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}
+button,select{text-transform:none}
+button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}
+button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}
+button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}
+fieldset{padding:.35em .75em .625em}
+legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}
+progress{vertical-align:baseline}
+textarea{overflow:auto}
+[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}
+[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}
+[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}
+[type="search"]::-webkit-search-decoration{-webkit-appearance:none}
+::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
+details{display:block}
+summary{display:list-item}
+template{display:none}
+[hidden]{display:none}
+
+html {
+ height: 100%;
+}
+
+body {
+ height: 100%;
+ font-family: Ubuntu,"Helvetica Neue",Arial,sans-serif;
+ color: #404040;
+ font-weight: normal;
+ background: #fcfcfc;
+}
+.content {
+ box-sizing: border-box;
+ padding: 1em 1em;
+ width: 100%;
+
+ position: static;
+ max-width: none;
+ margin: 0;
+ margin-top: 1em;
+
+ /*
+ background: #fcfcfc;
+ */
+ border-radius: 6px;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
+}
+p, li {
+ line-height: 150%;
+}
+.menu {
+ box-sizing: border-box;
+ position: static;
+ width: 100%;
+ max-width: none;
+
+ background: #f8f8f8;
+ padding: 0.5em 1em;
+ overflow: hidden;
+ white-space: nowrap;
+
+ border-radius: 6px;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
+}
+
+.projectbox, .sponsorbox, .profilebox {
+ box-sizing: border-box;
+ display: inline-block;
+ position: relative; /* so stuff can be abs-positined inside */
+ width: 100%;
+ background: #f1f1f1;
+ padding: 1em 1em;
+ margin: 0.5em;
+ border-radius: 6px;
+}
+.profilebox {
+ text-align: center;
+}
+.projectbox h3, .sponsorbox h3 {
+ color:#444;
+ font-size: 110%;
+}
+.sponsorbox img {
+ margin-bottom: 8px;
+ height: 50px;
+}
+
+img.stars-badge {
+ position: absolute;
+ display: block;
+ top: 7px;
+ right: 7px;
+}
+
+img.profile {
+ width: 100px;
+ height: 100px;
+ border-radius: 50px;
+}
+
+@media screen and (min-width: 500px) {
+ .projectbox {
+ width: 450px;
+ }
+ .sponsorbox {
+ width: 450px;
+ }
+ .profilebox {
+ width: 150px;
+ }
+}
+@media screen and (min-width: 1300px) {
+ .content {
+ width: 1000px;
+ padding: 1em 1.5em;
+ margin-left: auto;
+ margin-right: 370px;
+ }
+ .absspacer {
+ height: 100px;
+ }
+ .menu {
+ position: fixed;
+ top: 1em;
+ right: 10px;
+ max-width: 250px;
+ margin-top: 0;
+ }
+}
+@media screen and (min-width: 1650px) {
+ .content {
+ margin-right: auto;
+ }
+ .menu {
+ left: calc(50% + 500px + 20px);
+ max-width: 300px;
+ }
+}
+
+a:link, a:visited, a:active {
+ color: #36C;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+.menu .header {
+ color: #aaa;
+ display: block;
+ border-bottom: 1px solid #ccc;
+}
+.menu a {
+ font-size: 120%;
+ color: #000;
+ line-height: 150%;
+ margin-left: 0.5em;
+}
+.menu a.current {
+ font-weight: bold;
+}
+.menu a.sub {
+ font-size: 90%;
+ /*margin-left: 1.5em;*/
+}
+
+.menu .ad {
+ box-sizing: border-box;
+ max-width: 270px;
+ white-space: normal;
+ text-align: center;
+}
+.menu .ad a {
+ font-size: 70%;
+ line-height: 100%;
+ margin: 0;
+}
+.menu .ad a.a-ad {
+ font-size: 95%;
+ text-decoration: none;
+}
+
+a.anch:hover {
+ text-decoration: none;
+}
+a.anch:hover h2::after {
+ content: " \00B6";
+ color: rgba(0, 0, 0, 0.3);
+ font-size: 80%;
+}
+hr {
+ height: 1px;
+ background: rgba(30, 60, 90, 0.2);
+ border: 0px solid #ccc;
+}
+.footer {
+ color: #888;
+ font-size: 80%;
+}
+code {
+ font-family: Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;
+ font-size: 90%;
+ color: #000;
+ background: #fff;
+ padding: 1px 5px;
+ white-space: nowrap;
+ border: solid 1px #e1e4e5;
+}
+.highlight { /*pygments */
+ font-family: dejavu sans mono,Consolas,"Andale Mono WT","Andale Mono","Lucida Console", "Courier New",Courier,monospace;
+ font-size: 12px;
+ color: #444;
+ background: #fff;
+ border: 1px solid #dddddd;
+ padding: 0em 1em;
+}
+h1, h2, h3, h4 {
+ color: #999;
+ font-family: Consolas, "DejaVu Sans Mono", Monaco, "Courier New", Courier, monospace;
+}
+a.header:hover {
+ color: #2A4;
+}
+h2 {
+ margin-top: 1.3em;
+ border-bottom: 1px solid rgba(20, 100, 40, 0.3);
+}
+h2 code, h3 code, h4 code {
+ color: #369;
+ padding-left: 0;
+ background: none;
+ border: 0px;
+ font-size: 85%;
+}
+span.post-date-tags {
+ float: right;
+ color: #888;
+ font-size: 80%;
+}
+img.thumb {
+ width: 128px;
+ height: 128px;
+ float: left;
+ margin: 0 1em 0.5em 0;
+ border-radius: 4px;
+}
diff --git a/template.html b/template.html
new file mode 100644
index 0000000..51a3d4a
--- /dev/null
+++ b/template.html
@@ -0,0 +1,36 @@
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+ {body}
+
+
+
+
+
+
+
+
+
+
+
+
+