diff --git a/app/public/animeface-cascade.json b/app/public/animeface-cascade.json new file mode 100644 index 0000000..a32167c --- /dev/null +++ b/app/public/animeface-cascade.json @@ -0,0 +1 @@ +{"license":"lbpcascade_animeface, https://github.com/nagadomi/lbpcascade_animeface\nThe MIT License (MIT)\n\nCopyright (C) 2011 by nagadomi@nurs.or.jp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.","w":24,"h":24,"stages":[[-1.263674259185791,[[103,[-1302,-1,-2097218,-1,-1,-1,-1,-1],[-0.8628431558609009,0.8288403153419495]],[111,[-270553558,-16593,-1342197841,-24657,-1069073,-28753,-268456017,-28753],[-0.8301590085029602,0.5024479627609253]],[274,[-791883568,-570557057,-10521123,-2261129,-168835596,-14680065,-536870913,-577],[-0.6802006363868713,0.4293278455734253]]]],[-1.636199951171875,[[17,[-67110150,-1,-1,-1,-1,-1,-1,-1],[-0.7564964890480042,0.6566571593284607]],[226,[-21846,-18501,-546118,-17745,-67130689,-20481,-21589,-5185],[-0.6622880697250366,0.45048093795776367]],[167,[-17104902,269658770,-5046274,-14539742,-1,-67897614,-1,-11301],[-0.6063612699508667,0.41438210010528564]],[410,[-67109392,-5,-537133576,-1282,1154814128,-4638,-1448359800,-71304194],[-0.5924391746520996,0.35992857813835144]],[329,[-422583808,2030231298,-445326064,-35408702,1313755528,222323776,-589330296,-3146022],[-0.5127382278442383,0.39016643166542053]],[196,[-927858550,1525075679,149029012,-71367005,1365708031,-386200322,-419477761,-1110321],[-0.4261343479156494,0.3813542425632477]]]],[-1.6592464447021484,[[112,[-18,-1,-17,-1,-1,-1,-1,-1],[-0.7186874747276306,0.5039339661598206]],[76,[-98500654,-327681,-81592321,-1,-67641345,-1,-525313,-1],[-0.6528414487838745,0.40119534730911255]],[405,[-571474700,-4097,-1409548289,-1,-1939013504,-65537,-269484808,-1],[-0.5936161279678345,0.3178099989891052]],[121,[-486563073,-218104129,553639679,-88085761,1082066687,-151040033,-37889,-32769],[-0.38881537318229675,0.46969282627105713]],[91,[-28754,-4741217,-5787766,-4272213,-268464465,-71364690,-340815953,-1616049394],[-0.5380537509918213,0.3086091876029968]],[446,[419443088,285235408,254456305,-16204592,-1391444552,-1359433799,-1424163143,-1079260968],[-0.42636507749557495,0.3572568893432617]],[327,[-597814534,-555989829,-69009699,-13080945,1523056856,-23444034,-222757126,-72695094],[-0.3299240469932556,0.4131940007209778]],[564,[253993232,855774768,261231921,1605348720,1061134545,-1109396681,-8408880,-1073744225],[-0.43008872866630554,0.29400870203971863]]]],[-1.54403817653656,[[288,[-65537,-331809,-5586945,-5595969,-4524038,-5578822,-72683526,-1431651670],[-0.49188971519470215,0.4876288175582886]],[65,[-168317009,-167792753,-101341265,-677969,-201347153,-675921,-67260497,-1077966929],[-0.5765073299407959,0.3772933781147003]],[505,[-264195,-4677924,-67109379,-7370536,-16903,-16933387,-1614832472,-542162864],[-0.46903330087661743,0.4083128273487091]],[267,[-2295048,-3,-34537480,-131073,-1281,-5121,-1,-1],[-0.5294912457466125,0.295517235994339]],[359,[134218832,1346373084,485543039,-571861671,470810624,-1715453444,-671033126,-1146484562],[-0.43582525849342346,0.34078335762023926]],[531,[-205802701,855872035,-1275381997,-1751670853,-545348677,-1210587213,-71320902,-1161839685],[-0.3371037542819977,0.453489750623703]],[118,[-487093505,-218127617,-1560391937,-62721,1077346555,2063540991,-134275329,-47553],[-0.31872883439064026,0.44437479972839355]],[50,[-142610526,-253073502,-92102144,-741350074,-150497792,4218884,-785930752,-246946298],[-0.35306012630462646,0.33983978629112244]],[207,[-1024537970,-689028406,147245278,1885009118,-937884982,911296300,-280506658,-73676801],[-0.32728925347328186,0.3312322199344635]],[570,[-1086335181,-2026903933,402122549,-11553395,-2038958942,-2047885573,-747113584,-3150353],[-0.2583779990673065,0.41633638739585876]],[198,[-98401792,-257947744,744505346,-44030416,1449330176,-1224726752,-13357430,-215989504],[-0.33786505460739136,0.3310038447380066]]]],[-1.5968077182769775,[[161,[-327682,-234815982,-9217,-327702,-67108865,-19,-35169,-1025],[-0.6077409982681274,0.3348584771156311]],[42,[-234901585,-134893649,-101453937,-676433,-167792721,-840273,-67662929,-61685],[-0.5801490545272827,0.2930995523929596]],[462,[-8391180,-9439249,-262916,-265250,2137191648,-8976,-1048856,-71303446],[-0.5099717378616333,0.3178481161594391]],[295,[-607128576,-2136813818,-646465352,-33556977,-732956532,-871835636,-1461741368,-5243185],[-0.4626306891441345,0.2914928197860718]],[227,[-833836928,-547492971,-36372488,-6209,-69293062,-1788225,-67174657,-1],[-0.710062563419342,0.15655440092086792]],[120,[-1024463121,-202396945,-1428264195,-2102581,1473185007,-33559809,-83932193,-39353],[-0.2852258086204529,0.4243815243244171]],[327,[-665695145,-638053093,-634372129,2134510939,-62259176,-570765889,-66531622,-1154847045],[-0.2963193356990814,0.3593541085720062]],[548,[-1357384943,-343742928,-1612720912,-538063840,-1956010591,-2003587624,131028417,-538978871],[-0.3405310809612274,0.3084990084171295]],[535,[-138422528,588489479,1067695873,-1074794561,-27345985,-1342177351,-1360024918,-1429214225],[-0.2772350311279297,0.3785536289215088]],[32,[-69217521,318980663,-546308677,553910191,-1119418848,1023541829,-1078203530,1426587151],[-0.34162473678588867,0.2840517461299896]],[68,[-272896094,-1060178292,-563023360,-740103008,-421596795,4254308,-151788864,-804784960],[-0.3335507810115814,0.25905099511146545]],[218,[2080341787,1060846641,-1107591393,-608438981,-537157638,-1879067494,1226316347,272137786],[-0.40187957882881165,0.21486374735832214]],[171,[-1057233345,-629705185,-500534530,807650042,68650975,-92965029,2013228271,-135284769],[-0.3021503984928131,0.2727862298488617]],[221,[1509885074,2004291294,555376605,1073426430,2135919867,267332795,-620820929,1006580307],[-0.33670711517333984,0.25498929619789124]]]],[-1.5330305099487305,[[242,[-168299308,-103680513,-17039875,-16513,-104858115,-1093,-68157441,-1074807809],[-0.5250251889228821,0.3111686110496521]],[146,[-1052690,-134217745,-4114,-134744587,-67108865,-101187585,-7185,-17],[-0.5587370991706848,0.2360144555568695]],[472,[-1086455823,520682672,2147352049,-5382736,-1433403459,-21364743,-1883244099,-541076003],[-0.31670504808425903,0.4062115252017975]],[202,[1224607962,1392201471,-2097544450,-88674562,136185054,-251707461,-319042850,-67147009],[-0.435680627822876,0.2630350887775421]],[246,[-327758,-1682395368,-101499042,-4484198,-264197,-4276368,-67372516,-64294],[-0.3509634733200073,0.2855299413204193]],[568,[-1221597405,926924550,-1216481488,-7340245,-6297677,-571475207,-142608393,-81],[-0.25762778520584106,0.3931208550930023]],[145,[-550531409,-4223169,-1799410513,-40915442,-100806657,-71439443,-290865,-5271921],[-0.36129701137542725,0.25089186429977417]],[518,[973252242,1059044139,691879211,1033633599,1061157819,1052455857,1001855675,994000443],[-0.40735718607902527,0.21259017288684845]],[292,[-155561249,-121033,-227353697,822221407,-732409764,-134860169,-5579014,-81596545],[-0.2780284583568573,0.3157797157764435]],[117,[-492377361,-67966273,-487796737,-537989558,1157580795,-623117,-33726753,-571901],[-0.23659884929656982,0.3690859377384186]],[49,[-3146078,-87102466,1929375776,-110170912,-209718716,1073987584,-172501248,-263724778],[-0.3126767873764038,0.2740819752216339]],[608,[-17830961,-52594705,1894769378,-759235926,-746588864,1145539840,-476648760,-255332678],[-0.23596332967281342,0.33085909485816956]],[294,[1787490066,-264197568,2011318014,-10345952,1816350540,1309473484,-1075852357,-347121653],[-0.3239920735359192,0.23825044929981232]],[58,[538050560,10548908,633938201,761344263,1178943808,1145357893,1473577739,17132883],[-0.43849360942840576,0.17257992923259735]],[62,[-1343377669,-67129357,-1478513667,536586967,-1342462217,-1141683202,-805589267,268372159],[-0.3904886543750763,0.19998973608016968]],[469,[-1141917951,2055732023,-67115040,-5259282,891279264,-1073742033,-252723072,-70254961],[-0.32046061754226685,0.2269742637872696]],[148,[-123294037,-629126677,-129217217,-106165406,-58765569,-53989993,-92475474,-260742313],[-0.22333471477031708,0.33217769861221313]],[270,[11436672,899321384,-2035277788,-134750226,494898348,805293739,-134222129,-1330],[-0.5181822776794434,0.14062055945396423]]]],[-1.617965817451477,[[251,[-16842753,-65537,-1078263809,992149503,-21315590,-65537,-4530178,-1155334401],[-0.40925416350364685,0.37826526165008545]],[506,[-13,-37748737,-67108878,-1,-2061,-16385,-134744078,-134217729],[-0.33893144130706787,0.38312068581581116]],[123,[-33558546,-34087433,-34078866,-33554433,-68423681,-17175821,-100925441,-1025],[-0.6069239974021912,0.1872701644897461]],[495,[1061158675,993509311,-1073758401,-1077936129,2139094975,2147450879,-68175185,721352703],[-0.34320926666259766,0.2941015064716339]],[188,[-889254689,-606139649,-822276097,-536932833,-894497025,-1694560673,-302051361,1543438867],[-0.3382209241390228,0.2658557891845703]],[387,[2110840317,-170373132,-67108867,-492679,-1898390020,-458756,-5701956,-1414920152],[-0.30869945883750916,0.2828564941883087]],[40,[-390074625,-96482225,-35656193,-571112243,216501503,-1393570577,-1933586817,1087717587],[-0.3291912078857422,0.22378942370414734]],[438,[-8388612,-71569970,-12587010,-5313858,-885750552,-17113860,-303256326,-1398104918],[-0.3155522048473358,0.2283312976360321]],[300,[-44092621,1534539583,2113879867,-72385733,2147434111,-4440513,2034203967,1937456447],[-0.5197641849517822,0.14146961271762848]],[168,[-352334129,-767580353,552889982,1920724734,-713601266,1983999843,-3411973,-201588757],[-0.2473347932100296,0.2858215272426605]],[609,[-6225,-295247985,-759442752,1084948202,-68348,1141308928,-86191156,-253235222],[-0.24679842591285706,0.2772952914237976]],[419,[151016821,184578384,-550121483,524098780,139726044,-1076822018,-1879556100,-1078263671],[-0.256909042596817,0.266051709651947]],[302,[-539102470,-2497,-1107343618,-33566754,-268571713,-16450,-1093681969,-67633153],[-0.2787766456604004,0.2446342259645462]],[485,[-4743040,-1107956700,-1376293472,-812648524,-846885756,-1113473919,-2031764308,76926853],[-0.2878116965293884,0.2346356213092804]],[86,[-209870857,2010119803,83886091,1057456929,2046623741,-174587905,565776015,2012079087],[-0.21731185913085938,0.297241747379303]],[173,[2130440170,-1494225046,-1116212241,1877473231,58685261,1179325709,-612376625,-5169],[-0.24395407736301422,0.2575765550136566]],[618,[-92280577,-106446978,-151355790,1624177388,-402930100,-145101820,-1224807732,-798818356],[-0.22018355131149292,0.28403928875923157]],[199,[1711793897,430272415,788433915,1073703931,2113740797,2113925119,-33605377,553123759],[-0.30896610021591187,0.20028412342071533]],[588,[-278925405,-1884375241,-17322192,-541066313,-1755323001,126740978,-671090992,-135268353],[-0.21448580920696259,0.29228198528289795]],[15,[-134217809,-605290706,-344462688,-236258702,-12599545,1128721759,-134241472,-134220842],[-0.20150026679039001,0.3063429594039917]],[308,[-62849320,2136267568,1429265656,-39780400,2105335016,-4222980,-222437720,-131148],[-0.5517482161521912,0.11506053060293198]],[385,[269753717,520624093,-1124313601,2109618489,894436372,-1619636227,1608012104,-1414922213],[-0.2652745842933655,0.22207336127758026]]]],[-1.5240784883499146,[[209,[-16777473,-83890433,-764480770,1914896094,-8241,-4194369,-17,-8388609],[-0.37706291675567627,0.34414002299308777]],[471,[-638582861,-214171669,-503319568,-33554449,-2144,-340000769,1375195600,-671088657],[-0.401825875043869,0.28752782940864563]],[4,[-20561,-235683841,-937985,-33574963,-201774161,-67426137,-67260433,-8977137],[-0.3826923668384552,0.23733018338680267]],[538,[-142608637,858242867,-2285,-1077936385,-2121,-1073750089,-269484113,-1358958865],[-0.25483861565589905,0.3326546549797058]],[204,[1484409054,-81102033,36761342,1375797182,1248987294,-11018250,-30220550,-15466497],[-0.3411288559436798,0.24120210111141205]],[371,[-539165220,-131082,-281215236,-1130504,-54722596,-272891953,-54724376,-5138],[-0.4527590572834015,0.17377805709838867]],[25,[-1493,-202376410,-1023938528,-251920682,-140545280,1682265604,-143133120,-211288085],[-0.27601632475852966,0.2942805588245392]],[210,[2130668066,-1372889750,-1073898641,-1058833,1147095110,1192205127,-872415281,-11534385],[-0.27625253796577454,0.26642629504203796]],[99,[-4200469,-135335198,-203431774,-70261556,-67111946,-2147048126,-145230366,-203950650],[-0.19389793276786804,0.3761134743690491]],[115,[-353457425,-74805585,-353472977,-1176914,1693926379,2146838449,-134410392,2135245639],[-0.2886756360530853,0.24483714997768402]],[508,[-1148733519,780856288,-1276684815,-15737456,-1346395480,-1880315220,97883549,-570434343],[-0.24260485172271729,0.28319093585014343]],[612,[-335550942,1122675878,39428642,44487402,-202381696,147984,-530587456,-1060116485],[-0.3207935690879822,0.2175094038248062]],[487,[-2118783,-1086344025,-1094734171,-1375868937,-5919064,-1073826123,-1598643539,16056055],[-0.23602557182312012,0.2590687870979309]],[637,[-134222161,-892340722,-85984544,-521142342,-202377530,2323523,-135792960,-255066161],[-0.21411967277526855,0.2883610129356384]],[122,[-621281281,305332202,-262286,-4728238,-36700161,2138570743,-8388641,-201],[-0.21160617470741272,0.2865854501724243]],[66,[-68164637,-909443356,-415296912,-679676768,-1153701470,-2033649432,1979708660,-716703808],[-0.21729548275470734,0.2750277519226074]],[166,[-488641938,1886375671,-894547222,1886317154,1869480350,571498307,-212336949,-227541041],[-0.24970392882823944,0.22789211571216583]],[474,[-1961000015,-1450948560,-688194123,-33689132,-2113163334,-1933923148,537001,-1981819431],[-0.22741013765335083,0.2472570389509201]],[234,[16385024,813192181,1071056213,1557346127,1002160826,-1178714129,-544243969,-1420854370],[-0.5305399894714355,0.10699179023504257]],[594,[-100673809,-201327669,-117970254,-756099110,-136314996,1917240912,-77683000,-229638210],[-0.17796075344085693,0.3293799161911011]],[545,[285687569,991770411,-268453965,-586154031,248379123,-1477454505,1467478960,-556794161],[-0.22852730751037598,0.24927006661891937]],[233,[1350551227,280743415,-2000770498,855637755,1912588527,955645951,-150999109,551549951],[-0.28712141513824463,0.21096032857894897]]]],[-1.6037335395812988,[[203,[-84213766,1490747034,-136087790,-1334,-610271233,-16781314,-287582050,-9042],[-0.4084002375602722,0.24690119922161102]],[536,[-146803789,322156531,-536873673,-1111623753,-167791621,-285185,-1073742917,-1077952581],[-0.22172482311725616,0.40040645003318787]],[255,[-171060792,-620071486,-1626088625,-1078985329,-2142658748,1330043724,-78652593,1263533903],[-0.32599684596061707,0.23890890181064606]],[95,[-34493009,-235280985,-240005745,-222980691,-168720385,-240078929,-487551569,-574910835],[-0.42127540707588196,0.18673938512802124]],[325,[-539101185,2138416567,760823775,-154689,-5141009,-1229192670,771426991,15139330],[-0.3363325297832489,0.23910291492938995]],[101,[-630216070,-757354758,-1830057793,-100695298,-788821861,-1160568079,-2101537,1509884027],[-0.2783498764038086,0.23219773173332214]],[350,[-133121,-396388,-575086596,-2880236,-605245958,-2565644,-7287816,201345105],[-0.341799795627594,0.19213064014911652]],[290,[-933606177,-657453485,-298165537,302651455,-858897400,-83995559,-588587302,-13967649],[-0.2805669605731964,0.2174130082130432]],[525,[437239691,1000060863,221724059,-1073743937,2095005119,-1094770761,1043807935,1054674879],[-0.3048814535140991,0.20650653541088104]],[184,[-18895094,-131137,-176166466,-203950081,-679546994,-458838209,-68609,-83903493],[-0.47668248414993286,0.1267579048871994]],[12,[2070372663,1998954923,1360207630,1065855538,1377767440,1048259495,1532963338,1968650755],[-0.2667206823825836,0.2210763692855835]],[507,[-922484943,-2027893262,-12585487,-8266672,-1488986463,-1674711105,1393415553,-542141047],[-0.2740706503391266,0.22819964587688446]],[194,[1354368042,814917051,-1126355395,880586619,1559710910,2029785056,-89175330,553090803],[-0.3573450744152069,0.15573251247406006]],[589,[-282073181,261095098,1871689490,-541068327,55427495,8912388,612324563,-705694721],[-0.2390359789133072,0.23758962750434875]],[104,[-206573910,-422925430,-386145678,-136123704,-286278705,1141151376,-479206272,-708320058],[-0.23658229410648346,0.2405620813369751]],[206,[-654652226,-2476513,-1573208611,855821951,-660959876,-9016767,-1131742502,-14483617],[-0.26215147972106934,0.21193760633468628]],[390,[-9010944,-680453375,-1887353857,-47901136,-608526176,-574418016,-1046431574,536871426],[-0.3288300037384033,0.16450397670269012]],[448,[39859029,1953028272,222289789,-538380840,665888169,-71795760,248524477,-1950416872],[-0.2608546018600464,0.2046564668416977]],[24,[686465762,-1275658177,692424703,-1350105627,-1308678529,-1141437607,88731642,802765499],[-0.4210531413555145,0.12438227236270905]],[617,[-16784885,-92289121,-1600719916,-798303556,-159384564,805854224,-128647992,-536622122],[-0.21299290657043457,0.24448233842849731]],[163,[-521214738,-220246129,-1561452498,-219421974,1080886061,308284728,1942440958,-139730089],[-0.23567050695419312,0.20919010043144226]],[559,[-218116608,2854795,586149408,231713907,-136605256,888171669,-152650100,-1393765652],[-0.2231275737285614,0.22375470399856567]],[581,[-180380895,1437970737,1001598225,-147463246,375874851,623058979,589757370,50832259],[-0.26574811339378357,0.18162758648395538]],[176,[-349183357,-205064856,1591152880,-177359388,-1414794578,-846925632,100053228,-985415232],[-0.18771494925022125,0.2632349133491516]],[360,[1329651669,1544846740,-1624838371,-81289607,1174445848,-636449096,-811966501,-609253157],[-0.14781230688095093,0.32182273268699646]],[426,[320255795,-1122912577,1562588985,-3678414,81212071,-1345083980,1775598843,828858931],[-0.3038376569747925,0.15808410942554474]],[611,[-14681,-1041249298,551283406,1089139306,-721422588,14971399,-404950336,-1057492274],[-0.2350015789270401,0.19808870553970337]],[327,[-656473793,-1621491117,-252526727,360189542,-967046976,-54702987,2092583960,-81592757],[-0.22867658734321594,0.21082723140716553]],[195,[-213859158,-17798588,-2144935180,-674987740,-681448807,-826009773,-993984833,-74230701],[-0.17290616035461426,0.2766934335231781]]]],[-1.5635454654693604,[[29,[-218103809,276561887,-138412065,-1,822083583,1895301085,-131073,-8193],[-0.2779134511947632,0.2914063334465027]],[410,[-134217732,-2124833,-1141117716,-545051746,-327187232,-16385,-923826040,-290455878],[-0.47651201486587524,0.16560232639312744]],[291,[-488453418,-36125012,1580386906,-6863783,1078215240,1288454875,-834384306,-536917493],[-0.2843717634677887,0.23990501463413239]],[196,[-631371010,-571503013,-394375427,-53617,-699199745,-100726021,-168882689,2079264479],[-0.2852992117404938,0.22660185396671295]],[217,[-548034902,-67108929,-678035457,-33557569,-203424081,-3147385,-6913,-2097425],[-0.38240256905555725,0.16137607395648956]],[566,[-1155272272,-550848592,-304122592,-1055752,-807405859,-842933767,1224691796,-572523826],[-0.2944502532482147,0.21465636789798737]],[70,[-202375193,-152109334,-939854174,-1049502,-146817149,1111796162,-201330492,-237504574],[-0.20517322421073914,0.29565006494522095]],[362,[-562278827,-589558940,-709079073,1597507911,-242185992,-37871622,-7815682,-1423310294],[-0.2691945731639862,0.2276601344347]],[456,[938981281,1039662303,1071988533,-1077430345,2138399927,2128867248,1844920255,553084791],[-0.3116950988769531,0.1914835125207901]],[595,[-6001,-738460161,-491325280,-253577536,-101257344,-268405248,-218703928,-262931494],[-0.19845137000083923,0.28662288188934326]],[219,[-572575425,-134758593,-17432673,-33572033,-41975809,-606209,1819508539,991508795],[-0.2848263084888458,0.19544802606105804]],[309,[-923858941,16533375,687803098,16641781,1425998848,7298130,-117618036,-1459880273],[-0.3118402063846588,0.17289452254772186]],[38,[43975170,570610210,1259547439,91211681,1344147713,71570948,157771867,1194450935],[-0.40333592891693115,0.13441282510757446]],[547,[-620234959,-144179343,-146804237,-275775558,-1956118605,-1364333069,1464849904,-273678849],[-0.22020047903060913,0.2527577579021454]],[207,[-889729313,-553657669,-1560807681,1383525103,-889272690,-278931701,-3289105,-8913157],[-0.20864352583885193,0.2517823278903961]],[386,[1527208720,453049088,1849883952,-577776648,119313820,-635902472,235845803,-1081104213],[-0.2004029005765915,0.24476240575313568]],[105,[-1792337,-135877649,-877376633,-6542642,-170938369,-1158881281,-335549505,-45848810],[-0.1526852548122406,0.32784590125083923]],[45,[-207358038,-420480598,-201337216,-706480664,-2247288,38040834,-201852992,-707267118],[-0.2064659297466278,0.24018539488315582]],[489,[2125956864,876459318,801054519,-1343357377,2029731224,1039974069,517353000,549746646],[-0.34877002239227295,0.1354045271873474]],[305,[1573977072,-545230924,-503523394,-50543968,-10687586,-806753917,-132104,-131700],[-0.39878302812576294,0.11964873224496841]],[119,[-152047962,-219222301,-386358750,-134552838,1919010662,3367751,-234912529,-203950258],[-0.18678680062294006,0.2509252727031708]],[57,[-1548225838,-544303113,169017858,-1079528078,-65317898,1003848540,366421887,-8390913],[-0.3702678382396698,0.12458038330078125]],[236,[1558449156,1469328420,1056312229,972818209,2062028732,867725236,-299020370,944057152],[-0.32043197751045227,0.1403111219406128]],[372,[-935886289,247647375,584647234,1086240770,1243551262,140912131,1181705168,-433064409],[-0.25840097665786743,0.17777059972286224]],[89,[553422030,-646738485,-1378666531,-1326610755,-1193300225,-51663377,-1544714034,13992968],[-0.32469943165779114,0.13890093564987183]],[586,[-104865536,823385006,-1803553008,-137497690,73373105,557062035,-134236512,-493357073],[-0.21948955953121185,0.2058616727590561]],[575,[-621064755,-225691,-187123238,-211615832,-163588416,-834187516,-222115108,-532480182],[-0.16864611208438873,0.2542310059070587]],[359,[1080299799,403982237,-1396998339,1063594813,-1828978672,-1140929027,-64447240,-1414919521],[-0.2084922343492508,0.20172889530658722]],[429,[-278726750,1605286067,-1984582,-1183826,2008218293,-303018266,-454955821,1710712691],[-0.2077249437570572,0.20896007120609283]],[275,[1480501015,415012755,-1133019206,-1620136218,1060451253,988740863,-1174410540,-1157825603],[-0.18233922123908997,0.23548512160778046]],[5,[-14680093,-404291672,-76582128,-748686168,-138428475,-925915262,-143379488,-775432703],[-0.1731029748916626,0.2502557933330536]],[212,[-1537244639,1407124485,955327325,2007572271,1904150032,925836884,-671762604,-80740449],[-0.17120473086833954,0.24483337998390198]],[90,[242724607,-1477218697,133671801,267644576,-1896108289,-1476658180,-352868111,267886847],[-0.43268245458602905,0.09933996200561523]]]],[-1.4806630611419678,[[171,[-352321537,-16783377,-85984514,-218107170,-35652609,-5769225,-262165,-142606337],[-0.3028380870819092,0.2633202373981476]],[546,[-524299,-67633156,-537657347,-8228,-1073762819,-4219400,1300190552,-571517612],[-0.3547776937484741,0.2022661715745926]],[121,[-486544641,-136840193,-486552833,-4358,-134252545,-40001,-134258721,-41105],[-0.18016183376312256,0.3356707692146301]],[108,[-35928657,-47065673,-168170073,-172632151,-254750721,-638271497,-103690801,-610826356],[-0.34803321957588196,0.1765604317188263]],[446,[152059857,453032944,268172721,-1081999912,1058848761,-1074468867,-1155547143,-1077967479],[-0.24453158676624298,0.22779227793216705]],[512,[1060093883,-2118241,521748443,-543178753,1034941423,1054804398,1039581151,1068449727],[-0.2960316240787506,0.1986057609319687]],[74,[-1055825,-154210581,-489790880,-68688222,-143657140,1883399558,1909716800,-143132733],[-0.20965711772441864,0.2508717477321625]],[570,[2000680707,-1895041657,2143282995,-5243477,-1086853213,-1349915235,1373599043,-540016705],[-0.22155438363552094,0.22709301114082336]],[167,[-67108993,-147652614,-802882,-80484704,-8650787,-788545,-2832,-2685102],[-0.1801174432039261,0.27400344610214233]],[633,[-5393,-153096193,-85197202,-521211906,-136321466,1145489220,-67651890,-523766818],[-0.18812599778175354,0.2648163139820099]],[13,[-1105,-202377882,-537677054,-67700788,-73927229,1146024982,-147589632,-706219049],[-0.17558620870113373,0.2530442774295807]],[324,[1309147731,1342867306,1855638557,1060310665,1279543388,1581780126,2128137819,463161099],[-0.215001180768013,0.20435485243797302]],[136,[470028175,-134246505,-609905701,-40167997,-236079105,-50477073,-1544564785,-1350038900],[-0.2391396313905716,0.1823912262916565]],[627,[-335548501,49278626,-968163296,1621881580,-469768313,71821925,-135800892,-709367826],[-0.21279625594615936,0.20251362025737762]],[85,[2071061109,1597442983,2142580527,60883457,601335733,2012720631,-268461122,1151825767],[-0.24757970869541168,0.1766466647386551]],[526,[-35661939,-1077936193,-301995075,-1183745,-154163571,-122046731,-1449525588,-1933180945],[-0.12376438826322556,0.34610992670059204]],[413,[-68157449,2012427621,-135270402,-318116709,93786352,878040771,1928528416,-252444969],[-0.3528245687484741,0.1253521740436554]],[97,[-299178321,-253579410,-505086341,-106781557,-528036177,-12284558,-269619473,-918469118],[-0.18893779814243317,0.2243262231349945]],[156,[1324150526,1610280955,-1358958593,-438721,1861166843,2063056823,-33851009,1374120788],[-0.23356471955776215,0.17476162314414978]],[254,[-855766433,1584267795,1915910911,156285,-153399800,-211608747,-293675318,-553456689],[-0.2666938602924347,0.15304766595363617]],[578,[-738212306,-188874866,-1569000750,-789874034,-142702264,2031301632,-774720374,-800870518],[-0.19974224269390106,0.1993950754404068]],[549,[-466664669,648734646,1438586999,-35130723,-1414815573,717124154,94090911,-806368801],[-0.13644744455814362,0.2769096791744232]],[342,[1249838607,693062607,184519193,144110238,2055029833,204701535,2054005330,-288359529],[-0.24984607100486755,0.15084804594516754]],[72,[-9443413,-23204177,-507513166,-137462546,-159909905,-1006442233,-192945170,-235407873],[-0.11998380720615387,0.3182705342769623]],[421,[1494269173,-720349708,1063892989,-545465860,-122642948,-1074219268,-1347932744,-1417019368],[-0.24514228105545044,0.15939368307590485]],[336,[1602171163,-69345349,2092678079,-1342178561,847146299,-352846093,1809574457,2071953275],[-0.25552767515182495,0.14843150973320007]],[348,[-151650305,-572701128,-843776304,-712703488,-275972610,-611461836,-23282034,67390465],[-0.19797073304653168,0.19756489992141724]],[80,[-6353,1920937967,-1066998110,-209788174,1460628998,1095128909,1900515906,-178784682],[-0.20146924257278442,0.19506125152111053]],[535,[671069953,868219827,-1355307213,-1246761157,1008137220,1700181427,1596434984,143191592],[-0.29868990182876587,0.12109743058681488]],[102,[247622355,165839509,803910905,1273476677,134210527,-541883513,-1375763256,265808373],[-0.38485702872276306,0.0911681279540062]],[96,[-37750801,-812843030,-220433808,-144715680,-121636937,-309815552,1626855360,-716966446],[-0.16374103724956512,0.22773180902004242]],[543,[153361681,286744433,-1351190668,-1368652198,389244373,1069414655,41379716,-322010229],[-0.2370050847530365,0.15526917576789856]],[328,[-866462505,-765927816,1978289788,1058089531,1599921360,-539435782,-291741862,-1626666481],[-0.15901291370391846,0.23870913684368134]],[283,[-656868899,-1688724035,-1510113953,863235675,-1872148424,-5628044,-5072136,-1079878326],[-0.20384007692337036,0.1838795691728592]],[134,[-1367079007,-1114562671,1840407673,-80561904,-1480914455,-809727614,-374463023,67108864],[-0.23268628120422363,0.15638644993305206]],[481,[-101203328,-579340569,-676354283,-839058037,-572153460,-388903440,-859517276,214036430],[-0.19198764860630035,0.19319075345993042]],[605,[-209752881,-137424530,-487666946,1884224456,-10503608,-228324026,-84216120,-228393014],[-0.16494055092334747,0.21874834597110748]],[567,[-742399053,1128773464,2012640561,-684720133,96431507,1146418932,903034192,-2143824025],[-0.24089863896369934,0.14685575664043427]]]],[-1.531322717666626,[[118,[-755055873,-603985153,-83902465,-21765,-176712705,-21761,-173313,-45313],[-0.1970607340335846,0.3158511221408844]],[379,[-1,-5,-453312513,-67436549,-520375056,-1048577,-1409303830,-67371013],[-0.20109902322292328,0.2925731837749481]],[472,[-1157431311,721466416,-5538351,-551232336,-1442447427,-6095109,267895293,-810822183],[-0.246646910905838,0.21074186265468597]],[208,[-754987265,-635491769,-422379777,268627711,-293623114,2135249669,-12546,-11011073],[-0.22645103931427002,0.22744908928871155]],[498,[1602221841,941538719,-1107427819,-1614811905,2038431645,1073474047,-34735681,1023278847],[-0.2475070357322693,0.21533721685409546]],[596,[-4213,-655362609,-925375808,-253107478,-2099320,1145502980,-118489462,-255332370],[-0.1756095141172409,0.27202552556991577]],[246,[-262146,-575078470,-1242334370,-302312,1004126207,-16418,-449086918,-536885544],[-0.22480174899101257,0.19904428720474243]],[98,[1404276623,-239733835,2100921775,-705445473,-251883793,-84835913,-118452273,-609710193],[-0.3100532591342926,0.14657047390937805]],[21,[-1050709,-219156762,-488639934,-253308274,-9444029,1078966087,-167774398,-235408433],[-0.19676518440246582,0.224064901471138]],[210,[-17597718,390550447,1543499727,-1051889,938702543,-955581534,800062155,-4194737],[-0.2056204229593277,0.21108107268810272]],[148,[-231825238,-118708248,-126915649,-35385563,-51306769,-122622556,-64495777,1883338499],[-0.23083308339118958,0.19100864231586456]],[544,[1461976433,-687707020,-671102467,-69411352,-283119619,-34615555,-545307240,-825239080],[-0.2323579341173172,0.1847982257604599]],[214,[1076339200,27197442,-1430500781,-15102177,345466176,1407143952,1909970806,-8423553],[-0.2082488238811493,0.19525180757045746]],[562,[-1910004736,227976629,-1397791456,-1647335945,-1090738556,1023222964,-1359086680,-1431505921],[-0.2151651829481125,0.18513250350952148]],[107,[-72356977,-674304370,-88611088,-68163352,-210769969,1615099074,-186211200,-170421565],[-0.15987128019332886,0.25031208992004395]],[19,[35788594,-570473098,173184975,-63105,686291999,-5259537,205462959,-811630449],[-0.3976033329963684,0.10220080614089966]],[482,[-335567743,-134878025,-1109925897,-273158221,-68311108,-67174475,-1088045138,549775094],[-0.18809646368026733,0.20953576266765594]],[571,[-1372850253,-13974142,-1678790192,-76566280,-1973441117,247966637,1465861521,-570433813],[-0.19961197674274445,0.19941739737987518]],[635,[-687870257,-253248569,-150996374,-1058605588,-42473594,1111936836,-562907444,-730337333],[-0.17394885420799255,0.2222379446029663]],[205,[1117962991,1383945687,-500503332,809498271,243157775,2115858421,-75727153,-2363489],[-0.21008644998073578,0.18192526698112488]],[397,[1546701304,1502674361,1862028696,-68619312,1224269564,-8404279,737211100,-20651148],[-0.45097893476486206,0.08725540339946747]],[60,[-393501017,-151003489,-8925315,-1073880539,-2068927745,-1157701426,-858402901,67453124],[-0.22688327729701996,0.16925102472305298]],[232,[1510739604,863309605,-1360064513,-1713172665,1993154559,953815035,-79736866,725943043],[-0.30244749784469604,0.12280921638011932]],[293,[-431620010,1347961407,1645639766,673317522,1083067468,1591659708,-467746,-14463397],[-0.2931867241859436,0.13229398429393768]],[507,[-1912234189,788717234,2000052609,-544997440,-1960506219,-1405096517,1362056513,-913318435],[-0.2361961156129837,0.16040034592151642]],[400,[-995127773,732950504,712712466,586101759,2120161651,242180931,2013206538,-3257],[-0.21951305866241455,0.17167043685913086]],[0,[-13634581,-55838738,-1242056846,-540019520,-112200001,-1060453490,-202902559,-705171148],[-0.1406068205833435,0.2656737267971039]],[276,[1598289887,520138729,-4239399,-1224492136,796904957,-1703941,-103457531,-1141394078],[-0.13021205365657806,0.27274754643440247]],[151,[-271652182,-24632894,-899102988,-10017558,-1209538625,-201046843,-136907778,-137935742],[-0.14946521818637848,0.23473292589187622]],[116,[-756028693,-218968594,-2101451038,-7430,1120845454,2014372962,1666642894,-168057645],[-0.13821019232273102,0.2557409107685089]],[33,[1534696261,1062842133,-1342824577,16852899,2098415232,2098566439,-403313733,1346862895],[-0.22740162909030914,0.15381287038326263]],[238,[295636992,821772143,1801930031,995190037,2063248814,-1080066052,-1165455861,779029435],[-0.4081084132194519,0.087570421397686]],[299,[-10350288,1953972019,2142658045,-168792567,-849660939,-20977786,1374479412,1879061282],[-0.3079519271850586,0.1165873184800148]],[565,[674213888,234891194,1069719170,1066331550,322168757,297054135,100431778,-940649218],[-0.25961834192276,0.1387970894575119]],[461,[-857747457,-542310940,-336830604,1856806596,-35369559,-33792609,-2064905188,-858837124],[-0.14236339926719666,0.2515232264995575]],[137,[-1208749149,-808992822,1941781200,-248516152,-1493452850,-932784438,1406792916,-171445034],[-0.15282876789569855,0.22942787408828735]],[169,[-358692337,1913603755,1780904038,847543030,2130366282,1465284100,-8519941,-146296866],[-0.20485755801200867,0.16542507708072662]],[598,[-624964407,1929293739,1660384790,-235366402,-781729664,1950379016,-86250104,-251396098],[-0.14611224830150604,0.23367854952812195]],[417,[470098293,68724625,2137775989,-1892868772,2124421621,-19018243,977084317,700385347],[-0.20630234479904175,0.1670524924993515]],[326,[-366232099,-190253736,-1380867905,-69371941,540412351,2038193550,178462734,1624598175],[-0.22028318047523499,0.15421783924102783]],[615,[-341839709,-1026897234,-2084621072,-2054694368,-1493174289,191097334,-53018662,-2117081441],[-0.15134109556674957,0.2281082272529602]],[306,[-553664381,1415565023,-2072524587,1367308795,-252592630,1556899377,-22070406,-525337637],[-0.13117921352386475,0.2531299889087677]],[377,[108201239,9837407,592193301,136324358,1550649125,247541396,1311001108,1141928512],[-0.3607528507709503,0.09472320973873138]],[261,[-933699454,1776876279,-820938308,-539560229,-42108166,-554199719,-544738,-84412445],[-0.37590476870536804,0.08685655891895294]]]],[-1.496147632598877,[[327,[-620831233,-537069577,-101417505,-13168873,-556086056,-536873473,-543629570,-81080321],[-0.20265258848667145,0.2719714343547821]],[458,[2147483575,2147483647,-1073873409,-2228257,2146795519,2130572534,2130018047,2012705527],[-0.2265007197856903,0.23255248367786407]],[447,[-274730064,-1828490718,369062224,-542114548,-1364483424,-1347425350,1423397120,-978321525],[-0.36266660690307617,0.13946540653705597]],[185,[-572799778,-658813697,1438600447,-537458434,-805311745,-923335015,-800005,1610025215],[-0.20851968228816986,0.22624310851097107]],[249,[-856105761,-33882273,-1171097633,-1224545442,-861365000,-2458133,-71565606,-72679542],[-0.23597721755504608,0.18162688612937927]],[48,[-21,-3741714,-34078816,-527638,-113,13434820,-135536062,-168298546],[-0.15795913338661194,0.27534615993499756]],[410,[-202379265,-33554465,-269488146,-17891337,-1023410948,-66561,-1091883318,-1052929],[-0.15668000280857086,0.26539573073387146]],[264,[2038380347,-67289281,1824356671,-101238593,-631259777,-67492802,1886915711,528432447],[-0.35712456703186035,0.1191648617386818]],[593,[-100673841,-69224881,-118170894,-260898840,-1050808,-163419068,-195891512,-261881906],[-0.17496080696582794,0.24545976519584656]],[191,[-789133138,1357808511,344927999,276753983,-117987153,-119992597,-369100129,552039275],[-0.2423415631055832,0.15460087358951569]],[568,[993523459,1068448807,-134417102,-40896850,121108099,-1929384980,2004276738,-1207959621],[-0.23254016041755676,0.1672218292951584]],[445,[1043149619,134807472,-147062799,-1389785608,778508721,-1078985799,1052163003,-1347437061],[-0.15260687470436096,0.2397785186767578]],[224,[420417423,363269511,-1181413745,-41029713,487264223,-67125253,-1141534769,-7385137],[-0.2702747583389282,0.1346423476934433]],[510,[137232896,33988648,1929270085,1977700989,548021027,539665932,585611707,-1880109889],[-0.20664082467556,0.18262037634849548]],[83,[-537395337,1915903126,-1083662512,536906274,-1726232561,1041620991,-271596849,-780664993],[-0.3592633008956909,0.10130912810564041]],[610,[-16781429,-152051773,-469767584,1626332074,-136839290,423686,-253248888,-255332390],[-0.17431952059268951,0.2102852165699005]],[133,[-336599041,-8133922,-1141113930,-37248794,-912916546,-308113878,-1007163154,-993664890],[-0.12619158625602722,0.26890262961387634]],[157,[-491853138,1357672160,573189794,-69711698,1661179311,541861739,2002744015,-170436793],[-0.18496830761432648,0.1792939454317093]],[625,[-5457,-479461718,-343330784,-236206134,-734006329,1113998678,-486541632,-707789857],[-0.14289534091949463,0.23088638484477997]],[244,[34604197,314455839,709823416,1055849114,242550243,1006448564,777592829,-1075859328],[-0.23958338797092438,0.13976256549358368]],[331,[-1672813806,2117239444,1981954909,790760502,-1761080108,-2823052,-7282818,-352180693],[-0.204872727394104,0.1605996936559677]],[584,[-14682192,1410054962,930545776,-704669782,1462208945,485762336,2094510506,756531114],[-0.19781313836574554,0.16727229952812195]],[313,[-8917097,-1090285819,-1361920071,-1365204576,-587337803,-21561419,-1628979300,-1092178412],[-0.13299308717250824,0.24190054833889008]],[553,[-67124722,-244319281,-159678928,-235658710,-235168120,556126992,-640763208,-247201910],[-0.1459912806749344,0.21836180984973907]],[432,[-1574459869,1801694139,688355093,1137244896,1597321139,667307894,2103932223,-875206861],[-0.1999962031841278,0.1616845428943634]],[467,[1415519575,2086366073,-277518529,223099000,2085048285,-1107460609,2103254796,-2004256296],[-0.173945352435112,0.17971649765968323]],[631,[-136321265,1345830367,-352396334,1615494266,-807156,-1773853180,-156565858,-797708465],[-0.15880204737186432,0.19731280207633972]],[153,[1244179144,1261546146,-363663553,-268711247,1351877499,-280641,-84327201,184487424],[-0.294883668422699,0.10669518262147903]],[287,[139709719,43029023,716864123,145948174,72842328,-542019334,520215374,-68543909],[-0.20783309638500214,0.15214750170707703]],[388,[-12132331,-1257178152,536162265,-1073758883,1531509500,-1143015176,677185805,-1916803061],[-0.13157057762145996,0.2420303374528885]],[508,[-1297104925,908165800,-1346954911,-1658201648,-1566723143,699314924,459246985,-1918904868],[-0.18554656207561493,0.171234130859375]],[178,[-2030042720,-638576553,-856623777,-1155485679,-272560641,-73725530,-1699976999,-2013232128],[-0.19297820329666138,0.16354376077651978]],[523,[-802903936,219991281,-1380735344,-1677912129,-1196039740,-1924874396,-285426550,-1981250341],[-0.16431039571762085,0.19124364852905273]],[187,[786271998,353427327,769929200,901316269,-2013419265,1070845648,-1929524065,745335551],[-0.34417709708213806,0.09130412340164185]],[258,[678666755,1795098123,715934564,-11026437,2029921792,58675732,-672615084,-138415361],[-0.16051757335662842,0.19698475301265717]],[537,[757053313,1066712229,-1220563659,-1077439073,884821983,-1073791051,-137412097,-1343329298],[-0.1701008826494217,0.17927823960781097]],[82,[1389352639,1912983387,552783085,-1334070951,1883213615,875581417,1061134046,1628826575],[-0.2355739176273346,0.135020449757576]],[43,[-148378625,-454104340,-444894224,-3679494,-170132001,-2004452736,1979433184,-705169834],[-0.13890986144542694,0.22841182351112366]],[292,[-562163691,-612498656,-398797281,453517379,1576666200,-69248251,-836204322,-14537969],[-0.19448943436145782,0.15733850002288818]],[394,[-802954462,-214240474,-171104766,-526590,167222773,669761943,-138184978,1684498211],[-0.2066839337348938,0.15022484958171844]],[444,[-323100186,-21839924,-1663054876,-1785792953,279978382,685830218,1700463695,1075309899],[-0.23692423105239868,0.12721052765846252]],[183,[-470300502,-779440474,317666416,-237112100,-1623348022,-837984724,1892930262,-720943418],[-0.15833552181720734,0.19168458878993988]],[419,[286629325,419495792,-281560203,2100829820,-1760265995,-1107509763,-1347405073,-1416952693],[-0.14504113793373108,0.21510179340839386]],[231,[-127662976,-1087598676,773459488,-35385847,1652563440,-170590944,-386639138,675557121],[-0.2552856206893921,0.12368638068437576]],[431,[-571158532,-1123001732,-541836356,1911335552,-730029140,-520339162,-1192711304,-1090725180],[-0.416248083114624,0.07490038871765137]],[84,[-232924694,974885303,-1350574289,1336799215,291214187,-2140233508,1643271951,1607457567],[-0.20407570898532867,0.15173958241939545]],[307,[-554874737,413568674,-1442634037,281801574,2020022853,671503039,-193074614,-390597677],[-0.18695932626724243,0.1645810306072235]],[75,[-135269693,-742200265,-1410699614,-13836096,1237270351,1879118941,-203115702,-243795973],[-0.1366724967956543,0.2194761484861374]],[41,[149662391,267466357,1751289335,248190673,662646779,1052753397,-1476979457,233637266],[-0.4083424508571625,0.07441364228725433]]]],[-1.3947604894638062,[[201,[1258228431,1541393407,-362082691,-2102673,1138427903,-33607787,-36885,-8451185],[-0.22683575749397278,0.21698297560214996]],[542,[-142609485,-147341345,2147430871,-1244137569,-671091209,-536948741,-175245326,-167774241],[-0.21389076113700867,0.22244058549404144]],[311,[-67108865,-1683297558,-1522913379,-1365204446,-537268289,-340083462,-277894725,-286274934],[-0.1599258929491043,0.2807232439517975]],[385,[1493442005,487622616,796877695,-541253667,1548222680,-537371139,-4661570,-1141957985],[-0.16421283781528473,0.25366565585136414]],[453,[2012324787,-786541,2127984599,-1712157,1467463095,-21102638,635748263,552953719],[-0.23738333582878113,0.17089855670928955]],[251,[-655697441,-109412993,1067113279,251802999,-1364535078,-14766082,-1077216354,-366813250],[-0.2866738736629486,0.1445491462945938]],[44,[-67108865,-741279774,-404489384,-586351998,-68816978,-222027262,-143300086,-783949310],[-0.13955391943454742,0.2744167447090149]],[470,[335904627,688454032,-576326285,-821588848,-2057208907,-1716607047,-1955598855,-1884303895],[-0.18432578444480896,0.20841632783412933]],[577,[-603989137,-18,-1291846942,-253431302,-8388664,-1132204282,-1226317176,-800850994],[-0.15180069208145142,0.2387949675321579]],[92,[-1107974265,-1107488321,-438980181,-34271041,-705322561,-1779060809,-1852239449,1035052479],[-0.26681289076805115,0.13597114384174347]],[130,[547815938,539725376,590284047,254798327,111100181,1881297024,1050504923,1086317355],[-0.29581764340400696,0.11302649974822998]],[402,[-202970846,1997573378,-807003368,-33822848,1383556917,2146907953,-8949889,-8951981],[-0.29145026206970215,0.11627478897571564]],[548,[-1351616079,-275847760,-807451663,-7342864,-1959811935,-2102481202,91706817,-577790499],[-0.22961539030075073,0.14421683549880981]],[315,[-173279301,-570831744,-591597857,-1920606976,-875315793,-743121507,-84161761,80942663],[-0.16261959075927734,0.20734244585037231]],[154,[-295507282,-222108994,-337368354,-9054488,1695393195,1276183616,1465712383,-203995565],[-0.1629696786403656,0.20082713663578033]],[529,[743681152,1060998324,-1108396745,-1109544971,2146248364,-1131831369,-1109106804,751052416],[-0.2580071985721588,0.12539248168468475]],[624,[-134222193,-823203865,-355090400,-219170820,-9699354,1120917334,-236982078,-168297777],[-0.15296129882335663,0.21096526086330414]],[8,[-142608477,-292753778,-105415054,-168900640,924034995,1086518660,2012737924,-705178122],[-0.16148518025875092,0.19514200091362]],[164,[-635449601,1383502423,-491069862,818069179,-763408706,-132708062,-550005,-143263077],[-0.159551739692688,0.19565145671367645]],[304,[-91324273,-638948913,1459102352,-234883138,-411134456,608256066,-201791862,-213123857],[-0.124847911298275,0.24199718236923218]],[141,[1924049967,-68243729,-1443053961,-102600915,-17306377,-56061955,-17170689,-247107769],[-0.13098762929439545,0.22698824107646942]],[273,[-34538584,-36058140,-1051656288,-35789003,-268782921,-131587919,-10961985,-938149707],[-0.3826730251312256,0.08256648480892181]],[128,[-676125470,-422419785,733606671,1331165455,-1581583803,1441710245,-1152161,-1048625],[-0.20781999826431274,0.1617911159992218]],[541,[335879441,857358129,-1080172780,1036762602,812061588,-1074364481,1313293872,-1344439906],[-0.23761694133281708,0.13307097554206848]],[330,[-155579051,1977548902,-109936641,527894909,-754445200,-138473890,-72820226,-83217585],[-0.1539456993341446,0.1961182951927185]],[109,[-105978129,-89202036,1206379146,-137441666,1935124399,1946251504,-1326188912,-146279937],[-0.13842856884002686,0.21920017898082733]],[490,[1833936128,997703423,-1255263405,-1113732166,1549563407,-100835876,-1365059701,960064042],[-0.28704747557640076,0.10284743458032608]],[298,[1085179907,1558038199,-647534401,-780680693,1246731967,-495023986,-2065708378,458754],[-0.2138298749923706,0.138645738363266]],[630,[-235929793,-35017425,-974852131,-1473993990,-154806396,-39664848,-218780980,-1069420542],[-0.13126356899738312,0.2278546243906021]],[380,[1164428247,330592127,221719867,220515469,1525638961,771553661,6317897,71567872],[-0.22609888017177582,0.1348307579755783]],[450,[-168189951,-537955555,-261358634,-237300738,-220334180,-301885632,-674048822,-784091958],[-0.12464315444231033,0.23845015466213226]],[248,[202362591,529960950,687636059,715847819,49893116,-1075370050,-1090575784,-275320512],[-0.18483379483222961,0.1581660807132721]],[369,[-59776341,-546708861,-1996605390,-751324454,-5509237,1212568876,1987049206,-84414181],[-0.10926777869462967,0.2634029686450958]],[322,[-859874849,-589690915,-1078272001,-269396281,1444829252,-361363,-1088889270,-14524897],[-0.1407398134469986,0.2058841586112976]],[558,[-656422264,1404815771,215051032,1573253083,-1258510968,1056763359,-1342722118,-1431572741],[-0.1293744295835495,0.22350135445594788]],[580,[-538976242,-706933761,-1157899644,362671324,-616768120,76414080,-1145002358,-257163377],[-0.13464896380901337,0.21594136953353882]],[500,[125169463,419200829,-1376628745,952940586,936641716,-1309459009,1895069444,-389533710],[-0.19427908957004547,0.151099294424057]],[197,[-936448836,296527797,1725790972,1040187375,2094788862,989460023,-119468066,821391343],[-0.22964347898960114,0.12504659593105316]],[27,[-8452601,1934077794,1941583474,-1298074901,2048863812,2084627268,1498884488,-481959089],[-0.16685572266578674,0.1735781729221344]],[363,[-854920272,1466228596,1841174397,-1350754044,-1912043332,-544412176,-1074884950,-1351636949],[-0.1349325031042099,0.2078791856765747]],[399,[-759714941,1319853762,465518886,25093107,1311451838,262313924,1264269234,-67639305],[-0.1470017284154892,0.19267882406711578]],[285,[-62210118,-95633686,-543498916,-5647854,-582576008,-858743621,-1076589240,533272664],[-0.2338734269142151,0.122316375374794]],[63,[663490442,868591391,944180691,-1725045803,-1515212829,-35061268,-960230409,-1358689591],[-0.2431904673576355,0.11639515310525894]],[476,[-488118528,-1001853142,-1284508299,-24149798,1848082858,706898854,-822111589,-1027604833],[-0.15382418036460876,0.18594354391098022]],[68,[-344199257,-355533682,-1103629790,-672858904,1070593471,-997397002,-152595754,-246940218],[-0.1509171724319458,0.18637944757938385]],[31,[1793322823,6471586,2136661902,544275114,813647735,1076229465,388745606,1349749552],[-0.5150347352027893,0.05549565702676773]],[479,[-1051226111,-906782909,-1752851951,-573214941,-514355772,-34304515,-1917518166,-2080365046],[-0.18393677473068237,0.148982435464859]],[561,[1139012403,1401008551,133777171,366319523,381097650,1549129111,1842035418,547765943],[-0.21134988963603973,0.12946300208568573]],[53,[-525423697,-1284755713,-208931,-1357271134,1492289279,-108851201,-327389860,-694524],[-0.12473458796739578,0.2229464203119278]],[408,[-1053781711,1488768055,-1883353875,1772158314,-328087099,-1117247528,-937669396,144711680],[-0.24978365004062653,0.11137084662914276]],[158,[1652543131,308775679,-956675393,-1083456334,1883606772,377030805,980155213,1904691731],[-0.17729288339614868,0.15420784056186676]],[211,[-228447566,1986675278,-1710034373,539124387,1681978890,-2146306,2146938783,-277360677],[-0.24232980608940125,0.11379676312208176]],[344,[1316210703,384430053,-1605375990,-1886392130,1727284535,788489447,-84560439,-34605257],[-0.19972075521945953,0.137681245803833]]]],[-1.4742519855499268,[[121,[-1023416577,-4353,-218103809,-4199429,-469764097,-38913,-218146817,-33041],[-0.15614232420921326,0.2845953106880188]],[569,[-14419983,-40645392,2105497077,-17200,-1074790407,-1963226659,1509905749,-573581860],[-0.26622509956359863,0.17384453117847443]],[277,[-2228225,-537002337,-993984612,-570614067,-2360321,-539165186,-1086001221,-582353009],[-0.15457338094711304,0.2492058277130127]],[501,[1497849681,-46703179,-541254857,-152143362,1245972736,-19211521,-355578294,-930420486],[-0.22253337502479553,0.16836225986480713]],[359,[1348230493,-572761801,901578239,-575135907,-665562984,-587575815,-577217316,-1079359265],[-0.2195247858762741,0.16715624928474426]],[530,[-22751,924006327,-1242563275,-1343226995,1740615048,-286293035,-1375884146,143518351],[-0.2085951864719391,0.16993917524814606]],[18,[-4177,-252706834,-89435614,-152109174,2012210959,1212315399,-137891006,-196608261],[-0.1806883066892624,0.1935441941022873]],[619,[-16782645,-67108979,-220049174,-1292960022,-671090880,2085024332,-261162808,-261619974],[-0.1650635004043579,0.2022944837808609]],[420,[-382737744,50788352,-1994165808,-15731432,-1707289424,-1424360140,1166807040,-1995450709],[-0.2954464256763458,0.11053616553544998]],[395,[-237505662,-7875693,-44228609,-541082249,-140249609,-2689391,-168296713,1441130294],[-0.18134017288684845,0.181584432721138]],[252,[1223614047,-566850177,452714110,1377860351,1314115214,-25223481,-887124790,-14950405],[-0.1985388845205307,0.16829168796539307]],[104,[-139465237,-221860946,-386469728,-5064,-67109970,-1027308284,-252448634,-170396417],[-0.1359160840511322,0.24295561015605927]],[181,[1547110426,-594080544,1242779512,2136932364,-31486309,-633908553,-93345025,1407648427],[-0.21696040034294128,0.1479005366563797]],[370,[1499790452,2109508592,-575091844,-302188396,-591074644,-68222826,-88022980,-109264732],[-0.4229183793067932,0.07374207675457001]],[383,[-606273731,-196612,-37945347,-1078413452,-68622672,-1073833673,-886375426,-357847909],[-0.13053973019123077,0.23168803751468658]],[42,[-504256601,-103693889,-1552631861,1907462573,-248469585,-243578961,-1001289745,-181465207],[-0.2983328104019165,0.10050108283758163]],[54,[-134222325,-59852914,2059134464,-236458270,-11550966,273579030,2011488840,-179834277],[-0.16351912915706635,0.17984919250011444]],[443,[1599067921,487038815,-780142176,-36833527,-228755067,-1147797541,-67116406,-1936719953],[-0.14494937658309937,0.2044524997472763]],[423,[-1060117723,105172292,-1816406059,-25779,724509227,585891728,1289685275,-1951405137],[-0.13382528722286224,0.21853315830230713]],[478,[-86114193,-313171,1111290354,-203817238,-2438456,-162604796,-233662248,-268173862],[-0.1297757625579834,0.22842766344547272]],[34,[2046686481,456335616,2042029853,864486144,-933104412,1873167254,-196099146,1728444263],[-0.21469497680664062,0.13323721289634705]],[465,[343343103,-570466369,1857224703,672685290,1434275581,-50389665,1962159104,-1933164800],[-0.17130576074123383,0.16903875768184662]],[589,[-818941053,87013538,-111184397,-37754531,-1898061693,176551654,1383588499,-943720513],[-0.16567197442054749,0.17757371068000793]],[519,[788713130,-1156586598,521182527,-1916946,2134622139,-541888753,-1893755925,1066076831],[-0.17230549454689026,0.16435682773590088]],[143,[-922759554,318630654,616432687,-302831042,-727324161,41682483,1920329832,1348428442],[-0.2049153596162796,0.13880960643291473]],[2,[723823356,-1076777961,-548951307,84377713,737552635,-1210946315,-1625454337,51577009],[-0.3284936249256134,0.08823275566101074]],[552,[-393329,-201327633,-220237262,1908912120,-536940600,-1069443776,-222112070,-245114118],[-0.11370333284139633,0.24661171436309814]],[280,[137866647,330497943,-1369628875,-1075382238,1313256957,1000285681,770980944,-279533994],[-0.15602348744869232,0.18064066767692566]],[39,[-876627029,-341120470,1475736498,-712395712,-1079659634,-2134787454,324531680,-985140140],[-0.16076508164405823,0.16893979907035828]],[230,[-790584800,-798953804,-1461039873,-33961394,-146490212,1421872403,-21414210,-1710152177],[-0.1537749469280243,0.17769819498062134]],[170,[2122294795,1377502722,1392496187,1342876671,-21078444,1174557952,-18705082,-213683390],[-0.19270925223827362,0.13925707340240479]],[636,[-286265721,1087368110,577492704,1623915622,-135286528,1145196902,1993571328,1347679734],[-0.2657798230648041,0.09998544305562973]],[398,[1982039846,397254535,791353898,-1627349,1729455863,1974525379,1867999102,1742958439],[-0.23255635797977448,0.11416502296924591]],[155,[-930355482,1391502078,-1394668033,-36122826,385550518,889011848,1061650251,812083063],[-0.21627771854400635,0.12228459119796753]],[474,[-1575620175,1360049280,-2023751683,-2633232,-2102872143,-1474564867,724025243,-879765027],[-0.13511040806770325,0.19969221949577332]],[576,[-19674853,-209862853,-1174651909,-247081470,-947934244,-67273344,-361432166,-268305918],[-0.12421544641256332,0.21285703778266907]],[56,[1606418223,1888960082,-549204542,9024010,-1749831699,-176720023,-617002195,-117478001],[-0.2160504013299942,0.12219076603651047]],[533,[532659985,481076660,1069384497,-1109532747,897535455,-1146290249,2093248670,986457767],[-0.2275753915309906,0.11520259827375412]],[407,[4710295,70527783,636325663,772164110,73221521,255335261,1734675540,14912],[-0.2595272362232208,0.10210981220006943]],[129,[1981030261,-784558837,26018085,1070988785,1565896703,528994556,791806603,1902506983],[-0.15912999212741852,0.1656181961297989]],[260,[67381250,-1627145297,-322568424,1301083899,-547766386,-1073754371,-270867206,-1351352674],[-0.4017013907432556,0.06479436159133911]],[225,[1425967788,2082375658,-1345048904,502528730,-168390835,-32914981,-35926390,818686652],[-0.19302360713481903,0.1390720158815384]],[547,[1868527361,-1221977296,-1891735758,2074998596,-821039197,-308147286,1432343616,-812652087],[-0.22188051044940948,0.12489976733922958]],[131,[-1557400589,-1248274,-613056656,1439425388,-1213219890,-2112947072,-1027083856,-706494382],[-0.13170881569385529,0.2017805129289627]],[614,[-1006640382,1080797230,-1475932128,1123009640,865575328,113265744,-1334843708,-462948914],[-0.20952266454696655,0.12552599608898163]],[360,[-1013973025,486547392,1073128087,-1626784775,105420508,-1152767500,-874785925,-611646437],[-0.11399252712726593,0.23128414154052734]],[338,[-830633942,2127447645,-1191940048,1916927034,-251667124,1112837698,-176687438,-250616629],[-0.14943134784698486,0.17533594369888306]],[346,[1417087799,280764279,113190420,231275440,1397886477,1081212924,1978420548,-792412416],[-0.17520387470722198,0.1438317745923996]],[316,[67284919,168863207,-550613281,765994050,-1262428729,-1076672520,-649814438,-1369156349],[-0.18277360498905182,0.13956578075885773]],[320,[-1004017133,1535149056,653028909,1572339459,408031108,2023804820,147856938,1606763075],[-0.16832774877548218,0.14991748332977295]],[403,[1304393556,1433759573,77663858,-1499455289,1970754580,888309687,1747679550,1786390635],[-0.38091838359832764,0.06628952920436859]],[6,[-856294403,-2317726,-1552485576,-369560984,-469158006,-62120224,-6132168,-783949310],[-0.13586831092834473,0.18866735696792603]],[585,[-570439936,818277201,-1159260624,367517684,2128999812,1485249311,-1397821528,-1576556801],[-0.15883666276931763,0.16120925545692444]],[292,[1555765623,-1621160633,-95796705,1999587663,1161353270,-8647867,-546809186,-16056501],[-0.12610651552677155,0.20256702601909637]],[509,[-165168237,-1036863384,-962127881,2140655952,-2085503053,-1361645123,721875631,-907031362],[-0.10026291012763977,0.2529263198375702]],[138,[-1041372593,-34650142,526856810,-142651678,-134299697,-472860218,-545794344,-706219574],[-0.11102330684661865,0.2323770970106125]],[430,[-895385053,-74011487,-2139413994,-269627264,907480823,9225072,325710813,-807964109],[-0.1419346034526825,0.1777896285057068]],[532,[-33182560,431646387,-1444991724,-1883374155,873740209,-1109836522,-84107345,-1345837813],[-0.13789501786231995,0.18308758735656738]],[591,[1512284963,1141564138,1212531296,1113560758,-1557154909,177122818,100626839,-1018171923],[-0.16264808177947998,0.1549759954214096]],[621,[-465586289,-1650720961,1677193050,-1245645074,1249883404,-1321181114,-924458918,1096148570],[-0.1387788951396942,0.1835004836320877]]]],[-1.4156285524368286,[[534,[-638102187,-679525099,-1028701999,-39250355,-50358343,-16824947,-70270465,-264417],[-0.2394787222146988,0.17213819921016693]],[246,[-102,-327766,-1317552232,-2088,-5261442,-67190785,-11965510,-539143080],[-0.23085838556289673,0.16734325885772705]],[117,[-286283025,1945448179,-287698945,-1201662,1962887167,-2019343,-36869,2138326851],[-0.2055557668209076,0.16909728944301605]],[446,[202504181,487127216,2143026545,-13386576,-1088807427,-1153572867,1002289913,-1145108339],[-0.1936747282743454,0.17333285510540009]],[607,[-81,-203438129,-119610708,-788532244,-3145908,1564880128,-69535000,-247203890],[-0.14611057937145233,0.22614458203315735]],[150,[83988110,-16658643,-5992498,2139989165,-1895069745,-1184840063,-303839025,-15741813],[-0.42162150144577026,0.0763353630900383]],[290,[1281287903,-992067593,-1707189025,304231711,-1931918256,-822131563,-664085938,-553461153],[-0.24101121723651886,0.1289917677640915]],[522,[-99,-67111489,-3670529,-2097155,-16465,-4334435,-151633,-1930571891],[-0.08985897898674011,0.3546532094478607]],[67,[-138414161,-204477202,-562727296,-1706382,-7871509,-2066808860,-170413340,-706218018],[-0.12456064671278,0.23621052503585815]],[440,[-22020619,-1688271921,-1268777234,-1126483556,-731392784,-5256193,-508600094,-1343229953],[-0.21830691397190094,0.13843542337417603]],[204,[-595670817,2137779887,842165854,696450687,-898069314,-13148161,-293893442,-11035046],[-0.16304337978363037,0.1813068836927414]],[177,[654378409,-215812202,-1617431212,-104401568,214730925,-624893564,-520853376,-1001098172],[-0.1843288540840149,0.15385165810585022]],[268,[-923466703,882443319,684861821,214183562,1216310488,821823419,-385921285,552943443],[-0.26095736026763916,0.10501748323440552]],[418,[-498279535,268466432,-135469008,2098156943,-733180535,958528317,296415627,-1894785397],[-0.149624302983284,0.1872091442346573]],[587,[-80747741,-440533585,917467936,-1117913366,1400489367,108504455,-470302329,-154675522],[-0.1663636416196823,0.16690850257873535]],[55,[-18880725,-202391769,-759287262,-119277466,1457470730,1950626060,1998312834,-263979350],[-0.16729773581027985,0.1605130285024643]],[599,[-603994229,1911548847,-194527230,-236195126,-134744696,12806020,-51512662,-247205941],[-0.11797425895929337,0.22724191844463348]],[297,[-489829469,1226741,800489375,-1350581579,1377190149,1085406199,-604275713,-145],[-0.12585067749023438,0.20891451835632324]],[3,[-1971608833,-1288591395,1475176695,1563627259,-1375785217,-17649729,-537679105,1337656827],[-0.23411718010902405,0.11347461491823196]],[437,[-591344417,2063130525,-1896214768,-307205284,-571913191,-59966273,-393475068,-2001993712],[-0.20314617455005646,0.1376352310180664]],[282,[101187391,364219279,-1176650755,1000767114,912373727,-1937409,2136472389,-12101881],[-0.1254676729440689,0.21802827715873718]],[99,[-207622237,1976559278,-421595998,-36702260,-4523009,-395844156,-151653426,-168299300],[-0.106666199862957,0.24177177250385284]],[247,[-176423049,42000772,-1076962432,153287170,2069362679,-1434126487,1743140804,-1608450558],[-0.3063652515411377,0.08508893102407455]],[215,[1572799552,1555302911,502016374,-783578511,-639160325,-1910286183,-272236801,1497303731],[-0.16857950389385223,0.15063311159610748]],[618,[-536892241,-34722976,-754993444,1925642998,-440,-565086396,1902047384,-800915489],[-0.14988699555397034,0.16982515156269073]],[10,[-76547105,1459287522,-83019108,-110054718,1164994324,-91993341,-851351024,1900143122],[-0.14859989285469055,0.16797077655792236]],[493,[210804112,1014671005,-135055168,-1078395007,2113214933,-1074900241,-5022132,1021654866],[-0.2165178656578064,0.11568113416433334]],[473,[1864359859,1195796354,-1679085072,1603270029,25372035,13287051,1163609361,1166013371],[-0.2746153175830841,0.09260080009698868]],[23,[-914427905,-1288795169,-303404033,-1078546750,1419528191,-31168917,-35070209,-61577692],[-0.10754923522472382,0.24138067662715912]],[243,[-864105242,445290293,-1393758754,951036643,752302524,-1078070320,-542736642,-1126565110],[-0.1480167955160141,0.16932740807533264]],[142,[-273756954,1942415086,575448290,-237796308,-148918550,-1005259754,1940834010,-238567265],[-0.13087278604507446,0.19493532180786133]],[322,[9446003,202403103,1824152607,147849757,1288968284,1576228077,1479597122,522870367],[-0.23417076468467712,0.10817093402147293]],[139,[-487937866,-262094681,-1100039561,-6056955,-877674561,-5305710,-322568449,-1002108093],[-0.15504436194896698,0.1600046455860138]],[81,[-145030135,1397697579,-368311472,2104467426,1948713096,1040733696,1984303748,-213412093],[-0.15200254321098328,0.1641494780778885]],[582,[-553663488,457408791,1690141440,1367473802,1628234624,8189057,-168234104,-2104755233],[-0.16342587769031525,0.15110038220882416]],[559,[-536951808,429892094,-1375777504,501218759,284931200,-1195353611,-1376873302,-1410664983],[-0.15035997331142426,0.16500341892242432]],[528,[-44894976,-1624795335,-1917477097,-1074532871,-1260347476,-53365552,-94048341,-1349298635],[-0.12065671384334564,0.20961812138557434]],[393,[-391658845,-878467154,948331218,2028076114,-617926264,1225967168,810803670,-403971773],[-0.13285785913467407,0.1815747171640396]],[382,[235150099,2111413,-288488515,797719756,273701972,-3580555,235419741,775624979],[-0.22059056162834167,0.11039188504219055]],[375,[-109781508,1400132593,-553322700,-1067589039,-277020752,-893591409,-400248840,-1575945558],[-0.35007745027542114,0.07238174974918365]],[263,[1442780976,2147431737,-1666169666,-537068613,-50644545,-101694274,1208049627,1394086042],[-0.24647489190101624,0.09931936115026474]],[73,[-202903617,2062270127,905170938,-577838430,-177209377,-97988910,-7930226,-195566882],[-0.09162109345197678,0.2710406482219696]],[545,[168785687,1235052478,786401219,781123582,-1706219793,-1632109057,-1127231054,-907025697],[-0.1308988332748413,0.19008301198482513]],[406,[120052671,78952369,1815027037,40380180,1968535543,500178705,1182027596,1141154900],[-0.1992865800857544,0.12260554730892181]],[59,[-2123369037,-1007755834,-75536528,-539651674,-1347699781,-863068216,1903277778,-716704080],[-0.13662229478359222,0.18261213600635529]],[419,[18421695,-1616330919,1605960693,-1079042055,-1083695656,-583092739,-1389350577,-1414856417],[-0.10727475583553314,0.21985870599746704]],[468,[1078396753,354631550,-1967409351,428813808,2020671776,822063083,1609375984,201788848],[-0.2235974669456482,0.11054740101099014]],[425,[-136461589,-219700788,-1199836930,-205259036,-682692658,-35350016,-144121122,-246950930],[-0.09437340497970581,0.2560350298881531]],[79,[-159406545,1899276489,-465811838,1922100042,2029683712,406996630,1410543184,-800720294],[-0.16321568191051483,0.14754760265350342]],[401,[707767079,489540902,946742334,468429541,1549207925,513686359,2137543788,-839975353],[-0.20473939180374146,0.11587535589933395]],[566,[-1895302736,-1962819824,1692382641,-1351708978,1991000568,-1779630215,1152997709,-845161478],[-0.17858609557151794,0.1356053501367569]],[323,[-134476363,-2967727,-100720641,1308314038,-475336,-1368359364,-1558008326,706880550],[-0.27970603108406067,0.08576671779155731]],[110,[1109568162,965390091,-68932241,-316003794,1404437503,-152829605,-363761031,1476677383],[-0.21853850781917572,0.10969985276460648]],[555,[-134485751,-204476433,2021424858,1893059240,-59263232,-329974006,-1360480358,-1320953925],[-0.13401556015014648,0.18581999838352203]],[579,[-11036405,-777934827,2013002256,1897900916,-33842751,-1532740203,-1140932694,-1056772933],[-0.11947764456272125,0.2034149467945099]],[269,[-120159104,-590068356,1249209660,838451038,2026278092,-1460363686,-54526018,-1202980021],[-0.1263890117406845,0.1901579201221466]],[626,[-9458765,864484918,-1511008592,-1243354942,824178116,-2115851904,2108896696,-974655785],[-0.11565050482749939,0.20734450221061707]],[354,[841991296,285521189,-1619726583,-1383762267,269749288,1909506021,1324502735,136255191],[-0.18641577661037445,0.12567663192749023]],[64,[-1689540600,-171047537,367437942,-545536546,-27554929,-201455017,132968039,-56919],[-0.35486286878585815,0.06668654829263687]],[427,[-605489791,-1143709809,-33693697,-537069149,-711441259,-1200820238,-2033713755,71617805],[-0.16660074889659882,0.14271223545074463]],[477,[1308689921,584290,1663521749,-146281108,-1077992529,1108859814,-1259897617,-2080382578],[-0.13657788932323456,0.16795969009399414]],[71,[1120340511,285084254,-117988732,-1107318042,1085786093,-252053507,-1002113816,1348812912],[-0.22501066327095032,0.10219082236289978]],[352,[-1422610505,85245907,-1904285905,-1920684632,659881463,-1259208707,-11206692,-1963834010],[-0.14944109320640564,0.15598995983600616]],[52,[-10490065,1498147823,-553944480,-134217734,2145379919,1434648139,-118032742,-178274362],[-0.11018689721822739,0.20814745128154755]],[30,[-2134754710,37889724,-1915431920,794654769,608172791,1106720444,1637266185,262899994],[-0.42608824372291565,0.05488509684801102]],[557,[-601044984,2043973039,674530844,-201458955,1488507776,-1334981997,-395001942,-1827697397],[-0.13864666223526,0.167188823223114]]]],[-1.3544789552688599,[[540,[-144706765,-1289764937,-1210088649,-1073758505,-939528193,-1091062785,-1025,-1073745921],[-0.14142465591430664,0.26060691475868225]],[207,[-1025521953,-562077953,-489238786,1111423727,-896495890,-11540707,-270799138,-144703521],[-0.2282639443874359,0.16443152725696564]],[310,[-922776433,209603519,788277263,149814190,2129497804,246132079,-120197154,-1461715061],[-0.24959196150302887,0.13804991543293]],[507,[1997012981,-1089741086,2101339632,-852493088,-1154547807,-1416646978,1565513153,-1894785592],[-0.2350001186132431,0.14007705450057983]],[318,[-2621697,-572527203,-7537699,-540158759,-1285,-1084358700,-87233877,1527643417],[-0.13948769867420197,0.2373763918876648]],[516,[-12497,-67110961,1895293114,-202902104,-4473080,-582642432,-138761558,-247202870],[-0.12023023515939713,0.25221487879753113]],[489,[2138824465,1006370495,-1628438593,-1343230209,2147476917,1023206896,-323568595,619075335],[-0.22485122084617615,0.1354176253080368]],[387,[-581657131,-715649732,1609916351,-1086636163,-36644456,-201803780,-38076486,-1414920182],[-0.1813817024230957,0.16125626862049103]],[193,[1983843871,1509371575,1804010935,-76854621,1420240815,-139122955,-144183573,2068252417],[-0.19591303169727325,0.1467915028333664]],[622,[-2097425,-134218881,-20059542,-252920114,-1048690,1433398614,-134679350,-168300550],[-0.1234404668211937,0.2348768562078476]],[69,[-135790657,-101712898,1790961276,-1117268,-167790693,-1092435916,-134748966,-171444278],[-0.0936608761548996,0.2928491234779358]],[334,[1426527010,-134922701,-283812105,-2623147,-1643650329,-789597,-285199645,-2130690814],[-0.19195295870304108,0.1388207972049713]],[128,[-188748058,-604825097,228284687,-1059465,-744525073,-134502345,-74199121,-2098289],[-0.1702539324760437,0.1586102694272995]],[504,[1444369751,1571127165,1068923765,-418381862,1832345573,-1136660769,1164394880,-1928344087],[-0.17193807661533356,0.16115176677703857]],[212,[577742851,-69269885,687362130,939513855,1131934720,1456416085,930627893,-2100289],[-0.13450787961483002,0.1927766501903534]],[371,[-799212524,-209768513,-76223492,-117260294,-830701092,-357900157,-1262178576,-68422742],[-0.38245826959609985,0.06848365813493729]],[278,[207857047,271759280,-286880937,783085486,242614261,-1090523169,-287801388,-1158721947],[-0.19277557730674744,0.13489215075969696]],[220,[-509434961,-78710066,-1275726622,-753404420,-203423798,-1895375284,-673909042,-716971826],[-0.11536061018705368,0.22317345440387726]],[433,[-966602846,464451514,1072605484,-1080755764,2001878519,2011854582,-759073,2145875767],[-0.17061035335063934,0.14551682770252228]],[570,[-813715661,2085594419,2080225111,-1085276791,-2024283721,456261102,1944045462,-537989201],[-0.15524835884571075,0.16316364705562592]],[0,[-4194317,-437584658,-34446,-8391184,-1048677,-892667004,2009592769,-145255149],[-0.11445097625255585,0.21161417663097382]],[160,[-354096414,1341058784,-21551621,805293574,1727467967,2145870579,-954962,1608997489],[-0.24929216504096985,0.09536799788475037]],[175,[1133488646,19079215,1923684923,531607301,1643598741,16213639,1617959234,1911899943],[-0.2060607522726059,0.11344346404075623]],[638,[-201331985,785378799,-89140512,-506473810,-117440562,4699124,-137366572,-237519977],[-0.1317863017320633,0.17929145693778992]],[333,[1500975106,-591757505,-758294281,-159531458,1694185913,-1025496001,1422394875,2130083615],[-0.31970807909965515,0.07318614423274994]],[186,[-252197833,-222679057,-691545539,2146925322,-6971745,-20010264,-23089430,809597526],[-0.15578804910182953,0.154715895652771]],[472,[528507857,220264592,61472209,-1121194504,-1442796359,-325000515,254280095,-1913940771],[-0.16838937997817993,0.14214324951171875]],[113,[-475271441,-85279086,-969873294,-134745356,-251673889,1917934182,2004345816,-238553377],[-0.11203543841838837,0.20810666680335999]],[222,[191466152,330415091,1572941818,995482546,-438844995,1062600569,-1777930,803654826],[-0.29350510239601135,0.07969966530799866]],[149,[-357896534,-759148596,-2131974922,-70545720,-2069117009,1145131456,1431040222,-250146674],[-0.14853553473949432,0.1520741581916809]],[100,[-1045909650,-485611602,-1652363393,-544464370,-210833459,-205732025,-412155925,1161839428],[-0.18374817073345184,0.12407581508159637]],[484,[-2095020544,657773984,-1117496013,-272004556,-2094691888,-1119555728,770155170,618987575],[-0.22434021532535553,0.10049981623888016]],[551,[-604093873,-680840318,-1293353250,1356493274,-100805812,-1358042356,-255656308,-800923830],[-0.15413197875022888,0.1519971489906311]],[422,[-434654828,-1852916328,1623711061,-39002980,-1050953440,-356992779,-853603320,-909122417],[-0.15186285972595215,0.15066848695278168]],[362,[-547512961,-614724543,-5289121,1609498725,-593979176,-30505107,-4653062,-5570290],[-0.09397383779287338,0.2500700056552887]],[502,[421738261,512647543,-369466298,644660618,2014315985,-122826753,-1422579952,144522760],[-0.22758260369300842,0.1047687903046608]],[414,[-1743450761,-1667229731,-1111617731,-1918337240,-925364899,-1401618467,-396751348,149751812],[-0.20826348662376404,0.11135675758123398]],[327,[-589833067,1358228811,-1174324513,1295585037,1415054460,-677683843,-333918502,-81110961],[-0.15275685489177704,0.14786334335803986]],[513,[268447386,-768359989,971741463,-746591329,527673597,-1371129041,267492031,976958107],[-0.21398673951625824,0.1099848747253418]],[287,[458687,74623383,-2110418338,547384028,1146347479,-300476649,1564515406,-549791922],[-0.1906532496213913,0.12086330354213715]],[321,[-1292446785,-1832472107,-874707144,-1316029507,221482943,-54635093,545718286,1952442355],[-0.1822858303785324,0.12629486620426178]],[613,[-270555509,-2024964282,-340115456,-225707460,-214435894,140869335,-755499270,-514854962],[-0.12966547906398773,0.17814037203788757]],[237,[1082294452,279297776,739665110,-1112548388,1558433004,2084562652,-285242720,822082519],[-0.21644125878810883,0.10754965990781784]],[436,[-15204353,1555857143,-370262113,250109974,-813863457,-118130955,1282485260,-1069443322],[-0.11899302154779434,0.19272921979427338]],[195,[-458694430,-18865688,-1560262314,-174816220,1940735675,2090354613,1843399327,-619769841],[-0.14177682995796204,0.1578184813261032]],[396,[-281556317,1261252513,-1844107376,-1199966654,1342875535,19047986,2048844537,-968363117],[-0.12672142684459686,0.1768610030412674]],[349,[1148552471,346904839,-953859756,-2069582840,808277389,-1544759304,278153766,4483088],[-0.21089854836463928,0.10583920031785965]],[26,[-7511510,-490213457,-1597284352,-261998934,1392468480,403119942,1463169024,1346631127],[-0.28440985083580017,0.07838387042284012]],[235,[-190183264,1490329067,-1474001346,-1131666523,1952776616,-1196417886,-93570934,-1442369763],[-0.15732380747795105,0.14436712861061096]],[77,[-234098945,1368758007,-117547043,-4245393,1380304763,-1137653377,-214434069,-302694186],[-0.10450263321399689,0.21435043215751648]],[617,[-71305489,-102079985,-2135231236,-1835277350,-268448192,1963334918,-1485652036,1347612190],[-0.16540440917015076,0.1348690539598465]],[359,[-643869705,-1701290179,-88374529,-4249741,-615216080,-17244690,-248849941,-1146418449],[-0.08967950940132141,0.25216883420944214]],[259,[-2018016477,-135365668,-1220542478,-176295708,-1145529441,-809304950,-718277394,-716994988],[-0.11881193518638611,0.18229863047599792]],[521,[-659116544,-39007946,-1632034575,-1109530378,-507923208,215233620,-38434133,-1994050161],[-0.132778137922287,0.16686922311782837]],[250,[139447519,-1707244049,543030878,-1876755446,418206416,-1344311399,1070800532,-549216572],[-0.1713460087776184,0.12920229136943817]],[563,[1065351043,-1747387425,1440546065,-1130366210,1655662591,-1481415173,-1230520517,-1409895749],[-0.12196241319179535,0.18406112492084503]],[590,[-1489788317,-215482932,1167516208,-1243059476,924747395,731626188,494917253,-1008733729],[-0.14610977470874786,0.15349069237709045]],[132,[-404231235,-154083334,-386960532,-102894914,-1494941761,-523327360,-341382230,-993681342],[-0.11227646470069885,0.19230186939239502]],[189,[461571246,898745267,1045718764,1067757070,838700687,523352002,752994271,746684614],[-0.31202149391174316,0.06988177448511124]],[319,[34848759,77858295,1755294463,-1612362699,279183197,-1091412483,74513294,-583830953],[-0.11771897226572037,0.1843392699956894]],[412,[-251804257,-1722448993,2138034394,1294782986,-571552448,-178921642,-290600310,604546090],[-0.1507510393857956,0.14529505372047424]],[286,[1276237310,1447458454,-934498184,1623452708,-1280217356,1357512592,552402944,-528812416],[-0.3494941294193268,0.06317922472953796]],[449,[1663467552,416069478,-1587070595,-1216236355,394667813,694428982,203361851,-1578764654],[-0.18098776042461395,0.12132294476032257]],[554,[-54546545,-142747138,-1662192658,-117332434,-558788200,1595934520,-1372737862,-1603959254],[-0.11871227622032166,0.18538504838943481]],[580,[-136315383,1373438347,2030042960,1935331222,-1732324440,-1866576357,-104547704,-1603561958],[-0.12392864376306534,0.17591291666030884]],[37,[1179711253,932391543,-159717504,1907212206,1953330597,1588556119,1323654868,-59937033],[-0.12764546275138855,0.17045098543167114]]]],[-1.3531562089920044,[[539,[503783185,488612115,-1644183017,-6291490,1583288237,-8273,-67147553,-50467906],[-0.23604758083820343,0.14624665677547455]],[125,[-754980097,-92799137,586973151,-1206650282,-1015162113,-524401,1993957087,-172492917],[-0.20661883056163788,0.16887924075126648]],[384,[-669517935,16839696,-584633040,2098167213,17062277,404341136,716110059,789549483],[-0.19015172123908997,0.16193783283233643]],[94,[-232805713,-20973057,-34603081,-42607105,-356519937,-118109025,-387977505,-2134520828],[-0.1541261523962021,0.1933469921350479]],[466,[528479223,1904195575,-55119905,-10575890,-947918857,-288358401,1944539762,-135803137],[-0.20460499823093414,0.14255046844482422]],[47,[-201327709,-19927066,1384109026,-739317020,-12587089,1179121028,-241174704,-168296506],[-0.1505453735589981,0.185949444770813]],[339,[-581632048,-606277136,-368021128,-176383780,-956433298,-268467029,1551686188,-36312104],[-0.30771997570991516,0.09145938605070114]],[229,[1595362495,532541183,-1879371521,1474776767,2138187743,1066097155,-537382949,1073678919],[-0.21706582605838776,0.1526443213224411]],[254,[-864133153,-692189161,-1099006309,304102399,1492012696,-93634736,1580914378,1929543391],[-0.2538846433162689,0.10271452367305756]],[457,[-64904797,-305660509,1468164915,-565,1059143671,2107965041,2079816191,-18385129],[-0.1301027238368988,0.1987140029668808]],[511,[-220210673,-536887410,-757597954,-235406646,-67108980,-867369216,-31934210,-251404342],[-0.10590624809265137,0.23959128558635712]],[497,[-740299264,146795517,-50352379,-1912628641,1053777812,901722868,771235244,546303999],[-0.22008468210697174,0.11258465051651001]],[355,[343358231,213430181,-1364197793,1073696095,1515411668,-1059425,-13050290,-1078259937],[-0.11953093856573105,0.20282387733459473]],[373,[1245651467,183707959,582887578,31777007,1691296546,237724247,1348920347,-203426945],[-0.2097310721874237,0.11584123969078064]],[35,[2043933695,-144771167,19989255,1072001427,-506069031,-1929904385,592718063,-169511337],[-0.12918440997600555,0.18772880733013153]],[606,[-8388705,-6177,1207429886,-170921106,-34868280,-53228224,-120789286,-263979270],[-0.0942123755812645,0.25828978419303894]],[179,[1023876484,-256822903,-1178663971,-236886881,-236217863,-52522546,833036237,-8385148],[-0.19691897928714752,0.11818353086709976]],[124,[-85998033,1383065263,1880502784,1516235723,2004838208,1346401792,1406554770,-214434290],[-0.1725260466337204,0.1334143728017807]],[508,[-215485453,-1146159840,-1684308559,-551551504,-1448107335,-1364266068,197057439,-536879651],[-0.11676036566495895,0.1934867650270462]],[388,[1867466672,-654132904,-1678940751,-1073748483,-1814283784,982153656,163645464,-1409292229],[-0.13684672117233276,0.17416830360889435]],[241,[809177088,255278841,2052280087,2064460633,-357326549,-1147540547,1220038219,2137729130],[-0.3888067305088043,0.05829756334424019]],[463,[2113404919,2119573369,-1216467140,686062828,1599431925,-35250219,-379253932,-588874972],[-0.15329168736934662,0.15723362565040588]],[74,[-142872849,-1123585,-1226054942,-168821790,-16843777,-682478139,-148908590,-170396193],[-0.1018904522061348,0.22314058244228363]],[491,[666353457,301069589,-134324227,-1850148995,-163618376,-1430878019,-272720724,747437600],[-0.15450522303581238,0.1451505720615387]],[265,[-1520456817,-672619806,-1650134352,-739249448,-410644,1850609408,-671883070,-716704562],[-0.1173662468791008,0.1908893585205078]],[640,[-268439637,1743224491,-1062209824,2075129070,-100680209,17194724,-503319132,-705694017],[-0.1188521459698677,0.1887996941804886]],[351,[-235865865,-778321560,-773813480,-977205055,-671809621,-2037042305,-1373573570,33902919],[-0.16477152705192566,0.13532666862010956]],[281,[1288503263,496561871,-41996993,-44831381,513794520,-6296133,-11544504,-545596144],[-0.137915700674057,0.1612575650215149]],[592,[-536990993,-545437826,-526919430,1927770592,-1149453240,-12157692,-795681042,-263995441],[-0.12841881811618805,0.18196146190166473]],[200,[-998586714,1923629704,-1567936970,-106682636,1246039486,1391923172,2128445439,-1161220571],[-0.15602493286132812,0.1402648389339447]],[574,[-902847741,1122672495,1740485216,-252907012,716157611,40020694,-1354763301,-705694561],[-0.12058999389410019,0.18453432619571686]],[411,[43190589,237000119,-1479864545,218107700,1727349849,-84927193,168837124,201328132],[-0.2745037078857422,0.080660879611969]],[503,[1145396501,-596306061,-1883250830,604912112,1244529921,1003257717,2066631424,-2012515040],[-0.22889545559883118,0.0964030921459198]],[337,[-704658769,-746617960,1373614706,-235613442,-75520946,1213059150,-705694758,-783549270],[-0.10090714693069458,0.21733656525611877]],[239,[-555432832,-1192997968,-286225699,-35864850,798508188,-1108076048,-365142037,-1475542263],[-0.12968091666698456,0.16475941240787506]],[288,[-573549447,-676853411,-1052201605,118622855,2113410296,-336957660,-618632262,-1442645118],[-0.2590109705924988,0.0811891183257103]],[368,[59193218,2011775753,1576662974,-4195534,1435621351,-1702058057,-2017202266,1977972599],[-0.181268572807312,0.1182248443365097]],[499,[-988850988,1016641345,160914500,-474084890,154236052,-1078989845,1441076224,1040163634],[-0.31549975275993347,0.06667444109916687]],[364,[4349984,639681328,129645151,668510722,1148195120,2062963956,153885736,1647779843],[-0.22751913964748383,0.09298747777938843]],[602,[-276828158,1130870371,-1531186142,1894639586,1969214592,21205461,-621813270,-792462373],[-0.14819277822971344,0.1404644250869751]],[353,[118533695,270567044,1288523533,-1113072698,827708669,1605729269,-1091248161,-1141436093],[-0.11226476728916168,0.1889517605304718]],[340,[-85094773,1487850663,721041258,1374657596,1581250843,1112227395,-132434392,-362545185],[-0.11850622296333313,0.17904981970787048]],[424,[-1044577783,-3170331,-210830594,-539528456,-1410339954,-1907435776,-201789206,-213651366],[-0.10257221013307571,0.2029486894607544]],[632,[-176173273,-63971382,-92540894,549647982,-1338560,1476888389,-78600722,1081266138],[-0.18841557204723358,0.11295656859874725]],[520,[-671890176,-181237993,-1548798927,2005171473,319759748,-51300395,-1366522344,71618053],[-0.20921583473682404,0.10211694240570068]],[88,[-1602114652,-1100433156,-2115529,-1184520048,-1984574213,-297169991,366750175,88360004],[-0.1925288438796997,0.10750143229961395]],[216,[39586,2101407957,-35376942,-644184357,-1586894610,-971554785,-5808574,-784335945],[-0.38588806986808777,0.054346900433301926]],[496,[-1706958944,451947443,-277301988,-1936926482,1031093973,-1141080111,-100907,-5619417],[-0.1415555328130722,0.15182578563690186]],[385,[135519103,-1701046404,-476620169,532383797,-110098979,-17150466,789376991,-1153760421],[-0.09721077978610992,0.22095255553722382]],[119,[-68163926,-758185242,-457882974,1943200494,1903550542,1144214864,892323778,-178276493],[-0.14763863384723663,0.14450113475322723]],[163,[-894505938,1726040062,-1434959774,1382606572,877626085,912263760,854318029,-235154495],[-0.19948244094848633,0.11108753830194473]],[114,[-391748914,-87495070,1790763645,-34746582,-1894316363,-151775424,1299316735,-380410251],[-0.13808603584766388,0.15448948740959167]],[514,[-222361681,-142684161,-1011944610,-254805175,-178667764,-968192512,-267197752,-251479346],[-0.12034154683351517,0.17198434472084045]],[377,[202861343,507076415,721758013,687907104,1979993013,-907174864,1142178076,1145104400],[-0.21315360069274902,0.09752862900495529]],[510,[-928024797,453054,-218994892,-185220464,-2126798169,577432266,713554911,-1090527317],[-0.11146946996450424,0.18797753751277924]],[611,[-4213,-1009925217,-1562674624,-1151402642,-234885490,-590488821,-269837110,-774636550],[-0.10050378739833832,0.2044314742088318]],[442,[-797631013,362314737,-1347420291,-1079419975,1280903925,-1262749233,-1377484823,-2001796897],[-0.10836073011159897,0.19475993514060974]],[432,[-316169693,-1344106541,942691362,-202380558,930163489,240608166,1442141879,-470334095],[-0.17380046844482422,0.11888035386800766]],[252,[148589439,-598544709,1720098523,302055418,1690358359,-537721785,1465335308,-581808818],[-0.1894366443157196,0.10867591947317123]],[272,[-227276285,417733055,141878230,-1930563657,443500552,-1197990268,-1091601874,-1428702701],[-0.15042302012443542,0.13850454986095428]],[365,[-295443825,1881192242,602866173,-1583502408,217335229,1880585519,771686574,74185330],[-0.15697461366653442,0.13160589337348938]],[240,[-266098716,966942003,-1407291626,-1109396138,1947725557,517720022,-2137792561,822083471],[-0.1492486596107483,0.14093182981014252]],[467,[1147473171,1279326997,-821293199,17582980,1561349141,1035756917,1693723716,134813760],[-0.26860836148262024,0.0791471004486084]],[480,[-745413331,-1188194,1267976940,-244057892,-117834344,-1182421888,-1244286022,-675048758],[-0.09677238017320633,0.22355352342128754]],[378,[-602815049,2117625195,-1582168171,-443273818,-810235495,-1412593179,-906457953,-301186044],[-0.12496618181467056,0.16549517214298248]],[223,[784214531,-1481197765,-1175381145,-142769749,-1364414680,-1560957006,1999583123,705541055],[-0.21282529830932617,0.10288743674755096]],[528,[-182344572,446549361,-1443603113,-1212176388,-554397288,-1963900929,-84743208,-1364216305],[-0.1101992055773735,0.18950794637203217]],[616,[-1303385209,-896561650,-523800238,-151652106,-548431989,1168430244,-503320351,-2064649010],[-0.1337122768163681,0.15766866505146027]],[22,[-218105953,-679114691,-1267388358,-73099594,1743867279,2020921186,-6432370,-179836046],[-0.09166912734508514,0.2281249612569809]],[332,[1790410753,1194205785,712913755,1475242910,1659961600,360013679,1966797212,-10529238],[-0.139383465051651,0.14807218313217163]],[488,[-56065920,-160826232,1866034740,-2282635,-1612249423,-2037227788,-1350786837,-1091042443],[-0.12180531024932861,0.16849491000175476]]]],[-1.3695704936981201,[[120,[-893410561,-536892673,-353374337,-2103706,-203480129,-21509,-167809029,-1881225],[-0.16063986718654633,0.20273709297180176]],[505,[-33816579,2073344700,-1558201345,-4208492,-1141113363,-1076259588,1055902713,-575711916],[-0.26204735040664673,0.12330465018749237]],[335,[-293634177,1400863359,-389223329,-201985,-834109825,-285447041,1851805055,1870364287],[-0.2892760634422302,0.11206240952014923]],[357,[1515124725,369927936,-1610666831,925855537,272631836,1531994580,528108223,455021023],[-0.1916453242301941,0.1483595371246338]],[560,[1060087328,398437823,79655216,-1914176017,1558052245,520000392,-1268336765,212632751],[-0.26242268085479736,0.10239273309707642]],[623,[-2629905,-78655505,-891554078,-1058015250,-264250,13096772,-168561212,-235405345],[-0.13851559162139893,0.19113600254058838]],[162,[-202375721,-2111636702,-721301001,-1574913280,56560078,-18677777,1087758426,-353913415],[-0.24820300936698914,0.10685873031616211]],[50,[-8917009,-103854097,-225450462,-1575174,-8377,608239190,-209718336,-171445561],[-0.13929376006126404,0.17934627830982208]],[588,[-543165517,533196199,-1426074862,-544347146,1130218371,8697232,2010559954,-741343554],[-0.15528613328933716,0.153925821185112]],[527,[-411054193,-1281884233,-1115833348,-1079673394,-1581284984,-1148589108,-289498744,-2000102976],[-0.10468549281358719,0.2299884855747223]],[172,[-25174417,2117286413,-804523330,539753215,-1568838361,2136045431,-87049377,2002249599],[-0.24891264736652374,0.09397143125534058]],[366,[1210754048,537666304,161348605,627257024,1158955796,1668769269,2008953695,1744796787],[-0.22588098049163818,0.10365047305822372]],[180,[-281288273,-70327314,-796921740,-738730264,-76562513,-808792948,-1745881398,-136841842],[-0.09550877660512924,0.24650630354881287]],[341,[-886059513,168764323,-267976118,-973090298,2097352643,176495367,1802990159,-385882317],[-0.1835000216960907,0.1264064460992813]],[595,[-1,-134221825,-218431816,-235405578,-573046902,-35956864,-177472566,-170394389],[-0.08009938150644302,0.28082871437072754]],[314,[-14155881,-1111314545,-1353609437,-1358649856,-20250627,-75628649,2146789220,-69994733],[-0.10310708731412888,0.214595764875412]],[404,[85077823,206351119,984419861,20769365,1278506495,509705715,1276407516,-599458444],[-0.18984027206897736,0.11972358822822571]],[206,[-665271074,1464887091,-2145223045,83944575,-664089410,1012828430,-1092945194,-12125474],[-0.20856955647468567,0.10800682753324509]],[49,[-138414161,-152571922,-234883034,-218173190,-69748337,-796834004,-51382306,-235410218],[-0.1027757078409195,0.20767506957054138]],[420,[-75500559,-1089602040,-645940751,-3162657,-1738551119,-1692862817,1308311003,-1993363541],[-0.09340593218803406,0.2308514267206192]],[455,[-853368159,-1054814,389048702,-1710123,-1154186313,249649875,-10095637,-606120681],[-0.1059320941567421,0.20079787075519562]],[11,[-2094007297,-442069004,-605586698,-34146080,-538208337,-1964457677,1912461024,-168302604],[-0.1310754418373108,0.16108058393001556]],[198,[-158677344,-181113360,-1528008969,-571176396,1048269475,1988456372,-27492353,-11978479],[-0.128440260887146,0.16143999993801117]],[544,[1596188085,-987304491,452284863,-5645158,-497029639,-48137,1575704025,-806361704],[-0.15479210019111633,0.13679373264312744]],[416,[-803355391,269612059,-103964896,-584190505,-1253042176,-1287846874,-1364704630,70252047],[-0.1722220629453659,0.1234646886587143]],[486,[-474377216,339541264,1577057677,-1916842185,-281745276,-1666877764,1774055176,135393823],[-0.17964713275432587,0.11774364113807678]],[381,[-169103377,-138944407,-4966664,-1117990427,-67251458,-75752779,-34803588,-1920930339],[-0.1593102514743805,0.13285309076309204]],[1,[575365347,953914139,-1078241825,530438005,8352735,-1140861665,-2360355,100616191],[-0.20287837088108063,0.1128615140914917]],[301,[-2087738451,-236978972,1945051832,-204474216,-4213090,1812100654,-163122454,-721178166],[-0.10795794427394867,0.1885324865579605]],[406,[-763363841,1161592123,1218843669,589341989,1147453433,878107283,1673862468,71320641],[-0.1465872824192047,0.14068178832530975]],[448,[338702321,-955723536,223548671,-1618687592,640728233,-1892106840,253306637,160038936],[-0.2504936456680298,0.0826164186000824]],[309,[-1069741553,285037405,549372488,157368060,1549206792,476787267,-223284936,-1460015186],[-0.17551939189434052,0.11551206558942795]],[16,[-559645958,-82033479,-315888674,990559098,-363475207,-1143281825,-279609123,2137035924],[-0.11419827491044998,0.18112944066524506]],[641,[-1880100957,172207746,-2042666496,-1609982232,-877665398,84329482,1961357506,1153890247],[-0.2089102417230606,0.09833736717700958]],[19,[139296,2079280460,-1035450949,-69253746,155985087,2113766731,135764411,1608486923],[-0.4137556254863739,0.04951225593686104]],[312,[-732977643,277903282,-1368217772,-459228376,813142006,837885431,267818252,-1528500380],[-0.15581859648227692,0.13339102268218994]],[517,[-83917399,-173673311,1626321594,-237716638,-42225124,1728340229,-1232225590,-217329398],[-0.11577171832323074,0.1834014356136322]],[316,[771960639,134409859,258363165,-1394364928,1598414807,2012884973,1584664128,708838210],[-0.216262087225914,0.0954771488904953]],[572,[666873121,-1893565126,-805613104,2079585760,774873761,571125668,387929937,-1046488867],[-0.19903813302516937,0.10453511774539948]],[28,[1635451153,957686021,-451272973,1032273426,1958556740,1036989745,177547808,720439818],[-0.23054862022399902,0.08963410556316376]],[70,[-12625,-404502550,-612371614,-540029722,-8650866,-1469579796,-67108900,-171445794],[-0.09864431619644165,0.21435028314590454]],[451,[1576224017,1500983061,-1166946313,1564704015,-289709104,-1073863490,-828084534,469780763],[-0.16094450652599335,0.12683431804180145]],[245,[1216020163,491947673,-499451301,-71647474,170564572,-37852258,-346584676,-75029248],[-0.15302763879299164,0.13355283439159393]],[524,[77630976,505053924,-1913377232,-1381980602,569648564,1022552432,-1514830304,12870692],[-0.2971450686454773,0.06969578564167023]],[550,[-496000253,-894677310,586380658,-1071684144,-1104764373,640414632,1062711983,-537927685],[-0.11207269877195358,0.1844300478696823]],[317,[-805261899,832624929,-898801809,-1107465875,-413863428,-1242559753,-17457656,783226135],[-0.11341021209955215,0.1786959171295166]],[578,[-672676857,-671259169,-1426431184,-255886694,2067195272,1640486343,-139008630,-247225717],[-0.11504149436950684,0.17776061594486237]],[144,[-257186129,-55922845,-1077084195,-144891130,-20726341,-119362033,-410783813,-850035177],[-0.10251285135746002,0.19268713891506195]],[600,[-143655125,1979709255,1894602946,-754483258,-161491838,1154539319,-192615542,-237504533],[-0.10122653841972351,0.19483508169651031]],[356,[2135702864,-1079215788,-36702019,800889112,-1154973384,-1120359512,-340087880,-1574789070],[-0.2610571086406708,0.07492011785507202]],[367,[-1064320849,-888408663,-1962966286,1885579722,-23076338,773293122,-85339410,-481827590],[-0.11406119912862778,0.1742027997970581]],[374,[-213751904,1605143820,-154679544,-16761,1735620549,1320605166,-504922689,2011657827],[-0.1686951071023941,0.11749855428934097]],[503,[341128023,1349080691,-1529909789,1250525156,1550284273,-72450689,1588033856,-2012225976],[-0.1634955108165741,0.11994868516921997]],[256,[-32515877,-176855289,-261031297,1057333783,-1032058104,-9935818,-5099570,-13137826],[-0.07970619946718216,0.24850650131702423]],[61,[-2068804128,-614601995,1600116177,-71247792,2029357231,-1082179864,-2010903395,205586689],[-0.1640268862247467,0.11898551881313324]],[213,[-1435261405,1677689511,645850208,-236093449,657455952,121542592,2002383380,-210522417],[-0.12863418459892273,0.15204958617687225]],[439,[157636991,-102735875,1049370419,1031056562,208487861,712996735,-44814592,-1994037727],[-0.15739968419075012,0.1273098737001419]],[435,[-1060257076,2111306357,-1702557168,-590175618,2013791500,2051870375,-52680182,-1454021666],[-0.19914233684539795,0.10423366725444794]],[358,[-625293571,-19622942,-8397475,-314591297,27992299,2121821739,136847918,1146475566],[-0.19054318964481354,0.10671291500329971]],[492,[1789983778,1018022897,1861236018,-1611852202,876642213,975183222,1055947774,-1343409645],[-0.15487708151340485,0.12867331504821777]],[279,[327569,218144993,-1778603565,-1353503484,1175427833,-1882729345,-1546330360,-1154386112],[-0.17322303354740143,0.11569347232580185]],[601,[-349703245,1433019314,-818911184,-1010310506,1433778070,920808880,1642167992,-1328024681],[-0.12355220317840576,0.1641998440027237]],[93,[-14441821,-609294870,-757607712,2138420300,60657067,-893238130,-182328108,-706218300],[-0.12934589385986328,0.15674130618572235]],[474,[-1928604261,-1929346844,-581011079,-541065844,-1962793025,-1959006544,563584939,-1880104977],[-0.08118641376495361,0.24935099482536316]],[597,[-420879597,-589318153,-893880704,-1313297248,2046665348,-1124139690,-1345913272,-264117294],[-0.11719886213541031,0.16577914357185364]],[382,[405429943,35157617,-142668179,-1390625436,1507609804,-621125793,-1973510450,-1165812617],[-0.1302436739206314,0.1481214165687561]],[147,[-3217685,-30740518,1938426620,-538214752,-18219057,-1255846052,-201922374,-237023073],[-0.08887697756290436,0.21971827745437622]],[639,[-738726997,187692013,2011134000,-1955661828,-210119423,38790407,-755108400,-987308314],[-0.144447460770607,0.13702638447284698]],[452,[-206995293,-545767542,1907093886,1935741694,-335563268,-280903864,-675039554,-211558710],[-0.09513164311647415,0.210707426071167]],[271,[-1002116576,1325996544,-665522936,-597824890,1711699965,-73301301,-20448286,-110104046],[-0.41298919916152954,0.04899816960096359]],[407,[207949127,547888959,694611503,671116356,81544535,-709767,4261201,4220420],[-0.286625474691391,0.0699523314833641]],[127,[-1060119505,1644687048,-1964847366,582987502,-784636060,827683077,1339008607,1415048063],[-0.34356600046157837,0.056598760187625885]],[266,[-803222897,-61161545,1635244831,-1845510438,2098171271,-116434698,-21512229,-513563541],[-0.09976167976856232,0.2059866040945053]],[51,[-347603025,-1870744068,679009562,-113708376,1464161920,-312465308,-41992630,-246943142],[-0.11211391538381577,0.17646948993206024]],[190,[-2013321968,-742767341,1337165114,-72328646,-1787329601,-841026207,512443531,314072475],[-0.18131621181964874,0.1113327294588089]],[319,[236183479,76657666,176155709,908181400,409361495,-1141645905,-36647350,-1153869993],[-0.1289842575788498,0.15491998195648193]],[586,[1266667810,1511123775,2094253840,-44707245,333808663,628498310,-402685040,-962095185],[-0.17281804978847504,0.11423593014478683]],[192,[-236396348,-727146278,1815085813,-38007520,-171378486,-37738304,-206180609,-107261665],[-0.10720640420913696,0.18341147899627686]]]],[-1.3276469707489014,[[434,[1206384503,2004349367,-587202625,-570458153,1962898935,-135266313,-1091309570,-20973573],[-0.17421774566173553,0.17884284257888794]],[165,[-134742022,-627902534,1968427302,-560480,806424439,-67650565,51447330,-2133022],[-0.2147657871246338,0.133590966463089]],[515,[1057161146,-6307941,496502567,-1617216785,391903915,959252225,772505566,974076347],[-0.2319275587797165,0.12110722810029984]],[257,[-1700555934,-500504126,370126658,-1079154897,-284626360,-1924137013,387672847,-3156277],[-0.252623975276947,0.10572800785303116]],[159,[-1000152458,1895493330,-281178273,2146355560,2093311095,855405813,-543404209,821946229],[-0.206059068441391,0.12582625448703766]],[87,[-34608193,-201856262,-352407873,-1461520,-1057490945,-307438354,-52433718,-1060773758],[-0.11796126514673233,0.224318727850914]],[106,[1362202543,-776621899,-193898077,-652093965,-805254933,-1070494539,-755921969,-553071219],[-0.2505123019218445,0.1018962562084198]],[168,[-897588481,1378039343,586731134,849323774,-615806066,1984956738,-10032198,-212598805],[-0.1737886518239975,0.13972662389278412]],[620,[-1329,-67371793,-1023412510,-219219134,-146872510,-950677692,-170852800,-263718034],[-0.14755988121032715,0.1593601256608963]],[409,[-807993858,-444659489,-222269284,-806825748,-620958468,-268492551,-286463746,203485430],[-0.2265692502260208,0.10586404800415039]],[14,[-33558609,-65083682,-811175422,-123558262,2138007431,436495750,2139054674,-263724334],[-0.13516700267791748,0.16823019087314606]],[327,[-749235619,-740780991,1904177247,1594623829,1529354448,-172286859,-15541176,-1358771366],[-0.18130488693714142,0.12141185253858566]],[182,[1584990352,-473857834,1605700853,-100717318,-212604370,2125138082,-883446021,1534527706],[-0.15860173106193542,0.14470864832401276]],[573,[-1507874397,113426414,310100625,-621282947,573547171,583934606,1300686811,-908069413],[-0.11836400628089905,0.18674106895923615]],[429,[1929570099,1923122619,-1622715780,-1577842,-213749257,-303632667,-168461162,1626695507],[-0.17876005172729492,0.12363989651203156]],[391,[-73430485,-9438562,2060940976,-201347366,-1144276274,-2029070526,-134228252,-146546470],[-0.1017240658402443,0.21338729560375214]],[376,[240453683,156000191,695730801,151579604,1499214964,243072615,1282672708,1080359008],[-0.28052809834480286,0.0774340033531189]],[470,[962023287,34183056,-852552331,-1347425040,1042303461,-1413613637,-2073063769,-1884301079],[-0.13582970201969147,0.15512573719024658]],[140,[-816847897,-235995450,1886384818,-707134232,-335615089,1147431968,-136072004,-171444858],[-0.11215072125196457,0.1839037537574768]],[564,[1059584305,-1155622093,766342448,-1098140737,337323539,-289461769,222106113,-1917846854],[-0.20957885682582855,0.09911903738975525]],[78,[-2102645,-249598262,318267466,-345408,-622854336,1577202951,-246163506,-247012394],[-0.1202320083975792,0.16939368844032288]],[343,[1291652149,416690045,162229011,199289850,16481324,2128737110,2062154440,-287838248],[-0.16363579034805298,0.12322180718183517]],[135,[167774464,-153547823,2081759095,-34798827,1856352957,-1309945394,-343857497,-1476130752],[-0.1910814493894577,0.10656961053609848]],[5,[-207883269,-337118226,-220200976,-672139828,-205541893,1284500906,1402970448,-984099979],[-0.12751424312591553,0.159833624958992]],[361,[-46205443,-77802884,1811618781,-675364873,-61313858,-1149589828,-824262757,-487644637],[-0.10761560499668121,0.1867380440235138]],[289,[-1308578941,545317504,-1228750258,-546977248,1313629625,1084548279,-448115765,1312899649],[-0.1779690682888031,0.11297041922807693]],[236,[2124418214,497585588,-273412583,-36860403,410545912,-21749827,-57253938,977282818],[-0.20535370707511902,0.0986572876572609]],[634,[-2628625,1397615486,-222948678,1760951400,-707857434,-108282272,-255668840,-246153218],[-0.09891577810049057,0.20372001826763153]],[441,[1349520221,349187989,1306762509,220739316,1551189437,-1786970795,-860861440,137176337],[-0.21401090919971466,0.09538363665342331]],[483,[-127866240,-1243498252,2075148792,-656172,-1077434979,1089506804,-1075988545,-1614085205],[-0.1046956405043602,0.19067412614822388]],[253,[-573002017,446301319,-1152229985,51053115,-585457444,-897361,-2109064294,-1442650241],[-0.18012882769107819,0.11004448682069778]],[494,[-252189692,66845267,-974148203,-1923221545,1325215904,951641541,2060832424,677760795],[-0.18193097412586212,0.1096099242568016]],[583,[-738863711,1434287531,1197897784,1438902146,1641133962,-66261760,-37880920,-2004551028],[-0.10881505161523819,0.18144680559635162]],[174,[573541027,1676608015,2242082,-206569477,997981440,1449923588,911537988,-212864065],[-0.1332176774740219,0.14928372204303741]],[421,[1746154929,-570884612,1069104059,1601896941,-1187998728,-1080239892,196885913,-1414887399],[-0.15190990269184113,0.13106991350650787]],[428,[-821319005,-814945359,1779284706,-1712448,-210261089,1179740053,-1214778225,-136349857],[-0.09939505159854889,0.20520499348640442]],[380,[-145235969,-541098113,-1368522945,-1478098961,-177733697,-13142085,-1287296305,-1073370811],[-0.08019614964723587,0.24394263327121735]],[7,[-1846704337,-178936003,-1015991060,-772726649,-1444167761,-1145590493,-772690101,1503989547],[-0.20097000896930695,0.10192957520484924]],[202,[1316145887,271851495,572705407,273870363,2131758335,-246658261,-14692406,-143193986],[-0.1798076182603836,0.10900650173425674]],[347,[-103681070,2069215860,1035229628,-688071392,596173394,-72609495,-80709704,-669718415],[-0.21817618608474731,0.09075445681810379]],[417,[495269373,487127029,1072919743,2108013936,2055212316,-1615987457,175799945,-1359017709],[-0.12845875322818756,0.1588725596666336]],[296,[585867779,-2097176059,-2098537921,-539000365,1583727364,1442265348,-1814891014,-204483334],[-0.10467831045389175,0.18451270461082458]],[303,[-607653333,-9692435,-247750602,-747325454,-13637809,38165774,-204802086,-775686706],[-0.10023513436317444,0.19359007477760315]],[629,[-545306625,2064440055,-352337922,318168918,-74269108,-143238655,-828194850,-1069094165],[-0.1195247545838356,0.16566169261932373]],[97,[-1610421825,2086727596,2091054521,-43774102,1956117671,-1079985741,529529502,-855371243],[-0.1406981199979782,0.1364859640598297]],[604,[581936675,583427210,717332896,1614603900,1779402659,71308546,930838418,1172302558],[-0.2065264880657196,0.09351257979869843]],[541,[404419349,932903717,1877421366,755818324,743838869,-1145519202,2007639040,-1363402412],[-0.17568394541740417,0.10853296518325806]],[603,[-67115261,1783467842,62649872,-177358022,1674508167,1627845012,2071982800,-1061706326],[-0.14163950085639954,0.1350509077310562]],[464,[-725234227,-65396777,-1603801204,-2002121734,-574092835,-559480843,-664465910,-928072645],[-0.11832091212272644,0.16329441964626312]],[9,[1740090031,-119056410,-48862274,-36986092,1905130415,-458821676,-1686142136,-789056533],[-0.09134785085916519,0.20984113216400146]],[389,[1163198464,-642500693,2032893884,-68817014,486915972,-364436958,2111264989,1551847259],[-0.32341209053993225,0.05920873209834099]],[454,[-89996128,2143001535,-319868938,-303249964,-941319771,-17356811,-1376506227,12609509],[-0.16184104979038239,0.12333205342292786]],[407,[224468919,20399931,275107615,218193753,359924797,357845277,1715740495,67410948],[-0.20427259802818298,0.09396296739578247]],[116,[2093999747,2076877547,-1536818480,-68260112,342087583,977675846,-2054892578,-143178538],[-0.11508999764919281,0.16942168772220612]],[262,[61902593,-1083933765,-53051661,-19728780,-758080769,-274773161,-2065401704,-2140078764],[-0.13256002962589264,0.14487990736961365]],[532,[-357248640,-1176763950,-1380155585,-537027259,844380056,805091765,787808170,-287169169],[-0.1147993728518486,0.1661256104707718]],[34,[1897919481,934478619,1607954095,1062273795,-568662347,2092388823,-471626574,1140325367],[-0.13866116106510162,0.13873730599880219]],[372,[-1037057913,-1657025808,46547560,273413291,1868532619,1378320161,-743476278,-347081181],[-0.11563215404748917,0.16552960872650146]],[546,[-1199314443,-408265787,-913588740,-1655009092,-1372935763,1873526476,-914524407,-979905444],[-0.1930251270532608,0.10073655843734741]],[392,[1034106418,-711186286,-1664267794,-171720173,606214007,-1128035247,564213287,1934636578],[-0.329284131526947,0.05941547453403473]],[126,[-687891445,-614004341,-433115782,2129635058,385995416,1494300484,1184757738,-237518882],[-0.11321404576301575,0.17364130914211273]],[558,[-50542464,-203846926,-1517043500,2113515223,-1091188084,-1864583803,-387006582,-880564821],[-0.10755964368581772,0.17839278280735016]],[475,[-357780685,-2045314380,429770709,-139071500,-1492218965,61726170,728218027,-339771255],[-0.10421722382307053,0.18218158185482025]],[556,[-302139391,1593041108,735992730,1353726386,-1293624936,-1196135168,-340731990,-1591537478],[-0.12337599694728851,0.15013375878334045]],[275,[1547535029,278984577,-1224785328,-1934989504,788137925,955118580,-1444508712,-1463819671],[-0.1586635708808899,0.11958929151296616]],[176,[-1439519837,-336661820,-1812236650,-712007984,-1960207458,-868173878,857715686,-973873982],[-0.12497423589229584,0.15143482387065887]],[359,[-761946313,1449128951,2079574803,1577003953,1515347100,-537167905,-67450178,-4500465],[-0.0817737728357315,0.2283615618944168]],[152,[-496441618,-750877463,-389118725,-37689332,1436594609,-578584890,-706221607,1893151375],[-0.13894660770893097,0.13676303625106812]],[504,[68506993,604858146,237580290,-2121958112,1638929,-1097225097,340092224,34511200],[-0.27272871136665344,0.0688985139131546]],[415,[-2147155497,1560733239,-303387241,-42356291,-598665995,-1162842499,-295060500,-1410399929],[-0.09935402125120163,0.19108591973781586]],[228,[-202202453,-590590115,1756643743,2109944543,871348042,-786157488,1542764527,-1693726949],[-0.09322153031826019,0.2062782347202301]],[460,[-1500254282,1435658607,582487508,484240674,2099271125,-46970395,1709452864,-467317454],[-0.15570642054080963,0.11965727806091309]],[46,[-919902696,-123093042,398152976,-135141176,1743172232,-1396378320,2083515430,1413872256],[-0.18301638960838318,0.10124789923429489]],[36,[-751855101,-145897929,2019538276,817985198,803892992,71726960,2134193492,1932970373],[-0.18066957592964172,0.099701888859272]],[20,[268485266,-899463709,586345979,2106381807,541077489,-84983361,1246258371,-1474337],[-0.30872756242752075,0.05945972725749016]],[322,[1213715019,-2076172757,615109711,232265406,1473517724,-1362446289,-1316954598,-1153808357],[-0.15449893474578857,0.12041906267404556]],[628,[-357568517,323871454,788404112,300650740,1538176507,-1983070516,-707831379,-1002326334],[-0.10190636664628983,0.18244536221027374]],[459,[1757579808,985458211,-1912341890,329521286,922752757,2094473459,132997598,-336317625],[-0.16428998112678528,0.1118251159787178]],[345,[-771571504,-9274104,969443530,77684746,-1178752308,1794067453,-1562083078,-604642545],[-0.1964097023010254,0.09406990557909012]],[284,[583589,-1811895121,-1462990311,-1594353,1953890287,-420466765,-104632354,-1678170365],[-0.10462117195129395,0.18044032156467438]]]]],"features":[[0,0,1,1],[0,0,5,7],[0,0,6,5],[0,0,6,6],[0,0,7,6],[0,1,1,1],[0,1,1,3],[0,1,8,6],[0,2,1,1],[0,2,1,2],[0,2,1,4],[0,2,2,1],[0,3,2,7],[0,4,1,1],[0,4,1,2],[0,5,1,1],[0,5,6,2],[0,5,8,6],[0,6,1,1],[0,6,6,5],[0,6,6,6],[0,7,1,1],[0,7,1,2],[0,7,3,2],[0,7,6,3],[0,8,1,1],[0,9,1,1],[0,9,1,2],[0,9,2,5],[0,9,4,3],[0,9,5,2],[0,11,4,2],[0,13,4,3],[0,14,5,3],[0,15,5,3],[0,15,8,3],[0,16,1,1],[0,16,3,2],[0,19,8,1],[1,0,1,1],[1,0,4,4],[1,0,5,6],[1,0,7,6],[1,1,1,1],[1,1,1,4],[1,2,1,1],[1,2,1,2],[1,3,1,1],[1,4,1,1],[1,5,1,1],[1,6,1,1],[1,6,1,2],[1,7,1,1],[1,7,3,2],[1,8,1,1],[1,9,1,1],[1,12,4,2],[1,14,7,2],[1,18,7,2],[2,0,1,1],[2,0,4,3],[2,0,5,2],[2,0,5,6],[2,0,6,4],[2,0,6,7],[2,0,7,6],[2,1,1,1],[2,2,1,1],[2,3,1,1],[2,3,1,2],[2,4,1,1],[2,4,2,4],[2,5,1,1],[2,5,1,2],[2,6,1,1],[2,7,1,1],[2,7,5,5],[2,8,2,3],[2,9,1,1],[2,9,1,2],[2,10,1,1],[2,11,1,2],[2,11,2,2],[2,12,4,3],[2,14,7,1],[2,15,5,3],[2,15,7,3],[3,0,1,4],[3,0,3,2],[3,0,4,4],[3,0,4,6],[3,0,6,5],[3,0,7,6],[3,1,1,1],[3,1,3,3],[3,1,6,5],[3,2,1,1],[3,2,2,3],[3,2,6,4],[3,3,1,1],[3,3,2,4],[3,3,2,5],[3,3,4,5],[3,3,6,7],[3,4,1,1],[3,4,2,3],[3,4,6,3],[3,5,1,1],[3,5,6,3],[3,6,1,1],[3,6,2,2],[3,6,6,3],[3,6,6,4],[3,7,1,1],[3,7,2,1],[3,7,2,2],[3,8,1,1],[3,8,2,2],[3,8,2,3],[3,9,1,1],[3,9,2,2],[3,10,2,2],[3,10,3,3],[3,10,6,1],[3,11,1,1],[3,11,2,2],[3,14,1,1],[3,14,2,1],[3,15,6,1],[3,15,7,3],[3,19,7,1],[4,0,1,1],[4,0,1,2],[4,0,1,3],[4,0,5,2],[4,0,5,3],[4,0,5,4],[4,1,1,1],[4,2,1,1],[4,2,2,3],[4,3,1,1],[4,3,2,3],[4,4,1,1],[4,4,1,4],[4,4,2,2],[4,4,5,3],[4,4,5,5],[4,5,1,1],[4,5,2,2],[4,6,1,1],[4,6,5,2],[4,7,1,1],[4,7,1,2],[4,7,2,3],[4,8,1,1],[4,8,2,1],[4,8,2,2],[4,9,1,1],[4,9,1,2],[4,9,2,1],[4,9,2,2],[4,9,3,4],[4,9,3,5],[4,10,1,1],[4,10,1,2],[4,10,3,3],[4,11,1,1],[4,11,3,3],[4,12,1,1],[4,13,1,1],[4,14,1,1],[4,14,2,1],[4,15,2,1],[4,15,5,1],[4,16,1,1],[4,18,3,2],[5,0,1,1],[5,0,3,1],[5,0,5,2],[5,0,5,4],[5,1,1,1],[5,1,1,6],[5,1,1,7],[5,2,1,1],[5,2,5,6],[5,3,1,5],[5,3,2,3],[5,3,3,4],[5,4,1,4],[5,4,4,3],[5,4,6,3],[5,5,2,2],[5,6,1,1],[5,6,1,3],[5,6,2,2],[5,7,1,1],[5,7,1,3],[5,7,2,2],[5,8,1,1],[5,8,2,2],[5,9,1,1],[5,9,1,2],[5,10,1,2],[5,10,2,3],[5,11,1,2],[5,12,1,1],[5,12,1,2],[5,13,1,1],[5,14,1,1],[5,15,2,1],[5,15,5,1],[5,16,3,1],[5,17,1,1],[5,18,1,1],[5,18,2,1],[6,0,1,7],[6,0,3,8],[6,0,4,7],[6,0,6,5],[6,0,6,6],[6,1,1,1],[6,1,1,6],[6,1,3,5],[6,1,6,6],[6,3,4,3],[6,4,2,3],[6,4,4,4],[6,4,4,5],[6,5,1,1],[6,5,1,4],[6,6,1,1],[6,6,1,2],[6,6,1,3],[6,6,2,2],[6,6,6,5],[6,7,1,1],[6,7,1,2],[6,7,2,2],[6,7,6,4],[6,8,1,1],[6,8,2,2],[6,8,6,3],[6,8,6,4],[6,10,1,1],[6,11,1,1],[6,11,1,2],[6,11,2,3],[6,11,2,4],[6,12,1,1],[6,12,1,2],[6,13,1,1],[6,13,1,2],[6,14,1,1],[6,14,1,2],[6,15,1,1],[6,15,4,1],[6,16,2,1],[6,16,4,1],[6,18,1,1],[7,0,1,1],[7,0,3,7],[7,0,3,8],[7,0,4,1],[7,0,5,5],[7,0,5,6],[7,1,1,1],[7,3,2,2],[7,5,3,5],[7,6,1,2],[7,7,1,1],[7,7,3,2],[7,7,3,4],[7,8,1,1],[7,8,3,4],[7,8,5,4],[7,10,1,1],[7,10,1,2],[7,10,3,3],[7,11,1,1],[7,11,1,2],[7,12,1,1],[7,12,1,2],[7,13,1,1],[7,13,1,2],[7,13,2,1],[7,13,2,2],[7,13,4,3],[7,14,1,1],[7,14,1,2],[7,14,2,1],[7,15,1,1],[7,15,2,1],[7,16,1,1],[7,16,2,1],[7,16,3,1],[7,16,4,1],[7,18,1,1],[7,19,3,1],[8,0,2,3],[8,0,5,4],[8,0,5,6],[8,1,1,1],[8,1,3,7],[8,2,1,1],[8,4,1,1],[8,5,2,5],[8,6,1,1],[8,6,2,1],[8,6,2,5],[8,7,1,1],[8,8,1,1],[8,9,1,5],[8,10,1,1],[8,10,1,2],[8,11,1,2],[8,12,2,2],[8,13,1,1],[8,13,2,1],[8,13,3,2],[8,14,1,1],[8,14,2,1],[8,14,5,3],[8,15,1,1],[8,15,1,2],[8,15,2,1],[8,15,3,3],[8,15,5,3],[8,16,1,1],[8,16,2,1],[8,16,3,1],[8,17,1,1],[8,17,2,1],[8,19,1,1],[9,0,3,6],[9,0,4,3],[9,0,5,6],[9,1,4,6],[9,2,1,1],[9,3,1,1],[9,5,1,5],[9,6,1,1],[9,6,2,1],[9,7,1,1],[9,8,1,1],[9,8,2,1],[9,8,2,3],[9,9,1,1],[9,9,2,4],[9,10,2,3],[9,11,1,1],[9,11,2,3],[9,12,2,2],[9,13,1,1],[9,14,1,1],[9,14,2,1],[9,15,1,1],[9,15,1,2],[9,15,2,1],[9,15,5,3],[9,16,1,1],[9,16,2,1],[9,16,2,2],[9,17,1,1],[9,17,2,1],[9,18,2,1],[9,18,5,2],[9,19,2,1],[10,4,1,1],[10,4,3,2],[10,5,1,1],[10,5,1,5],[10,5,2,5],[10,6,1,1],[10,7,1,1],[10,7,3,1],[10,8,2,4],[10,9,1,1],[10,10,1,1],[10,10,1,2],[10,10,4,3],[10,11,1,1],[10,11,2,3],[10,15,1,1],[10,15,1,2],[10,15,2,1],[10,16,1,1],[10,16,2,1],[10,17,1,1],[10,17,2,1],[11,0,2,7],[11,0,3,3],[11,1,1,1],[11,1,3,4],[11,4,1,1],[11,4,2,2],[11,4,3,2],[11,5,1,1],[11,5,1,5],[11,5,2,2],[11,6,1,1],[11,7,1,1],[11,8,1,1],[11,8,2,1],[11,8,4,3],[11,9,1,1],[11,9,3,4],[11,10,1,1],[11,11,1,1],[11,11,1,2],[11,11,2,3],[11,11,3,3],[11,12,1,1],[11,12,2,2],[11,12,4,2],[11,13,1,1],[11,14,1,1],[11,14,2,1],[11,15,1,1],[11,15,2,1],[11,16,1,1],[11,16,2,1],[11,17,1,1],[11,17,2,1],[11,18,2,1],[12,0,1,1],[12,1,1,1],[12,3,2,3],[12,4,3,2],[12,5,1,1],[12,5,2,2],[12,6,1,1],[12,6,1,5],[12,7,1,1],[12,8,2,1],[12,9,4,3],[12,10,2,2],[12,11,1,1],[12,11,1,2],[12,11,2,3],[12,12,1,1],[12,12,3,2],[12,13,1,1],[12,14,1,1],[12,14,2,1],[12,14,4,3],[12,15,1,1],[12,16,1,1],[12,16,2,1],[12,17,1,1],[12,18,2,2],[13,0,1,1],[13,0,3,3],[13,2,1,1],[13,4,2,3],[13,5,2,2],[13,6,1,1],[13,6,2,2],[13,7,1,1],[13,7,2,2],[13,8,1,1],[13,10,1,1],[13,10,1,2],[13,10,2,3],[13,11,1,1],[13,11,1,2],[13,12,1,1],[13,12,3,2],[13,13,1,1],[13,14,1,1],[13,14,2,1],[13,15,1,1],[13,15,2,1],[13,16,1,1],[13,16,2,1],[13,17,1,1],[13,18,1,1],[13,18,2,1],[13,19,1,1],[14,0,1,1],[14,1,2,3],[14,2,1,1],[14,4,2,2],[14,4,2,3],[14,5,1,1],[14,5,1,2],[14,5,2,2],[14,6,2,1],[14,6,2,2],[14,7,1,1],[14,7,2,2],[14,7,2,3],[14,7,3,2],[14,8,1,1],[14,8,1,2],[14,8,2,1],[14,8,2,2],[14,9,1,1],[14,9,2,1],[14,9,2,2],[14,9,2,4],[14,11,1,1],[14,11,1,2],[14,12,1,1],[14,13,1,1],[14,14,1,1],[14,14,1,2],[14,14,2,1],[14,15,1,1],[14,16,1,1],[14,17,1,1],[14,18,1,1],[15,0,1,1],[15,0,1,6],[15,0,1,7],[15,1,1,1],[15,1,1,6],[15,2,1,1],[15,3,1,1],[15,3,1,5],[15,4,1,5],[15,4,2,3],[15,5,1,1],[15,5,2,3],[15,6,1,1],[15,6,1,2],[15,6,1,3],[15,6,2,2],[15,6,3,2],[15,7,1,1],[15,7,1,2],[15,7,2,2],[15,7,2,3],[15,8,1,1],[15,8,1,2],[15,8,1,5],[15,8,2,2],[15,8,2,3],[15,9,1,2],[15,9,2,2],[15,10,1,2],[15,10,2,2],[15,11,1,1],[15,11,2,2],[15,12,1,1],[15,12,1,2],[15,13,1,1],[15,13,1,2],[15,14,1,1],[15,15,1,1],[15,17,1,1],[15,18,1,1],[16,0,1,1],[16,2,1,1],[16,3,1,1],[16,3,1,2],[16,4,1,1],[16,5,1,1],[16,6,1,1],[16,7,1,1],[16,8,1,1],[16,8,1,2],[16,8,2,3],[16,9,1,1],[16,9,1,2],[16,10,1,2],[16,11,1,1],[16,11,1,2],[16,11,2,2],[16,12,1,1],[16,12,1,2],[16,13,1,1],[16,14,1,1],[16,15,1,1],[16,16,1,1],[16,17,1,1],[17,0,1,1],[17,0,1,3],[17,2,1,1],[17,3,1,1],[17,5,1,1],[17,6,1,1],[17,6,1,3],[17,7,1,1],[17,7,1,2],[17,7,1,3],[17,8,1,1],[17,10,1,1],[17,11,1,1],[17,12,1,1],[17,13,1,1],[17,14,1,1],[17,16,1,1],[18,0,1,1],[18,1,1,1],[18,2,1,1],[18,3,1,1],[18,4,1,1],[18,4,1,2],[18,5,1,1],[18,6,1,1],[18,7,1,1],[18,8,1,2],[18,9,1,1],[18,10,1,1],[18,16,1,1],[19,0,1,1],[19,1,1,2],[19,2,1,1],[19,3,1,1],[19,4,1,1],[19,5,1,1],[19,6,1,1],[19,8,1,1],[19,9,1,1],[19,10,1,1],[19,11,1,1],[19,12,1,1],[20,0,1,1],[20,1,1,1],[20,2,1,1],[20,3,1,1],[20,3,1,3],[20,4,1,1],[20,5,1,1],[20,7,1,1],[20,8,1,1],[20,8,1,2],[20,9,1,1],[20,11,1,2],[21,0,1,1],[21,0,1,4],[21,1,1,1],[21,2,1,1],[21,3,1,1],[21,3,1,2],[21,4,1,1],[21,5,1,1],[21,6,1,1],[21,7,1,1],[21,8,1,1],[21,9,1,1],[21,10,1,1]]} \ No newline at end of file diff --git a/app/public/animeface.js b/app/public/animeface.js new file mode 100644 index 0000000..d2fff29 --- /dev/null +++ b/app/public/animeface.js @@ -0,0 +1,97 @@ +// Detector de rostros de anime, en un Worker para no trabar el estante. +// Evalúa en JavaScript la cascada LBP «lbpcascade_animeface» de nagadomi: https://github.com/nagadomi/lbpcascade_animeface +// La cascada (animeface-cascade.json) es MIT, (C) 2011 nagadomi@nurs.or.jp; el aviso completo va en el propio archivo. +const integral = (gray, w, h) => { + const I = new Int32Array((w + 1) * (h + 1)) + for (let y = 0; y < h; y++) { + let row = 0 + for (let x = 0; x < w; x++) { + row += gray[y * w + x] + I[(y + 1) * (w + 1) + x + 1] = I[y * (w + 1) + x + 1] + row + } + } + return I +} +function equalize(gray) { + const hist = new Int32Array(256), n = gray.length + for (let i = 0; i < n; i++) hist[gray[i]]++ + let i = 0 + while (i < 255 && !hist[i]) i++ + const out = new Uint8Array(n), first = hist[i] + if (first === n) return out.fill(i) + const scale = 255 / (n - first), lut = new Uint8Array(256) + let sum = 0 + for (i++; i < 256; i++) { sum += hist[i]; lut[i] = Math.max(0, Math.min(255, Math.round(sum * scale))) } + for (let k = 0; k < n; k++) out[k] = lut[gray[k]] + return out +} +function shrink(gray, w, h, W, H) { // promedio por área, como INTER_LINEAR al reducir poco + const out = new Uint8Array(W * H), fx = w / W, fy = h / H + for (let y = 0; y < H; y++) { + const sy = (y + 0.5) * fy - 0.5, y0 = Math.max(0, Math.floor(sy)), y1 = Math.min(h - 1, y0 + 1), ty = sy - Math.floor(sy) + for (let x = 0; x < W; x++) { + const sx = (x + 0.5) * fx - 0.5, x0 = Math.max(0, Math.floor(sx)), x1 = Math.min(w - 1, x0 + 1), tx = sx - Math.floor(sx) + const a = gray[y0 * w + x0] * (1 - tx) + gray[y0 * w + x1] * tx, b = gray[y1 * w + x0] * (1 - tx) + gray[y1 * w + x1] * tx + out[y * W + x] = a * (1 - ty) + b * ty + 0.5 + } + } + return out +} +function scan(cascade, gray, W, H, step, emit) { + const I = integral(gray, W, H), S = W + 1, { stages, features, w: cw, h: ch } = cascade + for (let wy = 0; wy + ch <= H; wy += step) { + for (let wx = 0; wx + cw <= W; wx += step) { + let pass = true + for (let s = 0; s < stages.length && pass; s++) { + const [limit, weak] = stages[s] + let sum = 0 + for (let q = 0; q < weak.length; q++) { + const [f, subset, leaf] = weak[q], r = features[f], fx = wx + r[0], fy = wy + r[1], bw = r[2], bh = r[3] + const a = fy * S + fx, y1 = a + bh * S, y2 = a + 2 * bh * S, y3 = a + 3 * bh * S + const p0 = I[a], p1 = I[a + bw], p2 = I[a + 2 * bw], p3 = I[a + 3 * bw] + const p4 = I[y1], p5 = I[y1 + bw], p6 = I[y1 + 2 * bw], p7 = I[y1 + 3 * bw] + const p8 = I[y2], p9 = I[y2 + bw], p10 = I[y2 + 2 * bw], p11 = I[y2 + 3 * bw] + const p12 = I[y3], p13 = I[y3 + bw], p14 = I[y3 + 2 * bw], p15 = I[y3 + 3 * bw] + const c = p10 - p9 - p6 + p5 + const code = ((p5 - p4 - p1 + p0 >= c) << 7) | ((p6 - p5 - p2 + p1 >= c) << 6) | ((p7 - p6 - p3 + p2 >= c) << 5) | ((p11 - p10 - p7 + p6 >= c) << 4) | ((p15 - p14 - p11 + p10 >= c) << 3) | ((p14 - p13 - p10 + p9 >= c) << 2) | ((p13 - p12 - p9 + p8 >= c) << 1) | (p9 - p8 - p5 + p4 >= c) + sum += leaf[subset[code >> 5] & (1 << (code & 31)) ? 0 : 1] + } + if (sum < limit) pass = false + } + if (pass) emit(wx, wy) + } + } +} +// Agrupa ventanas parecidas, como groupRectangles de OpenCV. +function group(rects, minNeighbors, eps = 0.2) { + const parent = rects.map((_, i) => i), find = i => (parent[i] === i ? i : (parent[i] = find(parent[i]))) + const close = (a, b) => { const d = eps * 0.5 * (Math.min(a.w, b.w) + Math.min(a.h, b.h)); return Math.abs(a.x - b.x) <= d && Math.abs(a.y - b.y) <= d && Math.abs(a.x + a.w - b.x - b.w) <= d && Math.abs(a.y + a.h - b.y - b.h) <= d } + for (let i = 0; i < rects.length; i++) for (let j = 0; j < i; j++) if (close(rects[i], rects[j])) parent[find(i)] = find(j) + const by = new Map() + rects.forEach((r, i) => { const k = find(i); const g = by.get(k) || { x: 0, y: 0, w: 0, h: 0, n: 0 }; g.x += r.x; g.y += r.y; g.w += r.w; g.h += r.h; g.n++; by.set(k, g) }) + const out = [...by.values()].filter(g => g.n > minNeighbors).map(g => ({ x: Math.round(g.x / g.n), y: Math.round(g.y / g.n), w: Math.round(g.w / g.n), h: Math.round(g.h / g.n), n: g.n })) + // Quita los que quedan dentro de otro mayor con más apoyos. + return out.filter(a => !out.some(b => b !== a && b.n >= a.n && a.x >= b.x - b.w * 0.2 && a.y >= b.y - b.h * 0.2 && a.x + a.w <= b.x + b.w * 1.2 && a.y + a.h <= b.y + b.h * 1.2 && b.w > a.w)) +} +/** data: RGBA. Devuelve los rostros en píxeles de la imagen, con `n` = ventanas que los apoyan (más es más fiable). */ +function detectFaces(cascade, data, w, h, { scale = 1.1, minNeighbors = 5, minSize = 24 } = {}) { + const raw = new Uint8Array(w * h) + for (let i = 0; i < raw.length; i++) raw[i] = data[i * 4] * 0.299 + data[i * 4 + 1] * 0.587 + data[i * 4 + 2] * 0.114 + 0.5 + const gray = equalize(raw), rects = [] + for (let f = 1; ; f *= scale) { + const size = Math.round(cascade.w * f) + const W = Math.round(w / f), H = Math.round(h / f) + if (W < cascade.w || H < cascade.h) break + if (size < minSize) continue + scan(cascade, f === 1 ? gray : shrink(gray, w, h, W, H), W, H, f > 2 ? 1 : 2, (x, y) => rects.push({ x: Math.round(x * f), y: Math.round(y * f), w: size, h: size })) + } + return group(rects, minNeighbors) +} + +let cascade = null +self.onmessage = async ({ data: { id, pixels, w, h } }) => { + try { + cascade ||= await fetch(`/animeface-cascade.json${self.location.search}`).then(r => { if (!r.ok) throw new Error(r.status); return r.json() }) + self.postMessage({ id, faces: detectFaces(cascade, new Uint8ClampedArray(pixels), w, h, { minNeighbors: 3 }) }) + } catch (e) { self.postMessage({ id, faces: null }) } +} diff --git a/app/public/app.js b/app/public/app.js index 44e5cad..93cf720 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -364,6 +364,7 @@ window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); instal // ---------- Router ---------- let prevHash = '', curHash = '' // para volver atrás sin pasar por el lector +let shelfTrip = null // { mangaId } mientras se lee algo abierto desde el estante: libro 3D y vuelta al estante async function router() { if (location.hash !== curHash) { prevHash = curHash; curHash = location.hash } if (cleanup) { cleanup(); cleanup = null } @@ -371,6 +372,7 @@ async function router() { const [pathPart, query] = location.hash.slice(1).split('?') const parts = (pathPart || '/').split('/').filter(Boolean) const params = new URLSearchParams(query || '') + if (parts[0] !== 'read' && parts[0] !== 'shelf') shelfTrip = null document.querySelectorAll('#topbar nav a').forEach(a => a.classList.toggle('active', a.getAttribute('href') === `#/${parts[0] ?? ''}`)) if (!me) { @@ -386,6 +388,7 @@ async function router() { if (parts[0] === 'read') return await viewReader(parts[1], parts[2], Number(params.get('page') || 0) - 1) if (parts[0] === 'history') return (location.hash = '#/library') // antiguo nombre if (parts[0] === 'library') return await viewLibrary(params) + if (parts[0] === 'shelf') return await viewShelf() if (parts[0] === 'downloads') return (location.hash = '#/queues') // antiguo nombre if (parts[0] === 'queues') return await viewQueues() if (parts[0] === 'stats') return await viewStats() @@ -2102,7 +2105,7 @@ async function viewReader(mangaId, chapterId, startPage) { app.innerHTML = `
- ${icon('back')} + ${icon('back')}
${esc(manga.title)}
@@ -3067,7 +3070,8 @@ async function viewReader(mangaId, chapterId, startPage) { } // --- Libro 3D: otro módulo dibuja el libro; el lector sigue llevando la página, el progreso y los gestos. - const b3dWanted = () => B3D_OK && !!settings.book3d && isPaged() && settings.fit !== 'width' + const fromShelf = shelfTrip?.mangaId === mangaId // desde el estante se lee en 3D aunque el ajuste diga otra cosa (sin guardarlo) + const b3dWanted = () => B3D_OK && (!!settings.book3d || fromShelf) && isPaged() && settings.fit !== 'width' const b3dOn = () => b3dWanted() && !atEnd const b3dPaper = () => { const hex = book ? (BOOK_PAPERS[bookSettings().paper] || BOOK_PAPERS.light).bg : '#efeadf' @@ -3817,6 +3821,120 @@ const libCard = ({ href, cover, title, sub, pct, badge, del }) => `${esc(title)}
${sub ? `
${sub}
` : ''}
` +// ---------- Estante 3D ---------- +const SHELF_OK = 'WebGL2RenderingContext' in window // también con ratón o teclado: a diferencia del lector, no depende de los gestos táctiles +// Foto del último fotograma del estante, encima de todo mientras el lector se monta; se desvanece cuando el libro 3D ya está dibujado. +function handoffCover(src) { + document.querySelector('.handoff')?.remove() + const img = Object.assign(document.createElement('img'), { className: 'handoff', src, alt: '' }) + document.body.append(img) + const t0 = performance.now() + const fade = () => { img.classList.add('out'); setTimeout(() => img.remove(), 450) } + const wait = () => { + if (!img.isConnected) return + const ready = document.querySelector('#b3d:not([hidden]) canvas') + if (ready) return setTimeout(fade, 350) // las texturas de la tapa tardan un instante + if (performance.now() - t0 > 2500 || !location.hash.startsWith('#/read/')) return fade() + setTimeout(wait, 60) + } + wait() +} +async function viewShelf() { + if (!SHELF_OK) { + app.innerHTML = `

Estante 3D

Este navegador no tiene WebGL2.

${icon('library')} Volver a la librería

` + return + } + document.body.classList.add('reading') // sin la barra de arriba + app.innerHTML = `
${icon('back')} Librería

Colocando los tomos…

+ ` + const note = $('#shelfnote'), card = $('#shelfpeek') + const focusOn = shelfTrip?.mangaId || null // al volver del lector, con ese tomo a la vista + shelfTrip = null + let shelf = null, gone = false + const onKey = e => { if (e.key === 'Escape') shelf?.unpeek() } + document.addEventListener('keydown', onKey) + cleanup = () => { gone = true; document.removeEventListener('keydown', onKey); shelf?.destroy(); shelf = null } + // Vistazo: lo que se sabe del tomo sacado, junto a él. + const readOf = info => { + const { work, vol, state } = info + if (state === 'reading' && work.chapterId) return readUrl(work.id, work.chapterId, work.page || 0) + if (vol.chapters?.length) return readUrl(work.id, vol.chapters[0]) + return work.chapterId ? readUrl(work.id, work.chapterId, work.page || 0) : `#/manga/${work.id}` + } + // A la mesa: el estante posa el tomo como el lector lo mostrará y una foto del último fotograma tapa el cambio. + let leaving = false + const toTable = info => { + if (leaving) return + const href = readOf(info) + const go = snap => { + if (gone) return + if (href.startsWith('#/read/')) shelfTrip = { mangaId: info.work.id } + if (snap) handoffCover(snap) + location.hash = href + } + leaving = true + if (!href.startsWith('#/read/') || !shelf?.fly(go)) go(null) + } + const showPeek = info => { + if (!info) { card.classList.remove('on'); clearTimeout(card.hideTimer); card.hideTimer = setTimeout(() => { if (!card.classList.contains('on')) card.hidden = true }, 250); return } + const { work, vol, state } = info, n = vol.chapters?.length || 0, at = n && work.chapterId ? vol.chapters.indexOf(work.chapterId) : -1 + const facts = [vol.vol ? `Tomo ${esc(vol.vol)}` : '', n ? `${n} ${n === 1 ? 'capítulo' : 'capítulos'}` : '', vol.pages ? `${vol.pages} páginas` : ''].filter(Boolean).join(' · ') + const pct = state === 'read' ? 100 : at >= 0 ? Math.round((at / n) * 100) : 0 + const where = state === 'read' ? 'Leído' : state === 'reading' ? (at >= 0 ? `Vas por el capítulo ${at + 1} de ${n} de este tomo` : 'Lo estás leyendo') : 'Sin empezar' + card.innerHTML = ` +

${esc(work.title)}

${facts ? `

${facts}

` : ''} +

${where}

+
${icon('book')} ${state === 'reading' ? 'Seguir leyendo' : 'Leer'}Ver ficha
` + card.querySelector('.peekx').addEventListener('click', () => shelf?.unpeek()) + card.querySelector('#peekread').addEventListener('click', e => { e.preventDefault(); toTable(info) }) + clearTimeout(card.hideTimer) + card.hidden = false + requestAnimationFrame(() => card.classList.add('on')) + } + const ver = name => document.querySelector(`meta[name=${name}-version]`)?.content || '' + try { + const [{ works }, lib, mod] = await Promise.all([api('/api/shelf'), import(`/book3d.js?v=${ver('b3d')}`), import(`/shelf3d.js?v=${ver('shelf')}`)]) + if (gone) return + if (!works.length) { note.textContent = 'Aún no hay nada en tu biblioteca: los tomos de lo que leas, sigas o descargues aparecerán aquí.'; return } + shelf = mod.createShelf3D($('#shelf3d'), lib, { + works, + still: matchMedia('(prefers-reduced-motion: reduce)').matches, + onPeek: showPeek, + onOpen: info => toTable(info), // tocar el tomo sacado lo lleva a la mesa + table: me?.tableV ? `/api/table?v=${me.tableV}` : null, + // Un tomo sin portada saca el arte del lomo de sus primeras páginas: la 0 suele ser de créditos. + pagesOf: chapterId => api(`/api/chapter/${chapterId}?saver=0&hd=0`).then(d => (d?.pages || []).slice(1, 6), () => []), + }) + if (!shelf) throw new Error('sin WebGL2') + if (focusOn) shelf.focus(focusOn) + // Los tomos llegan aparte, de pocos en pocos: la primera vez pueden tardar por las fuentes externas. + let next = 0, pending = works.length + const worker = async () => { + while (next < works.length && !gone) { + const w = works[next++] + const r = await api(`/api/volumes/${w.id}?lang=${encodeURIComponent(w.lang || 'es')}`).catch(() => null) + if (gone) return + if (r?.volumes?.length) shelf.setVolumes(w.id, r.volumes) + if (--pending === 0) note.hidden = true + } + } + for (let k = 0; k < 3; k++) worker() + // Logos para el título de los lomos: la primera vez el servidor los busca en Wikimedia Commons, sin prisa. + let nextLogo = 0 + const logoWorker = async () => { + while (nextLogo < works.length && !gone) { + const w = works[nextLogo++] + const r = await api(`/api/logo/${w.id}?lang=${encodeURIComponent(w.lang || 'es')}`).catch(() => null) + if (gone) return + if (r?.logo) shelf.setLogo(w.id, r.logo) + } + } + for (let k = 0; k < 2; k++) logoWorker() + } catch (e) { + if (!gone) note.textContent = `No se pudo abrir el estante: ${e.message}` + } +} + async function viewLibrary(params) { const tab = params.get('tab') || 'reading' app.innerHTML = '
Cargando…
' @@ -3837,7 +3955,7 @@ async function viewLibrary(params) { ...lists.map(l => [`list-${l.id}`, l.name, 'bookmark', contentMode() === 'both' ? l.count : 0]), // el total de la lista incluye lo filtrado ] const curList = tab.startsWith('list-') ? lists.find(l => `list-${l.id}` === tab) : null - app.innerHTML = `

Librería

+ app.innerHTML = `

Librería

${SHELF_OK ? `${icon('library')} Estante 3D` : ''}
${tabs.map(([k, label, ic, n]) => ``).join('')} ${online ? `` : ''}
${curList ? `
Lista propia · añade mangas desde su ficha con ${icon('bookmark')} Listas @@ -4046,7 +4164,7 @@ async function viewQueues() {

${icon('book')} Biblioteca descargada

-

CBZ en /zdata/media/mangas (compatibles con Kavita, Komga, Mihon…); se leen desde el disco.${list.length ? ` Total: ${fmtSize(total)}.` : ''}

+

CBZ en ./mangas (compatibles con Kavita, Komga, Mihon…); se leen desde el disco.${list.length ? ` Total: ${fmtSize(total)}.` : ''}

${library.length ? `
${library.map(d => qcard({ key: `lib:${d.manga_id}`, id: d.manga_id, cover: d.cover, title: d.title, pct: d.total ? Math.min(100, (d.done / d.total) * 100) : null, @@ -4269,7 +4387,7 @@ function viewSettings() { Si un manga tiene copia local, los capítulos nuevos se descargan automáticamente. Además puedes recibir una notificación:

ntfy - + Telegram (opcional) diff --git a/app/public/book3d.js b/app/public/book3d.js index f7cc9d4..3c314bb 100644 --- a/app/public/book3d.js +++ b/app/public/book3d.js @@ -115,9 +115,9 @@ export function fitCamera(points, aspect, marginX, marginY) { // Colores de la portada para la contraportada y sus botones. const luma = c => { const f = v => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4); return 0.2126 * f(c[0]) + 0.7152 * f(c[1]) + 0.0722 * f(c[2]) } const contrast = (a, b) => { const x = luma(a), y = luma(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05) } -const inkOn = c => (contrast(c, [1, 1, 1]) >= contrast(c, [0.07, 0.06, 0.05]) ? [1, 1, 1] : [0.07, 0.06, 0.05]) +export const inkOn = c => (contrast(c, [1, 1, 1]) >= contrast(c, [0.07, 0.06, 0.05]) ? [1, 1, 1] : [0.07, 0.06, 0.05]) const mixColor = (a, b, t) => a.map((n, k) => n + (b[k] - n) * t) -const css = c => `rgb(${c.map(n => Math.round(n * 255)).join(' ')})` +export const css = c => `rgb(${c.map(n => Math.round(n * 255)).join(' ')})` // Color dominante y color de acento: el acento es el que más destaca sobre el dominante, sin ser una mota de la imagen. export function coverPalette(data) { const boxes = new Map() @@ -143,7 +143,7 @@ export function coverPalette(data) { } // Madera generada: se repite sin que se note el corte. -function woodCanvas() { +export function woodCanvas() { const S = 1024, PLANKS = 5, canvas = Object.assign(document.createElement('canvas'), { width: S, height: S }), g = canvas.getContext('2d') let seed = 7 const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647 @@ -243,7 +243,7 @@ void main() { }` // Junta rejillas en una malla: pos(3) normal(3) uv(2). -class Shape { +export class Shape { constructor() { this.v = []; this.i = [] } grid(cols, rows, at) { const base = this.v.length / 8 @@ -305,8 +305,9 @@ function rimShape(pts, y0, y1, thick) { // El sensor: los ejes de la pantalla a partir de la orientación del dispositivo. La inclinación se desvanece con el tiempo. // MAX_DEG: lo que hay que girar el dispositivo para llegar al tope. ORBIT_*: cuánto rodea entonces la cámara al libro, en grados. -const MAX_DEG = 15, FADE_S = 2.5, QUIET = 0.6, ORBIT_TILT = 7, ORBIT_YAW = 10, ORBIT_BACK = 0.06 -function createTilt(onChange) { +export const MAX_DEG = 15 +const FADE_S = 2.5, QUIET = 0.6, ORBIT_TILT = 7, ORBIT_YAW = 10, ORBIT_BACK = 0.06 +export function createTilt(onChange) { const target = { x: 0, y: 0 } let listening = false, gotOrientation = false, lastT = 0, lastOri = null, lastFade = 0, askOnTouch = null const clamp = v => Math.max(-MAX_DEG, Math.min(MAX_DEG, v)) diff --git a/app/public/fonts/LICENSE.txt b/app/public/fonts/LICENSE.txt new file mode 100644 index 0000000..ced165d --- /dev/null +++ b/app/public/fonts/LICENSE.txt @@ -0,0 +1,3 @@ +Fuentes manuscritas de las notas del estante, de Google Fonts (solo el subconjunto latino): +Caveat, Patrick Hand, Kalam, Gochi Hand, Indie Flower y Shadows Into Light: SIL Open Font License 1.1 (https://openfontlicense.org). +Permanent Marker: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0). diff --git a/app/public/fonts/caveat.woff2 b/app/public/fonts/caveat.woff2 new file mode 100644 index 0000000..36e42dc Binary files /dev/null and b/app/public/fonts/caveat.woff2 differ diff --git a/app/public/fonts/gochihand.woff2 b/app/public/fonts/gochihand.woff2 new file mode 100644 index 0000000..b929dad Binary files /dev/null and b/app/public/fonts/gochihand.woff2 differ diff --git a/app/public/fonts/indieflower.woff2 b/app/public/fonts/indieflower.woff2 new file mode 100644 index 0000000..f28fef8 Binary files /dev/null and b/app/public/fonts/indieflower.woff2 differ diff --git a/app/public/fonts/kalam.woff2 b/app/public/fonts/kalam.woff2 new file mode 100644 index 0000000..0a6bd55 Binary files /dev/null and b/app/public/fonts/kalam.woff2 differ diff --git a/app/public/fonts/patrickhand.woff2 b/app/public/fonts/patrickhand.woff2 new file mode 100644 index 0000000..e80b54b Binary files /dev/null and b/app/public/fonts/patrickhand.woff2 differ diff --git a/app/public/fonts/permanentmarker.woff2 b/app/public/fonts/permanentmarker.woff2 new file mode 100644 index 0000000..203ccae Binary files /dev/null and b/app/public/fonts/permanentmarker.woff2 differ diff --git a/app/public/fonts/shadows.woff2 b/app/public/fonts/shadows.woff2 new file mode 100644 index 0000000..f15b203 Binary files /dev/null and b/app/public/fonts/shadows.woff2 differ diff --git a/app/public/index.html b/app/public/index.html index 4237778..7ea0308 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -4,6 +4,7 @@ + Manga Reader diff --git a/app/public/shelf3d.js b/app/public/shelf3d.js new file mode 100644 index 0000000..9fb55db --- /dev/null +++ b/app/public/shelf3d.js @@ -0,0 +1,1285 @@ +// Estante 3D: un mueble de madera con los tomos de la biblioteca. Mira hacia +z, con y hacia arriba; se reciben las piezas comunes de book3d.js. +export const SHELF = { PITCH: 1.2, BOOK_H: 1, DEPTH: 0.82, FRONT: 0.74, FACE_W: 0.7, FACE_Z: 0.58, FACE_T: 0.09, GAP: 0.07, BOARD: 0.055, SIDE: 0.075 } + +// ----- Arte del lomo ----- +const hslOf = (r, g, b) => { + const max = Math.max(r, g, b), min = Math.min(r, g, b) + return { sat: max ? (max - min) / max : 0, val: max / 255, chroma: (max - min) / 255 } +} +/** Lo a color que es una imagen: la parte de sus píxeles con color de verdad. El blanco, el negro y los grises no cuentan. */ +export function colorfulness(data) { + let colored = 0, n = 0 + for (let p = 0; p < data.length; p += 4) { + n++ + if (hslOf(data[p], data[p + 1], data[p + 2]).chroma > 0.16) colored++ + } + return n ? colored / n : 0 +} +export const COLOR_PAGE = 0.25 +/** La columna más vistosa de una imagen, para el lomo: mucho color y detalle, sin los bordes, donde suele haber marcos. Devuelve su x izquierda. */ +export function bestColumn(data, w, h, colW, from = 0) { + const cols = new Float64Array(w), detail = new Float64Array(w) + for (let x = 0; x < w; x++) { + let last = 0 + for (let y = from; y < h; y++) { + const p = (y * w + x) * 4, { chroma } = hslOf(data[p], data[p + 1], data[p + 2]), lum = (data[p] * 0.3 + data[p + 1] * 0.59 + data[p + 2] * 0.11) / 255 + cols[x] += chroma + if (y > from) detail[x] += Math.min(0.25, Math.abs(lum - last)) + last = lum + } + } + const lo = Math.round(w * 0.06), hi = Math.max(lo, Math.round(w * 0.94) - colW) + let best = lo, bestScore = -1 + for (let x = lo; x <= hi; x++) { + let score = 0 + for (let k = x; k < x + colW && k < w; k++) score += cols[k] + detail[k] * 0.8 + if (score > bestScore) { bestScore = score; best = x } + } + return best +} + +const FACE_LOGO_Y = 0.24, FACE_LOGO_W = 0.26 +const FACE_MIN = 0.14 // Los rostros más pequeños, respecto al ancho de la portada, casi siempre son errores o figuras lejanas. + +/** El rostro para el lomo: el mayor entre los de tamaño suficiente. Devuelve [x, y, ancho, alto] en fracciones de la imagen, o 0. */ +export function chooseFace(faces, w, h) { + // Pequeño y en lo alto de la portada suele ser el logo: la calavera de One Piece tiene cara de rostro. + const big = (faces || []).filter(f => f.w >= w * FACE_MIN && !(f.y + f.h / 2 < h * FACE_LOGO_Y && f.w < w * FACE_LOGO_W)) + if (!big.length) return 0 + const f = big.reduce((a, b) => (b.w > a.w ? b : a)) + return [f.x / w, f.y / h, f.w / w, f.h / h].map(v => Math.round(v * 1000) / 1000) +} +/** La franja alrededor de un rostro, como en los lomos de verdad: algo de pelo arriba y la cara debajo. Todo en píxeles de la imagen. */ +export function faceStrip(face, w, h) { + const fx = face[0] * w, fy = face[1] * h, fw = face[2] * w, fh = face[3] * h + const visible = 1 / ART_RATIO, sw = Math.min(fw * 1.3, h / visible, w) // visible: alto que se ve, en anchos de franja + return { + sx: Math.max(0, Math.min(w - sw, fx + fw / 2 - sw / 2)), + sy: Math.max(0, Math.min(h - sw * visible, fy - fh * 0.4)), + sw, + } +} + +// Medidas de un tomo, en altos de un B6 (18,2 cm). El grosor sale de las páginas, como en papel: unos 0,07 mm por página más las tapas. +// Sin páginas contadas se estiman por capítulos. Con tope, para que un tomo enorme no parezca un ladrillo. +export const SPINE_MIN = 0.06, SPINE_MAX = 0.1 +export const spineWidth = (pages, chapters) => { + const p = pages > 0 ? pages : chapters ? chapters * 19 : 180 + return Math.max(SPINE_MIN, Math.min(SPINE_MAX, (2 + p * 0.07) / 182)) +} +// Alto y fondo de una colección, según la proporción de sus portadas: la de bolsillo de las revistas shōnen (11,2 × 17,6 cm) +// es más estrecha que la B6 de las seinen (12,8 × 18,2 cm). Todos los tomos de una obra miden lo mismo; solo cambia el grosor. +export function bookSize(aspect) { + const a = Math.max(0.6, Math.min(0.76, aspect || 0.703)), mm = a < 0.655 ? 176 : 182 + return { h: mm / 182, d: (SHELF.FACE_W * mm * a) / 128 } +} + +/** El tomo que se muestra de frente: por el que se va o, si no se ha empezado, el primero. */ +export function faceIndex(vols, chapterId) { + const i = vols.findIndex(v => (v.chapters || []).includes(chapterId)) + return i < 0 ? 0 : i +} + +/** Reparte las obras en filas de un ancho dado. Una obra sin tomos conocidos es un solo libro, de frente. */ +export function layoutShelf(works, rowWidth, sizeOf = () => bookSize()) { + const rows = [[]] + let x = 0 + const place = (item, w) => { + if (x + w > rowWidth && rows.at(-1).length) { rows.push([]); x = 0 } + rows.at(-1).push({ ...item, x, w }) + x += w + } + for (const work of works) { + if (rows.at(-1).length) x += SHELF.GAP + const vols = work.vols?.length ? work.vols : [{ vol: null, chapters: [], thumb: work.cover }] + const face = faceIndex(vols, work.chapterId), { h, d } = sizeOf(work) + vols.forEach((vol, i) => { + const isFace = i === face, t = spineWidth(vol.pages, vol.chapters?.length) + place({ work, vol, face: isFace, h, d, t }, isFace ? d : t) + }) + } + return rows.filter(r => r.length) +} + +const VS = `#version 300 es +uniform mat4 uM, uModel; // uModel: solo el tomo que se saca; lo demás ya está en el mueble +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aNor; +layout(location = 2) in vec2 aUV; +out vec3 vPos; out vec3 vNor; out vec2 vUV; +void main() { vec4 p = uModel * vec4(aPos, 1.0); vPos = p.xyz; vNor = mat3(uModel) * aNor; vUV = aUV; gl_Position = uM * p; }` +const FS = `#version 300 es +precision highp float; +uniform int uKind; +uniform sampler2D uTex; +uniform vec3 uColor; +uniform float uHas, uPitch, uDepth, uDim, uAlpha; +uniform vec4 uSpot; // mesa: centro del tomo y radio de la luz +uniform vec2 uTab; // mesa propia: escala de la foto; sin ella, madera repetida +uniform vec2 uSize; +uniform vec4 uHide; // x0, x1, y0, y1 del hueco del tomo sacado: ni él ni sus costados se pintan en la fila +in vec3 vPos; in vec3 vNor; in vec2 vUV; +out vec4 color; +const vec3 L = normalize(vec3(-0.22, 0.42, 0.88)); +void main() { + if (uKind != 0 && uHide.y > uHide.x && vPos.y > uHide.z && vPos.y < uHide.w) { + const float e = 0.0002; + bool inside = vPos.x > uHide.x + e && vPos.x < uHide.y - e; + bool own = (abs(vPos.x - uHide.x) <= e && vNor.x < -0.5) || (abs(vPos.x - uHide.y) <= e && vNor.x > 0.5); + if (inside || own) discard; // los costados de los vecinos sí quedan a la vista + } + vec3 c; + if (uKind == 0) c = texture(uTex, vUV, -0.4).rgb * vec3(0.92, 0.84, 0.76); // madera del mueble, algo más oscura que la mesa + else if (uKind == 1) c = texture(uTex, vUV).rgb; // lomos + else if (uKind == 2) c = uHas > 0.5 ? texture(uTex, vUV, -0.5).rgb : uColor; // portada + else if (uKind == 3 || uKind == 5) { // las hojas, vistas por arriba: rayas irregulares a lo ancho del tomo, medidas en el mueble + float a = uKind == 3 ? vUV.x : vUV.y, v = a / 0.0042, w = fwidth(v), k = floor(v); + float n = fract(sin(k * 12.9898) * 43758.5453); + float line = 1.0 - smoothstep(0.08, 0.08 + max(w, 0.25), abs(fract(v) - 0.5)); + float dense = 1.0 - smoothstep(0.45, 0.9, w); // muy lejos, las rayas se funden en un tono + c = vec3(0.91, 0.88, 0.81) * (1.0 - (0.1 + 0.05 * n) * dense) * (1.0 - line * (0.16 + 0.22 * n) * dense); + // Entre un tomo y otro queda una rendija oscura. + if (uKind == 3) c *= mix(0.45, 1.0, smoothstep(0.0, 0.0035, min(vUV.x, vUV.y - vUV.x))); + } else if (uKind == 6) { // la mesa del lector, que aparece detrás del tomo + vec3 t = uHas > 0.5 ? texture(uTex, clamp(vUV * uTab + 0.5, 0.0, 1.0)).rgb : texture(uTex, vUV / 1.7).rgb; + float d = length(vPos - uSpot.xyz) / uSpot.w; + color = vec4(t * mix(1.04, 0.42, smoothstep(0.35, 1.6, d)), uAlpha); + return; + } else c = uColor; // tapas, de canto + float lit = mix(0.62, 1.05, clamp(dot(normalize(vNor), L), 0.0, 1.0)); + // Lo del fondo y lo que queda bajo la balda de arriba recibe menos luz. + float z = clamp(vPos.z / uDepth, 0.0, 1.0); + float under = fract(vPos.y / uPitch); + float shade = mix(0.5, 1.0, smoothstep(0.0, 0.95, z)) * mix(mix(0.5, 1.0, z), 1.0, smoothstep(0.62, 0.98, 1.0 - under)); + vec2 q = gl_FragCoord.xy / uSize - 0.5; + float room = 1.0 - 0.34 * smoothstep(0.25, 0.8, length(q * vec2(1.0, 0.8))); + color = vec4(c * lit * shade * room * uDim * vec3(1.03, 1.0, 0.95), uAlpha); +}` + +// Un cuadrilátero: origen, dos lados, normal y coordenadas de textura en sus esquinas. +function quad(sh, o, a, b, n, uv = [0, 0, 1, 1]) { + sh.grid(1, 1, (c, r) => [o[0] + a[0] * c + b[0] * r, o[1] + a[1] * c + b[1] * r, o[2] + a[2] * c + b[2] * r, ...n, uv[0] + (uv[2] - uv[0]) * c, uv[1] + (uv[3] - uv[1]) * r]) +} +const hash = s => { let h = 7; for (const ch of String(s)) h = (h * 31 + ch.charCodeAt(0)) | 0; return Math.abs(h) } +const hsl = (h, s, l) => { const a = s * Math.min(l, 1 - l), f = n => { const k = (n + h / 30) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)) }; return [f(0), f(8), f(4)] } + +const STORE = 'shelfPalette' +const STRIP_PER_UNIT = 440, STRIP_MAX = 2048, STRIP_H = 512, IMG_AT_ONCE = 4 // ancho de la tira según el de la fila; más de 2048 no se nota y gasta memoria +const blurBox = typeof document === 'undefined' ? null : document.createElement('canvas') // de paso, para desenfocar bajo las letras +const ART_SKIP = 0.26, FACE_INPUT = 360 // sin rostro se salta lo alto de la portada; ancho con que se buscan los rostros +const ART_RATIO = 0.14, ART_H = 512, ART_KEEP = 240 // franja guardada: algo más ancha que el lomo más grueso +// Notas adhesivas con el nombre de cada colección: fuentes manuscritas propias (fonts/LICENSE.txt) y colores de pósit. +const NOTE_FONTS = [['Caveat', 'caveat', 600, 1.15], ['Patrick Hand', 'patrickhand', 400, 1], ['Kalam', 'kalam', 700, 0.95], ['Gochi Hand', 'gochihand', 400, 1], ['Indie Flower', 'indieflower', 400, 0.95], ['Permanent Marker', 'permanentmarker', 400, 0.85], ['Shadows Into Light', 'shadows', 400, 1.05]] +const NOTE_COLORS = ['#fff27a', '#ffb8cf', '#a9dcff', '#c4f2a8', '#ffd38f', '#e3c8ff'] +const NOTE_INKS = ['#24242e', '#1b2b6e', '#3a1f5c', '#123c2c'] +const NOTE_H = 0.13, NOTE_PX = 128 +// El nombre de la nota: como mucho 40 letras, cortando entre palabras. +export function shortTitle(title, max = 40) { + const t = String(title || '').replace(/\s+/g, ' ').trim() + if (t.length <= max) return t + const cut = t.slice(0, max), sp = cut.lastIndexOf(' ') + return `${(sp > 0 ? cut.slice(0, sp) : cut).replace(/[\s,.;:·–—-]+$/, '')}…` +} +// Aspecto de la nota de una obra: el mismo en el mueble y en la ficha del tomo sacado. +export function noteStyle(id) { + const seed = hash(id), [family, , weight, scale] = NOTE_FONTS[seed % NOTE_FONTS.length] + return { seed, family: `shelf ${family}`, weight, scale, paper: NOTE_COLORS[(seed >> 3) % NOTE_COLORS.length], ink: NOTE_INKS[(seed >> 6) % NOTE_INKS.length], tilt: ((seed % 97) / 96 - 0.5) * 0.1 } +} +let notesFonts = null +export function loadNoteFonts() { + if (notesFonts || typeof FontFace === 'undefined') return notesFonts || Promise.resolve() + notesFonts = Promise.all(NOTE_FONTS.map(([name, file, weight]) => new FontFace(`shelf ${name}`, `url(/fonts/${file}.woff2)`, { weight: String(weight) }).load().then(f => { document.fonts.add(f) }, () => {}))) + return notesFonts +} +const IDENTITY = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) +const FLY_MS = 900 +const PEEK_OPEN_MS = 720, PEEK_CLOSE_MS = 520, PEEK_SLIDE = 0.34 // el tomo sale recto de la fila y luego gira hacia la cámara +const ZOOM_MAX = 3, ORBIT_TILT = 10, ORBIT_YAW = 18, ORBIT_BACK = 0.08, REST_TILT = -3, FRICTION_MS = 320 + +export function createShelf3D(container, lib, { works: firstWorks = [], rowsLandscape = 2.3, rowsPortrait = 3, still = false, pagesOf = null, onPeek = null, onOpen = null, table = null } = {}) { + const { Shape, woodCanvas, cameraMatrix, project, FOV, coverPalette, inkOn, css, createTilt, MAX_DEG } = lib + const canvas = document.createElement('canvas') + canvas.className = 'shelfcanvas' + const gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: true, powerPreference: 'high-performance' }) + if (!gl) return null + container.replaceChildren(canvas) + const maxTex = gl.getParameter(gl.MAX_TEXTURE_SIZE) || 2048 + + let dead = false, raf = 0, u = {}, prog = null, wood = null, furniture = null, works = firstWorks, aniso = null + let rows = [], rowWidth = 3, size = { w: 1, h: 1 }, visibleH = 2.8, top = 0, bottom = 0, y = 0, layoutTimer = 0, rowsKey = '' + const built = new Map(), covers = new Map(), loadingImg = new Map(), waiting = [] + const arts = new Map(), panels = new Map(), askedPanel = new Set() // arte de los lomos, y página a color de los tomos sin portada + let x = 0, zoom = 1, velocity = 0, lastMove = 0, focusId = null // focusId: obra que se mantiene a la vista hasta que se toque el mueble + const tiltNow = { x: 0, y: 0 } + const tilt = createTilt(() => draw()) + let imgBusy = 0 + let palettes = {} + try { palettes = JSON.parse(localStorage.getItem(STORE) || '{}') } catch {} + let saveTimer = 0 + const savePalettes = () => { + clearTimeout(saveTimer) + saveTimer = setTimeout(() => { try { localStorage.setItem(STORE, JSON.stringify(palettes)) } catch {} }, 800) + } + + function makeMesh(sh) { + const vao = gl.createVertexArray(), vb = gl.createBuffer(), ib = gl.createBuffer() + gl.bindVertexArray(vao) + gl.bindBuffer(gl.ARRAY_BUFFER, vb) + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(sh.v), gl.STATIC_DRAW) + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib) + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(sh.i), gl.STATIC_DRAW) + gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 32, 0) + gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 32, 12) + gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 32, 24) + gl.bindVertexArray(null) + return { vao, vb, ib, n: sh.i.length } + } + const freeMesh = m => { if (m) { gl.deleteVertexArray(m.vao); gl.deleteBuffer(m.vb); gl.deleteBuffer(m.ib) } } + const drawMesh = m => { gl.bindVertexArray(m.vao); gl.drawElements(gl.TRIANGLES, m.n, gl.UNSIGNED_SHORT, 0) } + function texFrom(source, tex = gl.createTexture()) { + gl.bindTexture(gl.TEXTURE_2D, tex) + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source) + gl.generateMipmap(gl.TEXTURE_2D) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + if (aniso) gl.texParameterf(gl.TEXTURE_2D, aniso.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(8, gl.getParameter(aniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT))) + return tex + } + + function initGL() { + const shader = (type, text) => { const s = gl.createShader(type); gl.shaderSource(s, text); gl.compileShader(s); return s } + prog = gl.createProgram() + gl.attachShader(prog, shader(gl.VERTEX_SHADER, VS)) + gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, FS)) + gl.linkProgram(prog) + if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog) || 'shader') + gl.useProgram(prog) + u = {} + for (const n of ['uM', 'uModel', 'uKind', 'uTex', 'uColor', 'uHas', 'uPitch', 'uDepth', 'uSize', 'uDim', 'uHide', 'uAlpha', 'uSpot', 'uTab']) u[n] = gl.getUniformLocation(prog, n) + gl.uniform1i(u.uTex, 0) + gl.uniformMatrix4fv(u.uModel, false, IDENTITY) + gl.uniform1f(u.uDim, 1) + gl.uniform1f(u.uAlpha, 1) + gl.uniform4f(u.uHide, 0, 0, 0, 0) + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + gl.uniform1f(u.uPitch, SHELF.PITCH) + gl.uniform1f(u.uDepth, SHELF.DEPTH) + gl.enable(gl.DEPTH_TEST) + gl.depthFunc(gl.LEQUAL) + gl.enable(gl.CULL_FACE) + gl.disable(gl.CULL_FACE) // las mallas no siguen un solo sentido de giro + gl.clearColor(0.09, 0.055, 0.035, 1) + aniso = gl.getExtension('EXT_texture_filter_anisotropic') + wood = texFrom(woodCanvas()) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT) + } + + // ----- Colocación ----- + const rowBase = k => -(k + 1) * SHELF.PITCH // la cara de arriba de la balda de esa fila + function buildFurniture() { + freeMesh(furniture) + const sh = new Shape(), { BOARD, SIDE, DEPTH } = SHELF, W = rowWidth, low = rowBase(Math.max(rows.length, 1) - 1) - BOARD, TILE = 1.7 + // Fondo. + quad(sh, [-SIDE, low, 0], [W + 2 * SIDE, 0, 0], [0, -low + BOARD, 0], [0, 0, 1], [0, low / TILE, (W + 2 * SIDE) / TILE, BOARD / TILE]) + // Laterales, por dentro y de canto. + for (const [x, n] of [[0, 1], [W, -1]]) quad(sh, [x, low, 0], [0, 0, DEPTH], [0, -low + BOARD, 0], [n, 0, 0], [0.2, low / TILE, 0.2 + DEPTH / TILE, BOARD / TILE]) + for (const x of [-SIDE, W]) quad(sh, [x, low, DEPTH], [SIDE, 0, 0], [0, -low + BOARD, 0], [0, 0, 1], [x, low / TILE, x + SIDE / TILE, BOARD / TILE]) + // Baldas, con la veta a lo largo. + const board = yTop => { + const uv = (a, b) => [yTop * 0.37, a / TILE, yTop * 0.37 + b / TILE, 0] + quad(sh, [W, yTop, 0], [-W, 0, 0], [0, 0, DEPTH], [0, 1, 0], [yTop * 0.37, 0, yTop * 0.37 + DEPTH / TILE, W / TILE]) + quad(sh, [0, yTop - BOARD, DEPTH], [W, 0, 0], [0, BOARD, 0], [0, 0, 1], uv(W, 0)) + quad(sh, [0, yTop - BOARD, 0], [W, 0, 0], [0, 0, DEPTH], [0, -1, 0], [yTop * 0.37, 0, yTop * 0.37 + DEPTH / TILE, W / TILE]) + } + for (let k = 0; k < Math.max(rows.length, 1); k++) board(rowBase(k)) + // Tapa de arriba. + quad(sh, [-SIDE, 0, DEPTH], [W + 2 * SIDE, 0, 0], [0, BOARD, 0], [0, 0, 1], [0, 0, W / TILE, 0.04]) + quad(sh, [-SIDE, BOARD, 0], [W + 2 * SIDE, 0, 0], [0, 0, DEPTH], [0, 1, 0], [0, 0, W / TILE, DEPTH / TILE]) + quad(sh, [0, 0, 0], [W, 0, 0], [0, 0, DEPTH], [0, -1, 0], [0, 0, W / TILE, DEPTH / TILE]) + furniture = makeMesh(sh) + } + const urlOf = item => item.vol.art || item.vol.thumb || item.work.cover || null + const panelKey = item => item.vol.chapters?.[0] || null + // De dónde sale el arte de este lomo: su portada, una página a color o, en último caso, la portada de la obra. + const artOf = item => item.vol.art || item.vol.thumb || panels.get(panelKey(item)) || item.work.cover || null + const fallbackPalette = item => { + const h = hash(item.work.id) % 360, base = hsl(h, 0.42, 0.34) + return { base, accent: hsl((h + 40) % 360, 0.55, 0.72) } + } + const paletteOf = item => palettes[artOf(item)] || fallbackPalette(item) + // Una obra sin portada: su color y su título. + function plainCover(item) { + const c = Object.assign(document.createElement('canvas'), { width: 358, height: 512 }), g = c.getContext('2d'), { base, accent } = paletteOf(item) + g.fillStyle = css(base) + g.fillRect(0, 0, c.width, c.height) + g.strokeStyle = css(accent) + g.lineWidth = 5 + g.strokeRect(22, 22, c.width - 44, c.height - 44) + g.fillStyle = css(inkOn(base)) + g.textAlign = 'center' + g.font = '700 34px system-ui, sans-serif' + const lines = [] + for (const word of String(item.work.title).split(/\s+/)) { + const last = lines.at(-1) + if (last != null && g.measureText(`${last} ${word}`).width <= c.width - 80) lines[lines.length - 1] = `${last} ${word}` + else lines.push(word) + } + const shown = lines.slice(0, 8) + shown.forEach((line, i) => g.fillText(line, c.width / 2, c.height / 2 + (i - (shown.length - 1) / 2) * 42 + 12, c.width - 70)) + return c + } + const coverKey = item => urlOf(item) || `plain:${item.work.id}` + const plainFor = key => item => { if (!covers.has(key)) { covers.set(key, texFrom(plainCover(item))); draw() } } + function loadImage(url, done) { + if (loadingImg.has(url)) { loadingImg.get(url).push(done); return } + loadingImg.set(url, [done]) + waiting.push(url) + pumpImages() + } + function pumpImages() { + while (!dead && imgBusy < IMG_AT_ONCE && waiting.length) { + const url = waiting.pop() // lo último pedido es lo que está a la vista + imgBusy++ + const im = new Image() + im.decoding = 'async' + // Una imagen que no acaba (una fuente colgada) no puede tapar la cola: a los 15 s deja su hueco, y si llega luego se usa igual. + let freed = false + const free = () => { if (!freed) { freed = true; imgBusy--; clearTimeout(slow); pumpImages() } } + const slow = setTimeout(free, 15000) + const finish = ok => { + const list = loadingImg.get(url) || [] + loadingImg.delete(url) + if (!dead) for (const f of list) f(ok ? im : null) + free() + } + im.onload = () => finish(true) + im.onerror = () => finish(false) + im.src = url + } + } + // Alto y fondo de cada obra, por la proporción de su portada. Lo aprendido rehace el mueble una vez; luego se recuerda. + const sizeOf = work => bookSize(palettes[`size:${work.id}`]) + function learnSize(item, url, im) { + const work = item.work + const key = `size:${work.id}` + if (palettes[key] || !im?.naturalHeight || !url || url !== (item.vol.art || item.vol.thumb || work.cover)) return // solo portadas, no páginas + const before = sizeOf(work) + palettes[key] = Math.round((im.naturalWidth / im.naturalHeight) * 1000) / 1000 + savePalettes() + const now = sizeOf(work) + if (now.h !== before.h || Math.abs(now.d - before.d) > 0.004) api.relayout() + } + function learnPalette(url, im) { + if (palettes[url]) return + try { + const small = Object.assign(document.createElement('canvas'), { width: 48, height: 72 }), g = small.getContext('2d', { willReadFrequently: true }) + g.drawImage(im, 0, 0, 48, 72) + const { base, accent } = coverPalette(g.getImageData(0, 0, 48, 72).data) + palettes[url] = { base, accent } + savePalettes() + } catch {} + } + + // Rostros: de uno en uno y lo último pedido primero. Lo encontrado se recuerda, y también lo no encontrado. + const faceWaiting = [] + let faceWorker = null, faceBusy = false, faceSeq = 0 + function askFace(url) { + if (faceWaiting.some(q => q === url)) return + faceWaiting.push(url) + pumpFaces() + } + function startFaceWorker() { + try { + faceWorker = new Worker(`/animeface.js${new URL(import.meta.url).search}`) + faceWorker.onerror = () => { faceWorker?.terminate(); faceWorker = false; faceBusy = false; faceWaiting.length = 0 } + } catch { faceWorker = false } + return faceWorker + } + function pumpFaces() { + if (dead || faceBusy || !faceWaiting.length || faceWorker === false || (!faceWorker && !startFaceWorker())) return + const url = faceWaiting.pop() + if (palettes[`face:${url}`] !== undefined) return pumpFaces() + faceBusy = true + let given = false + const giveUp = setTimeout(() => { given = true; faceBusy = false; pumpFaces() }, 30000) // la imagen no llega: se sigue con las demás + loadImage(url, im => { + clearTimeout(giveUp) + if (given) return + let pixels = null, w = FACE_INPUT, h = 0 + if (im) try { + h = Math.max(1, Math.round((w * im.naturalHeight) / im.naturalWidth)) + const c = Object.assign(document.createElement('canvas'), { width: w, height: h }), g = c.getContext('2d', { willReadFrequently: true }) + g.drawImage(im, 0, 0, w, h) + pixels = g.getImageData(0, 0, w, h).data.buffer + } catch { pixels = null } + if (!pixels || !faceWorker) { faceBusy = false; return pumpFaces() } + const id = ++faceSeq + faceWorker.onmessage = ({ data }) => { + if (data.id !== id) return + faceBusy = false + if (dead) return + if (data.faces) { + const face = chooseFace(data.faces, w, h) + palettes[`face:${url}`] = face + savePalettes() + if (face) { + cropArt(url, im, face) + // La fila pudo rehacerse mientras tanto: se avisa a las que hay ahora. + for (const b of built.values()) if (rows[b.k]?.some(it => !it.face && artOf(it) === url)) b.repaint?.() + } + } + pumpFaces() + } + faceWorker.postMessage({ id, pixels, w, h }, [pixels]) + }) + } + function learnArt(url, im, wantFace) { + if (!arts.has(url)) cropArt(url, im, palettes[`face:${url}`] || null) + if (wantFace && palettes[`face:${url}`] === undefined) askFace(url) + } + function cropArt(url, im, face) { + try { + const w = im.naturalWidth, h = im.naturalHeight + const small = Object.assign(document.createElement('canvas'), { width: 64, height: Math.max(1, Math.round((64 * h) / w)) }), g = small.getContext('2d', { willReadFrequently: true }) + g.drawImage(im, 0, 0, small.width, small.height) + const colW = Math.max(2, Math.round(small.height * ART_RATIO)) + const out = Object.assign(document.createElement('canvas'), { width: Math.round(ART_H * ART_RATIO), height: ART_H }) + if (face) { + const { sx, sy, sw } = faceStrip(face, w, h) + out.getContext('2d').drawImage(im, sx, sy, sw, sw / ART_RATIO, 0, 0, out.width, out.height) + } else { + // Sin rostro: de la banda de en medio hacia abajo, porque arriba suelen ir el logo y la editorial. + const keep = 1 - ART_SKIP, from = Math.round(small.height * ART_SKIP), cw = Math.max(2, Math.round(colW * keep)) + const at = bestColumn(g.getImageData(0, 0, small.width, small.height).data, small.width, small.height, Math.min(cw, small.width), from) + out.getContext('2d').drawImage(im, (at / small.width) * w, h * ART_SKIP, h * ART_RATIO * keep, h * keep, 0, 0, out.width, out.height) + } + arts.set(url, out) + if (arts.size > ART_KEEP) arts.delete(arts.keys().next().value) // se vuelve a sacar de la caché del navegador si hace falta + } catch {} + } + // Tomo sin portada: la primera de sus páginas que sea a color. Lo encontrado se recuerda; lo no encontrado, también, para no volver a descargar páginas. + function findPanel(item, done) { + const key = panelKey(item) + if (!key || !pagesOf || askedPanel.has(key)) return + askedPanel.add(key) + const saved = palettes[`panel:${key}`] + if (saved !== undefined) { if (saved) { panels.set(key, saved); done() } return } + pagesOf(key).then(async urls => { + let found = null + for (const url of urls) { + const im = await new Promise(res => loadImage(url, res)) + if (dead) return + if (!im) continue + try { + const c = Object.assign(document.createElement('canvas'), { width: 40, height: 60 }), g = c.getContext('2d', { willReadFrequently: true }) + g.drawImage(im, 0, 0, 40, 60) + if (colorfulness(g.getImageData(0, 0, 40, 60).data) >= COLOR_PAGE) { found = url; break } + } catch {} + } + palettes[`panel:${key}`] = found || false + savePalettes() + if (found) { panels.set(key, found); done() } + }, () => {}) + } + // Los lomos de una fila se dibujan juntos en una tira: una textura por fila, por muchos tomos que tenga. + const stripWidth = () => Math.max(512, Math.min(STRIP_MAX, maxTex, Math.round(rowWidth * STRIP_PER_UNIT))) + // ----- Logos de las obras (Wikimedia Commons, por el proxy propio para poder pintarlos en la tira) ----- + const logos = new Map() + function logoLight(im) { + try { + const c = Object.assign(document.createElement('canvas'), { width: 96, height: Math.max(1, Math.round((96 * im.naturalHeight) / im.naturalWidth)) }), g = c.getContext('2d', { willReadFrequently: true }) + g.drawImage(im, 0, 0, c.width, c.height) + const px = g.getImageData(0, 0, c.width, c.height).data + let lum = 0, a = 0 + for (let p = 0; p < px.length; p += 4) { const al = px[p + 3] / 255; lum += al * (px[p] * 0.299 + px[p + 1] * 0.587 + px[p + 2] * 0.114); a += al } + return a ? lum / a / 255 : 0.5 + } catch { return 0.5 } + } + function setLogo(id, url) { + if (!url || logos.has(id)) return + loadImage(url, im => { + if (!im || dead || im.naturalWidth < im.naturalHeight * 1.2) return + logos.set(id, { im, light: logoLight(im) }) + for (const b of built.values()) if ((rows[b.k] || []).some(i => i.work.id === id)) b.repaint?.() + }) + } + + // ----- Notas: un pósit por colección, pegado al canto de la balda de arriba ----- + function rowNotes(row) { + const groups = [] + for (let i = 0; i < row.length;) { + let j = i + while (j + 1 < row.length && row[j + 1].work === row[i].work) j++ + groups.push({ work: row[i].work, x0: row[i].x, x1: row[j].x + row[j].w }) + i = j + 1 + } + const out = [] + let free = 0.02 + for (const n of groups) { + const span = n.x1 - n.x0, w = Math.max(0.26, Math.min(0.42, span * 0.85)) + let x = Math.max(free, n.x0 + Math.min(0.03, span * 0.1)) + if (x + w > rowWidth - 0.02) x = rowWidth - 0.02 - w + if (x < free - 1e-6 || x > n.x1 + 0.12) continue // no cabe junto a los suyos + out.push({ ...n, x, w, seed: hash(n.work.id) }) + free = x + w + 0.03 + } + return out + } + function paintNotes(notes, canvas) { + const g = canvas.getContext('2d') + g.setTransform(1, 0, 0, 1, 0, 0) + g.clearRect(0, 0, canvas.width, canvas.height) + let x = 0 + for (const n of notes) { + const W = n.px, H = NOTE_PX, { family, weight, scale, paper, ink } = noteStyle(n.work.id) + g.save() + g.translate(x, 0) + g.fillStyle = paper + g.fillRect(0, 0, W, H) + g.fillStyle = 'rgba(0,0,0,.07)' // la franja con pegamento + g.fillRect(0, 0, W, H * 0.16) + const curl = g.createLinearGradient(0, H * 0.7, 0, H) + curl.addColorStop(0, 'rgba(0,0,0,0)') + curl.addColorStop(1, 'rgba(0,0,0,.14)') + g.fillStyle = curl + g.fillRect(0, 0, W, H) + g.fillStyle = ink + g.textAlign = 'center' + g.textBaseline = 'middle' + const title = shortTitle(n.work.title), font = px => `${weight} ${Math.round(px)}px "${family}", cursive` + let size = H * 0.5 * scale + g.font = font(size) + const room = W * 0.9 + if (g.measureText(title).width > room) size = Math.max(H * 0.3 * scale, (size * room) / g.measureText(title).width) + g.font = font(size) + if (g.measureText(title).width > room) { + // Dos renglones, partiendo por el espacio más cercano al centro. + const words = title.split(/\s+/) + let best = 1, bestD = Infinity + for (let i = 1; i < words.length; i++) { const d = Math.abs(words.slice(0, i).join(' ').length - title.length / 2); if (d < bestD) { bestD = d; best = i } } + const a = words.slice(0, best).join(' '), b = words.slice(best).join(' ') + size = Math.min(H * 0.36 * scale, (size * room) / Math.max(g.measureText(a).width, g.measureText(b).width, 1) * 1) + g.font = font(size) + g.fillText(a, W / 2, H * 0.4, room) + g.fillText(b, W / 2, H * 0.4 + size * 0.95, room) + } else g.fillText(title, W / 2, H * 0.56, room) + g.restore() + x += W + } + } + function buildNotes(b, k) { + const notes = rowNotes(rows[k]) + if (!notes.length) return + const base = rowBase(k), edge = k === 0 ? base + SHELF.PITCH : base + SHELF.PITCH - SHELF.BOARD, top = edge + SHELF.BOARD * 0.75, z = SHELF.DEPTH + 0.004 + for (const n of notes) n.px = Math.round((NOTE_PX * n.w) / NOTE_H) + const canvas = Object.assign(document.createElement('canvas'), { width: Math.min(maxTex, notes.reduce((a, n) => a + n.px, 0)), height: NOTE_PX }) + const sh = new Shape() + let u = 0 + for (const n of notes) { + const a = noteStyle(n.work.id).tilt, c = Math.cos(a), sn = Math.sin(a), U0 = u / canvas.width, U1 = (u + n.px) / canvas.width + quad(sh, [n.x, top, z], [n.w * c, n.w * sn, 0], [NOTE_H * sn, -NOTE_H * c, 0], [0, 0, 1], [U0, 0, U1, 1]) + u += n.px + } + paintNotes(notes, canvas) + b.notes = { mesh: makeMesh(sh), canvas, tex: texFrom(canvas), list: notes } + loadNoteFonts().then(() => { if (!dead && built.get(k) === b && b.notes) { paintNotes(notes, canvas); texFrom(canvas, b.notes.tex); draw() } }) + } + + function paintStrip(row, strip) { + const g = strip.getContext('2d'), toPx = strip.width / rowWidth + g.setTransform(1, 0, 0, 1, 0, 0) + g.fillStyle = '#222' + g.fillRect(0, 0, strip.width, STRIP_H) + for (const it of row) { + if (it.face) continue + const { base, accent } = paletteOf(it) + const x = it.x * toPx, w = it.w * toPx, PX = STRIP_H / it.h + // Se escribe en unidades de la pantalla: en una fila estrecha la tira se estira a lo ancho y el texto se deformaría. + const sx = toPx / PX, wText = it.w * PX + g.setTransform(1, 0, 0, 1, x, 0) + const art = arts.get(artOf(it)) + let ink = inkOn(base), backing = null + if (art) { + // El arte ocupa todo el lomo; de la franja guardada se usa el centro, lo que corresponde a su grosor. + const part = Math.min(art.width, (art.height * it.w) / it.h) + g.imageSmoothingEnabled = true + g.imageSmoothingQuality = 'high' + g.drawImage(art, (art.width - part) / 2, 0, part, art.height, 0, 0, w, STRIP_H) + backing = (x0, y0, bw, bh, force) => { + // Desenfoque suave bajo las letras: se reduce lo pintado y se vuelve a estirar. + const X = x + x0, sw = Math.max(2, Math.round(bw / 4)), sh = Math.max(2, Math.round(bh / 4)) + blurBox.width = sw; blurBox.height = sh + const bg = blurBox.getContext('2d', { willReadFrequently: true }) + bg.imageSmoothingEnabled = true + bg.drawImage(strip, X, y0, bw, bh, 0, 0, sw, sh) + const px = bg.getImageData(0, 0, sw, sh).data + let lum = 0 + for (let p = 0; p < px.length; p += 4) lum += px[p] * 0.299 + px[p + 1] * 0.587 + px[p + 2] * 0.114 + const light = force ? force === 'light' : lum / (px.length / 4) / 255 > 0.6 + g.save() + g.beginPath() + if (g.roundRect) g.roundRect(x0, y0, bw, bh, Math.min(bw, bh) * 0.45) + else g.rect(x0, y0, bw, bh) + g.clip() + g.drawImage(blurBox, 0, 0, sw, sh, x0 - bw * 0.04, y0 - bh * 0.04, bw * 1.08, bh * 1.08) + g.fillStyle = light ? 'rgba(255,255,255,.34)' : 'rgba(0,0,0,.32)' + g.fillRect(x0, y0, bw, bh) + g.restore() + return light ? [0.08, 0.08, 0.09] : [0.98, 0.97, 0.95] + } + } else { + g.fillStyle = css(base) + g.fillRect(0, 0, w, STRIP_H) + g.fillStyle = css(accent) + g.fillRect(0, STRIP_H * 0.045, w, STRIP_H * 0.012) + g.fillRect(0, STRIP_H * 0.86, w, STRIP_H * 0.012) + } + g.textAlign = 'center' + g.textBaseline = 'middle' + // El título, en la mitad de abajo para no tapar la cara: primero se reduce la letra; si aun así no cabe, se estrecha. + const from = art ? 0.4 : 0.07, to = art ? 0.865 : 0.85, room = STRIP_H * (to - from) + const logo = logos.get(it.work.id) + if (logo) { + // El logo de la obra en lugar del texto, girado como el título; la pastilla contrasta con el propio logo. + const lw = logo.im.naturalWidth, lh = logo.im.naturalHeight, sc = Math.min(room / lw, (wText * 0.74) / lh), len = lw * sc, thick = lh * sc + const mid = art ? to - len / STRIP_H / 2 - 0.012 : (from + to) / 2 + if (backing) { const bw = Math.min(w * 0.96, (thick + wText * 0.2) * sx), pad = wText * 0.16; ink = backing((w - bw) / 2, STRIP_H * mid - len / 2 - pad, bw, len + pad * 2, logo.light > 0.55 ? 'dark' : 'light') } + g.save() + g.scale(sx, 1) + g.translate(wText / 2, STRIP_H * mid) + g.rotate(Math.PI / 2) + if (!backing) { g.shadowColor = logo.light > 0.55 ? 'rgba(0,0,0,.6)' : 'rgba(255,255,255,.6)'; g.shadowBlur = 4 } + g.imageSmoothingEnabled = true + g.imageSmoothingQuality = 'high' + g.drawImage(logo.im, -len / 2, -thick / 2, len, thick) + g.restore() + } + let size = wText * (art ? 0.44 : 0.5) + g.font = `700 ${Math.round(size)}px system-ui, sans-serif` + let long = g.measureText(it.work.title).width + if (long > room) { size = Math.max(wText * 0.32, (size * room) / long); g.font = `700 ${Math.round(size)}px system-ui, sans-serif`; long = g.measureText(it.work.title).width } + const len = Math.min(long, room), mid = art ? to - len / STRIP_H / 2 - 0.012 : (from + to) / 2 + if (backing && !logo) { + const bw = size * 1.45 * sx, pad = size * 0.55 + ink = backing((w - bw) / 2, STRIP_H * mid - len / 2 - pad, bw, len + pad * 2) + } + g.save() + g.scale(sx, 1) + if (logo) g.globalAlpha = 0 // ya está el logo + g.fillStyle = css(ink) + if (backing) { g.shadowColor = ink[0] > 0.5 ? 'rgba(0,0,0,.55)' : 'rgba(255,255,255,.5)'; g.shadowBlur = 3 } + g.translate(wText / 2, STRIP_H * mid) + g.rotate(Math.PI / 2) + g.fillText(it.work.title, 0, 0, room) + g.restore() + if (it.vol.vol != null) { + const nSize = wText * 0.5, label = String(it.vol.vol), cy = STRIP_H * 0.935 + g.font = `800 ${Math.round(nSize)}px system-ui, sans-serif` + let numInk = ink + if (backing) { + const tw = Math.min(g.measureText(label).width, wText * 0.86) * sx, bw = Math.min(w * 0.94, tw + nSize * 0.6 * sx), bh = nSize * 1.35 + numInk = backing((w - bw) / 2, cy - bh / 2, bw, bh) + } + g.save() + g.scale(sx, 1) + g.fillStyle = css(numInk) + if (backing) { g.shadowColor = numInk[0] > 0.5 ? 'rgba(0,0,0,.55)' : 'rgba(255,255,255,.5)'; g.shadowBlur = 3 } + g.fillText(label, wText / 2, cy, wText * 0.86) + g.restore() + } + // Redondez del lomo: se oscurece hacia los bordes. + const round = g.createLinearGradient(0, 0, w, 0) + round.addColorStop(0, 'rgba(0,0,0,.38)'); round.addColorStop(0.16, 'rgba(0,0,0,.05)'); round.addColorStop(0.42, 'rgba(255,255,255,.12)'); round.addColorStop(0.8, 'rgba(0,0,0,.05)'); round.addColorStop(1, 'rgba(0,0,0,.42)') + g.fillStyle = round + g.fillRect(0, 0, w, STRIP_H) + } + g.setTransform(1, 0, 0, 1, 0, 0) + } + function buildRow(k) { + const row = rows[k], base = rowBase(k), F = SHELF.FRONT, strip = Object.assign(document.createElement('canvas'), { width: stripWidth(), height: STRIP_H }) + const spines = new Shape(), paper = new Shape(), faces = [] + for (const it of row) { + const x0 = it.x, x1 = it.x + it.w, U0 = x0 / rowWidth, U1 = x1 / rowWidth, H = it.h, D = it.d + if (it.face) { + const z = SHELF.FACE_Z, t = it.t, sh = new Shape(), edge = new Shape(), pages = new Shape() + quad(sh, [x0, base + H, z], [it.w, 0, 0], [0, -H, 0], [0, 0, 1]) + quad(edge, [x0, base, z - t], [0, 0, t], [0, H, 0], [-1, 0, 0]) + quad(edge, [x1, base, z], [0, 0, -t], [0, H, 0], [1, 0, 0]) + quad(pages, [x0, base + H, z - t], [it.w, 0, 0], [0, 0, t], [0, 1, 0], [0, 0, it.w, t]) + faces.push({ it, mesh: makeMesh(sh), edge: makeMesh(edge), pages: makeMesh(pages) }) + continue + } + quad(spines, [x0, base + H, F], [it.w, 0, 0], [0, -H, 0], [0, 0, 1], [U0, 0, U1, 1]) + // Los costados son las tapas: un solo color del lomo, no una columna de la tira estirada a lo largo del fondo. + const Um = (U0 + U1) / 2, side = [Um, 0.02, Um, 0.02] + quad(spines, [x0, base, F - D], [0, 0, D], [0, H, 0], [-1, 0, 0], side) + quad(spines, [x1, base, F], [0, 0, -D], [0, H, 0], [1, 0, 0], side) + quad(paper, [x0, base + H, F - D], [it.w, 0, 0], [0, 0, D], [0, 1, 0], [0, it.w, it.w, it.w]) // u: a lo ancho, en el mueble; v: el grosor + } + paintStrip(row, strip) + const b = { k, key: rowKey(row), strip, tex: texFrom(strip), spines: spines.i.length ? makeMesh(spines) : null, paper: paper.i.length ? makeMesh(paper) : null, faces, stale: 0 } + built.set(k, b) + buildNotes(b, k) + // Colores y portadas, de poco en poco: lo de fuera de la vista espera. + for (const it of row) { + const url = it.face ? urlOf(it) : artOf(it) + if (!url) { + if (it.face) plainFor(coverKey(it))(it) + } + // A intervalos, y no durante un gesto: repintar y subir la tira en pleno arrastre daría tirones. + const repaint = () => { + if (built.get(k) !== b || b.stale) return + b.stale = setTimeout(() => { + b.stale = 0 + if (built.get(k) !== b || dead) return + if (pointers.size || velocity) return repaint() + paintStrip(row, strip) + texFrom(strip, b.tex) + draw() + }, 400) + } + b.repaint = repaint + if (!it.face && !it.vol.art && !it.vol.thumb) findPanel(it, () => { + const panel = artOf(it) + if (panel && !arts.has(panel)) loadImage(panel, im => { if (im) { learnPalette(panel, im); learnArt(panel, im, true); repaint() } }) + else repaint() + }) + if (!url) continue + if (it.face) { + if (!covers.has(url)) loadImage(url, im => { + if (!im) return plainFor(url)(it) + learnSize(it, url, im) + learnPalette(url, im) + learnArt(url, im) + covers.set(url, texFrom(im)) + draw() + }) + } else if (arts.has(url)) { + if (palettes[`face:${url}`] === undefined) askFace(url) // su arte salió de la portada de frente, donde no se buscan rostros + } else loadImage(url, im => { + if (!im) return + learnSize(it, url, im) + learnPalette(url, im) + learnArt(url, im, true) + repaint() + }) + } + return b + } + function freeRow(b) { + clearTimeout(b.stale) + gl.deleteTexture(b.tex) + freeMesh(b.spines); freeMesh(b.paper) + for (const f of b.faces) { freeMesh(f.mesh); freeMesh(f.edge); freeMesh(f.pages) } + if (b.notes) { freeMesh(b.notes.mesh); gl.deleteTexture(b.notes.tex) } + built.delete(b.k) + } + const rowKey = row => row ? row.map(i => `${i.work.id}/${i.vol.vol}/${i.x.toFixed(4)}/${i.w.toFixed(4)}/${i.h.toFixed(3)}/${i.d.toFixed(3)}/${i.face ? 1 : 0}/${urlOf(i)}`).join() : '' + function dropAllRows() { for (const b of [...built.values()]) freeRow(b) } + + // Lo que se puede recorrer depende del zoom: de cerca hay más mueble fuera de la pantalla. + function limits() { + const half = visibleH / 2 / zoom + top = SHELF.BOARD + 0.045 - half + bottom = Math.min(top, rowBase(Math.max(rows.length, 1) - 1) - SHELF.BOARD - 0.045 + half) + } + function clampView() { + const hit = y > top || y < bottom + y = Math.max(bottom, Math.min(top, y)) + if (hit) velocity = 0 + const room = (visibleH * (size.w / size.h) / 2) * (1 - 1 / zoom) + x = Math.max(-room, Math.min(room, x)) + } + function setZoom(next, cx = size.w / 2, cy = size.h / 2) { + const z = Math.max(1, Math.min(ZOOM_MAX, next)) + if (z === zoom) return + // El punto bajo los dedos no se mueve. + const per = visibleH / size.h, dx = cx - size.w / 2, dy = cy - size.h / 2 + x += dx * per * (1 / zoom - 1 / z) + y -= dy * per * (1 / zoom - 1 / z) + zoom = z + limits() + clampView() + draw() + } + function layout() { + const rect = container.getBoundingClientRect() + size = { w: Math.max(1, rect.width), h: Math.max(1, rect.height) } + const aspect = size.w / size.h + visibleH = (size.w >= size.h ? rowsLandscape : rowsPortrait) * SHELF.PITCH + rowWidth = visibleH * aspect - 2 * SHELF.SIDE - 0.05 + rows = layoutShelf(works, rowWidth, sizeOf) + // Solo se rehace lo que cambia: al aprender tamaños, casi todas las filas siguen igual. + const key = `${rowWidth.toFixed(3)}:${rows.length}` + if (key !== rowsKey) { + rowsKey = key + dropAllRows() + buildFurniture() + } else for (const b of [...built.values()]) if (b.key !== rowKey(rows[b.k])) freeRow(b) + const was = top + limits() + y = y === was || !y ? top : y + if (focusId) { const k = rows.findIndex(r => r.some(i => i.work.id === focusId)); if (k >= 0) y = rowBase(k) + SHELF.PITCH * 0.42 } + clampView() + } + + // ----- Dibujo ----- + function draw() { + if (dead || raf) return + raf = requestAnimationFrame(paint) + } + function stepInertia(now) { + if (!velocity || pointers.size) return false + const dt = Math.min(120, now - lastMove) + lastMove = now + y += velocity * dt + velocity *= Math.exp(-dt / FRICTION_MS) + clampView() + if (Math.abs(velocity) < 0.00004) velocity = 0 + return !!velocity + } + function stepTilt(now) { + tilt.fade(now) + const on = !still && zoom === 1 + const want = on ? { x: tilt.target.x / MAX_DEG, y: tilt.target.y / MAX_DEG } : { x: 0, y: 0 } + let moving = !!(tilt.target.x || tilt.target.y) && on + for (const k of ['x', 'y']) { + const d = want[k] - tiltNow[k] + if (Math.abs(d) < 0.002) tiltNow[k] = want[k] + else { tiltNow[k] += d * 0.25; moving = true } + } + return moving + } + function paint() { + raf = 0 + if (dead) return + const rect = container.getBoundingClientRect(), dpr = Math.min(devicePixelRatio || 1, 2.5) + const w = Math.max(1, Math.round(rect.width * dpr)), h = Math.max(1, Math.round(rect.height * dpr)) + if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h } + if (rect.width !== size.w || rect.height !== size.h) layout() + const now = performance.now() + let again = stepInertia(now) + if (stepTilt(now)) again = true + if (stepPeek(now)) again = true + gl.viewport(0, 0, w, h) + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + if (again) draw() + if (!furniture) return + const aspect = rect.width / rect.height + const round = Math.max(Math.abs(tiltNow.x), Math.abs(tiltNow.y)) + const dist = (visibleH / 2 / Math.tan(FOV / 2) / zoom) * (1 + ORBIT_BACK * round) + const M = cameraMatrix([rowWidth / 2 + x, y, SHELF.DEPTH], dist, aspect, ((REST_TILT - tiltNow.x * ORBIT_TILT) * Math.PI) / 180, (-tiltNow.y * ORBIT_YAW * Math.PI) / 180) + gl.uniformMatrix4fv(u.uM, false, M) + lastM = M + gl.uniform2f(u.uSize, w, h) + gl.uniform1f(u.uDim, peek ? (1 - 0.42 * ease(peek.t)) * (1 - 0.6 * ease(peek.fly?.t || 0)) : 1) + if (peek) gl.uniform4f(u.uHide, peek.it.x, peek.it.x + peek.it.w, peek.base - 0.002, peek.base + peek.it.h + 0.002) + gl.activeTexture(gl.TEXTURE0) + gl.uniform1i(u.uKind, 0) + gl.bindTexture(gl.TEXTURE_2D, wood) + drawMesh(furniture) + const nearest = Math.round((-y - SHELF.PITCH / 2) / SHELF.PITCH) + const reach = Math.ceil(visibleH / zoom / SHELF.PITCH / 2) + 1 + for (const b of [...built.values()]) if (Math.abs(b.k - nearest) > reach + 2) freeRow(b) + for (let k = Math.max(0, nearest - reach); k <= Math.min(rows.length - 1, nearest + reach); k++) { + const b = built.get(k) || buildRow(k) + if (b.spines) { + gl.uniform1i(u.uKind, 1) + gl.bindTexture(gl.TEXTURE_2D, b.tex) + drawMesh(b.spines) + } + if (b.notes) { + gl.uniform1i(u.uKind, 1) + gl.bindTexture(gl.TEXTURE_2D, b.notes.tex) + drawMesh(b.notes.mesh) + } + gl.uniform1i(u.uKind, 3) + if (b.paper) drawMesh(b.paper) + for (const f of b.faces) { + const tex = covers.get(coverKey(f.it)), { base } = paletteOf(f.it) + gl.uniform1i(u.uKind, 2) + gl.uniform1f(u.uHas, tex ? 1 : 0) + gl.uniform3fv(u.uColor, base) + if (tex) gl.bindTexture(gl.TEXTURE_2D, tex) + drawMesh(f.mesh) + gl.uniform1i(u.uKind, 4) + drawMesh(f.edge) + gl.uniform1i(u.uKind, 5) + drawMesh(f.pages) + } + } + if (peek) { + // Ya delante, el tomo no sigue al giroscopio: su cámara pierde la inclinación a medida que se acerca. + const k = 1 - ease(peek.t), dk = (visibleH / 2 / Math.tan(FOV / 2) / zoom) * (1 + ORBIT_BACK * round * k) + peekM = cameraMatrix([rowWidth / 2 + x, y, SHELF.DEPTH], dk, aspect, ((REST_TILT - tiltNow.x * ORBIT_TILT * k) * Math.PI) / 180, (-tiltNow.y * ORBIT_YAW * k * Math.PI) / 180) + gl.uniformMatrix4fv(u.uM, false, peekM) + if (peek.fly) drawTable() + drawPeek() + if (peek.fly && peek.fly.t >= 1 && !peek.fly.shot) { + // Último fotograma, tomado ahora que el lienzo aún lo tiene: tapa el cambio al lector. + peek.fly.shot = true + let snap = null + try { snap = canvas.toDataURL('image/jpeg', 0.9) } catch {} + const done = peek.fly.done + setTimeout(() => done?.(snap), 0) + } + } + } + + // ----- Vistazo: el tomo sale recto de la fila, gira y se acerca enseñando la portada ----- + let peek = null, lastM = null, peekM = null, deferLayout = false + const ease = t => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2) + const lerp3 = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t) + // Caja centrada en el origen con la portada hacia +z y el lomo a la derecha (+x), como un manga: en la fila, la portada mira a la izquierda. + function peekBox(W, H, T, U0, U1) { + const cover = new Shape(), spine = new Shape(), back = new Shape(), paper = new Shape(), X = W / 2, Y = H / 2, Z = T / 2 + quad(cover, [-X, Y, Z], [W, 0, 0], [0, -H, 0], [0, 0, 1]) + quad(spine, [X, Y, Z], [0, 0, -T], [0, -H, 0], [1, 0, 0], [U0, 0, U1, 1]) // en la fila, con el giro, coincide con su trozo de la tira + quad(back, [X, Y, -Z], [-W, 0, 0], [0, -H, 0], [0, 0, -1]) + quad(paper, [-X, Y, -Z], [W, 0, 0], [0, 0, T], [0, 1, 0], [0, 0, W, T]) + quad(paper, [-X, -Y, -Z], [W, 0, 0], [0, 0, T], [0, -1, 0], [0, 0, W, T]) + quad(paper, [-X, -Y, -Z], [0, H, 0], [0, 0, T], [-1, 0, 0], [0, 0, H, T]) + return [cover, spine, back, paper].map(makeMesh) + } + // Dónde queda delante de la cámara: a un lado de la ficha en apaisado, encima en vertical. + function peekTarget(W, H) { + const aspect = size.w / size.h, tan = Math.tan(FOV / 2), dist = visibleH / 2 / tan / zoom, wide = aspect >= 1 + let dd = H / ((wide ? 0.62 : 0.42) * 2 * tan) + if (!wide) dd = Math.max(dd, W / (0.6 * 2 * tan * aspect)) + dd = Math.min(dd, dist * 0.92) + const vh = 2 * tan * dd, vw = vh * aspect + // Sobre el eje de la cámara en reposo (mira algo hacia abajo), y desplazado en su plano. + const tl = (REST_TILT * Math.PI) / 180, T = [rowWidth / 2 + x, y, SHELF.DEPTH], eye = [T[0], T[1] - dist * Math.sin(tl), T[2] + dist * Math.cos(tl)] + const f = [0, T[1] - eye[1], T[2] - eye[2]], n = Math.hypot(f[1], f[2]), F = [0, f[1] / n, f[2] / n], U = [0, -F[2], F[1]] // derecha: +x + const dx = wide ? -vw * 0.2 : 0, dy = wide ? 0 : vh * 0.16 + return [eye[0] + dx, eye[1] + F[1] * dd + U[1] * dy, eye[2] + F[2] * dd + U[2] * dy] + } + function peekInfo(it) { + const vols = it.work.vols || [], i = vols.indexOf(it.vol), ch = it.work.chapterId + const cur = ch ? vols.findIndex(v => (v.chapters || []).includes(ch)) : -1 + const state = !ch ? 'new' : !vols.length ? 'reading' : cur < 0 ? 'unknown' : i < cur ? 'read' : i === cur ? 'reading' : 'new' + return { work: it.work, vol: it.vol, index: i, count: vols.length, state } + } + function startPeek(it, k) { + const b = built.get(k) + if (!b) return + if (peek) freePeek() + const base = rowBase(k), out = !it.face, W = out ? it.d : it.w, H = it.h, T = out ? it.w : it.t + const from = out ? { pos: [it.x + it.w / 2, base + H / 2, SHELF.FRONT - it.d / 2], rot: -Math.PI / 2 } : { pos: [it.x + it.w / 2, base + H / 2, SHELF.FACE_Z - it.t / 2], rot: 0 } + const mid = { pos: [from.pos[0], from.pos[1] + 0.006, from.pos[2] + (out ? it.d + 0.03 : 0.14)], rot: from.rot } + peek = { it, k, b, base, out, W, H, T, from, mid, to: { pos: peekTarget(W, H), rot: 0 }, box: peekBox(W, H, T, it.x / rowWidth, (it.x + it.w) / rowWidth), t: 0, dir: 1, at: performance.now(), tex: null, rank: -1 } + velocity = 0 + // Portada: primero la que ya hay, luego la grande. + const p = peek, shared = covers.get(coverKey(it)) + if (shared) p.shared = shared + else { p.tex = texFrom(plainCover(it)); p.rank = 0 } + ;[it.vol.thumb || it.work.cover, it.vol.art, it.vol.front].forEach((url, rank) => { + if (url) loadImage(url, im => { + if (peek !== p || !im || rank + 1 <= p.rank) return + if (p.tex) gl.deleteTexture(p.tex) + p.tex = texFrom(im); p.rank = rank + 1; p.shared = null + draw() + }) + }) + onPeek?.(peekInfo(it)) + draw() + } + function freePeek() { + if (!peek) return + for (const m of peek.box) freeMesh(m) + if (peek.tex && !gl.isContextLost()) gl.deleteTexture(peek.tex) + peek = null + if (deferLayout) { deferLayout = false; api.relayout() } + } + function unpeek() { + if (!peek || peek.dir < 0 || peek.fly) return + peek.dir = -1 + peek.at = performance.now() + onPeek?.(null) + draw() + } + function peekPose(p) { + if (p.fly) { + const a = p.fly.from, e = ease(p.fly.t) + return { pos: lerp3(a.pos, p.fly.to.pos, e), rot: a.rot + (p.fly.to.rot - a.rot) * e, lean: (a.lean || 0) + (p.fly.to.lean - (a.lean || 0)) * e } + } + const t = p.t + if (t <= PEEK_SLIDE) return { pos: lerp3(p.from.pos, p.mid.pos, ease(t / PEEK_SLIDE)), rot: p.from.rot } + const e = ease((t - PEEK_SLIDE) / (1 - PEEK_SLIDE)) + return { pos: lerp3(p.mid.pos, p.to.pos, e), rot: p.from.rot + (p.to.rot - p.from.rot) * e } + } + function stepPeek(now) { + if (!peek) return false + if (peek.fly) { + const f = peek.fly + f.t = still ? 1 : Math.min(1, f.t + (now - f.at) / FLY_MS) + f.at = now + return f.t < 1 || !f.shot + } + const dt = still ? 1 : (now - peek.at) / (peek.dir > 0 ? PEEK_OPEN_MS : PEEK_CLOSE_MS) + peek.at = now + peek.t = Math.max(0, Math.min(1, peek.t + peek.dir * dt)) + if (peek.dir < 0 && peek.t === 0) { freePeek(); return true } + return peek.t < 1 || peek.dir < 0 + } + function drawPeek() { + const p = peek, pose = peekPose(p), [cover, spine, back, paper] = p.box + gl.uniform4f(u.uHide, 0, 0, 0, 0) + gl.uniform1f(u.uDim, 1) + gl.uniformMatrix4fv(u.uModel, false, modelOf(pose)) + const tex = p.tex || p.shared + gl.uniform3fv(u.uColor, paletteOf(p.it).base) + gl.uniform1i(u.uKind, 2) + gl.uniform1f(u.uHas, tex ? 1 : 0) + if (tex) gl.bindTexture(gl.TEXTURE_2D, tex) + drawMesh(cover) + if (p.out && built.get(p.k) === p.b) { gl.uniform1i(u.uKind, 1); gl.bindTexture(gl.TEXTURE_2D, p.b.tex) } else gl.uniform1i(u.uKind, 4) + drawMesh(spine) + gl.uniform1i(u.uKind, 4) + drawMesh(back) + gl.uniform1i(u.uKind, 5) + drawMesh(paper) + gl.uniformMatrix4fv(u.uModel, false, IDENTITY) + } + // Giro del tomo: primero sobre su eje vertical (sacarlo y enseñar la portada), luego hacia atrás (posarlo en la mesa). + function modelOf({ pos, rot, lean = 0 }) { + const cy = Math.cos(rot), sy = Math.sin(rot), cx = Math.cos(lean), sx = Math.sin(lean) + return new Float32Array([cy, sx * sy, -cx * sy, 0, 0, cx, sx, 0, sy, -sx * cy, cx * cy, 0, pos[0], pos[1], pos[2], 1]) + } + // ----- A la mesa: el tomo se centra y se recuesta como en el lector, y la mesa aparece detrás ----- + let tableTex = null, tableAspect = 1, tableMesh = null + if (table) loadImage(table, im => { if (im && !dead) { tableTex = texFrom(im); tableAspect = im.naturalWidth / im.naturalHeight; draw() } }) + function flyTarget(W, H) { + // Como el encuadre del tomo cerrado en el lector: centrado, con un margen de 14 a 40 px. + const aspect = size.w / size.h, tan = Math.tan(FOV / 2), dist = visibleH / 2 / tan / zoom + const m = Math.max(14, Math.min(40, Math.min(size.w, size.h) * 0.04)) + let dd = (H * 1.04) / (2 * tan * (1 - (2 * m) / size.h)) + dd = Math.max(dd, (W * 1.06) / (2 * tan * aspect * (1 - (2 * m) / size.w))) + dd = Math.min(dd, dist * 0.95) + const tl = (REST_TILT * Math.PI) / 180, T = [rowWidth / 2 + x, y, SHELF.DEPTH], eye = [T[0], T[1] - dist * Math.sin(tl), T[2] + dist * Math.cos(tl)] + const f = [T[1] - eye[1], T[2] - eye[2]], n = Math.hypot(f[0], f[1]) + return { pos: [eye[0], eye[1] + (f[0] / n) * dd, eye[2] + (f[1] / n) * dd], rot: 0, lean: (-(14 - REST_TILT) * Math.PI) / 180 } // en el lector la mesa se ve a 14° + } + function drawTable() { + const f = peek.fly, a = Math.min(1, Math.max(0, (f.t - 0.1) / 0.75)) + if (a <= 0) return + const pose = peekPose(peek), Tz = peek.T / 2 + 0.003 + if (!tableMesh) { + const sh = new Shape(), S = 14 + quad(sh, [-S, S, 0], [2 * S, 0, 0], [0, -2 * S, 0], [0, 0, 1], [-S, S, S, -S]) + tableMesh = makeMesh(sh) + } + gl.uniform4f(u.uHide, 0, 0, 0, 0) // el hueco del tomo en la fila no recorta la mesa + const M = modelOf(pose) + // La mesa queda bajo el tomo: se baja a lo largo de su normal. + M[12] -= M[8] * Tz; M[13] -= M[9] * Tz; M[14] -= M[10] * Tz + gl.uniformMatrix4fv(u.uModel, false, M) + gl.uniform4f(u.uSpot, pose.pos[0], pose.pos[1], pose.pos[2], Math.max(peek.W, peek.H) * 0.9) + const tex = tableTex || wood, k = Math.max(peek.H * 1.6, (peek.W * 2.2) / tableAspect) + gl.uniform1f(u.uHas, tableTex ? 1 : 0) + gl.uniform2f(u.uTab, 1 / (k * tableAspect), -1 / k) + gl.bindTexture(gl.TEXTURE_2D, tex) + gl.uniform1i(u.uKind, 6) + gl.uniform1f(u.uAlpha, ease(a)) + // De fondo, aunque al recostarse se meta tras el mueble; luego el tomo se dibuja encima sin que el mueble lo tape. + gl.enable(gl.BLEND) + gl.disable(gl.DEPTH_TEST) + drawMesh(tableMesh) + gl.enable(gl.DEPTH_TEST) + gl.disable(gl.BLEND) + gl.clear(gl.DEPTH_BUFFER_BIT) + gl.uniform1f(u.uAlpha, 1) + } + function fly(done) { + if (!peek || peek.dir < 0 || peek.fly) return false + const from = peekPose(peek) + peek.fly = { t: 0, at: performance.now(), from, to: flyTarget(peek.W, peek.H), done, shot: false } + onPeek?.(null) + draw() + return true + } + + // Pantalla: píxeles CSS dentro del lienzo. + const toScreen = (q, m = lastM) => { const [nx, ny] = project(m, q); return [((nx + 1) / 2) * size.w, ((1 - ny) / 2) * size.h] } + function itemRect(it, k) { + const base = rowBase(k), z = it.face ? SHELF.FACE_Z : SHELF.FRONT, [ax, ay] = toScreen([it.x, base + it.h, z]), [bx, by] = toScreen([it.x + it.w, base, z]) + return { l: Math.min(ax, bx), r: Math.max(ax, bx), t: Math.min(ay, by), b: Math.max(ay, by) } + } + // El lomo bajo el dedo; con lomos estrechos, el de centro más cercano. + function pickAt(px, py) { + if (!lastM) return null + let best = null + for (const b of built.values()) for (const it of rows[b.k] || []) { + const r = itemRect(it, b.k), slack = 4 + if (px < r.l - slack || px > r.r + slack || py < r.t || py > r.b) continue + const d = Math.abs(px - (r.l + r.r) / 2) / Math.max(1, r.r - r.l) + if (!best || d < best.d) best = { it, k: b.k, d } + } + return best + } + function peekRect() { + if (!peekM || !peek) return null + const { pos } = peekPose(peek), [ax, ay] = toScreen([pos[0] - peek.W / 2, pos[1] + peek.H / 2, pos[2]], peekM), [bx, by] = toScreen([pos[0] + peek.W / 2, pos[1] - peek.H / 2, pos[2]], peekM) + return { l: Math.min(ax, bx), r: Math.max(ax, bx), t: Math.min(ay, by), b: Math.max(ay, by) } + } + function onPeekBook(px, py) { + const r = peekRect() + return !!r && px >= r.l && px <= r.r && py >= r.t && py <= r.b + } + function onTap(cx, cy, wasMoving) { + const r = canvas.getBoundingClientRect(), px = cx - r.left, py = cy - r.top + if (peek?.fly) return + if (peek) { + if (peek.dir > 0 && peek.t >= 1 && onPeekBook(px, py) && onOpen) return onOpen(peekInfo(peek.it)) + return unpeek() + } + if (wasMoving) return // ese toque solo frenó la inercia + const hit = pickAt(px, py) + if (hit) startPeek(hit.it, hit.k) + } + + // ----- Gestos: un dedo recorre, dos acercan ----- + const pointers = new Map() + let pinch = null, track = null, tap = null + const unitsPerPx = () => visibleH / Math.max(1, size.h) / zoom + const spread = () => { const [a, b] = [...pointers.values()]; return { d: Math.hypot(a.x - b.x, a.y - b.y), cx: (a.x + b.x) / 2, cy: (a.y + b.y) / 2 } } + function onDown(e) { + pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) + try { canvas.setPointerCapture(e.pointerId) } catch {} + tap = pointers.size === 1 ? { x: e.clientX, y: e.clientY, t: e.timeStamp, moving: !!velocity } : null + focusId = null + velocity = 0 + track = null + if (pointers.size === 2) { const s = spread(); pinch = { ...s, zoom } } + else if (pointers.size === 1) track = { x: e.clientX, y: e.clientY, t: e.timeStamp, v: 0 } + draw() + } + function onMove(e) { + const p = pointers.get(e.pointerId) + if (!p) return + const dx = e.clientX - p.x, dy = e.clientY - p.y + p.x = e.clientX + p.y = e.clientY + if (tap && Math.hypot(e.clientX - tap.x, e.clientY - tap.y) > 8) tap = null + if (peek) return // con un tomo fuera, el mueble se queda quieto + if (pinch && pointers.size >= 2) { + const s = spread() + setZoom(pinch.zoom * (s.d / Math.max(1, pinch.d)), s.cx, s.cy) + return + } + if (pointers.size !== 1) return + y += dy * unitsPerPx() + x -= dx * unitsPerPx() + clampView() + if (track) { + const dt = Math.max(1, e.timeStamp - track.t) + track.v = track.v * 0.6 + ((dy * unitsPerPx()) / dt) * 0.4 + track.t = e.timeStamp + } + draw() + } + function onUp(e) { + if (!pointers.delete(e.pointerId)) return + if (pointers.size < 2) pinch = null + if (tap && !pointers.size && e.type === 'pointerup' && e.timeStamp - tap.t < 450) { const { moving } = tap; tap = null; track = null; return onTap(e.clientX, e.clientY, moving) } + tap = null + if (!pointers.size && track && e.timeStamp - track.t < 80 && Math.abs(track.v) > 0.0002) { + velocity = track.v + lastMove = performance.now() + draw() + } + if (pointers.size) track = null + } + canvas.addEventListener('pointerdown', onDown) + canvas.addEventListener('pointermove', onMove) + canvas.addEventListener('pointerup', onUp) + canvas.addEventListener('pointercancel', onUp) + canvas.addEventListener('wheel', e => { + e.preventDefault() + if (peek) return + focusId = null + velocity = 0 + if (e.ctrlKey) return setZoom(zoom * Math.exp(-e.deltaY * 0.01), e.clientX, e.clientY) // pellizco en el panel táctil + y -= e.deltaY * unitsPerPx() + x += e.deltaX * unitsPerPx() + clampView() + draw() + }, { passive: false }) + const onVis = () => { if (document.visibilityState === 'visible') { if (!still) tilt.permit() } else tilt.listen(false) } + document.addEventListener('visibilitychange', onVis) + + const ro = new ResizeObserver(() => { layout(); draw() }) + ro.observe(container) + canvas.addEventListener('webglcontextlost', e => { + e.preventDefault() + built.clear(); covers.clear(); furniture = null; rowsKey = '' + if (peek) { const f = peek.fly; peek = null; onPeek?.(null); f?.done?.(null) } // sus mallas y texturas se perdieron con el contexto + tableMesh = null; tableTex = null + }) + canvas.addEventListener('webglcontextrestored', () => { initGL(); layout(); draw() }) + initGL() + layout() + if (!still) tilt.permit() + draw() + + const api = { + setWorks(next) { works = next; api.relayout() }, + relayout() { + clearTimeout(layoutTimer) + layoutTimer = setTimeout(() => { + if (dead) return + if (peek) { deferLayout = true; return } // con un tomo fuera, el mueble no se reordena hasta que vuelva + layout(); draw() + }, 350) + }, + setVolumes(id, vols) { + const w = works.find(x => x.id === id) + if (!w) return + w.vols = vols + api.relayout() + }, + unpeek, + setLogo, + fly, + focus(id) { focusId = id; layout(); draw() }, + peeked: () => (peek && peek.dir > 0 ? peekInfo(peek.it) : null), + // Centro en pantalla de un tomo (para las pruebas y para enfocar). + screenOf(id, vol) { + if (!lastM) return null + for (const b of built.values()) for (const it of rows[b.k] || []) if (it.work.id === id && (vol === undefined || it.vol.vol === vol)) { const r = itemRect(it, b.k); return { x: (r.l + r.r) / 2, y: (r.t + r.b) / 2, w: r.r - r.l, h: r.b - r.t } } + return null + }, + scrollTo(next) { velocity = 0; y = next; clampView(); draw() }, + setZoom, + draw, + // Para las pruebas. + debug: () => ({ faces: Object.entries(palettes).filter(([k, v]) => k.startsWith('face:') && v).length, facesAsked: Object.keys(palettes).filter(k => k.startsWith('face:')).length, faceWaiting: faceWaiting.length, faceBusy, faceWorker: faceWorker === false ? 'off' : !!faceWorker, rows: rows.map(r => r.map(i => ({ id: i.work.id, vol: i.vol.vol, x: i.x, w: i.w, h: i.h, d: i.d, face: i.face }))), rowWidth, y, x, zoom, velocity, top, bottom, peek: peek && { id: peek.it.work.id, vol: peek.it.vol.vol, t: peek.t, dir: peek.dir, rank: peek.rank, rect: peekRect(), fly: peek.fly ? peek.fly.t : null }, focusId, notes: [...built.values()].map(b => ({ k: b.k, notes: (b.notes?.list || []).map(n => ({ id: n.work.id, x: n.x, w: n.w })) })), logos: logos.size, tilt: { ...tiltNow }, listening: tilt.listening, arts: arts.size, panels: panels.size, built: [...built.keys()], covers: covers.size, palettes: Object.keys(palettes).length, waiting: waiting.length, busy: imgBusy, ctx: !gl.isContextLost() }), + destroy() { + if (faceWorker) faceWorker.terminate() + dead = true + cancelAnimationFrame(raf) + clearTimeout(layoutTimer) + clearTimeout(saveTimer) + ro.disconnect() + tilt.stop() + document.removeEventListener('visibilitychange', onVis) + try { localStorage.setItem(STORE, JSON.stringify(palettes)) } catch {} + freePeek() + freeMesh(tableMesh) + if (tableTex) gl.deleteTexture(tableTex) + dropAllRows() + for (const t of covers.values()) gl.deleteTexture(t) + covers.clear() + freeMesh(furniture) + gl.deleteTexture(wood) + gl.deleteProgram(prog) + gl.getExtension('WEBGL_lose_context')?.loseContext() + canvas.remove() + }, + } + canvas.shelf = api + return api +} diff --git a/app/public/style.css b/app/public/style.css index e4d5e54..8bc42d4 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -460,6 +460,11 @@ body.reading #netbanner { display: none; } @media (max-width: 640px) { .kv { grid-template-columns: 1fr; gap: 0; } .kv dd { margin-bottom: 8px; } .banner { height: 120px; } } .chapters .meta .cdate.approx { opacity: .7; } /* Libro 3D: el canvas dibuja el libro y #pages sigue debajo de los dedos, invisible, para los gestos. */ +#shelf3d { position: fixed; inset: 0; z-index: 0; touch-action: none; background: #17100b; } +#shelf3d canvas { display: block; width: 100%; height: 100%; } +.shelfback { position: fixed; z-index: 2; top: calc(10px + env(safe-area-inset-top)); left: calc(10px + env(safe-area-inset-left)); } +.shelfnote { position: fixed; z-index: 2; left: 50%; bottom: calc(14px + env(safe-area-inset-bottom)); transform: translateX(-50%); margin: 0; padding: 6px 12px; max-width: calc(100vw - 32px); border-radius: 8px; background: rgb(0 0 0 / .62); color: #fff; font-size: .85rem; text-align: center; } +.libhead { justify-content: space-between; align-items: center; } #b3d { position: fixed; inset: 0; z-index: 0; } #b3d[hidden] { display: none; } #b3d canvas { display: block; width: 100%; height: 100%; } @@ -481,3 +486,19 @@ body.reading #netbanner { display: none; } .reader.b3d.b3dcrop { background: #000; } .reader.b3d.b3dcrop > #b3d { display: none; } .reader.b3d.b3dcrop > #pages img { visibility: visible; } +/* Vistazo del estante: junto al tomo sacado, a la derecha en apaisado y abajo en vertical */ +.shelfpeek { position: fixed; z-index: 3; right: calc(4vw + env(safe-area-inset-right)); top: 50%; width: min(38vw, 380px); transform: translate(12px, -50%); opacity: 0; transition: opacity .22s, transform .22s; padding: 18px 18px 16px; border-radius: 14px; background: rgb(20 14 10 / .78); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); color: #f4ede4; box-shadow: 0 10px 30px rgb(0 0 0 / .4); } +.shelfpeek.on { opacity: 1; transform: translate(0, -50%); } +.shelfpeek .ptitle { margin: 0 34px 4px 0; font-size: 1.2rem; line-height: 1.25; } +.shelfpeek .psub, .shelfpeek .pwhere { margin: 4px 0; color: #cdbfae; font-size: .9rem; } +.shelfpeek .pbar { height: 4px; margin: 12px 0 6px; border-radius: 2px; background: rgb(255 255 255 / .16); overflow: hidden; } +.shelfpeek .pbar i { display: block; height: 100%; background: var(--accent); } +.shelfpeek .pacts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; } +.shelfpeek .pacts .btn { flex: 1 1 auto; justify-content: center; } +.shelfpeek .peekx { position: absolute; top: 10px; right: 10px; width: 32px; height: 32px; display: grid; place-items: center; border: 0; border-radius: 50%; background: rgb(255 255 255 / .1); color: inherit; cursor: pointer; } +@media (orientation: portrait) { + .shelfpeek { left: calc(12px + env(safe-area-inset-left)); right: calc(12px + env(safe-area-inset-right)); top: auto; bottom: calc(12px + env(safe-area-inset-bottom)); width: auto; transform: translateY(12px); } + .shelfpeek.on { transform: none; } +} +.handoff { position: fixed; inset: 0; z-index: 60; width: 100vw; height: 100vh; object-fit: cover; pointer-events: none; transition: opacity .35s ease; } +.handoff.out { opacity: 0; } diff --git a/app/public/sw.js b/app/public/sw.js index bc4a48a..b0783e6 100644 --- a/app/public/sw.js +++ b/app/public/sw.js @@ -2,7 +2,7 @@ // dispositivo" (caché OFFLINE, la llena app.js) se leen sin red. const SHELL = 'shell-v1' const OFFLINE = 'offline-v1' -const SHELL_FILES = ['/', '/app.js', '/style.css', '/book3d.js', '/icon.svg', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png'] +const SHELL_FILES = ['/', '/app.js', '/style.css', '/book3d.js', '/shelf3d.js', '/animeface.js', '/animeface-cascade.json', '/fonts/caveat.woff2', '/fonts/patrickhand.woff2', '/fonts/kalam.woff2', '/fonts/gochihand.woff2', '/fonts/indieflower.woff2', '/fonts/permanentmarker.woff2', '/fonts/shadows.woff2', '/icon.svg', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png'] // Se guarda la portada tal como la sirve el servidor y justo las versiones de app.js y style.css que // enlaza (?v=…); cache: 'reload' para no copiar lo que tenga la caché HTTP (un proxy puede alargarla horas). @@ -21,7 +21,7 @@ self.addEventListener('install', e => { }) self.addEventListener('activate', e => { e.waitUntil(caches.keys() - .then(keys => Promise.all(keys.filter(k => k !== SHELL && k !== OFFLINE).map(k => caches.delete(k)))) + .then(keys => Promise.all(keys.filter(k => k !== SHELL && k !== OFFLINE && !k.startsWith('shelf-')) // shelf-*: lo que guarda el estante (shelf3d.js).map(k => caches.delete(k)))) .then(() => self.clients.claim())) }) diff --git a/app/server.js b/app/server.js index 6d05d65..218ded3 100644 --- a/app/server.js +++ b/app/server.js @@ -17,6 +17,7 @@ import { setupConnectivity } from './connectivity.js' import { setupMeta } from './meta.js' import { setupVolumes } from './volumes.js' import { setupCredits } from './credits.js' +import { setupShelf } from './shelf.js' import { setupBooks, isBookId } from './books.js' const PORT = Number(process.env.PORT || 3352) @@ -834,7 +835,7 @@ const requireUser = req => { return u } -const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.webmanifest': 'application/manifest+json', '.png': 'image/png' } +const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.webmanifest': 'application/manifest+json', '.png': 'image/png', '.json': 'application/json', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8' } // index.html enlaza app.js y style.css con ?v=: si un proxy delante cambia el // no-cache por una caché larga (Nginx Proxy Manager con «Cache Assets»), cada versión nueva tiene otra // URL y el navegador no se queda con la vieja. Los archivos no cambian mientras el contenedor vive. @@ -843,6 +844,7 @@ const INDEX_HTML = fs.readFileSync(path.join(PUBLIC_DIR, 'index.html'), 'utf8') .replace('src="/app.js"', `src="/app.js?v=${assetVersion('app.js')}"`) .replace('href="/style.css"', `href="/style.css?v=${assetVersion('style.css')}"`) .replace('', ``) + .replace('', ``) .replace('', ``) function serveStatic(res, pathname) { let file = path.join(PUBLIC_DIR, path.normalize(pathname).replace(/^(\.\.[/\\])+/, '')) @@ -924,6 +926,7 @@ async function route(req, res) { if (p.startsWith('/api/bans') && security.route(p, method, req, res, json) !== false) return if ((await features.route(p, method, q, req, res, user)) !== false) return if ((await credits.route(p, method, req, res, user)) !== false) return + if (shelf.route(p, method, res, user) !== false) return if ((await upscale.route(p, method, req, res)) !== false) { // Pedir el reescalado (o el automático) lo pone en la biblioteca de quien lo pide. if (method === 'POST' && (m = /^\/api\/upscale\/([^/]+)(\/auto)?$/.exec(p)) && db.prepare('SELECT 1 FROM downloads WHERE manga_id = ?').get(m[1])) linkDownload(user.id, m[1]) @@ -996,6 +999,9 @@ async function route(req, res) { if ((m = /^\/api\/volumes\/([^/]+)$/.exec(p)) && MANGA_ID.test(m[1]) && method === 'GET') { return json(res, 200, { volumes: isBookId(m[1]) ? [] : await volumes.list(m[1], lang) }) } + if ((m = /^\/api\/logo\/([^/]+)$/.exec(p)) && MANGA_ID.test(m[1]) && method === 'GET') { + return json(res, 200, { logo: isBookId(m[1]) ? null : await volumes.logo(m[1], lang).catch(() => null) }) + } if ((m = /^\/api\/tome\/([^/]+)\/([^/]+)\/(front|back)$/.exec(p)) && method === 'GET') { let vol try { vol = decodeURIComponent(m[2]) } catch { throw new HttpError(404, 'Cubierta no encontrada') } @@ -1170,6 +1176,7 @@ const library = setupLibrary({ db, HttpError, json, readBody, MANGA_ID, CHAPTER_ net.whenOnline(() => pumpDownloads()) const meta = setupMeta({ db, md, sourceOf, fetchMangaFull, HttpError, json, net, decode, imgUrl, UA, prepareBanner }) const credits = setupCredits({ db, HttpError, json, readBody, MANGA_ID, CHAPTER_ID }) +const shelf = setupShelf({ db, json, isBookId, downloadCover, MY_DOWNLOADS }) const volumes = setupVolumes({ md, sourceOf, fetchMangaFull, stored: meta.stored, mdMatches: meta.mdMatches, muVolumes: meta.muVolumes, similar: meta.similar, miSearch: q => mi.search(q), imgUrl, DATA_DIR, UA, HttpError, net }) const clips = setupClips({ db, DATA_DIR, HttpError, json, readBody, MANGA_ID, CHAPTER_ID, upscale }) diff --git a/app/shelf.js b/app/shelf.js new file mode 100644 index 0000000..f764faf --- /dev/null +++ b/app/shelf.js @@ -0,0 +1,36 @@ +// Estante 3D: las obras de un usuario que pueden ir en el mueble. Los tomos de cada una se piden aparte (/api/volumes), porque pueden tardar. +import { hasNoVolumes } from './volumes.js' + +export function setupShelf({ db, json, isBookId, downloadCover, MY_DOWNLOADS }) { + function works(userId) { + const out = new Map() + const add = (id, fields) => { + const w = out.get(id) || { id, title: '', cover: null, last: 0, chapterId: null, page: 0, lang: null } + w.title ||= fields.title || '' + w.cover ||= fields.cover || null + w.last = Math.max(w.last, fields.at || 0) + if (fields.chapterId) Object.assign(w, { chapterId: fields.chapterId, page: fields.page || 0, lang: fields.lang || null }) + out.set(id, w) + } + for (const r of db.prepare('SELECT manga_id, manga_title, cover, chapter_id, page, lang, updated_at FROM progress WHERE user_id = ?').all(userId)) { + add(r.manga_id, { title: r.manga_title, cover: r.cover, chapterId: r.chapter_id, page: r.page, lang: r.lang, at: r.updated_at }) + } + for (const r of db.prepare('SELECT manga_id, title, cover, last_new_at, created_at FROM follows WHERE user_id = ? AND active = 1').all(userId)) { + add(r.manga_id, { title: r.title, cover: r.cover, at: r.last_new_at || r.created_at }) + } + for (const r of db.prepare(`SELECT manga_id, title, folder, updated_at, json_extract(meta, '$.manga.cover') meta_cover FROM downloads WHERE ${MY_DOWNLOADS}`).all(userId)) { + add(r.manga_id, { title: r.title, cover: downloadCover(r.manga_id, r.folder, r.meta_cover), at: r.updated_at }) + } + for (const r of db.prepare('SELECT i.manga_id, i.title, i.cover, i.added_at FROM list_items i JOIN lists l ON l.id = i.list_id WHERE l.user_id = ?').all(userId)) { + add(r.manga_id, { title: r.title, cover: r.cover, at: r.added_at }) + } + const typeOf = db.prepare('SELECT type FROM manga_meta WHERE manga_id = ?') + return [...out.values()].filter(w => !isBookId(w.id) && !hasNoVolumes(typeOf.get(w.id)?.type)).sort((a, b) => b.last - a.last) + } + function route(p, method, res, user) { + if (p !== '/api/shelf' || method !== 'GET') return false + json(res, 200, { works: works(user.id) }) + return true + } + return { route, works } +} diff --git a/app/volumes.js b/app/volumes.js index cef66dd..a8f05be 100644 --- a/app/volumes.js +++ b/app/volumes.js @@ -454,6 +454,28 @@ export function workTitle(title) { /** Manhwa y manhua no se publican por tomos: se leen por capítulos. */ export const hasNoVolumes = (type, original) => ['manhwa', 'manhua', 'webtoon'].includes(String(type || '').toLowerCase()) || ['ko', 'zh', 'zh-hk'].includes(original) +// Palabras de un título para comparar con nombres de archivo: sin tildes ni vocales largas («Sōsō» y «Sousou» son «soso»). +const LOGO_STOP = new Set(['the', 'and', 'wa', 'no', 'ga', 'wo', 'ni', 'de', 'to']) +const LOGO_EXTRA = new Set(['logo', 'logotype', 'wordmark', 'manga', 'anime', 'series', 'title', 'english', 'japanese', 'png', 'svg', 'webp', 'file', 'the', 'and', 'tv', 'version', 'new', 'old', 'int', 'international', 'with', 'colour', 'color', 'gradient', 'variant', 'alt', 'alternative', 'text', 'black', 'white', 'transparent', 'vector', 'official']) +export const logoWords = s => String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/ou/g, 'o').replace(/uu/g, 'u').replace(/oo/g, 'o').split(/[^\p{L}\p{N}]+/u).filter(w => w.length >= 2 && !LOGO_STOP.has(w) && !/^\d+$/.test(w)) +/** El archivo que mejor encaja con algún nombre de la obra: tiene «logo», casi todas las palabras del nombre y nada más que relleno. */ +export function pickLogo(files, names) { + let best = null, bestScore = 0 + files.forEach((file, i) => { + if (!/\.(png|svg|webp)$/i.test(file)) return + const fw = logoWords(file.replace(/^File:/, '')) + if (!fw.includes('logo')) return + names.forEach((name, k) => { + const nw = logoWords(name) + if (!nw.length || nw.join('').length < 4) return + const hit = nw.filter(w => fw.includes(w)).length / nw.length + const stray = fw.filter(w => !nw.includes(w) && !LOGO_EXTRA.has(w)) + const score = hit + (k ? 0 : 0.05) - i * 0.001 // el nombre principal manda + if (hit >= 0.66 && !stray.length && score > bestScore) { best = file; bestScore = score } + }) + }) + return best +} export const wikiKey = t => String(t).toLowerCase().replace(/×/g, 'x').replace(/[^\p{L}\p{N}]/gu, '') export function setupVolumes({ md, sourceOf, fetchMangaFull, stored, mdMatches, muVolumes, similar, miSearch, imgUrl, DATA_DIR, UA, HttpError, net }) { @@ -642,6 +664,44 @@ export function setupVolumes({ md, sourceOf, fetchMangaFull, stored, mdMatches, return inflight.get(key) } + // ---------- Logo de la obra (Wikimedia Commons), para el título del lomo ---------- + const COMMONS = 'https://commons.wikimedia.org/w/api.php' + let commonsRest = 0 // si Commons pide calma, no se le pregunta nada en un rato (y no se guarda el fallo) + async function commonsGet(params) { + if (Date.now() < commonsRest) throw new HttpError(503, 'Wikimedia Commons pidió esperar') + await turn() + net.guard() + const r = await fetch(`${COMMONS}?${new URLSearchParams({ ...params, format: 'json', formatversion: '2', maxlag: '5' })}`, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(30000) }).catch(() => { net.failed(); return null }) + const text = r?.ok ? await r.text() : '' + if (r?.status === 429 || (r?.ok && !text.startsWith('{')) || /"code":\s*"maxlag"/.test(text)) commonsRest = Date.now() + 10 * 60e3 + const data = text.startsWith('{') ? JSON.parse(text) : null + if (!data || data.error) throw new HttpError(502, 'No se pudo consultar Wikimedia Commons') + return data + } + async function commonsLogo(names) { + const queries = [...new Set([...names.slice(0, 3), ...names.slice(0, 1).flatMap(n => logoWords(n).sort((a, b) => b.length - a.length).slice(0, 1))])] + for (const q of queries) { + const res = await commonsGet({ action: 'query', list: 'search', srnamespace: '6', srsearch: `${q} logo`, srlimit: '15' }) + const best = pickLogo((res.query?.search || []).map(h => h.title), names) + if (!best) continue + const info = await commonsGet({ action: 'query', titles: best, prop: 'imageinfo', iiprop: 'url|size|mime', iiurlwidth: '960' }) // Wikimedia solo sirve miniaturas de ciertos anchos (960 sí, 800 no) + const ii = info.query?.pages?.[0]?.imageinfo?.[0] + if (ii && ii.width >= ii.height * 1.2) return ii.thumburl || ii.url || null // un logotipo es apaisado + } + return null + } + // Una vez por obra; lo no encontrado se vuelve a buscar a la semana. + function logo(mangaId, lang) { + return stored(`logo:${mangaId}`, v => (v ? 60 : 7) * DAY, async () => { + const { manga } = await fetchMangaFull(mangaId, lang) + if (hasNoVolumes(manga.type)) return null + const ids = sourceOf(mangaId) === 'md' ? [mangaId] : (await mdMatches(mangaId).catch(() => [])).slice(0, 1) + const data = ids.length ? await mdVolumes(ids[0], lang).catch(() => null) : null + const names = [...new Set([workTitle(manga.title), manga.title, ...(data?.enTitles || []), ...(data?.roTitles || [])])].filter(Boolean) + return commonsLogo(names) + }, true) + } + // ---------- API ---------- async function list(mangaId, lang) { const { manga, chapters } = await fetchMangaFull(mangaId, lang) @@ -680,12 +740,16 @@ export function setupVolumes({ md, sourceOf, fetchMangaFull, stored, mdMatches, if (!groups.has(vol)) groups.set(vol, []) groups.get(vol).push(chapters[i].id) }) + // Páginas del tomo, para el grosor del lomo en el estante. Si falta algún capítulo por contar, no se da. + const pagesOf = ids => { let n = 0; for (const id of ids) { const p = chapters.find(c => c.id === id)?.pages; if (!(p > 0)) return null; n += p } return n } return [...groups.keys()].sort(volOrder).map(vol => { const cover = data?.covers[vol] return { vol, chapters: groups.get(vol), + pages: pagesOf(groups.get(vol)), thumb: cover ? imgUrl(`https://uploads.mangadex.org/covers/${mdId}/${cover.file}.256.jpg`) : null, + art: cover ? imgUrl(`https://uploads.mangadex.org/covers/${mdId}/${cover.file}.512.jpg`) : null, // para sacar el arte del lomo front: mdId && VOL.test(vol) ? `/api/tome/${mdId}/${encodeURIComponent(vol)}/front` : null, back: mdId && VOL.test(vol) ? `/api/tome/${mdId}/${encodeURIComponent(vol)}/back` : null, } @@ -702,5 +766,5 @@ export function setupVolumes({ md, sourceOf, fetchMangaFull, stored, mdMatches, res.writeHead(200, { 'Content-Type': imageKind(head) || 'application/octet-stream', 'Cache-Control': 'private, max-age=2592000', 'X-Content-Type-Options': 'nosniff' }) fs.createReadStream(file).pipe(res) } - return { list, sendFace } + return { list, sendFace, logo: async (mangaId, lang) => { const u = await logo(mangaId, lang); return u ? imgUrl(u) : null } } }