tag:blogger.com,1999:blog-335347822024-02-13T10:28:48.040+01:00WangLu's Notes久病成医 | Prolonged Illness Makes the Patient a Good DoctorLu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.comBlogger380125tag:blogger.com,1999:blog-33534782.post-29175943780099730462024-02-12T23:22:00.002+01:002024-02-13T10:28:15.702+01:00Live Migrate Ubuntu 22.04.3 to Debian 12.5 on Raspberry Pi 4<p>I had a Ubuntu 22.04.3 on a Raspberry Pi 4. Recently I decided to switch it to Debian 12.5 <b>in-place</b>.</p><p>Mostly I was following this script: <a href="https://github.com/alexmyczko/autoexec.bat/blob/master/config.sys/ubuntu-deluxe">https://github.com/alexmyczko/autoexec.bat/blob/master/config.sys/ubuntu-deluxe</a> </p><p>It worked surprisingly well, however, the system was eventually broken (unsurprisingly) near the end, when I tried to upgrade the kernel.</p><p><br /></p><p>I tried to fix initramfs by copying dtbs files, which seemed to work. Some references:</p><p><a href="https://qiita.com/takasan/items/ef93be9e9d3f791eee66">https://qiita.com/takasan/items/ef93be9e9d3f791eee66</a></p><p><a href="https://bugs.launchpad.net/ubuntu/+source/flash-kernel/+bug/2012750">https://bugs.launchpad.net/ubuntu/+source/flash-kernel/+bug/2012750</a></p><p><br /></p><p>But the kernel did not load after reboot. Reverting /boot/config.txt didn't help.</p><p>Then I downloaded a Debian image and replaced only the boot partition of the micro SD card. This time the kernel was able to boot, but it couldn't load the filesystem.</p><p><br /></p><p>I modified cmdline.txt, replaced "root=LABEL=RASPIROOT" with "root=/dev/mmcblk1p2", such that the kernel was able to load the filesystem. But a new error appeared: Cannot open access to console, the root account is locked.</p><p><br /></p><p>At this point I found the process no longer fun, because it was such a pain to modify anything in the boot partition (power off Raspberry Pi, unplug the micro SD card and plug it into a PC, edit, unplug the micro SD card and plug it into Raspberry Pi, power on Raspberry Pi).</p><p><br /></p><p>Eventually I just installed formatted the micro SD card, installed the Debian image and reconfigured the system. It was actually not slower than the in-place process.</p><p><br /></p><p>I'd the say the ubuntu-deluxe script works pretty well. Most of the time I was just dealing with the difference between both distos (e.g. config files). Later I learned that the Ubuntu and Debian images used different methods for booting up Raspberry Pi. </p><p>So theoretically it is possible to migrate from Ubuntu to Debian inplace. In fact there is a <a href="https://wiki.debian.org/DebianTakeover">debtakeover </a>script, which allows migrating to Debian from many other distros. On the other hand, normally it might make more sense to just reinstall the system.</p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-72908115097010622822024-01-24T20:51:00.005+01:002024-01-25T01:09:42.967+01:00测试rclone和restic<p>最近忽然想试试restic,看看几年后有什么变化。</p><p>实际上之前做数据备份计划时大致比较了rclone和restic,经过简单测试以后决定用rclone。当时对restic印象一般,主要考虑两点:</p><p>1. restic是专有格式,没有程序文件的话无法访问备份的数据。</p><p>2. 之前测试restic的时候莫名其妙备份仓库数据有损坏。</p><p>不过用了rclone几年以后发现第一条对rclone的加密仓库也成立。</p><p><br /></p><h2 style="text-align: left;">测试数据</h2><p>我挑选了一些数据,主要是图片和视频,分别用restic和rclone备份。</p><p>数据包含121191个文件,总共3.869TiB。由于ZFS压缩,实际占用磁盘3.6TiB。</p><div><br /></div><h2 style="text-align: left;">测试环境</h2><p>原始数据保存在一台Ubuntu 22.04.3的机器上,文件系统使用ZFS,启用zstd压缩,没有开启去重。</p><p>备份机器是一台群晖的机器,文件系统使用ext4.</p><p>两台机器用千兆网连接。在源机器上通过sftp访问备份机器。</p><p><br /></p><div><br /></div><h2 style="text-align: left;">restic备份</h2><p style="text-align: left;">restic版本 restic 0.16.3 compiled with go1.21.6 on linux/amd64<br />备份仓库用默认参数创建。</p><div style="text-align: left;"><br /><br />最终是用了42个半小时结束。restic报告是3.655 TiB added to repo, 3.531 TiB stored on disk。备份仓库里最后有220568个文件。<br /><br /><br /><br />我对结果比较惊讶,因为按这个结果看数据里有大概200GiB的重复数据块,我没想到有这么多。<br />不过最后占用磁盘大小跟原始文件在ZFS上的占用大小差不多,我也没想到。按理说已经去掉了200GiB文件,并且ZFS和restic我都用的zstd默认压缩率(也许它们的默认压缩率差很多?),我本来以为restic的仓库要再小点。<br /><br /><br />效率方面感觉42个多小时也太慢了点。不过我没找到性能瓶颈,很奇怪:<br /><br /><br />- 两台机器CPU占用率都不高<br /> - 源机器上CPU有8核,但是restic基本用不到200%<br />- restic要调用ssh访问sftp,ssh大概也占用10%CPU<br />- 网络数据量大概50MB/s,这感觉很低<br />- 磁盘也没有充分利用。ZFS那边我见过几百MB/s的速度,而群晖那边也显示磁盘使用率在一半左右。<br /><br /><br />所以最后我也没搞清楚原因。<br /><br /><br />还有一个问题,restic经常跑跑停停,有时看似完全休眠了一般,CPU,磁盘,网络完全没有动静,不知道什么情况。</div><div><span face="Arial, sans-serif" style="color: #222222;"><span style="background-color: white; font-size: 15.008px;"><br /></span></span></div><div><img height="381" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA6sAAAN/CAYAAAA4TU68AAAAAXNSR0IArs4c6QAAIABJREFUeF7svV9oG9e+9/3tzfsc2MQ9JRHB5LVn+zxETsJ+ptGh8UuoY1wwCYQatoOOLlqwQgqiCYJ9U0OMHIKphQPuTUHsFEFKZHAuXBNvcCivQ6DGdgjbedhxTUljh3O8J35C2KQhpwkPnOe96cvM6M9IGklLoxlpZvTVTRz7N2v9fp+1Zs18tdZvrXcA/AZ+SIAESIAESIAESIAESIAESIAESMBFBN6hWHVRa9AVEiABEiABEiABEiABEiABEiABjQDFKjsCCZAACZAACZAACZBAlsB+/Mvxd/Hr5n/gFZmQAAmQQFMJlI8/FKtNbQBWRgIkQAIkQAIkQAJuJkCx6ubWoW8k4G8CFKv+bl9GRwIkQAIkQAIkQAINEaBYbQgfLyYBEmiAAMVqA/B4KQmQAAmQAAmQAAn4nQDFqt9bmPGRgHsJUKy6t23oGQmQAAmQAAmQAAm0nADFasubgA6QgAcJyLKMra2tBj2nWG0QIC8nARIgARIgARIgAT8TqCVWZYRTSUz0d+kQ9tYxl0ljZkF/SZXHUkgOSEWAlNUM0jMLyFpgLJWEpCQQnzG82IZTWBpUkIjPZO38zLh9Y+uNfIzIZ0fQCeDFyirmpzewXYoj1IcLFwfwgdqNlCf4y/U7uPuoYJTrY8pqSR+CDBlb7D9C3SuEE0MX8W6J7a8/xvDwZeGXgeA43u8eREcH8ObNCp79OI0dw991yzBSS4NQMgqkqISV4TgWynyoPm4UzClWhZqPRiRAAiRAAiRAAiTQngSqiVX1pXQC/ZjDVGIGqj6Vw2NIRoHEsC4y5bElzA6sYiqxjB0NYBBnkhP4FHMY1WxkjC3NYmB1FMMlYnUzqmRtRMlLkEImto8UKPlfi9j8M/75n03K+c//xH/mf93ONqLtUc1Owun057gobeP65R+g4CA+vDaCYdzH5TN3DIK1D1d+HgFmv8H8/wvg8Ef40+QB/M9PvsK3qmANp7A5uILj8QWEU5sYXDmOuKaM1H6VhJQZzv7fDp99XEZgHOfODgLPFbwxhGkUq8GhZZzsUPD4p+v4+2vgvfev4eQh4PH3Z4oErdomKcR17saf8+XWHjcoVn3c1xgaCZAACZAACZAACdhHoLJYzQlRXXSaf0xt5DEszQ5gdXQYM1v2idW+9DRGTpn4sbaI8diG9gcRm0OhIRzdb1LOq59x79Fz7Q/tbGNb3wpJwKPC1whAVpheHceX89laIuex+NkvRQK2d/wL/Anf4dK0on0ZEtvNClJ1Nr4nrX3pEU4tIaokir8Asc1xHxakidUe/JSJZb9UEokxhBPnbqFr7xPcflg01Y2l2C4S6R0EY0n0pNX7vFCeyLhBsSrCnzYkQAIkQAIkQAIk0PYEKonVCiKzhBfFqv9Er3O3hIQLy5/j0A2DWA19jD/fOoC/HL2Ju9mKT6en8cfdb/JiNQldlKp9Tf05gaT2b9FMvXNO+6PkYBrRP+zi+9vTKFvVWzHCCIaik8CDo7inL5swKNIwxmI92E0vY6Eob1Vs3KBY9Ue3YhQkQAIkQAIkQAIk4DCBSmI1jNTmBDCVW3Zp7ka5WFVz1WYxIdm/DFhk1lTEpp1nTUVid6zDqbOok8B1gzBV61JnUq+NAn+bXcVf//0Y/vjZL/g6v1RYXVIaBRQFkgRkTPMjHfPYPwWrYvWkvizhLYB96r/Pr2Lt3nyJeA0hEDyM33cPoatDwpufLuPejmFWtSYRsXGDYrUmSBqQAAmQAAmQAAmQAAkANojVT7ObL2Vx7q3PGTZO4jJgry05duauMFkCrFUk4UL633Bo9wme9xzBB6f2A2uL+DpWvBGTcefZyhsuOeO5P0oNIRAAXr7MCs9ABENnJ9HxuGSJLwobMXV0SMDeDaw9LBW01YhQrPqjvzAKEiABEiABEiABEnAFAQeWARfFRbFKsaov//1gRV/aa/yoS37/n3uGZcHIbsyERYxk85A1e3Ujn5404ruxChsuueJm8pQTgRPLONu1UmVpcIWc1apRchmwpzoBnSUBEiABEiABEiABdxOovMGSuvtqYTmveRQim6mo5USV4t2AteukjLbLq+hHZImviI3IUth2thFtDzG7PlxZHkGniVDVN1wawPPczr+5AkvzWNWNlaKKtgM1Kmy4JOaLB6xyy3XfziJze9pRh2uLVUDEptRJkXGjcA2PrnG0kVk4CZAACZAACZAACXibQI2jazYnIK1PIRHPnpsqh5FKDmIlEdePsskeXVNtx2Dt+JEJYG40ru8aqpYxWzsftpyryLE0IjbtfCyNSOw29ehQH67cGkHnbPmMql6D+YyrlsM6+ETfIVjdWTop5XNVc5ssGTdc8tNGS5o4PNYNYA2ZTMyuhtDOWFWPqcmfmVq2DFhf/otn1/Ewl6NacalwLbf0pcDVxg2K1VoM+XcSIAESIAESIAESIAFUzlnNwtHE6QT682mpe1ifSyCePatCSKxqqziXMFEoBHvrUxiuY1aVTeU1AroQHVZ1V+nnmfGsVX3m9V+7X+HFMwDd+9H5TD2b9SbuaumVMmR5C4UNZ/2+4VIIweBh4PV8QVja0PSBYBqnTp7CPjzDW3Rr/z5/fBn3DEfSFNm87ca+fZU2YRJwqMa4QbEqwJAmJEACJEACJEACJEAC1WZWjXRU0QBsFR1TUT8940Y59V/NK/xLQEJvCNq5rNsCQbIfCUAyMwmEEIBhoyXTYko2Y7JYlX5ZrXGDy4AbwsuLSYAESIAESIAESMDfBETFqr8pMLr2IJBfZvv8KjL35gFDjmjuDFK32Dids+qOFqdYdUc70AsSIAESIAESIAEScCUBilVXNgudcoRAcOhnnDykHjI6q+2CC0OO6INMDDsA3GJjX86qIyhtKpRi1SaQLIYESIAESIAESIAE/EiAYtWPrcqYKhHQc0Jfv57Hy5eqTQiB4GG8V5Qj6jYbP7cmxaqfW5exkQAJkAAJkAAJkECDBChWGwTIy0mABCwToFi1jI4XkgAJkAAJkAAJkID/CVCs+r+NGSEJuJUAxapbW4Z+kQAJkAAJkAAJkAAJkAAJkAAJGAi8A+A3EiEBEiABEiABEiABEiABEiABEiABNxGgWHVTa9AXEiABEiABEiABEmgpAS4Dbil+Vk4CbU2Ay4DbuvkZPAmQAAmQAAmQAAlUJ0Cxyh5CAiTQKgIUq60iz3pJgARIgARIgARIwAMEKFY90Eh0kQR8SoBi1acNy7BIgARIgARIgARIwA4CFKt2UGQZJEACVghQrFqhxmtIgARIgARIgARIoE0IUKy2SUMzTBKwlYAsy9ja2mqwTIrVBgHychIgARIgARIgARLwM4FaYlVGOJXERH+XDmFvHXOZNGYWDC+pchhjsSgGJADKKjLpGRj/7Gd6jK06gd7Ix4h8dgSdAF6srGJ+egPbRZdIuJD+CIdKinl+/Sa+faT/Uh5LITkgQVlNID5jFEcyZGyhUbnUHm0Ywomhi3i3JNhff4zh4cvCLwPBcbz/h0F0AHizdwM/PpyH4c9ZwzBSS4NQMgqkqISV4TgWyiAKjBvaNRSr7dH/GCUJkAAJkAAJkAAJWCJQTayqL6UT6MccphK6AJXDY0hGgcTwTFYkhJHanIC0PoVEegfBmCpsFUwdN3uBteQgL/IkAQmn05/jorSN65d/gIKD+PDaCIZxH5fP3CkI1tDH+POtI8DaL3hhiDMvVsMpbA6u4Hh8AeHUJgZXjiOuKSMZY0tJSJnh7P89Cal5TgfGce7sIPBcwRtDrQWxGkJw6BZOdqzhwdp1vMZh/P7UJI5hFt/fni4WrOEUUojr3I0/58sVGTdyxhSrzesErIkESIAESIAESIAEPEegsliVx5YwO7CK0bwwLQ9Os5EympjIfVRREVVGMVw0C+Y5MHS4UQIhCXikGErpw5WfR4Cr4/hyPvtrTawewF+O3sRdk/rU/hXbzQrScApLPWmtX4VTS4gqCfYx0TbSxGoPfsrEsFPpmkAIeJmdztZsIhiKTgIPjuKe8SJ5DEux3fyXUz3pYRhvdZFxo+ACxapoE9KOBEiABEiABEiABNqQQCWxqs5czWJgtZro1G2kTG62K4tPnQ2LKlVFbhuCZsiQcGH5cxy6YRCrkfNY/OyX4tlWAylV+CShi9Lczwkk878jVEECwTSif9gtnyWtenkIJ87dwrs/lYhV9Rpt6X8PdtPLWCjKWxUZN4yVUqwKtiDNSIAESIAESIAESKAdCVQSq/ryXkyVCNEiRKpNFMroMGaCY1iaGMDq1DBmdsawNCshw6XA7dihKsesCtNJ4LpxFlX7Xa92jboMWMttXVvE17Fcbqu6pDQKKAokCciY5kcSc00Cqlg9eUozewtgn/rv86tYu2eWk5otTbsGeFBtNrasYpFxg2K1ZnvRgARIgARIgARIgARIQCVgl1hNYXOiH+uquKVYZdcqI2CyBFizkdAbArZzy4VDfbhyawSds9/g0nRhCbFx59nKGy4Re2UCIQQCwMvcMt9ABENnJ9Hx+BPcfmhc+psrocIS4JqIKVZrIqIBCZAACZAACZAACZCAKAEuAxYlRTurBPTlvx+sFAvQSqX1jn+Ba4NPipcGqxv59KQR341V2HDJqm/te13gxDLOdq2YLA3Wl/927VUSstWYcRlw+/YoRk4CJEACJEACXiNwegg/XzwEvHiMo5cees37NvG38gZL6kZJE9Jc1dxTs82UzDZdainM3FLTZyU70RqdsstGJFCTujSBNrpfWwJ7KbYBiPgjUlfLbfpwZXkEnYJCVXW3TKyqGytFFW0HalTYcKnlYXrQAXOxGsHQuUl0WBKqOgSRcaOAizmrHuw6dJkESIAESIAE/EEgdOEcbg2r2VHPcXTknj+C8l0UNY6uyR1LE1/Qj6qRw0glB7GSiOtnqao7g84OYFXNWzX7vwt45YQgsF2cL2nwzS4bkXDN6jqdnsZFNaUwK6iRFa/VfBapq6U2FZb0FnzSz1j9X9dv4m5uJWrpNWr/Skr5XFWzDZe463StVtbPWFWPqdnJHZpqtgy45tLgWvXk/m44zqrSuJEvimJVlCrtSIAESIAESIAEbCcQQOT0e4Cyg/lt2wtngbYQqCZWc+J0Av1ducr2sD6XQNxwVoWWQ/hpP7AHdHWV/90WNxsqRMLpyEHg6UZBFJWVZ5eNiKNmdZX+TsQfkbpaZaMv/R3uNqnfMMPdGzmPP032ohOv8AL7tX//Nvsdvsznq8qQ5S0UNpzlhktWWjQQTOPUyVPYh2d4i27t3+ePL+NePl9VX/p7TP1usfTz1uSs1VpOaF9qVR839CIoVmuh5N9JgARIgARIgARIoI0J1BCreTKqaAC2io6pKMZm3ASnjYEydCsEQhLUPYHzGy3VKIN9zQpkAIEQAjBstGSxGPHLao0bFKviLGlJAiRAAiRAAiRgLwHmrNrL05HSRMWqI5XXXWh+Ce3aIkYq5HbSZj9gkc/ImTt1t4mXLtDyNI91A8+vInNvHsgd6WKYPXSLTeb2tJfQWvSVYtUiOF5GAiRAAiRAAiTQKAHmrDZKsBnXe0usiuR20qZ67ms1PiNHbzaj07WsjuDQzzh5SD1kVF/aipx4xVr+PFG32GQysZZxal7FFKvNY82aSIAESIAESIAESggEEDr9Hg4zZ9XFPcNbYlU9l1PNP1WebmBb25RHQm/kIKSifFTaNM7HxV22IddCCAYP4/XrebzUNhsKIRA8jPdezxc2H4LbbBoK2OUXU6y6vIHoHgmQAAmQAAmQAAm0koDXxGorWbFuEiABewlQrNrLk6WRAAmQAAmQAAmQgK8IUKz6qjkZDAl4igDFqqeai86SAAmQAAmQAAmQAAmQAAmQQLsSeAfAb+0aPOMmARIgARIgARIgARIgARIgARJwJwGKVXe2C70iARIgARIgARIggRYQ4DLgFkBnlSRAAhoBLgNmRyABEiABEiABEiABEqhIgGKVnYMESKBVBChWW0We9ZIACZAACZAACZCABwhQrHqgkegiCfiUAMWqTxuWYZEACZAACZAACZCAHQQoVu2gyDJIgASsEKBYtUKN15AACZAACZAACdhB4PR5LF7sBV7cx8ilO3aUWLmMZtblbCRNLp1itcnAWR0J+IKALMvY2tpqMBaK1QYB8nISIAESIAESIAGrBHovfIFrw/sBbGNk5KbVYoSua2ZdQg55xqiWWJURTiUx0d+lR7S3jrlMGjMLpS+pMsJjMfTsmv3NMzDoqM0EeiMfI/LZEXQCeLGyivnpDWyb1FHNTh5LITkgQVlNID5j7HcyZGyhUblkc8ieKC74cRrduI57dx4V+RvoG8f7xwbRcQB48+QGfrwzj5dlEYWRWhqEklEgRSWsDMexUGYjOm5QrHqiw9BJEiABEiABEvAnAQm9pw9CUjZw1+wN1dagm1mXrY63uLBqYlV9KZ1AP+YwlZiBqk/l8BiSUSAxPJMVCWGkNlWbrJadG8VwkaBocXisvkUEJJxOf46L0jauX/4BCg7iw2sjGMZ9XD5zp0iwnk5/gYvSL1i68QPuPz2IDy8O4AOs4lJsAwinsDm4guPxBYRTmxhcOY64poxkjC0lIWWGs/9vUZgerDbw8TLOftgNbF9F5uZ8IYK+NKIjEh4vXsbf/wG899E1nDywgu+/mi4WrOEUUojr3I0/50sSGTdyxhSrHuxCdJkESIAESIAESIAEmkWgsliVx5YwO7CK0bwwreaTKh5mMbBKsdqslnN9PSEJeKQY3OzDlZ9HgKvj+DKnkSLnsTgJXD96E3dNAlL7YGw3K0jDKSz1pLUvQ8KpJUSVBL8YqbcTSOM493kP9u5LOHbghkGshnDii1t4d/Uo7m3kCtV/1/XkE9w2zsDKY1iK7SKR3kEwlkRPehjG76fqGzcoVuttQn/bM5/H3+3L6EiABEjAbQSa+dxpZl1u49yQP5XEar3is177hpzmxZ4kIOHC8uc4dKMgVk+np/HH3W9wadooagvBqcInCV2U5n5OIJn/nScxtMzpCIa++Ay/fncGf/8fyzhbJFYjGJr+DL9+cwYPDU2hzcIW2WWdl8MYi/VgN72MhaK81XrHAYrVlnUHN1bMfB43tgp9IgESIAH/Emjmc6eZdfmrxSqJVX15L6Zyyy5rRV3vS2qt8vh33xEom0XNiddv8Nf//hH+OHhAD1lZxdexXG6ruqQ0CigKJAnImOZH+o6UAwEVz5KWi1CzWVT9d8cwW74UuKKH9Y4bFKsONLaXi2Q+j5dbj76TAAmQgPcINPO508y6vNcSlT2mWPVTa7o3FpMlwNDF6nD3K/zt6neYn1ewHerDBTW3VVnEiJqzmv0Yd56tvOGSe6NvtWfB8z/jJAo5qqYzptoS4VHse7WG579I6DigYO8JcOzILsVqqxuQ9ZMACZAACZAACZBAexLgMuD2bPdmRq2L0g9WSpf7li8L1rwKfYw/3zqAvxjzWNWNfHrSiO/GKmy41Mx4PFZXduOkt9sK3uRcP3AKh/AMz39ZwbMfprFjXPorhQA8wksFqLgMuCKCeldYcGa1HGU757S0c+weG1foLgmQAAm4jUBume2Lvy3i0peFGY+qfjbzudPMutzWOA35U3mDJXX31QlpzlUbLPWOf4Fro/vxYm1R3y1W5KMtP+0FnpXvRCtyeTNsLMUl4ljLY+/DleURdJYJVd1505zVUrGqbqwUVbQdqFFhwyURFG1rI0UQPFgc/bvHPsOxAyt4sPrveL1hdjyN9q2B+QZLNUDWN25QrJbhbOeclnaOvW0HKAZOAiRAAjYROH1lGhf/VT0s8T4uXyo+eqJSFc187jSzLpuQuqSYGkfXbE5AWp9CIr6gH1Ujh5FKDmIlEdeOsil86p1RsRa+Km4unkJdwjMnBNXzfivtOmvNG/uushKXSO0tjT3Uhyu3RtA5W3kDJX0W9Qj+5ydf4dvskZ8aC2SXAas7zyalfK6q2YZLPCpJpCcU25gvA44giPn8LKt+xI2CB+Mx7NRVhZ63KjZuUKyaoJVw+vRBoClnvtXVsk0wbufYm4CXVZAACZCArwlYeYZYucYqxGbWZdVHN15XTazmxOkE+rtyvu9hfS6BePasCu2Yik/zfzQEuI6p43FoR2La+pFwOnIQeLqBu1lxU7t4K9fULtVeC6d8dKrcWtHn8lFN7EpnuCPn8efJXnQ+e4UX3fuBtfv4Opb7QkyGLG+hsOEsN1yqRV7k72ZiNdCXxqmRU9j36hmwvxt4tYYH38WKlgiLlK3ZaF9qVR43CuVQrAozpSEJkAAJkAAJkAAJtB+BGmI1D0QVDcBW0TEV7UeLETtIICSh95GCbYEqjBsuCZjTpB4CUggB5RFe1nNNRdta44ZXxGoz80yaWZdJw+WXKf1tESOiOT92lWMx9pb6bMuNwkJIgARIoHkE7Bozm+exWE2W4rL43BHzqMSqmXVZchCwxNBiXeKXiYpV8RKdtMwva13LLhM1ycmkjTo7aY3PyJk7TjZfy8vWl7Z2A9vZnXG1zYdOAa8Kx7O4xSbz1XTLeTnvgEfEajPzTJpZl1kDW8n5sascq7G30mfnbxLWQAIkQAL2ErBrzLTXq8ZLsxKX1eeOFW+bWZcV/9RrrDC0Wpf4dd4Sq6W5nchuuGTMR6VNIae3Xj4jR2+Kdx0PWmpHuPQiL06RE69Yy+dmusUmMx7zIOF6XfaIWAWaeTZaM+syazA9p0ZRNrAtss6hYptbKcdq7FbqcjL2em8E2pMACZBAMwnYNWY202eRuqzEZfW5I+JPqU0z67Lin3qNFYZW6xK9zltiVWMYOQjl6Qa2tZxVCb2Rg5CKclhp0zgf0f7jNbsQgn2H8fof89rRLOqOt4G+w3jvH4WNhdTfucvGa4zr8dczYrWeoGhLAiRAAiRAAiRAAiRgDwGviVV7omYpJEACbiBAseqGVqAPJEACJEACJEACJOBSAhSrLm0YukUCbUCAYrUNGpkhkgAJkAAJkAAJkAAJkAAJkID3CbwD4Dfvh8EISIAESIAESIAESIAESIAESIAE/ESAYtVPrclYSIAESIAESIAESKAhAlwG3BA+XkwCJNAAAS4DbgAeLyUBEiABEiABEiABvxOgWPV7CzM+EnAvAYpV97YNPSMBEiABEiABEiCBlhOgWG15E9ABEmhbAhSrbdv0DJwESIAESIAESIAEahOgWK3NiBYkQALOEHBarEbOY3GyF3h2H5fP3MG2WRR22ZiU3Tv+Ba6N7gfWFjES2xBjKOKPSF3NLMekrpbGLsLHLhsRziJ8LJYj0qkstYVIwbQhARJwHQGR+72lNiJjnV3js0jriPgjMobbVZfFckTatKxoB+Nq1J+RM3cM7lKsinQL2pAACRQTkGUZW1tbDWJxWKzmB0ts4/rRm7hb7QHYoI0ZidPpaVw8hepiueRCEZ9F6kJOKFeJy65yzHxuZexmX0qI+GPFRoSzCB+r5YjcgSJxiZRDGxIgAfcTELnfW2kjMtaJPJusjvOlZYs8c0XGcNMvw216vov4LNKmXipn5OjNOsSqjHAqiYn+Lv2avXXMZdKYWSi8pMrhMcSiA5AAKKsZpGcW0OgrrPtHA3ooQqA38jEiQ0fQKQEvlCf46/U7uPuo/ErN7rMj6ATwYmUV89Mb+UkweSyF5IAEZTWB+IyxZ8mQscW+JtIQCOHE0EW8W2L7648xPHxZ+GUgOI73uwfR0QG8ebOCZz9OY8fwd90yjNTSIJSMAikqYWU4joUyH2qPG/olDotVQEJv5CCkpxumHU93wi4bs5aQcDpyEMrTDWybdHzzthPxR6SuZpZjVlcrYxfhY5eNCGcRPlbLERkBrLSFSLm0IQEScB8Bkfu9lTYiY51d47NI64j4IzKG21WX1XJE2rS0bCfjsssf85fFQiTqS+kE+jGHqcQMVH2qCtNkFEgMz2AL6gvpLCakdUwl0thBEGeSE/gUcxjV/s5POxM4nf4CF6VfsHTjB9x/CkgX/02bZFr65Ct8a3hvL7Y7iA8vDuADrOKSumoynMLm4AqOxxcQTm1icOU44poykjG2lISUGc7+v51JC8QeGMe5s4PAcwVvDOZGsRocWsbJDgWPf7qOv78G3nv/Gk4eAh5/f6ZI0KptkkJc5278OV9urXHD6K/jYlUADk1IgARIgARIgARIgARcSqDyMmB5bAmzA6vVhacsA0VLAcNIbU4AUzlR4dKw6VYLCEi4sPw5Plj5BpemFb1+bbk8Kq7QVPtgbDcrSMMpLPWkMTyzhXBqCVElof3MjwABTaz24KdMDDsC5rpJCCfO3ULX3ie4/dDw7YI8hqXYLhLpHQRjSfSkh2FsBqFxI++D02JVJB+jnW1MOkNZnoldfJpZl10+WyynLFSL5TjWFgZ/ivOChEcHGpIACbiUQDPGjdweEF6sy23js9v8cUubiuWsqjNXsxhYHa1TEOjXSRmKVZcOYy10qw9Xfh4Bro7jy3ndDXWJ/R93DeK1xDtV+CShi9Lczwkk879rYTDeqjqYRvQPu/j+9jTKVvVWjCSCoegk8OAo7pUqXDmMsVgPdtPLWCj6sqreccNhsSqSZ2KWP1Oa++FXG5GcH7tib2ZddvlstZzSe8rN/bA4L8hb4xq9JQESKCfQLs8vv4zPXnpe5Pb+aEYfE8tZtThDqi7b1CZWzfLYOKq0HwF9OfyHQ8fwgXQAL258hy/ns7Oq0GdaD934Bn/97x/hj4MHdDzKKr6O5XJW1SWlUUBRIElAxjQ/sv2o1h2xKlZPqhv9AG8B7FP/fX4Va/fmS8RrCIHgYfy+ewhdHRLe/HQZ93aEcy31fNa6VlY4LFbN81FL8yhEcjb8amPWlZzi08y6Wt1epbG22h+RNq17WOEFJEACriQgcr+3s43bxme3+eO2vqHyqbQMuN6XTrUsK9e48kanU7YRkHAh/REOAeiUDgArq/g6v3mSLlaHu1/hb1e/w/zdUJYcAAAgAElEQVS8gu1QHy5cG8GwUnzSh3Hn2cobLtnmtA8LCiEQAF6+zArPQARDZyfR8bhkia9hI6aODgnYu4G1h6WCthqeescAx8WqD9uSIZEACZAACZAACZBA2xCoJFbrXc5Xr33bAGageQKlOau5mdXCsmDNNPQx/nzrAP5iPGlE3cinJ434bqzChkvEXC+BwIllnO1aqbI0uELOatWK6h0HnBarVXIFX6wt6rt40aboaJ3cklXyaY++wZzVeodO2pOAuwlwDN+Pas+vstbjO4Ar3wHEclbVjT43MSGJ7Oyr7/4p1Z3f2qT7XeS83ya50kg1ZeOPSWEiNtXuU6ffWzT/Bp8gl5tvmrNaKlbVjZWiirYDNSpsuNQIV1ddm1uu+3YWmdvTjrpWW6wCIjalToqPG+qVDotVN+cKNjP3g3XpCfLGM2+t5jv5rRzmrDo6zrJwEmg6Ab+NUXY/v0obhO8J+wHDeexu6T9iOatqa+pL+qT1KSTi2bNT5TBSyUGsJOLaUTZQ/z87AWmu3o2Ymnf7ipz32zxvrNckct6viE21+9S+9xZ9+S/u/YBvczmqoT5cuTWCzlnDhkqaMD2C/2k4zkaLAdllwOrOs0kpn6tqtuGSn3YE1sThsW4Aa8hkYtY7S9GV+hmr6jE1+TNTy5YB6zZ4dh0PczmqFZcK13JLYNzIF+GwWFVzVtVzTlF0zmrp72hDPrke2c59o9aNzb+TAAl4g0A7j2MisZe2It8B3P0OYD6zUdSKmjidQH9X7rd7WJ9LIK6dVaEv+fs0/zfDlXsiM7LNuuvN+mGz6razHpE4RGxE7tPG/e6NnMefJnvRiVd48Ww/OruhrcwobJ6UrSNyHn9W7Z69wovu/cDafXwduwN981AZsrxlOB3J7xsuhRAMHgZezxeEZeNNgUAwjVMnT2EfnuEturV/nz++jHuGI2mKbN52Y9++SpswCThUddwwXu+4WBVwliYkQAIkQAIkQAIkQAIuJVD5nNVih1XRoB6pynMtXdqQLnZLQm8I2H6U2wW4gqshCb2PlKxIrR6OccMlFwfuPtcCIQRg2GjJ1MOSzZgaiqLWuOG0WHXx+ZZePqfOLeewkSHQaFs4nfvR0PjBi0mABOom0OiYwHG18XGVDBtnKJqzWvcN0oQLeA+qM4/ZJbIOvIf7/b0lv8z2+VVk7s0DhhzR3BmkbrFxOme1CberQBUOi1XmorgzF8XuHCS35Nh4MS77cj8E7neakAAJOE6A4yHyGwZxbwLvvgOI56w6fkvVXQHvQWfvQb+/twSHfsZJ9Rydt7PaLrgw5Ig+yMSwA8AtNvblrNZ9mzXxAofFKs9ZPQjJJF9XebqBbe0YIy+c/+m2M9/86k8T73tWRQIk4CABv45RjKs9n93qrSK6DNjB26quotlXm9NX62oUDxnrOaGvX8/j5UvV7RACwcN4ryhH1G02HsJbt6uOi9W6PeIFJEACJEACJEACJEACriHgNbHqGnB0hARIoGECFKsNI2QBJEACJEACJEACJOBfAhSr/m1bRkYCbidAser2FqJ/JEACJEACJEACJEACJEACJEACAN4B8BtJkAAJkAAJkAAJkAAJkAAJkAAJkICbCFCsuqk16AsJkAAJkAAJkAAJtJQAlwG3FD8rJ4G2JsBlwG3d/AyeBEiABEiABEiABKoToFhlDyEBEmgVAYrVVpFnvSRAAiRAAiRAAiTgAQIUqx5oJLpIAj4lQLHq04ZlWCRAAiRAAiRAAiRgBwGKVTsosgwSIAErBJwWq5HzmJ7sBZ7dxzdn7kABII1/gc9H9wNrixiPbQC0IR/2DSt3L68hARJoNQE+v/j88unza/zMHcPdRbHa6qGG9ZOAFwnIsoytra0GXXdYrOaFKbaxePQmNgD0pacxcgr5Bxxy4pU25NPGfaPBO5mXkwAJtIAAn3H7AT67ffnsHj96sw6xKiOcSmKiv0u/Zm8dc5k0ZhYKL6lyeAyxwQFIEqAoq1hJz8Dw5xbcvazSLQR6Ix8j8tkRdAJ4sbKK+ekNbJc4p9kMHUGnBLxQnuCv1+/g7qOCkTyWQnJAgrKaQHzGKI5kyNhCo3LJLayc9SOEE0MX8W5JJb/+GMPDl4VfBoLjeP8Pg+gA8GbvBn58OA/Dn7OGYaSWBqFkFEhRCSvDcSyUOV973NAvcVisAhKkyEEcfLqBjXynktAXOYh/PN2Aov2ONuST68Ht3DecHYJYOgmQgBME+Pzi88vvzy/zl8XC3aS+lE6gH3OYSugCVBWmySiQGJ7RREI4tYQJSdEE7PIOEIypwhaYGx1Gka5w4hZlmS4mIOF0+nNclLZx/fIPUHAQH14bwTDu4/KZO3nBejr9BS5Kv2Dpxg+4/xSQLv4bLp4Clj75Ct+qOiKcwubgCo7HFxBObWJw5TjimjKSMbaUhJQZzv7fxSjc4FpgHOfODgLPFbwx+FMQqyEEh27hZMcaHqxdx2scxu9PTeIYZvH97eliwRpOIYW4zt34c77c2uNGwQXHxaob6NMHEiABEiABEiABEiABawQqLwOWx5YwO7CK0awwFStfFRGzGFgdxTDVqhgyv1qFJOCRmiSY+/Thys8jwNVxfDlfKWgJF5Y/xwcr3+DStAK1D8Z2s4I0nMJST1rrV+qXJFElwT4m2nc0sdqDnzIx7FS6JhACXhqmtBHBUHQSeHAU94wXyWNYiu0ikd7RvpzqSRd/MVXfuOG0WGU+D/N5fJrP43gOtujgQjsSMCNgMvYSlAME+IzjM86nzzixnFWrojOM1OYEMJWbAXPg3mSRHiWgC9FDN6qJ1WJBqwqfJHRRmvs5gWT+dx4F0Xy3g2lE/7BbPkta1ZMQTpy7hXd/KhGr6jVyGGOxHuyml7FQlLda77jhsFhlPg/zeZirbC1P2/g9Y/NHLNbodQJmY6/XY3Kj/3zG8Rnn12ecWM5qPaJThhwO4szgIAYkCUomgTiTVt04rLXWp8h5LE4C14/exN0iTyT0Rg7iw6Fj+EA6gBc3vsOX87k3JXVJaVRNhtZyojOm+ZGtDcsTtati9aS6qRDwFsA+9d/nV7F2zywnNRuRdg3woNpsbFnw9Ywb6sUOi1XmozJfl7nK6o1mJRfXE0MbnXQtAbNcStc662HHmLPKnNVc97Uyznuh/5i/LOpR1/PSKWMsFYOkPhFVRbGaQWJmgRvfeHj0s9/1akuAJVxIf4RDADqlA8DKKr4u2YjJuPNs5Q2X7PfaPyWGEAgAL3PLfAMRDJ2dRMfjT3D7oXHpby7iCkuAawKpZ9xoilit6TENSIAESIAESIAESIAEXEugUs5qvcv5cgFavc61gOhYwwSK81CrF1fBVt3IpyeN+G6swoZLDTvZdgUETizjbNeKydJgfflv114lIVsNVb33v9Mzq1XyeV6tLeKrGues0qb6ObTk42M+bTQk5pZS5vtzG8XuWKjMWXUMbVHBfMaZ5qzy2eT9Z5NYzqq60ecmJqS5OjdYgpZbWP/GTA7e1try016tPxt3onWwxupF2+WPxXJ6x7/AtdH9eLG2iEvqu7qjnz5cWR5BZ3bDJJGqNP8GnxTaSt1YKapoO1CjwoZLIuXSppiAuViNYOjcJDosCVW9/PrGDYfFKvN5mM/j13wep+Nqp5zV0rOX2yl2px6MzFl1imxxuXzG8Rnn9LOgVWfRi+Wsaq+d2mZJ0voUEvHssl45jFRyECuJOBa29OW/WDGcu6r+fXYC0px7dgPOiTP13ODyfMnmjCfGWuzyx2o5p9PT2vEwjov3UB+u3BpB56y+s2/5R1/+i3s/4NtcjmrpNerOs0kpn6tqtuESd52u1Yf1M1bVY2p2coemmi0Drrk0uFY9ub/XGjeM5TgsVnO5ejA5Z7XwOz3Pgza5hinlQT7t2TdEb3g/2Jn1cT/E1coYyLQ59Dk+t+f43E7trt5JlY+u0e4zTZxOoL8rd9ftYX0ugXj2WBo5nEJyoh9d2MPeXhe6uoA9o7htzs1aoxYJp7PvonfN0vOa7qNd/lgtx+p19YDSl/MOd5tcY5jh7o2cx58me9GJV3jxbD86u6HN+H4d28iexSpDlrdQ2HCWGy7V0wo520AwjVMnT2EfnuEturV/nz++jHv5fFV96e8xdeel0s9bk7NWazlRY9woXO64WK3lKf9OAiRAAiRAAiRAAiTgXgI1xGrecVU0AFtFx1QYo6r1d/cSoGduICChNwRsF53LWtkv44ZLbvDeMz4EQgjAsNGS447XGhcqiVWeHcez43x6dpzj56Py3uG9w3sH+eWxa4sYr7E3AWgDcNzguOGycUM0Z9Xx91gLFeSXvq4tYiQ7/pTmo9JmP9Sx1wqfkTN3LLSKdy7R8jSPdQPPryJzbx7IHelimD10i03m9rR3wFr21ESsSsBvrcqR8GvuB+OydtYo+yHzwXjv8N4ZyeZNqV80cUzgmMAxoTljgnjOquU3UMcuLM23VMcNdbMgYz4qbQr5qPXyGTl607G2c0PBwaGfcVI9IycrTpETr1jLnyfqFptMJuYGZA77UHEZsBfO/vLrmWaM6x9PN6BoOSPshzzDMDcG8r7gfcExQYrw7G6e3a3fB+peH80ZE9T6RJcBO/zOKly8nm+pPN3AdvZdojdyENLTDRTyUWnTOB/hBvGYYQjB4GG8fj2Pl9pmQyEEgofx3uv5wuZDcJuNxxDX5S5zVuvCRWMSIAESIAESIAESaC8CXhOr7dU6jJYE/E2AYtXf7cvoSIAESIAESIAESKAhAhSrDeHjxSRAAg0QoFhtAB4vJQESIAESIAESIAESIAESIAESaBaBd6BusMQPCZAACZAACZAACZAACZAACZAACbiIAMWqixqDrpAACZAACZAACZBAawlwGXBr+bN2EmhnAlwG3M6tz9hJgARIgARIgARIoAYBilV2ERIggVYRoFhtFXnWSwIkQAIkQAIkQAIeIECx6oFGoosk4FMCFKs+bViGRQIkQAIkQAIkQAJ2EKBYtYMiyyABErBCoJJYjZzH9GQv8Ow+vjlzB4p6BPX4F/h8dD+wtojx2AZAG/Jh3+B9wTGB4yGfBXwW8Fngu2fB+Jk7hrdKilUrr9i8hgTanYAsy9ja2moQg4lYldTdgHPCFNtYPHoTGwD60tMYOYX8Q5k2+wHyYd/gfcExQX2p45jJ8ZDPSr4n+OgdafzozTrEqoxwKomJ/i79mr11zGXSmFkwf0kNj6UwiDTiM42+xDb4DszLXUGgN/IxIkNH0CkBL5Qn+Ov1O7j7qNg1zeazI+gE8GJlFfPTG9g2mMhjKSQHJCiriZJ+JUPGFtjTRJo6hBNDF/FuiemvP8bw8GXhl4HgON7/wyA6ALzZu4EfH87D8OesYRippUEoGQVSVMLKcBwLZS6IjhsVlwFLkCIHcfDpBjbyHUZCX+Qg/vF0A4r2O9qQT67nsW/wvuCYwDGTzwt9ROR4yPHQT+OhGku1mVX1pXQC/ZjDVGIGqj6Vw2NIRoHE8EyZSJDHljD7aRewPoXj8fLXV5FXatr4h8Dp9Be4KP2CpRs/4P5TQLr4b7h4Clj65Ct8m9Uap9Of46K0jeuXf4CCg/jw2giGcR+Xz9zRBWs4hc3BFa0/hVObGFw5Dr1ryRhbSkLKDGf/7x9ujkQSGMe5s4PAcwVvDBUUxGoIwaFbONmxhgdr1/Eah/H7U5M4hll8f3u6WLCGU0ghrnM3/pwvt55xgzmrjrQ3CyUBEiABEiABEiABfxCoLFY18TmwilETYVo+kTKGpVkJq3MSPpUyFKv+6Bw2RyHhwvLn+GDlG1yaVpMQAYQk4FH2Z+0Xfbjy8whwdRxfzgNqH4ztZgVpOIWlnjSGZ7YQTi0hqiS0n/kRIKCJ1R78lIlhp5J5IAS8NE57RzAUnQQeHMU940XyGJZiu0ikdxCMJdGTHoaxGeoaN0y+LLP3nFXmMjGXiblM1nKZBMYVmpBARQImYy9pOUCAzzg+43z6jBPLWVVnrmYxsDoqIAjUmZQolMQwls8sYZZi1YEByQ9FFgtR84h0QXvoRkGsJqGLUlUEqT8nkNT+pVCto08E04j+Ybd8lrRqESGcOHcL7/5UIlbVa+QwxmI92E0vY6Eob7WecUMtyOGZ1fymTMztZG4nczvryu00fodYx1BDUxLQCJiNvURjPwE+47h3g1/39RDLWQ0jtTkBTOWWXVa6x4pfTrVZFYpV+wckz5YooTdyEB8OHcMH0gG8uPEdvpyv8hYUOY/FSeD60Zu4q8WsfxECRYEkARnT/EjPwmme46pYPakm3gNvAexT/31+FWv3zHJSs25p1wAPqs3GlkUgOm7kLnRYrDKvlXm/zHnWpIOFfO/mjU+syY8EzPYU8GOcrY6Jezdw74ZcH7Qyznuh/5jPbOhRi710qnmEEyjkqFKstnrcclv9Ei6kP8IhAJ3SAWBlFV+XbKBU8LjyzKtx59nKGy65LXY3+RNCIAC8zC3zDUQwdHYSHY8/we2HJTteaW5XWAJcMySxcaNQjONitabHNCABEiABEiABEiABEnAtgUo5qwLL+dTNbyYk7K0r2jGI2kfqRz/2sK6sYiWtb8rEDwlkO0d5zmoejUk+qxGbupFPTxrx3ViFDZfIuF4CgRPLONu1YrI0WF/+27VXSchWq0lg3Ci63GmxWiWf59XaIr6qcV4rbaqfZ0s+PuZT74hCexIwEmDOanP6A59xpjmrfDZ5/9kklrOqbvS5iQlprvIGS3IY4WDx7dgzGMWn0iqmMrvYWVhoybEiveNf4NrofrxYW8Sl7Lvo4mSv1p/zu8xaGUW0JarF5dhVV1k5Jv6J1CViU1a0Ia6RojN4rUCqfo3m3+CTknbow5XlEXQaN14qEapLUUXbgRoVNlyy31P/l2guViMYOjeJDktCVWdWc9xoplhlPg/zefyaz+N0XMxZ9f9DwMkImbPqJN1C2XzG8Rnn9LOgVWc4i+Wsaq+dWt6qtD6FRDwrPOUwUslBrCTiprOmblgGfDo9rR2RkhOnKmdVvALbhlzI+seRnBA0lmNXXaXlGM8ZzXkqUpeITWnkxrhGis7grZ+RYQTVlv/i3g/4NpejGurDlVsj6Jw17gZs8jtjterOs0kpn6ua22TJuOESN1qq1U76GavqMTU7uUNTzZYB11waXKue3N/rGTecnlnN5urB5LzWwu/0PA/a5BqwlAf5tGffEL3haUcCZgTMxg2Ssp8Ax+f2HJ/bqd3Vu6baOav6rp+p5AT6u3J32B7W5xKIVzgyxA1iVd1L4nT23fNu7jzPov9bHS1Ky1XLsasus7JL/RSpS8SmVrlW+RRf1xs5jz9N9qITr/Di2X50dkOb7f46tqGfoQp96e9wt0l9+VlwGbK8hcKGs9xwyUrrBIJpnDp5CvvwDG/Rrf37/PFl3Mvnq+pLf4+pOy+Vft6anLVaywnhccNxsVrLU/6dBEiABEiABEiABEjAvQRqiNW846poALaKjqlwb1T0zE0EJPSGgO2i81Qb88+44VJjJbXZ1YEQAjBstOR4+LXGDafFKs+g4xl0Pj2D7hub43J8LGAFJEACTSGQXxq8tojxGvsygDYA3xNc+Z4gmrPalJuqzkryS1bXFjFSIR+VNvuhjj9W+Dids1pnc9turuVpHusGnl9F5t48kDvSxTB76BabzO1p2+N3X4EOi1Xm8zCfx6/5PHbH5b7BgR6RAAlYIdDHM6XrOlO6VTmZdo/hfmt38ZxVK3eJs9eI5GTSpnq+bjU+9uWsOtsPrJYeHPoZJ9VzdLLiFDnxirX8eaJusclkYlbD9NB1DotVnrPKc1Z5zqo6Hoicv+ehcYOukgAJVCEgcr/T5h9PN6BkcwWlCJ+V7n1Wql1ddBmwWwYGPSdTebqB7Wwf640chPR0A3p+qv5cpk2jfNzS3nb7EUIweBivX8/jpbbZUAiB4GG893q+sPkQ3GZjNwM3lee4WHVTsPSFBEiABEiABEiABEigPgJeE6v1RUdrEiABNxOgWHVz69A3EiABEiABEiABEmgxAYrVFjcAqyeBNiZAsdrGjc/QSYAESIAESIAESIAESIAESMA7BN4B8Jt33KWnJEACJEACJEACJEACJEACJEAC7UCAYrUdWpkxkgAJkAAJkAAJkIAQAS4DFsJEIxIgAQcIcBmwA1BZJAmQAAmQAAmQAAn4hQDFql9aknGQgPcIUKx6r83oMQmQAAmQAAmQAAk0jQDFatNQsyISIIESAhSr7BIkQAIkQAIkQAIkQAIVCVCssnOQAAm0ioDTYjVyHouTvcCz+7h85g62AfSOf4Fro/uBtUWMxDYA2pBPG/eNkTN3WnX3s14SIAEHCPAZV/35Xoac7wCufAcofjZRrDowVLBIEvA9AVmWsbW11WCcDovV/EMb27h+9CbuAjidnsbFU8gPzsiJV9qQTxv2jZGjNxu8iXk5CZCAmwjwGVf9+V7aVnxP2A+48P2n+NlUS6zKCKeSmOjv0pt3bx1zmTRmFnIvqTLGUjFIJY2vpOOYafQ91k03P32xRKA38jEinx1BJ4AXK6uYn97QJrcKHwkX0h/hUEnpz6/fxLeP9F/KYykkByQoqwnEizqVDBlbYDerv2mCH6fRjeu4dycLOVtEoG8c7x8bRMcB4M2TG/jxzjxelhUfRmppEEpGgRSVsDIcx0KZTa1xI3eBw2IVkNAbOQjp6Qbu5mOVcDpyEMrTDWxrv6MN+eQ6ZDv3jfoHEl5BAiTgRgLtPI6JxF7aZnwHcPc7gNpe1cSq+lI6gX7MYSoxA1WfyuExJKNAYnhGFwnyGJZmB4B1BYqh+SlW3Th+NdMnCafTn+OitI3rl3+AgoP48NoIhlFYjal5E/oYf751BFj7BS8M7uXFajiFzcEVHI8vIJzaxODKccQ1ZSRjbCkJKTOc/X8zY/N2XYGPl3H2w25g+yoyN+cLwfSlER2R8HjxMv7+D+C9j67h5IEVfP/VdLFgDaeQQlznbvw5X5LAuJG3dVyserux6D0JkAAJkAAJkAAJtDeBymJVHlvC7MAqRnPC1AyUJlYlZI6bza60N9m2jz4kAY+MX2H04crPI8DVcXyZ00iaWD2Av2RXaJYyU/tgbDcrSMMpLPWkMTyzhXBqCVElof3MTx0EpHGc+7wHe/clHDtwwyBWQzjxxS28u3oU9zZy5em/63ryCW4bZ2DVez62i0R6B8FYEj3p4aJVFELjRtPEKnNRqueimPQdx/KdmllXi9u9LNQW+1OtTZmzWscASlMS8AABx8ZwF49j9exJ4bbx2W3+uKX/iOWsqjNXsxhYHa0uCNSZr6hSXdB64N6mi80gIOHC8uc4dMMgVtWx77Nf8nvfmInVJHRRqoog9ecEktq/FKr1tlkEQ198hl+/O4O//49lnC0SqxEMTX+GX785g4eG7xe0Wdgiu2ydchhjsR7sppexUJS3KjhuNEusMhelei5K8Zp8vVWcyndqZl2tzkMuvTXd3A+Zs1rvQEp7EnA3AafG8FaPq3bF5bbx2W3+2MW50XLEclbDSG1OAFO5ZZcV7k1VrE70a3/cA6Bmtu6tTyERX2AuobuHs+Z7p30ph/w+N5oDuS/q1JxWQM9tXVvE17Fcbqu6pDQKKAokCciY5kc2PxTv1Vg8S1ouQs1mUfXfHcNs+VLgigAEx41miVXmo9bK1zVrSZGcHys2zayr1TlIpbG22h+R9vLesEaPSYAE2mlcFRnHRGzcNj67zR8Rhs20UflUWgYs+tIpQ5ZR2BVUDiM1OwFprsaMLAeYNiNgsgRYIyChNwRs55YLh/pw5dYIOme/waXpwhSfcefZyhsutRnSOsINnv8ZJ1HIUTWdMdWWCI9i36s1PP9FQscBBXtPgGNHdr0sVuugRFMSIAESIAESIAESIAGXEagkVutdzlcIq76cNZfhoDsOENCX/36wUixAK1WkrZgbfFK8NFjdyKcnjfhurMKGSw647Zcisxsnvd1W8CYX04FTOIRneP7LCp79MI0d49JfKQTgEV4qQMVlwBXZ1DtuOL3BkkmOTZnvdtmYQMkt/1SXC1xSz3QV+Yj4I1JXM8upksvUkthF+NhlI8JZhI/FckS6lKV+KFIwbUiABFxHQOR+b6mNyFhn1/gs0joi/oiM4XbVZbEckTYVef9xSzliOavqRp+bmJDm6s5HdVKsljEU6T8O9sOWtqnFe1nkNrDPpg9XlkfQKShU1XrLxKq6sVJU0XagRoUNl+zz14clSREEDxbH9e6xz3DswAoerP47Xm+YHU+j2lfYYKkGovrGDYfFqlmuYKn/dtmYcSnN2TDL27Tij0hdZvlFIt1bJM9ExOdWxi6SH2uXjQhnqzmrIpyttKlIPxQplzYkQALuIyAy9rbSRmTMFHnGWR3DrYyrImO4yLgq8r4h0qOc9Eekb4gwbLQcsZxV1RN9KbBkzEFVl/kmB7GSiGNhSz9jdTet/pz13OFlwCLvUSI2dnFutC2uZ3fBtVKO1XtZ5D6wxabCkt5C2foZq//r+s3CEZil16g7zyalfK5qbpMl44ZL3Gip/tYyXwYcQRDz+VlW/YgbBQ/GY9ipq4pa44axMIfFqrrOXD1TFUXnrJZGY5eNGSWRsq34I1KXlbrVckuvEynHzEbkOqdiF+Fjl41InCJ8rJYjcneKlC1SDm1IgATcT0Dkfm+ljUjddo3PIq0l4o/IGG5XXVbLEYlD5JnrtnJUn6uds6oeZ6mK0wn0qzsnaZ89rM8lEM8eGSKHU0hO9KMLe9hDl/av8e8ixOuzEXmPErGxq71a2aZW7+X6iFuz1pf+DnebXP2scNZqb+Q8/jTZi068wgvs1/792+x3+DKfr6rmRG+hsOEsN1yy1h7FV5mJ1UBfGqdGTmHfq2fA/m7g1RoefBcrWiIsXHeNcaNQjuNiVdhlGpIACZAACZAACZAACbiOQA2xmve3ZCOl0jhkGTIMGy25Lk465GoCIQm9MB9H55MAACAASURBVGy0VMNZ44ZLro7Li85JIQSUR3hpi+81xg2TL8veAfCbLXXbWYhILoFd9TWzLhOfy85YsxiXpXIsxm6pLgdjt4iMl5EACZBAUwjYNWY2xdk6KrEUl8XnTh1uFUybWZclB7O5eKP7gbVFjIjutWGxLvHLRMWqeIlOWrrlrNp6zh/2ks9+Px9eX9raDWxnd8bVNh86BbwqHM/iFpvMV9NO3kouKdsjM6t25ZmIUG9mXWb+tDInwWrsrfRZpE1pQwIkQAJuImDXmOmmmFRfrMRl9bljJfZm1mXFP6sMrdYlfp23xKpIPiptAGSX2tZ7hrPfz4fXjnBRp3Cz4hQ58Yq1fG6mW2wy4zHx29izlh4Rq+bntTpF3exMTqfqMiu39Pw0q3VbKcdq7FbqcjJ2q8x4HQmQAAk0g4BdY2YzfK2nDitxWX3u1ONXzraZdVnxT73GCkOrdYle5y2xWs7QC2etN/PsXLvqEu0/XrMLIdh3GK//Ma8dzaLueBvoO4z3/lHYWEj9nbtsvMa4Hn89I1brCYq2JEACJEACJEACJEAC9hDwmli1J2qWQgIk4AYCFKtuaAX6QAIkQAIkQAIkQAIuJUCx6tKGoVsk0AYEKFbboJEZIgmQAAmQAAmQAAmQAAmQAAl4n4A7dwP2PldGQAIkQAIkQAIkQAIkQAIkQAIk0AABitUG4PFSEiABEiABEiABEvAXAS4D9ld7MhoS8BIBLgP2UmvRVxIgARIgARIgARJoMgGK1SYDZ3UkQAJ5AhSr7AwkQAIkQAIkQAIkQAIVCVCssnOQAAm0igDFaqvIs14SIAESIAESIAES8AABilUPNBJdJAGfEqBY9WnDWgzr9BB+vngIePEYRy89tFgILyMBEiABEmhHAqEL53BreB/wtwc4+uWOGIJmPneaWZdY9B6xolj1SEPRTRJwFQFZlrG1tdWgTxSrDQL01+X5Fw08x9GRe/4KjtGQAAmQAAk4SiByJYrJf4X2hecnlx7ikUBtzXzuNLMugdA9ZFJLrMoIp5KY6O/SY9pbx1wmjZmF4pdUOTyGWHQAEgBlNYP0zAIafY31EES6WoFAb+RjRIaOoFMCXihP8Nfrd3DXdPCQcHr8I/zf//4Dvp1XikqTx1JIDkhQVhOIzxh7lQwZW+xnQr0vhBNDF/Fuie2vP8bw8GXhl4HgON7vHkRHB/DmzQqe/TiNHcPfdcswUkuDUDIKpKiEleE4Fsp8EBs3AIpVoeZrH6MAQqffw2FlB/Pb7RM1IyUBEiABErCDQACR0+/hqbKDR8LPkGY+d5pZlx083VJGNbGqvpROoB9zmErMQNWnqihNRoHE8ExeJIRTS5iQFE3ELu8EcSYWxQAyGI6Xv8K6JWr64TyB0+kvcFH6BUs3fsD9p4B08d9w8RSw9MlX+DYvWPtw5ecRqN+DqZ8Xs9/g0rRBrIZT2BxcwfH4AsKpTQyuHIferWSMLSUhZYaz/3c+Hk/XEBjHubODwHMFbwyBGMVqcGgZJzsUPP7pOv7+Gnjv/Ws4eQh4/P2ZIkGLcAopxHXuxp/z5YqNG7o5xaqn+xWdJwESIAESIAESIAFnCVQWq/LYEmYHVjFqEKZlvqhiYgKYOm42u+Ks5yzdawQkXFj+HB+slAhSLQzzv6l9MLabFaThFJZ60hie2YL6BUlUSWg/8yNAQBOrPfgpE4NgEgeAEE6cu4WuvU9w+6FhOlwew1JsF4n0DoKxJHrSwzA2g9C4kXeZYlWg9WhCAiRAAiRAAiTgCIFm5pE2sy5HYLWq0EpiVZ25msXA6mhVQaDOdkWV6jatioz1uo2APouKq+P4cr7Ut8piNQldlKoiSP05gaT2L4VqHe0bTCP6h118f3saZat6KxYTwVB0EnhwFPdKFa4cxlisB7vpZSwU5a2KjRuFKilW62hFmpIACZAACZAACdhJoJl5pM2sy05GrS+rklgNI6VPmVZZZqm/mEqZUaz0xBAdUDNW1aTVDBJx5qy2vm3d4IGE3shBfDh0DB9IB/Dixnf4siQnVfey0qyruqQ0CigKJAnImOZHuiFOl/ugitWTpzQn3wLYp/77/CrW7s2XiNcQAsHD+H33ELo6JLz56TLu7YjsUJCLX2TcMLKiWHV5z6F7JEACJEACJOBnAs3MI21mXX5qs8bF6qdde1ifSiC9sIUtdcYlOYFPlSktz5Cfdicg4UL6IxwC0CkdAFZW8fX0BsrT3qstEQaMO89W3nCp3VlXiz+EQAB4+TIrPAMRDJ2dRMfjkiW+6tLf7EZMHR0SsHcDaw9LBW21eihW2QtJgARIgARIgARIgARsI9DIMuDczGrJ7Kua0zYrIcM8VttayR8F1Z+zmo9b3cinJ434bqzChkv+INTMKAInlnG2a6XK0uAKOatVneQy4Ga2IesiARIgARIgARJohADzSBuh16RrK2+wpOajTkhzVTdYMs1ZdZtYjZzH4mQv8Ow+Lp+5YzKrJ4jaYjm941/g2uh+vFhbxKXYhmllIjaCXtY0K6vLENfImTs1r2/EQKt78IlJO1QRsurGSlFF24EaFTZcasQnV12bW677dhaZ29OOulZbrAIiNqVOiowbhWu4DNjRRmbhJEACJEACJEAClQkwj9QLvaPG0TWbE5DWpwo5qHIYqeQgVhJx7SgbaMJ0AKujhR1BtZdVuGcZcE6cAdu4fvQm7lpsFqvlnE5Pa0e2VBPLIjYW3S67rLQuZMW0ymfk6E2bqtGX/+Ke4dzUUB+u3BpBZ+nxNFqNFcSq2r+SUj5XNbfJknHDJT9ttKSJw2PdANaQycRsagt9aa96TE3+zNSyZcC6DZ5dx8NcjmrFpcK13NKXAlcdN/JFUKzWosm/kwAJkAAJkAAJOEZAP5sVPN/bMcKNF1xNrKpiVBWnE+jvytW0h/W5BOLGsyrUma+JfnTt7WGvqwtYn0MiXjiHtXEfGy1BwunIQeDpBu7Ws1dMWbVWyxG5TsSmUQ6560vrcqbu3sh5/GmyF514hRfP9qOzG9rs8texQs5q4QuA0thyXyzIkOUtFDac9fuGSyEEg4eB1/MFYWlDsweCaZw6eQr78Axv0a39+/zxZdwzHElTZPO2G/v2VdqEScAhkXFDK4ZiVYAmTUiABEiABEiABEigXQnUEKt5LKpoALaKjqkoYSbLkLe2wJMv27UvVYpbQm8I2H6k2AbGuOGSbYW2Q0GBEAIwbLRkGnPJZkwNcak1blCsNoSXF5MACZAACZAACTRAgDmrDcBr1qWiYrVZ/lSvJz8Tt7aIETX/0ySPlDb7AYt8nM5ZbXUvyi+zfX4VmXvzgCFHNHcGqVtsnM5ZbXVb6PVTrLqjHegFCZAACZAACbQhAeaseqHRvSVWq+Vb5vJRaVPIjzXmo4rwsS9n1Z19Pzj0M06q5+i8ndV2wYUhR/RBJoYdAG6xsS9n1Z1tQbHq5nahbyRAAiRAAiTQFgR49qn7m9lbYlXdiEfNP1WebmBbyz+V0Bs5CKkoH5U2jfNxf8+15qGeE/r69TxevlRLCCEQPIz3inJE3WZjLVJvXMWZVW+0E70kARIgARIgARIggZYQ8JpYbQkkVkoCJOAIAYpVR7CyUBIgARIgARIgARLwBwGKVX+0I6MgAS8SoFj1YqvRZxIgARIgARIgARIgARIgARJoOwLvAPit7aJmwCRAAiRAAiRAAiRAAiRAAiRAAq4mQLHq6uahcyRAAiRAAiRAAiTQTAJcBtxM2qyLBEjASIDLgNkfSIAESIAESIAESIAEKhKgWGXnIAESaBUBitVWkWe9JEACJEACJEACJOABAhSrHmgkukgCPiVAserThmVYJEACJEACJEACJGAHAYpVOyiyDBIgASsEKFatUOM1JEACJEACJEACJNAmBChW26ShGSYJ2EpAlmVsbW01WCbFaoMAeTkJkAAJkAAJkAAJ+JlALbEqI5xKYqK/S4ewt465TBozC+pLqoyxVAySKR4F6fgMGn2V9TP5doitN/IxIp8dQSeAFyurmJ/ewHY+cAkX0h/hkCmIXzAfu6PZymMpJAckKKsJxGeMPUqGjC32MaGOFMKJoYt4t8T21x9jePiy8MtAcBzv/2EQHQDe7N3Ajw/nYfhz1jCM1NIglIwCKSphZTiOhTIfqo0bRmOKVaHmoxEJkAAJkAAJkAAJtCeBamJVfSmdQD/mMJWYgapP5fAYklEgMawKUXOxKkn96FKmcDxe/grbnozbMWoJp9Of46K0jeuXf4CCg/jw2giGcR+Xz+giFDAXq51SLzqVRYzENoBwCpuDK1pfCqc2MbhyHHq3kjG2lISUGc7+vx0Z1xFzYBznzg4CzxW8MVxWEKshBIdu4WTHGh6sXcdrHMbvT03iGGbx/e3pYsEaTiGFuM7d+HO+3FrjBsVqHS1HUxIgARIgARIgARJoZwKVxao8toTZgVWMasJU8COPYSkpIWM62yJYBs38QSAkAY8UQyx9uPLzCHB1HF/OVwgx9DH+fO0A/nLmJu5qs6pLiO1mBWk4haWeNIZnthBOLSGqJLSf+REgoInVHvyUiWGnknkgBLx8ZPhrBEPRSeDBUdwzXqTe47FdJNI7CMaS6EkPw9gM9Y0bnFkVaD2akAAJkAAJkAAJkEC7EqgkVtWZq1kMrI7WIQj0a6RMbvarXZkybnMCEi4sf45DNyqJ1fK/q8InCV2U5n5OIJn/HUkLEgimEf3DbvksadXLQzhx7hbe/alErKrXyGGMxXqwm17GQlHear3jBsWqYAvSjARIgARIgARIgATakUAlsRpGanMCmKpDeBqWbLYjScZcg0DkPBYngetH9VnTso/696HH+vLf/EddUhoFFAWSBM7YW+1kqlg9eUq7+i2Afeq/z69i7Z5ZTmq2Eu0a4EG12dgyf+odNyhWrTYpryMBEiABEiABEiCBNiBgl1jlrGobdJYGQqy1BLj6rKtx59nKGy414J7vLw0hEABe5pb5BiIYOjuJjsef4PZD49LfHIgKS4BrcqJYrYmIBiRAAiRAAiRAAiRAAqIEbFoGrOaxzUrIHDfbGVTUF9r5k4AuRD9Y+QaXpo05rIZo1VzVWwfwl0qzrupGPj1pxHdjFTZc8ic5J6MKnFjG2a4Vk6XB+vLfrr1KQraaV1wG7GSbsWwSIAESIAESIAESaDMClTdYUndfnZDmxDZYUpcARxUx22YT1paf9gLPjDvR2uNE7/gXuDa6Hy/WFnGpaPmqPeUbSymry8G47PO+D1eWR9BZTaiqlamxfPaLYadggwfqxkpRRduBGhU2XLLP3/YpyVysRjB0bhIdloSqzq6ucQNcBtw+PY6RkgAJkAAJkAAJkEDdBGocXbM5AWl9Con4gr4jsBxGKjmIlURcO8om96lvB9C6nWzogpzIA7Yr50tarOF0ehoX1VRAB4RwqUuldSErlJ2IyyKO4stCfbhyawSds1VmVLNXaG00+KRcrJbsLm224RJ3BK7VWvoZq+oxNTu5Q1PNlgHXXBpcq57c3/WlwCLjBihWRaHSjgRIgARIgARIgATakUA1sZoTpxPo78qx2cP6XALxkiNDNLEqZVx6tqqE05GDwNMN3DVLz2uo2Z0su9Sx0rqaWXe9kPSlv8PdJteZCHtNrPaslmyupH07AlneQmHDWW64VG9LqPaBYBqnTp7CPjzDW3Rr/z5/fBn38vmq+tLfY+rOS6WftyZnrdZyQvtSq/a4QbFaCyT/TgIkQAIkQAIkQAJtTaCGWM2zUUUDsFV0TEVbg2PwLSRg3HCphW54r+pACAEYNlpyPIJa4waXATveBKyABEiABEiABEiABLxLQFSsuiPC/JLetUV9Fs4kb5M2+wGLfEbO3HFHQzvkhZaneawbeH4VmXvzQO5IF8PsoVtsMrenHaLgpmIpVt3UGvSFBEiABEiABEiABFxGwFtiVSRvkzaFHFqzvNZqfEaO3nRZ/7TXneDQzzh5SD1kVF/aipx4xVr+PFG32GQyMXuDd2VpFKuubBY6RQIkQAIkQAIkQALuIOAtsQroeZrK0w1sa/mnEnojByEV5aPSpnE+7uid9nsRQjB4GK9fz+OlttlQCIHgYbz3er6w+RDcZmM/BfeUSLHqnragJyRAAiRAAiRAAiTgOgJeE6uuA0iHSIAELBOgWLWMjheSAAmQAAmQAAmQgP8JUKz6v40ZIQm4lQDFqltbhn6RAAmQAAmQAAmQAAmQAAmQAAkYCLwD4DcSIQESIAESIAESIAESIAESIAESIAE3EaBYdVNr0BcSIAESIAESIAESaCkBLgNuKX5WTgJtTYDLgNu6+Rk8CZAACZAACZAACVQnQLHKHkICJNAqAhSrrSLPekmABEiABEiABEjAAwQoVj3QSHSRBHxKgGLVpw3LsEiABEiABEiABEjADgIUq3ZQZBkkQAJWCFCsWqHGa0iABEiABEiABEigTQi4V6zKYynEkEZ8ZqtN2oJhkkC7EaBYbbcWZ7wkQAIkQAIkQAIkUAeBamI1jNTSIJREHK3Qi+HUJiYwhePxhTriyZqGU1iKSlAywyi6XB7DUlJCZjiO4lLVWKNQEsOFWOUwUsko+ru69EL31jGViGPB9dpZRngshp7dNGYsOiuHxxCLDkACoKxmkJ5ZQFnYchhjsSgGdCNk0jMuYNOk2A09MjyWwmCLv1QRai/Bu6hmWba2O8WqYLPQjARIgARIgARIgATakUANsboZhTJqEHBNRNSIWFWvjUp76FIyJWI3jNTmBDB1vFjEhlPQf50Vsaqonf0UWJ9CIr2ArS0ZcjiG5OAKhq2I56Zw02Prz9a1NzeK4bq/ZZARTs1iQlKFeRo7COJMcgKfYg6jwzMGwarXJWl8dhCMJTHRrxT4NSVeYyXNjL1Qrzy2hNlPu7R+YulLlYY5ibaXWEXh1BImJAVzmTSWd4I4o34ZgYyhz9vd7hSrYi1DKxIgARIgARIgARJoSwL2iFU5HEZQ47eDhQqzebIcRlA3ws6CyUwdVEEY1MrZ2VlAMGZ1ZlV9oR7EyqiC6KyETE6AZtvXTASX/q4+oaz6DajTiu6YdJUxtjSLgVUrYhWALANbxkjKBb4m0qTiLwK0LwgUi3Xadu85H3veVe0LDQmrcxI+LWFhWzgiBQm0l0gxKP3CxuQi+9udYlWobWhEAiRAAiRAAiRAAu1JoFGxqi6fVWfz1rGuAJD6tZ+njMtsw2NYmvhUW0araDYS+ruAuaIZ29JyJEjoQpdiYcZKfemOKhgdXsaZpVlImRqzqNAFTsGuwuxrhQ6iCVt1OrNls2uljjUo2MriLOVT+v/8twBZ7sYZ2GbfVU7HnounsGx8+Uy5cG921MX1VWifGk7V/rLBiXanWG1tX2HtJEACJEACJEACJOBqAo2JVZFZSqgzpvJW0WSdyExmfbObBcjGl26zmSCgRIxmZ8jyM7Da/wewKrj82fditWzGTeWXXR4eVL+IGMDq1DBmdvSZxtKZ7OZ2f5vFqulsY3Ed5n2suVEX1SYwQ1ruXU6IjmKlJ4aolois5iJnkIjnVkE40e4Uqy3sKayaBEiABEiABEiABNxOoBGxKpj/mUegL/M90zMIaaAf/flZU/NyrIlVwwu1upK1VIhmfakqaEvFajZ/Vd9mab2FeZmifclOwWbWNkbRoub69mNdzQH2nVgV65fuEqv1rQoo9Ci9z3zatYf1qQTS6pJ2dSMlNV+56D7NfUlhV7tTrIre1bQjARIgARIgARIggTYk0IBYrTQDWfb7whLfudUV7C7vAGeSmMjl+VUox5JY1WaVclsMFZpTE1PG7X+rLhUuEby5YioIX/d1GrvEaqVynFgOahdFh2PX+peEvXUF6op27aMtfd/DurKKlZbuiNxI7BXatKjPO9HuFKt29XyWQwIkQAIkQAIkQAI+JNCAWC1dTpujU7IM0SwXrng2SmwGSwR+7bryTmaXsmYgzQ5ipWgTpuwsU2m+bEWx6sUNlmr5rH/BIFXYpEmcs0ir2WkjItgaiF0OI5zdJCzndc9gFJ9Kq5jK7FbYOMzO+CqVVb29CldVjt00Z7Wkz9vf7hSrzegdrIMESIAESIAESIAEPEqgEbGqrrJVj+5QMDWaPX80u3TQuBNt2Quudn7phGEZMKDNokqG41FyS2/r2rSowoxohRy+ysfbQHUou7x1FPHs7sZyOIXkBMx3F/bYBktV82zV9pmdgFTt6BvTpdLieb7O3Sy1xWrDsZc43/JlwCLtlfW5euzludplqxtsb3eKVefuBZZMAiRAAiRAAiRAAp4nUEusFs7tLIS6Z9jJVz3nUT1jswt7e0BXl7rp75RhU5Zc3uin6MIe9qCeSTmHjCJhQloxnE2Zy5kD9gB07c1hanWgsFRYhHN+aW/pbrTVcmuz+ZbGJcL5ydcUlib6VY+znz2szyUQLzm71C0bLOXP/CxjVZ5nW9nnQjuUFbNXfNaqPJZC8tN+rcG61FxHEzYizWaHTbNjN/rcWrEq3l6qzzX7ajjb5/f2sKfezOtzSMSL7yd7251i1Y7+zzJIgARIgARIgARIwKcEqonV+kKWZRlbRedzllwvy5C3ap1FKkNGLZv6/GrYWvVb9ckdh6g2HI7dBdRsd7srZHnOExC4V+1pd4pV5xuTNZAACZAACZAACZCAZwnYJ1Y9i4COkwAJtIgAxWqLwLNaEiABEiABEiABEvACAYpVL7QSfSQBfxKgWPVnuzIqEiABEiABEiABErCFAMWqLRhZCAmQgAUCFKsWoPESEiABEiABEiABEmgXAhSr7dLSjJME3EeAYtV9bUKPSIAESIAESIAESMA1BChWXdMUdIQE2o4AxWrbNTkDJgESIAESIAESIAESIAESIAEvEngHwG9edJw+kwAJkAAJkAAJkAAJkAAJkAAJ+JcAxap/25aRkQAJkAAJkAAJkECdBLgMuE5gNCcBErCNAJcB24aSBZEACZAACZAACZCA/whQrPqvTRkRCXiFAMWqV1qKfpIACZAACZAACZBACwhQrLYAOqskARLQCFCssiOQAAmQAAmQAAmQAAlUJECxys5BAiTQKgIUq60iz3pJgARIgARIgARIwAMEKFY90Eh0kQR8SoBi1acNy7BIgARIgARIgARIwA4CFKt2UGQZJEACVghQrFqhxmtIgARIgARIgARIoE0IqC+LAfyfp0/w/H+3ScgMkwRIwB0EfncIRw7/N7zc/A+8ynrEo2vc0TT0ggRIgARIgARIgARcQOB3OHTkMP7by038R+5t0QVe0QUSIIE2ILD/X3A88H/w9Mlz5L4ro1htg3ZniCRAAiRAAiRAAiQgSuB3h47gcOD/w55hdkP0WtqRAAmQgDUC6qqOLvxfL5/iiWFZB8WqNZq8igRIgARIgARIgAR8S2D/vxxHV8d/4eXeHv7z1f/Oz3L4NmAGRgIk0CICv8Pv9v8zuroC+Kc3e9gsWdJBsdqiZmG1JEACJEACJEACJOBmAr/b/y/oCnTgn/7JzV7SNxIgAc8T+K//wpuXe/iPV+WJ8hSrnm9dBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5AhSrnm9CBkACJEACJEACJEACJEACJEAC/iNAseq/NmVEJEACJEACJEACJEACJEACJOB5Au9Eo9HfPB8FAyABEiABEiABEiABEiABEiABEvAVAc6s+qo5GQwJkAAJkAAJkAAJkAAJkAAJ+IMAxao/2pFRkAAJkAAJkAAJkAAJkAAJkICvCFCs+qo5GQwJkAAJkAAJkAAJkAAJkAAJ+IMAxao/2pFRkAAJkAAJkAAJkAAJkAAJkICvCFCs+qo5GQwJkAAJkAAJkAAJkAAJkAAJ+IMAxao/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IECAAAECBAgQIECgFgHJai09pZ3HElj9i5/Fj//s6xEP34v1m/ePday5P3zxh7G5/u2Ip7+I9XffT3+8xjanL04gAQLLKdByPqwRq9Uc3tKn1blaovZ5rpZN9DECBAYkIFkdUGe6lOkCr76+Gd9dje2E8Xvvvh8Pe8TaXdjjV7G+8Xb6zDW2OX1xAgkQWEqBtvNhjVht5vC2Pm3O1da0z3O1baPPESAwHAHJ6nD60pXMFFiNVy+uxKPP78fDR31TrcbqxZVY+fx+3J/r3DW2uW9b5yNAoC6BtvNhXVe509o2c3hbnzbnamva57nattHnCBAYioBkdSg96ToIECBAgAABAgQIECAwIAHJ6oA606VMF1BjY3QQIECAQJ8C1p0+tZ2LAIGhCkhWh9qzruuAgBobA4IAAQIE+hSw7vSp7VwECAxVQLI61J51XRMCamwMCQIECBDoU8C606e2cxEgMEwByeow+9VVESBAgAABAgQIECBAoGoByWrV3afxBAgQIECAAAECBAgQGKaAZHWY/eqqCBAgQIAAAQIECBAgULWAZLXq7tN4AgQIECBAgAABAgQIDFNAsjrMfnVVBAgQIECAAAECBAgQqFpAslp192k8AQIECBAgQIAAAQIEhikgWR1mv7oqAgQIECBAgAABAgQIVC0gWa26+zSeAAECBAgQIECAAAECwxSQrA6zX10VAQIECBAgQIAAAQIEqhaQrFbdfRpPgAABAgQIECBAgACBYQpIVofZr66KAAECBAgQIECAAAECVQtIVqvuPo0nQIAAAQIECBAgQIDAMAUkq8PsV1dFgAABAgQIECBAgACBqgUkq1V3n8YTIECAAAECBAgQIEBgmAKS1WH2q6siQIAAAQIECBAgQIBA1QKS1aq7T+MJECBAgAABAgQIECAwTAHJ6jD71VURIECAAAECBAgQIECgagHJatXdp/EECBAgQIAAAQIECBAYpoBkdZj96qoIECBAgAABAgQIECBQtYBkteru03gCBAgQIECAAAECBAgMU0CyOsx+dVUECBAgQIAAAQIECBCoWkCyWnX3aTwBAgQIECBAgAABAgSGKSBZHWa/uioCBAgQIECAAAECBAhULSBZrbr7NJ4AAQIECBAgQIAAAQLDFJCsDrNfXRUBAgQIECBAgAABAgSqFpCsVt19Gk+AAAECBAgQIECAAIFhCkhWh9mvrooAAQIECBAgQIAAAQJVC0hWq+4+jSdAgAABAgQIECBAgMAwBSSrw+xXV0WAAAECBAgQIECAAIGqBSSrVXefxhMgQIAAAQIECBAgQGCYApLVYfarqyJAgAABAgQIECBAgEDVAv8PLP0nNekE1nIAAAAASUVORK5CYII=" width="400" /></div><p><br /></p><p><br /></p><h2 style="text-align: left;">rclone 备份</h2><p>rclone版本是 v1.65.0</p><p>启用了数据加密和文件名加密。</p><p>由于有一些文件名过长,ext4那边会报错,所以我去掉了一些数据,最后是53356个文件,总共2.44TiB。</p><p>rclone最终花了15小时44分传输完毕。</p><p>由于这个运行的比较快,中间我也没看系统占用率。</p><p><br /></p><p><br /></p><h2 style="text-align: left;">比较</h2><p>平均算下来restic处理速度大概27MB/s,rclone处理速度大概45MB/s。</p><p>restic比rclone慢点是正常的,但是我觉得这两个整体都偏慢,至少我认为网络应该是瓶颈,应该能跑满才对。</p><p>(更新1:后来又做了一番测试,感觉是群晖那边CPU是瓶颈,处理SSH的加密比较吃力)</p><p>(更新2:虽然也有CPU的问题,但我发现每隔一段时间源服务器防火墙会屏蔽备份服务器的ip一分钟,这个大概就是效率低下的原因了。得检查一下防火墙哪里有问题)</p><p>(更新3:查到了restic关于sftp的<a href="https://github.com/restic/restic/issues/4209">bug</a>,用rclone做中继后可以跑满带宽了)</p><p>rclone加密不能很好的应对长文件名,这个很头疼。此外,一些网盘(比如OneDrive)对路径总长也有限制。</p><p>restic支持压缩很好,rclone的压缩还是实验功能</p><p><br /></p><p><br /></p><h2 style="text-align: left;">总结</h2><p>感觉restic还是有很多亮点的,尤其是了解了rclone的缺点之后。</p><p>接下来考虑一下哪些地方适合用上restic。</p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-85933691190445662542024-01-20T15:13:00.006+01:002024-01-20T15:13:43.433+01:00整理了一下RSS和Podcast<p>之前Google Reader挂了之后我基本一直用Feedly,后来也开始用Google Now看新闻,用Google Podcast听Podcast。</p><p><br /></p><p>这几天决定切换到自己的服务器上,有若干原因:</p><p></p><ul style="text-align: left;"><li>越来越不喜欢Google Now的自动推送。感觉我的眼界越来越窄。</li><li>Feedly经常遇到收费功能。</li><li>Google Podcast要关闭了。</li><li>我正好也有服务器了</li></ul><div><br /></div><p>RSS抓取用的Tiny Tiny RSS,这个之前就搭建好了,作为Feedly的备份。最近升级发现docker compose文件有若干变化,数据库版本也变了,稍微折腾了一下。</p><p>类似的还有几个</p><p></p><ul style="text-align: left;"><li>Miniflux</li><li>FreshRSS</li><li>NewBlur</li></ul><div>不过这些其实都大同小异。主要Tiny Tiny RSS导入导出备份都挺方便。</div><p></p><p><br /></p><p>手机端用的Feedme,直接支持Tiny Tiny RSS的API,也可以放Podcast</p><p><br /></p><p>我的RSS源都很老了,有好多都没法用了,Feedly也不提示我。发现了两个不错的RSS网站</p><p></p><ul style="text-align: left;"><li>https://docs.rsshub.app/</li><li>https://feedx.net/</li></ul><div><br /></div><div>Podcast回头也得整理一下,不过目前问题不大。</div><p></p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-48651486877478663142023-10-30T02:55:00.014+01:002023-10-31T01:07:15.067+01:00Code Study Notes: Sphere Eversion<h2 style="text-align: left;">The Video</h2><iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" frameborder="0" height="315" src="https://www.youtube.com/embed/B_BY0eJFuGQ?si=Dwx8HhL6IlssNSzd" title="YouTube video player" width="560"></iframe><div><h2 style="text-align: left;"><br /></h2><h2 style="text-align: left;">A Bit Background</h2><p>Thanks to Youtube's algorithms, I got to watch <a href="https://www.youtube.com/watch?v=sKqt6e7EcCs">this video</a> again after many years. The video, made in 1994, explains the sphere eversion problem and visualizes one solution: Thurston's corrugations. More information can be found in the following links: <a href="https://en.wikipedia.org/wiki/Sphere_eversion">1</a>, <a href="https://www.cs.ubc.ca/~tmm/gc/">2</a>, <a href="https://chrishills.org.uk/ChrisHills/sphereeversion/">3</a>. There is also an <a href="https://www.youtube.com/watch?v=OI-To1eUtuU">HD version</a>.</p><p>The animations in the video are really fascinating. I figured it'd be a good excercise to recreate it with Blender, especially with geometry nodes. However I immediately got stuck after 20 minutes, because I had no clue how everything works.</p><p>Luckily I found a <a href="https://github.com/etale-cohomology/evert-cuda">port</a> of the original source code. (The original <a href="http://www.geom.uiuc.edu/docs/outreach/oi/software.html">source code</a> seems no longer available). The author also created a <a href="https://www.youtube.com/watch?v=5fFBu1BMKDg">nice video</a>. I'll refer this version as mathIsART's version. I also found <a href="https://profs.etsmtl.ca/mmcguffin/eversion/">another port</a>, which I'll refer to as McGuffin's version.</p><p>Mostly I studied mathIsART's version, and occasionally used McGuffin's version as reference. The code was really fun to read, and I learned a lot. Here I'd like to summarize my learnings.</p><h2 style="text-align: left;">The Sphere</h2><div>The most important thing I learned, is to parameterize everything. This allows a precise definition of the surface, and it also makes it easy to animate.</div><div><br /></div><div>Each point on a sphere can be define by two angles: the latitude and the longitude. In mathIsART's code, phi describes the latitude, and theta describes the longtitude. </div><div><br /></div><div>In blender, I'd start with a plane where both x and y values are in [0, 1], x and y will be mapped to phi and theta respectively. A subdivide modifer will give enough grid points and a set of geometry nodes can easily map the plane to the stripe. Clearly I only need to build a 1/8 sphere at the north hemisphere, the rest can be obtained by rotating the surface.</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhYzEXuji-AOpR7vHyZef8Ehn3sZ9j9jENlcXYixxj_yXXp9iSbT9hv8Y_pI6hLdig8A_1kiwBftEz-Ha3UeR6vK-UOiRIKCPkxXO-LG05lZ_GSsckaISfER8hY77dD95Yh-B144ey9fGsQsz8aMWFhAV3tyQOa99KChSE0-pes787iMQBqeCTFRQ" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="543" data-original-width="684" src="https://blogger.googleusercontent.com/img/a/AVvXsEhYzEXuji-AOpR7vHyZef8Ehn3sZ9j9jENlcXYixxj_yXXp9iSbT9hv8Y_pI6hLdig8A_1kiwBftEz-Ha3UeR6vK-UOiRIKCPkxXO-LG05lZ_GSsckaISfER8hY77dD95Yh-B144ey9fGsQsz8aMWFhAV3tyQOa99KChSE0-pes787iMQBqeCTFRQ=s16000" /></a></div><div><br /></div><h2 style="text-align: left;">The Corrugation</h2><div>A jellyfish!</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhQdIXNsuHI9t19hZMd49XTCUidZWwQ1WKhWP4agUps9YMQXLW-0h4BB0pwwmDgwDTfkBX2-hLojJIIeOOXeZ0FkIj3oEF0QiUvyWm8XVKs6Eer2DTWlCKqJ5j9zHDuYiFAHKFCS2hZty_NAg4unbOjfhnpSUTIkPOWvQ6UgGtTDoLT5agb-y_UqA" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="638" data-original-width="807" src="https://blogger.googleusercontent.com/img/a/AVvXsEhQdIXNsuHI9t19hZMd49XTCUidZWwQ1WKhWP4agUps9YMQXLW-0h4BB0pwwmDgwDTfkBX2-hLojJIIeOOXeZ0FkIj3oEF0QiUvyWm8XVKs6Eer2DTWlCKqJ5j9zHDuYiFAHKFCS2hZty_NAg4unbOjfhnpSUTIkPOWvQ6UgGtTDoLT5agb-y_UqA=s16000" /></a></div><br />At each point we can compute the local partial derivatives, actually we only care about the directions, i.e. east and north, from which we can also compute the up direction, i.e. normal.</div><div><br /></div><div>McGuffin's version implemented 2-order jets while mathIsART implemented only 1-order jets. Since it is annoying to add lots of math nodes in Blender, I only use plain vectors unless the derivatives are absolutely needed. After several rounds of simplification, it seems that this is the only place where we need the derivatives. </div><div><br /></div><div>Corrugation is achieved by adding local offsets to each point, mathIsART's version uses all three directions, but I only used up and east. The offset on each direction is determined by a function of theta. The screenshot below shows the function for the up (or down) direction.</div><div><br /></div><div>From mathIsART's code I can clearly see the trace of programming in 1990's. There are no complicated functions, yet specific curves/functions are obtained by multiple layers of interpolations. Today I got the luxury of the curve widget and instant rendering. I cannot imagine how I could implement it in 1995.</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgFQwcNNR74kd-UaUttiw5qKt_XWNHDth6kSmrdY_DUg1pCCePzULk0FJf1E1lMSNsSfP457BehJ-mB-bWCY3rxsJuLQr76P3hfZSBhJGvrM0nPSRhmbJ4QUrL351aoZsM1jfH6xCMuvP4OTWWyxIPgoukUl9XARnhFeeeqztgkOXxDSSbYB-XgFA" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="911" data-original-width="908" src="https://blogger.googleusercontent.com/img/a/AVvXsEgFQwcNNR74kd-UaUttiw5qKt_XWNHDth6kSmrdY_DUg1pCCePzULk0FJf1E1lMSNsSfP457BehJ-mB-bWCY3rxsJuLQr76P3hfZSBhJGvrM0nPSRhmbJ4QUrL351aoZsM1jfH6xCMuvP4OTWWyxIPgoukUl9XARnhFeeeqztgkOXxDSSbYB-XgFA=s16000" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><br /></div>The size of the corrugation is a function of phi. Note that there is hardly any corrugation near the north pole. The piece looks like a squid:</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEiySfu8WGr7HgxCOuFrVPUG6oiSXGnph71dq8dHYtbq9qKOWYYnm37wXAg08Ku1MAbXpAcUhvWgQ5mzOn4jpDDHGa2za8gLza4hgcY-p7e-Ct2vkIxUh8pm11UCnjtN8Jc9KLE3q9uOmvZRz0nzf4fymZzovJqjlYc7RkxLZlS1csKuJ-AUpJallQ" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="709" data-original-width="703" src="https://blogger.googleusercontent.com/img/a/AVvXsEiySfu8WGr7HgxCOuFrVPUG6oiSXGnph71dq8dHYtbq9qKOWYYnm37wXAg08Ku1MAbXpAcUhvWgQ5mzOn4jpDDHGa2za8gLza4hgcY-p7e-Ct2vkIxUh8pm11UCnjtN8Jc9KLE3q9uOmvZRz0nzf4fymZzovJqjlYc7RkxLZlS1csKuJ-AUpJallQ=s16000" /></a></div><br /><br /></div><h2 style="text-align: left;">The Push</h2><div>A tabacco pipe!</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgfFYo5eQYIQm66bnMGZfNdFxNMNe91WMq7pH7qDFi_oimu3VdqnDTDH5dXgfHyi7BuraA2jOwhOvK_QArxw6PiT0hn3A9ONaCxUf8fkL7ArZC4JRjwUPcosIHOFT_CssrxcHT0zAXjsRC9VVbxMZMUmFB9cxD9E0bug_GX2FTtYqxgTxmmriRkYg" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="632" data-original-width="709" src="https://blogger.googleusercontent.com/img/a/AVvXsEgfFYo5eQYIQm66bnMGZfNdFxNMNe91WMq7pH7qDFi_oimu3VdqnDTDH5dXgfHyi7BuraA2jOwhOvK_QArxw6PiT0hn3A9ONaCxUf8fkL7ArZC4JRjwUPcosIHOFT_CssrxcHT0zAXjsRC9VVbxMZMUmFB9cxD9E0bug_GX2FTtYqxgTxmmriRkYg=s16000" /></a></div><br />This is one of the most inspiring things I learned from the code. At first I had no clue how to build this shape nicely, but the implementation was so short and elegant. It is a linear interpolation between two shapes, a positive sphere (actually a ellipsoid) and a negative sphere. The interpolation factor is a function of phi. Near the north pole (now being pushed to the south) it is the negative sphere and near the equator it is the positive sphere. </div><div><br /></div><div>This is actually how Bezier curves work, but I never realized that we can do the same for 3d surfaces!</div><div><br /></div><h2 style="text-align: left;">The Twist</h2><div>A cobra!</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhX-v-xYdbx3EPkqf28ENCkgfsEiKOsBP7q0xR0h3NbJzvrA0LGNNXuwMsTbmSKwx1Fm7X59Tqqg0q2cW7nGlFw71PpJKO-rgQvEpYR1WME1SltPlwBmm8uKafytP_1hWooOo8XbLCasFBPJq8K-UX5p9WEmUvwRqJguRnhqkiFZgE2-HQUZ2oHOg" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="653" data-original-width="650" src="https://blogger.googleusercontent.com/img/a/AVvXsEhX-v-xYdbx3EPkqf28ENCkgfsEiKOsBP7q0xR0h3NbJzvrA0LGNNXuwMsTbmSKwx1Fm7X59Tqqg0q2cW7nGlFw71PpJKO-rgQvEpYR1WME1SltPlwBmm8uKafytP_1hWooOo8XbLCasFBPJq8K-UX5p9WEmUvwRqJguRnhqkiFZgE2-HQUZ2oHOg=s16000" /></a></div></div><div><br /></div><div>The twist is no longer a mystery, once I learned about the interpolation trick: we rotate the shape along different axes near the equator and near the north pole, and everything in the middle is an interpolation.</div><div><br /></div><div>However there is still an interesting & confusing piece. Each point on the equator is locally rotated along with the axis that passes through that point and the sphere center. This could be quite counterintuitive at the first glance: does it mean the points are not moved at all? This is actually true if there is no corrugation, but the corrugation gives a local offset to the east or the west, the offset is a function of theta: size * sin(4 * PI * theta), where size is determined by phi.</div><div><br /></div><div>When the offset is zero, the point is stable during the twist. There are 4 stable points on each stripe:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen="" class="BLOG_video_class" height="375" src="https://www.youtube.com/embed/v47zB_ZFmXg" width="451" youtube-src-id="v47zB_ZFmXg"></iframe></div><br /><div>Interestingly, consider a point p1 on the equator that is near a stable point p2. Suppose p1 and p2 are respectively determined by theta1 and theta2, and let dt = theta1-theta2. </div><div>Now we have |p1-p2| ~= R*sin(dt) ~= R*dt</div><div>Because p2 is a stable point, sin (4 * PI * theta2) = 0, therefore the offset at p1 is:</div><div>size * sin(4 * PI * theta1) ~= size * 4 * PI * dt.</div><div>So the offset at p1 is linear to the distance between p1 and p2. This gives an illusion that p1 is rotating along with p2's axis (towards center), while p1 is actually rotating along its own axis (after applying the offset).</div><div><br /></div><h2 style="text-align: left;">The Conclusion</h2><div>Now the rest of the animation is more or less straightforward, just controlling all the parameters and add more interpolations.</div><div><br /></div><div>Before making this animation, I only heard about "geometry nodes are powerful" but I have never tried them myself. I can confirm that they are indeed powerful and very easy to use. Thanks to Blender I can greatly simplify mathIsART's code:</div><div><br /></div><div>- I don't need to worry about rendering at all. Blender takes care of everything.</div><div>- I don't need to implement something like "rotate along an axis", because there are already geometry nodes or modifiers for that.</div><div>- I can easily construct a function of a desired shape (e.g. using the curve widget), instead of trying to manually craft a magic function.</div><div>- Sometimes grid points phi (or theta) will not distribute evenly if a function is a applied. While 100% sure, I think mathIsART's code sometimes tries to migitate that by remapping phi, kind of like a timing function. However, I just ignore it and add more grid points. (Later I learned this is called arclength reparameterization)</div><div><br /></div><div>By making this animation, not ony did I learned about parameterization, but I also refreshed my understanding of partial derivatives. What a journey!</div><div><br /></div><h2 style="text-align: left;">Appendix: More about the Problem</h2><div>There are many other solutions to this problem. Here is another elegant one: <a href="https://www.youtube.com/watch?v=cdMLLmlS4Dc">video</a>, <a href="https://new.math.uiuc.edu/optiverse/">info</a>, <a href="https://rreusser.github.io/explorations/sphere-eversion/">demo</a>.</div><div><br /></div><div>However, unfortunatlely I am not able to fully understand what's going on at the moment. Maybe I will try to take another look later.</div><div></div></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-2565052654642732532022-09-22T21:55:00.006+02:002022-09-23T13:50:06.808+02:00Thoughts on Herb Sutter's cppfront<p>Recently I learned about <a href="https://github.com/hsutter/cppfront">cppfront</a>, which is an experimental new syntax for C++ by Herb Sutter. It was nicely explained in CppCon, and I really enjoyed watching <a href="https://youtu.be/ELeZAKCN4tY">the video</a>.</p><p>Roughly I'd view it as Rust with C++ interop. It's kind of some syntax sugar, preprocessor or a dialect. It has also enforced style guide or annotations that are compiler-aware. I like it mostly, I feel excited. </p><p>And let me try to explain in a logic way.</p><h4 style="text-align: left;">The Syntax</h4><p>I don't like it, nor do I hate it. I asked myself, do I not like it, just because I am not familar with it? The answer is yes. So it's my problem, not cpp2's. It didn't took me too much time to get myself comfortable (but not fluent) with Rust, so I think cpp2 won't be a problem.</p><h4 style="text-align: left;">Comparing with Rust</h4><p>I got this several times while reading the docs or watching the video: </p><p></p><ul style="text-align: left;"><li>If something is bad, let's remove it from the language instead of keeping teaching "don't do this, don't do that". Examples: NULL, union, unsafe casts.</li><li>If something is good, let's make it by default. Examples: std::move, std::unique_ptr, [[nodiscard]].</li></ul><p></p><p>Well, this are not new to me, I saw almost exactly the same words before. Rust is a good example of designs with such goals in mind.</p><p>Suppose we want really want to achieve them with current C++:</p><p>Some may be achieved by adding new flags to existing compilers, say, a new flag to make [[nodiscard]] default for all functions.</p><p>Some may be achieved by teaching and engineer-enforced style guides, e.g. the usage of std::move or std:unique_ptr.</p><p>Some may be achieved by static analyzers, linters, e.g. usage before initialization (only partially achievable)</p><p>Some may be achieved by libraries, including macros, e.g. returning multiple variables via tuple or a temporary struct.</p><p>However it'd never be as good as native support in compilers. </p><p>A small example is absl::Status in abseil vs the question mark syntax in Rust. And a large example would be the general memory safety, ownership in Rust.</p><p>I always think that C, C++ and Rust are essentially the same. All technologies are there for building compilers and analyzers, it's just a matter of:</p><p></p><ul style="text-align: left;"><li>How much information do we provide to compilers? E.g. ownership, types.</li><li>How much freedom do we have? E.g. raw pointer arithmetics, arbitrary type casts.</li></ul><div>I believe that by inventing enough annotations (e.g. ownerships, lifetime) and limiting some language features (e.g. raw pointers arithmetics), it'd be possible to make C++ "Rust alike". But is it worth it?</div><p></p><h3 style="text-align: left;">The Biggest Obstacle </h3><p>I agree with Herb that "backwards compatibility" is the biggest and probably the only obstacle. Again, Rust is a perfect example without such constraint.</p><p>Honestly the usage of "<<" and ">>" for streams already sounds a bit odd, but I've never had strong feelings because that was in the hello world code while I was learning C++, and (luckily?) I didn't have a C background. </p><p>It surprised me that the meaning of "auto" was (successfully) modified in C++11. And the usage of "&&" for rvalue references also sounds a bit strange to me. It feels to me that the process is "we have a new shiny feature, let's see what existing symbols can we borrow to give a new meaning". A similar case is the usage of "const" at the end of class member function declarations. I remember I asked someone, why here? And the answer was: probably they didn't have a better place to put it.</p><p>My guess (which could be really random, since I don't really know much behind-the-scene design thoughts) would be, it'd cause minimum changes to existing compilers. Or even better, no change at all: overloading operators has been a proper C++ feature, so why not repurposing it for streams since "left shift" does not make sense anyways? (Therfore nobody is/should use it as a shift operator).</p><p>I like the idea that cpp2 and cpp1 can interop at the source level. If cpp2 will become successful, some features may be left in cpp1 as the "unsafe features", like C# and Rust do.</p><p>I like Rust, but I've been hesitating to use it because I know most of the time I'd have to start from scratch. C and C++ are good examples here. If I need a PNG library, I'd download libpng, read the doc and start coding. Python is another good example. </p><p>I feel that Rust is still too young. The language itself (mostly syntax & compiler but without the ecosystem) is probably mature enough for Linux kernel, but I'm not sure about the libraries, after all I have not seen many lib**-rust in the Ubuntu repository.</p><p>Yes I probably should look at the Cargo registry, and yes my knowledge is quite outdated, so probably the libraries are already enough for me, but I'd yet to double check. But this is the problem: I'm already path-depending C++ (and Python), so "easily migration from C++" would be a huge selling point for me.</p><h3>The Preprossor</h3><div>Quoting Herb "there is no preprocessor in cpp2". But actually I think cppfront itself, or at least part of it, is a preprocessor.</div><div><br /></div><div>My favorite part is the optional out-of-bound check for the "x[i]" operator, the check may be turned on or off by compiler flags, it's not as ugly/long as "x.super_safe_index_subscript_with_optional_boundary_check(i)", not is it as wild as "MAGICAL_MACRO_SUBSCRIPT(x, i)". I also like that it's a loose protocol with the compiler: it automatically works as long as x.size() is defined. Somehow like how range-based for works.</div><div><br /></div><div>So if we want this feature, either we redefine the "[]" operator in C++, or translate every single "x[i]" to "x.super_safe_index_subscript_with_optional_boundary_check(i)", which is a preprocessor. (maybe compiler is the correct term, but it feels like a preprocessor to me).</div><div><br /></div><div>I still remember a reading about technique in a game programming book, before Lua became popular. The technique was to define a set of C/C++ macros, to create a mini-scriping language. Then engineers may add lots of game logic using this scripting language, that will be way more readable and less error-prone than C++. Somehow cppfront resembles that technique a bit.</div><h3 style="text-align: left;">Conclusions</h3><div>I like almost everything about cpp2 so far. But it is (too) early, and there could be lots of hidden/implied issues that I don't know yet, or that are not revealed yet.</div><div><br /></div><div>Rust would be a very good competitor here, only if it has C++ interop at the source level. It probably never will, but that's OK.</div><div><br /></div><div>Carbon sounds very similar as well. Its README mentioned the analogue of "JavaScript and TypeScript". Interestingly, Herb mentioned the same thing as well for cpp2. And this is why I see huge overlaps between cpp2 and Carbon. I do hope both projects will not divert too far from each other, such that most knowledge, technologies or code can be shared.</div><div><br /></div><div>Overall I feel excited about recent changes and proposals of C++. My overly-simplified view is C++11 was driven by nice features in languages like C# and Java. And this new wave is probably driven by nice features in languages like Go and Rust. </div><div><br /></div><div>Hopefully, one day, the reason that I like C++ will no longer be Stockholm syndrome.</div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-70884673820826881312022-09-22T20:28:00.002+02:002022-09-22T20:28:28.697+02:00清理Ubuntu软件包<p>我的小服务器上一直装了个Ubuntu Desktop,不过安装之后一直没用过GUI,而且各种依赖的包有时也挺烦人的,比如gvfs和tracker自带的systemd user service,我还得手动给若干用户禁用掉。</p><p>本来我是想着留个Desktop,万一紧急情况可以上网查查命令。不过有网的话,最差情况我应该也能临时装一个X和浏览器,估计问题不太大。</p><p>于是我决定把Ubuntu Desktop换成Ubuntu Server,主要还是把gnome的包都删了。</p><p>一番折腾以后,安装包的数量从大约1800降到了800以下。舒服!</p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-78296505277993751252022-09-20T20:24:00.008+02:002022-09-21T00:34:35.139+02:00Moving Items Along Bezier Curves with CSS Animation (Part 2: Time Warp)<p>This is a follow-up of my <a href="https://blog.wang-lu.com/2022/08/moving-along-bezier-curvespaths-with.html">earlier article.</a> I realized that there is another way of achieving the same effect.</p><p><a href="https://css-tricks.com/advanced-css-animation-using-cubic-bezier/">This article</a> has lots of nice examples and explanations, the basic idea is to make very simple @keyframe rules, usually just a linear movement, then use timing function to distort the time, such that the motion path becomes the desired curve.</p><p>I'd like to call it the "time warp" hack.</p><p><br /></p><h2 style="text-align: left;">Demo</h2><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/wvjeeqJ?default-tab=result" style="width: 100%;" title="Interactive cubic Bezier curve + CSS animation">
See the Pen <a href="https://codepen.io/coolwanglu/pen/wvjeeqJ">
Interactive cubic Bezier curve + CSS animation</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div><br /><h2 style="text-align: left;">How does it work?</h2><div><br /></div><div>Recall that a cubic Bezier curve is defined by <a href="https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves">this formula</a>:</div></div><div><br /></div><div>\[B(t) = (1-t)^3P_0+3(1-t)^2tP_1+3(1-t)t^2P_2+t^3P_3,\ 0 \le t \le 1.\]</div><div><br /></div><div>In the 2D case, \(B(t)\) has two coordinates, \(x(t)\) and \(y(t)\). Define \(x_i\) to the be x coordinate of \(P_i\), then we have:</div><div><br /></div><div>\[x(t) = (1-t)^3x_0+3(1-t)^2tx_1+3(1-t)t^2x_2+t^3x_3,\ 0 \le t \le 1.\]</div><div><br /></div><div>So, for our animated element, we want to make sure that the x coordiante (i.e. the "left" CSS property) is \(x(t)\) at time \(t\). </div><div><br /></div><div>Because \(x(0)=x_0\) and \(x(1)=x_3\), we know that the @keyframes rule must be defined as</div><div><br /></div>
<pre>@keyframes move-x {
from { left: x0; }
to { left: x3; }
}</pre><div>Now to determine the timing function, suppose the function is</div><pre>cubic-bezier(u1, v1, u2, v2)</pre><div>Note that this is again a 2D cubic Bezier curve, defined by four points \((0, 0), (u_1, v_1), (u_2, v_2), (1, 1)\). And the function for each coordinate would be:</div><div><br /></div><div>\[ u(t) = 3(1-t)^2tu_1 + 3(1-t)t^2u_2 + t^3 \]</div><div>\[ v(t) = 3(1-t)^2tv_1 + 3(1-t)t^2v_2 + t^3 \]</div><div><br /></div><div>Recall that, according to the CSS spec, at any time \(t\), the animate value \(\text{left}(t)\) is calculated as:</div><div><br /></div><div>\[ \text{left}(t) = x_0 + v(t')(x_3 - x_0), \text{where}\ u(t')=t \]</div><div><br /></div><div>Our first step is to set \(u_1=1/3\) and \(u_2=2/3\), such that \(u(t)=t\) for all \(t\).</div><div><br /></div><div>Then we set:</div><div>\[ v_1 = \frac{x_1-x_0}{x_3-x_0}, v_2 = \frac{x_2-x_0}{x_3-x_0} \]</div><div><br /></div><div>This way we have</div><div><br /></div><div>\[ v(t) = \frac{x(t) - x_0}{x_3-x_0} \]</div><div><br /></div><div>Combining everything together, we know that if we set the animation-timing-function as</div><div><br /></div><div>\[ \text{cubic-bezier}(\frac{1}{3}, \frac{x_1-x_0}{x_3-x_0}, \frac{2}{3}, \frac{x_2-x_0}{x_3-x_0}) \]</div><div><br /></div><div>then we have \(\text{left}(t)=x(t)\) as desired.</div><div><br /></div><div>Simliarly we can define @keyframes and animation-timing-function for \(y(t)\), then our CSS animation is completed.</div><div><br /></div><div>Note: obviously the method does not work when \(x_0=x_3\) or \(y_0=y_3\), but in practice we can add a tiny offset in such cases.</div><h2 style="text-align: left;">Animation Timing</h2><div><br /></div><div>Observe that \(u(t)\) controls the mapping between the animation progress and the variable \(t\) of the curve. \(u_1=1/3\) and \(u_2=2/3\) are chosen to achieve the default linear timing. We can tweak the values of \(u_1\) and \(u_2\) to alter the timing.</div><div><br /></div><div>Note that the methods from the previous article supports any timing functions, including "steps()" and "cubic-bezier()".</div><div><br /></div><div>It's easy to see that a "cubic-bezier(u1, 1/3, u2, 2/3)" timing function for the previous article would be the same as setting the same values of \(u_1\) and \(u_2\) for the "time warp" version. In other words, animation timing is limited here, we have only the input progress mapping, but not the output progress mapping.</div><div><br /></div><div>Of course the reason is we are already using the output progress mapping for the time warp effect.</div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com3tag:blogger.com,1999:blog-33534782.post-7877749772411240262022-09-15T02:52:00.003+02:002022-09-15T14:54:23.624+02:00Restricting Network Access of Processes<p>I recently read <a href="https://blog.lilydjwg.me/2022/9/7/offline-software.216461.html">this article</a>, which talks about restricting (proactive) internet access of a process.</p><p>It is easy to completely disable internet/network access, by throwing a process into a new private network namespace. I think all popular sandboxing tools support it nowadays:</p><p></p><ul style="text-align: left;"><li>unshare -n</li><li>bwrap --unshare-net</li><li>systemd.service has PrivateNetwork=yes</li><li>docker has internal network</li></ul><div>But the trickier, and more realistic scenario is:</div><div><ul style="text-align: left;"><li>[Inbound] The process needs to listen one or more ports, and/or</li><li>[Outbound] The process needs to access one or more specific IP address/domain</li></ul><div>I can think of a few options.</div><div><br /></div><h3 style="text-align: left;">Option 1: Firewall Rules</h3><div><br /></div><div>Both iptables and nftables support filter packets by uid and gid. So the steps are clear:</div><div><ul style="text-align: left;"><li>Run the process with a dedicate uid and/or gid</li><li>Filter packets in the firewall</li><li>If needs, regularly query DNS and update the allowed set of IP addresses.</li><ul><li>This is somehow similar to <a href="https://github.com/WireGuard/wireguard-tools/blob/master/contrib/reresolve-dns/reresolve-dns.sh">reresolve-dns.sh</a> from WireGuard.</li></ul></ul><div>This option is not very complicated, and I think the overhead is low. While the DNS part is a bit ugly, it is flexiable and solves both inbound and outbound filtering.</div><div><br /></div><div>On the other hand, it might be a bit difficult to maintain it, because the constraints (firewall rules) and the processes are in different places.</div></div><div><br /></div><h3 style="text-align: left;">Option 2: Systemd Service with Socket Activation</h3><div><br /></div><div>Recently I've been playing with <a href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Sandboxing">sandboxing flags</a> in systemd. Especially systemd-analyze. Our problem can be solved with systemd + socket activation like this:</div><div><ul style="text-align: left;"><li>Create my-service.socket that listens to the desire address and port</li><li>Create my-service.service for the process, with PrivateNetwork=yes.</li><ul><li>The process has no access to network, it receives a socket from systemd instead, i.e. socket activation</li></ul></ul>However, it only works if the process supports socket activation. If not, there is a handy tool <a href="https://www.freedesktop.org/software/systemd/man/systemd-socket-proxyd.html">systemd-socket-proxyd</a> designed for this case. There are nice examples in the manual.</div><div><br /></div><div>I tested the following setup:</div><div><ul style="text-align: left;"><li>my-service-proxy.socket, which activate the corresponding service</li><li>my-service-proxy.service, which runs systemd-socket-proxyd.</li><ul><li>The service must have PrivateNetwork=yes and JoinsNamespaceOf=my-service.service</li></ul><li>my-service.service, the real process, with PrivateNetwork=yes</li></ul><div>This way, the process can accept connections at a pre-defined address/port, but has no network access otherwise.</div><div><br /></div><div>It works for me, but with a few shortcomings:</div></div><div><ul style="text-align: left;"><li>It only worked for system services (running with root systemd). I suspected that it might work with PrivateUsers=yes, but it didn't.</li><li>It is quite some hassle to write and maintain all these files.</li></ul><div>For outbound traffic, systemd can filter by <a href="https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#IPAddressAllow=ADDRESS%5B/PREFIXLENGTH%5D%E2%80%A6">IP addresses</a>, but I'm not sure about ports. For domain filtering, it might be possible to borrrow ideas from the other two options, but I suppose it won't be easy.</div></div><div><br /><h3 style="text-align: left;">Option 3: Docker with Proxy</h3></div><div><br /></div><div>If the process in question is in a Docker container, inbound traffic is already handled by Docker (via iptables rules).</div><div><br /></div><div>For outbound traffic, the firewall option also works well for IP addresses. Actually it might be easier to filter packets this way.</div><div><br /></div><div>For domains, there is another interesting solution: use a proxy. Originally I had some vague ideas about this option, then I found <a href="https://fruty.medium.com/how-to-restrict-outbound-traffic-on-a-docker-infrastructure-7effc45e313d">this article</a>. I learned a lot from it and I also extended it.</div><div><br /></div><div>To explain how it works, here's an example docker compose snippet:</div><div><br /></div><div><span style="font-family: Source Code Pro;">networks:</span></div><div><span style="font-family: Source Code Pro;"> network-internal:</span></div><div><span style="font-family: Source Code Pro;"> internal: true</span></div><div><span style="font-family: Source Code Pro;"> network-proxy:</span></div><div><span style="font-family: Source Code Pro;"> ...</span></div><div><span style="font-family: Source Code Pro;"><br /></span></div><div><span style="font-family: Source Code Pro;">services:</span></div><div><span style="font-family: Source Code Pro;"> my-service:</span></div><div><span style="font-family: Source Code Pro;"> # needs to access https://my-domain.com</span></div><div><span style="font-family: Source Code Pro;"> networks:</span></div><div><span style="font-family: Source Code Pro;"> - network-internal</span></div><div><span style="font-family: Source Code Pro;"> ...</span></div><div><span style="font-family: Source Code Pro;"> my-proxy:</span></div><div><span style="font-family: Source Code Pro;"> # forwards 443 to my-domain.com:443</span></div><div><span style="font-family: Source Code Pro;"> networks:</span></div><div><span style="font-family: Source Code Pro;"> - network-internal</span></div><div><span style="font-family: Source Code Pro;"> - network-proxy</span></div><div><span style="font-family: Source Code Pro;"> ...</span></div><div><br /></div><div><br /></div><div>The idea is that my-service runs in network-internal, which has no Internet access. But my-service may access selected endpoints via my-proxy.</div><div><br /></div><div>There are two detailed problems to solve:</div><div><ul style="text-align: left;"><li>Which proxy to use?</li><li>How to make my-service talks to my-proxy?</li></ul><h4 style="text-align: left;"><br /></h4><h4 style="text-align: left;">Choosing the Proxy</h4><div><br /></div><div>In the article the author uses nginx. Originally I had thought it'd be a mess of setting up SSL (root) certificates. But later I learned that nginx can act as a <a href="https://nginx.org/en/docs/stream/ngx_stream_core_module.html">stream proxy</a> that forwards TCP/UDP ports, which make thing much easier.</div><div><br /></div><div>On the other hand, I often use socat to forwards ports as well, which can also be used here. </div><div><br /></div><div>Comparing both:</div><div><ul style="text-align: left;"><li>socat is lighter-weighted, the <a href="https://hub.docker.com/r/alpine/socat">alpine/socat</a> docker image is about 5MB, while the <a href="https://hub.docker.com/_/nginx/tags">nginx</a> docker image is about 55MB.</li><li>socat can be configured via command line flags, but nginx needs a configuration file.</li><li>socat can support only one port, but nginx can manage multiple ports with one instance.</li></ul></div><div>So in practice I'd use socat for one or two ports, but I'd switch to nginx for more. It'd be a hassle to create one container for each port.</div><h4 style="text-align: left;"><br /></h4><h4 style="text-align: left;">Enabling the Proxy</h4></div><div><br /></div><div>If my-service needs to be externally accessible, the ports must be forwarded and exposed by my-proxy.</div><div><br /></div><div>For outbound traffic, we want to trick my-service, such that it will see my-proxy when it wants to resolve, for example, my-domain.com.</div><div><br /></div><div>I'm aware of three options:</div><div><br /></div><div>#1 That article uses <a href="https://docs.docker.com/compose/networking/#links">links</a>, but the option is designed for inter-container communcations, and it is <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#links">deprecated</a>.</div><div><br /></div><div>#2 Another option is to assign a static IP of my-proxy, then add an entry to <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#extra_hosts">extra_hosts</a> of my-service.</div><div><br /></div><div>#3 Add an <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases">aliases</a> entry of my-proxy on network-internal.</div><div><br /></div><div>While #3 seems better, it is does not just work like that, because when my-proxy wants to send the real traffic to my-domain.com, it will actually send to itself because of the aliases.</div><div><br /></div><div>To fix it, I have a very hacky solution:</div><div><br /></div><div><div><span style="font-family: Source Code Pro;">networks:</span></div><div><span style="font-family: Source Code Pro;"> network-internal:</span></div><div><span style="font-family: Source Code Pro;"> internal: true</span></div><div><span style="font-family: Source Code Pro;"> network-proxy:</span></div><div><span style="font-family: Source Code Pro;"> ...</span></div><div><span style="font-family: Source Code Pro;"><br /></span></div><div><span style="font-family: Source Code Pro;">services:</span></div><div><span style="font-family: Source Code Pro;"> my-service:</span></div><div><span style="font-family: Source Code Pro;"> networks:</span></div><div><span style="font-family: Source Code Pro;"> - network-internal</span></div><div><span style="font-family: Source Code Pro;"> ...</span></div><div><span style="font-family: Source Code Pro;"> my-proxy1:</span></div><div><span style="font-family: Source Code Pro;"> # forwards 443 to my-proxy2:443</span></div><div><span style="font-family: Source Code Pro;"> networks:</span></div><div><span style="font-family: Source Code Pro;"> network-internal:</span></div><div><span style="font-family: Source Code Pro;"> aliases:</span></div><div><span style="font-family: Source Code Pro;"> - my-domain.com</span></div><div><span style="font-family: Source Code Pro;"> network-proxy:</span></div><div><span style="font-family: Source Code Pro;"> ...</span></div><div><span style="font-family: Source Code Pro;"> my-proxy2:</span></div></div><div><span style="font-family: Source Code Pro;"> # forwards 443 to my-domain.com:443</span></div><div><span style="font-family: Source Code Pro;"> networks:</span></div><div><span style="font-family: Source Code Pro;"> - network-proxy</span></div><div><span style="font-family: Source Code Pro;"> ...</span></div><div><br /></div><div><br /></div><div>In this version, my-proxy1 injects the domain and thus hijacks traffic from my-service. Then my-proxy1 forwards traffic to my-proxy2. Finally my-proxy2 forwards traffic to the real my-domain.com. Note that my-proxy2 can correctly resolve the domain because it is not in network-internal.</div><div><br /></div><div>On the other hand, it might be possible to tweak the process to ignore local hosts, but I'm not aware of any easy soltuion. </div><div><br /></div></div><div>I use #3 in practice despite it is ugly and hacky, mostly because I don't want to set up static IP for #2.</div><div><br /></div><div>More on Docker, or Docker Compose, it is possible to specify the <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#network">network</a> for building containers, which could be handy.</div><div><br /></div><h3 style="text-align: left;">Conclusions</h3><div><br /></div><div>In practice I use option 3 with a bit of option 1. </div><div><br /></div><div>With option 3, if I already have a Docker container/image, it'd be just adding a few lines in docker-compose.yml, maybe plus a short nginx.conf file.</div><div><br /></div><div>With option 1, the main concern is the rules may become out of sync with the processes. For example, if the environment of the process is changed (e.g. uid, pid, IP address etc), I may need to update the firewall rules to stay up-to-date. But this could be easily missed. I'd set up firewall rules for stable services and generic rules</div><div><br /></div><div>Option 2 could be useful in some cases, but I don't enjoy writing the service files. And it seems harder to extend (e.g. add a proxy).</div><p></p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-42112470066168675612022-09-13T03:01:00.003+02:002022-09-13T03:01:40.608+02:00Migrating from iptables to nftables<p>nftables has been enabled by default in latest Ubuntu and Debian, but not fully supported by Docker.</p><p>I've been hestitating about migrating from iptables to nftables, but managed to do it today.</p><p>Here are my thoughts.</p><h3 style="text-align: left;">Scripting nftables</h3><div>The syntax of iptables and nftables are different, but not that different, both are more or less human readable. However, nftables is clearly more friendly for scripting.</div><div><br /></div><div>I spent quite some time in a python script to generate a iptables rule set, and I was worried that I need lots of time migrating the script. Aftering studying the syntax of nftables, I realized that I could just write /etc/nftables.conf directly. </div><div><br /></div><div>In the conf file I can manage tables and chains in a structured way. I'm free to use indentations and new lines, and I no longer need to write "-I CHAIN" for every rule.</div><div><br /></div><div>Besides, I can group similar rules (e.g. same rule for different tcp ports) easily, and I can define variables and reuse them. </div><div><br /></div><div>Eventually I was able to write a nice nftables rule set quickly with basic scripting syntax. It was not as powerful as my custom python script, but it is definitely easier to write. Further, I think it might be worth learning mapping in the future.</div><div><br /></div><h3 style="text-align: left;">Tables & Chains in nftables</h3><div>Unlike iptables, nftables is decentralized. Instead of pre-defined tables (e.g. filter) and chains (e.g. INPUT), nftables uses hooks and priorities. It sounds like event listeners in JavaScript.</div><div><br /></div><div>One big difference is: a packet is dropped if it is dropped any matching rule, and a packet is accepted only if all relevant chains accept the packet. Again, this is similar to event listeners. On the other hand, in iptables, a packet is accepted if it is accepted by any rule. It sounds a bit confusing at the beginning, but I think nftables is more flexible, especially in my cases, see below.</div><div><br /></div><h3 style="text-align: left;">Docker & nftables</h3><div>Docker does not support nftables, but it add rules via iptables-nft. It was painful to managed iptables rules with Docker:</div><div><ul style="text-align: left;"><li>Docker creates its own DOCKER and DOCKER-USER chains, which may accept some rules.</li><li>If I need to control the traffic from/to containers, I need to make sure that the rules are defined before or in DOCKER-USER.</li><li>Docker may or may not be started at boot. And Docker adds DOCKER to INPUT, so I need to make sure that my rules are in effect in all cases.</li></ul><div>Well all the mess is because: in iptables, a packet is accepted if it is accepted by any rule. That means I must insert my REJECT rules before DOCKER/DOCKER-UESR, which might accept the packet.</div><div><br /></div><div>This is no longer an issue in nftables! I can simply define my own tables and reject some packets as I like.</div><div><br /></div><div>Finally, I don't need to touch the tables created by Docker via iptables-nft, instead I can create my own nft tables.</div><div><br /></div></div><h3 style="text-align: left;">Conclusions</h3><div>I had lots of worries about nftables, about scripting and working with Docker. As it turned out, none was actually an issue thanks to the new design of nftables!</div><div><br /></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-75794794606938812232022-09-11T17:29:00.007+02:002022-09-23T03:11:01.666+02:00Migrating to Rootless Docker<p> There are three ways of running Docker:</p><p></p><ul style="text-align: left;"><li>Privileged: dockerd run with root, container root = host root</li><li>Unprivileged: dockerd run with root, container root = mapped user</li><li>Rootless: dockerd run with some user, container root = some user</li></ul><div>I've been hestitating between Unprivileged and Rootless. On one hand, rootless sounds like a great idea; on the other hand, some considers unprivileged user namespace as a <a href="https://wiki.archlinux.org/title/Security#Sandboxing_applications" target="_blank">security risk</a>.</div><div><br /></div><div>Today I decided to migrate all my unprivileged containers to rootless ones. I had to enable unprivileged user namespace for a rootless LXC container anyways.</div><div><br /></div><div><br /></div><h2 style="text-align: left;">A Cryptic Issue</h2><div><br /></div><div>The migration is overall smooth, except for a cryptic issue: sometimes DNS does not work inside the container.</div><div><br /></div><div>The symptom is rather unusual: curl works but apt-get does not work. For quite a while I'd thought that apt-get uses some special DNS mechanism.</div><div><br /></div><div>After some debugging, especially comparing files /etc/ between a unprivileged container and a rootless container, I realized that non-root users cannot access /etc/resolve.conf. This is also quite hidden because I apt-get uses a non-root user to fetch HTTP.</div><div><br /></div><div>Further digging, eventually I figured that there are special POXIS acl on ~/.local/share/docker/containers, and I should set o+rx by default.</div><div><br /></div><h2 style="text-align: left;">Pros</h2><div>It is definitely an advantage to elimiate root processes. It is also now easier to manage the containers. I no longer need special visudo files to call maintenance scripts.</div><div><br /></div><div>With rootless containers, all network interfaces are in a dedicate namespace. A nice side-effect is that all iptables rules will be constrained in this namespace as well. Services running on the host are no longer accessible by the containers, if they are listening on 0.0.0.0 or localhost. Further, Docker will no longer pollute my iptables rules. It will also be easier to migrate to nftables (on the host)</div><div><br /></div><h2 style="text-align: left;">Cons</h2><div>There is another side-effect with network namespaces: it is trickier to manage port forwarding and firewall rules between the host and containers. slirp4netns and docker proxy handles most parts well, but still a big ugly. Perhaps lxc-user-nic might work better, but it is only experimentally supported in rootlesskit at the moment.</div><div><br /></div><p></p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-75075511973359455432022-08-02T21:11:00.014+02:002022-09-21T00:36:15.575+02:00Moving Items Along Bezier Curves with CSS Animation (Part 1: Constructions)<p>TLDR: This article is NOT about cubic-bezier(), and it is not about layered CSS animations (for X and Y axes respectively). It is about carefully crafted combined animation, that moves an element along any quadratic/cubic Bezier curve.</p><p>UPDATE: Here's the link to <a href="https://blog.wang-lu.com/2022/09/moving-items-along-bezier-curves-with.html">part 2</a>.</p><p>Following my <a href="https://blog.wang-lu.com/2022/08/animation-state-study-of-css-3d.html">previous post</a>, I continued investigating competing CSS animations, which are two or more CSS animations affecting the same property.</p><p>I observed that two animations may "compete". In the following example, the box has two simple linear animations, move1 and move2. It turns out the box actually moves along a curved path:</p><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/qBoxPBp?default-tab=result" style="width: 100%;" title="CSS Bezier Path">
See the Pen <a href="https://codepen.io/coolwanglu/pen/qBoxPBp">
CSS Bezier Path</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><p>So clearly it must be the combined effects from both animations. Note that in move2, the `from` keyframe was not specified. It'd look like this if it is specified:</p><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/OJvQxXe?default-tab=result" style="width: 100%;" title="For Blog 2022-08-02">
See the Pen <a href="https://codepen.io/coolwanglu/pen/OJvQxXe">
For Blog 2022-08-02</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><p>In this case, it seems only the second animation takes effects.</p><p>Actually this is not surprising, in the first case, the starting point of move2 would be "the current location from move1". But in the second case, move2 does not need any "current location", so move1 would take no effect.</p><p>I further examined the actual behavior of the first case. If move1 is "move from \(P_0\) to \(P_1\)" and move2 is "move to \(P_2\)". At time \(t\):</p><p>- The animated location of move1 is \(Q_1=(1-t)P_0 + tP_1\)</p><p>- The animated location of move1+move2 is \(Q_2=(1-t)Q_1 + tP_2\)</p><p>This formula actually looks very similar to the Bezier curve, but I just double checked from Wikipedia, they are not the same.</p><p>Fun fact: I came up with this string art pattern during high school, I realized that it is not a circle arc, but I didn't know what kind of curve it is. Now I understand that it is a Bezier curve.</p><a href="https://commons.wikimedia.org/wiki/File:Quadratic_Beziers_in_string_art.svg" title="Cmglee, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons"><img alt="Quadratic Beziers in string art" src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/09/Quadratic_Beziers_in_string_art.svg/256px-Quadratic_Beziers_in_string_art.svg.png" width="256" /></a><p><br /></p><h2 style="text-align: left;">Build a quadratic Bezier animation path with two simple animations</h2><p></p><p>The quadratic Bezier curve is defined by <a href="https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves">this formula</a>:</p><p>\[B(t) = (1-t)^2P_0 + 2(1-t)tP_1 + t^2P_2, 0\le t \le 1\]</p><p>But our curve looks like \(f(t) = (1-t)^2 P_0 + t(1-t)P_1' + tP_2\). Note that I use \(P_1'\) to distinguish it from P1 above.</p><p>If we set \(P_1'=2P_1-P_2\), we'll see that f(t)=B(t) for all t. So it <i>is</i> a Bezier curve, just the control point is a bit different.</p><p>Here's an interactive demo, which is based on <a href="https://codepen.io/Shadosky/pen/jrEYeP">this codepen</a>.</p><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/oNqEqzK?default-tab=result" style="width: 100%;" title="Interactive quadratic Bezier curve + CSS animation">
See the Pen <a href="https://codepen.io/coolwanglu/pen/oNqEqzK">
Interactive quadratic Bezier curve + CSS animation</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><p><br /></p><h2 style="text-align: left;">An Alternative Version</h2><p>Another option is to follow the construction of a Bezier curve:</p><p><a href="https://commons.wikimedia.org/wiki/File:B%C3%A9zier_2_big.gif" title="Phil Tregoning, Public domain, via Wikimedia Commons"><img alt="Bézier 2 big" src="https://upload.wikimedia.org/wikipedia/commons/3/3d/B%C3%A9zier_2_big.gif" width="256" /></a></p><p>This version needs slightly more code, but it does not require much math. Just observe that a quadratic Bezier curve is a linear interpolation of two moving points, which are in turn obtained by another two linear interpolations.</p><p>All these linear interpolation can be easily implemented with CSS animation on custom properties. Here is an example:</p><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/ExEQErq?default-tab=result" style="width: 100%;" title="Interactive quadratic Bezier curve + CSS animation">
See the Pen <a href="https://codepen.io/coolwanglu/pen/ExEQErq">
Interactive quadratic Bezier curve + CSS animation</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><p>Here I just have one animation, which does multiple linear interpolation at the same time. In this case I have to make sure all animated custom properties are defined with @property, which was not the case in the previous example.</p><h2 style="text-align: left;">How about cubic Bezier curves?</h2><div>Both versions can be extended to make animation along cubic Bezier paths.</div><div><br /></div><div>The first version needs a bit more math, but doable. </div><div><br /></div><div>\[ P_1' = 3P_1 - 3P_2 + P_3\]</div><div>\[ P_2' = 3P_2 - 2P_3\]</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/eYMVPQe?default-tab=result" style="width: 100%;" title="Interactive cubic Bezier curve + CSS animation">
See the Pen <a href="https://codepen.io/coolwanglu/pen/eYMVPQe">
Interactive cubic Bezier curve + CSS animation</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>The second version just involves more custom properties.</div><div><br /></div><div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/jOzZeoB?default-tab=result" style="width: 100%;" title="Interactive cubic Bezier curve + CSS animation">
See the Pen <a href="https://codepen.io/coolwanglu/pen/jOzZeoB">
Interactive cubic Bezier curve + CSS animation</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>Actually both versions may be extended to even higher-degree Bezier curves, and 3D versions.</div></div><div><br /></div><div>For the first version, I suppose there would be a generic formula for any \(P_i'\) for any \(N\)-order curve, but I did not spend time in it.</div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-27771247713314577962022-08-01T22:12:00.006+02:002022-08-04T01:48:53.911+02:00Studying CSS Animation State and Multiple (Competing) Animations<p>[2022-08-04 Update] Most of my observations seems confirmed in the CSS animation <a href="https://www.w3.org/TR/css-animations-1/">spec</a>.</p><p>I stumped upon <a href="https://youtu.be/2B5rbsOoIUE">this youtube video</a>. Then after some discussion with colleages, I was really into CSS-only projects.</p><p>Of course I'd start with a rotating cube:</p><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/WNzXPrG?default-tab=result" style="width: 100%;" title="CSS 3d Animation Test">
See the Pen <a href="https://codepen.io/coolwanglu/pen/WNzXPrG">
CSS 3d Animation Test</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><p>Then I built this CSS-only first person (rail) shooter:</p><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="800" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/VwXyavg?default-tab=result" style="width: 100%;" title="CSS-Only Wolfenstein">
See the Pen <a href="https://codepen.io/coolwanglu/pen/VwXyavg">
CSS-Only Wolfenstein</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><p>It was lots of fun. Especially I learned about the <a href="https://css-tricks.com/the-checkbox-hack/">checkbox hack</a>. </p><p>However there were two problems that took me long to solve.</p><h2 style="text-align: left;"><br /></h2><h2 style="text-align: left;">1. Move from in the middle of an animation</h2><div>The desired effect is:</div><div><ul style="text-align: left;"><li>An element is moving from point A to point B.</li><li>If something is clicked, the element should move from its current state (somewhere between A and B, in the middle of the animation/transition) to another point C.</li></ul><div>This is for the last hit of the boss, the boss is fastly moving. I'd like the boss to slowly move to the center if being hit.</div></div><div><br /></div><div>The first try: just set a new animation "move-to-new-direction" when triggered, but it does not work. The new animation starts with the "original location" instead of the "current location".</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/MWVQWbB?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/MWVQWbB">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>This is because the old animation was removed in the new rule, so the animation state would be instantly lost.</div><div><br /></div><div>As a side note, it turned out this is easier to achieve with CSS transition:</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/wvmyvqr?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/wvmyvqr">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>However transition does not well for me because I need non-trivial scripted animation.</div><div><br /></div><div>In order to let the browser "remember the state", I tried to "append a new animation" instead of "setting new animations". Which kind of works.</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/xxWYxWK?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/xxWYxWK">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>However it is clear that the first animation is not stopped. Both animations are competing with the transform property, and eventually the box will always stop, due to the second animation. It'd be obvious that the order of the animation matters.</div><div><br /></div><div>Then I tried to play with the first animation in the triggerd rule. </div><div><br /></div><div>The original first animation is "default-move 2s infinite 0s". It turns out that it does not matter too much if I</div><div><ul style="text-align: left;"><li>change infinite to 100 (or another large number), and I trigger the change before the animation finished</li><li>change delay from 0s to 2s (or another multiple of 2s), and I trigger the change after that delay.</li></ul><div>However it will be an obvious change if I modify the duration, or change the delay to 1s:</div></div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/jOzZOpp?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/jOzZOpp">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>So I figured that the browser will keep the state of "animation name" and "play time". It'd re-compute the state once the new value is set for the animation property. </div><div><br /></div><div>To stop the first animation from "competing", I figured that I could just set the state to paused, which works well:</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/NWYyWoz?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/NWYyWoz">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>And similary, I should keep all the setting (duration, delay etc) of the first animation, of carefully change it to a "compatible" set.</div><div><br /></div><div>This basically solves my problem. And finally, actually I realized that the boss should just die at where he's shot, so I'd just pause the current animation. It's also easier than copying all existing animations and adding a new one, in the CSS ruleset.</div><div><br /></div><div><br /></div><h2 style="text-align: left;">2. Replaying Animation with Multiple Triggers</h2><div>This is needed for the weapon firing animation, whenever an enemy is being shot, the "weapon firing" animation should play.</div><div><br /></div><div>This simple implementation does not work well:</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/WNzMbNa?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/WNzMbNa">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>Both input elements work individually, however if the first checkbox is checked before clicking on the second, the animation does not replay.</div><div><br /></div><div>Now it should be clear why this happens: the browser would remember the state of "animation name" and "play time". This can be verified by tweaking duration and delay:</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/abYqzvd?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/abYqzvd">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>Especially, it would be a good solution, if I know exactly when the animation should be played (despite of the trigger time). This is not the case for my game, because I want to play the weapon-firing animation immediately after the trigger.</div><div><br /></div><div>To make both triggers work, one solution is to "append the animation":</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/RwMQNaj?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/RwMQNaj">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>But this could be repetitive and hard to maintain for a large number of triggers (on the same element).</div><div><br /></div><div>A better solution is to define multiple identical @keyframes with different names. Then different triggers can just set individual animation names. The browser would replay the animation because the animation name changes:</div><div><br /></div><iframe allowfullscreen="true" allowtransparency="true" frameborder="no" height="300" loading="lazy" scrolling="no" src="https://codepen.io/coolwanglu/embed/oNqEgLZ?default-tab=result" style="width: 100%;" title="For blog 2022-08-01">
See the Pen <a href="https://codepen.io/coolwanglu/pen/oNqEgLZ">
For blog 2022-08-01</a> by Lu Wang (<a href="https://codepen.io/coolwanglu">@coolwanglu</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe><div><br /></div><div>I think this one is better because the animation rules can be written with a @for loop in SCSS.</div><div><br /></div><div>In my game I ended up using multiple (duplicate) elements with individual triggers. All elements are hidden by default. Upon triggered, each element would show up, play the animation, then hide.</div><div><br /></div><div>One nice thing about this solution, I can still control the "non-firing weapon" with scripted animations, because the triggers will not override the animation CSS property.</div><div><br /></div><div>Note that here it is assumed the triggers will start in a pre-defined order, otherwise the CSS rules would not work properly.</div><div><br /></div><div><br /></div><h2 style="text-align: left;">Conclusion</h2><div>I find most CSS-only projects fascinating. Pure animatoin is one thing, but some interactive projects, especially games, are quite inspiring.</div><div><br /></div><div>I'm also wondering if there is any practical value besides the "fun factor". It'd be interesting if we can export some simple 3D models/animations from Blender to CSS.</div><div><br /></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-87664445850996677122022-05-06T13:51:00.000+02:002022-05-06T13:51:35.484+02:00Setting up sslh as transparent proxy for a remote container<p> I have an NGINX server that is publicly accessible. It has been deployed in the following manner:</p><p></p><ul style="text-align: left;"><li>Machine A</li><ul><li>Port forwarding with socat: localhost:4443 ==> 0.0.0.0:443</li></ul><li>Machine B</li><ul><li>Running NGINX in a Docker container</li><li>Port forwarding by Docker: <container_ip>:443 ==> localhost:4443</li><li>Port forwarding by SSH to Machine A: localhost(B):4443 ==> localhost(A):4443</li></ul></ul><div>This in general works. Machine A is published to my domain, and the traffic to 443 is forwarded to NGINX in a few hops.</div><div><br /></div><div>However there is a problem: the NGINX server never sees the real IP of the client, so it is impossible to depoly fail2ban or other IP address based tools. So I wanted to fix it.</div><div><br /></div><div><br /></div><h2 style="text-align: left;">Step 1: VPN</h2><div>The first step is to connect machine A and B with a VPN. I feel that it would also work without it, but the iptables rules could be more tricky. </div><div><br /></div><div>WireGuard is my choice. I made a simple setup:</div><div><ul style="text-align: left;"><li>Machine A has IP: 10.0.0.2/24</li><li>Machine B has IP: 10.0.0.1/24</li><li>On both machines, the interface is called wg0, AllowedIPs of the other peer is <other_peer_ip>/32 </li><li>wg-quick and systemd are used manage the interface.</li></ul><div><br /></div></div><h2 style="text-align: left;">Step 2: Machine A</h2><div>Configure sslh:</div><div><br /></div><div><code>sslh --user sslh --transparent --listen 0.0.0.0:443 --tls 10.0.0.1:4443</code></div><div><br /></div><div>This way sslh will create a transparent socket that talks to Machine B. When the reply packets come back, we need to redirect them to the transparent socket:</div><div><br /></div><div><code>iptables -t mangle -N MY-SERVER</code></div><div><code>iptables -t mangle -I PREROUTING -p tcp -m socket --transparent -j MY-SERVER</code></div><div><code>iptables -t mangle -A MY-SERVER -j MARK --set-mark 0x1</code></div><div><code>iptables -t mangle -A MY-SERVER -j ACCEPT</code></div><div><code>ip rule add fwmark 0x1 lookup 100</code></div><div><code>ip route add local 0.0.0.0/0 dev lo table 100
</code><div><br /></div><div>Here I'm forwarding all transparent sockets, which is OK because sslh is the only one that creates such traffic.</div><div><br /></div><h2 style="text-align: left;">Step 3: Machine B</h2><div><div>Now machine A will start routing packets, the source address will be of the real HTTP client, not Machine A. However WireGuard will block them because of AllowedIPs. </div><div><br /></div><div>To unblock:</div><div><br /></div><div><code>wg set wg0 peer MACHINE_A_PUB_KEY allowed-ips 10.0.0.2/32,0.0.0.0/0</code></div></div><div><br /></div><div><div>Note that I cannot simply add 0.0.0.0/0 to AllowedIPs in the conf file, because wg-quick will automatically set ip routing.</div></div><div><br /></div><div><div>My Linux distro and Docker already set up some good default values for forwarding traffic towards containers:</div><div><ul style="text-align: left;"><li>IP forwarding is enabled</li><li>-j DNAT is set to translate the destination IP address and port.</li></ul><div>Now NGINX can see the real IP addresses of clients. It will also send response traffic back to that real IP. I need make sure that the traffic is sent back to machine A.</div></div><div><br /></div><div>Note that if NGINX proactively initiates traffic to the Internet, I still want it to go through the default routing on machine B. But I suppose it is also OK to route all traffic to machine A if preferred/needed.</div><div><br /></div></div><div><code>iptables -N MY-SERVER</code></div><div><span style="font-family: monospace;"># Tag incoming traffic towards NGINX</span></div><div><span style="font-family: monospace;">iptables -I FORWARD -i wg0 -o docker0 -m conntrack --ctorigdst 10.0.0.1 --ctorigdstport 4443 -j MY-SERVER</span></div><div><span style="font-family: monospace;">iptables -A MY-SERVER -j CONNMARK --set-xmark 0x01/0x0f</span></div><div><span style="font-family: monospace;">iptables -A MY-SERVER -j ACCEPT</span></div><div><span style="font-family: monospace;"># Tag response traffic from NGINX</span></div><div><span style="font-family: monospace;">iptables -t mangle -I PREROUTING -i docker0 -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f</span></div><div><span style="font-family: monospace;"><br /></span></div><div><span style="font-family: monospace;"># Route all tagged traffic via wg0</span></div><div><span style="font-family: monospace;">ip rule add fwmark 0x1 lookup 100</span></div><div><code>ip route add 0.0.0.0/0 dev wg0 via 10.0.0.2 table 100</code></div><div><br /></div><div>Now everything should work.</div><div><br /></div><h2 style="text-align: left;">Notes</h2><div>I mainly referred to the <a href="https://github.com/yrutschle/sslh/blob/master/doc/tproxy.md">official guide of sslh</a>. I also referred to a few other sources like Arch Wiki. </div><div><br /></div><div>In practice, some instructions did not apply to my case:</div><div><br /></div><div><ul style="text-align: left;"><li>I did not need to grant CAP_NET_RAW or CAP_NET_ADMIN to sslh. Althougth it is mentioned in <a href="https://github.com/yrutschle/sslh/blob/master/doc/config.md#capabilities-support">an sslh doc</a> and <a href="https://man7.org/linux/man-pages/man7/ip.7.html">a manpage</a>. Maybe the sslh package already handled it automatically.</li><li>On machine A I did not need to enable IP forwading. Actually this could make sense, because routing is happening on machine B.</li><li>I did not need to enable route_localnet on machine A</li></ul></div><div><br /></div><p></p></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-53417051281792045652022-04-02T15:36:00.005+02:002022-04-04T11:13:45.650+02:00Home Server TinkeringWeeks ago I purchased a secondhand machine. Since then I have been tinkering this little box.<div><br /></div><div>The <a href="https://perfectmediaserver.com/index.html">Perfect Media Server</a> site is a good place to start with. Arch Linux Wiki is my go-to learnning resource, even though I use Ubuntu.</div><div><br /></div><h2 style="text-align: left;">Filesystem</h2><div>I'd be super paranoid and careful, as this is my first time manually configuring a disk array. Basically my optoins include:</div><div><br /></div><div><ul style="text-align: left;"><li>ZFS</li><li>btrfs</li><li>Snapraid (or even combnied ZFS/btrfs)</li><li>Unraid</li></ul><div>My considerations include:</div></div><div><ul style="text-align: left;"><li>Data integrety, which is the most important.</li><li>Maintenance. I want everything easy to set up and maintain.</li><li>Popularity. There will be more doc/tutorial/discussions if the technology is more popular.</li></ul><div>Eventually I decided to use ZFS with raidz2 on 4 disks. </div><div><br /></div><div>I also took this chance to learn configuring disk encryption. I decided to use LUKS beneath ZFS. I could have just used ZFS's built-in encryption, but I thought LUKS is fun to learn. It really was. The commands are way more user-friendly that I had expected.</div><div><br /></div><h2 style="text-align: left;">Hardening SSH</h2><div>Most popular best practices include:</div></div><div><ul style="text-align: left;"><li>Use a non-guessable port.</li><li>Use public key authenticatoin and disable password authentication.</li><li>Optionally use an OTP (e.g. Google Authenticator) authentication.</li><li>Set up chroot and command restrictions if applicable. E.g. for backup users.</li></ul></div><div><br /></div><h2 style="text-align: left;">Various Routines</h2><div><ul style="text-align: left;"><li>Set up remote disk decryption via SSH, with dropbear.</li><li>Set up mail/postfix, so I will receive all kinds of system errors/warnings. E.g. from cron.</li><li>Set up ZED. Schedule scrubbing with sanoid.</li><li>Set up samba and other services.</li><li>Set up backup routines.</li></ul><div><br /></div><h2 style="text-align: left;">Containers</h2><div>I also took the chance to learn about Docker, and tried a couple of images. Not all of them are useful, but I found a few very useful:</div><div><ul style="text-align: left;"><li>Grafana + Prometheus. Monitoring system, UPS, air quality etc.</li><li>Photoprism. Managing personal photos</li><li>Pi-hole. Well I do have it running on my Pi, but I guess it's nice to have another option.</li><li>Hosting GUI software with web access. E.g. firefox. </li></ul><div>However there may be security concerns. See below.</div></div><br /></div><h2 style="text-align: left;">Security Considerations</h2><div>While I'd like to run userful software and services, I'd also want to keep my data safe. </div><div><br /></div><div>I want to protect my data from two scenarios:</div><div><ul style="text-align: left;"><li>Malicious/Untrusted code. I have heard so many news about malicious NPM packages in the last few years.</li><li>Human/Script errors. It happened with a popular package, where a whitepsace was unintended added into the install script, such that the command became "rm -rf / usr/lib/...". Horrible. For similar reason, I don't trust scripts that I wrote myself either.</li></ul><div>At this moment I am not worried about DoS attacks.</div></div><div><br /></div><h3 style="text-align: left;">User and File Permissions</h3><div>The easiest option is to use different users for diffrent tasks. Avoid using root when possible. Also limit the resources that each user can access. </div><div><br /></div><div>This is a natural choice when I want other devices to back up data to my server. It is also useful when I need to run code in a "sandbox like environment". This is explained well in <a href="https://wiki.gentoo.org/wiki/Simple_sandbox">Gentoo Wiki</a>.</div><div><br /></div><div>There are two issues with this approach:</div><div><ol style="text-align: left;"><li>It is not really a sandbox. It is straightfoward to prevent a user reading/writing some files, but it'd be trickier to limit other resource, like network, memory etc.</li><li>It is tricky to maintain permissions for multiple users, especially when they need to access the same files with different scopes. ACL are better than the classic Linux permission bits, yet it can still become too complicated. I believe that complicated rules equal to security holes.</li></ol><div><br /></div></div><div>I created and applied different users for each docker image, but it was not enough. Discussed later below.</div><div><br /></div><div><br /></div><h3 style="text-align: left;">Mandatory Access Control (MAC)</h3><div>Examples include AppArmor and SELinux.</div><div><br /></div><div>Funny enough, years ago I thought AppArmor was quite annoying, because it kept showing popup messages. And now I proactively write AppArmor profiles.</div><div><br /></div><div>I decided to write AppArmor profiles for docker images and all my scripts in crontab. I feel more assured knowing that my backup scripts cannot silently delete all my data.</div><div><br /></div><h3 style="text-align: left;">Sandboxing</h3><div>I had thought that chroot was a nice security tool, until I learned that it isn't. I found a couple of sandboxing options on <a href="https://wiki.archlinux.org/title/Security#Sandboxing_applications">Arch Wiki</a>. </div><div><br /></div><div>However I don't see them fit well in my case. It should work for my scripts, but dedicated user + MAC sounds simpler to me. I also want to protect against malicious install scripts (e.g. NPM packages), and I feel that firejail/bubblewrap won't help too much here.</div><div><br /></div><div>Sandboxing seems to be useful for beast software, like web browsers. However I'll also need to access it remotely, so I'd just go for containers or VMs.</div><div><br /></div><div>I suppose I may find useful scenarios later.</div><div><br /></div><h3 style="text-align: left;">Docker / Container / VM</h3><div>I use Docker when</div><div><ul style="text-align: left;"><li>User + MAC is not enough.</li><li>I do not trust the code.</li><li>It is difficult to deploy to the host.</li></ul><div>Security best practices include:</div><div><ul style="text-align: left;"><li>Do not run as root</li><li>Drop all unnecessary capabilities. (Most images don't need anything at all)</li><li>Set no-new-priviledges to true.</li><li>Apply AppArmor profiles.</li></ul><div>[UPDATE: Obviously I did not see the whole story. Added a new section below]</div>I was quite surprised when I learned that root@container == root@host, unless I'm running rootless docker. What's worse, almost all docker images that I found use root by default.</div></div><div><br /></div><div>While I managed to run most containers without root, many GUI-related container really want to use root. I really hate it and I started looking for rootless options.</div><div><br /></div><div>Instead of mutiple GUI containers, I decided to run an entire OS. This will be my playground which has no access to my data. Docker is not designed for this task, although probably it can still do the job if configured correctly.</div><div><br /></div><div>VMs (e.g. VirtualBox) are my last resorts. They are quite laggy on my box. It is also tricky to dynamically balance the load, e.g. I have to specify the max CPU/RAM beforehand. </div><div><br /></div><div>I learned that Kata Container is good for this tast. It is fast and considered very secure. However I didn't find an easy way of depolying it. (Somehow I don't like Snap and disabled it on my machine. Well now snap is almost required by Kata Container, LXD and Firefox, maybe I should give it a go some time?)</div><div><br /></div><div>Eventually I turned to LXC. It was quite easy to deploy a Ubuntu box. I am very happy with the toolchain and the design choices. For example the root filesystem of the container is exposed as a plain directory tree on the host, instead of (virtual disk) images.</div><div><br /></div><h3 style="text-align: left;">[UPDATE] Containers without root@host</h3><div>I really dislike it, that processes in containers can be run as root@host. Therefore I was looking for "rootless options". There are in fact two options:</div><div><br /></div><div><ol style="text-align: left;"><li>Container daemon run as root. Contaniers run as non-root. </li><li>Both container daemon and containers run as non-root.</li></ol></div><div><br /></div><div>#1 means to turn on user namespace mapping for containers in <a href="https://docs.docker.com/engine/security/userns-remap/">Docker</a> or <a href="https://docs.docker.com/engine/security/userns-remap/">LXC</a>. #2 means to further configure the daemon of <a href="https://docs.docker.com/engine/security/rootless/">Docker</a> or <a href="https://linuxcontainers.org/lxc/getting-started/#creating-unprivileged-containers-as-a-user">LXC</a>.</div><div><br /></div><div>#2 seems more secure, but it requires another kernal feature CONFIG_USER_NS_UNPRIVILEGED, which might have <a href="https://wiki.archlinux.org/title/Security#Sandboxing_applications">security concerns</a>. So funny enough, it is both "more secure" and "less secure" than #1.</div><div><br /></div><div>While I don't know anything deeper, I'm slighly leaning towards #1. I will keep an eye on #2 and maybe turn to it when the secury concerns are resolved.</div><div><br /></div><h2 style="text-align: left;">What's Next</h2><div>Probably I will try to improve the box to reload services/container on failure/reboot. Maybe systemd is enough, or maybe I need something like Kubernetes or Ansible. Or maybe I can live well without them.</div><div><br /></div><div><br /></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-89678336502482350372022-03-29T19:43:00.003+02:002022-03-29T19:43:43.338+02:00Fix broken sudoers files<p>Lesson learned today: An invalid sudoers file can break the sudo command, which in turn prevent the sudoers file from being edited via sudo. </p><p>The good practice is to always use visudo to modify sudoers file. In my case I needed to modify a file inside /etc/sudoers.d, where I should have used `visudo -f`.</p><p><br /></p><p>To recover from invalid sudoers files, it is possible to run `pkexec bash` to gain root access. However I got an error "polkit-agent-helper-1: error response to PolicyKit daemon: GDBus.Error:org.freedesktop.PolicyKit1.Error.Failed: No session for cookie"</p><p><br /></p><p>Solution to this error:</p><p>Source: https://github.com/NixOS/nixpkgs/issues/18012#issuecomment-335350903</p><p>- Open two terminals. (tmux also works)</p><p>- In terminal #1, get PID by running `echo $$`</p><p>- In terminal #2, run `pkttyagent --process <PID>`</p><p>- In terminal #1, run `pkexec bash`</p><p><br /></p><p><br /></p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-10543680897503980462022-01-16T19:37:00.007+01:002022-01-17T14:57:46.658+01:00On Data Backup<p>Around 2013, every year I'd burn <i>all</i> my important data into a single DVD, that is 4.7GB. Nowadays I have ~5TB data, and I don't even bother optimizing 5GB data.</p><h2 style="text-align: left;">Background</h2><div>I realized that it is the time to consider backup. I guess I have seen enough signs.</div><div><ul style="text-align: left;"><li>The NAS shows that the disks are quite full.</li><li>I just happened to see article and videos about data backup.</li><li>I found corrupted data in my old DVDs.</li><li>I realized that most of my important data are not properly backed up.</li><li>I have a few scripts that manage different files, which might contains bugs.</li></ul><div>The goal is to have good coverage under acceptable cost.</div></div><div><br /></div><h2 style="text-align: left;">The Plan</h2><div>All my data are categorized into 4 classes.</div><div><br /></div><h3 style="text-align: left;">Class 1: Most Important + Frequently Accessed</h3><div>Rougly ~50GB in total. Average file size is ~5MB. </div><div>Examples include official documents, my artworks and source code.</div><div><br /></div><div>The plan: sync into multpile locations to maximize robustness. Sometimes I chooes a smaller subset when I don't have enough free space.</div><div>In case some copies are down/corrupted, I can still access the data quickly.</div><div><br /></div><h3 style="text-align: left;">Class 2: Important + Frequently Modified</h3><div>Roughly ~500GB in total. Average file size is ~500KB.</div><div>Typically there are groups of small files, which must be used together.</div><div>Examples include source code, git repo and backup repo.</div><div>Note that it overlaps with Class 1.</div><div><br /></div><div>The plan: hot backup with versioning/snapshots, yearly cold archives.</div><div><br /></div><h3 style="text-align: left;">Class 3: Important + Frozen</h3><div>Roughly ~1TB in total. Average flie size is ~50MB.</div><div>Frozen means they are never (or at least rarely) changed once created.</div><div>Most data of this class are labeled, for example /Media/Video/2020/2020-01-03.mp4</div><div>Examples include raw GoPro footages.</div><div>Note that it overlaps with Class 1.</div><div><br /></div><div>The plan: hot backup with versioning/snapshots, labeled data are directly sync'ed to cold storage, unlabled data go to yearly cold archives.</div><div><br /></div><h3 style="text-align: left;">Class 4: Unimportant</h3><div>The rest of the data are not important, I wouldn't worry too much if they are lost, but I'm happy to keep them with minimum cost.</div><div>Examples include downloaded Steam games.</div><div><br /></div><div>The plan: upload some to hot backup storage, shoud I have enough quota. </div><div>No cold archive is planned.</div><div><b><br /></b></div><h3 style="text-align: left;"><b>Thoughts</b></h3><div><div>I have put lots of thought when designing the plan, and I'm happy with the result. </div><div>On the other hand, I had too many headaches throughout the process. </div><div>To name a few:</div><div><div><br /></div><div><br /></div><div><b>Hot Backup or Cold Archive</b></div><div>I had a hard time choosing between hot backups and cold archvies. Hot backups are more up-to-date but cold archives are safer.</div><div><br /></div><div>Originally I had planned to use only one per data class (and the classes were defined slightly differently). But I just couldn't decide. </div><div><br /></div><div>The decision is to try both then revisit later.</div><div><br /></div></div><div><br /></div><div><b>Format of Cold Archives</b></div><div>There are two possibilities:</div><div><ol style="text-align: left;"><li>Directly uploading the files, with the same local file structure</li><li>Create an archive and upload it. This also includes chunk-based backup methods.</li></ol></div><div>Note that cold storage are special:</div><div><ul style="text-align: left;"><li>Files on cold storage cannot be modified/moved/renamed, or more preciely it is more expensive to do so. </li><li>There is typically cost per API/object. So we get a penalty for too many small objects.</li></ul></div><div>With option 1 I can easily access individual files on the storage, but if I rename or move some files locally, it will be a diaster in the next backup cycle.</div><div>With oiption 2 there is no problem with too many files, but I have to download the whole archive in order to access a single file inside. Also I will need to make sure that the archives do not overlap (too much), or they will just waste space.</div><div><br /></div><div>My solution is to organize and label the data, mostly by year. Good news is that most frozen data can be labeled this way, and they are often large files. This way it is mostly safe to upload them directly, since files may be added or removed, they are unlikely to be renamed or modified.</div><div><br /></div><div>For unlabeled data, I'd just create archives every year, the size is small comparing with labeled data, so I wouldn't worry about it.</div><div><br /></div><div><br /></div><div><b>Format of Hot Backups</b></div><div style="text-align: left;">There are also two possibilities:</div><div style="text-align: left;"><ol style="text-align: left;"><li>File-based. Every time a file is modified or removed, the old version is saved somewhere else.</li><li>Chunk-based. All files are broken and store in chunks. Like git repos.</li></ol><div>There are lots of things to consider, e.g. size, speed, safety/robustness, easiness to access.</div><div><br /></div><div>The decision is to go for #1 for all relevant data classes. My thoughts are:</div><div><ul style="text-align: left;"><li>In the worse case, the whole chunk-based repo may be affected by a few rotten bits. This is not the caes for file-based solutions.</li><li>I want to be able to access individual files without special tools.</li><li>Benefits of chunk-base approaches include deduplication and smaller sizes (mostly for changed files). But it does not really apply for my data. Most big files are large video files. They are rarely changed and cannot be compressed much.</li></ul><div>On the other hand, I do plan to try out some chunk-based software in the future.</div></div><div><br /></div><div><br /></div></div><div style="text-align: left;"><b>Backup for Repos</b></div><div>In my NAS I have a few git repos and (chunk-based) backup repos. So how should I back them up?</div><div><br /></div><div>On one hand, the repos already saved versions of source files, so simply syncing them into cloud storage should work well enough.</div><div>On the other hand, should there be local data corruption, the cloud version will also be damaged after one data sync.</div><div><br /></div><div>The decision is to keep versions in the repo backups as well. Fortunately they are not very big.</div><div>I plan to revisit this later. And hopefully I never need to recover a repo like this. </div><div><br /></div></div><div><br /></div><div><br /></div><h2 style="text-align: left;">Backup Storage</h2><div>I don't have enough extra HDDS to back up all my data. Anyways I prefer cloud storage for this task.</div><div>Both hot and cold ones need to be discussed individually.</div><div><br /></div><div><br /></div><div><b>Hot Backup</b></div><div><b><br /></b></div><div>Most cloud storage services would work well as a hot backup repo. In general the files are always available for reading or writing, which make them suitable for simple rsync-alike backups, or chunk-based backups.</div><div><br /></div><div>It is not too difficult to fit Class 1 data into free quota, although I do need to subset it.</div><div><br /></div><div>It is tricky to choose one service for other classes, as I'd like to keep all backup data together. </div><div>I have check a number of services. I found the followings especially interesting.</div><div><ul style="text-align: left;"><li>Backblaze B2</li><li>Amazon S3</li><li>Google Cloud Storage</li><li>Google Storage</li></ul><div>I'd just pick one while balancing cost, speed and reputation etc. I wouldn't worry too much about software support, since all of them are popular.</div></div><div><br /></div><div><br /></div><div><b>Cold Archive</b></div><div><b><br /></b></div><div>I only learned recently about cold archives from Jeff Geerling's <a href="https://www.jeffgeerling.com/blog/2021/my-backup-plan">backup plan</a>. After some readings I find the concept really interesting.</div><div><br /></div><div>Mostly I'd narrow down to the following:</div><div><ul style="text-align: left;"><li>Amazon S3 Glacier Deep Archive</li><li>Google Archival Cloud Storage</li><li>Azure Archive</li></ul><div>I remember also seeing similar storage class from Huawei and Tencent, but the software support is not as good among open source tools that I have found.</div></div><div><br /></div><div><br /></div><h2 style="text-align: left;">Software</h2><div>I'd like to manage all backup tasks on my Raspberry Pi. </div><div><br /></div><div><a href="https://rclone.org/">rclone</a>, so-called the swiss army knife for cloud storages, is an easy winner. I just couldn't find another one the matches with it. The more I learn about the tool, the more I like it. To name a few observations:</div><div><ul style="text-align: left;"><li>Great coverage on cloud storage providers.</li><li>Comprehensive and well-written documents.</li><li>Active community.</li><li>Lots of useful features and safety checks</li><li>Output both human-readable and machine-readable information.</li></ul><div>So I ended up writing my own scripts that calls rclone. It is always easy to execute a task like "copy all files from A to B, and in case some files in B need to be modified, save a copy in C". So I just needed to focus on defining my tasks, scoping the data and set up the routines. Well it is not trivial though, more on that later.</div></div><div><br /></div><div>I also spent quite some time researching chunk-based backup tools. For example</div><div><ul style="text-align: left;"><li><a href="https://borgbackup.readthedocs.io/en/stable/">BorgBackup</a></li><li><a href="https://restic.net/">restic</a></li><li><a href="https://duplicacy.com/">Duplicacy</a></li></ul><div>I also checked a few others, but not as extensive as these three. Here are a few useful lists:</div></div><div><ul style="text-align: left;"><li><a href="https://wiki.archlinux.org/title/Synchronization_and_backup_programs">A comprehensive list on Arch Linux Wiki</a></li><li><a href="https://github.com/restic/others">A list made by restic's creator</a></li><li><a href="https://news.ycombinator.com/item?id=29210222">A comment from Hacker News about issues of various tools</a></li></ul></div><div>I pulled myself out of the rabbit hole, as soon as I realized that I don't need them at the moment. While I still cannot decide which one to use, should I need a chunk-based backup today. Here's a summary of my 2-page notes on these tools:</div><div><ul style="text-align: left;"><li>BorgBackup is mature (forked from Attic in 2010), but have limited support on backends.</li><li>restic is relatively new (first GitHub commit in 2014). It used to have performance issues with pruning, which seems to have been fixed. The backup format is not fixed (yet).</li><li>Duplicacy is even younger (first GitHub commit in 2016). The license is not standard, which concerns many people. Due to the lock-free design it is measured faster than others, especially when there are multiple clients connecting to the same repo. However it might waste some space in order to achieve that.</li></ul><div>Maybe thing will change in a few years. I will keey an eye on them.</div></div><div><br /></div><div><br /></div><h2 style="text-align: left;">Technical Issues</h2><div>I had quite a few issues when using OneDrive + WebDAV.</div><div><ul style="text-align: left;"><li>Limit on max path length</li><li>Limit on max file length</li><li>No checksums</li><li>No quota metrics.</li></ul><div>Fortunatley most of them are not big problems.</div></div><div><br /></div><div>Another issue, about 7zip, is that I cannot add empty directories in the archive without adding files in the directories. This is particularly important for my cold yearly archives.</div><div><br /></div><div>Eventually I used Python and tarfile to achieve it. I probably can do the same with a 7z python library. But I used tarfile anyway because it is natively available in Python, plus I realized that most of the archives cannot be effectively compressed.</div><div><br /></div><div><br /></div><h2 style="text-align: left;">Next Steps</h2><div>I probably will add a few more scripts to monitor and to verify the backups. For example, download and verify ~10GB data that is randomly selected from the backup repo.</div><div><br /></div><div>I will also keep an eye on chunk-based solutions.</div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-6597812336252811662021-11-11T00:47:00.002+01:002021-11-11T00:47:25.956+01:00Windows的KVM软件<p> KVM即是共享鼠标键盘。</p><p>之前Windows和Ubuntu我都是用Synergy,不过最近一查发现变成收费软件了。</p><p>Github上有个fork叫Barrier,下载以后配了好久也不好用。有可能跟我的屏幕配置有关系,一个电脑连了多个显示器,而且有的显示器还是禁用。最后放弃了。</p><p>后来搜到了微软的Mouse without Borders,简单配一下就能用了,还是不错。</p><p>网址是http://aka.ms/mm</p>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-28410979810830517392021-11-01T15:14:00.004+01:002021-11-02T02:28:47.979+01:00Euclidea 14.5 解法与证明<p><a href="https://www.euclidea.xyz/">Euclidea</a> 14.5</p><p>如图,给定三个两两相切的圆,O,O_1, O_2。三个圆心共线。</p><p>任务:尺规作出一个圆,与给定的三个圆都相切。</p><div class="separator" style="clear: both; text-align: center;"><a href="https://1.bp.blogspot.com/-qVZi4ETlO48/YX_pSiAbv0I/AAAAAAAAw5o/vaaZpA9BKPIE5NNzdJOT-mvHteDP38JtQCLcBGAsYHQ/s2048/problem.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1366" data-original-width="2048" height="426" src="https://1.bp.blogspot.com/-qVZi4ETlO48/YX_pSiAbv0I/AAAAAAAAw5o/vaaZpA9BKPIE5NNzdJOT-mvHteDP38JtQCLcBGAsYHQ/w640-h426/problem.png" width="640" /></a></div><br /><div class="separator" style="clear: both; text-align: center;"><br /></div>解法:<div class="separator" style="clear: both; text-align: center;"><a href="https://1.bp.blogspot.com/-qTJZAvTrNWI/YX_rATqjZoI/AAAAAAAAw5w/aBJtkX35ETM4HFTjJWKRChtAvd54074xQCLcBGAsYHQ/s2048/solution.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1366" data-original-width="2048" height="426" src="https://1.bp.blogspot.com/-qTJZAvTrNWI/YX_rATqjZoI/AAAAAAAAw5w/aBJtkX35ETM4HFTjJWKRChtAvd54074xQCLcBGAsYHQ/w640-h426/solution.png" width="640" /></a></div><br /><div><br /><p></p><ol style="text-align: left;"><li>设圆O与圆O_1的切点为A</li><li>连接OA</li><li>过O做OA垂线交圆O于D</li><li>以D为圆心,DA为半径作圆,交圆O_1和圆O_2于E和F。</li><li>作直线EO_1和FO_2交于G</li><li>以G为圆心,GE为半径作圆</li><li>圆G即为所求圆</li></ol><p></p><p><br /></p><p><br /></p><p>简要证明:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://1.bp.blogspot.com/-e5Yyc5be1UM/YX_wFBrNAwI/AAAAAAAAw6A/wzjRJ_vlFkg8joqSVyV90iNn0bebCetlwCLcBGAsYHQ/s2048/proof.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1366" data-original-width="2048" height="426" src="https://1.bp.blogspot.com/-e5Yyc5be1UM/YX_wFBrNAwI/AAAAAAAAw6A/wzjRJ_vlFkg8joqSVyV90iNn0bebCetlwCLcBGAsYHQ/w640-h426/proof.png" width="640" /></a></div><ol style="text-align: left;"><li>如图,设三个圆切点为A,B,C。明显A,B,C,O,O_1,O_2五点共线。</li><li>以A为中心作反演变换,取反演圆与圆O_2正交。于是圆O_2经反演变换后不变。B,C互为反演点。</li><li>分别过B和C做AB的垂线L_B和L_C。易知圆O1和圆O经反演变换后分别为L_B和L_C。</li><li>如图作圆G'同时与圆O_2,L_B和L_C相切于E'和F'。</li><li>过E'F'作直线L,易证C在L上,并且∠E'CB为45°。</li><li>设L经过反演变换得到的圆为圆D',考虑圆D'的性质:</li><ol><li>直线AD'与直线L垂直</li><li>圆D'经过A(因为L不经过A)</li><li>圆D'经过B(因为L经过C,而B,C互为反演点)</li></ol><li>于是可知圆D就是圆D',因此E和E', F和F'分别互为反演点。</li><li>设圆G'经过反演变换为圆G''。因为圆G'与L_B和圆O_2相切于E'和F',所以圆G''与圆O_1和圆O_2相切于E和F。</li><li>所以G''即是EO_1和FO_2的交点,且圆G''半径是G''E。于是圆G即是圆G''。</li><li>因为圆G'与L_B,圆O_2,L_C都相切,所以圆G与三个给定圆都相切。证明完毕。</li></ol><br /><p></p></div><div>参考资料:<a href="https://en.wikipedia.org/wiki/Pappus_chain">Pappus chain</a></div><div><br /></div><br />Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-58425004074149562222021-04-28T18:13:00.006+02:002021-04-28T20:21:13.321+02:00再度音乐寻宝平时脑子里会无缘无故,不由自主地蹦出一些旋律。这些旋律很熟悉,也肯定不是自己现编的,但是就是想不起来具体的名字,歌手或者哪里听到的。<div><br /></div><div>最简单的情况是记得歌词,或者可以哼唱检索。最难的大概是影视作品的配乐,我觉得成功的配乐会让人记得当时的“感觉”而不是曲子本身的细节。</div><div><br /></div><div>继上次<a href="http://coolwanglu.blogspot.com/2016/10/blog-post.html">音乐寻宝</a>之后,又一个旋律出现了,令我吃不香睡不着。</div><div><br /></div><div>经过几天的查找,最终还是找到了,结果是井上昌己的《Up Side Down 永遠の環》,出自圣少女的ED。过程还是挺有趣的。</div><div><br /></div><div>1. 我大概记得开头intro的旋律,以及歌词的整体节奏,类似词牌。然而试了若干哼唱检索,都没有结果。</div><div>2. 我大概记得一些歌词片段,于是去各种网站搜索。然而结果证明我记忆的片段是错误的。</div><div>3. 我“感觉”这是一个日本动画的ED, 于是去搜索了80 90年代引进的日本动画, 以及00-08年流行的日本动画的OP ED,然而没有找到。这个比较巧妙,因为圣少女国内引进过,但是节选了OP和ED。我其实也翻了日版动画,大概看了前几集和后几集的OP ED,万没想到结果是中间的。</div><div>4. 我“感觉”动画是跟魔法少女有关,所以翻了翻魔卡少女樱和Marybell的乐集,一无所获。当时其实也过了一遍圣少女的乐集,不知为何漏掉了。</div><div>5. 后来放宽了条件,搜了一些类型相似的动画乐集,虽然没找到当前这个,但是意外找到了我的女神里的《優しい心》。这个也是以前突然蹦出来的旋律。同时翻到了同作品里的《願い》,感觉跟要找的相似,然而其实没啥相似,除了都是欢快的曲子。</div><div>6. 于是放弃了,能试的都试了,只能随缘了。</div><div>7. 后来调整了下思维,想到有没有可能是其他类属性。于是翻了一下邓丽君的日文歌集,甚至《梅兰梅兰我爱你》,当然还是无果。</div><div>8. 最后真的是随了缘,重翻了一下圣少女的乐集,竟然找到了。</div><div><br /></div><div>这个过程里我觉得最有趣的是两点:</div><div>一是我记忆里的“感觉”,日本动画, ED, 魔法少女。这样的记忆比曲子本身深刻一些。这也让找曲子更困难了一些。</div><div>二是我记忆里的不确定因素,包括旋律歌词。我试想过多种可能的乐器组合,以及歌词韵脚,感觉都有可能。最后也证明我这部分记忆都是不准确的。</div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-30272395374407620682021-04-26T18:09:00.004+02:002021-04-26T18:10:12.359+02:00Notes on Color #9: Color CalibrationIn January, as part of the preparation for digital painting training, I calibrated my laptop display and my pen display. Yet in April I realized that images look quite different on my laptop, on the pen display, on my phone or on others' devices.<div><br /></div><div>After hours of research, trail and error, I managed to learn more principles and calibrated my hardware and software. Here are some notes:</div><div><br /></div><div>1. Colorimeters are not spectrometers. Instead of measuring the full (visible) spectrum, the main goal is to simulate a standard observer (with three color receptors), or XYZ values.</div><div><br /></div><div>2. Due to #1, there are assumptions made here and there to cut down the cost without hurting the quality too much. However an important aspect is the type of monitor (e.g. WLED, WLED+phosphor, GB LED, RGB LED, OLED etc.). The chracteristics of each type is diffrent, mostly on the "base spectrum". The calibration may look off if the wrong correction matrix is used. Note that old colorimeters might not support newer display technologies.</div><div><br /></div><div>3. The default "Photos" app in Windows does not support embeded color profiles in images. I'm now using <a href="https://www.xnview.com/en/xnviewmp/">XnView MP</a>.</div><div><br /></div><div>4. Chrome (and likely other browsers) usually support embeded color profiles, however it'd be safer to simply convert the image to sRGB when upaloaded to the Web. The reason is the color profile may be stripped or incorrectly processed by the websites.</div><div><br /></div><div>5. Chrome by default uses the default color profile (for the current monitor) of the system. It might make sense to change it to sRGB.</div><div><br /></div><div>6. After trying a few calibration tools by the colorimter vendors (e.g. SpyderExpress, i1profiler), I still prefer <a href="https://displaycal.net/">DisplayCAL</a>. Note that it still makes sense to install the vendor software, such that DsiplayCal may import correction data.</div><div><br /></div><div>7. According to the author of DisplayCAL, ICC v4 is not necessisarily better than ICC v2 in practice. And v2 is way more comptable for now.</div><div><br /></div><div>8. It might be necessary to verify and recalibrate the displays every month.</div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-87088097328432821852021-04-02T21:54:00.008+02:002021-04-02T22:10:28.985+02:00Notes on Color #8: Idealized Gamut Mask<p>Continuing with the <a href="http://coolwanglu.blogspot.com/2021/04/notes-on-color-7-revisiting-james.html">previous post</a>, in this one I'll try to identify the goal of gamut mapping, and to create an idealized model. </p><div><h2>The Color "Wheel"</h2><div>A quick word with color wheel before we can proceed.</div><div><br /></div><div><div>The color wheel arranges all paint (or device) colors (or more accurately, chromacity) in a hue-chroma system. The modern version is the uniform color system (UCS). I'll use CAM16UCS as an example. Here is the full visible gamut under D65, projected into CAM16UCS.</div></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-PWlwETcN3HU/YGcMUoJICTI/AAAAAAAAwcU/q48hpvKE47ohjsmFSKg6dg1O-Mx5LkK-ACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="712" data-original-width="712" height="400" src="https://lh3.googleusercontent.com/-PWlwETcN3HU/YGcMUoJICTI/AAAAAAAAwcU/q48hpvKE47ohjsmFSKg6dg1O-Mx5LkK-ACLcBGAsYHQ/w400-h400/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">"top view" of the D65 visible gamut in CAM16UCS.<br />The color at (0, 0) is white.<br />The colors have been mapped to sRGB.</td></tr></tbody></table><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-ZqtB6ICSrlU/YGcNOEioFqI/AAAAAAAAwcc/Vcs5OiJmzNsMI68BFNtqD-rowqGsTerMQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="734" data-original-width="734" height="400" src="https://lh3.googleusercontent.com/-ZqtB6ICSrlU/YGcNOEioFqI/AAAAAAAAwcc/Vcs5OiJmzNsMI68BFNtqD-rowqGsTerMQCLcBGAsYHQ/w400-h400/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">"Side view" of the D65 visible gamut in CAM16UCS.<br />Note the chroma is 0 at the topmost and the bottom-most.<br />The colors have been mapped to sRGB.</td></tr></tbody></table><br /><br /></div><div>Observe that the top view resembles the the color wheel, but it is nothing like a perfect circle. We could say this is our modern version of the color "wheel", which presents chromacity uniformly on a 2d plane.</div><div><br /></div><div>I also included a side view here. We can see that the volume of is a irregular cylinder-alike shape. The volumne is not regular in any direction, because our eyes are more sensitive to some wavelengths (green/yellow) than others (blue).</div><div><br /></div><h2>The Idealized Model</h2><div>James once mentioned that the gamut mask could be used to simulate/achieve <a href="http://gurneyjourney.blogspot.com/2011/08/color-grading.html">color grading</a>. While color grading in general invovles multiple aspects such as contract, black level, details etc., I believe the focus here is color balance/correction.</div><div><br /></div><div>Look at the following image:</div><div><br /></div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://1.bp.blogspot.com/-g-QtwHyUnew/YGY0G7j9J9I/AAAAAAAAwbY/lnT4bYjtblwJq7TChKVV50rv-F_tYbGlQCLcBGAsYHQ/s600/111lotto.jpg" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="316" data-original-width="600" height="211" src="https://1.bp.blogspot.com/-g-QtwHyUnew/YGY0G7j9J9I/AAAAAAAAwbY/lnT4bYjtblwJq7TChKVV50rv-F_tYbGlQCLcBGAsYHQ/w400-h211/111lotto.jpg" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;"><br /><br />After Figure 6.10 from Dale Purves and R. Beau Lotto's book <i>Why We See What We Do; An Empirical Theory of Vision</i> (2003, revised 2011)<br />Source: <a href="http://www.huevaluechroma.com/111.php">http://www.huevaluechroma.com/111.php</a></td></tr></tbody></table><br /><span style="text-align: center;">The blue tiles in A and the yellow tiles in B are actually both neutral gray without the context, which can be verified by sampling the RGB value of the pixels. This demonstrates our abilitiy of <a href="https://en.wikipedia.org/wiki/Chromatic_adaptation">c</a></span><a href="https://en.wikipedia.org/wiki/Chromatic_adaptation">hromatic adaptation</a>. We have to keep this in mind when paining a scene with tinted light, or "mood".</div><div><div><br /></div><div><div>To simulate this effect, I simply took standard D65 illuminant, then muted ~1/3 visible spectra on the blue end. This resulting test illuminant would appear strongly yellow.</div><div>Under this illuminant, S cells won't receive any (reflected) light, while L and M cells are not affected much. We will not see any "real" blue (under D65) colors, but grey (under D65) objects may appear as blue-ish.</div><div><br /></div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Cone-fundamentals-with-srgb-spectrum.svg/1200px-Cone-fundamentals-with-srgb-spectrum.svg.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="563" data-original-width="800" height="225" src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Cone-fundamentals-with-srgb-spectrum.svg/1200px-Cone-fundamentals-with-srgb-spectrum.svg.png" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Cone cell response curves.<br />Source: <a href="https://commons.wikimedia.org/wiki/File:Cone-fundamentals-with-srgb-spectrum.svg#/media/File:Cone-fundamentals-with-srgb-spectrum.svg">Wikipedia</a></td></tr></tbody></table><br /><div><br /></div><div>Under such an illuminant, the visible gamut is reduced, as shown below: (we do not consider self-emitting objects here)</div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-ZEWgyJ9ZGT0/YGceT7mv_iI/AAAAAAAAwcs/8oJhUd_Mf3MZqsve688PK02YeFt2-kEUACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="734" data-original-width="734" height="400" src="https://lh3.googleusercontent.com/-ZEWgyJ9ZGT0/YGceT7mv_iI/AAAAAAAAwcs/8oJhUd_Mf3MZqsve688PK02YeFt2-kEUACLcBGAsYHQ/w400-h400/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">"top view" of the visible gamut under the test illuminant.<br />Note that it is much smaller than the D65 version, especially the blue part.<br />The colors have been mapped to sRGB.<br /></td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-zOVhjS_cQq0/YGcep9YH78I/AAAAAAAAwc0/BgHaJpBmUxwJR78afmmakozFvNOKMwNxgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="734" data-original-width="734" height="400" src="https://lh3.googleusercontent.com/-zOVhjS_cQq0/YGcep9YH78I/AAAAAAAAwc0/BgHaJpBmUxwJR78afmmakozFvNOKMwNxgCLcBGAsYHQ/w400-h400/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">"Side view" of the visible gamut under the test illuminant.<br />Note the chroma is 0 at the bottom-most, but large at the topmost.<br />The colors have been mapped to sRGB.</td></tr></tbody></table><br />Observed that almost all blue/purple fractions are missing, comparing with the D65 gamut.<br /><br />We are not done yet. Definitely it's not the case where blue-purple colors suddenly disappear while all other colors stay the same. We still need to figure out how colors are shifted.</div><div><br /></div><div>To do so, we study the Munsell colors (at value of 5) under D65 and the test illuminant. </div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-U0bhdaxhzvk/YGcgnUdTF4I/AAAAAAAAwdE/GKnEeYM0zyY_zWKvIEsgIMu-ZzYiG43cgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="314" height="386" src="https://lh3.googleusercontent.com/-U0bhdaxhzvk/YGcgnUdTF4I/AAAAAAAAwdE/GKnEeYM0zyY_zWKvIEsgIMu-ZzYiG43cgCLcBGAsYHQ/w400-h386/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Munsell Colors (V=5) under D65, in CAM16UCS.<br />Black dots indicate colors that are outside of sRGB</td></tr></tbody></table><br /><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-XbtvIZLL7ws/YGc_u0SkW9I/AAAAAAAAwdU/b2jh578tLocXbcQZZsdgz3ZwVIRs6uxsgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="314" height="386" src="https://lh3.googleusercontent.com/-XbtvIZLL7ws/YGc_u0SkW9I/AAAAAAAAwdU/b2jh578tLocXbcQZZsdgz3ZwVIRs6uxsgCLcBGAsYHQ/w400-h386/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Munsell Colors (V=5) under the test illuminant, in CAM16UCS.<br />Black dots indicate colors that are outside of sRGB.</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-jHhUgrRZDNY/YGdi3QdPYOI/AAAAAAAAwdk/ZUymPqMXMIw_QRJCGLCgPs94PPIu40nPQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="314" height="385" src="https://lh3.googleusercontent.com/-jHhUgrRZDNY/YGdi3QdPYOI/AAAAAAAAwdk/ZUymPqMXMIw_QRJCGLCgPs94PPIu40nPQCLcBGAsYHQ/w400-h385/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Munsell Colors (V=2) under the test illuminant, in CAM16UCS.<br />Black dots indicate colors that are outside of sRGB.</td></tr></tbody></table><br />Observations and interpretations:</div><div><ol><li>Only the top half of the original gamut is covered by the test version. All the colors are shifted towards the new "white" under the test illuminant, which appears yellow if compared with D65. Some colors are pushed outside sRGB and some are pulled inside.</li><li>The yellow (D65) area (black dot on the top) is very crowded, while the blue (D65) area (blue-green dots on the bottom) is sparse. Remember that chroma and hue reflect wavelength and relative strength of the dominanting spectrum, therefore removing blue-ish spectra has much greater impact on blue-ish colors than yellow-ish colors.</li><li>Comparing the V=2 version and the V=5 version. As V increases, the center of the Munsell colors is moving from black toward the illuminant color. This is actually the black/grey/white value scale under the test illuminant. </li></ol></div><div>Assuming the standard Munsell colors represents a uniform color wheel, the shifted Munsell colors would work as a modern version of the gamut mask.</div><div><br /></div><div><h2 style="text-align: left;">Comparing with the Orignial Version</h2>Interestingly, the original triangular gamut mask works in a very similar way:</div><div><ul style="text-align: left;"><li>The triangle covers the top-center area of color wheel. (More accurately, it is important that the gask is off-center, not necessarily at the top-center).</li><li>The center of the mask is for white/neurtal grey in the painting</li><li>At least one primary color is completely out of the mask, we have to use grey or grey-ish color instead.</li><li>Consider the color at the center of the mask. When we mix the lighter and dark versions, it naturally ( and roughly) follows the path from black to the illuminant color.</li></ul></div><div>I think these may well explain the color science behind the gamut mask method.</div><div><br /></div><div>Meanwhile, also note that:</div><div><ul style="text-align: left;"><li>It is not (always) true that "all colors in the gamut mask may be obtained by mixing the gamut primaries. Paint mixing is not linear, it is somwhere <a href="http://www.huevaluechroma.com/061.php">between additive and subtractive</a>.</li><li>The shape of the gamut should be more like ellipse, if we want to cover the entire chromacity. However that way it'd be difficult to identifiy primaries or to obtain colors inside the mask.</li><li>We need to pay attention to the white point as well as the distribution/division of hue & chroma inside the mask, which should not be uniform in general.</li></ul><div>These a few points might worth some attention when we are dicussing color theories, but they may be far less important when we are painting in practice.</div></div><div><br /></div><h2 style="text-align: left;">Final Thoughts</h2><div>In this post I tried to interpret and extend the gamut mapping method with modern color theories. If you agree with my arguments, please stay skeptical and be aware of my shallow knowledge of color science. I would appreciate critiques.</div><div><br /></div><div>As I mentioned in the last section, while there are a few issues, the gamut mask method works quite well in practice, as it is indeed supported by the color science. I find it fascinating that someone was able to come up with it in the 1920s, which is even earlier than the first modern CIE color space (in 1931).</div><div><br /></div><div>I also believe that these "issues" won't affect much in traditional painting. Nothing is really mathematically accurate anyways, artists are indeed free to adjust chrome/hue/value, or to decide the shape of the mask. Besides, in real life we rarely see the whole visible gamut. In fact, I believe color harmony implies bias/limiting the palette/gamut.</div><div><br /></div><div>Regarding digital versions, it is also true that most of the issues may be overcome with decent art skills. Yet I think it is important to be aware and conscious the issues when using the tool. </div><div><br /></div><div>On the other hand, maybe I can improve it by apply the method for my Zorn palette. We'll see.</div><div><br /></div><div><br /></div><h2 style="text-align: left;">Appendix: More on Munsell Colors</h2><div>I'd like to discuss a few experiments on the Munsell colors. These do not conflict with the points above, but they are less interesting, so I'll just briefly talk about them here in the end.</div><div><br /></div><div>When calculating Munsell colors under a specific illuminant, it is incorrect to simply apply chromatic adaption on the Munsell colors. That would affect models "self-emitting LEDs under the test illuminant". But we want "reflecting objects under the test illuminant". <br /></div><div><br /></div><div>To simulate real reflecting objects we have to start with the spectral reflectance. I ended up using <a href="https://colour.readthedocs.io/en/v0.3.16_a/generated/colour.XYZ_to_sd.html#colour-xyz-to-sd">colour.XYZ_to_sd</a>. But keep in mind that this can never be perfect. Information is lost when we convert a spectral distribution into a XYZ value. Also different two sets of spectral distribution may correspond to the same XYZ value, which means they look exactly the same (by the idealized observer).</div><div><br /></div><div>One interesting question, at first I expected blue colors would appear much darker under the test illuminant, because I removed all blue-ish wavelengths. However it did not turn out like that. I briefly examined the output of XYZ_to_sd, it seems keep a fraction of reflection of red-ish spectra, even for pure blue in sRGB.</div><div><br /></div><div>It might be interesting to test a spectral distribution database of paints or real life objects.</div><div><br /></div><div>In the experiment above, I removed the ~1/3 visible spectra on the blue end from D65. Actually I did the same for 1/3 spectra on the red end or in the middle.</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-faxsMbjUiLI/YGd1SPmZHvI/AAAAAAAAwdw/YzLW9oAOKTsfwiqrpElESMaHuScdKZdZgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="314" height="308" src="https://lh3.googleusercontent.com/-faxsMbjUiLI/YGd1SPmZHvI/AAAAAAAAwdw/YzLW9oAOKTsfwiqrpElESMaHuScdKZdZgCLcBGAsYHQ/w320-h308/image.png" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Removed ~1/3 visible spectra on the red end.</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-DBTL8GzTMiA/YGd1b9pSn2I/AAAAAAAAwd0/RjA65iS9jLQZMQtn16_t_x9ackd3lu-5ACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="314" height="308" src="https://lh3.googleusercontent.com/-DBTL8GzTMiA/YGd1b9pSn2I/AAAAAAAAwd0/RjA65iS9jLQZMQtn16_t_x9ackd3lu-5ACLcBGAsYHQ/w320-h308/image.png" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Reduced the intensity of ~1/3 visible spectra in the middle to 30%</td></tr></tbody></table><br />The results are in general similar, but the impact is quite different. The lost of the red-ish spectra did not have much impact, while the middle spectra had huge impact. In fact I only reduced the intensity or middle spectra to 30%, otherwise all the colors will be pushed out of sRGB.</div><div><br /></div><div>This effect can be easily understood if we examine the cone cell response curves. The right-most 1/3 span has moderate effects on L cells, but not much on M cells. Meanwhile, the middle 1/3 span covers a large fraction of visible & high sensitive ranges of both L and M. <br /></div></div></div><div><br /></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-24792339280729268112021-04-02T18:44:00.004+02:002021-04-02T18:44:48.178+02:00Notes on Color #7: Revisiting James Gurney's Gamut Mask<p>Gamut masks, or gamut mapping, is a color managing tool made popular by <a href="http://gurneyjourney.blogspot.com/">James Gurney</a>. It is a set of practical instructions, which allows us to easily create a palette of harmonic colors.</p><p></p><div>James has explained the method in various formats:</div><div><ul style="text-align: left;"><li><a href="http://gurneyjourney.blogspot.com/2008/02/from-mask-to-palette.html">2008 blog post</a></li><li>2011 series <a href="http://gurneyjourney.blogspot.com/2011/09/part-1-gamut-masking-method.html">1</a>, <a href="http://gurneyjourney.blogspot.com/2011/09/part-2-gamut-masking-method.html">2</a>, <a href="http://gurneyjourney.blogspot.com/2011/09/part-3-gamut-masking-method.html">3</a></li><li><a href="https://youtu.be/qfE4E5goEIc">Video</a> on Youtube.</li><li>His book <i>Color and Light: A Guide for the Realist Painter</i></li></ul></div><div>I found this method so inspiring when I first learned about it around 2014. Recently it came back to my mind when I started developing the <a href="http://coolwanglu.blogspot.com/2021/03/notes-on-color-6-creating-zorn-palette.html">digital Zorn palette</a>, which turned out to <a href="https://www.instagram.com/p/CNGFHfRFWCg/">work quite well</a>. I decided to revisit the cool method, in the hope of getting better understanding the method and some color therories.</div><div><br /></div><div>The goal includes:</div><div><ul style="text-align: left;"><li>Recognizing the limitation of physical paints.</li><li>Figuring out an idealized model of the method.</li><li>Adapting the method for digtal painting.</li></ul></div><div><br /></div><div><h2 style="text-align: left;">The Original Method</h2></div><p></p><p>I'd summarize the original gamut mapping method as the following 3 steps:</p><div><p></p><ul><li>Start with a color wheel.</li><li>Maskthe color wheel with a simple shape, typically a triangle.</li><li>Use only colors in the mask.</li></ul><div>This is it. Believing or not, these super simple steps actual work! </div><div><br /></div><div>James once mentioned that the method could go back (at least) to the 1920s. He adapted the method from the book <i>The Enjoyment and Use of Color</i> by Walter Sargent.</div><div><br /></div><div>On the other hand, there is some hidden, ambiguous information that are often overlooked or misinterpreted. This could be well explained by examining the typical digital implementation.</div><div><br /></div><h2 style="text-align: left;">The Typical Digital Version</h2><div>The gamut mask is available in Krita, which I will examine in details. There are also a few other versions, online, plugins or standalone binaries, which are basically the same.</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-kXE9nnzv0aY/YGYiUkbftHI/AAAAAAAAwbM/u1UfyUTaNa0jNQKqwGgb0HsBgZ4AfXr2QCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="572" data-original-width="522" height="240" src="https://lh3.googleusercontent.com/-kXE9nnzv0aY/YGYiUkbftHI/AAAAAAAAwbM/u1UfyUTaNa0jNQKqwGgb0HsBgZ4AfXr2QCLcBGAsYHQ/image.png" width="219" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Gamut Mask in Krita.</td></tr></tbody></table><br />In Krita, we start with a HSV (or HSL, HSY) color wheel. For the mask, the user may choose from a few predefined shape, or draw a custom version. In the UI there is a slider where you can adjust value/lightness/luma. More details can be found <a href="https://docs.krita.org/en/user_manual/gamut_masks.html">here</a>.</div><div><br />Well this digital adaption look so natural and intuitive that I didn't have any doubt, until recently.</div><div><br /></div><h2 style="text-align: left;">What Is Wrong? </h2><div>The first issue invovles the choice of the color wheel. In previous posts (<a href="http://coolwanglu.blogspot.com/2021/03/notes-on-color-2-whats-wrong-with-hsv.html">1</a>, <a href="http://coolwanglu.blogspot.com/2021/03/notes-on-color-4-hsy.html">2</a>) I discussed issues of value/brightness in HSV/HSL/HSY. However for gamut mask, we need something else, namely uniform distribution of the hues.</div><div><br /></div><div>In the book <i>Color and Light: A Guide for the Realist Painter</i>, James mentioned that the traditional RYB color wheel suffers from uneven distribution of hues. The red-orange-yellow section is too "loose", while the green-blue secction is too "crowded".</div><div><br /></div><div>Prior to modern color spaces, the Munsell color system was the best hue-chroma-value system that is perceptually uniform. Even today, the Munsell colors are often used to test modern color spaces. It is easy to observe the difference between HSL and CAM16UCS (a modern uniform color system), if we <a href="http://coolwanglu.blogspot.com/2021/03/notes-on-color-5-projecting-munsell.html">plot</a> the Munsell colors:</div><div><br /></div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://1.bp.blogspot.com/-EbBBhEVboV0/YGCBAg9pvlI/AAAAAAAAwYc/ikNFKhMtiSc9Vhwy5qThtW-cnANLGD6aACPcBGAYYCw/s336/image.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="282" data-original-width="336" src="https://1.bp.blogspot.com/-EbBBhEVboV0/YGCBAg9pvlI/AAAAAAAAwYc/ikNFKhMtiSc9Vhwy5qThtW-cnANLGD6aACPcBGAYYCw/s320/image.png" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Munsell Colors in HSL</td></tr></tbody></table><div><br /><br /></div></div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://1.bp.blogspot.com/-1Kf-rwuwoxU/YGB8D4oWLsI/AAAAAAAAwW4/A6IjNJHe0oc7gMjV7r2ADPcLAEyVWKoKQCPcBGAYYCw/s325/image.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="268" data-original-width="325" src="https://1.bp.blogspot.com/-1Kf-rwuwoxU/YGB8D4oWLsI/AAAAAAAAwW4/A6IjNJHe0oc7gMjV7r2ADPcLAEyVWKoKQCPcBGAYYCw/s320/image.png" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Munsell Colors in CAM16UCS</td></tr></tbody></table><br />The second issue is about chroma. Note that there is <a href="http://www.huevaluechroma.com/012.php">difference between saturation and chroma</a>. Briefly speaking, chroma is independent and absolute, while saturation is relative and depends on hue and/or value.<div><br /></div><div>In the digital version, when we adjust the V/L/Y channel, the H(ue) and S(aturation) channels remain the same. This means chroma would change along. (Well I didn't even mention the poor performance of uniformity in these models, the weird defintion of "saturation" in HSL and the horrendous stretching of chroma in HSY)<div><br />In the original version, however, James explicitly mentioned maintaining chroma when mixing colors. Well sometimes he also mentioned intensity or saturation, but I do believe he meant chroma. A solid evidence is that James obtained lighter/darker versions of the base colors by mixing other high-chroma colors, instead of with pure white/black. </div><div><br /></div><div>Next, I would justify my arguments by analyzing the idealized model.</div><div><br /></div><h2 style="text-align: left;"><br /></h2></div><div><div><p></p></div></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-41290371727325551872021-03-30T00:48:00.005+02:002021-04-01T14:08:00.971+02:00Notes on Color #6: Creating a Zorn Palette<p>Update: the palette for Krita is available <a href="https://github.com/coolwanglu/Zorn-Palette-For-Krita">here</a>.</p><p>For beginners, limited palette is a useful tool for learning to use colors. Among many of those, the Zorn palette, used by Anders Zorn, seems popular in some ateliers.</p><p>There are a few variations of the Zorn palette. The version that I'm learning consists of the following base colors:</p><p></p><ul style="text-align: left;"><li>Ivory Black</li><li>Permanent White or Titanium White</li><li>Yellow Ochre</li><li>Cadmium Red Light</li></ul><div>When painting, you are only allowed to obtain colors by mixing these base cases. Depsite of its simplicity, the palette is surprisingly powerful, especially for portrait painting.</div><div><br /></div><div>Since I'm learning both painting and color theories, I find it interesting to make a digital version.</div><div><br /></div><h2 style="text-align: left;">Mixing Paints</h2><div>The process of mixing paints is rather <a href="http://www.huevaluechroma.com/061.php">complicated</a>. It is somewhere between additive-average and subtractive. However the situation is simple because the Black and White has very few chroma, and the Red and Yellow are very close in the color space.</div><div><br /></div><div>In this case we could get quite good estimation of the mixed color by taking (weighted) geoemtric means of the spectral reflectance curves. More details can be found <a href="http://scottburns.us/subtractive-color-mixture/">here</a>. A more realistic result can be obtained <span style="text-align: center;">with </span><a href="http://zsolt-kovacs.unibs.it/colormixingtools" style="text-align: center;">ColorMixingTools</a><span style="text-align: center;">. Here is a comparison, they look close enough.</span></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-4kOq_CKruy4/YGJOog8E5LI/AAAAAAAAwZQ/xyQJHwdMRI8RddZ52CnyKv9UmeADIfIJQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="800" data-original-width="1600" height="200" src="https://lh3.googleusercontent.com/-4kOq_CKruy4/YGJOog8E5LI/AAAAAAAAwZQ/xyQJHwdMRI8RddZ52CnyKv9UmeADIfIJQCLcBGAsYHQ/w400-h200/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Spectral Reflection Curves of Cadmium Red, Yellow Orche and their 1:1 geometric mean.</td></tr></tbody></table><br /><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-ElNQQG0H8tw/YGJPbrPFU1I/AAAAAAAAwZY/ViDDxsLqaBch4vAzDhST5-m5R1X1Cf_QACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="1260" data-original-width="1604" height="314" src="https://lh3.googleusercontent.com/-ElNQQG0H8tw/YGJPbrPFU1I/AAAAAAAAwZY/ViDDxsLqaBch4vAzDhST5-m5R1X1Cf_QACLcBGAsYHQ/w400-h314/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Mixing Cadmium Red and Yellow Orche using drop2color.</td></tr></tbody></table><br /><br />Then I plotted mixes of pairs of base colors in XYZ and CAM16UCS.</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-5gMobhef6-Q/YGJRJgfGk6I/AAAAAAAAwZg/XHRNcxIgC8gzDk7mljfUDfVYSUxcIhE1ACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="1000" data-original-width="1000" height="400" src="https://lh3.googleusercontent.com/-5gMobhef6-Q/YGJRJgfGk6I/AAAAAAAAwZg/XHRNcxIgC8gzDk7mljfUDfVYSUxcIhE1ACLcBGAsYHQ/w400-h400/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Mix of pairs of base colors in XYZ<br /></td></tr></tbody></table><br /><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-uH59Qbmbt88/YGJRZaa1woI/AAAAAAAAwZo/ZchglTYtergATeZmLE9uMFVY4NkQypT7ACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="1000" data-original-width="1000" height="400" src="https://lh3.googleusercontent.com/-uH59Qbmbt88/YGJRZaa1woI/AAAAAAAAwZo/ZchglTYtergATeZmLE9uMFVY4NkQypT7ACLcBGAsYHQ/w400-h400/image.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Mix of pairs of base colors in CAM16UCS</td></tr></tbody></table><br />Interestingly, the edges look quite straight. This means we could even simply use linear combination as a good estimate. Note that linear combinations does make sense in term of mixing lights, and it is much easier to compute.</div><div><br /></div><div><h2 style="text-align: left;">Computing the Zorn Color Space</h2>Now the task is to compute all linear combinations of the colors. More accurately, we want all weighted arithmetic means of these colors. This is naturally the volume enclosed by the convex hull of the 4 colors.<br /><br />The convex hull may be computed in XYZ or a linear RGB space. Note that since XYZ and linear RGB are just linear tranformation of each other, the result color space are essentially the same.</div><div><br /></div><div>To me it was not trivial how to arrange the color space into a palette. Note that the Zorn color space is a 3d volume, but a palette is ususally 1d or 2d. After some research I decided to put the volume in CAM16UCS, then take slices of the volume at different luma's, which fit the way I intend to use it in painting.</div><div><br /></div><div>At last, just for fun, I also computed the convext hull in CAM16UCS for comparison, which may or may not make any sense.</div><div><br /></div><div>Here's the result:<br /></div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen="" class="BLOG_video_class" height="266" src="https://www.youtube.com/embed/Oc6uA5QyRZo?playlist=Oc6uA5QyRZo&loop=1" width="320" youtube-src-id="Oc6uA5QyRZo"></iframe></div><br />While both versions look simliar, the XYZ version seems better.<div><br /></div><div><br /><h2 style="text-align: left;">Producing the Palette</h2><div>Now the palette can be obtained by taking samples of the volume at grid points. Here are two slices.</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-P00hswFt29Q/YGJU5iq9_MI/AAAAAAAAwZw/ovxuFtmVGwMmngj49qhJYtq079t3-R9MgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="231" data-original-width="231" height="240" src="https://lh3.googleusercontent.com/-P00hswFt29Q/YGJU5iq9_MI/AAAAAAAAwZw/ovxuFtmVGwMmngj49qhJYtq079t3-R9MgCLcBGAsYHQ/image.png" width="240" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">The Zorn Palette at J=35<span> </span></td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-zBVNVedP2Zk/YGJVCd0oUoI/AAAAAAAAwZ0/VpBMfNsMLiw_B8jmVIHKOVI2ZYGuVLmUgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="231" data-original-width="231" height="240" src="https://lh3.googleusercontent.com/-zBVNVedP2Zk/YGJVCd0oUoI/AAAAAAAAwZ0/VpBMfNsMLiw_B8jmVIHKOVI2ZYGuVLmUgCLcBGAsYHQ/image.png" width="240" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">The Zorn Palette at J=65</td></tr></tbody></table><br />I was also able to export the palette for Krita.</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-MD4Yjsts7O8/YGJWCEXP2EI/AAAAAAAAwaA/UZTOItT08u8Qs-uV0B698FUyYyabMctmwCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="1394" data-original-width="244" height="640" src="https://lh3.googleusercontent.com/-MD4Yjsts7O8/YGJWCEXP2EI/AAAAAAAAwaA/UZTOItT08u8Qs-uV0B698FUyYyabMctmwCLcBGAsYHQ/w112-h640/image.png" width="112" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Zorn Palette for Krita.<br /></td></tr></tbody></table><br /><h2 style="text-align: left;">Final Thoughts</h2><div>While it is merely a quick hack with random decisions here and there. I reckon this palette would serve well in my learning of the palette.</div><div><br /></div><div>The Zorn palette may be viewed as a simple specific version of color gamut masks, which I plan to study further. In fact I do have questions and complaints about popular implementations of gamut masks. For example, common implementations involve:</div><div><ul style="text-align: left;"><li>The HSV/HSL/HSY color wheel</li><li>A regular, fixed shape on the color wheel, regardless of the value.</li></ul></div><div>However I don't find good color/math theories supporting these choices. As shown above, I expect the shape of the mask to be irregular and changing at different values.</div></div><div><br /></div><div>On the other hand, probably it doesn't matter at all. After all this is merely a guide for artists. It is up to the artists to make decisions based on their knowledge and styles.</div><p></p></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com2tag:blogger.com,1999:blog-33534782.post-88551762785190056482021-03-28T15:13:00.009+02:002022-09-02T21:59:54.228+02:00Notes on Color #5: Projecting Munsell Colors<p>Before the digital era, the <a href="https://en.wikipedia.org/wiki/Munsell_color_system">Munsell Color System</a> was probably the best perceptually uniform color system with hue, chroma and value components. It is also used nowadays.</p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Munsell_1943_color_solid_cylindrical_coordinates_gray.png/1200px-Munsell_1943_color_solid_cylindrical_coordinates_gray.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="600" data-original-width="800" height="240" src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Munsell_1943_color_solid_cylindrical_coordinates_gray.png/1200px-Munsell_1943_color_solid_cylindrical_coordinates_gray.png" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">The 1943 Munsell renotations (with portion cut away).<br />Source: <a href="https://commons.wikimedia.org/wiki/File:Munsell_1943_color_solid_cylindrical_coordinates_gray.png#/media/File:Munsell_1943_color_solid_cylindrical_coordinates_gray.png">Wikipedia</a> <a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY-SA 3.0</a></td></tr></tbody></table><br />When reading the introduction page of <a href="https://bottosson.github.io/posts/oklab/">Oklab</a>, I learned about the idea of projecting Munsell colors into diffrent color space. I find it an intuitive and fun way to study color space. Who does not like colorful demos?<div><br /></div><div>Here we have to assume the quality of the Munsell data, which might not be 100% scientific. Anways I think it should be good enough, as proved by generations of aritist.</div><div><br /></div><div>With this assmption, we may examine munsell colors in the target color space, and observe the following:</div><div><br /></div><div>- Do the points with same chroma form a perfect circle? Are they distributed evenly?</div><div>- Do the points with same hue form a straight line? Are they distributed evenly?</div><div>- For luminance/brightness, actually I assume decient color spaces are already good enough. </div><div><br /></div><div style="text-align: left;"><h2 style="text-align: left;">The Results</h2><div>Here are projections of Munsell colors with value = 5.</div><div><br /></div><h3 style="text-align: left;">My farvorites: CAM16-UCS and Oklab. </h3><div><br /></div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-1Kf-rwuwoxU/YGB8D4oWLsI/AAAAAAAAwWo/cJ-v3HTtDuwKGYs0-CbJf26rFPCQ0FOCACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="268" data-original-width="325" height="240" src="https://lh3.googleusercontent.com/-1Kf-rwuwoxU/YGB8D4oWLsI/AAAAAAAAwWo/cJ-v3HTtDuwKGYs0-CbJf26rFPCQ0FOCACLcBGAsYHQ/image.png" width="291" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">CAM16-UCS</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-dga38WAbxmI/YGB8LCJSduI/AAAAAAAAwWs/0dRFsGMUlQMJBgabiTpzoveWGYcmHwGAACLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="267" data-original-width="331" height="240" src="https://lh3.googleusercontent.com/-dga38WAbxmI/YGB8LCJSduI/AAAAAAAAwWs/0dRFsGMUlQMJBgabiTpzoveWGYcmHwGAACLcBGAsYHQ/image.png" width="298" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Oklab</td></tr></tbody></table><h3>Others.</h3></div><div>Note that some models are not even designed for perception. They are simply presented here for fun.</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-YLSyQwmU7KU/YGB9U7xr5AI/AAAAAAAAwXA/Rx3zSDfq4KMoQlPE2Lgf2f6xlXrOLvcqQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="300" data-original-width="321" height="240" src="https://lh3.googleusercontent.com/-YLSyQwmU7KU/YGB9U7xr5AI/AAAAAAAAwXA/Rx3zSDfq4KMoQlPE2Lgf2f6xlXrOLvcqQCLcBGAsYHQ/image.png" width="257" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">CIELAB</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-t4eBrW0102U/YGB9bCmf5NI/AAAAAAAAwXE/ou2fArU3DtQHVGcCuANYmSUvhYK2XL0mgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="275" data-original-width="327" height="240" src="https://lh3.googleusercontent.com/-t4eBrW0102U/YGB9bCmf5NI/AAAAAAAAwXE/ou2fArU3DtQHVGcCuANYmSUvhYK2XL0mgCLcBGAsYHQ/image.png" width="285" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">CIELUV</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-YqX8TywqOmU/YGB9gaYGcGI/AAAAAAAAwXI/uK-wviwfhdImumo3zwjAqCTTt1RsdadMQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="294" height="240" src="https://lh3.googleusercontent.com/-YqX8TywqOmU/YGB9gaYGcGI/AAAAAAAAwXI/uK-wviwfhdImumo3zwjAqCTTt1RsdadMQCLcBGAsYHQ/image.png" width="233" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;"><div class="separator" style="clear: both; text-align: center;">Hunter Lab</div><br /></td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-aDpPm_jsWAQ/YGB9t_mW-sI/AAAAAAAAwXY/bSmlcQhU4Cou-utGXIB_z2tCHAONOGapgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="305" data-original-width="305" height="240" src="https://lh3.googleusercontent.com/-aDpPm_jsWAQ/YGB9t_mW-sI/AAAAAAAAwXY/bSmlcQhU4Cou-utGXIB_z2tCHAONOGapgCLcBGAsYHQ/image.png" width="240" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">IPT</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-_L_H_1DNMCA/YGB9zFFk7iI/AAAAAAAAwXc/KqoTGEmVnzE1O6r1EGgeyD21zNoyG6yVwCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="283" height="240" src="https://lh3.googleusercontent.com/-_L_H_1DNMCA/YGB9zFFk7iI/AAAAAAAAwXc/KqoTGEmVnzE1O6r1EGgeyD21zNoyG6yVwCLcBGAsYHQ/image.png" width="224" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">OSA UCS<br /></td></tr></tbody></table><br /><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-lGdhqjWDxno/YGB933T-f5I/AAAAAAAAwXk/A3Jn1a6vl7MMjG9PkmwKN8_3c3kFe-OMQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="174" data-original-width="321" height="173" src="https://lh3.googleusercontent.com/-lGdhqjWDxno/YGB933T-f5I/AAAAAAAAwXk/A3Jn1a6vl7MMjG9PkmwKN8_3c3kFe-OMQCLcBGAsYHQ/image.png" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">SRLab2</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-hsiK_xQVy9A/YGB-HOdoOBI/AAAAAAAAwX4/W-fJURbWP-cu3wrQXptlzmy2JwaYsmGvgCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="251" height="240" src="https://lh3.googleusercontent.com/-hsiK_xQVy9A/YGB-HOdoOBI/AAAAAAAAwX4/W-fJURbWP-cu3wrQXptlzmy2JwaYsmGvgCLcBGAsYHQ/image.png" width="199" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">YCbCr</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-TyLBrOGMQrw/YGB-3H9UyEI/AAAAAAAAwYI/9h2NVmQ2j3o6LL9zPxUFqoeSKYLvEVb1wCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="306" data-original-width="129" height="240" src="https://lh3.googleusercontent.com/-TyLBrOGMQrw/YGB-3H9UyEI/AAAAAAAAwYI/9h2NVmQ2j3o6LL9zPxUFqoeSKYLvEVb1wCLcBGAsYHQ/image.png" width="101" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">CIEXYZ</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-TVyq37nX8gA/YGC--Mj-26I/AAAAAAAAwY0/bw-a7n7rjDQzTNHigjPdTb3G9d0UcBjNQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="303" data-original-width="300" height="240" src="https://lh3.googleusercontent.com/-TVyq37nX8gA/YGC--Mj-26I/AAAAAAAAwY0/bw-a7n7rjDQzTNHigjPdTb3G9d0UcBjNQCLcBGAsYHQ/image.png" width="238" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">xyY</td></tr></tbody></table><br /><br /><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-zkTEwtuwLIQ/YGCA8t1pPpI/AAAAAAAAwYQ/xQ__eH0RYdIoMjgtt8LxFQaxjoVS64jOQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="277" data-original-width="335" height="240" src="https://lh3.googleusercontent.com/-zkTEwtuwLIQ/YGCA8t1pPpI/AAAAAAAAwYQ/xQ__eH0RYdIoMjgtt8LxFQaxjoVS64jOQCLcBGAsYHQ/image.png" width="290" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">HSV</td></tr></tbody></table><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-EbBBhEVboV0/YGCBAg9pvlI/AAAAAAAAwYU/EOvSh2AlOt4Z3R8kBIITcw33h7wjYZHPwCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="282" data-original-width="336" height="240" src="https://lh3.googleusercontent.com/-EbBBhEVboV0/YGCBAg9pvlI/AAAAAAAAwYU/EOvSh2AlOt4Z3R8kBIITcw33h7wjYZHPwCLcBGAsYHQ/image.png" width="286" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">HSL</td></tr></tbody></table></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0tag:blogger.com,1999:blog-33534782.post-25061547152301475562021-03-28T14:31:00.002+02:002021-03-28T14:36:35.893+02:00Notes on Color #4: HSY<p><a href="https://coolwanglu.blogspot.com/2021/03/notes-on-color-2-whats-wrong-with-hsv.html">Previously</a> I discussed why HSV and HSL are bad, despite that they are quite popular adopted by digital painting tools.</p><p>I learnd about HSY from Krita, which seems to solve a number of issues. Here I did some quick explorations in order to learn more about it's properties.</p><p>First of all, HSY is very similar to other HS* family members. The definition of H and S should be the same as in HSL. Y is for Luma, which is a weighted sum of (gamma-corrected) all three components. The weights reflect our brightness sensitivity of different wavelengths. The specific values depend on the actual primary colors.</p><p>Here's a HSY disk at Y=0.5, for sRGB.<br /></p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-k7nL81hbScY/YGBdrntiZXI/AAAAAAAAwV4/lCJNBmsU4PAM3eKTyq4_u1dQDtg3Lpu5wCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="231" data-original-width="231" height="240" src="https://lh3.googleusercontent.com/-k7nL81hbScY/YGBdrntiZXI/AAAAAAAAwV4/lCJNBmsU4PAM3eKTyq4_u1dQDtg3Lpu5wCLcBGAsYHQ/image.png" width="240" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">HCY disk with Y=0.5</td></tr></tbody></table><br />Comparing with HSV or HSL disk, this one looks smoother, and a bit "muddy" near the center. This means the Y value does predicts the actual luminance well. The gray version (converted via CIELAB) may verify this observation:<div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-hjqTIHaxXTc/YGBruFKnp2I/AAAAAAAAwWI/p4OI_DVLMI8DlEu8QZANC6NTSFNOzio-QCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="231" data-original-width="231" height="240" src="https://lh3.googleusercontent.com/-hjqTIHaxXTc/YGBruFKnp2I/AAAAAAAAwWI/p4OI_DVLMI8DlEu8QZANC6NTSFNOzio-QCLcBGAsYHQ/image.png" width="240" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">L(CIELAB) channel of the HCY disk.</td></tr></tbody></table><br /><br /></div><div>So there is a huge improvement over other HS* models. It seems good enough for digital painting, right? Well, yes and no. I mean no.</div><div><br /></div><h2 style="text-align: left;">The Two Lies</h2><div>Well the "huge improment" part is true, but there are two lies above.</div><div><br /></div><div>First of all, notice the "HCY" in the captions, that was a not a typo. The distance to the center represents chroma rather than saturation.</div><div><br /></div><div>Second, you may notice some lighter areas in the grey version, near the purple area and green area. That is not an illusion.</div><div><br /></div><div>This changes the story entirely. Allow me to reveal the imperfect truth.</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-qGZ-zvlVbTs/YGBuegRvGgI/AAAAAAAAwWQ/AhTzAvw9teIpgYoa7cnnWoWoai-6_xAZQCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="231" data-original-width="231" height="240" src="https://lh3.googleusercontent.com/-qGZ-zvlVbTs/YGBuegRvGgI/AAAAAAAAwWQ/AhTzAvw9teIpgYoa7cnnWoWoai-6_xAZQCLcBGAsYHQ/image.png" width="240" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">sRGB colors in the HCY disk where Y=0.5.</td></tr></tbody></table> <br />This weird shape represents all sRGB colors on the disk. At first I was quite sure that something is wrong in my code. Later I realized that if (r, g, b) has a luma of 0.5, then so does (1-r, 1-g, 1-b) , provided that the sum of the component weights is 1.</div><div><br /></div><div>In the previous colorful version, the out-of-gamut colors were capped, therefore not accurate.</div><div><br /></div><div>This weird shape is problematic, somtimes it is no longer possible to mix two colors by picking a point on the line segment. On the other hand, in Krita we do have a full-circle version:</div><div><br /></div><div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://lh3.googleusercontent.com/-ZdR6kTQt6no/YGBzrEXzV4I/AAAAAAAAwWY/4CjFlsLtZ5wuHPA4QqHpdSYdABgUAW_4gCLcBGAsYHQ/image.png" style="margin-left: auto; margin-right: auto;"><img alt="" data-original-height="866" data-original-width="1020" height="240" src="https://lh3.googleusercontent.com/-ZdR6kTQt6no/YGBzrEXzV4I/AAAAAAAAwWY/4CjFlsLtZ5wuHPA4QqHpdSYdABgUAW_4gCLcBGAsYHQ/image.png" width="283" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">HSY disk in Krita, with Y near 0.5</td></tr></tbody></table><br /><br /></div><div>It appears more "muddy" here. If you examine the colors near the border, red-ish and blue-ish areas look fine, but other parts look gray-ish. </div><div><br /></div><div>In fact this version is obatained by stretching the HCY disk. Each radius is stretched to [0, 1] independently. This way the grey-ish area at the center appears much bigger than it is.</div><div><br /></div><div>Personally I don't think this transformation makes much sense. Now the saturation value depends on both hue and brightness, so two saturation values are not really comparable. I think we should instead accept something like, the most "colorful yellow" is always brighter than the most "colorful blue" (within a (usual) RGB model). Therefore we should always be careful when shifting hues for high-chroma colors.</div><div><br /></div><div><br /><br /></div>Lu Wanghttp://www.blogger.com/profile/00576609954224192924noreply@blogger.com0