diff --git a/.gitignore b/.gitignore index 40c89ca..454750b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ cert.cer !package.json !package-lock.json +recording/ diff --git a/js/scenes/campFire.js b/js/scenes/campFire.js new file mode 100644 index 0000000..867824c --- /dev/null +++ b/js/scenes/campFire.js @@ -0,0 +1,48 @@ +/* + This scene is an example of how to use procedural texture + to animate the shape of an object. In this case the object + is a waving flag. The noise function is used to animate + the position of each vertex of the flag geometry. +*/ + +import * as cg from "../render/core/cg.js"; + +export const init = async model => { + // Define a tall, thin grid for the flame + clay.defineMesh('flame', clay.createGrid(20, 30)); + + // fire layer holder + let fireLayers = []; + for (let i = 0; i < 6; i++){ + let fire = model.add('flame').color(1, .4, 0); // Orange color + fire.angle = i * 30; // angle = 0, 60, 120. + fireLayers.push(fire); + } + + // logs + model.txtrSrc(1, '../media/textures/log1.png'); + let logs =[] + for (let i = 0; i < 6; i++){ + let log =model.add('tubeX').color(0.4,0.3,0.3).txtr(1).scale(1.3,0.07,0.07).move(0,0,0); + log.angle = i * 30 + //log.turnY(log.angle); + logs.push(log); + } + + model.scale(0.3).move(0,4,0).animate(() => { + // Animate the flame by modifying its vertices + fireLayers.forEach(fire => { + fire.identity().turnY(fire.angle); + fire.setVertices((u,v) => { + return [0.8*(u-0.5)*(1-v), + 2*v, + .3 * v * cg.noise(5*u,5*v-model.time*3,model.time) + ]; + }); + }); + + // logs.forEach(log => { + // log.identity().turnY(log.angle); + // }); + }); +} diff --git a/js/scenes/car.js b/js/scenes/car.js new file mode 100644 index 0000000..15812d6 --- /dev/null +++ b/js/scenes/car.js @@ -0,0 +1,56 @@ +/* + Create and animate hierarchical joints. +*/ +let speed = 0.8 + +export const init = async model => { + + model.txtrSrc(1, '../media/textures/tire.png'); + + // CREATE NODES WITH NO SHAPES AS JOINTS FOR ANIMATION. + let carbody = model.add(); + + //wheel center joint + let wheelFLCenter = carbody.add(); + let wheelFRCenter = carbody.add(); + let wheelBLCenter = carbody.add(); + let wheelBRCenter = carbody.add(); + + //wheel joint + let wheelFL = wheelFLCenter.add(); + let wheelFR = wheelFRCenter.add(); + let wheelBL = wheelBLCenter.add(); + let wheelBR = wheelBRCenter.add(); + + // CREATE AND PLACE SHAPES THAT WILL MOVE WITH EACH JOINT. + // carboday + carbody.add('cube').scale(.8,.25,.4).move(0.8,1.5,-1).color(1,0,0); + //car cabin + carbody.add('cube').scale(.5,.2,.35).move(0.8,3.8,-1.2).color(1,1,1);; + + // wheel centers + wheelFRCenter.move(1.2,0,0) + wheelBLCenter.move(0,0,-0.8) + wheelFLCenter.move(1.2,0,-0.8) + + wheelBR.add('torusZ').scale(.18,.18,.2).txtr(1);; + wheelFR.add('torusZ').scale(.18,.18,.2).txtr(1);; + wheelBL.add('torusZ').scale(.18,.18,.2).txtr(1);; + wheelFL.add('torusZ').scale(.18,.18,.2).txtr(1);; + + // ANIMATE THE JOINTS OVER TIME. + model.scale(0.8,0.8,0.8).move(-0.5,1.3,0).animate(() => { + carbody.identity() + .turnY(Math.sin(speed*model.time)*.7+.7); + + wheelFL.identity() + .turnZ(Math.sin(speed*model.time)*.7+.7); + wheelFR.identity() + .turnZ(Math.sin(speed*model.time)*.7+.7); + wheelBL.identity() + .turnZ(Math.sin(speed*model.time)*.7+.7); + wheelBR.identity() + .turnZ(Math.sin(speed*model.time)*.7+.7); + }); +} + diff --git a/js/scenes/carDrive.js b/js/scenes/carDrive.js new file mode 100644 index 0000000..1cdf5b1 --- /dev/null +++ b/js/scenes/carDrive.js @@ -0,0 +1,83 @@ +/* + This is a very simple example of how to use the + inputEvents object. + + When the scene is in XR mode, the x position of + the left controller controls the red component + of the cube's color, and the x position of the + right controller controls the blue component of + the cube's color. +*/ +export const init = async model => { + // See what the inputEvents can do + // console.log(inputEvents); + + model.txtrSrc(1, '../media/textures/tire.png'); + + let speed = 0; + let color = [1,0,0]; + // CREATE NODES WITH NO SHAPES AS JOINTS FOR ANIMATION. + let carbody = model.add(); + + //wheel center joint + let wheelFLCenter = carbody.add(); + let wheelFRCenter = carbody.add(); + let wheelBLCenter = carbody.add(); + let wheelBRCenter = carbody.add(); + + //wheel joint + let wheelFL = wheelFLCenter.add(); + let wheelFR = wheelFRCenter.add(); + let wheelBL = wheelBLCenter.add(); + let wheelBR = wheelBRCenter.add(); + + // CREATE AND PLACE SHAPES THAT WILL MOVE WITH EACH JOINT. + // carboday + let chassis = carbody.add('cube').scale(.8,.25,.4).move(0.8,1.5,-1).color(color); + //car cabin + carbody.add('cube').scale(.5,.2,.35).move(0.8,3.8,-1.2).color(1,1,1);; + + // wheel centers + wheelFRCenter.move(1.2,0,0) + wheelBLCenter.move(0,0,-0.8) + wheelFLCenter.move(1.2,0,-0.8) + + wheelBR.add('torusZ').scale(.18,.18,.2).txtr(1);; + wheelFR.add('torusZ').scale(.18,.18,.2).txtr(1);; + wheelBL.add('torusZ').scale(.18,.18,.2).txtr(1);; + wheelFL.add('torusZ').scale(.18,.18,.2).txtr(1);; + + // USING THE GLOBAL inputEvents OBJECT + + inputEvents.onMove = hand => { + if (isXR()) { + if (hand == 'left'){ + color[0] = inputEvents.pos(hand)[0] * .5 + .5; + color[1] = inputEvents.pos(hand)[2] * .5 + .5; + } + } + } + + model.scale(0.3).move(-0.5,4.5,0).animate(() => { + if (inputEvents.isPressed('right')) { + speed += 0.005; + } else { + speed *= 0.95; + } + + //.identity resets everything + carbody.identity().move(speed * 2, 0, 0); + chassis.color(color); + + wheelFL.identity() + .turnZ(-speed*model.time*.7); + wheelFR.identity() + .turnZ(-speed*model.time*.7); + wheelBL.identity() + .turnZ(-speed*model.time*.7); + wheelBR.identity() + .turnZ(-speed*model.time*.7); + + }); +} + diff --git a/js/scenes/joints.js b/js/scenes/joints.js index f828488..e36d5b6 100644 --- a/js/scenes/joints.js +++ b/js/scenes/joints.js @@ -3,7 +3,7 @@ */ export const init = async model => { - + // CREATE NODES WITH NO SHAPES AS JOINTS FOR ANIMATION. let shoulder = model.add(); diff --git a/js/scenes/master2.js b/js/scenes/master2.js new file mode 100644 index 0000000..f582c93 --- /dev/null +++ b/js/scenes/master2.js @@ -0,0 +1,112 @@ + +// MASTER-OWNED MULTIPLAYER COMET: +// - CONTROL GOES TO THE MOST RECENT TRIGGER PRESS (master2 pattern). +// - COLOR FOLLOWS LEFT/RIGHT HAND INPUT (multiplayer1 pattern). +// - EVERYONE SEES THE SAME FADING TRAIL. + +window.sharedState = { + time: 0, + pos: [0, 1.5, 0], + color: [1, 0.2, 0.2], + pulse: 0, + controller: {}, + trail: [], +}; + +const TRAIL_COUNT = 16; + +const colorForInput = (hand, id) => { + let hash = 0; + for (let i = 0; i < id.length; i++) + hash = (hash * 31 + id.charCodeAt(i)) % 997; + const n = (hash % 100) / 100; + return hand == 'left' ? [0.2 + 0.5 * n, 1, 0.2] : [0.2, 0.5 + 0.5 * n, 1]; +}; + +export const init = async model => { + let comet = model.add('sphere'); + let halo = model.add('ringY'); + let trail = []; + for (let i = 0; i < TRAIL_COUNT; i++) + trail.push(model.add('sphere')); + + inputEvents.onPress = hand => { + const id = hand + clientID; + sharedState.controller[id] = { + pos: [...inputEvents.pos(hand)], + time: sharedState.time, + color: colorForInput(hand, id), + }; + server.broadcastGlobal('sharedState'); + }; + + inputEvents.onDrag = hand => { + const id = hand + clientID; + if (sharedState.controller[id]) { + sharedState.controller[id].pos = [...inputEvents.pos(hand)]; + server.broadcastGlobal('sharedState'); + } + }; + + inputEvents.onRelease = hand => { + delete sharedState.controller[hand + clientID]; + server.broadcastGlobal('sharedState'); + }; + + model.animate(() => { + sharedState = server.synchronize('sharedState'); + + if (clientID == clients[0]) { + sharedState.time = model.time; + + let newest = null; + let newestTime = -1; + for (let id in sharedState.controller) + if (sharedState.controller[id].time > newestTime) { + newestTime = sharedState.controller[id].time; + newest = sharedState.controller[id]; + } + + if (newest) { + sharedState.pos = [...newest.pos]; + sharedState.color = [...newest.color]; + sharedState.pulse = 1; + sharedState.trail.unshift({ + pos: [...sharedState.pos], + color: [...sharedState.color], + time: sharedState.time, + }); + } else { + sharedState.pos = [ + sharedState.pos[0], + 1.45 + 0.1 * Math.sin(2 * sharedState.time), + sharedState.pos[2], + ]; + sharedState.pulse *= 0.95; + } + + while (sharedState.trail.length > TRAIL_COUNT) + sharedState.trail.pop(); + sharedState.trail = sharedState.trail.filter(p => sharedState.time - p.time < 2.5); + server.broadcastGlobal('sharedState'); + } + + const pulse = 1 + 0.25 * sharedState.pulse * Math.abs(Math.sin(10 * sharedState.time)); + comet.identity().move(sharedState.pos).scale(0.07 * pulse).color(...sharedState.color); + halo.identity().move(sharedState.pos).turnY(2 * sharedState.time).scale(0.11).color(...sharedState.color); + + for (let i = 0; i < TRAIL_COUNT; i++) { + const p = sharedState.trail[i]; + if (!p) { + trail[i].identity().scale(0); + continue; + } + const age = Math.max(0, sharedState.time - p.time); + const fade = Math.max(0, 1 - age / 2.5); + trail[i].identity() + .move(p.pos) + .scale(0.05 * fade) + .color(p.color[0] * fade, p.color[1] * fade, p.color[2] * fade); + } + }); +}; diff --git a/js/scenes/scenes.js b/js/scenes/scenes.js index e7f61ce..a3250e6 100644 --- a/js/scenes/scenes.js +++ b/js/scenes/scenes.js @@ -31,6 +31,16 @@ export default () => { { name: "headGaze" , path: "./headGaze.js" , public: true }, { name: "reading" , path: "./reading.js" , public: true }, { name: "parse2" , path: "./parse2.js" , public: true }, + { name: "car" , path: "./car.js" , public: true }, + { name: "carDrive" , path: "./carDrive.js" , public: true }, + { name: "campFire" , path: "./campFire.js" , public: true }, + { name: "textHW" , path: "./textHW.js" , public: true }, + { name: "master2" , path: "./master2.js" , public: true }, + { name: "car" , path: "./car.js" , public: true }, + { name: "carDrive" , path: "./carDrive.js" , public: true }, + { name: "campFire" , path: "./campFire.js" , public: true }, + { name: "textHW" , path: "./textHW.js" , public: true }, + { name: "master2" , path: "./master2.js" , public: true } ] }; } diff --git a/js/scenes/spirit_exercise.js b/js/scenes/spirit_exercise.js new file mode 100644 index 0000000..e3e5b26 --- /dev/null +++ b/js/scenes/spirit_exercise.js @@ -0,0 +1,248 @@ +import * as cg from "../render/core/cg.js"; +import { loadSound, playSoundAtPosition } from "../util/positional-audio.js"; + +const TARGET_COUNT = 36; +const TRAIL_COUNT = 24; +const STRIKE_Z = -0.55; +const DESPAWN_Z = -0.15; +const SPAWN_Z = -5.6; + +const lanes = [-1.0, -0.35, 0.35, 1.0]; +const heights = [1.15, 1.5, 1.85]; + +const leftColor = [0.22, 0.9, 1.0]; +const rightColor = [1.0, 0.35, 0.25]; +const neutralColor = [0.95, 0.9, 0.35]; +const pulseColor = [0.2, 0.9, 0.5]; + +let soundBuffer = [], loadSounds = []; +for (let i = 0; i < 6; i++) + loadSounds.push(loadSound("../../media/sound/bounce/" + i + ".wav", buffer => soundBuffer[i] = buffer)); +Promise.all(loadSounds); + +const makeTarget = () => ({ + active: false, + hand: "any", + lane: 0, + level: 1, + spawnTime: 0, + life: 0, + speed: 0, + hitFlash: 0, + pos: [0, 1.5, SPAWN_Z], +}); + +const targetColor = hand => hand == "left" ? leftColor : hand == "right" ? rightColor : neutralColor; + +const choosePattern = beat => { + const phase = beat % 16; + + if (phase == 12 || phase == 13 || phase == 14) + return { step: 0.23, count: 1, speed: 3.4, mode: "burst" }; + + if (phase >= 8) + return { step: 0.36, count: 1, speed: 3.0, mode: "cross" }; + + return { step: 0.42, count: 1, speed: 2.7, mode: "flow" }; +}; + +export const init = async model => { + let beat = 0; + let nextSpawnTime = 0; + let combo = 0; + let bestCombo = 0; + let score = 0; + let hits = 0; + let misses = 0; + let coachPulse = 0; + + let targets = []; + let trails = []; + + for (let i = 0; i < TARGET_COUNT; i++) { + targets.push(makeTarget()); + model.add("sphere"); + } + + for (let i = 0; i < TRAIL_COUNT; i++) { + trails.push({ pos: [0, 1.5, -1], life: 0, color: [1, 1, 1] }); + model.add("sphere"); + } + + const leftGuide = model.add("ringZ"); + const rightGuide = model.add("ringZ"); + const pulseRing = model.add("ringY"); + + const horizon = model.add("square"); + const floor = model.add("square"); + const leftPeak = model.add("coneY"); + const rightPeak = model.add("coneY"); + const moon = model.add("sphere"); + + let addTrail = (pos, color) => { + trails.unshift({ pos: [...pos], life: 1, color: [...color] }); + trails.pop(); + }; + + let spawnTarget = (spawnAt, patternMode, speed) => { + for (let i = 0; i < TARGET_COUNT; i++) { + if (targets[i].active) + continue; + + const lane = patternMode == "cross" + ? (beat % 2 == 0 ? 0 : 3) + : 4 * Math.random() >> 0; + + const level = patternMode == "burst" + ? ((beat + i) % 3) + : (3 * Math.random() >> 0); + + let hand = "any"; + if (lane < 2) + hand = "left"; + if (lane > 1) + hand = "right"; + if (patternMode == "flow" && beat % 4 == 3) + hand = "any"; + + targets[i].active = true; + targets[i].hand = hand; + targets[i].lane = lane; + targets[i].level = level; + targets[i].spawnTime = spawnAt; + targets[i].life = 1; + targets[i].speed = speed; + targets[i].hitFlash = 0; + targets[i].pos = [lanes[lane], heights[level], SPAWN_Z]; + return; + } + }; + + let playHit = index => { + if (soundBuffer.length == 0) + return; + playSoundAtPosition(soundBuffer[index % soundBuffer.length], targets[index].pos); + }; + + model.animate(() => { + const t = model.time; + const dt = model.deltaTime; + const bpm = 132 + 16 * Math.sin(0.05 * t); + const beatDuration = 60 / bpm; + + while (t >= nextSpawnTime) { + const pattern = choosePattern(beat); + for (let i = 0; i < pattern.count; i++) + spawnTarget(nextSpawnTime + i * pattern.step, pattern.mode, pattern.speed); + + nextSpawnTime += beatDuration; + beat++; + coachPulse = 1; + } + + const leftHand = clientState.finger(clientID, "left", 1); + const rightHand = clientState.finger(clientID, "right", 1); + + if (Array.isArray(leftHand)) + addTrail(leftHand, leftColor); + if (Array.isArray(rightHand)) + addTrail(rightHand, rightColor); + + coachPulse *= 0.93; + + for (let i = 0; i < TARGET_COUNT; i++) { + const targetNode = model.child(i); + const target = targets[i]; + + if (!target.active) { + targetNode.identity().scale(0); + continue; + } + + target.pos[2] += target.speed * dt; + target.hitFlash *= 0.86; + + let gotHit = false; + const hitRadius = 0.28; + + if (Array.isArray(leftHand) && target.hand != "right" && cg.distance(leftHand, target.pos) < hitRadius) + gotHit = true; + + if (Array.isArray(rightHand) && target.hand != "left" && cg.distance(rightHand, target.pos) < hitRadius) + gotHit = true; + + if (gotHit) { + target.hitFlash = 1; + target.active = false; + combo++; + hits++; + score += 10 + combo; + bestCombo = Math.max(bestCombo, combo); + coachPulse = 1; + playHit(i); + continue; + } + + if (target.pos[2] > DESPAWN_Z) { + target.active = false; + misses++; + combo = 0; + continue; + } + + const c = targetColor(target.hand); + const glow = 0.2 + 0.25 * Math.sin(14 * t + i); + const depthScale = 1.05 + 0.8 * (target.pos[2] - STRIKE_Z) / (SPAWN_Z - STRIKE_Z); + + targetNode.identity() + .move(target.pos) + .scale(0.13 * depthScale) + .color(c[0] + glow, c[1] + glow, c[2] + glow); + } + + for (let i = 0; i < TRAIL_COUNT; i++) { + const n = model.child(TARGET_COUNT + i); + const tr = trails[i]; + tr.life *= 0.9; + + if (tr.life < 0.06) { + n.identity().scale(0); + continue; + } + + n.identity().move(tr.pos).scale(0.04 * tr.life) + .color(tr.color[0] * tr.life, tr.color[1] * tr.life, tr.color[2] * tr.life); + } + + const leftGuidePos = Array.isArray(leftHand) ? leftHand : [-0.45, 1.4, -0.55]; + const rightGuidePos = Array.isArray(rightHand) ? rightHand : [0.45, 1.4, -0.55]; + + leftGuide.identity().move(leftGuidePos).scale(0.1 + 0.03 * Math.sin(9 * t)).color(...leftColor); + rightGuide.identity().move(rightGuidePos).scale(0.1 + 0.03 * Math.sin(9 * t + 1)).color(...rightColor); + + pulseRing.identity().move(0, 1.5, STRIKE_Z - 0.02) + .turnY(2 * t) + .scale(1.0 + 0.45 * coachPulse) + .color(pulseColor[0], pulseColor[1], pulseColor[2]); + + horizon.identity().move(0, 2.0, -7.5).scale(9.5, 4.8, 1).color(0.06, 0.1, 0.2); + floor.identity().move(0, 0.72, -2.8).turnX(-Math.PI / 2).scale(3.2, 3.2, 1).color(0.04, 0.06, 0.12); + leftPeak.identity().move(-4.8, 0.8, -8.0).scale(1.8, 3.2, 1.8).color(0.08, 0.14, 0.2); + rightPeak.identity().move(4.8, 0.82, -8.1).scale(2.1, 3.4, 2.1).color(0.09, 0.12, 0.22); + moon.identity().move(2.4, 3.1, -7.0).scale(0.32).color(0.85, 0.95, 1.0); + + const total = hits + misses; + const accuracy = total > 0 ? Math.floor(100 * hits / total) : 100; + + while (model.nChildren() > TARGET_COUNT + TRAIL_COUNT + 8) + model.remove(TARGET_COUNT + TRAIL_COUNT + 8); + + model.add(clay.text("RHYTHM CARDIO")).move(-1.05, 2.52, -1.85).scale(1.95).color(0.9, 1.0, 1.0); + model.add(clay.text("SCORE " + score)).move(-1.05, 2.30, -1.85).scale(1.2).color(0.85, 0.95, 1.0); + model.add(clay.text("COMBO " + combo + " BEST " + bestCombo)).move(-1.05, 2.13, -1.85).scale(1.08).color(1.0, 0.95, 0.5); + model.add(clay.text("ACCURACY " + accuracy + "%")) + .move(-1.05, 1.96, -1.85).scale(1.08).color(0.5, 1.0, 0.8); + model.add(clay.text("STRIKE IN TIME WITH THE BEAT")) + .move(-1.05, 1.73, -1.85).scale(0.92).color(0.65, 0.85, 1.0); + }); +}; diff --git a/js/scenes/text1.js b/js/scenes/text1.js index 8c06bb9..db5ea63 100644 --- a/js/scenes/text1.js +++ b/js/scenes/text1.js @@ -10,4 +10,3 @@ export const init = async model => { model.add(myText).move(-.1,1.45,0).color(1,1,1).scale(.1); }); } - diff --git a/js/scenes/textHW.js b/js/scenes/textHW.js new file mode 100644 index 0000000..f280885 --- /dev/null +++ b/js/scenes/textHW.js @@ -0,0 +1,61 @@ +export const init = async model => { + let words = [ + "WOW", + "PLAY", + "DANCE", + "BOUNCE", + "SPIN", + "LAUGH", + "JUMP", + "GLOW" + ]; + + let t = 0; + let speed = 0.004; + let zOffset = -2.2; + + let wordPose = (i, tt) => { + let a = 0.75 * tt + (2 * Math.PI * i) / words.length; + let r = 1.9 + 0.08 * Math.sin(1.5 * tt + i); + return { + x: r * Math.cos(a) - 0.23, + y: 2.1 + 0.1 * Math.sin(2 * tt + i) + 0.04, + z: -1.25 + r * Math.sin(a) + zOffset, + }; + }; + + model.animate(() => { + while (model.nChildren()) + model.remove(0); + + let paused = false; + if (typeof inputEvents.isPressed == "function") + paused = inputEvents.isPressed("left") || inputEvents.isPressed("right"); + + if (!paused) + t += speed; + + model.add(clay.text("TEXT PARTY")) + .move(-0.95, 1.18, -0.59+zOffset) + .scale(4.8) + .color(1, 0.2, 0.95); + + for (let i = 0; i < words.length; i++) { + let w = wordPose(i, t); + let red = 0.5 + 0.5 * Math.sin(1.4 * t + i); + let green = 0.5 + 0.5 * Math.sin(1.4 * t + i + 2.1); + let blue = 0.5 + 0.5 * Math.sin(1.4 * t + i + 4.2); + + model.add(clay.text(words[i])) + .move(w.x, w.y, w.z) + .scale(2.2) + .color(paused ? 1 : red, paused ? 0.95 : green, paused ? 0.2 : blue); + } + + let hint = paused ? "PAUSED - RELEASE TRIGGER TO RESUME" : "HOLD LEFT/RIGHT TRIGGER TO PAUSE"; + model.add(clay.text(hint)) + .move(-0.95, 0.5, -0.59+zOffset) + .scale(1.6) + .color(0.2, 1, 0.95); + }); +}; diff --git a/js/util/texts.js b/js/util/texts.js index 8eb6ca5..74cf8e5 100644 --- a/js/util/texts.js +++ b/js/util/texts.js @@ -1,4 +1,6 @@ export let texts = [ +`Hello, world! +`, ` import * as cg from "../render/core/cg.js"; import { G3 } from "../util/g3.js"; diff --git a/media/textures/log.png b/media/textures/log.png new file mode 100644 index 0000000..1e520d8 Binary files /dev/null and b/media/textures/log.png differ diff --git a/media/textures/log1.png b/media/textures/log1.png new file mode 100644 index 0000000..538097d Binary files /dev/null and b/media/textures/log1.png differ diff --git a/media/textures/tire.png b/media/textures/tire.png new file mode 100644 index 0000000..ed9d69b Binary files /dev/null and b/media/textures/tire.png differ diff --git a/package.json b/package.json index 3809a5f..2e69cb4 100644 --- a/package.json +++ b/package.json @@ -39,5 +39,8 @@ "devDependencies": { "gltf-import-export": "^1.0.16", "prettier": "2.2.1" + }, + "volta": { + "node": "18.20.8" } } diff --git a/server/main.js b/server/main.js index 15e4426..487393d 100644 --- a/server/main.js +++ b/server/main.js @@ -1,3 +1,8 @@ +const os = require('os'); +if (!os.tmpDir) { + os.tmpDir = os.tmpdir; +} + var bodyParser = require("body-parser"); var express = require("express"); var formidable = require("formidable");