2025

Busy Work Period

Marking season. Two words that can send shivers down an academic’s spine. While I genuinely love seeing what students create, the sheer volume and, let’s be honest, sometimes the state of submissions can be a real grind. My personal nemesis? The sprawling project folder with code scattered everywhere, forcing me into a click-fest of opening and closing files. A meme, my students were quick to adopt from my constant reminders, “Use the F***EN TEMPLATE!” though never said out loud in this manner. Students were quick to make the jokes… but alas, for some, that’s as far as the advice goes.

Even though tools like VS Code have a “Find in Files” feature, my brain prefers a single stream of information for that initial pass. Multiple tabs quickly become visual noise for me, and I lose my flow.

This frustration led to a little weekend project: a Python script! It’s nothing fancy, but it’s become my secret weapon. I point it at a student’s project folder, and bam – it sucks all the code into one consolidated file. Suddenly, I can get a bird’s-eye view, spot if comments are MIA, and generally assess the lay of the land without playing tab-roulette. It’s amazing how much smoother this makes the initial triage.

I’m planning to share this little script with you all because if it can save me this much headache, maybe it can help you too. Let’s make marking season a little more manageable, shall we?

import os

def combine_files_by_type(root_dir, file_extensions, output_base_name):
    """
    Combines all files of specified extensions in a directory and its subdirectories into separate output files, one for each extension.

    Args:
        root_dir (str): The path to the root directory to search for files.
        file_extensions (list): A list of file extensions to process (e.g., ['.php', '.js']).
        output_base_name (str): The base name for the output files.
                                (e.g., 'combined' will result in 'combined.php', 'combined.js')
    """
    if not os.path.isdir(root_dir):
        print(f"Error: Directory '{root_dir}' not found.")
        return

    for extension in file_extensions:
        output_filename = f"{output_base_name}{extension}"
        files_found_for_extension = False
        
        try:
            with open(output_filename, 'w', encoding='utf-8') as outfile:
                print(f"\nProcessing '{extension}' files into '{output_filename}'...")
                file_count = 0
                for subdir, _, files in os.walk(root_dir):
                    # Sort files for consistent order, helps if order matters somewhat
                    sorted_files = sorted(files)
                    for filename in sorted_files:
                        if filename.endswith(extension):
                            files_found_for_extension = True
                            filepath = os.path.join(subdir, filename)
                            relative_path = os.path.relpath(filepath, root_dir) # Get path relative to root_dir
                            try:
                                with open(filepath, 'r', encoding='utf-8') as infile:
                                    content = infile.read()
                                    # Use relative path in the separator for better context
                                    outfile.write(f"\n\n----- {relative_path} -----\n\n")
                                    outfile.write(content)
                                    print(f"  Added: {filepath}")
                                    file_count += 1
                            except Exception as e:
                                print(f"  Error reading file {filepath}: {e}")
                
                if files_found_for_extension:
                    print(f"Successfully combined {file_count} '{extension}' file(s) into '{output_filename}'")
                else:
                    print(f"No '{extension}' files found in '{root_dir}'. '{output_filename}' created empty or not at all if it existed.")
                    # If no files were found, we would have an empty combined file. We can delete it if it's truly empty.
                    if os.path.exists(output_filename) and os.path.getsize(output_filename) == 0:
                        os.remove(output_filename)
                        print(f"Removed empty output file: '{output_filename}'")


        except Exception as e:
            print(f"Error writing to output file {output_filename}: {e}")

if __name__ == "__main__":
    input_directory = input("Enter the root directory to search: ")
    output_basename = input("Enter the base name for the combined output files (e.g., 'combined_project'): ")

    # Define the file types you want to process
    extensions_to_process = ['.php', '.js', '.html', '.css', '.sql']

    if input_directory and output_basename:
        combine_files_by_type(input_directory, extensions_to_process, output_basename)
    else:
        print("Input directory and output base name cannot be empty.")
Code language: Python (python)

Embarking on an Adventure: My Journey to Off-Grid Living and the Launch of Galleon Acre!

Hey everyone,

I’m thrilled to finally share some exciting plans I’ve been working on and to officially announce the launch of my new YouTube channel, Galleon Acre! You can find it here: https://www.youtube.com/@GalleonAcre

Welcome to Galleon Acre! This channel is where I, Charles, and my faithful companion Rusty, will be documenting our life-changing adventure in the stunning heart of Scotland’s Highlands and Islands.

Our mission? To breathe new life into an overgrown acre of land and a tired old stone cottage. We’re slowly but surely shaping them into a place where we can live simply, sustainably, and eventually, completely off-grid.

The next two years are all about preparation. Before I can fully embrace the off-grid lifestyle for a whole year, there’s a lot to do. This initial phase is all about learning, building, planting, and getting everything to a point where the land can support me.

Alongside transforming the land, I’ve got some essential renovations to tackle on the cottage to make it a cozy and functional home. The bathroom, utility room, and porch are all on the list, as is building a greenhouse and, importantly, sealing the roof! It’s a big to-do list, but every step is a step closer to the dream.

Adding to the current hustle, I’m also on the home stretch of my Master’s degree, which I’m due to finish this July!

Now, before I can truly throw myself into the more physical aspects of this adventure, there’s a personal hurdle I need to address – a medical issue with my chest/heart. Getting this sorted is a top priority so I can regain my fitness and be ready for the demands of this lifestyle. The good news is, I can definitely work on my fitness levels while I’m prepping the land and the house – multitasking at its finest! And yes, somewhere in all this, I’m on a quest to find a dentist and get some new teeth!

The ultimate goal is to spend a full year living completely self-sufficiently. That means no connectivity. Just the rhythm of the land, the changing seasons, and the profound quiet of nature. I’ll still be using some technology to film, of course!

Now, you might be wondering how I’ll share this part of the journey if I’m offline. Well, while I’ll be stepping away from my digital life in terms of being connected during that year – even from uploading videos myself – the story won’t stop. I’ll be filming throughout, and a friend will be kindly sharing those moments here on Galleon Acre as they unfold.

And when that incredible year comes to an end? I’ll return to the channel to reflect on everything I’ve learned and what’s changed.

I envision the Galleon Acre channel as a personal and friendly space where I can share the highs, the lows, the learning curves, and the beauty of this entire process. Expect lots of updates, images, and videos of me, Rusty, the land, the renovation projects, and all the little moments in between, here on the blog.

I’m so incredibly excited to share this journey with you all. Please check out Galleon Acre, and I look forward to connecting with you there!

Wavey Line Video Effect

Creating a Hand-Drawn Wiggle/Boil Line Effect with Code (No After Effects Required!)

I’ve had the concept for my intro video in my head for a while for my Galleon Acre YouTube channel. It’s just a channel where I’ll document projects around my house, garden, and acre of land. But I wanted to sketch over a drone aerial shot of the garden with my rough plans for future projects — sort of like a “drawn-on-the-blueprint” visual.

I knew this boiling/wiggling effect is easily achieved in After Effects and similar programs, but I don’t own those or plan on buying them. I’m happy with my workflow in DaVinci Resolve and feel comfortable using it for my video editing. So the question became:

Can I create this boiling line effect using just code?

It didn’t seem (in my head) like an overly complex problem — just tricky enough to be interesting. My artistic ability is quite limited, so I’m always looking for ways to automate visual flourishes without much manual drawing.

So with this in mind, my process flow became

Input

Line Drawn Image

Process

Apply Effect and Generate Frames

Output

Animated File

Libraries Used and Why

I thought I’d take a second here to explain the libraries I ended up using. I don’t always fully understand every line of a library’s documentation, but I do like to know why I’m importing something. Here’s what made the cut:

  • Pillow (PIL): This is how I loaded in my line drawing image (a PNG file with transparency). It also lets me manipulate and save images — think of it as Photoshop’s extremely nerdy cousin.
  • NumPy: Essential for turning my image into raw numerical data that I can mess with. It’s great for working with grids, which is exactly what an image is.
  • SciPy (scipy.ndimage): This is where the magic happened. I used it to generate smooth noise with gaussian_filter, and most importantly, to apply the displacement with map_coordinates. That last one lets you remap where pixels go — and that’s really the heart of the effect.
  • ImageIO + imageio-ffmpeg: I wanted to output my frames as a .mov file with transparency (so I could layer it over drone footage in Resolve). MP4 doesn’t have transparency, so I used this combo to export it in ProRes 4444 format.

You can install all these using pip:

pip install pillow dumpy script image image-ffmpegCode language: Bash (bash)

Attempt One: Move the Entire Image Randomly

So, the best place to start would be to work out how this effect can be done. I encountered multiple “Solutions” to the problem, from randomly moving the entire image via a random offset to putting the frames together. While this did work – technically, it wasn’t the natural way I envisioned. I was thinking of something between a wiggle path or a hand-drawn animator effect. The produced effect isn’t natural… it lacks that hand-drawn look i was aiming for.

def jiggle_image(image, intensity=1):
    dx = np.random.randint(-intensity, intensity + 1)
    dy = np.random.randint(-intensity, intensity + 1)
    return image.transform(image.size, Image.AFFINE, (1, 0, dx, 0, 1, dy))Code language: Python (python)

Attempt Two: Per-Pixel Jiggle

For a second attempt, I thought, what if I jitter each line pixel independently?

This created an effect, but not the one I wanted. It was more like a noisy chalkboard or low-resolution scatter effect. The lines broke apart and lost their cohesion, and it lacked fluidity.

def per_pixel_jiggle(image, intensity=1):
    array = np.array(image)
    height, width, _ = array.shape
    new_array = np.zeros_like(array)
    alpha_channel = array[:, :, 3]
    y_coords, x_coords = np.where(alpha_channel > 0)
    for x, y in zip(x_coords, y_coords):
        dx = np.random.randint(-intensity, intensity + 1)
        dy = np.random.randint(-intensity, intensity + 1)
        new_x = x + dx
        new_y = y + dy
        if 0 <= new_x < width and 0 <= new_y < height:
            new_array[new_y, new_x] = array[y, x]
    return Image.fromarray(new_array, "RGBA")
Code language: Python (python)

Attempt Three: Use Displacement Maps

Back to the research, I encountered several articles that mentioned using displacement maps to create the effect in After Effects. This was the breakthrough moment; I decided to try and generate a smooth random displacement field and then warp the original image according to it

def generate_displacement_field(shape, scale=16, amplitude=1.5):
    h, w = shape
    low_res_shape = (h // scale, w // scale)
    dx = gaussian_filter(np.random.randn(*low_res_shape), sigma=1)
    dy = gaussian_filter(np.random.randn(*low_res_shape), sigma=1)
    zoom_factors = (h / dx.shape[0], w / dx.shape[1])
    dx = zoom(dx, zoom_factors, order=1)
    dy = zoom(dy, zoom_factors, order=1)
    return dx * amplitude, dy * amplitude


def apply_displacement(image, dx, dy):
    array = np.array(image)
    h, w, c = array.shape
    coords_y, coords_x = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
    coords = np.array([
        np.clip(coords_y + dy, 0, h - 1),
        np.clip(coords_x + dx, 0, w - 1)
    ])
    warped = np.zeros_like(array)
    for i in range(c):
        warped[..., i] = map_coordinates(array[..., i], coords, order=1, mode='reflect')
    return Image.fromarray(warped.astype(np.uint8), "RGBA")Code language: Python (python)

This version worked! The image stays cohesive, but the lines jitter organically to mimic the wobbly, hand-drawn animation I had in my head. It’s subtle, clean, and much more usable in a professional context.

Exporting to Transparent Video for Resolve

Since I planned to overlay this animation in DaVinci Resolve, I needed a transparent .mov video. MP4 doesn’t support alpha channels, so I used ProRes 4444:

A small example of the overlay applied to a video file, enjoy Rusty’s reaction to the drone being in the air!