[{"data":1,"prerenderedAt":2425},["ShallowReactive",2],{"demos":3},[4,352,520,1082,1232,1319,1432,1523,1598,1679,1795,1925,2069,2120,2205,2352],{"id":5,"title":6,"body":7,"cover":334,"date":335,"description":336,"extension":337,"featured":338,"github":339,"image":340,"live":334,"meta":341,"navigation":338,"path":342,"seo":343,"stem":344,"tags":345,"tech":350,"__hash__":351},"demos\u002Fdemos\u002Fao-lightmapper-babylonjs.md","AO Lightmapper — BabylonJS \u002F ThreeJS",{"type":8,"value":9,"toc":326},"minimark",[10,14,30,35,38,61,69,73,79,83,92,101,105,183,186,190,205,219,228,230,234,245,250,255,297,322],[11,12,13],"p",{},"GPU-accelerated AO lightmap baker that runs entirely in the browser. Load a GLB or FBX, bake, download PNG lightmaps — no Blender, no server.",[15,16,18,19,18,25],"figure",{"style":17},"margin: 2rem 0;","\n  ",[20,21],"img",{"src":22,"alt":23,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FAO-Lightmapper\u002Fmain\u002Fassets\u002Fscreenshot.jpg","AO Lightmapper — main interface","width:100%; border-radius:12px; border:1px solid rgba(255,255,255,0.08);",[26,27,29],"figcaption",{"style":28},"text-align:center; color:#8A8F98; font-size:0.85rem; margin-top:0.6rem;","Main interface — drag a GLB, configure, bake",[31,32,34],"h2",{"id":33},"how-it-works","How it works",[11,36,37],{},"UV2 unwrap via XAtlas (WASM), then a multi-pass WebGL2 pipeline:",[39,40,41,49,55],"ol",{},[42,43,44,48],"li",{},[45,46,47],"strong",{},"G-Buffer pass"," — render world position + normals to textures",[42,50,51,54],{},[45,52,53],{},"AO pass"," — hemisphere ray sampling in tangent space per texel (WebGL2 fragment shader + BVH)",[42,56,57,60],{},[45,58,59],{},"Bilateral blur pass"," — reduces noise without losing edges",[11,62,63,64,68],{},"FBX conversion runs on a local Node server (port 3001) via ",[65,66,67],"code",{},"fbx2gltf",".",[31,70,72],{"id":71},"bake-settings","Bake settings",[11,74,75,76,68],{},"Texture size, AO samples, radius, and denoising are all configurable. 64 samples ≈ 1.2s for 1024². Supports batch processing — add files to queue, click ",[45,77,78],{},"Bake All",[31,80,82],{"id":81},"export-modes","Export modes",[84,85,86,89],"ul",{},[42,87,88],{},"Separate GLB + lightmap PNG",[42,90,91],{},"Single embedded GLB with lightmap baked into occlusion texture",[15,93,18,94,18,98],{"style":17},[20,95],{"src":96,"alt":97,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FAO-Lightmapper\u002Fblob\u002Fmain\u002Fassets\u002Fscreenshot_2.jpg?raw=true","Embedded export result",[26,99,100],{"style":28},"Embedded export — single GLB with lightmap in occlusion channel",[31,102,104],{"id":103},"apply-in-babylonjs","Apply in BabylonJS",[106,107,112],"pre",{"className":108,"code":109,"language":110,"meta":111,"style":111},"language-typescript shiki shiki-themes one-dark-pro","mesh.material.lightmapTexture = new Texture('.\u002Fbaked_AO.png', scene)\nmesh.material.useLightmapAsShadowmap = true\n","typescript","",[65,113,114,163],{"__ignoreMap":111},[115,116,119,123,126,129,131,135,139,143,147,150,154,157,160],"span",{"class":117,"line":118},"line",1,[115,120,122],{"class":121},"sU0A5","mesh",[115,124,68],{"class":125},"sn6KH",[115,127,128],{"class":121},"material",[115,130,68],{"class":125},[115,132,134],{"class":133},"sVyAn","lightmapTexture",[115,136,138],{"class":137},"sjrmR"," =",[115,140,142],{"class":141},"seHd6"," new",[115,144,146],{"class":145},"sVbv2"," Texture",[115,148,149],{"class":125},"(",[115,151,153],{"class":152},"subq3","'.\u002Fbaked_AO.png'",[115,155,156],{"class":125},", ",[115,158,159],{"class":133},"scene",[115,161,162],{"class":125},")\n",[115,164,166,168,170,172,174,177,179],{"class":117,"line":165},2,[115,167,122],{"class":121},[115,169,68],{"class":125},[115,171,128],{"class":121},[115,173,68],{"class":125},[115,175,176],{"class":133},"useLightmapAsShadowmap",[115,178,138],{"class":137},[115,180,182],{"class":181},"sVC51"," true\n",[184,185],"hr",{},[31,187,189],{"id":188},"desktop-app-electron","Desktop App (Electron)",[11,191,192,193,196,197,200,201,204],{},"Packages the full web app as a standalone ",[65,194,195],{},".exe"," (Windows) or ",[65,198,199],{},".app","\u002F",[65,202,203],{},".dmg"," (macOS). Electron starts the FBX converter backend automatically — no terminal needed.",[84,206,207,213],{},[42,208,209,210],{},"Run in dev mode: ",[65,211,212],{},"npm run desktop",[42,214,215,216],{},"Build executable: ",[65,217,218],{},"npm run pack",[220,221,222],"blockquote",{},[11,223,224,227],{},[45,225,226],{},"Windows note:"," packaging requires symlink permissions. Run as Administrator or enable Developer Mode in Windows Settings → Privacy & security → For developers.",[184,229],{},[31,231,233],{"id":232},"chrome-extension","Chrome Extension",[11,235,236,237,240,241,244],{},"Bake AO directly inside ",[45,238,239],{},"BabylonJS Sandbox"," and ",[45,242,243],{},"Three.js Editor"," — no file upload, no local server.",[220,246,247],{},[11,248,249],{},"Currently in active development. Not yet on Chrome Web Store.",[11,251,252],{},[45,253,254],{},"Supported sites:",[256,257,258,271],"table",{},[259,260,261],"thead",{},[262,263,264,268],"tr",{},[265,266,267],"th",{},"Site",[265,269,270],{},"URL",[272,273,274,287],"tbody",{},[262,275,276,279],{},[277,278,239],"td",{},[277,280,281],{},[282,283,284],"a",{"href":284,"rel":285},"https:\u002F\u002Fsandbox.babylonjs.com",[286],"nofollow",[262,288,289,291],{},[277,290,243],{},[277,292,293],{},[282,294,295],{"href":295,"rel":296},"https:\u002F\u002Fthreejs.org\u002Feditor",[286],[298,299,18,301,18,313],"div",{"style":300},"display:grid; grid-template-columns:1fr 1fr; gap:1rem; margin: 2rem 0;",[15,302,304,305,304,309,18],{"style":303},"margin:0;","\n    ",[20,306],{"src":307,"alt":308,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FAO-Lightmapper\u002Fmain\u002Fassets\u002Fbjs-chrome-extension-1.jpg","Chrome Extension in BabylonJS Sandbox",[26,310,312],{"style":311},"color:#8A8F98; font-size:0.8rem; margin-top:0.5rem;","BabylonJS Sandbox integration",[15,314,304,315,304,319,18],{"style":303},[20,316],{"src":317,"alt":318,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FAO-Lightmapper\u002Fmain\u002Fassets\u002Fbjs-chrome-extension-2.jpg","Chrome Extension in Three.js Editor",[26,320,321],{"style":311},"Three.js Editor integration",[323,324,325],"style",{},"html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":111,"searchDepth":165,"depth":165,"links":327},[328,329,330,331,332,333],{"id":33,"depth":165,"text":34},{"id":71,"depth":165,"text":72},{"id":81,"depth":165,"text":82},{"id":103,"depth":165,"text":104},{"id":188,"depth":165,"text":189},{"id":232,"depth":165,"text":233},null,"2026-06-01","Ambient Occlusion lightmap baker running in the browser. Uses BabylonJS and Three.js render-to-texture to compute per-mesh AO maps without any server-side processing.","md",true,"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FAO-Lightmapper","\u002Fimages\u002Fdemos\u002Fao-lightmapper-babylonjs.webp",{},"\u002Fdemos\u002Fao-lightmapper-babylonjs",{"title":6,"description":336},"demos\u002Fao-lightmapper-babylonjs",[346,347,348,349],"BabylonJS","ThreeJS","WebGL","GLSL","BabylonJS, ThreeJS","OWDNkP586Y23-3BSWh_UVGrOgRWaWO2Knm3R2mbk5WM",{"id":353,"title":354,"body":355,"cover":334,"date":335,"description":510,"extension":337,"featured":338,"github":511,"image":512,"live":513,"meta":514,"navigation":338,"path":515,"seo":516,"stem":517,"tags":518,"tech":346,"__hash__":519},"demos\u002Fdemos\u002Fbjs-character-controller-v2.md","BabylonJS Character Controller V2",{"type":8,"value":356,"toc":504},[357,360,368,372,387,391,429,433,442,446],[11,358,359],{},"Rebuilt from scratch after V1 had animation blending issues and tight mesh-name coupling. Works with any GLB using standard Mixamo\u002FRPM bone naming.",[15,361,18,362,18,365],{"style":17},[20,363],{"src":364,"alt":354,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FBJS_Character_Controller_V2\u002Fmain\u002Fassets\u002Fscreenshot.jpg",[26,366,367],{"style":28},"Keyboard + gamepad, blend tree, capsule physics, mobile joystick",[31,369,371],{"id":370},"architecture","Architecture",[11,373,374,375,378,379,382,383,386],{},"Two modular classes: ",[65,376,377],{},"AnimCtrl"," (skeletal animation blend tree) and ",[65,380,381],{},"CharCtrl"," (input + physics). Config consolidated in ",[65,384,385],{},"DEFAULT_CHAR_CONFIG"," — one place for all tunable values.",[31,388,390],{"id":389},"features","Features",[84,392,393,399,405,411,417,423],{},[42,394,395,398],{},[45,396,397],{},"Input",": keyboard + gamepad unified through the same action map",[42,400,401,404],{},[45,402,403],{},"Animations",": blend tree smoothly transitions Idle → Walk → Sprint with weight lerping",[42,406,407,410],{},[45,408,409],{},"Physics",": capsule collider (stable, no mesh-collider jitter), slope alignment, squash-and-stretch on landing",[42,412,413,416],{},[45,414,415],{},"Camera",": adaptive lerped follow, dynamic FOV expansion at higher speeds, mobile framing adjustments",[42,418,419,422],{},[45,420,421],{},"Mobile",": glassmorphism virtual joystick, settings persisted via localStorage",[42,424,425,428],{},[45,426,427],{},"Crouch + sprint",": operate as toggles that coexist (high-speed crouched running works)",[31,430,432],{"id":431},"asset-pipeline","Asset pipeline",[11,434,435,436,441],{},"Uses the ",[282,437,440],{"href":438,"rel":439},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FFBX2GLB-Batch-Convert-Optimizer",[286],"FBX2GLB batch tool"," — animations merged and optimized into a single compressed GLB.",[31,443,445],{"id":444},"v1-v2","V1 → V2",[256,447,448,460],{},[259,449,450],{},[262,451,452,454,457],{},[265,453],{},[265,455,456],{},"V1",[265,458,459],{},"V2",[272,461,462,472,483,493],{},[262,463,464,466,469],{},[277,465,397],{},[277,467,468],{},"Keyboard only",[277,470,471],{},"Keyboard + Gamepad",[262,473,474,477,480],{},[277,475,476],{},"Collider",[277,478,479],{},"Mesh",[277,481,482],{},"Capsule",[262,484,485,487,490],{},[277,486,403],{},[277,488,489],{},"Start\u002Fstop",[277,491,492],{},"Blend tree",[262,494,495,498,501],{},[277,496,497],{},"Coupling",[277,499,500],{},"Tight to mesh names",[277,502,503],{},"Config-driven",{"title":111,"searchDepth":165,"depth":165,"links":505},[506,507,508,509],{"id":370,"depth":165,"text":371},{"id":389,"depth":165,"text":390},{"id":431,"depth":165,"text":432},{"id":444,"depth":165,"text":445},"Full character controller in BabylonJS — keyboard + gamepad input, blend tree animations, collision, camera follow. Drop-in reusable system.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FBJS_Character_Controller_V2","\u002Fimages\u002Fdemos\u002Fbjs-character-controller-v2.webp","https:\u002F\u002Fviseni.com\u002F_demos_\u002Fbjs_character_controller_v2\u002F",{},"\u002Fdemos\u002Fbjs-character-controller-v2",{"title":354,"description":510},"demos\u002Fbjs-character-controller-v2",[346],"A3MDi6933mgfRKazdppaqcYrJQIlHtX7aM6GmnShyoE",{"id":521,"title":522,"body":523,"cover":334,"date":335,"description":1068,"extension":337,"featured":1069,"github":334,"image":537,"live":1070,"meta":1071,"navigation":338,"path":1072,"seo":1073,"stem":1074,"tags":1075,"tech":1080,"__hash__":1081},"demos\u002Fdemos\u002Fsteampunk.md","Steampunk Parallax — Gyroscope & Face Tracking",{"type":8,"value":524,"toc":1059},[525,533,542,546,591,602,606,609,724,731,735,750,847,850,854,861,910,921,925,928,1015,1019,1037,1040,1044,1051,1056],[11,526,527,528,532],{},"AI-generated steampunk cityscape — ",[529,530,531],"em",{},"Aetherpunk, Ciudad de los Engranajes Eternos"," — deconstructed into 5 depth layers. On desktop the webcam tracks your face; on mobile the gyroscope drives the parallax. No framework, no build step.",[15,534,18,535,18,539],{"style":17},[20,536],{"src":537,"alt":538,"style":24},"https:\u002F\u002Fwww.viseni.com\u002F_demos_\u002Fsteampunk\u002Fimages\u002Fsteampunk.webp","Aetherpunk — steampunk parallax demo",[26,540,541],{"style":28},"AI-generated illustration split into 5 parallax depth planes",[31,543,545],{"id":544},"input-modes","Input modes",[256,547,548,561],{},[259,549,550],{},[262,551,552,555,558],{},[265,553,554],{},"Platform",[265,556,557],{},"Primary",[265,559,560],{},"Fallback",[272,562,563,578],{},[262,564,565,568,575],{},[277,566,567],{},"Desktop",[277,569,570,571,574],{},"Webcam face tracking (",[65,572,573],{},"face-api.js"," → nose tip)",[277,576,577],{},"Skin-blob detector → mouse",[262,579,580,582,588],{},[277,581,421],{},[277,583,584,587],{},[65,585,586],{},"DeviceOrientationEvent"," gyroscope",[277,589,590],{},"Touch drag",[11,592,593,594,597,598,601],{},"All modes converge on ",[65,595,596],{},"tgtX \u002F tgtY"," values that feed the same ",[65,599,600],{},"lerp → transform3d"," pipeline.",[31,603,605],{"id":604},"layer-stack","Layer stack",[11,607,608],{},"5 planes with independent depth multipliers. Each PNG was chroma-keyed in the browser at load time — no pre-processed PNGs needed:",[106,610,614],{"className":611,"code":612,"language":613,"meta":111,"style":111},"language-js shiki shiki-themes one-dark-pro","const LAYERS_CFG = [\n  { depth: 0.00 },  \u002F\u002F background — static\n  { depth: 0.12 },  \u002F\u002F distant buildings\n  { depth: 0.38 },  \u002F\u002F mid city\n  { depth: 0.72 },  \u002F\u002F foreground structures\n  { depth: 1.00 },  \u002F\u002F closest — maximum shift\n]\n","js",[65,615,616,629,650,667,684,701,718],{"__ignoreMap":111},[115,617,618,621,624,626],{"class":117,"line":118},[115,619,620],{"class":141},"const",[115,622,623],{"class":121}," LAYERS_CFG",[115,625,138],{"class":137},[115,627,628],{"class":125}," [\n",[115,630,631,634,637,640,643,646],{"class":117,"line":165},[115,632,633],{"class":125},"  { ",[115,635,636],{"class":133},"depth",[115,638,639],{"class":125},": ",[115,641,642],{"class":181},"0.00",[115,644,645],{"class":125}," },  ",[115,647,649],{"class":648},"sV9Aq","\u002F\u002F background — static\n",[115,651,653,655,657,659,662,664],{"class":117,"line":652},3,[115,654,633],{"class":125},[115,656,636],{"class":133},[115,658,639],{"class":125},[115,660,661],{"class":181},"0.12",[115,663,645],{"class":125},[115,665,666],{"class":648},"\u002F\u002F distant buildings\n",[115,668,670,672,674,676,679,681],{"class":117,"line":669},4,[115,671,633],{"class":125},[115,673,636],{"class":133},[115,675,639],{"class":125},[115,677,678],{"class":181},"0.38",[115,680,645],{"class":125},[115,682,683],{"class":648},"\u002F\u002F mid city\n",[115,685,687,689,691,693,696,698],{"class":117,"line":686},5,[115,688,633],{"class":125},[115,690,636],{"class":133},[115,692,639],{"class":125},[115,694,695],{"class":181},"0.72",[115,697,645],{"class":125},[115,699,700],{"class":648},"\u002F\u002F foreground structures\n",[115,702,704,706,708,710,713,715],{"class":117,"line":703},6,[115,705,633],{"class":125},[115,707,636],{"class":133},[115,709,639],{"class":125},[115,711,712],{"class":181},"1.00",[115,714,645],{"class":125},[115,716,717],{"class":648},"\u002F\u002F closest — maximum shift\n",[115,719,721],{"class":117,"line":720},7,[115,722,723],{"class":125},"]\n",[11,725,726,727,730],{},"Layers are drawn onto ",[65,728,729],{},"\u003Ccanvas>"," elements with cover-scale so they fill the viewport at any aspect ratio. On resize they redraw from the cached chroma-keyed source.",[31,732,734],{"id":733},"face-tracking","Face tracking",[11,736,737,738,745,746,749],{},"Uses ",[282,739,742],{"href":740,"rel":741},"https:\u002F\u002Fgithub.com\u002Fvladmandic\u002Fface-api",[286],[65,743,744],{},"@vladmandic\u002Fface-api"," (TinyFaceDetector + 68-point landmarks) loaded from CDN. Landmark 30 (nose tip) drives ",[65,747,748],{},"tgtX\u002FY"," — most stable point for translation tracking.",[106,751,753],{"className":611,"code":752,"language":613,"meta":111,"style":111},"const result = await faceapi\n  .detectSingleFace(videoEl, FA_OPTS)\n  .withFaceLandmarks(true)          \u002F\u002F tiny 68-point model\n\nconst nose = result.landmarks.positions[30]\n\u002F\u002F EMA-smooth the raw detection, then offset from calibration baseline\n",[65,754,755,770,790,808,813,842],{"__ignoreMap":111},[115,756,757,759,762,764,767],{"class":117,"line":118},[115,758,620],{"class":141},[115,760,761],{"class":121}," result",[115,763,138],{"class":137},[115,765,766],{"class":141}," await",[115,768,769],{"class":133}," faceapi\n",[115,771,772,775,778,780,783,785,788],{"class":117,"line":165},[115,773,774],{"class":125},"  .",[115,776,777],{"class":145},"detectSingleFace",[115,779,149],{"class":125},[115,781,782],{"class":133},"videoEl",[115,784,156],{"class":125},[115,786,787],{"class":121},"FA_OPTS",[115,789,162],{"class":125},[115,791,792,794,797,799,802,805],{"class":117,"line":652},[115,793,774],{"class":125},[115,795,796],{"class":145},"withFaceLandmarks",[115,798,149],{"class":125},[115,800,801],{"class":181},"true",[115,803,804],{"class":125},")          ",[115,806,807],{"class":648},"\u002F\u002F tiny 68-point model\n",[115,809,810],{"class":117,"line":669},[115,811,812],{"emptyLinePlaceholder":338},"\n",[115,814,815,817,820,822,824,826,829,831,834,837,840],{"class":117,"line":686},[115,816,620],{"class":141},[115,818,819],{"class":121}," nose",[115,821,138],{"class":137},[115,823,761],{"class":121},[115,825,68],{"class":125},[115,827,828],{"class":121},"landmarks",[115,830,68],{"class":125},[115,832,833],{"class":133},"positions",[115,835,836],{"class":125},"[",[115,838,839],{"class":181},"30",[115,841,723],{"class":125},[115,843,844],{"class":117,"line":703},[115,845,846],{"class":648},"\u002F\u002F EMA-smooth the raw detection, then offset from calibration baseline\n",[11,848,849],{},"Falls back to a YCrCb skin-blob detector (no ML model needed) if face-api fails, then to mouse.",[31,851,853],{"id":852},"gyroscope","Gyroscope",[11,855,856,857,860],{},"Heavy EMA (",[65,858,859],{},"α = 0.07",") kills sensor noise. Rate limiting (max 6°\u002Fsample) blocks gimbal-lock spikes near β = ±90°. Smooth dead zone of ±5° absorbs hand tremor.",[106,862,864],{"className":611,"code":863,"language":613,"meta":111,"style":111},"\u002F\u002F Smooth recalibration — baseline drifts toward current position via EMA\n\u002F\u002F so parallax fades to zero gradually instead of jumping\ngyroBaseGamma += (recalTarget.gamma - gyroBaseGamma) * GYRO_RECAL_SPD\n",[65,865,866,871,876],{"__ignoreMap":111},[115,867,868],{"class":117,"line":118},[115,869,870],{"class":648},"\u002F\u002F Smooth recalibration — baseline drifts toward current position via EMA\n",[115,872,873],{"class":117,"line":165},[115,874,875],{"class":648},"\u002F\u002F so parallax fades to zero gradually instead of jumping\n",[115,877,878,881,884,887,890,892,895,898,901,904,907],{"class":117,"line":652},[115,879,880],{"class":133},"gyroBaseGamma",[115,882,883],{"class":137}," +=",[115,885,886],{"class":125}," (",[115,888,889],{"class":121},"recalTarget",[115,891,68],{"class":125},[115,893,894],{"class":133},"gamma",[115,896,897],{"class":137}," -",[115,899,900],{"class":133}," gyroBaseGamma",[115,902,903],{"class":125},") ",[115,905,906],{"class":137},"*",[115,908,909],{"class":121}," GYRO_RECAL_SPD\n",[11,911,912,913,916,917,920],{},"Orientation remapping handles landscape rotation (",[65,914,915],{},"screen.orientation.angle"," 90°\u002F270°). iOS permission (",[65,918,919],{},"DeviceOrientationEvent.requestPermission",") gated behind the splash button.",[31,922,924],{"id":923},"zeppelins","Zeppelins",[11,926,927],{},"3 airship sprites fly across independent depth planes (z-index 15 and 25, between layer 4 and layer 3). Each respawns off-screen after a random delay, with Y-separation enforcement to avoid overlap:",[106,929,931],{"className":611,"code":930,"language":613,"meta":111,"style":111},"\u002F\u002F Gentle vertical bob synced to a per-zeppelin phase offset\nconst bobY = Math.sin(t * 0.4 + z.bob) * 4\n\u002F\u002F Parallax offset from head\u002Fgyro tracking applied on top\nconst px = curX * z.plane.depth\n",[65,932,933,938,984,989],{"__ignoreMap":111},[115,934,935],{"class":117,"line":118},[115,936,937],{"class":648},"\u002F\u002F Gentle vertical bob synced to a per-zeppelin phase offset\n",[115,939,940,942,945,947,950,952,955,957,960,963,966,969,972,974,977,979,981],{"class":117,"line":165},[115,941,620],{"class":141},[115,943,944],{"class":121}," bobY",[115,946,138],{"class":137},[115,948,949],{"class":121}," Math",[115,951,68],{"class":125},[115,953,954],{"class":145},"sin",[115,956,149],{"class":125},[115,958,959],{"class":133},"t",[115,961,962],{"class":137}," *",[115,964,965],{"class":181}," 0.4",[115,967,968],{"class":137}," +",[115,970,971],{"class":121}," z",[115,973,68],{"class":125},[115,975,976],{"class":133},"bob",[115,978,903],{"class":125},[115,980,906],{"class":137},[115,982,983],{"class":181}," 4\n",[115,985,986],{"class":117,"line":652},[115,987,988],{"class":648},"\u002F\u002F Parallax offset from head\u002Fgyro tracking applied on top\n",[115,990,991,993,996,998,1001,1003,1005,1007,1010,1012],{"class":117,"line":669},[115,992,620],{"class":141},[115,994,995],{"class":121}," px",[115,997,138],{"class":137},[115,999,1000],{"class":133}," curX",[115,1002,962],{"class":137},[115,1004,971],{"class":121},[115,1006,68],{"class":125},[115,1008,1009],{"class":121},"plane",[115,1011,68],{"class":125},[115,1013,1014],{"class":133},"depth\n",[31,1016,1018],{"id":1017},"particles","Particles",[84,1020,1021,1031],{},[42,1022,1023,1026,1027,1030],{},[45,1024,1025],{},"Steam"," — radial-gradient blobs spawned from three pipe clusters (left, right, center-bottom), CSS ",[65,1028,1029],{},"@keyframes"," rise + expand + fade",[42,1032,1033,1036],{},[45,1034,1035],{},"Embers"," — tiny glowing dots launched from furnace areas at ~12 fps, die after 2–6 s",[11,1038,1039],{},"Both are pure DOM elements with inline CSS custom properties — no canvas particle system.",[31,1041,1043],{"id":1042},"ai-generated-content","AI-generated content",[11,1045,1046,1047,1050],{},"Illustration and layer separation created with generative AI. Layers were refined post-generation for clean alpha at depth boundaries. The chroma-key pass (",[65,1048,1049],{},"G > R×1.35 && G > B×1.35",") removes green-screen backing added during generation.",[220,1052,1053],{},[11,1054,1055],{},"Experimental demo — no GitHub repo.",[323,1057,1058],{},"html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html pre.shiki code .sV9Aq, html code.shiki .sV9Aq{--shiki-default:#7F848E;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}",{"title":111,"searchDepth":165,"depth":165,"links":1060},[1061,1062,1063,1064,1065,1066,1067],{"id":544,"depth":165,"text":545},{"id":604,"depth":165,"text":605},{"id":733,"depth":165,"text":734},{"id":852,"depth":165,"text":853},{"id":923,"depth":165,"text":924},{"id":1017,"depth":165,"text":1018},{"id":1042,"depth":165,"text":1043},"AI-generated steampunk cityscape split into 5 parallax depth layers. Face tracking via webcam on desktop, gyroscope on mobile. Zeppelins, steam particles, and embers animate independently on each plane.",false,"https:\u002F\u002Fviseni.com\u002F_demos_\u002Fsteampunk\u002F",{},"\u002Fdemos\u002Fsteampunk",{"title":522,"description":1068},"demos\u002Fsteampunk",[1076,1077,1078,1079,573],"JavaScript","CSS","WebAPI","Generative AI","JavaScript, CSS","49G0v_drReDTrfYz6r86UR2SFeKav9swvJV96aLFWEI",{"id":1083,"title":1084,"body":1085,"cover":334,"date":1222,"description":1223,"extension":337,"featured":338,"github":438,"image":1224,"live":334,"meta":1225,"navigation":338,"path":1226,"seo":1227,"stem":1228,"tags":1229,"tech":1230,"__hash__":1231},"demos\u002Fdemos\u002Ffbx2glb-batch-converter.md","FBX to GLB Batch Converter & Optimizer",{"type":8,"value":1086,"toc":1215},[1087,1090,1094,1123,1127,1141,1145,1183,1187,1194,1198,1212],[11,1088,1089],{},"Convert a folder of FBX files to web-ready GLB in one CLI command. Built to solve the manual conversion bottleneck in 3D pipelines targeting BabylonJS or ThreeJS viewers.",[31,1091,1093],{"id":1092},"what-it-does","What it does",[84,1095,1096,1103,1109,1116],{},[42,1097,1098,1099,1102],{},"Walks a directory recursively for ",[65,1100,1101],{},".fbx"," files",[42,1104,1105,1106,1108],{},"Converts each with ",[65,1107,67],{}," → intermediate GLTF",[42,1110,1111,1112,1115],{},"Optimizes with ",[65,1113,1114],{},"gltf-transform",": Draco mesh compression, KTX2\u002FWebP textures, animation pruning, dedup\u002Fflatten\u002Fprune",[42,1117,1118,1119,1122],{},"Outputs ",[65,1120,1121],{},".glb"," alongside originals or to a target dir",[31,1124,1126],{"id":1125},"also-includes","Also includes",[84,1128,1129,1135],{},[42,1130,1131,1134],{},[45,1132,1133],{},"Animation merger"," — combines animations from a source GLB into a target character GLB, with Mixamo-to-UE\u002FUnity retargeting support",[42,1136,1137,1140],{},[45,1138,1139],{},"Optimize script"," — standalone post-process pass for already-converted files",[31,1142,1144],{"id":1143},"usage","Usage",[106,1146,1150],{"className":1147,"code":1148,"language":1149,"meta":111,"style":111},"language-bash shiki shiki-themes one-dark-pro","npm install -g fbx2glb-batch\nfbx2glb .\u002Fmodels .\u002Foutput --draco --webp-textures\n","bash",[65,1151,1152,1166],{"__ignoreMap":111},[115,1153,1154,1157,1160,1163],{"class":117,"line":118},[115,1155,1156],{"class":145},"npm",[115,1158,1159],{"class":152}," install",[115,1161,1162],{"class":181}," -g",[115,1164,1165],{"class":152}," fbx2glb-batch\n",[115,1167,1168,1171,1174,1177,1180],{"class":117,"line":165},[115,1169,1170],{"class":145},"fbx2glb",[115,1172,1173],{"class":152}," .\u002Fmodels",[115,1175,1176],{"class":152}," .\u002Foutput",[115,1178,1179],{"class":181}," --draco",[115,1181,1182],{"class":181}," --webp-textures\n",[31,1184,1186],{"id":1185},"results-real-project","Results (real project)",[11,1188,1189,1190,1193],{},"Average ",[45,1191,1192],{},"~87% size reduction"," vs. original FBX. A 210 MB environment scene → 18 MB GLB with Draco + WebP.",[31,1195,1197],{"id":1196},"_3dsmax-export-tips","3DsMax export tips",[84,1199,1200,1203,1206,1209],{},[42,1201,1202],{},"Collapse all modifiers, Reset Xform",[42,1204,1205],{},"Bake materials to single texture per slot",[42,1207,1208],{},"Export with Y-up axis",[42,1210,1211],{},"Disable \"Embed Media\"",[323,1213,1214],{},"html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":111,"searchDepth":165,"depth":165,"links":1216},[1217,1218,1219,1220,1221],{"id":1092,"depth":165,"text":1093},{"id":1125,"depth":165,"text":1126},{"id":1143,"depth":165,"text":1144},{"id":1185,"depth":165,"text":1186},{"id":1196,"depth":165,"text":1197},"2026-05-30","NodeJS tool to batch-convert FBX files to optimized GLB. Wraps fbx2gltf + gltf-transform to compress geometry, textures and animations in one pass.","\u002Fimages\u002Fdemos\u002Ffbx2glb-batch-converter.webp",{},"\u002Fdemos\u002Ffbx2glb-batch-converter",{"title":1084,"description":1223},"demos\u002Ffbx2glb-batch-converter",[1230,348,346,347],"3DsMax","zlCA19nm6drRxOjewl4QSzl5tHrWb5hz4BMbiHTkE0c",{"id":1233,"title":1234,"body":1235,"cover":334,"date":1308,"description":1309,"extension":337,"featured":1069,"github":1310,"image":1311,"live":1312,"meta":1313,"navigation":338,"path":1314,"seo":1315,"stem":1316,"tags":1317,"tech":346,"__hash__":1318},"demos\u002Fdemos\u002Fbabylonjs-scroll-navigation-tico.md","TICO — Scroll-Driven 3D Navigation with BabylonJS",{"type":8,"value":1236,"toc":1303},[1237,1240,1246,1248,1254,1265,1267,1286,1290,1297],[11,1238,1239],{},"Scroll position scrubs a 3D camera animation authored in 3DsMax. The camera path, timing, and easing are all designed in the DCC tool — BabylonJS just drives playback based on scroll progress.",[11,1241,1242],{},[20,1243],{"alt":1244,"src":1245},"Tico Screenshot","https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FTico_Scroll_Navigation\u002Fmain\u002Fresources\u002Ftico.webp",[31,1247,34],{"id":33},[11,1249,1250,1251,68],{},"The animation is a standard 3DsMax Path Constraint baked to keyframes on export. BabylonJS loads the GLB, pauses the animation group, and maps scroll progress (0–1) to the animation timeline via ",[65,1252,1253],{},"goToFrame()",[11,1255,1256,1257,1260,1261,1264],{},"Scroll events are lerped on each RAF for smooth scrubbing. Touch is supported via ",[65,1258,1259],{},"touchmove"," → ",[65,1262,1263],{},"scrollBy"," delegation.",[31,1266,371],{"id":370},[11,1268,1269,1270,1273,1274,1277,1278,1281,1282,1285],{},"Two files: ",[65,1271,1272],{},"main.js"," (scene setup, scroll binding, main variables) and ",[65,1275,1276],{},"navigation.js"," (scroll-position tracking tied to ",[65,1279,1280],{},"scroll-sections"," DOM elements). Section count must match ",[65,1283,1284],{},"scrollSteps"," values in config.",[31,1287,1289],{"id":1288},"key-insight","Key insight",[11,1291,1292,1293,1296],{},"Default ease-in\u002Fease-out keyframe tangents create weird acceleration when scrolling backwards. Fix: set all keyframes to ",[45,1294,1295],{},"Linear"," tangents in 3DsMax before export.",[11,1298,1299,1300,1302],{},"GLB is 2.1 MB (original 3DsMax scene: 340 MB). Animation baked at 30fps — halves ",[65,1301,1253],{}," evaluation cost with no visible quality loss.",{"title":111,"searchDepth":165,"depth":165,"links":1304},[1305,1306,1307],{"id":33,"depth":165,"text":34},{"id":370,"depth":165,"text":371},{"id":1288,"depth":165,"text":1289},"2026-05-28","A BabylonJS demo where scrolling drives a camera through a 3DsMax-authored animation exported to GLB. No CSS tricks — pure 3D timeline scrubbing.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FTico_Scroll_Navigation","\u002Fimages\u002Fdemos\u002Fbabylonjs-scroll-navigation-tico.webp","https:\u002F\u002Fviseni.com\u002F_demos_\u002Ftico",{},"\u002Fdemos\u002Fbabylonjs-scroll-navigation-tico",{"title":1234,"description":1309},"demos\u002Fbabylonjs-scroll-navigation-tico",[346,1230],"rN2CiRDFuOxW5wjf7YW43itts8WlJanLkddFEtGXAyk",{"id":1320,"title":1321,"body":1322,"cover":334,"date":1421,"description":1422,"extension":337,"featured":338,"github":1423,"image":1424,"live":1425,"meta":1426,"navigation":338,"path":1427,"seo":1428,"stem":1429,"tags":1430,"tech":346,"__hash__":1431},"demos\u002Fdemos\u002Fbabylonjs-character-navigation-ptn.md","BabylonJS Point-to-Navigate Character (PTN)",{"type":8,"value":1323,"toc":1416},[1324,1327,1329,1340,1343,1347,1364,1368],[11,1325,1326],{},"Click a point on the ground → character pathfinds and walks there. Common pattern in RPGs, strategy games, and architectural walkthroughs. Touch-friendly — tap = destination.",[31,1328,34],{"id":33},[11,1330,1331,1332,1335,1336,1339],{},"Scene picks ground mesh on pointer down, passes hit point to Recast crowd agent via ",[65,1333,1334],{},"agentGoto()",". Agent velocity drives animation state (idle\u002Fwalk) and character rotation via ",[65,1337,1338],{},"LerpAngle"," each frame.",[11,1341,1342],{},"Visual feedback: a pulsing emissive disc marker placed at the click point.",[31,1344,1346],{"id":1345},"pathfinding","Pathfinding",[11,1348,1349,1350,1353,1354,1357,1358,1360,1361,1363],{},"Recast navmesh built from the ground mesh at scene load. ",[65,1351,1352],{},"RecastJSPlugin"," + ",[65,1355,1356],{},"createCrowd()"," handles obstacle avoidance and path smoothing. Implementation logic in ",[65,1359,1272],{}," in the ",[65,1362,613],{}," folder.",[31,1365,1367],{"id":1366},"ptn-vs-wasd","PTN vs WASD",[256,1369,1370,1382],{},[259,1371,1372],{},[262,1373,1374,1376,1379],{},[265,1375],{},[265,1377,1378],{},"PTN",[265,1380,1381],{},"WASD",[272,1383,1384,1395,1405],{},[262,1385,1386,1389,1392],{},[277,1387,1388],{},"Feel",[277,1390,1391],{},"Strategic, deliberate",[277,1393,1394],{},"Immediate",[262,1396,1397,1399,1402],{},[277,1398,1346],{},[277,1400,1401],{},"Required",[277,1403,1404],{},"Not needed",[262,1406,1407,1410,1413],{},[277,1408,1409],{},"Touch",[277,1411,1412],{},"Natural (tap = destination)",[277,1414,1415],{},"Needs virtual stick",{"title":111,"searchDepth":165,"depth":165,"links":1417},[1418,1419,1420],{"id":33,"depth":165,"text":34},{"id":1345,"depth":165,"text":1346},{"id":1366,"depth":165,"text":1367},"2026-05-15","Click anywhere on the ground to move — point-to-navigate character controller in BabylonJS. Pathfinding via Recast, smooth rotation, blend animations.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fbabylonjs-character-navigation-ptn","\u002Fimages\u002Fdemos\u002Fbabylonjs-character-navigation-ptn.webp","https:\u002F\u002Fviseni.com\u002F_demos_\u002Fptn_navigation\u002F",{},"\u002Fdemos\u002Fbabylonjs-character-navigation-ptn",{"title":1321,"description":1422},"demos\u002Fbabylonjs-character-navigation-ptn",[346],"xM4kFYdkG8LU5S1cRtFq2SuyoVehM2eHhPUFpFjXny4",{"id":1433,"title":1434,"body":1435,"cover":334,"date":1512,"description":1513,"extension":337,"featured":1069,"github":1514,"image":1515,"live":1516,"meta":1517,"navigation":338,"path":1518,"seo":1519,"stem":1520,"tags":1521,"tech":346,"__hash__":1522},"demos\u002Fdemos\u002Fbabylonjs-readyplayerme-animation-combiner.md","Combining Animations on ReadyPlayerMe Characters — BabylonJS",{"type":8,"value":1436,"toc":1506},[1437,1444,1448,1455,1462,1466,1477,1481,1492,1496],[11,1438,1439,1440,1443],{},"By default, starting a second ",[65,1441,1442],{},"AnimationGroup"," stops the first — they fight over the same bone transforms. This demo shows how to play walk + wave + talking simultaneously on a ReadyPlayerMe avatar.",[31,1445,1447],{"id":1446},"the-technique","The technique",[11,1449,1450,1451,1454],{},"Clone each animation group and remove ",[65,1452,1453],{},"targetedAnimations"," for bones you don't want it to control. Walk drives lower body only, wave drives upper body only, talking drives jaw\u002Fface only. Each group runs independently with no conflicts.",[11,1456,1457,1458,1461],{},"Transitions between states use ",[65,1459,1460],{},"setWeightForAllAnimatables()"," lerped over time on each RAF.",[31,1463,1465],{"id":1464},"animations-source","Animations source",[11,1467,435,1468,1473,1474,68],{},[282,1469,1472],{"href":1470,"rel":1471},"https:\u002F\u002Fgithub.com\u002Freadyplayerme\u002Fanimation-library",[286],"ReadyPlayerMe animation library",", converted from FBX to GLB via the ",[282,1475,440],{"href":438,"rel":1476},[286],[31,1478,1480],{"id":1479},"rpm-export-requirements","RPM export requirements",[84,1482,1483,1486,1489],{},[42,1484,1485],{},"T-pose as frame 0 (reference frame for blending)",[42,1487,1488],{},"All clips in a single GLB",[42,1490,1491],{},"Standard Mixamo bone naming",[31,1493,1495],{"id":1494},"common-issue","Common issue",[11,1497,1498,1501,1502,1505],{},[45,1499,1500],{},"Hands snapping"," = two groups both targeting ",[65,1503,1504],{},"LeftHand"," with no masking. Bone-name filtering in the mask prevents this. Cleaner alternative when you control the asset: export separate clips per body region from the DCC tool.",{"title":111,"searchDepth":165,"depth":165,"links":1507},[1508,1509,1510,1511],{"id":1446,"depth":165,"text":1447},{"id":1464,"depth":165,"text":1465},{"id":1479,"depth":165,"text":1480},{"id":1494,"depth":165,"text":1495},"2026-04-15","How to layer and blend multiple AnimationGroups on a ReadyPlayerMe avatar in BabylonJS. Walk cycle + upper body wave + facial expression, all simultaneously.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fbabylonjs-ReadyPlayerMe-Animation-Combiner","\u002Fimages\u002Fdemos\u002Fbabylonjs-readyplayerme-animation-combiner.webp","https:\u002F\u002Fviseni.com\u002F_demos_\u002Freadyplayer_anim\u002F",{},"\u002Fdemos\u002Fbabylonjs-readyplayerme-animation-combiner",{"title":1434,"description":1513},"demos\u002Fbabylonjs-readyplayerme-animation-combiner",[346],"SpCGC69QcbqDcFPUISqiLVmlg6j4Vs6iePUpqAYbsc0",{"id":1524,"title":1525,"body":1526,"cover":334,"date":1587,"description":1588,"extension":337,"featured":1069,"github":1589,"image":1590,"live":1591,"meta":1592,"navigation":338,"path":1593,"seo":1594,"stem":1595,"tags":1596,"tech":346,"__hash__":1597},"demos\u002Fdemos\u002Freadyplayer-talk-babylonjs.md","ReadyPlayerMe Talking Character — BabylonJS",{"type":8,"value":1527,"toc":1582},[1528,1539,1541,1554,1564,1566,1572,1576,1579],[11,1529,1530,1531,1534,1535,1538],{},"A ReadyPlayerMe avatar that talks in sync with audio. Two inputs drive the mouth: a looped talking animation from the RPM animation library, and Web Audio ",[65,1532,1533],{},"AnalyserNode"," RMS amplitude mapped to the ",[65,1536,1537],{},"jawOpen"," ARKit morph target in real-time.",[31,1540,34],{"id":33},[11,1542,1543,1544,1547,1548,1550,1551,1553],{},"Load the RPM avatar with ",[65,1545,1546],{},"?morphTargets=ARKit&lod=1"," — this adds 52 ARKit blend shapes including ",[65,1549,1537],{},", lip shapes, and eye blinks. RMS amplitude (0–1) from the audio stream drives ",[65,1552,1537],{}," influence each frame, smoothed with a lerp to prevent jitter.",[11,1555,1556,1557,200,1560,1563],{},"Idle expressions layer on top: blinks every 3–5 seconds using ",[65,1558,1559],{},"eyeBlinkLeft",[65,1561,1562],{},"eyeBlinkRight"," morph targets animated via RAF callbacks.",[31,1565,1465],{"id":1464},[11,1567,1568,1569,68],{},"Animations converted from FBX to GLB via the ",[282,1570,440],{"href":438,"rel":1571},[286],[31,1573,1575],{"id":1574},"for-production","For production",[11,1577,1578],{},"Replace RMS amplitude with phoneme\u002Fviseme timestamps from a TTS service for accurate lip shapes per sound. RMS is the simplest path to a speaking avatar without full phoneme recognition.",[11,1580,1581],{},"32 stars — mostly people building AI chatbot avatars.",{"title":111,"searchDepth":165,"depth":165,"links":1583},[1584,1585,1586],{"id":33,"depth":165,"text":34},{"id":1464,"depth":165,"text":1465},{"id":1574,"depth":165,"text":1575},"2026-03-20","Driving lip sync and talking animations on a ReadyPlayerMe avatar in BabylonJS. Procedural jaw animation driven by audio amplitude + idle expression blending.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Freadyplayer-talk","\u002Fimages\u002Fdemos\u002Freadyplayer-talk-babylonjs.webp","https:\u002F\u002Fviseni.com\u002F_demos_\u002Freadyplayer_talk\u002F",{},"\u002Fdemos\u002Freadyplayer-talk-babylonjs",{"title":1525,"description":1588},"demos\u002Freadyplayer-talk-babylonjs",[346],"TRxZLjBp0E0VJ8CjYGN8FAS9fOVaTgmL0-DOk1I_sNA",{"id":1599,"title":1600,"body":1601,"cover":334,"date":1668,"description":1669,"extension":337,"featured":1069,"github":1670,"image":1671,"live":1672,"meta":1673,"navigation":338,"path":1674,"seo":1675,"stem":1676,"tags":1677,"tech":346,"__hash__":1678},"demos\u002Fdemos\u002Fbabylonjs-character-navigation.md","Character Navigation Demo (OLD) — BabylonJS 6",{"type":8,"value":1602,"toc":1663},[1603,1612,1619,1623,1636,1640,1648,1652],[220,1604,1605],{},[11,1606,1607,1608,1611],{},"Older version. See ",[282,1609,354],{"href":1610},"\u002Fbjs-character-controller-v2"," for the rewrite with physics, gamepad, and blend tree.",[11,1613,1614,1615,1618],{},"Third-person controller with WASD\u002Farrow key movement, Space to jump, and a mobile virtual stick. Uses ",[65,1616,1617],{},"MoveWithCollisions"," + simulated gravity — no physics engine.",[31,1620,1622],{"id":1621},"code-structure","Code structure",[11,1624,1625,1626,639,1629,1631,1632,1635],{},"Two files in ",[65,1627,1628],{},"js\u002F",[65,1630,1272],{}," (scene, camera, input) and ",[65,1633,1634],{},"controller.js"," (movement logic, animation state). Character is a GLB with Idle, Walk, Run, and Jump clips. Navmesh exported from 3DsMax as a separate GLB.",[31,1637,1639],{"id":1638},"controls","Controls",[84,1641,1642,1645],{},[42,1643,1644],{},"Desktop: WASD \u002F arrow keys + Space to jump",[42,1646,1647],{},"Mobile: left stick (move), right stick (rotate), button (jump)",[31,1649,1651],{"id":1650},"roadmap-from-readme","Roadmap (from README)",[84,1653,1654,1657,1660],{},[42,1655,1656],{},"Jump and falling animations",[42,1658,1659],{},"Double jump",[42,1661,1662],{},"Better 3D environment scene",{"title":111,"searchDepth":165,"depth":165,"links":1664},[1665,1666,1667],{"id":1621,"depth":165,"text":1622},{"id":1638,"depth":165,"text":1639},{"id":1650,"depth":165,"text":1651},"2026-02-10","Third-person character controller in BabylonJS 6. Click-to-move on a navmesh, root motion from GLB animations, smooth rotation toward movement direction.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fbabylonjs-character-navigation","\u002Fimages\u002Fdemos\u002Fbabylonjs-character-navigation.webp","https:\u002F\u002Fwww.viseni.com\u002F_demos_\u002Fcharacter_navigation",{},"\u002Fdemos\u002Fbabylonjs-character-navigation",{"title":1600,"description":1669},"demos\u002Fbabylonjs-character-navigation",[346,1230],"sXzML46APR2RTOcM_WfoSOL1rz25Wk2QLocTpv7R6Oo",{"id":1680,"title":1681,"body":1682,"cover":334,"date":1783,"description":1784,"extension":337,"featured":1069,"github":1785,"image":1786,"live":334,"meta":1787,"navigation":338,"path":1788,"seo":1789,"stem":1790,"tags":1791,"tech":1792,"__hash__":1794},"demos\u002Fdemos\u002Fwifi-manager-micropython.md","WiFi Manager for MicroPython (Pico W \u002F ESP32)",{"type":8,"value":1683,"toc":1778},[1684,1691,1700,1702,1722,1724,1746,1752,1756,1767,1775],[11,1685,1686,1687,1690],{},"Drop-in WiFi management for MicroPython projects. On first boot (or when saved network is unavailable) the device starts an Access Point with a captive portal — user connects, selects SSID, enters password. Credentials saved to ",[65,1688,1689],{},"wifi_credentials.json"," on flash.",[15,1692,18,1693,18,1697],{"style":17},[20,1694],{"src":1695,"alt":1696,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FWifiManager_MicroPython\u002Fmain\u002Fimages\u002Fwifi_config.jpg","WiFi captive portal on mobile",[26,1698,1699],{"style":28},"Captive portal — connects from any phone browser, no app needed",[31,1701,390],{"id":389},[84,1703,1704,1707,1710,1713,1719],{},[42,1705,1706],{},"Stores up to 5 networks, connects to strongest available",[42,1708,1709],{},"Checks connection every 20 seconds, auto-reconnects on drop",[42,1711,1712],{},"AP mode resets after 120 seconds of inactivity",[42,1714,1715,1716],{},"Config portal built in HTML\u002FCSS\u002FJS — customizable ",[65,1717,1718],{},"config_page.html",[42,1720,1721],{},"Optional TFT display integration",[31,1723,1144],{"id":1143},[106,1725,1729],{"className":1726,"code":1727,"language":1728,"meta":111,"style":111},"language-python shiki shiki-themes one-dark-pro","import wifimanager\nwifimanager.connect_wifi()   # try saved networks\nwifimanager.ap_mode()        # or start portal\n","python",[65,1730,1731,1736,1741],{"__ignoreMap":111},[115,1732,1733],{"class":117,"line":118},[115,1734,1735],{},"import wifimanager\n",[115,1737,1738],{"class":117,"line":165},[115,1739,1740],{},"wifimanager.connect_wifi()   # try saved networks\n",[115,1742,1743],{"class":117,"line":652},[115,1744,1745],{},"wifimanager.ap_mode()        # or start portal\n",[11,1747,1748,1751],{},[65,1749,1750],{},"boot.py"," in the repo shows the standard pattern: try connect → fallback to AP.",[31,1753,1755],{"id":1754},"tested-on","Tested on",[84,1757,1758,1761,1764],{},[42,1759,1760],{},"Raspberry Pi Pico W (RP2040 + CYW43439)",[42,1762,1763],{},"ESP32 (generic + TTGO T-Display)",[42,1765,1766],{},"MicroPython 1.20+",[11,1768,1769,1770,1774],{},"Used in ",[282,1771,1773],{"href":1772},"\u002Fcryptodash-picow","CryptoDash Pico W"," for first-boot setup.",[323,1776,1777],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":111,"searchDepth":165,"depth":165,"links":1779},[1780,1781,1782],{"id":389,"depth":165,"text":390},{"id":1143,"depth":165,"text":1144},{"id":1754,"depth":165,"text":1755},"2026-01-15","Drop-in WiFi manager script for MicroPython microcontrollers. Captive portal AP mode when no network saved — connect, select SSID, save credentials to flash.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FWifiManager_MicroPython","\u002Fimages\u002Fdemos\u002Fwifi-manager-micropython.webp",{},"\u002Fdemos\u002Fwifi-manager-micropython",{"title":1681,"description":1784},"demos\u002Fwifi-manager-micropython",[1792,1793],"MicroPython","Hardware","H2DrIuZF91vt44WJsY1pE_Bw5Cr4qjjrfjfRPc6uQqI",{"id":1796,"title":1797,"body":1798,"cover":334,"date":1915,"description":1916,"extension":337,"featured":1069,"github":1917,"image":1918,"live":334,"meta":1919,"navigation":338,"path":1920,"seo":1921,"stem":1922,"tags":1923,"tech":1792,"__hash__":1924},"demos\u002Fdemos\u002Fcryptodash-picow.md","CryptoDash — Raspberry Pi Pico W",{"type":8,"value":1799,"toc":1909},[1800,1803,1823,1826,1840,1842,1852,1859,1879,1883,1886,1890,1901],[11,1801,1802],{},"Total build cost: ~€17. Pico W + 2.8\" ILI9341 TFT + buttons + battery.",[298,1804,18,1805,18,1814],{"style":300},[15,1806,304,1807,304,1811,18],{"style":303},[20,1808],{"src":1809,"alt":1810,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash_PicoW\u002Fmain\u002Fimages\u002Fimg1.jpg","CryptoDash Pico W display",[26,1812,1813],{"style":311},"Live crypto prices on 2.8\" TFT",[15,1815,304,1816,304,1820,18],{"style":303},[20,1817],{"src":1818,"alt":1819,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash_PicoW\u002Fmain\u002Fimages\u002Fimg2.jpg","Hardware assembly",[26,1821,1822],{"style":311},"Pico W + ILI9341 + buttons + LiPo",[31,1824,1793],{"id":1825},"hardware",[84,1827,1828,1831,1834,1837],{},[42,1829,1830],{},"Raspberry Pi Pico W (RP2040 + WiFi)",[42,1832,1833],{},"2.8\" TFT LCD (ILI9341 driver, SPI)",[42,1835,1836],{},"2 buttons: short press = refresh, 3-second hold = reset to AP mode",[42,1838,1839],{},"1,500mAh LiPo battery (~37h continuous at ~40mA idle)",[31,1841,34],{"id":33},[11,1843,1844,1845,1847,1848,1851],{},"First boot starts as an Access Point for WiFi setup (credentials saved to ",[65,1846,1689],{},"). Subsequent boots connect automatically and poll Binance ",[65,1849,1850],{},"\u002Fticker\u002Fprice"," every 30 seconds. Display renders BTC, ETH, ADA, BNB, SOL by default.",[11,1853,737,1854,1858],{},[282,1855,1857],{"href":1785,"rel":1856},[286],"WifiManager_MicroPython"," for the AP configuration portal.",[298,1860,18,1861,18,1870],{"style":300},[15,1862,304,1863,304,1867,18],{"style":303},[20,1864],{"src":1865,"alt":1866,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash_PicoW\u002Fmain\u002Fimages\u002Fimg3.jpg","WiFi config portal",[26,1868,1869],{"style":311},"First-boot WiFi setup portal",[15,1871,304,1872,304,1876,18],{"style":303},[20,1873],{"src":1874,"alt":1875,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash_PicoW\u002Fmain\u002Fimages\u002Fimg4.jpg","Token selection screen",[26,1877,1878],{"style":311},"Token customization interface",[31,1880,1882],{"id":1881},"setup","Setup",[11,1884,1885],{},"Flash MicroPython firmware → upload files via Thonny → power on → connect to AP → enter WiFi credentials.",[31,1887,1889],{"id":1888},"in-progress","In progress",[84,1891,1892,1895,1898],{},[42,1893,1894],{},"Mini crypto logos",[42,1896,1897],{},"iOS Access Point compatibility",[42,1899,1900],{},"Screen rendering optimization",[11,1902,1903,1904,1908],{},"See also: ",[282,1905,1907],{"href":1906},"\u002Fcryptodash-python","CryptoDash Python"," — desktop version for Raspberry Pi with touchscreen.",{"title":111,"searchDepth":165,"depth":165,"links":1910},[1911,1912,1913,1914],{"id":1825,"depth":165,"text":1793},{"id":33,"depth":165,"text":34},{"id":1881,"depth":165,"text":1882},{"id":1888,"depth":165,"text":1889},"2025-12-10","Real-time cryptocurrency dashboard on Raspberry Pi Pico W. Pulls live prices from Binance API over WiFi, renders on a 2.8\" TFT display using MicroPython.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FCryptoDash_PicoW","\u002Fimages\u002Fdemos\u002Fcryptodash-picow.webp",{},"\u002Fdemos\u002Fcryptodash-picow",{"title":1797,"description":1916},"demos\u002Fcryptodash-picow",[1792,1793],"jCDbIQ9UoRlTsxUxoUVAMHmjgcNXvpfBkFFgFrJDo6Q",{"id":1926,"title":1927,"body":1928,"cover":334,"date":2058,"description":2059,"extension":337,"featured":1069,"github":2060,"image":2061,"live":334,"meta":2062,"navigation":338,"path":2063,"seo":2064,"stem":2065,"tags":2066,"tech":2067,"__hash__":2068},"demos\u002Fdemos\u002Fcryptodash-python.md","CryptoDash — Python Desktop (Raspberry Pi)",{"type":8,"value":1929,"toc":2053},[1930,1933,1953,1957,1982,1984,2009,2012,2032,2036,2046,2051],[11,1931,1932],{},"Built for a custom hardware project: Raspberry Pi inside a wooden enclosure with a 4.3\" touchscreen. PyQt5 full-screen UI polling Binance API every 30 seconds.",[298,1934,18,1935,18,1944],{"style":300},[15,1936,304,1937,304,1941,18],{"style":303},[20,1938],{"src":1939,"alt":1940,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash\u002Fmain\u002Fimages\u002Fimg1.jpg","CryptoDash dashboard view",[26,1942,1943],{"style":311},"Live dashboard — touch-optimized grid",[15,1945,304,1946,304,1950,18],{"style":303},[20,1947],{"src":1948,"alt":1949,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash\u002Fmain\u002Fimages\u002Fimg2.jpg","CryptoDash UI close-up",[26,1951,1952],{"style":311},"Coin cards with 24h change",[31,1954,1956],{"id":1955},"stack","Stack",[84,1958,1959,1962,1969,1976],{},[42,1960,1961],{},"Python 3 + PyQt5",[42,1963,1964,1965,1968],{},"Binance REST API (",[65,1966,1967],{},"\u002Fapi\u002Fv3\u002Fticker\u002F24hr",")",[42,1970,1971,1972,1975],{},"Network management via ",[65,1973,1974],{},"network.py"," (Raspberry Pi-specific)",[42,1977,1978,1979],{},"Autostart via ",[65,1980,1981],{},".config\u002Fautostart\u002Fcrypto_dash.desktop",[31,1983,1882],{"id":1881},[106,1985,1987],{"className":1147,"code":1986,"language":1149,"meta":111,"style":111},"pip install -r requirements.txt\npython crypto_dash.py\n",[65,1988,1989,2002],{"__ignoreMap":111},[115,1990,1991,1994,1996,1999],{"class":117,"line":118},[115,1992,1993],{"class":145},"pip",[115,1995,1159],{"class":152},[115,1997,1998],{"class":181}," -r",[115,2000,2001],{"class":152}," requirements.txt\n",[115,2003,2004,2006],{"class":117,"line":165},[115,2005,1728],{"class":145},[115,2007,2008],{"class":152}," crypto_dash.py\n",[11,2010,2011],{},"Full Raspberry Pi setup: OS via Imager, swap adjustment, PyQt5, VNC for remote access, unclutter to hide cursor.",[298,2013,18,2014,18,2023],{"style":300},[15,2015,304,2016,304,2020,18],{"style":303},[20,2017],{"src":2018,"alt":2019,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash\u002Fmain\u002Fimages\u002Fsettings.png","Settings screen",[26,2021,2022],{"style":311},"Settings panel",[15,2024,304,2025,304,2029,18],{"style":303},[20,2026],{"src":2027,"alt":2028,"style":24},"https:\u002F\u002Fraw.githubusercontent.com\u002Fcrazyramirez\u002FCryptoDash\u002Fmain\u002Fimages\u002Fimg3.jpg","Hardware build",[26,2030,2031],{"style":311},"Hardware build — Pi + 4.3\" touch in wooden case",[31,2033,2035],{"id":2034},"planned","Planned",[84,2037,2038,2041,2044],{},[42,2039,2040],{},"GPIO button for token switching",[42,2042,2043],{},"WiFi AP configuration",[42,2045,1878],{},[11,2047,1903,2048,2050],{},[282,2049,1773],{"href":1772}," — same concept on a $10 microcontroller.",[323,2052,1214],{},{"title":111,"searchDepth":165,"depth":165,"links":2054},[2055,2056,2057],{"id":1955,"depth":165,"text":1956},{"id":1881,"depth":165,"text":1882},{"id":2034,"depth":165,"text":2035},"2025-11-20","Full-screen cryptocurrency dashboard in Python for Raspberry Pi with touchscreen. Binance API live prices, touch-optimized UI with PyQt5, portrait or landscape layout.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FCryptoDash","\u002Fimages\u002Fdemos\u002Fcryptodash-python.webp",{},"\u002Fdemos\u002Fcryptodash-python",{"title":1927,"description":2059},"demos\u002Fcryptodash-python",[2067,1793],"Python","uN_HPzj-cQB_tR0OUOqAN_5v36Qi4CH_ZnShn_hYBiw",{"id":2070,"title":2071,"body":2072,"cover":334,"date":2109,"description":2110,"extension":337,"featured":1069,"github":2111,"image":2112,"live":334,"meta":2113,"navigation":338,"path":2114,"seo":2115,"stem":2116,"tags":2117,"tech":1076,"__hash__":2119},"demos\u002Fdemos\u002Ftodo-list.md","ToDo List — Minimal Web App",{"type":8,"value":2073,"toc":2106},[2074,2081,2083,2103],[11,2075,2076,2077,2080],{},"No React, no Vue, no build step. Drops into any server as a static file. Open ",[65,2078,2079],{},"index.html"," in browser — done.",[31,2082,390],{"id":389},[84,2084,2085,2088,2091,2094,2097,2100],{},[42,2086,2087],{},"Add \u002F complete \u002F delete tasks",[42,2089,2090],{},"Priority: Low \u002F Medium \u002F High (color coded)",[42,2092,2093],{},"Filter: All \u002F Active \u002F Completed",[42,2095,2096],{},"LocalStorage persistence",[42,2098,2099],{},"Keyboard: Enter to add, Escape to blur",[42,2101,2102],{},"Responsive — works on mobile",[11,2104,2105],{},"Good exercise in writing clean DOM code without a framework abstracting it away.",{"title":111,"searchDepth":165,"depth":165,"links":2107},[2108],{"id":389,"depth":165,"text":390},"2025-10-05","Clean, responsive ToDo List app. Vanilla HTML\u002FCSS\u002FJS — no framework, no build step. LocalStorage persistence, priority levels, filter by status.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FToDo_List","\u002Fimages\u002Fdemos\u002Ftodo-list.webp",{},"\u002Fdemos\u002Ftodo-list",{"title":2071,"description":2110},"demos\u002Ftodo-list",[1076,2118],"Web","KE6b6bxPnzLHF8XbNQL471BxZN17CxpcCptc1hv65Y4",{"id":2121,"title":2122,"body":2123,"cover":334,"date":2195,"description":2196,"extension":337,"featured":1069,"github":2197,"image":2198,"live":334,"meta":2199,"navigation":338,"path":2200,"seo":2201,"stem":2202,"tags":2203,"tech":1076,"__hash__":2204},"demos\u002Fdemos\u002Finvoicer.md","Invoicer — Quick Invoice Generator",{"type":8,"value":2124,"toc":2191},[2125,2131,2151,2153,2170,2174,2181],[11,2126,2127,2128,68],{},"100% client-side invoice generator. No backend, no account. Fill in client + vendor details and line items → printable PDF via ",[65,2129,2130],{},"window.print()",[298,2132,18,2133,18,2142],{"style":300},[15,2134,304,2135,304,2139,18],{"style":303},[20,2136],{"src":2137,"alt":2138,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FInvoicer\u002Fblob\u002Fmain\u002Fimages\u002Fdemo_1.jpg?raw=true","Bake result comparison",[26,2140,2141],{"style":311},"Invoice Demo 1",[15,2143,304,2144,304,2148,18],{"style":303},[20,2145],{"src":2146,"alt":2147,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FInvoicer\u002Fblob\u002Fmain\u002Fimages\u002Fdemo_2.jpg?raw=true","Batch processing mode",[26,2149,2150],{"style":311},"Invoice Demo 2",[31,2152,390],{"id":389},[84,2154,2155,2158,2161,2164,2167],{},[42,2156,2157],{},"Client & vendor details",[42,2159,2160],{},"Line items: qty × unit price × tax → auto-calculated totals",[42,2162,2163],{},"Invoice number + date",[42,2165,2166],{},"LocalStorage autosave — survives page refresh",[42,2168,2169],{},"Print CSS hides controls, shows only invoice layout",[31,2171,2173],{"id":2172},"built-with-cursor-ai","Built with Cursor AI",[11,2175,2176,2177,2180],{},"Boilerplate and DOM manipulation generated with GPT-4 via Cursor. Business logic (tax calculation, ",[65,2178,2179],{},"Intl.NumberFormat"," locale) reviewed and written manually.",[11,2182,2183,2184,2187,2188,2190],{},"Currency formatted for ",[65,2185,2186],{},"es-ES"," locale (EUR) by default — change ",[65,2189,2179],{}," options to match your region.",{"title":111,"searchDepth":165,"depth":165,"links":2192},[2193,2194],{"id":389,"depth":165,"text":390},{"id":2172,"depth":165,"text":2173},"2025-09-12","Mini web app to generate clean invoices fast. Built with Cursor + AI assistance. Fill in client details, line items, export to PDF — no account needed.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002FInvoicer","\u002Fimages\u002Fdemos\u002Finvoicer.webp",{},"\u002Fdemos\u002Finvoicer",{"title":2122,"description":2196},"demos\u002Finvoicer",[1076,2118],"_AjpV7img9DqlJECIFTeiOBO9qnBzBtZ2L8bgWozo8A",{"id":2206,"title":2207,"body":2208,"cover":334,"date":2341,"description":2342,"extension":337,"featured":1069,"github":2343,"image":2344,"live":334,"meta":2345,"navigation":338,"path":2346,"seo":2347,"stem":2348,"tags":2349,"tech":2350,"__hash__":2351},"demos\u002Fdemos\u002Fturbo-mailer.md","Turbo Mailer — Bulk Email Tool",{"type":8,"value":2209,"toc":2336},[2210,2217,2225,2227,2241,2245,2268,2271,2289,2307,2325,2329],[11,2211,2212,2213,2216],{},"Bulk mailer with CSV upload, ",[65,2214,2215],{},"{{variable}}"," template substitution, and live per-recipient progress. No database — session only, nothing persisted server-side.",[15,2218,18,2219,18,2223],{"style":17},[20,2220],{"src":2221,"alt":2222,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fogimage.jpg?raw=true","Tube Reply — main interface",[26,2224,29],{"style":28},[31,2226,1956],{"id":1955},[84,2228,2229,2232,2238],{},[42,2230,2231],{},"Vue 3 + Composition API",[42,2233,2234,2235],{},"Node.js backend with ",[65,2236,2237],{},"nodemailer",[42,2239,2240],{},"Papa Parse for CSV parsing client-side",[31,2242,2244],{"id":2243},"workflow","Workflow",[39,2246,2247,2250,2256,2262,2265],{},[42,2248,2249],{},"Configure SMTP (Gmail, Outlook, custom)",[42,2251,2252,2253],{},"Upload CSV — ",[65,2254,2255],{},"name,email,company,...",[42,2257,2258,2259,2261],{},"Write email template with ",[65,2260,2215],{}," placeholders",[42,2263,2264],{},"Preview per recipient",[42,2266,2267],{},"Send — live status: sent \u002F failed \u002F pending",[11,2269,2270],{},"Send queue throttles at 300ms between emails to avoid SMTP rate limits.",[298,2272,18,2273,18,2281],{"style":300},[15,2274,304,2275,304,2278,18],{"style":303},[20,2276],{"src":2277,"alt":2138,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fsc_1.webp?raw=true",[26,2279,2280],{"style":311},"Main Dashboard",[15,2282,304,2283,304,2286,18],{"style":303},[20,2284],{"src":2285,"alt":2147,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fsc_2.webp?raw=true",[26,2287,2288],{"style":311},"Pro Editor",[298,2290,18,2291,18,2299],{"style":300},[15,2292,304,2293,304,2296,18],{"style":303},[20,2294],{"src":2295,"alt":2138,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fsc_3.webp?raw=true",[26,2297,2298],{"style":311},"Conacts Admin",[15,2300,304,2301,304,2304,18],{"style":303},[20,2302],{"src":2303,"alt":2147,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fsc_4.webp?raw=true",[26,2305,2306],{"style":311},"Campaigns",[298,2308,18,2309,18,2317],{"style":300},[15,2310,304,2311,304,2314,18],{"style":303},[20,2312],{"src":2313,"alt":2138,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fsc_5.webp?raw=true",[26,2315,2316],{"style":311},"Campaing Detail",[15,2318,304,2319,304,2322,18],{"style":303},[20,2320],{"src":2321,"alt":2147,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fsc_6.webp?raw=true",[26,2323,2324],{"style":311},"Analytics",[31,2326,2328],{"id":2327},"gmail-note","Gmail note",[11,2330,2331,2332,2335],{},"Gmail requires an ",[45,2333,2334],{},"App Password"," (not your account password) since 2022. Settings → Security → 2-Step Verification → App Passwords.",{"title":111,"searchDepth":165,"depth":165,"links":2337},[2338,2339,2340],{"id":1955,"depth":165,"text":1956},{"id":2243,"depth":165,"text":2244},{"id":2327,"depth":165,"text":2328},"2025-08-20","Vue app for sending bulk email campaigns. Upload CSV of recipients, write template with variable substitution, send via SMTP. Tracks sent \u002F failed per session.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Fturbo-mailer","\u002Fimages\u002Fdemos\u002Fturbo-mailer.webp",{},"\u002Fdemos\u002Fturbo-mailer",{"title":2207,"description":2342},"demos\u002Fturbo-mailer",[2350,1076,2118],"Vue","A-ZQhnvN3iP29lG4H3VFH4edq_FHOQv-FgzNea0HG_Q",{"id":2353,"title":2354,"body":2355,"cover":334,"date":2415,"description":2416,"extension":337,"featured":1069,"github":2417,"image":2418,"live":334,"meta":2419,"navigation":338,"path":2420,"seo":2421,"stem":2422,"tags":2423,"tech":2350,"__hash__":2424},"demos\u002Fdemos\u002Ftube-reply.md","Tube Reply — YouTube Comment Assistant",{"type":8,"value":2356,"toc":2410},[2357,2360,2367,2369,2383,2385,2388,2392,2402],[11,2358,2359],{},"Paste a YouTube comment → get 4 reply options: friendly, professional, humorous, concise. For content creators managing high comment volume.",[15,2361,18,2362,18,2365],{"style":17},[20,2363],{"src":2364,"alt":2222,"style":24},"https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Ftube-reply\u002Fblob\u002Fmain\u002Fpublic\u002Fimages\u002Fogimage.jpg?raw=true",[26,2366,29],{"style":28},[31,2368,1956],{"id":1955},[84,2370,2371,2373,2380],{},[42,2372,2231],{},[42,2374,2375,2376,2379],{},"OpenAI ",[65,2377,2378],{},"gpt-4o-mini"," (fast, cheap for short text — ~$0.00006\u002Frequest)",[42,2381,2382],{},"No backend — direct API calls from client (personal use only)",[31,2384,34],{"id":33},[11,2386,2387],{},"Single prompt sends the comment + optional channel context and requests all 4 tones as JSON in one call. Each card shows the reply with a one-click copy button.",[31,2389,2391],{"id":2390},"cost","Cost",[11,2393,2394,2395,2397,2398,2401],{},"1,000 replies\u002Fday ≈ $0.06. ",[65,2396,2378],{}," is the right model for this — ",[65,2399,2400],{},"gpt-4o"," would be 25× the cost for no noticeable improvement in short reply quality.",[220,2403,2404],{},[11,2405,2406,2409],{},[65,2407,2408],{},"dangerouslyAllowBrowser: true"," — fine for personal tools, not for public deployment (exposes API key).",{"title":111,"searchDepth":165,"depth":165,"links":2411},[2412,2413,2414],{"id":1955,"depth":165,"text":1956},{"id":33,"depth":165,"text":34},{"id":2390,"depth":165,"text":2391},"2025-07-08","Vue app that helps generate replies to YouTube comments. Paste a comment, get AI-suggested responses in different tones. Built with Vue 3 + OpenAI API.","https:\u002F\u002Fgithub.com\u002Fcrazyramirez\u002Ftube-reply","\u002Fimages\u002Fdemos\u002Ftube-reply.webp",{},"\u002Fdemos\u002Ftube-reply",{"title":2354,"description":2416},"demos\u002Ftube-reply",[2350,1076,2118],"CbMdV6VlOtoKKUg2Z8gggfapjg0WY0n7PMHkvqA8GkY",1780330101316]