My team's capstone proof of concept reads 16 channels of forearm muscle activity, classifies gestures on an ESP32-S3, and drives a phone's media controls over Bluetooth while the wearer holds a ski pole.
I owned gesture recognition and the browser dashboard. The final build used per-don calibration and demonstrated two commands end to end. That architecture replaced a fixed int8 CNN after custom-electrode data exposed wearer and placement shift.
A published mod for the survival game Vintage Story, and the only thing I have built that strangers use. It lets players carve statues by placing frames, picking a voxel blueprint, and chiselling along chalk lines the mod draws for them.
Written in C#, with an in-game mesh-to-voxel converter that optimises placement for how much chiselling the player has to do. About 5,000 downloads across the main mod and its two published content packs, which supply 69 sculptures voxelised from real 3D scans.
Designing and building small fixed-wing RC aircraft from scratch rather than from a kit. The work runs the full conceptual design process: a written requirements document, a Python tool that sizes the aircraft by constraint analysis against those requirements, airfoil selection at low Reynolds number, CAD, and construction in foam and 3D print.
Two airframes came out of it. A swept flying wing came first and failed its first flight on a centre-of-gravity error. Sandpiper followed: conventional, high wing, detachable for transport. It took off from a beach, handled poorly, and crashed.
A desktop app that turns a web-novel chapter into an audiobook where every character has their own consistent voice. It scrapes the chapter, cleans up the prose with a grammar model, works out who speaks each line of dialogue with a local language model, and matches each character to a real speaker by embedding similarity on their described voice traits.
About 18,000 lines of Rust, running entirely locally. Attribution quality is measured against five hand-labelled chapters, and the engineering log records four prompt-engineering fixes that did nothing before a larger model solved it outright.
A group project for ENSC 351. We built a bench top CNC solenoid winding machine with a C-axis spindle and Z-axis linear traverse. I was responsible for the control software, written in Rust and running on a BeagleY-AI. The TUI interface displays real-time motor state and accepts movement commands over local HDMI or remote SSH.
Designed to compete in the 2024 robocup small size league. Our team's website is SFU Robot Soccer
I rendered the robot soccer mechanical components in SolidWorks. I have personally designed or revised many of the mechanical components of the robot, most notably the spiral-shaped dribbler, its drive train assembly, the horizontal structural plates, and the main mounting brackets.
The team's prototyping is done with 3D printing since it is rapid and strong enough to do limited testing. To meet our goals, I printed the yellow parts with PETG.
Below are some screenshots of my game development projects. The first image showcases a fog of war shaders and robust selection tools. The second is a game with fully developed gameplay. And finally, the third project was developed with my friends, with a limited colour palette and procedural animations.
Below are some code samples for several languages I have learned. They are files taken from some of my projects.
| use dupe::{Dupe, OptionDupedExt}; | |
| use horde_common::game::{card, player, score, view, Error}; | |
| use std::sync::Arc; | |
| use horde_common::game::state::Outcome; | |
| use crate::game::event; | |
| #[derive(Debug, Clone)] | |
| pub(crate) struct State { | |
| players: Arc<[player::Player]>, | |
| turn: usize, | |
| pub(crate) cards: card::List, | |
| } | |
| impl State { | |
| pub(crate) fn new( | |
| players: Arc<[player::Player]>, | |
| mut cards: card::List, | |
| ) -> Result<Self, Error> { | |
| const HAND_SIZE: usize = 5; | |
| if players.is_empty() { | |
| return Err(Error::InvalidState( | |
| "State cannot be made with zero players.".to_owned(), | |
| )); | |
| } | |
| let card_count = cards.count(); | |
| let card_min = HAND_SIZE * players.len(); | |
| for player in players.iter() { | |
| if cards.draw_exact(HAND_SIZE, player.id.dupe()).is_none() { | |
| return Err(Error::InvalidState( | |
| format!("State must be able to distribute enough cards. Deck has {card_count} cards, it needs at least {card_min}.") | |
| )); | |
| } | |
| } | |
| Ok(Self { | |
| players, | |
| turn: 0, | |
| cards, | |
| }) | |
| } | |
| pub(crate) fn view(&self, viewer: &player::Id, outcome: Option<Outcome>) -> view::StateView { | |
| let active = &self.active_player().id; | |
| view::StateView::new(&self.players, &self.cards, viewer, active, outcome) | |
| } | |
| pub(crate) fn apply(&mut self, events: &event::EventList) -> Option<Outcome> { | |
| for event in events.iter() { | |
| self.apply_inner(event, 0); | |
| } | |
| self.make_outcome(events) | |
| } | |
| pub(crate) fn players(&self) -> &[player::Player] { | |
| &self.players | |
| } | |
| pub(crate) fn active_player(&self) -> &player::Player { | |
| &self.players[self.turn % self.players.len()] | |
| } | |
| pub(crate) fn force_outcome(&self) -> Outcome { | |
| let scores = self.players.iter().map(|p| self.make_score(p)).collect(); | |
| Outcome { scores } | |
| } | |
| fn make_outcome(&self, events: &event::EventList) -> Option<Outcome> { | |
| if !self.is_game_finished(events) { | |
| return None; | |
| } | |
| let scores = self.players.iter().map(|p| self.make_score(p)).collect(); | |
| Some(Outcome { scores }) | |
| } | |
| fn make_score(&self, player: &player::Player) -> score::Score { | |
| let hand = self.cards.hand(&player.id); | |
| let adversary = self.cards.hand(&player.adversary); | |
| score::Score::create(player.name.dupe(), hand, adversary) | |
| } | |
| fn is_game_finished(&self, events: &event::EventList) -> bool { | |
| if events.get_sequential_skips() >= self.players.len() { | |
| return true; | |
| } | |
| if self | |
| .cards | |
| .hands() | |
| .iter() | |
| .all(|(_, cards)| cards.iter().all(|c| !c.is_playable())) | |
| { | |
| return true; | |
| } | |
| false | |
| } | |
| fn apply_inner(&mut self, event: &event::Event, block_layer: usize) { | |
| match event { | |
| event::Event::Skip => self.turn += 1, | |
| event::Event::Ability { source, variant } => { | |
| self.apply_ability(source, variant, block_layer) | |
| } | |
| } | |
| } | |
| fn apply_ability( | |
| &mut self, | |
| source: &card::Id, | |
| variant: &event::EventVariant, | |
| block_layer: usize, | |
| ) { | |
| self.cards.change_state(source, card::State::Played); | |
| if let event::EventVariant::Block { event } = variant { | |
| return self.apply_inner(event.as_ref(), block_layer + 1); | |
| } | |
| self.turn += 1; | |
| if block_layer % 2 == 1 { | |
| return; | |
| } | |
| match variant { | |
| event::EventVariant::Exchange { | |
| discard, | |
| keep, | |
| drawn, | |
| } => self.apply_exchange(source, discard, keep, drawn), | |
| event::EventVariant::Trade { | |
| own, | |
| responses, | |
| chosen, | |
| } => self.apply_trade(own, responses, chosen), | |
| event::EventVariant::Spy { peeked } => self.apply_spy(peeked), | |
| event::EventVariant::Swap { own, other } => self.apply_swap(own, other), | |
| event::EventVariant::Revive { target } => self.apply_revive(target), | |
| event::EventVariant::ChangeAdversary { target } => { | |
| self.apply_change_adversary(source, target) | |
| } | |
| event::EventVariant::Block { .. } => unreachable!("Block must be handled above."), | |
| } | |
| } | |
| fn apply_exchange( | |
| &mut self, | |
| source: &card::Id, | |
| discard: &[card::Id], | |
| keep: &[card::Id], | |
| drawn: &[card::Id], | |
| ) { | |
| let owner = { self.cards.find(source).and_then(|c| c.owner.duped()) }; | |
| for id in discard { | |
| self.cards.move_to(id, None); | |
| } | |
| for id in drawn { | |
| self.cards.move_to(id, None); | |
| } | |
| for id in keep { | |
| self.cards.move_to(id, owner.dupe()) | |
| } | |
| } | |
| fn apply_trade( | |
| &mut self, | |
| own: &card::Id, | |
| responses: &[(player::Id, card::Id)], | |
| chosen: &Option<card::Id>, | |
| ) { | |
| self.cards.change_state(own, card::State::Visible); | |
| for (_, cid) in responses { | |
| self.cards.change_state(cid, card::State::Visible); | |
| } | |
| if let Some(chosen) = chosen { | |
| self.apply_swap(own, chosen); | |
| } | |
| } | |
| fn apply_spy(&mut self, peeked: &[card::Id]) { | |
| for id in peeked { | |
| self.cards.change_state(id, card::State::Visible); | |
| } | |
| } | |
| fn apply_swap(&mut self, a: &card::Id, b: &card::Id) { | |
| let owner_a = { self.cards.find(a).and_then(|c| c.owner.duped()) }; | |
| let owner_b = { self.cards.find(b).and_then(|c| c.owner.duped()) }; | |
| self.cards.move_to(a, owner_b); | |
| self.cards.move_to(b, owner_a); | |
| } | |
| fn apply_revive(&mut self, target: &card::Id) { | |
| self.cards.change_state(target, card::State::Visible); | |
| } | |
| fn apply_change_adversary(&mut self, source: &card::Id, target: &player::Id) { | |
| let owner = self.cards.find(source).and_then(|c| c.owner); | |
| if let Some(owner) = owner { | |
| let players = Arc::make_mut(&mut self.players); | |
| if let Some(player) = players.iter_mut().find(|p| &p.id == owner) { | |
| player.adversary = target.dupe(); | |
| } | |
| } | |
| } | |
| } |
| import { Network } from "./network.mjs"; | |
| import { Camera } from "./camera.mjs"; | |
| import { Namer } from "./namer.mjs"; | |
| import { Eye } from "./eye.mjs"; | |
| export class Creature { | |
| x; | |
| y; | |
| angle; | |
| color; | |
| heartrate; | |
| satiety; | |
| eyeList; | |
| brainNetwork; | |
| speciesName; | |
| generationName; | |
| constructor( | |
| x, | |
| y, | |
| angle, | |
| color, | |
| heartrate, | |
| satiety, | |
| eyeList, | |
| brainNetwork, | |
| speciesName, | |
| generationName, | |
| parentRef) { | |
| this.x = x; | |
| this.y = y; | |
| this.angle = angle; | |
| this.color = color; | |
| this.heartrate = heartrate; | |
| this.satiety = satiety; | |
| this.eyeList = eyeList; | |
| this.brainNetwork = brainNetwork; | |
| this.speciesName = speciesName; | |
| this.generationName = generationName; | |
| this.parentRef = parentRef; | |
| } | |
| copy() { | |
| const x = this.x; | |
| const y = this.y; | |
| const angle = this.angle; | |
| const color = this.color; | |
| const heartrate = this.heartrate; | |
| const satiety = this.satiety; | |
| const eyeList = this.eyeList.map(eye => eye.copy()); | |
| const brainNetwork = this.brainNetwork.copy(); | |
| const speciesName = this.speciesName; | |
| const generationName = this.generationName; | |
| const parentRef = this.parentRef; | |
| return new Creature(x, | |
| y, | |
| angle, | |
| color, | |
| heartrate, | |
| satiety, | |
| eyeList, | |
| brainNetwork, | |
| speciesName, | |
| generationName, | |
| parentRef); | |
| } | |
| static speedLinearMax = 0.03; | |
| static speedAngularMax = 0.04; | |
| move(linear, angular, board) { | |
| this.x += Creature.speedLinearMax * linear * Math.cos(this.angle) * this.heartrate; | |
| this.y += Creature.speedLinearMax * linear * Math.sin(this.angle) * this.heartrate; | |
| this.angle += Creature.speedAngularMax * angular * this.heartrate; | |
| // Limit to boundaries | |
| this.x = Math.max(Math.min(this.x, board.size.x), 0); | |
| this.y = Math.max(Math.min(this.y, board.size.y), 0); | |
| } | |
| brainOutputs; | |
| brainInputsMem = []; | |
| brainSave = 0; | |
| static brainInputsMemSize = 100; | |
| runBrain(viewObjects) { | |
| // Get the inputs | |
| const inputs = []; | |
| this.eyeList.forEach(eye => { | |
| const values = eye.getValues(this.x, this.y, this.angle, viewObjects); | |
| inputs.push(values.left); | |
| inputs.push(values.right); | |
| }); | |
| // Get internal inputs | |
| inputs.push(0); // Hunger | |
| inputs.push((Math.sin(Math.PI * 2 * this.heartTime) + 1) * 0.5); // Heartbeat | |
| if (this.brainSave <= 0) { | |
| // Save the inputs to memory | |
| for (let index = 0; index < inputs.length; index++) { | |
| if (!this.brainInputsMem[index]) { | |
| this.brainInputsMem[index] = new Array(); | |
| } | |
| this.brainInputsMem[index].push(inputs[index]); | |
| if (this.brainInputsMem[index].length > Creature.brainInputsMemSize) { | |
| this.brainInputsMem[index].shift(); | |
| } | |
| } | |
| } | |
| this.brainSave = (this.brainSave + 1) % 3; | |
| // Get the outputs | |
| this.brainOutputs = this.brainNetwork.compute(inputs); | |
| } | |
| createMutations() { | |
| var result = []; | |
| // Color | |
| result.push(() => this.color += Math.random() * 0.2 - 0.1); | |
| // Eyes | |
| this.eyeList.forEach(eye => result.push(...eye.createMutations())); | |
| // Brain | |
| result.push(...this.brainNetwork.createMutations()); | |
| return result; | |
| } | |
| mutate() { | |
| const avaliableMutations = this.createMutations(); | |
| // Small mutations: { eye properties, brain axon values, brain neuron bias, body color } | |
| const chanceSmall = 0.5; // 50% chance for each of the options seperately | |
| avaliableMutations.forEach(mutation => { | |
| if (Math.random() <= chanceSmall) { | |
| mutation(); // Apply the mutation | |
| } | |
| }); | |
| // Large mutations: { eye count, brain neuron count } | |
| const chanceLarge = 0.01; // 1% chance | |
| } | |
| createOffspring() { | |
| const children = []; | |
| const childCount = 1; | |
| const childSatiety = 0.5; | |
| for (let i = 0; i < childCount; i++) { | |
| const child = this.copy(); | |
| child.mutate(); | |
| // Change the generation name | |
| child.generationName = Namer.namer.getName(); | |
| child.satiety = childSatiety; | |
| // Change the parent | |
| child.parentRef = this; | |
| children.push(child); | |
| } | |
| this.satiety -= childCount * childSatiety; | |
| return children; | |
| } | |
| brainLinearMovement() { | |
| return Math.min(Math.max(this.brainOutputs[0] - this.brainOutputs[1], 0), 1); // Clamp [0, 1] | |
| } | |
| brainAngularMovement() { | |
| return Math.min(Math.max(this.brainOutputs[2] - this.brainOutputs[3], -1), 1); // Clamp [-1, 1] | |
| } | |
| brainHeartTarget() { | |
| return (this.brainOutputs[4] - this.brainOutputs[5]) / 2 + 0.5; | |
| } | |
| tryEat(eatObject) { | |
| // Check eaten | |
| if (eatObject.isEaten) { | |
| return; | |
| } | |
| const radius = 0.5; | |
| // Check bounding box | |
| const dx = eatObject.x - this.x; | |
| const dy = eatObject.y - this.y; | |
| if (dx < -radius || dx > radius || dy < -radius || dy > radius) { | |
| return; | |
| } | |
| // Check radius | |
| const percentRangeSquared = (dx * dx + dy * dy) / (radius * radius); | |
| if (percentRangeSquared > 1) { | |
| return; | |
| } | |
| // Eat the object | |
| this.satiety = this.satiety + eatObject.eat(); // Clamp (-inf, 1] | |
| } | |
| die() { | |
| this.isDead = true; | |
| } | |
| heartTime = 0; | |
| runBody(board, eatObjects) { | |
| // Move the creature based on brain outputs | |
| const linear = this.brainLinearMovement(); | |
| const angular = this.brainAngularMovement(); | |
| const heartTarget = this.brainHeartTarget(); | |
| // Change the heartrate based on brain outputs | |
| const percent = 0.005; | |
| this.heartrate = Math.max(Math.min(this.heartrate * (1 - percent) + heartTarget * percent, 1), 0); | |
| this.heartTime += this.heartrate * 0.02 + 0.001; | |
| this.heartTime = this.heartTime % 1; | |
| // Reduce satiety based on heartrate | |
| this.satiety -= (this.heartrate + 0.4) * 0.0001; | |
| this.move(linear, angular, board); | |
| // Handle eating | |
| eatObjects.forEach(eatObject => { | |
| this.tryEat(eatObject); | |
| }); | |
| // Handle dying | |
| if (this.satiety <= 0) { | |
| this.die(); | |
| } | |
| // Handle offspring | |
| if (this.satiety > 1) { | |
| const offspring = this.createOffspring(); | |
| offspring.forEach(creature => { | |
| board.addCreature(creature); | |
| }); | |
| } | |
| } | |
| draw(ctx, camera) { | |
| const focus = camera.getFocus(this); | |
| if (focus == Camera.Focus.NONE) { | |
| return; | |
| } | |
| const tileSize = camera.getTileSize(); | |
| ctx.save(); | |
| ctx.translate(this.x * tileSize, this.y * tileSize); | |
| ctx.rotate(this.angle); | |
| this.drawBody(ctx, tileSize, focus); | |
| ctx.restore(); | |
| } | |
| drawBody(ctx, tileSize, focus) { | |
| const innerColor = "hsl(" + this.color * 360 + ",75%,75%)"; | |
| const outerColor = "hsl(" + this.color * 360 + ",75%,20%)"; | |
| // Draw the body | |
| ctx.lineWidth = Math.round(tileSize / 30); | |
| ctx.fillStyle = innerColor; | |
| ctx.strokeStyle = outerColor; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, 0); | |
| ctx.arc(0, 0, tileSize / 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| // Draw eyes | |
| ctx.strokeStyle = outerColor; | |
| this.eyeList.drawAll(ctx, tileSize, focus); | |
| } | |
| static newRandom(x, y) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const color = Math.random(); | |
| const heartrate = 0.1; | |
| const satiety = 1; | |
| const eyeList = new Eye.List(); | |
| const eyeCount = Math.round(Math.random() * 3); | |
| for (let j = 0; j < eyeCount; j++) { | |
| eyeList.add(Eye.newRandom()); | |
| } | |
| const brainInputCount = 2 + eyeCount * 2; // [ Hunger, Heartbeat, eye1left, eye1right, eye2left ... ] | |
| const brainOutputCount = 6; // [ goForward, goBackward, turnLeft, turnRight, increaseHeartrate, decreaseHeartrate ] | |
| const brainNetwork = Network.newRandom([ | |
| brainInputCount, | |
| Math.max(brainInputCount + 1, brainOutputCount + 1), | |
| brainOutputCount, | |
| ]); | |
| const speciesName = Namer.namer.getName(); | |
| const generationName = Namer.namer.getName(); | |
| return new Creature( | |
| x, | |
| y, | |
| angle, | |
| color, | |
| heartrate, | |
| satiety, | |
| eyeList, | |
| brainNetwork, | |
| speciesName, | |
| generationName | |
| ); | |
| } | |
| static List = class extends Array { | |
| drawAll(context, camera) { | |
| this.forEach((creature) => creature.draw(context, camera)); | |
| } | |
| }; | |
| } |
| using System.Collections; | |
| using System.Collections.Generic; | |
| using UnityEngine; | |
| public class AsteroidManager : MonoBehaviour | |
| { | |
| [Header("References")] | |
| [SerializeField] | |
| private List<GameObject> asteroidPrefabs; | |
| [Header("Spawn Area")] | |
| [SerializeField] | |
| private float spawnRadius = 30.0f; | |
| [SerializeField] | |
| private float spawnDepth = 5.0f; | |
| [SerializeField] | |
| private float minSpawnPeriod = 0.2f; // Seconds per spawn | |
| [SerializeField] | |
| private float maxSpawnPeriod = 1.0f; // Seconds per spawn | |
| [SerializeField] | |
| private int initialBatchSize = 10; | |
| [Header("Spawn Properties")] | |
| [SerializeField] | |
| private float maxForceLinear = 1.0f; // Newton | |
| [SerializeField] | |
| private Vector3 maxForceOffset = new Vector3(0.0f, 0.0f, 0.0f); // { Newton, Newton, Newton } | |
| [SerializeField] | |
| private float maxTorque = 10.0f; // Newton * Meter | |
| [Header("Despawn Area")] | |
| [SerializeField] | |
| private float despawnRadius = 40.0f; | |
| [SerializeField] | |
| private float despawnDepth = 60.0f; | |
| [Header("Variables")] | |
| public List<AsteroidController> asteroids = new List<AsteroidController>(); | |
| private Queue<AsteroidController> despawnQueue = new Queue<AsteroidController>(); | |
| private float timeToNextSpawn = 0.0f; | |
| public static AsteroidManager manager; | |
| private void Start() | |
| { | |
| if (manager != null) | |
| { | |
| Debug.LogError("AsteroidManager.manager Already exists. There should only be one."); | |
| Destroy(gameObject); | |
| return; | |
| } | |
| manager = this; | |
| for (int i = 0; i < initialBatchSize; i++) | |
| { | |
| SpawnAsteroid(); | |
| } | |
| } | |
| private void FixedUpdate() | |
| { | |
| timeToNextSpawn -= Time.fixedDeltaTime; | |
| bool doSpawn = timeToNextSpawn <= 0.0f; | |
| if (doSpawn) | |
| { | |
| SpawnAsteroid(); | |
| timeToNextSpawn = GetRandomSpawnTime(); | |
| } | |
| // Check despawn area | |
| float despawnDepthSqr = despawnDepth * despawnDepth; | |
| float despawnRadiusSqr = despawnRadius * despawnRadius; | |
| foreach (AsteroidController asteroid in asteroids) | |
| { | |
| Vector3 position = asteroid.transform.position - transform.position; | |
| Vector3 projection = Vector3.Project(position, transform.forward); | |
| float radiusSqr = (position - projection).sqrMagnitude; | |
| float depthSqr = projection.sqrMagnitude; | |
| bool isBehind = Vector3.Dot(transform.forward, position) < 0; | |
| if (depthSqr >= despawnDepthSqr || radiusSqr >= despawnRadiusSqr || isBehind) | |
| { | |
| despawnQueue.Enqueue(asteroid); | |
| } | |
| } | |
| // Despawn everything in the queue | |
| while (despawnQueue.Count > 0) | |
| { | |
| AsteroidController asteroid = despawnQueue.Dequeue(); | |
| asteroids.Remove(asteroid); | |
| Destroy(asteroid.gameObject); | |
| } | |
| } | |
| private void OnDrawGizmosSelected() | |
| { | |
| // Draw the spawn range | |
| Gizmos.color = Color.green; | |
| GizmosExpanded.DrawCylinder(transform.position, transform.position + transform.forward * spawnDepth, spawnRadius); | |
| // Draw the despawn range | |
| Gizmos.color = Color.red; | |
| GizmosExpanded.DrawCylinder(transform.position, transform.position + transform.forward * despawnDepth, despawnRadius); | |
| } | |
| private void SpawnAsteroid() | |
| { | |
| // Instantiate the asteroid | |
| GameObject prefab = GetRandomPrefab(); | |
| Vector3 position = GetRandomPosition(); | |
| Quaternion rotation = GetRandomRotation(); | |
| Transform parent = transform; | |
| GameObject asteroidObject = Instantiate(prefab, position, rotation, parent); | |
| AsteroidController asteroid = asteroidObject.GetComponent<AsteroidController>(); | |
| // Apply movement | |
| Vector3 force = GetRandomForce(); | |
| Vector3 torque = GetRandomTorque(); | |
| asteroid.ApplyMovement(force, torque); | |
| // Keep a reference | |
| asteroids.Add(asteroid); | |
| } | |
| private GameObject GetRandomPrefab() | |
| { | |
| int index = Random.Range(0, asteroidPrefabs.Count); | |
| GameObject prefab = asteroidPrefabs[index]; | |
| return prefab; | |
| } | |
| private Vector3 GetRandomPosition() | |
| { | |
| float angle = Random.Range(0, Mathf.PI * 2); | |
| float radius = Random.Range(0, spawnRadius); | |
| float depth = Random.Range(0, spawnDepth); | |
| Vector3 depthOffset = depth * transform.forward; | |
| Vector3 circleOffset = GizmosExpanded.GetCirclePosition(angle, radius, transform.forward); | |
| Vector3 position = depthOffset + circleOffset + transform.position; | |
| return position; | |
| } | |
| private Quaternion GetRandomRotation() | |
| { | |
| Quaternion rotation = Quaternion.identity; | |
| return rotation; | |
| } | |
| private Vector3 GetRandomForce() | |
| { | |
| float forceLinear = Random.Range(0, maxForceLinear); | |
| Vector3 forceOffset = Vector3.Scale(Random.insideUnitSphere, maxForceOffset); | |
| Vector3 force = transform.forward * forceLinear + forceOffset; | |
| return force; | |
| } | |
| private Vector3 GetRandomTorque() | |
| { | |
| Vector3 axis = Random.onUnitSphere; | |
| float amount = Random.Range(0.0f, maxTorque); | |
| Vector3 torque = axis * amount; | |
| return torque; | |
| } | |
| private float GetRandomSpawnTime() | |
| { | |
| float time = Random.Range(minSpawnPeriod, maxSpawnPeriod); | |
| return time; | |
| } | |
| } |
| % Clear everything | |
| clear; | |
| clc; | |
| close all; | |
| % Read the data from the AAPL.csv file | |
| data = readtable("AAPL.csv", "VariableNamingRule", "preserve"); | |
| % Draw the High, Low, Close, and Volume plots within one figure | |
| figure; | |
| x = data.Date(:); | |
| subplot(2,2,1); plot(x, data.High(:)); title("i) High"); xlabel("Time"); ylabel("Money"); | |
| subplot(2,2,2); plot(x, data.Low(:)); title("ii) Low"); xlabel("Time"); ylabel("Money"); | |
| subplot(2,2,3); plot(x, data.Close(:)); title("iii) Close"); xlabel("Time"); ylabel("Money"); | |
| subplot(2,2,4); plot(x, data.Volume(:)); title("iv) Volume"); xlabel("Time"); ylabel("Shares"); | |
| % Draw the High, Low, Open, Close figure | |
| figure; | |
| highlow(data); | |
| % Calculate the average price | |
| data.Average = (data.Open + data.Close)./2; | |
| % Save the new data to the APPL-new.csv | |
| writetable(data,"APPL-new.csv"); |
| library ieee; | |
| use ieee.std_logic_1164.all; | |
| use ieee.numeric_std.all; | |
| entity AlarmSystem is | |
| port(CLOCK_50: in std_logic; | |
| SW: in std_logic_vector(17 downto 0); | |
| KEY: in std_logic_vector(2 downto 0); | |
| LEDG: out std_logic_vector(7 downto 0); | |
| LEDR: out std_logic_vector(17 downto 0); | |
| HEX7, HEX6, HEX5, HEX4, HEX2, HEX1, HEX0: out std_logic_vector(6 downto 0); | |
| -- AUDIO STUFF | |
| I2C_SDAT : inout std_logic; | |
| I2C_SCLK, AUD_XCK : out std_logic; | |
| AUD_ADCDAT : in std_logic; | |
| AUD_DACDAT : out std_logic; | |
| AUD_ADCLRCK, AUD_DACLRCK, AUD_BCLK : in std_logic); | |
| end AlarmSystem; | |
| architecture behaviour of AlarmSystem is | |
| -- Define the component types | |
| component BlinkSystem is | |
| port(enable, slowClock: std_logic; | |
| Seg3, Seg2, Seg1, Seg0: out std_logic_vector(6 downto 0); | |
| green: out std_logic_vector(7 downto 0); | |
| red: out std_logic_vector(17 downto 0)); | |
| end component; | |
| component DisarmSystem is | |
| port(clock: in std_logic; | |
| w: in std_logic_vector(1 downto 0); | |
| sseg2, sseg1, sseg0: out std_logic_vector(6 downto 0); | |
| disarm: out std_logic; | |
| stateOut: out std_logic_vector(1 downto 0)); | |
| end component; | |
| component PreScale is | |
| port(inClock: in std_logic; outClock: out std_logic); | |
| end component; | |
| -- Setup states | |
| signal trigger, isActive, frontDoor, sideWindow, motionSensor: std_logic := '0'; | |
| signal disarm: std_logic := '1'; | |
| signal slowClock: std_logic; | |
| -- Define the Audio types | |
| signal AudioIn, AudioOut : signed(15 downto 0); | |
| signal SamClk: std_logic; | |
| component AudioInterface is | |
| generic ( SID : integer := 100 ); | |
| port (CLOCK_50 : in std_logic; | |
| init : in std_logic; | |
| I2C_SDAT : inout std_logic; | |
| I2C_SCLK, AudMclk : out std_logic; | |
| AUD_ADCDAT : in std_logic; | |
| AUD_DACDAT : out std_logic; | |
| AUD_ADCLRCK, AUD_DACLRCK, AUD_BCLK : in std_logic; | |
| SamClk : out std_logic; | |
| AudioIn : out signed(15 downto 0); | |
| AudioOut : in signed(15 downto 0)); | |
| end component; | |
| component ToneGenerator is | |
| port(clock, enable: in std_logic; Freq: in unsigned(15 downto 0); WaveOut: out signed(15 downto 0)); | |
| end component; | |
| begin | |
| -- Generate slow clock | |
| scale: PreScale port map(CLOCK_50, slowClock); | |
| -- Instantiate the major systems | |
| ds: DisarmSystem port map(KEY(0), SW(2 downto 1), HEX2, HEX1, HEX0, disarm); | |
| bs: BlinkSystem port map(trigger, slowClock, HEX7, HEX6, HEX5, HEX4, LEDG, LEDR); | |
| -- Handle the states | |
| isActive <= SW(0); | |
| frontDoor <= SW(17); | |
| sideWindow <= SW(16); | |
| motionSensor <= SW(15); | |
| process (CLOCK_50) | |
| begin | |
| if CLOCK_50='1' then | |
| if disarm='1' or KEY(1)='0' then | |
| trigger <= '0'; | |
| end if; | |
| if (isActive and (frontDoor or sideWindow or motionSensor))='1' then | |
| trigger <= '1'; | |
| end if; | |
| end if; | |
| end process; | |
| -- Handle Audio | |
| assm: AudioInterface generic map ( SID => 51512) | |
| port map( Clock_50 => CLOCK_50, AudMclk => AUD_XCK, -- period is 80 ns ( 12.5 Mhz ) | |
| init => KEY(2), -- +ve edge initiates I2C data | |
| I2C_Sclk => I2C_SCLK, | |
| I2C_Sdat => I2C_SDAT, | |
| AUD_BCLK => AUD_BCLK, AUD_ADCLRCK => AUD_ADCLRCK, AUD_DACLRCK => AUD_DACLRCK, | |
| AUD_ADCDAT => AUD_ADCDAT, AUD_DACDAT => AUD_DACDAT, | |
| AudioOut => AudioOut, AudioIn => AudioIn, SamClk => SamClk ); | |
| tg: ToneGenerator port map(clock => SamClk, enable => slowClock and trigger, Freq => "0100000000000000", waveOut => AudioOut); | |
| end architecture behaviour; |
This website is a good example of my skill with HTML, JS, and CSS.
The first image is a hand-soldered digital dice circuit (for the SFU class ENSC 220) with through-hole and surface mount components. The end result simulates rolling a die when you press the capacitive button.
The second and third images are of an automatic window actuation system controlled by weather forecast (for the SFU classes ENSC 100 and ENSC 105W). The project was completed from the ground up in a small team, with the goal of solving a problem we encounter in the world.
These images show some iterations of a tiny pocket-watch-like mechanism. As a design requirement, 3D printing was the manufacturing method.
Below are a few photos of some old and small personal projects: