Login
gpcr/spec/dark_transform_spec.cr
Login

[edit]

spec/dark_transform_spec.cr @ trunk

spec

require 'spec'
scale = 0.75
fontscale = 1.500
box wid 1.380 ht 0.340 with .nw at (0.035, -0.035) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.380 ht 0.340 with .nw at (0.000, 0.000) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.022) fill 0xffffff color 0xffffff
line from (0.140, 0.000) to (0.140, -0.340) color white
line from (1.240, 0.000) to (1.240, -0.340) color white
text "require 'spec'" color white at (0.690, -0.170)

crystal source

require "spec"

json block

{
  "kind": "require",
  "name": "spec",
  "doc": null,
  "children": []
}

../src/gpcr/tokens

require '../src/gpcr/tokens'
scale = 0.75
fontscale = 1.500
box wid 2.353 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.353 ht 0.340 with .nw at (0.000, -0.420) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.443) fill 0xffffff color 0xffffff
line from (0.140, -0.420) to (0.140, -0.760) color white
line from (2.213, -0.420) to (2.213, -0.760) color white
text "require '../src/gpcr/tokens'" color white at (1.177, -0.590)

crystal source

require "../src/gpcr/tokens"

json block

{
  "kind": "require",
  "name": "../src/gpcr/tokens",
  "doc": null,
  "children": []
}

private def parse_hex(hex : String) : {Int32, Int32, Int32}
  # Skip the "0x" prefix (2 chars), then parse 6 hex digits.
  s = hex.starts_with?("0x") || hex.starts_with?("0X") ? hex[2..] : hex
  r = s[0, 2].to_i(16)
  g = s[2, 2].to_i(16)
  b = s[4, 2].to_i(16)
  {r, g, b}
end

# Apply the Fossil pikchr dark transform to a single RGB triple.
# Returns the transformed {r, g, b} where each channel is in [0, 127].
private def pikchr_dark_transform(r : Int32, g : Int32, b : Int32) : {Int32, Int32, Int32}
  m = r > g ? (r > b ? r : b) : (g > b ? g : b)
  tr = (r + 254 - m) // 2
  tg = (g + 254 - m) // 2
  tb = (b + 254 - m) // 2
  # Clamp to [0, 127] (negative values from integer truncation clamp to 0;
  # max channel always yields exactly 127).
  {tr < 0 ? 0 : tr, tg < 0 ? 0 : tg, tb < 0 ? 0 : tb}
end

describe "Fossil pikchr dark transform — formula invariants" do
  it "max channel always transforms to 127" do
    # For any colour with max_channel = M, T(M) = (M + 254 - M) / 2 = 127.
    [71, 128, 192, 207, 230, 255].each do |m|
      tr, tg, tb = pikchr_dark_transform(m, 0, 0)
      tr.should eq(127), "T(max=#{m}, g=0, b=0): expected r=127 got #{tr}"
      tr2, tg2, tb2 = pikchr_dark_transform(0, m, 0)
      tg2.should eq(127), "T(r=0, max=#{m}, b=0): expected g=127 got #{tg2}"
      tr3, tg3, tb3 = pikchr_dark_transform(0, 0, m)
      tb3.should eq(127), "T(r=0, g=0, max=#{m}): expected b=127 got #{tb3}"
    end
  end

  it "min channel (0) with max=255 transforms to 0 (clamped from -0.5)" do
    # (0 + 254 - 255) / 2 = -1/2 = 0 after integer truncation and clamp.
    tr, tg, tb = pikchr_dark_transform(255, 0, 0)
    tg.should eq(0)
    tb.should eq(0)
  end

  it "output channels are always in [0, 127]" do
    samples = [
      {255, 171, 25}, {76, 151, 255}, {89, 192, 89},
      {255, 68, 68}, {153, 102, 255}, {255, 253, 231},
    ]
    samples.each do |rgb|
      r, g, b = pikchr_dark_transform(rgb[0], rgb[1], rgb[2])
      r.should be >= 0
      r.should be <= 127
      g.should be >= 0
      g.should be <= 127
      b.should be >= 0
      b.should be <= 127
    end
  end
end

describe "GPCR::Tokens dark transform — mode-agnostic palette dual appearances" do
  # For each token: assert T(hex) = expected_rgb exactly.
  # These expected values are annotated in tokens.cr colour comments and define
  # the dual-constraint specification: both the light-mode hex and the dark-mode
  # T(hex) must be vivid and category-identifiable.

  {
    "COLOR_DEFAULT"   => {GPCR::Tokens::COLOR_DEFAULT, {37, 75, 127}},
    "COLOR_CONTROL"   => {GPCR::Tokens::COLOR_CONTROL, {127, 85, 12}},
    "COLOR_REPORTER"  => {GPCR::Tokens::COLOR_REPORTER, {127, 69, 12}},
    "COLOR_BOOLEAN"   => {GPCR::Tokens::COLOR_BOOLEAN, {75, 127, 75}},
    "COLOR_STOP"      => {GPCR::Tokens::COLOR_STOP, {127, 33, 33}},
    "COLOR_PROCEDURE" => {GPCR::Tokens::COLOR_PROCEDURE, {76, 50, 127}},
    "COLOR_C_INNER"   => {GPCR::Tokens::COLOR_C_INNER, {127, 126, 115}},
    "COLOR_INPUT"     => {GPCR::Tokens::COLOR_INPUT, {127, 73, 127}},
    "COLOR_PINS"      => {GPCR::Tokens::COLOR_PINS, {81, 121, 127}},
    "COLOR_COMM"      => {GPCR::Tokens::COLOR_COMM, {49, 127, 115}},
    "COLOR_DATA"      => {GPCR::Tokens::COLOR_DATA, {127, 50, 63}},
    "COLOR_FILE"      => {GPCR::Tokens::COLOR_FILE, {74, 77, 127}},
    "COLOR_OPS"       => {GPCR::Tokens::COLOR_OPS, {89, 127, 47}},
    "COLOR_RENDERER"  => {GPCR::Tokens::COLOR_RENDERER, {77, 84, 127}},
    "COLOR_DRAW"      => {GPCR::Tokens::COLOR_DRAW, {63, 127, 121}},
    "COLOR_SPEAK"     => {GPCR::Tokens::COLOR_SPEAK, {20, 103, 127}},
    "COLOR_NODE"      => {GPCR::Tokens::COLOR_NODE, {100, 91, 127}},
    "COLOR_GALLERY"   => {GPCR::Tokens::COLOR_GALLERY, {127, 41, 70}},
    "COLOR_SHADOW"    => {GPCR::Tokens::COLOR_SHADOW, {116, 121, 127}},
  }.each do |name, pair|
    hex, expected = pair
    er, eg, eb = expected

    it "T(#{name} = #{hex}) = rgb(#{er},#{eg},#{eb})" do
      r, g, b = parse_hex(hex)
      tr, tg, tb = pikchr_dark_transform(r, g, b)
      tr.should eq(er), "#{name} dark R: expected #{er} got #{tr}"
      tg.should eq(eg), "#{name} dark G: expected #{eg} got #{tg}"
      tb.should eq(eb), "#{name} dark B: expected #{eb} got #{tb}"
    end
  end

  it "T(COLOR_SLOT = #FFFFFF) = rgb(127, 127, 127) — gray slot in dark mode" do
    r, g, b = parse_hex(GPCR::Tokens::COLOR_SLOT)
    tr, tg, tb = pikchr_dark_transform(r, g, b)
    tr.should eq(127)
    tg.should eq(127)
    tb.should eq(127)
  end

  it "T(COLOR_SLOT_TEXT = #263238) = rgb(118, 124, 127) — cool-gray slot text in dark mode" do
    r, g, b = parse_hex(GPCR::Tokens::COLOR_SLOT_TEXT)
    tr, tg, tb = pikchr_dark_transform(r, g, b)
    tr.should eq(118)
    tg.should eq(124)
    tb.should eq(127)
  end
end