Skip to content
Open
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
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
APP_DOMAIN='localhost:8888'
APP_CONTEXT='/'
APP_META_TITLE='Random Name Picker for Lucky Draw'
APP_META_TITLE='MAKE IT POSSIBLE - Amadeus Vietnam'
APP_META_DESCRIPTION='Simple HTML5 random name picker for picking lucky draw winner using Web Animations and AudioContext API.'
APP_META_KEYWORDS='lucky draw, lucky draw online, lucky draw app, random name picker, name picker'
APP_GOOGLE_TAG_MANAGER_ID='GTM-54B6D4G'
35 changes: 1 addition & 34 deletions logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@
"husky": "^8.0.1",
"lint-staged": "^15.2.0",
"mini-css-extract-plugin": "^2.7.7",
"node-sass": "^9.0.0",
"postcss-loader": "^8.0.0",
"prettier": "^3.2.4",
"pug": "^3.0.2",
"pug-html-loader": "^1.1.5",
"sass": "^1.93.3",
"sass-loader": "^14.0.0",
"style-loader": "^3.3.4",
"ts-loader": "^9.5.1",
Expand Down
Binary file modified src/assets/images/favicon.ico
Binary file not shown.
89 changes: 1 addition & 88 deletions src/assets/images/light-blubs.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 1 addition & 150 deletions src/assets/images/sunburst.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 1 addition & 148 deletions src/assets/images/title.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 41 additions & 31 deletions src/assets/js/SoundEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface SoundSeries {

/** Class for playing sound effects via AudioContext */
export default class SoundEffects {
/** Audio context instancce */
/** Audio context instance */
private audioContext?: AudioContext;

/** Indicator for whether this sound effect instance is muted */
Expand All @@ -28,7 +28,6 @@ export default class SoundEffects {
if (window.AudioContext || window.webkitAudioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}

this.isMuted = isMuted;
}

Expand All @@ -44,12 +43,15 @@ export default class SoundEffects {

/**
* Play a sound by providing a list of keys and duration
* @param sound Series of piano keys and it's durarion to play
* @param sound Series of piano keys and its duration to play
* @param config.type Oscillator type
* @param config.easeOut Whether to ease out to 1% during last 100ms
* @param config.volume Volume of the sound to play, value should be between 0.1 and 1
*/
private playSound(sound: SoundSeries[], { type = 'sine', easeOut: shouldEaseOut = true, volume = 0.1 }: SoundConfig = {}): void {
private playSound(
sound: SoundSeries[],
{ type = 'sine', easeOut: shouldEaseOut = true, volume = 0.1 }: SoundConfig = {}
): void {
const { audioContext } = this;

// graceful exit for browsers that don't support AudioContext
Expand All @@ -64,7 +66,7 @@ export default class SoundEffects {
gainNode.connect(audioContext.destination);

oscillator.type = type;
gainNode.gain.value = volume; // set default volume to 10%
gainNode.gain.value = volume; // default volume

const { currentTime: audioCurrentTime } = audioContext;

Expand All @@ -84,58 +86,66 @@ export default class SoundEffects {
}

/**
* Play the winning sound effect
* Play spinning sound effect for N seconds (Spy-style scanning tone)
* @param durationInSecond Duration of sound effect in seconds
* @returns Has sound effect been played
*/
public win(): Promise<boolean> {
public spin(durationInSecond: number): Promise<boolean> {
if (this.isMuted) {
return Promise.resolve(false);
}

const musicNotes: SoundSeries[] = [
{ key: 'C4', duration: 0.175 },
{ key: 'D4', duration: 0.175 },
{ key: 'E4', duration: 0.175 },
{ key: 'G4', duration: 0.275 },
{ key: 'E4', duration: 0.15 },
{ key: 'G4', duration: 0.9 }
// Tạo cảm giác "radar quét" bằng âm dao động giữa thấp và cao nhẹ
const pulseNotes: SoundSeries[] = [
{ key: 'A2', duration: 0.1 },
{ key: 'C3', duration: 0.1 },
{ key: 'E3', duration: 0.1 },
{ key: 'C3', duration: 0.1 }
];
const totalDuration = musicNotes
.reduce((currentNoteTime, { duration }) => currentNoteTime + duration, 0);

this.playSound(musicNotes, { type: 'triangle', volume: 1, easeOut: true });
const totalDuration = pulseNotes.reduce((sum, { duration }) => sum + duration, 0);
const repeatCount = Math.floor(durationInSecond / totalDuration) * pulseNotes.length;

this.playSound(
Array.from({ length: repeatCount }, (_, i) => pulseNotes[i % pulseNotes.length]),
{
type: 'triangle', // âm mềm, cảm giác “scan”
easeOut: false,
volume: 0.3
}
);

return new Promise<boolean>((resolve) => {
setTimeout(() => {
resolve(true);
}, totalDuration * 1000);
}, durationInSecond * 1000);
});
}

/**
* Play spinning sound effect for N seconds
* @param durationInSecond Duration of sound effect in seconds
* Play the winning sound effect (Spy-style "mission confirmed" tone)
* @returns Has sound effect been played
*/
public spin(durationInSecond: number): Promise<boolean> {
public win(): Promise<boolean> {
if (this.isMuted) {
return Promise.resolve(false);
}

// Âm "mission success" — nhẹ, ấm và bí ẩn
const musicNotes: SoundSeries[] = [
{ key: 'D#3', duration: 0.1 },
{ key: 'C#3', duration: 0.1 },
{ key: 'C3', duration: 0.1 }
{ key: 'C4', duration: 0.18 },
{ key: 'E4', duration: 0.18 },
{ key: 'G4', duration: 0.25 },
{ key: 'C5', duration: 0.45 }
];

const totalDuration = musicNotes
.reduce((currentNoteTime, { duration }) => currentNoteTime + duration, 0);
const totalDuration = musicNotes.reduce((sum, { duration }) => sum + duration, 0);

const duration = Math.floor(durationInSecond * 10);
this.playSound(
Array.from(Array(duration), (_, index) => musicNotes[index % 3]),
{ type: 'triangle', easeOut: false, volume: 2 }
);
this.playSound(musicNotes, {
type: 'sine', // âm mượt, không chói
easeOut: true,
volume: 0.35
});

return new Promise<boolean>((resolve) => {
setTimeout(() => {
Expand Down
8 changes: 8 additions & 0 deletions src/assets/js/random-name-picker.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "../../.."
}
],
"settings": {}
}
84 changes: 47 additions & 37 deletions src/assets/scss/_colors.scss
Original file line number Diff line number Diff line change
@@ -1,50 +1,60 @@
$color-white: #ffffff;
$color-light-grey: #e8e8e8;
$color-mid-grey: #999999;
$color-dark-grey: #666666;
// ===== SPY THEME — Amadeus 20 Years Mission Control (Balanced) =====

// base colors — nền dịu, dễ nhìn hơn
$color-white: #ecf6ff;
$color-light-grey: #1a2226;
$color-mid-grey: #2a3338;
$color-dark-grey: #101518;
$color-black: #000000;

$color-red-step1: #ff462d;
$color-red-step2: #ff422f;
$color-red-step3: #ff3634;
$color-red-step4: #ff223c;
$color-red-step5: #ff0c45;
// primary palette — cyan giảm độ sáng, tông “tech blue”
$color-red-step1: #273fa8; // sáng nhất, xanh lam tươi
$color-red-step2: #092f66; // lam đậm trung bình
$color-red-step3: #051a43; // lam navy vừa
$color-red-step4: #13226f; // lam navy đậm
$color-red-step5: #000835; // lam đậm nhất (theo yêu cầu)

$color-yellow: #ffbf1f;
$color-blood-orange: #e54c23;
$color-shadow: rgba($color-black, 0.2);
// accent & alert
$color-yellow: #3fe0ff; // accent chính: cyan dịu
$color-blood-orange: #ff445a; // cảnh báo đỏ nhẹ
$color-shadow: rgba($color-black, 0.65);

// body
$color-text-light: $color-white;
$color-text-dark: $color-black;
$color-link: rgba($color-white, 0.8);
$color-link-hover: rgba($color-white, 0.9);
$color-text-dark: #b9cbd3;
$color-link: rgba($color-yellow, 0.85);
$color-link-hover: rgba($color-yellow, 1);

// title
$color-title-border: $color-yellow;

// button
$color-button-text: $color-white;
$color-button-default-background: $color-yellow;
$color-button-default-hover-background: lighten($color-yellow, 5%);
$color-button-default-background: $color-red-step4;
$color-button-default-hover-background: lighten($color-red-step4, 7%);
$color-button-danger-background: $color-blood-orange;
$color-button-danger-hover-background: lighten($color-blood-orange, 5%);

// slot
$color-slot-background: $color-yellow;
$color-slot-shadow: $color-shadow;
$color-slot-inner-background: $color-white;
$color-slot-text: $color-black;

// settings
$color-settings-mask: rgba($color-black, 0.6);
$color-settings-background: darken($color-red-step5, 8%);

// textarea
$color-input-background: $color-light-grey;
$color-input-placeholder: $color-mid-grey;
$color-input-text: $color-black;

// switch
$color-switch-handle-disabled: $color-dark-grey;
$color-switch-handle: darken($color-red-step5, 8%);
$color-button-danger-hover-background: lighten($color-blood-orange, 7%);

// slot (name box)
$color-slot-background: rgba($color-red-step5, 0.35);
$color-slot-shadow: rgba($color-red-step2, 0.3);
$color-slot-inner-background: rgba($color-black, 0.82);
$color-slot-text: $color-red-step1;

// settings panel
$color-settings-mask: rgba($color-black, 0.8);
$color-settings-background: darken($color-dark-grey, 3%);

// textarea / inputs
$color-input-background: #0d1316;
$color-input-placeholder: #718c97;
$color-input-text: $color-white;

// switch / toggles
$color-switch-handle-disabled: #60686d;
$color-switch-handle: $color-red-step3;

// ===== nền tổng thể dịu (thay vì đen tuyệt đối) =====
body {
background: radial-gradient(circle at 30% 25%, #142227 0%, #050708 90%);
}
Loading