Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions components/annotated-source.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { IGACAnnotation } from '../services/gac-annotations';
import Highlight from 'react-highlight';
import { useRef, useState, useMemo, useLayoutEffect } from 'react';
import ReactMarkdown from 'react-markdown/with-html';

interface IAnnotatedSourceProps {
code: string;
annotations: IGACAnnotation[];
}

function annotationLines(annotation: IGACAnnotation) {
const result = [];
for (let line = annotation.line; line <= annotation.endLine; line++) {
result.push(line);
}
return result;
}

function AnnotationMarkers({ annotations }: { annotations: IGACAnnotation[] }) {
const result = [];
for (const annotation of annotations) {
for (const line of annotationLines(annotation)) {
result[line - 1] = (
<div className="marker" key={line - 1}>
&nbsp;
</div>
);
}
}
for (let line = 0; line < result.length; line++) {
result[line] = result[line] ?? <div key={line}>&nbsp;</div>;
}
return <>{result}</>;
}

export function AnnotatedSource({ code, annotations }: IAnnotatedSourceProps) {
const codeBoxRef = useRef<HTMLDivElement>(null);
const markersRef = useRef<HTMLDivElement>(null);

const [activeAnnotation, setActiveAnnotation] = useState<IGACAnnotation | null>(null);

const highlightedCode = useMemo(() => <Highlight>{code}</Highlight>, [code]);
const annotatedLines = useMemo(() => {
const result = new Map<number, IGACAnnotation>();
for (const annotation of annotations) {
for (const line of annotationLines(annotation)) {
result.set(line, annotation);
}
}
return result;
}, annotations);

const [codeLineHeight, setCodeLineHeight] = useState(0);
const codeLineOffset = (line: number) => (line - 1) * codeLineHeight;
useLayoutEffect(() => {
const measure = () =>
setCodeLineHeight(
markersRef.current?.firstElementChild?.getBoundingClientRect()?.height || 0,
);
measure();
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, [markersRef.current]);

const handleMouseOver = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const codeBox = codeBoxRef.current;
if (!codeBox) {
return;
}
const { target } = e;
if (target instanceof HTMLElement && target.closest('.annotation-info')) {
return;
}
if (codeLineHeight) {
const line = Math.floor((e.pageY - codeBox.offsetTop) / codeLineHeight) + 1;
setActiveAnnotation(annotatedLines.get(line) ?? null);
}
};

const annotationOffset = codeLineOffset(activeAnnotation?.line ?? 1);
return (
<div
className="code-box"
onMouseMoveCapture={handleMouseOver}
onMouseLeave={() => setActiveAnnotation(null)}
ref={codeBoxRef}
>
<div
className="highlight"
style={{
width: activeAnnotation ? '100%' : 0,
visibility: activeAnnotation ? 'visible' : 'hidden',
top: annotationOffset,
height: codeLineOffset((activeAnnotation?.endLine ?? 1) + 1) - annotationOffset,
}}
>
&nbsp;
</div>
<div
className={`mask ${activeAnnotation ? 'mask-active' : ''}`}
style={{
height: annotationOffset,
}}
/>
<div
className={`mask ${activeAnnotation ? 'mask-active' : ''}`}
style={{
top: codeLineOffset((activeAnnotation?.endLine ?? 1) + 1),
bottom: 0,
}}
/>
<div className="annotation-markers" ref={markersRef}>
<AnnotationMarkers annotations={annotations} />
</div>
{highlightedCode}
{activeAnnotation && (
<div
className="annotation-info"
style={{
top: codeLineOffset(activeAnnotation.endLine + 1),
}}
>
<ReactMarkdown
source={activeAnnotation.value}
linkTarget={(url) => (url.startsWith('#') ? '' : '_blank')}
/>
</div>
)}
<style jsx>{`
.code-box {
overflow: auto;
line-height: 1.5;
position: relative;
display: flex;
}
.code-box > :global(pre) {
margin-top: 0;
}
.code-box > :global(pre > code) {
background: transparent;
padding: 0;
}

.annotation-markers {
width: 8px;
}

:global(.marker) {
position: relative;
width: 4px;
background: #00ffc3;
}

.highlight {
position: absolute;
background: #00ffc3;
width: 100%;
transition: width 0.7s;
z-index: -1;
}

.mask {
opacity: 0;
position: absolute;
background: white;
width: 100%;
top: 0;
pointer-events: none;
}
.mask.mask-active {
opacity: 0.6;
transition: opacity 1s;
}

.annotation-info {
position: absolute;
background: white;
padding: 0.5em;
border: solid black 1px;
width: 100%;
}

.annotation-info :global(p:first-child) {
margin-top: 0;
}
.annotation-info :global(p:last-child) {
margin-bottom: 0;
}
`}</style>
</div>
);
}
71 changes: 60 additions & 11 deletions content/simon/sketch.ino
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,70 @@

Copyright (C) 2016, Uri Shaked

Licensed under the MIT License.
Released under the MIT License.
*/

/* gac: `pitches.h` defines constants for the different music notes */
#include "pitches.h"

/* Constants - define pin numbers for LEDs,
buttons and speaker, and also the game tones: */
char ledPins[] = {9, 10, 11, 12};
char buttonPins[] = {2, 3, 4, 5};
/* gac:start
✅ Define pin numbers as constants at the beginning of the program
*/
const byte ledPins[] = {9, 10, 11, 12};
const byte buttonPins[] = {2, 3, 4, 5};
#define SPEAKER_PIN 8
/* gac:end */

/* gac:
✅ Defining the max game length as a constant makes it clear
what this "magic" number controls. */
#define MAX_GAME_LENGTH 100

int gameTones[] = { NOTE_G3, NOTE_C4, NOTE_E4, NOTE_G5};
/* gac: The tone for each button is stored in an array,
so we can easily match each LED, button and their corresponding
tone by looking at a specific index of the `ledPins`, `buttonPins`,
and `gameTones` arrays.
*/
const int gameTones[] = { NOTE_G3, NOTE_C4, NOTE_E4, NOTE_G5};

/* Global variales - store the game state */
/* gac:start
✅ It's a good pratice to initialize all variables to a default value when
delcaring them.

We use `gameSequence` to keep track of the current color sequence
that the user has to repeat, and `gameIndex` tells us how long the
sequence is, so we know how many cells of the array we should look at.
*/
byte gameSequence[MAX_GAME_LENGTH] = {0};
byte gameIndex = 0;
/* gac:end */

/**
Set up the Arduino board and initialize Serial communication
*/
void setup() {
Serial.begin(9600);
for (int i = 0; i < 4; i++) {
/* gac:start
Defining the pins for the LEDs and Buttons in an array allows
us to initialize them using a `for` loop, instead of calling
the `pinMode()` function for each individual button / LED.
*/
for (byte i = 0; i < 4; i++) {
pinMode(ledPins[i], OUTPUT);
pinMode(buttonPins[i], INPUT_PULLUP);
}
/* gac:end */
pinMode(SPEAKER_PIN, OUTPUT);
// The following line primes the random number generator.
// It assumes pin A0 is floating (disconnected):
/* gac: In arduino, the `random()` function will return the sequence
of numbers every time you start your program, unless you call
`randomSeed()` with a different value each time. To learn more about
this trick, check out the ["Making random() more Random"
video](https://www.youtube.com/watch?v=FwnXqZB2eo8). */
randomSeed(analogRead(A0));
}

Expand All @@ -53,7 +86,7 @@ void lightLedAndPlaySound(byte ledIndex) {
*/
void playSequence() {
for (int i = 0; i < gameIndex; i++) {
char currentLed = gameSequence[i];
byte currentLed = gameSequence[i];
lightLedAndPlaySound(currentLed);
delay(50);
}
Expand All @@ -64,8 +97,8 @@ void playSequence() {
and returns the index of that button
*/
byte readButton() {
for (;;) {
for (int i = 0; i < 4; i++) {
while (true) {
for (byte i = 0; i < 4; i++) {
byte buttonPin = buttonPins[i];
if (digitalRead(buttonPin) == LOW) {
return i;
Expand All @@ -83,14 +116,15 @@ void gameOver() {
Serial.println(gameIndex - 1);
gameIndex = 0;
delay(200);

// Play a Wah-Wah-Wah-Wah sound
tone(SPEAKER_PIN, NOTE_DS5);
delay(300);
tone(SPEAKER_PIN, NOTE_D5);
delay(300);
tone(SPEAKER_PIN, NOTE_CS5);
delay(300);
for (int i = 0; i < 200; i++) {
for (byte i = 0; i < 200; i++) {
tone(SPEAKER_PIN, NOTE_C5 + (i % 20 - 10));
delay(5);
}
Expand All @@ -104,8 +138,8 @@ void gameOver() {
*/
void checkUserSequence() {
for (int i = 0; i < gameIndex; i++) {
char expectedButton = gameSequence[i];
char actualButton = readButton();
byte expectedButton = gameSequence[i];
byte actualButton = readButton();
lightLedAndPlaySound(actualButton);
if (expectedButton == actualButton) {
/* good */
Expand Down Expand Up @@ -142,6 +176,21 @@ void loop() {
// Add a random color to the end of the sequence
gameSequence[gameIndex] = random(0, 4);
gameIndex++;
/* gac:start
✅ Array boundary check

The C++ language does not protect us from accessing data beyond the
end of the array. Therefore, our code must always check that we are
still within the array boundaries. Otherwise, we'll get unexpected
behavior and the program may crash.

In this case, we make sure that `gameIndex` always stays below
`MAX_GAME_LENGTH`, which is the size of the `gameSequence` array.
*/
if (gameIndex >= MAX_GAME_LENGTH) {
gameIndex = MAX_GAME_LENGTH - 1;
}
/* gac:end */

playSequence();
checkUserSequence();
Expand Down
4 changes: 3 additions & 1 deletion pages/api/download-project/[project].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { promisify } from 'util';
import ZipStream from 'zip-stream';
import { getProjectCode } from '../../../services/projects';
import { extractCodeAnnotations } from '../../../services/gac-annotations';

export default async (req: NextApiRequest, res: NextApiResponse) => {
const {
Expand All @@ -17,7 +18,8 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {

zip.pipe(res);
for (const file of await getProjectCode(projectId)) {
await promisify(zip.entry).call(zip, file.code, { name: file.name });
const { code } = extractCodeAnnotations(file.code);
await promisify(zip.entry).call(zip, code, { name: file.name });
}
zip.finalize();
};
Loading