This commit is contained in:
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ap-host
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: which docker && docker --version
|
- run: which docker && docker --version
|
||||||
|
|||||||
15
Source/ProofOfConcept/Models/Token.cs
Normal file
15
Source/ProofOfConcept/Models/Token.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ProofOfConcept.Models;
|
||||||
|
|
||||||
|
public record Token
|
||||||
|
{
|
||||||
|
public string AccessToken { get; init; }
|
||||||
|
public DateTimeOffset Expires { get; init; }
|
||||||
|
public string TokenType { get; init; }
|
||||||
|
|
||||||
|
public Token(string AccessToken, int ExpiresIn, string TokenType, DateTimeOffset? received = null)
|
||||||
|
{
|
||||||
|
this.AccessToken = AccessToken;
|
||||||
|
this.Expires = received?.AddSeconds(ExpiresIn) ?? DateTimeOffset.Now.AddSeconds(ExpiresIn);
|
||||||
|
this.TokenType = TokenType;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Source/ProofOfConcept/Pages/Index.cshtml
Normal file
40
Source/ProofOfConcept/Pages/Index.cshtml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@page
|
||||||
|
@model ProofOfConcept.Pages.Index
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
|
<title>Launching soon!</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="assets/img/favicon.ico" />
|
||||||
|
<link href="assets/css/style.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--masthead-bg-color: rgba(0, 0, 0, 0.8) ;
|
||||||
|
--body-bg-color: #455A64 ;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
|
<!-- Background Video-->
|
||||||
|
<video class="bg-video" playsinline="playsinline" autoplay="autoplay" muted="muted" loop="loop"><source src="assets/mp4/bg.mp4" type="video/mp4" /></video>
|
||||||
|
<!-- Masthead-->
|
||||||
|
<div class="masthead">
|
||||||
|
<div class="masthead-content text-white">
|
||||||
|
<div class="container-fluid px-4 px-lg-0">
|
||||||
|
<h1 class="lh-1 mb-4">Our website is launching soon..</h1>
|
||||||
|
<p class="mb-5">We are working hard to finish the development of this site.</p>
|
||||||
|
<p class="footer"><a href='mailto:info@automatic-parking.app'>info@automatic-parking.app</a> | <a href='https://automatic-parking.app'>automatic-parking.app</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/main.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
Source/ProofOfConcept/Pages/Index.cshtml.cs
Normal file
11
Source/ProofOfConcept/Pages/Index.cshtml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace ProofOfConcept.Pages;
|
||||||
|
|
||||||
|
public class Index : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,45 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using ProofOfConcept.Services;
|
using ProofOfConcept.Services;
|
||||||
|
using ProofOfConcept.Utilities;
|
||||||
|
|
||||||
var builder = WebApplication.CreateSlimBuilder(args);
|
var builder = WebApplication.CreateSlimBuilder(args);
|
||||||
|
|
||||||
|
// Load static web assets manifest (referenced libs + your wwwroot)
|
||||||
|
builder.WebHost.UseStaticWebAssets();
|
||||||
|
|
||||||
// builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); });
|
// builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); });
|
||||||
|
|
||||||
|
// Add services
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
builder.Services.AddMediator();
|
builder.Services.AddMediator();
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddHybridCache();
|
builder.Services.AddHybridCache();
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.AddRazorPages();
|
||||||
|
|
||||||
|
// Add own services
|
||||||
builder.Services.AddSingleton<IMessageProcessor, MessageProcessor>();
|
builder.Services.AddSingleton<IMessageProcessor, MessageProcessor>();
|
||||||
|
builder.Services.AddTransient<TeslaAuthenticatorService>();
|
||||||
|
|
||||||
|
// Add hosted services
|
||||||
builder.Services.AddHostedService<MQTTServer>();
|
builder.Services.AddHostedService<MQTTServer>();
|
||||||
builder.Services.AddHostedService<MQTTClient>();
|
builder.Services.AddHostedService<MQTTClient>();
|
||||||
|
|
||||||
var app = builder.Build();
|
//Build app
|
||||||
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
|
app.MapGet("/GetPartnerAuthenticationToken", ([FromServices] TeslaAuthenticatorService service) => service.AcquirePartnerAuthenticationTokenAsync());
|
||||||
|
app.MapGet("/CheckRegisteredApplication", ([FromServices] TeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync());
|
||||||
|
app.MapGet("/RegisterApplication", ([FromServices] TeslaAuthenticatorService service) => service.RegisterApplicationAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
//Map tesla required public key file
|
//Map static assets
|
||||||
app.MapGet("/.well-known/appspecific/com.tesla.3p.public-key.pem", (IMemoryCache memoryCache) => memoryCache.GetOrCreateAsync("publicKeyCert", async (_) => await File.ReadAllTextAsync("Resources/Signature/public-key.pem")));
|
app.MapStaticAssets();
|
||||||
|
|
||||||
//Map an under constrcution page...
|
app.MapRazorPages();
|
||||||
app.Map("/", ()=> "Under construction...");
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
<PublishAot>true</PublishAot>
|
<!--<PublishAot>true</PublishAot>-->
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.7.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.7.0" />
|
||||||
<PackageReference Include="MQTTnet" Version="5.0.1.1416" />
|
<PackageReference Include="MQTTnet" Version="5.0.1.1416" />
|
||||||
<PackageReference Include="MQTTnet.Server" Version="5.0.1.1416" />
|
<PackageReference Include="MQTTnet.Server" Version="5.0.1.1416" />
|
||||||
|
<PackageReference Include="SzakatsA.Result" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -28,14 +29,9 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Models\" />
|
|
||||||
<Folder Include="Utilities\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Resources\Signature\public-key.pem">
|
<None Update="Resources\Signature\public-key.pem">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
153
Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs
Normal file
153
Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using ProofOfConcept.Models;
|
||||||
|
using ProofOfConcept.Utilities;
|
||||||
|
using SzakatsA.Result;
|
||||||
|
|
||||||
|
namespace ProofOfConcept.Services;
|
||||||
|
|
||||||
|
public class TeslaAuthenticatorService
|
||||||
|
{
|
||||||
|
private readonly ILogger<TeslaAuthenticatorService> logger;
|
||||||
|
private readonly TeslaAuthenticatorServiceConfiguration configuration;
|
||||||
|
private readonly IHttpClientFactory httpClientFactory;
|
||||||
|
private readonly IMemoryCache memoryCache;
|
||||||
|
|
||||||
|
const string authEndpointURL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token";
|
||||||
|
const string euBaseURL = "https://fleet-api.prd.eu.vn.cloud.tesla.com";
|
||||||
|
|
||||||
|
public TeslaAuthenticatorService(ILogger<TeslaAuthenticatorService> logger, IOptions<TeslaAuthenticatorServiceConfiguration> options, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.httpClientFactory = httpClientFactory;
|
||||||
|
this.memoryCache = memoryCache;
|
||||||
|
this.configuration = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronously retrieves an authentication token from the Tesla partner API.
|
||||||
|
/// This method sends a POST request with client credentials to obtain an
|
||||||
|
/// OAuth2 access token that grants access to Tesla partner services.
|
||||||
|
/// <return>Returns a string containing the authentication token received from the API.</return>
|
||||||
|
/// <exception cref="HttpRequestException">Thrown when the HTTP request to the API fails.</exception>
|
||||||
|
public async Task<Token> AcquirePartnerAuthenticationTokenAsync()
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Acquiring authentication token from Tesla partner API...");
|
||||||
|
|
||||||
|
using HttpClient client = httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
// URL to request the token
|
||||||
|
const string audience = euBaseURL;
|
||||||
|
|
||||||
|
// Prepare the form-urlencoded body
|
||||||
|
Dictionary<string, string> formData = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "grant_type", "client_credentials" },
|
||||||
|
{ "client_id", this.configuration.ClientID },
|
||||||
|
{ "client_secret", this.configuration.ClientSecret },
|
||||||
|
{ "scope", "openid offline_access vehicle_device_data vehicle_location" },
|
||||||
|
{ "audience", audience }
|
||||||
|
};
|
||||||
|
|
||||||
|
FormUrlEncodedContent content = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
// Send POST request
|
||||||
|
HttpResponseMessage response = await client.PostAsync(authEndpointURL, content);
|
||||||
|
this.logger.LogTrace("Response status code: {0}", response.StatusCode);
|
||||||
|
|
||||||
|
// Throw if not successful
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
// Read the response body as a string
|
||||||
|
string json = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Deserialize
|
||||||
|
Token? result = JsonSerializer.Deserialize<Token>(json);
|
||||||
|
|
||||||
|
if (result is null)
|
||||||
|
throw new FormatException($"Could not deserialize token response: {json}");
|
||||||
|
|
||||||
|
// Return the token response
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a cached partner authentication token or acquires a new one if not available.
|
||||||
|
/// This method first attempts to fetch the token from the memory cache. If the token is not found
|
||||||
|
/// or has expired, it invokes the method to retrieve a new token from the Tesla partner API and stores it in the cache.
|
||||||
|
/// The cached token is set to expire slightly earlier than its actual expiration time to avoid unexpected token expiry during usage.
|
||||||
|
/// <return>Returns a Token object containing the authentication token and its expiration details.</return>
|
||||||
|
/// <exception cref="HttpRequestException">Thrown when the HTTP request to acquire a new partner token fails.</exception>
|
||||||
|
/// <exception cref="Exception">Thrown for any unexpected errors during the token retrieval or caching process.</exception>
|
||||||
|
public async Task<Token> GetPartnerAuthenticationTokenAsync()
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Getting partner authentication token...");
|
||||||
|
|
||||||
|
//Token is available in cache
|
||||||
|
if (this.memoryCache.TryGetValue(Keys.TeslaPartnerToken, out Token? token) && token is not null)
|
||||||
|
{
|
||||||
|
this.logger.LogInformation("Partner authentication token provided from cache");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Acquire token
|
||||||
|
token = await AcquirePartnerAuthenticationTokenAsync();
|
||||||
|
|
||||||
|
//Save to cache
|
||||||
|
this.memoryCache.Set(Keys.TeslaPartnerToken, token, token.Expires.Subtract(this.configuration.MemoryCacheDelta));
|
||||||
|
|
||||||
|
this.logger.LogInformation("Partner authentication token provided provided by acquiring from API");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> RegisterApplicationAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Registering application to Tesla fleet API...");
|
||||||
|
|
||||||
|
//Create client and get partner token
|
||||||
|
HttpClient httpClient = httpClientFactory.CreateClient();
|
||||||
|
Token partnerToken = await GetPartnerAuthenticationTokenAsync();
|
||||||
|
|
||||||
|
//Set headers
|
||||||
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerToken.AccessToken);
|
||||||
|
|
||||||
|
//Prepare body
|
||||||
|
var postBody = new { domain = this.configuration.Domain };
|
||||||
|
|
||||||
|
//Send request
|
||||||
|
HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{euBaseURL}/api/1/partner_accounts", postBody, cancellationToken);
|
||||||
|
bool success = response.IsSuccessStatusCode;
|
||||||
|
|
||||||
|
logger.LogInformation("Application registration result: {Success}", success ? "Success" : "Failed");
|
||||||
|
return new Result(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<bool>> CheckApplicationRegistrationAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.logger.LogTrace("Checking application registration...");
|
||||||
|
|
||||||
|
//Create client and get partner token
|
||||||
|
HttpClient httpClient = httpClientFactory.CreateClient();
|
||||||
|
Token partnerToken = await GetPartnerAuthenticationTokenAsync();
|
||||||
|
|
||||||
|
//Set headers
|
||||||
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerToken.AccessToken);
|
||||||
|
|
||||||
|
//Send request
|
||||||
|
HttpResponseMessage response = await httpClient.GetAsync($"{euBaseURL}/api/1/partner_accounts", cancellationToken);
|
||||||
|
string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Application registration result: {Result}", responseBody);
|
||||||
|
return new Result<bool>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TeslaAuthenticatorServiceConfiguration
|
||||||
|
{
|
||||||
|
public string ClientID { get; set; } = "b2240ee4-332a-4252-91aa-bbcc24f78fdb";
|
||||||
|
public string ClientSecret { get; set; } = "ta-secret.YG+XSdlvr6Lv8U-x";
|
||||||
|
public TimeSpan MemoryCacheDelta { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
public string Domain { get; set; } = "tesla-connector.automatic-parking.app";
|
||||||
|
}
|
||||||
7
Source/ProofOfConcept/Utilities/Keys.cs
Normal file
7
Source/ProofOfConcept/Utilities/Keys.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ProofOfConcept.Utilities;
|
||||||
|
|
||||||
|
public static class Keys
|
||||||
|
{
|
||||||
|
public const string PublicKeyCert = "PublicKeyCert";
|
||||||
|
public const string TeslaPartnerToken = "PartnerToken";
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/2+0ZNUwcsa7C2kkQa6J4egse3+N
|
||||||
|
TnFd7DKlLOcEZGrCBn/mfxsZUpQoVCeoh4j6w5ue471Ott70qpsRuOtjbQ==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
12324
Source/ProofOfConcept/wwwroot/assets/css/style.min.css
vendored
Normal file
12324
Source/ProofOfConcept/wwwroot/assets/css/style.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Source/ProofOfConcept/wwwroot/assets/img/favicon.ico
Normal file
BIN
Source/ProofOfConcept/wwwroot/assets/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
67
Source/ProofOfConcept/wwwroot/assets/js/main.min.js
vendored
Normal file
67
Source/ProofOfConcept/wwwroot/assets/js/main.min.js
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
let c = i('canvas'),
|
||||||
|
w = (canvas.width = window.innerWidth),
|
||||||
|
h = (canvas.height = window.innerHeight);
|
||||||
|
class firefly {
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.x = Math.random() * w;
|
||||||
|
this.y = Math.random() * h;
|
||||||
|
this.s = Math.random() * 2;
|
||||||
|
this.ang = Math.random() * 2 * Math.PI;
|
||||||
|
this.v = this.s * this.s / 4
|
||||||
|
}
|
||||||
|
move()
|
||||||
|
{
|
||||||
|
this.x += this.v * Math.cos(this.ang);
|
||||||
|
this.y += this.v * Math.sin(this.ang);
|
||||||
|
this.ang += Math.random() * 20 * Math.PI / 180 - 10 * Math.PI / 180
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
{
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(this.x, this.y, this.s, 0, 2 * Math.PI);
|
||||||
|
c.fillStyle = '#fddba3';
|
||||||
|
c.fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let f = [];
|
||||||
|
function a() {
|
||||||
|
if (f.length < 500)
|
||||||
|
for (let j = 0; j < 10; j++)
|
||||||
|
f.push(new firefly());
|
||||||
|
for (let i = 0; i < f.length; i++) {
|
||||||
|
f[i].move();
|
||||||
|
f[i].show();
|
||||||
|
f[i].x < 0 || f[i].x > w || f[i].y < 0 || f[i].y > h && f.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let g = {};
|
||||||
|
let A = {};
|
||||||
|
canvas.addEventListener('mousemove', function(e) {
|
||||||
|
A.x = g.x;
|
||||||
|
A.y = g.y;
|
||||||
|
g.x = e.pageX - this.offsetLeft;
|
||||||
|
g.y = e.pageY - this.offsetTop
|
||||||
|
}, !1);
|
||||||
|
function i(_) {
|
||||||
|
let b = document.getElementById(_),
|
||||||
|
c = b.getContext('2d');
|
||||||
|
c.fillStyle = 'rgba(30,30,30,1)';
|
||||||
|
c.fillRect(0, 0, (b.width = window.innerWidth), (b.height = window.innerHeight));
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
window.requestAnimFrame = (() => (window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(B) {
|
||||||
|
window.setTimeout(B)
|
||||||
|
}));
|
||||||
|
function j() {
|
||||||
|
window.requestAnimFrame(j);
|
||||||
|
c.clearRect(0, 0, w, h);
|
||||||
|
a()
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
((w = canvas.width = window.innerWidth),
|
||||||
|
(h = canvas.height = window.innerHeight));
|
||||||
|
j()
|
||||||
|
});
|
||||||
|
j();
|
||||||
|
setInterval(j, 1000 / 60);
|
||||||
Reference in New Issue
Block a user