packages feed

moonlight-planar-1.1.0.0: docs/art-common/Moonlight/Planar/Exhibit/IllustrationStudy.hs

-- | Artistic intent lives here; curves, paint and SVG remain library-owned.
module Moonlight.Planar.Exhibit.IllustrationStudy
  ( StudyControls, StudyControl (..), StudyControlError (..), defaultControls
  , editControl, StudyPart (..), partName, partContour, partPort
  , studyPicture, diagnosticPicture
  ) where

import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Sequence as Seq
import Moonlight.Planar.Affine
  ( AffineIso2, affine2, affineIsoMap, translationAffineIso2, identityAffineIso2 )
import Moonlight.Planar.Curve
  ( ClosedTrail, Located, OpenTrail, Subpath (..), ellipse, location, path, transformLocatedClosedTrail )
import Moonlight.Planar.Curve.Authoring
  ( Knot (Interpolating), ProfileStation (..), bowedTrail, cardinalClosed, cardinalOpen
  , polygonTrail, profileOutline )
import Moonlight.Planar.Exact
import Moonlight.Planar.Illustration
import Moonlight.Planar.Illustration.Motif
  ( Motif, motif, motifPicture, motifPort, attachMotif )

-- | Values are physical drawing units, not anonymous control-point indices.
data StudyControls = StudyControls
  { hornSweep :: !ExactRational
  , eyeSpacing :: !ExactRational
  , eyeTilt :: !ExactRational
  , cloakFullness :: !ExactRational
  , fullerWidth :: !ExactRational
  } deriving stock (Eq, Show)

data StudyControl = LeftHornSweep | EyeSpacing | EyeTilt | CloakFullness | FullerWidth
  deriving stock (Eq, Ord, Show, Enum, Bounded)

data StudyControlError = ControlOutsideRange !StudyControl !ExactRational
  deriving stock (Eq, Show)

defaultControls :: StudyControls
defaultControls = StudyControls 0 57 7 0 12

-- | Absolute semantic edits. Bounds keep the authored family intentional.
editControl :: StudyControl -> ExactRational -> StudyControls -> Either StudyControlError StudyControls
editControl control value controls
  | value < low || value > high = Left (ControlOutsideRange control value)
  | otherwise = Right $ case control of
      LeftHornSweep -> controls { hornSweep = value }
      EyeSpacing -> controls { eyeSpacing = value }
      EyeTilt -> controls { eyeTilt = value }
      CloakFullness -> controls { cloakFullness = value }
      FullerWidth -> controls { fullerWidth = value }
 where
  (low, high) = case control of
    LeftHornSweep -> (-45, 45)
    EyeSpacing -> (40, 75)
    EyeTilt -> (-15, 15)
    CloakFullness -> (-25, 55)
    FullerWidth -> (5, 24)

data StudyPart = LeftHorn | RightHorn | Mask | LeftEye | RightEye | Cloak
  | LeftFoot | RightFoot | Blade | Fuller | Guard | Grip
  deriving stock (Eq, Ord, Show, Enum, Bounded)

partName :: StudyPart -> String
partName part = case part of
  LeftHorn -> "knight-left-horn"
  RightHorn -> "knight-right-horn"
  Mask -> "knight-mask"
  LeftEye -> "knight-left-eye"
  RightEye -> "knight-right-eye"
  Cloak -> "knight-cloak"
  LeftFoot -> "knight-left-foot"
  RightFoot -> "knight-right-foot"
  Blade -> "crescent-blade"
  Fuller -> "crescent-fuller"
  Guard -> "crescent-guard"
  Grip -> "crescent-grip"

-- | Geometry and its semantic root are authored together. Roots need not be
-- the first boundary point (notably horns and ellipses).
data PartGeometry
  = WorldContour !(Located ClosedTrail)
  | AttachedContour !(Located ClosedTrail) !ExactVector

data RootPort = RootPort

partPort :: StudyControls -> StudyPart -> AffineIso2
partPort controls part = motifPort (partMotif controls part (Solid ink)) RootPort

partMotif :: StudyControls -> StudyPart -> Paint -> Motif RootPort StudyPart
partMotif controls part paint = case partGeometry controls part of
  WorldContour contour -> motif
    (annotate part (fill NonZero paint (contour :| []))) (const (pointFrame (location contour)))
  AttachedContour contour rootOffset -> attachMotif RootPort (translationAffineIso2 rootOffset) $
    motif (annotate part (fill NonZero (localPaint rootOffset paint) (contour :| [])))
      (const identityAffineIso2)

partContour :: StudyControls -> StudyPart -> Located ClosedTrail
partContour controls part = case partGeometry controls part of
  WorldContour contour -> contour
  AttachedContour contour rootOffset ->
    transformLocatedClosedTrail (affineIsoMap (translationAffineIso2 rootOffset)) contour

partGeometry :: StudyControls -> StudyPart -> PartGeometry
partGeometry controls part = case part of
  LeftHorn -> AttachedContour (horn (hornSweep controls)) (ExactVector 336 359)
  RightHorn -> AttachedContour
    (transformLocatedClosedTrail
      (affine2 (ExactVector (-1) 0) (ExactVector 0 1) (ExactVector 0 0)) (horn 0))
    (ExactVector 484 359)
  LeftEye -> eyeGeometry (-1) controls
  RightEye -> eyeGeometry 1 controls

  Mask -> boundary $ cardinalClosed unitZero $ fmap (Interpolating . uncurry exactPoint)
    ((298,344) :| [(410,326),(522,344),(535,409),(507,485),(473,519)
      ,(410,529),(347,519),(313,485),(285,409)])
  Cloak -> boundary $ let f = cloakFullness controls in cardinalClosed unitHalf $
    fmap (Interpolating . uncurry exactPoint)
      ((359,499) :| [(320-f*exactHalf,612),(257-f,759),(288-f,775),(317-f,804)
        ,(350,798),(385,817),(420,805),(454,811),(488,795),(518+f,778)
        ,(560+f,751),(507+f*exactHalf,612),(461,499),(410,515)])
  LeftFoot -> boundary $ oval (357,825) (37,15) (-5)
  RightFoot -> boundary $ oval (464,825) (38,15) 5
  Blade -> boundary $ profileOutline unitHalf $
    ProfileStation (exactPoint 844 735) (ExactVector (-32) 0) :|
    [ ProfileStation (exactPoint 848 580) (ExactVector (-38) 0)
    , ProfileStation (exactPoint 874 420) (ExactVector (-32) 0)
    , ProfileStation (exactPoint 914 280) (ExactVector (-17) 0)
    , ProfileStation (exactPoint 958 194) (ExactVector 0 0) ]
  Fuller -> boundary $ let w=fullerWidth controls in profileOutline unitHalf $
    ProfileStation (exactPoint 894 350) (ExactVector 0 0) :|
    [ ProfileStation (exactPoint 871 440) (ExactVector (-w) 0)
    , ProfileStation (exactPoint 850 550) (ExactVector (-w*exactHalf) 0)
    , ProfileStation (exactPoint 843 630) (ExactVector 0 0) ]
  Guard -> boundary $ profileOutline unitZero $
    ProfileStation (exactPoint 743 736) (ExactVector 0 0) :|
    [ ProfileStation (exactPoint 783 760) (ExactVector 0 (-10))
    , ProfileStation (exactPoint 815 747) (ExactVector 0 (-10))
    , ProfileStation (exactPoint 843 735) (ExactVector 0 (-15))
    , ProfileStation (exactPoint 871 747) (ExactVector 0 (-10))
    , ProfileStation (exactPoint 903 760) (ExactVector 0 (-10))
    , ProfileStation (exactPoint 943 736) (ExactVector 0 0) ]
  Grip -> boundary $ profileOutline unitHalf $
    ProfileStation (exactPoint 843 753) (ExactVector (-14) 0) :|
    [ ProfileStation (exactPoint 843 795) (ExactVector (-11) 0)
    , ProfileStation (exactPoint 843 853) (ExactVector (-15) 0) ]

 where
  boundary :: Located ClosedTrail -> PartGeometry
  boundary = WorldContour

pointFrame :: ExactPoint -> AffineIso2
pointFrame point = let (x,y) = exactPointCoordinates point
                   in translationAffineIso2 (ExactVector x y)

-- World-space paints must be pulled back by the attachment translation.
-- All attached roots in this author are translations, so radii stay unchanged;
-- this helper does not pretend to support arbitrary affine radial gradients.
localPaint :: ExactVector -> Paint -> Paint
localPaint (ExactVector x y) paint = case paint of
  Solid color -> Solid color
  LinearGradient start end stops -> LinearGradient (local start) (local end) stops
  RadialGradient center radius stops -> RadialGradient (local center) radius stops
 where
  local :: ExactPoint -> ExactPoint
  local point = translateExactPoint point (ExactVector (negate x) (negate y))

eyeGeometry :: ExactRational -> StudyControls -> PartGeometry
eyeGeometry side controls =
  let center = exactPoint (410 + side * eyeSpacing controls) 420
      (x,y) = exactPointCoordinates center
      local = ellipse (ExactVector 27 (negate side * eyeTilt controls)) (ExactVector 0 43)
   in AttachedContour local (ExactVector x y)

type XY = (ExactRational, ExactRational)

oval :: XY -> XY -> ExactRational -> Located ClosedTrail
oval (x,y) (rx,ry) shear = transformLocatedClosedTrail
  (affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector x y))
  (ellipse (ExactVector rx shear) (ExactVector 0 ry))

rectangle :: XY -> XY -> Located ClosedTrail
rectangle (x,y) (w,h) = polygonTrail $
  fmap (uncurry exactPoint) ((x,y) :| [(x+w,y),(x+w,y+h),(x,y+h)])

-- | One tapering horn profile; its mirror is the same authored shape. Sweep
-- changes the upper spine, not a collection of unrelated tangent handles.
horn :: ExactRational -> Located ClosedTrail
horn sweep = profileOutline unitZero $
  ProfileStation (exactPoint 0 0) (ExactVector (-18) 10) :|
  [ ProfileStation (exactPoint (-34) (-78)) (ExactVector (-14) 0)
  , ProfileStation (exactPoint (-55+sweep*exactHalf) (-146)) (ExactVector (-6) 0)
  , ProfileStation (exactPoint (-42+sweep) (-213)) (ExactVector 0 0) ]


ink, ivory, gold, blue :: Color
ink = RGB 11 23 36
ivory = RGB 227 242 236
gold = RGB 194 162 108
blue = RGB 95 169 194

linear :: XY -> XY -> Color -> Color -> Color -> Paint
linear a b lo mid hi = LinearGradient (uncurry exactPoint a) (uncurry exactPoint b)
  (gradientStops (GradientStop unitZero lo unitOne :|
    [GradientStop unitHalf mid unitOne, GradientStop unitOne hi unitOne]))

studyPicture :: StudyControls -> Picture StudyPart
studyPicture c = backdrop <> knight <> sword
 where
  shape :: StudyPart -> Paint -> Picture StudyPart
  shape part paint = motifPicture (partMotif c part paint)
  knight :: Picture StudyPart
  knight = shape LeftFoot (Solid ink) <> shape RightFoot (Solid ink)
    <> shape Cloak (linear (270,500) (540,810) (RGB 48 69 83) (RGB 24 43 57) (RGB 10 23 34))
    <> cloakFolds c
    <> shape LeftHorn porcelain <> shape RightHorn porcelain
    <> shape Mask porcelain <> maskRim c
    <> shape LeftEye (Solid ink) <> shape RightEye (Solid ink)
    <> eyeGlints c
  porcelain :: Paint
  porcelain = linear (300,330) (520,500) (RGB 166 196 198) ivory (RGB 199 221 217)
  bladeContours :: NonEmpty (Located ClosedTrail)
  bladeContours = partContour c Blade :| [partContour c Fuller]
  sword :: Picture StudyPart
  sword = shape Grip (linear (827,750) (859,855) ink (RGB 62 81 86) ink)
    <> gripBindings
    <> annotate Blade (fill EvenOdd steel bladeContours)
    <> annotate Blade (clip EvenOdd bladeContours (reflection <> bladeFacets))
    <> annotate Blade (outline blue (partContour c Blade))
    <> annotate Fuller (outline (RGB 161 208 211) (partContour c Fuller))
    <> shape Guard (linear (740,725) (938,778) (RGB 92 82 68) (RGB 229 211 162) gold)
    <> outline gold (partContour c Guard)
    <> fill NonZero (Solid gold) (oval (843,862) (17,12) 0 :| [])
    <> fill NonZero (linear (834,848) (850,870) blue ivory ink) (oval (843,859) (7,8) 0 :| [])
  steel :: Paint
  steel = linear (790,390) (912,590) (RGB 47 90 113) (RGB 178 220 223) (RGB 81 137 156)
  reflection :: Picture StudyPart
  reflection = opacity unitHalf $ fill NonZero
    (linear (760,420) (925,590) (RGB 116 196 214) ivory (RGB 95 157 180))
    (polygonTrail (fmap (uncurry exactPoint) ((760,530) :| [(928,413),(928,477),(760,594)])) :| [])
  bladeFacets :: Picture StudyPart
  bladeFacets = opacity unitHalf $ fill NonZero (Solid (RGB 226 247 240))
    (polygonTrail (fmap (uncurry exactPoint) ((838,727) :| [(860,510),(958,194),(883,480)])) :| [])

backdrop :: Picture StudyPart
backdrop = fill NonZero (linear (0,0) (1200,1000) (RGB 9 20 31) (RGB 19 38 53) (RGB 6 17 28))
  (rectangle (0,0) (1200,1000) :| [])
  <> opacity unitHalf (fill NonZero (Solid (RGB 42 65 78)) (oval (815,335) (246,246) 0 :| []))
  <> outline (RGB 65 87 96) (oval (815,335) (263,263) 0)
  <> fill NonZero (Solid (RGB 8 20 29)) (oval (608,873) (403,27) 0 :| [])
  <> foldMap star [(156,220),(210,387),(658,173),(1026,584),(690,706),(1060,230)]
  <> outline (RGB 64 88 98) (rectangle (54,54) (1092,892))
 where
  star :: XY -> Picture StudyPart
  star (x,y) = place (affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector x y)) $
    fill NonZero (Solid (RGB 134 165 176))
      (polygonTrail (fmap (uncurry exactPoint)
        ((0,-5) :| [(2,-1),(6,0),(2,1),(0,5),(-2,1),(-6,0),(-2,-1)])) :| [])

outline :: Color -> Located ClosedTrail -> Picture part
outline color closed = stroke (pen color) (path (Seq.singleton (ClosedSubpath closed)))

pen :: Color -> StrokeStyle
pen color = StrokeStyle (Solid color) positiveOne LocalUnits RoundCap RoundJoin

-- | Decorative strokes are bowed chords or interpolated landmarks.
inkLine :: Color -> Located OpenTrail -> Picture part
inkLine color trail = stroke (pen color) (path (Seq.singleton (OpenSubpath trail)))

cloakFolds :: StudyControls -> Picture StudyPart
cloakFolds c = annotate Cloak $ clip NonZero (partContour c Cloak :| []) $
  foldMap foldLine [(-2),(-1),0,1,2]
 where
  foldLine :: ExactRational -> Picture StudyPart
  foldLine axis = inkLine (RGB 64 84 96) $
    bowedTrail (axis*exactThird*exactThird*exactHalf)
      (exactPoint (410+axis*19) 526)
      (exactPoint (410+axis*(51+cloakFullness c*exactHalf)) 813)

maskRim :: StudyControls -> Picture StudyPart
maskRim c = annotate Mask $ outline (RGB 145 178 182) (partContour c Mask)
  <> inkLine (RGB 244 252 242) (cardinalOpen unitHalf $
    fmap (Interpolating . uncurry exactPoint) ((302,369) :| [(294,419),(320,480),(353,504)]))

eyeGlints :: StudyControls -> Picture StudyPart
eyeGlints c = foldMap glint [LeftEye,RightEye]
 where
  glint :: StudyPart -> Picture StudyPart
  glint part = annotate part $ place (affineIsoMap (partPort c part)) $
    opacity unitHalf $ fill NonZero (Solid (RGB 89 124 139))
      (oval (-10,-17) (4,9) 0 :| [])

gripBindings :: Picture StudyPart
gripBindings = annotate Grip $ foldMap binding [0..6 :: Int]
 where
  binding :: Int -> Picture StudyPart
  binding i = place
    (affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector 0 (770 + fromIntegral i*11)))
    (inkLine gold (bowedTrail (exactThird*exactHalf) (exactPoint 831 0) (exactPoint 855 0)))

-- | Visible landmarks are annotation, never geometric identity or mesh indices.
diagnosticPicture :: StudyControls -> Picture StudyPart
diagnosticPicture controls = studyPicture controls <> foldMap landmark [minBound..maxBound]
 where
  landmark :: StudyPart -> Picture StudyPart
  landmark part = annotate part $ outline (RGB 247 133 104)
    (transformLocatedClosedTrail
      (affineIsoMap (partPort controls part))
      (ellipse (ExactVector 5 0) (ExactVector 0 5)))