3

I have drawing service in Angular with several methods that take various drawing-related parameters and a parameter that references the dynamically-generated Id of a canvas element like the drawLizard method herein:

drawing.service:

import { Injectable } from '@angular/core';
import { Genotype } from './genotype.model';

@Injectable({
  providedIn: 'root'
})
export class DrawingService {
  constructor() {
  }

  drawArc(x: number, y: number, radius: number, startAngle:number, endAngle: number, elementId: string, color: string, fillStatus: boolean) {
    let canvas = <HTMLCanvasElement> document.getElementById(elementId);
    // console.log(canvas); //TODO fix
    if (canvas) {
      let ctx = canvas.getContext('2d');
      ctx.beginPath();
      ctx.arc(x, y, radius, startAngle, endAngle);
      if (fillStatus) {
        ctx.fillStyle = color;
        ctx.fill();
      } else {
        ctx.lineWidth = 1;
        ctx.strokeStyle = color;
        ctx.stroke();
      }
    } else {
      //TODO handle error here
    }
  }

  drawTriangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3:number, elementId: string, color: string, fillStatus: boolean){

    // let canvasElementTest = <HTMLCanvasElement>document.getElementById("first");
    // console.log(canvasElementTest); //TODO fix
    let canvasElement = <HTMLCanvasElement>document.getElementById(elementId);
    // let canvasElement: Element = <HTMLCanvasElement>document.getElementsByClassName(elementId);
    console.log(canvasElement); //TODO fix
    if (canvasElement){
      let context = canvasElement.getContext("2d");
      context.beginPath();
      context.moveTo(x1, y1);
      context.lineTo(x2, y2);
      context.lineTo(x3, y3);
      context.closePath();
      context.lineWidth = 1;
      context.strokeStyle = color;
      context.stroke();
      if(fillStatus){
        context.fillStyle = color;
        context.fill();
    }
  } else{
    //TODO throw/handle error here
  }
}

drawEllipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle:number, endAngle:number, elementId: string, color: string, fillStatus: boolean){
  let canvas = <HTMLCanvasElement> document.getElementById(elementId);
  if(canvas){
    let ctx = canvas.getContext('2d');
    // ctx.setLineDash([])
    ctx.beginPath();
    ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, false); //x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise
    ctx.stroke();
    if(fillStatus){
      ctx.fillStyle = color;
      ctx.fill();
    }
    ctx.closePath();
  } else{
    //TODO handle error here
  }

}

drawLizard(canvasId: string, genotype: Genotype){
  //head
  this.drawTriangle(37.5,87.5,37.5,112.5,25,100, canvasId,"black", true);
  this.drawArc(37.5, 100, 12.5, 3*Math.PI/2, Math.PI/2, canvasId,"black", true);
  // console.log("mark got here, too"); //TODO fix

  //eyes
  this.drawArc(37.5, 106.25, 2, 0, 2*Math.PI, canvasId,"white", true);
  this.drawArc(37.5, 93.75, 2, 0, 2*Math.PI, canvasId,"white", true);
  this.drawArc(37.5, 106.25, 0.5, 0, 2*Math.PI, canvasId,"black", true);
  this.drawArc(37.5, 93.75, 0.5, 0, 2*Math.PI, canvasId,"black", true);

  //body
  this.drawEllipse(100, 100, 50, 10, 0, 0, 2 * Math.PI, canvasId, 'black', true);

  //legs
  this.drawEllipse(140, 110, 30, 3, 0.65, 0, 2 * Math.PI, canvasId, 'black', true);
  this.drawEllipse(140, 90, 30, 3, 0.9+Math.PI/2, 0, 2 * Math.PI, canvasId, 'black', true);
  this.drawEllipse(70, 110, 30, 3, -Math.PI/6, 0, 2 * Math.PI, canvasId, 'black', true);
  this.drawEllipse(70, 90, 30, 3, Math.PI/6, 0, 2 * Math.PI, canvasId, 'black', true);

  //random polka dots
  // let colorArray = new Array<string>("blue", "pink", "orange", "#FF00FF", "red", "#00FFFF", "#800000", "#00FF00", "#008000", "#00FFFF", "#008080", "#BFBFFE", "#800080");
  // this.drawArc(77, 95, 3, 0, 2*Math.PI, canvasId, colorArray[Math.floor(Math.random()*colorArray.length)], true);

  this.drawArc(60, 101, 3, 0, 2*Math.PI, canvasId, genotype.getAllele1(), true);
  this.drawArc(85, 102, 3, 0, 2*Math.PI, canvasId, genotype.getAllele2(), true);
  this.drawArc(109, 94, 3, 0, 2*Math.PI, canvasId, genotype.getAllele1(), true);
  this.drawArc(120, 102, 3, 0, 2*Math.PI, canvasId, genotype.getAllele2(), true);
}
}

I have several such canvas elements dynamically generated in my lizard-display.component.html:

...
<mat-card small class="example-card" *ngFor="let individual of individuals; let i=index">
...
<canvas id="lizard-canvas{{i}}" width="200" height="200" style="border:1px solid #c3c3c3;">
    Your browser does not support the canvas element.
</canvas>
...
</mat-card>
...

In my lizard-display.component.ts file, I generate an example lizard and attempt to draw it:

import { OnInit, Component } from '@angular/core';
import { DrawingService } from '../drawing.service';
import { Genotype } from '../genotype.model';
import { Gene } from '../gene.model';
import { Organism } from '../organism.model';
import { ColorNameService } from '../color-name.service';
import { IndividualGenerationService } from '../individual-generation.service';

@Component({
  selector: 'app-lizard-display',
  templateUrl: './lizard-display.component.html',
  styleUrls: ['./lizard-display.component.css'],
  providers: [DrawingService]
})
export class LizardDisplayComponent implements OnInit {
  private individuals: Array<Organism> = new Array<Organism>();

  constructor(private ds: DrawingService, private cns: ColorNameService, private individualGenService: IndividualGenerationService) { }

  ngOnInit() {

      //TODO delete me after fleshed out more
      let testIndividual: Organism = this.individualGenService.makeIndividual("green", "blue");
      let genotype: Genotype = testIndividual.getGeneByName("spot color").getGenotype();
      this.individuals.push(testIndividual);

      this.ds.drawLizard('lizard-canvas0', genotype);
  } 
}

Lots of solutions out there (e.g.,here) seem to work for folks who don't have to then apply .getContext('2d') on their element.

As it is, I can confirm that canvasElement (at least in the drawTriangle method called by drawLizard in the drawing service) using these solutions on SO is null.

I'm hoping for Angular2+-like solutions to this issue.

A functional, reasonably minimal reproducible example can be found here:

git clone https://github.com/Atticus29/population-fragmentation.git
cd population-fragmentation
git checkout dynamicCanvasSO
npm install
ng serve

Navigate to http://localhost:4200/ in a browser

Atticus29
  • 4,190
  • 18
  • 47
  • 84
  • Have you attempted to use ViewChildren? – bryan60 Sep 14 '18 at 20:21
  • I have not and wouldn't quite know how to start. I've used it to pass data between a child component and a parent component, but never for something like this... – Atticus29 Sep 14 '18 at 20:23

1 Answers1

3

the angular way to get html element references is with ViewChildren like so:

export class LizardDisplayComponent implements OnInit, AfterViewInit {
  @ViewChildren('canvases') canvases: QueryList<ElementRef>;
  private individuals: Array<Organism> = new Array<Organism>();

  constructor(private ds: DrawingService, private cns: ColorNameService, private individualGenService: IndividualGenerationService) { }

  ngOnInit() { } 

  ngAfterViewInit() { // view children arent available till this hook
     this.canvases.forEach(canvas => console.log(canvas)); 
     //here you'll see each has a nativeElement property that is a reference to that element, you can theoretically pass that to your service to be drawn on, this is just looping over everything with the '#canvases' tag applied to it

     let testIndividual: Organism = this.individualGenService.makeIndividual("green", "blue");
     let genotype: Genotype = testIndividual.getGeneByName("spot color").getGenotype();
     this.individuals.push(testIndividual);

     //you can pass them to your service like this instead of the id
     this.ds.drawLizard(this.canvases.toArray()[0], genotype);
  }
}

the viewchildren decorator basically queries the component for any elements that have the have the tag you specify in the decorator, you apply the tag in HTML like:

...
<mat-card small class="example-card" *ngFor="let individual of individuals; let i=index">
...
<canvas #canvases id="lizard-canvas{{i}}" width="200" height="200" style="border:1px solid #c3c3c3;">
    Your browser does not support the canvas element.
</canvas>
...
</mat-card>
...

then you can pass it to your service:

drawLizard(canvasRef: ElementRef, genotype: Genotype){
  this.drawTriangle(37.5,87.5,37.5,112.5,25,100, canvasRef,"black", true);
  ...
}

drawTriangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3:number, elementRef: ElementRef, color: string, fillStatus: boolean){
  let canvasElement = <HTMLCanvasElement>elementRef.nativeElement;
  ...
}
bryan60
  • 28,215
  • 4
  • 48
  • 65
  • Thanks, @bryan60! I'll try this out in a bit. For the time being, I fleshed out the drawing service for you in the question. – Atticus29 Sep 14 '18 at 20:36
  • P.S. There's a repo available. Feel welcome to have a look. – Atticus29 Sep 14 '18 at 20:37
  • I'm getting a TypeError: Cannot read property 'nativeElement' of undefined – Atticus29 Sep 14 '18 at 23:11
  • not really sure, this should work, looks like you're calling the function before the viewchildren are available or you're not tagging / querying them correctly. You could put together a stack blitz or something. No offense but i dont really want to pull down and execute untrusted code on my machine. – bryan60 Sep 17 '18 at 13:34
  • I'm in the mountains for the week and won't have time or wifi to put the stack blitz together before the expiration date. Thanks for the help you've been able to provide so far; I totally understand your unwillingness to pull the code down. The only suggestion I can make is that looking at the code on github without downloading it might help? – Atticus29 Sep 17 '18 at 17:52
  • I made a quick video of what I'm seeing, my code, and the errors I'm seeing in the console, in case you can use that to help. Again, much appreciated in advance: https://youtu.be/CjcTuAQV1TU – Atticus29 Sep 17 '18 at 18:05
  • nvm. My viewChildren was looking for the 'cavas' tag, not the 'canvases' tag. Thanks! – Atticus29 Sep 19 '18 at 18:27