initial commit

main
Louis Pearson 2024-01-13 12:31:03 -07:00
commit b6cb2a2750
4 changed files with 7422 additions and 0 deletions

7053
PhotoGroove/app.js Normal file

File diff suppressed because it is too large Load Diff

29
PhotoGroove/elm.json Normal file
View File

@ -0,0 +1,29 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.1",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/random": "1.0.0"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.3"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

59
PhotoGroove/index.html Normal file
View File

@ -0,0 +1,59 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="http://elm-in-action.com/styles.css">
<link rel="stylesheet" href="http://elm-in-action.com/range-slider.css">
<script src="http://elm-in-action.com/range-slider.js"></script>
<script>
class RangeSlider extends HTMLElement {
connectedCallback() {
var input = document.createElement("input");
this.appendChild(input);
var jsr = new JSR(input, {
max: this.max,
values: [this.val],
sliders: 1,
grid: false
});
var rangeSliderNode = this;
jsr.addEventListener("update", function(elem, value) {
var event = new CustomEvent("slide", {
detail: {userSlidTo: value}
});
rangeSliderNode.dispatchEvent(event);
});
}
}
window.customElements.define("range-slider", RangeSlider);
</script>
</head>
<body>
<div id="app"></div> <!-- Elm application renders into this div --!>
<script src="http://elm-in-action.com/pasta.js"></script>
<script src="app.js"></script> <!-- PhotoGroove.elm will get compiled into app.js --!>
<script>
var app = Elm.PhotoGroove.init({
node: document.getElementById("app"),
flags: Pasta.version
}); // Elm object comes from app.js
app.ports.setFilters.subscribe(function(options) {
requestAnimationFrame(function() {
Pasta.apply(document.getElementById("main-canvas"), options);
});
});
Pasta.addActivityListener(function(activity) {
console.log("Got some activity to send to Elm:", activity);
app.ports.activityChanges.send(activity);
});
</script>
</body>
</html>

View File

@ -0,0 +1,281 @@
port module PhotoGroove exposing (main)
import Array exposing (Array)
import Browser
import Html exposing (..)
import Html.Attributes as Attr exposing (class, classList, id, name, src, title, type_)
import Html.Events exposing (onClick, on)
import Http
import Json.Decode exposing (Decoder, at, int, list, string, succeed)
import Json.Decode.Pipeline exposing (optional, required)
import Json.Encode
import Random
urlPrefix : String
urlPrefix =
"https://elm-in-action.com/"
type Msg
= ClickedPhoto String
| SetSize ThumbnailSize
| ClickedSurpriseMe
| GotRandomPhoto Photo
| GotActivity String
| GotPhotos (Result Http.Error (List Photo))
| SlidHue Int
| SlidRipple Int
| SlidNoise Int
view : Model -> Html Msg
view model =
div [ class "content" ] <|
case model.status of
Loaded photos selectedUrl ->
viewLoaded photos selectedUrl model
Loading ->
[]
Errored errorMessage ->
[ text ("Error: " ++ errorMessage) ]
viewFilter : (Int -> Msg) -> String -> Int -> Html Msg
viewFilter toMsg name magnitude =
div [ class "filter-slider" ]
[ label [] [ text name ]
, rangeSlider
[ Attr.max "11"
, Attr.property "val" (Json.Encode.int magnitude)
, onSlide toMsg
]
[]
, label [] [ text (String.fromInt magnitude) ]
]
viewLoaded : List Photo -> String -> Model -> List (Html Msg)
viewLoaded photos selectedUrl model =
[ h1 [] [ text "Photo Groove" ]
, button
[ onClick ClickedSurpriseMe ]
[ text "Surprise Me!" ]
, div [ class "activity" ] [ text model.activity ]
, div [ class "filters" ]
[ viewFilter SlidHue "Hue" model.hue
, viewFilter SlidRipple "Ripple" model.ripple
, viewFilter SlidNoise "Noise" model.noise
]
, h3 [] [ text "Thumbnail Size:" ]
, div [ id "choose-size" ]
(List.map viewSizeChooser [ Small, Medium, Large ])
, div [ id "thumbnails", class (sizeToString model.chosenSize) ]
(List.map (viewThumbnail selectedUrl) photos)
, canvas [ id "main-canvas", class "large" ] []
]
viewThumbnail : String -> Photo -> Html Msg
viewThumbnail selectedUrl thumb =
img
[ src (urlPrefix ++ thumb.url)
, title (thumb.title ++ " [" ++ String.fromInt thumb.size ++ " KB]")
, classList [ ( "selected", selectedUrl == thumb.url ) ]
, onClick (ClickedPhoto thumb.url)
]
[]
viewSizeChooser : ThumbnailSize -> Html Msg
viewSizeChooser size =
label []
[ input [ type_ "radio", name "size", onClick (SetSize size) ] []
, text (sizeToString size)
]
sizeToString : ThumbnailSize -> String
sizeToString size =
case size of
Small ->
"small"
Medium ->
"medium"
Large ->
"large"
type ThumbnailSize
= Small
| Medium
| Large
port setFilters : FilterOptions -> Cmd msg
port activityChanges : (String -> msg) -> Sub msg
type alias FilterOptions =
{ url : String
, filters : List { name : String, amount : Float }
}
type alias Photo =
{ url : String
, size : Int
, title : String
}
photoDecoder : Decoder Photo
photoDecoder =
succeed Photo
|> required "url" string
|> required "size" int
|> optional "title" string "(untitled)"
type Status
= Loading
| Loaded (List Photo) String
| Errored String
type alias Model =
{ status : Status
, activity : String
, chosenSize : ThumbnailSize
, hue : Int
, ripple : Int
, noise : Int
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotActivity activity ->
( { model | activity = activity }, Cmd.none)
GotPhotos (Ok photos) ->
case photos of
first ::rest ->
applyFilters
{ model
| status =
case List.head photos of
Just photo ->
Loaded photos photo.url
Nothing ->
Loaded [] ""
}
[] ->
( { model | status = Errored "0 photos found"}, Cmd.none )
GotPhotos (Err _) ->
( model, Cmd.none )
GotRandomPhoto photo ->
applyFilters { model | status = selectUrl photo.url model.status }
ClickedPhoto url ->
applyFilters { model | status = selectUrl url model.status }
SetSize size ->
( { model | chosenSize = size }, Cmd.none )
ClickedSurpriseMe ->
case model.status of
Loaded (firstPhoto :: otherPhotos) _ ->
( model
, Random.generate GotRandomPhoto
(Random.uniform firstPhoto otherPhotos)
)
Loaded [] _ ->
( model, Cmd.none )
Loading ->
( model, Cmd.none )
Errored errorMessage ->
( model, Cmd.none )
SlidHue hue ->
applyFilters { model | hue = hue }
SlidRipple ripple ->
applyFilters { model | ripple = ripple }
SlidNoise noise ->
applyFilters { model | noise = noise }
applyFilters : Model -> ( Model, Cmd msg )
applyFilters model =
case model.status of
Loaded photos selectedUrl ->
let
filters =
[ { name = "Hue", amount = toFloat model.hue / 11}
, { name = "Ripple", amount = toFloat model.ripple / 11}
, { name = "Noise", amount = toFloat model.noise / 11}
]
url =
urlPrefix ++ "large/" ++ selectedUrl
in
( model, setFilters { url = url, filters = filters } )
Loading ->
( model, Cmd.none )
Errored errorMessage ->
( model, Cmd.none )
selectUrl : String -> Status -> Status
selectUrl url status =
case status of
Loaded photos _ ->
Loaded photos url
Loading ->
status
Errored errorMessage ->
status
initialModel : Model
initialModel =
{ status = Loading
, activity = ""
, chosenSize = Medium
, hue = 5
, ripple = 5
, noise = 5
}
initialCmd : Cmd Msg
initialCmd =
Http.get
{ url = "http://elm-in-action.com/photos/list.json"
, expect = Http.expectJson GotPhotos (list photoDecoder)
}
main : Program Float Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
init : Float -> ( Model, Cmd Msg )
init flags =
let
activity =
"Initializing Pasta v" ++ String.fromFloat flags
in
( { initialModel | activity = activity }, initialCmd )
subscriptions : Model -> Sub Msg
subscriptions model =
activityChanges GotActivity
rangeSlider : List (Attribute msg) -> List (Html msg) -> Html msg
rangeSlider attributes children =
node "range-slider" attributes children
onSlide : (Int -> msg) -> Attribute msg
onSlide toMsg =
at [ "detail", "userSlidTo" ] int
|> Json.Decode.map toMsg
|> on "slide"