diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8e5ad8..0c83565 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,7 @@ jobs: deploy: needs: build + if: github.ref == 'refs/heads/main' permissions: pages: write # to deploy to Pages id-token: write # to verify the deployment originates from an appropriate source 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..408b9fa --- /dev/null +++ b/makesite.py @@ -0,0 +1,389 @@ +""" +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('Pages') + 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("
Current page") + menu += [ + f"{title}" for title in subtitles + ] + + if NEWS: + menu.append('
News') + 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'

{page.title}

') + 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 = "".join(c for c in title if ord(c) < 256).strip() + title_short = title_short.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().replace(" ", "-") + 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..1742dee --- /dev/null +++ b/pages/index.md @@ -0,0 +1,71 @@ + +pygfx-org + +*The collective behind the Pygfx render engine and associated projects.* + +## ๐Ÿ’ซ Projects + +
+ +

Pygfx

+ A powerful render engine for Python

+ github.com/pygfx/pygfx
+ pygfx.readthedocs.io
+
+ +
+ +

wgpu-py

+ WebGPU for Python

+ github.com/pygfx/wgpu
+ wgpu-py.readthedocs.io
+
+ +
+

Other

+ Projects that we also contribute to

+ wgpu-native
+ jupyter_rfb
+ pylinalg +
+ + +## ๐Ÿš€ Mission + +We are dedicated to bring powerful and reliable visualization to the Python world. +We believe that WebGPU 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 open source and free to use. To develop these projects we rely on funding from our sponsors. The more groups "chip in", the more time we can spend on moving the projects forwards. Recurring funding is especially welcome. [Learn more ...](sponsor.html) + +
+

Ramona optics

+
+
https://ramonaoptics.com +
+ +
+

The Flatiron institute

+
+ https://simonsfoundation.org/flatiron/ +
+ + + +## ๐Ÿ‘ฅ Team + +
+
+ @almarklein +
+ +
+
+ @korijn +
+ diff --git a/pages/sponsor.md b/pages/sponsor.md new file mode 100644 index 0000000..731232b --- /dev/null +++ b/pages/sponsor.md @@ -0,0 +1,48 @@ + + +# 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 + +The pygfx-org is a trademark of *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 are published on a yearly basis. + + +### Via Github + +You can also sponsor via Github's sponsor system: [https://github.com/sponsors/pygfx](https://github.com/sponsors/pygfx). These funds are payed out by GitHub to the same bank account as mentioned above. + + +### 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. + + +## ๐Ÿ’ฐ 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/posts/report_2024_09.md b/posts/report_2024_09.md new file mode 100644 index 0000000..e69de29 diff --git a/static/gh32.png b/static/gh32.png new file mode 100644 index 0000000..176ab33 Binary files /dev/null and b/static/gh32.png differ 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/static/rtd.png b/static/rtd.png new file mode 100644 index 0000000..bd2ecb2 Binary files /dev/null and b/static/rtd.png differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..228ca00 --- /dev/null +++ b/style.css @@ -0,0 +1,264 @@ +/*! 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: #fafafa; +} +.content { + box-sizing: border-box; + padding: 1em 1em; + width: 100%; + + position: static; + max-width: none; + margin: 0; + margin-top: 1em; + + background: #fff; + border: 1px solid #aaa; + border-radius: 8px; + xx-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; + + padding: 0.5em 1em; + overflow: hidden; + white-space: nowrap; + + background: #fff; + border: 1px solid #aaa; + border-radius: 8px; + xx-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%; + line-height: 1.5; + background: #fcfcfc; + padding: 20px; + margin: 0.5em; + border: 1px solid #ccc; + border-radius: 8px; +} +.profilebox { + text-align: center; +} +.projectbox, .sponsorbox { + padding-top: 0; +} +.projectbox h3, .sponsorbox h3 { + color:#444; + font-size: 110%; +} +.sponsorbox img { + margin-bottom: 8px; + height: 50px; +} + +img.stars-badge { + position: absolute; + display: block; + top: 10px; + right: 10px; +} + +img.profile { + width: 80px; + height: 80px; + border-radius: 40px; +} + +@media screen and (min-width: 500px) { + .projectbox { + width: 450px; + } + .sponsorbox { + width: 450px; + } + .profilebox { + width: 130px; + } +} +@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: #555; + font-family: Consolas, "DejaVu Sans Mono", Monaco, "Courier New", Courier, monospace; +} +a.header:hover { + color: #2A4; +} +h2 { + margin-top: 1.3em; + xx-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} + +
+ +
+ + + +
+ +
+ + +