Skip to content

Circle Drawer

Live islandRuns locally in this page

Page shell

<article>
  <h1 style="text-align: center; margin-bottom: 5px;"
  >Circle Drawer</h1>
  <circle-drawer></circle-drawer>
</article>
<script src="./build/cami.cdn.js"></script>
<!-- CDN version below -->
<!-- <script src="https://unpkg.com/cami@0.4/build/cami.cdn.js"></script> -->
<style>
  .circle-drawer-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 20px;
  }
  .button-group {
    margin-bottom: 10px;
    display: flex;
    flex-direction: row;
    gap: 10px;
  }
  .canvas-container {
    width: 500px;
    height: 400px;
    border: 1px solid black;
    position: relative;
  }
</style>
<script type="module" src="./island.js"></script>

Island source

const { html, ReactiveElement } = cami
class CircleDrawerElement extends ReactiveElement {
    circles = [];
    history = [[]];
    historyIndex = 0;
    addCircle(event) {
        const canvas = event.currentTarget;
        const rect = canvas.getBoundingClientRect();
        const circle = {
            x: event.clientX - rect.left,
            y: event.clientY - rect.top,
            radius: 25,
            color: 'white',
            borderColor: 'black',
        };
        this.circles = [...this.circles, circle];
        this.commitHistory();
    }
    setCircleColor(index, color) {
        this.circles = this.circles.map((circle, circleIndex) => circleIndex === index ? { ...circle, color } : circle);
    }
    commitHistory() {
        this.history = [...this.history.slice(0, this.historyIndex + 1), structuredClone(this.circles)];
        this.historyIndex += 1;
    }
    undo() {
        if (this.historyIndex === 0)
            return;
        this.historyIndex -= 1;
        this.circles = structuredClone(this.history[this.historyIndex]);
    }
    redo() {
        if (this.historyIndex >= this.history.length - 1)
            return;
        this.historyIndex += 1;
        this.circles = structuredClone(this.history[this.historyIndex]);
    }
    template() {
        return html `
      <main class="circle-drawer-container">
        <section class="button-group">
          <button @click=${() => this.undo()} ?disabled=${this.historyIndex === 0}>Undo</button>
          <button @click=${() => this.redo()} ?disabled=${this.historyIndex === this.history.length - 1}>Redo</button>
        </section>
        <section class="canvas-container" @click=${(event) => this.addCircle(event)}>
          ${this.circles.map((circle, index) => html `
            <span
              role="img"
              aria-label="circle"
              style=${`position:absolute;left:${circle.x - circle.radius}px;top:${circle.y - circle.radius}px;width:${circle.radius * 2}px;height:${circle.radius * 2}px;border-radius:50%;background:${circle.color};border:1px solid ${circle.borderColor}`}
              @mouseover=${() => this.setCircleColor(index, 'gray')}
              @mouseout=${() => this.setCircleColor(index, 'white')}
            ></span>
          `)}
        </section>
      </main>
    `;
    }
}
customElements.define('circle-drawer', CircleDrawerElement);
import { html, ReactiveElement } from 'cami'

interface Circle {
  x: number
  y: number
  radius: number
  color: 'white' | 'gray'
  borderColor: string
}

class CircleDrawerElement extends ReactiveElement {
  circles: Circle[] = []
  history: Circle[][] = [[]]
  historyIndex: number = 0

  addCircle(event: MouseEvent): void {
    const canvas = event.currentTarget as HTMLElement
    const rect = canvas.getBoundingClientRect()
    const circle: Circle = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
      radius: 25,
      color: 'white',
      borderColor: 'black',
    }
    this.circles = [...this.circles, circle]
    this.commitHistory()
  }

  setCircleColor(index: number, color: Circle['color']): void {
    this.circles = this.circles.map((circle, circleIndex) =>
      circleIndex === index ? { ...circle, color } : circle,
    )
  }

  commitHistory(): void {
    this.history = [...this.history.slice(0, this.historyIndex + 1), structuredClone(this.circles)]
    this.historyIndex += 1
  }

  undo(): void {
    if (this.historyIndex === 0) return
    this.historyIndex -= 1
    this.circles = structuredClone(this.history[this.historyIndex])
  }

  redo(): void {
    if (this.historyIndex >= this.history.length - 1) return
    this.historyIndex += 1
    this.circles = structuredClone(this.history[this.historyIndex])
  }

  template(): ReturnType<typeof html> {
    return html`
      <main class="circle-drawer-container">
        <section class="button-group">
          <button @click=${() => this.undo()} ?disabled=${this.historyIndex === 0}>Undo</button>
          <button @click=${() => this.redo()} ?disabled=${this.historyIndex === this.history.length - 1}>Redo</button>
        </section>
        <section class="canvas-container" @click=${(event: MouseEvent) => this.addCircle(event)}>
          ${this.circles.map((circle: Circle, index: number) => html`
            <span
              role="img"
              aria-label="circle"
              style=${`position:absolute;left:${circle.x - circle.radius}px;top:${circle.y - circle.radius}px;width:${circle.radius * 2}px;height:${circle.radius * 2}px;border-radius:50%;background:${circle.color};border:1px solid ${circle.borderColor}`}
              @mouseover=${() => this.setCircleColor(index, 'gray')}
              @mouseout=${() => this.setCircleColor(index, 'white')}
            ></span>
          `)}
        </section>
      </main>
    `
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'circle-drawer': CircleDrawerElement
  }
}

customElements.define('circle-drawer', CircleDrawerElement)