From b89d0a29131dbc3e089401a0fc335cf1213f30e6 Mon Sep 17 00:00:00 2001 From: APRIASZ Date: Wed, 17 Sep 2025 16:03:29 +0200 Subject: [PATCH 1/2] creating new stain effect --- src/Components/Tracks/updatedTracks.tsx | 18 +-- src/Helpers/Blocks/pianoInteraction.ts | 7 +- src/Helpers/Blocks/updatedBlocks.ts | 2 +- src/Helpers/Effects/DesignedEffects/Stain.ts | 152 +++++++++++++++++++ src/Helpers/Effects/Effects.ts | 4 + src/Helpers/Effects/EffectsManager.ts | 4 +- 6 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 src/Helpers/Effects/DesignedEffects/Stain.ts diff --git a/src/Components/Tracks/updatedTracks.tsx b/src/Components/Tracks/updatedTracks.tsx index 145b1cb..802c7d6 100644 --- a/src/Components/Tracks/updatedTracks.tsx +++ b/src/Components/Tracks/updatedTracks.tsx @@ -41,7 +41,7 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys}: //TODO: IMPLEMENT THIS INTO OPTIONS // const TRACKS_CONFIGURATION:TRACKS_CONF_TYPE = { - piano_height_ratio: 1/5, + piano_height_ratio: 1/1.5, key_wh_to_bl_ratio: 2/3 } @@ -55,15 +55,6 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys}: } },[blocks]) - /** Cancel animation frame when component is deleted */ - useEffect(()=>{ - return () => { - if(animation_frame.current !== 0){ - window.cancelAnimationFrame(animation_frame.current); - } - } - },[]) - /** * main animation frame, renders blocks. */ @@ -100,6 +91,7 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys}: },[mainCtx.current, blocks, pianoBlack.current, EffectCtx.current]) //Here set up all functions/handlers which require blocks to exist + //Also setup clearing the animationFrame when page closes useEffect(()=>{ if(blocks !== undefined){ Player.setEventHandler(add_event) @@ -109,6 +101,12 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys}: } main_animation_frame(); } + + return () => { + if(animation_frame.current !== 0){ + window.cancelAnimationFrame(animation_frame.current); + } + } },[blocks]) return( diff --git a/src/Helpers/Blocks/pianoInteraction.ts b/src/Helpers/Blocks/pianoInteraction.ts index 14ec518..ed3c7ab 100644 --- a/src/Helpers/Blocks/pianoInteraction.ts +++ b/src/Helpers/Blocks/pianoInteraction.ts @@ -1,8 +1,6 @@ //TODO: Add option for Key Gradient to be like fire: https://codepen.io/Capse/pen/aNOeee // Current RadialGradient is very basic and unatractive // Also, that radial gradient could be prerendered and reused -//TODO: Combine white_ctx and black_ctx into one single canvas (goal - reduce .clearRect function as much as possible) -//TODO: Combine radial gradient with piano lighting up - generate offsets for canvas... (goal - reduce .clearRect function as much as possible) import { keyInfo } from "../../Utils/TypesForMidi"; import { CanvasRoundRect, addShadow } from '../../Utils/CanvasFuntions'; @@ -69,7 +67,6 @@ export default class pianoInteraction{ */ public render():void { const HEIGHT_OFFSET = this.height - this.TR_CONF.piano_height_ratio * this.height; - //!! TO MANY CLEAR RECTS this.main_ctx.clearRect(0,HEIGHT_OFFSET,this.width, this.white_key_height); //Clears only the bottom part of the screen, as upper was cleared earlier this.black_ctx.clearRect(0,0,this.width, this.black_key_height ); //Here I don't know yet how to proceed, but It will stay like this this.main_ctx.shadowBlur = 0; @@ -78,10 +75,10 @@ export default class pianoInteraction{ const height = key.type === 'BLACK' ? this.black_key_height : this.white_key_height if(key.type === "BLACK"){ CanvasRoundRect(this.black_ctx,key.color,key.position + 1,0 - 2,key.width,height,3); - addShadow(this.main_ctx,key.position + 1,HEIGHT_OFFSET,height - 5, key.width); + //addShadow(this.main_ctx,key.position + 1,HEIGHT_OFFSET,height - 5, key.width); //This shadow makes almost no change, but still takes computing time }else{ CanvasRoundRect(this.main_ctx,key.color,key.position,HEIGHT_OFFSET,key.width,height,3); - addShadow(this.main_ctx,key.position,HEIGHT_OFFSET,height, key.width); + addShadow(this.main_ctx,key.position,HEIGHT_OFFSET,height, key.width); //Better to have gradient defined for keys (as it is always the same), and just fill rect } this.gradient.generateGradient(key.position + key.width/2, key.width * 1.5); this.gradient.updateGradient(); diff --git a/src/Helpers/Blocks/updatedBlocks.ts b/src/Helpers/Blocks/updatedBlocks.ts index 7e56407..1c831bb 100644 --- a/src/Helpers/Blocks/updatedBlocks.ts +++ b/src/Helpers/Blocks/updatedBlocks.ts @@ -149,7 +149,7 @@ class Blocks{ this.key_positions_map = this.__create_key_position_map(width,nr_of_keys,key_width); this.positions_to_render_line = this.RenderOctaveLines() this.key_interactor = new pianoInteraction(canvases.blackKeyCtx,canvases.mainCtx,this.width,height,TR_CONF,options); - this.effect_manager = new EffectsManager(canvases.effectsCtx, width, height - (height / 5), options.Effect, this.options); + this.effect_manager = new EffectsManager(canvases.effectsCtx, width, this.height, options.Effect, this.options); // //Binding // diff --git a/src/Helpers/Effects/DesignedEffects/Stain.ts b/src/Helpers/Effects/DesignedEffects/Stain.ts new file mode 100644 index 0000000..b3d2a2f --- /dev/null +++ b/src/Helpers/Effects/DesignedEffects/Stain.ts @@ -0,0 +1,152 @@ +import { Options } from "../../../Utils/TypesForOptions"; +import Effect from "../_Effect"; + +interface Stain_Options{ + max_width:number, + max_height:number, + offset_margin:number, + speed:number, +} + +export type { Stain_Options }; + +interface path_element{ + x0:number, + y0:number, + x1:number, + y1:number, + r:number +} + +class Stain_Entity{ + private reached_destination:boolean = false; + private end_x:number + private end_y:number + private render_next:number = 0 + public static DEFAULT_CONF:Stain_Options = { + max_height:20, + max_width:20, + offset_margin:10, + speed:1 + } + private path_element:path_element[]; + + constructor(start_x:number, start_y:number, private color:string, private configuration:Stain_Options ){ + this.end_x = start_x + Math.random() * configuration.offset_margin * (Math.random() > 0.5 ? 1 : -1) - 200; + this.end_y = start_y - Math.random() * configuration.offset_margin - 100; + this.path_element = this._generate_shape(); + } + + public update(){ + + } + + public render(ctx:CanvasRenderingContext2D){ + ctx.beginPath(); + this.path_element.forEach((el,index) =>{ + if(index * 30 <= this.render_next){ + ctx.moveTo(el.x0,el.y0) + ctx.arcTo((el.x0 + el.x1)/2,(el.y0 + el.y1)/2, el.x1, el.y1, el.r); + ctx.lineTo(el.x1,el.y1); + } + this.render_next+=1; + }) + ctx.fillStyle = this.color; + ctx.strokeStyle = this.color; + ctx.fill(); + } + + private _reach_destination():void{ + + } + + private _generate_shape():Array{ + let width = Math.random() * this.configuration.max_width; + let height = Math.random() * this.configuration.max_height; + const turns_per_horizon = 3; + let start_x = this.end_x; + let start_y = this.end_y; + let forward = 1; + let dir = (Math.random() > 0.5 ? 1 : -1) + const radius = 5; + const path_arr:path_element[] = []; + for(;forward>=-1;forward-=2){ + for(let x = 0; x < turns_per_horizon; x++){ + dir *= -1; + const dest_x = start_x + width/turns_per_horizon * (x+1) * forward; + const dest_y = start_y + (Math.random() * this.configuration.max_height/2) * dir; + const path:path_element = { + x0: start_x, + y0: start_y, + x1: dest_x, + y1: dest_y, + r: radius + } + start_x = dest_x; + start_y = dest_y; + path_arr.push(path); + } + for(let y = 0; y < turns_per_horizon; y++){ + dir *=-1; + const dest_x = start_x + (Math.random() * this.configuration.max_width/2) * dir; + const dest_y = start_y + height/turns_per_horizon * (y+1) * forward; + const path:path_element = { + x0: start_x, + y0: start_y, + x1: dest_x, + y1: dest_y, + r: radius + } + start_x = dest_x; + start_y = dest_y; + path_arr.push(path); + } + width = Math.abs(start_x - this.end_x) + height = Math.abs(start_y - this.end_y) + } + const path:path_element = { + x0: start_x, + y0: start_y, + x1: this.end_x, + y1: this.end_y, + r: radius + } + path_arr.push(path); + console.log(path_arr); + return path_arr; + } +} + + +export default class Stain extends Effect{ + private entities:Stain_Entity[] = []; + private rendered:boolean = false; + + constructor(ctx: CanvasRenderingContext2D, width:number, height:number,private options: Options){ + super(ctx, width, height); + } + + public create_effect(pos_x: number, pos_y: number, key_width: number): void { + if(!this.rendered) + this.entities.push(new Stain_Entity(pos_x,pos_y, this.options.Color, Stain_Entity.DEFAULT_CONF)) + this.rendered = true; + } + + public update_effect(): void { + + } + + public render_effect(): void { + const vanish_speed = 0.02 + this.ctx.fillStyle = 'rgba(42,44,46,' + vanish_speed.toString() + ')'; + this.ctx.fillRect(0,0,this.width,this.height); + this.entities.forEach(entity =>{ + entity.render(this.ctx); + }) + } + + public handle_resze(width: number, height: number): void { + this.width = width; + this.height = height; + } +} \ No newline at end of file diff --git a/src/Helpers/Effects/Effects.ts b/src/Helpers/Effects/Effects.ts index 85ab9e9..403c805 100644 --- a/src/Helpers/Effects/Effects.ts +++ b/src/Helpers/Effects/Effects.ts @@ -1,3 +1,7 @@ +/** + * @deprecated File deprecated + */ + import { Options as OptionsType } from "../../Utils/TypesForOptions"; import {Fountain, DancingLines, HexagonEffect, StickyBalls, Firework,Sparks, EmptyEffect,DNA} from '../CanvasEffects'; diff --git a/src/Helpers/Effects/EffectsManager.ts b/src/Helpers/Effects/EffectsManager.ts index 74cb133..d7e896c 100644 --- a/src/Helpers/Effects/EffectsManager.ts +++ b/src/Helpers/Effects/EffectsManager.ts @@ -6,6 +6,7 @@ import { Options as OptionsType } from '../../Utils/TypesForOptions'; */ import Sparks from './DesignedEffects/Sparks'; import Squared from './DesignedEffects/Squared'; +import Stain from './DesignedEffects/Stain'; import Blank from './DesignedEffects/Blank'; /** @@ -35,7 +36,8 @@ export default class EffectsManager{ this.effects = new Squared(ctx,width,height,2.5,3,[options.Color,options.ThinerBlockColor],0.05); break; case "None": - this.effects = new Blank(ctx, 0, 0) + //this.effects = new Blank(ctx, 0, 0); + this.effects = new Stain(ctx,width,height, options); break default: this.effects = new Blank(ctx, 0, 0); From 84454e8b3ed50fbf0d1ce40f6f26e7b6625b6f92 Mon Sep 17 00:00:00 2001 From: APRIASZ Date: Fri, 19 Sep 2025 17:54:28 +0200 Subject: [PATCH 2/2] implemented firework effect --- .../NewOptions/OptionsType/OptionsType.tsx | 7 + src/Components/Tracks/updatedTracks.tsx | 2 +- src/Helpers/Effects/DesignedEffects/Stain.ts | 197 +++++++++++------- src/Helpers/Effects/EffectsManager.ts | 6 +- src/Utils/TypesForOptions.ts | 2 +- 5 files changed, 129 insertions(+), 85 deletions(-) diff --git a/src/Components/NewOptions/OptionsType/OptionsType.tsx b/src/Components/NewOptions/OptionsType/OptionsType.tsx index a08f3da..a7b0bee 100644 --- a/src/Components/NewOptions/OptionsType/OptionsType.tsx +++ b/src/Components/NewOptions/OptionsType/OptionsType.tsx @@ -98,6 +98,13 @@ function Options_Effects({isOpened,onGoBack,options,handleOptionsChange}:Options
  • Performance indicator: Good
  • + +
      +
    • Colorfull fireworks flying from keys when played
    • +
    • Warning: Background Image does not work with this effect
    • +
    • Performance indicator: Mid
    • +
    +
    ) diff --git a/src/Components/Tracks/updatedTracks.tsx b/src/Components/Tracks/updatedTracks.tsx index 802c7d6..49d9980 100644 --- a/src/Components/Tracks/updatedTracks.tsx +++ b/src/Components/Tracks/updatedTracks.tsx @@ -41,7 +41,7 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys}: //TODO: IMPLEMENT THIS INTO OPTIONS // const TRACKS_CONFIGURATION:TRACKS_CONF_TYPE = { - piano_height_ratio: 1/1.5, + piano_height_ratio: 1/5, key_wh_to_bl_ratio: 2/3 } diff --git a/src/Helpers/Effects/DesignedEffects/Stain.ts b/src/Helpers/Effects/DesignedEffects/Stain.ts index b3d2a2f..607cfc2 100644 --- a/src/Helpers/Effects/DesignedEffects/Stain.ts +++ b/src/Helpers/Effects/DesignedEffects/Stain.ts @@ -6,54 +6,104 @@ interface Stain_Options{ max_height:number, offset_margin:number, speed:number, + max_radius:number, + bounce:number, } export type { Stain_Options }; interface path_element{ - x0:number, - y0:number, - x1:number, - y1:number, - r:number + pos_x:number, + pos_y:number, + radius:number, + opacity:number, + falling_speed:number, + opacity_fade_speed:number, + x_movement:number, + sin_opacity:number, + sin_opacity_nominator:number, + color:string //In HEX format } class Stain_Entity{ - private reached_destination:boolean = false; + private gravitation:number = 0.08 private end_x:number private end_y:number + private create_time:number = Date.now() + private speed_x:number = (Math.random() > 0.5 ? 1 : -1) * Math.random() * 2 + 0.5 + private speed_y:number = Math.random() * 7.5 + 8.5 * -1 + private stop_time:number = Math.random()*1500 + 1000 + private initial_radius:number = Math.random() * this.configuration.max_radius + 0.5 private render_next:number = 0 + private TT_MARGIN = 15 public static DEFAULT_CONF:Stain_Options = { - max_height:20, - max_width:20, + max_height:50, + max_width:90, offset_margin:10, - speed:1 + max_radius:4, + speed:1, + bounce: 0.9 } private path_element:path_element[]; - constructor(start_x:number, start_y:number, private color:string, private configuration:Stain_Options ){ - this.end_x = start_x + Math.random() * configuration.offset_margin * (Math.random() > 0.5 ? 1 : -1) - 200; - this.end_y = start_y - Math.random() * configuration.offset_margin - 100; + constructor(start_x:number, start_y:number, private color:string[], private configuration:Stain_Options, private height:number ){ + this.end_x = start_x; + this.end_y = start_y; this.path_element = this._generate_shape(); } - public update(){ - + public update():boolean{ + if(Date.now() - this.create_time >= this.stop_time ){ + + let is_each_zero = false; + this.path_element.forEach((el,index) =>{ + el.sin_opacity+=el.sin_opacity_nominator; + el.color = el.color.slice(0,7) + (Math.abs(Math.floor(el.opacity * Math.cos(el.sin_opacity))) < 16 ? '0' : '') + Math.abs(Math.floor(el.opacity * Math.cos(el.sin_opacity))).toString(16) + el.pos_y += el.falling_speed; + el.pos_x += el.x_movement; + el.x_movement -= this.gravitation * (el.x_movement < 0 ? -1 : 1); + el.falling_speed += this.gravitation; + if(index * this.TT_MARGIN <= this.render_next){ + el.opacity -= el.opacity_fade_speed; + } + if(el.opacity > 0){ + is_each_zero = true; + } + }) + return is_each_zero; + }//If yet not the explosion time + this.end_x += this.speed_x; + this.end_y += this.speed_y; + this.speed_y += this.gravitation; + this.path_element.forEach(el =>{ + el.pos_x = this.end_x; + el.pos_y = this.end_y; + }) + return true; } public render(ctx:CanvasRenderingContext2D){ - ctx.beginPath(); - this.path_element.forEach((el,index) =>{ - if(index * 30 <= this.render_next){ - ctx.moveTo(el.x0,el.y0) - ctx.arcTo((el.x0 + el.x1)/2,(el.y0 + el.y1)/2, el.x1, el.y1, el.r); - ctx.lineTo(el.x1,el.y1); - } - this.render_next+=1; - }) - ctx.fillStyle = this.color; - ctx.strokeStyle = this.color; - ctx.fill(); + if(Date.now() - this.create_time >= this.stop_time){ + this.path_element.forEach((el,index) =>{ + if(index * this.TT_MARGIN <= this.render_next && el.opacity > 0){ + ctx.beginPath(); + ctx.fillStyle = el.color; + ctx.arc(el.pos_x,el.pos_y,el.radius,0,Math.PI * 2); + ctx.fill(); + } + this.render_next+=1; + }) + return; + }//else + const TRAIL_SIZE = 15; + for(let x = 0; x < TRAIL_SIZE; x++){ + const pos_x = this.end_x - this.speed_x*(x+1) + const pos_y = this.end_y - this.speed_y*(x+1) + ctx.beginPath(); + ctx.fillStyle = this.color[0] + Math.abs(Math.floor(255/ (0.5*(x+2)))).toString(16) + ctx.arc(pos_x, pos_y, this.initial_radius / (0.20*(x+5)), 0, Math.PI *2); + ctx.fill(); + } } private _reach_destination():void{ @@ -61,58 +111,28 @@ class Stain_Entity{ } private _generate_shape():Array{ - let width = Math.random() * this.configuration.max_width; - let height = Math.random() * this.configuration.max_height; - const turns_per_horizon = 3; - let start_x = this.end_x; - let start_y = this.end_y; - let forward = 1; - let dir = (Math.random() > 0.5 ? 1 : -1) - const radius = 5; + const DOTS_PER_ENTITY = 30; + const ENTITY_RADIUS = this.initial_radius; + const CENTER_X = this.end_x; + const CENTER_Y = this.end_y; const path_arr:path_element[] = []; - for(;forward>=-1;forward-=2){ - for(let x = 0; x < turns_per_horizon; x++){ - dir *= -1; - const dest_x = start_x + width/turns_per_horizon * (x+1) * forward; - const dest_y = start_y + (Math.random() * this.configuration.max_height/2) * dir; - const path:path_element = { - x0: start_x, - y0: start_y, - x1: dest_x, - y1: dest_y, - r: radius - } - start_x = dest_x; - start_y = dest_y; - path_arr.push(path); + for(let x = 0; x < DOTS_PER_ENTITY; x++){ + const DENOM_X = Math.random() > 0.5 ? 1 : -1; + const DENOM_Y = Math.random() > 0.5 ? 1 : -1; + const element:path_element = { + opacity:Math.floor(204 * Math.random() + 50), + pos_x: CENTER_X, + pos_y: CENTER_Y, + radius: ENTITY_RADIUS * Math.random(), + falling_speed: Math.random() * 3.5 * ( Math.random() > 0.5 ? -1 : 0.2), + opacity_fade_speed: Math.random() * 3 + 1, + x_movement: Math.random() * 2.6 * (DENOM_X), + sin_opacity: DENOM_Y === -1 ? Math.random() * 90 : 0, + sin_opacity_nominator: DENOM_Y === -1 ? Math.random() * 0.1 + 0.2 : 0, + color: this.color[Math.floor(Math.random() * this.color.length)] } - for(let y = 0; y < turns_per_horizon; y++){ - dir *=-1; - const dest_x = start_x + (Math.random() * this.configuration.max_width/2) * dir; - const dest_y = start_y + height/turns_per_horizon * (y+1) * forward; - const path:path_element = { - x0: start_x, - y0: start_y, - x1: dest_x, - y1: dest_y, - r: radius - } - start_x = dest_x; - start_y = dest_y; - path_arr.push(path); - } - width = Math.abs(start_x - this.end_x) - height = Math.abs(start_y - this.end_y) + path_arr.push(element); } - const path:path_element = { - x0: start_x, - y0: start_y, - x1: this.end_x, - y1: this.end_y, - r: radius - } - path_arr.push(path); - console.log(path_arr); return path_arr; } } @@ -120,24 +140,39 @@ class Stain_Entity{ export default class Stain extends Effect{ private entities:Stain_Entity[] = []; - private rendered:boolean = false; + private occupied_pos:{id:number,time:number}[] = []; constructor(ctx: CanvasRenderingContext2D, width:number, height:number,private options: Options){ super(ctx, width, height); } public create_effect(pos_x: number, pos_y: number, key_width: number): void { - if(!this.rendered) - this.entities.push(new Stain_Entity(pos_x,pos_y, this.options.Color, Stain_Entity.DEFAULT_CONF)) - this.rendered = true; + const TIMESPAN = 750; + const time_now = Date.now(); + const el = this.occupied_pos.find(el => el.id === pos_x); + if(el){ + if(time_now - el.time > TIMESPAN){ + el.time = time_now; + this.entities.push(new Stain_Entity(pos_x + key_width/2,pos_y, [this.options.Color, this.options.ThinerBlockColor], Stain_Entity.DEFAULT_CONF, this.height)) + return; + } + return; + }//if element does not exist + this.occupied_pos.push({ + id:pos_x, + time:time_now + }) + this.entities.push(new Stain_Entity(pos_x + key_width/2,pos_y, [this.options.Color, this.options.ThinerBlockColor], Stain_Entity.DEFAULT_CONF, this.height)); + + } public update_effect(): void { - + this.entities = this.entities.filter(entity => entity.update()); } public render_effect(): void { - const vanish_speed = 0.02 + const vanish_speed = 1; this.ctx.fillStyle = 'rgba(42,44,46,' + vanish_speed.toString() + ')'; this.ctx.fillRect(0,0,this.width,this.height); this.entities.forEach(entity =>{ diff --git a/src/Helpers/Effects/EffectsManager.ts b/src/Helpers/Effects/EffectsManager.ts index d7e896c..1389551 100644 --- a/src/Helpers/Effects/EffectsManager.ts +++ b/src/Helpers/Effects/EffectsManager.ts @@ -35,9 +35,11 @@ export default class EffectsManager{ case 'Squared': this.effects = new Squared(ctx,width,height,2.5,3,[options.Color,options.ThinerBlockColor],0.05); break; - case "None": - //this.effects = new Blank(ctx, 0, 0); + case "Firework": this.effects = new Stain(ctx,width,height, options); + break; + case "None": + this.effects = new Blank(ctx, 0, 0); break default: this.effects = new Blank(ctx, 0, 0); diff --git a/src/Utils/TypesForOptions.ts b/src/Utils/TypesForOptions.ts index e446670..0afe411 100644 --- a/src/Utils/TypesForOptions.ts +++ b/src/Utils/TypesForOptions.ts @@ -17,7 +17,7 @@ export interface Options{ ShadowColor:string, EffectsColor:string, OctaveLines:boolean, - Effect:"Squares" | "Sparks" | "None", + Effect:"Squares" | "Sparks" | "Firework" | "None", ThinerBlockColor:string, }