diff --git a/EnvelopeGenerator.Web/Controllers/HomeController.cs b/EnvelopeGenerator.Web/Controllers/HomeController.cs index cb82faa9..b6275548 100644 --- a/EnvelopeGenerator.Web/Controllers/HomeController.cs +++ b/EnvelopeGenerator.Web/Controllers/HomeController.cs @@ -6,7 +6,6 @@ using EnvelopeGenerator.Web.Services; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; -using System.Diagnostics; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using DigitalData.Core.API; @@ -17,16 +16,14 @@ namespace EnvelopeGenerator.Web.Controllers public class HomeController : BaseController { private readonly EnvelopeOldService envelopeOldService; - private readonly IConfiguration _config; private readonly IEnvelopeReceiverService _envRcvService; private readonly IEnvelopeService _envelopeService; private readonly IEnvelopeHistoryService _historyService; - public HomeController(DatabaseService databaseService, EnvelopeOldService envelopeOldService, ILogger logger, IConfiguration configuration, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeService envelopeService, IEnvelopeHistoryService historyService) : base(databaseService, logger) + public HomeController(DatabaseService databaseService, EnvelopeOldService envelopeOldService, ILogger logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeService envelopeService, IEnvelopeHistoryService historyService) : base(databaseService, logger) { this.envelopeOldService = envelopeOldService; _envRcvService = envelopeReceiverService; _envelopeService = envelopeService; - _config = configuration; _historyService = historyService; } @@ -68,12 +65,22 @@ namespace EnvelopeGenerator.Web.Controllers _logger.LogError(ex, MessageKey.UnexpectedError.ToString()); } - return View("EnvelopeLocked").WithData("EnvelopeKey", envelopeReceiverId); + return Redirect($"{envelopeReceiverId}/Locked"); } [HttpGet("EnvelopeKey/{envelopeReceiverId}/Locked")] public IActionResult EnvelopeLocked([FromRoute] string envelopeReceiverId) { + (string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId(); + if (uuid is null || signature is null) + { + return View("_Error", new ErrorViewModel() + { + Title = "404", + Subtitle = "Anfrage fehlgeschlagen!", + Body = "Das angeforderte Umschlag wurde nicht gefunden." + }); + } return View().WithData("EnvelopeKey", envelopeReceiverId); } @@ -87,7 +94,7 @@ namespace EnvelopeGenerator.Web.Controllers if(uuid is null || signature is null) { _logger.LogWarning($"{MessageKey.WrongEnvelopeReceiverId.ToString()}"); - return BadRequest(_envelopeService.CreateMessage(false, MessageKey.WrongEnvelopeReceiverId.ToString())); + return BadRequest(_envelopeService.CreateMessage(false, MessageKey.WrongEnvelopeReceiverId.ToString())); } _logger.LogInformation($"Envelope UUID: [{uuid}]\nReceiver Signature: [{signature}]"); @@ -182,7 +189,7 @@ namespace EnvelopeGenerator.Web.Controllers [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + return View(); } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj index 7cd5767a..4df31124 100644 --- a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj +++ b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/EnvelopeGenerator.Web/Models/ContactLink.cs b/EnvelopeGenerator.Web/Models/ContactLink.cs new file mode 100644 index 00000000..8221d4b4 --- /dev/null +++ b/EnvelopeGenerator.Web/Models/ContactLink.cs @@ -0,0 +1,60 @@ +namespace EnvelopeGenerator.Web.Models +{ + /// + /// Represents a hyperlink for contact purposes with various HTML attributes. + /// + public class ContactLink + { + /// + /// Gets or sets the label of the hyperlink. + /// + public string Label { get; init; } = "Contact"; + + /// + /// Gets or sets the URL that the hyperlink points to. + /// + public string Href { get; set; } = string.Empty; + + /// + /// Gets or sets the target where the hyperlink should open. + /// Commonly used values are "_blank", "_self", "_parent", "_top". + /// + public string Target { get; set; } = "_blank"; + + /// + /// Gets or sets the relationship of the linked URL as space-separated link types. + /// Examples include "nofollow", "noopener", "noreferrer". + /// + public string Rel { get; set; } = string.Empty; + + /// + /// Gets or sets the filename that should be downloaded when clicking the hyperlink. + /// This attribute will only have an effect if the href attribute is set. + /// + public string Download { get; set; } = string.Empty; + + /// + /// Gets or sets the language of the linked resource. Useful when linking to + /// content in another language. + /// + public string HrefLang { get; set; } = "en"; + + /// + /// Gets or sets the MIME type of the linked URL. Helps browsers to handle + /// the type correctly when the link is clicked. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets additional information about the hyperlink, typically viewed + /// as a tooltip when the mouse hovers over the link. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets an identifier for the hyperlink, unique within the HTML document. + /// + public string Id { get; set; } = string.Empty; + } + +} diff --git a/EnvelopeGenerator.Web/Models/ErrorViewModel.cs b/EnvelopeGenerator.Web/Models/ErrorViewModel.cs index a4489520..2173e705 100644 --- a/EnvelopeGenerator.Web/Models/ErrorViewModel.cs +++ b/EnvelopeGenerator.Web/Models/ErrorViewModel.cs @@ -2,8 +2,10 @@ namespace EnvelopeGenerator.Web.Models { public class ErrorViewModel { - public string? RequestId { get; set; } + public string Title { get; init; } = "404"; - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + public string Subtitle { get; init; } = "Hmmm..."; + + public string Body { get; init; } = "It looks like one of the developers fell asleep"; } } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Program.cs b/EnvelopeGenerator.Web/Program.cs index dae95eb4..0d424845 100644 --- a/EnvelopeGenerator.Web/Program.cs +++ b/EnvelopeGenerator.Web/Program.cs @@ -14,6 +14,7 @@ using DigitalData.Core.API; using Microsoft.AspNetCore.Authentication.Cookies; using DigitalData.Core.Application; using DigitalData.UserManager.Application.MappingProfiles; +using EnvelopeGenerator.Web.Models; var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); logger.Info("Logging initialized!"); @@ -105,10 +106,10 @@ try builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { - options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security + options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites - // Set up event handlers for dynamic login and logout paths + options.Events = new CookieAuthenticationEvents { OnRedirectToLogin = context => @@ -132,6 +133,8 @@ try }; }); + builder.Services.AddSingleton(_ => builder.Configuration.GetSection("ContactLink").Get() ?? new ContactLink()); + builder.Services.AddCookieConsentSettings(); var app = builder.Build(); diff --git a/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml b/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml index 0e824a25..c3de39c5 100644 --- a/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/EnvelopeLocked.cshtml @@ -1,18 +1,6 @@ @{ ViewData["Title"] = "Dokument geschützt"; } -@if(ViewData["Test1"] is string test1) -{ - -} -@if (ViewData["Test2"] is string test2) -{ - -}
@@ -32,7 +20,7 @@
- +
diff --git a/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml b/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml index 613fba3e..7b339119 100644 --- a/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml +++ b/EnvelopeGenerator.Web/Views/Home/ShowEnvelope.cshtml @@ -38,7 +38,7 @@
@($"{envelope.Title}")

@($"Sie haben {(pages.Count())} Briefe zu unterschreiben. Bitte prüfen Sie die Seiten {stPageIndexes}.")

-

Erstellt am @envelope.AddedWhen

+

Erstellt am @envelope.AddedWhen von @sender?.Prename @sender?.Name. Sie können den Absender über @sender?.Email kontaktieren.

@@ -63,9 +63,9 @@ { var envelopeResponse = ViewData["EnvelopeResponse"]; var settings = new Newtonsoft.Json.JsonSerializerSettings - { - ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() - }; + { + ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() + }; var envelopeResponseJson = Newtonsoft.Json.JsonConvert.SerializeObject(envelopeResponse, settings); var documentBase64String = Convert.ToBase64String(documentBytes); diff --git a/EnvelopeGenerator.Web/Views/Shared/Error.cshtml b/EnvelopeGenerator.Web/Views/Shared/Error.cshtml deleted file mode 100644 index a1e04783..00000000 --- a/EnvelopeGenerator.Web/Views/Shared/Error.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@model ErrorViewModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

diff --git a/EnvelopeGenerator.Web/Views/Shared/_Error.cshtml b/EnvelopeGenerator.Web/Views/Shared/_Error.cshtml new file mode 100644 index 00000000..f72914f1 --- /dev/null +++ b/EnvelopeGenerator.Web/Views/Shared/_Error.cshtml @@ -0,0 +1,69 @@ +@{ + Layout = null; +} +@model ErrorViewModel +@{ + var errorModel = Model ?? new ErrorViewModel(); +} +@inject ContactLink _contactLink + + + + + + + Document + + + + +
+
+
+
+ +
+
+
+
+
+ +
+
@errorModel.Title
+
@errorModel.Subtitle
+
@errorModel.Body
+ @_contactLink.Label +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/EnvelopeGenerator.Web/Views/_ViewStart.cshtml b/EnvelopeGenerator.Web/Views/_ViewStart.cshtml index a5f10045..1af6e494 100644 --- a/EnvelopeGenerator.Web/Views/_ViewStart.cshtml +++ b/EnvelopeGenerator.Web/Views/_ViewStart.cshtml @@ -1,3 +1,3 @@ @{ Layout = "_Layout"; -} +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/appsettings.json b/EnvelopeGenerator.Web/appsettings.json index fd26c28d..5dcb02dc 100644 --- a/EnvelopeGenerator.Web/appsettings.json +++ b/EnvelopeGenerator.Web/appsettings.json @@ -28,7 +28,7 @@ "maxArchiveDays": 30 } }, - // Trace, Debug, Info, Warn, Error and Fatal + // Trace, Debug, Info, Warn, Error and *Fatal* "rules": [ { "logger": "*", @@ -80,5 +80,12 @@ "ModalId": "bootstrapCookieConsentSettingsModal", "AlsoUseLocalStorage": false, "Categories": [ "necessary" ] + }, + "ContactLink": { + "Label": "Kontakt", + "Href": "https://digitaldata.works/", + "HrefLang": "de", + "Target": "_blank", + "Title": "Digital Data GmbH" } } \ No newline at end of file diff --git a/EnvelopeGenerator.Web/wwwroot/css/error-space.css b/EnvelopeGenerator.Web/wwwroot/css/error-space.css new file mode 100644 index 00000000..1cf74752 --- /dev/null +++ b/EnvelopeGenerator.Web/wwwroot/css/error-space.css @@ -0,0 +1,386 @@ +html, +body { + height: 100%; + width: 100%; + margin: 0px; + background: linear-gradient(90deg, rgba(47, 54, 64, 1) 23%, rgba(24, 27, 32, 1) 100%); +} + +.moon { + background: linear-gradient(90deg, rgba(208, 208, 208, 1) 48%, rgba(145, 145, 145, 1) 100%); + position: absolute; + top: -100px; + left: -300px; + width: 900px; + height: 900px; + content: ''; + border-radius: 100%; + box-shadow: 0px 0px 30px -4px rgba(0, 0, 0, 0.5); +} + +.moon__crater { + position: absolute; + content: ''; + border-radius: 100%; + background: linear-gradient(90deg, rgba(122, 122, 122, 1) 38%, rgba(195, 195, 195, 1) 100%); + opacity: 0.6; +} + +.moon__crater1 { + top: 250px; + left: 500px; + width: 60px; + height: 180px; +} + +.moon__crater2 { + top: 650px; + left: 340px; + width: 40px; + height: 80px; + transform: rotate(55deg); +} + +.moon__crater3 { + top: -20px; + left: 40px; + width: 65px; + height: 120px; + transform: rotate(250deg); +} + +.star { + background: grey; + position: absolute; + width: 5px; + height: 5px; + content: ''; + border-radius: 100%; + transform: rotate(250deg); + opacity: 0.4; + animation-name: shimmer; + animation-duration: 1.5s; + animation-iteration-count: infinite; + animation-direction: alternate; +} + +@keyframes shimmer { + from { + opacity: 0; + } + + to { + opacity: 0.7; + } +} + +.star1 { + top: 40%; + left: 50%; + animation-delay: 1s; +} + +.star2 { + top: 60%; + left: 90%; + animation-delay: 3s; +} + +.star3 { + top: 10%; + left: 70%; + animation-delay: 2s; +} + +.star4 { + top: 90%; + left: 40%; +} + +.star5 { + top: 20%; + left: 30%; + animation-delay: 0.5s; +} + +.error { + position: absolute; + left: 100px; + top: 400px; + transform: translateY(-60%); + font-family: 'Montserrat', sans-serif; + color: #363e49; +} + +.error__title { + font-size: 10em; +} + +.error__subtitle { + font-size: 2em; +} + +.error__description { + opacity: 0.5; +} + +.error__button { + display: inline-block; + margin-top: 3em; + margin-right: 0.5em; + padding: 0.5em 2em; + outline: none; + border: 2px solid #2f3640; + background-color: transparent; + border-radius: 8em; + color: #576375; + cursor: pointer; + transition-duration: 0.2s; + font-size: 0.75em; + font-family: 'Montserrat', sans-serif; + text-decoration: none; +} + +.error__button:hover { + color: #21252c; +} + +.error__button--active { + background-color: #e67e22; + border: 2px solid #e67e22; + color: white; +} + +.error__button--active:hover { + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.5); + color: white; +} + +.astronaut { + position: absolute; + width: 185px; + height: 300px; + left: 70%; + top: 50%; + transform: translate(-50%, -50%) rotate(20deg) scale(1.2); +} + +.astronaut__head { + background-color: white; + position: absolute; + top: 60px; + left: 60px; + width: 60px; + height: 60px; + content: ''; + border-radius: 2em; +} + +.astronaut__head-visor-flare1 { + background-color: #7f8fa6; + position: absolute; + top: 28px; + left: 40px; + width: 10px; + height: 10px; + content: ''; + border-radius: 2em; + opacity: 0.5; +} + +.astronaut__head-visor-flare2 { + background-color: #718093; + position: absolute; + top: 40px; + left: 38px; + width: 5px; + height: 5px; + content: ''; + border-radius: 2em; + opacity: 0.3; +} + +.astronaut__backpack { + background-color: #bfbfbf; + position: absolute; + top: 90px; + left: 47px; + width: 86px; + height: 90px; + content: ''; + border-radius: 8px; +} + +.astronaut__body { + background-color: #e6e6e6; + position: absolute; + top: 115px; + left: 55px; + width: 70px; + height: 80px; + content: ''; + border-radius: 8px; +} + +.astronaut__body__chest { + background-color: #d9d9d9; + position: absolute; + top: 140px; + left: 68px; + width: 45px; + height: 25px; + content: ''; + border-radius: 6px; +} + +.astronaut__arm-left1 { + background-color: #e6e6e6; + position: absolute; + top: 127px; + left: 9px; + width: 65px; + height: 20px; + content: ''; + border-radius: 8px; + transform: rotate(-30deg); +} + +.astronaut__arm-left2 { + background-color: #e6e6e6; + position: absolute; + top: 102px; + left: 7px; + width: 20px; + height: 45px; + content: ''; + border-radius: 8px; + transform: rotate(-12deg); + border-top-left-radius: 8em; + border-top-right-radius: 8em; +} + +.astronaut__arm-right1 { + background-color: #e6e6e6; + position: absolute; + top: 113px; + left: 100px; + width: 65px; + height: 20px; + content: ''; + border-radius: 8px; + transform: rotate(-10deg); +} + +.astronaut__arm-right2 { + background-color: #e6e6e6; + position: absolute; + top: 78px; + left: 141px; + width: 20px; + height: 45px; + content: ''; + border-radius: 8px; + transform: rotate(-10deg); + border-top-left-radius: 8em; + border-top-right-radius: 8em; +} + +.astronaut__arm-thumb-left { + background-color: #e6e6e6; + position: absolute; + top: 110px; + left: 21px; + width: 10px; + height: 6px; + content: ''; + border-radius: 8em; + transform: rotate(-35deg); +} + +.astronaut__arm-thumb-right { + background-color: #e6e6e6; + position: absolute; + top: 90px; + left: 133px; + width: 10px; + height: 6px; + content: ''; + border-radius: 8em; + transform: rotate(20deg); +} + +.astronaut__wrist-left { + background-color: #e67e22; + position: absolute; + top: 122px; + left: 6.5px; + width: 21px; + height: 4px; + content: ''; + border-radius: 8em; + transform: rotate(-15deg); +} + +.astronaut__wrist-right { + background-color: #e67e22; + position: absolute; + top: 98px; + left: 141px; + width: 21px; + height: 4px; + content: ''; + border-radius: 8em; + transform: rotate(-10deg); +} + +.astronaut__leg-left { + background-color: #e6e6e6; + position: absolute; + top: 188px; + left: 50px; + width: 23px; + height: 75px; + content: ''; + transform: rotate(10deg); +} + +.astronaut__leg-right { + background-color: #e6e6e6; + position: absolute; + top: 188px; + left: 108px; + width: 23px; + height: 75px; + content: ''; + transform: rotate(-10deg); +} + +.astronaut__foot-left { + background-color: white; + position: absolute; + top: 240px; + left: 43px; + width: 28px; + height: 20px; + content: ''; + transform: rotate(10deg); + border-radius: 3px; + border-top-left-radius: 8em; + border-top-right-radius: 8em; + border-bottom: 4px solid #e67e22; +} + +.astronaut__foot-right { + background-color: white; + position: absolute; + top: 240px; + left: 111px; + width: 28px; + height: 20px; + content: ''; + transform: rotate(-10deg); + border-radius: 3px; + border-top-left-radius: 8em; + border-top-right-radius: 8em; + border-bottom: 4px solid #e67e22; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Web/wwwroot/error.html b/EnvelopeGenerator.Web/wwwroot/error.html new file mode 100644 index 00000000..41ba16de --- /dev/null +++ b/EnvelopeGenerator.Web/wwwroot/error.html @@ -0,0 +1,61 @@ + + + + + + + Document + + + + +
+
+
+
+ +
+
+
+
+
+ +
+
404
+
Hmmm...
+
It looks like one of the developers fell asleep
+ CONTACT +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/EnvelopeGenerator.Web/wwwroot/js/app.js b/EnvelopeGenerator.Web/wwwroot/js/app.js index a0e021c4..2756c48f 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/app.js +++ b/EnvelopeGenerator.Web/wwwroot/js/app.js @@ -34,7 +34,6 @@ class App { this.currentReceiver = this.envelopeResponse.receiver // Load the document from the filestore - console.debug('Loading document from filestore') const documentResponse = this.documentBytes if (documentResponse.fatal || documentResponse.error) { @@ -45,11 +44,9 @@ class App { icon: 'error', }) } - console.log(documentResponse.data) - console.log(this.documentBytes) const arrayBuffer = this.documentBytes - console.log(arrayBuffer) + // Load PSPDFKit this.Instance = await this.UI.loadPSPDFKit(arrayBuffer, this.container) this.UI.configurePSPDFKit(this.Instance, this.handleClick.bind(this)) diff --git a/EnvelopeGenerator.Web/wwwroot/js/error-space.js b/EnvelopeGenerator.Web/wwwroot/js/error-space.js new file mode 100644 index 00000000..80f1cae3 --- /dev/null +++ b/EnvelopeGenerator.Web/wwwroot/js/error-space.js @@ -0,0 +1,77 @@ +function drawVisor() { + const canvas = document.getElementById('visor'); + const ctx = canvas.getContext('2d'); + + ctx.beginPath(); + ctx.moveTo(5, 45); + ctx.bezierCurveTo(15, 64, 45, 64, 55, 45); + + ctx.lineTo(55, 20); + ctx.bezierCurveTo(55, 15, 50, 10, 45, 10); + + ctx.lineTo(15, 10); + + ctx.bezierCurveTo(15, 10, 5, 10, 5, 20); + ctx.lineTo(5, 45); + + ctx.fillStyle = '#2f3640'; + ctx.strokeStyle = '#f5f6fa'; + ctx.fill(); + ctx.stroke(); +} + +const cordCanvas = document.getElementById('cord'); +const ctx = cordCanvas.getContext('2d'); + +let y1 = 160; +let y2 = 100; +let y3 = 100; + +let y1Forward = true; +let y2Forward = false; +let y3Forward = true; + +function animate() { + requestAnimationFrame(animate); + ctx.clearRect(0, 0, innerWidth, innerHeight); + + ctx.beginPath(); + ctx.moveTo(130, 170); + ctx.bezierCurveTo(250, y1, 345, y2, 400, y3); + + ctx.strokeStyle = 'white'; + ctx.lineWidth = 8; + ctx.stroke(); + + + if (y1 === 100) { + y1Forward = true; + } + + if (y1 === 300) { + y1Forward = false; + } + + if (y2 === 100) { + y2Forward = true; + } + + if (y2 === 310) { + y2Forward = false; + } + + if (y3 === 100) { + y3Forward = true; + } + + if (y3 === 317) { + y3Forward = false; + } + + y1Forward ? y1 += 1 : y1 -= 1; + y2Forward ? y2 += 1 : y2 -= 1; + y3Forward ? y3 += 1 : y3 -= 1; +} + +drawVisor(); +animate(); \ No newline at end of file