|
| 1 | + |
| 2 | +module CrossSectionalCharts |
| 3 | + |
| 4 | +using Colors |
| 5 | +using Base64 |
| 6 | + |
| 7 | +struct CrossSectionalChartSVG <: Main.SVG |
| 8 | + buf::IOBuffer |
| 9 | +end |
| 10 | + |
| 11 | +struct Axis |
| 12 | + index::Int # compornent index |
| 13 | + label::String |
| 14 | + range::AbstractRange |
| 15 | +end |
| 16 | +Base.first(a::Axis) = first(a.range) |
| 17 | +Base.last(a::Axis) = last(a.range) |
| 18 | + |
| 19 | +function crosssection(::Type{C}; |
| 20 | + x::Tuple=(1, "X", 0:1), |
| 21 | + y::Tuple=(2, "Y", 0:1), |
| 22 | + z::Tuple=(3, "Z", 0:1)) where C <: Color |
| 23 | + crosssection(C, Axis(x...), Axis(y...), Axis(z...)) |
| 24 | +end |
| 25 | +function crosssection(::Type{C}, x::Axis, y::Axis, z::Axis) where C <: Color |
| 26 | + io = IOBuffer() |
| 27 | + id = String(nameof(C)) |
| 28 | + |
| 29 | + write(io, |
| 30 | + """ |
| 31 | + <svg xmlns="http://www.w3.org/2000/svg" |
| 32 | + xmlns:xlink="http://www.w3.org/1999/xlink" |
| 33 | + id="svg_$id" version="1.1" |
| 34 | + viewBox="0 0 40 30" width="27.09mm" height="20.32mm" |
| 35 | + stroke="none" style="display:inline; margin-left:2em; margin-bottom:1em"> |
| 36 | + <defs> |
| 37 | + <filter id="filter_g_$id" filterUnits="userSpaceOnUse" x="0" y="-0.5" width="15.5" height="16"> |
| 38 | + <feGaussianBlur stdDeviation="0.1" result="blur"/> |
| 39 | + <feMerge> |
| 40 | + <feMergeNode in="SourceGraphic"/> |
| 41 | + <feMergeNode in="blur"/> |
| 42 | + </feMerge> |
| 43 | + </filter> |
| 44 | + <style type="text/css"><![CDATA[ |
| 45 | + #svg_$id g, #svg_$id image { |
| 46 | + transition: all 400ms ease 200ms; |
| 47 | + } |
| 48 | + #svg_$id path.b { |
| 49 | + stroke: currentColor; |
| 50 | + stroke-width: 0.5; |
| 51 | + opacity: 0; |
| 52 | + transition: all 200ms ease 0ms; |
| 53 | + } |
| 54 | + #svg_$id image { |
| 55 | + opacity: 0; |
| 56 | + filter:url(#filter_g_$id); |
| 57 | + } |
| 58 | + #svg_$id rect:active ~ image { |
| 59 | + opacity: 1; |
| 60 | + transition: all 200ms ease 0ms; |
| 61 | + } |
| 62 | + #svg_$id rect:hover ~ image { |
| 63 | + opacity: 1; |
| 64 | + transition: all 200ms ease 0ms; |
| 65 | + } |
| 66 | + #svg_$id rect:active ~ path.b { |
| 67 | + opacity: 0.8; |
| 68 | + } |
| 69 | + #svg_$id rect:hover ~ path.b { |
| 70 | + opacity: 0.8; |
| 71 | + } |
| 72 | + #svg_$id text { |
| 73 | + fill: currentColor; |
| 74 | + fill-opacity: 0.8; |
| 75 | + stroke: #aaa; |
| 76 | + stroke-width: 0.2; |
| 77 | + stroke-opacity: 0.4; |
| 78 | + font-size: 3px; |
| 79 | + } |
| 80 | + #svg_$id text.n { |
| 81 | + opacity:0; |
| 82 | + } |
| 83 | + #svg_$id:hover text.n { |
| 84 | + opacity:1; |
| 85 | + } |
| 86 | + #svg_$id:active text.n { |
| 87 | + opacity:1; |
| 88 | + } |
| 89 | + ]]></style> |
| 90 | + </defs> |
| 91 | + """) |
| 92 | + |
| 93 | + xs = [xv for xv in range(first(x), stop=last(x), length=16)] |
| 94 | + ys = [yv for yv in range(last(y), stop=first(y), length=16)] |
| 95 | + zs = [zv for zv in range(first(z), stop=last(z), length=11)] |
| 96 | + xmidf = (first(x) + last(x)) * 0.5 |
| 97 | + ymidf = (first(y) + last(y)) * 0.5 |
| 98 | + xmid = isinteger(xmidf) ? Int(xmidf) : xmidf |
| 99 | + ymid = isinteger(ymidf) ? Int(ymidf) : ymidf |
| 100 | + |
| 101 | + vec = [0.0, 0.0, 0.0] |
| 102 | + function col(xv, yv, zv) |
| 103 | + vec[x.index] = xv |
| 104 | + vec[y.index] = yv |
| 105 | + vec[z.index] = zv |
| 106 | + # TODO: Add more appropriate out-of-gamut color handling |
| 107 | + xyz = convert(XYZ, C(vec...)) |
| 108 | + rgb = convert(RGB, XYZ(max(xyz.x,0), max(xyz.y,0), max(xyz.z,0))) |
| 109 | + end |
| 110 | + |
| 111 | + # add swatches of color bar and planes by layer |
| 112 | + for i = 1:11 |
| 113 | + zi = isodd(i) ? 6 - i÷2 : 6 + i÷2 # zigzag order |
| 114 | + plane = [col(xs[xi], ys[yi], zs[zi]) for yi = 1:16, xi = 1:16] |
| 115 | + ccolor = col(xmid, ymid, zs[zi]) # center color (for color bar) |
| 116 | + barh = i == 1 ? 30 : 16.5 - 3*(i÷2) |
| 117 | + op = i == 1 ? "style=\"opacity:1;\"" : "" |
| 118 | + write(io, |
| 119 | + """ |
| 120 | + <g> |
| 121 | + <rect fill="#$(hex(ccolor))" width="4" height="$(barh)" x="36" y="$(isodd(i) ? 30 - barh : 0)" /> |
| 122 | + <image width="16" height="16" transform="scale(2) translate(-.5,0)" $op xlink:href="data:image/png;base64,""") |
| 123 | + b64enc = Base64EncodePipe(io) |
| 124 | + write_png(b64enc, plane) |
| 125 | + close(b64enc) |
| 126 | + write(io, "\" />\n") |
| 127 | + write(io, |
| 128 | + """ |
| 129 | + <path d="m35,$(33-3zi) h 5" class="b"/> |
| 130 | + </g> |
| 131 | + """) |
| 132 | + end |
| 133 | + # add labels |
| 134 | + if first(x.range) * last(x.range) < 0 |
| 135 | + write(io, |
| 136 | + """ |
| 137 | + <path d="M0,15 h30 M15,0 v30" style="stroke:currentColor;stroke-width:0.125"/> |
| 138 | + <text x="29.5" y="14" style="text-anchor:end;">$(x.label)</text> |
| 139 | + <text x="29.5" y="18" style="text-anchor:end;" class="n">$(last(x))</text> |
| 140 | + <text x="16" y="3" style="text-anchor:start;">$(y.label)</text> |
| 141 | + <text x="14" y="3" style="text-anchor:end;" class="n">$(last(y))</text> |
| 142 | + <text x="14" y="18" style="text-anchor:end;" class="n">0</text> |
| 143 | + """) |
| 144 | + else |
| 145 | + write(io, |
| 146 | + """ |
| 147 | + <text x="29.5" y="26" style="text-anchor:end;">$(x.label)</text> |
| 148 | + <text x="29.5" y="29" style="text-anchor:end;" class="n">$(last(x))</text> |
| 149 | + <text x="15" y="29" style="text-anchor:middle;" class="n">$xmid</text> |
| 150 | + <text x="6" y="3" style="text-anchor:start;">$(y.label)</text> |
| 151 | + <text x="0.5" y="3" style="text-anchor:start;" class="n">$(last(y))</text> |
| 152 | + <text x="0.5" y="16" style="text-anchor:start;" class="n">$ymid</text> |
| 153 | + <text x="0.5" y="29" style="text-anchor:start;" class="n">0</text> |
| 154 | + """) |
| 155 | + end |
| 156 | + write(io, |
| 157 | + """ |
| 158 | + <text style="text-anchor:middle;" transform="translate(35,15) rotate(-90)">$(z.label)</text> |
| 159 | + <text x="36" y="3" style="text-anchor:end;" class="n">$(last(z))</text> |
| 160 | + <text x="36" y="29" style="text-anchor:end;" class="n">$(first(z))</text> |
| 161 | + <text x="2" y="26" style="fill:#fff;fill-opacity:1;text-anchor:start;">$id</text> |
| 162 | + <path d="m0,0 h40 v30 h-40 z" style="fill:none;stroke:none;" /> |
| 163 | + </svg>""") |
| 164 | + CrossSectionalChartSVG(io) |
| 165 | +end |
| 166 | + |
| 167 | + |
| 168 | +function write_png(io::IO, cs::AbstractArray{T}) where T <: Color |
| 169 | + buf = IOBuffer() # to calculate chunk CRCs |
| 170 | + n = 16 # 16 x 16 |
| 171 | + u8(x) = write(buf, UInt8(x & 0xFF)) |
| 172 | + u16(x) = (u8((x & 0xFFFF)>>8); u8(x)) |
| 173 | + u32(x) = (u16((x & 0xFFFFFFFF)>>16); u16(x)) |
| 174 | + b(bstr) = write(buf, bstr) |
| 175 | + function pallet(c::Color) |
| 176 | + rgb24 = convert(RGB24,c) |
| 177 | + u8(rgb24.color>>16); u8(rgb24.color>>8); u8(rgb24.color) |
| 178 | + end |
| 179 | + crct(x) = (for i = 1:8; x = x & 1==1 ? 0xEDB88320 ⊻ (x>>1) : x>>1 end; x) |
| 180 | + table = UInt32[crct(i) for i = 0x00:0xFF] |
| 181 | + function crc32() |
| 182 | + seekstart(buf) |
| 183 | + crc = 0xFFFFFFFF |
| 184 | + while !eof(buf) |
| 185 | + crc = (crc>>8) ⊻ table[(crc&0xFF) ⊻ read(buf, UInt8) + 1] |
| 186 | + end |
| 187 | + u32(crc ⊻ 0xFFFFFFFF) |
| 188 | + end |
| 189 | + flush() = write(io, take!(seekstart(buf))) |
| 190 | + |
| 191 | + # The following is a pre-encoded 256-indexed-color PNG with size of 16x16. |
| 192 | + # We only rewrite "pallets". |
| 193 | + b(b"\x89PNG\x0D\x0A\x1A\x0A") |
| 194 | + # Image header |
| 195 | + u32(13); flush(); b(b"IHDR"); u32(n); u32(n); u8(8); u8(3); u8(0); u8(0); u8(0); crc32() |
| 196 | + # Palette |
| 197 | + u32(n * n * 3); flush(); |
| 198 | + b(b"PLTE") |
| 199 | + for y = 1:n, x = 1:n |
| 200 | + pallet(cs[y,x]) |
| 201 | + end |
| 202 | + crc32() |
| 203 | + # Image data |
| 204 | + u32(58); flush(); b(b"IDAT") |
| 205 | + b(b"\x78\xDA\x63\x64\x60\x44\x03\x02\xE8\x02\x0A\xE8\x02\x06\xE8\x02") |
| 206 | + b(b"\x0E\xE8\x02\x01\xE8\x02\x09\xE8\x02\x05\xE8\x02\x0D\xE8\x02\x13") |
| 207 | + b(b"\xD0\x05\x16\xA0\x0B\x6C\x40\x17\x38\x80\x2E\x70\x01\x5D\xE0\x01") |
| 208 | + b(b"\xBA\xC0\x07\x34\x3E\x00\x54\x4D\x08\x81"); crc32() |
| 209 | + # Image trailer |
| 210 | + u32(0); flush(); b(b"IEND"); crc32() |
| 211 | + flush() |
| 212 | +end |
| 213 | + |
| 214 | +""" |
| 215 | +# Image data |
| 216 | +using CodecZlib |
| 217 | +raw = IOBuffer() |
| 218 | +for y = 0:15 |
| 219 | + write(raw, UInt8(1)) # filter: SUB |
| 220 | + write(raw, UInt8(y*16)) # line head |
| 221 | + write(raw, UInt8[1 for i=1:15]) # left + 1 |
| 222 | +end |
| 223 | +flush(raw) |
| 224 | +cd = ZlibCompressorStream(raw,level=9) |
| 225 | +flush(cd) |
| 226 | +seekstart(cd) |
| 227 | +@show read(cd) # UInt8[0x78, 0xda, 0x63, 0x64, ... |
| 228 | +""" |
| 229 | + |
| 230 | +crosssection(::Type{HSV}) = crosssection(HSV, x=(2, "S", 0:1), |
| 231 | + y=(3, "V", 0:1), |
| 232 | + z=(1, "H", 0:360)) |
| 233 | +crosssection(::Type{HSL}) = crosssection(HSL, x=(2, "S", 0:1), |
| 234 | + y=(3, "L", 0:1), |
| 235 | + z=(1, "H", 0:360)) |
| 236 | +crosssection(::Type{HSI}) = crosssection(HSI, x=(2, "S", 0:1), |
| 237 | + y=(3, "I", 0:1), |
| 238 | + z=(1, "H", 0:360)) |
| 239 | + |
| 240 | +crosssection(::Type{Lab}) = crosssection(Lab, x=(2, "a*", -100:100), |
| 241 | + y=(3, "b*", -100:100), |
| 242 | + z=(1, "L*", 0:100)) |
| 243 | +crosssection(::Type{Luv}) = crosssection(Luv, x=(2, "u*", -100:100), |
| 244 | + y=(3, "v*", -100:100), |
| 245 | + z=(1, "L*", 0:100)) |
| 246 | +crosssection(::Type{LCHab}) = crosssection(LCHab, x=(2, "C*", 0:100), |
| 247 | + y=(1, "L*", 0:100), |
| 248 | + z=(3, "H", 0:360)) |
| 249 | +crosssection(::Type{LCHuv}) = crosssection(LCHuv, x=(2, "C*", 0:100), |
| 250 | + y=(1, "L*", 0:100), |
| 251 | + z=(3, "H", 0:360)) |
| 252 | + |
| 253 | +crosssection(::Type{YIQ}) = crosssection(YIQ, x=(2, "I", -1:1), |
| 254 | + y=(3, "Q", -1:1), |
| 255 | + z=(1, "Y", 0:1)) |
| 256 | +crosssection(::Type{YCbCr}) = crosssection(YCbCr, x=(2, "Cb", 0:256), |
| 257 | + y=(3, "Cr", 0:256), |
| 258 | + z=(1, "Y", 0:256)) |
| 259 | +end |
0 commit comments