moonlight-planar-1.1.0.0: docs/art-common/Moonlight/Planar/Exhibit/SpaceSword.hs
-- | A standalone orbital sword authored from profiles, typed attachment frames,
-- repeated inscriptions and one affine pose. No hand-traced Bezier handles.
module Moonlight.Planar.Exhibit.SpaceSword
( SpaceSwordControls (..)
, defaultSpaceSwordControls
, overchargedSpaceSwordControls
, SpaceSwordPart (..)
, spaceSwordPartName
, spaceSwordPicture
) where
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Sequence as Seq
import Moonlight.Planar.Affine
( Affine2, affine2, identityAffineIso2, translationAffineIso2 )
import Moonlight.Planar.Curve
( ClosedTrail, Located, OpenTrail, Subpath (..), circle, path )
import Moonlight.Planar.Curve.Authoring
( Knot (Interpolating), ProfileStation (..), bowedTrail, cardinalOpen
, polygonTrail, profileOutline )
import Moonlight.Planar.Exact
( ExactPoint, ExactRational, ExactVector (..), PositiveExact, UnitInterval
, exactHalf, exactPoint, exactPointCoordinates, exactThird, positiveOne, positiveTwo
, unitHalf, unitOne, unitZero, unitIntervalValue )
import Moonlight.Planar.Illustration
( Color (..), FillRule (..), GradientStop (..), LineCap (..), LineJoin (..)
, Paint (..), Picture, StrokeStyle (..), StrokeUnits (..), annotate, clip
, fill, gradientStops, opacity, place, stroke )
import Moonlight.Planar.Illustration.Motif
( Motif, attachMotif, motif, motifPicture, motifPort )
-- | Charge widens the recessed energy channel and lengthens its trailing wake.
-- Sweep bends the cutting edge without disturbing the hilt assembly.
data SpaceSwordControls = SpaceSwordControls
{ bladeCharge :: !UnitInterval
, bladeSweep :: !ExactRational
} deriving stock (Eq, Show)
defaultSpaceSwordControls :: SpaceSwordControls
defaultSpaceSwordControls = SpaceSwordControls unitHalf 28
overchargedSpaceSwordControls :: SpaceSwordControls
overchargedSpaceSwordControls = SpaceSwordControls unitOne 40
data SpaceSwordPart
= VoidField
| NebulaVeil
| BladeAura
| BladeShell
| BladeCore
| BladeRunes
| Crossguard
| ReactorGrip
| Counterweight
| IonWake
deriving stock (Eq, Ord, Show, Enum, Bounded)
spaceSwordPartName :: SpaceSwordPart -> String
spaceSwordPartName part = case part of
VoidField -> "void-field"
NebulaVeil -> "nebula-veil"
BladeAura -> "blade-aura"
BladeShell -> "blade-shell"
BladeCore -> "blade-core"
BladeRunes -> "blade-runes"
Crossguard -> "crossguard"
ReactorGrip -> "reactor-grip"
Counterweight -> "counterweight"
IonWake -> "ion-wake"
data BladePort = BladeRoot | BladeTip
data RunePort = RuneRoot
data HiltPort = BladeSocket
spaceSwordPicture :: SpaceSwordControls -> Picture SpaceSwordPart
spaceSwordPicture controls =
annotate VoidField voidField
<> annotate NebulaVeil nebulaVeil
<> swordAssembly controls
-- | Blade and hilt are attached in their authoring frame, then the complete
-- assembly receives one affine action. Geometry, clips, paint and ornament
-- therefore cannot drift apart when the weapon is posed.
swordAssembly :: SpaceSwordControls -> Picture SpaceSwordPart
swordAssembly controls =
let hilt = hiltMotif
blade = attachMotif BladeRoot (motifPort hilt BladeSocket) (bladeMotif controls)
assembled = annotate IonWake (ionWake controls)
<> motifPicture blade
<> motifPicture hilt
in place swordPose assembled
-- | A similarity pose: orthogonal columns of equal squared length. The socket
-- at @(604,745)@ lands at @(350,710)@, sending the blade toward the upper right.
swordPose :: Affine2
swordPose = affine2
(ExactVector threeQuarters 1)
(ExactVector (-1) threeQuarters)
(ExactVector 642 (negate (452 + threeQuarters)))
where
threeQuarters = exactHalf + exactHalf * exactHalf
bladeMotif :: SpaceSwordControls -> Motif BladePort SpaceSwordPart
bladeMotif controls = motif picture port
where
charge = unitIntervalValue (bladeCharge controls)
sweep = bladeSweep controls
aura = bladeProfile (42 + 6 * charge) sweep
shell = bladeProfile 34 sweep
channel = channelProfile charge sweep
picture = annotate BladeAura
(opacity unitHalf (fill NonZero (Solid (RGB 27 118 151)) (aura :| [])))
<> annotate BladeShell
(fill NonZero shellPaint (shell :| [])
<> clip NonZero (shell :| [])
(bladeSpineFacet sweep <> cuttingBevel sweep <> bladeReflection sweep)
<> outline (RGB 118 154 166) shell)
<> annotate BladeCore
(fill NonZero corePaint (channel :| [])
<> outline (RGB 128 232 215) channel)
<> annotate BladeRunes (runeColumn controls)
port selected = case selected of
BladeRoot -> identityAffineIso2
BladeTip -> translationAffineIso2 (ExactVector sweep (-625))
-- | The lower spine remains nearly straight while the cutting edge carries the
-- changing width and sweep. The silhouette is still one editable profile.
bladeProfile :: ExactRational -> ExactRational -> Located ClosedTrail
bladeProfile width sweep = profileOutline unitHalf $
ProfileStation (exactPoint 0 0) (ExactVector width 0) :|
[ ProfileStation (exactPoint (width * exactThird) (-150))
(ExactVector (width + width * exactThird) 0)
, ProfileStation (exactPoint (width * exactThird + sweep * exactThird) (-345))
(ExactVector (width + 5) 0)
, ProfileStation (exactPoint (sweep - width * exactThird) (-530))
(ExactVector (width * exactHalf + 3) 0)
, ProfileStation (exactPoint sweep (-625)) (ExactVector 0 0)
]
channelProfile :: ExactRational -> ExactRational -> Located ClosedTrail
channelProfile charge sweep = profileOutline unitHalf $
ProfileStation (exactPoint 8 (-28)) (ExactVector (4 + 2 * charge) 0) :|
[ ProfileStation (exactPoint 12 (-145)) (ExactVector (5 + 2 * charge) 0)
, ProfileStation (exactPoint 19 (-315)) (ExactVector (6 + 2 * charge) 0)
, ProfileStation (exactPoint (sweep - 7) (-490)) (ExactVector (4 + charge) 0)
, ProfileStation (exactPoint sweep (-579)) (ExactVector 0 0)
]
bladeSpineFacet :: ExactRational -> Picture SpaceSwordPart
bladeSpineFacet sweep = fill NonZero (Solid (RGB 8 18 30)) $
polygonTrail (exactPoint (-34) 0 :|
[ exactPoint (-34) (-148)
, exactPoint (-19) (-345)
, exactPoint (sweep - 22) (-528)
, exactPoint (sweep - 5) (-602)
, exactPoint 2 (-340)
, exactPoint (-5) (-80)
]) :| []
cuttingBevel :: ExactRational -> Picture SpaceSwordPart
cuttingBevel sweep = fill NonZero (Solid (RGB 148 182 190)) $
polygonTrail (exactPoint 34 0 :|
[ exactPoint 57 (-150)
, exactPoint 63 (-345)
, exactPoint (sweep + 20) (-530)
, exactPoint sweep (-625)
, exactPoint (sweep + 8) (-520)
, exactPoint 43 (-320)
, exactPoint 28 (-55)
]) :| []
bladeReflection :: ExactRational -> Picture SpaceSwordPart
bladeReflection sweep = opacity unitHalf $ fill NonZero (Solid (RGB 220 239 224)) $
polygonTrail (exactPoint 27 (-302) :|
[ exactPoint 42 (-336)
, exactPoint (sweep + 13) (-485)
, exactPoint (sweep + 6) (-522)
, exactPoint 31 (-386)
]) :| []
runeColumn :: SpaceSwordControls -> Picture SpaceSwordPart
runeColumn controls = foldMap placeRune runeStations
where
charge = unitIntervalValue (bladeCharge controls)
sweep = bladeSweep controls
rune = runeMotif charge
placeRune :: (ExactRational, ExactRational) -> Picture SpaceSwordPart
placeRune (x,y) = motifPicture $
attachMotif RuneRoot (translationAffineIso2 (ExactVector x y)) rune
runeStations :: [(ExactRational, ExactRational)]
runeStations = [(12,-136),(21,-322),(sweep-6,-474)]
runeMotif :: ExactRational -> Motif RunePort SpaceSwordPart
runeMotif charge = motif picture (const identityAffineIso2)
where
size = 5 + 2 * charge
rune = polygonTrail (exactPoint 0 (negate size) :|
[ exactPoint (size * exactHalf) 0
, exactPoint 0 size
, exactPoint (negate (size * exactHalf)) 0
])
picture :: Picture SpaceSwordPart
picture = opacity unitHalf (fill NonZero (Solid (RGB 108 221 202)) (rune :| []))
<> outline (RGB 174 236 213) rune
ionWake :: SpaceSwordControls -> Picture SpaceSwordPart
ionWake controls = opacity unitHalf $
ink positiveTwo (RGB 39 146 184) outer
<> ink positiveOne (RGB 105 210 202) inner
where
charge = unitIntervalValue (bladeCharge controls)
sweep = bladeSweep controls
outer = cardinalOpen unitHalf (Interpolating (exactPoint 50 (-18)) :|
(Interpolating <$>
[ exactPoint (78 + 18 * charge) (-155)
, exactPoint (72 + 27 * charge) (-355)
, exactPoint (sweep + 38 + 36 * charge) (-574)
]))
inner = bowedTrail (exactThird + charge * exactThird)
(exactPoint 46 (-42)) (exactPoint (sweep + 22) (-545))
hiltMotif :: Motif HiltPort SpaceSwordPart
hiltMotif = motif picture port
where
picture = annotate Crossguard crossguard
<> annotate ReactorGrip reactorGrip
<> annotate Counterweight counterweight
port BladeSocket = translationAffineIso2 (ExactVector 604 745)
crossguard :: Picture SpaceSwordPart
crossguard =
fill NonZero guardPaint (guardContour :| [])
<> fill NonZero (Solid (RGB 166 135 74)) (guardInlay :| [])
<> outline (RGB 177 151 91) guardContour
<> fill NonZero (Solid (RGB 12 29 39)) (socketContour :| [])
<> outline (RGB 91 143 148) socketContour
guardContour :: Located ClosedTrail
guardContour = polygonTrail (exactPoint 495 750 :|
[ exactPoint 552 730
, exactPoint 591 735
, exactPoint 604 721
, exactPoint 618 735
, exactPoint 663 728
, exactPoint 718 746
, exactPoint 658 765
, exactPoint 618 757
, exactPoint 604 770
, exactPoint 590 758
, exactPoint 550 766
])
guardInlay :: Located ClosedTrail
guardInlay = polygonTrail (exactPoint 512 750 :|
[ exactPoint 556 738
, exactPoint 590 744
, exactPoint 590 750
, exactPoint 552 756
])
socketContour :: Located ClosedTrail
socketContour = polygonTrail (exactPoint 584 733 :|
[ exactPoint 624 733
, exactPoint 632 758
, exactPoint 619 774
, exactPoint 589 774
, exactPoint 576 758
])
reactorGrip :: Picture SpaceSwordPart
reactorGrip =
fill NonZero gripPaint (gripContour :| [])
<> clip NonZero (gripContour :| []) (foldMap gripBand [0..4 :: Int])
<> outline (RGB 73 105 113) gripContour
<> gripAxis
gripContour :: Located ClosedTrail
gripContour = profileOutline unitHalf $
ProfileStation (exactPoint 604 766) (ExactVector 19 0) :|
[ ProfileStation (exactPoint 604 815) (ExactVector 17 0)
, ProfileStation (exactPoint 604 884) (ExactVector 20 0)
, ProfileStation (exactPoint 604 922) (ExactVector 15 0)
]
gripBand :: Int -> Picture SpaceSwordPart
gripBand index = opacity unitHalf $ fill NonZero (Solid (RGB 148 124 78)) $
polygonTrail (exactPoint 582 y :|
[ exactPoint 626 (y + 12)
, exactPoint 626 (y + 19)
, exactPoint 582 (y + 7)
]) :| []
where
y = 780 + fromIntegral index * 27
gripAxis :: Picture SpaceSwordPart
gripAxis = opacity unitHalf $ ink positiveOne (RGB 81 159 158) $
cardinalOpen unitHalf (Interpolating (exactPoint 604 772) :|
(Interpolating <$> [exactPoint 600 822, exactPoint 607 875, exactPoint 604 916]))
counterweight :: Picture SpaceSwordPart
counterweight =
fill NonZero guardPaint (counterweightContour :| [])
<> outline (RGB 147 132 91) counterweightContour
<> fill NonZero (Solid (RGB 91 198 184)) (emitterSlit :| [])
where
counterweightContour = polygonTrail (exactPoint 604 913 :|
[ exactPoint 622 929
, exactPoint 619 954
, exactPoint 604 972
, exactPoint 589 954
, exactPoint 586 929
])
emitterSlit = polygonTrail (exactPoint 599 939 :|
[exactPoint 609 939, exactPoint 611 949, exactPoint 604 956, exactPoint 597 949])
voidField :: Picture SpaceSwordPart
voidField = fill NonZero voidPaint
(polygonTrail (exactPoint 0 0 :|
[exactPoint 1200 0,exactPoint 1200 1000,exactPoint 0 1000]) :| [])
<> foldMap star stars
<> foldMap brightStar brightStars
where
star :: (ExactRational, ExactRational, ExactRational) -> Picture SpaceSwordPart
star (x,y,size) = place (uniformAt size (exactPoint x y)) $
opacity unitHalf (fill NonZero (Solid (RGB 139 166 184)) (circle positiveOne :| []))
brightStar :: (ExactRational, ExactRational, ExactRational) -> Picture SpaceSwordPart
brightStar (x,y,size) = place (uniformAt size (exactPoint x y)) $
opacity unitHalf (fill NonZero (Solid (RGB 173 205 207)) (spark :| []))
stars :: [(ExactRational, ExactRational, ExactRational)]
stars = [(82,132,1),(188,310,1),(298,91,2),(482,178,1),(699,85,1)
,(884,152,1),(1094,88,1),(1057,416,2),(946,817,1),(1111,912,1)]
brightStars :: [(ExactRational, ExactRational, ExactRational)]
brightStars = [(229,655,2),(741,798,2),(1010,598,1)]
nebulaVeil :: Picture SpaceSwordPart
nebulaVeil = opacity unitHalf $
fill NonZero nebulaPaint (nebulaContour :| [])
where
nebulaContour = profileOutline unitHalf $
ProfileStation (exactPoint (-120) 940) (ExactVector 76 108) :|
[ ProfileStation (exactPoint 236 755) (ExactVector 66 101)
, ProfileStation (exactPoint 596 506) (ExactVector 54 88)
, ProfileStation (exactPoint 951 263) (ExactVector 42 67)
, ProfileStation (exactPoint 1280 72) (ExactVector 24 36)
]
spark :: Located ClosedTrail
spark = polygonTrail (exactPoint 0 (-3) :|
[exactPoint 1 (-1),exactPoint 4 0,exactPoint 1 1,exactPoint 0 4
,exactPoint (-1) 1,exactPoint (-4) 0,exactPoint (-1) (-1)])
voidPaint, nebulaPaint, shellPaint, corePaint, guardPaint, gripPaint :: Paint
voidPaint = linear (exactPoint 0 0) (exactPoint 1200 1000)
(RGB 2 5 13) (RGB 12 16 37) (RGB 4 9 22)
nebulaPaint = linear (exactPoint 0 1000) (exactPoint 1200 0)
(RGB 39 17 57) (RGB 25 72 86) (RGB 8 20 43)
shellPaint = linear (exactPoint (-40) 0) (exactPoint 70 (-625))
(RGB 13 27 43) (RGB 104 126 137) (RGB 15 49 68)
corePaint = linear (exactPoint 8 (-28)) (exactPoint 40 (-579))
(RGB 26 78 89) (RGB 120 229 204) (RGB 31 113 139)
guardPaint = linear (exactPoint 495 730) (exactPoint 718 766)
(RGB 18 28 39) (RGB 112 99 72) (RGB 20 42 53)
gripPaint = linear (exactPoint 584 766) (exactPoint 624 922)
(RGB 7 14 24) (RGB 40 51 54) (RGB 8 18 27)
linear :: ExactPoint -> ExactPoint -> Color -> Color -> Color -> Paint
linear a b low middle high = LinearGradient a b $ gradientStops
(GradientStop unitZero low unitOne :|
[GradientStop unitHalf middle unitOne, GradientStop unitOne high unitOne])
outline :: Color -> Located ClosedTrail -> Picture part
outline color curve = stroke
(StrokeStyle (Solid color) positiveOne OutputUnits RoundCap RoundJoin)
(path (Seq.singleton (ClosedSubpath curve)))
ink :: PositiveExact -> Color -> Located OpenTrail -> Picture part
ink width color curve = stroke
(StrokeStyle (Solid color) width OutputUnits RoundCap RoundJoin)
(path (Seq.singleton (OpenSubpath curve)))
uniformAt :: ExactRational -> ExactPoint -> Affine2
uniformAt scale center = affine2
(ExactVector scale 0) (ExactVector 0 scale)
(uncurry ExactVector (exactPointCoordinates center))