From 058975643d72c37b045f91321c54230e8481e618 Mon Sep 17 00:00:00 2001 From: ccamel Date: Mon, 30 Dec 2024 18:05:05 +0100 Subject: [PATCH 1/3] :sparkles: Add showcase terrain --- src/App/Messages.elm | 3 + src/App/Models.elm | 5 +- src/App/Pages.elm | 5 + src/App/Routing.elm | 4 + src/App/Update.elm | 10 + src/Page/Terrain.elm | 457 +++++++++++++++++++++++++++++++++++++++++++ static/terrain.webp | Bin 0 -> 18512 bytes 7 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 src/Page/Terrain.elm create mode 100644 static/terrain.webp diff --git a/src/App/Messages.elm b/src/App/Messages.elm index 7e79ce8f..45bffebe 100644 --- a/src/App/Messages.elm +++ b/src/App/Messages.elm @@ -12,6 +12,7 @@ import Page.Maze import Page.Physics import Page.SoundWaveToggle import Page.Term +import Page.Terrain import Url exposing (Url) @@ -27,6 +28,7 @@ type Page | Dapp | SoundWaveToggle | Glsl + | Terrain type Msg @@ -45,3 +47,4 @@ type Msg | DappPageMsg Page.Dapp.Msg | SoundWaveTogglePageMsg Page.SoundWaveToggle.Msg | GlslPageMsg Page.Glsl.Msg + | TerrainPageMsg Page.Terrain.Msg diff --git a/src/App/Models.elm b/src/App/Models.elm index 410084f6..2b13466f 100644 --- a/src/App/Models.elm +++ b/src/App/Models.elm @@ -14,6 +14,7 @@ import Page.Maze import Page.Physics import Page.SoundWaveToggle import Page.Term +import Page.Terrain type alias PagesModel = @@ -28,6 +29,7 @@ type alias PagesModel = , dappPage : Maybe Page.Dapp.Model , soundWaveTogglePage : Maybe Page.SoundWaveToggle.Model , glslPage : Maybe Page.Glsl.Model + , terrainPage : Maybe Page.Terrain.Model } @@ -41,7 +43,7 @@ type alias Model = } -emptyPagesModel : { aboutPage : Maybe a, calcPage : Maybe b, lissajousPage : Maybe c, digitalClockPage : Maybe d, mazePage : Maybe e, physicsPage : Maybe f, termPage : Maybe g, asteroidsPage : Maybe h, dappPage : Maybe i, soundWaveTogglePage : Maybe j, glslPage : Maybe k } +emptyPagesModel : { aboutPage : Maybe a, calcPage : Maybe b, lissajousPage : Maybe c, digitalClockPage : Maybe d, mazePage : Maybe e, physicsPage : Maybe f, termPage : Maybe g, asteroidsPage : Maybe h, dappPage : Maybe i, soundWaveTogglePage : Maybe j, glslPage : Maybe k, terrainPage : Maybe l } emptyPagesModel = { aboutPage = Nothing , calcPage = Nothing @@ -54,4 +56,5 @@ emptyPagesModel = , dappPage = Nothing , soundWaveTogglePage = Nothing , glslPage = Nothing + , terrainPage = Nothing } diff --git a/src/App/Pages.elm b/src/App/Pages.elm index 768c5e9a..47a51918 100644 --- a/src/App/Pages.elm +++ b/src/App/Pages.elm @@ -15,6 +15,7 @@ import Page.Maze import Page.Physics import Page.SoundWaveToggle import Page.Term +import Page.Terrain emptyNode : Html msg @@ -48,6 +49,7 @@ pages = , Dapp , SoundWaveToggle , Glsl + , Terrain ] @@ -120,6 +122,9 @@ pageSpec page = Glsl -> toSpec Page.Glsl.info Page.Glsl.view Page.Glsl.subscriptions GlslPageMsg (\model -> model.pages.glslPage) + Terrain -> + toSpec Page.Terrain.info Page.Terrain.view Page.Terrain.subscriptions TerrainPageMsg (\model -> model.pages.terrainPage) + pageName : Page -> String pageName page = diff --git a/src/App/Routing.elm b/src/App/Routing.elm index d1d99dca..916b625f 100644 --- a/src/App/Routing.elm +++ b/src/App/Routing.elm @@ -12,6 +12,7 @@ import Page.Maze import Page.Physics import Page.SoundWaveToggle import Page.Term +import Page.Terrain import Url exposing (Url) import Url.Parser exposing (Parser, fragment, map, oneOf, parse, s) @@ -73,6 +74,9 @@ parseFragment fragment = else if p == Page.Glsl.info.name then Page Glsl + else if p == Page.Terrain.info.name then + Page Terrain + else NotFoundRoute diff --git a/src/App/Update.elm b/src/App/Update.elm index a6f2dccd..1c1d1c69 100644 --- a/src/App/Update.elm +++ b/src/App/Update.elm @@ -19,6 +19,7 @@ import Page.Maze import Page.Physics import Page.SoundWaveToggle import Page.Term +import Page.Terrain import Task import Tuple exposing (first, second) import Url @@ -95,6 +96,9 @@ update msg model = ( glslModel, glslCmd ) = Page.Glsl.init + + ( terrainModel, terrainCmd ) = + Page.Terrain.init in case newRoute of NotFoundRoute -> @@ -136,6 +140,9 @@ update msg model = Page Glsl -> ( { clearedModel | route = newRoute, pages = { emptyPagesModel | glslPage = Just glslModel } }, Cmd.map GlslPageMsg glslCmd ) + Page Terrain -> + ( { clearedModel | route = newRoute, pages = { emptyPagesModel | terrainPage = Just terrainModel } }, Cmd.map TerrainPageMsg terrainCmd ) + -- messages from pages AboutPageMsg m -> convert model m .aboutPage Page.About.update (\mdl -> { model | pages = { pages | aboutPage = Just mdl } }) AboutPageMsg @@ -170,6 +177,9 @@ update msg model = GlslPageMsg m -> convert model m .glslPage Page.Glsl.update (\mdl -> { model | pages = { pages | glslPage = Just mdl } }) GlslPageMsg + TerrainPageMsg m -> + convert model m .terrainPage Page.Terrain.update (\mdl -> { model | pages = { pages | terrainPage = Just mdl } }) TerrainPageMsg + init : Flags -> Url.Url -> Nav.Key -> ( Model, Cmd App.Messages.Msg ) init flags url navKey = diff --git a/src/Page/Terrain.elm b/src/Page/Terrain.elm new file mode 100644 index 00000000..28fe61a7 --- /dev/null +++ b/src/Page/Terrain.elm @@ -0,0 +1,457 @@ +module Page.Terrain exposing (Model, Msg, info, init, subscriptions, update, view) + +import Basics.Extra exposing (curry) +import Browser.Events exposing (onAnimationFrameDelta) +import Html exposing (Html, div, section) +import Html.Attributes as Attr +import Lib.Page +import Markdown +import Math.Vector2 as Vec2 exposing (Vec2, vec2) +import Random exposing (Seed) +import String exposing (fromFloat, fromInt, join) +import Svg exposing (Svg, rect, svg) +import Svg.Attributes exposing (class, d, fill, height, points, stroke, strokeWidth, style, transform, version, viewBox, width, x, y) + + + +-- PAGE INFO + + +info : Lib.Page.PageInfo Msg +info = + { name = "terrain" + , hash = "terrain" + , date = "2024-12-31" + , description = Markdown.toHtml [ Attr.class "content" ] """ +A retro-inspired endless terrain flyover, featuring a procedurally generated 1D landscape. + """ + , srcRel = "Page/Terrain.elm" + } + + + +-- MODEL + + +type alias Parameters = + { width : Int + , height : Int + , speed : Float + , nbCurves : Int + } + + +type alias ModelRecord = + { parameters : Parameters + , terrain : Terrain -- the terrain to render + , time : Float -- for animation, in milliseconds + , seed : Seed -- for random + } + + +type Model + = Model ModelRecord + + +init : ( Model, Cmd Msg ) +init = + let + parameters = + initialParameters + + initialSeed = + Random.initialSeed 42 + + ( curves, finalSeed ) = + Random.step (terrainGenerator (\idx -> toFloat idx) parameters.nbCurves) initialSeed + in + ( Model + { parameters = parameters + , terrain = curves + , time = 0.0 + , seed = finalSeed + } + , Cmd.none + ) + + +initialParameters : Parameters +initialParameters = + { width = 320 + , height = 200 + , speed = 20 + , nbCurves = 40 + } + + + +-- MESSAGES + + +type Msg + = GotAnimationFrameDeltaMilliseconds Float + + + +-- UPDATE + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg (Model ({ terrain, time, parameters, seed } as model)) = + Tuple.mapFirst Model <| + case msg of + GotAnimationFrameDeltaMilliseconds delta -> + let + deltaZ = + -1 * (parameters.speed * delta / 1000) + + updatedTerrain = + terrain + |> moveTerrain deltaZ + |> List.filter (\{ offset } -> offset > 0) + + terrainSize = + List.length updatedTerrain + + neededLayers = + parameters.nbCurves - terrainSize + + ( newTerrain, newSeed ) = + Random.step (terrainGenerator (\idx -> toFloat (terrainSize + idx + 1)) neededLayers) seed + in + ( { model + | time = time + delta + , terrain = updatedTerrain ++ newTerrain + , seed = newSeed + } + , Cmd.none + ) + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions _ = + onAnimationFrameDelta GotAnimationFrameDeltaMilliseconds + + + +-- VIEW + + +view : Model -> Html Msg +view (Model { parameters, terrain }) = + let + scaleFactor = + 2 + + offsetX = + toFloat parameters.width * (1 - scaleFactor) / 2 + + offsetYPct = + 0.4 + + offsetY = + offsetYPct * toFloat parameters.height + in + section [ Attr.class "section pt-1 has-background-black-bis" ] + [ div [ Attr.class "container is-max-tablet" ] + [ div + [ Attr.id "terrain" ] + [ div [ class "columns is-centered mt-1" ] + [ div [ class "column is-four-fifths" ] + [ div [ class "has-text-centered" ] + [ div + [ style "" + ] + [ svg + [ version "1.1" + , class "world mx-auto" + , width "100%" + , style "max-width: 1024px" + , height "100%" + , viewBox (join " " [ "0", "0", fromInt parameters.width, fromInt parameters.height ]) + ] + [ Svg.g + [ Attr.id "background" ] + [ rect + [ x "0" + , y "0" + , width (fromInt parameters.width) + , height (fromInt parameters.height) + , fill "black" + , strokeWidth "0" + ] + [] + ] + , Svg.g + [ transform ("translate(" ++ fromFloat offsetX ++ "," ++ fromFloat (toFloat parameters.height + offsetY) ++ ") scale(" ++ fromFloat scaleFactor ++ ", -1)") ] + [ Svg.g + [ Attr.id "terrain" ] + (viewTerrain parameters.width terrain) + ] + ] + ] + ] + ] + ] + ] + ] + ] + + +viewTerrain : Int -> Terrain -> List (Svg Msg) +viewTerrain width terrain = + terrain + |> List.reverse + |> List.indexedMap + (\idx { curve, offset } -> + let + near = + 300.0 + + z = + offset * 20 + + ( zoomX, zoomY ) = + ( toFloat width / (toFloat <| List.length curve) + , 1 + ) + + perspectiveFactor = + near / (near + z) + + ( perspectiveX, perspectiveY ) = + ( perspectiveFactor, perspectiveFactor ) + + ( scaleX, scaleY ) = + ( zoomX * perspectiveX + , zoomY * perspectiveY + ) + + ( translateX, translateY ) = + ( toFloat width * (1 - perspectiveX) / 2 + , z * perspectiveY + ) + in + Svg.g + [ Attr.id ("layer-" ++ fromInt idx) + , transform + ("translate(" + ++ fromFloat translateX + ++ "," + ++ fromFloat translateY + ++ ") " + ++ "scale(" + ++ fromFloat scaleX + ++ "," + ++ fromFloat scaleY + ++ ")" + ) + ] + (viewCurve curve) + ) + + +viewCurve : Curve -> List (Svg Msg) +viewCurve curve = + let + pts = + curve + |> curvePoints + |> List.map (\p -> ( p |> Vec2.getX, p |> Vec2.getY )) + + path = + pts + |> List.indexedMap + (\idx ( x, y ) -> + let + sx = + x |> String.fromFloat + + sy = + y |> String.fromFloat + + point = + sx ++ "," ++ sy + in + if idx == 0 then + "M 0, 0 L " ++ point + + else if idx == List.length curve - 1 then + point ++ " L " ++ sx ++ ", 0 L 0, 0" + + else + "L " ++ point + ) + |> join " " + + polyline = + pts + |> List.map + (\( x, y ) -> + String.fromFloat x ++ "," ++ String.fromFloat y + ) + |> join " " + in + [ Svg.path [ d path, fill "black", stroke "none" ] [] + , Svg.polyline [ points polyline, stroke "blue", strokeWidth "0.5" ] [] + ] + + + +-- CURVE + + +{-| A curve is a list of floats representing the height of the terrain at each point. +-} +type alias Curve = + List Float + + +{-| A curve located at a specific offset. +-} +type alias LocatedCurve = + { curve : Curve + , offset : Float -- the offset of the curve in the z direction. 0 is the front of the screen. + } + + +curveAt : Float -> Curve -> LocatedCurve +curveAt offset curve = + { curve = curve + , offset = offset + } + + +moveCurve : Float -> LocatedCurve -> LocatedCurve +moveCurve delta { curve, offset } = + { curve = curve, offset = offset + delta } + + +curvePointsWith : (Int -> Float) -> Curve -> List Vec2 +curvePointsWith indexToX curve = + List.indexedMap (\i y -> vec2 (indexToX i) y) curve + + +curvePoints : Curve -> List Vec2 +curvePoints = + curvePointsWith toFloat + + +initialCurve : Random.Generator Curve +initialCurve = + Random.list 7 + (Random.float 0 100 + |> Random.andThen + (\p -> + if p < 70 then + Random.float 10 100 + + else + Random.constant 0 + ) + ) + + +generateFractal : Random.Generator Curve +generateFractal = + fBm 4 0.3 initialCurve + + + +-- TERRAIN + + +{-| A list of curves representing the terrain. +-} +type alias Terrain = + List LocatedCurve + + +moveTerrain : Float -> Terrain -> Terrain +moveTerrain delta = + List.map (moveCurve delta) + + +terrainGenerator : (Int -> Float) -> Int -> Random.Generator Terrain +terrainGenerator indexToOffset count = + Random.list count generateFractal + |> Random.map (List.indexedMap (\idx curve -> curveAt (indexToOffset idx) curve)) + + + +-- MISCELLANEOUS + + +fBm : Int -> Float -> Random.Generator Curve -> Random.Generator Curve +fBm depth hurst baseGenerator = + List.foldl + (\_ gen -> fBmAtDepth depth hurst gen) + baseGenerator + (List.range 1 depth) + + +fBmAtDepth : Int -> Float -> Random.Generator Curve -> Random.Generator Curve +fBmAtDepth depth hurst curveGenerator = + if depth == 0 then + curveGenerator + + else + let + generateMidpoint : Random.Generator Float -> Random.Generator Float -> Random.Generator Float + generateMidpoint a b = + Random.map2 (curry (midpointDisplacementGenerator (2 * hurst * (depth |> toFloat)))) a b + |> Random.andThen identity + + refine : List Float -> Random.Generator Curve + refine = + List.map Random.constant + >> insertBetween generateMidpoint + >> combineGenerators + in + curveGenerator + |> Random.andThen refine + + +midpointDisplacementGenerator : Float -> ( Float, Float ) -> Random.Generator Float +midpointDisplacementGenerator hurst ( a, b ) = + let + midpoint : Float + midpoint = + (a + b) / 2 + + delta : Float + delta = + abs (a - b) * 2 ^ -hurst + + rg : Random.Generator Float + rg = + Random.float -delta delta + in + rg |> Random.map (\r -> midpoint + r) + + +combineGenerators : List (Random.Generator a) -> Random.Generator (List a) +combineGenerators list = + case list of + [] -> + Random.constant [] + + gen :: gens -> + Random.map2 (::) gen (combineGenerators gens) + + +insertBetween : (a -> a -> a) -> List a -> List a +insertBetween f list = + case list of + [] -> + [] + + [ x ] -> + [ x ] + + x :: y :: rest -> + x :: f x y :: insertBetween f (y :: rest) diff --git a/static/terrain.webp b/static/terrain.webp new file mode 100644 index 0000000000000000000000000000000000000000..f572a9f2c9259f3aee55c5c34a0340443fc42255 GIT binary patch literal 18512 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 q Date: Tue, 31 Dec 2024 11:49:41 +0100 Subject: [PATCH 2/3] :memo: Add description for Terrain showcase --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c06142ff..77a73840 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ The demo is a pure [SPA][] (100% clientside application) written in [elm][]. List of showcases: +- [terrain](https://ccamel.github.io/playground-elm/#terrain): A retro-inspired endless terrain flyover, featuring a procedurally generated 1D landscape, rendered in [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics). + - [glsl](https://ccamel.github.io/playground-elm/#glsl): A dynamic [WebGL](https://www.khronos.org/webgl/) electricity effect created with [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) shaders, featuring interactive 3D rotation with smooth inertia. - [soundWave toggle](https://ccamel.github.io/playground-elm/#sound-wave-toggle): A simple sound wave toggle button From 109b216f7907792be2cb32d0bfa829c9395841a6 Mon Sep 17 00:00:00 2001 From: ccamel Date: Tue, 31 Dec 2024 12:22:05 +0100 Subject: [PATCH 3/3] :recycle: Parametrize values in Terrain showcase --- src/Page/Terrain.elm | 45 +++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/Page/Terrain.elm b/src/Page/Terrain.elm index 28fe61a7..7762cf8b 100644 --- a/src/Page/Terrain.elm +++ b/src/Page/Terrain.elm @@ -38,6 +38,10 @@ type alias Parameters = , height : Int , speed : Float , nbCurves : Int + , near : Float + , offsetFactor : Float + , depth : Int + , hurst : Float } @@ -62,8 +66,11 @@ init = initialSeed = Random.initialSeed 42 + curveGenerator = + generateFractal parameters.depth parameters.hurst + ( curves, finalSeed ) = - Random.step (terrainGenerator (\idx -> toFloat idx) parameters.nbCurves) initialSeed + Random.step (terrainGenerator (\idx -> toFloat idx) curveGenerator parameters.nbCurves) initialSeed in ( Model { parameters = parameters @@ -81,6 +88,10 @@ initialParameters = , height = 200 , speed = 20 , nbCurves = 40 + , near = 300 + , offsetFactor = 20.0 + , depth = 4 + , hurst = 0.3 } @@ -116,8 +127,11 @@ update msg (Model ({ terrain, time, parameters, seed } as model)) = neededLayers = parameters.nbCurves - terrainSize + curveGenerator = + generateFractal parameters.depth parameters.hurst + ( newTerrain, newSeed ) = - Random.step (terrainGenerator (\idx -> toFloat (terrainSize + idx + 1)) neededLayers) seed + Random.step (terrainGenerator (\idx -> toFloat (terrainSize + idx + 1)) curveGenerator neededLayers) seed in ( { model | time = time + delta @@ -190,7 +204,7 @@ view (Model { parameters, terrain }) = [ transform ("translate(" ++ fromFloat offsetX ++ "," ++ fromFloat (toFloat parameters.height + offsetY) ++ ") scale(" ++ fromFloat scaleFactor ++ ", -1)") ] [ Svg.g [ Attr.id "terrain" ] - (viewTerrain parameters.width terrain) + (viewTerrain parameters terrain) ] ] ] @@ -202,18 +216,19 @@ view (Model { parameters, terrain }) = ] -viewTerrain : Int -> Terrain -> List (Svg Msg) -viewTerrain width terrain = +type alias ViewTerrainParams a = + { a | width : Int, near : Float, offsetFactor : Float } + + +viewTerrain : ViewTerrainParams a -> Terrain -> List (Svg Msg) +viewTerrain { width, near, offsetFactor } terrain = terrain |> List.reverse |> List.indexedMap (\idx { curve, offset } -> let - near = - 300.0 - z = - offset * 20 + offset * offsetFactor ( zoomX, zoomY ) = ( toFloat width / (toFloat <| List.length curve) @@ -356,9 +371,9 @@ initialCurve = ) -generateFractal : Random.Generator Curve -generateFractal = - fBm 4 0.3 initialCurve +generateFractal : Int -> Float -> Random.Generator Curve +generateFractal depth hurst = + fBm depth hurst initialCurve @@ -376,9 +391,9 @@ moveTerrain delta = List.map (moveCurve delta) -terrainGenerator : (Int -> Float) -> Int -> Random.Generator Terrain -terrainGenerator indexToOffset count = - Random.list count generateFractal +terrainGenerator : (Int -> Float) -> Random.Generator Curve -> Int -> Random.Generator Terrain +terrainGenerator indexToOffset curveGenerator nbCurves = + Random.list nbCurves curveGenerator |> Random.map (List.indexedMap (\idx curve -> curveAt (indexToOffset idx) curve))