See More

import os, re, json EXAMPLES_ROOT = "/home/pep/Projects/processing-cpp.github.io/assets/examples" EXAMPLES_JS_ROOT = "/home/pep/Projects/processing-cpp.github.io/assets/examples_js" def strip_comment(code): code = code.strip() if code.startswith("/**"): end = code.find("*/") if end != -1: code = code[end+2:].strip() return code def get_canvas_size(js_code): m = re.search(r'createCanvas\s*\(\s*(\d+)\s*,\s*(\d+)', js_code) if m: return int(m.group(1)), int(m.group(2)) m = re.search(r'\bsize\s*\(\s*(\d+)\s*,\s*(\d+)', js_code) if m: return int(m.group(1)), int(m.group(2)) return 640, 360 def scan_section(section_name): """ Scans assets/examples////.pde (and the matching assets/examples_js//... for the JS translation, if it exists yet) into the same {category: [examples]} shape the rest of this script already expects from `data`. Used for both "Basics" (the only section with content today) and "Topics" (currently empty placeholder folders -- this picks it up automatically the moment real .pde/.js files are added, with zero further changes needed here). """ base_cpp = os.path.join(EXAMPLES_ROOT, section_name) base_js = os.path.join(EXAMPLES_JS_ROOT, section_name) if not os.path.isdir(base_cpp): return {} section_data = {} for cat in sorted(os.listdir(base_cpp)): cat_path = os.path.join(base_cpp, cat) if not os.path.isdir(cat_path): continue examples = [] for example in sorted(os.listdir(cat_path)): ex_path = os.path.join(cat_path, example) if not os.path.isdir(ex_path): continue pde = os.path.join(ex_path, example + ".pde") if not os.path.exists(pde): continue with open(pde) as f: code = f.read() js_file = os.path.join(base_js, cat, example, example + ".js") js_code = "" w, h = 640, 360 if os.path.exists(js_file): with open(js_file) as f: js_code = strip_comment(f.read()) w, h = get_canvas_size(js_code) slug = example.replace("_", "-").lower() examples.append({ "id": section_name + "_" + cat + "_" + example, "name": example.replace("_", " "), "slug": slug, "code": code, "js": js_code, "w": w, "h": h, }) if examples: # skip categories that exist as empty folders with no real content yet section_data[cat] = examples return section_data # Every top-level folder under assets/examples/ is treated as a section # (e.g. "Basics", "Topics") -- discovered automatically, not hardcoded, so # adding real content under an existing empty Topics// folder # is all that's needed to make it show up here; no script change required. SECTION_NAMES = sorted(os.listdir(EXAMPLES_ROOT)) if os.path.isdir(EXAMPLES_ROOT) else [] sections = {} for _section_name in SECTION_NAMES: if not os.path.isdir(os.path.join(EXAMPLES_ROOT, _section_name)): continue scanned = scan_section(_section_name) if scanned: # only keep sections that actually have at least one real example sections[_section_name] = scanned # `data` is kept as an alias for the first/primary section (normally # "Basics") for backwards compatibility with anything below that hasn't # been updated to iterate over `sections` yet. data = sections.get("Basics", {}) def build_page_sidebar(active_id=""): s = "" for i, (section_name, section_data) in enumerate(sections.items()): key = section_name.lower() # First section starts open, the rest start collapsed -- matches # the original Basics-open/Topics-collapsed behavior. open_by_default = True arrow = "â–¾" display = "block" s += ( f'

' f'{section_name}{arrow}
' f'
' ) for cat, examples in section_data.items(): s += f'
{cat.replace("_"," ").title()}
' for ex in examples: active = 'class="active"' if ex["id"] == active_id else "" s += f'{ex["name"]}' s += '
' s += '
' # Any section folder that exists but has no real example content yet # (e.g. Topics before its .pde/.js files are added) still gets a # collapsed "Coming soon" entry, so the sidebar doesn't just silently # omit it. empty_sections = [ name for name in SECTION_NAMES if name not in sections and os.path.isdir(os.path.join(EXAMPLES_ROOT, name)) ] for name in empty_sections: key = name.lower() s += ( f'
' f'{name}â–¸
' f'' ) return s shared_css = '''* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #111; background: #fff; } a { color: #111; text-decoration: none; } nav { border-bottom: 1px solid #e0e0e0; padding: 0 2rem; display: flex; align-items: center; justify-content: space-between; height: 60px; position: sticky; top: 0; background: #fff; z-index: 100; } .hamburger { background: none; border: none; cursor: pointer; font-size: 22px; padding: 4px 8px; display: none; } .layout { display: flex; min-height: calc(100vh - 60px); } .sidebar-outer { width: 220px; min-width: 220px; border-right: 1px solid #e0e0e0; display: flex; flex-direction: column; position: sticky; top: 60px; height: calc(100vh - 60px); } #site-sidebar { padding: 1.5rem 1.5rem 1rem; border-bottom: 1px solid #e0e0e0; display: flex; flex-direction: column; } #site-sidebar a { font-size: 14px; color: #555; padding: 0.4rem 0; display: block; } #site-sidebar a:hover { color: #111; } #site-sidebar a.active { color: #111; font-weight: 500; } .sidebar-examples { flex: 1; overflow-y: auto; } .section-header { display: flex; justify-content: space-between; align-items: center; padding: 0.85rem 1.5rem; font-size: 13px; font-weight: 600; color: #111; cursor: pointer; border-bottom: 1px solid #e0e0e0; user-select: none; } .section-header:hover { background: #f8f8f8; } .arrow { font-size: 11px; color: #aaa; } .category { margin-bottom: 0.25rem; } .category-title { font-size: 11px; font-weight: 600; color: #aaa; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.75rem 1.5rem 0.25rem; } .category a { display: block; font-size: 13px; color: #555; padding: 0.3rem 1.5rem; } .category a:hover { color: #111; background: #f8f8f8; } .category a.active { color: #111; font-weight: 500; background: #f4f4f4; } .content { flex: 1; padding: 3rem 4rem; max-width: 900px; } .content h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #e8b400; } .preview-wrap { border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; margin-bottom: 2rem; display: block; max-width: 100%; width: fit-content; } .preview-wrap iframe { display: block; border: none; max-width: 100%; } .code-block { background: #f8f8f8; border-radius: 8px; overflow: hidden; } .code-header { padding: 0.75rem 1.25rem; border-bottom: 1px solid #e0e0e0; font-size: 12px; color: #888; font-family: monospace; display: flex; align-items: center; justify-content: space-between; } .copy-btn { font-size: 12px; color: #555; background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 3px 10px; cursor: pointer; font-family: inherit; } .copy-btn:hover { background: #f0f0f0; } .copy-btn.copied { color: #090; border-color: #090; } pre { padding: 1.5rem; font-family: "SF Mono","Fira Code",monospace; font-size: 13px; line-height: 1.7; overflow-x: auto; white-space: pre; } .welcome h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 1rem; } .welcome p { color: #555; line-height: 1.8; max-width: 500px; } footer { border-top: 1px solid #e0e0e0; padding: 2rem; text-align: center; font-size: 13px; color: #888; } .footer-contact { margin-top: 0.4rem; font-size: 12px; } .footer-contact a { color: #aaa; border-bottom: 1px solid transparent; } .footer-contact a:hover { color: #111; border-bottom-color: #111; } .footer-sep { color: #ccc; margin: 0 0.5rem; } @media (max-width: 768px) { .hamburger { display: block; } .sidebar-outer { position: fixed; top: 60px; left: -240px; width: 240px; height: calc(100vh - 60px); background: #fff; z-index: 200; transition: left 0.25s ease; box-shadow: 2px 0 12px rgba(0,0,0,0.08); } .sidebar-outer.open { left: 0; } .content { padding: 2rem 1.25rem; } .preview-wrap { max-width: 100%; } pre { font-size: 12px; } }''' shared_js = '''function copyCode() { navigator.clipboard.writeText(document.getElementById('code-pre').innerText).then(() => { const btn = document.querySelector('.copy-btn'); btn.textContent = 'Copied!'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000); }); } function toggleSection(name) { const sec = document.getElementById(name+'-section'); const arrow = document.getElementById(name+'-arrow'); const open = sec.style.display !== 'none'; sec.style.display = open ? 'none' : 'block'; arrow.textContent = open ? 'â–¸' : 'â–¾'; }''' def fix_asset_paths(js_code): base = "https://processing-cpp.github.io/assets/data/" pat = re.compile(r"""load(Image|Font|Model)\s*\(\s*["']([^"']+)["']\s*((?:,[^)]*)?)\)""") def replacer(m): return f'load{m.group(1)}("{base}{m.group(2)}"{m.group(3)})' return pat.sub(replacer, js_code) def make_iframe(js_code, w, h): js_code = fix_asset_paths(js_code) safe = js_code.replace('', '<\\/script>').replace('`','\\`') return f'''

`);doc.close();}})();''' out_dir = "/home/pep/Projects/processing-cpp.github.io/examples" for fname in os.listdir(out_dir): if fname != "index.html": os.remove(os.path.join(out_dir, fname)) for section_name, section_data in sections.items(): for cat, examples in section_data.items(): for ex in examples: escaped = ex["code"].replace("&","&").replace("<","<").replace(">",">") has_js = bool(ex["js"].strip()) preview = make_iframe(ex["js"], ex["w"], ex["h"]) if has_js else "" page = f'''

{ex["name"]} - C++ Mode Examples

{ex["name"]}

{preview}
{ex["name"].lower().replace(" ","-")}.pde
{escaped}

''' with open(os.path.join(out_dir, ex["slug"]+".html"),"w") as f: f.write(page) THUMBS_MANIFEST = "/home/pep/Projects/processing-cpp.github.io/assets/examples_thumbs/manifest.json" def load_thumb_manifest(): if not os.path.exists(THUMBS_MANIFEST): print(f"NOTE: {THUMBS_MANIFEST} not found -- run generate_example_thumbnails.py " f"first to populate the examples gallery with real thumbnails. " f"examples/index.html will fall back to the plain placeholder text for now.") return None with open(THUMBS_MANIFEST) as f: return json.load(f) def build_examples_gallery(manifest, sections): """Build the Processing.org-style thumbnail gallery: one heading per section (Basics, Topics, ...), with one sub-section per category inside it, each example shown as a clickable card with its thumbnail -- grouped and ordered the same way the sidebar does.""" if not manifest: return '

Examples

' thumb_by_key = {} for m in manifest: # Key on (section, slug) so Basics and Topics can have same-named # examples without one overwriting the other in the lookup table. key = (m.get("section", "Basics"), m["slug"]) thumb_by_key[key] = m section_blocks = [] for section_name, section_data in sections.items(): cat_blocks = [] for cat, examples in section_data.items(): cards = [] for ex in examples: thumb = thumb_by_key.get((section_name, ex["slug"])) if thumb: thumb_src = f'../assets/examples_thumbs/{thumb.get("section", "Basics")}/{thumb["category"]}/{thumb["thumb"]}' else: thumb_src = "" # no thumbnail generated yet for this example img_html = ( f'{ex[' if thumb_src else '

' ) cards.append( f'' f'

' f'' f"" ) cat_blocks.append( f'

' ) section_blocks.append( f'

" ) return ( '

' + "".join(section_blocks) ) gallery_css = ''' .gallery-intro { margin-bottom: 2.5rem; } .gallery-intro h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 0.5rem; color: #e8b400; } .gallery-intro p { color: #555; } .gallery-section-group { margin-bottom: 2rem; } .gallery-section-title { font-size: 1.4rem; font-weight: 700; margin-bottom: 1.5rem; padding-bottom: 0.75rem; border-bottom: 2px solid #111; color: #e8b400; } .gallery-section { margin-bottom: 3rem; } .gallery-section h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #e0e0e0; color: #b8860b; } .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1.25rem; } .gallery-card { display: block; } .gallery-thumb { width: 100%; background: #111; border-radius: 6px; overflow: hidden; border: 1px solid #e0e0e0; line-height: 0; } .gallery-thumb img { width: 100%; height: auto; display: block; } .gallery-thumb-missing { width: 100%; aspect-ratio: 16/9; background: #1a1a1a; } .gallery-card-title { font-size: 13px; color: #555; margin-top: 0.5rem; text-align: center; } .gallery-card:hover .gallery-thumb { border-color: #aaa; } .gallery-card:hover .gallery-card-title { color: #111; } ''' thumb_manifest = load_thumb_manifest() gallery_html = build_examples_gallery(thumb_manifest, sections) ex_sidebar = build_page_sidebar() with open(os.path.join(out_dir,"index.html"),"w") as f: f.write(f'''

Examples - C++ Mode for Processing
{gallery_html}

''') print(f"done — {sum(len(v) for section_data in sections.values() for v in section_data.values())} examples generated across {len(sections)} section(s): {', '.join(sections.keys())}") # --------------------------------------------------------------------------- # Homepage random example previews # # Picks 3 random examples (from the same `data` built above) that have no # external asset dependencies (loadImage/loadFont/etc.), forces their # canvas to a small fixed square (so they fit the homepage's tiny preview # boxes regardless of what size the original sketch requests), and embeds # each one as an isolated iframe -- same technique already used for the # full-size example pages, just shrunk down. This replaces the old # hand-written instance-mode sketches that used to live inline in # index.html (which had a p.document bug and weren't randomized at all). # --------------------------------------------------------------------------- import random HOMEPAGE_PREVIEW_SIZE = 300 # px, square; matches .example-canvas's CSS aspect-ratio:1 box _data_loading_re = re.compile(r"loadImage|loadFont|loadModel|loadStrings|loadJSON|loadTable|requestImage|loadXML") _canvas_size_re = re.compile(r"(createCanvas|size)\s*\(\s*\d+\s*,\s*\d+\s*((?:,[^)]*)?)\)") def force_square_canvas(js_code, size): """Rewrite any createCanvas(w,h[,...])/size(w,h) call to a fixed square size, so the sketch renders at the small homepage preview size regardless of what resolution it originally asked for. Any extra arguments after the width/height (e.g. a renderer like WEBGL) are preserved. If the sketch has neither call (rare), nothing is changed -- p5 defaults to 100x100 in that case, an acceptable degraded fallback rather than a crash.""" return _canvas_size_re.sub(lambda m: f"{m.group(1)}({size},{size}{m.group(2)})", js_code, count=1) def make_preview_iframe(js_code, canvas_id, size): js_code = fix_asset_paths(js_code) js_code = force_square_canvas(js_code, size) safe = js_code.replace("", "<\\/script>").replace("`", "\\`") return ( f'' f"

`);" f"doc.close();" f"}})();" ) def pick_random_homepage_examples(sections, count=3): pool = [] for section_name, section_data in sections.items(): for cat, examples in section_data.items(): for ex in examples: if not ex["js"].strip(): continue if _data_loading_re.search(ex["js"]): continue # skip anything that loads external assets -- too # likely to render blank/broken in a tiny decorative box pool.append({**ex, "category": cat.replace("_", " ").title()}) if len(pool) < count: return pool return random.sample(pool, count) def update_homepage_examples(repo_root, sections, fixed_picks=None): index_path = os.path.join(repo_root, "index.html") if not os.path.exists(index_path): print(f"WARNING: {index_path} not found, skipping homepage example update.") return with open(index_path) as f: html = f.read() picks = fixed_picks if fixed_picks is not None else pick_random_homepage_examples(sections, count=3) if len(picks) < 3: print(f"WARNING: only found {len(picks)} eligible examples for the homepage, expected 3.") canvas_ids = ["c1", "c2", "c3"] replaced_count = 0 card_num = 1 for canvas_id, ex in zip(canvas_ids, picks): iframe_html = make_preview_iframe(ex["js"], canvas_id, HOMEPAGE_PREVIEW_SIZE) replacement = ( f'\n' f" {iframe_html}\n" f'

{ex["name"]}

' f'

{ex["category"]} example

\n' f' ' ) # Match either the original card or an already-replaced