From 21dbefab2df41944685b6cb82471add209dd04f7 Mon Sep 17 00:00:00 2001 From: Alex Huddleston Date: Sat, 7 Dec 2019 20:41:23 -0600 Subject: [PATCH] New repo who dis --- .gitignore | 7 + add-iamge.sh | 18 + assets/images/favicon/favicon.png | Bin 0 -> 2047 bytes assets/javascripts/detect_mobile.js | 2 + assets/styles/styles.css | 402 ++++++++++++++ elm.json | 28 + index.html | 25 + index.php | 1 + make.sh | 3 + src/Main.elm | 805 ++++++++++++++++++++++++++++ 10 files changed, 1291 insertions(+) create mode 100644 .gitignore create mode 100755 add-iamge.sh create mode 100644 assets/images/favicon/favicon.png create mode 100644 assets/javascripts/detect_mobile.js create mode 100644 assets/styles/styles.css create mode 100644 elm.json create mode 100644 index.html create mode 100644 index.php create mode 100755 make.sh create mode 100644 src/Main.elm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04a9d44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/elm-stuff/ +/downloads/ +/guidelines/ +assets/images/420/ +assets/images/full/ +assets/other/ +assets/javascripts/main.js diff --git a/add-iamge.sh b/add-iamge.sh new file mode 100755 index 0000000..08285fe --- /dev/null +++ b/add-iamge.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +IMG=$1 + +if ! [ -f $IMG ]; then + echo "Please specify a path to an image" + exit +fi + +PATHFULL=assets/images/full +PATH420=assets/images/420 + +mkdir -p $PATHFULL $PATH420 + +file=$(basename $IMG) + +convert $IMG -resize 420x420 "${PATH420}/${file%.*}.png" +convert $IMG "${PATHFULL}/${file%.*}.png" diff --git a/assets/images/favicon/favicon.png b/assets/images/favicon/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d23e82e37714df17e8a88159476edf85631873c8 GIT binary patch literal 2047 zcmVje$xG5{*F< z6EP~HlAy7~^a~)8Mlt%KYK%MsDs70Q*q|*K`bg=$T+Uj4n7z-Ly=L~g=WTLwc0s2 zSvg+7tboGhz+&Lig^qbrfRu8h!CfW*tXk+`a{;*km{`F0pUCtd19lkP1uQkdVCe$h z0R;2yC))&`27Z=noa*BrfEaw*;7`EYfN|iz3OfWoY=BC5$Y8a=Er0?uz&*h6`2Z9}qA%BclrM zYr%Vfr*aRdH1=I7#^M4lGMG|$M*&k62n$SL1RBc8`6=+-`GN>QniT<9q5_4nNisl| z&h}fRiiRsJ0q4@wQv>dRZ45@&@73B*O**gX^ zk`{%FHJH=;71|~}2D+?By$*~5+koE#XU_}Btb~}JD<)X^qV>QfamR@_`i?uQa7j&Z zdMs4Gse|a=1J(e)2F@55NS`)Q7_J=O;HzRVq%h_jD-$FpO;}-7-{|U@AZ4YJa|u4v z8VE?=s8yL>c-aO*N#v#B#3a{gh^z@CF)Nz6=T<}?1=bA&uvUfB-x{mIX3oFyUr;y$R0(e*vx|pYfc+zhdAv(uc{O{Hxh>M5=-mo`C4@{MECv-ORd~B+BxWXwUYYxm zoC;h$Fc5&3hu~1VJJ<%5;Oz0sDpMWqkuK}k3tAS0x zg99;AiU8*+T-pUd1SYm`S2sAzghjPJ)XggA_)VrpSScSV^}4nR1O!Hgfh&PYVA4W4 zM}^_;tfveFWgJ^I#bpk~Gwf7ru4?Hb#yIdu$;Kw&eBhZTMhb)V3ZGJ8wF##x0YuKO zJwx8+eN<|^N(!#>5qgZYfLY+?QrFKnF;W22z)c-tmBJ4dX1hwo3OAWpJ)&j%68^0hVWh94}xtc*-|b_>l@Ly~4FRX+8EC%iPn5sce490IUEJoP8_s z1K=9q15Jzo{8(XoEyOmMRQz(!j(>hmCOj+Am8c!Ks=T$6urf~4W&wOo;aZavKr+KI zLr&2q^GeJz0ulS6&+suI;Jt(p3~+#2Qwc>`m&3T=Bf6nEK15ru&`wPIujX?a&c{Y5=&4u73f{}vqzSITV zz(fYr2=F}`_9sQt%$ZZFDTma2#3^2zkXX4mTIHgbtCy}l4Xg!j>dT0Nsh|UeT<;^2 z0pl5-!{M|30kJu^3EPKnkO{$EMhyOA(D5eb!*MM{XB6;kB+;o48~z={SD{T@-ZZ>w-jg}qHPnuOE=%q>ZnxfxH(Zw>A< z_@BXn#Ob^4%?&Y-);ll19f3a1-wY_ zz2+M^0Ipv1qXMHYK_Ew?w`qJfvaCeG9#F z+!GGuUV}gTR(9{7mD z^$HgnoNmHVg}*60im{vC2{{J067~ltRocTk|4y?Kb~r8T)rYNbmlG~;`fpC7ZUT;- d%s)&4{|9VMOlyBa#W4T?002ovPDHLkV1it3s-ge@ literal 0 HcmV?d00001 diff --git a/assets/javascripts/detect_mobile.js b/assets/javascripts/detect_mobile.js new file mode 100644 index 0000000..0ca0a56 --- /dev/null +++ b/assets/javascripts/detect_mobile.js @@ -0,0 +1,2 @@ +function isMobile(){return (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))){return true;}else{return false;}})(navigator.userAgent||navigator.vendor||window.opera);} + diff --git a/assets/styles/styles.css b/assets/styles/styles.css new file mode 100644 index 0000000..bfd2d83 --- /dev/null +++ b/assets/styles/styles.css @@ -0,0 +1,402 @@ +body { + background-color: #ffffff; + margin: 0; + font-family: sans-serif; +} + +.mobile-nav .dropdown { + position: relative !important; +} + +.mobile-nav.hidden { + display: none; +} + +header { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1; + color: #999999; + background-color: #292929; + border-color: #101010; + border-style: solid; + border-width: 0; + border-bottom-width: 1px; +} + +.desktop-header { + height: 50px; +} + +.mobile-header { + max-height: 100vh; + overflow: auto; +} + +.mobile-header .show-nav { + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: space-around; + height: 50px; +} + +.mobile-header .show-nav.active { + color: #ffffff; + background-color: #2266ff; +} + +.mobile-header .show-nav p { + margin: 0; + padding: 0 1em; +} + +.carat { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top-color: currentcolor; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + +nav { + background-color: #292929; +} + +.desktop-nav { + margin: 0 auto; + height: 100%; +} + +.nav-wrapper { + background-color: #292929; +} + +.nav-wrapper ul { + padding: 0; + list-style-type: none; +} + +.nav-wrapper > ul { + margin: 0; +} + +.desktop-nav-wrapper > ul { + display: flex; + height: 100%; +} + +.mobile-header .show-nav, +.nav-wrapper > ul > li { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome and Opera */ +} + +.desktop-nav-wrapper > ul > li { + margin: 0; + text-align: center; +} + +.nav-wrapper > ul > li:hover, +.nav-wrapper > ul > li .dropdown-active { + color: #ffffff; + background-color: #2266ff; +} + +.desktop-nav-wrapper > ul > li:hover, +.desktop-nav-wrapper > ul > li .dropdown-active { + border-color: #2266ff; + border-style: solid; + border-width: 0; + border-bottom-width: 1px; + margin-bottom: -1px; +} + +.nav-wrapper > ul > li .button-wrapper { + cursor: pointer; +} + +.desktop-nav-wrapper > ul > li .button-wrapper { + display: flex; + flex-direction: column; + justify-content: space-around; + height: 100%; +} + +.desktop-nav-wrapper > ul > li .button-wrapper p { + margin: 0; + padding: 0 1em; + white-space: nowrap; +} + +.mobile-nav-wrapper > ul > li .button-wrapper p { + margin: 0; + padding: 1em; + white-space: nowrap; +} + +.nav-wrapper > ul > li .nav-link { + display: block; + text-decoration: none; + color: inherit; +} + +.mobile-nav .nav-item > div { + display: block !important; +} + +.desktop-nav-wrapper > ul > li.home-item .button-wrapper p { + font-weight: lighter; +} + +.mobile-nav-wrapper > ul > li.home-item .button-wrapper p { + font-weight: bold; +} + +.nav-wrapper > ul > li.nav-item .button-wrapper p { + font-weight: bold; + font-size: smaller; +} + +.desktop-nav-wrapper > ul > li .dropdown { + color: #292929; + background-color: #ffffff; + text-align: left; + min-width: 10em; + border-bottom: 1px solid #999; + margin-top: 1px; +} + +.mobile-nav-wrapper > ul > li .dropdown { + color: #cccccc; + background-color: #404040; + display: none; +} + +.mobile-nav-wrapper > ul > li .dropdown-active { + display: block; +} + +.desktop-nav-wrapper > ul > li .dropdown, +.desktop-nav-wrapper > ul > li ul.dropdown > li.sub-item:last-child p { + border-bottom-left-radius: 0.4em; + border-bottom-right-radius: 0.4em; +} + +.desktop-nav-wrapper > ul > li .dropdown li.sub-item { + cursor: pointer; +} + +.nav-wrapper > ul > li .dropdown li.sub-item p { + margin: 0; + font-size: smaller; +} + +.mobile-nav-wrapper > ul > li .dropdown li.sub-item p { + padding: 0.8em 1em; +} + +.desktop-nav-wrapper > ul > li .dropdown li.sub-item p { + padding: 0.4em 1em; +} + +.nav-wrapper > ul > li .dropdown li.sub-item p:hover { + color: #ffffff; + background-color: #2266ff; +} + +.welcome { + background-color: #444444; + background-image: url('/assets/images/full/pexels-photo-downscale.png'); + background-attachment: local; + background-size: cover; + position: relative; + z-index: 0; + height: 100vh; + max-height: 500px; + margin-top: 51px; +} + +.welcome .dimmer { + opacity: 0.7; + background-color: #000000; + color: #ffffff; +} + +.welcome .centered { + margin-left: auto; + margin-right: auto; +} + +.welcome .content { + padding: 0.5em 1em; +} + +.welcome .content h1 { + font-size: 3em; +} + +.welcome .content p { + padding: 0.5em 0; + font-size: 1.2em; +} + +.welcome .content h1, +.welcome .content p { + margin: 0; +} + +.gallery { + padding: 1em; + background-color: #efefef; + border-bottom: 1px solid #dbdbdb; +} + +.gallery .gallery-wrapper { + margin: 0 auto; + width: 100%; +} + +.gallery .gallery-info { + margin: 0 1em; +} + +.gallery .gallery-columns { + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 0 0.5em; +} + +.gallery .gallery-column { + margin: 0 auto; + flex: 1; + max-width: 465px; +} + +.gallery .gallery-image { + margin: 1em 0.5em; +} + +.gallery .gallery-image-wrapper { + padding: 1em; + background-color: #ffffff; + box-shadow: 0px 2px 3px #999999; +} + +.gallery .gallery-image img { + display: block; + width: 100%; + max-width: 420px; +} + +footer { + background-color: #f7f7f7; +} + +footer a { + text-decoration: none; + color: #00b0ff; +} + +footer a:hover { + color: #5555f0; +} + +footer a:active { + color: #00b0ff; +} + +footer .footer-wrapper { + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; +} + +footer .footer-column { + margin: 0 auto; + min-width: 300px; + max-width: 500px; +} + +footer .footer-column-wrapper { + padding: 1em; +} + +@media (min-width: 632px) { + .desktop-nav { + width: 900px; + } + + .gallery .gallery-wrapper { + width: 600px; + } + + footer .footer-wrapper { + width: 600px; + } + + footer .footer-column { + width: 300px; + } +} + +@media (min-width: 932px) { + .desktop-nav { + width: 900px; + } + + .welcome .centered { + width: 900px; + } + + .gallery .gallery-wrapper { + width: 900px; + } + + footer .footer-wrapper { + width: 900px + } + + footer .footer-column { + width: 300px; + } +} + +@media (min-width: 1200px) { + .desktop-nav { + width: 1170px; + } + + .welcome .centered { + width: 1170px; + } + + .gallery .gallery-wrapper { + width: 1170px; + } + + footer .footer-wrapper { + width: 1170px; + } + + footer .footer-column { + width: 390px; + } +} + diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..6c75063 --- /dev/null +++ b/elm.json @@ -0,0 +1,28 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.0", + "dependencies": { + "direct": { + "elm/browser": "1.0.1", + "elm/core": "1.0.2", + "elm/html": "1.0.0", + "hercules-ci/elm-dropdown": "1.0.1" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/parser": "1.1.0", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2", + "elm-community/json-extra": "4.2.0", + "rtfeldman/elm-iso8601-date-strings": "1.1.3" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..9e30cff --- /dev/null +++ b/index.html @@ -0,0 +1,25 @@ + + + + + + Werefox Software + + + + + + +
+

Please enable javascript to view this website

+
+ + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..ea07709 --- /dev/null +++ b/index.php @@ -0,0 +1 @@ + diff --git a/make.sh b/make.sh new file mode 100755 index 0000000..4352481 --- /dev/null +++ b/make.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +elm make --output="assets/javascripts/main.js" src/Main.elm \ No newline at end of file diff --git a/src/Main.elm b/src/Main.elm new file mode 100644 index 0000000..1699f63 --- /dev/null +++ b/src/Main.elm @@ -0,0 +1,805 @@ +module Main exposing (main) + +{-| This is the main module of the application. + + +# main function + +@docs main + +-} + +import Html exposing (Attribute, Html, a, article, button, div, footer, h1, h2, h3, h4, header, img, li, nav, p, section, span, text, ul) +import Html.Attributes exposing (class, href, rel, src, title) +import Html.Events exposing (onClick) +import Array exposing (Array) +import Dropdown exposing (ToggleEvent(..), drawer, dropdown, toggle) +import Browser.Events exposing (onResize) +import Browser.Dom exposing (getViewport) +import Browser exposing (document) +import Task exposing (perform) + + +-- MAIN + + +{-| The main function of the application +-} +main : Program Flags Model Msg +main = + document + { init = init + , view = (\model -> { title = "Your Mom", body = view model }) + , update = update + , subscriptions = subscriptions + } + + + +-- MODEL + + +type alias Model = + { flags : Flags + , nav : Navigation + , welcome : WelcomeBanner + , gallery : Gallery + , footer : List FooterSection + } + + +type alias Navigation = + { smallNav : Bool + , showNav : Bool + , navText : String + , items : Array NavItem + } + + +type NavItem + = HomeLink String String + | InitDropdown String (Array NavItem) + | Dropdown String DropdownConfig (Array NavItem) + | NavLink String String + + +type alias DropdownConfig = + { name : String + , event : ToggleEvent + , attribute : Attribute Msg + , state : Dropdown.State + , message : Bool -> Msg + } + + +type alias WelcomeBanner = + { title : String + , description : String + } + + +type alias Gallery = + { title : String + , description : String + , url : String + , urltext : String + , columns : Int + , images : List GalleryItem + } + + +type GalleryItem + = Image GalleryImage + | Project GalleryProject + | ProjectLink GalleryLink + + +type alias GalleryImage = + { url : String + , mouseoverText : String + } + + +type alias GalleryProject = + { url : String + , mouseoverText : String + , title : String + , description : List String + } + + +type alias GalleryLink = + { url : String + , mouseoverText : String + , title : String + , description : List String + , links : List ( String, String ) + } + + +type alias FooterSection = + { title : String + , description : String + , link : String + , url : String + } + + +type alias Flags = + { mobile : Bool + } + + + +-- INIT + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( { flags = flags + , nav = + { showNav = False + , smallNav = False + , navText = "Navigation" + , items = + initDropdowns + [ HomeLink "Home" "/" + , InitDropdown + "Social Media" + (initDropdowns + [ NavLink "Facebook" "https://facebook.com/shadow8t4" + , NavLink "Twitter" "https://twitter.com/Shadow8t4" + , NavLink "Google+" "https://plus.google.com/u/2/118253409016956205819" + ] + ) + , InitDropdown + "Games" + (initDropdowns + [ NavLink "Angels and Demons" "https://gitea.werefoxsoftware.com/shadow8t4/angels-and-demons" + , NavLink "Monster Chase" "https://gitea.werefoxsoftware.com/shadow8t4/MochaPine64Backup" + , NavLink "Monster Chase Server" "https://gitea.werefoxsoftware.com/shadow8t4/MochaServerPine64Backup" + , NavLink "Revival Survival" "/revival-survival" + , NavLink "Project Undercover" "/project-undercover" + , NavLink "Project Undercover Git Repo" "https://gitea.werefoxsoftware.com/shadow8t4/Project-Undercover" + , NavLink "So Bow-y Cute" "https://double-darling-duo-deluxe.itch.io/so-bow-y-cute" + ] + ) + , InitDropdown + "Other Projects" + (initDropdowns + [ NavLink "Re-Procedural City" "https://gitea.werefoxsoftware.com/shadow8t4/Re-ProceduralCity" + , NavLink "Procedural City" "https://gitea.werefoxsoftware.com/shadow8t4/ProceduralCity" + , NavLink "PSVR Git Repo" "https://gitea.werefoxsoftware.com/shadow8t4/Public-Speaking-VR" + , NavLink "PSVR Research Paper" "/assets/other/Public_Speaking_in_VR_Research_Paper.pdf" + ] + ) + ] + } + , welcome = welcomeBanner "Werefox Software" "A self-hosted portfolio site for and by Alex Huddleston" + , gallery = + initGallery + "Portfolio Gallery" + "A few projects I've worked on. The source code for many of these can be viewed on my self-hosted git service (hosted through Gitea)." + "https://gitea.werefoxsoftware.com" + "Git subdomain" + [ galleryLink + "angels-and-demons.png" + "A completed game board." + "Angels and Demons" + [ "Angels and Demons is a board game concept I created during my time in college, specifically" + ++ " during my Game History class. I decided on my own time to take up converting it to a" + ++ " video game. Right now, it's focused for mainly Android and WebGL builds. It's currently" + ++ " (as of June 2018) in very early development. The basic mechanics and logic work, but " + ++ " it can only be played locally with very little indication of turns or progress. There" + ++ " are also no instructions aside from a rough document I made for the class, which has" + ++ " been converted to the repository's readme." + ] + [ ( "Git Repo" + , "https://gitea.werefoxsoftware.com/shadow8t4/angels-and-demons" + ) + ] + , galleryLink + "projectundercover-guard.png" + "The guard's view in ProjectUndercover." + "ProjectUndercover - guard view" + [ "Project Undercover is a game about pretending to be an AI. Or at least -- that's part of it." + ++ " Players will compete against one another, with one side trying to blend in with a crowd" + ++ " of non-player characters, and the other side attempting to identify them. It takes place" + ++ " at a party, and the undercover players are agents trying to infiltrate and complete" + ++ " several missions before the guard, or overseer, catches them." + , "The overseer is limited" + ++ " by a set of cameras, and slowly receives information over the course of the game to" + ++ " help identify the agents, thus putting the heat on them." + , "This picture is from the perspective of one of the guard's cameras." + ] + [ ( "Git Repo" + , "https://gitea.werefoxsoftware.com/shadow8t4/Project-Undercover" + ) + ] + , galleryLink + "revival-survival.png" + "A game where you save a soul you accidentally reaped as the grim reaper." + "Revival Survival" + [ "This was my entry for Chillennium 2017, a game jam held and hosted by students at Texas A&M." + ++ " I was in a group with 3 other students, and mostly worked on the sound design and" + ++ " game mechanics from the game. The game was made using Unity." + , "In Revival Survival, you play as the Grim Reaper in a 2D platformer that has to return" + ++ " the soul of someone they accidentally reaped. Throughout the game you are met with" + ++ " adversaries - hellhounds that want to eat the soul you reaped and paladins who wish to avenge" + ++ " that soul you reaped." + , "You can find a link to play this game in browser on the nav bar at the top of this page, as well" + ++ " as the link below. I've also provided a link to the itch.io submission page, where you" + ++ " can download a Windows standalone copy of the game." + ] + [ ( "itch.io Page Link" + , "https://d4-team.itch.io/revival-survival" + ) + , ( " / " + , "/" + ) + , ( "In-browser Game Link" + , "/revival-survival" + ) + ] + , galleryLink + "mocha_bordered.png" + "The status screen of the game." + "Monster Chase" + [ "Monster Chase was a fitness game/app developed during my senior capstone class in college." + ++ " The development team consisted of me and 4 other computer science majors, and the" + ++ " objective was to create a fitness game designed to track users' steps through engaging" + ++ " gameplay of the user being virtually chased by a monster that they could customize and" + ++ " use to challenge other players online. The game makes use of the FitBit API if a device" + ++ " and subsequent account are available to help track data and utilizes and separate server" + ++ " to keep track of points for an online leaderboard functionality. While the finaly build" + ++ "is still in many cases a prototype compared to what we initially intended to develop, it" + ++ "is functional and the source code for both the app and server are available in their" + ++ "subsequent repositories." + , "During the development of the game, I worked mainly on the front-end design and functionality" + ++ " of the game through the Unity editor. Most scripted events and transitions in the front end" + ++ " were also programmed by me, or I at least had a hand in." + ] + [ ( "Git Repo " + , "https://gitea.werefoxsoftware.com/shadow8t4/MochaPine64Backup" + ) + , ( " / " + , "/" + ) + , ( "Server Git Repo" + , "https://gitea.werefoxsoftware.com/shadow8t4/MochaServerPine64Backup" + ) + ] + , galleryProject + "projectundercover-spy.png" + "A spy's view in ProjectUndercover." + "Project Undercover - spy view" + [ "This is another picture from Project Undercover, this time from the perspective" + ++ " of a spy. In this picture, the spy is finishing a waving interaction with an AI spy." + ] + , galleryLink + "public-speaking-vr.png" + "A Case Study on Public Speaking in Virtual Reality." + "Public Speaking in VR" + [ "In this project, and the resulting research paper, a group I was assigned in and I show the results of" + ++ " a case study on the effectiveness and realism of using Virtual Reality technology to simulate the" + ++ " experience of public speaking in an effort to practice one’s speech skills." + , "We used a Development" + ++ " Kit 2 version of the Oculus Rift and the Unity editor to create and run our simulation. In the" + ++ " simulation, we presented users a large classroom environment with a handful of characters to" + ++ " present a mock speech in front of. We provided the users with an Xbox game controller to allow" + ++ " for easier camera movement as well as a way to switch through the given slides in the mock presentation." + , "The resulting research paper from the case study and a link to the project's github page can be found" + ++ " below." + ] + [ ( "Git Repo " + , "https://gitea.werefoxsoftware.com/shadow8t4/Public-Speaking-VR" + ) + , ( " / " + , "/" + ) + , ( "Research Paper" + , "/assets/other/Public_Speaking_in_VR_Research_Paper.pdf" + ) + ] + , galleryLink + "re-procedural-city.png" + "An image of the re-implementation's output." + "Re:Procedural City" + [ "A Rust implementation of the Procedural City project. This was done to gain some insight" + ++ " into the programming language and to refamiliarize myself with the work itself. In" + ++ " addition, the project is done through solely open-source libraries as opposed to the" + ++ " ones used in the original project provided by my professor, meaning that documentation" + ++ " of the library functions is much more accessible." + ] + [ ( "Git Repo" + , "https://gitea.werefoxsoftware.com/shadow8t4/Re-ProceduralCity" + ) + ] + , galleryLink + "procedural-city.png" + "A picture of some output from ProceduralCity" + "Procedural City" + [ "In this project my partner, Jeremy Martin, and I presented a way of procedurally generating a basic city by" + ++ " creating an .obj mesh file using some given template meshes. The goal was that when given a" + ++ " user input population density and some template meshes, the program would output" + ++ " a resulting mesh of a procedurally generated “city” using the template" + ++ " meshes along with some defined rules. The program takes the input files and duplicates it at" + ++ " procedurally determined intersections, creating a basic city structure in the" + ++ " resulting output .obj mesh file." + , " Unfortunately, the program was unable to be fully" + ++ " completed before our deadline, and currently takes one input mesh file and duplicates it" + ++ " based on user-defined amounts of layers from a central point with user-defined spacing." + , "Specific documentation on how to operate the program is detailed on the respective github page." + ] + [ ( "Git Repo" + , "https://gitea.werefoxsoftware.com/shadow8t4/ProceduralCity" + ) + ] + , galleryLink + "so-bow-y-cute.png" + "A game where you become cute." + "So Bow-y Cute" + [ "This was my entry for Chillennium 2016, a game jam held and hosted by students at Texas A&M." + ++ " This was my first time at the game jam and I was randomly patnered with one other person," + ++ " who did the art for this game. Everything else was done by me. The game was made using Unity." + , "In So Bow-y Cute - When your parent decides to limit you cuteness potential by prohibiting all bows, you must" + ++ " rebel against their tyranny and become the cutest you possibly can. Be careful not to get" + ++ " caught! or else you're out of luck." + , "You can find a link to the itch.io page below." + ] + [ ( "itch.io Page Link" + , "https://double-darling-duo-deluxe.itch.io/so-bow-y-cute" + ) + ] + ] + , footer = + [ initFooterSection + "" + "" + "" + "/" + , initFooterSection + "" + "This website was programmed in elm with the help of my friend Riley." + "source code" + "https://gitea.werefox.dev/shadow8t4/portfolio-site" + , initFooterSection + "" + "" + "" + "/" + ] + } + , Task.perform WindowWidth (getViewport + |> Task.map (\viewport -> floor viewport.scene.width)) + ) + + +smallImagePath : String -> String +smallImagePath filename = + "/assets/images/420/" ++ filename + + +fullImagePath : String -> String +fullImagePath filename = + "/assets/images/full/" ++ filename + + +defaultDropdownConfig : DropdownConfig +defaultDropdownConfig = + { name = "dropdown1" + , event = OnClick + , attribute = class "dropdown-active" + , state = False + , message = ToggleDropdown 0 + } + + +initDropdown : Int -> NavItem -> NavItem +initDropdown index item = + case item of + Dropdown name config items -> + Dropdown + name + { config + | name = "dropdown" ++ (String.fromInt index) + , message = ToggleDropdown index + } + items + + InitDropdown name items -> + Dropdown + name + { defaultDropdownConfig + | name = "dropdown" ++ (String.fromInt index) + , message = ToggleDropdown index + } + items + + other -> + other + + +initDropdowns : List NavItem -> Array NavItem +initDropdowns = + Array.indexedMap initDropdown << Array.fromList + + +welcomeBanner : String -> String -> WelcomeBanner +welcomeBanner title description = + { title = title, description = description } + + +galleryImage : String -> String -> GalleryItem +galleryImage url text = + Image { url = url, mouseoverText = text } + + +galleryProject : String -> String -> String -> List String -> GalleryItem +galleryProject url text title description = + Project { url = url, mouseoverText = text, title = title, description = description } + + +galleryLink : String -> String -> String -> List String -> List ( String, String ) -> GalleryItem +galleryLink url text title description links = + ProjectLink { url = url, mouseoverText = text, title = title, description = description, links = links } + + +initGallery : String -> String -> String -> String -> List GalleryItem -> Gallery +initGallery title description url urltext images = + { title = title, description = description, url = url, urltext = urltext, columns = 1, images = images } + + +initFooterSection : String -> String -> String -> String -> FooterSection +initFooterSection title description link url = + { title = title, description = description, link = link, url = url } + + + +-- VIEW + + +view : Model -> List (Html Msg) +view model = + [ headerBar model + , welcome model.welcome + , viewGallery model.gallery + , footerSections model.footer + ] + + +welcome : WelcomeBanner -> Html Msg +welcome banner = + section [ class "welcome" ] + [ div [ class "welcome-wrapper" ] + [ div [ class "dimmer" ] + [ div [ class "centered" ] + [ article [ class "content" ] + [ h1 [ class "title" ] [ text banner.title ] + , p [ class "description" ] [ text banner.description ] + ] + ] + ] + ] + ] + + +headerBar : Model -> Html Msg +headerBar model = + if model.flags.mobile || model.nav.smallNav then + mobileHeaderbar model + else + desktopHeaderBar model + + +desktopHeaderBar : Model -> Html Msg +desktopHeaderBar model = + header [ class "desktop-header" ] + [ nav [ class "desktop-nav" ] + [ div [ class "desktop-nav-wrapper", class "nav-wrapper" ] + [ ul [] (Array.map navItem model.nav.items |> Array.toList) + ] + ] + ] + + +mobileHeaderbar : Model -> Html Msg +mobileHeaderbar model = + header [ class "mobile-header" ] + [ div + ([ class "show-nav", onClick (ToggleNav (not model.nav.showNav)) ] + ++ if model.nav.showNav then + [ class "active" ] + else + [] + ) + [ p [] + [ text model.nav.navText + , span [ class "carat" ] [] + ] + ] + , nav + ([ class "mobile-nav" ] + ++ if model.nav.showNav then + [] + else + [ class "hidden" ] + ) + [ div [ class "mobile-nav-wrapper", class "nav-wrapper" ] + [ ul [] (Array.map navItem model.nav.items |> Array.toList) + ] + ] + ] + + +homeLink : String -> String -> Html Msg +homeLink name url = + li [ class "home-item" ] + [ div [] + [ a [ class "nav-link", href url ] + [ div [ class "button-wrapper" ] + [ p [] [ text name ] + ] + ] + ] + ] + + +navItem : NavItem -> Html Msg +navItem item = + case item of + NavLink name url -> + navLink name url + + Dropdown name config items -> + navDropdown name config items + + HomeLink name url -> + homeLink name url + + _ -> + text "" + + +navLink : String -> String -> Html Msg +navLink name url = + li [ class "nav-item" ] + [ div [] + [ a [ class "nav-link", href url ] + [ div [ class "button-wrapper" ] + [ p [] [ text name ] + ] + ] + ] + ] + + +navDropdown : String -> DropdownConfig -> Array NavItem -> Html Msg +navDropdown name config items = + li [ class "nav-item" ] + [ dropdown div [] + [ + (toggle div + ([ class "button-wrapper" ] + ++ if config.state then + [ class "dropdown-active" ] + else + [] + ) + [ p [] + [ text name + , span [ class "carat" ] [] + ] + ] + ) + , (drawer ul [ class "dropdown" ] (Array.map subItem items |> Array.toList)) + ] + config.state + (dropdownConfig config) + ] + + +subItem : NavItem -> Html Msg +subItem item = + case item of + NavLink name url -> + li [ class "sub-item" ] + [ a [ class "nav-link", href url ] + [ p [] [ text name ] + ] + ] + + _ -> + text "" + + +dropdownConfig : DropdownConfig -> Dropdown.Config Msg +dropdownConfig config = + Dropdown.Config + config.name + config.event + config.attribute + config.message + + +viewGallery : Gallery -> Html Msg +viewGallery gallery = + let + arr : Array (List GalleryItem) + arr = + gallery.images + |> List.indexedMap (\index item -> ( index, item )) + |> List.foldl + (\( index, item ) acc -> + if (remainderBy gallery.columns index) < Array.length acc then + case + acc + |> Array.get (remainderBy gallery.columns index) + |> Maybe.map ((::) item) + of + Just column -> + Array.set (remainderBy gallery.columns index) column acc + + Nothing -> + acc + else + Array.push [ item ] acc + ) + Array.empty + in + div [ class "gallery" ] + [ section [ class "gallery-wrapper" ] + [ article [ class "gallery-info" ] + [ h2 [] [ text gallery.title ] + , p [] [ text gallery.description ] + , a [ href gallery.url ] [ text gallery.urltext ] + ] + , div [ class "gallery-columns" ] + (arr + |> Array.map (div [ class "gallery-column" ] << List.map displayImage << List.reverse) + |> Array.toList + ) + ] + ] + + +displayImage : GalleryItem -> Html Msg +displayImage item = + case item of + Image image -> + article [ class "gallery-image" ] + [ div [ class "gallery-image-wrapper" ] + [ a [ href (fullImagePath image.url) ] + [ img [ src (smallImagePath image.url), title image.mouseoverText ] [] + ] + ] + ] + + Project project -> + article [ class "gallery-image" ] + [ div [ class "gallery-image-wrapper" ] + [ a [ href (fullImagePath project.url) ] + [ img [ src (smallImagePath project.url), title project.mouseoverText ] [] + ] + , div [ class "gallery-image-info" ] + [ h4 [] [ text project.title ] + , div [] (List.map (\t -> p [] [ text t ]) project.description) + ] + ] + ] + + ProjectLink projectlink -> + article [ class "gallery-image" ] + [ div [ class "gallery-image-wrapper" ] + [ a [ href (fullImagePath projectlink.url) ] + [ img [ src (smallImagePath projectlink.url), title projectlink.mouseoverText ] [] + ] + , div [ class "gallery-image-info" ] + [ h4 [] [ text projectlink.title ] + , div [] (List.map (\t -> p [] [ text t ]) projectlink.description) + , p [] (List.map (\( t, url ) -> a [ href url ] [ text t ]) projectlink.links) + ] + ] + ] + + +footerSection : FooterSection -> Html Msg +footerSection item = + div [ class "footer-column" ] + [ article [ class "footer-column-wrapper" ] + [ h3 [] [ text item.title ] + , p [] [ text item.description ] + , p [] + [ a [ href item.url ] + [ text item.link ] + ] + ] + ] + + +footerSections : List FooterSection -> Html Msg +footerSections items = + footer [] + [ section [ class "footer-wrapper" ] (List.map footerSection items) + ] + + + +-- UPDATE + + +type Msg + = WindowWidth Int + | ToggleDropdown Int Bool + | ToggleNav Bool + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + WindowWidth x -> + ( { model + | gallery = updateGalleryWidth model.flags.mobile x model.gallery + , nav = updateNavWidth x model.nav + } + , Cmd.none + ) + + ToggleDropdown index state -> + ( { model | nav = updateDropdown index state model.nav } + , Cmd.none + ) + + ToggleNav state -> + ( { model | nav = toggleNav state model.nav } + , Cmd.none + ) + + +toggleNav : Bool -> Navigation -> Navigation +toggleNav state nav = + { nav | showNav = state } + + +updateGalleryWidth : Bool -> Int -> Gallery -> Gallery +updateGalleryWidth mobile width gallery = + if mobile then + gallery + else if width < 632 then + { gallery | columns = 1 } + else if width < 932 then + { gallery | columns = 2 } + else + { gallery | columns = 3 } + + +updateNavWidth : Int -> Navigation -> Navigation +updateNavWidth width nav = + if width < 700 then + { nav | smallNav = True } + else + { nav | smallNav = False, showNav = False } + + +updateDropdown : Int -> Bool -> Navigation -> Navigation +updateDropdown index state nav = + case Array.get index nav.items of + Just dropdown -> + case dropdown of + Dropdown name config items -> + { nav | items = Array.set index (Dropdown name { config | state = state } items) nav.items } + + _ -> + nav + + Nothing -> + nav + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions model = + onResize (\width _ -> WindowWidth width)