create-gametau 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-gametau — Scaffold a Tauri game with web + desktop deployment.
4
+ *
5
+ * Usage:
6
+ * bunx create-gametau my-game
7
+ * bunx create-gametau my-game --template pixi
8
+ * bunx create-gametau my-game --template vanilla
9
+ */
10
+ declare const TEMPLATES: readonly ["three", "pixi", "vanilla"];
11
+ type Template = (typeof TEMPLATES)[number];
12
+ interface Options {
13
+ projectName: string;
14
+ template: Template;
15
+ }
16
+ export declare function scaffold(options: Options, cwd?: string): void;
17
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-gametau — Scaffold a Tauri game with web + desktop deployment.
4
+ *
5
+ * Usage:
6
+ * bunx create-gametau my-game
7
+ * bunx create-gametau my-game --template pixi
8
+ * bunx create-gametau my-game --template vanilla
9
+ */
10
+ import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, cpSync } from "fs";
11
+ import { join, resolve, dirname } from "path";
12
+ import { fileURLToPath } from "url";
13
+ const TEMPLATES = ["three", "pixi", "vanilla"];
14
+ function parseArgs(args) {
15
+ const positional = [];
16
+ let template = "three";
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+ if (arg === "--template" || arg === "-t") {
20
+ const next = args[++i];
21
+ if (!next || !TEMPLATES.includes(next)) {
22
+ console.error(`Invalid template: ${next}`);
23
+ console.error(`Available: ${TEMPLATES.join(", ")}`);
24
+ process.exit(1);
25
+ }
26
+ template = next;
27
+ }
28
+ else if (arg === "--help" || arg === "-h") {
29
+ printHelp();
30
+ process.exit(0);
31
+ }
32
+ else if (!arg.startsWith("-")) {
33
+ positional.push(arg);
34
+ }
35
+ }
36
+ if (positional.length === 0) {
37
+ console.error("Error: project name required.\n");
38
+ printHelp();
39
+ process.exit(1);
40
+ }
41
+ return { projectName: positional[0], template };
42
+ }
43
+ function printHelp() {
44
+ console.log(`
45
+ create-gametau — Scaffold a Tauri game with web + desktop deployment
46
+
47
+ Usage:
48
+ bunx create-gametau <project-name> [options]
49
+
50
+ Options:
51
+ --template, -t Template to use: three (default), pixi, vanilla
52
+ --help, -h Show this help message
53
+
54
+ Examples:
55
+ bunx create-gametau my-game
56
+ bunx create-gametau my-game --template pixi
57
+ bun create gametau my-game
58
+ `.trim());
59
+ }
60
+ function getTemplatesDir() {
61
+ // Works both when running from source (dev) and from dist (published)
62
+ const thisFile = fileURLToPath(import.meta.url);
63
+ const packageRoot = resolve(dirname(thisFile), "..");
64
+ return join(packageRoot, "templates");
65
+ }
66
+ export function scaffold(options, cwd) {
67
+ const { projectName, template } = options;
68
+ const targetDir = resolve(cwd || process.cwd(), projectName);
69
+ if (existsSync(targetDir)) {
70
+ const contents = readdirSync(targetDir);
71
+ if (contents.length > 0) {
72
+ throw new Error(`directory "${projectName}" already exists and is not empty.`);
73
+ }
74
+ }
75
+ const templatesDir = getTemplatesDir();
76
+ const baseDir = join(templatesDir, "base");
77
+ const overlayDir = join(templatesDir, template);
78
+ if (!existsSync(baseDir)) {
79
+ throw new Error(`base template not found at ${baseDir}`);
80
+ }
81
+ // Copy base template
82
+ mkdirSync(targetDir, { recursive: true });
83
+ cpSync(baseDir, targetDir, { recursive: true });
84
+ // Copy template overlay (overwrites base files where applicable)
85
+ if (existsSync(overlayDir)) {
86
+ cpSync(overlayDir, targetDir, { recursive: true });
87
+ }
88
+ // Replace {{PROJECT_NAME}} placeholders
89
+ replaceInDir(targetDir, "{{PROJECT_NAME}}", projectName);
90
+ }
91
+ function replaceInDir(dir, search, replace) {
92
+ const entries = readdirSync(dir, { withFileTypes: true });
93
+ for (const entry of entries) {
94
+ const fullPath = join(dir, entry.name);
95
+ if (entry.isDirectory()) {
96
+ replaceInDir(fullPath, search, replace);
97
+ }
98
+ else {
99
+ try {
100
+ const content = readFileSync(fullPath, "utf-8");
101
+ if (content.includes(search)) {
102
+ writeFileSync(fullPath, content.replaceAll(search, replace));
103
+ }
104
+ }
105
+ catch {
106
+ // Skip binary files
107
+ }
108
+ }
109
+ }
110
+ }
111
+ function isDirectExecution() {
112
+ // Bun exposes import.meta.main directly.
113
+ if (typeof Bun !== "undefined") {
114
+ return import.meta.main;
115
+ }
116
+ // Node 20 does not expose import.meta.main; compare current module to argv[1].
117
+ const entry = process.argv[1];
118
+ if (!entry)
119
+ return false;
120
+ try {
121
+ const currentFile = fileURLToPath(import.meta.url);
122
+ return resolve(entry) === currentFile;
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ // Main — only runs when executed directly (not when imported by tests)
129
+ if (isDirectExecution()) {
130
+ const args = process.argv.slice(2);
131
+ const options = parseArgs(args);
132
+ try {
133
+ console.log(`Creating ${options.projectName} with ${options.template} template...`);
134
+ scaffold(options);
135
+ console.log(`
136
+ Done! Your game is ready.
137
+
138
+ cd ${options.projectName}
139
+ bun install
140
+ bun run dev # web dev server
141
+ bun run dev:tauri # desktop dev (requires Tauri CLI)
142
+ bun run build:web # build for web deployment
143
+ bun run build:desktop # build desktop app
144
+ `);
145
+ }
146
+ catch (err) {
147
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
148
+ process.exit(1);
149
+ }
150
+ }
151
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "create-gametau",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "Scaffold a Tauri game that deploys to web + desktop",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-gametau": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "templates"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "bun test",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "^1.2.0",
21
+ "typescript": "^5.8.0"
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{PROJECT_NAME}}</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
10
+ canvas { display: block; }
11
+ #app { width: 100%; height: 100%; }
12
+ #hud {
13
+ position: fixed;
14
+ top: 16px;
15
+ left: 16px;
16
+ color: #fff;
17
+ font-family: monospace;
18
+ font-size: 14px;
19
+ z-index: 10;
20
+ pointer-events: none;
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div id="hud">
26
+ <div>Score: <span id="score">0</span></div>
27
+ <div>Tick: <span id="tick">0</span></div>
28
+ </div>
29
+ <div id="app"></div>
30
+ <script type="module" src="/src/index.ts"></script>
31
+ </body>
32
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "dev:tauri": "tauri dev",
9
+ "build:web": "vite build",
10
+ "build:desktop": "tauri build",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "webtau": "^0.1.0-alpha.2"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5.8.0",
18
+ "vite": "^6.0.0",
19
+ "webtau-vite": "^0.1.0-alpha.2",
20
+ "@tauri-apps/cli": "^2.0.0",
21
+ "@tauri-apps/api": "^2.0.0"
22
+ }
23
+ }
@@ -0,0 +1,28 @@
1
+ type TickFn = (dt: number) => void;
2
+ type RenderFn = () => void;
3
+
4
+ let running = false;
5
+ let lastTime = 0;
6
+
7
+ export function startGameLoop(tick: TickFn, render: RenderFn): void {
8
+ running = true;
9
+ lastTime = performance.now();
10
+
11
+ function frame(now: number) {
12
+ if (!running) return;
13
+
14
+ const dt = (now - lastTime) / 1000; // seconds
15
+ lastTime = now;
16
+
17
+ tick(dt);
18
+ render();
19
+
20
+ requestAnimationFrame(frame);
21
+ }
22
+
23
+ requestAnimationFrame(frame);
24
+ }
25
+
26
+ export function stopGameLoop(): void {
27
+ running = false;
28
+ }
@@ -0,0 +1,55 @@
1
+ import { configure, isTauri } from "webtau";
2
+ import { getWorldView, tickWorld } from "./services/backend";
3
+ import { startGameLoop } from "./game/loop";
4
+ import { initScene, updateScene } from "./game/scene";
5
+
6
+ async function main() {
7
+ // Configure webtau for web mode (no-op in Tauri)
8
+ if (!isTauri()) {
9
+ configure({
10
+ loadWasm: async () => {
11
+ const wasm = await import("./wasm/{{PROJECT_NAME}}_wasm");
12
+ await wasm.default(); // Initialize WASM
13
+ wasm.init(42); // Initialize game state
14
+ return wasm;
15
+ },
16
+ });
17
+ }
18
+
19
+ // Set up the renderer
20
+ const app = document.getElementById("app")!;
21
+ await initScene(app);
22
+
23
+ // Get initial state
24
+ const view = await getWorldView();
25
+ document.getElementById("score")!.textContent = String(view.score);
26
+ document.getElementById("tick")!.textContent = String(view.tick_count);
27
+
28
+ // Start game loop
29
+ let tickAccumulator = 0;
30
+ let tickInFlight = false;
31
+ const TICK_RATE = 1 / 10; // 10 ticks per second
32
+
33
+ startGameLoop(
34
+ (dt) => {
35
+ tickAccumulator += dt;
36
+ if (!tickInFlight && tickAccumulator >= TICK_RATE) {
37
+ tickAccumulator -= TICK_RATE;
38
+ tickInFlight = true;
39
+ tickWorld()
40
+ .then(() => getWorldView())
41
+ .then((view) => {
42
+ document.getElementById("score")!.textContent = String(view.score);
43
+ document.getElementById("tick")!.textContent = String(view.tick_count);
44
+ })
45
+ .catch(console.error)
46
+ .finally(() => { tickInFlight = false; });
47
+ }
48
+ },
49
+ () => {
50
+ updateScene();
51
+ },
52
+ );
53
+ }
54
+
55
+ main().catch(console.error);
@@ -0,0 +1,18 @@
1
+ import { invoke } from "webtau";
2
+
3
+ export interface WorldView {
4
+ score: number;
5
+ tick_count: number;
6
+ }
7
+
8
+ export interface TickResult {
9
+ score_delta: number;
10
+ }
11
+
12
+ export async function getWorldView(): Promise<WorldView> {
13
+ return invoke<WorldView>("get_world_view");
14
+ }
15
+
16
+ export async function tickWorld(): Promise<TickResult> {
17
+ return invoke<TickResult>("tick_world");
18
+ }
@@ -0,0 +1,7 @@
1
+ [workspace]
2
+ resolver = "2"
3
+ members = ["core", "commands", "app", "wasm"]
4
+
5
+ [workspace.package]
6
+ version = "0.1.0"
7
+ edition = "2021"
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "{{PROJECT_NAME}}-app"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+
6
+ [lib]
7
+ name = "app_lib"
8
+ crate-type = ["staticlib", "cdylib", "rlib"]
9
+
10
+ [build-dependencies]
11
+ tauri-build = { version = "2", features = [] }
12
+
13
+ [dependencies]
14
+ tauri = { version = "2", features = [] }
15
+ tauri-plugin-opener = "2"
16
+ serde = { version = "1", features = ["derive"] }
17
+ serde_json = "1"
18
+ {{PROJECT_NAME}}-core = { path = "../core" }
19
+ {{PROJECT_NAME}}-commands = { path = "../commands" }
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ tauri_build::build();
3
+ }
@@ -0,0 +1,14 @@
1
+ use std::sync::Mutex;
2
+
3
+ use {{PROJECT_NAME}}_core::GameWorld;
4
+ use {{PROJECT_NAME}}_commands::{get_world_view, tick_world};
5
+
6
+ #[cfg_attr(mobile, tauri::mobile_entry_point)]
7
+ pub fn run() {
8
+ tauri::Builder::default()
9
+ .plugin(tauri_plugin_opener::init())
10
+ .manage(Mutex::new(GameWorld::new(42)))
11
+ .invoke_handler(tauri::generate_handler![get_world_view, tick_world])
12
+ .run(tauri::generate_context!())
13
+ .expect("error while running tauri application");
14
+ }
@@ -0,0 +1,6 @@
1
+ // Prevents additional console window on Windows in release
2
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
+
4
+ fn main() {
5
+ app_lib::run();
6
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/nicholasgasior/tauri/tauri-v2/crates/tauri-config-schema/schema.json",
3
+ "productName": "{{PROJECT_NAME}}",
4
+ "version": "0.1.0",
5
+ "identifier": "com.gametau.{{PROJECT_NAME}}",
6
+ "build": {
7
+ "frontendDist": "../../../dist",
8
+ "devUrl": "http://localhost:1420",
9
+ "beforeDevCommand": "bun run dev",
10
+ "beforeBuildCommand": "bun run build:web"
11
+ },
12
+ "app": {
13
+ "windows": [
14
+ {
15
+ "title": "{{PROJECT_NAME}}",
16
+ "width": 1280,
17
+ "height": 720,
18
+ "resizable": true,
19
+ "fullscreen": false
20
+ }
21
+ ]
22
+ },
23
+ "bundle": {
24
+ "active": true,
25
+ "targets": "all",
26
+ "icon": []
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "{{PROJECT_NAME}}-commands"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+
6
+ [dependencies]
7
+ {{PROJECT_NAME}}-core = { path = "../core" }
8
+ webtau = "0.1.0-alpha.2"
9
+
10
+ [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
11
+ tauri = { version = "2", features = [] }
12
+
13
+ [target.'cfg(target_arch = "wasm32")'.dependencies]
14
+ wasm-bindgen = "0.2"
15
+ serde = { version = "1", features = ["derive"] }
16
+ serde-wasm-bindgen = "0.6"
@@ -0,0 +1,20 @@
1
+ use {{PROJECT_NAME}}_core::{GameWorld, WorldView, TickResult};
2
+
3
+ #[cfg(target_arch = "wasm32")]
4
+ webtau::wasm_state!(GameWorld);
5
+
6
+ #[cfg(target_arch = "wasm32")]
7
+ #[wasm_bindgen::prelude::wasm_bindgen]
8
+ pub fn init(seed: u32) {
9
+ set_state(GameWorld::new(seed as u64));
10
+ }
11
+
12
+ #[webtau::command]
13
+ pub fn get_world_view(state: &GameWorld) -> WorldView {
14
+ state.view()
15
+ }
16
+
17
+ #[webtau::command]
18
+ pub fn tick_world(state: &mut GameWorld) -> TickResult {
19
+ state.tick()
20
+ }
@@ -0,0 +1,7 @@
1
+ mod commands;
2
+
3
+ #[cfg(not(target_arch = "wasm32"))]
4
+ pub use commands::{get_world_view, tick_world};
5
+
6
+ #[cfg(target_arch = "wasm32")]
7
+ pub use commands::{init, get_world_view, tick_world};
@@ -0,0 +1,8 @@
1
+ [package]
2
+ name = "{{PROJECT_NAME}}-core"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+
6
+ [dependencies]
7
+ serde = { version = "1", features = ["derive"] }
8
+ rand = { version = "0.8", features = ["std_rng"] }
@@ -0,0 +1,45 @@
1
+ use rand::rngs::StdRng;
2
+ use rand::SeedableRng;
3
+ use serde::Serialize;
4
+
5
+ #[derive(Serialize, Clone)]
6
+ pub struct WorldView {
7
+ pub score: i32,
8
+ pub tick_count: u32,
9
+ }
10
+
11
+ #[derive(Serialize, Clone)]
12
+ pub struct TickResult {
13
+ pub score_delta: i32,
14
+ }
15
+
16
+ pub struct GameWorld {
17
+ score: i32,
18
+ tick_count: u32,
19
+ rng: StdRng,
20
+ }
21
+
22
+ impl GameWorld {
23
+ pub fn new(seed: u64) -> Self {
24
+ Self {
25
+ score: 0,
26
+ tick_count: 0,
27
+ rng: StdRng::seed_from_u64(seed),
28
+ }
29
+ }
30
+
31
+ pub fn view(&self) -> WorldView {
32
+ WorldView {
33
+ score: self.score,
34
+ tick_count: self.tick_count,
35
+ }
36
+ }
37
+
38
+ pub fn tick(&mut self) -> TickResult {
39
+ use rand::Rng;
40
+ let delta = self.rng.gen_range(-1..=3);
41
+ self.score += delta;
42
+ self.tick_count += 1;
43
+ TickResult { score_delta: delta }
44
+ }
45
+ }
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "{{PROJECT_NAME}}-wasm"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+
6
+ [lib]
7
+ crate-type = ["cdylib"]
8
+
9
+ [dependencies]
10
+ wasm-bindgen = "0.2"
11
+ serde = { version = "1", features = ["derive"] }
12
+ serde-wasm-bindgen = "0.6"
13
+ webtau = "0.1.0-alpha.2"
14
+ {{PROJECT_NAME}}-core = { path = "../core" }
15
+ {{PROJECT_NAME}}-commands = { path = "../commands" }
16
+
17
+ [profile.release]
18
+ lto = true
19
+ opt-level = "z"
20
+ codegen-units = 1
21
+ strip = true
@@ -0,0 +1,2 @@
1
+ // Link commands crate so its wasm_bindgen exports become part of this cdylib.
2
+ use {{PROJECT_NAME}}_commands as _;
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "isolatedModules": true
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from "vite";
2
+ import webtauVite from "webtau-vite";
3
+
4
+ const host = process.env.TAURI_DEV_HOST;
5
+
6
+ export default defineConfig({
7
+ // webtauVite() auto-detects the standard layout (src-tauri/wasm,
8
+ // src-tauri/core, etc.) — no config needed for scaffolded projects.
9
+ plugins: [webtauVite()],
10
+ clearScreen: false,
11
+ server: {
12
+ port: 1420,
13
+ strictPort: true,
14
+ host: host || false,
15
+ hmr: host
16
+ ? { protocol: "ws", host, port: 1421 }
17
+ : { protocol: "ws", host: "localhost", port: 1421 },
18
+ watch: {
19
+ ignored: ["**/src-tauri/**"],
20
+ },
21
+ },
22
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "dev:tauri": "tauri dev",
9
+ "build:web": "vite build",
10
+ "build:desktop": "tauri build",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "pixi.js": "^8.0.0",
15
+ "webtau": "^0.1.0-alpha.2"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.8.0",
19
+ "vite": "^6.0.0",
20
+ "webtau-vite": "^0.1.0-alpha.2",
21
+ "@tauri-apps/cli": "^2.0.0",
22
+ "@tauri-apps/api": "^2.0.0"
23
+ }
24
+ }
@@ -0,0 +1,46 @@
1
+ import { Application, Graphics, Text, TextStyle } from "pixi.js";
2
+
3
+ let app: Application;
4
+ let rect: Graphics;
5
+ let angle = 0;
6
+
7
+ export async function initScene(container: HTMLElement): Promise<void> {
8
+ app = new Application();
9
+ await app.init({
10
+ resizeTo: window,
11
+ background: 0x1a1a2e,
12
+ antialias: true,
13
+ });
14
+ container.appendChild(app.canvas);
15
+
16
+ // Spinning rectangle
17
+ rect = new Graphics();
18
+ drawRect();
19
+ rect.x = window.innerWidth / 2;
20
+ rect.y = window.innerHeight / 2;
21
+ rect.pivot.set(50, 50);
22
+ app.stage.addChild(rect);
23
+
24
+ // Title text
25
+ const style = new TextStyle({
26
+ fontFamily: "monospace",
27
+ fontSize: 16,
28
+ fill: 0x00d4aa,
29
+ });
30
+ const text = new Text({ text: "gametau + PixiJS", style });
31
+ text.x = window.innerWidth / 2;
32
+ text.y = window.innerHeight - 40;
33
+ text.anchor.set(0.5);
34
+ app.stage.addChild(text);
35
+ }
36
+
37
+ function drawRect(): void {
38
+ rect.clear();
39
+ rect.rect(0, 0, 100, 100);
40
+ rect.fill(0x00d4aa);
41
+ }
42
+
43
+ export function updateScene(): void {
44
+ angle += 0.02;
45
+ rect.rotation = angle;
46
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "dev:tauri": "tauri dev",
9
+ "build:web": "vite build",
10
+ "build:desktop": "tauri build",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "three": "^0.172.0",
15
+ "webtau": "^0.1.0-alpha.2"
16
+ },
17
+ "devDependencies": {
18
+ "@types/three": "^0.172.0",
19
+ "typescript": "^5.8.0",
20
+ "vite": "^6.0.0",
21
+ "webtau-vite": "^0.1.0-alpha.2",
22
+ "@tauri-apps/cli": "^2.0.0",
23
+ "@tauri-apps/api": "^2.0.0"
24
+ }
25
+ }
@@ -0,0 +1,54 @@
1
+ import * as THREE from "three";
2
+
3
+ let renderer: THREE.WebGLRenderer;
4
+ let scene: THREE.Scene;
5
+ let camera: THREE.PerspectiveCamera;
6
+ let cube: THREE.Mesh;
7
+
8
+ export async function initScene(container: HTMLElement): Promise<void> {
9
+ // Renderer
10
+ renderer = new THREE.WebGLRenderer({ antialias: true });
11
+ renderer.setSize(window.innerWidth, window.innerHeight);
12
+ renderer.setPixelRatio(window.devicePixelRatio);
13
+ container.appendChild(renderer.domElement);
14
+
15
+ // Scene
16
+ scene = new THREE.Scene();
17
+ scene.background = new THREE.Color(0x1a1a2e);
18
+
19
+ // Camera
20
+ camera = new THREE.PerspectiveCamera(
21
+ 75,
22
+ window.innerWidth / window.innerHeight,
23
+ 0.1,
24
+ 1000,
25
+ );
26
+ camera.position.z = 3;
27
+
28
+ // Lighting
29
+ const ambient = new THREE.AmbientLight(0x404040, 2);
30
+ scene.add(ambient);
31
+
32
+ const directional = new THREE.DirectionalLight(0xffffff, 1);
33
+ directional.position.set(5, 5, 5);
34
+ scene.add(directional);
35
+
36
+ // Spinning cube
37
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
38
+ const material = new THREE.MeshStandardMaterial({ color: 0x00d4aa });
39
+ cube = new THREE.Mesh(geometry, material);
40
+ scene.add(cube);
41
+
42
+ // Handle resize
43
+ window.addEventListener("resize", () => {
44
+ camera.aspect = window.innerWidth / window.innerHeight;
45
+ camera.updateProjectionMatrix();
46
+ renderer.setSize(window.innerWidth, window.innerHeight);
47
+ });
48
+ }
49
+
50
+ export function updateScene(): void {
51
+ cube.rotation.x += 0.01;
52
+ cube.rotation.y += 0.01;
53
+ renderer.render(scene, camera);
54
+ }
@@ -0,0 +1,43 @@
1
+ let canvas: HTMLCanvasElement;
2
+ let ctx: CanvasRenderingContext2D;
3
+ let angle = 0;
4
+
5
+ export async function initScene(container: HTMLElement): Promise<void> {
6
+ canvas = document.createElement("canvas");
7
+ canvas.width = window.innerWidth;
8
+ canvas.height = window.innerHeight;
9
+ container.appendChild(canvas);
10
+
11
+ ctx = canvas.getContext("2d")!;
12
+
13
+ window.addEventListener("resize", () => {
14
+ canvas.width = window.innerWidth;
15
+ canvas.height = window.innerHeight;
16
+ });
17
+ }
18
+
19
+ export function updateScene(): void {
20
+ const w = canvas.width;
21
+ const h = canvas.height;
22
+
23
+ // Clear
24
+ ctx.fillStyle = "#1a1a2e";
25
+ ctx.fillRect(0, 0, w, h);
26
+
27
+ // Spinning square
28
+ const size = 80;
29
+ ctx.save();
30
+ ctx.translate(w / 2, h / 2);
31
+ ctx.rotate(angle);
32
+ ctx.fillStyle = "#00d4aa";
33
+ ctx.fillRect(-size / 2, -size / 2, size, size);
34
+ ctx.restore();
35
+
36
+ angle += 0.02;
37
+
38
+ // Label
39
+ ctx.fillStyle = "#00d4aa";
40
+ ctx.font = "16px monospace";
41
+ ctx.textAlign = "center";
42
+ ctx.fillText("gametau + Canvas2D", w / 2, h - 24);
43
+ }