[{"data":1,"prerenderedAt":1698},["ShallowReactive",2],{"i-base:v-logo":3,"i-lucide:user":8,"i-lucide:folder-kanban":11,"i-lucide:heart":13,"i-lucide:file-text":15,"i-lucide:sun":17,"i-lucide:github":19,"i-lucide:twitter":22,"i-lucide:linkedin":24,"i-simple-icons:reddit":26,"i-lucide:globe":28,"article-encoding-elevation-into-pixels":30,"articles-all":1630,"i-lucide:arrow-left":1696},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,512,false,"\u003Cg fill=\"currentColor\">\u003Cg transform=\"translate(0,512) scale(0.1,-0.1)\">\n    \u003Cpath d=\"M2866 3193 c-203 -209 -341 -393 -541 -718 -57 -93 -198 -309 -212 -325 -9 -9 -22 26 -28 75 -4 27 -8 61 -10 75 -2 14 -7 70 -10 125 -21 363 -72 541 -187 656 -30 30 -54 64 -56 78 -7 55 -65 113 -130 130 -21 6 -34 1 -62 -23 -64 -57 -86 -84 -97 -119 -12 -41 -9 -201 7 -349 6 -57 12 -204 12 -327 0 -191 2 -232 18 -270 10 -27 20 -89 24 -148 5 -92 21 -182 84 -478 12 -56 30 -105 46 -129 14 -21 27 -48 27 -60 1 -12 3 -39 4 -61 1 -23 8 -41 16 -44 8 -2 39 -31 69 -63 66 -72 98 -90 159 -91 133 -1 245 118 254 271 4 67 7 74 45 114 22 24 50 55 61 70 33 42 81 138 81 162 0 12 21 44 50 74 27 28 63 80 80 114 57 114 131 230 244 378 48 64 213 266 249 305 78 85 167 207 167 230 0 14 11 47 23 73 34 67 29 133 -20 252 l-38 95 -50 6 c-28 3 -67 13 -87 22 -21 10 -47 17 -58 17 -13 0 -62 -43 -134 -117z\"\u002F>\n    \u003Cpath d=\"M3070 1649 c-30 -44 -76 -96 -102 -115 -27 -20 -48 -40 -48 -45 0 -5 8 -21 17 -36 17 -25 17 -27 -5 -41 l-22 -14 30 -35 c17 -18 30 -39 30 -47 0 -36 34 -118 58 -140 102 -92 120 -104 159 -100 108 10 153 24 153 49 0 7 27 28 59 45 62 33 92 74 90 121 -1 13 10 57 25 97 32 87 33 90 17 165 -15 70 -24 75 -177 111 -159 38 -194 48 -200 57 -12 21 -32 4 -84 -72z\"\u002F>\n  \u003C\u002Fg>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":10},24,"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"\u002F>\u003Ccircle cx=\"12\" cy=\"7\" r=\"4\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":12},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2m4-10v4m4-4v2m4-2v6\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":14},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2 9.5a5.5 5.5 0 0 1 9.591-3.676a.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":16},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"\u002F>\u003Cpath d=\"M14 2v5a1 1 0 0 0 1 1h5M10 9H8m8 4H8m8 4H8\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":18},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"4\"\u002F>\u003Cpath d=\"M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":20,"hidden":21},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5c.08-1.25-.27-2.48-1-3.5c.28-1.15.28-2.35 0-3.5c0 0-1 0-3 1.5c-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5c-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4\"\u002F>\u003Cpath d=\"M9 18c-4.51 2-5-2-7-2\"\u002F>\u003C\u002Fg>",true,{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":23,"hidden":21},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6c2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4c-.9-4.2 4-6.6 7-3.8c1.1 0 3-1.2 3-1.2\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":25,"hidden":21},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2a2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6M2 9h4v12H2z\"\u002F>\u003Ccircle cx=\"4\" cy=\"4\" r=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":27},"\u003Cpath fill=\"currentColor\" d=\"M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286A.72.72 0 0 0 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0m4.388 3.199a1.999 1.999 0 1 1-1.947 2.46v.002a2.37 2.37 0 0 0-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363a2.802 2.802 0 1 1 2.908 4.753c-.088 3.256-3.637 5.876-7.997 5.876c-4.361 0-7.905-2.617-7.998-5.87a2.8 2.8 0 0 1 1.189-5.34c.645 0 1.239.218 1.712.585c1.275-.79 2.881-1.291 4.64-1.365v-.01a3.23 3.23 0 0 1 2.88-3.207a2 2 0 0 1 1.959-1.595m-8.085 8.376c-.784 0-1.459.78-1.506 1.797s.64 1.429 1.426 1.429s1.371-.369 1.418-1.385s-.553-1.841-1.338-1.841m7.406 0c-.786 0-1.385.824-1.338 1.841s.634 1.385 1.418 1.385c.785 0 1.473-.413 1.426-1.429c-.046-1.017-.721-1.797-1.506-1.797m-3.703 4.013c-.974 0-1.907.048-2.77.135a.222.222 0 0 0-.183.305a3.2 3.2 0 0 0 2.953 1.964a3.2 3.2 0 0 0 2.953-1.964a.222.222 0 0 0-.184-.305a28 28 0 0 0-2.769-.135\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":29},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"M12 2a14.5 14.5 0 0 0 0 20a14.5 14.5 0 0 0 0-20M2 12h20\"\u002F>\u003C\u002Fg>",{"id":31,"title":32,"body":33,"category":1614,"date":1615,"description":1616,"extension":1617,"image":1618,"meta":1619,"navigation":21,"path":1620,"seo":1621,"status":1622,"stem":1623,"tags":1624,"updatedAt":1618,"__hash__":1629},"articles\u002Farticles\u002F8.encoding-elevation-into-pixels.md","Encoding elevation into pixels: DEM terrain tiles in Rust",{"type":34,"value":35,"toc":1607},"minimark",[36,45,68,73,81,105,116,442,452,456,471,478,599,602,695,699,702,717,724,819,838,841,1164,1178,1185,1189,1192,1466,1473,1476,1529,1543,1547,1554,1579,1582,1585,1594,1603],[37,38,39,40,44],"p",{},"Modern web maps are expected to do 3D terrain. Hillshade, contour lines, the tilted camera flying over mountains — all of it is powered by something delightfully dumb: PNG images where the color of each pixel ",[41,42,43],"em",{},"is"," the elevation.",[37,46,47,48,55,56,61,62,67],{},"I recently shipped native DEM tile encoding in ",[49,50,54],"a",{"href":51,"rel":52},"https:\u002F\u002Fgithub.com\u002Fvinayakkulkarni\u002Ftileserver-rs",[53],"nofollow","tileserver-rs"," (",[49,57,60],{"href":58,"rel":59},"https:\u002F\u002Fgithub.com\u002Fvinayakkulkarni\u002Ftileserver-rs\u002Fpull\u002F1220",[53],"#1220",", closing ",[49,63,66],{"href":64,"rel":65},"https:\u002F\u002Fgithub.com\u002Fvinayakkulkarni\u002Ftileserver-rs\u002Fissues\u002F1008",[53],"#1008","), and the whole thing fits in one Rust file. But that file is full of decisions that will silently produce garbage terrain if you get them wrong, and none of them are documented in one place. So here's that place.",[69,70,72],"h2",{"id":71},"elevation-as-rgb","Elevation as RGB",[37,74,75,76,80],{},"The trick behind ",[77,78,79],"code",{},"raster-dem"," sources is packing a floating-point elevation into 24 bits of RGB. There are two conventions, and MapLibre GL JS decodes both:",[82,83,84,96],"ul",{},[85,86,87,91,92,95],"li",{},[88,89,90],"strong",{},"Terrarium",": ",[77,93,94],{},"elevation = R*256 + G + B\u002F256 - 32768",", precision 1\u002F256 m",[85,97,98,91,101,104],{},[88,99,100],{},"Mapbox-RGB",[77,102,103],{},"elevation = -10000 + (R*65536 + G*256 + B) * 0.1",", precision 0.1 m",[37,106,107,108,115],{},"Encoding is the reverse: scale the elevation into integer units, split into three bytes. Here's the Terrarium encoder from ",[49,109,112],{"href":110,"rel":111},"https:\u002F\u002Fgithub.com\u002Fvinayakkulkarni\u002Ftileserver-rs\u002Fblob\u002Fmain\u002Fcrates\u002Ftileserver-rs\u002Fsrc\u002Fsources\u002Fdem.rs",[53],[77,113,114],{},"sources\u002Fdem.rs",":",[117,118,123],"pre",{"className":119,"code":120,"language":121,"meta":122,"style":122},"language-rust shiki shiki-themes github-dark github-dark github-light","\u002F\u002F\u002F Terrarium base offset in metres: decoded = packed - 32768.\nconst TERRARIUM_BASE: f64 = 32768.0;\n\u002F\u002F\u002F Largest packed value representable in 24 bits (2^24 - 1).\nconst MAX_U24: f64 = 16_777_215.0;\n\n#[must_use]\npub fn encode_terrarium(elevation: f64) -> [u8; 3] {\n    \u002F\u002F Work in 1\u002F256-m units so the blue byte is an exact integer; round to\n    \u002F\u002F nearest before splitting (the round-before-truncate rule), then clamp\n    \u002F\u002F to the 24-bit packed range [0, 256^3 - 1].\n    let units = ((elevation + TERRARIUM_BASE) * 256.0).round();\n    let packed = units.clamp(0.0, MAX_U24) as u32;\n    let r = (packed >> 16) & 0xFF;\n    let g = (packed >> 8) & 0xFF;\n    let b = packed & 0xFF;\n    [r as u8, g as u8, b as u8]\n}\n","rust","",[77,124,125,134,161,167,186,192,198,238,244,250,256,296,336,365,390,408,436],{"__ignoreMap":122},[126,127,130],"span",{"class":128,"line":129},"line",1,[126,131,133],{"class":132},"sxXm1","\u002F\u002F\u002F Terrarium base offset in metres: decoded = packed - 32768.\n",[126,135,137,141,145,147,151,154,157],{"class":128,"line":136},2,[126,138,140],{"class":139},"s6ubI","const",[126,142,144],{"class":143},"sXWYR"," TERRARIUM_BASE",[126,146,115],{"class":139},[126,148,150],{"class":149},"s5n6i"," f64",[126,152,153],{"class":139}," =",[126,155,156],{"class":143}," 32768.0",[126,158,160],{"class":159},"sAvwS",";\n",[126,162,164],{"class":128,"line":163},3,[126,165,166],{"class":132},"\u002F\u002F\u002F Largest packed value representable in 24 bits (2^24 - 1).\n",[126,168,170,172,175,177,179,181,184],{"class":128,"line":169},4,[126,171,140],{"class":139},[126,173,174],{"class":143}," MAX_U24",[126,176,115],{"class":139},[126,178,150],{"class":149},[126,180,153],{"class":139},[126,182,183],{"class":143}," 16_777_215.0",[126,185,160],{"class":159},[126,187,189],{"class":128,"line":188},5,[126,190,191],{"emptyLinePlaceholder":21},"\n",[126,193,195],{"class":128,"line":194},6,[126,196,197],{"class":159},"#[must_use]\n",[126,199,201,204,207,210,213,215,217,220,223,226,229,232,235],{"class":128,"line":200},7,[126,202,203],{"class":139},"pub",[126,205,206],{"class":139}," fn",[126,208,209],{"class":149}," encode_terrarium",[126,211,212],{"class":159},"(elevation",[126,214,115],{"class":139},[126,216,150],{"class":149},[126,218,219],{"class":159},") ",[126,221,222],{"class":139},"->",[126,224,225],{"class":159}," [",[126,227,228],{"class":149},"u8",[126,230,231],{"class":159},"; ",[126,233,234],{"class":143},"3",[126,236,237],{"class":159},"] {\n",[126,239,241],{"class":128,"line":240},8,[126,242,243],{"class":132},"    \u002F\u002F Work in 1\u002F256-m units so the blue byte is an exact integer; round to\n",[126,245,247],{"class":128,"line":246},9,[126,248,249],{"class":132},"    \u002F\u002F nearest before splitting (the round-before-truncate rule), then clamp\n",[126,251,253],{"class":128,"line":252},10,[126,254,255],{"class":132},"    \u002F\u002F to the 24-bit packed range [0, 256^3 - 1].\n",[126,257,259,262,265,268,271,274,276,278,281,284,287,290,293],{"class":128,"line":258},11,[126,260,261],{"class":139},"    let",[126,263,264],{"class":159}," units ",[126,266,267],{"class":139},"=",[126,269,270],{"class":159}," ((elevation ",[126,272,273],{"class":139},"+",[126,275,144],{"class":143},[126,277,219],{"class":159},[126,279,280],{"class":139},"*",[126,282,283],{"class":143}," 256.0",[126,285,286],{"class":159},")",[126,288,289],{"class":139},".",[126,291,292],{"class":149},"round",[126,294,295],{"class":159},"();\n",[126,297,299,301,304,306,309,311,314,317,320,323,326,328,331,334],{"class":128,"line":298},12,[126,300,261],{"class":139},[126,302,303],{"class":159}," packed ",[126,305,267],{"class":139},[126,307,308],{"class":159}," units",[126,310,289],{"class":139},[126,312,313],{"class":149},"clamp",[126,315,316],{"class":159},"(",[126,318,319],{"class":143},"0.0",[126,321,322],{"class":159},", ",[126,324,325],{"class":143},"MAX_U24",[126,327,219],{"class":159},[126,329,330],{"class":139},"as",[126,332,333],{"class":149}," u32",[126,335,160],{"class":159},[126,337,339,341,344,346,349,352,355,357,360,363],{"class":128,"line":338},13,[126,340,261],{"class":139},[126,342,343],{"class":159}," r ",[126,345,267],{"class":139},[126,347,348],{"class":159}," (packed ",[126,350,351],{"class":139},">>",[126,353,354],{"class":143}," 16",[126,356,219],{"class":159},[126,358,359],{"class":139},"&",[126,361,362],{"class":143}," 0xFF",[126,364,160],{"class":159},[126,366,368,370,373,375,377,379,382,384,386,388],{"class":128,"line":367},14,[126,369,261],{"class":139},[126,371,372],{"class":159}," g ",[126,374,267],{"class":139},[126,376,348],{"class":159},[126,378,351],{"class":139},[126,380,381],{"class":143}," 8",[126,383,219],{"class":159},[126,385,359],{"class":139},[126,387,362],{"class":143},[126,389,160],{"class":159},[126,391,393,395,398,400,402,404,406],{"class":128,"line":392},15,[126,394,261],{"class":139},[126,396,397],{"class":159}," b ",[126,399,267],{"class":139},[126,401,303],{"class":159},[126,403,359],{"class":139},[126,405,362],{"class":143},[126,407,160],{"class":159},[126,409,411,414,416,419,422,424,426,429,431,433],{"class":128,"line":410},16,[126,412,413],{"class":159},"    [r ",[126,415,330],{"class":139},[126,417,418],{"class":149}," u8",[126,420,421],{"class":159},", g ",[126,423,330],{"class":139},[126,425,418],{"class":149},[126,427,428],{"class":159},", b ",[126,430,330],{"class":139},[126,432,418],{"class":149},[126,434,435],{"class":159},"]\n",[126,437,439],{"class":128,"line":438},17,[126,440,441],{"class":159},"}\n",[37,443,444,445,448,449,289],{},"Twelve lines. The interesting part is the two functions hiding inside them: ",[77,446,447],{},".round()"," and ",[77,450,451],{},".clamp()",[69,453,455],{"id":454},"the-round-before-truncate-rule","The round-before-truncate rule",[37,457,458,459,462,463,466,467,470],{},"The classic bug in this space comes from ",[77,460,461],{},"rio-rgbify"," and its descendants: scale the elevation, then let the integer cast truncate it. Floating-point arithmetic being what it is, ",[77,464,465],{},"(1000.5 + 32768.0) * 256.0"," can land at ",[77,468,469],{},"8644735.999999...",", and truncation eats a full least-significant bit. Your terrain is now systematically one elevation-interval low, and nobody notices until someone compares contour lines against a survey.",[37,472,473,474,477],{},"MapLibre's own ",[77,475,476],{},"packDEMData"," rounds before splitting, for exactly this reason. So does this encoder, and there's a regression test pinning the exact case:",[117,479,481],{"className":119,"code":480,"language":121,"meta":122,"style":122},"#[test]\nfn mapbox_round_before_truncate() {\n    \u002F\u002F The classic rgbify bug: 1000.5 m packs to exactly 110005.0; a\n    \u002F\u002F truncating encoder that computed 110004.999.. would lose the LSB.\n    \u002F\u002F Round-then-split must land on the exact triplet.\n    let rgb = encode_mapbox(1000.5);\n    let decoded = decode_mapbox(rgb);\n    assert!(\n        (decoded - 1000.5).abs() \u003C 0.001,\n        \"mapbox round-before-truncate 1000.5 → {rgb:?} → {decoded}\"\n    );\n}\n",[77,482,483,488,499,504,509,514,534,549,557,584,590,595],{"__ignoreMap":122},[126,484,485],{"class":128,"line":129},[126,486,487],{"class":159},"#[test]\n",[126,489,490,493,496],{"class":128,"line":136},[126,491,492],{"class":139},"fn",[126,494,495],{"class":149}," mapbox_round_before_truncate",[126,497,498],{"class":159},"() {\n",[126,500,501],{"class":128,"line":163},[126,502,503],{"class":132},"    \u002F\u002F The classic rgbify bug: 1000.5 m packs to exactly 110005.0; a\n",[126,505,506],{"class":128,"line":169},[126,507,508],{"class":132},"    \u002F\u002F truncating encoder that computed 110004.999.. would lose the LSB.\n",[126,510,511],{"class":128,"line":188},[126,512,513],{"class":132},"    \u002F\u002F Round-then-split must land on the exact triplet.\n",[126,515,516,518,521,523,526,528,531],{"class":128,"line":194},[126,517,261],{"class":139},[126,519,520],{"class":159}," rgb ",[126,522,267],{"class":139},[126,524,525],{"class":149}," encode_mapbox",[126,527,316],{"class":159},[126,529,530],{"class":143},"1000.5",[126,532,533],{"class":159},");\n",[126,535,536,538,541,543,546],{"class":128,"line":200},[126,537,261],{"class":139},[126,539,540],{"class":159}," decoded ",[126,542,267],{"class":139},[126,544,545],{"class":149}," decode_mapbox",[126,547,548],{"class":159},"(rgb);\n",[126,550,551,554],{"class":128,"line":240},[126,552,553],{"class":149},"    assert!",[126,555,556],{"class":159},"(\n",[126,558,559,562,565,568,570,572,575,578,581],{"class":128,"line":246},[126,560,561],{"class":159},"        (decoded ",[126,563,564],{"class":139},"-",[126,566,567],{"class":143}," 1000.5",[126,569,286],{"class":159},[126,571,289],{"class":139},[126,573,574],{"class":149},"abs",[126,576,577],{"class":159},"() \u003C ",[126,579,580],{"class":143},"0.001",[126,582,583],{"class":159},",\n",[126,585,586],{"class":128,"line":252},[126,587,589],{"class":588},"s4Y1p","        \"mapbox round-before-truncate 1000.5 → {rgb:?} → {decoded}\"\n",[126,591,592],{"class":128,"line":258},[126,593,594],{"class":159},"    );\n",[126,596,597],{"class":128,"line":298},[126,598,441],{"class":159},[37,600,601],{},"The clamp matters too. An absurd input — say a corrupt raster cell claiming 40,000 m — must saturate at the 24-bit ceiling, not wrap around a byte and come back as a plausible-looking valley:",[117,603,605],{"className":119,"code":604,"language":121,"meta":122,"style":122},"#[test]\nfn terrarium_clamps_out_of_range() {\n    assert_eq!(encode_terrarium(-40000.0), [0, 0, 0], \"terrarium underflow\");\n    assert_eq!(encode_terrarium(40000.0), [255, 255, 255], \"terrarium overflow\");\n}\n",[77,606,607,611,620,659,691],{"__ignoreMap":122},[126,608,609],{"class":128,"line":129},[126,610,487],{"class":159},[126,612,613,615,618],{"class":128,"line":136},[126,614,492],{"class":139},[126,616,617],{"class":149}," terrarium_clamps_out_of_range",[126,619,498],{"class":159},[126,621,622,625,627,630,632,634,637,640,643,645,647,649,651,654,657],{"class":128,"line":163},[126,623,624],{"class":149},"    assert_eq!",[126,626,316],{"class":159},[126,628,629],{"class":149},"encode_terrarium",[126,631,316],{"class":159},[126,633,564],{"class":139},[126,635,636],{"class":143},"40000.0",[126,638,639],{"class":159},"), [",[126,641,642],{"class":143},"0",[126,644,322],{"class":159},[126,646,642],{"class":143},[126,648,322],{"class":159},[126,650,642],{"class":143},[126,652,653],{"class":159},"], ",[126,655,656],{"class":588},"\"terrarium underflow\"",[126,658,533],{"class":159},[126,660,661,663,665,667,669,671,673,676,678,680,682,684,686,689],{"class":128,"line":169},[126,662,624],{"class":149},[126,664,316],{"class":159},[126,666,629],{"class":149},[126,668,316],{"class":159},[126,670,636],{"class":143},[126,672,639],{"class":159},[126,674,675],{"class":143},"255",[126,677,322],{"class":159},[126,679,675],{"class":143},[126,681,322],{"class":159},[126,683,675],{"class":143},[126,685,653],{"class":159},[126,687,688],{"class":588},"\"terrarium overflow\"",[126,690,533],{"class":159},[126,692,693],{"class":128,"line":188},[126,694,441],{"class":159},[69,696,698],{"id":697},"the-alpha-channel-will-betray-you","The alpha channel will betray you",[37,700,701],{},"Here's the gotcha I want you to remember if you remember nothing else. Elevation rasters have nodata — ocean, void fills, the edge of coverage. The obvious move is to write those pixels as transparent and call it a day.",[37,703,704,705,708,709,712,713,716],{},"MapLibre does not care about your transparency. Its ",[77,706,707],{},"DEMData"," constructor reads R, G, B and ",[88,710,711],{},"ignores the alpha channel entirely",". A transparent pixel still decodes its RGB to an elevation, and if you wrote ",[77,714,715],{},"(0, 0, 0, 0)"," in Mapbox-RGB encoding, that decodes to −10,000 m. Congratulations: your coastline now has a trench deeper than the Mariana next to every beach.",[37,718,719,720,723],{},"The fix is a ",[41,721,722],{},"sentinel RGB"," — a color that decodes to a deliberately out-of-band elevation:",[117,725,727],{"className":119,"code":726,"language":121,"meta":122,"style":122},"\u002F\u002F\u002F Default Terrarium nodata sentinel: (0, 0, 0) decodes to −32768 m.\nconst TERRARIUM_NODATA: [u8; 3] = [0, 0, 0];\n\u002F\u002F\u002F Default Mapbox-RGB nodata sentinel: (1, 134, 160) decodes to 0 m\n\u002F\u002F\u002F (sea level), the Mapbox convention for \"no data \u002F ocean\".\nconst MAPBOX_NODATA: [u8; 3] = [1, 134, 160];\n",[77,728,729,734,771,776,781],{"__ignoreMap":122},[126,730,731],{"class":128,"line":129},[126,732,733],{"class":132},"\u002F\u002F\u002F Default Terrarium nodata sentinel: (0, 0, 0) decodes to −32768 m.\n",[126,735,736,738,741,743,745,747,749,751,754,756,758,760,762,764,766,768],{"class":128,"line":136},[126,737,140],{"class":139},[126,739,740],{"class":143}," TERRARIUM_NODATA",[126,742,115],{"class":139},[126,744,225],{"class":159},[126,746,228],{"class":149},[126,748,231],{"class":159},[126,750,234],{"class":143},[126,752,753],{"class":159},"] ",[126,755,267],{"class":139},[126,757,225],{"class":159},[126,759,642],{"class":143},[126,761,322],{"class":159},[126,763,642],{"class":143},[126,765,322],{"class":159},[126,767,642],{"class":143},[126,769,770],{"class":159},"];\n",[126,772,773],{"class":128,"line":163},[126,774,775],{"class":132},"\u002F\u002F\u002F Default Mapbox-RGB nodata sentinel: (1, 134, 160) decodes to 0 m\n",[126,777,778],{"class":128,"line":169},[126,779,780],{"class":132},"\u002F\u002F\u002F (sea level), the Mapbox convention for \"no data \u002F ocean\".\n",[126,782,783,785,788,790,792,794,796,798,800,802,804,807,809,812,814,817],{"class":128,"line":188},[126,784,140],{"class":139},[126,786,787],{"class":143}," MAPBOX_NODATA",[126,789,115],{"class":139},[126,791,225],{"class":159},[126,793,228],{"class":149},[126,795,231],{"class":159},[126,797,234],{"class":143},[126,799,753],{"class":159},[126,801,267],{"class":139},[126,803,225],{"class":159},[126,805,806],{"class":143},"1",[126,808,322],{"class":159},[126,810,811],{"class":143},"134",[126,813,322],{"class":159},[126,815,816],{"class":143},"160",[126,818,770],{"class":159},[37,820,821,822,825,826,829,830,833,834,837],{},"That ",[77,823,824],{},"(1, 134, 160)"," is worth decoding by hand once: ",[77,827,828],{},"1*65536 + 134*256 + 160 = 100000",", and ",[77,831,832],{},"-10000 + 100000 * 0.1 = 0",". Sea level. Missing data renders as flat ocean, which is almost always what you want. The encoder still writes alpha 0 on nodata pixels — but as a courtesy to clients that ",[41,835,836],{},"do"," honor alpha, not as the mechanism.",[37,839,840],{},"The pixel loop treats three things as nodata: the band's declared nodata value, NaN, and infinity (GDAL masked reads produce non-finite values):",[117,842,844],{"className":119,"code":843,"language":121,"meta":122,"style":122},"#[must_use]\npub fn encode_pixels(values: &[f64], params: &EncodeParams) -> Vec\u003Cu8> {\n    let mut out = Vec::with_capacity(values.len() * 4);\n    for &raw in values {\n        let is_nodata = !raw.is_finite()\n            || params\n                .nodata_value\n                .is_some_and(|nd| (raw - nd).abs() \u003C f64::EPSILON);\n        if is_nodata {\n            out.extend_from_slice(&params.nodata_rgba);\n        } else {\n            let elevation = raw * params.scale + params.offset;\n            let [r, g, b] = encode_elevation(elevation, params.encoding);\n            out.extend_from_slice(&[r, g, b, 255]);\n        }\n    }\n    out\n}\n",[77,845,846,850,898,935,951,975,983,991,1031,1039,1061,1072,1104,1124,1144,1149,1154,1159],{"__ignoreMap":122},[126,847,848],{"class":128,"line":129},[126,849,197],{"class":159},[126,851,852,854,856,859,862,864,867,870,873,876,878,880,883,885,887,890,893,895],{"class":128,"line":136},[126,853,203],{"class":139},[126,855,206],{"class":139},[126,857,858],{"class":149}," encode_pixels",[126,860,861],{"class":159},"(values",[126,863,115],{"class":139},[126,865,866],{"class":139}," &",[126,868,869],{"class":159},"[",[126,871,872],{"class":149},"f64",[126,874,875],{"class":159},"], params",[126,877,115],{"class":139},[126,879,866],{"class":139},[126,881,882],{"class":149},"EncodeParams",[126,884,219],{"class":159},[126,886,222],{"class":139},[126,888,889],{"class":149}," Vec",[126,891,892],{"class":159},"\u003C",[126,894,228],{"class":149},[126,896,897],{"class":159},"> {\n",[126,899,900,902,905,908,910,912,915,918,920,922,925,928,930,933],{"class":128,"line":163},[126,901,261],{"class":139},[126,903,904],{"class":139}," mut",[126,906,907],{"class":159}," out ",[126,909,267],{"class":139},[126,911,889],{"class":149},[126,913,914],{"class":139},"::",[126,916,917],{"class":149},"with_capacity",[126,919,861],{"class":159},[126,921,289],{"class":139},[126,923,924],{"class":149},"len",[126,926,927],{"class":159},"() ",[126,929,280],{"class":139},[126,931,932],{"class":143}," 4",[126,934,533],{"class":159},[126,936,937,940,942,945,948],{"class":128,"line":169},[126,938,939],{"class":139},"    for",[126,941,866],{"class":139},[126,943,944],{"class":159},"raw ",[126,946,947],{"class":139},"in",[126,949,950],{"class":159}," values {\n",[126,952,953,956,959,961,964,967,969,972],{"class":128,"line":188},[126,954,955],{"class":139},"        let",[126,957,958],{"class":159}," is_nodata ",[126,960,267],{"class":139},[126,962,963],{"class":139}," !",[126,965,966],{"class":159},"raw",[126,968,289],{"class":139},[126,970,971],{"class":149},"is_finite",[126,973,974],{"class":159},"()\n",[126,976,977,980],{"class":128,"line":194},[126,978,979],{"class":139},"            ||",[126,981,982],{"class":159}," params\n",[126,984,985,988],{"class":128,"line":200},[126,986,987],{"class":139},"                .",[126,989,990],{"class":159},"nodata_value\n",[126,992,993,995,998,1000,1003,1006,1008,1011,1013,1016,1018,1020,1022,1024,1026,1029],{"class":128,"line":240},[126,994,987],{"class":139},[126,996,997],{"class":149},"is_some_and",[126,999,316],{"class":159},[126,1001,1002],{"class":139},"|",[126,1004,1005],{"class":159},"nd",[126,1007,1002],{"class":139},[126,1009,1010],{"class":159}," (raw ",[126,1012,564],{"class":139},[126,1014,1015],{"class":159}," nd)",[126,1017,289],{"class":139},[126,1019,574],{"class":149},[126,1021,577],{"class":159},[126,1023,872],{"class":149},[126,1025,914],{"class":139},[126,1027,1028],{"class":143},"EPSILON",[126,1030,533],{"class":159},[126,1032,1033,1036],{"class":128,"line":246},[126,1034,1035],{"class":139},"        if",[126,1037,1038],{"class":159}," is_nodata {\n",[126,1040,1041,1044,1046,1049,1051,1053,1056,1058],{"class":128,"line":252},[126,1042,1043],{"class":159},"            out",[126,1045,289],{"class":139},[126,1047,1048],{"class":149},"extend_from_slice",[126,1050,316],{"class":159},[126,1052,359],{"class":139},[126,1054,1055],{"class":159},"params",[126,1057,289],{"class":139},[126,1059,1060],{"class":159},"nodata_rgba);\n",[126,1062,1063,1066,1069],{"class":128,"line":258},[126,1064,1065],{"class":159},"        } ",[126,1067,1068],{"class":139},"else",[126,1070,1071],{"class":159}," {\n",[126,1073,1074,1077,1080,1082,1085,1087,1090,1092,1095,1097,1099,1101],{"class":128,"line":298},[126,1075,1076],{"class":139},"            let",[126,1078,1079],{"class":159}," elevation ",[126,1081,267],{"class":139},[126,1083,1084],{"class":159}," raw ",[126,1086,280],{"class":139},[126,1088,1089],{"class":159}," params",[126,1091,289],{"class":139},[126,1093,1094],{"class":159},"scale ",[126,1096,273],{"class":139},[126,1098,1089],{"class":159},[126,1100,289],{"class":139},[126,1102,1103],{"class":159},"offset;\n",[126,1105,1106,1108,1111,1113,1116,1119,1121],{"class":128,"line":338},[126,1107,1076],{"class":139},[126,1109,1110],{"class":159}," [r, g, b] ",[126,1112,267],{"class":139},[126,1114,1115],{"class":149}," encode_elevation",[126,1117,1118],{"class":159},"(elevation, params",[126,1120,289],{"class":139},[126,1122,1123],{"class":159},"encoding);\n",[126,1125,1126,1128,1130,1132,1134,1136,1139,1141],{"class":128,"line":367},[126,1127,1043],{"class":159},[126,1129,289],{"class":139},[126,1131,1048],{"class":149},[126,1133,316],{"class":159},[126,1135,359],{"class":139},[126,1137,1138],{"class":159},"[r, g, b, ",[126,1140,675],{"class":143},[126,1142,1143],{"class":159},"]);\n",[126,1145,1146],{"class":128,"line":392},[126,1147,1148],{"class":159},"        }\n",[126,1150,1151],{"class":128,"line":410},[126,1152,1153],{"class":159},"    }\n",[126,1155,1156],{"class":128,"line":438},[126,1157,1158],{"class":159},"    out\n",[126,1160,1162],{"class":128,"line":1161},18,[126,1163,441],{"class":159},[37,1165,1166,1167,448,1170,1173,1174,1177],{},"Note ",[77,1168,1169],{},"scale",[77,1171,1172],{},"offset"," in there: if your DEM is stored in feet (looking at you, some USGS products), ",[77,1175,1176],{},"scale = 0.3048"," fixes it at encode time instead of requiring a raster rewrite.",[37,1179,1180,1181,1184],{},"This function is deliberately pure — no GDAL types, just ",[77,1182,1183],{},"&[f64]"," in and RGBA bytes out. That's what makes every branch unit-testable without a native library in the loop, which is how the module carries 17 unit tests covering sea level, Everest (8848.86 m), the Mariana Trench floor (−10994 m), clamps, NaN, and the feet-to-meters path.",[69,1186,1188],{"id":1187},"from-geotiff-to-tile","From GeoTIFF to tile",[37,1190,1191],{},"The serving path is GDAL doing what GDAL does: warp the source raster to the tile's Web Mercator bounding box, read floats, encode, PNG:",[117,1193,1195],{"className":119,"code":1194,"language":121,"meta":122,"style":122},"let px = (maxx - minx) \u002F f64::from(tile_size);\nlet py = (maxy - miny) \u002F f64::from(tile_size);\nwarped.set_geo_transform(&[minx, px, 0.0, maxy, 0.0, -py])?;\nwarped.set_spatial_ref(&web_mercator)?;\nreproject(dataset, &warped)?;\n\nlet buffer: Buffer\u003Cf64> = out_band.read_as::\u003Cf64>(\n    (0, 0),\n    (tile_size as usize, tile_size as usize),\n    (tile_size as usize, tile_size as usize),\n    Some(resampling),\n)?;\n\nlet rgba = encode_pixels(buffer.data(), &params);\n",[77,1196,1197,1228,1255,1291,1311,1328,1332,1370,1384,1403,1419,1427,1435,1439],{"__ignoreMap":122},[126,1198,1199,1202,1205,1207,1210,1212,1215,1218,1220,1222,1225],{"class":128,"line":129},[126,1200,1201],{"class":139},"let",[126,1203,1204],{"class":159}," px ",[126,1206,267],{"class":139},[126,1208,1209],{"class":159}," (maxx ",[126,1211,564],{"class":139},[126,1213,1214],{"class":159}," minx) ",[126,1216,1217],{"class":139},"\u002F",[126,1219,150],{"class":149},[126,1221,914],{"class":139},[126,1223,1224],{"class":149},"from",[126,1226,1227],{"class":159},"(tile_size);\n",[126,1229,1230,1232,1235,1237,1240,1242,1245,1247,1249,1251,1253],{"class":128,"line":136},[126,1231,1201],{"class":139},[126,1233,1234],{"class":159}," py ",[126,1236,267],{"class":139},[126,1238,1239],{"class":159}," (maxy ",[126,1241,564],{"class":139},[126,1243,1244],{"class":159}," miny) ",[126,1246,1217],{"class":139},[126,1248,150],{"class":149},[126,1250,914],{"class":139},[126,1252,1224],{"class":149},[126,1254,1227],{"class":159},[126,1256,1257,1260,1262,1265,1267,1269,1272,1274,1277,1279,1281,1283,1286,1289],{"class":128,"line":163},[126,1258,1259],{"class":159},"warped",[126,1261,289],{"class":139},[126,1263,1264],{"class":149},"set_geo_transform",[126,1266,316],{"class":159},[126,1268,359],{"class":139},[126,1270,1271],{"class":159},"[minx, px, ",[126,1273,319],{"class":143},[126,1275,1276],{"class":159},", maxy, ",[126,1278,319],{"class":143},[126,1280,322],{"class":159},[126,1282,564],{"class":139},[126,1284,1285],{"class":159},"py])",[126,1287,1288],{"class":139},"?",[126,1290,160],{"class":159},[126,1292,1293,1295,1297,1300,1302,1304,1307,1309],{"class":128,"line":169},[126,1294,1259],{"class":159},[126,1296,289],{"class":139},[126,1298,1299],{"class":149},"set_spatial_ref",[126,1301,316],{"class":159},[126,1303,359],{"class":139},[126,1305,1306],{"class":159},"web_mercator)",[126,1308,1288],{"class":139},[126,1310,160],{"class":159},[126,1312,1313,1316,1319,1321,1324,1326],{"class":128,"line":188},[126,1314,1315],{"class":149},"reproject",[126,1317,1318],{"class":159},"(dataset, ",[126,1320,359],{"class":139},[126,1322,1323],{"class":159},"warped)",[126,1325,1288],{"class":139},[126,1327,160],{"class":159},[126,1329,1330],{"class":128,"line":194},[126,1331,191],{"emptyLinePlaceholder":21},[126,1333,1334,1336,1339,1341,1344,1346,1348,1351,1353,1356,1358,1361,1363,1365,1367],{"class":128,"line":200},[126,1335,1201],{"class":139},[126,1337,1338],{"class":159}," buffer",[126,1340,115],{"class":139},[126,1342,1343],{"class":149}," Buffer",[126,1345,892],{"class":159},[126,1347,872],{"class":149},[126,1349,1350],{"class":159},"> ",[126,1352,267],{"class":139},[126,1354,1355],{"class":159}," out_band",[126,1357,289],{"class":139},[126,1359,1360],{"class":149},"read_as",[126,1362,914],{"class":139},[126,1364,892],{"class":159},[126,1366,872],{"class":149},[126,1368,1369],{"class":159},">(\n",[126,1371,1372,1375,1377,1379,1381],{"class":128,"line":240},[126,1373,1374],{"class":159},"    (",[126,1376,642],{"class":143},[126,1378,322],{"class":159},[126,1380,642],{"class":143},[126,1382,1383],{"class":159},"),\n",[126,1385,1386,1389,1391,1394,1397,1399,1401],{"class":128,"line":246},[126,1387,1388],{"class":159},"    (tile_size ",[126,1390,330],{"class":139},[126,1392,1393],{"class":149}," usize",[126,1395,1396],{"class":159},", tile_size ",[126,1398,330],{"class":139},[126,1400,1393],{"class":149},[126,1402,1383],{"class":159},[126,1404,1405,1407,1409,1411,1413,1415,1417],{"class":128,"line":252},[126,1406,1388],{"class":159},[126,1408,330],{"class":139},[126,1410,1393],{"class":149},[126,1412,1396],{"class":159},[126,1414,330],{"class":139},[126,1416,1393],{"class":149},[126,1418,1383],{"class":159},[126,1420,1421,1424],{"class":128,"line":258},[126,1422,1423],{"class":149},"    Some",[126,1425,1426],{"class":159},"(resampling),\n",[126,1428,1429,1431,1433],{"class":128,"line":298},[126,1430,286],{"class":159},[126,1432,1288],{"class":139},[126,1434,160],{"class":159},[126,1436,1437],{"class":128,"line":338},[126,1438,191],{"emptyLinePlaceholder":21},[126,1440,1441,1443,1446,1448,1450,1453,1455,1458,1461,1463],{"class":128,"line":367},[126,1442,1201],{"class":139},[126,1444,1445],{"class":159}," rgba ",[126,1447,267],{"class":139},[126,1449,858],{"class":149},[126,1451,1452],{"class":159},"(buffer",[126,1454,289],{"class":139},[126,1456,1457],{"class":149},"data",[126,1459,1460],{"class":159},"(), ",[126,1462,359],{"class":139},[126,1464,1465],{"class":159},"params);\n",[37,1467,1468,1469,1472],{},"One decision here saved real precision: the pipeline reads ",[77,1470,1471],{},"Buffer\u003Cf64>"," straight from GDAL and encodes float-to-RGB directly. No intermediate 8-bit PNG, no quantize-then-requantize round trip. The float leaves the raster exactly once.",[37,1474,1475],{},"The config side wraps any existing raster source, so an operator who already serves a Copernicus COG adds terrain with four lines of TOML:",[117,1477,1481],{"className":1478,"code":1479,"language":1480,"meta":122,"style":122},"language-toml shiki shiki-themes github-dark github-dark github-light","[[sources]]\nid = \"terrain\"\ntype = \"dem\"\ninput_source = \"copernicus-dem\" # reuse the already-open COG\ndem_encoding = \"terrarium\"\n","toml",[77,1482,1483,1494,1502,1510,1521],{"__ignoreMap":122},[126,1484,1485,1488,1491],{"class":128,"line":129},[126,1486,1487],{"class":159},"[[",[126,1489,1490],{"class":149},"sources",[126,1492,1493],{"class":159},"]]\n",[126,1495,1496,1499],{"class":128,"line":136},[126,1497,1498],{"class":159},"id = ",[126,1500,1501],{"class":588},"\"terrain\"\n",[126,1503,1504,1507],{"class":128,"line":163},[126,1505,1506],{"class":159},"type = ",[126,1508,1509],{"class":588},"\"dem\"\n",[126,1511,1512,1515,1518],{"class":128,"line":169},[126,1513,1514],{"class":159},"input_source = ",[126,1516,1517],{"class":588},"\"copernicus-dem\"",[126,1519,1520],{"class":132}," # reuse the already-open COG\n",[126,1522,1523,1526],{"class":128,"line":188},[126,1524,1525],{"class":159},"dem_encoding = ",[126,1527,1528],{"class":588},"\"terrarium\"\n",[37,1530,1531,1534,1535,1538,1539,1542],{},[77,1532,1533],{},"input_source"," shares the referenced source's cached GDAL dataset handle — no second file open, no second block cache. The alternative was every operator pre-baking terrain tiles with ",[77,1536,1537],{},"rio terrain"," or ",[77,1540,1541],{},"gdaldem"," and doubling their storage, or running a second tile server just for elevation. We already had GDAL in the building; the encoder is a couple hundred lines.",[69,1544,1546],{"id":1545},"the-tests-pass-is-not-the-finish-line","\"The tests pass\" is not the finish line",[37,1548,1549,1550,1553],{},"The part of this feature I'm most confident in isn't the unit tests — it's the QA that happened after them, because encoding correctness and ",[41,1551,1552],{},"served-tile"," correctness are different claims. From the PR:",[82,1555,1556,1563,1573],{},[85,1557,1558,1559,1562],{},"Built with ",[77,1560,1561],{},"--features dem",", served both encodings, fetched real tiles over HTTP: Terrarium 173 KB, Mapbox-RGB 130 KB, both valid PNG.",[85,1564,1565,1568,1569,1572],{},[88,1566,1567],{},"Decoded the served tiles back to elevations"," and checked them against the fixture's known range: got 0–818.6 m against a truth of −41 to 1041 m. The decode-what-you-served step is the one that catches a bug the encoder's own round-trip tests structurally cannot — anything that happens between ",[77,1570,1571],{},"encode_pixels"," and the HTTP response.",[85,1574,1575,1576,1578],{},"Drove MapLibre GL JS in a real browser with the server as a ",[77,1577,79],{}," hillshade source: 20+ tiles at HTTP 200, actual relief on the actual screen, zero console errors.",[37,1580,1581],{},"A byte-exact encoder proves your math. Decoding your own served tiles proves your pipeline. A browser rendering hillshade proves the whole claim. They're three different tests, and terrain has enough silent-garbage failure modes that I now consider all three mandatory for anything raster-shaped.",[37,1583,1584],{},"For throughput nerds: the encoder benches at roughly a nanosecond per pixel — a full 256×256 tile encodes at 110–195 Melem\u002Fs — so the cost of a DEM tile is dominated by the GDAL warp, not the encoding.",[37,1586,1587,1588,1593],{},"If you want to see the result instead of reading about it, there's a ",[49,1589,1592],{"href":1590,"rel":1591},"https:\u002F\u002Fdemo.tileserver.app\u002Fstyles\u002Feverest-terrain\u002F#8.35\u002F27.461\u002F86.504\u002F0\u002F60",[53],"live demo of Everest in 3D terrain"," — that's a Copernicus DEM going through this exact encoder, rendered by MapLibre with the camera pitched at the Himalayas.",[37,1595,1596,1597,1602],{},"If you're serving elevation from COGs and want terrain without another service in the stack, the feature ships in ",[49,1598,1601],{"href":1599,"rel":1600},"https:\u002F\u002Fgithub.com\u002Fvinayakkulkarni\u002Ftileserver-rs\u002Freleases",[53],"tileserver-rs v2.34.0+",". And if you're writing your own encoder: round before you truncate, clamp before you cast, and never, ever trust the alpha channel.",[1604,1605,1606],"style",{},"html pre.shiki code .sxXm1, html code.shiki .sxXm1{--shiki-default:#6A737D;--shiki-dark:#6A737D;--shiki-light:#6A737D}html pre.shiki code .s6ubI, html code.shiki .s6ubI{--shiki-default:#F97583;--shiki-dark:#F97583;--shiki-light:#D73A49}html pre.shiki code .sXWYR, html code.shiki .sXWYR{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-light:#005CC5}html pre.shiki code .s5n6i, html code.shiki .s5n6i{--shiki-default:#B392F0;--shiki-dark:#B392F0;--shiki-light:#6F42C1}html pre.shiki code .sAvwS, html code.shiki .sAvwS{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8;--shiki-light:#24292E}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .s4Y1p, html code.shiki .s4Y1p{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-light:#032F62}",{"title":122,"searchDepth":136,"depth":136,"links":1608},[1609,1610,1611,1612,1613],{"id":71,"depth":136,"text":72},{"id":454,"depth":136,"text":455},{"id":697,"depth":136,"text":698},{"id":1187,"depth":136,"text":1188},{"id":1545,"depth":136,"text":1546},"gis","2026-07-12T11:30:00.000+05:30","How tileserver-rs turns raw GDAL elevation rasters into Terrarium and Mapbox-RGB terrain tiles that MapLibre renders as 3D terrain. The math, the classic 1-LSB encoding bug, and why the alpha channel will betray you.","md",null,{"position":240},"\u002Farticles\u002Fencoding-elevation-into-pixels",{"title":32,"description":1616},"published","articles\u002F8.encoding-elevation-into-pixels",[121,1614,1625,1626,1627,1628],"maplibre","dem","terrain","gdal","4PlI61yzQ6sXJ0pOd_tcxtvSC7X_47BLeeU1-SK1nK0",[1631,1633,1646,1655,1664,1672,1681,1687],{"path":1620,"title":32,"description":1616,"date":1615,"updatedAt":1618,"category":1614,"tags":1632,"image":1618,"status":1622},[121,1614,1625,1626,1627,1628],{"path":1634,"title":1635,"description":1636,"date":1637,"updatedAt":1618,"category":1638,"tags":1639,"image":1618,"status":1622},"\u002Farticles\u002Fcutting-a-supabase-bill-to-zero-with-cloudflare-d1","How I cut a $35 Supabase bill to $0 with Cloudflare D1","Moving a live multi-tenant SaaS off Supabase Postgres onto Cloudflare D1 with zero downtime. The schema rewrite was the easy part. The date-coercion bug, the PostGIS audit, and the break-even math were not.","2026-07-07T18:30:00.000+05:30","cloudflare",[1638,1640,1641,1642,1643,1644,1645],"d1","supabase","sqlite","drizzle","nuxt","migration",{"path":1647,"title":1648,"description":1649,"date":1650,"updatedAt":1618,"category":1644,"tags":1651,"image":1618,"status":1622},"\u002Farticles\u002Ftil-shiki-multitheme-code-block-background","TIL: Shiki multi-theme highlighting does not switch the code-block background","Shiki multi-theme mode swaps token colours per colour scheme but leaves the pre background variable empty, so light-theme text can land on a dark block. You have to theme the container yourself.","2026-07-07T02:30:00.000+05:30",[1644,1652,1653,1654],"shiki","nuxt-content","dark-mode",{"path":1656,"title":1657,"description":1658,"date":1659,"updatedAt":1618,"category":1638,"tags":1660,"image":1618,"status":1622},"\u002Farticles\u002Fdynamic-og-images-on-cloudflare-workers","Generating dynamic Open Graph images on Cloudflare Workers","Rendering per-page and per-article social cards at the edge with Satori and @cf-wasm\u002Fog: how the route works, what Satori refuses to render, and the cold-versus-warm numbers.","2026-07-06T22:00:00.000+05:30",[1638,1661,1662,1663,1644],"workers","open-graph","satori",{"path":1665,"title":1666,"description":1667,"date":1668,"updatedAt":1618,"category":1638,"tags":1669,"image":1618,"status":1622},"\u002Farticles\u002Fmigrating-a-nuxt-site-from-cloudflare-pages-to-workers","Migrating a Nuxt site from Cloudflare Pages to Workers","The full path from the cloudflare-pages preset to a cloudflare_module Worker: the config changes, the custom-domain cutover that no deploy step handles for you, and the gotchas that only show up in CI.","2026-07-06T21:00:00.000+05:30",[1638,1661,1644,1670,1671],"nitro","devops",{"path":1673,"title":1674,"description":1675,"date":1676,"updatedAt":1618,"category":1677,"tags":1678,"image":1618,"status":1622},"\u002Farticles\u002Ftil-tailwind-v4-webkit-mask-prefix","TIL: Tailwind v4 mask utilities need the webkit prefix in plain CSS","Porting a Tailwind mask-composite utility to scoped CSS quietly breaks in Chrome and Safari unless you also write the -webkit-mask-* properties, because Tailwind was emitting them for you.","2026-07-06T20:15:00.000+05:30","css",[1679,1677,1680],"tailwind","webkit",{"path":1682,"title":1683,"description":1684,"date":1685,"updatedAt":1618,"category":1638,"tags":1686,"image":1618,"status":1622},"\u002Farticles\u002Ftil-workers-cache-nitro-passthrough","TIL: Enabling Workers Cache through Nitro without a native option","Nitro forwards arbitrary keys into the generated wrangler config, so you can turn on Cloudflare Workers Cache with a single line even though there is no dedicated Nitro option for it.","2026-07-06T14:30:00.000+05:30",[1638,1661,1670,1644],{"path":1688,"title":1689,"description":1690,"date":1691,"updatedAt":1618,"category":1692,"tags":1693,"image":1618,"status":1622},"\u002Farticles\u002Fbuild-and-publish-vue-3-components-using-rollup","Build & Publish Vue 3 Components using Rollup.js","Let's build a reusable Vue 3 component using rollup","2022-07-04T16:20:09.000+05:30","vue",[1694,1695],"vue 3","rollup",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":1697},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m12 19l-7-7l7-7m7 7H5\"\u002F>",1783847885538]