diff --git a/assets/img/jentry.png b/assets/img/jentry.png new file mode 100644 index 0000000..a00c8ae Binary files /dev/null and b/assets/img/jentry.png differ diff --git a/elm.json b/elm.json index d086743..8a0d238 100644 --- a/elm.json +++ b/elm.json @@ -3,6 +3,7 @@ "direct": { "andrewMacmurray/elm-simple-animation": "2.3.2", "avh4/elm-color": "1.0.0", + "elm-community/list-extra": "8.7.0", "elm-explorations/webgl": "1.1.3", "elm/browser": "1.0.2", "elm/core": "1.0.5", diff --git a/src/Main.elm b/src/Main.elm index dfe6d5f..4a3ab79 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -20,6 +20,8 @@ import Html.Attributes import Http import Json.Decode as Decode import Length +import List exposing (length) +import List.Extra exposing (getAt) import Obj.Decode import Pixels import Point3d exposing (Point3d) @@ -76,7 +78,20 @@ main = type alias Model = - { w : Int, h : Int, last : String, url : Url.Url, key : Nav.Key, mesh : Maybe Object3d, textures : Maybe (Material.Textured Obj.Decode.ObjCoordinates), angle : Float, radius : Float, elevation : Float } + { w : Int + , h : Int + , last : String + , url : Url.Url + , key : Nav.Key + , mesh : Maybe Object3d + , textures : Maybe (Material.Textured Obj.Decode.ObjCoordinates) + , angle : Float + , radius : Float + , elevation : Float + , mainColor : Color.Color + , show : Bool + , entry : String + } type alias Flags = @@ -89,7 +104,22 @@ init flags url key = ( width, height ) = flags in - ( { w = width, h = height, last = "", url = url, key = key, mesh = Nothing, textures = Nothing, angle = 0, radius = 7.5, elevation = 5 }, Cmd.batch [ getMesh, getTexture ] ) + ( { w = width + , h = height + , last = "" + , url = url + , key = key + , mesh = Nothing + , textures = Nothing + , angle = 0 + , radius = 7.5 + , elevation = 5 + , mainColor = Color.rgb 173 111 101 + , show = False + , entry = "zero" + } + , Cmd.batch [ getMesh, getTexture ] + ) type alias Object3d = @@ -106,6 +136,7 @@ type Msg | Copy String String | Key String | Scroll String + | ScrollToEntry String modulo : Float -> Float -> Float @@ -118,6 +149,67 @@ canonicalize angle = modulo 360 angle +frac : Float -> Float +frac x = + x - toFloat (floor x) + + +mainColors : List ( Float, Float, Float ) +mainColors = + [ ( 173, 111, 101 ), ( 200, 0, 100 ), ( 100, 0, 200 ), ( 100, 200, 0 ), ( 200, 100, 0 ), ( 173, 111, 101 ) ] + + +blend : Float -> ( Float, Float, Float ) -> ( Float, Float, Float ) -> Color.Color +blend p ( r1, g1, b1 ) ( r2, g2, b2 ) = + let + blendFlux : Float -> Float -> Float -> Int + blendFlux t a b = + round (sqrt ((1 - t) * (a ^ 2) + t * (b ^ 2))) + in + Color.rgb255 (blendFlux p r1 r2) (blendFlux p g1 g2) (blendFlux p b1 b2) + + +blender : Float -> Color.Color +blender t = + let + n : Int + n = + length mainColors + + p : Float + p = + frac (t * toFloat n) + + c1 : Maybe ( Float, Float, Float ) + c1 = + getAt (floor (t * toFloat n)) mainColors + + c2 : Maybe ( Float, Float, Float ) + c2 = + getAt (ceiling (t * toFloat n)) mainColors + in + case c2 of + Nothing -> + case c1 of + Nothing -> + -- no colors to blend + Color.rgb255 173 111 101 + + Just ( r, g, b ) -> + -- missing color to blend + Color.rgb255 (round r) (round g) (round b) + + Just color2 -> + case c1 of + Nothing -> + -- missing main color to blend + Color.rgb255 173 111 101 + + Just color1 -> + -- blend both colors + blend p color1 color2 + + update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = let @@ -170,6 +262,8 @@ update msg model = | angle = canonicalize (model.angle + 2 * (2 + sin (toFloat (Time.posixToMillis time) / 1000))) , radius = 7.5 + 1 / 8 * model.radius + 5 * 13 / 11 * sin (toFloat (Time.posixToMillis time + 250) / 1000) , elevation = 9 + 1 / 4 * model.elevation + 5 * cos (toFloat (Time.posixToMillis time) / 1000) + , mainColor = blender (frac (toFloat (Time.posixToMillis time) / 3500)) + , show = modBy 4000 (Time.posixToMillis time) > 2000 } Copy label text -> @@ -181,6 +275,9 @@ update msg model = Scroll loc -> ( model, scrollTo loc ) + ScrollToEntry entry -> + ( { model | entry = entry }, scrollTo entry ) + keyDecoder : Decode.Decoder String keyDecoder = @@ -234,6 +331,22 @@ linkBtn disp addr = newTabLink btnStyle { url = addr, label = text (String.toUpper disp) } +linkBtnInt : String -> String -> Element msg +linkBtnInt disp addr = + link btnStyle { url = addr, label = text (String.toUpper disp) } + + +entryLink : String -> String -> Element Msg +entryLink disp addr = + button + [ Font.underline + , Font.bold + ] + { onPress = Just (ScrollToEntry addr) + , label = text disp + } + + btn : String -> Msg -> Element Msg btn disp act = button btnStyle { onPress = Just act, label = text (String.toUpper disp) } @@ -370,7 +483,10 @@ itemize multikeys entry = sitemap : Dict String (Model -> List (Html Msg)) sitemap = - Dict.fromList (itemize ([ "/", "/index.html" ] ++ getPaths "src" ++ getPaths "index" ++ getPaths "home") pageHome) + Dict.fromList + (itemize ([ "/", "/index.html" ] ++ getPaths "src" ++ getPaths "index" ++ getPaths "home") pageHome + ++ itemize (getPaths "entries") pageEntries + ) loadUrl : Model -> List (Html Msg) @@ -608,6 +724,407 @@ pageHome model = ] +pageEntries : Model -> List (Html Msg) +pageEntries model = + [ Element.layout + [ width fill + ] + (column [ width fill ] + [ column + [ width fill + , height fill + , spacing (vh2pt model -100) + , id "zero" + ] + [ el + [ width fill + , height (vh2px model 100) + , Background.color black + ] + Element.none + , animatedEl crossfadeIn + [ width fill + , height (vh2px model 100) + , Background.gradient { angle = 360 - 45, steps = [ rgb255 100 125 50, rgb255 125 50 100 ] } + ] + Element.none + , animatedEl crossfadeOut + [ width fill + , height (vh2px model 100) + , Background.gradient { angle = 360 - 45, steps = [ rgb255 100 50 175, rgb255 100 175 50 ] } + ] + Element.none + , el + ([ alignLeft + , alignTop + , width (vw2px model 50) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 20 - 96 + , bottom = 0 + , left = vw2pt model 10 + , right = 0 + } + ] + ++ heading + ) + (column + [ spacing 35 + ] + [ menu model + , text "Meet the entries" + , paragraph bodyText + [ entryLink "A entry" "A" + , text ", " + , entryLink "Bentry" "B" + , text " (Office of the President), " + , entryLink "Centry" "C" + , text " (the C is silent), " + , entryLink "Dentry" "D" + , text ", " + , entryLink "E entry" "E" + , text ", " + , entryLink "Fentry" "F" + , text ", " + , entryLink "Gentry" "G" + , text " (hard G), " + , entryLink "Hentry" "H" + , text ", 𝑖 entry (imaginary)," + , image [ height (px 24), padding 10 ] { description = "flag of j entry", src = "../assets/img/jentry.png" } + , entryLink "Jentry" "J" + ] + , column [ spacing 15 ] + [ row + [ spacing 15 + ] + [ linkBtnInt "← Back" "/" + , btn "Explore ↓" (ScrollToEntry "J") + ] + ] + ] + ) + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "zero" then + view3DColors model + + else + Element.none + ) + ] + , column + (page model ++ [ id "J", Background.color (rgb255 76 76 254) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "zero") + , btn "↓" (ScrollToEntry "A") + ] + , paragraph + subheading + [ text "Welcome to the Jentry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "(J Entry) The heart of MacGregor. Historic culture, big lore. Jentry memes. Greatest MacGregor Housecomm representation of any entry. Jamily is forever." + , image [ width (px 48), padding 12 ] { description = "flag of j entry", src = "../assets/img/jentry.png" } + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "J" then + view3DEntries "J" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "A", Background.color (rgb255 5 93 243) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "J") + , btn "↓" (ScrollToEntry "B") + ] + , paragraph + subheading + [ text "Welcome to A Entry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "Top floor. Top dog. Lots of dogs, in fact. Historic MacGregor cultural epicenter." + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "A" then + view3DEntries "A" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "B", Background.color (rgb255 5 180 93) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "A") + , btn "↓" (ScrollToEntry "C") + ] + , paragraph + subheading + [ text "Welcome to the Bentry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "(B Entry) Office of the President. Sweeping views. Crossroads of the High Rise. B is for burgers?" + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "B" then + view3DEntries "B" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "C", Background.color (rgb255 5 140 40) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "B") + , btn "↓" (ScrollToEntry "D") + ] + , paragraph + subheading + [ text "Welcome to Centry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "(C Entry) Ceriously quiet. High enough to see the clouds, but low enough to stay grounded. MacGregor's creative capital core." + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "C" then + view3DEntries "C" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "D", Background.color (rgb255 240 140 40) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "C") + , btn "↓" (ScrollToEntry "E") + ] + , paragraph + subheading + [ text "Welcome to Dentry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "(D Entry) Office of the Vice President. Active after dark. Diligently working until dusk. The culture is palpable." + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "D" then + view3DEntries "D" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "E", Background.color (rgb255 40 20 30) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "D") + , btn "↓" (ScrollToEntry "F") + ] + , paragraph + subheading + [ text "Welcome to E entry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "Board games over Briggs field. Excellence in everything. End of story." + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "E" then + view3DEntries "E" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "F", Background.color (rgb255 200 50 75) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "E") + , btn "↓" (ScrollToEntry "G") + ] + , paragraph + subheading + [ text "Welcome to the Fentry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "(F entry) F is for Food. F is for Feast. F is for First (low rise block). F is Forever." + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "F" then + view3DEntries "F" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "G", Background.color (rgb255 200 50 150) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "F") + , btn "↓" (ScrollToEntry "H") + ] + , paragraph + subheading + [ text "Welcome to the Gentry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "(G entry) G is for Greatness. Spans the Charles. Overlooks Fenway. Good things start in Gentry." + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "G" then + view3DEntries "G" model + + else + Element.none + ) + ] + , column + (page model ++ [ id "H", Background.color (rgb255 200 150 50) ]) + [ column (pageText model) + [ row [ spacing 10 ] + [ btn "↑" (ScrollToEntry "G") + , btn "TOP" (ScrollToEntry "zero") + ] + , paragraph + subheading + [ text "Welcome to the Hentry." ] + , paragraph (bodyText ++ [ Font.size 32 ]) + [ text "(H entry) Lots of chickens. Crossroads of the Low Rise. Largest entry in MacGregor." + ] + ] + , el + [ alignRight + , alignTop + , width (vw2px model 60) + , height (vh2px model 100) + , paddingEach + { top = vh2pt model 25 + , bottom = vh2pt model 25 + , left = 0 + , right = 0 + } + ] + (if model.entry == "H" then + view3DEntries "H" model + + else + Element.none + ) + ] + ] + ) + ] + + notFound : Model -> List (Html Msg) notFound model = [ Element.layout @@ -785,6 +1302,195 @@ view3D model = ) +view3DColors : Model -> Element msg +view3DColors model = + Element.html + (let + entity : Entity Obj.Decode.ObjCoordinates + entity = + case model.mesh of + Nothing -> + Scene3d.mesh (Material.matte model.mainColor) pyramidMesh + + Just mesh -> + Scene3d.mesh (Material.matte model.mainColor) (Mesh.texturedFacets mesh) + + camera : Camera3d.Camera3d Length.Meters coordinates + camera = + Camera3d.perspective + { viewpoint = + Viewpoint3d.lookAt + { focalPoint = Point3d.origin + , eyePoint = + let + theta : Angle.Angle + theta = + Angle.degrees (5 / 2 * model.angle) + in + Point3d.meters (10 * Angle.cos theta) 2 (10 * Angle.sin theta) + , upDirection = Direction3d.xy (Angle.degrees 90) + } + , verticalFieldOfView = Angle.degrees 100 + } + in + Scene3d.sunny + { entities = [ entity ] + , camera = camera + , upDirection = Direction3d.z + , sunlightDirection = Direction3d.yz (Angle.degrees -120) + , background = Scene3d.transparentBackground + , clipDepth = Length.centimeters 1 + , shadows = False + , dimensions = ( Pixels.int (round (vw model 60)), Pixels.int (round (vh model 100)) ) + } + ) + + +view3DEntries : String -> Model -> Element msg +view3DEntries entry model = + Element.html + (let + entity : Entity Obj.Decode.ObjCoordinates + entity = + case model.mesh of + Nothing -> + Scene3d.mesh (Material.matte model.mainColor) pyramidMesh + + Just mesh -> + case model.textures of + Nothing -> + Scene3d.mesh (Material.matte model.mainColor) (Mesh.texturedFacets mesh) + + Just textures -> + if model.show then + Scene3d.mesh (Material.matte model.mainColor) (Mesh.texturedFacets mesh) + + else + Scene3d.mesh textures (Mesh.texturedFacets mesh) + + height : Float + height = + case entry of + "A" -> + 10 + + "B" -> + 7 + + "C" -> + 5 + + "E" -> + 1 + + _ -> + 2 + + perspective : Angle.Angle + perspective = + let + theta : Float + theta = + 180 * frac (model.angle / 180) + + winding : Float + winding = + 360 * frac (model.angle / 360) + + direction : Float + direction = + abs (180 - winding) / (180 - winding) + + interp : Float -> Float -> Angle.Angle + interp a b = + Angle.degrees + (if direction > 0 then + a + (b - a) * theta / 180 + + else + b + (a - b) * theta / 180 + ) + in + case entry of + "J" -> + interp -45 30 + + "E" -> + interp 30 110 + + "F" -> + interp 140 230 + + "G" -> + interp 230 290 + + "H" -> + interp 290 315 + + _ -> + Angle.degrees model.angle + + orbit : Float + orbit = + case entry of + "A" -> + 4 + + "B" -> + 6 + + "C" -> + 6 + + "D" -> + 10 + + "F" -> + 12 + + "G" -> + 10 + + "J" -> + 12 + + "H" -> + 11 + + _ -> + 7 + + camera : Camera3d.Camera3d Length.Meters coordinates + camera = + Camera3d.perspective + { viewpoint = + Viewpoint3d.lookAt + { focalPoint = Point3d.origin + , eyePoint = + let + theta : Angle.Angle + theta = + perspective + in + Point3d.meters (orbit * Angle.cos theta) height (orbit * Angle.sin theta) + , upDirection = Direction3d.xy (Angle.degrees 90) + } + , verticalFieldOfView = Angle.degrees 60 + } + in + Scene3d.sunny + { entities = [ entity ] + , camera = camera + , upDirection = Direction3d.z + , sunlightDirection = Direction3d.yz (Angle.degrees -120) + , background = Scene3d.transparentBackground + , clipDepth = Length.centimeters 1 + , shadows = False + , dimensions = ( Pixels.int (round (vw model 60)), Pixels.int (round (vh model 100)) ) + } + ) + + view3DTower : Model -> Element msg view3DTower model = Element.html