Skip to content

Why does javascript create spurious images using ajax?

Firstly I must apologise for the length of the code in this question. It is based on Django and javascript and I have stripped out as much as I can to give a working example

The challenge is to create a composite image using a random number of rooks – all black rooks on the top row and all red rooks on the bottom row

enter image description here

This works perfectly when the page is first loaded, but if I click on the New Board button, then randomly red rooks might appear in the top row, or black in the bottom

enter image description here

(The image was downloaded from here)

[Edit] If I add a trace in the javascript function displayPieceImage it reports the correct number of images being drawn[/Edit}

html

<!--  pages/home.html -->
{% load static %}

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">

    <script src="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>

</head>
<body>
    {% block content %}
        <div style="display:none;" id="pieceImages"></div>
        <p>Red:<span id="red-count"></span> Black:<span id="black-count"></span></p>
        <canvas id="top-image" style="background-color:#ffff00;"></canvas>
        <p><button id="new-board">New Board</button>
    {% endblock content %}

    <script type="text/javascript" src="{% static 'js/test.js' %}"></script>
    
</body>
</html>

urls.py

#pages/urls.py

from django.urls import path

from .views import HomePageView, NewBoard

urlpatterns = [
    path('', HomePageView.as_view()),
    path('new-board', NewBoard.as_view(), name= 'new-board'), 
]

views.py

# pages/views.py

import os
import io
import random
import base64
from PIL import Image as PilImage

from django.views.generic import View
from django.shortcuts import render
from django.http import JsonResponse
from django.conf import settings

PIECE_NAMES = ['red_rook', 'black_rook']
IMAGE_EXTENSION = 'png'

class HomePageView(View):
    template = 'pages/home.html'

    def get(self, request):
        context = {}
        return render(request, self.template, context)

class NewBoard(View):
    template = 'pages/home.html'

    def get(self, request):
        pieces = []
        red_count = random.randint(1, 5)
        black_count = random.randint(1, 5)
        for index in range(red_count):
            pieces.append('red_rook')
        for index in range(black_count):
            pieces.append('black_rook')
        context = {
            'pieces': pieces,
            'colour_length': {'red': red_count, 'black': black_count},
            'max_colour_length': max(red_count, black_count),
            'piece_images': piece_images,
        }
        return JsonResponse(context, safe=False)

def encode_image(image):
    """Return image encoded to base64 from a PIL.Image.Image."""
    io_buffer = io.BytesIO()
    image.save(io_buffer, format='PNG')
    saved_image = io_buffer.getvalue()
    encoded_image = ''.join(['data:image/jpg;base64,', base64.b64encode(saved_image).decode()])
    return encoded_image

def _get_piece_images():
    """Return a dict of encoded piece images."""
    images = {}
    piece_directory = os.sep.join([settings.STATICFILES_DIRS[0], 'images'])
    for piece in PIECE_NAMES:
        image_path = os.path.join(piece_directory, f'{piece}.{IMAGE_EXTENSION}')
        image = PilImage.open(image_path)
        piece_image = encode_image(image)
        images[piece] = piece_image
    return images

# piece_images is a dict that contains all the valid pieces
piece_images = _get_piece_images()

test.js

// static/js/test.js

// 'Constants' to define image cropping
var RAW_WIDTH = 552
var RAW_HEIGHT = 640
var SCALE = 0.1
var PIECE_WIDTH = RAW_WIDTH * SCALE
var PIECE_HEIGHT = RAW_HEIGHT * SCALE
var CANVAS_HEIGHT = 3 * PIECE_HEIGHT;

// Initialise global variables
var last_colour = '';
var colour_row = 0;
var colour_column = 0;

$(document).ready(function () {   
    var new_board_link = document.getElementById('new-board');
    new_board_link.onclick = function(){getNewBoard()};
    getNewBoard();
});

function getNewBoard() {
    $.ajax(
        {
            type: "GET",
            url: 'new-board',
            cache: false,
            success: function (context) {
                displayPieces(context);
            }
        }
    );
}

function displayPieces(context) {
    // Display all of the pieces for a given position 
    var red_count = document.getElementById('red-count');
    var black_count = document.getElementById('black-count');
    red_count.innerText = context.colour_length['red']
    black_count.innerText = context.colour_length['black']

    var max_colour_length = context.max_colour_length;
    var position_id = 'top-image'  // used to identify the position
    var ctx =  prepareCanvas(position_id, max_colour_length)
    var piece_index = 0;
    context.pieces.forEach(piece_name => {
        createModelImage (position_id, piece_index) 
        displayPieceImage (context, ctx, position_id, piece_name, piece_index)
        piece_index ++;
        }
    )
}

function prepareCanvas(position_id, max_colour_length) {
    // Create a canvas and return the canvas context (ctx) for a given position 
    canvas = document.getElementById(position_id);
    canvas.width = max_colour_length * PIECE_WIDTH;
    canvas.height = CANVAS_HEIGHT;
    var ctx = canvas.getContext('2d');
    return ctx
}

function displayPieceImage (context, ctx, position_id, piece_name, piece_index) {
    // Draw a piece and its object.
    var image_id = 'source'+position_id+piece_index
    var image = document.getElementById(image_id);
    var position = piecePosition(piece_name)
    var pos_x = position['pos_x']
    var pos_y = position['pos_y']
    image.src = context.piece_images[piece_name];
    image.addEventListener('load', e => {    
        ctx.drawImage(image, 0, 0, RAW_WIDTH, RAW_HEIGHT, pos_x, pos_y, PIECE_WIDTH, PIECE_HEIGHT);
    });
}

function piecePosition(piece_name) {
    // Return the position of the piece relative to the canvas.
    var piece_colour = piece_name.substring(0, 1);
    if (last_colour != piece_colour) {
        last_colour = piece_colour;
        colour_column = -1;
    }
    colour_row = 'br'.indexOf(piece_colour.substr(0, 1));
    colour_column ++;
    position = {'pos_x': colour_column * PIECE_WIDTH, 'pos_y': colour_row * PIECE_HEIGHT}
    return position
}

function createModelImage (position_id, index) {
    // Add a piece image to the hidden element 'pieceImages'
    var pieceImages = document.getElementById('pieceImages');
    var img = document.createElement('img');
    img.id = 'source'+position_id+index;
    pieceImages.appendChild(img);
}

Answer

The problem is quite simple. In the function createModelImage you add images to the div with the id pieceImages. You never remove these images from the div when creating a new board. So there are old images with the ids which get used instead of the image tag that should be used. Instead you should remove these old image tags when making a new board:

function displayPieces(context) {
    document.getElementById('pieceImages').innerHTML = '';
    // Your original code here
}